Resolve "API tokens" #41

Merged
leon.hoppe merged 4 commits from feature/tokens into dev 2024-12-21 17:36:13 +01:00
13 changed files with 58 additions and 21 deletions
Showing only changes of commit 59c452ff73 - Show all commits

14
.idea/.idea.HopFrame/.idea/discord.xml generated Normal file
View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DiscordProjectSettings">
<option name="show" value="PROJECT_FILES" />
<option name="description" value="" />
<option name="applicationTheme" value="default" />
<option name="iconsTheme" value="default" />
<option name="button1Title" value="" />
<option name="button1Url" value="" />
<option name="button2Title" value="" />
<option name="button2Url" value="" />
<option name="customApplicationId" value="" />
</component>
</project>

View File

@@ -70,6 +70,7 @@
</wpf:ResourceDictionary> </wpf:ResourceDictionary>

View File

@@ -6,5 +6,6 @@ public interface ITokenRepository {
Task<Token> GetToken(string content); Task<Token> GetToken(string content);
Task<Token> CreateToken(int type, User owner); Task<Token> CreateToken(int type, User owner);
Task DeleteUserTokens(User owner); Task DeleteUserTokens(User owner);
Task DeleteToken(Token token);
Task<Token> CreateApiToken(User owner, DateTime expirationDate); Task<Token> CreateApiToken(User owner, DateTime expirationDate);
} }

View File

