From 9af18f7994b604ba701f23b34307572a212f8b03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Can=20Karag=C3=B6z?= Date: Sun, 1 Feb 2026 02:14:02 +0300 Subject: [PATCH 1/9] Code Refactoring & Polishing --- ...teAuthClientServiceCollectionExtensions.cs | 1 + .../Services/DefaultCredentialClient.cs | 4 ++-- .../Abstractions/Stores/ISessionStore.cs | 2 +- .../Endpoints/DefaultPkceEndpointHandler.cs | 19 ++++++++++--------- .../Refresh/DefaultSessionTouchService.cs | 2 +- .../EfCoreSessionStore.cs | 12 +++++------- .../InMemorySessionStore.cs | 4 +++- 7 files changed, 23 insertions(+), 21 deletions(-) diff --git a/src/CodeBeam.UltimateAuth.Client/Extensions/UltimateAuthClientServiceCollectionExtensions.cs b/src/CodeBeam.UltimateAuth.Client/Extensions/UltimateAuthClientServiceCollectionExtensions.cs index db0e6428..c867a03a 100644 --- a/src/CodeBeam.UltimateAuth.Client/Extensions/UltimateAuthClientServiceCollectionExtensions.cs +++ b/src/CodeBeam.UltimateAuth.Client/Extensions/UltimateAuthClientServiceCollectionExtensions.cs @@ -102,6 +102,7 @@ private static IServiceCollection AddUltimateAuthClientInternal(this IServiceCol services.TryAddScoped(); services.TryAddScoped(); services.TryAddScoped(); + services.TryAddScoped(); services.TryAddScoped(); services.AddScoped(sp => diff --git a/src/CodeBeam.UltimateAuth.Client/Services/DefaultCredentialClient.cs b/src/CodeBeam.UltimateAuth.Client/Services/DefaultCredentialClient.cs index 0d00f49b..7fc79cd1 100644 --- a/src/CodeBeam.UltimateAuth.Client/Services/DefaultCredentialClient.cs +++ b/src/CodeBeam.UltimateAuth.Client/Services/DefaultCredentialClient.cs @@ -7,12 +7,12 @@ namespace CodeBeam.UltimateAuth.Client.Services { - internal sealed class DefaultUserCredentialClient : ICredentialClient + internal sealed class DefaultCredentialClient : ICredentialClient { private readonly IUAuthRequestClient _request; private readonly UAuthClientOptions _options; - public DefaultUserCredentialClient(IUAuthRequestClient request, IOptions options) + public DefaultCredentialClient(IUAuthRequestClient request, IOptions options) { _request = request; _options = options.Value; diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ISessionStore.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ISessionStore.cs index cca6ea0a..778447ca 100644 --- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ISessionStore.cs +++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ISessionStore.cs @@ -24,7 +24,7 @@ public interface ISessionStore /// Task RotateSessionAsync(AuthSessionId currentSessionId, IssuedSession newSession, SessionStoreContext context, CancellationToken ct = default); - Task TouchSessionAsync(AuthSessionId sessionId, DateTimeOffset at, SessionTouchMode mode = SessionTouchMode.IfNeeded, CancellationToken ct = default); + Task TouchSessionAsync(string? tenantId, AuthSessionId sessionId, DateTimeOffset at, SessionTouchMode mode = SessionTouchMode.IfNeeded, CancellationToken ct = default); /// /// Revokes a single session. diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultPkceEndpointHandler.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultPkceEndpointHandler.cs index a4db8cfa..a10eb123 100644 --- a/src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultPkceEndpointHandler.cs +++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultPkceEndpointHandler.cs @@ -120,7 +120,7 @@ public async Task CompleteAsync(HttpContext ctx) if (!validation.Success) { artifact.RegisterAttempt(); - return RedirectToLoginWithError(ctx, authContext, "invalid"); + return await RedirectToLoginWithErrorAsync(ctx, authContext, "invalid"); } var loginRequest = new LoginRequest @@ -141,7 +141,7 @@ public async Task CompleteAsync(HttpContext ctx) var result = await _flow.LoginAsync(authContext, execution, loginRequest, ctx.RequestAborted); if (!result.IsSuccess) - return RedirectToLoginWithError(ctx, authContext, "invalid"); + return await RedirectToLoginWithErrorAsync(ctx, authContext, "invalid"); if (result.SessionId is not null) { @@ -224,26 +224,27 @@ public async Task CompleteAsync(HttpContext ctx) return null; } - private IResult RedirectToLoginWithError(HttpContext ctx, AuthFlowContext auth, string error) + private async Task RedirectToLoginWithErrorAsync(HttpContext ctx, AuthFlowContext auth, string error) { var basePath = auth.OriginalOptions.Hub.LoginPath ?? "/login"; - var hubKey = ctx.Request.Query["hub"].ToString(); if (!string.IsNullOrWhiteSpace(hubKey)) { var key = new AuthArtifactKey(hubKey); - var artifact = _authStore.GetAsync(key, ctx.RequestAborted).Result; + var artifact = await _authStore.GetAsync(key, ctx.RequestAborted); if (artifact is HubFlowArtifact hub) { hub.MarkCompleted(); - _authStore.StoreAsync(key, hub, ctx.RequestAborted); + await _authStore.StoreAsync(key, hub, ctx.RequestAborted); } - return Results.Redirect($"{basePath}?hub={Uri.EscapeDataString(hubKey)}&__uauth_error={Uri.EscapeDataString(error)}"); + + return Results.Redirect( + $"{basePath}?hub={Uri.EscapeDataString(hubKey)}&__uauth_error={Uri.EscapeDataString(error)}"); } - return Results.Redirect($"{basePath}?__uauth_error={Uri.EscapeDataString(error)}"); + return Results.Redirect( + $"{basePath}?__uauth_error={Uri.EscapeDataString(error)}"); } - } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/DefaultSessionTouchService.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/DefaultSessionTouchService.cs index c8a6c817..a4195fd2 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/DefaultSessionTouchService.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/DefaultSessionTouchService.cs @@ -33,7 +33,7 @@ public async Task RefreshAsync(SessionValidationResult val // didTouch = true; //} - didTouch = await _sessionStore.TouchSessionAsync(validation.SessionId.Value, now, sessionTouchMode, ct); + didTouch = await _sessionStore.TouchSessionAsync(validation.TenantId, validation.SessionId.Value, now, sessionTouchMode, ct); } return SessionRefreshResult.Success(validation.SessionId.Value, didTouch); diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/EfCoreSessionStore.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/EfCoreSessionStore.cs index 59a52927..24e4dc1b 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/EfCoreSessionStore.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/EfCoreSessionStore.cs @@ -142,13 +142,13 @@ await _kernel.ExecuteAsync(async ct => }, ct); } - public async Task TouchSessionAsync(AuthSessionId sessionId, DateTimeOffset at, SessionTouchMode mode = SessionTouchMode.IfNeeded, CancellationToken ct = default) + public async Task TouchSessionAsync(string? tenantId, AuthSessionId sessionId, DateTimeOffset at, SessionTouchMode mode = SessionTouchMode.IfNeeded, CancellationToken ct = default) { var touched = false; await _kernel.ExecuteAsync(async ct => { - var projection = await _db.Sessions.SingleOrDefaultAsync(x => x.SessionId == sessionId, ct); + var projection = await _db.Sessions.SingleOrDefaultAsync(x => x.TenantId == tenantId && x.SessionId == sessionId, ct); if (projection is null) return; @@ -209,7 +209,7 @@ await _kernel.ExecuteAsync(async ct => if (chain.ActiveSessionId is not null) { - var sessionProjection = await _db.Sessions.SingleOrDefaultAsync(x => x.SessionId == chain.ActiveSessionId, ct); + var sessionProjection = await _db.Sessions.SingleOrDefaultAsync(x => x.TenantId == tenantId && x.SessionId == chain.ActiveSessionId, ct); if (sessionProjection is not null) { @@ -243,8 +243,7 @@ public Task RevokeChainAsync(string? tenantId, SessionChainId chainId, DateTimeO if (chain.ActiveSessionId is not null) { - var sessionProjection = await _db.Sessions - .SingleOrDefaultAsync(x => x.SessionId == chain.ActiveSessionId, ct); + var sessionProjection = await _db.Sessions.SingleOrDefaultAsync(x => x.TenantId == tenantId && x.SessionId == chain.ActiveSessionId, ct); if (sessionProjection is not null) { @@ -278,8 +277,7 @@ public Task RevokeRootAsync(string? tenantId, UserKey userKey, DateTimeOffset at if (chain.ActiveSessionId is not null) { - var sessionProjection = await _db.Sessions - .SingleOrDefaultAsync(x => x.SessionId == chain.ActiveSessionId, ct); + var sessionProjection = await _db.Sessions.SingleOrDefaultAsync(x => x.TenantId == tenantId && x.SessionId == chain.ActiveSessionId, ct); if (sessionProjection is not null) { diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/InMemorySessionStore.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/InMemorySessionStore.cs index ed5f958f..6402d1df 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/InMemorySessionStore.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/InMemorySessionStore.cs @@ -5,6 +5,8 @@ using Microsoft.Extensions.Options; using System.Security; +namespace CodeBeam.UltimateAuth.Sessions.InMemory; + public sealed class InMemorySessionStore : ISessionStore { private readonly ISessionStoreKernelFactory _factory; @@ -95,7 +97,7 @@ await k.ExecuteAsync(async (ct) => }, ct); } - public async Task TouchSessionAsync(AuthSessionId sessionId, DateTimeOffset at, SessionTouchMode mode = SessionTouchMode.IfNeeded, CancellationToken ct = default) + public async Task TouchSessionAsync(string? tenantId, AuthSessionId sessionId, DateTimeOffset at, SessionTouchMode mode = SessionTouchMode.IfNeeded, CancellationToken ct = default) { var k = Kernel(null); bool touched = false; From a90c38c2136da75476b8dc856aeb9d56fee92ef4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Can=20Karag=C3=B6z?= Date: Sun, 1 Feb 2026 16:30:03 +0300 Subject: [PATCH 2/9] Core Project Cleanup --- .../Authority/IAccessAuthority.cs | 10 +- .../Authority/IAccessInvariant.cs | 9 +- .../Abstractions/Authority/IAccessPolicy.cs | 11 +- .../Abstractions/Authority/IAuthAuthority.cs | 9 +- .../Authority/IAuthorityInvariant.cs | 9 +- .../Authority/IAuthorityPolicy.cs | 11 +- .../Abstractions/Hub/IHubCapabilities.cs | 9 +- .../Hub/IHubCredentialResolver.cs | 9 +- .../Abstractions/Hub/IHubFlowReader.cs | 9 +- .../Abstractions/Infrastructure/IClock.cs | 17 +- .../Infrastructure/ITokenHasher.cs | 19 +- .../Infrastructure/IUAuthPasswordHasher.cs | 21 +- .../Issuers/IJwtTokenGenerator.cs | 17 +- .../Issuers/IOpaqueTokenGenerator.cs | 17 +- .../Abstractions/Issuers/ISessionIssuer.cs | 19 +- .../Principals/IUserIdConverter.cs | 83 ++++--- .../Principals/IUserIdConverterResolver.cs | 37 ++- .../Abstractions/Principals/IUserIdFactory.cs | 23 +- .../Abstractions/Services/ISessionService.cs | 12 - .../Abstractions/Services/IUAuthService.cs | 15 -- .../Services/IUAuthSessionManager.cs | 33 ++- .../Services/IUAuthUserService.cs | 15 -- .../Stores/DefaultSessionStoreFactory.cs | 20 -- .../Stores/IAccessTokenIdStore.cs | 21 +- .../Abstractions/Stores/ISessionStore.cs | 77 +++--- .../Stores/ISessionStoreKernel.cs | 52 ++-- .../Stores/ISessionStoreKernelFactory.cs | 33 ++- .../Stores/ITenantAwareSessionStore.cs | 9 +- .../Abstractions/Stores/IUAuthUserStore.cs | 52 ---- .../Abstractions/Stores/IUserStoreFactory.cs | 26 -- .../Abstractions/Validators/IJwtValidator.cs | 17 +- .../Contracts/Authority/AccessContext.cs | 89 ++++--- .../Contracts/Authority/AccessDecision.cs | 62 +++-- .../Authority/AccessDecisionResult.cs | 41 ++-- .../Contracts/Authority/AuthContext.cs | 19 +- .../Contracts/Authority/AuthOperation.cs | 19 +- .../Authority/AuthorizationDecision.cs | 14 +- .../Authority/DeviceMismatchBehavior.cs | 14 +- .../Contracts/Common/DeleteMode.cs | 11 +- .../Contracts/Common/PagedResult.cs | 19 +- .../Contracts/Common/UAuthResult.cs | 28 +-- .../Contracts/Login/ExternalLoginRequest.cs | 16 +- .../Contracts/Login/LoginContinuation.cs | 31 ++- .../Contracts/Login/LoginContinuationType.cs | 13 +- .../Contracts/Login/LoginRequest.cs | 33 ++- .../Contracts/Login/LoginResult.cs | 71 +++--- .../Contracts/Login/LoginStatus.cs | 13 +- .../Contracts/Login/ReauthRequest.cs | 13 +- .../Contracts/Login/ReauthResult.cs | 9 +- .../Contracts/Login/UAuthLoginType.cs | 11 +- .../Contracts/Logout/LogoutAllRequest.cs | 30 ++- .../Contracts/Logout/LogoutRequest.cs | 14 +- .../Contracts/Mfa/BeginMfaRequest.cs | 9 +- .../Contracts/Mfa/CompleteMfaRequest.cs | 11 +- .../Contracts/Mfa/MfaChallengeResult.cs | 11 +- .../Contracts/Pkce/PkceCompleteRequest.cs | 17 +- .../Contracts/Refresh/RefreshFlowRequest.cs | 17 +- .../Contracts/Refresh/RefreshFlowResult.cs | 59 +++-- .../Contracts/Refresh/RefreshStrategy.cs | 17 +- .../Refresh/RefreshTokenPersistence.cs | 29 ++- .../Refresh/RefreshTokenValidationContext.cs | 18 +- .../Contracts/Session/AuthStateSnapshot.cs | 18 +- .../Contracts/Session/AuthValidationResult.cs | 37 ++- .../Session/AuthenticatedSessionContext.cs | 47 ++-- .../Contracts/Session/IssuedSession.cs | 36 ++- .../Session/ResolvedRefreshSession.cs | 57 +++-- .../Contracts/Session/SessionContext.cs | 39 ++- .../Session/SessionRefreshRequest.cs | 11 +- .../Contracts/Session/SessionRefreshResult.cs | 57 +++-- .../Contracts/Session/SessionResult.cs | 63 +++-- .../Session/SessionRotationContext.cs | 21 +- .../Session/SessionSecurityContext.cs | 18 +- .../Contracts/Session/SessionStoreContext.cs | 63 +++-- .../Contracts/Session/SessionTouchMode.cs | 23 +- .../Session/SessionValidationContext.cs | 15 +- .../Session/SessionValidationResult.cs | 87 ++++--- .../Contracts/Token/AccessToken.cs | 47 ++-- .../Contracts/Token/AuthTokens.cs | 23 +- .../Contracts/Token/OpaqueTokenRecord.cs | 18 -- .../Contracts/Token/PrimaryToken.cs | 28 +-- .../Contracts/Token/PrimaryTokenKind.cs | 11 +- .../Contracts/Token/RefreshToken.cs | 33 ++- .../Token/RefreshTokenFailureReason.cs | 10 - .../Token/RefreshTokenRotationExecution.cs | 19 +- .../Token/RefreshTokenValidationResult.cs | 95 ++++--- .../Contracts/Token/TokenFormat.cs | 13 +- .../Contracts/Token/TokenInvalidReason.cs | 27 +- .../Contracts/Token/TokenIssuanceContext.cs | 19 +- .../Contracts/Token/TokenIssueContext.cs | 13 +- .../Contracts/Token/TokenRefreshContext.cs | 11 +- .../Contracts/Token/TokenType.cs | 14 +- .../Contracts/Token/TokenValidationResult.cs | 117 +++++---- .../Contracts/Unit.cs | 9 +- .../Contracts/User/AuthUserSnapshot.cs | 47 ++-- .../User/UserAuthenticationResult.cs | 33 ++- .../Contracts/User/UserContext.cs | 15 +- .../User/ValidateCredentialsRequest.cs | 24 -- .../Domain/AuthFlowType.cs | 45 ++-- .../Domain/Device/DeviceContext.cs | 34 ++- .../Domain/Hub/HubCredentials.cs | 13 +- .../Domain/Hub/HubFlowState.cs | 24 +- .../Domain/Principals/AuthFailureReason.cs | 23 +- .../Principals/ClaimsSnapshotBuilder.cs | 51 ++-- .../Domain/Principals/CredentialKind.cs | 13 +- .../Principals/PrimaryCredentialKind.cs | 11 +- .../Domain/Principals/ReauthBehavior.cs | 13 +- .../Domain/Principals/UAuthClaim.cs | 4 - .../Domain/Session/AuthSessionId.cs | 47 ++-- .../Domain/Session/ClaimsSnapshot.cs | 231 +++++++++--------- .../Domain/Session/ISession.cs | 161 ++++++------ .../Domain/Token/StoredRefreshToken.cs | 41 ++-- .../Domain/Token/UAuthJwtTokenDescriptor.cs | 29 +-- .../Domain/User/AuthUserRecord.cs | 49 ++++ .../Domain/User/IAuthSubject.cs | 31 ++- .../Domain/{ => User}/ICurrentUser.cs | 0 .../Domain/User/UserKey.cs | 96 ++++---- .../Extensions/ClaimsSnapshotExtensions.cs | 77 +++--- ...UltimateAuthServiceCollectionExtensions.cs | 143 ++++++----- .../UltimateAuthSessionStoreExtensions.cs | 102 -------- .../UserIdConverterRegistrationExtensions.cs | 101 ++++---- .../Infrastructure/AuthUserRecord.cs | 50 ---- .../Authority/DefaultAuthAuthority.cs | 63 +++-- .../Authority/DeviceMismatchPolicy.cs | 38 ++- .../Authority/DevicePresenceInvariant.cs | 20 +- .../Authority/ExpiredSessionInvariant.cs | 29 ++- .../InvalidOrRevokedSessionInvariant.cs | 37 ++- .../Authority/UAuthModeOperationPolicy.cs | 55 ++--- .../Infrastructure/Base64Url.cs | 72 +++--- .../Infrastructure/GuidUserIdFactory.cs | 9 +- .../Infrastructure/IInMemoryUserIdProvider.cs | 11 +- .../Infrastructure/NoOpAccessTokenIdStore.cs | 19 +- .../Infrastructure/RandomIdGenerator.cs | 54 ---- .../Infrastructure/SeedRunner.cs | 2 +- .../Infrastructure/StringUserIdFactory.cs | 9 +- .../UAuthSessionStoreKernelFactory.cs | 19 ++ .../Infrastructure/UAuthUserIdConverter.cs | 177 +++++++------- .../UAuthUserIdConverterResolver.cs | 71 +++--- .../Infrastructure/UserIdFactory.cs | 9 +- .../Infrastructure/UserKeyJsonConverter.cs | 23 +- .../MultiTenancy/CompositeTenantResolver.cs | 53 ++-- .../MultiTenancy/FixedTenantResolver.cs | 39 ++- .../MultiTenancy/HeaderTenantResolver.cs | 55 ++--- .../MultiTenancy/HostTenantResolver.cs | 39 ++- .../MultiTenancy/ITenantIdResolver.cs | 23 +- .../MultiTenancy/PathTenantResolver.cs | 59 +++-- .../MultiTenancy/TenantResolutionContext.cs | 105 ++++---- .../MultiTenancy/TenantValidation.cs | 33 ++- .../MultiTenancy/UAuthTenantContext.cs | 35 ++- .../Options/IClientProfileDetector.cs | 9 +- .../Options/IServerProfileDetector.cs | 9 - .../Options/UAuthClientProfile.cs | 21 +- .../Options/UAuthLoginOptions.cs | 31 ++- .../Options/UAuthMode.cs | 71 +++--- .../Options/UAuthMultiTenantOptions.cs | 143 ++++++----- .../UAuthMultiTenantOptionsValidator.cs | 115 +++++---- .../Options/UAuthOptions.cs | 107 ++++---- .../Options/UAuthOptionsValidator.cs | 55 ++--- .../Options/UAuthPkceOptions.cs | 37 ++- .../Options/UAuthPkceOptionsValidator.cs | 25 +- .../Options/UAuthSessionOptions.cs | 207 ++++++++-------- .../Options/UAuthSessionOptionsValidator.cs | 135 +++++----- .../Options/UAuthTokenOptions.cs | 129 +++++----- .../Options/UAuthTokenOptionsValidator.cs | 65 +++-- .../Runtime/IUAuthHubMarker.cs | 15 +- .../Runtime/IUAuthProductInfoProvider.cs | 9 +- .../Runtime/UAuthProductInfo.cs | 21 +- .../Runtime/UAuthProductInfoProvider.cs | 33 ++- .../UltimateAuthServerBuilderValidation.cs | 4 +- .../UAuthServerServiceCollectionExtensions.cs | 1 - .../Login/DefaultLoginOrchestrator.cs | 2 +- .../Options/UAuthServerProfileDetector.cs | 28 --- .../Services/DefaultSessionService.cs | 35 --- 172 files changed, 2973 insertions(+), 3578 deletions(-) delete mode 100644 src/CodeBeam.UltimateAuth.Core/Abstractions/Services/ISessionService.cs delete mode 100644 src/CodeBeam.UltimateAuth.Core/Abstractions/Services/IUAuthService.cs delete mode 100644 src/CodeBeam.UltimateAuth.Core/Abstractions/Services/IUAuthUserService.cs delete mode 100644 src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/DefaultSessionStoreFactory.cs delete mode 100644 src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/IUAuthUserStore.cs delete mode 100644 src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/IUserStoreFactory.cs delete mode 100644 src/CodeBeam.UltimateAuth.Core/Contracts/Token/OpaqueTokenRecord.cs delete mode 100644 src/CodeBeam.UltimateAuth.Core/Contracts/Token/RefreshTokenFailureReason.cs delete mode 100644 src/CodeBeam.UltimateAuth.Core/Contracts/User/ValidateCredentialsRequest.cs delete mode 100644 src/CodeBeam.UltimateAuth.Core/Domain/Principals/UAuthClaim.cs create mode 100644 src/CodeBeam.UltimateAuth.Core/Domain/User/AuthUserRecord.cs rename src/CodeBeam.UltimateAuth.Core/Domain/{ => User}/ICurrentUser.cs (100%) delete mode 100644 src/CodeBeam.UltimateAuth.Core/Extensions/UltimateAuthSessionStoreExtensions.cs delete mode 100644 src/CodeBeam.UltimateAuth.Core/Infrastructure/AuthUserRecord.cs delete mode 100644 src/CodeBeam.UltimateAuth.Core/Infrastructure/RandomIdGenerator.cs create mode 100644 src/CodeBeam.UltimateAuth.Core/Infrastructure/UAuthSessionStoreKernelFactory.cs delete mode 100644 src/CodeBeam.UltimateAuth.Core/Options/IServerProfileDetector.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Options/UAuthServerProfileDetector.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Services/DefaultSessionService.cs diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAccessAuthority.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAccessAuthority.cs index bf61d883..a5b3f69c 100644 --- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAccessAuthority.cs +++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAccessAuthority.cs @@ -1,10 +1,8 @@ using CodeBeam.UltimateAuth.Core.Contracts; -namespace CodeBeam.UltimateAuth.Core.Abstractions -{ - public interface IAccessAuthority - { - AccessDecision Decide(AccessContext context, IEnumerable runtimePolicies); - } +namespace CodeBeam.UltimateAuth.Core.Abstractions; +public interface IAccessAuthority +{ + AccessDecision Decide(AccessContext context, IEnumerable runtimePolicies); } diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAccessInvariant.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAccessInvariant.cs index 806d6c91..c043d44d 100644 --- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAccessInvariant.cs +++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAccessInvariant.cs @@ -1,9 +1,8 @@ using CodeBeam.UltimateAuth.Core.Contracts; -namespace CodeBeam.UltimateAuth.Core.Abstractions +namespace CodeBeam.UltimateAuth.Core.Abstractions; + +public interface IAccessInvariant { - public interface IAccessInvariant - { - AccessDecision Decide(AccessContext context); - } + AccessDecision Decide(AccessContext context); } diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAccessPolicy.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAccessPolicy.cs index 487072fe..49a1efad 100644 --- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAccessPolicy.cs +++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAccessPolicy.cs @@ -1,10 +1,9 @@ using CodeBeam.UltimateAuth.Core.Contracts; -namespace CodeBeam.UltimateAuth.Core.Abstractions +namespace CodeBeam.UltimateAuth.Core.Abstractions; + +public interface IAccessPolicy { - public interface IAccessPolicy - { - bool AppliesTo(AccessContext context); - AccessDecision Decide(AccessContext context); - } + bool AppliesTo(AccessContext context); + AccessDecision Decide(AccessContext context); } diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAuthAuthority.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAuthAuthority.cs index 9a294587..4e5eface 100644 --- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAuthAuthority.cs +++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAuthAuthority.cs @@ -1,9 +1,8 @@ using CodeBeam.UltimateAuth.Core.Contracts; -namespace CodeBeam.UltimateAuth.Core.Abstractions +namespace CodeBeam.UltimateAuth.Core.Abstractions; + +public interface IAuthAuthority { - public interface IAuthAuthority - { - AccessDecisionResult Decide(AuthContext context, IEnumerable? policies = null); - } + AccessDecisionResult Decide(AuthContext context, IEnumerable? policies = null); } diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAuthorityInvariant.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAuthorityInvariant.cs index dc0cc0a5..2fe227d1 100644 --- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAuthorityInvariant.cs +++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAuthorityInvariant.cs @@ -1,9 +1,8 @@ using CodeBeam.UltimateAuth.Core.Contracts; -namespace CodeBeam.UltimateAuth.Core.Abstractions +namespace CodeBeam.UltimateAuth.Core.Abstractions; + +public interface IAuthorityInvariant { - public interface IAuthorityInvariant - { - AccessDecisionResult Decide(AuthContext context); - } + AccessDecisionResult Decide(AuthContext context); } diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAuthorityPolicy.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAuthorityPolicy.cs index 2b2021a2..5d2bc41d 100644 --- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAuthorityPolicy.cs +++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAuthorityPolicy.cs @@ -1,10 +1,9 @@ using CodeBeam.UltimateAuth.Core.Contracts; -namespace CodeBeam.UltimateAuth.Core.Abstractions +namespace CodeBeam.UltimateAuth.Core.Abstractions; + +public interface IAuthorityPolicy { - public interface IAuthorityPolicy - { - bool AppliesTo(AuthContext context); - AccessDecisionResult Decide(AuthContext context); - } + bool AppliesTo(AuthContext context); + AccessDecisionResult Decide(AuthContext context); } diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Hub/IHubCapabilities.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Hub/IHubCapabilities.cs index 36bd1b34..3d5a5817 100644 --- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Hub/IHubCapabilities.cs +++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Hub/IHubCapabilities.cs @@ -1,7 +1,6 @@ -namespace CodeBeam.UltimateAuth.Core.Abstractions +namespace CodeBeam.UltimateAuth.Core.Abstractions; + +public interface IHubCapabilities { - public interface IHubCapabilities - { - bool SupportsPkce { get; } - } + bool SupportsPkce { get; } } diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Hub/IHubCredentialResolver.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Hub/IHubCredentialResolver.cs index 78ecb59f..f3e075b4 100644 --- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Hub/IHubCredentialResolver.cs +++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Hub/IHubCredentialResolver.cs @@ -1,9 +1,8 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Core.Abstractions +namespace CodeBeam.UltimateAuth.Core.Abstractions; + +public interface IHubCredentialResolver { - public interface IHubCredentialResolver - { - Task ResolveAsync(HubSessionId hubSessionId, CancellationToken ct = default); - } + Task ResolveAsync(HubSessionId hubSessionId, CancellationToken ct = default); } diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Hub/IHubFlowReader.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Hub/IHubFlowReader.cs index 82764fb4..0096d891 100644 --- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Hub/IHubFlowReader.cs +++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Hub/IHubFlowReader.cs @@ -1,9 +1,8 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Core.Abstractions +namespace CodeBeam.UltimateAuth.Core.Abstractions; + +public interface IHubFlowReader { - public interface IHubFlowReader - { - Task GetStateAsync(HubSessionId hubSessionId, CancellationToken ct = default); - } + Task GetStateAsync(HubSessionId hubSessionId, CancellationToken ct = default); } diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Infrastructure/IClock.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Infrastructure/IClock.cs index a624091b..71e7a186 100644 --- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Infrastructure/IClock.cs +++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Infrastructure/IClock.cs @@ -1,11 +1,10 @@ -namespace CodeBeam.UltimateAuth.Core.Abstractions +namespace CodeBeam.UltimateAuth.Core.Abstractions; + +/// +/// Provides an abstracted time source for the system. +/// Used to improve testability and ensure consistent time handling. +/// +public interface IClock { - /// - /// Provides an abstracted time source for the system. - /// Used to improve testability and ensure consistent time handling. - /// - public interface IClock - { - DateTimeOffset UtcNow { get; } - } + DateTimeOffset UtcNow { get; } } diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Infrastructure/ITokenHasher.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Infrastructure/ITokenHasher.cs index ebf44998..8112e451 100644 --- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Infrastructure/ITokenHasher.cs +++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Infrastructure/ITokenHasher.cs @@ -1,12 +1,11 @@ -namespace CodeBeam.UltimateAuth.Core.Abstractions +namespace CodeBeam.UltimateAuth.Core.Abstractions; + +/// +/// Hashes and verifies sensitive tokens. +/// Used for refresh tokens, session ids, opaque tokens. +/// +public interface ITokenHasher { - /// - /// Hashes and verifies sensitive tokens. - /// Used for refresh tokens, session ids, opaque tokens. - /// - public interface ITokenHasher - { - string Hash(string plaintext); - bool Verify(string plaintext, string hash); - } + string Hash(string plaintext); + bool Verify(string plaintext, string hash); } diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Infrastructure/IUAuthPasswordHasher.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Infrastructure/IUAuthPasswordHasher.cs index d6596c91..039a8216 100644 --- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Infrastructure/IUAuthPasswordHasher.cs +++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Infrastructure/IUAuthPasswordHasher.cs @@ -1,13 +1,12 @@ -namespace CodeBeam.UltimateAuth.Core.Abstractions +namespace CodeBeam.UltimateAuth.Core.Abstractions; + +/// +/// Securely hashes and verifies user passwords. +/// Designed for slow, adaptive, memory-hard algorithms +/// such as Argon2 or bcrypt. +/// +public interface IUAuthPasswordHasher { - /// - /// Securely hashes and verifies user passwords. - /// Designed for slow, adaptive, memory-hard algorithms - /// such as Argon2 or bcrypt. - /// - public interface IUAuthPasswordHasher - { - string Hash(string password); - bool Verify(string hash, string secret); - } + string Hash(string password); + bool Verify(string hash, string secret); } diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Issuers/IJwtTokenGenerator.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Issuers/IJwtTokenGenerator.cs index 0fe74224..a03e1256 100644 --- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Issuers/IJwtTokenGenerator.cs +++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Issuers/IJwtTokenGenerator.cs @@ -1,13 +1,12 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Core.Abstractions +namespace CodeBeam.UltimateAuth.Core.Abstractions; + +/// +/// Low-level JWT creation abstraction. +/// Can be replaced for asymmetric keys, external KMS, etc. +/// +public interface IJwtTokenGenerator { - /// - /// Low-level JWT creation abstraction. - /// Can be replaced for asymmetric keys, external KMS, etc. - /// - public interface IJwtTokenGenerator - { - string CreateToken(UAuthJwtTokenDescriptor descriptor); - } + string CreateToken(UAuthJwtTokenDescriptor descriptor); } diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Issuers/IOpaqueTokenGenerator.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Issuers/IOpaqueTokenGenerator.cs index 0c49dcfc..a5332d1f 100644 --- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Issuers/IOpaqueTokenGenerator.cs +++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Issuers/IOpaqueTokenGenerator.cs @@ -1,11 +1,10 @@ -namespace CodeBeam.UltimateAuth.Core.Abstractions +namespace CodeBeam.UltimateAuth.Core.Abstractions; + +/// +/// Generates cryptographically secure random tokens +/// for opaque identifiers, refresh tokens, session ids. +/// +public interface IOpaqueTokenGenerator { - /// - /// Generates cryptographically secure random tokens - /// for opaque identifiers, refresh tokens, session ids. - /// - public interface IOpaqueTokenGenerator - { - string Generate(int byteLength = 32); - } + string Generate(int byteLength = 32); } diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Issuers/ISessionIssuer.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Issuers/ISessionIssuer.cs index 39ec7c59..b5d7b3b2 100644 --- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Issuers/ISessionIssuer.cs +++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Issuers/ISessionIssuer.cs @@ -1,20 +1,19 @@ using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Core.Abstractions +namespace CodeBeam.UltimateAuth.Core.Abstractions; + +public interface ISessionIssuer { - public interface ISessionIssuer - { - Task IssueLoginSessionAsync(AuthenticatedSessionContext context, CancellationToken cancellationToken = default); + Task IssueLoginSessionAsync(AuthenticatedSessionContext context, CancellationToken cancellationToken = default); - Task RotateSessionAsync(SessionRotationContext context, CancellationToken cancellationToken = default); + Task RotateSessionAsync(SessionRotationContext context, CancellationToken cancellationToken = default); - Task RevokeSessionAsync(string? tenantId, AuthSessionId sessionId, DateTimeOffset at, CancellationToken cancellationToken = default); + Task RevokeSessionAsync(string? tenantId, AuthSessionId sessionId, DateTimeOffset at, CancellationToken cancellationToken = default); - Task RevokeChainAsync(string? tenantId, SessionChainId chainId, DateTimeOffset at, CancellationToken cancellationToken = default); + Task RevokeChainAsync(string? tenantId, SessionChainId chainId, DateTimeOffset at, CancellationToken cancellationToken = default); - Task RevokeAllChainsAsync(string? tenantId, UserKey userKey, SessionChainId? exceptChainId, DateTimeOffset at, CancellationToken ct = default); + Task RevokeAllChainsAsync(string? tenantId, UserKey userKey, SessionChainId? exceptChainId, DateTimeOffset at, CancellationToken ct = default); - Task RevokeRootAsync(string? tenantId, UserKey userKey, DateTimeOffset at,CancellationToken ct = default); - } + Task RevokeRootAsync(string? tenantId, UserKey userKey, DateTimeOffset at,CancellationToken ct = default); } diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Principals/IUserIdConverter.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Principals/IUserIdConverter.cs index 288e05bb..1a222d83 100644 --- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Principals/IUserIdConverter.cs +++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Principals/IUserIdConverter.cs @@ -1,49 +1,48 @@ -namespace CodeBeam.UltimateAuth.Core.Abstractions +namespace CodeBeam.UltimateAuth.Core.Abstractions; + +/// +/// Defines conversion logic for transforming user identifiers between +/// strongly typed values, string representations, and binary formats. +/// Implementations enable consistent storage, token serialization, +/// and multitenant key partitioning. +/// Returned string must be stable and culture-invariant. +/// Implementations must be deterministic and reversible. +/// +public interface IUserIdConverter { /// - /// Defines conversion logic for transforming user identifiers between - /// strongly typed values, string representations, and binary formats. - /// Implementations enable consistent storage, token serialization, - /// and multitenant key partitioning. - /// Returned string must be stable and culture-invariant. - /// Implementations must be deterministic and reversible. + /// Converts the typed user identifier into its canonical string representation. /// - public interface IUserIdConverter - { - /// - /// Converts the typed user identifier into its canonical string representation. - /// - /// The user identifier to convert. - /// A stable and reversible string representation of the identifier. - string ToString(TUserId id); + /// The user identifier to convert. + /// A stable and reversible string representation of the identifier. + string ToCanonicalString(TUserId id); - /// - /// Converts the typed user identifier into a binary representation suitable for efficient storage or hashing operations. - /// - /// The user identifier to convert. - /// A byte array representing the identifier. - byte[] ToBytes(TUserId id); + /// + /// Converts the typed user identifier into a binary representation suitable for efficient storage or hashing operations. + /// + /// The user identifier to convert. + /// A byte array representing the identifier. + byte[] ToBytes(TUserId id); - /// - /// Reconstructs a typed user identifier from its string representation. - /// - /// The string-encoded identifier. - /// The reconstructed user identifier. - /// - /// Thrown when the input value cannot be parsed into a valid identifier. - /// - TUserId FromString(string value); - bool TryFromString(string value, out TUserId userId); + /// + /// Reconstructs a typed user identifier from its string representation. + /// + /// The string-encoded identifier. + /// The reconstructed user identifier. + /// + /// Thrown when the input value cannot be parsed into a valid identifier. + /// + TUserId FromString(string value); + bool TryFromString(string value, out TUserId userId); - /// - /// Reconstructs a typed user identifier from its binary representation. - /// - /// The byte array containing the encoded identifier. - /// The reconstructed user identifier. - /// - /// Thrown when the input binary value cannot be parsed into a valid identifier. - /// - TUserId FromBytes(byte[] binary); - bool TryFromBytes(byte[] binary, out TUserId userId); - } + /// + /// Reconstructs a typed user identifier from its binary representation. + /// + /// The byte array containing the encoded identifier. + /// The reconstructed user identifier. + /// + /// Thrown when the input binary value cannot be parsed into a valid identifier. + /// + TUserId FromBytes(byte[] binary); + bool TryFromBytes(byte[] binary, out TUserId userId); } diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Principals/IUserIdConverterResolver.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Principals/IUserIdConverterResolver.cs index fc642d37..bd8dd807 100644 --- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Principals/IUserIdConverterResolver.cs +++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Principals/IUserIdConverterResolver.cs @@ -1,23 +1,22 @@ -namespace CodeBeam.UltimateAuth.Core.Abstractions +namespace CodeBeam.UltimateAuth.Core.Abstractions; + +/// +/// Resolves the appropriate instance +/// for a given user identifier type. Used internally by UltimateAuth to +/// ensure consistent serialization and parsing of user IDs across all components. +/// +public interface IUserIdConverterResolver { /// - /// Resolves the appropriate instance - /// for a given user identifier type. Used internally by UltimateAuth to - /// ensure consistent serialization and parsing of user IDs across all components. + /// Retrieves the registered for the specified user ID type. /// - public interface IUserIdConverterResolver - { - /// - /// Retrieves the registered for the specified user ID type. - /// - /// The type of the user identifier. - /// - /// A converter capable of transforming the user ID to and from its string - /// and binary representations. - /// - /// - /// Thrown if no converter has been registered for the requested user ID type. - /// - IUserIdConverter GetConverter(string? purpose = null); - } + /// The type of the user identifier. + /// + /// A converter capable of transforming the user ID to and from its string + /// and binary representations. + /// + /// + /// Thrown if no converter has been registered for the requested user ID type. + /// + IUserIdConverter GetConverter(string? purpose = null); } diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Principals/IUserIdFactory.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Principals/IUserIdFactory.cs index b5d2715d..c72f650f 100644 --- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Principals/IUserIdFactory.cs +++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Principals/IUserIdFactory.cs @@ -1,16 +1,15 @@ -namespace CodeBeam.UltimateAuth.Core.Abstractions +namespace CodeBeam.UltimateAuth.Core.Abstractions; + +/// +/// Responsible for creating new user identifiers. +/// This abstraction allows UltimateAuth to remain +/// independent from the concrete user ID type. +/// +/// User identifier type. +public interface IUserIdFactory { /// - /// Responsible for creating new user identifiers. - /// This abstraction allows UltimateAuth to remain - /// independent from the concrete user ID type. + /// Creates a new unique user identifier. /// - /// User identifier type. - public interface IUserIdFactory - { - /// - /// Creates a new unique user identifier. - /// - TUserId Create(); - } + TUserId Create(); } diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Services/ISessionService.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Services/ISessionService.cs deleted file mode 100644 index 9dc9df9f..00000000 --- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Services/ISessionService.cs +++ /dev/null @@ -1,12 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Contracts; -using CodeBeam.UltimateAuth.Core.Domain; - -namespace CodeBeam.UltimateAuth.Core.Abstractions -{ - public interface ISessionService - { - Task RevokeAllAsync(AuthContext authContext, UserKey userKey, CancellationToken ct = default); - Task RevokeAllExceptChainAsync(AuthContext authContext, UserKey userKey, SessionChainId exceptChainId, CancellationToken ct = default); - Task RevokeRootAsync(AuthContext authContext, UserKey userKey, CancellationToken ct = default); - } -} diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Services/IUAuthService.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Services/IUAuthService.cs deleted file mode 100644 index 9486398d..00000000 --- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Services/IUAuthService.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace CodeBeam.UltimateAuth.Core.Abstractions -{ - /// - /// High-level facade for UltimateAuth. - /// Provides access to authentication flows, - /// session lifecycle and user operations. - /// - //public interface IUAuthService - //{ - // //IUAuthFlowService Flow { get; } - // IUAuthSessionManager Sessions { get; } - // //IUAuthTokenService Tokens { get; } - // IUAuthUserService Users { get; } - //} -} diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Services/IUAuthSessionManager.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Services/IUAuthSessionManager.cs index 2f759e92..9cded19d 100644 --- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Services/IUAuthSessionManager.cs +++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Services/IUAuthSessionManager.cs @@ -1,27 +1,26 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Core.Abstractions +namespace CodeBeam.UltimateAuth.Core.Abstractions; + +/// +/// Provides high-level session lifecycle operations such as creation, refresh, validation, and revocation. +/// +public interface IUAuthSessionManager { - /// - /// Provides high-level session lifecycle operations such as creation, refresh, validation, and revocation. - /// - public interface IUAuthSessionManager - { - Task> GetChainsAsync(string? tenantId, UserKey userKey); + Task> GetChainsAsync(string? tenantId, UserKey userKey); - Task> GetSessionsAsync(string? tenantId, SessionChainId chainId); + Task> GetSessionsAsync(string? tenantId, SessionChainId chainId); - Task GetCurrentSessionAsync(string? tenantId, AuthSessionId sessionId); + Task GetCurrentSessionAsync(string? tenantId, AuthSessionId sessionId); - Task RevokeSessionAsync(string? tenantId, AuthSessionId sessionId, DateTimeOffset at); + Task RevokeSessionAsync(string? tenantId, AuthSessionId sessionId, DateTimeOffset at); - Task RevokeChainAsync(string? tenantId, SessionChainId chainId, DateTimeOffset at); + Task RevokeChainAsync(string? tenantId, SessionChainId chainId, DateTimeOffset at); - Task ResolveChainIdAsync(string? tenantId, AuthSessionId sessionId); + Task ResolveChainIdAsync(string? tenantId, AuthSessionId sessionId); - Task RevokeAllChainsAsync(string? tenantId, UserKey userKey, SessionChainId? exceptChainId, DateTimeOffset at); - - // Hard revoke - admin - Task RevokeRootAsync(string? tenantId, UserKey userKey, DateTimeOffset at); - } + Task RevokeAllChainsAsync(string? tenantId, UserKey userKey, SessionChainId? exceptChainId, DateTimeOffset at); + + // Hard revoke - admin + Task RevokeRootAsync(string? tenantId, UserKey userKey, DateTimeOffset at); } diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Services/IUAuthUserService.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Services/IUAuthUserService.cs deleted file mode 100644 index d546e55f..00000000 --- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Services/IUAuthUserService.cs +++ /dev/null @@ -1,15 +0,0 @@ -//using CodeBeam.UltimateAuth.Core.Contracts; - -//namespace CodeBeam.UltimateAuth.Core.Abstractions -//{ -// /// -// /// Defines the minimal user authentication contract expected by UltimateAuth. -// /// This service does not manage sessions, tokens, or transport concerns. -// /// For user management, CodeBeam.UltimateAuth.Users package is recommended. -// /// -// public interface IUAuthUserService -// { -// Task> AuthenticateAsync(string? tenantId, string identifier, string secret, CancellationToken cancellationToken = default); -// Task ValidateCredentialsAsync(ValidateCredentialsRequest request, CancellationToken cancellationToken = default); -// } -//} diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/DefaultSessionStoreFactory.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/DefaultSessionStoreFactory.cs deleted file mode 100644 index 25fac768..00000000 --- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/DefaultSessionStoreFactory.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; - -namespace CodeBeam.UltimateAuth.Core.Abstractions -{ - /// - /// Default session store factory that throws until a real store implementation is registered. - /// - internal sealed class DefaultSessionStoreFactory : ISessionStoreKernelFactory - { - private readonly IServiceProvider _sp; - - public DefaultSessionStoreFactory(IServiceProvider sp) - { - _sp = sp; - } - - public ISessionStoreKernel Create(string? tenantId) - => _sp.GetRequiredService(); - } -} diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/IAccessTokenIdStore.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/IAccessTokenIdStore.cs index edf2d58e..94d2933c 100644 --- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/IAccessTokenIdStore.cs +++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/IAccessTokenIdStore.cs @@ -1,15 +1,14 @@ -namespace CodeBeam.UltimateAuth.Core.Abstractions +namespace CodeBeam.UltimateAuth.Core.Abstractions; + +/// +/// Optional persistence for access token identifiers (jti). +/// Used for revocation and replay protection. +/// +public interface IAccessTokenIdStore { - /// - /// Optional persistence for access token identifiers (jti). - /// Used for revocation and replay protection. - /// - public interface IAccessTokenIdStore - { - Task StoreAsync(string? tenantId, string jti, DateTimeOffset expiresAt, CancellationToken ct = default); + Task StoreAsync(string? tenantId, string jti, DateTimeOffset expiresAt, CancellationToken ct = default); - Task IsRevokedAsync(string? tenantId, string jti, CancellationToken ct = default); + Task IsRevokedAsync(string? tenantId, string jti, CancellationToken ct = default); - Task RevokeAsync(string? tenantId, string jti, DateTimeOffset revokedAt, CancellationToken ct = default); - } + Task RevokeAsync(string? tenantId, string jti, DateTimeOffset revokedAt, CancellationToken ct = default); } diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ISessionStore.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ISessionStore.cs index 778447ca..1a8a69a5 100644 --- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ISessionStore.cs +++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ISessionStore.cs @@ -1,46 +1,45 @@ using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Core.Abstractions +namespace CodeBeam.UltimateAuth.Core.Abstractions; + +/// +/// High-level session store abstraction used by UltimateAuth. +/// Encapsulates session, chain, and root orchestration. +/// +public interface ISessionStore { /// - /// High-level session store abstraction used by UltimateAuth. - /// Encapsulates session, chain, and root orchestration. + /// Retrieves an active session by id. + /// + Task GetSessionAsync(string? tenantId, AuthSessionId sessionId, CancellationToken ct = default); + + /// + /// Creates a new session and associates it with the appropriate chain and root. + /// + Task CreateSessionAsync(IssuedSession issuedSession, SessionStoreContext context, CancellationToken ct = default); + + /// + /// Refreshes (rotates) the active session within its chain. + /// + Task RotateSessionAsync(AuthSessionId currentSessionId, IssuedSession newSession, SessionStoreContext context, CancellationToken ct = default); + + Task TouchSessionAsync(string? tenantId, AuthSessionId sessionId, DateTimeOffset at, SessionTouchMode mode = SessionTouchMode.IfNeeded, CancellationToken ct = default); + + /// + /// Revokes a single session. + /// + Task RevokeSessionAsync(string? tenantId, AuthSessionId sessionId, DateTimeOffset at, CancellationToken ct = default); + + /// + /// Revokes all sessions for a specific user (all devices). /// - public interface ISessionStore - { - /// - /// Retrieves an active session by id. - /// - Task GetSessionAsync(string? tenantId, AuthSessionId sessionId, CancellationToken ct = default); - - /// - /// Creates a new session and associates it with the appropriate chain and root. - /// - Task CreateSessionAsync(IssuedSession issuedSession, SessionStoreContext context, CancellationToken ct = default); - - /// - /// Refreshes (rotates) the active session within its chain. - /// - Task RotateSessionAsync(AuthSessionId currentSessionId, IssuedSession newSession, SessionStoreContext context, CancellationToken ct = default); - - Task TouchSessionAsync(string? tenantId, AuthSessionId sessionId, DateTimeOffset at, SessionTouchMode mode = SessionTouchMode.IfNeeded, CancellationToken ct = default); - - /// - /// Revokes a single session. - /// - Task RevokeSessionAsync(string? tenantId, AuthSessionId sessionId, DateTimeOffset at, CancellationToken ct = default); - - /// - /// Revokes all sessions for a specific user (all devices). - /// - Task RevokeAllChainsAsync(string? tenantId, UserKey userKey, SessionChainId? exceptChainId, DateTimeOffset at, CancellationToken ct = default); - - /// - /// Revokes all sessions within a specific chain (single device). - /// - Task RevokeChainAsync(string? tenantId, SessionChainId chainId, DateTimeOffset at, CancellationToken ct = default); - - Task RevokeRootAsync(string? tenantId, UserKey userKey, DateTimeOffset at, CancellationToken ct = default); - } + Task RevokeAllChainsAsync(string? tenantId, UserKey userKey, SessionChainId? exceptChainId, DateTimeOffset at, CancellationToken ct = default); + + /// + /// Revokes all sessions within a specific chain (single device). + /// + Task RevokeChainAsync(string? tenantId, SessionChainId chainId, DateTimeOffset at, CancellationToken ct = default); + + Task RevokeRootAsync(string? tenantId, UserKey userKey, DateTimeOffset at, CancellationToken ct = default); } diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ISessionStoreKernel.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ISessionStoreKernel.cs index 578f2427..1ddced8f 100644 --- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ISessionStoreKernel.cs +++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ISessionStoreKernel.cs @@ -1,36 +1,34 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Core.Abstractions +namespace CodeBeam.UltimateAuth.Core.Abstractions; + +public interface ISessionStoreKernel { - public interface ISessionStoreKernel - { - Task ExecuteAsync(Func action, CancellationToken ct = default); - //string? TenantId { get; } + Task ExecuteAsync(Func action, CancellationToken ct = default); - // Session - Task GetSessionAsync(AuthSessionId sessionId); - Task SaveSessionAsync(ISession session); - Task RevokeSessionAsync(AuthSessionId sessionId, DateTimeOffset at); + // Session + Task GetSessionAsync(AuthSessionId sessionId); + Task SaveSessionAsync(ISession session); + Task RevokeSessionAsync(AuthSessionId sessionId, DateTimeOffset at); - // Chain - Task GetChainAsync(SessionChainId chainId); - Task SaveChainAsync(ISessionChain chain); - Task RevokeChainAsync(SessionChainId chainId, DateTimeOffset at); - Task GetActiveSessionIdAsync(SessionChainId chainId); - Task SetActiveSessionIdAsync(SessionChainId chainId, AuthSessionId sessionId); + // Chain + Task GetChainAsync(SessionChainId chainId); + Task SaveChainAsync(ISessionChain chain); + Task RevokeChainAsync(SessionChainId chainId, DateTimeOffset at); + Task GetActiveSessionIdAsync(SessionChainId chainId); + Task SetActiveSessionIdAsync(SessionChainId chainId, AuthSessionId sessionId); - // Root - Task GetSessionRootByUserAsync(UserKey userKey); - Task GetSessionRootByIdAsync(SessionRootId rootId); - Task SaveSessionRootAsync(ISessionRoot root); - Task RevokeSessionRootAsync(UserKey userKey, DateTimeOffset at); + // Root + Task GetSessionRootByUserAsync(UserKey userKey); + Task GetSessionRootByIdAsync(SessionRootId rootId); + Task SaveSessionRootAsync(ISessionRoot root); + Task RevokeSessionRootAsync(UserKey userKey, DateTimeOffset at); - // Helpers - Task GetChainIdBySessionAsync(AuthSessionId sessionId); - Task> GetChainsByUserAsync(UserKey userKey); - Task> GetSessionsByChainAsync(SessionChainId chainId); + // Helpers + Task GetChainIdBySessionAsync(AuthSessionId sessionId); + Task> GetChainsByUserAsync(UserKey userKey); + Task> GetSessionsByChainAsync(SessionChainId chainId); - // Maintenance - Task DeleteExpiredSessionsAsync(DateTimeOffset at); - } + // Maintenance + Task DeleteExpiredSessionsAsync(DateTimeOffset at); } diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ISessionStoreKernelFactory.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ISessionStoreKernelFactory.cs index b529fa62..5bf3a8e9 100644 --- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ISessionStoreKernelFactory.cs +++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ISessionStoreKernelFactory.cs @@ -1,21 +1,20 @@ -namespace CodeBeam.UltimateAuth.Core.Abstractions +namespace CodeBeam.UltimateAuth.Core.Abstractions; + +/// +/// Provides a factory abstraction for creating tenant-scoped session store +/// instances capable of persisting sessions, chains, and session roots. +/// Implementations typically resolve concrete types from the dependency injection container. +/// +public interface ISessionStoreKernelFactory { /// - /// Provides a factory abstraction for creating tenant-scoped session store - /// instances capable of persisting sessions, chains, and session roots. - /// Implementations typically resolve concrete types from the dependency injection container. + /// Creates and returns a session store instance for the specified user ID type within the given tenant context. /// - public interface ISessionStoreKernelFactory - { - /// - /// Creates and returns a session store instance for the specified user ID type within the given tenant context. - /// - /// - /// The tenant identifier for multi-tenant environments, or null for single-tenant mode. - /// - /// - /// An implementation able to perform session persistence operations. - /// - ISessionStoreKernel Create(string? tenantId); - } + /// + /// The tenant identifier for multi-tenant environments, or null for single-tenant mode. + /// + /// + /// An implementation able to perform session persistence operations. + /// + ISessionStoreKernel Create(string? tenantId); } diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ITenantAwareSessionStore.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ITenantAwareSessionStore.cs index 2a90b92c..08a9f23e 100644 --- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ITenantAwareSessionStore.cs +++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ITenantAwareSessionStore.cs @@ -1,7 +1,6 @@ -namespace CodeBeam.UltimateAuth.Core.Abstractions +namespace CodeBeam.UltimateAuth.Core.Abstractions; + +public interface ITenantAwareSessionStore { - public interface ITenantAwareSessionStore - { - void BindTenant(string? tenantId); - } + void BindTenant(string? tenantId); } diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/IUAuthUserStore.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/IUAuthUserStore.cs deleted file mode 100644 index b97c47e1..00000000 --- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/IUAuthUserStore.cs +++ /dev/null @@ -1,52 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Domain; -using CodeBeam.UltimateAuth.Core.Infrastructure; - -namespace CodeBeam.UltimateAuth.Core.Abstractions -{ - /// - /// Provides minimal user lookup and security metadata required for authentication. - /// This store does not manage user creation, claims, or profile data — these belong - /// to higher-level application services outside UltimateAuth. - /// - public interface IUAuthUserStore - { - Task?> FindByIdAsync(string? tenantId, TUserId userId, CancellationToken token = default); - - Task?> FindByUsernameAsync(string? tenantId, string username, CancellationToken ct = default); - - /// - /// Retrieves a user by a login credential such as username or email. - /// Returns null if no matching user exists. - /// - /// The user instance or null if not found. - Task?> FindByLoginAsync(string? tenantId, string login, CancellationToken token = default); - - /// - /// Returns the password hash for the specified user, if the user participates - /// in password-based authentication. Returns null for passwordless users - /// (e.g., external login or passkey-only accounts). - /// - /// The password hash or null. - Task GetPasswordHashAsync(string? tenantId, TUserId userId, CancellationToken token = default); - - /// - /// Updates the password hash for the specified user. This method is invoked by - /// password management services and not by . - /// - Task SetPasswordHashAsync(string? tenantId, TUserId userId, string passwordHash, CancellationToken token = default); - - /// - /// Retrieves the security version associated with the user. - /// This value increments whenever critical security actions occur, such as: - /// password reset, MFA reset, external login removal, or account recovery. - /// - /// The current security version. - Task GetSecurityVersionAsync(string? tenantId, TUserId userId, CancellationToken token = default); - - /// - /// Increments the user's security version, invalidating all existing sessions. - /// This is typically called after sensitive security events occur. - /// - Task IncrementSecurityVersionAsync(string? tenantId, TUserId userId, CancellationToken token = default); - } -} diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/IUserStoreFactory.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/IUserStoreFactory.cs deleted file mode 100644 index 23f85519..00000000 --- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/IUserStoreFactory.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace CodeBeam.UltimateAuth.Core.Abstractions -{ - /// - /// Provides a factory abstraction for creating tenant-scoped user store - /// instances used for retrieving basic user information required by - /// UltimateAuth authentication services. - /// - public interface IUserStoreFactory - { - /// - /// Creates and returns a user store instance for the specified user ID type within the given tenant context. - /// - /// The type used to uniquely identify users. - /// - /// The tenant identifier for multi-tenant environments, or null - /// in single-tenant deployments. - /// - /// - /// An implementation capable of user lookup and security metadata retrieval. - /// - /// - /// Thrown if no user store implementation has been registered for the given user ID type. - /// - IUAuthUserStore Create(string tenantId); - } -} diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Validators/IJwtValidator.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Validators/IJwtValidator.cs index f342e7c6..404422a8 100644 --- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Validators/IJwtValidator.cs +++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Validators/IJwtValidator.cs @@ -1,13 +1,12 @@ using CodeBeam.UltimateAuth.Core.Contracts; -namespace CodeBeam.UltimateAuth.Core.Abstractions +namespace CodeBeam.UltimateAuth.Core.Abstractions; + +/// +/// Validates access tokens (JWT or opaque) and resolves +/// the authenticated user context. +/// +public interface IJwtValidator { - /// - /// Validates access tokens (JWT or opaque) and resolves - /// the authenticated user context. - /// - public interface IJwtValidator - { - Task> ValidateAsync(string token, CancellationToken ct = default); - } + Task> ValidateAsync(string token, CancellationToken ct = default); } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Authority/AccessContext.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Authority/AccessContext.cs index 238390b5..1d79bbe4 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Authority/AccessContext.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Authority/AccessContext.cs @@ -1,55 +1,54 @@ using CodeBeam.UltimateAuth.Core.Domain; using System.Collections; -namespace CodeBeam.UltimateAuth.Core.Contracts +namespace CodeBeam.UltimateAuth.Core.Contracts; + +public sealed class AccessContext { - public sealed class AccessContext + // Actor + public UserKey? ActorUserKey { get; init; } + public string? ActorTenantId { get; init; } + public bool IsAuthenticated { get; init; } + public bool IsSystemActor { get; init; } + + // Target + public string? Resource { get; init; } + public string? ResourceId { get; init; } + public string? ResourceTenantId { get; init; } + + public string Action { get; init; } = default!; + public IReadOnlyDictionary Attributes { get; init; } = EmptyAttributes.Instance; + + public bool IsCrossTenant => ActorTenantId != null && ResourceTenantId != null && !string.Equals(ActorTenantId, ResourceTenantId, StringComparison.Ordinal); + public bool IsSelfAction => ActorUserKey != null && ResourceId != null && string.Equals(ActorUserKey.Value, ResourceId, StringComparison.Ordinal); + public bool HasActor => ActorUserKey != null; + public bool HasTarget => ResourceId != null; + + public UserKey GetTargetUserKey() { - // Actor - public UserKey? ActorUserKey { get; init; } - public string? ActorTenantId { get; init; } - public bool IsAuthenticated { get; init; } - public bool IsSystemActor { get; init; } - - // Target - public string? Resource { get; init; } - public string? ResourceId { get; init; } - public string? ResourceTenantId { get; init; } - - public string Action { get; init; } = default!; - public IReadOnlyDictionary Attributes { get; init; } = EmptyAttributes.Instance; - - public bool IsCrossTenant => ActorTenantId != null && ResourceTenantId != null && !string.Equals(ActorTenantId, ResourceTenantId, StringComparison.Ordinal); - public bool IsSelfAction => ActorUserKey != null && ResourceId != null && string.Equals(ActorUserKey.Value, ResourceId, StringComparison.Ordinal); - public bool HasActor => ActorUserKey != null; - public bool HasTarget => ResourceId != null; - - public UserKey GetTargetUserKey() - { - if (ResourceId is null) - throw new InvalidOperationException("Target user is not specified."); - - return UserKey.Parse(ResourceId, null); - } + if (ResourceId is null) + throw new InvalidOperationException("Target user is not specified."); + + return UserKey.Parse(ResourceId, null); } +} + +internal sealed class EmptyAttributes : IReadOnlyDictionary +{ + public static readonly EmptyAttributes Instance = new(); + + private EmptyAttributes() { } - internal sealed class EmptyAttributes : IReadOnlyDictionary + public IEnumerable Keys => Array.Empty(); + public IEnumerable Values => Array.Empty(); + public int Count => 0; + public object this[string key] => throw new KeyNotFoundException(); + public bool ContainsKey(string key) => false; + public bool TryGetValue(string key, out object value) { - public static readonly EmptyAttributes Instance = new(); - - private EmptyAttributes() { } - - public IEnumerable Keys => Array.Empty(); - public IEnumerable Values => Array.Empty(); - public int Count => 0; - public object this[string key] => throw new KeyNotFoundException(); - public bool ContainsKey(string key) => false; - public bool TryGetValue(string key, out object value) - { - value = default!; - return false; - } - public IEnumerator> GetEnumerator() => Enumerable.Empty>().GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + value = default!; + return false; } + public IEnumerator> GetEnumerator() => Enumerable.Empty>().GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Authority/AccessDecision.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Authority/AccessDecision.cs index 2320615a..fa89076e 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Authority/AccessDecision.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Authority/AccessDecision.cs @@ -1,40 +1,36 @@ -namespace CodeBeam.UltimateAuth.Core.Contracts +namespace CodeBeam.UltimateAuth.Core.Contracts; + +public sealed record AccessDecision { - public sealed record AccessDecision - { - public bool IsAllowed { get; } - public bool RequiresReauthentication { get; } - public string? DenyReason { get; } + public bool IsAllowed { get; } + public bool RequiresReauthentication { get; } + public string? DenyReason { get; } - private AccessDecision( - bool isAllowed, - bool requiresReauthentication, - string? denyReason) - { - IsAllowed = isAllowed; - RequiresReauthentication = requiresReauthentication; - DenyReason = denyReason; - } + private AccessDecision(bool isAllowed, bool requiresReauthentication, string? denyReason) + { + IsAllowed = isAllowed; + RequiresReauthentication = requiresReauthentication; + DenyReason = denyReason; + } - public static AccessDecision Allow() - => new( - isAllowed: true, - requiresReauthentication: false, - denyReason: null); + public static AccessDecision Allow() + => new( + isAllowed: true, + requiresReauthentication: false, + denyReason: null); - public static AccessDecision Deny(string reason) - => new( - isAllowed: false, - requiresReauthentication: false, - denyReason: reason); + public static AccessDecision Deny(string reason) + => new( + isAllowed: false, + requiresReauthentication: false, + denyReason: reason); - public static AccessDecision ReauthenticationRequired(string? reason = null) - => new( - isAllowed: false, - requiresReauthentication: true, - denyReason: reason); + public static AccessDecision ReauthenticationRequired(string? reason = null) + => new( + isAllowed: false, + requiresReauthentication: true, + denyReason: reason); - public bool IsDenied => - !IsAllowed && !RequiresReauthentication; - } + public bool IsDenied => + !IsAllowed && !RequiresReauthentication; } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Authority/AccessDecisionResult.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Authority/AccessDecisionResult.cs index e157c940..a1e2002b 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Authority/AccessDecisionResult.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Authority/AccessDecisionResult.cs @@ -1,29 +1,26 @@ -namespace CodeBeam.UltimateAuth.Core.Contracts -{ - public sealed class AccessDecisionResult - { - public AuthorizationDecision Decision { get; } - public string? Reason { get; } +namespace CodeBeam.UltimateAuth.Core.Contracts; - private AccessDecisionResult(AuthorizationDecision decision, string? reason) - { - Decision = decision; - Reason = reason; - } +public sealed class AccessDecisionResult +{ + public AuthorizationDecision Decision { get; } + public string? Reason { get; } - public static AccessDecisionResult Allow() - => new(AuthorizationDecision.Allow, null); + private AccessDecisionResult(AuthorizationDecision decision, string? reason) + { + Decision = decision; + Reason = reason; + } - public static AccessDecisionResult Deny(string reason) - => new(AuthorizationDecision.Deny, reason); + public static AccessDecisionResult Allow() + => new(AuthorizationDecision.Allow, null); - public static AccessDecisionResult Challenge(string reason) - => new(AuthorizationDecision.Challenge, reason); + public static AccessDecisionResult Deny(string reason) + => new(AuthorizationDecision.Deny, reason); - // Developer happiness helpers - public bool IsAllowed => Decision == AuthorizationDecision.Allow; - public bool IsDenied => Decision == AuthorizationDecision.Deny; - public bool RequiresChallenge => Decision == AuthorizationDecision.Challenge; - } + public static AccessDecisionResult Challenge(string reason) + => new(AuthorizationDecision.Challenge, reason); + public bool IsAllowed => Decision == AuthorizationDecision.Allow; + public bool IsDenied => Decision == AuthorizationDecision.Deny; + public bool RequiresChallenge => Decision == AuthorizationDecision.Challenge; } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Authority/AuthContext.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Authority/AuthContext.cs index d31c6e24..6f9a6755 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Authority/AuthContext.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Authority/AuthContext.cs @@ -1,19 +1,18 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Core.Contracts +namespace CodeBeam.UltimateAuth.Core.Contracts; + +public sealed record AuthContext { - public sealed record AuthContext - { - public string? TenantId { get; init; } + public string? TenantId { get; init; } - public AuthOperation Operation { get; init; } + public AuthOperation Operation { get; init; } - public UAuthMode Mode { get; init; } + public UAuthMode Mode { get; init; } - public SessionSecurityContext? Session { get; init; } + public SessionSecurityContext? Session { get; init; } - public required DeviceContext Device { get; init; } + public required DeviceContext Device { get; init; } - public DateTimeOffset At { get; init; } - } + public DateTimeOffset At { get; init; } } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Authority/AuthOperation.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Authority/AuthOperation.cs index 8f41f0d7..9f886535 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Authority/AuthOperation.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Authority/AuthOperation.cs @@ -1,12 +1,11 @@ -namespace CodeBeam.UltimateAuth.Core.Contracts +namespace CodeBeam.UltimateAuth.Core.Contracts; + +public enum AuthOperation { - public enum AuthOperation - { - Login, - Access, - Refresh, - Revoke, - Logout, - System - } + Login, + Access, + Refresh, + Revoke, + Logout, + System } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Authority/AuthorizationDecision.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Authority/AuthorizationDecision.cs index 80d71022..5f329623 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Authority/AuthorizationDecision.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Authority/AuthorizationDecision.cs @@ -1,10 +1,8 @@ -namespace CodeBeam.UltimateAuth.Core.Contracts -{ - public enum AuthorizationDecision - { - Allow, - Deny, - Challenge - } +namespace CodeBeam.UltimateAuth.Core.Contracts; +public enum AuthorizationDecision +{ + Allow, + Deny, + Challenge } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Authority/DeviceMismatchBehavior.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Authority/DeviceMismatchBehavior.cs index a4cd82ad..46d8241a 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Authority/DeviceMismatchBehavior.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Authority/DeviceMismatchBehavior.cs @@ -1,10 +1,8 @@ -namespace CodeBeam.UltimateAuth.Core.Contracts -{ - public enum DeviceMismatchBehavior - { - Reject, // 401 - Allow, // Accept session - AllowAndRebind // Accept and update device info - } +namespace CodeBeam.UltimateAuth.Core.Contracts; +public enum DeviceMismatchBehavior +{ + Reject, + Allow, + AllowAndRebind } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Common/DeleteMode.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Common/DeleteMode.cs index e28fa7b8..5063bda6 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Common/DeleteMode.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Common/DeleteMode.cs @@ -1,8 +1,7 @@ -namespace CodeBeam.UltimateAuth.Core.Contracts +namespace CodeBeam.UltimateAuth.Core.Contracts; + +public enum DeleteMode { - public enum DeleteMode - { - Soft, - Hard - } + Soft, + Hard } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Common/PagedResult.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Common/PagedResult.cs index 404b62b8..e3b92ad3 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Common/PagedResult.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Common/PagedResult.cs @@ -1,14 +1,13 @@ -namespace CodeBeam.UltimateAuth.Core.Contracts +namespace CodeBeam.UltimateAuth.Core.Contracts; + +public sealed class PagedResult { - public sealed class PagedResult - { - public IReadOnlyList Items { get; } - public int TotalCount { get; } + public IReadOnlyList Items { get; } + public int TotalCount { get; } - public PagedResult(IReadOnlyList items, int totalCount) - { - Items = items; - TotalCount = totalCount; - } + public PagedResult(IReadOnlyList items, int totalCount) + { + Items = items; + TotalCount = totalCount; } } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Common/UAuthResult.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Common/UAuthResult.cs index 2437c850..31f83adb 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Common/UAuthResult.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Common/UAuthResult.cs @@ -1,20 +1,18 @@ -namespace CodeBeam.UltimateAuth.Core.Contracts -{ - public class UAuthResult - { - public bool Ok { get; init; } - public int Status { get; init; } +namespace CodeBeam.UltimateAuth.Core.Contracts; - public string? Error { get; init; } - public string? ErrorCode { get; init; } +public class UAuthResult +{ + public bool Ok { get; init; } + public int Status { get; init; } - public bool IsUnauthorized => Status == 401; - public bool IsForbidden => Status == 403; - } + public string? Error { get; init; } + public string? ErrorCode { get; init; } - public sealed class UAuthResult : UAuthResult - { - public T? Value { get; init; } - } + public bool IsUnauthorized => Status == 401; + public bool IsForbidden => Status == 403; +} +public sealed class UAuthResult : UAuthResult +{ + public T? Value { get; init; } } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Login/ExternalLoginRequest.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Login/ExternalLoginRequest.cs index 6126e7d7..10c1c4c6 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Login/ExternalLoginRequest.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Login/ExternalLoginRequest.cs @@ -1,11 +1,9 @@ -namespace CodeBeam.UltimateAuth.Core.Contracts -{ - public sealed record ExternalLoginRequest - { - public string? TenantId { get; init; } - public string Provider { get; init; } = default!; - public string ExternalToken { get; init; } = default!; - public string? DeviceId { get; init; } - } +namespace CodeBeam.UltimateAuth.Core.Contracts; +public sealed record ExternalLoginRequest +{ + public string? TenantId { get; init; } + public string Provider { get; init; } = default!; + public string ExternalToken { get; init; } = default!; + public string? DeviceId { get; init; } } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Login/LoginContinuation.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Login/LoginContinuation.cs index ec5fb02b..7d39fc7d 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Login/LoginContinuation.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Login/LoginContinuation.cs @@ -1,20 +1,19 @@ -namespace CodeBeam.UltimateAuth.Core.Contracts +namespace CodeBeam.UltimateAuth.Core.Contracts; + +public sealed record LoginContinuation { - public sealed record LoginContinuation - { - /// - /// Gets the type of login continuation required. - /// - public LoginContinuationType Type { get; init; } + /// + /// Gets the type of login continuation required. + /// + public LoginContinuationType Type { get; init; } - /// - /// Opaque continuation token used to resume the login flow. - /// - public string ContinuationToken { get; init; } = default!; + /// + /// Opaque continuation token used to resume the login flow. + /// + public string ContinuationToken { get; init; } = default!; - /// - /// Optional hint for UX (e.g. "Enter MFA code", "Verify device"). - /// - public string? Hint { get; init; } - } + /// + /// Optional hint for UX (e.g. "Enter MFA code", "Verify device"). + /// + public string? Hint { get; init; } } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Login/LoginContinuationType.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Login/LoginContinuationType.cs index 662fbef9..d8d953d3 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Login/LoginContinuationType.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Login/LoginContinuationType.cs @@ -1,9 +1,8 @@ -namespace CodeBeam.UltimateAuth.Core.Contracts +namespace CodeBeam.UltimateAuth.Core.Contracts; + +public enum LoginContinuationType { - public enum LoginContinuationType - { - Mfa, - Pkce, - External - } + Mfa, + Pkce, + External } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Login/LoginRequest.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Login/LoginRequest.cs index 3ff02bd0..0c31e6c7 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Login/LoginRequest.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Login/LoginRequest.cs @@ -1,23 +1,22 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Core.Contracts +namespace CodeBeam.UltimateAuth.Core.Contracts; + +public sealed record LoginRequest { - public sealed record LoginRequest - { - public string? TenantId { get; init; } - public string Identifier { get; init; } = default!; // username, email etc. - public string Secret { get; init; } = default!; // password - public DateTimeOffset? At { get; init; } - public required DeviceContext Device { get; init; } - public IReadOnlyDictionary? Metadata { get; init; } + public string? TenantId { get; init; } + public string Identifier { get; init; } = default!; + public string Secret { get; init; } = default!; + public DateTimeOffset? At { get; init; } + public required DeviceContext Device { get; init; } + public IReadOnlyDictionary? Metadata { get; init; } - /// - /// Hint to request access/refresh tokens when the server mode supports it. - /// Server policy may still ignore this. - /// - public bool RequestTokens { get; init; } = true; + /// + /// Hint to request access/refresh tokens when the server mode supports it. + /// Server policy may still ignore this. + /// + public bool RequestTokens { get; init; } = true; - // Optional - public SessionChainId? ChainId { get; init; } - } + // Optional + public SessionChainId? ChainId { get; init; } } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Login/LoginResult.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Login/LoginResult.cs index 8324739e..31548156 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Login/LoginResult.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Login/LoginResult.cs @@ -1,44 +1,41 @@ -using CodeBeam.UltimateAuth.Core.Contracts; -using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Core.Contracts +namespace CodeBeam.UltimateAuth.Core.Contracts; + +public sealed record LoginResult { - public sealed record LoginResult - { - public LoginStatus Status { get; init; } - public AuthSessionId? SessionId { get; init; } - public AccessToken? AccessToken { get; init; } - public RefreshToken? RefreshToken { get; init; } - public LoginContinuation? Continuation { get; init; } - public AuthFailureReason? FailureReason { get; init; } + public LoginStatus Status { get; init; } + public AuthSessionId? SessionId { get; init; } + public AccessToken? AccessToken { get; init; } + public RefreshToken? RefreshToken { get; init; } + public LoginContinuation? Continuation { get; init; } + public AuthFailureReason? FailureReason { get; init; } - // Helpers - public bool IsSuccess => Status == LoginStatus.Success; - public bool RequiresContinuation => Continuation is not null; - public bool RequiresMfa => Continuation?.Type == LoginContinuationType.Mfa; - public bool RequiresPkce => Continuation?.Type == LoginContinuationType.Pkce; + public bool IsSuccess => Status == LoginStatus.Success; + public bool RequiresContinuation => Continuation is not null; + public bool RequiresMfa => Continuation?.Type == LoginContinuationType.Mfa; + public bool RequiresPkce => Continuation?.Type == LoginContinuationType.Pkce; - public static LoginResult Failed(AuthFailureReason? reason = null) - => new() - { - Status = LoginStatus.Failed, - FailureReason = reason - }; + public static LoginResult Failed(AuthFailureReason? reason = null) + => new() + { + Status = LoginStatus.Failed, + FailureReason = reason + }; - public static LoginResult Success(AuthSessionId sessionId, AuthTokens? tokens = null) - => new() - { - Status = LoginStatus.Success, - SessionId = sessionId, - AccessToken = tokens?.AccessToken, - RefreshToken = tokens?.RefreshToken - }; + public static LoginResult Success(AuthSessionId sessionId, AuthTokens? tokens = null) + => new() + { + Status = LoginStatus.Success, + SessionId = sessionId, + AccessToken = tokens?.AccessToken, + RefreshToken = tokens?.RefreshToken + }; - public static LoginResult Continue(LoginContinuation continuation) - => new() - { - Status = LoginStatus.RequiresContinuation, - Continuation = continuation - }; - } + public static LoginResult Continue(LoginContinuation continuation) + => new() + { + Status = LoginStatus.RequiresContinuation, + Continuation = continuation + }; } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Login/LoginStatus.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Login/LoginStatus.cs index 94a3902c..95a03a12 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Login/LoginStatus.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Login/LoginStatus.cs @@ -1,9 +1,8 @@ -namespace CodeBeam.UltimateAuth.Core.Contracts +namespace CodeBeam.UltimateAuth.Core.Contracts; + +public enum LoginStatus { - public enum LoginStatus - { - Success, - RequiresContinuation, - Failed - } + Success, + RequiresContinuation, + Failed } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Login/ReauthRequest.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Login/ReauthRequest.cs index b1d25650..e1736304 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Login/ReauthRequest.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Login/ReauthRequest.cs @@ -1,11 +1,10 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Core.Contracts +namespace CodeBeam.UltimateAuth.Core.Contracts; + +public sealed record ReauthRequest { - public sealed record ReauthRequest - { - public string? TenantId { get; init; } - public AuthSessionId SessionId { get; init; } - public string Secret { get; init; } = default!; - } + public string? TenantId { get; init; } + public AuthSessionId SessionId { get; init; } + public string Secret { get; init; } = default!; } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Login/ReauthResult.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Login/ReauthResult.cs index d14eb108..a047ff1d 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Login/ReauthResult.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Login/ReauthResult.cs @@ -1,7 +1,6 @@ -namespace CodeBeam.UltimateAuth.Core.Contracts +namespace CodeBeam.UltimateAuth.Core.Contracts; + +public sealed record ReauthResult { - public sealed record ReauthResult - { - public bool Success { get; init; } - } + public bool Success { get; init; } } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Login/UAuthLoginType.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Login/UAuthLoginType.cs index 4263a08f..2395ccb4 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Login/UAuthLoginType.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Login/UAuthLoginType.cs @@ -1,8 +1,7 @@ -namespace CodeBeam.UltimateAuth.Core.Contracts +namespace CodeBeam.UltimateAuth.Core.Contracts; + +public enum UAuthLoginType { - public enum UAuthLoginType - { - Password, // /auth/login - Pkce // /auth/pkce/complete - } + Password, // /auth/login + Pkce // /auth/pkce/complete } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Logout/LogoutAllRequest.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Logout/LogoutAllRequest.cs index 2aa6b6af..08fd5301 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Logout/LogoutAllRequest.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Logout/LogoutAllRequest.cs @@ -1,23 +1,21 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Core.Contracts -{ - public sealed class LogoutAllRequest - { - public string? TenantId { get; init; } +namespace CodeBeam.UltimateAuth.Core.Contracts; - /// - /// The current session initiating the logout-all operation. - /// Used to resolve the active chain when ExceptCurrent is true. - /// - public AuthSessionId? CurrentSessionId { get; init; } +public sealed class LogoutAllRequest +{ + public string? TenantId { get; init; } - /// - /// If true, the current session will NOT be revoked. - /// - public bool ExceptCurrent { get; init; } + /// + /// The current session initiating the logout-all operation. + /// Used to resolve the active chain when ExceptCurrent is true. + /// + public AuthSessionId? CurrentSessionId { get; init; } - public DateTimeOffset? At { get; init; } - } + /// + /// If true, the current session will NOT be revoked. + /// + public bool ExceptCurrent { get; init; } + public DateTimeOffset? At { get; init; } } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Logout/LogoutRequest.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Logout/LogoutRequest.cs index 7229f0ad..7aebff41 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Logout/LogoutRequest.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Logout/LogoutRequest.cs @@ -1,12 +1,10 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Core.Contracts -{ - public sealed record LogoutRequest - { - public string? TenantId { get; init; } - public AuthSessionId SessionId { get; init; } +namespace CodeBeam.UltimateAuth.Core.Contracts; - public DateTimeOffset? At { get; init; } - } +public sealed record LogoutRequest +{ + public string? TenantId { get; init; } + public AuthSessionId SessionId { get; init; } + public DateTimeOffset? At { get; init; } } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Mfa/BeginMfaRequest.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Mfa/BeginMfaRequest.cs index 86af91a4..38f945b1 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Mfa/BeginMfaRequest.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Mfa/BeginMfaRequest.cs @@ -1,7 +1,6 @@ -namespace CodeBeam.UltimateAuth.Core.Contracts +namespace CodeBeam.UltimateAuth.Core.Contracts; + +public sealed record BeginMfaRequest { - public sealed record BeginMfaRequest - { - public string MfaToken { get; init; } = default!; - } + public string MfaToken { get; init; } = default!; } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Mfa/CompleteMfaRequest.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Mfa/CompleteMfaRequest.cs index 5d575d0b..abf719ff 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Mfa/CompleteMfaRequest.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Mfa/CompleteMfaRequest.cs @@ -1,8 +1,7 @@ -namespace CodeBeam.UltimateAuth.Core.Contracts +namespace CodeBeam.UltimateAuth.Core.Contracts; + +public sealed record CompleteMfaRequest { - public sealed record CompleteMfaRequest - { - public string ChallengeId { get; init; } = default!; - public string Code { get; init; } = default!; - } + public string ChallengeId { get; init; } = default!; + public string Code { get; init; } = default!; } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Mfa/MfaChallengeResult.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Mfa/MfaChallengeResult.cs index 9bb085c8..f12ccedd 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Mfa/MfaChallengeResult.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Mfa/MfaChallengeResult.cs @@ -1,8 +1,7 @@ -namespace CodeBeam.UltimateAuth.Core.Contracts +namespace CodeBeam.UltimateAuth.Core.Contracts; + +public sealed record MfaChallengeResult { - public sealed record MfaChallengeResult - { - public string ChallengeId { get; init; } = default!; - public string Method { get; init; } = default!; // totp, sms, email etc. - } + public string ChallengeId { get; init; } = default!; + public string Method { get; init; } = default!; // totp, sms, email etc. } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Pkce/PkceCompleteRequest.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Pkce/PkceCompleteRequest.cs index 12a10364..d04b6eca 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Pkce/PkceCompleteRequest.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Pkce/PkceCompleteRequest.cs @@ -1,11 +1,10 @@ -namespace CodeBeam.UltimateAuth.Core.Contracts +namespace CodeBeam.UltimateAuth.Core.Contracts; + +internal sealed class PkceCompleteRequest { - internal sealed class PkceCompleteRequest - { - public string AuthorizationCode { get; init; } = default!; - public string CodeVerifier { get; init; } = default!; - public string Identifier { get; init; } = default!; - public string Secret { get; init; } = default!; - public string ReturnUrl { get; init; } = default!; - } + public string AuthorizationCode { get; init; } = default!; + public string CodeVerifier { get; init; } = default!; + public string Identifier { get; init; } = default!; + public string Secret { get; init; } = default!; + public string ReturnUrl { get; init; } = default!; } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Refresh/RefreshFlowRequest.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Refresh/RefreshFlowRequest.cs index 21b180eb..a6120541 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Refresh/RefreshFlowRequest.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Refresh/RefreshFlowRequest.cs @@ -1,13 +1,12 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Core.Contracts +namespace CodeBeam.UltimateAuth.Core.Contracts; + +public sealed class RefreshFlowRequest { - public sealed class RefreshFlowRequest - { - public AuthSessionId? SessionId { get; init; } - public string? RefreshToken { get; init; } - public required DeviceContext Device { get; init; } - public DateTimeOffset Now { get; init; } - public SessionTouchMode TouchMode { get; init; } = SessionTouchMode.IfNeeded; - } + public AuthSessionId? SessionId { get; init; } + public string? RefreshToken { get; init; } + public required DeviceContext Device { get; init; } + public DateTimeOffset Now { get; init; } + public SessionTouchMode TouchMode { get; init; } = SessionTouchMode.IfNeeded; } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Refresh/RefreshFlowResult.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Refresh/RefreshFlowResult.cs index 7c1f26eb..51b81396 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Refresh/RefreshFlowResult.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Refresh/RefreshFlowResult.cs @@ -1,40 +1,39 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Core.Contracts +namespace CodeBeam.UltimateAuth.Core.Contracts; + +public sealed class RefreshFlowResult { - public sealed class RefreshFlowResult - { - public bool Succeeded { get; init; } - public RefreshOutcome Outcome { get; init; } + public bool Succeeded { get; init; } + public RefreshOutcome Outcome { get; init; } - public AuthSessionId? SessionId { get; init; } - public AccessToken? AccessToken { get; init; } - public RefreshToken? RefreshToken { get; init; } + public AuthSessionId? SessionId { get; init; } + public AccessToken? AccessToken { get; init; } + public RefreshToken? RefreshToken { get; init; } - public static RefreshFlowResult ReauthRequired() + public static RefreshFlowResult ReauthRequired() + { + return new RefreshFlowResult { - return new RefreshFlowResult - { - Succeeded = false, - Outcome = RefreshOutcome.ReauthRequired - }; - } + Succeeded = false, + Outcome = RefreshOutcome.ReauthRequired + }; + } - public static RefreshFlowResult Success( - RefreshOutcome outcome, - AuthSessionId? sessionId = null, - AccessToken? accessToken = null, - RefreshToken? refreshToken = null) + public static RefreshFlowResult Success( + RefreshOutcome outcome, + AuthSessionId? sessionId = null, + AccessToken? accessToken = null, + RefreshToken? refreshToken = null) + { + return new RefreshFlowResult { - return new RefreshFlowResult - { - Succeeded = true, - Outcome = outcome, - SessionId = sessionId, - AccessToken = accessToken, - RefreshToken = refreshToken - }; - } - + Succeeded = true, + Outcome = outcome, + SessionId = sessionId, + AccessToken = accessToken, + RefreshToken = refreshToken + }; } + } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Refresh/RefreshStrategy.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Refresh/RefreshStrategy.cs index e4352d05..3c22c330 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Refresh/RefreshStrategy.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Refresh/RefreshStrategy.cs @@ -1,11 +1,10 @@ -namespace CodeBeam.UltimateAuth.Core.Contracts +namespace CodeBeam.UltimateAuth.Core.Contracts; + +public enum RefreshStrategy { - public enum RefreshStrategy - { - NotSupported, - SessionOnly, // PureOpaque - TokenOnly, // PureJwt - TokenWithSessionCheck, // SemiHybrid - SessionAndToken // Hybrid - } + NotSupported, + SessionOnly, // PureOpaque + TokenOnly, // PureJwt + TokenWithSessionCheck, // SemiHybrid + SessionAndToken // Hybrid } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Refresh/RefreshTokenPersistence.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Refresh/RefreshTokenPersistence.cs index dc5891cf..a9d308d0 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Refresh/RefreshTokenPersistence.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Refresh/RefreshTokenPersistence.cs @@ -1,18 +1,17 @@ -namespace CodeBeam.UltimateAuth.Core.Contracts +namespace CodeBeam.UltimateAuth.Core.Contracts; + +public enum RefreshTokenPersistence { - public enum RefreshTokenPersistence - { - /// - /// Refresh token store'a yazılır. - /// Login, first-issue gibi normal akışlar için. - /// - Persist, + /// + /// Refresh token store'a yazılır. + /// Login, first-issue gibi normal akışlar için. + /// + Persist, - /// - /// Refresh token store'a yazılmaz. - /// Rotation gibi özel akışlarda, - /// caller tarafından kontrol edilir. - /// - DoNotPersist - } + /// + /// Refresh token store'a yazılmaz. + /// Rotation gibi özel akışlarda, + /// caller tarafından kontrol edilir. + /// + DoNotPersist } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Refresh/RefreshTokenValidationContext.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Refresh/RefreshTokenValidationContext.cs index 6b9375de..7cd62cc2 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Refresh/RefreshTokenValidationContext.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Refresh/RefreshTokenValidationContext.cs @@ -1,15 +1,13 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Core.Contracts +namespace CodeBeam.UltimateAuth.Core.Contracts; + +public sealed record RefreshTokenValidationContext { - public sealed record RefreshTokenValidationContext - { - public string? TenantId { get; init; } - public string RefreshToken { get; init; } = default!; - public DateTimeOffset Now { get; init; } + public string? TenantId { get; init; } + public string RefreshToken { get; init; } = default!; + public DateTimeOffset Now { get; init; } - // For Hybrid & Advanced - public required DeviceContext Device { get; init; } - public AuthSessionId? ExpectedSessionId { get; init; } - } + public required DeviceContext Device { get; init; } + public AuthSessionId? ExpectedSessionId { get; init; } } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/AuthStateSnapshot.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/AuthStateSnapshot.cs index 704398a6..c56c36b3 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/AuthStateSnapshot.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/AuthStateSnapshot.cs @@ -1,16 +1,14 @@ using CodeBeam.UltimateAuth.Core.Domain; -using System.Security.Claims; -namespace CodeBeam.UltimateAuth.Core.Contracts +namespace CodeBeam.UltimateAuth.Core.Contracts; + +public sealed record AuthStateSnapshot { - public sealed record AuthStateSnapshot - { - // It's not UserId type - public string? UserId { get; init; } - public string? TenantId { get; init; } + // It's not UserId type + public string? UserId { get; init; } + public string? TenantId { get; init; } - public ClaimsSnapshot Claims { get; init; } = ClaimsSnapshot.Empty; + public ClaimsSnapshot Claims { get; init; } = ClaimsSnapshot.Empty; - public DateTimeOffset? AuthenticatedAt { get; init; } - } + public DateTimeOffset? AuthenticatedAt { get; init; } } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/AuthValidationResult.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/AuthValidationResult.cs index 1cf3e36b..0baf518b 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/AuthValidationResult.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/AuthValidationResult.cs @@ -1,26 +1,25 @@ -namespace CodeBeam.UltimateAuth.Core.Contracts +namespace CodeBeam.UltimateAuth.Core.Contracts; + +public sealed record AuthValidationResult { - public sealed record AuthValidationResult - { - public bool IsValid { get; init; } - public string? State { get; init; } - public int? RemainingAttempts { get; init; } + public bool IsValid { get; init; } + public string? State { get; init; } + public int? RemainingAttempts { get; init; } - public AuthStateSnapshot? Snapshot { get; init; } + public AuthStateSnapshot? Snapshot { get; init; } - public static AuthValidationResult Valid(AuthStateSnapshot? snapshot = null) + public static AuthValidationResult Valid(AuthStateSnapshot? snapshot = null) + => new() + { + IsValid = true, + State = "active", + Snapshot = snapshot + }; + + public static AuthValidationResult Invalid(string state) => new() { - IsValid = true, - State = "active", - Snapshot = snapshot + IsValid = false, + State = state }; - - public static AuthValidationResult Invalid(string state) - => new() - { - IsValid = false, - State = state - }; - } } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/AuthenticatedSessionContext.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/AuthenticatedSessionContext.cs index 08890b7a..2d39adf7 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/AuthenticatedSessionContext.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/AuthenticatedSessionContext.cs @@ -1,31 +1,30 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Core.Contracts +namespace CodeBeam.UltimateAuth.Core.Contracts; + +/// +/// Represents the context in which a session is issued +/// (login, refresh, reauthentication). +/// +public sealed class AuthenticatedSessionContext { + public string? TenantId { get; init; } + public required UserKey UserKey { get; init; } + public required DeviceContext Device { get; init; } + public DateTimeOffset Now { get; init; } + public ClaimsSnapshot? Claims { get; init; } + public required SessionMetadata Metadata { get; init; } + /// - /// Represents the context in which a session is issued - /// (login, refresh, reauthentication). + /// Optional chain identifier. + /// If null, a new chain will be created. + /// If provided, session will be issued under the existing chain. /// - public sealed class AuthenticatedSessionContext - { - public string? TenantId { get; init; } - public required UserKey UserKey { get; init; } - public required DeviceContext Device { get; init; } - public DateTimeOffset Now { get; init; } - public ClaimsSnapshot? Claims { get; init; } - public required SessionMetadata Metadata { get; init; } - - /// - /// Optional chain identifier. - /// If null, a new chain will be created. - /// If provided, session will be issued under the existing chain. - /// - public SessionChainId? ChainId { get; init; } + public SessionChainId? ChainId { get; init; } - /// - /// Indicates that authentication has already been completed. - /// This context MUST NOT be constructed from raw credentials. - /// - public bool IsAuthenticated { get; init; } = true; - } + /// + /// Indicates that authentication has already been completed. + /// This context MUST NOT be constructed from raw credentials. + /// + public bool IsAuthenticated { get; init; } = true; } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/IssuedSession.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/IssuedSession.cs index 0d1622de..aecbe0f9 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/IssuedSession.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/IssuedSession.cs @@ -1,27 +1,25 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Core.Contracts +namespace CodeBeam.UltimateAuth.Core.Contracts; + +/// +/// Represents the result of a session issuance operation. +/// +public sealed class IssuedSession { /// - /// Represents the result of a session issuance operation. + /// The issued domain session. /// - public sealed class IssuedSession - { - /// - /// The issued domain session. - /// - public required ISession Session { get; init; } - - /// - /// Opaque session identifier returned to the client. - /// - public required string OpaqueSessionId { get; init; } + public required ISession Session { get; init; } - /// - /// Indicates whether this issuance is metadata-only - /// (used in SemiHybrid mode). - /// - public bool IsMetadataOnly { get; init; } - } + /// + /// Opaque session identifier returned to the client. + /// + public required string OpaqueSessionId { get; init; } + /// + /// Indicates whether this issuance is metadata-only + /// (used in SemiHybrid mode). + /// + public bool IsMetadataOnly { get; init; } } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/ResolvedRefreshSession.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/ResolvedRefreshSession.cs index ece8d802..513e54ec 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/ResolvedRefreshSession.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/ResolvedRefreshSession.cs @@ -1,38 +1,37 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Core.Contracts +namespace CodeBeam.UltimateAuth.Core.Contracts; + +public sealed record ResolvedRefreshSession { - public sealed record ResolvedRefreshSession - { - public bool IsValid { get; init; } - public bool IsReuseDetected { get; init; } + public bool IsValid { get; init; } + public bool IsReuseDetected { get; init; } - public ISession? Session { get; init; } - public ISessionChain? Chain { get; init; } + public ISession? Session { get; init; } + public ISessionChain? Chain { get; init; } - private ResolvedRefreshSession() { } + private ResolvedRefreshSession() { } - public static ResolvedRefreshSession Invalid() - => new() - { - IsValid = false - }; + public static ResolvedRefreshSession Invalid() + => new() + { + IsValid = false + }; - public static ResolvedRefreshSession Reused() - => new() - { - IsValid = false, - IsReuseDetected = true - }; + public static ResolvedRefreshSession Reused() + => new() + { + IsValid = false, + IsReuseDetected = true + }; - public static ResolvedRefreshSession Valid( - ISession session, - ISessionChain chain) - => new() - { - IsValid = true, - Session = session, - Chain = chain - }; - } + public static ResolvedRefreshSession Valid( + ISession session, + ISessionChain chain) + => new() + { + IsValid = true, + Session = session, + Chain = chain + }; } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionContext.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionContext.cs index 93d5aba9..d56ac757 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionContext.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionContext.cs @@ -1,29 +1,26 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Core.Contracts +namespace CodeBeam.UltimateAuth.Core.Contracts; + +/// +/// Lightweight session context resolved from the incoming request. +/// Does NOT load or validate the session. +/// Used only by middleware and engines as input. +/// +public sealed class SessionContext { - /// - /// Lightweight session context resolved from the incoming request. - /// Does NOT load or validate the session. - /// Used only by middleware and engines as input. - /// - public sealed class SessionContext - { - public AuthSessionId? SessionId { get; } - public string? TenantId { get; } + public AuthSessionId? SessionId { get; } + public string? TenantId { get; } - public bool IsAnonymous => SessionId is null; + public bool IsAnonymous => SessionId is null; - private SessionContext(AuthSessionId? sessionId, string? tenantId) - { - SessionId = sessionId; - TenantId = tenantId; - } + private SessionContext(AuthSessionId? sessionId, string? tenantId) + { + SessionId = sessionId; + TenantId = tenantId; + } - public static SessionContext Anonymous() - => new(null, null); + public static SessionContext Anonymous() => new(null, null); - public static SessionContext FromSessionId(AuthSessionId sessionId, string? tenantId) - => new(sessionId, tenantId); - } + public static SessionContext FromSessionId(AuthSessionId sessionId, string? tenantId) => new(sessionId, tenantId); } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionRefreshRequest.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionRefreshRequest.cs index 9343883f..9c88acd4 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionRefreshRequest.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionRefreshRequest.cs @@ -1,8 +1,7 @@ -namespace CodeBeam.UltimateAuth.Core.Contracts +namespace CodeBeam.UltimateAuth.Core.Contracts; + +public sealed record SessionRefreshRequest { - public sealed record SessionRefreshRequest - { - public string? TenantId { get; init; } - public string RefreshToken { get; init; } = default!; - } + public string? TenantId { get; init; } + public string RefreshToken { get; init; } = default!; } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionRefreshResult.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionRefreshResult.cs index d06a8542..9d5c578b 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionRefreshResult.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionRefreshResult.cs @@ -1,47 +1,46 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Core.Contracts -{ - public sealed record SessionRefreshResult - { - public SessionRefreshStatus Status { get; init; } +namespace CodeBeam.UltimateAuth.Core.Contracts; - public AuthSessionId? SessionId { get; init; } +public sealed record SessionRefreshResult +{ + public SessionRefreshStatus Status { get; init; } - public bool DidTouch { get; init; } + public AuthSessionId? SessionId { get; init; } - public bool IsSuccess => Status == SessionRefreshStatus.Success; - public bool RequiresReauth => Status == SessionRefreshStatus.ReauthRequired; + public bool DidTouch { get; init; } - private SessionRefreshResult() { } + public bool IsSuccess => Status == SessionRefreshStatus.Success; + public bool RequiresReauth => Status == SessionRefreshStatus.ReauthRequired; - public static SessionRefreshResult Success( - AuthSessionId sessionId, - bool didTouch = false) - => new() - { - Status = SessionRefreshStatus.Success, - SessionId = sessionId, - DidTouch = didTouch - }; + private SessionRefreshResult() { } - public static SessionRefreshResult ReauthRequired() + public static SessionRefreshResult Success( + AuthSessionId sessionId, + bool didTouch = false) => new() { - Status = SessionRefreshStatus.ReauthRequired + Status = SessionRefreshStatus.Success, + SessionId = sessionId, + DidTouch = didTouch }; - public static SessionRefreshResult InvalidRequest() - => new() - { - Status = SessionRefreshStatus.InvalidRequest - }; + public static SessionRefreshResult ReauthRequired() + => new() + { + Status = SessionRefreshStatus.ReauthRequired + }; - public static SessionRefreshResult Failed() + public static SessionRefreshResult InvalidRequest() => new() { - Status = SessionRefreshStatus.Failed + Status = SessionRefreshStatus.InvalidRequest }; - } + public static SessionRefreshResult Failed() + => new() + { + Status = SessionRefreshStatus.Failed + }; + } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionResult.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionResult.cs index 8517fccb..a08d673e 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionResult.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionResult.cs @@ -1,40 +1,39 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Core.Contracts +namespace CodeBeam.UltimateAuth.Core.Contracts; + +// TODO: IsNewChain, IsNewRoot flags? +/// +/// Represents the result of a session operation within UltimateAuth, such as +/// login or session refresh. +/// +/// A session operation may produce: +/// - a newly created session, +/// - an updated session chain (rotation), +/// - an updated session root (e.g., after adding a new chain). +/// +/// This wrapper provides a unified model so downstream components — such as +/// token services, event emitters, logging pipelines, or application-level +/// consumers — can easily access all updated authentication structures. +/// +public sealed class SessionResult { - // TODO: IsNewChain, IsNewRoot flags? /// - /// Represents the result of a session operation within UltimateAuth, such as - /// login or session refresh. - /// - /// A session operation may produce: - /// - a newly created session, - /// - an updated session chain (rotation), - /// - an updated session root (e.g., after adding a new chain). - /// - /// This wrapper provides a unified model so downstream components — such as - /// token services, event emitters, logging pipelines, or application-level - /// consumers — can easily access all updated authentication structures. + /// Gets the active session produced by the operation. + /// This is the newest session and the one that should be used when issuing tokens. /// - public sealed class SessionResult - { - /// - /// Gets the active session produced by the operation. - /// This is the newest session and the one that should be used when issuing tokens. - /// - public required ISession Session { get; init; } + public required ISession Session { get; init; } - /// - /// Gets the session chain associated with the session. - /// The chain may be newly created (login) or updated (session rotation). - /// - public required ISessionChain Chain { get; init; } + /// + /// Gets the session chain associated with the session. + /// The chain may be newly created (login) or updated (session rotation). + /// + public required ISessionChain Chain { get; init; } - /// - /// Gets the user's session root. - /// This structure may be updated when new chains are added or when security - /// properties change. - /// - public required ISessionRoot Root { get; init; } - } + /// + /// Gets the user's session root. + /// This structure may be updated when new chains are added or when security + /// properties change. + /// + public required ISessionRoot Root { get; init; } } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionRotationContext.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionRotationContext.cs index 0d23664c..5e96052a 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionRotationContext.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionRotationContext.cs @@ -1,15 +1,14 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Core.Contracts +namespace CodeBeam.UltimateAuth.Core.Contracts; + +public sealed record SessionRotationContext { - public sealed record SessionRotationContext - { - public string? TenantId { get; init; } - public AuthSessionId CurrentSessionId { get; init; } - public UserKey UserKey { get; init; } - public DateTimeOffset Now { get; init; } - public required DeviceContext Device { get; init; } - public ClaimsSnapshot? Claims { get; init; } - public required SessionMetadata Metadata { get; init; } = SessionMetadata.Empty; - } + public string? TenantId { get; init; } + public AuthSessionId CurrentSessionId { get; init; } + public UserKey UserKey { get; init; } + public DateTimeOffset Now { get; init; } + public required DeviceContext Device { get; init; } + public ClaimsSnapshot? Claims { get; init; } + public required SessionMetadata Metadata { get; init; } = SessionMetadata.Empty; } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionSecurityContext.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionSecurityContext.cs index a16d81ca..5e52414e 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionSecurityContext.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionSecurityContext.cs @@ -1,18 +1,16 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Core.Contracts -{ - public sealed record SessionSecurityContext - { - public required UserKey? UserKey { get; init; } +namespace CodeBeam.UltimateAuth.Core.Contracts; - public required AuthSessionId SessionId { get; init; } +public sealed record SessionSecurityContext +{ + public required UserKey? UserKey { get; init; } - public SessionState State { get; init; } + public required AuthSessionId SessionId { get; init; } - public SessionChainId? ChainId { get; init; } + public SessionState State { get; init; } - public DeviceId? BoundDeviceId { get; init; } - } + public SessionChainId? ChainId { get; init; } + public DeviceId? BoundDeviceId { get; init; } } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionStoreContext.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionStoreContext.cs index 76b089a5..d3646e63 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionStoreContext.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionStoreContext.cs @@ -1,43 +1,42 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Core.Contracts +namespace CodeBeam.UltimateAuth.Core.Contracts; + +/// +/// Context information required by the session store when +/// creating or rotating sessions. +/// +public sealed class SessionStoreContext { /// - /// Context information required by the session store when - /// creating or rotating sessions. + /// The authenticated user identifier. /// - public sealed class SessionStoreContext - { - /// - /// The authenticated user identifier. - /// - public required UserKey UserKey { get; init; } + public required UserKey UserKey { get; init; } - /// - /// The tenant identifier, if multi-tenancy is enabled. - /// - public string? TenantId { get; init; } + /// + /// The tenant identifier, if multi-tenancy is enabled. + /// + public string? TenantId { get; init; } - /// - /// Optional chain identifier. - /// If null, a new chain should be created. - /// - public SessionChainId? ChainId { get; init; } + /// + /// Optional chain identifier. + /// If null, a new chain should be created. + /// + public SessionChainId? ChainId { get; init; } - /// - /// Indicates whether the session is metadata-only - /// (used in SemiHybrid mode). - /// - public bool IsMetadataOnly { get; init; } + /// + /// Indicates whether the session is metadata-only + /// (used in SemiHybrid mode). + /// + public bool IsMetadataOnly { get; init; } - /// - /// The UTC timestamp when the session was issued. - /// - public DateTimeOffset IssuedAt { get; init; } + /// + /// The UTC timestamp when the session was issued. + /// + public DateTimeOffset IssuedAt { get; init; } - /// - /// Optional device or client identifier. - /// - public required DeviceContext Device { get; init; } - } + /// + /// Optional device or client identifier. + /// + public required DeviceContext Device { get; init; } } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionTouchMode.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionTouchMode.cs index f7f42262..820f19fd 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionTouchMode.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionTouchMode.cs @@ -1,15 +1,14 @@ -namespace CodeBeam.UltimateAuth.Core.Contracts +namespace CodeBeam.UltimateAuth.Core.Contracts; + +public enum SessionTouchMode { - public enum SessionTouchMode - { - /// - /// Touch only if store policy allows (interval, throttling, etc.) - /// - IfNeeded, + /// + /// Touch only if store policy allows (interval, throttling, etc.) + /// + IfNeeded, - /// - /// Always update session activity, ignoring store heuristics. - /// - Force - } + /// + /// Always update session activity, ignoring store heuristics. + /// + Force } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionValidationContext.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionValidationContext.cs index bcae9016..afa90f4b 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionValidationContext.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionValidationContext.cs @@ -1,12 +1,11 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Core.Contracts +namespace CodeBeam.UltimateAuth.Core.Contracts; + +public sealed record SessionValidationContext { - public sealed record SessionValidationContext - { - public string? TenantId { get; init; } - public AuthSessionId SessionId { get; init; } - public DateTimeOffset Now { get; init; } - public required DeviceContext Device { get; init; } - } + public string? TenantId { get; init; } + public AuthSessionId SessionId { get; init; } + public DateTimeOffset Now { get; init; } + public required DeviceContext Device { get; init; } } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionValidationResult.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionValidationResult.cs index d760b286..59dcb029 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionValidationResult.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionValidationResult.cs @@ -1,66 +1,65 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Core.Contracts -{ - public sealed class SessionValidationResult - { - public string? TenantId { get; init; } +namespace CodeBeam.UltimateAuth.Core.Contracts; - public required SessionState State { get; init; } +public sealed class SessionValidationResult +{ + public string? TenantId { get; init; } - public UserKey? UserKey { get; init; } + public required SessionState State { get; init; } - public AuthSessionId? SessionId { get; init; } + public UserKey? UserKey { get; init; } - public SessionChainId? ChainId { get; init; } + public AuthSessionId? SessionId { get; init; } - public SessionRootId? RootId { get; init; } + public SessionChainId? ChainId { get; init; } - public DeviceId? BoundDeviceId { get; init; } + public SessionRootId? RootId { get; init; } - public ClaimsSnapshot Claims { get; init; } = ClaimsSnapshot.Empty; + public DeviceId? BoundDeviceId { get; init; } - public bool IsValid => State == SessionState.Active; + public ClaimsSnapshot Claims { get; init; } = ClaimsSnapshot.Empty; - private SessionValidationResult() { } + public bool IsValid => State == SessionState.Active; - public static SessionValidationResult Active( - string? tenantId, - UserKey? userId, - AuthSessionId sessionId, - SessionChainId chainId, - SessionRootId rootId, - ClaimsSnapshot claims, - DeviceId? boundDeviceId = null) - => new() - { - TenantId = tenantId, - State = SessionState.Active, - UserKey = userId, - SessionId = sessionId, - ChainId = chainId, - RootId = rootId, - Claims = claims, - BoundDeviceId = boundDeviceId - }; + private SessionValidationResult() { } - public static SessionValidationResult Invalid( - SessionState state, - UserKey? userId = null, - AuthSessionId? sessionId = null, - SessionChainId? chainId = null, - SessionRootId? rootId = null, - DeviceId? boundDeviceId = null) + public static SessionValidationResult Active( + string? tenantId, + UserKey? userId, + AuthSessionId sessionId, + SessionChainId chainId, + SessionRootId rootId, + ClaimsSnapshot claims, + DeviceId? boundDeviceId = null) => new() { - TenantId = null, - State = state, + TenantId = tenantId, + State = SessionState.Active, UserKey = userId, SessionId = sessionId, ChainId = chainId, RootId = rootId, - Claims = ClaimsSnapshot.Empty, + Claims = claims, BoundDeviceId = boundDeviceId }; - } + + public static SessionValidationResult Invalid( + SessionState state, + UserKey? userId = null, + AuthSessionId? sessionId = null, + SessionChainId? chainId = null, + SessionRootId? rootId = null, + DeviceId? boundDeviceId = null) + => new() + { + TenantId = null, + State = state, + UserKey = userId, + SessionId = sessionId, + ChainId = chainId, + RootId = rootId, + Claims = ClaimsSnapshot.Empty, + BoundDeviceId = boundDeviceId + }; } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/AccessToken.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/AccessToken.cs index 32843501..459c8d0d 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/AccessToken.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/AccessToken.cs @@ -1,32 +1,31 @@ -namespace CodeBeam.UltimateAuth.Core.Contracts +namespace CodeBeam.UltimateAuth.Core.Contracts; + +/// +/// Represents an issued access token (JWT or opaque). +/// +public sealed class AccessToken { /// - /// Represents an issued access token (JWT or opaque). + /// The actual token value sent to the client. /// - public sealed class AccessToken - { - /// - /// The actual token value sent to the client. - /// - public required string Token { get; init; } + public required string Token { get; init; } - // TODO: TokenKind enum? - /// - /// Token type: "jwt" or "opaque". - /// Used for diagnostics and middleware behavior. - /// - public TokenType Type { get; init; } + // TODO: TokenKind enum? + /// + /// Token type: "jwt" or "opaque". + /// Used for diagnostics and middleware behavior. + /// + public TokenType Type { get; init; } - /// - /// Expiration time of the token. - /// - public required DateTimeOffset ExpiresAt { get; init; } + /// + /// Expiration time of the token. + /// + public required DateTimeOffset ExpiresAt { get; init; } - /// - /// Optional session id this token is bound to (Hybrid / SemiHybrid). - /// - public string? SessionId { get; init; } + /// + /// Optional session id this token is bound to (Hybrid / SemiHybrid). + /// + public string? SessionId { get; init; } - public string? Scope { get; init; } - } + public string? Scope { get; init; } } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/AuthTokens.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/AuthTokens.cs index 344fedd9..be61e290 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/AuthTokens.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/AuthTokens.cs @@ -1,17 +1,16 @@ -namespace CodeBeam.UltimateAuth.Core.Contracts +namespace CodeBeam.UltimateAuth.Core.Contracts; + +/// +/// Represents a set of authentication tokens issued as a result of a successful login. +/// This model is intentionally extensible to support additional token types in the future. +/// +public sealed record AuthTokens { /// - /// Represents a set of authentication tokens issued as a result of a successful login. - /// This model is intentionally extensible to support additional token types in the future. + /// The issued access token. + /// Always present when is returned. /// - public sealed record AuthTokens - { - /// - /// The issued access token. - /// Always present when is returned. - /// - public AccessToken AccessToken { get; init; } = default!; + public AccessToken AccessToken { get; init; } = default!; - public RefreshToken? RefreshToken { get; init; } - } + public RefreshToken? RefreshToken { get; init; } } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/OpaqueTokenRecord.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/OpaqueTokenRecord.cs deleted file mode 100644 index ed13a6ae..00000000 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/OpaqueTokenRecord.cs +++ /dev/null @@ -1,18 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Domain; -using System.Security.Claims; - -namespace CodeBeam.UltimateAuth.Core.Contracts -{ - public sealed class OpaqueTokenRecord - { - public string TokenHash { get; init; } = default!; - public string UserId { get; init; } = default!; - public string? TenantId { get; init; } - public AuthSessionId? SessionId { get; init; } - public DateTimeOffset ExpiresAt { get; init; } - public bool IsRevoked { get; init; } - public DateTimeOffset? RevokedAt { get; init; } - public IReadOnlyCollection Claims { get; init; } = Array.Empty(); - } - -} diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/PrimaryToken.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/PrimaryToken.cs index cb43d693..be3a120c 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/PrimaryToken.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/PrimaryToken.cs @@ -1,23 +1,19 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Core.Contracts -{ - public sealed record PrimaryToken - { - public PrimaryTokenKind Kind { get; } - public string Value { get; } - - private PrimaryToken(PrimaryTokenKind kind, string value) - { - Kind = kind; - Value = value; - } +namespace CodeBeam.UltimateAuth.Core.Contracts; - public static PrimaryToken FromSession(AuthSessionId sessionId) - => new(PrimaryTokenKind.Session, sessionId.ToString()); +public sealed record PrimaryToken +{ + public PrimaryTokenKind Kind { get; } + public string Value { get; } - public static PrimaryToken FromAccessToken(AccessToken token) - => new(PrimaryTokenKind.AccessToken, token.Token); + private PrimaryToken(PrimaryTokenKind kind, string value) + { + Kind = kind; + Value = value; } + public static PrimaryToken FromSession(AuthSessionId sessionId) => new(PrimaryTokenKind.Session, sessionId.ToString()); + + public static PrimaryToken FromAccessToken(AccessToken token) => new(PrimaryTokenKind.AccessToken, token.Token); } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/PrimaryTokenKind.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/PrimaryTokenKind.cs index 821c3d19..0ef2e9f6 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/PrimaryTokenKind.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/PrimaryTokenKind.cs @@ -1,8 +1,7 @@ -namespace CodeBeam.UltimateAuth.Core.Contracts +namespace CodeBeam.UltimateAuth.Core.Contracts; + +public enum PrimaryTokenKind { - public enum PrimaryTokenKind - { - Session = 1, - AccessToken = 2 - } + Session = 1, + AccessToken = 2 } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/RefreshToken.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/RefreshToken.cs index 54306e68..d741b858 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/RefreshToken.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/RefreshToken.cs @@ -1,23 +1,22 @@ -namespace CodeBeam.UltimateAuth.Core.Contracts +namespace CodeBeam.UltimateAuth.Core.Contracts; + +/// +/// Transport model for refresh token. Returned to client once upon creation. +/// +public sealed class RefreshToken { /// - /// Transport model for refresh token. Returned to client once upon creation. + /// Plain refresh token value (returned to client once). /// - public sealed class RefreshToken - { - /// - /// Plain refresh token value (returned to client once). - /// - public required string Token { get; init; } + public required string Token { get; init; } - /// - /// Hash of the refresh token to be persisted. - /// - public required string TokenHash { get; init; } + /// + /// Hash of the refresh token to be persisted. + /// + public required string TokenHash { get; init; } - /// - /// Expiration time. - /// - public required DateTimeOffset ExpiresAt { get; init; } - } + /// + /// Expiration time. + /// + public required DateTimeOffset ExpiresAt { get; init; } } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/RefreshTokenFailureReason.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/RefreshTokenFailureReason.cs deleted file mode 100644 index 8e4cefc1..00000000 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/RefreshTokenFailureReason.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace CodeBeam.UltimateAuth.Core.Contracts -{ - public enum RefreshTokenFailureReason - { - Invalid, - Expired, - Revoked, - Reused - } -} diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/RefreshTokenRotationExecution.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/RefreshTokenRotationExecution.cs index e565b329..69aba273 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/RefreshTokenRotationExecution.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/RefreshTokenRotationExecution.cs @@ -1,15 +1,14 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Core.Contracts +namespace CodeBeam.UltimateAuth.Core.Contracts; + +public sealed record RefreshTokenRotationExecution { - public sealed record RefreshTokenRotationExecution - { - public RefreshTokenRotationResult Result { get; init; } = default!; + public RefreshTokenRotationResult Result { get; init; } = default!; - // INTERNAL – flow/orchestrator only - public UserKey? UserKey { get; init; } - public AuthSessionId? SessionId { get; init; } - public SessionChainId? ChainId { get; init; } - public string? TenantId { get; init; } - } + // INTERNAL – flow/orchestrator only + public UserKey? UserKey { get; init; } + public AuthSessionId? SessionId { get; init; } + public SessionChainId? ChainId { get; init; } + public string? TenantId { get; init; } } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/RefreshTokenValidationResult.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/RefreshTokenValidationResult.cs index e9423502..3fb78779 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/RefreshTokenValidationResult.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/RefreshTokenValidationResult.cs @@ -1,63 +1,62 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Core.Contracts -{ - public sealed record RefreshTokenValidationResult - { - public bool IsValid { get; init; } - public bool IsReuseDetected { get; init; } +namespace CodeBeam.UltimateAuth.Core.Contracts; - public string? TokenHash { get; init; } +public sealed record RefreshTokenValidationResult +{ + public bool IsValid { get; init; } + public bool IsReuseDetected { get; init; } - public string? TenantId { get; init; } - public UserKey? UserKey { get; init; } - public AuthSessionId? SessionId { get; init; } - public SessionChainId? ChainId { get; init; } + public string? TokenHash { get; init; } - public DateTimeOffset? ExpiresAt { get; init; } + public string? TenantId { get; init; } + public UserKey? UserKey { get; init; } + public AuthSessionId? SessionId { get; init; } + public SessionChainId? ChainId { get; init; } + public DateTimeOffset? ExpiresAt { get; init; } - private RefreshTokenValidationResult() { } - public static RefreshTokenValidationResult Invalid() - => new() - { - IsValid = false, - IsReuseDetected = false - }; + private RefreshTokenValidationResult() { } - public static RefreshTokenValidationResult ReuseDetected( - string? tenantId = null, - AuthSessionId? sessionId = null, - string? tokenHash = null, - SessionChainId? chainId = null, - UserKey? userKey = default) + public static RefreshTokenValidationResult Invalid() => new() { IsValid = false, - IsReuseDetected = true, - TenantId = tenantId, - SessionId = sessionId, - TokenHash = tokenHash, - ChainId = chainId, - UserKey = userKey, + IsReuseDetected = false }; - public static RefreshTokenValidationResult Valid( - string? tenantId, - UserKey userKey, - AuthSessionId sessionId, - string? tokenHash, - SessionChainId? chainId = null) - => new() - { - IsValid = true, - IsReuseDetected = false, - TenantId = tenantId, - UserKey = userKey, - SessionId = sessionId, - ChainId = chainId, - TokenHash = tokenHash - }; - } + public static RefreshTokenValidationResult ReuseDetected( + string? tenantId = null, + AuthSessionId? sessionId = null, + string? tokenHash = null, + SessionChainId? chainId = null, + UserKey? userKey = default) + => new() + { + IsValid = false, + IsReuseDetected = true, + TenantId = tenantId, + SessionId = sessionId, + TokenHash = tokenHash, + ChainId = chainId, + UserKey = userKey, + }; + + public static RefreshTokenValidationResult Valid( + string? tenantId, + UserKey userKey, + AuthSessionId sessionId, + string? tokenHash, + SessionChainId? chainId = null) + => new() + { + IsValid = true, + IsReuseDetected = false, + TenantId = tenantId, + UserKey = userKey, + SessionId = sessionId, + ChainId = chainId, + TokenHash = tokenHash + }; } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenFormat.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenFormat.cs index b36c1df6..3e17fc19 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenFormat.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenFormat.cs @@ -1,9 +1,8 @@ -namespace CodeBeam.UltimateAuth.Core.Contracts +namespace CodeBeam.UltimateAuth.Core.Contracts; + +// It's not primary token kind, it's about transport format. +public enum TokenFormat { - // It's not primary token kind, it's about transport format. - public enum TokenFormat - { - Opaque = 1, - Jwt = 2 - } + Opaque = 1, + Jwt = 2 } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenInvalidReason.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenInvalidReason.cs index 96ce78b1..5e80df5d 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenInvalidReason.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenInvalidReason.cs @@ -1,16 +1,15 @@ -namespace CodeBeam.UltimateAuth.Core.Contracts +namespace CodeBeam.UltimateAuth.Core.Contracts; + +public enum TokenInvalidReason { - public enum TokenInvalidReason - { - Invalid, - Expired, - Revoked, - Malformed, - SignatureInvalid, - AudienceMismatch, - IssuerMismatch, - MissingSubject, - Unknown, - NotImplemented - } + Invalid, + Expired, + Revoked, + Malformed, + SignatureInvalid, + AudienceMismatch, + IssuerMismatch, + MissingSubject, + Unknown, + NotImplemented } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenIssuanceContext.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenIssuanceContext.cs index f070cd12..080fb35e 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenIssuanceContext.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenIssuanceContext.cs @@ -1,14 +1,13 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Core.Contracts +namespace CodeBeam.UltimateAuth.Core.Contracts; + +public sealed record TokenIssuanceContext { - public sealed record TokenIssuanceContext - { - public required UserKey UserKey { get; init; } - public string? TenantId { get; init; } - public IReadOnlyDictionary Claims { get; set; } = new Dictionary(); - public AuthSessionId? SessionId { get; init; } - public SessionChainId? ChainId { get; init; } - public DateTimeOffset IssuedAt { get; init; } - } + public required UserKey UserKey { get; init; } + public string? TenantId { get; init; } + public IReadOnlyDictionary Claims { get; set; } = new Dictionary(); + public AuthSessionId? SessionId { get; init; } + public SessionChainId? ChainId { get; init; } + public DateTimeOffset IssuedAt { get; init; } } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenIssueContext.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenIssueContext.cs index d7428ae7..d31377e5 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenIssueContext.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenIssueContext.cs @@ -1,11 +1,10 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Core.Contracts +namespace CodeBeam.UltimateAuth.Core.Contracts; + +public sealed record TokenIssueContext { - public sealed record TokenIssueContext - { - public string? TenantId { get; init; } - public ISession Session { get; init; } = default!; - public DateTimeOffset At { get; init; } - } + public string? TenantId { get; init; } + public ISession Session { get; init; } = default!; + public DateTimeOffset At { get; init; } } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenRefreshContext.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenRefreshContext.cs index 95074428..ffc4e589 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenRefreshContext.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenRefreshContext.cs @@ -1,9 +1,8 @@ -namespace CodeBeam.UltimateAuth.Core.Contracts +namespace CodeBeam.UltimateAuth.Core.Contracts; + +public sealed record TokenRefreshContext { - public sealed record TokenRefreshContext - { - public string? TenantId { get; init; } + public string? TenantId { get; init; } - public string RefreshToken { get; init; } = default!; - } + public string RefreshToken { get; init; } = default!; } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenType.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenType.cs index dc94f72e..1c26c007 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenType.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenType.cs @@ -1,10 +1,8 @@ -namespace CodeBeam.UltimateAuth.Core.Contracts -{ - public enum TokenType - { - Opaque, - Jwt, - Unknown - } +namespace CodeBeam.UltimateAuth.Core.Contracts; +public enum TokenType +{ + Opaque, + Jwt, + Unknown } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenValidationResult.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenValidationResult.cs index 15485523..011a8d00 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenValidationResult.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenValidationResult.cs @@ -1,67 +1,66 @@ using CodeBeam.UltimateAuth.Core.Domain; using System.Security.Claims; -namespace CodeBeam.UltimateAuth.Core.Contracts +namespace CodeBeam.UltimateAuth.Core.Contracts; + +public sealed record TokenValidationResult { - public sealed record TokenValidationResult - { - public bool IsValid { get; init; } - public TokenType Type { get; init; } - public string? TenantId { get; init; } - public TUserId? UserId { get; init; } - public AuthSessionId? SessionId { get; init; } - public IReadOnlyCollection Claims { get; init; } = Array.Empty(); - public TokenInvalidReason? InvalidReason { get; init; } - public DateTimeOffset? ExpiresAt { get; set; } + public bool IsValid { get; init; } + public TokenType Type { get; init; } + public string? TenantId { get; init; } + public TUserId? UserId { get; init; } + public AuthSessionId? SessionId { get; init; } + public IReadOnlyCollection Claims { get; init; } = Array.Empty(); + public TokenInvalidReason? InvalidReason { get; init; } + public DateTimeOffset? ExpiresAt { get; set; } - private TokenValidationResult( - bool isValid, - TokenType type, - string? tenantId, - TUserId? userId, - AuthSessionId? sessionId, - IReadOnlyCollection? claims, - TokenInvalidReason? invalidReason, - DateTimeOffset? expiresAt - ) - { - IsValid = isValid; - TenantId = tenantId; - UserId = userId; - SessionId = sessionId; - Claims = claims ?? Array.Empty(); - InvalidReason = invalidReason; - ExpiresAt = expiresAt; - } + private TokenValidationResult( + bool isValid, + TokenType type, + string? tenantId, + TUserId? userId, + AuthSessionId? sessionId, + IReadOnlyCollection? claims, + TokenInvalidReason? invalidReason, + DateTimeOffset? expiresAt + ) + { + IsValid = isValid; + TenantId = tenantId; + UserId = userId; + SessionId = sessionId; + Claims = claims ?? Array.Empty(); + InvalidReason = invalidReason; + ExpiresAt = expiresAt; + } - public static TokenValidationResult Valid( - TokenType type, - string? tenantId, - TUserId userId, - AuthSessionId? sessionId, - IReadOnlyCollection claims, - DateTimeOffset? expiresAt) - => new( - isValid: true, - type, - tenantId, - userId, - sessionId, - claims, - invalidReason: null, - expiresAt - ); + public static TokenValidationResult Valid( + TokenType type, + string? tenantId, + TUserId userId, + AuthSessionId? sessionId, + IReadOnlyCollection claims, + DateTimeOffset? expiresAt) + => new( + isValid: true, + type, + tenantId, + userId, + sessionId, + claims, + invalidReason: null, + expiresAt + ); - public static TokenValidationResult Invalid(TokenType type, TokenInvalidReason reason) - => new( - isValid: false, - type, - tenantId: null, - userId: default, - sessionId: null, - claims: null, - invalidReason: reason, - expiresAt: null - ); - } + public static TokenValidationResult Invalid(TokenType type, TokenInvalidReason reason) + => new( + isValid: false, + type, + tenantId: null, + userId: default, + sessionId: null, + claims: null, + invalidReason: reason, + expiresAt: null + ); } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Unit.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Unit.cs index e921add4..d2964274 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Unit.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Unit.cs @@ -1,7 +1,6 @@ -namespace CodeBeam.UltimateAuth.Core.Contracts +namespace CodeBeam.UltimateAuth.Core.Contracts; + +public readonly struct Unit { - public readonly struct Unit - { - public static readonly Unit Value = new(); - } + public static readonly Unit Value = new(); } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/User/AuthUserSnapshot.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/User/AuthUserSnapshot.cs index ef8cdcce..2f61fa68 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/User/AuthUserSnapshot.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/User/AuthUserSnapshot.cs @@ -1,28 +1,27 @@ -namespace CodeBeam.UltimateAuth.Core.Contracts -{ - // This is for AuthFlowContext, with minimal data and no db access - /// - /// Represents the minimal authentication state of the current request. - /// This type is request-scoped and contains no domain or persistence data. - /// - /// AuthUserSnapshot answers only the question: - /// "Is there an authenticated user associated with this execution context?" - /// - /// It must not be used for user discovery, lifecycle decisions, - /// or authorization policies. - /// - public sealed class AuthUserSnapshot - { - public bool IsAuthenticated { get; } - public TUserId? UserId { get; } +namespace CodeBeam.UltimateAuth.Core.Contracts; - private AuthUserSnapshot(bool isAuthenticated, TUserId? userId) - { - IsAuthenticated = isAuthenticated; - UserId = userId; - } +// This is for AuthFlowContext, with minimal data and no db access +/// +/// Represents the minimal authentication state of the current request. +/// This type is request-scoped and contains no domain or persistence data. +/// +/// AuthUserSnapshot answers only the question: +/// "Is there an authenticated user associated with this execution context?" +/// +/// It must not be used for user discovery, lifecycle decisions, +/// or authorization policies. +/// +public sealed class AuthUserSnapshot +{ + public bool IsAuthenticated { get; } + public TUserId? UserId { get; } - public static AuthUserSnapshot Authenticated(TUserId userId) => new(true, userId); - public static AuthUserSnapshot Anonymous() => new(false, default); + private AuthUserSnapshot(bool isAuthenticated, TUserId? userId) + { + IsAuthenticated = isAuthenticated; + UserId = userId; } + + public static AuthUserSnapshot Authenticated(TUserId userId) => new(true, userId); + public static AuthUserSnapshot Anonymous() => new(false, default); } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/User/UserAuthenticationResult.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/User/UserAuthenticationResult.cs index 64e2c34b..a105c336 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/User/UserAuthenticationResult.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/User/UserAuthenticationResult.cs @@ -1,26 +1,25 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Core.Contracts +namespace CodeBeam.UltimateAuth.Core.Contracts; + +public sealed class UserAuthenticationResult { - public sealed class UserAuthenticationResult - { - public bool Succeeded { get; init; } + public bool Succeeded { get; init; } - public TUserId? UserId { get; init; } + public TUserId? UserId { get; init; } - public ClaimsSnapshot? Claims { get; init; } + public ClaimsSnapshot? Claims { get; init; } - public bool RequiresMfa { get; init; } + public bool RequiresMfa { get; init; } - public static UserAuthenticationResult Fail() => new() { Succeeded = false }; + public static UserAuthenticationResult Fail() => new() { Succeeded = false }; - public static UserAuthenticationResult Success(TUserId userId, ClaimsSnapshot claims, bool requiresMfa = false) - => new() - { - Succeeded = true, - UserId = userId, - Claims = claims, - RequiresMfa = requiresMfa - }; - } + public static UserAuthenticationResult Success(TUserId userId, ClaimsSnapshot claims, bool requiresMfa = false) + => new() + { + Succeeded = true, + UserId = userId, + Claims = claims, + RequiresMfa = requiresMfa + }; } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/User/UserContext.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/User/UserContext.cs index 2063d0c1..5a20021b 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/User/UserContext.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/User/UserContext.cs @@ -1,14 +1,13 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Core.Contracts +namespace CodeBeam.UltimateAuth.Core.Contracts; + +public sealed class UserContext { - public sealed class UserContext - { - public TUserId? UserId { get; init; } - public IAuthSubject? User { get; init; } + public TUserId? UserId { get; init; } + public IAuthSubject? User { get; init; } - public bool IsAuthenticated => UserId is not null; + public bool IsAuthenticated => UserId is not null; - public static UserContext Anonymous() => new(); - } + public static UserContext Anonymous() => new(); } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/User/ValidateCredentialsRequest.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/User/ValidateCredentialsRequest.cs deleted file mode 100644 index fc1dd7ef..00000000 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/User/ValidateCredentialsRequest.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace CodeBeam.UltimateAuth.Core.Contracts -{ - /// - /// Request to validate user credentials. - /// Used during login flows. - /// - public sealed class ValidateCredentialsRequest - { - /// - /// User identifier (same value used during registration). - /// - public required string Identifier { get; init; } - - /// - /// Plain-text password provided by the user. - /// - public required string Password { get; init; } - - /// - /// Optional tenant identifier. - /// - public string? TenantId { get; init; } - } -} diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/AuthFlowType.cs b/src/CodeBeam.UltimateAuth.Core/Domain/AuthFlowType.cs index 8d077b7b..905d54df 100644 --- a/src/CodeBeam.UltimateAuth.Core/Domain/AuthFlowType.cs +++ b/src/CodeBeam.UltimateAuth.Core/Domain/AuthFlowType.cs @@ -1,31 +1,30 @@ -namespace CodeBeam.UltimateAuth.Core.Domain +namespace CodeBeam.UltimateAuth.Core.Domain; + +public enum AuthFlowType { - public enum AuthFlowType - { - Login, - Reauthentication, + Login, + Reauthentication, - Logout, - RefreshSession, - ValidateSession, + Logout, + RefreshSession, + ValidateSession, - IssueToken, - RefreshToken, - IntrospectToken, - RevokeToken, + IssueToken, + RefreshToken, + IntrospectToken, + RevokeToken, - QuerySession, - RevokeSession, + QuerySession, + RevokeSession, - UserInfo, - PermissionQuery, + UserInfo, + PermissionQuery, - UserManagement, - UserProfileManagement, - UserIdentifierManagement, - CredentialManagement, - AuthorizationManagement, + UserManagement, + UserProfileManagement, + UserIdentifierManagement, + CredentialManagement, + AuthorizationManagement, - ApiAccess - } + ApiAccess } diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Device/DeviceContext.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Device/DeviceContext.cs index e345dfb9..fcc44dde 100644 --- a/src/CodeBeam.UltimateAuth.Core/Domain/Device/DeviceContext.cs +++ b/src/CodeBeam.UltimateAuth.Core/Domain/Device/DeviceContext.cs @@ -1,27 +1,23 @@ -namespace CodeBeam.UltimateAuth.Core.Domain -{ - public sealed class DeviceContext - { - public DeviceId? DeviceId { get; init; } +namespace CodeBeam.UltimateAuth.Core.Domain; - public bool HasDeviceId => DeviceId is not null; +public sealed class DeviceContext +{ + public DeviceId? DeviceId { get; init; } - private DeviceContext(DeviceId? deviceId) - { - DeviceId = deviceId; - } + public bool HasDeviceId => DeviceId is not null; - public static DeviceContext Anonymous() - => new(null); + private DeviceContext(DeviceId? deviceId) + { + DeviceId = deviceId; + } - public static DeviceContext FromDeviceId(DeviceId deviceId) - => new(deviceId); + public static DeviceContext Anonymous() => new(null); - // DeviceInfo is a transport object. - // AuthFlowContextFactory changes it to a useable DeviceContext - // DeviceContext doesn't have fields like IsTrusted etc. It's authority layer's responsibility. - // IP, Geo, Fingerprint, Platform, UA will be added here. + public static DeviceContext FromDeviceId(DeviceId deviceId) => new(deviceId); - } + // DeviceInfo is a transport object. + // AuthFlowContextFactory changes it to a useable DeviceContext + // DeviceContext doesn't have fields like IsTrusted etc. It's authority layer's responsibility. + // IP, Geo, Fingerprint, Platform, UA will be added here. } diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Hub/HubCredentials.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Hub/HubCredentials.cs index 0c059345..458ca78f 100644 --- a/src/CodeBeam.UltimateAuth.Core/Domain/Hub/HubCredentials.cs +++ b/src/CodeBeam.UltimateAuth.Core/Domain/Hub/HubCredentials.cs @@ -1,11 +1,10 @@ using CodeBeam.UltimateAuth.Core.Options; -namespace CodeBeam.UltimateAuth.Core.Domain +namespace CodeBeam.UltimateAuth.Core.Domain; + +public sealed class HubCredentials { - public sealed class HubCredentials - { - public string AuthorizationCode { get; init; } = default!; - public string CodeVerifier { get; init; } = default!; - public UAuthClientProfile ClientProfile { get; init; } - } + public string AuthorizationCode { get; init; } = default!; + public string CodeVerifier { get; init; } = default!; + public UAuthClientProfile ClientProfile { get; init; } } diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Hub/HubFlowState.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Hub/HubFlowState.cs index 344f1a6c..b45b995e 100644 --- a/src/CodeBeam.UltimateAuth.Core/Domain/Hub/HubFlowState.cs +++ b/src/CodeBeam.UltimateAuth.Core/Domain/Hub/HubFlowState.cs @@ -1,18 +1,16 @@ using CodeBeam.UltimateAuth.Core.Options; -namespace CodeBeam.UltimateAuth.Core.Domain -{ - public sealed class HubFlowState - { - public HubSessionId HubSessionId { get; init; } - public HubFlowType FlowType { get; init; } - public UAuthClientProfile ClientProfile { get; init; } - public string? ReturnUrl { get; init; } +namespace CodeBeam.UltimateAuth.Core.Domain; - public bool IsActive { get; init; } - public bool IsExpired { get; init; } - public bool IsCompleted { get; init; } - public bool Exists { get; init; } - } +public sealed class HubFlowState +{ + public HubSessionId HubSessionId { get; init; } + public HubFlowType FlowType { get; init; } + public UAuthClientProfile ClientProfile { get; init; } + public string? ReturnUrl { get; init; } + public bool IsActive { get; init; } + public bool IsExpired { get; init; } + public bool IsCompleted { get; init; } + public bool Exists { get; init; } } diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Principals/AuthFailureReason.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Principals/AuthFailureReason.cs index 24f16326..7d3ba4cd 100644 --- a/src/CodeBeam.UltimateAuth.Core/Domain/Principals/AuthFailureReason.cs +++ b/src/CodeBeam.UltimateAuth.Core/Domain/Principals/AuthFailureReason.cs @@ -1,14 +1,13 @@ -namespace CodeBeam.UltimateAuth.Core.Domain +namespace CodeBeam.UltimateAuth.Core.Domain; + +public enum AuthFailureReason { - public enum AuthFailureReason - { - InvalidCredentials, - LockedOut, - RequiresMfa, - SessionExpired, - SessionRevoked, - TenantDisabled, - Unauthorized, - Unknown - } + InvalidCredentials, + LockedOut, + RequiresMfa, + SessionExpired, + SessionRevoked, + TenantDisabled, + Unauthorized, + Unknown } diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Principals/ClaimsSnapshotBuilder.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Principals/ClaimsSnapshotBuilder.cs index 6ceca575..399c1cdd 100644 --- a/src/CodeBeam.UltimateAuth.Core/Domain/Principals/ClaimsSnapshotBuilder.cs +++ b/src/CodeBeam.UltimateAuth.Core/Domain/Principals/ClaimsSnapshotBuilder.cs @@ -1,39 +1,38 @@ using System.Security.Claims; -namespace CodeBeam.UltimateAuth.Core.Domain +namespace CodeBeam.UltimateAuth.Core.Domain; + +public sealed class ClaimsSnapshotBuilder { - public sealed class ClaimsSnapshotBuilder - { - private readonly Dictionary> _claims = new(StringComparer.Ordinal); + private readonly Dictionary> _claims = new(StringComparer.Ordinal); - public ClaimsSnapshotBuilder Add(string type, string value) + public ClaimsSnapshotBuilder Add(string type, string value) + { + if (!_claims.TryGetValue(type, out var set)) { - if (!_claims.TryGetValue(type, out var set)) - { - set = new HashSet(StringComparer.Ordinal); - _claims[type] = set; - } - - set.Add(value); - return this; + set = new HashSet(StringComparer.Ordinal); + _claims[type] = set; } - public ClaimsSnapshotBuilder AddMany(string type, IEnumerable values) - { - foreach (var v in values) - Add(type, v); + set.Add(value); + return this; + } - return this; - } + public ClaimsSnapshotBuilder AddMany(string type, IEnumerable values) + { + foreach (var v in values) + Add(type, v); - public ClaimsSnapshotBuilder AddRole(string role) => Add(ClaimTypes.Role, role); + return this; + } - public ClaimsSnapshotBuilder AddPermission(string permission) => Add("uauth:permission", permission); + public ClaimsSnapshotBuilder AddRole(string role) => Add(ClaimTypes.Role, role); - public ClaimsSnapshot Build() - { - var frozen = _claims.ToDictionary(kv => kv.Key, kv => (IReadOnlyCollection)kv.Value.ToArray(), StringComparer.Ordinal); - return new ClaimsSnapshot(frozen); - } + public ClaimsSnapshotBuilder AddPermission(string permission) => Add("uauth:permission", permission); + + public ClaimsSnapshot Build() + { + var frozen = _claims.ToDictionary(kv => kv.Key, kv => (IReadOnlyCollection)kv.Value.ToArray(), StringComparer.Ordinal); + return new ClaimsSnapshot(frozen); } } diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Principals/CredentialKind.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Principals/CredentialKind.cs index 0bae1432..38e076be 100644 --- a/src/CodeBeam.UltimateAuth.Core/Domain/Principals/CredentialKind.cs +++ b/src/CodeBeam.UltimateAuth.Core/Domain/Principals/CredentialKind.cs @@ -1,9 +1,8 @@ -namespace CodeBeam.UltimateAuth.Core.Domain +namespace CodeBeam.UltimateAuth.Core.Domain; + +public enum CredentialKind { - public enum CredentialKind - { - Session, - AccessToken, - RefreshToken - } + Session, + AccessToken, + RefreshToken } diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Principals/PrimaryCredentialKind.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Principals/PrimaryCredentialKind.cs index e791ae67..e5ddc547 100644 --- a/src/CodeBeam.UltimateAuth.Core/Domain/Principals/PrimaryCredentialKind.cs +++ b/src/CodeBeam.UltimateAuth.Core/Domain/Principals/PrimaryCredentialKind.cs @@ -1,8 +1,7 @@ -namespace CodeBeam.UltimateAuth.Core.Domain +namespace CodeBeam.UltimateAuth.Core.Domain; + +public enum PrimaryCredentialKind { - public enum PrimaryCredentialKind - { - Stateful, - Stateless - } + Stateful, + Stateless } diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Principals/ReauthBehavior.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Principals/ReauthBehavior.cs index 3bf0cd41..315337c0 100644 --- a/src/CodeBeam.UltimateAuth.Core/Domain/Principals/ReauthBehavior.cs +++ b/src/CodeBeam.UltimateAuth.Core/Domain/Principals/ReauthBehavior.cs @@ -1,9 +1,8 @@ -namespace CodeBeam.UltimateAuth.Core.Domain +namespace CodeBeam.UltimateAuth.Core.Domain; + +public enum ReauthBehavior { - public enum ReauthBehavior - { - RedirectToLogin, - None, - RaiseEvent - } + RedirectToLogin, + None, + RaiseEvent } diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Principals/UAuthClaim.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Principals/UAuthClaim.cs deleted file mode 100644 index c0b05117..00000000 --- a/src/CodeBeam.UltimateAuth.Core/Domain/Principals/UAuthClaim.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace CodeBeam.UltimateAuth.Core.Domain -{ - public sealed record UAuthClaim(string Type, string Value); -} diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Session/AuthSessionId.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Session/AuthSessionId.cs index 8663562d..c07fadc0 100644 --- a/src/CodeBeam.UltimateAuth.Core/Domain/Session/AuthSessionId.cs +++ b/src/CodeBeam.UltimateAuth.Core/Domain/Session/AuthSessionId.cs @@ -1,35 +1,34 @@ -namespace CodeBeam.UltimateAuth.Core.Domain +namespace CodeBeam.UltimateAuth.Core.Domain; + +// AuthSessionId is a opaque token, because it's more sensitive data. SessionChainId and SessionRootId are Guid. +public readonly record struct AuthSessionId { - // AuthSessionId is a opaque token, because it's more sensitive data. SessionChainId and SessionRootId are Guid. - public readonly record struct AuthSessionId + public string Value { get; } + + private AuthSessionId(string value) { - public string Value { get; } + Value = value; + } - private AuthSessionId(string value) + public static bool TryCreate(string raw, out AuthSessionId id) + { + if (string.IsNullOrWhiteSpace(raw)) { - Value = value; + id = default; + return false; } - public static bool TryCreate(string raw, out AuthSessionId id) + if (raw.Length < 32) { - if (string.IsNullOrWhiteSpace(raw)) - { - id = default; - return false; - } - - if (raw.Length < 32) - { - id = default; - return false; - } - - id = new AuthSessionId(raw); - return true; + id = default; + return false; } - public override string ToString() => Value; - - public static implicit operator string(AuthSessionId id) => id.Value; + id = new AuthSessionId(raw); + return true; } + + public override string ToString() => Value; + + public static implicit operator string(AuthSessionId id) => id.Value; } diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Session/ClaimsSnapshot.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Session/ClaimsSnapshot.cs index c6f5f7bd..6d497ba9 100644 --- a/src/CodeBeam.UltimateAuth.Core/Domain/Session/ClaimsSnapshot.cs +++ b/src/CodeBeam.UltimateAuth.Core/Domain/Session/ClaimsSnapshot.cs @@ -1,173 +1,172 @@ using System.Security.Claims; using System.Text.Json.Serialization; -namespace CodeBeam.UltimateAuth.Core.Domain +namespace CodeBeam.UltimateAuth.Core.Domain; + +public sealed class ClaimsSnapshot { - public sealed class ClaimsSnapshot + private readonly IReadOnlyDictionary> _claims; + public IReadOnlyDictionary> Claims => _claims; + + [JsonConstructor] + public ClaimsSnapshot(IReadOnlyDictionary> claims) { - private readonly IReadOnlyDictionary> _claims; - public IReadOnlyDictionary> Claims => _claims; + _claims = claims; + } - [JsonConstructor] - public ClaimsSnapshot(IReadOnlyDictionary> claims) - { - _claims = claims; - } + public static ClaimsSnapshot Empty { get; } = new(new Dictionary>()); - public static ClaimsSnapshot Empty { get; } = new(new Dictionary>()); + public string? Get(string type) => _claims.TryGetValue(type, out var values) ? values.FirstOrDefault() : null; + public bool TryGet(string type, out string value) + { + value = null!; - public string? Get(string type) => _claims.TryGetValue(type, out var values) ? values.FirstOrDefault() : null; - public bool TryGet(string type, out string value) - { - value = null!; + if (!Claims.TryGetValue(type, out var values)) + return false; - if (!Claims.TryGetValue(type, out var values)) - return false; + var first = values.FirstOrDefault(); + if (first is null) + return false; - var first = values.FirstOrDefault(); - if (first is null) - return false; + value = first; + return true; + } + public IReadOnlyCollection GetAll(string type) => _claims.TryGetValue(type, out var values) ? values : Array.Empty(); - value = first; - return true; - } - public IReadOnlyCollection GetAll(string type) => _claims.TryGetValue(type, out var values) ? values : Array.Empty(); + public bool Has(string type) => _claims.ContainsKey(type); + public bool HasValue(string type, string value) => _claims.TryGetValue(type, out var values) && values.Contains(value); - public bool Has(string type) => _claims.ContainsKey(type); - public bool HasValue(string type, string value) => _claims.TryGetValue(type, out var values) && values.Contains(value); + public IReadOnlyCollection Roles => GetAll(ClaimTypes.Role); + public IReadOnlyCollection Permissions => GetAll("uauth:permission"); - public IReadOnlyCollection Roles => GetAll(ClaimTypes.Role); - public IReadOnlyCollection Permissions => GetAll("uauth:permission"); + public bool IsInRole(string role) => HasValue(ClaimTypes.Role, role); + public bool HasPermission(string permission) => HasValue("uauth:permission", permission); - public bool IsInRole(string role) => HasValue(ClaimTypes.Role, role); - public bool HasPermission(string permission) => HasValue("uauth:permission", permission); + /// + /// Flattens claims by taking the first value of each claim. + /// Useful for logging, diagnostics, or legacy consumers. + /// + public IReadOnlyDictionary AsDictionary() + { + var dict = new Dictionary(StringComparer.Ordinal); - /// - /// Flattens claims by taking the first value of each claim. - /// Useful for logging, diagnostics, or legacy consumers. - /// - public IReadOnlyDictionary AsDictionary() + foreach (var (type, values) in Claims) { - var dict = new Dictionary(StringComparer.Ordinal); + var first = values.FirstOrDefault(); + if (first is not null) + dict[type] = first; + } - foreach (var (type, values) in Claims) - { - var first = values.FirstOrDefault(); - if (first is not null) - dict[type] = first; - } + return dict; + } - return dict; - } + public override bool Equals(object? obj) + { + if (obj is not ClaimsSnapshot other) + return false; - public override bool Equals(object? obj) + if (Claims.Count != other.Claims.Count) + return false; + + foreach (var (type, values) in Claims) { - if (obj is not ClaimsSnapshot other) + if (!other.Claims.TryGetValue(type, out var otherValues)) return false; - if (Claims.Count != other.Claims.Count) + if (values.Count != otherValues.Count) return false; - foreach (var (type, values) in Claims) - { - if (!other.Claims.TryGetValue(type, out var otherValues)) - return false; - - if (values.Count != otherValues.Count) - return false; - - if (!values.All(v => otherValues.Contains(v))) - return false; - } - - return true; + if (!values.All(v => otherValues.Contains(v))) + return false; } - public override int GetHashCode() + return true; + } + + public override int GetHashCode() + { + unchecked { - unchecked + int hash = 17; + + foreach (var (type, values) in Claims.OrderBy(x => x.Key)) { - int hash = 17; + hash = hash * 23 + type.GetHashCode(); - foreach (var (type, values) in Claims.OrderBy(x => x.Key)) + foreach (var value in values.OrderBy(v => v)) { - hash = hash * 23 + type.GetHashCode(); - - foreach (var value in values.OrderBy(v => v)) - { - hash = hash * 23 + value.GetHashCode(); - } + hash = hash * 23 + value.GetHashCode(); } - - return hash; } + + return hash; } + } - public static ClaimsSnapshot From(params (string Type, string Value)[] claims) - { - var dict = new Dictionary>(StringComparer.Ordinal); + public static ClaimsSnapshot From(params (string Type, string Value)[] claims) + { + var dict = new Dictionary>(StringComparer.Ordinal); - foreach (var (type, value) in claims) + foreach (var (type, value) in claims) + { + if (!dict.TryGetValue(type, out var set)) { - if (!dict.TryGetValue(type, out var set)) - { - set = new HashSet(StringComparer.Ordinal); - dict[type] = set; - } - - set.Add(value); + set = new HashSet(StringComparer.Ordinal); + dict[type] = set; } - return new ClaimsSnapshot(dict.ToDictionary(kv => kv.Key, kv => (IReadOnlyCollection)kv.Value.ToArray(), StringComparer.Ordinal)); + set.Add(value); } - public ClaimsSnapshot With(params (string Type, string Value)[] claims) - { - if (claims.Length == 0) - return this; + return new ClaimsSnapshot(dict.ToDictionary(kv => kv.Key, kv => (IReadOnlyCollection)kv.Value.ToArray(), StringComparer.Ordinal)); + } - var dict = Claims.ToDictionary(kv => kv.Key, kv => new HashSet(kv.Value, StringComparer.Ordinal), StringComparer.Ordinal); + public ClaimsSnapshot With(params (string Type, string Value)[] claims) + { + if (claims.Length == 0) + return this; - foreach (var (type, value) in claims) - { - if (!dict.TryGetValue(type, out var set)) - { - set = new HashSet(StringComparer.Ordinal); - dict[type] = set; - } + var dict = Claims.ToDictionary(kv => kv.Key, kv => new HashSet(kv.Value, StringComparer.Ordinal), StringComparer.Ordinal); - set.Add(value); + foreach (var (type, value) in claims) + { + if (!dict.TryGetValue(type, out var set)) + { + set = new HashSet(StringComparer.Ordinal); + dict[type] = set; } - return new ClaimsSnapshot(dict.ToDictionary(kv => kv.Key, kv => (IReadOnlyCollection)kv.Value.ToArray(), StringComparer.Ordinal)); + set.Add(value); } - public ClaimsSnapshot Merge(ClaimsSnapshot other) - { - if (other is null || other.Claims.Count == 0) - return this; + return new ClaimsSnapshot(dict.ToDictionary(kv => kv.Key, kv => (IReadOnlyCollection)kv.Value.ToArray(), StringComparer.Ordinal)); + } - if (Claims.Count == 0) - return other; + public ClaimsSnapshot Merge(ClaimsSnapshot other) + { + if (other is null || other.Claims.Count == 0) + return this; - var dict = Claims.ToDictionary(kv => kv.Key, kv => new HashSet(kv.Value, StringComparer.Ordinal), StringComparer.Ordinal); + if (Claims.Count == 0) + return other; - foreach (var (type, values) in other.Claims) - { - if (!dict.TryGetValue(type, out var set)) - { - set = new HashSet(StringComparer.Ordinal); - dict[type] = set; - } + var dict = Claims.ToDictionary(kv => kv.Key, kv => new HashSet(kv.Value, StringComparer.Ordinal), StringComparer.Ordinal); - foreach (var value in values) - set.Add(value); + foreach (var (type, values) in other.Claims) + { + if (!dict.TryGetValue(type, out var set)) + { + set = new HashSet(StringComparer.Ordinal); + dict[type] = set; } - return new ClaimsSnapshot(dict.ToDictionary(kv => kv.Key, kv => (IReadOnlyCollection)kv.Value.ToArray(), StringComparer.Ordinal)); + foreach (var value in values) + set.Add(value); } - public static ClaimsSnapshotBuilder Create() => new ClaimsSnapshotBuilder(); - + return new ClaimsSnapshot(dict.ToDictionary(kv => kv.Key, kv => (IReadOnlyCollection)kv.Value.ToArray(), StringComparer.Ordinal)); } + + public static ClaimsSnapshotBuilder Create() => new ClaimsSnapshotBuilder(); + } diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Session/ISession.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Session/ISession.cs index ac2f975a..9de8c16a 100644 --- a/src/CodeBeam.UltimateAuth.Core/Domain/Session/ISession.cs +++ b/src/CodeBeam.UltimateAuth.Core/Domain/Session/ISession.cs @@ -1,86 +1,83 @@ -using CodeBeam.UltimateAuth.Core.Contracts; - -namespace CodeBeam.UltimateAuth.Core.Domain +namespace CodeBeam.UltimateAuth.Core.Domain; + +/// +/// Represents a single authentication session belonging to a user. +/// Sessions are immutable, security-critical units used for validation, +/// sliding expiration, revocation, and device analytics. +/// +public interface ISession { /// - /// Represents a single authentication session belonging to a user. - /// Sessions are immutable, security-critical units used for validation, - /// sliding expiration, revocation, and device analytics. + /// Gets the unique identifier of the session. + /// + AuthSessionId SessionId { get; } + + string? TenantId { get; } + + /// + /// Gets the identifier of the user who owns this session. /// - public interface ISession - { - /// - /// Gets the unique identifier of the session. - /// - AuthSessionId SessionId { get; } - - string? TenantId { get; } - - /// - /// Gets the identifier of the user who owns this session. - /// - UserKey UserKey { get; } - - SessionChainId ChainId { get; } - - /// - /// Gets the timestamp when this session was originally created. - /// - DateTimeOffset CreatedAt { get; } - - /// - /// Gets the timestamp when the session becomes invalid due to expiration. - /// - DateTimeOffset ExpiresAt { get; } - - /// - /// Gets the timestamp of the last successful usage. - /// Used when evaluating sliding expiration policies. - /// - DateTimeOffset? LastSeenAt { get; } - - /// - /// Gets a value indicating whether this session has been explicitly revoked. - /// - bool IsRevoked { get; } - - /// - /// Gets the timestamp when the session was revoked, if applicable. - /// - DateTimeOffset? RevokedAt { get; } - - /// - /// Gets the user's security version at the moment of session creation. - /// If the stored version does not match the user's current version, - /// the session becomes invalid (e.g., after password or MFA reset). - /// - long SecurityVersionAtCreation { get; } - - /// - /// Gets metadata describing the client device that created the session. - /// Includes platform, OS, IP address, fingerprint, and more. - /// - DeviceContext Device { get; } - - ClaimsSnapshot Claims { get; } - - /// - /// Gets session-scoped metadata used for application-specific extensions, - /// such as tenant data, app version, locale, or CSRF tokens. - /// - SessionMetadata Metadata { get; } - - /// - /// Computes the effective runtime state of the session (Active, Expired, - /// Revoked, SecurityVersionMismatch, etc.) based on the provided timestamp. - /// - /// The evaluated of this session. - SessionState GetState(DateTimeOffset at, TimeSpan? idleTimeout); - - ISession Touch(DateTimeOffset now); - ISession Revoke(DateTimeOffset at); - - ISession WithChain(SessionChainId chainId); - - } + UserKey UserKey { get; } + + SessionChainId ChainId { get; } + + /// + /// Gets the timestamp when this session was originally created. + /// + DateTimeOffset CreatedAt { get; } + + /// + /// Gets the timestamp when the session becomes invalid due to expiration. + /// + DateTimeOffset ExpiresAt { get; } + + /// + /// Gets the timestamp of the last successful usage. + /// Used when evaluating sliding expiration policies. + /// + DateTimeOffset? LastSeenAt { get; } + + /// + /// Gets a value indicating whether this session has been explicitly revoked. + /// + bool IsRevoked { get; } + + /// + /// Gets the timestamp when the session was revoked, if applicable. + /// + DateTimeOffset? RevokedAt { get; } + + /// + /// Gets the user's security version at the moment of session creation. + /// If the stored version does not match the user's current version, + /// the session becomes invalid (e.g., after password or MFA reset). + /// + long SecurityVersionAtCreation { get; } + + /// + /// Gets metadata describing the client device that created the session. + /// Includes platform, OS, IP address, fingerprint, and more. + /// + DeviceContext Device { get; } + + ClaimsSnapshot Claims { get; } + + /// + /// Gets session-scoped metadata used for application-specific extensions, + /// such as tenant data, app version, locale, or CSRF tokens. + /// + SessionMetadata Metadata { get; } + + /// + /// Computes the effective runtime state of the session (Active, Expired, + /// Revoked, SecurityVersionMismatch, etc.) based on the provided timestamp. + /// + /// The evaluated of this session. + SessionState GetState(DateTimeOffset at, TimeSpan? idleTimeout); + + ISession Touch(DateTimeOffset now); + ISession Revoke(DateTimeOffset at); + + ISession WithChain(SessionChainId chainId); + } diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Token/StoredRefreshToken.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Token/StoredRefreshToken.cs index f1592b64..792f8204 100644 --- a/src/CodeBeam.UltimateAuth.Core/Domain/Token/StoredRefreshToken.cs +++ b/src/CodeBeam.UltimateAuth.Core/Domain/Token/StoredRefreshToken.cs @@ -1,33 +1,32 @@ using System.ComponentModel.DataAnnotations.Schema; -namespace CodeBeam.UltimateAuth.Core.Domain +namespace CodeBeam.UltimateAuth.Core.Domain; + +/// +/// Represents a persisted refresh token bound to a session. +/// Stored as a hashed value for security reasons. +/// +public sealed record StoredRefreshToken { - /// - /// Represents a persisted refresh token bound to a session. - /// Stored as a hashed value for security reasons. - /// - public sealed record StoredRefreshToken - { - public string TokenHash { get; init; } = default!; + public string TokenHash { get; init; } = default!; - public string? TenantId { get; init; } + public string? TenantId { get; init; } - public required UserKey UserKey { get; init; } + public required UserKey UserKey { get; init; } - public AuthSessionId SessionId { get; init; } = default!; - public SessionChainId? ChainId { get; init; } + public AuthSessionId SessionId { get; init; } = default!; + public SessionChainId? ChainId { get; init; } - public DateTimeOffset IssuedAt { get; init; } - public DateTimeOffset ExpiresAt { get; init; } - public DateTimeOffset? RevokedAt { get; init; } + public DateTimeOffset IssuedAt { get; init; } + public DateTimeOffset ExpiresAt { get; init; } + public DateTimeOffset? RevokedAt { get; init; } - public string? ReplacedByTokenHash { get; init; } + public string? ReplacedByTokenHash { get; init; } - [NotMapped] - public bool IsRevoked => RevokedAt.HasValue; + [NotMapped] + public bool IsRevoked => RevokedAt.HasValue; - public bool IsExpired(DateTimeOffset now) => ExpiresAt <= now; + public bool IsExpired(DateTimeOffset now) => ExpiresAt <= now; - public bool IsActive(DateTimeOffset now) => !IsRevoked && !IsExpired(now) && ReplacedByTokenHash is null; - } + public bool IsActive(DateTimeOffset now) => !IsRevoked && !IsExpired(now) && ReplacedByTokenHash is null; } diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Token/UAuthJwtTokenDescriptor.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Token/UAuthJwtTokenDescriptor.cs index 3961fbdb..005d8d97 100644 --- a/src/CodeBeam.UltimateAuth.Core/Domain/Token/UAuthJwtTokenDescriptor.cs +++ b/src/CodeBeam.UltimateAuth.Core/Domain/Token/UAuthJwtTokenDescriptor.cs @@ -1,24 +1,21 @@ -using System.Security.Claims; +namespace CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Core.Domain +/// +/// Framework-agnostic JWT description used by IJwtTokenGenerator. +/// +public sealed class UAuthJwtTokenDescriptor { - /// - /// Framework-agnostic JWT description used by IJwtTokenGenerator. - /// - public sealed class UAuthJwtTokenDescriptor - { - public required string Subject { get; init; } + public required string Subject { get; init; } - public required string Issuer { get; init; } + public required string Issuer { get; init; } - public required string Audience { get; init; } + public required string Audience { get; init; } - public required DateTimeOffset IssuedAt { get; init; } - public required DateTimeOffset ExpiresAt { get; init; } - public string? TenantId { get; init; } + public required DateTimeOffset IssuedAt { get; init; } + public required DateTimeOffset ExpiresAt { get; init; } + public string? TenantId { get; init; } - public IReadOnlyDictionary? Claims { get; init; } + public IReadOnlyDictionary? Claims { get; init; } - public string? KeyId { get; init; } // kid - } + public string? KeyId { get; init; } // kid } diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/User/AuthUserRecord.cs b/src/CodeBeam.UltimateAuth.Core/Domain/User/AuthUserRecord.cs new file mode 100644 index 00000000..ab60e166 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Core/Domain/User/AuthUserRecord.cs @@ -0,0 +1,49 @@ +namespace CodeBeam.UltimateAuth.Core.Domain; + +/// +/// Represents the minimal, immutable user snapshot required by the UltimateAuth Core +/// during authentication discovery and subject binding. +/// +/// This type is NOT a domain user model. +/// It contains only normalized, opinionless fields that determine whether +/// a user can participate in authentication flows. +/// +/// AuthUserRecord is produced by the Users domain as a boundary projection +/// and is never mutated by the Core. +/// +public sealed record AuthUserRecord +{ + /// + /// Application-level user identifier. + /// + public required TUserId Id { get; init; } + + /// + /// Primary login identifier (username, email, etc). + /// Used only for discovery and uniqueness checks. + /// + public required string Identifier { get; init; } + + /// + /// Indicates whether the user is considered active for authentication purposes. + /// Domain-specific statuses are normalized into this flag by the Users domain. + /// + public required bool IsActive { get; init; } + + /// + /// Indicates whether the user is deleted. + /// Deleted users are never eligible for authentication. + /// + public required bool IsDeleted { get; init; } + + /// + /// The timestamp when the user was originally created. + /// Provided for invariant validation and auditing purposes. + /// + public required DateTimeOffset CreatedAt { get; init; } + + /// + /// The timestamp when the user was deleted, if applicable. + /// + public DateTimeOffset? DeletedAt { get; init; } +} diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/User/IAuthSubject.cs b/src/CodeBeam.UltimateAuth.Core/Domain/User/IAuthSubject.cs index 97eec361..9099cbf6 100644 --- a/src/CodeBeam.UltimateAuth.Core/Domain/User/IAuthSubject.cs +++ b/src/CodeBeam.UltimateAuth.Core/Domain/User/IAuthSubject.cs @@ -1,21 +1,20 @@ -namespace CodeBeam.UltimateAuth.Core.Domain +namespace CodeBeam.UltimateAuth.Core.Domain; + +/// +/// Represents the minimal user abstraction required by UltimateAuth. +/// Includes the unique user identifier and an optional set of claims that +/// may be used during authentication or session creation. +/// +public interface IAuthSubject { /// - /// Represents the minimal user abstraction required by UltimateAuth. - /// Includes the unique user identifier and an optional set of claims that - /// may be used during authentication or session creation. + /// Gets the unique identifier of the user. /// - public interface IAuthSubject - { - /// - /// Gets the unique identifier of the user. - /// - TUserId UserId { get; } + TUserId UserId { get; } - /// - /// Gets an optional collection of user claims that may be used to construct - /// session-level claim snapshots. Implementations may return null if no claims are available. - /// - IReadOnlyDictionary? Claims { get; } - } + /// + /// Gets an optional collection of user claims that may be used to construct + /// session-level claim snapshots. Implementations may return null if no claims are available. + /// + IReadOnlyDictionary? Claims { get; } } diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/ICurrentUser.cs b/src/CodeBeam.UltimateAuth.Core/Domain/User/ICurrentUser.cs similarity index 100% rename from src/CodeBeam.UltimateAuth.Core/Domain/ICurrentUser.cs rename to src/CodeBeam.UltimateAuth.Core/Domain/User/ICurrentUser.cs diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/User/UserKey.cs b/src/CodeBeam.UltimateAuth.Core/Domain/User/UserKey.cs index 3e42de9d..e45d5220 100644 --- a/src/CodeBeam.UltimateAuth.Core/Domain/User/UserKey.cs +++ b/src/CodeBeam.UltimateAuth.Core/Domain/User/UserKey.cs @@ -1,69 +1,67 @@ using CodeBeam.UltimateAuth.Core.Infrastructure; using System.Text.Json.Serialization; -namespace CodeBeam.UltimateAuth.Core.Domain +namespace CodeBeam.UltimateAuth.Core.Domain; + +[JsonConverter(typeof(UserKeyJsonConverter))] +public readonly record struct UserKey : IParsable { - [JsonConverter(typeof(UserKeyJsonConverter))] - public readonly record struct UserKey : IParsable - { - public string Value { get; } + public string Value { get; } - private UserKey(string value) - { - Value = value; - } + private UserKey(string value) + { + Value = value; + } - /// - /// Creates a UserKey from a GUID (default and recommended). - /// - public static UserKey FromGuid(Guid value) => new(value.ToString("N")); + /// + /// Creates a UserKey from a GUID (default and recommended). + /// + public static UserKey FromGuid(Guid value) => new(value.ToString("N")); - /// - /// Creates a UserKey from a canonical string. - /// Caller is responsible for stability and uniqueness. - /// - public static UserKey FromString(string value) - { - if (string.IsNullOrWhiteSpace(value)) - throw new ArgumentException("UserKey cannot be empty.", nameof(value)); + /// + /// Creates a UserKey from a canonical string. + /// Caller is responsible for stability and uniqueness. + /// + public static UserKey FromString(string value) + { + if (string.IsNullOrWhiteSpace(value)) + throw new ArgumentException("UserKey cannot be empty.", nameof(value)); - return new UserKey(value); - } + return new UserKey(value); + } - /// - /// Generates a new GUID-based UserKey. - /// - public static UserKey New() => FromGuid(Guid.NewGuid()); + /// + /// Generates a new GUID-based UserKey. + /// + public static UserKey New() => FromGuid(Guid.NewGuid()); - public static bool TryParse(string? s, IFormatProvider? provider, out UserKey result) + public static bool TryParse(string? s, IFormatProvider? provider, out UserKey result) + { + if (string.IsNullOrWhiteSpace(s)) { - if (string.IsNullOrWhiteSpace(s)) - { - result = default; - return false; - } - - if (Guid.TryParse(s, out var guid)) - { - result = FromGuid(guid); - return true; - } - - result = FromString(s); - return true; + result = default; + return false; } - public static UserKey Parse(string s, IFormatProvider? provider) + if (Guid.TryParse(s, out var guid)) { - if (!TryParse(s, provider, out var result)) - throw new FormatException($"Invalid UserKey value: '{s}'"); - - return result; + result = FromGuid(guid); + return true; } - public override string ToString() => Value; + result = FromString(s); + return true; + } - public static implicit operator string(UserKey key) => key.Value; + public static UserKey Parse(string s, IFormatProvider? provider) + { + if (!TryParse(s, provider, out var result)) + throw new FormatException($"Invalid UserKey value: '{s}'"); + + return result; } + public override string ToString() => Value; + + public static implicit operator string(UserKey key) => key.Value; } diff --git a/src/CodeBeam.UltimateAuth.Core/Extensions/ClaimsSnapshotExtensions.cs b/src/CodeBeam.UltimateAuth.Core/Extensions/ClaimsSnapshotExtensions.cs index fa7bc2f2..d54703ec 100644 --- a/src/CodeBeam.UltimateAuth.Core/Extensions/ClaimsSnapshotExtensions.cs +++ b/src/CodeBeam.UltimateAuth.Core/Extensions/ClaimsSnapshotExtensions.cs @@ -1,61 +1,60 @@ using CodeBeam.UltimateAuth.Core.Domain; using System.Security.Claims; -namespace CodeBeam.UltimateAuth.Core.Extensions +namespace CodeBeam.UltimateAuth.Core.Extensions; + +public static class ClaimsSnapshotExtensions { - public static class ClaimsSnapshotExtensions + /// + /// Converts a ClaimsSnapshot into an ASP.NET Core ClaimsPrincipal. + /// + public static ClaimsPrincipal ToClaimsPrincipal(this ClaimsSnapshot snapshot, string authenticationType = "UltimateAuth") { - /// - /// Converts a ClaimsSnapshot into an ASP.NET Core ClaimsPrincipal. - /// - public static ClaimsPrincipal ToClaimsPrincipal(this ClaimsSnapshot snapshot, string authenticationType = "UltimateAuth") - { - if (snapshot == null) - return new ClaimsPrincipal(new ClaimsIdentity()); + if (snapshot == null) + return new ClaimsPrincipal(new ClaimsIdentity()); - var claims = snapshot.Claims.SelectMany(kv => kv.Value.Select(value => new Claim(kv.Key, value))); + var claims = snapshot.Claims.SelectMany(kv => kv.Value.Select(value => new Claim(kv.Key, value))); - var identity = new ClaimsIdentity(claims, authenticationType); - return new ClaimsPrincipal(identity); - } + var identity = new ClaimsIdentity(claims, authenticationType); + return new ClaimsPrincipal(identity); + } - /// - /// Converts an ASP.NET Core ClaimsPrincipal into a ClaimsSnapshot. - /// - public static ClaimsSnapshot ToClaimsSnapshot(this ClaimsPrincipal principal) - { - if (principal is null) - return ClaimsSnapshot.Empty; + /// + /// Converts an ASP.NET Core ClaimsPrincipal into a ClaimsSnapshot. + /// + public static ClaimsSnapshot ToClaimsSnapshot(this ClaimsPrincipal principal) + { + if (principal is null) + return ClaimsSnapshot.Empty; - if (principal.Identity?.IsAuthenticated != true) - return ClaimsSnapshot.Empty; + if (principal.Identity?.IsAuthenticated != true) + return ClaimsSnapshot.Empty; - var dict = new Dictionary>(StringComparer.Ordinal); + var dict = new Dictionary>(StringComparer.Ordinal); - foreach (var claim in principal.Claims) + foreach (var claim in principal.Claims) + { + if (!dict.TryGetValue(claim.Type, out var set)) { - if (!dict.TryGetValue(claim.Type, out var set)) - { - set = new HashSet(StringComparer.Ordinal); - dict[claim.Type] = set; - } - - set.Add(claim.Value); + set = new HashSet(StringComparer.Ordinal); + dict[claim.Type] = set; } - return new ClaimsSnapshot(dict.ToDictionary(kv => kv.Key, kv => (IReadOnlyCollection)kv.Value.ToArray(), StringComparer.Ordinal)); + set.Add(claim.Value); } - public static IEnumerable ToClaims(this ClaimsSnapshot snapshot) + return new ClaimsSnapshot(dict.ToDictionary(kv => kv.Key, kv => (IReadOnlyCollection)kv.Value.ToArray(), StringComparer.Ordinal)); + } + + public static IEnumerable ToClaims(this ClaimsSnapshot snapshot) + { + foreach (var (type, values) in snapshot.Claims) { - foreach (var (type, values) in snapshot.Claims) + foreach (var value in values) { - foreach (var value in values) - { - yield return new Claim(type, value); - } + yield return new Claim(type, value); } } - } + } diff --git a/src/CodeBeam.UltimateAuth.Core/Extensions/UltimateAuthServiceCollectionExtensions.cs b/src/CodeBeam.UltimateAuth.Core/Extensions/UltimateAuthServiceCollectionExtensions.cs index a8a7f035..e9674428 100644 --- a/src/CodeBeam.UltimateAuth.Core/Extensions/UltimateAuthServiceCollectionExtensions.cs +++ b/src/CodeBeam.UltimateAuth.Core/Extensions/UltimateAuthServiceCollectionExtensions.cs @@ -1,5 +1,4 @@ using CodeBeam.UltimateAuth.Core.Abstractions; -using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Infrastructure; using CodeBeam.UltimateAuth.Core.Options; using CodeBeam.UltimateAuth.Core.Runtime; @@ -8,88 +7,86 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; -namespace CodeBeam.UltimateAuth.Core.Extensions +namespace CodeBeam.UltimateAuth.Core.Extensions; + +// TODO: Check it before stable release +/// +/// Provides extension methods for registering UltimateAuth core services into +/// the application's dependency injection container. +/// +/// These methods configure options, validators, converters, and factories required +/// for the authentication subsystem. +/// +/// IMPORTANT: +/// This extension registers only CORE services — session stores, token factories, +/// PKCE handlers, and any server-specific logic must be added from the Server package +/// (e.g., AddUltimateAuthServer()). +/// +public static class UltimateAuthServiceCollectionExtensions { - // TODO: Check it before stable release /// - /// Provides extension methods for registering UltimateAuth core services into - /// the application's dependency injection container. - /// - /// These methods configure options, validators, converters, and factories required - /// for the authentication subsystem. + /// Registers UltimateAuth services using configuration binding (e.g., appsettings.json). /// - /// IMPORTANT: - /// This extension registers only CORE services — session stores, token factories, - /// PKCE handlers, and any server-specific logic must be added from the Server package - /// (e.g., AddUltimateAuthServer()). + /// The provided configuration section must contain valid UltimateAuthOptions and nested + /// Session, Token, PKCE, and MultiTenant configuration sections. Validation occurs + /// at application startup via IValidateOptions. /// - public static class UltimateAuthServiceCollectionExtensions + public static IServiceCollection AddUltimateAuth(this IServiceCollection services, IConfiguration configurationSection) { - /// - /// Registers UltimateAuth services using configuration binding (e.g., appsettings.json). - /// - /// The provided configuration section must contain valid UltimateAuthOptions and nested - /// Session, Token, PKCE, and MultiTenant configuration sections. Validation occurs - /// at application startup via IValidateOptions. - /// - public static IServiceCollection AddUltimateAuth(this IServiceCollection services, IConfiguration configurationSection) - { - services.Configure(configurationSection); - return services.AddUltimateAuthInternal(); - } - - /// - /// Registers UltimateAuth services using programmatic configuration. - /// This is useful when settings are derived dynamically or are not stored - /// in appsettings.json. - /// - public static IServiceCollection AddUltimateAuth(this IServiceCollection services, Action configure) - { - services.Configure(configure); - return services.AddUltimateAuthInternal(); - } + services.Configure(configurationSection); + return services.AddUltimateAuthInternal(); + } - /// - /// Registers UltimateAuth services using default empty configuration. - /// Intended for advanced or fully manual scenarios where options will be - /// configured later or overridden by the server layer. - /// - public static IServiceCollection AddUltimateAuth(this IServiceCollection services) - { - services.Configure(_ => { }); - return services.AddUltimateAuthInternal(); - } + /// + /// Registers UltimateAuth services using programmatic configuration. + /// This is useful when settings are derived dynamically or are not stored + /// in appsettings.json. + /// + public static IServiceCollection AddUltimateAuth(this IServiceCollection services, Action configure) + { + services.Configure(configure); + return services.AddUltimateAuthInternal(); + } - /// - /// Internal shared registration pipeline invoked by all AddUltimateAuth overloads. - /// Registers validators, user ID converters, and placeholder factories. - /// Core-level invariant validation. - /// Server layer may add additional validators. - /// NOTE: - /// This method does NOT register session stores or server-side services. - /// A server project must explicitly call: - /// - /// services.AddUltimateAuthSessionStore'TStore'(); - /// - /// to provide a concrete ISessionStore implementation. - /// - private static IServiceCollection AddUltimateAuthInternal(this IServiceCollection services) - { - services.AddSingleton, UAuthOptionsValidator>(); - services.AddSingleton, UAuthSessionOptionsValidator>(); - services.AddSingleton, UAuthTokenOptionsValidator>(); - services.AddSingleton, UAuthPkceOptionsValidator>(); - services.AddSingleton, UAuthMultiTenantOptionsValidator>(); + /// + /// Registers UltimateAuth services using default empty configuration. + /// Intended for advanced or fully manual scenarios where options will be + /// configured later or overridden by the server layer. + /// + public static IServiceCollection AddUltimateAuth(this IServiceCollection services) + { + services.Configure(_ => { }); + return services.AddUltimateAuthInternal(); + } - // Nested options are bound automatically by the options binder. - // Server layer may override or extend these settings. + /// + /// Internal shared registration pipeline invoked by all AddUltimateAuth overloads. + /// Registers validators, user ID converters, and placeholder factories. + /// Core-level invariant validation. + /// Server layer may add additional validators. + /// NOTE: + /// This method does NOT register session stores or server-side services. + /// A server project must explicitly call: + /// + /// services.AddUltimateAuthSessionStore'TStore'(); + /// + /// to provide a concrete ISessionStore implementation. + /// + private static IServiceCollection AddUltimateAuthInternal(this IServiceCollection services) + { + services.AddSingleton, UAuthOptionsValidator>(); + services.AddSingleton, UAuthSessionOptionsValidator>(); + services.AddSingleton, UAuthTokenOptionsValidator>(); + services.AddSingleton, UAuthPkceOptionsValidator>(); + services.AddSingleton, UAuthMultiTenantOptionsValidator>(); - services.AddSingleton(); - services.TryAddSingleton(); - services.TryAddSingleton(); + // Nested options are bound automatically by the options binder. + // Server layer may override or extend these settings. - return services; - } + services.AddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + return services; } } diff --git a/src/CodeBeam.UltimateAuth.Core/Extensions/UltimateAuthSessionStoreExtensions.cs b/src/CodeBeam.UltimateAuth.Core/Extensions/UltimateAuthSessionStoreExtensions.cs deleted file mode 100644 index cf96f955..00000000 --- a/src/CodeBeam.UltimateAuth.Core/Extensions/UltimateAuthSessionStoreExtensions.cs +++ /dev/null @@ -1,102 +0,0 @@ -//using CodeBeam.UltimateAuth.Core.Abstractions; -//using Microsoft.Extensions.DependencyInjection; -//using Microsoft.Extensions.DependencyInjection.Extensions; - -//namespace CodeBeam.UltimateAuth.Core.Extensions -//{ -// /// -// /// Provides extension methods for registering a concrete -// /// implementation into the application's dependency injection container. -// /// -// /// UltimateAuth requires exactly one session store implementation that determines -// /// how sessions, chains, and roots are persisted (e.g., EF Core, Dapper, Redis, MongoDB). -// /// This extension performs automatic generic type resolution and registers the correct -// /// ISessionStore<TUserId> for the application's user ID type. -// /// -// /// The method enforces that the provided store implements ISessionStore'TUserId';. -// /// If the type cannot be determined, an exception is thrown to prevent misconfiguration. -// /// -// public static class UltimateAuthSessionStoreExtensions -// { -// /// -// /// Registers a custom session store implementation for UltimateAuth. -// /// The supplied must implement ISessionStore'TUserId'; -// /// exactly once with a single TUserId generic argument. -// /// -// /// After registration, the internal session store factory resolves the correct -// /// ISessionStore instance at runtime for the active tenant and TUserId type. -// /// -// /// The concrete session store implementation. -// public static IServiceCollection AddUltimateAuthSessionStore(this IServiceCollection services) -// where TStore : class -// { -// var storeInterface = typeof(TStore) -// .GetInterfaces() -// .FirstOrDefault(i => -// i.IsGenericType && -// i.GetGenericTypeDefinition() == typeof(ISessionStoreKernel<>)); - -// if (storeInterface is null) -// { -// throw new InvalidOperationException( -// $"{typeof(TStore).Name} must implement ISessionStoreKernel."); -// } - -// var userIdType = storeInterface.GetGenericArguments()[0]; -// var typedInterface = typeof(ISessionStoreKernel<>).MakeGenericType(userIdType); - -// services.TryAddScoped(typedInterface, typeof(TStore)); - -// services.AddSingleton(sp => -// new GenericSessionStoreFactory(sp, userIdType)); - -// return services; -// } -// } - -// /// -// /// Default session store factory used by UltimateAuth to dynamically create -// /// the correct ISessionStore<TUserId> implementation at runtime. -// /// -// /// This factory ensures type safety by validating the requested TUserId against -// /// the registered session store’s user ID type. Attempting to resolve a mismatched -// /// TUserId results in a descriptive exception to prevent silent misconfiguration. -// /// -// /// Tenant ID is passed through so that multi-tenant implementations can perform -// /// tenant-aware routing, filtering, or partition-based selection. -// /// -// internal sealed class GenericSessionStoreFactory : ISessionStoreFactory -// { -// private readonly IServiceProvider _sp; -// private readonly Type _userIdType; - -// /// -// /// Initializes a new instance of the class. -// /// -// public GenericSessionStoreFactory(IServiceProvider sp, Type userIdType) -// { -// _sp = sp; -// _userIdType = userIdType; -// } - -// /// -// /// Creates and returns the registered ISessionStore<TUserId> implementation -// /// for the specified tenant and user ID type. -// /// Throws if the requested TUserId does not match the registered store's type. -// /// -// public ISessionStoreKernel Create(string? tenantId) -// { -// if (typeof(TUserId) != _userIdType) -// { -// throw new InvalidOperationException( -// $"SessionStore registered for TUserId='{_userIdType.Name}', " + -// $"but requested with TUserId='{typeof(TUserId).Name}'."); -// } - -// var typed = typeof(ISessionStoreKernel<>).MakeGenericType(_userIdType); -// var store = _sp.GetRequiredService(typed); - -// return (ISessionStoreKernel)store; -// } -// } -//} diff --git a/src/CodeBeam.UltimateAuth.Core/Extensions/UserIdConverterRegistrationExtensions.cs b/src/CodeBeam.UltimateAuth.Core/Extensions/UserIdConverterRegistrationExtensions.cs index 6c40f103..8c87ae75 100644 --- a/src/CodeBeam.UltimateAuth.Core/Extensions/UserIdConverterRegistrationExtensions.cs +++ b/src/CodeBeam.UltimateAuth.Core/Extensions/UserIdConverterRegistrationExtensions.cs @@ -1,64 +1,63 @@ using Microsoft.Extensions.DependencyInjection; using CodeBeam.UltimateAuth.Core.Abstractions; -namespace CodeBeam.UltimateAuth.Core.Extensions +namespace CodeBeam.UltimateAuth.Core.Extensions; + +// TODO: Decide converter obligatory or optional on boundary UserKey TUserId conversion +/// +/// Provides extension methods for registering custom +/// implementations into the dependency injection container. +/// +/// UltimateAuth internally relies on user ID normalization for: +/// - session store lookups +/// - token generation and validation +/// - logging and diagnostics +/// - multi-tenant user routing +/// +/// By default, a simple "UAuthUserIdConverter{TUserId}" is used, but +/// applications may override this with stronger or domain-specific converters +/// (e.g., ULIDs, Snowflakes, encrypted identifiers, composite keys). +/// +public static class UserIdConverterRegistrationExtensions { /// - /// Provides extension methods for registering custom - /// implementations into the dependency injection container. + /// Registers a custom implementation. /// - /// UltimateAuth internally relies on user ID normalization for: - /// - session store lookups - /// - token generation and validation - /// - logging and diagnostics - /// - multi-tenant user routing + /// Use this overload when you want to supply your own converter type. + /// Ideal for stateless converters that simply translate user IDs to/from + /// string or byte representations (database keys, token subjects, etc.). /// - /// By default, a simple "UAuthUserIdConverter{TUserId}" is used, but - /// applications may override this with stronger or domain-specific converters - /// (e.g., ULIDs, Snowflakes, encrypted identifiers, composite keys). + /// The converter is registered as a singleton because: + /// - conversion is pure and stateless, + /// - high-performance lookup is required, + /// - converters are reused across multiple services (tokens, sessions, stores). /// - public static class UserIdConverterRegistrationExtensions + /// The application's user ID type. + /// The custom converter implementation. + public static IServiceCollection AddUltimateAuthUserIdConverter( + this IServiceCollection services) + where TConverter : class, IUserIdConverter { - /// - /// Registers a custom implementation. - /// - /// Use this overload when you want to supply your own converter type. - /// Ideal for stateless converters that simply translate user IDs to/from - /// string or byte representations (database keys, token subjects, etc.). - /// - /// The converter is registered as a singleton because: - /// - conversion is pure and stateless, - /// - high-performance lookup is required, - /// - converters are reused across multiple services (tokens, sessions, stores). - /// - /// The application's user ID type. - /// The custom converter implementation. - public static IServiceCollection AddUltimateAuthUserIdConverter( - this IServiceCollection services) - where TConverter : class, IUserIdConverter - { - services.AddSingleton, TConverter>(); - return services; - } + services.AddSingleton, TConverter>(); + return services; + } #pragma warning disable CS1573 - /// - /// Registers a specific instance of . - /// - /// Use this overload when: - /// - the converter requires configuration or external initialization, - /// - the converter contains state (e.g., encryption keys, salt pools), - /// - multiple converters need DI-managed lifetime control. - /// - /// The application's user ID type. - /// The converter instance to register. - public static IServiceCollection AddUltimateAuthUserIdConverter( - this IServiceCollection services, - IUserIdConverter instance) - { - services.AddSingleton(instance); - return services; - } + /// + /// Registers a specific instance of . + /// + /// Use this overload when: + /// - the converter requires configuration or external initialization, + /// - the converter contains state (e.g., encryption keys, salt pools), + /// - multiple converters need DI-managed lifetime control. + /// + /// The application's user ID type. + /// The converter instance to register. + public static IServiceCollection AddUltimateAuthUserIdConverter( + this IServiceCollection services, + IUserIdConverter instance) + { + services.AddSingleton(instance); + return services; } - } diff --git a/src/CodeBeam.UltimateAuth.Core/Infrastructure/AuthUserRecord.cs b/src/CodeBeam.UltimateAuth.Core/Infrastructure/AuthUserRecord.cs deleted file mode 100644 index 79885c21..00000000 --- a/src/CodeBeam.UltimateAuth.Core/Infrastructure/AuthUserRecord.cs +++ /dev/null @@ -1,50 +0,0 @@ -namespace CodeBeam.UltimateAuth.Core.Infrastructure -{ - /// - /// Represents the minimal, immutable user snapshot required by the UltimateAuth Core - /// during authentication discovery and subject binding. - /// - /// This type is NOT a domain user model. - /// It contains only normalized, opinionless fields that determine whether - /// a user can participate in authentication flows. - /// - /// AuthUserRecord is produced by the Users domain as a boundary projection - /// and is never mutated by the Core. - /// - public sealed record AuthUserRecord - { - /// - /// Application-level user identifier. - /// - public required TUserId Id { get; init; } - - /// - /// Primary login identifier (username, email, etc). - /// Used only for discovery and uniqueness checks. - /// - public required string Identifier { get; init; } - - /// - /// Indicates whether the user is considered active for authentication purposes. - /// Domain-specific statuses are normalized into this flag by the Users domain. - /// - public required bool IsActive { get; init; } - - /// - /// Indicates whether the user is deleted. - /// Deleted users are never eligible for authentication. - /// - public required bool IsDeleted { get; init; } - - /// - /// The timestamp when the user was originally created. - /// Provided for invariant validation and auditing purposes. - /// - public required DateTimeOffset CreatedAt { get; init; } - - /// - /// The timestamp when the user was deleted, if applicable. - /// - public DateTimeOffset? DeletedAt { get; init; } - } -} diff --git a/src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/DefaultAuthAuthority.cs b/src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/DefaultAuthAuthority.cs index 1b826920..e4084ece 100644 --- a/src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/DefaultAuthAuthority.cs +++ b/src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/DefaultAuthAuthority.cs @@ -1,50 +1,49 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Contracts; -namespace CodeBeam.UltimateAuth.Core.Infrastructure +namespace CodeBeam.UltimateAuth.Core.Infrastructure; + +public sealed class DefaultAuthAuthority : IAuthAuthority { - public sealed class DefaultAuthAuthority : IAuthAuthority + private readonly IEnumerable _invariants; + private readonly IEnumerable _policies; + + public DefaultAuthAuthority(IEnumerable invariants, IEnumerable policies) { - private readonly IEnumerable _invariants; - private readonly IEnumerable _policies; + _invariants = invariants ?? Array.Empty(); + _policies = policies ?? Array.Empty(); + } - public DefaultAuthAuthority(IEnumerable invariants, IEnumerable policies) + public AccessDecisionResult Decide(AuthContext context, IEnumerable? policies = null) + { + foreach (var invariant in _invariants) { - _invariants = invariants ?? Array.Empty(); - _policies = policies ?? Array.Empty(); + var result = invariant.Decide(context); + if (!result.IsAllowed) + return result; } - public AccessDecisionResult Decide(AuthContext context, IEnumerable? policies = null) - { - foreach (var invariant in _invariants) - { - var result = invariant.Decide(context); - if (!result.IsAllowed) - return result; - } + bool challenged = false; - bool challenged = false; + var effectivePolicies = _policies.Concat(policies ?? Enumerable.Empty()); - var effectivePolicies = _policies.Concat(policies ?? Enumerable.Empty()); - - foreach (var policy in effectivePolicies) - { - if (!policy.AppliesTo(context)) - continue; - - var result = policy.Decide(context); + foreach (var policy in effectivePolicies) + { + if (!policy.AppliesTo(context)) + continue; - if (!result.IsAllowed) - return result; + var result = policy.Decide(context); - if (result.RequiresChallenge) - challenged = true; - } + if (!result.IsAllowed) + return result; - return challenged - ? AccessDecisionResult.Challenge("Additional verification required.") - : AccessDecisionResult.Allow(); + if (result.RequiresChallenge) + challenged = true; } + return challenged + ? AccessDecisionResult.Challenge("Additional verification required.") + : AccessDecisionResult.Allow(); } + } diff --git a/src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/DeviceMismatchPolicy.cs b/src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/DeviceMismatchPolicy.cs index 1d53f385..5de4ac5e 100644 --- a/src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/DeviceMismatchPolicy.cs +++ b/src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/DeviceMismatchPolicy.cs @@ -1,32 +1,30 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Contracts; -namespace CodeBeam.UltimateAuth.Core.Infrastructure +namespace CodeBeam.UltimateAuth.Core.Infrastructure; + +public sealed class DeviceMismatchPolicy : IAuthorityPolicy { - public sealed class DeviceMismatchPolicy : IAuthorityPolicy - { - public bool AppliesTo(AuthContext context) - => context.Device is not null; + public bool AppliesTo(AuthContext context) => context.Device is not null; - public AccessDecisionResult Decide(AuthContext context) - { - var device = context.Device; + public AccessDecisionResult Decide(AuthContext context) + { + var device = context.Device; - //if (device.IsKnownDevice) - // return AuthorizationResult.Allow(); + //if (device.IsKnownDevice) + // return AuthorizationResult.Allow(); - return context.Operation switch - { - AuthOperation.Access => - AccessDecisionResult.Deny("Access from unknown device."), + return context.Operation switch + { + AuthOperation.Access => + AccessDecisionResult.Deny("Access from unknown device."), - AuthOperation.Refresh => - AccessDecisionResult.Challenge("Device verification required."), + AuthOperation.Refresh => + AccessDecisionResult.Challenge("Device verification required."), - AuthOperation.Login => AccessDecisionResult.Allow(), // login establishes device + AuthOperation.Login => AccessDecisionResult.Allow(), // login establishes device - _ => AccessDecisionResult.Allow() - }; - } + _ => AccessDecisionResult.Allow() + }; } } diff --git a/src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/DevicePresenceInvariant.cs b/src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/DevicePresenceInvariant.cs index 5bcd7328..b9498b81 100644 --- a/src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/DevicePresenceInvariant.cs +++ b/src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/DevicePresenceInvariant.cs @@ -1,20 +1,18 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Contracts; -namespace CodeBeam.UltimateAuth.Core.Infrastructure +namespace CodeBeam.UltimateAuth.Core.Infrastructure; + +public sealed class DevicePresenceInvariant : IAuthorityInvariant { - public sealed class DevicePresenceInvariant : IAuthorityInvariant + public AccessDecisionResult Decide(AuthContext context) { - public AccessDecisionResult Decide(AuthContext context) + if (context.Operation is AuthOperation.Login or AuthOperation.Refresh) { - if (context.Operation is AuthOperation.Login or AuthOperation.Refresh) - { - if (context.Device is null) - return AccessDecisionResult.Deny("Device information is required."); - } - - return AccessDecisionResult.Allow(); + if (context.Device is null) + return AccessDecisionResult.Deny("Device information is required."); } - } + return AccessDecisionResult.Allow(); + } } diff --git a/src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/ExpiredSessionInvariant.cs b/src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/ExpiredSessionInvariant.cs index cb9e14c6..6ef97ad7 100644 --- a/src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/ExpiredSessionInvariant.cs +++ b/src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/ExpiredSessionInvariant.cs @@ -2,26 +2,25 @@ using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Core.Infrastructure +namespace CodeBeam.UltimateAuth.Core.Infrastructure; + +public sealed class ExpiredSessionInvariant : IAuthorityInvariant { - public sealed class ExpiredSessionInvariant : IAuthorityInvariant + public AccessDecisionResult Decide(AuthContext context) { - public AccessDecisionResult Decide(AuthContext context) - { - if (context.Operation == AuthOperation.Login) - return AccessDecisionResult.Allow(); - - var session = context.Session; - - if (session is null) - return AccessDecisionResult.Allow(); + if (context.Operation == AuthOperation.Login) + return AccessDecisionResult.Allow(); - if (session.State == SessionState.Expired) - { - return AccessDecisionResult.Deny("Session has expired."); - } + var session = context.Session; + if (session is null) return AccessDecisionResult.Allow(); + + if (session.State == SessionState.Expired) + { + return AccessDecisionResult.Deny("Session has expired."); } + + return AccessDecisionResult.Allow(); } } diff --git a/src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/InvalidOrRevokedSessionInvariant.cs b/src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/InvalidOrRevokedSessionInvariant.cs index 7d8fe9a5..0b971799 100644 --- a/src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/InvalidOrRevokedSessionInvariant.cs +++ b/src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/InvalidOrRevokedSessionInvariant.cs @@ -2,30 +2,29 @@ using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Core.Infrastructure +namespace CodeBeam.UltimateAuth.Core.Infrastructure; + +public sealed class InvalidOrRevokedSessionInvariant : IAuthorityInvariant { - public sealed class InvalidOrRevokedSessionInvariant : IAuthorityInvariant + public AccessDecisionResult Decide(AuthContext context) { - public AccessDecisionResult Decide(AuthContext context) - { - if (context.Operation == AuthOperation.Login) - return AccessDecisionResult.Allow(); - - var session = context.Session; + if (context.Operation == AuthOperation.Login) + return AccessDecisionResult.Allow(); - if (session is null) - return AccessDecisionResult.Deny("Session is required for this operation."); + var session = context.Session; - if (session.State == SessionState.Invalid || - session.State == SessionState.NotFound || - session.State == SessionState.Revoked || - session.State == SessionState.SecurityMismatch || - session.State == SessionState.DeviceMismatch) - { - return AccessDecisionResult.Deny($"Session state is invalid: {session.State}"); - } + if (session is null) + return AccessDecisionResult.Deny("Session is required for this operation."); - return AccessDecisionResult.Allow(); + if (session.State == SessionState.Invalid || + session.State == SessionState.NotFound || + session.State == SessionState.Revoked || + session.State == SessionState.SecurityMismatch || + session.State == SessionState.DeviceMismatch) + { + return AccessDecisionResult.Deny($"Session state is invalid: {session.State}"); } + + return AccessDecisionResult.Allow(); } } diff --git a/src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/UAuthModeOperationPolicy.cs b/src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/UAuthModeOperationPolicy.cs index 459e4ca8..d4ac9b7e 100644 --- a/src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/UAuthModeOperationPolicy.cs +++ b/src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/UAuthModeOperationPolicy.cs @@ -1,39 +1,38 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Contracts; -namespace CodeBeam.UltimateAuth.Core.Infrastructure +namespace CodeBeam.UltimateAuth.Core.Infrastructure; + +public sealed class AuthModeOperationPolicy : IAuthorityPolicy { - public sealed class AuthModeOperationPolicy : IAuthorityPolicy - { - public bool AppliesTo(AuthContext context) => true; // Applies to all contexts + public bool AppliesTo(AuthContext context) => true; // Applies to all contexts - public AccessDecisionResult Decide(AuthContext context) - { - return context.Mode switch - { - UAuthMode.PureOpaque => DecideForPureOpaque(context), - UAuthMode.PureJwt => DecideForPureJwt(context), - UAuthMode.Hybrid => AccessDecisionResult.Allow(), - UAuthMode.SemiHybrid => AccessDecisionResult.Allow(), - - _ => AccessDecisionResult.Deny("Unsupported authentication mode.") - }; - } - - private static AccessDecisionResult DecideForPureOpaque(AuthContext context) + public AccessDecisionResult Decide(AuthContext context) + { + return context.Mode switch { - if (context.Operation == AuthOperation.Refresh) - return AccessDecisionResult.Deny("Refresh operation is not supported in PureOpaque mode."); + UAuthMode.PureOpaque => DecideForPureOpaque(context), + UAuthMode.PureJwt => DecideForPureJwt(context), + UAuthMode.Hybrid => AccessDecisionResult.Allow(), + UAuthMode.SemiHybrid => AccessDecisionResult.Allow(), - return AccessDecisionResult.Allow(); - } + _ => AccessDecisionResult.Deny("Unsupported authentication mode.") + }; + } - private static AccessDecisionResult DecideForPureJwt(AuthContext context) - { - if (context.Operation == AuthOperation.Access) - return AccessDecisionResult.Deny("Session-based access is not supported in PureJwt mode."); + private static AccessDecisionResult DecideForPureOpaque(AuthContext context) + { + if (context.Operation == AuthOperation.Refresh) + return AccessDecisionResult.Deny("Refresh operation is not supported in PureOpaque mode."); + + return AccessDecisionResult.Allow(); + } + + private static AccessDecisionResult DecideForPureJwt(AuthContext context) + { + if (context.Operation == AuthOperation.Access) + return AccessDecisionResult.Deny("Session-based access is not supported in PureJwt mode."); - return AccessDecisionResult.Allow(); - } + return AccessDecisionResult.Allow(); } } diff --git a/src/CodeBeam.UltimateAuth.Core/Infrastructure/Base64Url.cs b/src/CodeBeam.UltimateAuth.Core/Infrastructure/Base64Url.cs index 48fb6c83..14b2de0d 100644 --- a/src/CodeBeam.UltimateAuth.Core/Infrastructure/Base64Url.cs +++ b/src/CodeBeam.UltimateAuth.Core/Infrastructure/Base64Url.cs @@ -1,49 +1,43 @@ -namespace CodeBeam.UltimateAuth.Core.Infrastructure +namespace CodeBeam.UltimateAuth.Core.Infrastructure; + +/// +/// Provides Base64 URL-safe encoding and decoding utilities. +/// +/// RFC 4648-compliant transformation replacing '+' → '-', '/' → '_' +/// and removing padding characters '='. Commonly used in PKCE, +/// JWT segments, and opaque token representations. +/// +public static class Base64Url { /// - /// Provides Base64 URL-safe encoding and decoding utilities. - /// - /// RFC 4648-compliant transformation replacing '+' → '-', '/' → '_' - /// and removing padding characters '='. Commonly used in PKCE, - /// JWT segments, and opaque token representations. + /// Encodes a byte array into a URL-safe Base64 string by applying + /// RFC 4648 URL-safe transformations and removing padding. /// - public static class Base64Url + /// The binary data to encode. + /// A URL-safe Base64 encoded string. + public static string Encode(byte[] input) { - /// - /// Encodes a byte array into a URL-safe Base64 string by applying - /// RFC 4648 URL-safe transformations and removing padding. - /// - /// The binary data to encode. - /// A URL-safe Base64 encoded string. - public static string Encode(byte[] input) - { - var base64 = Convert.ToBase64String(input); - return base64 - .Replace("+", "-") - .Replace("/", "_") - .Replace("=", ""); - } - - /// - /// Decodes a URL-safe Base64 string into its original binary form. - /// Automatically restores required padding before decoding. - /// - /// The URL-safe Base64 encoded string. - /// The decoded binary data. - public static byte[] Decode(string input) - { - var padded = input - .Replace("-", "+") - .Replace("_", "/"); + var base64 = Convert.ToBase64String(input); + return base64.Replace("+", "-").Replace("/", "_").Replace("=", ""); + } - switch (padded.Length % 4) - { - case 2: padded += "=="; break; - case 3: padded += "="; break; - } + /// + /// Decodes a URL-safe Base64 string into its original binary form. + /// Automatically restores required padding before decoding. + /// + /// The URL-safe Base64 encoded string. + /// The decoded binary data. + public static byte[] Decode(string input) + { + var padded = input.Replace("-", "+").Replace("_", "/"); - return Convert.FromBase64String(padded); + switch (padded.Length % 4) + { + case 2: padded += "=="; break; + case 3: padded += "="; break; } + return Convert.FromBase64String(padded); } + } diff --git a/src/CodeBeam.UltimateAuth.Core/Infrastructure/GuidUserIdFactory.cs b/src/CodeBeam.UltimateAuth.Core/Infrastructure/GuidUserIdFactory.cs index afe906be..b96d09a0 100644 --- a/src/CodeBeam.UltimateAuth.Core/Infrastructure/GuidUserIdFactory.cs +++ b/src/CodeBeam.UltimateAuth.Core/Infrastructure/GuidUserIdFactory.cs @@ -1,9 +1,8 @@ using CodeBeam.UltimateAuth.Core.Abstractions; -namespace CodeBeam.UltimateAuth.Core.Infrastructure +namespace CodeBeam.UltimateAuth.Core.Infrastructure; + +public sealed class GuidUserIdFactory : IUserIdFactory { - public sealed class GuidUserIdFactory : IUserIdFactory - { - public Guid Create() => Guid.NewGuid(); - } + public Guid Create() => Guid.NewGuid(); } diff --git a/src/CodeBeam.UltimateAuth.Core/Infrastructure/IInMemoryUserIdProvider.cs b/src/CodeBeam.UltimateAuth.Core/Infrastructure/IInMemoryUserIdProvider.cs index 3c61b2fa..57a25023 100644 --- a/src/CodeBeam.UltimateAuth.Core/Infrastructure/IInMemoryUserIdProvider.cs +++ b/src/CodeBeam.UltimateAuth.Core/Infrastructure/IInMemoryUserIdProvider.cs @@ -1,8 +1,7 @@ -namespace CodeBeam.UltimateAuth.Core.Infrastructure +namespace CodeBeam.UltimateAuth.Core.Infrastructure; + +public interface IInMemoryUserIdProvider { - public interface IInMemoryUserIdProvider - { - TUserId GetAdminUserId(); - TUserId GetUserUserId(); - } + TUserId GetAdminUserId(); + TUserId GetUserUserId(); } diff --git a/src/CodeBeam.UltimateAuth.Core/Infrastructure/NoOpAccessTokenIdStore.cs b/src/CodeBeam.UltimateAuth.Core/Infrastructure/NoOpAccessTokenIdStore.cs index ebb56c66..73514ccf 100644 --- a/src/CodeBeam.UltimateAuth.Core/Infrastructure/NoOpAccessTokenIdStore.cs +++ b/src/CodeBeam.UltimateAuth.Core/Infrastructure/NoOpAccessTokenIdStore.cs @@ -1,16 +1,15 @@ using CodeBeam.UltimateAuth.Core.Abstractions; -namespace CodeBeam.UltimateAuth.Core.Infrastructure +namespace CodeBeam.UltimateAuth.Core.Infrastructure; + +internal sealed class NoopAccessTokenIdStore : IAccessTokenIdStore { - internal sealed class NoopAccessTokenIdStore : IAccessTokenIdStore - { - public Task StoreAsync(string? tenantId, string jti, DateTimeOffset expiresAt, CancellationToken ct = default) - => Task.CompletedTask; + public Task StoreAsync(string? tenantId, string jti, DateTimeOffset expiresAt, CancellationToken ct = default) + => Task.CompletedTask; - public Task IsRevokedAsync(string? tenantId, string jti, CancellationToken ct = default) - => Task.FromResult(false); + public Task IsRevokedAsync(string? tenantId, string jti, CancellationToken ct = default) + => Task.FromResult(false); - public Task RevokeAsync(string? tenantId, string jti, DateTimeOffset revokedAt, CancellationToken ct = default) - => Task.CompletedTask; - } + public Task RevokeAsync(string? tenantId, string jti, DateTimeOffset revokedAt, CancellationToken ct = default) + => Task.CompletedTask; } diff --git a/src/CodeBeam.UltimateAuth.Core/Infrastructure/RandomIdGenerator.cs b/src/CodeBeam.UltimateAuth.Core/Infrastructure/RandomIdGenerator.cs deleted file mode 100644 index b2faa234..00000000 --- a/src/CodeBeam.UltimateAuth.Core/Infrastructure/RandomIdGenerator.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System.Security.Cryptography; - -namespace CodeBeam.UltimateAuth.Core.Infrastructure -{ - /// - /// Provides cryptographically secure random ID generation. - /// - /// Produces opaque identifiers suitable for session IDs, PKCE codes, - /// refresh tokens, and other entropy-critical values. Output is encoded - /// using Base64Url for safe transport in URLs and headers. - /// - public static class RandomIdGenerator - { - /// - /// Generates a cryptographically secure random identifier with the - /// specified byte length and returns it as a URL-safe Base64 string. - /// - /// The number of random bytes to generate. - /// A URL-safe Base64 encoded random value. - /// - /// Thrown when is zero or negative. - /// - public static string Generate(int byteLength) - { - if (byteLength <= 0) - throw new ArgumentOutOfRangeException(nameof(byteLength)); - - var buffer = new byte[byteLength]; - RandomNumberGenerator.Fill(buffer); - - return Base64Url.Encode(buffer); - } - - /// - /// Generates a cryptographically secure random byte array with the - /// specified length. - /// - /// The number of bytes to generate. - /// A randomly filled byte array. - /// - /// Thrown when is zero or negative. - /// - public static byte[] GenerateBytes(int byteLength) - { - if (byteLength <= 0) - throw new ArgumentOutOfRangeException(nameof(byteLength)); - - var buffer = new byte[byteLength]; - RandomNumberGenerator.Fill(buffer); - return buffer; - } - - } -} diff --git a/src/CodeBeam.UltimateAuth.Core/Infrastructure/SeedRunner.cs b/src/CodeBeam.UltimateAuth.Core/Infrastructure/SeedRunner.cs index d0bf6adf..44fca7ae 100644 --- a/src/CodeBeam.UltimateAuth.Core/Infrastructure/SeedRunner.cs +++ b/src/CodeBeam.UltimateAuth.Core/Infrastructure/SeedRunner.cs @@ -9,7 +9,7 @@ public sealed class SeedRunner public SeedRunner(IEnumerable contributors) { _contributors = contributors; - Console.WriteLine("SeedRunner contributors:"); + foreach (var c in contributors) { Console.WriteLine($"- {c.GetType().FullName}"); diff --git a/src/CodeBeam.UltimateAuth.Core/Infrastructure/StringUserIdFactory.cs b/src/CodeBeam.UltimateAuth.Core/Infrastructure/StringUserIdFactory.cs index a622edfa..12c7f209 100644 --- a/src/CodeBeam.UltimateAuth.Core/Infrastructure/StringUserIdFactory.cs +++ b/src/CodeBeam.UltimateAuth.Core/Infrastructure/StringUserIdFactory.cs @@ -1,9 +1,8 @@ using CodeBeam.UltimateAuth.Core.Abstractions; -namespace CodeBeam.UltimateAuth.Core.Infrastructure +namespace CodeBeam.UltimateAuth.Core.Infrastructure; + +public sealed class StringUserIdFactory : IUserIdFactory { - public sealed class StringUserIdFactory : IUserIdFactory - { - public string Create() => Guid.NewGuid().ToString("N"); - } + public string Create() => Guid.NewGuid().ToString("N"); } diff --git a/src/CodeBeam.UltimateAuth.Core/Infrastructure/UAuthSessionStoreKernelFactory.cs b/src/CodeBeam.UltimateAuth.Core/Infrastructure/UAuthSessionStoreKernelFactory.cs new file mode 100644 index 00000000..3f769f92 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Core/Infrastructure/UAuthSessionStoreKernelFactory.cs @@ -0,0 +1,19 @@ +using CodeBeam.UltimateAuth.Core.Abstractions; +using Microsoft.Extensions.DependencyInjection; + +namespace CodeBeam.UltimateAuth.Core.Infrastructure; + +/// +/// Default session store factory that throws until a real store implementation is registered. +/// +internal sealed class UAuthSessionStoreKernelFactory : ISessionStoreKernelFactory +{ + private readonly IServiceProvider _sp; + + public UAuthSessionStoreKernelFactory(IServiceProvider sp) + { + _sp = sp; + } + + public ISessionStoreKernel Create(string? tenantId) => _sp.GetRequiredService(); +} diff --git a/src/CodeBeam.UltimateAuth.Core/Infrastructure/UAuthUserIdConverter.cs b/src/CodeBeam.UltimateAuth.Core/Infrastructure/UAuthUserIdConverter.cs index 44465ac5..dd2507f4 100644 --- a/src/CodeBeam.UltimateAuth.Core/Infrastructure/UAuthUserIdConverter.cs +++ b/src/CodeBeam.UltimateAuth.Core/Infrastructure/UAuthUserIdConverter.cs @@ -5,111 +5,110 @@ using System.Text; using System.Text.Json; -namespace CodeBeam.UltimateAuth.Core.Infrastructure +namespace CodeBeam.UltimateAuth.Core.Infrastructure; + +/// +/// Default implementation of that provides +/// normalization and serialization for user identifiers. +/// +/// Supports primitive types (, , , ) +/// with optimized formats. For custom types, JSON serialization is used as a safe fallback. +/// +/// Converters are used throughout UltimateAuth for: +/// - token generation +/// - session-store keys +/// - multi-tenancy boundaries +/// - logging and diagnostics +/// +public sealed class UAuthUserIdConverter : IUserIdConverter { /// - /// Default implementation of that provides - /// normalization and serialization for user identifiers. - /// - /// Supports primitive types (, , , ) - /// with optimized formats. For custom types, JSON serialization is used as a safe fallback. - /// - /// Converters are used throughout UltimateAuth for: - /// - token generation - /// - session-store keys - /// - multi-tenancy boundaries - /// - logging and diagnostics + /// Converts the specified user id into a canonical string representation. + /// Primitive types use invariant culture or compact formats; complex objects + /// are serialized via JSON. /// - public sealed class UAuthUserIdConverter : IUserIdConverter + /// The user identifier to convert. + /// A normalized string representation of the user id. + public string ToCanonicalString(TUserId id) { - /// - /// Converts the specified user id into a canonical string representation. - /// Primitive types use invariant culture or compact formats; complex objects - /// are serialized via JSON. - /// - /// The user identifier to convert. - /// A normalized string representation of the user id. - public string ToString(TUserId id) + return id switch { - return id switch - { - UserKey v => v.Value, - Guid v => v.ToString("N"), - string v => v, - int v => v.ToString(CultureInfo.InvariantCulture), - long v => v.ToString(CultureInfo.InvariantCulture), + UserKey v => v.Value, + Guid v => v.ToString("N"), + string v => v, + int v => v.ToString(CultureInfo.InvariantCulture), + long v => v.ToString(CultureInfo.InvariantCulture), - _ => throw new InvalidOperationException($"Unsupported UserId type: {typeof(TUserId).FullName}. " + - "Provide a custom IUserIdConverter.") - }; - } + _ => throw new InvalidOperationException($"Unsupported UserId type: {typeof(TUserId).FullName}. " + + "Provide a custom IUserIdConverter.") + }; + } - /// - /// Converts the user id into UTF-8 encoded bytes derived from its - /// normalized string representation. - /// - /// The user identifier to convert. - /// UTF-8 encoded bytes representing the user id. - public byte[] ToBytes(TUserId id) => Encoding.UTF8.GetBytes(ToString(id)); + /// + /// Converts the user id into UTF-8 encoded bytes derived from its + /// normalized string representation. + /// + /// The user identifier to convert. + /// UTF-8 encoded bytes representing the user id. + public byte[] ToBytes(TUserId id) => Encoding.UTF8.GetBytes(ToCanonicalString(id)); - /// - /// Converts a canonical string representation back into a user id. - /// Supports primitives and restores complex types via JSON deserialization. - /// - /// The string representation of the user id. - /// The reconstructed user id. - /// - /// Thrown when deserialization of complex types fails. - /// - public TUserId FromString(string value) + /// + /// Converts a canonical string representation back into a user id. + /// Supports primitives and restores complex types via JSON deserialization. + /// + /// The string representation of the user id. + /// The reconstructed user id. + /// + /// Thrown when deserialization of complex types fails. + /// + public TUserId FromString(string value) + { + return typeof(TUserId) switch { - return typeof(TUserId) switch - { - Type t when t == typeof(UserKey) => (TUserId)(object)UserKey.FromString(value), - Type t when t == typeof(Guid) => (TUserId)(object)Guid.Parse(value), - Type t when t == typeof(string) => (TUserId)(object)value, - Type t when t == typeof(int) => (TUserId)(object)int.Parse(value, CultureInfo.InvariantCulture), - Type t when t == typeof(long) => (TUserId)(object)long.Parse(value, CultureInfo.InvariantCulture), + Type t when t == typeof(UserKey) => (TUserId)(object)UserKey.FromString(value), + Type t when t == typeof(Guid) => (TUserId)(object)Guid.Parse(value), + Type t when t == typeof(string) => (TUserId)(object)value, + Type t when t == typeof(int) => (TUserId)(object)int.Parse(value, CultureInfo.InvariantCulture), + Type t when t == typeof(long) => (TUserId)(object)long.Parse(value, CultureInfo.InvariantCulture), - _ => JsonSerializer.Deserialize(value) - ?? throw new UAuthInternalException("Cannot deserialize TUserId") - }; - } + _ => JsonSerializer.Deserialize(value) + ?? throw new UAuthInternalException("Cannot deserialize TUserId") + }; + } - public bool TryFromString(string value, out TUserId? id) + public bool TryFromString(string value, out TUserId? id) + { + try { - try - { - id = FromString(value); - return true; - } - catch - { - id = default; - return false; - } + id = FromString(value); + return true; } + catch + { + id = default; + return false; + } + } - /// - /// Converts a UTF-8 encoded binary representation back into a user id. - /// - /// Binary data representing the user id. - /// The reconstructed user id. - public TUserId FromBytes(byte[] binary) => FromString(Encoding.UTF8.GetString(binary)); + /// + /// Converts a UTF-8 encoded binary representation back into a user id. + /// + /// Binary data representing the user id. + /// The reconstructed user id. + public TUserId FromBytes(byte[] binary) => FromString(Encoding.UTF8.GetString(binary)); - public bool TryFromBytes(byte[] binary, out TUserId? id) + public bool TryFromBytes(byte[] binary, out TUserId? id) + { + try { - try - { - id = FromBytes(binary); - return true; - } - catch - { - id = default; - return false; - } + id = FromBytes(binary); + return true; + } + catch + { + id = default; + return false; } - } + } diff --git a/src/CodeBeam.UltimateAuth.Core/Infrastructure/UAuthUserIdConverterResolver.cs b/src/CodeBeam.UltimateAuth.Core/Infrastructure/UAuthUserIdConverterResolver.cs index 0d7a489e..8c4c716f 100644 --- a/src/CodeBeam.UltimateAuth.Core/Infrastructure/UAuthUserIdConverterResolver.cs +++ b/src/CodeBeam.UltimateAuth.Core/Infrastructure/UAuthUserIdConverterResolver.cs @@ -1,47 +1,46 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using Microsoft.Extensions.DependencyInjection; -namespace CodeBeam.UltimateAuth.Core.Infrastructure +namespace CodeBeam.UltimateAuth.Core.Infrastructure; + +/// +/// Resolves instances from the DI container. +/// +/// If no custom converter is registered for a given TUserId, this resolver falls back +/// to the default implementation. +/// +/// This allows applications to optionally plug in specialized converters for certain +/// user id types while retaining safe defaults for all others. +/// +public sealed class UAuthUserIdConverterResolver : IUserIdConverterResolver { + private readonly IServiceProvider _sp; + /// - /// Resolves instances from the DI container. - /// - /// If no custom converter is registered for a given TUserId, this resolver falls back - /// to the default implementation. - /// - /// This allows applications to optionally plug in specialized converters for certain - /// user id types while retaining safe defaults for all others. + /// Initializes a new instance of the class. /// - public sealed class UAuthUserIdConverterResolver : IUserIdConverterResolver + /// The service provider used to resolve converters from DI. + public UAuthUserIdConverterResolver(IServiceProvider sp) { - private readonly IServiceProvider _sp; - - /// - /// Initializes a new instance of the class. - /// - /// The service provider used to resolve converters from DI. - public UAuthUserIdConverterResolver(IServiceProvider sp) - { - _sp = sp; - } - - /// - /// Returns a converter for the specified TUserId type. - /// - /// Resolution order: - /// 1. Try to resolve from DI. - /// 2. If not found, return a new instance. - /// - /// The user id type for which to resolve a converter. - /// An instance. - public IUserIdConverter GetConverter(string? provider) - { - var converter = _sp.GetService>(); - if (converter != null) - return converter; + _sp = sp; + } - return new UAuthUserIdConverter(); - } + /// + /// Returns a converter for the specified TUserId type. + /// + /// Resolution order: + /// 1. Try to resolve from DI. + /// 2. If not found, return a new instance. + /// + /// The user id type for which to resolve a converter. + /// An instance. + public IUserIdConverter GetConverter(string? provider) + { + var converter = _sp.GetService>(); + if (converter != null) + return converter; + return new UAuthUserIdConverter(); } + } diff --git a/src/CodeBeam.UltimateAuth.Core/Infrastructure/UserIdFactory.cs b/src/CodeBeam.UltimateAuth.Core/Infrastructure/UserIdFactory.cs index 8872df75..f024b89d 100644 --- a/src/CodeBeam.UltimateAuth.Core/Infrastructure/UserIdFactory.cs +++ b/src/CodeBeam.UltimateAuth.Core/Infrastructure/UserIdFactory.cs @@ -1,10 +1,9 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Core.Infrastructure +namespace CodeBeam.UltimateAuth.Core.Infrastructure; + +public sealed class UserIdFactory : IUserIdFactory { - public sealed class UserIdFactory : IUserIdFactory - { - public UserKey Create() => UserKey.New(); - } + public UserKey Create() => UserKey.New(); } diff --git a/src/CodeBeam.UltimateAuth.Core/Infrastructure/UserKeyJsonConverter.cs b/src/CodeBeam.UltimateAuth.Core/Infrastructure/UserKeyJsonConverter.cs index 21f731ee..db6570c5 100644 --- a/src/CodeBeam.UltimateAuth.Core/Infrastructure/UserKeyJsonConverter.cs +++ b/src/CodeBeam.UltimateAuth.Core/Infrastructure/UserKeyJsonConverter.cs @@ -2,21 +2,20 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace CodeBeam.UltimateAuth.Core.Infrastructure +namespace CodeBeam.UltimateAuth.Core.Infrastructure; + +public sealed class UserKeyJsonConverter : JsonConverter { - public sealed class UserKeyJsonConverter : JsonConverter + public override UserKey Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - public override UserKey Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType != JsonTokenType.String) - throw new JsonException("UserKey must be a string."); + if (reader.TokenType != JsonTokenType.String) + throw new JsonException("UserKey must be a string."); - return UserKey.FromString(reader.GetString()!); - } + return UserKey.FromString(reader.GetString()!); + } - public override void Write(Utf8JsonWriter writer, UserKey value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } + public override void Write(Utf8JsonWriter writer, UserKey value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.Value); } } diff --git a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/CompositeTenantResolver.cs b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/CompositeTenantResolver.cs index af82c698..ffc9040e 100644 --- a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/CompositeTenantResolver.cs +++ b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/CompositeTenantResolver.cs @@ -1,37 +1,36 @@ -namespace CodeBeam.UltimateAuth.Core.MultiTenancy +namespace CodeBeam.UltimateAuth.Core.MultiTenancy; + +/// +/// Executes multiple tenant resolvers in order; the first resolver returning a non-null tenant id wins. +/// +public sealed class CompositeTenantResolver : ITenantIdResolver { + private readonly IReadOnlyList _resolvers; + /// - /// Executes multiple tenant resolvers in order; the first resolver returning a non-null tenant id wins. + /// Creates a composite resolver that will evaluate the provided resolvers sequentially. /// - public sealed class CompositeTenantResolver : ITenantIdResolver + /// Ordered list of resolvers to execute. + public CompositeTenantResolver(IEnumerable resolvers) { - private readonly IReadOnlyList _resolvers; - - /// - /// Creates a composite resolver that will evaluate the provided resolvers sequentially. - /// - /// Ordered list of resolvers to execute. - public CompositeTenantResolver(IEnumerable resolvers) - { - _resolvers = resolvers.ToList(); - } + _resolvers = resolvers.ToList(); + } - /// - /// Executes each resolver in sequence and returns the first non-null tenant id. - /// Returns null if no resolver can determine a tenant id. - /// - /// Resolution context containing user id, session, request metadata, etc. - public async Task ResolveTenantIdAsync(TenantResolutionContext context) + /// + /// Executes each resolver in sequence and returns the first non-null tenant id. + /// Returns null if no resolver can determine a tenant id. + /// + /// Resolution context containing user id, session, request metadata, etc. + public async Task ResolveTenantIdAsync(TenantResolutionContext context) + { + foreach (var resolver in _resolvers) { - foreach (var resolver in _resolvers) - { - var tid = await resolver.ResolveTenantIdAsync(context); - if (!string.IsNullOrWhiteSpace(tid)) - return tid; - } - - return null; + var tid = await resolver.ResolveTenantIdAsync(context); + if (!string.IsNullOrWhiteSpace(tid)) + return tid; } + return null; } + } diff --git a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/FixedTenantResolver.cs b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/FixedTenantResolver.cs index 28b85062..dd10d648 100644 --- a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/FixedTenantResolver.cs +++ b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/FixedTenantResolver.cs @@ -1,27 +1,26 @@ -namespace CodeBeam.UltimateAuth.Core.MultiTenancy +namespace CodeBeam.UltimateAuth.Core.MultiTenancy; + +/// +/// Returns a constant tenant id for all resolution requests; useful for single-tenant or statically configured systems. +/// +public sealed class FixedTenantResolver : ITenantIdResolver { + private readonly string _tenantId; + /// - /// Returns a constant tenant id for all resolution requests; useful for single-tenant or statically configured systems. + /// Creates a resolver that always returns the specified tenant id. /// - public sealed class FixedTenantResolver : ITenantIdResolver + /// The tenant id that will be returned for all requests. + public FixedTenantResolver(string tenantId) { - private readonly string _tenantId; - - /// - /// Creates a resolver that always returns the specified tenant id. - /// - /// The tenant id that will be returned for all requests. - public FixedTenantResolver(string tenantId) - { - _tenantId = tenantId; - } + _tenantId = tenantId; + } - /// - /// Returns the fixed tenant id regardless of context. - /// - public Task ResolveTenantIdAsync(TenantResolutionContext context) - { - return Task.FromResult(_tenantId); - } + /// + /// Returns the fixed tenant id regardless of context. + /// + public Task ResolveTenantIdAsync(TenantResolutionContext context) + { + return Task.FromResult(_tenantId); } } diff --git a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/HeaderTenantResolver.cs b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/HeaderTenantResolver.cs index e969f0d7..5ae8263e 100644 --- a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/HeaderTenantResolver.cs +++ b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/HeaderTenantResolver.cs @@ -1,38 +1,37 @@ -namespace CodeBeam.UltimateAuth.Core.MultiTenancy +namespace CodeBeam.UltimateAuth.Core.MultiTenancy; + +/// +/// Resolves the tenant id from a specific HTTP header. +/// Example: X-Tenant: foo → returns "foo". +/// Useful when multi-tenancy is controlled by API gateways or reverse proxies. +/// +public sealed class HeaderTenantResolver : ITenantIdResolver { + private readonly string _headerName; + /// - /// Resolves the tenant id from a specific HTTP header. - /// Example: X-Tenant: foo → returns "foo". - /// Useful when multi-tenancy is controlled by API gateways or reverse proxies. + /// Creates a resolver that reads the tenant id from the given header name. /// - public sealed class HeaderTenantResolver : ITenantIdResolver + /// The name of the HTTP header to inspect. + public HeaderTenantResolver(string headerName) { - private readonly string _headerName; - - /// - /// Creates a resolver that reads the tenant id from the given header name. - /// - /// The name of the HTTP header to inspect. - public HeaderTenantResolver(string headerName) - { - _headerName = headerName; - } + _headerName = headerName; + } - /// - /// Attempts to resolve the tenant id by reading the configured header from the request context. - /// Returns null if the header is missing or empty. - /// - public Task ResolveTenantIdAsync(TenantResolutionContext context) + /// + /// Attempts to resolve the tenant id by reading the configured header from the request context. + /// Returns null if the header is missing or empty. + /// + public Task ResolveTenantIdAsync(TenantResolutionContext context) + { + if (context.Headers != null && + context.Headers.TryGetValue(_headerName, out var value) && + !string.IsNullOrWhiteSpace(value)) { - if (context.Headers != null && - context.Headers.TryGetValue(_headerName, out var value) && - !string.IsNullOrWhiteSpace(value)) - { - return Task.FromResult(value); - } - - return Task.FromResult(null); + return Task.FromResult(value); } + return Task.FromResult(null); } + } diff --git a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/HostTenantResolver.cs b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/HostTenantResolver.cs index f411b8dd..02ab394f 100644 --- a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/HostTenantResolver.cs +++ b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/HostTenantResolver.cs @@ -1,30 +1,29 @@ -namespace CodeBeam.UltimateAuth.Core.MultiTenancy +namespace CodeBeam.UltimateAuth.Core.MultiTenancy; + +/// +/// Resolves the tenant id based on the request host name. +/// Example: foo.example.com → returns "foo". +/// Useful in subdomain-based multi-tenant architectures. +/// +public sealed class HostTenantResolver : ITenantIdResolver { /// - /// Resolves the tenant id based on the request host name. - /// Example: foo.example.com → returns "foo". - /// Useful in subdomain-based multi-tenant architectures. + /// Attempts to resolve the tenant id from the host portion of the incoming request. + /// Returns null if the host is missing, invalid, or does not contain a subdomain. /// - public sealed class HostTenantResolver : ITenantIdResolver + public Task ResolveTenantIdAsync(TenantResolutionContext context) { - /// - /// Attempts to resolve the tenant id from the host portion of the incoming request. - /// Returns null if the host is missing, invalid, or does not contain a subdomain. - /// - public Task ResolveTenantIdAsync(TenantResolutionContext context) - { - var host = context.Host; + var host = context.Host; - if (string.IsNullOrWhiteSpace(host)) - return Task.FromResult(null); + if (string.IsNullOrWhiteSpace(host)) + return Task.FromResult(null); - var parts = host.Split('.', StringSplitOptions.RemoveEmptyEntries); + var parts = host.Split('.', StringSplitOptions.RemoveEmptyEntries); - // Expecting at least: {tenant}.{domain}.{tld} - if (parts.Length < 3) - return Task.FromResult(null); + // Expecting at least: {tenant}.{domain}.{tld} + if (parts.Length < 3) + return Task.FromResult(null); - return Task.FromResult(parts[0]); - } + return Task.FromResult(parts[0]); } } diff --git a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/ITenantIdResolver.cs b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/ITenantIdResolver.cs index cc7867c4..5289e08a 100644 --- a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/ITenantIdResolver.cs +++ b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/ITenantIdResolver.cs @@ -1,16 +1,15 @@ -namespace CodeBeam.UltimateAuth.Core.MultiTenancy +namespace CodeBeam.UltimateAuth.Core.MultiTenancy; + +/// +/// Defines a strategy for resolving the tenant id for the current request. +/// Implementations may extract the tenant from headers, hostnames, +/// authentication tokens, or any other application-defined source. +/// +public interface ITenantIdResolver { /// - /// Defines a strategy for resolving the tenant id for the current request. - /// Implementations may extract the tenant from headers, hostnames, - /// authentication tokens, or any other application-defined source. + /// Attempts to resolve the tenant id given the contextual request data. + /// Returns null when no tenant can be determined. /// - public interface ITenantIdResolver - { - /// - /// Attempts to resolve the tenant id given the contextual request data. - /// Returns null when no tenant can be determined. - /// - Task ResolveTenantIdAsync(TenantResolutionContext context); - } + Task ResolveTenantIdAsync(TenantResolutionContext context); } diff --git a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/PathTenantResolver.cs b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/PathTenantResolver.cs index 38c3f772..c04cafce 100644 --- a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/PathTenantResolver.cs +++ b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/PathTenantResolver.cs @@ -1,40 +1,39 @@ -namespace CodeBeam.UltimateAuth.Core.MultiTenancy +namespace CodeBeam.UltimateAuth.Core.MultiTenancy; + +/// +/// Resolves the tenant id from the request path. +/// Example pattern: /t/{tenantId}/... → returns the extracted tenant id. +/// +public sealed class PathTenantResolver : ITenantIdResolver { + private readonly string _prefix; + /// - /// Resolves the tenant id from the request path. - /// Example pattern: /t/{tenantId}/... → returns the extracted tenant id. + /// Creates a resolver that looks for tenant ids under a specific URL prefix. + /// Default prefix is "t", meaning URLs like /t/foo/api will resolve "foo". /// - public sealed class PathTenantResolver : ITenantIdResolver + public PathTenantResolver(string prefix = "t") { - private readonly string _prefix; - - /// - /// Creates a resolver that looks for tenant ids under a specific URL prefix. - /// Default prefix is "t", meaning URLs like /t/foo/api will resolve "foo". - /// - public PathTenantResolver(string prefix = "t") - { - _prefix = prefix; - } - - /// - /// Extracts the tenant id from the request path, if present. - /// Returns null when the prefix is not matched or the path is insufficient. - /// - public Task ResolveTenantIdAsync(TenantResolutionContext context) - { - var path = context.Path; - if (string.IsNullOrWhiteSpace(path)) - return Task.FromResult(null); + _prefix = prefix; + } - var segments = path.Split('/', StringSplitOptions.RemoveEmptyEntries); + /// + /// Extracts the tenant id from the request path, if present. + /// Returns null when the prefix is not matched or the path is insufficient. + /// + public Task ResolveTenantIdAsync(TenantResolutionContext context) + { + var path = context.Path; + if (string.IsNullOrWhiteSpace(path)) + return Task.FromResult(null); - // Format: /{prefix}/{tenantId}/... - if (segments.Length >= 2 && segments[0] == _prefix) - return Task.FromResult(segments[1]); + var segments = path.Split('/', StringSplitOptions.RemoveEmptyEntries); - return Task.FromResult(null); - } + // Format: /{prefix}/{tenantId}/... + if (segments.Length >= 2 && segments[0] == _prefix) + return Task.FromResult(segments[1]); + return Task.FromResult(null); } + } diff --git a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/TenantResolutionContext.cs b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/TenantResolutionContext.cs index 6ae1c483..d5271f0d 100644 --- a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/TenantResolutionContext.cs +++ b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/TenantResolutionContext.cs @@ -1,66 +1,65 @@ -namespace CodeBeam.UltimateAuth.Core.MultiTenancy +namespace CodeBeam.UltimateAuth.Core.MultiTenancy; + +/// +/// Represents the normalized request information used during tenant resolution. +/// Resolvers inspect these fields to derive the correct tenant id. +/// +public sealed class TenantResolutionContext { /// - /// Represents the normalized request information used during tenant resolution. - /// Resolvers inspect these fields to derive the correct tenant id. + /// The request host value (e.g., "foo.example.com"). + /// Used by HostTenantResolver. /// - public sealed class TenantResolutionContext - { - /// - /// The request host value (e.g., "foo.example.com"). - /// Used by HostTenantResolver. - /// - public string? Host { get; init; } + public string? Host { get; init; } - /// - /// The request path (e.g., "/t/foo/api/..."). - /// Used by PathTenantResolver. - /// - public string? Path { get; init; } + /// + /// The request path (e.g., "/t/foo/api/..."). + /// Used by PathTenantResolver. + /// + public string? Path { get; init; } - /// - /// Request headers. Used by HeaderTenantResolver. - /// - public IReadOnlyDictionary? Headers { get; init; } + /// + /// Request headers. Used by HeaderTenantResolver. + /// + public IReadOnlyDictionary? Headers { get; init; } - /// - /// Query string parameters. Used by future resolvers or custom logic. - /// - public IReadOnlyDictionary? Query { get; init; } + /// + /// Query string parameters. Used by future resolvers or custom logic. + /// + public IReadOnlyDictionary? Query { get; init; } - /// - /// The raw framework-specific request context (e.g., HttpContext). - /// Used only when advanced resolver logic needs full access. - /// RawContext SHOULD NOT be used by built-in resolvers. - /// It exists only for advanced or custom implementations. - /// - public object? RawContext { get; init; } + /// + /// The raw framework-specific request context (e.g., HttpContext). + /// Used only when advanced resolver logic needs full access. + /// RawContext SHOULD NOT be used by built-in resolvers. + /// It exists only for advanced or custom implementations. + /// + public object? RawContext { get; init; } - /// - /// Gets an empty instance of the TenantResolutionContext class. - /// - /// Use this property to represent a context with no tenant information. This instance - /// can be used as a default or placeholder when no tenant has been resolved. - /// - public static TenantResolutionContext Empty { get; } = new(); + /// + /// Gets an empty instance of the TenantResolutionContext class. + /// + /// Use this property to represent a context with no tenant information. This instance + /// can be used as a default or placeholder when no tenant has been resolved. + /// + public static TenantResolutionContext Empty { get; } = new(); - private TenantResolutionContext() { } + private TenantResolutionContext() { } - public static TenantResolutionContext Create( - IReadOnlyDictionary? headers = null, - IReadOnlyDictionary? Query = null, - string? host = null, - string? path = null, - object? rawContext = null) + public static TenantResolutionContext Create( + IReadOnlyDictionary? headers = null, + IReadOnlyDictionary? Query = null, + string? host = null, + string? path = null, + object? rawContext = null) + { + return new TenantResolutionContext { - return new TenantResolutionContext - { - Headers = headers, - Query = Query, - Host = host, - Path = path, - RawContext = rawContext - }; - } + Headers = headers, + Query = Query, + Host = host, + Path = path, + RawContext = rawContext + }; } } diff --git a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/TenantValidation.cs b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/TenantValidation.cs index c33d8b77..d6f4113d 100644 --- a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/TenantValidation.cs +++ b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/TenantValidation.cs @@ -1,28 +1,27 @@ using System.Text.RegularExpressions; using CodeBeam.UltimateAuth.Core.Options; -namespace CodeBeam.UltimateAuth.Core.MultiTenancy +namespace CodeBeam.UltimateAuth.Core.MultiTenancy; + +internal static class TenantValidation { - internal static class TenantValidation + public static UAuthTenantContext FromResolvedTenant( + string rawTenantId, + UAuthMultiTenantOptions options) { - public static UAuthTenantContext FromResolvedTenant( - string rawTenantId, - UAuthMultiTenantOptions options) - { - if (string.IsNullOrWhiteSpace(rawTenantId)) - return UAuthTenantContext.NotResolved(); + if (string.IsNullOrWhiteSpace(rawTenantId)) + return UAuthTenantContext.NotResolved(); - var tenantId = options.NormalizeToLowercase - ? rawTenantId.ToLowerInvariant() - : rawTenantId; + var tenantId = options.NormalizeToLowercase + ? rawTenantId.ToLowerInvariant() + : rawTenantId; - if (!Regex.IsMatch(tenantId, options.TenantIdRegex)) - return UAuthTenantContext.NotResolved(); + if (!Regex.IsMatch(tenantId, options.TenantIdRegex)) + return UAuthTenantContext.NotResolved(); - if (options.ReservedTenantIds.Contains(tenantId)) - return UAuthTenantContext.NotResolved(); + if (options.ReservedTenantIds.Contains(tenantId)) + return UAuthTenantContext.NotResolved(); - return UAuthTenantContext.Resolved(tenantId); - } + return UAuthTenantContext.Resolved(tenantId); } } diff --git a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/UAuthTenantContext.cs b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/UAuthTenantContext.cs index 9874068a..8229e2b7 100644 --- a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/UAuthTenantContext.cs +++ b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/UAuthTenantContext.cs @@ -1,23 +1,22 @@ -namespace CodeBeam.UltimateAuth.Core.MultiTenancy +namespace CodeBeam.UltimateAuth.Core.MultiTenancy; + +/// +/// Represents the resolved tenant result for the current request. +/// +public sealed class UAuthTenantContext { - /// - /// Represents the resolved tenant result for the current request. - /// - public sealed class UAuthTenantContext - { - public string? TenantId { get; } - public bool IsResolved { get; } + public string? TenantId { get; } + public bool IsResolved { get; } - private UAuthTenantContext(string? tenantId, bool resolved) - { - TenantId = tenantId; - IsResolved = resolved; - } + private UAuthTenantContext(string? tenantId, bool resolved) + { + TenantId = tenantId; + IsResolved = resolved; + } - public static UAuthTenantContext NotResolved() - => new(null, false); + public static UAuthTenantContext NotResolved() + => new(null, false); - public static UAuthTenantContext Resolved(string tenantId) - => new(tenantId, true); - } + public static UAuthTenantContext Resolved(string tenantId) + => new(tenantId, true); } diff --git a/src/CodeBeam.UltimateAuth.Core/Options/IClientProfileDetector.cs b/src/CodeBeam.UltimateAuth.Core/Options/IClientProfileDetector.cs index 42226d11..65236210 100644 --- a/src/CodeBeam.UltimateAuth.Core/Options/IClientProfileDetector.cs +++ b/src/CodeBeam.UltimateAuth.Core/Options/IClientProfileDetector.cs @@ -1,9 +1,6 @@ -using Microsoft.Extensions.DependencyInjection; +namespace CodeBeam.UltimateAuth.Core.Options; -namespace CodeBeam.UltimateAuth.Core.Options +public interface IClientProfileDetector { - public interface IClientProfileDetector - { - UAuthClientProfile Detect(IServiceProvider services); - } + UAuthClientProfile Detect(IServiceProvider services); } diff --git a/src/CodeBeam.UltimateAuth.Core/Options/IServerProfileDetector.cs b/src/CodeBeam.UltimateAuth.Core/Options/IServerProfileDetector.cs deleted file mode 100644 index 33af8f2b..00000000 --- a/src/CodeBeam.UltimateAuth.Core/Options/IServerProfileDetector.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; - -namespace CodeBeam.UltimateAuth.Core.Options -{ - public interface IServerProfileDetector - { - UAuthClientProfile Detect(IServiceProvider services); - } -} diff --git a/src/CodeBeam.UltimateAuth.Core/Options/UAuthClientProfile.cs b/src/CodeBeam.UltimateAuth.Core/Options/UAuthClientProfile.cs index f8c75a2a..c5bf2c3c 100644 --- a/src/CodeBeam.UltimateAuth.Core/Options/UAuthClientProfile.cs +++ b/src/CodeBeam.UltimateAuth.Core/Options/UAuthClientProfile.cs @@ -1,13 +1,12 @@ -namespace CodeBeam.UltimateAuth.Core.Options +namespace CodeBeam.UltimateAuth.Core.Options; + +public enum UAuthClientProfile { - public enum UAuthClientProfile - { - NotSpecified, - BlazorWasm, - BlazorServer, - Maui, - WebServer, - Api, - UAuthHub = 1000 - } + NotSpecified, + BlazorWasm, + BlazorServer, + Maui, + WebServer, + Api, + UAuthHub = 1000 } diff --git a/src/CodeBeam.UltimateAuth.Core/Options/UAuthLoginOptions.cs b/src/CodeBeam.UltimateAuth.Core/Options/UAuthLoginOptions.cs index 46677d74..0912e65a 100644 --- a/src/CodeBeam.UltimateAuth.Core/Options/UAuthLoginOptions.cs +++ b/src/CodeBeam.UltimateAuth.Core/Options/UAuthLoginOptions.cs @@ -1,21 +1,20 @@ -namespace CodeBeam.UltimateAuth.Core.Options +namespace CodeBeam.UltimateAuth.Core.Options; + +/// +/// Configuration settings related to interactive user login behavior, +/// including lockout policies and failed-attempt thresholds. +/// +public sealed class UAuthLoginOptions { /// - /// Configuration settings related to interactive user login behavior, - /// including lockout policies and failed-attempt thresholds. + /// Maximum number of consecutive failed login attempts allowed + /// before the user is temporarily locked out. /// - public sealed class UAuthLoginOptions - { - /// - /// Maximum number of consecutive failed login attempts allowed - /// before the user is temporarily locked out. - /// - public int MaxFailedAttempts { get; set; } = 5; + public int MaxFailedAttempts { get; set; } = 5; - /// - /// Duration (in minutes) for which the user is locked out - /// after exceeding . - /// - public int LockoutMinutes { get; set; } = 15; - } + /// + /// Duration (in minutes) for which the user is locked out + /// after exceeding . + /// + public int LockoutMinutes { get; set; } = 15; } diff --git a/src/CodeBeam.UltimateAuth.Core/Options/UAuthMode.cs b/src/CodeBeam.UltimateAuth.Core/Options/UAuthMode.cs index 941a93a5..e9d3533b 100644 --- a/src/CodeBeam.UltimateAuth.Core/Options/UAuthMode.cs +++ b/src/CodeBeam.UltimateAuth.Core/Options/UAuthMode.cs @@ -1,43 +1,42 @@ -namespace CodeBeam.UltimateAuth.Core +namespace CodeBeam.UltimateAuth.Core; + +/// +/// Defines the authentication execution model for UltimateAuth. +/// Each mode represents a fundamentally different security +/// and lifecycle strategy. +/// +public enum UAuthMode { /// - /// Defines the authentication execution model for UltimateAuth. - /// Each mode represents a fundamentally different security - /// and lifecycle strategy. + /// Pure opaque, session-based authentication. + /// No JWT, no refresh token. + /// Full server-side control with sliding expiration. + /// Best for Blazor Server, MVC, intranet apps. /// - public enum UAuthMode - { - /// - /// Pure opaque, session-based authentication. - /// No JWT, no refresh token. - /// Full server-side control with sliding expiration. - /// Best for Blazor Server, MVC, intranet apps. - /// - PureOpaque = 0, + PureOpaque = 0, - /// - /// Full hybrid mode. - /// Session + JWT + refresh token. - /// Server-side session control with JWT performance. - /// Default mode. - /// - Hybrid = 1, + /// + /// Full hybrid mode. + /// Session + JWT + refresh token. + /// Server-side session control with JWT performance. + /// Default mode. + /// + Hybrid = 1, - /// - /// Semi-hybrid mode. - /// JWT is fully stateless at runtime. - /// Session exists only as metadata/control plane - /// (logout, disable, audit, device tracking). - /// No request-time session lookup. - /// - SemiHybrid = 2, + /// + /// Semi-hybrid mode. + /// JWT is fully stateless at runtime. + /// Session exists only as metadata/control plane + /// (logout, disable, audit, device tracking). + /// No request-time session lookup. + /// + SemiHybrid = 2, - /// - /// Pure JWT mode. - /// Fully stateless authentication. - /// No session, no server-side lookup. - /// Revocation only via token expiration. - /// - PureJwt = 3 - } + /// + /// Pure JWT mode. + /// Fully stateless authentication. + /// No session, no server-side lookup. + /// Revocation only via token expiration. + /// + PureJwt = 3 } diff --git a/src/CodeBeam.UltimateAuth.Core/Options/UAuthMultiTenantOptions.cs b/src/CodeBeam.UltimateAuth.Core/Options/UAuthMultiTenantOptions.cs index 9c0fdec5..cfbf30be 100644 --- a/src/CodeBeam.UltimateAuth.Core/Options/UAuthMultiTenantOptions.cs +++ b/src/CodeBeam.UltimateAuth.Core/Options/UAuthMultiTenantOptions.cs @@ -1,86 +1,85 @@ -namespace CodeBeam.UltimateAuth.Core.Options +namespace CodeBeam.UltimateAuth.Core.Options; + +/// +/// Multi-tenancy configuration for UltimateAuth. +/// Controls whether tenants are required, how they are resolved, +/// and how tenant identifiers are normalized. +/// +public sealed class UAuthMultiTenantOptions { /// - /// Multi-tenancy configuration for UltimateAuth. - /// Controls whether tenants are required, how they are resolved, - /// and how tenant identifiers are normalized. + /// Enables multi-tenant mode. + /// When disabled, all requests operate under a single implicit tenant. /// - public sealed class UAuthMultiTenantOptions - { - /// - /// Enables multi-tenant mode. - /// When disabled, all requests operate under a single implicit tenant. - /// - public bool Enabled { get; set; } = false; + public bool Enabled { get; set; } = false; - /// - /// If tenant cannot be resolved, this value is used. - /// If null and RequireTenant = true, request fails. - /// - public string? DefaultTenantId { get; set; } + /// + /// If tenant cannot be resolved, this value is used. + /// If null and RequireTenant = true, request fails. + /// + public string? DefaultTenantId { get; set; } - /// - /// If true, a resolved tenant id must always exist. - /// If resolver cannot determine tenant, request will fail. - /// - public bool RequireTenant { get; set; } = false; + /// + /// If true, a resolved tenant id must always exist. + /// If resolver cannot determine tenant, request will fail. + /// + public bool RequireTenant { get; set; } = false; - /// - /// If true, a tenant id returned by resolver does NOT need to be known beforehand. - /// If false, unknown tenants must be explicitly registered. - /// (Useful for multi-tenant SaaS with dynamic tenant provisioning) - /// - public bool AllowUnknownTenants { get; set; } = true; + /// + /// If true, a tenant id returned by resolver does NOT need to be known beforehand. + /// If false, unknown tenants must be explicitly registered. + /// (Useful for multi-tenant SaaS with dynamic tenant provisioning) + /// + public bool AllowUnknownTenants { get; set; } = true; - /// - /// Tenant ids that cannot be used by clients. - /// Protects system-level tenant identifiers. - /// - public HashSet ReservedTenantIds { get; set; } = new() - { - "system", - "root", - "admin", - "public" - }; + /// + /// Tenant ids that cannot be used by clients. + /// Protects system-level tenant identifiers. + /// + public HashSet ReservedTenantIds { get; set; } = new() + { + "system", + "root", + "admin", + "public" + }; - /// - /// If true, tenant identifiers are normalized to lowercase. - /// Recommended for host-based tenancy. - /// - public bool NormalizeToLowercase { get; set; } = true; + /// + /// If true, tenant identifiers are normalized to lowercase. + /// Recommended for host-based tenancy. + /// + public bool NormalizeToLowercase { get; set; } = true; - /// - /// Optional validation for tenant id format. - /// Default: alphanumeric + hyphens allowed. - /// - public string TenantIdRegex { get; set; } = "^[a-zA-Z0-9\\-]+$"; + /// + /// Optional validation for tenant id format. + /// Default: alphanumeric + hyphens allowed. + /// + public string TenantIdRegex { get; set; } = "^[a-zA-Z0-9\\-]+$"; - /// - /// Enables tenant resolution from the URL path and - /// exposes auth endpoints under /{tenant}/{routePrefix}/... - /// - public bool EnableRoute { get; set; } = true; - public bool EnableHeader { get; set; } = false; - public bool EnableDomain { get; set; } = false; + /// + /// Enables tenant resolution from the URL path and + /// exposes auth endpoints under /{tenant}/{routePrefix}/... + /// + public bool EnableRoute { get; set; } = true; + public bool EnableHeader { get; set; } = false; + public bool EnableDomain { get; set; } = false; - // Header config - public string HeaderName { get; set; } = "X-Tenant"; + // Header config + public string HeaderName { get; set; } = "X-Tenant"; - internal UAuthMultiTenantOptions Clone() => new() - { - Enabled = Enabled, - DefaultTenantId = DefaultTenantId, - RequireTenant = RequireTenant, - AllowUnknownTenants = AllowUnknownTenants, - ReservedTenantIds = new HashSet(ReservedTenantIds), - NormalizeToLowercase = NormalizeToLowercase, - TenantIdRegex = TenantIdRegex, - EnableRoute = EnableRoute, - EnableHeader = EnableHeader, - EnableDomain = EnableDomain, - HeaderName = HeaderName - }; + internal UAuthMultiTenantOptions Clone() => new() + { + Enabled = Enabled, + DefaultTenantId = DefaultTenantId, + RequireTenant = RequireTenant, + AllowUnknownTenants = AllowUnknownTenants, + ReservedTenantIds = new HashSet(ReservedTenantIds), + NormalizeToLowercase = NormalizeToLowercase, + TenantIdRegex = TenantIdRegex, + EnableRoute = EnableRoute, + EnableHeader = EnableHeader, + EnableDomain = EnableDomain, + HeaderName = HeaderName + }; - } } diff --git a/src/CodeBeam.UltimateAuth.Core/Options/UAuthMultiTenantOptionsValidator.cs b/src/CodeBeam.UltimateAuth.Core/Options/UAuthMultiTenantOptionsValidator.cs index 74828a1d..db745d49 100644 --- a/src/CodeBeam.UltimateAuth.Core/Options/UAuthMultiTenantOptionsValidator.cs +++ b/src/CodeBeam.UltimateAuth.Core/Options/UAuthMultiTenantOptionsValidator.cs @@ -1,85 +1,84 @@ using System.Text.RegularExpressions; using Microsoft.Extensions.Options; -namespace CodeBeam.UltimateAuth.Core.Options +namespace CodeBeam.UltimateAuth.Core.Options; + +/// +/// Validates at application startup. +/// Ensures that tenant configuration values (regex patterns, defaults, +/// reserved identifiers, and requirement rules) are logically consistent +/// and safe to use before multi-tenant authentication begins. +/// +internal sealed class UAuthMultiTenantOptionsValidator : IValidateOptions { /// - /// Validates at application startup. - /// Ensures that tenant configuration values (regex patterns, defaults, - /// reserved identifiers, and requirement rules) are logically consistent - /// and safe to use before multi-tenant authentication begins. + /// Performs validation on the provided instance. + /// This method enforces: + /// - valid tenant id regex format, + /// - reserved tenant ids matching the regex, + /// - default tenant id consistency, + /// - requirement rules coherence. /// - internal sealed class UAuthMultiTenantOptionsValidator : IValidateOptions + /// Optional configuration section name. + /// The options instance to validate. + /// + /// A indicating success or the + /// specific configuration error encountered. + /// + public ValidateOptionsResult Validate(string? name, UAuthMultiTenantOptions options) { - /// - /// Performs validation on the provided instance. - /// This method enforces: - /// - valid tenant id regex format, - /// - reserved tenant ids matching the regex, - /// - default tenant id consistency, - /// - requirement rules coherence. - /// - /// Optional configuration section name. - /// The options instance to validate. - /// - /// A indicating success or the - /// specific configuration error encountered. - /// - public ValidateOptionsResult Validate(string? name, UAuthMultiTenantOptions options) + // Multi-tenancy disabled → no validation needed + if (!options.Enabled) + return ValidateOptionsResult.Success; + + try + { + _ = new Regex(options.TenantIdRegex, RegexOptions.Compiled); + } + catch (Exception ex) { - // Multi-tenancy disabled → no validation needed - if (!options.Enabled) - return ValidateOptionsResult.Success; + return ValidateOptionsResult.Fail( + $"Invalid TenantIdRegex '{options.TenantIdRegex}'. Regex error: {ex.Message}"); + } - try + foreach (var reserved in options.ReservedTenantIds) + { + if (string.IsNullOrWhiteSpace(reserved)) { - _ = new Regex(options.TenantIdRegex, RegexOptions.Compiled); + return ValidateOptionsResult.Fail( + "ReservedTenantIds cannot contain empty or whitespace values."); } - catch (Exception ex) + + if (!Regex.IsMatch(reserved, options.TenantIdRegex)) { return ValidateOptionsResult.Fail( - $"Invalid TenantIdRegex '{options.TenantIdRegex}'. Regex error: {ex.Message}"); + $"Reserved tenant id '{reserved}' does not match TenantIdRegex '{options.TenantIdRegex}'."); } + } - foreach (var reserved in options.ReservedTenantIds) + if (options.DefaultTenantId != null) + { + if (string.IsNullOrWhiteSpace(options.DefaultTenantId)) { - if (string.IsNullOrWhiteSpace(reserved)) - { - return ValidateOptionsResult.Fail( - "ReservedTenantIds cannot contain empty or whitespace values."); - } - - if (!Regex.IsMatch(reserved, options.TenantIdRegex)) - { - return ValidateOptionsResult.Fail( - $"Reserved tenant id '{reserved}' does not match TenantIdRegex '{options.TenantIdRegex}'."); - } + return ValidateOptionsResult.Fail("DefaultTenantId cannot be empty or whitespace."); } - if (options.DefaultTenantId != null) + if (!Regex.IsMatch(options.DefaultTenantId, options.TenantIdRegex)) { - if (string.IsNullOrWhiteSpace(options.DefaultTenantId)) - { - return ValidateOptionsResult.Fail("DefaultTenantId cannot be empty or whitespace."); - } - - if (!Regex.IsMatch(options.DefaultTenantId, options.TenantIdRegex)) - { - return ValidateOptionsResult.Fail($"DefaultTenantId '{options.DefaultTenantId}' does not match TenantIdRegex '{options.TenantIdRegex}'."); - } - - if (options.ReservedTenantIds.Contains(options.DefaultTenantId)) - { - return ValidateOptionsResult.Fail($"DefaultTenantId '{options.DefaultTenantId}' is listed in ReservedTenantIds."); - } + return ValidateOptionsResult.Fail($"DefaultTenantId '{options.DefaultTenantId}' does not match TenantIdRegex '{options.TenantIdRegex}'."); } - if (options.RequireTenant && options.DefaultTenantId == null) + if (options.ReservedTenantIds.Contains(options.DefaultTenantId)) { - return ValidateOptionsResult.Fail("RequireTenant = true, but DefaultTenantId is null. Provide a default tenant id or disable RequireTenant."); + return ValidateOptionsResult.Fail($"DefaultTenantId '{options.DefaultTenantId}' is listed in ReservedTenantIds."); } + } - return ValidateOptionsResult.Success; + if (options.RequireTenant && options.DefaultTenantId == null) + { + return ValidateOptionsResult.Fail("RequireTenant = true, but DefaultTenantId is null. Provide a default tenant id or disable RequireTenant."); } + + return ValidateOptionsResult.Success; } } diff --git a/src/CodeBeam.UltimateAuth.Core/Options/UAuthOptions.cs b/src/CodeBeam.UltimateAuth.Core/Options/UAuthOptions.cs index 8992fb19..a65a1d51 100644 --- a/src/CodeBeam.UltimateAuth.Core/Options/UAuthOptions.cs +++ b/src/CodeBeam.UltimateAuth.Core/Options/UAuthOptions.cs @@ -1,61 +1,60 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Events; -namespace CodeBeam.UltimateAuth.Core.Options +namespace CodeBeam.UltimateAuth.Core.Options; + +/// +/// Top-level configuration container for all UltimateAuth features. +/// Combines login policies, session lifecycle rules, token behavior, +/// PKCE settings, multi-tenancy behavior, and user-id normalization. +/// +/// All sub-options are resolved from configuration (appsettings.json) +/// or through inline setup in AddUltimateAuth(). +/// +public sealed class UAuthOptions { /// - /// Top-level configuration container for all UltimateAuth features. - /// Combines login policies, session lifecycle rules, token behavior, - /// PKCE settings, multi-tenancy behavior, and user-id normalization. - /// - /// All sub-options are resolved from configuration (appsettings.json) - /// or through inline setup in AddUltimateAuth(). + /// Configuration settings for interactive login flows, + /// including lockout thresholds and failed-attempt policies. /// - public sealed class UAuthOptions - { - /// - /// Configuration settings for interactive login flows, - /// including lockout thresholds and failed-attempt policies. - /// - public UAuthLoginOptions Login { get; set; } = new(); - - /// - /// Settings that control session creation, refresh behavior, - /// sliding expiration, idle timeouts, device limits, and chain rules. - /// - public UAuthSessionOptions Session { get; set; } = new(); - - /// - /// Token issuance configuration, including JWT and opaque token - /// generation, lifetimes, signing keys, and audience/issuer values. - /// - public UAuthTokenOptions Token { get; set; } = new(); - - /// - /// PKCE (Proof Key for Code Exchange) configuration used for - /// browser-based login flows and WASM authentication. - /// - public UAuthPkceOptions Pkce { get; set; } = new(); - - /// - /// Event hooks raised during authentication lifecycle events - /// such as login, logout, session creation, refresh, or revocation. - /// - public UAuthEvents UAuthEvents { get; set; } = new(); - - /// - /// Multi-tenancy configuration controlling how tenants are resolved, - /// validated, and optionally enforced. - /// - public UAuthMultiTenantOptions MultiTenant { get; set; } = new(); - - /// - /// Provides converters used to normalize and serialize TUserId - /// across the system (sessions, stores, tokens, logging). - /// - public IUserIdConverterResolver? UserIdConverters { get; set; } - - public UAuthClientProfile ClientProfile { get; set; } = UAuthClientProfile.NotSpecified; - public bool AutoDetectClientProfile { get; set; } = true; - } + public UAuthLoginOptions Login { get; set; } = new(); + + /// + /// Settings that control session creation, refresh behavior, + /// sliding expiration, idle timeouts, device limits, and chain rules. + /// + public UAuthSessionOptions Session { get; set; } = new(); + + /// + /// Token issuance configuration, including JWT and opaque token + /// generation, lifetimes, signing keys, and audience/issuer values. + /// + public UAuthTokenOptions Token { get; set; } = new(); + + /// + /// PKCE (Proof Key for Code Exchange) configuration used for + /// browser-based login flows and WASM authentication. + /// + public UAuthPkceOptions Pkce { get; set; } = new(); + + /// + /// Event hooks raised during authentication lifecycle events + /// such as login, logout, session creation, refresh, or revocation. + /// + public UAuthEvents UAuthEvents { get; set; } = new(); + + /// + /// Multi-tenancy configuration controlling how tenants are resolved, + /// validated, and optionally enforced. + /// + public UAuthMultiTenantOptions MultiTenant { get; set; } = new(); + + /// + /// Provides converters used to normalize and serialize TUserId + /// across the system (sessions, stores, tokens, logging). + /// + public IUserIdConverterResolver? UserIdConverters { get; set; } + + public UAuthClientProfile ClientProfile { get; set; } = UAuthClientProfile.NotSpecified; + public bool AutoDetectClientProfile { get; set; } = true; } diff --git a/src/CodeBeam.UltimateAuth.Core/Options/UAuthOptionsValidator.cs b/src/CodeBeam.UltimateAuth.Core/Options/UAuthOptionsValidator.cs index 405681e0..aa7dff30 100644 --- a/src/CodeBeam.UltimateAuth.Core/Options/UAuthOptionsValidator.cs +++ b/src/CodeBeam.UltimateAuth.Core/Options/UAuthOptionsValidator.cs @@ -1,44 +1,43 @@ using Microsoft.Extensions.Options; -namespace CodeBeam.UltimateAuth.Core.Options +namespace CodeBeam.UltimateAuth.Core.Options; + +internal sealed class UAuthOptionsValidator : IValidateOptions { - internal sealed class UAuthOptionsValidator : IValidateOptions + public ValidateOptionsResult Validate(string? name, UAuthOptions options) { - public ValidateOptionsResult Validate(string? name, UAuthOptions options) - { - var errors = new List(); + var errors = new List(); - if (options.Login is null) - errors.Add("UltimateAuth.Login configuration section is missing."); + if (options.Login is null) + errors.Add("UltimateAuth.Login configuration section is missing."); - if (options.Session is null) - errors.Add("UltimateAuth.Session configuration section is missing."); + if (options.Session is null) + errors.Add("UltimateAuth.Session configuration section is missing."); - if (options.Token is null) - errors.Add("UltimateAuth.Token configuration section is missing."); + if (options.Token is null) + errors.Add("UltimateAuth.Token configuration section is missing."); - if (options.Pkce is null) - errors.Add("UltimateAuth.Pkce configuration section is missing."); + if (options.Pkce is null) + errors.Add("UltimateAuth.Pkce configuration section is missing."); - if (errors.Count > 0) - return ValidateOptionsResult.Fail(errors); + if (errors.Count > 0) + return ValidateOptionsResult.Fail(errors); - // Only add cross-option validation beyond this point, individual options should validate in their own validators. - if (options.Token!.AccessTokenLifetime > options.Session!.MaxLifetime) - { - errors.Add("Token.AccessTokenLifetime cannot exceed Session.MaxLifetime."); - } - - if (options.Token.RefreshTokenLifetime > options.Session.MaxLifetime) - { - errors.Add("Token.RefreshTokenLifetime cannot exceed Session.MaxLifetime."); - } + // Only add cross-option validation beyond this point, individual options should validate in their own validators. + if (options.Token!.AccessTokenLifetime > options.Session!.MaxLifetime) + { + errors.Add("Token.AccessTokenLifetime cannot exceed Session.MaxLifetime."); + } - return errors.Count == 0 - ? ValidateOptionsResult.Success - : ValidateOptionsResult.Fail(errors); + if (options.Token.RefreshTokenLifetime > options.Session.MaxLifetime) + { + errors.Add("Token.RefreshTokenLifetime cannot exceed Session.MaxLifetime."); } + + return errors.Count == 0 + ? ValidateOptionsResult.Success + : ValidateOptionsResult.Fail(errors); } } diff --git a/src/CodeBeam.UltimateAuth.Core/Options/UAuthPkceOptions.cs b/src/CodeBeam.UltimateAuth.Core/Options/UAuthPkceOptions.cs index b66d85cf..ab13f894 100644 --- a/src/CodeBeam.UltimateAuth.Core/Options/UAuthPkceOptions.cs +++ b/src/CodeBeam.UltimateAuth.Core/Options/UAuthPkceOptions.cs @@ -1,28 +1,25 @@ -using CodeBeam.UltimateAuth.Core.Contracts; +namespace CodeBeam.UltimateAuth.Core.Options; -namespace CodeBeam.UltimateAuth.Core.Options +/// +/// Configuration settings for PKCE (Proof Key for Code Exchange) +/// authorization flows. Controls how long authorization codes remain +/// valid before they must be exchanged for tokens. +/// +public sealed class UAuthPkceOptions { /// - /// Configuration settings for PKCE (Proof Key for Code Exchange) - /// authorization flows. Controls how long authorization codes remain - /// valid before they must be exchanged for tokens. + /// Lifetime of a PKCE authorization code in seconds. + /// Shorter values provide stronger replay protection, + /// while longer values allow more tolerance for slow clients. /// - public sealed class UAuthPkceOptions - { - /// - /// Lifetime of a PKCE authorization code in seconds. - /// Shorter values provide stronger replay protection, - /// while longer values allow more tolerance for slow clients. - /// - public int AuthorizationCodeLifetimeSeconds { get; set; } = 120; + public int AuthorizationCodeLifetimeSeconds { get; set; } = 120; - public int MaxVerificationAttempts { get; set; } = 5; + public int MaxVerificationAttempts { get; set; } = 5; - internal UAuthPkceOptions Clone() => new() - { - AuthorizationCodeLifetimeSeconds = AuthorizationCodeLifetimeSeconds, - MaxVerificationAttempts = MaxVerificationAttempts, - }; + internal UAuthPkceOptions Clone() => new() + { + AuthorizationCodeLifetimeSeconds = AuthorizationCodeLifetimeSeconds, + MaxVerificationAttempts = MaxVerificationAttempts, + }; - } } diff --git a/src/CodeBeam.UltimateAuth.Core/Options/UAuthPkceOptionsValidator.cs b/src/CodeBeam.UltimateAuth.Core/Options/UAuthPkceOptionsValidator.cs index 744d81a5..4ee9c2ab 100644 --- a/src/CodeBeam.UltimateAuth.Core/Options/UAuthPkceOptionsValidator.cs +++ b/src/CodeBeam.UltimateAuth.Core/Options/UAuthPkceOptionsValidator.cs @@ -1,21 +1,20 @@ using Microsoft.Extensions.Options; -namespace CodeBeam.UltimateAuth.Core.Options +namespace CodeBeam.UltimateAuth.Core.Options; + +internal sealed class UAuthPkceOptionsValidator : IValidateOptions { - internal sealed class UAuthPkceOptionsValidator : IValidateOptions + public ValidateOptionsResult Validate(string? name, UAuthPkceOptions options) { - public ValidateOptionsResult Validate(string? name, UAuthPkceOptions options) - { - var errors = new List(); - - if (options.AuthorizationCodeLifetimeSeconds <= 0) - { - errors.Add("Pkce.AuthorizationCodeLifetimeSeconds must be > 0."); - } + var errors = new List(); - return errors.Count == 0 - ? ValidateOptionsResult.Success - : ValidateOptionsResult.Fail(errors); + if (options.AuthorizationCodeLifetimeSeconds <= 0) + { + errors.Add("Pkce.AuthorizationCodeLifetimeSeconds must be > 0."); } + + return errors.Count == 0 + ? ValidateOptionsResult.Success + : ValidateOptionsResult.Fail(errors); } } diff --git a/src/CodeBeam.UltimateAuth.Core/Options/UAuthSessionOptions.cs b/src/CodeBeam.UltimateAuth.Core/Options/UAuthSessionOptions.cs index 2d323487..35fd2631 100644 --- a/src/CodeBeam.UltimateAuth.Core/Options/UAuthSessionOptions.cs +++ b/src/CodeBeam.UltimateAuth.Core/Options/UAuthSessionOptions.cs @@ -1,112 +1,111 @@ using CodeBeam.UltimateAuth.Core.Contracts; -namespace CodeBeam.UltimateAuth.Core.Options +namespace CodeBeam.UltimateAuth.Core.Options; + +// TODO: Add rotate on refresh (especially for Hybrid). Default behavior should be single session in chain for Hybrid, but can be configured. +// And add RotateAsync method. + +/// +/// Defines configuration settings that control the lifecycle, +/// security behavior, and device constraints of UltimateAuth +/// session management. +/// +/// These values influence how sessions are created, refreshed, +/// expired, revoked, and grouped into device chains. +/// +public sealed class UAuthSessionOptions { - // TODO: Add rotate on refresh (especially for Hybrid). Default behavior should be single session in chain for Hybrid, but can be configured. - // And add RotateAsync method. + /// + /// The standard lifetime of a session before it expires. + /// This is the duration added during login or refresh. + /// + public TimeSpan Lifetime { get; set; } = TimeSpan.FromDays(7); + + /// + /// Maximum absolute lifetime a session may have, even when + /// sliding expiration is enabled. If null, no hard cap + /// is applied. + /// + public TimeSpan? MaxLifetime { get; set; } + + /// + /// When enabled, each refresh extends the session's expiration, + /// allowing continuous usage until MaxLifetime or idle rules apply. + /// On PureOpaque (or one token usage) mode, each touch restarts the session lifetime. + /// + public bool SlidingExpiration { get; set; } = true; + + /// + /// Maximum allowed idle time before the session becomes invalid. + /// If null or zero, idle expiration is disabled entirely. + /// + public TimeSpan? IdleTimeout { get; set; } + + /// + /// Minimum interval between LastSeenAt updates. + /// Prevents excessive writes under high traffic. + /// + public TimeSpan? TouchInterval { get; set; } = TimeSpan.FromMinutes(5); /// - /// Defines configuration settings that control the lifecycle, - /// security behavior, and device constraints of UltimateAuth - /// session management. - /// - /// These values influence how sessions are created, refreshed, - /// expired, revoked, and grouped into device chains. + /// Maximum number of device session chains a single user may have. + /// Set to zero to indicate no user-level chain limit. /// - public sealed class UAuthSessionOptions + public int MaxChainsPerUser { get; set; } = 0; + + /// + /// Maximum number of session rotations within a single chain. + /// Used for cleanup, replay protection, and analytics. + /// + public int MaxSessionsPerChain { get; set; } = 100; + + /// + /// Optional limit on the number of session chains allowed per platform + /// (e.g. "web" = 1, "mobile" = 1). + /// + public Dictionary? MaxChainsPerPlatform { get; set; } + + /// + /// Defines platform categories that map multiple platforms + /// into a single abstract group (e.g. mobile: [ "ios", "android", "tablet" ]). + /// + public Dictionary? PlatformCategories { get; set; } + + /// + /// Limits how many session chains can exist per platform category + /// (e.g. mobile = 1, desktop = 2). + /// + public Dictionary? MaxChainsPerCategory { get; set; } + + /// + /// Enables binding sessions to the user's IP address. + /// When enabled, IP mismatches can invalidate a session. + /// + public bool EnableIpBinding { get; set; } = false; + + /// + /// Enables binding sessions to the user's User-Agent header. + /// When enabled, UA mismatches can invalidate a session. + /// + public bool EnableUserAgentBinding { get; set; } = false; + + public DeviceMismatchBehavior DeviceMismatchBehavior { get; set; } = DeviceMismatchBehavior.Reject; + + internal UAuthSessionOptions Clone() => new() { - /// - /// The standard lifetime of a session before it expires. - /// This is the duration added during login or refresh. - /// - public TimeSpan Lifetime { get; set; } = TimeSpan.FromDays(7); - - /// - /// Maximum absolute lifetime a session may have, even when - /// sliding expiration is enabled. If null, no hard cap - /// is applied. - /// - public TimeSpan? MaxLifetime { get; set; } - - /// - /// When enabled, each refresh extends the session's expiration, - /// allowing continuous usage until MaxLifetime or idle rules apply. - /// On PureOpaque (or one token usage) mode, each touch restarts the session lifetime. - /// - public bool SlidingExpiration { get; set; } = true; - - /// - /// Maximum allowed idle time before the session becomes invalid. - /// If null or zero, idle expiration is disabled entirely. - /// - public TimeSpan? IdleTimeout { get; set; } - - /// - /// Minimum interval between LastSeenAt updates. - /// Prevents excessive writes under high traffic. - /// - public TimeSpan? TouchInterval { get; set; } = TimeSpan.FromMinutes(5); - - /// - /// Maximum number of device session chains a single user may have. - /// Set to zero to indicate no user-level chain limit. - /// - public int MaxChainsPerUser { get; set; } = 0; - - /// - /// Maximum number of session rotations within a single chain. - /// Used for cleanup, replay protection, and analytics. - /// - public int MaxSessionsPerChain { get; set; } = 100; - - /// - /// Optional limit on the number of session chains allowed per platform - /// (e.g. "web" = 1, "mobile" = 1). - /// - public Dictionary? MaxChainsPerPlatform { get; set; } - - /// - /// Defines platform categories that map multiple platforms - /// into a single abstract group (e.g. mobile: [ "ios", "android", "tablet" ]). - /// - public Dictionary? PlatformCategories { get; set; } - - /// - /// Limits how many session chains can exist per platform category - /// (e.g. mobile = 1, desktop = 2). - /// - public Dictionary? MaxChainsPerCategory { get; set; } - - /// - /// Enables binding sessions to the user's IP address. - /// When enabled, IP mismatches can invalidate a session. - /// - public bool EnableIpBinding { get; set; } = false; - - /// - /// Enables binding sessions to the user's User-Agent header. - /// When enabled, UA mismatches can invalidate a session. - /// - public bool EnableUserAgentBinding { get; set; } = false; - - public DeviceMismatchBehavior DeviceMismatchBehavior { get; set; } = DeviceMismatchBehavior.Reject; - - internal UAuthSessionOptions Clone() => new() - { - SlidingExpiration = SlidingExpiration, - IdleTimeout = IdleTimeout, - Lifetime = Lifetime, - MaxLifetime = MaxLifetime, - TouchInterval = TouchInterval, - DeviceMismatchBehavior = DeviceMismatchBehavior, - MaxChainsPerUser = MaxChainsPerUser, - MaxSessionsPerChain = MaxSessionsPerChain, - MaxChainsPerPlatform = MaxChainsPerPlatform is null ? null : new Dictionary(MaxChainsPerPlatform), - MaxChainsPerCategory = MaxChainsPerCategory is null ? null : new Dictionary(MaxChainsPerCategory), - PlatformCategories = PlatformCategories is null ? null : PlatformCategories.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.ToArray()), - EnableIpBinding = EnableIpBinding, - EnableUserAgentBinding = EnableUserAgentBinding - }; - - } + SlidingExpiration = SlidingExpiration, + IdleTimeout = IdleTimeout, + Lifetime = Lifetime, + MaxLifetime = MaxLifetime, + TouchInterval = TouchInterval, + DeviceMismatchBehavior = DeviceMismatchBehavior, + MaxChainsPerUser = MaxChainsPerUser, + MaxSessionsPerChain = MaxSessionsPerChain, + MaxChainsPerPlatform = MaxChainsPerPlatform is null ? null : new Dictionary(MaxChainsPerPlatform), + MaxChainsPerCategory = MaxChainsPerCategory is null ? null : new Dictionary(MaxChainsPerCategory), + PlatformCategories = PlatformCategories is null ? null : PlatformCategories.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.ToArray()), + EnableIpBinding = EnableIpBinding, + EnableUserAgentBinding = EnableUserAgentBinding + }; + } diff --git a/src/CodeBeam.UltimateAuth.Core/Options/UAuthSessionOptionsValidator.cs b/src/CodeBeam.UltimateAuth.Core/Options/UAuthSessionOptionsValidator.cs index 1d81b1d0..c757772b 100644 --- a/src/CodeBeam.UltimateAuth.Core/Options/UAuthSessionOptionsValidator.cs +++ b/src/CodeBeam.UltimateAuth.Core/Options/UAuthSessionOptionsValidator.cs @@ -1,100 +1,99 @@ using Microsoft.Extensions.Options; -namespace CodeBeam.UltimateAuth.Core.Options +namespace CodeBeam.UltimateAuth.Core.Options; + +internal sealed class UAuthSessionOptionsValidator : IValidateOptions { - internal sealed class UAuthSessionOptionsValidator : IValidateOptions + public ValidateOptionsResult Validate(string? name, UAuthSessionOptions options) { - public ValidateOptionsResult Validate(string? name, UAuthSessionOptions options) - { - var errors = new List(); + var errors = new List(); - if (options.Lifetime <= TimeSpan.Zero) - errors.Add("Session.Lifetime must be greater than zero."); + if (options.Lifetime <= TimeSpan.Zero) + errors.Add("Session.Lifetime must be greater than zero."); - if (options.MaxLifetime < options.Lifetime) - errors.Add("Session.MaxLifetime must be greater than or equal to Session.Lifetime."); + if (options.MaxLifetime < options.Lifetime) + errors.Add("Session.MaxLifetime must be greater than or equal to Session.Lifetime."); - if (options.IdleTimeout.HasValue && options.IdleTimeout < TimeSpan.Zero) - errors.Add("Session.IdleTimeout cannot be negative."); + if (options.IdleTimeout.HasValue && options.IdleTimeout < TimeSpan.Zero) + errors.Add("Session.IdleTimeout cannot be negative."); - if (options.IdleTimeout.HasValue && - options.IdleTimeout > TimeSpan.Zero && - options.IdleTimeout > options.MaxLifetime) - { - errors.Add("Session.IdleTimeout cannot exceed Session.MaxLifetime."); - } + if (options.IdleTimeout.HasValue && + options.IdleTimeout > TimeSpan.Zero && + options.IdleTimeout > options.MaxLifetime) + { + errors.Add("Session.IdleTimeout cannot exceed Session.MaxLifetime."); + } - if (options.MaxChainsPerUser <= 0) - errors.Add("Session.MaxChainsPerUser must be at least 1."); + if (options.MaxChainsPerUser <= 0) + errors.Add("Session.MaxChainsPerUser must be at least 1."); - if (options.MaxSessionsPerChain <= 0) - errors.Add("Session.MaxSessionsPerChain must be at least 1."); + if (options.MaxSessionsPerChain <= 0) + errors.Add("Session.MaxSessionsPerChain must be at least 1."); - if (options.MaxChainsPerPlatform != null) + if (options.MaxChainsPerPlatform != null) + { + foreach (var kv in options.MaxChainsPerPlatform) { - foreach (var kv in options.MaxChainsPerPlatform) - { - if (string.IsNullOrWhiteSpace(kv.Key)) - errors.Add("Session.MaxChainsPerPlatform contains an empty platform key."); + if (string.IsNullOrWhiteSpace(kv.Key)) + errors.Add("Session.MaxChainsPerPlatform contains an empty platform key."); - if (kv.Value <= 0) - errors.Add($"Session.MaxChainsPerPlatform['{kv.Key}'] must be >= 1."); - } + if (kv.Value <= 0) + errors.Add($"Session.MaxChainsPerPlatform['{kv.Key}'] must be >= 1."); } + } - if (options.PlatformCategories != null) + if (options.PlatformCategories != null) + { + foreach (var cat in options.PlatformCategories) { - foreach (var cat in options.PlatformCategories) + var categoryName = cat.Key; + var platforms = cat.Value; + + if (string.IsNullOrWhiteSpace(categoryName)) + errors.Add("Session.PlatformCategories contains an empty category name."); + + if (platforms == null || platforms.Length == 0) + errors.Add($"Session.PlatformCategories['{categoryName}'] must contain at least one platform."); + + var duplicates = platforms? + .GroupBy(p => p) + .Where(g => g.Count() > 1) + .Select(g => g.Key); + if (duplicates?.Any() == true) { - var categoryName = cat.Key; - var platforms = cat.Value; - - if (string.IsNullOrWhiteSpace(categoryName)) - errors.Add("Session.PlatformCategories contains an empty category name."); - - if (platforms == null || platforms.Length == 0) - errors.Add($"Session.PlatformCategories['{categoryName}'] must contain at least one platform."); - - var duplicates = platforms? - .GroupBy(p => p) - .Where(g => g.Count() > 1) - .Select(g => g.Key); - if (duplicates?.Any() == true) - { - errors.Add($"Session.PlatformCategories['{categoryName}'] contains duplicate platforms: {string.Join(", ", duplicates)}"); - } + errors.Add($"Session.PlatformCategories['{categoryName}'] contains duplicate platforms: {string.Join(", ", duplicates)}"); } } + } - if (options.MaxChainsPerCategory != null) + if (options.MaxChainsPerCategory != null) + { + foreach (var kv in options.MaxChainsPerCategory) { - foreach (var kv in options.MaxChainsPerCategory) - { - if (string.IsNullOrWhiteSpace(kv.Key)) - errors.Add("Session.MaxChainsPerCategory contains an empty category key."); + if (string.IsNullOrWhiteSpace(kv.Key)) + errors.Add("Session.MaxChainsPerCategory contains an empty category key."); - if (kv.Value <= 0) - errors.Add($"Session.MaxChainsPerCategory['{kv.Key}'] must be >= 1."); - } + if (kv.Value <= 0) + errors.Add($"Session.MaxChainsPerCategory['{kv.Key}'] must be >= 1."); } + } - if (options.PlatformCategories != null && options.MaxChainsPerCategory != null) + if (options.PlatformCategories != null && options.MaxChainsPerCategory != null) + { + foreach (var category in options.PlatformCategories.Keys) { - foreach (var category in options.PlatformCategories.Keys) + if (!options.MaxChainsPerCategory.ContainsKey(category)) { - if (!options.MaxChainsPerCategory.ContainsKey(category)) - { - errors.Add( - $"Session.MaxChainsPerCategory must define a limit for category '{category}' " + - "because it exists in Session.PlatformCategories."); - } + errors.Add( + $"Session.MaxChainsPerCategory must define a limit for category '{category}' " + + "because it exists in Session.PlatformCategories."); } } + } - if (errors.Count == 0) - return ValidateOptionsResult.Success; + if (errors.Count == 0) + return ValidateOptionsResult.Success; - return ValidateOptionsResult.Fail(errors); - } + return ValidateOptionsResult.Fail(errors); } } diff --git a/src/CodeBeam.UltimateAuth.Core/Options/UAuthTokenOptions.cs b/src/CodeBeam.UltimateAuth.Core/Options/UAuthTokenOptions.cs index 5ef2e046..9afd48d0 100644 --- a/src/CodeBeam.UltimateAuth.Core/Options/UAuthTokenOptions.cs +++ b/src/CodeBeam.UltimateAuth.Core/Options/UAuthTokenOptions.cs @@ -1,80 +1,79 @@ -namespace CodeBeam.UltimateAuth.Core.Options +namespace CodeBeam.UltimateAuth.Core.Options; + +/// +/// Configuration settings for access and refresh token behavior +/// within UltimateAuth. Includes JWT and opaque token generation, +/// lifetimes, and cryptographic settings. +/// +public sealed class UAuthTokenOptions { /// - /// Configuration settings for access and refresh token behavior - /// within UltimateAuth. Includes JWT and opaque token generation, - /// lifetimes, and cryptographic settings. + /// Determines whether JWT-format access tokens should be issued. + /// Recommended for APIs that rely on claims-based authorization. /// - public sealed class UAuthTokenOptions - { - /// - /// Determines whether JWT-format access tokens should be issued. - /// Recommended for APIs that rely on claims-based authorization. - /// - public bool IssueJwt { get; set; } = true; + public bool IssueJwt { get; set; } = true; - /// - /// Determines whether opaque tokens (session-id based) should be issued. - /// Useful for high-security APIs where token introspection is required. - /// - public bool IssueOpaque { get; set; } = true; + /// + /// Determines whether opaque tokens (session-id based) should be issued. + /// Useful for high-security APIs where token introspection is required. + /// + public bool IssueOpaque { get; set; } = true; - public bool IssueRefresh { get; set; } = true; + public bool IssueRefresh { get; set; } = true; - /// - /// Lifetime of access tokens (JWT or opaque). - /// Short lifetimes improve security but require more frequent refreshes. - /// - public TimeSpan AccessTokenLifetime { get; set; } = TimeSpan.FromMinutes(10); + /// + /// Lifetime of access tokens (JWT or opaque). + /// Short lifetimes improve security but require more frequent refreshes. + /// + public TimeSpan AccessTokenLifetime { get; set; } = TimeSpan.FromMinutes(10); - /// - /// Lifetime of refresh tokens, used in PKCE or session rotation flows. - /// - public TimeSpan RefreshTokenLifetime { get; set; } = TimeSpan.FromDays(7); + /// + /// Lifetime of refresh tokens, used in PKCE or session rotation flows. + /// + public TimeSpan RefreshTokenLifetime { get; set; } = TimeSpan.FromDays(7); - /// - /// Number of bytes of randomness used when generating opaque token IDs. - /// Larger values increase entropy and reduce collision risk. - /// - public int OpaqueIdBytes { get; set; } = 32; + /// + /// Number of bytes of randomness used when generating opaque token IDs. + /// Larger values increase entropy and reduce collision risk. + /// + public int OpaqueIdBytes { get; set; } = 32; - /// - /// Value assigned to the JWT "iss" (issuer) claim. - /// Identifies the authority that issued the token. - /// - public string Issuer { get; set; } = "UAuth"; + /// + /// Value assigned to the JWT "iss" (issuer) claim. + /// Identifies the authority that issued the token. + /// + public string Issuer { get; set; } = "UAuth"; - /// - /// Value assigned to the JWT "aud" (audience) claim. - /// Controls which clients or APIs are permitted to consume the token. - /// - public string Audience { get; set; } = "UAuthClient"; + /// + /// Value assigned to the JWT "aud" (audience) claim. + /// Controls which clients or APIs are permitted to consume the token. + /// + public string Audience { get; set; } = "UAuthClient"; - /// - /// If true, adds a unique 'jti' (JWT ID) claim to each issued JWT. - /// Useful for token replay detection and advanced auditing. - /// - public bool AddJwtIdClaim { get; set; } = false; + /// + /// If true, adds a unique 'jti' (JWT ID) claim to each issued JWT. + /// Useful for token replay detection and advanced auditing. + /// + public bool AddJwtIdClaim { get; set; } = false; - /// - /// Optional key identifier to select signing key. - /// If null, default key will be used. - /// - public string? KeyId { get; set; } + /// + /// Optional key identifier to select signing key. + /// If null, default key will be used. + /// + public string? KeyId { get; set; } - internal UAuthTokenOptions Clone() => new() - { - IssueOpaque = IssueOpaque, - IssueJwt = IssueJwt, - IssueRefresh = IssueRefresh, - AccessTokenLifetime = AccessTokenLifetime, - RefreshTokenLifetime = RefreshTokenLifetime, - OpaqueIdBytes = OpaqueIdBytes, - Issuer = Issuer, - Audience = Audience, - AddJwtIdClaim = AddJwtIdClaim, - KeyId = KeyId - }; + internal UAuthTokenOptions Clone() => new() + { + IssueOpaque = IssueOpaque, + IssueJwt = IssueJwt, + IssueRefresh = IssueRefresh, + AccessTokenLifetime = AccessTokenLifetime, + RefreshTokenLifetime = RefreshTokenLifetime, + OpaqueIdBytes = OpaqueIdBytes, + Issuer = Issuer, + Audience = Audience, + AddJwtIdClaim = AddJwtIdClaim, + KeyId = KeyId + }; - } } diff --git a/src/CodeBeam.UltimateAuth.Core/Options/UAuthTokenOptionsValidator.cs b/src/CodeBeam.UltimateAuth.Core/Options/UAuthTokenOptionsValidator.cs index c9de6e06..7d374cb0 100644 --- a/src/CodeBeam.UltimateAuth.Core/Options/UAuthTokenOptionsValidator.cs +++ b/src/CodeBeam.UltimateAuth.Core/Options/UAuthTokenOptionsValidator.cs @@ -1,49 +1,48 @@ using Microsoft.Extensions.Options; -namespace CodeBeam.UltimateAuth.Core.Options +namespace CodeBeam.UltimateAuth.Core.Options; + +internal sealed class UAuthTokenOptionsValidator : IValidateOptions { - internal sealed class UAuthTokenOptionsValidator : IValidateOptions + public ValidateOptionsResult Validate(string? name, UAuthTokenOptions options) { - public ValidateOptionsResult Validate(string? name, UAuthTokenOptions options) - { - var errors = new List(); + var errors = new List(); - if (!options.IssueJwt && !options.IssueOpaque) - errors.Add("Token: At least one of IssueJwt or IssueOpaque must be enabled."); + if (!options.IssueJwt && !options.IssueOpaque) + errors.Add("Token: At least one of IssueJwt or IssueOpaque must be enabled."); - if (options.AccessTokenLifetime <= TimeSpan.Zero) - errors.Add("Token.AccessTokenLifetime must be greater than zero."); + if (options.AccessTokenLifetime <= TimeSpan.Zero) + errors.Add("Token.AccessTokenLifetime must be greater than zero."); - if (options.RefreshTokenLifetime <= TimeSpan.Zero) - errors.Add("Token.RefreshTokenLifetime must be greater than zero."); + if (options.RefreshTokenLifetime <= TimeSpan.Zero) + errors.Add("Token.RefreshTokenLifetime must be greater than zero."); - if (options.RefreshTokenLifetime <= options.AccessTokenLifetime) - errors.Add("Token.RefreshTokenLifetime must be greater than Token.AccessTokenLifetime."); + if (options.RefreshTokenLifetime <= options.AccessTokenLifetime) + errors.Add("Token.RefreshTokenLifetime must be greater than Token.AccessTokenLifetime."); - if (options.IssueJwt) - { - if (string.IsNullOrWhiteSpace(options.Issuer)) // TODO: Min 3 chars - errors.Add("Token.Issuer must not be empty when IssueJwt = true."); + if (options.IssueJwt) + { + if (string.IsNullOrWhiteSpace(options.Issuer)) // TODO: Min 3 chars + errors.Add("Token.Issuer must not be empty when IssueJwt = true."); - if (string.IsNullOrWhiteSpace(options.Audience)) - errors.Add("Token.Audience must not be empty when IssueJwt = true."); - } + if (string.IsNullOrWhiteSpace(options.Audience)) + errors.Add("Token.Audience must not be empty when IssueJwt = true."); + } - if (options.IssueOpaque) - { - if (options.OpaqueIdBytes < 16) - errors.Add("Token.OpaqueIdBytes must be at least 16 (128-bit entropy)."); - } + if (options.IssueOpaque) + { + if (options.OpaqueIdBytes < 16) + errors.Add("Token.OpaqueIdBytes must be at least 16 (128-bit entropy)."); + } - if (options.IssueRefresh && options.RefreshTokenLifetime <= TimeSpan.Zero) - { - errors.Add("RefreshTokenLifetime must be set when IssueRefresh is enabled."); - } + if (options.IssueRefresh && options.RefreshTokenLifetime <= TimeSpan.Zero) + { + errors.Add("RefreshTokenLifetime must be set when IssueRefresh is enabled."); + } - return errors.Count == 0 - ? ValidateOptionsResult.Success - : ValidateOptionsResult.Fail(errors); - } + return errors.Count == 0 + ? ValidateOptionsResult.Success + : ValidateOptionsResult.Fail(errors); } } diff --git a/src/CodeBeam.UltimateAuth.Core/Runtime/IUAuthHubMarker.cs b/src/CodeBeam.UltimateAuth.Core/Runtime/IUAuthHubMarker.cs index 495e3cc4..d91d578b 100644 --- a/src/CodeBeam.UltimateAuth.Core/Runtime/IUAuthHubMarker.cs +++ b/src/CodeBeam.UltimateAuth.Core/Runtime/IUAuthHubMarker.cs @@ -1,10 +1,9 @@ -namespace CodeBeam.UltimateAuth.Core.Runtime +namespace CodeBeam.UltimateAuth.Core.Runtime; + +/// +/// Marker interface indicating that the current application +/// hosts an UltimateAuth Hub. +/// +public interface IUAuthHubMarker { - /// - /// Marker interface indicating that the current application - /// hosts an UltimateAuth Hub. - /// - public interface IUAuthHubMarker - { - } } diff --git a/src/CodeBeam.UltimateAuth.Core/Runtime/IUAuthProductInfoProvider.cs b/src/CodeBeam.UltimateAuth.Core/Runtime/IUAuthProductInfoProvider.cs index e7345c0b..d5238bce 100644 --- a/src/CodeBeam.UltimateAuth.Core/Runtime/IUAuthProductInfoProvider.cs +++ b/src/CodeBeam.UltimateAuth.Core/Runtime/IUAuthProductInfoProvider.cs @@ -1,9 +1,6 @@ -using CodeBeam.UltimateAuth.Core.Runtime; +namespace CodeBeam.UltimateAuth.Core.Runtime; -namespace CodeBeam.UltimateAuth.Core.Runtime +public interface IUAuthProductInfoProvider { - public interface IUAuthProductInfoProvider - { - UAuthProductInfo Get(); - } + UAuthProductInfo Get(); } diff --git a/src/CodeBeam.UltimateAuth.Core/Runtime/UAuthProductInfo.cs b/src/CodeBeam.UltimateAuth.Core/Runtime/UAuthProductInfo.cs index 3b28ca6a..629c25a9 100644 --- a/src/CodeBeam.UltimateAuth.Core/Runtime/UAuthProductInfo.cs +++ b/src/CodeBeam.UltimateAuth.Core/Runtime/UAuthProductInfo.cs @@ -1,17 +1,16 @@ using CodeBeam.UltimateAuth.Core.Options; -namespace CodeBeam.UltimateAuth.Core.Runtime +namespace CodeBeam.UltimateAuth.Core.Runtime; + +public sealed class UAuthProductInfo { - public sealed class UAuthProductInfo - { - public string ProductName { get; init; } = "UltimateAuth"; - public string Version { get; init; } = default!; - public string? InformationalVersion { get; init; } + public string ProductName { get; init; } = "UltimateAuth"; + public string Version { get; init; } = default!; + public string? InformationalVersion { get; init; } - public UAuthClientProfile ClientProfile { get; init; } - public bool ClientProfileAutoDetected { get; init; } + public UAuthClientProfile ClientProfile { get; init; } + public bool ClientProfileAutoDetected { get; init; } - public DateTimeOffset StartedAt { get; init; } - public string RuntimeId { get; init; } = Guid.NewGuid().ToString("n"); - } + public DateTimeOffset StartedAt { get; init; } + public string RuntimeId { get; init; } = Guid.NewGuid().ToString("n"); } diff --git a/src/CodeBeam.UltimateAuth.Core/Runtime/UAuthProductInfoProvider.cs b/src/CodeBeam.UltimateAuth.Core/Runtime/UAuthProductInfoProvider.cs index d6da1567..90de44f8 100644 --- a/src/CodeBeam.UltimateAuth.Core/Runtime/UAuthProductInfoProvider.cs +++ b/src/CodeBeam.UltimateAuth.Core/Runtime/UAuthProductInfoProvider.cs @@ -2,27 +2,26 @@ using Microsoft.Extensions.Options; using System.Reflection; -namespace CodeBeam.UltimateAuth.Core.Runtime +namespace CodeBeam.UltimateAuth.Core.Runtime; + +internal sealed class UAuthProductInfoProvider : IUAuthProductInfoProvider { - internal sealed class UAuthProductInfoProvider : IUAuthProductInfoProvider + private readonly UAuthProductInfo _info; + + public UAuthProductInfoProvider(IOptions options) { - private readonly UAuthProductInfo _info; + var asm = typeof(UAuthProductInfoProvider).Assembly; - public UAuthProductInfoProvider(IOptions options) + _info = new UAuthProductInfo { - var asm = typeof(UAuthProductInfoProvider).Assembly; - - _info = new UAuthProductInfo - { - Version = asm.GetName().Version?.ToString(3) ?? "unknown", - InformationalVersion = asm.GetCustomAttribute()?.InformationalVersion, + Version = asm.GetName().Version?.ToString(3) ?? "unknown", + InformationalVersion = asm.GetCustomAttribute()?.InformationalVersion, - ClientProfile = options.Value.ClientProfile, - ClientProfileAutoDetected = options.Value.AutoDetectClientProfile, - StartedAt = DateTimeOffset.UtcNow - }; - } - - public UAuthProductInfo Get() => _info; + ClientProfile = options.Value.ClientProfile, + ClientProfileAutoDetected = options.Value.AutoDetectClientProfile, + StartedAt = DateTimeOffset.UtcNow + }; } + + public UAuthProductInfo Get() => _info; } diff --git a/src/CodeBeam.UltimateAuth.Server/Composition/UltimateAuthServerBuilderValidation.cs b/src/CodeBeam.UltimateAuth.Server/Composition/UltimateAuthServerBuilderValidation.cs index 780d68f1..8370bce1 100644 --- a/src/CodeBeam.UltimateAuth.Server/Composition/UltimateAuthServerBuilderValidation.cs +++ b/src/CodeBeam.UltimateAuth.Server/Composition/UltimateAuthServerBuilderValidation.cs @@ -12,8 +12,8 @@ public static IServiceCollection Build(this UltimateAuthServerBuilder builder) if (!services.Any(sd => sd.ServiceType == typeof(IUAuthPasswordHasher))) throw new InvalidOperationException("No IUAuthPasswordHasher registered. Call UseArgon2() or another hasher."); - if (!services.Any(sd => sd.ServiceType.IsAssignableTo(typeof(IUAuthUserStore<>)))) - throw new InvalidOperationException("No credential store registered."); + //if (!services.Any(sd => sd.ServiceType.IsAssignableTo(typeof(IUAuthUserStore<>)))) + // throw new InvalidOperationException("No credential store registered."); if (!services.Any(sd => sd.ServiceType.IsAssignableTo(typeof(ISessionStore)))) throw new InvalidOperationException("No session store registered."); diff --git a/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthServerServiceCollectionExtensions.cs b/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthServerServiceCollectionExtensions.cs index 494af80d..a885acc1 100644 --- a/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthServerServiceCollectionExtensions.cs +++ b/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthServerServiceCollectionExtensions.cs @@ -176,7 +176,6 @@ private static IServiceCollection AddUltimateAuthServerInternal(this IServiceCol services.TryAddScoped(); services.TryAddScoped(); services.TryAddScoped(); - services.TryAddScoped(); services.TryAddScoped(typeof(ISessionQueryService), typeof(UAuthSessionQueryService)); services.TryAddScoped(typeof(IRefreshTokenResolver), typeof(DefaultRefreshTokenResolver)); services.TryAddScoped(typeof(ISessionTouchService), typeof(DefaultSessionTouchService)); diff --git a/src/CodeBeam.UltimateAuth.Server/Login/DefaultLoginOrchestrator.cs b/src/CodeBeam.UltimateAuth.Server/Login/DefaultLoginOrchestrator.cs index 028f6608..56243758 100644 --- a/src/CodeBeam.UltimateAuth.Server/Login/DefaultLoginOrchestrator.cs +++ b/src/CodeBeam.UltimateAuth.Server/Login/DefaultLoginOrchestrator.cs @@ -83,7 +83,7 @@ public async Task LoginAsync(AuthFlowContext flow, LoginRequest req { securityState = await _userSecurityStateProvider.GetAsync(request.TenantId, validatedUserId, ct); var converter = _userIdConverterResolver.GetConverter(); - userKey = UserKey.FromString(converter.ToString(validatedUserId)); + userKey = UserKey.FromString(converter.ToCanonicalString(validatedUserId)); } var user = userKey is not null diff --git a/src/CodeBeam.UltimateAuth.Server/Options/UAuthServerProfileDetector.cs b/src/CodeBeam.UltimateAuth.Server/Options/UAuthServerProfileDetector.cs deleted file mode 100644 index 8eedf8a6..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Options/UAuthServerProfileDetector.cs +++ /dev/null @@ -1,28 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Options; -using Microsoft.Extensions.DependencyInjection; - -namespace CodeBeam.UltimateAuth.Server.Options -{ - internal sealed class UAuthServerProfileDetector : IServerProfileDetector - { - public UAuthClientProfile Detect(IServiceProvider sp) - { - if (Type.GetType("Microsoft.Maui.Controls.Application, Microsoft.Maui.Controls",throwOnError: false) is not null) - return UAuthClientProfile.Maui; - - if (AppDomain.CurrentDomain.GetAssemblies().Any(a => a.GetName().Name == "Microsoft.AspNetCore.Components.WebAssembly")) - return UAuthClientProfile.BlazorWasm; - - // Warning: This detection method may not be 100% reliable in all hosting scenarios. - if (AppDomain.CurrentDomain.GetAssemblies().Any(a => a.GetName().Name == "Microsoft.AspNetCore.Components.Server")) - { - return UAuthClientProfile.BlazorServer; - } - - //if (sp.GetService() is not null) - // return UAuthClientProfile.WebServer; - - return UAuthClientProfile.NotSpecified; - } - } -} diff --git a/src/CodeBeam.UltimateAuth.Server/Services/DefaultSessionService.cs b/src/CodeBeam.UltimateAuth.Server/Services/DefaultSessionService.cs deleted file mode 100644 index 7e444b25..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Services/DefaultSessionService.cs +++ /dev/null @@ -1,35 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Abstractions; -using CodeBeam.UltimateAuth.Core.Contracts; -using CodeBeam.UltimateAuth.Core.Domain; -using CodeBeam.UltimateAuth.Server.Infrastructure; -using CodeBeam.UltimateAuth.Server.Infrastructure.Orchestrator; - -namespace CodeBeam.UltimateAuth.Server.Services -{ - internal sealed class DefaultSessionService : ISessionService - { - private readonly ISessionOrchestrator _orchestrator; - private readonly IClock _clock; - - public DefaultSessionService(ISessionOrchestrator orchestrator, IClock clock) - { - _orchestrator = orchestrator; - _clock = clock; - } - - public Task RevokeAllAsync(AuthContext authContext, UserKey userKey, CancellationToken ct) - { - return _orchestrator.ExecuteAsync(authContext, new RevokeAllUserSessionsCommand(userKey), ct); - } - - public Task RevokeAllExceptChainAsync(AuthContext authContext, UserKey userKey, SessionChainId exceptChainId, CancellationToken ct) - { - return _orchestrator.ExecuteAsync(authContext, new RevokeAllChainsCommand(userKey, exceptChainId), ct); - } - - public Task RevokeRootAsync(AuthContext authContext, UserKey userKey, CancellationToken ct) - { - return _orchestrator.ExecuteAsync(authContext, new RevokeRootCommand(userKey), ct); - } - } -} From 942e91a05a9827fc2b06a470232ea6dce10d6f2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Can=20Karag=C3=B6z?= Date: Sun, 1 Feb 2026 16:39:35 +0300 Subject: [PATCH 3/9] Remove Base64Url Duplicated Methods --- .../Services/DefaultFlowClient.cs | 13 +++---------- .../Options/HeaderTokenFormat.cs | 11 +++++------ .../Options/TokenResponseMode.cs | 15 +++++++-------- .../Pkce/PkceAuthorizationValidator.cs | 13 +++---------- .../Core/UserIdConverterTests.cs | 14 +++++++------- 5 files changed, 25 insertions(+), 41 deletions(-) diff --git a/src/CodeBeam.UltimateAuth.Client/Services/DefaultFlowClient.cs b/src/CodeBeam.UltimateAuth.Client/Services/DefaultFlowClient.cs index cda6ed49..9466d623 100644 --- a/src/CodeBeam.UltimateAuth.Client/Services/DefaultFlowClient.cs +++ b/src/CodeBeam.UltimateAuth.Client/Services/DefaultFlowClient.cs @@ -6,6 +6,7 @@ using CodeBeam.UltimateAuth.Client.Options; using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.Infrastructure; using CodeBeam.UltimateAuth.Core.Options; using Microsoft.AspNetCore.Components; using Microsoft.Extensions.Options; @@ -200,22 +201,14 @@ private Task NavigateToHubLoginAsync(string authorizationCode, string codeVerifi private static string CreateVerifier() { var bytes = RandomNumberGenerator.GetBytes(32); - return Base64UrlEncode(bytes); + return Base64Url.Encode(bytes); } private static string CreateChallenge(string verifier) { using var sha256 = SHA256.Create(); var hash = sha256.ComputeHash(Encoding.ASCII.GetBytes(verifier)); - return Base64UrlEncode(hash); - } - - private static string Base64UrlEncode(byte[] input) - { - return Convert.ToBase64String(input) - .TrimEnd('=') - .Replace('+', '-') - .Replace('/', '_'); + return Base64Url.Encode(hash); } } } diff --git a/src/CodeBeam.UltimateAuth.Core/Options/HeaderTokenFormat.cs b/src/CodeBeam.UltimateAuth.Core/Options/HeaderTokenFormat.cs index 691dbd40..826703c8 100644 --- a/src/CodeBeam.UltimateAuth.Core/Options/HeaderTokenFormat.cs +++ b/src/CodeBeam.UltimateAuth.Core/Options/HeaderTokenFormat.cs @@ -1,8 +1,7 @@ -namespace CodeBeam.UltimateAuth.Core.Options +namespace CodeBeam.UltimateAuth.Core.Options; + +public enum HeaderTokenFormat { - public enum HeaderTokenFormat - { - Bearer, - Raw - } + Bearer, + Raw } diff --git a/src/CodeBeam.UltimateAuth.Core/Options/TokenResponseMode.cs b/src/CodeBeam.UltimateAuth.Core/Options/TokenResponseMode.cs index ce777e15..5d5ded63 100644 --- a/src/CodeBeam.UltimateAuth.Core/Options/TokenResponseMode.cs +++ b/src/CodeBeam.UltimateAuth.Core/Options/TokenResponseMode.cs @@ -1,10 +1,9 @@ -namespace CodeBeam.UltimateAuth.Core.Options +namespace CodeBeam.UltimateAuth.Core.Options; + +public enum TokenResponseMode { - public enum TokenResponseMode - { - None, - Cookie, - Header, - Body - } + None, + Cookie, + Header, + Body } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Pkce/PkceAuthorizationValidator.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Pkce/PkceAuthorizationValidator.cs index d479e3a7..c9478627 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Pkce/PkceAuthorizationValidator.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Pkce/PkceAuthorizationValidator.cs @@ -1,4 +1,5 @@ -using System.Security.Cryptography; +using CodeBeam.UltimateAuth.Core.Infrastructure; +using System.Security.Cryptography; using System.Text; namespace CodeBeam.UltimateAuth.Server.Infrastructure; @@ -55,16 +56,8 @@ private static bool IsVerifierValid(string verifier, string expectedChallenge) using var sha256 = SHA256.Create(); byte[] hash = sha256.ComputeHash(Encoding.ASCII.GetBytes(verifier)); - string computedChallenge = Base64UrlEncode(hash); + string computedChallenge = Base64Url.Encode(hash); return CryptographicOperations.FixedTimeEquals(Encoding.ASCII.GetBytes(computedChallenge), Encoding.ASCII.GetBytes(expectedChallenge)); } - - private static string Base64UrlEncode(byte[] input) - { - return Convert.ToBase64String(input) - .TrimEnd('=') - .Replace('+', '-') - .Replace('/', '_'); - } } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Core/UserIdConverterTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Core/UserIdConverterTests.cs index a5d74ae8..0516b946 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Core/UserIdConverterTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Core/UserIdConverterTests.cs @@ -13,7 +13,7 @@ public void UserKey_Roundtrip_Should_Preserve_Value() var key = UserKey.New(); var converter = new UAuthUserIdConverter(); - var str = converter.ToString(key); + var str = converter.ToCanonicalString(key); var parsed = converter.FromString(str); Assert.Equal(key, parsed); @@ -25,7 +25,7 @@ public void Guid_Roundtrip_Should_Work() var id = Guid.NewGuid(); var converter = new UAuthUserIdConverter(); - var str = converter.ToString(id); + var str = converter.ToCanonicalString(id); var parsed = converter.FromString(str); Assert.Equal(id, parsed); @@ -37,7 +37,7 @@ public void String_Roundtrip_Should_Work() var id = "user_123"; var converter = new UAuthUserIdConverter(); - var str = converter.ToString(id); + var str = converter.ToCanonicalString(id); var parsed = converter.FromString(str); Assert.Equal(id, parsed); @@ -49,7 +49,7 @@ public void Int_Should_Use_Invariant_Culture() var id = 1234; var converter = new UAuthUserIdConverter(); - var str = converter.ToString(id); + var str = converter.ToCanonicalString(id); Assert.Equal(id.ToString(CultureInfo.InvariantCulture), str); } @@ -60,7 +60,7 @@ public void Long_Roundtrip_Should_Work() var id = 9_223_372_036_854_775_000L; var converter = new UAuthUserIdConverter(); - var str = converter.ToString(id); + var str = converter.ToCanonicalString(id); var parsed = converter.FromString(str); Assert.Equal(id, parsed); @@ -71,7 +71,7 @@ public void Double_UserId_Should_Throw() { var converter = new UAuthUserIdConverter(); - Assert.ThrowsAny(() => converter.ToString(12.34)); + Assert.ThrowsAny(() => converter.ToCanonicalString(12.34)); } private sealed class CustomUserId @@ -84,7 +84,7 @@ public void Custom_UserId_Should_Fail() { var converter = new UAuthUserIdConverter(); - Assert.ThrowsAny(() => converter.ToString(new CustomUserId())); + Assert.ThrowsAny(() => converter.ToCanonicalString(new CustomUserId())); } [Fact] From 00b1e50593993d7d54aef230d14481f684933d0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Can=20Karag=C3=B6z?= Date: Mon, 2 Feb 2026 01:25:55 +0300 Subject: [PATCH 4/9] Remove Session Domain Interfaces & EFCore Session Multi Tenant Enhancements --- .../Services/IUAuthSessionManager.cs | 39 +- .../Abstractions/Stores/ISessionStore.cs | 45 --- .../Stores/ISessionStoreKernel.cs | 24 +- .../Stores/ITenantAwareSessionStore.cs | 6 - .../Contracts/Session/IssuedSession.cs | 2 +- .../Session/ResolvedRefreshSession.cs | 8 +- .../Contracts/Session/SessionResult.cs | 39 -- .../Contracts/Token/TokenIssueContext.cs | 2 +- .../Domain/Session/ISession.cs | 83 ---- .../Domain/Session/ISessionChain.cs | 66 --- .../Domain/Session/ISessionRoot.cs | 60 --- .../Domain/Session/RefreshOutcome.cs | 17 +- .../Domain/Session/SessionChainId.cs | 45 +-- .../Domain/Session/SessionMetadata.cs | 67 ++-- .../Domain/Session/SessionRefreshStatus.cs | 15 +- .../Domain/Session/SessionRootId.cs | 35 +- .../Domain/Session/SessionState.cs | 29 +- .../Domain/Session/UAuthSession.cs | 378 +++++++++--------- .../Domain/Session/UAuthSessionChain.cs | 267 ++++++------- .../Domain/Session/UAuthSessionRoot.cs | 195 +++++---- .../Domain/User/AuthUserRecord.cs | 49 --- .../UAuthSessionStoreKernelFactory.cs | 19 - .../MultiTenancy/TenantContext.cs | 13 + .../MultiTenancy/TenantValidation.cs | 4 +- .../UltimateAuthServerBuilderValidation.cs | 2 +- .../Orchestrator/RevokeChainCommand.cs | 12 +- .../Orchestrator/RevokeSessionCommand.cs | 13 +- .../Refresh/DefaultSessionTouchService.cs | 59 +-- .../Infrastructure/User/UAuthUserAccessor.cs | 53 ++- .../Issuers/UAuthSessionIssuer.cs | 110 +++-- .../Services/ISessionQueryService.cs | 38 +- .../Services/UAuthFlowService.cs | 2 +- .../Services/UAuthSessionManager.cs | 109 ++--- .../Services/UAuthSessionQueryService.cs | 150 +++---- .../Stores/UAuthSessionStoreFactory.cs | 33 -- .../AuthSessionIdEfConverter.cs | 38 -- .../{ => Data}/UAuthSessionDbContext.cs | 17 +- .../EfCoreSessionStore.cs | 374 ----------------- .../EfCoreSessionStoreKernelFactory.cs | 20 - .../ServiceCollectionExtensions.cs | 1 - .../AuthSessionIdEfConverter.cs | 36 ++ .../JsonValueConverter.cs | 0 .../NullableAuthSessionIdConverter.cs | 0 .../Mappers/SessionChainProjectionMapper.cs | 4 +- .../Mappers/SessionProjectionMapper.cs | 4 +- .../Mappers/SessionRootProjectionMapper.cs | 6 +- .../{ => Stores}/EfCoreSessionStoreKernel.cs | 23 +- .../Stores/EfCoreSessionStoreKernelFactory.cs | 25 ++ .../InMemorySessionStore.cs | 156 -------- .../InMemorySessionStoreFactory.cs | 24 -- .../InMemorySessionStoreKernel.cs | 47 ++- .../InMemorySessionStoreKernelFactory.cs | 17 + .../ServiceCollectionExtensions.cs | 15 +- 53 files changed, 1002 insertions(+), 1893 deletions(-) delete mode 100644 src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ISessionStore.cs delete mode 100644 src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ITenantAwareSessionStore.cs delete mode 100644 src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionResult.cs delete mode 100644 src/CodeBeam.UltimateAuth.Core/Domain/Session/ISession.cs delete mode 100644 src/CodeBeam.UltimateAuth.Core/Domain/Session/ISessionChain.cs delete mode 100644 src/CodeBeam.UltimateAuth.Core/Domain/Session/ISessionRoot.cs delete mode 100644 src/CodeBeam.UltimateAuth.Core/Domain/User/AuthUserRecord.cs delete mode 100644 src/CodeBeam.UltimateAuth.Core/Infrastructure/UAuthSessionStoreKernelFactory.cs create mode 100644 src/CodeBeam.UltimateAuth.Core/MultiTenancy/TenantContext.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Stores/UAuthSessionStoreFactory.cs delete mode 100644 src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/AuthSessionIdEfConverter.cs rename src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/{ => Data}/UAuthSessionDbContext.cs (86%) delete mode 100644 src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/EfCoreSessionStore.cs delete mode 100644 src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/EfCoreSessionStoreKernelFactory.cs rename src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/{ => Extensions}/ServiceCollectionExtensions.cs (90%) create mode 100644 src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Infrastructure/AuthSessionIdEfConverter.cs rename src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/{ => Infrastructure}/JsonValueConverter.cs (100%) rename src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/{ => Infrastructure}/NullableAuthSessionIdConverter.cs (100%) rename src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/{ => Stores}/EfCoreSessionStoreKernel.cs (88%) create mode 100644 src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Stores/EfCoreSessionStoreKernelFactory.cs delete mode 100644 src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/InMemorySessionStore.cs delete mode 100644 src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/InMemorySessionStoreFactory.cs create mode 100644 src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/InMemorySessionStoreKernelFactory.cs diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Services/IUAuthSessionManager.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Services/IUAuthSessionManager.cs index 9cded19d..996b5a64 100644 --- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Services/IUAuthSessionManager.cs +++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Services/IUAuthSessionManager.cs @@ -3,24 +3,29 @@ namespace CodeBeam.UltimateAuth.Core.Abstractions; /// -/// Provides high-level session lifecycle operations such as creation, refresh, validation, and revocation. +/// Application-level session command API. +/// Represents explicit intent to mutate session state. +/// All operations are authorization- and policy-aware. /// public interface IUAuthSessionManager { - Task> GetChainsAsync(string? tenantId, UserKey userKey); - - Task> GetSessionsAsync(string? tenantId, SessionChainId chainId); - - Task GetCurrentSessionAsync(string? tenantId, AuthSessionId sessionId); - - Task RevokeSessionAsync(string? tenantId, AuthSessionId sessionId, DateTimeOffset at); - - Task RevokeChainAsync(string? tenantId, SessionChainId chainId, DateTimeOffset at); - - Task ResolveChainIdAsync(string? tenantId, AuthSessionId sessionId); - - Task RevokeAllChainsAsync(string? tenantId, UserKey userKey, SessionChainId? exceptChainId, DateTimeOffset at); - - // Hard revoke - admin - Task RevokeRootAsync(string? tenantId, UserKey userKey, DateTimeOffset at); + /// + /// Revokes a single session (logout current device). + /// + Task RevokeSessionAsync(AuthSessionId sessionId, CancellationToken ct = default); + + /// + /// Revokes all sessions in a specific chain (logout a device). + /// + Task RevokeChainAsync(SessionChainId chainId, CancellationToken ct = default); + + /// + /// Revokes all session chains for the current user (logout all devices). + /// + Task RevokeAllChainsAsync(UserKey userKey, SessionChainId? exceptChainId = null, CancellationToken ct = default); + + /// + /// Hard revoke: revokes the entire session root (admin / security action). + /// + Task RevokeRootAsync(UserKey userKey, CancellationToken ct = default); } diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ISessionStore.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ISessionStore.cs deleted file mode 100644 index 1a8a69a5..00000000 --- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ISessionStore.cs +++ /dev/null @@ -1,45 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Contracts; -using CodeBeam.UltimateAuth.Core.Domain; - -namespace CodeBeam.UltimateAuth.Core.Abstractions; - -/// -/// High-level session store abstraction used by UltimateAuth. -/// Encapsulates session, chain, and root orchestration. -/// -public interface ISessionStore -{ - /// - /// Retrieves an active session by id. - /// - Task GetSessionAsync(string? tenantId, AuthSessionId sessionId, CancellationToken ct = default); - - /// - /// Creates a new session and associates it with the appropriate chain and root. - /// - Task CreateSessionAsync(IssuedSession issuedSession, SessionStoreContext context, CancellationToken ct = default); - - /// - /// Refreshes (rotates) the active session within its chain. - /// - Task RotateSessionAsync(AuthSessionId currentSessionId, IssuedSession newSession, SessionStoreContext context, CancellationToken ct = default); - - Task TouchSessionAsync(string? tenantId, AuthSessionId sessionId, DateTimeOffset at, SessionTouchMode mode = SessionTouchMode.IfNeeded, CancellationToken ct = default); - - /// - /// Revokes a single session. - /// - Task RevokeSessionAsync(string? tenantId, AuthSessionId sessionId, DateTimeOffset at, CancellationToken ct = default); - - /// - /// Revokes all sessions for a specific user (all devices). - /// - Task RevokeAllChainsAsync(string? tenantId, UserKey userKey, SessionChainId? exceptChainId, DateTimeOffset at, CancellationToken ct = default); - - /// - /// Revokes all sessions within a specific chain (single device). - /// - Task RevokeChainAsync(string? tenantId, SessionChainId chainId, DateTimeOffset at, CancellationToken ct = default); - - Task RevokeRootAsync(string? tenantId, UserKey userKey, DateTimeOffset at, CancellationToken ct = default); -} diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ISessionStoreKernel.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ISessionStoreKernel.cs index 1ddced8f..05148ed1 100644 --- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ISessionStoreKernel.cs +++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ISessionStoreKernel.cs @@ -6,29 +6,23 @@ public interface ISessionStoreKernel { Task ExecuteAsync(Func action, CancellationToken ct = default); - // Session - Task GetSessionAsync(AuthSessionId sessionId); - Task SaveSessionAsync(ISession session); + Task GetSessionAsync(AuthSessionId sessionId); + Task SaveSessionAsync(UAuthSession session); Task RevokeSessionAsync(AuthSessionId sessionId, DateTimeOffset at); - // Chain - Task GetChainAsync(SessionChainId chainId); - Task SaveChainAsync(ISessionChain chain); + Task GetChainAsync(SessionChainId chainId); + Task SaveChainAsync(UAuthSessionChain chain); Task RevokeChainAsync(SessionChainId chainId, DateTimeOffset at); Task GetActiveSessionIdAsync(SessionChainId chainId); Task SetActiveSessionIdAsync(SessionChainId chainId, AuthSessionId sessionId); - // Root - Task GetSessionRootByUserAsync(UserKey userKey); - Task GetSessionRootByIdAsync(SessionRootId rootId); - Task SaveSessionRootAsync(ISessionRoot root); + Task GetSessionRootByUserAsync(UserKey userKey); + Task GetSessionRootByIdAsync(SessionRootId rootId); + Task SaveSessionRootAsync(UAuthSessionRoot root); Task RevokeSessionRootAsync(UserKey userKey, DateTimeOffset at); - // Helpers Task GetChainIdBySessionAsync(AuthSessionId sessionId); - Task> GetChainsByUserAsync(UserKey userKey); - Task> GetSessionsByChainAsync(SessionChainId chainId); - - // Maintenance + Task> GetChainsByUserAsync(UserKey userKey); + Task> GetSessionsByChainAsync(SessionChainId chainId); Task DeleteExpiredSessionsAsync(DateTimeOffset at); } diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ITenantAwareSessionStore.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ITenantAwareSessionStore.cs deleted file mode 100644 index 08a9f23e..00000000 --- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ITenantAwareSessionStore.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace CodeBeam.UltimateAuth.Core.Abstractions; - -public interface ITenantAwareSessionStore -{ - void BindTenant(string? tenantId); -} diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/IssuedSession.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/IssuedSession.cs index aecbe0f9..6a76788b 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/IssuedSession.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/IssuedSession.cs @@ -10,7 +10,7 @@ public sealed class IssuedSession /// /// The issued domain session. /// - public required ISession Session { get; init; } + public required UAuthSession Session { get; init; } /// /// Opaque session identifier returned to the client. diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/ResolvedRefreshSession.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/ResolvedRefreshSession.cs index 513e54ec..42c51971 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/ResolvedRefreshSession.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/ResolvedRefreshSession.cs @@ -7,8 +7,8 @@ public sealed record ResolvedRefreshSession public bool IsValid { get; init; } public bool IsReuseDetected { get; init; } - public ISession? Session { get; init; } - public ISessionChain? Chain { get; init; } + public UAuthSession? Session { get; init; } + public UAuthSessionChain? Chain { get; init; } private ResolvedRefreshSession() { } @@ -25,9 +25,7 @@ public static ResolvedRefreshSession Reused() IsReuseDetected = true }; - public static ResolvedRefreshSession Valid( - ISession session, - ISessionChain chain) + public static ResolvedRefreshSession Valid(UAuthSession session, UAuthSessionChain chain) => new() { IsValid = true, diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionResult.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionResult.cs deleted file mode 100644 index a08d673e..00000000 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionResult.cs +++ /dev/null @@ -1,39 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Domain; - -namespace CodeBeam.UltimateAuth.Core.Contracts; - -// TODO: IsNewChain, IsNewRoot flags? -/// -/// Represents the result of a session operation within UltimateAuth, such as -/// login or session refresh. -/// -/// A session operation may produce: -/// - a newly created session, -/// - an updated session chain (rotation), -/// - an updated session root (e.g., after adding a new chain). -/// -/// This wrapper provides a unified model so downstream components — such as -/// token services, event emitters, logging pipelines, or application-level -/// consumers — can easily access all updated authentication structures. -/// -public sealed class SessionResult -{ - /// - /// Gets the active session produced by the operation. - /// This is the newest session and the one that should be used when issuing tokens. - /// - public required ISession Session { get; init; } - - /// - /// Gets the session chain associated with the session. - /// The chain may be newly created (login) or updated (session rotation). - /// - public required ISessionChain Chain { get; init; } - - /// - /// Gets the user's session root. - /// This structure may be updated when new chains are added or when security - /// properties change. - /// - public required ISessionRoot Root { get; init; } -} diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenIssueContext.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenIssueContext.cs index d31377e5..37a20cbf 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenIssueContext.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenIssueContext.cs @@ -5,6 +5,6 @@ namespace CodeBeam.UltimateAuth.Core.Contracts; public sealed record TokenIssueContext { public string? TenantId { get; init; } - public ISession Session { get; init; } = default!; + public UAuthSession Session { get; init; } = default!; public DateTimeOffset At { get; init; } } diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Session/ISession.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Session/ISession.cs deleted file mode 100644 index 9de8c16a..00000000 --- a/src/CodeBeam.UltimateAuth.Core/Domain/Session/ISession.cs +++ /dev/null @@ -1,83 +0,0 @@ -namespace CodeBeam.UltimateAuth.Core.Domain; - -/// -/// Represents a single authentication session belonging to a user. -/// Sessions are immutable, security-critical units used for validation, -/// sliding expiration, revocation, and device analytics. -/// -public interface ISession -{ - /// - /// Gets the unique identifier of the session. - /// - AuthSessionId SessionId { get; } - - string? TenantId { get; } - - /// - /// Gets the identifier of the user who owns this session. - /// - UserKey UserKey { get; } - - SessionChainId ChainId { get; } - - /// - /// Gets the timestamp when this session was originally created. - /// - DateTimeOffset CreatedAt { get; } - - /// - /// Gets the timestamp when the session becomes invalid due to expiration. - /// - DateTimeOffset ExpiresAt { get; } - - /// - /// Gets the timestamp of the last successful usage. - /// Used when evaluating sliding expiration policies. - /// - DateTimeOffset? LastSeenAt { get; } - - /// - /// Gets a value indicating whether this session has been explicitly revoked. - /// - bool IsRevoked { get; } - - /// - /// Gets the timestamp when the session was revoked, if applicable. - /// - DateTimeOffset? RevokedAt { get; } - - /// - /// Gets the user's security version at the moment of session creation. - /// If the stored version does not match the user's current version, - /// the session becomes invalid (e.g., after password or MFA reset). - /// - long SecurityVersionAtCreation { get; } - - /// - /// Gets metadata describing the client device that created the session. - /// Includes platform, OS, IP address, fingerprint, and more. - /// - DeviceContext Device { get; } - - ClaimsSnapshot Claims { get; } - - /// - /// Gets session-scoped metadata used for application-specific extensions, - /// such as tenant data, app version, locale, or CSRF tokens. - /// - SessionMetadata Metadata { get; } - - /// - /// Computes the effective runtime state of the session (Active, Expired, - /// Revoked, SecurityVersionMismatch, etc.) based on the provided timestamp. - /// - /// The evaluated of this session. - SessionState GetState(DateTimeOffset at, TimeSpan? idleTimeout); - - ISession Touch(DateTimeOffset now); - ISession Revoke(DateTimeOffset at); - - ISession WithChain(SessionChainId chainId); - -} diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Session/ISessionChain.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Session/ISessionChain.cs deleted file mode 100644 index 7659f475..00000000 --- a/src/CodeBeam.UltimateAuth.Core/Domain/Session/ISessionChain.cs +++ /dev/null @@ -1,66 +0,0 @@ -namespace CodeBeam.UltimateAuth.Core.Domain -{ - /// - /// Represents a device- or login-scoped session chain. - /// A chain groups all rotated sessions belonging to a single logical login - /// (e.g., a browser instance, mobile app installation, or device fingerprint). - /// - public interface ISessionChain - { - /// - /// Gets the unique identifier of the session chain. - /// - SessionChainId ChainId { get; } - - SessionRootId RootId { get; } - - string? TenantId { get; } - - /// - /// Gets the identifier of the user who owns this chain. - /// Each chain represents one device/login family for this user. - /// - UserKey UserKey { get; } - - /// - /// Gets the number of refresh token rotations performed within this chain. - /// - int RotationCount { get; } - - /// - /// Gets the user's security version at the time the chain was created. - /// If the user's current security version is higher, the entire chain - /// becomes invalid (e.g., after password or MFA reset). - /// - long SecurityVersionAtCreation { get; } - - /// - /// Gets an optional snapshot of claims taken at chain creation time. - /// Useful for offline clients, WASM apps, and environments where - /// full user lookup cannot be performed on each request. - /// - ClaimsSnapshot ClaimsSnapshot { get; } - - /// - /// Gets the identifier of the currently active authentication session, if one exists. - /// - AuthSessionId? ActiveSessionId { get; } - - /// - /// Gets a value indicating whether this chain has been revoked. - /// Revoking a chain performs a device-level logout, invalidating - /// all sessions it contains. - /// - bool IsRevoked { get; } - - /// - /// Gets the timestamp when the chain was revoked, if applicable. - /// - DateTimeOffset? RevokedAt { get; } - - ISessionChain AttachSession(AuthSessionId sessionId); - ISessionChain RotateSession(AuthSessionId sessionId); - ISessionChain Revoke(DateTimeOffset at); - } - -} diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Session/ISessionRoot.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Session/ISessionRoot.cs deleted file mode 100644 index b839292a..00000000 --- a/src/CodeBeam.UltimateAuth.Core/Domain/Session/ISessionRoot.cs +++ /dev/null @@ -1,60 +0,0 @@ -namespace CodeBeam.UltimateAuth.Core.Domain -{ - /// - /// Represents the root container for all authentication session chains of a user. - /// A session root is tenant-scoped and acts as the authoritative security boundary, - /// controlling global revocation, security versioning, and device/login families. - /// - public interface ISessionRoot - { - SessionRootId RootId { get; } - - /// - /// Gets the tenant identifier associated with this session root. - /// Used to isolate authentication domains in multi-tenant systems. - /// - string? TenantId { get; } - - /// - /// Gets the identifier of the user who owns this session root. - /// Each user has one root per tenant. - /// - UserKey UserKey { get; } - - /// - /// Gets a value indicating whether the entire session root is revoked. - /// When true, all chains and sessions belonging to this root are invalid, - /// regardless of their individual states. - /// - bool IsRevoked { get; } - - /// - /// Gets the timestamp when the session root was revoked, if applicable. - /// - DateTimeOffset? RevokedAt { get; } - - /// - /// Gets the current security version of the user within this tenant. - /// Incrementing this value invalidates all sessions, even if they are still active. - /// Common triggers include password reset, MFA reset, and account recovery. - /// - long SecurityVersion { get; } - - /// - /// Gets the complete set of session chains associated with this root. - /// Each chain represents a device or login-family (browser instance, mobile app, etc.). - /// The root is immutable; modifications must go through SessionService or SessionStore. - /// - IReadOnlyList Chains { get; } - - /// - /// Gets the timestamp when this root structure was last updated. - /// Useful for caching, concurrency handling, and incremental synchronization. - /// - DateTimeOffset LastUpdatedAt { get; } - - ISessionRoot AttachChain(ISessionChain chain, DateTimeOffset at); - - ISessionRoot Revoke(DateTimeOffset at); - } -} diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Session/RefreshOutcome.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Session/RefreshOutcome.cs index dec5c2f1..f0aa8db4 100644 --- a/src/CodeBeam.UltimateAuth.Core/Domain/Session/RefreshOutcome.cs +++ b/src/CodeBeam.UltimateAuth.Core/Domain/Session/RefreshOutcome.cs @@ -1,11 +1,10 @@ -namespace CodeBeam.UltimateAuth.Core.Domain +namespace CodeBeam.UltimateAuth.Core.Domain; + +public enum RefreshOutcome { - public enum RefreshOutcome - { - None, - NoOp, - Touched, - Rotated, - ReauthRequired - } + None, + NoOp, + Touched, + Rotated, + ReauthRequired } diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Session/SessionChainId.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Session/SessionChainId.cs index 5c253e65..d2edc1d3 100644 --- a/src/CodeBeam.UltimateAuth.Core/Domain/Session/SessionChainId.cs +++ b/src/CodeBeam.UltimateAuth.Core/Domain/Session/SessionChainId.cs @@ -1,33 +1,32 @@ -namespace CodeBeam.UltimateAuth.Core.Domain +namespace CodeBeam.UltimateAuth.Core.Domain; + +public readonly record struct SessionChainId(Guid Value) { - public readonly record struct SessionChainId(Guid Value) - { - public static SessionChainId New() => new(Guid.NewGuid()); + public static SessionChainId New() => new(Guid.NewGuid()); - /// - /// Indicates that the chain must be assigned by the store. - /// - public static readonly SessionChainId Unassigned = new(Guid.Empty); + /// + /// Indicates that the chain must be assigned by the store. + /// + public static readonly SessionChainId Unassigned = new(Guid.Empty); - public bool IsUnassigned => Value == Guid.Empty; + public bool IsUnassigned => Value == Guid.Empty; - public static SessionChainId From(Guid value) - => value == Guid.Empty - ? throw new ArgumentException("ChainId cannot be empty.", nameof(value)) - : new SessionChainId(value); + public static SessionChainId From(Guid value) + => value == Guid.Empty + ? throw new ArgumentException("ChainId cannot be empty.", nameof(value)) + : new SessionChainId(value); - public static bool TryCreate(string raw, out SessionChainId id) + public static bool TryCreate(string raw, out SessionChainId id) + { + if (Guid.TryParse(raw, out var guid) && guid != Guid.Empty) { - if (Guid.TryParse(raw, out var guid) && guid != Guid.Empty) - { - id = new SessionChainId(guid); - return true; - } - - id = default; - return false; + id = new SessionChainId(guid); + return true; } - public override string ToString() => Value.ToString("N"); + id = default; + return false; } + + public override string ToString() => Value.ToString("N"); } diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Session/SessionMetadata.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Session/SessionMetadata.cs index ca81551a..899c3b03 100644 --- a/src/CodeBeam.UltimateAuth.Core/Domain/Session/SessionMetadata.cs +++ b/src/CodeBeam.UltimateAuth.Core/Domain/Session/SessionMetadata.cs @@ -1,42 +1,41 @@ -namespace CodeBeam.UltimateAuth.Core.Domain +namespace CodeBeam.UltimateAuth.Core.Domain; + +/// +/// Represents additional metadata attached to an authentication session. +/// This information is application-defined and commonly used for analytics, +/// UI adaptation, multi-tenant context, and CSRF/session-related security data. +/// +public sealed class SessionMetadata { /// - /// Represents additional metadata attached to an authentication session. - /// This information is application-defined and commonly used for analytics, - /// UI adaptation, multi-tenant context, and CSRF/session-related security data. + /// Represents an empty or uninitialized session metadata instance. /// - public sealed class SessionMetadata - { - /// - /// Represents an empty or uninitialized session metadata instance. - /// - /// Use this field to represent a default or non-existent session when no metadata is - /// available. This instance contains default values for all properties and can be used for comparison or as a - /// placeholder. - public static readonly SessionMetadata Empty = new SessionMetadata(); + /// Use this field to represent a default or non-existent session when no metadata is + /// available. This instance contains default values for all properties and can be used for comparison or as a + /// placeholder. + public static readonly SessionMetadata Empty = new SessionMetadata(); - /// - /// Gets the version of the client application that created the session. - /// Useful for enforcing upgrade policies or troubleshooting version-related issues. - /// - public string? AppVersion { get; init; } + /// + /// Gets the version of the client application that created the session. + /// Useful for enforcing upgrade policies or troubleshooting version-related issues. + /// + public string? AppVersion { get; init; } - /// - /// Gets the locale or culture identifier associated with the session, - /// such as en-US, tr-TR, or fr-FR. - /// - public string? Locale { get; init; } + /// + /// Gets the locale or culture identifier associated with the session, + /// such as en-US, tr-TR, or fr-FR. + /// + public string? Locale { get; init; } - /// - /// Gets a Cross-Site Request Forgery token or other session-scoped secret - /// used for request integrity validation in web applications. - /// - public string? CsrfToken { get; init; } + /// + /// Gets a Cross-Site Request Forgery token or other session-scoped secret + /// used for request integrity validation in web applications. + /// + public string? CsrfToken { get; init; } - /// - /// Gets a dictionary for storing arbitrary application-defined metadata. - /// Allows extensions without modifying the core authentication model. - /// - public Dictionary? Custom { get; init; } - } + /// + /// Gets a dictionary for storing arbitrary application-defined metadata. + /// Allows extensions without modifying the core authentication model. + /// + public Dictionary? Custom { get; init; } } diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Session/SessionRefreshStatus.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Session/SessionRefreshStatus.cs index d8724ba1..1d4927fa 100644 --- a/src/CodeBeam.UltimateAuth.Core/Domain/Session/SessionRefreshStatus.cs +++ b/src/CodeBeam.UltimateAuth.Core/Domain/Session/SessionRefreshStatus.cs @@ -1,10 +1,9 @@ -namespace CodeBeam.UltimateAuth.Core.Domain +namespace CodeBeam.UltimateAuth.Core.Domain; + +public enum SessionRefreshStatus { - public enum SessionRefreshStatus - { - Success, - ReauthRequired, - InvalidRequest, - Failed - } + Success, + ReauthRequired, + InvalidRequest, + Failed } diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Session/SessionRootId.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Session/SessionRootId.cs index 68d595a2..be7c1513 100644 --- a/src/CodeBeam.UltimateAuth.Core/Domain/Session/SessionRootId.cs +++ b/src/CodeBeam.UltimateAuth.Core/Domain/Session/SessionRootId.cs @@ -1,26 +1,25 @@ -namespace CodeBeam.UltimateAuth.Core.Domain +namespace CodeBeam.UltimateAuth.Core.Domain; + +public readonly record struct SessionRootId(Guid Value) { - public readonly record struct SessionRootId(Guid Value) - { - public static SessionRootId New() => new(Guid.NewGuid()); + public static SessionRootId New() => new(Guid.NewGuid()); - public static SessionRootId From(Guid value) - => value == Guid.Empty - ? throw new ArgumentException("SessionRootId cannot be empty.", nameof(value)) - : new SessionRootId(value); + public static SessionRootId From(Guid value) + => value == Guid.Empty + ? throw new ArgumentException("SessionRootId cannot be empty.", nameof(value)) + : new SessionRootId(value); - public static bool TryCreate(string raw, out SessionRootId id) + public static bool TryCreate(string raw, out SessionRootId id) + { + if (Guid.TryParse(raw, out var guid) && guid != Guid.Empty) { - if (Guid.TryParse(raw, out var guid) && guid != Guid.Empty) - { - id = new SessionRootId(guid); - return true; - } - - id = default; - return false; + id = new SessionRootId(guid); + return true; } - public override string ToString() => Value.ToString("N"); + id = default; + return false; } + + public override string ToString() => Value.ToString("N"); } diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Session/SessionState.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Session/SessionState.cs index 95a6af0a..a2c01f45 100644 --- a/src/CodeBeam.UltimateAuth.Core/Domain/Session/SessionState.cs +++ b/src/CodeBeam.UltimateAuth.Core/Domain/Session/SessionState.cs @@ -1,17 +1,16 @@ -namespace CodeBeam.UltimateAuth.Core.Domain +namespace CodeBeam.UltimateAuth.Core.Domain; + +/// +/// Represents the effective runtime state of an authentication session. +/// Evaluated based on expiration rules, revocation status, and security version checks. +/// +public enum SessionState { - /// - /// Represents the effective runtime state of an authentication session. - /// Evaluated based on expiration rules, revocation status, and security version checks. - /// - public enum SessionState - { - Active, - Expired, - Revoked, - NotFound, - Invalid, - SecurityMismatch, - DeviceMismatch - } + Active, + Expired, + Revoked, + NotFound, + Invalid, + SecurityMismatch, + DeviceMismatch } diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSession.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSession.cs index 78786ef1..e29878a9 100644 --- a/src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSession.cs +++ b/src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSession.cs @@ -1,24 +1,142 @@ -using CodeBeam.UltimateAuth.Core.Contracts; +namespace CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Core.Domain +public sealed class UAuthSession { - public sealed class UAuthSession : ISession + public AuthSessionId SessionId { get; } + public string? TenantId { get; } + public UserKey UserKey { get; } + public SessionChainId ChainId { get; } + public DateTimeOffset CreatedAt { get; } + public DateTimeOffset ExpiresAt { get; } + public DateTimeOffset? LastSeenAt { get; } + public bool IsRevoked { get; } + public DateTimeOffset? RevokedAt { get; } + public long SecurityVersionAtCreation { get; } + public DeviceContext Device { get; } + public ClaimsSnapshot Claims { get; } + public SessionMetadata Metadata { get; } + + private UAuthSession( + AuthSessionId sessionId, + string? tenantId, + UserKey userKey, + SessionChainId chainId, + DateTimeOffset createdAt, + DateTimeOffset expiresAt, + DateTimeOffset? lastSeenAt, + bool isRevoked, + DateTimeOffset? revokedAt, + long securityVersionAtCreation, + DeviceContext device, + ClaimsSnapshot claims, + SessionMetadata metadata) { - public AuthSessionId SessionId { get; } - public string? TenantId { get; } - public UserKey UserKey { get; } - public SessionChainId ChainId { get; } - public DateTimeOffset CreatedAt { get; } - public DateTimeOffset ExpiresAt { get; } - public DateTimeOffset? LastSeenAt { get; } - public bool IsRevoked { get; } - public DateTimeOffset? RevokedAt { get; } - public long SecurityVersionAtCreation { get; } - public DeviceContext Device { get; } - public ClaimsSnapshot Claims { get; } - public SessionMetadata Metadata { get; } - - private UAuthSession( + SessionId = sessionId; + TenantId = tenantId; + UserKey = userKey; + ChainId = chainId; + CreatedAt = createdAt; + ExpiresAt = expiresAt; + LastSeenAt = lastSeenAt; + IsRevoked = isRevoked; + RevokedAt = revokedAt; + SecurityVersionAtCreation = securityVersionAtCreation; + Device = device; + Claims = claims; + Metadata = metadata; + } + + public static UAuthSession Create( + AuthSessionId sessionId, + string? tenantId, + UserKey userKey, + SessionChainId chainId, + DateTimeOffset now, + DateTimeOffset expiresAt, + DeviceContext device, + ClaimsSnapshot claims, + SessionMetadata metadata) + { + return new( + sessionId, + tenantId, + userKey, + chainId, + createdAt: now, + expiresAt: expiresAt, + lastSeenAt: now, + isRevoked: false, + revokedAt: null, + securityVersionAtCreation: 0, + device: device, + claims: claims, + metadata: metadata + ); + } + + public UAuthSession WithSecurityVersion(long version) + { + if (SecurityVersionAtCreation == version) + return this; + + return new UAuthSession( + SessionId, + TenantId, + UserKey, + ChainId, + CreatedAt, + ExpiresAt, + LastSeenAt, + IsRevoked, + RevokedAt, + version, + Device, + Claims, + Metadata + ); + } + + public UAuthSession Touch(DateTimeOffset at) + { + return new UAuthSession( + SessionId, + TenantId, + UserKey, + ChainId, + CreatedAt, + ExpiresAt, + at, + IsRevoked, + RevokedAt, + SecurityVersionAtCreation, + Device, + Claims, + Metadata + ); + } + + public UAuthSession Revoke(DateTimeOffset at) + { + if (IsRevoked) return this; + + return new UAuthSession( + SessionId, + TenantId, + UserKey, + ChainId, + CreatedAt, + ExpiresAt, + LastSeenAt, + true, + at, + SecurityVersionAtCreation, + Device, + Claims, + Metadata + ); + } + + internal static UAuthSession FromProjection( AuthSessionId sessionId, string? tenantId, UserKey userKey, @@ -32,180 +150,58 @@ private UAuthSession( DeviceContext device, ClaimsSnapshot claims, SessionMetadata metadata) - { - SessionId = sessionId; - TenantId = tenantId; - UserKey = userKey; - ChainId = chainId; - CreatedAt = createdAt; - ExpiresAt = expiresAt; - LastSeenAt = lastSeenAt; - IsRevoked = isRevoked; - RevokedAt = revokedAt; - SecurityVersionAtCreation = securityVersionAtCreation; - Device = device; - Claims = claims; - Metadata = metadata; - } - - public static UAuthSession Create( - AuthSessionId sessionId, - string? tenantId, - UserKey userKey, - SessionChainId chainId, - DateTimeOffset now, - DateTimeOffset expiresAt, - DeviceContext device, - ClaimsSnapshot claims, - SessionMetadata metadata) - { - return new( - sessionId, - tenantId, - userKey, - chainId, - createdAt: now, - expiresAt: expiresAt, - lastSeenAt: now, - isRevoked: false, - revokedAt: null, - securityVersionAtCreation: 0, - device: device, - claims: claims, - metadata: metadata - ); - } - - public UAuthSession WithSecurityVersion(long version) - { - if (SecurityVersionAtCreation == version) - return this; - - return new UAuthSession( - SessionId, - TenantId, - UserKey, - ChainId, - CreatedAt, - ExpiresAt, - LastSeenAt, - IsRevoked, - RevokedAt, - version, - Device, - Claims, - Metadata - ); - } - - public ISession Touch(DateTimeOffset at) - { - return new UAuthSession( - SessionId, - TenantId, - UserKey, - ChainId, - CreatedAt, - ExpiresAt, - at, - IsRevoked, - RevokedAt, - SecurityVersionAtCreation, - Device, - Claims, - Metadata - ); - } - - public ISession Revoke(DateTimeOffset at) - { - if (IsRevoked) return this; - - return new UAuthSession( - SessionId, - TenantId, - UserKey, - ChainId, - CreatedAt, - ExpiresAt, - LastSeenAt, - true, - at, - SecurityVersionAtCreation, - Device, - Claims, - Metadata - ); - } - - internal static UAuthSession FromProjection( - AuthSessionId sessionId, - string? tenantId, - UserKey userKey, - SessionChainId chainId, - DateTimeOffset createdAt, - DateTimeOffset expiresAt, - DateTimeOffset? lastSeenAt, - bool isRevoked, - DateTimeOffset? revokedAt, - long securityVersionAtCreation, - DeviceContext device, - ClaimsSnapshot claims, - SessionMetadata metadata) - { - return new UAuthSession( - sessionId, - tenantId, - userKey, - chainId, - createdAt, - expiresAt, - lastSeenAt, - isRevoked, - revokedAt, - securityVersionAtCreation, - device, - claims, - metadata - ); - } - - public SessionState GetState(DateTimeOffset at, TimeSpan? idleTimeout) - { - if (IsRevoked) - return SessionState.Revoked; - - if (at >= ExpiresAt) - return SessionState.Expired; - - if (idleTimeout.HasValue && at - LastSeenAt >= idleTimeout.Value) - return SessionState.Expired; - - return SessionState.Active; - } - - public ISession WithChain(SessionChainId chainId) - { - if (!ChainId.IsUnassigned) - throw new InvalidOperationException("Chain already assigned."); - - return new UAuthSession( - sessionId: SessionId, - tenantId: TenantId, - userKey: UserKey, - chainId: chainId, - createdAt: CreatedAt, - expiresAt: ExpiresAt, - lastSeenAt: LastSeenAt, - isRevoked: IsRevoked, - revokedAt: RevokedAt, - securityVersionAtCreation: SecurityVersionAtCreation, - device: Device, - claims: Claims, - metadata: Metadata - ); - } + { + return new UAuthSession( + sessionId, + tenantId, + userKey, + chainId, + createdAt, + expiresAt, + lastSeenAt, + isRevoked, + revokedAt, + securityVersionAtCreation, + device, + claims, + metadata + ); + } + public SessionState GetState(DateTimeOffset at, TimeSpan? idleTimeout) + { + if (IsRevoked) + return SessionState.Revoked; + + if (at >= ExpiresAt) + return SessionState.Expired; + + if (idleTimeout.HasValue && at - LastSeenAt >= idleTimeout.Value) + return SessionState.Expired; + + return SessionState.Active; + } + + public UAuthSession WithChain(SessionChainId chainId) + { + if (!ChainId.IsUnassigned) + throw new InvalidOperationException("Chain already assigned."); + + return new UAuthSession( + sessionId: SessionId, + tenantId: TenantId, + userKey: UserKey, + chainId: chainId, + createdAt: CreatedAt, + expiresAt: ExpiresAt, + lastSeenAt: LastSeenAt, + isRevoked: IsRevoked, + revokedAt: RevokedAt, + securityVersionAtCreation: SecurityVersionAtCreation, + device: Device, + claims: Claims, + metadata: Metadata + ); } } diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSessionChain.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSessionChain.cs index 9403f95e..92a61c08 100644 --- a/src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSessionChain.cs +++ b/src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSessionChain.cs @@ -1,146 +1,145 @@ -namespace CodeBeam.UltimateAuth.Core.Domain -{ - public sealed class UAuthSessionChain : ISessionChain - { - public SessionChainId ChainId { get; } - public SessionRootId RootId { get; } - public string? TenantId { get; } - public UserKey UserKey { get; } - public int RotationCount { get; } - public long SecurityVersionAtCreation { get; } - public ClaimsSnapshot ClaimsSnapshot { get; } - public AuthSessionId? ActiveSessionId { get; } - public bool IsRevoked { get; } - public DateTimeOffset? RevokedAt { get; } +namespace CodeBeam.UltimateAuth.Core.Domain; - private UAuthSessionChain( - SessionChainId chainId, - SessionRootId rootId, - string? tenantId, - UserKey userKey, - int rotationCount, - long securityVersionAtCreation, - ClaimsSnapshot claimsSnapshot, - AuthSessionId? activeSessionId, - bool isRevoked, - DateTimeOffset? revokedAt) - { - ChainId = chainId; - RootId = rootId; - TenantId = tenantId; - UserKey = userKey; - RotationCount = rotationCount; - SecurityVersionAtCreation = securityVersionAtCreation; - ClaimsSnapshot = claimsSnapshot; - ActiveSessionId = activeSessionId; - IsRevoked = isRevoked; - RevokedAt = revokedAt; - } +public sealed class UAuthSessionChain +{ + public SessionChainId ChainId { get; } + public SessionRootId RootId { get; } + public string? TenantId { get; } + public UserKey UserKey { get; } + public int RotationCount { get; } + public long SecurityVersionAtCreation { get; } + public ClaimsSnapshot ClaimsSnapshot { get; } + public AuthSessionId? ActiveSessionId { get; } + public bool IsRevoked { get; } + public DateTimeOffset? RevokedAt { get; } - public static UAuthSessionChain Create( - SessionChainId chainId, - SessionRootId rootId, - string? tenantId, - UserKey userKey, - long securityVersion, - ClaimsSnapshot claimsSnapshot) - { - return new UAuthSessionChain( - chainId, - rootId, - tenantId, - userKey, - rotationCount: 0, - securityVersionAtCreation: securityVersion, - claimsSnapshot: claimsSnapshot, - activeSessionId: null, - isRevoked: false, - revokedAt: null - ); - } + private UAuthSessionChain( + SessionChainId chainId, + SessionRootId rootId, + string? tenantId, + UserKey userKey, + int rotationCount, + long securityVersionAtCreation, + ClaimsSnapshot claimsSnapshot, + AuthSessionId? activeSessionId, + bool isRevoked, + DateTimeOffset? revokedAt) + { + ChainId = chainId; + RootId = rootId; + TenantId = tenantId; + UserKey = userKey; + RotationCount = rotationCount; + SecurityVersionAtCreation = securityVersionAtCreation; + ClaimsSnapshot = claimsSnapshot; + ActiveSessionId = activeSessionId; + IsRevoked = isRevoked; + RevokedAt = revokedAt; + } - public ISessionChain AttachSession(AuthSessionId sessionId) - { - if (IsRevoked) - return this; + public static UAuthSessionChain Create( + SessionChainId chainId, + SessionRootId rootId, + string? tenantId, + UserKey userKey, + long securityVersion, + ClaimsSnapshot claimsSnapshot) + { + return new UAuthSessionChain( + chainId, + rootId, + tenantId, + userKey, + rotationCount: 0, + securityVersionAtCreation: securityVersion, + claimsSnapshot: claimsSnapshot, + activeSessionId: null, + isRevoked: false, + revokedAt: null + ); + } - return new UAuthSessionChain( - ChainId, - RootId, - TenantId, - UserKey, - RotationCount, // Unchanged on first attach - SecurityVersionAtCreation, - ClaimsSnapshot, - activeSessionId: sessionId, - isRevoked: false, - revokedAt: null - ); - } + public UAuthSessionChain AttachSession(AuthSessionId sessionId) + { + if (IsRevoked) + return this; - public ISessionChain RotateSession(AuthSessionId sessionId) - { - if (IsRevoked) - return this; + return new UAuthSessionChain( + ChainId, + RootId, + TenantId, + UserKey, + RotationCount, // Unchanged on first attach + SecurityVersionAtCreation, + ClaimsSnapshot, + activeSessionId: sessionId, + isRevoked: false, + revokedAt: null + ); + } - return new UAuthSessionChain( - ChainId, - RootId, - TenantId, - UserKey, - RotationCount + 1, - SecurityVersionAtCreation, - ClaimsSnapshot, - activeSessionId: sessionId, - isRevoked: false, - revokedAt: null - ); - } + public UAuthSessionChain RotateSession(AuthSessionId sessionId) + { + if (IsRevoked) + return this; - public ISessionChain Revoke(DateTimeOffset at) - { - if (IsRevoked) - return this; + return new UAuthSessionChain( + ChainId, + RootId, + TenantId, + UserKey, + RotationCount + 1, + SecurityVersionAtCreation, + ClaimsSnapshot, + activeSessionId: sessionId, + isRevoked: false, + revokedAt: null + ); + } - return new UAuthSessionChain( - ChainId, - RootId, - TenantId, - UserKey, - RotationCount, - SecurityVersionAtCreation, - ClaimsSnapshot, - ActiveSessionId, - isRevoked: true, - revokedAt: at - ); - } + public UAuthSessionChain Revoke(DateTimeOffset at) + { + if (IsRevoked) + return this; - internal static UAuthSessionChain FromProjection( - SessionChainId chainId, - SessionRootId rootId, - string? tenantId, - UserKey userKey, - int rotationCount, - long securityVersionAtCreation, - ClaimsSnapshot claimsSnapshot, - AuthSessionId? activeSessionId, - bool isRevoked, - DateTimeOffset? revokedAt) - { - return new UAuthSessionChain( - chainId, - rootId, - tenantId, - userKey, - rotationCount, - securityVersionAtCreation, - claimsSnapshot, - activeSessionId, - isRevoked, - revokedAt - ); - } + return new UAuthSessionChain( + ChainId, + RootId, + TenantId, + UserKey, + RotationCount, + SecurityVersionAtCreation, + ClaimsSnapshot, + ActiveSessionId, + isRevoked: true, + revokedAt: at + ); + } + internal static UAuthSessionChain FromProjection( + SessionChainId chainId, + SessionRootId rootId, + string? tenantId, + UserKey userKey, + int rotationCount, + long securityVersionAtCreation, + ClaimsSnapshot claimsSnapshot, + AuthSessionId? activeSessionId, + bool isRevoked, + DateTimeOffset? revokedAt) + { + return new UAuthSessionChain( + chainId, + rootId, + tenantId, + userKey, + rotationCount, + securityVersionAtCreation, + claimsSnapshot, + activeSessionId, + isRevoked, + revokedAt + ); } + } diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSessionRoot.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSessionRoot.cs index 0153210f..9efd9c99 100644 --- a/src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSessionRoot.cs +++ b/src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSessionRoot.cs @@ -1,109 +1,108 @@ -namespace CodeBeam.UltimateAuth.Core.Domain +namespace CodeBeam.UltimateAuth.Core.Domain; + +public sealed class UAuthSessionRoot { - public sealed class UAuthSessionRoot : ISessionRoot - { - public SessionRootId RootId { get; } - public UserKey UserKey { get; } - public string? TenantId { get; } - public bool IsRevoked { get; } - public DateTimeOffset? RevokedAt { get; } - public long SecurityVersion { get; } - public IReadOnlyList Chains { get; } - public DateTimeOffset LastUpdatedAt { get; } + public SessionRootId RootId { get; } + public UserKey UserKey { get; } + public string? TenantId { get; } + public bool IsRevoked { get; } + public DateTimeOffset? RevokedAt { get; } + public long SecurityVersion { get; } + public IReadOnlyList Chains { get; } + public DateTimeOffset LastUpdatedAt { get; } - private UAuthSessionRoot( - SessionRootId rootId, - string? tenantId, - UserKey userKey, - bool isRevoked, - DateTimeOffset? revokedAt, - long securityVersion, - IReadOnlyList chains, - DateTimeOffset lastUpdatedAt) - { - RootId = rootId; - TenantId = tenantId; - UserKey = userKey; - IsRevoked = isRevoked; - RevokedAt = revokedAt; - SecurityVersion = securityVersion; - Chains = chains; - LastUpdatedAt = lastUpdatedAt; - } + private UAuthSessionRoot( + SessionRootId rootId, + string? tenantId, + UserKey userKey, + bool isRevoked, + DateTimeOffset? revokedAt, + long securityVersion, + IReadOnlyList chains, + DateTimeOffset lastUpdatedAt) + { + RootId = rootId; + TenantId = tenantId; + UserKey = userKey; + IsRevoked = isRevoked; + RevokedAt = revokedAt; + SecurityVersion = securityVersion; + Chains = chains; + LastUpdatedAt = lastUpdatedAt; + } - public static ISessionRoot Create( - string? tenantId, - UserKey userKey, - DateTimeOffset issuedAt) - { - return new UAuthSessionRoot( - SessionRootId.New(), - tenantId, - userKey, - isRevoked: false, - revokedAt: null, - securityVersion: 0, - chains: Array.Empty(), - lastUpdatedAt: issuedAt - ); - } + public static UAuthSessionRoot Create( + string? tenantId, + UserKey userKey, + DateTimeOffset issuedAt) + { + return new UAuthSessionRoot( + SessionRootId.New(), + tenantId, + userKey, + isRevoked: false, + revokedAt: null, + securityVersion: 0, + chains: Array.Empty(), + lastUpdatedAt: issuedAt + ); + } - public ISessionRoot Revoke(DateTimeOffset at) - { - if (IsRevoked) - return this; + public UAuthSessionRoot Revoke(DateTimeOffset at) + { + if (IsRevoked) + return this; - return new UAuthSessionRoot( - RootId, - TenantId, - UserKey, - isRevoked: true, - revokedAt: at, - securityVersion: SecurityVersion, - chains: Chains, - lastUpdatedAt: at - ); - } + return new UAuthSessionRoot( + RootId, + TenantId, + UserKey, + isRevoked: true, + revokedAt: at, + securityVersion: SecurityVersion, + chains: Chains, + lastUpdatedAt: at + ); + } - public ISessionRoot AttachChain(ISessionChain chain, DateTimeOffset at) - { - if (IsRevoked) - return this; + public UAuthSessionRoot AttachChain(UAuthSessionChain chain, DateTimeOffset at) + { + if (IsRevoked) + return this; - return new UAuthSessionRoot( - RootId, - TenantId, - UserKey, - IsRevoked, - RevokedAt, - SecurityVersion, - Chains.Concat(new[] { chain }).ToArray(), - at - ); - } + return new UAuthSessionRoot( + RootId, + TenantId, + UserKey, + IsRevoked, + RevokedAt, + SecurityVersion, + Chains.Concat(new[] { chain }).ToArray(), + at + ); + } - internal static UAuthSessionRoot FromProjection( - SessionRootId rootId, - string? tenantId, - UserKey userKey, - bool isRevoked, - DateTimeOffset? revokedAt, - long securityVersion, - IReadOnlyList chains, - DateTimeOffset lastUpdatedAt) - { - return new UAuthSessionRoot( - rootId, - tenantId, - userKey, - isRevoked, - revokedAt, - securityVersion, - chains, - lastUpdatedAt - ); - } + internal static UAuthSessionRoot FromProjection( + SessionRootId rootId, + string? tenantId, + UserKey userKey, + bool isRevoked, + DateTimeOffset? revokedAt, + long securityVersion, + IReadOnlyList chains, + DateTimeOffset lastUpdatedAt) + { + return new UAuthSessionRoot( + rootId, + tenantId, + userKey, + isRevoked, + revokedAt, + securityVersion, + chains, + lastUpdatedAt + ); + } - } } diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/User/AuthUserRecord.cs b/src/CodeBeam.UltimateAuth.Core/Domain/User/AuthUserRecord.cs deleted file mode 100644 index ab60e166..00000000 --- a/src/CodeBeam.UltimateAuth.Core/Domain/User/AuthUserRecord.cs +++ /dev/null @@ -1,49 +0,0 @@ -namespace CodeBeam.UltimateAuth.Core.Domain; - -/// -/// Represents the minimal, immutable user snapshot required by the UltimateAuth Core -/// during authentication discovery and subject binding. -/// -/// This type is NOT a domain user model. -/// It contains only normalized, opinionless fields that determine whether -/// a user can participate in authentication flows. -/// -/// AuthUserRecord is produced by the Users domain as a boundary projection -/// and is never mutated by the Core. -/// -public sealed record AuthUserRecord -{ - /// - /// Application-level user identifier. - /// - public required TUserId Id { get; init; } - - /// - /// Primary login identifier (username, email, etc). - /// Used only for discovery and uniqueness checks. - /// - public required string Identifier { get; init; } - - /// - /// Indicates whether the user is considered active for authentication purposes. - /// Domain-specific statuses are normalized into this flag by the Users domain. - /// - public required bool IsActive { get; init; } - - /// - /// Indicates whether the user is deleted. - /// Deleted users are never eligible for authentication. - /// - public required bool IsDeleted { get; init; } - - /// - /// The timestamp when the user was originally created. - /// Provided for invariant validation and auditing purposes. - /// - public required DateTimeOffset CreatedAt { get; init; } - - /// - /// The timestamp when the user was deleted, if applicable. - /// - public DateTimeOffset? DeletedAt { get; init; } -} diff --git a/src/CodeBeam.UltimateAuth.Core/Infrastructure/UAuthSessionStoreKernelFactory.cs b/src/CodeBeam.UltimateAuth.Core/Infrastructure/UAuthSessionStoreKernelFactory.cs deleted file mode 100644 index 3f769f92..00000000 --- a/src/CodeBeam.UltimateAuth.Core/Infrastructure/UAuthSessionStoreKernelFactory.cs +++ /dev/null @@ -1,19 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Abstractions; -using Microsoft.Extensions.DependencyInjection; - -namespace CodeBeam.UltimateAuth.Core.Infrastructure; - -/// -/// Default session store factory that throws until a real store implementation is registered. -/// -internal sealed class UAuthSessionStoreKernelFactory : ISessionStoreKernelFactory -{ - private readonly IServiceProvider _sp; - - public UAuthSessionStoreKernelFactory(IServiceProvider sp) - { - _sp = sp; - } - - public ISessionStoreKernel Create(string? tenantId) => _sp.GetRequiredService(); -} diff --git a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/TenantContext.cs b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/TenantContext.cs new file mode 100644 index 00000000..3c596c63 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/TenantContext.cs @@ -0,0 +1,13 @@ +namespace CodeBeam.UltimateAuth.Core.MultiTenancy; + +public sealed class TenantContext +{ + public string? TenantId { get; } + public bool IsGlobal { get; } + + public TenantContext(string? tenantId, bool isGlobal = false) + { + TenantId = tenantId; + IsGlobal = isGlobal; + } +} diff --git a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/TenantValidation.cs b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/TenantValidation.cs index d6f4113d..7df4ef93 100644 --- a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/TenantValidation.cs +++ b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/TenantValidation.cs @@ -5,9 +5,7 @@ namespace CodeBeam.UltimateAuth.Core.MultiTenancy; internal static class TenantValidation { - public static UAuthTenantContext FromResolvedTenant( - string rawTenantId, - UAuthMultiTenantOptions options) + public static UAuthTenantContext FromResolvedTenant(string rawTenantId, UAuthMultiTenantOptions options) { if (string.IsNullOrWhiteSpace(rawTenantId)) return UAuthTenantContext.NotResolved(); diff --git a/src/CodeBeam.UltimateAuth.Server/Composition/UltimateAuthServerBuilderValidation.cs b/src/CodeBeam.UltimateAuth.Server/Composition/UltimateAuthServerBuilderValidation.cs index 8370bce1..db88f962 100644 --- a/src/CodeBeam.UltimateAuth.Server/Composition/UltimateAuthServerBuilderValidation.cs +++ b/src/CodeBeam.UltimateAuth.Server/Composition/UltimateAuthServerBuilderValidation.cs @@ -15,7 +15,7 @@ public static IServiceCollection Build(this UltimateAuthServerBuilder builder) //if (!services.Any(sd => sd.ServiceType.IsAssignableTo(typeof(IUAuthUserStore<>)))) // throw new InvalidOperationException("No credential store registered."); - if (!services.Any(sd => sd.ServiceType.IsAssignableTo(typeof(ISessionStore)))) + if (!services.Any(sd => sd.ServiceType.IsAssignableTo(typeof(ISessionStoreKernel)))) throw new InvalidOperationException("No session store registered."); return services; diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeChainCommand.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeChainCommand.cs index c8c1a374..3801af39 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeChainCommand.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeChainCommand.cs @@ -13,17 +13,9 @@ public RevokeChainCommand(SessionChainId chainId) ChainId = chainId; } - public async Task ExecuteAsync( - AuthContext context, - ISessionIssuer issuer, - CancellationToken ct) + public async Task ExecuteAsync(AuthContext context, ISessionIssuer issuer, CancellationToken ct) { - await issuer.RevokeChainAsync( - context.TenantId, - ChainId, - context.At, - ct); - + await issuer.RevokeChainAsync(context.TenantId, ChainId, context.At, ct); return Unit.Value; } } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeSessionCommand.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeSessionCommand.cs index 86fa7fd4..b32e9d4f 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeSessionCommand.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeSessionCommand.cs @@ -2,14 +2,13 @@ using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Server.Infrastructure +namespace CodeBeam.UltimateAuth.Server.Infrastructure; + +internal sealed record RevokeSessionCommand(AuthSessionId SessionId) : ISessionCommand { - internal sealed record RevokeSessionCommand(string? TenantId, AuthSessionId SessionId) : ISessionCommand + public async Task ExecuteAsync(AuthContext context, ISessionIssuer issuer, CancellationToken ct) { - public async Task ExecuteAsync(AuthContext _, ISessionIssuer issuer, CancellationToken ct) - { - await issuer.RevokeSessionAsync(TenantId, SessionId, _.At, ct); - return Unit.Value; - } + await issuer.RevokeSessionAsync(context.TenantId, SessionId, context.At, ct); + return Unit.Value; } } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/DefaultSessionTouchService.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/DefaultSessionTouchService.cs index a4195fd2..ab232d25 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/DefaultSessionTouchService.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/DefaultSessionTouchService.cs @@ -1,42 +1,45 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Contracts; -namespace CodeBeam.UltimateAuth.Server.Infrastructure +namespace CodeBeam.UltimateAuth.Server.Infrastructure; + +public sealed class DefaultSessionTouchService : ISessionTouchService { - public sealed class DefaultSessionTouchService : ISessionTouchService + private readonly ISessionStoreKernelFactory _kernelFactory; + + public DefaultSessionTouchService(ISessionStoreKernelFactory kernelFactory) { - private readonly ISessionStore _sessionStore; + _kernelFactory = kernelFactory; + } - public DefaultSessionTouchService(ISessionStore sessionStore) - { - _sessionStore = sessionStore; - } + // It's designed for PureOpaque sessions, which do not issue new refresh tokens on refresh. + // That's why the service access store direcly: There is no security flow here, only validate and touch session. + public async Task RefreshAsync(SessionValidationResult validation, SessionTouchPolicy policy, SessionTouchMode sessionTouchMode, DateTimeOffset now, CancellationToken ct = default) + { + if (!validation.IsValid || validation.SessionId is null) + return SessionRefreshResult.ReauthRequired(); - // It's designed for PureOpaque sessions, which do not issue new refresh tokens on refresh. - // That's why the service access store direcly: There is no security flow here, only validate and touch session. - public async Task RefreshAsync(SessionValidationResult validation, SessionTouchPolicy policy, SessionTouchMode sessionTouchMode, DateTimeOffset now, CancellationToken ct = default) - { - if (!validation.IsValid) - return SessionRefreshResult.ReauthRequired(); + if (!policy.TouchInterval.HasValue) + return SessionRefreshResult.Success(validation.SessionId.Value, didTouch: false); - //var session = validation.Session; - bool didTouch = false; + var kernel = _kernelFactory.Create(validation.TenantId); - if (policy.TouchInterval.HasValue) - { - //var elapsed = now - session.LastSeenAt; + bool didTouch = false; + + await kernel.ExecuteAsync(async _ => + { + var session = await kernel.GetSessionAsync(validation.SessionId.Value); + if (session is null || session.IsRevoked) + return; - //if (elapsed >= policy.TouchInterval.Value) - //{ - // var touched = session.Touch(now); - // await _activityWriter.TouchAsync(validation.TenantId, touched, ct); - // didTouch = true; - //} + if (sessionTouchMode == SessionTouchMode.IfNeeded && now - session.LastSeenAt < policy.TouchInterval.Value) + return; - didTouch = await _sessionStore.TouchSessionAsync(validation.TenantId, validation.SessionId.Value, now, sessionTouchMode, ct); - } + var touched = session.Touch(now); + await kernel.SaveSessionAsync(touched); + didTouch = true; + }, ct); - return SessionRefreshResult.Success(validation.SessionId.Value, didTouch); - } + return SessionRefreshResult.Success(validation.SessionId.Value, didTouch); } } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/User/UAuthUserAccessor.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/User/UAuthUserAccessor.cs index 34f76772..8c9d46ba 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/User/UAuthUserAccessor.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/User/UAuthUserAccessor.cs @@ -4,42 +4,39 @@ using CodeBeam.UltimateAuth.Server.Middlewares; using Microsoft.AspNetCore.Http; -namespace CodeBeam.UltimateAuth.Server.Infrastructure +namespace CodeBeam.UltimateAuth.Server.Infrastructure; + +public sealed class UAuthUserAccessor : IUserAccessor { - public sealed class UAuthUserAccessor : IUserAccessor + private readonly ISessionStoreKernelFactory _kernelFactory; + private readonly IUserIdConverter _userIdConverter; + + public UAuthUserAccessor(ISessionStoreKernelFactory kernelFactory, IUserIdConverterResolver converterResolver) { - private readonly ISessionStore _sessionStore; - private readonly IUserIdConverter _userIdConverter; + _kernelFactory = kernelFactory; + _userIdConverter = converterResolver.GetConverter(); + } - public UAuthUserAccessor( - ISessionStore sessionStore, - IUserIdConverterResolver converterResolver) - { - _sessionStore = sessionStore; - _userIdConverter = converterResolver.GetConverter(); - } + public async Task ResolveAsync(HttpContext context) + { + var sessionCtx = context.GetSessionContext(); - public async Task ResolveAsync(HttpContext context) + if (sessionCtx.IsAnonymous || sessionCtx.SessionId is null) { - var sessionCtx = context.GetSessionContext(); - - if (sessionCtx.IsAnonymous) - { - context.Items[UserMiddleware.UserContextKey] = AuthUserSnapshot.Anonymous(); - return; - } - - var session = await _sessionStore.GetSessionAsync(sessionCtx.TenantId, sessionCtx.SessionId!.Value); + context.Items[UserMiddleware.UserContextKey] = AuthUserSnapshot.Anonymous(); + return; + } - if (session is null || session.IsRevoked) - { - context.Items[UserMiddleware.UserContextKey] = AuthUserSnapshot.Anonymous(); - return; - } + var kernel = _kernelFactory.Create(sessionCtx.TenantId); + var session = await kernel.GetSessionAsync(sessionCtx.SessionId.Value); - var userId = _userIdConverter.FromString(session.UserKey.Value); - context.Items[UserMiddleware.UserContextKey] = AuthUserSnapshot.Authenticated(userId); + if (session is null || session.IsRevoked) + { + context.Items[UserMiddleware.UserContextKey] = AuthUserSnapshot.Anonymous(); + return; } + var userId = _userIdConverter.FromString(session.UserKey.Value); + context.Items[UserMiddleware.UserContextKey] = AuthUserSnapshot.Authenticated(userId); } } diff --git a/src/CodeBeam.UltimateAuth.Server/Issuers/UAuthSessionIssuer.cs b/src/CodeBeam.UltimateAuth.Server/Issuers/UAuthSessionIssuer.cs index a10a2b19..a15a08a9 100644 --- a/src/CodeBeam.UltimateAuth.Server/Issuers/UAuthSessionIssuer.cs +++ b/src/CodeBeam.UltimateAuth.Server/Issuers/UAuthSessionIssuer.cs @@ -13,14 +13,18 @@ namespace CodeBeam.UltimateAuth.Server.Issuers { public sealed class UAuthSessionIssuer : IHttpSessionIssuer { - private readonly ISessionStore _sessionStore; + private readonly ISessionStoreKernelFactory _kernelFactory; private readonly IOpaqueTokenGenerator _opaqueGenerator; private readonly UAuthServerOptions _options; private readonly IUAuthCookieManager _cookieManager; - public UAuthSessionIssuer(ISessionStore sessionStore, IOpaqueTokenGenerator opaqueGenerator, IOptions options, IUAuthCookieManager cookieManager) + public UAuthSessionIssuer( + ISessionStoreKernelFactory kernelFactory, + IOpaqueTokenGenerator opaqueGenerator, + IOptions options, + IUAuthCookieManager cookieManager) { - _sessionStore = sessionStore; + _kernelFactory = kernelFactory; _opaqueGenerator = opaqueGenerator; _options = options.Value; _cookieManager = cookieManager; @@ -80,17 +84,40 @@ private async Task IssueLoginInternalAsync(HttpContext? httpConte IsMetadataOnly = _options.Mode == UAuthMode.SemiHybrid }; - await _sessionStore.CreateSessionAsync(issued, - new SessionStoreContext + var kernel = _kernelFactory.Create(context.TenantId); + + await kernel.ExecuteAsync(async _ => + { + var root = await kernel.GetSessionRootByUserAsync(context.UserKey) + ?? UAuthSessionRoot.Create(context.TenantId, context.UserKey, now); + + UAuthSessionChain chain; + + if (context.ChainId is not null) { - TenantId = context.TenantId, - UserKey = context.UserKey, - ChainId = context.ChainId, - IssuedAt = now, - Device = context.Device - }, - ct - ); + chain = await kernel.GetChainAsync(context.ChainId.Value) + ?? throw new SecurityException("Chain not found."); + } + else + { + chain = UAuthSessionChain.Create( + SessionChainId.New(), + root.RootId, + context.TenantId, + context.UserKey, + root.SecurityVersion, + ClaimsSnapshot.Empty); + + await kernel.SaveChainAsync(chain); + root = root.AttachChain(chain, now); + } + + var boundSession = session.WithChain(chain.ChainId); + + await kernel.SaveSessionAsync(boundSession); + await kernel.SetActiveSessionIdAsync(chain.ChainId, boundSession.SessionId); + await kernel.SaveSessionRootAsync(root); + }, ct); return issued; } @@ -110,6 +137,7 @@ public Task RotateSessionAsync(HttpContext httpContext, SessionRo private async Task RotateInternalAsync(HttpContext httpContext, SessionRotationContext context, CancellationToken ct = default) { + var kernel = _kernelFactory.Create(context.TenantId); var now = context.Now; var opaqueSessionId = _opaqueGenerator.Generate(); @@ -141,38 +169,66 @@ private async Task RotateInternalAsync(HttpContext httpContext, S IsMetadataOnly = _options.Mode == UAuthMode.SemiHybrid }; - await _sessionStore.RotateSessionAsync(context.CurrentSessionId, issued, - new SessionStoreContext - { - TenantId = context.TenantId, - UserKey = context.UserKey, - IssuedAt = now, - Device = context.Device, - }, - ct - ); + await kernel.ExecuteAsync(async _ => + { + var oldSession = await kernel.GetSessionAsync(context.CurrentSessionId) + ?? throw new SecurityException("Session not found"); + + if (oldSession.IsRevoked || oldSession.ExpiresAt <= now) + throw new SecurityException("Session is not valid"); + + var chain = await kernel.GetChainAsync(oldSession.ChainId) + ?? throw new SecurityException("Chain not found"); + + var bound = issued.Session.WithChain(chain.ChainId); + + await kernel.SaveSessionAsync(bound); + await kernel.SetActiveSessionIdAsync(chain.ChainId, bound.SessionId); + await kernel.RevokeSessionAsync(oldSession.SessionId, now); + }, ct); return issued; } public async Task RevokeSessionAsync(string? tenantId, AuthSessionId sessionId, DateTimeOffset at, CancellationToken ct = default) { - await _sessionStore.RevokeSessionAsync(tenantId, sessionId, at, ct ); + var kernel = _kernelFactory.Create(tenantId); + await kernel.ExecuteAsync(_ => kernel.RevokeSessionAsync(sessionId, at), ct); } public async Task RevokeChainAsync(string? tenantId, SessionChainId chainId, DateTimeOffset at, CancellationToken ct = default) { - await _sessionStore.RevokeChainAsync(tenantId, chainId, at, ct ); + var kernel = _kernelFactory.Create(tenantId); + await kernel.ExecuteAsync(_ => kernel.RevokeChainAsync(chainId, at), ct); } public async Task RevokeAllChainsAsync(string? tenantId, UserKey userKey, SessionChainId? exceptChainId, DateTimeOffset at, CancellationToken ct = default) { - await _sessionStore.RevokeAllChainsAsync(tenantId, userKey, exceptChainId, at, ct ); + var kernel = _kernelFactory.Create(tenantId); + await kernel.ExecuteAsync(async _ => + { + var chains = await kernel.GetChainsByUserAsync(userKey); + + foreach (var chain in chains) + { + if (exceptChainId.HasValue && chain.ChainId == exceptChainId.Value) + continue; + + if (!chain.IsRevoked) + await kernel.RevokeChainAsync(chain.ChainId, at); + + var activeSessionId = await kernel.GetActiveSessionIdAsync(chain.ChainId); + if (activeSessionId is not null) + await kernel.RevokeSessionAsync(activeSessionId.Value, at); + } + }, ct); } + // TODO: Discuss revoking chains/sessions when root is revoked public async Task RevokeRootAsync(string? tenantId, UserKey userKey, DateTimeOffset at, CancellationToken ct = default) { - await _sessionStore.RevokeRootAsync(tenantId, userKey, at, ct ); + var kernel = _kernelFactory.Create(tenantId); + await kernel.ExecuteAsync(_ => kernel.RevokeSessionRootAsync(userKey, at), ct); } } diff --git a/src/CodeBeam.UltimateAuth.Server/Services/ISessionQueryService.cs b/src/CodeBeam.UltimateAuth.Server/Services/ISessionQueryService.cs index fa674213..9ff5cc6d 100644 --- a/src/CodeBeam.UltimateAuth.Server/Services/ISessionQueryService.cs +++ b/src/CodeBeam.UltimateAuth.Server/Services/ISessionQueryService.cs @@ -1,18 +1,38 @@ using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Server.Services +// TenantId parameter only come from AuthFlowContext. +namespace CodeBeam.UltimateAuth.Server.Services; + +/// +/// Read-only session query API. +/// Used for validation, UI, monitoring, and diagnostics. +/// +public interface ISessionQueryService { - public interface ISessionQueryService - { - Task ValidateSessionAsync(SessionValidationContext context, CancellationToken ct = default); + /// + /// Validates a session for runtime authentication. + /// Hot path – must be fast and side-effect free. + /// + Task ValidateSessionAsync(SessionValidationContext context, CancellationToken ct = default); - Task GetSessionAsync(string? tenantId, AuthSessionId sessionId, CancellationToken ct = default); + /// + /// Retrieves a specific session by id. + /// + Task GetSessionAsync(AuthSessionId sessionId, CancellationToken ct = default); - Task> GetSessionsByChainAsync(string? tenantId, SessionChainId chainId, CancellationToken ct = default); + /// + /// Retrieves all sessions belonging to a specific chain. + /// + Task> GetSessionsByChainAsync(SessionChainId chainId, CancellationToken ct = default); - Task> GetChainsByUserAsync(string? tenantId, UserKey userKey, CancellationToken ct = default); + /// + /// Retrieves all session chains for a user. + /// + Task> GetChainsByUserAsync(UserKey userKey, CancellationToken ct = default); - Task ResolveChainIdAsync(string? tenantId, AuthSessionId sessionId, CancellationToken ct = default); - } + /// + /// Resolves the chain id for a given session. + /// + Task ResolveChainIdAsync(AuthSessionId sessionId, CancellationToken ct = default); } diff --git a/src/CodeBeam.UltimateAuth.Server/Services/UAuthFlowService.cs b/src/CodeBeam.UltimateAuth.Server/Services/UAuthFlowService.cs index 6864311e..7ef7ece7 100644 --- a/src/CodeBeam.UltimateAuth.Server/Services/UAuthFlowService.cs +++ b/src/CodeBeam.UltimateAuth.Server/Services/UAuthFlowService.cs @@ -73,7 +73,7 @@ public Task LogoutAsync(LogoutRequest request, CancellationToken ct = default) var now = request.At ?? DateTimeOffset.UtcNow; var authContext = authFlow.ToAuthContext(now); - return _orchestrator.ExecuteAsync(authContext, new RevokeSessionCommand(request.TenantId, request.SessionId), ct); + return _orchestrator.ExecuteAsync(authContext, new RevokeSessionCommand(request.SessionId), ct); } public async Task LogoutAllAsync(LogoutAllRequest request, CancellationToken ct = default) diff --git a/src/CodeBeam.UltimateAuth.Server/Services/UAuthSessionManager.cs b/src/CodeBeam.UltimateAuth.Server/Services/UAuthSessionManager.cs index 3e05168f..3ac5e5e4 100644 --- a/src/CodeBeam.UltimateAuth.Server/Services/UAuthSessionManager.cs +++ b/src/CodeBeam.UltimateAuth.Server/Services/UAuthSessionManager.cs @@ -1,5 +1,4 @@ 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; @@ -7,84 +6,46 @@ using CodeBeam.UltimateAuth.Server.Infrastructure.Orchestrator; // TODO: Add wrapper service in client project. Validate method also may add. -namespace CodeBeam.UltimateAuth.Server.Services -{ - internal sealed class UAuthSessionManager : IUAuthSessionManager - { - private readonly IAuthFlowContextAccessor _authFlow; - private readonly ISessionOrchestrator _orchestrator; - private readonly ISessionQueryService _sessionQueryService; - private readonly IClock _clock; - - public UAuthSessionManager(IAuthFlowContextAccessor authFlow, ISessionOrchestrator orchestrator, ISessionQueryService sessionQueryService, IClock clock) - { - _authFlow = authFlow; - _orchestrator = orchestrator; - _sessionQueryService = sessionQueryService; - _clock = clock; - } - - public Task> GetChainsAsync( - string? tenantId, - UserKey userKey) - => _sessionQueryService.GetChainsByUserAsync(tenantId, userKey); - - public Task> GetSessionsAsync( - string? tenantId, - SessionChainId chainId) - => _sessionQueryService.GetSessionsByChainAsync(tenantId, chainId); - - public Task GetSessionAsync( - string? tenantId, - AuthSessionId sessionId) - => _sessionQueryService.GetSessionAsync(tenantId, sessionId); - - public Task RevokeSessionAsync(string? tenantId, AuthSessionId sessionId, DateTimeOffset at) - { - var authContext = _authFlow.Current.ToAuthContext(_clock.UtcNow); - var command = new RevokeSessionCommand(tenantId, sessionId); - - return _orchestrator.ExecuteAsync(authContext, command); - } - - public Task ResolveChainIdAsync(string? tenantId, AuthSessionId sessionId) - => _sessionQueryService.ResolveChainIdAsync(tenantId, sessionId); - - public Task RevokeAllChainsAsync(string? tenantId, UserKey userKey, SessionChainId? exceptChainId, DateTimeOffset at) - { - var authContext = _authFlow.Current.ToAuthContext(_clock.UtcNow); - var command = new RevokeAllChainsCommand(userKey, exceptChainId); - - return _orchestrator.ExecuteAsync(authContext, command); - } - - public Task RevokeChainAsync(string? tenantId, SessionChainId chainId, DateTimeOffset at) - { - var authContext = _authFlow.Current.ToAuthContext(_clock.UtcNow); - var command = new RevokeChainCommand(chainId); +namespace CodeBeam.UltimateAuth.Server.Services; - return _orchestrator.ExecuteAsync(authContext, command); - } - - public Task RevokeRootAsync(string? tenantId, UserKey userKey, DateTimeOffset at) - { - var authContext = _authFlow.Current.ToAuthContext(_clock.UtcNow); - var command = new RevokeRootCommand(userKey); - - return _orchestrator.ExecuteAsync(authContext, command); - } +internal sealed class UAuthSessionManager : IUAuthSessionManager +{ + private readonly IAuthFlowContextAccessor _authFlow; + private readonly ISessionOrchestrator _orchestrator; + private readonly IClock _clock; - public async Task GetCurrentSessionAsync(string? tenantId, AuthSessionId sessionId) - { - var chainId = await _sessionQueryService.ResolveChainIdAsync(tenantId, sessionId); + public UAuthSessionManager(IAuthFlowContextAccessor authFlow, ISessionOrchestrator orchestrator, IClock clock) + { + _authFlow = authFlow; + _orchestrator = orchestrator; + _clock = clock; + } - if (chainId is null) - return null; + public Task RevokeSessionAsync(AuthSessionId sessionId, CancellationToken ct = default) + { + var authContext = _authFlow.Current.ToAuthContext(_clock.UtcNow); + var command = new RevokeSessionCommand(sessionId); + return _orchestrator.ExecuteAsync(authContext, command, ct); + } - var sessions = await _sessionQueryService.GetSessionsByChainAsync(tenantId, chainId.Value); + public Task RevokeChainAsync(SessionChainId chainId, CancellationToken ct = default) + { + var authContext = _authFlow.Current.ToAuthContext(_clock.UtcNow); + var command = new RevokeChainCommand(chainId); + return _orchestrator.ExecuteAsync(authContext, command, ct); + } - return sessions.FirstOrDefault(s => s.SessionId == sessionId); - } + public Task RevokeAllChainsAsync(UserKey userKey, SessionChainId? exceptChainId, CancellationToken ct = default) + { + var authContext = _authFlow.Current.ToAuthContext(_clock.UtcNow); + var command = new RevokeAllChainsCommand(userKey, exceptChainId); + return _orchestrator.ExecuteAsync(authContext, command, ct); + } + public Task RevokeRootAsync(UserKey userKey, CancellationToken ct = default) + { + var authContext = _authFlow.Current.ToAuthContext(_clock.UtcNow); + var command = new RevokeRootCommand(userKey); + return _orchestrator.ExecuteAsync(authContext, command, ct); } } diff --git a/src/CodeBeam.UltimateAuth.Server/Services/UAuthSessionQueryService.cs b/src/CodeBeam.UltimateAuth.Server/Services/UAuthSessionQueryService.cs index a6736a01..e9e3e19d 100644 --- a/src/CodeBeam.UltimateAuth.Server/Services/UAuthSessionQueryService.cs +++ b/src/CodeBeam.UltimateAuth.Server/Services/UAuthSessionQueryService.cs @@ -1,82 +1,90 @@ -using CodeBeam.UltimateAuth.Authorization; -using CodeBeam.UltimateAuth.Core; +using CodeBeam.UltimateAuth.Core; using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Server.Auth; using CodeBeam.UltimateAuth.Server.Options; using Microsoft.Extensions.Options; -using System.ComponentModel.DataAnnotations; -namespace CodeBeam.UltimateAuth.Server.Services +namespace CodeBeam.UltimateAuth.Server.Services; + +public sealed class UAuthSessionQueryService : ISessionQueryService { - public sealed class UAuthSessionQueryService : ISessionQueryService + private readonly ISessionStoreKernelFactory _storeFactory; + private readonly IUserClaimsProvider _claimsProvider; + private readonly IAuthFlowContextAccessor _authFlow; + private readonly UAuthServerOptions _options; + + public UAuthSessionQueryService( + ISessionStoreKernelFactory storeFactory, + IUserClaimsProvider claimsProvider, + IAuthFlowContextAccessor authFlow, + IOptions options) + { + _storeFactory = storeFactory; + _claimsProvider = claimsProvider; + _authFlow = authFlow; + _options = options.Value; + } + + // Validate runs before AuthFlowContext is set, do not call _authFlow here. + // TODO: Seperate this method to ISessionValidator service? + public async Task ValidateSessionAsync(SessionValidationContext context, CancellationToken ct = default) + { + var kernel = _storeFactory.Create(context.TenantId); + var session = await kernel.GetSessionAsync(context.SessionId); + + if (session is null) + return SessionValidationResult.Invalid(SessionState.NotFound, sessionId: context.SessionId); + + var state = session.GetState(context.Now, _options.Session.IdleTimeout); + if (state != SessionState.Active) + return SessionValidationResult.Invalid(state, sessionId: session.SessionId, chainId: session.ChainId); + + var chain = await kernel.GetChainAsync(session.ChainId); + if (chain is null || chain.IsRevoked) + return SessionValidationResult.Invalid(SessionState.Revoked, session.UserKey, session.SessionId, session.ChainId); + + var root = await kernel.GetSessionRootByUserAsync(session.UserKey); + if (root is null || root.IsRevoked) + return SessionValidationResult.Invalid(SessionState.Revoked, session.UserKey, session.SessionId, session.ChainId, root?.RootId); + + if (session.SecurityVersionAtCreation != root.SecurityVersion) + return SessionValidationResult.Invalid(SessionState.SecurityMismatch, session.UserKey, session.SessionId, session.ChainId, root.RootId); + + // TODO: Implement device id, AllowAndRebind behavior and check device mathing in blazor server circuit and external http calls. + // Currently this line has error on refresh flow. + //if (!session.Device.Matches(context.Device) && _options.Session.DeviceMismatchBehavior == DeviceMismatchBehavior.Reject) + // return SessionValidationResult.Invalid(SessionState.DeviceMismatch); + + var claims = await _claimsProvider.GetClaimsAsync(context.TenantId, session.UserKey, ct); + return SessionValidationResult.Active(context.TenantId, session.UserKey, session.SessionId, session.ChainId, root.RootId, claims, boundDeviceId: session.Device.DeviceId); + } + + public Task GetSessionAsync(AuthSessionId sessionId, CancellationToken ct = default) + { + return CreateKernel().GetSessionAsync(sessionId); + } + + public Task> GetSessionsByChainAsync(SessionChainId chainId, CancellationToken ct = default) + { + return CreateKernel().GetSessionsByChainAsync(chainId); + } + + public Task> GetChainsByUserAsync(UserKey userKey, CancellationToken ct = default) + { + return CreateKernel().GetChainsByUserAsync(userKey); + } + + public Task ResolveChainIdAsync(AuthSessionId sessionId, CancellationToken ct = default) + { + return CreateKernel().GetChainIdBySessionAsync(sessionId); + } + + private ISessionStoreKernel CreateKernel() { - private readonly ISessionStoreKernelFactory _storeFactory; - private readonly IUserClaimsProvider _claimsProvider; - private readonly UAuthServerOptions _options; - - public UAuthSessionQueryService(ISessionStoreKernelFactory storeFactory, IUserClaimsProvider claimsProvider, IOptions options) - { - _storeFactory = storeFactory; - _claimsProvider = claimsProvider; - _options = options.Value; - } - - public async Task ValidateSessionAsync(SessionValidationContext context, CancellationToken ct = default) - { - var kernel = _storeFactory.Create(context.TenantId); - - var session = await kernel.GetSessionAsync(context.SessionId); - if (session is null) - return SessionValidationResult.Invalid(SessionState.NotFound, sessionId: context.SessionId); - - var state = session.GetState(context.Now, _options.Session.IdleTimeout); - if (state != SessionState.Active) - return SessionValidationResult.Invalid(state, sessionId: session.SessionId, chainId: session.ChainId); - - var chain = await kernel.GetChainAsync(session.ChainId); - if (chain is null || chain.IsRevoked) - return SessionValidationResult.Invalid(SessionState.Revoked, session.UserKey, session.SessionId, session.ChainId); - - var root = await kernel.GetSessionRootByUserAsync(session.UserKey); - if (root is null || root.IsRevoked) - return SessionValidationResult.Invalid(SessionState.Revoked, session.UserKey, session.SessionId, session.ChainId, root?.RootId); - - if (session.SecurityVersionAtCreation != root.SecurityVersion) - return SessionValidationResult.Invalid(SessionState.SecurityMismatch, session.UserKey, session.SessionId, session.ChainId, root.RootId); - - // TODO: Implement device id, AllowAndRebind behavior and check device mathing in blazor server circuit and external http calls. - // Currently this line has error on refresh flow. - //if (!session.Device.Matches(context.Device) && _options.Session.DeviceMismatchBehavior == DeviceMismatchBehavior.Reject) - // return SessionValidationResult.Invalid(SessionState.DeviceMismatch); - - var claims = await _claimsProvider.GetClaimsAsync(context.TenantId, session.UserKey, ct); - return SessionValidationResult.Active(context.TenantId, session.UserKey, session.SessionId, session.ChainId, root.RootId, claims, boundDeviceId: session.Device.DeviceId); - } - - public Task GetSessionAsync(string? tenantId, AuthSessionId sessionId, CancellationToken ct = default) - { - var kernel = _storeFactory.Create(tenantId); - return kernel.GetSessionAsync(sessionId); - } - - public Task> GetSessionsByChainAsync(string? tenantId, SessionChainId chainId, CancellationToken ct = default) - { - var kernel = _storeFactory.Create(tenantId); - return kernel.GetSessionsByChainAsync(chainId); - } - - public Task> GetChainsByUserAsync(string? tenantId, UserKey userKey, CancellationToken ct = default) - { - var kernel = _storeFactory.Create(tenantId); - return kernel.GetChainsByUserAsync(userKey); - } - - public Task ResolveChainIdAsync(string? tenantId, AuthSessionId sessionId, CancellationToken ct = default) - { - var kernel = _storeFactory.Create(tenantId); - return kernel.GetChainIdBySessionAsync(sessionId); - } + var tenantId = _authFlow.Current.TenantId; + return _storeFactory.Create(tenantId); } } diff --git a/src/CodeBeam.UltimateAuth.Server/Stores/UAuthSessionStoreFactory.cs b/src/CodeBeam.UltimateAuth.Server/Stores/UAuthSessionStoreFactory.cs deleted file mode 100644 index d1e541d7..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Stores/UAuthSessionStoreFactory.cs +++ /dev/null @@ -1,33 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Abstractions; -using Microsoft.Extensions.DependencyInjection; - -namespace CodeBeam.UltimateAuth.Server.Stores -{ - /// - /// UltimateAuth default session store factory. - /// Resolves session store kernels from DI and provides them - /// to framework-level session stores. - /// - public sealed class UAuthSessionStoreFactory : ISessionStoreKernelFactory - { - private readonly IServiceProvider _provider; - - public UAuthSessionStoreFactory(IServiceProvider provider) - { - _provider = provider; - } - - public ISessionStoreKernel Create(string? tenantId) - { - var kernel = _provider.GetService(); - - if (kernel is ITenantAwareSessionStore tenantAware) - { - tenantAware.BindTenant(tenantId); - } - - return kernel; - } - - } -} diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/AuthSessionIdEfConverter.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/AuthSessionIdEfConverter.cs deleted file mode 100644 index 75245e2a..00000000 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/AuthSessionIdEfConverter.cs +++ /dev/null @@ -1,38 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Domain; - -namespace CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore -{ - internal static class AuthSessionIdEfConverter - { - public static AuthSessionId FromDatabase(string raw) - { - if (!AuthSessionId.TryCreate(raw, out var id)) - { - throw new InvalidOperationException( - $"Invalid AuthSessionId value in database: '{raw}'"); - } - - return id; - } - - public static string ToDatabase(AuthSessionId id) - => id.Value; - - public static AuthSessionId? FromDatabaseNullable(string? raw) - { - if (raw is null) - return null; - - if (!AuthSessionId.TryCreate(raw, out var id)) - { - throw new InvalidOperationException( - $"Invalid AuthSessionId value in database: '{raw}'"); - } - - return id; - } - - public static string? ToDatabaseNullable(AuthSessionId? id) - => id?.Value; - } -} diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/UAuthSessionDbContext.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Data/UAuthSessionDbContext.cs similarity index 86% rename from src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/UAuthSessionDbContext.cs rename to src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Data/UAuthSessionDbContext.cs index 6e1b3f55..7a8e77fc 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/UAuthSessionDbContext.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Data/UAuthSessionDbContext.cs @@ -1,4 +1,5 @@ using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; using Microsoft.EntityFrameworkCore; namespace CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore @@ -9,13 +10,25 @@ internal sealed class UltimateAuthSessionDbContext : DbContext public DbSet Chains => Set(); public DbSet Sessions => Set(); - public UltimateAuthSessionDbContext(DbContextOptions options) : base(options) - { + private readonly TenantContext _tenant; + + public UltimateAuthSessionDbContext(DbContextOptions options, TenantContext tenant) : base(options) + { + _tenant = tenant; } protected override void OnModelCreating(ModelBuilder b) { + b.Entity() + .HasQueryFilter(x => _tenant.IsGlobal || x.TenantId == _tenant.TenantId); + + b.Entity() + .HasQueryFilter(x => _tenant.IsGlobal || x.TenantId == _tenant.TenantId); + + b.Entity() + .HasQueryFilter(x => _tenant.IsGlobal || x.TenantId == _tenant.TenantId); + b.Entity(e => { e.HasKey(x => x.Id); diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/EfCoreSessionStore.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/EfCoreSessionStore.cs deleted file mode 100644 index 24e4dc1b..00000000 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/EfCoreSessionStore.cs +++ /dev/null @@ -1,374 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Abstractions; -using CodeBeam.UltimateAuth.Core.Contracts; -using CodeBeam.UltimateAuth.Core.Domain; -using Microsoft.EntityFrameworkCore; -using System.Security; - -namespace CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore; - -internal sealed class EfCoreSessionStore : ISessionStore -{ - private readonly EfCoreSessionStoreKernel _kernel; - private readonly UltimateAuthSessionDbContext _db; - - public EfCoreSessionStore(EfCoreSessionStoreKernel kernel, UltimateAuthSessionDbContext db) - { - _kernel = kernel; - _db = db; - } - - public async Task GetSessionAsync(string? tenantId, AuthSessionId sessionId, CancellationToken ct = default) - { - var projection = await _db.Sessions - .AsNoTracking() - .SingleOrDefaultAsync( - x => x.SessionId == sessionId && - x.TenantId == tenantId, - ct); - - return projection?.ToDomain(); - } - - public async Task CreateSessionAsync(IssuedSession issued, SessionStoreContext ctx, CancellationToken ct = default) - { - await _kernel.ExecuteAsync(async ct => - { - var now = ctx.IssuedAt; - - var rootProjection = await _db.Roots - .SingleOrDefaultAsync( - x => x.TenantId == ctx.TenantId && - x.UserKey == ctx.UserKey, - ct); - - ISessionRoot root; - - if (rootProjection is null) - { - root = UAuthSessionRoot.Create(ctx.TenantId, ctx.UserKey, now); - _db.Roots.Add(root.ToProjection()); - } - else - { - var chainProjections = await _db.Chains - .AsNoTracking() - .Where(x => x.RootId == rootProjection.RootId) - .ToListAsync(ct); - - root = rootProjection.ToDomain( - chainProjections.Select(c => c.ToDomain()).ToList()); - } - - ISessionChain chain; - - if (ctx.ChainId is not null) - { - var chainProjection = await _db.Chains - .SingleAsync(x => x.ChainId == ctx.ChainId.Value, ct); - - chain = chainProjection.ToDomain(); - } - else - { - chain = UAuthSessionChain.Create( - SessionChainId.New(), - root.RootId, - ctx.TenantId, - ctx.UserKey, - root.SecurityVersion, - ClaimsSnapshot.Empty); - - _db.Chains.Add(chain.ToProjection()); - root = root.AttachChain(chain, now); - } - - var issuedSession = (UAuthSession)issued.Session; - - if (!issuedSession.ChainId.IsUnassigned) - throw new InvalidOperationException("Issued session already has chain."); - - var session = issuedSession.WithChain(chain.ChainId); - - _db.Sessions.Add(session.ToProjection()); - - var updatedChain = chain.AttachSession(session.SessionId); - _db.Chains.Update(updatedChain.ToProjection()); - }, ct); - } - - public async Task RotateSessionAsync(AuthSessionId currentSessionId, IssuedSession issued, SessionStoreContext ctx, CancellationToken ct = default) - { - await _kernel.ExecuteAsync(async ct => - { - var now = ctx.IssuedAt; - - var oldProjection = await _db.Sessions - .SingleOrDefaultAsync( - x => x.SessionId == currentSessionId && - x.TenantId == ctx.TenantId, - ct); - - if (oldProjection is null) - throw new SecurityException("Session not found."); - - var oldSession = oldProjection.ToDomain(); - - if (oldSession.IsRevoked || oldSession.ExpiresAt <= now) - throw new SecurityException("Session is no longer valid."); - - var chainProjection = await _db.Chains - .SingleOrDefaultAsync( - x => x.ChainId == oldSession.ChainId, - ct); - - if (chainProjection is null) - throw new SecurityException("Chain not found."); - - var chain = chainProjection.ToDomain(); - - if (chain.IsRevoked) - throw new SecurityException("Chain is revoked."); - - var newSession = ((UAuthSession)issued.Session) - .WithChain(chain.ChainId); - - _db.Sessions.Add(newSession.ToProjection()); - - var rotatedChain = chain.RotateSession(newSession.SessionId); - _db.Chains.Update(rotatedChain.ToProjection()); - - var revokedOld = oldSession.Revoke(now); - _db.Sessions.Update(revokedOld.ToProjection()); - }, ct); - } - - public async Task TouchSessionAsync(string? tenantId, AuthSessionId sessionId, DateTimeOffset at, SessionTouchMode mode = SessionTouchMode.IfNeeded, CancellationToken ct = default) - { - var touched = false; - - await _kernel.ExecuteAsync(async ct => - { - var projection = await _db.Sessions.SingleOrDefaultAsync(x => x.TenantId == tenantId && x.SessionId == sessionId, ct); - - if (projection is null) - return; - - var session = projection.ToDomain(); - - if (session.IsRevoked) - return; - - if (mode == SessionTouchMode.IfNeeded && at - session.LastSeenAt < TimeSpan.FromMinutes(1)) - return; - - var updated = session.Touch(at); - _db.Sessions.Update(updated.ToProjection()); - - touched = true; - }, ct); - - return touched; - } - - public Task RevokeSessionAsync(string? tenantId, AuthSessionId sessionId, DateTimeOffset at, CancellationToken ct = default) - => _kernel.ExecuteAsync(async ct => - { - var projection = await _db.Sessions.SingleOrDefaultAsync(x => x.SessionId == sessionId && x.TenantId == tenantId, ct); - - if (projection is null) - return; - - var session = projection.ToDomain(); - - if (session.IsRevoked) - return; - - _db.Sessions.Update(session.Revoke(at).ToProjection()); - }, ct); - - public async Task RevokeAllChainsAsync(string? tenantId, UserKey userKey, SessionChainId? exceptChainId, DateTimeOffset at, CancellationToken ct = default) - { - await _kernel.ExecuteAsync(async ct => - { - var chains = await _db.Chains - .Where(x => - x.TenantId == tenantId && - x.UserKey == userKey) - .ToListAsync(ct); - - foreach (var chainProjection in chains) - { - if (exceptChainId.HasValue && - chainProjection.ChainId == exceptChainId.Value) - continue; - - var chain = chainProjection.ToDomain(); - - if (!chain.IsRevoked) - _db.Chains.Update(chain.Revoke(at).ToProjection()); - - if (chain.ActiveSessionId is not null) - { - var sessionProjection = await _db.Sessions.SingleOrDefaultAsync(x => x.TenantId == tenantId && x.SessionId == chain.ActiveSessionId, ct); - - if (sessionProjection is not null) - { - var session = sessionProjection.ToDomain(); - if (!session.IsRevoked) - _db.Sessions.Update(session.Revoke(at).ToProjection()); - } - } - } - }, ct); - } - - public Task RevokeChainAsync(string? tenantId, SessionChainId chainId, DateTimeOffset at, CancellationToken ct = default) - => _kernel.ExecuteAsync(async ct => - { - var projection = await _db.Chains - .SingleOrDefaultAsync( - x => x.ChainId == chainId && - x.TenantId == tenantId, - ct); - - if (projection is null) - return; - - var chain = projection.ToDomain(); - - if (chain.IsRevoked) - return; - - _db.Chains.Update(chain.Revoke(at).ToProjection()); - - if (chain.ActiveSessionId is not null) - { - var sessionProjection = await _db.Sessions.SingleOrDefaultAsync(x => x.TenantId == tenantId && x.SessionId == chain.ActiveSessionId, ct); - - if (sessionProjection is not null) - { - var session = sessionProjection.ToDomain(); - if (!session.IsRevoked) - _db.Sessions.Update(session.Revoke(at).ToProjection()); - } - } - }, ct); - - public Task RevokeRootAsync(string? tenantId, UserKey userKey, DateTimeOffset at, CancellationToken ct = default) - => _kernel.ExecuteAsync(async ct => - { - var rootProjection = await _db.Roots - .SingleOrDefaultAsync( - x => x.TenantId == tenantId && - x.UserKey == userKey, - ct); - - if (rootProjection is null) - return; - - var chainProjections = await _db.Chains - .Where(x => x.RootId == rootProjection.RootId) - .ToListAsync(ct); - - foreach (var chainProjection in chainProjections) - { - var chain = chainProjection.ToDomain(); - _db.Chains.Update(chain.Revoke(at).ToProjection()); - - if (chain.ActiveSessionId is not null) - { - var sessionProjection = await _db.Sessions.SingleOrDefaultAsync(x => x.TenantId == tenantId && x.SessionId == chain.ActiveSessionId, ct); - - if (sessionProjection is not null) - { - var session = sessionProjection.ToDomain(); - _db.Sessions.Update(session.Revoke(at).ToProjection()); - } - } - } - - var root = rootProjection.ToDomain(chainProjections.Select(c => c.ToDomain()).ToList()); - - _db.Roots.Update(root.Revoke(at).ToProjection()); - }, ct); - - public async Task> GetSessionsByChainAsync(string? tenantId, SessionChainId chainId, CancellationToken ct = default) - { - var projections = await _db.Sessions - .AsNoTracking() - .Where(x => - x.ChainId == chainId && - x.TenantId == tenantId) - .ToListAsync(ct); - - return projections.Select(x => x.ToDomain()).ToList(); - } - - public async Task> GetChainsByUserAsync(string? tenantId, UserKey userKey, CancellationToken ct = default) - { - var projections = await _db.Chains - .AsNoTracking() - .Where(x => - x.TenantId == tenantId && - x.UserKey.Equals(userKey)) - .ToListAsync(ct); - - return projections.Select(x => x.ToDomain()).ToList(); - } - - public async Task GetChainAsync(string? tenantId, SessionChainId chainId, CancellationToken ct = default) - { - var projection = await _db.Chains - .AsNoTracking() - .SingleOrDefaultAsync( - x => x.ChainId == chainId && - x.TenantId == tenantId, - ct); - - return projection?.ToDomain(); - } - - public async Task GetChainIdBySessionAsync(string? tenantId, AuthSessionId sessionId, CancellationToken ct = default) - { - return await _db.Sessions - .AsNoTracking() - .Where(x => - x.SessionId == sessionId && - x.TenantId == tenantId) - .Select(x => (SessionChainId?)x.ChainId) - .SingleOrDefaultAsync(ct); - } - - public async Task GetActiveSessionIdAsync(string? tenantId, SessionChainId chainId, CancellationToken ct = default) - { - return await _db.Chains - .AsNoTracking() - .Where(x => - x.ChainId == chainId && - x.TenantId == tenantId) - .Select(x => x.ActiveSessionId) - .SingleOrDefaultAsync(ct); - } - - public async Task GetSessionRootAsync(string? tenantId, UserKey userKey, CancellationToken ct = default) - { - var rootProjection = await _db.Roots - .AsNoTracking() - .SingleOrDefaultAsync( - x => x.TenantId == tenantId && - x.UserKey!.Equals(userKey), - ct); - - if (rootProjection is null) - return null; - - var chainProjections = await _db.Chains - .AsNoTracking() - .Where(x => - x.TenantId == tenantId && - x.UserKey!.Equals(userKey)) - .ToListAsync(ct); - - return rootProjection.ToDomain(chainProjections.Select(x => x.ToDomain()).ToList()); - } -} diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/EfCoreSessionStoreKernelFactory.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/EfCoreSessionStoreKernelFactory.cs deleted file mode 100644 index 86770175..00000000 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/EfCoreSessionStoreKernelFactory.cs +++ /dev/null @@ -1,20 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Abstractions; -using Microsoft.Extensions.DependencyInjection; - -namespace CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore -{ - public sealed class EfCoreSessionStoreKernelFactory : ISessionStoreKernelFactory - { - private readonly IServiceProvider _sp; - - public EfCoreSessionStoreKernelFactory(IServiceProvider sp) - { - _sp = sp; - } - - public ISessionStoreKernel Create(string? tenantId) - { - return _sp.GetRequiredService(); - } - } -} diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/ServiceCollectionExtensions.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs similarity index 90% rename from src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/ServiceCollectionExtensions.cs rename to src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs index 2b786a5c..7be3e1b2 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/ServiceCollectionExtensions.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs @@ -10,7 +10,6 @@ public static IServiceCollection AddUltimateAuthEntityFrameworkCoreSessions(configureDb); services.AddScoped(); - services.AddScoped(); return services; } diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Infrastructure/AuthSessionIdEfConverter.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Infrastructure/AuthSessionIdEfConverter.cs new file mode 100644 index 00000000..7aae5ad5 --- /dev/null +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Infrastructure/AuthSessionIdEfConverter.cs @@ -0,0 +1,36 @@ +using CodeBeam.UltimateAuth.Core.Domain; + +namespace CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore; + +internal static class AuthSessionIdEfConverter +{ + public static AuthSessionId FromDatabase(string raw) + { + if (!AuthSessionId.TryCreate(raw, out var id)) + { + throw new InvalidOperationException( + $"Invalid AuthSessionId value in database: '{raw}'"); + } + + return id; + } + + public static string ToDatabase(AuthSessionId id) + => id.Value; + + public static AuthSessionId? FromDatabaseNullable(string? raw) + { + if (raw is null) + return null; + + if (!AuthSessionId.TryCreate(raw, out var id)) + { + throw new InvalidOperationException( + $"Invalid AuthSessionId value in database: '{raw}'"); + } + + return id; + } + + public static string? ToDatabaseNullable(AuthSessionId? id) => id?.Value; +} diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/JsonValueConverter.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Infrastructure/JsonValueConverter.cs similarity index 100% rename from src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/JsonValueConverter.cs rename to src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Infrastructure/JsonValueConverter.cs diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/NullableAuthSessionIdConverter.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Infrastructure/NullableAuthSessionIdConverter.cs similarity index 100% rename from src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/NullableAuthSessionIdConverter.cs rename to src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Infrastructure/NullableAuthSessionIdConverter.cs diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionChainProjectionMapper.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionChainProjectionMapper.cs index b1b1c32e..f93d05a7 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionChainProjectionMapper.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionChainProjectionMapper.cs @@ -4,7 +4,7 @@ namespace CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore { internal static class SessionChainProjectionMapper { - public static ISessionChain ToDomain(this SessionChainProjection p) + public static UAuthSessionChain ToDomain(this SessionChainProjection p) { return UAuthSessionChain.FromProjection( p.ChainId, @@ -20,7 +20,7 @@ public static ISessionChain ToDomain(this SessionChainProjection p) ); } - public static SessionChainProjection ToProjection(this ISessionChain chain) + public static SessionChainProjection ToProjection(this UAuthSessionChain chain) { return new SessionChainProjection { diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionProjectionMapper.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionProjectionMapper.cs index ed2a371e..692aea12 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionProjectionMapper.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionProjectionMapper.cs @@ -4,7 +4,7 @@ namespace CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore { internal static class SessionProjectionMapper { - public static ISession ToDomain(this SessionProjection p) + public static UAuthSession ToDomain(this SessionProjection p) { return UAuthSession.FromProjection( p.SessionId, @@ -23,7 +23,7 @@ public static ISession ToDomain(this SessionProjection p) ); } - public static SessionProjection ToProjection(this ISession s) + public static SessionProjection ToProjection(this UAuthSession s) { return new SessionProjection { diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionRootProjectionMapper.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionRootProjectionMapper.cs index e8f0f950..a28fde46 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionRootProjectionMapper.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionRootProjectionMapper.cs @@ -4,7 +4,7 @@ namespace CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore { internal static class SessionRootProjectionMapper { - public static ISessionRoot ToDomain(this SessionRootProjection root, IReadOnlyList? chains = null) + public static UAuthSessionRoot ToDomain(this SessionRootProjection root, IReadOnlyList? chains = null) { return UAuthSessionRoot.FromProjection( root.RootId, @@ -13,12 +13,12 @@ public static ISessionRoot ToDomain(this SessionRootProjection root, IReadOnlyLi root.IsRevoked, root.RevokedAt, root.SecurityVersion, - chains ?? Array.Empty(), + chains ?? Array.Empty(), root.LastUpdatedAt ); } - public static SessionRootProjection ToProjection(this ISessionRoot root) + public static SessionRootProjection ToProjection(this UAuthSessionRoot root) { return new SessionRootProjection { diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/EfCoreSessionStoreKernel.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Stores/EfCoreSessionStoreKernel.cs similarity index 88% rename from src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/EfCoreSessionStoreKernel.cs rename to src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Stores/EfCoreSessionStoreKernel.cs index 05b51217..ed07e8b4 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/EfCoreSessionStoreKernel.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Stores/EfCoreSessionStoreKernel.cs @@ -1,5 +1,6 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; using Microsoft.EntityFrameworkCore; using System.Data; @@ -8,10 +9,12 @@ namespace CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore internal sealed class EfCoreSessionStoreKernel : ISessionStoreKernel { private readonly UltimateAuthSessionDbContext _db; + private readonly TenantContext _tenant; - public EfCoreSessionStoreKernel(UltimateAuthSessionDbContext db) + public EfCoreSessionStoreKernel(UltimateAuthSessionDbContext db, TenantContext tenant) { _db = db; + _tenant = tenant; } public async Task ExecuteAsync(Func action, CancellationToken ct = default) @@ -45,7 +48,7 @@ await strategy.ExecuteAsync(async () => }); } - public async Task GetSessionAsync(AuthSessionId sessionId) + public async Task GetSessionAsync(AuthSessionId sessionId) { var projection = await _db.Sessions .AsNoTracking() @@ -54,7 +57,7 @@ await strategy.ExecuteAsync(async () => return projection?.ToDomain(); } - public async Task SaveSessionAsync(ISession session) + public async Task SaveSessionAsync(UAuthSession session) { var projection = session.ToProjection(); @@ -83,7 +86,7 @@ public async Task RevokeSessionAsync(AuthSessionId sessionId, DateTimeOffset at) _db.Sessions.Update(revoked.ToProjection()); } - public async Task GetChainAsync(SessionChainId chainId) + public async Task GetChainAsync(SessionChainId chainId) { var projection = await _db.Chains .AsNoTracking() @@ -92,7 +95,7 @@ public async Task RevokeSessionAsync(AuthSessionId sessionId, DateTimeOffset at) return projection?.ToDomain(); } - public async Task SaveChainAsync(ISessionChain chain) + public async Task SaveChainAsync(UAuthSessionChain chain) { var projection = chain.ToProjection(); @@ -141,7 +144,7 @@ public async Task SetActiveSessionIdAsync(SessionChainId chainId, AuthSessionId _db.Chains.Update(projection); } - public async Task GetSessionRootByUserAsync(UserKey userKey) + public async Task GetSessionRootByUserAsync(UserKey userKey) { var rootProjection = await _db.Roots .AsNoTracking() @@ -158,7 +161,7 @@ public async Task SetActiveSessionIdAsync(SessionChainId chainId, AuthSessionId return rootProjection.ToDomain(chains.Select(c => c.ToDomain()).ToList()); } - public async Task SaveSessionRootAsync(ISessionRoot root) + public async Task SaveSessionRootAsync(UAuthSessionRoot root) { var projection = root.ToProjection(); @@ -191,7 +194,7 @@ public async Task RevokeSessionRootAsync(UserKey userKey, DateTimeOffset at) .SingleOrDefaultAsync(); } - public async Task> GetChainsByUserAsync(UserKey userKey) + public async Task> GetChainsByUserAsync(UserKey userKey) { var projections = await _db.Chains .AsNoTracking() @@ -201,7 +204,7 @@ public async Task> GetChainsByUserAsync(UserKey use return projections.Select(x => x.ToDomain()).ToList(); } - public async Task> GetSessionsByChainAsync(SessionChainId chainId) + public async Task> GetSessionsByChainAsync(SessionChainId chainId) { var projections = await _db.Sessions .AsNoTracking() @@ -211,7 +214,7 @@ public async Task> GetSessionsByChainAsync(SessionChainI return projections.Select(x => x.ToDomain()).ToList(); } - public async Task GetSessionRootByIdAsync(SessionRootId rootId) + public async Task GetSessionRootByIdAsync(SessionRootId rootId) { var rootProjection = await _db.Roots .AsNoTracking() diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Stores/EfCoreSessionStoreKernelFactory.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Stores/EfCoreSessionStoreKernelFactory.cs new file mode 100644 index 00000000..96c01c37 --- /dev/null +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Stores/EfCoreSessionStoreKernelFactory.cs @@ -0,0 +1,25 @@ +using CodeBeam.UltimateAuth.Core.Abstractions; +using CodeBeam.UltimateAuth.Core.MultiTenancy; +using Microsoft.Extensions.DependencyInjection; + +namespace CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore; + +public sealed class EfCoreSessionStoreKernelFactory : ISessionStoreKernelFactory +{ + private readonly IServiceProvider _sp; + + public EfCoreSessionStoreKernelFactory(IServiceProvider sp) + { + _sp = sp; + } + + public ISessionStoreKernel Create(string? tenantId) + { + return ActivatorUtilities.CreateInstance(_sp, new TenantContext(tenantId)); + } + + public ISessionStoreKernel CreateGlobal() + { + return ActivatorUtilities.CreateInstance(_sp, new TenantContext(null, isGlobal: true)); + } +} diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/InMemorySessionStore.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/InMemorySessionStore.cs deleted file mode 100644 index 6402d1df..00000000 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/InMemorySessionStore.cs +++ /dev/null @@ -1,156 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Abstractions; -using CodeBeam.UltimateAuth.Core.Contracts; -using CodeBeam.UltimateAuth.Core.Domain; -using CodeBeam.UltimateAuth.Server.Options; -using Microsoft.Extensions.Options; -using System.Security; - -namespace CodeBeam.UltimateAuth.Sessions.InMemory; - -public sealed class InMemorySessionStore : ISessionStore -{ - private readonly ISessionStoreKernelFactory _factory; - private readonly UAuthServerOptions _options; - - public InMemorySessionStore(ISessionStoreKernelFactory factory, IOptions options) - { - _factory = factory; - _options = options.Value; - } - - private ISessionStoreKernel Kernel(string? tenantId) - => _factory.Create(tenantId); - - public Task GetSessionAsync(string? tenantId, AuthSessionId sessionId, CancellationToken ct = default) - => Kernel(tenantId).GetSessionAsync(sessionId); - - public async Task CreateSessionAsync(IssuedSession issued, SessionStoreContext ctx, CancellationToken ct = default) - { - var k = Kernel(ctx.TenantId); - - await k.ExecuteAsync(async (ct) => - { - var now = ctx.IssuedAt; - - var root = await k.GetSessionRootByUserAsync(ctx.UserKey) ?? UAuthSessionRoot.Create(ctx.TenantId, ctx.UserKey, now); - ISessionChain chain; - - if (ctx.ChainId is not null) - { - chain = await k.GetChainAsync(ctx.ChainId.Value) ?? throw new InvalidOperationException("Chain not found."); - } - else - { - chain = UAuthSessionChain.Create( - SessionChainId.New(), - root.RootId, - ctx.TenantId, - ctx.UserKey, - root.SecurityVersion, - ClaimsSnapshot.Empty); - - root = root.AttachChain(chain, now); - } - - var session = issued.Session; - - if (!session.ChainId.IsUnassigned) - { - throw new InvalidOperationException("Issued session already has a chain assigned."); - } - - session = session.WithChain(chain.ChainId); - - // Persist (order intentional) - await k.SaveSessionRootAsync(root); - await k.SaveChainAsync(chain); - await k.SaveSessionAsync(session); - await k.SetActiveSessionIdAsync(chain.ChainId, session.SessionId); - }, ct); - } - - public async Task RotateSessionAsync(AuthSessionId currentSessionId, IssuedSession issued, SessionStoreContext ctx, CancellationToken ct = default) - { - var k = Kernel(ctx.TenantId); - - await k.ExecuteAsync(async (ct) => - { - var now = ctx.IssuedAt; - - var old = await k.GetSessionAsync(currentSessionId) - ?? throw new SecurityException("Session not found."); - - if (old.IsRevoked || old.ExpiresAt <= now) - throw new SecurityException("Session is no longer valid."); - - var chain = await k.GetChainAsync(old.ChainId) - ?? throw new SecurityException("Chain not found."); - - if (chain.IsRevoked) - throw new SecurityException("Chain is revoked."); - - var newSession = ((UAuthSession)issued.Session).WithChain(chain.ChainId); - - await k.SaveSessionAsync(newSession); - await k.SetActiveSessionIdAsync(chain.ChainId, newSession.SessionId); - await k.RevokeSessionAsync(old.SessionId, now); - }, ct); - } - - public async Task TouchSessionAsync(string? tenantId, AuthSessionId sessionId, DateTimeOffset at, SessionTouchMode mode = SessionTouchMode.IfNeeded, CancellationToken ct = default) - { - var k = Kernel(null); - bool touched = false; - - await k.ExecuteAsync(async (ct) => - { - var session = await k.GetSessionAsync(sessionId); - if (session is null || session.IsRevoked) - return; - - if (mode == SessionTouchMode.IfNeeded) - { - var elapsed = at - session.LastSeenAt; - if (elapsed < _options.Session.TouchInterval) - return; - } - - var updated = session.Touch(at); - await k.SaveSessionAsync(updated); - - touched = true; - }, ct); - - return touched; - } - - public Task RevokeSessionAsync(string? tenantId, AuthSessionId sessionId, DateTimeOffset at, CancellationToken ct = default) - => Kernel(tenantId).RevokeSessionAsync(sessionId, at); - - public async Task RevokeAllChainsAsync(string? tenantId, UserKey userKey, SessionChainId? exceptChainId, DateTimeOffset at, CancellationToken ct = default) - { - var k = Kernel(tenantId); - - await k.ExecuteAsync(async (ct) => - { - var chains = await k.GetChainsByUserAsync(userKey); - - foreach (var chain in chains) - { - if (exceptChainId.HasValue && chain.ChainId == exceptChainId.Value) - continue; - - await k.RevokeChainAsync(chain.ChainId, at); - - if (chain.ActiveSessionId is not null) - await k.RevokeSessionAsync(chain.ActiveSessionId.Value, at); - } - }, ct); - } - - public Task RevokeChainAsync(string? tenantId, SessionChainId chainId, DateTimeOffset at, CancellationToken ct = default) - => Kernel(tenantId).RevokeChainAsync(chainId, at); - - public Task RevokeRootAsync(string? tenantId, UserKey userKey, DateTimeOffset at, CancellationToken ct = default) - => Kernel(tenantId).RevokeSessionRootAsync(userKey, at); -} diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/InMemorySessionStoreFactory.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/InMemorySessionStoreFactory.cs deleted file mode 100644 index 157bfd8f..00000000 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/InMemorySessionStoreFactory.cs +++ /dev/null @@ -1,24 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Abstractions; -using System.Collections.Concurrent; - -namespace CodeBeam.UltimateAuth.Sessions.InMemory -{ - public sealed class InMemorySessionStoreFactory : ISessionStoreKernelFactory - { - private readonly ConcurrentDictionary _stores = new(); - - public ISessionStoreKernel Create(string? tenantId) - { - var key = tenantId ?? "__single__"; - - var store = _stores.GetOrAdd(key, _ => - { - var k = new InMemorySessionStoreKernel(); - k.BindTenant(tenantId); - return k; - }); - - return (ISessionStoreKernel)store; - } - } -} diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/InMemorySessionStoreKernel.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/InMemorySessionStoreKernel.cs index 3aaeee4c..9a2dc197 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/InMemorySessionStoreKernel.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/InMemorySessionStoreKernel.cs @@ -2,22 +2,15 @@ using CodeBeam.UltimateAuth.Core.Domain; using System.Collections.Concurrent; -internal sealed class InMemorySessionStoreKernel : ISessionStoreKernel, ITenantAwareSessionStore +internal sealed class InMemorySessionStoreKernel : ISessionStoreKernel { private readonly SemaphoreSlim _tx = new(1, 1); - private readonly ConcurrentDictionary _sessions = new(); - private readonly ConcurrentDictionary _chains = new(); - private readonly ConcurrentDictionary _roots = new(); + private readonly ConcurrentDictionary _sessions = new(); + private readonly ConcurrentDictionary _chains = new(); + private readonly ConcurrentDictionary _roots = new(); private readonly ConcurrentDictionary _activeSessions = new(); - public string? TenantId { get; private set; } - - public void BindTenant(string? tenantId) - { - TenantId = tenantId ?? "__single__"; - } - public async Task ExecuteAsync(Func action, CancellationToken ct = default) { await _tx.WaitAsync(ct); @@ -31,10 +24,10 @@ public async Task ExecuteAsync(Func action, Cancellatio } } - public Task GetSessionAsync(AuthSessionId sessionId) + public Task GetSessionAsync(AuthSessionId sessionId) => Task.FromResult(_sessions.TryGetValue(sessionId, out var s) ? s : null); - public Task SaveSessionAsync(ISession session) + public Task SaveSessionAsync(UAuthSession session) { _sessions[session.SessionId] = session; return Task.CompletedTask; @@ -49,10 +42,10 @@ public Task RevokeSessionAsync(AuthSessionId sessionId, DateTimeOffset at) return Task.CompletedTask; } - public Task GetChainAsync(SessionChainId chainId) + public Task GetChainAsync(SessionChainId chainId) => Task.FromResult(_chains.TryGetValue(chainId, out var c) ? c : null); - public Task SaveChainAsync(ISessionChain chain) + public Task SaveChainAsync(UAuthSessionChain chain) { _chains[chain.ChainId] = chain; return Task.CompletedTask; @@ -76,13 +69,13 @@ public Task SetActiveSessionIdAsync(SessionChainId chainId, AuthSessionId sessio return Task.CompletedTask; } - public Task GetSessionRootByUserAsync(UserKey userKey) + public Task GetSessionRootByUserAsync(UserKey userKey) => Task.FromResult(_roots.TryGetValue(userKey, out var r) ? r : null); - public Task GetSessionRootByIdAsync(SessionRootId rootId) + public Task GetSessionRootByIdAsync(SessionRootId rootId) => Task.FromResult(_roots.Values.FirstOrDefault(r => r.RootId == rootId)); - public Task SaveSessionRootAsync(ISessionRoot root) + public Task SaveSessionRootAsync(UAuthSessionRoot root) { _roots[root.UserKey] = root; return Task.CompletedTask; @@ -105,21 +98,21 @@ public Task RevokeSessionRootAsync(UserKey userKey, DateTimeOffset at) return Task.FromResult(null); } - public Task> GetChainsByUserAsync(UserKey userKey) + public Task> GetChainsByUserAsync(UserKey userKey) { if (!_roots.TryGetValue(userKey, out var root)) - return Task.FromResult>(Array.Empty()); + return Task.FromResult>(Array.Empty()); - return Task.FromResult>(root.Chains.ToList()); + return Task.FromResult>(root.Chains.ToList()); } - public Task> GetSessionsByChainAsync(SessionChainId chainId) + public Task> GetSessionsByChainAsync(SessionChainId chainId) { var result = _sessions.Values .Where(s => s.ChainId == chainId) .ToList(); - return Task.FromResult>(result); + return Task.FromResult>(result); } public Task DeleteExpiredSessionsAsync(DateTimeOffset at) @@ -130,7 +123,13 @@ public Task DeleteExpiredSessionsAsync(DateTimeOffset at) if (session.ExpiresAt <= at) { - _sessions[kvp.Key] = session.Revoke(at); + var revoked = session.Revoke(at); + _sessions[kvp.Key] = revoked; + + if (_activeSessions.TryGetValue(revoked.ChainId, out var activeId) && activeId == revoked.SessionId) + { + _activeSessions.TryRemove(revoked.ChainId, out _); + } } } diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/InMemorySessionStoreKernelFactory.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/InMemorySessionStoreKernelFactory.cs new file mode 100644 index 00000000..7682086e --- /dev/null +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/InMemorySessionStoreKernelFactory.cs @@ -0,0 +1,17 @@ +using CodeBeam.UltimateAuth.Core.Abstractions; +using System.Collections.Concurrent; + +namespace CodeBeam.UltimateAuth.Sessions.InMemory; + +public sealed class InMemorySessionStoreKernelFactory : ISessionStoreKernelFactory +{ + private readonly ConcurrentDictionary _kernels = new(); + + public ISessionStoreKernel Create(string? tenantId) + { + //var key = TenantKey.Normalize(tenantId); + var key = tenantId ?? "__default__"; + + return _kernels.GetOrAdd(key, _ => new InMemorySessionStoreKernel()); + } +} diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/ServiceCollectionExtensions.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/ServiceCollectionExtensions.cs index c12a8157..aebffb25 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/ServiceCollectionExtensions.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/ServiceCollectionExtensions.cs @@ -1,16 +1,13 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using Microsoft.Extensions.DependencyInjection; -namespace CodeBeam.UltimateAuth.Sessions.InMemory +namespace CodeBeam.UltimateAuth.Sessions.InMemory; + +public static class ServiceCollectionExtensions { - public static class ServiceCollectionExtensions + public static IServiceCollection AddUltimateAuthInMemorySessions(this IServiceCollection services) { - public static IServiceCollection AddUltimateAuthInMemorySessions(this IServiceCollection services) - { - services.AddSingleton(); - // TODO: Discuss it to be singleton or scoped - services.AddScoped(); - return services; - } + services.AddSingleton(); + return services; } } From b07161f7b3f00d01407e2ca8625d48382da6d6f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Can=20Karag=C3=B6z?= Date: Mon, 2 Feb 2026 11:34:29 +0300 Subject: [PATCH 5/9] Added ISessionValidator --- .../Abstractions/IHttpSessionIssuer.cs | 19 - .../Auth/Context/AuthFlowContextFactory.cs | 8 +- .../UAuthAuthenticationHandler.cs | 51 +-- .../DefaultValidateEndpointHandler.cs | 131 ++++--- .../UAuthServerServiceCollectionExtensions.cs | 35 +- .../Issuers/UAuthSessionIssuer.cs | 335 ++++++++---------- .../Services/DefaultRefreshFlowService.cs | 17 +- .../Services/ISessionQueryService.cs | 6 - .../Services/ISessionValidator.cs | 13 + .../Services/UAuthSessionQueryService.cs | 48 +-- .../Services/UAuthSessionValidator.cs | 58 +++ 11 files changed, 304 insertions(+), 417 deletions(-) delete mode 100644 src/CodeBeam.UltimateAuth.Server/Abstractions/IHttpSessionIssuer.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Services/ISessionValidator.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Services/UAuthSessionValidator.cs diff --git a/src/CodeBeam.UltimateAuth.Server/Abstractions/IHttpSessionIssuer.cs b/src/CodeBeam.UltimateAuth.Server/Abstractions/IHttpSessionIssuer.cs deleted file mode 100644 index 75edff58..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Abstractions/IHttpSessionIssuer.cs +++ /dev/null @@ -1,19 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Abstractions; -using CodeBeam.UltimateAuth.Core.Contracts; -using CodeBeam.UltimateAuth.Core.Domain; -using Microsoft.AspNetCore.Http; - -namespace CodeBeam.UltimateAuth.Server.Abstractions -{ - /// - /// HTTP-aware session issuer used by UltimateAuth server components. - /// Extends the core ISessionIssuer contract with HttpContext-bound - /// operations required for cookie-based session binding. - /// - public interface IHttpSessionIssuer : ISessionIssuer - { - Task IssueLoginSessionAsync(HttpContext httpContext, AuthenticatedSessionContext context, CancellationToken ct = default); - - Task RotateSessionAsync(HttpContext httpContext, SessionRotationContext context, CancellationToken ct = default); - } -} diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthFlowContextFactory.cs b/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthFlowContextFactory.cs index 6b3e618d..165def4a 100644 --- a/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthFlowContextFactory.cs +++ b/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthFlowContextFactory.cs @@ -22,7 +22,7 @@ internal sealed class DefaultAuthFlowContextFactory : IAuthFlowContextFactory private readonly IAuthResponseResolver _authResponseResolver; private readonly IDeviceResolver _deviceResolver; private readonly IDeviceContextFactory _deviceContextFactory; - private readonly ISessionQueryService _sessionQueryService; + private readonly ISessionValidator _sessionValidator; public DefaultAuthFlowContextFactory( IClientProfileReader clientProfileReader, @@ -31,7 +31,7 @@ public DefaultAuthFlowContextFactory( IAuthResponseResolver authResponseResolver, IDeviceResolver deviceResolver, IDeviceContextFactory deviceContextFactory, - ISessionQueryService sessionQueryService) + ISessionValidator sessionValidator) { _clientProfileReader = clientProfileReader; _primaryTokenResolver = primaryTokenResolver; @@ -39,7 +39,7 @@ public DefaultAuthFlowContextFactory( _authResponseResolver = authResponseResolver; _deviceResolver = deviceResolver; _deviceContextFactory = deviceContextFactory; - _sessionQueryService = sessionQueryService; + _sessionValidator = sessionValidator; } public async ValueTask CreateAsync(HttpContext ctx, AuthFlowType flowType, CancellationToken ct = default) @@ -64,7 +64,7 @@ public async ValueTask CreateAsync(HttpContext ctx, AuthFlowTyp if (!sessionCtx.IsAnonymous) { - var validation = await _sessionQueryService.ValidateSessionAsync( + var validation = await _sessionValidator.ValidateSessionAsync( new SessionValidationContext { TenantId = sessionCtx.TenantId, diff --git a/src/CodeBeam.UltimateAuth.Server/Authentication/UAuthAuthenticationHandler.cs b/src/CodeBeam.UltimateAuth.Server/Authentication/UAuthAuthenticationHandler.cs index 107c95d8..67507aef 100644 --- a/src/CodeBeam.UltimateAuth.Server/Authentication/UAuthAuthenticationHandler.cs +++ b/src/CodeBeam.UltimateAuth.Server/Authentication/UAuthAuthenticationHandler.cs @@ -4,18 +4,16 @@ using CodeBeam.UltimateAuth.Core.Extensions; using CodeBeam.UltimateAuth.Server.Defaults; using CodeBeam.UltimateAuth.Server.Infrastructure; -using CodeBeam.UltimateAuth.Server.Services; using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using System.Security.Claims; namespace CodeBeam.UltimateAuth.Server.Authentication; internal sealed class UAuthAuthenticationHandler : AuthenticationHandler { private readonly ITransportCredentialResolver _transportCredentialResolver; - private readonly ISessionQueryService _sessionQuery; + private readonly ISessionValidator _sessionValidator; private readonly IDeviceContextFactory _deviceContextFactory; private readonly IClock _clock; @@ -25,13 +23,13 @@ public UAuthAuthenticationHandler( ILoggerFactory logger, System.Text.Encodings.Web.UrlEncoder encoder, ISystemClock clock, - ISessionQueryService sessionQuery, + ISessionValidator sessionValidator, IDeviceContextFactory deviceContextFactory, IClock uauthClock) : base(options, logger, encoder, clock) { _transportCredentialResolver = transportCredentialResolver; - _sessionQuery = sessionQuery; + _sessionValidator = sessionValidator; _deviceContextFactory = deviceContextFactory; _clock = uauthClock; } @@ -45,7 +43,7 @@ protected override async Task HandleAuthenticateAsync() if (!AuthSessionId.TryCreate(credential.Value, out var sessionId)) return AuthenticateResult.Fail("Invalid credential"); - var result = await _sessionQuery.ValidateSessionAsync( + var result = await _sessionValidator.ValidateSessionAsync( new SessionValidationContext { TenantId = credential.TenantId, @@ -59,46 +57,5 @@ protected override async Task HandleAuthenticateAsync() var principal = result.Claims.ToClaimsPrincipal(UAuthCookieDefaults.AuthenticationScheme); return AuthenticateResult.Success(new AuthenticationTicket(principal, UAuthCookieDefaults.AuthenticationScheme)); - - - //var principal = CreatePrincipal(result); - //var ticket = new AuthenticationTicket(principal,UAuthCookieDefaults.AuthenticationScheme); - - //return AuthenticateResult.Success(ticket); } - - private static ClaimsPrincipal CreatePrincipal(SessionValidationResult result) - { - //var claims = new List - //{ - // new Claim(ClaimTypes.NameIdentifier, result.UserKey.Value), - // new Claim("uauth:session_id", result.SessionId.ToString()) - //}; - - //if (!string.IsNullOrEmpty(result.TenantId)) - //{ - // claims.Add(new Claim("uauth:tenant", result.TenantId)); - //} - - //// Session claims (snapshot) - //foreach (var (key, value) in result.Claims.AsDictionary()) - //{ - // if (key == "http://schemas.microsoft.com/ws/2008/06/identity/claims/role") - // { - // foreach (var role in value.Split(',')) - // claims.Add(new Claim(ClaimTypes.Role, role)); - // } - // else - // { - // claims.Add(new Claim(key, value)); - // } - //} - - //var identity = new ClaimsIdentity(claims, UAuthCookieDefaults.AuthenticationScheme); - //return new ClaimsPrincipal(identity); - - var identity = new ClaimsIdentity(result.Claims.ToClaims(), UAuthCookieDefaults.AuthenticationScheme); - return new ClaimsPrincipal(identity); - } - } \ No newline at end of file diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultValidateEndpointHandler.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultValidateEndpointHandler.cs index f47f8a6a..4135b0c9 100644 --- a/src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultValidateEndpointHandler.cs +++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultValidateEndpointHandler.cs @@ -1,97 +1,94 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; -using CodeBeam.UltimateAuth.Core.Extensions; using CodeBeam.UltimateAuth.Server.Auth; using CodeBeam.UltimateAuth.Server.Infrastructure; -using CodeBeam.UltimateAuth.Server.Services; using Microsoft.AspNetCore.Http; -namespace CodeBeam.UltimateAuth.Server.Endpoints +namespace CodeBeam.UltimateAuth.Server.Endpoints; + +internal sealed class DefaultValidateEndpointHandler : IValidateEndpointHandler { - internal sealed class DefaultValidateEndpointHandler : IValidateEndpointHandler + private readonly IAuthFlowContextAccessor _authContext; + private readonly IFlowCredentialResolver _credentialResolver; + private readonly ISessionValidator _sessionValidator; + private readonly IClock _clock; + + public DefaultValidateEndpointHandler( + IAuthFlowContextAccessor authContext, + IFlowCredentialResolver credentialResolver, + ISessionValidator sessionValidator, + IClock clock) + { + _authContext = authContext; + _credentialResolver = credentialResolver; + _sessionValidator = sessionValidator; + _clock = clock; + } + + public async Task ValidateAsync(HttpContext context, CancellationToken ct = default) { - private readonly IAuthFlowContextAccessor _authContext; - private readonly IFlowCredentialResolver _credentialResolver; - private readonly ISessionQueryService _sessionValidator; - private readonly IClock _clock; + var auth = _authContext.Current; + var credential = _credentialResolver.Resolve(context, auth.Response); - public DefaultValidateEndpointHandler( - IAuthFlowContextAccessor authContext, - IFlowCredentialResolver credentialResolver, - ISessionQueryService sessionValidator, - IClock clock) + if (credential is null) { - _authContext = authContext; - _credentialResolver = credentialResolver; - _sessionValidator = sessionValidator; - _clock = clock; + return Results.Json( + new AuthValidationResult + { + IsValid = false, + State = "missing" + }, + statusCode: StatusCodes.Status401Unauthorized + ); } - public async Task ValidateAsync(HttpContext context, CancellationToken ct = default) + if (credential.Kind == PrimaryCredentialKind.Stateful) { - var auth = _authContext.Current; - var credential = _credentialResolver.Resolve(context, auth.Response); - - if (credential is null) + if (!AuthSessionId.TryCreate(credential.Value, out var sessionId)) { return Results.Json( new AuthValidationResult { IsValid = false, - State = "missing" + State = "invalid" }, statusCode: StatusCodes.Status401Unauthorized ); } - if (credential.Kind == PrimaryCredentialKind.Stateful) - { - if (!AuthSessionId.TryCreate(credential.Value, out var sessionId)) + var result = await _sessionValidator.ValidateSessionAsync( + new SessionValidationContext { - return Results.Json( - new AuthValidationResult - { - IsValid = false, - State = "invalid" - }, - statusCode: StatusCodes.Status401Unauthorized - ); - } - - var result = await _sessionValidator.ValidateSessionAsync( - new SessionValidationContext - { - TenantId = credential.TenantId, - SessionId = sessionId, - Now = _clock.UtcNow, - Device = auth.Device - }, - ct); - - return Results.Ok(new AuthValidationResult - { - IsValid = result.IsValid, - State = result.IsValid ? "active" : result.State.ToString().ToLowerInvariant(), - Snapshot = new AuthStateSnapshot - { - UserId = result.UserKey, - TenantId = result.TenantId, - Claims = result.Claims, - AuthenticatedAt = _clock.UtcNow, - } - }); - } + TenantId = credential.TenantId, + SessionId = sessionId, + Now = _clock.UtcNow, + Device = auth.Device + }, + ct); - // Stateless (JWT / Opaque) – 0.0.1 no support yet - return Results.Json( - new AuthValidationResult + return Results.Ok(new AuthValidationResult + { + IsValid = result.IsValid, + State = result.IsValid ? "active" : result.State.ToString().ToLowerInvariant(), + Snapshot = new AuthStateSnapshot { - IsValid = false, - State = "unsupported" - }, - statusCode: StatusCodes.Status401Unauthorized - ); + UserId = result.UserKey, + TenantId = result.TenantId, + Claims = result.Claims, + AuthenticatedAt = _clock.UtcNow, + } + }); } + + // Stateless (JWT / Opaque) – 0.0.1 no support yet + return Results.Json( + new AuthValidationResult + { + IsValid = false, + State = "unsupported" + }, + statusCode: StatusCodes.Status401Unauthorized + ); } } diff --git a/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthServerServiceCollectionExtensions.cs b/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthServerServiceCollectionExtensions.cs index a885acc1..4f6cf0b2 100644 --- a/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthServerServiceCollectionExtensions.cs +++ b/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthServerServiceCollectionExtensions.cs @@ -179,6 +179,7 @@ private static IServiceCollection AddUltimateAuthServerInternal(this IServiceCol services.TryAddScoped(typeof(ISessionQueryService), typeof(UAuthSessionQueryService)); services.TryAddScoped(typeof(IRefreshTokenResolver), typeof(DefaultRefreshTokenResolver)); services.TryAddScoped(typeof(ISessionTouchService), typeof(DefaultSessionTouchService)); + services.TryAddScoped(); services.TryAddScoped(); services.TryAddScoped(); services.TryAddScoped(); @@ -229,7 +230,6 @@ private static IServiceCollection AddUltimateAuthServerInternal(this IServiceCol services.TryAddScoped(); - services.TryAddSingleton(); // Endpoint handlers @@ -247,43 +247,10 @@ private static IServiceCollection AddUltimateAuthServerInternal(this IServiceCol services.TryAddScoped>(); services.TryAddScoped(); - //services.TryAddScoped(); - //services.TryAddScoped(); - //services.TryAddScoped(); return services; } - //internal static IServiceCollection AddUltimateAuthPolicies(this IServiceCollection services, Action? configure = null) - //{ - // if (services.Any(d => d.ServiceType == typeof(AccessPolicyRegistry))) - // throw new InvalidOperationException("UltimateAuth policies already registered."); - - // var registry = new AccessPolicyRegistry(); - - // DefaultPolicySet.Register(registry); - // configure?.Invoke(registry); - // services.AddSingleton(registry); - // services.AddSingleton(sp => - // { - // var compiled = registry.Build(); - // return new DefaultAccessPolicyProvider(compiled, sp); - // }); - - // services.TryAddScoped(sp => - // { - // var invariants = sp.GetServices(); - // var globalPolicies = sp.GetServices(); - - // return new DefaultAccessAuthority(invariants, globalPolicies); - // }); - - // services.TryAddScoped(); - - - // return services; - //} - internal static IServiceCollection AddUltimateAuthPolicies(this IServiceCollection services, Action? configure = null) { if (services.Any(d => d.ServiceType == typeof(AccessPolicyRegistry))) diff --git a/src/CodeBeam.UltimateAuth.Server/Issuers/UAuthSessionIssuer.cs b/src/CodeBeam.UltimateAuth.Server/Issuers/UAuthSessionIssuer.cs index a15a08a9..fb360ef2 100644 --- a/src/CodeBeam.UltimateAuth.Server/Issuers/UAuthSessionIssuer.cs +++ b/src/CodeBeam.UltimateAuth.Server/Issuers/UAuthSessionIssuer.cs @@ -2,234 +2,201 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; -using CodeBeam.UltimateAuth.Server.Abstractions; -using CodeBeam.UltimateAuth.Server.Cookies; using CodeBeam.UltimateAuth.Server.Options; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; using System.Security; -namespace CodeBeam.UltimateAuth.Server.Issuers +namespace CodeBeam.UltimateAuth.Server.Issuers; + +public sealed class UAuthSessionIssuer : ISessionIssuer { - public sealed class UAuthSessionIssuer : IHttpSessionIssuer + private readonly ISessionStoreKernelFactory _kernelFactory; + private readonly IOpaqueTokenGenerator _opaqueGenerator; + private readonly UAuthServerOptions _options; + + public UAuthSessionIssuer( + ISessionStoreKernelFactory kernelFactory, + IOpaqueTokenGenerator opaqueGenerator, + IOptions options) + { + _kernelFactory = kernelFactory; + _opaqueGenerator = opaqueGenerator; + _options = options.Value; + } + + public async Task IssueLoginSessionAsync(AuthenticatedSessionContext context, CancellationToken ct = default) { - private readonly ISessionStoreKernelFactory _kernelFactory; - private readonly IOpaqueTokenGenerator _opaqueGenerator; - private readonly UAuthServerOptions _options; - private readonly IUAuthCookieManager _cookieManager; - - public UAuthSessionIssuer( - ISessionStoreKernelFactory kernelFactory, - IOpaqueTokenGenerator opaqueGenerator, - IOptions options, - IUAuthCookieManager cookieManager) + // Defensive guard — enforcement belongs to Authority + if (_options.Mode == UAuthMode.PureJwt) { - _kernelFactory = kernelFactory; - _opaqueGenerator = opaqueGenerator; - _options = options.Value; - _cookieManager = cookieManager; + throw new InvalidOperationException("Session issuance is not allowed in PureJwt mode."); } - public Task IssueLoginSessionAsync(AuthenticatedSessionContext context, CancellationToken ct = default) + 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); + + if (_options.Session.MaxLifetime is not null) { - return IssueLoginInternalAsync(httpContext: null, context, ct); + var absoluteExpiry = now.Add(_options.Session.MaxLifetime.Value); + if (absoluteExpiry < expiresAt) + expiresAt = absoluteExpiry; } - public Task IssueLoginSessionAsync(HttpContext httpContext, AuthenticatedSessionContext context, CancellationToken ct = default) + var session = UAuthSession.Create( + sessionId: sessionId, + tenantId: context.TenantId, + userKey: context.UserKey, + chainId: SessionChainId.Unassigned, + now: now, + expiresAt: expiresAt, + claims: context.Claims, + device: context.Device, + metadata: context.Metadata + ); + + var issued = new IssuedSession { - if (httpContext is null) - throw new ArgumentNullException(nameof(httpContext)); + Session = session, + OpaqueSessionId = opaqueSessionId, + IsMetadataOnly = _options.Mode == UAuthMode.SemiHybrid + }; - return IssueLoginInternalAsync(httpContext, context, ct); - } + var kernel = _kernelFactory.Create(context.TenantId); - private async Task IssueLoginInternalAsync(HttpContext? httpContext, AuthenticatedSessionContext context, CancellationToken ct = default) + await kernel.ExecuteAsync(async _ => { - // Defensive guard — enforcement belongs to Authority - if (_options.Mode == UAuthMode.PureJwt) - { - throw new InvalidOperationException("Session issuance is not allowed in PureJwt mode."); - } - - var now = context.Now; - var opaqueSessionId = _opaqueGenerator.Generate(); - if (!AuthSessionId.TryCreate(opaqueSessionId, out AuthSessionId sessionId)) - throw new InvalidCastException("Can't create opaque id."); + var root = await kernel.GetSessionRootByUserAsync(context.UserKey) + ?? UAuthSessionRoot.Create(context.TenantId, context.UserKey, now); - var expiresAt = now.Add(_options.Session.Lifetime); + UAuthSessionChain chain; - if (_options.Session.MaxLifetime is not null) + if (context.ChainId is not null) + { + chain = await kernel.GetChainAsync(context.ChainId.Value) + ?? throw new SecurityException("Chain not found."); + } + else { - var absoluteExpiry = now.Add(_options.Session.MaxLifetime.Value); - if (absoluteExpiry < expiresAt) - expiresAt = absoluteExpiry; + chain = UAuthSessionChain.Create( + SessionChainId.New(), + root.RootId, + context.TenantId, + context.UserKey, + root.SecurityVersion, + ClaimsSnapshot.Empty); + + await kernel.SaveChainAsync(chain); + root = root.AttachChain(chain, now); } - var session = UAuthSession.Create( - sessionId: sessionId, - tenantId: context.TenantId, - userKey: context.UserKey, - chainId: SessionChainId.Unassigned, - now: now, - expiresAt: expiresAt, - claims: context.Claims, - device: context.Device, - metadata: context.Metadata - ); + var boundSession = session.WithChain(chain.ChainId); - var issued = new IssuedSession - { - Session = session, - OpaqueSessionId = opaqueSessionId, - IsMetadataOnly = _options.Mode == UAuthMode.SemiHybrid - }; + await kernel.SaveSessionAsync(boundSession); + await kernel.SetActiveSessionIdAsync(chain.ChainId, boundSession.SessionId); + await kernel.SaveSessionRootAsync(root); + }, ct); - var kernel = _kernelFactory.Create(context.TenantId); + return issued; + } - await kernel.ExecuteAsync(async _ => - { - var root = await kernel.GetSessionRootByUserAsync(context.UserKey) - ?? UAuthSessionRoot.Create(context.TenantId, context.UserKey, now); - - UAuthSessionChain chain; - - if (context.ChainId is not null) - { - chain = await kernel.GetChainAsync(context.ChainId.Value) - ?? throw new SecurityException("Chain not found."); - } - else - { - chain = UAuthSessionChain.Create( - SessionChainId.New(), - root.RootId, - context.TenantId, - context.UserKey, - root.SecurityVersion, - ClaimsSnapshot.Empty); - - await kernel.SaveChainAsync(chain); - root = root.AttachChain(chain, now); - } - - var boundSession = session.WithChain(chain.ChainId); - - await kernel.SaveSessionAsync(boundSession); - await kernel.SetActiveSessionIdAsync(chain.ChainId, boundSession.SessionId); - await kernel.SaveSessionRootAsync(root); - }, ct); - - return issued; - } + public async Task RotateSessionAsync(SessionRotationContext context, CancellationToken ct = default) + { + var kernel = _kernelFactory.Create(context.TenantId); + var now = context.Now; - public Task RotateSessionAsync(SessionRotationContext context, CancellationToken ct = default) - { - return RotateInternalAsync(httpContext: null, context, ct); - } + var opaqueSessionId = _opaqueGenerator.Generate(); + if (!AuthSessionId.TryCreate(opaqueSessionId, out var newSessionId)) + throw new InvalidCastException("Can't create opaque session id."); - public Task RotateSessionAsync(HttpContext httpContext, SessionRotationContext context, CancellationToken ct = default) + var expiresAt = now.Add(_options.Session.Lifetime); + if (_options.Session.MaxLifetime is not null) { - if (httpContext is null) - throw new ArgumentNullException(nameof(httpContext)); - - return RotateInternalAsync(httpContext, context, ct); + var absoluteExpiry = now.Add(_options.Session.MaxLifetime.Value); + if (absoluteExpiry < expiresAt) + expiresAt = absoluteExpiry; } - private async Task RotateInternalAsync(HttpContext httpContext, SessionRotationContext context, CancellationToken ct = default) + var issued = new IssuedSession { - var kernel = _kernelFactory.Create(context.TenantId); - var now = context.Now; - - var opaqueSessionId = _opaqueGenerator.Generate(); - if (!AuthSessionId.TryCreate(opaqueSessionId, out var newSessionId)) - throw new InvalidCastException("Can't create opaque session id."); + Session = UAuthSession.Create( + sessionId: newSessionId, + tenantId: context.TenantId, + userKey: context.UserKey, + chainId: SessionChainId.Unassigned, + now: now, + expiresAt: expiresAt, + device: context.Device, + claims: context.Claims, + metadata: context.Metadata + ), + OpaqueSessionId = opaqueSessionId, + IsMetadataOnly = _options.Mode == UAuthMode.SemiHybrid + }; - var expiresAt = now.Add(_options.Session.Lifetime); - if (_options.Session.MaxLifetime is not null) - { - var absoluteExpiry = now.Add(_options.Session.MaxLifetime.Value); - if (absoluteExpiry < expiresAt) - expiresAt = absoluteExpiry; - } + await kernel.ExecuteAsync(async _ => + { + var oldSession = await kernel.GetSessionAsync(context.CurrentSessionId) + ?? throw new SecurityException("Session not found"); - var issued = new IssuedSession - { - Session = UAuthSession.Create( - sessionId: newSessionId, - tenantId: context.TenantId, - userKey: context.UserKey, - chainId: SessionChainId.Unassigned, - now: now, - expiresAt: expiresAt, - device: context.Device, - claims: context.Claims, - metadata: context.Metadata - ), - OpaqueSessionId = opaqueSessionId, - IsMetadataOnly = _options.Mode == UAuthMode.SemiHybrid - }; - - await kernel.ExecuteAsync(async _ => - { - var oldSession = await kernel.GetSessionAsync(context.CurrentSessionId) - ?? throw new SecurityException("Session not found"); + if (oldSession.IsRevoked || oldSession.ExpiresAt <= now) + throw new SecurityException("Session is not valid"); - if (oldSession.IsRevoked || oldSession.ExpiresAt <= now) - throw new SecurityException("Session is not valid"); + var chain = await kernel.GetChainAsync(oldSession.ChainId) + ?? throw new SecurityException("Chain not found"); - var chain = await kernel.GetChainAsync(oldSession.ChainId) - ?? throw new SecurityException("Chain not found"); + var bound = issued.Session.WithChain(chain.ChainId); - var bound = issued.Session.WithChain(chain.ChainId); + await kernel.SaveSessionAsync(bound); + await kernel.SetActiveSessionIdAsync(chain.ChainId, bound.SessionId); + await kernel.RevokeSessionAsync(oldSession.SessionId, now); + }, ct); - await kernel.SaveSessionAsync(bound); - await kernel.SetActiveSessionIdAsync(chain.ChainId, bound.SessionId); - await kernel.RevokeSessionAsync(oldSession.SessionId, now); - }, ct); + return issued; + } - return issued; - } + public async Task RevokeSessionAsync(string? tenantId, AuthSessionId sessionId, DateTimeOffset at, CancellationToken ct = default) + { + var kernel = _kernelFactory.Create(tenantId); + await kernel.ExecuteAsync(_ => kernel.RevokeSessionAsync(sessionId, at), ct); + } - public async Task RevokeSessionAsync(string? tenantId, AuthSessionId sessionId, DateTimeOffset at, CancellationToken ct = default) - { - var kernel = _kernelFactory.Create(tenantId); - await kernel.ExecuteAsync(_ => kernel.RevokeSessionAsync(sessionId, at), ct); - } + public async Task RevokeChainAsync(string? tenantId, SessionChainId chainId, DateTimeOffset at, CancellationToken ct = default) + { + var kernel = _kernelFactory.Create(tenantId); + await kernel.ExecuteAsync(_ => kernel.RevokeChainAsync(chainId, at), ct); + } - public async Task RevokeChainAsync(string? tenantId, SessionChainId chainId, DateTimeOffset at, CancellationToken ct = default) + public async Task RevokeAllChainsAsync(string? tenantId, UserKey userKey, SessionChainId? exceptChainId, DateTimeOffset at, CancellationToken ct = default) + { + var kernel = _kernelFactory.Create(tenantId); + await kernel.ExecuteAsync(async _ => { - var kernel = _kernelFactory.Create(tenantId); - await kernel.ExecuteAsync(_ => kernel.RevokeChainAsync(chainId, at), ct); - } + var chains = await kernel.GetChainsByUserAsync(userKey); - public async Task RevokeAllChainsAsync(string? tenantId, UserKey userKey, SessionChainId? exceptChainId, DateTimeOffset at, CancellationToken ct = default) - { - var kernel = _kernelFactory.Create(tenantId); - await kernel.ExecuteAsync(async _ => + foreach (var chain in chains) { - var chains = await kernel.GetChainsByUserAsync(userKey); + if (exceptChainId.HasValue && chain.ChainId == exceptChainId.Value) + continue; - foreach (var chain in chains) - { - if (exceptChainId.HasValue && chain.ChainId == exceptChainId.Value) - continue; + if (!chain.IsRevoked) + await kernel.RevokeChainAsync(chain.ChainId, at); - if (!chain.IsRevoked) - await kernel.RevokeChainAsync(chain.ChainId, at); - - var activeSessionId = await kernel.GetActiveSessionIdAsync(chain.ChainId); - if (activeSessionId is not null) - await kernel.RevokeSessionAsync(activeSessionId.Value, at); - } - }, ct); - } - - // TODO: Discuss revoking chains/sessions when root is revoked - public async Task RevokeRootAsync(string? tenantId, UserKey userKey, DateTimeOffset at, CancellationToken ct = default) - { - var kernel = _kernelFactory.Create(tenantId); - await kernel.ExecuteAsync(_ => kernel.RevokeSessionRootAsync(userKey, at), ct); - } + var activeSessionId = await kernel.GetActiveSessionIdAsync(chain.ChainId); + if (activeSessionId is not null) + await kernel.RevokeSessionAsync(activeSessionId.Value, at); + } + }, ct); + } + // TODO: Discuss revoking chains/sessions when root is revoked + public async Task RevokeRootAsync(string? tenantId, UserKey userKey, DateTimeOffset at, CancellationToken ct = default) + { + var kernel = _kernelFactory.Create(tenantId); + await kernel.ExecuteAsync(_ => kernel.RevokeSessionRootAsync(userKey, at), ct); } + } diff --git a/src/CodeBeam.UltimateAuth.Server/Services/DefaultRefreshFlowService.cs b/src/CodeBeam.UltimateAuth.Server/Services/DefaultRefreshFlowService.cs index db6542d6..ae818a52 100644 --- a/src/CodeBeam.UltimateAuth.Server/Services/DefaultRefreshFlowService.cs +++ b/src/CodeBeam.UltimateAuth.Server/Services/DefaultRefreshFlowService.cs @@ -10,24 +10,21 @@ namespace CodeBeam.UltimateAuth.Server.Services { internal sealed class DefaultRefreshFlowService : IRefreshFlowService { - private readonly ISessionQueryService _sessionQueries; + private readonly ISessionValidator _sessionValidator; private readonly ISessionTouchService _sessionRefresh; private readonly IRefreshTokenRotationService _tokenRotation; private readonly IRefreshTokenStore _refreshTokenStore; - private readonly IUserIdConverterResolver _userIdConverterResolver; public DefaultRefreshFlowService( - ISessionQueryService sessionQueries, + ISessionValidator sessionValidator, ISessionTouchService sessionRefresh, IRefreshTokenRotationService tokenRotation, - IRefreshTokenStore refreshTokenStore, - IUserIdConverterResolver userIdConverterResolver) + IRefreshTokenStore refreshTokenStore) { - _sessionQueries = sessionQueries; + _sessionValidator = sessionValidator; _sessionRefresh = sessionRefresh; _tokenRotation = tokenRotation; _refreshTokenStore = refreshTokenStore; - _userIdConverterResolver = userIdConverterResolver; } public async Task RefreshAsync(AuthFlowContext flow, RefreshFlowRequest request, CancellationToken ct = default) @@ -55,7 +52,7 @@ private async Task HandleSessionOnlyAsync(AuthFlowContext flo if (request.SessionId is null) return RefreshFlowResult.ReauthRequired(); - var validation = await _sessionQueries.ValidateSessionAsync( + var validation = await _sessionValidator.ValidateSessionAsync( new SessionValidationContext { TenantId = flow.TenantId, @@ -128,7 +125,7 @@ private async Task HandleHybridAsync(AuthFlowContext flow, Re if (request.SessionId is null || string.IsNullOrWhiteSpace(request.RefreshToken)) return RefreshFlowResult.ReauthRequired(); - var validation = await _sessionQueries.ValidateSessionAsync( + var validation = await _sessionValidator.ValidateSessionAsync( new SessionValidationContext { TenantId = flow.TenantId, @@ -179,7 +176,7 @@ private async Task HandleSemiHybridAsync(AuthFlowContext flow if (request.SessionId is null || string.IsNullOrWhiteSpace(request.RefreshToken)) return RefreshFlowResult.ReauthRequired(); - var validation = await _sessionQueries.ValidateSessionAsync( + var validation = await _sessionValidator.ValidateSessionAsync( new SessionValidationContext { TenantId = flow.TenantId, diff --git a/src/CodeBeam.UltimateAuth.Server/Services/ISessionQueryService.cs b/src/CodeBeam.UltimateAuth.Server/Services/ISessionQueryService.cs index 9ff5cc6d..6e88570a 100644 --- a/src/CodeBeam.UltimateAuth.Server/Services/ISessionQueryService.cs +++ b/src/CodeBeam.UltimateAuth.Server/Services/ISessionQueryService.cs @@ -10,12 +10,6 @@ namespace CodeBeam.UltimateAuth.Server.Services; /// public interface ISessionQueryService { - /// - /// Validates a session for runtime authentication. - /// Hot path – must be fast and side-effect free. - /// - Task ValidateSessionAsync(SessionValidationContext context, CancellationToken ct = default); - /// /// Retrieves a specific session by id. /// diff --git a/src/CodeBeam.UltimateAuth.Server/Services/ISessionValidator.cs b/src/CodeBeam.UltimateAuth.Server/Services/ISessionValidator.cs new file mode 100644 index 00000000..665455ee --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Services/ISessionValidator.cs @@ -0,0 +1,13 @@ +using CodeBeam.UltimateAuth.Core.Contracts; + +// This is a seperate service because validation runs only once before AuthFlowContext is created. +namespace CodeBeam.UltimateAuth.Server; + +public interface ISessionValidator +{ + /// + /// Validates a session for runtime authentication. + /// Hot path – must be fast and side-effect free. + /// + Task ValidateSessionAsync(SessionValidationContext context, CancellationToken ct = default); +} diff --git a/src/CodeBeam.UltimateAuth.Server/Services/UAuthSessionQueryService.cs b/src/CodeBeam.UltimateAuth.Server/Services/UAuthSessionQueryService.cs index e9e3e19d..f2edb58e 100644 --- a/src/CodeBeam.UltimateAuth.Server/Services/UAuthSessionQueryService.cs +++ b/src/CodeBeam.UltimateAuth.Server/Services/UAuthSessionQueryService.cs @@ -1,64 +1,20 @@ -using CodeBeam.UltimateAuth.Core; -using CodeBeam.UltimateAuth.Core.Abstractions; -using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Server.Auth; -using CodeBeam.UltimateAuth.Server.Options; -using Microsoft.Extensions.Options; namespace CodeBeam.UltimateAuth.Server.Services; public sealed class UAuthSessionQueryService : ISessionQueryService { private readonly ISessionStoreKernelFactory _storeFactory; - private readonly IUserClaimsProvider _claimsProvider; private readonly IAuthFlowContextAccessor _authFlow; - private readonly UAuthServerOptions _options; public UAuthSessionQueryService( ISessionStoreKernelFactory storeFactory, - IUserClaimsProvider claimsProvider, - IAuthFlowContextAccessor authFlow, - IOptions options) + IAuthFlowContextAccessor authFlow) { _storeFactory = storeFactory; - _claimsProvider = claimsProvider; _authFlow = authFlow; - _options = options.Value; - } - - // Validate runs before AuthFlowContext is set, do not call _authFlow here. - // TODO: Seperate this method to ISessionValidator service? - public async Task ValidateSessionAsync(SessionValidationContext context, CancellationToken ct = default) - { - var kernel = _storeFactory.Create(context.TenantId); - var session = await kernel.GetSessionAsync(context.SessionId); - - if (session is null) - return SessionValidationResult.Invalid(SessionState.NotFound, sessionId: context.SessionId); - - var state = session.GetState(context.Now, _options.Session.IdleTimeout); - if (state != SessionState.Active) - return SessionValidationResult.Invalid(state, sessionId: session.SessionId, chainId: session.ChainId); - - var chain = await kernel.GetChainAsync(session.ChainId); - if (chain is null || chain.IsRevoked) - return SessionValidationResult.Invalid(SessionState.Revoked, session.UserKey, session.SessionId, session.ChainId); - - var root = await kernel.GetSessionRootByUserAsync(session.UserKey); - if (root is null || root.IsRevoked) - return SessionValidationResult.Invalid(SessionState.Revoked, session.UserKey, session.SessionId, session.ChainId, root?.RootId); - - if (session.SecurityVersionAtCreation != root.SecurityVersion) - return SessionValidationResult.Invalid(SessionState.SecurityMismatch, session.UserKey, session.SessionId, session.ChainId, root.RootId); - - // TODO: Implement device id, AllowAndRebind behavior and check device mathing in blazor server circuit and external http calls. - // Currently this line has error on refresh flow. - //if (!session.Device.Matches(context.Device) && _options.Session.DeviceMismatchBehavior == DeviceMismatchBehavior.Reject) - // return SessionValidationResult.Invalid(SessionState.DeviceMismatch); - - var claims = await _claimsProvider.GetClaimsAsync(context.TenantId, session.UserKey, ct); - return SessionValidationResult.Active(context.TenantId, session.UserKey, session.SessionId, session.ChainId, root.RootId, claims, boundDeviceId: session.Device.DeviceId); } public Task GetSessionAsync(AuthSessionId sessionId, CancellationToken ct = default) diff --git a/src/CodeBeam.UltimateAuth.Server/Services/UAuthSessionValidator.cs b/src/CodeBeam.UltimateAuth.Server/Services/UAuthSessionValidator.cs new file mode 100644 index 00000000..9c134022 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Services/UAuthSessionValidator.cs @@ -0,0 +1,58 @@ +using CodeBeam.UltimateAuth.Core; +using CodeBeam.UltimateAuth.Core.Abstractions; +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Server.Options; +using Microsoft.Extensions.Options; + +namespace CodeBeam.UltimateAuth.Server.Services; + +internal sealed class UAuthSessionValidator : ISessionValidator +{ + private readonly ISessionStoreKernelFactory _storeFactory; + private readonly IUserClaimsProvider _claimsProvider; + private readonly UAuthServerOptions _options; + + public UAuthSessionValidator( + ISessionStoreKernelFactory storeFactory, + IUserClaimsProvider claimsProvider, + IOptions options) + { + _storeFactory = storeFactory; + _claimsProvider = claimsProvider; + _options = options.Value; + } + + // Validate runs before AuthFlowContext is set, do not call _authFlow here. + public async Task ValidateSessionAsync(SessionValidationContext context, CancellationToken ct = default) + { + var kernel = _storeFactory.Create(context.TenantId); + var session = await kernel.GetSessionAsync(context.SessionId); + + if (session is null) + return SessionValidationResult.Invalid(SessionState.NotFound, sessionId: context.SessionId); + + var state = session.GetState(context.Now, _options.Session.IdleTimeout); + if (state != SessionState.Active) + return SessionValidationResult.Invalid(state, sessionId: session.SessionId, chainId: session.ChainId); + + var chain = await kernel.GetChainAsync(session.ChainId); + if (chain is null || chain.IsRevoked) + return SessionValidationResult.Invalid(SessionState.Revoked, session.UserKey, session.SessionId, session.ChainId); + + var root = await kernel.GetSessionRootByUserAsync(session.UserKey); + if (root is null || root.IsRevoked) + return SessionValidationResult.Invalid(SessionState.Revoked, session.UserKey, session.SessionId, session.ChainId, root?.RootId); + + if (session.SecurityVersionAtCreation != root.SecurityVersion) + return SessionValidationResult.Invalid(SessionState.SecurityMismatch, session.UserKey, session.SessionId, session.ChainId, root.RootId); + + // TODO: Implement device id, AllowAndRebind behavior and check device mathing in blazor server circuit and external http calls. + // Currently this line has error on refresh flow. + //if (!session.Device.Matches(context.Device) && _options.Session.DeviceMismatchBehavior == DeviceMismatchBehavior.Reject) + // return SessionValidationResult.Invalid(SessionState.DeviceMismatch); + + var claims = await _claimsProvider.GetClaimsAsync(context.TenantId, session.UserKey, ct); + return SessionValidationResult.Active(context.TenantId, session.UserKey, session.SessionId, session.ChainId, root.RootId, claims, boundDeviceId: session.Device.DeviceId); + } +} From 756e62c4f543e6de4db4baa1759aeea4fae841e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Can=20Karag=C3=B6z?= Date: Tue, 3 Feb 2026 00:10:19 +0300 Subject: [PATCH 6/9] Added TenantKey Value Object --- .../Program.cs | 7 +- .../Authentication/UAuthState.cs | 10 +- .../Infrastructure/ISeedContributor.cs | 6 +- .../Abstractions/Issuers/ISessionIssuer.cs | 9 +- .../Principals/IUserClaimsProvider.cs | 3 +- .../Stores/IAccessTokenIdStore.cs | 10 +- .../Abstractions/Stores/IRefreshTokenStore.cs | 13 +- .../Stores/ISessionStoreKernelFactory.cs | 9 +- .../User/IUserRuntimeStateProvider.cs | 3 +- .../Contracts/Authority/AccessContext.cs | 7 +- .../Contracts/Authority/AuthContext.cs | 3 +- .../Contracts/Login/ExternalLoginRequest.cs | 6 +- .../Contracts/Login/LoginRequest.cs | 3 +- .../Contracts/Login/ReauthRequest.cs | 3 +- .../Contracts/Logout/LogoutAllRequest.cs | 3 +- .../Contracts/Logout/LogoutRequest.cs | 3 +- .../Contracts/Pkce/PkceLoginRequest.cs | 6 +- .../Refresh/RefreshTokenValidationContext.cs | 3 +- .../Contracts/Session/AuthStateSnapshot.cs | 6 +- .../Session/AuthenticatedSessionContext.cs | 3 +- .../Contracts/Session/SessionContext.cs | 9 +- .../Session/SessionRefreshRequest.cs | 6 +- .../Session/SessionRotationContext.cs | 3 +- .../Contracts/Session/SessionStoreContext.cs | 3 +- .../Session/SessionValidationContext.cs | 3 +- .../Session/SessionValidationResult.cs | 8 +- .../Token/RefreshTokenRotationExecution.cs | 3 +- .../Token/RefreshTokenValidationResult.cs | 11 +- .../Contracts/Token/TokenIssuanceContext.cs | 3 +- .../Contracts/Token/TokenIssueContext.cs | 3 +- .../Contracts/Token/TokenRefreshContext.cs | 6 +- .../Contracts/Token/TokenValidationResult.cs | 15 +- .../Domain/Hub/HubFlowArtifact.cs | 9 +- .../Domain/Session/UAuthSession.cs | 26 +- .../Domain/Session/UAuthSessionChain.cs | 24 +- .../Domain/Session/UAuthSessionRoot.cs | 22 +- .../Domain/Token/StoredRefreshToken.cs | 5 +- .../Domain/Token/UAuthJwtTokenDescriptor.cs | 6 +- .../DefaultRefreshTokenValidator.cs | 8 +- .../Infrastructure/NoOpAccessTokenIdStore.cs | 7 +- .../Infrastructure/SeedRunner.cs | 8 +- .../MultiTenancy/FixedTenantResolver.cs | 18 +- .../MultiTenancy/HeaderTenantResolver.cs | 2 +- .../MultiTenancy/TenantContext.cs | 6 +- .../MultiTenancy/TenantKey.cs | 102 +++++++ .../MultiTenancy/TenantResolutionResult.cs | 23 ++ .../MultiTenancy/TenantValidation.cs | 25 -- .../MultiTenancy/UAuthTenantContext.cs | 20 +- .../Options/UAuthMultiTenantOptions.cs | 30 +-- .../UAuthMultiTenantOptionsValidator.cs | 75 +----- .../Abstractions/ResolvedCredential.cs | 3 +- .../Auth/Context/AuthFlowContext.cs | 13 +- .../Auth/Context/AuthFlowContextFactory.cs | 20 +- .../Context/DefaultAccessContextFactory.cs | 93 ++++--- .../Auth/Context/IAccessContextFactory.cs | 11 +- .../UAuthAuthenticationHandler.cs | 5 +- .../Endpoints/DefaultLoginEndpointHandler.cs | 3 +- .../Endpoints/DefaultLogoutEndpointHandler.cs | 2 +- .../Endpoints/DefaultPkceEndpointHandler.cs | 6 +- .../DefaultValidateEndpointHandler.cs | 9 +- .../Extensions/AuthFlowContextExtensions.cs | 4 +- .../Extensions/HttpContextTenantExtensions.cs | 23 +- .../UAuthServerServiceCollectionExtensions.cs | 8 +- .../DefaultTransportCredentialResolver.cs | 251 +++++++++--------- .../AspNetCore/TransportCredential.cs | 15 +- .../DefaultFlowCredentialResolver.cs | 6 +- .../DefaultJwtTokenGenerator.cs | 6 +- .../Orchestrator/RevokeAllChainsCommand.cs | 2 +- .../Orchestrator/RevokeAllSessionsCommand.cs | 2 +- .../Orchestrator/RevokeChainCommand.cs | 2 +- .../Orchestrator/RevokeRootCommand.cs | 2 +- .../Orchestrator/RevokeSessionCommand.cs | 2 +- .../Pkce/PkceAuthorizationValidator.cs | 2 +- .../Pkce/PkceContextSnapshot.cs | 9 +- .../Refresh/DefaultSessionTouchService.cs | 2 +- .../Infrastructure/User/UAuthUserAccessor.cs | 8 +- .../Issuers/UAuthSessionIssuer.cs | 29 +- .../Issuers/UAuthTokenIssuer.cs | 8 +- .../Login/DefaultLoginOrchestrator.cs | 14 +- .../Login/LoginDecisionContext.cs | 3 +- .../SessionResolutionMiddleware.cs | 4 +- .../Middlewares/TenantMiddleware.cs | 59 ++-- .../MultiTenancy/ITenantResolver.cs | 10 +- .../MultiTenancy/UAuthTenantContextFactory.cs | 29 +- .../MultiTenancy/UAuthTenantResolver.cs | 84 ++---- .../Options/UAuthServerOptionsValidator.cs | 19 -- .../Services/DefaultRefreshFlowService.cs | 6 +- .../Services/RefreshTokenRotationService.cs | 21 +- .../Services/UAuthJwtValidator.cs | 122 +++++---- .../Services/UAuthSessionQueryService.cs | 2 +- .../Services/UAuthSessionValidator.cs | 6 +- .../InMemoryAuthorizationSeedContributor.cs | 5 +- .../Stores/InMemoryUserRoleStore.cs | 15 +- .../DefaultRolePermissionResolver.cs | 3 +- .../DefaultUserPermissionStore.cs | 7 +- .../Services/DefaultUserRoleService.cs | 6 +- .../Abstractions/IRolePermissionResolver.cs | 10 +- .../Abstractions/IUserPermissionStore.cs | 3 +- .../Abstractions/IUserRoleStore.cs | 14 +- .../DefaultAuthorizationClaimsProvider.cs | 7 +- .../InMemoryCredentialSeedContributor.cs | 13 +- .../InMemoryCredentialStore.cs | 57 ++-- .../PasswordUserLifecycleIntegration.cs | 9 +- .../IUserCredentialsInternalService.cs | 10 +- .../Services/DefaultUserCredentialsService.cs | 25 +- .../Abstractions/ICredentialSecretStore.cs | 5 +- .../Abstractions/ICredentialStore.cs | 21 +- .../Policies/RequireActiveUserPolicy.cs | 2 +- .../SessionChainProjection.cs | 3 +- .../EntityProjections/SessionProjection.cs | 3 +- .../SessionRootProjection.cs | 3 +- .../Stores/EfCoreSessionStoreKernelFactory.cs | 4 +- .../InMemorySessionStoreKernelFactory.cs | 10 +- .../EfCoreTokenStore.cs | 29 +- .../Projections/RefreshTokenProjection.cs | 3 +- .../Projections/RevokedIdTokenProjection.cs | 6 +- .../InMemoryRefreshTokenStore.cs | 25 +- .../Requests/RegisterUserRequest.cs | 6 +- .../InMemoryUserSecurityStateProvider.cs | 15 +- .../InMemoryUserSeedContributor.cs | 17 +- .../Stores/InMemoryUserIdentifierStore.cs | 49 ++-- .../Stores/InMemoryUserLifecycleStore.cs | 31 +-- .../Stores/InMemoryUserProfileStore.cs | 27 +- .../Domain/UserIdentifier.cs | 3 +- .../Domain/UserLifecycle.cs | 3 +- .../Domain/UserProfile.cs | 3 +- .../Services/UserApplicationService.cs | 55 ++-- .../Stores/IUserIdentifierStore.cs | 21 +- .../Stores/IUserLifecycleStore.cs | 15 +- .../Stores/IUserProfileStore.cs | 13 +- .../Stores/UserRuntimeStore.cs | 5 +- .../Abstractions/IUserLifecycleIntegration.cs | 5 +- .../IUserSecurityStateProvider.cs | 11 +- 133 files changed, 1086 insertions(+), 1021 deletions(-) create mode 100644 src/CodeBeam.UltimateAuth.Core/MultiTenancy/TenantKey.cs create mode 100644 src/CodeBeam.UltimateAuth.Core/MultiTenancy/TenantResolutionResult.cs delete mode 100644 src/CodeBeam.UltimateAuth.Core/MultiTenancy/TenantValidation.cs diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Program.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Program.cs index 1d022200..6791f6d6 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Program.cs +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Program.cs @@ -6,6 +6,7 @@ using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Core.Extensions; using CodeBeam.UltimateAuth.Core.Infrastructure; +using CodeBeam.UltimateAuth.Core.MultiTenancy; using CodeBeam.UltimateAuth.Credentials; using CodeBeam.UltimateAuth.Credentials.InMemory.Extensions; using CodeBeam.UltimateAuth.Credentials.Reference; @@ -112,13 +113,9 @@ app.MapOpenApi(); app.MapScalarApiReference(); using var scope = app.Services.CreateScope(); - //scope.ServiceProvider.GetRequiredService(); - //scope.ServiceProvider.GetRequiredService(); - //scope.ServiceProvider.GetRequiredService(); - //scope.ServiceProvider.GetRequiredService>(); var seedRunner = scope.ServiceProvider.GetRequiredService(); - await seedRunner.RunAsync(tenantId: null); + await seedRunner.RunAsync(null); } app.UseHttpsRedirection(); diff --git a/src/CodeBeam.UltimateAuth.Client/Authentication/UAuthState.cs b/src/CodeBeam.UltimateAuth.Client/Authentication/UAuthState.cs index 10ef9f57..e90c4909 100644 --- a/src/CodeBeam.UltimateAuth.Client/Authentication/UAuthState.cs +++ b/src/CodeBeam.UltimateAuth.Client/Authentication/UAuthState.cs @@ -1,5 +1,6 @@ using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; using System.Security.Claims; namespace CodeBeam.UltimateAuth.Client.Authentication @@ -18,7 +19,7 @@ private UAuthState() { } public UserKey? UserKey { get; private set; } - public string? TenantId { get; private set; } + public TenantKey Tenant { get; private set; } /// /// When this authentication snapshot was created. @@ -44,14 +45,14 @@ private UAuthState() { } internal void ApplySnapshot(AuthStateSnapshot snapshot, DateTimeOffset validatedAt) { - if (string.IsNullOrWhiteSpace(snapshot.UserId)) + if (string.IsNullOrWhiteSpace(snapshot.UserKey)) { Clear(); return; } - UserKey = CodeBeam.UltimateAuth.Core.Domain.UserKey.FromString(snapshot.UserId); - TenantId = snapshot.TenantId; + UserKey = CodeBeam.UltimateAuth.Core.Domain.UserKey.FromString(snapshot.UserKey); + Tenant = snapshot.Tenant; Claims = snapshot.Claims; IsAuthenticated = true; @@ -89,7 +90,6 @@ internal void Clear() Claims = ClaimsSnapshot.Empty; UserKey = null; - TenantId = null; IsAuthenticated = false; AuthenticatedAt = null; diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Infrastructure/ISeedContributor.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Infrastructure/ISeedContributor.cs index 148ddc34..f2fd00f8 100644 --- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Infrastructure/ISeedContributor.cs +++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Infrastructure/ISeedContributor.cs @@ -1,4 +1,6 @@ -namespace CodeBeam.UltimateAuth.Core.Abstractions; +using CodeBeam.UltimateAuth.Core.MultiTenancy; + +namespace CodeBeam.UltimateAuth.Core.Abstractions; /// /// Contributes seed data for a specific domain (Users, Credentials, Authorization, etc). @@ -12,5 +14,5 @@ public interface ISeedContributor /// int Order { get; } - Task SeedAsync(string? tenantId, CancellationToken ct = default); + Task SeedAsync(TenantKey tenant, CancellationToken ct = default); } diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Issuers/ISessionIssuer.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Issuers/ISessionIssuer.cs index b5d7b3b2..3e8bca20 100644 --- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Issuers/ISessionIssuer.cs +++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Issuers/ISessionIssuer.cs @@ -1,5 +1,6 @@ using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; namespace CodeBeam.UltimateAuth.Core.Abstractions; @@ -9,11 +10,11 @@ public interface ISessionIssuer Task RotateSessionAsync(SessionRotationContext context, CancellationToken cancellationToken = default); - Task RevokeSessionAsync(string? tenantId, AuthSessionId sessionId, DateTimeOffset at, CancellationToken cancellationToken = default); + Task RevokeSessionAsync(TenantKey tenant, AuthSessionId sessionId, DateTimeOffset at, CancellationToken cancellationToken = default); - Task RevokeChainAsync(string? tenantId, SessionChainId chainId, DateTimeOffset at, CancellationToken cancellationToken = default); + Task RevokeChainAsync(TenantKey tenant, SessionChainId chainId, DateTimeOffset at, CancellationToken cancellationToken = default); - Task RevokeAllChainsAsync(string? tenantId, UserKey userKey, SessionChainId? exceptChainId, DateTimeOffset at, CancellationToken ct = default); + Task RevokeAllChainsAsync(TenantKey tenant, UserKey userKey, SessionChainId? exceptChainId, DateTimeOffset at, CancellationToken ct = default); - Task RevokeRootAsync(string? tenantId, UserKey userKey, DateTimeOffset at,CancellationToken ct = default); + Task RevokeRootAsync(TenantKey tenant, UserKey userKey, DateTimeOffset at,CancellationToken ct = default); } diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Principals/IUserClaimsProvider.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Principals/IUserClaimsProvider.cs index 3be15c91..b51990e9 100644 --- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Principals/IUserClaimsProvider.cs +++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Principals/IUserClaimsProvider.cs @@ -1,8 +1,9 @@ using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; namespace CodeBeam.UltimateAuth.Core; public interface IUserClaimsProvider { - Task GetClaimsAsync(string? tenantId, UserKey userKey, CancellationToken ct = default); + Task GetClaimsAsync(TenantKey tenant, UserKey userKey, CancellationToken ct = default); } diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/IAccessTokenIdStore.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/IAccessTokenIdStore.cs index 94d2933c..7006f2d1 100644 --- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/IAccessTokenIdStore.cs +++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/IAccessTokenIdStore.cs @@ -1,4 +1,6 @@ -namespace CodeBeam.UltimateAuth.Core.Abstractions; +using CodeBeam.UltimateAuth.Core.MultiTenancy; + +namespace CodeBeam.UltimateAuth.Core.Abstractions; /// /// Optional persistence for access token identifiers (jti). @@ -6,9 +8,9 @@ /// public interface IAccessTokenIdStore { - Task StoreAsync(string? tenantId, string jti, DateTimeOffset expiresAt, CancellationToken ct = default); + Task StoreAsync(TenantKey tenant, string jti, DateTimeOffset expiresAt, CancellationToken ct = default); - Task IsRevokedAsync(string? tenantId, string jti, CancellationToken ct = default); + Task IsRevokedAsync(TenantKey tenant, string jti, CancellationToken ct = default); - Task RevokeAsync(string? tenantId, string jti, DateTimeOffset revokedAt, CancellationToken ct = default); + Task RevokeAsync(TenantKey tenant, string jti, DateTimeOffset revokedAt, CancellationToken ct = default); } diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/IRefreshTokenStore.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/IRefreshTokenStore.cs index eb6e52c3..cf43b46a 100644 --- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/IRefreshTokenStore.cs +++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/IRefreshTokenStore.cs @@ -1,4 +1,5 @@ using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; namespace CodeBeam.UltimateAuth.Core.Abstractions; @@ -8,15 +9,15 @@ namespace CodeBeam.UltimateAuth.Core.Abstractions; /// public interface IRefreshTokenStore { - Task StoreAsync(string? tenantId, StoredRefreshToken token, CancellationToken ct = default); + Task StoreAsync(TenantKey tenant, StoredRefreshToken token, CancellationToken ct = default); - Task FindByHashAsync(string? tenantId, string tokenHash, CancellationToken ct = default); + Task FindByHashAsync(TenantKey tenant, string tokenHash, CancellationToken ct = default); - Task RevokeAsync(string? tenantId, string tokenHash, DateTimeOffset revokedAt, string? replacedByTokenHash = null, CancellationToken ct = default); + Task RevokeAsync(TenantKey tenant, string tokenHash, DateTimeOffset revokedAt, string? replacedByTokenHash = null, CancellationToken ct = default); - Task RevokeBySessionAsync(string? tenantId, AuthSessionId sessionId, DateTimeOffset revokedAt, CancellationToken ct = default); + Task RevokeBySessionAsync(TenantKey tenant, AuthSessionId sessionId, DateTimeOffset revokedAt, CancellationToken ct = default); - Task RevokeByChainAsync(string? tenantId, SessionChainId chainId, DateTimeOffset revokedAt, CancellationToken ct = default); + Task RevokeByChainAsync(TenantKey tenant, SessionChainId chainId, DateTimeOffset revokedAt, CancellationToken ct = default); - Task RevokeAllForUserAsync(string? tenantId, UserKey userKey, DateTimeOffset revokedAt, CancellationToken ct = default); + Task RevokeAllForUserAsync(TenantKey tenant, UserKey userKey, DateTimeOffset revokedAt, CancellationToken ct = default); } diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ISessionStoreKernelFactory.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ISessionStoreKernelFactory.cs index 5bf3a8e9..936731e2 100644 --- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ISessionStoreKernelFactory.cs +++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ISessionStoreKernelFactory.cs @@ -1,4 +1,6 @@ -namespace CodeBeam.UltimateAuth.Core.Abstractions; +using CodeBeam.UltimateAuth.Core.MultiTenancy; + +namespace CodeBeam.UltimateAuth.Core.Abstractions; /// /// Provides a factory abstraction for creating tenant-scoped session store @@ -10,11 +12,8 @@ public interface ISessionStoreKernelFactory /// /// Creates and returns a session store instance for the specified user ID type within the given tenant context. /// - /// - /// The tenant identifier for multi-tenant environments, or null for single-tenant mode. - /// /// /// An implementation able to perform session persistence operations. /// - ISessionStoreKernel Create(string? tenantId); + ISessionStoreKernel Create(TenantKey tenant); } diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/User/IUserRuntimeStateProvider.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/User/IUserRuntimeStateProvider.cs index ae7defd3..1d335fc3 100644 --- a/src/CodeBeam.UltimateAuth.Core/Abstractions/User/IUserRuntimeStateProvider.cs +++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/User/IUserRuntimeStateProvider.cs @@ -1,4 +1,5 @@ using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; namespace CodeBeam.UltimateAuth.Core.Abstractions; @@ -8,5 +9,5 @@ namespace CodeBeam.UltimateAuth.Core.Abstractions; /// public interface IUserRuntimeStateProvider { - Task GetAsync(string? tenantId, UserKey userKey, CancellationToken ct = default); + Task GetAsync(TenantKey tenant, UserKey userKey, CancellationToken ct = default); } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Authority/AccessContext.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Authority/AccessContext.cs index 1d79bbe4..ca4b8406 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Authority/AccessContext.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Authority/AccessContext.cs @@ -1,4 +1,5 @@ using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; using System.Collections; namespace CodeBeam.UltimateAuth.Core.Contracts; @@ -7,19 +8,19 @@ public sealed class AccessContext { // Actor public UserKey? ActorUserKey { get; init; } - public string? ActorTenantId { get; init; } + public TenantKey ActorTenant { get; init; } public bool IsAuthenticated { get; init; } public bool IsSystemActor { get; init; } // Target public string? Resource { get; init; } public string? ResourceId { get; init; } - public string? ResourceTenantId { get; init; } + public TenantKey ResourceTenant { get; init; } public string Action { get; init; } = default!; public IReadOnlyDictionary Attributes { get; init; } = EmptyAttributes.Instance; - public bool IsCrossTenant => ActorTenantId != null && ResourceTenantId != null && !string.Equals(ActorTenantId, ResourceTenantId, StringComparison.Ordinal); + public bool IsCrossTenant => !string.Equals(ActorTenant, ResourceTenant, StringComparison.Ordinal); public bool IsSelfAction => ActorUserKey != null && ResourceId != null && string.Equals(ActorUserKey.Value, ResourceId, StringComparison.Ordinal); public bool HasActor => ActorUserKey != null; public bool HasTarget => ResourceId != null; diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Authority/AuthContext.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Authority/AuthContext.cs index 6f9a6755..65eeadaf 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Authority/AuthContext.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Authority/AuthContext.cs @@ -1,10 +1,11 @@ using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; namespace CodeBeam.UltimateAuth.Core.Contracts; public sealed record AuthContext { - public string? TenantId { get; init; } + public TenantKey Tenant { get; init; } public AuthOperation Operation { get; init; } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Login/ExternalLoginRequest.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Login/ExternalLoginRequest.cs index 10c1c4c6..9a9555bd 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Login/ExternalLoginRequest.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Login/ExternalLoginRequest.cs @@ -1,8 +1,10 @@ -namespace CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.MultiTenancy; + +namespace CodeBeam.UltimateAuth.Core.Contracts; public sealed record ExternalLoginRequest { - public string? TenantId { get; init; } + public TenantKey Tenant { get; init; } public string Provider { get; init; } = default!; public string ExternalToken { get; init; } = default!; public string? DeviceId { get; init; } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Login/LoginRequest.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Login/LoginRequest.cs index 0c31e6c7..23769bec 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Login/LoginRequest.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Login/LoginRequest.cs @@ -1,10 +1,11 @@ using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; namespace CodeBeam.UltimateAuth.Core.Contracts; public sealed record LoginRequest { - public string? TenantId { get; init; } + public TenantKey Tenant { get; init; } public string Identifier { get; init; } = default!; public string Secret { get; init; } = default!; public DateTimeOffset? At { get; init; } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Login/ReauthRequest.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Login/ReauthRequest.cs index e1736304..5717252f 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Login/ReauthRequest.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Login/ReauthRequest.cs @@ -1,10 +1,11 @@ using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; namespace CodeBeam.UltimateAuth.Core.Contracts; public sealed record ReauthRequest { - public string? TenantId { get; init; } + public TenantKey Tenant { get; init; } public AuthSessionId SessionId { get; init; } public string Secret { get; init; } = default!; } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Logout/LogoutAllRequest.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Logout/LogoutAllRequest.cs index 08fd5301..5cc76251 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Logout/LogoutAllRequest.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Logout/LogoutAllRequest.cs @@ -1,10 +1,11 @@ using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; namespace CodeBeam.UltimateAuth.Core.Contracts; public sealed class LogoutAllRequest { - public string? TenantId { get; init; } + public TenantKey Tenant { get; init; } /// /// The current session initiating the logout-all operation. diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Logout/LogoutRequest.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Logout/LogoutRequest.cs index 7aebff41..050a9b9d 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Logout/LogoutRequest.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Logout/LogoutRequest.cs @@ -1,10 +1,11 @@ using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; namespace CodeBeam.UltimateAuth.Core.Contracts; public sealed record LogoutRequest { - public string? TenantId { get; init; } + public TenantKey Tenant { get; init; } public AuthSessionId SessionId { get; init; } public DateTimeOffset? At { get; init; } } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Pkce/PkceLoginRequest.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Pkce/PkceLoginRequest.cs index 6c0a2f89..9d446030 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Pkce/PkceLoginRequest.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Pkce/PkceLoginRequest.cs @@ -1,4 +1,6 @@ -namespace CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.MultiTenancy; + +namespace CodeBeam.UltimateAuth.Core.Contracts; public sealed class PkceLoginRequest { @@ -9,5 +11,5 @@ public sealed class PkceLoginRequest public string Identifier { get; init; } = default!; public string Secret { get; init; } = default!; - public string? TenantId { get; init; } + public TenantKey Tenant { get; init; } } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Refresh/RefreshTokenValidationContext.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Refresh/RefreshTokenValidationContext.cs index 7cd62cc2..01d25a1e 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Refresh/RefreshTokenValidationContext.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Refresh/RefreshTokenValidationContext.cs @@ -1,10 +1,11 @@ using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; namespace CodeBeam.UltimateAuth.Core.Contracts; public sealed record RefreshTokenValidationContext { - public string? TenantId { get; init; } + public TenantKey Tenant { get; init; } public string RefreshToken { get; init; } = default!; public DateTimeOffset Now { get; init; } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/AuthStateSnapshot.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/AuthStateSnapshot.cs index c56c36b3..c5bff8da 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/AuthStateSnapshot.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/AuthStateSnapshot.cs @@ -1,12 +1,12 @@ using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; namespace CodeBeam.UltimateAuth.Core.Contracts; public sealed record AuthStateSnapshot { - // It's not UserId type - public string? UserId { get; init; } - public string? TenantId { get; init; } + public UserKey UserKey { get; init; } + public TenantKey Tenant { get; init; } public ClaimsSnapshot Claims { get; init; } = ClaimsSnapshot.Empty; diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/AuthenticatedSessionContext.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/AuthenticatedSessionContext.cs index 2d39adf7..4a83eb93 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/AuthenticatedSessionContext.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/AuthenticatedSessionContext.cs @@ -1,4 +1,5 @@ using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; namespace CodeBeam.UltimateAuth.Core.Contracts; @@ -8,7 +9,7 @@ namespace CodeBeam.UltimateAuth.Core.Contracts; /// public sealed class AuthenticatedSessionContext { - public string? TenantId { get; init; } + public TenantKey Tenant { get; init; } public required UserKey UserKey { get; init; } public required DeviceContext Device { get; init; } public DateTimeOffset Now { get; init; } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionContext.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionContext.cs index d56ac757..0426b18a 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionContext.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionContext.cs @@ -1,4 +1,5 @@ using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; namespace CodeBeam.UltimateAuth.Core.Contracts; @@ -10,17 +11,17 @@ namespace CodeBeam.UltimateAuth.Core.Contracts; public sealed class SessionContext { public AuthSessionId? SessionId { get; } - public string? TenantId { get; } + public TenantKey? Tenant { get; } public bool IsAnonymous => SessionId is null; - private SessionContext(AuthSessionId? sessionId, string? tenantId) + private SessionContext(AuthSessionId? sessionId, TenantKey? tenant) { SessionId = sessionId; - TenantId = tenantId; + Tenant = tenant; } public static SessionContext Anonymous() => new(null, null); - public static SessionContext FromSessionId(AuthSessionId sessionId, string? tenantId) => new(sessionId, tenantId); + public static SessionContext FromSessionId(AuthSessionId sessionId, TenantKey tenant) => new(sessionId, tenant); } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionRefreshRequest.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionRefreshRequest.cs index 9c88acd4..d15f1cfc 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionRefreshRequest.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionRefreshRequest.cs @@ -1,7 +1,9 @@ -namespace CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.MultiTenancy; + +namespace CodeBeam.UltimateAuth.Core.Contracts; public sealed record SessionRefreshRequest { - public string? TenantId { get; init; } + public TenantKey Tenant { get; init; } public string RefreshToken { get; init; } = default!; } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionRotationContext.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionRotationContext.cs index 5e96052a..223176fe 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionRotationContext.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionRotationContext.cs @@ -1,10 +1,11 @@ using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; namespace CodeBeam.UltimateAuth.Core.Contracts; public sealed record SessionRotationContext { - public string? TenantId { get; init; } + public TenantKey Tenant { get; init; } public AuthSessionId CurrentSessionId { get; init; } public UserKey UserKey { get; init; } public DateTimeOffset Now { get; init; } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionStoreContext.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionStoreContext.cs index d3646e63..3bcda73b 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionStoreContext.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionStoreContext.cs @@ -1,4 +1,5 @@ using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; namespace CodeBeam.UltimateAuth.Core.Contracts; @@ -16,7 +17,7 @@ public sealed class SessionStoreContext /// /// The tenant identifier, if multi-tenancy is enabled. /// - public string? TenantId { get; init; } + public TenantKey Tenant { get; init; } /// /// Optional chain identifier. diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionValidationContext.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionValidationContext.cs index afa90f4b..6a493e06 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionValidationContext.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionValidationContext.cs @@ -1,10 +1,11 @@ using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; namespace CodeBeam.UltimateAuth.Core.Contracts; public sealed record SessionValidationContext { - public string? TenantId { get; init; } + public TenantKey Tenant { get; init; } public AuthSessionId SessionId { get; init; } public DateTimeOffset Now { get; init; } public required DeviceContext Device { get; init; } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionValidationResult.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionValidationResult.cs index 59dcb029..45e4e04b 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionValidationResult.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionValidationResult.cs @@ -1,10 +1,11 @@ using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; namespace CodeBeam.UltimateAuth.Core.Contracts; public sealed class SessionValidationResult { - public string? TenantId { get; init; } + public TenantKey Tenant { get; init; } public required SessionState State { get; init; } @@ -25,7 +26,7 @@ public sealed class SessionValidationResult private SessionValidationResult() { } public static SessionValidationResult Active( - string? tenantId, + TenantKey tenant, UserKey? userId, AuthSessionId sessionId, SessionChainId chainId, @@ -34,7 +35,7 @@ public static SessionValidationResult Active( DeviceId? boundDeviceId = null) => new() { - TenantId = tenantId, + Tenant = tenant, State = SessionState.Active, UserKey = userId, SessionId = sessionId, @@ -53,7 +54,6 @@ public static SessionValidationResult Invalid( DeviceId? boundDeviceId = null) => new() { - TenantId = null, State = state, UserKey = userId, SessionId = sessionId, diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/RefreshTokenRotationExecution.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/RefreshTokenRotationExecution.cs index 69aba273..76c63775 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/RefreshTokenRotationExecution.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/RefreshTokenRotationExecution.cs @@ -1,4 +1,5 @@ using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; namespace CodeBeam.UltimateAuth.Core.Contracts; @@ -10,5 +11,5 @@ public sealed record RefreshTokenRotationExecution public UserKey? UserKey { get; init; } public AuthSessionId? SessionId { get; init; } public SessionChainId? ChainId { get; init; } - public string? TenantId { get; init; } + public TenantKey Tenant { get; init; } } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/RefreshTokenValidationResult.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/RefreshTokenValidationResult.cs index 3fb78779..8ab8977b 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/RefreshTokenValidationResult.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/RefreshTokenValidationResult.cs @@ -1,4 +1,5 @@ using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; namespace CodeBeam.UltimateAuth.Core.Contracts; @@ -9,7 +10,7 @@ public sealed record RefreshTokenValidationResult public string? TokenHash { get; init; } - public string? TenantId { get; init; } + public TenantKey Tenant { get; init; } public UserKey? UserKey { get; init; } public AuthSessionId? SessionId { get; init; } public SessionChainId? ChainId { get; init; } @@ -27,7 +28,7 @@ public static RefreshTokenValidationResult Invalid() }; public static RefreshTokenValidationResult ReuseDetected( - string? tenantId = null, + TenantKey tenant, AuthSessionId? sessionId = null, string? tokenHash = null, SessionChainId? chainId = null, @@ -36,7 +37,7 @@ public static RefreshTokenValidationResult ReuseDetected( { IsValid = false, IsReuseDetected = true, - TenantId = tenantId, + Tenant = tenant, SessionId = sessionId, TokenHash = tokenHash, ChainId = chainId, @@ -44,7 +45,7 @@ public static RefreshTokenValidationResult ReuseDetected( }; public static RefreshTokenValidationResult Valid( - string? tenantId, + TenantKey tenant, UserKey userKey, AuthSessionId sessionId, string? tokenHash, @@ -53,7 +54,7 @@ public static RefreshTokenValidationResult Valid( { IsValid = true, IsReuseDetected = false, - TenantId = tenantId, + Tenant = tenant, UserKey = userKey, SessionId = sessionId, ChainId = chainId, diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenIssuanceContext.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenIssuanceContext.cs index 080fb35e..fb95004f 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenIssuanceContext.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenIssuanceContext.cs @@ -1,11 +1,12 @@ using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; namespace CodeBeam.UltimateAuth.Core.Contracts; public sealed record TokenIssuanceContext { public required UserKey UserKey { get; init; } - public string? TenantId { get; init; } + public TenantKey Tenant { get; init; } public IReadOnlyDictionary Claims { get; set; } = new Dictionary(); public AuthSessionId? SessionId { get; init; } public SessionChainId? ChainId { get; init; } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenIssueContext.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenIssueContext.cs index 37a20cbf..d440a7e6 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenIssueContext.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenIssueContext.cs @@ -1,10 +1,11 @@ using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; namespace CodeBeam.UltimateAuth.Core.Contracts; public sealed record TokenIssueContext { - public string? TenantId { get; init; } + public TenantKey Tenant { get; init; } public UAuthSession Session { get; init; } = default!; public DateTimeOffset At { get; init; } } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenRefreshContext.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenRefreshContext.cs index ffc4e589..2796946b 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenRefreshContext.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenRefreshContext.cs @@ -1,8 +1,10 @@ -namespace CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.MultiTenancy; + +namespace CodeBeam.UltimateAuth.Core.Contracts; public sealed record TokenRefreshContext { - public string? TenantId { get; init; } + public TenantKey Tenant { get; init; } public string RefreshToken { get; init; } = default!; } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenValidationResult.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenValidationResult.cs index 011a8d00..f0247ddf 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenValidationResult.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenValidationResult.cs @@ -1,4 +1,5 @@ using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; using System.Security.Claims; namespace CodeBeam.UltimateAuth.Core.Contracts; @@ -7,7 +8,7 @@ public sealed record TokenValidationResult { public bool IsValid { get; init; } public TokenType Type { get; init; } - public string? TenantId { get; init; } + public TenantKey? Tenant { get; init; } public TUserId? UserId { get; init; } public AuthSessionId? SessionId { get; init; } public IReadOnlyCollection Claims { get; init; } = Array.Empty(); @@ -17,7 +18,7 @@ public sealed record TokenValidationResult private TokenValidationResult( bool isValid, TokenType type, - string? tenantId, + TenantKey? tenant, TUserId? userId, AuthSessionId? sessionId, IReadOnlyCollection? claims, @@ -26,7 +27,7 @@ private TokenValidationResult( ) { IsValid = isValid; - TenantId = tenantId; + Tenant = tenant; UserId = userId; SessionId = sessionId; Claims = claims ?? Array.Empty(); @@ -36,7 +37,7 @@ private TokenValidationResult( public static TokenValidationResult Valid( TokenType type, - string? tenantId, + TenantKey tenant, TUserId userId, AuthSessionId? sessionId, IReadOnlyCollection claims, @@ -44,7 +45,7 @@ public static TokenValidationResult Valid( => new( isValid: true, type, - tenantId, + tenant, userId, sessionId, claims, @@ -55,8 +56,8 @@ public static TokenValidationResult Valid( public static TokenValidationResult Invalid(TokenType type, TokenInvalidReason reason) => new( isValid: false, - type, - tenantId: null, + type: type, + tenant: null, userId: default, sessionId: null, claims: null, diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Hub/HubFlowArtifact.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Hub/HubFlowArtifact.cs index 98704a10..3d6c99a0 100644 --- a/src/CodeBeam.UltimateAuth.Core/Domain/Hub/HubFlowArtifact.cs +++ b/src/CodeBeam.UltimateAuth.Core/Domain/Hub/HubFlowArtifact.cs @@ -1,4 +1,5 @@ -using CodeBeam.UltimateAuth.Core.Options; +using CodeBeam.UltimateAuth.Core.MultiTenancy; +using CodeBeam.UltimateAuth.Core.Options; namespace CodeBeam.UltimateAuth.Core.Domain; @@ -8,7 +9,7 @@ public sealed class HubFlowArtifact : AuthArtifact public HubFlowType FlowType { get; } public UAuthClientProfile ClientProfile { get; } - public string? TenantId { get; } + public TenantKey Tenant { get; } public string? ReturnUrl { get; } public HubFlowPayload Payload { get; } @@ -17,7 +18,7 @@ public HubFlowArtifact( HubSessionId hubSessionId, HubFlowType flowType, UAuthClientProfile clientProfile, - string? tenantId, + TenantKey tenant, string? returnUrl, HubFlowPayload payload, DateTimeOffset expiresAt) @@ -26,7 +27,7 @@ public HubFlowArtifact( HubSessionId = hubSessionId; FlowType = flowType; ClientProfile = clientProfile; - TenantId = tenantId; + Tenant = tenant; ReturnUrl = returnUrl; Payload = payload; } diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSession.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSession.cs index e29878a9..81b08720 100644 --- a/src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSession.cs +++ b/src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSession.cs @@ -1,9 +1,11 @@ -namespace CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; + +namespace CodeBeam.UltimateAuth.Core.Domain; public sealed class UAuthSession { public AuthSessionId SessionId { get; } - public string? TenantId { get; } + public TenantKey Tenant { get; } public UserKey UserKey { get; } public SessionChainId ChainId { get; } public DateTimeOffset CreatedAt { get; } @@ -18,7 +20,7 @@ public sealed class UAuthSession private UAuthSession( AuthSessionId sessionId, - string? tenantId, + TenantKey tenant, UserKey userKey, SessionChainId chainId, DateTimeOffset createdAt, @@ -32,7 +34,7 @@ private UAuthSession( SessionMetadata metadata) { SessionId = sessionId; - TenantId = tenantId; + Tenant = tenant; UserKey = userKey; ChainId = chainId; CreatedAt = createdAt; @@ -48,7 +50,7 @@ private UAuthSession( public static UAuthSession Create( AuthSessionId sessionId, - string? tenantId, + TenantKey tenant, UserKey userKey, SessionChainId chainId, DateTimeOffset now, @@ -59,7 +61,7 @@ public static UAuthSession Create( { return new( sessionId, - tenantId, + tenant, userKey, chainId, createdAt: now, @@ -81,7 +83,7 @@ public UAuthSession WithSecurityVersion(long version) return new UAuthSession( SessionId, - TenantId, + Tenant, UserKey, ChainId, CreatedAt, @@ -100,7 +102,7 @@ public UAuthSession Touch(DateTimeOffset at) { return new UAuthSession( SessionId, - TenantId, + Tenant, UserKey, ChainId, CreatedAt, @@ -121,7 +123,7 @@ public UAuthSession Revoke(DateTimeOffset at) return new UAuthSession( SessionId, - TenantId, + Tenant, UserKey, ChainId, CreatedAt, @@ -138,7 +140,7 @@ public UAuthSession Revoke(DateTimeOffset at) internal static UAuthSession FromProjection( AuthSessionId sessionId, - string? tenantId, + TenantKey tenant, UserKey userKey, SessionChainId chainId, DateTimeOffset createdAt, @@ -153,7 +155,7 @@ internal static UAuthSession FromProjection( { return new UAuthSession( sessionId, - tenantId, + tenant, userKey, chainId, createdAt, @@ -189,7 +191,7 @@ public UAuthSession WithChain(SessionChainId chainId) return new UAuthSession( sessionId: SessionId, - tenantId: TenantId, + tenant: Tenant, userKey: UserKey, chainId: chainId, createdAt: CreatedAt, diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSessionChain.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSessionChain.cs index 92a61c08..dfecbcb3 100644 --- a/src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSessionChain.cs +++ b/src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSessionChain.cs @@ -1,10 +1,12 @@ -namespace CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; + +namespace CodeBeam.UltimateAuth.Core.Domain; public sealed class UAuthSessionChain { public SessionChainId ChainId { get; } public SessionRootId RootId { get; } - public string? TenantId { get; } + public TenantKey Tenant { get; } public UserKey UserKey { get; } public int RotationCount { get; } public long SecurityVersionAtCreation { get; } @@ -16,7 +18,7 @@ public sealed class UAuthSessionChain private UAuthSessionChain( SessionChainId chainId, SessionRootId rootId, - string? tenantId, + TenantKey tenant, UserKey userKey, int rotationCount, long securityVersionAtCreation, @@ -27,7 +29,7 @@ private UAuthSessionChain( { ChainId = chainId; RootId = rootId; - TenantId = tenantId; + Tenant = tenant; UserKey = userKey; RotationCount = rotationCount; SecurityVersionAtCreation = securityVersionAtCreation; @@ -40,7 +42,7 @@ private UAuthSessionChain( public static UAuthSessionChain Create( SessionChainId chainId, SessionRootId rootId, - string? tenantId, + TenantKey tenant, UserKey userKey, long securityVersion, ClaimsSnapshot claimsSnapshot) @@ -48,7 +50,7 @@ public static UAuthSessionChain Create( return new UAuthSessionChain( chainId, rootId, - tenantId, + tenant, userKey, rotationCount: 0, securityVersionAtCreation: securityVersion, @@ -67,7 +69,7 @@ public UAuthSessionChain AttachSession(AuthSessionId sessionId) return new UAuthSessionChain( ChainId, RootId, - TenantId, + Tenant, UserKey, RotationCount, // Unchanged on first attach SecurityVersionAtCreation, @@ -86,7 +88,7 @@ public UAuthSessionChain RotateSession(AuthSessionId sessionId) return new UAuthSessionChain( ChainId, RootId, - TenantId, + Tenant, UserKey, RotationCount + 1, SecurityVersionAtCreation, @@ -105,7 +107,7 @@ public UAuthSessionChain Revoke(DateTimeOffset at) return new UAuthSessionChain( ChainId, RootId, - TenantId, + Tenant, UserKey, RotationCount, SecurityVersionAtCreation, @@ -119,7 +121,7 @@ public UAuthSessionChain Revoke(DateTimeOffset at) internal static UAuthSessionChain FromProjection( SessionChainId chainId, SessionRootId rootId, - string? tenantId, + TenantKey tenant, UserKey userKey, int rotationCount, long securityVersionAtCreation, @@ -131,7 +133,7 @@ internal static UAuthSessionChain FromProjection( return new UAuthSessionChain( chainId, rootId, - tenantId, + tenant, userKey, rotationCount, securityVersionAtCreation, diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSessionRoot.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSessionRoot.cs index 9efd9c99..3eb85942 100644 --- a/src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSessionRoot.cs +++ b/src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSessionRoot.cs @@ -1,10 +1,12 @@ -namespace CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; + +namespace CodeBeam.UltimateAuth.Core.Domain; public sealed class UAuthSessionRoot { public SessionRootId RootId { get; } public UserKey UserKey { get; } - public string? TenantId { get; } + public TenantKey Tenant { get; } public bool IsRevoked { get; } public DateTimeOffset? RevokedAt { get; } public long SecurityVersion { get; } @@ -13,7 +15,7 @@ public sealed class UAuthSessionRoot private UAuthSessionRoot( SessionRootId rootId, - string? tenantId, + TenantKey tenant, UserKey userKey, bool isRevoked, DateTimeOffset? revokedAt, @@ -22,7 +24,7 @@ private UAuthSessionRoot( DateTimeOffset lastUpdatedAt) { RootId = rootId; - TenantId = tenantId; + Tenant = tenant; UserKey = userKey; IsRevoked = isRevoked; RevokedAt = revokedAt; @@ -32,13 +34,13 @@ private UAuthSessionRoot( } public static UAuthSessionRoot Create( - string? tenantId, + TenantKey tenant, UserKey userKey, DateTimeOffset issuedAt) { return new UAuthSessionRoot( SessionRootId.New(), - tenantId, + tenant, userKey, isRevoked: false, revokedAt: null, @@ -55,7 +57,7 @@ public UAuthSessionRoot Revoke(DateTimeOffset at) return new UAuthSessionRoot( RootId, - TenantId, + Tenant, UserKey, isRevoked: true, revokedAt: at, @@ -72,7 +74,7 @@ public UAuthSessionRoot AttachChain(UAuthSessionChain chain, DateTimeOffset at) return new UAuthSessionRoot( RootId, - TenantId, + Tenant, UserKey, IsRevoked, RevokedAt, @@ -84,7 +86,7 @@ public UAuthSessionRoot AttachChain(UAuthSessionChain chain, DateTimeOffset at) internal static UAuthSessionRoot FromProjection( SessionRootId rootId, - string? tenantId, + TenantKey tenant, UserKey userKey, bool isRevoked, DateTimeOffset? revokedAt, @@ -94,7 +96,7 @@ internal static UAuthSessionRoot FromProjection( { return new UAuthSessionRoot( rootId, - tenantId, + tenant, userKey, isRevoked, revokedAt, diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Token/StoredRefreshToken.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Token/StoredRefreshToken.cs index 792f8204..31cb67eb 100644 --- a/src/CodeBeam.UltimateAuth.Core/Domain/Token/StoredRefreshToken.cs +++ b/src/CodeBeam.UltimateAuth.Core/Domain/Token/StoredRefreshToken.cs @@ -1,4 +1,5 @@ -using System.ComponentModel.DataAnnotations.Schema; +using CodeBeam.UltimateAuth.Core.MultiTenancy; +using System.ComponentModel.DataAnnotations.Schema; namespace CodeBeam.UltimateAuth.Core.Domain; @@ -10,7 +11,7 @@ public sealed record StoredRefreshToken { public string TokenHash { get; init; } = default!; - public string? TenantId { get; init; } + public TenantKey Tenant { get; init; } public required UserKey UserKey { get; init; } diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Token/UAuthJwtTokenDescriptor.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Token/UAuthJwtTokenDescriptor.cs index 005d8d97..1fe6ef8e 100644 --- a/src/CodeBeam.UltimateAuth.Core/Domain/Token/UAuthJwtTokenDescriptor.cs +++ b/src/CodeBeam.UltimateAuth.Core/Domain/Token/UAuthJwtTokenDescriptor.cs @@ -1,4 +1,6 @@ -namespace CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; + +namespace CodeBeam.UltimateAuth.Core.Domain; /// /// Framework-agnostic JWT description used by IJwtTokenGenerator. @@ -13,7 +15,7 @@ public sealed class UAuthJwtTokenDescriptor public required DateTimeOffset IssuedAt { get; init; } public required DateTimeOffset ExpiresAt { get; init; } - public string? TenantId { get; init; } + public TenantKey Tenant { get; init; } public IReadOnlyDictionary? Claims { get; init; } diff --git a/src/CodeBeam.UltimateAuth.Core/Infrastructure/DefaultRefreshTokenValidator.cs b/src/CodeBeam.UltimateAuth.Core/Infrastructure/DefaultRefreshTokenValidator.cs index 6f2b08f4..cb6e2f62 100644 --- a/src/CodeBeam.UltimateAuth.Core/Infrastructure/DefaultRefreshTokenValidator.cs +++ b/src/CodeBeam.UltimateAuth.Core/Infrastructure/DefaultRefreshTokenValidator.cs @@ -17,21 +17,21 @@ public DefaultRefreshTokenValidator(IRefreshTokenStore store, ITokenHasher hashe public async Task ValidateAsync(RefreshTokenValidationContext context, CancellationToken ct = default) { var hash = _hasher.Hash(context.RefreshToken); - var stored = await _store.FindByHashAsync(context.TenantId, hash, ct); + var stored = await _store.FindByHashAsync(context.Tenant, hash, ct); if (stored is null) return RefreshTokenValidationResult.Invalid(); if (stored.IsRevoked) return RefreshTokenValidationResult.ReuseDetected( - tenantId: stored.TenantId, + tenant: stored.Tenant, sessionId: stored.SessionId, chainId: stored.ChainId, userKey: stored.UserKey); if (stored.IsExpired(context.Now)) { - await _store.RevokeAsync(context.TenantId, hash, context.Now, null, ct); + await _store.RevokeAsync(context.Tenant, hash, context.Now, null, ct); return RefreshTokenValidationResult.Invalid(); } @@ -45,7 +45,7 @@ public async Task ValidateAsync(RefreshTokenValida // return Invalid(); return RefreshTokenValidationResult.Valid( - tenantId: stored.TenantId, + tenant: stored.Tenant, stored.UserKey, stored.SessionId, hash, diff --git a/src/CodeBeam.UltimateAuth.Core/Infrastructure/NoOpAccessTokenIdStore.cs b/src/CodeBeam.UltimateAuth.Core/Infrastructure/NoOpAccessTokenIdStore.cs index 73514ccf..18fa200a 100644 --- a/src/CodeBeam.UltimateAuth.Core/Infrastructure/NoOpAccessTokenIdStore.cs +++ b/src/CodeBeam.UltimateAuth.Core/Infrastructure/NoOpAccessTokenIdStore.cs @@ -1,15 +1,16 @@ using CodeBeam.UltimateAuth.Core.Abstractions; +using CodeBeam.UltimateAuth.Core.MultiTenancy; namespace CodeBeam.UltimateAuth.Core.Infrastructure; internal sealed class NoopAccessTokenIdStore : IAccessTokenIdStore { - public Task StoreAsync(string? tenantId, string jti, DateTimeOffset expiresAt, CancellationToken ct = default) + public Task StoreAsync(TenantKey tenant, string jti, DateTimeOffset expiresAt, CancellationToken ct = default) => Task.CompletedTask; - public Task IsRevokedAsync(string? tenantId, string jti, CancellationToken ct = default) + public Task IsRevokedAsync(TenantKey tenant, string jti, CancellationToken ct = default) => Task.FromResult(false); - public Task RevokeAsync(string? tenantId, string jti, DateTimeOffset revokedAt, CancellationToken ct = default) + public Task RevokeAsync(TenantKey tenant, string jti, DateTimeOffset revokedAt, CancellationToken ct = default) => Task.CompletedTask; } diff --git a/src/CodeBeam.UltimateAuth.Core/Infrastructure/SeedRunner.cs b/src/CodeBeam.UltimateAuth.Core/Infrastructure/SeedRunner.cs index 44fca7ae..6ccb72ab 100644 --- a/src/CodeBeam.UltimateAuth.Core/Infrastructure/SeedRunner.cs +++ b/src/CodeBeam.UltimateAuth.Core/Infrastructure/SeedRunner.cs @@ -1,4 +1,5 @@ using CodeBeam.UltimateAuth.Core.Abstractions; +using CodeBeam.UltimateAuth.Core.MultiTenancy; namespace CodeBeam.UltimateAuth.Core.Infrastructure; @@ -16,11 +17,14 @@ public SeedRunner(IEnumerable contributors) } } - public async Task RunAsync(string? tenantId, CancellationToken ct = default) + public async Task RunAsync(TenantKey? tenant, CancellationToken ct = default) { + if (tenant == null) + tenant = TenantKey.Single; + foreach (var c in _contributors.OrderBy(x => x.Order)) { - await c.SeedAsync(tenantId, ct); + await c.SeedAsync((TenantKey)tenant, ct); } } } diff --git a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/FixedTenantResolver.cs b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/FixedTenantResolver.cs index dd10d648..83a675a7 100644 --- a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/FixedTenantResolver.cs +++ b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/FixedTenantResolver.cs @@ -1,26 +1,16 @@ namespace CodeBeam.UltimateAuth.Core.MultiTenancy; -/// -/// Returns a constant tenant id for all resolution requests; useful for single-tenant or statically configured systems. -/// public sealed class FixedTenantResolver : ITenantIdResolver { private readonly string _tenantId; - /// - /// Creates a resolver that always returns the specified tenant id. - /// - /// The tenant id that will be returned for all requests. public FixedTenantResolver(string tenantId) { + if (string.IsNullOrWhiteSpace(tenantId)) + throw new ArgumentException("Tenant id cannot be empty.", nameof(tenantId)); + _tenantId = tenantId; } - /// - /// Returns the fixed tenant id regardless of context. - /// - public Task ResolveTenantIdAsync(TenantResolutionContext context) - { - return Task.FromResult(_tenantId); - } + public Task ResolveTenantIdAsync(TenantResolutionContext context) => Task.FromResult(_tenantId); } diff --git a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/HeaderTenantResolver.cs b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/HeaderTenantResolver.cs index 5ae8263e..512ef583 100644 --- a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/HeaderTenantResolver.cs +++ b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/HeaderTenantResolver.cs @@ -28,7 +28,7 @@ public HeaderTenantResolver(string headerName) context.Headers.TryGetValue(_headerName, out var value) && !string.IsNullOrWhiteSpace(value)) { - return Task.FromResult(value); + return Task.FromResult(value.ToString().Trim()); } return Task.FromResult(null); diff --git a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/TenantContext.cs b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/TenantContext.cs index 3c596c63..17e51cbc 100644 --- a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/TenantContext.cs +++ b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/TenantContext.cs @@ -2,12 +2,12 @@ public sealed class TenantContext { - public string? TenantId { get; } + public TenantKey Tenant { get; } public bool IsGlobal { get; } - public TenantContext(string? tenantId, bool isGlobal = false) + public TenantContext(TenantKey tenant, bool isGlobal = false) { - TenantId = tenantId; + Tenant = tenant; IsGlobal = isGlobal; } } diff --git a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/TenantKey.cs b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/TenantKey.cs new file mode 100644 index 00000000..4a8ca986 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/TenantKey.cs @@ -0,0 +1,102 @@ +using System.Security; +using System.Text.RegularExpressions; + +namespace CodeBeam.UltimateAuth.Core.MultiTenancy; + +public readonly record struct TenantKey : IParsable +{ + public string Value { get; } + + private TenantKey(string value) + { + Value = value; + } + + private static readonly Regex Allowed = new(@"^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$", RegexOptions.Compiled); + + internal static readonly TenantKey Single = new("__single__"); + internal static readonly TenantKey System = new("__system__"); + internal static readonly TenantKey Unresolved = new("__unresolved__"); + + public bool IsSingle => Value == Single.Value; + public bool IsSystem => Value == System.Value; + public bool IsUnresolved => Value == Unresolved.Value; + + /// + /// True only for real, customer-defined tenants. + /// + public bool IsNormal => !IsSingle && !IsSystem && !IsUnresolved; + + public static TenantKey Parse(string s, IFormatProvider? provider) + { + if (!TryParse(s, provider, out var result)) + throw new FormatException($"Invalid TenantKey value: '{s}'"); + + return result; + } + + public static bool TryParse(string? s, IFormatProvider? provider, out TenantKey result) + { + result = default; + + if (string.IsNullOrWhiteSpace(s)) + return false; + + try + { + result = FromExternal(s); + return true; + } + catch + { + return false; + } + } + + /// + /// Creates a tenant key from EXTERNAL input (HTTP, headers, tokens). + /// System-reserved values are rejected. + /// + public static TenantKey FromExternal(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + throw new SecurityException("Missing tenant claim."); + + var normalized = Normalize(value); + + if (normalized == Single.Value || normalized == System.Value || normalized == Unresolved.Value) + { + throw new ArgumentException("Reserved tenant id."); + } + + return new TenantKey(normalized); + } + + /// + /// Internal creation for framework use only. + /// + internal static TenantKey FromInternal(string value) => new(value); + + private static string Normalize(string value) + { + if (value is null) + throw new ArgumentNullException(nameof(value)); + + var normalized = value.Trim(); + + if (normalized.Length == 0) + throw new ArgumentException("TenantKey cannot be empty."); + + if (normalized.Length > 128) + throw new ArgumentException("TenantKey is too long."); + + if (!Allowed.IsMatch(normalized)) + throw new ArgumentException("TenantKey contains invalid characters."); + + return normalized; + } + + public override string ToString() => Value; + + public static implicit operator string(TenantKey key) => key.Value; +} diff --git a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/TenantResolutionResult.cs b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/TenantResolutionResult.cs new file mode 100644 index 00000000..f358afa2 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/TenantResolutionResult.cs @@ -0,0 +1,23 @@ +namespace CodeBeam.UltimateAuth.Core.MultiTenancy; + +public sealed record TenantResolutionResult +{ + public bool IsResolved { get; } + public TenantKey Tenant { get; } + + private TenantResolutionResult(bool isResolved, TenantKey tenant) + { + IsResolved = isResolved; + Tenant = tenant; + } + + /// + /// Indicates that no tenant could be resolved from the request. + /// + public static TenantResolutionResult NotResolved() => new(isResolved: false, tenant: TenantKey.Unresolved); + + /// + /// Indicates that a tenant has been successfully resolved. + /// + public static TenantResolutionResult Resolved(TenantKey tenant) => new(isResolved: true, tenant); +} diff --git a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/TenantValidation.cs b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/TenantValidation.cs deleted file mode 100644 index 7df4ef93..00000000 --- a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/TenantValidation.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Text.RegularExpressions; -using CodeBeam.UltimateAuth.Core.Options; - -namespace CodeBeam.UltimateAuth.Core.MultiTenancy; - -internal static class TenantValidation -{ - public static UAuthTenantContext FromResolvedTenant(string rawTenantId, UAuthMultiTenantOptions options) - { - if (string.IsNullOrWhiteSpace(rawTenantId)) - return UAuthTenantContext.NotResolved(); - - var tenantId = options.NormalizeToLowercase - ? rawTenantId.ToLowerInvariant() - : rawTenantId; - - if (!Regex.IsMatch(tenantId, options.TenantIdRegex)) - return UAuthTenantContext.NotResolved(); - - if (options.ReservedTenantIds.Contains(tenantId)) - return UAuthTenantContext.NotResolved(); - - return UAuthTenantContext.Resolved(tenantId); - } -} diff --git a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/UAuthTenantContext.cs b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/UAuthTenantContext.cs index 8229e2b7..017c36f9 100644 --- a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/UAuthTenantContext.cs +++ b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/UAuthTenantContext.cs @@ -5,18 +5,20 @@ /// public sealed class UAuthTenantContext { - public string? TenantId { get; } - public bool IsResolved { get; } + public TenantKey Tenant { get; } - private UAuthTenantContext(string? tenantId, bool resolved) + private UAuthTenantContext(TenantKey tenant) { - TenantId = tenantId; - IsResolved = resolved; + if (tenant.IsUnresolved) + throw new InvalidOperationException("Runtime tenant context cannot be unresolved."); + + Tenant = tenant; } - public static UAuthTenantContext NotResolved() - => new(null, false); + public bool IsSingleTenant => Tenant.IsSingle; + public bool IsSystem => Tenant.IsSystem; - public static UAuthTenantContext Resolved(string tenantId) - => new(tenantId, true); + public static UAuthTenantContext SingleTenant() => new(TenantKey.Single); + public static UAuthTenantContext System() => new(TenantKey.System); + public static UAuthTenantContext Resolved(TenantKey tenant) => new(tenant); } diff --git a/src/CodeBeam.UltimateAuth.Core/Options/UAuthMultiTenantOptions.cs b/src/CodeBeam.UltimateAuth.Core/Options/UAuthMultiTenantOptions.cs index cfbf30be..2d76b691 100644 --- a/src/CodeBeam.UltimateAuth.Core/Options/UAuthMultiTenantOptions.cs +++ b/src/CodeBeam.UltimateAuth.Core/Options/UAuthMultiTenantOptions.cs @@ -14,14 +14,8 @@ public sealed class UAuthMultiTenantOptions public bool Enabled { get; set; } = false; /// - /// If tenant cannot be resolved, this value is used. - /// If null and RequireTenant = true, request fails. - /// - public string? DefaultTenantId { get; set; } - - /// - /// If true, a resolved tenant id must always exist. - /// If resolver cannot determine tenant, request will fail. + /// If true, tenant resolution MUST succeed for external requests. + /// If false, unresolved tenants fall back to single-tenant behavior. /// public bool RequireTenant { get; set; } = false; @@ -32,29 +26,12 @@ public sealed class UAuthMultiTenantOptions /// public bool AllowUnknownTenants { get; set; } = true; - /// - /// Tenant ids that cannot be used by clients. - /// Protects system-level tenant identifiers. - /// - public HashSet ReservedTenantIds { get; set; } = new() - { - "system", - "root", - "admin", - "public" - }; - /// /// If true, tenant identifiers are normalized to lowercase. /// Recommended for host-based tenancy. /// public bool NormalizeToLowercase { get; set; } = true; - /// - /// Optional validation for tenant id format. - /// Default: alphanumeric + hyphens allowed. - /// - public string TenantIdRegex { get; set; } = "^[a-zA-Z0-9\\-]+$"; /// /// Enables tenant resolution from the URL path and @@ -70,12 +47,9 @@ public sealed class UAuthMultiTenantOptions internal UAuthMultiTenantOptions Clone() => new() { Enabled = Enabled, - DefaultTenantId = DefaultTenantId, RequireTenant = RequireTenant, AllowUnknownTenants = AllowUnknownTenants, - ReservedTenantIds = new HashSet(ReservedTenantIds), NormalizeToLowercase = NormalizeToLowercase, - TenantIdRegex = TenantIdRegex, EnableRoute = EnableRoute, EnableHeader = EnableHeader, EnableDomain = EnableDomain, diff --git a/src/CodeBeam.UltimateAuth.Core/Options/UAuthMultiTenantOptionsValidator.cs b/src/CodeBeam.UltimateAuth.Core/Options/UAuthMultiTenantOptionsValidator.cs index db745d49..ec416dfc 100644 --- a/src/CodeBeam.UltimateAuth.Core/Options/UAuthMultiTenantOptionsValidator.cs +++ b/src/CodeBeam.UltimateAuth.Core/Options/UAuthMultiTenantOptionsValidator.cs @@ -1,82 +1,29 @@ -using System.Text.RegularExpressions; -using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options; +using CodeBeam.UltimateAuth.Core.MultiTenancy; namespace CodeBeam.UltimateAuth.Core.Options; -/// -/// Validates at application startup. -/// Ensures that tenant configuration values (regex patterns, defaults, -/// reserved identifiers, and requirement rules) are logically consistent -/// and safe to use before multi-tenant authentication begins. -/// internal sealed class UAuthMultiTenantOptionsValidator : IValidateOptions { - /// - /// Performs validation on the provided instance. - /// This method enforces: - /// - valid tenant id regex format, - /// - reserved tenant ids matching the regex, - /// - default tenant id consistency, - /// - requirement rules coherence. - /// - /// Optional configuration section name. - /// The options instance to validate. - /// - /// A indicating success or the - /// specific configuration error encountered. - /// public ValidateOptionsResult Validate(string? name, UAuthMultiTenantOptions options) { - // Multi-tenancy disabled → no validation needed if (!options.Enabled) - return ValidateOptionsResult.Success; - - try - { - _ = new Regex(options.TenantIdRegex, RegexOptions.Compiled); - } - catch (Exception ex) - { - return ValidateOptionsResult.Fail( - $"Invalid TenantIdRegex '{options.TenantIdRegex}'. Regex error: {ex.Message}"); - } - - foreach (var reserved in options.ReservedTenantIds) { - if (string.IsNullOrWhiteSpace(reserved)) - { - return ValidateOptionsResult.Fail( - "ReservedTenantIds cannot contain empty or whitespace values."); - } - - if (!Regex.IsMatch(reserved, options.TenantIdRegex)) + if (options.RequireTenant) { - return ValidateOptionsResult.Fail( - $"Reserved tenant id '{reserved}' does not match TenantIdRegex '{options.TenantIdRegex}'."); + return ValidateOptionsResult.Fail("RequireTenant cannot be true when multi-tenancy is disabled."); } - } - if (options.DefaultTenantId != null) - { - if (string.IsNullOrWhiteSpace(options.DefaultTenantId)) - { - return ValidateOptionsResult.Fail("DefaultTenantId cannot be empty or whitespace."); - } - - if (!Regex.IsMatch(options.DefaultTenantId, options.TenantIdRegex)) - { - return ValidateOptionsResult.Fail($"DefaultTenantId '{options.DefaultTenantId}' does not match TenantIdRegex '{options.TenantIdRegex}'."); - } - - if (options.ReservedTenantIds.Contains(options.DefaultTenantId)) - { - return ValidateOptionsResult.Fail($"DefaultTenantId '{options.DefaultTenantId}' is listed in ReservedTenantIds."); - } + return ValidateOptionsResult.Success; } - if (options.RequireTenant && options.DefaultTenantId == null) + if (!options.EnableRoute && + !options.EnableHeader && + !options.EnableDomain) { - return ValidateOptionsResult.Fail("RequireTenant = true, but DefaultTenantId is null. Provide a default tenant id or disable RequireTenant."); + return ValidateOptionsResult.Fail( + "Multi-tenancy is enabled but no tenant resolver is active " + + "(route, header, or domain)."); } return ValidateOptionsResult.Success; diff --git a/src/CodeBeam.UltimateAuth.Server/Abstractions/ResolvedCredential.cs b/src/CodeBeam.UltimateAuth.Server/Abstractions/ResolvedCredential.cs index 6c030046..cd87c461 100644 --- a/src/CodeBeam.UltimateAuth.Server/Abstractions/ResolvedCredential.cs +++ b/src/CodeBeam.UltimateAuth.Server/Abstractions/ResolvedCredential.cs @@ -1,5 +1,6 @@ using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; namespace CodeBeam.UltimateAuth.Server.Abstractions { @@ -12,7 +13,7 @@ public sealed record ResolvedCredential /// public string Value { get; init; } = default!; - public string? TenantId { get; init; } = default!; + public TenantKey Tenant { get; init; } public DeviceInfo Device { get; init; } = default!; } diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthFlowContext.cs b/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthFlowContext.cs index 253b0c65..e0f59305 100644 --- a/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthFlowContext.cs +++ b/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthFlowContext.cs @@ -1,6 +1,7 @@ using CodeBeam.UltimateAuth.Core; using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; using CodeBeam.UltimateAuth.Core.Options; using CodeBeam.UltimateAuth.Server.Options; @@ -14,7 +15,7 @@ public sealed class AuthFlowContext public UAuthMode EffectiveMode { get; } public DeviceContext Device { get; } - public string? TenantId { get; } + public TenantKey Tenant { get; } public SessionSecurityContext? Session { get; } public bool IsAuthenticated { get; } public UserKey? UserKey { get; } @@ -30,12 +31,15 @@ public sealed class AuthFlowContext Response.AccessTokenDelivery.Mode != TokenResponseMode.None || Response.RefreshTokenDelivery.Mode != TokenResponseMode.None; + public bool IsSingleTenant => Tenant.IsSingle; + public bool IsMultiTenant => !Tenant.IsSingle && !Tenant.IsSystem; + internal AuthFlowContext( AuthFlowType flowType, UAuthClientProfile clientProfile, UAuthMode effectiveMode, DeviceContext device, - string? tenantId, + TenantKey tenantKey, bool isAuthenticated, UserKey? userKey, SessionSecurityContext? session, @@ -44,12 +48,15 @@ internal AuthFlowContext( EffectiveAuthResponse response, PrimaryTokenKind primaryTokenKind) { + if (tenantKey.IsUnresolved) + throw new InvalidOperationException("AuthFlowContext cannot be created with unresolved tenant."); + FlowType = flowType; ClientProfile = clientProfile; EffectiveMode = effectiveMode; Device = device; - TenantId = tenantId; + Tenant = tenantKey; Session = session; IsAuthenticated = isAuthenticated; UserKey = userKey; diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthFlowContextFactory.cs b/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthFlowContextFactory.cs index 165def4a..8e527269 100644 --- a/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthFlowContextFactory.cs +++ b/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthFlowContextFactory.cs @@ -1,10 +1,10 @@ -using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Abstractions; +using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Server.Abstractions; using CodeBeam.UltimateAuth.Server.Extensions; using CodeBeam.UltimateAuth.Server.Infrastructure; using CodeBeam.UltimateAuth.Server.Options; -using CodeBeam.UltimateAuth.Server.Services; using Microsoft.AspNetCore.Http; namespace CodeBeam.UltimateAuth.Server.Auth @@ -23,6 +23,7 @@ internal sealed class DefaultAuthFlowContextFactory : IAuthFlowContextFactory private readonly IDeviceResolver _deviceResolver; private readonly IDeviceContextFactory _deviceContextFactory; private readonly ISessionValidator _sessionValidator; + private readonly IClock _clock; public DefaultAuthFlowContextFactory( IClientProfileReader clientProfileReader, @@ -31,7 +32,8 @@ public DefaultAuthFlowContextFactory( IAuthResponseResolver authResponseResolver, IDeviceResolver deviceResolver, IDeviceContextFactory deviceContextFactory, - ISessionValidator sessionValidator) + ISessionValidator sessionValidator, + IClock clock) { _clientProfileReader = clientProfileReader; _primaryTokenResolver = primaryTokenResolver; @@ -40,11 +42,12 @@ public DefaultAuthFlowContextFactory( _deviceResolver = deviceResolver; _deviceContextFactory = deviceContextFactory; _sessionValidator = sessionValidator; + _clock = clock; } public async ValueTask CreateAsync(HttpContext ctx, AuthFlowType flowType, CancellationToken ct = default) { - var tenant = ctx.GetTenantContext(); + var tenant = ctx.GetTenant(); var sessionCtx = ctx.GetSessionContext(); var user = ctx.GetUserContext(); @@ -67,16 +70,19 @@ public async ValueTask CreateAsync(HttpContext ctx, AuthFlowTyp var validation = await _sessionValidator.ValidateSessionAsync( new SessionValidationContext { - TenantId = sessionCtx.TenantId, + Tenant = tenant, SessionId = sessionCtx.SessionId!.Value, Device = deviceContext, - Now = DateTimeOffset.UtcNow + Now = _clock.UtcNow }, ct); sessionSecurityContext = SessionValidationMapper.ToSecurityContext(validation); } + if (tenant.IsUnresolved) + throw new InvalidOperationException("AuthFlowContext cannot be created with unresolved tenant."); + // TODO: Implement invariant checker //_invariantChecker.Validate(flowType, effectiveMode, response, effectiveOptions); @@ -85,7 +91,7 @@ public async ValueTask CreateAsync(HttpContext ctx, AuthFlowTyp clientProfile, effectiveMode, deviceContext, - tenant?.TenantId, + tenant, user?.IsAuthenticated ?? false, user?.UserId, sessionSecurityContext, diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/Context/DefaultAccessContextFactory.cs b/src/CodeBeam.UltimateAuth.Server/Auth/Context/DefaultAccessContextFactory.cs index 9ff7b684..7e3368ca 100644 --- a/src/CodeBeam.UltimateAuth.Server/Auth/Context/DefaultAccessContextFactory.cs +++ b/src/CodeBeam.UltimateAuth.Server/Auth/Context/DefaultAccessContextFactory.cs @@ -1,53 +1,66 @@ using CodeBeam.UltimateAuth.Authorization; using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.MultiTenancy; using System.Collections.ObjectModel; -namespace CodeBeam.UltimateAuth.Server.Auth +namespace CodeBeam.UltimateAuth.Server.Auth; + +internal sealed class DefaultAccessContextFactory : IAccessContextFactory { - internal sealed class DefaultAccessContextFactory : IAccessContextFactory + private readonly IUserRoleStore _roleStore; + + public DefaultAccessContextFactory(IUserRoleStore roleStore) + { + _roleStore = roleStore; + } + + public async Task CreateAsync(AuthFlowContext authFlow, string action, string resource, string? resourceId = null, IDictionary? attributes = null, CancellationToken ct = default) + { + return await CreateInternalAsync(authFlow, action, resource, authFlow.Tenant, resourceId, attributes, ct); + } + + public async Task CreateForExplicitTenantResourceAsync(AuthFlowContext authFlow, string action, string resource, TenantKey resourceTenant, string? resourceId = null, IDictionary? attributes = null, CancellationToken ct = default) + { + if (resourceTenant.IsSystem || resourceTenant.IsUnresolved) + throw new InvalidOperationException("Invalid resource tenant."); + + return await CreateInternalAsync(authFlow, action, resource, resourceTenant, resourceId, attributes, ct); + } + + private async Task CreateInternalAsync(AuthFlowContext authFlow, string action, string resource, TenantKey resourceTenant, string? resourceId, IDictionary? attributes, CancellationToken ct) { - private readonly IUserRoleStore _roleStore; + if (string.IsNullOrWhiteSpace(action)) + throw new ArgumentException("Action is required.", nameof(action)); + + if (string.IsNullOrWhiteSpace(resource)) + throw new ArgumentException("Resource is required.", nameof(resource)); + + var attrs = attributes != null + ? new Dictionary(attributes) + : new Dictionary(); - public DefaultAccessContextFactory(IUserRoleStore roleStore) + if (authFlow.IsAuthenticated && authFlow.UserKey is not null) { - _roleStore = roleStore; + var roles = await _roleStore.GetRolesAsync(authFlow.Tenant, authFlow.UserKey.Value, ct); + attrs["roles"] = roles; } - public async Task CreateAsync(AuthFlowContext authFlow, string action, string resource, string? resourceId = null, string? resourceTenantId = null, IDictionary? attributes = null, CancellationToken ct = default) + return new AccessContext { - if (string.IsNullOrWhiteSpace(action)) - throw new ArgumentException("Action is required.", nameof(action)); - - if (string.IsNullOrWhiteSpace(resource)) - throw new ArgumentException("Resource is required.", nameof(resource)); - - var attrs = attributes is not null - ? new Dictionary(attributes) - : new Dictionary(); - - if (authFlow.IsAuthenticated && authFlow.UserKey is not null) - { - var roles = await _roleStore.GetRolesAsync(authFlow.TenantId, authFlow.UserKey.Value, ct); - attrs["roles"] = roles; - } - - return new AccessContext - { - ActorUserKey = authFlow.UserKey, - ActorTenantId = authFlow.TenantId, - IsAuthenticated = authFlow.IsAuthenticated, - IsSystemActor = false, - - Resource = resource, - ResourceId = resourceId, - ResourceTenantId = resourceTenantId ?? authFlow.TenantId, - - Action = action, - - Attributes = attrs.Count > 0 - ? new ReadOnlyDictionary(attrs) - : EmptyAttributes.Instance - }; - } + ActorUserKey = authFlow.UserKey, + ActorTenant = authFlow.Tenant, + IsAuthenticated = authFlow.IsAuthenticated, + IsSystemActor = authFlow.Tenant.IsSystem, + + Resource = resource, + ResourceId = resourceId, + ResourceTenant = resourceTenant, + + Action = action, + Attributes = attrs.Count > 0 + ? new ReadOnlyDictionary(attrs) + : EmptyAttributes.Instance + }; } + } diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/Context/IAccessContextFactory.cs b/src/CodeBeam.UltimateAuth.Server/Auth/Context/IAccessContextFactory.cs index 4bc5aaad..7c6a5fe2 100644 --- a/src/CodeBeam.UltimateAuth.Server/Auth/Context/IAccessContextFactory.cs +++ b/src/CodeBeam.UltimateAuth.Server/Auth/Context/IAccessContextFactory.cs @@ -1,9 +1,10 @@ using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.MultiTenancy; -namespace CodeBeam.UltimateAuth.Server.Auth +namespace CodeBeam.UltimateAuth.Server.Auth; + +public interface IAccessContextFactory { - public interface IAccessContextFactory - { - Task CreateAsync(AuthFlowContext authFlow, string action, string resource, string? resourceId = null, string? resourceTenantId = null, IDictionary? attributes = null, CancellationToken ct = default); - } + Task CreateAsync(AuthFlowContext authFlow, string action, string resource, string? resourceId = null, IDictionary? attributes = null, CancellationToken ct = default); + Task CreateForExplicitTenantResourceAsync(AuthFlowContext authFlow, string action, string resource, TenantKey resourceTenant, string? resourceId = null, IDictionary? attributes = null, CancellationToken ct = default); } diff --git a/src/CodeBeam.UltimateAuth.Server/Authentication/UAuthAuthenticationHandler.cs b/src/CodeBeam.UltimateAuth.Server/Authentication/UAuthAuthenticationHandler.cs index 67507aef..f9b5db30 100644 --- a/src/CodeBeam.UltimateAuth.Server/Authentication/UAuthAuthenticationHandler.cs +++ b/src/CodeBeam.UltimateAuth.Server/Authentication/UAuthAuthenticationHandler.cs @@ -3,6 +3,7 @@ using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Core.Extensions; using CodeBeam.UltimateAuth.Server.Defaults; +using CodeBeam.UltimateAuth.Server.Extensions; using CodeBeam.UltimateAuth.Server.Infrastructure; using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.Logging; @@ -43,10 +44,12 @@ protected override async Task HandleAuthenticateAsync() if (!AuthSessionId.TryCreate(credential.Value, out var sessionId)) return AuthenticateResult.Fail("Invalid credential"); + var tenant = Context.GetTenant(); + var result = await _sessionValidator.ValidateSessionAsync( new SessionValidationContext { - TenantId = credential.TenantId, + Tenant = tenant, SessionId = sessionId, Device = _deviceContextFactory.Create(credential.Device), Now = _clock.UtcNow diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultLoginEndpointHandler.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultLoginEndpointHandler.cs index bb215f45..05289292 100644 --- a/src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultLoginEndpointHandler.cs +++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultLoginEndpointHandler.cs @@ -4,7 +4,6 @@ using CodeBeam.UltimateAuth.Core.Options; using CodeBeam.UltimateAuth.Server.Abstractions; using CodeBeam.UltimateAuth.Server.Auth; -using CodeBeam.UltimateAuth.Server.Extensions; using CodeBeam.UltimateAuth.Server.Infrastructure; using CodeBeam.UltimateAuth.Server.Options; using CodeBeam.UltimateAuth.Server.Services; @@ -57,7 +56,7 @@ public async Task LoginAsync(HttpContext ctx) { Identifier = identifier, Secret = secret, - TenantId = authFlow.TenantId, + Tenant = authFlow.Tenant, At = _clock.UtcNow, Device = authFlow.Device, RequestTokens = shouldIssueTokens diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultLogoutEndpointHandler.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultLogoutEndpointHandler.cs index 9a99f0ef..1d030ff9 100644 --- a/src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultLogoutEndpointHandler.cs +++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultLogoutEndpointHandler.cs @@ -36,7 +36,7 @@ public async Task LogoutAsync(HttpContext ctx) { var request = new LogoutRequest { - TenantId = auth.TenantId, + Tenant = auth.Tenant, SessionId = session.SessionId, At = _clock.UtcNow, }; diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultPkceEndpointHandler.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultPkceEndpointHandler.cs index a10eb123..0772e4d7 100644 --- a/src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultPkceEndpointHandler.cs +++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultPkceEndpointHandler.cs @@ -64,7 +64,7 @@ public async Task AuthorizeAsync(HttpContext ctx) var snapshot = new PkceContextSnapshot( clientProfile: authContext.ClientProfile, - tenantId: authContext.TenantId, + tenant: authContext.Tenant, redirectUri: request.RedirectUri, deviceId: string.Empty // TODO: Fix here with device binding ); @@ -112,7 +112,7 @@ public async Task CompleteAsync(HttpContext ctx) var validation = _validator.Validate(artifact, request.CodeVerifier, new PkceContextSnapshot( clientProfile: authContext.ClientProfile, - tenantId: authContext.TenantId, + tenant: authContext.Tenant, redirectUri: null, deviceId: string.Empty), _clock.UtcNow); @@ -127,7 +127,7 @@ public async Task CompleteAsync(HttpContext ctx) { Identifier = request.Identifier, Secret = request.Secret, - TenantId = authContext.TenantId, + Tenant = authContext.Tenant, At = _clock.UtcNow, Device = authContext.Device, RequestTokens = authContext.AllowsTokenIssuance diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultValidateEndpointHandler.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultValidateEndpointHandler.cs index 4135b0c9..1a91f919 100644 --- a/src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultValidateEndpointHandler.cs +++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultValidateEndpointHandler.cs @@ -2,6 +2,7 @@ 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 Microsoft.AspNetCore.Http; @@ -57,10 +58,12 @@ public async Task ValidateAsync(HttpContext context, CancellationToken ); } + var tenant = context.GetTenant(); + var result = await _sessionValidator.ValidateSessionAsync( new SessionValidationContext { - TenantId = credential.TenantId, + Tenant = tenant, SessionId = sessionId, Now = _clock.UtcNow, Device = auth.Device @@ -73,8 +76,8 @@ public async Task ValidateAsync(HttpContext context, CancellationToken State = result.IsValid ? "active" : result.State.ToString().ToLowerInvariant(), Snapshot = new AuthStateSnapshot { - UserId = result.UserKey, - TenantId = result.TenantId, + UserKey = (UserKey)result.UserKey, + Tenant = result.Tenant, Claims = result.Claims, AuthenticatedAt = _clock.UtcNow, } diff --git a/src/CodeBeam.UltimateAuth.Server/Extensions/AuthFlowContextExtensions.cs b/src/CodeBeam.UltimateAuth.Server/Extensions/AuthFlowContextExtensions.cs index fb276f16..12f61e52 100644 --- a/src/CodeBeam.UltimateAuth.Server/Extensions/AuthFlowContextExtensions.cs +++ b/src/CodeBeam.UltimateAuth.Server/Extensions/AuthFlowContextExtensions.cs @@ -10,7 +10,7 @@ public static AuthContext ToAuthContext(this AuthFlowContext flow, DateTimeOffse { return new AuthContext { - TenantId = flow.TenantId, + Tenant = flow.Tenant, Operation = flow.FlowType.ToAuthOperation(), Mode = flow.EffectiveMode, At = now, @@ -26,7 +26,7 @@ public static AuthFlowContext WithClientProfile(this AuthFlowContext flow, UAuth profile, flow.EffectiveMode, flow.Device, - flow.TenantId, + flow.Tenant, flow.IsAuthenticated, flow.UserKey, flow.Session, diff --git a/src/CodeBeam.UltimateAuth.Server/Extensions/HttpContextTenantExtensions.cs b/src/CodeBeam.UltimateAuth.Server/Extensions/HttpContextTenantExtensions.cs index 6a74f8a3..bd1c749d 100644 --- a/src/CodeBeam.UltimateAuth.Server/Extensions/HttpContextTenantExtensions.cs +++ b/src/CodeBeam.UltimateAuth.Server/Extensions/HttpContextTenantExtensions.cs @@ -2,26 +2,17 @@ using CodeBeam.UltimateAuth.Server.Middlewares; using Microsoft.AspNetCore.Http; -namespace CodeBeam.UltimateAuth.Server.Extensions +namespace CodeBeam.UltimateAuth.Server.Extensions; + +public static class HttpContextTenantExtensions { - public static class HttpContextTenantExtensions + public static TenantKey GetTenant(this HttpContext context) { - public static string? GetTenantId(this HttpContext ctx) + if (!context.Items.TryGetValue(TenantMiddleware.TenantContextKey, out var value) || value is not UAuthTenantContext tenantCtx) { - return ctx.GetTenantContext().TenantId; + throw new InvalidOperationException("TenantContext is missing. TenantMiddleware must run before authentication."); } - public static UAuthTenantContext GetTenantContext(this HttpContext ctx) - { - if (ctx.Items.TryGetValue( - TenantMiddleware.TenantContextKey, - out var value) - && value is UAuthTenantContext tenant) - { - return tenant; - } - - return UAuthTenantContext.NotResolved(); - } + return tenantCtx.Tenant; } } diff --git a/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthServerServiceCollectionExtensions.cs b/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthServerServiceCollectionExtensions.cs index 4f6cf0b2..7b520733 100644 --- a/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthServerServiceCollectionExtensions.cs +++ b/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthServerServiceCollectionExtensions.cs @@ -126,7 +126,7 @@ private static IServiceCollection AddUltimateAuthServerInternal(this IServiceCol return resolvers.Count switch { - 0 => new FixedTenantResolver(opts.DefaultTenantId ?? "default"), + 0 => new NullTenantResolver(), 1 => resolvers[0], _ => new CompositeTenantResolver(resolvers) }; @@ -335,4 +335,10 @@ public static IServiceCollection AddUAuthServerInfrastructure(this IServiceColle } } + + internal sealed class NullTenantResolver : ITenantIdResolver + { + public Task ResolveTenantIdAsync(TenantResolutionContext context) => Task.FromResult(null); + } + } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/AspNetCore/DefaultTransportCredentialResolver.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/AspNetCore/DefaultTransportCredentialResolver.cs index 657f99b6..ee283b56 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/AspNetCore/DefaultTransportCredentialResolver.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/AspNetCore/DefaultTransportCredentialResolver.cs @@ -1,171 +1,168 @@ -using CodeBeam.UltimateAuth.Core.Contracts; -using CodeBeam.UltimateAuth.Core.Domain; -using CodeBeam.UltimateAuth.Server.Extensions; +using CodeBeam.UltimateAuth.Server.Extensions; using CodeBeam.UltimateAuth.Server.Options; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; -namespace CodeBeam.UltimateAuth.Server.Infrastructure -{ - internal sealed class DefaultTransportCredentialResolver : ITransportCredentialResolver - { - private readonly IOptionsMonitor _server; - - public DefaultTransportCredentialResolver(IOptionsMonitor server) - { - _server = server; - } - - public TransportCredential? Resolve(HttpContext context) - { - var cookies = _server.CurrentValue.Cookie; - - // 1️⃣ Authorization header (Bearer) - if (TryFromAuthorizationHeader(context, out var bearer)) - return bearer; +namespace CodeBeam.UltimateAuth.Server.Infrastructure; - // 2️⃣ Cookies (session / refresh / access) - if (TryFromCookies(context, cookies, out var cookie)) - return cookie; +internal sealed class DefaultTransportCredentialResolver : ITransportCredentialResolver +{ + private readonly IOptionsMonitor _server; - // 3️⃣ Query (legacy / special flows) - if (TryFromQuery(context, out var query)) - return query; + public DefaultTransportCredentialResolver(IOptionsMonitor server) + { + _server = server; + } - // 4️⃣ Body (rare, but possible – PKCE / device flows) - if (TryFromBody(context, out var body)) - return body; + public TransportCredential? Resolve(HttpContext context) + { + var cookies = _server.CurrentValue.Cookie; - // 5️⃣ Hub / external authority - if (TryFromHub(context, out var hub)) - return hub; + // 1️⃣ Authorization header (Bearer) + if (TryFromAuthorizationHeader(context, out var bearer)) + return bearer; - return null; - } + // 2️⃣ Cookies (session / refresh / access) + if (TryFromCookies(context, cookies, out var cookie)) + return cookie; - // ---------- resolvers ---------- + // 3️⃣ Query (legacy / special flows) + if (TryFromQuery(context, out var query)) + return query; - // TODO: Make scheme configurable, shouldn't be hard coded - private static bool TryFromAuthorizationHeader(HttpContext ctx, out TransportCredential credential) - { - credential = default!; + // 4️⃣ Body (rare, but possible – PKCE / device flows) + if (TryFromBody(context, out var body)) + return body; - if (!ctx.Request.Headers.TryGetValue("Authorization", out var header)) - return false; + // 5️⃣ Hub / external authority + if (TryFromHub(context, out var hub)) + return hub; - var value = header.ToString(); - if (!value.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) - return false; + return null; + } - var token = value["Bearer ".Length..].Trim(); - if (string.IsNullOrWhiteSpace(token)) - return false; + // ---------- resolvers ---------- - credential = new TransportCredential - { - Kind = TransportCredentialKind.AccessToken, - Value = token, - TenantId = ctx.GetTenantContext().TenantId, - Device = ctx.GetDevice() - }; + // TODO: Make scheme configurable, shouldn't be hard coded + private static bool TryFromAuthorizationHeader(HttpContext ctx, out TransportCredential credential) + { + credential = default!; - return true; - } + if (!ctx.Request.Headers.TryGetValue("Authorization", out var header)) + return false; - private static bool TryFromCookies( - HttpContext ctx, - UAuthCookieSetOptions cookieSet, - out TransportCredential credential) - { - credential = default!; - - // Session cookie - if (TryReadCookie(ctx, cookieSet.Session.Name, out var session)) - { - credential = Build(ctx, TransportCredentialKind.Session, session); - return true; - } - - // Refresh token cookie - if (TryReadCookie(ctx, cookieSet.RefreshToken.Name, out var refresh)) - { - credential = Build(ctx, TransportCredentialKind.RefreshToken, refresh); - return true; - } - - // Access token cookie (optional) - if (TryReadCookie(ctx, cookieSet.AccessToken.Name, out var access)) - { - credential = Build(ctx, TransportCredentialKind.AccessToken, access); - return true; - } + var value = header.ToString(); + if (!value.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) + return false; + var token = value["Bearer ".Length..].Trim(); + if (string.IsNullOrWhiteSpace(token)) return false; - } - private static bool TryFromQuery(HttpContext ctx, out TransportCredential credential) + credential = new TransportCredential { - credential = default!; - - if (!ctx.Request.Query.TryGetValue("access_token", out var token)) - return false; + Kind = TransportCredentialKind.AccessToken, + Value = token, + TenantId = ctx.GetTenant().Value, + Device = ctx.GetDevice() + }; - var value = token.ToString(); - if (string.IsNullOrWhiteSpace(value)) - return false; + return true; + } - credential = new TransportCredential - { - Kind = TransportCredentialKind.AccessToken, - Value = value, - TenantId = ctx.GetTenantContext().TenantId, - Device = ctx.GetDevice() - }; + private static bool TryFromCookies( + HttpContext ctx, + UAuthCookieSetOptions cookieSet, + out TransportCredential credential) + { + credential = default!; + // Session cookie + if (TryReadCookie(ctx, cookieSet.Session.Name, out var session)) + { + credential = Build(ctx, TransportCredentialKind.Session, session); return true; } - private static bool TryFromBody(HttpContext ctx, out TransportCredential credential) + // Refresh token cookie + if (TryReadCookie(ctx, cookieSet.RefreshToken.Name, out var refresh)) { - credential = default!; - // intentionally empty for now - // body parsing is expensive and opt-in later - return false; + credential = Build(ctx, TransportCredentialKind.RefreshToken, refresh); + return true; } - private static bool TryFromHub(HttpContext ctx, out TransportCredential credential) + // Access token cookie (optional) + if (TryReadCookie(ctx, cookieSet.AccessToken.Name, out var access)) { - credential = default!; - // UAuthHub detection can live here later - return false; + credential = Build(ctx, TransportCredentialKind.AccessToken, access); + return true; } - private static bool TryReadCookie(HttpContext ctx, string name, out string value) - { - value = string.Empty; - - if (string.IsNullOrWhiteSpace(name)) - return false; + return false; + } - if (!ctx.Request.Cookies.TryGetValue(name, out var raw)) - return false; + private static bool TryFromQuery(HttpContext ctx, out TransportCredential credential) + { + credential = default!; - raw = raw?.Trim(); - if (string.IsNullOrWhiteSpace(raw)) - return false; + if (!ctx.Request.Query.TryGetValue("access_token", out var token)) + return false; - value = raw; - return true; - } + var value = token.ToString(); + if (string.IsNullOrWhiteSpace(value)) + return false; - private static TransportCredential Build(HttpContext ctx, TransportCredentialKind kind, string value) - => new() + credential = new TransportCredential { - Kind = kind, + Kind = TransportCredentialKind.AccessToken, Value = value, - TenantId = ctx.GetTenantContext().TenantId, + TenantId = ctx.GetTenant().Value, Device = ctx.GetDevice() }; + return true; } + + private static bool TryFromBody(HttpContext ctx, out TransportCredential credential) + { + credential = default!; + // intentionally empty for now + // body parsing is expensive and opt-in later + return false; + } + + private static bool TryFromHub(HttpContext ctx, out TransportCredential credential) + { + credential = default!; + // UAuthHub detection can live here later + return false; + } + + private static bool TryReadCookie(HttpContext ctx, string name, out string value) + { + value = string.Empty; + + if (string.IsNullOrWhiteSpace(name)) + return false; + + if (!ctx.Request.Cookies.TryGetValue(name, out var raw)) + return false; + + raw = raw?.Trim(); + if (string.IsNullOrWhiteSpace(raw)) + return false; + + value = raw; + return true; + } + + private static TransportCredential Build(HttpContext ctx, TransportCredentialKind kind, string value) + => new() + { + Kind = kind, + Value = value, + TenantId = ctx.GetTenant().Value, + Device = ctx.GetDevice() + }; + } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/AspNetCore/TransportCredential.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/AspNetCore/TransportCredential.cs index 361b3acd..aac3255f 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/AspNetCore/TransportCredential.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/AspNetCore/TransportCredential.cs @@ -1,13 +1,12 @@ using CodeBeam.UltimateAuth.Core.Contracts; -namespace CodeBeam.UltimateAuth.Server.Infrastructure +namespace CodeBeam.UltimateAuth.Server.Infrastructure; + +public sealed class TransportCredential { - public sealed class TransportCredential - { - public required TransportCredentialKind Kind { get; init; } - public required string Value { get; init; } + public required TransportCredentialKind Kind { get; init; } + public required string Value { get; init; } - public string? TenantId { get; init; } - public required DeviceInfo Device { get; init; } - } + public string? TenantId { get; init; } + public required DeviceInfo Device { get; init; } } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/DefaultFlowCredentialResolver.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/DefaultFlowCredentialResolver.cs index 921447aa..4f6d7cad 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/DefaultFlowCredentialResolver.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/DefaultFlowCredentialResolver.cs @@ -3,9 +3,7 @@ using CodeBeam.UltimateAuth.Server.Abstractions; using CodeBeam.UltimateAuth.Server.Auth; using CodeBeam.UltimateAuth.Server.Extensions; -using CodeBeam.UltimateAuth.Server.Options; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Options; namespace CodeBeam.UltimateAuth.Server.Infrastructure { @@ -52,7 +50,7 @@ public DefaultFlowCredentialResolver(IPrimaryCredentialResolver primaryResolver) { Kind = PrimaryCredentialKind.Stateful, Value = raw.Trim(), - TenantId = context.GetTenantContext().TenantId, + Tenant = context.GetTenant(), Device = context.GetDevice() }; } @@ -84,7 +82,7 @@ public DefaultFlowCredentialResolver(IPrimaryCredentialResolver primaryResolver) { Kind = PrimaryCredentialKind.Stateless, Value = value, - TenantId = context.GetTenantContext().TenantId, + Tenant = context.GetTenant(), Device = context.GetDevice() }; } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/DefaultJwtTokenGenerator.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/DefaultJwtTokenGenerator.cs index 6d9f9006..4d9c9634 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/DefaultJwtTokenGenerator.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/DefaultJwtTokenGenerator.cs @@ -1,5 +1,6 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; using CodeBeam.UltimateAuth.Server.Abstractions; using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Tokens; @@ -51,10 +52,7 @@ private static IDictionary BuildClaims(UAuthJwtTokenDescriptor d ["sub"] = descriptor.Subject }; - if (descriptor.TenantId is not null) - { - claims["tenant"] = descriptor.TenantId; - } + claims["tenant"] = descriptor.Tenant; if (descriptor.Claims is not null) { diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeAllChainsCommand.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeAllChainsCommand.cs index 47102fb5..0052c717 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeAllChainsCommand.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeAllChainsCommand.cs @@ -17,7 +17,7 @@ public RevokeAllChainsCommand(UserKey userKey, SessionChainId? exceptChainId) public async Task ExecuteAsync(AuthContext context, ISessionIssuer issuer, CancellationToken ct) { - await issuer.RevokeAllChainsAsync(context.TenantId, UserKey, ExceptChainId, context.At, ct); + await issuer.RevokeAllChainsAsync(context.Tenant, UserKey, ExceptChainId, context.At, ct); return Unit.Value; } } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeAllSessionsCommand.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeAllSessionsCommand.cs index cdffd578..fbe86749 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeAllSessionsCommand.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeAllSessionsCommand.cs @@ -16,7 +16,7 @@ public RevokeAllUserSessionsCommand(UserKey userKey) // TODO: This method should call its own logic. Not revoke root. public async Task ExecuteAsync(AuthContext context, ISessionIssuer issuer, CancellationToken ct) { - await issuer.RevokeRootAsync(context.TenantId, UserKey, context.At, ct); + await issuer.RevokeRootAsync(context.Tenant, UserKey, context.At, ct); return Unit.Value; } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeChainCommand.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeChainCommand.cs index 3801af39..7eb4087d 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeChainCommand.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeChainCommand.cs @@ -15,7 +15,7 @@ public RevokeChainCommand(SessionChainId chainId) public async Task ExecuteAsync(AuthContext context, ISessionIssuer issuer, CancellationToken ct) { - await issuer.RevokeChainAsync(context.TenantId, ChainId, context.At, ct); + await issuer.RevokeChainAsync(context.Tenant, ChainId, context.At, ct); return Unit.Value; } } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeRootCommand.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeRootCommand.cs index a4f272af..22b36441 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeRootCommand.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeRootCommand.cs @@ -15,7 +15,7 @@ public RevokeRootCommand(UserKey userKey) public async Task ExecuteAsync(AuthContext context, ISessionIssuer issuer, CancellationToken ct) { - await issuer.RevokeRootAsync(context.TenantId, UserKey, context.At, ct); + await issuer.RevokeRootAsync(context.Tenant, UserKey, context.At, ct); return Unit.Value; } } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeSessionCommand.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeSessionCommand.cs index b32e9d4f..0bd2ee32 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeSessionCommand.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeSessionCommand.cs @@ -8,7 +8,7 @@ internal sealed record RevokeSessionCommand(AuthSessionId SessionId) : ISessionC { public async Task ExecuteAsync(AuthContext context, ISessionIssuer issuer, CancellationToken ct) { - await issuer.RevokeSessionAsync(context.TenantId, SessionId, context.At, ct); + await issuer.RevokeSessionAsync(context.Tenant, SessionId, context.At, ct); return Unit.Value; } } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Pkce/PkceAuthorizationValidator.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Pkce/PkceAuthorizationValidator.cs index c9478627..49b617f2 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Pkce/PkceAuthorizationValidator.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Pkce/PkceAuthorizationValidator.cs @@ -36,7 +36,7 @@ private static bool IsContextValid(PkceContextSnapshot original, PkceContextSnap if (!original.ClientProfile.Equals(completion.ClientProfile)) return false; - if (!string.Equals(original.TenantId, completion.TenantId, StringComparison.Ordinal)) + if (!string.Equals(original.Tenant, completion.Tenant, StringComparison.Ordinal)) return false; if (!string.Equals(original.RedirectUri, completion.RedirectUri, StringComparison.Ordinal)) diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Pkce/PkceContextSnapshot.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Pkce/PkceContextSnapshot.cs index c826b12a..2de64d01 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Pkce/PkceContextSnapshot.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Pkce/PkceContextSnapshot.cs @@ -1,4 +1,5 @@ -using CodeBeam.UltimateAuth.Core.Options; +using CodeBeam.UltimateAuth.Core.MultiTenancy; +using CodeBeam.UltimateAuth.Core.Options; namespace CodeBeam.UltimateAuth.Server.Infrastructure; @@ -11,12 +12,12 @@ public sealed class PkceContextSnapshot { public PkceContextSnapshot( UAuthClientProfile clientProfile, - string? tenantId, + TenantKey tenant, string? redirectUri, string? deviceId) { ClientProfile = clientProfile; - TenantId = tenantId; + Tenant = tenant; RedirectUri = redirectUri; DeviceId = deviceId; } @@ -29,7 +30,7 @@ public PkceContextSnapshot( /// /// Tenant context at the time of authorization. /// - public string? TenantId { get; } + public TenantKey Tenant { get; } /// /// Redirect URI used during authorization. diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/DefaultSessionTouchService.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/DefaultSessionTouchService.cs index ab232d25..885e955b 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/DefaultSessionTouchService.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/DefaultSessionTouchService.cs @@ -22,7 +22,7 @@ public async Task RefreshAsync(SessionValidationResult val if (!policy.TouchInterval.HasValue) return SessionRefreshResult.Success(validation.SessionId.Value, didTouch: false); - var kernel = _kernelFactory.Create(validation.TenantId); + var kernel = _kernelFactory.Create(validation.Tenant); bool didTouch = false; diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/User/UAuthUserAccessor.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/User/UAuthUserAccessor.cs index 8c9d46ba..9a7098a0 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/User/UAuthUserAccessor.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/User/UAuthUserAccessor.cs @@ -1,5 +1,6 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.MultiTenancy; using CodeBeam.UltimateAuth.Server.Extensions; using CodeBeam.UltimateAuth.Server.Middlewares; using Microsoft.AspNetCore.Http; @@ -27,7 +28,12 @@ public async Task ResolveAsync(HttpContext context) return; } - var kernel = _kernelFactory.Create(sessionCtx.TenantId); + if (sessionCtx.Tenant is not TenantKey tenant) + { + throw new InvalidOperationException("Tenant context is missing."); + } + + var kernel = _kernelFactory.Create(tenant); var session = await kernel.GetSessionAsync(sessionCtx.SessionId.Value); if (session is null || session.IsRevoked) diff --git a/src/CodeBeam.UltimateAuth.Server/Issuers/UAuthSessionIssuer.cs b/src/CodeBeam.UltimateAuth.Server/Issuers/UAuthSessionIssuer.cs index fb360ef2..acda7056 100644 --- a/src/CodeBeam.UltimateAuth.Server/Issuers/UAuthSessionIssuer.cs +++ b/src/CodeBeam.UltimateAuth.Server/Issuers/UAuthSessionIssuer.cs @@ -2,6 +2,7 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; using CodeBeam.UltimateAuth.Server.Options; using Microsoft.Extensions.Options; using System.Security; @@ -48,7 +49,7 @@ public async Task IssueLoginSessionAsync(AuthenticatedSessionCont var session = UAuthSession.Create( sessionId: sessionId, - tenantId: context.TenantId, + tenant: context.Tenant, userKey: context.UserKey, chainId: SessionChainId.Unassigned, now: now, @@ -65,12 +66,12 @@ public async Task IssueLoginSessionAsync(AuthenticatedSessionCont IsMetadataOnly = _options.Mode == UAuthMode.SemiHybrid }; - var kernel = _kernelFactory.Create(context.TenantId); + var kernel = _kernelFactory.Create(context.Tenant); await kernel.ExecuteAsync(async _ => { var root = await kernel.GetSessionRootByUserAsync(context.UserKey) - ?? UAuthSessionRoot.Create(context.TenantId, context.UserKey, now); + ?? UAuthSessionRoot.Create(context.Tenant, context.UserKey, now); UAuthSessionChain chain; @@ -84,7 +85,7 @@ await kernel.ExecuteAsync(async _ => chain = UAuthSessionChain.Create( SessionChainId.New(), root.RootId, - context.TenantId, + context.Tenant, context.UserKey, root.SecurityVersion, ClaimsSnapshot.Empty); @@ -105,7 +106,7 @@ await kernel.ExecuteAsync(async _ => public async Task RotateSessionAsync(SessionRotationContext context, CancellationToken ct = default) { - var kernel = _kernelFactory.Create(context.TenantId); + var kernel = _kernelFactory.Create(context.Tenant); var now = context.Now; var opaqueSessionId = _opaqueGenerator.Generate(); @@ -124,7 +125,7 @@ public async Task RotateSessionAsync(SessionRotationContext conte { Session = UAuthSession.Create( sessionId: newSessionId, - tenantId: context.TenantId, + tenant: context.Tenant, userKey: context.UserKey, chainId: SessionChainId.Unassigned, now: now, @@ -158,21 +159,21 @@ await kernel.ExecuteAsync(async _ => return issued; } - public async Task RevokeSessionAsync(string? tenantId, AuthSessionId sessionId, DateTimeOffset at, CancellationToken ct = default) + public async Task RevokeSessionAsync(TenantKey tenant, AuthSessionId sessionId, DateTimeOffset at, CancellationToken ct = default) { - var kernel = _kernelFactory.Create(tenantId); + var kernel = _kernelFactory.Create(tenant); await kernel.ExecuteAsync(_ => kernel.RevokeSessionAsync(sessionId, at), ct); } - public async Task RevokeChainAsync(string? tenantId, SessionChainId chainId, DateTimeOffset at, CancellationToken ct = default) + public async Task RevokeChainAsync(TenantKey tenant, SessionChainId chainId, DateTimeOffset at, CancellationToken ct = default) { - var kernel = _kernelFactory.Create(tenantId); + var kernel = _kernelFactory.Create(tenant); await kernel.ExecuteAsync(_ => kernel.RevokeChainAsync(chainId, at), ct); } - public async Task RevokeAllChainsAsync(string? tenantId, UserKey userKey, SessionChainId? exceptChainId, DateTimeOffset at, CancellationToken ct = default) + public async Task RevokeAllChainsAsync(TenantKey tenant, UserKey userKey, SessionChainId? exceptChainId, DateTimeOffset at, CancellationToken ct = default) { - var kernel = _kernelFactory.Create(tenantId); + var kernel = _kernelFactory.Create(tenant); await kernel.ExecuteAsync(async _ => { var chains = await kernel.GetChainsByUserAsync(userKey); @@ -193,9 +194,9 @@ await kernel.ExecuteAsync(async _ => } // TODO: Discuss revoking chains/sessions when root is revoked - public async Task RevokeRootAsync(string? tenantId, UserKey userKey, DateTimeOffset at, CancellationToken ct = default) + public async Task RevokeRootAsync(TenantKey tenant, UserKey userKey, DateTimeOffset at, CancellationToken ct = default) { - var kernel = _kernelFactory.Create(tenantId); + var kernel = _kernelFactory.Create(tenant); await kernel.ExecuteAsync(_ => kernel.RevokeSessionRootAsync(userKey, at), ct); } diff --git a/src/CodeBeam.UltimateAuth.Server/Issuers/UAuthTokenIssuer.cs b/src/CodeBeam.UltimateAuth.Server/Issuers/UAuthTokenIssuer.cs index ad22a080..7b00f8f1 100644 --- a/src/CodeBeam.UltimateAuth.Server/Issuers/UAuthTokenIssuer.cs +++ b/src/CodeBeam.UltimateAuth.Server/Issuers/UAuthTokenIssuer.cs @@ -65,7 +65,7 @@ UAuthMode.SemiHybrid or var stored = new StoredRefreshToken { - TenantId = flow.TenantId, + Tenant = flow.Tenant, TokenHash = hash, UserKey = context.UserKey, // TODO: Check here again @@ -77,7 +77,7 @@ UAuthMode.SemiHybrid or if (persistence == RefreshTokenPersistence.Persist) { - await _refreshTokenStore.StoreAsync(flow.TenantId, stored, ct); + await _refreshTokenStore.StoreAsync(flow.Tenant, stored, ct); } return new RefreshToken @@ -106,7 +106,7 @@ private AccessToken IssueJwtAccessToken(TokenIssuanceContext context, UAuthToken var claims = new Dictionary { ["sub"] = context.UserKey, - ["tenant"] = context.TenantId + ["tenant"] = context.Tenant }; foreach (var kv in context.Claims) @@ -125,7 +125,7 @@ private AccessToken IssueJwtAccessToken(TokenIssuanceContext context, UAuthToken Audience = tokens.Audience, IssuedAt = _clock.UtcNow, ExpiresAt = expires, - TenantId = context.TenantId, + Tenant = context.Tenant, Claims = claims, KeyId = tokens.KeyId }; diff --git a/src/CodeBeam.UltimateAuth.Server/Login/DefaultLoginOrchestrator.cs b/src/CodeBeam.UltimateAuth.Server/Login/DefaultLoginOrchestrator.cs index 56243758..b324c5d9 100644 --- a/src/CodeBeam.UltimateAuth.Server/Login/DefaultLoginOrchestrator.cs +++ b/src/CodeBeam.UltimateAuth.Server/Login/DefaultLoginOrchestrator.cs @@ -52,7 +52,7 @@ public async Task LoginAsync(AuthFlowContext flow, LoginRequest req var now = request.At ?? DateTimeOffset.UtcNow; - var credentials = await _credentialStore.FindByLoginAsync(request.TenantId, request.Identifier, ct); + var credentials = await _credentialStore.FindByLoginAsync(request.Tenant, request.Identifier, ct); var orderedCredentials = credentials .OfType() .Where(c => c.Security.IsUsable(now)) @@ -81,13 +81,13 @@ public async Task LoginAsync(AuthFlowContext flow, LoginRequest req if (credentialsValid) { - securityState = await _userSecurityStateProvider.GetAsync(request.TenantId, validatedUserId, ct); + securityState = await _userSecurityStateProvider.GetAsync(request.Tenant, validatedUserId, ct); var converter = _userIdConverterResolver.GetConverter(); userKey = UserKey.FromString(converter.ToCanonicalString(validatedUserId)); } var user = userKey is not null - ? await _users.GetAsync(request.TenantId, userKey.Value, ct) + ? await _users.GetAsync(request.Tenant, userKey.Value, ct) : null; if (user is null || user.IsDeleted || !user.IsActive) @@ -98,7 +98,7 @@ public async Task LoginAsync(AuthFlowContext flow, LoginRequest req var decisionContext = new LoginDecisionContext { - TenantId = request.TenantId, + Tenant = request.Tenant, Identifier = request.Identifier, CredentialsValid = credentialsValid, UserExists = userExists, @@ -126,11 +126,11 @@ public async Task LoginAsync(AuthFlowContext flow, LoginRequest req return LoginResult.Failed(); } - var claims = await _claimsProvider.GetClaimsAsync(request.TenantId, validUserKey, ct); + var claims = await _claimsProvider.GetClaimsAsync(request.Tenant, validUserKey, ct); var sessionContext = new AuthenticatedSessionContext { - TenantId = request.TenantId, + Tenant = request.Tenant, UserKey = validUserKey, Now = now, Device = request.Device, @@ -148,7 +148,7 @@ public async Task LoginAsync(AuthFlowContext flow, LoginRequest req { var tokenContext = new TokenIssuanceContext { - TenantId = request.TenantId, + Tenant = request.Tenant, UserKey = validUserKey, SessionId = issuedSession.Session.SessionId, ChainId = request.ChainId, diff --git a/src/CodeBeam.UltimateAuth.Server/Login/LoginDecisionContext.cs b/src/CodeBeam.UltimateAuth.Server/Login/LoginDecisionContext.cs index 695d19d2..03398305 100644 --- a/src/CodeBeam.UltimateAuth.Server/Login/LoginDecisionContext.cs +++ b/src/CodeBeam.UltimateAuth.Server/Login/LoginDecisionContext.cs @@ -1,4 +1,5 @@ using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; using CodeBeam.UltimateAuth.Users; namespace CodeBeam.UltimateAuth.Server.Login @@ -12,7 +13,7 @@ public sealed class LoginDecisionContext /// /// Gets the tenant identifier. /// - public string? TenantId { get; init; } + public TenantKey Tenant { get; init; } /// /// Gets the login identifier (e.g. username or email). diff --git a/src/CodeBeam.UltimateAuth.Server/Middlewares/SessionResolutionMiddleware.cs b/src/CodeBeam.UltimateAuth.Server/Middlewares/SessionResolutionMiddleware.cs index cbd53bc5..2eabb27b 100644 --- a/src/CodeBeam.UltimateAuth.Server/Middlewares/SessionResolutionMiddleware.cs +++ b/src/CodeBeam.UltimateAuth.Server/Middlewares/SessionResolutionMiddleware.cs @@ -19,14 +19,14 @@ public async Task InvokeAsync(HttpContext context) { var sessionIdResolver = context.RequestServices.GetRequiredService(); - var tenant = context.GetTenantContext(); + var tenant = context.GetTenant(); var sessionId = sessionIdResolver.Resolve(context); var sessionContext = sessionId is null ? SessionContext.Anonymous() : SessionContext.FromSessionId( sessionId.Value, - tenant.TenantId); + tenant); context.Items[SessionContextItemKeys.SessionContext] = sessionContext; diff --git a/src/CodeBeam.UltimateAuth.Server/Middlewares/TenantMiddleware.cs b/src/CodeBeam.UltimateAuth.Server/Middlewares/TenantMiddleware.cs index a9b82ffb..1f9e4a4d 100644 --- a/src/CodeBeam.UltimateAuth.Server/Middlewares/TenantMiddleware.cs +++ b/src/CodeBeam.UltimateAuth.Server/Middlewares/TenantMiddleware.cs @@ -4,44 +4,49 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; -namespace CodeBeam.UltimateAuth.Server.Middlewares +namespace CodeBeam.UltimateAuth.Server.Middlewares; + +public sealed class TenantMiddleware { - public sealed class TenantMiddleware + private readonly RequestDelegate _next; + public const string TenantContextKey = "__UAuthTenant"; + + public TenantMiddleware(RequestDelegate next) { - private readonly RequestDelegate _next; + _next = next; + } - public const string TenantContextKey = "__UAuthTenant"; + public async Task InvokeAsync(HttpContext context, ITenantResolver resolver, IOptions options) + { + var opts = options.Value; + TenantResolutionResult resolution; - public TenantMiddleware(RequestDelegate next) + if (!opts.Enabled) { - _next = next; + context.Items[TenantContextKey] = UAuthTenantContext.SingleTenant(); + await _next(context); + return; } - public async Task InvokeAsync(HttpContext context, ITenantResolver resolver, IOptions options) - { - var opts = options.Value; - - UAuthTenantContext tenantContext; + resolution = await resolver.ResolveAsync(context); - if (!opts.Enabled) - { - tenantContext = UAuthTenantContext.NotResolved(); - } - else + if (!resolution.IsResolved) + { + if (opts.RequireTenant) { - tenantContext = await resolver.ResolveAsync(context); - - if (opts.RequireTenant && !tenantContext.IsResolved) - { - context.Response.StatusCode = StatusCodes.Status400BadRequest; - await context.Response.WriteAsync("Tenant is required but could not be resolved."); - return; - } + context.Response.StatusCode = StatusCodes.Status400BadRequest; + await context.Response.WriteAsync("Tenant is required."); + return; } - context.Items[TenantContextKey] = tenantContext; - - await _next(context); + context.Response.StatusCode = StatusCodes.Status400BadRequest; + await context.Response.WriteAsync("Tenant could not be resolved."); + return; } + + var tenantContext = UAuthTenantContext.Resolved(resolution.Tenant); + + context.Items[TenantContextKey] = tenantContext; + await _next(context); } } diff --git a/src/CodeBeam.UltimateAuth.Server/MultiTenancy/ITenantResolver.cs b/src/CodeBeam.UltimateAuth.Server/MultiTenancy/ITenantResolver.cs index 60b1223a..8cf41528 100644 --- a/src/CodeBeam.UltimateAuth.Server/MultiTenancy/ITenantResolver.cs +++ b/src/CodeBeam.UltimateAuth.Server/MultiTenancy/ITenantResolver.cs @@ -1,11 +1,9 @@ using CodeBeam.UltimateAuth.Core.MultiTenancy; using Microsoft.AspNetCore.Http; -namespace CodeBeam.UltimateAuth.Server.MultiTenancy +namespace CodeBeam.UltimateAuth.Server.MultiTenancy; + +public interface ITenantResolver { - public interface ITenantResolver - { - Task ResolveAsync(HttpContext ctx); - } + Task ResolveAsync(HttpContext context); } - diff --git a/src/CodeBeam.UltimateAuth.Server/MultiTenancy/UAuthTenantContextFactory.cs b/src/CodeBeam.UltimateAuth.Server/MultiTenancy/UAuthTenantContextFactory.cs index c9531f0e..11c55e92 100644 --- a/src/CodeBeam.UltimateAuth.Server/MultiTenancy/UAuthTenantContextFactory.cs +++ b/src/CodeBeam.UltimateAuth.Server/MultiTenancy/UAuthTenantContextFactory.cs @@ -1,22 +1,29 @@ using CodeBeam.UltimateAuth.Core.MultiTenancy; using CodeBeam.UltimateAuth.Core.Options; -using System.Text.RegularExpressions; + +namespace CodeBeam.UltimateAuth.Server.MultiTenancy; public static class UAuthTenantContextFactory { - public static UAuthTenantContext Create( - string tenantId, - UAuthMultiTenantOptions options) + public static UAuthTenantContext Create(string? rawTenantId, UAuthMultiTenantOptions options) { - if (options.NormalizeToLowercase) - tenantId = tenantId.ToLowerInvariant(); + // 🔹 Single-tenant mode + if (!options.Enabled) + return UAuthTenantContext.SingleTenant(); + + if (string.IsNullOrWhiteSpace(rawTenantId)) + { + if (options.RequireTenant) + throw new InvalidOperationException("Tenant is required but could not be resolved."); - if (!Regex.IsMatch(tenantId, options.TenantIdRegex)) - return UAuthTenantContext.NotResolved(); + throw new InvalidOperationException("Tenant could not be resolved."); + } - if (options.ReservedTenantIds.Contains(tenantId)) - return UAuthTenantContext.NotResolved(); + var tenantId = options.NormalizeToLowercase + ? rawTenantId.Trim().ToLowerInvariant() + : rawTenantId.Trim(); - return UAuthTenantContext.Resolved(tenantId); + var tenantKey = TenantKey.FromExternal(tenantId); + return UAuthTenantContext.Resolved(tenantKey); } } diff --git a/src/CodeBeam.UltimateAuth.Server/MultiTenancy/UAuthTenantResolver.cs b/src/CodeBeam.UltimateAuth.Server/MultiTenancy/UAuthTenantResolver.cs index ddbe0eb4..028f16ed 100644 --- a/src/CodeBeam.UltimateAuth.Server/MultiTenancy/UAuthTenantResolver.cs +++ b/src/CodeBeam.UltimateAuth.Server/MultiTenancy/UAuthTenantResolver.cs @@ -3,74 +3,36 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; -namespace CodeBeam.UltimateAuth.Server.MultiTenancy -{ - /// - /// Server-level tenant resolver. - /// Responsible for executing core tenant id resolvers and - /// applying UltimateAuth tenant policies. - /// - public sealed class UAuthTenantResolver : ITenantResolver - { - private readonly ITenantIdResolver _idResolver; - private readonly UAuthMultiTenantOptions _options; - - public UAuthTenantResolver( - ITenantIdResolver idResolver, - IOptions options) - { - _idResolver = idResolver; - _options = options.Value; - } - - public async Task ResolveAsync(HttpContext context) - { - if (!_options.Enabled) - return UAuthTenantContext.NotResolved(); - - var resolutionContext = - TenantResolutionContextFactory.FromHttpContext(context); - - var rawTenantId = - await _idResolver.ResolveTenantIdAsync(resolutionContext); +namespace CodeBeam.UltimateAuth.Server.MultiTenancy; - if (string.IsNullOrWhiteSpace(rawTenantId)) - { - if (_options.RequireTenant) - return UAuthTenantContext.NotResolved(); - - if (_options.DefaultTenantId is null) - return UAuthTenantContext.NotResolved(); - - return UAuthTenantContext.Resolved( - Normalize(_options.DefaultTenantId)); - } +public sealed class UAuthTenantResolver : ITenantResolver +{ + private readonly ITenantIdResolver _idResolver; + private readonly UAuthMultiTenantOptions _options; - var tenantId = Normalize(rawTenantId); + public UAuthTenantResolver(ITenantIdResolver idResolver, IOptions options) + { + _idResolver = idResolver; + _options = options.Value; + } - if (!IsValid(tenantId)) - return UAuthTenantContext.NotResolved(); + public async Task ResolveAsync(HttpContext context) + { + var resolutionContext = + TenantResolutionContextFactory.FromHttpContext(context); - return UAuthTenantContext.Resolved(tenantId); - } + var raw = await _idResolver.ResolveTenantIdAsync(resolutionContext); - private string Normalize(string tenantId) - { - return _options.NormalizeToLowercase - ? tenantId.ToLowerInvariant() - : tenantId; - } + if (string.IsNullOrWhiteSpace(raw)) + return TenantResolutionResult.NotResolved(); - private bool IsValid(string tenantId) - { - if (!System.Text.RegularExpressions.Regex - .IsMatch(tenantId, _options.TenantIdRegex)) - return false; + var normalized = _options.NormalizeToLowercase + ? raw.Trim().ToLowerInvariant() + : raw.Trim(); - if (_options.ReservedTenantIds.Contains(tenantId)) - return false; + if (!TenantKey.TryParse(normalized, null, out var tenant)) + return TenantResolutionResult.NotResolved(); - return true; - } + return TenantResolutionResult.Resolved(tenant); } } diff --git a/src/CodeBeam.UltimateAuth.Server/Options/UAuthServerOptionsValidator.cs b/src/CodeBeam.UltimateAuth.Server/Options/UAuthServerOptionsValidator.cs index 1b77131c..3d135cb2 100644 --- a/src/CodeBeam.UltimateAuth.Server/Options/UAuthServerOptionsValidator.cs +++ b/src/CodeBeam.UltimateAuth.Server/Options/UAuthServerOptionsValidator.cs @@ -48,25 +48,6 @@ public ValidateOptionsResult Validate( } } - // ------------------------- - // MULTI-TENANT VALIDATION - // ------------------------- - if (options.MultiTenant.Enabled) - { - if (options.MultiTenant.RequireTenant && - string.IsNullOrWhiteSpace(options.MultiTenant.DefaultTenantId)) - { - // This is allowed, but warn-worthy logic - // We still allow it, middleware will reject requests - } - - if (string.IsNullOrWhiteSpace(options.MultiTenant.TenantIdRegex)) - { - return ValidateOptionsResult.Fail( - "MultiTenant.TenantIdRegex must be specified."); - } - } - return ValidateOptionsResult.Success; } } diff --git a/src/CodeBeam.UltimateAuth.Server/Services/DefaultRefreshFlowService.cs b/src/CodeBeam.UltimateAuth.Server/Services/DefaultRefreshFlowService.cs index ae818a52..81210e77 100644 --- a/src/CodeBeam.UltimateAuth.Server/Services/DefaultRefreshFlowService.cs +++ b/src/CodeBeam.UltimateAuth.Server/Services/DefaultRefreshFlowService.cs @@ -55,7 +55,7 @@ private async Task HandleSessionOnlyAsync(AuthFlowContext flo var validation = await _sessionValidator.ValidateSessionAsync( new SessionValidationContext { - TenantId = flow.TenantId, + Tenant = flow.Tenant, SessionId = request.SessionId.Value, Now = request.Now, Device = request.Device @@ -128,7 +128,7 @@ private async Task HandleHybridAsync(AuthFlowContext flow, Re var validation = await _sessionValidator.ValidateSessionAsync( new SessionValidationContext { - TenantId = flow.TenantId, + Tenant = flow.Tenant, SessionId = request.SessionId.Value, Now = request.Now, Device = request.Device @@ -179,7 +179,7 @@ private async Task HandleSemiHybridAsync(AuthFlowContext flow var validation = await _sessionValidator.ValidateSessionAsync( new SessionValidationContext { - TenantId = flow.TenantId, + Tenant = flow.Tenant, SessionId = request.SessionId.Value, Now = request.Now, Device = request.Device diff --git a/src/CodeBeam.UltimateAuth.Server/Services/RefreshTokenRotationService.cs b/src/CodeBeam.UltimateAuth.Server/Services/RefreshTokenRotationService.cs index e61298fe..6a8f2c52 100644 --- a/src/CodeBeam.UltimateAuth.Server/Services/RefreshTokenRotationService.cs +++ b/src/CodeBeam.UltimateAuth.Server/Services/RefreshTokenRotationService.cs @@ -1,6 +1,7 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; using CodeBeam.UltimateAuth.Server.Abstactions; using CodeBeam.UltimateAuth.Server.Auth; using System; @@ -28,7 +29,7 @@ public async Task RotateAsync(AuthFlowContext flo var validation = await _validator.ValidateAsync( new RefreshTokenValidationContext { - TenantId = flow.TenantId, + Tenant = flow.Tenant, RefreshToken = context.RefreshToken, Now = context.Now, Device = context.Device, @@ -43,11 +44,11 @@ public async Task RotateAsync(AuthFlowContext flo { if (validation.ChainId is not null) { - await _store.RevokeByChainAsync(validation.TenantId, validation.ChainId.Value, context.Now, ct); + await _store.RevokeByChainAsync(validation.Tenant, validation.ChainId.Value, context.Now, ct); } else if (validation.SessionId is not null) { - await _store.RevokeBySessionAsync(validation.TenantId, validation.SessionId.Value, context.Now, ct); + await _store.RevokeBySessionAsync(validation.Tenant, validation.SessionId.Value, context.Now, ct); } return new RefreshTokenRotationExecution() { Result = RefreshTokenRotationResult.Failed() }; @@ -60,9 +61,9 @@ public async Task RotateAsync(AuthFlowContext flo var tokenContext = new TokenIssuanceContext { - TenantId = flow.OriginalOptions.MultiTenant.Enabled - ? validation.TenantId - : null, + Tenant = flow.OriginalOptions.MultiTenant.Enabled + ? validation.Tenant + : TenantKey.Single, UserKey = uKey, SessionId = validation.SessionId, @@ -80,11 +81,11 @@ public async Task RotateAsync(AuthFlowContext flo // Never issue new refresh token before revoke old. Upperline doesn't persist token currently. // TODO: Add _store.ExecuteAsync here to wrap RevokeAsync and StoreAsync - await _store.RevokeAsync(validation.TenantId, validation.TokenHash, context.Now, refreshToken.TokenHash, ct); + await _store.RevokeAsync(validation.Tenant, validation.TokenHash, context.Now, refreshToken.TokenHash, ct); var stored = new StoredRefreshToken { - TenantId = flow.TenantId, + Tenant = flow.Tenant, TokenHash = refreshToken.TokenHash, UserKey = uKey, SessionId = validation.SessionId.Value, @@ -92,11 +93,11 @@ public async Task RotateAsync(AuthFlowContext flo IssuedAt = _clock.UtcNow, ExpiresAt = refreshToken.ExpiresAt }; - await _store.StoreAsync(validation.TenantId, stored); + await _store.StoreAsync(validation.Tenant, stored); return new RefreshTokenRotationExecution() { - TenantId = validation.TenantId, + Tenant = validation.Tenant, UserKey = validation.UserKey, SessionId = validation.SessionId, ChainId = validation.ChainId, diff --git a/src/CodeBeam.UltimateAuth.Server/Services/UAuthJwtValidator.cs b/src/CodeBeam.UltimateAuth.Server/Services/UAuthJwtValidator.cs index 931fd7dd..ab4b6d4b 100644 --- a/src/CodeBeam.UltimateAuth.Server/Services/UAuthJwtValidator.cs +++ b/src/CodeBeam.UltimateAuth.Server/Services/UAuthJwtValidator.cs @@ -4,82 +4,80 @@ using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Tokens; using System.Security.Claims; +using CodeBeam.UltimateAuth.Core.MultiTenancy; -namespace CodeBeam.UltimateAuth.Server.Services +namespace CodeBeam.UltimateAuth.Server.Services; + +internal sealed class UAuthJwtValidator : IJwtValidator { - internal sealed class UAuthJwtValidator : IJwtValidator + private readonly JsonWebTokenHandler _jwtHandler; + private readonly TokenValidationParameters _jwtParameters; + private readonly IUserIdConverterResolver _converters; + + public UAuthJwtValidator(TokenValidationParameters jwtParameters, IUserIdConverterResolver converters) { - private readonly JsonWebTokenHandler _jwtHandler; - private readonly TokenValidationParameters _jwtParameters; - private readonly IUserIdConverterResolver _converters; + _jwtHandler = new JsonWebTokenHandler(); + _jwtParameters = jwtParameters; + _converters = converters; + } - public UAuthJwtValidator( - TokenValidationParameters jwtParameters, - IUserIdConverterResolver converters) - { - _jwtHandler = new JsonWebTokenHandler(); - _jwtParameters = jwtParameters; - _converters = converters; - } + public async Task> ValidateAsync(string token, CancellationToken ct = default) + { + var result = await _jwtHandler.ValidateTokenAsync(token, _jwtParameters); - public async Task> ValidateAsync(string token, CancellationToken ct = default) + if (!result.IsValid) { - var result = await _jwtHandler.ValidateTokenAsync(token, _jwtParameters); - - if (!result.IsValid) - { - return TokenValidationResult.Invalid(TokenType.Jwt, MapJwtError(result.Exception)); - } - - var jwt = (JsonWebToken)result.SecurityToken; - var claims = jwt.Claims.ToArray(); - - var converter = _converters.GetConverter(); + return TokenValidationResult.Invalid(TokenType.Jwt, MapJwtError(result.Exception)); + } - var userIdString = jwt.GetClaim(ClaimTypes.NameIdentifier)?.Value ?? jwt.GetClaim("sub")?.Value; - if (string.IsNullOrWhiteSpace(userIdString)) - { - return TokenValidationResult.Invalid(TokenType.Jwt, TokenInvalidReason.MissingSubject); - } + var jwt = (JsonWebToken)result.SecurityToken; + var claims = jwt.Claims.ToArray(); - TUserId userId; - try - { - userId = converter.FromString(userIdString); - } - catch - { - return TokenValidationResult.Invalid(TokenType.Jwt, TokenInvalidReason.Malformed); - } + var converter = _converters.GetConverter(); - var tenantId = jwt.GetClaim("tenant")?.Value ?? jwt.GetClaim("tid")?.Value; - AuthSessionId? sessionId = null; - var sid = jwt.GetClaim("sid")?.Value; - if (!AuthSessionId.TryCreate(sid, out AuthSessionId ssid)) - { - sessionId = ssid; - } + var userIdString = jwt.GetClaim(ClaimTypes.NameIdentifier)?.Value ?? jwt.GetClaim("sub")?.Value; + if (string.IsNullOrWhiteSpace(userIdString)) + { + return TokenValidationResult.Invalid(TokenType.Jwt, TokenInvalidReason.MissingSubject); + } - return TokenValidationResult.Valid( - type: TokenType.Jwt, - tenantId: tenantId, - userId, - sessionId: sessionId, - claims: claims, - expiresAt: jwt.ValidTo); + TUserId userId; + try + { + userId = converter.FromString(userIdString); + } + catch + { + return TokenValidationResult.Invalid(TokenType.Jwt, TokenInvalidReason.Malformed); } - private static TokenInvalidReason MapJwtError(Exception? ex) + var tenantId = jwt.GetClaim("tenant")?.Value ?? jwt.GetClaim("tid")?.Value; + AuthSessionId? sessionId = null; + var sid = jwt.GetClaim("sid")?.Value; + if (AuthSessionId.TryCreate(sid, out AuthSessionId ssid)) { - return ex switch - { - SecurityTokenExpiredException => TokenInvalidReason.Expired, - SecurityTokenInvalidSignatureException => TokenInvalidReason.SignatureInvalid, - SecurityTokenInvalidAudienceException => TokenInvalidReason.AudienceMismatch, - SecurityTokenInvalidIssuerException => TokenInvalidReason.IssuerMismatch, - _ => TokenInvalidReason.Invalid - }; + sessionId = ssid; } + return TokenValidationResult.Valid( + type: TokenType.Jwt, + tenant: TenantKey.FromExternal(tenantId), + userId, + sessionId: sessionId, + claims: claims, + expiresAt: jwt.ValidTo); } + + private static TokenInvalidReason MapJwtError(Exception? ex) + { + return ex switch + { + SecurityTokenExpiredException => TokenInvalidReason.Expired, + SecurityTokenInvalidSignatureException => TokenInvalidReason.SignatureInvalid, + SecurityTokenInvalidAudienceException => TokenInvalidReason.AudienceMismatch, + SecurityTokenInvalidIssuerException => TokenInvalidReason.IssuerMismatch, + _ => TokenInvalidReason.Invalid + }; + } + } diff --git a/src/CodeBeam.UltimateAuth.Server/Services/UAuthSessionQueryService.cs b/src/CodeBeam.UltimateAuth.Server/Services/UAuthSessionQueryService.cs index f2edb58e..04d34fe4 100644 --- a/src/CodeBeam.UltimateAuth.Server/Services/UAuthSessionQueryService.cs +++ b/src/CodeBeam.UltimateAuth.Server/Services/UAuthSessionQueryService.cs @@ -39,7 +39,7 @@ public Task> GetChainsByUserAsync(UserKey userK private ISessionStoreKernel CreateKernel() { - var tenantId = _authFlow.Current.TenantId; + var tenantId = _authFlow.Current.Tenant; return _storeFactory.Create(tenantId); } diff --git a/src/CodeBeam.UltimateAuth.Server/Services/UAuthSessionValidator.cs b/src/CodeBeam.UltimateAuth.Server/Services/UAuthSessionValidator.cs index 9c134022..f9ab8aa0 100644 --- a/src/CodeBeam.UltimateAuth.Server/Services/UAuthSessionValidator.cs +++ b/src/CodeBeam.UltimateAuth.Server/Services/UAuthSessionValidator.cs @@ -26,7 +26,7 @@ public UAuthSessionValidator( // Validate runs before AuthFlowContext is set, do not call _authFlow here. public async Task ValidateSessionAsync(SessionValidationContext context, CancellationToken ct = default) { - var kernel = _storeFactory.Create(context.TenantId); + var kernel = _storeFactory.Create(context.Tenant); var session = await kernel.GetSessionAsync(context.SessionId); if (session is null) @@ -52,7 +52,7 @@ public async Task ValidateSessionAsync(SessionValidatio //if (!session.Device.Matches(context.Device) && _options.Session.DeviceMismatchBehavior == DeviceMismatchBehavior.Reject) // return SessionValidationResult.Invalid(SessionState.DeviceMismatch); - var claims = await _claimsProvider.GetClaimsAsync(context.TenantId, session.UserKey, ct); - return SessionValidationResult.Active(context.TenantId, session.UserKey, session.SessionId, session.ChainId, root.RootId, claims, boundDeviceId: session.Device.DeviceId); + var claims = await _claimsProvider.GetClaimsAsync(context.Tenant, session.UserKey, ct); + return SessionValidationResult.Active(context.Tenant, session.UserKey, session.SessionId, session.ChainId, root.RootId, claims, boundDeviceId: session.Device.DeviceId); } } diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/InMemoryAuthorizationSeedContributor.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/InMemoryAuthorizationSeedContributor.cs index cd459d7d..327aacf1 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/InMemoryAuthorizationSeedContributor.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/InMemoryAuthorizationSeedContributor.cs @@ -1,6 +1,7 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Core.Infrastructure; +using CodeBeam.UltimateAuth.Core.MultiTenancy; namespace CodeBeam.UltimateAuth.Authorization.InMemory; @@ -17,10 +18,10 @@ public InMemoryAuthorizationSeedContributor(IUserRoleStore roles, IInMemoryUserI _ids = ids; } - public async Task SeedAsync(string? tenantId, CancellationToken ct = default) + public async Task SeedAsync(TenantKey tenant, CancellationToken ct = default) { var adminKey = _ids.GetAdminUserId(); - await _roles.AssignAsync(tenantId, adminKey, "Admin", ct); + await _roles.AssignAsync(tenant, adminKey, "Admin", ct); } } diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Stores/InMemoryUserRoleStore.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Stores/InMemoryUserRoleStore.cs index 3f54673f..4616d46c 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Stores/InMemoryUserRoleStore.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Stores/InMemoryUserRoleStore.cs @@ -1,17 +1,18 @@ using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; using System.Collections.Concurrent; namespace CodeBeam.UltimateAuth.Authorization.InMemory; internal sealed class InMemoryUserRoleStore : IUserRoleStore { - private readonly ConcurrentDictionary<(string? TenantId, UserKey UserKey), HashSet> _roles = new(); + private readonly ConcurrentDictionary<(TenantKey Tenant, UserKey UserKey), HashSet> _roles = new(); - public Task> GetRolesAsync(string? tenantId, UserKey userKey, CancellationToken ct = default) + public Task> GetRolesAsync(TenantKey tenant, UserKey userKey, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); - if (_roles.TryGetValue((tenantId, userKey), out var set)) + if (_roles.TryGetValue((tenant, userKey), out var set)) { lock (set) { @@ -22,11 +23,11 @@ public Task> GetRolesAsync(string? tenantId, UserKey return Task.FromResult>(Array.Empty()); } - public Task AssignAsync(string? tenantId, UserKey userKey, string role, CancellationToken ct = default) + public Task AssignAsync(TenantKey tenant, UserKey userKey, string role, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); - var set = _roles.GetOrAdd((tenantId, userKey), _ => new HashSet(StringComparer.OrdinalIgnoreCase)); + var set = _roles.GetOrAdd((tenant, userKey), _ => new HashSet(StringComparer.OrdinalIgnoreCase)); lock (set) { set.Add(role); @@ -35,11 +36,11 @@ public Task AssignAsync(string? tenantId, UserKey userKey, string role, Cancella return Task.CompletedTask; } - public Task RemoveAsync(string? tenantId, UserKey userKey, string role, CancellationToken ct = default) + public Task RemoveAsync(TenantKey tenant, UserKey userKey, string role, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); - if (_roles.TryGetValue((tenantId, userKey), out var set)) + if (_roles.TryGetValue((tenant, userKey), out var set)) { lock (set) { diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Infrastructure/DefaultRolePermissionResolver.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Infrastructure/DefaultRolePermissionResolver.cs index 2d2f3bd9..bd1ef3c1 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Infrastructure/DefaultRolePermissionResolver.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Infrastructure/DefaultRolePermissionResolver.cs @@ -1,4 +1,5 @@ using CodeBeam.UltimateAuth.Authorization.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; namespace CodeBeam.UltimateAuth.Authorization.Reference { @@ -18,7 +19,7 @@ private static readonly IReadOnlyDictionary _map } }; - public Task> ResolveAsync(string? tenantId, IEnumerable roles, CancellationToken ct = default) + public Task> ResolveAsync(TenantKey tenant, IEnumerable roles, CancellationToken ct = default) { var result = new List(); diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Infrastructure/DefaultUserPermissionStore.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Infrastructure/DefaultUserPermissionStore.cs index a7aae0fa..6a44dc7a 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Infrastructure/DefaultUserPermissionStore.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Infrastructure/DefaultUserPermissionStore.cs @@ -1,5 +1,6 @@ using CodeBeam.UltimateAuth.Authorization.Domain; using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; namespace CodeBeam.UltimateAuth.Authorization.Reference { @@ -14,10 +15,10 @@ public DefaultUserPermissionStore(IUserRoleStore roles, IRolePermissionResolver _resolver = resolver; } - public async Task> GetPermissionsAsync(string? tenantId, UserKey userKey, CancellationToken ct = default) + public async Task> GetPermissionsAsync(TenantKey tenant, UserKey userKey, CancellationToken ct = default) { - var roles = await _roles.GetRolesAsync(tenantId, userKey, ct); - return await _resolver.ResolveAsync(tenantId, roles, ct); + var roles = await _roles.GetRolesAsync(tenant, userKey, ct); + return await _resolver.ResolveAsync(tenant, roles, ct); } } diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Services/DefaultUserRoleService.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Services/DefaultUserRoleService.cs index 08129ed4..1dc54560 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Services/DefaultUserRoleService.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Services/DefaultUserRoleService.cs @@ -26,7 +26,7 @@ public async Task AssignAsync(AccessContext context, UserKey targetUserKey, stri var cmd = new AssignUserRoleCommand(Array.Empty(), async innerCt => { - await _store.AssignAsync(context.ResourceTenantId, targetUserKey, role, innerCt); + await _store.AssignAsync(context.ResourceTenant, targetUserKey, role, innerCt); }); await _accessOrchestrator.ExecuteAsync(context, cmd, ct); @@ -42,7 +42,7 @@ public async Task RemoveAsync(AccessContext context, UserKey targetUserKey, stri var cmd = new RemoveUserRoleCommand(Array.Empty(), async innerCt => { - await _store.RemoveAsync(context.ResourceTenantId, targetUserKey, role, innerCt); + await _store.RemoveAsync(context.ResourceTenant, targetUserKey, role, innerCt); }); await _accessOrchestrator.ExecuteAsync(context, cmd, ct); @@ -54,7 +54,7 @@ public async Task> GetRolesAsync(AccessContext conte ct.ThrowIfCancellationRequested(); var cmd = new GetUserRolesCommand(Array.Empty(), - innerCt => _store.GetRolesAsync(context.ResourceTenantId, targetUserKey, innerCt)); + innerCt => _store.GetRolesAsync(context.ResourceTenant, targetUserKey, innerCt)); return await _accessOrchestrator.ExecuteAsync(context, cmd, ct); } diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization/Abstractions/IRolePermissionResolver.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization/Abstractions/IRolePermissionResolver.cs index 7042d45b..d6e09167 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization/Abstractions/IRolePermissionResolver.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization/Abstractions/IRolePermissionResolver.cs @@ -1,9 +1,9 @@ using CodeBeam.UltimateAuth.Authorization.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; -namespace CodeBeam.UltimateAuth.Authorization +namespace CodeBeam.UltimateAuth.Authorization; + +public interface IRolePermissionResolver { - public interface IRolePermissionResolver - { - Task> ResolveAsync(string? tenantId, IEnumerable roles, CancellationToken ct = default); - } + Task> ResolveAsync(TenantKey tenant, IEnumerable roles, CancellationToken ct = default); } diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization/Abstractions/IUserPermissionStore.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization/Abstractions/IUserPermissionStore.cs index f093d400..db519dc2 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization/Abstractions/IUserPermissionStore.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization/Abstractions/IUserPermissionStore.cs @@ -1,9 +1,10 @@ using CodeBeam.UltimateAuth.Authorization.Domain; using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; namespace CodeBeam.UltimateAuth.Authorization; public interface IUserPermissionStore { - Task> GetPermissionsAsync(string? tenantId, UserKey userKey, CancellationToken ct = default); + Task> GetPermissionsAsync(TenantKey tenant, UserKey userKey, CancellationToken ct = default); } diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization/Abstractions/IUserRoleStore.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization/Abstractions/IUserRoleStore.cs index e2aae185..028f5f6a 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization/Abstractions/IUserRoleStore.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization/Abstractions/IUserRoleStore.cs @@ -1,11 +1,11 @@ using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; -namespace CodeBeam.UltimateAuth.Authorization +namespace CodeBeam.UltimateAuth.Authorization; + +public interface IUserRoleStore { - public interface IUserRoleStore - { - Task AssignAsync(string? tenantId, UserKey userKey, string role, CancellationToken ct = default); - Task RemoveAsync(string? tenantId, UserKey userKey, string role, CancellationToken ct = default); - Task> GetRolesAsync(string? tenantId, UserKey userKey, CancellationToken ct = default); - } + Task AssignAsync(TenantKey tenant, UserKey userKey, string role, CancellationToken ct = default); + Task RemoveAsync(TenantKey tenant, UserKey userKey, string role, CancellationToken ct = default); + Task> GetRolesAsync(TenantKey tenant, UserKey userKey, CancellationToken ct = default); } diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization/DefaultAuthorizationClaimsProvider.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization/DefaultAuthorizationClaimsProvider.cs index e5dff350..3e0507ea 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization/DefaultAuthorizationClaimsProvider.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization/DefaultAuthorizationClaimsProvider.cs @@ -1,5 +1,6 @@ using CodeBeam.UltimateAuth.Core; using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; using System.Security.Claims; namespace CodeBeam.UltimateAuth.Authorization @@ -15,10 +16,10 @@ public DefaultAuthorizationClaimsProvider(IUserRoleStore roles, IUserPermissionS _permissions = permissions; } - public async Task GetClaimsAsync(string? tenantId, UserKey userKey, CancellationToken ct = default) + public async Task GetClaimsAsync(TenantKey tenant, UserKey userKey, CancellationToken ct = default) { - var roles = await _roles.GetRolesAsync(tenantId, userKey, ct); - var perms = await _permissions.GetPermissionsAsync(tenantId, userKey, ct); + var roles = await _roles.GetRolesAsync(tenant, userKey, ct); + var perms = await _permissions.GetPermissionsAsync(tenant, userKey, ct); var builder = ClaimsSnapshot.Create(); diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/InMemoryCredentialSeedContributor.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/InMemoryCredentialSeedContributor.cs index 468092e2..b6d5dbcb 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/InMemoryCredentialSeedContributor.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/InMemoryCredentialSeedContributor.cs @@ -1,6 +1,7 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Core.Infrastructure; +using CodeBeam.UltimateAuth.Core.MultiTenancy; using CodeBeam.UltimateAuth.Credentials.Contracts; using CodeBeam.UltimateAuth.Credentials.Reference; @@ -21,18 +22,18 @@ public InMemoryCredentialSeedContributor(ICredentialStore credentials, _hasher = hasher; } - public async Task SeedAsync(string? tenantId, CancellationToken ct = default) + public async Task SeedAsync(TenantKey tenant, CancellationToken ct = default) { - await SeedCredentialAsync("admin", _ids.GetAdminUserId(), tenantId, ct); - await SeedCredentialAsync("user", _ids.GetUserUserId(), tenantId, ct); + await SeedCredentialAsync("admin", _ids.GetAdminUserId(), tenant, ct); + await SeedCredentialAsync("user", _ids.GetUserUserId(), tenant, ct); } - private async Task SeedCredentialAsync(string login, UserKey userKey, string? tenantId, CancellationToken ct) + private async Task SeedCredentialAsync(string login, UserKey userKey, TenantKey tenant, CancellationToken ct) { - if (await _credentials.ExistsAsync(tenantId, userKey, CredentialType.Password, ct)) + if (await _credentials.ExistsAsync(tenant, userKey, CredentialType.Password, ct)) return; - await _credentials.AddAsync(tenantId, + await _credentials.AddAsync(tenant, new PasswordCredential( userKey, login, diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/InMemoryCredentialStore.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/InMemoryCredentialStore.cs index 8bc957b0..66d3d7bf 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/InMemoryCredentialStore.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/InMemoryCredentialStore.cs @@ -1,8 +1,7 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Infrastructure; -using CodeBeam.UltimateAuth.Credentials; +using CodeBeam.UltimateAuth.Core.MultiTenancy; using CodeBeam.UltimateAuth.Credentials.Contracts; -using CodeBeam.UltimateAuth.Credentials.InMemory; using CodeBeam.UltimateAuth.Credentials.Reference; using System.Collections.Concurrent; @@ -10,8 +9,8 @@ namespace CodeBeam.UltimateAuth.Credentials.InMemory; internal sealed class InMemoryCredentialStore : ICredentialStore, ICredentialSecretStore where TUserId : notnull { - private readonly ConcurrentDictionary<(string? TenantId, string Login), InMemoryPasswordCredentialState> _byLogin; - private readonly ConcurrentDictionary<(string? TenantId, TUserId UserId), List>> _byUser; + private readonly ConcurrentDictionary<(TenantKey Tenant, string Login), InMemoryPasswordCredentialState> _byLogin; + private readonly ConcurrentDictionary<(TenantKey Tenant, TUserId UserId), List>> _byUser; private readonly IUAuthPasswordHasher _hasher; private readonly IInMemoryUserIdProvider _userIdProvider; @@ -21,35 +20,35 @@ public InMemoryCredentialStore(IUAuthPasswordHasher hasher, IInMemoryUserIdProvi _hasher = hasher; _userIdProvider = userIdProvider; - _byLogin = new ConcurrentDictionary<(string?, string), InMemoryPasswordCredentialState>(); - _byUser = new ConcurrentDictionary<(string?, TUserId), List>>(); + _byLogin = new ConcurrentDictionary<(TenantKey, string), InMemoryPasswordCredentialState>(); + _byUser = new ConcurrentDictionary<(TenantKey, TUserId), List>>(); } - public Task>> FindByLoginAsync(string? tenantId, string loginIdentifier, CancellationToken ct = default) + public Task>> FindByLoginAsync(TenantKey tenant, string loginIdentifier, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); - if (!_byLogin.TryGetValue((tenantId, loginIdentifier), out var state)) + if (!_byLogin.TryGetValue((tenant, loginIdentifier), out var state)) return Task.FromResult>>(Array.Empty>()); return Task.FromResult>>(new[] { Map(state) }); } - public Task>> GetByUserAsync(string? tenantId, TUserId userId, CancellationToken ct = default) + public Task>> GetByUserAsync(TenantKey tenant, TUserId userId, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); - if (!_byUser.TryGetValue((tenantId, userId), out var list)) + if (!_byUser.TryGetValue((tenant, userId), out var list)) return Task.FromResult>>(Array.Empty>()); return Task.FromResult>>(list.Select(Map).ToArray()); } - public Task>> GetByUserAndTypeAsync(string? tenantId, TUserId userId, CredentialType type, CancellationToken ct = default) + public Task>> GetByUserAndTypeAsync(TenantKey tenant, TUserId userId, CredentialType type, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); - if (!_byUser.TryGetValue((tenantId, userId), out var list)) + if (!_byUser.TryGetValue((tenant, userId), out var list)) return Task.FromResult>>(Array.Empty>()); return Task.FromResult>>( @@ -58,14 +57,14 @@ public Task>> GetByUserAndTypeAsync(str .ToArray()); } - public Task ExistsAsync(string? tenantId, TUserId userId, CredentialType type, CancellationToken ct = default) + public Task ExistsAsync(TenantKey tenant, TUserId userId, CredentialType type, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); - return Task.FromResult(_byUser.TryGetValue((tenantId, userId), out var list) && list.Any(c => c.Type == type)); + return Task.FromResult(_byUser.TryGetValue((tenant, userId), out var list) && list.Any(c => c.Type == type)); } - public Task AddAsync(string? tenantId, ICredential credential, CancellationToken ct = default) + public Task AddAsync(TenantKey tenant, ICredential credential, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); @@ -81,10 +80,10 @@ public Task AddAsync(string? tenantId, ICredential credential, Cancella Metadata = pwd.Metadata }; - _byLogin[(tenantId, pwd.LoginIdentifier)] = state; + _byLogin[(tenant, pwd.LoginIdentifier)] = state; _byUser.AddOrUpdate( - (tenantId, pwd.UserId), + (tenant, pwd.UserId), _ => new List> { state }, (_, list) => { @@ -95,11 +94,11 @@ public Task AddAsync(string? tenantId, ICredential credential, Cancella return Task.CompletedTask; } - public Task UpdateSecurityStateAsync(string? tenantId, TUserId userId, CredentialType type, CredentialSecurityState securityState, CancellationToken ct = default) + public Task UpdateSecurityStateAsync(TenantKey tenant, TUserId userId, CredentialType type, CredentialSecurityState securityState, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); - if (_byUser.TryGetValue((tenantId, userId), out var list)) + if (_byUser.TryGetValue((tenant, userId), out var list)) { var state = list.FirstOrDefault(c => c.Type == type); if (state != null) @@ -109,11 +108,11 @@ public Task UpdateSecurityStateAsync(string? tenantId, TUserId userId, Credentia return Task.CompletedTask; } - public Task UpdateMetadataAsync(string? tenantId, TUserId userId, CredentialType type, CredentialMetadata metadata, CancellationToken ct = default) + public Task UpdateMetadataAsync(TenantKey tenant, TUserId userId, CredentialType type, CredentialMetadata metadata, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); - if (_byUser.TryGetValue((tenantId, userId), out var list)) + if (_byUser.TryGetValue((tenant, userId), out var list)) { var state = list.FirstOrDefault(c => c.Type == type); if (state != null) @@ -123,11 +122,11 @@ public Task UpdateMetadataAsync(string? tenantId, TUserId userId, CredentialType return Task.CompletedTask; } - public Task SetAsync(string? tenantId, TUserId userId, CredentialType type, string secretHash, CancellationToken ct = default) + public Task SetAsync(TenantKey tenant, TUserId userId, CredentialType type, string secretHash, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); - if (_byUser.TryGetValue((tenantId, userId), out var list)) + if (_byUser.TryGetValue((tenant, userId), out var list)) { var state = list.FirstOrDefault(c => c.Type == type); if (state != null) @@ -137,31 +136,31 @@ public Task SetAsync(string? tenantId, TUserId userId, CredentialType type, stri return Task.CompletedTask; } - public Task DeleteAsync(string? tenantId, TUserId userId, CredentialType type, CancellationToken ct = default) + public Task DeleteAsync(TenantKey tenant, TUserId userId, CredentialType type, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); - if (_byUser.TryGetValue((tenantId, userId), out var list)) + if (_byUser.TryGetValue((tenant, userId), out var list)) { var state = list.FirstOrDefault(c => c.Type == type); if (state != null) { list.Remove(state); - _byLogin.TryRemove((tenantId, state.Login), out _); + _byLogin.TryRemove((tenant, state.Login), out _); } } return Task.CompletedTask; } - public Task DeleteByUserAsync(string? tenantId, TUserId userId, CancellationToken ct = default) + public Task DeleteByUserAsync(TenantKey tenant, TUserId userId, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); - if (_byUser.TryRemove((tenantId, userId), out var list)) + if (_byUser.TryRemove((tenant, userId), out var list)) { foreach (var credential in list) - _byLogin.TryRemove((tenantId, credential.Login), out _); + _byLogin.TryRemove((tenant, credential.Login), out _); } return Task.CompletedTask; diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Infrastructure/PasswordUserLifecycleIntegration.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Infrastructure/PasswordUserLifecycleIntegration.cs index 29084e7b..2f5a14eb 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Infrastructure/PasswordUserLifecycleIntegration.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Infrastructure/PasswordUserLifecycleIntegration.cs @@ -1,6 +1,7 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; using CodeBeam.UltimateAuth.Credentials.Contracts; using CodeBeam.UltimateAuth.Users.Abstractions; using CodeBeam.UltimateAuth.Users.Contracts; @@ -20,7 +21,7 @@ public PasswordUserLifecycleIntegration(ICredentialStore credentialStor _clock = clock; } - public async Task OnUserCreatedAsync(string? tenantId, UserKey userKey, object request, CancellationToken ct) + public async Task OnUserCreatedAsync(TenantKey tenant, UserKey userKey, object request, CancellationToken ct) { if (request is not CreateUserRequest r) return; @@ -37,11 +38,11 @@ public async Task OnUserCreatedAsync(string? tenantId, UserKey userKey, object r security: new CredentialSecurityState(CredentialSecurityStatus.Active, null, null, null), metadata: new CredentialMetadata(_clock.UtcNow, _clock.UtcNow, null)); - await _credentialStore.AddAsync(tenantId, credential, ct); + await _credentialStore.AddAsync(tenant, credential, ct); } - public async Task OnUserDeletedAsync(string? tenantId, UserKey userKey, DeleteMode mode, CancellationToken ct) + public async Task OnUserDeletedAsync(TenantKey tenant, UserKey userKey, DeleteMode mode, CancellationToken ct) { - await _credentialStore.DeleteByUserAsync(tenantId, userKey, ct); + await _credentialStore.DeleteByUserAsync(tenant, userKey, ct); } } diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Internal/IUserCredentialsInternalService.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Internal/IUserCredentialsInternalService.cs index 568b7cfc..05035c5e 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Internal/IUserCredentialsInternalService.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Internal/IUserCredentialsInternalService.cs @@ -1,10 +1,10 @@ using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; using CodeBeam.UltimateAuth.Credentials.Contracts; -namespace CodeBeam.UltimateAuth.Credentials.Reference.Internal +namespace CodeBeam.UltimateAuth.Credentials.Reference.Internal; + +internal interface IUserCredentialsInternalService { - internal interface IUserCredentialsInternalService - { - Task DeleteInternalAsync(string? tenantId, UserKey userKey, CancellationToken ct = default); - } + Task DeleteInternalAsync(TenantKey tenant, UserKey userKey, CancellationToken ct = default); } diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Services/DefaultUserCredentialsService.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Services/DefaultUserCredentialsService.cs index 99f0b219..9489d37c 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Services/DefaultUserCredentialsService.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Services/DefaultUserCredentialsService.cs @@ -1,6 +1,7 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; using CodeBeam.UltimateAuth.Credentials.Contracts; using CodeBeam.UltimateAuth.Credentials.Reference.Internal; using CodeBeam.UltimateAuth.Server.Infrastructure; @@ -39,7 +40,7 @@ public async Task GetAllAsync(AccessContext context, Cance if (context.ActorUserKey is not UserKey userKey) throw new UnauthorizedAccessException(); - var creds = await _credentials.GetByUserAsync(context.ResourceTenantId, userKey, innerCt); + var creds = await _credentials.GetByUserAsync(context.ResourceTenant, userKey, innerCt); var dtos = creds .OfType() @@ -71,7 +72,7 @@ public async Task AddAsync(AccessContext context, AddCreden if (context.ActorUserKey is not UserKey userKey) throw new UnauthorizedAccessException(); - var exists = await _credentials.ExistsAsync(context.ResourceTenantId, userKey, request.Type, innerCt); + var exists = await _credentials.ExistsAsync(context.ResourceTenant, userKey, request.Type, innerCt); if (exists) return AddCredentialResult.Fail("credential_already_exists"); @@ -88,7 +89,7 @@ public async Task AddAsync(AccessContext context, AddCreden null, request.Source)); - await _credentials.AddAsync(context.ResourceTenantId, credential, innerCt); + await _credentials.AddAsync(context.ResourceTenant, credential, innerCt); return AddCredentialResult.Success(request.Type); }); @@ -110,7 +111,7 @@ public async Task ChangeAsync(AccessContext context, Cre var hash = _hasher.Hash(request.NewSecret); - await _secrets.SetAsync(context.ResourceTenantId, userKey, type, hash, innerCt); + await _secrets.SetAsync(context.ResourceTenant, userKey, type, hash, innerCt); return ChangeCredentialResult.Success(type); }); @@ -135,7 +136,7 @@ public async Task RevokeAsync(AccessContext context, Cre expiresAt: null, reason: request.Reason); - await _credentials.UpdateSecurityStateAsync(context.ResourceTenantId, userKey, type, security, innerCt); + await _credentials.UpdateSecurityStateAsync(context.ResourceTenant, userKey, type, security, innerCt); return CredentialActionResult.Success(); }); @@ -153,7 +154,7 @@ public async Task ActivateAsync(AccessContext context, C throw new UnauthorizedAccessException(); var security = new CredentialSecurityState(CredentialSecurityStatus.Active); - await _credentials.UpdateSecurityStateAsync(context.ResourceTenantId, userKey, type, security, innerCt); + await _credentials.UpdateSecurityStateAsync(context.ResourceTenant, userKey, type, security, innerCt); return CredentialActionResult.Success(); }); @@ -170,7 +171,7 @@ public async Task BeginResetAsync(AccessContext context, CredentialType type, Be var security = new CredentialSecurityState(CredentialSecurityStatus.ResetRequested, reason: request.Reason); - await _credentials.UpdateSecurityStateAsync(context.ResourceTenantId, userKey, type, security, innerCt); + await _credentials.UpdateSecurityStateAsync(context.ResourceTenant, userKey, type, security, innerCt); return CredentialActionResult.Success(); }); @@ -186,10 +187,10 @@ public async Task CompleteResetAsync(AccessContext context, CredentialType type, var hash = _hasher.Hash(request.NewSecret); - await _secrets.SetAsync(context.ResourceTenantId, userKey, type, hash, innerCt); + await _secrets.SetAsync(context.ResourceTenant, userKey, type, hash, innerCt); var security = new CredentialSecurityState(CredentialSecurityStatus.Active); - await _credentials.UpdateSecurityStateAsync(context.ResourceTenantId, userKey, type, security, innerCt); + await _credentials.UpdateSecurityStateAsync(context.ResourceTenant, userKey, type, security, innerCt); return CredentialActionResult.Success(); }); @@ -206,7 +207,7 @@ public async Task DeleteAsync(AccessContext context, Cre if (context.ActorUserKey is not UserKey userKey) throw new UnauthorizedAccessException(); - await _credentials.DeleteAsync(context.ResourceTenantId, userKey, type, innerCt); + await _credentials.DeleteAsync(context.ResourceTenant, userKey, type, innerCt); return CredentialActionResult.Success(); }); @@ -216,11 +217,11 @@ public async Task DeleteAsync(AccessContext context, Cre // ---------------------------------------- // INTERNAL ONLY - NEVER CALL THEM DIRECTLY // ---------------------------------------- - async Task IUserCredentialsInternalService.DeleteInternalAsync(string? tenantId, UserKey userKey, CancellationToken ct) + async Task IUserCredentialsInternalService.DeleteInternalAsync(TenantKey tenant, UserKey userKey, CancellationToken ct) { ct.ThrowIfCancellationRequested(); - await _credentials.DeleteByUserAsync(tenantId, userKey, ct); + await _credentials.DeleteByUserAsync(tenant, userKey, ct); return CredentialActionResult.Success(); } } diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials/Abstractions/ICredentialSecretStore.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials/Abstractions/ICredentialSecretStore.cs index f74ded1c..8bad6d77 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials/Abstractions/ICredentialSecretStore.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials/Abstractions/ICredentialSecretStore.cs @@ -1,9 +1,10 @@ -using CodeBeam.UltimateAuth.Credentials.Contracts; +using CodeBeam.UltimateAuth.Core.MultiTenancy; +using CodeBeam.UltimateAuth.Credentials.Contracts; namespace CodeBeam.UltimateAuth.Credentials { public interface ICredentialSecretStore { - Task SetAsync(string? tenantId, TUserId userId, CredentialType type, string secretHash, CancellationToken ct = default); + Task SetAsync(TenantKey tenant, TUserId userId, CredentialType type, string secretHash, CancellationToken ct = default); } } diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials/Abstractions/ICredentialStore.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials/Abstractions/ICredentialStore.cs index 7429b1eb..2f3a3b0f 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials/Abstractions/ICredentialStore.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials/Abstractions/ICredentialStore.cs @@ -1,17 +1,18 @@ -using CodeBeam.UltimateAuth.Credentials.Contracts; +using CodeBeam.UltimateAuth.Core.MultiTenancy; +using CodeBeam.UltimateAuth.Credentials.Contracts; namespace CodeBeam.UltimateAuth.Credentials { public interface ICredentialStore { - Task>>FindByLoginAsync(string? tenantId, string loginIdentifier, CancellationToken ct = default); - Task>>GetByUserAsync(string? tenantId, TUserId userId, CancellationToken ct = default); - Task>>GetByUserAndTypeAsync(string? tenantId, TUserId userId, CredentialType type, CancellationToken ct = default); - Task AddAsync(string? tenantId, ICredential credential, CancellationToken ct = default); - Task UpdateSecurityStateAsync(string? tenantId, TUserId userId, CredentialType type, CredentialSecurityState securityState, CancellationToken ct = default); - Task UpdateMetadataAsync(string? tenantId, TUserId userId, CredentialType type, CredentialMetadata metadata, CancellationToken ct = default); - Task DeleteAsync(string? tenantId, TUserId userId, CredentialType type, CancellationToken ct = default); - Task DeleteByUserAsync(string? tenantId, TUserId userId, CancellationToken ct = default); - Task ExistsAsync(string? tenantId, TUserId userId, CredentialType type, CancellationToken ct = default); + Task>>FindByLoginAsync(TenantKey tenant, string loginIdentifier, CancellationToken ct = default); + Task>>GetByUserAsync(TenantKey tenant, TUserId userId, CancellationToken ct = default); + Task>>GetByUserAndTypeAsync(TenantKey tenant, TUserId userId, CredentialType type, CancellationToken ct = default); + Task AddAsync(TenantKey tenant, ICredential credential, CancellationToken ct = default); + Task UpdateSecurityStateAsync(TenantKey tenant, TUserId userId, CredentialType type, CredentialSecurityState securityState, CancellationToken ct = default); + Task UpdateMetadataAsync(TenantKey tenant, TUserId userId, CredentialType type, CredentialMetadata metadata, CancellationToken ct = default); + Task DeleteAsync(TenantKey tenant, TUserId userId, CredentialType type, CancellationToken ct = default); + Task DeleteByUserAsync(TenantKey tenant, TUserId userId, CancellationToken ct = default); + Task ExistsAsync(TenantKey tenant, TUserId userId, CredentialType type, CancellationToken ct = default); } } diff --git a/src/policies/CodeBeam.UltimateAuth.Policies/Policies/RequireActiveUserPolicy.cs b/src/policies/CodeBeam.UltimateAuth.Policies/Policies/RequireActiveUserPolicy.cs index 1fc8f4be..4ad9c65c 100644 --- a/src/policies/CodeBeam.UltimateAuth.Policies/Policies/RequireActiveUserPolicy.cs +++ b/src/policies/CodeBeam.UltimateAuth.Policies/Policies/RequireActiveUserPolicy.cs @@ -17,7 +17,7 @@ public AccessDecision Decide(AccessContext context) if (context.ActorUserKey is null) return AccessDecision.Deny("missing_actor"); - var state = _runtime.GetAsync(context.ActorTenantId, context.ActorUserKey!.Value).GetAwaiter().GetResult(); + var state = _runtime.GetAsync(context.ActorTenant, context.ActorUserKey!.Value).GetAwaiter().GetResult(); if (state == null || !state.Exists || state.IsDeleted) return AccessDecision.Deny("user_not_found"); diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/EntityProjections/SessionChainProjection.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/EntityProjections/SessionChainProjection.cs index d0d5a59f..ce08b22d 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/EntityProjections/SessionChainProjection.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/EntityProjections/SessionChainProjection.cs @@ -1,4 +1,5 @@ using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; namespace CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore { @@ -9,7 +10,7 @@ internal sealed class SessionChainProjection public SessionChainId ChainId { get; set; } = default!; public SessionRootId RootId { get; } - public string? TenantId { get; set; } + public TenantKey Tenant { get; set; } public UserKey UserKey { get; set; } public int RotationCount { get; set; } diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/EntityProjections/SessionProjection.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/EntityProjections/SessionProjection.cs index e2230497..639652c6 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/EntityProjections/SessionProjection.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/EntityProjections/SessionProjection.cs @@ -1,4 +1,5 @@ using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; namespace CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore { @@ -9,7 +10,7 @@ internal sealed class SessionProjection public AuthSessionId SessionId { get; set; } = default!; public SessionChainId ChainId { get; set; } = default!; - public string? TenantId { get; set; } + public TenantKey Tenant { get; set; } public UserKey UserKey { get; set; } = default!; public DateTimeOffset CreatedAt { get; set; } diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/EntityProjections/SessionRootProjection.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/EntityProjections/SessionRootProjection.cs index c49aae0f..0753c61b 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/EntityProjections/SessionRootProjection.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/EntityProjections/SessionRootProjection.cs @@ -1,4 +1,5 @@ using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; namespace CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore { @@ -6,7 +7,7 @@ internal sealed class SessionRootProjection { public long Id { get; set; } public SessionRootId RootId { get; set; } - public string? TenantId { get; set; } + public TenantKey Tenant { get; set; } public UserKey UserKey { get; set; } public bool IsRevoked { get; set; } diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Stores/EfCoreSessionStoreKernelFactory.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Stores/EfCoreSessionStoreKernelFactory.cs index 96c01c37..1c3cda87 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Stores/EfCoreSessionStoreKernelFactory.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Stores/EfCoreSessionStoreKernelFactory.cs @@ -13,9 +13,9 @@ public EfCoreSessionStoreKernelFactory(IServiceProvider sp) _sp = sp; } - public ISessionStoreKernel Create(string? tenantId) + public ISessionStoreKernel Create(TenantKey tenant) { - return ActivatorUtilities.CreateInstance(_sp, new TenantContext(tenantId)); + return ActivatorUtilities.CreateInstance(_sp, new TenantContext(tenant)); } public ISessionStoreKernel CreateGlobal() diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/InMemorySessionStoreKernelFactory.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/InMemorySessionStoreKernelFactory.cs index 7682086e..6bd845eb 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/InMemorySessionStoreKernelFactory.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/InMemorySessionStoreKernelFactory.cs @@ -1,17 +1,15 @@ using CodeBeam.UltimateAuth.Core.Abstractions; +using CodeBeam.UltimateAuth.Core.MultiTenancy; using System.Collections.Concurrent; namespace CodeBeam.UltimateAuth.Sessions.InMemory; public sealed class InMemorySessionStoreKernelFactory : ISessionStoreKernelFactory { - private readonly ConcurrentDictionary _kernels = new(); + private readonly ConcurrentDictionary _kernels = new(); - public ISessionStoreKernel Create(string? tenantId) + public ISessionStoreKernel Create(TenantKey tenant) { - //var key = TenantKey.Normalize(tenantId); - var key = tenantId ?? "__default__"; - - return _kernels.GetOrAdd(key, _ => new InMemorySessionStoreKernel()); + return _kernels.GetOrAdd(tenant, _ => new InMemorySessionStoreKernel()); } } diff --git a/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/EfCoreTokenStore.cs b/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/EfCoreTokenStore.cs index 05dc0c1a..1c66fde0 100644 --- a/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/EfCoreTokenStore.cs +++ b/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/EfCoreTokenStore.cs @@ -1,5 +1,6 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; using CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; @@ -12,14 +13,14 @@ public EfCoreRefreshTokenStore(UltimateAuthTokenDbContext db, IUserIdConverterRe _db = db; } - public async Task StoreAsync(string? tenantId, StoredRefreshToken token, CancellationToken ct = default) + public async Task StoreAsync(TenantKey tenantId, StoredRefreshToken token, CancellationToken ct = default) { - if (token.TenantId != tenantId) + if (token.Tenant != tenantId) throw new InvalidOperationException("TenantId mismatch between context and token."); _db.RefreshTokens.Add(new RefreshTokenProjection { - TenantId = tenantId, + Tenant = tenantId, TokenHash = token.TokenHash, UserKey = token.UserKey, SessionId = token.SessionId, @@ -31,13 +32,13 @@ public async Task StoreAsync(string? tenantId, StoredRefreshToken token, Cancell await _db.SaveChangesAsync(ct); } - public async Task FindByHashAsync(string? tenantId, string tokenHash, CancellationToken ct = default) + public async Task FindByHashAsync(TenantKey tenant, string tokenHash, CancellationToken ct = default) { var e = await _db.RefreshTokens .AsNoTracking() .SingleOrDefaultAsync( x => x.TokenHash == tokenHash && - x.TenantId == tenantId, + x.Tenant == tenant, ct); if (e is null) @@ -45,7 +46,7 @@ public async Task StoreAsync(string? tenantId, StoredRefreshToken token, Cancell return new StoredRefreshToken { - TenantId = e.TenantId, + Tenant = e.Tenant, TokenHash = e.TokenHash, UserKey = e.UserKey, SessionId = e.SessionId, @@ -56,12 +57,12 @@ public async Task StoreAsync(string? tenantId, StoredRefreshToken token, Cancell }; } - public Task RevokeAsync(string? tenantId, string tokenHash, DateTimeOffset revokedAt, string? replacedByTokenHash = null, CancellationToken ct = default) + public Task RevokeAsync(TenantKey tenant, string tokenHash, DateTimeOffset revokedAt, string? replacedByTokenHash = null, CancellationToken ct = default) { var query = _db.RefreshTokens .Where(x => x.TokenHash == tokenHash && - x.TenantId == tenantId && + x.Tenant == tenant && x.RevokedAt == null); if (replacedByTokenHash == null) @@ -76,28 +77,28 @@ public Task RevokeAsync(string? tenantId, string tokenHash, DateTimeOffset revok ct); } - public Task RevokeBySessionAsync(string? tenantId, AuthSessionId sessionId, DateTimeOffset revokedAt, CancellationToken ct = default) + public Task RevokeBySessionAsync(TenantKey tenant, AuthSessionId sessionId, DateTimeOffset revokedAt, CancellationToken ct = default) => _db.RefreshTokens .Where(x => - x.TenantId == tenantId && + x.Tenant == tenant && x.SessionId == sessionId.Value && x.RevokedAt == null) .ExecuteUpdateAsync(x => x.SetProperty(t => t.RevokedAt, revokedAt), ct); - public Task RevokeByChainAsync(string? tenantId, SessionChainId chainId, DateTimeOffset revokedAt, CancellationToken ct = default) + public Task RevokeByChainAsync(TenantKey tenant, SessionChainId chainId, DateTimeOffset revokedAt, CancellationToken ct = default) => _db.RefreshTokens .Where(x => - x.TenantId == tenantId && + x.Tenant == tenant && x.ChainId == chainId && x.RevokedAt == null) .ExecuteUpdateAsync(x => x.SetProperty(t => t.RevokedAt, revokedAt), ct); - public Task RevokeAllForUserAsync(string? tenantId, UserKey userKey, DateTimeOffset revokedAt, CancellationToken ct = default) + public Task RevokeAllForUserAsync(TenantKey tenant, UserKey userKey, DateTimeOffset revokedAt, CancellationToken ct = default) { return _db.RefreshTokens .Where(x => - x.TenantId == tenantId && + x.Tenant == tenant && x.UserKey == userKey && x.RevokedAt == null) .ExecuteUpdateAsync(x => x.SetProperty(t => t.RevokedAt, revokedAt), ct); diff --git a/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/Projections/RefreshTokenProjection.cs b/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/Projections/RefreshTokenProjection.cs index 14a759a2..138f800d 100644 --- a/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/Projections/RefreshTokenProjection.cs +++ b/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/Projections/RefreshTokenProjection.cs @@ -1,4 +1,5 @@ using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; namespace CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore; @@ -6,7 +7,7 @@ namespace CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore; internal sealed class RefreshTokenProjection { public long Id { get; set; } // Surrogate PK - public string? TenantId { get; set; } + public TenantKey Tenant { get; set; } public string TokenHash { get; set; } = default!; public UserKey UserKey { get; set; } = default!; diff --git a/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/Projections/RevokedIdTokenProjection.cs b/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/Projections/RevokedIdTokenProjection.cs index 5499bace..0dc5a1e5 100644 --- a/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/Projections/RevokedIdTokenProjection.cs +++ b/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/Projections/RevokedIdTokenProjection.cs @@ -1,9 +1,11 @@ -namespace CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore; +using CodeBeam.UltimateAuth.Core.MultiTenancy; + +namespace CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore; internal sealed class RevokedTokenIdProjection { public long Id { get; set; } - public string? TenantId { get; set; } + public TenantKey Tenant { get; set; } public string Jti { get; set; } = default!; diff --git a/src/tokens/CodeBeam.UltimateAuth.Tokens.InMemory/InMemoryRefreshTokenStore.cs b/src/tokens/CodeBeam.UltimateAuth.Tokens.InMemory/InMemoryRefreshTokenStore.cs index 3d4b09b5..408f8141 100644 --- a/src/tokens/CodeBeam.UltimateAuth.Tokens.InMemory/InMemoryRefreshTokenStore.cs +++ b/src/tokens/CodeBeam.UltimateAuth.Tokens.InMemory/InMemoryRefreshTokenStore.cs @@ -1,6 +1,7 @@ using System.Collections.Concurrent; using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; namespace CodeBeam.UltimateAuth.Tokens.InMemory; @@ -10,25 +11,25 @@ public sealed class InMemoryRefreshTokenStore : IRefreshTokenStore private readonly ConcurrentDictionary _tokens = new(); - public Task StoreAsync(string? tenantId, StoredRefreshToken token, CancellationToken ct = default) + public Task StoreAsync(TenantKey tenant, StoredRefreshToken token, CancellationToken ct = default) { - var key = new TokenKey(NormalizeTenant(tenantId), token.TokenHash); + var key = new TokenKey(NormalizeTenant(tenant), token.TokenHash); _tokens[key] = token; return Task.CompletedTask; } - public Task FindByHashAsync(string? tenantId, string tokenHash, CancellationToken ct = default) + public Task FindByHashAsync(TenantKey tenant, string tokenHash, CancellationToken ct = default) { - var key = new TokenKey(NormalizeTenant(tenantId), tokenHash); + var key = new TokenKey(NormalizeTenant(tenant), tokenHash); _tokens.TryGetValue(key, out var token); return Task.FromResult(token); } - public Task RevokeAsync(string? tenantId, string tokenHash, DateTimeOffset revokedAt, string? replacedByTokenHash = null, CancellationToken ct = default) + public Task RevokeAsync(TenantKey tenant, string tokenHash, DateTimeOffset revokedAt, string? replacedByTokenHash = null, CancellationToken ct = default) { - var key = new TokenKey(NormalizeTenant(tenantId), tokenHash); + var key = new TokenKey(NormalizeTenant(tenant), tokenHash); if (_tokens.TryGetValue(key, out var token) && !token.IsRevoked) { @@ -42,10 +43,8 @@ public Task RevokeAsync(string? tenantId, string tokenHash, DateTimeOffset revok return Task.CompletedTask; } - public Task RevokeBySessionAsync(string? tenantId, AuthSessionId sessionId, DateTimeOffset revokedAt, CancellationToken ct = default) + public Task RevokeBySessionAsync(TenantKey tenant, AuthSessionId sessionId, DateTimeOffset revokedAt, CancellationToken ct = default) { - var tenant = NormalizeTenant(tenantId); - foreach (var (key, token) in _tokens) { if (key.TenantId == tenant && @@ -59,10 +58,8 @@ public Task RevokeBySessionAsync(string? tenantId, AuthSessionId sessionId, Date return Task.CompletedTask; } - public Task RevokeByChainAsync(string? tenantId, SessionChainId chainId, DateTimeOffset revokedAt, CancellationToken ct = default) + public Task RevokeByChainAsync(TenantKey tenant, SessionChainId chainId, DateTimeOffset revokedAt, CancellationToken ct = default) { - var tenant = NormalizeTenant(tenantId); - foreach (var (key, token) in _tokens) { if (key.TenantId == tenant && @@ -76,10 +73,8 @@ public Task RevokeByChainAsync(string? tenantId, SessionChainId chainId, DateTim return Task.CompletedTask; } - public Task RevokeAllForUserAsync(string? tenantId, UserKey userKey, DateTimeOffset revokedAt, CancellationToken ct = default) + public Task RevokeAllForUserAsync(TenantKey tenant, UserKey userKey, DateTimeOffset revokedAt, CancellationToken ct = default) { - var tenant = NormalizeTenant(tenantId); - foreach (var (key, token) in _tokens) { if (key.TenantId == tenant && diff --git a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/RegisterUserRequest.cs b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/RegisterUserRequest.cs index e8482441..56b11369 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/RegisterUserRequest.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/RegisterUserRequest.cs @@ -1,4 +1,6 @@ -namespace CodeBeam.UltimateAuth.Users.Contracts +using CodeBeam.UltimateAuth.Core.MultiTenancy; + +namespace CodeBeam.UltimateAuth.Users.Contracts { /// /// Request to register a new user with credentials. @@ -20,7 +22,7 @@ public sealed class RegisterUserRequest /// /// Optional tenant identifier. /// - public string? TenantId { get; init; } + public TenantKey Tenant { get; init; } /// /// Optional initial claims or metadata. diff --git a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Infrastructure/InMemoryUserSecurityStateProvider.cs b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Infrastructure/InMemoryUserSecurityStateProvider.cs index bd7b9ddb..55077d7b 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Infrastructure/InMemoryUserSecurityStateProvider.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Infrastructure/InMemoryUserSecurityStateProvider.cs @@ -1,13 +1,12 @@ -using CodeBeam.UltimateAuth.Users; +using CodeBeam.UltimateAuth.Core.MultiTenancy; -namespace CodeBeam.UltimateAuth.Users.InMemory +namespace CodeBeam.UltimateAuth.Users.InMemory; + +internal sealed class InMemoryUserSecurityStateProvider : IUserSecurityStateProvider { - internal sealed class InMemoryUserSecurityStateProvider : IUserSecurityStateProvider + public Task GetAsync(TenantKey tenant, TUserId userId, CancellationToken ct = default) { - public Task GetAsync(string? tenantId, TUserId userId, CancellationToken ct = default) - { - // InMemory default: no MFA, no lockout, no risk signals - return Task.FromResult(null); - } + // InMemory default: no MFA, no lockout, no risk signals + return Task.FromResult(null); } } diff --git a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Infrastructure/InMemoryUserSeedContributor.cs b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Infrastructure/InMemoryUserSeedContributor.cs index 4a04f938..a0945010 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Infrastructure/InMemoryUserSeedContributor.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Infrastructure/InMemoryUserSeedContributor.cs @@ -1,6 +1,7 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Core.Infrastructure; +using CodeBeam.UltimateAuth.Core.MultiTenancy; using CodeBeam.UltimateAuth.Users.Contracts; using CodeBeam.UltimateAuth.Users.Reference; @@ -30,18 +31,18 @@ public InMemoryUserSeedContributor( _clock = clock; } - public async Task SeedAsync(string? tenantId, CancellationToken ct = default) + public async Task SeedAsync(TenantKey tenant, CancellationToken ct = default) { - await SeedUserAsync(tenantId, _ids.GetAdminUserId(), "Administrator", "admin", ct); - await SeedUserAsync(tenantId, _ids.GetUserUserId(), "User", "user", ct); + await SeedUserAsync(tenant, _ids.GetAdminUserId(), "Administrator", "admin", ct); + await SeedUserAsync(tenant, _ids.GetUserUserId(), "User", "user", ct); } - private async Task SeedUserAsync(string? tenantId, UserKey userKey, string displayName, string username, CancellationToken ct) + private async Task SeedUserAsync(TenantKey tenant, UserKey userKey, string displayName, string username, CancellationToken ct) { - if (await _lifecycle.ExistsAsync(tenantId, userKey, ct)) + if (await _lifecycle.ExistsAsync(tenant, userKey, ct)) return; - await _lifecycle.CreateAsync(tenantId, + await _lifecycle.CreateAsync(tenant, new UserLifecycle { UserKey = userKey, @@ -49,7 +50,7 @@ await _lifecycle.CreateAsync(tenantId, CreatedAt = _clock.UtcNow }, ct); - await _profiles.CreateAsync(tenantId, + await _profiles.CreateAsync(tenant, new UserProfile { UserKey = userKey, @@ -57,7 +58,7 @@ await _profiles.CreateAsync(tenantId, CreatedAt = _clock.UtcNow }, ct); - await _identifiers.CreateAsync(tenantId, + await _identifiers.CreateAsync(tenant, new UserIdentifier { UserKey = userKey, diff --git a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserIdentifierStore.cs b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserIdentifierStore.cs index ec9f36bf..9358fa82 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserIdentifierStore.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserIdentifierStore.cs @@ -1,5 +1,6 @@ using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; using CodeBeam.UltimateAuth.Users.Contracts; using CodeBeam.UltimateAuth.Users.Reference; @@ -7,16 +8,16 @@ namespace CodeBeam.UltimateAuth.Users.InMemory { public sealed class InMemoryUserIdentifierStore : IUserIdentifierStore { - private readonly Dictionary<(string? TenantId, UserIdentifierType Type, string Value), UserIdentifier> _store = new(); + private readonly Dictionary<(TenantKey Tenant, UserIdentifierType Type, string Value), UserIdentifier> _store = new(); - public Task ExistsAsync(string? tenantId, UserIdentifierType type, string value, CancellationToken ct = default) + public Task ExistsAsync(TenantKey tenant, UserIdentifierType type, string value, CancellationToken ct = default) { - return Task.FromResult(_store.TryGetValue((tenantId, type, value), out var id) && !id.IsDeleted); + return Task.FromResult(_store.TryGetValue((tenant, type, value), out var id) && !id.IsDeleted); } - public Task GetAsync(string? tenantId, UserIdentifierType type, string value, CancellationToken ct = default) + public Task GetAsync(TenantKey tenant, UserIdentifierType type, string value, CancellationToken ct = default) { - if (!_store.TryGetValue((tenantId, type, value), out var id)) + if (!_store.TryGetValue((tenant, type, value), out var id)) return Task.FromResult(null); if (id.IsDeleted) @@ -25,10 +26,10 @@ public Task ExistsAsync(string? tenantId, UserIdentifierType type, string return Task.FromResult(id); } - public Task> GetByUserAsync(string? tenantId, UserKey userKey, CancellationToken ct = default) + public Task> GetByUserAsync(TenantKey tenant, UserKey userKey, CancellationToken ct = default) { var result = _store.Values - .Where(x => x.TenantId == tenantId) + .Where(x => x.Tenant == tenant) .Where(x => x.UserKey == userKey) .Where(x => !x.IsDeleted) .OrderByDescending(x => x.IsPrimary) @@ -39,9 +40,9 @@ public Task> GetByUserAsync(string? tenantId, User return Task.FromResult>(result); } - public Task CreateAsync(string? tenantId, UserIdentifier identifier, CancellationToken ct = default) + public Task CreateAsync(TenantKey tenant, UserIdentifier identifier, CancellationToken ct = default) { - var key = (tenantId, identifier.Type, identifier.Value); + var key = (tenant, identifier.Type, identifier.Value); if (_store.TryGetValue(key, out var existing) && !existing.IsDeleted) throw new InvalidOperationException("Identifier already exists."); @@ -50,19 +51,19 @@ public Task CreateAsync(string? tenantId, UserIdentifier identifier, Cancellatio return Task.CompletedTask; } - public Task UpdateValueAsync(string? tenantId, UserIdentifierType type, string oldValue, string newValue, DateTimeOffset updatedAt, CancellationToken ct = default) + public Task UpdateValueAsync(TenantKey tenant, UserIdentifierType type, string oldValue, string newValue, DateTimeOffset updatedAt, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); if (string.Equals(oldValue, newValue, StringComparison.Ordinal)) throw new InvalidOperationException("identifier_value_unchanged"); - var oldKey = (tenantId, type, oldValue); + var oldKey = (tenant, type, oldValue); if (!_store.TryGetValue(oldKey, out var identifier) || identifier.IsDeleted) throw new InvalidOperationException("identifier_not_found"); - var newKey = (tenantId, type, newValue); + var newKey = (tenant, type, newValue); if (_store.ContainsKey(newKey)) throw new InvalidOperationException("identifier_value_already_exists"); @@ -79,9 +80,9 @@ public Task UpdateValueAsync(string? tenantId, UserIdentifierType type, string o return Task.CompletedTask; } - public Task MarkVerifiedAsync(string? tenantId, UserIdentifierType type, string value, DateTimeOffset verifiedAt, CancellationToken ct = default) + public Task MarkVerifiedAsync(TenantKey tenant, UserIdentifierType type, string value, DateTimeOffset verifiedAt, CancellationToken ct = default) { - var key = (tenantId, type, value); + var key = (tenant, type, value); if (!_store.TryGetValue(key, out var id) || id.IsDeleted) throw new InvalidOperationException("Identifier not found."); @@ -95,10 +96,10 @@ public Task MarkVerifiedAsync(string? tenantId, UserIdentifierType type, string return Task.CompletedTask; } - public Task SetPrimaryAsync(string? tenantId, UserKey userKey, UserIdentifierType type, string value, CancellationToken ct = default) + public Task SetPrimaryAsync(TenantKey tenant, UserKey userKey, UserIdentifierType type, string value, CancellationToken ct = default) { foreach (var id in _store.Values.Where(x => - x.TenantId == tenantId && + x.Tenant == tenant && x.UserKey == userKey && x.Type == type && !x.IsDeleted && @@ -107,7 +108,7 @@ public Task SetPrimaryAsync(string? tenantId, UserKey userKey, UserIdentifierTyp id.IsPrimary = false; } - var key = (tenantId, type, value); + var key = (tenant, type, value); if (!_store.TryGetValue(key, out var target) || target.IsDeleted) throw new InvalidOperationException("Identifier not found."); @@ -116,9 +117,9 @@ public Task SetPrimaryAsync(string? tenantId, UserKey userKey, UserIdentifierTyp return Task.CompletedTask; } - public Task UnsetPrimaryAsync(string? tenantId, UserKey userKey, UserIdentifierType type, string value, CancellationToken ct = default) + public Task UnsetPrimaryAsync(TenantKey tenant, UserKey userKey, UserIdentifierType type, string value, CancellationToken ct = default) { - var key = (tenantId, type, value); + var key = (tenant, type, value); if (!_store.TryGetValue(key, out var target) || target.IsDeleted) throw new InvalidOperationException("Identifier not found."); @@ -127,9 +128,9 @@ public Task UnsetPrimaryAsync(string? tenantId, UserKey userKey, UserIdentifierT return Task.CompletedTask; } - public Task DeleteAsync(string? tenantId, UserIdentifierType type, string value, DeleteMode mode, DateTimeOffset deletedAt, CancellationToken ct = default) + public Task DeleteAsync(TenantKey tenant, UserIdentifierType type, string value, DeleteMode mode, DateTimeOffset deletedAt, CancellationToken ct = default) { - var key = (tenantId, type, value); + var key = (tenant, type, value); if (!_store.TryGetValue(key, out var id)) return Task.CompletedTask; @@ -150,10 +151,10 @@ public Task DeleteAsync(string? tenantId, UserIdentifierType type, string value, return Task.CompletedTask; } - public Task DeleteByUserAsync(string? tenantId, UserKey userKey, DeleteMode mode, DateTimeOffset deletedAt, CancellationToken ct = default) + public Task DeleteByUserAsync(TenantKey tenant, UserKey userKey, DeleteMode mode, DateTimeOffset deletedAt, CancellationToken ct = default) { var identifiers = _store.Values - .Where(x => x.TenantId == tenantId) + .Where(x => x.Tenant == tenant) .Where(x => x.UserKey == userKey) .ToList(); @@ -161,7 +162,7 @@ public Task DeleteByUserAsync(string? tenantId, UserKey userKey, DeleteMode mode { if (mode == DeleteMode.Hard) { - _store.Remove((tenantId, id.Type, id.Value)); + _store.Remove((tenant, id.Type, id.Value)); } else { diff --git a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserLifecycleStore.cs b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserLifecycleStore.cs index 0302cc22..aea09138 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserLifecycleStore.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserLifecycleStore.cs @@ -1,5 +1,6 @@ using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; using CodeBeam.UltimateAuth.Users.Contracts; using CodeBeam.UltimateAuth.Users.Reference; @@ -7,16 +8,16 @@ namespace CodeBeam.UltimateAuth.Users.InMemory; public sealed class InMemoryUserLifecycleStore : IUserLifecycleStore { - private readonly Dictionary<(string?, UserKey), UserLifecycle> _store = new(); + private readonly Dictionary<(TenantKey, UserKey), UserLifecycle> _store = new(); - public Task ExistsAsync(string? tenantId, UserKey userKey, CancellationToken ct = default) + public Task ExistsAsync(TenantKey tenant, UserKey userKey, CancellationToken ct = default) { - return Task.FromResult(_store.TryGetValue((tenantId, userKey), out var entity) && !entity.IsDeleted); + return Task.FromResult(_store.TryGetValue((tenant, userKey), out var entity) && !entity.IsDeleted); } - public Task GetAsync(string? tenantId, UserKey userKey, CancellationToken ct = default) + public Task GetAsync(TenantKey tenant, UserKey userKey, CancellationToken ct = default) { - if (!_store.TryGetValue((tenantId, userKey), out var entity)) + if (!_store.TryGetValue((tenant, userKey), out var entity)) return Task.FromResult(null); if (entity.IsDeleted) @@ -25,11 +26,11 @@ public Task ExistsAsync(string? tenantId, UserKey userKey, CancellationTok return Task.FromResult(entity); } - public Task> QueryAsync(string? tenantId, UserLifecycleQuery query, CancellationToken ct = default) + public Task> QueryAsync(TenantKey tenant, UserLifecycleQuery query, CancellationToken ct = default) { var baseQuery = _store.Values .Where(x => x?.UserKey != null) - .Where(x => x.TenantId == tenantId); + .Where(x => x.Tenant == tenant); if (!query.IncludeDeleted) baseQuery = baseQuery.Where(x => !x.IsDeleted); @@ -49,9 +50,9 @@ public Task> QueryAsync(string? tenantId, UserLifecyc return Task.FromResult(new PagedResult(items, totalCount)); } - public Task CreateAsync(string? tenantId, UserLifecycle lifecycle, CancellationToken ct = default) + public Task CreateAsync(TenantKey tenant, UserLifecycle lifecycle, CancellationToken ct = default) { - var key = (tenantId, lifecycle.UserKey); + var key = (tenant, lifecycle.UserKey); if (_store.ContainsKey(key)) throw new InvalidOperationException("UserLifecycle already exists."); @@ -60,9 +61,9 @@ public Task CreateAsync(string? tenantId, UserLifecycle lifecycle, CancellationT return Task.CompletedTask; } - public Task ChangeStatusAsync(string? tenantId, UserKey userKey, UserStatus newStatus, DateTimeOffset updatedAt, CancellationToken ct = default) + public Task ChangeStatusAsync(TenantKey tenant, UserKey userKey, UserStatus newStatus, DateTimeOffset updatedAt, CancellationToken ct = default) { - if (!_store.TryGetValue((tenantId, userKey), out var entity) || entity.IsDeleted) + if (!_store.TryGetValue((tenant, userKey), out var entity) || entity.IsDeleted) throw new InvalidOperationException("UserLifecycle not found."); entity.Status = newStatus; @@ -71,9 +72,9 @@ public Task ChangeStatusAsync(string? tenantId, UserKey userKey, UserStatus newS return Task.CompletedTask; } - public Task ChangeSecurityStampAsync(string? tenantId, UserKey userKey, Guid newSecurityStamp, DateTimeOffset updatedAt, CancellationToken ct = default) + public Task ChangeSecurityStampAsync(TenantKey tenant, UserKey userKey, Guid newSecurityStamp, DateTimeOffset updatedAt, CancellationToken ct = default) { - if (!_store.TryGetValue((tenantId, userKey), out var entity) || entity.IsDeleted) + if (!_store.TryGetValue((tenant, userKey), out var entity) || entity.IsDeleted) throw new InvalidOperationException("UserLifecycle not found."); if (entity.SecurityStamp == newSecurityStamp) @@ -85,9 +86,9 @@ public Task ChangeSecurityStampAsync(string? tenantId, UserKey userKey, Guid new return Task.CompletedTask; } - public Task DeleteAsync(string? tenantId, UserKey userKey, DeleteMode mode, DateTimeOffset deletedAt, CancellationToken ct = default) + public Task DeleteAsync(TenantKey tenant, UserKey userKey, DeleteMode mode, DateTimeOffset deletedAt, CancellationToken ct = default) { - var key = (tenantId, userKey); + var key = (tenant, userKey); if (!_store.TryGetValue(key, out var entity)) return Task.CompletedTask; diff --git a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserProfileStore.cs b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserProfileStore.cs index 162f1100..b0ee2678 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserProfileStore.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserProfileStore.cs @@ -1,21 +1,22 @@ using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; using CodeBeam.UltimateAuth.Users.Reference; namespace CodeBeam.UltimateAuth.Users.InMemory; public sealed class InMemoryUserProfileStore : IUserProfileStore { - private readonly Dictionary<(string? TenantId, UserKey UserKey), UserProfile> _store = new(); + private readonly Dictionary<(TenantKey Tenant, UserKey UserKey), UserProfile> _store = new(); - public Task ExistsAsync(string? tenantId, UserKey userKey, CancellationToken ct = default) + public Task ExistsAsync(TenantKey tenant, UserKey userKey, CancellationToken ct = default) { - return Task.FromResult(_store.TryGetValue((tenantId, userKey), out var profile) && profile.DeletedAt == null); + return Task.FromResult(_store.TryGetValue((tenant, userKey), out var profile) && profile.DeletedAt == null); } - public Task GetAsync(string? tenantId, UserKey userKey, CancellationToken ct = default) + public Task GetAsync(TenantKey tenant, UserKey userKey, CancellationToken ct = default) { - if (!_store.TryGetValue((tenantId, userKey), out var profile)) + if (!_store.TryGetValue((tenant, userKey), out var profile)) return Task.FromResult(null); if (profile.DeletedAt != null) @@ -24,10 +25,10 @@ public Task ExistsAsync(string? tenantId, UserKey userKey, CancellationTok return Task.FromResult(profile); } - public Task> QueryAsync(string? tenantId, UserProfileQuery query, CancellationToken ct = default) + public Task> QueryAsync(TenantKey tenant, UserProfileQuery query, CancellationToken ct = default) { var baseQuery = _store.Values - .Where(x => x.TenantId == tenantId); + .Where(x => x.Tenant == tenant); if (!query.IncludeDeleted) baseQuery = baseQuery.Where(x => x.DeletedAt == null); @@ -44,9 +45,9 @@ public Task> QueryAsync(string? tenantId, UserProfileQu return Task.FromResult(new PagedResult(items, totalCount)); } - public Task CreateAsync(string? tenantId, UserProfile profile, CancellationToken ct = default) + public Task CreateAsync(TenantKey tenant, UserProfile profile, CancellationToken ct = default) { - var key = (tenantId, profile.UserKey); + var key = (tenant, profile.UserKey); if (_store.ContainsKey(key)) throw new InvalidOperationException("UserProfile already exists."); @@ -55,9 +56,9 @@ public Task CreateAsync(string? tenantId, UserProfile profile, CancellationToken return Task.CompletedTask; } - public Task UpdateAsync(string? tenantId, UserKey userKey, UserProfileUpdate update, DateTimeOffset updatedAt, CancellationToken ct = default) + public Task UpdateAsync(TenantKey tenant, UserKey userKey, UserProfileUpdate update, DateTimeOffset updatedAt, CancellationToken ct = default) { - var key = (tenantId, userKey); + var key = (tenant, userKey); if (!_store.TryGetValue(key, out var existing) || existing.DeletedAt != null) throw new InvalidOperationException("UserProfile not found."); @@ -78,9 +79,9 @@ public Task UpdateAsync(string? tenantId, UserKey userKey, UserProfileUpdate upd return Task.CompletedTask; } - public Task DeleteAsync(string? tenantId, UserKey userKey, DeleteMode mode, DateTimeOffset deletedAt, CancellationToken ct = default) + public Task DeleteAsync(TenantKey tenant, UserKey userKey, DeleteMode mode, DateTimeOffset deletedAt, CancellationToken ct = default) { - var key = (tenantId, userKey); + var key = (tenant, userKey); if (!_store.TryGetValue(key, out var profile)) return Task.CompletedTask; diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Domain/UserIdentifier.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Domain/UserIdentifier.cs index 96c2f27f..e4ff872d 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Domain/UserIdentifier.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Domain/UserIdentifier.cs @@ -1,11 +1,12 @@ using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; using CodeBeam.UltimateAuth.Users.Contracts; namespace CodeBeam.UltimateAuth.Users.Reference; public sealed record UserIdentifier { - public string? TenantId { get; set; } + public TenantKey Tenant { get; set; } public UserKey UserKey { get; init; } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Domain/UserLifecycle.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Domain/UserLifecycle.cs index 017b25b5..085a2e71 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Domain/UserLifecycle.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Domain/UserLifecycle.cs @@ -1,11 +1,12 @@ using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; using CodeBeam.UltimateAuth.Users.Contracts; namespace CodeBeam.UltimateAuth.Users.Reference; public sealed record class UserLifecycle { - public string? TenantId { get; set; } + public TenantKey Tenant { get; set; } public UserKey UserKey { get; init; } = default!; diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Domain/UserProfile.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Domain/UserProfile.cs index b8cb4a70..15c9ee22 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Domain/UserProfile.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Domain/UserProfile.cs @@ -1,11 +1,12 @@ using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; namespace CodeBeam.UltimateAuth.Users.Reference; // TODO: Multi profile (e.g., public profiles, private profiles, profiles per application, etc. with ProfileKey) public sealed record class UserProfile { - public string? TenantId { get; set; } + public TenantKey Tenant { get; set; } public UserKey UserKey { get; init; } = default!; diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Services/UserApplicationService.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Services/UserApplicationService.cs index 8d396f83..c22ffd3e 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Services/UserApplicationService.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Services/UserApplicationService.cs @@ -1,6 +1,7 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; using CodeBeam.UltimateAuth.Server.Infrastructure; using CodeBeam.UltimateAuth.Users.Abstractions; using CodeBeam.UltimateAuth.Users.Contracts; @@ -40,7 +41,7 @@ public async Task GetMeAsync(AccessContext context, CancellationTok if (context.ActorUserKey is null) throw new UnauthorizedAccessException(); - return await BuildUserViewAsync(context.ResourceTenantId, context.ActorUserKey.Value, innerCt); + return await BuildUserViewAsync(context.ResourceTenant, context.ActorUserKey.Value, innerCt); }); return await _accessOrchestrator.ExecuteAsync(context, command, ct); @@ -53,7 +54,7 @@ public async Task GetUserProfileAsync(AccessContext context, Cancel // Target user MUST exist in context var targetUserKey = context.GetTargetUserKey(); - return await BuildUserViewAsync(context.ResourceTenantId, targetUserKey, innerCt); + return await BuildUserViewAsync(context.ResourceTenant, targetUserKey, innerCt); }); @@ -72,7 +73,7 @@ public async Task CreateUserAsync(AccessContext context, Creat return UserCreateResult.Failed("primary_identifier_type_required"); } - await _lifecycleStore.CreateAsync(context.ResourceTenantId, + await _lifecycleStore.CreateAsync(context.ResourceTenant, new UserLifecycle { UserKey = userKey, @@ -81,7 +82,7 @@ await _lifecycleStore.CreateAsync(context.ResourceTenantId, }, innerCt); - await _profileStore.CreateAsync(context.ResourceTenantId, + await _profileStore.CreateAsync(context.ResourceTenant, new UserProfile { UserKey = userKey, @@ -101,7 +102,7 @@ await _profileStore.CreateAsync(context.ResourceTenantId, if (!string.IsNullOrWhiteSpace(request.PrimaryIdentifierValue) && request.PrimaryIdentifierType is not null) { - await _identifierStore.CreateAsync(context.ResourceTenantId, + await _identifierStore.CreateAsync(context.ResourceTenant, new UserIdentifier { UserKey = userKey, @@ -117,7 +118,7 @@ await _identifierStore.CreateAsync(context.ResourceTenantId, foreach (var integration in _integrations) { - await integration.OnUserCreatedAsync(context.ResourceTenantId, userKey, request, innerCt); + await integration.OnUserCreatedAsync(context.ResourceTenant, userKey, request, innerCt); } return UserCreateResult.Success(userKey); @@ -138,7 +139,7 @@ public async Task ChangeUserStatusAsync(AccessContext context, object request, C }; var targetUserKey = context.GetTargetUserKey(); - var current = await _lifecycleStore.GetAsync(context.ResourceTenantId, targetUserKey, innerCt); + var current = await _lifecycleStore.GetAsync(context.ResourceTenant, targetUserKey, innerCt); if (current is null) throw new InvalidOperationException("user_not_found"); @@ -152,7 +153,7 @@ public async Task ChangeUserStatusAsync(AccessContext context, object request, C throw new InvalidOperationException("admin_cannot_set_self_status"); } - await _lifecycleStore.ChangeStatusAsync(context.ResourceTenantId, targetUserKey, newStatus, _clock.UtcNow, innerCt); + await _lifecycleStore.ChangeStatusAsync(context.ResourceTenant, targetUserKey, newStatus, _clock.UtcNow, innerCt); }); await _accessOrchestrator.ExecuteAsync(context, command, ct); @@ -165,7 +166,7 @@ public async Task UpdateUserProfileAsync(AccessContext context, UpdateProfileReq var targetUserKey = context.GetTargetUserKey(); var update = UserProfileMapper.ToUpdate(request); - await _profileStore.UpdateAsync(context.ResourceTenantId, targetUserKey, update, _clock.UtcNow, innerCt); + await _profileStore.UpdateAsync(context.ResourceTenant, targetUserKey, update, _clock.UtcNow, innerCt); }); await _accessOrchestrator.ExecuteAsync(context, command, ct); @@ -176,7 +177,7 @@ public async Task> GetIdentifiersByUserAsync(Ac var command = new GetUserIdentifiersCommand(async innerCt => { var targetUserKey = context.GetTargetUserKey(); - var identifiers = await _identifierStore.GetByUserAsync(context.ResourceTenantId, targetUserKey, innerCt); + var identifiers = await _identifierStore.GetByUserAsync(context.ResourceTenant, targetUserKey, innerCt); return identifiers.Select(UserIdentifierMapper.ToDto).ToList().AsReadOnly(); }); @@ -188,7 +189,7 @@ public async Task> GetIdentifiersByUserAsync(Ac { var command = new GetUserIdentifierCommand(async innerCt => { - var identifier = await _identifierStore.GetAsync(context.ResourceTenantId, type, value, innerCt); + var identifier = await _identifierStore.GetAsync(context.ResourceTenant, type, value, innerCt); return identifier is null ? null : UserIdentifierMapper.ToDto(identifier); @@ -201,7 +202,7 @@ public async Task UserIdentifierExistsAsync(AccessContext context, UserIde { var command = new UserIdentifierExistsCommand(async innerCt => { - return await _identifierStore.ExistsAsync(context.ResourceTenantId, type, value, innerCt); + return await _identifierStore.ExistsAsync(context.ResourceTenant, type, value, innerCt); }); return await _accessOrchestrator.ExecuteAsync(context, command, ct); @@ -213,7 +214,7 @@ public async Task AddUserIdentifierAsync(AccessContext context, AddUserIdentifie { var userKey = context.GetTargetUserKey(); - await _identifierStore.CreateAsync(context.ResourceTenantId, + await _identifierStore.CreateAsync(context.ResourceTenant, new UserIdentifier { UserKey = userKey, @@ -236,7 +237,7 @@ public async Task UpdateUserIdentifierAsync(AccessContext context, UpdateUserIde if (string.Equals(request.OldValue, request.NewValue, StringComparison.Ordinal)) throw new InvalidOperationException("identifier_value_unchanged"); - await _identifierStore.UpdateValueAsync(context.ResourceTenantId, request.Type, request.OldValue, request.NewValue, _clock.UtcNow, innerCt); + await _identifierStore.UpdateValueAsync(context.ResourceTenant, request.Type, request.OldValue, request.NewValue, _clock.UtcNow, innerCt); }); await _accessOrchestrator.ExecuteAsync(context, command, ct); @@ -248,7 +249,7 @@ public async Task SetPrimaryUserIdentifierAsync(AccessContext context, SetPrimar { var userKey = context.GetTargetUserKey(); - await _identifierStore.SetPrimaryAsync(context.ResourceTenantId, userKey, request.Type, request.Value, innerCt); + await _identifierStore.SetPrimaryAsync(context.ResourceTenant, userKey, request.Type, request.Value, innerCt); }); await _accessOrchestrator.ExecuteAsync(context, command, ct); @@ -260,7 +261,7 @@ public async Task UnsetPrimaryUserIdentifierAsync(AccessContext context, UnsetPr { var userKey = context.GetTargetUserKey(); - var identifiers = await _identifierStore.GetByUserAsync(context.ResourceTenantId, userKey, innerCt); + var identifiers = await _identifierStore.GetByUserAsync(context.ResourceTenant, userKey, innerCt); var target = identifiers.FirstOrDefault(i => i.Type == request.Type && string.Equals(i.Value, request.Value, StringComparison.OrdinalIgnoreCase) && !i.IsDeleted); if (target is null) @@ -273,7 +274,7 @@ public async Task UnsetPrimaryUserIdentifierAsync(AccessContext context, UnsetPr if (otherLoginIdentifiers.Count == 0) throw new InvalidOperationException("cannot_unset_last_primary_login_identifier"); - await _identifierStore.UnsetPrimaryAsync(context.ResourceTenantId, userKey, target.Type, target.Value, innerCt); + await _identifierStore.UnsetPrimaryAsync(context.ResourceTenant, userKey, target.Type, target.Value, innerCt); }); await _accessOrchestrator.ExecuteAsync(context, command, ct); @@ -283,7 +284,7 @@ public async Task VerifyUserIdentifierAsync(AccessContext context, VerifyUserIde { var command = new VerifyUserIdentifierCommand(async innerCt => { - await _identifierStore.MarkVerifiedAsync(context.ResourceTenantId, request.Type, request.Value, _clock.UtcNow, innerCt); + await _identifierStore.MarkVerifiedAsync(context.ResourceTenant, request.Type, request.Value, _clock.UtcNow, innerCt); }); await _accessOrchestrator.ExecuteAsync(context, command, ct); @@ -295,7 +296,7 @@ public async Task DeleteUserIdentifierAsync(AccessContext context, DeleteUserIde { var targetUserKey = context.GetTargetUserKey(); - var identifiers = await _identifierStore.GetByUserAsync(context.ResourceTenantId, targetUserKey, innerCt); + var identifiers = await _identifierStore.GetByUserAsync(context.ResourceTenant, targetUserKey, innerCt); var target = identifiers.FirstOrDefault(i => i.Type == request.Type && string.Equals(i.Value, request.Value, StringComparison.OrdinalIgnoreCase) && !i.IsDeleted); if (target is null) @@ -308,7 +309,7 @@ public async Task DeleteUserIdentifierAsync(AccessContext context, DeleteUserIde if (target.IsPrimary) throw new InvalidOperationException("cannot_delete_primary_identifier"); - await _identifierStore.DeleteAsync(context.ResourceTenantId, request.Type, request.Value, request.Mode, _clock.UtcNow, innerCt); + await _identifierStore.DeleteAsync(context.ResourceTenant, request.Type, request.Value, request.Mode, _clock.UtcNow, innerCt); }); await _accessOrchestrator.ExecuteAsync(context, command, ct); @@ -321,27 +322,27 @@ public async Task DeleteUserAsync(AccessContext context, DeleteUserRequest reque var targetUserKey = context.GetTargetUserKey(); var now = _clock.UtcNow; - await _lifecycleStore.DeleteAsync(context.ResourceTenantId, targetUserKey, request.Mode, now, innerCt); - await _identifierStore.DeleteByUserAsync(context.ResourceTenantId, targetUserKey, request.Mode, now, innerCt); - await _profileStore.DeleteAsync(context.ResourceTenantId, targetUserKey, request.Mode, now, innerCt); + await _lifecycleStore.DeleteAsync(context.ResourceTenant, targetUserKey, request.Mode, now, innerCt); + await _identifierStore.DeleteByUserAsync(context.ResourceTenant, targetUserKey, request.Mode, now, innerCt); + await _profileStore.DeleteAsync(context.ResourceTenant, targetUserKey, request.Mode, now, innerCt); foreach (var integration in _integrations) { - await integration.OnUserDeletedAsync(context.ResourceTenantId, targetUserKey, request.Mode, innerCt); + await integration.OnUserDeletedAsync(context.ResourceTenant, targetUserKey, request.Mode, innerCt); } }); await _accessOrchestrator.ExecuteAsync(context, command, ct); } - private async Task BuildUserViewAsync(string? tenantId, UserKey userKey, CancellationToken ct) + private async Task BuildUserViewAsync(TenantKey tenant, UserKey userKey, CancellationToken ct) { - var profile = await _profileStore.GetAsync(tenantId, userKey, ct); + var profile = await _profileStore.GetAsync(tenant, userKey, ct); if (profile is null || profile.IsDeleted) throw new InvalidOperationException("user_profile_not_found"); - var identifiers = await _identifierStore.GetByUserAsync(tenantId, userKey, ct); + var identifiers = await _identifierStore.GetByUserAsync(tenant, userKey, ct); var username = identifiers.FirstOrDefault(x => x.Type == UserIdentifierType.Username && x.IsPrimary); var primaryEmail = identifiers.FirstOrDefault(x => x.Type == UserIdentifierType.Email && x.IsPrimary); diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Stores/IUserIdentifierStore.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Stores/IUserIdentifierStore.cs index 71a42be7..b2bac720 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Stores/IUserIdentifierStore.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Stores/IUserIdentifierStore.cs @@ -1,28 +1,29 @@ using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; using CodeBeam.UltimateAuth.Users.Contracts; namespace CodeBeam.UltimateAuth.Users.Reference; public interface IUserIdentifierStore { - Task ExistsAsync(string? tenantId, UserIdentifierType type, string value, CancellationToken ct = default); + Task ExistsAsync(TenantKey tenant, UserIdentifierType type, string value, CancellationToken ct = default); - Task> GetByUserAsync(string? tenantId, UserKey userKey, CancellationToken ct = default); + Task> GetByUserAsync(TenantKey tenant, UserKey userKey, CancellationToken ct = default); - Task GetAsync(string? tenantId, UserIdentifierType type, string value, CancellationToken ct = default); + Task GetAsync(TenantKey tenant, UserIdentifierType type, string value, CancellationToken ct = default); - Task CreateAsync(string? tenantId, UserIdentifier identifier, CancellationToken ct = default); + Task CreateAsync(TenantKey tenant, UserIdentifier identifier, CancellationToken ct = default); - Task UpdateValueAsync(string? tenantId, UserIdentifierType type, string oldValue, string newValue, DateTimeOffset updatedAt, CancellationToken ct = default); + Task UpdateValueAsync(TenantKey tenant, UserIdentifierType type, string oldValue, string newValue, DateTimeOffset updatedAt, CancellationToken ct = default); - Task MarkVerifiedAsync(string? tenantId, UserIdentifierType type, string value, DateTimeOffset verifiedAt, CancellationToken ct = default); + Task MarkVerifiedAsync(TenantKey tenant, UserIdentifierType type, string value, DateTimeOffset verifiedAt, CancellationToken ct = default); - Task SetPrimaryAsync(string? tenantId, UserKey userKey, UserIdentifierType type, string value, CancellationToken ct = default); + Task SetPrimaryAsync(TenantKey tenant, UserKey userKey, UserIdentifierType type, string value, CancellationToken ct = default); - Task UnsetPrimaryAsync(string? tenantId, UserKey userKey, UserIdentifierType type, string value, CancellationToken ct = default); + Task UnsetPrimaryAsync(TenantKey tenant, UserKey userKey, UserIdentifierType type, string value, CancellationToken ct = default); - Task DeleteAsync(string? tenantId, UserIdentifierType type, string value, DeleteMode mode, DateTimeOffset deletedAt, CancellationToken ct = default); + Task DeleteAsync(TenantKey tenant, UserIdentifierType type, string value, DeleteMode mode, DateTimeOffset deletedAt, CancellationToken ct = default); - Task DeleteByUserAsync(string? tenantId, UserKey userKey, DeleteMode mode, DateTimeOffset deletedAt, CancellationToken ct = default); + Task DeleteByUserAsync(TenantKey tenant, UserKey userKey, DeleteMode mode, DateTimeOffset deletedAt, CancellationToken ct = default); } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Stores/IUserLifecycleStore.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Stores/IUserLifecycleStore.cs index 44507c80..2e2894ad 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Stores/IUserLifecycleStore.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Stores/IUserLifecycleStore.cs @@ -1,23 +1,24 @@ using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; using CodeBeam.UltimateAuth.Users.Contracts; namespace CodeBeam.UltimateAuth.Users.Reference { public interface IUserLifecycleStore { - Task ExistsAsync(string? tenantId, UserKey userKey, CancellationToken ct = default); + Task ExistsAsync(TenantKey tenant, UserKey userKey, CancellationToken ct = default); - Task GetAsync(string? tenantId, UserKey userKey, CancellationToken ct = default); + Task GetAsync(TenantKey tenant, UserKey userKey, CancellationToken ct = default); - Task> QueryAsync(string? tenantId, UserLifecycleQuery query, CancellationToken ct = default); + Task> QueryAsync(TenantKey tenant, UserLifecycleQuery query, CancellationToken ct = default); - Task CreateAsync(string? tenantId, UserLifecycle lifecycle, CancellationToken ct = default); + Task CreateAsync(TenantKey tenant, UserLifecycle lifecycle, CancellationToken ct = default); - Task ChangeStatusAsync(string? tenantId, UserKey userKey, UserStatus newStatus, DateTimeOffset updatedAt, CancellationToken ct = default); + Task ChangeStatusAsync(TenantKey tenant, UserKey userKey, UserStatus newStatus, DateTimeOffset updatedAt, CancellationToken ct = default); - Task ChangeSecurityStampAsync(string? tenantId, UserKey userKey, Guid newSecurityStamp, DateTimeOffset updatedAt, CancellationToken ct = default); + Task ChangeSecurityStampAsync(TenantKey tenant, UserKey userKey, Guid newSecurityStamp, DateTimeOffset updatedAt, CancellationToken ct = default); - Task DeleteAsync(string? tenantId, UserKey userKey, DeleteMode mode, DateTimeOffset deletedAt, CancellationToken ct = default); + Task DeleteAsync(TenantKey tenant, UserKey userKey, DeleteMode mode, DateTimeOffset deletedAt, CancellationToken ct = default); } } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Stores/IUserProfileStore.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Stores/IUserProfileStore.cs index 78502085..8c34959d 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Stores/IUserProfileStore.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Stores/IUserProfileStore.cs @@ -1,19 +1,20 @@ using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; namespace CodeBeam.UltimateAuth.Users.Reference; public interface IUserProfileStore { - Task ExistsAsync(string? tenantId, UserKey userKey, CancellationToken ct = default); + Task ExistsAsync(TenantKey tenant, UserKey userKey, CancellationToken ct = default); - Task GetAsync(string? tenantId, UserKey userKey, CancellationToken ct = default); + Task GetAsync(TenantKey tenant, UserKey userKey, CancellationToken ct = default); - Task> QueryAsync(string? tenantId, UserProfileQuery query, CancellationToken ct = default); + Task> QueryAsync(TenantKey tenant, UserProfileQuery query, CancellationToken ct = default); - Task CreateAsync(string? tenantId, UserProfile profile, CancellationToken ct = default); + Task CreateAsync(TenantKey tenant, UserProfile profile, CancellationToken ct = default); - Task UpdateAsync(string? tenantId, UserKey userKey, UserProfileUpdate update, DateTimeOffset updatedAt, CancellationToken ct = default); + Task UpdateAsync(TenantKey tenant, UserKey userKey, UserProfileUpdate update, DateTimeOffset updatedAt, CancellationToken ct = default); - Task DeleteAsync(string? tenantId, UserKey userKey, DeleteMode mode, DateTimeOffset deletedAt, CancellationToken ct = default); + Task DeleteAsync(TenantKey tenant, UserKey userKey, DeleteMode mode, DateTimeOffset deletedAt, CancellationToken ct = default); } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Stores/UserRuntimeStore.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Stores/UserRuntimeStore.cs index 8c02456a..41573216 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Stores/UserRuntimeStore.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Stores/UserRuntimeStore.cs @@ -1,6 +1,7 @@ using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Users.Contracts; +using CodeBeam.UltimateAuth.Core.MultiTenancy; namespace CodeBeam.UltimateAuth.Users.Reference { @@ -13,9 +14,9 @@ public UserRuntimeStore(IUserLifecycleStore lifecycleStore) _lifecycleStore = lifecycleStore; } - public async Task GetAsync(string? tenantId, UserKey userKey, CancellationToken ct = default) + public async Task GetAsync(TenantKey tenant, UserKey userKey, CancellationToken ct = default) { - var lifecycle = await _lifecycleStore.GetAsync(tenantId, userKey, ct); + var lifecycle = await _lifecycleStore.GetAsync(tenant, userKey, ct); if (lifecycle is null) return null; diff --git a/src/users/CodeBeam.UltimateAuth.Users/Abstractions/IUserLifecycleIntegration.cs b/src/users/CodeBeam.UltimateAuth.Users/Abstractions/IUserLifecycleIntegration.cs index 130390a5..068a54d2 100644 --- a/src/users/CodeBeam.UltimateAuth.Users/Abstractions/IUserLifecycleIntegration.cs +++ b/src/users/CodeBeam.UltimateAuth.Users/Abstractions/IUserLifecycleIntegration.cs @@ -1,5 +1,6 @@ using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; namespace CodeBeam.UltimateAuth.Users.Abstractions; @@ -9,7 +10,7 @@ namespace CodeBeam.UltimateAuth.Users.Abstractions; /// public interface IUserLifecycleIntegration { - Task OnUserCreatedAsync(string? tenantId, UserKey userKey, object request, CancellationToken ct = default); + Task OnUserCreatedAsync(TenantKey tenant, UserKey userKey, object request, CancellationToken ct = default); - Task OnUserDeletedAsync(string? tenantId, UserKey userKey, DeleteMode mode, CancellationToken ct = default); + Task OnUserDeletedAsync(TenantKey tenant, UserKey userKey, DeleteMode mode, CancellationToken ct = default); } diff --git a/src/users/CodeBeam.UltimateAuth.Users/Abstractions/IUserSecurityStateProvider.cs b/src/users/CodeBeam.UltimateAuth.Users/Abstractions/IUserSecurityStateProvider.cs index f001bef8..b819f744 100644 --- a/src/users/CodeBeam.UltimateAuth.Users/Abstractions/IUserSecurityStateProvider.cs +++ b/src/users/CodeBeam.UltimateAuth.Users/Abstractions/IUserSecurityStateProvider.cs @@ -1,7 +1,8 @@ -namespace CodeBeam.UltimateAuth.Users +using CodeBeam.UltimateAuth.Core.MultiTenancy; + +namespace CodeBeam.UltimateAuth.Users; + +public interface IUserSecurityStateProvider { - public interface IUserSecurityStateProvider - { - Task GetAsync(string? tenantId, TUserId userId, CancellationToken ct = default); - } + Task GetAsync(TenantKey tenant, TUserId userId, CancellationToken ct = default); } From 466b7c990d9007f54bd670df4e9a72e64088293f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Can=20Karag=C3=B6z?= Date: Tue, 3 Feb 2026 01:14:58 +0300 Subject: [PATCH 7/9] Refactoring on Credential Projects --- .../Controllers/HubLoginController.cs | 3 +- .../Abstractions/IBrowserStorage.cs | 15 +- .../Abstractions/ISessionCoordinator.cs | 27 +- .../DefaultUAuthStateManager.cs | 79 ++-- .../Authentication/IUAuthStateManager.cs | 55 ++- .../UAuthAuthenticatonStateProvider.cs | 33 +- .../UAuthCascadingStateProvider.cs | 35 +- .../Authentication/UAuthState.cs | 165 ++++----- .../Authentication/UAuthStateChangeReason.cs | 15 +- .../Components/UALoginForm.razor.cs | 224 ++++++----- .../UAuthAuthenticationState.razor.cs | 61 ++- .../Components/UAuthClientProvider.razor.cs | 74 ++-- .../Contracts/CoordinatorTerminationReason.cs | 11 +- .../Contracts/PkceClientState.cs | 11 +- .../Contracts/RefreshResult.cs | 13 +- .../Contracts/StorageScope.cs | 11 +- .../Contracts/UAuthTransportResult.cs | 15 +- .../Device/IDeviceIdGenerator.cs | 9 +- .../Device/IDeviceIdProvider.cs | 9 +- .../Device/IDeviceIdStorage.cs | 11 +- .../Extensions/LoginRequestFormExtensions.cs | 20 +- .../Extensions/ServiceCollectionExtensions.cs | 138 +++++++ ...teAuthClientServiceCollectionExtensions.cs | 139 ------- .../BlazorServerSessionCoordinator.cs | 151 ++++---- .../Infrastructure/BrowserStorage.cs | 37 +- .../Infrastructure/IUAuthRequestClient.cs | 15 +- .../Infrastructure/NoOpHubCapabilities.cs | 9 +- .../NoOpHubCredentialResolver.cs | 9 +- .../Infrastructure/NoOpHubFlowReader.cs | 9 +- .../Infrastructure/NoOpSessionCoordinator.cs | 20 +- .../Infrastructure/RefreshOutcomeParser.cs | 27 +- .../Infrastructure/UAuthRequestClient.cs | 105 +++--- .../Infrastructure/UAuthResultMapper.cs | 63 ++-- .../Infrastructure/UAuthUrlBuilder.cs | 11 +- .../Options/PkceLoginOptions.cs | 38 +- .../Options/UAuthClientOptions.cs | 123 +++--- .../Options/UAuthClientProfileDetector.cs | 37 +- .../Options/UAuthOptionsPostConfigure.cs | 35 +- .../ProductInfo/UAuthClientProductInfo.cs | 11 +- .../Runtime/IUAuthClientBootstrapper.cs | 9 +- .../Runtime/UAuthClientBootstrapper.cs | 71 ++-- .../Services/DefaultAuthorizationClient.cs | 87 +++-- .../Services/DefaultCredentialClient.cs | 181 +++++---- .../Services/DefaultFlowClient.cs | 305 ++++++++------- .../Services/DefaultUserClient.cs | 115 +++--- .../Services/DefaultUserIdentifierClient.cs | 215 ++++++----- .../Services/IAuthorizationClient.cs | 17 +- .../Services/ICredentialClient.cs | 33 +- .../Services/IUAuthClient.cs | 17 +- .../Services/IUserClient.cs | 23 +- .../Services/IUserIdentifierClient.cs | 35 +- .../Storage/.gitkeep | 1 - .../AssemblyVisibility.cs | 1 + .../Session/UAuthSessionExpiredException.cs | 35 +- .../MultiTenancy/TenantKeys.cs | 7 + .../Endpoints/UAuthEndpointRegistrar.cs | 350 +++++++++--------- .../Dtos/CredentialDto.cs | 25 +- .../Dtos/CredentialMetadata.cs | 10 +- .../Extensions/CredentialTypeParser.cs | 47 ++- .../Request/AddCredentialRequest.cs | 13 +- .../Request/BeginCredentialResetRequest.cs | 9 +- .../Request/CompleteCredentialResetRequest.cs | 11 +- .../Request/ResetPasswordRequest.cs | 19 +- .../Request/RevokeAllCredentialsRequest.cs | 9 +- .../Request/RevokeCredentialRequest.cs | 17 +- .../Responses/AddCredentialResult.cs | 34 +- .../Responses/ChangeCredentialResult.cs | 23 +- .../Responses/CredentialActionResult.cs | 30 +- .../Responses/CredentialChangeResult.cs | 15 +- .../Responses/CredentialProvisionResult.cs | 2 - .../Responses/CredentialValidationResult.cs | 61 +-- .../CredentialValidationResultDto.cs | 11 - .../Responses/GetCredentialsResult.cs | 7 +- ...uth.Credentials.EntityFrameworkCore.csproj | 1 + .../Configuration/ConventionResolver.cs | 29 +- .../CredentialUserMappingBuilder.cs | 113 +++--- .../EfCoreAuthUser.cs | 17 +- .../Infrastructure/EfCoreUserStore.cs | 83 ----- .../ServiceCollectionExtensions.cs | 10 +- ...m.UltimateAuth.Credentials.InMemory.csproj | 1 + .../InMemoryCredentialSeedContributor.cs | 61 ++- .../InMemoryPasswordCredentialState.cs | 19 +- ...ions.cs => ServiceCollectionExtensions.cs} | 2 +- .../Commands/ActivateCredentialCommand.cs | 4 +- .../Commands/AddCredentialCommand.cs | 22 +- .../Commands/BeginCredentialResetCommand.cs | 4 +- .../Commands/ChangeCredentialCommand.cs | 3 +- .../Commands/GetAllCredentialsCommand.cs | 19 +- .../Commands/SetInitialCredentialCommand.cs | 19 +- .../DefaultCredentialEndpointHandler.cs | 5 +- .../PasswordUserLifecycleIntegration.cs | 2 +- .../Services/DefaultUserCredentialsService.cs | 26 +- .../Abstractions/ICredentialDescriptor.cs | 13 +- .../Abstractions/ICredentialSecretStore.cs | 9 +- .../Abstractions/ICredentialStore.cs | 25 +- .../Abstractions/IPublicKeyCredential.cs | 9 +- .../Abstractions/ISecurableCredential.cs | 9 +- .../CodeBeam.UltimateAuth.Policies.csproj | 5 - .../Defaults/CompiledAccessPolicySet.cs | 37 +- .../Fluent/ConditionalPolicyBuilder.cs | 39 +- .../Fluent/ConditionalScopeBuilder.cs | 62 ++-- .../Fluent/IConditionalPolicyBuilder.cs | 11 +- .../Fluent/IPolicyBuilder.cs | 11 +- .../Fluent/IPolicyScopeBuilder.cs | 17 +- .../Fluent/PolicyBuilder.cs | 25 +- .../Fluent/PolicyScopeBuilder.cs | 68 ++-- .../Policies/ConditionalAccessPolicy.cs | 29 +- .../Policies/RequireActiveUserPolicy.cs | 77 ++-- .../Policies/RequireSystemPolicy.cs | 17 +- ...deBeam.UltimateAuth.Security.Argon2.csproj | 1 + .../Data/UAuthSessionDbContext.cs | 183 +++++---- .../Mappers/SessionChainProjectionMapper.cs | 4 +- .../Mappers/SessionProjectionMapper.cs | 4 +- .../Mappers/SessionRootProjectionMapper.cs | 4 +- .../Stores/EfCoreSessionStoreKernelFactory.cs | 9 +- .../UAuthTokenDbContext.cs | 14 +- .../Dtos/UserStatus.cs | 25 +- .../Core/RefreshTokenValidatorTests.cs | 15 +- .../Core/UAuthSessionChainTests.cs | 13 +- .../Core/UAuthSessionTests.cs | 5 +- 120 files changed, 2362 insertions(+), 2536 deletions(-) create mode 100644 src/CodeBeam.UltimateAuth.Client/Extensions/ServiceCollectionExtensions.cs delete mode 100644 src/CodeBeam.UltimateAuth.Client/Extensions/UltimateAuthClientServiceCollectionExtensions.cs delete mode 100644 src/CodeBeam.UltimateAuth.Client/Storage/.gitkeep create mode 100644 src/CodeBeam.UltimateAuth.Core/MultiTenancy/TenantKeys.cs delete mode 100644 src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Responses/CredentialValidationResultDto.cs delete mode 100644 src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Infrastructure/EfCoreUserStore.cs rename src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/{UltimateAuthDefaultsInMemoryExtensions.cs => ServiceCollectionExtensions.cs} (92%) diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Controllers/HubLoginController.cs b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Controllers/HubLoginController.cs index 30e32614..fa7a1ae5 100644 --- a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Controllers/HubLoginController.cs +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Controllers/HubLoginController.cs @@ -1,5 +1,6 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; using CodeBeam.UltimateAuth.Core.Options; using CodeBeam.UltimateAuth.Server.Options; using CodeBeam.UltimateAuth.Server.Stores; @@ -41,7 +42,7 @@ public async Task BeginLogin( hubSessionId: hubSessionId, flowType: HubFlowType.Login, clientProfile: client_profile, - tenantId: null, + tenant: TenantKeys.System, returnUrl: return_url, payload: payload, expiresAt: _clock.UtcNow.Add(_options.Hub.FlowLifetime)); diff --git a/src/CodeBeam.UltimateAuth.Client/Abstractions/IBrowserStorage.cs b/src/CodeBeam.UltimateAuth.Client/Abstractions/IBrowserStorage.cs index f2f18eba..77e8b19a 100644 --- a/src/CodeBeam.UltimateAuth.Client/Abstractions/IBrowserStorage.cs +++ b/src/CodeBeam.UltimateAuth.Client/Abstractions/IBrowserStorage.cs @@ -1,12 +1,11 @@ using CodeBeam.UltimateAuth.Client.Contracts; -namespace CodeBeam.UltimateAuth.Client.Utilities +namespace CodeBeam.UltimateAuth.Client.Utilities; + +public interface IBrowserStorage { - public interface IBrowserStorage - { - ValueTask SetAsync(StorageScope scope, string key, string value); - ValueTask GetAsync(StorageScope scope, string key); - ValueTask RemoveAsync(StorageScope scope, string key); - ValueTask ExistsAsync(StorageScope scope, string key); - } + ValueTask SetAsync(StorageScope scope, string key, string value); + ValueTask GetAsync(StorageScope scope, string key); + ValueTask RemoveAsync(StorageScope scope, string key); + ValueTask ExistsAsync(StorageScope scope, string key); } diff --git a/src/CodeBeam.UltimateAuth.Client/Abstractions/ISessionCoordinator.cs b/src/CodeBeam.UltimateAuth.Client/Abstractions/ISessionCoordinator.cs index d71d9f57..ce1781aa 100644 --- a/src/CodeBeam.UltimateAuth.Client/Abstractions/ISessionCoordinator.cs +++ b/src/CodeBeam.UltimateAuth.Client/Abstractions/ISessionCoordinator.cs @@ -1,18 +1,17 @@ -namespace CodeBeam.UltimateAuth.Client.Abstractions +namespace CodeBeam.UltimateAuth.Client.Abstractions; + +public interface ISessionCoordinator : IAsyncDisposable { - public interface ISessionCoordinator : IAsyncDisposable - { - /// - /// Starts session coordination. - /// Should be idempotent. - /// - Task StartAsync(CancellationToken cancellationToken = default); + /// + /// Starts session coordination. + /// Should be idempotent. + /// + Task StartAsync(CancellationToken cancellationToken = default); - /// - /// Stops coordination (optional). - /// - Task StopAsync(); + /// + /// Stops coordination (optional). + /// + Task StopAsync(); - event Action? ReauthRequired; - } + event Action? ReauthRequired; } diff --git a/src/CodeBeam.UltimateAuth.Client/Authentication/DefaultUAuthStateManager.cs b/src/CodeBeam.UltimateAuth.Client/Authentication/DefaultUAuthStateManager.cs index 1eb5b372..987726af 100644 --- a/src/CodeBeam.UltimateAuth.Client/Authentication/DefaultUAuthStateManager.cs +++ b/src/CodeBeam.UltimateAuth.Client/Authentication/DefaultUAuthStateManager.cs @@ -1,55 +1,54 @@ using CodeBeam.UltimateAuth.Client.Runtime; using CodeBeam.UltimateAuth.Core.Abstractions; -namespace CodeBeam.UltimateAuth.Client.Authentication -{ - internal sealed class DefaultUAuthStateManager : IUAuthStateManager - { - private readonly IUAuthClient _client; - private readonly IClock _clock; - private readonly IUAuthClientBootstrapper _bootstrapper; - - public UAuthState State { get; } = UAuthState.Anonymous(); - - public DefaultUAuthStateManager(IUAuthClient client, IClock clock, IUAuthClientBootstrapper bootstrapper) - { - _client = client; - _clock = clock; - _bootstrapper = bootstrapper; - } +namespace CodeBeam.UltimateAuth.Client.Authentication; - public async Task EnsureAsync(CancellationToken ct = default) - { - if (State.IsAuthenticated && !State.IsStale) - return; +internal sealed class DefaultUAuthStateManager : IUAuthStateManager +{ + private readonly IUAuthClient _client; + private readonly IClock _clock; + private readonly IUAuthClientBootstrapper _bootstrapper; - await _bootstrapper.EnsureStartedAsync(); - var result = await _client.Flows.ValidateAsync(); + public UAuthState State { get; } = UAuthState.Anonymous(); - if (!result.IsValid) - { - State.Clear(); - return; - } + public DefaultUAuthStateManager(IUAuthClient client, IClock clock, IUAuthClientBootstrapper bootstrapper) + { + _client = client; + _clock = clock; + _bootstrapper = bootstrapper; + } - State.ApplySnapshot(result.Snapshot, _clock.UtcNow); - } + public async Task EnsureAsync(CancellationToken ct = default) + { + if (State.IsAuthenticated && !State.IsStale) + return; - public Task OnLoginAsync() - { - State.MarkStale(); - return Task.CompletedTask; - } + await _bootstrapper.EnsureStartedAsync(); + var result = await _client.Flows.ValidateAsync(); - public Task OnLogoutAsync() + if (!result.IsValid) { State.Clear(); - return Task.CompletedTask; + return; } - public void MarkStale() - { - State.MarkStale(); - } + State.ApplySnapshot(result.Snapshot, _clock.UtcNow); + } + + public Task OnLoginAsync() + { + State.MarkStale(); + return Task.CompletedTask; + } + + public Task OnLogoutAsync() + { + State.Clear(); + return Task.CompletedTask; + } + + public void MarkStale() + { + State.MarkStale(); } } diff --git a/src/CodeBeam.UltimateAuth.Client/Authentication/IUAuthStateManager.cs b/src/CodeBeam.UltimateAuth.Client/Authentication/IUAuthStateManager.cs index e97c8c87..a48b5bdf 100644 --- a/src/CodeBeam.UltimateAuth.Client/Authentication/IUAuthStateManager.cs +++ b/src/CodeBeam.UltimateAuth.Client/Authentication/IUAuthStateManager.cs @@ -1,36 +1,35 @@ -namespace CodeBeam.UltimateAuth.Client.Authentication +namespace CodeBeam.UltimateAuth.Client.Authentication; + +/// +/// Orchestrates the lifecycle of UAuthState. +/// This is the single authority responsible for keeping +/// client-side authentication state in sync with the server. +/// +public interface IUAuthStateManager { /// - /// Orchestrates the lifecycle of UAuthState. - /// This is the single authority responsible for keeping - /// client-side authentication state in sync with the server. + /// Current in-memory authentication state. /// - public interface IUAuthStateManager - { - /// - /// Current in-memory authentication state. - /// - UAuthState State { get; } + UAuthState State { get; } - /// - /// Ensures the authentication state is valid. - /// May call server validate/refresh if needed. - /// - Task EnsureAsync(CancellationToken ct = default); + /// + /// Ensures the authentication state is valid. + /// May call server validate/refresh if needed. + /// + Task EnsureAsync(CancellationToken ct = default); - /// - /// Called after a successful login. - /// - Task OnLoginAsync(); + /// + /// Called after a successful login. + /// + Task OnLoginAsync(); - /// - /// Called after logout. - /// - Task OnLogoutAsync(); + /// + /// Called after logout. + /// + Task OnLogoutAsync(); - /// - /// Forces state to be cleared and re-validation required. - /// - void MarkStale(); - } + /// + /// Forces state to be cleared and re-validation required. + /// + void MarkStale(); } diff --git a/src/CodeBeam.UltimateAuth.Client/Authentication/UAuthAuthenticatonStateProvider.cs b/src/CodeBeam.UltimateAuth.Client/Authentication/UAuthAuthenticatonStateProvider.cs index 89d48fad..57c64931 100644 --- a/src/CodeBeam.UltimateAuth.Client/Authentication/UAuthAuthenticatonStateProvider.cs +++ b/src/CodeBeam.UltimateAuth.Client/Authentication/UAuthAuthenticatonStateProvider.cs @@ -1,24 +1,21 @@ -using CodeBeam.UltimateAuth.Client.Abstractions; -using Microsoft.AspNetCore.Components.Authorization; -using System.Security.Principal; +using Microsoft.AspNetCore.Components.Authorization; -namespace CodeBeam.UltimateAuth.Client.Authentication -{ - internal sealed class UAuthAuthenticationStateProvider : AuthenticationStateProvider - { - private readonly IUAuthStateManager _stateManager; +namespace CodeBeam.UltimateAuth.Client.Authentication; - public UAuthAuthenticationStateProvider(IUAuthStateManager stateManager) - { - _stateManager = stateManager; - _stateManager.State.Changed += _ => NotifyAuthenticationStateChanged(GetAuthenticationStateAsync()); - } +internal sealed class UAuthAuthenticationStateProvider : AuthenticationStateProvider +{ + private readonly IUAuthStateManager _stateManager; - public override Task GetAuthenticationStateAsync() - { - var principal = _stateManager.State.ToClaimsPrincipal(); - return Task.FromResult(new AuthenticationState(principal)); - } + public UAuthAuthenticationStateProvider(IUAuthStateManager stateManager) + { + _stateManager = stateManager; + _stateManager.State.Changed += _ => NotifyAuthenticationStateChanged(GetAuthenticationStateAsync()); + } + public override Task GetAuthenticationStateAsync() + { + var principal = _stateManager.State.ToClaimsPrincipal(); + return Task.FromResult(new AuthenticationState(principal)); } + } diff --git a/src/CodeBeam.UltimateAuth.Client/Authentication/UAuthCascadingStateProvider.cs b/src/CodeBeam.UltimateAuth.Client/Authentication/UAuthCascadingStateProvider.cs index 5c45d210..c8733f43 100644 --- a/src/CodeBeam.UltimateAuth.Client/Authentication/UAuthCascadingStateProvider.cs +++ b/src/CodeBeam.UltimateAuth.Client/Authentication/UAuthCascadingStateProvider.cs @@ -1,26 +1,25 @@ using Microsoft.AspNetCore.Components; -namespace CodeBeam.UltimateAuth.Client.Authentication +namespace CodeBeam.UltimateAuth.Client.Authentication; + +internal sealed class UAuthCascadingStateProvider : CascadingValueSource, IDisposable { - internal sealed class UAuthCascadingStateProvider : CascadingValueSource, IDisposable - { - private readonly IUAuthStateManager _stateManager; + private readonly IUAuthStateManager _stateManager; - public UAuthCascadingStateProvider(IUAuthStateManager stateManager) - : base(() => stateManager.State, isFixed: false) - { - _stateManager = stateManager; - _stateManager.State.Changed += OnStateChanged; - } + public UAuthCascadingStateProvider(IUAuthStateManager stateManager) + : base(() => stateManager.State, isFixed: false) + { + _stateManager = stateManager; + _stateManager.State.Changed += OnStateChanged; + } - private void OnStateChanged(UAuthStateChangeReason _) - { - NotifyChangedAsync(); - } + private void OnStateChanged(UAuthStateChangeReason _) + { + NotifyChangedAsync(); + } - public void Dispose() - { - _stateManager.State.Changed -= OnStateChanged; - } + public void Dispose() + { + _stateManager.State.Changed -= OnStateChanged; } } diff --git a/src/CodeBeam.UltimateAuth.Client/Authentication/UAuthState.cs b/src/CodeBeam.UltimateAuth.Client/Authentication/UAuthState.cs index e90c4909..405abfc1 100644 --- a/src/CodeBeam.UltimateAuth.Client/Authentication/UAuthState.cs +++ b/src/CodeBeam.UltimateAuth.Client/Authentication/UAuthState.cs @@ -3,117 +3,116 @@ using CodeBeam.UltimateAuth.Core.MultiTenancy; using System.Security.Claims; -namespace CodeBeam.UltimateAuth.Client.Authentication +namespace CodeBeam.UltimateAuth.Client.Authentication; + +/// +/// Represents the client-side authentication snapshot for UltimateAuth. +/// +/// This is a lightweight, memory-only view of the current authentication state. +/// It is not a security boundary and must always be validated server-side. +/// +public sealed class UAuthState { - /// - /// Represents the client-side authentication snapshot for UltimateAuth. - /// - /// This is a lightweight, memory-only view of the current authentication state. - /// It is not a security boundary and must always be validated server-side. - /// - public sealed class UAuthState - { - private UAuthState() { } + private UAuthState() { } - public bool IsAuthenticated { get; private set; } + public bool IsAuthenticated { get; private set; } - public UserKey? UserKey { get; private set; } + public UserKey? UserKey { get; private set; } - public TenantKey Tenant { get; private set; } + public TenantKey Tenant { get; private set; } - /// - /// When this authentication snapshot was created. - /// - public DateTimeOffset? AuthenticatedAt { get; private set; } + /// + /// When this authentication snapshot was created. + /// + public DateTimeOffset? AuthenticatedAt { get; private set; } - /// - /// When this snapshot was last validated or refreshed. - /// - public DateTimeOffset? LastValidatedAt { get; private set; } + /// + /// When this snapshot was last validated or refreshed. + /// + public DateTimeOffset? LastValidatedAt { get; private set; } - /// - /// Indicates whether the snapshot may be stale - /// (e.g. after navigation, reload, or time-based heuristics). - /// - public bool IsStale { get; private set; } + /// + /// Indicates whether the snapshot may be stale + /// (e.g. after navigation, reload, or time-based heuristics). + /// + public bool IsStale { get; private set; } - public ClaimsSnapshot Claims { get; private set; } = ClaimsSnapshot.Empty; + public ClaimsSnapshot Claims { get; private set; } = ClaimsSnapshot.Empty; - public event Action? Changed; + public event Action? Changed; - public static UAuthState Anonymous() => new(); + public static UAuthState Anonymous() => new(); - internal void ApplySnapshot(AuthStateSnapshot snapshot, DateTimeOffset validatedAt) + internal void ApplySnapshot(AuthStateSnapshot snapshot, DateTimeOffset validatedAt) + { + if (string.IsNullOrWhiteSpace(snapshot.UserKey)) { - if (string.IsNullOrWhiteSpace(snapshot.UserKey)) - { - Clear(); - return; - } - - UserKey = CodeBeam.UltimateAuth.Core.Domain.UserKey.FromString(snapshot.UserKey); - Tenant = snapshot.Tenant; - Claims = snapshot.Claims; + Clear(); + return; + } - IsAuthenticated = true; + UserKey = CodeBeam.UltimateAuth.Core.Domain.UserKey.FromString(snapshot.UserKey); + Tenant = snapshot.Tenant; + Claims = snapshot.Claims; - AuthenticatedAt = snapshot.AuthenticatedAt; - LastValidatedAt = validatedAt; - IsStale = false; + IsAuthenticated = true; - Changed?.Invoke(UAuthStateChangeReason.Authenticated); - } + AuthenticatedAt = snapshot.AuthenticatedAt; + LastValidatedAt = validatedAt; + IsStale = false; + Changed?.Invoke(UAuthStateChangeReason.Authenticated); + } - internal void MarkValidated(DateTimeOffset now) - { - if (!IsAuthenticated) - return; - LastValidatedAt = now; - IsStale = false; + internal void MarkValidated(DateTimeOffset now) + { + if (!IsAuthenticated) + return; - Changed?.Invoke(UAuthStateChangeReason.Validated); - } + LastValidatedAt = now; + IsStale = false; - internal void MarkStale() - { - if (!IsAuthenticated) - return; + Changed?.Invoke(UAuthStateChangeReason.Validated); + } - IsStale = true; - Changed?.Invoke(UAuthStateChangeReason.MarkedStale); - } + internal void MarkStale() + { + if (!IsAuthenticated) + return; - internal void Clear() - { - Claims = ClaimsSnapshot.Empty; + IsStale = true; + Changed?.Invoke(UAuthStateChangeReason.MarkedStale); + } - UserKey = null; - IsAuthenticated = false; + internal void Clear() + { + Claims = ClaimsSnapshot.Empty; - AuthenticatedAt = null; - LastValidatedAt = null; - IsStale = false; + UserKey = null; + IsAuthenticated = false; - Changed?.Invoke(UAuthStateChangeReason.Cleared); - } + AuthenticatedAt = null; + LastValidatedAt = null; + IsStale = false; - /// - /// Creates a ClaimsPrincipal view for ASP.NET / Blazor integration. - /// - public ClaimsPrincipal ToClaimsPrincipal(string authenticationType = "UltimateAuth") - { - if (!IsAuthenticated) - return new ClaimsPrincipal(new ClaimsIdentity()); + Changed?.Invoke(UAuthStateChangeReason.Cleared); + } - var identity = new ClaimsIdentity( - Claims.AsDictionary() - .Select(kv => new Claim(kv.Key, kv.Value)), - authenticationType); + /// + /// Creates a ClaimsPrincipal view for ASP.NET / Blazor integration. + /// + public ClaimsPrincipal ToClaimsPrincipal(string authenticationType = "UltimateAuth") + { + if (!IsAuthenticated) + return new ClaimsPrincipal(new ClaimsIdentity()); - return new ClaimsPrincipal(identity); - } + var identity = new ClaimsIdentity( + Claims.AsDictionary() + .Select(kv => new Claim(kv.Key, kv.Value)), + authenticationType); + return new ClaimsPrincipal(identity); } + } diff --git a/src/CodeBeam.UltimateAuth.Client/Authentication/UAuthStateChangeReason.cs b/src/CodeBeam.UltimateAuth.Client/Authentication/UAuthStateChangeReason.cs index b2b72dde..ad2fa368 100644 --- a/src/CodeBeam.UltimateAuth.Client/Authentication/UAuthStateChangeReason.cs +++ b/src/CodeBeam.UltimateAuth.Client/Authentication/UAuthStateChangeReason.cs @@ -1,10 +1,9 @@ -namespace CodeBeam.UltimateAuth.Client.Authentication +namespace CodeBeam.UltimateAuth.Client.Authentication; + +public enum UAuthStateChangeReason { - public enum UAuthStateChangeReason - { - Authenticated, - Validated, - MarkedStale, - Cleared - } + Authenticated, + Validated, + MarkedStale, + Cleared } diff --git a/src/CodeBeam.UltimateAuth.Client/Components/UALoginForm.razor.cs b/src/CodeBeam.UltimateAuth.Client/Components/UALoginForm.razor.cs index 8dbab50d..b747c717 100644 --- a/src/CodeBeam.UltimateAuth.Client/Components/UALoginForm.razor.cs +++ b/src/CodeBeam.UltimateAuth.Client/Components/UALoginForm.razor.cs @@ -3,166 +3,164 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; -using CodeBeam.UltimateAuth.Core.Options; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.WebUtilities; using Microsoft.JSInterop; -namespace CodeBeam.UltimateAuth.Client +namespace CodeBeam.UltimateAuth.Client; + +public partial class UALoginForm { - public partial class UALoginForm - { - [Inject] IDeviceIdProvider DeviceIdProvider { get; set; } = null!; - private DeviceId? _deviceId; + [Inject] IDeviceIdProvider DeviceIdProvider { get; set; } = null!; + private DeviceId? _deviceId; - [Inject] - IHubCredentialResolver HubCredentialResolver { get; set; } = null!; + [Inject] + IHubCredentialResolver HubCredentialResolver { get; set; } = null!; - [Inject] - IHubFlowReader HubFlowReader { get; set; } = null!; + [Inject] + IHubFlowReader HubFlowReader { get; set; } = null!; - [Inject] - IHubCapabilities HubCapabilities { get; set; } = null!; + [Inject] + IHubCapabilities HubCapabilities { get; set; } = null!; - [Parameter] - public string? Identifier { get; set; } + [Parameter] + public string? Identifier { get; set; } - [Parameter] - public string? Secret { get; set; } + [Parameter] + public string? Secret { get; set; } - [Parameter] - public string? Endpoint { get; set; } + [Parameter] + public string? Endpoint { get; set; } - [Parameter] - public string? ReturnUrl { get; set; } + [Parameter] + public string? ReturnUrl { get; set; } - //[Parameter] - //public IHubCredentialResolver? HubCredentialResolver { get; set; } + //[Parameter] + //public IHubCredentialResolver? HubCredentialResolver { get; set; } - //[Parameter] - //public IHubFlowReader? HubFlowReader { get; set; } + //[Parameter] + //public IHubFlowReader? HubFlowReader { get; set; } - [Parameter] - public HubSessionId? HubSessionId { get; set; } + [Parameter] + public HubSessionId? HubSessionId { get; set; } - [Parameter] - public UAuthLoginType LoginType { get; set; } = UAuthLoginType.Password; + [Parameter] + public UAuthLoginType LoginType { get; set; } = UAuthLoginType.Password; - [Parameter] - public RenderFragment? ChildContent { get; set; } + [Parameter] + public RenderFragment? ChildContent { get; set; } - [Parameter] - public bool AllowEnterKeyToSubmit { get; set; } = true; + [Parameter] + public bool AllowEnterKeyToSubmit { get; set; } = true; - private ElementReference _form; + private ElementReference _form; - private HubCredentials? _credentials; - private HubFlowState? _flow; - protected override async Task OnParametersSetAsync() - { - await base.OnParametersSetAsync(); - - await ReloadCredentialsAsync(); - await ReloadStateAsync(); + private HubCredentials? _credentials; + private HubFlowState? _flow; + protected override async Task OnParametersSetAsync() + { + await base.OnParametersSetAsync(); - if (LoginType == UAuthLoginType.Pkce && !HubCapabilities.SupportsPkce) - { - throw new InvalidOperationException("PKCE login requires UAuthHub (Blazor Server). " + - "PKCE is not supported in this client profile." + - "Change LoginType to password or place this component to a server-side project."); - } + await ReloadCredentialsAsync(); + await ReloadStateAsync(); - //if (LoginType == UAuthLoginType.Pkce && EffectiveHubSessionId is null) - //{ - // throw new InvalidOperationException("PKCE login requires an active Hub flow. " + - // "No 'hub' query parameter was found." - // ); - //} + if (LoginType == UAuthLoginType.Pkce && !HubCapabilities.SupportsPkce) + { + throw new InvalidOperationException("PKCE login requires UAuthHub (Blazor Server). " + + "PKCE is not supported in this client profile." + + "Change LoginType to password or place this component to a server-side project."); } - protected override async Task OnAfterRenderAsync(bool firstRender) - { - if (!firstRender) - return; + //if (LoginType == UAuthLoginType.Pkce && EffectiveHubSessionId is null) + //{ + // throw new InvalidOperationException("PKCE login requires an active Hub flow. " + + // "No 'hub' query parameter was found." + // ); + //} + } - _deviceId = await DeviceIdProvider.GetOrCreateAsync(); - StateHasChanged(); - } + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (!firstRender) + return; - public async Task ReloadCredentialsAsync() - { - if (LoginType != UAuthLoginType.Pkce) - return; + _deviceId = await DeviceIdProvider.GetOrCreateAsync(); + StateHasChanged(); + } - if (HubCredentialResolver is null || EffectiveHubSessionId is null) - return; + public async Task ReloadCredentialsAsync() + { + if (LoginType != UAuthLoginType.Pkce) + return; - _credentials = await HubCredentialResolver.ResolveAsync(EffectiveHubSessionId.Value); - } + if (HubCredentialResolver is null || EffectiveHubSessionId is null) + return; - public async Task ReloadStateAsync() - { - if (LoginType != UAuthLoginType.Pkce || EffectiveHubSessionId is null || HubFlowReader is null) - return; + _credentials = await HubCredentialResolver.ResolveAsync(EffectiveHubSessionId.Value); + } - _flow = await HubFlowReader.GetStateAsync(EffectiveHubSessionId.Value); - } + public async Task ReloadStateAsync() + { + if (LoginType != UAuthLoginType.Pkce || EffectiveHubSessionId is null || HubFlowReader is null) + return; - public async Task SubmitAsync() - { - if (_form.Context is null) - throw new InvalidOperationException("Form is not yet rendered. Call SubmitAsync after OnAfterRender."); + _flow = await HubFlowReader.GetStateAsync(EffectiveHubSessionId.Value); + } - await JS.InvokeVoidAsync("uauth.submitForm", _form); - } + public async Task SubmitAsync() + { + if (_form.Context is null) + throw new InvalidOperationException("Form is not yet rendered. Call SubmitAsync after OnAfterRender."); - private string ClientProfileValue => CoreOptions.Value.ClientProfile.ToString(); + await JS.InvokeVoidAsync("uauth.submitForm", _form); + } + + private string ClientProfileValue => CoreOptions.Value.ClientProfile.ToString(); - private string EffectiveEndpoint => LoginType == UAuthLoginType.Pkce - ? Options.Value.Endpoints.PkceComplete - : Options.Value.Endpoints.Login; + private string EffectiveEndpoint => LoginType == UAuthLoginType.Pkce + ? Options.Value.Endpoints.PkceComplete + : Options.Value.Endpoints.Login; - private string ResolvedEndpoint + private string ResolvedEndpoint + { + get { - get - { - var loginPath = string.IsNullOrWhiteSpace(Endpoint) - ? EffectiveEndpoint - : Endpoint; + var loginPath = string.IsNullOrWhiteSpace(Endpoint) + ? EffectiveEndpoint + : Endpoint; - var baseUrl = UAuthUrlBuilder.Combine(Options.Value.Endpoints.Authority, loginPath); - var returnUrl = EffectiveReturnUrl; + var baseUrl = UAuthUrlBuilder.Combine(Options.Value.Endpoints.Authority, loginPath); + var returnUrl = EffectiveReturnUrl; - if (string.IsNullOrWhiteSpace(returnUrl)) - return baseUrl; + if (string.IsNullOrWhiteSpace(returnUrl)) + return baseUrl; - return $"{baseUrl}?{(_credentials != null ? "hub=" + EffectiveHubSessionId + "&" : null)}returnUrl={Uri.EscapeDataString(returnUrl)}"; - } + return $"{baseUrl}?{(_credentials != null ? "hub=" + EffectiveHubSessionId + "&" : null)}returnUrl={Uri.EscapeDataString(returnUrl)}"; } + } - private string EffectiveReturnUrl => !string.IsNullOrWhiteSpace(ReturnUrl) - ? ReturnUrl - : LoginType == UAuthLoginType.Pkce ? _flow?.ReturnUrl ?? string.Empty : Navigation.Uri; + private string EffectiveReturnUrl => !string.IsNullOrWhiteSpace(ReturnUrl) + ? ReturnUrl + : LoginType == UAuthLoginType.Pkce ? _flow?.ReturnUrl ?? string.Empty : Navigation.Uri; - private HubSessionId? EffectiveHubSessionId + private HubSessionId? EffectiveHubSessionId + { + get { - get - { - if (HubSessionId is not null) - return HubSessionId; - - var uri = Navigation.ToAbsoluteUri(Navigation.Uri); - var query = QueryHelpers.ParseQuery(uri.Query); + if (HubSessionId is not null) + return HubSessionId; - if (query.TryGetValue("hub", out var hubValue) && CodeBeam.UltimateAuth.Core.Domain.HubSessionId.TryParse(hubValue, out var parsed)) - { - return parsed; - } + var uri = Navigation.ToAbsoluteUri(Navigation.Uri); + var query = QueryHelpers.ParseQuery(uri.Query); - return null; + if (query.TryGetValue("hub", out var hubValue) && CodeBeam.UltimateAuth.Core.Domain.HubSessionId.TryParse(hubValue, out var parsed)) + { + return parsed; } - } + return null; + } } + } diff --git a/src/CodeBeam.UltimateAuth.Client/Components/UAuthAuthenticationState.razor.cs b/src/CodeBeam.UltimateAuth.Client/Components/UAuthAuthenticationState.razor.cs index f66787af..96b1c37c 100644 --- a/src/CodeBeam.UltimateAuth.Client/Components/UAuthAuthenticationState.razor.cs +++ b/src/CodeBeam.UltimateAuth.Client/Components/UAuthAuthenticationState.razor.cs @@ -1,46 +1,45 @@ using CodeBeam.UltimateAuth.Client.Authentication; using Microsoft.AspNetCore.Components; -namespace CodeBeam.UltimateAuth.Client.Components +namespace CodeBeam.UltimateAuth.Client.Components; + +public partial class UAuthAuthenticationState { - public partial class UAuthAuthenticationState - { - private bool _initialized; - private UAuthState _uauthState; + private bool _initialized; + private UAuthState _uauthState; - [Parameter] - public RenderFragment ChildContent { get; set; } = default!; + [Parameter] + public RenderFragment ChildContent { get; set; } = default!; - protected override async Task OnAfterRenderAsync(bool firstRender) - { - if (!firstRender) - return; + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (!firstRender) + return; - if (_initialized) - return; + if (_initialized) + return; - _initialized = true; - //await Bootstrapper.EnsureStartedAsync(); - await StateManager.EnsureAsync(); - _uauthState = StateManager.State; + _initialized = true; + //await Bootstrapper.EnsureStartedAsync(); + await StateManager.EnsureAsync(); + _uauthState = StateManager.State; - StateManager.State.Changed += OnStateChanged; - } + StateManager.State.Changed += OnStateChanged; + } - private void OnStateChanged(UAuthStateChangeReason _) + private void OnStateChanged(UAuthStateChangeReason _) + { + //StateManager.EnsureAsync(); + if (_ == UAuthStateChangeReason.MarkedStale) { - //StateManager.EnsureAsync(); - if (_ == UAuthStateChangeReason.MarkedStale) - { - StateManager.EnsureAsync(); - } - _uauthState = StateManager.State; - InvokeAsync(StateHasChanged); + StateManager.EnsureAsync(); } + _uauthState = StateManager.State; + InvokeAsync(StateHasChanged); + } - public void Dispose() - { - StateManager.State.Changed -= OnStateChanged; - } + public void Dispose() + { + StateManager.State.Changed -= OnStateChanged; } } diff --git a/src/CodeBeam.UltimateAuth.Client/Components/UAuthClientProvider.razor.cs b/src/CodeBeam.UltimateAuth.Client/Components/UAuthClientProvider.razor.cs index c3d28274..8bbcd7e2 100644 --- a/src/CodeBeam.UltimateAuth.Client/Components/UAuthClientProvider.razor.cs +++ b/src/CodeBeam.UltimateAuth.Client/Components/UAuthClientProvider.razor.cs @@ -1,43 +1,41 @@ -using CodeBeam.UltimateAuth.Client.Diagnostics; -using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components; -namespace CodeBeam.UltimateAuth.Client +namespace CodeBeam.UltimateAuth.Client; + +// TODO: Add CircuitHandler to manage start/stop of coordinator in server-side Blazor +public partial class UAuthClientProvider : ComponentBase, IAsyncDisposable { - // TODO: Add CircuitHandler to manage start/stop of coordinator in server-side Blazor - public partial class UAuthClientProvider : ComponentBase, IAsyncDisposable + private bool _started; + + [Parameter] + public EventCallback OnReauthRequired { get; set; } + + protected override async Task OnInitializedAsync() + { + Coordinator.ReauthRequired += HandleReauthRequired; + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (!firstRender || _started) + return; + + _started = true; + // TODO: Add device id auto creation for MVC, this is only for blazor. + var deviceId = await DeviceIdProvider.GetOrCreateAsync(); + await BrowserUAuthBridge.SetDeviceIdAsync(deviceId.Value); + await Coordinator.StartAsync(); + StateHasChanged(); + } + + private async void HandleReauthRequired() + { + if (OnReauthRequired.HasDelegate) + await OnReauthRequired.InvokeAsync(); + } + + public async ValueTask DisposeAsync() { - private bool _started; - - [Parameter] - public EventCallback OnReauthRequired { get; set; } - - protected override async Task OnInitializedAsync() - { - Coordinator.ReauthRequired += HandleReauthRequired; - } - - protected override async Task OnAfterRenderAsync(bool firstRender) - { - if (!firstRender || _started) - return; - - _started = true; - // TODO: Add device id auto creation for MVC, this is only for blazor. - var deviceId = await DeviceIdProvider.GetOrCreateAsync(); - await BrowserUAuthBridge.SetDeviceIdAsync(deviceId.Value); - await Coordinator.StartAsync(); - StateHasChanged(); - } - - private async void HandleReauthRequired() - { - if (OnReauthRequired.HasDelegate) - await OnReauthRequired.InvokeAsync(); - } - - public async ValueTask DisposeAsync() - { - await Coordinator.StopAsync(); - } + await Coordinator.StopAsync(); } } diff --git a/src/CodeBeam.UltimateAuth.Client/Contracts/CoordinatorTerminationReason.cs b/src/CodeBeam.UltimateAuth.Client/Contracts/CoordinatorTerminationReason.cs index 58a398b2..3b079766 100644 --- a/src/CodeBeam.UltimateAuth.Client/Contracts/CoordinatorTerminationReason.cs +++ b/src/CodeBeam.UltimateAuth.Client/Contracts/CoordinatorTerminationReason.cs @@ -1,8 +1,7 @@ -namespace CodeBeam.UltimateAuth.Client.Contracts +namespace CodeBeam.UltimateAuth.Client.Contracts; + +public enum CoordinatorTerminationReason { - public enum CoordinatorTerminationReason - { - None = 0, - ReauthRequired = 1 - } + None = 0, + ReauthRequired = 1 } diff --git a/src/CodeBeam.UltimateAuth.Client/Contracts/PkceClientState.cs b/src/CodeBeam.UltimateAuth.Client/Contracts/PkceClientState.cs index a8fcad43..a22da4f0 100644 --- a/src/CodeBeam.UltimateAuth.Client/Contracts/PkceClientState.cs +++ b/src/CodeBeam.UltimateAuth.Client/Contracts/PkceClientState.cs @@ -1,8 +1,7 @@ -namespace CodeBeam.UltimateAuth.Client.Contracts +namespace CodeBeam.UltimateAuth.Client.Contracts; + +internal sealed class PkceClientState { - internal sealed class PkceClientState - { - public string Verifier { get; init; } = default!; - public string AuthorizationCode { get; init; } = default!; - } + public string Verifier { get; init; } = default!; + public string AuthorizationCode { get; init; } = default!; } diff --git a/src/CodeBeam.UltimateAuth.Client/Contracts/RefreshResult.cs b/src/CodeBeam.UltimateAuth.Client/Contracts/RefreshResult.cs index d60efdbe..5e35d6ac 100644 --- a/src/CodeBeam.UltimateAuth.Client/Contracts/RefreshResult.cs +++ b/src/CodeBeam.UltimateAuth.Client/Contracts/RefreshResult.cs @@ -1,11 +1,10 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Client.Contracts +namespace CodeBeam.UltimateAuth.Client.Contracts; + +public sealed record RefreshResult { - public sealed record RefreshResult - { - public bool Ok { get; init; } - public int Status { get; init; } - public RefreshOutcome Outcome { get; init; } - } + public bool Ok { get; init; } + public int Status { get; init; } + public RefreshOutcome Outcome { get; init; } } diff --git a/src/CodeBeam.UltimateAuth.Client/Contracts/StorageScope.cs b/src/CodeBeam.UltimateAuth.Client/Contracts/StorageScope.cs index 9e823eef..322f397c 100644 --- a/src/CodeBeam.UltimateAuth.Client/Contracts/StorageScope.cs +++ b/src/CodeBeam.UltimateAuth.Client/Contracts/StorageScope.cs @@ -1,8 +1,7 @@ -namespace CodeBeam.UltimateAuth.Client.Contracts +namespace CodeBeam.UltimateAuth.Client.Contracts; + +public enum StorageScope { - public enum StorageScope - { - Session, - Local - } + Session, + Local } diff --git a/src/CodeBeam.UltimateAuth.Client/Contracts/UAuthTransportResult.cs b/src/CodeBeam.UltimateAuth.Client/Contracts/UAuthTransportResult.cs index 867cfe8c..747acb6d 100644 --- a/src/CodeBeam.UltimateAuth.Client/Contracts/UAuthTransportResult.cs +++ b/src/CodeBeam.UltimateAuth.Client/Contracts/UAuthTransportResult.cs @@ -1,12 +1,11 @@ using System.Text.Json; -namespace CodeBeam.UltimateAuth.Client.Contracts +namespace CodeBeam.UltimateAuth.Client.Contracts; + +public sealed class UAuthTransportResult { - public sealed class UAuthTransportResult - { - public bool Ok { get; init; } - public int Status { get; init; } - public string? RefreshOutcome { get; init; } - public JsonElement? Body { get; init; } - } + public bool Ok { get; init; } + public int Status { get; init; } + public string? RefreshOutcome { get; init; } + public JsonElement? Body { get; init; } } diff --git a/src/CodeBeam.UltimateAuth.Client/Device/IDeviceIdGenerator.cs b/src/CodeBeam.UltimateAuth.Client/Device/IDeviceIdGenerator.cs index b19b0dc7..033d82f0 100644 --- a/src/CodeBeam.UltimateAuth.Client/Device/IDeviceIdGenerator.cs +++ b/src/CodeBeam.UltimateAuth.Client/Device/IDeviceIdGenerator.cs @@ -1,9 +1,8 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Client.Device +namespace CodeBeam.UltimateAuth.Client.Device; + +public interface IDeviceIdGenerator { - public interface IDeviceIdGenerator - { - DeviceId Generate(); - } + DeviceId Generate(); } diff --git a/src/CodeBeam.UltimateAuth.Client/Device/IDeviceIdProvider.cs b/src/CodeBeam.UltimateAuth.Client/Device/IDeviceIdProvider.cs index f8983b5a..5bc079ef 100644 --- a/src/CodeBeam.UltimateAuth.Client/Device/IDeviceIdProvider.cs +++ b/src/CodeBeam.UltimateAuth.Client/Device/IDeviceIdProvider.cs @@ -1,9 +1,8 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Client.Device +namespace CodeBeam.UltimateAuth.Client.Device; + +public interface IDeviceIdProvider { - public interface IDeviceIdProvider - { - ValueTask GetOrCreateAsync(CancellationToken ct = default); - } + ValueTask GetOrCreateAsync(CancellationToken ct = default); } diff --git a/src/CodeBeam.UltimateAuth.Client/Device/IDeviceIdStorage.cs b/src/CodeBeam.UltimateAuth.Client/Device/IDeviceIdStorage.cs index c3555525..c91457d3 100644 --- a/src/CodeBeam.UltimateAuth.Client/Device/IDeviceIdStorage.cs +++ b/src/CodeBeam.UltimateAuth.Client/Device/IDeviceIdStorage.cs @@ -1,8 +1,7 @@ -namespace CodeBeam.UltimateAuth.Client.Device +namespace CodeBeam.UltimateAuth.Client.Device; + +public interface IDeviceIdStorage { - public interface IDeviceIdStorage - { - ValueTask LoadAsync(CancellationToken ct = default); - ValueTask SaveAsync(string deviceId, CancellationToken ct = default); - } + ValueTask LoadAsync(CancellationToken ct = default); + ValueTask SaveAsync(string deviceId, CancellationToken ct = default); } diff --git a/src/CodeBeam.UltimateAuth.Client/Extensions/LoginRequestFormExtensions.cs b/src/CodeBeam.UltimateAuth.Client/Extensions/LoginRequestFormExtensions.cs index a6e3e639..8e9defcb 100644 --- a/src/CodeBeam.UltimateAuth.Client/Extensions/LoginRequestFormExtensions.cs +++ b/src/CodeBeam.UltimateAuth.Client/Extensions/LoginRequestFormExtensions.cs @@ -1,15 +1,13 @@ using CodeBeam.UltimateAuth.Core.Contracts; -namespace CodeBeam.UltimateAuth.Client.Extensions -{ - internal static class LoginRequestFormExtensions - { - public static IDictionary ToDictionary(this LoginRequest request) - => new Dictionary - { - ["Identifier"] = request.Identifier, - ["Secret"] = request.Secret - }; - } +namespace CodeBeam.UltimateAuth.Client.Extensions; +internal static class LoginRequestFormExtensions +{ + public static IDictionary ToDictionary(this LoginRequest request) + => new Dictionary + { + ["Identifier"] = request.Identifier, + ["Secret"] = request.Secret + }; } diff --git a/src/CodeBeam.UltimateAuth.Client/Extensions/ServiceCollectionExtensions.cs b/src/CodeBeam.UltimateAuth.Client/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..e3046940 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Client/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,138 @@ +using CodeBeam.UltimateAuth.Client.Abstractions; +using CodeBeam.UltimateAuth.Client.Authentication; +using CodeBeam.UltimateAuth.Client.Device; +using CodeBeam.UltimateAuth.Client.Devices; +using CodeBeam.UltimateAuth.Client.Diagnostics; +using CodeBeam.UltimateAuth.Client.Infrastructure; +using CodeBeam.UltimateAuth.Client.Options; +using CodeBeam.UltimateAuth.Client.Runtime; +using CodeBeam.UltimateAuth.Client.Services; +using CodeBeam.UltimateAuth.Client.Utilities; +using CodeBeam.UltimateAuth.Core.Abstractions; +using CodeBeam.UltimateAuth.Core.Options; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; + +namespace CodeBeam.UltimateAuth.Client.Extensions; + +/// +/// Provides extension methods for registering UltimateAuth client services. +/// +/// This layer is responsible for: +/// - Client-side authentication actions (login, logout, refresh, reauth) +/// - Browser-based POST infrastructure (JS form submit) +/// - Endpoint configuration for auth mutations +/// +/// This extension can safely be used together with AddUltimateAuthServer() +/// in Blazor Server applications. +/// +public static class ServiceCollectionExtensions +{ + /// + /// Registers UltimateAuth client services using configuration binding + /// (e.g. appsettings.json). + /// + public static IServiceCollection AddUltimateAuthClient(this IServiceCollection services, IConfiguration configurationSection) + { + services.Configure(configurationSection); + return services.AddUltimateAuthClientInternal(); + } + + /// + /// Registers UltimateAuth client services using programmatic configuration. + /// + public static IServiceCollection AddUltimateAuthClient(this IServiceCollection services, Action configure) + { + services.Configure(configure); + return services.AddUltimateAuthClientInternal(); + } + + /// + /// Registers UltimateAuth client services with default (empty) configuration. + /// + /// Intended for advanced scenarios where configuration is fully controlled + /// by the hosting application or overridden later. + /// + public static IServiceCollection AddUltimateAuthClient(this IServiceCollection services) + { + services.Configure(_ => { }); + return services.AddUltimateAuthClientInternal(); + } + + /// + /// Internal shared registration pipeline for UltimateAuth client services. + /// + /// This method registers: + /// - Client infrastructure + /// - Public client abstractions + /// + /// NOTE: + /// This method does NOT register any server-side services. + /// + private static IServiceCollection AddUltimateAuthClientInternal(this IServiceCollection services) + { + // Options validation can be added here later if needed + // services.AddSingleton, ...>(); + + services.AddSingleton(); + services.AddSingleton, UAuthOptionsPostConfigure>(); + services.TryAddSingleton(); + + //services.PostConfigure(o => + //{ + // if (!o.AutoDetectClientProfile || o.ClientProfile != UAuthClientProfile.NotSpecified) + // return; + + // using var sp = services.BuildServiceProvider(); + // var detector = sp.GetRequiredService(); + // o.ClientProfile = detector.Detect(sp); + //}); + + services.PostConfigure(o => + { + o.Refresh.Interval ??= TimeSpan.FromMinutes(5); + }); + + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + + services.AddScoped(sp => + { + var core = sp.GetRequiredService>().Value; + + return core.ClientProfile == UAuthClientProfile.BlazorServer + ? sp.GetRequiredService() + : sp.GetRequiredService(); + }); + + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + services.AddScoped(); + services.AddScoped>(sp => sp.GetRequiredService()); + + return services; + } +} diff --git a/src/CodeBeam.UltimateAuth.Client/Extensions/UltimateAuthClientServiceCollectionExtensions.cs b/src/CodeBeam.UltimateAuth.Client/Extensions/UltimateAuthClientServiceCollectionExtensions.cs deleted file mode 100644 index c867a03a..00000000 --- a/src/CodeBeam.UltimateAuth.Client/Extensions/UltimateAuthClientServiceCollectionExtensions.cs +++ /dev/null @@ -1,139 +0,0 @@ -using CodeBeam.UltimateAuth.Client.Abstractions; -using CodeBeam.UltimateAuth.Client.Authentication; -using CodeBeam.UltimateAuth.Client.Device; -using CodeBeam.UltimateAuth.Client.Devices; -using CodeBeam.UltimateAuth.Client.Diagnostics; -using CodeBeam.UltimateAuth.Client.Infrastructure; -using CodeBeam.UltimateAuth.Client.Options; -using CodeBeam.UltimateAuth.Client.Runtime; -using CodeBeam.UltimateAuth.Client.Services; -using CodeBeam.UltimateAuth.Client.Utilities; -using CodeBeam.UltimateAuth.Core.Abstractions; -using CodeBeam.UltimateAuth.Core.Options; -using Microsoft.AspNetCore.Components; -using Microsoft.AspNetCore.Components.Authorization; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Options; - -namespace CodeBeam.UltimateAuth.Client.Extensions -{ - /// - /// Provides extension methods for registering UltimateAuth client services. - /// - /// This layer is responsible for: - /// - Client-side authentication actions (login, logout, refresh, reauth) - /// - Browser-based POST infrastructure (JS form submit) - /// - Endpoint configuration for auth mutations - /// - /// This extension can safely be used together with AddUltimateAuthServer() - /// in Blazor Server applications. - /// - public static class UltimateAuthClientServiceCollectionExtensions - { - /// - /// Registers UltimateAuth client services using configuration binding - /// (e.g. appsettings.json). - /// - public static IServiceCollection AddUltimateAuthClient(this IServiceCollection services, IConfiguration configurationSection) - { - services.Configure(configurationSection); - return services.AddUltimateAuthClientInternal(); - } - - /// - /// Registers UltimateAuth client services using programmatic configuration. - /// - public static IServiceCollection AddUltimateAuthClient(this IServiceCollection services, Action configure) - { - services.Configure(configure); - return services.AddUltimateAuthClientInternal(); - } - - /// - /// Registers UltimateAuth client services with default (empty) configuration. - /// - /// Intended for advanced scenarios where configuration is fully controlled - /// by the hosting application or overridden later. - /// - public static IServiceCollection AddUltimateAuthClient(this IServiceCollection services) - { - services.Configure(_ => { }); - return services.AddUltimateAuthClientInternal(); - } - - /// - /// Internal shared registration pipeline for UltimateAuth client services. - /// - /// This method registers: - /// - Client infrastructure - /// - Public client abstractions - /// - /// NOTE: - /// This method does NOT register any server-side services. - /// - private static IServiceCollection AddUltimateAuthClientInternal(this IServiceCollection services) - { - // Options validation can be added here later if needed - // services.AddSingleton, ...>(); - - services.AddSingleton(); - services.AddSingleton, UAuthOptionsPostConfigure>(); - services.TryAddSingleton(); - - //services.PostConfigure(o => - //{ - // if (!o.AutoDetectClientProfile || o.ClientProfile != UAuthClientProfile.NotSpecified) - // return; - - // using var sp = services.BuildServiceProvider(); - // var detector = sp.GetRequiredService(); - // o.ClientProfile = detector.Detect(sp); - //}); - - services.PostConfigure(o => - { - o.Refresh.Interval ??= TimeSpan.FromMinutes(5); - }); - - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - - services.AddScoped(sp => - { - var core = sp.GetRequiredService>().Value; - - return core.ClientProfile == UAuthClientProfile.BlazorServer - ? sp.GetRequiredService() - : sp.GetRequiredService(); - }); - - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - - services.AddScoped(); - services.AddScoped>(sp => sp.GetRequiredService()); - - return services; - } - } -} diff --git a/src/CodeBeam.UltimateAuth.Client/Infrastructure/BlazorServerSessionCoordinator.cs b/src/CodeBeam.UltimateAuth.Client/Infrastructure/BlazorServerSessionCoordinator.cs index 0b0d06ed..0857f31b 100644 --- a/src/CodeBeam.UltimateAuth.Client/Infrastructure/BlazorServerSessionCoordinator.cs +++ b/src/CodeBeam.UltimateAuth.Client/Infrastructure/BlazorServerSessionCoordinator.cs @@ -6,98 +6,97 @@ using Microsoft.AspNetCore.Components; using Microsoft.Extensions.Options; -namespace CodeBeam.UltimateAuth.Client.Infrastructure +namespace CodeBeam.UltimateAuth.Client.Infrastructure; + +internal sealed class BlazorServerSessionCoordinator : ISessionCoordinator { - internal sealed class BlazorServerSessionCoordinator : ISessionCoordinator - { - private readonly IUAuthClient _client; - private readonly NavigationManager _navigation; - private readonly UAuthClientOptions _options; - private readonly UAuthClientDiagnostics _diagnostics; + private readonly IUAuthClient _client; + private readonly NavigationManager _navigation; + private readonly UAuthClientOptions _options; + private readonly UAuthClientDiagnostics _diagnostics; - private PeriodicTimer? _timer; - private CancellationTokenSource? _cts; + private PeriodicTimer? _timer; + private CancellationTokenSource? _cts; - public event Action? ReauthRequired; + public event Action? ReauthRequired; - public BlazorServerSessionCoordinator(IUAuthClient client, NavigationManager navigation, IOptions options, UAuthClientDiagnostics diagnostics) - { - _client = client; - _navigation = navigation; - _options = options.Value; - _diagnostics = diagnostics; - } + public BlazorServerSessionCoordinator(IUAuthClient client, NavigationManager navigation, IOptions options, UAuthClientDiagnostics diagnostics) + { + _client = client; + _navigation = navigation; + _options = options.Value; + _diagnostics = diagnostics; + } - public async Task StartAsync(CancellationToken cancellationToken = default) - { - if (_timer is not null) - return; + public async Task StartAsync(CancellationToken cancellationToken = default) + { + if (_timer is not null) + return; - _diagnostics.MarkStarted(); - _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - var interval = _options.Refresh.Interval ?? TimeSpan.FromMinutes(5); - _timer = new PeriodicTimer(interval); + _diagnostics.MarkStarted(); + _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + var interval = _options.Refresh.Interval ?? TimeSpan.FromMinutes(5); + _timer = new PeriodicTimer(interval); - _ = RunAsync(_cts.Token); - } + _ = RunAsync(_cts.Token); + } - private async Task RunAsync(CancellationToken ct) + private async Task RunAsync(CancellationToken ct) + { + try { - try + while (await _timer!.WaitForNextTickAsync(ct)) { - while (await _timer!.WaitForNextTickAsync(ct)) + _diagnostics.MarkAutomaticRefresh(); + var result = await _client.Flows.RefreshAsync(isAuto: true); + + switch (result.Outcome) { - _diagnostics.MarkAutomaticRefresh(); - var result = await _client.Flows.RefreshAsync(isAuto: true); - - switch (result.Outcome) - { - case RefreshOutcome.Touched: - break; - - case RefreshOutcome.NoOp: - break; - - case RefreshOutcome.None: - break; - - case RefreshOutcome.ReauthRequired: - switch (_options.Reauth.Behavior) - { - case ReauthBehavior.RedirectToLogin: - _navigation.NavigateTo(_options.Reauth.LoginPath, forceLoad: true); - break; - - case ReauthBehavior.RaiseEvent: - ReauthRequired?.Invoke(); - break; - - case ReauthBehavior.None: - break; - } - _diagnostics.MarkTerminated(CoordinatorTerminationReason.ReauthRequired); - return; - } + case RefreshOutcome.Touched: + break; + + case RefreshOutcome.NoOp: + break; + + case RefreshOutcome.None: + break; + + case RefreshOutcome.ReauthRequired: + switch (_options.Reauth.Behavior) + { + case ReauthBehavior.RedirectToLogin: + _navigation.NavigateTo(_options.Reauth.LoginPath, forceLoad: true); + break; + + case ReauthBehavior.RaiseEvent: + ReauthRequired?.Invoke(); + break; + + case ReauthBehavior.None: + break; + } + _diagnostics.MarkTerminated(CoordinatorTerminationReason.ReauthRequired); + return; } } - catch (OperationCanceledException) - { - // expected - } } - - public Task StopAsync() + catch (OperationCanceledException) { - _diagnostics.MarkStopped(); - _cts?.Cancel(); - _timer?.Dispose(); - _timer = null; - return Task.CompletedTask; + // expected } + } - public async ValueTask DisposeAsync() - { - await StopAsync(); - } + public Task StopAsync() + { + _diagnostics.MarkStopped(); + _cts?.Cancel(); + _timer?.Dispose(); + _timer = null; + return Task.CompletedTask; + } + + public async ValueTask DisposeAsync() + { + await StopAsync(); } } diff --git a/src/CodeBeam.UltimateAuth.Client/Infrastructure/BrowserStorage.cs b/src/CodeBeam.UltimateAuth.Client/Infrastructure/BrowserStorage.cs index b62442d2..e270af2a 100644 --- a/src/CodeBeam.UltimateAuth.Client/Infrastructure/BrowserStorage.cs +++ b/src/CodeBeam.UltimateAuth.Client/Infrastructure/BrowserStorage.cs @@ -1,30 +1,29 @@ using CodeBeam.UltimateAuth.Client.Contracts; using Microsoft.JSInterop; -namespace CodeBeam.UltimateAuth.Client.Utilities +namespace CodeBeam.UltimateAuth.Client.Utilities; + +public sealed class BrowserStorage : IBrowserStorage { - public sealed class BrowserStorage : IBrowserStorage - { - private readonly IJSRuntime _js; + private readonly IJSRuntime _js; - public BrowserStorage(IJSRuntime js) - { - _js = js; - } + public BrowserStorage(IJSRuntime js) + { + _js = js; + } - public ValueTask SetAsync(StorageScope scope, string key, string value) - => _js.InvokeVoidAsync("uauth.storage.set", Scope(scope), key, value); + public ValueTask SetAsync(StorageScope scope, string key, string value) + => _js.InvokeVoidAsync("uauth.storage.set", Scope(scope), key, value); - public ValueTask GetAsync(StorageScope scope, string key) - => _js.InvokeAsync("uauth.storage.get", Scope(scope), key); + public ValueTask GetAsync(StorageScope scope, string key) + => _js.InvokeAsync("uauth.storage.get", Scope(scope), key); - public ValueTask RemoveAsync(StorageScope scope, string key) - => _js.InvokeVoidAsync("uauth.storage.remove", Scope(scope), key); + public ValueTask RemoveAsync(StorageScope scope, string key) + => _js.InvokeVoidAsync("uauth.storage.remove", Scope(scope), key); - public async ValueTask ExistsAsync(StorageScope scope, string key) - => await _js.InvokeAsync("uauth.storage.exists", Scope(scope), key); + public async ValueTask ExistsAsync(StorageScope scope, string key) + => await _js.InvokeAsync("uauth.storage.exists", Scope(scope), key); - private static string Scope(StorageScope scope) - => scope == StorageScope.Local ? "local" : "session"; - } + private static string Scope(StorageScope scope) + => scope == StorageScope.Local ? "local" : "session"; } diff --git a/src/CodeBeam.UltimateAuth.Client/Infrastructure/IUAuthRequestClient.cs b/src/CodeBeam.UltimateAuth.Client/Infrastructure/IUAuthRequestClient.cs index f93dd98d..484d9b8f 100644 --- a/src/CodeBeam.UltimateAuth.Client/Infrastructure/IUAuthRequestClient.cs +++ b/src/CodeBeam.UltimateAuth.Client/Infrastructure/IUAuthRequestClient.cs @@ -1,15 +1,14 @@ using CodeBeam.UltimateAuth.Client.Contracts; -namespace CodeBeam.UltimateAuth.Client.Infrastructure +namespace CodeBeam.UltimateAuth.Client.Infrastructure; + +public interface IUAuthRequestClient { - public interface IUAuthRequestClient - { - Task NavigateAsync(string endpoint, IDictionary? form = null, CancellationToken ct = default); + Task NavigateAsync(string endpoint, IDictionary? form = null, CancellationToken ct = default); - Task SendFormAsync(string endpoint, IDictionary? form = null, CancellationToken ct = default); + Task SendFormAsync(string endpoint, IDictionary? form = null, CancellationToken ct = default); - Task SendFormForJsonAsync(string endpoint, IDictionary? form = null, CancellationToken ct = default); + Task SendFormForJsonAsync(string endpoint, IDictionary? form = null, CancellationToken ct = default); - Task SendJsonAsync(string endpoint, object? payload = null, CancellationToken ct = default); - } + Task SendJsonAsync(string endpoint, object? payload = null, CancellationToken ct = default); } diff --git a/src/CodeBeam.UltimateAuth.Client/Infrastructure/NoOpHubCapabilities.cs b/src/CodeBeam.UltimateAuth.Client/Infrastructure/NoOpHubCapabilities.cs index 24042f5c..002a7c27 100644 --- a/src/CodeBeam.UltimateAuth.Client/Infrastructure/NoOpHubCapabilities.cs +++ b/src/CodeBeam.UltimateAuth.Client/Infrastructure/NoOpHubCapabilities.cs @@ -1,9 +1,8 @@ using CodeBeam.UltimateAuth.Core.Abstractions; -namespace CodeBeam.UltimateAuth.Client.Infrastructure +namespace CodeBeam.UltimateAuth.Client.Infrastructure; + +internal sealed class NoOpHubCapabilities : IHubCapabilities { - internal sealed class NoOpHubCapabilities : IHubCapabilities - { - public bool SupportsPkce => false; - } + public bool SupportsPkce => false; } diff --git a/src/CodeBeam.UltimateAuth.Client/Infrastructure/NoOpHubCredentialResolver.cs b/src/CodeBeam.UltimateAuth.Client/Infrastructure/NoOpHubCredentialResolver.cs index 658a8653..69c2b989 100644 --- a/src/CodeBeam.UltimateAuth.Client/Infrastructure/NoOpHubCredentialResolver.cs +++ b/src/CodeBeam.UltimateAuth.Client/Infrastructure/NoOpHubCredentialResolver.cs @@ -1,10 +1,9 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Client.Infrastructure +namespace CodeBeam.UltimateAuth.Client.Infrastructure; + +internal sealed class NoOpHubCredentialResolver : IHubCredentialResolver { - internal sealed class NoOpHubCredentialResolver : IHubCredentialResolver - { - public Task ResolveAsync(HubSessionId sessionId, CancellationToken ct = default) => Task.FromResult(null); - } + public Task ResolveAsync(HubSessionId sessionId, CancellationToken ct = default) => Task.FromResult(null); } diff --git a/src/CodeBeam.UltimateAuth.Client/Infrastructure/NoOpHubFlowReader.cs b/src/CodeBeam.UltimateAuth.Client/Infrastructure/NoOpHubFlowReader.cs index 9b6a7768..9cdfa747 100644 --- a/src/CodeBeam.UltimateAuth.Client/Infrastructure/NoOpHubFlowReader.cs +++ b/src/CodeBeam.UltimateAuth.Client/Infrastructure/NoOpHubFlowReader.cs @@ -1,10 +1,9 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Client.Infrastructure +namespace CodeBeam.UltimateAuth.Client.Infrastructure; + +internal sealed class NoOpHubFlowReader : IHubFlowReader { - internal sealed class NoOpHubFlowReader : IHubFlowReader - { - public Task GetStateAsync(HubSessionId sessionId, CancellationToken ct = default) => Task.FromResult(null); - } + public Task GetStateAsync(HubSessionId sessionId, CancellationToken ct = default) => Task.FromResult(null); } diff --git a/src/CodeBeam.UltimateAuth.Client/Infrastructure/NoOpSessionCoordinator.cs b/src/CodeBeam.UltimateAuth.Client/Infrastructure/NoOpSessionCoordinator.cs index 5784d106..19fb3d96 100644 --- a/src/CodeBeam.UltimateAuth.Client/Infrastructure/NoOpSessionCoordinator.cs +++ b/src/CodeBeam.UltimateAuth.Client/Infrastructure/NoOpSessionCoordinator.cs @@ -1,18 +1,12 @@ using CodeBeam.UltimateAuth.Client.Abstractions; -namespace CodeBeam.UltimateAuth.Client.Infrastructure -{ - internal sealed class NoOpSessionCoordinator : ISessionCoordinator - { - public event Action? ReauthRequired; - - public Task StartAsync(CancellationToken cancellationToken = default) - => Task.CompletedTask; +namespace CodeBeam.UltimateAuth.Client.Infrastructure; - public Task StopAsync() - => Task.CompletedTask; +internal sealed class NoOpSessionCoordinator : ISessionCoordinator +{ + public event Action? ReauthRequired; - public ValueTask DisposeAsync() - => ValueTask.CompletedTask; - } + public Task StartAsync(CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task StopAsync() => Task.CompletedTask; + public ValueTask DisposeAsync() => ValueTask.CompletedTask; } diff --git a/src/CodeBeam.UltimateAuth.Client/Infrastructure/RefreshOutcomeParser.cs b/src/CodeBeam.UltimateAuth.Client/Infrastructure/RefreshOutcomeParser.cs index b5ccf9a1..88cb7aa5 100644 --- a/src/CodeBeam.UltimateAuth.Client/Infrastructure/RefreshOutcomeParser.cs +++ b/src/CodeBeam.UltimateAuth.Client/Infrastructure/RefreshOutcomeParser.cs @@ -1,21 +1,20 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Client.Infrastructure +namespace CodeBeam.UltimateAuth.Client.Infrastructure; + +internal static class RefreshOutcomeParser { - internal static class RefreshOutcomeParser + public static RefreshOutcome Parse(string? value) { - public static RefreshOutcome Parse(string? value) - { - if (string.IsNullOrWhiteSpace(value)) - return RefreshOutcome.None; + if (string.IsNullOrWhiteSpace(value)) + return RefreshOutcome.None; - return value switch - { - "no-op" => RefreshOutcome.NoOp, - "touched" => RefreshOutcome.Touched, - "reauth-required" => RefreshOutcome.ReauthRequired, - _ => RefreshOutcome.None - }; - } + return value switch + { + "no-op" => RefreshOutcome.NoOp, + "touched" => RefreshOutcome.Touched, + "reauth-required" => RefreshOutcome.ReauthRequired, + _ => RefreshOutcome.None + }; } } diff --git a/src/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthRequestClient.cs b/src/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthRequestClient.cs index beb15f80..bedb3795 100644 --- a/src/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthRequestClient.cs +++ b/src/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthRequestClient.cs @@ -5,75 +5,74 @@ using Microsoft.JSInterop; // TODO: Add fluent helper API like RequiredOk -namespace CodeBeam.UltimateAuth.Client.Infrastructure +namespace CodeBeam.UltimateAuth.Client.Infrastructure; + +internal sealed class UAuthRequestClient : IUAuthRequestClient { - internal sealed class UAuthRequestClient : IUAuthRequestClient + private readonly IJSRuntime _js; + private UAuthOptions _coreOptions; + + public UAuthRequestClient(IJSRuntime js, IOptions coreOptions) { - private readonly IJSRuntime _js; - private UAuthOptions _coreOptions; + _js = js; + _coreOptions = coreOptions.Value; + } - public UAuthRequestClient(IJSRuntime js, IOptions coreOptions) - { - _js = js; - _coreOptions = coreOptions.Value; - } + public Task NavigateAsync(string endpoint, IDictionary? form = null, CancellationToken ct = default) + { + ct.ThrowIfCancellationRequested(); - public Task NavigateAsync(string endpoint, IDictionary? form = null, CancellationToken ct = default) + return _js.InvokeVoidAsync("uauth.post", ct, new { - ct.ThrowIfCancellationRequested(); + url = endpoint, + mode = "navigate", + data = form, + clientProfile = _coreOptions.ClientProfile.ToString() + }).AsTask(); + } - return _js.InvokeVoidAsync("uauth.post", ct, new - { - url = endpoint, - mode = "navigate", - data = form, - clientProfile = _coreOptions.ClientProfile.ToString() - }).AsTask(); - } + public async Task SendFormAsync(string endpoint, IDictionary? form = null, CancellationToken ct = default) + { + ct.ThrowIfCancellationRequested(); - public async Task SendFormAsync(string endpoint, IDictionary? form = null, CancellationToken ct = default) + var result = await _js.InvokeAsync("uauth.post", ct, new { - ct.ThrowIfCancellationRequested(); + url = endpoint, + mode = "fetch", + expectJson = false, + data = form, + clientProfile = _coreOptions.ClientProfile.ToString() + }); + + return result; + } + + public async Task SendFormForJsonAsync(string endpoint, IDictionary? form = null, CancellationToken ct = default) + { + ct.ThrowIfCancellationRequested(); - var result = await _js.InvokeAsync("uauth.post", ct, new + var postData = form ?? new Dictionary(); + return await _js.InvokeAsync("uauth.post", ct, + new { url = endpoint, mode = "fetch", - expectJson = false, - data = form, + expectJson = true, + data = postData, clientProfile = _coreOptions.ClientProfile.ToString() }); + } - return result; - } - - public async Task SendFormForJsonAsync(string endpoint, IDictionary? form = null, CancellationToken ct = default) - { - ct.ThrowIfCancellationRequested(); - - var postData = form ?? new Dictionary(); - return await _js.InvokeAsync("uauth.post", ct, - new - { - url = endpoint, - mode = "fetch", - expectJson = true, - data = postData, - clientProfile = _coreOptions.ClientProfile.ToString() - }); - } + public async Task SendJsonAsync(string endpoint, object? payload = default, CancellationToken ct = default) + { + ct.ThrowIfCancellationRequested(); - public async Task SendJsonAsync(string endpoint, object? payload = default, CancellationToken ct = default) + return await _js.InvokeAsync("uauth.postJson", ct, new { - ct.ThrowIfCancellationRequested(); - - return await _js.InvokeAsync("uauth.postJson", ct, new - { - url = endpoint, - payload = payload, - clientProfile = _coreOptions.ClientProfile.ToString() - }); - } - + url = endpoint, + payload = payload, + clientProfile = _coreOptions.ClientProfile.ToString() + }); } + } diff --git a/src/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthResultMapper.cs b/src/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthResultMapper.cs index 245575ee..cf4ebfea 100644 --- a/src/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthResultMapper.cs +++ b/src/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthResultMapper.cs @@ -2,50 +2,49 @@ using CodeBeam.UltimateAuth.Core.Contracts; using System.Text.Json; -namespace CodeBeam.UltimateAuth.Client.Infrastructure +namespace CodeBeam.UltimateAuth.Client.Infrastructure; + +internal static class UAuthResultMapper { - internal static class UAuthResultMapper + public static UAuthResult FromJson(UAuthTransportResult raw) { - public static UAuthResult FromJson(UAuthTransportResult raw) + if (!raw.Ok) { - if (!raw.Ok) - { - return new UAuthResult - { - Ok = false, - Status = raw.Status - }; - } - - if (raw.Body is null) + return new UAuthResult { - return new UAuthResult - { - Ok = true, - Status = raw.Status, - Value = default - }; - } - - var value = raw.Body.Value.Deserialize( - new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true - }); + Ok = false, + Status = raw.Status + }; + } + if (raw.Body is null) + { return new UAuthResult { Ok = true, Status = raw.Status, - Value = value + Value = default }; } - public static UAuthResult FromStatus(UAuthTransportResult raw) - => new() + var value = raw.Body.Value.Deserialize( + new JsonSerializerOptions { - Ok = raw.Ok, - Status = raw.Status - }; + PropertyNameCaseInsensitive = true + }); + + return new UAuthResult + { + Ok = true, + Status = raw.Status, + Value = value + }; } + + public static UAuthResult FromStatus(UAuthTransportResult raw) + => new() + { + Ok = raw.Ok, + Status = raw.Status + }; } diff --git a/src/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthUrlBuilder.cs b/src/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthUrlBuilder.cs index 6f88564f..2d4cbb74 100644 --- a/src/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthUrlBuilder.cs +++ b/src/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthUrlBuilder.cs @@ -1,10 +1,9 @@ -namespace CodeBeam.UltimateAuth.Client.Infrastructure +namespace CodeBeam.UltimateAuth.Client.Infrastructure; + +internal static class UAuthUrlBuilder { - internal static class UAuthUrlBuilder + public static string Combine(string authority, string relative) { - public static string Combine(string authority, string relative) - { - return authority.TrimEnd('/') + "/" + relative.TrimStart('/'); - } + return authority.TrimEnd('/') + "/" + relative.TrimStart('/'); } } diff --git a/src/CodeBeam.UltimateAuth.Client/Options/PkceLoginOptions.cs b/src/CodeBeam.UltimateAuth.Client/Options/PkceLoginOptions.cs index 4f8f3ba8..2f82cd8f 100644 --- a/src/CodeBeam.UltimateAuth.Client/Options/PkceLoginOptions.cs +++ b/src/CodeBeam.UltimateAuth.Client/Options/PkceLoginOptions.cs @@ -1,27 +1,25 @@ using CodeBeam.UltimateAuth.Core.Contracts; -namespace CodeBeam.UltimateAuth.Client.Options -{ - public sealed class PkceLoginOptions - { - /// - /// Enables PKCE login support. - /// - public bool Enabled { get; set; } = true; +namespace CodeBeam.UltimateAuth.Client.Options; - public string? ReturnUrl { get; init; } +public sealed class PkceLoginOptions +{ + /// + /// Enables PKCE login support. + /// + public bool Enabled { get; set; } = true; - /// - /// Called after authorization_code is issued, - /// before redirecting to the Hub. - /// - public Func? OnAuthorized { get; init; } + public string? ReturnUrl { get; init; } - /// - /// If false, BeginPkceAsync will NOT redirect automatically. - /// Caller is responsible for navigation. - /// - public bool AutoRedirect { get; init; } = true; - } + /// + /// Called after authorization_code is issued, + /// before redirecting to the Hub. + /// + public Func? OnAuthorized { get; init; } + /// + /// If false, BeginPkceAsync will NOT redirect automatically. + /// Caller is responsible for navigation. + /// + public bool AutoRedirect { get; init; } = true; } diff --git a/src/CodeBeam.UltimateAuth.Client/Options/UAuthClientOptions.cs b/src/CodeBeam.UltimateAuth.Client/Options/UAuthClientOptions.cs index 94f0af80..0aabb867 100644 --- a/src/CodeBeam.UltimateAuth.Client/Options/UAuthClientOptions.cs +++ b/src/CodeBeam.UltimateAuth.Client/Options/UAuthClientOptions.cs @@ -1,75 +1,74 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Client.Options +namespace CodeBeam.UltimateAuth.Client.Options; + +public sealed class UAuthClientOptions { - public sealed class UAuthClientOptions - { - public AuthEndpointOptions Endpoints { get; set; } = new(); - public LoginOptions Login { get; set; } = new(); - public UAuthClientRefreshOptions Refresh { get; set; } = new(); - public ReauthOptions Reauth { get; init; } = new(); - } + public AuthEndpointOptions Endpoints { get; set; } = new(); + public LoginOptions Login { get; set; } = new(); + public UAuthClientRefreshOptions Refresh { get; set; } = new(); + public ReauthOptions Reauth { get; init; } = new(); +} - public sealed class AuthEndpointOptions - { - /// - /// Base URL of UAuthHub (e.g. https://localhost:6110) - /// - public string Authority { get; set; } = "/auth"; +public sealed class AuthEndpointOptions +{ + /// + /// Base URL of UAuthHub (e.g. https://localhost:6110) + /// + public string Authority { get; set; } = "/auth"; - public string Login { get; set; } = "/login"; - public string Logout { get; set; } = "/logout"; - public string Refresh { get; set; } = "/refresh"; - public string Reauth { get; set; } = "/reauth"; - public string Validate { get; set; } = "/validate"; - public string PkceAuthorize { get; set; } = "/pkce/authorize"; - public string PkceComplete { get; set; } = "/pkce/complete"; - public string HubLoginPath { get; set; } = "/uauthhub/login"; - } + public string Login { get; set; } = "/login"; + public string Logout { get; set; } = "/logout"; + public string Refresh { get; set; } = "/refresh"; + public string Reauth { get; set; } = "/reauth"; + public string Validate { get; set; } = "/validate"; + public string PkceAuthorize { get; set; } = "/pkce/authorize"; + public string PkceComplete { get; set; } = "/pkce/complete"; + public string HubLoginPath { get; set; } = "/uauthhub/login"; +} - public sealed class LoginOptions - { - /// - /// Default return URL after a successful login flow. - /// If not set, current location will be used. - /// - public string? DefaultReturnUrl { get; set; } +public sealed class LoginOptions +{ + /// + /// Default return URL after a successful login flow. + /// If not set, current location will be used. + /// + public string? DefaultReturnUrl { get; set; } - /// - /// Options related to PKCE-based login flows. - /// - public PkceLoginOptions Pkce { get; set; } = new(); + /// + /// Options related to PKCE-based login flows. + /// + public PkceLoginOptions Pkce { get; set; } = new(); - /// - /// Enables or disables direct credential-based login. - /// - public bool AllowDirectLogin { get; set; } = true; - } + /// + /// Enables or disables direct credential-based login. + /// + public bool AllowDirectLogin { get; set; } = true; +} - public sealed class UAuthClientRefreshOptions - { - /// - /// Enables background refresh coordination. - /// Default: true for BlazorServer, false otherwise. - /// - public bool Enabled { get; set; } = true; +public sealed class UAuthClientRefreshOptions +{ + /// + /// Enables background refresh coordination. + /// Default: true for BlazorServer, false otherwise. + /// + public bool Enabled { get; set; } = true; - /// - /// Interval for background refresh attempts. - /// This is a UX / keep-alive setting, NOT a security policy. - /// - public TimeSpan? Interval { get; set; } + /// + /// Interval for background refresh attempts. + /// This is a UX / keep-alive setting, NOT a security policy. + /// + public TimeSpan? Interval { get; set; } - /// - /// Optional jitter to avoid synchronized refresh storms. - /// - public TimeSpan? Jitter { get; set; } - } + /// + /// Optional jitter to avoid synchronized refresh storms. + /// + public TimeSpan? Jitter { get; set; } +} - // TODO: Add ClearCookieOnReauth - public sealed class ReauthOptions - { - public ReauthBehavior Behavior { get; set; } = ReauthBehavior.RedirectToLogin; - public string LoginPath { get; set; } = "/login"; - } +// TODO: Add ClearCookieOnReauth +public sealed class ReauthOptions +{ + public ReauthBehavior Behavior { get; set; } = ReauthBehavior.RedirectToLogin; + public string LoginPath { get; set; } = "/login"; } diff --git a/src/CodeBeam.UltimateAuth.Client/Options/UAuthClientProfileDetector.cs b/src/CodeBeam.UltimateAuth.Client/Options/UAuthClientProfileDetector.cs index 5d3e8a3a..96e06a19 100644 --- a/src/CodeBeam.UltimateAuth.Client/Options/UAuthClientProfileDetector.cs +++ b/src/CodeBeam.UltimateAuth.Client/Options/UAuthClientProfileDetector.cs @@ -2,30 +2,29 @@ using CodeBeam.UltimateAuth.Core.Runtime; using Microsoft.Extensions.DependencyInjection; -namespace CodeBeam.UltimateAuth.Client.Options +namespace CodeBeam.UltimateAuth.Client.Options; + +internal sealed class UAuthClientProfileDetector : IClientProfileDetector { - internal sealed class UAuthClientProfileDetector : IClientProfileDetector + public UAuthClientProfile Detect(IServiceProvider sp) { - public UAuthClientProfile Detect(IServiceProvider sp) - { - if (sp.GetService() != null) - return UAuthClientProfile.UAuthHub; - - if (Type.GetType("Microsoft.Maui.Controls.Application, Microsoft.Maui.Controls", throwOnError: false) is not null) - return UAuthClientProfile.Maui; + if (sp.GetService() != null) + return UAuthClientProfile.UAuthHub; - if (AppDomain.CurrentDomain.GetAssemblies().Any(a => a.GetName().Name == "Microsoft.AspNetCore.Components.WebAssembly")) - return UAuthClientProfile.BlazorWasm; + if (Type.GetType("Microsoft.Maui.Controls.Application, Microsoft.Maui.Controls", throwOnError: false) is not null) + return UAuthClientProfile.Maui; - // Warning: This detection method may not be 100% reliable in all hosting scenarios. - if (AppDomain.CurrentDomain.GetAssemblies().Any(a => a.GetName().Name == "Microsoft.AspNetCore.Components.Server")) - { - return UAuthClientProfile.BlazorServer; - } + if (AppDomain.CurrentDomain.GetAssemblies().Any(a => a.GetName().Name == "Microsoft.AspNetCore.Components.WebAssembly")) + return UAuthClientProfile.BlazorWasm; - // Default to WebServer profile for other ASP.NET Core scenarios such as MVC, Razor Pages, minimal APIs, etc. - // NotSpecified should only be used when user explicitly sets it. (For example in unit tests) - return UAuthClientProfile.WebServer; + // Warning: This detection method may not be 100% reliable in all hosting scenarios. + if (AppDomain.CurrentDomain.GetAssemblies().Any(a => a.GetName().Name == "Microsoft.AspNetCore.Components.Server")) + { + return UAuthClientProfile.BlazorServer; } + + // Default to WebServer profile for other ASP.NET Core scenarios such as MVC, Razor Pages, minimal APIs, etc. + // NotSpecified should only be used when user explicitly sets it. (For example in unit tests) + return UAuthClientProfile.WebServer; } } diff --git a/src/CodeBeam.UltimateAuth.Client/Options/UAuthOptionsPostConfigure.cs b/src/CodeBeam.UltimateAuth.Client/Options/UAuthOptionsPostConfigure.cs index b99dccde..4c2aa91c 100644 --- a/src/CodeBeam.UltimateAuth.Client/Options/UAuthOptionsPostConfigure.cs +++ b/src/CodeBeam.UltimateAuth.Client/Options/UAuthOptionsPostConfigure.cs @@ -1,28 +1,27 @@ using CodeBeam.UltimateAuth.Core.Options; using Microsoft.Extensions.Options; -namespace CodeBeam.UltimateAuth.Client.Infrastructure +namespace CodeBeam.UltimateAuth.Client.Infrastructure; + +internal sealed class UAuthOptionsPostConfigure : IPostConfigureOptions { - internal sealed class UAuthOptionsPostConfigure : IPostConfigureOptions - { - private readonly IClientProfileDetector _detector; - private readonly IServiceProvider _services; + private readonly IClientProfileDetector _detector; + private readonly IServiceProvider _services; - public UAuthOptionsPostConfigure(IClientProfileDetector detector, IServiceProvider services) - { - _detector = detector; - _services = services; - } + public UAuthOptionsPostConfigure(IClientProfileDetector detector, IServiceProvider services) + { + _detector = detector; + _services = services; + } - public void PostConfigure(string? name, UAuthOptions options) - { - if (!options.AutoDetectClientProfile) - return; + public void PostConfigure(string? name, UAuthOptions options) + { + if (!options.AutoDetectClientProfile) + return; - if (options.ClientProfile != UAuthClientProfile.NotSpecified) - return; + if (options.ClientProfile != UAuthClientProfile.NotSpecified) + return; - options.ClientProfile = _detector.Detect(_services); - } + options.ClientProfile = _detector.Detect(_services); } } diff --git a/src/CodeBeam.UltimateAuth.Client/ProductInfo/UAuthClientProductInfo.cs b/src/CodeBeam.UltimateAuth.Client/ProductInfo/UAuthClientProductInfo.cs index 4ad5ad14..50f66883 100644 --- a/src/CodeBeam.UltimateAuth.Client/ProductInfo/UAuthClientProductInfo.cs +++ b/src/CodeBeam.UltimateAuth.Client/ProductInfo/UAuthClientProductInfo.cs @@ -1,10 +1,9 @@ using CodeBeam.UltimateAuth.Core.Runtime; -namespace CodeBeam.UltimateAuth.Client.Runtime +namespace CodeBeam.UltimateAuth.Client.Runtime; + +public sealed class UAuthClientProductInfo { - public sealed class UAuthClientProductInfo - { - public string ProductName { get; init; } = "UltimateAuthClient"; - public UAuthProductInfo Core { get; init; } = default!; - } + public string ProductName { get; init; } = "UltimateAuthClient"; + public UAuthProductInfo Core { get; init; } = default!; } diff --git a/src/CodeBeam.UltimateAuth.Client/Runtime/IUAuthClientBootstrapper.cs b/src/CodeBeam.UltimateAuth.Client/Runtime/IUAuthClientBootstrapper.cs index 222d8eee..448016e8 100644 --- a/src/CodeBeam.UltimateAuth.Client/Runtime/IUAuthClientBootstrapper.cs +++ b/src/CodeBeam.UltimateAuth.Client/Runtime/IUAuthClientBootstrapper.cs @@ -1,9 +1,6 @@ -using System.Threading.Tasks; +namespace CodeBeam.UltimateAuth.Client.Runtime; -namespace CodeBeam.UltimateAuth.Client.Runtime +public interface IUAuthClientBootstrapper { - public interface IUAuthClientBootstrapper - { - Task EnsureStartedAsync(); - } + Task EnsureStartedAsync(); } diff --git a/src/CodeBeam.UltimateAuth.Client/Runtime/UAuthClientBootstrapper.cs b/src/CodeBeam.UltimateAuth.Client/Runtime/UAuthClientBootstrapper.cs index 0acb423e..e6b12b35 100644 --- a/src/CodeBeam.UltimateAuth.Client/Runtime/UAuthClientBootstrapper.cs +++ b/src/CodeBeam.UltimateAuth.Client/Runtime/UAuthClientBootstrapper.cs @@ -3,51 +3,50 @@ using CodeBeam.UltimateAuth.Client.Infrastructure; // DeviceId is automatically created and managed by UAuthClientProvider. This class is for advanced situations. -namespace CodeBeam.UltimateAuth.Client.Runtime +namespace CodeBeam.UltimateAuth.Client.Runtime; + +internal sealed class UAuthClientBootstrapper : IUAuthClientBootstrapper { - internal sealed class UAuthClientBootstrapper : IUAuthClientBootstrapper - { - private readonly SemaphoreSlim _gate = new(1, 1); - private bool _started; + private readonly SemaphoreSlim _gate = new(1, 1); + private bool _started; - private readonly IDeviceIdProvider _deviceIdProvider; - private readonly IBrowserUAuthBridge _browser; - private readonly ISessionCoordinator _coordinator; + private readonly IDeviceIdProvider _deviceIdProvider; + private readonly IBrowserUAuthBridge _browser; + private readonly ISessionCoordinator _coordinator; - public bool IsStarted => _started; + public bool IsStarted => _started; - public UAuthClientBootstrapper( - IDeviceIdProvider deviceIdProvider, - IBrowserUAuthBridge browser, - ISessionCoordinator coordinator) - { - _deviceIdProvider = deviceIdProvider; - _browser = browser; - _coordinator = coordinator; - } + public UAuthClientBootstrapper( + IDeviceIdProvider deviceIdProvider, + IBrowserUAuthBridge browser, + ISessionCoordinator coordinator) + { + _deviceIdProvider = deviceIdProvider; + _browser = browser; + _coordinator = coordinator; + } - public async Task EnsureStartedAsync() + public async Task EnsureStartedAsync() + { + if (_started) + return; + + await _gate.WaitAsync(); + try { if (_started) return; - await _gate.WaitAsync(); - try - { - if (_started) - return; - - var deviceId = await _deviceIdProvider.GetOrCreateAsync(); - await _browser.SetDeviceIdAsync(deviceId.Value); - await _coordinator.StartAsync(); - - _started = true; - } - finally - { - _gate.Release(); - } - } + var deviceId = await _deviceIdProvider.GetOrCreateAsync(); + await _browser.SetDeviceIdAsync(deviceId.Value); + await _coordinator.StartAsync(); + _started = true; + } + finally + { + _gate.Release(); + } } + } diff --git a/src/CodeBeam.UltimateAuth.Client/Services/DefaultAuthorizationClient.cs b/src/CodeBeam.UltimateAuth.Client/Services/DefaultAuthorizationClient.cs index 1985a95f..2e77508d 100644 --- a/src/CodeBeam.UltimateAuth.Client/Services/DefaultAuthorizationClient.cs +++ b/src/CodeBeam.UltimateAuth.Client/Services/DefaultAuthorizationClient.cs @@ -5,61 +5,60 @@ using CodeBeam.UltimateAuth.Core.Domain; using Microsoft.Extensions.Options; -namespace CodeBeam.UltimateAuth.Client.Services +namespace CodeBeam.UltimateAuth.Client.Services; + +internal sealed class DefaultAuthorizationClient : IAuthorizationClient { - internal sealed class DefaultAuthorizationClient : IAuthorizationClient + private readonly IUAuthRequestClient _request; + private readonly UAuthClientOptions _options; + + public DefaultAuthorizationClient(IUAuthRequestClient request, IOptions options) { - private readonly IUAuthRequestClient _request; - private readonly UAuthClientOptions _options; + _request = request; + _options = options.Value; + } - public DefaultAuthorizationClient(IUAuthRequestClient request, IOptions options) - { - _request = request; - _options = options.Value; - } + public async Task> CheckAsync(AuthorizationCheckRequest request) + { + var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, "/authorization/check"); + var raw = await _request.SendJsonAsync(url, request); + return UAuthResultMapper.FromJson(raw); + } - public async Task> CheckAsync(AuthorizationCheckRequest request) - { - var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, "/authorization/check"); - var raw = await _request.SendJsonAsync(url, request); - return UAuthResultMapper.FromJson(raw); - } + public async Task> GetMyRolesAsync() + { + var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, "/authorization/users/me/roles/get"); + var raw = await _request.SendFormForJsonAsync(url); + return UAuthResultMapper.FromJson(raw); + } - public async Task> GetMyRolesAsync() - { - var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, "/authorization/users/me/roles/get"); - var raw = await _request.SendFormForJsonAsync(url); - return UAuthResultMapper.FromJson(raw); - } + public async Task> GetUserRolesAsync(UserKey userKey) + { + var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, $"/admin/authorization/users/{userKey}/roles/get"); + var raw = await _request.SendFormForJsonAsync(url); + return UAuthResultMapper.FromJson(raw); + } - public async Task> GetUserRolesAsync(UserKey userKey) + public async Task AssignRoleAsync(UserKey userKey, string role) + { + var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, $"/admin/authorization/users/{userKey}/roles/post"); + var raw = await _request.SendJsonAsync(url, new AssignRoleRequest { - var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, $"/admin/authorization/users/{userKey}/roles/get"); - var raw = await _request.SendFormForJsonAsync(url); - return UAuthResultMapper.FromJson(raw); - } + Role = role + }); - public async Task AssignRoleAsync(UserKey userKey, string role) - { - var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, $"/admin/authorization/users/{userKey}/roles/post"); - var raw = await _request.SendJsonAsync(url, new AssignRoleRequest - { - Role = role - }); + return UAuthResultMapper.FromStatus(raw); + } - return UAuthResultMapper.FromStatus(raw); - } + public async Task RemoveRoleAsync(UserKey userKey, string role) + { + var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, $"/admin/authorization/users/{userKey}/roles/delete"); - public async Task RemoveRoleAsync(UserKey userKey, string role) + var raw = await _request.SendJsonAsync(url, new AssignRoleRequest { - var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, $"/admin/authorization/users/{userKey}/roles/delete"); - - var raw = await _request.SendJsonAsync(url, new AssignRoleRequest - { - Role = role - }); + Role = role + }); - return UAuthResultMapper.FromStatus(raw); - } + return UAuthResultMapper.FromStatus(raw); } } diff --git a/src/CodeBeam.UltimateAuth.Client/Services/DefaultCredentialClient.cs b/src/CodeBeam.UltimateAuth.Client/Services/DefaultCredentialClient.cs index 7fc79cd1..f88bd15d 100644 --- a/src/CodeBeam.UltimateAuth.Client/Services/DefaultCredentialClient.cs +++ b/src/CodeBeam.UltimateAuth.Client/Services/DefaultCredentialClient.cs @@ -5,99 +5,98 @@ using CodeBeam.UltimateAuth.Credentials.Contracts; using Microsoft.Extensions.Options; -namespace CodeBeam.UltimateAuth.Client.Services +namespace CodeBeam.UltimateAuth.Client.Services; + +internal sealed class DefaultCredentialClient : ICredentialClient { - internal sealed class DefaultCredentialClient : ICredentialClient + private readonly IUAuthRequestClient _request; + private readonly UAuthClientOptions _options; + + public DefaultCredentialClient(IUAuthRequestClient request, IOptions options) + { + _request = request; + _options = options.Value; + } + + private string Url(string path) => UAuthUrlBuilder.Combine(_options.Endpoints.Authority, path); + + public async Task> GetMyAsync() + { + var raw = await _request.SendFormForJsonAsync(Url("/credentials/get")); + return UAuthResultMapper.FromJson(raw); + } + + public async Task> AddMyAsync(AddCredentialRequest request) + { + var raw = await _request.SendJsonAsync(Url("/credentials/add"), request); + return UAuthResultMapper.FromJson(raw); + } + + public async Task> ChangeMyAsync(CredentialType type, ChangeCredentialRequest request) + { + var raw = await _request.SendJsonAsync(Url($"/credentials/{type}/change"), request); + return UAuthResultMapper.FromJson(raw); + } + + public async Task RevokeMyAsync(CredentialType type, RevokeCredentialRequest request) { - private readonly IUAuthRequestClient _request; - private readonly UAuthClientOptions _options; - - public DefaultCredentialClient(IUAuthRequestClient request, IOptions options) - { - _request = request; - _options = options.Value; - } - - private string Url(string path) => UAuthUrlBuilder.Combine(_options.Endpoints.Authority, path); - - public async Task> GetMyAsync() - { - var raw = await _request.SendFormForJsonAsync(Url("/credentials/get")); - return UAuthResultMapper.FromJson(raw); - } - - public async Task> AddMyAsync(AddCredentialRequest request) - { - var raw = await _request.SendJsonAsync(Url("/credentials/add"), request); - return UAuthResultMapper.FromJson(raw); - } - - public async Task> ChangeMyAsync(CredentialType type, ChangeCredentialRequest request) - { - var raw = await _request.SendJsonAsync(Url($"/credentials/{type}/change"), request); - return UAuthResultMapper.FromJson(raw); - } - - public async Task RevokeMyAsync(CredentialType type, RevokeCredentialRequest request) - { - var raw = await _request.SendJsonAsync(Url($"/credentials/{type}/revoke"), request); - return UAuthResultMapper.FromStatus(raw); - } - - public async Task BeginResetMyAsync(CredentialType type, BeginCredentialResetRequest request) - { - var raw = await _request.SendJsonAsync(Url($"/credentials/{type}/reset/begin"), request); - return UAuthResultMapper.FromStatus(raw); - } - - public async Task CompleteResetMyAsync(CredentialType type, CompleteCredentialResetRequest request) - { - var raw = await _request.SendJsonAsync(Url($"/credentials/{type}/reset/complete"), request); - return UAuthResultMapper.FromStatus(raw); - } - - - public async Task> GetUserAsync(UserKey userKey) - { - var raw = await _request.SendFormForJsonAsync(Url($"/admin/users/{userKey}/credentials/get")); - return UAuthResultMapper.FromJson(raw); - } - - public async Task> AddUserAsync(UserKey userKey, AddCredentialRequest request) - { - var raw = await _request.SendJsonAsync(Url($"/admin/users/{userKey}/credentials/add"), request); - return UAuthResultMapper.FromJson(raw); - } - - public async Task RevokeUserAsync(UserKey userKey, CredentialType type, RevokeCredentialRequest request) - { - var raw = await _request.SendJsonAsync(Url($"/admin/users/{userKey}/credentials/{type}/revoke"), request); - return UAuthResultMapper.FromStatus(raw); - } - - public async Task ActivateUserAsync(UserKey userKey, CredentialType type) - { - var raw = await _request.SendFormAsync(Url($"/admin/users/{userKey}/credentials/{type}/activate")); - return UAuthResultMapper.FromStatus(raw); - } - - public async Task BeginResetUserAsync(UserKey userKey, CredentialType type, BeginCredentialResetRequest request) - { - var raw = await _request.SendJsonAsync(Url($"/admin/users/{userKey}/credentials/{type}/reset/begin"), request); - return UAuthResultMapper.FromStatus(raw); - } - - public async Task CompleteResetUserAsync(UserKey userKey, CredentialType type, CompleteCredentialResetRequest request) - { - var raw = await _request.SendJsonAsync(Url($"/admin/users/{userKey}/credentials/{type}/reset/complete"), request); - return UAuthResultMapper.FromStatus(raw); - } - - public async Task DeleteUserAsync(UserKey userKey, CredentialType type) - { - var raw = await _request.SendFormAsync(Url($"/admin/users/{userKey}/credentials/{type}/delete")); - return UAuthResultMapper.FromStatus(raw); - } + var raw = await _request.SendJsonAsync(Url($"/credentials/{type}/revoke"), request); + return UAuthResultMapper.FromStatus(raw); + } + public async Task BeginResetMyAsync(CredentialType type, BeginCredentialResetRequest request) + { + var raw = await _request.SendJsonAsync(Url($"/credentials/{type}/reset/begin"), request); + return UAuthResultMapper.FromStatus(raw); } + + public async Task CompleteResetMyAsync(CredentialType type, CompleteCredentialResetRequest request) + { + var raw = await _request.SendJsonAsync(Url($"/credentials/{type}/reset/complete"), request); + return UAuthResultMapper.FromStatus(raw); + } + + + public async Task> GetUserAsync(UserKey userKey) + { + var raw = await _request.SendFormForJsonAsync(Url($"/admin/users/{userKey}/credentials/get")); + return UAuthResultMapper.FromJson(raw); + } + + public async Task> AddUserAsync(UserKey userKey, AddCredentialRequest request) + { + var raw = await _request.SendJsonAsync(Url($"/admin/users/{userKey}/credentials/add"), request); + return UAuthResultMapper.FromJson(raw); + } + + public async Task RevokeUserAsync(UserKey userKey, CredentialType type, RevokeCredentialRequest request) + { + var raw = await _request.SendJsonAsync(Url($"/admin/users/{userKey}/credentials/{type}/revoke"), request); + return UAuthResultMapper.FromStatus(raw); + } + + public async Task ActivateUserAsync(UserKey userKey, CredentialType type) + { + var raw = await _request.SendFormAsync(Url($"/admin/users/{userKey}/credentials/{type}/activate")); + return UAuthResultMapper.FromStatus(raw); + } + + public async Task BeginResetUserAsync(UserKey userKey, CredentialType type, BeginCredentialResetRequest request) + { + var raw = await _request.SendJsonAsync(Url($"/admin/users/{userKey}/credentials/{type}/reset/begin"), request); + return UAuthResultMapper.FromStatus(raw); + } + + public async Task CompleteResetUserAsync(UserKey userKey, CredentialType type, CompleteCredentialResetRequest request) + { + var raw = await _request.SendJsonAsync(Url($"/admin/users/{userKey}/credentials/{type}/reset/complete"), request); + return UAuthResultMapper.FromStatus(raw); + } + + public async Task DeleteUserAsync(UserKey userKey, CredentialType type) + { + var raw = await _request.SendFormAsync(Url($"/admin/users/{userKey}/credentials/{type}/delete")); + return UAuthResultMapper.FromStatus(raw); + } + } diff --git a/src/CodeBeam.UltimateAuth.Client/Services/DefaultFlowClient.cs b/src/CodeBeam.UltimateAuth.Client/Services/DefaultFlowClient.cs index 9466d623..b07b0d16 100644 --- a/src/CodeBeam.UltimateAuth.Client/Services/DefaultFlowClient.cs +++ b/src/CodeBeam.UltimateAuth.Client/Services/DefaultFlowClient.cs @@ -14,201 +14,200 @@ using System.Text; using System.Text.Json; -namespace CodeBeam.UltimateAuth.Client.Services +namespace CodeBeam.UltimateAuth.Client.Services; + +internal class DefaultFlowClient : IFlowClient { - internal class DefaultFlowClient : IFlowClient + private readonly IUAuthRequestClient _post; + private readonly UAuthClientOptions _options; + private readonly UAuthOptions _coreOptions; + private readonly UAuthClientDiagnostics _diagnostics; + private readonly NavigationManager _nav; + + public DefaultFlowClient( + IUAuthRequestClient post, + IOptions options, + IOptions coreOptions, + UAuthClientDiagnostics diagnostics, + NavigationManager nav) { - private readonly IUAuthRequestClient _post; - private readonly UAuthClientOptions _options; - private readonly UAuthOptions _coreOptions; - private readonly UAuthClientDiagnostics _diagnostics; - private readonly NavigationManager _nav; - - public DefaultFlowClient( - IUAuthRequestClient post, - IOptions options, - IOptions coreOptions, - UAuthClientDiagnostics diagnostics, - NavigationManager nav) - { - _post = post; - _options = options.Value; - _coreOptions = coreOptions.Value; - _diagnostics = diagnostics; - _nav = nav; - } + _post = post; + _options = options.Value; + _coreOptions = coreOptions.Value; + _diagnostics = diagnostics; + _nav = nav; + } + + public async Task LoginAsync(LoginRequest request) + { + var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, _options.Endpoints.Login); + await _post.NavigateAsync(url, request.ToDictionary()); + } - public async Task LoginAsync(LoginRequest request) + public async Task LogoutAsync() + { + var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, _options.Endpoints.Logout); + await _post.NavigateAsync(url); + } + + public async Task RefreshAsync(bool isAuto = false) + { + if (isAuto == false) { - var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, _options.Endpoints.Login); - await _post.NavigateAsync(url, request.ToDictionary()); + _diagnostics.MarkManualRefresh(); } - public async Task LogoutAsync() + var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, _options.Endpoints.Refresh); + var result = await _post.SendFormAsync(url); + var refreshOutcome = RefreshOutcomeParser.Parse(result.RefreshOutcome); + switch (refreshOutcome) { - var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, _options.Endpoints.Logout); - await _post.NavigateAsync(url); + case RefreshOutcome.NoOp: + _diagnostics.MarkRefreshNoOp(); + break; + case RefreshOutcome.Touched: + _diagnostics.MarkRefreshTouched(); + break; + case RefreshOutcome.ReauthRequired: + _diagnostics.MarkRefreshReauthRequired(); + break; + case RefreshOutcome.None: + _diagnostics.MarkRefreshUnknown(); + break; } - public async Task RefreshAsync(bool isAuto = false) + return new RefreshResult { - if (isAuto == false) - { - _diagnostics.MarkManualRefresh(); - } + Ok = result.Ok, + Status = result.Status, + Outcome = refreshOutcome + }; + } - var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, _options.Endpoints.Refresh); - var result = await _post.SendFormAsync(url); - var refreshOutcome = RefreshOutcomeParser.Parse(result.RefreshOutcome); - switch (refreshOutcome) - { - case RefreshOutcome.NoOp: - _diagnostics.MarkRefreshNoOp(); - break; - case RefreshOutcome.Touched: - _diagnostics.MarkRefreshTouched(); - break; - case RefreshOutcome.ReauthRequired: - _diagnostics.MarkRefreshReauthRequired(); - break; - case RefreshOutcome.None: - _diagnostics.MarkRefreshUnknown(); - break; - } - - return new RefreshResult - { - Ok = result.Ok, - Status = result.Status, - Outcome = refreshOutcome - }; - } + public async Task ReauthAsync() + { + var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, _options.Endpoints.Reauth); + await _post.NavigateAsync(_options.Endpoints.Reauth); + } - public async Task ReauthAsync() - { - var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, _options.Endpoints.Reauth); - await _post.NavigateAsync(_options.Endpoints.Reauth); - } + public async Task ValidateAsync() + { + var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, _options.Endpoints.Validate); + var raw = await _post.SendFormForJsonAsync(url); - public async Task ValidateAsync() + if (!raw.Ok || raw.Body is null) { - var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, _options.Endpoints.Validate); - var raw = await _post.SendFormForJsonAsync(url); - - if (!raw.Ok || raw.Body is null) - { - return new AuthValidationResult - { - IsValid = false, - State = "transport" - }; - } - - var body = raw.Body.Value.Deserialize( - new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true - }); - - return body ?? new AuthValidationResult + return new AuthValidationResult { IsValid = false, - State = "deserialize" + State = "transport" }; } - public async Task BeginPkceAsync(string? returnUrl = null) + var body = raw.Body.Value.Deserialize( + new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }); + + return body ?? new AuthValidationResult { - var pkce = _options.Login.Pkce; + IsValid = false, + State = "deserialize" + }; + } - if (!pkce.Enabled) - throw new InvalidOperationException("PKCE login is disabled by configuration."); + public async Task BeginPkceAsync(string? returnUrl = null) + { + var pkce = _options.Login.Pkce; - var verifier = CreateVerifier(); - var challenge = CreateChallenge(verifier); + if (!pkce.Enabled) + throw new InvalidOperationException("PKCE login is disabled by configuration."); - var authorizeUrl = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, _options.Endpoints.PkceAuthorize); + var verifier = CreateVerifier(); + var challenge = CreateChallenge(verifier); - var raw = await _post.SendFormForJsonAsync( - authorizeUrl, - new Dictionary - { - ["code_challenge"] = challenge, - ["challenge_method"] = "S256" - }); + var authorizeUrl = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, _options.Endpoints.PkceAuthorize); - if (!raw.Ok || raw.Body is null) - throw new InvalidOperationException("PKCE authorize failed."); + var raw = await _post.SendFormForJsonAsync( + authorizeUrl, + new Dictionary + { + ["code_challenge"] = challenge, + ["challenge_method"] = "S256" + }); - var response = raw.Body.Value.Deserialize( - new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + if (!raw.Ok || raw.Body is null) + throw new InvalidOperationException("PKCE authorize failed."); - if (response is null || string.IsNullOrWhiteSpace(response.AuthorizationCode)) - throw new InvalidOperationException("Invalid PKCE authorize response."); + var response = raw.Body.Value.Deserialize( + new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); - if (pkce.OnAuthorized is not null) - await pkce.OnAuthorized(response); + if (response is null || string.IsNullOrWhiteSpace(response.AuthorizationCode)) + throw new InvalidOperationException("Invalid PKCE authorize response."); - var resolvedReturnUrl = returnUrl - ?? pkce.ReturnUrl - ?? _options.Login.DefaultReturnUrl - ?? _nav.Uri; + if (pkce.OnAuthorized is not null) + await pkce.OnAuthorized(response); - if (pkce.AutoRedirect) - { - await NavigateToHubLoginAsync(response.AuthorizationCode, verifier, resolvedReturnUrl); - } - } + var resolvedReturnUrl = returnUrl + ?? pkce.ReturnUrl + ?? _options.Login.DefaultReturnUrl + ?? _nav.Uri; - public async Task CompletePkceLoginAsync(PkceLoginRequest request) + if (pkce.AutoRedirect) { - if (request is null) - throw new ArgumentNullException(nameof(request)); + await NavigateToHubLoginAsync(response.AuthorizationCode, verifier, resolvedReturnUrl); + } + } - var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, _options.Endpoints.PkceComplete); + public async Task CompletePkceLoginAsync(PkceLoginRequest request) + { + if (request is null) + throw new ArgumentNullException(nameof(request)); - var payload = new Dictionary - { - ["authorization_code"] = request.AuthorizationCode, - ["code_verifier"] = request.CodeVerifier, - ["return_url"] = request.ReturnUrl, + var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, _options.Endpoints.PkceComplete); - ["Identifier"] = request.Identifier ?? string.Empty, - ["Secret"] = request.Secret ?? string.Empty - }; + var payload = new Dictionary + { + ["authorization_code"] = request.AuthorizationCode, + ["code_verifier"] = request.CodeVerifier, + ["return_url"] = request.ReturnUrl, - await _post.NavigateAsync(url, payload); - } + ["Identifier"] = request.Identifier ?? string.Empty, + ["Secret"] = request.Secret ?? string.Empty + }; - private Task NavigateToHubLoginAsync(string authorizationCode, string codeVerifier, string returnUrl) - { - var hubLoginUrl = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, _options.Endpoints.HubLoginPath); + await _post.NavigateAsync(url, payload); + } - var data = new Dictionary - { - ["authorization_code"] = authorizationCode, - ["code_verifier"] = codeVerifier, - ["return_url"] = returnUrl, - ["client_profile"] = _coreOptions.ClientProfile.ToString() - }; + private Task NavigateToHubLoginAsync(string authorizationCode, string codeVerifier, string returnUrl) + { + var hubLoginUrl = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, _options.Endpoints.HubLoginPath); - return _post.NavigateAsync(hubLoginUrl, data); - } + var data = new Dictionary + { + ["authorization_code"] = authorizationCode, + ["code_verifier"] = codeVerifier, + ["return_url"] = returnUrl, + ["client_profile"] = _coreOptions.ClientProfile.ToString() + }; + return _post.NavigateAsync(hubLoginUrl, data); + } - // ---------------- PKCE CRYPTO ---------------- - private static string CreateVerifier() - { - var bytes = RandomNumberGenerator.GetBytes(32); - return Base64Url.Encode(bytes); - } + // ---------------- PKCE CRYPTO ---------------- - private static string CreateChallenge(string verifier) - { - using var sha256 = SHA256.Create(); - var hash = sha256.ComputeHash(Encoding.ASCII.GetBytes(verifier)); - return Base64Url.Encode(hash); - } + private static string CreateVerifier() + { + var bytes = RandomNumberGenerator.GetBytes(32); + return Base64Url.Encode(bytes); + } + + private static string CreateChallenge(string verifier) + { + using var sha256 = SHA256.Create(); + var hash = sha256.ComputeHash(Encoding.ASCII.GetBytes(verifier)); + return Base64Url.Encode(hash); } } diff --git a/src/CodeBeam.UltimateAuth.Client/Services/DefaultUserClient.cs b/src/CodeBeam.UltimateAuth.Client/Services/DefaultUserClient.cs index 9616a1b7..b1218a94 100644 --- a/src/CodeBeam.UltimateAuth.Client/Services/DefaultUserClient.cs +++ b/src/CodeBeam.UltimateAuth.Client/Services/DefaultUserClient.cs @@ -5,73 +5,72 @@ using CodeBeam.UltimateAuth.Users.Contracts; using Microsoft.Extensions.Options; -namespace CodeBeam.UltimateAuth.Client.Services +namespace CodeBeam.UltimateAuth.Client.Services; + +internal sealed class DefaultUserClient : IUserClient { - internal sealed class DefaultUserClient : IUserClient - { - private readonly IUAuthRequestClient _request; - private readonly UAuthClientOptions _options; + private readonly IUAuthRequestClient _request; + private readonly UAuthClientOptions _options; - public DefaultUserClient(IUAuthRequestClient request, IOptions options) - { - _request = request; - _options = options.Value; - } + public DefaultUserClient(IUAuthRequestClient request, IOptions options) + { + _request = request; + _options = options.Value; + } - public async Task> GetMeAsync() - { - var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, "/users/me/get"); - var raw = await _request.SendFormForJsonAsync(url); - return UAuthResultMapper.FromJson(raw); - } + public async Task> GetMeAsync() + { + var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, "/users/me/get"); + var raw = await _request.SendFormForJsonAsync(url); + return UAuthResultMapper.FromJson(raw); + } - public async Task UpdateMeAsync(UpdateProfileRequest request) - { - var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, "/users/me/update"); - var raw = await _request.SendJsonAsync(url, request); - return UAuthResultMapper.FromStatus(raw); - } + public async Task UpdateMeAsync(UpdateProfileRequest request) + { + var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, "/users/me/update"); + var raw = await _request.SendJsonAsync(url, request); + return UAuthResultMapper.FromStatus(raw); + } - public async Task> CreateAsync(CreateUserRequest request) - { - var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, "/users/create"); - var raw = await _request.SendJsonAsync(url, request); - return UAuthResultMapper.FromJson(raw); - } + public async Task> CreateAsync(CreateUserRequest request) + { + var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, "/users/create"); + var raw = await _request.SendJsonAsync(url, request); + return UAuthResultMapper.FromJson(raw); + } - public async Task> ChangeStatusSelfAsync(ChangeUserStatusSelfRequest request) - { - var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, $"/users/me/status"); - var raw = await _request.SendJsonAsync(url, request); - return UAuthResultMapper.FromJson(raw); - } + public async Task> ChangeStatusSelfAsync(ChangeUserStatusSelfRequest request) + { + var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, $"/users/me/status"); + var raw = await _request.SendJsonAsync(url, request); + return UAuthResultMapper.FromJson(raw); + } - public async Task> ChangeStatusAdminAsync(ChangeUserStatusAdminRequest request) - { - var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, $"/admin/users/{request.UserKey.Value}/status"); - var raw = await _request.SendJsonAsync(url, request); - return UAuthResultMapper.FromJson(raw); - } + public async Task> ChangeStatusAdminAsync(ChangeUserStatusAdminRequest request) + { + var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, $"/admin/users/{request.UserKey.Value}/status"); + var raw = await _request.SendJsonAsync(url, request); + return UAuthResultMapper.FromJson(raw); + } - public async Task> DeleteAsync(DeleteUserRequest request) - { - var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, "/users/delete"); - var raw = await _request.SendJsonAsync(url, request); - return UAuthResultMapper.FromJson(raw); - } + public async Task> DeleteAsync(DeleteUserRequest request) + { + var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, "/users/delete"); + var raw = await _request.SendJsonAsync(url, request); + return UAuthResultMapper.FromJson(raw); + } - public async Task> GetProfileAsync(UserKey userKey) - { - var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, $"/admin/users/{userKey}/profile/get"); - var raw = await _request.SendFormForJsonAsync(url); - return UAuthResultMapper.FromJson(raw); - } + public async Task> GetProfileAsync(UserKey userKey) + { + var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, $"/admin/users/{userKey}/profile/get"); + var raw = await _request.SendFormForJsonAsync(url); + return UAuthResultMapper.FromJson(raw); + } - public async Task UpdateProfileAsync(UserKey userKey, UpdateProfileRequest request) - { - var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, $"/admin/users/{userKey}/profile/update"); - var raw = await _request.SendJsonAsync(url, request); - return UAuthResultMapper.FromStatus(raw); - } + public async Task UpdateProfileAsync(UserKey userKey, UpdateProfileRequest request) + { + var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, $"/admin/users/{userKey}/profile/update"); + var raw = await _request.SendJsonAsync(url, request); + return UAuthResultMapper.FromStatus(raw); } } diff --git a/src/CodeBeam.UltimateAuth.Client/Services/DefaultUserIdentifierClient.cs b/src/CodeBeam.UltimateAuth.Client/Services/DefaultUserIdentifierClient.cs index 50a0a6cc..8fb2c12a 100644 --- a/src/CodeBeam.UltimateAuth.Client/Services/DefaultUserIdentifierClient.cs +++ b/src/CodeBeam.UltimateAuth.Client/Services/DefaultUserIdentifierClient.cs @@ -5,116 +5,115 @@ using CodeBeam.UltimateAuth.Users.Contracts; using Microsoft.Extensions.Options; -namespace CodeBeam.UltimateAuth.Client.Services +namespace CodeBeam.UltimateAuth.Client.Services; + +public class DefaultUserIdentifierClient : IUserIdentifierClient { - public class DefaultUserIdentifierClient : IUserIdentifierClient + private readonly IUAuthRequestClient _request; + private readonly UAuthClientOptions _options; + + public DefaultUserIdentifierClient(IUAuthRequestClient request, IOptions options) + { + _request = request; + _options = options.Value; + } + + public async Task>> GetMyIdentifiersAsync() + { + var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, "/users/me/identifiers/get"); + var raw = await _request.SendFormForJsonAsync(url); + return UAuthResultMapper.FromJson>(raw); + } + + public async Task AddSelfAsync(AddUserIdentifierRequest request) + { + var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, $"/users/me/identifiers/add"); + var raw = await _request.SendJsonAsync(url, request); + return UAuthResultMapper.FromStatus(raw); + } + + public async Task UpdateSelfAsync(UpdateUserIdentifierRequest request) + { + var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, $"/users/me/identifiers/update"); + var raw = await _request.SendJsonAsync(url, request); + return UAuthResultMapper.FromStatus(raw); + } + + public async Task SetPrimarySelfAsync(SetPrimaryUserIdentifierRequest request) + { + var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, $"/users/me/identifiers/set-primary"); + var raw = await _request.SendJsonAsync(url, request); + return UAuthResultMapper.FromStatus(raw); + } + + public async Task UnsetPrimarySelfAsync(UnsetPrimaryUserIdentifierRequest request) + { + var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, $"/users/me/identifiers/unset-primary"); + var raw = await _request.SendJsonAsync(url, request); + return UAuthResultMapper.FromStatus(raw); + } + + public async Task VerifySelfAsync(VerifyUserIdentifierRequest request) + { + var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, $"/users/me/identifiers/verify"); + var raw = await _request.SendJsonAsync(url, request); + return UAuthResultMapper.FromStatus(raw); + } + + public async Task DeleteSelfAsync(DeleteUserIdentifierRequest request) + { + var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, $"/users/me/identifiers/delete"); + var raw = await _request.SendJsonAsync(url, request); + return UAuthResultMapper.FromStatus(raw); + } + + public async Task>> GetUserIdentifiersAsync(UserKey userKey) + { + var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, $"/admin/users/{userKey.Value}/identifiers/get"); + var raw = await _request.SendFormForJsonAsync(url); + return UAuthResultMapper.FromJson>(raw); + } + + public async Task AddAdminAsync(UserKey userKey, AddUserIdentifierRequest request) + { + var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, $"/admin/users/{userKey}/identifiers/add"); + var raw = await _request.SendJsonAsync(url, request); + return UAuthResultMapper.FromStatus(raw); + } + + public async Task UpdateAdminAsync(UserKey userKey, UpdateUserIdentifierRequest request) + { + var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, $"/admin/users/{userKey}/identifiers/update"); + var raw = await _request.SendJsonAsync(url, request); + return UAuthResultMapper.FromStatus(raw); + } + + public async Task SetPrimaryAdminAsync(UserKey userKey, SetPrimaryUserIdentifierRequest request) { - private readonly IUAuthRequestClient _request; - private readonly UAuthClientOptions _options; - - public DefaultUserIdentifierClient(IUAuthRequestClient request, IOptions options) - { - _request = request; - _options = options.Value; - } - - public async Task>> GetMyIdentifiersAsync() - { - var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, "/users/me/identifiers/get"); - var raw = await _request.SendFormForJsonAsync(url); - return UAuthResultMapper.FromJson>(raw); - } - - public async Task AddSelfAsync(AddUserIdentifierRequest request) - { - var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, $"/users/me/identifiers/add"); - var raw = await _request.SendJsonAsync(url, request); - return UAuthResultMapper.FromStatus(raw); - } - - public async Task UpdateSelfAsync(UpdateUserIdentifierRequest request) - { - var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, $"/users/me/identifiers/update"); - var raw = await _request.SendJsonAsync(url, request); - return UAuthResultMapper.FromStatus(raw); - } - - public async Task SetPrimarySelfAsync(SetPrimaryUserIdentifierRequest request) - { - var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, $"/users/me/identifiers/set-primary"); - var raw = await _request.SendJsonAsync(url, request); - return UAuthResultMapper.FromStatus(raw); - } - - public async Task UnsetPrimarySelfAsync(UnsetPrimaryUserIdentifierRequest request) - { - var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, $"/users/me/identifiers/unset-primary"); - var raw = await _request.SendJsonAsync(url, request); - return UAuthResultMapper.FromStatus(raw); - } - - public async Task VerifySelfAsync(VerifyUserIdentifierRequest request) - { - var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, $"/users/me/identifiers/verify"); - var raw = await _request.SendJsonAsync(url, request); - return UAuthResultMapper.FromStatus(raw); - } - - public async Task DeleteSelfAsync(DeleteUserIdentifierRequest request) - { - var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, $"/users/me/identifiers/delete"); - var raw = await _request.SendJsonAsync(url, request); - return UAuthResultMapper.FromStatus(raw); - } - - public async Task>> GetUserIdentifiersAsync(UserKey userKey) - { - var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, $"/admin/users/{userKey.Value}/identifiers/get"); - var raw = await _request.SendFormForJsonAsync(url); - return UAuthResultMapper.FromJson>(raw); - } - - public async Task AddAdminAsync(UserKey userKey, AddUserIdentifierRequest request) - { - var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, $"/admin/users/{userKey}/identifiers/add"); - var raw = await _request.SendJsonAsync(url, request); - return UAuthResultMapper.FromStatus(raw); - } - - public async Task UpdateAdminAsync(UserKey userKey, UpdateUserIdentifierRequest request) - { - var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, $"/admin/users/{userKey}/identifiers/update"); - var raw = await _request.SendJsonAsync(url, request); - return UAuthResultMapper.FromStatus(raw); - } - - public async Task SetPrimaryAdminAsync(UserKey userKey, SetPrimaryUserIdentifierRequest request) - { - var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, $"/admin/users/{userKey}/identifiers/set-primary"); - var raw = await _request.SendJsonAsync(url, request); - return UAuthResultMapper.FromStatus(raw); - } - - public async Task UnsetPrimaryAdminAsync(UserKey userKey, UnsetPrimaryUserIdentifierRequest request) - { - var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, $"/admin/users/{userKey}/identifiers/unset-primary"); - var raw = await _request.SendJsonAsync(url, request); - return UAuthResultMapper.FromStatus(raw); - } - - public async Task VerifyAdminAsync(UserKey userKey, VerifyUserIdentifierRequest request) - { - var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, $"/admin/users/{userKey}/identifiers/verify"); - var raw = await _request.SendJsonAsync(url, request); - return UAuthResultMapper.FromStatus(raw); - } - - public async Task DeleteAdminAsync(UserKey userKey, DeleteUserIdentifierRequest request) - { - var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, $"/admin/users/{userKey}/identifiers/delete"); - var raw = await _request.SendJsonAsync(url, request); - return UAuthResultMapper.FromStatus(raw); - } + var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, $"/admin/users/{userKey}/identifiers/set-primary"); + var raw = await _request.SendJsonAsync(url, request); + return UAuthResultMapper.FromStatus(raw); + } + public async Task UnsetPrimaryAdminAsync(UserKey userKey, UnsetPrimaryUserIdentifierRequest request) + { + var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, $"/admin/users/{userKey}/identifiers/unset-primary"); + var raw = await _request.SendJsonAsync(url, request); + return UAuthResultMapper.FromStatus(raw); } + + public async Task VerifyAdminAsync(UserKey userKey, VerifyUserIdentifierRequest request) + { + var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, $"/admin/users/{userKey}/identifiers/verify"); + var raw = await _request.SendJsonAsync(url, request); + return UAuthResultMapper.FromStatus(raw); + } + + public async Task DeleteAdminAsync(UserKey userKey, DeleteUserIdentifierRequest request) + { + var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, $"/admin/users/{userKey}/identifiers/delete"); + var raw = await _request.SendJsonAsync(url, request); + return UAuthResultMapper.FromStatus(raw); + } + } diff --git a/src/CodeBeam.UltimateAuth.Client/Services/IAuthorizationClient.cs b/src/CodeBeam.UltimateAuth.Client/Services/IAuthorizationClient.cs index 75400057..192ef5c0 100644 --- a/src/CodeBeam.UltimateAuth.Client/Services/IAuthorizationClient.cs +++ b/src/CodeBeam.UltimateAuth.Client/Services/IAuthorizationClient.cs @@ -2,18 +2,17 @@ using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Client.Services +namespace CodeBeam.UltimateAuth.Client.Services; + +public interface IAuthorizationClient { - public interface IAuthorizationClient - { - Task> CheckAsync(AuthorizationCheckRequest request); + Task> CheckAsync(AuthorizationCheckRequest request); - Task> GetMyRolesAsync(); + Task> GetMyRolesAsync(); - Task> GetUserRolesAsync(UserKey userKey); + Task> GetUserRolesAsync(UserKey userKey); - Task AssignRoleAsync(UserKey userKey, string role); + Task AssignRoleAsync(UserKey userKey, string role); - Task RemoveRoleAsync(UserKey userKey, string role); - } + Task RemoveRoleAsync(UserKey userKey, string role); } diff --git a/src/CodeBeam.UltimateAuth.Client/Services/ICredentialClient.cs b/src/CodeBeam.UltimateAuth.Client/Services/ICredentialClient.cs index 468dbcce..eb92db92 100644 --- a/src/CodeBeam.UltimateAuth.Client/Services/ICredentialClient.cs +++ b/src/CodeBeam.UltimateAuth.Client/Services/ICredentialClient.cs @@ -2,23 +2,22 @@ using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Credentials.Contracts; -namespace CodeBeam.UltimateAuth.Client.Services +namespace CodeBeam.UltimateAuth.Client.Services; + +public interface ICredentialClient { - public interface ICredentialClient - { - Task> GetMyAsync(); - Task> AddMyAsync(AddCredentialRequest request); - Task> ChangeMyAsync(CredentialType type, ChangeCredentialRequest request); - Task RevokeMyAsync(CredentialType type, RevokeCredentialRequest request); - Task BeginResetMyAsync(CredentialType type, BeginCredentialResetRequest request); - Task CompleteResetMyAsync(CredentialType type, CompleteCredentialResetRequest request); + Task> GetMyAsync(); + Task> AddMyAsync(AddCredentialRequest request); + Task> ChangeMyAsync(CredentialType type, ChangeCredentialRequest request); + Task RevokeMyAsync(CredentialType type, RevokeCredentialRequest request); + Task BeginResetMyAsync(CredentialType type, BeginCredentialResetRequest request); + Task CompleteResetMyAsync(CredentialType type, CompleteCredentialResetRequest request); - Task> GetUserAsync(UserKey userKey); - Task> AddUserAsync(UserKey userKey, AddCredentialRequest request); - Task RevokeUserAsync(UserKey userKey, CredentialType type, RevokeCredentialRequest request); - Task ActivateUserAsync(UserKey userKey, CredentialType type); - Task BeginResetUserAsync(UserKey userKey, CredentialType type, BeginCredentialResetRequest request); - Task CompleteResetUserAsync(UserKey userKey, CredentialType type, CompleteCredentialResetRequest request); - Task DeleteUserAsync(UserKey userKey, CredentialType type); - } + Task> GetUserAsync(UserKey userKey); + Task> AddUserAsync(UserKey userKey, AddCredentialRequest request); + Task RevokeUserAsync(UserKey userKey, CredentialType type, RevokeCredentialRequest request); + Task ActivateUserAsync(UserKey userKey, CredentialType type); + Task BeginResetUserAsync(UserKey userKey, CredentialType type, BeginCredentialResetRequest request); + Task CompleteResetUserAsync(UserKey userKey, CredentialType type, CompleteCredentialResetRequest request); + Task DeleteUserAsync(UserKey userKey, CredentialType type); } diff --git a/src/CodeBeam.UltimateAuth.Client/Services/IUAuthClient.cs b/src/CodeBeam.UltimateAuth.Client/Services/IUAuthClient.cs index 0a97c461..272959ef 100644 --- a/src/CodeBeam.UltimateAuth.Client/Services/IUAuthClient.cs +++ b/src/CodeBeam.UltimateAuth.Client/Services/IUAuthClient.cs @@ -1,13 +1,12 @@ using CodeBeam.UltimateAuth.Client.Services; -namespace CodeBeam.UltimateAuth.Client +namespace CodeBeam.UltimateAuth.Client; + +public interface IUAuthClient { - public interface IUAuthClient - { - IFlowClient Flows { get; } - IUserClient Users { get; } - IUserIdentifierClient Identifiers { get; } - ICredentialClient Credentials { get; } - IAuthorizationClient Authorization { get; } - } + IFlowClient Flows { get; } + IUserClient Users { get; } + IUserIdentifierClient Identifiers { get; } + ICredentialClient Credentials { get; } + IAuthorizationClient Authorization { get; } } diff --git a/src/CodeBeam.UltimateAuth.Client/Services/IUserClient.cs b/src/CodeBeam.UltimateAuth.Client/Services/IUserClient.cs index a59a4cdb..88914fff 100644 --- a/src/CodeBeam.UltimateAuth.Client/Services/IUserClient.cs +++ b/src/CodeBeam.UltimateAuth.Client/Services/IUserClient.cs @@ -2,19 +2,18 @@ using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Users.Contracts; -namespace CodeBeam.UltimateAuth.Client.Services +namespace CodeBeam.UltimateAuth.Client.Services; + +public interface IUserClient { - public interface IUserClient - { - Task> CreateAsync(CreateUserRequest request); - Task> ChangeStatusSelfAsync(ChangeUserStatusSelfRequest request); - Task> ChangeStatusAdminAsync(ChangeUserStatusAdminRequest request); - Task> DeleteAsync(DeleteUserRequest request); + Task> CreateAsync(CreateUserRequest request); + Task> ChangeStatusSelfAsync(ChangeUserStatusSelfRequest request); + Task> ChangeStatusAdminAsync(ChangeUserStatusAdminRequest request); + Task> DeleteAsync(DeleteUserRequest request); - Task> GetMeAsync(); - Task UpdateMeAsync(UpdateProfileRequest request); + Task> GetMeAsync(); + Task UpdateMeAsync(UpdateProfileRequest request); - Task> GetProfileAsync(UserKey userKey); - Task UpdateProfileAsync(UserKey userKey, UpdateProfileRequest request); - } + Task> GetProfileAsync(UserKey userKey); + Task UpdateProfileAsync(UserKey userKey, UpdateProfileRequest request); } diff --git a/src/CodeBeam.UltimateAuth.Client/Services/IUserIdentifierClient.cs b/src/CodeBeam.UltimateAuth.Client/Services/IUserIdentifierClient.cs index 5d408165..7f019423 100644 --- a/src/CodeBeam.UltimateAuth.Client/Services/IUserIdentifierClient.cs +++ b/src/CodeBeam.UltimateAuth.Client/Services/IUserIdentifierClient.cs @@ -2,24 +2,23 @@ using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Users.Contracts; -namespace CodeBeam.UltimateAuth.Client.Services +namespace CodeBeam.UltimateAuth.Client.Services; + +public interface IUserIdentifierClient { - public interface IUserIdentifierClient - { - Task>> GetMyIdentifiersAsync(); - Task AddSelfAsync(AddUserIdentifierRequest request); - Task UpdateSelfAsync(UpdateUserIdentifierRequest request); - Task SetPrimarySelfAsync(SetPrimaryUserIdentifierRequest request); - Task UnsetPrimarySelfAsync(UnsetPrimaryUserIdentifierRequest request); - Task VerifySelfAsync(VerifyUserIdentifierRequest request); - Task DeleteSelfAsync(DeleteUserIdentifierRequest request); + Task>> GetMyIdentifiersAsync(); + Task AddSelfAsync(AddUserIdentifierRequest request); + Task UpdateSelfAsync(UpdateUserIdentifierRequest request); + Task SetPrimarySelfAsync(SetPrimaryUserIdentifierRequest request); + Task UnsetPrimarySelfAsync(UnsetPrimaryUserIdentifierRequest request); + Task VerifySelfAsync(VerifyUserIdentifierRequest request); + Task DeleteSelfAsync(DeleteUserIdentifierRequest request); - Task>> GetUserIdentifiersAsync(UserKey userKey); - Task AddAdminAsync(UserKey userKey, AddUserIdentifierRequest request); - Task UpdateAdminAsync(UserKey userKey, UpdateUserIdentifierRequest request); - Task SetPrimaryAdminAsync(UserKey userKey, SetPrimaryUserIdentifierRequest request); - Task UnsetPrimaryAdminAsync(UserKey userKey, UnsetPrimaryUserIdentifierRequest request); - Task VerifyAdminAsync(UserKey userKey, VerifyUserIdentifierRequest request); - Task DeleteAdminAsync(UserKey userKey, DeleteUserIdentifierRequest request); - } + Task>> GetUserIdentifiersAsync(UserKey userKey); + Task AddAdminAsync(UserKey userKey, AddUserIdentifierRequest request); + Task UpdateAdminAsync(UserKey userKey, UpdateUserIdentifierRequest request); + Task SetPrimaryAdminAsync(UserKey userKey, SetPrimaryUserIdentifierRequest request); + Task UnsetPrimaryAdminAsync(UserKey userKey, UnsetPrimaryUserIdentifierRequest request); + Task VerifyAdminAsync(UserKey userKey, VerifyUserIdentifierRequest request); + Task DeleteAdminAsync(UserKey userKey, DeleteUserIdentifierRequest request); } diff --git a/src/CodeBeam.UltimateAuth.Client/Storage/.gitkeep b/src/CodeBeam.UltimateAuth.Client/Storage/.gitkeep deleted file mode 100644 index 5f282702..00000000 --- a/src/CodeBeam.UltimateAuth.Client/Storage/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/CodeBeam.UltimateAuth.Core/AssemblyVisibility.cs b/src/CodeBeam.UltimateAuth.Core/AssemblyVisibility.cs index 50348cae..768f9e27 100644 --- a/src/CodeBeam.UltimateAuth.Core/AssemblyVisibility.cs +++ b/src/CodeBeam.UltimateAuth.Core/AssemblyVisibility.cs @@ -2,3 +2,4 @@ [assembly: InternalsVisibleTo("CodeBeam.UltimateAuth.Server")] [assembly: InternalsVisibleTo("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore")] +[assembly: InternalsVisibleTo("CodeBeam.UltimateAuth.Tests.Unit")] diff --git a/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionExpiredException.cs b/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionExpiredException.cs index f84fa777..aa7277ad 100644 --- a/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionExpiredException.cs +++ b/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionExpiredException.cs @@ -1,26 +1,25 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Core.Errors +namespace CodeBeam.UltimateAuth.Core.Errors; + +/// +/// Represents an authentication-domain exception thrown when a session +/// has passed its expiration time. +/// +/// This exception is raised during validation or refresh attempts where +/// the session's timestamp +/// indicates that it is no longer valid. +/// +/// Once expired, a session cannot be refreshed — the user must log in again. +/// +public sealed class UAuthSessionExpiredException : UAuthSessionException { /// - /// Represents an authentication-domain exception thrown when a session - /// has passed its expiration time. - /// - /// This exception is raised during validation or refresh attempts where - /// the session's timestamp - /// indicates that it is no longer valid. - /// - /// Once expired, a session cannot be refreshed — the user must log in again. + /// Initializes a new instance of the class + /// using the expired session's identifier. /// - public sealed class UAuthSessionExpiredException : UAuthSessionException + /// The identifier of the expired session. + public UAuthSessionExpiredException(AuthSessionId sessionId) : base(sessionId, $"Session '{sessionId}' has expired.") { - /// - /// Initializes a new instance of the class - /// using the expired session's identifier. - /// - /// The identifier of the expired session. - public UAuthSessionExpiredException(AuthSessionId sessionId) : base(sessionId, $"Session '{sessionId}' has expired.") - { - } } } diff --git a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/TenantKeys.cs b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/TenantKeys.cs new file mode 100644 index 00000000..7467d4d4 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/TenantKeys.cs @@ -0,0 +1,7 @@ +namespace CodeBeam.UltimateAuth.Core.MultiTenancy; + +public static class TenantKeys +{ + public static TenantKey Single => TenantKey.Single; + public static TenantKey System => TenantKey.System; +} diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/UAuthEndpointRegistrar.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/UAuthEndpointRegistrar.cs index 2aa483c8..781d64b3 100644 --- a/src/CodeBeam.UltimateAuth.Server/Endpoints/UAuthEndpointRegistrar.cs +++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/UAuthEndpointRegistrar.cs @@ -5,256 +5,254 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; -using static CodeBeam.UltimateAuth.Server.Defaults.UAuthActions; -namespace CodeBeam.UltimateAuth.Server.Endpoints +namespace CodeBeam.UltimateAuth.Server.Endpoints; + +public interface IAuthEndpointRegistrar { - public interface IAuthEndpointRegistrar - { - void MapEndpoints(RouteGroupBuilder rootGroup, UAuthServerOptions options); - } + void MapEndpoints(RouteGroupBuilder rootGroup, UAuthServerOptions options); +} - // TODO: Add Scalar/Swagger integration - // TODO: Add endpoint based guards - public class UAuthEndpointRegistrar : IAuthEndpointRegistrar +// TODO: Add Scalar/Swagger integration +// TODO: Add endpoint based guards +public class UAuthEndpointRegistrar : IAuthEndpointRegistrar +{ + public void MapEndpoints(RouteGroupBuilder rootGroup, UAuthServerOptions options) { - public void MapEndpoints(RouteGroupBuilder rootGroup, UAuthServerOptions options) - { - // Default base: /auth - string basePrefix = options.RoutePrefix.TrimStart('/'); - bool useRouteTenant = options.MultiTenant.Enabled && options.MultiTenant.EnableRoute; - - RouteGroupBuilder group = useRouteTenant - ? rootGroup.MapGroup("/{tenant}/" + basePrefix) - : rootGroup.MapGroup("/" + basePrefix); + // Default base: /auth + string basePrefix = options.RoutePrefix.TrimStart('/'); + bool useRouteTenant = options.MultiTenant.Enabled && options.MultiTenant.EnableRoute; - group.AddEndpointFilter(); + RouteGroupBuilder group = useRouteTenant + ? rootGroup.MapGroup("/{tenant}/" + basePrefix) + : rootGroup.MapGroup("/" + basePrefix); - if (options.EnableLoginEndpoints != false) - { - group.MapPost("/login", async ([FromServices] ILoginEndpointHandler h, HttpContext ctx) - => await h.LoginAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Login)); + group.AddEndpointFilter(); - group.MapPost("/validate", async ([FromServices] IValidateEndpointHandler h, HttpContext ctx) - => await h.ValidateAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.ValidateSession)); + if (options.EnableLoginEndpoints != false) + { + group.MapPost("/login", async ([FromServices] ILoginEndpointHandler h, HttpContext ctx) + => await h.LoginAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Login)); - group.MapPost("/logout", async ([FromServices] ILogoutEndpointHandler h, HttpContext ctx) - => await h.LogoutAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Logout)); + group.MapPost("/validate", async ([FromServices] IValidateEndpointHandler h, HttpContext ctx) + => await h.ValidateAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.ValidateSession)); - group.MapPost("/refresh", async ([FromServices] IRefreshEndpointHandler h, HttpContext ctx) - => await h.RefreshAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.RefreshSession)); + group.MapPost("/logout", async ([FromServices] ILogoutEndpointHandler h, HttpContext ctx) + => await h.LogoutAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Logout)); - group.MapPost("/reauth", async ([FromServices] IReauthEndpointHandler h, HttpContext ctx) - => await h.ReauthAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Reauthentication)); - } + group.MapPost("/refresh", async ([FromServices] IRefreshEndpointHandler h, HttpContext ctx) + => await h.RefreshAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.RefreshSession)); - if (options.EnablePkceEndpoints != false) - { - var pkce = group.MapGroup("/pkce"); + group.MapPost("/reauth", async ([FromServices] IReauthEndpointHandler h, HttpContext ctx) + => await h.ReauthAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Reauthentication)); + } - pkce.MapPost("/authorize", async ([FromServices] IPkceEndpointHandler h, HttpContext ctx) - => await h.AuthorizeAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Login)); + if (options.EnablePkceEndpoints != false) + { + var pkce = group.MapGroup("/pkce"); - pkce.MapPost("/complete", async ([FromServices] IPkceEndpointHandler h, HttpContext ctx) - => await h.CompleteAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Login)); - } + pkce.MapPost("/authorize", async ([FromServices] IPkceEndpointHandler h, HttpContext ctx) + => await h.AuthorizeAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Login)); - if (options.EnableTokenEndpoints != false) - { - var token = group.MapGroup(""); + pkce.MapPost("/complete", async ([FromServices] IPkceEndpointHandler h, HttpContext ctx) + => await h.CompleteAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Login)); + } - token.MapPost("/token", async ([FromServices] ITokenEndpointHandler h, HttpContext ctx) - => await h.GetTokenAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.IssueToken)); + if (options.EnableTokenEndpoints != false) + { + var token = group.MapGroup(""); - token.MapPost("/refresh-token", async ([FromServices] ITokenEndpointHandler h, HttpContext ctx) - => await h.RefreshTokenAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.RefreshToken)); + token.MapPost("/token", async ([FromServices] ITokenEndpointHandler h, HttpContext ctx) + => await h.GetTokenAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.IssueToken)); - token.MapPost("/introspect", async ([FromServices] ITokenEndpointHandler h, HttpContext ctx) - => await h.IntrospectAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.IntrospectToken)); + token.MapPost("/refresh-token", async ([FromServices] ITokenEndpointHandler h, HttpContext ctx) + => await h.RefreshTokenAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.RefreshToken)); - token.MapPost("/revoke", async ([FromServices] ITokenEndpointHandler h, HttpContext ctx) - => await h.RevokeAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.RevokeToken)); - } + token.MapPost("/introspect", async ([FromServices] ITokenEndpointHandler h, HttpContext ctx) + => await h.IntrospectAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.IntrospectToken)); - if (options.EnableSessionEndpoints != false) - { - var session = group.MapGroup("/session"); + token.MapPost("/revoke", async ([FromServices] ITokenEndpointHandler h, HttpContext ctx) + => await h.RevokeAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.RevokeToken)); + } - session.MapPost("/current", async ([FromServices] ISessionManagementHandler h, HttpContext ctx) - => await h.GetCurrentSessionAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.QuerySession)); + if (options.EnableSessionEndpoints != false) + { + var session = group.MapGroup("/session"); - session.MapPost("/list", async ([FromServices] ISessionManagementHandler h, HttpContext ctx) - => await h.GetAllSessionsAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.QuerySession)); + session.MapPost("/current", async ([FromServices] ISessionManagementHandler h, HttpContext ctx) + => await h.GetCurrentSessionAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.QuerySession)); - session.MapPost("/revoke/{sessionId}", async ([FromServices] ISessionManagementHandler h, string sessionId, HttpContext ctx) - => await h.RevokeSessionAsync(sessionId, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.RevokeSession)); + session.MapPost("/list", async ([FromServices] ISessionManagementHandler h, HttpContext ctx) + => await h.GetAllSessionsAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.QuerySession)); - session.MapPost("/revoke-all", async ([FromServices] ISessionManagementHandler h, HttpContext ctx) - => await h.RevokeAllAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.RevokeSession)); - } + session.MapPost("/revoke/{sessionId}", async ([FromServices] ISessionManagementHandler h, string sessionId, HttpContext ctx) + => await h.RevokeSessionAsync(sessionId, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.RevokeSession)); - var user = group.MapGroup(""); - var users = group.MapGroup("/users"); - var adminUsers = group.MapGroup("/admin/users"); + session.MapPost("/revoke-all", async ([FromServices] ISessionManagementHandler h, HttpContext ctx) + => await h.RevokeAllAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.RevokeSession)); + } - //if (options.EnableUserInfoEndpoints != false) - //{ - // user.MapPost("/userinfo", async ([FromServices] IUserInfoEndpointHandler h, HttpContext ctx) - // => await h.GetUserInfoAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserInfo)); + var user = group.MapGroup(""); + var users = group.MapGroup("/users"); + var adminUsers = group.MapGroup("/admin/users"); - // user.MapPost("/permissions", async ([FromServices] IUserInfoEndpointHandler h, HttpContext ctx) - // => await h.GetPermissionsAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.PermissionQuery)); + //if (options.EnableUserInfoEndpoints != false) + //{ + // user.MapPost("/userinfo", async ([FromServices] IUserInfoEndpointHandler h, HttpContext ctx) + // => await h.GetUserInfoAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserInfo)); - // user.MapPost("/permissions/check", async ([FromServices] IUserInfoEndpointHandler h, HttpContext ctx) - // => await h.CheckPermissionAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.PermissionQuery)); - //} + // user.MapPost("/permissions", async ([FromServices] IUserInfoEndpointHandler h, HttpContext ctx) + // => await h.GetPermissionsAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.PermissionQuery)); - if (options.EnableUserLifecycleEndpoints != false) - { - users.MapPost("/create", async ([FromServices] IUserEndpointHandler h, HttpContext ctx) - => await h.CreateAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserManagement)); + // user.MapPost("/permissions/check", async ([FromServices] IUserInfoEndpointHandler h, HttpContext ctx) + // => await h.CheckPermissionAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.PermissionQuery)); + //} - users.MapPost("/me/status", async ([FromServices] IUserEndpointHandler h, HttpContext ctx) - => await h.ChangeStatusSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserManagement)); + if (options.EnableUserLifecycleEndpoints != false) + { + users.MapPost("/create", async ([FromServices] IUserEndpointHandler h, HttpContext ctx) + => await h.CreateAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserManagement)); - adminUsers.MapPost("/{userKey}/status", async ([FromServices] IUserEndpointHandler h, UserKey userKey, HttpContext ctx) - => await h.ChangeStatusAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserManagement)); + users.MapPost("/me/status", async ([FromServices] IUserEndpointHandler h, HttpContext ctx) + => await h.ChangeStatusSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserManagement)); - // Post is intended for Auth - adminUsers.MapPost("/{userKey}/delete", async ([FromServices] IUserEndpointHandler h, UserKey userKey, HttpContext ctx) - => await h.DeleteAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserManagement)); - } + adminUsers.MapPost("/{userKey}/status", async ([FromServices] IUserEndpointHandler h, UserKey userKey, HttpContext ctx) + => await h.ChangeStatusAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserManagement)); - if (options.EnableUserProfileEndpoints != false) - { - users.MapPost("/me/get", async ([FromServices] IUserEndpointHandler h, HttpContext ctx) - => await h.GetMeAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserProfileManagement)); + // Post is intended for Auth + adminUsers.MapPost("/{userKey}/delete", async ([FromServices] IUserEndpointHandler h, UserKey userKey, HttpContext ctx) + => await h.DeleteAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserManagement)); + } - users.MapPost("/me/update", async ([FromServices] IUserEndpointHandler h, HttpContext ctx) - => await h.UpdateMeAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserProfileManagement)); + if (options.EnableUserProfileEndpoints != false) + { + users.MapPost("/me/get", async ([FromServices] IUserEndpointHandler h, HttpContext ctx) + => await h.GetMeAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserProfileManagement)); - adminUsers.MapPost("/{userKey}/profile/get", async ([FromServices] IUserEndpointHandler h, UserKey userKey, HttpContext ctx) - => await h.GetUserAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserProfileManagement)); + users.MapPost("/me/update", async ([FromServices] IUserEndpointHandler h, HttpContext ctx) + => await h.UpdateMeAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserProfileManagement)); - adminUsers.MapPost("/{userKey}/profile/update", async ([FromServices] IUserEndpointHandler h, UserKey userKey, HttpContext ctx) - => await h.UpdateUserAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserProfileManagement)); - } + adminUsers.MapPost("/{userKey}/profile/get", async ([FromServices] IUserEndpointHandler h, UserKey userKey, HttpContext ctx) + => await h.GetUserAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserProfileManagement)); - if (options.EnableUserIdentifierEndpoints != false) - { - users.MapPost("/me/identifiers/get", async ([FromServices] IUserEndpointHandler h, HttpContext ctx) - => await h.GetMyIdentifiersAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement)); + adminUsers.MapPost("/{userKey}/profile/update", async ([FromServices] IUserEndpointHandler h, UserKey userKey, HttpContext ctx) + => await h.UpdateUserAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserProfileManagement)); + } - users.MapPost("/me/identifiers/add", async ([FromServices] IUserEndpointHandler h, HttpContext ctx) - => await h.AddUserIdentifierSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement)); + if (options.EnableUserIdentifierEndpoints != false) + { + users.MapPost("/me/identifiers/get", async ([FromServices] IUserEndpointHandler h, HttpContext ctx) + => await h.GetMyIdentifiersAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement)); - users.MapPost("/me/identifiers/update", async ([FromServices] IUserEndpointHandler h, HttpContext ctx) - => await h.UpdateUserIdentifierSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement)); + users.MapPost("/me/identifiers/add", async ([FromServices] IUserEndpointHandler h, HttpContext ctx) + => await h.AddUserIdentifierSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement)); - users.MapPost("/me/identifiers/set-primary",async ([FromServices] IUserEndpointHandler h, HttpContext ctx) - => await h.SetPrimaryUserIdentifierSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement)); + users.MapPost("/me/identifiers/update", async ([FromServices] IUserEndpointHandler h, HttpContext ctx) + => await h.UpdateUserIdentifierSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement)); - users.MapPost("/me/identifiers/unset-primary", async ([FromServices] IUserEndpointHandler h, HttpContext ctx) - => await h.UnsetPrimaryUserIdentifierSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement)); + users.MapPost("/me/identifiers/set-primary",async ([FromServices] IUserEndpointHandler h, HttpContext ctx) + => await h.SetPrimaryUserIdentifierSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement)); - users.MapPost("/me/identifiers/verify", async ([FromServices] IUserEndpointHandler h, HttpContext ctx) - => await h.VerifyUserIdentifierSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement)); + users.MapPost("/me/identifiers/unset-primary", async ([FromServices] IUserEndpointHandler h, HttpContext ctx) + => await h.UnsetPrimaryUserIdentifierSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement)); - users.MapPost("/me/identifiers/delete", async ([FromServices] IUserEndpointHandler h, HttpContext ctx) - => await h.DeleteUserIdentifierSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement)); + users.MapPost("/me/identifiers/verify", async ([FromServices] IUserEndpointHandler h, HttpContext ctx) + => await h.VerifyUserIdentifierSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement)); + users.MapPost("/me/identifiers/delete", async ([FromServices] IUserEndpointHandler h, HttpContext ctx) + => await h.DeleteUserIdentifierSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement)); - adminUsers.MapPost("/{userKey}/identifiers/get", async ([FromServices] IUserEndpointHandler h, UserKey userKey, HttpContext ctx) - => await h.GetUserIdentifiersAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement)); - adminUsers.MapPost("/{userKey}/identifiers/add", async ([FromServices] IUserEndpointHandler h, UserKey userKey, HttpContext ctx) - => await h.AddUserIdentifierAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement)); + adminUsers.MapPost("/{userKey}/identifiers/get", async ([FromServices] IUserEndpointHandler h, UserKey userKey, HttpContext ctx) + => await h.GetUserIdentifiersAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement)); - adminUsers.MapPost("/{userKey}/identifiers/update", async ([FromServices] IUserEndpointHandler h, UserKey userKey, HttpContext ctx) - => await h.UpdateUserIdentifierAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement)); + adminUsers.MapPost("/{userKey}/identifiers/add", async ([FromServices] IUserEndpointHandler h, UserKey userKey, HttpContext ctx) + => await h.AddUserIdentifierAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement)); - adminUsers.MapPost("/{userKey}/identifiers/set-primary", async ([FromServices] IUserEndpointHandler h, UserKey userKey, HttpContext ctx) - => await h.SetPrimaryUserIdentifierAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement)); + adminUsers.MapPost("/{userKey}/identifiers/update", async ([FromServices] IUserEndpointHandler h, UserKey userKey, HttpContext ctx) + => await h.UpdateUserIdentifierAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement)); - adminUsers.MapPost("/{userKey}/identifiers/unset-primary", async ([FromServices] IUserEndpointHandler h, UserKey userKey, HttpContext ctx) - => await h.UnsetPrimaryUserIdentifierAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement)); + adminUsers.MapPost("/{userKey}/identifiers/set-primary", async ([FromServices] IUserEndpointHandler h, UserKey userKey, HttpContext ctx) + => await h.SetPrimaryUserIdentifierAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement)); - adminUsers.MapPost("/{userKey}/identifiers/verify", async ([FromServices] IUserEndpointHandler h, UserKey userKey, HttpContext ctx) - => await h.VerifyUserIdentifierAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement)); + adminUsers.MapPost("/{userKey}/identifiers/unset-primary", async ([FromServices] IUserEndpointHandler h, UserKey userKey, HttpContext ctx) + => await h.UnsetPrimaryUserIdentifierAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement)); - adminUsers.MapPost("/{userKey}/identifiers/delete", async ([FromServices] IUserEndpointHandler h, UserKey userKey, HttpContext ctx) - => await h.DeleteUserIdentifierAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement)); - } + adminUsers.MapPost("/{userKey}/identifiers/verify", async ([FromServices] IUserEndpointHandler h, UserKey userKey, HttpContext ctx) + => await h.VerifyUserIdentifierAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement)); - if (options.EnableCredentialsEndpoints != false) - { - var credentials = group.MapGroup("/credentials"); - var adminCredentials = group.MapGroup("/admin/users/{userKey}/credentials"); + adminUsers.MapPost("/{userKey}/identifiers/delete", async ([FromServices] IUserEndpointHandler h, UserKey userKey, HttpContext ctx) + => await h.DeleteUserIdentifierAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement)); + } - credentials.MapPost("/get", async ([FromServices] ICredentialEndpointHandler h, HttpContext ctx) - => await h.GetAllAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement)); + if (options.EnableCredentialsEndpoints != false) + { + var credentials = group.MapGroup("/credentials"); + var adminCredentials = group.MapGroup("/admin/users/{userKey}/credentials"); - credentials.MapPost("/add", async ([FromServices] ICredentialEndpointHandler h, HttpContext ctx) - => await h.AddAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement)); + credentials.MapPost("/get", async ([FromServices] ICredentialEndpointHandler h, HttpContext ctx) + => await h.GetAllAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement)); - credentials.MapPost("/{type}/change", async ([FromServices] ICredentialEndpointHandler h, string type, HttpContext ctx) - => await h.ChangeAsync(type, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement)); + credentials.MapPost("/add", async ([FromServices] ICredentialEndpointHandler h, HttpContext ctx) + => await h.AddAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement)); - credentials.MapPost("/{type}/revoke", async ([FromServices] ICredentialEndpointHandler h, string type, HttpContext ctx) - => await h.RevokeAsync(type, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement)); + credentials.MapPost("/{type}/change", async ([FromServices] ICredentialEndpointHandler h, string type, HttpContext ctx) + => await h.ChangeAsync(type, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement)); - credentials.MapPost("/{type}/reset/begin", async ([FromServices] ICredentialEndpointHandler h, string type, HttpContext ctx) - => await h.BeginResetAsync(type, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement)); + credentials.MapPost("/{type}/revoke", async ([FromServices] ICredentialEndpointHandler h, string type, HttpContext ctx) + => await h.RevokeAsync(type, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement)); - credentials.MapPost("/{type}/reset/complete", async ([FromServices] ICredentialEndpointHandler h, string type, HttpContext ctx) - => await h.CompleteResetAsync(type, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement)); + credentials.MapPost("/{type}/reset/begin", async ([FromServices] ICredentialEndpointHandler h, string type, HttpContext ctx) + => await h.BeginResetAsync(type, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement)); + credentials.MapPost("/{type}/reset/complete", async ([FromServices] ICredentialEndpointHandler h, string type, HttpContext ctx) + => await h.CompleteResetAsync(type, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement)); - adminCredentials.MapPost("/get", async ([FromServices] ICredentialEndpointHandler h, UserKey userKey, HttpContext ctx) - => await h.GetAllAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement)); - adminCredentials.MapPost("/add", async ([FromServices] ICredentialEndpointHandler h, UserKey userKey, HttpContext ctx) - => await h.AddAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement)); + adminCredentials.MapPost("/get", async ([FromServices] ICredentialEndpointHandler h, UserKey userKey, HttpContext ctx) + => await h.GetAllAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement)); - adminCredentials.MapPost("/{type}/revoke", async ([FromServices] ICredentialEndpointHandler h, UserKey userKey, string type, HttpContext ctx) - => await h.RevokeAdminAsync(userKey, type, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement)); + adminCredentials.MapPost("/add", async ([FromServices] ICredentialEndpointHandler h, UserKey userKey, HttpContext ctx) + => await h.AddAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement)); - adminCredentials.MapPost("/{type}/activate", async ([FromServices] ICredentialEndpointHandler h, UserKey userKey, string type, HttpContext ctx) - => await h.ActivateAdminAsync(userKey, type, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement)); + adminCredentials.MapPost("/{type}/revoke", async ([FromServices] ICredentialEndpointHandler h, UserKey userKey, string type, HttpContext ctx) + => await h.RevokeAdminAsync(userKey, type, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement)); - adminCredentials.MapPost("/{type}/reset/begin", async ([FromServices] ICredentialEndpointHandler h, UserKey userKey, string type, HttpContext ctx) - => await h.BeginResetAdminAsync(userKey, type, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement)); + adminCredentials.MapPost("/{type}/activate", async ([FromServices] ICredentialEndpointHandler h, UserKey userKey, string type, HttpContext ctx) + => await h.ActivateAdminAsync(userKey, type, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement)); - adminCredentials.MapPost("/{type}/reset/complete", async ([FromServices] ICredentialEndpointHandler h, UserKey userKey, string type, HttpContext ctx) - => await h.CompleteResetAdminAsync(userKey, type, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement)); + adminCredentials.MapPost("/{type}/reset/begin", async ([FromServices] ICredentialEndpointHandler h, UserKey userKey, string type, HttpContext ctx) + => await h.BeginResetAdminAsync(userKey, type, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement)); - adminCredentials.MapPost("/{type}/delete", async ([FromServices] ICredentialEndpointHandler h, UserKey userKey, string type, HttpContext ctx) - => await h.DeleteAdminAsync(userKey, type, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement)); - } + adminCredentials.MapPost("/{type}/reset/complete", async ([FromServices] ICredentialEndpointHandler h, UserKey userKey, string type, HttpContext ctx) + => await h.CompleteResetAdminAsync(userKey, type, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement)); - if (options.EnableAuthorizationEndpoints != false) - { - var authz = group.MapGroup("/authorization"); - var adminAuthz = group.MapGroup("/admin/authorization"); + adminCredentials.MapPost("/{type}/delete", async ([FromServices] ICredentialEndpointHandler h, UserKey userKey, string type, HttpContext ctx) + => await h.DeleteAdminAsync(userKey, type, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement)); + } - authz.MapPost("/check", async ([FromServices] IAuthorizationEndpointHandler h, HttpContext ctx) - => await h.CheckAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.AuthorizationManagement)); + if (options.EnableAuthorizationEndpoints != false) + { + var authz = group.MapGroup("/authorization"); + var adminAuthz = group.MapGroup("/admin/authorization"); - authz.MapPost("/users/me/roles/get", async ([FromServices] IAuthorizationEndpointHandler h, HttpContext ctx) - => await h.GetMyRolesAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.AuthorizationManagement)); + authz.MapPost("/check", async ([FromServices] IAuthorizationEndpointHandler h, HttpContext ctx) + => await h.CheckAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.AuthorizationManagement)); + authz.MapPost("/users/me/roles/get", async ([FromServices] IAuthorizationEndpointHandler h, HttpContext ctx) + => await h.GetMyRolesAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.AuthorizationManagement)); - adminAuthz.MapPost("/users/{userKey}/roles/get", async ([FromServices] IAuthorizationEndpointHandler h, UserKey userKey, HttpContext ctx) - => await h.GetUserRolesAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.AuthorizationManagement)); - adminAuthz.MapPost("/users/{userKey}/roles/post", async ([FromServices] IAuthorizationEndpointHandler h, UserKey userKey, HttpContext ctx) - => await h.AssignRoleAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.AuthorizationManagement)); + adminAuthz.MapPost("/users/{userKey}/roles/get", async ([FromServices] IAuthorizationEndpointHandler h, UserKey userKey, HttpContext ctx) + => await h.GetUserRolesAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.AuthorizationManagement)); - adminAuthz.MapPost("/users/{userKey}/roles/delete", async ([FromServices] IAuthorizationEndpointHandler h, UserKey userKey, HttpContext ctx) - => await h.RemoveRoleAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.AuthorizationManagement)); - } + adminAuthz.MapPost("/users/{userKey}/roles/post", async ([FromServices] IAuthorizationEndpointHandler h, UserKey userKey, HttpContext ctx) + => await h.AssignRoleAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.AuthorizationManagement)); + adminAuthz.MapPost("/users/{userKey}/roles/delete", async ([FromServices] IAuthorizationEndpointHandler h, UserKey userKey, HttpContext ctx) + => await h.RemoveRoleAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.AuthorizationManagement)); } } + } diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Dtos/CredentialDto.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Dtos/CredentialDto.cs index a4d8a2f6..9d01e8a2 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Dtos/CredentialDto.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Dtos/CredentialDto.cs @@ -1,11 +1,18 @@ -namespace CodeBeam.UltimateAuth.Credentials.Contracts +namespace CodeBeam.UltimateAuth.Credentials.Contracts; + +public sealed record CredentialDto { - public sealed record CredentialDto( - CredentialType Type, - CredentialSecurityStatus Status, - DateTimeOffset CreatedAt, - DateTimeOffset? LastUsedAt, - DateTimeOffset? RestrictedUntil, - DateTimeOffset? ExpiresAt, - string? Source); + public CredentialType Type { get; init; } + + public CredentialSecurityStatus Status { get; init; } + + public DateTimeOffset CreatedAt { get; init; } + + public DateTimeOffset? LastUsedAt { get; init; } + + public DateTimeOffset? RestrictedUntil { get; init; } + + public DateTimeOffset? ExpiresAt { get; init; } + + public string? Source { get; init; } } diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Dtos/CredentialMetadata.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Dtos/CredentialMetadata.cs index 134dee3b..a3fed36e 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Dtos/CredentialMetadata.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Dtos/CredentialMetadata.cs @@ -1,6 +1,8 @@ namespace CodeBeam.UltimateAuth.Credentials.Contracts; -public sealed record CredentialMetadata( - DateTimeOffset CreatedAt, - DateTimeOffset? LastUsedAt, - string? Source); +public sealed record CredentialMetadata +{ + public DateTimeOffset CreatedAt { get; init; } + public DateTimeOffset? LastUsedAt { get; init; } + public string? Source { get; init; } +} diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Extensions/CredentialTypeParser.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Extensions/CredentialTypeParser.cs index 7b101443..d90804d7 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Extensions/CredentialTypeParser.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Extensions/CredentialTypeParser.cs @@ -1,35 +1,34 @@ -namespace CodeBeam.UltimateAuth.Credentials.Contracts +namespace CodeBeam.UltimateAuth.Credentials.Contracts; + +public static class CredentialTypeParser { - public static class CredentialTypeParser - { - private static readonly Dictionary _map = - new(StringComparer.OrdinalIgnoreCase) - { - ["password"] = CredentialType.Password, + private static readonly Dictionary _map = + new(StringComparer.OrdinalIgnoreCase) + { + ["password"] = CredentialType.Password, - ["otp"] = CredentialType.OneTimeCode, - ["one-time-code"] = CredentialType.OneTimeCode, + ["otp"] = CredentialType.OneTimeCode, + ["one-time-code"] = CredentialType.OneTimeCode, - ["email-otp"] = CredentialType.EmailOtp, - ["sms-otp"] = CredentialType.SmsOtp, + ["email-otp"] = CredentialType.EmailOtp, + ["sms-otp"] = CredentialType.SmsOtp, - ["totp"] = CredentialType.Totp, + ["totp"] = CredentialType.Totp, - ["passkey"] = CredentialType.Passkey, + ["passkey"] = CredentialType.Passkey, - ["certificate"] = CredentialType.Certificate, - ["cert"] = CredentialType.Certificate, + ["certificate"] = CredentialType.Certificate, + ["cert"] = CredentialType.Certificate, - ["api-key"] = CredentialType.ApiKey, - ["apikey"] = CredentialType.ApiKey, + ["api-key"] = CredentialType.ApiKey, + ["apikey"] = CredentialType.ApiKey, - ["external"] = CredentialType.External - }; + ["external"] = CredentialType.External + }; - public static bool TryParse(string value, out CredentialType type) => _map.TryGetValue(value, out type); + public static bool TryParse(string value, out CredentialType type) => _map.TryGetValue(value, out type); - public static CredentialType ParseOrThrow(string value) => TryParse(value, out var type) - ? type - : throw new InvalidOperationException($"Unsupported credential type: '{value}'"); - } + public static CredentialType ParseOrThrow(string value) => TryParse(value, out var type) + ? type + : throw new InvalidOperationException($"Unsupported credential type: '{value}'"); } diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Request/AddCredentialRequest.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Request/AddCredentialRequest.cs index dd26c9d3..89a903c5 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Request/AddCredentialRequest.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Request/AddCredentialRequest.cs @@ -1,9 +1,8 @@ -namespace CodeBeam.UltimateAuth.Credentials.Contracts +namespace CodeBeam.UltimateAuth.Credentials.Contracts; + +public sealed record AddCredentialRequest() { - public sealed record AddCredentialRequest() - { - public CredentialType Type { get; set; } - public required string Secret { get; set; } - public string? Source { get; set; } - } + public CredentialType Type { get; set; } + public required string Secret { get; set; } + public string? Source { get; set; } } diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Request/BeginCredentialResetRequest.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Request/BeginCredentialResetRequest.cs index bd6cd4be..98eb2e4c 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Request/BeginCredentialResetRequest.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Request/BeginCredentialResetRequest.cs @@ -1,7 +1,6 @@ -namespace CodeBeam.UltimateAuth.Credentials.Contracts +namespace CodeBeam.UltimateAuth.Credentials.Contracts; + +public sealed record BeginCredentialResetRequest { - public sealed record BeginCredentialResetRequest - { - public string? Reason { get; init; } - } + public string? Reason { get; init; } } diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Request/CompleteCredentialResetRequest.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Request/CompleteCredentialResetRequest.cs index 6afdea41..7dcaf3da 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Request/CompleteCredentialResetRequest.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Request/CompleteCredentialResetRequest.cs @@ -1,8 +1,7 @@ -namespace CodeBeam.UltimateAuth.Credentials.Contracts +namespace CodeBeam.UltimateAuth.Credentials.Contracts; + +public sealed record CompleteCredentialResetRequest { - public sealed record CompleteCredentialResetRequest - { - public required string NewSecret { get; init; } - public string? Source { get; init; } - } + public required string NewSecret { get; init; } + public string? Source { get; init; } } diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Request/ResetPasswordRequest.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Request/ResetPasswordRequest.cs index 1e144536..a895d5e7 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Request/ResetPasswordRequest.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Request/ResetPasswordRequest.cs @@ -1,15 +1,14 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Credentials.Contracts +namespace CodeBeam.UltimateAuth.Credentials.Contracts; + +public sealed record ResetPasswordRequest { - public sealed record ResetPasswordRequest - { - public UserKey UserKey { get; init; } = default!; - public required string NewPassword { get; init; } + public UserKey UserKey { get; init; } = default!; + public required string NewPassword { get; init; } - /// - /// Optional reset token or verification code. - /// - public string? Token { get; init; } - } + /// + /// Optional reset token or verification code. + /// + public string? Token { get; init; } } diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Request/RevokeAllCredentialsRequest.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Request/RevokeAllCredentialsRequest.cs index dda45bf3..855b606e 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Request/RevokeAllCredentialsRequest.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Request/RevokeAllCredentialsRequest.cs @@ -1,9 +1,8 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Credentials.Contracts +namespace CodeBeam.UltimateAuth.Credentials.Contracts; + +public sealed class RevokeAllCredentialsRequest { - public sealed class RevokeAllCredentialsRequest - { - public required UserKey UserKey { get; init; } - } + public required UserKey UserKey { get; init; } } diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Request/RevokeCredentialRequest.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Request/RevokeCredentialRequest.cs index ad049edc..108fa25c 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Request/RevokeCredentialRequest.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Request/RevokeCredentialRequest.cs @@ -1,6 +1,15 @@ -namespace CodeBeam.UltimateAuth.Credentials.Contracts +namespace CodeBeam.UltimateAuth.Credentials.Contracts; + +public sealed record RevokeCredentialRequest { - public sealed record RevokeCredentialRequest( - DateTimeOffset? Until = null, - string? Reason = null); + /// + /// If specified, credential is revoked until this time. + /// Null means permanent revocation. + /// + public DateTimeOffset? Until { get; init; } + + /// + /// Optional human-readable reason for audit/logging purposes. + /// + public string? Reason { get; init; } } diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Responses/AddCredentialResult.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Responses/AddCredentialResult.cs index b6956b44..dd9f3daa 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Responses/AddCredentialResult.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Responses/AddCredentialResult.cs @@ -1,14 +1,24 @@ -namespace CodeBeam.UltimateAuth.Credentials.Contracts +namespace CodeBeam.UltimateAuth.Credentials.Contracts; + +public sealed record AddCredentialResult { - public sealed record AddCredentialResult( - bool Succeeded, - string? Error, - CredentialType? Type = null) - { - public static AddCredentialResult Success(CredentialType type) - => new(true, null, type); - - public static AddCredentialResult Fail(string error) - => new(false, error); - } + public bool Succeeded { get; init; } + + public string? Error { get; init; } + + public CredentialType? Type { get; init; } + + public static AddCredentialResult Success(CredentialType type) + => new() + { + Succeeded = true, + Type = type + }; + + public static AddCredentialResult Fail(string error) + => new() + { + Succeeded = false, + Error = error + }; } diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Responses/ChangeCredentialResult.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Responses/ChangeCredentialResult.cs index 59250862..8579bc23 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Responses/ChangeCredentialResult.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Responses/ChangeCredentialResult.cs @@ -1,13 +1,24 @@ namespace CodeBeam.UltimateAuth.Credentials.Contracts; -public sealed record ChangeCredentialResult( - bool Succeeded, - string? Error, - CredentialType? Type = null) +public sealed record ChangeCredentialResult { + public bool Succeeded { get; init; } + + public string? Error { get; init; } + + public CredentialType? Type { get; init; } + public static ChangeCredentialResult Success(CredentialType type) - => new(true, null, type); + => new() + { + Succeeded = true, + Type = type + }; public static ChangeCredentialResult Fail(string error) - => new(false, error); + => new() + { + Succeeded = false, + Error = error + }; } diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Responses/CredentialActionResult.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Responses/CredentialActionResult.cs index acdac294..f45a6659 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Responses/CredentialActionResult.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Responses/CredentialActionResult.cs @@ -1,13 +1,21 @@ -namespace CodeBeam.UltimateAuth.Credentials.Contracts +namespace CodeBeam.UltimateAuth.Credentials.Contracts; + +public sealed record CredentialActionResult { - public sealed record CredentialActionResult( - bool Succeeded, - string? Error) - { - public static CredentialActionResult Success() - => new(true, null); - - public static CredentialActionResult Fail(string error) - => new(false, error); - } + public bool Succeeded { get; init; } + + public string? Error { get; init; } + + public static CredentialActionResult Success() + => new() + { + Succeeded = true + }; + + public static CredentialActionResult Fail(string error) + => new() + { + Succeeded = false, + Error = error + }; } diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Responses/CredentialChangeResult.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Responses/CredentialChangeResult.cs index 9ccd3c79..ad00b575 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Responses/CredentialChangeResult.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Responses/CredentialChangeResult.cs @@ -1,6 +1,13 @@ namespace CodeBeam.UltimateAuth.Credentials.Contracts; -public sealed record CredentialChangeResult( - bool Succeeded, - bool SecurityInvalidated, - string? FailureReason = null); +public sealed record CredentialChangeResult +{ + public bool Succeeded { get; init; } + + /// + /// Indicates whether security version / sessions were invalidated. + /// + public bool SecurityInvalidated { get; init; } + + public string? FailureReason { get; init; } +} diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Responses/CredentialProvisionResult.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Responses/CredentialProvisionResult.cs index 4952fda4..36d9ae8d 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Responses/CredentialProvisionResult.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Responses/CredentialProvisionResult.cs @@ -14,8 +14,6 @@ public sealed record CredentialProvisionResult public string? FailureReason { get; init; } - /* ----------------- Helpers ----------------- */ - public static CredentialProvisionResult Success(CredentialType type) => new() { diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Responses/CredentialValidationResult.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Responses/CredentialValidationResult.cs index e861b999..8cb7ea61 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Responses/CredentialValidationResult.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Responses/CredentialValidationResult.cs @@ -1,33 +1,36 @@ -namespace CodeBeam.UltimateAuth.Credentials.Contracts +namespace CodeBeam.UltimateAuth.Credentials.Contracts; + +public sealed record CredentialValidationResult { - public sealed record CredentialValidationResult( - bool IsValid, - bool RequiresReauthentication, - bool RequiresSecurityVersionIncrement, - string? FailureReason = null) - { - public static CredentialValidationResult Success( - bool requiresSecurityVersionIncrement = false) - => new( - IsValid: true, - RequiresReauthentication: false, - RequiresSecurityVersionIncrement: requiresSecurityVersionIncrement); + public bool IsValid { get; init; } + public bool RequiresReauthentication { get; init; } + public bool RequiresSecurityVersionIncrement { get; init; } + public string? FailureReason { get; init; } + + public static CredentialValidationResult Success( + bool requiresSecurityVersionIncrement = false) + => new() + { + IsValid = true, + RequiresSecurityVersionIncrement = requiresSecurityVersionIncrement + }; - public static CredentialValidationResult Failed( - string? reason = null, - bool requiresReauthentication = false) - => new( - IsValid: false, - RequiresReauthentication: requiresReauthentication, - RequiresSecurityVersionIncrement: false, - FailureReason: reason); + public static CredentialValidationResult Failed( + string? reason = null, + bool requiresReauthentication = false) + => new() + { + IsValid = false, + RequiresReauthentication = requiresReauthentication, + FailureReason = reason + }; - public static CredentialValidationResult ReauthenticationRequired( - string? reason = null) - => new( - IsValid: false, - RequiresReauthentication: true, - RequiresSecurityVersionIncrement: false, - FailureReason: reason); - } + public static CredentialValidationResult ReauthenticationRequired( + string? reason = null) + => new() + { + IsValid = false, + RequiresReauthentication = true, + FailureReason = reason + }; } diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Responses/CredentialValidationResultDto.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Responses/CredentialValidationResultDto.cs deleted file mode 100644 index c8018a51..00000000 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Responses/CredentialValidationResultDto.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace CodeBeam.UltimateAuth.Credentials.Contracts; - -public sealed record CredentialValidationResultDto -{ - public bool IsValid { get; init; } - - public bool RequiresReauthentication { get; init; } - public bool RequiresSecurityVersionIncrement { get; init; } - - public string? FailureReason { get; init; } -} diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Responses/GetCredentialsResult.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Responses/GetCredentialsResult.cs index 0ad73e96..ce621456 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Responses/GetCredentialsResult.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Responses/GetCredentialsResult.cs @@ -1,5 +1,6 @@ -namespace CodeBeam.UltimateAuth.Credentials.Contracts +namespace CodeBeam.UltimateAuth.Credentials.Contracts; + +public sealed record GetCredentialsResult { - public sealed record GetCredentialsResult( - IReadOnlyCollection Credentials); + public IReadOnlyCollection Credentials { get; init; } = Array.Empty(); } diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore.csproj b/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore.csproj index 4ffa082d..fcba3cc0 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore.csproj +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore.csproj @@ -6,6 +6,7 @@ enable 0.0.1-preview true + $(NoWarn);1591 diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Configuration/ConventionResolver.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Configuration/ConventionResolver.cs index 4b2c9f2e..08378ed6 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Configuration/ConventionResolver.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Configuration/ConventionResolver.cs @@ -1,24 +1,23 @@ using System.Linq.Expressions; -namespace CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore +namespace CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore; + +internal static class ConventionResolver { - internal static class ConventionResolver + public static Expression>? TryResolve(params string[] names) { - public static Expression>? TryResolve(params string[] names) - { - var prop = typeof(TUser) - .GetProperties() - .FirstOrDefault(p => - names.Contains(p.Name, StringComparer.OrdinalIgnoreCase) && - typeof(TProp).IsAssignableFrom(p.PropertyType)); + var prop = typeof(TUser) + .GetProperties() + .FirstOrDefault(p => + names.Contains(p.Name, StringComparer.OrdinalIgnoreCase) && + typeof(TProp).IsAssignableFrom(p.PropertyType)); - if (prop is null) - return null; + if (prop is null) + return null; - var param = Expression.Parameter(typeof(TUser), "u"); - var body = Expression.Property(param, prop); + var param = Expression.Parameter(typeof(TUser), "u"); + var body = Expression.Property(param, prop); - return Expression.Lambda>(body, param); - } + return Expression.Lambda>(body, param); } } diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Configuration/CredentialUserMappingBuilder.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Configuration/CredentialUserMappingBuilder.cs index dfb653c7..4baa14c7 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Configuration/CredentialUserMappingBuilder.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Configuration/CredentialUserMappingBuilder.cs @@ -1,75 +1,74 @@ -namespace CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore +namespace CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore; + +internal static class CredentialUserMappingBuilder { - internal static class CredentialUserMappingBuilder + public static CredentialUserMapping Build(CredentialUserMappingOptions options) { - public static CredentialUserMapping Build(CredentialUserMappingOptions options) + if (options.UserId is null) { - if (options.UserId is null) - { - var expr = ConventionResolver.TryResolve("Id", "UserId"); - if (expr != null) - options.ApplyUserId(expr); - } + var expr = ConventionResolver.TryResolve("Id", "UserId"); + if (expr != null) + options.ApplyUserId(expr); + } - if (options.Username is null) - { - var expr = ConventionResolver.TryResolve( - "Username", - "UserName", - "Email", - "EmailAddress", - "Login"); + if (options.Username is null) + { + var expr = ConventionResolver.TryResolve( + "Username", + "UserName", + "Email", + "EmailAddress", + "Login"); - if (expr != null) - options.ApplyUsername(expr); - } + if (expr != null) + options.ApplyUsername(expr); + } - // Never add "Password" as a convention to avoid accidental mapping to plaintext password properties - if (options.PasswordHash is null) - { - var expr = ConventionResolver.TryResolve( - "PasswordHash", - "Passwordhash", - "PasswordHashV2"); + // Never add "Password" as a convention to avoid accidental mapping to plaintext password properties + if (options.PasswordHash is null) + { + var expr = ConventionResolver.TryResolve( + "PasswordHash", + "Passwordhash", + "PasswordHashV2"); - if (expr != null) - options.ApplyPasswordHash(expr); - } + if (expr != null) + options.ApplyPasswordHash(expr); + } - if (options.SecurityVersion is null) - { - var expr = ConventionResolver.TryResolve( - "SecurityVersion", - "SecurityStamp", - "AuthVersion"); + if (options.SecurityVersion is null) + { + var expr = ConventionResolver.TryResolve( + "SecurityVersion", + "SecurityStamp", + "AuthVersion"); - if (expr != null) - options.ApplySecurityVersion(expr); - } + if (expr != null) + options.ApplySecurityVersion(expr); + } - if (options.UserId is null) - throw new InvalidOperationException("UserId mapping is required. Use MapUserId(...) or ensure a conventional property exists."); + if (options.UserId is null) + throw new InvalidOperationException("UserId mapping is required. Use MapUserId(...) or ensure a conventional property exists."); - if (options.Username is null) - throw new InvalidOperationException("Username mapping is required. Use MapUsername(...) or ensure a conventional property exists."); + if (options.Username is null) + throw new InvalidOperationException("Username mapping is required. Use MapUsername(...) or ensure a conventional property exists."); - if (options.PasswordHash is null) - throw new InvalidOperationException("PasswordHash mapping is required. Use MapPasswordHash(...) or ensure a conventional property exists."); + if (options.PasswordHash is null) + throw new InvalidOperationException("PasswordHash mapping is required. Use MapPasswordHash(...) or ensure a conventional property exists."); - if (options.SecurityVersion is null) - throw new InvalidOperationException("SecurityVersion mapping is required. Use MapSecurityVersion(...) or ensure a conventional property exists."); + if (options.SecurityVersion is null) + throw new InvalidOperationException("SecurityVersion mapping is required. Use MapSecurityVersion(...) or ensure a conventional property exists."); - var canAuthenticateExpr = options.CanAuthenticate ?? (_ => true); + var canAuthenticateExpr = options.CanAuthenticate ?? (_ => true); - return new CredentialUserMapping - { - UserId = options.UserId.Compile(), - Username = options.Username.Compile(), - PasswordHash = options.PasswordHash.Compile(), - SecurityVersion = options.SecurityVersion.Compile(), - CanAuthenticate = canAuthenticateExpr.Compile() - }; - } + return new CredentialUserMapping + { + UserId = options.UserId.Compile(), + Username = options.Username.Compile(), + PasswordHash = options.PasswordHash.Compile(), + SecurityVersion = options.SecurityVersion.Compile(), + CanAuthenticate = canAuthenticateExpr.Compile() + }; } } diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/EfCoreAuthUser.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/EfCoreAuthUser.cs index 7c91dd85..61a8b904 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/EfCoreAuthUser.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/EfCoreAuthUser.cs @@ -1,16 +1,15 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore +namespace CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore; + +internal sealed class EfCoreAuthUser : IAuthSubject { - internal sealed class EfCoreAuthUser : IAuthSubject - { - public TUserId UserId { get; } + public TUserId UserId { get; } - IReadOnlyDictionary? IAuthSubject.Claims => null; + IReadOnlyDictionary? IAuthSubject.Claims => null; - public EfCoreAuthUser(TUserId userId) - { - UserId = userId; - } + public EfCoreAuthUser(TUserId userId) + { + UserId = userId; } } diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Infrastructure/EfCoreUserStore.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Infrastructure/EfCoreUserStore.cs deleted file mode 100644 index 1ede89b8..00000000 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Infrastructure/EfCoreUserStore.cs +++ /dev/null @@ -1,83 +0,0 @@ -//using CodeBeam.UltimateAuth.Core.Abstractions; -//using CodeBeam.UltimateAuth.Core.Domain; -//using CodeBeam.UltimateAuth.Core.Infrastructure; -//using Microsoft.EntityFrameworkCore; -//using Microsoft.Extensions.Options; - -//namespace CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore; - -//internal sealed class EfCoreUserStore : IUAuthUserStore where TUser : class -//{ -// private readonly DbContext _db; -// private readonly CredentialUserMapping _map; - -// public EfCoreUserStore(DbContext db, IOptions> options) -// { -// _db = db; -// _map = CredentialUserMappingBuilder.Build(options.Value); -// } - -// public async Task?> FindByIdAsync(string? tenantId, TUserId userId, CancellationToken ct = default) -// { -// var user = await _db.Set().FirstOrDefaultAsync(u => _map.UserId(u)!.Equals(userId), ct); - -// if (user is null || !_map.CanAuthenticate(user)) -// return null; - -// return new EfCoreAuthUser(_map.UserId(user)); -// } - -// public async Task?> FindByUsernameAsync(string? tenantId, string username, CancellationToken ct = default) -// { -// var user = await _db.Set().FirstOrDefaultAsync(u => _map.Username(u) == username, ct); - -// if (user is null || !_map.CanAuthenticate(user)) -// return null; - -// return new UserRecord -// { -// Id = _map.UserId(user), -// Username = _map.Username(user), -// PasswordHash = _map.PasswordHash(user), -// IsActive = true, -// CreatedAt = DateTimeOffset.UtcNow, -// IsDeleted = false -// }; -// } - -// public async Task?> FindByLoginAsync(string? tenantId, string login, CancellationToken ct = default) -// { -// var user = await _db.Set().FirstOrDefaultAsync(u => _map.Username(u) == login, ct); - -// if (user is null || !_map.CanAuthenticate(user)) -// return null; - -// return new EfCoreAuthUser(_map.UserId(user)); -// } - -// public Task GetPasswordHashAsync(string? tenantId, TUserId userId, CancellationToken ct = default) -// { -// return _db.Set() -// .Where(u => _map.UserId(u)!.Equals(userId)) -// .Select(u => _map.PasswordHash(u)) -// .FirstOrDefaultAsync(ct); -// } - -// public Task SetPasswordHashAsync(string? tenantId, TUserId userId, string passwordHash, CancellationToken token = default) -// { -// throw new NotSupportedException("Password updates are not supported by EfCoreUserStore. " + -// "Use application-level user management services."); -// } - -// public async Task GetSecurityVersionAsync(string? tenantId, TUserId userId, CancellationToken ct = default) -// { -// var user = await _db.Set().FirstOrDefaultAsync(u => _map.UserId(u)!.Equals(userId), ct); -// return user is null ? 0 : _map.SecurityVersion(user); -// } - -// public Task IncrementSecurityVersionAsync(string? tenantId, TUserId userId, CancellationToken token = default) -// { -// throw new NotSupportedException("Security version updates must be handled by the application."); -// } - -//} diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/ServiceCollectionExtensions.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/ServiceCollectionExtensions.cs index 060b8665..97442ecb 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/ServiceCollectionExtensions.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/ServiceCollectionExtensions.cs @@ -1,19 +1,13 @@ -using CodeBeam.UltimateAuth.Core.Abstractions; -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; namespace CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore; public static class ServiceCollectionExtensions { - public static IServiceCollection AddUltimateAuthEfCoreCredentials( - this IServiceCollection services, - Action> configure) - where TUser : class + public static IServiceCollection AddUltimateAuthEfCoreCredentials(this IServiceCollection services, Action> configure) where TUser : class { services.Configure(configure); - //services.AddScoped, EfCoreUserStore>(); - return services; } } diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/CodeBeam.UltimateAuth.Credentials.InMemory.csproj b/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/CodeBeam.UltimateAuth.Credentials.InMemory.csproj index dbc070bb..944f0a0a 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/CodeBeam.UltimateAuth.Credentials.InMemory.csproj +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/CodeBeam.UltimateAuth.Credentials.InMemory.csproj @@ -6,6 +6,7 @@ enable 0.0.1-preview true + $(NoWarn);1591 diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/InMemoryCredentialSeedContributor.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/InMemoryCredentialSeedContributor.cs index b6d5dbcb..6d51c4f1 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/InMemoryCredentialSeedContributor.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/InMemoryCredentialSeedContributor.cs @@ -5,42 +5,41 @@ using CodeBeam.UltimateAuth.Credentials.Contracts; using CodeBeam.UltimateAuth.Credentials.Reference; -namespace CodeBeam.UltimateAuth.Credentials.InMemory +namespace CodeBeam.UltimateAuth.Credentials.InMemory; + +internal sealed class InMemoryCredentialSeedContributor : ISeedContributor { - internal sealed class InMemoryCredentialSeedContributor : ISeedContributor - { - public int Order => 10; + public int Order => 10; - private readonly ICredentialStore _credentials; - private readonly IInMemoryUserIdProvider _ids; - private readonly IUAuthPasswordHasher _hasher; + private readonly ICredentialStore _credentials; + private readonly IInMemoryUserIdProvider _ids; + private readonly IUAuthPasswordHasher _hasher; - public InMemoryCredentialSeedContributor(ICredentialStore credentials, IInMemoryUserIdProvider ids, IUAuthPasswordHasher hasher) - { - _credentials = credentials; - _ids = ids; - _hasher = hasher; - } + public InMemoryCredentialSeedContributor(ICredentialStore credentials, IInMemoryUserIdProvider ids, IUAuthPasswordHasher hasher) + { + _credentials = credentials; + _ids = ids; + _hasher = hasher; + } - public async Task SeedAsync(TenantKey tenant, CancellationToken ct = default) - { - await SeedCredentialAsync("admin", _ids.GetAdminUserId(), tenant, ct); - await SeedCredentialAsync("user", _ids.GetUserUserId(), tenant, ct); - } + public async Task SeedAsync(TenantKey tenant, CancellationToken ct = default) + { + await SeedCredentialAsync("admin", _ids.GetAdminUserId(), tenant, ct); + await SeedCredentialAsync("user", _ids.GetUserUserId(), tenant, ct); + } - private async Task SeedCredentialAsync(string login, UserKey userKey, TenantKey tenant, CancellationToken ct) - { - if (await _credentials.ExistsAsync(tenant, userKey, CredentialType.Password, ct)) - return; + private async Task SeedCredentialAsync(string login, UserKey userKey, TenantKey tenant, CancellationToken ct) + { + if (await _credentials.ExistsAsync(tenant, userKey, CredentialType.Password, ct)) + return; - await _credentials.AddAsync(tenant, - new PasswordCredential( - userKey, - login, - _hasher.Hash(login), - CredentialSecurityState.Active, - new CredentialMetadata(DateTimeOffset.Now, null, null)), - ct); - } + await _credentials.AddAsync(tenant, + new PasswordCredential( + userKey, + login, + _hasher.Hash(login), + CredentialSecurityState.Active, + new CredentialMetadata { CreatedAt = DateTimeOffset.Now}), + ct); } } diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/InMemoryPasswordCredentialState.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/InMemoryPasswordCredentialState.cs index c9c06d7d..b9da6751 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/InMemoryPasswordCredentialState.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/InMemoryPasswordCredentialState.cs @@ -1,16 +1,15 @@ using CodeBeam.UltimateAuth.Credentials.Contracts; -namespace CodeBeam.UltimateAuth.Credentials.InMemory +namespace CodeBeam.UltimateAuth.Credentials.InMemory; + +internal sealed class InMemoryPasswordCredentialState { - internal sealed class InMemoryPasswordCredentialState - { - public TUserId UserId { get; init; } = default!; - public CredentialType Type { get; } = CredentialType.Password; + public TUserId UserId { get; init; } = default!; + public CredentialType Type { get; } = CredentialType.Password; - public string Login { get; init; } = default!; - public string SecretHash { get; set; } = default!; + public string Login { get; init; } = default!; + public string SecretHash { get; set; } = default!; - public CredentialSecurityState Security { get; set; } = default!; - public CredentialMetadata Metadata { get; set; } = default!; - } + public CredentialSecurityState Security { get; set; } = default!; + public CredentialMetadata Metadata { get; set; } = default!; } diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/UltimateAuthDefaultsInMemoryExtensions.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/ServiceCollectionExtensions.cs similarity index 92% rename from src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/UltimateAuthDefaultsInMemoryExtensions.cs rename to src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/ServiceCollectionExtensions.cs index 422d72a1..8385ec31 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/UltimateAuthDefaultsInMemoryExtensions.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/ServiceCollectionExtensions.cs @@ -4,7 +4,7 @@ namespace CodeBeam.UltimateAuth.Credentials.InMemory.Extensions { - public static class UltimateAuthCredentialsInMemoryExtensions + public static class ServiceCollectionExtensions { public static IServiceCollection AddUltimateAuthCredentialsInMemory(this IServiceCollection services) { diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Commands/ActivateCredentialCommand.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Commands/ActivateCredentialCommand.cs index 78526c99..d496e7f7 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Commands/ActivateCredentialCommand.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Commands/ActivateCredentialCommand.cs @@ -1,6 +1,4 @@ -using CodeBeam.UltimateAuth.Core.Abstractions; -using CodeBeam.UltimateAuth.Core.Contracts; -using CodeBeam.UltimateAuth.Credentials.Contracts; +using CodeBeam.UltimateAuth.Credentials.Contracts; using CodeBeam.UltimateAuth.Server.Infrastructure; namespace CodeBeam.UltimateAuth.Credentials.Reference; diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Commands/AddCredentialCommand.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Commands/AddCredentialCommand.cs index 60fa352d..e0426f87 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Commands/AddCredentialCommand.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Commands/AddCredentialCommand.cs @@ -1,20 +1,16 @@ -using CodeBeam.UltimateAuth.Core.Abstractions; -using CodeBeam.UltimateAuth.Core.Contracts; -using CodeBeam.UltimateAuth.Credentials.Contracts; +using CodeBeam.UltimateAuth.Credentials.Contracts; using CodeBeam.UltimateAuth.Server.Infrastructure; -namespace CodeBeam.UltimateAuth.Credentials.Reference -{ - internal sealed class AddCredentialCommand : IAccessCommand - { - private readonly Func> _execute; +namespace CodeBeam.UltimateAuth.Credentials.Reference; - public AddCredentialCommand(Func> execute) - { - _execute = execute; - } +internal sealed class AddCredentialCommand : IAccessCommand +{ + private readonly Func> _execute; - public Task ExecuteAsync(CancellationToken ct = default) => _execute(ct); + public AddCredentialCommand(Func> execute) + { + _execute = execute; } + public Task ExecuteAsync(CancellationToken ct = default) => _execute(ct); } diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Commands/BeginCredentialResetCommand.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Commands/BeginCredentialResetCommand.cs index e3e6fd7a..7cd3bde0 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Commands/BeginCredentialResetCommand.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Commands/BeginCredentialResetCommand.cs @@ -1,6 +1,4 @@ -using CodeBeam.UltimateAuth.Core.Abstractions; -using CodeBeam.UltimateAuth.Core.Contracts; -using CodeBeam.UltimateAuth.Credentials.Contracts; +using CodeBeam.UltimateAuth.Credentials.Contracts; using CodeBeam.UltimateAuth.Server.Infrastructure; namespace CodeBeam.UltimateAuth.Credentials.Reference; diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Commands/ChangeCredentialCommand.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Commands/ChangeCredentialCommand.cs index 14b476de..f9117ea8 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Commands/ChangeCredentialCommand.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Commands/ChangeCredentialCommand.cs @@ -1,5 +1,4 @@ - -using CodeBeam.UltimateAuth.Credentials.Contracts; +using CodeBeam.UltimateAuth.Credentials.Contracts; using CodeBeam.UltimateAuth.Server.Infrastructure; namespace CodeBeam.UltimateAuth.Credentials.Reference; diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Commands/GetAllCredentialsCommand.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Commands/GetAllCredentialsCommand.cs index 7c3461ce..098d01bc 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Commands/GetAllCredentialsCommand.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Commands/GetAllCredentialsCommand.cs @@ -1,17 +1,16 @@ using CodeBeam.UltimateAuth.Credentials.Contracts; using CodeBeam.UltimateAuth.Server.Infrastructure; -namespace CodeBeam.UltimateAuth.Credentials.Reference -{ - internal sealed class GetAllCredentialsCommand : IAccessCommand - { - private readonly Func> _execute; +namespace CodeBeam.UltimateAuth.Credentials.Reference; - public GetAllCredentialsCommand(Func> execute) - { - _execute = execute; - } +internal sealed class GetAllCredentialsCommand : IAccessCommand +{ + private readonly Func> _execute; - public Task ExecuteAsync(CancellationToken ct = default) => _execute(ct); + public GetAllCredentialsCommand(Func> execute) + { + _execute = execute; } + + public Task ExecuteAsync(CancellationToken ct = default) => _execute(ct); } diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Commands/SetInitialCredentialCommand.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Commands/SetInitialCredentialCommand.cs index 113e914d..4efed9f9 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Commands/SetInitialCredentialCommand.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Commands/SetInitialCredentialCommand.cs @@ -1,16 +1,15 @@ using CodeBeam.UltimateAuth.Server.Infrastructure; -namespace CodeBeam.UltimateAuth.Credentials.Reference -{ - internal sealed class SetInitialCredentialCommand : IAccessCommand - { - private readonly Func _execute; +namespace CodeBeam.UltimateAuth.Credentials.Reference; - public SetInitialCredentialCommand(Func execute) - { - _execute = execute; - } +internal sealed class SetInitialCredentialCommand : IAccessCommand +{ + private readonly Func _execute; - public Task ExecuteAsync(CancellationToken ct = default) => _execute(ct); + public SetInitialCredentialCommand(Func execute) + { + _execute = execute; } + + public Task ExecuteAsync(CancellationToken ct = default) => _execute(ct); } diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Endpoints/DefaultCredentialEndpointHandler.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Endpoints/DefaultCredentialEndpointHandler.cs index 25ea1b96..259ea480 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Endpoints/DefaultCredentialEndpointHandler.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Endpoints/DefaultCredentialEndpointHandler.cs @@ -14,10 +14,7 @@ public sealed class DefaultCredentialEndpointHandler : ICredentialEndpointHandle private readonly IAccessContextFactory _accessContextFactory; private readonly IUserCredentialsService _credentials; - public DefaultCredentialEndpointHandler( - IAuthFlowContextAccessor authFlow, - IAccessContextFactory accessContextFactory, - IUserCredentialsService credentials) + public DefaultCredentialEndpointHandler(IAuthFlowContextAccessor authFlow, IAccessContextFactory accessContextFactory, IUserCredentialsService credentials) { _authFlow = authFlow; _accessContextFactory = accessContextFactory; diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Infrastructure/PasswordUserLifecycleIntegration.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Infrastructure/PasswordUserLifecycleIntegration.cs index 2f5a14eb..da6165c6 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Infrastructure/PasswordUserLifecycleIntegration.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Infrastructure/PasswordUserLifecycleIntegration.cs @@ -36,7 +36,7 @@ public async Task OnUserCreatedAsync(TenantKey tenant, UserKey userKey, object r loginIdentifier: r.PrimaryIdentifierValue!, secretHash: hash, security: new CredentialSecurityState(CredentialSecurityStatus.Active, null, null, null), - metadata: new CredentialMetadata(_clock.UtcNow, _clock.UtcNow, null)); + metadata: new CredentialMetadata { CreatedAt = _clock.UtcNow, LastUsedAt = _clock.UtcNow }); await _credentialStore.AddAsync(tenant, credential, ct); } diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Services/DefaultUserCredentialsService.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Services/DefaultUserCredentialsService.cs index 9489d37c..ec0ec362 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Services/DefaultUserCredentialsService.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Services/DefaultUserCredentialsService.cs @@ -44,17 +44,20 @@ public async Task GetAllAsync(AccessContext context, Cance var dtos = creds .OfType() - .Select(c => new CredentialDto( - c.Type, - c.Security.Status, - c.Metadata.CreatedAt, - c.Metadata.LastUsedAt, - c.Security.RestrictedUntil, - c.Security.ExpiresAt, - c.Metadata.Source)) + .Select(c => new CredentialDto { + Type = c.Type, + Status = c.Security.Status, + CreatedAt = c.Metadata.CreatedAt, + LastUsedAt = c.Metadata.LastUsedAt, + RestrictedUntil = c.Security.RestrictedUntil, + ExpiresAt = c.Security.ExpiresAt, + Source = c.Metadata.Source}) .ToArray(); - return new GetCredentialsResult(dtos); + return new GetCredentialsResult + { + Credentials = dtos + }; }); return await _accessOrchestrator.ExecuteAsync(context, cmd, ct); @@ -84,10 +87,7 @@ public async Task AddAsync(AccessContext context, AddCreden loginIdentifier: userKey.Value, secretHash: hash, security: new CredentialSecurityState(CredentialSecurityStatus.Active), - metadata: new CredentialMetadata( - _clock.UtcNow, - null, - request.Source)); + metadata: new CredentialMetadata { CreatedAt = _clock.UtcNow, Source = request.Source }); await _credentials.AddAsync(context.ResourceTenant, credential, innerCt); diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials/Abstractions/ICredentialDescriptor.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials/Abstractions/ICredentialDescriptor.cs index 82a654a1..c8a4af55 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials/Abstractions/ICredentialDescriptor.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials/Abstractions/ICredentialDescriptor.cs @@ -1,11 +1,10 @@ using CodeBeam.UltimateAuth.Credentials.Contracts; -namespace CodeBeam.UltimateAuth.Credentials +namespace CodeBeam.UltimateAuth.Credentials; + +public interface ICredentialDescriptor { - public interface ICredentialDescriptor - { - CredentialType Type { get; } - CredentialSecurityState Security { get; } - CredentialMetadata Metadata { get; } - } + CredentialType Type { get; } + CredentialSecurityState Security { get; } + CredentialMetadata Metadata { get; } } diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials/Abstractions/ICredentialSecretStore.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials/Abstractions/ICredentialSecretStore.cs index 8bad6d77..2686afc3 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials/Abstractions/ICredentialSecretStore.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials/Abstractions/ICredentialSecretStore.cs @@ -1,10 +1,9 @@ using CodeBeam.UltimateAuth.Core.MultiTenancy; using CodeBeam.UltimateAuth.Credentials.Contracts; -namespace CodeBeam.UltimateAuth.Credentials +namespace CodeBeam.UltimateAuth.Credentials; + +public interface ICredentialSecretStore { - public interface ICredentialSecretStore - { - Task SetAsync(TenantKey tenant, TUserId userId, CredentialType type, string secretHash, CancellationToken ct = default); - } + Task SetAsync(TenantKey tenant, TUserId userId, CredentialType type, string secretHash, CancellationToken ct = default); } diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials/Abstractions/ICredentialStore.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials/Abstractions/ICredentialStore.cs index 2f3a3b0f..0d1f87f3 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials/Abstractions/ICredentialStore.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials/Abstractions/ICredentialStore.cs @@ -1,18 +1,17 @@ using CodeBeam.UltimateAuth.Core.MultiTenancy; using CodeBeam.UltimateAuth.Credentials.Contracts; -namespace CodeBeam.UltimateAuth.Credentials +namespace CodeBeam.UltimateAuth.Credentials; + +public interface ICredentialStore { - public interface ICredentialStore - { - Task>>FindByLoginAsync(TenantKey tenant, string loginIdentifier, CancellationToken ct = default); - Task>>GetByUserAsync(TenantKey tenant, TUserId userId, CancellationToken ct = default); - Task>>GetByUserAndTypeAsync(TenantKey tenant, TUserId userId, CredentialType type, CancellationToken ct = default); - Task AddAsync(TenantKey tenant, ICredential credential, CancellationToken ct = default); - Task UpdateSecurityStateAsync(TenantKey tenant, TUserId userId, CredentialType type, CredentialSecurityState securityState, CancellationToken ct = default); - Task UpdateMetadataAsync(TenantKey tenant, TUserId userId, CredentialType type, CredentialMetadata metadata, CancellationToken ct = default); - Task DeleteAsync(TenantKey tenant, TUserId userId, CredentialType type, CancellationToken ct = default); - Task DeleteByUserAsync(TenantKey tenant, TUserId userId, CancellationToken ct = default); - Task ExistsAsync(TenantKey tenant, TUserId userId, CredentialType type, CancellationToken ct = default); - } + Task>>FindByLoginAsync(TenantKey tenant, string loginIdentifier, CancellationToken ct = default); + Task>>GetByUserAsync(TenantKey tenant, TUserId userId, CancellationToken ct = default); + Task>>GetByUserAndTypeAsync(TenantKey tenant, TUserId userId, CredentialType type, CancellationToken ct = default); + Task AddAsync(TenantKey tenant, ICredential credential, CancellationToken ct = default); + Task UpdateSecurityStateAsync(TenantKey tenant, TUserId userId, CredentialType type, CredentialSecurityState securityState, CancellationToken ct = default); + Task UpdateMetadataAsync(TenantKey tenant, TUserId userId, CredentialType type, CredentialMetadata metadata, CancellationToken ct = default); + Task DeleteAsync(TenantKey tenant, TUserId userId, CredentialType type, CancellationToken ct = default); + Task DeleteByUserAsync(TenantKey tenant, TUserId userId, CancellationToken ct = default); + Task ExistsAsync(TenantKey tenant, TUserId userId, CredentialType type, CancellationToken ct = default); } diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials/Abstractions/IPublicKeyCredential.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials/Abstractions/IPublicKeyCredential.cs index 0902d43b..e6f6a21d 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials/Abstractions/IPublicKeyCredential.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials/Abstractions/IPublicKeyCredential.cs @@ -1,7 +1,6 @@ -namespace CodeBeam.UltimateAuth.Credentials +namespace CodeBeam.UltimateAuth.Credentials; + +public interface IPublicKeyCredential : ICredential { - public interface IPublicKeyCredential : ICredential - { - byte[] PublicKey { get; } - } + byte[] PublicKey { get; } } diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials/Abstractions/ISecurableCredential.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials/Abstractions/ISecurableCredential.cs index 89989e16..ceb20f58 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials/Abstractions/ISecurableCredential.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials/Abstractions/ISecurableCredential.cs @@ -1,9 +1,8 @@ using CodeBeam.UltimateAuth.Credentials.Contracts; -namespace CodeBeam.UltimateAuth.Credentials +namespace CodeBeam.UltimateAuth.Credentials; + +public interface ISecurableCredential { - public interface ISecurableCredential - { - CredentialSecurityState Security { get; } - } + CredentialSecurityState Security { get; } } diff --git a/src/policies/CodeBeam.UltimateAuth.Policies/CodeBeam.UltimateAuth.Policies.csproj b/src/policies/CodeBeam.UltimateAuth.Policies/CodeBeam.UltimateAuth.Policies.csproj index eb3dffea..e03d7456 100644 --- a/src/policies/CodeBeam.UltimateAuth.Policies/CodeBeam.UltimateAuth.Policies.csproj +++ b/src/policies/CodeBeam.UltimateAuth.Policies/CodeBeam.UltimateAuth.Policies.csproj @@ -14,9 +14,4 @@ - - - - - diff --git a/src/policies/CodeBeam.UltimateAuth.Policies/Defaults/CompiledAccessPolicySet.cs b/src/policies/CodeBeam.UltimateAuth.Policies/Defaults/CompiledAccessPolicySet.cs index 7450d420..0d592554 100644 --- a/src/policies/CodeBeam.UltimateAuth.Policies/Defaults/CompiledAccessPolicySet.cs +++ b/src/policies/CodeBeam.UltimateAuth.Policies/Defaults/CompiledAccessPolicySet.cs @@ -2,33 +2,32 @@ using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Policies.Registry; -namespace CodeBeam.UltimateAuth.Policies.Defaults +namespace CodeBeam.UltimateAuth.Policies.Defaults; + +public sealed class CompiledAccessPolicySet { - public sealed class CompiledAccessPolicySet + private readonly PolicyRegistration[] _registrations; + + internal CompiledAccessPolicySet(PolicyRegistration[] registrations) { - private readonly PolicyRegistration[] _registrations; + _registrations = registrations; + } - internal CompiledAccessPolicySet(PolicyRegistration[] registrations) - { - _registrations = registrations; - } + public IReadOnlyList Resolve(AccessContext context, IServiceProvider services) + { + var list = new List(); - public IReadOnlyList Resolve(AccessContext context, IServiceProvider services) + foreach (var r in _registrations) { - var list = new List(); - - foreach (var r in _registrations) + if (context.Action.StartsWith(r.ActionPrefix, StringComparison.OrdinalIgnoreCase)) { - if (context.Action.StartsWith(r.ActionPrefix, StringComparison.OrdinalIgnoreCase)) - { - var policy = r.Factory(services); + var policy = r.Factory(services); - if (policy.AppliesTo(context)) - list.Add(policy); - } + if (policy.AppliesTo(context)) + list.Add(policy); } - - return list; } + + return list; } } diff --git a/src/policies/CodeBeam.UltimateAuth.Policies/Fluent/ConditionalPolicyBuilder.cs b/src/policies/CodeBeam.UltimateAuth.Policies/Fluent/ConditionalPolicyBuilder.cs index 7cdf491a..0a0fd764 100644 --- a/src/policies/CodeBeam.UltimateAuth.Policies/Fluent/ConditionalPolicyBuilder.cs +++ b/src/policies/CodeBeam.UltimateAuth.Policies/Fluent/ConditionalPolicyBuilder.cs @@ -1,32 +1,23 @@ using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Policies.Registry; -namespace CodeBeam.UltimateAuth.Policies -{ - internal sealed class ConditionalPolicyBuilder : IConditionalPolicyBuilder - { - private readonly string _prefix; - private readonly Func _condition; - private readonly AccessPolicyRegistry _registry; - private readonly IServiceProvider _services; - - public ConditionalPolicyBuilder( - string prefix, - Func condition, - AccessPolicyRegistry registry, - IServiceProvider services) - { - _prefix = prefix; - _condition = condition; - _registry = registry; - _services = services; - } +namespace CodeBeam.UltimateAuth.Policies; - public IPolicyScopeBuilder Then() - => new ConditionalScopeBuilder(_prefix, _condition, true, _registry, _services); +internal sealed class ConditionalPolicyBuilder : IConditionalPolicyBuilder +{ + private readonly string _prefix; + private readonly Func _condition; + private readonly AccessPolicyRegistry _registry; + private readonly IServiceProvider _services; - public IPolicyScopeBuilder Otherwise() - => new ConditionalScopeBuilder(_prefix, _condition, false, _registry, _services); + public ConditionalPolicyBuilder(string prefix, Func condition, AccessPolicyRegistry registry, IServiceProvider services) + { + _prefix = prefix; + _condition = condition; + _registry = registry; + _services = services; } + public IPolicyScopeBuilder Then() => new ConditionalScopeBuilder(_prefix, _condition, true, _registry, _services); + public IPolicyScopeBuilder Otherwise() => new ConditionalScopeBuilder(_prefix, _condition, false, _registry, _services); } diff --git a/src/policies/CodeBeam.UltimateAuth.Policies/Fluent/ConditionalScopeBuilder.cs b/src/policies/CodeBeam.UltimateAuth.Policies/Fluent/ConditionalScopeBuilder.cs index b9e832ca..594f2723 100644 --- a/src/policies/CodeBeam.UltimateAuth.Policies/Fluent/ConditionalScopeBuilder.cs +++ b/src/policies/CodeBeam.UltimateAuth.Policies/Fluent/ConditionalScopeBuilder.cs @@ -3,44 +3,34 @@ using CodeBeam.UltimateAuth.Policies.Registry; using Microsoft.Extensions.DependencyInjection; -namespace CodeBeam.UltimateAuth.Policies -{ - internal sealed class ConditionalScopeBuilder : IPolicyScopeBuilder - { - private readonly string _prefix; - private readonly Func _condition; - private readonly bool _expected; - private readonly AccessPolicyRegistry _registry; - private readonly IServiceProvider _services; - - public ConditionalScopeBuilder(string prefix, Func condition, bool expected, AccessPolicyRegistry registry, IServiceProvider services) - { - _prefix = prefix; - _condition = condition; - _expected = expected; - _registry = registry; - _services = services; - } - - private IPolicyScopeBuilder Add() where TPolicy : IAccessPolicy - { - _registry.Add(_prefix, sp => new ConditionalAccessPolicy(_condition, _expected, ActivatorUtilities.CreateInstance(sp))); - return this; - } - - public IPolicyScopeBuilder RequireSelf() - => Add(); - - public IPolicyScopeBuilder RequireAdmin() - => Add(); +namespace CodeBeam.UltimateAuth.Policies; - public IPolicyScopeBuilder RequireSelfOrAdmin() - => Add(); +internal sealed class ConditionalScopeBuilder : IPolicyScopeBuilder +{ + private readonly string _prefix; + private readonly Func _condition; + private readonly bool _expected; + private readonly AccessPolicyRegistry _registry; + private readonly IServiceProvider _services; - public IPolicyScopeBuilder RequireAuthenticated() - => Add(); + public ConditionalScopeBuilder(string prefix, Func condition, bool expected, AccessPolicyRegistry registry, IServiceProvider services) + { + _prefix = prefix; + _condition = condition; + _expected = expected; + _registry = registry; + _services = services; + } - public IPolicyScopeBuilder DenyCrossTenant() - => Add(); + private IPolicyScopeBuilder Add() where TPolicy : IAccessPolicy + { + _registry.Add(_prefix, sp => new ConditionalAccessPolicy(_condition, _expected, ActivatorUtilities.CreateInstance(sp))); + return this; } + + public IPolicyScopeBuilder RequireSelf() => Add(); + public IPolicyScopeBuilder RequireAdmin() => Add(); + public IPolicyScopeBuilder RequireSelfOrAdmin() => Add(); + public IPolicyScopeBuilder RequireAuthenticated() => Add(); + public IPolicyScopeBuilder DenyCrossTenant() => Add(); } diff --git a/src/policies/CodeBeam.UltimateAuth.Policies/Fluent/IConditionalPolicyBuilder.cs b/src/policies/CodeBeam.UltimateAuth.Policies/Fluent/IConditionalPolicyBuilder.cs index 3444934f..64b0ba3c 100644 --- a/src/policies/CodeBeam.UltimateAuth.Policies/Fluent/IConditionalPolicyBuilder.cs +++ b/src/policies/CodeBeam.UltimateAuth.Policies/Fluent/IConditionalPolicyBuilder.cs @@ -1,8 +1,7 @@ -namespace CodeBeam.UltimateAuth.Policies +namespace CodeBeam.UltimateAuth.Policies; + +public interface IConditionalPolicyBuilder { - public interface IConditionalPolicyBuilder - { - IPolicyScopeBuilder Then(); - IPolicyScopeBuilder Otherwise(); - } + IPolicyScopeBuilder Then(); + IPolicyScopeBuilder Otherwise(); } diff --git a/src/policies/CodeBeam.UltimateAuth.Policies/Fluent/IPolicyBuilder.cs b/src/policies/CodeBeam.UltimateAuth.Policies/Fluent/IPolicyBuilder.cs index 22d2eede..d265c7e6 100644 --- a/src/policies/CodeBeam.UltimateAuth.Policies/Fluent/IPolicyBuilder.cs +++ b/src/policies/CodeBeam.UltimateAuth.Policies/Fluent/IPolicyBuilder.cs @@ -1,8 +1,7 @@ -namespace CodeBeam.UltimateAuth.Policies +namespace CodeBeam.UltimateAuth.Policies; + +public interface IPolicyBuilder { - public interface IPolicyBuilder - { - IPolicyScopeBuilder For(string actionPrefix); - IPolicyScopeBuilder Global(); - } + IPolicyScopeBuilder For(string actionPrefix); + IPolicyScopeBuilder Global(); } diff --git a/src/policies/CodeBeam.UltimateAuth.Policies/Fluent/IPolicyScopeBuilder.cs b/src/policies/CodeBeam.UltimateAuth.Policies/Fluent/IPolicyScopeBuilder.cs index 44c2faed..1916eeee 100644 --- a/src/policies/CodeBeam.UltimateAuth.Policies/Fluent/IPolicyScopeBuilder.cs +++ b/src/policies/CodeBeam.UltimateAuth.Policies/Fluent/IPolicyScopeBuilder.cs @@ -1,11 +1,10 @@ -namespace CodeBeam.UltimateAuth.Policies +namespace CodeBeam.UltimateAuth.Policies; + +public interface IPolicyScopeBuilder { - public interface IPolicyScopeBuilder - { - IPolicyScopeBuilder RequireAuthenticated(); - IPolicyScopeBuilder RequireSelf(); - IPolicyScopeBuilder RequireAdmin(); - IPolicyScopeBuilder RequireSelfOrAdmin(); - IPolicyScopeBuilder DenyCrossTenant(); - } + IPolicyScopeBuilder RequireAuthenticated(); + IPolicyScopeBuilder RequireSelf(); + IPolicyScopeBuilder RequireAdmin(); + IPolicyScopeBuilder RequireSelfOrAdmin(); + IPolicyScopeBuilder DenyCrossTenant(); } diff --git a/src/policies/CodeBeam.UltimateAuth.Policies/Fluent/PolicyBuilder.cs b/src/policies/CodeBeam.UltimateAuth.Policies/Fluent/PolicyBuilder.cs index 2ecc488c..4f3f8afc 100644 --- a/src/policies/CodeBeam.UltimateAuth.Policies/Fluent/PolicyBuilder.cs +++ b/src/policies/CodeBeam.UltimateAuth.Policies/Fluent/PolicyBuilder.cs @@ -1,20 +1,19 @@ using CodeBeam.UltimateAuth.Policies.Registry; -namespace CodeBeam.UltimateAuth.Policies +namespace CodeBeam.UltimateAuth.Policies; + +internal sealed class PolicyBuilder : IPolicyBuilder { - internal sealed class PolicyBuilder : IPolicyBuilder - { - private readonly AccessPolicyRegistry _registry; - private readonly IServiceProvider _services; + private readonly AccessPolicyRegistry _registry; + private readonly IServiceProvider _services; - public PolicyBuilder(AccessPolicyRegistry registry, IServiceProvider services) - { - _registry = registry; - _services = services; - } + public PolicyBuilder(AccessPolicyRegistry registry, IServiceProvider services) + { + _registry = registry; + _services = services; + } - public IPolicyScopeBuilder For(string actionPrefix) => new PolicyScopeBuilder(actionPrefix, _registry, _services); + public IPolicyScopeBuilder For(string actionPrefix) => new PolicyScopeBuilder(actionPrefix, _registry, _services); - public IPolicyScopeBuilder Global() => new PolicyScopeBuilder(string.Empty, _registry, _services); - } + public IPolicyScopeBuilder Global() => new PolicyScopeBuilder(string.Empty, _registry, _services); } diff --git a/src/policies/CodeBeam.UltimateAuth.Policies/Fluent/PolicyScopeBuilder.cs b/src/policies/CodeBeam.UltimateAuth.Policies/Fluent/PolicyScopeBuilder.cs index 61c73fb6..61adab98 100644 --- a/src/policies/CodeBeam.UltimateAuth.Policies/Fluent/PolicyScopeBuilder.cs +++ b/src/policies/CodeBeam.UltimateAuth.Policies/Fluent/PolicyScopeBuilder.cs @@ -3,51 +3,35 @@ using CodeBeam.UltimateAuth.Policies.Registry; using Microsoft.Extensions.DependencyInjection; -namespace CodeBeam.UltimateAuth.Policies -{ - internal sealed class PolicyScopeBuilder : IPolicyScopeBuilder - { - private readonly string _prefix; - private readonly AccessPolicyRegistry _registry; - private readonly IServiceProvider _services; - - public PolicyScopeBuilder( - string prefix, - AccessPolicyRegistry registry, - IServiceProvider services) - { - _prefix = prefix; - _registry = registry; - _services = services; - } - - public IPolicyScopeBuilder RequireAuthenticated() - => Add(); - - public IPolicyScopeBuilder RequireSelf() - => Add(); - - public IPolicyScopeBuilder RequireAdmin() - => Add(); - - public IPolicyScopeBuilder RequireSelfOrAdmin() - => Add(); +namespace CodeBeam.UltimateAuth.Policies; - public IPolicyScopeBuilder DenyCrossTenant() - => Add(); - - private IPolicyScopeBuilder Add() - where TPolicy : IAccessPolicy - { - _registry.Add(_prefix, sp => ActivatorUtilities.CreateInstance(sp)); - return this; - } +internal sealed class PolicyScopeBuilder : IPolicyScopeBuilder +{ + private readonly string _prefix; + private readonly AccessPolicyRegistry _registry; + private readonly IServiceProvider _services; - public IConditionalPolicyBuilder When(Func predicate) - { - return new ConditionalPolicyBuilder(_prefix, predicate, _registry, _services); - } + public PolicyScopeBuilder(string prefix, AccessPolicyRegistry registry, IServiceProvider services) + { + _prefix = prefix; + _registry = registry; + _services = services; + } + public IPolicyScopeBuilder RequireAuthenticated() => Add(); + public IPolicyScopeBuilder RequireSelf() => Add(); + public IPolicyScopeBuilder RequireAdmin() => Add(); + public IPolicyScopeBuilder RequireSelfOrAdmin() => Add(); + public IPolicyScopeBuilder DenyCrossTenant() => Add(); + + private IPolicyScopeBuilder Add() where TPolicy : IAccessPolicy + { + _registry.Add(_prefix, sp => ActivatorUtilities.CreateInstance(sp)); + return this; } + public IConditionalPolicyBuilder When(Func predicate) + { + return new ConditionalPolicyBuilder(_prefix, predicate, _registry, _services); + } } diff --git a/src/policies/CodeBeam.UltimateAuth.Policies/Policies/ConditionalAccessPolicy.cs b/src/policies/CodeBeam.UltimateAuth.Policies/Policies/ConditionalAccessPolicy.cs index 696c0f2c..eb7e33bc 100644 --- a/src/policies/CodeBeam.UltimateAuth.Policies/Policies/ConditionalAccessPolicy.cs +++ b/src/policies/CodeBeam.UltimateAuth.Policies/Policies/ConditionalAccessPolicy.cs @@ -1,23 +1,22 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Contracts; -namespace CodeBeam.UltimateAuth.Policies +namespace CodeBeam.UltimateAuth.Policies; + +internal sealed class ConditionalAccessPolicy : IAccessPolicy { - internal sealed class ConditionalAccessPolicy : IAccessPolicy - { - private readonly Func _condition; - private readonly bool _expected; - private readonly IAccessPolicy _inner; + private readonly Func _condition; + private readonly bool _expected; + private readonly IAccessPolicy _inner; - public ConditionalAccessPolicy(Func condition, bool expected, IAccessPolicy inner) - { - _condition = condition; - _expected = expected; - _inner = inner; - } + public ConditionalAccessPolicy(Func condition, bool expected, IAccessPolicy inner) + { + _condition = condition; + _expected = expected; + _inner = inner; + } - public bool AppliesTo(AccessContext context) => _condition(context) == _expected; + public bool AppliesTo(AccessContext context) => _condition(context) == _expected; - public AccessDecision Decide(AccessContext context) => _inner.Decide(context); - } + public AccessDecision Decide(AccessContext context) => _inner.Decide(context); } diff --git a/src/policies/CodeBeam.UltimateAuth.Policies/Policies/RequireActiveUserPolicy.cs b/src/policies/CodeBeam.UltimateAuth.Policies/Policies/RequireActiveUserPolicy.cs index 4ad9c65c..fa9bc520 100644 --- a/src/policies/CodeBeam.UltimateAuth.Policies/Policies/RequireActiveUserPolicy.cs +++ b/src/policies/CodeBeam.UltimateAuth.Policies/Policies/RequireActiveUserPolicy.cs @@ -1,46 +1,45 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Contracts; -namespace CodeBeam.UltimateAuth.Policies +namespace CodeBeam.UltimateAuth.Policies; + +internal sealed class RequireActiveUserPolicy : IAccessPolicy { - internal sealed class RequireActiveUserPolicy : IAccessPolicy + private readonly IUserRuntimeStateProvider _runtime; + + public RequireActiveUserPolicy(IUserRuntimeStateProvider runtime) + { + _runtime = runtime; + } + + public AccessDecision Decide(AccessContext context) + { + if (context.ActorUserKey is null) + return AccessDecision.Deny("missing_actor"); + + var state = _runtime.GetAsync(context.ActorTenant, context.ActorUserKey!.Value).GetAwaiter().GetResult(); + + if (state == null || !state.Exists || state.IsDeleted) + return AccessDecision.Deny("user_not_found"); + + return state.IsActive + ? AccessDecision.Allow() + : AccessDecision.Deny("user_not_active"); + } + + public bool AppliesTo(AccessContext context) { - private readonly IUserRuntimeStateProvider _runtime; - - public RequireActiveUserPolicy(IUserRuntimeStateProvider runtime) - { - _runtime = runtime; - } - - public AccessDecision Decide(AccessContext context) - { - if (context.ActorUserKey is null) - return AccessDecision.Deny("missing_actor"); - - var state = _runtime.GetAsync(context.ActorTenant, context.ActorUserKey!.Value).GetAwaiter().GetResult(); - - if (state == null || !state.Exists || state.IsDeleted) - return AccessDecision.Deny("user_not_found"); - - return state.IsActive - ? AccessDecision.Allow() - : AccessDecision.Deny("user_not_active"); - } - - public bool AppliesTo(AccessContext context) - { - if (!context.IsAuthenticated || context.IsSystemActor) - return false; - - return !AllowedForInactive.Any(prefix => context.Action.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)); - } - - private static readonly string[] AllowedForInactive = - { - "users.status.change.", - "credentials.password.reset.", - "login.", - "reauth." - }; + if (!context.IsAuthenticated || context.IsSystemActor) + return false; + + return !AllowedForInactive.Any(prefix => context.Action.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)); } + + private static readonly string[] AllowedForInactive = + { + "users.status.change.", + "credentials.password.reset.", + "login.", + "reauth." + }; } diff --git a/src/policies/CodeBeam.UltimateAuth.Policies/Policies/RequireSystemPolicy.cs b/src/policies/CodeBeam.UltimateAuth.Policies/Policies/RequireSystemPolicy.cs index 0524b71f..8f2740ab 100644 --- a/src/policies/CodeBeam.UltimateAuth.Policies/Policies/RequireSystemPolicy.cs +++ b/src/policies/CodeBeam.UltimateAuth.Policies/Policies/RequireSystemPolicy.cs @@ -1,15 +1,14 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Contracts; -namespace CodeBeam.UltimateAuth.Policies +namespace CodeBeam.UltimateAuth.Policies; + +internal sealed class RequireSystemPolicy : IAccessPolicy { - internal sealed class RequireSystemPolicy : IAccessPolicy - { - public AccessDecision Decide(AccessContext context) - => context.IsSystemActor - ? AccessDecision.Allow() - : AccessDecision.Deny("system_actor_required"); + public AccessDecision Decide(AccessContext context) + => context.IsSystemActor + ? AccessDecision.Allow() + : AccessDecision.Deny("system_actor_required"); - public bool AppliesTo(AccessContext context) => context.Action.EndsWith(".system", StringComparison.Ordinal); - } + public bool AppliesTo(AccessContext context) => context.Action.EndsWith(".system", StringComparison.Ordinal); } diff --git a/src/security/CodeBeam.UltimateAuth.Security.Argon2/CodeBeam.UltimateAuth.Security.Argon2.csproj b/src/security/CodeBeam.UltimateAuth.Security.Argon2/CodeBeam.UltimateAuth.Security.Argon2.csproj index a3e1cf08..f29fc2e2 100644 --- a/src/security/CodeBeam.UltimateAuth.Security.Argon2/CodeBeam.UltimateAuth.Security.Argon2.csproj +++ b/src/security/CodeBeam.UltimateAuth.Security.Argon2/CodeBeam.UltimateAuth.Security.Argon2.csproj @@ -6,6 +6,7 @@ enable 0.0.1-preview true + $(NoWarn);1591 diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Data/UAuthSessionDbContext.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Data/UAuthSessionDbContext.cs index 7a8e77fc..16666c7b 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Data/UAuthSessionDbContext.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Data/UAuthSessionDbContext.cs @@ -2,122 +2,121 @@ using CodeBeam.UltimateAuth.Core.MultiTenancy; using Microsoft.EntityFrameworkCore; -namespace CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore -{ - internal sealed class UltimateAuthSessionDbContext : DbContext - { - public DbSet Roots => Set(); - public DbSet Chains => Set(); - public DbSet Sessions => Set(); - - - private readonly TenantContext _tenant; - - public UltimateAuthSessionDbContext(DbContextOptions options, TenantContext tenant) : base(options) - { - _tenant = tenant; - } - - protected override void OnModelCreating(ModelBuilder b) - { - b.Entity() - .HasQueryFilter(x => _tenant.IsGlobal || x.TenantId == _tenant.TenantId); - - b.Entity() - .HasQueryFilter(x => _tenant.IsGlobal || x.TenantId == _tenant.TenantId); +namespace CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore; - b.Entity() - .HasQueryFilter(x => _tenant.IsGlobal || x.TenantId == _tenant.TenantId); - - b.Entity(e => - { - e.HasKey(x => x.Id); +internal sealed class UltimateAuthSessionDbContext : DbContext +{ + public DbSet Roots => Set(); + public DbSet Chains => Set(); + public DbSet Sessions => Set(); - e.Property(x => x.RowVersion) - .IsRowVersion(); - e.Property(x => x.UserKey) - .IsRequired(); + private readonly TenantContext _tenant; - e.HasIndex(x => new { x.TenantId, x.UserKey }) - .IsUnique(); + public UltimateAuthSessionDbContext(DbContextOptions options, TenantContext tenant) : base(options) + { + _tenant = tenant; + } - e.Property(x => x.SecurityVersion) - .IsRequired(); + protected override void OnModelCreating(ModelBuilder b) + { + b.Entity() + .HasQueryFilter(x => _tenant.IsGlobal || x.Tenant == _tenant.Tenant); - e.Property(x => x.LastUpdatedAt) - .IsRequired(); + b.Entity() + .HasQueryFilter(x => _tenant.IsGlobal || x.Tenant == _tenant.Tenant); - e.Property(x => x.RootId) - .HasConversion( - v => v.Value, - v => SessionRootId.From(v)) - .IsRequired(); + b.Entity() + .HasQueryFilter(x => _tenant.IsGlobal || x.Tenant == _tenant.Tenant); - e.HasIndex(x => new { x.TenantId, x.RootId }); + b.Entity(e => + { + e.HasKey(x => x.Id); - }); + e.Property(x => x.RowVersion) + .IsRowVersion(); - b.Entity(e => - { - e.HasKey(x => x.Id); + e.Property(x => x.UserKey) + .IsRequired(); - e.Property(x => x.RowVersion) - .IsRowVersion(); + e.HasIndex(x => new { x.Tenant, x.UserKey }) + .IsUnique(); - e.Property(x => x.UserKey) - .IsRequired(); + e.Property(x => x.SecurityVersion) + .IsRequired(); - e.HasIndex(x => new { x.TenantId, x.ChainId }).IsUnique(); + e.Property(x => x.LastUpdatedAt) + .IsRequired(); - e.Property(x => x.ChainId) - .HasConversion( - v => v.Value, - v => SessionChainId.From(v)) - .IsRequired(); + e.Property(x => x.RootId) + .HasConversion( + v => v.Value, + v => SessionRootId.From(v)) + .IsRequired(); - e.Property(x => x.ActiveSessionId) - .HasConversion(new NullableAuthSessionIdConverter()); + e.HasIndex(x => new { x.Tenant, x.RootId }); - e.Property(x => x.ClaimsSnapshot) - .HasConversion(new JsonValueConverter()) - .IsRequired(); + }); - e.Property(x => x.SecurityVersionAtCreation) - .IsRequired(); - }); + b.Entity(e => + { + e.HasKey(x => x.Id); - b.Entity(e => - { - e.HasKey(x => x.Id); - e.Property(x => x.RowVersion).IsRowVersion(); + e.Property(x => x.RowVersion) + .IsRowVersion(); - e.HasIndex(x => new { x.TenantId, x.SessionId }).IsUnique(); - e.HasIndex(x => new { x.TenantId, x.ChainId, x.RevokedAt }); + e.Property(x => x.UserKey) + .IsRequired(); - e.Property(x => x.SessionId) - .HasConversion(new AuthSessionIdConverter()) - .IsRequired(); + e.HasIndex(x => new { x.Tenant, x.ChainId }).IsUnique(); - e.Property(x => x.ChainId) - .HasConversion( - v => v.Value, - v => SessionChainId.From(v)) - .IsRequired(); + e.Property(x => x.ChainId) + .HasConversion( + v => v.Value, + v => SessionChainId.From(v)) + .IsRequired(); - e.Property(x => x.Device) - .HasConversion(new JsonValueConverter()) - .IsRequired(); + e.Property(x => x.ActiveSessionId) + .HasConversion(new NullableAuthSessionIdConverter()); - e.Property(x => x.Claims) - .HasConversion(new JsonValueConverter()) - .IsRequired(); + e.Property(x => x.ClaimsSnapshot) + .HasConversion(new JsonValueConverter()) + .IsRequired(); - e.Property(x => x.Metadata) - .HasConversion(new JsonValueConverter()) - .IsRequired(); - }); - } + e.Property(x => x.SecurityVersionAtCreation) + .IsRequired(); + }); + b.Entity(e => + { + e.HasKey(x => x.Id); + e.Property(x => x.RowVersion).IsRowVersion(); + + e.HasIndex(x => new { x.Tenant, x.SessionId }).IsUnique(); + e.HasIndex(x => new { x.Tenant, x.ChainId, x.RevokedAt }); + + e.Property(x => x.SessionId) + .HasConversion(new AuthSessionIdConverter()) + .IsRequired(); + + e.Property(x => x.ChainId) + .HasConversion( + v => v.Value, + v => SessionChainId.From(v)) + .IsRequired(); + + e.Property(x => x.Device) + .HasConversion(new JsonValueConverter()) + .IsRequired(); + + e.Property(x => x.Claims) + .HasConversion(new JsonValueConverter()) + .IsRequired(); + + e.Property(x => x.Metadata) + .HasConversion(new JsonValueConverter()) + .IsRequired(); + }); } + } diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionChainProjectionMapper.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionChainProjectionMapper.cs index f93d05a7..a4cc4951 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionChainProjectionMapper.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionChainProjectionMapper.cs @@ -9,7 +9,7 @@ public static UAuthSessionChain ToDomain(this SessionChainProjection p) return UAuthSessionChain.FromProjection( p.ChainId, p.RootId, - p.TenantId, + p.Tenant, p.UserKey, p.RotationCount, p.SecurityVersionAtCreation, @@ -25,7 +25,7 @@ public static SessionChainProjection ToProjection(this UAuthSessionChain chain) return new SessionChainProjection { ChainId = chain.ChainId, - TenantId = chain.TenantId, + Tenant = chain.Tenant, UserKey = chain.UserKey, RotationCount = chain.RotationCount, diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionProjectionMapper.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionProjectionMapper.cs index 692aea12..0b5f8c43 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionProjectionMapper.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionProjectionMapper.cs @@ -8,7 +8,7 @@ public static UAuthSession ToDomain(this SessionProjection p) { return UAuthSession.FromProjection( p.SessionId, - p.TenantId, + p.Tenant, p.UserKey, p.ChainId, p.CreatedAt, @@ -28,7 +28,7 @@ public static SessionProjection ToProjection(this UAuthSession s) return new SessionProjection { SessionId = s.SessionId, - TenantId = s.TenantId, + Tenant = s.Tenant, UserKey = s.UserKey, ChainId = s.ChainId, diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionRootProjectionMapper.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionRootProjectionMapper.cs index a28fde46..3cd3666d 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionRootProjectionMapper.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionRootProjectionMapper.cs @@ -8,7 +8,7 @@ public static UAuthSessionRoot ToDomain(this SessionRootProjection root, IReadOn { return UAuthSessionRoot.FromProjection( root.RootId, - root.TenantId, + root.Tenant, root.UserKey, root.IsRevoked, root.RevokedAt, @@ -23,7 +23,7 @@ public static SessionRootProjection ToProjection(this UAuthSessionRoot root) return new SessionRootProjection { RootId = root.RootId, - TenantId = root.TenantId, + Tenant = root.Tenant, UserKey = root.UserKey, IsRevoked = root.IsRevoked, diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Stores/EfCoreSessionStoreKernelFactory.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Stores/EfCoreSessionStoreKernelFactory.cs index 1c3cda87..240b9b9d 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Stores/EfCoreSessionStoreKernelFactory.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Stores/EfCoreSessionStoreKernelFactory.cs @@ -18,8 +18,9 @@ public ISessionStoreKernel Create(TenantKey tenant) return ActivatorUtilities.CreateInstance(_sp, new TenantContext(tenant)); } - public ISessionStoreKernel CreateGlobal() - { - return ActivatorUtilities.CreateInstance(_sp, new TenantContext(null, isGlobal: true)); - } + // TODO: Implement global here + //public ISessionStoreKernel CreateGlobal() + //{ + // return ActivatorUtilities.CreateInstance(_sp, new TenantContext(null, isGlobal: true)); + //} } diff --git a/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/UAuthTokenDbContext.cs b/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/UAuthTokenDbContext.cs index 7b958e2d..be84d6e1 100644 --- a/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/UAuthTokenDbContext.cs +++ b/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/UAuthTokenDbContext.cs @@ -24,14 +24,14 @@ protected override void OnModelCreating(ModelBuilder b) e.Property(x => x.TokenHash) .IsRequired(); - e.HasIndex(x => new { x.TenantId, x.TokenHash }) + e.HasIndex(x => new { x.Tenant, x.TokenHash }) .IsUnique(); - e.HasIndex(x => new { x.TenantId, x.UserKey }); - e.HasIndex(x => new { x.TenantId, x.SessionId }); - e.HasIndex(x => new { x.TenantId, x.ChainId }); - e.HasIndex(x => new { x.TenantId, x.ExpiresAt }); - e.HasIndex(x => new { x.TenantId, x.ReplacedByTokenHash }); + e.HasIndex(x => new { x.Tenant, x.UserKey }); + e.HasIndex(x => new { x.Tenant, x.SessionId }); + e.HasIndex(x => new { x.Tenant, x.ChainId }); + e.HasIndex(x => new { x.Tenant, x.ExpiresAt }); + e.HasIndex(x => new { x.Tenant, x.ReplacedByTokenHash }); e.Property(x => x.ExpiresAt).IsRequired(); }); @@ -49,7 +49,7 @@ protected override void OnModelCreating(ModelBuilder b) e.HasIndex(x => x.Jti) .IsUnique(); - e.HasIndex(x => new { x.TenantId, x.Jti }); + e.HasIndex(x => new { x.Tenant, x.Jti }); e.Property(x => x.ExpiresAt) .IsRequired(); diff --git a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserStatus.cs b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserStatus.cs index f02ebfcb..9aff5b20 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserStatus.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserStatus.cs @@ -1,21 +1,20 @@ -namespace CodeBeam.UltimateAuth.Users.Contracts +namespace CodeBeam.UltimateAuth.Users.Contracts; + +public enum UserStatus { - public enum UserStatus - { - Active = 0, + Active = 0, - SelfSuspended = 10, + SelfSuspended = 10, - Disabled = 20, - Suspended = 30, + Disabled = 20, + Suspended = 30, - Locked = 40, - RiskHold = 50, + Locked = 40, + RiskHold = 50, - PendingActivation = 60, - PendingVerification = 70, + PendingActivation = 60, + PendingVerification = 70, - Deactivated = 80, - } + Deactivated = 80, } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Core/RefreshTokenValidatorTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Core/RefreshTokenValidatorTests.cs index ad919a4e..9111c5cb 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Core/RefreshTokenValidatorTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Core/RefreshTokenValidatorTests.cs @@ -2,6 +2,7 @@ using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Core.Infrastructure; +using CodeBeam.UltimateAuth.Core.MultiTenancy; using CodeBeam.UltimateAuth.Server.Infrastructure; using CodeBeam.UltimateAuth.Tokens.InMemory; using System.Text; @@ -31,7 +32,7 @@ public async Task Invalid_When_Token_Not_Found() var result = await validator.ValidateAsync( new RefreshTokenValidationContext { - TenantId = null, + Tenant = TenantKey.Single, RefreshToken = "non-existing", Now = DateTimeOffset.UtcNow, Device = DeviceContext.FromDeviceId(DeviceId.Create(ValidDeviceId)), @@ -53,9 +54,9 @@ public async Task Reuse_Detected_When_Token_is_Revoked() var rawToken = "refresh-token-1"; var hash = hasher.Hash(rawToken); - await store.StoreAsync(null, new StoredRefreshToken + await store.StoreAsync(TenantKey.Single, new StoredRefreshToken { - TenantId = null, + Tenant = TenantKey.Single, TokenHash = hash, UserKey = UserKey.FromString("user-1"), SessionId = TestIds.Session("session-1-aaaaaaaaaaaaaaaaaaaaaa"), @@ -68,7 +69,7 @@ public async Task Reuse_Detected_When_Token_is_Revoked() var result = await validator.ValidateAsync( new RefreshTokenValidationContext { - TenantId = null, + Tenant = TenantKey.Single, RefreshToken = rawToken, Now = now, Device = DeviceContext.FromDeviceId(DeviceId.Create(ValidDeviceId)), @@ -86,9 +87,9 @@ public async Task Invalid_When_Expected_Session_Id_Does_Not_Match() var now = DateTimeOffset.UtcNow; - await store.StoreAsync(null, new StoredRefreshToken + await store.StoreAsync(TenantKey.Single, new StoredRefreshToken { - TenantId = null, + Tenant = TenantKey.Single, TokenHash = "hash-2", UserKey = UserKey.FromString("user-1"), SessionId = TestIds.Session("session-1-bbbbbbbbbbbbbbbbbbbbbb"), @@ -100,7 +101,7 @@ public async Task Invalid_When_Expected_Session_Id_Does_Not_Match() var result = await validator.ValidateAsync( new RefreshTokenValidationContext { - TenantId = null, + Tenant = TenantKey.Single, RefreshToken = "hash-2", ExpectedSessionId = TestIds.Session("session-2-cccccccccccccccccccccc"), Now = now, diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Core/UAuthSessionChainTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Core/UAuthSessionChainTests.cs index 605ff14e..8c010928 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Core/UAuthSessionChainTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Core/UAuthSessionChainTests.cs @@ -1,4 +1,5 @@ using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; namespace CodeBeam.UltimateAuth.Tests.Unit; @@ -17,7 +18,7 @@ public void New_chain_has_expected_initial_state() var chain = UAuthSessionChain.Create( SessionChainId.New(), SessionRootId.New(), - tenantId: null, + tenant: TenantKey.Single, userKey: UserKey.FromString("user-1"), securityVersion: 0, ClaimsSnapshot.Empty); @@ -33,7 +34,7 @@ public void Rotating_chain_sets_active_session_and_increments_rotation() var chain = UAuthSessionChain.Create( SessionChainId.New(), SessionRootId.New(), - null, + TenantKey.Single, UserKey.FromString("user-1"), 0, ClaimsSnapshot.Empty); @@ -52,7 +53,7 @@ public void Multiple_rotations_increment_rotation_count() var chain = UAuthSessionChain.Create( SessionChainId.New(), SessionRootId.New(), - null, + TenantKey.Single, UserKey.FromString("user-1"), 0, ClaimsSnapshot.Empty); @@ -72,7 +73,7 @@ public void Revoked_chain_does_not_rotate() var chain = UAuthSessionChain.Create( SessionChainId.New(), SessionRootId.New(), - null, + TenantKey.Single, UserKey.FromString("user-1"), 0, ClaimsSnapshot.Empty); @@ -92,7 +93,7 @@ public void Revoking_chain_sets_revocation_fields() var chain = UAuthSessionChain.Create( SessionChainId.New(), SessionRootId.New(), - null, + TenantKey.Single, UserKey.FromString("user-1"), 0, ClaimsSnapshot.Empty); @@ -111,7 +112,7 @@ public void Revoking_already_revoked_chain_is_idempotent() var chain = UAuthSessionChain.Create( SessionChainId.New(), SessionRootId.New(), - null, + TenantKey.Single, UserKey.FromString("user-1"), 0, ClaimsSnapshot.Empty); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Core/UAuthSessionTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Core/UAuthSessionTests.cs index 7548b28d..049f6233 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Core/UAuthSessionTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Core/UAuthSessionTests.cs @@ -1,4 +1,5 @@ using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; using Xunit; namespace CodeBeam.UltimateAuth.Tests.Unit; @@ -16,7 +17,7 @@ public void Revoke_marks_session_as_revoked() var session = UAuthSession.Create( sessionId: sessionId, - tenantId: null, + tenant: TenantKey.Single, userKey: UserKey.FromString("user-1"), chainId: SessionChainId.New(), now, @@ -40,7 +41,7 @@ public void Revoking_twice_returns_same_instance() var session = UAuthSession.Create( sessionId, - null, + TenantKey.Single, UserKey.FromString("user-1"), SessionChainId.New(), now, From 769ad20d9478ef7d2f564c091e352fe2b19f682f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Can=20Karag=C3=B6z?= Date: Tue, 3 Feb 2026 22:10:11 +0300 Subject: [PATCH 8/9] Main Refactoring --- ...thStateManager.cs => UAuthStateManager.cs} | 4 +- ...Generator.cs => UAuthDeviceIdGenerator.cs} | 2 +- ...IdProvider.cs => UAuthDeviceIdProvider.cs} | 4 +- .../Extensions/ServiceCollectionExtensions.cs | 16 +- ...nClient.cs => UAuthAuthorizationClient.cs} | 4 +- ...tialClient.cs => UAuthCredentialClient.cs} | 4 +- ...efaultFlowClient.cs => UAuthFlowClient.cs} | 7 +- ...efaultUserClient.cs => UAuthUserClient.cs} | 4 +- ...Client.cs => UAuthUserIdentifierClient.cs} | 4 +- .../Contracts/Logout/LogoutResponse.cs | 6 + ...ions.cs => ServiceCollectionExtensions.cs} | 2 +- ...faultAuthAuthority.cs => AuthAuthority.cs} | 4 +- ...dator.cs => UAuthRefreshTokenValidator.cs} | 4 +- .../Abstractions/ICredentialResponseWriter.cs | 13 +- .../Abstractions/IDeviceResolver.cs | 15 +- .../IPrimaryCredentialResolver.cs | 9 +- .../Abstractions/IRefreshTokenResolver.cs | 17 +- .../Abstractions/ISigningKeyProvider.cs | 19 +- .../Abstractions/ITokenIssuer.cs | 19 +- .../Abstractions/ResolvedCredential.cs | 20 - .../Auth/Accessor/AuthFlowContextAccessor.cs | 35 ++ .../DefaultAuthFlowContextAccessor.cs | 47 --- .../Auth/Accessor/IAuthFlowContextAccessor.cs | 10 +- .../Auth/ClientProfileReader.cs | 32 ++ ...textFactory.cs => AccessContextFactory.cs} | 4 +- .../Auth/Context/AuthContextFactory.cs | 23 ++ .../Auth/Context/AuthExecutionContext.cs | 9 +- .../Auth/Context/AuthFlow.cs | 28 ++ .../Auth/Context/AuthFlowContext.cs | 99 +++-- .../Auth/Context/AuthFlowContextFactory.cs | 154 ++++--- .../Auth/Context/AuthFlowEndpointFilter.cs | 30 +- .../Auth/Context/AuthFlowMetadata.cs | 15 +- .../Auth/Context/DefaultAuthContextFactory.cs | 24 -- .../Auth/Context/DefaultAuthFlow.cs | 30 -- .../Auth/Context/IAuthFlow.cs | 9 +- .../Auth/Context/IAuthFlowContextFactory.cs | 9 + .../Auth/DefaultClientProfileReader.cs | 33 -- .../DefaultEffectiveServerOptionsProvider.cs | 47 --- .../Auth/DefaultPrimaryTokenResolver.cs | 21 - .../Auth/EffectiveServerOptionsProvider.cs | 46 +++ .../Auth/EffectiveUAuthServerOptions.cs | 19 +- .../Auth/IClientProfileReader.cs | 9 +- .../Auth/IPrimaryTokenResolver.cs | 9 +- .../Auth/PrimaryTokenResolver.cs | 20 + ...AuthResponseOptionsModeTemplateResolver.cs | 233 ++++++----- .../Auth/Response/AuthResponseResolver.cs | 93 +++++ .../ClientProfileAuthResponseAdapter.cs | 73 ++-- .../Response/DefaultAuthResponseResolver.cs | 94 ----- .../DefaultEffectiveAuthModeResolver.cs | 26 -- .../Response/EffectiveAuthModeResolver.cs | 24 ++ .../Auth/Response/EffectiveAuthResponse.cs | 19 +- .../EffectiveLoginRedirectResponse.cs | 23 +- .../EffectiveLogoutRedirectResponse.cs | 17 +- .../Auth/Response/IAuthResponseResolver.cs | 9 +- .../Response/IEffectiveAuthModeResolver.cs | 9 +- .../Contracts/JwtSigningKey.cs | 10 + .../Contracts/LogoutResponse.cs | 7 - .../Contracts/RefreshTokenStatus.cs | 19 +- .../Contracts/ResolvedCredential.cs | 19 + .../Contracts/SessionRefreshResult.cs | 28 +- .../Cookies/IUAuthCookiePolicyBuilder.cs | 1 - ...CookieManager.cs => UAuthCookieManager.cs} | 2 +- ...Builder.cs => UAuthCookiePolicyBuilder.cs} | 2 +- .../Defaults/UAuthActions.cs | 118 +++--- .../IAuthorizationEndpointHandler.cs | 17 +- .../Abstractions/ILoginEndpointHandler.cs | 9 +- .../Abstractions/ILogoutEndpointHandler.cs | 9 +- .../Abstractions/IPkceEndpointHandler.cs | 31 +- .../Abstractions/IReauthEndpointHandler.cs | 9 +- .../Abstractions/IRefreshEndpointHandler.cs | 9 +- .../Abstractions/ISessionManagementHandler.cs | 15 +- .../Abstractions/ITokenEndpointHandler.cs | 15 +- .../Abstractions/IUAuthEndpointRegistrar.cs | 9 + .../Abstractions/IUserInfoEndpointHandler.cs | 13 +- .../Abstractions/IValidateEndpointHandler.cs | 9 +- .../Bridges/LoginEndpointHandlerBridge.cs | 16 + .../Bridges/LogoutEndpointHandlerBridge.cs | 17 + .../Bridges/PkceEndpointHandlerBridge.cs | 18 + .../Bridges/RefreshEndpointHandlerBridge.cs | 15 + .../Bridges/ValidateEndpointHandlerBridge.cs | 15 + .../Endpoints/DefaultLogoutEndpointHandler.cs | 75 ---- .../DefaultRefreshEndpointHandler.cs | 91 ----- ...ointHandler.cs => LoginEndpointHandler.cs} | 4 +- .../Endpoints/LoginEndpointHandlerBridge.cs | 17 - .../Endpoints/LogoutEndpointHandler.cs | 73 ++++ .../Endpoints/LogoutEndpointHandlerBridge.cs | 18 - ...pointHandler.cs => PkceEndpointHandler.cs} | 5 +- .../Endpoints/PkceEndpointHandlerBridge.cs | 19 - .../Endpoints/RefreshEndpointHandler.cs | 90 +++++ .../Endpoints/RefreshEndpointHandlerBridge.cs | 16 - .../Endpoints/UAuthEndpointDefaults.cs | 15 - .../Endpoints/UAuthEndpointRegistrar.cs | 6 - ...tHandler.cs => ValidateEndpointHandler.cs} | 4 +- .../ValidateEndpointHandlerBridge.cs | 17 - .../Extensions/AuthFlowContextExtensions.cs | 58 ++- .../Extensions/AuthFlowTypeExtensions.cs | 9 +- .../Extensions/ClaimsSnapshotExtensions.cs | 14 +- .../Extensions/DeviceExtensions.cs | 15 +- .../EndpointRouteBuilderExtensions.cs | 28 +- .../Extensions/HttpContextJsonExtensions.cs | 29 +- .../HttpContextSessionExtensions.cs | 19 +- .../Extensions/HttpContextUserExtensions.cs | 17 +- .../Extensions/ServiceCollectionExtensions.cs | 307 ++++++++++++++ .../TenantResolutionContextExtensions.cs | 25 -- .../UAuthApplicationBuilderExtensions.cs | 18 +- .../UAuthServerOptionsExtensions.cs | 11 +- .../UAuthServerServiceCollectionExtensions.cs | 344 ---------------- .../Flows/Login/ILoginAuthority.cs | 16 + .../Flows/Login/ILoginOrchestrator.cs | 15 + .../Flows/Login/LoginAuthority.cs | 33 ++ .../Flows/Login/LoginDecision.cs | 25 ++ .../Flows/Login/LoginDecisionContext.cs | 50 +++ .../Flows/Login/LoginDecisionKind.cs | 8 + .../Flows/Login/LoginOrchestrator.cs | 167 ++++++++ .../Pkce/IPkceAuthorizationValidator.cs | 2 +- .../Pkce/PkceAuthorizationArtifact.cs | 2 +- .../Pkce/PkceAuthorizationValidator.cs | 2 +- .../Pkce/PkceAuthorizeRequest.cs | 2 +- .../Flows/Pkce/PkceChallengeMethod.cs | 6 + .../Pkce/PkceContextSnapshot.cs | 2 +- .../Pkce/PkceValidationFailureReason.cs | 2 +- .../Pkce/PkceValidationResult.cs | 2 +- .../Flows/Refresh/IRefreshResponsePolicy.cs | 11 + .../Flows/Refresh/IRefreshResponseWriter.cs | 9 + .../Flows/Refresh/IRefreshService.cs | 9 + .../Flows/Refresh/ISessionTouchService.cs | 12 + .../Flows/Refresh/RefreshDecision.cs | 29 ++ .../Flows/Refresh/RefreshDecisionResolver.cs | 24 ++ .../Flows/Refresh/RefreshEvaluationResult.cs | 5 + .../Flows/Refresh/RefreshResponsePolicy.cs | 44 ++ .../Flows/Refresh/RefreshResponseWriter.cs | 31 ++ .../Flows/Refresh/RefreshStrategyResolver.cs | 20 + .../Flows/Refresh/RefreshTokenResolver.cs | 40 ++ .../Flows/Refresh/SessionTouchPolicy.cs | 6 + .../Refresh/SessionTouchService.cs} | 6 +- ...icyProvider.cs => AccessPolicyProvider.cs} | 4 +- .../Infrastructure/AuthRedirectResolver.cs | 86 ++-- .../ITransportCredentialResolver.cs | 9 +- .../AspNetCore/TransportCredentialKind.cs | 15 +- ...lver.cs => TransportCredentialResolver.cs} | 26 +- ...eWriter.cs => CredentialResponseWriter.cs} | 7 +- .../DefaultFlowCredentialResolver.cs | 91 ----- .../DefaultPrimaryCredentialResolver.cs | 39 -- .../DefaultUAuthBodyPolicyBuilder.cs | 13 - .../DefaultUAuthHeaderPolicyBuilder.cs | 23 -- .../Credentials/FlowCredentialResolver.cs | 91 +++++ .../Credentials/IFlowCredentialResolver.cs | 21 +- .../Credentials/PrimaryCredentialResolver.cs | 37 ++ .../Credentials/UAuthBodyPolicyBuilder.cs | 12 + .../Credentials/UAuthHeaderPolicyBuilder.cs | 21 + .../DefaultJwtTokenGenerator.cs | 69 ---- .../DefaultOpaqueTokenGenerator.cs | 11 - .../DevelopmentJwtSigningKeyProvider.cs | 40 +- .../Device/DefaultDeviceContextFactory.cs | 17 - .../Device/DefaultDeviceResolver.cs | 58 --- .../Device/DeviceContextFactory.cs | 15 + .../Infrastructure/Device/DeviceResolver.cs | 57 +++ .../Device/IDeviceContextFactory.cs | 9 +- .../Infrastructure/HmacSha256TokenHasher.cs | 48 ++- .../Hub/DefaultHubCredentialResolver.cs | 40 -- .../Hub/DefaultHubFlowReader.cs | 41 -- .../Hub/HubCredentialResolver.cs | 39 ++ .../Infrastructure/Hub/HubFlowReader.cs | 39 ++ .../Infrastructure/HubCapabilities.cs | 9 +- .../Issuers/UAuthSessionIssuer.cs | 2 +- .../Issuers/UAuthTokenIssuer.cs | 143 +++++++ .../Infrastructure/JwtTokenGenerator.cs | 66 +++ .../Infrastructure/OpaqueTokenGenerator.cs | 8 + .../Orchestrator/CreateLoginSessionCommand.cs | 11 +- .../Orchestrator/DefaultAccessAuthority.cs | 60 --- .../Orchestrator/IAccessCommand.cs | 20 +- .../Orchestrator/IAccessOrchestrator.cs | 11 +- .../Orchestrator/ISessionCommand.cs | 9 +- .../Orchestrator/ISessionOrchestrator.cs | 10 +- .../Orchestrator/RevokeAllChainsCommand.cs | 29 +- .../Orchestrator/RevokeAllSessionsCommand.cs | 29 +- .../Orchestrator/RevokeChainCommand.cs | 25 +- .../Orchestrator/RevokeRootCommand.cs | 25 +- .../Orchestrator/RotateSessionCommand.cs | 11 +- .../Orchestrator/UAuthAccessAuthority.cs | 59 +++ .../Orchestrator/UAuthAccessOrchestrator.cs | 63 ++- .../Orchestrator/UAuthSessionOrchestrator.cs | 55 ++- .../Pkce/PkceChallengeMethod.cs | 6 - .../Refresh/DefaultRefreshResponsePolicy.cs | 45 --- .../Refresh/DefaultRefreshResponseWriter.cs | 32 -- .../Refresh/DefaultRefreshTokenResolver.cs | 41 -- .../Refresh/IRefreshResponsePolicy.cs | 12 - .../Refresh/IRefreshResponseWriter.cs | 10 - .../Infrastructure/Refresh/IRefreshService.cs | 10 - .../Refresh/ISessionTouchService.cs | 13 - .../Infrastructure/Refresh/RefreshDecision.cs | 30 -- .../Refresh/RefreshDecisionResolver.cs | 24 -- .../Refresh/RefreshEvaluationResult.cs | 6 - .../Refresh/RefreshStrategyResolver.cs | 21 - .../Refresh/SessionTouchPolicy.cs | 7 - .../Session/DefaultSessionContextAccessor.cs | 30 -- .../Session/ISessionContextAccessor.cs | 15 +- .../Session/SessionContextAccessor.cs | 29 ++ .../Session/SessionContextItemKeys.cs | 9 +- .../Session/SessionValidationMapper.cs | 38 +- .../SessionId/BearerSessionIdResolver.cs | 36 +- .../SessionId/CompositeSessionIdResolver.cs | 33 +- .../SessionId/CookieSessionIdResolver.cs | 39 +- .../SessionId/HeaderSessionIdResolver.cs | 41 +- .../SessionId/IInnerSessionIdResolver.cs | 11 +- .../SessionId/ISessionIdResolver.cs | 9 +- .../SessionId/QuerySessionIdResolver.cs | 41 +- .../Infrastructure/SystemClock.cs | 9 +- .../User/HttpContextCurrentUser.cs | 24 +- .../Infrastructure/User/IUserAccessor.cs | 17 +- .../Infrastructure/User/UAuthUserId.cs | 15 +- .../Infrastructure/User/UserAccessorBridge.cs | 27 +- .../Issuers/UAuthTokenIssuer.cs | 145 ------- .../Login/DefaultLoginAuthority.cs | 34 -- .../Login/DefaultLoginOrchestrator.cs | 169 -------- .../Login/ILoginAuthority.cs | 17 - .../Login/ILoginOrchestrator.cs | 16 - .../Login/LoginDecision.cs | 26 -- .../Login/LoginDecisionContext.cs | 51 --- .../Login/LoginDecisionKind.cs | 9 - .../SessionResolutionMiddleware.cs | 39 +- .../Middlewares/UserMiddleware.cs | 29 +- .../TenantResolutionContextFactory.cs | 38 +- .../MultiTenancy/UAuthTenantContextFactory.cs | 1 - .../Options/AuthResponseOptions.cs | 33 +- .../Options/CredentialResponseOptions.cs | 79 ++-- .../Options/Defaults/ConfigureDefaults.cs | 223 ++++++----- .../IEffectiveServerOptionsProvider.cs | 11 +- .../Options/LoginRedirectOptions.cs | 38 +- .../Options/LogoutRedirectOptions.cs | 43 +- .../Options/PrimaryCredentialPolicy.cs | 34 +- .../Options/UAuthCookieLifetimeOptions.cs | 37 +- .../Options/UAuthCookieSetOptions.cs | 61 ++- .../Options/UAuthDiagnosticsOptions.cs | 25 +- .../Options/UAuthHubServerOptions.cs | 37 +- .../Options/UAuthServerOptions.cs | 321 ++++++++------- .../Options/UAuthServerOptionsValidator.cs | 64 ++- .../Options/UAuthSessionResolutionOptions.cs | 61 ++- .../Options/UserIdentifierOptions.cs | 46 +-- .../ProductInfo/UAuthServerProductInfo.cs | 21 +- .../Services/DefaultRefreshFlowService.cs | 238 ----------- .../Services/IRefreshFlowService.cs | 9 +- .../Services/IRefreshTokenRotationService.cs | 9 +- .../Services/ISessionQueryService.cs | 3 +- .../Services/IUAuthFlowService.cs | 31 +- .../Services/RefreshFlowService.cs | 207 ++++++++++ .../Services/RefreshTokenRotationService.cs | 1 - .../Services/UAuthFlowService.cs | 152 ++++--- .../Services/UAuthSessionManager.cs | 1 - .../Stores/AspNetIdentityUserStore.cs | 56 ++- .../Requests/AssignRoleRequest.cs | 9 +- .../Requests/AuthorizationCheckRequest.cs | 13 +- .../Responses/UserRolesResponse.cs | 12 +- .../AuthorizationInMemoryExtensions.cs | 19 - .../Extensions/ServiceCollectionExtensions.cs | 18 + .../IAuthorizationSeeder.cs | 9 +- .../InMemoryAuthorizationSeedContributor.cs | 1 - .../Commands/AssignUserRoleCommand.cs | 27 +- .../Commands/GetUserRolesCommand.cs | 27 +- .../Commands/RemoveUserRoleCommand.cs | 27 +- .../Endpoints/AuthorizationEndpointHandler.cs | 131 ++++++ .../DefaultAuthorizationEndpointHandler.cs | 132 ------ .../AuthorizationReferenceExtensions.cs | 21 - .../Extensions/ServiceCollectionExtensions.cs | 19 + .../DefaultRolePermissionResolver.cs | 36 -- .../DefaultUserPermissionStore.cs | 25 -- .../Infrastructure/RolePermissionResolver.cs | 34 ++ .../Infrastructure/UserPermissionStore.cs | 23 ++ .../Services/AuthorizationService.cs | 36 ++ .../Services/DefaultAuthorizationService.cs | 37 -- .../Services/DefaultUserRoleService.cs | 62 --- .../Services/IAuthorizationService.cs | 10 +- .../Services/UserRoleService.cs | 59 +++ .../Abstractions/IUserRoleService.cs | 13 +- .../AuthorizationClaimsProvider.cs | 34 ++ .../DefaultAuthorizationClaimsProvider.cs | 36 -- ...andler.cs => CredentialEndpointHandler.cs} | 4 +- .../Extensions/ServiceCollectionExtensions.cs | 6 +- ...lsService.cs => UserCredentialsService.cs} | 4 +- ...ialValidator.cs => CredentialValidator.cs} | 4 +- .../Argon2Options.cs | 19 +- .../Argon2PasswordHasher.cs | 81 ++-- .../ServiceCollectionExtensions.cs | 19 +- .../SessionChainProjection.cs | 31 +- .../EntityProjections/SessionProjection.cs | 38 +- .../SessionRootProjection.cs | 25 +- .../Extensions/ServiceCollectionExtensions.cs | 3 +- .../AuthSessionIdEfConverter.cs | 6 +- .../Infrastructure/JsonValueConverter.cs | 15 +- .../NullableAuthSessionIdConverter.cs | 17 +- .../Mappers/SessionChainProjectionMapper.cs | 63 ++- .../Mappers/SessionProjectionMapper.cs | 77 ++-- .../Mappers/SessionRootProjectionMapper.cs | 54 ++- .../Stores/EfCoreSessionStoreKernel.cs | 377 +++++++++--------- .../InMemorySessionStoreKernel.cs | 5 +- .../EfCoreTokenStore.cs | 3 +- .../InMemoryRefreshTokenStore.cs | 2 +- .../Dtos/MfaMethod.cs | 15 +- .../Dtos/UserAccessDecision.cs | 6 - .../Dtos/UserIdentifierDto.cs | 19 +- .../Dtos/UserIdentifierType.cs | 13 +- .../Dtos/UserMfaStatusDto.cs | 17 +- .../Dtos/UserProfileInput.cs | 10 - .../Dtos/UserViewDto.cs | 37 +- .../Requests/AddUserIdentifierRequest.cs | 13 +- .../Requests/BeginMfaSetupRequest.cs | 9 +- .../Requests/ChangeUserIdentifierRequest.cs | 13 +- .../Requests/ChangeUserStatusAdminRequest.cs | 11 +- .../Requests/ChangeUserStatusSelfRequest.cs | 9 +- .../Requests/CompleteMfaSetupRequest.cs | 11 +- .../Requests/DeleteUserIdentifierRequest.cs | 13 +- .../Requests/DeleteUserRequest.cs | 10 +- .../Requests/DisableMfaRequest.cs | 9 +- .../Requests/RegisterUserRequest.cs | 45 +-- .../SerPrimaryUserIdentifierRequest.cs | 8 - .../SetPrimaryUserIdentifierRequest.cs | 7 + .../UnsetPrimaryUserIdentifierRequest.cs | 11 +- .../Requests/UpdateUserIdentifierRequest.cs | 13 +- .../Requests/VerifyUserIdentifierRequest.cs | 11 +- .../Responses/BeginMfaSetupResult.cs | 13 +- .../Responses/GetUserIdentifiersResult.cs | 9 +- .../Responses/IdentifierChangeResult.cs | 15 +- .../Responses/IdentifierDeleteResult.cs | 15 +- .../Responses/IdentifierVerificationResult.cs | 15 +- .../Extensions/ServiceCollectionExtensions.cs | 25 ++ .../UltimateAuthUsersInMemoryExtensions.cs | 26 -- .../Infrastructure/InMemoryUserIdProvider.cs | 16 +- .../InMemoryUserSeedContributor.cs | 112 +++--- .../Stores/InMemoryUserIdentifierStore.cs | 253 ++++++------ .../Commands/AddUserIdentifierCommand.cs | 19 +- .../Commands/ChangeUserStatusCommand.cs | 23 +- .../Commands/CreateUserCommand.cs | 23 +- .../Commands/DeleteUserCommand.cs | 23 +- .../Commands/DeleteUserIdentifierCommand.cs | 23 +- .../Commands/GetMeCommand.cs | 4 +- .../Commands/GetUserIdentifierCommand.cs | 24 +- .../Commands/GetUserIdentifiersCommand.cs | 23 +- .../Commands/GetUserProfileCommand.cs | 4 +- .../SetPrimaryUserIdentifierCommand.cs | 19 +- .../UnsetPrimaryUserIdentifierCommand.cs | 19 +- .../Commands/UpdateUserIdentifierCommand.cs | 19 +- .../Commands/UpdateUserProfileAdminCommand.cs | 4 +- .../Commands/UserIdentifierExistsCommand.cs | 24 +- .../Commands/VerifyUserIdentifierCommand.cs | 23 +- .../Contracts/UserLifecycleQuery.cs | 15 +- .../Contracts/UserProfileQuery.cs | 13 +- ...pointHandler.cs => UserEndpointHandler.cs} | 4 +- .../Extensions/ServiceCollectonExtensions.cs | 2 +- .../Mapping/UserIdentifierMapper.cs | 27 +- .../Mapping/UserProfileMapper.cs | 27 +- .../Services/IUserApplicationService.cs | 37 +- .../Services/UserApplicationService.cs | 1 - .../Stores/IUserLifecycleStore.cs | 21 +- .../Stores/UserRuntimeStore.cs | 41 +- .../Abstractions/IUser.cs | 11 +- .../BlazorServerSessionCoordinatorTests.cs | 156 ++++---- .../Client/ClientDiagnosticsTests.cs | 193 +++++---- .../Client/RefreshOutcomeParserTests.cs | 46 +-- .../Core/RefreshTokenValidatorTests.cs | 173 ++++---- .../Core/UAuthSessionTests.cs | 1 - .../Core/UserIdConverterTests.cs | 141 ++++--- .../CredentialUserMappingBuilderTests.cs | 149 ++++--- .../Fake/FakeFlowClient.cs | 119 +++--- .../Fake/FakeNavigationManager.cs | 23 +- .../Policies/ActionTextTests.cs | 43 +- .../Server/EffectiveAuthModeResolverTests.cs | 56 ++- .../EffectiveServerOptionsProviderTests.cs | 215 +++++----- .../TestHelpers.cs | 11 +- .../TestIds.cs | 18 +- 369 files changed, 6216 insertions(+), 6766 deletions(-) rename src/CodeBeam.UltimateAuth.Client/Authentication/{DefaultUAuthStateManager.cs => UAuthStateManager.cs} (86%) rename src/CodeBeam.UltimateAuth.Client/Device/{DefaultDeviceIdGenerator.cs => UAuthDeviceIdGenerator.cs} (85%) rename src/CodeBeam.UltimateAuth.Client/Device/{DefaultDeviceIdProvider.cs => UAuthDeviceIdProvider.cs} (84%) rename src/CodeBeam.UltimateAuth.Client/Services/{DefaultAuthorizationClient.cs => UAuthAuthorizationClient.cs} (92%) rename src/CodeBeam.UltimateAuth.Client/Services/{DefaultCredentialClient.cs => UAuthCredentialClient.cs} (96%) rename src/CodeBeam.UltimateAuth.Client/Services/{DefaultFlowClient.cs => UAuthFlowClient.cs} (97%) rename src/CodeBeam.UltimateAuth.Client/Services/{DefaultUserClient.cs => UAuthUserClient.cs} (95%) rename src/CodeBeam.UltimateAuth.Client/Services/{DefaultUserIdentifierClient.cs => UAuthUserIdentifierClient.cs} (96%) create mode 100644 src/CodeBeam.UltimateAuth.Core/Contracts/Logout/LogoutResponse.cs rename src/CodeBeam.UltimateAuth.Core/Extensions/{UltimateAuthServiceCollectionExtensions.cs => ServiceCollectionExtensions.cs} (98%) rename src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/{DefaultAuthAuthority.cs => AuthAuthority.cs} (88%) rename src/CodeBeam.UltimateAuth.Core/Infrastructure/{DefaultRefreshTokenValidator.cs => UAuthRefreshTokenValidator.cs} (91%) delete mode 100644 src/CodeBeam.UltimateAuth.Server/Abstractions/ResolvedCredential.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Auth/Accessor/AuthFlowContextAccessor.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Auth/Accessor/DefaultAuthFlowContextAccessor.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Auth/ClientProfileReader.cs rename src/CodeBeam.UltimateAuth.Server/Auth/Context/{DefaultAccessContextFactory.cs => AccessContextFactory.cs} (94%) create mode 100644 src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthContextFactory.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthFlow.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Auth/Context/DefaultAuthContextFactory.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Auth/Context/DefaultAuthFlow.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Auth/Context/IAuthFlowContextFactory.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Auth/DefaultClientProfileReader.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Auth/DefaultEffectiveServerOptionsProvider.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Auth/DefaultPrimaryTokenResolver.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Auth/EffectiveServerOptionsProvider.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Auth/PrimaryTokenResolver.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Auth/Response/AuthResponseResolver.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Auth/Response/DefaultAuthResponseResolver.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Auth/Response/DefaultEffectiveAuthModeResolver.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Auth/Response/EffectiveAuthModeResolver.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Contracts/JwtSigningKey.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Contracts/LogoutResponse.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Contracts/ResolvedCredential.cs rename src/CodeBeam.UltimateAuth.Server/Cookies/{DefaultUAuthCookieManager.cs => UAuthCookieManager.cs} (88%) rename src/CodeBeam.UltimateAuth.Server/Cookies/{DefaultUAuthCookiePolicyBuilder.cs => UAuthCookiePolicyBuilder.cs} (97%) create mode 100644 src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IUAuthEndpointRegistrar.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Endpoints/Bridges/LoginEndpointHandlerBridge.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Endpoints/Bridges/LogoutEndpointHandlerBridge.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Endpoints/Bridges/PkceEndpointHandlerBridge.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Endpoints/Bridges/RefreshEndpointHandlerBridge.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Endpoints/Bridges/ValidateEndpointHandlerBridge.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultLogoutEndpointHandler.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultRefreshEndpointHandler.cs rename src/CodeBeam.UltimateAuth.Server/Endpoints/{DefaultLoginEndpointHandler.cs => LoginEndpointHandler.cs} (96%) delete mode 100644 src/CodeBeam.UltimateAuth.Server/Endpoints/LoginEndpointHandlerBridge.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Endpoints/LogoutEndpointHandler.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Endpoints/LogoutEndpointHandlerBridge.cs rename src/CodeBeam.UltimateAuth.Server/Endpoints/{DefaultPkceEndpointHandler.cs => PkceEndpointHandler.cs} (98%) delete mode 100644 src/CodeBeam.UltimateAuth.Server/Endpoints/PkceEndpointHandlerBridge.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Endpoints/RefreshEndpointHandler.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Endpoints/RefreshEndpointHandlerBridge.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Endpoints/UAuthEndpointDefaults.cs rename src/CodeBeam.UltimateAuth.Server/Endpoints/{DefaultValidateEndpointHandler.cs => ValidateEndpointHandler.cs} (96%) delete mode 100644 src/CodeBeam.UltimateAuth.Server/Endpoints/ValidateEndpointHandlerBridge.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Extensions/ServiceCollectionExtensions.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Extensions/TenantResolutionContextExtensions.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Extensions/UAuthServerServiceCollectionExtensions.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Flows/Login/ILoginAuthority.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Flows/Login/ILoginOrchestrator.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Flows/Login/LoginAuthority.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Flows/Login/LoginDecision.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Flows/Login/LoginDecisionContext.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Flows/Login/LoginDecisionKind.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Flows/Login/LoginOrchestrator.cs rename src/CodeBeam.UltimateAuth.Server/{Infrastructure => Flows}/Pkce/IPkceAuthorizationValidator.cs (77%) rename src/CodeBeam.UltimateAuth.Server/{Infrastructure => Flows}/Pkce/PkceAuthorizationArtifact.cs (96%) rename src/CodeBeam.UltimateAuth.Server/{Infrastructure => Flows}/Pkce/PkceAuthorizationValidator.cs (97%) rename src/CodeBeam.UltimateAuth.Server/{Infrastructure => Flows}/Pkce/PkceAuthorizeRequest.cs (78%) create mode 100644 src/CodeBeam.UltimateAuth.Server/Flows/Pkce/PkceChallengeMethod.cs rename src/CodeBeam.UltimateAuth.Server/{Infrastructure => Flows}/Pkce/PkceContextSnapshot.cs (95%) rename src/CodeBeam.UltimateAuth.Server/{Infrastructure => Flows}/Pkce/PkceValidationFailureReason.cs (75%) rename src/CodeBeam.UltimateAuth.Server/{Infrastructure => Flows}/Pkce/PkceValidationResult.cs (89%) create mode 100644 src/CodeBeam.UltimateAuth.Server/Flows/Refresh/IRefreshResponsePolicy.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Flows/Refresh/IRefreshResponseWriter.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Flows/Refresh/IRefreshService.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Flows/Refresh/ISessionTouchService.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Flows/Refresh/RefreshDecision.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Flows/Refresh/RefreshDecisionResolver.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Flows/Refresh/RefreshEvaluationResult.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Flows/Refresh/RefreshResponsePolicy.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Flows/Refresh/RefreshResponseWriter.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Flows/Refresh/RefreshStrategyResolver.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Flows/Refresh/RefreshTokenResolver.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Flows/Refresh/SessionTouchPolicy.cs rename src/CodeBeam.UltimateAuth.Server/{Infrastructure/Refresh/DefaultSessionTouchService.cs => Flows/Refresh/SessionTouchService.cs} (88%) rename src/CodeBeam.UltimateAuth.Server/Infrastructure/{DefaultAccessPolicyProvider.cs => AccessPolicyProvider.cs} (76%) rename src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/AspNetCore/{DefaultTransportCredentialResolver.cs => TransportCredentialResolver.cs} (87%) rename src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/{DefaultCredentialResponseWriter.cs => CredentialResponseWriter.cs} (93%) delete mode 100644 src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/DefaultFlowCredentialResolver.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/DefaultPrimaryCredentialResolver.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/DefaultUAuthBodyPolicyBuilder.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/DefaultUAuthHeaderPolicyBuilder.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/FlowCredentialResolver.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/PrimaryCredentialResolver.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/UAuthBodyPolicyBuilder.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/UAuthHeaderPolicyBuilder.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Infrastructure/DefaultJwtTokenGenerator.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Infrastructure/DefaultOpaqueTokenGenerator.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Infrastructure/Device/DefaultDeviceContextFactory.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Infrastructure/Device/DefaultDeviceResolver.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Infrastructure/Device/DeviceContextFactory.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Infrastructure/Device/DeviceResolver.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Infrastructure/Hub/DefaultHubCredentialResolver.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Infrastructure/Hub/DefaultHubFlowReader.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Infrastructure/Hub/HubCredentialResolver.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Infrastructure/Hub/HubFlowReader.cs rename src/CodeBeam.UltimateAuth.Server/{ => Infrastructure}/Issuers/UAuthSessionIssuer.cs (99%) create mode 100644 src/CodeBeam.UltimateAuth.Server/Infrastructure/Issuers/UAuthTokenIssuer.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Infrastructure/JwtTokenGenerator.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Infrastructure/OpaqueTokenGenerator.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/DefaultAccessAuthority.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/UAuthAccessAuthority.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Infrastructure/Pkce/PkceChallengeMethod.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/DefaultRefreshResponsePolicy.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/DefaultRefreshResponseWriter.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/DefaultRefreshTokenResolver.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/IRefreshResponsePolicy.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/IRefreshResponseWriter.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/IRefreshService.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/ISessionTouchService.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/RefreshDecision.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/RefreshDecisionResolver.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/RefreshEvaluationResult.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/RefreshStrategyResolver.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/SessionTouchPolicy.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Infrastructure/Session/DefaultSessionContextAccessor.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Infrastructure/Session/SessionContextAccessor.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Issuers/UAuthTokenIssuer.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Login/DefaultLoginAuthority.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Login/DefaultLoginOrchestrator.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Login/ILoginAuthority.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Login/ILoginOrchestrator.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Login/LoginDecision.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Login/LoginDecisionContext.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Login/LoginDecisionKind.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Services/DefaultRefreshFlowService.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Services/RefreshFlowService.cs delete mode 100644 src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Extensions/AuthorizationInMemoryExtensions.cs create mode 100644 src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Extensions/ServiceCollectionExtensions.cs create mode 100644 src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Endpoints/AuthorizationEndpointHandler.cs delete mode 100644 src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Endpoints/DefaultAuthorizationEndpointHandler.cs delete mode 100644 src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Extensions/AuthorizationReferenceExtensions.cs create mode 100644 src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Extensions/ServiceCollectionExtensions.cs delete mode 100644 src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Infrastructure/DefaultRolePermissionResolver.cs delete mode 100644 src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Infrastructure/DefaultUserPermissionStore.cs create mode 100644 src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Infrastructure/RolePermissionResolver.cs create mode 100644 src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Infrastructure/UserPermissionStore.cs create mode 100644 src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Services/AuthorizationService.cs delete mode 100644 src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Services/DefaultAuthorizationService.cs delete mode 100644 src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Services/DefaultUserRoleService.cs create mode 100644 src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Services/UserRoleService.cs create mode 100644 src/authorization/CodeBeam.UltimateAuth.Authorization/AuthorizationClaimsProvider.cs delete mode 100644 src/authorization/CodeBeam.UltimateAuth.Authorization/DefaultAuthorizationClaimsProvider.cs rename src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Endpoints/{DefaultCredentialEndpointHandler.cs => CredentialEndpointHandler.cs} (97%) rename src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Services/{DefaultUserCredentialsService.cs => UserCredentialsService.cs} (98%) rename src/credentials/CodeBeam.UltimateAuth.Credentials/Infrastructure/{DefaultCredentialValidator.cs => CredentialValidator.cs} (88%) delete mode 100644 src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserAccessDecision.cs delete mode 100644 src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserProfileInput.cs delete mode 100644 src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/SerPrimaryUserIdentifierRequest.cs create mode 100644 src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/SetPrimaryUserIdentifierRequest.cs create mode 100644 src/users/CodeBeam.UltimateAuth.Users.InMemory/Extensions/ServiceCollectionExtensions.cs delete mode 100644 src/users/CodeBeam.UltimateAuth.Users.InMemory/Extensions/UltimateAuthUsersInMemoryExtensions.cs rename src/users/CodeBeam.UltimateAuth.Users.Reference/Endpoints/{DefaultUserEndpointHandler.cs => UserEndpointHandler.cs} (98%) diff --git a/src/CodeBeam.UltimateAuth.Client/Authentication/DefaultUAuthStateManager.cs b/src/CodeBeam.UltimateAuth.Client/Authentication/UAuthStateManager.cs similarity index 86% rename from src/CodeBeam.UltimateAuth.Client/Authentication/DefaultUAuthStateManager.cs rename to src/CodeBeam.UltimateAuth.Client/Authentication/UAuthStateManager.cs index 987726af..4b0196a5 100644 --- a/src/CodeBeam.UltimateAuth.Client/Authentication/DefaultUAuthStateManager.cs +++ b/src/CodeBeam.UltimateAuth.Client/Authentication/UAuthStateManager.cs @@ -3,7 +3,7 @@ namespace CodeBeam.UltimateAuth.Client.Authentication; -internal sealed class DefaultUAuthStateManager : IUAuthStateManager +internal sealed class UAuthStateManager : IUAuthStateManager { private readonly IUAuthClient _client; private readonly IClock _clock; @@ -11,7 +11,7 @@ internal sealed class DefaultUAuthStateManager : IUAuthStateManager public UAuthState State { get; } = UAuthState.Anonymous(); - public DefaultUAuthStateManager(IUAuthClient client, IClock clock, IUAuthClientBootstrapper bootstrapper) + public UAuthStateManager(IUAuthClient client, IClock clock, IUAuthClientBootstrapper bootstrapper) { _client = client; _clock = clock; diff --git a/src/CodeBeam.UltimateAuth.Client/Device/DefaultDeviceIdGenerator.cs b/src/CodeBeam.UltimateAuth.Client/Device/UAuthDeviceIdGenerator.cs similarity index 85% rename from src/CodeBeam.UltimateAuth.Client/Device/DefaultDeviceIdGenerator.cs rename to src/CodeBeam.UltimateAuth.Client/Device/UAuthDeviceIdGenerator.cs index ccca8c95..1cf9fb4a 100644 --- a/src/CodeBeam.UltimateAuth.Client/Device/DefaultDeviceIdGenerator.cs +++ b/src/CodeBeam.UltimateAuth.Client/Device/UAuthDeviceIdGenerator.cs @@ -4,7 +4,7 @@ namespace CodeBeam.UltimateAuth.Client.Devices; -public sealed class DefaultDeviceIdGenerator : IDeviceIdGenerator +public sealed class UAuthDeviceIdGenerator : IDeviceIdGenerator { public DeviceId Generate() { diff --git a/src/CodeBeam.UltimateAuth.Client/Device/DefaultDeviceIdProvider.cs b/src/CodeBeam.UltimateAuth.Client/Device/UAuthDeviceIdProvider.cs similarity index 84% rename from src/CodeBeam.UltimateAuth.Client/Device/DefaultDeviceIdProvider.cs rename to src/CodeBeam.UltimateAuth.Client/Device/UAuthDeviceIdProvider.cs index c1f504a8..b7152b00 100644 --- a/src/CodeBeam.UltimateAuth.Client/Device/DefaultDeviceIdProvider.cs +++ b/src/CodeBeam.UltimateAuth.Client/Device/UAuthDeviceIdProvider.cs @@ -3,14 +3,14 @@ namespace CodeBeam.UltimateAuth.Client.Devices; -public sealed class DefaultDeviceIdProvider : IDeviceIdProvider +public sealed class UAuthDeviceIdProvider : IDeviceIdProvider { private readonly IDeviceIdStorage _storage; private readonly IDeviceIdGenerator _generator; private DeviceId? _cached; - public DefaultDeviceIdProvider(IDeviceIdStorage storage, IDeviceIdGenerator generator) + public UAuthDeviceIdProvider(IDeviceIdStorage storage, IDeviceIdGenerator generator) { _storage = storage; _generator = generator; diff --git a/src/CodeBeam.UltimateAuth.Client/Extensions/ServiceCollectionExtensions.cs b/src/CodeBeam.UltimateAuth.Client/Extensions/ServiceCollectionExtensions.cs index e3046940..0ae74a5e 100644 --- a/src/CodeBeam.UltimateAuth.Client/Extensions/ServiceCollectionExtensions.cs +++ b/src/CodeBeam.UltimateAuth.Client/Extensions/ServiceCollectionExtensions.cs @@ -99,11 +99,11 @@ private static IServiceCollection AddUltimateAuthClientInternal(this IServiceCol services.TryAddScoped(); services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); services.AddScoped(sp => { @@ -118,16 +118,16 @@ private static IServiceCollection AddUltimateAuthClientInternal(this IServiceCol services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.TryAddScoped(); services.TryAddScoped(); services.TryAddScoped(); - services.AddScoped(); + services.AddScoped(); services.AddScoped(); - services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/src/CodeBeam.UltimateAuth.Client/Services/DefaultAuthorizationClient.cs b/src/CodeBeam.UltimateAuth.Client/Services/UAuthAuthorizationClient.cs similarity index 92% rename from src/CodeBeam.UltimateAuth.Client/Services/DefaultAuthorizationClient.cs rename to src/CodeBeam.UltimateAuth.Client/Services/UAuthAuthorizationClient.cs index 2e77508d..d4ed5fa7 100644 --- a/src/CodeBeam.UltimateAuth.Client/Services/DefaultAuthorizationClient.cs +++ b/src/CodeBeam.UltimateAuth.Client/Services/UAuthAuthorizationClient.cs @@ -7,12 +7,12 @@ namespace CodeBeam.UltimateAuth.Client.Services; -internal sealed class DefaultAuthorizationClient : IAuthorizationClient +internal sealed class UAuthAuthorizationClient : IAuthorizationClient { private readonly IUAuthRequestClient _request; private readonly UAuthClientOptions _options; - public DefaultAuthorizationClient(IUAuthRequestClient request, IOptions options) + public UAuthAuthorizationClient(IUAuthRequestClient request, IOptions options) { _request = request; _options = options.Value; diff --git a/src/CodeBeam.UltimateAuth.Client/Services/DefaultCredentialClient.cs b/src/CodeBeam.UltimateAuth.Client/Services/UAuthCredentialClient.cs similarity index 96% rename from src/CodeBeam.UltimateAuth.Client/Services/DefaultCredentialClient.cs rename to src/CodeBeam.UltimateAuth.Client/Services/UAuthCredentialClient.cs index f88bd15d..55b1fcf4 100644 --- a/src/CodeBeam.UltimateAuth.Client/Services/DefaultCredentialClient.cs +++ b/src/CodeBeam.UltimateAuth.Client/Services/UAuthCredentialClient.cs @@ -7,12 +7,12 @@ namespace CodeBeam.UltimateAuth.Client.Services; -internal sealed class DefaultCredentialClient : ICredentialClient +internal sealed class UAuthCredentialClient : ICredentialClient { private readonly IUAuthRequestClient _request; private readonly UAuthClientOptions _options; - public DefaultCredentialClient(IUAuthRequestClient request, IOptions options) + public UAuthCredentialClient(IUAuthRequestClient request, IOptions options) { _request = request; _options = options.Value; diff --git a/src/CodeBeam.UltimateAuth.Client/Services/DefaultFlowClient.cs b/src/CodeBeam.UltimateAuth.Client/Services/UAuthFlowClient.cs similarity index 97% rename from src/CodeBeam.UltimateAuth.Client/Services/DefaultFlowClient.cs rename to src/CodeBeam.UltimateAuth.Client/Services/UAuthFlowClient.cs index b07b0d16..128cd83d 100644 --- a/src/CodeBeam.UltimateAuth.Client/Services/DefaultFlowClient.cs +++ b/src/CodeBeam.UltimateAuth.Client/Services/UAuthFlowClient.cs @@ -1,5 +1,4 @@ -using CodeBeam.UltimateAuth.Client.Abstractions; -using CodeBeam.UltimateAuth.Client.Contracts; +using CodeBeam.UltimateAuth.Client.Contracts; using CodeBeam.UltimateAuth.Client.Diagnostics; using CodeBeam.UltimateAuth.Client.Extensions; using CodeBeam.UltimateAuth.Client.Infrastructure; @@ -16,7 +15,7 @@ namespace CodeBeam.UltimateAuth.Client.Services; -internal class DefaultFlowClient : IFlowClient +internal class UAuthFlowClient : IFlowClient { private readonly IUAuthRequestClient _post; private readonly UAuthClientOptions _options; @@ -24,7 +23,7 @@ internal class DefaultFlowClient : IFlowClient private readonly UAuthClientDiagnostics _diagnostics; private readonly NavigationManager _nav; - public DefaultFlowClient( + public UAuthFlowClient( IUAuthRequestClient post, IOptions options, IOptions coreOptions, diff --git a/src/CodeBeam.UltimateAuth.Client/Services/DefaultUserClient.cs b/src/CodeBeam.UltimateAuth.Client/Services/UAuthUserClient.cs similarity index 95% rename from src/CodeBeam.UltimateAuth.Client/Services/DefaultUserClient.cs rename to src/CodeBeam.UltimateAuth.Client/Services/UAuthUserClient.cs index b1218a94..e13a3f8b 100644 --- a/src/CodeBeam.UltimateAuth.Client/Services/DefaultUserClient.cs +++ b/src/CodeBeam.UltimateAuth.Client/Services/UAuthUserClient.cs @@ -7,12 +7,12 @@ namespace CodeBeam.UltimateAuth.Client.Services; -internal sealed class DefaultUserClient : IUserClient +internal sealed class UAuthUserClient : IUserClient { private readonly IUAuthRequestClient _request; private readonly UAuthClientOptions _options; - public DefaultUserClient(IUAuthRequestClient request, IOptions options) + public UAuthUserClient(IUAuthRequestClient request, IOptions options) { _request = request; _options = options.Value; diff --git a/src/CodeBeam.UltimateAuth.Client/Services/DefaultUserIdentifierClient.cs b/src/CodeBeam.UltimateAuth.Client/Services/UAuthUserIdentifierClient.cs similarity index 96% rename from src/CodeBeam.UltimateAuth.Client/Services/DefaultUserIdentifierClient.cs rename to src/CodeBeam.UltimateAuth.Client/Services/UAuthUserIdentifierClient.cs index 8fb2c12a..6aeeedca 100644 --- a/src/CodeBeam.UltimateAuth.Client/Services/DefaultUserIdentifierClient.cs +++ b/src/CodeBeam.UltimateAuth.Client/Services/UAuthUserIdentifierClient.cs @@ -7,12 +7,12 @@ namespace CodeBeam.UltimateAuth.Client.Services; -public class DefaultUserIdentifierClient : IUserIdentifierClient +public class UAuthUserIdentifierClient : IUserIdentifierClient { private readonly IUAuthRequestClient _request; private readonly UAuthClientOptions _options; - public DefaultUserIdentifierClient(IUAuthRequestClient request, IOptions options) + public UAuthUserIdentifierClient(IUAuthRequestClient request, IOptions options) { _request = request; _options = options.Value; diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Logout/LogoutResponse.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Logout/LogoutResponse.cs new file mode 100644 index 00000000..00cd98ba --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Logout/LogoutResponse.cs @@ -0,0 +1,6 @@ +namespace CodeBeam.UltimateAuth.Core.Contracts; + +public sealed record LogoutResponse +{ + public bool Success { get; init; } +} diff --git a/src/CodeBeam.UltimateAuth.Core/Extensions/UltimateAuthServiceCollectionExtensions.cs b/src/CodeBeam.UltimateAuth.Core/Extensions/ServiceCollectionExtensions.cs similarity index 98% rename from src/CodeBeam.UltimateAuth.Core/Extensions/UltimateAuthServiceCollectionExtensions.cs rename to src/CodeBeam.UltimateAuth.Core/Extensions/ServiceCollectionExtensions.cs index e9674428..17204ca2 100644 --- a/src/CodeBeam.UltimateAuth.Core/Extensions/UltimateAuthServiceCollectionExtensions.cs +++ b/src/CodeBeam.UltimateAuth.Core/Extensions/ServiceCollectionExtensions.cs @@ -22,7 +22,7 @@ namespace CodeBeam.UltimateAuth.Core.Extensions; /// PKCE handlers, and any server-specific logic must be added from the Server package /// (e.g., AddUltimateAuthServer()). /// -public static class UltimateAuthServiceCollectionExtensions +public static class ServiceCollectionExtensions { /// /// Registers UltimateAuth services using configuration binding (e.g., appsettings.json). diff --git a/src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/DefaultAuthAuthority.cs b/src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/AuthAuthority.cs similarity index 88% rename from src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/DefaultAuthAuthority.cs rename to src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/AuthAuthority.cs index e4084ece..9f5179ce 100644 --- a/src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/DefaultAuthAuthority.cs +++ b/src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/AuthAuthority.cs @@ -3,12 +3,12 @@ namespace CodeBeam.UltimateAuth.Core.Infrastructure; -public sealed class DefaultAuthAuthority : IAuthAuthority +public sealed class AuthAuthority : IAuthAuthority { private readonly IEnumerable _invariants; private readonly IEnumerable _policies; - public DefaultAuthAuthority(IEnumerable invariants, IEnumerable policies) + public AuthAuthority(IEnumerable invariants, IEnumerable policies) { _invariants = invariants ?? Array.Empty(); _policies = policies ?? Array.Empty(); diff --git a/src/CodeBeam.UltimateAuth.Core/Infrastructure/DefaultRefreshTokenValidator.cs b/src/CodeBeam.UltimateAuth.Core/Infrastructure/UAuthRefreshTokenValidator.cs similarity index 91% rename from src/CodeBeam.UltimateAuth.Core/Infrastructure/DefaultRefreshTokenValidator.cs rename to src/CodeBeam.UltimateAuth.Core/Infrastructure/UAuthRefreshTokenValidator.cs index cb6e2f62..53ca29b6 100644 --- a/src/CodeBeam.UltimateAuth.Core/Infrastructure/DefaultRefreshTokenValidator.cs +++ b/src/CodeBeam.UltimateAuth.Core/Infrastructure/UAuthRefreshTokenValidator.cs @@ -3,12 +3,12 @@ namespace CodeBeam.UltimateAuth.Core.Infrastructure; -public sealed class DefaultRefreshTokenValidator : IRefreshTokenValidator +public sealed class UAuthRefreshTokenValidator : IRefreshTokenValidator { private readonly IRefreshTokenStore _store; private readonly ITokenHasher _hasher; - public DefaultRefreshTokenValidator(IRefreshTokenStore store, ITokenHasher hasher) + public UAuthRefreshTokenValidator(IRefreshTokenStore store, ITokenHasher hasher) { _store = store; _hasher = hasher; diff --git a/src/CodeBeam.UltimateAuth.Server/Abstractions/ICredentialResponseWriter.cs b/src/CodeBeam.UltimateAuth.Server/Abstractions/ICredentialResponseWriter.cs index 950b8fa3..11e5e962 100644 --- a/src/CodeBeam.UltimateAuth.Server/Abstractions/ICredentialResponseWriter.cs +++ b/src/CodeBeam.UltimateAuth.Server/Abstractions/ICredentialResponseWriter.cs @@ -2,12 +2,11 @@ using CodeBeam.UltimateAuth.Core.Domain; using Microsoft.AspNetCore.Http; -namespace CodeBeam.UltimateAuth.Server.Abstractions +namespace CodeBeam.UltimateAuth.Server.Abstractions; + +public interface ICredentialResponseWriter { - public interface ICredentialResponseWriter - { - void Write(HttpContext context, CredentialKind kind, AuthSessionId sessionId); - void Write(HttpContext context, CredentialKind kind, AccessToken accessToken); - void Write(HttpContext context, CredentialKind kind, RefreshToken refreshToken); - } + void Write(HttpContext context, CredentialKind kind, AuthSessionId sessionId); + void Write(HttpContext context, CredentialKind kind, AccessToken accessToken); + void Write(HttpContext context, CredentialKind kind, RefreshToken refreshToken); } diff --git a/src/CodeBeam.UltimateAuth.Server/Abstractions/IDeviceResolver.cs b/src/CodeBeam.UltimateAuth.Server/Abstractions/IDeviceResolver.cs index 06b0998a..8be92d7a 100644 --- a/src/CodeBeam.UltimateAuth.Server/Abstractions/IDeviceResolver.cs +++ b/src/CodeBeam.UltimateAuth.Server/Abstractions/IDeviceResolver.cs @@ -1,13 +1,12 @@ using Microsoft.AspNetCore.Http; using CodeBeam.UltimateAuth.Core.Contracts; -namespace CodeBeam.UltimateAuth.Server.Abstractions +namespace CodeBeam.UltimateAuth.Server.Abstractions; + +/// +/// Resolves device and client metadata from the current HTTP context. +/// +public interface IDeviceResolver { - /// - /// Resolves device and client metadata from the current HTTP context. - /// - public interface IDeviceResolver - { - DeviceInfo Resolve(HttpContext context); - } + DeviceInfo Resolve(HttpContext context); } diff --git a/src/CodeBeam.UltimateAuth.Server/Abstractions/IPrimaryCredentialResolver.cs b/src/CodeBeam.UltimateAuth.Server/Abstractions/IPrimaryCredentialResolver.cs index 554e9263..52d54c7e 100644 --- a/src/CodeBeam.UltimateAuth.Server/Abstractions/IPrimaryCredentialResolver.cs +++ b/src/CodeBeam.UltimateAuth.Server/Abstractions/IPrimaryCredentialResolver.cs @@ -1,10 +1,9 @@ using CodeBeam.UltimateAuth.Core.Domain; using Microsoft.AspNetCore.Http; -namespace CodeBeam.UltimateAuth.Server.Abstractions +namespace CodeBeam.UltimateAuth.Server.Abstractions; + +public interface IPrimaryCredentialResolver { - public interface IPrimaryCredentialResolver - { - PrimaryCredentialKind Resolve(HttpContext context); - } + PrimaryCredentialKind Resolve(HttpContext context); } diff --git a/src/CodeBeam.UltimateAuth.Server/Abstractions/IRefreshTokenResolver.cs b/src/CodeBeam.UltimateAuth.Server/Abstractions/IRefreshTokenResolver.cs index 1a727852..237668d9 100644 --- a/src/CodeBeam.UltimateAuth.Server/Abstractions/IRefreshTokenResolver.cs +++ b/src/CodeBeam.UltimateAuth.Server/Abstractions/IRefreshTokenResolver.cs @@ -1,13 +1,12 @@ using Microsoft.AspNetCore.Http; -namespace CodeBeam.UltimateAuth.Server.Abstractions +namespace CodeBeam.UltimateAuth.Server.Abstractions; + +public interface IRefreshTokenResolver { - public interface IRefreshTokenResolver - { - /// - /// Resolves refresh token from incoming HTTP request. - /// Returns null if no refresh token is present. - /// - string? Resolve(HttpContext context); - } + /// + /// Resolves refresh token from incoming HTTP request. + /// Returns null if no refresh token is present. + /// + string? Resolve(HttpContext context); } diff --git a/src/CodeBeam.UltimateAuth.Server/Abstractions/ISigningKeyProvider.cs b/src/CodeBeam.UltimateAuth.Server/Abstractions/ISigningKeyProvider.cs index 74ae5b49..189b43ac 100644 --- a/src/CodeBeam.UltimateAuth.Server/Abstractions/ISigningKeyProvider.cs +++ b/src/CodeBeam.UltimateAuth.Server/Abstractions/ISigningKeyProvider.cs @@ -1,17 +1,8 @@ -using Microsoft.IdentityModel.Tokens; +using CodeBeam.UltimateAuth.Server.Contracts; -namespace CodeBeam.UltimateAuth.Server.Abstractions -{ - public interface IJwtSigningKeyProvider - { - JwtSigningKey Resolve(string? keyId); - } - - public sealed class JwtSigningKey - { - public required string KeyId { get; init; } - public required SecurityKey Key { get; init; } - public required string Algorithm { get; init; } - } +namespace CodeBeam.UltimateAuth.Server.Abstractions; +public interface IJwtSigningKeyProvider +{ + JwtSigningKey Resolve(string? keyId); } diff --git a/src/CodeBeam.UltimateAuth.Server/Abstractions/ITokenIssuer.cs b/src/CodeBeam.UltimateAuth.Server/Abstractions/ITokenIssuer.cs index 885b37e6..9bb61132 100644 --- a/src/CodeBeam.UltimateAuth.Server/Abstractions/ITokenIssuer.cs +++ b/src/CodeBeam.UltimateAuth.Server/Abstractions/ITokenIssuer.cs @@ -1,15 +1,14 @@ using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Server.Auth; -namespace CodeBeam.UltimateAuth.Server.Abstactions +namespace CodeBeam.UltimateAuth.Server.Abstactions; + +/// +/// Issues access and refresh tokens according to the active auth mode. +/// Does not perform persistence or validation. +/// +public interface ITokenIssuer { - /// - /// Issues access and refresh tokens according to the active auth mode. - /// Does not perform persistence or validation. - /// - public interface ITokenIssuer - { - Task IssueAccessTokenAsync(AuthFlowContext flow, TokenIssuanceContext context, CancellationToken cancellationToken = default); - Task IssueRefreshTokenAsync(AuthFlowContext flow, TokenIssuanceContext context, RefreshTokenPersistence persistence, CancellationToken cancellationToken = default); - } + Task IssueAccessTokenAsync(AuthFlowContext flow, TokenIssuanceContext context, CancellationToken cancellationToken = default); + Task IssueRefreshTokenAsync(AuthFlowContext flow, TokenIssuanceContext context, RefreshTokenPersistence persistence, CancellationToken cancellationToken = default); } diff --git a/src/CodeBeam.UltimateAuth.Server/Abstractions/ResolvedCredential.cs b/src/CodeBeam.UltimateAuth.Server/Abstractions/ResolvedCredential.cs deleted file mode 100644 index cd87c461..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Abstractions/ResolvedCredential.cs +++ /dev/null @@ -1,20 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Contracts; -using CodeBeam.UltimateAuth.Core.Domain; -using CodeBeam.UltimateAuth.Core.MultiTenancy; - -namespace CodeBeam.UltimateAuth.Server.Abstractions -{ - public sealed record ResolvedCredential - { - public PrimaryCredentialKind Kind { get; init; } - - /// - /// Raw credential value (session id / jwt / opaque) - /// - public string Value { get; init; } = default!; - - public TenantKey Tenant { get; init; } - - public DeviceInfo Device { get; init; } = default!; - } -} diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/Accessor/AuthFlowContextAccessor.cs b/src/CodeBeam.UltimateAuth.Server/Auth/Accessor/AuthFlowContextAccessor.cs new file mode 100644 index 00000000..005c16eb --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Auth/Accessor/AuthFlowContextAccessor.cs @@ -0,0 +1,35 @@ +using Microsoft.AspNetCore.Http; + +namespace CodeBeam.UltimateAuth.Server.Auth; + +internal sealed class AuthFlowContextAccessor : IAuthFlowContextAccessor +{ + private static readonly object Key = new(); + + private readonly IHttpContextAccessor _http; + + public AuthFlowContextAccessor(IHttpContextAccessor http) + { + _http = http; + } + + public AuthFlowContext Current + { + get + { + var ctx = _http.HttpContext + ?? throw new InvalidOperationException("No HttpContext."); + + if (!ctx.Items.TryGetValue(Key, out var value) || value is not AuthFlowContext flow) + throw new InvalidOperationException("AuthFlowContext is not available for this request."); + + return flow; + } + } + + internal void Set(AuthFlowContext context) + { + var ctx = _http.HttpContext ?? throw new InvalidOperationException("No HttpContext."); + ctx.Items[Key] = context; + } +} diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/Accessor/DefaultAuthFlowContextAccessor.cs b/src/CodeBeam.UltimateAuth.Server/Auth/Accessor/DefaultAuthFlowContextAccessor.cs deleted file mode 100644 index 6313300f..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Auth/Accessor/DefaultAuthFlowContextAccessor.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Microsoft.AspNetCore.Http; - -namespace CodeBeam.UltimateAuth.Server.Auth -{ - internal sealed class DefaultAuthFlowContextAccessor : IAuthFlowContextAccessor - { - private static readonly object Key = new(); - - private readonly IHttpContextAccessor _http; - - public DefaultAuthFlowContextAccessor(IHttpContextAccessor http) - { - _http = http; - } - - public AuthFlowContext Current - { - get - { - var ctx = _http.HttpContext - ?? throw new InvalidOperationException("No HttpContext."); - - if (!ctx.Items.TryGetValue(Key, out var value) || value is not AuthFlowContext flow) - throw new InvalidOperationException("AuthFlowContext is not available for this request."); - - return flow; - } - } - - internal void Set(AuthFlowContext context) - { - var ctx = _http.HttpContext - ?? throw new InvalidOperationException("No HttpContext."); - - ctx.Items[Key] = context; - } - - //private static readonly AsyncLocal _current = new(); - - //public AuthFlowContext Current => _current.Value ?? throw new InvalidOperationException("AuthFlowContext is not available for this request."); - - //internal void Set(AuthFlowContext context) - //{ - // _current.Value = context; - //} - } -} diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/Accessor/IAuthFlowContextAccessor.cs b/src/CodeBeam.UltimateAuth.Server/Auth/Accessor/IAuthFlowContextAccessor.cs index 0b4666fc..6be75e85 100644 --- a/src/CodeBeam.UltimateAuth.Server/Auth/Accessor/IAuthFlowContextAccessor.cs +++ b/src/CodeBeam.UltimateAuth.Server/Auth/Accessor/IAuthFlowContextAccessor.cs @@ -1,8 +1,6 @@ -namespace CodeBeam.UltimateAuth.Server.Auth -{ - public interface IAuthFlowContextAccessor - { - AuthFlowContext Current { get; } - } +namespace CodeBeam.UltimateAuth.Server.Auth; +public interface IAuthFlowContextAccessor +{ + AuthFlowContext Current { get; } } diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/ClientProfileReader.cs b/src/CodeBeam.UltimateAuth.Server/Auth/ClientProfileReader.cs new file mode 100644 index 00000000..d48d42d0 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Auth/ClientProfileReader.cs @@ -0,0 +1,32 @@ +using CodeBeam.UltimateAuth.Core.Options; +using Microsoft.AspNetCore.Http; + +namespace CodeBeam.UltimateAuth.Server.Auth; + +internal sealed class ClientProfileReader : IClientProfileReader +{ + private const string HeaderName = "X-UAuth-ClientProfile"; + private const string FormFieldName = "__uauth_client_profile"; + + public UAuthClientProfile Read(HttpContext context) + { + if (context.Request.Headers.TryGetValue(HeaderName, out var headerValue) && TryParse(headerValue, out var headerProfile)) + { + return headerProfile; + } + + if (context.Request.HasFormContentType && context.Request.Form.TryGetValue(FormFieldName, out var formValue) && + TryParse(formValue, out var formProfile)) + { + return formProfile; + } + + return UAuthClientProfile.NotSpecified; + } + + private static bool TryParse(string value, out UAuthClientProfile profile) + { + return Enum.TryParse(value, ignoreCase: true, out profile); + } + +} diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/Context/DefaultAccessContextFactory.cs b/src/CodeBeam.UltimateAuth.Server/Auth/Context/AccessContextFactory.cs similarity index 94% rename from src/CodeBeam.UltimateAuth.Server/Auth/Context/DefaultAccessContextFactory.cs rename to src/CodeBeam.UltimateAuth.Server/Auth/Context/AccessContextFactory.cs index 7e3368ca..484c38f5 100644 --- a/src/CodeBeam.UltimateAuth.Server/Auth/Context/DefaultAccessContextFactory.cs +++ b/src/CodeBeam.UltimateAuth.Server/Auth/Context/AccessContextFactory.cs @@ -5,11 +5,11 @@ namespace CodeBeam.UltimateAuth.Server.Auth; -internal sealed class DefaultAccessContextFactory : IAccessContextFactory +internal sealed class AccessContextFactory : IAccessContextFactory { private readonly IUserRoleStore _roleStore; - public DefaultAccessContextFactory(IUserRoleStore roleStore) + public AccessContextFactory(IUserRoleStore roleStore) { _roleStore = roleStore; } diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthContextFactory.cs b/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthContextFactory.cs new file mode 100644 index 00000000..948cfc8f --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthContextFactory.cs @@ -0,0 +1,23 @@ +using CodeBeam.UltimateAuth.Core.Abstractions; +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Server.Extensions; + +namespace CodeBeam.UltimateAuth.Server.Auth; + +internal sealed class AuthContextFactory : IAuthContextFactory +{ + private readonly IAuthFlowContextAccessor _flow; + private readonly IClock _clock; + + public AuthContextFactory(IAuthFlowContextAccessor flow, IClock clock) + { + _flow = flow; + _clock = clock; + } + + public AuthContext Create(DateTimeOffset? at = null) + { + var flow = _flow.Current; + return flow.ToAuthContext(at ?? _clock.UtcNow); + } +} diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthExecutionContext.cs b/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthExecutionContext.cs index bb1fff7a..c163f0fd 100644 --- a/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthExecutionContext.cs +++ b/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthExecutionContext.cs @@ -1,9 +1,8 @@ using CodeBeam.UltimateAuth.Core.Options; -namespace CodeBeam.UltimateAuth.Server.Auth +namespace CodeBeam.UltimateAuth.Server.Auth; + +public sealed record AuthExecutionContext { - public sealed record AuthExecutionContext - { - public required UAuthClientProfile? EffectiveClientProfile { get; init; } - } + public required UAuthClientProfile? EffectiveClientProfile { get; init; } } diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthFlow.cs b/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthFlow.cs new file mode 100644 index 00000000..c45d509c --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthFlow.cs @@ -0,0 +1,28 @@ +using CodeBeam.UltimateAuth.Core.Domain; +using Microsoft.AspNetCore.Http; + +namespace CodeBeam.UltimateAuth.Server.Auth; + +internal sealed class AuthFlow : IAuthFlow +{ + private readonly IHttpContextAccessor _http; + private readonly IAuthFlowContextFactory _factory; + private readonly AuthFlowContextAccessor _accessor; + + public AuthFlow(IHttpContextAccessor http, IAuthFlowContextFactory factory, IAuthFlowContextAccessor accessor) + { + _http = http; + _factory = factory; + _accessor = (AuthFlowContextAccessor)accessor; + } + + public async ValueTask BeginAsync(AuthFlowType flowType, CancellationToken ct = default) + { + var ctx = _http.HttpContext ?? throw new InvalidOperationException("No HttpContext."); + + var flowContext = await _factory.CreateAsync(ctx, flowType); + _accessor.Set(flowContext); + + return flowContext; + } +} diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthFlowContext.cs b/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthFlowContext.cs index e0f59305..f68bee45 100644 --- a/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthFlowContext.cs +++ b/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthFlowContext.cs @@ -5,67 +5,66 @@ using CodeBeam.UltimateAuth.Core.Options; using CodeBeam.UltimateAuth.Server.Options; -namespace CodeBeam.UltimateAuth.Server.Auth +namespace CodeBeam.UltimateAuth.Server.Auth; + +public sealed class AuthFlowContext { - public sealed class AuthFlowContext - { - public AuthFlowType FlowType { get; } - public UAuthClientProfile ClientProfile { get; } - public UAuthMode EffectiveMode { get; } - public DeviceContext Device { get; } + public AuthFlowType FlowType { get; } + public UAuthClientProfile ClientProfile { get; } + public UAuthMode EffectiveMode { get; } + public DeviceContext Device { get; } - public TenantKey Tenant { get; } - public SessionSecurityContext? Session { get; } - public bool IsAuthenticated { get; } - public UserKey? UserKey { get; } + public TenantKey Tenant { get; } + public SessionSecurityContext? Session { get; } + public bool IsAuthenticated { get; } + public UserKey? UserKey { get; } - public UAuthServerOptions OriginalOptions { get; } - public EffectiveUAuthServerOptions EffectiveOptions { get; } + public UAuthServerOptions OriginalOptions { get; } + public EffectiveUAuthServerOptions EffectiveOptions { get; } - public EffectiveAuthResponse Response { get; } - public PrimaryTokenKind PrimaryTokenKind { get; } + public EffectiveAuthResponse Response { get; } + public PrimaryTokenKind PrimaryTokenKind { get; } - // Helpers - public bool AllowsTokenIssuance => - Response.AccessTokenDelivery.Mode != TokenResponseMode.None || - Response.RefreshTokenDelivery.Mode != TokenResponseMode.None; + // Helpers + public bool AllowsTokenIssuance => + Response.AccessTokenDelivery.Mode != TokenResponseMode.None || + Response.RefreshTokenDelivery.Mode != TokenResponseMode.None; - public bool IsSingleTenant => Tenant.IsSingle; - public bool IsMultiTenant => !Tenant.IsSingle && !Tenant.IsSystem; + public bool IsSingleTenant => Tenant.IsSingle; + public bool IsMultiTenant => !Tenant.IsSingle && !Tenant.IsSystem; - internal AuthFlowContext( - AuthFlowType flowType, - UAuthClientProfile clientProfile, - UAuthMode effectiveMode, - DeviceContext device, - TenantKey tenantKey, - bool isAuthenticated, - UserKey? userKey, - SessionSecurityContext? session, - UAuthServerOptions originalOptions, - EffectiveUAuthServerOptions effectiveOptions, - EffectiveAuthResponse response, - PrimaryTokenKind primaryTokenKind) - { - if (tenantKey.IsUnresolved) - throw new InvalidOperationException("AuthFlowContext cannot be created with unresolved tenant."); + internal AuthFlowContext( + AuthFlowType flowType, + UAuthClientProfile clientProfile, + UAuthMode effectiveMode, + DeviceContext device, + TenantKey tenantKey, + bool isAuthenticated, + UserKey? userKey, + SessionSecurityContext? session, + UAuthServerOptions originalOptions, + EffectiveUAuthServerOptions effectiveOptions, + EffectiveAuthResponse response, + PrimaryTokenKind primaryTokenKind) + { + if (tenantKey.IsUnresolved) + throw new InvalidOperationException("AuthFlowContext cannot be created with unresolved tenant."); - FlowType = flowType; - ClientProfile = clientProfile; - EffectiveMode = effectiveMode; - Device = device; + FlowType = flowType; + ClientProfile = clientProfile; + EffectiveMode = effectiveMode; + Device = device; - Tenant = tenantKey; - Session = session; - IsAuthenticated = isAuthenticated; - UserKey = userKey; + Tenant = tenantKey; + Session = session; + IsAuthenticated = isAuthenticated; + UserKey = userKey; - OriginalOptions = originalOptions; - EffectiveOptions = effectiveOptions; + OriginalOptions = originalOptions; + EffectiveOptions = effectiveOptions; - Response = response; - PrimaryTokenKind = primaryTokenKind; - } + 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 8e527269..3521182d 100644 --- a/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthFlowContextFactory.cs +++ b/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthFlowContextFactory.cs @@ -7,100 +7,94 @@ using CodeBeam.UltimateAuth.Server.Options; using Microsoft.AspNetCore.Http; -namespace CodeBeam.UltimateAuth.Server.Auth +namespace CodeBeam.UltimateAuth.Server.Auth; + +internal sealed class AuthFlowContextFactory : IAuthFlowContextFactory { - public interface IAuthFlowContextFactory + private readonly IClientProfileReader _clientProfileReader; + private readonly IPrimaryTokenResolver _primaryTokenResolver; + private readonly IEffectiveServerOptionsProvider _serverOptionsProvider; + private readonly IAuthResponseResolver _authResponseResolver; + private readonly IDeviceResolver _deviceResolver; + private readonly IDeviceContextFactory _deviceContextFactory; + private readonly ISessionValidator _sessionValidator; + private readonly IClock _clock; + + public AuthFlowContextFactory( + IClientProfileReader clientProfileReader, + IPrimaryTokenResolver primaryTokenResolver, + IEffectiveServerOptionsProvider serverOptionsProvider, + IAuthResponseResolver authResponseResolver, + IDeviceResolver deviceResolver, + IDeviceContextFactory deviceContextFactory, + ISessionValidator sessionValidator, + IClock clock) { - ValueTask CreateAsync(HttpContext httpContext, AuthFlowType flowType, CancellationToken ct = default); + _clientProfileReader = clientProfileReader; + _primaryTokenResolver = primaryTokenResolver; + _serverOptionsProvider = serverOptionsProvider; + _authResponseResolver = authResponseResolver; + _deviceResolver = deviceResolver; + _deviceContextFactory = deviceContextFactory; + _sessionValidator = sessionValidator; + _clock = clock; } - internal sealed class DefaultAuthFlowContextFactory : IAuthFlowContextFactory + public async ValueTask CreateAsync(HttpContext ctx, AuthFlowType flowType, CancellationToken ct = default) { - private readonly IClientProfileReader _clientProfileReader; - private readonly IPrimaryTokenResolver _primaryTokenResolver; - private readonly IEffectiveServerOptionsProvider _serverOptionsProvider; - private readonly IAuthResponseResolver _authResponseResolver; - private readonly IDeviceResolver _deviceResolver; - private readonly IDeviceContextFactory _deviceContextFactory; - private readonly ISessionValidator _sessionValidator; - private readonly IClock _clock; - - public DefaultAuthFlowContextFactory( - IClientProfileReader clientProfileReader, - IPrimaryTokenResolver primaryTokenResolver, - IEffectiveServerOptionsProvider serverOptionsProvider, - IAuthResponseResolver authResponseResolver, - IDeviceResolver deviceResolver, - IDeviceContextFactory deviceContextFactory, - ISessionValidator sessionValidator, - IClock clock) - { - _clientProfileReader = clientProfileReader; - _primaryTokenResolver = primaryTokenResolver; - _serverOptionsProvider = serverOptionsProvider; - _authResponseResolver = authResponseResolver; - _deviceResolver = deviceResolver; - _deviceContextFactory = deviceContextFactory; - _sessionValidator = sessionValidator; - _clock = clock; - } - - public async ValueTask CreateAsync(HttpContext ctx, AuthFlowType flowType, CancellationToken ct = default) - { - var tenant = ctx.GetTenant(); - var sessionCtx = ctx.GetSessionContext(); - var user = ctx.GetUserContext(); - - var clientProfile = _clientProfileReader.Read(ctx); - var originalOptions = _serverOptionsProvider.GetOriginal(ctx); - var effectiveOptions = _serverOptionsProvider.GetEffective(ctx, flowType, clientProfile); + var tenant = ctx.GetTenant(); + var sessionCtx = ctx.GetSessionContext(); + var user = ctx.GetUserContext(); - var effectiveMode = effectiveOptions.Mode; - var primaryTokenKind = _primaryTokenResolver.Resolve(effectiveMode); + var clientProfile = _clientProfileReader.Read(ctx); + var originalOptions = _serverOptionsProvider.GetOriginal(ctx); + var effectiveOptions = _serverOptionsProvider.GetEffective(ctx, flowType, clientProfile); - var response = _authResponseResolver.Resolve(effectiveMode, flowType, clientProfile, effectiveOptions); + var effectiveMode = effectiveOptions.Mode; + var primaryTokenKind = _primaryTokenResolver.Resolve(effectiveMode); - var deviceInfo = _deviceResolver.Resolve(ctx); - var deviceContext = _deviceContextFactory.Create(deviceInfo); + var response = _authResponseResolver.Resolve(effectiveMode, flowType, clientProfile, effectiveOptions); - SessionSecurityContext? sessionSecurityContext = null; + var deviceInfo = _deviceResolver.Resolve(ctx); + var deviceContext = _deviceContextFactory.Create(deviceInfo); - if (!sessionCtx.IsAnonymous) - { - var validation = await _sessionValidator.ValidateSessionAsync( - new SessionValidationContext - { - Tenant = tenant, - SessionId = sessionCtx.SessionId!.Value, - Device = deviceContext, - Now = _clock.UtcNow - }, - ct); + SessionSecurityContext? sessionSecurityContext = null; - sessionSecurityContext = SessionValidationMapper.ToSecurityContext(validation); - } + if (!sessionCtx.IsAnonymous) + { + var validation = await _sessionValidator.ValidateSessionAsync( + new SessionValidationContext + { + Tenant = tenant, + SessionId = sessionCtx.SessionId!.Value, + Device = deviceContext, + Now = _clock.UtcNow + }, + ct); - if (tenant.IsUnresolved) - throw new InvalidOperationException("AuthFlowContext cannot be created with unresolved tenant."); + sessionSecurityContext = SessionValidationMapper.ToSecurityContext(validation); + } - // TODO: Implement invariant checker - //_invariantChecker.Validate(flowType, effectiveMode, response, effectiveOptions); + if (tenant.IsUnresolved) + throw new InvalidOperationException("AuthFlowContext cannot be created with unresolved tenant."); - return new AuthFlowContext( - flowType, - clientProfile, - effectiveMode, - deviceContext, - tenant, - user?.IsAuthenticated ?? false, - user?.UserId, - sessionSecurityContext, - originalOptions, - effectiveOptions, - response, - primaryTokenKind - ); - } + // TODO: Implement invariant checker + //_invariantChecker.Validate(flowType, effectiveMode, response, effectiveOptions); + return new AuthFlowContext( + flowType, + clientProfile, + effectiveMode, + deviceContext, + tenant, + user?.IsAuthenticated ?? false, + user?.UserId, + sessionSecurityContext, + originalOptions, + effectiveOptions, + response, + primaryTokenKind + ); } + } diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthFlowEndpointFilter.cs b/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthFlowEndpointFilter.cs index f1e4bcef..2c6304d3 100644 --- a/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthFlowEndpointFilter.cs +++ b/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthFlowEndpointFilter.cs @@ -1,26 +1,24 @@ using Microsoft.AspNetCore.Http; -namespace CodeBeam.UltimateAuth.Server.Auth +namespace CodeBeam.UltimateAuth.Server.Auth; + +internal sealed class AuthFlowEndpointFilter : IEndpointFilter { - internal sealed class AuthFlowEndpointFilter : IEndpointFilter + private readonly IAuthFlow _authFlow; + + public AuthFlowEndpointFilter(IAuthFlow authFlow) { - private readonly IAuthFlow _authFlow; + _authFlow = authFlow; + } - public AuthFlowEndpointFilter(IAuthFlow authFlow) - { - _authFlow = authFlow; - } + public async ValueTask InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next) + { + var metadata = context.HttpContext.GetEndpoint()?.Metadata.GetMetadata(); - public async ValueTask InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next) + if (metadata != null) { - var metadata = context.HttpContext.GetEndpoint()?.Metadata.GetMetadata(); - - if (metadata != null) - { - await _authFlow.BeginAsync(metadata.FlowType); - } - return await next(context); + await _authFlow.BeginAsync(metadata.FlowType); } - + return await next(context); } } diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthFlowMetadata.cs b/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthFlowMetadata.cs index 8c46b33f..f02bdbfe 100644 --- a/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthFlowMetadata.cs +++ b/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthFlowMetadata.cs @@ -1,14 +1,13 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Server.Auth +namespace CodeBeam.UltimateAuth.Server.Auth; + +public sealed class AuthFlowMetadata { - public sealed class AuthFlowMetadata - { - public AuthFlowType FlowType { get; } + public AuthFlowType FlowType { get; } - public AuthFlowMetadata(AuthFlowType flowType) - { - FlowType = flowType; - } + public AuthFlowMetadata(AuthFlowType flowType) + { + FlowType = flowType; } } diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/Context/DefaultAuthContextFactory.cs b/src/CodeBeam.UltimateAuth.Server/Auth/Context/DefaultAuthContextFactory.cs deleted file mode 100644 index 8d11f2b9..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Auth/Context/DefaultAuthContextFactory.cs +++ /dev/null @@ -1,24 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Abstractions; -using CodeBeam.UltimateAuth.Core.Contracts; -using CodeBeam.UltimateAuth.Server.Extensions; - -namespace CodeBeam.UltimateAuth.Server.Auth -{ - internal sealed class DefaultAuthContextFactory : IAuthContextFactory - { - private readonly IAuthFlowContextAccessor _flow; - private readonly IClock _clock; - - public DefaultAuthContextFactory(IAuthFlowContextAccessor flow, IClock clock) - { - _flow = flow; - _clock = clock; - } - - public AuthContext Create(DateTimeOffset? at = null) - { - var flow = _flow.Current; - return flow.ToAuthContext(at ?? _clock.UtcNow); - } - } -} diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/Context/DefaultAuthFlow.cs b/src/CodeBeam.UltimateAuth.Server/Auth/Context/DefaultAuthFlow.cs deleted file mode 100644 index 2340fd38..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Auth/Context/DefaultAuthFlow.cs +++ /dev/null @@ -1,30 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Domain; -using Microsoft.AspNetCore.Http; - -namespace CodeBeam.UltimateAuth.Server.Auth -{ - internal sealed class DefaultAuthFlow : IAuthFlow - { - private readonly IHttpContextAccessor _http; - private readonly IAuthFlowContextFactory _factory; - private readonly DefaultAuthFlowContextAccessor _accessor; - - public DefaultAuthFlow(IHttpContextAccessor http, IAuthFlowContextFactory factory, IAuthFlowContextAccessor accessor) - { - _http = http; - _factory = factory; - _accessor = (DefaultAuthFlowContextAccessor)accessor; - } - - public async ValueTask BeginAsync(AuthFlowType flowType, CancellationToken ct = default) - { - var ctx = _http.HttpContext ?? throw new InvalidOperationException("No HttpContext."); - - var flowContext = await _factory.CreateAsync(ctx, flowType); - _accessor.Set(flowContext); - - return flowContext; - } - - } -} diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/Context/IAuthFlow.cs b/src/CodeBeam.UltimateAuth.Server/Auth/Context/IAuthFlow.cs index 41c6e1e9..1584acb0 100644 --- a/src/CodeBeam.UltimateAuth.Server/Auth/Context/IAuthFlow.cs +++ b/src/CodeBeam.UltimateAuth.Server/Auth/Context/IAuthFlow.cs @@ -1,9 +1,8 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Server.Auth +namespace CodeBeam.UltimateAuth.Server.Auth; + +public interface IAuthFlow { - public interface IAuthFlow - { - ValueTask BeginAsync(AuthFlowType flowType, CancellationToken ct = default); - } + ValueTask BeginAsync(AuthFlowType flowType, CancellationToken ct = default); } diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/Context/IAuthFlowContextFactory.cs b/src/CodeBeam.UltimateAuth.Server/Auth/Context/IAuthFlowContextFactory.cs new file mode 100644 index 00000000..2aff1465 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Auth/Context/IAuthFlowContextFactory.cs @@ -0,0 +1,9 @@ +using CodeBeam.UltimateAuth.Core.Domain; +using Microsoft.AspNetCore.Http; + +namespace CodeBeam.UltimateAuth.Server.Auth; + +public interface IAuthFlowContextFactory +{ + ValueTask CreateAsync(HttpContext httpContext, AuthFlowType flowType, CancellationToken ct = default); +} diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/DefaultClientProfileReader.cs b/src/CodeBeam.UltimateAuth.Server/Auth/DefaultClientProfileReader.cs deleted file mode 100644 index 0ef907c7..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Auth/DefaultClientProfileReader.cs +++ /dev/null @@ -1,33 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Options; -using Microsoft.AspNetCore.Http; - -namespace CodeBeam.UltimateAuth.Server.Auth -{ - internal sealed class DefaultClientProfileReader : IClientProfileReader - { - private const string HeaderName = "X-UAuth-ClientProfile"; - private const string FormFieldName = "__uauth_client_profile"; - - public UAuthClientProfile Read(HttpContext context) - { - if (context.Request.Headers.TryGetValue(HeaderName, out var headerValue) && TryParse(headerValue, out var headerProfile)) - { - return headerProfile; - } - - if (context.Request.HasFormContentType && context.Request.Form.TryGetValue(FormFieldName, out var formValue) && - TryParse(formValue, out var formProfile)) - { - return formProfile; - } - - return UAuthClientProfile.NotSpecified; - } - - private static bool TryParse(string value, out UAuthClientProfile profile) - { - return Enum.TryParse(value, ignoreCase: true, out profile); - } - - } -} diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/DefaultEffectiveServerOptionsProvider.cs b/src/CodeBeam.UltimateAuth.Server/Auth/DefaultEffectiveServerOptionsProvider.cs deleted file mode 100644 index c8188cf1..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Auth/DefaultEffectiveServerOptionsProvider.cs +++ /dev/null @@ -1,47 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Domain; -using CodeBeam.UltimateAuth.Core.Options; -using CodeBeam.UltimateAuth.Server.Auth; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Options; - -namespace CodeBeam.UltimateAuth.Server.Options -{ - internal sealed class DefaultEffectiveServerOptionsProvider : IEffectiveServerOptionsProvider - { - private readonly IOptions _baseOptions; - private readonly IEffectiveAuthModeResolver _modeResolver; - - public DefaultEffectiveServerOptionsProvider(IOptions baseOptions, IEffectiveAuthModeResolver modeResolver) - { - _baseOptions = baseOptions; - _modeResolver = modeResolver; - } - - public UAuthServerOptions GetOriginal(HttpContext context) - { - return _baseOptions.Value; - } - - public EffectiveUAuthServerOptions GetEffective(HttpContext context, AuthFlowType flowType, UAuthClientProfile clientProfile) - { - var original = _baseOptions.Value; - var effectiveMode = _modeResolver.Resolve(original.Mode, clientProfile, flowType); - var options = original.Clone(); - options.Mode = effectiveMode; - - ConfigureDefaults.ApplyModeDefaults(options); - - if (original.ModeConfigurations.TryGetValue(effectiveMode, out var configure)) - { - configure(options); - } - - return new EffectiveUAuthServerOptions - { - Mode = effectiveMode, - Options = options - }; - } - - } -} diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/DefaultPrimaryTokenResolver.cs b/src/CodeBeam.UltimateAuth.Server/Auth/DefaultPrimaryTokenResolver.cs deleted file mode 100644 index 8d8ff763..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Auth/DefaultPrimaryTokenResolver.cs +++ /dev/null @@ -1,21 +0,0 @@ -using CodeBeam.UltimateAuth.Core; -using CodeBeam.UltimateAuth.Core.Contracts; - -namespace CodeBeam.UltimateAuth.Server.Auth -{ - internal sealed class DefaultPrimaryTokenResolver : IPrimaryTokenResolver - { - public PrimaryTokenKind Resolve(UAuthMode effectiveMode) - { - return effectiveMode switch - { - UAuthMode.PureOpaque => PrimaryTokenKind.Session, - UAuthMode.Hybrid => PrimaryTokenKind.Session, - UAuthMode.SemiHybrid => PrimaryTokenKind.AccessToken, - UAuthMode.PureJwt => PrimaryTokenKind.AccessToken, - _ => throw new InvalidOperationException( - $"Unsupported auth mode: {effectiveMode}") - }; - } - } -} diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/EffectiveServerOptionsProvider.cs b/src/CodeBeam.UltimateAuth.Server/Auth/EffectiveServerOptionsProvider.cs new file mode 100644 index 00000000..5f93b4c0 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Auth/EffectiveServerOptionsProvider.cs @@ -0,0 +1,46 @@ +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.Options; +using CodeBeam.UltimateAuth.Server.Auth; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; + +namespace CodeBeam.UltimateAuth.Server.Options; + +internal sealed class EffectiveServerOptionsProvider : IEffectiveServerOptionsProvider +{ + private readonly IOptions _baseOptions; + private readonly IEffectiveAuthModeResolver _modeResolver; + + public EffectiveServerOptionsProvider(IOptions baseOptions, IEffectiveAuthModeResolver modeResolver) + { + _baseOptions = baseOptions; + _modeResolver = modeResolver; + } + + public UAuthServerOptions GetOriginal(HttpContext context) + { + return _baseOptions.Value; + } + + public EffectiveUAuthServerOptions GetEffective(HttpContext context, AuthFlowType flowType, UAuthClientProfile clientProfile) + { + var original = _baseOptions.Value; + var effectiveMode = _modeResolver.Resolve(original.Mode, clientProfile, flowType); + var options = original.Clone(); + options.Mode = effectiveMode; + + ConfigureDefaults.ApplyModeDefaults(options); + + if (original.ModeConfigurations.TryGetValue(effectiveMode, out var configure)) + { + configure(options); + } + + return new EffectiveUAuthServerOptions + { + Mode = effectiveMode, + Options = options + }; + } + +} diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/EffectiveUAuthServerOptions.cs b/src/CodeBeam.UltimateAuth.Server/Auth/EffectiveUAuthServerOptions.cs index cf76608c..22cf6f84 100644 --- a/src/CodeBeam.UltimateAuth.Server/Auth/EffectiveUAuthServerOptions.cs +++ b/src/CodeBeam.UltimateAuth.Server/Auth/EffectiveUAuthServerOptions.cs @@ -1,17 +1,16 @@ using CodeBeam.UltimateAuth.Core; using CodeBeam.UltimateAuth.Server.Options; -namespace CodeBeam.UltimateAuth.Server.Auth +namespace CodeBeam.UltimateAuth.Server.Auth; + +public sealed class EffectiveUAuthServerOptions { - public sealed class EffectiveUAuthServerOptions - { - public UAuthMode Mode { get; init; } + public UAuthMode Mode { get; init; } - /// - /// Cloned, per-request server options - /// - public UAuthServerOptions Options { get; init; } = default!; + /// + /// Cloned, per-request server options + /// + public UAuthServerOptions Options { get; init; } = default!; - public AuthResponseOptions AuthResponse => Options.AuthResponse; - } + public AuthResponseOptions AuthResponse => Options.AuthResponse; } diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/IClientProfileReader.cs b/src/CodeBeam.UltimateAuth.Server/Auth/IClientProfileReader.cs index 5b49da9b..e64d3d7a 100644 --- a/src/CodeBeam.UltimateAuth.Server/Auth/IClientProfileReader.cs +++ b/src/CodeBeam.UltimateAuth.Server/Auth/IClientProfileReader.cs @@ -1,10 +1,9 @@ using CodeBeam.UltimateAuth.Core.Options; using Microsoft.AspNetCore.Http; -namespace CodeBeam.UltimateAuth.Server.Auth +namespace CodeBeam.UltimateAuth.Server.Auth; + +public interface IClientProfileReader { - public interface IClientProfileReader - { - UAuthClientProfile Read(HttpContext context); - } + UAuthClientProfile Read(HttpContext context); } diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/IPrimaryTokenResolver.cs b/src/CodeBeam.UltimateAuth.Server/Auth/IPrimaryTokenResolver.cs index 248bfaf0..b53d9ef8 100644 --- a/src/CodeBeam.UltimateAuth.Server/Auth/IPrimaryTokenResolver.cs +++ b/src/CodeBeam.UltimateAuth.Server/Auth/IPrimaryTokenResolver.cs @@ -1,10 +1,9 @@ using CodeBeam.UltimateAuth.Core; using CodeBeam.UltimateAuth.Core.Contracts; -namespace CodeBeam.UltimateAuth.Server.Auth +namespace CodeBeam.UltimateAuth.Server.Auth; + +public interface IPrimaryTokenResolver { - public interface IPrimaryTokenResolver - { - PrimaryTokenKind Resolve(UAuthMode effectiveMode); - } + PrimaryTokenKind Resolve(UAuthMode effectiveMode); } diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/PrimaryTokenResolver.cs b/src/CodeBeam.UltimateAuth.Server/Auth/PrimaryTokenResolver.cs new file mode 100644 index 00000000..8e5baa88 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Auth/PrimaryTokenResolver.cs @@ -0,0 +1,20 @@ +using CodeBeam.UltimateAuth.Core; +using CodeBeam.UltimateAuth.Core.Contracts; + +namespace CodeBeam.UltimateAuth.Server.Auth; + +internal sealed class PrimaryTokenResolver : IPrimaryTokenResolver +{ + public PrimaryTokenKind Resolve(UAuthMode effectiveMode) + { + return effectiveMode switch + { + UAuthMode.PureOpaque => PrimaryTokenKind.Session, + UAuthMode.Hybrid => PrimaryTokenKind.Session, + UAuthMode.SemiHybrid => PrimaryTokenKind.AccessToken, + UAuthMode.PureJwt => PrimaryTokenKind.AccessToken, + _ => throw new InvalidOperationException( + $"Unsupported auth mode: {effectiveMode}") + }; + } +} diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/Response/AuthResponseOptionsModeTemplateResolver.cs b/src/CodeBeam.UltimateAuth.Server/Auth/Response/AuthResponseOptionsModeTemplateResolver.cs index 0fc05b22..8eae3aba 100644 --- a/src/CodeBeam.UltimateAuth.Server/Auth/Response/AuthResponseOptionsModeTemplateResolver.cs +++ b/src/CodeBeam.UltimateAuth.Server/Auth/Response/AuthResponseOptionsModeTemplateResolver.cs @@ -4,132 +4,131 @@ using CodeBeam.UltimateAuth.Core.Options; using CodeBeam.UltimateAuth.Server.Options; -namespace CodeBeam.UltimateAuth.Server.Auth +namespace CodeBeam.UltimateAuth.Server.Auth; + +internal sealed class AuthResponseOptionsModeTemplateResolver { - internal sealed class AuthResponseOptionsModeTemplateResolver + public AuthResponseOptions Resolve(UAuthMode mode, AuthFlowType flowType) { - public AuthResponseOptions Resolve(UAuthMode mode, AuthFlowType flowType) + return mode switch { - return mode switch - { - UAuthMode.PureOpaque => PureOpaque(flowType), - UAuthMode.Hybrid => Hybrid(flowType), - UAuthMode.SemiHybrid => SemiHybrid(flowType), - UAuthMode.PureJwt => PureJwt(flowType), - _ => throw new InvalidOperationException($"Unsupported mode: {mode}") - }; - } + UAuthMode.PureOpaque => PureOpaque(flowType), + UAuthMode.Hybrid => Hybrid(flowType), + UAuthMode.SemiHybrid => SemiHybrid(flowType), + UAuthMode.PureJwt => PureJwt(flowType), + _ => throw new InvalidOperationException($"Unsupported mode: {mode}") + }; + } - private static AuthResponseOptions PureOpaque(AuthFlowType flow) - => new() + private static AuthResponseOptions PureOpaque(AuthFlowType flow) + => new() + { + SessionIdDelivery = new() { - SessionIdDelivery = new() - { - Name = "uas", - Kind = CredentialKind.Session, - TokenFormat = TokenFormat.Opaque, - Mode = TokenResponseMode.Cookie, - }, - AccessTokenDelivery = new() - { - Name = "uat", - Kind = CredentialKind.AccessToken, - TokenFormat = TokenFormat.Opaque, - Mode = TokenResponseMode.None - }, - RefreshTokenDelivery = new() - { - Name = "uar", - Kind = CredentialKind.RefreshToken, - TokenFormat = TokenFormat.Opaque, - Mode = TokenResponseMode.None - }, - Login = { RedirectEnabled = true }, - Logout = { RedirectEnabled = true } - }; + Name = "uas", + Kind = CredentialKind.Session, + TokenFormat = TokenFormat.Opaque, + Mode = TokenResponseMode.Cookie, + }, + AccessTokenDelivery = new() + { + Name = "uat", + Kind = CredentialKind.AccessToken, + TokenFormat = TokenFormat.Opaque, + Mode = TokenResponseMode.None + }, + RefreshTokenDelivery = new() + { + Name = "uar", + Kind = CredentialKind.RefreshToken, + TokenFormat = TokenFormat.Opaque, + Mode = TokenResponseMode.None + }, + Login = { RedirectEnabled = true }, + Logout = { RedirectEnabled = true } + }; - private static AuthResponseOptions Hybrid(AuthFlowType flow) - => new() + private static AuthResponseOptions Hybrid(AuthFlowType flow) + => new() + { + SessionIdDelivery = new() + { + Name = "uas", + Kind = CredentialKind.Session, + TokenFormat = TokenFormat.Opaque, + Mode = TokenResponseMode.Cookie + }, + AccessTokenDelivery = new() { - SessionIdDelivery = new() - { - Name = "uas", - Kind = CredentialKind.Session, - TokenFormat = TokenFormat.Opaque, - Mode = TokenResponseMode.Cookie - }, - AccessTokenDelivery = new() - { - Name = "uat", - Kind = CredentialKind.AccessToken, - TokenFormat = TokenFormat.Jwt, - Mode = TokenResponseMode.Header - }, - RefreshTokenDelivery = new() - { - Name = "uar", - Kind = CredentialKind.RefreshToken, - TokenFormat = TokenFormat.Opaque, - Mode = TokenResponseMode.Cookie - }, - Login = { RedirectEnabled = true }, - Logout = { RedirectEnabled = true } - }; + Name = "uat", + Kind = CredentialKind.AccessToken, + TokenFormat = TokenFormat.Jwt, + Mode = TokenResponseMode.Header + }, + RefreshTokenDelivery = new() + { + Name = "uar", + Kind = CredentialKind.RefreshToken, + TokenFormat = TokenFormat.Opaque, + Mode = TokenResponseMode.Cookie + }, + Login = { RedirectEnabled = true }, + Logout = { RedirectEnabled = true } + }; - private static AuthResponseOptions SemiHybrid(AuthFlowType flow) - => new() + private static AuthResponseOptions SemiHybrid(AuthFlowType flow) + => new() + { + SessionIdDelivery = new() + { + Name = "uas", + Kind = CredentialKind.Session, + TokenFormat = TokenFormat.Opaque, + Mode = TokenResponseMode.None + }, + AccessTokenDelivery = new() + { + Name = "uat", + Kind = CredentialKind.AccessToken, + TokenFormat = TokenFormat.Jwt, + Mode = TokenResponseMode.Header + }, + RefreshTokenDelivery = new() { - SessionIdDelivery = new() - { - Name = "uas", - Kind = CredentialKind.Session, - TokenFormat = TokenFormat.Opaque, - Mode = TokenResponseMode.None - }, - AccessTokenDelivery = new() - { - Name = "uat", - Kind = CredentialKind.AccessToken, - TokenFormat = TokenFormat.Jwt, - Mode = TokenResponseMode.Header - }, - RefreshTokenDelivery = new() - { - Name = "uar", - Kind = CredentialKind.RefreshToken, - TokenFormat = TokenFormat.Opaque, - Mode = TokenResponseMode.Header - }, - Login = { RedirectEnabled = true }, - Logout = { RedirectEnabled = true } - }; + Name = "uar", + Kind = CredentialKind.RefreshToken, + TokenFormat = TokenFormat.Opaque, + Mode = TokenResponseMode.Header + }, + Login = { RedirectEnabled = true }, + Logout = { RedirectEnabled = true } + }; - private static AuthResponseOptions PureJwt(AuthFlowType flow) - => new() + private static AuthResponseOptions PureJwt(AuthFlowType flow) + => new() + { + SessionIdDelivery = new() { - SessionIdDelivery = new() - { - Name = "uas", - Kind = CredentialKind.Session, - TokenFormat = TokenFormat.Opaque, - Mode = TokenResponseMode.None - }, - AccessTokenDelivery = new() - { - Name = "uat", - Kind = CredentialKind.AccessToken, - TokenFormat = TokenFormat.Jwt, - Mode = TokenResponseMode.Header - }, - RefreshTokenDelivery = new() - { - Name = "uar", - Kind = CredentialKind.RefreshToken, - TokenFormat = TokenFormat.Opaque, - Mode = TokenResponseMode.Header - }, - Login = { RedirectEnabled = true }, - Logout = { RedirectEnabled = true } - }; - } + Name = "uas", + Kind = CredentialKind.Session, + TokenFormat = TokenFormat.Opaque, + Mode = TokenResponseMode.None + }, + AccessTokenDelivery = new() + { + Name = "uat", + Kind = CredentialKind.AccessToken, + TokenFormat = TokenFormat.Jwt, + Mode = TokenResponseMode.Header + }, + RefreshTokenDelivery = new() + { + Name = "uar", + Kind = CredentialKind.RefreshToken, + TokenFormat = TokenFormat.Opaque, + Mode = TokenResponseMode.Header + }, + Login = { RedirectEnabled = true }, + Logout = { RedirectEnabled = true } + }; } diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/Response/AuthResponseResolver.cs b/src/CodeBeam.UltimateAuth.Server/Auth/Response/AuthResponseResolver.cs new file mode 100644 index 00000000..8306b0e3 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Auth/Response/AuthResponseResolver.cs @@ -0,0 +1,93 @@ +using CodeBeam.UltimateAuth.Core; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.Options; +using CodeBeam.UltimateAuth.Server.Options; + +namespace CodeBeam.UltimateAuth.Server.Auth; + +internal sealed class AuthResponseResolver : IAuthResponseResolver +{ + private readonly AuthResponseOptionsModeTemplateResolver _template; + private readonly ClientProfileAuthResponseAdapter _adapter; + + public AuthResponseResolver(AuthResponseOptionsModeTemplateResolver template, ClientProfileAuthResponseAdapter adapter) + { + _template = template; + _adapter = adapter; + } + + public EffectiveAuthResponse Resolve(UAuthMode effectiveMode, AuthFlowType flowType, UAuthClientProfile clientProfile, EffectiveUAuthServerOptions effectiveOptions) + { + var template = _template.Resolve(effectiveMode, flowType); + var adapted = _adapter.Adapt(template, clientProfile, effectiveMode, effectiveOptions); + + var bound = BindCookies(adapted, effectiveOptions.Options); + // TODO: This is currently implicit + Validate(bound); + + return new EffectiveAuthResponse( + bound.SessionIdDelivery, + bound.AccessTokenDelivery, + bound.RefreshTokenDelivery, + + new EffectiveLoginRedirectResponse( + bound.Login.RedirectEnabled, + bound.Login.SuccessRedirect, + bound.Login.FailureRedirect, + bound.Login.FailureQueryKey, + bound.Login.CodeQueryKey, + bound.Login.FailureCodes + ), + + new EffectiveLogoutRedirectResponse( + bound.Logout.RedirectEnabled, + bound.Logout.RedirectUrl, + bound.Logout.AllowReturnUrlOverride + ) + ); + } + + private static AuthResponseOptions BindCookies(AuthResponseOptions response, UAuthServerOptions server) + { + return new AuthResponseOptions + { + SessionIdDelivery = Bind(response.SessionIdDelivery, server), + AccessTokenDelivery = Bind(response.AccessTokenDelivery, server), + RefreshTokenDelivery = Bind(response.RefreshTokenDelivery, server), + Login = response.Login, + Logout = response.Logout + }; + } + + private static CredentialResponseOptions Bind(CredentialResponseOptions delivery, UAuthServerOptions server) + { + if (delivery.Mode != TokenResponseMode.Cookie) + return delivery; + + var cookie = delivery.Kind switch + { + CredentialKind.Session => server.Cookie.Session, + CredentialKind.AccessToken => server.Cookie.AccessToken, + CredentialKind.RefreshToken => server.Cookie.RefreshToken, + _ => throw new InvalidOperationException($"Unsupported credential kind: {delivery.Kind}") + }; + + return delivery.WithCookie(cookie); + } + + private static void Validate(AuthResponseOptions response) + { + ValidateDelivery(response.SessionIdDelivery); + ValidateDelivery(response.AccessTokenDelivery); + ValidateDelivery(response.RefreshTokenDelivery); + } + + private static void ValidateDelivery(CredentialResponseOptions delivery) + { + if (delivery.Mode == TokenResponseMode.Cookie && delivery.Cookie is null) + { + throw new InvalidOperationException($"Credential '{delivery.Kind}' is configured as Cookie but no cookie options were bound."); + } + } + +} diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/Response/ClientProfileAuthResponseAdapter.cs b/src/CodeBeam.UltimateAuth.Server/Auth/Response/ClientProfileAuthResponseAdapter.cs index 5f97c238..16e75fdb 100644 --- a/src/CodeBeam.UltimateAuth.Server/Auth/Response/ClientProfileAuthResponseAdapter.cs +++ b/src/CodeBeam.UltimateAuth.Server/Auth/Response/ClientProfileAuthResponseAdapter.cs @@ -4,52 +4,51 @@ using CodeBeam.UltimateAuth.Core.Options; using CodeBeam.UltimateAuth.Server.Options; -namespace CodeBeam.UltimateAuth.Server.Auth +namespace CodeBeam.UltimateAuth.Server.Auth; + +internal sealed class ClientProfileAuthResponseAdapter { - internal sealed class ClientProfileAuthResponseAdapter + public AuthResponseOptions Adapt(AuthResponseOptions template, UAuthClientProfile clientProfile, UAuthMode effectiveMode, EffectiveUAuthServerOptions effectiveOptions) { - public AuthResponseOptions Adapt(AuthResponseOptions template, UAuthClientProfile clientProfile, UAuthMode effectiveMode, EffectiveUAuthServerOptions effectiveOptions) - { - return new AuthResponseOptions - { - SessionIdDelivery = AdaptCredential(template.SessionIdDelivery, CredentialKind.Session, clientProfile), - AccessTokenDelivery = AdaptCredential(template.AccessTokenDelivery, CredentialKind.AccessToken, clientProfile), - RefreshTokenDelivery = AdaptCredential(template.RefreshTokenDelivery, CredentialKind.RefreshToken, clientProfile), - - Login = template.Login, - Logout = template.Logout - }; - } - - // NOTE: - // effectiveMode and effectiveOptions are intentionally passed - // to keep this adapter policy-extensible. - // They will be used for future mode/option based response enforcement. - private static CredentialResponseOptions AdaptCredential(CredentialResponseOptions original, CredentialKind kind, UAuthClientProfile clientProfile) + return new AuthResponseOptions { - if (clientProfile == UAuthClientProfile.Maui && original.Mode == TokenResponseMode.Cookie) - { - return ToHeader(original); - } + SessionIdDelivery = AdaptCredential(template.SessionIdDelivery, CredentialKind.Session, clientProfile), + AccessTokenDelivery = AdaptCredential(template.AccessTokenDelivery, CredentialKind.AccessToken, clientProfile), + RefreshTokenDelivery = AdaptCredential(template.RefreshTokenDelivery, CredentialKind.RefreshToken, clientProfile), - if (original.TokenFormat == TokenFormat.Jwt && original.Mode == TokenResponseMode.Cookie) - { - return ToHeader(original); - } + Login = template.Login, + Logout = template.Logout + }; + } - return original; + // NOTE: + // effectiveMode and effectiveOptions are intentionally passed + // to keep this adapter policy-extensible. + // They will be used for future mode/option based response enforcement. + private static CredentialResponseOptions AdaptCredential(CredentialResponseOptions original, CredentialKind kind, UAuthClientProfile clientProfile) + { + if (clientProfile == UAuthClientProfile.Maui && original.Mode == TokenResponseMode.Cookie) + { + return ToHeader(original); } - private static CredentialResponseOptions ToHeader(CredentialResponseOptions original) + if (original.TokenFormat == TokenFormat.Jwt && original.Mode == TokenResponseMode.Cookie) { - return new CredentialResponseOptions - { - TokenFormat = original.TokenFormat, - Mode = TokenResponseMode.Header, - HeaderFormat = HeaderTokenFormat.Bearer, - Name = original.Name - }; + return ToHeader(original); } + return original; + } + + private static CredentialResponseOptions ToHeader(CredentialResponseOptions original) + { + return new CredentialResponseOptions + { + TokenFormat = original.TokenFormat, + Mode = TokenResponseMode.Header, + HeaderFormat = HeaderTokenFormat.Bearer, + Name = original.Name + }; } + } diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/Response/DefaultAuthResponseResolver.cs b/src/CodeBeam.UltimateAuth.Server/Auth/Response/DefaultAuthResponseResolver.cs deleted file mode 100644 index 705099e0..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Auth/Response/DefaultAuthResponseResolver.cs +++ /dev/null @@ -1,94 +0,0 @@ -using CodeBeam.UltimateAuth.Core; -using CodeBeam.UltimateAuth.Core.Domain; -using CodeBeam.UltimateAuth.Core.Options; -using CodeBeam.UltimateAuth.Server.Options; - -namespace CodeBeam.UltimateAuth.Server.Auth -{ - internal sealed class DefaultAuthResponseResolver : IAuthResponseResolver - { - private readonly AuthResponseOptionsModeTemplateResolver _template; - private readonly ClientProfileAuthResponseAdapter _adapter; - - public DefaultAuthResponseResolver(AuthResponseOptionsModeTemplateResolver template, ClientProfileAuthResponseAdapter adapter) - { - _template = template; - _adapter = adapter; - } - - public EffectiveAuthResponse Resolve(UAuthMode effectiveMode, AuthFlowType flowType, UAuthClientProfile clientProfile, EffectiveUAuthServerOptions effectiveOptions) - { - var template = _template.Resolve(effectiveMode, flowType); - var adapted = _adapter.Adapt(template, clientProfile, effectiveMode, effectiveOptions); - - var bound = BindCookies(adapted, effectiveOptions.Options); - // TODO: This is currently implicit - Validate(bound); - - return new EffectiveAuthResponse( - bound.SessionIdDelivery, - bound.AccessTokenDelivery, - bound.RefreshTokenDelivery, - - new EffectiveLoginRedirectResponse( - bound.Login.RedirectEnabled, - bound.Login.SuccessRedirect, - bound.Login.FailureRedirect, - bound.Login.FailureQueryKey, - bound.Login.CodeQueryKey, - bound.Login.FailureCodes - ), - - new EffectiveLogoutRedirectResponse( - bound.Logout.RedirectEnabled, - bound.Logout.RedirectUrl, - bound.Logout.AllowReturnUrlOverride - ) - ); - } - - private static AuthResponseOptions BindCookies(AuthResponseOptions response, UAuthServerOptions server) - { - return new AuthResponseOptions - { - SessionIdDelivery = Bind(response.SessionIdDelivery, server), - AccessTokenDelivery = Bind(response.AccessTokenDelivery, server), - RefreshTokenDelivery = Bind(response.RefreshTokenDelivery, server), - Login = response.Login, - Logout = response.Logout - }; - } - - private static CredentialResponseOptions Bind(CredentialResponseOptions delivery, UAuthServerOptions server) - { - if (delivery.Mode != TokenResponseMode.Cookie) - return delivery; - - var cookie = delivery.Kind switch - { - CredentialKind.Session => server.Cookie.Session, - CredentialKind.AccessToken => server.Cookie.AccessToken, - CredentialKind.RefreshToken => server.Cookie.RefreshToken, - _ => throw new InvalidOperationException($"Unsupported credential kind: {delivery.Kind}") - }; - - return delivery.WithCookie(cookie); - } - - private static void Validate(AuthResponseOptions response) - { - ValidateDelivery(response.SessionIdDelivery); - ValidateDelivery(response.AccessTokenDelivery); - ValidateDelivery(response.RefreshTokenDelivery); - } - - private static void ValidateDelivery(CredentialResponseOptions delivery) - { - if (delivery.Mode == TokenResponseMode.Cookie && delivery.Cookie is null) - { - throw new InvalidOperationException($"Credential '{delivery.Kind}' is configured as Cookie but no cookie options were bound."); - } - } - - } -} diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/Response/DefaultEffectiveAuthModeResolver.cs b/src/CodeBeam.UltimateAuth.Server/Auth/Response/DefaultEffectiveAuthModeResolver.cs deleted file mode 100644 index 6c2ad30a..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Auth/Response/DefaultEffectiveAuthModeResolver.cs +++ /dev/null @@ -1,26 +0,0 @@ -using CodeBeam.UltimateAuth.Core; -using CodeBeam.UltimateAuth.Core.Domain; -using CodeBeam.UltimateAuth.Core.Options; - -namespace CodeBeam.UltimateAuth.Server.Auth -{ - internal sealed class DefaultEffectiveAuthModeResolver : IEffectiveAuthModeResolver - { - public UAuthMode Resolve(UAuthMode? configuredMode, UAuthClientProfile clientProfile, AuthFlowType flowType) - { - if (configuredMode.HasValue) - return configuredMode.Value; - - return clientProfile switch - { - UAuthClientProfile.BlazorServer => UAuthMode.PureOpaque, - UAuthClientProfile.WebServer => UAuthMode.Hybrid, - UAuthClientProfile.BlazorWasm => UAuthMode.Hybrid, - UAuthClientProfile.Maui => UAuthMode.Hybrid, - UAuthClientProfile.Api => UAuthMode.PureJwt, - _ => UAuthMode.Hybrid - }; - } - } - -} diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/Response/EffectiveAuthModeResolver.cs b/src/CodeBeam.UltimateAuth.Server/Auth/Response/EffectiveAuthModeResolver.cs new file mode 100644 index 00000000..35422857 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Auth/Response/EffectiveAuthModeResolver.cs @@ -0,0 +1,24 @@ +using CodeBeam.UltimateAuth.Core; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.Options; + +namespace CodeBeam.UltimateAuth.Server.Auth; + +internal sealed class EffectiveAuthModeResolver : IEffectiveAuthModeResolver +{ + public UAuthMode Resolve(UAuthMode? configuredMode, UAuthClientProfile clientProfile, AuthFlowType flowType) + { + if (configuredMode.HasValue) + return configuredMode.Value; + + return clientProfile switch + { + UAuthClientProfile.BlazorServer => UAuthMode.PureOpaque, + UAuthClientProfile.WebServer => UAuthMode.Hybrid, + UAuthClientProfile.BlazorWasm => UAuthMode.Hybrid, + UAuthClientProfile.Maui => UAuthMode.Hybrid, + UAuthClientProfile.Api => UAuthMode.PureJwt, + _ => UAuthMode.Hybrid + }; + } +} diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/Response/EffectiveAuthResponse.cs b/src/CodeBeam.UltimateAuth.Server/Auth/Response/EffectiveAuthResponse.cs index 24da3658..b0280bed 100644 --- a/src/CodeBeam.UltimateAuth.Server/Auth/Response/EffectiveAuthResponse.cs +++ b/src/CodeBeam.UltimateAuth.Server/Auth/Response/EffectiveAuthResponse.cs @@ -1,12 +1,11 @@ using CodeBeam.UltimateAuth.Server.Options; -namespace CodeBeam.UltimateAuth.Server.Auth -{ - public sealed record EffectiveAuthResponse( - CredentialResponseOptions SessionIdDelivery, - CredentialResponseOptions AccessTokenDelivery, - CredentialResponseOptions RefreshTokenDelivery, - EffectiveLoginRedirectResponse Login, - EffectiveLogoutRedirectResponse Logout - ); -} +namespace CodeBeam.UltimateAuth.Server.Auth; + +public sealed record EffectiveAuthResponse( + CredentialResponseOptions SessionIdDelivery, + CredentialResponseOptions AccessTokenDelivery, + CredentialResponseOptions RefreshTokenDelivery, + EffectiveLoginRedirectResponse Login, + EffectiveLogoutRedirectResponse Logout +); diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/Response/EffectiveLoginRedirectResponse.cs b/src/CodeBeam.UltimateAuth.Server/Auth/Response/EffectiveLoginRedirectResponse.cs index 75d01ad4..8408cadd 100644 --- a/src/CodeBeam.UltimateAuth.Server/Auth/Response/EffectiveLoginRedirectResponse.cs +++ b/src/CodeBeam.UltimateAuth.Server/Auth/Response/EffectiveLoginRedirectResponse.cs @@ -1,14 +1,13 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Server.Auth -{ - public sealed record EffectiveLoginRedirectResponse - ( - bool RedirectEnabled, - string SuccessPath, - string FailurePath, - string FailureQueryKey, - string CodeQueryKey, - IReadOnlyDictionary FailureCodes - ); -} +namespace CodeBeam.UltimateAuth.Server.Auth; + +public sealed record EffectiveLoginRedirectResponse +( + bool RedirectEnabled, + string SuccessPath, + string FailurePath, + string FailureQueryKey, + string CodeQueryKey, + IReadOnlyDictionary FailureCodes +); diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/Response/EffectiveLogoutRedirectResponse.cs b/src/CodeBeam.UltimateAuth.Server/Auth/Response/EffectiveLogoutRedirectResponse.cs index 34b4a672..f042e790 100644 --- a/src/CodeBeam.UltimateAuth.Server/Auth/Response/EffectiveLogoutRedirectResponse.cs +++ b/src/CodeBeam.UltimateAuth.Server/Auth/Response/EffectiveLogoutRedirectResponse.cs @@ -1,9 +1,8 @@ -namespace CodeBeam.UltimateAuth.Server.Auth -{ - public sealed record EffectiveLogoutRedirectResponse - ( - bool RedirectEnabled, - string RedirectPath, - bool AllowReturnUrlOverride - ); -} +namespace CodeBeam.UltimateAuth.Server.Auth; + +public sealed record EffectiveLogoutRedirectResponse +( + bool RedirectEnabled, + string RedirectPath, + bool AllowReturnUrlOverride +); diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/Response/IAuthResponseResolver.cs b/src/CodeBeam.UltimateAuth.Server/Auth/Response/IAuthResponseResolver.cs index 92f73fd6..080ca5a7 100644 --- a/src/CodeBeam.UltimateAuth.Server/Auth/Response/IAuthResponseResolver.cs +++ b/src/CodeBeam.UltimateAuth.Server/Auth/Response/IAuthResponseResolver.cs @@ -2,10 +2,9 @@ using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Core.Options; -namespace CodeBeam.UltimateAuth.Server.Auth +namespace CodeBeam.UltimateAuth.Server.Auth; + +public interface IAuthResponseResolver { - public interface IAuthResponseResolver - { - EffectiveAuthResponse Resolve(UAuthMode effectiveMode, AuthFlowType flowType, UAuthClientProfile clientProfile, EffectiveUAuthServerOptions effectiveOptions); - } + EffectiveAuthResponse Resolve(UAuthMode effectiveMode, AuthFlowType flowType, UAuthClientProfile clientProfile, EffectiveUAuthServerOptions effectiveOptions); } diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/Response/IEffectiveAuthModeResolver.cs b/src/CodeBeam.UltimateAuth.Server/Auth/Response/IEffectiveAuthModeResolver.cs index 6de3a100..2477a12d 100644 --- a/src/CodeBeam.UltimateAuth.Server/Auth/Response/IEffectiveAuthModeResolver.cs +++ b/src/CodeBeam.UltimateAuth.Server/Auth/Response/IEffectiveAuthModeResolver.cs @@ -2,10 +2,9 @@ using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Core.Options; -namespace CodeBeam.UltimateAuth.Server.Auth +namespace CodeBeam.UltimateAuth.Server.Auth; + +public interface IEffectiveAuthModeResolver { - public interface IEffectiveAuthModeResolver - { - UAuthMode Resolve(UAuthMode? configuredMode, UAuthClientProfile clientProfile, AuthFlowType flowType); - } + UAuthMode Resolve(UAuthMode? configuredMode, UAuthClientProfile clientProfile, AuthFlowType flowType); } diff --git a/src/CodeBeam.UltimateAuth.Server/Contracts/JwtSigningKey.cs b/src/CodeBeam.UltimateAuth.Server/Contracts/JwtSigningKey.cs new file mode 100644 index 00000000..36f6dfcb --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Contracts/JwtSigningKey.cs @@ -0,0 +1,10 @@ +using Microsoft.IdentityModel.Tokens; + +namespace CodeBeam.UltimateAuth.Server.Contracts; + +public sealed class JwtSigningKey +{ + public required string KeyId { get; init; } + public required SecurityKey Key { get; init; } + public required string Algorithm { get; init; } +} diff --git a/src/CodeBeam.UltimateAuth.Server/Contracts/LogoutResponse.cs b/src/CodeBeam.UltimateAuth.Server/Contracts/LogoutResponse.cs deleted file mode 100644 index 2f804228..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Contracts/LogoutResponse.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace CodeBeam.UltimateAuth.Server.Contracts -{ - public sealed record LogoutResponse - { - public bool Success { get; init; } - } -} diff --git a/src/CodeBeam.UltimateAuth.Server/Contracts/RefreshTokenStatus.cs b/src/CodeBeam.UltimateAuth.Server/Contracts/RefreshTokenStatus.cs index a37b36a2..0accae3c 100644 --- a/src/CodeBeam.UltimateAuth.Server/Contracts/RefreshTokenStatus.cs +++ b/src/CodeBeam.UltimateAuth.Server/Contracts/RefreshTokenStatus.cs @@ -1,12 +1,11 @@ -namespace CodeBeam.UltimateAuth.Server.Contracts +namespace CodeBeam.UltimateAuth.Server.Contracts; + +public enum RefreshTokenStatus { - public enum RefreshTokenStatus - { - Valid = 0, - Expired = 1, - Revoked = 2, - NotFound = 3, - Reused = 4, - SessionMismatch = 5 - } + Valid = 0, + Expired = 1, + Revoked = 2, + NotFound = 3, + Reused = 4, + SessionMismatch = 5 } diff --git a/src/CodeBeam.UltimateAuth.Server/Contracts/ResolvedCredential.cs b/src/CodeBeam.UltimateAuth.Server/Contracts/ResolvedCredential.cs new file mode 100644 index 00000000..1d7a15d0 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Contracts/ResolvedCredential.cs @@ -0,0 +1,19 @@ +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; + +namespace CodeBeam.UltimateAuth.Server.Contracts; + +public sealed record ResolvedCredential +{ + public PrimaryCredentialKind Kind { get; init; } + + /// + /// Raw credential value (session id / jwt / opaque) + /// + public string Value { get; init; } = default!; + + public TenantKey Tenant { get; init; } + + public DeviceInfo Device { get; init; } = default!; +} diff --git a/src/CodeBeam.UltimateAuth.Server/Contracts/SessionRefreshResult.cs b/src/CodeBeam.UltimateAuth.Server/Contracts/SessionRefreshResult.cs index 6a017479..e0d6fc42 100644 --- a/src/CodeBeam.UltimateAuth.Server/Contracts/SessionRefreshResult.cs +++ b/src/CodeBeam.UltimateAuth.Server/Contracts/SessionRefreshResult.cs @@ -1,20 +1,16 @@ -namespace CodeBeam.UltimateAuth.Server.Contracts -{ - public sealed class SessionRefreshResult - { - public bool Succeeded { get; } - public string? NewSessionId { get; } - - private SessionRefreshResult(bool succeeded, string? newSessionId) - { - Succeeded = succeeded; - NewSessionId = newSessionId; - } +namespace CodeBeam.UltimateAuth.Server.Contracts; - public static SessionRefreshResult Success(string? newSessionId = null) - => new(true, newSessionId); +public sealed class SessionRefreshResult +{ + public bool Succeeded { get; } + public string? NewSessionId { get; } - public static SessionRefreshResult Failed() - => new(false, null); + private SessionRefreshResult(bool succeeded, string? newSessionId) + { + Succeeded = succeeded; + NewSessionId = newSessionId; } + + public static SessionRefreshResult Success(string? newSessionId = null) => new(true, newSessionId); + public static SessionRefreshResult Failed() => new(false, null); } diff --git a/src/CodeBeam.UltimateAuth.Server/Cookies/IUAuthCookiePolicyBuilder.cs b/src/CodeBeam.UltimateAuth.Server/Cookies/IUAuthCookiePolicyBuilder.cs index f90df8ea..8f445d73 100644 --- a/src/CodeBeam.UltimateAuth.Server/Cookies/IUAuthCookiePolicyBuilder.cs +++ b/src/CodeBeam.UltimateAuth.Server/Cookies/IUAuthCookiePolicyBuilder.cs @@ -1,7 +1,6 @@ using Microsoft.AspNetCore.Http; using CodeBeam.UltimateAuth.Server.Auth; using CodeBeam.UltimateAuth.Server.Options; -using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; namespace CodeBeam.UltimateAuth.Server.Cookies; diff --git a/src/CodeBeam.UltimateAuth.Server/Cookies/DefaultUAuthCookieManager.cs b/src/CodeBeam.UltimateAuth.Server/Cookies/UAuthCookieManager.cs similarity index 88% rename from src/CodeBeam.UltimateAuth.Server/Cookies/DefaultUAuthCookieManager.cs rename to src/CodeBeam.UltimateAuth.Server/Cookies/UAuthCookieManager.cs index 5aaa3785..0bc8e1fc 100644 --- a/src/CodeBeam.UltimateAuth.Server/Cookies/DefaultUAuthCookieManager.cs +++ b/src/CodeBeam.UltimateAuth.Server/Cookies/UAuthCookieManager.cs @@ -2,7 +2,7 @@ namespace CodeBeam.UltimateAuth.Server.Cookies; -internal sealed class DefaultUAuthCookieManager : IUAuthCookieManager +internal sealed class UAuthCookieManager : IUAuthCookieManager { public void Write(HttpContext context, string name, string value, CookieOptions options) { diff --git a/src/CodeBeam.UltimateAuth.Server/Cookies/DefaultUAuthCookiePolicyBuilder.cs b/src/CodeBeam.UltimateAuth.Server/Cookies/UAuthCookiePolicyBuilder.cs similarity index 97% rename from src/CodeBeam.UltimateAuth.Server/Cookies/DefaultUAuthCookiePolicyBuilder.cs rename to src/CodeBeam.UltimateAuth.Server/Cookies/UAuthCookiePolicyBuilder.cs index 088b17fd..92523b04 100644 --- a/src/CodeBeam.UltimateAuth.Server/Cookies/DefaultUAuthCookiePolicyBuilder.cs +++ b/src/CodeBeam.UltimateAuth.Server/Cookies/UAuthCookiePolicyBuilder.cs @@ -6,7 +6,7 @@ namespace CodeBeam.UltimateAuth.Server.Cookies; -internal sealed class DefaultUAuthCookiePolicyBuilder : IUAuthCookiePolicyBuilder +internal sealed class UAuthCookiePolicyBuilder : IUAuthCookiePolicyBuilder { public CookieOptions Build(CredentialResponseOptions response, AuthFlowContext context, CredentialKind kind) { diff --git a/src/CodeBeam.UltimateAuth.Server/Defaults/UAuthActions.cs b/src/CodeBeam.UltimateAuth.Server/Defaults/UAuthActions.cs index 1d0ce66b..8220ca7e 100644 --- a/src/CodeBeam.UltimateAuth.Server/Defaults/UAuthActions.cs +++ b/src/CodeBeam.UltimateAuth.Server/Defaults/UAuthActions.cs @@ -1,70 +1,68 @@ -namespace CodeBeam.UltimateAuth.Server.Defaults +namespace CodeBeam.UltimateAuth.Server.Defaults; + +public static class UAuthActions { - public static class UAuthActions + public static class Users { - public static class Users - { - public const string Create = "users.create"; - public const string DeleteAdmin = "users.delete.admin"; - public const string ChangeStatusSelf = "users.status.change.self"; - public const string ChangeStatusAdmin = "users.status.change.admin"; - } + public const string Create = "users.create"; + public const string DeleteAdmin = "users.delete.admin"; + public const string ChangeStatusSelf = "users.status.change.self"; + public const string ChangeStatusAdmin = "users.status.change.admin"; + } - public static class UserProfiles - { - public const string GetSelf = "users.profile.get.self"; - public const string UpdateSelf = "users.profile.update.self"; - public const string GetAdmin = "users.profile.get.admin"; - public const string UpdateAdmin = "users.profile.update.admin"; - } + public static class UserProfiles + { + public const string GetSelf = "users.profile.get.self"; + public const string UpdateSelf = "users.profile.update.self"; + public const string GetAdmin = "users.profile.get.admin"; + public const string UpdateAdmin = "users.profile.update.admin"; + } - public static class UserIdentifiers - { - public const string GetSelf = "users.identifiers.get.self"; - public const string GetAdmin = "users.identifiers.get.admin"; - public const string AddSelf = "users.identifiers.add.self"; - public const string AddAdmin = "users.identifiers.add.admin"; - public const string UpdateSelf = "users.identifiers.update.self"; - public const string UpdateAdmin = "users.identifiers.update.admin"; - public const string SetPrimarySelf = "users.identifiers.setprimary.self"; - public const string SetPrimaryAdmin = "users.identifiers.setprimary.admin"; - public const string UnsetPrimarySelf = "users.identifiers.unsetprimary.self"; - public const string UnsetPrimaryAdmin = "users.identifiers.unsetprimary.admin"; - public const string VerifySelf = "users.identifiers.verify.self"; - public const string VerifyAdmin = "users.identifiers.verify.admin"; - public const string DeleteSelf = "users.identifiers.delete.self"; - public const string DeleteAdmin = "users.identifiers.delete.admin"; - } + public static class UserIdentifiers + { + public const string GetSelf = "users.identifiers.get.self"; + public const string GetAdmin = "users.identifiers.get.admin"; + public const string AddSelf = "users.identifiers.add.self"; + public const string AddAdmin = "users.identifiers.add.admin"; + public const string UpdateSelf = "users.identifiers.update.self"; + public const string UpdateAdmin = "users.identifiers.update.admin"; + public const string SetPrimarySelf = "users.identifiers.setprimary.self"; + public const string SetPrimaryAdmin = "users.identifiers.setprimary.admin"; + public const string UnsetPrimarySelf = "users.identifiers.unsetprimary.self"; + public const string UnsetPrimaryAdmin = "users.identifiers.unsetprimary.admin"; + public const string VerifySelf = "users.identifiers.verify.self"; + public const string VerifyAdmin = "users.identifiers.verify.admin"; + public const string DeleteSelf = "users.identifiers.delete.self"; + public const string DeleteAdmin = "users.identifiers.delete.admin"; + } - public static class Credentials - { - public const string ListSelf = "credentials.list.self"; - public const string ListAdmin = "credentials.list.admin"; - public const string AddSelf = "credentials.add.self"; - public const string AddAdmin = "credentials.add.admin"; - public const string ChangeSelf = "credentials.change.self"; - public const string ChangeAdmin = "credentials.change.admin"; - public const string RevokeSelf = "credentials.revoke.self"; - public const string RevokeAdmin = "credentials.revoke.admin"; - public const string ActivateSelf = "credentials.activate.self"; - public const string ActivateAdmin = "credentials.activate.admin"; - public const string BeginResetSelf = "credentials.beginreset.self"; - public const string BeginResetAdmin = "credentials.beginreset.admin"; - public const string CompleteResetSelf = "credentials.completereset.self"; - public const string CompleteResetAdmin = "credentials.completereset.admin"; - public const string DeleteAdmin = "credentials.delete.admin"; - } + public static class Credentials + { + public const string ListSelf = "credentials.list.self"; + public const string ListAdmin = "credentials.list.admin"; + public const string AddSelf = "credentials.add.self"; + public const string AddAdmin = "credentials.add.admin"; + public const string ChangeSelf = "credentials.change.self"; + public const string ChangeAdmin = "credentials.change.admin"; + public const string RevokeSelf = "credentials.revoke.self"; + public const string RevokeAdmin = "credentials.revoke.admin"; + public const string ActivateSelf = "credentials.activate.self"; + public const string ActivateAdmin = "credentials.activate.admin"; + public const string BeginResetSelf = "credentials.beginreset.self"; + public const string BeginResetAdmin = "credentials.beginreset.admin"; + public const string CompleteResetSelf = "credentials.completereset.self"; + public const string CompleteResetAdmin = "credentials.completereset.admin"; + public const string DeleteAdmin = "credentials.delete.admin"; + } - public static class Authorization + public static class Authorization + { + public static class Roles { - public static class Roles - { - public const string ReadSelf = "authorization.roles.read.self"; - public const string ReadAdmin = "authorization.roles.read.admin"; - public const string AssignAdmin = "authorization.roles.assign.admin"; - public const string RemoveAdmin = "authorization.roles.remove.admin"; - } + public const string ReadSelf = "authorization.roles.read.self"; + public const string ReadAdmin = "authorization.roles.read.admin"; + public const string AssignAdmin = "authorization.roles.assign.admin"; + public const string RemoveAdmin = "authorization.roles.remove.admin"; } - } } diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IAuthorizationEndpointHandler.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IAuthorizationEndpointHandler.cs index ff635f09..2844105f 100644 --- a/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IAuthorizationEndpointHandler.cs +++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IAuthorizationEndpointHandler.cs @@ -1,14 +1,13 @@ using CodeBeam.UltimateAuth.Core.Domain; using Microsoft.AspNetCore.Http; -namespace CodeBeam.UltimateAuth.Server.Endpoints +namespace CodeBeam.UltimateAuth.Server.Endpoints; + +public interface IAuthorizationEndpointHandler { - public interface IAuthorizationEndpointHandler - { - Task CheckAsync(HttpContext ctx); - Task GetMyRolesAsync(HttpContext ctx); - Task GetUserRolesAsync(UserKey userKey, HttpContext ctx); - Task AssignRoleAsync(UserKey userKey, HttpContext ctx); - Task RemoveRoleAsync(UserKey userKey, HttpContext ctx); - } + Task CheckAsync(HttpContext ctx); + Task GetMyRolesAsync(HttpContext ctx); + Task GetUserRolesAsync(UserKey userKey, HttpContext ctx); + Task AssignRoleAsync(UserKey userKey, HttpContext ctx); + Task RemoveRoleAsync(UserKey userKey, HttpContext ctx); } diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/ILoginEndpointHandler.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/ILoginEndpointHandler.cs index 6b464500..72b3df38 100644 --- a/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/ILoginEndpointHandler.cs +++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/ILoginEndpointHandler.cs @@ -1,9 +1,8 @@ using Microsoft.AspNetCore.Http; -namespace CodeBeam.UltimateAuth.Server.Endpoints +namespace CodeBeam.UltimateAuth.Server.Endpoints; + +public interface ILoginEndpointHandler { - public interface ILoginEndpointHandler - { - Task LoginAsync(HttpContext ctx); - } + Task LoginAsync(HttpContext ctx); } diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/ILogoutEndpointHandler.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/ILogoutEndpointHandler.cs index 3185a333..424560f2 100644 --- a/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/ILogoutEndpointHandler.cs +++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/ILogoutEndpointHandler.cs @@ -1,9 +1,8 @@ using Microsoft.AspNetCore.Http; -namespace CodeBeam.UltimateAuth.Server.Endpoints +namespace CodeBeam.UltimateAuth.Server.Endpoints; + +public interface ILogoutEndpointHandler { - public interface ILogoutEndpointHandler - { - Task LogoutAsync(HttpContext ctx); - } + Task LogoutAsync(HttpContext ctx); } diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IPkceEndpointHandler.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IPkceEndpointHandler.cs index f26dc4a7..547dcf9b 100644 --- a/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IPkceEndpointHandler.cs +++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IPkceEndpointHandler.cs @@ -1,21 +1,20 @@ using Microsoft.AspNetCore.Http; -namespace CodeBeam.UltimateAuth.Server.Endpoints +namespace CodeBeam.UltimateAuth.Server.Endpoints; + +public interface IPkceEndpointHandler { - public interface IPkceEndpointHandler - { - /// - /// Starts the PKCE authorization flow. - /// Creates and stores a PKCE authorization artifact - /// and returns an authorization code or redirect instruction. - /// - Task AuthorizeAsync(HttpContext ctx); + /// + /// Starts the PKCE authorization flow. + /// Creates and stores a PKCE authorization artifact + /// and returns an authorization code or redirect instruction. + /// + Task AuthorizeAsync(HttpContext ctx); - /// - /// Completes the PKCE flow. - /// Atomically validates and consumes the authorization code, - /// then issues a session or token. - /// - Task CompleteAsync(HttpContext ctx); - } + /// + /// Completes the PKCE flow. + /// Atomically validates and consumes the authorization code, + /// then issues a session or token. + /// + Task CompleteAsync(HttpContext ctx); } diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IReauthEndpointHandler.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IReauthEndpointHandler.cs index 4de1bae8..8dfe0c20 100644 --- a/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IReauthEndpointHandler.cs +++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IReauthEndpointHandler.cs @@ -1,9 +1,8 @@ using Microsoft.AspNetCore.Http; -namespace CodeBeam.UltimateAuth.Server.Endpoints +namespace CodeBeam.UltimateAuth.Server.Endpoints; + +public interface IReauthEndpointHandler { - public interface IReauthEndpointHandler - { - Task ReauthAsync(HttpContext ctx); - } + Task ReauthAsync(HttpContext ctx); } diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IRefreshEndpointHandler.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IRefreshEndpointHandler.cs index b79cd0d7..70cfc80f 100644 --- a/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IRefreshEndpointHandler.cs +++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IRefreshEndpointHandler.cs @@ -1,9 +1,8 @@ using Microsoft.AspNetCore.Http; -namespace CodeBeam.UltimateAuth.Server.Endpoints +namespace CodeBeam.UltimateAuth.Server.Endpoints; + +public interface IRefreshEndpointHandler { - public interface IRefreshEndpointHandler - { - Task RefreshAsync(HttpContext ctx); - } + Task RefreshAsync(HttpContext ctx); } diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/ISessionManagementHandler.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/ISessionManagementHandler.cs index 1d5c9288..a4bcd598 100644 --- a/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/ISessionManagementHandler.cs +++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/ISessionManagementHandler.cs @@ -1,12 +1,11 @@ using Microsoft.AspNetCore.Http; -namespace CodeBeam.UltimateAuth.Server.Endpoints +namespace CodeBeam.UltimateAuth.Server.Endpoints; + +public interface ISessionManagementHandler { - public interface ISessionManagementHandler - { - Task GetCurrentSessionAsync(HttpContext ctx); - Task GetAllSessionsAsync(HttpContext ctx); - Task RevokeSessionAsync(string sessionId, HttpContext ctx); - Task RevokeAllAsync(HttpContext ctx); - } + Task GetCurrentSessionAsync(HttpContext ctx); + Task GetAllSessionsAsync(HttpContext ctx); + Task RevokeSessionAsync(string sessionId, HttpContext ctx); + Task RevokeAllAsync(HttpContext ctx); } diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/ITokenEndpointHandler.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/ITokenEndpointHandler.cs index e69a1e55..0056c86f 100644 --- a/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/ITokenEndpointHandler.cs +++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/ITokenEndpointHandler.cs @@ -1,12 +1,11 @@ using Microsoft.AspNetCore.Http; -namespace CodeBeam.UltimateAuth.Server.Endpoints +namespace CodeBeam.UltimateAuth.Server.Endpoints; + +public interface ITokenEndpointHandler { - public interface ITokenEndpointHandler - { - Task GetTokenAsync(HttpContext ctx); - Task RefreshTokenAsync(HttpContext ctx); - Task IntrospectAsync(HttpContext ctx); - Task RevokeAsync(HttpContext ctx); - } + Task GetTokenAsync(HttpContext ctx); + Task RefreshTokenAsync(HttpContext ctx); + Task IntrospectAsync(HttpContext ctx); + Task RevokeAsync(HttpContext ctx); } diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IUAuthEndpointRegistrar.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IUAuthEndpointRegistrar.cs new file mode 100644 index 00000000..7d9f3264 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IUAuthEndpointRegistrar.cs @@ -0,0 +1,9 @@ +using CodeBeam.UltimateAuth.Server.Options; +using Microsoft.AspNetCore.Routing; + +namespace CodeBeam.UltimateAuth.Server.Endpoints; + +public interface IAuthEndpointRegistrar +{ + void MapEndpoints(RouteGroupBuilder rootGroup, UAuthServerOptions options); +} diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IUserInfoEndpointHandler.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IUserInfoEndpointHandler.cs index 54c0eae6..04a1a23c 100644 --- a/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IUserInfoEndpointHandler.cs +++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IUserInfoEndpointHandler.cs @@ -1,11 +1,10 @@ using Microsoft.AspNetCore.Http; -namespace CodeBeam.UltimateAuth.Server.Endpoints +namespace CodeBeam.UltimateAuth.Server.Endpoints; + +public interface IUserInfoEndpointHandler { - public interface IUserInfoEndpointHandler - { - Task GetUserInfoAsync(HttpContext ctx); - Task GetPermissionsAsync(HttpContext ctx); - Task CheckPermissionAsync(HttpContext ctx); - } + Task GetUserInfoAsync(HttpContext ctx); + Task GetPermissionsAsync(HttpContext ctx); + Task CheckPermissionAsync(HttpContext ctx); } diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IValidateEndpointHandler.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IValidateEndpointHandler.cs index 94a395ab..d97aee7e 100644 --- a/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IValidateEndpointHandler.cs +++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IValidateEndpointHandler.cs @@ -1,9 +1,8 @@ using Microsoft.AspNetCore.Http; -namespace CodeBeam.UltimateAuth.Server.Endpoints +namespace CodeBeam.UltimateAuth.Server.Endpoints; + +public interface IValidateEndpointHandler { - public interface IValidateEndpointHandler - { - Task ValidateAsync(HttpContext context, CancellationToken ct = default); - } + Task ValidateAsync(HttpContext context, CancellationToken ct = default); } diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/Bridges/LoginEndpointHandlerBridge.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/Bridges/LoginEndpointHandlerBridge.cs new file mode 100644 index 00000000..3050581d --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/Bridges/LoginEndpointHandlerBridge.cs @@ -0,0 +1,16 @@ +using CodeBeam.UltimateAuth.Core.Domain; +using Microsoft.AspNetCore.Http; + +namespace CodeBeam.UltimateAuth.Server.Endpoints; + +internal sealed class LoginEndpointHandlerBridge : ILoginEndpointHandler +{ + private readonly LoginEndpointHandler _inner; + + public LoginEndpointHandlerBridge(LoginEndpointHandler inner) + { + _inner = inner; + } + + public Task LoginAsync(HttpContext ctx) => _inner.LoginAsync(ctx); +} diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/Bridges/LogoutEndpointHandlerBridge.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/Bridges/LogoutEndpointHandlerBridge.cs new file mode 100644 index 00000000..d6d79662 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/Bridges/LogoutEndpointHandlerBridge.cs @@ -0,0 +1,17 @@ +using CodeBeam.UltimateAuth.Core.Domain; +using Microsoft.AspNetCore.Http; + +namespace CodeBeam.UltimateAuth.Server.Endpoints; + +internal sealed class LogoutEndpointHandlerBridge : ILogoutEndpointHandler +{ + private readonly LogoutEndpointHandler _inner; + + public LogoutEndpointHandlerBridge(LogoutEndpointHandler inner) + { + _inner = inner; + } + + public Task LogoutAsync(HttpContext ctx) + => _inner.LogoutAsync(ctx); +} diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/Bridges/PkceEndpointHandlerBridge.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/Bridges/PkceEndpointHandlerBridge.cs new file mode 100644 index 00000000..6d515515 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/Bridges/PkceEndpointHandlerBridge.cs @@ -0,0 +1,18 @@ +using CodeBeam.UltimateAuth.Core.Domain; +using Microsoft.AspNetCore.Http; + +namespace CodeBeam.UltimateAuth.Server.Endpoints; + +internal sealed class PkceEndpointHandlerBridge : IPkceEndpointHandler +{ + private readonly PkceEndpointHandler _inner; + + public PkceEndpointHandlerBridge(PkceEndpointHandler inner) + { + _inner = inner; + } + + public Task AuthorizeAsync(HttpContext ctx) => _inner.AuthorizeAsync(ctx); + + public Task CompleteAsync(HttpContext ctx) => _inner.CompleteAsync(ctx); +} diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/Bridges/RefreshEndpointHandlerBridge.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/Bridges/RefreshEndpointHandlerBridge.cs new file mode 100644 index 00000000..9a23cc1b --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/Bridges/RefreshEndpointHandlerBridge.cs @@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.Http; + +namespace CodeBeam.UltimateAuth.Server.Endpoints; + +internal sealed class RefreshEndpointHandlerBridge : IRefreshEndpointHandler +{ + private readonly RefreshEndpointHandler _inner; + + public RefreshEndpointHandlerBridge(RefreshEndpointHandler inner) + { + _inner = inner; + } + + public Task RefreshAsync(HttpContext ctx) => _inner.RefreshAsync(ctx); +} diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/Bridges/ValidateEndpointHandlerBridge.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/Bridges/ValidateEndpointHandlerBridge.cs new file mode 100644 index 00000000..412bcef8 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/Bridges/ValidateEndpointHandlerBridge.cs @@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.Http; + +namespace CodeBeam.UltimateAuth.Server.Endpoints; + +internal sealed class ValidateEndpointHandlerBridge : IValidateEndpointHandler +{ + private readonly ValidateEndpointHandler _inner; + + public ValidateEndpointHandlerBridge(ValidateEndpointHandler inner) + { + _inner = inner; + } + + public Task ValidateAsync(HttpContext context, CancellationToken ct = default) => _inner.ValidateAsync(context, ct); +} diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultLogoutEndpointHandler.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultLogoutEndpointHandler.cs deleted file mode 100644 index 1d030ff9..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultLogoutEndpointHandler.cs +++ /dev/null @@ -1,75 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Abstractions; -using CodeBeam.UltimateAuth.Core.Contracts; -using CodeBeam.UltimateAuth.Core.Options; -using CodeBeam.UltimateAuth.Server.Auth; -using CodeBeam.UltimateAuth.Server.Contracts; -using CodeBeam.UltimateAuth.Server.Cookies; -using CodeBeam.UltimateAuth.Server.Infrastructure; -using CodeBeam.UltimateAuth.Server.Options; -using CodeBeam.UltimateAuth.Server.Services; -using Microsoft.AspNetCore.Http; - -namespace CodeBeam.UltimateAuth.Server.Endpoints -{ - public sealed class DefaultLogoutEndpointHandler : ILogoutEndpointHandler - { - private readonly IAuthFlowContextAccessor _authContext; - private readonly IUAuthFlowService _flow; - private readonly IClock _clock; - private readonly IUAuthCookieManager _cookieManager; - private readonly AuthRedirectResolver _redirectResolver; - - public DefaultLogoutEndpointHandler(IAuthFlowContextAccessor authContext, IUAuthFlowService flow, IClock clock, IUAuthCookieManager cookieManager, AuthRedirectResolver redirectResolver) - { - _authContext = authContext; - _flow = flow; - _clock = clock; - _cookieManager = cookieManager; - _redirectResolver = redirectResolver; - } - - public async Task LogoutAsync(HttpContext ctx) - { - var auth = _authContext.Current; - - if (auth.Session is SessionSecurityContext session) - { - var request = new LogoutRequest - { - Tenant = auth.Tenant, - SessionId = session.SessionId, - At = _clock.UtcNow, - }; - - await _flow.LogoutAsync(request, ctx.RequestAborted); - } - - DeleteIfCookie(ctx, auth.Response.SessionIdDelivery); - DeleteIfCookie(ctx, auth.Response.RefreshTokenDelivery); - DeleteIfCookie(ctx, auth.Response.AccessTokenDelivery); - - if (auth.Response.Logout.RedirectEnabled) - { - var redirectUrl = _redirectResolver.ResolveRedirect(ctx, auth.Response.Logout.RedirectPath); - return Results.Redirect(redirectUrl); - } - - return Results.Ok(new LogoutResponse - { - Success = true - }); - } - - private void DeleteIfCookie(HttpContext ctx, CredentialResponseOptions delivery) - { - if (delivery.Mode != TokenResponseMode.Cookie) - return; - - if (delivery.Cookie == null) - return; - - _cookieManager.Delete(ctx, delivery.Cookie.Name); - } - - } -} diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultRefreshEndpointHandler.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultRefreshEndpointHandler.cs deleted file mode 100644 index 83b703ad..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultRefreshEndpointHandler.cs +++ /dev/null @@ -1,91 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Contracts; -using CodeBeam.UltimateAuth.Core.Domain; -using CodeBeam.UltimateAuth.Server.Abstractions; -using CodeBeam.UltimateAuth.Server.Auth; -using CodeBeam.UltimateAuth.Server.Infrastructure; -using CodeBeam.UltimateAuth.Server.Services; -using Microsoft.AspNetCore.Http; - -namespace CodeBeam.UltimateAuth.Server.Endpoints -{ - public sealed class DefaultRefreshEndpointHandler : IRefreshEndpointHandler - { - private readonly IAuthFlowContextAccessor _authContext; - private readonly IRefreshFlowService _refreshFlow; - private readonly ICredentialResponseWriter _credentialWriter; - private readonly IRefreshResponseWriter _refreshWriter; - private readonly IRefreshTokenResolver _refreshTokenResolver; - private readonly IRefreshResponsePolicy _refreshPolicy; - - public DefaultRefreshEndpointHandler( - IAuthFlowContextAccessor authContext, - IRefreshFlowService refreshFlow, - ICredentialResponseWriter credentialWriter, - IRefreshResponseWriter refreshWriter, - IRefreshTokenResolver refreshTokenResolver, - IRefreshResponsePolicy refreshPolicy) - { - _authContext = authContext; - _refreshFlow = refreshFlow; - _credentialWriter = credentialWriter; - _refreshWriter = refreshWriter; - _refreshTokenResolver = refreshTokenResolver; - _refreshPolicy = refreshPolicy; - } - - public async Task RefreshAsync(HttpContext ctx) - { - var flow = _authContext.Current; - - if (flow.Session is not SessionSecurityContext session) - { - //_logger.LogDebug("Refresh called without active session."); - return Results.Ok(RefreshOutcome.None); - } - - var request = new RefreshFlowRequest - { - SessionId = session.SessionId, - RefreshToken = _refreshTokenResolver.Resolve(ctx), - Device = flow.Device, - Now = DateTimeOffset.UtcNow - }; - - var result = await _refreshFlow.RefreshAsync(flow, request, ctx.RequestAborted); - - if (!result.Succeeded) - { - WriteRefreshHeader(ctx, flow, RefreshOutcome.ReauthRequired); - return Results.Unauthorized(); - } - - var primary = _refreshPolicy.SelectPrimary(flow, request, result); - - if (primary == CredentialKind.Session && result.SessionId is not null) - { - _credentialWriter.Write(ctx, CredentialKind.Session, result.SessionId.Value); - } - else if (primary == CredentialKind.AccessToken && result.AccessToken is not null) - { - _credentialWriter.Write(ctx, CredentialKind.AccessToken, result.AccessToken); - } - - if (_refreshPolicy.WriteRefreshToken(flow) && result.RefreshToken is not null) - { - _credentialWriter.Write(ctx, CredentialKind.RefreshToken, result.RefreshToken); - } - - WriteRefreshHeader(ctx, flow, result.Outcome); - return Results.NoContent(); - } - - private void WriteRefreshHeader(HttpContext ctx, AuthFlowContext flow, RefreshOutcome outcome) - { - if (!flow.OriginalOptions.Diagnostics.EnableRefreshHeaders) - return; - - _refreshWriter.Write(ctx, outcome); - } - - } -} diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultLoginEndpointHandler.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/LoginEndpointHandler.cs similarity index 96% rename from src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultLoginEndpointHandler.cs rename to src/CodeBeam.UltimateAuth.Server/Endpoints/LoginEndpointHandler.cs index 05289292..5b5361e9 100644 --- a/src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultLoginEndpointHandler.cs +++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/LoginEndpointHandler.cs @@ -11,7 +11,7 @@ namespace CodeBeam.UltimateAuth.Server.Endpoints; -public sealed class DefaultLoginEndpointHandler : ILoginEndpointHandler +public sealed class LoginEndpointHandler : ILoginEndpointHandler { private readonly IAuthFlowContextAccessor _authFlow; private readonly IUAuthFlowService _flowService; @@ -19,7 +19,7 @@ public sealed class DefaultLoginEndpointHandler : ILoginEndpointHandler private readonly ICredentialResponseWriter _credentialResponseWriter; private readonly AuthRedirectResolver _redirectResolver; - public DefaultLoginEndpointHandler( + public LoginEndpointHandler( IAuthFlowContextAccessor authFlow, IUAuthFlowService flowService, IClock clock, diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/LoginEndpointHandlerBridge.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/LoginEndpointHandlerBridge.cs deleted file mode 100644 index 7b769f14..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Endpoints/LoginEndpointHandlerBridge.cs +++ /dev/null @@ -1,17 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Domain; -using Microsoft.AspNetCore.Http; - -namespace CodeBeam.UltimateAuth.Server.Endpoints -{ - internal sealed class LoginEndpointHandlerBridge : ILoginEndpointHandler - { - private readonly DefaultLoginEndpointHandler _inner; - - public LoginEndpointHandlerBridge(DefaultLoginEndpointHandler inner) - { - _inner = inner; - } - - public Task LoginAsync(HttpContext ctx) => _inner.LoginAsync(ctx); - } -} diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/LogoutEndpointHandler.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/LogoutEndpointHandler.cs new file mode 100644 index 00000000..d99f9e1e --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/LogoutEndpointHandler.cs @@ -0,0 +1,73 @@ +using CodeBeam.UltimateAuth.Core.Abstractions; +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Options; +using CodeBeam.UltimateAuth.Server.Auth; +using CodeBeam.UltimateAuth.Server.Cookies; +using CodeBeam.UltimateAuth.Server.Infrastructure; +using CodeBeam.UltimateAuth.Server.Options; +using CodeBeam.UltimateAuth.Server.Services; +using Microsoft.AspNetCore.Http; + +namespace CodeBeam.UltimateAuth.Server.Endpoints; + +public sealed class LogoutEndpointHandler : ILogoutEndpointHandler +{ + private readonly IAuthFlowContextAccessor _authContext; + private readonly IUAuthFlowService _flow; + private readonly IClock _clock; + private readonly IUAuthCookieManager _cookieManager; + private readonly AuthRedirectResolver _redirectResolver; + + public LogoutEndpointHandler(IAuthFlowContextAccessor authContext, IUAuthFlowService flow, IClock clock, IUAuthCookieManager cookieManager, AuthRedirectResolver redirectResolver) + { + _authContext = authContext; + _flow = flow; + _clock = clock; + _cookieManager = cookieManager; + _redirectResolver = redirectResolver; + } + + public async Task LogoutAsync(HttpContext ctx) + { + var auth = _authContext.Current; + + if (auth.Session is SessionSecurityContext session) + { + var request = new LogoutRequest + { + Tenant = auth.Tenant, + SessionId = session.SessionId, + At = _clock.UtcNow, + }; + + await _flow.LogoutAsync(request, ctx.RequestAborted); + } + + DeleteIfCookie(ctx, auth.Response.SessionIdDelivery); + DeleteIfCookie(ctx, auth.Response.RefreshTokenDelivery); + DeleteIfCookie(ctx, auth.Response.AccessTokenDelivery); + + if (auth.Response.Logout.RedirectEnabled) + { + var redirectUrl = _redirectResolver.ResolveRedirect(ctx, auth.Response.Logout.RedirectPath); + return Results.Redirect(redirectUrl); + } + + return Results.Ok(new LogoutResponse + { + Success = true + }); + } + + private void DeleteIfCookie(HttpContext ctx, CredentialResponseOptions delivery) + { + if (delivery.Mode != TokenResponseMode.Cookie) + return; + + if (delivery.Cookie == null) + return; + + _cookieManager.Delete(ctx, delivery.Cookie.Name); + } + +} diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/LogoutEndpointHandlerBridge.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/LogoutEndpointHandlerBridge.cs deleted file mode 100644 index f35f05cc..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Endpoints/LogoutEndpointHandlerBridge.cs +++ /dev/null @@ -1,18 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Domain; -using Microsoft.AspNetCore.Http; - -namespace CodeBeam.UltimateAuth.Server.Endpoints -{ - internal sealed class LogoutEndpointHandlerBridge : ILogoutEndpointHandler - { - private readonly DefaultLogoutEndpointHandler _inner; - - public LogoutEndpointHandlerBridge(DefaultLogoutEndpointHandler inner) - { - _inner = inner; - } - - public Task LogoutAsync(HttpContext ctx) - => _inner.LogoutAsync(ctx); - } -} diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultPkceEndpointHandler.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/PkceEndpointHandler.cs similarity index 98% rename from src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultPkceEndpointHandler.cs rename to src/CodeBeam.UltimateAuth.Server/Endpoints/PkceEndpointHandler.cs index 0772e4d7..f53a6bb2 100644 --- a/src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultPkceEndpointHandler.cs +++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/PkceEndpointHandler.cs @@ -4,6 +4,7 @@ using CodeBeam.UltimateAuth.Core.Options; using CodeBeam.UltimateAuth.Server.Abstractions; using CodeBeam.UltimateAuth.Server.Auth; +using CodeBeam.UltimateAuth.Server.Flows; using CodeBeam.UltimateAuth.Server.Infrastructure; using CodeBeam.UltimateAuth.Server.Services; using CodeBeam.UltimateAuth.Server.Stores; @@ -12,7 +13,7 @@ namespace CodeBeam.UltimateAuth.Server.Endpoints; -internal sealed class DefaultPkceEndpointHandler : IPkceEndpointHandler +internal sealed class PkceEndpointHandler : IPkceEndpointHandler { private readonly IAuthFlowContextAccessor _authContext; private readonly IUAuthFlowService _flow; @@ -23,7 +24,7 @@ internal sealed class DefaultPkceEndpointHandler : IPkceEndpointHandler private readonly ICredentialResponseWriter _credentialResponseWriter; private readonly AuthRedirectResolver _redirectResolver; - public DefaultPkceEndpointHandler( + public PkceEndpointHandler( IAuthFlowContextAccessor authContext, IUAuthFlowService flow, IAuthStore authStore, diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/PkceEndpointHandlerBridge.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/PkceEndpointHandlerBridge.cs deleted file mode 100644 index 8f8f803b..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Endpoints/PkceEndpointHandlerBridge.cs +++ /dev/null @@ -1,19 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Domain; -using Microsoft.AspNetCore.Http; - -namespace CodeBeam.UltimateAuth.Server.Endpoints -{ - internal sealed class PkceEndpointHandlerBridge : IPkceEndpointHandler - { - private readonly DefaultPkceEndpointHandler _inner; - - public PkceEndpointHandlerBridge(DefaultPkceEndpointHandler inner) - { - _inner = inner; - } - - public Task AuthorizeAsync(HttpContext ctx) => _inner.AuthorizeAsync(ctx); - - public Task CompleteAsync(HttpContext ctx) => _inner.CompleteAsync(ctx); - } -} diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/RefreshEndpointHandler.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/RefreshEndpointHandler.cs new file mode 100644 index 00000000..4f97e151 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/RefreshEndpointHandler.cs @@ -0,0 +1,90 @@ +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Server.Abstractions; +using CodeBeam.UltimateAuth.Server.Auth; +using CodeBeam.UltimateAuth.Server.Flows; +using CodeBeam.UltimateAuth.Server.Services; +using Microsoft.AspNetCore.Http; + +namespace CodeBeam.UltimateAuth.Server.Endpoints; + +public sealed class RefreshEndpointHandler : IRefreshEndpointHandler +{ + private readonly IAuthFlowContextAccessor _authContext; + private readonly IRefreshFlowService _refreshFlow; + private readonly ICredentialResponseWriter _credentialWriter; + private readonly IRefreshResponseWriter _refreshWriter; + private readonly IRefreshTokenResolver _refreshTokenResolver; + private readonly IRefreshResponsePolicy _refreshPolicy; + + public RefreshEndpointHandler( + IAuthFlowContextAccessor authContext, + IRefreshFlowService refreshFlow, + ICredentialResponseWriter credentialWriter, + IRefreshResponseWriter refreshWriter, + IRefreshTokenResolver refreshTokenResolver, + IRefreshResponsePolicy refreshPolicy) + { + _authContext = authContext; + _refreshFlow = refreshFlow; + _credentialWriter = credentialWriter; + _refreshWriter = refreshWriter; + _refreshTokenResolver = refreshTokenResolver; + _refreshPolicy = refreshPolicy; + } + + public async Task RefreshAsync(HttpContext ctx) + { + var flow = _authContext.Current; + + if (flow.Session is not SessionSecurityContext session) + { + //_logger.LogDebug("Refresh called without active session."); + return Results.Ok(RefreshOutcome.None); + } + + var request = new RefreshFlowRequest + { + SessionId = session.SessionId, + RefreshToken = _refreshTokenResolver.Resolve(ctx), + Device = flow.Device, + Now = DateTimeOffset.UtcNow + }; + + var result = await _refreshFlow.RefreshAsync(flow, request, ctx.RequestAborted); + + if (!result.Succeeded) + { + WriteRefreshHeader(ctx, flow, RefreshOutcome.ReauthRequired); + return Results.Unauthorized(); + } + + var primary = _refreshPolicy.SelectPrimary(flow, request, result); + + if (primary == CredentialKind.Session && result.SessionId is not null) + { + _credentialWriter.Write(ctx, CredentialKind.Session, result.SessionId.Value); + } + else if (primary == CredentialKind.AccessToken && result.AccessToken is not null) + { + _credentialWriter.Write(ctx, CredentialKind.AccessToken, result.AccessToken); + } + + if (_refreshPolicy.WriteRefreshToken(flow) && result.RefreshToken is not null) + { + _credentialWriter.Write(ctx, CredentialKind.RefreshToken, result.RefreshToken); + } + + WriteRefreshHeader(ctx, flow, result.Outcome); + return Results.NoContent(); + } + + private void WriteRefreshHeader(HttpContext ctx, AuthFlowContext flow, RefreshOutcome outcome) + { + if (!flow.OriginalOptions.Diagnostics.EnableRefreshHeaders) + return; + + _refreshWriter.Write(ctx, outcome); + } + +} diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/RefreshEndpointHandlerBridge.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/RefreshEndpointHandlerBridge.cs deleted file mode 100644 index 22e776fa..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Endpoints/RefreshEndpointHandlerBridge.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Microsoft.AspNetCore.Http; - -namespace CodeBeam.UltimateAuth.Server.Endpoints -{ - internal sealed class RefreshEndpointHandlerBridge : IRefreshEndpointHandler - { - private readonly DefaultRefreshEndpointHandler _inner; - - public RefreshEndpointHandlerBridge(DefaultRefreshEndpointHandler inner) - { - _inner = inner; - } - - public Task RefreshAsync(HttpContext ctx) => _inner.RefreshAsync(ctx); - } -} diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/UAuthEndpointDefaults.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/UAuthEndpointDefaults.cs deleted file mode 100644 index 3eea6f60..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Endpoints/UAuthEndpointDefaults.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace CodeBeam.UltimateAuth.Server -{ - /// - /// Represents which endpoint groups are enabled by default - /// for a given authentication mode. - /// - public sealed class UAuthEndpointDefaults - { - public bool Login { get; init; } - public bool Pkce { get; init; } - public bool Token { get; init; } - public bool Session { get; init; } - public bool UserInfo { get; init; } - } -} diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/UAuthEndpointRegistrar.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/UAuthEndpointRegistrar.cs index 781d64b3..546358de 100644 --- a/src/CodeBeam.UltimateAuth.Server/Endpoints/UAuthEndpointRegistrar.cs +++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/UAuthEndpointRegistrar.cs @@ -8,11 +8,6 @@ namespace CodeBeam.UltimateAuth.Server.Endpoints; -public interface IAuthEndpointRegistrar -{ - void MapEndpoints(RouteGroupBuilder rootGroup, UAuthServerOptions options); -} - // TODO: Add Scalar/Swagger integration // TODO: Add endpoint based guards public class UAuthEndpointRegistrar : IAuthEndpointRegistrar @@ -254,5 +249,4 @@ public void MapEndpoints(RouteGroupBuilder rootGroup, UAuthServerOptions options } } - } diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultValidateEndpointHandler.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/ValidateEndpointHandler.cs similarity index 96% rename from src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultValidateEndpointHandler.cs rename to src/CodeBeam.UltimateAuth.Server/Endpoints/ValidateEndpointHandler.cs index 1a91f919..971827ba 100644 --- a/src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultValidateEndpointHandler.cs +++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/ValidateEndpointHandler.cs @@ -8,14 +8,14 @@ namespace CodeBeam.UltimateAuth.Server.Endpoints; -internal sealed class DefaultValidateEndpointHandler : IValidateEndpointHandler +internal sealed class ValidateEndpointHandler : IValidateEndpointHandler { private readonly IAuthFlowContextAccessor _authContext; private readonly IFlowCredentialResolver _credentialResolver; private readonly ISessionValidator _sessionValidator; private readonly IClock _clock; - public DefaultValidateEndpointHandler( + public ValidateEndpointHandler( IAuthFlowContextAccessor authContext, IFlowCredentialResolver credentialResolver, ISessionValidator sessionValidator, diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/ValidateEndpointHandlerBridge.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/ValidateEndpointHandlerBridge.cs deleted file mode 100644 index f9a2be56..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Endpoints/ValidateEndpointHandlerBridge.cs +++ /dev/null @@ -1,17 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Domain; -using Microsoft.AspNetCore.Http; - -namespace CodeBeam.UltimateAuth.Server.Endpoints -{ - internal sealed class ValidateEndpointHandlerBridge : IValidateEndpointHandler - { - private readonly DefaultValidateEndpointHandler _inner; - - public ValidateEndpointHandlerBridge(DefaultValidateEndpointHandler inner) - { - _inner = inner; - } - - public Task ValidateAsync(HttpContext context, CancellationToken ct = default) => _inner.ValidateAsync(context, ct); - } -} diff --git a/src/CodeBeam.UltimateAuth.Server/Extensions/AuthFlowContextExtensions.cs b/src/CodeBeam.UltimateAuth.Server/Extensions/AuthFlowContextExtensions.cs index 12f61e52..e48c09d3 100644 --- a/src/CodeBeam.UltimateAuth.Server/Extensions/AuthFlowContextExtensions.cs +++ b/src/CodeBeam.UltimateAuth.Server/Extensions/AuthFlowContextExtensions.cs @@ -2,39 +2,37 @@ using CodeBeam.UltimateAuth.Core.Options; using CodeBeam.UltimateAuth.Server.Auth; -namespace CodeBeam.UltimateAuth.Server.Extensions +namespace CodeBeam.UltimateAuth.Server.Extensions; + +public static class AuthFlowContextExtensions { - public static class AuthFlowContextExtensions + public static AuthContext ToAuthContext(this AuthFlowContext flow, DateTimeOffset now) { - public static AuthContext ToAuthContext(this AuthFlowContext flow, DateTimeOffset now) - { - return new AuthContext - { - Tenant = flow.Tenant, - Operation = flow.FlowType.ToAuthOperation(), - Mode = flow.EffectiveMode, - At = now, - Device = flow.Device, - Session = flow.Session - }; - } - - public static AuthFlowContext WithClientProfile(this AuthFlowContext flow, UAuthClientProfile profile) + return new AuthContext { - return new AuthFlowContext( - flow.FlowType, - profile, - flow.EffectiveMode, - flow.Device, - flow.Tenant, - flow.IsAuthenticated, - flow.UserKey, - flow.Session, - flow.OriginalOptions, - flow.EffectiveOptions, - flow.Response, - flow.PrimaryTokenKind); - } + Tenant = flow.Tenant, + Operation = flow.FlowType.ToAuthOperation(), + Mode = flow.EffectiveMode, + At = now, + Device = flow.Device, + Session = flow.Session + }; + } + public static AuthFlowContext WithClientProfile(this AuthFlowContext flow, UAuthClientProfile profile) + { + return new AuthFlowContext( + flow.FlowType, + profile, + flow.EffectiveMode, + flow.Device, + flow.Tenant, + flow.IsAuthenticated, + flow.UserKey, + flow.Session, + flow.OriginalOptions, + flow.EffectiveOptions, + flow.Response, + flow.PrimaryTokenKind); } } diff --git a/src/CodeBeam.UltimateAuth.Server/Extensions/AuthFlowTypeExtensions.cs b/src/CodeBeam.UltimateAuth.Server/Extensions/AuthFlowTypeExtensions.cs index ea1803c8..a61260b8 100644 --- a/src/CodeBeam.UltimateAuth.Server/Extensions/AuthFlowTypeExtensions.cs +++ b/src/CodeBeam.UltimateAuth.Server/Extensions/AuthFlowTypeExtensions.cs @@ -1,11 +1,11 @@ using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Server.Extensions +namespace CodeBeam.UltimateAuth.Server.Extensions; + +public static class AuthFlowTypeExtensions { - public static class AuthFlowTypeExtensions - { - public static AuthOperation ToAuthOperation(this AuthFlowType flowType) + public static AuthOperation ToAuthOperation(this AuthFlowType flowType) => flowType switch { AuthFlowType.Login => AuthOperation.Login, @@ -29,5 +29,4 @@ public static AuthOperation ToAuthOperation(this AuthFlowType flowType) _ => throw new InvalidOperationException($"Unsupported flow type: {flowType}") }; - } } diff --git a/src/CodeBeam.UltimateAuth.Server/Extensions/ClaimsSnapshotExtensions.cs b/src/CodeBeam.UltimateAuth.Server/Extensions/ClaimsSnapshotExtensions.cs index bfdbee3d..d0e17f40 100644 --- a/src/CodeBeam.UltimateAuth.Server/Extensions/ClaimsSnapshotExtensions.cs +++ b/src/CodeBeam.UltimateAuth.Server/Extensions/ClaimsSnapshotExtensions.cs @@ -1,14 +1,10 @@ using CodeBeam.UltimateAuth.Core.Domain; using System.Security.Claims; -namespace CodeBeam.UltimateAuth.Server.Extensions +namespace CodeBeam.UltimateAuth.Server.Extensions; + +public static class ClaimsSnapshotExtensions { - public static class ClaimsSnapshotExtensions - { - public static IReadOnlyCollection AsClaims( - this ClaimsSnapshot snapshot) - => snapshot.AsDictionary() - .Select(kv => new Claim(kv.Key, kv.Value)) - .ToArray(); - } + public static IReadOnlyCollection AsClaims(this ClaimsSnapshot snapshot) + => snapshot.AsDictionary().Select(kv => new Claim(kv.Key, kv.Value)).ToArray(); } diff --git a/src/CodeBeam.UltimateAuth.Server/Extensions/DeviceExtensions.cs b/src/CodeBeam.UltimateAuth.Server/Extensions/DeviceExtensions.cs index e6fbcf67..de1bd560 100644 --- a/src/CodeBeam.UltimateAuth.Server/Extensions/DeviceExtensions.cs +++ b/src/CodeBeam.UltimateAuth.Server/Extensions/DeviceExtensions.cs @@ -3,16 +3,13 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; -namespace CodeBeam.UltimateAuth.Server.Extensions +namespace CodeBeam.UltimateAuth.Server.Extensions; + +public static class DeviceExtensions { - public static class DeviceExtensions + public static DeviceInfo GetDevice(this HttpContext context) { - public static DeviceInfo GetDevice(this HttpContext context) - { - var resolver = context.RequestServices - .GetRequiredService(); - - return resolver.Resolve(context); - } + var resolver = context.RequestServices.GetRequiredService(); + return resolver.Resolve(context); } } diff --git a/src/CodeBeam.UltimateAuth.Server/Extensions/EndpointRouteBuilderExtensions.cs b/src/CodeBeam.UltimateAuth.Server/Extensions/EndpointRouteBuilderExtensions.cs index a501a1b4..f27f3828 100644 --- a/src/CodeBeam.UltimateAuth.Server/Extensions/EndpointRouteBuilderExtensions.cs +++ b/src/CodeBeam.UltimateAuth.Server/Extensions/EndpointRouteBuilderExtensions.cs @@ -5,27 +5,21 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; -namespace CodeBeam.UltimateAuth.Server.Extensions +namespace CodeBeam.UltimateAuth.Server.Extensions; + +public static class EndpointRouteBuilderExtensions { - public static class EndpointRouteBuilderExtensions + public static IEndpointRouteBuilder MapUAuthEndpoints(this IEndpointRouteBuilder endpoints) { - public static IEndpointRouteBuilder MapUAuthEndpoints(this IEndpointRouteBuilder endpoints) - { - using var scope = endpoints.ServiceProvider.CreateScope(); - - var registrar = scope.ServiceProvider - .GetRequiredService(); - - var options = scope.ServiceProvider - .GetRequiredService>() - .Value; + using var scope = endpoints.ServiceProvider.CreateScope(); + var registrar = scope.ServiceProvider.GetRequiredService(); + var options = scope.ServiceProvider.GetRequiredService>().Value; - // Root group ("/") - var rootGroup = endpoints.MapGroup(""); + // Root group ("/") + var rootGroup = endpoints.MapGroup(""); - registrar.MapEndpoints(rootGroup, options); + registrar.MapEndpoints(rootGroup, options); - return endpoints; - } + return endpoints; } } diff --git a/src/CodeBeam.UltimateAuth.Server/Extensions/HttpContextJsonExtensions.cs b/src/CodeBeam.UltimateAuth.Server/Extensions/HttpContextJsonExtensions.cs index 4b2c9b13..c429e7c5 100644 --- a/src/CodeBeam.UltimateAuth.Server/Extensions/HttpContextJsonExtensions.cs +++ b/src/CodeBeam.UltimateAuth.Server/Extensions/HttpContextJsonExtensions.cs @@ -1,26 +1,25 @@ using System.Text.Json; using Microsoft.AspNetCore.Http; -namespace CodeBeam.UltimateAuth.Server.Extensions +namespace CodeBeam.UltimateAuth.Server.Extensions; + +public static class HttpContextJsonExtensions { - public static class HttpContextJsonExtensions - { - private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web); + private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web); - public static async Task ReadJsonAsync(this HttpContext ctx, CancellationToken ct = default) - { - if (!ctx.Request.HasJsonContentType()) - throw new InvalidOperationException("Request content type must be application/json."); + public static async Task ReadJsonAsync(this HttpContext ctx, CancellationToken ct = default) + { + if (!ctx.Request.HasJsonContentType()) + throw new InvalidOperationException("Request content type must be application/json."); - if (ctx.Request.Body is null) - throw new InvalidOperationException("Request body is empty."); + if (ctx.Request.Body is null) + throw new InvalidOperationException("Request body is empty."); - var result = await JsonSerializer.DeserializeAsync(ctx.Request.Body, JsonOptions, ct); + var result = await JsonSerializer.DeserializeAsync(ctx.Request.Body, JsonOptions, ct); - if (result is null) - throw new InvalidOperationException("Request body could not be deserialized."); + if (result is null) + throw new InvalidOperationException("Request body could not be deserialized."); - return result; - } + return result; } } diff --git a/src/CodeBeam.UltimateAuth.Server/Extensions/HttpContextSessionExtensions.cs b/src/CodeBeam.UltimateAuth.Server/Extensions/HttpContextSessionExtensions.cs index 4208268f..51641fd5 100644 --- a/src/CodeBeam.UltimateAuth.Server/Extensions/HttpContextSessionExtensions.cs +++ b/src/CodeBeam.UltimateAuth.Server/Extensions/HttpContextSessionExtensions.cs @@ -2,20 +2,17 @@ using CodeBeam.UltimateAuth.Server.Infrastructure; using Microsoft.AspNetCore.Http; -namespace CodeBeam.UltimateAuth.Server.Extensions +namespace CodeBeam.UltimateAuth.Server.Extensions; + +public static class HttpContextSessionExtensions { - public static class HttpContextSessionExtensions + public static SessionContext GetSessionContext(this HttpContext context) { - public static SessionContext GetSessionContext(this HttpContext context) + if (context.Items.TryGetValue(SessionContextItemKeys.SessionContext, out var value) && value is SessionContext session) { - if (context.Items.TryGetValue(SessionContextItemKeys.SessionContext, out var value) - && value is SessionContext session) - { - return session; - } - - return SessionContext.Anonymous(); + return session; } - } + return SessionContext.Anonymous(); + } } diff --git a/src/CodeBeam.UltimateAuth.Server/Extensions/HttpContextUserExtensions.cs b/src/CodeBeam.UltimateAuth.Server/Extensions/HttpContextUserExtensions.cs index ed598241..33dbd730 100644 --- a/src/CodeBeam.UltimateAuth.Server/Extensions/HttpContextUserExtensions.cs +++ b/src/CodeBeam.UltimateAuth.Server/Extensions/HttpContextUserExtensions.cs @@ -3,18 +3,17 @@ using CodeBeam.UltimateAuth.Server.Middlewares; using Microsoft.AspNetCore.Http; -namespace CodeBeam.UltimateAuth.Server.Extensions +namespace CodeBeam.UltimateAuth.Server.Extensions; + +public static class HttpContextUserExtensions { - public static class HttpContextUserExtensions + public static AuthUserSnapshot GetUserContext(this HttpContext ctx) { - public static AuthUserSnapshot GetUserContext(this HttpContext ctx) + if (ctx.Items.TryGetValue(UserMiddleware.UserContextKey, out var value) && value is AuthUserSnapshot user) { - if (ctx.Items.TryGetValue(UserMiddleware.UserContextKey, out var value) && value is AuthUserSnapshot user) - { - return user; - } - - return AuthUserSnapshot.Anonymous(); + return user; } + + return AuthUserSnapshot.Anonymous(); } } diff --git a/src/CodeBeam.UltimateAuth.Server/Extensions/ServiceCollectionExtensions.cs b/src/CodeBeam.UltimateAuth.Server/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..dc44adab --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,307 @@ +using CodeBeam.UltimateAuth.Authorization; +using CodeBeam.UltimateAuth.Core; +using CodeBeam.UltimateAuth.Core.Abstractions; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.Extensions; +using CodeBeam.UltimateAuth.Core.Infrastructure; +using CodeBeam.UltimateAuth.Core.MultiTenancy; +using CodeBeam.UltimateAuth.Core.Options; +using CodeBeam.UltimateAuth.Credentials; +using CodeBeam.UltimateAuth.Policies.Abstractions; +using CodeBeam.UltimateAuth.Policies.Defaults; +using CodeBeam.UltimateAuth.Policies.Registry; +using CodeBeam.UltimateAuth.Server.Abstactions; +using CodeBeam.UltimateAuth.Server.Abstractions; +using CodeBeam.UltimateAuth.Server.Auth; +using CodeBeam.UltimateAuth.Server.Cookies; +using CodeBeam.UltimateAuth.Server.Endpoints; +using CodeBeam.UltimateAuth.Server.Flows; +using CodeBeam.UltimateAuth.Server.Infrastructure; +using CodeBeam.UltimateAuth.Server.MultiTenancy; +using CodeBeam.UltimateAuth.Server.Options; +using CodeBeam.UltimateAuth.Server.Services; +using CodeBeam.UltimateAuth.Server.Stores; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; + +namespace CodeBeam.UltimateAuth.Server.Extensions; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddUltimateAuthServer(this IServiceCollection services) + { + services.AddUltimateAuth(); + AddUsersInternal(services); + AddCredentialsInternal(services); + AddAuthorizationInternal(services); + AddUltimateAuthPolicies(services); + return services.AddUltimateAuthServerInternal(); + } + + public static IServiceCollection AddUltimateAuthServer(this IServiceCollection services, IConfiguration configuration) + { + services.AddUltimateAuth(configuration); + AddUsersInternal(services); + AddCredentialsInternal(services); + AddAuthorizationInternal(services); + AddUltimateAuthPolicies(services); + services.Configure(configuration.GetSection("UltimateAuth:Server")); + + return services.AddUltimateAuthServerInternal(); + } + + public static IServiceCollection AddUltimateAuthServer(this IServiceCollection services, Action configure) + { + services.AddUltimateAuth(); + AddUsersInternal(services); + AddCredentialsInternal(services); + AddAuthorizationInternal(services); + AddUltimateAuthPolicies(services); + services.Configure(configure); + + return services.AddUltimateAuthServerInternal(); + } + + private static IServiceCollection AddUltimateAuthServerInternal(this IServiceCollection services) + { + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + + services.TryAddSingleton(sp => + { + var keyProvider = sp.GetRequiredService(); + var key = keyProvider.Resolve(null); + + return new HmacSha256TokenHasher(((SymmetricSecurityKey)key.Key).Key); + }); + + + // ----------------------------- + // OPTIONS VALIDATION + // ----------------------------- + services.TryAddEnumerable(ServiceDescriptor.Singleton, UAuthServerOptionsValidator>()); + + // Tenant Resolution + services.TryAddSingleton(sp => + { + var opts = sp.GetRequiredService>().Value; + + var resolvers = new List(); + + if (opts.EnableRoute) + resolvers.Add(new PathTenantResolver()); + + if (opts.EnableHeader) + resolvers.Add(new HeaderTenantResolver(opts.HeaderName)); + + if (opts.EnableDomain) + resolvers.Add(new HostTenantResolver()); + + return resolvers.Count switch + { + 0 => new NullTenantResolver(), + 1 => resolvers[0], + _ => new CompositeTenantResolver(resolvers) + }; + }); + + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + // Public resolver + services.TryAddScoped(); + services.TryAddScoped(); + + services.TryAddScoped(typeof(IUAuthFlowService<>), typeof(UAuthFlowService<>)); + services.TryAddScoped(); + services.TryAddScoped(); + + services.TryAddSingleton(); + + // TODO: Allow custom cookie manager via options + //services.AddSingleton(); + //if (options.CustomCookieManagerType is not null) + //{ + // services.AddSingleton(typeof(IUAuthSessionCookieManager), options.CustomCookieManagerType); + //} + //else + //{ + // services.AddSingleton(); + //} + + services.TryAddScoped(); + services.TryAddScoped(); + + services.AddHttpContextAccessor(); + services.TryAddScoped(typeof(IUserAccessor), typeof(UAuthUserAccessor)); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + + services.TryAddScoped(); + services.TryAddScoped(typeof(ILoginOrchestrator<>), typeof(LoginOrchestrator<>)); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddScoped(); + services.TryAddScoped(); + + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddSingleton(); + services.TryAddScoped(); + + services.TryAddSingleton(); + services.TryAddScoped(); + + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddScoped(); + + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddSingleton(); + services.TryAddScoped(); + + services.TryAddScoped(); + + // ----------------------------- + // ENDPOINTS + // ----------------------------- + services.TryAddScoped(); + services.TryAddSingleton(); + + services.TryAddScoped>(); + services.TryAddScoped(); + + services.TryAddScoped(); + services.TryAddScoped(); + + services.TryAddScoped>(); + services.TryAddScoped(); + + services.TryAddScoped(); + services.TryAddScoped(); + + services.TryAddScoped>(); + services.TryAddScoped(); + + return services; + } + + internal static IServiceCollection AddUltimateAuthPolicies(this IServiceCollection services, Action? configure = null) + { + if (services.Any(d => d.ServiceType == typeof(AccessPolicyRegistry))) + throw new InvalidOperationException("UltimateAuth policies already registered."); + + var registry = new AccessPolicyRegistry(); + + DefaultPolicySet.Register(registry); + configure?.Invoke(registry); + + services.AddSingleton(registry); + + services.AddSingleton(sp => + { + var r = sp.GetRequiredService(); + return r.Build(); + }); + + services.AddScoped(); + + services.TryAddScoped(sp => + { + var invariants = sp.GetServices(); + var globalPolicies = sp.GetServices(); + return new UAuthAccessAuthority(invariants, globalPolicies); + }); + + services.TryAddScoped(); + + return services; + } + + // ========================= + // Users (Framework-Required) + // ========================= + internal static IServiceCollection AddUsersInternal(IServiceCollection services) + { + services.TryAddScoped(typeof(IUserAccessor), typeof(UAuthUserAccessor)); + services.TryAddScoped(); + return services; + } + + // ========================= + // Credentials (Framework-Required) + // ========================= + internal static IServiceCollection AddCredentialsInternal(IServiceCollection services) + { + services.TryAddScoped(); + return services; + } + + // ========================= + // Authorization (Framework-Required) + // ========================= + internal static IServiceCollection AddAuthorizationInternal(IServiceCollection services) + { + services.TryAddScoped(typeof(IUserClaimsProvider), typeof(AuthorizationClaimsProvider)); + return services; + } + + + public static IServiceCollection AddUAuthServerInfrastructure(this IServiceCollection services) + { + // Flow orchestration + services.TryAddScoped(typeof(IUAuthFlowService<>), typeof(UAuthFlowService<>)); + + // Issuers + services.TryAddScoped(typeof(ISessionIssuer), typeof(UAuthSessionIssuer)); + services.TryAddScoped(typeof(ITokenIssuer), typeof(UAuthTokenIssuer)); + + // Endpoints + services.TryAddSingleton(); + + // Cookie management (default) + services.TryAddSingleton(); + + return services; + } + +} + +internal sealed class NullTenantResolver : ITenantIdResolver +{ + public Task ResolveTenantIdAsync(TenantResolutionContext context) => Task.FromResult(null); +} diff --git a/src/CodeBeam.UltimateAuth.Server/Extensions/TenantResolutionContextExtensions.cs b/src/CodeBeam.UltimateAuth.Server/Extensions/TenantResolutionContextExtensions.cs deleted file mode 100644 index 11a98d17..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Extensions/TenantResolutionContextExtensions.cs +++ /dev/null @@ -1,25 +0,0 @@ -//using Microsoft.AspNetCore.Http; -//using CodeBeam.UltimateAuth.Core.MultiTenancy; - -//namespace CodeBeam.UltimateAuth.Server.MultiTenancy -//{ -// public static class TenantResolutionContextExtensions -// { -// public static TenantResolutionContext FromHttpContext(this HttpContext ctx) -// { -// var headers = ctx.Request.Headers -// .ToDictionary( -// h => h.Key, -// h => h.Value.ToString(), -// StringComparer.OrdinalIgnoreCase); - -// return new TenantResolutionContext -// { -// Headers = headers, -// Host = ctx.Request.Host.Host, -// Path = ctx.Request.Path.Value, -// RawContext = ctx -// }; -// } -// } -//} diff --git a/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthApplicationBuilderExtensions.cs b/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthApplicationBuilderExtensions.cs index 9981235b..5d3cf175 100644 --- a/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthApplicationBuilderExtensions.cs +++ b/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthApplicationBuilderExtensions.cs @@ -1,18 +1,16 @@ using CodeBeam.UltimateAuth.Server.Middlewares; using Microsoft.AspNetCore.Builder; -namespace CodeBeam.UltimateAuth.Server.Extensions +namespace CodeBeam.UltimateAuth.Server.Extensions; + +public static class UltimateAuthApplicationBuilderExtensions { - public static class UltimateAuthApplicationBuilderExtensions + public static IApplicationBuilder UseUltimateAuthServer(this IApplicationBuilder app) { - public static IApplicationBuilder UseUltimateAuthServer(this IApplicationBuilder app) - { - app.UseMiddleware(); - app.UseMiddleware(); - app.UseMiddleware(); + app.UseMiddleware(); + app.UseMiddleware(); + app.UseMiddleware(); - return app; - } + return app; } - } diff --git a/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthServerOptionsExtensions.cs b/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthServerOptionsExtensions.cs index 61ad1d3f..07c268da 100644 --- a/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthServerOptionsExtensions.cs +++ b/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthServerOptionsExtensions.cs @@ -1,13 +1,12 @@ using CodeBeam.UltimateAuth.Core; using CodeBeam.UltimateAuth.Server.Options; -namespace CodeBeam.UltimateAuth.Server.Extensions +namespace CodeBeam.UltimateAuth.Server.Extensions; + +public static class UAuthServerOptionsExtensions { - public static class UAuthServerOptionsExtensions + public static void ConfigureMode(this UAuthServerOptions options, UAuthMode mode, Action configure) { - public static void ConfigureMode(this UAuthServerOptions options, UAuthMode mode, Action configure) - { - options.ModeConfigurations[mode] = configure; - } + options.ModeConfigurations[mode] = configure; } } diff --git a/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthServerServiceCollectionExtensions.cs b/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthServerServiceCollectionExtensions.cs deleted file mode 100644 index 7b520733..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthServerServiceCollectionExtensions.cs +++ /dev/null @@ -1,344 +0,0 @@ -using CodeBeam.UltimateAuth.Authorization; -using CodeBeam.UltimateAuth.Core; -using CodeBeam.UltimateAuth.Core.Abstractions; -using CodeBeam.UltimateAuth.Core.Domain; -using CodeBeam.UltimateAuth.Core.Extensions; -using CodeBeam.UltimateAuth.Core.Infrastructure; -using CodeBeam.UltimateAuth.Core.MultiTenancy; -using CodeBeam.UltimateAuth.Core.Options; -using CodeBeam.UltimateAuth.Credentials; -using CodeBeam.UltimateAuth.Policies.Abstractions; -using CodeBeam.UltimateAuth.Policies.Defaults; -using CodeBeam.UltimateAuth.Policies.Registry; -using CodeBeam.UltimateAuth.Server.Abstactions; -using CodeBeam.UltimateAuth.Server.Abstractions; -using CodeBeam.UltimateAuth.Server.Auth; -using CodeBeam.UltimateAuth.Server.Cookies; -using CodeBeam.UltimateAuth.Server.Endpoints; -using CodeBeam.UltimateAuth.Server.Infrastructure; -using CodeBeam.UltimateAuth.Server.Infrastructure.Hub; -using CodeBeam.UltimateAuth.Server.Infrastructure.Session; -using CodeBeam.UltimateAuth.Server.Issuers; -using CodeBeam.UltimateAuth.Server.Login; -using CodeBeam.UltimateAuth.Server.Login.Orchestrators; -using CodeBeam.UltimateAuth.Server.MultiTenancy; -using CodeBeam.UltimateAuth.Server.Options; -using CodeBeam.UltimateAuth.Server.Services; -using CodeBeam.UltimateAuth.Server.Stores; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Options; -using Microsoft.IdentityModel.Tokens; - -namespace CodeBeam.UltimateAuth.Server.Extensions -{ - public static class UAuthServerServiceCollectionExtensions - { - public static IServiceCollection AddUltimateAuthServer(this IServiceCollection services) - { - services.AddUltimateAuth(); - AddUsersInternal(services); - AddCredentialsInternal(services); - AddUltimateAuthPolicies(services); - return services.AddUltimateAuthServerInternal(); - } - - public static IServiceCollection AddUltimateAuthServer(this IServiceCollection services, IConfiguration configuration) - { - services.AddUltimateAuth(configuration); - AddUsersInternal(services); - AddCredentialsInternal(services); - AddUltimateAuthPolicies(services); - services.Configure(configuration.GetSection("UltimateAuth:Server")); - - return services.AddUltimateAuthServerInternal(); - } - - public static IServiceCollection AddUltimateAuthServer(this IServiceCollection services, Action configure) - { - services.AddUltimateAuth(); - AddUsersInternal(services); - AddCredentialsInternal(services); - AddUltimateAuthPolicies(services); - services.Configure(configure); - - return services.AddUltimateAuthServerInternal(); - } - - private static IServiceCollection AddUltimateAuthServerInternal(this IServiceCollection services) - { - //services.AddSingleton(); - //services.PostConfigure(o => - //{ - // if (!o.AutoDetectClientProfile || o.ClientProfile != UAuthClientProfile.NotSpecified) - // return; - - // using var sp = services.BuildServiceProvider(); - // var detector = sp.GetRequiredService(); - // o.ClientProfile = detector.Detect(sp); - //}); - - //services.AddOptions() - // .PostConfigure>((server, core) => - // { - // ConfigureDefaults.ApplyClientProfileDefaults(server, core.Value); - // ConfigureDefaults.ApplyModeDefaults(server); - // ConfigureDefaults.ApplyAuthResponseDefaults(server, core.Value); - // }); - - services.TryAddSingleton(); - services.TryAddSingleton(); - services.TryAddSingleton(); - - services.TryAddSingleton(sp => - { - var keyProvider = sp.GetRequiredService(); - var key = keyProvider.Resolve(null); - - return new HmacSha256TokenHasher( - ((SymmetricSecurityKey)key.Key).Key); - }); - - - // ----------------------------- - // OPTIONS VALIDATION - // ----------------------------- - services.TryAddEnumerable(ServiceDescriptor.Singleton, UAuthServerOptionsValidator>()); - - // ----------------------------- - // TENANT RESOLUTION - // ----------------------------- - services.TryAddSingleton(sp => - { - var opts = sp.GetRequiredService>().Value; - - var resolvers = new List(); - - if (opts.EnableRoute) - resolvers.Add(new PathTenantResolver()); - - if (opts.EnableHeader) - resolvers.Add(new HeaderTenantResolver(opts.HeaderName)); - - if (opts.EnableDomain) - resolvers.Add(new HostTenantResolver()); - - return resolvers.Count switch - { - 0 => new NullTenantResolver(), - 1 => resolvers[0], - _ => new CompositeTenantResolver(resolvers) - }; - }); - - // Inner resolvers - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - - // Public resolver - services.TryAddScoped(); - - services.TryAddScoped(); - - services.TryAddScoped(typeof(IUAuthFlowService<>), typeof(UAuthFlowService<>)); - services.TryAddScoped(typeof(IRefreshFlowService), typeof(DefaultRefreshFlowService)); - services.TryAddScoped(typeof(IUAuthSessionManager), typeof(UAuthSessionManager)); - - services.TryAddSingleton(); - - // TODO: Allow custom cookie manager via options - //services.AddSingleton(); - //if (options.CustomCookieManagerType is not null) - //{ - // services.AddSingleton(typeof(IUAuthSessionCookieManager), options.CustomCookieManagerType); - //} - //else - //{ - // services.AddSingleton(); - //} - - // ----------------------------- - // SESSION / TOKEN ISSUERS - // ----------------------------- - services.TryAddScoped(typeof(ISessionIssuer), typeof(UAuthSessionIssuer)); - services.TryAddScoped(typeof(ITokenIssuer), typeof(UAuthTokenIssuer)); - - services.TryAddScoped(typeof(IUserAccessor), typeof(UAuthUserAccessor)); - services.TryAddScoped(); - - //services.TryAddScoped(typeof(IUserAuthenticator<>), typeof(DefaultUserAuthenticator<>)); - services.TryAddScoped(typeof(ISessionOrchestrator), typeof(UAuthSessionOrchestrator)); - services.TryAddScoped(typeof(ILoginOrchestrator<>), typeof(DefaultLoginOrchestrator<>)); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(typeof(ISessionQueryService), typeof(UAuthSessionQueryService)); - services.TryAddScoped(typeof(IRefreshTokenResolver), typeof(DefaultRefreshTokenResolver)); - services.TryAddScoped(typeof(ISessionTouchService), typeof(DefaultSessionTouchService)); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - - services.TryAddSingleton(); - services.TryAddSingleton(); - services.TryAddSingleton(); - services.TryAddSingleton(); - services.TryAddSingleton(); - services.TryAddScoped(); - services.TryAddSingleton(); - services.TryAddScoped(); - - services.TryAddScoped(typeof(IRefreshTokenValidator), typeof(DefaultRefreshTokenValidator)); - services.TryAddScoped(); - services.TryAddScoped(typeof(IRefreshTokenRotationService), typeof(RefreshTokenRotationService)); - - services.TryAddSingleton(); - services.TryAddSingleton(); - services.TryAddSingleton(); - services.TryAddScoped(); - - services.TryAddScoped(); - - services.TryAddSingleton(); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - - services.TryAddScoped(); - - // ----------------------------- - // ENDPOINTS - // ----------------------------- - services.AddHttpContextAccessor(); - - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - - services.TryAddScoped(); - - services.TryAddSingleton(); - - // Endpoint handlers - services.TryAddScoped>(); - services.TryAddScoped(); - - services.TryAddScoped(); - services.TryAddScoped(); - - services.TryAddScoped>(); - services.TryAddScoped(); - - services.TryAddScoped(); - services.TryAddScoped(); - - services.TryAddScoped>(); - services.TryAddScoped(); - - return services; - } - - internal static IServiceCollection AddUltimateAuthPolicies(this IServiceCollection services, Action? configure = null) - { - if (services.Any(d => d.ServiceType == typeof(AccessPolicyRegistry))) - throw new InvalidOperationException("UltimateAuth policies already registered."); - - var registry = new AccessPolicyRegistry(); - - DefaultPolicySet.Register(registry); - configure?.Invoke(registry); - - // 1. Registry (global, mutable until Build) - services.AddSingleton(registry); - - // 2. Compiled policy set (immutable, singleton) - services.AddSingleton(sp => - { - var r = sp.GetRequiredService(); - return r.Build(); - }); - - // 3. Policy provider MUST be scoped - services.AddScoped(); - - // 4. Authority (scoped, correct) - services.TryAddScoped(sp => - { - var invariants = sp.GetServices(); - var globalPolicies = sp.GetServices(); - return new DefaultAccessAuthority(invariants, globalPolicies); - }); - - // 5. Orchestrator (scoped) - services.TryAddScoped(); - - return services; - } - - // ========================= - // USERS (FRAMEWORK-REQUIRED) - // ========================= - internal static IServiceCollection AddUsersInternal(IServiceCollection services) - { - // Core user abstractions - services.TryAddScoped(typeof(IUserAccessor), typeof(UAuthUserAccessor)); - services.TryAddScoped(); - - // Security state - //services.TryAddScoped(typeof(IUserSecurityEvents<>), typeof(DefaultUserSecurityEvents<>)); - - // TODO: Move this into AddAuthorizaionInternal method - services.TryAddScoped(typeof(IUserClaimsProvider), typeof(DefaultAuthorizationClaimsProvider)); - - return services; - } - - // ========================= - // CREDENTIALS (FRAMEWORK-REQUIRED) - // ========================= - internal static IServiceCollection AddCredentialsInternal(IServiceCollection services) - { - services.TryAddScoped(); - return services; - } - - - public static IServiceCollection AddUAuthServerInfrastructure(this IServiceCollection services) - { - // Flow orchestration - services.TryAddScoped(typeof(IUAuthFlowService<>), typeof(UAuthFlowService<>)); - - // Issuers - services.TryAddScoped(typeof(ISessionIssuer), typeof(UAuthSessionIssuer)); - services.TryAddScoped(typeof(ITokenIssuer), typeof(UAuthTokenIssuer)); - - // Endpoints - services.TryAddSingleton(); - - // Cookie management (default) - services.TryAddSingleton(); - - return services; - } - - } - - internal sealed class NullTenantResolver : ITenantIdResolver - { - public Task ResolveTenantIdAsync(TenantResolutionContext context) => Task.FromResult(null); - } - -} diff --git a/src/CodeBeam.UltimateAuth.Server/Flows/Login/ILoginAuthority.cs b/src/CodeBeam.UltimateAuth.Server/Flows/Login/ILoginAuthority.cs new file mode 100644 index 00000000..2c99ca03 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Flows/Login/ILoginAuthority.cs @@ -0,0 +1,16 @@ +namespace CodeBeam.UltimateAuth.Server.Flows; + +/// +/// Represents the authority responsible for making login decisions. +/// This authority determines whether a login attempt is allowed, +/// denied, or requires additional verification (e.g. MFA). +/// +public interface ILoginAuthority +{ + /// + /// Evaluates a login attempt based on the provided decision context. + /// + /// The login decision context. + /// The login decision. + LoginDecision Decide(LoginDecisionContext context); +} diff --git a/src/CodeBeam.UltimateAuth.Server/Flows/Login/ILoginOrchestrator.cs b/src/CodeBeam.UltimateAuth.Server/Flows/Login/ILoginOrchestrator.cs new file mode 100644 index 00000000..f8f8eac4 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Flows/Login/ILoginOrchestrator.cs @@ -0,0 +1,15 @@ +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Server.Auth; + +namespace CodeBeam.UltimateAuth.Server.Flows; + +/// +/// Orchestrates the login flow. +/// Responsible for executing the login process by coordinating +/// credential validation, user resolution, authority decision, +/// and session creation. +/// +public interface ILoginOrchestrator +{ + Task LoginAsync(AuthFlowContext flow, LoginRequest request, CancellationToken ct = default); +} diff --git a/src/CodeBeam.UltimateAuth.Server/Flows/Login/LoginAuthority.cs b/src/CodeBeam.UltimateAuth.Server/Flows/Login/LoginAuthority.cs new file mode 100644 index 00000000..defffe30 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Flows/Login/LoginAuthority.cs @@ -0,0 +1,33 @@ +namespace CodeBeam.UltimateAuth.Server.Flows; + +/// +/// Default implementation of the login authority. +/// Applies basic security checks for login attempts. +/// +public sealed class LoginAuthority : ILoginAuthority +{ + public LoginDecision Decide(LoginDecisionContext context) + { + if (!context.CredentialsValid) + { + return LoginDecision.Deny("Invalid credentials."); + } + + if (!context.UserExists || context.UserKey is null) + { + return LoginDecision.Deny("Invalid credentials."); + } + + var state = context.SecurityState; + if (state is not null) + { + if (state.IsLocked) + return LoginDecision.Deny("user_is_locked"); + + if (state.RequiresReauthentication) + return LoginDecision.Challenge("reauth_required"); + } + + return LoginDecision.Allow(); + } +} diff --git a/src/CodeBeam.UltimateAuth.Server/Flows/Login/LoginDecision.cs b/src/CodeBeam.UltimateAuth.Server/Flows/Login/LoginDecision.cs new file mode 100644 index 00000000..68db4061 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Flows/Login/LoginDecision.cs @@ -0,0 +1,25 @@ +namespace CodeBeam.UltimateAuth.Server.Flows; + +/// +/// Represents the outcome of a login decision. +/// +public sealed class LoginDecision +{ + public LoginDecisionKind Kind { get; } + public string? Reason { get; } + + private LoginDecision(LoginDecisionKind kind, string? reason = null) + { + Kind = kind; + Reason = reason; + } + + public static LoginDecision Allow() + => new(LoginDecisionKind.Allow); + + public static LoginDecision Deny(string reason) + => new(LoginDecisionKind.Deny, reason); + + public static LoginDecision Challenge(string reason) + => new(LoginDecisionKind.Challenge, reason); +} diff --git a/src/CodeBeam.UltimateAuth.Server/Flows/Login/LoginDecisionContext.cs b/src/CodeBeam.UltimateAuth.Server/Flows/Login/LoginDecisionContext.cs new file mode 100644 index 00000000..72a53af6 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Flows/Login/LoginDecisionContext.cs @@ -0,0 +1,50 @@ +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; +using CodeBeam.UltimateAuth.Users; + +namespace CodeBeam.UltimateAuth.Server.Flows; + +/// +/// Represents all information required by the login authority +/// to make a login decision. +/// +public sealed class LoginDecisionContext +{ + /// + /// Gets the tenant identifier. + /// + public TenantKey Tenant { get; init; } + + /// + /// Gets the login identifier (e.g. username or email). + /// + public required string Identifier { get; init; } + + /// + /// Indicates whether the provided credentials were successfully validated. + /// + public bool CredentialsValid { get; init; } + + /// + /// Gets the resolved user identifier if available. + /// + public UserKey? UserKey { get; init; } + + /// + /// Gets the user security state if the user could be resolved. + /// + public IUserSecurityState? SecurityState { get; init; } + + /// + /// Indicates whether the user exists. + /// This allows the authority to distinguish between + /// invalid credentials and non-existent users. + /// + public bool UserExists { get; init; } + + /// + /// Indicates whether this login attempt is part of a chained flow + /// (e.g. reauthentication, MFA completion). + /// + public bool IsChained { get; init; } +} diff --git a/src/CodeBeam.UltimateAuth.Server/Flows/Login/LoginDecisionKind.cs b/src/CodeBeam.UltimateAuth.Server/Flows/Login/LoginDecisionKind.cs new file mode 100644 index 00000000..e4044bd7 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Flows/Login/LoginDecisionKind.cs @@ -0,0 +1,8 @@ +namespace CodeBeam.UltimateAuth.Server.Flows; + +public enum LoginDecisionKind +{ + Allow = 1, + Deny = 2, + Challenge = 3 +} diff --git a/src/CodeBeam.UltimateAuth.Server/Flows/Login/LoginOrchestrator.cs b/src/CodeBeam.UltimateAuth.Server/Flows/Login/LoginOrchestrator.cs new file mode 100644 index 00000000..40e008dc --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Flows/Login/LoginOrchestrator.cs @@ -0,0 +1,167 @@ +using CodeBeam.UltimateAuth.Core; +using CodeBeam.UltimateAuth.Core.Abstractions; +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Credentials; +using CodeBeam.UltimateAuth.Server.Abstactions; +using CodeBeam.UltimateAuth.Server.Auth; +using CodeBeam.UltimateAuth.Server.Extensions; +using CodeBeam.UltimateAuth.Server.Infrastructure; +using CodeBeam.UltimateAuth.Users; + +namespace CodeBeam.UltimateAuth.Server.Flows; + +internal sealed class LoginOrchestrator : ILoginOrchestrator +{ + private readonly ICredentialStore _credentialStore; // authentication + private readonly ICredentialValidator _credentialValidator; + private readonly IUserRuntimeStateProvider _users; // eligible + private readonly IUserSecurityStateProvider _userSecurityStateProvider; // runtime risk + private readonly ILoginAuthority _authority; + private readonly ISessionOrchestrator _sessionOrchestrator; + private readonly ITokenIssuer _tokens; + private readonly IUserClaimsProvider _claimsProvider; + private readonly IUserIdConverterResolver _userIdConverterResolver; + + public LoginOrchestrator( + ICredentialStore credentialStore, + ICredentialValidator credentialValidator, + IUserRuntimeStateProvider users, + IUserSecurityStateProvider userSecurityStateProvider, + ILoginAuthority authority, + ISessionOrchestrator sessionOrchestrator, + ITokenIssuer tokens, + IUserClaimsProvider claimsProvider, + IUserIdConverterResolver userIdConverterResolver) + { + _credentialStore = credentialStore; + _credentialValidator = credentialValidator; + _users = users; + _userSecurityStateProvider = userSecurityStateProvider; + _authority = authority; + _sessionOrchestrator = sessionOrchestrator; + _tokens = tokens; + _claimsProvider = claimsProvider; + _userIdConverterResolver = userIdConverterResolver; + } + + public async Task LoginAsync(AuthFlowContext flow, LoginRequest request, CancellationToken ct = default) + { + ct.ThrowIfCancellationRequested(); + + var now = request.At ?? DateTimeOffset.UtcNow; + + var credentials = await _credentialStore.FindByLoginAsync(request.Tenant, request.Identifier, ct); + var orderedCredentials = credentials + .OfType() + .Where(c => c.Security.IsUsable(now)) + .Cast>() + .ToList(); + + TUserId validatedUserId = default!; + bool credentialsValid = false; + + foreach (var credential in orderedCredentials) + { + var result = await _credentialValidator.ValidateAsync(credential, request.Secret, ct); + + if (result.IsValid) + { + validatedUserId = credential.UserId; + credentialsValid = true; + break; + } + } + + bool userExists = credentialsValid; + + IUserSecurityState? securityState = null; + UserKey? userKey = null; + + if (credentialsValid) + { + securityState = await _userSecurityStateProvider.GetAsync(request.Tenant, validatedUserId, ct); + var converter = _userIdConverterResolver.GetConverter(); + userKey = UserKey.FromString(converter.ToCanonicalString(validatedUserId)); + } + + var user = userKey is not null + ? await _users.GetAsync(request.Tenant, userKey.Value, ct) + : null; + + if (user is null || user.IsDeleted || !user.IsActive) + { + // Deliberately vague + return LoginResult.Failed(); + } + + var decisionContext = new LoginDecisionContext + { + Tenant = request.Tenant, + Identifier = request.Identifier, + CredentialsValid = credentialsValid, + UserExists = userExists, + UserKey = userKey, + SecurityState = securityState, + IsChained = request.ChainId is not null + }; + + var decision = _authority.Decide(decisionContext); + + if (decision.Kind == LoginDecisionKind.Deny) + return LoginResult.Failed(); + + if (decision.Kind == LoginDecisionKind.Challenge) + { + return LoginResult.Continue(new LoginContinuation + { + Type = LoginContinuationType.Mfa, + Hint = decision.Reason + }); + } + + if (userKey is not UserKey validUserKey) + { + return LoginResult.Failed(); + } + + var claims = await _claimsProvider.GetClaimsAsync(request.Tenant, validUserKey, ct); + + var sessionContext = new AuthenticatedSessionContext + { + Tenant = request.Tenant, + UserKey = validUserKey, + Now = now, + Device = request.Device, + Claims = claims, + ChainId = request.ChainId, + Metadata = SessionMetadata.Empty + }; + + var authContext = flow.ToAuthContext(now); + var issuedSession = await _sessionOrchestrator.ExecuteAsync(authContext, new CreateLoginSessionCommand(sessionContext), ct); + + AuthTokens? tokens = null; + + if (request.RequestTokens) + { + var tokenContext = new TokenIssuanceContext + { + Tenant = request.Tenant, + UserKey = validUserKey, + SessionId = issuedSession.Session.SessionId, + ChainId = request.ChainId, + Claims = claims.AsDictionary() + }; + + tokens = new AuthTokens + { + AccessToken = await _tokens.IssueAccessTokenAsync(flow, tokenContext, ct), + RefreshToken = await _tokens.IssueRefreshTokenAsync(flow, tokenContext, RefreshTokenPersistence.Persist, ct) + }; + } + + return LoginResult.Success(issuedSession.Session.SessionId, tokens); + + } +} diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Pkce/IPkceAuthorizationValidator.cs b/src/CodeBeam.UltimateAuth.Server/Flows/Pkce/IPkceAuthorizationValidator.cs similarity index 77% rename from src/CodeBeam.UltimateAuth.Server/Infrastructure/Pkce/IPkceAuthorizationValidator.cs rename to src/CodeBeam.UltimateAuth.Server/Flows/Pkce/IPkceAuthorizationValidator.cs index cef6bdaa..e0bafe9a 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Pkce/IPkceAuthorizationValidator.cs +++ b/src/CodeBeam.UltimateAuth.Server/Flows/Pkce/IPkceAuthorizationValidator.cs @@ -1,4 +1,4 @@ -namespace CodeBeam.UltimateAuth.Server.Infrastructure; +namespace CodeBeam.UltimateAuth.Server.Flows; public interface IPkceAuthorizationValidator { diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Pkce/PkceAuthorizationArtifact.cs b/src/CodeBeam.UltimateAuth.Server/Flows/Pkce/PkceAuthorizationArtifact.cs similarity index 96% rename from src/CodeBeam.UltimateAuth.Server/Infrastructure/Pkce/PkceAuthorizationArtifact.cs rename to src/CodeBeam.UltimateAuth.Server/Flows/Pkce/PkceAuthorizationArtifact.cs index 605a8179..3f1fdc72 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Pkce/PkceAuthorizationArtifact.cs +++ b/src/CodeBeam.UltimateAuth.Server/Flows/Pkce/PkceAuthorizationArtifact.cs @@ -1,7 +1,7 @@ using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Server.Stores; -namespace CodeBeam.UltimateAuth.Server.Infrastructure; +namespace CodeBeam.UltimateAuth.Server.Flows; /// /// Represents a PKCE authorization process that has been initiated diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Pkce/PkceAuthorizationValidator.cs b/src/CodeBeam.UltimateAuth.Server/Flows/Pkce/PkceAuthorizationValidator.cs similarity index 97% rename from src/CodeBeam.UltimateAuth.Server/Infrastructure/Pkce/PkceAuthorizationValidator.cs rename to src/CodeBeam.UltimateAuth.Server/Flows/Pkce/PkceAuthorizationValidator.cs index 49b617f2..0265ce7f 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Pkce/PkceAuthorizationValidator.cs +++ b/src/CodeBeam.UltimateAuth.Server/Flows/Pkce/PkceAuthorizationValidator.cs @@ -2,7 +2,7 @@ using System.Security.Cryptography; using System.Text; -namespace CodeBeam.UltimateAuth.Server.Infrastructure; +namespace CodeBeam.UltimateAuth.Server.Flows; internal sealed class PkceAuthorizationValidator : IPkceAuthorizationValidator { diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Pkce/PkceAuthorizeRequest.cs b/src/CodeBeam.UltimateAuth.Server/Flows/Pkce/PkceAuthorizeRequest.cs similarity index 78% rename from src/CodeBeam.UltimateAuth.Server/Infrastructure/Pkce/PkceAuthorizeRequest.cs rename to src/CodeBeam.UltimateAuth.Server/Flows/Pkce/PkceAuthorizeRequest.cs index d1115964..50622f30 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Pkce/PkceAuthorizeRequest.cs +++ b/src/CodeBeam.UltimateAuth.Server/Flows/Pkce/PkceAuthorizeRequest.cs @@ -1,4 +1,4 @@ -namespace CodeBeam.UltimateAuth.Server.Infrastructure; +namespace CodeBeam.UltimateAuth.Server.Flows; internal sealed class PkceAuthorizeRequest { diff --git a/src/CodeBeam.UltimateAuth.Server/Flows/Pkce/PkceChallengeMethod.cs b/src/CodeBeam.UltimateAuth.Server/Flows/Pkce/PkceChallengeMethod.cs new file mode 100644 index 00000000..287d8406 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Flows/Pkce/PkceChallengeMethod.cs @@ -0,0 +1,6 @@ +namespace CodeBeam.UltimateAuth.Server.Flows; + +public enum PkceChallengeMethod +{ + S256 +} diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Pkce/PkceContextSnapshot.cs b/src/CodeBeam.UltimateAuth.Server/Flows/Pkce/PkceContextSnapshot.cs similarity index 95% rename from src/CodeBeam.UltimateAuth.Server/Infrastructure/Pkce/PkceContextSnapshot.cs rename to src/CodeBeam.UltimateAuth.Server/Flows/Pkce/PkceContextSnapshot.cs index 2de64d01..37c10714 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Pkce/PkceContextSnapshot.cs +++ b/src/CodeBeam.UltimateAuth.Server/Flows/Pkce/PkceContextSnapshot.cs @@ -1,7 +1,7 @@ using CodeBeam.UltimateAuth.Core.MultiTenancy; using CodeBeam.UltimateAuth.Core.Options; -namespace CodeBeam.UltimateAuth.Server.Infrastructure; +namespace CodeBeam.UltimateAuth.Server.Flows; /// /// Immutable snapshot of relevant request and client context diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Pkce/PkceValidationFailureReason.cs b/src/CodeBeam.UltimateAuth.Server/Flows/Pkce/PkceValidationFailureReason.cs similarity index 75% rename from src/CodeBeam.UltimateAuth.Server/Infrastructure/Pkce/PkceValidationFailureReason.cs rename to src/CodeBeam.UltimateAuth.Server/Flows/Pkce/PkceValidationFailureReason.cs index 6f3dc072..29a6eef5 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Pkce/PkceValidationFailureReason.cs +++ b/src/CodeBeam.UltimateAuth.Server/Flows/Pkce/PkceValidationFailureReason.cs @@ -1,4 +1,4 @@ -namespace CodeBeam.UltimateAuth.Server.Infrastructure; +namespace CodeBeam.UltimateAuth.Server.Flows; public enum PkceValidationFailureReason { diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Pkce/PkceValidationResult.cs b/src/CodeBeam.UltimateAuth.Server/Flows/Pkce/PkceValidationResult.cs similarity index 89% rename from src/CodeBeam.UltimateAuth.Server/Infrastructure/Pkce/PkceValidationResult.cs rename to src/CodeBeam.UltimateAuth.Server/Flows/Pkce/PkceValidationResult.cs index ea7b6051..a6c0dae9 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Pkce/PkceValidationResult.cs +++ b/src/CodeBeam.UltimateAuth.Server/Flows/Pkce/PkceValidationResult.cs @@ -1,4 +1,4 @@ -namespace CodeBeam.UltimateAuth.Server.Infrastructure; +namespace CodeBeam.UltimateAuth.Server.Flows; public sealed class PkceValidationResult { diff --git a/src/CodeBeam.UltimateAuth.Server/Flows/Refresh/IRefreshResponsePolicy.cs b/src/CodeBeam.UltimateAuth.Server/Flows/Refresh/IRefreshResponsePolicy.cs new file mode 100644 index 00000000..fbb34c10 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Flows/Refresh/IRefreshResponsePolicy.cs @@ -0,0 +1,11 @@ +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Server.Auth; + +namespace CodeBeam.UltimateAuth.Server.Flows; + +public interface IRefreshResponsePolicy +{ + CredentialKind SelectPrimary(AuthFlowContext flow, RefreshFlowRequest request, RefreshFlowResult result); + bool WriteRefreshToken(AuthFlowContext flow); +} diff --git a/src/CodeBeam.UltimateAuth.Server/Flows/Refresh/IRefreshResponseWriter.cs b/src/CodeBeam.UltimateAuth.Server/Flows/Refresh/IRefreshResponseWriter.cs new file mode 100644 index 00000000..9318e534 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Flows/Refresh/IRefreshResponseWriter.cs @@ -0,0 +1,9 @@ +using CodeBeam.UltimateAuth.Core.Domain; +using Microsoft.AspNetCore.Http; + +namespace CodeBeam.UltimateAuth.Server.Flows; + +public interface IRefreshResponseWriter +{ + void Write(HttpContext context, RefreshOutcome outcome); +} diff --git a/src/CodeBeam.UltimateAuth.Server/Flows/Refresh/IRefreshService.cs b/src/CodeBeam.UltimateAuth.Server/Flows/Refresh/IRefreshService.cs new file mode 100644 index 00000000..71c821e0 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Flows/Refresh/IRefreshService.cs @@ -0,0 +1,9 @@ +namespace CodeBeam.UltimateAuth.Server.Flows; + +/// +/// Base contract for refresh-related services. +/// Refresh services renew authentication artifacts according to AuthMode. +/// +public interface IRefreshService +{ +} diff --git a/src/CodeBeam.UltimateAuth.Server/Flows/Refresh/ISessionTouchService.cs b/src/CodeBeam.UltimateAuth.Server/Flows/Refresh/ISessionTouchService.cs new file mode 100644 index 00000000..fc16eef8 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Flows/Refresh/ISessionTouchService.cs @@ -0,0 +1,12 @@ +using CodeBeam.UltimateAuth.Core.Contracts; + +namespace CodeBeam.UltimateAuth.Server.Flows; + +/// +/// Refreshes session lifecycle artifacts. +/// Used by PureOpaque and Hybrid modes. +/// +public interface ISessionTouchService : IRefreshService +{ + Task RefreshAsync(SessionValidationResult validation, SessionTouchPolicy policy, SessionTouchMode sessionTouchMode, DateTimeOffset now, CancellationToken ct = default); +} diff --git a/src/CodeBeam.UltimateAuth.Server/Flows/Refresh/RefreshDecision.cs b/src/CodeBeam.UltimateAuth.Server/Flows/Refresh/RefreshDecision.cs new file mode 100644 index 00000000..00c9eb6d --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Flows/Refresh/RefreshDecision.cs @@ -0,0 +1,29 @@ +namespace CodeBeam.UltimateAuth.Server.Flows; + +/// +/// Determines which authentication artifacts can be refreshed +/// for the current AuthMode. +/// This is a server-side decision and must be enforced centrally. +/// +public enum RefreshDecision +{ + /// + /// Refresh endpoint is disabled for this mode. + /// + NotSupported = 0, + + /// + /// Only session lifetime is extended. + /// No access / refresh token issued. + /// (PureOpaque) + /// + SessionTouch = 1, + + /// + /// Refresh token is rotated and + /// a new access token is issued. + /// Session MAY also be touched depending on policy. + /// (Hybrid, SemiHybrid, PureJwt) + /// + TokenRotation = 2 +} diff --git a/src/CodeBeam.UltimateAuth.Server/Flows/Refresh/RefreshDecisionResolver.cs b/src/CodeBeam.UltimateAuth.Server/Flows/Refresh/RefreshDecisionResolver.cs new file mode 100644 index 00000000..8108d0bb --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Flows/Refresh/RefreshDecisionResolver.cs @@ -0,0 +1,24 @@ +using CodeBeam.UltimateAuth.Core; + +namespace CodeBeam.UltimateAuth.Server.Flows; + +/// +/// Resolves refresh behavior based on AuthMode. +/// This class is the single source of truth for refresh capability. +/// +public static class RefreshDecisionResolver +{ + public static RefreshDecision Resolve(UAuthMode mode) + { + return mode switch + { + UAuthMode.PureOpaque => RefreshDecision.SessionTouch, + + UAuthMode.Hybrid + or UAuthMode.SemiHybrid + or UAuthMode.PureJwt => RefreshDecision.TokenRotation, + + _ => RefreshDecision.NotSupported + }; + } +} diff --git a/src/CodeBeam.UltimateAuth.Server/Flows/Refresh/RefreshEvaluationResult.cs b/src/CodeBeam.UltimateAuth.Server/Flows/Refresh/RefreshEvaluationResult.cs new file mode 100644 index 00000000..f5a7f856 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Flows/Refresh/RefreshEvaluationResult.cs @@ -0,0 +1,5 @@ +using CodeBeam.UltimateAuth.Core.Domain; + +namespace CodeBeam.UltimateAuth.Server.Flows; + +internal sealed record RefreshEvaluationResult(RefreshOutcome Outcome); diff --git a/src/CodeBeam.UltimateAuth.Server/Flows/Refresh/RefreshResponsePolicy.cs b/src/CodeBeam.UltimateAuth.Server/Flows/Refresh/RefreshResponsePolicy.cs new file mode 100644 index 00000000..6e909cdb --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Flows/Refresh/RefreshResponsePolicy.cs @@ -0,0 +1,44 @@ +using CodeBeam.UltimateAuth.Core; +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.Options; +using CodeBeam.UltimateAuth.Server.Auth; + +namespace CodeBeam.UltimateAuth.Server.Flows; + +internal class RefreshResponsePolicy : IRefreshResponsePolicy +{ + public CredentialKind SelectPrimary(AuthFlowContext flow, RefreshFlowRequest request, RefreshFlowResult result) + { + if (flow.EffectiveMode == UAuthMode.PureOpaque) + return CredentialKind.Session; + + if (flow.EffectiveMode == UAuthMode.PureJwt) + return CredentialKind.AccessToken; + + if (!string.IsNullOrWhiteSpace(request.RefreshToken) && request.SessionId == null) + { + return CredentialKind.AccessToken; + } + + if (request.SessionId != null) + { + return CredentialKind.Session; + } + + if (flow.ClientProfile == UAuthClientProfile.Api) + return CredentialKind.AccessToken; + + return CredentialKind.Session; + } + + + public bool WriteRefreshToken(AuthFlowContext flow) + { + if (flow.EffectiveMode != UAuthMode.PureOpaque) + return true; + + return false; + } + +} diff --git a/src/CodeBeam.UltimateAuth.Server/Flows/Refresh/RefreshResponseWriter.cs b/src/CodeBeam.UltimateAuth.Server/Flows/Refresh/RefreshResponseWriter.cs new file mode 100644 index 00000000..caf1f95e --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Flows/Refresh/RefreshResponseWriter.cs @@ -0,0 +1,31 @@ +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Server.Options; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; + +namespace CodeBeam.UltimateAuth.Server.Flows; + +internal sealed class RefreshResponseWriter : IRefreshResponseWriter +{ + private readonly UAuthDiagnosticsOptions _diagnostics; + + public RefreshResponseWriter(IOptions options) + { + _diagnostics = options.Value.Diagnostics; + } + + public void Write(HttpContext context, RefreshOutcome outcome) + { + if (!_diagnostics.EnableRefreshHeaders) + return; + + context.Response.Headers["X-UAuth-Refresh"] = outcome switch + { + RefreshOutcome.NoOp => "no-op", + RefreshOutcome.Touched => "touched", + RefreshOutcome.ReauthRequired => "reauth-required", + _ => "unknown" + }; + } + +} diff --git a/src/CodeBeam.UltimateAuth.Server/Flows/Refresh/RefreshStrategyResolver.cs b/src/CodeBeam.UltimateAuth.Server/Flows/Refresh/RefreshStrategyResolver.cs new file mode 100644 index 00000000..52da1aa4 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Flows/Refresh/RefreshStrategyResolver.cs @@ -0,0 +1,20 @@ +using CodeBeam.UltimateAuth.Core; +using CodeBeam.UltimateAuth.Core.Contracts; +using System.Security; + +namespace CodeBeam.UltimateAuth.Server.Flows; + +public class RefreshStrategyResolver +{ + public static RefreshStrategy Resolve(UAuthMode mode) + { + return mode switch + { + UAuthMode.PureOpaque => RefreshStrategy.SessionOnly, + UAuthMode.PureJwt => RefreshStrategy.TokenOnly, + UAuthMode.SemiHybrid => RefreshStrategy.TokenWithSessionCheck, + UAuthMode.Hybrid => RefreshStrategy.SessionAndToken, + _ => throw new SecurityException("Unsupported refresh mode") + }; + } +} diff --git a/src/CodeBeam.UltimateAuth.Server/Flows/Refresh/RefreshTokenResolver.cs b/src/CodeBeam.UltimateAuth.Server/Flows/Refresh/RefreshTokenResolver.cs new file mode 100644 index 00000000..3140b965 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Flows/Refresh/RefreshTokenResolver.cs @@ -0,0 +1,40 @@ +using CodeBeam.UltimateAuth.Server.Abstractions; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; + +namespace CodeBeam.UltimateAuth.Server.Flows; + +internal sealed class RefreshTokenResolver : IRefreshTokenResolver +{ + private const string DefaultCookieName = "uar"; + private const string BearerPrefix = "Bearer "; + private const string RefreshHeaderName = "X-Refresh-Token"; + + public string? Resolve(HttpContext context) + { + if (context.Request.Cookies.TryGetValue(DefaultCookieName, out var cookieToken) && + !string.IsNullOrWhiteSpace(cookieToken)) + { + return cookieToken; + } + + if (context.Request.Headers.TryGetValue("Authorization", out StringValues authHeader)) + { + var value = authHeader.ToString(); + if (value.StartsWith(BearerPrefix, StringComparison.OrdinalIgnoreCase)) + { + var token = value.Substring(BearerPrefix.Length).Trim(); + if (!string.IsNullOrWhiteSpace(token)) + return token; + } + } + + if (context.Request.Headers.TryGetValue(RefreshHeaderName, out var headerToken) && + !string.IsNullOrWhiteSpace(headerToken)) + { + return headerToken.ToString(); + } + + return null; + } +} diff --git a/src/CodeBeam.UltimateAuth.Server/Flows/Refresh/SessionTouchPolicy.cs b/src/CodeBeam.UltimateAuth.Server/Flows/Refresh/SessionTouchPolicy.cs new file mode 100644 index 00000000..e72e8ad3 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Flows/Refresh/SessionTouchPolicy.cs @@ -0,0 +1,6 @@ +namespace CodeBeam.UltimateAuth.Server.Flows; + +public sealed class SessionTouchPolicy +{ + public TimeSpan? TouchInterval { get; init; } +} diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/DefaultSessionTouchService.cs b/src/CodeBeam.UltimateAuth.Server/Flows/Refresh/SessionTouchService.cs similarity index 88% rename from src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/DefaultSessionTouchService.cs rename to src/CodeBeam.UltimateAuth.Server/Flows/Refresh/SessionTouchService.cs index 885e955b..5df517b3 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/DefaultSessionTouchService.cs +++ b/src/CodeBeam.UltimateAuth.Server/Flows/Refresh/SessionTouchService.cs @@ -1,13 +1,13 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Contracts; -namespace CodeBeam.UltimateAuth.Server.Infrastructure; +namespace CodeBeam.UltimateAuth.Server.Flows; -public sealed class DefaultSessionTouchService : ISessionTouchService +public sealed class SessionTouchService : ISessionTouchService { private readonly ISessionStoreKernelFactory _kernelFactory; - public DefaultSessionTouchService(ISessionStoreKernelFactory kernelFactory) + public SessionTouchService(ISessionStoreKernelFactory kernelFactory) { _kernelFactory = kernelFactory; } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/DefaultAccessPolicyProvider.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/AccessPolicyProvider.cs similarity index 76% rename from src/CodeBeam.UltimateAuth.Server/Infrastructure/DefaultAccessPolicyProvider.cs rename to src/CodeBeam.UltimateAuth.Server/Infrastructure/AccessPolicyProvider.cs index 008ca145..0a991193 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/DefaultAccessPolicyProvider.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/AccessPolicyProvider.cs @@ -5,12 +5,12 @@ namespace CodeBeam.UltimateAuth.Server.Infrastructure; -internal sealed class DefaultAccessPolicyProvider : IAccessPolicyProvider +internal sealed class AccessPolicyProvider : IAccessPolicyProvider { private readonly CompiledAccessPolicySet _set; private readonly IServiceProvider _services; - public DefaultAccessPolicyProvider(CompiledAccessPolicySet set, IServiceProvider services) + public AccessPolicyProvider(CompiledAccessPolicySet set, IServiceProvider services) { _set = set; _services = services; diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/AuthRedirectResolver.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/AuthRedirectResolver.cs index c0ed6ffe..54667497 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/AuthRedirectResolver.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/AuthRedirectResolver.cs @@ -2,64 +2,62 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; -namespace CodeBeam.UltimateAuth.Server.Infrastructure +namespace CodeBeam.UltimateAuth.Server.Infrastructure; + +public sealed class AuthRedirectResolver { - public sealed class AuthRedirectResolver + private readonly UAuthServerOptions _options; + + public AuthRedirectResolver(IOptions options) { - private readonly UAuthServerOptions _options; + _options = options.Value; + } - public AuthRedirectResolver(IOptions options) + // TODO: Add allowed origins validation + public string ResolveClientBase(HttpContext ctx) + { + if (ctx.Request.Query.TryGetValue("returnUrl", out var returnUrl) && + Uri.TryCreate(returnUrl!, UriKind.Absolute, out var ru)) { - _options = options.Value; + return ru.GetLeftPart(UriPartial.Authority); } - // TODO: Add allowed origins validation - public string ResolveClientBase(HttpContext ctx) + if (ctx.Request.Headers.TryGetValue("Origin", out var origin) && + Uri.TryCreate(origin!, UriKind.Absolute, out var originUri)) { - if (ctx.Request.Query.TryGetValue("returnUrl", out var returnUrl) && - Uri.TryCreate(returnUrl!, UriKind.Absolute, out var ru)) - { - return ru.GetLeftPart(UriPartial.Authority); - } - - if (ctx.Request.Headers.TryGetValue("Origin", out var origin) && - Uri.TryCreate(origin!, UriKind.Absolute, out var originUri)) - { - return originUri.GetLeftPart(UriPartial.Authority); - } - - if (ctx.Request.Headers.TryGetValue("Referer", out var referer) && - Uri.TryCreate(referer!, UriKind.Absolute, out var refUri)) - { - return refUri.GetLeftPart(UriPartial.Authority); - } - - if (!string.IsNullOrWhiteSpace(_options.Hub.ClientBaseAddress)) - return _options.Hub.ClientBaseAddress; - - return $"{ctx.Request.Scheme}://{ctx.Request.Host}"; + return originUri.GetLeftPart(UriPartial.Authority); } - public string ResolveRedirect(HttpContext ctx, string path, IDictionary? query = null) + if (ctx.Request.Headers.TryGetValue("Referer", out var referer) && + Uri.TryCreate(referer!, UriKind.Absolute, out var refUri)) { - var url = Combine(ResolveClientBase(ctx), path); + return refUri.GetLeftPart(UriPartial.Authority); + } - if (query is null || query.Count == 0) - return url; + if (!string.IsNullOrWhiteSpace(_options.Hub.ClientBaseAddress)) + return _options.Hub.ClientBaseAddress; - var qs = string.Join("&", query - .Where(kv => !string.IsNullOrWhiteSpace(kv.Value)) - .Select(kv => $"{kv.Key}={Uri.EscapeDataString(kv.Value!)}")); + return $"{ctx.Request.Scheme}://{ctx.Request.Host}"; + } - return string.IsNullOrWhiteSpace(qs) - ? url - : $"{url}?{qs}"; - } + public string ResolveRedirect(HttpContext ctx, string path, IDictionary? query = null) + { + var url = Combine(ResolveClientBase(ctx), path); - private static string Combine(string baseUri, string path) - { - return baseUri.TrimEnd('/') + "/" + path.TrimStart('/'); - } + if (query is null || query.Count == 0) + return url; + + var qs = string.Join("&", query + .Where(kv => !string.IsNullOrWhiteSpace(kv.Value)) + .Select(kv => $"{kv.Key}={Uri.EscapeDataString(kv.Value!)}")); + + return string.IsNullOrWhiteSpace(qs) + ? url + : $"{url}?{qs}"; } + private static string Combine(string baseUri, string path) + { + return baseUri.TrimEnd('/') + "/" + path.TrimStart('/'); + } } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/AspNetCore/ITransportCredentialResolver.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/AspNetCore/ITransportCredentialResolver.cs index 593e592c..22015398 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/AspNetCore/ITransportCredentialResolver.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/AspNetCore/ITransportCredentialResolver.cs @@ -1,9 +1,8 @@ using Microsoft.AspNetCore.Http; -namespace CodeBeam.UltimateAuth.Server.Infrastructure +namespace CodeBeam.UltimateAuth.Server.Infrastructure; + +public interface ITransportCredentialResolver { - public interface ITransportCredentialResolver - { - TransportCredential? Resolve(HttpContext context); - } + TransportCredential? Resolve(HttpContext context); } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/AspNetCore/TransportCredentialKind.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/AspNetCore/TransportCredentialKind.cs index 2638040c..a33ad8bd 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/AspNetCore/TransportCredentialKind.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/AspNetCore/TransportCredentialKind.cs @@ -1,10 +1,9 @@ -namespace CodeBeam.UltimateAuth.Server.Infrastructure +namespace CodeBeam.UltimateAuth.Server.Infrastructure; + +public enum TransportCredentialKind { - public enum TransportCredentialKind - { - Session, - AccessToken, - RefreshToken, - Hub - } + Session, + AccessToken, + RefreshToken, + Hub } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/AspNetCore/DefaultTransportCredentialResolver.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/AspNetCore/TransportCredentialResolver.cs similarity index 87% rename from src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/AspNetCore/DefaultTransportCredentialResolver.cs rename to src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/AspNetCore/TransportCredentialResolver.cs index ee283b56..71cfcff3 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/AspNetCore/DefaultTransportCredentialResolver.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/AspNetCore/TransportCredentialResolver.cs @@ -5,11 +5,11 @@ namespace CodeBeam.UltimateAuth.Server.Infrastructure; -internal sealed class DefaultTransportCredentialResolver : ITransportCredentialResolver +internal sealed class TransportCredentialResolver : ITransportCredentialResolver { private readonly IOptionsMonitor _server; - public DefaultTransportCredentialResolver(IOptionsMonitor server) + public TransportCredentialResolver(IOptionsMonitor server) { _server = server; } @@ -18,31 +18,24 @@ public DefaultTransportCredentialResolver(IOptionsMonitor se { var cookies = _server.CurrentValue.Cookie; - // 1️⃣ Authorization header (Bearer) if (TryFromAuthorizationHeader(context, out var bearer)) return bearer; - // 2️⃣ Cookies (session / refresh / access) if (TryFromCookies(context, cookies, out var cookie)) return cookie; - // 3️⃣ Query (legacy / special flows) if (TryFromQuery(context, out var query)) return query; - // 4️⃣ Body (rare, but possible – PKCE / device flows) if (TryFromBody(context, out var body)) return body; - // 5️⃣ Hub / external authority if (TryFromHub(context, out var hub)) return hub; return null; } - // ---------- resolvers ---------- - // TODO: Make scheme configurable, shouldn't be hard coded private static bool TryFromAuthorizationHeader(HttpContext ctx, out TransportCredential credential) { @@ -157,12 +150,11 @@ private static bool TryReadCookie(HttpContext ctx, string name, out string value } private static TransportCredential Build(HttpContext ctx, TransportCredentialKind kind, string value) - => new() - { - Kind = kind, - Value = value, - TenantId = ctx.GetTenant().Value, - Device = ctx.GetDevice() - }; - + => new() + { + Kind = kind, + Value = value, + TenantId = ctx.GetTenant().Value, + Device = ctx.GetDevice() + }; } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/DefaultCredentialResponseWriter.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/CredentialResponseWriter.cs similarity index 93% rename from src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/DefaultCredentialResponseWriter.cs rename to src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/CredentialResponseWriter.cs index 31f07bac..0334dd82 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/DefaultCredentialResponseWriter.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/CredentialResponseWriter.cs @@ -4,20 +4,19 @@ using CodeBeam.UltimateAuth.Server.Abstractions; using CodeBeam.UltimateAuth.Server.Auth; using CodeBeam.UltimateAuth.Server.Cookies; -using CodeBeam.UltimateAuth.Server.Infrastructure; using CodeBeam.UltimateAuth.Server.Options; using Microsoft.AspNetCore.Http; -namespace CodeBeam.UltimateAuth.Server; +namespace CodeBeam.UltimateAuth.Server.Infrastructure; -internal sealed class DefaultCredentialResponseWriter : ICredentialResponseWriter +internal sealed class CredentialResponseWriter : ICredentialResponseWriter { private readonly IAuthFlowContextAccessor _authContext; private readonly IUAuthCookieManager _cookieManager; private readonly IUAuthCookiePolicyBuilder _cookiePolicy; private readonly IUAuthHeaderPolicyBuilder _headerPolicy; - public DefaultCredentialResponseWriter( + public CredentialResponseWriter( IAuthFlowContextAccessor authContext, IUAuthCookieManager cookieManager, IUAuthCookiePolicyBuilder cookiePolicy, diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/DefaultFlowCredentialResolver.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/DefaultFlowCredentialResolver.cs deleted file mode 100644 index 4f6d7cad..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/DefaultFlowCredentialResolver.cs +++ /dev/null @@ -1,91 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Domain; -using CodeBeam.UltimateAuth.Core.Options; -using CodeBeam.UltimateAuth.Server.Abstractions; -using CodeBeam.UltimateAuth.Server.Auth; -using CodeBeam.UltimateAuth.Server.Extensions; -using Microsoft.AspNetCore.Http; - -namespace CodeBeam.UltimateAuth.Server.Infrastructure -{ - internal sealed class DefaultFlowCredentialResolver : IFlowCredentialResolver - { - private readonly IPrimaryCredentialResolver _primaryResolver; - - public DefaultFlowCredentialResolver(IPrimaryCredentialResolver primaryResolver) - { - _primaryResolver = primaryResolver; - } - - public ResolvedCredential? Resolve(HttpContext context, EffectiveAuthResponse response) - { - var kind = _primaryResolver.Resolve(context); - - return kind switch - { - PrimaryCredentialKind.Stateful => ResolveSession(context, response), - PrimaryCredentialKind.Stateless => ResolveAccessToken(context, response), - - _ => null - }; - } - - private static ResolvedCredential? ResolveSession(HttpContext context, EffectiveAuthResponse response) - { - var delivery = response.SessionIdDelivery; - - if (delivery.Mode != TokenResponseMode.Cookie) - return null; - - var cookie = delivery.Cookie; - if (cookie is null) - return null; - - if (!context.Request.Cookies.TryGetValue(cookie.Name, out var raw)) - return null; - - if (string.IsNullOrWhiteSpace(raw)) - return null; - - return new ResolvedCredential - { - Kind = PrimaryCredentialKind.Stateful, - Value = raw.Trim(), - Tenant = context.GetTenant(), - Device = context.GetDevice() - }; - } - - private static ResolvedCredential? ResolveAccessToken(HttpContext context, EffectiveAuthResponse response) - { - var delivery = response.AccessTokenDelivery; - - if (delivery.Mode != TokenResponseMode.Header) - return null; - - var headerName = delivery.Name ?? "Authorization"; - - if (!context.Request.Headers.TryGetValue(headerName, out var header)) - return null; - - var value = header.ToString(); - - if (delivery.HeaderFormat == HeaderTokenFormat.Bearer && - value.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) - { - value = value["Bearer ".Length..].Trim(); - } - - if (string.IsNullOrWhiteSpace(value)) - return null; - - return new ResolvedCredential - { - Kind = PrimaryCredentialKind.Stateless, - Value = value, - Tenant = context.GetTenant(), - Device = context.GetDevice() - }; - } - - } -} diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/DefaultPrimaryCredentialResolver.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/DefaultPrimaryCredentialResolver.cs deleted file mode 100644 index 90a2b016..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/DefaultPrimaryCredentialResolver.cs +++ /dev/null @@ -1,39 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Domain; -using CodeBeam.UltimateAuth.Server.Abstractions; -using CodeBeam.UltimateAuth.Server.Options; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Options; - -namespace CodeBeam.UltimateAuth.Server.Infrastructure -{ - // TODO: Enhance class (endpoint-based, tenant-based, route-based) - internal sealed class DefaultPrimaryCredentialResolver : IPrimaryCredentialResolver - { - private readonly UAuthServerOptions _options; - - public DefaultPrimaryCredentialResolver(IOptions options) - { - _options = options.Value; - } - - public PrimaryCredentialKind Resolve(HttpContext context) - { - if (IsApiRequest(context)) - return _options.PrimaryCredential.Api; - - return _options.PrimaryCredential.Ui; - } - - private static bool IsApiRequest(HttpContext context) - { - if (context.Request.Path.StartsWithSegments("/api")) - return true; - - if (context.Request.Headers.ContainsKey("Authorization")) - return true; - - return false; - } - } - -} diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/DefaultUAuthBodyPolicyBuilder.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/DefaultUAuthBodyPolicyBuilder.cs deleted file mode 100644 index 4b35299c..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/DefaultUAuthBodyPolicyBuilder.cs +++ /dev/null @@ -1,13 +0,0 @@ -using CodeBeam.UltimateAuth.Server.Auth; -using CodeBeam.UltimateAuth.Server.Options; - -namespace CodeBeam.UltimateAuth.Server.Infrastructure -{ - internal class DefaultUAuthBodyPolicyBuilder : IUAuthBodyPolicyBuilder - { - public object BuildBodyValue(string rawValue, CredentialResponseOptions response, AuthFlowContext context) - { - throw new NotImplementedException(); - } - } -} diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/DefaultUAuthHeaderPolicyBuilder.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/DefaultUAuthHeaderPolicyBuilder.cs deleted file mode 100644 index 0e628c34..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/DefaultUAuthHeaderPolicyBuilder.cs +++ /dev/null @@ -1,23 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Options; -using CodeBeam.UltimateAuth.Server.Auth; -using CodeBeam.UltimateAuth.Server.Options; - -namespace CodeBeam.UltimateAuth.Server.Infrastructure -{ - internal sealed class DefaultUAuthHeaderPolicyBuilder : IUAuthHeaderPolicyBuilder - { - public string BuildHeaderValue(string rawValue, CredentialResponseOptions response, AuthFlowContext context) - { - if (string.IsNullOrWhiteSpace(rawValue)) - throw new ArgumentException("Header value cannot be empty.", nameof(rawValue)); - - return response.HeaderFormat switch - { - HeaderTokenFormat.Bearer => $"Bearer {rawValue}", - HeaderTokenFormat.Raw => rawValue, - - _ => throw new InvalidOperationException($"Unsupported header token format: {response.HeaderFormat}") - }; - } - } -} diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/FlowCredentialResolver.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/FlowCredentialResolver.cs new file mode 100644 index 00000000..ffe1092d --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/FlowCredentialResolver.cs @@ -0,0 +1,91 @@ +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.Options; +using CodeBeam.UltimateAuth.Server.Abstractions; +using CodeBeam.UltimateAuth.Server.Auth; +using CodeBeam.UltimateAuth.Server.Contracts; +using CodeBeam.UltimateAuth.Server.Extensions; +using Microsoft.AspNetCore.Http; + +namespace CodeBeam.UltimateAuth.Server.Infrastructure; + +internal sealed class FlowCredentialResolver : IFlowCredentialResolver +{ + private readonly IPrimaryCredentialResolver _primaryResolver; + + public FlowCredentialResolver(IPrimaryCredentialResolver primaryResolver) + { + _primaryResolver = primaryResolver; + } + + public ResolvedCredential? Resolve(HttpContext context, EffectiveAuthResponse response) + { + var kind = _primaryResolver.Resolve(context); + + return kind switch + { + PrimaryCredentialKind.Stateful => ResolveSession(context, response), + PrimaryCredentialKind.Stateless => ResolveAccessToken(context, response), + + _ => null + }; + } + + private static ResolvedCredential? ResolveSession(HttpContext context, EffectiveAuthResponse response) + { + var delivery = response.SessionIdDelivery; + + if (delivery.Mode != TokenResponseMode.Cookie) + return null; + + var cookie = delivery.Cookie; + if (cookie is null) + return null; + + if (!context.Request.Cookies.TryGetValue(cookie.Name, out var raw)) + return null; + + if (string.IsNullOrWhiteSpace(raw)) + return null; + + return new ResolvedCredential + { + Kind = PrimaryCredentialKind.Stateful, + Value = raw.Trim(), + Tenant = context.GetTenant(), + Device = context.GetDevice() + }; + } + + private static ResolvedCredential? ResolveAccessToken(HttpContext context, EffectiveAuthResponse response) + { + var delivery = response.AccessTokenDelivery; + + if (delivery.Mode != TokenResponseMode.Header) + return null; + + var headerName = delivery.Name ?? "Authorization"; + + if (!context.Request.Headers.TryGetValue(headerName, out var header)) + return null; + + var value = header.ToString(); + + if (delivery.HeaderFormat == HeaderTokenFormat.Bearer && + value.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) + { + value = value["Bearer ".Length..].Trim(); + } + + if (string.IsNullOrWhiteSpace(value)) + return null; + + return new ResolvedCredential + { + Kind = PrimaryCredentialKind.Stateless, + Value = value, + Tenant = context.GetTenant(), + Device = context.GetDevice() + }; + } + +} diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/IFlowCredentialResolver.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/IFlowCredentialResolver.cs index 9c849c63..a798db95 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/IFlowCredentialResolver.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/IFlowCredentialResolver.cs @@ -1,15 +1,14 @@ -using CodeBeam.UltimateAuth.Server.Abstractions; -using CodeBeam.UltimateAuth.Server.Auth; +using CodeBeam.UltimateAuth.Server.Auth; +using CodeBeam.UltimateAuth.Server.Contracts; using Microsoft.AspNetCore.Http; -namespace CodeBeam.UltimateAuth.Server.Infrastructure +namespace CodeBeam.UltimateAuth.Server.Infrastructure; + +/// +/// Gets the credential from the HTTP context. +/// IPrimaryCredentialResolver is used to determine which kind of credential to resolve. +/// +public interface IFlowCredentialResolver { - /// - /// Gets the credential from the HTTP context. - /// IPrimaryCredentialResolver is used to determine which kind of credential to resolve. - /// - public interface IFlowCredentialResolver - { - ResolvedCredential? Resolve(HttpContext context, EffectiveAuthResponse response); - } + ResolvedCredential? Resolve(HttpContext context, EffectiveAuthResponse response); } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/PrimaryCredentialResolver.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/PrimaryCredentialResolver.cs new file mode 100644 index 00000000..595f7aae --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/PrimaryCredentialResolver.cs @@ -0,0 +1,37 @@ +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Server.Abstractions; +using CodeBeam.UltimateAuth.Server.Options; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; + +namespace CodeBeam.UltimateAuth.Server.Infrastructure; + +// TODO: Enhance class (endpoint-based, tenant-based, route-based) +internal sealed class PrimaryCredentialResolver : IPrimaryCredentialResolver +{ + private readonly UAuthServerOptions _options; + + public PrimaryCredentialResolver(IOptions options) + { + _options = options.Value; + } + + public PrimaryCredentialKind Resolve(HttpContext context) + { + if (IsApiRequest(context)) + return _options.PrimaryCredential.Api; + + return _options.PrimaryCredential.Ui; + } + + private static bool IsApiRequest(HttpContext context) + { + if (context.Request.Path.StartsWithSegments("/api")) + return true; + + if (context.Request.Headers.ContainsKey("Authorization")) + return true; + + return false; + } +} diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/UAuthBodyPolicyBuilder.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/UAuthBodyPolicyBuilder.cs new file mode 100644 index 00000000..8aac2412 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/UAuthBodyPolicyBuilder.cs @@ -0,0 +1,12 @@ +using CodeBeam.UltimateAuth.Server.Auth; +using CodeBeam.UltimateAuth.Server.Options; + +namespace CodeBeam.UltimateAuth.Server.Infrastructure; + +internal class UAuthBodyPolicyBuilder : IUAuthBodyPolicyBuilder +{ + public object BuildBodyValue(string rawValue, CredentialResponseOptions response, AuthFlowContext context) + { + throw new NotImplementedException(); + } +} diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/UAuthHeaderPolicyBuilder.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/UAuthHeaderPolicyBuilder.cs new file mode 100644 index 00000000..49789b5e --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/UAuthHeaderPolicyBuilder.cs @@ -0,0 +1,21 @@ +using CodeBeam.UltimateAuth.Core.Options; +using CodeBeam.UltimateAuth.Server.Auth; +using CodeBeam.UltimateAuth.Server.Options; + +namespace CodeBeam.UltimateAuth.Server.Infrastructure; + +internal sealed class UAuthHeaderPolicyBuilder : IUAuthHeaderPolicyBuilder +{ + public string BuildHeaderValue(string rawValue, CredentialResponseOptions response, AuthFlowContext context) + { + if (string.IsNullOrWhiteSpace(rawValue)) + throw new ArgumentException("Header value cannot be empty.", nameof(rawValue)); + + return response.HeaderFormat switch + { + HeaderTokenFormat.Bearer => $"Bearer {rawValue}", + HeaderTokenFormat.Raw => rawValue, + _ => throw new InvalidOperationException($"Unsupported header token format: {response.HeaderFormat}") + }; + } +} diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/DefaultJwtTokenGenerator.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/DefaultJwtTokenGenerator.cs deleted file mode 100644 index 4d9c9634..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/DefaultJwtTokenGenerator.cs +++ /dev/null @@ -1,69 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Abstractions; -using CodeBeam.UltimateAuth.Core.Domain; -using CodeBeam.UltimateAuth.Core.MultiTenancy; -using CodeBeam.UltimateAuth.Server.Abstractions; -using Microsoft.IdentityModel.JsonWebTokens; -using Microsoft.IdentityModel.Tokens; - -namespace CodeBeam.UltimateAuth.Server.Infrastructure -{ - public sealed class DefaultJwtTokenGenerator : IJwtTokenGenerator - { - private readonly IJwtSigningKeyProvider _keyProvider; - private readonly JsonWebTokenHandler _handler = new(); - - public DefaultJwtTokenGenerator(IJwtSigningKeyProvider keyProvider) - { - _keyProvider = keyProvider; - } - - public string CreateToken(UAuthJwtTokenDescriptor descriptor) - { - var signingKey = _keyProvider.Resolve(descriptor.KeyId); - - var tokenDescriptor = new SecurityTokenDescriptor - { - Issuer = descriptor.Issuer, - Audience = descriptor.Audience, - Subject = null, - NotBefore = descriptor.IssuedAt.UtcDateTime, - IssuedAt = descriptor.IssuedAt.UtcDateTime, - Expires = descriptor.ExpiresAt.UtcDateTime, - - Claims = BuildClaims(descriptor), - - SigningCredentials = new SigningCredentials( - signingKey.Key, - signingKey.Algorithm) - }; - - tokenDescriptor.AdditionalHeaderClaims = new Dictionary - { - ["kid"] = signingKey.KeyId - }; - - return _handler.CreateToken(tokenDescriptor); - } - - private static IDictionary BuildClaims(UAuthJwtTokenDescriptor descriptor) - { - var claims = new Dictionary - { - ["sub"] = descriptor.Subject - }; - - claims["tenant"] = descriptor.Tenant; - - if (descriptor.Claims is not null) - { - foreach (var kv in descriptor.Claims) - { - claims[kv.Key] = kv.Value; - } - } - - return claims; - } - } - -} diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/DefaultOpaqueTokenGenerator.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/DefaultOpaqueTokenGenerator.cs deleted file mode 100644 index 8a31f216..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/DefaultOpaqueTokenGenerator.cs +++ /dev/null @@ -1,11 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Abstractions; - -namespace CodeBeam.UltimateAuth.Server.Infrastructure -{ - internal sealed class DefaultOpaqueTokenGenerator : IOpaqueTokenGenerator - { - public string Generate(int bytes) - => Convert.ToBase64String(System.Security.Cryptography.RandomNumberGenerator.GetBytes(bytes)); - } - -} diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/DevelopmentJwtSigningKeyProvider.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/DevelopmentJwtSigningKeyProvider.cs index d3eac437..975f6c1f 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/DevelopmentJwtSigningKeyProvider.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/DevelopmentJwtSigningKeyProvider.cs @@ -1,32 +1,32 @@ using CodeBeam.UltimateAuth.Server.Abstractions; +using CodeBeam.UltimateAuth.Server.Contracts; using Microsoft.IdentityModel.Tokens; using System.Text; -namespace CodeBeam.UltimateAuth.Server.Infrastructure +namespace CodeBeam.UltimateAuth.Server.Infrastructure; + +public sealed class DevelopmentJwtSigningKeyProvider : IJwtSigningKeyProvider { - public sealed class DevelopmentJwtSigningKeyProvider : IJwtSigningKeyProvider + private readonly JwtSigningKey _key; + + public DevelopmentJwtSigningKeyProvider() { - private readonly JwtSigningKey _key; + var rawKey = Encoding.UTF8.GetBytes("DEV_ONLY__ULTIMATEAUTH__DO_NOT_USE_IN_PROD"); - public DevelopmentJwtSigningKeyProvider() + _key = new JwtSigningKey { - var rawKey = Encoding.UTF8.GetBytes("DEV_ONLY__ULTIMATEAUTH__DO_NOT_USE_IN_PROD"); - - _key = new JwtSigningKey + KeyId = "dev-uauth", + Algorithm = SecurityAlgorithms.HmacSha256, + Key = new SymmetricSecurityKey(rawKey) { - KeyId = "dev-uauth", - Algorithm = SecurityAlgorithms.HmacSha256, - Key = new SymmetricSecurityKey(rawKey) - { - KeyId = "dev-uauth" - } - }; - } + KeyId = "dev-uauth" + } + }; + } - public JwtSigningKey Resolve(string? keyId) - { - // signing veya verify için tek key - return _key; - } + public JwtSigningKey Resolve(string? keyId) + { + // signing veya verify için tek key + return _key; } } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Device/DefaultDeviceContextFactory.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Device/DefaultDeviceContextFactory.cs deleted file mode 100644 index 5a69b57e..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Device/DefaultDeviceContextFactory.cs +++ /dev/null @@ -1,17 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Contracts; -using CodeBeam.UltimateAuth.Core.Domain; -using System.Security; - -namespace CodeBeam.UltimateAuth.Server.Infrastructure -{ - internal sealed class DefaultDeviceContextFactory : IDeviceContextFactory - { - public DeviceContext Create(DeviceInfo device) - { - if (string.IsNullOrWhiteSpace(device.DeviceId.Value)) - return DeviceContext.Anonymous(); - - return DeviceContext.FromDeviceId(device.DeviceId); - } - } -} diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Device/DefaultDeviceResolver.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Device/DefaultDeviceResolver.cs deleted file mode 100644 index a8c7cc9a..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Device/DefaultDeviceResolver.cs +++ /dev/null @@ -1,58 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Contracts; -using CodeBeam.UltimateAuth.Core.Domain; -using CodeBeam.UltimateAuth.Server.Abstractions; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Primitives; - -namespace CodeBeam.UltimateAuth.Server.Infrastructure -{ - public sealed class DefaultDeviceResolver : IDeviceResolver - { - public DeviceInfo Resolve(HttpContext context) - { - var request = context.Request; - - var rawDeviceId = ResolveRawDeviceId(context); - DeviceId.TryCreate(rawDeviceId, out var deviceId); - - return new DeviceInfo - { - DeviceId = deviceId, - Platform = ResolvePlatform(request), - UserAgent = request.Headers.UserAgent.ToString(), - IpAddress = context.Connection.RemoteIpAddress?.ToString() - }; - } - - - private static string? ResolveRawDeviceId(HttpContext context) - { - if (context.Request.Headers.TryGetValue("X-UDID", out var header)) - return header.ToString(); - - if (context.Request.HasFormContentType && context.Request.Form.TryGetValue("__uauth_device", out var formValue) && !StringValues.IsNullOrEmpty(formValue)) - { - return formValue.ToString(); - } - - if (context.Request.Cookies.TryGetValue("udid", out var cookie)) - return cookie; - - return null; - } - - private static string? ResolvePlatform(HttpRequest request) - { - var ua = request.Headers.UserAgent.ToString().ToLowerInvariant(); - - if (ua.Contains("android")) return "android"; - if (ua.Contains("iphone") || ua.Contains("ipad")) return "ios"; - if (ua.Contains("windows")) return "windows"; - if (ua.Contains("mac os")) return "macos"; - if (ua.Contains("linux")) return "linux"; - - return "web"; - } - - } -} diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Device/DeviceContextFactory.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Device/DeviceContextFactory.cs new file mode 100644 index 00000000..0a3a41b6 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Device/DeviceContextFactory.cs @@ -0,0 +1,15 @@ +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Domain; + +namespace CodeBeam.UltimateAuth.Server.Infrastructure; + +internal sealed class DeviceContextFactory : IDeviceContextFactory +{ + public DeviceContext Create(DeviceInfo device) + { + if (string.IsNullOrWhiteSpace(device.DeviceId.Value)) + return DeviceContext.Anonymous(); + + return DeviceContext.FromDeviceId(device.DeviceId); + } +} diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Device/DeviceResolver.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Device/DeviceResolver.cs new file mode 100644 index 00000000..682fd36f --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Device/DeviceResolver.cs @@ -0,0 +1,57 @@ +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Server.Abstractions; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; + +namespace CodeBeam.UltimateAuth.Server.Infrastructure; + +public sealed class DeviceResolver : IDeviceResolver +{ + public DeviceInfo Resolve(HttpContext context) + { + var request = context.Request; + + var rawDeviceId = ResolveRawDeviceId(context); + DeviceId.TryCreate(rawDeviceId, out var deviceId); + + return new DeviceInfo + { + DeviceId = deviceId, + Platform = ResolvePlatform(request), + UserAgent = request.Headers.UserAgent.ToString(), + IpAddress = context.Connection.RemoteIpAddress?.ToString() + }; + } + + + private static string? ResolveRawDeviceId(HttpContext context) + { + if (context.Request.Headers.TryGetValue("X-UDID", out var header)) + return header.ToString(); + + if (context.Request.HasFormContentType && context.Request.Form.TryGetValue("__uauth_device", out var formValue) && !StringValues.IsNullOrEmpty(formValue)) + { + return formValue.ToString(); + } + + if (context.Request.Cookies.TryGetValue("udid", out var cookie)) + return cookie; + + return null; + } + + private static string? ResolvePlatform(HttpRequest request) + { + var ua = request.Headers.UserAgent.ToString().ToLowerInvariant(); + + if (ua.Contains("android")) return "android"; + if (ua.Contains("iphone") || ua.Contains("ipad")) return "ios"; + if (ua.Contains("windows")) return "windows"; + if (ua.Contains("mac os")) return "macos"; + if (ua.Contains("linux")) return "linux"; + + return "web"; + } + +} diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Device/IDeviceContextFactory.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Device/IDeviceContextFactory.cs index 6ca4c192..b26db0de 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Device/IDeviceContextFactory.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Device/IDeviceContextFactory.cs @@ -1,10 +1,9 @@ using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Server.Infrastructure +namespace CodeBeam.UltimateAuth.Server.Infrastructure; + +public interface IDeviceContextFactory { - public interface IDeviceContextFactory - { - DeviceContext Create(DeviceInfo requestDevice); - } + DeviceContext Create(DeviceInfo requestDevice); } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/HmacSha256TokenHasher.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/HmacSha256TokenHasher.cs index ab493228..31b36020 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/HmacSha256TokenHasher.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/HmacSha256TokenHasher.cs @@ -2,36 +2,34 @@ using System.Security.Cryptography; using System.Text; -namespace CodeBeam.UltimateAuth.Server.Infrastructure +namespace CodeBeam.UltimateAuth.Server.Infrastructure; + +public sealed class HmacSha256TokenHasher : ITokenHasher { - public sealed class HmacSha256TokenHasher : ITokenHasher - { - private readonly byte[] _key; + private readonly byte[] _key; - public HmacSha256TokenHasher(byte[] key) - { - if (key is null || key.Length == 0) - throw new ArgumentException("Token hashing key must be provided.", nameof(key)); + public HmacSha256TokenHasher(byte[] key) + { + if (key is null || key.Length == 0) + throw new ArgumentException("Token hashing key must be provided.", nameof(key)); - _key = key; - } + _key = key; + } - public string Hash(string plaintext) - { - using var hmac = new HMACSHA256(_key); - var bytes = Encoding.UTF8.GetBytes(plaintext); - var hash = hmac.ComputeHash(bytes); - return Convert.ToBase64String(hash); - } + public string Hash(string plaintext) + { + using var hmac = new HMACSHA256(_key); + var bytes = Encoding.UTF8.GetBytes(plaintext); + var hash = hmac.ComputeHash(bytes); + return Convert.ToBase64String(hash); + } - public bool Verify(string plaintext, string hash) - { - var computed = Hash(plaintext); + public bool Verify(string plaintext, string hash) + { + var computed = Hash(plaintext); - return CryptographicOperations.FixedTimeEquals( - Convert.FromBase64String(computed), - Convert.FromBase64String(hash)); - } + return CryptographicOperations.FixedTimeEquals( + Convert.FromBase64String(computed), + Convert.FromBase64String(hash)); } - } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Hub/DefaultHubCredentialResolver.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Hub/DefaultHubCredentialResolver.cs deleted file mode 100644 index bfc20d41..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Hub/DefaultHubCredentialResolver.cs +++ /dev/null @@ -1,40 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Abstractions; -using CodeBeam.UltimateAuth.Core.Domain; -using CodeBeam.UltimateAuth.Server.Stores; - -namespace CodeBeam.UltimateAuth.Server.Infrastructure.Hub -{ - internal sealed class DefaultHubCredentialResolver : IHubCredentialResolver - { - private readonly IAuthStore _store; - - public DefaultHubCredentialResolver(IAuthStore store) - { - _store = store; - } - - public async Task ResolveAsync(HubSessionId hubSessionId, CancellationToken ct = default) - { - var artifact = await _store.GetAsync(new AuthArtifactKey(hubSessionId.Value), ct); - - if (artifact is not HubFlowArtifact flow) - return null; - - if (flow.IsCompleted) - return null; - - if (!flow.Payload.TryGet("authorization_code", out string? authorizationCode)) - return null; - - if (!flow.Payload.TryGet("code_verifier", out string? codeVerifier)) - return null; - - return new HubCredentials - { - AuthorizationCode = authorizationCode, - CodeVerifier = codeVerifier, - ClientProfile = flow.ClientProfile, - }; - } - } -} diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Hub/DefaultHubFlowReader.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Hub/DefaultHubFlowReader.cs deleted file mode 100644 index a49409ad..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Hub/DefaultHubFlowReader.cs +++ /dev/null @@ -1,41 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Abstractions; -using CodeBeam.UltimateAuth.Core.Domain; -using CodeBeam.UltimateAuth.Server.Stores; - -namespace CodeBeam.UltimateAuth.Server.Infrastructure -{ - internal sealed class DefaultHubFlowReader : IHubFlowReader - { - private readonly IAuthStore _store; - private readonly IClock _clock; - - public DefaultHubFlowReader(IAuthStore store, IClock clock) - { - _store = store; - _clock = clock; - } - - public async Task GetStateAsync(HubSessionId hubSessionId, CancellationToken ct = default) - { - var artifact = await _store.GetAsync(new AuthArtifactKey(hubSessionId.Value), ct); - - if (artifact is not HubFlowArtifact flow) - return null; - - var now = _clock.UtcNow; - - return new HubFlowState - { - Exists = true, - HubSessionId = flow.HubSessionId, - FlowType = flow.FlowType, - ClientProfile = flow.ClientProfile, - ReturnUrl = flow.ReturnUrl, - IsExpired = flow.IsExpired(now), - IsCompleted = flow.IsCompleted, - IsActive = !flow.IsExpired(now) && !flow.IsCompleted - }; - } - } - -} diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Hub/HubCredentialResolver.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Hub/HubCredentialResolver.cs new file mode 100644 index 00000000..0dd73836 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Hub/HubCredentialResolver.cs @@ -0,0 +1,39 @@ +using CodeBeam.UltimateAuth.Core.Abstractions; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Server.Stores; + +namespace CodeBeam.UltimateAuth.Server.Infrastructure; + +internal sealed class HubCredentialResolver : IHubCredentialResolver +{ + private readonly IAuthStore _store; + + public HubCredentialResolver(IAuthStore store) + { + _store = store; + } + + public async Task ResolveAsync(HubSessionId hubSessionId, CancellationToken ct = default) + { + var artifact = await _store.GetAsync(new AuthArtifactKey(hubSessionId.Value), ct); + + if (artifact is not HubFlowArtifact flow) + return null; + + if (flow.IsCompleted) + return null; + + if (!flow.Payload.TryGet("authorization_code", out string? authorizationCode)) + return null; + + if (!flow.Payload.TryGet("code_verifier", out string? codeVerifier)) + return null; + + return new HubCredentials + { + AuthorizationCode = authorizationCode, + CodeVerifier = codeVerifier, + ClientProfile = flow.ClientProfile, + }; + } +} diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Hub/HubFlowReader.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Hub/HubFlowReader.cs new file mode 100644 index 00000000..646e780a --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Hub/HubFlowReader.cs @@ -0,0 +1,39 @@ +using CodeBeam.UltimateAuth.Core.Abstractions; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Server.Stores; + +namespace CodeBeam.UltimateAuth.Server.Infrastructure; + +internal sealed class HubFlowReader : IHubFlowReader +{ + private readonly IAuthStore _store; + private readonly IClock _clock; + + public HubFlowReader(IAuthStore store, IClock clock) + { + _store = store; + _clock = clock; + } + + public async Task GetStateAsync(HubSessionId hubSessionId, CancellationToken ct = default) + { + var artifact = await _store.GetAsync(new AuthArtifactKey(hubSessionId.Value), ct); + + if (artifact is not HubFlowArtifact flow) + return null; + + var now = _clock.UtcNow; + + return new HubFlowState + { + Exists = true, + HubSessionId = flow.HubSessionId, + FlowType = flow.FlowType, + ClientProfile = flow.ClientProfile, + ReturnUrl = flow.ReturnUrl, + IsExpired = flow.IsExpired(now), + IsCompleted = flow.IsCompleted, + IsActive = !flow.IsExpired(now) && !flow.IsCompleted + }; + } +} diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/HubCapabilities.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/HubCapabilities.cs index bfbc2ba8..e46d2bda 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/HubCapabilities.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/HubCapabilities.cs @@ -1,9 +1,8 @@ using CodeBeam.UltimateAuth.Core.Abstractions; -namespace CodeBeam.UltimateAuth.Server.Infrastructure +namespace CodeBeam.UltimateAuth.Server.Infrastructure; + +internal sealed class HubCapabilities : IHubCapabilities { - internal sealed class HubCapabilities : IHubCapabilities - { - public bool SupportsPkce => true; - } + public bool SupportsPkce => true; } diff --git a/src/CodeBeam.UltimateAuth.Server/Issuers/UAuthSessionIssuer.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Issuers/UAuthSessionIssuer.cs similarity index 99% rename from src/CodeBeam.UltimateAuth.Server/Issuers/UAuthSessionIssuer.cs rename to src/CodeBeam.UltimateAuth.Server/Infrastructure/Issuers/UAuthSessionIssuer.cs index acda7056..709aea0c 100644 --- a/src/CodeBeam.UltimateAuth.Server/Issuers/UAuthSessionIssuer.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Issuers/UAuthSessionIssuer.cs @@ -7,7 +7,7 @@ using Microsoft.Extensions.Options; using System.Security; -namespace CodeBeam.UltimateAuth.Server.Issuers; +namespace CodeBeam.UltimateAuth.Server.Infrastructure; public sealed class UAuthSessionIssuer : ISessionIssuer { diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Issuers/UAuthTokenIssuer.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Issuers/UAuthTokenIssuer.cs new file mode 100644 index 00000000..07f6e3e4 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Issuers/UAuthTokenIssuer.cs @@ -0,0 +1,143 @@ +using CodeBeam.UltimateAuth.Core; +using CodeBeam.UltimateAuth.Core.Abstractions; +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.Options; +using CodeBeam.UltimateAuth.Server.Abstactions; +using CodeBeam.UltimateAuth.Server.Auth; + +namespace CodeBeam.UltimateAuth.Server.Infrastructure; + +/// +/// Default UltimateAuth token issuer. +/// Opinionated implementation of ITokenIssuer. +/// Mode-aware (PureOpaque, Hybrid, SemiHybrid, PureJwt). +/// +public sealed class UAuthTokenIssuer : ITokenIssuer +{ + private readonly IOpaqueTokenGenerator _opaqueGenerator; + private readonly IJwtTokenGenerator _jwtGenerator; + private readonly ITokenHasher _tokenHasher; + private readonly IRefreshTokenStore _refreshTokenStore; + private readonly IUserIdConverterResolver _converterResolver; + private readonly IClock _clock; + + public UAuthTokenIssuer(IOpaqueTokenGenerator opaqueGenerator, IJwtTokenGenerator jwtGenerator, ITokenHasher tokenHasher, IRefreshTokenStore refreshTokenStore,IUserIdConverterResolver converterResolver, IClock clock) + { + _opaqueGenerator = opaqueGenerator; + _jwtGenerator = jwtGenerator; + _tokenHasher = tokenHasher; + _refreshTokenStore = refreshTokenStore; + _converterResolver = converterResolver; + _clock = clock; + } + + public Task IssueAccessTokenAsync(AuthFlowContext flow, TokenIssuanceContext context, CancellationToken ct = default) + { + var tokens = flow.OriginalOptions.Tokens; + var now = _clock.UtcNow; + var expires = now.Add(tokens.AccessTokenLifetime); + + return flow.EffectiveMode switch + { + // TODO: Discuss, Hybrid token may be JWT. + UAuthMode.PureOpaque or UAuthMode.Hybrid => + Task.FromResult(IssueOpaqueAccessToken(expires, flow?.Session?.SessionId.ToString())), + + UAuthMode.SemiHybrid or + UAuthMode.PureJwt => + Task.FromResult(IssueJwtAccessToken(context, tokens, expires)), + + _ => throw new InvalidOperationException( + $"Unsupported auth mode: {flow.EffectiveMode}") + }; + } + + public async Task IssueRefreshTokenAsync(AuthFlowContext flow, TokenIssuanceContext context, RefreshTokenPersistence persistence, CancellationToken ct = default) + { + if (flow.EffectiveMode == UAuthMode.PureOpaque) + return null; + + var expires = _clock.UtcNow.Add(flow.OriginalOptions.Tokens.RefreshTokenLifetime); + + var raw = _opaqueGenerator.Generate(); + var hash = _tokenHasher.Hash(raw); + + var stored = new StoredRefreshToken + { + Tenant = flow.Tenant, + TokenHash = hash, + UserKey = context.UserKey, + // TODO: Check here again + SessionId = (AuthSessionId)context.SessionId, + ChainId = context.ChainId, + IssuedAt = _clock.UtcNow, + ExpiresAt = expires + }; + + if (persistence == RefreshTokenPersistence.Persist) + { + await _refreshTokenStore.StoreAsync(flow.Tenant, stored, ct); + } + + return new RefreshToken + { + Token = raw, + TokenHash = hash, + ExpiresAt = expires + }; + } + + private AccessToken IssueOpaqueAccessToken(DateTimeOffset expires, string? sessionId) + { + string token = _opaqueGenerator.Generate(); + + return new AccessToken + { + Token = token, + Type = TokenType.Opaque, + ExpiresAt = expires, + SessionId = sessionId + }; + } + + private AccessToken IssueJwtAccessToken(TokenIssuanceContext context, UAuthTokenOptions tokens, DateTimeOffset expires) + { + var claims = new Dictionary + { + ["sub"] = context.UserKey, + ["tenant"] = context.Tenant + }; + + foreach (var kv in context.Claims) + claims[kv.Key] = kv.Value; + + if (context.SessionId != null) + claims["sid"] = context.SessionId!; + + if (tokens.AddJwtIdClaim) + claims["jti"] = _opaqueGenerator.Generate(16); + + var descriptor = new UAuthJwtTokenDescriptor + { + Subject = context.UserKey, + Issuer = tokens.Issuer, + Audience = tokens.Audience, + IssuedAt = _clock.UtcNow, + ExpiresAt = expires, + Tenant = context.Tenant, + Claims = claims, + KeyId = tokens.KeyId + }; + + var jwt = _jwtGenerator.CreateToken(descriptor); + + return new AccessToken + { + Token = jwt, + Type = TokenType.Jwt, + ExpiresAt = expires, + SessionId = context.SessionId.ToString() + }; + } +} diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/JwtTokenGenerator.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/JwtTokenGenerator.cs new file mode 100644 index 00000000..a9ab6d46 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/JwtTokenGenerator.cs @@ -0,0 +1,66 @@ +using CodeBeam.UltimateAuth.Core.Abstractions; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Server.Abstractions; +using Microsoft.IdentityModel.JsonWebTokens; +using Microsoft.IdentityModel.Tokens; + +namespace CodeBeam.UltimateAuth.Server.Infrastructure; + +public sealed class JwtTokenGenerator : IJwtTokenGenerator +{ + private readonly IJwtSigningKeyProvider _keyProvider; + private readonly JsonWebTokenHandler _handler = new(); + + public JwtTokenGenerator(IJwtSigningKeyProvider keyProvider) + { + _keyProvider = keyProvider; + } + + public string CreateToken(UAuthJwtTokenDescriptor descriptor) + { + var signingKey = _keyProvider.Resolve(descriptor.KeyId); + + var tokenDescriptor = new SecurityTokenDescriptor + { + Issuer = descriptor.Issuer, + Audience = descriptor.Audience, + Subject = null, + NotBefore = descriptor.IssuedAt.UtcDateTime, + IssuedAt = descriptor.IssuedAt.UtcDateTime, + Expires = descriptor.ExpiresAt.UtcDateTime, + + Claims = BuildClaims(descriptor), + + SigningCredentials = new SigningCredentials( + signingKey.Key, + signingKey.Algorithm) + }; + + tokenDescriptor.AdditionalHeaderClaims = new Dictionary + { + ["kid"] = signingKey.KeyId + }; + + return _handler.CreateToken(tokenDescriptor); + } + + private static IDictionary BuildClaims(UAuthJwtTokenDescriptor descriptor) + { + var claims = new Dictionary + { + ["sub"] = descriptor.Subject + }; + + claims["tenant"] = descriptor.Tenant; + + if (descriptor.Claims is not null) + { + foreach (var kv in descriptor.Claims) + { + claims[kv.Key] = kv.Value; + } + } + + return claims; + } +} diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/OpaqueTokenGenerator.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/OpaqueTokenGenerator.cs new file mode 100644 index 00000000..5de0cf3d --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/OpaqueTokenGenerator.cs @@ -0,0 +1,8 @@ +using CodeBeam.UltimateAuth.Core.Abstractions; + +namespace CodeBeam.UltimateAuth.Server.Infrastructure; + +internal sealed class OpaqueTokenGenerator : IOpaqueTokenGenerator +{ + public string Generate(int bytes) => Convert.ToBase64String(System.Security.Cryptography.RandomNumberGenerator.GetBytes(bytes)); +} diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/CreateLoginSessionCommand.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/CreateLoginSessionCommand.cs index 6868542d..cd44daf7 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/CreateLoginSessionCommand.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/CreateLoginSessionCommand.cs @@ -1,13 +1,12 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Contracts; -namespace CodeBeam.UltimateAuth.Server.Infrastructure +namespace CodeBeam.UltimateAuth.Server.Infrastructure; + +internal sealed record CreateLoginSessionCommand(AuthenticatedSessionContext LoginContext) : ISessionCommand { - internal sealed record CreateLoginSessionCommand(AuthenticatedSessionContext LoginContext) : ISessionCommand + public Task ExecuteAsync(AuthContext _, ISessionIssuer issuer, CancellationToken ct) { - public Task ExecuteAsync(AuthContext _, ISessionIssuer issuer, CancellationToken ct) - { - return issuer.IssueLoginSessionAsync(LoginContext, ct); - } + return issuer.IssueLoginSessionAsync(LoginContext, ct); } } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/DefaultAccessAuthority.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/DefaultAccessAuthority.cs deleted file mode 100644 index 3f4f539e..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/DefaultAccessAuthority.cs +++ /dev/null @@ -1,60 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Abstractions; -using CodeBeam.UltimateAuth.Core.Contracts; - -namespace CodeBeam.UltimateAuth.Server.Infrastructure -{ - public sealed class DefaultAccessAuthority : IAccessAuthority - { - private readonly IEnumerable _invariants; - private readonly IEnumerable _globalPolicies; - - public DefaultAccessAuthority(IEnumerable invariants, IEnumerable globalPolicies) - { - _invariants = invariants ?? Array.Empty(); - _globalPolicies = globalPolicies ?? Array.Empty(); - } - - public AccessDecision Decide(AccessContext context, IEnumerable runtimePolicies) - { - foreach (var invariant in _invariants) - { - var result = invariant.Decide(context); - if (!result.IsAllowed) - return result; - } - - foreach (var policy in _globalPolicies) - { - if (!policy.AppliesTo(context)) - continue; - - var result = policy.Decide(context); - - if (!result.IsAllowed) - return result; - - // Allow here means "no objection", NOT permission - } - - bool requiresReauth = false; - - foreach (var policy in runtimePolicies) - { - if (!policy.AppliesTo(context)) - continue; - - var result = policy.Decide(context); - - if (!result.IsAllowed) - return result; - - if (result.RequiresReauthentication) - requiresReauth = true; - } - - return requiresReauth - ? AccessDecision.ReauthenticationRequired() - : AccessDecision.Allow(); - } - } -} diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/IAccessCommand.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/IAccessCommand.cs index a0dfdd31..9a2c61c5 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/IAccessCommand.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/IAccessCommand.cs @@ -1,14 +1,12 @@ -namespace CodeBeam.UltimateAuth.Server.Infrastructure -{ - public interface IAccessCommand - { - Task ExecuteAsync(CancellationToken ct = default); - } +namespace CodeBeam.UltimateAuth.Server.Infrastructure; - // For get commands - public interface IAccessCommand - { - Task ExecuteAsync(CancellationToken ct = default); - } +public interface IAccessCommand +{ + Task ExecuteAsync(CancellationToken ct = default); +} +// For get commands +public interface IAccessCommand +{ + Task ExecuteAsync(CancellationToken ct = default); } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/IAccessOrchestrator.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/IAccessOrchestrator.cs index 5f8e8d9e..9ad05368 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/IAccessOrchestrator.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/IAccessOrchestrator.cs @@ -1,10 +1,9 @@ using CodeBeam.UltimateAuth.Core.Contracts; -namespace CodeBeam.UltimateAuth.Server.Infrastructure +namespace CodeBeam.UltimateAuth.Server.Infrastructure; + +public interface IAccessOrchestrator { - public interface IAccessOrchestrator - { - Task ExecuteAsync(AccessContext context, IAccessCommand command, CancellationToken ct = default); - Task ExecuteAsync(AccessContext context, IAccessCommand command, CancellationToken ct = default); - } + Task ExecuteAsync(AccessContext context, IAccessCommand command, CancellationToken ct = default); + Task ExecuteAsync(AccessContext context, IAccessCommand command, CancellationToken ct = default); } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/ISessionCommand.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/ISessionCommand.cs index 0040e5a8..e60da79a 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/ISessionCommand.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/ISessionCommand.cs @@ -1,10 +1,9 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Contracts; -namespace CodeBeam.UltimateAuth.Server.Infrastructure +namespace CodeBeam.UltimateAuth.Server.Infrastructure; + +public interface ISessionCommand { - public interface ISessionCommand - { - Task ExecuteAsync(AuthContext context, ISessionIssuer issuer, CancellationToken ct); - } + Task ExecuteAsync(AuthContext context, ISessionIssuer issuer, CancellationToken ct); } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/ISessionOrchestrator.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/ISessionOrchestrator.cs index 668a9d28..c1e75e5e 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/ISessionOrchestrator.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/ISessionOrchestrator.cs @@ -1,8 +1,8 @@ using CodeBeam.UltimateAuth.Core.Contracts; -namespace CodeBeam.UltimateAuth.Server.Infrastructure + +namespace CodeBeam.UltimateAuth.Server.Infrastructure; + +internal interface ISessionOrchestrator { - internal interface ISessionOrchestrator - { - Task ExecuteAsync(AuthContext authContext, ISessionCommand command, CancellationToken ct = default); - } + Task ExecuteAsync(AuthContext authContext, ISessionCommand command, CancellationToken ct = default); } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeAllChainsCommand.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeAllChainsCommand.cs index 0052c717..51e814cd 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeAllChainsCommand.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeAllChainsCommand.cs @@ -2,23 +2,22 @@ using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Server.Infrastructure +namespace CodeBeam.UltimateAuth.Server.Infrastructure; + +public sealed class RevokeAllChainsCommand : ISessionCommand { - public sealed class RevokeAllChainsCommand : ISessionCommand - { - public UserKey UserKey { get; } - public SessionChainId? ExceptChainId { get; } + public UserKey UserKey { get; } + public SessionChainId? ExceptChainId { get; } - public RevokeAllChainsCommand(UserKey userKey, SessionChainId? exceptChainId) - { - UserKey = userKey; - ExceptChainId = exceptChainId; - } + public RevokeAllChainsCommand(UserKey userKey, SessionChainId? exceptChainId) + { + UserKey = userKey; + ExceptChainId = exceptChainId; + } - public async Task ExecuteAsync(AuthContext context, ISessionIssuer issuer, CancellationToken ct) - { - await issuer.RevokeAllChainsAsync(context.Tenant, UserKey, ExceptChainId, context.At, ct); - return Unit.Value; - } + public async Task ExecuteAsync(AuthContext context, ISessionIssuer issuer, CancellationToken ct) + { + await issuer.RevokeAllChainsAsync(context.Tenant, UserKey, ExceptChainId, context.At, ct); + return Unit.Value; } } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeAllSessionsCommand.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeAllSessionsCommand.cs index fbe86749..4fc9fcb5 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeAllSessionsCommand.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeAllSessionsCommand.cs @@ -2,23 +2,22 @@ using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Server.Infrastructure -{ - public sealed class RevokeAllUserSessionsCommand : ISessionCommand - { - public UserKey UserKey { get; } +namespace CodeBeam.UltimateAuth.Server.Infrastructure; - public RevokeAllUserSessionsCommand(UserKey userKey) - { - UserKey = userKey; - } +public sealed class RevokeAllUserSessionsCommand : ISessionCommand +{ + public UserKey UserKey { get; } - // TODO: This method should call its own logic. Not revoke root. - public async Task ExecuteAsync(AuthContext context, ISessionIssuer issuer, CancellationToken ct) - { - await issuer.RevokeRootAsync(context.Tenant, UserKey, context.At, ct); - return Unit.Value; - } + public RevokeAllUserSessionsCommand(UserKey userKey) + { + UserKey = userKey; + } + // TODO: This method should call its own logic. Not revoke root. + public async Task ExecuteAsync(AuthContext context, ISessionIssuer issuer, CancellationToken ct) + { + await issuer.RevokeRootAsync(context.Tenant, UserKey, context.At, ct); + return Unit.Value; } + } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeChainCommand.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeChainCommand.cs index 7eb4087d..2432db8a 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeChainCommand.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeChainCommand.cs @@ -2,21 +2,20 @@ using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Server.Infrastructure.Orchestrator +namespace CodeBeam.UltimateAuth.Server.Infrastructure; + +public sealed class RevokeChainCommand : ISessionCommand { - public sealed class RevokeChainCommand : ISessionCommand - { - public SessionChainId ChainId { get; } + public SessionChainId ChainId { get; } - public RevokeChainCommand(SessionChainId chainId) - { - ChainId = chainId; - } + public RevokeChainCommand(SessionChainId chainId) + { + ChainId = chainId; + } - public async Task ExecuteAsync(AuthContext context, ISessionIssuer issuer, CancellationToken ct) - { - await issuer.RevokeChainAsync(context.Tenant, ChainId, context.At, ct); - return Unit.Value; - } + public async Task ExecuteAsync(AuthContext context, ISessionIssuer issuer, CancellationToken ct) + { + await issuer.RevokeChainAsync(context.Tenant, ChainId, context.At, ct); + return Unit.Value; } } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeRootCommand.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeRootCommand.cs index 22b36441..ab3b2ce1 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeRootCommand.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeRootCommand.cs @@ -2,21 +2,20 @@ using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Server.Infrastructure.Orchestrator +namespace CodeBeam.UltimateAuth.Server.Infrastructure; + +public sealed class RevokeRootCommand : ISessionCommand { - public sealed class RevokeRootCommand : ISessionCommand - { - public UserKey UserKey { get; } + public UserKey UserKey { get; } - public RevokeRootCommand(UserKey userKey) - { - UserKey = userKey; - } + public RevokeRootCommand(UserKey userKey) + { + UserKey = userKey; + } - public async Task ExecuteAsync(AuthContext context, ISessionIssuer issuer, CancellationToken ct) - { - await issuer.RevokeRootAsync(context.Tenant, UserKey, context.At, ct); - return Unit.Value; - } + public async Task ExecuteAsync(AuthContext context, ISessionIssuer issuer, CancellationToken ct) + { + await issuer.RevokeRootAsync(context.Tenant, UserKey, context.At, ct); + return Unit.Value; } } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RotateSessionCommand.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RotateSessionCommand.cs index 1e52d029..70fc768f 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RotateSessionCommand.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RotateSessionCommand.cs @@ -1,13 +1,12 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Contracts; -namespace CodeBeam.UltimateAuth.Server.Infrastructure +namespace CodeBeam.UltimateAuth.Server.Infrastructure; + +internal sealed record RotateSessionCommand(SessionRotationContext RotationContext) : ISessionCommand { - internal sealed record RotateSessionCommand(SessionRotationContext RotationContext) : ISessionCommand + public Task ExecuteAsync(AuthContext _, ISessionIssuer issuer, CancellationToken ct) { - public Task ExecuteAsync(AuthContext _, ISessionIssuer issuer, CancellationToken ct) - { - return issuer.RotateSessionAsync(RotationContext, ct); - } + return issuer.RotateSessionAsync(RotationContext, ct); } } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/UAuthAccessAuthority.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/UAuthAccessAuthority.cs new file mode 100644 index 00000000..01d91819 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/UAuthAccessAuthority.cs @@ -0,0 +1,59 @@ +using CodeBeam.UltimateAuth.Core.Abstractions; +using CodeBeam.UltimateAuth.Core.Contracts; + +namespace CodeBeam.UltimateAuth.Server.Infrastructure; + +public sealed class UAuthAccessAuthority : IAccessAuthority +{ + private readonly IEnumerable _invariants; + private readonly IEnumerable _globalPolicies; + + public UAuthAccessAuthority(IEnumerable invariants, IEnumerable globalPolicies) + { + _invariants = invariants ?? Array.Empty(); + _globalPolicies = globalPolicies ?? Array.Empty(); + } + + public AccessDecision Decide(AccessContext context, IEnumerable runtimePolicies) + { + foreach (var invariant in _invariants) + { + var result = invariant.Decide(context); + if (!result.IsAllowed) + return result; + } + + foreach (var policy in _globalPolicies) + { + if (!policy.AppliesTo(context)) + continue; + + var result = policy.Decide(context); + + if (!result.IsAllowed) + return result; + + // Allow here means "no objection", NOT permission + } + + bool requiresReauth = false; + + foreach (var policy in runtimePolicies) + { + if (!policy.AppliesTo(context)) + continue; + + var result = policy.Decide(context); + + if (!result.IsAllowed) + return result; + + if (result.RequiresReauthentication) + requiresReauth = true; + } + + return requiresReauth + ? AccessDecision.ReauthenticationRequired() + : AccessDecision.Allow(); + } +} diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/UAuthAccessOrchestrator.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/UAuthAccessOrchestrator.cs index 305c09b8..094fa5f2 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/UAuthAccessOrchestrator.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/UAuthAccessOrchestrator.cs @@ -3,49 +3,48 @@ using CodeBeam.UltimateAuth.Core.Errors; using CodeBeam.UltimateAuth.Policies.Abstractions; -namespace CodeBeam.UltimateAuth.Server.Infrastructure +namespace CodeBeam.UltimateAuth.Server.Infrastructure; + +public sealed class UAuthAccessOrchestrator : IAccessOrchestrator { - public sealed class UAuthAccessOrchestrator : IAccessOrchestrator - { - private readonly IAccessAuthority _authority; - private readonly IAccessPolicyProvider _policyProvider; + private readonly IAccessAuthority _authority; + private readonly IAccessPolicyProvider _policyProvider; - public UAuthAccessOrchestrator(IAccessAuthority authority, IAccessPolicyProvider policyProvider) - { - _authority = authority; - _policyProvider = policyProvider; - } + public UAuthAccessOrchestrator(IAccessAuthority authority, IAccessPolicyProvider policyProvider) + { + _authority = authority; + _policyProvider = policyProvider; + } - public async Task ExecuteAsync(AccessContext context, IAccessCommand command, CancellationToken ct = default) - { - ct.ThrowIfCancellationRequested(); + public async Task ExecuteAsync(AccessContext context, IAccessCommand command, CancellationToken ct = default) + { + ct.ThrowIfCancellationRequested(); - var policies = _policyProvider.GetPolicies(context); - var decision = _authority.Decide(context, policies); + var policies = _policyProvider.GetPolicies(context); + var decision = _authority.Decide(context, policies); - if (!decision.IsAllowed) - throw new UAuthAuthorizationException(decision.DenyReason); + if (!decision.IsAllowed) + throw new UAuthAuthorizationException(decision.DenyReason); - if (decision.RequiresReauthentication) - throw new InvalidOperationException("Requires reauthentication."); + if (decision.RequiresReauthentication) + throw new InvalidOperationException("Requires reauthentication."); - await command.ExecuteAsync(ct); - } + await command.ExecuteAsync(ct); + } - public async Task ExecuteAsync(AccessContext context, IAccessCommand command, CancellationToken ct = default) - { - ct.ThrowIfCancellationRequested(); + public async Task ExecuteAsync(AccessContext context, IAccessCommand command, CancellationToken ct = default) + { + ct.ThrowIfCancellationRequested(); - var policies = _policyProvider.GetPolicies(context); - var decision = _authority.Decide(context, policies); + var policies = _policyProvider.GetPolicies(context); + var decision = _authority.Decide(context, policies); - if (!decision.IsAllowed) - throw new UAuthAuthorizationException(decision.DenyReason); + if (!decision.IsAllowed) + throw new UAuthAuthorizationException(decision.DenyReason); - if (decision.RequiresReauthentication) - throw new InvalidOperationException("Requires reauthentication."); + if (decision.RequiresReauthentication) + throw new InvalidOperationException("Requires reauthentication."); - return await command.ExecuteAsync(ct); - } + return await command.ExecuteAsync(ct); } } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/UAuthSessionOrchestrator.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/UAuthSessionOrchestrator.cs index 2a9c93c9..bc55c658 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/UAuthSessionOrchestrator.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/UAuthSessionOrchestrator.cs @@ -2,43 +2,42 @@ using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Errors; -namespace CodeBeam.UltimateAuth.Server.Infrastructure -{ - public sealed class UAuthSessionOrchestrator : ISessionOrchestrator - { - private readonly IAuthAuthority _authority; - private readonly ISessionIssuer _issuer; - private bool _executed; +namespace CodeBeam.UltimateAuth.Server.Infrastructure; - public UAuthSessionOrchestrator(IAuthAuthority authority, ISessionIssuer issuer) - { - _authority = authority; - _issuer = issuer; - } +public sealed class UAuthSessionOrchestrator : ISessionOrchestrator +{ + private readonly IAuthAuthority _authority; + private readonly ISessionIssuer _issuer; + private bool _executed; - public async Task ExecuteAsync(AuthContext authContext, ISessionCommand command, CancellationToken ct = default) - { - if (_executed) - throw new InvalidOperationException("Session orchestrator can only be executed once per operation."); + public UAuthSessionOrchestrator(IAuthAuthority authority, ISessionIssuer issuer) + { + _authority = authority; + _issuer = issuer; + } - _executed = true; + public async Task ExecuteAsync(AuthContext authContext, ISessionCommand command, CancellationToken ct = default) + { + if (_executed) + throw new InvalidOperationException("Session orchestrator can only be executed once per operation."); - var decision = _authority.Decide(authContext); + _executed = true; - switch (decision.Decision) - { - case AuthorizationDecision.Deny: - throw new UAuthAuthorizationException(decision.Reason); + var decision = _authority.Decide(authContext); - case AuthorizationDecision.Challenge: - throw new UAuthChallengeRequiredException(decision.Reason); + switch (decision.Decision) + { + case AuthorizationDecision.Deny: + throw new UAuthAuthorizationException(decision.Reason); - case AuthorizationDecision.Allow: - break; - } + case AuthorizationDecision.Challenge: + throw new UAuthChallengeRequiredException(decision.Reason); - return await command.ExecuteAsync(authContext, _issuer, ct); + case AuthorizationDecision.Allow: + break; } + return await command.ExecuteAsync(authContext, _issuer, ct); } + } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Pkce/PkceChallengeMethod.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Pkce/PkceChallengeMethod.cs deleted file mode 100644 index 39de36f0..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Pkce/PkceChallengeMethod.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace CodeBeam.UltimateAuth.Server.Infrastructure; - -public enum PkceChallengeMethod -{ - S256 -} diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/DefaultRefreshResponsePolicy.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/DefaultRefreshResponsePolicy.cs deleted file mode 100644 index ef3ce7ae..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/DefaultRefreshResponsePolicy.cs +++ /dev/null @@ -1,45 +0,0 @@ -using CodeBeam.UltimateAuth.Core; -using CodeBeam.UltimateAuth.Core.Contracts; -using CodeBeam.UltimateAuth.Core.Domain; -using CodeBeam.UltimateAuth.Core.Options; -using CodeBeam.UltimateAuth.Server.Auth; - -namespace CodeBeam.UltimateAuth.Server.Infrastructure -{ - internal class DefaultRefreshResponsePolicy : IRefreshResponsePolicy - { - public CredentialKind SelectPrimary(AuthFlowContext flow, RefreshFlowRequest request, RefreshFlowResult result) - { - if (flow.EffectiveMode == UAuthMode.PureOpaque) - return CredentialKind.Session; - - if (flow.EffectiveMode == UAuthMode.PureJwt) - return CredentialKind.AccessToken; - - if (!string.IsNullOrWhiteSpace(request.RefreshToken) && request.SessionId == null) - { - return CredentialKind.AccessToken; - } - - if (request.SessionId != null) - { - return CredentialKind.Session; - } - - if (flow.ClientProfile == UAuthClientProfile.Api) - return CredentialKind.AccessToken; - - return CredentialKind.Session; - } - - - public bool WriteRefreshToken(AuthFlowContext flow) - { - if (flow.EffectiveMode != UAuthMode.PureOpaque) - return true; - - return false; - } - - } -} diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/DefaultRefreshResponseWriter.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/DefaultRefreshResponseWriter.cs deleted file mode 100644 index 837a3430..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/DefaultRefreshResponseWriter.cs +++ /dev/null @@ -1,32 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Domain; -using CodeBeam.UltimateAuth.Server.Options; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Options; - -namespace CodeBeam.UltimateAuth.Server.Infrastructure -{ - internal sealed class DefaultRefreshResponseWriter : IRefreshResponseWriter - { - private readonly UAuthDiagnosticsOptions _diagnostics; - - public DefaultRefreshResponseWriter(IOptions options) - { - _diagnostics = options.Value.Diagnostics; - } - - public void Write(HttpContext context, RefreshOutcome outcome) - { - if (!_diagnostics.EnableRefreshHeaders) - return; - - context.Response.Headers["X-UAuth-Refresh"] = outcome switch - { - RefreshOutcome.NoOp => "no-op", - RefreshOutcome.Touched => "touched", - RefreshOutcome.ReauthRequired => "reauth-required", - _ => "unknown" - }; - } - - } -} diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/DefaultRefreshTokenResolver.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/DefaultRefreshTokenResolver.cs deleted file mode 100644 index f6fc373d..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/DefaultRefreshTokenResolver.cs +++ /dev/null @@ -1,41 +0,0 @@ -using CodeBeam.UltimateAuth.Server.Abstractions; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Primitives; - -namespace CodeBeam.UltimateAuth.Server.Infrastructure -{ - internal sealed class DefaultRefreshTokenResolver : IRefreshTokenResolver - { - private const string DefaultCookieName = "uar"; - private const string BearerPrefix = "Bearer "; - private const string RefreshHeaderName = "X-Refresh-Token"; - - public string? Resolve(HttpContext context) - { - if (context.Request.Cookies.TryGetValue(DefaultCookieName, out var cookieToken) && - !string.IsNullOrWhiteSpace(cookieToken)) - { - return cookieToken; - } - - if (context.Request.Headers.TryGetValue("Authorization", out StringValues authHeader)) - { - var value = authHeader.ToString(); - if (value.StartsWith(BearerPrefix, StringComparison.OrdinalIgnoreCase)) - { - var token = value.Substring(BearerPrefix.Length).Trim(); - if (!string.IsNullOrWhiteSpace(token)) - return token; - } - } - - if (context.Request.Headers.TryGetValue(RefreshHeaderName, out var headerToken) && - !string.IsNullOrWhiteSpace(headerToken)) - { - return headerToken.ToString(); - } - - return null; - } - } -} diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/IRefreshResponsePolicy.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/IRefreshResponsePolicy.cs deleted file mode 100644 index 8c05919f..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/IRefreshResponsePolicy.cs +++ /dev/null @@ -1,12 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Contracts; -using CodeBeam.UltimateAuth.Core.Domain; -using CodeBeam.UltimateAuth.Server.Auth; - -namespace CodeBeam.UltimateAuth.Server.Infrastructure -{ - public interface IRefreshResponsePolicy - { - CredentialKind SelectPrimary(AuthFlowContext flow, RefreshFlowRequest request, RefreshFlowResult result); - bool WriteRefreshToken(AuthFlowContext flow); - } -} diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/IRefreshResponseWriter.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/IRefreshResponseWriter.cs deleted file mode 100644 index 551fc0c1..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/IRefreshResponseWriter.cs +++ /dev/null @@ -1,10 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Domain; -using Microsoft.AspNetCore.Http; - -namespace CodeBeam.UltimateAuth.Server.Infrastructure -{ - public interface IRefreshResponseWriter - { - void Write(HttpContext context, RefreshOutcome outcome); - } -} diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/IRefreshService.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/IRefreshService.cs deleted file mode 100644 index b4f85e26..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/IRefreshService.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace CodeBeam.UltimateAuth.Server.Infrastructure -{ - /// - /// Base contract for refresh-related services. - /// Refresh services renew authentication artifacts according to AuthMode. - /// - public interface IRefreshService - { - } -} diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/ISessionTouchService.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/ISessionTouchService.cs deleted file mode 100644 index 375fc310..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/ISessionTouchService.cs +++ /dev/null @@ -1,13 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Contracts; - -namespace CodeBeam.UltimateAuth.Server.Infrastructure -{ - /// - /// Refreshes session lifecycle artifacts. - /// Used by PureOpaque and Hybrid modes. - /// - public interface ISessionTouchService : IRefreshService - { - Task RefreshAsync(SessionValidationResult validation, SessionTouchPolicy policy, SessionTouchMode sessionTouchMode, DateTimeOffset now, CancellationToken ct = default); - } -} diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/RefreshDecision.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/RefreshDecision.cs deleted file mode 100644 index 1a9ccec6..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/RefreshDecision.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace CodeBeam.UltimateAuth.Server.Infrastructure -{ - /// - /// Determines which authentication artifacts can be refreshed - /// for the current AuthMode. - /// This is a server-side decision and must be enforced centrally. - /// - public enum RefreshDecision - { - /// - /// Refresh endpoint is disabled for this mode. - /// - NotSupported = 0, - - /// - /// Only session lifetime is extended. - /// No access / refresh token issued. - /// (PureOpaque) - /// - SessionTouch = 1, - - /// - /// Refresh token is rotated and - /// a new access token is issued. - /// Session MAY also be touched depending on policy. - /// (Hybrid, SemiHybrid, PureJwt) - /// - TokenRotation = 2 - } -} diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/RefreshDecisionResolver.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/RefreshDecisionResolver.cs deleted file mode 100644 index b5e0b373..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/RefreshDecisionResolver.cs +++ /dev/null @@ -1,24 +0,0 @@ -using CodeBeam.UltimateAuth.Core; -namespace CodeBeam.UltimateAuth.Server.Infrastructure -{ - /// - /// Resolves refresh behavior based on AuthMode. - /// This class is the single source of truth for refresh capability. - /// - public static class RefreshDecisionResolver - { - public static RefreshDecision Resolve(UAuthMode mode) - { - return mode switch - { - UAuthMode.PureOpaque => RefreshDecision.SessionTouch, - - UAuthMode.Hybrid - or UAuthMode.SemiHybrid - or UAuthMode.PureJwt => RefreshDecision.TokenRotation, - - _ => RefreshDecision.NotSupported - }; - } - } -} diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/RefreshEvaluationResult.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/RefreshEvaluationResult.cs deleted file mode 100644 index f22aa1b8..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/RefreshEvaluationResult.cs +++ /dev/null @@ -1,6 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Domain; - -namespace CodeBeam.UltimateAuth.Server.Infrastructure -{ - internal sealed record RefreshEvaluationResult(RefreshOutcome Outcome); -} diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/RefreshStrategyResolver.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/RefreshStrategyResolver.cs deleted file mode 100644 index 33581937..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/RefreshStrategyResolver.cs +++ /dev/null @@ -1,21 +0,0 @@ -using CodeBeam.UltimateAuth.Core; -using CodeBeam.UltimateAuth.Core.Contracts; -using System.Security; - -namespace CodeBeam.UltimateAuth.Server.Infrastructure -{ - public class RefreshStrategyResolver - { - public static RefreshStrategy Resolve(UAuthMode mode) - { - return mode switch - { - UAuthMode.PureOpaque => RefreshStrategy.SessionOnly, - UAuthMode.PureJwt => RefreshStrategy.TokenOnly, - UAuthMode.SemiHybrid => RefreshStrategy.TokenWithSessionCheck, - UAuthMode.Hybrid => RefreshStrategy.SessionAndToken, - _ => throw new SecurityException("Unsupported refresh mode") - }; - } - } -} diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/SessionTouchPolicy.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/SessionTouchPolicy.cs deleted file mode 100644 index 9f4fa8b5..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/SessionTouchPolicy.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace CodeBeam.UltimateAuth.Server.Infrastructure -{ - public sealed class SessionTouchPolicy - { - public TimeSpan? TouchInterval { get; init; } - } -} diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Session/DefaultSessionContextAccessor.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Session/DefaultSessionContextAccessor.cs deleted file mode 100644 index b5b02b64..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Session/DefaultSessionContextAccessor.cs +++ /dev/null @@ -1,30 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Contracts; -using Microsoft.AspNetCore.Http; - -namespace CodeBeam.UltimateAuth.Server.Infrastructure.Session -{ - public sealed class DefaultSessionContextAccessor : ISessionContextAccessor - { - private readonly IHttpContextAccessor _httpContextAccessor; - - public DefaultSessionContextAccessor(IHttpContextAccessor httpContextAccessor) - { - _httpContextAccessor = httpContextAccessor; - } - - public SessionContext? Current - { - get - { - var ctx = _httpContextAccessor.HttpContext; - if (ctx is null) - return null; - - if (ctx.Items.TryGetValue(SessionContextItemKeys.SessionContext, out var value)) - return value as SessionContext; - - return null; - } - } - } -} diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Session/ISessionContextAccessor.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Session/ISessionContextAccessor.cs index ac69fc9f..252a38ef 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Session/ISessionContextAccessor.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Session/ISessionContextAccessor.cs @@ -1,12 +1,11 @@ using CodeBeam.UltimateAuth.Core.Contracts; -namespace CodeBeam.UltimateAuth.Server.Infrastructure +namespace CodeBeam.UltimateAuth.Server.Infrastructure; + +/// +/// The single point of truth for accessing the current session context +/// +public interface ISessionContextAccessor { - /// - /// The single point of truth for accessing the current session context - /// - public interface ISessionContextAccessor - { - SessionContext? Current { get; } - } + SessionContext? Current { get; } } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Session/SessionContextAccessor.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Session/SessionContextAccessor.cs new file mode 100644 index 00000000..434bcb52 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Session/SessionContextAccessor.cs @@ -0,0 +1,29 @@ +using CodeBeam.UltimateAuth.Core.Contracts; +using Microsoft.AspNetCore.Http; + +namespace CodeBeam.UltimateAuth.Server.Infrastructure; + +public sealed class SessionContextAccessor : ISessionContextAccessor +{ + private readonly IHttpContextAccessor _httpContextAccessor; + + public SessionContextAccessor(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + } + + public SessionContext? Current + { + get + { + var ctx = _httpContextAccessor.HttpContext; + if (ctx is null) + return null; + + if (ctx.Items.TryGetValue(SessionContextItemKeys.SessionContext, out var value)) + return value as SessionContext; + + return null; + } + } +} diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Session/SessionContextItemKeys.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Session/SessionContextItemKeys.cs index b028fdc1..38e1379e 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Session/SessionContextItemKeys.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Session/SessionContextItemKeys.cs @@ -1,7 +1,6 @@ -namespace CodeBeam.UltimateAuth.Server.Infrastructure +namespace CodeBeam.UltimateAuth.Server.Infrastructure; + +internal static class SessionContextItemKeys { - internal static class SessionContextItemKeys - { - public const string SessionContext = "__UAuth.SessionContext"; - } + public const string SessionContext = "__UAuth.SessionContext"; } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Session/SessionValidationMapper.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Session/SessionValidationMapper.cs index 5f852417..eb649433 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Session/SessionValidationMapper.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Session/SessionValidationMapper.cs @@ -1,36 +1,34 @@ using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Server.Infrastructure +namespace CodeBeam.UltimateAuth.Server.Infrastructure; + +internal static class SessionValidationMapper { - internal static class SessionValidationMapper + public static SessionSecurityContext? ToSecurityContext(SessionValidationResult result) { - public static SessionSecurityContext? ToSecurityContext(SessionValidationResult result) + if (!result.IsValid) { - if (!result.IsValid) - { - if (result?.SessionId is null) - return null; - - return new SessionSecurityContext - { - SessionId = result.SessionId.Value, - State = result.State, - ChainId = result.ChainId, - UserKey = result.UserKey, - BoundDeviceId = result.BoundDeviceId - }; - } + if (result?.SessionId is null) + return null; return new SessionSecurityContext { - SessionId = result.SessionId!.Value, - State = SessionState.Active, + SessionId = result.SessionId.Value, + State = result.State, ChainId = result.ChainId, UserKey = result.UserKey, BoundDeviceId = result.BoundDeviceId }; } - } + return new SessionSecurityContext + { + SessionId = result.SessionId!.Value, + State = SessionState.Active, + ChainId = result.ChainId, + UserKey = result.UserKey, + BoundDeviceId = result.BoundDeviceId + }; + } } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/SessionId/BearerSessionIdResolver.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/SessionId/BearerSessionIdResolver.cs index 6165f93f..c57b633f 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/SessionId/BearerSessionIdResolver.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/SessionId/BearerSessionIdResolver.cs @@ -1,30 +1,28 @@ using CodeBeam.UltimateAuth.Core.Domain; using Microsoft.AspNetCore.Http; -namespace CodeBeam.UltimateAuth.Server.Infrastructure +namespace CodeBeam.UltimateAuth.Server.Infrastructure; + +public sealed class BearerSessionIdResolver : IInnerSessionIdResolver { - public sealed class BearerSessionIdResolver : IInnerSessionIdResolver - { - public string Key => "bearer"; + public string Key => "bearer"; - public AuthSessionId? Resolve(HttpContext context) - { - var header = context.Request.Headers.Authorization.ToString(); - if (string.IsNullOrWhiteSpace(header)) - return null; + public AuthSessionId? Resolve(HttpContext context) + { + var header = context.Request.Headers.Authorization.ToString(); + if (string.IsNullOrWhiteSpace(header)) + return null; - if (!header.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) - return null; + if (!header.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) + return null; - var raw = header["Bearer ".Length..].Trim(); - if (string.IsNullOrWhiteSpace(raw)) - return null; + var raw = header["Bearer ".Length..].Trim(); + if (string.IsNullOrWhiteSpace(raw)) + return null; - if (!AuthSessionId.TryCreate(raw, out var sessionId)) - return null; + if (!AuthSessionId.TryCreate(raw, out var sessionId)) + return null; - return sessionId; - } + return sessionId; } - } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/SessionId/CompositeSessionIdResolver.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/SessionId/CompositeSessionIdResolver.cs index 978a55f1..fcec4400 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/SessionId/CompositeSessionIdResolver.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/SessionId/CompositeSessionIdResolver.cs @@ -1,28 +1,27 @@ using CodeBeam.UltimateAuth.Core.Domain; using Microsoft.AspNetCore.Http; -namespace CodeBeam.UltimateAuth.Server.Infrastructure +namespace CodeBeam.UltimateAuth.Server.Infrastructure; + +// TODO: Add policy and effective auth resolver. +public sealed class CompositeSessionIdResolver : ISessionIdResolver { - // TODO: Add policy and effective auth resolver. - public sealed class CompositeSessionIdResolver : ISessionIdResolver + private readonly IReadOnlyList _resolvers; + + public CompositeSessionIdResolver(IEnumerable resolvers) { - private readonly IReadOnlyList _resolvers; + _resolvers = resolvers.ToList(); + } - public CompositeSessionIdResolver(IEnumerable resolvers) + public AuthSessionId? Resolve(HttpContext context) + { + foreach (var resolver in _resolvers) { - _resolvers = resolvers.ToList(); + var id = resolver.Resolve(context); + if (id is not null) + return id; } - public AuthSessionId? Resolve(HttpContext context) - { - foreach (var resolver in _resolvers) - { - var id = resolver.Resolve(context); - if (id is not null) - return id; - } - - return null; - } + return null; } } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/SessionId/CookieSessionIdResolver.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/SessionId/CookieSessionIdResolver.cs index 3c905b1c..c1bab7c3 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/SessionId/CookieSessionIdResolver.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/SessionId/CookieSessionIdResolver.cs @@ -3,33 +3,32 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; -namespace CodeBeam.UltimateAuth.Server.Infrastructure +namespace CodeBeam.UltimateAuth.Server.Infrastructure; + +public sealed class CookieSessionIdResolver : IInnerSessionIdResolver { - public sealed class CookieSessionIdResolver : IInnerSessionIdResolver - { - public string Key => "cookie"; + public string Key => "cookie"; - private readonly UAuthServerOptions _options; + private readonly UAuthServerOptions _options; - public CookieSessionIdResolver(IOptions options) - { - _options = options.Value; - } + public CookieSessionIdResolver(IOptions options) + { + _options = options.Value; + } - public AuthSessionId? Resolve(HttpContext context) - { - var cookieName = _options.Cookie.Session.Name; + public AuthSessionId? Resolve(HttpContext context) + { + var cookieName = _options.Cookie.Session.Name; - if (!context.Request.Cookies.TryGetValue(cookieName, out var raw)) - return null; + if (!context.Request.Cookies.TryGetValue(cookieName, out var raw)) + return null; - if (string.IsNullOrWhiteSpace(raw)) - return null; + if (string.IsNullOrWhiteSpace(raw)) + return null; - if (!AuthSessionId.TryCreate(raw, out var sessionId)) - return null; + if (!AuthSessionId.TryCreate(raw, out var sessionId)) + return null; - return sessionId; - } + return sessionId; } } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/SessionId/HeaderSessionIdResolver.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/SessionId/HeaderSessionIdResolver.cs index fe40bc17..7c1bca18 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/SessionId/HeaderSessionIdResolver.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/SessionId/HeaderSessionIdResolver.cs @@ -3,32 +3,31 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; -namespace CodeBeam.UltimateAuth.Server.Infrastructure +namespace CodeBeam.UltimateAuth.Server.Infrastructure; + +public sealed class HeaderSessionIdResolver : IInnerSessionIdResolver { - public sealed class HeaderSessionIdResolver : IInnerSessionIdResolver - { - public string Key => "header"; - private readonly UAuthServerOptions _options; + public string Key => "header"; + private readonly UAuthServerOptions _options; - public HeaderSessionIdResolver(IOptions options) - { - _options = options.Value; - } + public HeaderSessionIdResolver(IOptions options) + { + _options = options.Value; + } - public AuthSessionId? Resolve(HttpContext context) - { - if (!context.Request.Headers.TryGetValue(_options.SessionResolution.HeaderName, out var values)) - return null; + public AuthSessionId? Resolve(HttpContext context) + { + if (!context.Request.Headers.TryGetValue(_options.SessionResolution.HeaderName, out var values)) + return null; - var raw = values.FirstOrDefault(); - - if (string.IsNullOrWhiteSpace(raw)) - return null; + var raw = values.FirstOrDefault(); + + if (string.IsNullOrWhiteSpace(raw)) + return null; - if (!AuthSessionId.TryCreate(raw, out var sessionId)) - return null; + if (!AuthSessionId.TryCreate(raw, out var sessionId)) + return null; - return sessionId; - } + return sessionId; } } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/SessionId/IInnerSessionIdResolver.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/SessionId/IInnerSessionIdResolver.cs index 861c72a0..5ef9dc1f 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/SessionId/IInnerSessionIdResolver.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/SessionId/IInnerSessionIdResolver.cs @@ -1,11 +1,10 @@ using CodeBeam.UltimateAuth.Core.Domain; using Microsoft.AspNetCore.Http; -namespace CodeBeam.UltimateAuth.Server.Infrastructure +namespace CodeBeam.UltimateAuth.Server.Infrastructure; + +public interface IInnerSessionIdResolver { - public interface IInnerSessionIdResolver - { - string Key { get; } - AuthSessionId? Resolve(HttpContext context); - } + string Key { get; } + AuthSessionId? Resolve(HttpContext context); } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/SessionId/ISessionIdResolver.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/SessionId/ISessionIdResolver.cs index 46c063b6..f46bbff3 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/SessionId/ISessionIdResolver.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/SessionId/ISessionIdResolver.cs @@ -1,10 +1,9 @@ using CodeBeam.UltimateAuth.Core.Domain; using Microsoft.AspNetCore.Http; -namespace CodeBeam.UltimateAuth.Server.Infrastructure +namespace CodeBeam.UltimateAuth.Server.Infrastructure; + +public interface ISessionIdResolver { - public interface ISessionIdResolver - { - AuthSessionId? Resolve(HttpContext context); - } + AuthSessionId? Resolve(HttpContext context); } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/SessionId/QuerySessionIdResolver.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/SessionId/QuerySessionIdResolver.cs index bb3cc9e1..2082c612 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/SessionId/QuerySessionIdResolver.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/SessionId/QuerySessionIdResolver.cs @@ -3,33 +3,32 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; -namespace CodeBeam.UltimateAuth.Server.Infrastructure -{ - public sealed class QuerySessionIdResolver : IInnerSessionIdResolver - { - public string Key => "query"; - private readonly UAuthServerOptions _options; +namespace CodeBeam.UltimateAuth.Server.Infrastructure; - public QuerySessionIdResolver(IOptions options) - { - _options = options.Value; - } +public sealed class QuerySessionIdResolver : IInnerSessionIdResolver +{ + public string Key => "query"; + private readonly UAuthServerOptions _options; - public AuthSessionId? Resolve(HttpContext context) - { - if (!context.Request.Query.TryGetValue(_options.SessionResolution.QueryParameterName, out var values)) - return null; + public QuerySessionIdResolver(IOptions options) + { + _options = options.Value; + } - var raw = values.FirstOrDefault(); + public AuthSessionId? Resolve(HttpContext context) + { + if (!context.Request.Query.TryGetValue(_options.SessionResolution.QueryParameterName, out var values)) + return null; - if (string.IsNullOrWhiteSpace(raw)) - return null; + var raw = values.FirstOrDefault(); - if (!AuthSessionId.TryCreate(raw, out var sessionId)) - return null; + if (string.IsNullOrWhiteSpace(raw)) + return null; - return sessionId; - } + if (!AuthSessionId.TryCreate(raw, out var sessionId)) + return null; + return sessionId; } + } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/SystemClock.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/SystemClock.cs index c6e526a6..3794ea24 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/SystemClock.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/SystemClock.cs @@ -1,9 +1,8 @@ using CodeBeam.UltimateAuth.Core.Abstractions; -namespace CodeBeam.UltimateAuth.Server.Infrastructure +namespace CodeBeam.UltimateAuth.Server.Infrastructure; + +public sealed class SystemClock : IClock { - public sealed class SystemClock : IClock - { - public DateTimeOffset UtcNow => DateTimeOffset.UtcNow; - } + public DateTimeOffset UtcNow => DateTimeOffset.UtcNow; } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/User/HttpContextCurrentUser.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/User/HttpContextCurrentUser.cs index dcd8c17a..cf780253 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/User/HttpContextCurrentUser.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/User/HttpContextCurrentUser.cs @@ -4,22 +4,20 @@ using CodeBeam.UltimateAuth.Server.Middlewares; using Microsoft.AspNetCore.Http; -namespace CodeBeam.UltimateAuth.Server.Infrastructure -{ - internal sealed class HttpContextCurrentUser : ICurrentUser - { - private readonly IHttpContextAccessor _http; +namespace CodeBeam.UltimateAuth.Server.Infrastructure; - public HttpContextCurrentUser(IHttpContextAccessor http) - { - _http = http; - } +internal sealed class HttpContextCurrentUser : ICurrentUser +{ + private readonly IHttpContextAccessor _http; - public bool IsAuthenticated => Snapshot?.IsAuthenticated == true; + public HttpContextCurrentUser(IHttpContextAccessor http) + { + _http = http; + } - public UserKey UserKey => Snapshot?.UserId ?? throw new InvalidOperationException("Current user is not authenticated."); + public bool IsAuthenticated => Snapshot?.IsAuthenticated == true; - private AuthUserSnapshot? Snapshot => _http.HttpContext?.Items[UserMiddleware.UserContextKey] as AuthUserSnapshot; - } + public UserKey UserKey => Snapshot?.UserId ?? throw new InvalidOperationException("Current user is not authenticated."); + private AuthUserSnapshot? Snapshot => _http.HttpContext?.Items[UserMiddleware.UserContextKey] as AuthUserSnapshot; } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/User/IUserAccessor.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/User/IUserAccessor.cs index 4df17305..0f14a8ab 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/User/IUserAccessor.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/User/IUserAccessor.cs @@ -1,14 +1,13 @@ using Microsoft.AspNetCore.Http; -namespace CodeBeam.UltimateAuth.Server.Infrastructure +namespace CodeBeam.UltimateAuth.Server.Infrastructure; + +public interface IUserAccessor { - public interface IUserAccessor - { - Task ResolveAsync(HttpContext context); - } + Task ResolveAsync(HttpContext context); +} - public interface IUserAccessor - { - Task ResolveAsync(HttpContext context); - } +public interface IUserAccessor +{ + Task ResolveAsync(HttpContext context); } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/User/UAuthUserId.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/User/UAuthUserId.cs index d42d35b3..caf8cb45 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/User/UAuthUserId.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/User/UAuthUserId.cs @@ -1,12 +1,11 @@ -namespace CodeBeam.UltimateAuth.Server.Infrastructure +namespace CodeBeam.UltimateAuth.Server.Infrastructure; + +public readonly record struct UAuthUserId(Guid Value) { - public readonly record struct UAuthUserId(Guid Value) - { - public override string ToString() => Value.ToString("N"); + public override string ToString() => Value.ToString("N"); - public static UAuthUserId New() => new(Guid.NewGuid()); + public static UAuthUserId New() => new(Guid.NewGuid()); - public static implicit operator Guid(UAuthUserId id) => id.Value; - public static implicit operator UAuthUserId(Guid value) => new(value); - } + public static implicit operator Guid(UAuthUserId id) => id.Value; + public static implicit operator UAuthUserId(Guid value) => new(value); } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/User/UserAccessorBridge.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/User/UserAccessorBridge.cs index 3fabb84e..2bacd92e 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/User/UserAccessorBridge.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/User/UserAccessorBridge.cs @@ -2,22 +2,21 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; -namespace CodeBeam.UltimateAuth.Server.Infrastructure -{ - internal sealed class UserAccessorBridge : IUserAccessor - { - private readonly IServiceProvider _services; +namespace CodeBeam.UltimateAuth.Server.Infrastructure; - public UserAccessorBridge(IServiceProvider services) - { - _services = services; - } +internal sealed class UserAccessorBridge : IUserAccessor +{ + private readonly IServiceProvider _services; - public async Task ResolveAsync(HttpContext context) - { - var accessor = _services.GetRequiredService>(); - await accessor.ResolveAsync(context); - } + public UserAccessorBridge(IServiceProvider services) + { + _services = services; + } + public async Task ResolveAsync(HttpContext context) + { + var accessor = _services.GetRequiredService>(); + await accessor.ResolveAsync(context); } + } diff --git a/src/CodeBeam.UltimateAuth.Server/Issuers/UAuthTokenIssuer.cs b/src/CodeBeam.UltimateAuth.Server/Issuers/UAuthTokenIssuer.cs deleted file mode 100644 index 7b00f8f1..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Issuers/UAuthTokenIssuer.cs +++ /dev/null @@ -1,145 +0,0 @@ -using CodeBeam.UltimateAuth.Core; -using CodeBeam.UltimateAuth.Core.Abstractions; -using CodeBeam.UltimateAuth.Core.Contracts; -using CodeBeam.UltimateAuth.Core.Domain; -using CodeBeam.UltimateAuth.Core.Options; -using CodeBeam.UltimateAuth.Server.Abstactions; -using CodeBeam.UltimateAuth.Server.Auth; - -namespace CodeBeam.UltimateAuth.Server.Issuers -{ - /// - /// Default UltimateAuth token issuer. - /// Opinionated implementation of ITokenIssuer. - /// Mode-aware (PureOpaque, Hybrid, SemiHybrid, PureJwt). - /// - public sealed class UAuthTokenIssuer : ITokenIssuer - { - private readonly IOpaqueTokenGenerator _opaqueGenerator; - private readonly IJwtTokenGenerator _jwtGenerator; - private readonly ITokenHasher _tokenHasher; - private readonly IRefreshTokenStore _refreshTokenStore; - private readonly IUserIdConverterResolver _converterResolver; - private readonly IClock _clock; - - public UAuthTokenIssuer(IOpaqueTokenGenerator opaqueGenerator, IJwtTokenGenerator jwtGenerator, ITokenHasher tokenHasher, IRefreshTokenStore refreshTokenStore,IUserIdConverterResolver converterResolver, IClock clock) - { - _opaqueGenerator = opaqueGenerator; - _jwtGenerator = jwtGenerator; - _tokenHasher = tokenHasher; - _refreshTokenStore = refreshTokenStore; - _converterResolver = converterResolver; - _clock = clock; - } - - public Task IssueAccessTokenAsync(AuthFlowContext flow, TokenIssuanceContext context, CancellationToken ct = default) - { - var tokens = flow.OriginalOptions.Tokens; - var now = _clock.UtcNow; - var expires = now.Add(tokens.AccessTokenLifetime); - - return flow.EffectiveMode switch - { - // TODO: Discuss, Hybrid token may be JWT. - UAuthMode.PureOpaque or UAuthMode.Hybrid => - Task.FromResult(IssueOpaqueAccessToken(expires, flow?.Session?.SessionId.ToString())), - - UAuthMode.SemiHybrid or - UAuthMode.PureJwt => - Task.FromResult(IssueJwtAccessToken(context, tokens, expires)), - - _ => throw new InvalidOperationException( - $"Unsupported auth mode: {flow.EffectiveMode}") - }; - } - - public async Task IssueRefreshTokenAsync(AuthFlowContext flow, TokenIssuanceContext context, RefreshTokenPersistence persistence, CancellationToken ct = default) - { - if (flow.EffectiveMode == UAuthMode.PureOpaque) - return null; - - var expires = _clock.UtcNow.Add(flow.OriginalOptions.Tokens.RefreshTokenLifetime); - - var raw = _opaqueGenerator.Generate(); - var hash = _tokenHasher.Hash(raw); - - var stored = new StoredRefreshToken - { - Tenant = flow.Tenant, - TokenHash = hash, - UserKey = context.UserKey, - // TODO: Check here again - SessionId = (AuthSessionId)context.SessionId, - ChainId = context.ChainId, - IssuedAt = _clock.UtcNow, - ExpiresAt = expires - }; - - if (persistence == RefreshTokenPersistence.Persist) - { - await _refreshTokenStore.StoreAsync(flow.Tenant, stored, ct); - } - - return new RefreshToken - { - Token = raw, - TokenHash = hash, - ExpiresAt = expires - }; - } - - private AccessToken IssueOpaqueAccessToken(DateTimeOffset expires, string? sessionId) - { - string token = _opaqueGenerator.Generate(); - - return new AccessToken - { - Token = token, - Type = TokenType.Opaque, - ExpiresAt = expires, - SessionId = sessionId - }; - } - - private AccessToken IssueJwtAccessToken(TokenIssuanceContext context, UAuthTokenOptions tokens, DateTimeOffset expires) - { - var claims = new Dictionary - { - ["sub"] = context.UserKey, - ["tenant"] = context.Tenant - }; - - foreach (var kv in context.Claims) - claims[kv.Key] = kv.Value; - - if (context.SessionId != null) - claims["sid"] = context.SessionId!; - - if (tokens.AddJwtIdClaim) - claims["jti"] = _opaqueGenerator.Generate(16); - - var descriptor = new UAuthJwtTokenDescriptor - { - Subject = context.UserKey, - Issuer = tokens.Issuer, - Audience = tokens.Audience, - IssuedAt = _clock.UtcNow, - ExpiresAt = expires, - Tenant = context.Tenant, - Claims = claims, - KeyId = tokens.KeyId - }; - - var jwt = _jwtGenerator.CreateToken(descriptor); - - return new AccessToken - { - Token = jwt, - Type = TokenType.Jwt, - ExpiresAt = expires, - SessionId = context.SessionId.ToString() - }; - } - - } -} diff --git a/src/CodeBeam.UltimateAuth.Server/Login/DefaultLoginAuthority.cs b/src/CodeBeam.UltimateAuth.Server/Login/DefaultLoginAuthority.cs deleted file mode 100644 index 7836ef64..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Login/DefaultLoginAuthority.cs +++ /dev/null @@ -1,34 +0,0 @@ -namespace CodeBeam.UltimateAuth.Server.Login -{ - /// - /// Default implementation of the login authority. - /// Applies basic security checks for login attempts. - /// - public sealed class DefaultLoginAuthority : ILoginAuthority - { - public LoginDecision Decide(LoginDecisionContext context) - { - if (!context.CredentialsValid) - { - return LoginDecision.Deny("Invalid credentials."); - } - - if (!context.UserExists || context.UserKey is null) - { - return LoginDecision.Deny("Invalid credentials."); - } - - var state = context.SecurityState; - if (state is not null) - { - if (state.IsLocked) - return LoginDecision.Deny("user_is_locked"); - - if (state.RequiresReauthentication) - return LoginDecision.Challenge("reauth_required"); - } - - return LoginDecision.Allow(); - } - } -} diff --git a/src/CodeBeam.UltimateAuth.Server/Login/DefaultLoginOrchestrator.cs b/src/CodeBeam.UltimateAuth.Server/Login/DefaultLoginOrchestrator.cs deleted file mode 100644 index b324c5d9..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Login/DefaultLoginOrchestrator.cs +++ /dev/null @@ -1,169 +0,0 @@ -using CodeBeam.UltimateAuth.Core; -using CodeBeam.UltimateAuth.Core.Abstractions; -using CodeBeam.UltimateAuth.Core.Contracts; -using CodeBeam.UltimateAuth.Core.Domain; -using CodeBeam.UltimateAuth.Credentials; -using CodeBeam.UltimateAuth.Server.Abstactions; -using CodeBeam.UltimateAuth.Server.Auth; -using CodeBeam.UltimateAuth.Server.Extensions; -using CodeBeam.UltimateAuth.Server.Infrastructure; -using CodeBeam.UltimateAuth.Users; -using CodeBeam.UltimateAuth.Users.Abstractions; - -namespace CodeBeam.UltimateAuth.Server.Login.Orchestrators -{ - internal sealed class DefaultLoginOrchestrator : ILoginOrchestrator - { - private readonly ICredentialStore _credentialStore; // authentication - private readonly ICredentialValidator _credentialValidator; - private readonly IUserRuntimeStateProvider _users; // eligible - private readonly IUserSecurityStateProvider _userSecurityStateProvider; // runtime risk - private readonly ILoginAuthority _authority; - private readonly ISessionOrchestrator _sessionOrchestrator; - private readonly ITokenIssuer _tokens; - private readonly IUserClaimsProvider _claimsProvider; - private readonly IUserIdConverterResolver _userIdConverterResolver; - - public DefaultLoginOrchestrator( - ICredentialStore credentialStore, - ICredentialValidator credentialValidator, - IUserRuntimeStateProvider users, - IUserSecurityStateProvider userSecurityStateProvider, - ILoginAuthority authority, - ISessionOrchestrator sessionOrchestrator, - ITokenIssuer tokens, - IUserClaimsProvider claimsProvider, - IUserIdConverterResolver userIdConverterResolver) - { - _credentialStore = credentialStore; - _credentialValidator = credentialValidator; - _users = users; - _userSecurityStateProvider = userSecurityStateProvider; - _authority = authority; - _sessionOrchestrator = sessionOrchestrator; - _tokens = tokens; - _claimsProvider = claimsProvider; - _userIdConverterResolver = userIdConverterResolver; - } - - public async Task LoginAsync(AuthFlowContext flow, LoginRequest request, CancellationToken ct = default) - { - ct.ThrowIfCancellationRequested(); - - var now = request.At ?? DateTimeOffset.UtcNow; - - var credentials = await _credentialStore.FindByLoginAsync(request.Tenant, request.Identifier, ct); - var orderedCredentials = credentials - .OfType() - .Where(c => c.Security.IsUsable(now)) - .Cast>() - .ToList(); - - TUserId validatedUserId = default!; - bool credentialsValid = false; - - foreach (var credential in orderedCredentials) - { - var result = await _credentialValidator.ValidateAsync(credential, request.Secret, ct); - - if (result.IsValid) - { - validatedUserId = credential.UserId; - credentialsValid = true; - break; - } - } - - bool userExists = credentialsValid; - - IUserSecurityState? securityState = null; - UserKey? userKey = null; - - if (credentialsValid) - { - securityState = await _userSecurityStateProvider.GetAsync(request.Tenant, validatedUserId, ct); - var converter = _userIdConverterResolver.GetConverter(); - userKey = UserKey.FromString(converter.ToCanonicalString(validatedUserId)); - } - - var user = userKey is not null - ? await _users.GetAsync(request.Tenant, userKey.Value, ct) - : null; - - if (user is null || user.IsDeleted || !user.IsActive) - { - // Deliberately vague - return LoginResult.Failed(); - } - - var decisionContext = new LoginDecisionContext - { - Tenant = request.Tenant, - Identifier = request.Identifier, - CredentialsValid = credentialsValid, - UserExists = userExists, - UserKey = userKey, - SecurityState = securityState, - IsChained = request.ChainId is not null - }; - - var decision = _authority.Decide(decisionContext); - - if (decision.Kind == LoginDecisionKind.Deny) - return LoginResult.Failed(); - - if (decision.Kind == LoginDecisionKind.Challenge) - { - return LoginResult.Continue(new LoginContinuation - { - Type = LoginContinuationType.Mfa, - Hint = decision.Reason - }); - } - - if (userKey is not UserKey validUserKey) - { - return LoginResult.Failed(); - } - - var claims = await _claimsProvider.GetClaimsAsync(request.Tenant, validUserKey, ct); - - var sessionContext = new AuthenticatedSessionContext - { - Tenant = request.Tenant, - UserKey = validUserKey, - Now = now, - Device = request.Device, - Claims = claims, - ChainId = request.ChainId, - Metadata = SessionMetadata.Empty - }; - - var authContext = flow.ToAuthContext(now); - var issuedSession = await _sessionOrchestrator.ExecuteAsync(authContext, new CreateLoginSessionCommand(sessionContext), ct); - - AuthTokens? tokens = null; - - if (request.RequestTokens) - { - var tokenContext = new TokenIssuanceContext - { - Tenant = request.Tenant, - UserKey = validUserKey, - SessionId = issuedSession.Session.SessionId, - ChainId = request.ChainId, - Claims = claims.AsDictionary() - }; - - tokens = new AuthTokens - { - AccessToken = await _tokens.IssueAccessTokenAsync(flow, tokenContext, ct), - RefreshToken = await _tokens.IssueRefreshTokenAsync(flow, tokenContext, RefreshTokenPersistence.Persist, ct) - }; - } - - return LoginResult.Success(issuedSession.Session.SessionId, tokens); - - } - } -} diff --git a/src/CodeBeam.UltimateAuth.Server/Login/ILoginAuthority.cs b/src/CodeBeam.UltimateAuth.Server/Login/ILoginAuthority.cs deleted file mode 100644 index d05fac67..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Login/ILoginAuthority.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace CodeBeam.UltimateAuth.Server.Login -{ - /// - /// Represents the authority responsible for making login decisions. - /// This authority determines whether a login attempt is allowed, - /// denied, or requires additional verification (e.g. MFA). - /// - public interface ILoginAuthority - { - /// - /// Evaluates a login attempt based on the provided decision context. - /// - /// The login decision context. - /// The login decision. - LoginDecision Decide(LoginDecisionContext context); - } -} diff --git a/src/CodeBeam.UltimateAuth.Server/Login/ILoginOrchestrator.cs b/src/CodeBeam.UltimateAuth.Server/Login/ILoginOrchestrator.cs deleted file mode 100644 index d22d605b..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Login/ILoginOrchestrator.cs +++ /dev/null @@ -1,16 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Contracts; -using CodeBeam.UltimateAuth.Server.Auth; - -namespace CodeBeam.UltimateAuth.Server.Login -{ - /// - /// Orchestrates the login flow. - /// Responsible for executing the login process by coordinating - /// credential validation, user resolution, authority decision, - /// and session creation. - /// - public interface ILoginOrchestrator - { - Task LoginAsync(AuthFlowContext flow, LoginRequest request, CancellationToken ct = default); - } -} diff --git a/src/CodeBeam.UltimateAuth.Server/Login/LoginDecision.cs b/src/CodeBeam.UltimateAuth.Server/Login/LoginDecision.cs deleted file mode 100644 index 625d3113..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Login/LoginDecision.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace CodeBeam.UltimateAuth.Server.Login -{ - /// - /// Represents the outcome of a login decision. - /// - public sealed class LoginDecision - { - public LoginDecisionKind Kind { get; } - public string? Reason { get; } - - private LoginDecision(LoginDecisionKind kind, string? reason = null) - { - Kind = kind; - Reason = reason; - } - - public static LoginDecision Allow() - => new(LoginDecisionKind.Allow); - - public static LoginDecision Deny(string reason) - => new(LoginDecisionKind.Deny, reason); - - public static LoginDecision Challenge(string reason) - => new(LoginDecisionKind.Challenge, reason); - } -} diff --git a/src/CodeBeam.UltimateAuth.Server/Login/LoginDecisionContext.cs b/src/CodeBeam.UltimateAuth.Server/Login/LoginDecisionContext.cs deleted file mode 100644 index 03398305..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Login/LoginDecisionContext.cs +++ /dev/null @@ -1,51 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Domain; -using CodeBeam.UltimateAuth.Core.MultiTenancy; -using CodeBeam.UltimateAuth.Users; - -namespace CodeBeam.UltimateAuth.Server.Login -{ - /// - /// Represents all information required by the login authority - /// to make a login decision. - /// - public sealed class LoginDecisionContext - { - /// - /// Gets the tenant identifier. - /// - public TenantKey Tenant { get; init; } - - /// - /// Gets the login identifier (e.g. username or email). - /// - public required string Identifier { get; init; } - - /// - /// Indicates whether the provided credentials were successfully validated. - /// - public bool CredentialsValid { get; init; } - - /// - /// Gets the resolved user identifier if available. - /// - public UserKey? UserKey { get; init; } - - /// - /// Gets the user security state if the user could be resolved. - /// - public IUserSecurityState? SecurityState { get; init; } - - /// - /// Indicates whether the user exists. - /// This allows the authority to distinguish between - /// invalid credentials and non-existent users. - /// - public bool UserExists { get; init; } - - /// - /// Indicates whether this login attempt is part of a chained flow - /// (e.g. reauthentication, MFA completion). - /// - public bool IsChained { get; init; } - } -} diff --git a/src/CodeBeam.UltimateAuth.Server/Login/LoginDecisionKind.cs b/src/CodeBeam.UltimateAuth.Server/Login/LoginDecisionKind.cs deleted file mode 100644 index c1086d05..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Login/LoginDecisionKind.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace CodeBeam.UltimateAuth.Server.Login -{ - public enum LoginDecisionKind - { - Allow = 1, - Deny = 2, - Challenge = 3 - } -} diff --git a/src/CodeBeam.UltimateAuth.Server/Middlewares/SessionResolutionMiddleware.cs b/src/CodeBeam.UltimateAuth.Server/Middlewares/SessionResolutionMiddleware.cs index 2eabb27b..51d3eb3e 100644 --- a/src/CodeBeam.UltimateAuth.Server/Middlewares/SessionResolutionMiddleware.cs +++ b/src/CodeBeam.UltimateAuth.Server/Middlewares/SessionResolutionMiddleware.cs @@ -4,33 +4,30 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; -namespace CodeBeam.UltimateAuth.Server.Middlewares +namespace CodeBeam.UltimateAuth.Server.Middlewares; + +public sealed class SessionResolutionMiddleware { - public sealed class SessionResolutionMiddleware - { - private readonly RequestDelegate _next; + private readonly RequestDelegate _next; - public SessionResolutionMiddleware(RequestDelegate next) - { - _next = next; - } + public SessionResolutionMiddleware(RequestDelegate next) + { + _next = next; + } - public async Task InvokeAsync(HttpContext context) - { - var sessionIdResolver = context.RequestServices.GetRequiredService(); + public async Task InvokeAsync(HttpContext context) + { + var sessionIdResolver = context.RequestServices.GetRequiredService(); - var tenant = context.GetTenant(); - var sessionId = sessionIdResolver.Resolve(context); + var tenant = context.GetTenant(); + var sessionId = sessionIdResolver.Resolve(context); - var sessionContext = sessionId is null - ? SessionContext.Anonymous() - : SessionContext.FromSessionId( - sessionId.Value, - tenant); + var sessionContext = sessionId is null + ? SessionContext.Anonymous() + : SessionContext.FromSessionId(sessionId.Value, tenant); - context.Items[SessionContextItemKeys.SessionContext] = sessionContext; + context.Items[SessionContextItemKeys.SessionContext] = sessionContext; - await _next(context); - } + await _next(context); } } diff --git a/src/CodeBeam.UltimateAuth.Server/Middlewares/UserMiddleware.cs b/src/CodeBeam.UltimateAuth.Server/Middlewares/UserMiddleware.cs index 77a41e16..46e9b6c2 100644 --- a/src/CodeBeam.UltimateAuth.Server/Middlewares/UserMiddleware.cs +++ b/src/CodeBeam.UltimateAuth.Server/Middlewares/UserMiddleware.cs @@ -2,24 +2,23 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; -namespace CodeBeam.UltimateAuth.Server.Middlewares +namespace CodeBeam.UltimateAuth.Server.Middlewares; + +public sealed class UserMiddleware { - public sealed class UserMiddleware - { - private readonly RequestDelegate _next; + private readonly RequestDelegate _next; - public const string UserContextKey = "__UAuthUser"; + public const string UserContextKey = "__UAuthUser"; - public UserMiddleware(RequestDelegate next) - { - _next = next; - } + public UserMiddleware(RequestDelegate next) + { + _next = next; + } - public async Task InvokeAsync(HttpContext context) - { - var userAccessor = context.RequestServices.GetRequiredService(); - await userAccessor.ResolveAsync(context); - await _next(context); - } + public async Task InvokeAsync(HttpContext context) + { + var userAccessor = context.RequestServices.GetRequiredService(); + await userAccessor.ResolveAsync(context); + await _next(context); } } diff --git a/src/CodeBeam.UltimateAuth.Server/MultiTenancy/TenantResolutionContextFactory.cs b/src/CodeBeam.UltimateAuth.Server/MultiTenancy/TenantResolutionContextFactory.cs index ae7457a9..66341adf 100644 --- a/src/CodeBeam.UltimateAuth.Server/MultiTenancy/TenantResolutionContextFactory.cs +++ b/src/CodeBeam.UltimateAuth.Server/MultiTenancy/TenantResolutionContextFactory.cs @@ -1,31 +1,21 @@ using CodeBeam.UltimateAuth.Core.MultiTenancy; using Microsoft.AspNetCore.Http; -namespace CodeBeam.UltimateAuth.Server.MultiTenancy +namespace CodeBeam.UltimateAuth.Server.MultiTenancy; + +public static class TenantResolutionContextFactory { - public static class TenantResolutionContextFactory + public static TenantResolutionContext FromHttpContext(HttpContext ctx) { - public static TenantResolutionContext FromHttpContext(HttpContext ctx) - { - var headers = ctx.Request.Headers - .ToDictionary( - h => h.Key, - h => h.Value.ToString(), - StringComparer.OrdinalIgnoreCase); - - var query = ctx.Request.Query - .ToDictionary( - q => q.Key, - q => q.Value.ToString(), - StringComparer.OrdinalIgnoreCase); - - return TenantResolutionContext.Create( - headers: headers, - Query: query, - host: ctx.Request.Host.Host, - path: ctx.Request.Path.Value, - rawContext: ctx - ); - } + var headers = ctx.Request.Headers.ToDictionary(h => h.Key, h => h.Value.ToString(), StringComparer.OrdinalIgnoreCase); + var query = ctx.Request.Query.ToDictionary(q => q.Key, q => q.Value.ToString(), StringComparer.OrdinalIgnoreCase); + + return TenantResolutionContext.Create( + headers: headers, + Query: query, + host: ctx.Request.Host.Host, + path: ctx.Request.Path.Value, + rawContext: ctx + ); } } diff --git a/src/CodeBeam.UltimateAuth.Server/MultiTenancy/UAuthTenantContextFactory.cs b/src/CodeBeam.UltimateAuth.Server/MultiTenancy/UAuthTenantContextFactory.cs index 11c55e92..be3e7fc5 100644 --- a/src/CodeBeam.UltimateAuth.Server/MultiTenancy/UAuthTenantContextFactory.cs +++ b/src/CodeBeam.UltimateAuth.Server/MultiTenancy/UAuthTenantContextFactory.cs @@ -7,7 +7,6 @@ public static class UAuthTenantContextFactory { public static UAuthTenantContext Create(string? rawTenantId, UAuthMultiTenantOptions options) { - // 🔹 Single-tenant mode if (!options.Enabled) return UAuthTenantContext.SingleTenant(); diff --git a/src/CodeBeam.UltimateAuth.Server/Options/AuthResponseOptions.cs b/src/CodeBeam.UltimateAuth.Server/Options/AuthResponseOptions.cs index 41d5a0d1..0c9dda0e 100644 --- a/src/CodeBeam.UltimateAuth.Server/Options/AuthResponseOptions.cs +++ b/src/CodeBeam.UltimateAuth.Server/Options/AuthResponseOptions.cs @@ -1,22 +1,21 @@ -namespace CodeBeam.UltimateAuth.Server.Options +namespace CodeBeam.UltimateAuth.Server.Options; + +public sealed class AuthResponseOptions { - public sealed class AuthResponseOptions - { - public CredentialResponseOptions SessionIdDelivery { get; set; } = new(); - public CredentialResponseOptions AccessTokenDelivery { get; set; } = new(); - public CredentialResponseOptions RefreshTokenDelivery { get; set; } = new(); + public CredentialResponseOptions SessionIdDelivery { get; set; } = new(); + public CredentialResponseOptions AccessTokenDelivery { get; set; } = new(); + public CredentialResponseOptions RefreshTokenDelivery { get; set; } = new(); - public LoginRedirectOptions Login { get; set; } = new(); - public LogoutRedirectOptions Logout { get; set; } = new(); + public LoginRedirectOptions Login { get; set; } = new(); + public LogoutRedirectOptions Logout { get; set; } = new(); - internal AuthResponseOptions Clone() => new() - { - SessionIdDelivery = SessionIdDelivery.Clone(), - AccessTokenDelivery = AccessTokenDelivery.Clone(), - RefreshTokenDelivery = RefreshTokenDelivery.Clone(), - Login = Login.Clone(), - Logout = Logout.Clone() - }; + internal AuthResponseOptions Clone() => new() + { + SessionIdDelivery = SessionIdDelivery.Clone(), + AccessTokenDelivery = AccessTokenDelivery.Clone(), + RefreshTokenDelivery = RefreshTokenDelivery.Clone(), + Login = Login.Clone(), + Logout = Logout.Clone() + }; - } } diff --git a/src/CodeBeam.UltimateAuth.Server/Options/CredentialResponseOptions.cs b/src/CodeBeam.UltimateAuth.Server/Options/CredentialResponseOptions.cs index ba0ef111..9419dd2c 100644 --- a/src/CodeBeam.UltimateAuth.Server/Options/CredentialResponseOptions.cs +++ b/src/CodeBeam.UltimateAuth.Server/Options/CredentialResponseOptions.cs @@ -2,46 +2,45 @@ using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Core.Options; -namespace CodeBeam.UltimateAuth.Server.Options +namespace CodeBeam.UltimateAuth.Server.Options; + +public sealed class CredentialResponseOptions { - public sealed class CredentialResponseOptions + public CredentialKind Kind { get; init; } + public TokenResponseMode Mode { get; set; } = TokenResponseMode.None; + + /// + /// Header or body name + /// + public string? Name { get; set; } + + /// + /// Applies when Mode = Header + /// + public HeaderTokenFormat HeaderFormat { get; set; } = HeaderTokenFormat.Bearer; + public TokenFormat TokenFormat { get; set; } + + // Only for cookie + public UAuthCookieOptions? Cookie { get; init; } + + internal CredentialResponseOptions Clone() => new() + { + Mode = Mode, + Name = Name, + HeaderFormat = HeaderFormat, + TokenFormat = TokenFormat, + Cookie = Cookie?.Clone() + }; + + public CredentialResponseOptions WithCookie(UAuthCookieOptions cookie) + => new() { - public CredentialKind Kind { get; init; } - public TokenResponseMode Mode { get; set; } = TokenResponseMode.None; - - /// - /// Header or body name - /// - public string? Name { get; set; } - - /// - /// Applies when Mode = Header - /// - public HeaderTokenFormat HeaderFormat { get; set; } = HeaderTokenFormat.Bearer; - public TokenFormat TokenFormat { get; set; } - - // Only for cookie - public UAuthCookieOptions? Cookie { get; init; } - - internal CredentialResponseOptions Clone() => new() - { - Mode = Mode, - Name = Name, - HeaderFormat = HeaderFormat, - TokenFormat = TokenFormat, - Cookie = Cookie?.Clone() - }; - - public CredentialResponseOptions WithCookie(UAuthCookieOptions cookie) - => new() - { - Kind = Kind, - Mode = Mode, - Name = Name, - HeaderFormat = HeaderFormat, - TokenFormat = TokenFormat, - Cookie = cookie - }; - - } + Kind = Kind, + Mode = Mode, + Name = Name, + HeaderFormat = HeaderFormat, + TokenFormat = TokenFormat, + Cookie = cookie + }; + } diff --git a/src/CodeBeam.UltimateAuth.Server/Options/Defaults/ConfigureDefaults.cs b/src/CodeBeam.UltimateAuth.Server/Options/Defaults/ConfigureDefaults.cs index e96fbef0..8a8ae0aa 100644 --- a/src/CodeBeam.UltimateAuth.Server/Options/Defaults/ConfigureDefaults.cs +++ b/src/CodeBeam.UltimateAuth.Server/Options/Defaults/ConfigureDefaults.cs @@ -2,140 +2,139 @@ using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Options; -namespace CodeBeam.UltimateAuth.Server.Options +namespace CodeBeam.UltimateAuth.Server.Options; + +internal class ConfigureDefaults { - internal class ConfigureDefaults + internal static void ApplyModeDefaults(UAuthServerOptions o) { - internal static void ApplyModeDefaults(UAuthServerOptions o) + switch (o.Mode) { - switch (o.Mode) - { - case UAuthMode.PureOpaque: - ApplyPureOpaqueDefaults(o); - break; - - case UAuthMode.Hybrid: - ApplyHybridDefaults(o); - break; - - case UAuthMode.SemiHybrid: - ApplySemiHybridDefaults(o); - break; - - case UAuthMode.PureJwt: - ApplyPureJwtDefaults(o); - break; - - default: - throw new InvalidOperationException($"Unsupported UAuthMode: {o.Mode}"); - } - } + case UAuthMode.PureOpaque: + ApplyPureOpaqueDefaults(o); + break; - private static void ApplyPureOpaqueDefaults(UAuthServerOptions o) - { - var s = o.Session; - var t = o.Tokens; - var c = o.Cookie; - var r = o.AuthResponse; + case UAuthMode.Hybrid: + ApplyHybridDefaults(o); + break; - // Session behavior - s.SlidingExpiration = true; + case UAuthMode.SemiHybrid: + ApplySemiHybridDefaults(o); + break; - // Default: long-lived idle session (UX friendly) - s.IdleTimeout ??= TimeSpan.FromDays(7); + case UAuthMode.PureJwt: + ApplyPureJwtDefaults(o); + break; - s.TouchInterval ??= TimeSpan.FromDays(1); + default: + throw new InvalidOperationException($"Unsupported UAuthMode: {o.Mode}"); + } + } - // Hard re-auth boundary is an advanced security feature - // Do NOT enable by default - s.MaxLifetime ??= null; - s.DeviceMismatchBehavior = DeviceMismatchBehavior.Allow; + private static void ApplyPureOpaqueDefaults(UAuthServerOptions o) + { + var s = o.Session; + var t = o.Tokens; + var c = o.Cookie; + var r = o.AuthResponse; - // SessionId is the primary opaque token, carried via cookie - t.IssueJwt = false; + // Session behavior + s.SlidingExpiration = true; - // No separate opaque access token is issued outside the session cookie - t.IssueOpaque = false; + // Default: long-lived idle session (UX friendly) + s.IdleTimeout ??= TimeSpan.FromDays(7); - // Refresh token does not exist in PureOpaque - t.IssueRefresh = false; + s.TouchInterval ??= TimeSpan.FromDays(1); - c.Session.Lifetime.IdleBuffer = TimeSpan.FromDays(2); + // Hard re-auth boundary is an advanced security feature + // Do NOT enable by default + s.MaxLifetime ??= null; + s.DeviceMismatchBehavior = DeviceMismatchBehavior.Allow; - r.RefreshTokenDelivery = new CredentialResponseOptions - { - Mode = TokenResponseMode.None, - TokenFormat = TokenFormat.Opaque - }; - } + // SessionId is the primary opaque token, carried via cookie + t.IssueJwt = false; - private static void ApplyHybridDefaults(UAuthServerOptions o) - { - var s = o.Session; - var t = o.Tokens; - var c = o.Cookie; - var r = o.AuthResponse; - - s.SlidingExpiration = true; - s.TouchInterval = null; - - t.IssueJwt = true; - t.IssueOpaque = true; - t.AccessTokenLifetime = TimeSpan.FromMinutes(10); - t.RefreshTokenLifetime = TimeSpan.FromDays(7); - - c.Session.Lifetime.IdleBuffer = TimeSpan.FromMinutes(5); - c.RefreshToken.Lifetime.IdleBuffer = TimeSpan.FromMinutes(5); - - r.RefreshTokenDelivery = new CredentialResponseOptions - { - Mode = TokenResponseMode.Cookie, - TokenFormat = TokenFormat.Opaque - }; - } + // No separate opaque access token is issued outside the session cookie + t.IssueOpaque = false; + + // Refresh token does not exist in PureOpaque + t.IssueRefresh = false; - private static void ApplySemiHybridDefaults(UAuthServerOptions o) + c.Session.Lifetime.IdleBuffer = TimeSpan.FromDays(2); + + r.RefreshTokenDelivery = new CredentialResponseOptions { - var s = o.Session; - var t = o.Tokens; - var p = o.Pkce; - var c = o.Cookie; - - s.SlidingExpiration = false; - s.TouchInterval = null; - - t.IssueJwt = true; - t.IssueOpaque = true; - t.AccessTokenLifetime = TimeSpan.FromMinutes(10); - t.RefreshTokenLifetime = TimeSpan.FromDays(7); - t.AddJwtIdClaim = true; - - c.AccessToken.Lifetime.IdleBuffer = TimeSpan.FromMinutes(5); - c.RefreshToken.Lifetime.IdleBuffer = TimeSpan.FromMinutes(5); - } + Mode = TokenResponseMode.None, + TokenFormat = TokenFormat.Opaque + }; + } - private static void ApplyPureJwtDefaults(UAuthServerOptions o) + private static void ApplyHybridDefaults(UAuthServerOptions o) + { + var s = o.Session; + var t = o.Tokens; + var c = o.Cookie; + var r = o.AuthResponse; + + s.SlidingExpiration = true; + s.TouchInterval = null; + + t.IssueJwt = true; + t.IssueOpaque = true; + t.AccessTokenLifetime = TimeSpan.FromMinutes(10); + t.RefreshTokenLifetime = TimeSpan.FromDays(7); + + c.Session.Lifetime.IdleBuffer = TimeSpan.FromMinutes(5); + c.RefreshToken.Lifetime.IdleBuffer = TimeSpan.FromMinutes(5); + + r.RefreshTokenDelivery = new CredentialResponseOptions { - var s = o.Session; - var t = o.Tokens; - var p = o.Pkce; - var c = o.Cookie; + Mode = TokenResponseMode.Cookie, + TokenFormat = TokenFormat.Opaque + }; + } - s.TouchInterval = null; + private static void ApplySemiHybridDefaults(UAuthServerOptions o) + { + var s = o.Session; + var t = o.Tokens; + var p = o.Pkce; + var c = o.Cookie; + + s.SlidingExpiration = false; + s.TouchInterval = null; + + t.IssueJwt = true; + t.IssueOpaque = true; + t.AccessTokenLifetime = TimeSpan.FromMinutes(10); + t.RefreshTokenLifetime = TimeSpan.FromDays(7); + t.AddJwtIdClaim = true; + + c.AccessToken.Lifetime.IdleBuffer = TimeSpan.FromMinutes(5); + c.RefreshToken.Lifetime.IdleBuffer = TimeSpan.FromMinutes(5); + } + + private static void ApplyPureJwtDefaults(UAuthServerOptions o) + { + var s = o.Session; + var t = o.Tokens; + var p = o.Pkce; + var c = o.Cookie; - o.Session.SlidingExpiration = false; - o.Session.IdleTimeout = null; - o.Session.MaxLifetime = null; + s.TouchInterval = null; - t.IssueJwt = true; - t.IssueOpaque = false; - t.AccessTokenLifetime = TimeSpan.FromMinutes(10); - t.RefreshTokenLifetime = TimeSpan.FromDays(7); - t.AddJwtIdClaim = true; + o.Session.SlidingExpiration = false; + o.Session.IdleTimeout = null; + o.Session.MaxLifetime = null; - c.AccessToken.Lifetime.IdleBuffer = TimeSpan.FromMinutes(5); - c.RefreshToken.Lifetime.IdleBuffer = TimeSpan.FromMinutes(5); - } + t.IssueJwt = true; + t.IssueOpaque = false; + t.AccessTokenLifetime = TimeSpan.FromMinutes(10); + t.RefreshTokenLifetime = TimeSpan.FromDays(7); + t.AddJwtIdClaim = true; + c.AccessToken.Lifetime.IdleBuffer = TimeSpan.FromMinutes(5); + c.RefreshToken.Lifetime.IdleBuffer = TimeSpan.FromMinutes(5); } + } diff --git a/src/CodeBeam.UltimateAuth.Server/Options/IEffectiveServerOptionsProvider.cs b/src/CodeBeam.UltimateAuth.Server/Options/IEffectiveServerOptionsProvider.cs index 05a20b9e..a2b7f955 100644 --- a/src/CodeBeam.UltimateAuth.Server/Options/IEffectiveServerOptionsProvider.cs +++ b/src/CodeBeam.UltimateAuth.Server/Options/IEffectiveServerOptionsProvider.cs @@ -3,11 +3,10 @@ using CodeBeam.UltimateAuth.Server.Auth; using Microsoft.AspNetCore.Http; -namespace CodeBeam.UltimateAuth.Server.Options +namespace CodeBeam.UltimateAuth.Server.Options; + +public interface IEffectiveServerOptionsProvider { - public interface IEffectiveServerOptionsProvider - { - UAuthServerOptions GetOriginal(HttpContext context); - EffectiveUAuthServerOptions GetEffective(HttpContext context, AuthFlowType flowType, UAuthClientProfile clientProfile); - } + UAuthServerOptions GetOriginal(HttpContext context); + EffectiveUAuthServerOptions GetEffective(HttpContext context, AuthFlowType flowType, UAuthClientProfile clientProfile); } diff --git a/src/CodeBeam.UltimateAuth.Server/Options/LoginRedirectOptions.cs b/src/CodeBeam.UltimateAuth.Server/Options/LoginRedirectOptions.cs index e4968027..b542d2aa 100644 --- a/src/CodeBeam.UltimateAuth.Server/Options/LoginRedirectOptions.cs +++ b/src/CodeBeam.UltimateAuth.Server/Options/LoginRedirectOptions.cs @@ -1,28 +1,26 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Server.Options -{ - public sealed class LoginRedirectOptions - { - public bool RedirectEnabled { get; set; } = true; +namespace CodeBeam.UltimateAuth.Server.Options; - public string SuccessRedirect { get; init; } = "/"; - public string FailureRedirect { get; init; } = "/login"; +public sealed class LoginRedirectOptions +{ + public bool RedirectEnabled { get; set; } = true; - public string FailureQueryKey { get; init; } = "error"; - public string CodeQueryKey { get; set; } = "code"; + public string SuccessRedirect { get; init; } = "/"; + public string FailureRedirect { get; init; } = "/login"; - public Dictionary FailureCodes { get; set; } = new(); + public string FailureQueryKey { get; init; } = "error"; + public string CodeQueryKey { get; set; } = "code"; - internal LoginRedirectOptions Clone() => new() - { - RedirectEnabled = RedirectEnabled, - SuccessRedirect = SuccessRedirect, - FailureRedirect = FailureRedirect, - FailureQueryKey = FailureQueryKey, - CodeQueryKey = CodeQueryKey, - FailureCodes = new Dictionary(FailureCodes) - }; + public Dictionary FailureCodes { get; set; } = new(); - } + internal LoginRedirectOptions Clone() => new() + { + RedirectEnabled = RedirectEnabled, + SuccessRedirect = SuccessRedirect, + FailureRedirect = FailureRedirect, + FailureQueryKey = FailureQueryKey, + CodeQueryKey = CodeQueryKey, + FailureCodes = new Dictionary(FailureCodes) + }; } diff --git a/src/CodeBeam.UltimateAuth.Server/Options/LogoutRedirectOptions.cs b/src/CodeBeam.UltimateAuth.Server/Options/LogoutRedirectOptions.cs index 90ea5e2f..0126e299 100644 --- a/src/CodeBeam.UltimateAuth.Server/Options/LogoutRedirectOptions.cs +++ b/src/CodeBeam.UltimateAuth.Server/Options/LogoutRedirectOptions.cs @@ -1,28 +1,27 @@ -namespace CodeBeam.UltimateAuth.Server.Options +namespace CodeBeam.UltimateAuth.Server.Options; + +public sealed class LogoutRedirectOptions { - public sealed class LogoutRedirectOptions - { - /// - /// Whether logout endpoint performs a redirect. - /// - public bool RedirectEnabled { get; set; } = true; + /// + /// Whether logout endpoint performs a redirect. + /// + public bool RedirectEnabled { get; set; } = true; - /// - /// Default redirect URL after logout. - /// - public string RedirectUrl { get; set; } = "/login"; + /// + /// Default redirect URL after logout. + /// + public string RedirectUrl { get; set; } = "/login"; - /// - /// Whether query-based returnUrl override is allowed. - /// - public bool AllowReturnUrlOverride { get; set; } = true; + /// + /// Whether query-based returnUrl override is allowed. + /// + public bool AllowReturnUrlOverride { get; set; } = true; - internal LogoutRedirectOptions Clone() => new() - { - RedirectEnabled = RedirectEnabled, - RedirectUrl = RedirectUrl, - AllowReturnUrlOverride = AllowReturnUrlOverride - }; + internal LogoutRedirectOptions Clone() => new() + { + RedirectEnabled = RedirectEnabled, + RedirectUrl = RedirectUrl, + AllowReturnUrlOverride = AllowReturnUrlOverride + }; - } } diff --git a/src/CodeBeam.UltimateAuth.Server/Options/PrimaryCredentialPolicy.cs b/src/CodeBeam.UltimateAuth.Server/Options/PrimaryCredentialPolicy.cs index 77dd2055..dd143c9e 100644 --- a/src/CodeBeam.UltimateAuth.Server/Options/PrimaryCredentialPolicy.cs +++ b/src/CodeBeam.UltimateAuth.Server/Options/PrimaryCredentialPolicy.cs @@ -1,24 +1,22 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Server.Options -{ - public sealed class PrimaryCredentialPolicy - { - /// - /// Default primary credential for UI-style requests. - /// - public PrimaryCredentialKind Ui { get; set; } = PrimaryCredentialKind.Stateful; +namespace CodeBeam.UltimateAuth.Server.Options; - /// - /// Default primary credential for API requests. - /// - public PrimaryCredentialKind Api { get; set; } = PrimaryCredentialKind.Stateless; +public sealed class PrimaryCredentialPolicy +{ + /// + /// Default primary credential for UI-style requests. + /// + public PrimaryCredentialKind Ui { get; set; } = PrimaryCredentialKind.Stateful; - internal PrimaryCredentialPolicy Clone() => new() - { - Ui = Ui, - Api = Api - }; + /// + /// Default primary credential for API requests. + /// + public PrimaryCredentialKind Api { get; set; } = PrimaryCredentialKind.Stateless; - } + internal PrimaryCredentialPolicy Clone() => new() + { + Ui = Ui, + Api = Api + }; } diff --git a/src/CodeBeam.UltimateAuth.Server/Options/UAuthCookieLifetimeOptions.cs b/src/CodeBeam.UltimateAuth.Server/Options/UAuthCookieLifetimeOptions.cs index 5a1af3fe..41fdc042 100644 --- a/src/CodeBeam.UltimateAuth.Server/Options/UAuthCookieLifetimeOptions.cs +++ b/src/CodeBeam.UltimateAuth.Server/Options/UAuthCookieLifetimeOptions.cs @@ -1,25 +1,22 @@ -using System.Xml.Linq; +namespace CodeBeam.UltimateAuth.Server.Options; -namespace CodeBeam.UltimateAuth.Server.Options +public sealed class UAuthCookieLifetimeOptions { - public sealed class UAuthCookieLifetimeOptions - { - /// - /// Extra lifetime added on top of the logical credential lifetime. - /// Prevents premature cookie eviction by the browser. - /// - public TimeSpan? IdleBuffer { get; set; } = TimeSpan.FromMinutes(5); + /// + /// Extra lifetime added on top of the logical credential lifetime. + /// Prevents premature cookie eviction by the browser. + /// + public TimeSpan? IdleBuffer { get; set; } = TimeSpan.FromMinutes(5); - /// - /// Allows developer to fully override cookie lifetime. - /// If set, buffer logic is ignored. - /// - public TimeSpan? AbsoluteLifetimeOverride { get; set; } + /// + /// Allows developer to fully override cookie lifetime. + /// If set, buffer logic is ignored. + /// + public TimeSpan? AbsoluteLifetimeOverride { get; set; } - internal UAuthCookieLifetimeOptions Clone() => new() - { - IdleBuffer = IdleBuffer, - AbsoluteLifetimeOverride = AbsoluteLifetimeOverride - }; - } + internal UAuthCookieLifetimeOptions Clone() => new() + { + IdleBuffer = IdleBuffer, + AbsoluteLifetimeOverride = AbsoluteLifetimeOverride + }; } diff --git a/src/CodeBeam.UltimateAuth.Server/Options/UAuthCookieSetOptions.cs b/src/CodeBeam.UltimateAuth.Server/Options/UAuthCookieSetOptions.cs index b0f73628..56755f2b 100644 --- a/src/CodeBeam.UltimateAuth.Server/Options/UAuthCookieSetOptions.cs +++ b/src/CodeBeam.UltimateAuth.Server/Options/UAuthCookieSetOptions.cs @@ -1,40 +1,39 @@ using Microsoft.AspNetCore.Http; -namespace CodeBeam.UltimateAuth.Server.Options +namespace CodeBeam.UltimateAuth.Server.Options; + +public sealed class UAuthCookieSetOptions { - public sealed class UAuthCookieSetOptions - { - public bool EnableSessionCookie { get; set; } = true; - public bool EnableAccessTokenCookie { get; set; } = true; - public bool EnableRefreshTokenCookie { get; set; } = true; + public bool EnableSessionCookie { get; set; } = true; + public bool EnableAccessTokenCookie { get; set; } = true; + public bool EnableRefreshTokenCookie { get; set; } = true; - public UAuthCookieOptions Session { get; init; } = new() - { - Name = "uas", - HttpOnly = true, - SameSite = SameSiteMode.None - }; + public UAuthCookieOptions Session { get; init; } = new() + { + Name = "uas", + HttpOnly = true, + SameSite = SameSiteMode.None + }; - public UAuthCookieOptions RefreshToken { get; init; } = new() - { - Name = "uar", - HttpOnly = true, - SameSite = SameSiteMode.None - }; + public UAuthCookieOptions RefreshToken { get; init; } = new() + { + Name = "uar", + HttpOnly = true, + SameSite = SameSiteMode.None + }; - public UAuthCookieOptions AccessToken { get; init; } = new() - { - Name = "uat", - HttpOnly = true, - SameSite = SameSiteMode.None - }; + public UAuthCookieOptions AccessToken { get; init; } = new() + { + Name = "uat", + HttpOnly = true, + SameSite = SameSiteMode.None + }; - internal UAuthCookieSetOptions Clone() => new() - { - Session = Session.Clone(), - RefreshToken = RefreshToken.Clone(), - AccessToken = AccessToken.Clone() - }; + internal UAuthCookieSetOptions Clone() => new() + { + Session = Session.Clone(), + RefreshToken = RefreshToken.Clone(), + AccessToken = AccessToken.Clone() + }; - } } diff --git a/src/CodeBeam.UltimateAuth.Server/Options/UAuthDiagnosticsOptions.cs b/src/CodeBeam.UltimateAuth.Server/Options/UAuthDiagnosticsOptions.cs index 20a83ec3..d8c3023c 100644 --- a/src/CodeBeam.UltimateAuth.Server/Options/UAuthDiagnosticsOptions.cs +++ b/src/CodeBeam.UltimateAuth.Server/Options/UAuthDiagnosticsOptions.cs @@ -1,19 +1,16 @@ -using CodeBeam.UltimateAuth.Core.Options; +namespace CodeBeam.UltimateAuth.Server.Options; -namespace CodeBeam.UltimateAuth.Server.Options +public sealed class UAuthDiagnosticsOptions { - public sealed class UAuthDiagnosticsOptions - { - /// - /// Enables debug / sample-only response headers such as X-UAuth-Refresh. - /// Should be disabled in production. - /// - public bool EnableRefreshHeaders { get; set; } = false; + /// + /// Enables debug / sample-only response headers such as X-UAuth-Refresh. + /// Should be disabled in production. + /// + public bool EnableRefreshHeaders { get; set; } = false; - internal UAuthDiagnosticsOptions Clone() => new() - { - EnableRefreshHeaders = EnableRefreshHeaders - }; + internal UAuthDiagnosticsOptions Clone() => new() + { + EnableRefreshHeaders = EnableRefreshHeaders + }; - } } diff --git a/src/CodeBeam.UltimateAuth.Server/Options/UAuthHubServerOptions.cs b/src/CodeBeam.UltimateAuth.Server/Options/UAuthHubServerOptions.cs index 9eefe565..432dc91e 100644 --- a/src/CodeBeam.UltimateAuth.Server/Options/UAuthHubServerOptions.cs +++ b/src/CodeBeam.UltimateAuth.Server/Options/UAuthHubServerOptions.cs @@ -1,28 +1,25 @@ -using CodeBeam.UltimateAuth.Core.Options; +namespace CodeBeam.UltimateAuth.Server.Options; -namespace CodeBeam.UltimateAuth.Server.Options +public sealed class UAuthHubServerOptions { - public sealed class UAuthHubServerOptions - { - public string? ClientBaseAddress { get; set; } + public string? ClientBaseAddress { get; set; } - public HashSet AllowedClientOrigins { get; set; } = new(); + public HashSet AllowedClientOrigins { get; set; } = new(); - /// - /// Lifetime of hub flow artifacts (UI orchestration). - /// Should be short-lived. - /// - public TimeSpan FlowLifetime { get; set; } = TimeSpan.FromMinutes(2); + /// + /// Lifetime of hub flow artifacts (UI orchestration). + /// Should be short-lived. + /// + public TimeSpan FlowLifetime { get; set; } = TimeSpan.FromMinutes(2); - public string? LoginPath { get; set; } = "/login"; + public string? LoginPath { get; set; } = "/login"; - internal UAuthHubServerOptions Clone() => new() - { - ClientBaseAddress = ClientBaseAddress, - AllowedClientOrigins = new HashSet(AllowedClientOrigins), - FlowLifetime = FlowLifetime, - LoginPath = LoginPath - }; + internal UAuthHubServerOptions Clone() => new() + { + ClientBaseAddress = ClientBaseAddress, + AllowedClientOrigins = new HashSet(AllowedClientOrigins), + FlowLifetime = FlowLifetime, + LoginPath = LoginPath + }; - } } diff --git a/src/CodeBeam.UltimateAuth.Server/Options/UAuthServerOptions.cs b/src/CodeBeam.UltimateAuth.Server/Options/UAuthServerOptions.cs index ddf940da..b80b1c6a 100644 --- a/src/CodeBeam.UltimateAuth.Server/Options/UAuthServerOptions.cs +++ b/src/CodeBeam.UltimateAuth.Server/Options/UAuthServerOptions.cs @@ -4,189 +4,188 @@ using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; -namespace CodeBeam.UltimateAuth.Server.Options +namespace CodeBeam.UltimateAuth.Server.Options; + +/// +/// Server-side configuration for UltimateAuth. +/// Does NOT duplicate Core options. +/// Instead, it composes SessionOptions, TokenOptions, PkceOptions, MultiTenantOptions +/// and adds server-only behaviors (routing, endpoint activation, policies). +/// +public sealed class UAuthServerOptions { /// - /// Server-side configuration for UltimateAuth. - /// Does NOT duplicate Core options. - /// Instead, it composes SessionOptions, TokenOptions, PkceOptions, MultiTenantOptions - /// and adds server-only behaviors (routing, endpoint activation, policies). + /// Defines how UltimateAuth executes authentication flows. + /// Default is Hybrid. /// - public sealed class UAuthServerOptions + public UAuthMode? Mode { get; set; } + + /// + /// Defines how UAuthHub is deployed relative to the application. + /// Default is Integrated + /// Blazor server projects should choose embedded mode for maximum security. + /// + public UAuthHubDeploymentMode HubDeploymentMode { get; set; } = UAuthHubDeploymentMode.Integrated; + + // ------------------------------------------------------- + // ROUTING + // ------------------------------------------------------- + + /// + /// Base API route. Default: "/auth" + /// Changing this prevents conflicts with other auth systems. + /// + public string RoutePrefix { get; set; } = "/auth"; + + + // ------------------------------------------------------- + // CORE OPTION COMPOSITION + // (Server must NOT duplicate Core options) + // ------------------------------------------------------- + + /// + /// Session behavior (lifetime, sliding expiration, etc.) + /// Fully defined in Core. + /// + public UAuthSessionOptions Session { get; set; } = new(); + + /// + /// Token issuing behavior (lifetimes, refresh policies). + /// Fully defined in Core. + /// + public UAuthTokenOptions Tokens { get; set; } = new(); + + /// + /// PKCE configuration (required for WASM). + /// Fully defined in Core. + /// + public UAuthPkceOptions Pkce { get; set; } = new(); + + /// + /// Multi-tenancy behavior (resolver, normalization, etc.) + /// Fully defined in Core. + /// + public UAuthMultiTenantOptions MultiTenant { get; set; } = new(); + + /// + /// Allows advanced users to override cookie behavior. + /// Unsafe combinations will be rejected at startup. + /// + public UAuthCookieSetOptions Cookie { get; set; } = new(); + + public UAuthDiagnosticsOptions Diagnostics { get; set; } = new(); + + internal Type? CustomCookieManagerType { get; private set; } + + public void ReplaceSessionCookieManager() where T : class, IUAuthCookieManager { - /// - /// Defines how UltimateAuth executes authentication flows. - /// Default is Hybrid. - /// - public UAuthMode? Mode { get; set; } - - /// - /// Defines how UAuthHub is deployed relative to the application. - /// Default is Integrated - /// Blazor server projects should choose embedded mode for maximum security. - /// - public UAuthHubDeploymentMode HubDeploymentMode { get; set; } = UAuthHubDeploymentMode.Integrated; - - // ------------------------------------------------------- - // ROUTING - // ------------------------------------------------------- - - /// - /// Base API route. Default: "/auth" - /// Changing this prevents conflicts with other auth systems. - /// - public string RoutePrefix { get; set; } = "/auth"; - - - // ------------------------------------------------------- - // CORE OPTION COMPOSITION - // (Server must NOT duplicate Core options) - // ------------------------------------------------------- - - /// - /// Session behavior (lifetime, sliding expiration, etc.) - /// Fully defined in Core. - /// - public UAuthSessionOptions Session { get; set; } = new(); - - /// - /// Token issuing behavior (lifetimes, refresh policies). - /// Fully defined in Core. - /// - public UAuthTokenOptions Tokens { get; set; } = new(); - - /// - /// PKCE configuration (required for WASM). - /// Fully defined in Core. - /// - public UAuthPkceOptions Pkce { get; set; } = new(); - - /// - /// Multi-tenancy behavior (resolver, normalization, etc.) - /// Fully defined in Core. - /// - public UAuthMultiTenantOptions MultiTenant { get; set; } = new(); - - /// - /// Allows advanced users to override cookie behavior. - /// Unsafe combinations will be rejected at startup. - /// - public UAuthCookieSetOptions Cookie { get; set; } = new(); - - public UAuthDiagnosticsOptions Diagnostics { get; set; } = new(); - - internal Type? CustomCookieManagerType { get; private set; } - - public void ReplaceSessionCookieManager() where T : class, IUAuthCookieManager - { - CustomCookieManagerType = typeof(T); - } + CustomCookieManagerType = typeof(T); + } - // ------------------------------------------------------- - // SERVER-ONLY BEHAVIOR - // ------------------------------------------------------- + // ------------------------------------------------------- + // SERVER-ONLY BEHAVIOR + // ------------------------------------------------------- - public PrimaryCredentialPolicy PrimaryCredential { get; init; } = new(); + public PrimaryCredentialPolicy PrimaryCredential { get; init; } = new(); - public AuthResponseOptions AuthResponse { get; init; } = new(); + public AuthResponseOptions AuthResponse { get; init; } = new(); - public UAuthHubServerOptions Hub { get; set; } = new(); + public UAuthHubServerOptions Hub { get; set; } = new(); - /// - /// Controls how session identifiers are resolved from incoming requests - /// (cookie, header, bearer, query, order, etc.) - /// - public UAuthSessionResolutionOptions SessionResolution { get; set; } = new(); + /// + /// Controls how session identifiers are resolved from incoming requests + /// (cookie, header, bearer, query, order, etc.) + /// + public UAuthSessionResolutionOptions SessionResolution { get; set; } = new(); - /// - /// Enables/disables specific endpoint groups. - /// Useful for API hardening. - /// - public bool? EnableLoginEndpoints { get; set; } = true; - public bool? EnablePkceEndpoints { get; set; } = true; - public bool? EnableTokenEndpoints { get; set; } = true; - public bool? EnableSessionEndpoints { get; set; } = true; - public bool? EnableUserInfoEndpoints { get; set; } = true; + /// + /// Enables/disables specific endpoint groups. + /// Useful for API hardening. + /// + public bool? EnableLoginEndpoints { get; set; } = true; + public bool? EnablePkceEndpoints { get; set; } = true; + public bool? EnableTokenEndpoints { get; set; } = true; + public bool? EnableSessionEndpoints { get; set; } = true; + public bool? EnableUserInfoEndpoints { get; set; } = true; - public bool? EnableUserLifecycleEndpoints { get; set; } = true; - public bool? EnableUserProfileEndpoints { get; set; } = true; - public bool? EnableUserIdentifierEndpoints { get; set; } = true; - public bool? EnableCredentialsEndpoints { get; set; } = true; - public bool? EnableAuthorizationEndpoints { get; set; } = true; + public bool? EnableUserLifecycleEndpoints { get; set; } = true; + public bool? EnableUserProfileEndpoints { get; set; } = true; + public bool? EnableUserIdentifierEndpoints { get; set; } = true; + public bool? EnableCredentialsEndpoints { get; set; } = true; + public bool? EnableAuthorizationEndpoints { get; set; } = true; - public UserIdentifierOptions UserIdentifiers { get; set; } = new(); + public UserIdentifierOptions UserIdentifiers { get; set; } = new(); - /// - /// If true, server will add anti-forgery headers - /// and require proper request metadata. - /// - public bool EnableAntiCsrfProtection { get; set; } = true; + /// + /// If true, server will add anti-forgery headers + /// and require proper request metadata. + /// + public bool EnableAntiCsrfProtection { get; set; } = true; - /// - /// If true, login attempts are rate-limited to prevent brute force attacks. - /// - public bool EnableLoginRateLimiting { get; set; } = true; + /// + /// If true, login attempts are rate-limited to prevent brute force attacks. + /// + public bool EnableLoginRateLimiting { get; set; } = true; - // ------------------------------------------------------- - // CUSTOMIZATION HOOKS - // ------------------------------------------------------- + // ------------------------------------------------------- + // CUSTOMIZATION HOOKS + // ------------------------------------------------------- - /// - /// Allows developers to mutate endpoint routing AFTER UltimateAuth registers defaults. - /// Example: adding new routes, overriding authorization, adding filters. - /// - public Action? OnConfigureEndpoints { get; set; } + /// + /// Allows developers to mutate endpoint routing AFTER UltimateAuth registers defaults. + /// Example: adding new routes, overriding authorization, adding filters. + /// + public Action? OnConfigureEndpoints { get; set; } - /// - /// Allows developers to add or replace server services before DI is built. - /// Example: overriding default ILoginService. - /// - public Action? ConfigureServices { get; set; } + /// + /// Allows developers to add or replace server services before DI is built. + /// Example: overriding default ILoginService. + /// + public Action? ConfigureServices { get; set; } - internal Dictionary> ModeConfigurations { get; set; } = new(); + internal Dictionary> ModeConfigurations { get; set; } = new(); - internal UAuthServerOptions Clone() + internal UAuthServerOptions Clone() + { + return new UAuthServerOptions { - return new UAuthServerOptions - { - Mode = Mode, - HubDeploymentMode = HubDeploymentMode, - RoutePrefix = RoutePrefix, - - Session = Session.Clone(), - Tokens = Tokens.Clone(), - Pkce = Pkce.Clone(), - MultiTenant = MultiTenant.Clone(), - Cookie = Cookie.Clone(), - Diagnostics = Diagnostics.Clone(), - - PrimaryCredential = PrimaryCredential.Clone(), - AuthResponse = AuthResponse.Clone(), - Hub = Hub.Clone(), - SessionResolution = SessionResolution.Clone(), - UserIdentifiers = UserIdentifiers.Clone(), - - EnableLoginEndpoints = EnableLoginEndpoints, - EnablePkceEndpoints = EnablePkceEndpoints, - EnableTokenEndpoints = EnableTokenEndpoints, - EnableSessionEndpoints = EnableSessionEndpoints, - EnableUserInfoEndpoints = EnableUserInfoEndpoints, - EnableUserLifecycleEndpoints = EnableUserLifecycleEndpoints, - EnableUserProfileEndpoints = EnableUserProfileEndpoints, - EnableCredentialsEndpoints = EnableCredentialsEndpoints, - EnableAuthorizationEndpoints = EnableAuthorizationEndpoints, - - EnableAntiCsrfProtection = EnableAntiCsrfProtection, - EnableLoginRateLimiting = EnableLoginRateLimiting, - - ModeConfigurations = ModeConfigurations, - OnConfigureEndpoints = OnConfigureEndpoints, - ConfigureServices = ConfigureServices, - CustomCookieManagerType = CustomCookieManagerType - }; - } + Mode = Mode, + HubDeploymentMode = HubDeploymentMode, + RoutePrefix = RoutePrefix, + + Session = Session.Clone(), + Tokens = Tokens.Clone(), + Pkce = Pkce.Clone(), + MultiTenant = MultiTenant.Clone(), + Cookie = Cookie.Clone(), + Diagnostics = Diagnostics.Clone(), + + PrimaryCredential = PrimaryCredential.Clone(), + AuthResponse = AuthResponse.Clone(), + Hub = Hub.Clone(), + SessionResolution = SessionResolution.Clone(), + UserIdentifiers = UserIdentifiers.Clone(), + + EnableLoginEndpoints = EnableLoginEndpoints, + EnablePkceEndpoints = EnablePkceEndpoints, + EnableTokenEndpoints = EnableTokenEndpoints, + EnableSessionEndpoints = EnableSessionEndpoints, + EnableUserInfoEndpoints = EnableUserInfoEndpoints, + EnableUserLifecycleEndpoints = EnableUserLifecycleEndpoints, + EnableUserProfileEndpoints = EnableUserProfileEndpoints, + EnableCredentialsEndpoints = EnableCredentialsEndpoints, + EnableAuthorizationEndpoints = EnableAuthorizationEndpoints, + + EnableAntiCsrfProtection = EnableAntiCsrfProtection, + EnableLoginRateLimiting = EnableLoginRateLimiting, + + ModeConfigurations = ModeConfigurations, + OnConfigureEndpoints = OnConfigureEndpoints, + ConfigureServices = ConfigureServices, + CustomCookieManagerType = CustomCookieManagerType + }; } } diff --git a/src/CodeBeam.UltimateAuth.Server/Options/UAuthServerOptionsValidator.cs b/src/CodeBeam.UltimateAuth.Server/Options/UAuthServerOptionsValidator.cs index 3d135cb2..1bc7c36f 100644 --- a/src/CodeBeam.UltimateAuth.Server/Options/UAuthServerOptionsValidator.cs +++ b/src/CodeBeam.UltimateAuth.Server/Options/UAuthServerOptionsValidator.cs @@ -1,54 +1,42 @@ using CodeBeam.UltimateAuth.Core; using Microsoft.Extensions.Options; -namespace CodeBeam.UltimateAuth.Server.Options +namespace CodeBeam.UltimateAuth.Server.Options; + +public sealed class UAuthServerOptionsValidator : IValidateOptions { - public sealed class UAuthServerOptionsValidator : IValidateOptions + public ValidateOptionsResult Validate(string? name, UAuthServerOptions options) { - public ValidateOptionsResult Validate( - string? name, - UAuthServerOptions options) + if (string.IsNullOrWhiteSpace(options.RoutePrefix)) { - if (string.IsNullOrWhiteSpace(options.RoutePrefix)) - { - return ValidateOptionsResult.Fail( - "RoutePrefix must be specified."); - } + return ValidateOptionsResult.Fail( "RoutePrefix must be specified."); + } - if (options.RoutePrefix.Contains("//")) - { - return ValidateOptionsResult.Fail("RoutePrefix cannot contain '//'."); - } + if (options.RoutePrefix.Contains("//")) + { + return ValidateOptionsResult.Fail("RoutePrefix cannot contain '//'."); + } - // ------------------------- - // AUTH MODE VALIDATION - // ------------------------- - if (options.Mode.HasValue && !Enum.IsDefined(typeof(UAuthMode), options.Mode)) + if (options.Mode.HasValue && !Enum.IsDefined(typeof(UAuthMode), options.Mode)) + { + return ValidateOptionsResult.Fail($"Invalid UAuthMode: {options.Mode}"); + } + + if (options.Mode != UAuthMode.PureJwt) + { + if (options.Session.Lifetime <= TimeSpan.Zero) { - return ValidateOptionsResult.Fail( - $"Invalid UAuthMode: {options.Mode}"); + return ValidateOptionsResult.Fail("Session.Lifetime must be greater than zero."); } - // ------------------------- - // SESSION VALIDATION - // ------------------------- - if (options.Mode != UAuthMode.PureJwt) + if (options.Session.MaxLifetime is not null && + options.Session.MaxLifetime <= TimeSpan.Zero) { - if (options.Session.Lifetime <= TimeSpan.Zero) - { - return ValidateOptionsResult.Fail( - "Session.Lifetime must be greater than zero."); - } - - if (options.Session.MaxLifetime is not null && - options.Session.MaxLifetime <= TimeSpan.Zero) - { - return ValidateOptionsResult.Fail( - "Session.MaxLifetime must be greater than zero when specified."); - } + return ValidateOptionsResult.Fail( + "Session.MaxLifetime must be greater than zero when specified."); } - - return ValidateOptionsResult.Success; } + + return ValidateOptionsResult.Success; } } diff --git a/src/CodeBeam.UltimateAuth.Server/Options/UAuthSessionResolutionOptions.cs b/src/CodeBeam.UltimateAuth.Server/Options/UAuthSessionResolutionOptions.cs index 37881c5a..c7876148 100644 --- a/src/CodeBeam.UltimateAuth.Server/Options/UAuthSessionResolutionOptions.cs +++ b/src/CodeBeam.UltimateAuth.Server/Options/UAuthSessionResolutionOptions.cs @@ -1,37 +1,36 @@ -namespace CodeBeam.UltimateAuth.Server.Options +namespace CodeBeam.UltimateAuth.Server.Options; + +// TODO: Check header/query parameter name conflicts with other auth mechanisms (e.g. API keys, OAuth tokens) +// We removed CookieName here because cookie-based session resolution, there may be other conflicts. +public sealed class UAuthSessionResolutionOptions { - // TODO: Check header/query parameter name conflicts with other auth mechanisms (e.g. API keys, OAuth tokens) - // We removed CookieName here because cookie-based session resolution, there may be other conflicts. - public sealed class UAuthSessionResolutionOptions - { - public bool EnableBearer { get; set; } = true; - public bool EnableHeader { get; set; } = true; - public bool EnableCookie { get; set; } = true; - public bool EnableQuery { get; set; } = false; + public bool EnableBearer { get; set; } = true; + public bool EnableHeader { get; set; } = true; + public bool EnableCookie { get; set; } = true; + public bool EnableQuery { get; set; } = false; - public string HeaderName { get; set; } = "X-UAuth-Session"; - public string QueryParameterName { get; set; } = "session_id"; + public string HeaderName { get; set; } = "X-UAuth-Session"; + public string QueryParameterName { get; set; } = "session_id"; - // Precedence order - // Example: Bearer, Header, Cookie, Query - public List Order { get; set; } = new() - { - "Bearer", - "Header", - "Cookie", - "Query" - }; + // Precedence order + // Example: Bearer, Header, Cookie, Query + public List Order { get; set; } = new() + { + "Bearer", + "Header", + "Cookie", + "Query" + }; - internal UAuthSessionResolutionOptions Clone() => new() - { - EnableBearer = EnableBearer, - EnableHeader = EnableHeader, - EnableCookie = EnableCookie, - EnableQuery = EnableQuery, - HeaderName = HeaderName, - QueryParameterName = QueryParameterName, - Order = new List(Order) - }; + internal UAuthSessionResolutionOptions Clone() => new() + { + EnableBearer = EnableBearer, + EnableHeader = EnableHeader, + EnableCookie = EnableCookie, + EnableQuery = EnableQuery, + HeaderName = HeaderName, + QueryParameterName = QueryParameterName, + Order = new List(Order) + }; - } } diff --git a/src/CodeBeam.UltimateAuth.Server/Options/UserIdentifierOptions.cs b/src/CodeBeam.UltimateAuth.Server/Options/UserIdentifierOptions.cs index 0f63cc91..e7e14434 100644 --- a/src/CodeBeam.UltimateAuth.Server/Options/UserIdentifierOptions.cs +++ b/src/CodeBeam.UltimateAuth.Server/Options/UserIdentifierOptions.cs @@ -1,29 +1,27 @@ -namespace CodeBeam.UltimateAuth.Server.Options -{ - public sealed class UserIdentifierOptions - { - public bool AllowUsernameChange { get; set; } = true; - public bool AllowMultipleUsernames { get; set; } = false; - public bool AllowMultipleEmail { get; set; } = true; - public bool AllowMultiplePhone { get; set; } = true; +namespace CodeBeam.UltimateAuth.Server.Options; - public bool RequireEmailVerification { get; set; } = false; - public bool RequirePhoneVerification { get; set; } = false; +public sealed class UserIdentifierOptions +{ + public bool AllowUsernameChange { get; set; } = true; + public bool AllowMultipleUsernames { get; set; } = false; + public bool AllowMultipleEmail { get; set; } = true; + public bool AllowMultiplePhone { get; set; } = true; - public bool AllowAdminOverride { get; set; } = true; - public bool AllowUserOverride { get; set; } = true; + public bool RequireEmailVerification { get; set; } = false; + public bool RequirePhoneVerification { get; set; } = false; - internal UserIdentifierOptions Clone() => new() - { - AllowUsernameChange = AllowUsernameChange, - AllowMultipleUsernames = AllowMultipleUsernames, - AllowMultipleEmail = AllowMultipleEmail, - AllowMultiplePhone = AllowMultiplePhone, - RequireEmailVerification = RequireEmailVerification, - RequirePhoneVerification = RequirePhoneVerification, - AllowAdminOverride = AllowAdminOverride, - AllowUserOverride = AllowUserOverride - }; - } + public bool AllowAdminOverride { get; set; } = true; + public bool AllowUserOverride { get; set; } = true; + internal UserIdentifierOptions Clone() => new() + { + AllowUsernameChange = AllowUsernameChange, + AllowMultipleUsernames = AllowMultipleUsernames, + AllowMultipleEmail = AllowMultipleEmail, + AllowMultiplePhone = AllowMultiplePhone, + RequireEmailVerification = RequireEmailVerification, + RequirePhoneVerification = RequirePhoneVerification, + AllowAdminOverride = AllowAdminOverride, + AllowUserOverride = AllowUserOverride + }; } diff --git a/src/CodeBeam.UltimateAuth.Server/ProductInfo/UAuthServerProductInfo.cs b/src/CodeBeam.UltimateAuth.Server/ProductInfo/UAuthServerProductInfo.cs index a24437f0..1919ec0a 100644 --- a/src/CodeBeam.UltimateAuth.Server/ProductInfo/UAuthServerProductInfo.cs +++ b/src/CodeBeam.UltimateAuth.Server/ProductInfo/UAuthServerProductInfo.cs @@ -2,18 +2,17 @@ using CodeBeam.UltimateAuth.Core.Runtime; using CodeBeam.UltimateAuth.Server.Options; -namespace CodeBeam.UltimateAuth.Server.Runtime +namespace CodeBeam.UltimateAuth.Server.Runtime; + +public sealed class UAuthServerProductInfo { - public sealed class UAuthServerProductInfo - { - public string ProductName { get; init; } = "UltimateAuthServer"; - public UAuthProductInfo Core { get; init; } = default!; + public string ProductName { get; init; } = "UltimateAuthServer"; + public UAuthProductInfo Core { get; init; } = default!; - public UAuthMode? AuthMode { get; init; } - public UAuthHubDeploymentMode HubDeploymentMode { get; init; } + public UAuthMode? AuthMode { get; init; } + public UAuthHubDeploymentMode HubDeploymentMode { get; init; } - public bool PkceEnabled { get; init; } - public bool RefreshEnabled { get; init; } - public bool MultiTenancyEnabled { get; init; } - } + public bool PkceEnabled { get; init; } + public bool RefreshEnabled { get; init; } + public bool MultiTenancyEnabled { get; init; } } diff --git a/src/CodeBeam.UltimateAuth.Server/Services/DefaultRefreshFlowService.cs b/src/CodeBeam.UltimateAuth.Server/Services/DefaultRefreshFlowService.cs deleted file mode 100644 index 81210e77..00000000 --- a/src/CodeBeam.UltimateAuth.Server/Services/DefaultRefreshFlowService.cs +++ /dev/null @@ -1,238 +0,0 @@ -using CodeBeam.UltimateAuth.Core; -using CodeBeam.UltimateAuth.Core.Abstractions; -using CodeBeam.UltimateAuth.Core.Contracts; -using CodeBeam.UltimateAuth.Core.Domain; -using CodeBeam.UltimateAuth.Server.Auth; -using CodeBeam.UltimateAuth.Server.Infrastructure; -using System.ComponentModel.DataAnnotations; - -namespace CodeBeam.UltimateAuth.Server.Services -{ - internal sealed class DefaultRefreshFlowService : IRefreshFlowService - { - private readonly ISessionValidator _sessionValidator; - private readonly ISessionTouchService _sessionRefresh; - private readonly IRefreshTokenRotationService _tokenRotation; - private readonly IRefreshTokenStore _refreshTokenStore; - - public DefaultRefreshFlowService( - ISessionValidator sessionValidator, - ISessionTouchService sessionRefresh, - IRefreshTokenRotationService tokenRotation, - IRefreshTokenStore refreshTokenStore) - { - _sessionValidator = sessionValidator; - _sessionRefresh = sessionRefresh; - _tokenRotation = tokenRotation; - _refreshTokenStore = refreshTokenStore; - } - - public async Task RefreshAsync(AuthFlowContext flow, RefreshFlowRequest request, CancellationToken ct = default) - { - return flow.EffectiveMode switch - { - UAuthMode.PureOpaque => - await HandleSessionOnlyAsync(flow, request, ct), - - UAuthMode.PureJwt => - await HandleTokenOnlyAsync(flow, request, ct), - - UAuthMode.Hybrid => - await HandleHybridAsync(flow, request, ct), - - UAuthMode.SemiHybrid => - await HandleSemiHybridAsync(flow, request, ct), - - _ => RefreshFlowResult.ReauthRequired() - }; - } - - private async Task HandleSessionOnlyAsync(AuthFlowContext flow, RefreshFlowRequest request, CancellationToken ct) - { - if (request.SessionId is null) - return RefreshFlowResult.ReauthRequired(); - - var validation = await _sessionValidator.ValidateSessionAsync( - new SessionValidationContext - { - Tenant = flow.Tenant, - SessionId = request.SessionId.Value, - Now = request.Now, - Device = request.Device - }, - ct); - - if (!validation.IsValid) - return RefreshFlowResult.ReauthRequired(); - - var touchPolicy = new SessionTouchPolicy - { - TouchInterval = flow.EffectiveOptions.Options.Session.TouchInterval - }; - - var refresh = await _sessionRefresh.RefreshAsync(validation, touchPolicy, request.TouchMode, request.Now, ct); - - if (!refresh.IsSuccess || refresh.SessionId is null) - return RefreshFlowResult.ReauthRequired(); - - return RefreshFlowResult.Success(refresh.DidTouch ? RefreshOutcome.Touched : RefreshOutcome.NoOp, refresh.SessionId); - } - - private async Task HandleTokenOnlyAsync(AuthFlowContext flow, RefreshFlowRequest request, CancellationToken ct) - { - if (string.IsNullOrWhiteSpace(request.RefreshToken)) - return RefreshFlowResult.ReauthRequired(); - - var rotation = await _tokenRotation.RotateAsync( - flow, - new RefreshTokenRotationContext - { - RefreshToken = request.RefreshToken!, - Now = request.Now, - Device = request.Device - }, - ct); - - if (!rotation.Result.IsSuccess) - return RefreshFlowResult.ReauthRequired(); - - //if (rotation.Result.RefreshToken is not null) - //{ - // var converter = _userIdConverterResolver.GetConverter(); - - // await _refreshTokenStore.StoreAsync( - // flow.TenantId, - // new StoredRefreshToken - // { - // TokenHash = rotation.Result.RefreshToken.TokenHash, - // UserId = rotation.UserId!, - // SessionId = rotation.SessionId!.Value, - // ChainId = rotation.ChainId, - // ExpiresAt = rotation.Result.RefreshToken.ExpiresAt, - // IssuedAt = request.Now - // }, - // ct); - //} - - return RefreshFlowResult.Success( - outcome: RefreshOutcome.Rotated, - accessToken: rotation.Result.AccessToken, - refreshToken: rotation.Result.RefreshToken); - } - - private async Task HandleHybridAsync(AuthFlowContext flow, RefreshFlowRequest request, CancellationToken ct) - { - if (request.SessionId is null || string.IsNullOrWhiteSpace(request.RefreshToken)) - return RefreshFlowResult.ReauthRequired(); - - var validation = await _sessionValidator.ValidateSessionAsync( - new SessionValidationContext - { - Tenant = flow.Tenant, - SessionId = request.SessionId.Value, - Now = request.Now, - Device = request.Device - }, - ct); - - if (!validation.IsValid) - return RefreshFlowResult.ReauthRequired(); - - var rotation = await _tokenRotation.RotateAsync( - flow, - new RefreshTokenRotationContext - { - RefreshToken = request.RefreshToken!, - Now = request.Now, - Device = request.Device, - ExpectedSessionId = request.SessionId.Value - }, - ct); - - if (!rotation.Result.IsSuccess) - return RefreshFlowResult.ReauthRequired(); - - var touchPolicy = new SessionTouchPolicy - { - TouchInterval = flow.EffectiveOptions.Options.Session.TouchInterval - }; - - var refresh = await _sessionRefresh.RefreshAsync(validation, touchPolicy, request.TouchMode, request.Now, ct); - - if (!refresh.IsSuccess || refresh.SessionId is null) - return RefreshFlowResult.ReauthRequired(); - - //await StoreRefreshTokenAsync(flow, rotation, request.Now, ct); - - return RefreshFlowResult.Success( - outcome: RefreshOutcome.Rotated, - sessionId: refresh.SessionId, - accessToken: rotation.Result.AccessToken, - refreshToken: rotation.Result.RefreshToken); - } - - private async Task HandleSemiHybridAsync(AuthFlowContext flow, RefreshFlowRequest request, CancellationToken ct) - { - if (request.SessionId is null || string.IsNullOrWhiteSpace(request.RefreshToken)) - return RefreshFlowResult.ReauthRequired(); - - var validation = await _sessionValidator.ValidateSessionAsync( - new SessionValidationContext - { - Tenant = flow.Tenant, - SessionId = request.SessionId.Value, - Now = request.Now, - Device = request.Device - }, - ct); - - if (!validation.IsValid) - return RefreshFlowResult.ReauthRequired(); - - var rotation = await _tokenRotation.RotateAsync( - flow, - new RefreshTokenRotationContext - { - RefreshToken = request.RefreshToken!, - Now = request.Now, - Device = request.Device, - ExpectedSessionId = request.SessionId.Value - }, - ct); - - if (!rotation.Result.IsSuccess) - return RefreshFlowResult.ReauthRequired(); - - // ❗ NO SESSION TOUCH HERE - // Session lifetime is fixed in SemiHybrid - - //await StoreRefreshTokenAsync(flow, rotation, request.Now, ct); - - return RefreshFlowResult.Success( - outcome: RefreshOutcome.Rotated, - sessionId: request.SessionId.Value, - accessToken: rotation.Result.AccessToken, - refreshToken: rotation.Result.RefreshToken); - } - - //private async Task StoreRefreshTokenAsync(AuthFlowContext flow, RefreshTokenRotationExecution rotation, DateTimeOffset now, CancellationToken ct) - //{ - // if (rotation.Result.RefreshToken is null) - // return; - - // await _refreshTokenStore.StoreAsync( - // flow.TenantId, - // new StoredRefreshToken - // { - // TokenHash = rotation.Result.RefreshToken.TokenHash, - // UserId = rotation.UserId!, - // SessionId = rotation.SessionId!.Value, - // ChainId = rotation.ChainId, - // ExpiresAt = rotation.Result.RefreshToken.ExpiresAt, - // IssuedAt = now - // }, - // ct); - //} - - } -} diff --git a/src/CodeBeam.UltimateAuth.Server/Services/IRefreshFlowService.cs b/src/CodeBeam.UltimateAuth.Server/Services/IRefreshFlowService.cs index 663bb9ba..0b13193c 100644 --- a/src/CodeBeam.UltimateAuth.Server/Services/IRefreshFlowService.cs +++ b/src/CodeBeam.UltimateAuth.Server/Services/IRefreshFlowService.cs @@ -1,10 +1,9 @@ using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Server.Auth; -namespace CodeBeam.UltimateAuth.Server.Services +namespace CodeBeam.UltimateAuth.Server.Services; + +public interface IRefreshFlowService { - public interface IRefreshFlowService - { - Task RefreshAsync(AuthFlowContext flow, RefreshFlowRequest request, CancellationToken ct = default); - } + Task RefreshAsync(AuthFlowContext flow, RefreshFlowRequest request, CancellationToken ct = default); } diff --git a/src/CodeBeam.UltimateAuth.Server/Services/IRefreshTokenRotationService.cs b/src/CodeBeam.UltimateAuth.Server/Services/IRefreshTokenRotationService.cs index 6bb844b2..16b55080 100644 --- a/src/CodeBeam.UltimateAuth.Server/Services/IRefreshTokenRotationService.cs +++ b/src/CodeBeam.UltimateAuth.Server/Services/IRefreshTokenRotationService.cs @@ -1,10 +1,9 @@ using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Server.Auth; -namespace CodeBeam.UltimateAuth.Server.Services +namespace CodeBeam.UltimateAuth.Server.Services; + +public interface IRefreshTokenRotationService { - public interface IRefreshTokenRotationService - { - Task RotateAsync(AuthFlowContext flow, RefreshTokenRotationContext context, CancellationToken ct = default); - } + Task RotateAsync(AuthFlowContext flow, RefreshTokenRotationContext context, CancellationToken ct = default); } diff --git a/src/CodeBeam.UltimateAuth.Server/Services/ISessionQueryService.cs b/src/CodeBeam.UltimateAuth.Server/Services/ISessionQueryService.cs index 6e88570a..fd6e472d 100644 --- a/src/CodeBeam.UltimateAuth.Server/Services/ISessionQueryService.cs +++ b/src/CodeBeam.UltimateAuth.Server/Services/ISessionQueryService.cs @@ -1,5 +1,4 @@ -using CodeBeam.UltimateAuth.Core.Contracts; -using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.Domain; // TenantId parameter only come from AuthFlowContext. namespace CodeBeam.UltimateAuth.Server.Services; diff --git a/src/CodeBeam.UltimateAuth.Server/Services/IUAuthFlowService.cs b/src/CodeBeam.UltimateAuth.Server/Services/IUAuthFlowService.cs index a0f00e8d..036c5481 100644 --- a/src/CodeBeam.UltimateAuth.Server/Services/IUAuthFlowService.cs +++ b/src/CodeBeam.UltimateAuth.Server/Services/IUAuthFlowService.cs @@ -1,28 +1,27 @@ using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Server.Auth; -namespace CodeBeam.UltimateAuth.Server.Services +namespace CodeBeam.UltimateAuth.Server.Services; + +/// +/// Handles authentication flows such as login, +/// logout, session refresh and reauthentication. +/// +public interface IUAuthFlowService { - /// - /// Handles authentication flows such as login, - /// logout, session refresh and reauthentication. - /// - public interface IUAuthFlowService - { - Task LoginAsync(AuthFlowContext flow, LoginRequest request, CancellationToken ct = default); + Task LoginAsync(AuthFlowContext flow, LoginRequest request, CancellationToken ct = default); - Task LoginAsync(AuthFlowContext auth, AuthExecutionContext execution, LoginRequest request, CancellationToken ct); + Task LoginAsync(AuthFlowContext auth, AuthExecutionContext execution, LoginRequest request, CancellationToken ct); - Task ExternalLoginAsync(ExternalLoginRequest request, CancellationToken ct = default); + Task ExternalLoginAsync(ExternalLoginRequest request, CancellationToken ct = default); - Task BeginMfaAsync(BeginMfaRequest request, CancellationToken ct = default); + Task BeginMfaAsync(BeginMfaRequest request, CancellationToken ct = default); - Task CompleteMfaAsync(CompleteMfaRequest request, CancellationToken ct = default); + Task CompleteMfaAsync(CompleteMfaRequest request, CancellationToken ct = default); - Task LogoutAsync(LogoutRequest request, CancellationToken ct = default); + Task LogoutAsync(LogoutRequest request, CancellationToken ct = default); - Task LogoutAllAsync(LogoutAllRequest request, CancellationToken ct = default); + Task LogoutAllAsync(LogoutAllRequest request, CancellationToken ct = default); - Task ReauthenticateAsync(ReauthRequest request, CancellationToken ct = default); - } + Task ReauthenticateAsync(ReauthRequest request, CancellationToken ct = default); } diff --git a/src/CodeBeam.UltimateAuth.Server/Services/RefreshFlowService.cs b/src/CodeBeam.UltimateAuth.Server/Services/RefreshFlowService.cs new file mode 100644 index 00000000..e3db3a89 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Services/RefreshFlowService.cs @@ -0,0 +1,207 @@ +using CodeBeam.UltimateAuth.Core; +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Server.Auth; +using CodeBeam.UltimateAuth.Server.Flows; + +namespace CodeBeam.UltimateAuth.Server.Services; + +internal sealed class RefreshFlowService : IRefreshFlowService +{ + private readonly ISessionValidator _sessionValidator; + private readonly ISessionTouchService _sessionRefresh; + private readonly IRefreshTokenRotationService _tokenRotation; + + public RefreshFlowService( + ISessionValidator sessionValidator, + ISessionTouchService sessionRefresh, + IRefreshTokenRotationService tokenRotation) + { + _sessionValidator = sessionValidator; + _sessionRefresh = sessionRefresh; + _tokenRotation = tokenRotation; + } + + public async Task RefreshAsync(AuthFlowContext flow, RefreshFlowRequest request, CancellationToken ct = default) + { + return flow.EffectiveMode switch + { + UAuthMode.PureOpaque => + await HandleSessionOnlyAsync(flow, request, ct), + + UAuthMode.PureJwt => + await HandleTokenOnlyAsync(flow, request, ct), + + UAuthMode.Hybrid => + await HandleHybridAsync(flow, request, ct), + + UAuthMode.SemiHybrid => + await HandleSemiHybridAsync(flow, request, ct), + + _ => RefreshFlowResult.ReauthRequired() + }; + } + + private async Task HandleSessionOnlyAsync(AuthFlowContext flow, RefreshFlowRequest request, CancellationToken ct) + { + if (request.SessionId is null) + return RefreshFlowResult.ReauthRequired(); + + var validation = await _sessionValidator.ValidateSessionAsync( + new SessionValidationContext + { + Tenant = flow.Tenant, + SessionId = request.SessionId.Value, + Now = request.Now, + Device = request.Device + }, + ct); + + if (!validation.IsValid) + return RefreshFlowResult.ReauthRequired(); + + var touchPolicy = new SessionTouchPolicy + { + TouchInterval = flow.EffectiveOptions.Options.Session.TouchInterval + }; + + var refresh = await _sessionRefresh.RefreshAsync(validation, touchPolicy, request.TouchMode, request.Now, ct); + + if (!refresh.IsSuccess || refresh.SessionId is null) + return RefreshFlowResult.ReauthRequired(); + + return RefreshFlowResult.Success(refresh.DidTouch ? RefreshOutcome.Touched : RefreshOutcome.NoOp, refresh.SessionId); + } + + private async Task HandleTokenOnlyAsync(AuthFlowContext flow, RefreshFlowRequest request, CancellationToken ct) + { + if (string.IsNullOrWhiteSpace(request.RefreshToken)) + return RefreshFlowResult.ReauthRequired(); + + var rotation = await _tokenRotation.RotateAsync( + flow, + new RefreshTokenRotationContext + { + RefreshToken = request.RefreshToken!, + Now = request.Now, + Device = request.Device + }, + ct); + + if (!rotation.Result.IsSuccess) + return RefreshFlowResult.ReauthRequired(); + + //if (rotation.Result.RefreshToken is not null) + //{ + // var converter = _userIdConverterResolver.GetConverter(); + + // await _refreshTokenStore.StoreAsync( + // flow.TenantId, + // new StoredRefreshToken + // { + // TokenHash = rotation.Result.RefreshToken.TokenHash, + // UserId = rotation.UserId!, + // SessionId = rotation.SessionId!.Value, + // ChainId = rotation.ChainId, + // ExpiresAt = rotation.Result.RefreshToken.ExpiresAt, + // IssuedAt = request.Now + // }, + // ct); + //} + + return RefreshFlowResult.Success( + outcome: RefreshOutcome.Rotated, + accessToken: rotation.Result.AccessToken, + refreshToken: rotation.Result.RefreshToken); + } + + private async Task HandleHybridAsync(AuthFlowContext flow, RefreshFlowRequest request, CancellationToken ct) + { + if (request.SessionId is null || string.IsNullOrWhiteSpace(request.RefreshToken)) + return RefreshFlowResult.ReauthRequired(); + + var validation = await _sessionValidator.ValidateSessionAsync( + new SessionValidationContext + { + Tenant = flow.Tenant, + SessionId = request.SessionId.Value, + Now = request.Now, + Device = request.Device + }, + ct); + + if (!validation.IsValid) + return RefreshFlowResult.ReauthRequired(); + + var rotation = await _tokenRotation.RotateAsync( + flow, + new RefreshTokenRotationContext + { + RefreshToken = request.RefreshToken!, + Now = request.Now, + Device = request.Device, + ExpectedSessionId = request.SessionId.Value + }, + ct); + + if (!rotation.Result.IsSuccess) + return RefreshFlowResult.ReauthRequired(); + + var touchPolicy = new SessionTouchPolicy + { + TouchInterval = flow.EffectiveOptions.Options.Session.TouchInterval + }; + + var refresh = await _sessionRefresh.RefreshAsync(validation, touchPolicy, request.TouchMode, request.Now, ct); + + if (!refresh.IsSuccess || refresh.SessionId is null) + return RefreshFlowResult.ReauthRequired(); + + //await StoreRefreshTokenAsync(flow, rotation, request.Now, ct); + + return RefreshFlowResult.Success( + outcome: RefreshOutcome.Rotated, + sessionId: refresh.SessionId, + accessToken: rotation.Result.AccessToken, + refreshToken: rotation.Result.RefreshToken); + } + + private async Task HandleSemiHybridAsync(AuthFlowContext flow, RefreshFlowRequest request, CancellationToken ct) + { + if (request.SessionId is null || string.IsNullOrWhiteSpace(request.RefreshToken)) + return RefreshFlowResult.ReauthRequired(); + + var validation = await _sessionValidator.ValidateSessionAsync( + new SessionValidationContext + { + Tenant = flow.Tenant, + SessionId = request.SessionId.Value, + Now = request.Now, + Device = request.Device + }, + ct); + + if (!validation.IsValid) + return RefreshFlowResult.ReauthRequired(); + + var rotation = await _tokenRotation.RotateAsync( + flow, + new RefreshTokenRotationContext + { + RefreshToken = request.RefreshToken!, + Now = request.Now, + Device = request.Device, + ExpectedSessionId = request.SessionId.Value + }, + ct); + + if (!rotation.Result.IsSuccess) + return RefreshFlowResult.ReauthRequired(); + + return RefreshFlowResult.Success( + outcome: RefreshOutcome.Rotated, + sessionId: request.SessionId.Value, + accessToken: rotation.Result.AccessToken, + refreshToken: rotation.Result.RefreshToken); + } +} diff --git a/src/CodeBeam.UltimateAuth.Server/Services/RefreshTokenRotationService.cs b/src/CodeBeam.UltimateAuth.Server/Services/RefreshTokenRotationService.cs index 6a8f2c52..ad628e19 100644 --- a/src/CodeBeam.UltimateAuth.Server/Services/RefreshTokenRotationService.cs +++ b/src/CodeBeam.UltimateAuth.Server/Services/RefreshTokenRotationService.cs @@ -4,7 +4,6 @@ using CodeBeam.UltimateAuth.Core.MultiTenancy; using CodeBeam.UltimateAuth.Server.Abstactions; using CodeBeam.UltimateAuth.Server.Auth; -using System; namespace CodeBeam.UltimateAuth.Server.Services; diff --git a/src/CodeBeam.UltimateAuth.Server/Services/UAuthFlowService.cs b/src/CodeBeam.UltimateAuth.Server/Services/UAuthFlowService.cs index 7ef7ece7..18202efc 100644 --- a/src/CodeBeam.UltimateAuth.Server/Services/UAuthFlowService.cs +++ b/src/CodeBeam.UltimateAuth.Server/Services/UAuthFlowService.cs @@ -1,112 +1,94 @@ -using CodeBeam.UltimateAuth.Core.Abstractions; -using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Core.Options; -using CodeBeam.UltimateAuth.Server.Abstactions; using CodeBeam.UltimateAuth.Server.Auth; using CodeBeam.UltimateAuth.Server.Extensions; using CodeBeam.UltimateAuth.Server.Infrastructure; -using CodeBeam.UltimateAuth.Server.Login; -using System; +using CodeBeam.UltimateAuth.Server.Flows; -namespace CodeBeam.UltimateAuth.Server.Services +namespace CodeBeam.UltimateAuth.Server.Services; + +internal sealed class UAuthFlowService : IUAuthFlowService { - internal sealed class UAuthFlowService : IUAuthFlowService + private readonly IAuthFlowContextAccessor _authFlow; + private readonly ILoginOrchestrator _loginOrchestrator; + private readonly ISessionOrchestrator _orchestrator; + + public UAuthFlowService( + IAuthFlowContextAccessor authFlow, + ILoginOrchestrator loginOrchestrator, + ISessionOrchestrator orchestrator) { - private readonly IAuthFlowContextAccessor _authFlow; - private readonly ILoginOrchestrator _loginOrchestrator; - private readonly ISessionOrchestrator _orchestrator; - private readonly ISessionQueryService _queries; - private readonly ITokenIssuer _tokens; - private readonly IUserIdConverterResolver _userIdConverterResolver; - private readonly IRefreshTokenValidator _tokenValidator; - - public UAuthFlowService( - IAuthFlowContextAccessor authFlow, - ILoginOrchestrator loginOrchestrator, - ISessionOrchestrator orchestrator, - ISessionQueryService queries, - ITokenIssuer tokens, - IUserIdConverterResolver userIdConverterResolver, - IRefreshTokenValidator tokenValidator) - { - _authFlow = authFlow; - _loginOrchestrator = loginOrchestrator; - _orchestrator = orchestrator; - _queries = queries; - _tokens = tokens; - _userIdConverterResolver = userIdConverterResolver; - _tokenValidator = tokenValidator; - } - - public Task BeginMfaAsync(BeginMfaRequest request, CancellationToken ct = default) - { - throw new NotImplementedException(); - } + _authFlow = authFlow; + _loginOrchestrator = loginOrchestrator; + _orchestrator = orchestrator; + } - public Task CompleteMfaAsync(CompleteMfaRequest request, CancellationToken ct = default) - { - throw new NotImplementedException(); - } + public Task BeginMfaAsync(BeginMfaRequest request, CancellationToken ct = default) + { + throw new NotImplementedException(); + } - public Task ExternalLoginAsync(ExternalLoginRequest request, CancellationToken ct = default) - { - throw new NotImplementedException(); - } + public Task CompleteMfaAsync(CompleteMfaRequest request, CancellationToken ct = default) + { + throw new NotImplementedException(); + } - public Task LoginAsync(AuthFlowContext flow, LoginRequest request, CancellationToken ct = default) - { - return _loginOrchestrator.LoginAsync(flow, request, ct); - } + public Task ExternalLoginAsync(ExternalLoginRequest request, CancellationToken ct = default) + { + throw new NotImplementedException(); + } - public Task LoginAsync(AuthFlowContext flow, AuthExecutionContext execution, LoginRequest request, CancellationToken ct = default) - { - var effectiveFlow = execution.EffectiveClientProfile is null - ? flow - : flow.WithClientProfile((UAuthClientProfile)execution.EffectiveClientProfile); - return _loginOrchestrator.LoginAsync(effectiveFlow, request, ct); - } + public Task LoginAsync(AuthFlowContext flow, LoginRequest request, CancellationToken ct = default) + { + return _loginOrchestrator.LoginAsync(flow, request, ct); + } - public Task LogoutAsync(LogoutRequest request, CancellationToken ct = default) - { - var authFlow = _authFlow.Current; - var now = request.At ?? DateTimeOffset.UtcNow; - var authContext = authFlow.ToAuthContext(now); + public Task LoginAsync(AuthFlowContext flow, AuthExecutionContext execution, LoginRequest request, CancellationToken ct = default) + { + var effectiveFlow = execution.EffectiveClientProfile is null + ? flow + : flow.WithClientProfile((UAuthClientProfile)execution.EffectiveClientProfile); + return _loginOrchestrator.LoginAsync(effectiveFlow, request, ct); + } - return _orchestrator.ExecuteAsync(authContext, new RevokeSessionCommand(request.SessionId), ct); - } + public Task LogoutAsync(LogoutRequest request, CancellationToken ct = default) + { + var authFlow = _authFlow.Current; + var now = request.At ?? DateTimeOffset.UtcNow; + var authContext = authFlow.ToAuthContext(now); - public async Task LogoutAllAsync(LogoutAllRequest request, CancellationToken ct = default) - { - var authFlow = _authFlow.Current; - var now = request.At ?? DateTimeOffset.UtcNow; + return _orchestrator.ExecuteAsync(authContext, new RevokeSessionCommand(request.SessionId), ct); + } - if (authFlow.Session is not SessionSecurityContext session) - throw new InvalidOperationException("LogoutAll requires an active session."); + public async Task LogoutAllAsync(LogoutAllRequest request, CancellationToken ct = default) + { + var authFlow = _authFlow.Current; + var now = request.At ?? DateTimeOffset.UtcNow; - var authContext = authFlow.ToAuthContext(now); - SessionChainId? exceptChainId = null; + if (authFlow.Session is not SessionSecurityContext session) + throw new InvalidOperationException("LogoutAll requires an active session."); - if (request.ExceptCurrent) - { - exceptChainId = session.ChainId; + var authContext = authFlow.ToAuthContext(now); + SessionChainId? exceptChainId = null; - if (exceptChainId is null) - throw new InvalidOperationException("Current session chain could not be resolved."); - } + if (request.ExceptCurrent) + { + exceptChainId = session.ChainId; - if (authFlow.UserKey is UserKey uaKey) - { - var command = new RevokeAllChainsCommand(uaKey, exceptChainId); - await _orchestrator.ExecuteAsync(authContext, command, ct); - } - + if (exceptChainId is null) + throw new InvalidOperationException("Current session chain could not be resolved."); } - public Task ReauthenticateAsync(ReauthRequest request, CancellationToken ct = default) + if (authFlow.UserKey is UserKey uaKey) { - throw new NotImplementedException(); + var command = new RevokeAllChainsCommand(uaKey, exceptChainId); + await _orchestrator.ExecuteAsync(authContext, command, ct); } + } + public Task ReauthenticateAsync(ReauthRequest request, CancellationToken ct = default) + { + throw new NotImplementedException(); } } diff --git a/src/CodeBeam.UltimateAuth.Server/Services/UAuthSessionManager.cs b/src/CodeBeam.UltimateAuth.Server/Services/UAuthSessionManager.cs index 3ac5e5e4..7cd1b428 100644 --- a/src/CodeBeam.UltimateAuth.Server/Services/UAuthSessionManager.cs +++ b/src/CodeBeam.UltimateAuth.Server/Services/UAuthSessionManager.cs @@ -3,7 +3,6 @@ using CodeBeam.UltimateAuth.Server.Auth; using CodeBeam.UltimateAuth.Server.Extensions; using CodeBeam.UltimateAuth.Server.Infrastructure; -using CodeBeam.UltimateAuth.Server.Infrastructure.Orchestrator; // TODO: Add wrapper service in client project. Validate method also may add. namespace CodeBeam.UltimateAuth.Server.Services; diff --git a/src/CodeBeam.UltimateAuth.Server/Stores/AspNetIdentityUserStore.cs b/src/CodeBeam.UltimateAuth.Server/Stores/AspNetIdentityUserStore.cs index ac2fc6f0..51b261f7 100644 --- a/src/CodeBeam.UltimateAuth.Server/Stores/AspNetIdentityUserStore.cs +++ b/src/CodeBeam.UltimateAuth.Server/Stores/AspNetIdentityUserStore.cs @@ -1,38 +1,32 @@ -using CodeBeam.UltimateAuth.Core.Abstractions; -using CodeBeam.UltimateAuth.Core.Domain; -using Microsoft.AspNetCore.Identity; +namespace CodeBeam.UltimateAuth.Server.Stores; -namespace CodeBeam.UltimateAuth.Server.Stores +public sealed class AspNetIdentityUserStore // : IUAuthUserStore { - public sealed class AspNetIdentityUserStore // : IUAuthUserStore - { - //private readonly UserManager _users; + //private readonly UserManager _users; - //public AspNetIdentityUserStore(UserManager users) - //{ - // _users = users; - //} + //public AspNetIdentityUserStore(UserManager users) + //{ + // _users = users; + //} - //public async Task?> FindByUsernameAsync( - // string? tenantId, - // string username, - // CancellationToken cancellationToken = default) - //{ - // var user = await _users.FindByNameAsync(username); - // if (user is null) - // return null; + //public async Task?> FindByUsernameAsync( + // string? tenantId, + // string username, + // CancellationToken cancellationToken = default) + //{ + // var user = await _users.FindByNameAsync(username); + // if (user is null) + // return null; - // var claims = await _users.GetClaimsAsync(user); - - // return new UAuthUserRecord - // { - // UserId = user.Id, - // Username = user.UserName!, - // PasswordHash = user.PasswordHash!, - // Claims = ClaimsSnapshot.From( - // claims.Select(c => (c.Type, c.Value)).ToArray()) - // }; - //} - } + // var claims = await _users.GetClaimsAsync(user); + // return new UAuthUserRecord + // { + // UserId = user.Id, + // Username = user.UserName!, + // PasswordHash = user.PasswordHash!, + // Claims = ClaimsSnapshot.From( + // claims.Select(c => (c.Type, c.Value)).ToArray()) + // }; + //} } diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.Contracts/Requests/AssignRoleRequest.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.Contracts/Requests/AssignRoleRequest.cs index a4bf9285..6afebc1d 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.Contracts/Requests/AssignRoleRequest.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.Contracts/Requests/AssignRoleRequest.cs @@ -1,7 +1,6 @@ -namespace CodeBeam.UltimateAuth.Authorization.Contracts +namespace CodeBeam.UltimateAuth.Authorization.Contracts; + +public sealed class AssignRoleRequest { - public sealed class AssignRoleRequest - { - public required string Role { get; init; } - } + public required string Role { get; init; } } diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.Contracts/Requests/AuthorizationCheckRequest.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.Contracts/Requests/AuthorizationCheckRequest.cs index 078a504f..4a1958d1 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.Contracts/Requests/AuthorizationCheckRequest.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.Contracts/Requests/AuthorizationCheckRequest.cs @@ -1,9 +1,8 @@ -namespace CodeBeam.UltimateAuth.Authorization.Contracts +namespace CodeBeam.UltimateAuth.Authorization.Contracts; + +public sealed class AuthorizationCheckRequest { - public sealed class AuthorizationCheckRequest - { - public required string Action { get; init; } - public string? Resource { get; init; } - public string? ResourceId { get; init; } - } + public required string Action { get; init; } + public string? Resource { get; init; } + public string? ResourceId { get; init; } } diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.Contracts/Responses/UserRolesResponse.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.Contracts/Responses/UserRolesResponse.cs index 17afba35..d345e84e 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.Contracts/Responses/UserRolesResponse.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.Contracts/Responses/UserRolesResponse.cs @@ -1,11 +1,9 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Authorization.Contracts -{ - public sealed record UserRolesResponse - { - public required UserKey UserKey { get; init; } - public required IReadOnlyCollection Roles { get; init; } - } +namespace CodeBeam.UltimateAuth.Authorization.Contracts; +public sealed record UserRolesResponse +{ + public required UserKey UserKey { get; init; } + public required IReadOnlyCollection Roles { get; init; } } diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Extensions/AuthorizationInMemoryExtensions.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Extensions/AuthorizationInMemoryExtensions.cs deleted file mode 100644 index 2e4bd92c..00000000 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Extensions/AuthorizationInMemoryExtensions.cs +++ /dev/null @@ -1,19 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Abstractions; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; - -namespace CodeBeam.UltimateAuth.Authorization.InMemory.Extensions -{ - public static class AuthorizationInMemoryExtensions - { - public static IServiceCollection AddUltimateAuthAuthorizationInMemory(this IServiceCollection services) - { - services.TryAddSingleton(); - - // Never try add - seeding is enumerated and all contributors are added. - services.AddSingleton(); - - return services; - } - } -} diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Extensions/ServiceCollectionExtensions.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..d4698be1 --- /dev/null +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,18 @@ +using CodeBeam.UltimateAuth.Core.Abstractions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace CodeBeam.UltimateAuth.Authorization.InMemory.Extensions; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddUltimateAuthAuthorizationInMemory(this IServiceCollection services) + { + services.TryAddSingleton(); + + // Never try add - seeding is enumerated and all contributors are added. + services.AddSingleton(); + + return services; + } +} diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/IAuthorizationSeeder.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/IAuthorizationSeeder.cs index e7d05618..750a4d66 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/IAuthorizationSeeder.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/IAuthorizationSeeder.cs @@ -1,7 +1,6 @@ -namespace CodeBeam.UltimateAuth.Authorization.InMemory +namespace CodeBeam.UltimateAuth.Authorization.InMemory; + +public interface IAuthorizationSeeder { - public interface IAuthorizationSeeder - { - Task SeedAsync(CancellationToken ct = default); - } + Task SeedAsync(CancellationToken ct = default); } diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/InMemoryAuthorizationSeedContributor.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/InMemoryAuthorizationSeedContributor.cs index 327aacf1..da4efad1 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/InMemoryAuthorizationSeedContributor.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/InMemoryAuthorizationSeedContributor.cs @@ -21,7 +21,6 @@ public InMemoryAuthorizationSeedContributor(IUserRoleStore roles, IInMemoryUserI public async Task SeedAsync(TenantKey tenant, CancellationToken ct = default) { var adminKey = _ids.GetAdminUserId(); - await _roles.AssignAsync(tenant, adminKey, "Admin", ct); } } diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Commands/AssignUserRoleCommand.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Commands/AssignUserRoleCommand.cs index 4f7474a1..52d62db5 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Commands/AssignUserRoleCommand.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Commands/AssignUserRoleCommand.cs @@ -1,22 +1,15 @@ -using CodeBeam.UltimateAuth.Core.Abstractions; -using CodeBeam.UltimateAuth.Core.Contracts; -using CodeBeam.UltimateAuth.Server.Infrastructure; +using CodeBeam.UltimateAuth.Server.Infrastructure; -namespace CodeBeam.UltimateAuth.Authorization.Reference -{ - internal sealed class AssignUserRoleCommand : IAccessCommand - { - private readonly Func _execute; - private readonly IEnumerable _policies; - - public AssignUserRoleCommand(IEnumerable policies, Func execute) - { - _policies = policies; - _execute = execute; - } +namespace CodeBeam.UltimateAuth.Authorization.Reference; - public IEnumerable GetPolicies(AccessContext context) => _policies; +internal sealed class AssignUserRoleCommand : IAccessCommand +{ + private readonly Func _execute; - public Task ExecuteAsync(CancellationToken ct = default) => _execute(ct); + public AssignUserRoleCommand(Func execute) + { + _execute = execute; } + + public Task ExecuteAsync(CancellationToken ct = default) => _execute(ct); } diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Commands/GetUserRolesCommand.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Commands/GetUserRolesCommand.cs index 58c51d2e..a57a1b16 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Commands/GetUserRolesCommand.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Commands/GetUserRolesCommand.cs @@ -1,22 +1,15 @@ -using CodeBeam.UltimateAuth.Core.Abstractions; -using CodeBeam.UltimateAuth.Core.Contracts; -using CodeBeam.UltimateAuth.Server.Infrastructure; +using CodeBeam.UltimateAuth.Server.Infrastructure; -namespace CodeBeam.UltimateAuth.Authorization.Reference -{ - internal sealed class GetUserRolesCommand : IAccessCommand> - { - private readonly IEnumerable _policies; - private readonly Func>> _execute; - - public GetUserRolesCommand(IEnumerable policies, Func>> execute) - { - _policies = policies; - _execute = execute; - } +namespace CodeBeam.UltimateAuth.Authorization.Reference; - public IEnumerable GetPolicies(AccessContext context) => _policies; +internal sealed class GetUserRolesCommand : IAccessCommand> +{ + private readonly Func>> _execute; - public Task> ExecuteAsync(CancellationToken ct = default) => _execute(ct); + public GetUserRolesCommand(Func>> execute) + { + _execute = execute; } + + public Task> ExecuteAsync(CancellationToken ct = default) => _execute(ct); } diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Commands/RemoveUserRoleCommand.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Commands/RemoveUserRoleCommand.cs index d380b76d..35693190 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Commands/RemoveUserRoleCommand.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Commands/RemoveUserRoleCommand.cs @@ -1,22 +1,15 @@ -using CodeBeam.UltimateAuth.Core.Abstractions; -using CodeBeam.UltimateAuth.Core.Contracts; -using CodeBeam.UltimateAuth.Server.Infrastructure; +using CodeBeam.UltimateAuth.Server.Infrastructure; -namespace CodeBeam.UltimateAuth.Authorization.Reference -{ - internal sealed class RemoveUserRoleCommand : IAccessCommand - { - private readonly Func _execute; - private readonly IEnumerable _policies; - - public RemoveUserRoleCommand(IEnumerable policies, Func execute) - { - _policies = policies; - _execute = execute; - } +namespace CodeBeam.UltimateAuth.Authorization.Reference; - public IEnumerable GetPolicies(AccessContext context) => _policies; +internal sealed class RemoveUserRoleCommand : IAccessCommand +{ + private readonly Func _execute; - public Task ExecuteAsync(CancellationToken ct = default) => _execute(ct); + public RemoveUserRoleCommand(Func execute) + { + _execute = execute; } + + public Task ExecuteAsync(CancellationToken ct = default) => _execute(ct); } diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Endpoints/AuthorizationEndpointHandler.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Endpoints/AuthorizationEndpointHandler.cs new file mode 100644 index 00000000..71d5dd18 --- /dev/null +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Endpoints/AuthorizationEndpointHandler.cs @@ -0,0 +1,131 @@ +using CodeBeam.UltimateAuth.Authorization.Contracts; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Server.Auth; +using CodeBeam.UltimateAuth.Server.Defaults; +using CodeBeam.UltimateAuth.Server.Endpoints; +using CodeBeam.UltimateAuth.Server.Extensions; +using Microsoft.AspNetCore.Http; + +namespace CodeBeam.UltimateAuth.Authorization.Reference; + +public sealed class AuthorizationEndpointHandler : IAuthorizationEndpointHandler +{ + private readonly IAuthFlowContextAccessor _authFlow; + private readonly IAuthorizationService _authorization; + private readonly IUserRoleService _roles; + private readonly IAccessContextFactory _accessContextFactory; + + public AuthorizationEndpointHandler(IAuthFlowContextAccessor authFlow, IAuthorizationService authorization, IUserRoleService roles, IAccessContextFactory accessContextFactory) + { + _authFlow = authFlow; + _authorization = authorization; + _roles = roles; + _accessContextFactory = accessContextFactory; + } + + public async Task CheckAsync(HttpContext ctx) + { + var flow = _authFlow.Current; + if (!flow.IsAuthenticated) + return Results.Unauthorized(); + + var req = await ctx.ReadJsonAsync(ctx.RequestAborted); + + var accessContext = await _accessContextFactory.CreateAsync( + flow, + action: req.Action, + resource: req.Resource, + resourceId: req.ResourceId + ); + + var result = await _authorization.AuthorizeAsync(accessContext, ctx.RequestAborted); + + if (result.RequiresReauthentication) + return Results.StatusCode(StatusCodes.Status428PreconditionRequired); + + return result.IsAllowed + ? Results.Ok(result) + : Results.Forbid(); + } + + public async Task GetMyRolesAsync(HttpContext ctx) + { + var flow = _authFlow.Current; + if (!flow.IsAuthenticated) + return Results.Unauthorized(); + var accessContext = await _accessContextFactory.CreateAsync( + flow, + action: UAuthActions.Authorization.Roles.ReadSelf, + resource: "authorization.roles", + resourceId: flow.UserKey!.Value + ); + + var roles = await _roles.GetRolesAsync(accessContext, flow.UserKey!.Value, ctx.RequestAborted); + return Results.Ok(new UserRolesResponse + { + UserKey = flow.UserKey!.Value, + Roles = roles + }); + + } + + public async Task GetUserRolesAsync(UserKey userKey, HttpContext ctx) + { + var flow = _authFlow.Current; + if (!flow.IsAuthenticated) + return Results.Unauthorized(); + + var accessContext = await _accessContextFactory.CreateAsync( + flow, + action: UAuthActions.Authorization.Roles.ReadAdmin, + resource: "authorization.roles", + resourceId: userKey.Value + ); + + var roles = await _roles.GetRolesAsync(accessContext, userKey, ctx.RequestAborted); + + return Results.Ok(new UserRolesResponse + { + UserKey = userKey, + Roles = roles + }); + } + + public async Task AssignRoleAsync(UserKey userKey, HttpContext ctx) + { + var flow = _authFlow.Current; + if (!flow.IsAuthenticated) + return Results.Unauthorized(); + + var req = await ctx.ReadJsonAsync(ctx.RequestAborted); + + var accessContext = await _accessContextFactory.CreateAsync( + flow, + action: UAuthActions.Authorization.Roles.AssignAdmin, + resource: "authorization.roles", + resourceId: userKey.Value + ); + + await _roles.AssignAsync(accessContext, userKey, req.Role, ctx.RequestAborted); + return Results.Ok(); + } + + public async Task RemoveRoleAsync(UserKey userKey, HttpContext ctx) + { + var flow = _authFlow.Current; + if (!flow.IsAuthenticated) + return Results.Unauthorized(); + + var req = await ctx.ReadJsonAsync(ctx.RequestAborted); + + var accessContext = await _accessContextFactory.CreateAsync( + flow, + action: UAuthActions.Authorization.Roles.RemoveAdmin, + resource: "authorization.roles", + resourceId: userKey.Value + ); + + await _roles.RemoveAsync(accessContext, userKey, req.Role, ctx.RequestAborted); + return Results.Ok(); + } +} diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Endpoints/DefaultAuthorizationEndpointHandler.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Endpoints/DefaultAuthorizationEndpointHandler.cs deleted file mode 100644 index 99f8258b..00000000 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Endpoints/DefaultAuthorizationEndpointHandler.cs +++ /dev/null @@ -1,132 +0,0 @@ -using CodeBeam.UltimateAuth.Authorization.Contracts; -using CodeBeam.UltimateAuth.Core.Domain; -using CodeBeam.UltimateAuth.Server.Auth; -using CodeBeam.UltimateAuth.Server.Defaults; -using CodeBeam.UltimateAuth.Server.Endpoints; -using CodeBeam.UltimateAuth.Server.Extensions; -using Microsoft.AspNetCore.Http; - -namespace CodeBeam.UltimateAuth.Authorization.Reference -{ - public sealed class DefaultAuthorizationEndpointHandler : IAuthorizationEndpointHandler - { - private readonly IAuthFlowContextAccessor _authFlow; - private readonly IAuthorizationService _authorization; - private readonly IUserRoleService _roles; - private readonly IAccessContextFactory _accessContextFactory; - - public DefaultAuthorizationEndpointHandler(IAuthFlowContextAccessor authFlow, IAuthorizationService authorization, IUserRoleService roles, IAccessContextFactory accessContextFactory) - { - _authFlow = authFlow; - _authorization = authorization; - _roles = roles; - _accessContextFactory = accessContextFactory; - } - - public async Task CheckAsync(HttpContext ctx) - { - var flow = _authFlow.Current; - if (!flow.IsAuthenticated) - return Results.Unauthorized(); - - var req = await ctx.ReadJsonAsync(ctx.RequestAborted); - - var accessContext = await _accessContextFactory.CreateAsync( - flow, - action: req.Action, - resource: req.Resource, - resourceId: req.ResourceId - ); - - var result = await _authorization.AuthorizeAsync(accessContext, ctx.RequestAborted); - - if (result.RequiresReauthentication) - return Results.StatusCode(StatusCodes.Status428PreconditionRequired); - - return result.IsAllowed - ? Results.Ok(result) - : Results.Forbid(); - } - - public async Task GetMyRolesAsync(HttpContext ctx) - { - var flow = _authFlow.Current; - if (!flow.IsAuthenticated) - return Results.Unauthorized(); - var accessContext = await _accessContextFactory.CreateAsync( - flow, - action: UAuthActions.Authorization.Roles.ReadSelf, - resource: "authorization.roles", - resourceId: flow.UserKey!.Value - ); - - var roles = await _roles.GetRolesAsync(accessContext, flow.UserKey!.Value, ctx.RequestAborted); - return Results.Ok(new UserRolesResponse - { - UserKey = flow.UserKey!.Value, - Roles = roles - }); - - } - - public async Task GetUserRolesAsync(UserKey userKey, HttpContext ctx) - { - var flow = _authFlow.Current; - if (!flow.IsAuthenticated) - return Results.Unauthorized(); - - var accessContext = await _accessContextFactory.CreateAsync( - flow, - action: UAuthActions.Authorization.Roles.ReadAdmin, - resource: "authorization.roles", - resourceId: userKey.Value - ); - - var roles = await _roles.GetRolesAsync(accessContext, userKey, ctx.RequestAborted); - - return Results.Ok(new UserRolesResponse - { - UserKey = userKey, - Roles = roles - }); - } - - public async Task AssignRoleAsync(UserKey userKey, HttpContext ctx) - { - var flow = _authFlow.Current; - if (!flow.IsAuthenticated) - return Results.Unauthorized(); - - var req = await ctx.ReadJsonAsync(ctx.RequestAborted); - - var accessContext = await _accessContextFactory.CreateAsync( - flow, - action: UAuthActions.Authorization.Roles.AssignAdmin, - resource: "authorization.roles", - resourceId: userKey.Value - ); - - await _roles.AssignAsync(accessContext, userKey, req.Role, ctx.RequestAborted); - return Results.Ok(); - } - - public async Task RemoveRoleAsync(UserKey userKey, HttpContext ctx) - { - var flow = _authFlow.Current; - if (!flow.IsAuthenticated) - return Results.Unauthorized(); - - var req = await ctx.ReadJsonAsync(ctx.RequestAborted); - - var accessContext = await _accessContextFactory.CreateAsync( - flow, - action: UAuthActions.Authorization.Roles.RemoveAdmin, - resource: "authorization.roles", - resourceId: userKey.Value - ); - - await _roles.RemoveAsync(accessContext, userKey, req.Role, ctx.RequestAborted); - return Results.Ok(); - } - } -} diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Extensions/AuthorizationReferenceExtensions.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Extensions/AuthorizationReferenceExtensions.cs deleted file mode 100644 index 8e49f8ca..00000000 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Extensions/AuthorizationReferenceExtensions.cs +++ /dev/null @@ -1,21 +0,0 @@ -using CodeBeam.UltimateAuth.Server.Endpoints; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; - -namespace CodeBeam.UltimateAuth.Authorization.Reference.Extensions -{ - public static class AuthorizationReferenceExtensions - { - public static IServiceCollection AddUltimateAuthAuthorizationReference(this IServiceCollection services) - { - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); - - return services; - } - } - -} diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Extensions/ServiceCollectionExtensions.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..3142d271 --- /dev/null +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,19 @@ +using CodeBeam.UltimateAuth.Server.Endpoints; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace CodeBeam.UltimateAuth.Authorization.Reference.Extensions; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddUltimateAuthAuthorizationReference(this IServiceCollection services) + { + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + + return services; + } +} diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Infrastructure/DefaultRolePermissionResolver.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Infrastructure/DefaultRolePermissionResolver.cs deleted file mode 100644 index bd1ef3c1..00000000 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Infrastructure/DefaultRolePermissionResolver.cs +++ /dev/null @@ -1,36 +0,0 @@ -using CodeBeam.UltimateAuth.Authorization.Domain; -using CodeBeam.UltimateAuth.Core.MultiTenancy; - -namespace CodeBeam.UltimateAuth.Authorization.Reference -{ - public sealed class DefaultRolePermissionResolver : IRolePermissionResolver - { - private static readonly IReadOnlyDictionary _map - = new Dictionary(StringComparer.OrdinalIgnoreCase) - { - ["admin"] = new[] - { - new Permission("*") - }, - ["user"] = new[] - { - new Permission("profile.read"), - new Permission("profile.update") - } - }; - - public Task> ResolveAsync(TenantKey tenant, IEnumerable roles, CancellationToken ct = default) - { - var result = new List(); - - foreach (var role in roles) - { - if (_map.TryGetValue(role, out var perms)) - result.AddRange(perms); - } - - return Task.FromResult>(result); - } - } - -} diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Infrastructure/DefaultUserPermissionStore.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Infrastructure/DefaultUserPermissionStore.cs deleted file mode 100644 index 6a44dc7a..00000000 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Infrastructure/DefaultUserPermissionStore.cs +++ /dev/null @@ -1,25 +0,0 @@ -using CodeBeam.UltimateAuth.Authorization.Domain; -using CodeBeam.UltimateAuth.Core.Domain; -using CodeBeam.UltimateAuth.Core.MultiTenancy; - -namespace CodeBeam.UltimateAuth.Authorization.Reference -{ - public sealed class DefaultUserPermissionStore : IUserPermissionStore - { - private readonly IUserRoleStore _roles; - private readonly IRolePermissionResolver _resolver; - - public DefaultUserPermissionStore(IUserRoleStore roles, IRolePermissionResolver resolver) - { - _roles = roles; - _resolver = resolver; - } - - public async Task> GetPermissionsAsync(TenantKey tenant, UserKey userKey, CancellationToken ct = default) - { - var roles = await _roles.GetRolesAsync(tenant, userKey, ct); - return await _resolver.ResolveAsync(tenant, roles, ct); - } - } - -} diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Infrastructure/RolePermissionResolver.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Infrastructure/RolePermissionResolver.cs new file mode 100644 index 00000000..4f5d5a7b --- /dev/null +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Infrastructure/RolePermissionResolver.cs @@ -0,0 +1,34 @@ +using CodeBeam.UltimateAuth.Authorization.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; + +namespace CodeBeam.UltimateAuth.Authorization.Reference; + +public sealed class RolePermissionResolver : IRolePermissionResolver +{ + private static readonly IReadOnlyDictionary _map + = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["admin"] = new[] + { + new Permission("*") + }, + ["user"] = new[] + { + new Permission("profile.read"), + new Permission("profile.update") + } + }; + + public Task> ResolveAsync(TenantKey tenant, IEnumerable roles, CancellationToken ct = default) + { + var result = new List(); + + foreach (var role in roles) + { + if (_map.TryGetValue(role, out var perms)) + result.AddRange(perms); + } + + return Task.FromResult>(result); + } +} diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Infrastructure/UserPermissionStore.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Infrastructure/UserPermissionStore.cs new file mode 100644 index 00000000..150eae91 --- /dev/null +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Infrastructure/UserPermissionStore.cs @@ -0,0 +1,23 @@ +using CodeBeam.UltimateAuth.Authorization.Domain; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; + +namespace CodeBeam.UltimateAuth.Authorization.Reference; + +public sealed class UserPermissionStore : IUserPermissionStore +{ + private readonly IUserRoleStore _roles; + private readonly IRolePermissionResolver _resolver; + + public UserPermissionStore(IUserRoleStore roles, IRolePermissionResolver resolver) + { + _roles = roles; + _resolver = resolver; + } + + public async Task> GetPermissionsAsync(TenantKey tenant, UserKey userKey, CancellationToken ct = default) + { + var roles = await _roles.GetRolesAsync(tenant, userKey, ct); + return await _resolver.ResolveAsync(tenant, roles, ct); + } +} diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Services/AuthorizationService.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Services/AuthorizationService.cs new file mode 100644 index 00000000..3b01d601 --- /dev/null +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Services/AuthorizationService.cs @@ -0,0 +1,36 @@ +using CodeBeam.UltimateAuth.Authorization.Contracts; +using CodeBeam.UltimateAuth.Core.Abstractions; +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Policies.Abstractions; + +namespace CodeBeam.UltimateAuth.Authorization.Reference; + +internal sealed class AuthorizationService : IAuthorizationService +{ + private readonly IAccessPolicyProvider _policyProvider; + private readonly IAccessAuthority _accessAuthority; + + public AuthorizationService(IAccessPolicyProvider policyProvider, IAccessAuthority accessAuthority) + { + _policyProvider = policyProvider; + _accessAuthority = accessAuthority; + } + + public Task AuthorizeAsync(AccessContext context, CancellationToken ct = default) + { + ct.ThrowIfCancellationRequested(); + + var policies = _policyProvider.GetPolicies(context); + var decision = _accessAuthority.Decide(context, policies); + + if (decision.RequiresReauthentication) + return Task.FromResult(AuthorizationResult.ReauthRequired()); + + return Task.FromResult( + decision.IsAllowed + ? AuthorizationResult.Allow() + : AuthorizationResult.Deny(decision.DenyReason) + ); + } + +} diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Services/DefaultAuthorizationService.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Services/DefaultAuthorizationService.cs deleted file mode 100644 index 78acfa89..00000000 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Services/DefaultAuthorizationService.cs +++ /dev/null @@ -1,37 +0,0 @@ -using CodeBeam.UltimateAuth.Authorization.Contracts; -using CodeBeam.UltimateAuth.Core.Abstractions; -using CodeBeam.UltimateAuth.Core.Contracts; -using CodeBeam.UltimateAuth.Policies.Abstractions; - -namespace CodeBeam.UltimateAuth.Authorization.Reference -{ - internal sealed class DefaultAuthorizationService : IAuthorizationService - { - private readonly IAccessPolicyProvider _policyProvider; - private readonly IAccessAuthority _accessAuthority; - - public DefaultAuthorizationService(IAccessPolicyProvider policyProvider, IAccessAuthority accessAuthority) - { - _policyProvider = policyProvider; - _accessAuthority = accessAuthority; - } - - public Task AuthorizeAsync(AccessContext context, CancellationToken ct = default) - { - ct.ThrowIfCancellationRequested(); - - var policies = _policyProvider.GetPolicies(context); - var decision = _accessAuthority.Decide(context, policies); - - if (decision.RequiresReauthentication) - return Task.FromResult(AuthorizationResult.ReauthRequired()); - - return Task.FromResult( - decision.IsAllowed - ? AuthorizationResult.Allow() - : AuthorizationResult.Deny(decision.DenyReason) - ); - } - - } -} diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Services/DefaultUserRoleService.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Services/DefaultUserRoleService.cs deleted file mode 100644 index 1dc54560..00000000 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Services/DefaultUserRoleService.cs +++ /dev/null @@ -1,62 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Abstractions; -using CodeBeam.UltimateAuth.Core.Contracts; -using CodeBeam.UltimateAuth.Core.Domain; -using CodeBeam.UltimateAuth.Server.Infrastructure; - -namespace CodeBeam.UltimateAuth.Authorization.Reference -{ - internal sealed class DefaultUserRoleService : IUserRoleService - { - private readonly IAccessOrchestrator _accessOrchestrator; - private readonly IUserRoleStore _store; - - public DefaultUserRoleService(IAccessOrchestrator accessOrchestrator, IUserRoleStore store) - { - _accessOrchestrator = accessOrchestrator; - _store = store; - } - - public async Task AssignAsync(AccessContext context, UserKey targetUserKey, string role, CancellationToken ct = default) - { - ct.ThrowIfCancellationRequested(); - - if (string.IsNullOrWhiteSpace(role)) - throw new ArgumentException("role_empty", nameof(role)); - - var cmd = new AssignUserRoleCommand(Array.Empty(), - async innerCt => - { - await _store.AssignAsync(context.ResourceTenant, targetUserKey, role, innerCt); - }); - - await _accessOrchestrator.ExecuteAsync(context, cmd, ct); - } - - public async Task RemoveAsync(AccessContext context, UserKey targetUserKey, string role, CancellationToken ct = default) - { - ct.ThrowIfCancellationRequested(); - - if (string.IsNullOrWhiteSpace(role)) - throw new ArgumentException("role_empty", nameof(role)); - - var cmd = new RemoveUserRoleCommand(Array.Empty(), - async innerCt => - { - await _store.RemoveAsync(context.ResourceTenant, targetUserKey, role, innerCt); - }); - - await _accessOrchestrator.ExecuteAsync(context, cmd, ct); - } - - - public async Task> GetRolesAsync(AccessContext context, UserKey targetUserKey, CancellationToken ct = default) - { - ct.ThrowIfCancellationRequested(); - - var cmd = new GetUserRolesCommand(Array.Empty(), - innerCt => _store.GetRolesAsync(context.ResourceTenant, targetUserKey, innerCt)); - - return await _accessOrchestrator.ExecuteAsync(context, cmd, ct); - } - } -} diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Services/IAuthorizationService.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Services/IAuthorizationService.cs index a3de9c7f..35d09545 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Services/IAuthorizationService.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Services/IAuthorizationService.cs @@ -1,11 +1,9 @@ using CodeBeam.UltimateAuth.Authorization.Contracts; using CodeBeam.UltimateAuth.Core.Contracts; -namespace CodeBeam.UltimateAuth.Authorization.Reference -{ - public interface IAuthorizationService - { - Task AuthorizeAsync(AccessContext context, CancellationToken ct = default); - } +namespace CodeBeam.UltimateAuth.Authorization.Reference; +public interface IAuthorizationService +{ + Task AuthorizeAsync(AccessContext context, CancellationToken ct = default); } diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Services/UserRoleService.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Services/UserRoleService.cs new file mode 100644 index 00000000..1def14bf --- /dev/null +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Services/UserRoleService.cs @@ -0,0 +1,59 @@ +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Server.Infrastructure; + +namespace CodeBeam.UltimateAuth.Authorization.Reference; + +internal sealed class UserRoleService : IUserRoleService +{ + private readonly IAccessOrchestrator _accessOrchestrator; + private readonly IUserRoleStore _store; + + public UserRoleService(IAccessOrchestrator accessOrchestrator, IUserRoleStore store) + { + _accessOrchestrator = accessOrchestrator; + _store = store; + } + + public async Task AssignAsync(AccessContext context, UserKey targetUserKey, string role, CancellationToken ct = default) + { + ct.ThrowIfCancellationRequested(); + + if (string.IsNullOrWhiteSpace(role)) + throw new ArgumentException("role_empty", nameof(role)); + + var cmd = new AssignUserRoleCommand( + async innerCt => + { + await _store.AssignAsync(context.ResourceTenant, targetUserKey, role, innerCt); + }); + + await _accessOrchestrator.ExecuteAsync(context, cmd, ct); + } + + public async Task RemoveAsync(AccessContext context, UserKey targetUserKey, string role, CancellationToken ct = default) + { + ct.ThrowIfCancellationRequested(); + + if (string.IsNullOrWhiteSpace(role)) + throw new ArgumentException("role_empty", nameof(role)); + + var cmd = new RemoveUserRoleCommand( + async innerCt => + { + await _store.RemoveAsync(context.ResourceTenant, targetUserKey, role, innerCt); + }); + + await _accessOrchestrator.ExecuteAsync(context, cmd, ct); + } + + + public async Task> GetRolesAsync(AccessContext context, UserKey targetUserKey, CancellationToken ct = default) + { + ct.ThrowIfCancellationRequested(); + + var cmd = new GetUserRolesCommand(innerCt => _store.GetRolesAsync(context.ResourceTenant, targetUserKey, innerCt)); + + return await _accessOrchestrator.ExecuteAsync(context, cmd, ct); + } +} diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization/Abstractions/IUserRoleService.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization/Abstractions/IUserRoleService.cs index b335d236..3c9a4f29 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization/Abstractions/IUserRoleService.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization/Abstractions/IUserRoleService.cs @@ -1,12 +1,11 @@ using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Authorization +namespace CodeBeam.UltimateAuth.Authorization; + +public interface IUserRoleService { - public interface IUserRoleService - { - Task AssignAsync(AccessContext context, UserKey targetUserKey, string role, CancellationToken ct = default); - Task RemoveAsync(AccessContext context, UserKey targetUserKey, string role, CancellationToken ct = default); - Task> GetRolesAsync(AccessContext context, UserKey targetUserKey, CancellationToken ct = default); - } + Task AssignAsync(AccessContext context, UserKey targetUserKey, string role, CancellationToken ct = default); + Task RemoveAsync(AccessContext context, UserKey targetUserKey, string role, CancellationToken ct = default); + Task> GetRolesAsync(AccessContext context, UserKey targetUserKey, CancellationToken ct = default); } diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization/AuthorizationClaimsProvider.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization/AuthorizationClaimsProvider.cs new file mode 100644 index 00000000..99fbd3a5 --- /dev/null +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization/AuthorizationClaimsProvider.cs @@ -0,0 +1,34 @@ +using CodeBeam.UltimateAuth.Core; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; +using System.Security.Claims; + +namespace CodeBeam.UltimateAuth.Authorization; + +public sealed class AuthorizationClaimsProvider : IUserClaimsProvider +{ + private readonly IUserRoleStore _roles; + private readonly IUserPermissionStore _permissions; + + public AuthorizationClaimsProvider(IUserRoleStore roles, IUserPermissionStore permissions) + { + _roles = roles; + _permissions = permissions; + } + + public async Task GetClaimsAsync(TenantKey tenant, UserKey userKey, CancellationToken ct = default) + { + var roles = await _roles.GetRolesAsync(tenant, userKey, ct); + var perms = await _permissions.GetPermissionsAsync(tenant, userKey, ct); + + var builder = ClaimsSnapshot.Create(); + + foreach (var role in roles) + builder.Add(ClaimTypes.Role, role); + + foreach (var perm in perms) + builder.Add("uauth:permission", perm.Value); + + return builder.Build(); + } +} diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization/DefaultAuthorizationClaimsProvider.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization/DefaultAuthorizationClaimsProvider.cs deleted file mode 100644 index 3e0507ea..00000000 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization/DefaultAuthorizationClaimsProvider.cs +++ /dev/null @@ -1,36 +0,0 @@ -using CodeBeam.UltimateAuth.Core; -using CodeBeam.UltimateAuth.Core.Domain; -using CodeBeam.UltimateAuth.Core.MultiTenancy; -using System.Security.Claims; - -namespace CodeBeam.UltimateAuth.Authorization -{ - public sealed class DefaultAuthorizationClaimsProvider : IUserClaimsProvider - { - private readonly IUserRoleStore _roles; - private readonly IUserPermissionStore _permissions; - - public DefaultAuthorizationClaimsProvider(IUserRoleStore roles, IUserPermissionStore permissions) - { - _roles = roles; - _permissions = permissions; - } - - public async Task GetClaimsAsync(TenantKey tenant, UserKey userKey, CancellationToken ct = default) - { - var roles = await _roles.GetRolesAsync(tenant, userKey, ct); - var perms = await _permissions.GetPermissionsAsync(tenant, userKey, ct); - - var builder = ClaimsSnapshot.Create(); - - foreach (var role in roles) - builder.Add(ClaimTypes.Role, role); - - foreach (var perm in perms) - builder.Add("uauth:permission", perm.Value); - - return builder.Build(); - } - } - -} diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Endpoints/DefaultCredentialEndpointHandler.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Endpoints/CredentialEndpointHandler.cs similarity index 97% rename from src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Endpoints/DefaultCredentialEndpointHandler.cs rename to src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Endpoints/CredentialEndpointHandler.cs index 259ea480..10b8092f 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Endpoints/DefaultCredentialEndpointHandler.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Endpoints/CredentialEndpointHandler.cs @@ -8,13 +8,13 @@ namespace CodeBeam.UltimateAuth.Credentials.Reference; -public sealed class DefaultCredentialEndpointHandler : ICredentialEndpointHandler +public sealed class CredentialEndpointHandler : ICredentialEndpointHandler { private readonly IAuthFlowContextAccessor _authFlow; private readonly IAccessContextFactory _accessContextFactory; private readonly IUserCredentialsService _credentials; - public DefaultCredentialEndpointHandler(IAuthFlowContextAccessor authFlow, IAccessContextFactory accessContextFactory, IUserCredentialsService credentials) + public CredentialEndpointHandler(IAuthFlowContextAccessor authFlow, IAccessContextFactory accessContextFactory, IUserCredentialsService credentials) { _authFlow = authFlow; _accessContextFactory = accessContextFactory; diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Extensions/ServiceCollectionExtensions.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Extensions/ServiceCollectionExtensions.cs index 0f191c3f..466afb4c 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Extensions/ServiceCollectionExtensions.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Extensions/ServiceCollectionExtensions.cs @@ -10,9 +10,9 @@ public static class ServiceCollectionExtensions { public static IServiceCollection AddUltimateAuthCredentialsReference(this IServiceCollection services) { - services.TryAddScoped(); - services.TryAddScoped(); - services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); services.TryAddScoped(); return services; diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Services/DefaultUserCredentialsService.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Services/UserCredentialsService.cs similarity index 98% rename from src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Services/DefaultUserCredentialsService.cs rename to src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Services/UserCredentialsService.cs index ec0ec362..02a12085 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Services/DefaultUserCredentialsService.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Services/UserCredentialsService.cs @@ -8,7 +8,7 @@ namespace CodeBeam.UltimateAuth.Credentials.Reference; -internal sealed class DefaultUserCredentialsService : IUserCredentialsService, IUserCredentialsInternalService +internal sealed class UserCredentialsService : IUserCredentialsService, IUserCredentialsInternalService { private readonly IAccessOrchestrator _accessOrchestrator; private readonly ICredentialStore _credentials; @@ -16,7 +16,7 @@ internal sealed class DefaultUserCredentialsService : IUserCredentialsService, I private readonly IUAuthPasswordHasher _hasher; private readonly IClock _clock; - public DefaultUserCredentialsService( + public UserCredentialsService( IAccessOrchestrator accessOrchestrator, ICredentialStore credentials, ICredentialSecretStore secrets, diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials/Infrastructure/DefaultCredentialValidator.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials/Infrastructure/CredentialValidator.cs similarity index 88% rename from src/credentials/CodeBeam.UltimateAuth.Credentials/Infrastructure/DefaultCredentialValidator.cs rename to src/credentials/CodeBeam.UltimateAuth.Credentials/Infrastructure/CredentialValidator.cs index 4416aef0..4071d4a7 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials/Infrastructure/DefaultCredentialValidator.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials/Infrastructure/CredentialValidator.cs @@ -3,12 +3,12 @@ namespace CodeBeam.UltimateAuth.Credentials; -public sealed class DefaultCredentialValidator : ICredentialValidator +public sealed class CredentialValidator : ICredentialValidator { private readonly IUAuthPasswordHasher _passwordHasher; private readonly IClock _clock; - public DefaultCredentialValidator(IUAuthPasswordHasher passwordHasher, IClock clock) + public CredentialValidator(IUAuthPasswordHasher passwordHasher, IClock clock) { _passwordHasher = passwordHasher; _clock = clock; diff --git a/src/security/CodeBeam.UltimateAuth.Security.Argon2/Argon2Options.cs b/src/security/CodeBeam.UltimateAuth.Security.Argon2/Argon2Options.cs index fc8c324d..4f73b643 100644 --- a/src/security/CodeBeam.UltimateAuth.Security.Argon2/Argon2Options.cs +++ b/src/security/CodeBeam.UltimateAuth.Security.Argon2/Argon2Options.cs @@ -1,13 +1,12 @@ -namespace CodeBeam.UltimateAuth.Security.Argon2 +namespace CodeBeam.UltimateAuth.Security.Argon2; + +public sealed class Argon2Options { - public sealed class Argon2Options - { - // OWASP recommended baseline - public int MemorySizeKb { get; init; } = 64 * 1024; // 64 MB - public int Iterations { get; init; } = 3; - public int Parallelism { get; init; } = Environment.ProcessorCount; + // OWASP recommended baseline + public int MemorySizeKb { get; init; } = 64 * 1024; // 64 MB + public int Iterations { get; init; } = 3; + public int Parallelism { get; init; } = Environment.ProcessorCount; - public int SaltSize { get; init; } = 16; - public int HashSize { get; init; } = 32; - } + public int SaltSize { get; init; } = 16; + public int HashSize { get; init; } = 32; } diff --git a/src/security/CodeBeam.UltimateAuth.Security.Argon2/Argon2PasswordHasher.cs b/src/security/CodeBeam.UltimateAuth.Security.Argon2/Argon2PasswordHasher.cs index 6ddf7d4a..c0d2dca4 100644 --- a/src/security/CodeBeam.UltimateAuth.Security.Argon2/Argon2PasswordHasher.cs +++ b/src/security/CodeBeam.UltimateAuth.Security.Argon2/Argon2PasswordHasher.cs @@ -3,60 +3,59 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using Konscious.Security.Cryptography; -namespace CodeBeam.UltimateAuth.Security.Argon2 +namespace CodeBeam.UltimateAuth.Security.Argon2; + +public sealed class Argon2PasswordHasher : IUAuthPasswordHasher { - public sealed class Argon2PasswordHasher : IUAuthPasswordHasher - { - private readonly Argon2Options _options; + private readonly Argon2Options _options; - public Argon2PasswordHasher(Argon2Options options) - { - _options = options; - } + public Argon2PasswordHasher(Argon2Options options) + { + _options = options; + } - public string Hash(string password) - { - if (string.IsNullOrEmpty(password)) - throw new ArgumentException("Password cannot be null or empty.", nameof(password)); + public string Hash(string password) + { + if (string.IsNullOrEmpty(password)) + throw new ArgumentException("Password cannot be null or empty.", nameof(password)); - var salt = RandomNumberGenerator.GetBytes(_options.SaltSize); + var salt = RandomNumberGenerator.GetBytes(_options.SaltSize); - var argon2 = CreateArgon2(password, salt); + var argon2 = CreateArgon2(password, salt); - var hash = argon2.GetBytes(_options.HashSize); + var hash = argon2.GetBytes(_options.HashSize); - // format: - // {salt}.{hash} - return $"{Convert.ToBase64String(salt)}.{Convert.ToBase64String(hash)}"; - } + // format: + // {salt}.{hash} + return $"{Convert.ToBase64String(salt)}.{Convert.ToBase64String(hash)}"; + } - public bool Verify(string hash, string secret) - { - if (string.IsNullOrWhiteSpace(secret) || string.IsNullOrWhiteSpace(hash)) - return false; + public bool Verify(string hash, string secret) + { + if (string.IsNullOrWhiteSpace(secret) || string.IsNullOrWhiteSpace(hash)) + return false; - var parts = hash.Split('.'); - if (parts.Length != 2) - return false; + var parts = hash.Split('.'); + if (parts.Length != 2) + return false; - var salt = Convert.FromBase64String(parts[0]); - var expectedHash = Convert.FromBase64String(parts[1]); + var salt = Convert.FromBase64String(parts[0]); + var expectedHash = Convert.FromBase64String(parts[1]); - var argon2 = CreateArgon2(secret, salt); - var actualHash = argon2.GetBytes(expectedHash.Length); + var argon2 = CreateArgon2(secret, salt); + var actualHash = argon2.GetBytes(expectedHash.Length); - return CryptographicOperations.FixedTimeEquals(actualHash, expectedHash); - } + return CryptographicOperations.FixedTimeEquals(actualHash, expectedHash); + } - private Argon2id CreateArgon2(string password, byte[] salt) + private Argon2id CreateArgon2(string password, byte[] salt) + { + return new Argon2id(Encoding.UTF8.GetBytes(password)) { - return new Argon2id(Encoding.UTF8.GetBytes(password)) - { - Salt = salt, - DegreeOfParallelism = _options.Parallelism, - Iterations = _options.Iterations, - MemorySize = _options.MemorySizeKb - }; - } + Salt = salt, + DegreeOfParallelism = _options.Parallelism, + Iterations = _options.Iterations, + MemorySize = _options.MemorySizeKb + }; } } diff --git a/src/security/CodeBeam.UltimateAuth.Security.Argon2/ServiceCollectionExtensions.cs b/src/security/CodeBeam.UltimateAuth.Security.Argon2/ServiceCollectionExtensions.cs index 12593d48..2227e6a9 100644 --- a/src/security/CodeBeam.UltimateAuth.Security.Argon2/ServiceCollectionExtensions.cs +++ b/src/security/CodeBeam.UltimateAuth.Security.Argon2/ServiceCollectionExtensions.cs @@ -1,19 +1,18 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using Microsoft.Extensions.DependencyInjection; -namespace CodeBeam.UltimateAuth.Security.Argon2 +namespace CodeBeam.UltimateAuth.Security.Argon2; + +public static class ServiceCollectionExtensions { - public static class ServiceCollectionExtensions + public static IServiceCollection AddUltimateAuthArgon2(this IServiceCollection services, Action? configure = null) { - public static IServiceCollection AddUltimateAuthArgon2(this IServiceCollection services, Action? configure = null) - { - var options = new Argon2Options(); - configure?.Invoke(options); + var options = new Argon2Options(); + configure?.Invoke(options); - services.AddSingleton(options); - services.AddSingleton(); + services.AddSingleton(options); + services.AddSingleton(); - return services; - } + return services; } } diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/EntityProjections/SessionChainProjection.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/EntityProjections/SessionChainProjection.cs index ce08b22d..20358cf6 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/EntityProjections/SessionChainProjection.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/EntityProjections/SessionChainProjection.cs @@ -1,28 +1,27 @@ using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Core.MultiTenancy; -namespace CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore +namespace CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore; + +internal sealed class SessionChainProjection { - internal sealed class SessionChainProjection - { - public long Id { get; set; } + public long Id { get; set; } - public SessionChainId ChainId { get; set; } = default!; - public SessionRootId RootId { get; } + public SessionChainId ChainId { get; set; } = default!; + public SessionRootId RootId { get; } - public TenantKey Tenant { get; set; } - public UserKey UserKey { get; set; } + public TenantKey Tenant { get; set; } + public UserKey UserKey { get; set; } - public int RotationCount { get; set; } - public long SecurityVersionAtCreation { get; set; } + public int RotationCount { get; set; } + public long SecurityVersionAtCreation { get; set; } - public ClaimsSnapshot ClaimsSnapshot { get; set; } = ClaimsSnapshot.Empty; + public ClaimsSnapshot ClaimsSnapshot { get; set; } = ClaimsSnapshot.Empty; - public AuthSessionId? ActiveSessionId { get; set; } + public AuthSessionId? ActiveSessionId { get; set; } - public bool IsRevoked { get; set; } - public DateTimeOffset? RevokedAt { get; set; } + public bool IsRevoked { get; set; } + public DateTimeOffset? RevokedAt { get; set; } - public byte[] RowVersion { get; set; } = default!; - } + public byte[] RowVersion { get; set; } = default!; } diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/EntityProjections/SessionProjection.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/EntityProjections/SessionProjection.cs index 639652c6..285559c6 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/EntityProjections/SessionProjection.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/EntityProjections/SessionProjection.cs @@ -1,32 +1,30 @@ using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Core.MultiTenancy; -namespace CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore -{ - internal sealed class SessionProjection - { - public long Id { get; set; } // EF internal PK +namespace CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore; - public AuthSessionId SessionId { get; set; } = default!; - public SessionChainId ChainId { get; set; } = default!; +internal sealed class SessionProjection +{ + public long Id { get; set; } // EF internal PK - public TenantKey Tenant { get; set; } - public UserKey UserKey { get; set; } = default!; + public AuthSessionId SessionId { get; set; } = default!; + public SessionChainId ChainId { get; set; } = default!; - public DateTimeOffset CreatedAt { get; set; } - public DateTimeOffset ExpiresAt { get; set; } - public DateTimeOffset? LastSeenAt { get; set; } + public TenantKey Tenant { get; set; } + public UserKey UserKey { get; set; } = default!; - public bool IsRevoked { get; set; } - public DateTimeOffset? RevokedAt { get; set; } + public DateTimeOffset CreatedAt { get; set; } + public DateTimeOffset ExpiresAt { get; set; } + public DateTimeOffset? LastSeenAt { get; set; } - public long SecurityVersionAtCreation { get; set; } + public bool IsRevoked { get; set; } + public DateTimeOffset? RevokedAt { get; set; } - public DeviceContext Device { get; set; } - public ClaimsSnapshot Claims { get; set; } = ClaimsSnapshot.Empty; - public SessionMetadata Metadata { get; set; } = SessionMetadata.Empty; + public long SecurityVersionAtCreation { get; set; } - public byte[] RowVersion { get; set; } = default!; - } + public DeviceContext Device { get; set; } + public ClaimsSnapshot Claims { get; set; } = ClaimsSnapshot.Empty; + public SessionMetadata Metadata { get; set; } = SessionMetadata.Empty; + public byte[] RowVersion { get; set; } = default!; } diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/EntityProjections/SessionRootProjection.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/EntityProjections/SessionRootProjection.cs index 0753c61b..05be286e 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/EntityProjections/SessionRootProjection.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/EntityProjections/SessionRootProjection.cs @@ -1,21 +1,20 @@ using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Core.MultiTenancy; -namespace CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore +namespace CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore; + +internal sealed class SessionRootProjection { - internal sealed class SessionRootProjection - { - public long Id { get; set; } - public SessionRootId RootId { get; set; } - public TenantKey Tenant { get; set; } - public UserKey UserKey { get; set; } + public long Id { get; set; } + public SessionRootId RootId { get; set; } + public TenantKey Tenant { get; set; } + public UserKey UserKey { get; set; } - public bool IsRevoked { get; set; } - public DateTimeOffset? RevokedAt { get; set; } + public bool IsRevoked { get; set; } + public DateTimeOffset? RevokedAt { get; set; } - public long SecurityVersion { get; set; } - public DateTimeOffset LastUpdatedAt { get; set; } + public long SecurityVersion { get; set; } + public DateTimeOffset LastUpdatedAt { get; set; } - public byte[] RowVersion { get; set; } = default!; - } + public byte[] RowVersion { get; set; } = default!; } diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs index 7be3e1b2..6f0f3e80 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs @@ -1,5 +1,4 @@ -using CodeBeam.UltimateAuth.Core.Abstractions; -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; namespace CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore; diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Infrastructure/AuthSessionIdEfConverter.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Infrastructure/AuthSessionIdEfConverter.cs index 7aae5ad5..60c6a5b1 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Infrastructure/AuthSessionIdEfConverter.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Infrastructure/AuthSessionIdEfConverter.cs @@ -8,8 +8,7 @@ public static AuthSessionId FromDatabase(string raw) { if (!AuthSessionId.TryCreate(raw, out var id)) { - throw new InvalidOperationException( - $"Invalid AuthSessionId value in database: '{raw}'"); + throw new InvalidOperationException($"Invalid AuthSessionId value in database: '{raw}'"); } return id; @@ -25,8 +24,7 @@ public static string ToDatabase(AuthSessionId id) if (!AuthSessionId.TryCreate(raw, out var id)) { - throw new InvalidOperationException( - $"Invalid AuthSessionId value in database: '{raw}'"); + throw new InvalidOperationException($"Invalid AuthSessionId value in database: '{raw}'"); } return id; diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Infrastructure/JsonValueConverter.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Infrastructure/JsonValueConverter.cs index 0d5b86db..68fb5ff1 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Infrastructure/JsonValueConverter.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Infrastructure/JsonValueConverter.cs @@ -1,15 +1,14 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using System.Text.Json; -namespace CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore +namespace CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore; + +internal sealed class JsonValueConverter : ValueConverter { - internal sealed class JsonValueConverter : ValueConverter + public JsonValueConverter() + : base( + v => JsonSerializer.Serialize(v, (JsonSerializerOptions?)null), + v => JsonSerializer.Deserialize(v, (JsonSerializerOptions?)null)!) { - public JsonValueConverter() - : base( - v => JsonSerializer.Serialize(v, (JsonSerializerOptions?)null), - v => JsonSerializer.Deserialize(v, (JsonSerializerOptions?)null)!) - { - } } } diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Infrastructure/NullableAuthSessionIdConverter.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Infrastructure/NullableAuthSessionIdConverter.cs index 545bce45..099a69cc 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Infrastructure/NullableAuthSessionIdConverter.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Infrastructure/NullableAuthSessionIdConverter.cs @@ -1,19 +1,18 @@ using CodeBeam.UltimateAuth.Core.Domain; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore +namespace CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore; + +internal sealed class AuthSessionIdConverter : ValueConverter { - internal sealed class AuthSessionIdConverter : ValueConverter + public AuthSessionIdConverter() : base(id => AuthSessionIdEfConverter.ToDatabase(id), raw => AuthSessionIdEfConverter.FromDatabase(raw)) { - public AuthSessionIdConverter() : base(id => AuthSessionIdEfConverter.ToDatabase(id), raw => AuthSessionIdEfConverter.FromDatabase(raw)) - { - } } +} - internal sealed class NullableAuthSessionIdConverter : ValueConverter +internal sealed class NullableAuthSessionIdConverter : ValueConverter +{ + public NullableAuthSessionIdConverter() : base(id => AuthSessionIdEfConverter.ToDatabaseNullable(id), raw => AuthSessionIdEfConverter.FromDatabaseNullable(raw)) { - public NullableAuthSessionIdConverter() : base(id => AuthSessionIdEfConverter.ToDatabaseNullable(id), raw => AuthSessionIdEfConverter.FromDatabaseNullable(raw)) - { - } } } diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionChainProjectionMapper.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionChainProjectionMapper.cs index a4cc4951..93c582f4 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionChainProjectionMapper.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionChainProjectionMapper.cs @@ -1,43 +1,42 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore +namespace CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore; + +internal static class SessionChainProjectionMapper { - internal static class SessionChainProjectionMapper + public static UAuthSessionChain ToDomain(this SessionChainProjection p) { - public static UAuthSessionChain ToDomain(this SessionChainProjection p) - { - return UAuthSessionChain.FromProjection( - p.ChainId, - p.RootId, - p.Tenant, - p.UserKey, - p.RotationCount, - p.SecurityVersionAtCreation, - p.ClaimsSnapshot, - p.ActiveSessionId, - p.IsRevoked, - p.RevokedAt - ); - } + return UAuthSessionChain.FromProjection( + p.ChainId, + p.RootId, + p.Tenant, + p.UserKey, + p.RotationCount, + p.SecurityVersionAtCreation, + p.ClaimsSnapshot, + p.ActiveSessionId, + p.IsRevoked, + p.RevokedAt + ); + } - public static SessionChainProjection ToProjection(this UAuthSessionChain chain) + public static SessionChainProjection ToProjection(this UAuthSessionChain chain) + { + return new SessionChainProjection { - return new SessionChainProjection - { - ChainId = chain.ChainId, - Tenant = chain.Tenant, - UserKey = chain.UserKey, - - RotationCount = chain.RotationCount, - SecurityVersionAtCreation = chain.SecurityVersionAtCreation, - ClaimsSnapshot = chain.ClaimsSnapshot, + ChainId = chain.ChainId, + Tenant = chain.Tenant, + UserKey = chain.UserKey, - ActiveSessionId = chain.ActiveSessionId, + RotationCount = chain.RotationCount, + SecurityVersionAtCreation = chain.SecurityVersionAtCreation, + ClaimsSnapshot = chain.ClaimsSnapshot, - IsRevoked = chain.IsRevoked, - RevokedAt = chain.RevokedAt - }; - } + ActiveSessionId = chain.ActiveSessionId, + IsRevoked = chain.IsRevoked, + RevokedAt = chain.RevokedAt + }; } + } diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionProjectionMapper.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionProjectionMapper.cs index 0b5f8c43..da2a7fe8 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionProjectionMapper.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionProjectionMapper.cs @@ -1,50 +1,49 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore +namespace CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore; + +internal static class SessionProjectionMapper { - internal static class SessionProjectionMapper + public static UAuthSession ToDomain(this SessionProjection p) { - public static UAuthSession ToDomain(this SessionProjection p) - { - return UAuthSession.FromProjection( - p.SessionId, - p.Tenant, - p.UserKey, - p.ChainId, - p.CreatedAt, - p.ExpiresAt, - p.LastSeenAt, - p.IsRevoked, - p.RevokedAt, - p.SecurityVersionAtCreation, - p.Device, - p.Claims, - p.Metadata - ); - } + return UAuthSession.FromProjection( + p.SessionId, + p.Tenant, + p.UserKey, + p.ChainId, + p.CreatedAt, + p.ExpiresAt, + p.LastSeenAt, + p.IsRevoked, + p.RevokedAt, + p.SecurityVersionAtCreation, + p.Device, + p.Claims, + p.Metadata + ); + } - public static SessionProjection ToProjection(this UAuthSession s) + public static SessionProjection ToProjection(this UAuthSession s) + { + return new SessionProjection { - return new SessionProjection - { - SessionId = s.SessionId, - Tenant = s.Tenant, - UserKey = s.UserKey, - ChainId = s.ChainId, - - CreatedAt = s.CreatedAt, - ExpiresAt = s.ExpiresAt, - LastSeenAt = s.LastSeenAt, + SessionId = s.SessionId, + Tenant = s.Tenant, + UserKey = s.UserKey, + ChainId = s.ChainId, - IsRevoked = s.IsRevoked, - RevokedAt = s.RevokedAt, + CreatedAt = s.CreatedAt, + ExpiresAt = s.ExpiresAt, + LastSeenAt = s.LastSeenAt, - SecurityVersionAtCreation = s.SecurityVersionAtCreation, - Device = s.Device, - Claims = s.Claims, - Metadata = s.Metadata - }; - } + IsRevoked = s.IsRevoked, + RevokedAt = s.RevokedAt, + SecurityVersionAtCreation = s.SecurityVersionAtCreation, + Device = s.Device, + Claims = s.Claims, + Metadata = s.Metadata + }; } + } diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionRootProjectionMapper.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionRootProjectionMapper.cs index 3cd3666d..a0c223ae 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionRootProjectionMapper.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionRootProjectionMapper.cs @@ -1,38 +1,36 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore +namespace CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore; + +internal static class SessionRootProjectionMapper { - internal static class SessionRootProjectionMapper + public static UAuthSessionRoot ToDomain(this SessionRootProjection root, IReadOnlyList? chains = null) { - public static UAuthSessionRoot ToDomain(this SessionRootProjection root, IReadOnlyList? chains = null) - { - return UAuthSessionRoot.FromProjection( - root.RootId, - root.Tenant, - root.UserKey, - root.IsRevoked, - root.RevokedAt, - root.SecurityVersion, - chains ?? Array.Empty(), - root.LastUpdatedAt - ); - } + return UAuthSessionRoot.FromProjection( + root.RootId, + root.Tenant, + root.UserKey, + root.IsRevoked, + root.RevokedAt, + root.SecurityVersion, + chains ?? Array.Empty(), + root.LastUpdatedAt + ); + } - public static SessionRootProjection ToProjection(this UAuthSessionRoot root) + public static SessionRootProjection ToProjection(this UAuthSessionRoot root) + { + return new SessionRootProjection { - return new SessionRootProjection - { - RootId = root.RootId, - Tenant = root.Tenant, - UserKey = root.UserKey, - - IsRevoked = root.IsRevoked, - RevokedAt = root.RevokedAt, + RootId = root.RootId, + Tenant = root.Tenant, + UserKey = root.UserKey, - SecurityVersion = root.SecurityVersion, - LastUpdatedAt = root.LastUpdatedAt - }; - } + IsRevoked = root.IsRevoked, + RevokedAt = root.RevokedAt, + SecurityVersion = root.SecurityVersion, + LastUpdatedAt = root.LastUpdatedAt + }; } } diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Stores/EfCoreSessionStoreKernel.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Stores/EfCoreSessionStoreKernel.cs index ed07e8b4..eb7616ba 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Stores/EfCoreSessionStoreKernel.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Stores/EfCoreSessionStoreKernel.cs @@ -4,246 +4,245 @@ using Microsoft.EntityFrameworkCore; using System.Data; -namespace CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore +namespace CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore; + +internal sealed class EfCoreSessionStoreKernel : ISessionStoreKernel { - internal sealed class EfCoreSessionStoreKernel : ISessionStoreKernel + private readonly UltimateAuthSessionDbContext _db; + private readonly TenantContext _tenant; + + public EfCoreSessionStoreKernel(UltimateAuthSessionDbContext db, TenantContext tenant) { - private readonly UltimateAuthSessionDbContext _db; - private readonly TenantContext _tenant; + _db = db; + _tenant = tenant; + } - public EfCoreSessionStoreKernel(UltimateAuthSessionDbContext db, TenantContext tenant) - { - _db = db; - _tenant = tenant; - } + public async Task ExecuteAsync(Func action, CancellationToken ct = default) + { + var strategy = _db.Database.CreateExecutionStrategy(); - public async Task ExecuteAsync(Func action, CancellationToken ct = default) + await strategy.ExecuteAsync(async () => { - var strategy = _db.Database.CreateExecutionStrategy(); + var connection = _db.Database.GetDbConnection(); + if (connection.State != ConnectionState.Open) + await connection.OpenAsync(ct); - await strategy.ExecuteAsync(async () => + await using var tx = await connection.BeginTransactionAsync(IsolationLevel.RepeatableRead, ct); + _db.Database.UseTransaction(tx); + + try { - var connection = _db.Database.GetDbConnection(); - if (connection.State != ConnectionState.Open) - await connection.OpenAsync(ct); - - await using var tx = await connection.BeginTransactionAsync(IsolationLevel.RepeatableRead, ct); - _db.Database.UseTransaction(tx); - - try - { - await action(ct); - await _db.SaveChangesAsync(ct); - await tx.CommitAsync(ct); - } - catch - { - await tx.RollbackAsync(ct); - throw; - } - finally - { - _db.Database.UseTransaction(null); - } - }); - } + await action(ct); + await _db.SaveChangesAsync(ct); + await tx.CommitAsync(ct); + } + catch + { + await tx.RollbackAsync(ct); + throw; + } + finally + { + _db.Database.UseTransaction(null); + } + }); + } - public async Task GetSessionAsync(AuthSessionId sessionId) - { - var projection = await _db.Sessions - .AsNoTracking() - .SingleOrDefaultAsync(x => x.SessionId == sessionId); + public async Task GetSessionAsync(AuthSessionId sessionId) + { + var projection = await _db.Sessions + .AsNoTracking() + .SingleOrDefaultAsync(x => x.SessionId == sessionId); - return projection?.ToDomain(); - } + return projection?.ToDomain(); + } - public async Task SaveSessionAsync(UAuthSession session) - { - var projection = session.ToProjection(); + public async Task SaveSessionAsync(UAuthSession session) + { + var projection = session.ToProjection(); - var exists = await _db.Sessions - .AnyAsync(x => x.SessionId == session.SessionId); + var exists = await _db.Sessions + .AnyAsync(x => x.SessionId == session.SessionId); - if (exists) - _db.Sessions.Update(projection); - else - _db.Sessions.Add(projection); - } + if (exists) + _db.Sessions.Update(projection); + else + _db.Sessions.Add(projection); + } - public async Task RevokeSessionAsync(AuthSessionId sessionId, DateTimeOffset at) - { - var projection = await _db.Sessions - .SingleOrDefaultAsync(x => x.SessionId == sessionId); + public async Task RevokeSessionAsync(AuthSessionId sessionId, DateTimeOffset at) + { + var projection = await _db.Sessions + .SingleOrDefaultAsync(x => x.SessionId == sessionId); - if (projection is null) - return; + if (projection is null) + return; - var session = projection.ToDomain(); - if (session.IsRevoked) - return; + var session = projection.ToDomain(); + if (session.IsRevoked) + return; - var revoked = session.Revoke(at); - _db.Sessions.Update(revoked.ToProjection()); - } + var revoked = session.Revoke(at); + _db.Sessions.Update(revoked.ToProjection()); + } - public async Task GetChainAsync(SessionChainId chainId) - { - var projection = await _db.Chains - .AsNoTracking() - .SingleOrDefaultAsync(x => x.ChainId == chainId); + public async Task GetChainAsync(SessionChainId chainId) + { + var projection = await _db.Chains + .AsNoTracking() + .SingleOrDefaultAsync(x => x.ChainId == chainId); - return projection?.ToDomain(); - } + return projection?.ToDomain(); + } - public async Task SaveChainAsync(UAuthSessionChain chain) - { - var projection = chain.ToProjection(); + public async Task SaveChainAsync(UAuthSessionChain chain) + { + var projection = chain.ToProjection(); - var exists = await _db.Chains - .AnyAsync(x => x.ChainId == chain.ChainId); + var exists = await _db.Chains + .AnyAsync(x => x.ChainId == chain.ChainId); - if (exists) - _db.Chains.Update(projection); - else - _db.Chains.Add(projection); - } + if (exists) + _db.Chains.Update(projection); + else + _db.Chains.Add(projection); + } - public async Task RevokeChainAsync(SessionChainId chainId, DateTimeOffset at) - { - var projection = await _db.Chains - .SingleOrDefaultAsync(x => x.ChainId == chainId); + public async Task RevokeChainAsync(SessionChainId chainId, DateTimeOffset at) + { + var projection = await _db.Chains + .SingleOrDefaultAsync(x => x.ChainId == chainId); - if (projection is null) - return; + if (projection is null) + return; - var chain = projection.ToDomain(); - if (chain.IsRevoked) - return; + var chain = projection.ToDomain(); + if (chain.IsRevoked) + return; - _db.Chains.Update(chain.Revoke(at).ToProjection()); - } + _db.Chains.Update(chain.Revoke(at).ToProjection()); + } - public async Task GetActiveSessionIdAsync(SessionChainId chainId) - { - return await _db.Chains - .AsNoTracking() - .Where(x => x.ChainId == chainId) - .Select(x => x.ActiveSessionId) - .SingleOrDefaultAsync(); - } + public async Task GetActiveSessionIdAsync(SessionChainId chainId) + { + return await _db.Chains + .AsNoTracking() + .Where(x => x.ChainId == chainId) + .Select(x => x.ActiveSessionId) + .SingleOrDefaultAsync(); + } - public async Task SetActiveSessionIdAsync(SessionChainId chainId, AuthSessionId sessionId) - { - var projection = await _db.Chains - .SingleOrDefaultAsync(x => x.ChainId == chainId); + public async Task SetActiveSessionIdAsync(SessionChainId chainId, AuthSessionId sessionId) + { + var projection = await _db.Chains + .SingleOrDefaultAsync(x => x.ChainId == chainId); - if (projection is null) - return; + if (projection is null) + return; - projection.ActiveSessionId = sessionId; - _db.Chains.Update(projection); - } + projection.ActiveSessionId = sessionId; + _db.Chains.Update(projection); + } - public async Task GetSessionRootByUserAsync(UserKey userKey) - { - var rootProjection = await _db.Roots - .AsNoTracking() - .SingleOrDefaultAsync(x => x.UserKey == userKey); + public async Task GetSessionRootByUserAsync(UserKey userKey) + { + var rootProjection = await _db.Roots + .AsNoTracking() + .SingleOrDefaultAsync(x => x.UserKey == userKey); - if (rootProjection is null) - return null; + if (rootProjection is null) + return null; - var chains = await _db.Chains - .AsNoTracking() - .Where(x => x.UserKey == userKey) - .ToListAsync(); + var chains = await _db.Chains + .AsNoTracking() + .Where(x => x.UserKey == userKey) + .ToListAsync(); - return rootProjection.ToDomain(chains.Select(c => c.ToDomain()).ToList()); - } + return rootProjection.ToDomain(chains.Select(c => c.ToDomain()).ToList()); + } - public async Task SaveSessionRootAsync(UAuthSessionRoot root) - { - var projection = root.ToProjection(); + public async Task SaveSessionRootAsync(UAuthSessionRoot root) + { + var projection = root.ToProjection(); - var exists = await _db.Roots - .AnyAsync(x => x.RootId == root.RootId); + var exists = await _db.Roots + .AnyAsync(x => x.RootId == root.RootId); - if (exists) - _db.Roots.Update(projection); - else - _db.Roots.Add(projection); - } + if (exists) + _db.Roots.Update(projection); + else + _db.Roots.Add(projection); + } - public async Task RevokeSessionRootAsync(UserKey userKey, DateTimeOffset at) - { - var projection = await _db.Roots.SingleOrDefaultAsync(x => x.UserKey == userKey); + public async Task RevokeSessionRootAsync(UserKey userKey, DateTimeOffset at) + { + var projection = await _db.Roots.SingleOrDefaultAsync(x => x.UserKey == userKey); - if (projection is null) - return; + if (projection is null) + return; - var root = projection.ToDomain(); - _db.Roots.Update(root.Revoke(at).ToProjection()); - } + var root = projection.ToDomain(); + _db.Roots.Update(root.Revoke(at).ToProjection()); + } - public async Task GetChainIdBySessionAsync(AuthSessionId sessionId) - { - return await _db.Sessions - .AsNoTracking() - .Where(x => x.SessionId == sessionId) - .Select(x => (SessionChainId?)x.ChainId) - .SingleOrDefaultAsync(); - } + public async Task GetChainIdBySessionAsync(AuthSessionId sessionId) + { + return await _db.Sessions + .AsNoTracking() + .Where(x => x.SessionId == sessionId) + .Select(x => (SessionChainId?)x.ChainId) + .SingleOrDefaultAsync(); + } - public async Task> GetChainsByUserAsync(UserKey userKey) - { - var projections = await _db.Chains - .AsNoTracking() - .Where(x => x.UserKey == userKey) - .ToListAsync(); + public async Task> GetChainsByUserAsync(UserKey userKey) + { + var projections = await _db.Chains + .AsNoTracking() + .Where(x => x.UserKey == userKey) + .ToListAsync(); - return projections.Select(x => x.ToDomain()).ToList(); - } + return projections.Select(x => x.ToDomain()).ToList(); + } - public async Task> GetSessionsByChainAsync(SessionChainId chainId) - { - var projections = await _db.Sessions - .AsNoTracking() - .Where(x => x.ChainId == chainId) - .ToListAsync(); + public async Task> GetSessionsByChainAsync(SessionChainId chainId) + { + var projections = await _db.Sessions + .AsNoTracking() + .Where(x => x.ChainId == chainId) + .ToListAsync(); - return projections.Select(x => x.ToDomain()).ToList(); - } + return projections.Select(x => x.ToDomain()).ToList(); + } - public async Task GetSessionRootByIdAsync(SessionRootId rootId) - { - var rootProjection = await _db.Roots - .AsNoTracking() - .SingleOrDefaultAsync(x => x.RootId == rootId); + public async Task GetSessionRootByIdAsync(SessionRootId rootId) + { + var rootProjection = await _db.Roots + .AsNoTracking() + .SingleOrDefaultAsync(x => x.RootId == rootId); - if (rootProjection is null) - return null; + if (rootProjection is null) + return null; - var chains = await _db.Chains - .AsNoTracking() - .Where(x => x.RootId == rootId) - .ToListAsync(); + var chains = await _db.Chains + .AsNoTracking() + .Where(x => x.RootId == rootId) + .ToListAsync(); - return rootProjection.ToDomain(chains.Select(c => c.ToDomain()).ToList()); - } + return rootProjection.ToDomain(chains.Select(c => c.ToDomain()).ToList()); + } - public async Task DeleteExpiredSessionsAsync(DateTimeOffset at) - { - var projections = await _db.Sessions - .Where(x => x.ExpiresAt <= at && !x.IsRevoked) - .ToListAsync(); + public async Task DeleteExpiredSessionsAsync(DateTimeOffset at) + { + var projections = await _db.Sessions + .Where(x => x.ExpiresAt <= at && !x.IsRevoked) + .ToListAsync(); - foreach (var p in projections) - { - var revoked = p.ToDomain().Revoke(at); - _db.Sessions.Update(revoked.ToProjection()); - } + foreach (var p in projections) + { + var revoked = p.ToDomain().Revoke(at); + _db.Sessions.Update(revoked.ToProjection()); } - } + } diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/InMemorySessionStoreKernel.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/InMemorySessionStoreKernel.cs index 9a2dc197..0b3cbb16 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/InMemorySessionStoreKernel.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/InMemorySessionStoreKernel.cs @@ -2,6 +2,8 @@ using CodeBeam.UltimateAuth.Core.Domain; using System.Collections.Concurrent; +namespace CodeBeam.UltimateAuth.Sessions.InMemory; + internal sealed class InMemorySessionStoreKernel : ISessionStoreKernel { private readonly SemaphoreSlim _tx = new(1, 1); @@ -24,8 +26,7 @@ public async Task ExecuteAsync(Func action, Cancellatio } } - public Task GetSessionAsync(AuthSessionId sessionId) - => Task.FromResult(_sessions.TryGetValue(sessionId, out var s) ? s : null); + public Task GetSessionAsync(AuthSessionId sessionId) => Task.FromResult(_sessions.TryGetValue(sessionId, out var s) ? s : null); public Task SaveSessionAsync(UAuthSession session) { diff --git a/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/EfCoreTokenStore.cs b/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/EfCoreTokenStore.cs index 1c66fde0..3c31d42f 100644 --- a/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/EfCoreTokenStore.cs +++ b/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/EfCoreTokenStore.cs @@ -1,9 +1,10 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Core.MultiTenancy; -using CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; +namespace CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore; + internal sealed class EfCoreRefreshTokenStore : IRefreshTokenStore { private readonly UltimateAuthTokenDbContext _db; diff --git a/src/tokens/CodeBeam.UltimateAuth.Tokens.InMemory/InMemoryRefreshTokenStore.cs b/src/tokens/CodeBeam.UltimateAuth.Tokens.InMemory/InMemoryRefreshTokenStore.cs index 408f8141..7046a019 100644 --- a/src/tokens/CodeBeam.UltimateAuth.Tokens.InMemory/InMemoryRefreshTokenStore.cs +++ b/src/tokens/CodeBeam.UltimateAuth.Tokens.InMemory/InMemoryRefreshTokenStore.cs @@ -7,7 +7,7 @@ namespace CodeBeam.UltimateAuth.Tokens.InMemory; public sealed class InMemoryRefreshTokenStore : IRefreshTokenStore { - private static string NormalizeTenant(string? tenantId) => tenantId ?? "__default__"; + private static string NormalizeTenant(string? tenantId) => tenantId ?? "__single__"; private readonly ConcurrentDictionary _tokens = new(); diff --git a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/MfaMethod.cs b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/MfaMethod.cs index 3a5b936b..f207ae2c 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/MfaMethod.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/MfaMethod.cs @@ -1,10 +1,9 @@ -namespace CodeBeam.UltimateAuth.Users.Contracts +namespace CodeBeam.UltimateAuth.Users.Contracts; + +public enum MfaMethod { - public enum MfaMethod - { - Totp = 10, - Sms = 20, - Email = 30, - Passkey = 40 - } + Totp = 10, + Sms = 20, + Email = 30, + Passkey = 40 } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserAccessDecision.cs b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserAccessDecision.cs deleted file mode 100644 index 04926b51..00000000 --- a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserAccessDecision.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace CodeBeam.UltimateAuth.Users.Contracts; - -public sealed record UserAccessDecision( - bool IsAllowed, - bool RequiresReauthentication, - string? DenyReason = null); diff --git a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserIdentifierDto.cs b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserIdentifierDto.cs index 0a0bc2de..caf74f5f 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserIdentifierDto.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserIdentifierDto.cs @@ -1,12 +1,11 @@ -namespace CodeBeam.UltimateAuth.Users.Contracts +namespace CodeBeam.UltimateAuth.Users.Contracts; + +public sealed record UserIdentifierDto { - public sealed record UserIdentifierDto - { - public required UserIdentifierType Type { get; init; } - public required string Value { get; init; } - public bool IsPrimary { get; init; } - public bool IsVerified { get; init; } - public DateTimeOffset CreatedAt { get; init; } - public DateTimeOffset? VerifiedAt { get; init; } - } + public required UserIdentifierType Type { get; init; } + public required string Value { get; init; } + public bool IsPrimary { get; init; } + public bool IsVerified { get; init; } + public DateTimeOffset CreatedAt { get; init; } + public DateTimeOffset? VerifiedAt { get; init; } } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserIdentifierType.cs b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserIdentifierType.cs index 57ef44fd..4694a8c6 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserIdentifierType.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserIdentifierType.cs @@ -1,9 +1,8 @@ -namespace CodeBeam.UltimateAuth.Users.Contracts +namespace CodeBeam.UltimateAuth.Users.Contracts; + +public enum UserIdentifierType { - public enum UserIdentifierType - { - Username, - Email, - Phone - } + Username, + Email, + Phone } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserMfaStatusDto.cs b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserMfaStatusDto.cs index bf652782..736ab39b 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserMfaStatusDto.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserMfaStatusDto.cs @@ -1,15 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +namespace CodeBeam.UltimateAuth.Users.Contracts; -namespace CodeBeam.UltimateAuth.Users.Contracts +public sealed record UserMfaStatusDto { - public sealed record UserMfaStatusDto - { - public bool IsEnabled { get; init; } - public IReadOnlyCollection EnabledMethods { get; init; } = Array.Empty(); - public MfaMethod? DefaultMethod { get; init; } - } + public bool IsEnabled { get; init; } + public IReadOnlyCollection EnabledMethods { get; init; } = Array.Empty(); + public MfaMethod? DefaultMethod { get; init; } } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserProfileInput.cs b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserProfileInput.cs deleted file mode 100644 index 2a9f029d..00000000 --- a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserProfileInput.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace CodeBeam.UltimateAuth.Users.Contracts; - -public sealed record UserProfileInput -{ - public string? FirstName { get; init; } - public string? LastName { get; init; } - public string? DisplayName { get; init; } - public string? Email { get; init; } - public string? Phone { get; init; } -} diff --git a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserViewDto.cs b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserViewDto.cs index 5ebd9a46..ce89dea8 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserViewDto.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserViewDto.cs @@ -1,26 +1,25 @@ -namespace CodeBeam.UltimateAuth.Users.Contracts +namespace CodeBeam.UltimateAuth.Users.Contracts; + +public sealed record UserViewDto { - public sealed record UserViewDto - { - public string UserKey { get; init; } = default!; + public string UserKey { get; init; } = default!; - public string? UserName { get; init; } - public string? PrimaryEmail { get; init; } - public string? PrimaryPhone { get; init; } + public string? UserName { get; init; } + public string? PrimaryEmail { get; init; } + public string? PrimaryPhone { get; init; } - public string? FirstName { get; init; } - public string? LastName { get; init; } - public string? DisplayName { get; init; } - public string? Bio { get; init; } - public DateOnly? BirthDate { get; init; } - public string? Gender { get; init; } + public string? FirstName { get; init; } + public string? LastName { get; init; } + public string? DisplayName { get; init; } + public string? Bio { get; init; } + public DateOnly? BirthDate { get; init; } + public string? Gender { get; init; } - public bool EmailVerified { get; init; } - public bool PhoneVerified { get; init; } + public bool EmailVerified { get; init; } + public bool PhoneVerified { get; init; } - public DateTimeOffset? CreatedAt { get; init; } - //public DateTimeOffset? LastLoginAt { get; init; } + public DateTimeOffset? CreatedAt { get; init; } + //public DateTimeOffset? LastLoginAt { get; init; } - public IReadOnlyDictionary? Metadata { get; init; } - } + public IReadOnlyDictionary? Metadata { get; init; } } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/AddUserIdentifierRequest.cs b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/AddUserIdentifierRequest.cs index 39de26bd..0ae55149 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/AddUserIdentifierRequest.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/AddUserIdentifierRequest.cs @@ -1,9 +1,8 @@ -namespace CodeBeam.UltimateAuth.Users.Contracts +namespace CodeBeam.UltimateAuth.Users.Contracts; + +public sealed record AddUserIdentifierRequest { - public sealed record AddUserIdentifierRequest - { - public UserIdentifierType Type { get; init; } - public string Value { get; init; } = default!; - public bool IsPrimary { get; init; } - } + public UserIdentifierType Type { get; init; } + public string Value { get; init; } = default!; + public bool IsPrimary { get; init; } } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/BeginMfaSetupRequest.cs b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/BeginMfaSetupRequest.cs index 7c5c9e28..c14eef0d 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/BeginMfaSetupRequest.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/BeginMfaSetupRequest.cs @@ -1,7 +1,6 @@ -namespace CodeBeam.UltimateAuth.Users.Contracts +namespace CodeBeam.UltimateAuth.Users.Contracts; + +public sealed record BeginMfaSetupRequest { - public sealed record BeginMfaSetupRequest - { - public MfaMethod Method { get; init; } - } + public MfaMethod Method { get; init; } } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/ChangeUserIdentifierRequest.cs b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/ChangeUserIdentifierRequest.cs index cf6a530f..d417d6a8 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/ChangeUserIdentifierRequest.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/ChangeUserIdentifierRequest.cs @@ -1,9 +1,8 @@ -namespace CodeBeam.UltimateAuth.Users.Contracts +namespace CodeBeam.UltimateAuth.Users.Contracts; + +public sealed record ChangeUserIdentifierRequest { - public sealed record ChangeUserIdentifierRequest - { - public required UserIdentifierType Type { get; init; } - public required string NewValue { get; init; } - public string? Reason { get; init; } - } + public required UserIdentifierType Type { get; init; } + public required string NewValue { get; init; } + public string? Reason { get; init; } } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/ChangeUserStatusAdminRequest.cs b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/ChangeUserStatusAdminRequest.cs index d88b519d..5b7561e3 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/ChangeUserStatusAdminRequest.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/ChangeUserStatusAdminRequest.cs @@ -1,10 +1,9 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Users.Contracts +namespace CodeBeam.UltimateAuth.Users.Contracts; + +public sealed class ChangeUserStatusAdminRequest { - public sealed class ChangeUserStatusAdminRequest - { - public required UserKey UserKey { get; init; } - public required UserStatus NewStatus { get; init; } - } + public required UserKey UserKey { get; init; } + public required UserStatus NewStatus { get; init; } } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/ChangeUserStatusSelfRequest.cs b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/ChangeUserStatusSelfRequest.cs index aaa7ad0c..dba43740 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/ChangeUserStatusSelfRequest.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/ChangeUserStatusSelfRequest.cs @@ -1,7 +1,6 @@ -namespace CodeBeam.UltimateAuth.Users.Contracts +namespace CodeBeam.UltimateAuth.Users.Contracts; + +public class ChangeUserStatusSelfRequest { - public class ChangeUserStatusSelfRequest - { - public required UserStatus NewStatus { get; init; } - } + public required UserStatus NewStatus { get; init; } } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/CompleteMfaSetupRequest.cs b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/CompleteMfaSetupRequest.cs index ab1ba705..ad398643 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/CompleteMfaSetupRequest.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/CompleteMfaSetupRequest.cs @@ -1,8 +1,7 @@ -namespace CodeBeam.UltimateAuth.Users.Contracts +namespace CodeBeam.UltimateAuth.Users.Contracts; + +public sealed record CompleteMfaSetupRequest { - public sealed record CompleteMfaSetupRequest - { - public MfaMethod Method { get; init; } - public string VerificationCode { get; init; } = default!; - } + public MfaMethod Method { get; init; } + public string VerificationCode { get; init; } = default!; } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/DeleteUserIdentifierRequest.cs b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/DeleteUserIdentifierRequest.cs index 33b95744..73509817 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/DeleteUserIdentifierRequest.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/DeleteUserIdentifierRequest.cs @@ -1,11 +1,10 @@ using CodeBeam.UltimateAuth.Core.Contracts; -namespace CodeBeam.UltimateAuth.Users.Contracts +namespace CodeBeam.UltimateAuth.Users.Contracts; + +public sealed record DeleteUserIdentifierRequest { - public sealed record DeleteUserIdentifierRequest - { - public required UserIdentifierType Type { get; init; } - public required string Value { get; init; } - public DeleteMode Mode { get; init; } = DeleteMode.Soft; - } + public required UserIdentifierType Type { get; init; } + public required string Value { get; init; } + public DeleteMode Mode { get; init; } = DeleteMode.Soft; } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/DeleteUserRequest.cs b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/DeleteUserRequest.cs index f24058c1..a6721e42 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/DeleteUserRequest.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/DeleteUserRequest.cs @@ -1,10 +1,8 @@ using CodeBeam.UltimateAuth.Core.Contracts; -using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Users.Contracts +namespace CodeBeam.UltimateAuth.Users.Contracts; + +public sealed class DeleteUserRequest { - public sealed class DeleteUserRequest - { - public DeleteMode Mode { get; init; } = DeleteMode.Soft; - } + public DeleteMode Mode { get; init; } = DeleteMode.Soft; } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/DisableMfaRequest.cs b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/DisableMfaRequest.cs index 63da5424..755a8de7 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/DisableMfaRequest.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/DisableMfaRequest.cs @@ -1,7 +1,6 @@ -namespace CodeBeam.UltimateAuth.Users.Contracts +namespace CodeBeam.UltimateAuth.Users.Contracts; + +public sealed record DisableMfaRequest { - public sealed record DisableMfaRequest - { - public MfaMethod? Method { get; init; } // null = all - } + public MfaMethod? Method { get; init; } // null = all } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/RegisterUserRequest.cs b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/RegisterUserRequest.cs index 56b11369..985e12e5 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/RegisterUserRequest.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/RegisterUserRequest.cs @@ -1,32 +1,31 @@ using CodeBeam.UltimateAuth.Core.MultiTenancy; -namespace CodeBeam.UltimateAuth.Users.Contracts +namespace CodeBeam.UltimateAuth.Users.Contracts; + +/// +/// Request to register a new user with credentials. +/// +public sealed class RegisterUserRequest { /// - /// Request to register a new user with credentials. + /// Unique user identifier (username, email, or external id). + /// Interpretation is application-specific. /// - public sealed class RegisterUserRequest - { - /// - /// Unique user identifier (username, email, or external id). - /// Interpretation is application-specific. - /// - public required string Identifier { get; init; } + public required string Identifier { get; init; } - /// - /// Plain-text password. - /// Will be hashed by the configured password hasher. - /// - public required string Password { get; init; } + /// + /// Plain-text password. + /// Will be hashed by the configured password hasher. + /// + public required string Password { get; init; } - /// - /// Optional tenant identifier. - /// - public TenantKey Tenant { get; init; } + /// + /// Optional tenant identifier. + /// + public TenantKey Tenant { get; init; } - /// - /// Optional initial claims or metadata. - /// - public IReadOnlyDictionary? Metadata { get; init; } - } + /// + /// Optional initial claims or metadata. + /// + public IReadOnlyDictionary? Metadata { get; init; } } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/SerPrimaryUserIdentifierRequest.cs b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/SerPrimaryUserIdentifierRequest.cs deleted file mode 100644 index 66802a50..00000000 --- a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/SerPrimaryUserIdentifierRequest.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace CodeBeam.UltimateAuth.Users.Contracts -{ - public sealed record SetPrimaryUserIdentifierRequest - { - public UserIdentifierType Type { get; init; } - public string Value { get; init; } = default!; - } -} diff --git a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/SetPrimaryUserIdentifierRequest.cs b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/SetPrimaryUserIdentifierRequest.cs new file mode 100644 index 00000000..b435933a --- /dev/null +++ b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/SetPrimaryUserIdentifierRequest.cs @@ -0,0 +1,7 @@ +namespace CodeBeam.UltimateAuth.Users.Contracts; + +public sealed record SetPrimaryUserIdentifierRequest +{ + public UserIdentifierType Type { get; init; } + public string Value { get; init; } = default!; +} diff --git a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/UnsetPrimaryUserIdentifierRequest.cs b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/UnsetPrimaryUserIdentifierRequest.cs index f17e69b8..2dc8ef76 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/UnsetPrimaryUserIdentifierRequest.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/UnsetPrimaryUserIdentifierRequest.cs @@ -1,8 +1,7 @@ -namespace CodeBeam.UltimateAuth.Users.Contracts +namespace CodeBeam.UltimateAuth.Users.Contracts; + +public sealed record UnsetPrimaryUserIdentifierRequest { - public sealed record UnsetPrimaryUserIdentifierRequest - { - public UserIdentifierType Type { get; init; } - public string Value { get; init; } = default!; - } + public UserIdentifierType Type { get; init; } + public string Value { get; init; } = default!; } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/UpdateUserIdentifierRequest.cs b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/UpdateUserIdentifierRequest.cs index 880d5916..50a9976d 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/UpdateUserIdentifierRequest.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/UpdateUserIdentifierRequest.cs @@ -1,9 +1,8 @@ -namespace CodeBeam.UltimateAuth.Users.Contracts +namespace CodeBeam.UltimateAuth.Users.Contracts; + +public sealed record UpdateUserIdentifierRequest { - public sealed record UpdateUserIdentifierRequest - { - public UserIdentifierType Type { get; init; } - public string OldValue { get; init; } = default!; - public string NewValue { get; init; } = default!; - } + public UserIdentifierType Type { get; init; } + public string OldValue { get; init; } = default!; + public string NewValue { get; init; } = default!; } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/VerifyUserIdentifierRequest.cs b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/VerifyUserIdentifierRequest.cs index 30049f2f..765fecdb 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/VerifyUserIdentifierRequest.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/VerifyUserIdentifierRequest.cs @@ -1,8 +1,7 @@ -namespace CodeBeam.UltimateAuth.Users.Contracts +namespace CodeBeam.UltimateAuth.Users.Contracts; + +public sealed record VerifyUserIdentifierRequest { - public sealed record VerifyUserIdentifierRequest - { - public required UserIdentifierType Type { get; init; } - public required string Value { get; init; } - } + public required UserIdentifierType Type { get; init; } + public required string Value { get; init; } } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Responses/BeginMfaSetupResult.cs b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Responses/BeginMfaSetupResult.cs index 0285c0a3..7fb41c67 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Responses/BeginMfaSetupResult.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Responses/BeginMfaSetupResult.cs @@ -1,9 +1,8 @@ -namespace CodeBeam.UltimateAuth.Users.Contracts +namespace CodeBeam.UltimateAuth.Users.Contracts; + +public sealed record BeginMfaSetupResult { - public sealed record BeginMfaSetupResult - { - public MfaMethod Method { get; init; } - public string? SharedSecret { get; init; } // TOTP - public string? QrCodeUri { get; init; } // TOTP - } + public MfaMethod Method { get; init; } + public string? SharedSecret { get; init; } // TOTP + public string? QrCodeUri { get; init; } // TOTP } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Responses/GetUserIdentifiersResult.cs b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Responses/GetUserIdentifiersResult.cs index a1e4b8e7..a0def373 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Responses/GetUserIdentifiersResult.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Responses/GetUserIdentifiersResult.cs @@ -1,7 +1,6 @@ -namespace CodeBeam.UltimateAuth.Users.Contracts +namespace CodeBeam.UltimateAuth.Users.Contracts; + +public sealed record GetUserIdentifiersResult { - public sealed record GetUserIdentifiersResult - { - public required IReadOnlyCollection Identifiers { get; init; } - } + public required IReadOnlyCollection Identifiers { get; init; } } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Responses/IdentifierChangeResult.cs b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Responses/IdentifierChangeResult.cs index db7376d8..da9f1da4 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Responses/IdentifierChangeResult.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Responses/IdentifierChangeResult.cs @@ -1,12 +1,11 @@ -namespace CodeBeam.UltimateAuth.Users.Contracts +namespace CodeBeam.UltimateAuth.Users.Contracts; + +public sealed record IdentifierChangeResult { - public sealed record IdentifierChangeResult - { - public bool Succeeded { get; init; } - public string? FailureReason { get; init; } + public bool Succeeded { get; init; } + public string? FailureReason { get; init; } - public static IdentifierChangeResult Success() => new() { Succeeded = true }; + public static IdentifierChangeResult Success() => new() { Succeeded = true }; - public static IdentifierChangeResult Failed(string reason) => new() { Succeeded = false, FailureReason = reason }; - } + public static IdentifierChangeResult Failed(string reason) => new() { Succeeded = false, FailureReason = reason }; } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Responses/IdentifierDeleteResult.cs b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Responses/IdentifierDeleteResult.cs index e62ec71e..00145603 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Responses/IdentifierDeleteResult.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Responses/IdentifierDeleteResult.cs @@ -1,11 +1,10 @@ -namespace CodeBeam.UltimateAuth.Users.Contracts +namespace CodeBeam.UltimateAuth.Users.Contracts; + +public sealed record IdentifierDeleteResult { - public sealed record IdentifierDeleteResult - { - public bool Succeeded { get; init; } - public string? FailureReason { get; init; } + public bool Succeeded { get; init; } + public string? FailureReason { get; init; } - public static IdentifierDeleteResult Success() => new() { Succeeded = true }; - public static IdentifierDeleteResult Fail(string reason) => new() { Succeeded = false, FailureReason = reason }; - } + public static IdentifierDeleteResult Success() => new() { Succeeded = true }; + public static IdentifierDeleteResult Fail(string reason) => new() { Succeeded = false, FailureReason = reason }; } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Responses/IdentifierVerificationResult.cs b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Responses/IdentifierVerificationResult.cs index 6c4b4464..b642949d 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Responses/IdentifierVerificationResult.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Responses/IdentifierVerificationResult.cs @@ -1,12 +1,11 @@ -namespace CodeBeam.UltimateAuth.Users.Contracts +namespace CodeBeam.UltimateAuth.Users.Contracts; + +public sealed record IdentifierVerificationResult { - public sealed record IdentifierVerificationResult - { - public bool Succeeded { get; init; } - public string? FailureReason { get; init; } + public bool Succeeded { get; init; } + public string? FailureReason { get; init; } - public static IdentifierVerificationResult Success() => new() { Succeeded = true }; + public static IdentifierVerificationResult Success() => new() { Succeeded = true }; - public static IdentifierVerificationResult Failed(string reason) => new() { Succeeded = false, FailureReason = reason }; - } + public static IdentifierVerificationResult Failed(string reason) => new() { Succeeded = false, FailureReason = reason }; } diff --git a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Extensions/ServiceCollectionExtensions.cs b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..e6209864 --- /dev/null +++ b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,25 @@ +using CodeBeam.UltimateAuth.Core.Abstractions; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.Infrastructure; +using CodeBeam.UltimateAuth.Users.Reference; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace CodeBeam.UltimateAuth.Users.InMemory.Extensions; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddUltimateAuthUsersInMemory(this IServiceCollection services) + { + services.TryAddScoped(typeof(IUserSecurityStateProvider<>), typeof(InMemoryUserSecurityStateProvider<>)); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton, InMemoryUserIdProvider>(); + + // Seed never try add + services.AddSingleton(); + + return services; + } +} diff --git a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Extensions/UltimateAuthUsersInMemoryExtensions.cs b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Extensions/UltimateAuthUsersInMemoryExtensions.cs deleted file mode 100644 index 641d5c47..00000000 --- a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Extensions/UltimateAuthUsersInMemoryExtensions.cs +++ /dev/null @@ -1,26 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Abstractions; -using CodeBeam.UltimateAuth.Core.Domain; -using CodeBeam.UltimateAuth.Core.Infrastructure; -using CodeBeam.UltimateAuth.Users.Reference; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; - -namespace CodeBeam.UltimateAuth.Users.InMemory.Extensions -{ - public static class UltimateAuthUsersInMemoryExtensions - { - public static IServiceCollection AddUltimateAuthUsersInMemory(this IServiceCollection services) - { - services.TryAddScoped(typeof(IUserSecurityStateProvider<>), typeof(InMemoryUserSecurityStateProvider<>)); - services.TryAddSingleton(); - services.TryAddSingleton(); - services.TryAddSingleton(); - services.TryAddSingleton, InMemoryUserIdProvider>(); - - // Seed never try add - services.AddSingleton(); - - return services; - } - } -} diff --git a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Infrastructure/InMemoryUserIdProvider.cs b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Infrastructure/InMemoryUserIdProvider.cs index 8ae70e3c..7f5c452f 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Infrastructure/InMemoryUserIdProvider.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Infrastructure/InMemoryUserIdProvider.cs @@ -1,15 +1,13 @@ using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Core.Infrastructure; -namespace CodeBeam.UltimateAuth.Users.InMemory -{ - public sealed class InMemoryUserIdProvider : IInMemoryUserIdProvider - { - private static readonly UserKey Admin = UserKey.FromString("admin"); - private static readonly UserKey User = UserKey.FromString("user"); +namespace CodeBeam.UltimateAuth.Users.InMemory; - public UserKey GetAdminUserId() => Admin; - public UserKey GetUserUserId() => User; - } +public sealed class InMemoryUserIdProvider : IInMemoryUserIdProvider +{ + private static readonly UserKey Admin = UserKey.FromString("admin"); + private static readonly UserKey User = UserKey.FromString("user"); + public UserKey GetAdminUserId() => Admin; + public UserKey GetUserUserId() => User; } diff --git a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Infrastructure/InMemoryUserSeedContributor.cs b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Infrastructure/InMemoryUserSeedContributor.cs index a0945010..61e694c0 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Infrastructure/InMemoryUserSeedContributor.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Infrastructure/InMemoryUserSeedContributor.cs @@ -5,70 +5,68 @@ using CodeBeam.UltimateAuth.Users.Contracts; using CodeBeam.UltimateAuth.Users.Reference; -namespace CodeBeam.UltimateAuth.Users.InMemory +namespace CodeBeam.UltimateAuth.Users.InMemory; + +internal sealed class InMemoryUserSeedContributor : ISeedContributor { - internal sealed class InMemoryUserSeedContributor : ISeedContributor - { - public int Order => 0; + public int Order => 0; - private readonly IUserLifecycleStore _lifecycle; - private readonly IUserProfileStore _profiles; - private readonly IUserIdentifierStore _identifiers; - private readonly IInMemoryUserIdProvider _ids; - private readonly IClock _clock; + private readonly IUserLifecycleStore _lifecycle; + private readonly IUserProfileStore _profiles; + private readonly IUserIdentifierStore _identifiers; + private readonly IInMemoryUserIdProvider _ids; + private readonly IClock _clock; - public InMemoryUserSeedContributor( - IUserLifecycleStore lifecycle, - IUserProfileStore profiles, - IUserIdentifierStore identifiers, - IInMemoryUserIdProvider ids, - IClock clock) - { - _lifecycle = lifecycle; - _profiles = profiles; - _ids = ids; - _identifiers = identifiers; - _clock = clock; - } + public InMemoryUserSeedContributor( + IUserLifecycleStore lifecycle, + IUserProfileStore profiles, + IUserIdentifierStore identifiers, + IInMemoryUserIdProvider ids, + IClock clock) + { + _lifecycle = lifecycle; + _profiles = profiles; + _ids = ids; + _identifiers = identifiers; + _clock = clock; + } - public async Task SeedAsync(TenantKey tenant, CancellationToken ct = default) - { - await SeedUserAsync(tenant, _ids.GetAdminUserId(), "Administrator", "admin", ct); - await SeedUserAsync(tenant, _ids.GetUserUserId(), "User", "user", ct); - } + public async Task SeedAsync(TenantKey tenant, CancellationToken ct = default) + { + await SeedUserAsync(tenant, _ids.GetAdminUserId(), "Administrator", "admin", ct); + await SeedUserAsync(tenant, _ids.GetUserUserId(), "User", "user", ct); + } - private async Task SeedUserAsync(TenantKey tenant, UserKey userKey, string displayName, string username, CancellationToken ct) - { - if (await _lifecycle.ExistsAsync(tenant, userKey, ct)) - return; + private async Task SeedUserAsync(TenantKey tenant, UserKey userKey, string displayName, string username, CancellationToken ct) + { + if (await _lifecycle.ExistsAsync(tenant, userKey, ct)) + return; - await _lifecycle.CreateAsync(tenant, - new UserLifecycle - { - UserKey = userKey, - Status = UserStatus.Active, - CreatedAt = _clock.UtcNow - }, ct); + await _lifecycle.CreateAsync(tenant, + new UserLifecycle + { + UserKey = userKey, + Status = UserStatus.Active, + CreatedAt = _clock.UtcNow + }, ct); - await _profiles.CreateAsync(tenant, - new UserProfile - { - UserKey = userKey, - DisplayName = displayName, - CreatedAt = _clock.UtcNow - }, ct); + await _profiles.CreateAsync(tenant, + new UserProfile + { + UserKey = userKey, + DisplayName = displayName, + CreatedAt = _clock.UtcNow + }, ct); - await _identifiers.CreateAsync(tenant, - new UserIdentifier - { - UserKey = userKey, - Type = UserIdentifierType.Username, - Value = username, - IsPrimary = true, - IsVerified = true, - CreatedAt = _clock.UtcNow - }, ct); - } + await _identifiers.CreateAsync(tenant, + new UserIdentifier + { + UserKey = userKey, + Type = UserIdentifierType.Username, + Value = username, + IsPrimary = true, + IsVerified = true, + CreatedAt = _clock.UtcNow + }, ct); } - } diff --git a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserIdentifierStore.cs b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserIdentifierStore.cs index 9358fa82..36733c96 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserIdentifierStore.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserIdentifierStore.cs @@ -4,179 +4,178 @@ using CodeBeam.UltimateAuth.Users.Contracts; using CodeBeam.UltimateAuth.Users.Reference; -namespace CodeBeam.UltimateAuth.Users.InMemory +namespace CodeBeam.UltimateAuth.Users.InMemory; + +public sealed class InMemoryUserIdentifierStore : IUserIdentifierStore { - public sealed class InMemoryUserIdentifierStore : IUserIdentifierStore + private readonly Dictionary<(TenantKey Tenant, UserIdentifierType Type, string Value), UserIdentifier> _store = new(); + + public Task ExistsAsync(TenantKey tenant, UserIdentifierType type, string value, CancellationToken ct = default) { - private readonly Dictionary<(TenantKey Tenant, UserIdentifierType Type, string Value), UserIdentifier> _store = new(); + return Task.FromResult(_store.TryGetValue((tenant, type, value), out var id) && !id.IsDeleted); + } - public Task ExistsAsync(TenantKey tenant, UserIdentifierType type, string value, CancellationToken ct = default) - { - return Task.FromResult(_store.TryGetValue((tenant, type, value), out var id) && !id.IsDeleted); - } + public Task GetAsync(TenantKey tenant, UserIdentifierType type, string value, CancellationToken ct = default) + { + if (!_store.TryGetValue((tenant, type, value), out var id)) + return Task.FromResult(null); - public Task GetAsync(TenantKey tenant, UserIdentifierType type, string value, CancellationToken ct = default) - { - if (!_store.TryGetValue((tenant, type, value), out var id)) - return Task.FromResult(null); + if (id.IsDeleted) + return Task.FromResult(null); - if (id.IsDeleted) - return Task.FromResult(null); + return Task.FromResult(id); + } - return Task.FromResult(id); - } + public Task> GetByUserAsync(TenantKey tenant, UserKey userKey, CancellationToken ct = default) + { + var result = _store.Values + .Where(x => x.Tenant == tenant) + .Where(x => x.UserKey == userKey) + .Where(x => !x.IsDeleted) + .OrderByDescending(x => x.IsPrimary) + .ThenBy(x => x.CreatedAt) + .ToList() + .AsReadOnly(); + + return Task.FromResult>(result); + } - public Task> GetByUserAsync(TenantKey tenant, UserKey userKey, CancellationToken ct = default) - { - var result = _store.Values - .Where(x => x.Tenant == tenant) - .Where(x => x.UserKey == userKey) - .Where(x => !x.IsDeleted) - .OrderByDescending(x => x.IsPrimary) - .ThenBy(x => x.CreatedAt) - .ToList() - .AsReadOnly(); - - return Task.FromResult>(result); - } + public Task CreateAsync(TenantKey tenant, UserIdentifier identifier, CancellationToken ct = default) + { + var key = (tenant, identifier.Type, identifier.Value); - public Task CreateAsync(TenantKey tenant, UserIdentifier identifier, CancellationToken ct = default) - { - var key = (tenant, identifier.Type, identifier.Value); + if (_store.TryGetValue(key, out var existing) && !existing.IsDeleted) + throw new InvalidOperationException("Identifier already exists."); - if (_store.TryGetValue(key, out var existing) && !existing.IsDeleted) - throw new InvalidOperationException("Identifier already exists."); + _store[key] = identifier; + return Task.CompletedTask; + } - _store[key] = identifier; - return Task.CompletedTask; - } + public Task UpdateValueAsync(TenantKey tenant, UserIdentifierType type, string oldValue, string newValue, DateTimeOffset updatedAt, CancellationToken ct = default) + { + ct.ThrowIfCancellationRequested(); - public Task UpdateValueAsync(TenantKey tenant, UserIdentifierType type, string oldValue, string newValue, DateTimeOffset updatedAt, CancellationToken ct = default) - { - ct.ThrowIfCancellationRequested(); + if (string.Equals(oldValue, newValue, StringComparison.Ordinal)) + throw new InvalidOperationException("identifier_value_unchanged"); - if (string.Equals(oldValue, newValue, StringComparison.Ordinal)) - throw new InvalidOperationException("identifier_value_unchanged"); + var oldKey = (tenant, type, oldValue); - var oldKey = (tenant, type, oldValue); + if (!_store.TryGetValue(oldKey, out var identifier) || identifier.IsDeleted) + throw new InvalidOperationException("identifier_not_found"); - if (!_store.TryGetValue(oldKey, out var identifier) || identifier.IsDeleted) - throw new InvalidOperationException("identifier_not_found"); + var newKey = (tenant, type, newValue); - var newKey = (tenant, type, newValue); + if (_store.ContainsKey(newKey)) + throw new InvalidOperationException("identifier_value_already_exists"); - if (_store.ContainsKey(newKey)) - throw new InvalidOperationException("identifier_value_already_exists"); + _store.Remove(oldKey); - _store.Remove(oldKey); + identifier.Value = newValue; + identifier.IsVerified = false; + identifier.VerifiedAt = null; + identifier.UpdatedAt = updatedAt; - identifier.Value = newValue; - identifier.IsVerified = false; - identifier.VerifiedAt = null; - identifier.UpdatedAt = updatedAt; + _store[newKey] = identifier; - _store[newKey] = identifier; + return Task.CompletedTask; + } - return Task.CompletedTask; - } + public Task MarkVerifiedAsync(TenantKey tenant, UserIdentifierType type, string value, DateTimeOffset verifiedAt, CancellationToken ct = default) + { + var key = (tenant, type, value); - public Task MarkVerifiedAsync(TenantKey tenant, UserIdentifierType type, string value, DateTimeOffset verifiedAt, CancellationToken ct = default) - { - var key = (tenant, type, value); + if (!_store.TryGetValue(key, out var id) || id.IsDeleted) + throw new InvalidOperationException("Identifier not found."); - if (!_store.TryGetValue(key, out var id) || id.IsDeleted) - throw new InvalidOperationException("Identifier not found."); + if (id.IsVerified) + return Task.CompletedTask; - if (id.IsVerified) - return Task.CompletedTask; + id.IsVerified = true; + id.VerifiedAt = verifiedAt; - id.IsVerified = true; - id.VerifiedAt = verifiedAt; + return Task.CompletedTask; + } - return Task.CompletedTask; + public Task SetPrimaryAsync(TenantKey tenant, UserKey userKey, UserIdentifierType type, string value, CancellationToken ct = default) + { + foreach (var id in _store.Values.Where(x => + x.Tenant == tenant && + x.UserKey == userKey && + x.Type == type && + !x.IsDeleted && + x.IsPrimary)) + { + id.IsPrimary = false; } - public Task SetPrimaryAsync(TenantKey tenant, UserKey userKey, UserIdentifierType type, string value, CancellationToken ct = default) - { - foreach (var id in _store.Values.Where(x => - x.Tenant == tenant && - x.UserKey == userKey && - x.Type == type && - !x.IsDeleted && - x.IsPrimary)) - { - id.IsPrimary = false; - } + var key = (tenant, type, value); - var key = (tenant, type, value); + if (!_store.TryGetValue(key, out var target) || target.IsDeleted) + throw new InvalidOperationException("Identifier not found."); - if (!_store.TryGetValue(key, out var target) || target.IsDeleted) - throw new InvalidOperationException("Identifier not found."); + target.IsPrimary = true; + return Task.CompletedTask; + } - target.IsPrimary = true; - return Task.CompletedTask; - } + public Task UnsetPrimaryAsync(TenantKey tenant, UserKey userKey, UserIdentifierType type, string value, CancellationToken ct = default) + { + var key = (tenant, type, value); - public Task UnsetPrimaryAsync(TenantKey tenant, UserKey userKey, UserIdentifierType type, string value, CancellationToken ct = default) - { - var key = (tenant, type, value); + if (!_store.TryGetValue(key, out var target) || target.IsDeleted) + throw new InvalidOperationException("Identifier not found."); - if (!_store.TryGetValue(key, out var target) || target.IsDeleted) - throw new InvalidOperationException("Identifier not found."); + target.IsPrimary = false; + return Task.CompletedTask; + } + + public Task DeleteAsync(TenantKey tenant, UserIdentifierType type, string value, DeleteMode mode, DateTimeOffset deletedAt, CancellationToken ct = default) + { + var key = (tenant, type, value); - target.IsPrimary = false; + if (!_store.TryGetValue(key, out var id)) return Task.CompletedTask; - } - public Task DeleteAsync(TenantKey tenant, UserIdentifierType type, string value, DeleteMode mode, DateTimeOffset deletedAt, CancellationToken ct = default) + if (mode == DeleteMode.Hard) { - var key = (tenant, type, value); - - if (!_store.TryGetValue(key, out var id)) - return Task.CompletedTask; + _store.Remove(key); + return Task.CompletedTask; + } - if (mode == DeleteMode.Hard) - { - _store.Remove(key); - return Task.CompletedTask; - } + if (id.IsDeleted) + return Task.CompletedTask; - if (id.IsDeleted) - return Task.CompletedTask; + id.IsDeleted = true; + id.DeletedAt = deletedAt; + id.IsPrimary = false; - id.IsDeleted = true; - id.DeletedAt = deletedAt; - id.IsPrimary = false; + return Task.CompletedTask; + } - return Task.CompletedTask; - } + public Task DeleteByUserAsync(TenantKey tenant, UserKey userKey, DeleteMode mode, DateTimeOffset deletedAt, CancellationToken ct = default) + { + var identifiers = _store.Values + .Where(x => x.Tenant == tenant) + .Where(x => x.UserKey == userKey) + .ToList(); - public Task DeleteByUserAsync(TenantKey tenant, UserKey userKey, DeleteMode mode, DateTimeOffset deletedAt, CancellationToken ct = default) + foreach (var id in identifiers) { - var identifiers = _store.Values - .Where(x => x.Tenant == tenant) - .Where(x => x.UserKey == userKey) - .ToList(); - - foreach (var id in identifiers) + if (mode == DeleteMode.Hard) { - if (mode == DeleteMode.Hard) - { - _store.Remove((tenant, id.Type, id.Value)); - } - else - { - if (id.IsDeleted) - continue; - - id.IsDeleted = true; - id.DeletedAt = deletedAt; - id.IsPrimary = false; - } + _store.Remove((tenant, id.Type, id.Value)); } + else + { + if (id.IsDeleted) + continue; - return Task.CompletedTask; + id.IsDeleted = true; + id.DeletedAt = deletedAt; + id.IsPrimary = false; + } } + return Task.CompletedTask; } + } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/AddUserIdentifierCommand.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/AddUserIdentifierCommand.cs index c35ef39c..1b7b8e20 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/AddUserIdentifierCommand.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/AddUserIdentifierCommand.cs @@ -1,16 +1,15 @@ using CodeBeam.UltimateAuth.Server.Infrastructure; -namespace CodeBeam.UltimateAuth.Users.Reference -{ - internal sealed class AddUserIdentifierCommand : IAccessCommand - { - private readonly Func _execute; +namespace CodeBeam.UltimateAuth.Users.Reference; - public AddUserIdentifierCommand(Func execute) - { - _execute = execute; - } +internal sealed class AddUserIdentifierCommand : IAccessCommand +{ + private readonly Func _execute; - public Task ExecuteAsync(CancellationToken ct = default) => _execute(ct); + public AddUserIdentifierCommand(Func execute) + { + _execute = execute; } + + public Task ExecuteAsync(CancellationToken ct = default) => _execute(ct); } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/ChangeUserStatusCommand.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/ChangeUserStatusCommand.cs index d6299789..954c6fa6 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/ChangeUserStatusCommand.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/ChangeUserStatusCommand.cs @@ -1,18 +1,15 @@ -using CodeBeam.UltimateAuth.Core.Abstractions; -using CodeBeam.UltimateAuth.Core.Contracts; -using CodeBeam.UltimateAuth.Server.Infrastructure; +using CodeBeam.UltimateAuth.Server.Infrastructure; -namespace CodeBeam.UltimateAuth.Users.Reference -{ - internal sealed class ChangeUserStatusCommand : IAccessCommand - { - private readonly Func _execute; +namespace CodeBeam.UltimateAuth.Users.Reference; - public ChangeUserStatusCommand(Func execute) - { - _execute = execute; - } +internal sealed class ChangeUserStatusCommand : IAccessCommand +{ + private readonly Func _execute; - public Task ExecuteAsync(CancellationToken ct = default) => _execute(ct); + public ChangeUserStatusCommand(Func execute) + { + _execute = execute; } + + public Task ExecuteAsync(CancellationToken ct = default) => _execute(ct); } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/CreateUserCommand.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/CreateUserCommand.cs index b29f57d5..675ebde9 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/CreateUserCommand.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/CreateUserCommand.cs @@ -1,19 +1,16 @@ -using CodeBeam.UltimateAuth.Core.Abstractions; -using CodeBeam.UltimateAuth.Core.Contracts; -using CodeBeam.UltimateAuth.Server.Infrastructure; +using CodeBeam.UltimateAuth.Server.Infrastructure; using CodeBeam.UltimateAuth.Users.Contracts; -namespace CodeBeam.UltimateAuth.Users.Reference -{ - internal sealed class CreateUserCommand : IAccessCommand - { - private readonly Func> _execute; +namespace CodeBeam.UltimateAuth.Users.Reference; - public CreateUserCommand(Func> execute) - { - _execute = execute; - } +internal sealed class CreateUserCommand : IAccessCommand +{ + private readonly Func> _execute; - public Task ExecuteAsync(CancellationToken ct = default) => _execute(ct); + public CreateUserCommand(Func> execute) + { + _execute = execute; } + + public Task ExecuteAsync(CancellationToken ct = default) => _execute(ct); } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/DeleteUserCommand.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/DeleteUserCommand.cs index 96039d38..da4acc96 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/DeleteUserCommand.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/DeleteUserCommand.cs @@ -1,18 +1,15 @@ -using CodeBeam.UltimateAuth.Core.Abstractions; -using CodeBeam.UltimateAuth.Core.Contracts; -using CodeBeam.UltimateAuth.Server.Infrastructure; +using CodeBeam.UltimateAuth.Server.Infrastructure; -namespace CodeBeam.UltimateAuth.Users.Reference -{ - internal sealed class DeleteUserCommand : IAccessCommand - { - private readonly Func _execute; +namespace CodeBeam.UltimateAuth.Users.Reference; - public DeleteUserCommand(Func execute) - { - _execute = execute; - } +internal sealed class DeleteUserCommand : IAccessCommand +{ + private readonly Func _execute; - public Task ExecuteAsync(CancellationToken ct = default) => _execute(ct); + public DeleteUserCommand(Func execute) + { + _execute = execute; } + + public Task ExecuteAsync(CancellationToken ct = default) => _execute(ct); } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/DeleteUserIdentifierCommand.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/DeleteUserIdentifierCommand.cs index 12f6b722..59b9d079 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/DeleteUserIdentifierCommand.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/DeleteUserIdentifierCommand.cs @@ -1,18 +1,15 @@ -using CodeBeam.UltimateAuth.Core.Abstractions; -using CodeBeam.UltimateAuth.Core.Contracts; -using CodeBeam.UltimateAuth.Server.Infrastructure; +using CodeBeam.UltimateAuth.Server.Infrastructure; -namespace CodeBeam.UltimateAuth.Users.Reference -{ - internal sealed class DeleteUserIdentifierCommand : IAccessCommand - { - private readonly Func _execute; +namespace CodeBeam.UltimateAuth.Users.Reference; - public DeleteUserIdentifierCommand(Func execute) - { - _execute = execute; - } +internal sealed class DeleteUserIdentifierCommand : IAccessCommand +{ + private readonly Func _execute; - public Task ExecuteAsync(CancellationToken ct = default) => _execute(ct); + public DeleteUserIdentifierCommand(Func execute) + { + _execute = execute; } + + public Task ExecuteAsync(CancellationToken ct = default) => _execute(ct); } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/GetMeCommand.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/GetMeCommand.cs index 813db3dd..90302ae1 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/GetMeCommand.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/GetMeCommand.cs @@ -1,6 +1,4 @@ -using CodeBeam.UltimateAuth.Core.Abstractions; -using CodeBeam.UltimateAuth.Core.Contracts; -using CodeBeam.UltimateAuth.Server.Infrastructure; +using CodeBeam.UltimateAuth.Server.Infrastructure; using CodeBeam.UltimateAuth.Users.Contracts; namespace CodeBeam.UltimateAuth.Users.Reference; diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/GetUserIdentifierCommand.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/GetUserIdentifierCommand.cs index 8da5b649..d5eb10f2 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/GetUserIdentifierCommand.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/GetUserIdentifierCommand.cs @@ -1,22 +1,16 @@ using CodeBeam.UltimateAuth.Server.Infrastructure; using CodeBeam.UltimateAuth.Users.Contracts; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -namespace CodeBeam.UltimateAuth.Users.Reference -{ - internal sealed class GetUserIdentifierCommand : IAccessCommand - { - private readonly Func> _execute; +namespace CodeBeam.UltimateAuth.Users.Reference; - public GetUserIdentifierCommand(Func> execute) - { - _execute = execute; - } +internal sealed class GetUserIdentifierCommand : IAccessCommand +{ + private readonly Func> _execute; - public Task ExecuteAsync(CancellationToken ct = default) => _execute(ct); + public GetUserIdentifierCommand(Func> execute) + { + _execute = execute; } + + public Task ExecuteAsync(CancellationToken ct = default) => _execute(ct); } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/GetUserIdentifiersCommand.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/GetUserIdentifiersCommand.cs index b931025e..a8219862 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/GetUserIdentifiersCommand.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/GetUserIdentifiersCommand.cs @@ -1,19 +1,16 @@ -using CodeBeam.UltimateAuth.Core.Abstractions; -using CodeBeam.UltimateAuth.Core.Contracts; -using CodeBeam.UltimateAuth.Server.Infrastructure; +using CodeBeam.UltimateAuth.Server.Infrastructure; using CodeBeam.UltimateAuth.Users.Contracts; -namespace CodeBeam.UltimateAuth.Users.Reference -{ - internal sealed class GetUserIdentifiersCommand : IAccessCommand> - { - private readonly Func>> _execute; +namespace CodeBeam.UltimateAuth.Users.Reference; - public GetUserIdentifiersCommand(Func>> execute) - { - _execute = execute; - } +internal sealed class GetUserIdentifiersCommand : IAccessCommand> +{ + private readonly Func>> _execute; - public Task> ExecuteAsync(CancellationToken ct = default) => _execute(ct); + public GetUserIdentifiersCommand(Func>> execute) + { + _execute = execute; } + + public Task> ExecuteAsync(CancellationToken ct = default) => _execute(ct); } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/GetUserProfileCommand.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/GetUserProfileCommand.cs index 4912a6b3..82e7fe12 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/GetUserProfileCommand.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/GetUserProfileCommand.cs @@ -1,6 +1,4 @@ -using CodeBeam.UltimateAuth.Core.Abstractions; -using CodeBeam.UltimateAuth.Core.Contracts; -using CodeBeam.UltimateAuth.Server.Infrastructure; +using CodeBeam.UltimateAuth.Server.Infrastructure; using CodeBeam.UltimateAuth.Users.Contracts; namespace CodeBeam.UltimateAuth.Users.Reference; diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/SetPrimaryUserIdentifierCommand.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/SetPrimaryUserIdentifierCommand.cs index 800d6c0e..8a56df8c 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/SetPrimaryUserIdentifierCommand.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/SetPrimaryUserIdentifierCommand.cs @@ -1,16 +1,15 @@ using CodeBeam.UltimateAuth.Server.Infrastructure; -namespace CodeBeam.UltimateAuth.Users.Reference -{ - internal sealed class SetPrimaryUserIdentifierCommand : IAccessCommand - { - private readonly Func _execute; +namespace CodeBeam.UltimateAuth.Users.Reference; - public SetPrimaryUserIdentifierCommand(Func execute) - { - _execute = execute; - } +internal sealed class SetPrimaryUserIdentifierCommand : IAccessCommand +{ + private readonly Func _execute; - public Task ExecuteAsync(CancellationToken ct = default) => _execute(ct); + public SetPrimaryUserIdentifierCommand(Func execute) + { + _execute = execute; } + + public Task ExecuteAsync(CancellationToken ct = default) => _execute(ct); } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/UnsetPrimaryUserIdentifierCommand.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/UnsetPrimaryUserIdentifierCommand.cs index f7f21b72..48a7ad89 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/UnsetPrimaryUserIdentifierCommand.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/UnsetPrimaryUserIdentifierCommand.cs @@ -1,16 +1,15 @@ using CodeBeam.UltimateAuth.Server.Infrastructure; -namespace CodeBeam.UltimateAuth.Users.Reference -{ - internal sealed class UnsetPrimaryUserIdentifierCommand : IAccessCommand - { - private readonly Func _execute; +namespace CodeBeam.UltimateAuth.Users.Reference; - public UnsetPrimaryUserIdentifierCommand(Func execute) - { - _execute = execute; - } +internal sealed class UnsetPrimaryUserIdentifierCommand : IAccessCommand +{ + private readonly Func _execute; - public Task ExecuteAsync(CancellationToken ct = default) => _execute(ct); + public UnsetPrimaryUserIdentifierCommand(Func execute) + { + _execute = execute; } + + public Task ExecuteAsync(CancellationToken ct = default) => _execute(ct); } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/UpdateUserIdentifierCommand.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/UpdateUserIdentifierCommand.cs index d2521fad..1005453d 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/UpdateUserIdentifierCommand.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/UpdateUserIdentifierCommand.cs @@ -1,16 +1,15 @@ using CodeBeam.UltimateAuth.Server.Infrastructure; -namespace CodeBeam.UltimateAuth.Users.Reference -{ - internal sealed class UpdateUserIdentifierCommand : IAccessCommand - { - private readonly Func _execute; +namespace CodeBeam.UltimateAuth.Users.Reference; - public UpdateUserIdentifierCommand(Func execute) - { - _execute = execute; - } +internal sealed class UpdateUserIdentifierCommand : IAccessCommand +{ + private readonly Func _execute; - public Task ExecuteAsync(CancellationToken ct = default) => _execute(ct); + public UpdateUserIdentifierCommand(Func execute) + { + _execute = execute; } + + public Task ExecuteAsync(CancellationToken ct = default) => _execute(ct); } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/UpdateUserProfileAdminCommand.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/UpdateUserProfileAdminCommand.cs index b9d550c3..aa38706a 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/UpdateUserProfileAdminCommand.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/UpdateUserProfileAdminCommand.cs @@ -1,6 +1,4 @@ -using CodeBeam.UltimateAuth.Core.Abstractions; -using CodeBeam.UltimateAuth.Core.Contracts; -using CodeBeam.UltimateAuth.Server.Infrastructure; +using CodeBeam.UltimateAuth.Server.Infrastructure; namespace CodeBeam.UltimateAuth.Users.Reference; diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/UserIdentifierExistsCommand.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/UserIdentifierExistsCommand.cs index 7cd93350..28602a36 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/UserIdentifierExistsCommand.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/UserIdentifierExistsCommand.cs @@ -1,21 +1,15 @@ using CodeBeam.UltimateAuth.Server.Infrastructure; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -namespace CodeBeam.UltimateAuth.Users.Reference.Commands -{ - internal sealed class UserIdentifierExistsCommand : IAccessCommand - { - private readonly Func> _execute; +namespace CodeBeam.UltimateAuth.Users.Reference; - public UserIdentifierExistsCommand(Func> execute) - { - _execute = execute; - } +internal sealed class UserIdentifierExistsCommand : IAccessCommand +{ + private readonly Func> _execute; - public Task ExecuteAsync(CancellationToken ct = default) => _execute(ct); + public UserIdentifierExistsCommand(Func> execute) + { + _execute = execute; } + + public Task ExecuteAsync(CancellationToken ct = default) => _execute(ct); } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/VerifyUserIdentifierCommand.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/VerifyUserIdentifierCommand.cs index 8e4f8985..186433d6 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/VerifyUserIdentifierCommand.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Commands/VerifyUserIdentifierCommand.cs @@ -1,18 +1,15 @@ -using CodeBeam.UltimateAuth.Core.Abstractions; -using CodeBeam.UltimateAuth.Core.Contracts; -using CodeBeam.UltimateAuth.Server.Infrastructure; +using CodeBeam.UltimateAuth.Server.Infrastructure; -namespace CodeBeam.UltimateAuth.Users.Reference -{ - internal sealed class VerifyUserIdentifierCommand : IAccessCommand - { - private readonly Func _execute; +namespace CodeBeam.UltimateAuth.Users.Reference; - public VerifyUserIdentifierCommand(Func execute) - { - _execute = execute; - } +internal sealed class VerifyUserIdentifierCommand : IAccessCommand +{ + private readonly Func _execute; - public Task ExecuteAsync(CancellationToken ct = default) => _execute(ct); + public VerifyUserIdentifierCommand(Func execute) + { + _execute = execute; } + + public Task ExecuteAsync(CancellationToken ct = default) => _execute(ct); } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Contracts/UserLifecycleQuery.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Contracts/UserLifecycleQuery.cs index 980c39f9..a6c6a944 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Contracts/UserLifecycleQuery.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Contracts/UserLifecycleQuery.cs @@ -1,13 +1,12 @@ using CodeBeam.UltimateAuth.Users.Contracts; -namespace CodeBeam.UltimateAuth.Users.Reference +namespace CodeBeam.UltimateAuth.Users.Reference; + +public sealed class UserLifecycleQuery { - public sealed class UserLifecycleQuery - { - public bool IncludeDeleted { get; init; } - public UserStatus? Status { get; init; } + public bool IncludeDeleted { get; init; } + public UserStatus? Status { get; init; } - public int Skip { get; init; } - public int Take { get; init; } = 50; - } + public int Skip { get; init; } + public int Take { get; init; } = 50; } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Contracts/UserProfileQuery.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Contracts/UserProfileQuery.cs index 9bc8a3ae..d0cd9262 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Contracts/UserProfileQuery.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Contracts/UserProfileQuery.cs @@ -1,10 +1,9 @@ -namespace CodeBeam.UltimateAuth.Users.Reference +namespace CodeBeam.UltimateAuth.Users.Reference; + +public sealed class UserProfileQuery { - public sealed class UserProfileQuery - { - public bool IncludeDeleted { get; init; } + public bool IncludeDeleted { get; init; } - public int Skip { get; init; } - public int Take { get; init; } = 50; - } + public int Skip { get; init; } + public int Take { get; init; } = 50; } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Endpoints/DefaultUserEndpointHandler.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Endpoints/UserEndpointHandler.cs similarity index 98% rename from src/users/CodeBeam.UltimateAuth.Users.Reference/Endpoints/DefaultUserEndpointHandler.cs rename to src/users/CodeBeam.UltimateAuth.Users.Reference/Endpoints/UserEndpointHandler.cs index 87b99025..3b67efe7 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Endpoints/DefaultUserEndpointHandler.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Endpoints/UserEndpointHandler.cs @@ -8,13 +8,13 @@ namespace CodeBeam.UltimateAuth.Users.Reference; -public sealed class DefaultUserEndpointHandler : IUserEndpointHandler +public sealed class UserEndpointHandler : IUserEndpointHandler { private readonly IAuthFlowContextAccessor _authFlow; private readonly IAccessContextFactory _accessContextFactory; private readonly IUserApplicationService _users; - public DefaultUserEndpointHandler(IAuthFlowContextAccessor authFlow, IAccessContextFactory accessContextFactory, IUserApplicationService users) + public UserEndpointHandler(IAuthFlowContextAccessor authFlow, IAccessContextFactory accessContextFactory, IUserApplicationService users) { _authFlow = authFlow; _accessContextFactory = accessContextFactory; diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Extensions/ServiceCollectonExtensions.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Extensions/ServiceCollectonExtensions.cs index 30df9de9..80d6bbc3 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Extensions/ServiceCollectonExtensions.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Extensions/ServiceCollectonExtensions.cs @@ -16,7 +16,7 @@ public static IServiceCollection AddUltimateAuthUsersReference(this IServiceColl services.TryAddScoped(); services.TryAddScoped(); - services.TryAddScoped(); + services.TryAddScoped(); return services; } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Mapping/UserIdentifierMapper.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Mapping/UserIdentifierMapper.cs index ccfee913..1c3df9c4 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Mapping/UserIdentifierMapper.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Mapping/UserIdentifierMapper.cs @@ -1,18 +1,17 @@ using CodeBeam.UltimateAuth.Users.Contracts; -namespace CodeBeam.UltimateAuth.Users.Reference +namespace CodeBeam.UltimateAuth.Users.Reference; + +public static class UserIdentifierMapper { - public static class UserIdentifierMapper - { - public static UserIdentifierDto ToDto(UserIdentifier record) - => new() - { - Type = record.Type, - Value = record.Value, - IsPrimary = record.IsPrimary, - IsVerified = record.IsVerified, - CreatedAt = record.CreatedAt, - VerifiedAt = record.VerifiedAt - }; - } + public static UserIdentifierDto ToDto(UserIdentifier record) + => new() + { + Type = record.Type, + Value = record.Value, + IsPrimary = record.IsPrimary, + IsVerified = record.IsVerified, + CreatedAt = record.CreatedAt, + VerifiedAt = record.VerifiedAt + }; } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Mapping/UserProfileMapper.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Mapping/UserProfileMapper.cs index 116083b5..5d0a536c 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Mapping/UserProfileMapper.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Mapping/UserProfileMapper.cs @@ -19,18 +19,17 @@ public static UserViewDto ToDto(UserProfile profile) }; public static UserProfileUpdate ToUpdate(UpdateProfileRequest request) - => new() - { - FirstName = request.FirstName, - LastName = request.LastName, - DisplayName = request.DisplayName, - BirthDate = request.BirthDate, - Gender = request.Gender, - Bio = request.Bio, - Language = request.Language, - TimeZone = request.TimeZone, - Culture = request.Culture, - Metadata = request.Metadata - }; - + => new() + { + FirstName = request.FirstName, + LastName = request.LastName, + DisplayName = request.DisplayName, + BirthDate = request.BirthDate, + Gender = request.Gender, + Bio = request.Bio, + Language = request.Language, + TimeZone = request.TimeZone, + Culture = request.Culture, + Metadata = request.Metadata + }; } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Services/IUserApplicationService.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Services/IUserApplicationService.cs index 494ad49c..b01bc97f 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Services/IUserApplicationService.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Services/IUserApplicationService.cs @@ -1,37 +1,36 @@ using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Users.Contracts; -namespace CodeBeam.UltimateAuth.Users.Reference +namespace CodeBeam.UltimateAuth.Users.Reference; + +public interface IUserApplicationService { - public interface IUserApplicationService - { - Task GetMeAsync(AccessContext context, CancellationToken ct = default); - Task GetUserProfileAsync(AccessContext context, CancellationToken ct = default); + Task GetMeAsync(AccessContext context, CancellationToken ct = default); + Task GetUserProfileAsync(AccessContext context, CancellationToken ct = default); - Task CreateUserAsync(AccessContext context, CreateUserRequest request, CancellationToken ct = default); + Task CreateUserAsync(AccessContext context, CreateUserRequest request, CancellationToken ct = default); - Task ChangeUserStatusAsync(AccessContext context, object request, CancellationToken ct = default); + Task ChangeUserStatusAsync(AccessContext context, object request, CancellationToken ct = default); - Task UpdateUserProfileAsync(AccessContext context, UpdateProfileRequest request, CancellationToken ct = default); + Task UpdateUserProfileAsync(AccessContext context, UpdateProfileRequest request, CancellationToken ct = default); - Task> GetIdentifiersByUserAsync(AccessContext context, CancellationToken ct = default); + Task> GetIdentifiersByUserAsync(AccessContext context, CancellationToken ct = default); - Task GetIdentifierAsync(AccessContext context, UserIdentifierType type, string value, CancellationToken ct = default); + Task GetIdentifierAsync(AccessContext context, UserIdentifierType type, string value, CancellationToken ct = default); - Task UserIdentifierExistsAsync(AccessContext context, UserIdentifierType type, string value, CancellationToken ct = default); + Task UserIdentifierExistsAsync(AccessContext context, UserIdentifierType type, string value, CancellationToken ct = default); - Task AddUserIdentifierAsync(AccessContext context, AddUserIdentifierRequest request, CancellationToken ct = default); + Task AddUserIdentifierAsync(AccessContext context, AddUserIdentifierRequest request, CancellationToken ct = default); - Task UpdateUserIdentifierAsync(AccessContext context, UpdateUserIdentifierRequest request, CancellationToken ct = default); + Task UpdateUserIdentifierAsync(AccessContext context, UpdateUserIdentifierRequest request, CancellationToken ct = default); - Task SetPrimaryUserIdentifierAsync(AccessContext context, SetPrimaryUserIdentifierRequest request, CancellationToken ct = default); + Task SetPrimaryUserIdentifierAsync(AccessContext context, SetPrimaryUserIdentifierRequest request, CancellationToken ct = default); - Task UnsetPrimaryUserIdentifierAsync(AccessContext context, UnsetPrimaryUserIdentifierRequest request, CancellationToken ct = default); + Task UnsetPrimaryUserIdentifierAsync(AccessContext context, UnsetPrimaryUserIdentifierRequest request, CancellationToken ct = default); - Task VerifyUserIdentifierAsync(AccessContext context, VerifyUserIdentifierRequest request, CancellationToken ct = default); + Task VerifyUserIdentifierAsync(AccessContext context, VerifyUserIdentifierRequest request, CancellationToken ct = default); - Task DeleteUserIdentifierAsync(AccessContext context, DeleteUserIdentifierRequest request, CancellationToken ct = default); + Task DeleteUserIdentifierAsync(AccessContext context, DeleteUserIdentifierRequest request, CancellationToken ct = default); - Task DeleteUserAsync(AccessContext context, DeleteUserRequest request, CancellationToken ct = default); - } + Task DeleteUserAsync(AccessContext context, DeleteUserRequest request, CancellationToken ct = default); } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Services/UserApplicationService.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Services/UserApplicationService.cs index c22ffd3e..179ed6f2 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Services/UserApplicationService.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Services/UserApplicationService.cs @@ -5,7 +5,6 @@ using CodeBeam.UltimateAuth.Server.Infrastructure; using CodeBeam.UltimateAuth.Users.Abstractions; using CodeBeam.UltimateAuth.Users.Contracts; -using CodeBeam.UltimateAuth.Users.Reference.Commands; namespace CodeBeam.UltimateAuth.Users.Reference; diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Stores/IUserLifecycleStore.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Stores/IUserLifecycleStore.cs index 2e2894ad..38b0c42f 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Stores/IUserLifecycleStore.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Stores/IUserLifecycleStore.cs @@ -3,22 +3,21 @@ using CodeBeam.UltimateAuth.Core.MultiTenancy; using CodeBeam.UltimateAuth.Users.Contracts; -namespace CodeBeam.UltimateAuth.Users.Reference +namespace CodeBeam.UltimateAuth.Users.Reference; + +public interface IUserLifecycleStore { - public interface IUserLifecycleStore - { - Task ExistsAsync(TenantKey tenant, UserKey userKey, CancellationToken ct = default); + Task ExistsAsync(TenantKey tenant, UserKey userKey, CancellationToken ct = default); - Task GetAsync(TenantKey tenant, UserKey userKey, CancellationToken ct = default); + Task GetAsync(TenantKey tenant, UserKey userKey, CancellationToken ct = default); - Task> QueryAsync(TenantKey tenant, UserLifecycleQuery query, CancellationToken ct = default); + Task> QueryAsync(TenantKey tenant, UserLifecycleQuery query, CancellationToken ct = default); - Task CreateAsync(TenantKey tenant, UserLifecycle lifecycle, CancellationToken ct = default); + Task CreateAsync(TenantKey tenant, UserLifecycle lifecycle, CancellationToken ct = default); - Task ChangeStatusAsync(TenantKey tenant, UserKey userKey, UserStatus newStatus, DateTimeOffset updatedAt, CancellationToken ct = default); + Task ChangeStatusAsync(TenantKey tenant, UserKey userKey, UserStatus newStatus, DateTimeOffset updatedAt, CancellationToken ct = default); - Task ChangeSecurityStampAsync(TenantKey tenant, UserKey userKey, Guid newSecurityStamp, DateTimeOffset updatedAt, CancellationToken ct = default); + Task ChangeSecurityStampAsync(TenantKey tenant, UserKey userKey, Guid newSecurityStamp, DateTimeOffset updatedAt, CancellationToken ct = default); - Task DeleteAsync(TenantKey tenant, UserKey userKey, DeleteMode mode, DateTimeOffset deletedAt, CancellationToken ct = default); - } + Task DeleteAsync(TenantKey tenant, UserKey userKey, DeleteMode mode, DateTimeOffset deletedAt, CancellationToken ct = default); } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Stores/UserRuntimeStore.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Stores/UserRuntimeStore.cs index 41573216..e77f70c9 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Stores/UserRuntimeStore.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Stores/UserRuntimeStore.cs @@ -3,31 +3,30 @@ using CodeBeam.UltimateAuth.Users.Contracts; using CodeBeam.UltimateAuth.Core.MultiTenancy; -namespace CodeBeam.UltimateAuth.Users.Reference +namespace CodeBeam.UltimateAuth.Users.Reference; + +internal sealed class UserRuntimeStore : IUserRuntimeStateProvider { - internal sealed class UserRuntimeStore : IUserRuntimeStateProvider - { - private readonly IUserLifecycleStore _lifecycleStore; + private readonly IUserLifecycleStore _lifecycleStore; - public UserRuntimeStore(IUserLifecycleStore lifecycleStore) - { - _lifecycleStore = lifecycleStore; - } + public UserRuntimeStore(IUserLifecycleStore lifecycleStore) + { + _lifecycleStore = lifecycleStore; + } - public async Task GetAsync(TenantKey tenant, UserKey userKey, CancellationToken ct = default) - { - var lifecycle = await _lifecycleStore.GetAsync(tenant, userKey, ct); + public async Task GetAsync(TenantKey tenant, UserKey userKey, CancellationToken ct = default) + { + var lifecycle = await _lifecycleStore.GetAsync(tenant, userKey, ct); - if (lifecycle is null) - return null; + if (lifecycle is null) + return null; - return new UserRuntimeRecord - { - UserKey = lifecycle.UserKey, - IsActive = lifecycle.Status == UserStatus.Active, - IsDeleted = lifecycle.IsDeleted, - Exists = true - }; - } + return new UserRuntimeRecord + { + UserKey = lifecycle.UserKey, + IsActive = lifecycle.Status == UserStatus.Active, + IsDeleted = lifecycle.IsDeleted, + Exists = true + }; } } diff --git a/src/users/CodeBeam.UltimateAuth.Users/Abstractions/IUser.cs b/src/users/CodeBeam.UltimateAuth.Users/Abstractions/IUser.cs index b4a93486..b3cda978 100644 --- a/src/users/CodeBeam.UltimateAuth.Users/Abstractions/IUser.cs +++ b/src/users/CodeBeam.UltimateAuth.Users/Abstractions/IUser.cs @@ -1,8 +1,7 @@ -namespace CodeBeam.UltimateAuth.Users +namespace CodeBeam.UltimateAuth.Users; + +public interface IUser { - public interface IUser - { - TUserId UserId { get; } - bool IsActive { get; } - } + TUserId UserId { get; } + bool IsActive { get; } } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/BlazorServerSessionCoordinatorTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/BlazorServerSessionCoordinatorTests.cs index 78922083..1fdc364e 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/BlazorServerSessionCoordinatorTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/BlazorServerSessionCoordinatorTests.cs @@ -5,86 +5,84 @@ using CodeBeam.UltimateAuth.Client.Options; using CodeBeam.UltimateAuth.Client.Services; using CodeBeam.UltimateAuth.Core.Domain; -using CodeBeam.UltimateAuth.Tests.Unit.Fake; using Microsoft.Extensions.Options; -namespace CodeBeam.UltimateAuth.Tests.Unit.Client +namespace CodeBeam.UltimateAuth.Tests.Unit; + +public sealed class BlazorServerSessionCoordinatorTests { - public sealed class BlazorServerSessionCoordinatorTests - { - //[Fact] - //public async Task StartAsync_MarksStarted_AndAutomaticRefresh() - //{ - // var diagnostics = new UAuthClientDiagnostics(); - // var client = new FakeFlowClient(RefreshOutcome.NoOp); - // var nav = new TestNavigationManager(); - - // var options = Options.Create(new UAuthClientOptions - // { - // Refresh = { Interval = TimeSpan.FromMilliseconds(10) } - // }); - - // var coordinator = new BlazorServerSessionCoordinator(new UAuthClient(client), - // nav, - // options, - // diagnostics); - - // await coordinator.StartAsync(); - // await Task.Delay(30); - // await coordinator.StopAsync(); - - // Assert.Equal(1, diagnostics.StartCount); - // Assert.True(diagnostics.AutomaticRefreshCount >= 1); - //} - - //[Fact] - //public async Task ReauthRequired_ShouldTerminateAndNavigate() - //{ - // var diagnostics = new UAuthClientDiagnostics(); - // var client = new FakeFlowClient(RefreshOutcome.ReauthRequired); - // var nav = new TestNavigationManager(); - - // var options = Options.Create(new UAuthClientOptions - // { - // Refresh = { Interval = TimeSpan.FromMilliseconds(5) }, - // Reauth = - // { - // Behavior = ReauthBehavior.RedirectToLogin, - // LoginPath = "/login" - // } - // }); - - // var coordinator = new BlazorServerSessionCoordinator(new UAuthClient(client), - // nav, - // options, - // diagnostics); - - // await coordinator.StartAsync(); - // await Task.Delay(20); - - // Assert.True(diagnostics.IsTerminated); - // Assert.Equal(CoordinatorTerminationReason.ReauthRequired, diagnostics.TerminationReason); - // Assert.Equal("/login", nav.LastNavigatedTo); - //} - - //[Fact] - //public async Task StopAsync_ShouldMarkStopped() - //{ - // var diagnostics = new UAuthClientDiagnostics(); - // var client = new FakeFlowClient(); - // var nav = new TestNavigationManager(); - - // var options = Options.Create(new UAuthClientOptions()); - - // var coordinator = new BlazorServerSessionCoordinator(new UAuthClient(client), - // nav, - // options, - // diagnostics); - - // await coordinator.StartAsync(); - // await coordinator.StopAsync(); - - // Assert.Equal(1, diagnostics.StopCount); - //} - } + //[Fact] + //public async Task StartAsync_MarksStarted_AndAutomaticRefresh() + //{ + // var diagnostics = new UAuthClientDiagnostics(); + // var client = new FakeFlowClient(RefreshOutcome.NoOp); + // var nav = new TestNavigationManager(); + + // var options = Options.Create(new UAuthClientOptions + // { + // Refresh = { Interval = TimeSpan.FromMilliseconds(10) } + // }); + + // var coordinator = new BlazorServerSessionCoordinator(new UAuthClient(client), + // nav, + // options, + // diagnostics); + + // await coordinator.StartAsync(); + // await Task.Delay(30); + // await coordinator.StopAsync(); + + // Assert.Equal(1, diagnostics.StartCount); + // Assert.True(diagnostics.AutomaticRefreshCount >= 1); + //} + + //[Fact] + //public async Task ReauthRequired_ShouldTerminateAndNavigate() + //{ + // var diagnostics = new UAuthClientDiagnostics(); + // var client = new FakeFlowClient(RefreshOutcome.ReauthRequired); + // var nav = new TestNavigationManager(); + + // var options = Options.Create(new UAuthClientOptions + // { + // Refresh = { Interval = TimeSpan.FromMilliseconds(5) }, + // Reauth = + // { + // Behavior = ReauthBehavior.RedirectToLogin, + // LoginPath = "/login" + // } + // }); + + // var coordinator = new BlazorServerSessionCoordinator(new UAuthClient(client), + // nav, + // options, + // diagnostics); + + // await coordinator.StartAsync(); + // await Task.Delay(20); + + // Assert.True(diagnostics.IsTerminated); + // Assert.Equal(CoordinatorTerminationReason.ReauthRequired, diagnostics.TerminationReason); + // Assert.Equal("/login", nav.LastNavigatedTo); + //} + + //[Fact] + //public async Task StopAsync_ShouldMarkStopped() + //{ + // var diagnostics = new UAuthClientDiagnostics(); + // var client = new FakeFlowClient(); + // var nav = new TestNavigationManager(); + + // var options = Options.Create(new UAuthClientOptions()); + + // var coordinator = new BlazorServerSessionCoordinator(new UAuthClient(client), + // nav, + // options, + // diagnostics); + + // await coordinator.StartAsync(); + // await coordinator.StopAsync(); + + // Assert.Equal(1, diagnostics.StopCount); + //} } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/ClientDiagnosticsTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/ClientDiagnosticsTests.cs index 054bd22c..0d2182a8 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/ClientDiagnosticsTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/ClientDiagnosticsTests.cs @@ -3,104 +3,103 @@ using CodeBeam.UltimateAuth.Client.Diagnostics; using Xunit; -namespace CodeBeam.UltimateAuth.Tests.Unit.Client +namespace CodeBeam.UltimateAuth.Tests.Unit; + +public class ClientDiagnosticsTests { - public class ClientDiagnosticsTests + [Fact] + public void MarkStarted_ShouldSetStartedAt_AndIncrementCounter() + { + var diagnostics = new UAuthClientDiagnostics(); + diagnostics.MarkStarted(); + + Assert.NotNull(diagnostics.StartedAt); + Assert.Equal(1, diagnostics.StartCount); + Assert.False(diagnostics.IsTerminated); + } + + [Fact] + public void MarkStopped_ShouldSetStoppedAt_AndIncrementCounter() + { + var diagnostics = new UAuthClientDiagnostics(); + diagnostics.MarkStopped(); + + Assert.NotNull(diagnostics.StoppedAt); + Assert.Equal(1, diagnostics.StopCount); + } + + [Fact] + public void MarkManualRefresh_ShouldIncrementManualAndTotalCounters() + { + var diagnostics = new UAuthClientDiagnostics(); + diagnostics.MarkManualRefresh(); + + Assert.Equal(1, diagnostics.RefreshAttemptCount); + Assert.Equal(1, diagnostics.ManualRefreshCount); + Assert.Equal(0, diagnostics.AutomaticRefreshCount); + } + + [Fact] + public void MarkAutomaticRefresh_ShouldIncrementAutomaticAndTotalCounters() { - [Fact] - public void MarkStarted_ShouldSetStartedAt_AndIncrementCounter() - { - var diagnostics = new UAuthClientDiagnostics(); - diagnostics.MarkStarted(); - - Assert.NotNull(diagnostics.StartedAt); - Assert.Equal(1, diagnostics.StartCount); - Assert.False(diagnostics.IsTerminated); - } - - [Fact] - public void MarkStopped_ShouldSetStoppedAt_AndIncrementCounter() - { - var diagnostics = new UAuthClientDiagnostics(); - diagnostics.MarkStopped(); - - Assert.NotNull(diagnostics.StoppedAt); - Assert.Equal(1, diagnostics.StopCount); - } - - [Fact] - public void MarkManualRefresh_ShouldIncrementManualAndTotalCounters() - { - var diagnostics = new UAuthClientDiagnostics(); - diagnostics.MarkManualRefresh(); - - Assert.Equal(1, diagnostics.RefreshAttemptCount); - Assert.Equal(1, diagnostics.ManualRefreshCount); - Assert.Equal(0, diagnostics.AutomaticRefreshCount); - } - - [Fact] - public void MarkAutomaticRefresh_ShouldIncrementAutomaticAndTotalCounters() - { - var diagnostics = new UAuthClientDiagnostics(); - diagnostics.MarkAutomaticRefresh(); - - Assert.Equal(1, diagnostics.RefreshAttemptCount); - Assert.Equal(1, diagnostics.AutomaticRefreshCount); - Assert.Equal(0, diagnostics.ManualRefreshCount); - } - - [Fact] - public void MarkRefreshOutcomes_ShouldIncrementCorrectCounters() - { - var diagnostics = new UAuthClientDiagnostics(); - diagnostics.MarkRefreshTouched(); - diagnostics.MarkRefreshNoOp(); - diagnostics.MarkRefreshReauthRequired(); - diagnostics.MarkRefreshUnknown(); - - Assert.Equal(1, diagnostics.RefreshTouchedCount); - Assert.Equal(1, diagnostics.RefreshNoOpCount); - Assert.Equal(1, diagnostics.RefreshReauthRequiredCount); - Assert.Equal(1, diagnostics.RefreshUnknownCount); - } - - [Fact] - public void MarkTerminated_ShouldSetTerminationState_AndIncrementCounter() - { - var diagnostics = new UAuthClientDiagnostics(); - diagnostics.MarkTerminated(CoordinatorTerminationReason.ReauthRequired); - - Assert.True(diagnostics.IsTerminated); - Assert.Equal(CoordinatorTerminationReason.ReauthRequired, diagnostics.TerminationReason); - Assert.Equal(1, diagnostics.TerminatedCount); - } - - [Fact] - public void ChangedEvent_ShouldFire_OnStateChanges() - { - var diagnostics = new UAuthClientDiagnostics(); - int changeCount = 0; - - diagnostics.Changed += () => changeCount++; - - diagnostics.MarkStarted(); - diagnostics.MarkManualRefresh(); - diagnostics.MarkRefreshTouched(); - diagnostics.MarkTerminated(CoordinatorTerminationReason.ReauthRequired); - - Assert.Equal(4, changeCount); - } - - [Fact] - public void MarkTerminated_ShouldBeIdempotent_ForStateButCountShouldIncrease() - { - var diagnostics = new UAuthClientDiagnostics(); - diagnostics.MarkTerminated(CoordinatorTerminationReason.ReauthRequired); - diagnostics.MarkTerminated(CoordinatorTerminationReason.ReauthRequired); - - Assert.True(diagnostics.IsTerminated); - Assert.Equal(2, diagnostics.TerminatedCount); - } + var diagnostics = new UAuthClientDiagnostics(); + diagnostics.MarkAutomaticRefresh(); + + Assert.Equal(1, diagnostics.RefreshAttemptCount); + Assert.Equal(1, diagnostics.AutomaticRefreshCount); + Assert.Equal(0, diagnostics.ManualRefreshCount); + } + + [Fact] + public void MarkRefreshOutcomes_ShouldIncrementCorrectCounters() + { + var diagnostics = new UAuthClientDiagnostics(); + diagnostics.MarkRefreshTouched(); + diagnostics.MarkRefreshNoOp(); + diagnostics.MarkRefreshReauthRequired(); + diagnostics.MarkRefreshUnknown(); + + Assert.Equal(1, diagnostics.RefreshTouchedCount); + Assert.Equal(1, diagnostics.RefreshNoOpCount); + Assert.Equal(1, diagnostics.RefreshReauthRequiredCount); + Assert.Equal(1, diagnostics.RefreshUnknownCount); + } + + [Fact] + public void MarkTerminated_ShouldSetTerminationState_AndIncrementCounter() + { + var diagnostics = new UAuthClientDiagnostics(); + diagnostics.MarkTerminated(CoordinatorTerminationReason.ReauthRequired); + + Assert.True(diagnostics.IsTerminated); + Assert.Equal(CoordinatorTerminationReason.ReauthRequired, diagnostics.TerminationReason); + Assert.Equal(1, diagnostics.TerminatedCount); + } + + [Fact] + public void ChangedEvent_ShouldFire_OnStateChanges() + { + var diagnostics = new UAuthClientDiagnostics(); + int changeCount = 0; + + diagnostics.Changed += () => changeCount++; + + diagnostics.MarkStarted(); + diagnostics.MarkManualRefresh(); + diagnostics.MarkRefreshTouched(); + diagnostics.MarkTerminated(CoordinatorTerminationReason.ReauthRequired); + + Assert.Equal(4, changeCount); + } + + [Fact] + public void MarkTerminated_ShouldBeIdempotent_ForStateButCountShouldIncrease() + { + var diagnostics = new UAuthClientDiagnostics(); + diagnostics.MarkTerminated(CoordinatorTerminationReason.ReauthRequired); + diagnostics.MarkTerminated(CoordinatorTerminationReason.ReauthRequired); + + Assert.True(diagnostics.IsTerminated); + Assert.Equal(2, diagnostics.TerminatedCount); } } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/RefreshOutcomeParserTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/RefreshOutcomeParserTests.cs index e0725041..b073c2a8 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/RefreshOutcomeParserTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/RefreshOutcomeParserTests.cs @@ -1,34 +1,32 @@ using CodeBeam.UltimateAuth.Client.Infrastructure; using CodeBeam.UltimateAuth.Core.Domain; -using Xunit; -namespace CodeBeam.UltimateAuth.Tests.Unit +namespace CodeBeam.UltimateAuth.Tests.Unit; + +public sealed class RefreshOutcomeParserTests { - public sealed class RefreshOutcomeParserTests + [Theory] + [InlineData("no-op", RefreshOutcome.NoOp)] + [InlineData("touched", RefreshOutcome.Touched)] + [InlineData("reauth-required", RefreshOutcome.ReauthRequired)] + public void Parse_KnownValues_ReturnsExpectedOutcome(string input, RefreshOutcome expected) { - [Theory] - [InlineData("no-op", RefreshOutcome.NoOp)] - [InlineData("touched", RefreshOutcome.Touched)] - [InlineData("reauth-required", RefreshOutcome.ReauthRequired)] - public void Parse_KnownValues_ReturnsExpectedOutcome(string input, RefreshOutcome expected) - { - var result = RefreshOutcomeParser.Parse(input); + var result = RefreshOutcomeParser.Parse(input); - Assert.Equal(expected, result); - } + Assert.Equal(expected, result); + } - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData(" ")] - [InlineData("unknown")] - [InlineData("NO-OP")] - [InlineData("Touched")] - public void Parse_UnknownOrInvalidValues_ReturnsNone(string? input) - { - var result = RefreshOutcomeParser.Parse(input); + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData("unknown")] + [InlineData("NO-OP")] + [InlineData("Touched")] + public void Parse_UnknownOrInvalidValues_ReturnsNone(string? input) + { + var result = RefreshOutcomeParser.Parse(input); - Assert.Equal(RefreshOutcome.None, result); - } + Assert.Equal(RefreshOutcome.None, result); } } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Core/RefreshTokenValidatorTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Core/RefreshTokenValidatorTests.cs index 9111c5cb..bf98b03d 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Core/RefreshTokenValidatorTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Core/RefreshTokenValidatorTests.cs @@ -7,111 +7,110 @@ using CodeBeam.UltimateAuth.Tokens.InMemory; using System.Text; -namespace CodeBeam.UltimateAuth.Tests.Unit.Core +namespace CodeBeam.UltimateAuth.Tests.Unit; + +public sealed class RefreshTokenValidatorTests { - public sealed class RefreshTokenValidatorTests + private const string ValidDeviceId = "deviceidshouldbelongandstrongenough!?1234567890"; + + private static UAuthRefreshTokenValidator CreateValidator(InMemoryRefreshTokenStore store) { - private const string ValidDeviceId = "deviceidshouldbelongandstrongenough!?1234567890"; + return new UAuthRefreshTokenValidator(store, CreateHasher()); + } - private static DefaultRefreshTokenValidator CreateValidator(InMemoryRefreshTokenStore store) - { - return new DefaultRefreshTokenValidator(store, CreateHasher()); - } + private static ITokenHasher CreateHasher() + { + return new HmacSha256TokenHasher(Encoding.UTF8.GetBytes("unit-test-secret-key")); + } - private static ITokenHasher CreateHasher() - { - return new HmacSha256TokenHasher(Encoding.UTF8.GetBytes("unit-test-secret-key")); - } + [Fact] + public async Task Invalid_When_Token_Not_Found() + { + var store = new InMemoryRefreshTokenStore(); + var validator = CreateValidator(store); - [Fact] - public async Task Invalid_When_Token_Not_Found() - { - var store = new InMemoryRefreshTokenStore(); - var validator = CreateValidator(store); - - var result = await validator.ValidateAsync( - new RefreshTokenValidationContext - { - Tenant = TenantKey.Single, - RefreshToken = "non-existing", - Now = DateTimeOffset.UtcNow, - Device = DeviceContext.FromDeviceId(DeviceId.Create(ValidDeviceId)), - }); - - Assert.False(result.IsValid); - Assert.False(result.IsReuseDetected); - } - - [Fact] - public async Task Reuse_Detected_When_Token_is_Revoked() - { - var store = new InMemoryRefreshTokenStore(); - var hasher = CreateHasher(); - var validator = CreateValidator(store); + var result = await validator.ValidateAsync( + new RefreshTokenValidationContext + { + Tenant = TenantKey.Single, + RefreshToken = "non-existing", + Now = DateTimeOffset.UtcNow, + Device = DeviceContext.FromDeviceId(DeviceId.Create(ValidDeviceId)), + }); - var now = DateTimeOffset.UtcNow; + Assert.False(result.IsValid); + Assert.False(result.IsReuseDetected); + } + + [Fact] + public async Task Reuse_Detected_When_Token_is_Revoked() + { + var store = new InMemoryRefreshTokenStore(); + var hasher = CreateHasher(); + var validator = CreateValidator(store); + + var now = DateTimeOffset.UtcNow; - var rawToken = "refresh-token-1"; - var hash = hasher.Hash(rawToken); + var rawToken = "refresh-token-1"; + var hash = hasher.Hash(rawToken); - await store.StoreAsync(TenantKey.Single, new StoredRefreshToken + await store.StoreAsync(TenantKey.Single, new StoredRefreshToken + { + Tenant = TenantKey.Single, + TokenHash = hash, + UserKey = UserKey.FromString("user-1"), + SessionId = TestIds.Session("session-1-aaaaaaaaaaaaaaaaaaaaaa"), + ChainId = SessionChainId.New(), + IssuedAt = now.AddMinutes(-5), + ExpiresAt = now.AddMinutes(5), + RevokedAt = now + }); + + var result = await validator.ValidateAsync( + new RefreshTokenValidationContext { Tenant = TenantKey.Single, - TokenHash = hash, - UserKey = UserKey.FromString("user-1"), - SessionId = TestIds.Session("session-1-aaaaaaaaaaaaaaaaaaaaaa"), - ChainId = SessionChainId.New(), - IssuedAt = now.AddMinutes(-5), - ExpiresAt = now.AddMinutes(5), - RevokedAt = now + RefreshToken = rawToken, + Now = now, + Device = DeviceContext.FromDeviceId(DeviceId.Create(ValidDeviceId)), }); - var result = await validator.ValidateAsync( - new RefreshTokenValidationContext - { - Tenant = TenantKey.Single, - RefreshToken = rawToken, - Now = now, - Device = DeviceContext.FromDeviceId(DeviceId.Create(ValidDeviceId)), - }); - - Assert.False(result.IsValid); - Assert.True(result.IsReuseDetected); - } - - [Fact] - public async Task Invalid_When_Expected_Session_Id_Does_Not_Match() - { - var store = new InMemoryRefreshTokenStore(); - var validator = CreateValidator(store); + Assert.False(result.IsValid); + Assert.True(result.IsReuseDetected); + } - var now = DateTimeOffset.UtcNow; + [Fact] + public async Task Invalid_When_Expected_Session_Id_Does_Not_Match() + { + var store = new InMemoryRefreshTokenStore(); + var validator = CreateValidator(store); + + var now = DateTimeOffset.UtcNow; - await store.StoreAsync(TenantKey.Single, new StoredRefreshToken + await store.StoreAsync(TenantKey.Single, new StoredRefreshToken + { + Tenant = TenantKey.Single, + TokenHash = "hash-2", + UserKey = UserKey.FromString("user-1"), + SessionId = TestIds.Session("session-1-bbbbbbbbbbbbbbbbbbbbbb"), + ChainId = SessionChainId.New(), + IssuedAt = now, + ExpiresAt = now.AddMinutes(10) + }); + + var result = await validator.ValidateAsync( + new RefreshTokenValidationContext { Tenant = TenantKey.Single, - TokenHash = "hash-2", - UserKey = UserKey.FromString("user-1"), - SessionId = TestIds.Session("session-1-bbbbbbbbbbbbbbbbbbbbbb"), - ChainId = SessionChainId.New(), - IssuedAt = now, - ExpiresAt = now.AddMinutes(10) + RefreshToken = "hash-2", + ExpectedSessionId = TestIds.Session("session-2-cccccccccccccccccccccc"), + Now = now, + Device = DeviceContext.FromDeviceId(DeviceId.Create(ValidDeviceId)), }); - var result = await validator.ValidateAsync( - new RefreshTokenValidationContext - { - Tenant = TenantKey.Single, - RefreshToken = "hash-2", - ExpectedSessionId = TestIds.Session("session-2-cccccccccccccccccccccc"), - Now = now, - Device = DeviceContext.FromDeviceId(DeviceId.Create(ValidDeviceId)), - }); - - Assert.False(result.IsValid); - Assert.False(result.IsReuseDetected); - } - + Assert.False(result.IsValid); + Assert.False(result.IsReuseDetected); } + } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Core/UAuthSessionTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Core/UAuthSessionTests.cs index 049f6233..6a858508 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Core/UAuthSessionTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Core/UAuthSessionTests.cs @@ -1,6 +1,5 @@ using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Core.MultiTenancy; -using Xunit; namespace CodeBeam.UltimateAuth.Tests.Unit; diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Core/UserIdConverterTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Core/UserIdConverterTests.cs index 0516b946..a887a0df 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Core/UserIdConverterTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Core/UserIdConverterTests.cs @@ -3,100 +3,99 @@ using System.Globalization; using System.Text.Json; -namespace CodeBeam.UltimateAuth.Tests.Unit +namespace CodeBeam.UltimateAuth.Tests.Unit; + +public sealed class UserIdConverterTests { - public sealed class UserIdConverterTests + [Fact] + public void UserKey_Roundtrip_Should_Preserve_Value() { - [Fact] - public void UserKey_Roundtrip_Should_Preserve_Value() - { - var key = UserKey.New(); - var converter = new UAuthUserIdConverter(); - - var str = converter.ToCanonicalString(key); - var parsed = converter.FromString(str); + var key = UserKey.New(); + var converter = new UAuthUserIdConverter(); - Assert.Equal(key, parsed); - } + var str = converter.ToCanonicalString(key); + var parsed = converter.FromString(str); - [Fact] - public void Guid_Roundtrip_Should_Work() - { - var id = Guid.NewGuid(); - var converter = new UAuthUserIdConverter(); + Assert.Equal(key, parsed); + } - var str = converter.ToCanonicalString(id); - var parsed = converter.FromString(str); + [Fact] + public void Guid_Roundtrip_Should_Work() + { + var id = Guid.NewGuid(); + var converter = new UAuthUserIdConverter(); - Assert.Equal(id, parsed); - } + var str = converter.ToCanonicalString(id); + var parsed = converter.FromString(str); - [Fact] - public void String_Roundtrip_Should_Work() - { - var id = "user_123"; - var converter = new UAuthUserIdConverter(); + Assert.Equal(id, parsed); + } - var str = converter.ToCanonicalString(id); - var parsed = converter.FromString(str); + [Fact] + public void String_Roundtrip_Should_Work() + { + var id = "user_123"; + var converter = new UAuthUserIdConverter(); - Assert.Equal(id, parsed); - } + var str = converter.ToCanonicalString(id); + var parsed = converter.FromString(str); - [Fact] - public void Int_Should_Use_Invariant_Culture() - { - var id = 1234; - var converter = new UAuthUserIdConverter(); + Assert.Equal(id, parsed); + } - var str = converter.ToCanonicalString(id); + [Fact] + public void Int_Should_Use_Invariant_Culture() + { + var id = 1234; + var converter = new UAuthUserIdConverter(); - Assert.Equal(id.ToString(CultureInfo.InvariantCulture), str); - } + var str = converter.ToCanonicalString(id); - [Fact] - public void Long_Roundtrip_Should_Work() - { - var id = 9_223_372_036_854_775_000L; - var converter = new UAuthUserIdConverter(); + Assert.Equal(id.ToString(CultureInfo.InvariantCulture), str); + } - var str = converter.ToCanonicalString(id); - var parsed = converter.FromString(str); + [Fact] + public void Long_Roundtrip_Should_Work() + { + var id = 9_223_372_036_854_775_000L; + var converter = new UAuthUserIdConverter(); - Assert.Equal(id, parsed); - } + var str = converter.ToCanonicalString(id); + var parsed = converter.FromString(str); - [Fact] - public void Double_UserId_Should_Throw() - { - var converter = new UAuthUserIdConverter(); + Assert.Equal(id, parsed); + } - Assert.ThrowsAny(() => converter.ToCanonicalString(12.34)); - } + [Fact] + public void Double_UserId_Should_Throw() + { + var converter = new UAuthUserIdConverter(); - private sealed class CustomUserId - { - public string Value { get; set; } = "x"; - } + Assert.ThrowsAny(() => converter.ToCanonicalString(12.34)); + } - [Fact] - public void Custom_UserId_Should_Fail() - { - var converter = new UAuthUserIdConverter(); + private sealed class CustomUserId + { + public string Value { get; set; } = "x"; + } - Assert.ThrowsAny(() => converter.ToCanonicalString(new CustomUserId())); - } + [Fact] + public void Custom_UserId_Should_Fail() + { + var converter = new UAuthUserIdConverter(); - [Fact] - public void UserKey_Json_Serialization_Should_Be_String() - { - var key = UserKey.New(); + Assert.ThrowsAny(() => converter.ToCanonicalString(new CustomUserId())); + } - var json = JsonSerializer.Serialize(key); - var roundtrip = JsonSerializer.Deserialize(json); + [Fact] + public void UserKey_Json_Serialization_Should_Be_String() + { + var key = UserKey.New(); - Assert.Equal(key, roundtrip); - } + var json = JsonSerializer.Serialize(key); + var roundtrip = JsonSerializer.Deserialize(json); + Assert.Equal(key, roundtrip); } + } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Credentials/CredentialUserMappingBuilderTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Credentials/CredentialUserMappingBuilderTests.cs index 3f8f07e0..6ab9d80f 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Credentials/CredentialUserMappingBuilderTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Credentials/CredentialUserMappingBuilderTests.cs @@ -1,95 +1,94 @@ using CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore; -namespace CodeBeam.UltimateAuth.Tests.Unit +namespace CodeBeam.UltimateAuth.Tests.Unit; + +public class CredentialUserMappingBuilderTests { - public class CredentialUserMappingBuilderTests + private sealed class ConventionUser { - private sealed class ConventionUser - { - public Guid Id { get; set; } - public string Email { get; set; } = default!; - public string PasswordHash { get; set; } = default!; - public long SecurityVersion { get; set; } - } + public Guid Id { get; set; } + public string Email { get; set; } = default!; + public string PasswordHash { get; set; } = default!; + public long SecurityVersion { get; set; } + } - private sealed class ExplicitUser - { - public Guid UserId { get; set; } - public string LoginName { get; set; } = default!; - public string PasswordHash { get; set; } = default!; - public long SecurityVersion { get; set; } - } + private sealed class ExplicitUser + { + public Guid UserId { get; set; } + public string LoginName { get; set; } = default!; + public string PasswordHash { get; set; } = default!; + public long SecurityVersion { get; set; } + } - private sealed class PlainPasswordUser - { - public Guid Id { get; set; } - public string Username { get; set; } = default!; - public string Password { get; set; } = default!; - public long SecurityVersion { get; set; } - } + private sealed class PlainPasswordUser + { + public Guid Id { get; set; } + public string Username { get; set; } = default!; + public string Password { get; set; } = default!; + public long SecurityVersion { get; set; } + } - [Fact] - public void Build_UsesConventions_WhenExplicitMappingIsNotProvided() + [Fact] + public void Build_UsesConventions_WhenExplicitMappingIsNotProvided() + { + var options = new CredentialUserMappingOptions(); + var mapping = CredentialUserMappingBuilder.Build(options); + var user = new ConventionUser { - var options = new CredentialUserMappingOptions(); - var mapping = CredentialUserMappingBuilder.Build(options); - var user = new ConventionUser - { - Id = Guid.NewGuid(), - Email = "test@example.com", - PasswordHash = "hash", - SecurityVersion = 3 - }; + Id = Guid.NewGuid(), + Email = "test@example.com", + PasswordHash = "hash", + SecurityVersion = 3 + }; - Assert.Equal(user.Id, mapping.UserId(user)); - Assert.Equal(user.Email, mapping.Username(user)); - Assert.Equal(user.PasswordHash, mapping.PasswordHash(user)); - Assert.Equal(user.SecurityVersion, mapping.SecurityVersion(user)); - Assert.True(mapping.CanAuthenticate(user)); - } + Assert.Equal(user.Id, mapping.UserId(user)); + Assert.Equal(user.Email, mapping.Username(user)); + Assert.Equal(user.PasswordHash, mapping.PasswordHash(user)); + Assert.Equal(user.SecurityVersion, mapping.SecurityVersion(user)); + Assert.True(mapping.CanAuthenticate(user)); + } - [Fact] - public void Build_ExplicitMapping_OverridesConvention() + [Fact] + public void Build_ExplicitMapping_OverridesConvention() + { + var options = new CredentialUserMappingOptions(); + options.MapUsername(u => u.LoginName); + var mapping = CredentialUserMappingBuilder.Build(options); + var user = new ExplicitUser { - var options = new CredentialUserMappingOptions(); - options.MapUsername(u => u.LoginName); - var mapping = CredentialUserMappingBuilder.Build(options); - var user = new ExplicitUser - { - UserId = Guid.NewGuid(), - LoginName = "custom-login", - PasswordHash = "hash", - SecurityVersion = 1 - }; + UserId = Guid.NewGuid(), + LoginName = "custom-login", + PasswordHash = "hash", + SecurityVersion = 1 + }; - Assert.Equal("custom-login", mapping.Username(user)); - } + Assert.Equal("custom-login", mapping.Username(user)); + } - [Fact] - public void Build_DoesNotMap_PlainPassword_Property() - { - var options = new CredentialUserMappingOptions(); - var ex = Assert.Throws(() => CredentialUserMappingBuilder.Build(options)); + [Fact] + public void Build_DoesNotMap_PlainPassword_Property() + { + var options = new CredentialUserMappingOptions(); + var ex = Assert.Throws(() => CredentialUserMappingBuilder.Build(options)); - Assert.Contains("PasswordHash mapping is required", ex.Message); - } + Assert.Contains("PasswordHash mapping is required", ex.Message); + } - [Fact] - public void Build_Defaults_CanAuthenticate_ToTrue() + [Fact] + public void Build_Defaults_CanAuthenticate_ToTrue() + { + var options = new CredentialUserMappingOptions(); + var mapping = CredentialUserMappingBuilder.Build(options); + var user = new ConventionUser { - var options = new CredentialUserMappingOptions(); - var mapping = CredentialUserMappingBuilder.Build(options); - var user = new ConventionUser - { - Id = Guid.NewGuid(), - Email = "active@example.com", - PasswordHash = "hash", - SecurityVersion = 0 - }; + Id = Guid.NewGuid(), + Email = "active@example.com", + PasswordHash = "hash", + SecurityVersion = 0 + }; - var canAuthenticate = mapping.CanAuthenticate(user); - Assert.True(canAuthenticate); - } + var canAuthenticate = mapping.CanAuthenticate(user); + Assert.True(canAuthenticate); } } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Fake/FakeFlowClient.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Fake/FakeFlowClient.cs index 5b6b633e..17c16dbb 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Fake/FakeFlowClient.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Fake/FakeFlowClient.cs @@ -1,83 +1,80 @@ -using CodeBeam.UltimateAuth.Client; -using CodeBeam.UltimateAuth.Client.Contracts; +using CodeBeam.UltimateAuth.Client.Contracts; using CodeBeam.UltimateAuth.Client.Services; using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; using System.Security.Claims; -namespace CodeBeam.UltimateAuth.Tests.Unit -{ - internal sealed class FakeFlowClient : IFlowClient - { - private readonly Queue _outcomes; +namespace CodeBeam.UltimateAuth.Tests.Unit; - public FakeFlowClient(params RefreshOutcome[] outcomes) - { - _outcomes = new Queue(outcomes); - } +internal sealed class FakeFlowClient : IFlowClient +{ + private readonly Queue _outcomes; - public Task BeginPkceAsync(bool navigateToHubLogin = true) - { - throw new NotImplementedException(); - } + public FakeFlowClient(params RefreshOutcome[] outcomes) + { + _outcomes = new Queue(outcomes); + } - public Task BeginPkceAsync(string? returnUrl = null) - { - throw new NotImplementedException(); - } + public Task BeginPkceAsync(bool navigateToHubLogin = true) + { + throw new NotImplementedException(); + } - public Task CompletePkceLoginAsync(LoginRequest request) - { - throw new NotImplementedException(); - } + public Task BeginPkceAsync(string? returnUrl = null) + { + throw new NotImplementedException(); + } - public Task CompletePkceLoginAsync(PkceLoginRequest request) - { - throw new NotImplementedException(); - } + public Task CompletePkceLoginAsync(LoginRequest request) + { + throw new NotImplementedException(); + } - public Task GetCurrentPrincipalAsync() - { - throw new NotImplementedException(); - } + public Task CompletePkceLoginAsync(PkceLoginRequest request) + { + throw new NotImplementedException(); + } - public Task LoginAsync(LoginRequest request) - { - throw new NotImplementedException(); - } + public Task GetCurrentPrincipalAsync() + { + throw new NotImplementedException(); + } - public Task LogoutAsync() - { - throw new NotImplementedException(); - } + public Task LoginAsync(LoginRequest request) + { + throw new NotImplementedException(); + } - public Task NavigateToHubLoginAsync(string authorizationCode, string codeVerifier, string? returnUrl = null) - { - throw new NotImplementedException(); - } + public Task LogoutAsync() + { + throw new NotImplementedException(); + } - public Task ReauthAsync() - { - throw new NotImplementedException(); - } + public Task NavigateToHubLoginAsync(string authorizationCode, string codeVerifier, string? returnUrl = null) + { + throw new NotImplementedException(); + } - public Task RefreshAsync(bool isAuto = false) - { - var outcome = _outcomes.Count > 0 - ? _outcomes.Dequeue() - : RefreshOutcome.None; + public Task ReauthAsync() + { + throw new NotImplementedException(); + } - return Task.FromResult(new RefreshResult - { - Ok = true, - Outcome = outcome - }); - } + public Task RefreshAsync(bool isAuto = false) + { + var outcome = _outcomes.Count > 0 + ? _outcomes.Dequeue() + : RefreshOutcome.None; - public Task ValidateAsync() + return Task.FromResult(new RefreshResult { - throw new NotImplementedException(); - } + Ok = true, + Outcome = outcome + }); } + public Task ValidateAsync() + { + throw new NotImplementedException(); + } } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Fake/FakeNavigationManager.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Fake/FakeNavigationManager.cs index aa80e271..a0cd5b50 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Fake/FakeNavigationManager.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Fake/FakeNavigationManager.cs @@ -1,19 +1,18 @@ using Microsoft.AspNetCore.Components; -namespace CodeBeam.UltimateAuth.Tests.Unit.Fake +namespace CodeBeam.UltimateAuth.Tests.Unit; + +internal sealed class TestNavigationManager : NavigationManager { - internal sealed class TestNavigationManager : NavigationManager - { - public string? LastNavigatedTo { get; private set; } + public string? LastNavigatedTo { get; private set; } - public TestNavigationManager() - { - Initialize("http://localhost/", "http://localhost/"); - } + public TestNavigationManager() + { + Initialize("http://localhost/", "http://localhost/"); + } - protected override void NavigateToCore(string uri, bool forceLoad) - { - LastNavigatedTo = uri; - } + protected override void NavigateToCore(string uri, bool forceLoad) + { + LastNavigatedTo = uri; } } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Policies/ActionTextTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Policies/ActionTextTests.cs index ff7c37b5..3c1c08f5 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Policies/ActionTextTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Policies/ActionTextTests.cs @@ -1,34 +1,33 @@ using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Policies; -namespace CodeBeam.UltimateAuth.Tests.Unit.Policies +namespace CodeBeam.UltimateAuth.Tests.Unit; + +public class ActionTextTests { - public class ActionTextTests + [Theory] + [InlineData("users.profile.get.admin", true)] + [InlineData("users.profile.get.self", false)] + [InlineData("users.profile.get", false)] + public void RequireAdminPolicy_AppliesTo_Works(string action, bool expected) { - [Theory] - [InlineData("users.profile.get.admin", true)] - [InlineData("users.profile.get.self", false)] - [InlineData("users.profile.get", false)] - public void RequireAdminPolicy_AppliesTo_Works(string action, bool expected) - { - var context = new AccessContext { Action = action }; - var policy = new RequireAdminPolicy(); + var context = new AccessContext { Action = action }; + var policy = new RequireAdminPolicy(); - Assert.Equal(expected, policy.AppliesTo(context)); - } + Assert.Equal(expected, policy.AppliesTo(context)); + } - [Fact] - public void RequireAdminPolicy_DoesNotMatch_Substrings() + [Fact] + public void RequireAdminPolicy_DoesNotMatch_Substrings() + { + var context = new AccessContext { - var context = new AccessContext - { - Action = "users.profile.get.administrator" - }; - - var policy = new RequireAdminPolicy(); + Action = "users.profile.get.administrator" + }; - Assert.False(policy.AppliesTo(context)); - } + var policy = new RequireAdminPolicy(); + Assert.False(policy.AppliesTo(context)); } + } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/EffectiveAuthModeResolverTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/EffectiveAuthModeResolverTests.cs index 02881234..9c27d724 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/EffectiveAuthModeResolverTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/EffectiveAuthModeResolverTests.cs @@ -1,40 +1,38 @@ -using System; -using CodeBeam.UltimateAuth.Core; +using CodeBeam.UltimateAuth.Core; using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Core.Options; using CodeBeam.UltimateAuth.Server.Auth; -namespace CodeBeam.UltimateAuth.Tests.Unit.Server -{ - public class EffectiveAuthModeResolverTests - { - private readonly DefaultEffectiveAuthModeResolver _resolver = new(); +namespace CodeBeam.UltimateAuth.Tests.Unit; - [Fact] - public void ConfiguredMode_Wins_Over_ClientProfile() - { - var mode = _resolver.Resolve( - configuredMode: UAuthMode.PureJwt, - clientProfile: UAuthClientProfile.BlazorWasm, - flowType: AuthFlowType.Login); +public class EffectiveAuthModeResolverTests +{ + private readonly EffectiveAuthModeResolver _resolver = new(); - Assert.Equal(UAuthMode.PureJwt, mode); - } + [Fact] + public void ConfiguredMode_Wins_Over_ClientProfile() + { + var mode = _resolver.Resolve( + configuredMode: UAuthMode.PureJwt, + clientProfile: UAuthClientProfile.BlazorWasm, + flowType: AuthFlowType.Login); - [Theory] - [InlineData(UAuthClientProfile.BlazorServer, UAuthMode.PureOpaque)] - [InlineData(UAuthClientProfile.BlazorWasm, UAuthMode.Hybrid)] - [InlineData(UAuthClientProfile.Maui, UAuthMode.Hybrid)] - [InlineData(UAuthClientProfile.Api, UAuthMode.PureJwt)] - public void Default_Mode_Is_Derived_From_ClientProfile(UAuthClientProfile profile, UAuthMode expected) - { - var mode = _resolver.Resolve( - configuredMode: null, - clientProfile: profile, - flowType: AuthFlowType.Login); + Assert.Equal(UAuthMode.PureJwt, mode); + } - Assert.Equal(expected, mode); - } + [Theory] + [InlineData(UAuthClientProfile.BlazorServer, UAuthMode.PureOpaque)] + [InlineData(UAuthClientProfile.BlazorWasm, UAuthMode.Hybrid)] + [InlineData(UAuthClientProfile.Maui, UAuthMode.Hybrid)] + [InlineData(UAuthClientProfile.Api, UAuthMode.PureJwt)] + public void Default_Mode_Is_Derived_From_ClientProfile(UAuthClientProfile profile, UAuthMode expected) + { + var mode = _resolver.Resolve( + configuredMode: null, + clientProfile: profile, + flowType: AuthFlowType.Login); + 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 d16e2ee0..17df3e17 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/EffectiveServerOptionsProviderTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/EffectiveServerOptionsProviderTests.cs @@ -5,142 +5,141 @@ using CodeBeam.UltimateAuth.Server.Options; using Microsoft.AspNetCore.Http; -namespace CodeBeam.UltimateAuth.Tests.Unit.Server +namespace CodeBeam.UltimateAuth.Tests.Unit; + +public class EffectiveServerOptionsProviderTests { - public class EffectiveServerOptionsProviderTests + [Fact] + public void Original_Options_Are_Not_Mutated() { - [Fact] - public void Original_Options_Are_Not_Mutated() + var baseOptions = new UAuthServerOptions { - var baseOptions = new UAuthServerOptions - { - Mode = UAuthMode.Hybrid - }; + Mode = UAuthMode.Hybrid + }; - var provider = TestHelpers.CreateEffectiveOptionsProvider(baseOptions); - var ctx = new DefaultHttpContext(); + var provider = TestHelpers.CreateEffectiveOptionsProvider(baseOptions); + var ctx = new DefaultHttpContext(); - var effective = provider.GetEffective( - ctx, - AuthFlowType.Login, - UAuthClientProfile.BlazorServer); + var effective = provider.GetEffective( + ctx, + AuthFlowType.Login, + UAuthClientProfile.BlazorServer); - effective.Options.Tokens.AccessTokenLifetime = TimeSpan.FromSeconds(10); + effective.Options.Tokens.AccessTokenLifetime = TimeSpan.FromSeconds(10); - Assert.NotEqual( - baseOptions.Tokens.AccessTokenLifetime, - effective.Options.Tokens.AccessTokenLifetime - ); - } + Assert.NotEqual( + baseOptions.Tokens.AccessTokenLifetime, + effective.Options.Tokens.AccessTokenLifetime + ); + } - [Fact] - public void EffectiveMode_Comes_From_ModeResolver() + [Fact] + public void EffectiveMode_Comes_From_ModeResolver() + { + var baseOptions = new UAuthServerOptions { - var baseOptions = new UAuthServerOptions - { - Mode = null // Not specified - }; + Mode = null // Not specified + }; - var provider = TestHelpers.CreateEffectiveOptionsProvider(baseOptions); - var ctx = new DefaultHttpContext(); + var provider = TestHelpers.CreateEffectiveOptionsProvider(baseOptions); + var ctx = new DefaultHttpContext(); - var effective = provider.GetEffective( - ctx, - AuthFlowType.Login, - UAuthClientProfile.Api); + var effective = provider.GetEffective( + ctx, + AuthFlowType.Login, + UAuthClientProfile.Api); - Assert.Equal(UAuthMode.PureJwt, effective.Mode); - } + Assert.Equal(UAuthMode.PureJwt, effective.Mode); + } - [Fact] - public void Mode_Defaults_Are_Applied() + [Fact] + public void Mode_Defaults_Are_Applied() + { + var baseOptions = new UAuthServerOptions { - var baseOptions = new UAuthServerOptions - { - Mode = UAuthMode.PureOpaque - }; + Mode = UAuthMode.PureOpaque + }; - var provider = TestHelpers.CreateEffectiveOptionsProvider(baseOptions); - var ctx = new DefaultHttpContext(); + var provider = TestHelpers.CreateEffectiveOptionsProvider(baseOptions); + var ctx = new DefaultHttpContext(); - var effective = provider.GetEffective( - ctx, - AuthFlowType.Login, - UAuthClientProfile.BlazorServer); + var effective = provider.GetEffective( + ctx, + AuthFlowType.Login, + UAuthClientProfile.BlazorServer); - Assert.True(effective.Options.Session.SlidingExpiration); - Assert.NotNull(effective.Options.Session.IdleTimeout); - } + Assert.True(effective.Options.Session.SlidingExpiration); + Assert.NotNull(effective.Options.Session.IdleTimeout); + } - [Fact] - public void ModeConfiguration_Overrides_Defaults() + [Fact] + public void ModeConfiguration_Overrides_Defaults() + { + var baseOptions = new UAuthServerOptions { - var baseOptions = new UAuthServerOptions - { - Mode = UAuthMode.Hybrid - }; - - baseOptions.ConfigureMode(UAuthMode.Hybrid, o => - { - o.Tokens.AccessTokenLifetime = TimeSpan.FromMinutes(1); - }); - - var provider = TestHelpers.CreateEffectiveOptionsProvider(baseOptions); - var ctx = new DefaultHttpContext(); - - var effective = provider.GetEffective( - ctx, - AuthFlowType.Login, - UAuthClientProfile.BlazorServer); - - Assert.Equal( - TimeSpan.FromMinutes(1), - effective.Options.Tokens.AccessTokenLifetime - ); - } - - [Fact] - public void Each_Call_Returns_New_EffectiveOptions_Instance() + Mode = UAuthMode.Hybrid + }; + + baseOptions.ConfigureMode(UAuthMode.Hybrid, o => { - var baseOptions = new UAuthServerOptions - { - Mode = UAuthMode.Hybrid - }; + o.Tokens.AccessTokenLifetime = TimeSpan.FromMinutes(1); + }); - var provider = TestHelpers.CreateEffectiveOptionsProvider(baseOptions); - var ctx = new DefaultHttpContext(); + var provider = TestHelpers.CreateEffectiveOptionsProvider(baseOptions); + var ctx = new DefaultHttpContext(); - var first = provider.GetEffective(ctx, AuthFlowType.Login, UAuthClientProfile.BlazorServer); - var second = provider.GetEffective(ctx, AuthFlowType.Login, UAuthClientProfile.BlazorServer); + var effective = provider.GetEffective( + ctx, + AuthFlowType.Login, + UAuthClientProfile.BlazorServer); - Assert.NotSame(first.Options, second.Options); - } + Assert.Equal( + TimeSpan.FromMinutes(1), + effective.Options.Tokens.AccessTokenLifetime + ); + } - // TODO: Discuss and enable - //[Fact] - //public void FlowType_Is_Passed_To_ModeResolver() - //{ - // var baseOptions = new UAuthServerOptions - // { - // Mode = null - // }; + [Fact] + public void Each_Call_Returns_New_EffectiveOptions_Instance() + { + var baseOptions = new UAuthServerOptions + { + Mode = UAuthMode.Hybrid + }; - // var provider = TestHelpers.CreateEffectiveOptionsProvider(baseOptions); - // var ctx = new DefaultHttpContext(); + var provider = TestHelpers.CreateEffectiveOptionsProvider(baseOptions); + var ctx = new DefaultHttpContext(); - // var login = provider.GetEffective( - // ctx, - // AuthFlowType.Login, - // UAuthClientProfile.Api); + var first = provider.GetEffective(ctx, AuthFlowType.Login, UAuthClientProfile.BlazorServer); + var second = provider.GetEffective(ctx, AuthFlowType.Login, UAuthClientProfile.BlazorServer); - // var api = provider.GetEffective( - // ctx, - // AuthFlowType.ApiAccess, - // UAuthClientProfile.Api); + Assert.NotSame(first.Options, second.Options); + } - // Assert.NotEqual(login.Mode, api.Mode); - //} + // TODO: Discuss and enable + //[Fact] + //public void FlowType_Is_Passed_To_ModeResolver() + //{ + // var baseOptions = new UAuthServerOptions + // { + // Mode = null + // }; + + // var provider = TestHelpers.CreateEffectiveOptionsProvider(baseOptions); + // var ctx = new DefaultHttpContext(); + + // var login = provider.GetEffective( + // ctx, + // AuthFlowType.Login, + // UAuthClientProfile.Api); + + // var api = provider.GetEffective( + // ctx, + // AuthFlowType.ApiAccess, + // UAuthClientProfile.Api); + + // Assert.NotEqual(login.Mode, api.Mode); + //} - } } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/TestHelpers.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/TestHelpers.cs index 46083ba0..bcfe4c34 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/TestHelpers.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/TestHelpers.cs @@ -2,13 +2,12 @@ using CodeBeam.UltimateAuth.Server.Options; using Microsoft.Extensions.Options; -namespace CodeBeam.UltimateAuth.Tests.Unit +namespace CodeBeam.UltimateAuth.Tests.Unit; + +internal static class TestHelpers { - internal static class TestHelpers + public static EffectiveServerOptionsProvider CreateEffectiveOptionsProvider(UAuthServerOptions options, IEffectiveAuthModeResolver? modeResolver = null) { - public static DefaultEffectiveServerOptionsProvider CreateEffectiveOptionsProvider(UAuthServerOptions options, IEffectiveAuthModeResolver? modeResolver = null) - { - return new DefaultEffectiveServerOptionsProvider(Options.Create(options), modeResolver ?? new DefaultEffectiveAuthModeResolver()); - } + return new EffectiveServerOptionsProvider(Options.Create(options), modeResolver ?? new EffectiveAuthModeResolver()); } } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/TestIds.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/TestIds.cs index ee19e828..4aa8dbc8 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/TestIds.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/TestIds.cs @@ -1,18 +1,14 @@ using CodeBeam.UltimateAuth.Core.Domain; -using System; -using System.Collections.Generic; -using System.Text; -namespace CodeBeam.UltimateAuth.Tests.Unit +namespace CodeBeam.UltimateAuth.Tests.Unit; + +internal static class TestIds { - internal static class TestIds + public static AuthSessionId Session(string raw) { - public static AuthSessionId Session(string raw) - { - if (!AuthSessionId.TryCreate(raw, out var id)) - throw new InvalidOperationException($"Invalid test AuthSessionId: {raw}"); + if (!AuthSessionId.TryCreate(raw, out var id)) + throw new InvalidOperationException($"Invalid test AuthSessionId: {raw}"); - return id; - } + return id; } } From e7e4d8d3c789002d11dd7ad0f92a1ce487c8489e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Can=20Karag=C3=B6z?= Date: Tue, 3 Feb 2026 23:26:23 +0300 Subject: [PATCH 9/9] Fix Compiler Warnings & Last Refactorings --- .../Components/Pages/Home.razor.cs | 8 +- .../Components/Pages/Home.razor | 2 +- .../Pages/Home.razor.cs | 2 +- .../Authentication/UAuthStateManager.cs | 2 +- .../UAuthAuthenticationState.razor.cs | 2 +- .../Infrastructure/NoOpSessionCoordinator.cs | 2 + .../Domain/Hub/HubSessionId.cs | 19 +++- .../Domain/Session/AuthSessionId.cs | 2 +- .../Domain/Session/UAuthSession.cs | 4 +- .../UAuthChallengeRequiredException.cs | 11 +-- .../Base/UAuthAuthorizationException.cs | 11 +-- .../Errors/Base/UAuthChainException.cs | 18 ++-- .../Errors/Base/UAuthDeveloperException.cs | 27 +++--- .../Errors/Base/UAuthDomainException.cs | 23 +++-- .../Errors/Base/UAuthException.cs | 39 ++++---- .../Errors/Base/UAuthSessionException.cs | 40 ++++---- .../Errors/Developer/UAuthConfigException.cs | 27 +++--- .../Developer/UAuthInternalException.cs | 31 +++--- .../Errors/Developer/UAuthStoreException.cs | 27 +++--- .../Session/UAuthChainLinkMissingException.cs | 13 +-- .../UAuthSessionChainNotFoundException.cs | 11 +-- .../UAuthSessionChainRevokedException.cs | 13 +-- .../UAuthSessionDeviceMismatchException.cs | 29 +++--- .../UAuthSessionInvalidStateException.cs | 21 ++-- .../Session/UAuthSessionNotActiveException.cs | 33 ++++--- .../Session/UAuthSessionNotFoundException.cs | 12 +-- .../Session/UAuthSessionRevokedException.cs | 37 ++++---- .../UAuthSessionRootRevokedException.cs | 17 ++-- .../UAuthSessionSecurityMismatchException.cs | 3 +- .../Errors/UAuthDeviceLimitException.cs | 37 ++++---- .../UAuthInvalidCredentialsException.cs | 23 +++-- .../Errors/UAuthInvalidPkceCodeException.cs | 27 +++--- .../Errors/UAuthRootRevokedException.cs | 27 +++--- .../Errors/UAuthTokenTamperedException.cs | 39 ++++---- .../Events/IAuthEventContext.cs | 13 ++- .../Events/SessionCreatedContext.cs | 73 +++++++------- .../Events/SessionRefreshedContext.cs | 95 +++++++++---------- .../Events/SessionRevokedContext.cs | 90 +++++++++--------- .../Events/UAuthEventDispatcher.cs | 87 +++++++++-------- .../Events/UAuthEvents.cs | 95 +++++++++---------- .../Events/UserLoggedInContext.cs | 69 +++++++------- .../Events/UserLoggedOutContext.cs | 67 +++++++------ .../Infrastructure/UAuthUserIdConverter.cs | 8 +- .../Auth/ClientProfileReader.cs | 2 +- .../UAuthAuthenticationHandler.cs | 6 +- .../Endpoints/ValidateEndpointHandler.cs | 14 ++- .../Hub/HubCredentialResolver.cs | 4 +- .../Issuers/UAuthTokenIssuer.cs | 6 +- .../Services/RefreshTokenRotationService.cs | 12 ++- .../Endpoints/AuthorizationEndpointHandler.cs | 6 ++ .../EntityProjections/SessionProjection.cs | 2 +- .../EfCoreTokenStore.cs | 3 + 52 files changed, 647 insertions(+), 644 deletions(-) diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/Pages/Home.razor.cs b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/Pages/Home.razor.cs index 00bebb1b..5d81798e 100644 --- a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/Pages/Home.razor.cs +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/Pages/Home.razor.cs @@ -27,7 +27,8 @@ protected override async Task OnParametersSetAsync() return; } - _state = await HubFlowReader.GetStateAsync(new HubSessionId(HubKey)); + if (!HubSessionId.TryParse(HubKey, out var hubSessionId)) + _state = await HubFlowReader.GetStateAsync(hubSessionId); } protected override async Task OnAfterRenderAsync(bool firstRender) @@ -74,7 +75,10 @@ private async Task ProgrammaticPkceLogin() if (hub is null) return; - var credentials = await HubCredentialResolver.ResolveAsync(new HubSessionId(HubKey)); + if (!HubSessionId.TryParse(HubKey, out var hubSessionId)) + return; + + var credentials = await HubCredentialResolver.ResolveAsync(hubSessionId); var request = new PkceLoginRequest { diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Home.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Home.razor index ebf7a93f..259133b0 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Home.razor +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Home.razor @@ -60,7 +60,7 @@ UAuthState @(StateManager.State.IsAuthenticated == true ? "Authenticated" : "Not Authenticated") - UserId:@(StateManager.State.UserKey) - Authorized context is shown. @context.User.Identity.IsAuthenticated + Authorized context is shown. @context?.User?.Identity?.IsAuthenticated Not Authorized context is shown. diff --git a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Home.razor.cs b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Home.razor.cs index 44c873a9..e9530d89 100644 --- a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Home.razor.cs +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Home.razor.cs @@ -11,7 +11,7 @@ namespace CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm.Pages public partial class Home { [CascadingParameter] - public UAuthState Auth { get; set; } + public UAuthState Auth { get; set; } = null!; private string? _username; private string? _password; diff --git a/src/CodeBeam.UltimateAuth.Client/Authentication/UAuthStateManager.cs b/src/CodeBeam.UltimateAuth.Client/Authentication/UAuthStateManager.cs index 4b0196a5..55e3f7e1 100644 --- a/src/CodeBeam.UltimateAuth.Client/Authentication/UAuthStateManager.cs +++ b/src/CodeBeam.UltimateAuth.Client/Authentication/UAuthStateManager.cs @@ -26,7 +26,7 @@ public async Task EnsureAsync(CancellationToken ct = default) await _bootstrapper.EnsureStartedAsync(); var result = await _client.Flows.ValidateAsync(); - if (!result.IsValid) + if (!result.IsValid || result.Snapshot == null) { State.Clear(); return; diff --git a/src/CodeBeam.UltimateAuth.Client/Components/UAuthAuthenticationState.razor.cs b/src/CodeBeam.UltimateAuth.Client/Components/UAuthAuthenticationState.razor.cs index 96b1c37c..c02a9c5a 100644 --- a/src/CodeBeam.UltimateAuth.Client/Components/UAuthAuthenticationState.razor.cs +++ b/src/CodeBeam.UltimateAuth.Client/Components/UAuthAuthenticationState.razor.cs @@ -6,7 +6,7 @@ namespace CodeBeam.UltimateAuth.Client.Components; public partial class UAuthAuthenticationState { private bool _initialized; - private UAuthState _uauthState; + private UAuthState _uauthState = UAuthState.Anonymous(); [Parameter] public RenderFragment ChildContent { get; set; } = default!; diff --git a/src/CodeBeam.UltimateAuth.Client/Infrastructure/NoOpSessionCoordinator.cs b/src/CodeBeam.UltimateAuth.Client/Infrastructure/NoOpSessionCoordinator.cs index 19fb3d96..800515b8 100644 --- a/src/CodeBeam.UltimateAuth.Client/Infrastructure/NoOpSessionCoordinator.cs +++ b/src/CodeBeam.UltimateAuth.Client/Infrastructure/NoOpSessionCoordinator.cs @@ -4,7 +4,9 @@ namespace CodeBeam.UltimateAuth.Client.Infrastructure; internal sealed class NoOpSessionCoordinator : ISessionCoordinator { +#pragma warning disable CS0067 public event Action? ReauthRequired; +#pragma warning restore CS0067 public Task StartAsync(CancellationToken cancellationToken = default) => Task.CompletedTask; public Task StopAsync() => Task.CompletedTask; diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Hub/HubSessionId.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Hub/HubSessionId.cs index 7f34bcff..ace40a14 100644 --- a/src/CodeBeam.UltimateAuth.Core/Domain/Hub/HubSessionId.cs +++ b/src/CodeBeam.UltimateAuth.Core/Domain/Hub/HubSessionId.cs @@ -1,11 +1,24 @@ namespace CodeBeam.UltimateAuth.Core.Domain; // TODO: Bind id with IP and UA -public readonly record struct HubSessionId(string Value) +public readonly record struct HubSessionId { + public string Value { get; } + + private HubSessionId(string value) + { + Value = value; + } + public static HubSessionId New() => new(Guid.NewGuid().ToString("N")); - public override string ToString() => Value; + public static HubSessionId Parse(string value) + { + if (!TryParse(value, out var id)) + throw new FormatException("Invalid HubSessionId."); + + return id; + } public static bool TryParse(string? value, out HubSessionId sessionId) { @@ -20,4 +33,6 @@ public static bool TryParse(string? value, out HubSessionId sessionId) sessionId = new HubSessionId(value); return true; } + + public override string ToString() => Value; } diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Session/AuthSessionId.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Session/AuthSessionId.cs index c07fadc0..ec3cdcfd 100644 --- a/src/CodeBeam.UltimateAuth.Core/Domain/Session/AuthSessionId.cs +++ b/src/CodeBeam.UltimateAuth.Core/Domain/Session/AuthSessionId.cs @@ -10,7 +10,7 @@ private AuthSessionId(string value) Value = value; } - public static bool TryCreate(string raw, out AuthSessionId id) + public static bool TryCreate(string? raw, out AuthSessionId id) { if (string.IsNullOrWhiteSpace(raw)) { diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSession.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSession.cs index 81b08720..bcb8b2a2 100644 --- a/src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSession.cs +++ b/src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSession.cs @@ -56,7 +56,7 @@ public static UAuthSession Create( DateTimeOffset now, DateTimeOffset expiresAt, DeviceContext device, - ClaimsSnapshot claims, + ClaimsSnapshot? claims, SessionMetadata metadata) { return new( @@ -71,7 +71,7 @@ public static UAuthSession Create( revokedAt: null, securityVersionAtCreation: 0, device: device, - claims: claims, + claims: claims ?? ClaimsSnapshot.Empty, metadata: metadata ); } diff --git a/src/CodeBeam.UltimateAuth.Core/Errors/Authorization/UAuthChallengeRequiredException.cs b/src/CodeBeam.UltimateAuth.Core/Errors/Authorization/UAuthChallengeRequiredException.cs index e927d419..892c6e06 100644 --- a/src/CodeBeam.UltimateAuth.Core/Errors/Authorization/UAuthChallengeRequiredException.cs +++ b/src/CodeBeam.UltimateAuth.Core/Errors/Authorization/UAuthChallengeRequiredException.cs @@ -1,10 +1,9 @@ -namespace CodeBeam.UltimateAuth.Core.Errors +namespace CodeBeam.UltimateAuth.Core.Errors; + +public sealed class UAuthChallengeRequiredException : UAuthException { - public sealed class UAuthChallengeRequiredException : UAuthException + public UAuthChallengeRequiredException(string? reason = null) + : base(reason ?? "Additional authentication is required to perform this operation.") { - public UAuthChallengeRequiredException(string? reason = null) - : base(reason ?? "Additional authentication is required to perform this operation.") - { - } } } diff --git a/src/CodeBeam.UltimateAuth.Core/Errors/Base/UAuthAuthorizationException.cs b/src/CodeBeam.UltimateAuth.Core/Errors/Base/UAuthAuthorizationException.cs index f7bbf0ee..783b3125 100644 --- a/src/CodeBeam.UltimateAuth.Core/Errors/Base/UAuthAuthorizationException.cs +++ b/src/CodeBeam.UltimateAuth.Core/Errors/Base/UAuthAuthorizationException.cs @@ -1,10 +1,9 @@ -namespace CodeBeam.UltimateAuth.Core.Errors +namespace CodeBeam.UltimateAuth.Core.Errors; + +public sealed class UAuthAuthorizationException : UAuthException { - public sealed class UAuthAuthorizationException : UAuthException + public UAuthAuthorizationException(string? reason = null) + : base(reason ?? "The current principal is not authorized to perform this operation.") { - public UAuthAuthorizationException(string? reason = null) - : base(reason ?? "The current principal is not authorized to perform this operation.") - { - } } } diff --git a/src/CodeBeam.UltimateAuth.Core/Errors/Base/UAuthChainException.cs b/src/CodeBeam.UltimateAuth.Core/Errors/Base/UAuthChainException.cs index a62b507c..fee2bb4a 100644 --- a/src/CodeBeam.UltimateAuth.Core/Errors/Base/UAuthChainException.cs +++ b/src/CodeBeam.UltimateAuth.Core/Errors/Base/UAuthChainException.cs @@ -1,17 +1,13 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Core.Errors +namespace CodeBeam.UltimateAuth.Core.Errors; + +public abstract class UAuthChainException : UAuthDomainException { - public abstract class UAuthChainException : UAuthDomainException - { - public SessionChainId ChainId { get; } + public SessionChainId ChainId { get; } - protected UAuthChainException( - SessionChainId chainId, - string message) - : base(message) - { - ChainId = chainId; - } + protected UAuthChainException(SessionChainId chainId, string message) : base(message) + { + ChainId = chainId; } } diff --git a/src/CodeBeam.UltimateAuth.Core/Errors/Base/UAuthDeveloperException.cs b/src/CodeBeam.UltimateAuth.Core/Errors/Base/UAuthDeveloperException.cs index 39b340d4..bc55cadd 100644 --- a/src/CodeBeam.UltimateAuth.Core/Errors/Base/UAuthDeveloperException.cs +++ b/src/CodeBeam.UltimateAuth.Core/Errors/Base/UAuthDeveloperException.cs @@ -1,18 +1,17 @@ -namespace CodeBeam.UltimateAuth.Core.Errors +namespace CodeBeam.UltimateAuth.Core.Errors; + +/// +/// Represents an exception that indicates a developer integration error +/// rather than a runtime or authentication failure. +/// These errors typically occur when UltimateAuth is misconfigured, +/// required services are not registered, or contracts are violated by the host application. +/// +public abstract class UAuthDeveloperException : UAuthException { /// - /// Represents an exception that indicates a developer integration error - /// rather than a runtime or authentication failure. - /// These errors typically occur when UltimateAuth is misconfigured, - /// required services are not registered, or contracts are violated by the host application. + /// Initializes a new instance of the class + /// with a specified error message describing the developer mistake. /// - public abstract class UAuthDeveloperException : UAuthException - { - /// - /// Initializes a new instance of the class - /// with a specified error message describing the developer mistake. - /// - /// The error message explaining the incorrect usage. - protected UAuthDeveloperException(string message) : base(message) { } - } + /// The error message explaining the incorrect usage. + protected UAuthDeveloperException(string message) : base(message) { } } diff --git a/src/CodeBeam.UltimateAuth.Core/Errors/Base/UAuthDomainException.cs b/src/CodeBeam.UltimateAuth.Core/Errors/Base/UAuthDomainException.cs index 4894aabf..7a19e378 100644 --- a/src/CodeBeam.UltimateAuth.Core/Errors/Base/UAuthDomainException.cs +++ b/src/CodeBeam.UltimateAuth.Core/Errors/Base/UAuthDomainException.cs @@ -1,16 +1,15 @@ -namespace CodeBeam.UltimateAuth.Core.Errors +namespace CodeBeam.UltimateAuth.Core.Errors; + +/// +/// Represents an exception triggered by a violation of UltimateAuth's domain rules or invariants. +/// These errors indicate that a business rule or authentication domain constraint has been broken (e.g., invalid session state transition, +/// illegal revoke action, or inconsistent security version). +/// +public abstract class UAuthDomainException : UAuthException { /// - /// Represents an exception triggered by a violation of UltimateAuth's domain rules or invariants. - /// These errors indicate that a business rule or authentication domain constraint has been broken (e.g., invalid session state transition, - /// illegal revoke action, or inconsistent security version). + /// Initializes a new instance of the class with a message describing the violated domain rule. /// - public abstract class UAuthDomainException : UAuthException - { - /// - /// Initializes a new instance of the class with a message describing the violated domain rule. - /// - /// The descriptive message for the domain error. - protected UAuthDomainException(string message) : base(message) { } - } + /// The descriptive message for the domain error. + protected UAuthDomainException(string message) : base(message) { } } diff --git a/src/CodeBeam.UltimateAuth.Core/Errors/Base/UAuthException.cs b/src/CodeBeam.UltimateAuth.Core/Errors/Base/UAuthException.cs index 08ea3e15..13662dc2 100644 --- a/src/CodeBeam.UltimateAuth.Core/Errors/Base/UAuthException.cs +++ b/src/CodeBeam.UltimateAuth.Core/Errors/Base/UAuthException.cs @@ -1,25 +1,24 @@ -namespace CodeBeam.UltimateAuth.Core.Errors +namespace CodeBeam.UltimateAuth.Core.Errors; + +/// +/// Represents the base type for all exceptions thrown by the UltimateAuth framework. +/// This class differentiates authentication-domain errors from general system exceptions +/// and provides a common abstraction for developer, domain, and runtime error types. +/// +public abstract class UAuthException : Exception { /// - /// Represents the base type for all exceptions thrown by the UltimateAuth framework. - /// This class differentiates authentication-domain errors from general system exceptions - /// and provides a common abstraction for developer, domain, and runtime error types. + /// Initializes a new instance of the class + /// with the specified error message. /// - public abstract class UAuthException : Exception - { - /// - /// Initializes a new instance of the class - /// with the specified error message. - /// - /// The message that describes the error. - protected UAuthException(string message) : base(message) { } + /// The message that describes the error. + protected UAuthException(string message) : base(message) { } - /// - /// Initializes a new instance of the class - /// with the specified error message and underlying exception. - /// - /// The message that describes the error. - /// The exception that caused the current error. - protected UAuthException(string message, Exception? inner) : base(message, inner) { } - } + /// + /// Initializes a new instance of the class + /// with the specified error message and underlying exception. + /// + /// The message that describes the error. + /// The exception that caused the current error. + protected UAuthException(string message, Exception? inner) : base(message, inner) { } } diff --git a/src/CodeBeam.UltimateAuth.Core/Errors/Base/UAuthSessionException.cs b/src/CodeBeam.UltimateAuth.Core/Errors/Base/UAuthSessionException.cs index 7836b57b..a27d9b9f 100644 --- a/src/CodeBeam.UltimateAuth.Core/Errors/Base/UAuthSessionException.cs +++ b/src/CodeBeam.UltimateAuth.Core/Errors/Base/UAuthSessionException.cs @@ -1,29 +1,27 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Core.Errors +namespace CodeBeam.UltimateAuth.Core.Errors; + +/// +/// Represents a domain-level exception associated with a specific authentication session. +/// This error indicates that a session-related invariant or rule has been violated, +/// such as attempting to refresh a revoked session, using an expired session, +/// or performing an operation that conflicts with the session's current state. +/// +public abstract class UAuthSessionException : UAuthDomainException { /// - /// Represents a domain-level exception associated with a specific authentication session. - /// This error indicates that a session-related invariant or rule has been violated, - /// such as attempting to refresh a revoked session, using an expired session, - /// or performing an operation that conflicts with the session's current state. + /// Gets the identifier of the session that triggered the exception. /// - public abstract class UAuthSessionException : UAuthDomainException - { - /// - /// Gets the identifier of the session that triggered the exception. - /// - public AuthSessionId SessionId { get; } + public AuthSessionId SessionId { get; } - /// - /// Initializes a new instance of the class with the session identifier and an explanatory error message. - /// - /// The session identifier associated with the error. - /// The message describing the session rule violation. - protected UAuthSessionException(AuthSessionId sessionId, string message) : base(message) - { - SessionId = sessionId; - } + /// + /// Initializes a new instance of the class with the session identifier and an explanatory error message. + /// + /// The session identifier associated with the error. + /// The message describing the session rule violation. + protected UAuthSessionException(AuthSessionId sessionId, string message) : base(message) + { + SessionId = sessionId; } - } diff --git a/src/CodeBeam.UltimateAuth.Core/Errors/Developer/UAuthConfigException.cs b/src/CodeBeam.UltimateAuth.Core/Errors/Developer/UAuthConfigException.cs index d247a6b8..8710b51e 100644 --- a/src/CodeBeam.UltimateAuth.Core/Errors/Developer/UAuthConfigException.cs +++ b/src/CodeBeam.UltimateAuth.Core/Errors/Developer/UAuthConfigException.cs @@ -1,18 +1,17 @@ -namespace CodeBeam.UltimateAuth.Core.Errors +namespace CodeBeam.UltimateAuth.Core.Errors; + +/// +/// Represents an exception that is thrown when UltimateAuth is configured +/// incorrectly or when required configuration values are missing or invalid. +/// This error indicates a developer-side setup issue rather than a runtime +/// authentication failure. +/// +public sealed class UAuthConfigException : UAuthDeveloperException { /// - /// Represents an exception that is thrown when UltimateAuth is configured - /// incorrectly or when required configuration values are missing or invalid. - /// This error indicates a developer-side setup issue rather than a runtime - /// authentication failure. + /// Initializes a new instance of the class + /// with a descriptive message explaining the configuration problem. /// - public sealed class UAuthConfigException : UAuthDeveloperException - { - /// - /// Initializes a new instance of the class - /// with a descriptive message explaining the configuration problem. - /// - /// The message describing the configuration error. - public UAuthConfigException(string message) : base(message) { } - } + /// The message describing the configuration error. + public UAuthConfigException(string message) : base(message) { } } diff --git a/src/CodeBeam.UltimateAuth.Core/Errors/Developer/UAuthInternalException.cs b/src/CodeBeam.UltimateAuth.Core/Errors/Developer/UAuthInternalException.cs index ad6678a3..703b8acb 100644 --- a/src/CodeBeam.UltimateAuth.Core/Errors/Developer/UAuthInternalException.cs +++ b/src/CodeBeam.UltimateAuth.Core/Errors/Developer/UAuthInternalException.cs @@ -1,20 +1,19 @@ -namespace CodeBeam.UltimateAuth.Core.Errors +namespace CodeBeam.UltimateAuth.Core.Errors; + +/// +/// Represents an unexpected internal error within the UltimateAuth framework. +/// This exception indicates a failure in internal logic, invariants, or service +/// coordination, rather than a configuration or authentication mistake by the developer. +/// +/// If this exception occurs, it typically means a bug or unhandled scenario +/// exists inside the framework itself. +/// +public sealed class UAuthInternalException : UAuthDeveloperException { /// - /// Represents an unexpected internal error within the UltimateAuth framework. - /// This exception indicates a failure in internal logic, invariants, or service - /// coordination, rather than a configuration or authentication mistake by the developer. - /// - /// If this exception occurs, it typically means a bug or unhandled scenario - /// exists inside the framework itself. + /// Initializes a new instance of the class + /// with a descriptive message explaining the internal framework error. /// - public sealed class UAuthInternalException : UAuthDeveloperException - { - /// - /// Initializes a new instance of the class - /// with a descriptive message explaining the internal framework error. - /// - /// The internal error message. - public UAuthInternalException(string message) : base(message) { } - } + /// The internal error message. + public UAuthInternalException(string message) : base(message) { } } diff --git a/src/CodeBeam.UltimateAuth.Core/Errors/Developer/UAuthStoreException.cs b/src/CodeBeam.UltimateAuth.Core/Errors/Developer/UAuthStoreException.cs index 13465d93..0d5eae3d 100644 --- a/src/CodeBeam.UltimateAuth.Core/Errors/Developer/UAuthStoreException.cs +++ b/src/CodeBeam.UltimateAuth.Core/Errors/Developer/UAuthStoreException.cs @@ -1,18 +1,17 @@ -namespace CodeBeam.UltimateAuth.Core.Errors +namespace CodeBeam.UltimateAuth.Core.Errors; + +/// +/// Represents an exception that occurs when a session or user store +/// behaves incorrectly or violates the UltimateAuth storage contract. +/// This typically indicates an implementation error in the application's +/// persistence layer rather than a framework or authentication issue. +/// +public sealed class UAuthStoreException : UAuthDeveloperException { /// - /// Represents an exception that occurs when a session or user store - /// behaves incorrectly or violates the UltimateAuth storage contract. - /// This typically indicates an implementation error in the application's - /// persistence layer rather than a framework or authentication issue. + /// Initializes a new instance of the class + /// with a descriptive message explaining the store failure. /// - public sealed class UAuthStoreException : UAuthDeveloperException - { - /// - /// Initializes a new instance of the class - /// with a descriptive message explaining the store failure. - /// - /// The message describing the store-related error. - public UAuthStoreException(string message) : base(message) { } - } + /// The message describing the store-related error. + public UAuthStoreException(string message) : base(message) { } } diff --git a/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthChainLinkMissingException.cs b/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthChainLinkMissingException.cs index 68ce8bd5..2294ae56 100644 --- a/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthChainLinkMissingException.cs +++ b/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthChainLinkMissingException.cs @@ -1,14 +1,11 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Core.Errors +namespace CodeBeam.UltimateAuth.Core.Errors; + +public sealed class UAuthSessionChainLinkMissingException : UAuthSessionException { - public sealed class UAuthSessionChainLinkMissingException : UAuthSessionException + public UAuthSessionChainLinkMissingException(AuthSessionId sessionId) + : base(sessionId, $"Session '{sessionId}' is not associated with any session chain.") { - public UAuthSessionChainLinkMissingException(AuthSessionId sessionId) - : base( - sessionId, - $"Session '{sessionId}' is not associated with any session chain.") - { - } } } diff --git a/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionChainNotFoundException.cs b/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionChainNotFoundException.cs index 91d0bafa..40a9b8d0 100644 --- a/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionChainNotFoundException.cs +++ b/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionChainNotFoundException.cs @@ -1,12 +1,11 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Core.Errors +namespace CodeBeam.UltimateAuth.Core.Errors; + +public sealed class UAuthSessionChainNotFoundException : UAuthChainException { - public sealed class UAuthSessionChainNotFoundException : UAuthChainException + public UAuthSessionChainNotFoundException(SessionChainId chainId) + : base(chainId, $"Session chain '{chainId}' was not found.") { - public UAuthSessionChainNotFoundException(SessionChainId chainId) - : base(chainId, $"Session chain '{chainId}' was not found.") - { - } } } diff --git a/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionChainRevokedException.cs b/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionChainRevokedException.cs index bd880af9..bc08a223 100644 --- a/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionChainRevokedException.cs +++ b/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionChainRevokedException.cs @@ -1,14 +1,11 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Core.Errors +namespace CodeBeam.UltimateAuth.Core.Errors; + +public sealed class UAuthSessionChainRevokedException : UAuthChainException { - public sealed class UAuthSessionChainRevokedException : UAuthChainException + public UAuthSessionChainRevokedException(SessionChainId chainId) + : base(chainId, $"Session chain '{chainId}' has been revoked.") { - public SessionChainId ChainId { get; } - - public UAuthSessionChainRevokedException(SessionChainId chainId) - : base(chainId, $"Session chain '{chainId}' has been revoked.") - { - } } } diff --git a/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionDeviceMismatchException.cs b/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionDeviceMismatchException.cs index 07e425e1..f6435ad8 100644 --- a/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionDeviceMismatchException.cs +++ b/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionDeviceMismatchException.cs @@ -1,23 +1,16 @@ -using CodeBeam.UltimateAuth.Core.Contracts; -using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Core.Errors +namespace CodeBeam.UltimateAuth.Core.Errors; + +public sealed class UAuthSessionDeviceMismatchException : UAuthSessionException { - public sealed class UAuthSessionDeviceMismatchException : UAuthSessionException - { - public DeviceInfo Expected { get; } - public DeviceInfo Actual { get; } + public DeviceContext Expected { get; } + public DeviceContext Actual { get; } - public UAuthSessionDeviceMismatchException( - AuthSessionId sessionId, - DeviceInfo expected, - DeviceInfo actual) - : base( - sessionId, - $"Session '{sessionId}' device mismatch detected.") - { - Expected = expected; - Actual = actual; - } + public UAuthSessionDeviceMismatchException(AuthSessionId sessionId, DeviceContext expected, DeviceContext actual) + : base(sessionId, $"Session '{sessionId}' device mismatch detected.") + { + Expected = expected; + Actual = actual; } } diff --git a/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionInvalidStateException.cs b/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionInvalidStateException.cs index bd396bd3..64d5b793 100644 --- a/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionInvalidStateException.cs +++ b/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionInvalidStateException.cs @@ -1,19 +1,14 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Core.Errors +namespace CodeBeam.UltimateAuth.Core.Errors; + +public sealed class UAuthSessionInvalidStateException : UAuthSessionException { - public sealed class UAuthSessionInvalidStateException : UAuthSessionException - { - public SessionState State { get; } + public SessionState State { get; } - public UAuthSessionInvalidStateException( - AuthSessionId sessionId, - SessionState state) - : base( - sessionId, - $"Session '{sessionId}' is in invalid state '{state}'.") - { - State = state; - } + public UAuthSessionInvalidStateException(AuthSessionId sessionId, SessionState state) + : base(sessionId, $"Session '{sessionId}' is in invalid state '{state}'.") + { + State = state; } } diff --git a/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionNotActiveException.cs b/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionNotActiveException.cs index 21f5aae7..a336dd17 100644 --- a/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionNotActiveException.cs +++ b/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionNotActiveException.cs @@ -1,25 +1,24 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Core.Errors +namespace CodeBeam.UltimateAuth.Core.Errors; + +/// +/// Represents an authentication-domain exception thrown when a session exists +/// but is not in the state. +/// This exception typically occurs during validation or refresh operations when: +/// - the session is revoked, +/// - the session has expired, +/// - the session belongs to a revoked chain, +/// - or the session is otherwise considered inactive by the runtime state machine. +/// Only active sessions are eligible for refresh and token issuance. +/// +public sealed class UAuthSessionNotActiveException : UAuthSessionException { /// - /// Represents an authentication-domain exception thrown when a session exists - /// but is not in the state. - /// This exception typically occurs during validation or refresh operations when: - /// - the session is revoked, - /// - the session has expired, - /// - the session belongs to a revoked chain, - /// - or the session is otherwise considered inactive by the runtime state machine. - /// Only active sessions are eligible for refresh and token issuance. + /// Initializes a new instance of the class. /// - public sealed class UAuthSessionNotActiveException : UAuthSessionException + /// The identifier of the session that is not active. + public UAuthSessionNotActiveException(AuthSessionId sessionId) : base(sessionId, $"Session '{sessionId}' is not active.") { - /// - /// Initializes a new instance of the class. - /// - /// The identifier of the session that is not active. - public UAuthSessionNotActiveException(AuthSessionId sessionId) : base(sessionId, $"Session '{sessionId}' is not active.") - { - } } } diff --git a/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionNotFoundException.cs b/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionNotFoundException.cs index 8cc0a593..b7f32d8f 100644 --- a/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionNotFoundException.cs +++ b/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionNotFoundException.cs @@ -1,13 +1,11 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Core.Errors +namespace CodeBeam.UltimateAuth.Core.Errors; + +public sealed class UAuthSessionNotFoundException : UAuthSessionException { - public sealed class UAuthSessionNotFoundException - : UAuthSessionException + public UAuthSessionNotFoundException(AuthSessionId sessionId) + : base(sessionId, $"Session '{sessionId}' was not found.") { - public UAuthSessionNotFoundException(AuthSessionId sessionId) - : base(sessionId, $"Session '{sessionId}' was not found.") - { - } } } diff --git a/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionRevokedException.cs b/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionRevokedException.cs index 7d7660c3..1bc96c93 100644 --- a/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionRevokedException.cs +++ b/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionRevokedException.cs @@ -1,27 +1,26 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Core.Errors +namespace CodeBeam.UltimateAuth.Core.Errors; + +/// +/// Represents an authentication-domain exception thrown when an operation attempts +/// to use a session that has been explicitly revoked by the user, administrator, +/// or by system-driven security policies. +/// +/// A revoked session is permanently invalid and cannot be refreshed, validated, +/// or used to obtain new tokens. Revocation typically occurs during actions such as +/// logout, device removal, or administrative account lockdown. +/// +/// This exception is raised in scenarios where a caller assumes the session is active +/// but the underlying session state indicates . +/// +public sealed class UAuthSessionRevokedException : UAuthSessionException { /// - /// Represents an authentication-domain exception thrown when an operation attempts - /// to use a session that has been explicitly revoked by the user, administrator, - /// or by system-driven security policies. - /// - /// A revoked session is permanently invalid and cannot be refreshed, validated, - /// or used to obtain new tokens. Revocation typically occurs during actions such as - /// logout, device removal, or administrative account lockdown. - /// - /// This exception is raised in scenarios where a caller assumes the session is active - /// but the underlying session state indicates . + /// Initializes a new instance of the class. /// - public sealed class UAuthSessionRevokedException : UAuthSessionException + /// The identifier of the revoked session. + public UAuthSessionRevokedException(AuthSessionId sessionId) : base(sessionId, $"Session '{sessionId}' has been revoked.") { - /// - /// Initializes a new instance of the class. - /// - /// The identifier of the revoked session. - public UAuthSessionRevokedException(AuthSessionId sessionId) : base(sessionId, $"Session '{sessionId}' has been revoked.") - { - } } } diff --git a/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionRootRevokedException.cs b/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionRootRevokedException.cs index f1c89786..985eaef7 100644 --- a/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionRootRevokedException.cs +++ b/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionRootRevokedException.cs @@ -1,13 +1,12 @@ -namespace CodeBeam.UltimateAuth.Core.Errors +namespace CodeBeam.UltimateAuth.Core.Errors; + +public sealed class UAuthSessionRootRevokedException : Exception { - public sealed class UAuthSessionRootRevokedException : Exception - { - public object UserId { get; } + public object UserId { get; } - public UAuthSessionRootRevokedException(object userId) - : base("All sessions for the user have been revoked.") - { - UserId = userId; - } + public UAuthSessionRootRevokedException(object userId) + : base("All sessions for the user have been revoked.") + { + UserId = userId; } } diff --git a/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionSecurityMismatchException.cs b/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionSecurityMismatchException.cs index 9ba17f19..657fc543 100644 --- a/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionSecurityMismatchException.cs +++ b/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionSecurityMismatchException.cs @@ -1,5 +1,6 @@ using CodeBeam.UltimateAuth.Core.Domain; -using CodeBeam.UltimateAuth.Core.Errors; + +namespace CodeBeam.UltimateAuth.Core.Errors; public sealed class UAuthSessionSecurityMismatchException : UAuthSessionException { diff --git a/src/CodeBeam.UltimateAuth.Core/Errors/UAuthDeviceLimitException.cs b/src/CodeBeam.UltimateAuth.Core/Errors/UAuthDeviceLimitException.cs index 16152021..c4aaa375 100644 --- a/src/CodeBeam.UltimateAuth.Core/Errors/UAuthDeviceLimitException.cs +++ b/src/CodeBeam.UltimateAuth.Core/Errors/UAuthDeviceLimitException.cs @@ -1,25 +1,24 @@ -namespace CodeBeam.UltimateAuth.Core.Errors +namespace CodeBeam.UltimateAuth.Core.Errors; + +/// +/// Represents a domain-level exception that is thrown when a user exceeds the allowed number of device or platform-specific session chains. +/// This typically occurs when UltimateAuth's session policy restricts the +/// number of concurrent logins for a given platform (e.g., web, mobile) +/// and the user attempts to create an additional session beyond the limit. +/// +public sealed class UAuthDeviceLimitException : UAuthDomainException { /// - /// Represents a domain-level exception that is thrown when a user exceeds the allowed number of device or platform-specific session chains. - /// This typically occurs when UltimateAuth's session policy restricts the - /// number of concurrent logins for a given platform (e.g., web, mobile) - /// and the user attempts to create an additional session beyond the limit. + /// Gets the platform for which the device or session-chain limit was exceeded. /// - public sealed class UAuthDeviceLimitException : UAuthDomainException - { - /// - /// Gets the platform for which the device or session-chain limit was exceeded. - /// - public string Platform { get; } + public string Platform { get; } - /// - /// Initializes a new instance of the class with the specified platform name. - /// - /// The platform on which the limit was exceeded. - public UAuthDeviceLimitException(string platform) : base($"Device limit exceeded for platform '{platform}'.") - { - Platform = platform; - } + /// + /// Initializes a new instance of the class with the specified platform name. + /// + /// The platform on which the limit was exceeded. + public UAuthDeviceLimitException(string platform) : base($"Device limit exceeded for platform '{platform}'.") + { + Platform = platform; } } diff --git a/src/CodeBeam.UltimateAuth.Core/Errors/UAuthInvalidCredentialsException.cs b/src/CodeBeam.UltimateAuth.Core/Errors/UAuthInvalidCredentialsException.cs index 05941d5d..8e573a85 100644 --- a/src/CodeBeam.UltimateAuth.Core/Errors/UAuthInvalidCredentialsException.cs +++ b/src/CodeBeam.UltimateAuth.Core/Errors/UAuthInvalidCredentialsException.cs @@ -1,18 +1,17 @@ -namespace CodeBeam.UltimateAuth.Core.Errors +namespace CodeBeam.UltimateAuth.Core.Errors; + +/// +/// Represents an authentication failure caused by invalid user credentials. +/// This error is thrown when the supplied username, password, or login +/// identifier does not match any valid user account. +/// +public sealed class UAuthInvalidCredentialsException : UAuthDomainException { /// - /// Represents an authentication failure caused by invalid user credentials. - /// This error is thrown when the supplied username, password, or login - /// identifier does not match any valid user account. + /// Initializes a new instance of the class + /// with a default message indicating incorrect credentials. /// - public sealed class UAuthInvalidCredentialsException : UAuthDomainException + public UAuthInvalidCredentialsException() : base("Invalid username or password.") { - /// - /// Initializes a new instance of the class - /// with a default message indicating incorrect credentials. - /// - public UAuthInvalidCredentialsException() : base("Invalid username or password.") - { - } } } diff --git a/src/CodeBeam.UltimateAuth.Core/Errors/UAuthInvalidPkceCodeException.cs b/src/CodeBeam.UltimateAuth.Core/Errors/UAuthInvalidPkceCodeException.cs index 51905d1d..574d269b 100644 --- a/src/CodeBeam.UltimateAuth.Core/Errors/UAuthInvalidPkceCodeException.cs +++ b/src/CodeBeam.UltimateAuth.Core/Errors/UAuthInvalidPkceCodeException.cs @@ -1,20 +1,19 @@ -namespace CodeBeam.UltimateAuth.Core.Errors +namespace CodeBeam.UltimateAuth.Core.Errors; + +/// +/// Represents an authentication failure occurring during the PKCE authorization +/// flow when the supplied authorization code is invalid, expired, or does not +/// match the original code challenge. +/// This exception indicates a failed PKCE verification rather than a general +/// credential or configuration error. +/// +public sealed class UAuthInvalidPkceCodeException : UAuthDomainException { /// - /// Represents an authentication failure occurring during the PKCE authorization - /// flow when the supplied authorization code is invalid, expired, or does not - /// match the original code challenge. - /// This exception indicates a failed PKCE verification rather than a general - /// credential or configuration error. + /// Initializes a new instance of the class + /// with a default message indicating an invalid PKCE authorization code. /// - public sealed class UAuthInvalidPkceCodeException : UAuthDomainException + public UAuthInvalidPkceCodeException() : base("Invalid PKCE authorization code.") { - /// - /// Initializes a new instance of the class - /// with a default message indicating an invalid PKCE authorization code. - /// - public UAuthInvalidPkceCodeException() : base("Invalid PKCE authorization code.") - { - } } } diff --git a/src/CodeBeam.UltimateAuth.Core/Errors/UAuthRootRevokedException.cs b/src/CodeBeam.UltimateAuth.Core/Errors/UAuthRootRevokedException.cs index fc1ad258..b0426bd4 100644 --- a/src/CodeBeam.UltimateAuth.Core/Errors/UAuthRootRevokedException.cs +++ b/src/CodeBeam.UltimateAuth.Core/Errors/UAuthRootRevokedException.cs @@ -1,20 +1,19 @@ -namespace CodeBeam.UltimateAuth.Core.Errors +namespace CodeBeam.UltimateAuth.Core.Errors; + +/// +/// Represents a domain-level authentication failure indicating that the user's +/// entire session root has been revoked. +/// When a root is revoked, all session chains and all sessions belonging to the +/// user become immediately invalid, regardless of their individual expiration +/// or revocation state. +/// +public sealed class UAuthRootRevokedException : UAuthDomainException { /// - /// Represents a domain-level authentication failure indicating that the user's - /// entire session root has been revoked. - /// When a root is revoked, all session chains and all sessions belonging to the - /// user become immediately invalid, regardless of their individual expiration - /// or revocation state. + /// Initializes a new instance of the class + /// with a default message indicating that all sessions under the root are invalid. /// - public sealed class UAuthRootRevokedException : UAuthDomainException + public UAuthRootRevokedException() : base("User root has been revoked. All sessions are invalid.") { - /// - /// Initializes a new instance of the class - /// with a default message indicating that all sessions under the root are invalid. - /// - public UAuthRootRevokedException() : base("User root has been revoked. All sessions are invalid.") - { - } } } diff --git a/src/CodeBeam.UltimateAuth.Core/Errors/UAuthTokenTamperedException.cs b/src/CodeBeam.UltimateAuth.Core/Errors/UAuthTokenTamperedException.cs index 0164df5f..66214ab2 100644 --- a/src/CodeBeam.UltimateAuth.Core/Errors/UAuthTokenTamperedException.cs +++ b/src/CodeBeam.UltimateAuth.Core/Errors/UAuthTokenTamperedException.cs @@ -1,26 +1,25 @@ -namespace CodeBeam.UltimateAuth.Core.Errors +namespace CodeBeam.UltimateAuth.Core.Errors; + +/// +/// Represents an authentication-domain exception thrown when a token fails its +/// integrity verification checks, indicating that the token may have been altered, +/// corrupted, or tampered with after issuance. +/// +/// This exception is raised during token validation when signature verification fails, +/// claims are inconsistent, or protected fields do not match their expected values. +/// Such failures generally imply either client-side manipulation or +/// man-in-the-middle interference. +/// +/// Applications catching this exception should treat the associated token as unsafe +/// and deny access immediately. Reauthentication or complete session invalidation +/// may be required depending on the security policy. +/// +public sealed class UAuthTokenTamperedException : UAuthDomainException { /// - /// Represents an authentication-domain exception thrown when a token fails its - /// integrity verification checks, indicating that the token may have been altered, - /// corrupted, or tampered with after issuance. - /// - /// This exception is raised during token validation when signature verification fails, - /// claims are inconsistent, or protected fields do not match their expected values. - /// Such failures generally imply either client-side manipulation or - /// man-in-the-middle interference. - /// - /// Applications catching this exception should treat the associated token as unsafe - /// and deny access immediately. Reauthentication or complete session invalidation - /// may be required depending on the security policy. + /// Initializes a new instance of the class. /// - public sealed class UAuthTokenTamperedException : UAuthDomainException + public UAuthTokenTamperedException() : base("Token integrity check failed (possible tampering).") { - /// - /// Initializes a new instance of the class. - /// - public UAuthTokenTamperedException() : base("Token integrity check failed (possible tampering).") - { - } } } diff --git a/src/CodeBeam.UltimateAuth.Core/Events/IAuthEventContext.cs b/src/CodeBeam.UltimateAuth.Core/Events/IAuthEventContext.cs index 2c786396..93713baa 100644 --- a/src/CodeBeam.UltimateAuth.Core/Events/IAuthEventContext.cs +++ b/src/CodeBeam.UltimateAuth.Core/Events/IAuthEventContext.cs @@ -1,7 +1,6 @@ -namespace CodeBeam.UltimateAuth.Core.Events -{ - /// - /// Marker interface for all UltimateAuth event context types. - /// - public interface IAuthEventContext { } -} +namespace CodeBeam.UltimateAuth.Core.Events; + +/// +/// Marker interface for all UltimateAuth event context types. +/// +public interface IAuthEventContext { } diff --git a/src/CodeBeam.UltimateAuth.Core/Events/SessionCreatedContext.cs b/src/CodeBeam.UltimateAuth.Core/Events/SessionCreatedContext.cs index 544b2eba..341acbdc 100644 --- a/src/CodeBeam.UltimateAuth.Core/Events/SessionCreatedContext.cs +++ b/src/CodeBeam.UltimateAuth.Core/Events/SessionCreatedContext.cs @@ -1,48 +1,47 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Core.Events +namespace CodeBeam.UltimateAuth.Core.Events; + +/// +/// Represents contextual data emitted when a new authentication session is created. +/// +/// This event is published immediately after a successful login or initial session +/// creation within a session chain. It provides the essential identifiers required +/// for auditing, monitoring, analytics, and external integrations. +/// +/// Handlers should treat this event as notification-only; modifying session state +/// or performing security-critical actions is not recommended unless explicitly intended. +/// +public sealed class SessionCreatedContext : IAuthEventContext { /// - /// Represents contextual data emitted when a new authentication session is created. - /// - /// This event is published immediately after a successful login or initial session - /// creation within a session chain. It provides the essential identifiers required - /// for auditing, monitoring, analytics, and external integrations. - /// - /// Handlers should treat this event as notification-only; modifying session state - /// or performing security-critical actions is not recommended unless explicitly intended. + /// Gets the identifier of the user for whom the new session was created. /// - public sealed class SessionCreatedContext : IAuthEventContext - { - /// - /// Gets the identifier of the user for whom the new session was created. - /// - public TUserId UserId { get; } + public TUserId UserId { get; } - /// - /// Gets the unique identifier of the newly created session. - /// - public AuthSessionId SessionId { get; } + /// + /// Gets the unique identifier of the newly created session. + /// + public AuthSessionId SessionId { get; } - /// - /// Gets the identifier of the session chain to which this session belongs. - /// - public SessionChainId ChainId { get; } + /// + /// Gets the identifier of the session chain to which this session belongs. + /// + public SessionChainId ChainId { get; } - /// - /// Gets the timestamp on which the session was created. - /// - public DateTimeOffset CreatedAt { get; } + /// + /// Gets the timestamp on which the session was created. + /// + public DateTimeOffset CreatedAt { get; } - /// - /// Initializes a new instance of the class. - /// - public SessionCreatedContext(TUserId userId, AuthSessionId sessionId, SessionChainId chainId, DateTimeOffset createdAt) - { - UserId = userId; - SessionId = sessionId; - ChainId = chainId; - CreatedAt = createdAt; - } + /// + /// Initializes a new instance of the class. + /// + public SessionCreatedContext(TUserId userId, AuthSessionId sessionId, SessionChainId chainId, DateTimeOffset createdAt) + { + UserId = userId; + SessionId = sessionId; + ChainId = chainId; + CreatedAt = createdAt; } } diff --git a/src/CodeBeam.UltimateAuth.Core/Events/SessionRefreshedContext.cs b/src/CodeBeam.UltimateAuth.Core/Events/SessionRefreshedContext.cs index 34720489..d3d4b0f3 100644 --- a/src/CodeBeam.UltimateAuth.Core/Events/SessionRefreshedContext.cs +++ b/src/CodeBeam.UltimateAuth.Core/Events/SessionRefreshedContext.cs @@ -1,60 +1,59 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Core.Events +namespace CodeBeam.UltimateAuth.Core.Events; + +/// +/// Represents contextual data emitted when an authentication session is refreshed. +/// +/// This event occurs whenever a valid session performs a rotation — typically during +/// a refresh-token exchange or session renewal flow. The old session becomes inactive, +/// and a new session inherits updated expiration and security metadata. +/// +/// This event is primarily used for analytics, auditing, security monitoring, and +/// external workflow triggers (e.g., notifying users of new logins, updating dashboards, +/// or tracking device activity). +/// +public sealed class SessionRefreshedContext : IAuthEventContext { /// - /// Represents contextual data emitted when an authentication session is refreshed. - /// - /// This event occurs whenever a valid session performs a rotation — typically during - /// a refresh-token exchange or session renewal flow. The old session becomes inactive, - /// and a new session inherits updated expiration and security metadata. - /// - /// This event is primarily used for analytics, auditing, security monitoring, and - /// external workflow triggers (e.g., notifying users of new logins, updating dashboards, - /// or tracking device activity). + /// Gets the identifier of the user whose session was refreshed. /// - public sealed class SessionRefreshedContext : IAuthEventContext - { - /// - /// Gets the identifier of the user whose session was refreshed. - /// - public TUserId UserId { get; } + public TUserId UserId { get; } - /// - /// Gets the identifier of the session that was replaced during the refresh operation. - /// - public AuthSessionId OldSessionId { get; } + /// + /// Gets the identifier of the session that was replaced during the refresh operation. + /// + public AuthSessionId OldSessionId { get; } - /// - /// Gets the identifier of the newly created session that replaces the old session. - /// - public AuthSessionId NewSessionId { get; } + /// + /// Gets the identifier of the newly created session that replaces the old session. + /// + public AuthSessionId NewSessionId { get; } - /// - /// Gets the identifier of the session chain to which both sessions belong. - /// - public SessionChainId ChainId { get; } + /// + /// Gets the identifier of the session chain to which both sessions belong. + /// + public SessionChainId ChainId { get; } - /// - /// Gets the timestamp at which the refresh occurred. - /// - public DateTimeOffset RefreshedAt { get; } + /// + /// Gets the timestamp at which the refresh occurred. + /// + public DateTimeOffset RefreshedAt { get; } - /// - /// Initializes a new instance of the class. - /// - public SessionRefreshedContext( - TUserId userId, - AuthSessionId oldSessionId, - AuthSessionId newSessionId, - SessionChainId chainId, - DateTimeOffset refreshedAt) - { - UserId = userId; - OldSessionId = oldSessionId; - NewSessionId = newSessionId; - ChainId = chainId; - RefreshedAt = refreshedAt; - } + /// + /// Initializes a new instance of the class. + /// + public SessionRefreshedContext( + TUserId userId, + AuthSessionId oldSessionId, + AuthSessionId newSessionId, + SessionChainId chainId, + DateTimeOffset refreshedAt) + { + UserId = userId; + OldSessionId = oldSessionId; + NewSessionId = newSessionId; + ChainId = chainId; + RefreshedAt = refreshedAt; } } diff --git a/src/CodeBeam.UltimateAuth.Core/Events/SessionRevokedContext.cs b/src/CodeBeam.UltimateAuth.Core/Events/SessionRevokedContext.cs index fc5167ab..04f0040d 100644 --- a/src/CodeBeam.UltimateAuth.Core/Events/SessionRevokedContext.cs +++ b/src/CodeBeam.UltimateAuth.Core/Events/SessionRevokedContext.cs @@ -1,57 +1,55 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Core.Events +namespace CodeBeam.UltimateAuth.Core.Events; + +/// +/// Represents contextual data emitted when an individual session is revoked. +/// +/// This event is triggered when a specific session is invalidated — either due to +/// explicit logout, administrator action, security enforcement, or anomaly detection. +/// Only the targeted session is revoked; other sessions in the same chain or root +/// may continue to remain active unless broader revocation policies apply. +/// +/// Typical use cases include: +/// - Auditing and compliance logs +/// - User notifications (e.g., “Your session on device X was logged out”) +/// - Security automations (SIEM integration, monitoring suspicious activity) +/// - Application workflows that must respond to session termination +/// +public sealed class SessionRevokedContext : IAuthEventContext { /// - /// Represents contextual data emitted when an individual session is revoked. - /// - /// This event is triggered when a specific session is invalidated — either due to - /// explicit logout, administrator action, security enforcement, or anomaly detection. - /// Only the targeted session is revoked; other sessions in the same chain or root - /// may continue to remain active unless broader revocation policies apply. - /// - /// Typical use cases include: - /// - Auditing and compliance logs - /// - User notifications (e.g., “Your session on device X was logged out”) - /// - Security automations (SIEM integration, monitoring suspicious activity) - /// - Application workflows that must respond to session termination + /// Gets the identifier of the user to whom the revoked session belongs. /// - public sealed class SessionRevokedContext : IAuthEventContext - { - /// - /// Gets the identifier of the user to whom the revoked session belongs. - /// - public TUserId UserId { get; } + public TUserId UserId { get; } - /// - /// Gets the identifier of the session that has been revoked. - /// - public AuthSessionId SessionId { get; } + /// + /// Gets the identifier of the session that has been revoked. + /// + public AuthSessionId SessionId { get; } - /// - /// Gets the identifier of the session chain containing the revoked session. - /// - public SessionChainId ChainId { get; } + /// + /// Gets the identifier of the session chain containing the revoked session. + /// + public SessionChainId ChainId { get; } - /// - /// Gets the timestamp at which the session revocation occurred. - /// - public DateTimeOffset RevokedAt { get; } + /// + /// Gets the timestamp at which the session revocation occurred. + /// + public DateTimeOffset RevokedAt { get; } - /// - /// Initializes a new instance of the class. - /// - public SessionRevokedContext( - TUserId userId, - AuthSessionId sessionId, - SessionChainId chainId, - DateTimeOffset revokedAt) - { - UserId = userId; - SessionId = sessionId; - ChainId = chainId; - RevokedAt = revokedAt; - } + /// + /// Initializes a new instance of the class. + /// + public SessionRevokedContext( + TUserId userId, + AuthSessionId sessionId, + SessionChainId chainId, + DateTimeOffset revokedAt) + { + UserId = userId; + SessionId = sessionId; + ChainId = chainId; + RevokedAt = revokedAt; } - } diff --git a/src/CodeBeam.UltimateAuth.Core/Events/UAuthEventDispatcher.cs b/src/CodeBeam.UltimateAuth.Core/Events/UAuthEventDispatcher.cs index 55522192..c7d08746 100644 --- a/src/CodeBeam.UltimateAuth.Core/Events/UAuthEventDispatcher.cs +++ b/src/CodeBeam.UltimateAuth.Core/Events/UAuthEventDispatcher.cs @@ -1,53 +1,52 @@ -namespace CodeBeam.UltimateAuth.Core.Events +namespace CodeBeam.UltimateAuth.Core.Events; + +internal sealed class UAuthEventDispatcher { - internal sealed class UAuthEventDispatcher - { - private readonly UAuthEvents _events; + private readonly UAuthEvents _events; - public UAuthEventDispatcher(UAuthEvents events) - { - _events = events; - } + public UAuthEventDispatcher(UAuthEvents events) + { + _events = events; + } - public async Task DispatchAsync(IAuthEventContext context) - { - if (_events.OnAnyEvent is not null) - await SafeInvoke(() => _events.OnAnyEvent(context)); - - switch (context) - { - case SessionCreatedContext c: - if (_events.OnSessionCreated != null) - await SafeInvoke(() => _events.OnSessionCreated(c)); - break; - - case SessionRefreshedContext c: - if (_events.OnSessionRefreshed != null) - await SafeInvoke(() => _events.OnSessionRefreshed(c)); - break; - - case SessionRevokedContext c: - if (_events.OnSessionRevoked != null) - await SafeInvoke(() => _events.OnSessionRevoked(c)); - break; - - case UserLoggedInContext c: - if (_events.OnUserLoggedIn != null) - await SafeInvoke(() => _events.OnUserLoggedIn(c)); - break; - - case UserLoggedOutContext c: - if (_events.OnUserLoggedOut != null) - await SafeInvoke(() => _events.OnUserLoggedOut(c)); - break; - } - } + public async Task DispatchAsync(IAuthEventContext context) + { + if (_events.OnAnyEvent is not null) + await SafeInvoke(() => _events.OnAnyEvent(context)); - private static async Task SafeInvoke(Func func) + switch (context) { - try { await func(); } - catch { /* swallow → event hook must not break auth flow */ } + case SessionCreatedContext c: + if (_events.OnSessionCreated != null) + await SafeInvoke(() => _events.OnSessionCreated(c)); + break; + + case SessionRefreshedContext c: + if (_events.OnSessionRefreshed != null) + await SafeInvoke(() => _events.OnSessionRefreshed(c)); + break; + + case SessionRevokedContext c: + if (_events.OnSessionRevoked != null) + await SafeInvoke(() => _events.OnSessionRevoked(c)); + break; + + case UserLoggedInContext c: + if (_events.OnUserLoggedIn != null) + await SafeInvoke(() => _events.OnUserLoggedIn(c)); + break; + + case UserLoggedOutContext c: + if (_events.OnUserLoggedOut != null) + await SafeInvoke(() => _events.OnUserLoggedOut(c)); + break; } + } + private static async Task SafeInvoke(Func func) + { + try { await func(); } + catch { /* swallow → event hook must not break auth flow */ } } + } diff --git a/src/CodeBeam.UltimateAuth.Core/Events/UAuthEvents.cs b/src/CodeBeam.UltimateAuth.Core/Events/UAuthEvents.cs index 5d67cd5a..fadd610d 100644 --- a/src/CodeBeam.UltimateAuth.Core/Events/UAuthEvents.cs +++ b/src/CodeBeam.UltimateAuth.Core/Events/UAuthEvents.cs @@ -1,57 +1,56 @@ -namespace CodeBeam.UltimateAuth.Core.Events +namespace CodeBeam.UltimateAuth.Core.Events; + +/// +/// Provides an optional, application-wide event hook system for UltimateAuth. +/// +/// This class allows consumers to attach callbacks to authentication-related events +/// without implementing a full event bus or subscribing via DI. All handlers here are +/// optional and are executed after the corresponding operation completes successfully. +/// +/// IMPORTANT: +/// These delegates are designed for lightweight reactions such as: +/// - logging +/// - metrics +/// - notifications +/// - auditing +/// Custom business workflows **should not** be implemented here; instead, use dedicated +/// application services or a domain event bus for complex logic. +/// +/// All handlers receive an instance. The generic +/// type parameter is normalized as object to allow uniform handling regardless +/// of the actual TUserId type used by the application. +/// +public class UAuthEvents { /// - /// Provides an optional, application-wide event hook system for UltimateAuth. - /// - /// This class allows consumers to attach callbacks to authentication-related events - /// without implementing a full event bus or subscribing via DI. All handlers here are - /// optional and are executed after the corresponding operation completes successfully. - /// - /// IMPORTANT: - /// These delegates are designed for lightweight reactions such as: - /// - logging - /// - metrics - /// - notifications - /// - auditing - /// Custom business workflows **should not** be implemented here; instead, use dedicated - /// application services or a domain event bus for complex logic. - /// - /// All handlers receive an instance. The generic - /// type parameter is normalized as object to allow uniform handling regardless - /// of the actual TUserId type used by the application. + /// Fired on every auth-related event. + /// This global hook allows logging, tracing or metrics pipelines to observe all events. /// - public class UAuthEvents - { - /// - /// Fired on every auth-related event. - /// This global hook allows logging, tracing or metrics pipelines to observe all events. - /// - public Func? OnAnyEvent { get; set; } + public Func? OnAnyEvent { get; set; } - /// - /// Fired when a new session is created (login or device bootstrap). - /// - public Func, Task>? OnSessionCreated { get; set; } + /// + /// Fired when a new session is created (login or device bootstrap). + /// + public Func, Task>? OnSessionCreated { get; set; } - /// - /// Fired when an existing session is refreshed and rotated. - /// - public Func, Task>? OnSessionRefreshed { get; set; } + /// + /// Fired when an existing session is refreshed and rotated. + /// + public Func, Task>? OnSessionRefreshed { get; set; } - /// - /// Fired when a specific session is revoked. - /// - public Func, Task>? OnSessionRevoked { get; set; } + /// + /// Fired when a specific session is revoked. + /// + public Func, Task>? OnSessionRevoked { get; set; } - /// - /// Fired when a user successfully completes the login process. - /// Note: separate from SessionCreated; this is a higher-level event. - /// - public Func, Task>? OnUserLoggedIn { get; set; } + /// + /// Fired when a user successfully completes the login process. + /// Note: separate from SessionCreated; this is a higher-level event. + /// + public Func, Task>? OnUserLoggedIn { get; set; } - /// - /// Fired when a user logs out or all sessions for the user are revoked. - /// - public Func, Task>? OnUserLoggedOut { get; set; } - } + /// + /// Fired when a user logs out or all sessions for the user are revoked. + /// + public Func, Task>? OnUserLoggedOut { get; set; } } diff --git a/src/CodeBeam.UltimateAuth.Core/Events/UserLoggedInContext.cs b/src/CodeBeam.UltimateAuth.Core/Events/UserLoggedInContext.cs index b661db79..54ef844b 100644 --- a/src/CodeBeam.UltimateAuth.Core/Events/UserLoggedInContext.cs +++ b/src/CodeBeam.UltimateAuth.Core/Events/UserLoggedInContext.cs @@ -1,42 +1,41 @@ -namespace CodeBeam.UltimateAuth.Core.Events +namespace CodeBeam.UltimateAuth.Core.Events; + +/// +/// Represents contextual data emitted when a user successfully completes the login process. +/// +/// This event is triggered after the authentication workflow validates credentials +/// (or external identity provider assertions) and before or after the session creation step, +/// depending on pipeline configuration. +/// +/// Typical use cases include: +/// - auditing successful logins +/// - triggering login notifications +/// - updating user activity dashboards +/// - integrating with SIEM or monitoring systems +/// +/// NOTE: +/// This event is distinct from . +/// A user may log in without creating a new session (e.g., external SSO), +/// or multiple sessions may be created after a single login depending on client application flows. +/// +public sealed class UserLoggedInContext : IAuthEventContext { /// - /// Represents contextual data emitted when a user successfully completes the login process. - /// - /// This event is triggered after the authentication workflow validates credentials - /// (or external identity provider assertions) and before or after the session creation step, - /// depending on pipeline configuration. - /// - /// Typical use cases include: - /// - auditing successful logins - /// - triggering login notifications - /// - updating user activity dashboards - /// - integrating with SIEM or monitoring systems - /// - /// NOTE: - /// This event is distinct from . - /// A user may log in without creating a new session (e.g., external SSO), - /// or multiple sessions may be created after a single login depending on client application flows. + /// Gets the identifier of the user who has logged in. /// - public sealed class UserLoggedInContext : IAuthEventContext - { - /// - /// Gets the identifier of the user who has logged in. - /// - public TUserId UserId { get; } + public TUserId UserId { get; } - /// - /// Gets the timestamp at which the login event occurred. - /// - public DateTimeOffset LoggedInAt { get; } + /// + /// Gets the timestamp at which the login event occurred. + /// + public DateTimeOffset LoggedInAt { get; } - /// - /// Initializes a new instance of the class. - /// - public UserLoggedInContext(TUserId userId, DateTimeOffset at) - { - UserId = userId; - LoggedInAt = at; - } + /// + /// Initializes a new instance of the class. + /// + public UserLoggedInContext(TUserId userId, DateTimeOffset at) + { + UserId = userId; + LoggedInAt = at; } } diff --git a/src/CodeBeam.UltimateAuth.Core/Events/UserLoggedOutContext.cs b/src/CodeBeam.UltimateAuth.Core/Events/UserLoggedOutContext.cs index 6f6e707f..85278192 100644 --- a/src/CodeBeam.UltimateAuth.Core/Events/UserLoggedOutContext.cs +++ b/src/CodeBeam.UltimateAuth.Core/Events/UserLoggedOutContext.cs @@ -1,41 +1,40 @@ -namespace CodeBeam.UltimateAuth.Core.Events +namespace CodeBeam.UltimateAuth.Core.Events; + +/// +/// Represents contextual data emitted when a user logs out of the system. +/// +/// This event is triggered when a logout operation is executed — either by explicit +/// user action, automatic revocation, administrative force-logout, or tenant-level +/// security policies. +/// +/// Unlike , which targets a specific +/// session, this event reflects a higher-level “user has logged out” state and may +/// represent logout from a single session or all sessions depending on the workflow. +/// +/// Typical use cases include: +/// - audit logging of logout activities +/// - updating user presence or activity services +/// - triggering notifications (e.g., “You have logged out from device X”) +/// - integrating with analytics or SIEM systems +/// +public sealed class UserLoggedOutContext : IAuthEventContext { /// - /// Represents contextual data emitted when a user logs out of the system. - /// - /// This event is triggered when a logout operation is executed — either by explicit - /// user action, automatic revocation, administrative force-logout, or tenant-level - /// security policies. - /// - /// Unlike , which targets a specific - /// session, this event reflects a higher-level “user has logged out” state and may - /// represent logout from a single session or all sessions depending on the workflow. - /// - /// Typical use cases include: - /// - audit logging of logout activities - /// - updating user presence or activity services - /// - triggering notifications (e.g., “You have logged out from device X”) - /// - integrating with analytics or SIEM systems + /// Gets the identifier of the user who has logged out. /// - public sealed class UserLoggedOutContext : IAuthEventContext - { - /// - /// Gets the identifier of the user who has logged out. - /// - public TUserId UserId { get; } + public TUserId UserId { get; } - /// - /// Gets the timestamp at which the logout occurred. - /// - public DateTimeOffset LoggedOutAt { get; } + /// + /// Gets the timestamp at which the logout occurred. + /// + public DateTimeOffset LoggedOutAt { get; } - /// - /// Initializes a new instance of the class. - /// - public UserLoggedOutContext(TUserId userId, DateTimeOffset at) - { - UserId = userId; - LoggedOutAt = at; - } + /// + /// Initializes a new instance of the class. + /// + public UserLoggedOutContext(TUserId userId, DateTimeOffset at) + { + UserId = userId; + LoggedOutAt = at; } } diff --git a/src/CodeBeam.UltimateAuth.Core/Infrastructure/UAuthUserIdConverter.cs b/src/CodeBeam.UltimateAuth.Core/Infrastructure/UAuthUserIdConverter.cs index dd2507f4..083a6555 100644 --- a/src/CodeBeam.UltimateAuth.Core/Infrastructure/UAuthUserIdConverter.cs +++ b/src/CodeBeam.UltimateAuth.Core/Infrastructure/UAuthUserIdConverter.cs @@ -76,7 +76,7 @@ public TUserId FromString(string value) }; } - public bool TryFromString(string value, out TUserId? id) + public bool TryFromString(string value, out TUserId id) { try { @@ -85,7 +85,7 @@ public bool TryFromString(string value, out TUserId? id) } catch { - id = default; + id = default!; return false; } } @@ -97,7 +97,7 @@ public bool TryFromString(string value, out TUserId? id) /// The reconstructed user id. public TUserId FromBytes(byte[] binary) => FromString(Encoding.UTF8.GetString(binary)); - public bool TryFromBytes(byte[] binary, out TUserId? id) + public bool TryFromBytes(byte[] binary, out TUserId id) { try { @@ -106,7 +106,7 @@ public bool TryFromBytes(byte[] binary, out TUserId? id) } catch { - id = default; + id = default!; return false; } } diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/ClientProfileReader.cs b/src/CodeBeam.UltimateAuth.Server/Auth/ClientProfileReader.cs index d48d42d0..44888e2a 100644 --- a/src/CodeBeam.UltimateAuth.Server/Auth/ClientProfileReader.cs +++ b/src/CodeBeam.UltimateAuth.Server/Auth/ClientProfileReader.cs @@ -24,7 +24,7 @@ public UAuthClientProfile Read(HttpContext context) return UAuthClientProfile.NotSpecified; } - private static bool TryParse(string value, out UAuthClientProfile profile) + private static bool TryParse(string? value, out UAuthClientProfile profile) { return Enum.TryParse(value, ignoreCase: true, out profile); } diff --git a/src/CodeBeam.UltimateAuth.Server/Authentication/UAuthAuthenticationHandler.cs b/src/CodeBeam.UltimateAuth.Server/Authentication/UAuthAuthenticationHandler.cs index f9b5db30..e2f56a2c 100644 --- a/src/CodeBeam.UltimateAuth.Server/Authentication/UAuthAuthenticationHandler.cs +++ b/src/CodeBeam.UltimateAuth.Server/Authentication/UAuthAuthenticationHandler.cs @@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using System.Text.Encodings.Web; namespace CodeBeam.UltimateAuth.Server.Authentication; @@ -22,12 +23,11 @@ public UAuthAuthenticationHandler( ITransportCredentialResolver transportCredentialResolver, IOptionsMonitor options, ILoggerFactory logger, - System.Text.Encodings.Web.UrlEncoder encoder, - ISystemClock clock, + UrlEncoder encoder, ISessionValidator sessionValidator, IDeviceContextFactory deviceContextFactory, IClock uauthClock) - : base(options, logger, encoder, clock) + : base(options, logger, encoder) { _transportCredentialResolver = transportCredentialResolver; _sessionValidator = sessionValidator; diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/ValidateEndpointHandler.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/ValidateEndpointHandler.cs index 971827ba..d76a8064 100644 --- a/src/CodeBeam.UltimateAuth.Server/Endpoints/ValidateEndpointHandler.cs +++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/ValidateEndpointHandler.cs @@ -70,13 +70,25 @@ public async Task ValidateAsync(HttpContext context, CancellationToken }, ct); + if (result.UserKey is not UserKey userKey) + { + return Results.Json( + new AuthValidationResult + { + IsValid = false, + State = "invalid" + }, + statusCode: StatusCodes.Status401Unauthorized + ); + } + return Results.Ok(new AuthValidationResult { IsValid = result.IsValid, State = result.IsValid ? "active" : result.State.ToString().ToLowerInvariant(), Snapshot = new AuthStateSnapshot { - UserKey = (UserKey)result.UserKey, + UserKey = userKey, Tenant = result.Tenant, Claims = result.Claims, AuthenticatedAt = _clock.UtcNow, diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Hub/HubCredentialResolver.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Hub/HubCredentialResolver.cs index 0dd73836..9dbf2d22 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Hub/HubCredentialResolver.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Hub/HubCredentialResolver.cs @@ -23,10 +23,10 @@ public HubCredentialResolver(IAuthStore store) if (flow.IsCompleted) return null; - if (!flow.Payload.TryGet("authorization_code", out string? authorizationCode)) + if (!flow.Payload.TryGet("authorization_code", out string? authorizationCode) || string.IsNullOrWhiteSpace(authorizationCode)) return null; - if (!flow.Payload.TryGet("code_verifier", out string? codeVerifier)) + if (!flow.Payload.TryGet("code_verifier", out string? codeVerifier) || string.IsNullOrWhiteSpace(codeVerifier)) return null; return new HubCredentials diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Issuers/UAuthTokenIssuer.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Issuers/UAuthTokenIssuer.cs index 07f6e3e4..a30b4062 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Issuers/UAuthTokenIssuer.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Issuers/UAuthTokenIssuer.cs @@ -63,13 +63,15 @@ UAuthMode.SemiHybrid or var raw = _opaqueGenerator.Generate(); var hash = _tokenHasher.Hash(raw); + if (context.SessionId is not AuthSessionId sessionId) + return null; + var stored = new StoredRefreshToken { Tenant = flow.Tenant, TokenHash = hash, UserKey = context.UserKey, - // TODO: Check here again - SessionId = (AuthSessionId)context.SessionId, + SessionId = sessionId, ChainId = context.ChainId, IssuedAt = _clock.UtcNow, ExpiresAt = expires diff --git a/src/CodeBeam.UltimateAuth.Server/Services/RefreshTokenRotationService.cs b/src/CodeBeam.UltimateAuth.Server/Services/RefreshTokenRotationService.cs index ad628e19..73222294 100644 --- a/src/CodeBeam.UltimateAuth.Server/Services/RefreshTokenRotationService.cs +++ b/src/CodeBeam.UltimateAuth.Server/Services/RefreshTokenRotationService.cs @@ -58,6 +58,16 @@ public async Task RotateAsync(AuthFlowContext flo throw new InvalidOperationException("Validated refresh token does not contain a UserKey."); } + if (validation.SessionId is not AuthSessionId sessionId) + { + throw new InvalidOperationException("Validated refresh token does not contain a SessionId."); + } + + if (validation.TokenHash == null) + { + throw new InvalidOperationException("Validated refresh token does not contain a hashed token."); + } + var tokenContext = new TokenIssuanceContext { Tenant = flow.OriginalOptions.MultiTenant.Enabled @@ -87,7 +97,7 @@ public async Task RotateAsync(AuthFlowContext flo Tenant = flow.Tenant, TokenHash = refreshToken.TokenHash, UserKey = uKey, - SessionId = validation.SessionId.Value, + SessionId = sessionId, ChainId = validation.ChainId, IssuedAt = _clock.UtcNow, ExpiresAt = refreshToken.ExpiresAt diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Endpoints/AuthorizationEndpointHandler.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Endpoints/AuthorizationEndpointHandler.cs index 71d5dd18..a86fb67b 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Endpoints/AuthorizationEndpointHandler.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Endpoints/AuthorizationEndpointHandler.cs @@ -31,6 +31,12 @@ public async Task CheckAsync(HttpContext ctx) var req = await ctx.ReadJsonAsync(ctx.RequestAborted); + if (string.IsNullOrWhiteSpace(req.Resource)) + return Results.BadRequest("Resource is required for authorization check."); + + if (string.IsNullOrWhiteSpace(req.Action)) + return Results.BadRequest("Action is required for authorization check."); + var accessContext = await _accessContextFactory.CreateAsync( flow, action: req.Action, diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/EntityProjections/SessionProjection.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/EntityProjections/SessionProjection.cs index 285559c6..fdf642ed 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/EntityProjections/SessionProjection.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/EntityProjections/SessionProjection.cs @@ -22,7 +22,7 @@ internal sealed class SessionProjection public long SecurityVersionAtCreation { get; set; } - public DeviceContext Device { get; set; } + public DeviceContext Device { get; set; } = DeviceContext.Anonymous(); public ClaimsSnapshot Claims { get; set; } = ClaimsSnapshot.Empty; public SessionMetadata Metadata { get; set; } = SessionMetadata.Empty; diff --git a/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/EfCoreTokenStore.cs b/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/EfCoreTokenStore.cs index 3c31d42f..f4f95708 100644 --- a/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/EfCoreTokenStore.cs +++ b/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/EfCoreTokenStore.cs @@ -19,6 +19,9 @@ public async Task StoreAsync(TenantKey tenantId, StoredRefreshToken token, Cance if (token.Tenant != tenantId) throw new InvalidOperationException("TenantId mismatch between context and token."); + if (token.ChainId is null) + throw new InvalidOperationException("Refresh token must have a ChainId before being stored."); + _db.RefreshTokens.Add(new RefreshTokenProjection { Tenant = tenantId,