Added OpenID authentication method

This commit is contained in:
2024-12-21 22:35:04 +01:00
parent df89450745
commit ba7584c771
17 changed files with 345 additions and 15 deletions

View File

@@ -5,6 +5,7 @@
<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>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIHttpContextAccessor_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Ffe1b239a13ce466b829e79538c654ff54e938_003Fa5_003F4093f165_003FIHttpContextAccessor_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIHttpContextAccessor_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Ffe1b239a13ce466b829e79538c654ff54e938_003Fa5_003F4093f165_003FIHttpContextAccessor_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AList_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fb7208b3f72528d22781d25fde9a55271bdf2b5aade4f03b1324579a25493cd8_003FList_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AList_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fb7208b3f72528d22781d25fde9a55271bdf2b5aade4f03b1324579a25493cd8_003FList_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AOpenIdConnectConfiguration_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F2820ca73717f4b1ab2b3b74fd61961bd1d5b0_003F7e_003F8be6b109_003FOpenIdConnectConfiguration_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AThrowHelper_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003F2c8e7ca976f350cba9836d5565dac56b11e0b56656fa786460eb1395857a6fa_003FThrowHelper_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AThrowHelper_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003F2c8e7ca976f350cba9836d5565dac56b11e0b56656fa786460eb1395857a6fa_003FThrowHelper_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AValidationMessageStore_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Ffc81648e473bb3cc818f71427c286ecddc3604d2f4c69c565205bb89e8b4ef4_003FValidationMessageStore_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AValidationMessageStore_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Ffc81648e473bb3cc818f71427c286ecddc3604d2f4c69c565205bb89e8b4ef4_003FValidationMessageStore_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/Environment/AssemblyExplorer/XmlDocument/@EntryValue">&lt;AssemblyExplorer&gt;&#xD; <s:String x:Key="/Default/Environment/AssemblyExplorer/XmlDocument/@EntryValue">&lt;AssemblyExplorer&gt;&#xD;

View File

@@ -8,6 +8,7 @@ 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; public const int ApiTokenType = 2;
public const int OpenIdTokenType = 3;
/// <summary> /// <summary>
/// Defines the Type of the stored Token /// Defines the Type of the stored Token

View File

@@ -1,5 +1,6 @@
using HopFrame.Database.Models; using HopFrame.Database.Models;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
namespace HopFrame.Database.Repositories.Implementation; namespace HopFrame.Database.Repositories.Implementation;

View File