@@ -70,6 +70,10 @@ internal sealed class PermissionRepository<TDbContext>(TDbContext context, IGrou
public async Task<IList<string>> GetFullPermissions(IPermissionOwner owner) { public async Task<IList<string>> GetFullPermissions(IPermissionOwner owner) {
var permissions = new List<string>(); var permissions = new List<string>();
if (owner is Token token && token.Type != Token.ApiTokenType) {
owner = token.Owner;
}
if (owner is User user) { if (owner is User user) {
var perms = await context.Permissions var perms = await context.Permissions
.Include(p => p.User) .Include(p => p.User)
@@ -86,11 +90,11 @@ internal sealed class PermissionRepository<TDbContext>(TDbContext context, IGrou
.ToListAsync(); .ToListAsync();
permissions.AddRange(perms.Select(p => p.PermissionName)); permissions.AddRange(perms.Select(p => p.PermissionName));
}else if (owner is Token token) { }else if (owner is Token apiToken) {
var perms = await context.Permissions var perms = await context.Permissions
.Include(p => p.Token) .Include(p => p.Token)
.Where(p => p.Token != null) .Where(p => p.Token != null)
.Where(p =>p.Token.TokenId == token.TokenId) .Where(p =>p.Token.TokenId == apiToken.TokenId)
.ToListAsync(); .ToListAsync();
permissions.AddRange(perms.Select(p => p.PermissionName)); permissions.AddRange(perms.Select(p => p.PermissionName));

View File

@@ -39,6 +39,11 @@ internal sealed class TokenRepository<TDbContext>(TDbContext context) : ITokenRe
await context.SaveChangesAsync(); await context.SaveChangesAsync();
} }
public async Task DeleteToken(Token token) {
context.Tokens.Remove(token);
await context.SaveChangesAsync();
}
public async Task<Token> CreateApiToken(User owner, DateTime expirationDate) { public async Task<Token> CreateApiToken(User owner, DateTime expirationDate) {
var token = new Token { var token = new Token {
CreatedAt = expirationDate, CreatedAt = expirationDate,

View File

@@ -47,15 +47,7 @@ public class HopFrameAuthentication(
new(HopFrameClaimTypes.UserId, tokenEntry.Owner.Id.ToString()) new(HopFrameClaimTypes.UserId, tokenEntry.Owner.Id.ToString())
}; };
IList<string> permissions; var permissions = await perms.GetFullPermissions(tokenEntry);
if (tokenEntry.Type == Token.ApiTokenType) {
permissions = await perms.GetFullPermissions(tokenEntry);
}
else {
permissions = await perms.GetFullPermissions(tokenEntry.Owner);
}
claims.AddRange(permissions.Select(perm => new Claim(HopFrameClaimTypes.Permission, perm))); claims.AddRange(permissions.Select(perm => new Claim(HopFrameClaimTypes.Permission, perm)));
var principal = new ClaimsPrincipal(); var principal = new ClaimsPrincipal();

View File

@@ -21,4 +21,6 @@ public interface ITokenContext {
/// The access token the user provided /// The access token the user provided
/// </summary> /// </summary>
Token AccessToken { get; } Token AccessToken { get; }
IList<string> ContextualPermissions { get; }
} }

View File

@@ -4,10 +4,12 @@ using Microsoft.AspNetCore.Http;
namespace HopFrame.Security.Claims; namespace HopFrame.Security.Claims;
internal sealed class TokenContextImplementor(IHttpContextAccessor accessor, IUserRepository users, ITokenRepository tokens) : ITokenContext { internal sealed class TokenContextImplementor(IHttpContextAccessor accessor, IUserRepository users, ITokenRepository tokens, IPermissionRepository permissions) : ITokenContext {
public bool IsAuthenticated => !string.IsNullOrEmpty(accessor.HttpContext?.User.GetAccessTokenId()); public bool IsAuthenticated => !string.IsNullOrEmpty(accessor.HttpContext?.User.GetAccessTokenId());
public User User => users.GetUser(Guid.Parse(accessor.HttpContext?.User.GetUserId() ?? Guid.Empty.ToString())).GetAwaiter().GetResult(); public User User => users.GetUser(Guid.Parse(accessor.HttpContext?.User.GetUserId() ?? Guid.Empty.ToString())).GetAwaiter().GetResult();
public Token AccessToken => tokens.GetToken(accessor.HttpContext?.User.GetAccessTokenId()).GetAwaiter().GetResult(); public Token AccessToken => tokens.GetToken(accessor.HttpContext?.User.GetAccessTokenId()).GetAwaiter().GetResult();
public IList<string> ContextualPermissions => permissions.GetFullPermissions(AccessToken).GetAwaiter().GetResult();
} }

View File

@@ -26,7 +26,7 @@ public sealed class AuthMiddleware(IAuthService auth, IPermissionRepository perm
new(HopFrameClaimTypes.UserId, token.Owner.Id.ToString()) new(HopFrameClaimTypes.UserId, token.Owner.Id.ToString())
}; };
var permissions = await perms.GetFullPermissions(token.Owner); var permissions = await perms.GetFullPermissions(token);
claims.AddRange(permissions.Select(perm => new Claim(HopFrameClaimTypes.Permission, perm))); claims.AddRange(permissions.Select(perm => new Claim(HopFrameClaimTypes.Permission, perm)));
context.User.AddIdentity(new ClaimsIdentity(claims, HopFrameAuthentication.SchemeName)); context.User.AddIdentity(new ClaimsIdentity(claims, HopFrameAuthentication.SchemeName));

View File

@@ -321,7 +321,7 @@
private async void Save() { private async void Save() {
if (_isEdit && _currentPage.Permissions.Update is not null) { if (_isEdit && _currentPage.Permissions.Update is not null) {
if (!await Permissions.HasPermission(Auth.User, _currentPage.Permissions.Update)) { if (!await Permissions.HasPermission(Auth.AccessToken, _currentPage.Permissions.Update)) {
await Alerts.FireAsync(new SweetAlertOptions { await Alerts.FireAsync(new SweetAlertOptions {
Title = "Unauthorized!", Title = "Unauthorized!",
Text = "You don't have the required permissions to edit an entry!", Text = "You don't have the required permissions to edit an entry!",
@@ -330,7 +330,7 @@
return; return;
} }
}else if (_currentPage.Permissions.Create is not null) { }else if (_currentPage.Permissions.Create is not null) {
if (!await Permissions.HasPermission(Auth.User, _currentPage.Permissions.Create)) { if (!await Permissions.HasPermission(Auth.AccessToken, _currentPage.Permissions.Create)) {
await Alerts.FireAsync(new SweetAlertOptions { await Alerts.FireAsync(new SweetAlertOptions {
Title = "Unauthorized!", Title = "Unauthorized!",
Text = "You don't have the required permissions to add an entry!", Text = "You don't have the required permissions to add an entry!",

View File

@@ -140,8 +140,8 @@
throw new ArgumentException($"AdminPage '{_pageData.Title}' does not specify a model repository!'"); throw new ArgumentException($"AdminPage '{_pageData.Title}' does not specify a model repository!'");
_modelProvider = _pageData.LoadModelProvider(Provider); _modelProvider = _pageData.LoadModelProvider(Provider);
_hasEditPermission = _pageData.Permissions.Update is null || await Permissions.HasPermission(Auth.User, _pageData.Permissions.Update); _hasEditPermission = _pageData.Permissions.Update is null || await Permissions.HasPermission(Auth.AccessToken, _pageData.Permissions.Update);
_hasDeletePermission = _pageData.Permissions.Delete is null || await Permissions.HasPermission(Auth.User, _pageData.Permissions.Delete); _hasDeletePermission = _pageData.Permissions.Delete is null || await Permissions.HasPermission(Auth.AccessToken, _pageData.Permissions.Delete);
await Reload(); await Reload();
} }

View File

@@ -1,5 +1,7 @@
using HopFrame.Api.Logic; using HopFrame.Api.Logic;
using HopFrame.Api.Models;
using HopFrame.Database.Models; using HopFrame.Database.Models;
using HopFrame.Database.Repositories;
using HopFrame.Security.Authorization; using HopFrame.Security.Authorization;
using HopFrame.Security.Claims; using HopFrame.Security.Claims;
using HopFrame.Testing.Api.Models; using HopFrame.Testing.Api.Models;
@@ -10,11 +12,11 @@ namespace HopFrame.Testing.Api.Controllers;
[ApiController] [ApiController]
[Route("test")] [Route("test")]
public class TestController(ITokenContext userContext, DatabaseContext context) : ControllerBase { public class TestController(ITokenContext userContext, DatabaseContext context, ITokenRepository tokens, IPermissionRepository permissions) : ControllerBase {
[HttpGet("permissions"), Authorized] [HttpGet("permissions"), Authorized]
public ActionResult<IList<Permission>> Permissions() { public ActionResult<IList<string>> Permissions() {
return new ActionResult<IList<Permission>>(userContext.User.Permissions); return new ActionResult<IList<string>>(userContext.ContextualPermissions);
} }
[HttpGet("generate")] [HttpGet("generate")]
@@ -51,4 +53,18 @@ public class TestController(ITokenContext userContext, DatabaseContext context)
return LogicResult<IList<Address>>.Ok(await context.Addresses.Include(e => e.Employee).ToListAsync()); return LogicResult<IList<Address>>.Ok(await context.Addresses.Include(e => e.Employee).ToListAsync());
} }
[HttpGet("token"), Authorized]
public async Task<ActionResult<SingleValueResult<string>>> GetApiToken() {
var token = await tokens.CreateApiToken(userContext.User, DateTime.MaxValue);
await permissions.AddPermission(token, "hopframe.admin");
await permissions.AddPermission(token, "hopframe.admin.users.read");
return LogicResult<SingleValueResult<string>>.Ok(token.TokenId.ToString());
}
[HttpDelete("token/{tokenId}")]
public async Task DeleteToken(string tokenId) {
var token = await tokens.GetToken(tokenId);
await tokens.DeleteToken(token);
}
} }

View File

@@ -18,7 +18,7 @@ builder.Services.AddSwaggerGen(c => {
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme { c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme {
Description = @"JWT Authorization header using the Bearer scheme. \r\n\r\n Description = @"JWT Authorization header using the Bearer scheme. \r\n\r\n
Enter 'Bearer' [space] and then your token in the text input below.", Enter 'Bearer' [space] and then your token in the text input below.",
Name = "Authorization", Name = "Token",
In = ParameterLocation.Header, In = ParameterLocation.Header,
Type = SecuritySchemeType.ApiKey, Type = SecuritySchemeType.ApiKey,
Scheme = "Bearer" Scheme = "Bearer"