finished OpenID integration
This commit is contained in:
@@ -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">
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAsyncValueTaskMethodBuilderT_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003F1e8feaadf5c3fa14d36ea2a638c432a2e1a47b7837d8b83d88303c5d9c15cf_003FAsyncValueTaskMethodBuilderT_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<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_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>
|
||||
@@ -6,6 +7,7 @@
|
||||
<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_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_003ARedirectResult_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003F2261f0cc7e883c3c89b8e6c7ea509ac7c52d0629e68690ed216e1cc0c8ac_003FRedirectResult_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/Environment/AssemblyExplorer/XmlDocument/@EntryValue"><AssemblyExplorer>
|
||||
@@ -74,6 +76,8 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
|
||||
namespace HopFrame.Api.Controller;
|
||||
|
||||
public class HopFrameFeatureProvider(params Type[] controllerTypes) : ControllerFeatureProvider {
|
||||
protected override bool IsController(TypeInfo typeInfo) {
|
||||
if (typeInfo.Namespace != typeof(HopFrameFeatureProvider).Namespace)
|
||||
return base.IsController(typeInfo);
|
||||
|
||||
if (controllerTypes.All(c => c.Name != typeInfo.Name))
|
||||
return false;
|
||||
|
||||
return base.IsController(typeInfo);
|
||||
}
|
||||
}
|
||||
@@ -10,10 +10,11 @@ namespace HopFrame.Api.Controller;
|
||||
|
||||
[ApiController, Route("api/v1/openid")]
|
||||
public class OpenIdController(IOpenIdAccessor accessor, IOptions<OpenIdOptions> options) : ControllerBase {
|
||||
public const string DefaultCallback = "api/v1/openid/callback";
|
||||
|
||||
[HttpGet("redirect")]
|
||||
public async Task<IActionResult> RedirectToProvider([FromQuery] string redirectAfter, [FromQuery] int performRedirect = 1) {
|
||||
var uri = await accessor.ConstructAuthUri(redirectAfter);
|
||||
var uri = await accessor.ConstructAuthUri(DefaultCallback, redirectAfter);
|
||||
|
||||
if (performRedirect == 1) {
|
||||
return Redirect(uri);
|
||||
@@ -28,7 +29,11 @@ public class OpenIdController(IOpenIdAccessor accessor, IOptions<OpenIdOptions>
|
||||
return BadRequest("Authorization code is missing");
|
||||
}
|
||||
|
||||
var token = await accessor.RequestToken(code);
|
||||
var token = await accessor.RequestToken(code, DefaultCallback);
|
||||
|
||||
if (token is null) {
|
||||
return Forbid("Authorization code is not valid");
|
||||
}
|
||||
|
||||
Response.Cookies.Append(ITokenContext.AccessTokenType, token.AccessToken, new CookieOptions {
|
||||
MaxAge = TimeSpan.FromSeconds(token.ExpiresIn),
|
||||
|
||||
@@ -19,7 +19,10 @@ public static class ServiceCollectionExtensions {
|
||||
/// <param name="configuration">The configuration used to configure HopFrame authentication</param>
|
||||
/// <typeparam name="TDbContext">The data source for all HopFrame entities</typeparam>
|
||||
public static void AddHopFrame<TDbContext>(this IServiceCollection services, ConfigurationManager configuration) where TDbContext : HopDbContextBase {
|
||||
var controllers = new List<Type> { typeof(AuthController) };
|
||||
var controllers = new List<Type>();
|
||||
|
||||
if (configuration.GetValue<bool>("HopFrame:Authentication:DefaultAuthentication"))
|
||||
controllers.Add(typeof(AuthController));
|
||||
|
||||
if (configuration.GetValue<bool>("HopFrame:Authentication:OpenID:Enabled"))
|
||||
controllers.Add(typeof(OpenIdController));
|
||||
|
||||
@@ -12,6 +12,8 @@ namespace HopFrame.Api.Logic.Implementation;
|
||||
internal class AuthLogic(IUserRepository users, ITokenRepository tokens, ITokenContext tokenContext, IHttpContextAccessor accessor, IOptions<HopFrameAuthenticationOptions> options) : IAuthLogic {
|
||||
|
||||
public async Task<LogicResult<SingleValueResult<string>>> Login(UserLogin login) {
|
||||
if (!options.Value.DefaultAuthentication) return LogicResult<SingleValueResult<string>>.BadRequest("HopFrame authentication scheme is disabled");
|
||||
|
||||
var user = await users.GetUserByEmail(login.Email);
|
||||
|
||||
if (user is null)
|
||||
@@ -38,6 +40,8 @@ internal class AuthLogic(IUserRepository users, ITokenRepository tokens, ITokenC
|
||||
}
|
||||
|
||||
public async Task<LogicResult<SingleValueResult<string>>> Register(UserRegister register) {
|
||||
if (!options.Value.DefaultAuthentication) return LogicResult<SingleValueResult<string>>.BadRequest("HopFrame authentication scheme is disabled");
|
||||
|
||||
if (register.Password.Length < 8)
|
||||
return LogicResult<SingleValueResult<string>>.BadRequest("Password needs to be at least 8 characters long");
|
||||
|
||||
@@ -69,6 +73,8 @@ internal class AuthLogic(IUserRepository users, ITokenRepository tokens, ITokenC
|
||||
}
|
||||
|
||||
public async Task<LogicResult<SingleValueResult<string>>> Authenticate() {
|
||||
if (!options.Value.DefaultAuthentication) return LogicResult<SingleValueResult<string>>.BadRequest("HopFrame authentication scheme is disabled");
|
||||
|
||||
var refreshToken = accessor.HttpContext?.Request.Cookies[ITokenContext.RefreshTokenType];
|
||||
|
||||
if (string.IsNullOrEmpty(refreshToken))
|
||||
|
||||
@@ -38,7 +38,7 @@ public class HopFrameAuthentication(
|
||||
|
||||
var tokenEntry = await tokens.GetToken(accessToken);
|
||||
|
||||
if (tokenEntry?.Type != Token.ApiTokenType && openIdOptions.Value.Enabled) {
|
||||
if (tokenEntry?.Type != Token.ApiTokenType && openIdOptions.Value.Enabled && !Guid.TryParse(accessToken, out _)) {
|
||||
var result = await accessor.InspectToken(accessToken);
|
||||
|
||||
if (result is null || !result.Active)
|
||||
@@ -65,10 +65,13 @@ public class HopFrameAuthentication(
|
||||
CreatedAt = DateTime.Now,
|
||||
Type = Token.OpenIdTokenType
|
||||
};
|
||||
var identity = await GenerateClaims(token);
|
||||
var identity = await GenerateClaims(token, perms);
|
||||
return AuthenticateResult.Success(new AuthenticationTicket(identity, Scheme.Name));
|
||||
}
|
||||
|
||||
if (!tokenOptions.Value.DefaultAuthentication)
|
||||
return AuthenticateResult.Fail("HopFrame authentication scheme is disabled");
|
||||
|
||||
if (tokenEntry is null) return AuthenticateResult.Fail("The provided Access Token does not exist");
|
||||
|
||||
if (tokenEntry.Type == Token.ApiTokenType) {
|
||||
@@ -78,11 +81,11 @@ public class HopFrameAuthentication(
|
||||
if (tokenEntry.Owner is null)
|
||||
return AuthenticateResult.Fail("The provided Access Token does not match any user");
|
||||
|
||||
var principal = await GenerateClaims(tokenEntry);
|
||||
var principal = await GenerateClaims(tokenEntry, perms);
|
||||
return AuthenticateResult.Success(new AuthenticationTicket(principal, Scheme.Name));
|
||||
}
|
||||
|
||||
private async Task<ClaimsPrincipal> GenerateClaims(Token token) {
|
||||
public static async Task<ClaimsPrincipal> GenerateClaims(Token token, IPermissionRepository perms) {
|
||||
var claims = new List<Claim> {
|
||||
new(HopFrameClaimTypes.AccessTokenId, token.TokenId.ToString()),
|
||||
new(HopFrameClaimTypes.UserId, token.Owner.Id.ToString())
|
||||
|
||||
@@ -8,6 +8,8 @@ public class HopFrameAuthenticationOptions : OptionsFromConfiguration {
|
||||
public TimeSpan AccessTokenTime => AccessToken is null ? new(0, 0, 5, 0) : AccessToken.ConstructTimeSpan;
|
||||
public TimeSpan RefreshTokenTime => RefreshToken is null ? new(30, 0, 0, 0) : RefreshToken.ConstructTimeSpan;
|
||||
|
||||
public bool DefaultAuthentication { get; set; } = true;
|
||||
|
||||
public TokenTime AccessToken { get; set; }
|
||||
public TokenTime RefreshToken { get; set; }
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ namespace HopFrame.Security.Authentication.OpenID;
|
||||
|
||||
public interface IOpenIdAccessor {
|
||||
Task<OpenIdConfiguration> LoadConfiguration();
|
||||
Task<OpenIdToken> RequestToken(string code);
|
||||
Task<string> ConstructAuthUri(string state = null);
|
||||
Task<OpenIdToken> RequestToken(string code, string defaultCallback);
|
||||
Task<string> ConstructAuthUri(string defaultCallback, string state = null);
|
||||
Task<OpenIdIntrospection> InspectToken(string token);
|
||||
Task<OpenIdToken> RefreshAccessToken(string refreshToken);
|
||||
}
|
||||
@@ -8,8 +8,6 @@ using Microsoft.Extensions.Options;
|
||||
namespace HopFrame.Security.Authentication.OpenID.Implementation;
|
||||
|
||||
internal class OpenIdAccessor(IHttpClientFactory clientFactory, IOptions<OpenIdOptions> options, IHttpContextAccessor accessor, IMemoryCache cache) : IOpenIdAccessor {
|
||||
private const string DefaultCallbackEndpoint = "api/v1/openid/callback";
|
||||
|
||||
private const string ConfigurationCacheKey = "HopFrame:OpenID:Configuration";
|
||||
private const string AuthCodeCacheKey = "HopFrame:OpenID:Code:";
|
||||
private const string TokenCacheKey = "HopFrame:OpenID:Token:";
|
||||
@@ -34,13 +32,13 @@ internal class OpenIdAccessor(IHttpClientFactory clientFactory, IOptions<OpenIdO
|
||||
return config;
|
||||
}
|
||||
|
||||
public async Task<OpenIdToken> RequestToken(string code) {
|
||||
public async Task<OpenIdToken> RequestToken(string code, string defaultCallback) {
|
||||
if (options.Value.Cache.Enabled && options.Value.Cache.Auth.Enabled && cache.TryGetValue(AuthCodeCacheKey + code, out object cachedToken)) {
|
||||
return cachedToken as OpenIdToken;
|
||||
}
|
||||
|
||||
var protocol = accessor.HttpContext!.Request.IsHttps ? "https" : "http";
|
||||
var callback = options.Value.Callback ?? $"{protocol}://{accessor.HttpContext!.Request.Host.Value}/{DefaultCallbackEndpoint}";
|
||||
var callback = options.Value.Callback ?? $"{protocol}://{accessor.HttpContext!.Request.Host.Value}/{defaultCallback}";
|
||||
|
||||
var configuration = await LoadConfiguration();
|
||||
|
||||
@@ -67,9 +65,9 @@ internal class OpenIdAccessor(IHttpClientFactory clientFactory, IOptions<OpenIdO
|
||||
return token;
|
||||
}
|
||||
|
||||
public async Task<string> ConstructAuthUri(string state = null) {
|
||||
public async Task<string> ConstructAuthUri(string defaultCallback, string state = null) {
|
||||
var protocol = accessor.HttpContext!.Request.IsHttps ? "https" : "http";
|
||||
var callback = options.Value.Callback ?? $"{protocol}://{accessor.HttpContext!.Request.Host.Value}/{DefaultCallbackEndpoint}";
|
||||
var callback = options.Value.Callback ?? $"{protocol}://{accessor.HttpContext!.Request.Host.Value}/{defaultCallback}";
|
||||
|
||||
var configuration = await LoadConfiguration();
|
||||
return $"{configuration.AuthorizationEndpoint}?response_type=code&client_id={options.Value.ClientId}&redirect_uri={callback}&scope=openid%20profile%20email%20offline_access&state={state}";
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using System.Security.Claims;
|
||||
using HopFrame.Database.Repositories;
|
||||
using HopFrame.Security.Authentication;
|
||||
using HopFrame.Security.Claims;
|
||||
using HopFrame.Web.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
@@ -21,15 +20,9 @@ public sealed class AuthMiddleware(IAuthService auth, IPermissionRepository perm
|
||||
return;
|
||||
}
|
||||
|
||||
var claims = new List<Claim> {
|
||||
new(HopFrameClaimTypes.AccessTokenId, token.TokenId.ToString()),
|
||||
new(HopFrameClaimTypes.UserId, token.Owner.Id.ToString())
|
||||
};
|
||||
|
||||
var permissions = await perms.GetFullPermissions(token);
|
||||
claims.AddRange(permissions.Select(perm => new Claim(HopFrameClaimTypes.Permission, perm)));
|
||||
|
||||
context.User.AddIdentity(new ClaimsIdentity(claims, HopFrameAuthentication.SchemeName));
|
||||
var principal = await HopFrameAuthentication.GenerateClaims(token, perms);
|
||||
if (principal?.Identity is ClaimsIdentity identity)
|
||||
context.User.AddIdentity(identity);
|
||||
}
|
||||
|
||||
await next?.Invoke(context);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using HopFrame.Database.Models;
|
||||
using HopFrame.Database.Repositories;
|
||||
using HopFrame.Security.Authentication;
|
||||
using HopFrame.Security.Authentication.OpenID;
|
||||
using HopFrame.Security.Authentication.OpenID.Options;
|
||||
using HopFrame.Security.Claims;
|
||||
using HopFrame.Security.Models;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
@@ -13,10 +15,15 @@ internal class AuthService(
|
||||
IHttpContextAccessor httpAccessor,
|
||||
ITokenRepository tokens,
|
||||
ITokenContext context,
|
||||
IOptions<HopFrameAuthenticationOptions> options)
|
||||
IOptions<HopFrameAuthenticationOptions> options,
|
||||
IOptions<OpenIdOptions> openIdOptions,
|
||||
IOpenIdAccessor accessor,
|
||||
IUserRepository users)
|
||||
: IAuthService {
|
||||
|
||||
public async Task Register(UserRegister register) {
|
||||
if (!options.Value.DefaultAuthentication) return;
|
||||
|
||||
var user = await userService.AddUser(new User {
|
||||
Username = register.Username,
|
||||
Email = register.Email,
|
||||
@@ -41,6 +48,8 @@ internal class AuthService(
|
||||
}
|
||||
|
||||
public async Task<bool> Login(UserLogin login) {
|
||||
if (!options.Value.DefaultAuthentication) return false;
|
||||
|
||||
var user = await userService.GetUserByEmail(login.Email);
|
||||
|
||||
if (user == null) return false;
|
||||
@@ -75,6 +84,45 @@ internal class AuthService(
|
||||
|
||||
if (string.IsNullOrWhiteSpace(refreshToken)) return null;
|
||||
|
||||
if (openIdOptions.Value.Enabled && !Guid.TryParse(refreshToken, out _)) {
|
||||
var openIdToken = await accessor.RefreshAccessToken(refreshToken);
|
||||
|
||||
if (openIdToken is null)
|
||||
return null;
|
||||
|
||||
var inspection = await accessor.InspectToken(openIdToken.AccessToken);
|
||||
|
||||
var email = inspection.Email;
|
||||
if (string.IsNullOrEmpty(email))
|
||||
return null;
|
||||
|
||||
var user = await users.GetUserByEmail(email);
|
||||
if (user is null) {
|
||||
if (!openIdOptions.Value.GenerateUsers)
|
||||
return null;
|
||||
|
||||
var username = inspection.PreferredUsername;
|
||||
user = await users.AddUser(new User {
|
||||
Email = email,
|
||||
Username = username
|
||||
});
|
||||
}
|
||||
|
||||
httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, openIdToken.AccessToken, new CookieOptions {
|
||||
MaxAge = TimeSpan.FromSeconds(openIdToken.ExpiresIn),
|
||||
HttpOnly = false,
|
||||
Secure = true
|
||||
});
|
||||
return new() {
|
||||
Owner = user,
|
||||
CreatedAt = DateTime.Now,
|
||||
Type = Token.OpenIdTokenType
|
||||
};
|
||||
}
|
||||
|
||||
if (!options.Value.DefaultAuthentication)
|
||||
return null;
|
||||
|
||||
var token = await tokens.GetToken(refreshToken);
|
||||
|
||||
if (token is null || token.Type != Token.RefreshTokenType) return null;
|
||||
@@ -96,7 +144,7 @@ internal class AuthService(
|
||||
var accessToken = context.AccessToken;
|
||||
|
||||
if (accessToken is null) return false;
|
||||
if (accessToken.Type != Token.AccessTokenType) return false;
|
||||
if (accessToken.Type != Token.AccessTokenType && accessToken.Type != Token.OpenIdTokenType) return false;
|
||||
if (accessToken.CreatedAt + options.Value.AccessTokenTime < DateTime.Now) return false;
|
||||
if (accessToken.Owner is null) return false;
|
||||
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
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> 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);
|
||||
}
|
||||
|
||||
[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));
|
||||
}
|
||||
}
|
||||
@@ -329,7 +329,7 @@ public class AuthLogicTests {
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Logout_With_NoAccessToken_Should_Fail() {
|
||||
public async Task Logout_With_NoAccessToken_Should_Succeed() {
|
||||
// Arrange
|
||||
var (auth, context) = SetupEnvironment(provideAccessToken: false);
|
||||
context.Response.Cookies.Append(ITokenContext.AccessTokenType, _accessToken.ToString());
|
||||
@@ -339,14 +339,13 @@ public class AuthLogicTests {
|
||||
var result = await auth.Logout();
|
||||
|
||||
// Assert
|
||||
Assert.False(result.IsSuccessful);
|
||||
Assert.Equal(HttpStatusCode.Conflict, result.State);
|
||||
Assert.Equal(_accessToken.ToString(), context.Response.Headers.FindCookie(ITokenContext.AccessTokenType));
|
||||
Assert.Equal(_refreshToken.ToString(), context.Response.Headers.FindCookie(ITokenContext.RefreshTokenType));
|
||||
Assert.True(result.IsSuccessful);
|
||||
Assert.Null(context.Response.Headers.FindCookie(ITokenContext.AccessTokenType));
|
||||
Assert.Null(context.Response.Headers.FindCookie(ITokenContext.RefreshTokenType));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Logout_With_NoRefreshToken_Should_Fail() {
|
||||
public async Task Logout_With_NoRefreshToken_Should_Succeed() {
|
||||
// Arrange
|
||||
var (auth, context) = SetupEnvironment();
|
||||
context.Response.Cookies.Append(ITokenContext.AccessTokenType, _accessToken.ToString());
|
||||
@@ -356,10 +355,9 @@ public class AuthLogicTests {
|
||||
var result = await auth.Logout();
|
||||
|
||||
// Assert
|
||||
Assert.False(result.IsSuccessful);
|
||||
Assert.Equal(HttpStatusCode.Conflict, result.State);
|
||||
Assert.Equal(_accessToken.ToString(), context.Response.Headers.FindCookie(ITokenContext.AccessTokenType));
|
||||
Assert.Equal(_refreshToken.ToString(), context.Response.Headers.FindCookie(ITokenContext.RefreshTokenType));
|
||||
Assert.True(result.IsSuccessful);
|
||||
Assert.Null(context.Response.Headers.FindCookie(ITokenContext.AccessTokenType));
|
||||
Assert.Null(context.Response.Headers.FindCookie(ITokenContext.RefreshTokenType));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using HopFrame.Database.Models;
|
||||
using HopFrame.Database.Repositories;
|
||||
using HopFrame.Security.Authentication;
|
||||
using HopFrame.Security.Authentication.OpenID;
|
||||
using HopFrame.Security.Authentication.OpenID.Options;
|
||||
using HopFrame.Security.Claims;
|
||||
using HopFrame.Security.Models;
|
||||
using HopFrame.Tests.Web.Extensions;
|
||||
@@ -68,7 +70,16 @@ public class AuthServiceTests {
|
||||
.Setup(c => c.AccessToken)
|
||||
.Returns(providedAccessToken);
|
||||
|
||||
return (new AuthService(users.Object, accessor, tokens.Object, context.Object, new OptionsWrapper<HopFrameAuthenticationOptions>(new HopFrameAuthenticationOptions())), accessor.HttpContext);
|
||||
return (new AuthService(
|
||||
users.Object,
|
||||
accessor,
|
||||
tokens.Object,
|
||||
context.Object,
|
||||
new OptionsWrapper<HopFrameAuthenticationOptions>(new HopFrameAuthenticationOptions()),
|
||||
new OptionsWrapper<OpenIdOptions>(new OpenIdOptions()),
|
||||
new Mock<IOpenIdAccessor>().Object,
|
||||
users.Object
|
||||
), accessor.HttpContext);
|
||||
}
|
||||
|
||||
private User CreateDummyUser() => new() {
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Security.Claims;
|
||||
using Bunit;
|
||||
using Bunit.TestDoubles;
|
||||
using HopFrame.Security.Authentication;
|
||||
using HopFrame.Security.Authentication.OpenID;
|
||||
using HopFrame.Security.Claims;
|
||||
using HopFrame.Web.Components;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
Reference in New Issue
Block a user