Merge branch 'feature/config' into 'dev'

Resolve "Configuratable token times"

See merge request leon.hoppe/hopframe!2
This commit was merged in pull request #40.
This commit is contained in:
2024-12-21 14:03:48 +00:00
27 changed files with 281 additions and 70 deletions

51
docs/authentication.md Normal file
View File

@@ -0,0 +1,51 @@
# 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.
#### Configuration example
```json
"HopFrame": {
"Authentication": {
"AccessToken": {
"Minutes": 30
},
"RefreshToken": {
"Days": 10,
"Hours": 5
}
}
}
```
#### Environment variables example
```dotenv
HOPFRAME__AUTHENTICATION__ACCESSTOKEN__MINUTES=30
HOPFRAME__AUTHENTICATION__REFRESHTOKEN__DAYS=10
HOPFRAME__AUTHENTICATION__REFRESHTOKEN__HOURS=5
```

80
docs/permissions.md Normal file
View File

@@ -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"
```

View File

@@ -7,6 +7,8 @@ The HopFrame comes in two variations, you can eiter only use the backend with so
- [Database](./database.md) - [Database](./database.md)
- [Repositories](./repositories.md) - [Repositories](./repositories.md)
- [Base Models](./models.md) - [Base Models](./models.md)
- [Authentication](./authentication.md)
- [Permissions](./permissions.md)
## HopFrame Web API ## HopFrame Web API

View File

@@ -4,6 +4,7 @@ using HopFrame.Api.Logic.Implementation;
using HopFrame.Database; using HopFrame.Database;
using HopFrame.Security.Authentication; using HopFrame.Security.Authentication;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.DependencyInjection.Extensions;
@@ -15,23 +16,25 @@ public static class ServiceCollectionExtensions {
/// Adds all HopFrame endpoints and services to the application /// Adds all HopFrame endpoints and services to the application
/// </summary> /// </summary>
/// <param name="services">The service provider to add the services to</param> /// <param name="services">The service provider to add the services to</param>
/// <param name="configuration">The configuration used to configure HopFrame authentication</param>
/// <typeparam name="TDbContext">The data source for all HopFrame entities</typeparam> /// <typeparam name="TDbContext">The data source for all HopFrame entities</typeparam>
public static void AddHopFrame<TDbContext>(this IServiceCollection services) where TDbContext : HopDbContextBase { public static void AddHopFrame<TDbContext>(this IServiceCollection services, ConfigurationManager configuration) where TDbContext : HopDbContextBase {
services.AddMvcCore().UseSpecificControllers(typeof(SecurityController)); services.AddMvcCore().UseSpecificControllers(typeof(SecurityController));
AddHopFrameNoEndpoints<TDbContext>(services); AddHopFrameNoEndpoints<TDbContext>(services, configuration);
} }
/// <summary> /// <summary>
/// Adds all HopFrame services to the application /// Adds all HopFrame services to the application
/// </summary> /// </summary>
/// <param name="services">The service provider to add the services to</param> /// <param name="services">The service provider to add the services to</param>
/// <param name="configuration">The configuration used to configure HopFrame authentication</param>
/// <typeparam name="TDbContext">The data source for all HopFrame entities</typeparam> /// <typeparam name="TDbContext">The data source for all HopFrame entities</typeparam>
public static void AddHopFrameNoEndpoints<TDbContext>(this IServiceCollection services) where TDbContext : HopDbContextBase { public static void AddHopFrameNoEndpoints<TDbContext>(this IServiceCollection services, ConfigurationManager configuration) where TDbContext : HopDbContextBase {
services.AddHopFrameRepositories<TDbContext>(); services.AddHopFrameRepositories<TDbContext>();
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>(); services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddScoped<IAuthLogic, AuthLogic>(); services.AddScoped<IAuthLogic, AuthLogic>();
services.AddHopFrameAuthentication(); services.AddHopFrameAuthentication(configuration);
} }
} }

View File

@@ -5,10 +5,11 @@ using HopFrame.Security.Authentication;
using HopFrame.Security.Claims; using HopFrame.Security.Claims;
using HopFrame.Security.Models; using HopFrame.Security.Models;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
namespace HopFrame.Api.Logic.Implementation; 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<HopFrameAuthenticationOptions> options) : IAuthLogic {
public async Task<LogicResult<SingleValueResult<string>>> Login(UserLogin login) { public async Task<LogicResult<SingleValueResult<string>>> Login(UserLogin login) {
var user = await users.GetUserByEmail(login.Email); 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); var accessToken = await tokens.CreateToken(Token.AccessTokenType, user);
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Content.ToString(), new CookieOptions { accessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Content.ToString(), new CookieOptions {
MaxAge = HopFrameAuthentication.RefreshTokenTime, MaxAge = options.Value.RefreshTokenTime,
HttpOnly = true, HttpOnly = true,
Secure = true Secure = true
}); });
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions { accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions {
MaxAge = HopFrameAuthentication.AccessTokenTime, MaxAge = options.Value.AccessTokenTime,
HttpOnly = true, HttpOnly = true,
Secure = true Secure = true
}); });
@@ -54,12 +55,12 @@ internal class AuthLogic(IUserRepository users, ITokenRepository tokens, ITokenC
var accessToken = await tokens.CreateToken(Token.AccessTokenType, user); var accessToken = await tokens.CreateToken(Token.AccessTokenType, user);
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Content.ToString(), new CookieOptions { accessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Content.ToString(), new CookieOptions {
MaxAge = HopFrameAuthentication.RefreshTokenTime, MaxAge = options.Value.RefreshTokenTime,
HttpOnly = true, HttpOnly = true,
Secure = true Secure = true
}); });
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions { accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions {
MaxAge = HopFrameAuthentication.AccessTokenTime, MaxAge = options.Value.AccessTokenTime,
HttpOnly = false, HttpOnly = false,
Secure = true Secure = true
}); });
@@ -81,13 +82,13 @@ internal class AuthLogic(IUserRepository users, ITokenRepository tokens, ITokenC
if (token.Type != Token.RefreshTokenType) if (token.Type != Token.RefreshTokenType)
return LogicResult<SingleValueResult<string>>.Conflict("The provided token is not a refresh token"); return LogicResult<SingleValueResult<string>>.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<SingleValueResult<string>>.Forbidden("Refresh token is expired"); return LogicResult<SingleValueResult<string>>.Forbidden("Refresh token is expired");
var accessToken = await tokens.CreateToken(Token.AccessTokenType, token.Owner); var accessToken = await tokens.CreateToken(Token.AccessTokenType, token.Owner);
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions { accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions {
MaxAge = HopFrameAuthentication.AccessTokenTime, MaxAge = options.Value.AccessTokenTime,
HttpOnly = false, HttpOnly = false,
Secure = true Secure = true
}); });

View File

@@ -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";
}

View File

@@ -17,23 +17,23 @@ public class HopFrameAuthentication(
UrlEncoder encoder, UrlEncoder encoder,
ISystemClock clock, ISystemClock clock,
ITokenRepository tokens, ITokenRepository tokens,
IPermissionRepository perms) IPermissionRepository perms,
IOptions<HopFrameAuthenticationOptions> tokenOptions)
: AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder, clock) { : AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder, clock) {
public const string SchemeName = "HopCore.Authentication"; public const string SchemeName = "HopFrame.Authentication";
public static readonly TimeSpan AccessTokenTime = new(0, 0, 5, 0);
public static readonly TimeSpan RefreshTokenTime = new(30, 0, 0, 0);
protected override async Task<AuthenticateResult> HandleAuthenticateAsync() { protected override async Task<AuthenticateResult> HandleAuthenticateAsync() {
var accessToken = Request.Cookies[ITokenContext.AccessTokenType]; var accessToken = Request.Cookies[ITokenContext.AccessTokenType];
if (string.IsNullOrEmpty(accessToken)) accessToken = Request.Headers[SchemeName]; if (string.IsNullOrEmpty(accessToken)) accessToken = Request.Headers[SchemeName];
if (string.IsNullOrEmpty(accessToken)) accessToken = Request.Headers["Token"]; 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"); if (string.IsNullOrEmpty(accessToken)) return AuthenticateResult.Fail("No Access Token provided");
var tokenEntry = await tokens.GetToken(accessToken); var tokenEntry = await tokens.GetToken(accessToken);
if (tokenEntry is null) return AuthenticateResult.Fail("The provided Access Token does not exist"); 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) if (tokenEntry.Owner is null)
return AuthenticateResult.Fail("The provided Access Token does not match any user"); return AuthenticateResult.Fail("The provided Access Token does not match any user");

View File

@@ -1,23 +1,28 @@
using HopFrame.Security.Authorization;
using HopFrame.Security.Claims; using HopFrame.Security.Claims;
using HopFrame.Security.Options;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.DependencyInjection.Extensions;
namespace HopFrame.Security.Authentication; namespace HopFrame.Security.Authentication;
public static class HopFrameAuthenticationExtensions { public static class HopFrameAuthenticationExtensions {
/// <summary> /// <summary>
/// Configures the WebApplication to use the authentication and authorization of the HopFrame API /// Configures the WebApplication to use the authentication and authorization of the HopFrame API
/// </summary> /// </summary>
/// <param name="service">The service provider to add the services to</param> /// <param name="service">The service provider to add the services to</param>
/// <typeparam name="TDbContext">The database object that saves all entities that are important for the security api</typeparam> /// <param name="configuration">The configuration used to configure HopFrame authentication</param>
/// <returns></returns> /// <returns></returns>
public static IServiceCollection AddHopFrameAuthentication(this IServiceCollection service) { public static IServiceCollection AddHopFrameAuthentication(this IServiceCollection service, ConfigurationManager configuration) {
service.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>(); service.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
service.AddScoped<ITokenContext, TokenContextImplementor>(); service.AddScoped<ITokenContext, TokenContextImplementor>();
service.AddOptionsFromConfiguration<HopFrameAuthenticationOptions>(configuration);
service.AddOptionsFromConfiguration<AdminPermissionOptions>(configuration);
service.AddAuthentication(HopFrameAuthentication.SchemeName).AddScheme<AuthenticationSchemeOptions, HopFrameAuthentication>(HopFrameAuthentication.SchemeName, _ => {}); service.AddAuthentication(HopFrameAuthentication.SchemeName).AddScheme<AuthenticationSchemeOptions, HopFrameAuthentication>(HopFrameAuthentication.SchemeName, _ => {});
service.AddAuthorization(); service.AddAuthorization();

View File

@@ -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; }
}
}

View File

@@ -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; }
}
}

View File

@@ -0,0 +1,5 @@
namespace HopFrame.Security.Options;
public abstract class OptionsFromConfiguration {
public abstract string Position { get; }
}

View File

@@ -0,0 +1,19 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace HopFrame.Security.Options;
public static class OptionsFromConfigurationExtensions {
public static void AddOptionsFromConfiguration<T>(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<T>)(options => {
IConfigurationSection section = configuration.GetSection(position);
section.Bind(options);
}));
}
}

View File

@@ -8,6 +8,6 @@ public sealed class AdminPermissionsAttribute(string view = null, string create
Create = create, Create = create,
Update = update, Update = update,
Delete = delete, Delete = delete,
View = view Read = view
}; };
} }

View File

@@ -24,7 +24,7 @@ public interface IAdminPageGenerator<TModel> {
/// </summary> /// </summary>
/// <param name="permission">the specified permission</param> /// <param name="permission">the specified permission</param>
/// <returns></returns> /// <returns></returns>
IAdminPageGenerator<TModel> ViewPermission(string permission); IAdminPageGenerator<TModel> ReadPermission(string permission);
/// <summary> /// <summary>
/// Sets the permission needed to create a new Entry /// Sets the permission needed to create a new Entry

View File

@@ -48,8 +48,8 @@ internal sealed class AdminPageGenerator<TModel> : IAdminPageGenerator<TModel>,
return this; return this;
} }
public IAdminPageGenerator<TModel> ViewPermission(string permission) { public IAdminPageGenerator<TModel> ReadPermission(string permission) {
Page.Permissions.View = permission; Page.Permissions.Read = permission;
return this; return this;
} }
@@ -165,7 +165,7 @@ internal sealed class AdminPageGenerator<TModel> : IAdminPageGenerator<TModel>,
var attribute = attributes.Single(a => a is AdminPermissionsAttribute) as AdminPermissionsAttribute; var attribute = attributes.Single(a => a is AdminPermissionsAttribute) as AdminPermissionsAttribute;
CreatePermission(attribute?.Permissions.Create); CreatePermission(attribute?.Permissions.Create);
UpdatePermission(attribute?.Permissions.Update); UpdatePermission(attribute?.Permissions.Update);
ViewPermission(attribute?.Permissions.View); ReadPermission(attribute?.Permissions.Read);
DeletePermission(attribute?.Permissions.Delete); DeletePermission(attribute?.Permissions.Delete);
} }

View File

@@ -1,7 +1,7 @@
namespace HopFrame.Web.Admin.Models; namespace HopFrame.Web.Admin.Models;
public sealed class AdminPagePermissions { public sealed class AdminPagePermissions {
public string View { get; set; } public string Read { get; set; }
public string Create { get; set; } public string Create { get; set; }
public string Update { get; set; } public string Update { get; set; }
public string Delete { get; set; } public string Delete { get; set; }

View File

@@ -1,15 +1,17 @@
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using HopFrame.Database.Models; using HopFrame.Database.Models;
using HopFrame.Security; using HopFrame.Security;
using HopFrame.Security.Authorization;
using HopFrame.Web.Admin; using HopFrame.Web.Admin;
using HopFrame.Web.Admin.Attributes; using HopFrame.Web.Admin.Attributes;
using HopFrame.Web.Admin.Generators; using HopFrame.Web.Admin.Generators;
using HopFrame.Web.Admin.Models; using HopFrame.Web.Admin.Models;
using HopFrame.Web.Provider; using HopFrame.Web.Provider;
using Microsoft.Extensions.Options;
namespace HopFrame.Web; namespace HopFrame.Web;
internal class HopAdminContext : AdminPagesContext { internal class HopAdminContext(IOptions<AdminPermissionOptions> options) : AdminPagesContext {
[AdminPageUrl("users")] [AdminPageUrl("users")]
public AdminPage<User> Users { get; set; } public AdminPage<User> Users { get; set; }
@@ -21,10 +23,10 @@ internal class HopAdminContext : AdminPagesContext {
generator.Page<User>() generator.Page<User>()
.Description("On this page you can manage all user accounts.") .Description("On this page you can manage all user accounts.")
.ConfigureProvider<UserProvider>() .ConfigureProvider<UserProvider>()
.ViewPermission(AdminPermissions.ViewUsers) .ReadPermission(options.Value.Users.Read)
.CreatePermission(AdminPermissions.AddUser) .CreatePermission(options.Value.Users.Create)
.UpdatePermission(AdminPermissions.EditUser) .UpdatePermission(options.Value.Users.Update)
.DeletePermission(AdminPermissions.DeleteUser); .DeletePermission(options.Value.Users.Delete);
generator.Page<User>().Property(u => u.Password) generator.Page<User>().Property(u => u.Password)
.DisplayInListing(false) .DisplayInListing(false)
@@ -64,10 +66,10 @@ internal class HopAdminContext : AdminPagesContext {
generator.Page<PermissionGroup>() generator.Page<PermissionGroup>()
.Description("On this page you can view, create, edit and delete permission groups.") .Description("On this page you can view, create, edit and delete permission groups.")
.ConfigureProvider<GroupProvider>() .ConfigureProvider<GroupProvider>()
.ViewPermission(AdminPermissions.ViewGroups) .ReadPermission(options.Value.Groups.Read)
.CreatePermission(AdminPermissions.AddGroup) .CreatePermission(options.Value.Groups.Create)
.UpdatePermission(AdminPermissions.EditGroup) .UpdatePermission(options.Value.Groups.Update)
.DeletePermission(AdminPermissions.DeleteGroup) .DeletePermission(options.Value.Groups.Delete)
.ListingProperty(g => g.Name); .ListingProperty(g => g.Name);
generator.Page<PermissionGroup>().Property(g => g.Name) generator.Page<PermissionGroup>().Property(g => g.Name)

View File

@@ -5,25 +5,26 @@
@using BlazorStrap @using BlazorStrap
@using HopFrame.Web.Pages.Administration.Layout @using HopFrame.Web.Pages.Administration.Layout
@using BlazorStrap.V5 @using BlazorStrap.V5
@using HopFrame.Security @using HopFrame.Security.Authorization
@using HopFrame.Web.Admin.Providers @using HopFrame.Web.Admin.Providers
@using HopFrame.Web.Components @using HopFrame.Web.Components
@using Microsoft.AspNetCore.Components.Web @using Microsoft.AspNetCore.Components.Web
@using Microsoft.Extensions.Options
@layout AdminLayout @layout AdminLayout
<AuthorizedView Permission="@AdminPermissions.IsAdmin" RedirectIfUnauthorized="/administration/login" /> <AuthorizedView Permission="@Options.Value.Dashboard" RedirectIfUnauthorized="/administration/login" />
<PageTitle>Admin Dashboard</PageTitle> <PageTitle>Admin Dashboard</PageTitle>
<BSContainer> <BSContainer>
<BSRow Justify="Justify.Center"> <BSRow Justify="Justify.Center">
@foreach (var adminPage in Pages.LoadRegisteredAdminPages()) { @foreach (var adminPage in Pages.LoadRegisteredAdminPages()) {
<AuthorizedView Permission="@adminPage.Permissions.View"> <AuthorizedView Permission="@adminPage.Permissions.Read">
<BSCol Column="4" style="margin-bottom: 10px"> <BSCol Column="4" style="margin-bottom: 10px">
<BSCard CardType="CardType.Card" Color="BSColor.Dark" style="min-height: 200px; min-width: 200px"> <BSCard CardType="CardType.Card" Color="BSColor.Dark" style="min-height: 200px; min-width: 200px">
<BSCard CardType="CardType.Body" style="display: flex; flex-direction: column"> <BSCard CardType="CardType.Body" style="display: flex; flex-direction: column">
<BSCard CardType="CardType.Title">@adminPage.Title</BSCard> <BSCard CardType="CardType.Title">@adminPage.Title</BSCard>
<BSCard CardType="CardType.Subtitle"><span style="color: gray">@adminPage.Permissions.View</span></BSCard> <BSCard CardType="CardType.Subtitle"><span style="color: gray">@adminPage.Permissions.Read</span></BSCard>
<BSCard CardType="CardType.Text">@adminPage.Description</BSCard> <BSCard CardType="CardType.Text">@adminPage.Description</BSCard>
<BSButton IsOutlined="true" MarginTop="Margins.Auto" style="width: max-content; align-self: center" OnClick="() => NavigateTo(adminPage.Url)" Color="BSColor.Light">Open</BSButton> <BSButton IsOutlined="true" MarginTop="Margins.Auto" style="width: max-content; align-self: center" OnClick="() => NavigateTo(adminPage.Url)" Color="BSColor.Light">Open</BSButton>
</BSCard> </BSCard>
@@ -36,6 +37,7 @@
@inject NavigationManager Navigator @inject NavigationManager Navigator
@inject IAdminPagesProvider Pages @inject IAdminPagesProvider Pages
@inject IOptions<AdminPermissionOptions> Options
@code { @code {

View File

@@ -18,7 +18,7 @@
@using HopFrame.Web.Components @using HopFrame.Web.Components
<PageTitle>@_pageData.Title</PageTitle> <PageTitle>@_pageData.Title</PageTitle>
<AuthorizedView Permission="@_pageData.Permissions.View" RedirectIfUnauthorized="@GenerateRedirectString()" /> <AuthorizedView Permission="@_pageData.Permissions.Read" RedirectIfUnauthorized="@GenerateRedirectString()" />
<AdminPageModal ReloadDelegate="Reload" @ref="_modal"/> <AdminPageModal ReloadDelegate="Reload" @ref="_modal"/>

View File

@@ -24,7 +24,7 @@
<BSNavItem IsActive="IsDashboardActive()" OnClick="NavigateToDashboard">Dashboard</BSNavItem> <BSNavItem IsActive="IsDashboardActive()" OnClick="NavigateToDashboard">Dashboard</BSNavItem>
@foreach (var adminPage in Pages.LoadRegisteredAdminPages()) { @foreach (var adminPage in Pages.LoadRegisteredAdminPages()) {
<AuthorizedView Permission="@adminPage.Permissions.View"> <AuthorizedView Permission="@adminPage.Permissions.Read">
<BSNavItem IsActive="IsNavItemActive(adminPage.Url)" OnClick="() => Navigate(adminPage.Url)">@adminPage.Title</BSNavItem> <BSNavItem IsActive="IsNavItemActive(adminPage.Url)" OnClick="() => Navigate(adminPage.Url)">@adminPage.Title</BSNavItem>
</AuthorizedView> </AuthorizedView>
} }

View File

@@ -6,12 +6,13 @@ using HopFrame.Web.Admin;
using HopFrame.Web.Services; using HopFrame.Web.Services;
using HopFrame.Web.Services.Implementation; using HopFrame.Web.Services.Implementation;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
namespace HopFrame.Web; namespace HopFrame.Web;
public static class ServiceCollectionExtensions { public static class ServiceCollectionExtensions {
public static IServiceCollection AddHopFrame<TDbContext>(this IServiceCollection services) where TDbContext : HopDbContextBase { public static IServiceCollection AddHopFrame<TDbContext>(this IServiceCollection services, ConfigurationManager configuration) where TDbContext : HopDbContextBase {
services.AddHttpClient(); services.AddHttpClient();
services.AddHopFrameRepositories<TDbContext>(); services.AddHopFrameRepositories<TDbContext>();
services.AddScoped<IAuthService, AuthService>(); services.AddScoped<IAuthService, AuthService>();
@@ -22,7 +23,7 @@ public static class ServiceCollectionExtensions {
services.AddSweetAlert2(); services.AddSweetAlert2();
services.AddBlazorStrap(); services.AddBlazorStrap();
services.AddHopFrameAuthentication(); services.AddHopFrameAuthentication(configuration);
return services; return services;
} }

View File

@@ -4,6 +4,7 @@ using HopFrame.Security.Authentication;
using HopFrame.Security.Claims; using HopFrame.Security.Claims;
using HopFrame.Security.Models; using HopFrame.Security.Models;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
namespace HopFrame.Web.Services.Implementation; namespace HopFrame.Web.Services.Implementation;
@@ -11,7 +12,8 @@ internal class AuthService(
IUserRepository userService, IUserRepository userService,
IHttpContextAccessor httpAccessor, IHttpContextAccessor httpAccessor,
ITokenRepository tokens, ITokenRepository tokens,
ITokenContext context) ITokenContext context,
IOptions<HopFrameAuthenticationOptions> options)
: IAuthService { : IAuthService {
public async Task Register(UserRegister register) { public async Task Register(UserRegister register) {
@@ -27,12 +29,12 @@ internal class AuthService(
var accessToken = await tokens.CreateToken(Token.AccessTokenType, user); var accessToken = await tokens.CreateToken(Token.AccessTokenType, user);
httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Content.ToString(), new CookieOptions { httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Content.ToString(), new CookieOptions {
MaxAge = HopFrameAuthentication.RefreshTokenTime, MaxAge = options.Value.RefreshTokenTime,
HttpOnly = true, HttpOnly = true,
Secure = true Secure = true
}); });
httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions { httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions {
MaxAge = HopFrameAuthentication.AccessTokenTime, MaxAge = options.Value.AccessTokenTime,
HttpOnly = false, HttpOnly = false,
Secure = true Secure = true
}); });
@@ -48,12 +50,12 @@ internal class AuthService(
var accessToken = await tokens.CreateToken(Token.AccessTokenType, user); var accessToken = await tokens.CreateToken(Token.AccessTokenType, user);
httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Content.ToString(), new CookieOptions { httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Content.ToString(), new CookieOptions {
MaxAge = HopFrameAuthentication.RefreshTokenTime, MaxAge = options.Value.RefreshTokenTime,
HttpOnly = true, HttpOnly = true,
Secure = true Secure = true
}); });
httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions { httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions {
MaxAge = HopFrameAuthentication.AccessTokenTime, MaxAge = options.Value.AccessTokenTime,
HttpOnly = false, HttpOnly = false,
Secure = true Secure = true
}); });
@@ -77,12 +79,12 @@ internal class AuthService(
if (token is null || token.Type != Token.RefreshTokenType) return null; 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); var accessToken = await tokens.CreateToken(Token.AccessTokenType, token.Owner);
httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions { httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions {
MaxAge = HopFrameAuthentication.AccessTokenTime, MaxAge = options.Value.AccessTokenTime,
HttpOnly = false, HttpOnly = false,
Secure = true Secure = true
}); });
@@ -95,7 +97,7 @@ internal class AuthService(
if (accessToken is null) return false; if (accessToken is null) return false;
if (accessToken.Type != Token.AccessTokenType) 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; if (accessToken.Owner is null) return false;
return true; return true;

View File

@@ -7,7 +7,7 @@ var builder = WebApplication.CreateBuilder(args);
// Add services to the container. // Add services to the container.
builder.Services.AddControllers(); builder.Services.AddControllers();
builder.Services.AddHopFrame<DatabaseContext>(); builder.Services.AddHopFrame<DatabaseContext>(builder.Configuration);
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer(); builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(); builder.Services.AddSwaggerGen();

View File

@@ -6,7 +6,7 @@ using HopFrame.Web.Admin;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<DatabaseContext>(); builder.Services.AddDbContext<DatabaseContext>();
builder.Services.AddHopFrame<DatabaseContext>(); builder.Services.AddHopFrame<DatabaseContext>(builder.Configuration);
builder.Services.AddAdminContext<AdminContext>(); builder.Services.AddAdminContext<AdminContext>();
// Add services to the container. // Add services to the container.

View File

@@ -10,6 +10,7 @@ using HopFrame.Security.Authentication;
using HopFrame.Security.Claims; using HopFrame.Security.Claims;
using HopFrame.Security.Models; using HopFrame.Security.Models;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Moq; using Moq;
namespace HopFrame.Tests.Api; namespace HopFrame.Tests.Api;
@@ -75,7 +76,7 @@ public class AuthLogicTests {
.Setup(c => c.User) .Setup(c => c.User)
.Returns(CreateDummyUser()); .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<HopFrameAuthenticationOptions>(new HopFrameAuthenticationOptions())), accessor.HttpContext);
} }
private User CreateDummyUser() => new() { private User CreateDummyUser() => new() {

View File

@@ -46,7 +46,7 @@ public class AuthenticationTests {
.Setup(x => x.GetFullPermissions(It.IsAny<User>())) .Setup(x => x.GetFullPermissions(It.IsAny<User>()))
.ReturnsAsync(new List<string>()); .ReturnsAsync(new List<string>());
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<HopFrameAuthenticationOptions>(new HopFrameAuthenticationOptions()));
var context = new DefaultHttpContext(); var context = new DefaultHttpContext();
if (provideCorrectToken) if (provideCorrectToken)
context.HttpContext.Request.Headers.Append(HopFrameAuthentication.SchemeName, correctToken.Content.ToString()); context.HttpContext.Request.Headers.Append(HopFrameAuthentication.SchemeName, correctToken.Content.ToString());

View File

@@ -1,11 +1,13 @@
using HopFrame.Database.Models; using HopFrame.Database.Models;
using HopFrame.Database.Repositories; using HopFrame.Database.Repositories;
using HopFrame.Security.Authentication;
using HopFrame.Security.Claims; using HopFrame.Security.Claims;
using HopFrame.Security.Models; using HopFrame.Security.Models;
using HopFrame.Tests.Web.Extensions; using HopFrame.Tests.Web.Extensions;
using HopFrame.Web.Services; using HopFrame.Web.Services;
using HopFrame.Web.Services.Implementation; using HopFrame.Web.Services.Implementation;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Moq; using Moq;
namespace HopFrame.Tests.Web; namespace HopFrame.Tests.Web;
@@ -66,7 +68,7 @@ public class AuthServiceTests {
.Setup(c => c.AccessToken) .Setup(c => c.AccessToken)
.Returns(providedAccessToken); .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<HopFrameAuthenticationOptions>(new HopFrameAuthenticationOptions())), accessor.HttpContext);
} }
private User CreateDummyUser() => new() { private User CreateDummyUser() => new() {