diff --git a/.idea/.idea.HopFrame/.idea/dataSources.xml b/.idea/.idea.HopFrame/.idea/dataSources.xml
index ded00e9..3e820ee 100644
--- a/.idea/.idea.HopFrame/.idea/dataSources.xml
+++ b/.idea/.idea.HopFrame/.idea/dataSources.xml
@@ -5,7 +5,7 @@
sqlite.xerial
true
org.sqlite.JDBC
- jdbc:sqlite:$PROJECT_DIR$/test/RestApiTest/bin/Debug/net8.0/test.db
+ jdbc:sqlite:C:\Users\leon\Documents\Projekte\HopFrame\testing\HopFrame.Testing.Api\bin\Debug\net8.0\test.db
diff --git a/.idea/.idea.HopFrame/.idea/discord.xml b/.idea/.idea.HopFrame/.idea/discord.xml
new file mode 100644
index 0000000..912db82
--- /dev/null
+++ b/.idea/.idea.HopFrame/.idea/discord.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/HopFrame.sln.DotSettings.user b/HopFrame.sln.DotSettings.user
index 1e30ef8..0d4f6c6 100644
--- a/HopFrame.sln.DotSettings.user
+++ b/HopFrame.sln.DotSettings.user
@@ -1,4 +1,5 @@
+ ForceIncluded
ForceIncluded
ForceIncluded
ForceIncluded
@@ -67,6 +68,9 @@
+
+
+
diff --git a/docs/authentication.md b/docs/authentication.md
index 469ceee..c3489d0 100644
--- a/docs/authentication.md
+++ b/docs/authentication.md
@@ -23,8 +23,6 @@ by configuring your configuration to load these.
> `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.
@@ -49,3 +47,28 @@ HOPFRAME__AUTHENTICATION__ACCESSTOKEN__MINUTES=30
HOPFRAME__AUTHENTICATION__REFRESHTOKEN__DAYS=10
HOPFRAME__AUTHENTICATION__REFRESHTOKEN__HOURS=5
```
+
+## API tokens
+
+API tokens are useful to use in automation environments that need to access an endpoint or page of your application.
+The HopFrame supports this natively and no further configuration is required in order to use them.
+
+### Create an api token
+
+You can create an api token via the `ITokenRepository`:
+```csharp
+tokens.CreateApiToken(user, DateTime.MaxValue);
+```
+
+This creates a new api token that is valid until the provided DateTime has passed. Note that in the database and the token
+model the `CreatedAt` property represents the expiration date on an api token. For security reasons the api token by default
+has no permissions. This allows you to create tokens that are just permitted to perform a single action. Note that an api token
+can **never** have more permissions than the user associated with it.
+
+### Add permissions to an api token
+
+You can add permissions to an api token like you would to a normal user or group:
+
+```csharp
+permissions.AddPermission(apiToken, "token.permission");
+```
diff --git a/docs/models.md b/docs/models.md
index 39ecc99..7f61e86 100644
--- a/docs/models.md
+++ b/docs/models.md
@@ -35,16 +35,18 @@ public class Permission {
public DateTime GrantedAt { get; set; }
public virtual User User { get; set; }
public virtual PermissionGroup Group { get; set; }
+ public virtual Token Token { get; set; }
}
```
## Token
```csharp
-public class Token {
+public class Token : IPermissionOwner {
public int Type { get; set; }
public Guid Content { get; set; }
public DateTime CreatedAt { get; set; }
public virtual User Owner { get; set; }
+ public virtual List Permissions { get; set; }
}
```
diff --git a/docs/repositories.md b/docs/repositories.md
index 25cb4ac..f72d876 100644
--- a/docs/repositories.md
+++ b/docs/repositories.md
@@ -71,5 +71,9 @@ public interface ITokenRepository {
Task CreateToken(int type, User owner);
Task DeleteUserTokens(User owner);
+
+ Task DeleteToken(Token token);
+
+ Task CreateApiToken(User owner, DateTime expirationDate);
}
```
diff --git a/src/HopFrame.Api/Logic/Implementation/AuthLogic.cs b/src/HopFrame.Api/Logic/Implementation/AuthLogic.cs
index acf8fb7..61e9681 100644
--- a/src/HopFrame.Api/Logic/Implementation/AuthLogic.cs
+++ b/src/HopFrame.Api/Logic/Implementation/AuthLogic.cs
@@ -23,18 +23,18 @@ internal class AuthLogic(IUserRepository users, ITokenRepository tokens, ITokenC
var refreshToken = await tokens.CreateToken(Token.RefreshTokenType, 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.TokenId.ToString(), new CookieOptions {
MaxAge = options.Value.RefreshTokenTime,
HttpOnly = true,
Secure = true
});
- accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions {
+ accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.TokenId.ToString(), new CookieOptions {
MaxAge = options.Value.AccessTokenTime,
HttpOnly = true,
Secure = true
});
- return LogicResult>.Ok(accessToken.Content.ToString());
+ return LogicResult>.Ok(accessToken.TokenId.ToString());
}
public async Task>> Register(UserRegister register) {
@@ -54,18 +54,18 @@ internal class AuthLogic(IUserRepository users, ITokenRepository tokens, ITokenC
var refreshToken = await tokens.CreateToken(Token.RefreshTokenType, 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.TokenId.ToString(), new CookieOptions {
MaxAge = options.Value.RefreshTokenTime,
HttpOnly = true,
Secure = true
});
- accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions {
+ accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.TokenId.ToString(), new CookieOptions {
MaxAge = options.Value.AccessTokenTime,
HttpOnly = false,
Secure = true
});
- return LogicResult>.Ok(accessToken.Content.ToString());
+ return LogicResult>.Ok(accessToken.TokenId.ToString());
}
public async Task>> Authenticate() {
@@ -87,13 +87,13 @@ internal class AuthLogic(IUserRepository users, ITokenRepository tokens, ITokenC
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.TokenId.ToString(), new CookieOptions {
MaxAge = options.Value.AccessTokenTime,
HttpOnly = false,
Secure = true
});
- return LogicResult>.Ok(accessToken.Content.ToString());
+ return LogicResult>.Ok(accessToken.TokenId.ToString());
}
public async Task Logout() {
diff --git a/src/HopFrame.Database/HopDbContextBase.cs b/src/HopFrame.Database/HopDbContextBase.cs
index 21342ea..cd03860 100644
--- a/src/HopFrame.Database/HopDbContextBase.cs
+++ b/src/HopFrame.Database/HopDbContextBase.cs
@@ -30,5 +30,10 @@ public abstract class HopDbContextBase : DbContext {
.HasMany(g => g.Permissions)
.WithOne(p => p.Group)
.OnDelete(DeleteBehavior.Cascade);
+
+ modelBuilder.Entity()
+ .HasMany(t => t.Permissions)
+ .WithOne(t => t.Token)
+ .OnDelete(DeleteBehavior.Cascade);
}
}
\ No newline at end of file
diff --git a/src/HopFrame.Database/Models/Permission.cs b/src/HopFrame.Database/Models/Permission.cs
index db111ba..658a90e 100644
--- a/src/HopFrame.Database/Models/Permission.cs
+++ b/src/HopFrame.Database/Models/Permission.cs
@@ -21,6 +21,9 @@ public class Permission {
[ForeignKey("GroupName"), JsonIgnore]
public virtual PermissionGroup Group { get; set; }
+ [ForeignKey("TokenId"), JsonIgnore]
+ public virtual Token Token { get; set; }
+
}
public interface IPermissionOwner;
diff --git a/src/HopFrame.Database/Models/Token.cs b/src/HopFrame.Database/Models/Token.cs
index a42d367..b22bd21 100644
--- a/src/HopFrame.Database/Models/Token.cs
+++ b/src/HopFrame.Database/Models/Token.cs
@@ -4,24 +4,32 @@ using System.Text.Json.Serialization;
namespace HopFrame.Database.Models;
-public class Token {
+public class Token : IPermissionOwner {
public const int RefreshTokenType = 0;
public const int AccessTokenType = 1;
+ public const int ApiTokenType = 2;
///
/// Defines the Type of the stored Token
/// 0: Refresh token
/// 1: Access token
+ /// 2: Api token
///
[Required, MinLength(1), MaxLength(1)]
public int Type { get; set; }
[Key, Required, MinLength(36), MaxLength(36)]
- public Guid Content { get; set; }
+ public Guid TokenId { get; set; }
+ ///
+ /// Defines the creation date of the token
+ /// In case of an api token it defines the date it becomes invalid
+ ///
[Required]
public DateTime CreatedAt { get; set; }
[ForeignKey("UserId"), JsonIgnore]
public virtual User Owner { get; set; }
+
+ public virtual List Permissions { get; set; }
}
\ No newline at end of file
diff --git a/src/HopFrame.Database/Repositories/ITokenRepository.cs b/src/HopFrame.Database/Repositories/ITokenRepository.cs
index bec3963..9447994 100644
--- a/src/HopFrame.Database/Repositories/ITokenRepository.cs
+++ b/src/HopFrame.Database/Repositories/ITokenRepository.cs
@@ -5,5 +5,7 @@ namespace HopFrame.Database.Repositories;
public interface ITokenRepository {
Task GetToken(string content);
Task CreateToken(int type, User owner);
- Task DeleteUserTokens(User owner);
+ Task DeleteUserTokens(User owner, bool includeApiTokens = false);
+ Task DeleteToken(Token token);
+ Task CreateApiToken(User owner, DateTime expirationDate);
}
\ No newline at end of file
diff --git a/src/HopFrame.Database/Repositories/Implementation/PermissionRepository.cs b/src/HopFrame.Database/Repositories/Implementation/PermissionRepository.cs
index 45bcfd8..3156361 100644
--- a/src/HopFrame.Database/Repositories/Implementation/PermissionRepository.cs
+++ b/src/HopFrame.Database/Repositories/Implementation/PermissionRepository.cs
@@ -5,6 +5,10 @@ namespace HopFrame.Database.Repositories.Implementation;
internal sealed class PermissionRepository(TDbContext context, IGroupRepository groupRepository) : IPermissionRepository where TDbContext : HopDbContextBase {
public async Task HasPermission(IPermissionOwner owner, params string[] permissions) {
+ if (owner is Token { Type: Token.ApiTokenType } token) {
+ if (!await HasPermission(token.Owner, permissions)) return false;
+ }
+
var perms = (await GetFullPermissions(owner)).ToArray();
foreach (var permission in permissions) {
@@ -24,6 +28,12 @@ internal sealed class PermissionRepository(TDbContext context, IGrou
entry.User = user;
}else if (owner is PermissionGroup group) {
entry.Group = group;
+ }else if (owner is Token token) {
+ if (token.Type != Token.ApiTokenType)
+ throw new ArgumentException("Only API tokens can have permissions!");
+ if (!await HasPermission(token.Owner, permission))
+ throw new ArgumentException("An api token cannot have more permissions than the owner has!");
+ entry.Token = token;
}
await context.Permissions.AddAsync(entry);
@@ -48,6 +58,13 @@ internal sealed class PermissionRepository(TDbContext context, IGrou
.Where(p =>p.Group.Name == group.Name)
.Where(p => p.PermissionName == permission)
.SingleOrDefaultAsync();
+ }else if (owner is Token token) {
+ entry = await context.Permissions
+ .Include(p => p.Token)
+ .Where(p => p.Token != null)
+ .Where(p => p.Token.TokenId == token.TokenId)
+ .Where(p => p.PermissionName == permission)
+ .SingleOrDefaultAsync();
}
if (entry is not null) {
@@ -58,6 +75,10 @@ internal sealed class PermissionRepository(TDbContext context, IGrou
public async Task> GetFullPermissions(IPermissionOwner owner) {
var permissions = new List();
+
+ if (owner is Token token && token.Type != Token.ApiTokenType) {
+ owner = token.Owner;
+ }
if (owner is User user) {
var perms = await context.Permissions
@@ -74,6 +95,14 @@ internal sealed class PermissionRepository(TDbContext context, IGrou
.Where(p =>p.Group.Name == group.Name)
.ToListAsync();
+ permissions.AddRange(perms.Select(p => p.PermissionName));
+ }else if (owner is Token apiToken) {
+ var perms = await context.Permissions
+ .Include(p => p.Token)
+ .Where(p => p.Token != null)
+ .Where(p =>p.Token.TokenId == apiToken.TokenId)
+ .ToListAsync();
+
permissions.AddRange(perms.Select(p => p.PermissionName));
}
diff --git a/src/HopFrame.Database/Repositories/Implementation/TokenRepository.cs b/src/HopFrame.Database/Repositories/Implementation/TokenRepository.cs
index 70f727a..29deaab 100644
--- a/src/HopFrame.Database/Repositories/Implementation/TokenRepository.cs
+++ b/src/HopFrame.Database/Repositories/Implementation/TokenRepository.cs
@@ -11,14 +11,14 @@ internal sealed class TokenRepository(TDbContext context) : ITokenRe
return await context.Tokens
.Include(t => t.Owner)
- .Where(t => t.Content == guid)
+ .Where(t => t.TokenId == guid)
.SingleOrDefaultAsync();
}
public async Task CreateToken(int type, User owner) {
var token = new Token {
CreatedAt = DateTime.Now,
- Content = Guid.NewGuid(),
+ TokenId = Guid.NewGuid(),
Type = type,
Owner = owner
};
@@ -29,13 +29,37 @@ internal sealed class TokenRepository(TDbContext context) : ITokenRe
return token;
}
- public async Task DeleteUserTokens(User owner) {
+ public async Task DeleteUserTokens(User owner, bool includeApiTokens = false) {
var tokens = await context.Tokens
.Include(t => t.Owner)
.Where(t => t.Owner.Id == owner.Id)
.ToListAsync();
+
+ if (!includeApiTokens)
+ tokens = tokens
+ .Where(t => t.Type != Token.ApiTokenType)
+ .ToList();
context.Tokens.RemoveRange(tokens);
await context.SaveChangesAsync();
}
+
+ public async Task DeleteToken(Token token) {
+ context.Tokens.Remove(token);
+ await context.SaveChangesAsync();
+ }
+
+ public async Task CreateApiToken(User owner, DateTime expirationDate) {
+ var token = new Token {
+ CreatedAt = expirationDate,
+ TokenId = Guid.NewGuid(),
+ Type = Token.ApiTokenType,
+ Owner = owner
+ };
+
+ await context.Tokens.AddAsync(token);
+ await context.SaveChangesAsync();
+
+ return token;
+ }
}
\ No newline at end of file
diff --git a/src/HopFrame.Security/Authentication/HopFrameAuthentication.cs b/src/HopFrame.Security/Authentication/HopFrameAuthentication.cs
index 8b0a3b1..88a95c1 100644
--- a/src/HopFrame.Security/Authentication/HopFrameAuthentication.cs
+++ b/src/HopFrame.Security/Authentication/HopFrameAuthentication.cs
@@ -1,5 +1,6 @@
using System.Security.Claims;
using System.Text.Encodings.Web;
+using HopFrame.Database.Models;
using HopFrame.Database.Repositories;
using HopFrame.Security.Claims;
using Microsoft.AspNetCore.Authentication;
@@ -33,7 +34,10 @@ public class HopFrameAuthentication(
var tokenEntry = await tokens.GetToken(accessToken);
if (tokenEntry is null) return AuthenticateResult.Fail("The provided Access Token does not exist");
- if (tokenEntry.CreatedAt + tokenOptions.Value.AccessTokenTime < DateTime.Now) return AuthenticateResult.Fail("The provided Access Token is expired");
+
+ if (tokenEntry.Type == Token.ApiTokenType) {
+ if (tokenEntry.CreatedAt < DateTime.Now) return AuthenticateResult.Fail("The provided API Token is expired");
+ }else 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");
@@ -43,7 +47,7 @@ public class HopFrameAuthentication(
new(HopFrameClaimTypes.UserId, tokenEntry.Owner.Id.ToString())
};
- var permissions = await perms.GetFullPermissions(tokenEntry.Owner);
+ var permissions = await perms.GetFullPermissions(tokenEntry);
claims.AddRange(permissions.Select(perm => new Claim(HopFrameClaimTypes.Permission, perm)));
var principal = new ClaimsPrincipal();
diff --git a/src/HopFrame.Security/Claims/ITokenContext.cs b/src/HopFrame.Security/Claims/ITokenContext.cs
index 6b5a590..6b052bc 100644
--- a/src/HopFrame.Security/Claims/ITokenContext.cs
+++ b/src/HopFrame.Security/Claims/ITokenContext.cs
@@ -21,4 +21,6 @@ public interface ITokenContext {
/// The access token the user provided
///
Token AccessToken { get; }
+
+ IList ContextualPermissions { get; }
}
\ No newline at end of file
diff --git a/src/HopFrame.Security/Claims/TokenContextImplementor.cs b/src/HopFrame.Security/Claims/TokenContextImplementor.cs
index dd50a08..47fce76 100644
--- a/src/HopFrame.Security/Claims/TokenContextImplementor.cs
+++ b/src/HopFrame.Security/Claims/TokenContextImplementor.cs
@@ -4,10 +4,12 @@ using Microsoft.AspNetCore.Http;
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 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 IList ContextualPermissions => permissions.GetFullPermissions(AccessToken).GetAwaiter().GetResult();
}
\ No newline at end of file
diff --git a/src/HopFrame.Web/AuthMiddleware.cs b/src/HopFrame.Web/AuthMiddleware.cs
index 33e2f52..b5fbc93 100644
--- a/src/HopFrame.Web/AuthMiddleware.cs
+++ b/src/HopFrame.Web/AuthMiddleware.cs
@@ -22,11 +22,11 @@ public sealed class AuthMiddleware(IAuthService auth, IPermissionRepository perm
}
var claims = new List {
- new(HopFrameClaimTypes.AccessTokenId, token.Content.ToString()),
+ new(HopFrameClaimTypes.AccessTokenId, token.TokenId.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)));
context.User.AddIdentity(new ClaimsIdentity(claims, HopFrameAuthentication.SchemeName));
diff --git a/src/HopFrame.Web/Components/Administration/AdminPageModal.razor b/src/HopFrame.Web/Components/Administration/AdminPageModal.razor
index 4876323..4e212b5 100644
--- a/src/HopFrame.Web/Components/Administration/AdminPageModal.razor
+++ b/src/HopFrame.Web/Components/Administration/AdminPageModal.razor
@@ -321,7 +321,7 @@
private async void Save() {
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 {
Title = "Unauthorized!",
Text = "You don't have the required permissions to edit an entry!",
@@ -330,7 +330,7 @@
return;
}
}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 {
Title = "Unauthorized!",
Text = "You don't have the required permissions to add an entry!",
diff --git a/src/HopFrame.Web/Pages/Administration/AdminPageList.razor b/src/HopFrame.Web/Pages/Administration/AdminPageList.razor
index d796aeb..1086918 100644
--- a/src/HopFrame.Web/Pages/Administration/AdminPageList.razor
+++ b/src/HopFrame.Web/Pages/Administration/AdminPageList.razor
@@ -140,8 +140,8 @@
throw new ArgumentException($"AdminPage '{_pageData.Title}' does not specify a model repository!'");
_modelProvider = _pageData.LoadModelProvider(Provider);
- _hasEditPermission = _pageData.Permissions.Update is null || await Permissions.HasPermission(Auth.User, _pageData.Permissions.Update);
- _hasDeletePermission = _pageData.Permissions.Delete is null || await Permissions.HasPermission(Auth.User, _pageData.Permissions.Delete);
+ _hasEditPermission = _pageData.Permissions.Update is null || await Permissions.HasPermission(Auth.AccessToken, _pageData.Permissions.Update);
+ _hasDeletePermission = _pageData.Permissions.Delete is null || await Permissions.HasPermission(Auth.AccessToken, _pageData.Permissions.Delete);
await Reload();
}
diff --git a/src/HopFrame.Web/Services/Implementation/AuthService.cs b/src/HopFrame.Web/Services/Implementation/AuthService.cs
index 6fca234..7bc38a4 100644
--- a/src/HopFrame.Web/Services/Implementation/AuthService.cs
+++ b/src/HopFrame.Web/Services/Implementation/AuthService.cs
@@ -28,12 +28,12 @@ internal class AuthService(
var refreshToken = await tokens.CreateToken(Token.RefreshTokenType, 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.TokenId.ToString(), new CookieOptions {
MaxAge = options.Value.RefreshTokenTime,
HttpOnly = true,
Secure = true
});
- httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions {
+ httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.TokenId.ToString(), new CookieOptions {
MaxAge = options.Value.AccessTokenTime,
HttpOnly = false,
Secure = true
@@ -49,12 +49,12 @@ internal class AuthService(
var refreshToken = await tokens.CreateToken(Token.RefreshTokenType, 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.TokenId.ToString(), new CookieOptions {
MaxAge = options.Value.RefreshTokenTime,
HttpOnly = true,
Secure = true
});
- httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions {
+ httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.TokenId.ToString(), new CookieOptions {
MaxAge = options.Value.AccessTokenTime,
HttpOnly = false,
Secure = true
@@ -83,7 +83,7 @@ internal class AuthService(
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.TokenId.ToString(), new CookieOptions {
MaxAge = options.Value.AccessTokenTime,
HttpOnly = false,
Secure = true
diff --git a/testing/HopFrame.Testing.Api/Controllers/TestController.cs b/testing/HopFrame.Testing.Api/Controllers/TestController.cs
index fb39666..d097592 100644
--- a/testing/HopFrame.Testing.Api/Controllers/TestController.cs
+++ b/testing/HopFrame.Testing.Api/Controllers/TestController.cs
@@ -1,5 +1,7 @@
using HopFrame.Api.Logic;
+using HopFrame.Api.Models;
using HopFrame.Database.Models;
+using HopFrame.Database.Repositories;
using HopFrame.Security.Authorization;
using HopFrame.Security.Claims;
using HopFrame.Testing.Api.Models;
@@ -10,11 +12,11 @@ namespace HopFrame.Testing.Api.Controllers;
[ApiController]
[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]
- public ActionResult> Permissions() {
- return new ActionResult>(userContext.User.Permissions);
+ public ActionResult> Permissions() {
+ return new ActionResult>(userContext.ContextualPermissions);
}
[HttpGet("generate")]
@@ -50,5 +52,19 @@ public class TestController(ITokenContext userContext, DatabaseContext context)
public async Task>> GetAddresses() {
return LogicResult>.Ok(await context.Addresses.Include(e => e.Employee).ToListAsync());
}
+
+ [HttpGet("token"), Authorized]
+ public async Task>> 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>.Ok(token.TokenId.ToString());
+ }
+
+ [HttpDelete("token/{tokenId}")]
+ public async Task DeleteToken(string tokenId) {
+ var token = await tokens.GetToken(tokenId);
+ await tokens.DeleteToken(token);
+ }
}
\ No newline at end of file
diff --git a/testing/HopFrame.Testing.Api/Program.cs b/testing/HopFrame.Testing.Api/Program.cs
index b728eb3..948be0d 100644
--- a/testing/HopFrame.Testing.Api/Program.cs
+++ b/testing/HopFrame.Testing.Api/Program.cs
@@ -18,7 +18,7 @@ builder.Services.AddSwaggerGen(c => {
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme {
Description = @"JWT Authorization header using the Bearer scheme. \r\n\r\n
Enter 'Bearer' [space] and then your token in the text input below.",
- Name = "Authorization",
+ Name = "Token",
In = ParameterLocation.Header,
Type = SecuritySchemeType.ApiKey,
Scheme = "Bearer"
diff --git a/tests/HopFrame.Tests.Api/AuthLogicTests.cs b/tests/HopFrame.Tests.Api/AuthLogicTests.cs
index a5163d2..39975f5 100644
--- a/tests/HopFrame.Tests.Api/AuthLogicTests.cs
+++ b/tests/HopFrame.Tests.Api/AuthLogicTests.cs
@@ -58,13 +58,13 @@ public class AuthLogicTests {
tokens
.Setup(t => t.CreateToken(It.Is(t => t == Token.RefreshTokenType), It.IsAny()))
.ReturnsAsync(new Token {
- Content = _refreshToken,
+ TokenId = _refreshToken,
Type = Token.RefreshTokenType
});
tokens
.Setup(t => t.CreateToken(It.Is(t => t == Token.AccessTokenType), It.IsAny()))
.ReturnsAsync(new Token {
- Content = _accessToken,
+ TokenId = _accessToken,
Type = Token.AccessTokenType
});
tokens
@@ -229,11 +229,11 @@ public class AuthLogicTests {
// Arrange
var token = new Token {
Type = Token.RefreshTokenType,
- Content = _refreshToken,
+ TokenId = _refreshToken,
CreatedAt = DateTime.Now,
Owner = CreateDummyUser()
};
- var (auth, context) = SetupEnvironment(true, token, token.Content.ToString());
+ var (auth, context) = SetupEnvironment(true, token, token.TokenId.ToString());
// Act
var result = await auth.Authenticate();
@@ -277,11 +277,11 @@ public class AuthLogicTests {
// Arrange
var token = new Token {
Type = Token.AccessTokenType,
- Content = _refreshToken,
+ TokenId = _refreshToken,
CreatedAt = DateTime.Now,
Owner = CreateDummyUser()
};
- var (auth, context) = SetupEnvironment(true, token, token.Content.ToString());
+ var (auth, context) = SetupEnvironment(true, token, token.TokenId.ToString());
// Act
var result = await auth.Authenticate();
@@ -297,11 +297,11 @@ public class AuthLogicTests {
// Arrange
var token = new Token {
Type = Token.RefreshTokenType,
- Content = _refreshToken,
+ TokenId = _refreshToken,
CreatedAt = DateTime.MinValue,
Owner = CreateDummyUser()
};
- var (auth, context) = SetupEnvironment(true, token, token.Content.ToString());
+ var (auth, context) = SetupEnvironment(true, token, token.TokenId.ToString());
// Act
var result = await auth.Authenticate();
diff --git a/tests/HopFrame.Tests.Database/Repositories/TokenRepositoryTests.cs b/tests/HopFrame.Tests.Database/Repositories/TokenRepositoryTests.cs
index 83dc770..d37fde2 100644
--- a/tests/HopFrame.Tests.Database/Repositories/TokenRepositoryTests.cs
+++ b/tests/HopFrame.Tests.Database/Repositories/TokenRepositoryTests.cs
@@ -14,7 +14,7 @@ public class TokenRepositoryTests {
for (int i = 0; i < count; i++) {
await context.Tokens.AddAsync(new() {
- Content = Guid.NewGuid(),
+ TokenId = Guid.NewGuid(),
Owner = CreateTestUser(),
Type = Token.AccessTokenType
});
@@ -37,7 +37,7 @@ public class TokenRepositoryTests {
var token = context.Tokens.First();
// Act
- var result = await repo.GetToken(token.Content.ToString());
+ var result = await repo.GetToken(token.TokenId.ToString());
// Assert
Assert.Equal(token, result);
@@ -64,12 +64,12 @@ public class TokenRepositoryTests {
var user = CreateTestUser();
await context.Tokens.AddRangeAsync(new List {
new() {
- Content = Guid.NewGuid(),
+ TokenId = Guid.NewGuid(),
Owner = user,
Type = Token.AccessTokenType
},
new() {
- Content = Guid.NewGuid(),
+ TokenId = Guid.NewGuid(),
Owner = user,
Type = Token.RefreshTokenType
}
diff --git a/tests/HopFrame.Tests.Security/AuthenticationTests.cs b/tests/HopFrame.Tests.Security/AuthenticationTests.cs
index 5cd6d44..17e3d1d 100644
--- a/tests/HopFrame.Tests.Security/AuthenticationTests.cs
+++ b/tests/HopFrame.Tests.Security/AuthenticationTests.cs
@@ -30,7 +30,7 @@ public class AuthenticationTests {
var provideCorrectToken = correctToken is null;
correctToken ??= new Token {
- Content = Guid.NewGuid(),
+ TokenId = Guid.NewGuid(),
CreatedAt = DateTime.Now,
Type = Token.AccessTokenType,
Owner = new User {
@@ -39,17 +39,17 @@ public class AuthenticationTests {
};
tokens
- .Setup(x => x.GetToken(It.Is(t => t == correctToken.Content.ToString())))
+ .Setup(x => x.GetToken(It.Is(t => t == correctToken.TokenId.ToString())))
.ReturnsAsync(correctToken);
perms
- .Setup(x => x.GetFullPermissions(It.IsAny()))
+ .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, new OptionsWrapper(new HopFrameAuthenticationOptions()));
var context = new DefaultHttpContext();
if (provideCorrectToken)
- context.HttpContext.Request.Headers.Append(HopFrameAuthentication.SchemeName, correctToken.Content.ToString());
+ context.HttpContext.Request.Headers.Append(HopFrameAuthentication.SchemeName, correctToken.TokenId.ToString());
if (providedToken is not null)
context.HttpContext.Request.Headers.Append(HopFrameAuthentication.SchemeName, providedToken);
@@ -101,12 +101,12 @@ public class AuthenticationTests {
public async Task Authentication_With_ExpiredToken_Should_Fail() {
// Arrange
var token = new Token {
- Content = Guid.NewGuid(),
+ TokenId = Guid.NewGuid(),
CreatedAt = DateTime.MinValue,
Type = Token.AccessTokenType,
Owner = new User()
};
- var auth = await SetupEnvironment(token, token.Content.ToString());
+ var auth = await SetupEnvironment(token, token.TokenId.ToString());
// Act
var result = await auth.AuthenticateAsync();
@@ -121,12 +121,12 @@ public class AuthenticationTests {
public async Task Authentication_With_UnownedToken_Should_Fail() {
// Arrange
var token = new Token {
- Content = Guid.NewGuid(),
+ TokenId = Guid.NewGuid(),
CreatedAt = DateTime.Now,
Type = Token.AccessTokenType,
Owner = null
};
- var auth = await SetupEnvironment(token, token.Content.ToString());
+ var auth = await SetupEnvironment(token, token.TokenId.ToString());
// Act
var result = await auth.AuthenticateAsync();
diff --git a/tests/HopFrame.Tests.Web/AuthMiddlewareTests.cs b/tests/HopFrame.Tests.Web/AuthMiddlewareTests.cs
index d9e136f..685e588 100644
--- a/tests/HopFrame.Tests.Web/AuthMiddlewareTests.cs
+++ b/tests/HopFrame.Tests.Web/AuthMiddlewareTests.cs
@@ -23,7 +23,7 @@ public class AuthMiddlewareTests {
var perms = new Mock();
perms
- .Setup(p => p.GetFullPermissions(It.Is(u => newToken.Owner.Id == u.Id)))
+ .Setup(p => p.GetFullPermissions(It.Is(u => newToken.Owner.Id == u.Owner.Id)))
.ReturnsAsync(CreateDummyUser().Permissions.Select(p => p.PermissionName).ToList);
return new AuthMiddleware(auth.Object, perms.Object);
@@ -61,7 +61,7 @@ public class AuthMiddlewareTests {
public async Task InvokeAsync_With_InvalidLoginValidToken_Should_Succeed() {
// Arrange
var token = new Token {
- Content = Guid.NewGuid(),
+ TokenId = Guid.NewGuid(),
CreatedAt = DateTime.Now,
Type = Token.AccessTokenType,
Owner = CreateDummyUser()
@@ -74,7 +74,7 @@ public class AuthMiddlewareTests {
// Assert
Assert.Equal(token.Owner.Id.ToString(), context.User.FindFirstValue(HopFrameClaimTypes.UserId));
- Assert.Equal(token.Content.ToString(), context.User.FindFirstValue(HopFrameClaimTypes.AccessTokenId));
+ Assert.Equal(token.TokenId.ToString(), context.User.FindFirstValue(HopFrameClaimTypes.AccessTokenId));
Assert.Equal(token.Owner.Permissions.First().PermissionName, context.User.FindFirstValue(HopFrameClaimTypes.Permission));
}
diff --git a/tests/HopFrame.Tests.Web/AuthServiceTests.cs b/tests/HopFrame.Tests.Web/AuthServiceTests.cs
index d5c5ad7..306a94b 100644
--- a/tests/HopFrame.Tests.Web/AuthServiceTests.cs
+++ b/tests/HopFrame.Tests.Web/AuthServiceTests.cs
@@ -47,13 +47,13 @@ public class AuthServiceTests {
tokens
.Setup(t => t.CreateToken(It.Is(t => t == Token.RefreshTokenType), It.IsAny()))
.ReturnsAsync(new Token {
- Content = _refreshToken,
+ TokenId = _refreshToken,
Type = Token.RefreshTokenType
});
tokens
.Setup(t => t.CreateToken(It.Is(t => t == Token.AccessTokenType), It.IsAny()))
.ReturnsAsync(new Token {
- Content = _accessToken,
+ TokenId = _accessToken,
Type = Token.AccessTokenType
});
tokens
@@ -171,18 +171,18 @@ public class AuthServiceTests {
// Arrange
var token = new Token {
Type = Token.RefreshTokenType,
- Content = _refreshToken,
+ TokenId = _refreshToken,
CreatedAt = DateTime.Now,
Owner = CreateDummyUser()
};
- var (service, context) = SetupEnvironment(true, token, token.Content.ToString());
+ var (service, context) = SetupEnvironment(true, token, token.TokenId.ToString());
// Act
var result = await service.RefreshLogin();
// Assert
Assert.NotNull(result);
- Assert.Equal(_accessToken, result.Content);
+ Assert.Equal(_accessToken, result.TokenId);
Assert.Equal(_accessToken.ToString(), context.Response.Headers.FindCookie(ITokenContext.AccessTokenType));
}
@@ -217,11 +217,11 @@ public class AuthServiceTests {
// Arrange
var token = new Token {
Type = Token.AccessTokenType,
- Content = _refreshToken,
+ TokenId = _refreshToken,
CreatedAt = DateTime.Now,
Owner = CreateDummyUser()
};
- var (service, context) = SetupEnvironment(true, token, token.Content.ToString());
+ var (service, context) = SetupEnvironment(true, token, token.TokenId.ToString());
// Act
var result = await service.RefreshLogin();
@@ -236,11 +236,11 @@ public class AuthServiceTests {
// Arrange
var token = new Token {
Type = Token.RefreshTokenType,
- Content = _refreshToken,
+ TokenId = _refreshToken,
CreatedAt = DateTime.MinValue,
Owner = CreateDummyUser()
};
- var (service, context) = SetupEnvironment(true, token, token.Content.ToString());
+ var (service, context) = SetupEnvironment(true, token, token.TokenId.ToString());
// Act
var result = await service.RefreshLogin();
@@ -255,7 +255,7 @@ public class AuthServiceTests {
// Arrange
var token = new Token {
Type = Token.AccessTokenType,
- Content = _accessToken,
+ TokenId = _accessToken,
CreatedAt = DateTime.Now,
Owner = CreateDummyUser()
};
@@ -285,7 +285,7 @@ public class AuthServiceTests {
// Arrange
var token = new Token {
Type = Token.RefreshTokenType,
- Content = _accessToken,
+ TokenId = _accessToken,
CreatedAt = DateTime.Now,
Owner = CreateDummyUser()
};
@@ -303,7 +303,7 @@ public class AuthServiceTests {
// Arrange
var token = new Token {
Type = Token.AccessTokenType,
- Content = _accessToken,
+ TokenId = _accessToken,
CreatedAt = DateTime.MinValue,
Owner = CreateDummyUser()
};
@@ -321,7 +321,7 @@ public class AuthServiceTests {
// Arrange
var token = new Token {
Type = Token.AccessTokenType,
- Content = _accessToken,
+ TokenId = _accessToken,
CreatedAt = DateTime.Now,
Owner = null
};