diff --git a/HopFrame.sln.DotSettings.user b/HopFrame.sln.DotSettings.user index ae702c9..01d3df3 100644 --- a/HopFrame.sln.DotSettings.user +++ b/HopFrame.sln.DotSettings.user @@ -80,6 +80,7 @@ + \ No newline at end of file diff --git a/src/HopFrame.Api/Controller/OpenIdController.cs b/src/HopFrame.Api/Controller/OpenIdController.cs index 50e8822..733821d 100644 --- a/src/HopFrame.Api/Controller/OpenIdController.cs +++ b/src/HopFrame.Api/Controller/OpenIdController.cs @@ -1,20 +1,17 @@ using HopFrame.Api.Models; using HopFrame.Security.Authentication.OpenID; -using HopFrame.Security.Authentication.OpenID.Options; using HopFrame.Security.Claims; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; namespace HopFrame.Api.Controller; [ApiController, Route("api/v1/openid")] -public class OpenIdController(IOpenIdAccessor accessor, IOptions options) : ControllerBase { +public class OpenIdController(IOpenIdAccessor accessor) : ControllerBase { public const string DefaultCallback = "api/v1/openid/callback"; [HttpGet("redirect")] public async Task RedirectToProvider([FromQuery] string redirectAfter, [FromQuery] int performRedirect = 1) { - var uri = await accessor.ConstructAuthUri(DefaultCallback, redirectAfter); + var uri = await accessor.ConstructAuthUri(redirectAfter); if (performRedirect == 1) { return Redirect(uri); @@ -29,22 +26,13 @@ public class OpenIdController(IOpenIdAccessor accessor, IOptions return BadRequest("Authorization code is missing"); } - var token = await accessor.RequestToken(code, DefaultCallback); + var token = await accessor.RequestToken(code); if (token is null) { return Forbid("Authorization code is not valid"); } - Response.Cookies.Append(ITokenContext.AccessTokenType, token.AccessToken, new CookieOptions { - MaxAge = TimeSpan.FromSeconds(token.ExpiresIn), - HttpOnly = false, - Secure = true - }); - Response.Cookies.Append(ITokenContext.RefreshTokenType, token.RefreshToken, new CookieOptions { - MaxAge = options.Value.RefreshToken.ConstructTimeSpan, - HttpOnly = false, - Secure = true - }); + accessor.SetAuthenticationCookies(token); if (string.IsNullOrEmpty(state)) { return Ok(new SingleValueResult(token.AccessToken)); @@ -65,19 +53,14 @@ public class OpenIdController(IOpenIdAccessor accessor, IOptions if (token is null) return NotFound("Refresh token not valid"); - Response.Cookies.Append(ITokenContext.AccessTokenType, token.AccessToken, new CookieOptions { - MaxAge = TimeSpan.FromSeconds(token.ExpiresIn), - HttpOnly = false, - Secure = true - }); + accessor.SetAuthenticationCookies(token); return Ok(new SingleValueResult(token.AccessToken)); } [HttpDelete("logout")] public IActionResult Logout() { - Response.Cookies.Delete(ITokenContext.RefreshTokenType); - Response.Cookies.Delete(ITokenContext.AccessTokenType); + accessor.Logout(); return Ok(); } diff --git a/src/HopFrame.Api/Extensions/ServiceCollectionExtensions.cs b/src/HopFrame.Api/Extensions/ServiceCollectionExtensions.cs index 19436eb..21ae878 100644 --- a/src/HopFrame.Api/Extensions/ServiceCollectionExtensions.cs +++ b/src/HopFrame.Api/Extensions/ServiceCollectionExtensions.cs @@ -3,6 +3,7 @@ using HopFrame.Api.Logic; using HopFrame.Api.Logic.Implementation; using HopFrame.Database; using HopFrame.Security.Authentication; +using HopFrame.Security.Authentication.OpenID; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -25,8 +26,10 @@ public static class ServiceCollectionExtensions { if (!defaultAuthenticationSection.Exists() || configuration.GetValue("HopFrame:Authentication:DefaultAuthentication")) controllers.Add(typeof(AuthController)); - if (configuration.GetValue("HopFrame:Authentication:OpenID:Enabled")) + if (configuration.GetValue("HopFrame:Authentication:OpenID:Enabled")) { + IOpenIdAccessor.DefaultCallback = OpenIdController.DefaultCallback; controllers.Add(typeof(OpenIdController)); + } AddHopFrameNoEndpoints(services, configuration); services.AddMvcCore().UseSpecificControllers(controllers.ToArray()); diff --git a/src/HopFrame.Security/Authentication/OpenID/IOpenIdAccessor.cs b/src/HopFrame.Security/Authentication/OpenID/IOpenIdAccessor.cs index 09dc54c..91ec80d 100644 --- a/src/HopFrame.Security/Authentication/OpenID/IOpenIdAccessor.cs +++ b/src/HopFrame.Security/Authentication/OpenID/IOpenIdAccessor.cs @@ -3,9 +3,13 @@ using HopFrame.Security.Authentication.OpenID.Models; namespace HopFrame.Security.Authentication.OpenID; public interface IOpenIdAccessor { + public static string DefaultCallback; + Task LoadConfiguration(); - Task RequestToken(string code, string defaultCallback); - Task ConstructAuthUri(string defaultCallback, string state = null); + Task RequestToken(string code); + Task ConstructAuthUri(string state = null); Task InspectToken(string token); Task RefreshAccessToken(string refreshToken); + void SetAuthenticationCookies(OpenIdToken token); + void Logout(); } \ No newline at end of file diff --git a/src/HopFrame.Security/Authentication/OpenID/Implementation/OpenIdAccessor.cs b/src/HopFrame.Security/Authentication/OpenID/Implementation/OpenIdAccessor.cs index 3dd1a82..2839d10 100644 --- a/src/HopFrame.Security/Authentication/OpenID/Implementation/OpenIdAccessor.cs +++ b/src/HopFrame.Security/Authentication/OpenID/Implementation/OpenIdAccessor.cs @@ -1,6 +1,7 @@ using System.Text.Json; using HopFrame.Security.Authentication.OpenID.Models; using HopFrame.Security.Authentication.OpenID.Options; +using HopFrame.Security.Claims; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; @@ -18,7 +19,7 @@ internal class OpenIdAccessor(IHttpClientFactory clientFactory, IOptions RequestToken(string code, string defaultCallback) { + public async Task RequestToken(string code) { if (options.Value.Cache.Enabled && options.Value.Cache.Auth.Enabled && cache.TryGetValue(AuthCodeCacheKey + code, out object cachedToken)) { return cachedToken as OpenIdToken; } var protocol = accessor.HttpContext!.Request.IsHttps ? "https" : "http"; - var callback = options.Value.Callback ?? $"{protocol}://{accessor.HttpContext!.Request.Host.Value}/{defaultCallback}"; + var callback = options.Value.Callback ?? Path.Combine($"{protocol}://{accessor.HttpContext!.Request.Host.Value}", IOpenIdAccessor.DefaultCallback).Replace("\\", "/"); var configuration = await LoadConfiguration(); @@ -65,9 +66,9 @@ internal class OpenIdAccessor(IHttpClientFactory clientFactory, IOptions ConstructAuthUri(string defaultCallback, string state = null) { + public async Task ConstructAuthUri(string state = null) { var protocol = accessor.HttpContext!.Request.IsHttps ? "https" : "http"; - var callback = options.Value.Callback ?? $"{protocol}://{accessor.HttpContext!.Request.Host.Value}/{defaultCallback}"; + var callback = options.Value.Callback ?? Path.Combine($"{protocol}://{accessor.HttpContext!.Request.Host.Value}", IOpenIdAccessor.DefaultCallback).Replace("\\", "/"); var configuration = await LoadConfiguration(); return $"{configuration.AuthorizationEndpoint}?response_type=code&client_id={options.Value.ClientId}&redirect_uri={callback}&scope=openid%20profile%20email%20offline_access&state={state}"; @@ -120,4 +121,25 @@ internal class OpenIdAccessor(IHttpClientFactory clientFactory, IOptions(await response.Content.ReadAsStreamAsync()); } + + public void SetAuthenticationCookies(OpenIdToken token) { + if (token.AccessToken is not null) + accessor.HttpContext!.Response.Cookies.Append(ITokenContext.AccessTokenType, token.AccessToken, new CookieOptions { + MaxAge = TimeSpan.FromSeconds(token.ExpiresIn), + HttpOnly = false, + Secure = true + }); + + if (token.RefreshToken is not null) + accessor.HttpContext!.Response.Cookies.Append(ITokenContext.RefreshTokenType, token.RefreshToken, new CookieOptions { + MaxAge = options.Value.RefreshToken.ConstructTimeSpan, + HttpOnly = false, + Secure = true + }); + } + + public void Logout() { + accessor.HttpContext!.Response.Cookies.Delete(ITokenContext.RefreshTokenType); + accessor.HttpContext!.Response.Cookies.Delete(ITokenContext.AccessTokenType); + } } \ No newline at end of file diff --git a/src/HopFrame.Security/Authentication/OpenID/Options/OpenIdOptions.cs b/src/HopFrame.Security/Authentication/OpenID/Options/OpenIdOptions.cs index 49a219c..cc9ef52 100644 --- a/src/HopFrame.Security/Authentication/OpenID/Options/OpenIdOptions.cs +++ b/src/HopFrame.Security/Authentication/OpenID/Options/OpenIdOptions.cs @@ -22,7 +22,7 @@ public sealed class OpenIdOptions : OptionsFromConfiguration { Configuration = new() { Enabled = true, TTL = new() { - Minutes = 10 + Hours = 24 } }, Auth = new() { diff --git a/src/HopFrame.Web/Models/HopFrameWebModuleConfig.cs b/src/HopFrame.Web/Models/HopFrameWebModuleConfig.cs new file mode 100644 index 0000000..226b144 --- /dev/null +++ b/src/HopFrame.Web/Models/HopFrameWebModuleConfig.cs @@ -0,0 +1,5 @@ +namespace HopFrame.Web.Models; + +public class HopFrameWebModuleConfig { + public string AdminLoginPageUri { get; set; } = "/administration/login"; +} \ No newline at end of file diff --git a/src/HopFrame.Web/Pages/Administration/AdminDashboard.razor b/src/HopFrame.Web/Pages/Administration/AdminDashboard.razor index fe7afb1..30acfcb 100644 --- a/src/HopFrame.Web/Pages/Administration/AdminDashboard.razor +++ b/src/HopFrame.Web/Pages/Administration/AdminDashboard.razor @@ -8,11 +8,12 @@ @using HopFrame.Security.Authorization @using HopFrame.Web.Admin.Providers @using HopFrame.Web.Components +@using HopFrame.Web.Models @using Microsoft.AspNetCore.Components.Web @using Microsoft.Extensions.Options @layout AdminLayout - + Admin Dashboard @@ -38,11 +39,16 @@ @inject NavigationManager Navigator @inject IAdminPagesProvider Pages @inject IOptions Options +@inject HopFrameWebModuleConfig Config @code { public void NavigateTo(string url) { - Navigator.NavigateTo("administration/" + url, true); + Navigator.NavigateTo("/administration/" + url, true); + } + + public string ConstructRedirectUri() { + return Config.AdminLoginPageUri + "?redirect=/administration"; } } diff --git a/src/HopFrame.Web/Pages/Administration/AdminLogin.razor b/src/HopFrame.Web/Pages/Administration/AdminLogin.razor index 8e0f1e1..a6d1566 100644 --- a/src/HopFrame.Web/Pages/Administration/AdminLogin.razor +++ b/src/HopFrame.Web/Pages/Administration/AdminLogin.razor @@ -65,6 +65,6 @@ return; } - Navigator.NavigateTo(string.IsNullOrEmpty(RedirectAfter) ? DefaultRedirect : "/administration/" + RedirectAfter, true); + Navigator.NavigateTo(string.IsNullOrEmpty(RedirectAfter) ? DefaultRedirect : RedirectAfter, true); } } \ No newline at end of file diff --git a/src/HopFrame.Web/Pages/Administration/AdminPageList.razor b/src/HopFrame.Web/Pages/Administration/AdminPageList.razor index 1086918..780d7ac 100644 --- a/src/HopFrame.Web/Pages/Administration/AdminPageList.razor +++ b/src/HopFrame.Web/Pages/Administration/AdminPageList.razor @@ -16,6 +16,7 @@ @using HopFrame.Security.Claims @using HopFrame.Web.Admin @using HopFrame.Web.Components +@using HopFrame.Web.Models @_pageData.Title @@ -107,6 +108,7 @@ @inject IPermissionRepository Permissions @inject SweetAlertService Alerts @inject NavigationManager Navigator +@inject HopFrameWebModuleConfig Config @code { [Parameter] @@ -251,6 +253,6 @@ } private string GenerateRedirectString() { - return "/administration/login?redirect=" + _pageData?.Url; + return Config.AdminLoginPageUri + "?redirect=/administration/" + _pageData?.Url; } } \ No newline at end of file diff --git a/src/HopFrame.Web/ServiceCollectionExtensions.cs b/src/HopFrame.Web/ServiceCollectionExtensions.cs index 4b6232a..f87dc4b 100644 --- a/src/HopFrame.Web/ServiceCollectionExtensions.cs +++ b/src/HopFrame.Web/ServiceCollectionExtensions.cs @@ -3,6 +3,7 @@ using CurrieTechnologies.Razor.SweetAlert2; using HopFrame.Database; using HopFrame.Security.Authentication; using HopFrame.Web.Admin; +using HopFrame.Web.Models; using HopFrame.Web.Services; using HopFrame.Web.Services.Implementation; using Microsoft.AspNetCore.Builder; @@ -12,12 +13,13 @@ using Microsoft.Extensions.DependencyInjection; namespace HopFrame.Web; public static class ServiceCollectionExtensions { - public static IServiceCollection AddHopFrame(this IServiceCollection services, ConfigurationManager configuration) where TDbContext : HopDbContextBase { + public static IServiceCollection AddHopFrame(this IServiceCollection services, ConfigurationManager configuration, HopFrameWebModuleConfig config = null) where TDbContext : HopDbContextBase { services.AddHttpClient(); services.AddHopFrameRepositories(); services.AddScoped(); services.AddTransient(); services.AddAdminContext(); + services.AddSingleton(config ?? new HopFrameWebModuleConfig()); // Component library's services.AddSweetAlert2(); diff --git a/src/HopFrame.Web/Services/Implementation/AuthService.cs b/src/HopFrame.Web/Services/Implementation/AuthService.cs index 5c95a8f..ddb10d5 100644 --- a/src/HopFrame.Web/Services/Implementation/AuthService.cs +++ b/src/HopFrame.Web/Services/Implementation/AuthService.cs @@ -108,11 +108,7 @@ internal class AuthService( }); } - httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, openIdToken.AccessToken, new CookieOptions { - MaxAge = TimeSpan.FromSeconds(openIdToken.ExpiresIn), - HttpOnly = false, - Secure = true - }); + accessor.SetAuthenticationCookies(openIdToken); return new() { Owner = user, CreatedAt = DateTime.Now, diff --git a/testing/HopFrame.Testing.Api/Controllers/TestController.cs b/testing/HopFrame.Testing.Api/Controllers/TestController.cs index 3a3affe..156dc86 100644 --- a/testing/HopFrame.Testing.Api/Controllers/TestController.cs +++ b/testing/HopFrame.Testing.Api/Controllers/TestController.cs @@ -2,6 +2,7 @@ using HopFrame.Api.Logic; using HopFrame.Api.Models; using HopFrame.Database.Models; using HopFrame.Database.Repositories; +using HopFrame.Security.Authentication.OpenID; using HopFrame.Security.Authorization; using HopFrame.Security.Claims; using HopFrame.Testing.Api.Models; @@ -68,9 +69,8 @@ public class TestController(ITokenContext userContext, DatabaseContext context, } [HttpGet("url")] - public async Task>> GetUrl() { - var protocol = Request.IsHttps ? "https" : "http"; - return Ok($"{protocol}://{Request.Host.Value}/auth/callback"); + public ActionResult GetUrl() { + return Ok(IOpenIdAccessor.DefaultCallback ?? "Not set"); } } \ No newline at end of file diff --git a/tests/HopFrame.Tests.Web/Pages/AdminLoginTests.cs b/tests/HopFrame.Tests.Web/Pages/AdminLoginTests.cs index 0e2909a..f729726 100644 --- a/tests/HopFrame.Tests.Web/Pages/AdminLoginTests.cs +++ b/tests/HopFrame.Tests.Web/Pages/AdminLoginTests.cs @@ -76,7 +76,7 @@ public class AdminLoginTests : TestContext { var password = component.Find("""input[type="password"]"""); var submit = component.Find("button"); - component.Instance.RedirectAfter = "testRedirect"; + component.Instance.RedirectAfter = "/administration/testRedirect"; // Act email.Change("test@example.com");