@@ -2,6 +2,8 @@ using System.Security.Claims;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using HopFrame.Database.Models; using HopFrame.Database.Models;
using HopFrame.Database.Repositories; using HopFrame.Database.Repositories;
using HopFrame.Security.Authentication.OpenID;
using HopFrame.Security.Authentication.OpenID.Options;
using HopFrame.Security.Claims; using HopFrame.Security.Claims;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -19,7 +21,10 @@ public class HopFrameAuthentication(
ISystemClock clock, ISystemClock clock,
ITokenRepository tokens, ITokenRepository tokens,
IPermissionRepository perms, IPermissionRepository perms,
IOptions<HopFrameAuthenticationOptions> tokenOptions) IOptions<HopFrameAuthenticationOptions> tokenOptions,
IOptions<OpenIdOptions> openIdOptions,
IUserRepository users,
IOpenIdAccessor accessor)
: AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder, clock) { : AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder, clock) {
public const string SchemeName = "HopFrame.Authentication"; public const string SchemeName = "HopFrame.Authentication";
@@ -30,8 +35,39 @@ public class HopFrameAuthentication(
if (string.IsNullOrEmpty(accessToken)) accessToken = Request.Headers["Token"]; if (string.IsNullOrEmpty(accessToken)) accessToken = Request.Headers["Token"];
if (string.IsNullOrEmpty(accessToken)) accessToken = Request.Query["token"]; if (string.IsNullOrEmpty(accessToken)) accessToken = Request.Query["token"];
if (string.IsNullOrEmpty(accessToken)) return AuthenticateResult.Fail("No Access Token provided"); if (string.IsNullOrEmpty(accessToken)) return AuthenticateResult.Fail("No Access Token provided");
var tokenEntry = await tokens.GetToken(accessToken); var tokenEntry = await tokens.GetToken(accessToken);
if (tokenEntry?.Type != Token.ApiTokenType && openIdOptions.Value.Enabled) {
var result = await accessor.InspectToken(accessToken);
if (result is null || !result.Active)
return AuthenticateResult.Fail("Invalid OpenID Connect token");
var email = result.Email;
if (string.IsNullOrEmpty(email))
return AuthenticateResult.Fail("OpenID user has no email associated to it");
var user = await users.GetUserByEmail(email);
if (user is null) {
if (!openIdOptions.Value.GenerateUsers)
return AuthenticateResult.Fail("OpenID user does not exist");
var username = result.PreferredUsername;
user = await users.AddUser(new User {
Email = email,
Username = username
});
}
var token = new Token {
Owner = user,
CreatedAt = DateTime.Now,
Type = Token.OpenIdTokenType
};
var identity = await GenerateClaims(token);
return AuthenticateResult.Success(new AuthenticationTicket(identity, Scheme.Name));
}
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");
@@ -42,17 +78,22 @@ public class HopFrameAuthentication(
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");
var principal = await GenerateClaims(tokenEntry);
return AuthenticateResult.Success(new AuthenticationTicket(principal, Scheme.Name));
}
private async Task<ClaimsPrincipal> GenerateClaims(Token token) {
var claims = new List<Claim> { var claims = new List<Claim> {
new(HopFrameClaimTypes.AccessTokenId, accessToken), new(HopFrameClaimTypes.AccessTokenId, token.TokenId.ToString()),
new(HopFrameClaimTypes.UserId, tokenEntry.Owner.Id.ToString()) new(HopFrameClaimTypes.UserId, token.Owner.Id.ToString())
}; };
var permissions = await perms.GetFullPermissions(tokenEntry); 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)));
var principal = new ClaimsPrincipal(); var principal = new ClaimsPrincipal();
principal.AddIdentity(new ClaimsIdentity(claims, SchemeName)); principal.AddIdentity(new ClaimsIdentity(claims, SchemeName));
return AuthenticateResult.Success(new AuthenticationTicket(principal, Scheme.Name)); return principal;
} }
} }

View File

