From 88c8fe612d69367bbdfe6d24f0e90826d5ab660d Mon Sep 17 00:00:00 2001 From: Leon Hoppe Date: Sat, 21 Dec 2024 14:04:49 +0100 Subject: [PATCH 1/4] Added configuration wrappers, authentication options and authentication documentation --- docs/authentication.md | 43 +++++++++++++++++++ .../Extensions/ServiceCollectionExtensions.cs | 11 +++-- .../Logic/Implementation/AuthLogic.cs | 15 ++++--- .../Authentication/HopFrameAuthentication.cs | 10 ++--- .../HopFrameAuthenticationExtensions.cs | 9 ++-- .../HopFrameAuthenticationOptions.cs | 20 +++++++++ .../Options/OptionsFromConfiguration.cs | 5 +++ .../OptionsFromConfigurationExtensions.cs | 19 ++++++++ .../ServiceCollectionExtensions.cs | 5 ++- .../Services/Implementation/AuthService.cs | 18 ++++---- testing/HopFrame.Testing.Api/Program.cs | 2 +- testing/HopFrame.Testing.Web/Program.cs | 2 +- tests/HopFrame.Tests.Api/AuthLogicTests.cs | 3 +- .../AuthenticationTests.cs | 2 +- tests/HopFrame.Tests.Web/AuthServiceTests.cs | 4 +- 15 files changed, 134 insertions(+), 34 deletions(-) create mode 100644 docs/authentication.md create mode 100644 src/HopFrame.Security/Authentication/HopFrameAuthenticationOptions.cs create mode 100644 src/HopFrame.Security/Options/OptionsFromConfiguration.cs create mode 100644 src/HopFrame.Security/Options/OptionsFromConfigurationExtensions.cs diff --git a/docs/authentication.md b/docs/authentication.md new file mode 100644 index 0000000..e1a0182 --- /dev/null +++ b/docs/authentication.md @@ -0,0 +1,43 @@ +# HopFrame Authentication + +HopFrame uses a token system with a short term access token and a long term refresh token for authenticating users. +These tokens are usually provided to the endpoints of the API / Blazor Pages through Cookies: + +| Cookie key | Cookie value sample | Description | +|--------------------------------|----------------------------------------|-----------------------------| +| HopFrame.Security.RefreshToken | `42047983-914d-418b-841a-4382614231be` | The long term refresh token | +| HopFrame.Security.AccessToken | `d39c9432-0831-42df-8844-5e2b70f03eda` | The short term access token | + +The advantage of these cookies is that they are automatically set by the backend and delete themselves, when they are +no longer valid. + +The access token can also be delivered through a header called `HopFrame.Authentication` or `Token`. +It can also be delivered through a query parameter called `token`. This simplifies requests for images for example +because you can directly specify the url in the img tag in html. + +## Authentication configuration + +You can also configure the time span that the tokens are valid using the `appsettings.json` or environment variables +by configuring your configuration to load these. +>**Hint**: Configuring your application to use environment variables works by simply adding +> `builder.Configuration.AddEnvironmentVariables();` to your startup configuration before you add the +> custom configurations / HopFrame services. + +### Example + +You can specify `Seconds`, `Minutes`, `Hours` and `Days` for either of the two token types. +These get combined to a single time span. + +```json + "HopFrame": { + "Authentication": { + "AccessToken": { + "Minutes": 30 + }, + "RefreshToken": { + "Days": 10, + "Hours": 5 + } + } + } +``` diff --git a/src/HopFrame.Api/Extensions/ServiceCollectionExtensions.cs b/src/HopFrame.Api/Extensions/ServiceCollectionExtensions.cs index 618a437..51eacde 100644 --- a/src/HopFrame.Api/Extensions/ServiceCollectionExtensions.cs +++ b/src/HopFrame.Api/Extensions/ServiceCollectionExtensions.cs @@ -4,6 +4,7 @@ using HopFrame.Api.Logic.Implementation; using HopFrame.Database; using HopFrame.Security.Authentication; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -15,23 +16,25 @@ public static class ServiceCollectionExtensions { /// Adds all HopFrame endpoints and services to the application /// /// The service provider to add the services to + /// The configuration used to configure HopFrame authentication /// The data source for all HopFrame entities - public static void AddHopFrame(this IServiceCollection services) where TDbContext : HopDbContextBase { + public static void AddHopFrame(this IServiceCollection services, ConfigurationManager configuration) where TDbContext : HopDbContextBase { services.AddMvcCore().UseSpecificControllers(typeof(SecurityController)); - AddHopFrameNoEndpoints(services); + AddHopFrameNoEndpoints(services, configuration); } /// /// Adds all HopFrame services to the application /// /// The service provider to add the services to + /// The configuration used to configure HopFrame authentication /// The data source for all HopFrame entities - public static void AddHopFrameNoEndpoints(this IServiceCollection services) where TDbContext : HopDbContextBase { + public static void AddHopFrameNoEndpoints(this IServiceCollection services, ConfigurationManager configuration) where TDbContext : HopDbContextBase { services.AddHopFrameRepositories(); services.TryAddSingleton(); services.AddScoped(); - services.AddHopFrameAuthentication(); + services.AddHopFrameAuthentication(configuration); } } diff --git a/src/HopFrame.Api/Logic/Implementation/AuthLogic.cs b/src/HopFrame.Api/Logic/Implementation/AuthLogic.cs index c1f3c90..acf8fb7 100644 --- a/src/HopFrame.Api/Logic/Implementation/AuthLogic.cs +++ b/src/HopFrame.Api/Logic/Implementation/AuthLogic.cs @@ -5,10 +5,11 @@ using HopFrame.Security.Authentication; using HopFrame.Security.Claims; using HopFrame.Security.Models; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; namespace HopFrame.Api.Logic.Implementation; -internal class AuthLogic(IUserRepository users, ITokenRepository tokens, ITokenContext tokenContext, IHttpContextAccessor accessor) : IAuthLogic { +internal class AuthLogic(IUserRepository users, ITokenRepository tokens, ITokenContext tokenContext, IHttpContextAccessor accessor, IOptions options) : IAuthLogic { public async Task>> Login(UserLogin login) { var user = await users.GetUserByEmail(login.Email); @@ -23,12 +24,12 @@ internal class AuthLogic(IUserRepository users, ITokenRepository tokens, ITokenC var accessToken = await tokens.CreateToken(Token.AccessTokenType, user); accessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Content.ToString(), new CookieOptions { - MaxAge = HopFrameAuthentication.RefreshTokenTime, + MaxAge = options.Value.RefreshTokenTime, HttpOnly = true, Secure = true }); accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions { - MaxAge = HopFrameAuthentication.AccessTokenTime, + MaxAge = options.Value.AccessTokenTime, HttpOnly = true, Secure = true }); @@ -54,12 +55,12 @@ internal class AuthLogic(IUserRepository users, ITokenRepository tokens, ITokenC var accessToken = await tokens.CreateToken(Token.AccessTokenType, user); accessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Content.ToString(), new CookieOptions { - MaxAge = HopFrameAuthentication.RefreshTokenTime, + MaxAge = options.Value.RefreshTokenTime, HttpOnly = true, Secure = true }); accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions { - MaxAge = HopFrameAuthentication.AccessTokenTime, + MaxAge = options.Value.AccessTokenTime, HttpOnly = false, Secure = true }); @@ -81,13 +82,13 @@ internal class AuthLogic(IUserRepository users, ITokenRepository tokens, ITokenC if (token.Type != Token.RefreshTokenType) return LogicResult>.Conflict("The provided token is not a refresh token"); - if (token.CreatedAt + HopFrameAuthentication.RefreshTokenTime < DateTime.Now) + if (token.CreatedAt + options.Value.RefreshTokenTime < DateTime.Now) return LogicResult>.Forbidden("Refresh token is expired"); var accessToken = await tokens.CreateToken(Token.AccessTokenType, token.Owner); accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions { - MaxAge = HopFrameAuthentication.AccessTokenTime, + MaxAge = options.Value.AccessTokenTime, HttpOnly = false, Secure = true }); diff --git a/src/HopFrame.Security/Authentication/HopFrameAuthentication.cs b/src/HopFrame.Security/Authentication/HopFrameAuthentication.cs index 8709f91..8b0a3b1 100644 --- a/src/HopFrame.Security/Authentication/HopFrameAuthentication.cs +++ b/src/HopFrame.Security/Authentication/HopFrameAuthentication.cs @@ -17,23 +17,23 @@ public class HopFrameAuthentication( UrlEncoder encoder, ISystemClock clock, ITokenRepository tokens, - IPermissionRepository perms) + IPermissionRepository perms, + IOptions tokenOptions) : AuthenticationHandler(options, logger, encoder, clock) { - public const string SchemeName = "HopCore.Authentication"; - public static readonly TimeSpan AccessTokenTime = new(0, 0, 5, 0); - public static readonly TimeSpan RefreshTokenTime = new(30, 0, 0, 0); + public const string SchemeName = "HopFrame.Authentication"; protected override async Task HandleAuthenticateAsync() { var accessToken = Request.Cookies[ITokenContext.AccessTokenType]; if (string.IsNullOrEmpty(accessToken)) accessToken = Request.Headers[SchemeName]; if (string.IsNullOrEmpty(accessToken)) accessToken = Request.Headers["Token"]; + if (string.IsNullOrEmpty(accessToken)) accessToken = Request.Query["token"]; if (string.IsNullOrEmpty(accessToken)) return AuthenticateResult.Fail("No Access Token provided"); var tokenEntry = await tokens.GetToken(accessToken); if (tokenEntry is null) return AuthenticateResult.Fail("The provided Access Token does not exist"); - if (tokenEntry.CreatedAt + AccessTokenTime < DateTime.Now) return AuthenticateResult.Fail("The provided Access Token is expired"); + if (tokenEntry.CreatedAt + tokenOptions.Value.AccessTokenTime < DateTime.Now) return AuthenticateResult.Fail("The provided Access Token is expired"); if (tokenEntry.Owner is null) return AuthenticateResult.Fail("The provided Access Token does not match any user"); diff --git a/src/HopFrame.Security/Authentication/HopFrameAuthenticationExtensions.cs b/src/HopFrame.Security/Authentication/HopFrameAuthenticationExtensions.cs index cf87810..d45b048 100644 --- a/src/HopFrame.Security/Authentication/HopFrameAuthenticationExtensions.cs +++ b/src/HopFrame.Security/Authentication/HopFrameAuthenticationExtensions.cs @@ -1,23 +1,26 @@ using HopFrame.Security.Claims; +using HopFrame.Security.Options; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; namespace HopFrame.Security.Authentication; public static class HopFrameAuthenticationExtensions { - /// /// Configures the WebApplication to use the authentication and authorization of the HopFrame API /// /// The service provider to add the services to - /// The database object that saves all entities that are important for the security api + /// The configuration used to configure HopFrame authentication /// - public static IServiceCollection AddHopFrameAuthentication(this IServiceCollection service) { + public static IServiceCollection AddHopFrameAuthentication(this IServiceCollection service, ConfigurationManager configuration) { service.TryAddSingleton(); service.AddScoped(); + service.AddOptionsFromConfiguration(configuration); + service.AddAuthentication(HopFrameAuthentication.SchemeName).AddScheme(HopFrameAuthentication.SchemeName, _ => {}); service.AddAuthorization(); diff --git a/src/HopFrame.Security/Authentication/HopFrameAuthenticationOptions.cs b/src/HopFrame.Security/Authentication/HopFrameAuthenticationOptions.cs new file mode 100644 index 0000000..b996d68 --- /dev/null +++ b/src/HopFrame.Security/Authentication/HopFrameAuthenticationOptions.cs @@ -0,0 +1,20 @@ +using HopFrame.Security.Options; + +namespace HopFrame.Security.Authentication; + +public class HopFrameAuthenticationOptions : OptionsFromConfiguration { + public override string Position { get; } = "HopFrame:Authentication"; + + public TimeSpan AccessTokenTime => AccessToken is null ? new(0, 0, 5, 0) : new(AccessToken.Days, AccessToken.Hours, AccessToken.Minutes, AccessToken.Seconds); + public TimeSpan RefreshTokenTime => RefreshToken is null ? new(30, 0, 0, 0) : new(RefreshToken.Days, RefreshToken.Hours, RefreshToken.Minutes, RefreshToken.Seconds); + + public TokenTime AccessToken { get; set; } + public TokenTime RefreshToken { get; set; } + + public class TokenTime { + public int Days { get; set; } + public int Hours { get; set; } + public int Minutes { get; set; } + public int Seconds { get; set; } + } +} \ No newline at end of file diff --git a/src/HopFrame.Security/Options/OptionsFromConfiguration.cs b/src/HopFrame.Security/Options/OptionsFromConfiguration.cs new file mode 100644 index 0000000..0f06fb8 --- /dev/null +++ b/src/HopFrame.Security/Options/OptionsFromConfiguration.cs @@ -0,0 +1,5 @@ +namespace HopFrame.Security.Options; + +public abstract class OptionsFromConfiguration { + public abstract string Position { get; } +} \ No newline at end of file diff --git a/src/HopFrame.Security/Options/OptionsFromConfigurationExtensions.cs b/src/HopFrame.Security/Options/OptionsFromConfigurationExtensions.cs new file mode 100644 index 0000000..f9b62c4 --- /dev/null +++ b/src/HopFrame.Security/Options/OptionsFromConfigurationExtensions.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace HopFrame.Security.Options; + +public static class OptionsFromConfigurationExtensions { + public static void AddOptionsFromConfiguration(this IServiceCollection services, IConfiguration configuration) where T : OptionsFromConfiguration { + T optionsInstance = (T)Activator.CreateInstance(typeof(T)); + string position = optionsInstance?.Position; + if (position is null) { + throw new ArgumentException($"""Configuration "{typeof(T).Name}" has no position configured!"""); + } + + services.Configure((Action)(options => { + IConfigurationSection section = configuration.GetSection(position); + section.Bind(options); + })); + } +} \ No newline at end of file diff --git a/src/HopFrame.Web/ServiceCollectionExtensions.cs b/src/HopFrame.Web/ServiceCollectionExtensions.cs index 548e2e9..4b6232a 100644 --- a/src/HopFrame.Web/ServiceCollectionExtensions.cs +++ b/src/HopFrame.Web/ServiceCollectionExtensions.cs @@ -6,12 +6,13 @@ using HopFrame.Web.Admin; using HopFrame.Web.Services; using HopFrame.Web.Services.Implementation; using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace HopFrame.Web; public static class ServiceCollectionExtensions { - public static IServiceCollection AddHopFrame(this IServiceCollection services) where TDbContext : HopDbContextBase { + public static IServiceCollection AddHopFrame(this IServiceCollection services, ConfigurationManager configuration) where TDbContext : HopDbContextBase { services.AddHttpClient(); services.AddHopFrameRepositories(); services.AddScoped(); @@ -22,7 +23,7 @@ public static class ServiceCollectionExtensions { services.AddSweetAlert2(); services.AddBlazorStrap(); - services.AddHopFrameAuthentication(); + services.AddHopFrameAuthentication(configuration); return services; } diff --git a/src/HopFrame.Web/Services/Implementation/AuthService.cs b/src/HopFrame.Web/Services/Implementation/AuthService.cs index e5f1ec0..6fca234 100644 --- a/src/HopFrame.Web/Services/Implementation/AuthService.cs +++ b/src/HopFrame.Web/Services/Implementation/AuthService.cs @@ -4,6 +4,7 @@ using HopFrame.Security.Authentication; using HopFrame.Security.Claims; using HopFrame.Security.Models; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; namespace HopFrame.Web.Services.Implementation; @@ -11,7 +12,8 @@ internal class AuthService( IUserRepository userService, IHttpContextAccessor httpAccessor, ITokenRepository tokens, - ITokenContext context) + ITokenContext context, + IOptions options) : IAuthService { public async Task Register(UserRegister register) { @@ -27,12 +29,12 @@ internal class AuthService( var accessToken = await tokens.CreateToken(Token.AccessTokenType, user); httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Content.ToString(), new CookieOptions { - MaxAge = HopFrameAuthentication.RefreshTokenTime, + MaxAge = options.Value.RefreshTokenTime, HttpOnly = true, Secure = true }); httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions { - MaxAge = HopFrameAuthentication.AccessTokenTime, + MaxAge = options.Value.AccessTokenTime, HttpOnly = false, Secure = true }); @@ -48,12 +50,12 @@ internal class AuthService( var accessToken = await tokens.CreateToken(Token.AccessTokenType, user); httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Content.ToString(), new CookieOptions { - MaxAge = HopFrameAuthentication.RefreshTokenTime, + MaxAge = options.Value.RefreshTokenTime, HttpOnly = true, Secure = true }); httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions { - MaxAge = HopFrameAuthentication.AccessTokenTime, + MaxAge = options.Value.AccessTokenTime, HttpOnly = false, Secure = true }); @@ -77,12 +79,12 @@ internal class AuthService( if (token is null || token.Type != Token.RefreshTokenType) return null; - if (token.CreatedAt + HopFrameAuthentication.RefreshTokenTime < DateTime.Now) return null; + if (token.CreatedAt + options.Value.RefreshTokenTime < DateTime.Now) return null; var accessToken = await tokens.CreateToken(Token.AccessTokenType, token.Owner); httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions { - MaxAge = HopFrameAuthentication.AccessTokenTime, + MaxAge = options.Value.AccessTokenTime, HttpOnly = false, Secure = true }); @@ -95,7 +97,7 @@ internal class AuthService( if (accessToken is null) return false; if (accessToken.Type != Token.AccessTokenType) return false; - if (accessToken.CreatedAt + HopFrameAuthentication.AccessTokenTime < DateTime.Now) return false; + if (accessToken.CreatedAt + options.Value.AccessTokenTime < DateTime.Now) return false; if (accessToken.Owner is null) return false; return true; diff --git a/testing/HopFrame.Testing.Api/Program.cs b/testing/HopFrame.Testing.Api/Program.cs index 6651ecd..b728eb3 100644 --- a/testing/HopFrame.Testing.Api/Program.cs +++ b/testing/HopFrame.Testing.Api/Program.cs @@ -7,7 +7,7 @@ var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllers(); -builder.Services.AddHopFrame(); +builder.Services.AddHopFrame(builder.Configuration); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); diff --git a/testing/HopFrame.Testing.Web/Program.cs b/testing/HopFrame.Testing.Web/Program.cs index 7957fff..481e7fc 100644 --- a/testing/HopFrame.Testing.Web/Program.cs +++ b/testing/HopFrame.Testing.Web/Program.cs @@ -6,7 +6,7 @@ using HopFrame.Web.Admin; var builder = WebApplication.CreateBuilder(args); builder.Services.AddDbContext(); -builder.Services.AddHopFrame(); +builder.Services.AddHopFrame(builder.Configuration); builder.Services.AddAdminContext(); // Add services to the container. diff --git a/tests/HopFrame.Tests.Api/AuthLogicTests.cs b/tests/HopFrame.Tests.Api/AuthLogicTests.cs index ca86b5b..a5163d2 100644 --- a/tests/HopFrame.Tests.Api/AuthLogicTests.cs +++ b/tests/HopFrame.Tests.Api/AuthLogicTests.cs @@ -10,6 +10,7 @@ using HopFrame.Security.Authentication; using HopFrame.Security.Claims; using HopFrame.Security.Models; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; using Moq; namespace HopFrame.Tests.Api; @@ -75,7 +76,7 @@ public class AuthLogicTests { .Setup(c => c.User) .Returns(CreateDummyUser()); - return (new AuthLogic(users.Object, tokens.Object, context.Object, accessor), accessor.HttpContext); + return (new AuthLogic(users.Object, tokens.Object, context.Object, accessor, new OptionsWrapper(new HopFrameAuthenticationOptions())), accessor.HttpContext); } private User CreateDummyUser() => new() { diff --git a/tests/HopFrame.Tests.Security/AuthenticationTests.cs b/tests/HopFrame.Tests.Security/AuthenticationTests.cs index 7c80e7d..5cd6d44 100644 --- a/tests/HopFrame.Tests.Security/AuthenticationTests.cs +++ b/tests/HopFrame.Tests.Security/AuthenticationTests.cs @@ -46,7 +46,7 @@ public class AuthenticationTests { .Setup(x => x.GetFullPermissions(It.IsAny())) .ReturnsAsync(new List()); - var auth = new HopFrameAuthentication(options.Object, logger.Object, encoder.Object, clock.Object, tokens.Object, perms.Object); + var auth = new HopFrameAuthentication(options.Object, logger.Object, encoder.Object, clock.Object, tokens.Object, perms.Object, new OptionsWrapper(new HopFrameAuthenticationOptions())); var context = new DefaultHttpContext(); if (provideCorrectToken) context.HttpContext.Request.Headers.Append(HopFrameAuthentication.SchemeName, correctToken.Content.ToString()); diff --git a/tests/HopFrame.Tests.Web/AuthServiceTests.cs b/tests/HopFrame.Tests.Web/AuthServiceTests.cs index a5df287..d5c5ad7 100644 --- a/tests/HopFrame.Tests.Web/AuthServiceTests.cs +++ b/tests/HopFrame.Tests.Web/AuthServiceTests.cs @@ -1,11 +1,13 @@ using HopFrame.Database.Models; using HopFrame.Database.Repositories; +using HopFrame.Security.Authentication; using HopFrame.Security.Claims; using HopFrame.Security.Models; using HopFrame.Tests.Web.Extensions; using HopFrame.Web.Services; using HopFrame.Web.Services.Implementation; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; using Moq; namespace HopFrame.Tests.Web; @@ -66,7 +68,7 @@ public class AuthServiceTests { .Setup(c => c.AccessToken) .Returns(providedAccessToken); - return (new AuthService(users.Object, accessor, tokens.Object, context.Object), accessor.HttpContext); + return (new AuthService(users.Object, accessor, tokens.Object, context.Object, new OptionsWrapper(new HopFrameAuthenticationOptions())), accessor.HttpContext); } private User CreateDummyUser() => new() { -- 2.49.1 From 422fd6c677ed1d89444271f487f4aef1c8522bb4 Mon Sep 17 00:00:00 2001 From: Leon Hoppe Date: Sat, 21 Dec 2024 14:08:47 +0100 Subject: [PATCH 2/4] Added authentication documentation to table of contents --- docs/readme.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/readme.md b/docs/readme.md index 289a64c..0fc3ff0 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -7,6 +7,7 @@ The HopFrame comes in two variations, you can eiter only use the backend with so - [Database](./database.md) - [Repositories](./repositories.md) - [Base Models](./models.md) +- [Authentication](./authentication.md) ## HopFrame Web API -- 2.49.1 From 51c15eff4ce3187fee71cf027d45c1dab9a4abdb Mon Sep 17 00:00:00 2001 From: Leon Hoppe Date: Sat, 21 Dec 2024 14:12:54 +0100 Subject: [PATCH 3/4] Added environment variable example for authentication configuration --- docs/authentication.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/authentication.md b/docs/authentication.md index e1a0182..469ceee 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -28,6 +28,7 @@ by configuring your configuration to load these. You can specify `Seconds`, `Minutes`, `Hours` and `Days` for either of the two token types. These get combined to a single time span. +#### Configuration example ```json "HopFrame": { "Authentication": { @@ -41,3 +42,10 @@ These get combined to a single time span. } } ``` + +#### Environment variables example +```dotenv +HOPFRAME__AUTHENTICATION__ACCESSTOKEN__MINUTES=30 +HOPFRAME__AUTHENTICATION__REFRESHTOKEN__DAYS=10 +HOPFRAME__AUTHENTICATION__REFRESHTOKEN__HOURS=5 +``` -- 2.49.1 From 92afc85dbaa72914fbbe1a3632bd5040c84b9049 Mon Sep 17 00:00:00 2001 From: Leon Hoppe Date: Sat, 21 Dec 2024 14:59:04 +0100 Subject: [PATCH 4/4] added permission configuration --- docs/permissions.md | 80 +++++++++++++++++++ docs/readme.md | 1 + src/HopFrame.Security/AdminPermissions.cs | 15 ---- .../HopFrameAuthenticationExtensions.cs | 2 + .../Authorization/AdminPermissionOptions.cs | 30 +++++++ .../Classes/AdminPermissionsAttribute.cs | 2 +- .../Generators/IAdminPageGenerator.cs | 2 +- .../Implementation/AdminPageGenerator.cs | 6 +- .../Models/AdminPagePermissions.cs | 2 +- src/HopFrame.Web/HopAdminContext.cs | 20 ++--- .../Pages/Administration/AdminDashboard.razor | 10 ++- .../Pages/Administration/AdminPageList.razor | 2 +- .../Administration/Layout/AdminMenu.razor | 2 +- 13 files changed, 138 insertions(+), 36 deletions(-) create mode 100644 docs/permissions.md delete mode 100644 src/HopFrame.Security/AdminPermissions.cs create mode 100644 src/HopFrame.Security/Authorization/AdminPermissionOptions.cs diff --git a/docs/permissions.md b/docs/permissions.md new file mode 100644 index 0000000..12a17cc --- /dev/null +++ b/docs/permissions.md @@ -0,0 +1,80 @@ +# HopFrame Permissions + +Permissions in the HopFrame are simple and effective to use. +As discussed in the [repositories](./repositories.md) documentation, you can manage user / group permissions +via the `IPermissionRepository` service. + +## How do permissions work in the HopFrame + +Permissions are defined using the . (dot) syntax. This enables you to nest permissions in namespaces. +You can also give a user or a group the permission to every permission in a namespace by using the * (star) syntax. + +| Permission | Example | Description | +|----------------------|-------------------------------|-------------------------------------------------------| +| `*` | `*` | all permissions | +| `[namespace].[name]` | `hopframe.admin.users.create` | single permission | +| `[namespace].*` | `hopframe.admin.*` | all permissions in that namespace (works recursively) | + +### Reserved namespaces + +| Namespace | Example | Description | +|-----------|---------------|------------------------------------------| +| `group` | `group.admin` | The user needs to be in a specific group | + +### Permission Groups + +You can manage them through the `IGroupRepository` as described in the [repositories](./repositories.md) documentation. +You add permissions just like you would to a user with the `IPermissionRepository`. +You can assign a user to a group by assigning the group permission to the user: +```csharp +permissionRepository.AddPermission(user, "group.admin"); +``` + +## Predefined Permissions + +| Permission | Description | +|--------------------------------|-------------------------------| +| `hopframe.admin` | Access to the admin dashboard | +| `hopframe.admin.users.read` | View all users | +| `hopframe.admin.users.update` | Edit a user | +| `hopframe.admin.users.delete` | Delete a user | +| `hopframe.admin.users.create` | Add a group | +| `hopframe.admin.groups.read` | View all groups | +| `hopframe.admin.groups.update` | Edit a group | +| `hopframe.admin.groups.delete` | Delete a group | +| `hopframe.admin.groups.create` | Add a group | + +### Configuring HopFrame permissions + +You can also configure the predefined permissions using the `appsettings.json` or environment variables +by configuring your configuration to load these. +>**Hint**: Configuring your application to use environment variables works by simply adding +> `builder.Configuration.AddEnvironmentVariables();` to your startup configuration before you add the +> custom configurations / HopFrame services. + +You can specify `Dashboard` for the dashboard permission and for `Users` and `Groups` you can specify +`Create`, `Read`, `Update` and `Delete` permissions. + +#### Configuration example +```json + "HopFrame": { + "Permissions": { + "Dashboard": "myapp.dashboard.view", + "Users": { + "Read": "myapp.read.users" + }, + "Groups": { + "Create": "myapp.create.groups", + "Update": "myapp.update.groups" + } + } + } +``` + +#### Environment variables example +```dotenv +HOPFRAME__PERMISSIONS__DASHBOARD="myapp.dashboard.view" +HOPFRAME__PERMISSIONS__USERS__READ="myapp.read.users" +HOPFRAME__PERMISSIONS__GROUPS__CREATE="myapp.create.groups" +HOPFRAME__PERMISSIONS__GROUPS__UPDATE="myapp.update.groups" +``` diff --git a/docs/readme.md b/docs/readme.md index 0fc3ff0..df7f363 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -8,6 +8,7 @@ The HopFrame comes in two variations, you can eiter only use the backend with so - [Repositories](./repositories.md) - [Base Models](./models.md) - [Authentication](./authentication.md) +- [Permissions](./permissions.md) ## HopFrame Web API diff --git a/src/HopFrame.Security/AdminPermissions.cs b/src/HopFrame.Security/AdminPermissions.cs deleted file mode 100644 index 7f45afc..0000000 --- a/src/HopFrame.Security/AdminPermissions.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace HopFrame.Security; - -public static class AdminPermissions { - public const string IsAdmin = "hopframe.admin"; - - public const string ViewUsers = "hopframe.admin.users.view"; - public const string EditUser = "hopframe.admin.users.edit"; - public const string DeleteUser = "hopframe.admin.users.delete"; - public const string AddUser = "hopframe.admin.users.add"; - - public const string ViewGroups = "hopframe.admin.groups.view"; - public const string EditGroup = "hopframe.admin.groups.edit"; - public const string DeleteGroup = "hopframe.admin.groups.delete"; - public const string AddGroup = "hopframe.admin.groups.add"; -} \ No newline at end of file diff --git a/src/HopFrame.Security/Authentication/HopFrameAuthenticationExtensions.cs b/src/HopFrame.Security/Authentication/HopFrameAuthenticationExtensions.cs index d45b048..e0b7d37 100644 --- a/src/HopFrame.Security/Authentication/HopFrameAuthenticationExtensions.cs +++ b/src/HopFrame.Security/Authentication/HopFrameAuthenticationExtensions.cs @@ -1,3 +1,4 @@ +using HopFrame.Security.Authorization; using HopFrame.Security.Claims; using HopFrame.Security.Options; using Microsoft.AspNetCore.Authentication; @@ -20,6 +21,7 @@ public static class HopFrameAuthenticationExtensions { service.AddScoped(); service.AddOptionsFromConfiguration(configuration); + service.AddOptionsFromConfiguration(configuration); service.AddAuthentication(HopFrameAuthentication.SchemeName).AddScheme(HopFrameAuthentication.SchemeName, _ => {}); service.AddAuthorization(); diff --git a/src/HopFrame.Security/Authorization/AdminPermissionOptions.cs b/src/HopFrame.Security/Authorization/AdminPermissionOptions.cs new file mode 100644 index 0000000..46fc7f0 --- /dev/null +++ b/src/HopFrame.Security/Authorization/AdminPermissionOptions.cs @@ -0,0 +1,30 @@ +using HopFrame.Security.Options; + +namespace HopFrame.Security.Authorization; + +public class AdminPermissionOptions : OptionsFromConfiguration { + public override string Position { get; } = "HopFrame:Permissions"; + + public string Dashboard { get; set; } = "hopframe.admin"; + + public CrudPermission Users { get; set; } = new() { + Read = "hopframe.admin.users.read", + Update = "hopframe.admin.users.update", + Delete = "hopframe.admin.users.delete", + Create = "hopframe.admin.users.create" + }; + + public CrudPermission Groups { get; set; } = new() { + Read = "hopframe.admin.groups.read", + Update = "hopframe.admin.groups.update", + Delete = "hopframe.admin.groups.delete", + Create = "hopframe.admin.groups.create" + }; + + public class CrudPermission { + public string Create { get; set; } + public string Read { get; set; } + public string Update { get; set; } + public string Delete { get; set; } + } +} \ No newline at end of file diff --git a/src/HopFrame.Web.Admin/Attributes/Classes/AdminPermissionsAttribute.cs b/src/HopFrame.Web.Admin/Attributes/Classes/AdminPermissionsAttribute.cs index 7d68eab..dc58ffb 100644 --- a/src/HopFrame.Web.Admin/Attributes/Classes/AdminPermissionsAttribute.cs +++ b/src/HopFrame.Web.Admin/Attributes/Classes/AdminPermissionsAttribute.cs @@ -8,6 +8,6 @@ public sealed class AdminPermissionsAttribute(string view = null, string create Create = create, Update = update, Delete = delete, - View = view + Read = view }; } diff --git a/src/HopFrame.Web.Admin/Generators/IAdminPageGenerator.cs b/src/HopFrame.Web.Admin/Generators/IAdminPageGenerator.cs index 65998bd..05ff528 100644 --- a/src/HopFrame.Web.Admin/Generators/IAdminPageGenerator.cs +++ b/src/HopFrame.Web.Admin/Generators/IAdminPageGenerator.cs @@ -24,7 +24,7 @@ public interface IAdminPageGenerator { /// /// the specified permission /// - IAdminPageGenerator ViewPermission(string permission); + IAdminPageGenerator ReadPermission(string permission); /// /// Sets the permission needed to create a new Entry diff --git a/src/HopFrame.Web.Admin/Generators/Implementation/AdminPageGenerator.cs b/src/HopFrame.Web.Admin/Generators/Implementation/AdminPageGenerator.cs index eb61f7d..3181c97 100644 --- a/src/HopFrame.Web.Admin/Generators/Implementation/AdminPageGenerator.cs +++ b/src/HopFrame.Web.Admin/Generators/Implementation/AdminPageGenerator.cs @@ -48,8 +48,8 @@ internal sealed class AdminPageGenerator : IAdminPageGenerator, return this; } - public IAdminPageGenerator ViewPermission(string permission) { - Page.Permissions.View = permission; + public IAdminPageGenerator ReadPermission(string permission) { + Page.Permissions.Read = permission; return this; } @@ -165,7 +165,7 @@ internal sealed class AdminPageGenerator : IAdminPageGenerator, var attribute = attributes.Single(a => a is AdminPermissionsAttribute) as AdminPermissionsAttribute; CreatePermission(attribute?.Permissions.Create); UpdatePermission(attribute?.Permissions.Update); - ViewPermission(attribute?.Permissions.View); + ReadPermission(attribute?.Permissions.Read); DeletePermission(attribute?.Permissions.Delete); } diff --git a/src/HopFrame.Web.Admin/Models/AdminPagePermissions.cs b/src/HopFrame.Web.Admin/Models/AdminPagePermissions.cs index e9629a6..0312aaa 100644 --- a/src/HopFrame.Web.Admin/Models/AdminPagePermissions.cs +++ b/src/HopFrame.Web.Admin/Models/AdminPagePermissions.cs @@ -1,7 +1,7 @@ namespace HopFrame.Web.Admin.Models; public sealed class AdminPagePermissions { - public string View { get; set; } + public string Read { get; set; } public string Create { get; set; } public string Update { get; set; } public string Delete { get; set; } diff --git a/src/HopFrame.Web/HopAdminContext.cs b/src/HopFrame.Web/HopAdminContext.cs index 0beffd2..198ce65 100644 --- a/src/HopFrame.Web/HopAdminContext.cs +++ b/src/HopFrame.Web/HopAdminContext.cs @@ -1,15 +1,17 @@ using System.Text.RegularExpressions; using HopFrame.Database.Models; using HopFrame.Security; +using HopFrame.Security.Authorization; using HopFrame.Web.Admin; using HopFrame.Web.Admin.Attributes; using HopFrame.Web.Admin.Generators; using HopFrame.Web.Admin.Models; using HopFrame.Web.Provider; +using Microsoft.Extensions.Options; namespace HopFrame.Web; -internal class HopAdminContext : AdminPagesContext { +internal class HopAdminContext(IOptions options) : AdminPagesContext { [AdminPageUrl("users")] public AdminPage Users { get; set; } @@ -21,10 +23,10 @@ internal class HopAdminContext : AdminPagesContext { generator.Page() .Description("On this page you can manage all user accounts.") .ConfigureProvider() - .ViewPermission(AdminPermissions.ViewUsers) - .CreatePermission(AdminPermissions.AddUser) - .UpdatePermission(AdminPermissions.EditUser) - .DeletePermission(AdminPermissions.DeleteUser); + .ReadPermission(options.Value.Users.Read) + .CreatePermission(options.Value.Users.Create) + .UpdatePermission(options.Value.Users.Update) + .DeletePermission(options.Value.Users.Delete); generator.Page().Property(u => u.Password) .DisplayInListing(false) @@ -64,10 +66,10 @@ internal class HopAdminContext : AdminPagesContext { generator.Page() .Description("On this page you can view, create, edit and delete permission groups.") .ConfigureProvider() - .ViewPermission(AdminPermissions.ViewGroups) - .CreatePermission(AdminPermissions.AddGroup) - .UpdatePermission(AdminPermissions.EditGroup) - .DeletePermission(AdminPermissions.DeleteGroup) + .ReadPermission(options.Value.Groups.Read) + .CreatePermission(options.Value.Groups.Create) + .UpdatePermission(options.Value.Groups.Update) + .DeletePermission(options.Value.Groups.Delete) .ListingProperty(g => g.Name); generator.Page().Property(g => g.Name) diff --git a/src/HopFrame.Web/Pages/Administration/AdminDashboard.razor b/src/HopFrame.Web/Pages/Administration/AdminDashboard.razor index 7ebb3cf..fe7afb1 100644 --- a/src/HopFrame.Web/Pages/Administration/AdminDashboard.razor +++ b/src/HopFrame.Web/Pages/Administration/AdminDashboard.razor @@ -5,25 +5,26 @@ @using BlazorStrap @using HopFrame.Web.Pages.Administration.Layout @using BlazorStrap.V5 -@using HopFrame.Security +@using HopFrame.Security.Authorization @using HopFrame.Web.Admin.Providers @using HopFrame.Web.Components @using Microsoft.AspNetCore.Components.Web +@using Microsoft.Extensions.Options @layout AdminLayout - + Admin Dashboard @foreach (var adminPage in Pages.LoadRegisteredAdminPages()) { - + @adminPage.Title - @adminPage.Permissions.View + @adminPage.Permissions.Read @adminPage.Description Open @@ -36,6 +37,7 @@ @inject NavigationManager Navigator @inject IAdminPagesProvider Pages +@inject IOptions Options @code { diff --git a/src/HopFrame.Web/Pages/Administration/AdminPageList.razor b/src/HopFrame.Web/Pages/Administration/AdminPageList.razor index bf4c17d..d796aeb 100644 --- a/src/HopFrame.Web/Pages/Administration/AdminPageList.razor +++ b/src/HopFrame.Web/Pages/Administration/AdminPageList.razor @@ -18,7 +18,7 @@ @using HopFrame.Web.Components @_pageData.Title - + diff --git a/src/HopFrame.Web/Pages/Administration/Layout/AdminMenu.razor b/src/HopFrame.Web/Pages/Administration/Layout/AdminMenu.razor index a47bafb..409b002 100644 --- a/src/HopFrame.Web/Pages/Administration/Layout/AdminMenu.razor +++ b/src/HopFrame.Web/Pages/Administration/Layout/AdminMenu.razor @@ -24,7 +24,7 @@ Dashboard @foreach (var adminPage in Pages.LoadRegisteredAdminPages()) { - + @adminPage.Title } -- 2.49.1