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/3] 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 db0e642..c867a03 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 0d00f49..7fc79cd 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 cca6ea0..778447c 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 a4db8cf..a10eb12 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 c8a6c81..a4195fd 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 59a5292..24e4dc1 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 ed5f958..6402d1d 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/3] 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 bf61d88..a5b3f69 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 806d6c9..c043d44 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 487072f..49a1efa 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 9a29458..4e5efac 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 dc0cc0a..2fe227d 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 2b2021a..5d2bc41 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 36bd1b3..3d5a581 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 78ecb59..f3e075b 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 82764fb..0096d89 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 a624091..71e7a18 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 ebf4499..8112e45 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 d6596c9..039a821 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 0fe7422..a03e125 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 0c49dcf..a5332d1 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 39ec7c5..b5d7b3b 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 288e05b..1a222d8 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 fc642d3..bd8dd80 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 b5d2715..c72f650 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 9dc9df9..0000000 --- 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 9486398..0000000 --- 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 2f759e9..9cded19 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 d546e55..0000000 --- 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 25fac76..0000000 --- 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 edf2d58..94d2933 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 778447c..1a8a69a 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 578f242..1ddced8 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 b529fa6..5bf3a8e 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 2a90b92..08a9f23 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 b97c47e..0000000 --- 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 23f8551..0000000 --- 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 f342e7c..404422a 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 238390b..1d79bbe 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 2320615..fa89076 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 e157c94..a1e2002 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 d31c6e2..6f9a675 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 8f41f0d..9f88653 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 80d7102..5f32962 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 a4cd82a..46d8241 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 e28fa7b..5063bda 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 404b62b..e3b92ad 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 2437c85..31f83ad 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 6126e7d..10c1c4c 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 ec5fb02..7d39fc7 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 662fbef..d8d953d 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 3ff02bd..0c31e6c 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 8324739..3154815 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 94a3902..95a03a1 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 b1d2565..e173630 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 d14eb10..a047ff1 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 4263a08..2395ccb 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 2aa6b6a..08fd530 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 7229f0a..7aebff4 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 86af91a..38f945b 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 5d575d0..abf719f 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 9bb085c..f12cced 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 12a1036..d04b6ec 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 21b180e..a612054 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 7c1f26e..51b8139 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 e4352d0..3c22c33 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 dc5891c..a9d308d 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 6b9375d..7cd62cc 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 704398a..c56c36b 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 1cf3e36..0baf518 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 08890b7..2d39adf 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 0d1622d..aecbe0f 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 ece8d80..513e54e 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 93d5aba..d56ac75 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 9343883..9c88acd 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 d06a854..9d5c578 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 8517fcc..a08d673 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 0d23664..5e96052 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 a16d81c..5e52414 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 76b089a..d3646e6 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 f7f4226..820f19f 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 bcae901..afa90f4 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 d760b28..59dcb02 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 3284350..459c8d0 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 344fedd..be61e29 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 ed13a6a..0000000 --- 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 cb43d69..be3a120 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 821c3d1..0ef2e9f 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 54306e6..d741b85 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 8e4cefc..0000000 --- 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 e565b32..69aba27 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 e942350..3fb7877 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 b36c1df..3e17fc1 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 96ce78b..5e80df5 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 f070cd1..080fb35 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 d7428ae..d31377e 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 9507442..ffc4e58 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 dc94f72..1c26c00 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 1548552..011a8d0 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 e921add..d296427 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 ef8cdcc..2f61fa6 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 64e2c34..a105c33 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 2063d0c..5a20021 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 fc1dd7e..0000000 --- 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 8d077b7..905d54d 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 e345dfb..fcc44dd 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 0c05934..458ca78 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 344f1a6..b45b995 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 24f1632..7d3ba4c 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 6ceca57..399c1cd 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 0bae143..38e076b 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 e791ae6..e5ddc54 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 3bf0cd4..315337c 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 c0b0511..0000000 --- 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 8663562..c07fadc 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 c6f5f7b..6d497ba 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 ac2f975..9de8c16 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 f1592b6..792f820 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 3961fbd..005d8d9 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 0000000..ab60e16 --- /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 97eec36..9099cbf 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 3e42de9..e45d522 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 fa7bc2f..d54703e 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 a8a7f03..e967442 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 cf96f95..0000000 --- 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 6c40f10..8c87ae7 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 79885c2..0000000 --- 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 1b82692..e4084ec 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 1d53f38..5de4ac5 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 5bcd732..b9498b8 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 cb9e14c..6ef97ad 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 7d8fe9a..0b97179 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 459e4ca..d4ac9b7 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 48fb6c8..14b2de0 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 afe906b..b96d09a 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 3c61b2f..57a2502 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 ebb56c6..73514cc 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 b2faa23..0000000 --- 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 d0bf6ad..44fca7a 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 a622edf..12c7f20 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 0000000..3f769f9 --- /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 44465ac..dd2507f 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 0d7a489..8c4c716 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 8872df7..f024b89 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 21f731e..db6570c 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 af82c69..ffc9040 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 28b8506..dd10d64 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 e969f0d..5ae8263 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 f411b8d..02ab394 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 cc7867c..5289e08 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 38c3f77..c04cafc 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 6ae1c48..d5271f0 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 c33d8b7..d6f4113 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 9874068..8229e2b 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 42226d1..6523621 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 33af8f2..0000000 --- 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 f8c75a2..c5bf2c3 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 46677d7..0912e65 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 941a93a..e9d3533 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 9c0fdec..cfbf30b 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 74828a1..db745d4 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 8992fb1..a65a1d5 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 405681e..aa7dff3 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 b66d85c..ab13f89 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 744d81a..4ee9c2a 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 2d32348..35fd263 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 1d81b1d..c757772 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 5ef2e04..9afd48d 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 c9de6e0..7d374cb 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 495e3cc..d91d578 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 e7345c0..d5238bc 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 3b28ca6..629c25a 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 d6da156..90de44f 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 780d68f..8370bce 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 494af80..a885acc 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 028f660..5624375 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 8eedf8a..0000000 --- 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 7e444b2..0000000 --- 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/3] 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 cda6ed4..9466d62 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 691dbd4..826703c 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 ce777e1..5d5ded6 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 d479e3a..c947862 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 a5d74ae..0516b94 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]