@@ -1,3 +1,6 @@
using HopFrame.Security.Authentication.OpenID;
using HopFrame.Security.Authentication.OpenID.Implementation;
using HopFrame.Security.Authentication.OpenID.Options;
using HopFrame.Security.Authorization; using HopFrame.Security.Authorization;
using HopFrame.Security.Claims; using HopFrame.Security.Claims;
using HopFrame.Security.Options; using HopFrame.Security.Options;
@@ -20,8 +23,12 @@ public static class HopFrameAuthenticationExtensions {
service.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>(); service.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
service.AddScoped<ITokenContext, TokenContextImplementor>(); service.AddScoped<ITokenContext, TokenContextImplementor>();
service.AddHttpClient();
service.AddScoped<IOpenIdAccessor, OpenIdAccessor>();
service.AddOptionsFromConfiguration<HopFrameAuthenticationOptions>(configuration); service.AddOptionsFromConfiguration<HopFrameAuthenticationOptions>(configuration);
service.AddOptionsFromConfiguration<AdminPermissionOptions>(configuration); service.AddOptionsFromConfiguration<AdminPermissionOptions>(configuration);
service.AddOptionsFromConfiguration<OpenIdOptions>(configuration);
service.AddAuthentication(HopFrameAuthentication.SchemeName).AddScheme<AuthenticationSchemeOptions, HopFrameAuthentication>(HopFrameAuthentication.SchemeName, _ => {}); service.AddAuthentication(HopFrameAuthentication.SchemeName).AddScheme<AuthenticationSchemeOptions, HopFrameAuthentication>(HopFrameAuthentication.SchemeName, _ => {});
service.AddAuthorization(); service.AddAuthorization();

View File

@@ -0,0 +1,10 @@
using HopFrame.Security.Authentication.OpenID.Models;
namespace HopFrame.Security.Authentication.OpenID;
public interface IOpenIdAccessor {
Task<OpenIdConfiguration> LoadConfiguration();
Task<OpenIdToken> RequestToken(string code);
Task<string> ConstructAuthUri(string state = null);
Task<OpenIdIntrospection> InspectToken(string token);
}

View File

@@ -0,0 +1,64 @@
using System.Text.Json;
using HopFrame.Security.Authentication.OpenID.Models;
using HopFrame.Security.Authentication.OpenID.Options;
using Microsoft.Extensions.Options;
namespace HopFrame.Security.Authentication.OpenID.Implementation;
internal class OpenIdAccessor(IHttpClientFactory clientFactory, IOptions<OpenIdOptions> options) : IOpenIdAccessor {
public async Task<OpenIdConfiguration> LoadConfiguration() {
var client = clientFactory.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Get, Path.Combine(options.Value.Issuer, ".well-known/openid-configuration"));
var response = await client.SendAsync(request);
if (!response.IsSuccessStatusCode)
return null;
return await JsonSerializer.DeserializeAsync<OpenIdConfiguration>(await response.Content.ReadAsStreamAsync());
}
public async Task<OpenIdToken> RequestToken(string code) {
var configuration = await LoadConfiguration();
var client = clientFactory.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Post, configuration.TokenEndpoint) {
Content = new FormUrlEncodedContent(new Dictionary<string, string> {
{ "grant_type", "authorization_code" },
{ "code", code },
{ "redirect_uri", options.Value.Callback },
{ "client_id", options.Value.ClientId },
{ "client_secret", options.Value.ClientSecret }
})
};
var response = await client.SendAsync(request);
if (!response.IsSuccessStatusCode)
return null;
return await JsonSerializer.DeserializeAsync<OpenIdToken>(await response.Content.ReadAsStreamAsync());
}
public async Task<string> ConstructAuthUri(string state = null) {
var configuration = await LoadConfiguration();
return $"{configuration.AuthorizationEndpoint}?response_type=code&client_id={options.Value.ClientId}&redirect_uri={options.Value.Callback}&scope=openid%20profile%20email&state={state}";
}
public async Task<OpenIdIntrospection> InspectToken(string token) {
var configuration = await LoadConfiguration();
var client = clientFactory.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Post, configuration.IntrospectionEndpoint) {
Content = new FormUrlEncodedContent(new Dictionary<string, string> {
{ "token", token },
{ "client_id", options.Value.ClientId },
{ "client_secret", options.Value.ClientSecret }
})
};
var response = await client.SendAsync(request);
if (!response.IsSuccessStatusCode)
return null;
return await JsonSerializer.DeserializeAsync<OpenIdIntrospection>(await response.Content.ReadAsStreamAsync());
}
}

View File

