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.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/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..996b5a6 100644 --- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Services/IUAuthSessionManager.cs +++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Services/IUAuthSessionManager.cs @@ -1,27 +1,31 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Core.Abstractions +namespace CodeBeam.UltimateAuth.Core.Abstractions; + +/// +/// Application-level session command API. +/// Represents explicit intent to mutate session state. +/// All operations are authorization- and policy-aware. +/// +public interface IUAuthSessionManager { /// - /// Provides high-level session lifecycle operations such as creation, refresh, validation, and revocation. + /// Revokes a single session (logout current device). /// - public interface IUAuthSessionManager - { - Task> GetChainsAsync(string? tenantId, UserKey userKey); - - Task> GetSessionsAsync(string? tenantId, SessionChainId chainId); - - Task GetCurrentSessionAsync(string? tenantId, AuthSessionId sessionId); + Task RevokeSessionAsync(AuthSessionId sessionId, CancellationToken ct = default); - Task RevokeSessionAsync(string? tenantId, AuthSessionId sessionId, DateTimeOffset at); - - Task RevokeChainAsync(string? tenantId, SessionChainId chainId, DateTimeOffset at); + /// + /// Revokes all sessions in a specific chain (logout a device). + /// + Task RevokeChainAsync(SessionChainId chainId, CancellationToken ct = default); - Task ResolveChainIdAsync(string? tenantId, AuthSessionId sessionId); + /// + /// Revokes all session chains for the current user (logout all devices). + /// + Task RevokeAllChainsAsync(UserKey userKey, SessionChainId? exceptChainId = null, CancellationToken ct = default); - Task RevokeAllChainsAsync(string? tenantId, UserKey userKey, SessionChainId? exceptChainId, DateTimeOffset at); - - // Hard revoke - admin - Task RevokeRootAsync(string? tenantId, UserKey userKey, DateTimeOffset at); - } + /// + /// Hard revoke: revokes the entire session root (admin / security action). + /// + Task RevokeRootAsync(UserKey userKey, CancellationToken ct = default); } diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/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 deleted file mode 100644 index cca6ea0..0000000 --- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ISessionStore.cs +++ /dev/null @@ -1,46 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Contracts; -using CodeBeam.UltimateAuth.Core.Domain; - -namespace CodeBeam.UltimateAuth.Core.Abstractions -{ - /// - /// High-level session store abstraction used by UltimateAuth. - /// Encapsulates session, chain, and root orchestration. - /// - public interface ISessionStore - { - /// - /// Retrieves an active session by id. - /// - Task GetSessionAsync(string? tenantId, AuthSessionId sessionId, CancellationToken ct = default); - - /// - /// Creates a new session and associates it with the appropriate chain and root. - /// - Task CreateSessionAsync(IssuedSession issuedSession, SessionStoreContext context, CancellationToken ct = default); - - /// - /// Refreshes (rotates) the active session within its chain. - /// - Task RotateSessionAsync(AuthSessionId currentSessionId, IssuedSession newSession, SessionStoreContext context, CancellationToken ct = default); - - Task TouchSessionAsync(AuthSessionId sessionId, DateTimeOffset at, SessionTouchMode mode = SessionTouchMode.IfNeeded, CancellationToken ct = default); - - /// - /// Revokes a single session. - /// - Task RevokeSessionAsync(string? tenantId, AuthSessionId sessionId, DateTimeOffset at, CancellationToken ct = default); - - /// - /// Revokes all sessions for a specific user (all devices). - /// - Task RevokeAllChainsAsync(string? tenantId, UserKey userKey, SessionChainId? exceptChainId, DateTimeOffset at, CancellationToken ct = default); - - /// - /// Revokes all sessions within a specific chain (single device). - /// - Task RevokeChainAsync(string? tenantId, SessionChainId chainId, DateTimeOffset at, CancellationToken ct = default); - - Task RevokeRootAsync(string? tenantId, UserKey userKey, DateTimeOffset at, CancellationToken ct = default); - } -} diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ISessionStoreKernel.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ISessionStoreKernel.cs index 578f242..05148ed 100644 --- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ISessionStoreKernel.cs +++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ISessionStoreKernel.cs @@ -1,36 +1,28 @@ using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Core.Abstractions -{ - public interface ISessionStoreKernel - { - Task ExecuteAsync(Func action, CancellationToken ct = default); - //string? TenantId { get; } +namespace CodeBeam.UltimateAuth.Core.Abstractions; - // Session - Task GetSessionAsync(AuthSessionId sessionId); - Task SaveSessionAsync(ISession session); - Task RevokeSessionAsync(AuthSessionId sessionId, DateTimeOffset at); +public interface ISessionStoreKernel +{ + Task ExecuteAsync(Func action, CancellationToken ct = default); - // 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); + Task GetSessionAsync(AuthSessionId sessionId); + Task SaveSessionAsync(UAuthSession session); + Task RevokeSessionAsync(AuthSessionId sessionId, DateTimeOffset at); - // Root - Task GetSessionRootByUserAsync(UserKey userKey); - Task GetSessionRootByIdAsync(SessionRootId rootId); - Task SaveSessionRootAsync(ISessionRoot root); - Task RevokeSessionRootAsync(UserKey userKey, DateTimeOffset at); + Task GetChainAsync(SessionChainId chainId); + Task SaveChainAsync(UAuthSessionChain chain); + Task RevokeChainAsync(SessionChainId chainId, DateTimeOffset at); + Task GetActiveSessionIdAsync(SessionChainId chainId); + Task SetActiveSessionIdAsync(SessionChainId chainId, AuthSessionId sessionId); - // Helpers - Task GetChainIdBySessionAsync(AuthSessionId sessionId); - Task> GetChainsByUserAsync(UserKey userKey); - Task> GetSessionsByChainAsync(SessionChainId chainId); + Task GetSessionRootByUserAsync(UserKey userKey); + Task GetSessionRootByIdAsync(SessionRootId rootId); + Task SaveSessionRootAsync(UAuthSessionRoot root); + Task RevokeSessionRootAsync(UserKey userKey, DateTimeOffset at); - // Maintenance - Task DeleteExpiredSessionsAsync(DateTimeOffset at); - } + Task GetChainIdBySessionAsync(AuthSessionId sessionId); + Task> GetChainsByUserAsync(UserKey userKey); + Task> GetSessionsByChainAsync(SessionChainId chainId); + 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 deleted file mode 100644 index 2a90b92..0000000 --- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ITenantAwareSessionStore.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace CodeBeam.UltimateAuth.Core.Abstractions -{ - public interface ITenantAwareSessionStore - { - 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..6a76788 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 UAuthSession 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..42c5197 100644 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/ResolvedRefreshSession.cs +++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/ResolvedRefreshSession.cs @@ -1,38 +1,35 @@ 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 UAuthSession? Session { get; init; } + public UAuthSessionChain? 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(UAuthSession session, UAuthSessionChain 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 deleted file mode 100644 index 8517fcc..0000000 --- a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionResult.cs +++ /dev/null @@ -1,40 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Domain; - -namespace CodeBeam.UltimateAuth.Core.Contracts -{ - // TODO: IsNewChain, IsNewRoot flags? - /// - /// Represents the result of a session operation within UltimateAuth, such as - /// login or session refresh. - /// - /// A session operation may produce: - /// - a newly created session, - /// - an updated session chain (rotation), - /// - an updated session root (e.g., after adding a new chain). - /// - /// This wrapper provides a unified model so downstream components — such as - /// token services, event emitters, logging pipelines, or application-level - /// consumers — can easily access all updated authentication structures. - /// - public sealed class SessionResult - { - /// - /// Gets the active session produced by the operation. - /// This is the newest session and the one that should be used when issuing tokens. - /// - public required ISession Session { get; init; } - - /// - /// Gets the session chain associated with the session. - /// The chain may be newly created (login) or updated (session rotation). - /// - public required ISessionChain Chain { get; init; } - - /// - /// Gets the user's session root. - /// This structure may be updated when new chains are added or when security - /// properties change. - /// - public required ISessionRoot Root { get; init; } - } -} diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/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..37a20cb 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 UAuthSession Session { get; init; } = default!; + public DateTimeOffset At { get; init; } } diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenRefreshContext.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenRefreshContext.cs index 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 deleted file mode 100644 index ac2f975..0000000 --- a/src/CodeBeam.UltimateAuth.Core/Domain/Session/ISession.cs +++ /dev/null @@ -1,86 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Contracts; - -namespace CodeBeam.UltimateAuth.Core.Domain -{ - /// - /// Represents a single authentication session belonging to a user. - /// Sessions are immutable, security-critical units used for validation, - /// sliding expiration, revocation, and device analytics. - /// - public interface ISession - { - /// - /// Gets the unique identifier of the session. - /// - AuthSessionId SessionId { get; } - - string? TenantId { get; } - - /// - /// Gets the identifier of the user who owns this session. - /// - UserKey UserKey { get; } - - SessionChainId ChainId { get; } - - /// - /// Gets the timestamp when this session was originally created. - /// - DateTimeOffset CreatedAt { get; } - - /// - /// Gets the timestamp when the session becomes invalid due to expiration. - /// - DateTimeOffset ExpiresAt { get; } - - /// - /// Gets the timestamp of the last successful usage. - /// Used when evaluating sliding expiration policies. - /// - DateTimeOffset? LastSeenAt { get; } - - /// - /// Gets a value indicating whether this session has been explicitly revoked. - /// - bool IsRevoked { get; } - - /// - /// Gets the timestamp when the session was revoked, if applicable. - /// - DateTimeOffset? RevokedAt { get; } - - /// - /// Gets the user's security version at the moment of session creation. - /// If the stored version does not match the user's current version, - /// the session becomes invalid (e.g., after password or MFA reset). - /// - long SecurityVersionAtCreation { get; } - - /// - /// Gets metadata describing the client device that created the session. - /// Includes platform, OS, IP address, fingerprint, and more. - /// - DeviceContext Device { get; } - - ClaimsSnapshot Claims { get; } - - /// - /// Gets session-scoped metadata used for application-specific extensions, - /// such as tenant data, app version, locale, or CSRF tokens. - /// - SessionMetadata Metadata { get; } - - /// - /// Computes the effective runtime state of the session (Active, Expired, - /// Revoked, SecurityVersionMismatch, etc.) based on the provided timestamp. - /// - /// The evaluated of this session. - SessionState GetState(DateTimeOffset at, TimeSpan? idleTimeout); - - ISession Touch(DateTimeOffset now); - ISession Revoke(DateTimeOffset at); - - ISession WithChain(SessionChainId chainId); - - } -} diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Session/ISessionChain.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Session/ISessionChain.cs deleted file mode 100644 index 7659f47..0000000 --- a/src/CodeBeam.UltimateAuth.Core/Domain/Session/ISessionChain.cs +++ /dev/null @@ -1,66 +0,0 @@ -namespace CodeBeam.UltimateAuth.Core.Domain -{ - /// - /// Represents a device- or login-scoped session chain. - /// A chain groups all rotated sessions belonging to a single logical login - /// (e.g., a browser instance, mobile app installation, or device fingerprint). - /// - public interface ISessionChain - { - /// - /// Gets the unique identifier of the session chain. - /// - SessionChainId ChainId { get; } - - SessionRootId RootId { get; } - - string? TenantId { get; } - - /// - /// Gets the identifier of the user who owns this chain. - /// Each chain represents one device/login family for this user. - /// - UserKey UserKey { get; } - - /// - /// Gets the number of refresh token rotations performed within this chain. - /// - int RotationCount { get; } - - /// - /// Gets the user's security version at the time the chain was created. - /// If the user's current security version is higher, the entire chain - /// becomes invalid (e.g., after password or MFA reset). - /// - long SecurityVersionAtCreation { get; } - - /// - /// Gets an optional snapshot of claims taken at chain creation time. - /// Useful for offline clients, WASM apps, and environments where - /// full user lookup cannot be performed on each request. - /// - ClaimsSnapshot ClaimsSnapshot { get; } - - /// - /// Gets the identifier of the currently active authentication session, if one exists. - /// - AuthSessionId? ActiveSessionId { get; } - - /// - /// Gets a value indicating whether this chain has been revoked. - /// Revoking a chain performs a device-level logout, invalidating - /// all sessions it contains. - /// - bool IsRevoked { get; } - - /// - /// Gets the timestamp when the chain was revoked, if applicable. - /// - DateTimeOffset? RevokedAt { get; } - - ISessionChain AttachSession(AuthSessionId sessionId); - ISessionChain RotateSession(AuthSessionId sessionId); - ISessionChain Revoke(DateTimeOffset at); - } - -} diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Session/ISessionRoot.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Session/ISessionRoot.cs deleted file mode 100644 index b839292..0000000 --- a/src/CodeBeam.UltimateAuth.Core/Domain/Session/ISessionRoot.cs +++ /dev/null @@ -1,60 +0,0 @@ -namespace CodeBeam.UltimateAuth.Core.Domain -{ - /// - /// Represents the root container for all authentication session chains of a user. - /// A session root is tenant-scoped and acts as the authoritative security boundary, - /// controlling global revocation, security versioning, and device/login families. - /// - public interface ISessionRoot - { - SessionRootId RootId { get; } - - /// - /// Gets the tenant identifier associated with this session root. - /// Used to isolate authentication domains in multi-tenant systems. - /// - string? TenantId { get; } - - /// - /// Gets the identifier of the user who owns this session root. - /// Each user has one root per tenant. - /// - UserKey UserKey { get; } - - /// - /// Gets a value indicating whether the entire session root is revoked. - /// When true, all chains and sessions belonging to this root are invalid, - /// regardless of their individual states. - /// - bool IsRevoked { get; } - - /// - /// Gets the timestamp when the session root was revoked, if applicable. - /// - DateTimeOffset? RevokedAt { get; } - - /// - /// Gets the current security version of the user within this tenant. - /// Incrementing this value invalidates all sessions, even if they are still active. - /// Common triggers include password reset, MFA reset, and account recovery. - /// - long SecurityVersion { get; } - - /// - /// Gets the complete set of session chains associated with this root. - /// Each chain represents a device or login-family (browser instance, mobile app, etc.). - /// The root is immutable; modifications must go through SessionService or SessionStore. - /// - IReadOnlyList Chains { get; } - - /// - /// Gets the timestamp when this root structure was last updated. - /// Useful for caching, concurrency handling, and incremental synchronization. - /// - DateTimeOffset LastUpdatedAt { get; } - - ISessionRoot AttachChain(ISessionChain chain, DateTimeOffset at); - - ISessionRoot Revoke(DateTimeOffset at); - } -} diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Session/RefreshOutcome.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Session/RefreshOutcome.cs index dec5c2f..f0aa8db 100644 --- a/src/CodeBeam.UltimateAuth.Core/Domain/Session/RefreshOutcome.cs +++ b/src/CodeBeam.UltimateAuth.Core/Domain/Session/RefreshOutcome.cs @@ -1,11 +1,10 @@ -namespace CodeBeam.UltimateAuth.Core.Domain +namespace CodeBeam.UltimateAuth.Core.Domain; + +public enum RefreshOutcome { - public enum RefreshOutcome - { - None, - NoOp, - Touched, - Rotated, - ReauthRequired - } + None, + NoOp, + Touched, + Rotated, + ReauthRequired } diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Session/SessionChainId.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Session/SessionChainId.cs index 5c253e6..d2edc1d 100644 --- a/src/CodeBeam.UltimateAuth.Core/Domain/Session/SessionChainId.cs +++ b/src/CodeBeam.UltimateAuth.Core/Domain/Session/SessionChainId.cs @@ -1,33 +1,32 @@ -namespace CodeBeam.UltimateAuth.Core.Domain +namespace CodeBeam.UltimateAuth.Core.Domain; + +public readonly record struct SessionChainId(Guid Value) { - public readonly record struct SessionChainId(Guid Value) - { - public static SessionChainId New() => new(Guid.NewGuid()); + public static SessionChainId New() => new(Guid.NewGuid()); - /// - /// Indicates that the chain must be assigned by the store. - /// - public static readonly SessionChainId Unassigned = new(Guid.Empty); + /// + /// Indicates that the chain must be assigned by the store. + /// + public static readonly SessionChainId Unassigned = new(Guid.Empty); - public bool IsUnassigned => Value == Guid.Empty; + public bool IsUnassigned => Value == Guid.Empty; - public static SessionChainId From(Guid value) - => value == Guid.Empty - ? throw new ArgumentException("ChainId cannot be empty.", nameof(value)) - : new SessionChainId(value); + public static SessionChainId From(Guid value) + => value == Guid.Empty + ? throw new ArgumentException("ChainId cannot be empty.", nameof(value)) + : new SessionChainId(value); - public static bool TryCreate(string raw, out SessionChainId id) + public static bool TryCreate(string raw, out SessionChainId id) + { + if (Guid.TryParse(raw, out var guid) && guid != Guid.Empty) { - if (Guid.TryParse(raw, out var guid) && guid != Guid.Empty) - { - id = new SessionChainId(guid); - return true; - } - - id = default; - return false; + id = new SessionChainId(guid); + return true; } - public override string ToString() => Value.ToString("N"); + id = default; + return false; } + + public override string ToString() => Value.ToString("N"); } diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Session/SessionMetadata.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Session/SessionMetadata.cs index ca81551..899c3b0 100644 --- a/src/CodeBeam.UltimateAuth.Core/Domain/Session/SessionMetadata.cs +++ b/src/CodeBeam.UltimateAuth.Core/Domain/Session/SessionMetadata.cs @@ -1,42 +1,41 @@ -namespace CodeBeam.UltimateAuth.Core.Domain +namespace CodeBeam.UltimateAuth.Core.Domain; + +/// +/// Represents additional metadata attached to an authentication session. +/// This information is application-defined and commonly used for analytics, +/// UI adaptation, multi-tenant context, and CSRF/session-related security data. +/// +public sealed class SessionMetadata { /// - /// Represents additional metadata attached to an authentication session. - /// This information is application-defined and commonly used for analytics, - /// UI adaptation, multi-tenant context, and CSRF/session-related security data. + /// Represents an empty or uninitialized session metadata instance. /// - public sealed class SessionMetadata - { - /// - /// Represents an empty or uninitialized session metadata instance. - /// - /// Use this field to represent a default or non-existent session when no metadata is - /// available. This instance contains default values for all properties and can be used for comparison or as a - /// placeholder. - public static readonly SessionMetadata Empty = new SessionMetadata(); + /// Use this field to represent a default or non-existent session when no metadata is + /// available. This instance contains default values for all properties and can be used for comparison or as a + /// placeholder. + public static readonly SessionMetadata Empty = new SessionMetadata(); - /// - /// Gets the version of the client application that created the session. - /// Useful for enforcing upgrade policies or troubleshooting version-related issues. - /// - public string? AppVersion { get; init; } + /// + /// Gets the version of the client application that created the session. + /// Useful for enforcing upgrade policies or troubleshooting version-related issues. + /// + public string? AppVersion { get; init; } - /// - /// Gets the locale or culture identifier associated with the session, - /// such as en-US, tr-TR, or fr-FR. - /// - public string? Locale { get; init; } + /// + /// Gets the locale or culture identifier associated with the session, + /// such as en-US, tr-TR, or fr-FR. + /// + public string? Locale { get; init; } - /// - /// Gets a Cross-Site Request Forgery token or other session-scoped secret - /// used for request integrity validation in web applications. - /// - public string? CsrfToken { get; init; } + /// + /// Gets a Cross-Site Request Forgery token or other session-scoped secret + /// used for request integrity validation in web applications. + /// + public string? CsrfToken { get; init; } - /// - /// Gets a dictionary for storing arbitrary application-defined metadata. - /// Allows extensions without modifying the core authentication model. - /// - public Dictionary? Custom { get; init; } - } + /// + /// Gets a dictionary for storing arbitrary application-defined metadata. + /// Allows extensions without modifying the core authentication model. + /// + public Dictionary? Custom { get; init; } } diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Session/SessionRefreshStatus.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Session/SessionRefreshStatus.cs index d8724ba..1d4927f 100644 --- a/src/CodeBeam.UltimateAuth.Core/Domain/Session/SessionRefreshStatus.cs +++ b/src/CodeBeam.UltimateAuth.Core/Domain/Session/SessionRefreshStatus.cs @@ -1,10 +1,9 @@ -namespace CodeBeam.UltimateAuth.Core.Domain +namespace CodeBeam.UltimateAuth.Core.Domain; + +public enum SessionRefreshStatus { - public enum SessionRefreshStatus - { - Success, - ReauthRequired, - InvalidRequest, - Failed - } + Success, + ReauthRequired, + InvalidRequest, + Failed } diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Session/SessionRootId.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Session/SessionRootId.cs index 68d595a..be7c151 100644 --- a/src/CodeBeam.UltimateAuth.Core/Domain/Session/SessionRootId.cs +++ b/src/CodeBeam.UltimateAuth.Core/Domain/Session/SessionRootId.cs @@ -1,26 +1,25 @@ -namespace CodeBeam.UltimateAuth.Core.Domain +namespace CodeBeam.UltimateAuth.Core.Domain; + +public readonly record struct SessionRootId(Guid Value) { - public readonly record struct SessionRootId(Guid Value) - { - public static SessionRootId New() => new(Guid.NewGuid()); + public static SessionRootId New() => new(Guid.NewGuid()); - public static SessionRootId From(Guid value) - => value == Guid.Empty - ? throw new ArgumentException("SessionRootId cannot be empty.", nameof(value)) - : new SessionRootId(value); + public static SessionRootId From(Guid value) + => value == Guid.Empty + ? throw new ArgumentException("SessionRootId cannot be empty.", nameof(value)) + : new SessionRootId(value); - public static bool TryCreate(string raw, out SessionRootId id) + public static bool TryCreate(string raw, out SessionRootId id) + { + if (Guid.TryParse(raw, out var guid) && guid != Guid.Empty) { - if (Guid.TryParse(raw, out var guid) && guid != Guid.Empty) - { - id = new SessionRootId(guid); - return true; - } - - id = default; - return false; + id = new SessionRootId(guid); + return true; } - public override string ToString() => Value.ToString("N"); + id = default; + return false; } + + public override string ToString() => Value.ToString("N"); } diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Session/SessionState.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Session/SessionState.cs index 95a6af0..a2c01f4 100644 --- a/src/CodeBeam.UltimateAuth.Core/Domain/Session/SessionState.cs +++ b/src/CodeBeam.UltimateAuth.Core/Domain/Session/SessionState.cs @@ -1,17 +1,16 @@ -namespace CodeBeam.UltimateAuth.Core.Domain +namespace CodeBeam.UltimateAuth.Core.Domain; + +/// +/// Represents the effective runtime state of an authentication session. +/// Evaluated based on expiration rules, revocation status, and security version checks. +/// +public enum SessionState { - /// - /// Represents the effective runtime state of an authentication session. - /// Evaluated based on expiration rules, revocation status, and security version checks. - /// - public enum SessionState - { - Active, - Expired, - Revoked, - NotFound, - Invalid, - SecurityMismatch, - DeviceMismatch - } + Active, + Expired, + Revoked, + NotFound, + Invalid, + SecurityMismatch, + DeviceMismatch } diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSession.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSession.cs index 78786ef..e29878a 100644 --- a/src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSession.cs +++ b/src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSession.cs @@ -1,24 +1,142 @@ -using CodeBeam.UltimateAuth.Core.Contracts; +namespace CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Core.Domain +public sealed class UAuthSession { - public sealed class UAuthSession : ISession + public AuthSessionId SessionId { get; } + public string? TenantId { get; } + public UserKey UserKey { get; } + public SessionChainId ChainId { get; } + public DateTimeOffset CreatedAt { get; } + public DateTimeOffset ExpiresAt { get; } + public DateTimeOffset? LastSeenAt { get; } + public bool IsRevoked { get; } + public DateTimeOffset? RevokedAt { get; } + public long SecurityVersionAtCreation { get; } + public DeviceContext Device { get; } + public ClaimsSnapshot Claims { get; } + public SessionMetadata Metadata { get; } + + private UAuthSession( + AuthSessionId sessionId, + string? tenantId, + UserKey userKey, + SessionChainId chainId, + DateTimeOffset createdAt, + DateTimeOffset expiresAt, + DateTimeOffset? lastSeenAt, + bool isRevoked, + DateTimeOffset? revokedAt, + long securityVersionAtCreation, + DeviceContext device, + ClaimsSnapshot claims, + SessionMetadata metadata) { - public AuthSessionId SessionId { get; } - public string? TenantId { get; } - public UserKey UserKey { get; } - public SessionChainId ChainId { get; } - public DateTimeOffset CreatedAt { get; } - public DateTimeOffset ExpiresAt { get; } - public DateTimeOffset? LastSeenAt { get; } - public bool IsRevoked { get; } - public DateTimeOffset? RevokedAt { get; } - public long SecurityVersionAtCreation { get; } - public DeviceContext Device { get; } - public ClaimsSnapshot Claims { get; } - public SessionMetadata Metadata { get; } - - private UAuthSession( + SessionId = sessionId; + TenantId = tenantId; + UserKey = userKey; + ChainId = chainId; + CreatedAt = createdAt; + ExpiresAt = expiresAt; + LastSeenAt = lastSeenAt; + IsRevoked = isRevoked; + RevokedAt = revokedAt; + SecurityVersionAtCreation = securityVersionAtCreation; + Device = device; + Claims = claims; + Metadata = metadata; + } + + public static UAuthSession Create( + AuthSessionId sessionId, + string? tenantId, + UserKey userKey, + SessionChainId chainId, + DateTimeOffset now, + DateTimeOffset expiresAt, + DeviceContext device, + ClaimsSnapshot claims, + SessionMetadata metadata) + { + return new( + sessionId, + tenantId, + userKey, + chainId, + createdAt: now, + expiresAt: expiresAt, + lastSeenAt: now, + isRevoked: false, + revokedAt: null, + securityVersionAtCreation: 0, + device: device, + claims: claims, + metadata: metadata + ); + } + + public UAuthSession WithSecurityVersion(long version) + { + if (SecurityVersionAtCreation == version) + return this; + + return new UAuthSession( + SessionId, + TenantId, + UserKey, + ChainId, + CreatedAt, + ExpiresAt, + LastSeenAt, + IsRevoked, + RevokedAt, + version, + Device, + Claims, + Metadata + ); + } + + public UAuthSession Touch(DateTimeOffset at) + { + return new UAuthSession( + SessionId, + TenantId, + UserKey, + ChainId, + CreatedAt, + ExpiresAt, + at, + IsRevoked, + RevokedAt, + SecurityVersionAtCreation, + Device, + Claims, + Metadata + ); + } + + public UAuthSession Revoke(DateTimeOffset at) + { + if (IsRevoked) return this; + + return new UAuthSession( + SessionId, + TenantId, + UserKey, + ChainId, + CreatedAt, + ExpiresAt, + LastSeenAt, + true, + at, + SecurityVersionAtCreation, + Device, + Claims, + Metadata + ); + } + + internal static UAuthSession FromProjection( AuthSessionId sessionId, string? tenantId, UserKey userKey, @@ -32,180 +150,58 @@ private UAuthSession( DeviceContext device, ClaimsSnapshot claims, SessionMetadata metadata) - { - SessionId = sessionId; - TenantId = tenantId; - UserKey = userKey; - ChainId = chainId; - CreatedAt = createdAt; - ExpiresAt = expiresAt; - LastSeenAt = lastSeenAt; - IsRevoked = isRevoked; - RevokedAt = revokedAt; - SecurityVersionAtCreation = securityVersionAtCreation; - Device = device; - Claims = claims; - Metadata = metadata; - } - - public static UAuthSession Create( - AuthSessionId sessionId, - string? tenantId, - UserKey userKey, - SessionChainId chainId, - DateTimeOffset now, - DateTimeOffset expiresAt, - DeviceContext device, - ClaimsSnapshot claims, - SessionMetadata metadata) - { - return new( - sessionId, - tenantId, - userKey, - chainId, - createdAt: now, - expiresAt: expiresAt, - lastSeenAt: now, - isRevoked: false, - revokedAt: null, - securityVersionAtCreation: 0, - device: device, - claims: claims, - metadata: metadata - ); - } - - public UAuthSession WithSecurityVersion(long version) - { - if (SecurityVersionAtCreation == version) - return this; - - return new UAuthSession( - SessionId, - TenantId, - UserKey, - ChainId, - CreatedAt, - ExpiresAt, - LastSeenAt, - IsRevoked, - RevokedAt, - version, - Device, - Claims, - Metadata - ); - } - - public ISession Touch(DateTimeOffset at) - { - return new UAuthSession( - SessionId, - TenantId, - UserKey, - ChainId, - CreatedAt, - ExpiresAt, - at, - IsRevoked, - RevokedAt, - SecurityVersionAtCreation, - Device, - Claims, - Metadata - ); - } - - public ISession Revoke(DateTimeOffset at) - { - if (IsRevoked) return this; - - return new UAuthSession( - SessionId, - TenantId, - UserKey, - ChainId, - CreatedAt, - ExpiresAt, - LastSeenAt, - true, - at, - SecurityVersionAtCreation, - Device, - Claims, - Metadata - ); - } - - internal static UAuthSession FromProjection( - AuthSessionId sessionId, - string? tenantId, - UserKey userKey, - SessionChainId chainId, - DateTimeOffset createdAt, - DateTimeOffset expiresAt, - DateTimeOffset? lastSeenAt, - bool isRevoked, - DateTimeOffset? revokedAt, - long securityVersionAtCreation, - DeviceContext device, - ClaimsSnapshot claims, - SessionMetadata metadata) - { - return new UAuthSession( - sessionId, - tenantId, - userKey, - chainId, - createdAt, - expiresAt, - lastSeenAt, - isRevoked, - revokedAt, - securityVersionAtCreation, - device, - claims, - metadata - ); - } - - public SessionState GetState(DateTimeOffset at, TimeSpan? idleTimeout) - { - if (IsRevoked) - return SessionState.Revoked; - - if (at >= ExpiresAt) - return SessionState.Expired; - - if (idleTimeout.HasValue && at - LastSeenAt >= idleTimeout.Value) - return SessionState.Expired; - - return SessionState.Active; - } - - public ISession WithChain(SessionChainId chainId) - { - if (!ChainId.IsUnassigned) - throw new InvalidOperationException("Chain already assigned."); - - return new UAuthSession( - sessionId: SessionId, - tenantId: TenantId, - userKey: UserKey, - chainId: chainId, - createdAt: CreatedAt, - expiresAt: ExpiresAt, - lastSeenAt: LastSeenAt, - isRevoked: IsRevoked, - revokedAt: RevokedAt, - securityVersionAtCreation: SecurityVersionAtCreation, - device: Device, - claims: Claims, - metadata: Metadata - ); - } + { + return new UAuthSession( + sessionId, + tenantId, + userKey, + chainId, + createdAt, + expiresAt, + lastSeenAt, + isRevoked, + revokedAt, + securityVersionAtCreation, + device, + claims, + metadata + ); + } + public SessionState GetState(DateTimeOffset at, TimeSpan? idleTimeout) + { + if (IsRevoked) + return SessionState.Revoked; + + if (at >= ExpiresAt) + return SessionState.Expired; + + if (idleTimeout.HasValue && at - LastSeenAt >= idleTimeout.Value) + return SessionState.Expired; + + return SessionState.Active; + } + + public UAuthSession WithChain(SessionChainId chainId) + { + if (!ChainId.IsUnassigned) + throw new InvalidOperationException("Chain already assigned."); + + return new UAuthSession( + sessionId: SessionId, + tenantId: TenantId, + userKey: UserKey, + chainId: chainId, + createdAt: CreatedAt, + expiresAt: ExpiresAt, + lastSeenAt: LastSeenAt, + isRevoked: IsRevoked, + revokedAt: RevokedAt, + securityVersionAtCreation: SecurityVersionAtCreation, + device: Device, + claims: Claims, + metadata: Metadata + ); } } diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSessionChain.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSessionChain.cs index 9403f95..92a61c0 100644 --- a/src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSessionChain.cs +++ b/src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSessionChain.cs @@ -1,146 +1,145 @@ -namespace CodeBeam.UltimateAuth.Core.Domain -{ - public sealed class UAuthSessionChain : ISessionChain - { - public SessionChainId ChainId { get; } - public SessionRootId RootId { get; } - public string? TenantId { get; } - public UserKey UserKey { get; } - public int RotationCount { get; } - public long SecurityVersionAtCreation { get; } - public ClaimsSnapshot ClaimsSnapshot { get; } - public AuthSessionId? ActiveSessionId { get; } - public bool IsRevoked { get; } - public DateTimeOffset? RevokedAt { get; } +namespace CodeBeam.UltimateAuth.Core.Domain; - private UAuthSessionChain( - SessionChainId chainId, - SessionRootId rootId, - string? tenantId, - UserKey userKey, - int rotationCount, - long securityVersionAtCreation, - ClaimsSnapshot claimsSnapshot, - AuthSessionId? activeSessionId, - bool isRevoked, - DateTimeOffset? revokedAt) - { - ChainId = chainId; - RootId = rootId; - TenantId = tenantId; - UserKey = userKey; - RotationCount = rotationCount; - SecurityVersionAtCreation = securityVersionAtCreation; - ClaimsSnapshot = claimsSnapshot; - ActiveSessionId = activeSessionId; - IsRevoked = isRevoked; - RevokedAt = revokedAt; - } +public sealed class UAuthSessionChain +{ + public SessionChainId ChainId { get; } + public SessionRootId RootId { get; } + public string? TenantId { get; } + public UserKey UserKey { get; } + public int RotationCount { get; } + public long SecurityVersionAtCreation { get; } + public ClaimsSnapshot ClaimsSnapshot { get; } + public AuthSessionId? ActiveSessionId { get; } + public bool IsRevoked { get; } + public DateTimeOffset? RevokedAt { get; } - public static UAuthSessionChain Create( - SessionChainId chainId, - SessionRootId rootId, - string? tenantId, - UserKey userKey, - long securityVersion, - ClaimsSnapshot claimsSnapshot) - { - return new UAuthSessionChain( - chainId, - rootId, - tenantId, - userKey, - rotationCount: 0, - securityVersionAtCreation: securityVersion, - claimsSnapshot: claimsSnapshot, - activeSessionId: null, - isRevoked: false, - revokedAt: null - ); - } + private UAuthSessionChain( + SessionChainId chainId, + SessionRootId rootId, + string? tenantId, + UserKey userKey, + int rotationCount, + long securityVersionAtCreation, + ClaimsSnapshot claimsSnapshot, + AuthSessionId? activeSessionId, + bool isRevoked, + DateTimeOffset? revokedAt) + { + ChainId = chainId; + RootId = rootId; + TenantId = tenantId; + UserKey = userKey; + RotationCount = rotationCount; + SecurityVersionAtCreation = securityVersionAtCreation; + ClaimsSnapshot = claimsSnapshot; + ActiveSessionId = activeSessionId; + IsRevoked = isRevoked; + RevokedAt = revokedAt; + } - public ISessionChain AttachSession(AuthSessionId sessionId) - { - if (IsRevoked) - return this; + public static UAuthSessionChain Create( + SessionChainId chainId, + SessionRootId rootId, + string? tenantId, + UserKey userKey, + long securityVersion, + ClaimsSnapshot claimsSnapshot) + { + return new UAuthSessionChain( + chainId, + rootId, + tenantId, + userKey, + rotationCount: 0, + securityVersionAtCreation: securityVersion, + claimsSnapshot: claimsSnapshot, + activeSessionId: null, + isRevoked: false, + revokedAt: null + ); + } - return new UAuthSessionChain( - ChainId, - RootId, - TenantId, - UserKey, - RotationCount, // Unchanged on first attach - SecurityVersionAtCreation, - ClaimsSnapshot, - activeSessionId: sessionId, - isRevoked: false, - revokedAt: null - ); - } + public UAuthSessionChain AttachSession(AuthSessionId sessionId) + { + if (IsRevoked) + return this; - public ISessionChain RotateSession(AuthSessionId sessionId) - { - if (IsRevoked) - return this; + return new UAuthSessionChain( + ChainId, + RootId, + TenantId, + UserKey, + RotationCount, // Unchanged on first attach + SecurityVersionAtCreation, + ClaimsSnapshot, + activeSessionId: sessionId, + isRevoked: false, + revokedAt: null + ); + } - return new UAuthSessionChain( - ChainId, - RootId, - TenantId, - UserKey, - RotationCount + 1, - SecurityVersionAtCreation, - ClaimsSnapshot, - activeSessionId: sessionId, - isRevoked: false, - revokedAt: null - ); - } + public UAuthSessionChain RotateSession(AuthSessionId sessionId) + { + if (IsRevoked) + return this; - public ISessionChain Revoke(DateTimeOffset at) - { - if (IsRevoked) - return this; + return new UAuthSessionChain( + ChainId, + RootId, + TenantId, + UserKey, + RotationCount + 1, + SecurityVersionAtCreation, + ClaimsSnapshot, + activeSessionId: sessionId, + isRevoked: false, + revokedAt: null + ); + } - return new UAuthSessionChain( - ChainId, - RootId, - TenantId, - UserKey, - RotationCount, - SecurityVersionAtCreation, - ClaimsSnapshot, - ActiveSessionId, - isRevoked: true, - revokedAt: at - ); - } + public UAuthSessionChain Revoke(DateTimeOffset at) + { + if (IsRevoked) + return this; - internal static UAuthSessionChain FromProjection( - SessionChainId chainId, - SessionRootId rootId, - string? tenantId, - UserKey userKey, - int rotationCount, - long securityVersionAtCreation, - ClaimsSnapshot claimsSnapshot, - AuthSessionId? activeSessionId, - bool isRevoked, - DateTimeOffset? revokedAt) - { - return new UAuthSessionChain( - chainId, - rootId, - tenantId, - userKey, - rotationCount, - securityVersionAtCreation, - claimsSnapshot, - activeSessionId, - isRevoked, - revokedAt - ); - } + return new UAuthSessionChain( + ChainId, + RootId, + TenantId, + UserKey, + RotationCount, + SecurityVersionAtCreation, + ClaimsSnapshot, + ActiveSessionId, + isRevoked: true, + revokedAt: at + ); + } + internal static UAuthSessionChain FromProjection( + SessionChainId chainId, + SessionRootId rootId, + string? tenantId, + UserKey userKey, + int rotationCount, + long securityVersionAtCreation, + ClaimsSnapshot claimsSnapshot, + AuthSessionId? activeSessionId, + bool isRevoked, + DateTimeOffset? revokedAt) + { + return new UAuthSessionChain( + chainId, + rootId, + tenantId, + userKey, + rotationCount, + securityVersionAtCreation, + claimsSnapshot, + activeSessionId, + isRevoked, + revokedAt + ); } + } diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSessionRoot.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSessionRoot.cs index 0153210..9efd9c9 100644 --- a/src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSessionRoot.cs +++ b/src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSessionRoot.cs @@ -1,109 +1,108 @@ -namespace CodeBeam.UltimateAuth.Core.Domain +namespace CodeBeam.UltimateAuth.Core.Domain; + +public sealed class UAuthSessionRoot { - public sealed class UAuthSessionRoot : ISessionRoot - { - public SessionRootId RootId { get; } - public UserKey UserKey { get; } - public string? TenantId { get; } - public bool IsRevoked { get; } - public DateTimeOffset? RevokedAt { get; } - public long SecurityVersion { get; } - public IReadOnlyList Chains { get; } - public DateTimeOffset LastUpdatedAt { get; } + public SessionRootId RootId { get; } + public UserKey UserKey { get; } + public string? TenantId { get; } + public bool IsRevoked { get; } + public DateTimeOffset? RevokedAt { get; } + public long SecurityVersion { get; } + public IReadOnlyList Chains { get; } + public DateTimeOffset LastUpdatedAt { get; } - private UAuthSessionRoot( - SessionRootId rootId, - string? tenantId, - UserKey userKey, - bool isRevoked, - DateTimeOffset? revokedAt, - long securityVersion, - IReadOnlyList chains, - DateTimeOffset lastUpdatedAt) - { - RootId = rootId; - TenantId = tenantId; - UserKey = userKey; - IsRevoked = isRevoked; - RevokedAt = revokedAt; - SecurityVersion = securityVersion; - Chains = chains; - LastUpdatedAt = lastUpdatedAt; - } + private UAuthSessionRoot( + SessionRootId rootId, + string? tenantId, + UserKey userKey, + bool isRevoked, + DateTimeOffset? revokedAt, + long securityVersion, + IReadOnlyList chains, + DateTimeOffset lastUpdatedAt) + { + RootId = rootId; + TenantId = tenantId; + UserKey = userKey; + IsRevoked = isRevoked; + RevokedAt = revokedAt; + SecurityVersion = securityVersion; + Chains = chains; + LastUpdatedAt = lastUpdatedAt; + } - public static ISessionRoot Create( - string? tenantId, - UserKey userKey, - DateTimeOffset issuedAt) - { - return new UAuthSessionRoot( - SessionRootId.New(), - tenantId, - userKey, - isRevoked: false, - revokedAt: null, - securityVersion: 0, - chains: Array.Empty(), - lastUpdatedAt: issuedAt - ); - } + public static UAuthSessionRoot Create( + string? tenantId, + UserKey userKey, + DateTimeOffset issuedAt) + { + return new UAuthSessionRoot( + SessionRootId.New(), + tenantId, + userKey, + isRevoked: false, + revokedAt: null, + securityVersion: 0, + chains: Array.Empty(), + lastUpdatedAt: issuedAt + ); + } - public ISessionRoot Revoke(DateTimeOffset at) - { - if (IsRevoked) - return this; + public UAuthSessionRoot Revoke(DateTimeOffset at) + { + if (IsRevoked) + return this; - return new UAuthSessionRoot( - RootId, - TenantId, - UserKey, - isRevoked: true, - revokedAt: at, - securityVersion: SecurityVersion, - chains: Chains, - lastUpdatedAt: at - ); - } + return new UAuthSessionRoot( + RootId, + TenantId, + UserKey, + isRevoked: true, + revokedAt: at, + securityVersion: SecurityVersion, + chains: Chains, + lastUpdatedAt: at + ); + } - public ISessionRoot AttachChain(ISessionChain chain, DateTimeOffset at) - { - if (IsRevoked) - return this; + public UAuthSessionRoot AttachChain(UAuthSessionChain chain, DateTimeOffset at) + { + if (IsRevoked) + return this; - return new UAuthSessionRoot( - RootId, - TenantId, - UserKey, - IsRevoked, - RevokedAt, - SecurityVersion, - Chains.Concat(new[] { chain }).ToArray(), - at - ); - } + return new UAuthSessionRoot( + RootId, + TenantId, + UserKey, + IsRevoked, + RevokedAt, + SecurityVersion, + Chains.Concat(new[] { chain }).ToArray(), + at + ); + } - internal static UAuthSessionRoot FromProjection( - SessionRootId rootId, - string? tenantId, - UserKey userKey, - bool isRevoked, - DateTimeOffset? revokedAt, - long securityVersion, - IReadOnlyList chains, - DateTimeOffset lastUpdatedAt) - { - return new UAuthSessionRoot( - rootId, - tenantId, - userKey, - isRevoked, - revokedAt, - securityVersion, - chains, - lastUpdatedAt - ); - } + internal static UAuthSessionRoot FromProjection( + SessionRootId rootId, + string? tenantId, + UserKey userKey, + bool isRevoked, + DateTimeOffset? revokedAt, + long securityVersion, + IReadOnlyList chains, + DateTimeOffset lastUpdatedAt) + { + return new UAuthSessionRoot( + rootId, + tenantId, + userKey, + isRevoked, + revokedAt, + securityVersion, + chains, + lastUpdatedAt + ); + } - } } diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/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/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/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/TenantContext.cs b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/TenantContext.cs new file mode 100644 index 0000000..3c596c6 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/TenantContext.cs @@ -0,0 +1,13 @@ +namespace CodeBeam.UltimateAuth.Core.MultiTenancy; + +public sealed class TenantContext +{ + public string? TenantId { get; } + public bool IsGlobal { get; } + + public TenantContext(string? tenantId, bool isGlobal = false) + { + TenantId = tenantId; + IsGlobal = isGlobal; + } +} diff --git a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/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..7df4ef9 100644 --- a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/TenantValidation.cs +++ b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/TenantValidation.cs @@ -1,28 +1,25 @@ 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/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/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/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.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/Abstractions/IHttpSessionIssuer.cs b/src/CodeBeam.UltimateAuth.Server/Abstractions/IHttpSessionIssuer.cs deleted file mode 100644 index 75edff5..0000000 --- a/src/CodeBeam.UltimateAuth.Server/Abstractions/IHttpSessionIssuer.cs +++ /dev/null @@ -1,19 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Abstractions; -using CodeBeam.UltimateAuth.Core.Contracts; -using CodeBeam.UltimateAuth.Core.Domain; -using Microsoft.AspNetCore.Http; - -namespace CodeBeam.UltimateAuth.Server.Abstractions -{ - /// - /// HTTP-aware session issuer used by UltimateAuth server components. - /// Extends the core ISessionIssuer contract with HttpContext-bound - /// operations required for cookie-based session binding. - /// - public interface IHttpSessionIssuer : ISessionIssuer - { - Task IssueLoginSessionAsync(HttpContext httpContext, AuthenticatedSessionContext context, CancellationToken ct = default); - - Task RotateSessionAsync(HttpContext httpContext, SessionRotationContext context, CancellationToken ct = default); - } -} diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthFlowContextFactory.cs b/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthFlowContextFactory.cs index 6b3e618..165def4 100644 --- a/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthFlowContextFactory.cs +++ b/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthFlowContextFactory.cs @@ -22,7 +22,7 @@ internal sealed class DefaultAuthFlowContextFactory : IAuthFlowContextFactory private readonly IAuthResponseResolver _authResponseResolver; private readonly IDeviceResolver _deviceResolver; private readonly IDeviceContextFactory _deviceContextFactory; - private readonly ISessionQueryService _sessionQueryService; + private readonly ISessionValidator _sessionValidator; public DefaultAuthFlowContextFactory( IClientProfileReader clientProfileReader, @@ -31,7 +31,7 @@ public DefaultAuthFlowContextFactory( IAuthResponseResolver authResponseResolver, IDeviceResolver deviceResolver, IDeviceContextFactory deviceContextFactory, - ISessionQueryService sessionQueryService) + ISessionValidator sessionValidator) { _clientProfileReader = clientProfileReader; _primaryTokenResolver = primaryTokenResolver; @@ -39,7 +39,7 @@ public DefaultAuthFlowContextFactory( _authResponseResolver = authResponseResolver; _deviceResolver = deviceResolver; _deviceContextFactory = deviceContextFactory; - _sessionQueryService = sessionQueryService; + _sessionValidator = sessionValidator; } public async ValueTask CreateAsync(HttpContext ctx, AuthFlowType flowType, CancellationToken ct = default) @@ -64,7 +64,7 @@ public async ValueTask CreateAsync(HttpContext ctx, AuthFlowTyp if (!sessionCtx.IsAnonymous) { - var validation = await _sessionQueryService.ValidateSessionAsync( + var validation = await _sessionValidator.ValidateSessionAsync( new SessionValidationContext { TenantId = sessionCtx.TenantId, diff --git a/src/CodeBeam.UltimateAuth.Server/Authentication/UAuthAuthenticationHandler.cs b/src/CodeBeam.UltimateAuth.Server/Authentication/UAuthAuthenticationHandler.cs index 107c95d..67507ae 100644 --- a/src/CodeBeam.UltimateAuth.Server/Authentication/UAuthAuthenticationHandler.cs +++ b/src/CodeBeam.UltimateAuth.Server/Authentication/UAuthAuthenticationHandler.cs @@ -4,18 +4,16 @@ using CodeBeam.UltimateAuth.Core.Extensions; using CodeBeam.UltimateAuth.Server.Defaults; using CodeBeam.UltimateAuth.Server.Infrastructure; -using CodeBeam.UltimateAuth.Server.Services; using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using System.Security.Claims; namespace CodeBeam.UltimateAuth.Server.Authentication; internal sealed class UAuthAuthenticationHandler : AuthenticationHandler { private readonly ITransportCredentialResolver _transportCredentialResolver; - private readonly ISessionQueryService _sessionQuery; + private readonly ISessionValidator _sessionValidator; private readonly IDeviceContextFactory _deviceContextFactory; private readonly IClock _clock; @@ -25,13 +23,13 @@ public UAuthAuthenticationHandler( ILoggerFactory logger, System.Text.Encodings.Web.UrlEncoder encoder, ISystemClock clock, - ISessionQueryService sessionQuery, + ISessionValidator sessionValidator, IDeviceContextFactory deviceContextFactory, IClock uauthClock) : base(options, logger, encoder, clock) { _transportCredentialResolver = transportCredentialResolver; - _sessionQuery = sessionQuery; + _sessionValidator = sessionValidator; _deviceContextFactory = deviceContextFactory; _clock = uauthClock; } @@ -45,7 +43,7 @@ protected override async Task HandleAuthenticateAsync() if (!AuthSessionId.TryCreate(credential.Value, out var sessionId)) return AuthenticateResult.Fail("Invalid credential"); - var result = await _sessionQuery.ValidateSessionAsync( + var result = await _sessionValidator.ValidateSessionAsync( new SessionValidationContext { TenantId = credential.TenantId, @@ -59,46 +57,5 @@ protected override async Task HandleAuthenticateAsync() var principal = result.Claims.ToClaimsPrincipal(UAuthCookieDefaults.AuthenticationScheme); return AuthenticateResult.Success(new AuthenticationTicket(principal, UAuthCookieDefaults.AuthenticationScheme)); - - - //var principal = CreatePrincipal(result); - //var ticket = new AuthenticationTicket(principal,UAuthCookieDefaults.AuthenticationScheme); - - //return AuthenticateResult.Success(ticket); } - - private static ClaimsPrincipal CreatePrincipal(SessionValidationResult result) - { - //var claims = new List - //{ - // new Claim(ClaimTypes.NameIdentifier, result.UserKey.Value), - // new Claim("uauth:session_id", result.SessionId.ToString()) - //}; - - //if (!string.IsNullOrEmpty(result.TenantId)) - //{ - // claims.Add(new Claim("uauth:tenant", result.TenantId)); - //} - - //// Session claims (snapshot) - //foreach (var (key, value) in result.Claims.AsDictionary()) - //{ - // if (key == "http://schemas.microsoft.com/ws/2008/06/identity/claims/role") - // { - // foreach (var role in value.Split(',')) - // claims.Add(new Claim(ClaimTypes.Role, role)); - // } - // else - // { - // claims.Add(new Claim(key, value)); - // } - //} - - //var identity = new ClaimsIdentity(claims, UAuthCookieDefaults.AuthenticationScheme); - //return new ClaimsPrincipal(identity); - - var identity = new ClaimsIdentity(result.Claims.ToClaims(), UAuthCookieDefaults.AuthenticationScheme); - return new ClaimsPrincipal(identity); - } - } \ No newline at end of file diff --git a/src/CodeBeam.UltimateAuth.Server/Composition/UltimateAuthServerBuilderValidation.cs b/src/CodeBeam.UltimateAuth.Server/Composition/UltimateAuthServerBuilderValidation.cs index 780d68f..db88f96 100644 --- a/src/CodeBeam.UltimateAuth.Server/Composition/UltimateAuthServerBuilderValidation.cs +++ b/src/CodeBeam.UltimateAuth.Server/Composition/UltimateAuthServerBuilderValidation.cs @@ -12,10 +12,10 @@ 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)))) + if (!services.Any(sd => sd.ServiceType.IsAssignableTo(typeof(ISessionStoreKernel)))) throw new InvalidOperationException("No session store registered."); return services; diff --git a/src/CodeBeam.UltimateAuth.Server/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/Endpoints/DefaultValidateEndpointHandler.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultValidateEndpointHandler.cs index f47f8a6..4135b0c 100644 --- a/src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultValidateEndpointHandler.cs +++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultValidateEndpointHandler.cs @@ -1,97 +1,94 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; -using CodeBeam.UltimateAuth.Core.Extensions; using CodeBeam.UltimateAuth.Server.Auth; using CodeBeam.UltimateAuth.Server.Infrastructure; -using CodeBeam.UltimateAuth.Server.Services; using Microsoft.AspNetCore.Http; -namespace CodeBeam.UltimateAuth.Server.Endpoints +namespace CodeBeam.UltimateAuth.Server.Endpoints; + +internal sealed class DefaultValidateEndpointHandler : IValidateEndpointHandler { - internal sealed class DefaultValidateEndpointHandler : IValidateEndpointHandler + private readonly IAuthFlowContextAccessor _authContext; + private readonly IFlowCredentialResolver _credentialResolver; + private readonly ISessionValidator _sessionValidator; + private readonly IClock _clock; + + public DefaultValidateEndpointHandler( + IAuthFlowContextAccessor authContext, + IFlowCredentialResolver credentialResolver, + ISessionValidator sessionValidator, + IClock clock) + { + _authContext = authContext; + _credentialResolver = credentialResolver; + _sessionValidator = sessionValidator; + _clock = clock; + } + + public async Task ValidateAsync(HttpContext context, CancellationToken ct = default) { - private readonly IAuthFlowContextAccessor _authContext; - private readonly IFlowCredentialResolver _credentialResolver; - private readonly ISessionQueryService _sessionValidator; - private readonly IClock _clock; + var auth = _authContext.Current; + var credential = _credentialResolver.Resolve(context, auth.Response); - public DefaultValidateEndpointHandler( - IAuthFlowContextAccessor authContext, - IFlowCredentialResolver credentialResolver, - ISessionQueryService sessionValidator, - IClock clock) + if (credential is null) { - _authContext = authContext; - _credentialResolver = credentialResolver; - _sessionValidator = sessionValidator; - _clock = clock; + return Results.Json( + new AuthValidationResult + { + IsValid = false, + State = "missing" + }, + statusCode: StatusCodes.Status401Unauthorized + ); } - public async Task ValidateAsync(HttpContext context, CancellationToken ct = default) + if (credential.Kind == PrimaryCredentialKind.Stateful) { - var auth = _authContext.Current; - var credential = _credentialResolver.Resolve(context, auth.Response); - - if (credential is null) + if (!AuthSessionId.TryCreate(credential.Value, out var sessionId)) { return Results.Json( new AuthValidationResult { IsValid = false, - State = "missing" + State = "invalid" }, statusCode: StatusCodes.Status401Unauthorized ); } - if (credential.Kind == PrimaryCredentialKind.Stateful) - { - if (!AuthSessionId.TryCreate(credential.Value, out var sessionId)) + var result = await _sessionValidator.ValidateSessionAsync( + new SessionValidationContext { - return Results.Json( - new AuthValidationResult - { - IsValid = false, - State = "invalid" - }, - statusCode: StatusCodes.Status401Unauthorized - ); - } - - var result = await _sessionValidator.ValidateSessionAsync( - new SessionValidationContext - { - TenantId = credential.TenantId, - SessionId = sessionId, - Now = _clock.UtcNow, - Device = auth.Device - }, - ct); - - return Results.Ok(new AuthValidationResult - { - IsValid = result.IsValid, - State = result.IsValid ? "active" : result.State.ToString().ToLowerInvariant(), - Snapshot = new AuthStateSnapshot - { - UserId = result.UserKey, - TenantId = result.TenantId, - Claims = result.Claims, - AuthenticatedAt = _clock.UtcNow, - } - }); - } + TenantId = credential.TenantId, + SessionId = sessionId, + Now = _clock.UtcNow, + Device = auth.Device + }, + ct); - // Stateless (JWT / Opaque) – 0.0.1 no support yet - return Results.Json( - new AuthValidationResult + return Results.Ok(new AuthValidationResult + { + IsValid = result.IsValid, + State = result.IsValid ? "active" : result.State.ToString().ToLowerInvariant(), + Snapshot = new AuthStateSnapshot { - IsValid = false, - State = "unsupported" - }, - statusCode: StatusCodes.Status401Unauthorized - ); + UserId = result.UserKey, + TenantId = result.TenantId, + Claims = result.Claims, + AuthenticatedAt = _clock.UtcNow, + } + }); } + + // Stateless (JWT / Opaque) – 0.0.1 no support yet + return Results.Json( + new AuthValidationResult + { + IsValid = false, + State = "unsupported" + }, + statusCode: StatusCodes.Status401Unauthorized + ); } } diff --git a/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthServerServiceCollectionExtensions.cs b/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthServerServiceCollectionExtensions.cs index 494af80..4f6cf0b 100644 --- a/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthServerServiceCollectionExtensions.cs +++ b/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthServerServiceCollectionExtensions.cs @@ -176,10 +176,10 @@ 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)); + services.TryAddScoped(); services.TryAddScoped(); services.TryAddScoped(); services.TryAddScoped(); @@ -230,7 +230,6 @@ private static IServiceCollection AddUltimateAuthServerInternal(this IServiceCol services.TryAddScoped(); - services.TryAddSingleton(); // Endpoint handlers @@ -248,43 +247,10 @@ private static IServiceCollection AddUltimateAuthServerInternal(this IServiceCol services.TryAddScoped>(); services.TryAddScoped(); - //services.TryAddScoped(); - //services.TryAddScoped(); - //services.TryAddScoped(); return services; } - //internal static IServiceCollection AddUltimateAuthPolicies(this IServiceCollection services, Action? configure = null) - //{ - // if (services.Any(d => d.ServiceType == typeof(AccessPolicyRegistry))) - // throw new InvalidOperationException("UltimateAuth policies already registered."); - - // var registry = new AccessPolicyRegistry(); - - // DefaultPolicySet.Register(registry); - // configure?.Invoke(registry); - // services.AddSingleton(registry); - // services.AddSingleton(sp => - // { - // var compiled = registry.Build(); - // return new DefaultAccessPolicyProvider(compiled, sp); - // }); - - // services.TryAddScoped(sp => - // { - // var invariants = sp.GetServices(); - // var globalPolicies = sp.GetServices(); - - // return new DefaultAccessAuthority(invariants, globalPolicies); - // }); - - // services.TryAddScoped(); - - - // return services; - //} - internal static IServiceCollection AddUltimateAuthPolicies(this IServiceCollection services, Action? configure = null) { if (services.Any(d => d.ServiceType == typeof(AccessPolicyRegistry))) diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeChainCommand.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeChainCommand.cs index c8c1a37..3801af3 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeChainCommand.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeChainCommand.cs @@ -13,17 +13,9 @@ public RevokeChainCommand(SessionChainId chainId) ChainId = chainId; } - public async Task ExecuteAsync( - AuthContext context, - ISessionIssuer issuer, - CancellationToken ct) + public async Task ExecuteAsync(AuthContext context, ISessionIssuer issuer, CancellationToken ct) { - await issuer.RevokeChainAsync( - context.TenantId, - ChainId, - context.At, - ct); - + await issuer.RevokeChainAsync(context.TenantId, ChainId, context.At, ct); return Unit.Value; } } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeSessionCommand.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeSessionCommand.cs index 86fa7fd..b32e9d4 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeSessionCommand.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeSessionCommand.cs @@ -2,14 +2,13 @@ using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Server.Infrastructure +namespace CodeBeam.UltimateAuth.Server.Infrastructure; + +internal sealed record RevokeSessionCommand(AuthSessionId SessionId) : ISessionCommand { - internal sealed record RevokeSessionCommand(string? TenantId, AuthSessionId SessionId) : ISessionCommand + public async Task ExecuteAsync(AuthContext context, ISessionIssuer issuer, CancellationToken ct) { - public async Task ExecuteAsync(AuthContext _, ISessionIssuer issuer, CancellationToken ct) - { - await issuer.RevokeSessionAsync(TenantId, SessionId, _.At, ct); - return Unit.Value; - } + await issuer.RevokeSessionAsync(context.TenantId, SessionId, context.At, ct); + return Unit.Value; } } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/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/src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/DefaultSessionTouchService.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/DefaultSessionTouchService.cs index c8a6c81..ab232d2 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/DefaultSessionTouchService.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Refresh/DefaultSessionTouchService.cs @@ -1,42 +1,45 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Contracts; -namespace CodeBeam.UltimateAuth.Server.Infrastructure +namespace CodeBeam.UltimateAuth.Server.Infrastructure; + +public sealed class DefaultSessionTouchService : ISessionTouchService { - public sealed class DefaultSessionTouchService : ISessionTouchService + private readonly ISessionStoreKernelFactory _kernelFactory; + + public DefaultSessionTouchService(ISessionStoreKernelFactory kernelFactory) { - private readonly ISessionStore _sessionStore; + _kernelFactory = kernelFactory; + } - public DefaultSessionTouchService(ISessionStore sessionStore) - { - _sessionStore = sessionStore; - } + // It's designed for PureOpaque sessions, which do not issue new refresh tokens on refresh. + // That's why the service access store direcly: There is no security flow here, only validate and touch session. + public async Task RefreshAsync(SessionValidationResult validation, SessionTouchPolicy policy, SessionTouchMode sessionTouchMode, DateTimeOffset now, CancellationToken ct = default) + { + if (!validation.IsValid || validation.SessionId is null) + return SessionRefreshResult.ReauthRequired(); - // It's designed for PureOpaque sessions, which do not issue new refresh tokens on refresh. - // That's why the service access store direcly: There is no security flow here, only validate and touch session. - public async Task RefreshAsync(SessionValidationResult validation, SessionTouchPolicy policy, SessionTouchMode sessionTouchMode, DateTimeOffset now, CancellationToken ct = default) - { - if (!validation.IsValid) - return SessionRefreshResult.ReauthRequired(); + if (!policy.TouchInterval.HasValue) + return SessionRefreshResult.Success(validation.SessionId.Value, didTouch: false); - //var session = validation.Session; - bool didTouch = false; + var kernel = _kernelFactory.Create(validation.TenantId); - if (policy.TouchInterval.HasValue) - { - //var elapsed = now - session.LastSeenAt; + bool didTouch = false; + + await kernel.ExecuteAsync(async _ => + { + var session = await kernel.GetSessionAsync(validation.SessionId.Value); + if (session is null || session.IsRevoked) + return; - //if (elapsed >= policy.TouchInterval.Value) - //{ - // var touched = session.Touch(now); - // await _activityWriter.TouchAsync(validation.TenantId, touched, ct); - // didTouch = true; - //} + if (sessionTouchMode == SessionTouchMode.IfNeeded && now - session.LastSeenAt < policy.TouchInterval.Value) + return; - didTouch = await _sessionStore.TouchSessionAsync(validation.SessionId.Value, now, sessionTouchMode, ct); - } + var touched = session.Touch(now); + await kernel.SaveSessionAsync(touched); + didTouch = true; + }, ct); - return SessionRefreshResult.Success(validation.SessionId.Value, didTouch); - } + return SessionRefreshResult.Success(validation.SessionId.Value, didTouch); } } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/User/UAuthUserAccessor.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/User/UAuthUserAccessor.cs index 34f7677..8c9d46b 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/User/UAuthUserAccessor.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/User/UAuthUserAccessor.cs @@ -4,42 +4,39 @@ using CodeBeam.UltimateAuth.Server.Middlewares; using Microsoft.AspNetCore.Http; -namespace CodeBeam.UltimateAuth.Server.Infrastructure +namespace CodeBeam.UltimateAuth.Server.Infrastructure; + +public sealed class UAuthUserAccessor : IUserAccessor { - public sealed class UAuthUserAccessor : IUserAccessor + private readonly ISessionStoreKernelFactory _kernelFactory; + private readonly IUserIdConverter _userIdConverter; + + public UAuthUserAccessor(ISessionStoreKernelFactory kernelFactory, IUserIdConverterResolver converterResolver) { - private readonly ISessionStore _sessionStore; - private readonly IUserIdConverter _userIdConverter; + _kernelFactory = kernelFactory; + _userIdConverter = converterResolver.GetConverter(); + } - public UAuthUserAccessor( - ISessionStore sessionStore, - IUserIdConverterResolver converterResolver) - { - _sessionStore = sessionStore; - _userIdConverter = converterResolver.GetConverter(); - } + public async Task ResolveAsync(HttpContext context) + { + var sessionCtx = context.GetSessionContext(); - public async Task ResolveAsync(HttpContext context) + if (sessionCtx.IsAnonymous || sessionCtx.SessionId is null) { - var sessionCtx = context.GetSessionContext(); - - if (sessionCtx.IsAnonymous) - { - context.Items[UserMiddleware.UserContextKey] = AuthUserSnapshot.Anonymous(); - return; - } - - var session = await _sessionStore.GetSessionAsync(sessionCtx.TenantId, sessionCtx.SessionId!.Value); + context.Items[UserMiddleware.UserContextKey] = AuthUserSnapshot.Anonymous(); + return; + } - if (session is null || session.IsRevoked) - { - context.Items[UserMiddleware.UserContextKey] = AuthUserSnapshot.Anonymous(); - return; - } + var kernel = _kernelFactory.Create(sessionCtx.TenantId); + var session = await kernel.GetSessionAsync(sessionCtx.SessionId.Value); - var userId = _userIdConverter.FromString(session.UserKey.Value); - context.Items[UserMiddleware.UserContextKey] = AuthUserSnapshot.Authenticated(userId); + if (session is null || session.IsRevoked) + { + context.Items[UserMiddleware.UserContextKey] = AuthUserSnapshot.Anonymous(); + return; } + var userId = _userIdConverter.FromString(session.UserKey.Value); + context.Items[UserMiddleware.UserContextKey] = AuthUserSnapshot.Authenticated(userId); } } diff --git a/src/CodeBeam.UltimateAuth.Server/Issuers/UAuthSessionIssuer.cs b/src/CodeBeam.UltimateAuth.Server/Issuers/UAuthSessionIssuer.cs index a10a2b1..fb360ef 100644 --- a/src/CodeBeam.UltimateAuth.Server/Issuers/UAuthSessionIssuer.cs +++ b/src/CodeBeam.UltimateAuth.Server/Issuers/UAuthSessionIssuer.cs @@ -2,178 +2,201 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; -using CodeBeam.UltimateAuth.Server.Abstractions; -using CodeBeam.UltimateAuth.Server.Cookies; using CodeBeam.UltimateAuth.Server.Options; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; using System.Security; -namespace CodeBeam.UltimateAuth.Server.Issuers +namespace CodeBeam.UltimateAuth.Server.Issuers; + +public sealed class UAuthSessionIssuer : ISessionIssuer { - public sealed class UAuthSessionIssuer : IHttpSessionIssuer + private readonly ISessionStoreKernelFactory _kernelFactory; + private readonly IOpaqueTokenGenerator _opaqueGenerator; + private readonly UAuthServerOptions _options; + + public UAuthSessionIssuer( + ISessionStoreKernelFactory kernelFactory, + IOpaqueTokenGenerator opaqueGenerator, + IOptions options) { - private readonly ISessionStore _sessionStore; - private readonly IOpaqueTokenGenerator _opaqueGenerator; - private readonly UAuthServerOptions _options; - private readonly IUAuthCookieManager _cookieManager; + _kernelFactory = kernelFactory; + _opaqueGenerator = opaqueGenerator; + _options = options.Value; + } - public UAuthSessionIssuer(ISessionStore sessionStore, IOpaqueTokenGenerator opaqueGenerator, IOptions options, IUAuthCookieManager cookieManager) + public async Task IssueLoginSessionAsync(AuthenticatedSessionContext context, CancellationToken ct = default) + { + // Defensive guard — enforcement belongs to Authority + if (_options.Mode == UAuthMode.PureJwt) { - _sessionStore = sessionStore; - _opaqueGenerator = opaqueGenerator; - _options = options.Value; - _cookieManager = cookieManager; + throw new InvalidOperationException("Session issuance is not allowed in PureJwt mode."); } - public Task IssueLoginSessionAsync(AuthenticatedSessionContext context, CancellationToken ct = default) + var now = context.Now; + var opaqueSessionId = _opaqueGenerator.Generate(); + if (!AuthSessionId.TryCreate(opaqueSessionId, out AuthSessionId sessionId)) + throw new InvalidCastException("Can't create opaque id."); + + var expiresAt = now.Add(_options.Session.Lifetime); + + if (_options.Session.MaxLifetime is not null) { - return IssueLoginInternalAsync(httpContext: null, context, ct); + var absoluteExpiry = now.Add(_options.Session.MaxLifetime.Value); + if (absoluteExpiry < expiresAt) + expiresAt = absoluteExpiry; } - public Task IssueLoginSessionAsync(HttpContext httpContext, AuthenticatedSessionContext context, CancellationToken ct = default) + var session = UAuthSession.Create( + sessionId: sessionId, + tenantId: context.TenantId, + userKey: context.UserKey, + chainId: SessionChainId.Unassigned, + now: now, + expiresAt: expiresAt, + claims: context.Claims, + device: context.Device, + metadata: context.Metadata + ); + + var issued = new IssuedSession { - if (httpContext is null) - throw new ArgumentNullException(nameof(httpContext)); + Session = session, + OpaqueSessionId = opaqueSessionId, + IsMetadataOnly = _options.Mode == UAuthMode.SemiHybrid + }; - return IssueLoginInternalAsync(httpContext, context, ct); - } + var kernel = _kernelFactory.Create(context.TenantId); - private async Task IssueLoginInternalAsync(HttpContext? httpContext, AuthenticatedSessionContext context, CancellationToken ct = default) + await kernel.ExecuteAsync(async _ => { - // Defensive guard — enforcement belongs to Authority - if (_options.Mode == UAuthMode.PureJwt) + var root = await kernel.GetSessionRootByUserAsync(context.UserKey) + ?? UAuthSessionRoot.Create(context.TenantId, context.UserKey, now); + + UAuthSessionChain chain; + + if (context.ChainId is not null) + { + chain = await kernel.GetChainAsync(context.ChainId.Value) + ?? throw new SecurityException("Chain not found."); + } + else { - throw new InvalidOperationException("Session issuance is not allowed in PureJwt mode."); + chain = UAuthSessionChain.Create( + SessionChainId.New(), + root.RootId, + context.TenantId, + context.UserKey, + root.SecurityVersion, + ClaimsSnapshot.Empty); + + await kernel.SaveChainAsync(chain); + root = root.AttachChain(chain, now); } - var now = context.Now; - var opaqueSessionId = _opaqueGenerator.Generate(); - if (!AuthSessionId.TryCreate(opaqueSessionId, out AuthSessionId sessionId)) - throw new InvalidCastException("Can't create opaque id."); + var boundSession = session.WithChain(chain.ChainId); - var expiresAt = now.Add(_options.Session.Lifetime); + await kernel.SaveSessionAsync(boundSession); + await kernel.SetActiveSessionIdAsync(chain.ChainId, boundSession.SessionId); + await kernel.SaveSessionRootAsync(root); + }, ct); - if (_options.Session.MaxLifetime is not null) - { - var absoluteExpiry = now.Add(_options.Session.MaxLifetime.Value); - if (absoluteExpiry < expiresAt) - expiresAt = absoluteExpiry; - } + return issued; + } - var session = UAuthSession.Create( - sessionId: sessionId, + public async Task RotateSessionAsync(SessionRotationContext context, CancellationToken ct = default) + { + var kernel = _kernelFactory.Create(context.TenantId); + var now = context.Now; + + var opaqueSessionId = _opaqueGenerator.Generate(); + if (!AuthSessionId.TryCreate(opaqueSessionId, out var newSessionId)) + throw new InvalidCastException("Can't create opaque session id."); + + var expiresAt = now.Add(_options.Session.Lifetime); + if (_options.Session.MaxLifetime is not null) + { + var absoluteExpiry = now.Add(_options.Session.MaxLifetime.Value); + if (absoluteExpiry < expiresAt) + expiresAt = absoluteExpiry; + } + + var issued = new IssuedSession + { + Session = UAuthSession.Create( + sessionId: newSessionId, tenantId: context.TenantId, userKey: context.UserKey, - chainId: SessionChainId.Unassigned, + chainId: SessionChainId.Unassigned, now: now, expiresAt: expiresAt, - claims: context.Claims, device: context.Device, + claims: context.Claims, metadata: context.Metadata - ); - - var issued = new IssuedSession - { - Session = session, - OpaqueSessionId = opaqueSessionId, - IsMetadataOnly = _options.Mode == UAuthMode.SemiHybrid - }; - - await _sessionStore.CreateSessionAsync(issued, - new SessionStoreContext - { - TenantId = context.TenantId, - UserKey = context.UserKey, - ChainId = context.ChainId, - IssuedAt = now, - Device = context.Device - }, - ct - ); - - return issued; - } + ), + OpaqueSessionId = opaqueSessionId, + IsMetadataOnly = _options.Mode == UAuthMode.SemiHybrid + }; - public Task RotateSessionAsync(SessionRotationContext context, CancellationToken ct = default) + await kernel.ExecuteAsync(async _ => { - return RotateInternalAsync(httpContext: null, context, ct); - } + var oldSession = await kernel.GetSessionAsync(context.CurrentSessionId) + ?? throw new SecurityException("Session not found"); - public Task RotateSessionAsync(HttpContext httpContext, SessionRotationContext context, CancellationToken ct = default) - { - if (httpContext is null) - throw new ArgumentNullException(nameof(httpContext)); + if (oldSession.IsRevoked || oldSession.ExpiresAt <= now) + throw new SecurityException("Session is not valid"); - return RotateInternalAsync(httpContext, context, ct); - } + var chain = await kernel.GetChainAsync(oldSession.ChainId) + ?? throw new SecurityException("Chain not found"); - private async Task RotateInternalAsync(HttpContext httpContext, SessionRotationContext context, CancellationToken ct = default) - { - var now = context.Now; + var bound = issued.Session.WithChain(chain.ChainId); - var opaqueSessionId = _opaqueGenerator.Generate(); - if (!AuthSessionId.TryCreate(opaqueSessionId, out var newSessionId)) - throw new InvalidCastException("Can't create opaque session id."); + await kernel.SaveSessionAsync(bound); + await kernel.SetActiveSessionIdAsync(chain.ChainId, bound.SessionId); + await kernel.RevokeSessionAsync(oldSession.SessionId, now); + }, ct); - var expiresAt = now.Add(_options.Session.Lifetime); - if (_options.Session.MaxLifetime is not null) - { - var absoluteExpiry = now.Add(_options.Session.MaxLifetime.Value); - if (absoluteExpiry < expiresAt) - expiresAt = absoluteExpiry; - } + return issued; + } - var issued = new IssuedSession - { - Session = UAuthSession.Create( - sessionId: newSessionId, - tenantId: context.TenantId, - userKey: context.UserKey, - chainId: SessionChainId.Unassigned, - now: now, - expiresAt: expiresAt, - device: context.Device, - claims: context.Claims, - metadata: context.Metadata - ), - OpaqueSessionId = opaqueSessionId, - IsMetadataOnly = _options.Mode == UAuthMode.SemiHybrid - }; - - await _sessionStore.RotateSessionAsync(context.CurrentSessionId, issued, - new SessionStoreContext - { - TenantId = context.TenantId, - UserKey = context.UserKey, - IssuedAt = now, - Device = context.Device, - }, - ct - ); - - return issued; - } + public async Task RevokeSessionAsync(string? tenantId, AuthSessionId sessionId, DateTimeOffset at, CancellationToken ct = default) + { + var kernel = _kernelFactory.Create(tenantId); + await kernel.ExecuteAsync(_ => kernel.RevokeSessionAsync(sessionId, at), ct); + } - public async Task RevokeSessionAsync(string? tenantId, AuthSessionId sessionId, DateTimeOffset at, CancellationToken ct = default) - { - await _sessionStore.RevokeSessionAsync(tenantId, sessionId, at, ct ); - } + public async Task RevokeChainAsync(string? tenantId, SessionChainId chainId, DateTimeOffset at, CancellationToken ct = default) + { + var kernel = _kernelFactory.Create(tenantId); + await kernel.ExecuteAsync(_ => kernel.RevokeChainAsync(chainId, at), ct); + } - public async Task RevokeChainAsync(string? tenantId, SessionChainId chainId, DateTimeOffset at, CancellationToken ct = default) + public async Task RevokeAllChainsAsync(string? tenantId, UserKey userKey, SessionChainId? exceptChainId, DateTimeOffset at, CancellationToken ct = default) + { + var kernel = _kernelFactory.Create(tenantId); + await kernel.ExecuteAsync(async _ => { - await _sessionStore.RevokeChainAsync(tenantId, chainId, at, ct ); - } + var chains = await kernel.GetChainsByUserAsync(userKey); - public async Task RevokeAllChainsAsync(string? tenantId, UserKey userKey, SessionChainId? exceptChainId, DateTimeOffset at, CancellationToken ct = default) - { - await _sessionStore.RevokeAllChainsAsync(tenantId, userKey, exceptChainId, at, ct ); - } + foreach (var chain in chains) + { + if (exceptChainId.HasValue && chain.ChainId == exceptChainId.Value) + continue; - public async Task RevokeRootAsync(string? tenantId, UserKey userKey, DateTimeOffset at, CancellationToken ct = default) - { - await _sessionStore.RevokeRootAsync(tenantId, userKey, at, ct ); - } + if (!chain.IsRevoked) + await kernel.RevokeChainAsync(chain.ChainId, at); + + var activeSessionId = await kernel.GetActiveSessionIdAsync(chain.ChainId); + if (activeSessionId is not null) + await kernel.RevokeSessionAsync(activeSessionId.Value, at); + } + }, ct); + } + // TODO: Discuss revoking chains/sessions when root is revoked + public async Task RevokeRootAsync(string? tenantId, UserKey userKey, DateTimeOffset at, CancellationToken ct = default) + { + var kernel = _kernelFactory.Create(tenantId); + await kernel.ExecuteAsync(_ => kernel.RevokeSessionRootAsync(userKey, at), ct); } + } 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/DefaultRefreshFlowService.cs b/src/CodeBeam.UltimateAuth.Server/Services/DefaultRefreshFlowService.cs index db6542d..ae818a5 100644 --- a/src/CodeBeam.UltimateAuth.Server/Services/DefaultRefreshFlowService.cs +++ b/src/CodeBeam.UltimateAuth.Server/Services/DefaultRefreshFlowService.cs @@ -10,24 +10,21 @@ namespace CodeBeam.UltimateAuth.Server.Services { internal sealed class DefaultRefreshFlowService : IRefreshFlowService { - private readonly ISessionQueryService _sessionQueries; + private readonly ISessionValidator _sessionValidator; private readonly ISessionTouchService _sessionRefresh; private readonly IRefreshTokenRotationService _tokenRotation; private readonly IRefreshTokenStore _refreshTokenStore; - private readonly IUserIdConverterResolver _userIdConverterResolver; public DefaultRefreshFlowService( - ISessionQueryService sessionQueries, + ISessionValidator sessionValidator, ISessionTouchService sessionRefresh, IRefreshTokenRotationService tokenRotation, - IRefreshTokenStore refreshTokenStore, - IUserIdConverterResolver userIdConverterResolver) + IRefreshTokenStore refreshTokenStore) { - _sessionQueries = sessionQueries; + _sessionValidator = sessionValidator; _sessionRefresh = sessionRefresh; _tokenRotation = tokenRotation; _refreshTokenStore = refreshTokenStore; - _userIdConverterResolver = userIdConverterResolver; } public async Task RefreshAsync(AuthFlowContext flow, RefreshFlowRequest request, CancellationToken ct = default) @@ -55,7 +52,7 @@ private async Task HandleSessionOnlyAsync(AuthFlowContext flo if (request.SessionId is null) return RefreshFlowResult.ReauthRequired(); - var validation = await _sessionQueries.ValidateSessionAsync( + var validation = await _sessionValidator.ValidateSessionAsync( new SessionValidationContext { TenantId = flow.TenantId, @@ -128,7 +125,7 @@ private async Task HandleHybridAsync(AuthFlowContext flow, Re if (request.SessionId is null || string.IsNullOrWhiteSpace(request.RefreshToken)) return RefreshFlowResult.ReauthRequired(); - var validation = await _sessionQueries.ValidateSessionAsync( + var validation = await _sessionValidator.ValidateSessionAsync( new SessionValidationContext { TenantId = flow.TenantId, @@ -179,7 +176,7 @@ private async Task HandleSemiHybridAsync(AuthFlowContext flow if (request.SessionId is null || string.IsNullOrWhiteSpace(request.RefreshToken)) return RefreshFlowResult.ReauthRequired(); - var validation = await _sessionQueries.ValidateSessionAsync( + var validation = await _sessionValidator.ValidateSessionAsync( new SessionValidationContext { TenantId = flow.TenantId, diff --git a/src/CodeBeam.UltimateAuth.Server/Services/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); - } - } -} diff --git a/src/CodeBeam.UltimateAuth.Server/Services/ISessionQueryService.cs b/src/CodeBeam.UltimateAuth.Server/Services/ISessionQueryService.cs index fa67421..6e88570 100644 --- a/src/CodeBeam.UltimateAuth.Server/Services/ISessionQueryService.cs +++ b/src/CodeBeam.UltimateAuth.Server/Services/ISessionQueryService.cs @@ -1,18 +1,32 @@ using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; -namespace CodeBeam.UltimateAuth.Server.Services -{ - public interface ISessionQueryService - { - Task ValidateSessionAsync(SessionValidationContext context, CancellationToken ct = default); +// TenantId parameter only come from AuthFlowContext. +namespace CodeBeam.UltimateAuth.Server.Services; - Task GetSessionAsync(string? tenantId, AuthSessionId sessionId, CancellationToken ct = default); +/// +/// Read-only session query API. +/// Used for validation, UI, monitoring, and diagnostics. +/// +public interface ISessionQueryService +{ + /// + /// Retrieves a specific session by id. + /// + Task GetSessionAsync(AuthSessionId sessionId, CancellationToken ct = default); - Task> GetSessionsByChainAsync(string? tenantId, SessionChainId chainId, CancellationToken ct = default); + /// + /// Retrieves all sessions belonging to a specific chain. + /// + Task> GetSessionsByChainAsync(SessionChainId chainId, CancellationToken ct = default); - Task> GetChainsByUserAsync(string? tenantId, UserKey userKey, CancellationToken ct = default); + /// + /// Retrieves all session chains for a user. + /// + Task> GetChainsByUserAsync(UserKey userKey, CancellationToken ct = default); - Task ResolveChainIdAsync(string? tenantId, AuthSessionId sessionId, CancellationToken ct = default); - } + /// + /// Resolves the chain id for a given session. + /// + Task ResolveChainIdAsync(AuthSessionId sessionId, CancellationToken ct = default); } diff --git a/src/CodeBeam.UltimateAuth.Server/Services/ISessionValidator.cs b/src/CodeBeam.UltimateAuth.Server/Services/ISessionValidator.cs new file mode 100644 index 0000000..665455e --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Services/ISessionValidator.cs @@ -0,0 +1,13 @@ +using CodeBeam.UltimateAuth.Core.Contracts; + +// This is a seperate service because validation runs only once before AuthFlowContext is created. +namespace CodeBeam.UltimateAuth.Server; + +public interface ISessionValidator +{ + /// + /// Validates a session for runtime authentication. + /// Hot path – must be fast and side-effect free. + /// + Task ValidateSessionAsync(SessionValidationContext context, CancellationToken ct = default); +} diff --git a/src/CodeBeam.UltimateAuth.Server/Services/UAuthFlowService.cs b/src/CodeBeam.UltimateAuth.Server/Services/UAuthFlowService.cs index 6864311..7ef7ece 100644 --- a/src/CodeBeam.UltimateAuth.Server/Services/UAuthFlowService.cs +++ b/src/CodeBeam.UltimateAuth.Server/Services/UAuthFlowService.cs @@ -73,7 +73,7 @@ public Task LogoutAsync(LogoutRequest request, CancellationToken ct = default) var now = request.At ?? DateTimeOffset.UtcNow; var authContext = authFlow.ToAuthContext(now); - return _orchestrator.ExecuteAsync(authContext, new RevokeSessionCommand(request.TenantId, request.SessionId), ct); + return _orchestrator.ExecuteAsync(authContext, new RevokeSessionCommand(request.SessionId), ct); } public async Task LogoutAllAsync(LogoutAllRequest request, CancellationToken ct = default) diff --git a/src/CodeBeam.UltimateAuth.Server/Services/UAuthSessionManager.cs b/src/CodeBeam.UltimateAuth.Server/Services/UAuthSessionManager.cs index 3e05168..3ac5e5e 100644 --- a/src/CodeBeam.UltimateAuth.Server/Services/UAuthSessionManager.cs +++ b/src/CodeBeam.UltimateAuth.Server/Services/UAuthSessionManager.cs @@ -1,5 +1,4 @@ using CodeBeam.UltimateAuth.Core.Abstractions; -using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Server.Auth; using CodeBeam.UltimateAuth.Server.Extensions; @@ -7,84 +6,46 @@ using CodeBeam.UltimateAuth.Server.Infrastructure.Orchestrator; // TODO: Add wrapper service in client project. Validate method also may add. -namespace CodeBeam.UltimateAuth.Server.Services -{ - internal sealed class UAuthSessionManager : IUAuthSessionManager - { - private readonly IAuthFlowContextAccessor _authFlow; - private readonly ISessionOrchestrator _orchestrator; - private readonly ISessionQueryService _sessionQueryService; - private readonly IClock _clock; - - public UAuthSessionManager(IAuthFlowContextAccessor authFlow, ISessionOrchestrator orchestrator, ISessionQueryService sessionQueryService, IClock clock) - { - _authFlow = authFlow; - _orchestrator = orchestrator; - _sessionQueryService = sessionQueryService; - _clock = clock; - } - - public Task> GetChainsAsync( - string? tenantId, - UserKey userKey) - => _sessionQueryService.GetChainsByUserAsync(tenantId, userKey); - - public Task> GetSessionsAsync( - string? tenantId, - SessionChainId chainId) - => _sessionQueryService.GetSessionsByChainAsync(tenantId, chainId); - - public Task GetSessionAsync( - string? tenantId, - AuthSessionId sessionId) - => _sessionQueryService.GetSessionAsync(tenantId, sessionId); - - public Task RevokeSessionAsync(string? tenantId, AuthSessionId sessionId, DateTimeOffset at) - { - var authContext = _authFlow.Current.ToAuthContext(_clock.UtcNow); - var command = new RevokeSessionCommand(tenantId, sessionId); - - return _orchestrator.ExecuteAsync(authContext, command); - } - - public Task ResolveChainIdAsync(string? tenantId, AuthSessionId sessionId) - => _sessionQueryService.ResolveChainIdAsync(tenantId, sessionId); - - public Task RevokeAllChainsAsync(string? tenantId, UserKey userKey, SessionChainId? exceptChainId, DateTimeOffset at) - { - var authContext = _authFlow.Current.ToAuthContext(_clock.UtcNow); - var command = new RevokeAllChainsCommand(userKey, exceptChainId); - - return _orchestrator.ExecuteAsync(authContext, command); - } - - public Task RevokeChainAsync(string? tenantId, SessionChainId chainId, DateTimeOffset at) - { - var authContext = _authFlow.Current.ToAuthContext(_clock.UtcNow); - var command = new RevokeChainCommand(chainId); +namespace CodeBeam.UltimateAuth.Server.Services; - return _orchestrator.ExecuteAsync(authContext, command); - } - - public Task RevokeRootAsync(string? tenantId, UserKey userKey, DateTimeOffset at) - { - var authContext = _authFlow.Current.ToAuthContext(_clock.UtcNow); - var command = new RevokeRootCommand(userKey); - - return _orchestrator.ExecuteAsync(authContext, command); - } +internal sealed class UAuthSessionManager : IUAuthSessionManager +{ + private readonly IAuthFlowContextAccessor _authFlow; + private readonly ISessionOrchestrator _orchestrator; + private readonly IClock _clock; - public async Task GetCurrentSessionAsync(string? tenantId, AuthSessionId sessionId) - { - var chainId = await _sessionQueryService.ResolveChainIdAsync(tenantId, sessionId); + public UAuthSessionManager(IAuthFlowContextAccessor authFlow, ISessionOrchestrator orchestrator, IClock clock) + { + _authFlow = authFlow; + _orchestrator = orchestrator; + _clock = clock; + } - if (chainId is null) - return null; + public Task RevokeSessionAsync(AuthSessionId sessionId, CancellationToken ct = default) + { + var authContext = _authFlow.Current.ToAuthContext(_clock.UtcNow); + var command = new RevokeSessionCommand(sessionId); + return _orchestrator.ExecuteAsync(authContext, command, ct); + } - var sessions = await _sessionQueryService.GetSessionsByChainAsync(tenantId, chainId.Value); + public Task RevokeChainAsync(SessionChainId chainId, CancellationToken ct = default) + { + var authContext = _authFlow.Current.ToAuthContext(_clock.UtcNow); + var command = new RevokeChainCommand(chainId); + return _orchestrator.ExecuteAsync(authContext, command, ct); + } - return sessions.FirstOrDefault(s => s.SessionId == sessionId); - } + public Task RevokeAllChainsAsync(UserKey userKey, SessionChainId? exceptChainId, CancellationToken ct = default) + { + var authContext = _authFlow.Current.ToAuthContext(_clock.UtcNow); + var command = new RevokeAllChainsCommand(userKey, exceptChainId); + return _orchestrator.ExecuteAsync(authContext, command, ct); + } + public Task RevokeRootAsync(UserKey userKey, CancellationToken ct = default) + { + var authContext = _authFlow.Current.ToAuthContext(_clock.UtcNow); + var command = new RevokeRootCommand(userKey); + return _orchestrator.ExecuteAsync(authContext, command, ct); } } diff --git a/src/CodeBeam.UltimateAuth.Server/Services/UAuthSessionQueryService.cs b/src/CodeBeam.UltimateAuth.Server/Services/UAuthSessionQueryService.cs index a6736a0..f2edb58 100644 --- a/src/CodeBeam.UltimateAuth.Server/Services/UAuthSessionQueryService.cs +++ b/src/CodeBeam.UltimateAuth.Server/Services/UAuthSessionQueryService.cs @@ -1,82 +1,46 @@ -using CodeBeam.UltimateAuth.Authorization; -using CodeBeam.UltimateAuth.Core; -using CodeBeam.UltimateAuth.Core.Abstractions; -using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Domain; -using CodeBeam.UltimateAuth.Server.Options; -using Microsoft.Extensions.Options; -using System.ComponentModel.DataAnnotations; +using CodeBeam.UltimateAuth.Server.Auth; -namespace CodeBeam.UltimateAuth.Server.Services -{ - public sealed class UAuthSessionQueryService : ISessionQueryService - { - private readonly ISessionStoreKernelFactory _storeFactory; - private readonly IUserClaimsProvider _claimsProvider; - private readonly UAuthServerOptions _options; - - public UAuthSessionQueryService(ISessionStoreKernelFactory storeFactory, IUserClaimsProvider claimsProvider, IOptions options) - { - _storeFactory = storeFactory; - _claimsProvider = claimsProvider; - _options = options.Value; - } - - public async Task ValidateSessionAsync(SessionValidationContext context, CancellationToken ct = default) - { - var kernel = _storeFactory.Create(context.TenantId); - - var session = await kernel.GetSessionAsync(context.SessionId); - if (session is null) - return SessionValidationResult.Invalid(SessionState.NotFound, sessionId: context.SessionId); +namespace CodeBeam.UltimateAuth.Server.Services; - var state = session.GetState(context.Now, _options.Session.IdleTimeout); - if (state != SessionState.Active) - return SessionValidationResult.Invalid(state, sessionId: session.SessionId, chainId: session.ChainId); - - var chain = await kernel.GetChainAsync(session.ChainId); - if (chain is null || chain.IsRevoked) - return SessionValidationResult.Invalid(SessionState.Revoked, session.UserKey, session.SessionId, session.ChainId); - - var root = await kernel.GetSessionRootByUserAsync(session.UserKey); - if (root is null || root.IsRevoked) - return SessionValidationResult.Invalid(SessionState.Revoked, session.UserKey, session.SessionId, session.ChainId, root?.RootId); - - if (session.SecurityVersionAtCreation != root.SecurityVersion) - return SessionValidationResult.Invalid(SessionState.SecurityMismatch, session.UserKey, session.SessionId, session.ChainId, root.RootId); +public sealed class UAuthSessionQueryService : ISessionQueryService +{ + private readonly ISessionStoreKernelFactory _storeFactory; + private readonly IAuthFlowContextAccessor _authFlow; - // TODO: Implement device id, AllowAndRebind behavior and check device mathing in blazor server circuit and external http calls. - // Currently this line has error on refresh flow. - //if (!session.Device.Matches(context.Device) && _options.Session.DeviceMismatchBehavior == DeviceMismatchBehavior.Reject) - // return SessionValidationResult.Invalid(SessionState.DeviceMismatch); + public UAuthSessionQueryService( + ISessionStoreKernelFactory storeFactory, + IAuthFlowContextAccessor authFlow) + { + _storeFactory = storeFactory; + _authFlow = authFlow; + } - var claims = await _claimsProvider.GetClaimsAsync(context.TenantId, session.UserKey, ct); - return SessionValidationResult.Active(context.TenantId, session.UserKey, session.SessionId, session.ChainId, root.RootId, claims, boundDeviceId: session.Device.DeviceId); - } + public Task GetSessionAsync(AuthSessionId sessionId, CancellationToken ct = default) + { + return CreateKernel().GetSessionAsync(sessionId); + } - public Task GetSessionAsync(string? tenantId, AuthSessionId sessionId, CancellationToken ct = default) - { - var kernel = _storeFactory.Create(tenantId); - return kernel.GetSessionAsync(sessionId); - } + public Task> GetSessionsByChainAsync(SessionChainId chainId, CancellationToken ct = default) + { + return CreateKernel().GetSessionsByChainAsync(chainId); + } - public Task> GetSessionsByChainAsync(string? tenantId, SessionChainId chainId, CancellationToken ct = default) - { - var kernel = _storeFactory.Create(tenantId); - return kernel.GetSessionsByChainAsync(chainId); - } + public Task> GetChainsByUserAsync(UserKey userKey, CancellationToken ct = default) + { + return CreateKernel().GetChainsByUserAsync(userKey); + } - public Task> GetChainsByUserAsync(string? tenantId, UserKey userKey, CancellationToken ct = default) - { - var kernel = _storeFactory.Create(tenantId); - return kernel.GetChainsByUserAsync(userKey); - } + public Task ResolveChainIdAsync(AuthSessionId sessionId, CancellationToken ct = default) + { + return CreateKernel().GetChainIdBySessionAsync(sessionId); + } - public Task ResolveChainIdAsync(string? tenantId, AuthSessionId sessionId, CancellationToken ct = default) - { - var kernel = _storeFactory.Create(tenantId); - return kernel.GetChainIdBySessionAsync(sessionId); - } + private ISessionStoreKernel CreateKernel() + { + var tenantId = _authFlow.Current.TenantId; + return _storeFactory.Create(tenantId); } } diff --git a/src/CodeBeam.UltimateAuth.Server/Services/UAuthSessionValidator.cs b/src/CodeBeam.UltimateAuth.Server/Services/UAuthSessionValidator.cs new file mode 100644 index 0000000..9c13402 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Services/UAuthSessionValidator.cs @@ -0,0 +1,58 @@ +using CodeBeam.UltimateAuth.Core; +using CodeBeam.UltimateAuth.Core.Abstractions; +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Server.Options; +using Microsoft.Extensions.Options; + +namespace CodeBeam.UltimateAuth.Server.Services; + +internal sealed class UAuthSessionValidator : ISessionValidator +{ + private readonly ISessionStoreKernelFactory _storeFactory; + private readonly IUserClaimsProvider _claimsProvider; + private readonly UAuthServerOptions _options; + + public UAuthSessionValidator( + ISessionStoreKernelFactory storeFactory, + IUserClaimsProvider claimsProvider, + IOptions options) + { + _storeFactory = storeFactory; + _claimsProvider = claimsProvider; + _options = options.Value; + } + + // Validate runs before AuthFlowContext is set, do not call _authFlow here. + public async Task ValidateSessionAsync(SessionValidationContext context, CancellationToken ct = default) + { + var kernel = _storeFactory.Create(context.TenantId); + var session = await kernel.GetSessionAsync(context.SessionId); + + if (session is null) + return SessionValidationResult.Invalid(SessionState.NotFound, sessionId: context.SessionId); + + var state = session.GetState(context.Now, _options.Session.IdleTimeout); + if (state != SessionState.Active) + return SessionValidationResult.Invalid(state, sessionId: session.SessionId, chainId: session.ChainId); + + var chain = await kernel.GetChainAsync(session.ChainId); + if (chain is null || chain.IsRevoked) + return SessionValidationResult.Invalid(SessionState.Revoked, session.UserKey, session.SessionId, session.ChainId); + + var root = await kernel.GetSessionRootByUserAsync(session.UserKey); + if (root is null || root.IsRevoked) + return SessionValidationResult.Invalid(SessionState.Revoked, session.UserKey, session.SessionId, session.ChainId, root?.RootId); + + if (session.SecurityVersionAtCreation != root.SecurityVersion) + return SessionValidationResult.Invalid(SessionState.SecurityMismatch, session.UserKey, session.SessionId, session.ChainId, root.RootId); + + // TODO: Implement device id, AllowAndRebind behavior and check device mathing in blazor server circuit and external http calls. + // Currently this line has error on refresh flow. + //if (!session.Device.Matches(context.Device) && _options.Session.DeviceMismatchBehavior == DeviceMismatchBehavior.Reject) + // return SessionValidationResult.Invalid(SessionState.DeviceMismatch); + + var claims = await _claimsProvider.GetClaimsAsync(context.TenantId, session.UserKey, ct); + return SessionValidationResult.Active(context.TenantId, session.UserKey, session.SessionId, session.ChainId, root.RootId, claims, boundDeviceId: session.Device.DeviceId); + } +} diff --git a/src/CodeBeam.UltimateAuth.Server/Stores/UAuthSessionStoreFactory.cs b/src/CodeBeam.UltimateAuth.Server/Stores/UAuthSessionStoreFactory.cs deleted file mode 100644 index d1e541d..0000000 --- a/src/CodeBeam.UltimateAuth.Server/Stores/UAuthSessionStoreFactory.cs +++ /dev/null @@ -1,33 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Abstractions; -using Microsoft.Extensions.DependencyInjection; - -namespace CodeBeam.UltimateAuth.Server.Stores -{ - /// - /// UltimateAuth default session store factory. - /// Resolves session store kernels from DI and provides them - /// to framework-level session stores. - /// - public sealed class UAuthSessionStoreFactory : ISessionStoreKernelFactory - { - private readonly IServiceProvider _provider; - - public UAuthSessionStoreFactory(IServiceProvider provider) - { - _provider = provider; - } - - public ISessionStoreKernel Create(string? tenantId) - { - var kernel = _provider.GetService(); - - if (kernel is ITenantAwareSessionStore tenantAware) - { - tenantAware.BindTenant(tenantId); - } - - return kernel; - } - - } -} diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/AuthSessionIdEfConverter.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/AuthSessionIdEfConverter.cs deleted file mode 100644 index 75245e2..0000000 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/AuthSessionIdEfConverter.cs +++ /dev/null @@ -1,38 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Domain; - -namespace CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore -{ - internal static class AuthSessionIdEfConverter - { - public static AuthSessionId FromDatabase(string raw) - { - if (!AuthSessionId.TryCreate(raw, out var id)) - { - throw new InvalidOperationException( - $"Invalid AuthSessionId value in database: '{raw}'"); - } - - return id; - } - - public static string ToDatabase(AuthSessionId id) - => id.Value; - - public static AuthSessionId? FromDatabaseNullable(string? raw) - { - if (raw is null) - return null; - - if (!AuthSessionId.TryCreate(raw, out var id)) - { - throw new InvalidOperationException( - $"Invalid AuthSessionId value in database: '{raw}'"); - } - - return id; - } - - public static string? ToDatabaseNullable(AuthSessionId? id) - => id?.Value; - } -} diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/UAuthSessionDbContext.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Data/UAuthSessionDbContext.cs similarity index 86% rename from src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/UAuthSessionDbContext.cs rename to src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Data/UAuthSessionDbContext.cs index 6e1b3f5..7a8e77f 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/UAuthSessionDbContext.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Data/UAuthSessionDbContext.cs @@ -1,4 +1,5 @@ using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; using Microsoft.EntityFrameworkCore; namespace CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore @@ -9,13 +10,25 @@ internal sealed class UltimateAuthSessionDbContext : DbContext public DbSet Chains => Set(); public DbSet Sessions => Set(); - public UltimateAuthSessionDbContext(DbContextOptions options) : base(options) - { + private readonly TenantContext _tenant; + + public UltimateAuthSessionDbContext(DbContextOptions options, TenantContext tenant) : base(options) + { + _tenant = tenant; } protected override void OnModelCreating(ModelBuilder b) { + b.Entity() + .HasQueryFilter(x => _tenant.IsGlobal || x.TenantId == _tenant.TenantId); + + b.Entity() + .HasQueryFilter(x => _tenant.IsGlobal || x.TenantId == _tenant.TenantId); + + b.Entity() + .HasQueryFilter(x => _tenant.IsGlobal || x.TenantId == _tenant.TenantId); + b.Entity(e => { e.HasKey(x => x.Id); diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/EfCoreSessionStore.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/EfCoreSessionStore.cs deleted file mode 100644 index 59a5292..0000000 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/EfCoreSessionStore.cs +++ /dev/null @@ -1,376 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Abstractions; -using CodeBeam.UltimateAuth.Core.Contracts; -using CodeBeam.UltimateAuth.Core.Domain; -using Microsoft.EntityFrameworkCore; -using System.Security; - -namespace CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore; - -internal sealed class EfCoreSessionStore : ISessionStore -{ - private readonly EfCoreSessionStoreKernel _kernel; - private readonly UltimateAuthSessionDbContext _db; - - public EfCoreSessionStore(EfCoreSessionStoreKernel kernel, UltimateAuthSessionDbContext db) - { - _kernel = kernel; - _db = db; - } - - public async Task GetSessionAsync(string? tenantId, AuthSessionId sessionId, CancellationToken ct = default) - { - var projection = await _db.Sessions - .AsNoTracking() - .SingleOrDefaultAsync( - x => x.SessionId == sessionId && - x.TenantId == tenantId, - ct); - - return projection?.ToDomain(); - } - - public async Task CreateSessionAsync(IssuedSession issued, SessionStoreContext ctx, CancellationToken ct = default) - { - await _kernel.ExecuteAsync(async ct => - { - var now = ctx.IssuedAt; - - var rootProjection = await _db.Roots - .SingleOrDefaultAsync( - x => x.TenantId == ctx.TenantId && - x.UserKey == ctx.UserKey, - ct); - - ISessionRoot root; - - if (rootProjection is null) - { - root = UAuthSessionRoot.Create(ctx.TenantId, ctx.UserKey, now); - _db.Roots.Add(root.ToProjection()); - } - else - { - var chainProjections = await _db.Chains - .AsNoTracking() - .Where(x => x.RootId == rootProjection.RootId) - .ToListAsync(ct); - - root = rootProjection.ToDomain( - chainProjections.Select(c => c.ToDomain()).ToList()); - } - - ISessionChain chain; - - if (ctx.ChainId is not null) - { - var chainProjection = await _db.Chains - .SingleAsync(x => x.ChainId == ctx.ChainId.Value, ct); - - chain = chainProjection.ToDomain(); - } - else - { - chain = UAuthSessionChain.Create( - SessionChainId.New(), - root.RootId, - ctx.TenantId, - ctx.UserKey, - root.SecurityVersion, - ClaimsSnapshot.Empty); - - _db.Chains.Add(chain.ToProjection()); - root = root.AttachChain(chain, now); - } - - var issuedSession = (UAuthSession)issued.Session; - - if (!issuedSession.ChainId.IsUnassigned) - throw new InvalidOperationException("Issued session already has chain."); - - var session = issuedSession.WithChain(chain.ChainId); - - _db.Sessions.Add(session.ToProjection()); - - var updatedChain = chain.AttachSession(session.SessionId); - _db.Chains.Update(updatedChain.ToProjection()); - }, ct); - } - - public async Task RotateSessionAsync(AuthSessionId currentSessionId, IssuedSession issued, SessionStoreContext ctx, CancellationToken ct = default) - { - await _kernel.ExecuteAsync(async ct => - { - var now = ctx.IssuedAt; - - var oldProjection = await _db.Sessions - .SingleOrDefaultAsync( - x => x.SessionId == currentSessionId && - x.TenantId == ctx.TenantId, - ct); - - if (oldProjection is null) - throw new SecurityException("Session not found."); - - var oldSession = oldProjection.ToDomain(); - - if (oldSession.IsRevoked || oldSession.ExpiresAt <= now) - throw new SecurityException("Session is no longer valid."); - - var chainProjection = await _db.Chains - .SingleOrDefaultAsync( - x => x.ChainId == oldSession.ChainId, - ct); - - if (chainProjection is null) - throw new SecurityException("Chain not found."); - - var chain = chainProjection.ToDomain(); - - if (chain.IsRevoked) - throw new SecurityException("Chain is revoked."); - - var newSession = ((UAuthSession)issued.Session) - .WithChain(chain.ChainId); - - _db.Sessions.Add(newSession.ToProjection()); - - var rotatedChain = chain.RotateSession(newSession.SessionId); - _db.Chains.Update(rotatedChain.ToProjection()); - - var revokedOld = oldSession.Revoke(now); - _db.Sessions.Update(revokedOld.ToProjection()); - }, ct); - } - - public async Task TouchSessionAsync(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); - - if (projection is null) - return; - - var session = projection.ToDomain(); - - if (session.IsRevoked) - return; - - if (mode == SessionTouchMode.IfNeeded && at - session.LastSeenAt < TimeSpan.FromMinutes(1)) - return; - - var updated = session.Touch(at); - _db.Sessions.Update(updated.ToProjection()); - - touched = true; - }, ct); - - return touched; - } - - public Task RevokeSessionAsync(string? tenantId, AuthSessionId sessionId, DateTimeOffset at, CancellationToken ct = default) - => _kernel.ExecuteAsync(async ct => - { - var projection = await _db.Sessions.SingleOrDefaultAsync(x => x.SessionId == sessionId && x.TenantId == tenantId, ct); - - if (projection is null) - return; - - var session = projection.ToDomain(); - - if (session.IsRevoked) - return; - - _db.Sessions.Update(session.Revoke(at).ToProjection()); - }, ct); - - public async Task RevokeAllChainsAsync(string? tenantId, UserKey userKey, SessionChainId? exceptChainId, DateTimeOffset at, CancellationToken ct = default) - { - await _kernel.ExecuteAsync(async ct => - { - var chains = await _db.Chains - .Where(x => - x.TenantId == tenantId && - x.UserKey == userKey) - .ToListAsync(ct); - - foreach (var chainProjection in chains) - { - if (exceptChainId.HasValue && - chainProjection.ChainId == exceptChainId.Value) - continue; - - var chain = chainProjection.ToDomain(); - - if (!chain.IsRevoked) - _db.Chains.Update(chain.Revoke(at).ToProjection()); - - if (chain.ActiveSessionId is not null) - { - var sessionProjection = await _db.Sessions.SingleOrDefaultAsync(x => x.SessionId == chain.ActiveSessionId, ct); - - if (sessionProjection is not null) - { - var session = sessionProjection.ToDomain(); - if (!session.IsRevoked) - _db.Sessions.Update(session.Revoke(at).ToProjection()); - } - } - } - }, ct); - } - - public Task RevokeChainAsync(string? tenantId, SessionChainId chainId, DateTimeOffset at, CancellationToken ct = default) - => _kernel.ExecuteAsync(async ct => - { - var projection = await _db.Chains - .SingleOrDefaultAsync( - x => x.ChainId == chainId && - x.TenantId == tenantId, - ct); - - if (projection is null) - return; - - var chain = projection.ToDomain(); - - if (chain.IsRevoked) - return; - - _db.Chains.Update(chain.Revoke(at).ToProjection()); - - if (chain.ActiveSessionId is not null) - { - var sessionProjection = await _db.Sessions - .SingleOrDefaultAsync(x => x.SessionId == chain.ActiveSessionId, ct); - - if (sessionProjection is not null) - { - var session = sessionProjection.ToDomain(); - if (!session.IsRevoked) - _db.Sessions.Update(session.Revoke(at).ToProjection()); - } - } - }, ct); - - public Task RevokeRootAsync(string? tenantId, UserKey userKey, DateTimeOffset at, CancellationToken ct = default) - => _kernel.ExecuteAsync(async ct => - { - var rootProjection = await _db.Roots - .SingleOrDefaultAsync( - x => x.TenantId == tenantId && - x.UserKey == userKey, - ct); - - if (rootProjection is null) - return; - - var chainProjections = await _db.Chains - .Where(x => x.RootId == rootProjection.RootId) - .ToListAsync(ct); - - foreach (var chainProjection in chainProjections) - { - var chain = chainProjection.ToDomain(); - _db.Chains.Update(chain.Revoke(at).ToProjection()); - - if (chain.ActiveSessionId is not null) - { - var sessionProjection = await _db.Sessions - .SingleOrDefaultAsync(x => x.SessionId == chain.ActiveSessionId, ct); - - if (sessionProjection is not null) - { - var session = sessionProjection.ToDomain(); - _db.Sessions.Update(session.Revoke(at).ToProjection()); - } - } - } - - var root = rootProjection.ToDomain(chainProjections.Select(c => c.ToDomain()).ToList()); - - _db.Roots.Update(root.Revoke(at).ToProjection()); - }, ct); - - public async Task> GetSessionsByChainAsync(string? tenantId, SessionChainId chainId, CancellationToken ct = default) - { - var projections = await _db.Sessions - .AsNoTracking() - .Where(x => - x.ChainId == chainId && - x.TenantId == tenantId) - .ToListAsync(ct); - - return projections.Select(x => x.ToDomain()).ToList(); - } - - public async Task> GetChainsByUserAsync(string? tenantId, UserKey userKey, CancellationToken ct = default) - { - var projections = await _db.Chains - .AsNoTracking() - .Where(x => - x.TenantId == tenantId && - x.UserKey.Equals(userKey)) - .ToListAsync(ct); - - return projections.Select(x => x.ToDomain()).ToList(); - } - - public async Task GetChainAsync(string? tenantId, SessionChainId chainId, CancellationToken ct = default) - { - var projection = await _db.Chains - .AsNoTracking() - .SingleOrDefaultAsync( - x => x.ChainId == chainId && - x.TenantId == tenantId, - ct); - - return projection?.ToDomain(); - } - - public async Task GetChainIdBySessionAsync(string? tenantId, AuthSessionId sessionId, CancellationToken ct = default) - { - return await _db.Sessions - .AsNoTracking() - .Where(x => - x.SessionId == sessionId && - x.TenantId == tenantId) - .Select(x => (SessionChainId?)x.ChainId) - .SingleOrDefaultAsync(ct); - } - - public async Task GetActiveSessionIdAsync(string? tenantId, SessionChainId chainId, CancellationToken ct = default) - { - return await _db.Chains - .AsNoTracking() - .Where(x => - x.ChainId == chainId && - x.TenantId == tenantId) - .Select(x => x.ActiveSessionId) - .SingleOrDefaultAsync(ct); - } - - public async Task GetSessionRootAsync(string? tenantId, UserKey userKey, CancellationToken ct = default) - { - var rootProjection = await _db.Roots - .AsNoTracking() - .SingleOrDefaultAsync( - x => x.TenantId == tenantId && - x.UserKey!.Equals(userKey), - ct); - - if (rootProjection is null) - return null; - - var chainProjections = await _db.Chains - .AsNoTracking() - .Where(x => - x.TenantId == tenantId && - x.UserKey!.Equals(userKey)) - .ToListAsync(ct); - - return rootProjection.ToDomain(chainProjections.Select(x => x.ToDomain()).ToList()); - } -} diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/EfCoreSessionStoreKernelFactory.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/EfCoreSessionStoreKernelFactory.cs deleted file mode 100644 index 8677017..0000000 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/EfCoreSessionStoreKernelFactory.cs +++ /dev/null @@ -1,20 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Abstractions; -using Microsoft.Extensions.DependencyInjection; - -namespace CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore -{ - public sealed class EfCoreSessionStoreKernelFactory : ISessionStoreKernelFactory - { - private readonly IServiceProvider _sp; - - public EfCoreSessionStoreKernelFactory(IServiceProvider sp) - { - _sp = sp; - } - - public ISessionStoreKernel Create(string? tenantId) - { - return _sp.GetRequiredService(); - } - } -} diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/ServiceCollectionExtensions.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs similarity index 90% rename from src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/ServiceCollectionExtensions.cs rename to src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs index 2b786a5..7be3e1b 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/ServiceCollectionExtensions.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs @@ -10,7 +10,6 @@ public static IServiceCollection AddUltimateAuthEntityFrameworkCoreSessions(configureDb); services.AddScoped(); - services.AddScoped(); return services; } diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Infrastructure/AuthSessionIdEfConverter.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Infrastructure/AuthSessionIdEfConverter.cs new file mode 100644 index 0000000..7aae5ad --- /dev/null +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Infrastructure/AuthSessionIdEfConverter.cs @@ -0,0 +1,36 @@ +using CodeBeam.UltimateAuth.Core.Domain; + +namespace CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore; + +internal static class AuthSessionIdEfConverter +{ + public static AuthSessionId FromDatabase(string raw) + { + if (!AuthSessionId.TryCreate(raw, out var id)) + { + throw new InvalidOperationException( + $"Invalid AuthSessionId value in database: '{raw}'"); + } + + return id; + } + + public static string ToDatabase(AuthSessionId id) + => id.Value; + + public static AuthSessionId? FromDatabaseNullable(string? raw) + { + if (raw is null) + return null; + + if (!AuthSessionId.TryCreate(raw, out var id)) + { + throw new InvalidOperationException( + $"Invalid AuthSessionId value in database: '{raw}'"); + } + + return id; + } + + public static string? ToDatabaseNullable(AuthSessionId? id) => id?.Value; +} diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/JsonValueConverter.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Infrastructure/JsonValueConverter.cs similarity index 100% rename from src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/JsonValueConverter.cs rename to src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Infrastructure/JsonValueConverter.cs diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/NullableAuthSessionIdConverter.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Infrastructure/NullableAuthSessionIdConverter.cs similarity index 100% rename from src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/NullableAuthSessionIdConverter.cs rename to src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Infrastructure/NullableAuthSessionIdConverter.cs diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionChainProjectionMapper.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionChainProjectionMapper.cs index b1b1c32..f93d05a 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionChainProjectionMapper.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionChainProjectionMapper.cs @@ -4,7 +4,7 @@ namespace CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore { internal static class SessionChainProjectionMapper { - public static ISessionChain ToDomain(this SessionChainProjection p) + public static UAuthSessionChain ToDomain(this SessionChainProjection p) { return UAuthSessionChain.FromProjection( p.ChainId, @@ -20,7 +20,7 @@ public static ISessionChain ToDomain(this SessionChainProjection p) ); } - public static SessionChainProjection ToProjection(this ISessionChain chain) + public static SessionChainProjection ToProjection(this UAuthSessionChain chain) { return new SessionChainProjection { diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionProjectionMapper.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionProjectionMapper.cs index ed2a371..692aea1 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionProjectionMapper.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionProjectionMapper.cs @@ -4,7 +4,7 @@ namespace CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore { internal static class SessionProjectionMapper { - public static ISession ToDomain(this SessionProjection p) + public static UAuthSession ToDomain(this SessionProjection p) { return UAuthSession.FromProjection( p.SessionId, @@ -23,7 +23,7 @@ public static ISession ToDomain(this SessionProjection p) ); } - public static SessionProjection ToProjection(this ISession s) + public static SessionProjection ToProjection(this UAuthSession s) { return new SessionProjection { diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionRootProjectionMapper.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionRootProjectionMapper.cs index e8f0f95..a28fde4 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionRootProjectionMapper.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Mappers/SessionRootProjectionMapper.cs @@ -4,7 +4,7 @@ namespace CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore { internal static class SessionRootProjectionMapper { - public static ISessionRoot ToDomain(this SessionRootProjection root, IReadOnlyList? chains = null) + public static UAuthSessionRoot ToDomain(this SessionRootProjection root, IReadOnlyList? chains = null) { return UAuthSessionRoot.FromProjection( root.RootId, @@ -13,12 +13,12 @@ public static ISessionRoot ToDomain(this SessionRootProjection root, IReadOnlyLi root.IsRevoked, root.RevokedAt, root.SecurityVersion, - chains ?? Array.Empty(), + chains ?? Array.Empty(), root.LastUpdatedAt ); } - public static SessionRootProjection ToProjection(this ISessionRoot root) + public static SessionRootProjection ToProjection(this UAuthSessionRoot root) { return new SessionRootProjection { diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/EfCoreSessionStoreKernel.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Stores/EfCoreSessionStoreKernel.cs similarity index 88% rename from src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/EfCoreSessionStoreKernel.cs rename to src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Stores/EfCoreSessionStoreKernel.cs index 05b5121..ed07e8b 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/EfCoreSessionStoreKernel.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Stores/EfCoreSessionStoreKernel.cs @@ -1,5 +1,6 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; using Microsoft.EntityFrameworkCore; using System.Data; @@ -8,10 +9,12 @@ namespace CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore internal sealed class EfCoreSessionStoreKernel : ISessionStoreKernel { private readonly UltimateAuthSessionDbContext _db; + private readonly TenantContext _tenant; - public EfCoreSessionStoreKernel(UltimateAuthSessionDbContext db) + public EfCoreSessionStoreKernel(UltimateAuthSessionDbContext db, TenantContext tenant) { _db = db; + _tenant = tenant; } public async Task ExecuteAsync(Func action, CancellationToken ct = default) @@ -45,7 +48,7 @@ await strategy.ExecuteAsync(async () => }); } - public async Task GetSessionAsync(AuthSessionId sessionId) + public async Task GetSessionAsync(AuthSessionId sessionId) { var projection = await _db.Sessions .AsNoTracking() @@ -54,7 +57,7 @@ await strategy.ExecuteAsync(async () => return projection?.ToDomain(); } - public async Task SaveSessionAsync(ISession session) + public async Task SaveSessionAsync(UAuthSession session) { var projection = session.ToProjection(); @@ -83,7 +86,7 @@ public async Task RevokeSessionAsync(AuthSessionId sessionId, DateTimeOffset at) _db.Sessions.Update(revoked.ToProjection()); } - public async Task GetChainAsync(SessionChainId chainId) + public async Task GetChainAsync(SessionChainId chainId) { var projection = await _db.Chains .AsNoTracking() @@ -92,7 +95,7 @@ public async Task RevokeSessionAsync(AuthSessionId sessionId, DateTimeOffset at) return projection?.ToDomain(); } - public async Task SaveChainAsync(ISessionChain chain) + public async Task SaveChainAsync(UAuthSessionChain chain) { var projection = chain.ToProjection(); @@ -141,7 +144,7 @@ public async Task SetActiveSessionIdAsync(SessionChainId chainId, AuthSessionId _db.Chains.Update(projection); } - public async Task GetSessionRootByUserAsync(UserKey userKey) + public async Task GetSessionRootByUserAsync(UserKey userKey) { var rootProjection = await _db.Roots .AsNoTracking() @@ -158,7 +161,7 @@ public async Task SetActiveSessionIdAsync(SessionChainId chainId, AuthSessionId return rootProjection.ToDomain(chains.Select(c => c.ToDomain()).ToList()); } - public async Task SaveSessionRootAsync(ISessionRoot root) + public async Task SaveSessionRootAsync(UAuthSessionRoot root) { var projection = root.ToProjection(); @@ -191,7 +194,7 @@ public async Task RevokeSessionRootAsync(UserKey userKey, DateTimeOffset at) .SingleOrDefaultAsync(); } - public async Task> GetChainsByUserAsync(UserKey userKey) + public async Task> GetChainsByUserAsync(UserKey userKey) { var projections = await _db.Chains .AsNoTracking() @@ -201,7 +204,7 @@ public async Task> GetChainsByUserAsync(UserKey use return projections.Select(x => x.ToDomain()).ToList(); } - public async Task> GetSessionsByChainAsync(SessionChainId chainId) + public async Task> GetSessionsByChainAsync(SessionChainId chainId) { var projections = await _db.Sessions .AsNoTracking() @@ -211,7 +214,7 @@ public async Task> GetSessionsByChainAsync(SessionChainI return projections.Select(x => x.ToDomain()).ToList(); } - public async Task GetSessionRootByIdAsync(SessionRootId rootId) + public async Task GetSessionRootByIdAsync(SessionRootId rootId) { var rootProjection = await _db.Roots .AsNoTracking() diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Stores/EfCoreSessionStoreKernelFactory.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Stores/EfCoreSessionStoreKernelFactory.cs new file mode 100644 index 0000000..96c01c3 --- /dev/null +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Stores/EfCoreSessionStoreKernelFactory.cs @@ -0,0 +1,25 @@ +using CodeBeam.UltimateAuth.Core.Abstractions; +using CodeBeam.UltimateAuth.Core.MultiTenancy; +using Microsoft.Extensions.DependencyInjection; + +namespace CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore; + +public sealed class EfCoreSessionStoreKernelFactory : ISessionStoreKernelFactory +{ + private readonly IServiceProvider _sp; + + public EfCoreSessionStoreKernelFactory(IServiceProvider sp) + { + _sp = sp; + } + + public ISessionStoreKernel Create(string? tenantId) + { + return ActivatorUtilities.CreateInstance(_sp, new TenantContext(tenantId)); + } + + public ISessionStoreKernel CreateGlobal() + { + return ActivatorUtilities.CreateInstance(_sp, new TenantContext(null, isGlobal: true)); + } +} diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/InMemorySessionStore.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/InMemorySessionStore.cs deleted file mode 100644 index ed5f958..0000000 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/InMemorySessionStore.cs +++ /dev/null @@ -1,154 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Abstractions; -using CodeBeam.UltimateAuth.Core.Contracts; -using CodeBeam.UltimateAuth.Core.Domain; -using CodeBeam.UltimateAuth.Server.Options; -using Microsoft.Extensions.Options; -using System.Security; - -public sealed class InMemorySessionStore : ISessionStore -{ - private readonly ISessionStoreKernelFactory _factory; - private readonly UAuthServerOptions _options; - - public InMemorySessionStore(ISessionStoreKernelFactory factory, IOptions options) - { - _factory = factory; - _options = options.Value; - } - - private ISessionStoreKernel Kernel(string? tenantId) - => _factory.Create(tenantId); - - public Task GetSessionAsync(string? tenantId, AuthSessionId sessionId, CancellationToken ct = default) - => Kernel(tenantId).GetSessionAsync(sessionId); - - public async Task CreateSessionAsync(IssuedSession issued, SessionStoreContext ctx, CancellationToken ct = default) - { - var k = Kernel(ctx.TenantId); - - await k.ExecuteAsync(async (ct) => - { - var now = ctx.IssuedAt; - - var root = await k.GetSessionRootByUserAsync(ctx.UserKey) ?? UAuthSessionRoot.Create(ctx.TenantId, ctx.UserKey, now); - ISessionChain chain; - - if (ctx.ChainId is not null) - { - chain = await k.GetChainAsync(ctx.ChainId.Value) ?? throw new InvalidOperationException("Chain not found."); - } - else - { - chain = UAuthSessionChain.Create( - SessionChainId.New(), - root.RootId, - ctx.TenantId, - ctx.UserKey, - root.SecurityVersion, - ClaimsSnapshot.Empty); - - root = root.AttachChain(chain, now); - } - - var session = issued.Session; - - if (!session.ChainId.IsUnassigned) - { - throw new InvalidOperationException("Issued session already has a chain assigned."); - } - - session = session.WithChain(chain.ChainId); - - // Persist (order intentional) - await k.SaveSessionRootAsync(root); - await k.SaveChainAsync(chain); - await k.SaveSessionAsync(session); - await k.SetActiveSessionIdAsync(chain.ChainId, session.SessionId); - }, ct); - } - - public async Task RotateSessionAsync(AuthSessionId currentSessionId, IssuedSession issued, SessionStoreContext ctx, CancellationToken ct = default) - { - var k = Kernel(ctx.TenantId); - - await k.ExecuteAsync(async (ct) => - { - var now = ctx.IssuedAt; - - var old = await k.GetSessionAsync(currentSessionId) - ?? throw new SecurityException("Session not found."); - - if (old.IsRevoked || old.ExpiresAt <= now) - throw new SecurityException("Session is no longer valid."); - - var chain = await k.GetChainAsync(old.ChainId) - ?? throw new SecurityException("Chain not found."); - - if (chain.IsRevoked) - throw new SecurityException("Chain is revoked."); - - var newSession = ((UAuthSession)issued.Session).WithChain(chain.ChainId); - - await k.SaveSessionAsync(newSession); - await k.SetActiveSessionIdAsync(chain.ChainId, newSession.SessionId); - await k.RevokeSessionAsync(old.SessionId, now); - }, ct); - } - - public async Task TouchSessionAsync(AuthSessionId sessionId, DateTimeOffset at, SessionTouchMode mode = SessionTouchMode.IfNeeded, CancellationToken ct = default) - { - var k = Kernel(null); - bool touched = false; - - await k.ExecuteAsync(async (ct) => - { - var session = await k.GetSessionAsync(sessionId); - if (session is null || session.IsRevoked) - return; - - if (mode == SessionTouchMode.IfNeeded) - { - var elapsed = at - session.LastSeenAt; - if (elapsed < _options.Session.TouchInterval) - return; - } - - var updated = session.Touch(at); - await k.SaveSessionAsync(updated); - - touched = true; - }, ct); - - return touched; - } - - public Task RevokeSessionAsync(string? tenantId, AuthSessionId sessionId, DateTimeOffset at, CancellationToken ct = default) - => Kernel(tenantId).RevokeSessionAsync(sessionId, at); - - public async Task RevokeAllChainsAsync(string? tenantId, UserKey userKey, SessionChainId? exceptChainId, DateTimeOffset at, CancellationToken ct = default) - { - var k = Kernel(tenantId); - - await k.ExecuteAsync(async (ct) => - { - var chains = await k.GetChainsByUserAsync(userKey); - - foreach (var chain in chains) - { - if (exceptChainId.HasValue && chain.ChainId == exceptChainId.Value) - continue; - - await k.RevokeChainAsync(chain.ChainId, at); - - if (chain.ActiveSessionId is not null) - await k.RevokeSessionAsync(chain.ActiveSessionId.Value, at); - } - }, ct); - } - - public Task RevokeChainAsync(string? tenantId, SessionChainId chainId, DateTimeOffset at, CancellationToken ct = default) - => Kernel(tenantId).RevokeChainAsync(chainId, at); - - public Task RevokeRootAsync(string? tenantId, UserKey userKey, DateTimeOffset at, CancellationToken ct = default) - => Kernel(tenantId).RevokeSessionRootAsync(userKey, at); -} diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/InMemorySessionStoreFactory.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/InMemorySessionStoreFactory.cs deleted file mode 100644 index 157bfd8..0000000 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/InMemorySessionStoreFactory.cs +++ /dev/null @@ -1,24 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Abstractions; -using System.Collections.Concurrent; - -namespace CodeBeam.UltimateAuth.Sessions.InMemory -{ - public sealed class InMemorySessionStoreFactory : ISessionStoreKernelFactory - { - private readonly ConcurrentDictionary _stores = new(); - - public ISessionStoreKernel Create(string? tenantId) - { - var key = tenantId ?? "__single__"; - - var store = _stores.GetOrAdd(key, _ => - { - var k = new InMemorySessionStoreKernel(); - k.BindTenant(tenantId); - return k; - }); - - return (ISessionStoreKernel)store; - } - } -} diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/InMemorySessionStoreKernel.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/InMemorySessionStoreKernel.cs index 3aaeee4..9a2dc19 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/InMemorySessionStoreKernel.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/InMemorySessionStoreKernel.cs @@ -2,22 +2,15 @@ using CodeBeam.UltimateAuth.Core.Domain; using System.Collections.Concurrent; -internal sealed class InMemorySessionStoreKernel : ISessionStoreKernel, ITenantAwareSessionStore +internal sealed class InMemorySessionStoreKernel : ISessionStoreKernel { private readonly SemaphoreSlim _tx = new(1, 1); - private readonly ConcurrentDictionary _sessions = new(); - private readonly ConcurrentDictionary _chains = new(); - private readonly ConcurrentDictionary _roots = new(); + private readonly ConcurrentDictionary _sessions = new(); + private readonly ConcurrentDictionary _chains = new(); + private readonly ConcurrentDictionary _roots = new(); private readonly ConcurrentDictionary _activeSessions = new(); - public string? TenantId { get; private set; } - - public void BindTenant(string? tenantId) - { - TenantId = tenantId ?? "__single__"; - } - public async Task ExecuteAsync(Func action, CancellationToken ct = default) { await _tx.WaitAsync(ct); @@ -31,10 +24,10 @@ public async Task ExecuteAsync(Func action, Cancellatio } } - public Task GetSessionAsync(AuthSessionId sessionId) + public Task GetSessionAsync(AuthSessionId sessionId) => Task.FromResult(_sessions.TryGetValue(sessionId, out var s) ? s : null); - public Task SaveSessionAsync(ISession session) + public Task SaveSessionAsync(UAuthSession session) { _sessions[session.SessionId] = session; return Task.CompletedTask; @@ -49,10 +42,10 @@ public Task RevokeSessionAsync(AuthSessionId sessionId, DateTimeOffset at) return Task.CompletedTask; } - public Task GetChainAsync(SessionChainId chainId) + public Task GetChainAsync(SessionChainId chainId) => Task.FromResult(_chains.TryGetValue(chainId, out var c) ? c : null); - public Task SaveChainAsync(ISessionChain chain) + public Task SaveChainAsync(UAuthSessionChain chain) { _chains[chain.ChainId] = chain; return Task.CompletedTask; @@ -76,13 +69,13 @@ public Task SetActiveSessionIdAsync(SessionChainId chainId, AuthSessionId sessio return Task.CompletedTask; } - public Task GetSessionRootByUserAsync(UserKey userKey) + public Task GetSessionRootByUserAsync(UserKey userKey) => Task.FromResult(_roots.TryGetValue(userKey, out var r) ? r : null); - public Task GetSessionRootByIdAsync(SessionRootId rootId) + public Task GetSessionRootByIdAsync(SessionRootId rootId) => Task.FromResult(_roots.Values.FirstOrDefault(r => r.RootId == rootId)); - public Task SaveSessionRootAsync(ISessionRoot root) + public Task SaveSessionRootAsync(UAuthSessionRoot root) { _roots[root.UserKey] = root; return Task.CompletedTask; @@ -105,21 +98,21 @@ public Task RevokeSessionRootAsync(UserKey userKey, DateTimeOffset at) return Task.FromResult(null); } - public Task> GetChainsByUserAsync(UserKey userKey) + public Task> GetChainsByUserAsync(UserKey userKey) { if (!_roots.TryGetValue(userKey, out var root)) - return Task.FromResult>(Array.Empty()); + return Task.FromResult>(Array.Empty()); - return Task.FromResult>(root.Chains.ToList()); + return Task.FromResult>(root.Chains.ToList()); } - public Task> GetSessionsByChainAsync(SessionChainId chainId) + public Task> GetSessionsByChainAsync(SessionChainId chainId) { var result = _sessions.Values .Where(s => s.ChainId == chainId) .ToList(); - return Task.FromResult>(result); + return Task.FromResult>(result); } public Task DeleteExpiredSessionsAsync(DateTimeOffset at) @@ -130,7 +123,13 @@ public Task DeleteExpiredSessionsAsync(DateTimeOffset at) if (session.ExpiresAt <= at) { - _sessions[kvp.Key] = session.Revoke(at); + var revoked = session.Revoke(at); + _sessions[kvp.Key] = revoked; + + if (_activeSessions.TryGetValue(revoked.ChainId, out var activeId) && activeId == revoked.SessionId) + { + _activeSessions.TryRemove(revoked.ChainId, out _); + } } } diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/InMemorySessionStoreKernelFactory.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/InMemorySessionStoreKernelFactory.cs new file mode 100644 index 0000000..7682086 --- /dev/null +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/InMemorySessionStoreKernelFactory.cs @@ -0,0 +1,17 @@ +using CodeBeam.UltimateAuth.Core.Abstractions; +using System.Collections.Concurrent; + +namespace CodeBeam.UltimateAuth.Sessions.InMemory; + +public sealed class InMemorySessionStoreKernelFactory : ISessionStoreKernelFactory +{ + private readonly ConcurrentDictionary _kernels = new(); + + public ISessionStoreKernel Create(string? tenantId) + { + //var key = TenantKey.Normalize(tenantId); + var key = tenantId ?? "__default__"; + + return _kernels.GetOrAdd(key, _ => new InMemorySessionStoreKernel()); + } +} diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/ServiceCollectionExtensions.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/ServiceCollectionExtensions.cs index c12a815..aebffb2 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/ServiceCollectionExtensions.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/ServiceCollectionExtensions.cs @@ -1,16 +1,13 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using Microsoft.Extensions.DependencyInjection; -namespace CodeBeam.UltimateAuth.Sessions.InMemory +namespace CodeBeam.UltimateAuth.Sessions.InMemory; + +public static class ServiceCollectionExtensions { - public static class ServiceCollectionExtensions + public static IServiceCollection AddUltimateAuthInMemorySessions(this IServiceCollection services) { - public static IServiceCollection AddUltimateAuthInMemorySessions(this IServiceCollection services) - { - services.AddSingleton(); - // TODO: Discuss it to be singleton or scoped - services.AddScoped(); - return services; - } + services.AddSingleton(); + return services; } } 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]