From 2ec02f2f204fe92f78058e21c250351df5243dcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Can=20Karag=C3=B6z?= Date: Mon, 12 Oct 2026 03:07:59 +0300 Subject: [PATCH] Remove Original Options from AuthFlowContext and Refactor Session Validation, Fix Mode Based Options not Using --- .../Session/SessionIssuanceContext.cs | 2 + .../Session/SessionRotationContext.cs | 2 + .../Session/SessionValidationContext.cs | 6 + .../Auth/Context/AuthFlowContext.cs | 9 +- .../Auth/Context/AuthFlowContextFactory.cs | 40 +++--- .../Auth/EffectiveServerOptionsProvider.cs | 8 +- .../Response/EffectiveAuthModeResolver.cs | 2 +- .../Response/IEffectiveAuthModeResolver.cs | 2 +- .../AspNetCore/UAuthAuthenticationHandler.cs | 1 + .../UAuthResourceAuthenticationHandler.cs | 2 + .../Endpoints/PkceEndpointHandler.cs | 2 +- .../Endpoints/RefreshEndpointHandler.cs | 2 +- .../Endpoints/ValidateEndpointHandler.cs | 3 +- .../UAuthApplicationBuilderExtensions.cs | 2 +- .../Flows/Login/LoginOrchestrator.cs | 22 ++- .../Cookies/UAuthCookiePolicyBuilder.cs | 10 +- .../Issuers/UAuthSessionIssuer.cs | 31 +++-- .../Issuers/UAuthTokenIssuer.cs | 4 +- .../Redirect/AuthRedirectResolver.cs | 8 +- ...s => RemoteSessionValidationMiddleware.cs} | 44 +++--- .../Options/Defaults/ConfigureDefaults.cs | 1 - .../IEffectiveServerOptionsProvider.cs | 4 +- .../UAuthServerSessionOptionsValidator.cs | 3 +- .../Services/RefreshFlowService.cs | 13 +- .../Services/RefreshTokenRotationService.cs | 4 +- .../Services/UAuthSessionValidator.cs | 12 +- .../Credentials/ChangePasswordTests.cs | 2 - .../Helpers/AuthFlowTestFactory.cs | 3 +- .../Helpers/TestAuthModeResolver.cs | 3 +- .../RemoteSessionValidatorTests.cs | 7 +- .../Server/CookieInfrastructureTests.cs | 28 ++-- .../Server/EffectiveAuthModeResolverTests.cs | 2 +- .../EffectiveServerOptionsProviderTests.cs | 36 ++--- .../Server/LogoutEndpointHandlerTests.cs | 6 +- .../Server/PkceEndpointHandlerTests.cs | 5 +- .../Server/RedirectTests.cs | 12 +- .../Server/RefreshFlowServiceTests.cs | 3 +- .../RefreshTokenRotationServiceTests.cs | 3 +- .../Server/SessionEndpointHandlerTests.cs | 3 +- .../Server/UAuthFlowServiceTests.cs | 3 +- ...UAuthResourceAuthenticationHandlerTests.cs | 18 ++- .../Server/UAuthSessionValidatorTests.cs | 6 +- .../Sessions/UAuthSessionIssuerTests.cs | 125 +++++++++--------- .../Tokens/UAuthTokenIssuerTests.cs | 47 ++++++- 44 files changed, 291 insertions(+), 260 deletions(-) rename src/CodeBeam.UltimateAuth.Server/Middlewares/{SessionValidationMiddleware.cs => RemoteSessionValidationMiddleware.cs} (51%) diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionIssuanceContext.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionIssuanceContext.cs index 1b7886a7..65e11a33 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionIssuanceContext.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionIssuanceContext.cs @@ -1,5 +1,6 @@ using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Core.MultiTenancy; +using CodeBeam.UltimateAuth.Core.Options; namespace CodeBeam.UltimateAuth.Core.Contracts; @@ -16,6 +17,7 @@ public sealed class SessionIssuanceContext public ClaimsSnapshot? Claims { get; init; } public required SessionMetadata Metadata { get; init; } public required UAuthMode Mode { get; init; } + public required UAuthSessionOptions SessionOptions { get; init; } /// /// Optional chain identifier. diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionRotationContext.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionRotationContext.cs index 3be8b529..9543328d 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionRotationContext.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionRotationContext.cs @@ -1,5 +1,6 @@ using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Core.MultiTenancy; +using CodeBeam.UltimateAuth.Core.Options; namespace CodeBeam.UltimateAuth.Core.Contracts; @@ -13,4 +14,5 @@ public sealed record SessionRotationContext public ClaimsSnapshot? Claims { get; init; } public required SessionMetadata Metadata { get; init; } = SessionMetadata.Empty; public required UAuthMode Mode { get; init; } + public required UAuthSessionOptions SessionOptions { get; init; } } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionValidationContext.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionValidationContext.cs index 6a493e06..da29eb5f 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionValidationContext.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionValidationContext.cs @@ -1,5 +1,6 @@ using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Core.MultiTenancy; +using CodeBeam.UltimateAuth.Core.Options; namespace CodeBeam.UltimateAuth.Core.Contracts; @@ -9,4 +10,9 @@ public sealed record SessionValidationContext public AuthSessionId SessionId { get; init; } public DateTimeOffset Now { get; init; } public required DeviceContext Device { get; init; } + + /// + /// Required session options for validation. Remote session validators may not include. + /// + public UAuthSessionOptions? SessionOptions { get; init; } } diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthFlowContext.cs b/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthFlowContext.cs index acef799a..ad1f8c9c 100644 --- a/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthFlowContext.cs +++ b/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthFlowContext.cs @@ -21,8 +21,7 @@ public sealed class AuthFlowContext public bool IsAuthenticated { get; } public UserKey? UserKey { get; } - public UAuthServerOptions OriginalOptions { get; } - public EffectiveUAuthServerOptions EffectiveOptions { get; } + public UAuthServerOptions Options { get; } public EffectiveAuthResponse Response { get; } public ReturnUrlInfo ReturnUrlInfo { get; } @@ -46,8 +45,7 @@ internal AuthFlowContext( bool isAuthenticated, UserKey? userKey, SessionSecurityContext? session, - UAuthServerOptions originalOptions, - EffectiveUAuthServerOptions effectiveOptions, + UAuthServerOptions options, EffectiveAuthResponse response, PrimaryTokenKind primaryTokenKind, ReturnUrlInfo returnUrlInfo) @@ -65,8 +63,7 @@ internal AuthFlowContext( IsAuthenticated = isAuthenticated; UserKey = userKey; - OriginalOptions = originalOptions; - EffectiveOptions = effectiveOptions; + Options = options; Response = response; PrimaryTokenKind = primaryTokenKind; diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthFlowContextFactory.cs b/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthFlowContextFactory.cs index 0172602d..94069813 100644 --- a/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthFlowContextFactory.cs +++ b/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthFlowContextFactory.cs @@ -2,7 +2,6 @@ using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Core.Infrastructure; -using CodeBeam.UltimateAuth.Core.MultiTenancy; using CodeBeam.UltimateAuth.Core.Options; using CodeBeam.UltimateAuth.Server.Abstractions; using CodeBeam.UltimateAuth.Server.Extensions; @@ -50,19 +49,20 @@ public async ValueTask CreateAsync(HttpContext ctx, AuthFlowTyp var user = ctx.GetUserContext(); var clientProfile = await _clientProfileReader.ReadAsync(ctx); - var originalOptions = _serverOptionsProvider.GetOriginal(ctx); - var effectiveOptions = _serverOptionsProvider.GetEffective(ctx, flowType, clientProfile); - var allowedModes = originalOptions.AllowedModes; + var effective = _serverOptionsProvider.GetEffective(ctx, clientProfile); + var options = effective.Options; - if (allowedModes is { Count: > 0 } && !allowedModes.Contains(effectiveOptions.Mode)) + var effectiveMode = effective.Mode; + var allowedModes = options.AllowedModes; + + if (allowedModes is { Count: > 0 } && !allowedModes.Contains(effectiveMode)) { - throw new InvalidOperationException($"Auth mode '{effectiveOptions.Mode}' is not allowed by server configuration."); + throw new InvalidOperationException($"Auth mode '{effectiveMode}' is not allowed by server configuration."); } - var effectiveMode = effectiveOptions.Mode; var primaryTokenKind = _primaryTokenResolver.Resolve(effectiveMode); - var response = _authResponseResolver.Resolve(effectiveMode, flowType, clientProfile, effectiveOptions); + var response = _authResponseResolver.Resolve(effectiveMode, flowType, clientProfile, effective); var deviceInfo = await _deviceResolver.ResolveAsync(ctx); var deviceContext = _deviceContextFactory.Create(deviceInfo); var returnUrl = await ctx.GetReturnUrlAsync(); @@ -78,6 +78,7 @@ public async ValueTask CreateAsync(HttpContext ctx, AuthFlowTyp Tenant = tenant, SessionId = sessionCtx.SessionId!.Value, Device = deviceContext, + SessionOptions = options.Session, Now = _clock.UtcNow }, ct); @@ -100,8 +101,7 @@ public async ValueTask CreateAsync(HttpContext ctx, AuthFlowTyp user?.IsAuthenticated ?? false, user?.UserId, sessionSecurityContext, - originalOptions, - effectiveOptions, + options, response, primaryTokenKind, returnUrlInfo @@ -113,19 +113,19 @@ public async ValueTask RecreateWithClientProfileAsync(AuthFlowC var flowType = existing.FlowType; var tenant = existing.Tenant; - var originalOptions = existing.OriginalOptions; - var effectiveOptions = _serverOptionsProvider.GetEffective(tenant, flowType, overriddenProfile); + var effective = _serverOptionsProvider.GetEffective(tenant, overriddenProfile); + var options = effective.Options; + var effectiveMode = effective.Mode; - var allowedModes = originalOptions.AllowedModes; + var allowedModes = options.AllowedModes; - if (allowedModes is { Count: > 0 } && !allowedModes.Contains(effectiveOptions.Mode)) + if (allowedModes is { Count: > 0 } && !allowedModes.Contains(effectiveMode)) { - throw new InvalidOperationException($"Auth mode '{effectiveOptions.Mode}' is not allowed by server configuration."); + throw new InvalidOperationException($"Auth mode '{effectiveMode}' is not allowed by server configuration."); } - var effectiveMode = effectiveOptions.Mode; var primaryTokenKind = _primaryTokenResolver.Resolve(effectiveMode); - var response = _authResponseResolver.Resolve(effectiveMode, flowType, overriddenProfile, effectiveOptions); + var response = _authResponseResolver.Resolve(effectiveMode, flowType, overriddenProfile, effective); var returnUrlInfo = existing.ReturnUrlInfo; var deviceContext = existing.Device; @@ -140,8 +140,7 @@ public async ValueTask RecreateWithClientProfileAsync(AuthFlowC existing.IsAuthenticated, existing.UserKey, session, - originalOptions, - effectiveOptions, + options, response, primaryTokenKind, returnUrlInfo @@ -162,8 +161,7 @@ public ValueTask RecreateWithDeviceAsync(AuthFlowContext existi existing.IsAuthenticated, existing.UserKey, existing.Session, - existing.OriginalOptions, - existing.EffectiveOptions, + existing.Options, existing.Response, existing.PrimaryTokenKind, existing.ReturnUrlInfo diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/EffectiveServerOptionsProvider.cs b/src/CodeBeam.UltimateAuth.Server/Auth/EffectiveServerOptionsProvider.cs index 4d3052ca..cb2528ab 100644 --- a/src/CodeBeam.UltimateAuth.Server/Auth/EffectiveServerOptionsProvider.cs +++ b/src/CodeBeam.UltimateAuth.Server/Auth/EffectiveServerOptionsProvider.cs @@ -24,16 +24,16 @@ public UAuthServerOptions GetOriginal(HttpContext context) return _baseOptions.Value; } - public EffectiveUAuthServerOptions GetEffective(HttpContext context, AuthFlowType flowType, UAuthClientProfile clientProfile) + public EffectiveUAuthServerOptions GetEffective(HttpContext context, UAuthClientProfile clientProfile) { var tenant = context.GetTenant(); - return GetEffective(tenant, flowType, clientProfile); + return GetEffective(tenant, clientProfile); } - public EffectiveUAuthServerOptions GetEffective(TenantKey tenant, AuthFlowType flowType, UAuthClientProfile clientProfile) + public EffectiveUAuthServerOptions GetEffective(TenantKey tenant, UAuthClientProfile clientProfile) { var original = _baseOptions.Value; - var effectiveMode = _modeResolver.Resolve(clientProfile, flowType); + var effectiveMode = _modeResolver.Resolve(clientProfile); var options = original.Clone(); ConfigureDefaults.ApplyModeDefaults(effectiveMode, options); diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/Response/EffectiveAuthModeResolver.cs b/src/CodeBeam.UltimateAuth.Server/Auth/Response/EffectiveAuthModeResolver.cs index 90865473..fa08ca38 100644 --- a/src/CodeBeam.UltimateAuth.Server/Auth/Response/EffectiveAuthModeResolver.cs +++ b/src/CodeBeam.UltimateAuth.Server/Auth/Response/EffectiveAuthModeResolver.cs @@ -6,7 +6,7 @@ namespace CodeBeam.UltimateAuth.Server.Auth; internal sealed class EffectiveAuthModeResolver : IEffectiveAuthModeResolver { - public UAuthMode Resolve(UAuthClientProfile clientProfile, AuthFlowType flowType) + public UAuthMode Resolve(UAuthClientProfile clientProfile) { return clientProfile switch { diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/Response/IEffectiveAuthModeResolver.cs b/src/CodeBeam.UltimateAuth.Server/Auth/Response/IEffectiveAuthModeResolver.cs index 71969091..8af87eee 100644 --- a/src/CodeBeam.UltimateAuth.Server/Auth/Response/IEffectiveAuthModeResolver.cs +++ b/src/CodeBeam.UltimateAuth.Server/Auth/Response/IEffectiveAuthModeResolver.cs @@ -6,5 +6,5 @@ namespace CodeBeam.UltimateAuth.Server.Auth; public interface IEffectiveAuthModeResolver { - UAuthMode Resolve(UAuthClientProfile clientProfile, AuthFlowType flowType); + UAuthMode Resolve(UAuthClientProfile clientProfile); } diff --git a/src/CodeBeam.UltimateAuth.Server/Authentication/AspNetCore/UAuthAuthenticationHandler.cs b/src/CodeBeam.UltimateAuth.Server/Authentication/AspNetCore/UAuthAuthenticationHandler.cs index 96fff082..fd8d351d 100644 --- a/src/CodeBeam.UltimateAuth.Server/Authentication/AspNetCore/UAuthAuthenticationHandler.cs +++ b/src/CodeBeam.UltimateAuth.Server/Authentication/AspNetCore/UAuthAuthenticationHandler.cs @@ -61,6 +61,7 @@ protected override async Task HandleAuthenticateAsync() Tenant = tenant, SessionId = sessionId, Device = _deviceContextFactory.Create(credential.Device), + SessionOptions = _serverOptions.Session, Now = _clock.UtcNow }); diff --git a/src/CodeBeam.UltimateAuth.Server/Authentication/AspNetCore/UAuthResourceAuthenticationHandler.cs b/src/CodeBeam.UltimateAuth.Server/Authentication/AspNetCore/UAuthResourceAuthenticationHandler.cs index a479aa56..6e8a32a5 100644 --- a/src/CodeBeam.UltimateAuth.Server/Authentication/AspNetCore/UAuthResourceAuthenticationHandler.cs +++ b/src/CodeBeam.UltimateAuth.Server/Authentication/AspNetCore/UAuthResourceAuthenticationHandler.cs @@ -1,8 +1,10 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Server.Auth; using CodeBeam.UltimateAuth.Server.Extensions; using CodeBeam.UltimateAuth.Server.Infrastructure; +using CodeBeam.UltimateAuth.Server.Options; using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/PkceEndpointHandler.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/PkceEndpointHandler.cs index 519dfbe2..e851f13f 100644 --- a/src/CodeBeam.UltimateAuth.Server/Endpoints/PkceEndpointHandler.cs +++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/PkceEndpointHandler.cs @@ -297,7 +297,7 @@ public async Task CompleteAsync(HttpContext ctx) private async Task RedirectToLoginWithErrorAsync(HttpContext ctx, AuthFlowContext auth, string error) { - var basePath = auth.OriginalOptions.Hub.LoginPath ?? "/login"; + var basePath = auth.Options.Hub.LoginPath ?? "/login"; var hubKey = await ResolveHubKeyAsync(ctx); if (!string.IsNullOrWhiteSpace(hubKey)) diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/RefreshEndpointHandler.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/RefreshEndpointHandler.cs index 2d4f37ee..6f1ff5cf 100644 --- a/src/CodeBeam.UltimateAuth.Server/Endpoints/RefreshEndpointHandler.cs +++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/RefreshEndpointHandler.cs @@ -72,7 +72,7 @@ public async Task RefreshAsync(HttpContext ctx) _credentialWriter.Write(ctx, GrantKind.RefreshToken, result.RefreshToken); } - if (flow.OriginalOptions.Diagnostics.EnableRefreshDetails) + if (flow.Options.Diagnostics.EnableRefreshDetails) { _refreshWriter.Write(ctx, result.Outcome); } diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/ValidateEndpointHandler.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/ValidateEndpointHandler.cs index 84f22cde..25726069 100644 --- a/src/CodeBeam.UltimateAuth.Server/Endpoints/ValidateEndpointHandler.cs +++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/ValidateEndpointHandler.cs @@ -67,7 +67,8 @@ public async Task ValidateAsync(HttpContext context, CancellationToken Tenant = tenant, SessionId = sessionId, Now = _clock.UtcNow, - Device = auth.Device + Device = auth.Device, + SessionOptions = auth.Options.Session }, ct); diff --git a/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthApplicationBuilderExtensions.cs b/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthApplicationBuilderExtensions.cs index d352d7f1..5d806c85 100644 --- a/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthApplicationBuilderExtensions.cs +++ b/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthApplicationBuilderExtensions.cs @@ -60,7 +60,7 @@ public static IApplicationBuilder UseUltimateAuthResourceApi(this IApplicationBu app.UseUAuthExceptionHandling(); app.UseMiddleware(); app.UseMiddleware(); - app.UseMiddleware(); + app.UseMiddleware(); app.UseMiddleware(); return app; diff --git a/src/CodeBeam.UltimateAuth.Server/Flows/Login/LoginOrchestrator.cs b/src/CodeBeam.UltimateAuth.Server/Flows/Login/LoginOrchestrator.cs index a8b8f377..425e450b 100644 --- a/src/CodeBeam.UltimateAuth.Server/Flows/Login/LoginOrchestrator.cs +++ b/src/CodeBeam.UltimateAuth.Server/Flows/Login/LoginOrchestrator.cs @@ -30,7 +30,6 @@ internal sealed class LoginOrchestrator : ILoginOrchestrator, IInternalLoginOrch private readonly ISessionStoreFactory _storeFactory; private readonly IAuthenticationSecurityManager _authenticationSecurityManager; // runtime risk private readonly UAuthEventDispatcher _events; - private readonly UAuthServerOptions _options; private readonly IClock _clock; public LoginOrchestrator( @@ -44,7 +43,6 @@ public LoginOrchestrator( ISessionStoreFactory storeFactory, IAuthenticationSecurityManager authenticationSecurityManager, UAuthEventDispatcher events, - IOptions options, IClock clock) { _identifierResolver = identifierResolver; @@ -57,7 +55,6 @@ public LoginOrchestrator( _storeFactory = storeFactory; _authenticationSecurityManager = authenticationSecurityManager; _events = events; - _options = options.Value; _clock = clock; } @@ -136,7 +133,7 @@ public async Task LoginAsync(AuthFlowContext flow, LoginRequest req if (chain is not null) { - var chainState = chain.GetState(now, _options.Session.IdleTimeout); + var chainState = chain.GetState(now, flow.Options.Session.IdleTimeout); if (chainState == SessionState.Active) { @@ -174,14 +171,14 @@ public async Task LoginAsync(AuthFlowContext flow, LoginRequest req request.CredentialType, state => state.RegisterFailure( now, - _options.Login.MaxFailedAttempts, - _options.Login.LockoutDuration, - _options.Login.FailureWindow, - _options.Login.ExtendLockOnFailure), + flow.Options.Login.MaxFailedAttempts, + flow.Options.Login.LockoutDuration, + flow.Options.Login.FailureWindow, + flow.Options.Login.ExtendLockOnFailure), ct); } - if (_options.Login.IncludeFailureDetails) + if (flow.Options.Login.IncludeFailureDetails) { var stateForResponse = factorState; @@ -190,9 +187,9 @@ public async Task LoginAsync(AuthFlowContext flow, LoginRequest req lockedUntil = stateForResponse.LockedUntil; remainingAttempts = 0; } - else if (_options.Login.MaxFailedAttempts > 0) + else if (flow.Options.Login.MaxFailedAttempts > 0) { - remainingAttempts = _options.Login.MaxFailedAttempts - stateForResponse.FailedAttempts; + remainingAttempts = flow.Options.Login.MaxFailedAttempts - stateForResponse.FailedAttempts; } } @@ -247,7 +244,8 @@ public async Task LoginAsync(AuthFlowContext flow, LoginRequest req Claims = claims, ChainId = chainId, Metadata = SessionMetadata.Empty, - Mode = flow.EffectiveMode + Mode = flow.EffectiveMode, + SessionOptions = flow.Options.Session }; var authContext = flow.ToAuthContext(now); diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Cookies/UAuthCookiePolicyBuilder.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Cookies/UAuthCookiePolicyBuilder.cs index 78b0af0a..1b52fb59 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Cookies/UAuthCookiePolicyBuilder.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Cookies/UAuthCookiePolicyBuilder.cs @@ -34,7 +34,7 @@ private static SameSiteMode ResolveSameSite(UAuthCookieOptions cookie, AuthFlowC if (cookie.SameSite is not null) return cookie.SameSite.Value; - return context.OriginalOptions.HubDeploymentMode switch + return context.Options.HubDeploymentMode switch { UAuthHubDeploymentMode.Embedded => SameSiteMode.Strict, UAuthHubDeploymentMode.Integrated => SameSiteMode.Lax, @@ -65,16 +65,16 @@ private static void ApplyLifetime(CookieOptions target, UAuthCookieOptions src, return kind switch { GrantKind.Session => ResolveSessionLifetime(context), - GrantKind.RefreshToken => context.EffectiveOptions.Options.Token.RefreshTokenLifetime, - GrantKind.AccessToken => context.EffectiveOptions.Options.Token.AccessTokenLifetime, + GrantKind.RefreshToken => context.Options.Token.RefreshTokenLifetime, + GrantKind.AccessToken => context.Options.Token.AccessTokenLifetime, _ => null }; } private static TimeSpan? ResolveSessionLifetime(AuthFlowContext context) { - var sessionIdle = context.EffectiveOptions.Options.Session.IdleTimeout; - var refresh = context.EffectiveOptions.Options.Token.RefreshTokenLifetime; + var sessionIdle = context.Options.Session.IdleTimeout; + var refresh = context.Options.Token.RefreshTokenLifetime; return context.EffectiveMode switch { diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Issuers/UAuthSessionIssuer.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Issuers/UAuthSessionIssuer.cs index a37ca6d6..e2ac81a0 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Issuers/UAuthSessionIssuer.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Issuers/UAuthSessionIssuer.cs @@ -4,8 +4,6 @@ using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Core.Errors; using CodeBeam.UltimateAuth.Core.MultiTenancy; -using CodeBeam.UltimateAuth.Server.Options; -using Microsoft.Extensions.Options; using System.Security; namespace CodeBeam.UltimateAuth.Server.Infrastructure; @@ -14,33 +12,32 @@ public sealed class UAuthSessionIssuer : ISessionIssuer { private readonly ISessionStoreFactory _storeFactory; private readonly IOpaqueTokenGenerator _opaqueGenerator; - private readonly UAuthServerOptions _options; - public UAuthSessionIssuer(ISessionStoreFactory storeFactory, IOpaqueTokenGenerator opaqueGenerator, IOptions options) + public UAuthSessionIssuer(ISessionStoreFactory storeFactory, IOpaqueTokenGenerator opaqueGenerator) { _storeFactory = storeFactory; _opaqueGenerator = opaqueGenerator; - _options = options.Value; } public async Task IssueSessionAsync(SessionIssuanceContext context, CancellationToken ct = default) { // Defensive guard — enforcement belongs to Authority if (context.Mode == UAuthMode.PureJwt) - { throw new InvalidOperationException("Session issuance is not allowed in PureJwt mode."); - } + + var sessionOptions = context.SessionOptions; var now = context.Now; var opaqueSessionId = _opaqueGenerator.Generate(); + if (!AuthSessionId.TryCreate(opaqueSessionId, out AuthSessionId sessionId)) throw new InvalidCastException("Can't create opaque id."); - var expiresAt = now.Add(_options.Session.Lifetime); + var expiresAt = now.Add(sessionOptions.Lifetime); - if (_options.Session.MaxLifetime is not null) + if (sessionOptions.MaxLifetime is not null) { - var absoluteExpiry = now.Add(_options.Session.MaxLifetime.Value); + var absoluteExpiry = now.Add(sessionOptions.MaxLifetime.Value); if (absoluteExpiry < expiresAt) expiresAt = absoluteExpiry; } @@ -94,7 +91,7 @@ await kernel.ExecuteAsync(async _ => //chain = await kernel.GetChainAsync(context.ChainId.Value) // ?? throw new UAuthNotFoundException("Chain not found."); - var chainState = chain.GetState(now, _options.Session.IdleTimeout); + var chainState = chain.GetState(now, sessionOptions.IdleTimeout); if (chainState != SessionState.Active) throw new UAuthValidationException("Chain is not active."); @@ -124,12 +121,12 @@ await kernel.ExecuteAsync(async _ => var sessions = await kernel.GetSessionsByChainAsync(chain.ChainId); - if (sessions.Count >= _options.Session.MaxSessionsPerChain) + if (sessions.Count >= sessionOptions.MaxSessionsPerChain) { var toDelete = sessions .Where(s => s.SessionId != chain.ActiveSessionId) .OrderBy(x => x.CreatedAt) - .Take(sessions.Count - _options.Session.MaxSessionsPerChain + 1) + .Take(sessions.Count - sessionOptions.MaxSessionsPerChain + 1) .ToList(); foreach (var old in toDelete) @@ -175,14 +172,16 @@ public async Task RotateSessionAsync(SessionRotationContext conte var kernel = _storeFactory.Create(context.Tenant); var now = context.Now; + var sessionOptions = context.SessionOptions; + var opaqueSessionId = _opaqueGenerator.Generate(); if (!AuthSessionId.TryCreate(opaqueSessionId, out var newSessionId)) throw new InvalidCastException("Can't create opaque session id."); - var expiresAt = now.Add(_options.Session.Lifetime); - if (_options.Session.MaxLifetime is not null) + var expiresAt = now.Add(sessionOptions.Lifetime); + if (sessionOptions.MaxLifetime is not null) { - var absoluteExpiry = now.Add(_options.Session.MaxLifetime.Value); + var absoluteExpiry = now.Add(sessionOptions.MaxLifetime.Value); if (absoluteExpiry < expiresAt) expiresAt = absoluteExpiry; } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Issuers/UAuthTokenIssuer.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Issuers/UAuthTokenIssuer.cs index 1649494c..36bdbd27 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Issuers/UAuthTokenIssuer.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Issuers/UAuthTokenIssuer.cs @@ -32,7 +32,7 @@ public UAuthTokenIssuer(IOpaqueTokenGenerator opaqueGenerator, IJwtTokenGenerato public Task IssueAccessTokenAsync(AuthFlowContext flow, TokenIssuanceContext context, CancellationToken ct = default) { - var tokens = flow.OriginalOptions.Token; + var tokens = flow.Options.Token; var now = _clock.UtcNow; var expires = now.Add(tokens.AccessTokenLifetime); @@ -59,7 +59,7 @@ UAuthMode.SemiHybrid or return null; var now = _clock.UtcNow; - var expires = now.Add(flow.OriginalOptions.Token.RefreshTokenLifetime); + var expires = now.Add(flow.Options.Token.RefreshTokenLifetime); var raw = _opaqueGenerator.Generate(); var hash = _tokenHasher.Hash(raw); diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Redirect/AuthRedirectResolver.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Redirect/AuthRedirectResolver.cs index 68b2e7a5..afddbead 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Redirect/AuthRedirectResolver.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Redirect/AuthRedirectResolver.cs @@ -44,13 +44,13 @@ private RedirectDecision Resolve(AuthFlowContext flow, HttpContext ctx, string? if (info.IsAbsolute && (info.AbsoluteUri!.Scheme == Uri.UriSchemeHttp || info.AbsoluteUri!.Scheme == Uri.UriSchemeHttps)) { var origin = info.AbsoluteUri!.GetLeftPart(UriPartial.Authority); - ValidateAllowed(origin, flow.OriginalOptions); + ValidateAllowed(origin, flow.Options); return RedirectDecision.To(info.AbsoluteUri.ToString()); } if (!string.IsNullOrWhiteSpace(info.RelativePath)) { - var baseAddress = _baseAddressResolver.Resolve(ctx, flow.OriginalOptions); + var baseAddress = _baseAddressResolver.Resolve(ctx, flow.Options); return RedirectDecision.To(UrlComposer.Combine(baseAddress, info.RelativePath)); } } @@ -58,7 +58,7 @@ private RedirectDecision Resolve(AuthFlowContext flow, HttpContext ctx, string? if (string.IsNullOrWhiteSpace(fallbackPath)) return RedirectDecision.None(); - var baseUrl = _baseAddressResolver.Resolve(ctx, flow.OriginalOptions); + var baseUrl = _baseAddressResolver.Resolve(ctx, flow.Options); var query = new Dictionary(); @@ -76,7 +76,7 @@ private RedirectDecision Resolve(AuthFlowContext flow, HttpContext ctx, string? Reason = failureReason }; - if (flow.FlowType == AuthFlowType.Login && loginResult is not null && flow.OriginalOptions.Login.IncludeFailureDetails) + if (flow.FlowType == AuthFlowType.Login && loginResult is not null && flow.Options.Login.IncludeFailureDetails) { payload = payload with { diff --git a/src/CodeBeam.UltimateAuth.Server/Middlewares/SessionValidationMiddleware.cs b/src/CodeBeam.UltimateAuth.Server/Middlewares/RemoteSessionValidationMiddleware.cs similarity index 51% rename from src/CodeBeam.UltimateAuth.Server/Middlewares/SessionValidationMiddleware.cs rename to src/CodeBeam.UltimateAuth.Server/Middlewares/RemoteSessionValidationMiddleware.cs index 17cf6471..cffb189e 100644 --- a/src/CodeBeam.UltimateAuth.Server/Middlewares/SessionValidationMiddleware.cs +++ b/src/CodeBeam.UltimateAuth.Server/Middlewares/RemoteSessionValidationMiddleware.cs @@ -3,25 +3,27 @@ using CodeBeam.UltimateAuth.Core.Defaults; using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Core.MultiTenancy; +using CodeBeam.UltimateAuth.Core.Options; +using CodeBeam.UltimateAuth.Server.Auth; using CodeBeam.UltimateAuth.Server.Extensions; +using CodeBeam.UltimateAuth.Server.Options; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; namespace CodeBeam.UltimateAuth.Server.Middlewares; -public class SessionValidationMiddleware +public class RemoteSessionValidationMiddleware { private readonly RequestDelegate _next; - private readonly ISessionValidator _validator; private readonly IClock _clock; - public SessionValidationMiddleware(RequestDelegate next, ISessionValidator validator, IClock clock) + public RemoteSessionValidationMiddleware(RequestDelegate next, IClock clock) { _next = next; - _validator = validator; _clock = clock; } - public async Task Invoke(HttpContext context) + public async Task InvokeAsync(HttpContext context, ISessionValidator validator) { var sessionCtx = context.GetSessionContext(); @@ -33,19 +35,31 @@ public async Task Invoke(HttpContext context) return; } - var info = await context.GetDeviceAsync(); - var device = DeviceContext.Create(info.DeviceId, info.DeviceType, info.Platform, info.OperatingSystem, info.Browser, info.IpAddress); - if (sessionCtx.Tenant is not TenantKey tenant) throw new InvalidOperationException("Tenant is not resolved."); - var result = await _validator.ValidateSessionAsync(new SessionValidationContext - { - Tenant = tenant, - SessionId = sessionCtx.SessionId!.Value, - Now = _clock.UtcNow, - Device = device - }); + if (sessionCtx.SessionId is not AuthSessionId sessionId) + throw new InvalidOperationException("Session ID is not resolved."); + + var info = await context.GetDeviceAsync(); + + var device = DeviceContext.Create( + info.DeviceId, + info.DeviceType, + info.Platform, + info.OperatingSystem, + info.Browser, + info.IpAddress); + + var result = await validator.ValidateSessionAsync( + new SessionValidationContext + { + Tenant = tenant, + SessionId = sessionId, + Now = _clock.UtcNow, + Device = device + }, + context.RequestAborted); context.Items[UAuthConstants.HttpItems.SessionValidationResult] = result; diff --git a/src/CodeBeam.UltimateAuth.Server/Options/Defaults/ConfigureDefaults.cs b/src/CodeBeam.UltimateAuth.Server/Options/Defaults/ConfigureDefaults.cs index 9b8d9f6c..abb1a703 100644 --- a/src/CodeBeam.UltimateAuth.Server/Options/Defaults/ConfigureDefaults.cs +++ b/src/CodeBeam.UltimateAuth.Server/Options/Defaults/ConfigureDefaults.cs @@ -49,7 +49,6 @@ private static void ApplyPureOpaqueDefaults(UAuthServerOptions o) // Hard re-auth boundary is an advanced security feature // Do NOT enable by default s.MaxLifetime ??= null; - s.DeviceMismatchBehavior = DeviceMismatchBehavior.Allow; // SessionId is the primary opaque token, carried via cookie t.IssueJwt = false; diff --git a/src/CodeBeam.UltimateAuth.Server/Options/IEffectiveServerOptionsProvider.cs b/src/CodeBeam.UltimateAuth.Server/Options/IEffectiveServerOptionsProvider.cs index 0eaf5182..f5697aba 100644 --- a/src/CodeBeam.UltimateAuth.Server/Options/IEffectiveServerOptionsProvider.cs +++ b/src/CodeBeam.UltimateAuth.Server/Options/IEffectiveServerOptionsProvider.cs @@ -9,6 +9,6 @@ namespace CodeBeam.UltimateAuth.Server.Options; public interface IEffectiveServerOptionsProvider { UAuthServerOptions GetOriginal(HttpContext context); - EffectiveUAuthServerOptions GetEffective(HttpContext context, AuthFlowType flowType, UAuthClientProfile clientProfile); - EffectiveUAuthServerOptions GetEffective(TenantKey tenant, AuthFlowType flowType, UAuthClientProfile clientProfile); + EffectiveUAuthServerOptions GetEffective(HttpContext context, UAuthClientProfile clientProfile); + EffectiveUAuthServerOptions GetEffective(TenantKey tenant, UAuthClientProfile clientProfile); } diff --git a/src/CodeBeam.UltimateAuth.Server/Options/Validators/UAuthServerSessionOptionsValidator.cs b/src/CodeBeam.UltimateAuth.Server/Options/Validators/UAuthServerSessionOptionsValidator.cs index 192b2786..9ab74357 100644 --- a/src/CodeBeam.UltimateAuth.Server/Options/Validators/UAuthServerSessionOptionsValidator.cs +++ b/src/CodeBeam.UltimateAuth.Server/Options/Validators/UAuthServerSessionOptionsValidator.cs @@ -15,8 +15,7 @@ public ValidateOptionsResult Validate(string? name, UAuthServerOptions options) if (session.MaxLifetime.HasValue && session.MaxLifetime <= TimeSpan.Zero) errors.Add("Session.MaxLifetime must be greater than zero when specified."); - if (session.MaxLifetime.HasValue && - session.MaxLifetime < session.Lifetime) + if (session.MaxLifetime.HasValue && session.MaxLifetime < session.Lifetime) errors.Add("Session.MaxLifetime must be greater than or equal to Session.Lifetime."); if (session.IdleTimeout.HasValue && session.IdleTimeout < TimeSpan.Zero) diff --git a/src/CodeBeam.UltimateAuth.Server/Services/RefreshFlowService.cs b/src/CodeBeam.UltimateAuth.Server/Services/RefreshFlowService.cs index 249de06c..e6af7b10 100644 --- a/src/CodeBeam.UltimateAuth.Server/Services/RefreshFlowService.cs +++ b/src/CodeBeam.UltimateAuth.Server/Services/RefreshFlowService.cs @@ -59,7 +59,8 @@ private async Task HandleSessionOnlyAsync(AuthFlowContext flo Tenant = flow.Tenant, SessionId = request.SessionId.Value, Now = now, - Device = request.Device + Device = request.Device, + SessionOptions = flow.Options.Session }, ct); @@ -68,7 +69,7 @@ private async Task HandleSessionOnlyAsync(AuthFlowContext flo var touchPolicy = new SessionTouchPolicy { - TouchInterval = flow.EffectiveOptions.Options.Session.TouchInterval + TouchInterval = flow.Options.Session.TouchInterval }; var refresh = await _sessionRefresh.RefreshAsync(validation, touchPolicy, request.TouchMode, now, ct); @@ -118,7 +119,8 @@ private async Task HandleHybridAsync(AuthFlowContext flow, Re Tenant = flow.Tenant, SessionId = request.SessionId.Value, Now = now, - Device = request.Device + Device = request.Device, + SessionOptions = flow.Options.Session }, ct); @@ -141,7 +143,7 @@ private async Task HandleHybridAsync(AuthFlowContext flow, Re var touchPolicy = new SessionTouchPolicy { - TouchInterval = flow.EffectiveOptions.Options.Session.TouchInterval + TouchInterval = flow.Options.Session.TouchInterval }; var refresh = await _sessionRefresh.RefreshAsync(validation, touchPolicy, request.TouchMode, now, ct); @@ -171,7 +173,8 @@ private async Task HandleSemiHybridAsync(AuthFlowContext flow Tenant = flow.Tenant, SessionId = request.SessionId.Value, Now = now, - Device = request.Device + Device = request.Device, + SessionOptions = flow.Options.Session }, ct); diff --git a/src/CodeBeam.UltimateAuth.Server/Services/RefreshTokenRotationService.cs b/src/CodeBeam.UltimateAuth.Server/Services/RefreshTokenRotationService.cs index fbdc931b..4d279ddc 100644 --- a/src/CodeBeam.UltimateAuth.Server/Services/RefreshTokenRotationService.cs +++ b/src/CodeBeam.UltimateAuth.Server/Services/RefreshTokenRotationService.cs @@ -41,7 +41,7 @@ public async Task RotateAsync(AuthFlowContext flo if (validation.State == RefreshTokenValidationState.Consumed) { - var concurrencyWindow = flow.OriginalOptions.Token.RefreshTokenConcurrentRequestWindow; + var concurrencyWindow = flow.Options.Token.RefreshTokenConcurrentRequestWindow; if (IsLikelyConcurrentRequest(validation, context.Now, concurrencyWindow)) { @@ -79,7 +79,7 @@ public async Task RotateAsync(AuthFlowContext flo var tokenContext = new TokenIssuanceContext { - Tenant = flow.OriginalOptions.MultiTenant.Enabled + Tenant = flow.Options.MultiTenant.Enabled ? validation.Tenant : TenantKey.Single, diff --git a/src/CodeBeam.UltimateAuth.Server/Services/UAuthSessionValidator.cs b/src/CodeBeam.UltimateAuth.Server/Services/UAuthSessionValidator.cs index fa97d6c1..f37c7a97 100644 --- a/src/CodeBeam.UltimateAuth.Server/Services/UAuthSessionValidator.cs +++ b/src/CodeBeam.UltimateAuth.Server/Services/UAuthSessionValidator.cs @@ -11,13 +11,11 @@ internal sealed class UAuthSessionValidator : ISessionValidator { private readonly ISessionStoreFactory _storeFactory; private readonly IUserClaimsProvider _claimsProvider; - private readonly UAuthServerOptions _options; - public UAuthSessionValidator(ISessionStoreFactory storeFactory, IUserClaimsProvider claimsProvider, IOptions options) + public UAuthSessionValidator(ISessionStoreFactory storeFactory, IUserClaimsProvider claimsProvider) { _storeFactory = storeFactory; _claimsProvider = claimsProvider; - _options = options.Value; } // TODO: Improve Device binding @@ -26,6 +24,8 @@ public UAuthSessionValidator(ISessionStoreFactory storeFactory, IUserClaimsProvi // TODO: Add GetSessionAggregate store method to 1 call instead of 3 calls of root chain session public async Task ValidateSessionAsync(SessionValidationContext context, CancellationToken ct = default) { + var options = context.SessionOptions ?? throw new InvalidOperationException("Session options are required for local session validation."); + var kernel = _storeFactory.Create(context.Tenant); var session = await kernel.GetSessionAsync(context.SessionId, ct); @@ -40,7 +40,7 @@ public async Task ValidateSessionAsync(SessionValidatio if (chain is null || chain.IsRevoked) return SessionValidationResult.Invalid(SessionState.Revoked, session.UserKey, session.SessionId, session.ChainId); - var chainState = chain.GetState(context.Now, _options.Session.IdleTimeout); + var chainState = chain.GetState(context.Now, options.IdleTimeout); if (chainState != SessionState.Active) return SessionValidationResult.Invalid(chainState, chain.UserKey, session.SessionId, chain.ChainId); @@ -67,10 +67,10 @@ public async Task ValidateSessionAsync(SessionValidatio { if (!Equals(chain.Device.DeviceId, context.Device.DeviceId)) { - if (_options.Session.DeviceMismatchBehavior == DeviceMismatchBehavior.Reject) + if (options.DeviceMismatchBehavior == DeviceMismatchBehavior.Reject) return SessionValidationResult.Invalid(SessionState.DeviceMismatch, chain.UserKey, session.SessionId, chain.ChainId, root.RootId); - //if (_options.Session.DeviceMismatchBehavior == AllowAndRebind) + //if (options.Session.DeviceMismatchBehavior == AllowAndRebind) } } //else diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Credentials/ChangePasswordTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Credentials/ChangePasswordTests.cs index 56003bfb..189c192c 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Credentials/ChangePasswordTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Credentials/ChangePasswordTests.cs @@ -126,6 +126,4 @@ await service.ChangeSecretAsync(context, newLogin.IsSuccess.Should().BeTrue(); } - - } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Helpers/AuthFlowTestFactory.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Helpers/AuthFlowTestFactory.cs index f7bba806..30c2cc97 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Helpers/AuthFlowTestFactory.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Helpers/AuthFlowTestFactory.cs @@ -37,8 +37,7 @@ public static AuthFlowContext New( isAuthenticated: isAuthenticated, userKey: userKey ?? (isAuthenticated ? UserKey.New() : null), session: session, - originalOptions: TestServerOptions.Default(), - effectiveOptions: TestServerOptions.Effective(), + options: TestServerOptions.Default(), response: response ?? new EffectiveAuthResponse( sessionIdDelivery: CredentialResponseOptions.Disabled(GrantKind.Session), diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Helpers/TestAuthModeResolver.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Helpers/TestAuthModeResolver.cs index 99e87dbb..0bbaf81d 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Helpers/TestAuthModeResolver.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Helpers/TestAuthModeResolver.cs @@ -7,6 +7,5 @@ namespace CodeBeam.UltimateAuth.Tests.Unit.Helpers; internal sealed class TestAuthModeResolver : IEffectiveAuthModeResolver { - public UAuthMode Resolve(UAuthClientProfile profile, AuthFlowType flowType) - => UAuthMode.PureOpaque; + public UAuthMode Resolve(UAuthClientProfile profile) => UAuthMode.PureOpaque; } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/ResourceApi/RemoteSessionValidatorTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/ResourceApi/RemoteSessionValidatorTests.cs index 948cb560..718b93b5 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/ResourceApi/RemoteSessionValidatorTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/ResourceApi/RemoteSessionValidatorTests.cs @@ -2,6 +2,7 @@ using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Core.Infrastructure; using CodeBeam.UltimateAuth.Core.MultiTenancy; +using CodeBeam.UltimateAuth.Core.Options; using CodeBeam.UltimateAuth.Server.ResourceApi; using FluentAssertions; using Microsoft.AspNetCore.Http; @@ -395,12 +396,10 @@ private static SessionValidationContext CreateValidationContext() return new SessionValidationContext { Tenant = Tenant, - SessionId = AuthSessionId.Parse(SessionValue, null), - Now = new DateTimeOffset(2026, 1, 1, 12, 0, 0, TimeSpan.Zero), - - Device = DeviceContext.Anonymous() + Device = DeviceContext.Anonymous(), + SessionOptions = new UAuthSessionOptions() }; } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/CookieInfrastructureTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/CookieInfrastructureTests.cs index 9bbd5e12..cfef81d3 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/CookieInfrastructureTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/CookieInfrastructureTests.cs @@ -127,7 +127,7 @@ public void Build_should_resolve_same_site_from_deployment_mode( cookie.SameSite = null; var context = AuthFlowTestFactory.New(); - context.OriginalOptions.HubDeploymentMode = deploymentMode; + context.Options.HubDeploymentMode = deploymentMode; var options = _builder.Build( CreateResponse(GrantKind.Session, cookie), @@ -148,7 +148,7 @@ public void Explicit_same_site_should_override_deployment_mode( cookie.SameSite = explicitMode; var context = AuthFlowTestFactory.New(); - context.OriginalOptions.HubDeploymentMode = + context.Options.HubDeploymentMode = UAuthHubDeploymentMode.External; var options = _builder.Build( @@ -175,7 +175,7 @@ public void Explicit_max_age_should_override_all_other_lifetimes() var context = AuthFlowTestFactory.New(); - context.EffectiveOptions.Options.Session.IdleTimeout = + context.Options.Session.IdleTimeout = TimeSpan.FromHours(4); var options = _builder.Build( @@ -198,7 +198,7 @@ public void Absolute_lifetime_override_should_take_precedence_over_grant_lifetim var context = AuthFlowTestFactory.New(); - context.EffectiveOptions.Options.Session.IdleTimeout = + context.Options.Session.IdleTimeout = TimeSpan.FromHours(4); var options = _builder.Build( @@ -251,10 +251,10 @@ public void Pure_opaque_session_should_use_idle_timeout() var context = AuthFlowTestFactory.New( mode: UAuthMode.PureOpaque); - context.EffectiveOptions.Options.Session.IdleTimeout = + context.Options.Session.IdleTimeout = TimeSpan.FromMinutes(20); - context.EffectiveOptions.Options.Token.RefreshTokenLifetime = + context.Options.Token.RefreshTokenLifetime = TimeSpan.FromDays(7); var options = _builder.Build( @@ -273,10 +273,10 @@ public void Hybrid_session_should_use_refresh_lifetime_when_longer() var context = AuthFlowTestFactory.New( mode: UAuthMode.Hybrid); - context.EffectiveOptions.Options.Session.IdleTimeout = + context.Options.Session.IdleTimeout = TimeSpan.FromMinutes(20); - context.EffectiveOptions.Options.Token.RefreshTokenLifetime = + context.Options.Token.RefreshTokenLifetime = TimeSpan.FromDays(7); var options = _builder.Build( @@ -296,10 +296,10 @@ public void Hybrid_session_should_use_idle_timeout_when_longer() var context = AuthFlowTestFactory.New( mode: UAuthMode.Hybrid); - context.EffectiveOptions.Options.Session.IdleTimeout = + context.Options.Session.IdleTimeout = TimeSpan.FromHours(12); - context.EffectiveOptions.Options.Token.RefreshTokenLifetime = + context.Options.Token.RefreshTokenLifetime = TimeSpan.FromHours(2); var options = _builder.Build( @@ -319,10 +319,10 @@ public void Non_hybrid_session_should_use_idle_timeout() var context = AuthFlowTestFactory.New( mode: UAuthMode.PureJwt); - context.EffectiveOptions.Options.Session.IdleTimeout = + context.Options.Session.IdleTimeout = TimeSpan.FromMinutes(15); - context.EffectiveOptions.Options.Token.RefreshTokenLifetime = + context.Options.Token.RefreshTokenLifetime = TimeSpan.FromDays(1); var options = _builder.Build( @@ -344,7 +344,7 @@ public void Access_token_cookie_should_use_access_token_lifetime() var context = AuthFlowTestFactory.New(); - context.EffectiveOptions.Options.Token.AccessTokenLifetime = + context.Options.Token.AccessTokenLifetime = TimeSpan.FromMinutes(15); var options = _builder.Build( @@ -362,7 +362,7 @@ public void Refresh_token_cookie_should_use_refresh_token_lifetime() var context = AuthFlowTestFactory.New(); - context.EffectiveOptions.Options.Token.RefreshTokenLifetime = + context.Options.Token.RefreshTokenLifetime = TimeSpan.FromDays(14); var options = _builder.Build( diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/EffectiveAuthModeResolverTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/EffectiveAuthModeResolverTests.cs index 9b72cccf..b5e00f60 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/EffectiveAuthModeResolverTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/EffectiveAuthModeResolverTests.cs @@ -16,7 +16,7 @@ public class EffectiveAuthModeResolverTests [InlineData(UAuthClientProfile.Api, UAuthMode.PureJwt)] public void Default_Mode_Is_Derived_From_ClientProfile(UAuthClientProfile profile, UAuthMode expected) { - var mode = _resolver.Resolve(clientProfile: profile, flowType: AuthFlowType.Login); + var mode = _resolver.Resolve(clientProfile: profile); Assert.Equal(expected, mode); } } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/EffectiveServerOptionsProviderTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/EffectiveServerOptionsProviderTests.cs index a386347f..ad1b9187 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/EffectiveServerOptionsProviderTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/EffectiveServerOptionsProviderTests.cs @@ -21,7 +21,7 @@ public void Original_Options_Are_Not_Mutated() var provider = TestHelpers.CreateEffectiveOptionsProvider(baseOptions); var ctx = TestHttpContext.Create(); - var effective = provider.GetEffective(ctx, AuthFlowType.Login, UAuthClientProfile.BlazorServer); + var effective = provider.GetEffective(ctx, UAuthClientProfile.BlazorServer); effective.Options.Token.AccessTokenLifetime = TimeSpan.FromSeconds(10); Assert.NotEqual(baseOptions.Token.AccessTokenLifetime, effective.Options.Token.AccessTokenLifetime); @@ -34,7 +34,7 @@ public void EffectiveMode_Is_Determined_By_ModeResolver() var provider = TestHelpers.CreateEffectiveOptionsProvider(baseOptions); var ctx = TestHttpContext.Create(); - var effective = provider.GetEffective(ctx, AuthFlowType.Login, UAuthClientProfile.Api); + var effective = provider.GetEffective(ctx, UAuthClientProfile.Api); Assert.Equal(UAuthMode.PureJwt, effective.Mode); } @@ -46,7 +46,7 @@ public void Mode_Defaults_Are_Applied_Before_Overrides() var provider = TestHelpers.CreateEffectiveOptionsProvider(baseOptions); var ctx = TestHttpContext.Create(); - var effective = provider.GetEffective(ctx, AuthFlowType.Login, UAuthClientProfile.BlazorServer); + var effective = provider.GetEffective(ctx, UAuthClientProfile.BlazorServer); Assert.True(effective.Options.Session.SlidingExpiration); Assert.NotNull(effective.Options.Session.IdleTimeout); @@ -64,7 +64,7 @@ public void ModeConfiguration_Overrides_Mode_Defaults() var provider = TestHelpers.CreateEffectiveOptionsProvider(baseOptions); var ctx = TestHttpContext.Create(); - var effective = provider.GetEffective(ctx, AuthFlowType.Login, UAuthClientProfile.BlazorServer); + var effective = provider.GetEffective(ctx, UAuthClientProfile.BlazorServer); Assert.Equal(TimeSpan.FromMinutes(1), effective.Options.Token.AccessTokenLifetime); } @@ -77,8 +77,8 @@ public void Each_Call_Returns_New_EffectiveOptions_Instance() var provider = TestHelpers.CreateEffectiveOptionsProvider(baseOptions); var ctx = TestHttpContext.Create(); - var first = provider.GetEffective(ctx, AuthFlowType.Login, UAuthClientProfile.BlazorServer); - var second = provider.GetEffective(ctx, AuthFlowType.Login, UAuthClientProfile.BlazorServer); + var first = provider.GetEffective(ctx, UAuthClientProfile.BlazorServer); + var second = provider.GetEffective(ctx, UAuthClientProfile.BlazorServer); Assert.NotSame(first.Options, second.Options); } @@ -95,10 +95,7 @@ public void Effective_options_should_apply_defaults_for_each_auth_mode( var original = new UAuthServerOptions(); var provider = CreateProviderForMode(original, mode); - var effective = provider.GetEffective( - TenantKey.Single, - AuthFlowType.Login, - UAuthClientProfile.BlazorServer); + var effective = provider.GetEffective(TenantKey.Single, UAuthClientProfile.BlazorServer); effective.Mode.Should().Be(mode); effective.Options.Should().NotBeSameAs(original); @@ -118,10 +115,7 @@ public void SemiHybrid_should_apply_expected_session_defaults() var provider = CreateProviderForMode(original, UAuthMode.SemiHybrid); - var effective = provider.GetEffective( - TenantKey.Single, - AuthFlowType.Login, - UAuthClientProfile.BlazorServer); + var effective = provider.GetEffective(TenantKey.Single, UAuthClientProfile.BlazorServer); effective.Options.Session.SlidingExpiration.Should().BeFalse(); effective.Options.Session.TouchInterval.Should().BeNull(); @@ -150,7 +144,6 @@ public void SemiHybrid_should_apply_expected_token_defaults() var effective = provider.GetEffective( TenantKey.Single, - AuthFlowType.Login, UAuthClientProfile.BlazorServer); var token = effective.Options.Token; @@ -171,7 +164,6 @@ public void SemiHybrid_should_apply_expected_cookie_defaults() var effective = provider.GetEffective( TenantKey.Single, - AuthFlowType.Login, UAuthClientProfile.BlazorServer); effective.Options.Cookie.AccessToken.Lifetime.IdleBuffer @@ -197,14 +189,13 @@ public void PureOpaque_should_initialize_missing_optional_session_defaults() var effective = provider.GetEffective( TenantKey.Single, - AuthFlowType.Login, UAuthClientProfile.BlazorServer); effective.Options.Session.IdleTimeout.Should().Be(TimeSpan.FromDays(7)); effective.Options.Session.TouchInterval.Should().Be(TimeSpan.FromDays(1)); effective.Options.Session.MaxLifetime.Should().BeNull(); effective.Options.Session.DeviceMismatchBehavior - .Should().Be(DeviceMismatchBehavior.Allow); + .Should().Be(DeviceMismatchBehavior.Reject); effective.Options.Token.IssueJwt.Should().BeFalse(); effective.Options.Token.IssueOpaque.Should().BeFalse(); @@ -237,7 +228,6 @@ public void PureOpaque_should_preserve_explicit_optional_session_values() var effective = provider.GetEffective( TenantKey.Single, - AuthFlowType.Login, UAuthClientProfile.BlazorServer); effective.Options.Session.IdleTimeout.Should().Be(TimeSpan.FromHours(4)); @@ -254,7 +244,6 @@ public void Hybrid_should_apply_expected_defaults() var effective = provider.GetEffective( TenantKey.Single, - AuthFlowType.Login, UAuthClientProfile.BlazorServer); var options = effective.Options; @@ -298,7 +287,6 @@ public void PureJwt_should_apply_stateless_defaults() var effective = provider.GetEffective( TenantKey.Single, - AuthFlowType.Login, UAuthClientProfile.Api); var options = effective.Options; @@ -333,7 +321,6 @@ public void Unsupported_auth_mode_should_throw() Action act = () => provider.GetEffective( TenantKey.Single, - AuthFlowType.Login, UAuthClientProfile.Api); act.Should() @@ -356,7 +343,6 @@ public void SemiHybrid_mode_configuration_should_override_defaults() var effective = provider.GetEffective( TenantKey.Single, - AuthFlowType.Login, UAuthClientProfile.BlazorServer); effective.Options.Token.AccessTokenLifetime @@ -387,9 +373,7 @@ public FixedAuthModeResolver(UAuthMode mode) _mode = mode; } - public UAuthMode Resolve( - UAuthClientProfile clientProfile, - AuthFlowType flowType) + public UAuthMode Resolve(UAuthClientProfile clientProfile) { return _mode; } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/LogoutEndpointHandlerTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/LogoutEndpointHandlerTests.cs index 9fda0983..cf6aad14 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/LogoutEndpointHandlerTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/LogoutEndpointHandlerTests.cs @@ -655,8 +655,7 @@ private static AuthFlowContext CreateFlowWithSession( isAuthenticated: true, userKey: session.UserKey, session: session, - originalOptions: source.OriginalOptions, - effectiveOptions: source.EffectiveOptions, + options: source.Options, response: source.Response, primaryTokenKind: source.PrimaryTokenKind, returnUrlInfo: source.ReturnUrlInfo @@ -676,8 +675,7 @@ private static AuthFlowContext CreateUnauthenticatedFlow() isAuthenticated: false, userKey: null, session: null, - originalOptions: source.OriginalOptions, - effectiveOptions: source.EffectiveOptions, + options: source.Options, response: source.Response, primaryTokenKind: source.PrimaryTokenKind, returnUrlInfo: source.ReturnUrlInfo diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/PkceEndpointHandlerTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/PkceEndpointHandlerTests.cs index 427b8429..ff12d3be 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/PkceEndpointHandlerTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/PkceEndpointHandlerTests.cs @@ -551,7 +551,7 @@ public async Task CompleteAsync_WhenLoginFailsWithoutHub_RedirectsToLogin() .Subject; redirect.Url.Should().Be( - fixture.Flow.OriginalOptions.Hub.LoginPath ?? "/login"); + fixture.Flow.Options.Hub.LoginPath ?? "/login"); fixture.CredentialWriter.VerifyNoOtherCalls(); fixture.RedirectResolver.VerifyNoOtherCalls(); @@ -829,8 +829,7 @@ private static AuthFlowContext CreateNonLoginFlow() isAuthenticated: source.IsAuthenticated, userKey: source.UserKey, session: source.Session, - originalOptions: source.OriginalOptions, - effectiveOptions: source.EffectiveOptions, + options: source.Options, response: source.Response, primaryTokenKind: source.PrimaryTokenKind, returnUrlInfo: source.ReturnUrlInfo); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/RedirectTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/RedirectTests.cs index 76c39738..b6cb8033 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/RedirectTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/RedirectTests.cs @@ -40,7 +40,7 @@ public void LoginFlow_Uses_Configured_Redirect_Options() var optionsProvider = provider.GetRequiredService(); var resolver = provider.GetRequiredService(); - var effective = optionsProvider.GetEffective(TenantKey.Single, AuthFlowType.Login, UAuthClientProfile.BlazorServer); + var effective = optionsProvider.GetEffective(TenantKey.Single, UAuthClientProfile.BlazorServer); var response = resolver.Resolve(effective.Mode, AuthFlowType.Login, UAuthClientProfile.BlazorServer, effective); response.Redirect.AllowReturnUrlOverride.Should().BeFalse(); @@ -59,12 +59,12 @@ public async Task ClientProfile_Is_Read_From_Header() } [Theory] - [InlineData(UAuthClientProfile.BlazorWasm, AuthFlowType.Login, UAuthMode.Hybrid)] - [InlineData(UAuthClientProfile.BlazorServer, AuthFlowType.Login, UAuthMode.PureOpaque)] - public void ClientProfile_Resolves_To_Correct_Mode(UAuthClientProfile profile, AuthFlowType flow, UAuthMode expected) + [InlineData(UAuthClientProfile.BlazorWasm, UAuthMode.Hybrid)] + [InlineData(UAuthClientProfile.BlazorServer, UAuthMode.PureOpaque)] + public void ClientProfile_Resolves_To_Correct_Mode(UAuthClientProfile profile, UAuthMode expected) { var resolver = new EffectiveAuthModeResolver(); - var mode = resolver.Resolve(profile, flow); + var mode = resolver.Resolve(profile); mode.Should().Be(expected); } @@ -176,7 +176,7 @@ public void Absolute_ReturnUrl_Outside_AllowedOrigins_Throws() var resolver = TestRedirectResolver.Create(); var ctx = TestHttpContext.Create(); - flow.OriginalOptions.Hub.AllowedClientOrigins.Add("https://app.example.com"); + flow.Options.Hub.AllowedClientOrigins.Add("https://app.example.com"); Action act = () => resolver.ResolveSuccess(flow, ctx); act.Should().Throw().WithMessage("*not allowed*"); } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/RefreshFlowServiceTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/RefreshFlowServiceTests.cs index df376009..ce25294b 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/RefreshFlowServiceTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/RefreshFlowServiceTests.cs @@ -496,8 +496,7 @@ private static AuthFlowContext CreateFlow(UAuthMode mode) isAuthenticated: true, userKey: UserKey.New(), session: null, - originalOptions: original, - effectiveOptions: effective, + options: effective.Options, response: new EffectiveAuthResponse( sessionIdDelivery: CredentialResponseOptions.Disabled(GrantKind.Session), accessTokenDelivery: CredentialResponseOptions.Disabled(GrantKind.AccessToken), diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/RefreshTokenRotationServiceTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/RefreshTokenRotationServiceTests.cs index 571095f9..4c96b47c 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/RefreshTokenRotationServiceTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/RefreshTokenRotationServiceTests.cs @@ -690,8 +690,7 @@ private static AuthFlowContext CreateFlow(TenantKey? tenant = null, bool multiTe isAuthenticated: true, userKey: UserKey.New(), session: null, - originalOptions: options, - effectiveOptions: TestServerOptions.Effective(UAuthMode.Hybrid), + options: options, response: new EffectiveAuthResponse( sessionIdDelivery: CredentialResponseOptions.Disabled(GrantKind.Session), accessTokenDelivery: CredentialResponseOptions.Disabled(GrantKind.AccessToken), diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/SessionEndpointHandlerTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/SessionEndpointHandlerTests.cs index 2c35608e..fd272709 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/SessionEndpointHandlerTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/SessionEndpointHandlerTests.cs @@ -675,8 +675,7 @@ private static AuthFlowContext CreateUnauthenticatedFlow() isAuthenticated: false, userKey: null, session: null, - originalOptions: authenticated.OriginalOptions, - effectiveOptions: authenticated.EffectiveOptions, + options: authenticated.Options, response: authenticated.Response, primaryTokenKind: authenticated.PrimaryTokenKind, returnUrlInfo: authenticated.ReturnUrlInfo); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/UAuthFlowServiceTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/UAuthFlowServiceTests.cs index 6b6c5e1d..1c911f4b 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/UAuthFlowServiceTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/UAuthFlowServiceTests.cs @@ -746,8 +746,7 @@ private static AuthFlowContext CreateFlow( isAuthenticated: userKey.HasValue, userKey: userKey, session: session, - originalOptions: TestServerOptions.Default(), - effectiveOptions: TestServerOptions.Effective(), + options: TestServerOptions.Default(), response: new EffectiveAuthResponse( sessionIdDelivery: CredentialResponseOptions.Disabled( diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/UAuthResourceAuthenticationHandlerTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/UAuthResourceAuthenticationHandlerTests.cs index f3c1f233..e7395f83 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/UAuthResourceAuthenticationHandlerTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/UAuthResourceAuthenticationHandlerTests.cs @@ -5,8 +5,10 @@ using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Core.MultiTenancy; using CodeBeam.UltimateAuth.Server; +using CodeBeam.UltimateAuth.Server.Auth; using CodeBeam.UltimateAuth.Server.Authentication; using CodeBeam.UltimateAuth.Server.Infrastructure; +using CodeBeam.UltimateAuth.Server.Options; using FluentAssertions; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; @@ -360,6 +362,13 @@ private static TestSetup CreateTestContext( var services = new ServiceCollection(); + var effectiveOptionsProvider = new Mock(); + + var clientProfileReader = new Mock(); + + services.AddSingleton(effectiveOptionsProvider.Object); + services.AddSingleton(clientProfileReader.Object); + services.AddLogging(); services.AddSingleton( @@ -374,10 +383,7 @@ private static TestSetup CreateTestContext( services.AddSingleton( clock.Object); - services - .AddAuthentication( - UAuthConstants.SchemeDefaults.GlobalScheme) - .AddUAuthResourceApi(); + services.AddAuthentication(UAuthConstants.SchemeDefaults.GlobalScheme).AddUAuthResourceApi(); var provider = services.BuildServiceProvider(); @@ -386,9 +392,7 @@ private static TestSetup CreateTestContext( RequestServices = provider }; - http.Items[ - UAuthConstants.HttpItems.TenantContextKey] = - UAuthTenantContext.Resolved(Tenant); + http.Items[UAuthConstants.HttpItems.TenantContextKey] = UAuthTenantContext.Resolved(Tenant); return new TestSetup( http, diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/UAuthSessionValidatorTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/UAuthSessionValidatorTests.cs index f67f92b3..2f6a06b0 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/UAuthSessionValidatorTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/UAuthSessionValidatorTests.cs @@ -567,15 +567,15 @@ private static Fixture CreateFixture( var sut = new UAuthSessionValidator( storeFactory.Object, - claimsProvider.Object, - Options.Create(options)); + claimsProvider.Object); var context = new SessionValidationContext { Tenant = tenant, SessionId = session.SessionId, Now = Now, - Device = requestDevice ?? TestDevice.Default() + Device = requestDevice ?? TestDevice.Default(), + SessionOptions = options.Session }; return new Fixture( diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Sessions/UAuthSessionIssuerTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Sessions/UAuthSessionIssuerTests.cs index 026ce404..8d89ece3 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Sessions/UAuthSessionIssuerTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Sessions/UAuthSessionIssuerTests.cs @@ -4,11 +4,12 @@ using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Core.Errors; using CodeBeam.UltimateAuth.Core.MultiTenancy; +using CodeBeam.UltimateAuth.Core.Options; +using CodeBeam.UltimateAuth.Server.Auth; using CodeBeam.UltimateAuth.Server.Infrastructure; using CodeBeam.UltimateAuth.Server.Options; using CodeBeam.UltimateAuth.Tests.Unit.Helpers; using FluentAssertions; -using Microsoft.Extensions.Options; using Moq; using System.Security; @@ -36,7 +37,7 @@ public sealed class UAuthSessionIssuerTests public async Task IssueSessionAsync_WhenModeIsPureJwt_ShouldRejectSessionIssuance() { var fixture = CreateFixture(); - var context = CreateIssuanceContext( + var context = CreateIssuanceContext(fixture, mode: UAuthMode.PureJwt); var act = () => @@ -60,7 +61,7 @@ public async Task IssueSessionAsync_WhenOpaqueGeneratorReturnsInvalidId_ShouldFa var fixture = CreateFixture( opaqueToken: "invalid"); - var context = CreateIssuanceContext(); + var context = CreateIssuanceContext(fixture); var act = () => fixture.Sut.IssueSessionAsync(context); @@ -81,7 +82,7 @@ await act.Should() public async Task IssueSessionAsync_WhenActiveRootDoesNotExist_ShouldCreateRoot() { var fixture = CreateFixture(); - var context = CreateIssuanceContext(); + var context = CreateIssuanceContext(fixture); fixture.Store .Setup(x => x.GetActiveRootByUserAsync( @@ -119,7 +120,7 @@ public async Task IssueSessionAsync_WhenActiveRootDoesNotExist_ShouldCreateRoot( public async Task IssueSessionAsync_WhenActiveRootExists_ShouldReuseRoot() { var fixture = CreateFixture(); - var context = CreateIssuanceContext(); + var context = CreateIssuanceContext(fixture); var root = CreateRoot( context.Tenant, @@ -154,7 +155,7 @@ public async Task IssueSessionAsync_WhenActiveRootExists_ShouldReuseRoot() public async Task IssueSessionAsync_WhenChainIdIsNotProvided_ShouldCreateNewChain() { var fixture = CreateFixture(); - var context = CreateIssuanceContext(); + var context = CreateIssuanceContext(fixture); var root = CreateRoot( context.Tenant, @@ -198,7 +199,7 @@ public async Task IssueSessionAsync_WhenChainIdIsNotProvided_ShouldCreateNewChai public async Task IssueSessionAsync_WhenExistingChainIsProvided_ShouldReuseChain() { var fixture = CreateFixture(); - var context = CreateIssuanceContext(); + var context = CreateIssuanceContext(fixture); var root = CreateRoot( context.Tenant, @@ -209,7 +210,7 @@ public async Task IssueSessionAsync_WhenExistingChainIsProvided_ShouldReuseChain context.UserKey, context.Tenant); - context = CreateIssuanceContext( + context = CreateIssuanceContext(fixture, userKey: context.UserKey, chainId: chain.ChainId); @@ -247,7 +248,7 @@ public async Task IssueSessionAsync_WhenExplicitChainDoesNotExist_CurrentlyCreat var requestedChainId = SessionChainId.New(); - var context = CreateIssuanceContext( + var context = CreateIssuanceContext(fixture, userKey: userKey, chainId: requestedChainId); @@ -310,7 +311,7 @@ public async Task IssueSessionAsync_WhenExistingChainBelongsToDifferentUser_Shou otherUser, Tenant); - var context = CreateIssuanceContext( + var context = CreateIssuanceContext(fixture, userKey: user, chainId: chain.ChainId); @@ -340,7 +341,7 @@ public async Task IssueSessionAsync_WhenExistingChainBelongsToDifferentTenant_Sh user, otherTenant); - var context = CreateIssuanceContext( + var context = CreateIssuanceContext(fixture, userKey: user, chainId: chain.ChainId); @@ -370,7 +371,7 @@ public async Task IssueSessionAsync_WhenChainBelongsToDifferentRoot_ShouldReject var chain = CreateChain(otherRoot, user, Tenant); - var context = CreateIssuanceContext( + var context = CreateIssuanceContext(fixture, userKey: user, chainId: chain.ChainId); @@ -399,7 +400,7 @@ public async Task IssueSessionAsync_WhenChainIsRevoked_ShouldReject() chain = chain.Revoke(Now); - var context = CreateIssuanceContext( + var context = CreateIssuanceContext(fixture, userKey: user, chainId: chain.ChainId); @@ -423,7 +424,7 @@ public async Task IssueSessionAsync_ShouldUseConfiguredSessionLifetime() var fixture = CreateFixture( lifetime: TimeSpan.FromHours(4)); - var context = CreateIssuanceContext(); + var context = CreateIssuanceContext(fixture); SetupNewIssuance(fixture, context); @@ -441,7 +442,7 @@ public async Task IssueSessionAsync_WhenMaxLifetimeIsShorter_ShouldCapExpiration lifetime: TimeSpan.FromHours(8), maxLifetime: TimeSpan.FromHours(2)); - var context = CreateIssuanceContext(); + var context = CreateIssuanceContext(fixture); SetupNewIssuance(fixture, context); @@ -459,7 +460,7 @@ public async Task IssueSessionAsync_WhenMaxLifetimeIsLonger_ShouldUseNormalLifet lifetime: TimeSpan.FromHours(4), maxLifetime: TimeSpan.FromHours(24)); - var context = CreateIssuanceContext(); + var context = CreateIssuanceContext(fixture); SetupNewIssuance(fixture, context); @@ -478,7 +479,7 @@ public async Task IssueSessionAsync_WhenMaxLifetimeIsLonger_ShouldUseNormalLifet public async Task IssueSessionAsync_ShouldPersistSessionAndAttachItToChain() { var fixture = CreateFixture(); - var context = CreateIssuanceContext(); + var context = CreateIssuanceContext(fixture); var root = CreateRoot( context.Tenant, @@ -537,9 +538,9 @@ public async Task IssueSessionAsync_ShouldPersistSessionAndAttachItToChain() [Fact] public async Task IssueSessionAsync_WhenModeIsSemiHybrid_ShouldMarkResultAsMetadataOnly() { - var fixture = CreateFixture(); + var fixture = CreateFixture(mode: UAuthMode.SemiHybrid); - var context = CreateIssuanceContext( + var context = CreateIssuanceContext(fixture, mode: UAuthMode.SemiHybrid); SetupNewIssuance(fixture, context); @@ -555,7 +556,7 @@ public async Task IssueSessionAsync_WhenModeIsHybrid_ShouldNotMarkResultAsMetada { var fixture = CreateFixture(); - var context = CreateIssuanceContext( + var context = CreateIssuanceContext(fixture, mode: UAuthMode.Hybrid); SetupNewIssuance(fixture, context); @@ -576,7 +577,7 @@ public async Task IssueSessionAsync_WhenSessionLimitReached_ShouldRemoveOldNonAc var fixture = CreateFixture( maxSessionsPerChain: 2); - var context = CreateIssuanceContext(); + var context = CreateIssuanceContext(fixture); var root = CreateRoot( context.Tenant, @@ -606,7 +607,7 @@ public async Task IssueSessionAsync_WhenSessionLimitReached_ShouldRemoveOldNonAc chain.ChainId, Now.AddHours(-4)); - context = CreateIssuanceContext( + context = CreateIssuanceContext(fixture, userKey: context.UserKey, chainId: chain.ChainId); @@ -657,7 +658,7 @@ public async Task IssueSessionAsync_WhenBelowSessionLimit_ShouldNotRemoveSession var fixture = CreateFixture( maxSessionsPerChain: 3); - var context = CreateIssuanceContext(); + var context = CreateIssuanceContext(fixture); SetupNewIssuance(fixture, context); @@ -680,7 +681,7 @@ public async Task RotateSessionAsync_WhenOpaqueGeneratorReturnsInvalidId_ShouldF var fixture = CreateFixture( opaqueToken: "invalid"); - var context = CreateRotationContext(); + var context = CreateRotationContext(fixture); var act = () => fixture.Sut.RotateSessionAsync(context); @@ -707,7 +708,7 @@ await act.Should() public async Task RotateSessionAsync_WhenRootDoesNotExist_ShouldThrowSecurityException() { var fixture = CreateFixture(); - var context = CreateRotationContext(); + var context = CreateRotationContext(fixture); fixture.Store .Setup(x => x.GetActiveRootByUserAsync( @@ -726,7 +727,7 @@ await act.Should() public async Task RotateSessionAsync_WhenSessionDoesNotExist_ShouldThrowSecurityException() { var fixture = CreateFixture(); - var context = CreateRotationContext(); + var context = CreateRotationContext(fixture); var root = CreateRoot( context.Tenant, @@ -751,7 +752,7 @@ await act.Should() public async Task RotateSessionAsync_WhenSessionIsRevoked_ShouldThrowSecurityException() { var fixture = CreateFixture(); - var context = CreateRotationContext(); + var context = CreateRotationContext(fixture); var root = CreateRoot( context.Tenant, @@ -786,7 +787,7 @@ await act.Should() public async Task RotateSessionAsync_WhenSessionIsExpired_ShouldThrowSecurityException() { var fixture = CreateFixture(); - var context = CreateRotationContext(); + var context = CreateRotationContext(fixture); var root = CreateRoot( context.Tenant, @@ -824,7 +825,7 @@ public async Task RotateSessionAsync_WhenSessionBelongsToDifferentUser_ShouldThr { var fixture = CreateFixture(); - var context = CreateRotationContext(); + var context = CreateRotationContext(fixture); var otherUser = UserKey.New(); var root = CreateRoot( @@ -857,7 +858,7 @@ await act.Should() public async Task RotateSessionAsync_WhenSecurityVersionDoesNotMatch_ShouldThrowSecurityException() { var fixture = CreateFixture(); - var context = CreateRotationContext(); + var context = CreateRotationContext(fixture); var root = CreateRoot( context.Tenant, @@ -889,7 +890,7 @@ await act.Should() public async Task RotateSessionAsync_WhenChainDoesNotExist_ShouldThrowSecurityException() { var fixture = CreateFixture(); - var context = CreateRotationContext(); + var context = CreateRotationContext(fixture); var root = CreateRoot( context.Tenant, @@ -925,7 +926,7 @@ await act.Should() public async Task RotateSessionAsync_WhenChainIsRevoked_ShouldThrowSecurityException() { var fixture = CreateFixture(); - var context = CreateRotationContext(); + var context = CreateRotationContext(fixture); var root = CreateRoot( context.Tenant, @@ -960,7 +961,7 @@ await act.Should() public async Task RotateSessionAsync_WhenChainBelongsToDifferentUser_ShouldThrowSecurityException() { var fixture = CreateFixture(); - var context = CreateRotationContext(); + var context = CreateRotationContext(fixture); var root = CreateRoot( context.Tenant, @@ -995,7 +996,7 @@ await act.Should() public async Task RotateSessionAsync_WhenChainBelongsToDifferentRoot_ShouldThrowSecurityException() { var fixture = CreateFixture(); - var context = CreateRotationContext(); + var context = CreateRotationContext(fixture); var activeRoot = CreateRoot( context.Tenant, @@ -1040,7 +1041,7 @@ await act.Should() public async Task RotateSessionAsync_ShouldCreateNewSessionOnSameChain() { var fixture = CreateFixture(); - var context = CreateRotationContext(); + var context = CreateRotationContext(fixture); var setup = SetupSuccessfulRotation( @@ -1077,7 +1078,7 @@ public async Task RotateSessionAsync_ShouldCreateNewSessionOnSameChain() public async Task RotateSessionAsync_ShouldRotateChainActiveSession() { var fixture = CreateFixture(); - var context = CreateRotationContext(); + var context = CreateRotationContext(fixture); var setup = SetupSuccessfulRotation( @@ -1110,7 +1111,7 @@ public async Task RotateSessionAsync_ShouldRotateChainActiveSession() public async Task RotateSessionAsync_ShouldRevokeOldSession() { var fixture = CreateFixture(); - var context = CreateRotationContext(); + var context = CreateRotationContext(fixture); var setup = SetupSuccessfulRotation( @@ -1146,7 +1147,7 @@ public async Task RotateSessionAsync_ShouldRevokeOldSession() public async Task RotateSessionAsync_ShouldReturnSameBoundSessionThatWasPersisted() { var fixture = CreateFixture(); - var context = CreateRotationContext(); + var context = CreateRotationContext(fixture); var setup = SetupSuccessfulRotation( @@ -1176,9 +1177,9 @@ public async Task RotateSessionAsync_ShouldReturnSameBoundSessionThatWasPersiste [Fact] public async Task RotateSessionAsync_WhenModeIsSemiHybrid_ShouldMarkResultAsMetadataOnly() { - var fixture = CreateFixture(); + var fixture = CreateFixture(mode: UAuthMode.SemiHybrid); - var context = CreateRotationContext( + var context = CreateRotationContext(fixture, mode: UAuthMode.SemiHybrid); SetupSuccessfulRotation( @@ -1198,7 +1199,7 @@ public async Task RotateSessionAsync_WhenMaxLifetimeIsShorter_ShouldCapNewSessio lifetime: TimeSpan.FromHours(8), maxLifetime: TimeSpan.FromHours(2)); - var context = CreateRotationContext(); + var context = CreateRotationContext(fixture); SetupSuccessfulRotation( fixture, @@ -1575,7 +1576,7 @@ await fixture.Sut.LogoutChainAsync( public async Task IssueSessionAsync_ShouldPassCancellationTokenToTransaction() { var fixture = CreateFixture(); - var context = CreateIssuanceContext(); + var context = CreateIssuanceContext(fixture); SetupNewIssuance(fixture, context); @@ -1597,7 +1598,7 @@ await fixture.Sut.IssueSessionAsync( public async Task RotateSessionAsync_ShouldPassCancellationTokenToTransaction() { var fixture = CreateFixture(); - var context = CreateRotationContext(); + var context = CreateRotationContext(fixture); SetupSuccessfulRotation( fixture, @@ -1625,7 +1626,8 @@ private static Fixture CreateFixture( string opaqueToken = OpaqueToken, TimeSpan? lifetime = null, TimeSpan? maxLifetime = null, - int maxSessionsPerChain = 5) + int maxSessionsPerChain = 5, + UAuthMode mode = UAuthMode.Hybrid) { var storeFactory = new Mock( @@ -1639,6 +1641,8 @@ private static Fixture CreateFixture( new Mock( MockBehavior.Strict); + var flowAccessor = new Mock(MockBehavior.Strict); + tokenGenerator .Setup(x => x.Generate()) .Returns(opaqueToken); @@ -1669,8 +1673,7 @@ private static Fixture CreateFixture( .Returns>, CancellationToken>( (action, ct) => action(ct)); - var options = - new UAuthServerOptions(); + var options = new UAuthServerOptions(); options.Session.Lifetime = lifetime ?? Lifetime; @@ -1684,14 +1687,14 @@ private static Fixture CreateFixture( var sut = new UAuthSessionIssuer( storeFactory.Object, - tokenGenerator.Object, - Options.Create(options)); + tokenGenerator.Object); return new Fixture( sut, storeFactory, store, - tokenGenerator); + tokenGenerator, + options.Session); } // ===================================================================== @@ -1861,10 +1864,7 @@ private static void SetupSessionAndChainPersistence( // Domain helpers // ===================================================================== - private static SessionIssuanceContext CreateIssuanceContext( - UserKey? userKey = null, - SessionChainId? chainId = null, - UAuthMode mode = UAuthMode.Hybrid) + private static SessionIssuanceContext CreateIssuanceContext(Fixture fixture, UserKey? userKey = null, SessionChainId? chainId = null, UAuthMode mode = UAuthMode.Hybrid) { return new SessionIssuanceContext { @@ -1875,26 +1875,26 @@ private static SessionIssuanceContext CreateIssuanceContext( Claims = ClaimsSnapshot.Empty, Metadata = SessionMetadata.Empty, Mode = mode, - ChainId = chainId + ChainId = chainId, + SessionOptions = fixture.SessionOptions }; } - private static SessionRotationContext CreateRotationContext( - UAuthMode mode = UAuthMode.Hybrid) + private static SessionRotationContext CreateRotationContext(Fixture fixture, UAuthMode mode = UAuthMode.Hybrid) { return new SessionRotationContext { Tenant = Tenant, - CurrentSessionId = - AuthSessionId.Parse( - "current-session-000000000000000000000000001", - null), + CurrentSessionId = AuthSessionId.Parse( + "current-session-000000000000000000000000001", + null), UserKey = UserKey.New(), Now = Now, Device = TestDevice.Default(), Claims = ClaimsSnapshot.Empty, Metadata = SessionMetadata.Empty, - Mode = mode + Mode = mode, + SessionOptions = fixture.SessionOptions }; } @@ -1954,5 +1954,6 @@ private sealed record Fixture( UAuthSessionIssuer Sut, Mock StoreFactory, Mock Store, - Mock TokenGenerator); + Mock TokenGenerator, + UAuthSessionOptions SessionOptions); } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Tokens/UAuthTokenIssuerTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Tokens/UAuthTokenIssuerTests.cs index 0aa2f3ae..6b2ff195 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Tokens/UAuthTokenIssuerTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Tokens/UAuthTokenIssuerTests.cs @@ -549,7 +549,7 @@ await fixture.Sut.IssueRefreshTokenAsync( captured.ExpiresAt.Should() .Be(Now.Add( - flow.OriginalOptions.Token.RefreshTokenLifetime)); + flow.Options.Token.RefreshTokenLifetime)); fixture.StoreFactory.Verify( x => x.Create(flow.Tenant), @@ -1068,6 +1068,39 @@ public async Task IssueAccessTokenAsync_WhenJwtIdDisabled_ShouldRejectCustomJti( Times.Never); } + [Fact] + public async Task IssueTokens_ShouldUseEffectiveLifetimes() + { + var fixture = CreateFixture(); + var flow = AuthFlowTestFactory.New( + mode: UAuthMode.Hybrid, + tenant: Tenant); + + flow.Options.Token.AccessTokenLifetime = + TimeSpan.FromMinutes(15); + flow.Options.Token.RefreshTokenLifetime = + TimeSpan.FromDays(7); + + flow.Options.Token.AccessTokenLifetime = + TimeSpan.FromMinutes(5); + flow.Options.Token.RefreshTokenLifetime = + TimeSpan.FromMinutes(30); + + var context = CreateTokenContext( + sessionId: CreateSessionId( + "effective-session-0000000000000000000001")); + + var access = await fixture.Sut.IssueAccessTokenAsync( + flow, context); + + var refresh = await fixture.Sut.IssueRefreshTokenAsync( + flow, context, RefreshTokenPersistence.DoNotPersist); + + access.ExpiresAt.Should().Be(Now.AddMinutes(5)); + refresh.Should().NotBeNull(); + refresh!.ExpiresAt.Should().Be(Now.AddMinutes(30)); + } + // ===================================================================== // Context helpers // ===================================================================== @@ -1095,20 +1128,20 @@ private static AuthFlowContext CreateFlow( mode: mode, tenant: Tenant); - flow.OriginalOptions.Token.AccessTokenLifetime = + flow.Options.Token.AccessTokenLifetime = accessTokenLifetime ?? TimeSpan.FromMinutes(15); - flow.OriginalOptions.Token.RefreshTokenLifetime = + flow.Options.Token.RefreshTokenLifetime = refreshTokenLifetime ?? TimeSpan.FromDays(30); if (issuer is not null) - flow.OriginalOptions.Token.Issuer = issuer; + flow.Options.Token.Issuer = issuer; if (audience is not null) - flow.OriginalOptions.Token.Audience = audience; + flow.Options.Token.Audience = audience; - flow.OriginalOptions.Token.KeyId = keyId; - flow.OriginalOptions.Token.AddJwtIdClaim = addJwtIdClaim; + flow.Options.Token.KeyId = keyId; + flow.Options.Token.AddJwtIdClaim = addJwtIdClaim; return flow; }