@@ -0,0 +1,68 @@
using System.Text.Json.Serialization;
namespace HopFrame.Security.Authentication.OpenID.Models;
public sealed class OpenIdConfiguration {
[JsonPropertyName("issuer")]
public string Issuer { get; set; }
[JsonPropertyName("authorization_endpoint")]
public string AuthorizationEndpoint { get; set; }
[JsonPropertyName("token_endpoint")]
public string TokenEndpoint { get; set; }
[JsonPropertyName("userinfo_endpoint")]
public string UserinfoEndpoint { get; set; }
[JsonPropertyName("end_session_endpoint")]
public string EndSessionEndpoint { get; set; }
[JsonPropertyName("introspection_endpoint")]
public string IntrospectionEndpoint { get; set; }
[JsonPropertyName("revocation_endpoint")]
public string RevocationEndpoint { get; set; }
[JsonPropertyName("device_authorization_endpoint")]
public string DeviceAuthorizationEndpoint { get; set; }
[JsonPropertyName("response_types_supported")]
public List<string> ResponseTypesSupported { get; set; }
[JsonPropertyName("response_modes_supported")]
public List<string> ResponseModesSupported { get; set; }
[JsonPropertyName("jwks_uri")]
public string JwksUri { get; set; }
[JsonPropertyName("grant_types_supported")]
public List<string> GrantTypesSupported { get; set; }
[JsonPropertyName("id_token_signing_alg_values_supported")]
public List<string> IdTokenSigningAlgValuesSupported { get; set; }
[JsonPropertyName("subject_types_supported")]
public List<string> SubjectTypesSupported { get; set; }
[JsonPropertyName("token_endpoint_auth_methods_supported")]
public List<string> TokenEndpointAuthMethodsSupported { get; set; }
[JsonPropertyName("acr_values_supported")]
public List<string> AcrValuesSupported { get; set; }
[JsonPropertyName("scopes_supported")]
public List<string> ScopesSupported { get; set; }
[JsonPropertyName("request_parameter_supported")]
public bool RequestParameterSupported { get; set; }
[JsonPropertyName("claims_supported")]
public List<string> ClaimsSupported { get; set; }
[JsonPropertyName("claims_parameter_supported")]
public bool ClaimsParameterSupported { get; set; }
[JsonPropertyName("code_challenge_methods_supported")]
public List<string> CodeChallengeMethodsSupported { get; set; }
}

View File

@@ -0,0 +1,62 @@
using System.Text.Json.Serialization;
namespace HopFrame.Security.Authentication.OpenID.Models;
public sealed class OpenIdIntrospection {
[JsonPropertyName("iss")]
public string Issuer { get; set; }
[JsonPropertyName("sub")]
public string Subject { get; set; }
[JsonPropertyName("aud")]
public string Audience { get; set; }
[JsonPropertyName("exp")]
public long Expiration { get; set; }
[JsonPropertyName("iat")]
public long IssuedAt { get; set; }
[JsonPropertyName("auth_time")]
public long AuthTime { get; set; }
[JsonPropertyName("acr")]
public string Acr { get; set; }
[JsonPropertyName("amr")]
public List<string> AuthenticationMethods { get; set; }
[JsonPropertyName("sid")]
public string SessionId { get; set; }
[JsonPropertyName("email")]
public string Email { get; set; }
[JsonPropertyName("email_verified")]
public bool EmailVerified { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("given_name")]
public string GivenName { get; set; }
[JsonPropertyName("preferred_username")]
public string PreferredUsername { get; set; }
[JsonPropertyName("nickname")]
public string Nickname { get; set; }
[JsonPropertyName("groups")]
public List<string> Groups { get; set; }
[JsonPropertyName("active")]
public bool Active { get; set; }
[JsonPropertyName("scope")]
public string Scope { get; set; }
[JsonPropertyName("client_id")]
public string ClientId { get; set; }
}

View File

@@ -0,0 +1,17 @@
using System.Text.Json.Serialization;
namespace HopFrame.Security.Authentication.OpenID.Models;
public sealed class OpenIdToken {
[JsonPropertyName("access_token")]
public string AccessToken { get; set; }
[JsonPropertyName("token_type")]
public string TokenType { get; set; }
[JsonPropertyName("expires_in")]
public int ExpiresIn { get; set; }
[JsonPropertyName("id_token")]
public string IdToken { get; set; }
}

View File

@@ -0,0 +1,15 @@
using HopFrame.Security.Options;
namespace HopFrame.Security.Authentication.OpenID.Options;
public sealed class OpenIdOptions : OptionsFromConfiguration {
public override string Position { get; } = "HopFrame:Authentication:OpenID";
public bool Enabled { get; set; } = false;
public bool GenerateUsers { get; set; } = true;
public string Issuer { get; set; }
public string ClientId { get; set; }
public string ClientSecret { get; set; }
public string Callback { get; set; }
}

View File

@@ -21,6 +21,4 @@ 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

