Resolve "API tokens" #41
2
.idea/.idea.HopFrame/.idea/dataSources.xml
generated
2
.idea/.idea.HopFrame/.idea/dataSources.xml
generated
@@ -5,7 +5,7 @@
|
|||||||
<driver-ref>sqlite.xerial</driver-ref>
|
<driver-ref>sqlite.xerial</driver-ref>
|
||||||
<synchronize>true</synchronize>
|
<synchronize>true</synchronize>
|
||||||
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
|
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
|
||||||
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/test/RestApiTest/bin/Debug/net8.0/test.db</jdbc-url>
|
<jdbc-url>jdbc:sqlite:C:\Users\leon\Documents\Projekte\HopFrame\testing\HopFrame.Testing.Api\bin\Debug\net8.0\test.db</jdbc-url>
|
||||||
<jdbc-additional-properties>
|
<jdbc-additional-properties>
|
||||||
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
|
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
|
||||||
</jdbc-additional-properties>
|
</jdbc-additional-properties>
|
||||||
|
|||||||
14
.idea/.idea.HopFrame/.idea/discord.xml
generated
Normal file
14
.idea/.idea.HopFrame/.idea/discord.xml
generated
Normal 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>
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAuthenticationHandler_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fca451c12d69fe026a0e7e9b1a0ddbf4cf6f6b8316cb2aec7984a7241813f648_003FAuthenticationHandler_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAuthenticationSchemeOptions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003F8525b7a9e58c77f532f1a88d4f2897e3c2baf316b9eb2c391b242a3885fcce6_003FAuthenticationSchemeOptions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAuthenticationSchemeOptions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003F8525b7a9e58c77f532f1a88d4f2897e3c2baf316b9eb2c391b242a3885fcce6_003FAuthenticationSchemeOptions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEditContextDataAnnotationsExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fbc307cd57fb42fc4c7fb9795381958122734d3750f41b6c1735c7d132ecda70_003FEditContextDataAnnotationsExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEditContextDataAnnotationsExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fbc307cd57fb42fc4c7fb9795381958122734d3750f41b6c1735c7d132ecda70_003FEditContextDataAnnotationsExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AExceptionDispatchInfo_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fbd1d5c50194fea68ff3559c160230b0ab50f5acf4ce3061bffd6d62958e2182_003FExceptionDispatchInfo_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AExceptionDispatchInfo_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fbd1d5c50194fea68ff3559c160230b0ab50f5acf4ce3061bffd6d62958e2182_003FExceptionDispatchInfo_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
@@ -67,6 +68,9 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -23,8 +23,6 @@ by configuring your configuration to load these.
|
|||||||
> `builder.Configuration.AddEnvironmentVariables();` to your startup configuration before you add the
|
> `builder.Configuration.AddEnvironmentVariables();` to your startup configuration before you add the
|
||||||
> custom configurations / HopFrame services.
|
> custom configurations / HopFrame services.
|
||||||
|
|
||||||
### Example
|
|
||||||
|
|
||||||
You can specify `Seconds`, `Minutes`, `Hours` and `Days` for either of the two token types.
|
You can specify `Seconds`, `Minutes`, `Hours` and `Days` for either of the two token types.
|
||||||
These get combined to a single time span.
|
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__DAYS=10
|
||||||
HOPFRAME__AUTHENTICATION__REFRESHTOKEN__HOURS=5
|
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");
|
||||||
|
```
|
||||||
|
|||||||
@@ -35,16 +35,18 @@ public class Permission {
|
|||||||
public DateTime GrantedAt { get; set; }
|
public DateTime GrantedAt { get; set; }
|
||||||
public virtual User User { get; set; }
|
public virtual User User { get; set; }
|
||||||
public virtual PermissionGroup Group { get; set; }
|
public virtual PermissionGroup Group { get; set; }
|
||||||
|
public virtual Token Token { get; set; }
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Token
|
## Token
|
||||||
```csharp
|
```csharp
|
||||||
public class Token {
|
public class Token : IPermissionOwner {
|
||||||
public int Type { get; set; }
|
public int Type { get; set; }
|
||||||
public Guid Content { get; set; }
|
public Guid Content { get; set; }
|
||||||
public DateTime CreatedAt { get; set; }
|
public DateTime CreatedAt { get; set; }
|
||||||
public virtual User Owner { get; set; }
|
public virtual User Owner { get; set; }
|
||||||
|
public virtual List<Permission> Permissions { get; set; }
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -71,5 +71,9 @@ public interface ITokenRepository {
|
|||||||
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);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -23,18 +23,18 @@ internal class AuthLogic(IUserRepository users, ITokenRepository tokens, ITokenC
|
|||||||
var refreshToken = await tokens.CreateToken(Token.RefreshTokenType, user);
|
var refreshToken = await tokens.CreateToken(Token.RefreshTokenType, user);
|
||||||
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.TokenId.ToString(), new CookieOptions {
|
||||||
MaxAge = options.Value.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.TokenId.ToString(), new CookieOptions {
|
||||||
MaxAge = options.Value.AccessTokenTime,
|
MaxAge = options.Value.AccessTokenTime,
|
||||||
HttpOnly = true,
|
HttpOnly = true,
|
||||||
Secure = true
|
Secure = true
|
||||||
});
|
});
|
||||||
|
|
||||||
return LogicResult<SingleValueResult<string>>.Ok(accessToken.Content.ToString());
|
return LogicResult<SingleValueResult<string>>.Ok(accessToken.TokenId.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<LogicResult<SingleValueResult<string>>> Register(UserRegister register) {
|
public async Task<LogicResult<SingleValueResult<string>>> Register(UserRegister register) {
|
||||||
@@ -54,18 +54,18 @@ internal class AuthLogic(IUserRepository users, ITokenRepository tokens, ITokenC
|
|||||||
var refreshToken = await tokens.CreateToken(Token.RefreshTokenType, user);
|
var refreshToken = await tokens.CreateToken(Token.RefreshTokenType, user);
|
||||||
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.TokenId.ToString(), new CookieOptions {
|
||||||
MaxAge = options.Value.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.TokenId.ToString(), new CookieOptions {
|
||||||
MaxAge = options.Value.AccessTokenTime,
|
MaxAge = options.Value.AccessTokenTime,
|
||||||
HttpOnly = false,
|
HttpOnly = false,
|
||||||
Secure = true
|
Secure = true
|
||||||
});
|
});
|
||||||
|
|
||||||
return LogicResult<SingleValueResult<string>>.Ok(accessToken.Content.ToString());
|
return LogicResult<SingleValueResult<string>>.Ok(accessToken.TokenId.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<LogicResult<SingleValueResult<string>>> Authenticate() {
|
public async Task<LogicResult<SingleValueResult<string>>> Authenticate() {
|
||||||
@@ -87,13 +87,13 @@ internal class AuthLogic(IUserRepository users, ITokenRepository tokens, ITokenC
|
|||||||
|
|
||||||
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.TokenId.ToString(), new CookieOptions {
|
||||||
MaxAge = options.Value.AccessTokenTime,
|
MaxAge = options.Value.AccessTokenTime,
|
||||||
HttpOnly = false,
|
HttpOnly = false,
|
||||||
Secure = true
|
Secure = true
|
||||||
});
|
});
|
||||||
|
|
||||||
return LogicResult<SingleValueResult<string>>.Ok(accessToken.Content.ToString());
|
return LogicResult<SingleValueResult<string>>.Ok(accessToken.TokenId.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<LogicResult> Logout() {
|
public async Task<LogicResult> Logout() {
|
||||||
|
|||||||
@@ -30,5 +30,10 @@ public abstract class HopDbContextBase : DbContext {
|
|||||||
.HasMany(g => g.Permissions)
|
.HasMany(g => g.Permissions)
|
||||||
.WithOne(p => p.Group)
|
.WithOne(p => p.Group)
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
modelBuilder.Entity<Token>()
|
||||||
|
.HasMany(t => t.Permissions)
|
||||||
|
.WithOne(t => t.Token)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -21,6 +21,9 @@ public class Permission {
|
|||||||
[ForeignKey("GroupName"), JsonIgnore]
|
[ForeignKey("GroupName"), JsonIgnore]
|
||||||
public virtual PermissionGroup Group { get; set; }
|
public virtual PermissionGroup Group { get; set; }
|
||||||
|
|
||||||
|
[ForeignKey("TokenId"), JsonIgnore]
|
||||||
|
public virtual Token Token { get; set; }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IPermissionOwner;
|
public interface IPermissionOwner;
|
||||||
|
|||||||
@@ -4,24 +4,32 @@ using System.Text.Json.Serialization;
|
|||||||
|
|
||||||
namespace HopFrame.Database.Models;
|
namespace HopFrame.Database.Models;
|
||||||
|
|
||||||
public class Token {
|
public class Token : IPermissionOwner {
|
||||||
public const int RefreshTokenType = 0;
|
public const int RefreshTokenType = 0;
|
||||||
public const int AccessTokenType = 1;
|
public const int AccessTokenType = 1;
|
||||||
|
public const int ApiTokenType = 2;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Defines the Type of the stored Token
|
/// Defines the Type of the stored Token
|
||||||
/// 0: Refresh token
|
/// 0: Refresh token
|
||||||
/// 1: Access token
|
/// 1: Access token
|
||||||
|
/// 2: Api token
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Required, MinLength(1), MaxLength(1)]
|
[Required, MinLength(1), MaxLength(1)]
|
||||||
public int Type { get; set; }
|
public int Type { get; set; }
|
||||||
|
|
||||||
[Key, Required, MinLength(36), MaxLength(36)]
|
[Key, Required, MinLength(36), MaxLength(36)]
|
||||||
public Guid Content { get; set; }
|
public Guid TokenId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Defines the creation date of the token
|
||||||
|
/// In case of an api token it defines the date it becomes invalid
|
||||||
|
/// </summary>
|
||||||
[Required]
|
[Required]
|
||||||
public DateTime CreatedAt { get; set; }
|
public DateTime CreatedAt { get; set; }
|
||||||
|
|
||||||
[ForeignKey("UserId"), JsonIgnore]
|
[ForeignKey("UserId"), JsonIgnore]
|
||||||
public virtual User Owner { get; set; }
|
public virtual User Owner { get; set; }
|
||||||
|
|
||||||
|
public virtual List<Permission> Permissions { get; set; }
|
||||||
}
|
}
|
||||||
@@ -5,5 +5,7 @@ namespace HopFrame.Database.Repositories;
|
|||||||
public interface ITokenRepository {
|
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, bool includeApiTokens = false);
|
||||||
|
Task DeleteToken(Token token);
|
||||||
|
Task<Token> CreateApiToken(User owner, DateTime expirationDate);
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,10 @@ namespace HopFrame.Database.Repositories.Implementation;
|
|||||||
|
|
||||||
internal sealed class PermissionRepository<TDbContext>(TDbContext context, IGroupRepository groupRepository) : IPermissionRepository where TDbContext : HopDbContextBase {
|
internal sealed class PermissionRepository<TDbContext>(TDbContext context, IGroupRepository groupRepository) : IPermissionRepository where TDbContext : HopDbContextBase {
|
||||||
public async Task<bool> HasPermission(IPermissionOwner owner, params string[] permissions) {
|
public async Task<bool> 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();
|
var perms = (await GetFullPermissions(owner)).ToArray();
|
||||||
|
|
||||||
foreach (var permission in permissions) {
|
foreach (var permission in permissions) {
|
||||||
@@ -24,6 +28,12 @@ internal sealed class PermissionRepository<TDbContext>(TDbContext context, IGrou
|
|||||||
entry.User = user;
|
entry.User = user;
|
||||||
}else if (owner is PermissionGroup group) {
|
}else if (owner is PermissionGroup group) {
|
||||||
entry.Group = 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);
|
await context.Permissions.AddAsync(entry);
|
||||||
@@ -48,6 +58,13 @@ internal sealed class PermissionRepository<TDbContext>(TDbContext context, IGrou
|
|||||||
.Where(p =>p.Group.Name == group.Name)
|
.Where(p =>p.Group.Name == group.Name)
|
||||||
.Where(p => p.PermissionName == permission)
|
.Where(p => p.PermissionName == permission)
|
||||||
.SingleOrDefaultAsync();
|
.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) {
|
if (entry is not null) {
|
||||||
@@ -59,6 +76,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)
|
||||||
@@ -74,6 +95,14 @@ internal sealed class PermissionRepository<TDbContext>(TDbContext context, IGrou
|
|||||||
.Where(p =>p.Group.Name == group.Name)
|
.Where(p =>p.Group.Name == group.Name)
|
||||||
.ToListAsync();
|
.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));
|
permissions.AddRange(perms.Select(p => p.PermissionName));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,14 +11,14 @@ internal sealed class TokenRepository<TDbContext>(TDbContext context) : ITokenRe
|
|||||||
|
|
||||||
return await context.Tokens
|
return await context.Tokens
|
||||||
.Include(t => t.Owner)
|
.Include(t => t.Owner)
|
||||||
.Where(t => t.Content == guid)
|
.Where(t => t.TokenId == guid)
|
||||||
.SingleOrDefaultAsync();
|
.SingleOrDefaultAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Token> CreateToken(int type, User owner) {
|
public async Task<Token> CreateToken(int type, User owner) {
|
||||||
var token = new Token {
|
var token = new Token {
|
||||||
CreatedAt = DateTime.Now,
|
CreatedAt = DateTime.Now,
|
||||||
Content = Guid.NewGuid(),
|
TokenId = Guid.NewGuid(),
|
||||||
Type = type,
|
Type = type,
|
||||||
Owner = owner
|
Owner = owner
|
||||||
};
|
};
|
||||||
@@ -29,13 +29,37 @@ internal sealed class TokenRepository<TDbContext>(TDbContext context) : ITokenRe
|
|||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DeleteUserTokens(User owner) {
|
public async Task DeleteUserTokens(User owner, bool includeApiTokens = false) {
|
||||||
var tokens = await context.Tokens
|
var tokens = await context.Tokens
|
||||||
.Include(t => t.Owner)
|
.Include(t => t.Owner)
|
||||||
.Where(t => t.Owner.Id == owner.Id)
|
.Where(t => t.Owner.Id == owner.Id)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
|
if (!includeApiTokens)
|
||||||
|
tokens = tokens
|
||||||
|
.Where(t => t.Type != Token.ApiTokenType)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
context.Tokens.RemoveRange(tokens);
|
context.Tokens.RemoveRange(tokens);
|
||||||
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) {
|
||||||
|
var token = new Token {
|
||||||
|
CreatedAt = expirationDate,
|
||||||
|
TokenId = Guid.NewGuid(),
|
||||||
|
Type = Token.ApiTokenType,
|
||||||
|
Owner = owner
|
||||||
|
};
|
||||||
|
|
||||||
|
await context.Tokens.AddAsync(token);
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
return token;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Text.Encodings.Web;
|
using System.Text.Encodings.Web;
|
||||||
|
using HopFrame.Database.Models;
|
||||||
using HopFrame.Database.Repositories;
|
using HopFrame.Database.Repositories;
|
||||||
using HopFrame.Security.Claims;
|
using HopFrame.Security.Claims;
|
||||||
using Microsoft.AspNetCore.Authentication;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
@@ -33,7 +34,10 @@ public class HopFrameAuthentication(
|
|||||||
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 + 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)
|
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");
|
||||||
@@ -43,7 +47,7 @@ public class HopFrameAuthentication(
|
|||||||
new(HopFrameClaimTypes.UserId, tokenEntry.Owner.Id.ToString())
|
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)));
|
claims.AddRange(permissions.Select(perm => new Claim(HopFrameClaimTypes.Permission, perm)));
|
||||||
|
|
||||||
var principal = new ClaimsPrincipal();
|
var principal = new ClaimsPrincipal();
|
||||||
|
|||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
@@ -22,11 +22,11 @@ public sealed class AuthMiddleware(IAuthService auth, IPermissionRepository perm
|
|||||||
}
|
}
|
||||||
|
|
||||||
var claims = new List<Claim> {
|
var claims = new List<Claim> {
|
||||||
new(HopFrameClaimTypes.AccessTokenId, token.Content.ToString()),
|
new(HopFrameClaimTypes.AccessTokenId, token.TokenId.ToString()),
|
||||||
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));
|
||||||
|
|||||||
@@ -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!",
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,12 +28,12 @@ internal class AuthService(
|
|||||||
var refreshToken = await tokens.CreateToken(Token.RefreshTokenType, user);
|
var refreshToken = await tokens.CreateToken(Token.RefreshTokenType, user);
|
||||||
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.TokenId.ToString(), new CookieOptions {
|
||||||
MaxAge = options.Value.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.TokenId.ToString(), new CookieOptions {
|
||||||
MaxAge = options.Value.AccessTokenTime,
|
MaxAge = options.Value.AccessTokenTime,
|
||||||
HttpOnly = false,
|
HttpOnly = false,
|
||||||
Secure = true
|
Secure = true
|
||||||
@@ -49,12 +49,12 @@ internal class AuthService(
|
|||||||
var refreshToken = await tokens.CreateToken(Token.RefreshTokenType, user);
|
var refreshToken = await tokens.CreateToken(Token.RefreshTokenType, user);
|
||||||
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.TokenId.ToString(), new CookieOptions {
|
||||||
MaxAge = options.Value.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.TokenId.ToString(), new CookieOptions {
|
||||||
MaxAge = options.Value.AccessTokenTime,
|
MaxAge = options.Value.AccessTokenTime,
|
||||||
HttpOnly = false,
|
HttpOnly = false,
|
||||||
Secure = true
|
Secure = true
|
||||||
@@ -83,7 +83,7 @@ internal class AuthService(
|
|||||||
|
|
||||||
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.TokenId.ToString(), new CookieOptions {
|
||||||
MaxAge = options.Value.AccessTokenTime,
|
MaxAge = options.Value.AccessTokenTime,
|
||||||
HttpOnly = false,
|
HttpOnly = false,
|
||||||
Secure = true
|
Secure = true
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -58,13 +58,13 @@ public class AuthLogicTests {
|
|||||||
tokens
|
tokens
|
||||||
.Setup(t => t.CreateToken(It.Is<int>(t => t == Token.RefreshTokenType), It.IsAny<User>()))
|
.Setup(t => t.CreateToken(It.Is<int>(t => t == Token.RefreshTokenType), It.IsAny<User>()))
|
||||||
.ReturnsAsync(new Token {
|
.ReturnsAsync(new Token {
|
||||||
Content = _refreshToken,
|
TokenId = _refreshToken,
|
||||||
Type = Token.RefreshTokenType
|
Type = Token.RefreshTokenType
|
||||||
});
|
});
|
||||||
tokens
|
tokens
|
||||||
.Setup(t => t.CreateToken(It.Is<int>(t => t == Token.AccessTokenType), It.IsAny<User>()))
|
.Setup(t => t.CreateToken(It.Is<int>(t => t == Token.AccessTokenType), It.IsAny<User>()))
|
||||||
.ReturnsAsync(new Token {
|
.ReturnsAsync(new Token {
|
||||||
Content = _accessToken,
|
TokenId = _accessToken,
|
||||||
Type = Token.AccessTokenType
|
Type = Token.AccessTokenType
|
||||||
});
|
});
|
||||||
tokens
|
tokens
|
||||||
@@ -229,11 +229,11 @@ public class AuthLogicTests {
|
|||||||
// Arrange
|
// Arrange
|
||||||
var token = new Token {
|
var token = new Token {
|
||||||
Type = Token.RefreshTokenType,
|
Type = Token.RefreshTokenType,
|
||||||
Content = _refreshToken,
|
TokenId = _refreshToken,
|
||||||
CreatedAt = DateTime.Now,
|
CreatedAt = DateTime.Now,
|
||||||
Owner = CreateDummyUser()
|
Owner = CreateDummyUser()
|
||||||
};
|
};
|
||||||
var (auth, context) = SetupEnvironment(true, token, token.Content.ToString());
|
var (auth, context) = SetupEnvironment(true, token, token.TokenId.ToString());
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await auth.Authenticate();
|
var result = await auth.Authenticate();
|
||||||
@@ -277,11 +277,11 @@ public class AuthLogicTests {
|
|||||||
// Arrange
|
// Arrange
|
||||||
var token = new Token {
|
var token = new Token {
|
||||||
Type = Token.AccessTokenType,
|
Type = Token.AccessTokenType,
|
||||||
Content = _refreshToken,
|
TokenId = _refreshToken,
|
||||||
CreatedAt = DateTime.Now,
|
CreatedAt = DateTime.Now,
|
||||||
Owner = CreateDummyUser()
|
Owner = CreateDummyUser()
|
||||||
};
|
};
|
||||||
var (auth, context) = SetupEnvironment(true, token, token.Content.ToString());
|
var (auth, context) = SetupEnvironment(true, token, token.TokenId.ToString());
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await auth.Authenticate();
|
var result = await auth.Authenticate();
|
||||||
@@ -297,11 +297,11 @@ public class AuthLogicTests {
|
|||||||
// Arrange
|
// Arrange
|
||||||
var token = new Token {
|
var token = new Token {
|
||||||
Type = Token.RefreshTokenType,
|
Type = Token.RefreshTokenType,
|
||||||
Content = _refreshToken,
|
TokenId = _refreshToken,
|
||||||
CreatedAt = DateTime.MinValue,
|
CreatedAt = DateTime.MinValue,
|
||||||
Owner = CreateDummyUser()
|
Owner = CreateDummyUser()
|
||||||
};
|
};
|
||||||
var (auth, context) = SetupEnvironment(true, token, token.Content.ToString());
|
var (auth, context) = SetupEnvironment(true, token, token.TokenId.ToString());
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await auth.Authenticate();
|
var result = await auth.Authenticate();
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ public class TokenRepositoryTests {
|
|||||||
|
|
||||||
for (int i = 0; i < count; i++) {
|
for (int i = 0; i < count; i++) {
|
||||||
await context.Tokens.AddAsync(new() {
|
await context.Tokens.AddAsync(new() {
|
||||||
Content = Guid.NewGuid(),
|
TokenId = Guid.NewGuid(),
|
||||||
Owner = CreateTestUser(),
|
Owner = CreateTestUser(),
|
||||||
Type = Token.AccessTokenType
|
Type = Token.AccessTokenType
|
||||||
});
|
});
|
||||||
@@ -37,7 +37,7 @@ public class TokenRepositoryTests {
|
|||||||
var token = context.Tokens.First();
|
var token = context.Tokens.First();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await repo.GetToken(token.Content.ToString());
|
var result = await repo.GetToken(token.TokenId.ToString());
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal(token, result);
|
Assert.Equal(token, result);
|
||||||
@@ -64,12 +64,12 @@ public class TokenRepositoryTests {
|
|||||||
var user = CreateTestUser();
|
var user = CreateTestUser();
|
||||||
await context.Tokens.AddRangeAsync(new List<Token> {
|
await context.Tokens.AddRangeAsync(new List<Token> {
|
||||||
new() {
|
new() {
|
||||||
Content = Guid.NewGuid(),
|
TokenId = Guid.NewGuid(),
|
||||||
Owner = user,
|
Owner = user,
|
||||||
Type = Token.AccessTokenType
|
Type = Token.AccessTokenType
|
||||||
},
|
},
|
||||||
new() {
|
new() {
|
||||||
Content = Guid.NewGuid(),
|
TokenId = Guid.NewGuid(),
|
||||||
Owner = user,
|
Owner = user,
|
||||||
Type = Token.RefreshTokenType
|
Type = Token.RefreshTokenType
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ public class AuthenticationTests {
|
|||||||
|
|
||||||
var provideCorrectToken = correctToken is null;
|
var provideCorrectToken = correctToken is null;
|
||||||
correctToken ??= new Token {
|
correctToken ??= new Token {
|
||||||
Content = Guid.NewGuid(),
|
TokenId = Guid.NewGuid(),
|
||||||
CreatedAt = DateTime.Now,
|
CreatedAt = DateTime.Now,
|
||||||
Type = Token.AccessTokenType,
|
Type = Token.AccessTokenType,
|
||||||
Owner = new User {
|
Owner = new User {
|
||||||
@@ -39,17 +39,17 @@ public class AuthenticationTests {
|
|||||||
};
|
};
|
||||||
|
|
||||||
tokens
|
tokens
|
||||||
.Setup(x => x.GetToken(It.Is<string>(t => t == correctToken.Content.ToString())))
|
.Setup(x => x.GetToken(It.Is<string>(t => t == correctToken.TokenId.ToString())))
|
||||||
.ReturnsAsync(correctToken);
|
.ReturnsAsync(correctToken);
|
||||||
|
|
||||||
perms
|
perms
|
||||||
.Setup(x => x.GetFullPermissions(It.IsAny<User>()))
|
.Setup(x => x.GetFullPermissions(It.IsAny<Token>()))
|
||||||
.ReturnsAsync(new List<string>());
|
.ReturnsAsync(new List<string>());
|
||||||
|
|
||||||
var auth = new HopFrameAuthentication(options.Object, logger.Object, encoder.Object, clock.Object, tokens.Object, perms.Object, new OptionsWrapper<HopFrameAuthenticationOptions>(new HopFrameAuthenticationOptions()));
|
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.TokenId.ToString());
|
||||||
if (providedToken is not null)
|
if (providedToken is not null)
|
||||||
context.HttpContext.Request.Headers.Append(HopFrameAuthentication.SchemeName, providedToken);
|
context.HttpContext.Request.Headers.Append(HopFrameAuthentication.SchemeName, providedToken);
|
||||||
|
|
||||||
@@ -101,12 +101,12 @@ public class AuthenticationTests {
|
|||||||
public async Task Authentication_With_ExpiredToken_Should_Fail() {
|
public async Task Authentication_With_ExpiredToken_Should_Fail() {
|
||||||
// Arrange
|
// Arrange
|
||||||
var token = new Token {
|
var token = new Token {
|
||||||
Content = Guid.NewGuid(),
|
TokenId = Guid.NewGuid(),
|
||||||
CreatedAt = DateTime.MinValue,
|
CreatedAt = DateTime.MinValue,
|
||||||
Type = Token.AccessTokenType,
|
Type = Token.AccessTokenType,
|
||||||
Owner = new User()
|
Owner = new User()
|
||||||
};
|
};
|
||||||
var auth = await SetupEnvironment(token, token.Content.ToString());
|
var auth = await SetupEnvironment(token, token.TokenId.ToString());
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await auth.AuthenticateAsync();
|
var result = await auth.AuthenticateAsync();
|
||||||
@@ -121,12 +121,12 @@ public class AuthenticationTests {
|
|||||||
public async Task Authentication_With_UnownedToken_Should_Fail() {
|
public async Task Authentication_With_UnownedToken_Should_Fail() {
|
||||||
// Arrange
|
// Arrange
|
||||||
var token = new Token {
|
var token = new Token {
|
||||||
Content = Guid.NewGuid(),
|
TokenId = Guid.NewGuid(),
|
||||||
CreatedAt = DateTime.Now,
|
CreatedAt = DateTime.Now,
|
||||||
Type = Token.AccessTokenType,
|
Type = Token.AccessTokenType,
|
||||||
Owner = null
|
Owner = null
|
||||||
};
|
};
|
||||||
var auth = await SetupEnvironment(token, token.Content.ToString());
|
var auth = await SetupEnvironment(token, token.TokenId.ToString());
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await auth.AuthenticateAsync();
|
var result = await auth.AuthenticateAsync();
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ public class AuthMiddlewareTests {
|
|||||||
|
|
||||||
var perms = new Mock<IPermissionRepository>();
|
var perms = new Mock<IPermissionRepository>();
|
||||||
perms
|
perms
|
||||||
.Setup(p => p.GetFullPermissions(It.Is<User>(u => newToken.Owner.Id == u.Id)))
|
.Setup(p => p.GetFullPermissions(It.Is<Token>(u => newToken.Owner.Id == u.Owner.Id)))
|
||||||
.ReturnsAsync(CreateDummyUser().Permissions.Select(p => p.PermissionName).ToList);
|
.ReturnsAsync(CreateDummyUser().Permissions.Select(p => p.PermissionName).ToList);
|
||||||
|
|
||||||
return new AuthMiddleware(auth.Object, perms.Object);
|
return new AuthMiddleware(auth.Object, perms.Object);
|
||||||
@@ -61,7 +61,7 @@ public class AuthMiddlewareTests {
|
|||||||
public async Task InvokeAsync_With_InvalidLoginValidToken_Should_Succeed() {
|
public async Task InvokeAsync_With_InvalidLoginValidToken_Should_Succeed() {
|
||||||
// Arrange
|
// Arrange
|
||||||
var token = new Token {
|
var token = new Token {
|
||||||
Content = Guid.NewGuid(),
|
TokenId = Guid.NewGuid(),
|
||||||
CreatedAt = DateTime.Now,
|
CreatedAt = DateTime.Now,
|
||||||
Type = Token.AccessTokenType,
|
Type = Token.AccessTokenType,
|
||||||
Owner = CreateDummyUser()
|
Owner = CreateDummyUser()
|
||||||
@@ -74,7 +74,7 @@ public class AuthMiddlewareTests {
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal(token.Owner.Id.ToString(), context.User.FindFirstValue(HopFrameClaimTypes.UserId));
|
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));
|
Assert.Equal(token.Owner.Permissions.First().PermissionName, context.User.FindFirstValue(HopFrameClaimTypes.Permission));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,13 +47,13 @@ public class AuthServiceTests {
|
|||||||
tokens
|
tokens
|
||||||
.Setup(t => t.CreateToken(It.Is<int>(t => t == Token.RefreshTokenType), It.IsAny<User>()))
|
.Setup(t => t.CreateToken(It.Is<int>(t => t == Token.RefreshTokenType), It.IsAny<User>()))
|
||||||
.ReturnsAsync(new Token {
|
.ReturnsAsync(new Token {
|
||||||
Content = _refreshToken,
|
TokenId = _refreshToken,
|
||||||
Type = Token.RefreshTokenType
|
Type = Token.RefreshTokenType
|
||||||
});
|
});
|
||||||
tokens
|
tokens
|
||||||
.Setup(t => t.CreateToken(It.Is<int>(t => t == Token.AccessTokenType), It.IsAny<User>()))
|
.Setup(t => t.CreateToken(It.Is<int>(t => t == Token.AccessTokenType), It.IsAny<User>()))
|
||||||
.ReturnsAsync(new Token {
|
.ReturnsAsync(new Token {
|
||||||
Content = _accessToken,
|
TokenId = _accessToken,
|
||||||
Type = Token.AccessTokenType
|
Type = Token.AccessTokenType
|
||||||
});
|
});
|
||||||
tokens
|
tokens
|
||||||
@@ -171,18 +171,18 @@ public class AuthServiceTests {
|
|||||||
// Arrange
|
// Arrange
|
||||||
var token = new Token {
|
var token = new Token {
|
||||||
Type = Token.RefreshTokenType,
|
Type = Token.RefreshTokenType,
|
||||||
Content = _refreshToken,
|
TokenId = _refreshToken,
|
||||||
CreatedAt = DateTime.Now,
|
CreatedAt = DateTime.Now,
|
||||||
Owner = CreateDummyUser()
|
Owner = CreateDummyUser()
|
||||||
};
|
};
|
||||||
var (service, context) = SetupEnvironment(true, token, token.Content.ToString());
|
var (service, context) = SetupEnvironment(true, token, token.TokenId.ToString());
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await service.RefreshLogin();
|
var result = await service.RefreshLogin();
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.NotNull(result);
|
Assert.NotNull(result);
|
||||||
Assert.Equal(_accessToken, result.Content);
|
Assert.Equal(_accessToken, result.TokenId);
|
||||||
Assert.Equal(_accessToken.ToString(), context.Response.Headers.FindCookie(ITokenContext.AccessTokenType));
|
Assert.Equal(_accessToken.ToString(), context.Response.Headers.FindCookie(ITokenContext.AccessTokenType));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,11 +217,11 @@ public class AuthServiceTests {
|
|||||||
// Arrange
|
// Arrange
|
||||||
var token = new Token {
|
var token = new Token {
|
||||||
Type = Token.AccessTokenType,
|
Type = Token.AccessTokenType,
|
||||||
Content = _refreshToken,
|
TokenId = _refreshToken,
|
||||||
CreatedAt = DateTime.Now,
|
CreatedAt = DateTime.Now,
|
||||||
Owner = CreateDummyUser()
|
Owner = CreateDummyUser()
|
||||||
};
|
};
|
||||||
var (service, context) = SetupEnvironment(true, token, token.Content.ToString());
|
var (service, context) = SetupEnvironment(true, token, token.TokenId.ToString());
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await service.RefreshLogin();
|
var result = await service.RefreshLogin();
|
||||||
@@ -236,11 +236,11 @@ public class AuthServiceTests {
|
|||||||
// Arrange
|
// Arrange
|
||||||
var token = new Token {
|
var token = new Token {
|
||||||
Type = Token.RefreshTokenType,
|
Type = Token.RefreshTokenType,
|
||||||
Content = _refreshToken,
|
TokenId = _refreshToken,
|
||||||
CreatedAt = DateTime.MinValue,
|
CreatedAt = DateTime.MinValue,
|
||||||
Owner = CreateDummyUser()
|
Owner = CreateDummyUser()
|
||||||
};
|
};
|
||||||
var (service, context) = SetupEnvironment(true, token, token.Content.ToString());
|
var (service, context) = SetupEnvironment(true, token, token.TokenId.ToString());
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await service.RefreshLogin();
|
var result = await service.RefreshLogin();
|
||||||
@@ -255,7 +255,7 @@ public class AuthServiceTests {
|
|||||||
// Arrange
|
// Arrange
|
||||||
var token = new Token {
|
var token = new Token {
|
||||||
Type = Token.AccessTokenType,
|
Type = Token.AccessTokenType,
|
||||||
Content = _accessToken,
|
TokenId = _accessToken,
|
||||||
CreatedAt = DateTime.Now,
|
CreatedAt = DateTime.Now,
|
||||||
Owner = CreateDummyUser()
|
Owner = CreateDummyUser()
|
||||||
};
|
};
|
||||||
@@ -285,7 +285,7 @@ public class AuthServiceTests {
|
|||||||
// Arrange
|
// Arrange
|
||||||
var token = new Token {
|
var token = new Token {
|
||||||
Type = Token.RefreshTokenType,
|
Type = Token.RefreshTokenType,
|
||||||
Content = _accessToken,
|
TokenId = _accessToken,
|
||||||
CreatedAt = DateTime.Now,
|
CreatedAt = DateTime.Now,
|
||||||
Owner = CreateDummyUser()
|
Owner = CreateDummyUser()
|
||||||
};
|
};
|
||||||
@@ -303,7 +303,7 @@ public class AuthServiceTests {
|
|||||||
// Arrange
|
// Arrange
|
||||||
var token = new Token {
|
var token = new Token {
|
||||||
Type = Token.AccessTokenType,
|
Type = Token.AccessTokenType,
|
||||||
Content = _accessToken,
|
TokenId = _accessToken,
|
||||||
CreatedAt = DateTime.MinValue,
|
CreatedAt = DateTime.MinValue,
|
||||||
Owner = CreateDummyUser()
|
Owner = CreateDummyUser()
|
||||||
};
|
};
|
||||||
@@ -321,7 +321,7 @@ public class AuthServiceTests {
|
|||||||
// Arrange
|
// Arrange
|
||||||
var token = new Token {
|
var token = new Token {
|
||||||
Type = Token.AccessTokenType,
|
Type = Token.AccessTokenType,
|
||||||
Content = _accessToken,
|
TokenId = _accessToken,
|
||||||
CreatedAt = DateTime.Now,
|
CreatedAt = DateTime.Now,
|
||||||
Owner = null
|
Owner = null
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user