@@ -1,15 +1,19 @@
using HopFrame.Database.Models; using HopFrame.Database.Models;
using HopFrame.Database.Repositories; using HopFrame.Database.Repositories;
using HopFrame.Security.Authentication.OpenID.Options;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
namespace HopFrame.Security.Claims; namespace HopFrame.Security.Claims;
internal sealed class TokenContextImplementor(IHttpContextAccessor accessor, IUserRepository users, ITokenRepository tokens, IPermissionRepository permissions) : ITokenContext { internal sealed class TokenContextImplementor(IHttpContextAccessor accessor, IUserRepository users, ITokenRepository tokens, IOptions<OpenIdOptions> options) : 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 => options.Value.Enabled ? new Token {
Owner = User,
public IList<string> ContextualPermissions => permissions.GetFullPermissions(AccessToken).GetAwaiter().GetResult(); Type = Token.OpenIdTokenType,
CreatedAt = DateTime.Now
} : tokens.GetToken(accessor.HttpContext?.User.GetAccessTokenId()).GetAwaiter().GetResult();
} }

View File

@@ -0,0 +1,28 @@
using HopFrame.Security.Authentication.OpenID;
using Microsoft.AspNetCore.Mvc;
using HopFrame.Security.Authentication.OpenID.Models;
namespace HopFrame.Testing.Api.Controllers;
public class AuthController(IOpenIdAccessor accessor) : Controller {
[HttpGet("auth/callback")]
public async Task<ActionResult<string>> Callback([FromQuery] string code, [FromQuery] string state) {
if (string.IsNullOrEmpty(code)) {
return BadRequest("Authorization code is missing");
}
var token = await accessor.RequestToken(code);
return Ok(token.AccessToken);
}
[HttpGet("auth")]
public async Task<ActionResult> Authenticate() {
return Redirect(await accessor.ConstructAuthUri());
}
[HttpGet("check")]
public async Task<ActionResult<OpenIdIntrospection>> Check([FromQuery] string token) {
return Ok(await accessor.InspectToken(token));
}
}

View File

@@ -15,8 +15,8 @@ namespace HopFrame.Testing.Api.Controllers;
public class TestController(ITokenContext userContext, DatabaseContext context, ITokenRepository tokens, IPermissionRepository permissions) : ControllerBase { public class TestController(ITokenContext userContext, DatabaseContext context, ITokenRepository tokens, IPermissionRepository permissions) : ControllerBase {
[HttpGet("permissions"), Authorized] [HttpGet("permissions"), Authorized]
public ActionResult<IList<string>> Permissions() { public async Task<ActionResult<IList<string>>> Permissions() {
return new ActionResult<IList<string>>(userContext.ContextualPermissions); return new ActionResult<IList<string>>(await permissions.GetFullPermissions(userContext.AccessToken));
} }
[HttpGet("generate")] [HttpGet("generate")]

View File

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

View File

@@ -2,6 +2,8 @@ using System.Text.Encodings.Web;
using HopFrame.Database.Models; using HopFrame.Database.Models;
using HopFrame.Database.Repositories; using HopFrame.Database.Repositories;
using HopFrame.Security.Authentication; using HopFrame.Security.Authentication;
using HopFrame.Security.Authentication.OpenID;
using HopFrame.Security.Authentication.OpenID.Options;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -46,7 +48,17 @@ public class AuthenticationTests {
.Setup(x => x.GetFullPermissions(It.IsAny<Token>())) .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()),
new OptionsWrapper<OpenIdOptions>(new OpenIdOptions()),
new Mock<IUserRepository>().Object,
new Mock<IOpenIdAccessor>().Object);
var context = new DefaultHttpContext(); var context = new DefaultHttpContext();
if (provideCorrectToken) if (provideCorrectToken)
context.HttpContext.Request.Headers.Append(HopFrameAuthentication.SchemeName, correctToken.TokenId.ToString()); context.HttpContext.Request.Headers.Append(HopFrameAuthentication.SchemeName, correctToken.TokenId.ToString());