diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Issuers/IJwtTokenGenerator.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Issuers/IJwtTokenGenerator.cs
new file mode 100644
index 0000000..0fe7422
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Issuers/IJwtTokenGenerator.cs
@@ -0,0 +1,13 @@
+using CodeBeam.UltimateAuth.Core.Domain;
+
+namespace CodeBeam.UltimateAuth.Core.Abstractions
+{
+ ///
+ /// Low-level JWT creation abstraction.
+ /// Can be replaced for asymmetric keys, external KMS, etc.
+ ///
+ public interface IJwtTokenGenerator
+ {
+ string CreateToken(UAuthJwtTokenDescriptor descriptor);
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Issuers/IOpaqueTokenGenerator.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Issuers/IOpaqueTokenGenerator.cs
new file mode 100644
index 0000000..0c49dcf
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Issuers/IOpaqueTokenGenerator.cs
@@ -0,0 +1,11 @@
+namespace CodeBeam.UltimateAuth.Core.Abstractions
+{
+ ///
+ /// Generates cryptographically secure random tokens
+ /// for opaque identifiers, refresh tokens, session ids.
+ ///
+ public interface IOpaqueTokenGenerator
+ {
+ 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
new file mode 100644
index 0000000..f19db3e
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Issuers/ISessionIssuer.cs
@@ -0,0 +1,13 @@
+using CodeBeam.UltimateAuth.Core.Contexts;
+using CodeBeam.UltimateAuth.Core.Domain;
+
+namespace CodeBeam.UltimateAuth.Core.Abstractions
+{
+ ///
+ /// Issues and manages authentication sessions.
+ ///
+ public interface ISessionIssuer
+ {
+ Task> IssueAsync(SessionIssueContext context, UAuthSessionChain chain, CancellationToken cancellationToken = default);
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Issuers/ITokenHasher.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Issuers/ITokenHasher.cs
new file mode 100644
index 0000000..ebf4499
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Issuers/ITokenHasher.cs
@@ -0,0 +1,12 @@
+namespace CodeBeam.UltimateAuth.Core.Abstractions
+{
+ ///
+ /// 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);
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Issuers/ITokenIssuer.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Issuers/ITokenIssuer.cs
new file mode 100644
index 0000000..948bf3b
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Issuers/ITokenIssuer.cs
@@ -0,0 +1,14 @@
+using CodeBeam.UltimateAuth.Core.Contexts;
+
+namespace CodeBeam.UltimateAuth.Core.Abstractions
+{
+ ///
+ /// Issues access and refresh tokens according to the active auth mode.
+ /// Does not perform persistence or validation.
+ ///
+ public interface ITokenIssuer
+ {
+ Task IssueAccessTokenAsync(TokenIssueContext context, CancellationToken cancellationToken = default);
+ Task IssueRefreshTokenAsync(TokenIssueContext context, CancellationToken cancellationToken = default);
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/DefaultSessionStoreFactory.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/DefaultSessionStoreFactory.cs
index 8ff089d..543ec2f 100644
--- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/DefaultSessionStoreFactory.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/DefaultSessionStoreFactory.cs
@@ -10,11 +10,11 @@ public sealed class DefaultSessionStoreFactory : ISessionStoreFactory
/// The type used to uniquely identify the user.
/// Never returns; always throws.
/// Thrown when no session store implementation has been configured.
- public ISessionStore Create(string? tenantId)
+ public ISessionStoreKernel Create(string? tenantId)
{
throw new InvalidOperationException(
- "No ISessionStore implementation registered. " +
- "Call AddUltimateAuthServer().AddSessionStore() to provide a real implementation."
+ "No session store has been configured." +
+ "Call AddUltimateAuthServer().AddSessionStore(...) to register one."
);
}
}
diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ISessionStore.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ISessionStore.cs
index 3729d71..e25ba9c 100644
--- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ISessionStore.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ISessionStore.cs
@@ -1,140 +1,58 @@
-using CodeBeam.UltimateAuth.Core.Domain;
+using CodeBeam.UltimateAuth.Core.Contexts;
+using CodeBeam.UltimateAuth.Core.Domain;
namespace CodeBeam.UltimateAuth.Core.Abstractions
{
///
- /// Defines the low-level persistence operations for sessions, session chains, and session roots in a multi-tenant or single-tenant environment.
- /// Store implementations provide durable and atomic data access.
+ /// High-level session store abstraction used by UltimateAuth.
+ /// Encapsulates session, chain, and root orchestration.
///
public interface ISessionStore
{
///
- /// Retrieves a session by its identifier within the given tenant context.
+ /// Retrieves an active session by id.
///
- /// The tenant identifier, or null for single-tenant mode.
- /// The session identifier.
- /// The session instance or null if not found.
- Task?> GetSessionAsync(string? tenantId, AuthSessionId sessionId);
+ Task?> GetSessionAsync(
+ string? tenantId,
+ AuthSessionId sessionId);
///
- /// Persists a new session or updates an existing one within the tenant scope.
- /// Implementations must ensure atomic writes.
+ /// Creates a new session and associates it with the appropriate chain and root.
///
- /// The tenant identifier, or null.
- /// The session to persist.
- Task SaveSessionAsync(string? tenantId, ISession session);
+ Task CreateSessionAsync(
+ IssuedSession issuedSession,
+ SessionStoreContext context);
///
- /// Marks the specified session as revoked, preventing future authentication.
- /// Revocation timestamp must be stored reliably.
+ /// Refreshes (rotates) the active session within its chain.
///
- /// The tenant identifier, or null.
- /// The session identifier.
- /// The UTC timestamp of revocation.
- Task RevokeSessionAsync(string? tenantId, AuthSessionId sessionId, DateTime at);
-
- ///
- /// Returns all sessions belonging to the specified chain, ordered according to store implementation rules.
- ///
- /// The tenant identifier, or null.
- /// The chain identifier.
- /// A read-only list of sessions.
- Task>> GetSessionsByChainAsync(string? tenantId, ChainId chainId);
-
- ///
- /// Retrieves a session chain by identifier. Returns null if the chain does not exist in the provided tenant context.
- ///
- /// The tenant identifier, or null.
- /// The chain identifier.
- /// The chain or null.
- Task?> GetChainAsync(string? tenantId, ChainId chainId);
-
- ///
- /// Inserts a new session chain into the store. Implementations must ensure consistency with the related sessions and session root.
- ///
- /// The tenant identifier, or null.
- /// The chain to save.
- Task SaveChainAsync(string? tenantId, ISessionChain chain);
-
- ///
- /// Updates an existing session chain, typically after session rotation or revocation. Implementations must preserve atomicity.
- ///
- /// The tenant identifier, or null.
- /// The updated session chain.
- Task UpdateChainAsync(string? tenantId, ISessionChain chain);
-
- ///
- /// Marks the entire session chain as revoked, invalidating all associated sessions for the device or app family.
- ///
- /// The tenant identifier, or null.
- /// The chain to revoke.
- /// The UTC timestamp of revocation.
- Task RevokeChainAsync(string? tenantId, ChainId chainId, DateTime at);
-
- ///
- /// Retrieves the active session identifier for the specified chain.
- /// This is typically an O(1) lookup and used for session rotation.
- ///
- /// The tenant identifier, or null.
- /// The chain whose active session is requested.
- /// The active session identifier or null.
- Task GetActiveSessionIdAsync(string? tenantId, ChainId chainId);
-
- ///
- /// Sets or replaces the active session identifier for the specified chain.
- /// Must be atomic to prevent race conditions during refresh.
- ///
- /// The tenant identifier, or null.
- /// The chain whose active session is being set.
- /// The new active session identifier.
- Task SetActiveSessionIdAsync(string? tenantId, ChainId chainId, AuthSessionId sessionId);
-
- ///
- /// Retrieves all session chains belonging to the specified user within the tenant scope.
- ///
- /// The tenant identifier, or null.
- /// The user whose chains are being retrieved.
- /// A read-only list of session chains.
- Task>> GetChainsByUserAsync(string? tenantId, TUserId userId);
+ Task RotateSessionAsync(
+ AuthSessionId currentSessionId,
+ IssuedSession newSession,
+ SessionStoreContext context);
///
- /// Retrieves the session root for the user, which represents the full set of chains and their associated security metadata.
- /// Returns null if the root does not exist.
+ /// Revokes a single session.
///
- /// The tenant identifier, or null.
- /// The user identifier.
- /// The session root or null.
- Task?> GetSessionRootAsync(string? tenantId, TUserId userId);
+ Task RevokeSessionAsync(
+ string? tenantId,
+ AuthSessionId sessionId,
+ DateTime at);
///
- /// Persists a session root structure, usually after chain creation, rotation, or security operations.
+ /// Revokes all sessions for a specific user (all devices).
///
- /// The tenant identifier, or null.
- /// The session root to save.
- Task SaveSessionRootAsync(string? tenantId, ISessionRoot root);
+ Task RevokeAllSessionsAsync(
+ string? tenantId,
+ TUserId userId,
+ DateTime at);
///
- /// Revokes the session root, invalidating all chains and sessions belonging to the specified user in the tenant scope.
+ /// Revokes all sessions within a specific chain (single device).
///
- /// The tenant identifier, or null.
- /// The user whose root should be revoked.
- /// The UTC timestamp of revocation.
- Task RevokeSessionRootAsync(string? tenantId, TUserId userId, DateTime at);
-
- ///
- /// Removes expired sessions from the store while leaving chains and session roots intact. Cleanup strategy is determined by the store implementation.
- ///
- /// The tenant identifier, or null.
- /// The current UTC timestamp.
- Task DeleteExpiredSessionsAsync(string? tenantId, DateTime now);
-
- ///
- /// Retrieves the chain identifier associated with the specified session.
- ///
- /// The tenant identifier, or null.
- /// The session identifier.
- /// The chain identifier or null.
- Task GetChainIdBySessionAsync(string? tenantId, AuthSessionId sessionId);
+ Task RevokeChainAsync(
+ string? tenantId,
+ ChainId chainId,
+ DateTime at);
}
-
}
diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ISessionStoreFactory.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ISessionStoreFactory.cs
index 0a18fe1..a49165d 100644
--- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ISessionStoreFactory.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ISessionStoreFactory.cs
@@ -3,7 +3,7 @@
///
/// 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.
+ /// Implementations typically resolve concrete types from the dependency injection container.
///
public interface ISessionStoreFactory
{
@@ -15,11 +15,11 @@ public interface ISessionStoreFactory
/// The tenant identifier for multi-tenant environments, or null for single-tenant mode.
///
///
- /// An implementation able to perform session persistence operations.
+ /// An implementation able to perform session persistence operations.
///
///
/// Thrown if no compatible session store implementation is registered.
///
- ISessionStore Create(string? tenantId);
+ ISessionStoreKernel Create(string? tenantId);
}
}
diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ISessionStoreKernel.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ISessionStoreKernel.cs
new file mode 100644
index 0000000..9d8763b
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ISessionStoreKernel.cs
@@ -0,0 +1,140 @@
+using CodeBeam.UltimateAuth.Core.Domain;
+
+namespace CodeBeam.UltimateAuth.Core.Abstractions
+{
+ ///
+ /// Defines the low-level persistence operations for sessions, session chains, and session roots in a multi-tenant or single-tenant environment.
+ /// Store implementations provide durable and atomic data access.
+ ///
+ public interface ISessionStoreKernel
+ {
+ ///
+ /// Retrieves a session by its identifier within the given tenant context.
+ ///
+ /// The tenant identifier, or null for single-tenant mode.
+ /// The session identifier.
+ /// The session instance or null if not found.
+ Task?> GetSessionAsync(string? tenantId, AuthSessionId sessionId);
+
+ ///
+ /// Persists a new session or updates an existing one within the tenant scope.
+ /// Implementations must ensure atomic writes.
+ ///
+ /// The tenant identifier, or null.
+ /// The session to persist.
+ Task SaveSessionAsync(string? tenantId, ISession session);
+
+ ///
+ /// Marks the specified session as revoked, preventing future authentication.
+ /// Revocation timestamp must be stored reliably.
+ ///
+ /// The tenant identifier, or null.
+ /// The session identifier.
+ /// The UTC timestamp of revocation.
+ Task RevokeSessionAsync(string? tenantId, AuthSessionId sessionId, DateTime at);
+
+ ///
+ /// Returns all sessions belonging to the specified chain, ordered according to store implementation rules.
+ ///
+ /// The tenant identifier, or null.
+ /// The chain identifier.
+ /// A read-only list of sessions.
+ Task>> GetSessionsByChainAsync(string? tenantId, ChainId chainId);
+
+ ///
+ /// Retrieves a session chain by identifier. Returns null if the chain does not exist in the provided tenant context.
+ ///
+ /// The tenant identifier, or null.
+ /// The chain identifier.
+ /// The chain or null.
+ Task?> GetChainAsync(string? tenantId, ChainId chainId);
+
+ ///
+ /// Inserts a new session chain into the store. Implementations must ensure consistency with the related sessions and session root.
+ ///
+ /// The tenant identifier, or null.
+ /// The chain to save.
+ Task SaveChainAsync(string? tenantId, ISessionChain chain);
+
+ ///
+ /// Updates an existing session chain, typically after session rotation or revocation. Implementations must preserve atomicity.
+ ///
+ /// The tenant identifier, or null.
+ /// The updated session chain.
+ Task UpdateChainAsync(string? tenantId, ISessionChain chain);
+
+ ///
+ /// Marks the entire session chain as revoked, invalidating all associated sessions for the device or app family.
+ ///
+ /// The tenant identifier, or null.
+ /// The chain to revoke.
+ /// The UTC timestamp of revocation.
+ Task RevokeChainAsync(string? tenantId, ChainId chainId, DateTime at);
+
+ ///
+ /// Retrieves the active session identifier for the specified chain.
+ /// This is typically an O(1) lookup and used for session rotation.
+ ///
+ /// The tenant identifier, or null.
+ /// The chain whose active session is requested.
+ /// The active session identifier or null.
+ Task GetActiveSessionIdAsync(string? tenantId, ChainId chainId);
+
+ ///
+ /// Sets or replaces the active session identifier for the specified chain.
+ /// Must be atomic to prevent race conditions during refresh.
+ ///
+ /// The tenant identifier, or null.
+ /// The chain whose active session is being set.
+ /// The new active session identifier.
+ Task SetActiveSessionIdAsync(string? tenantId, ChainId chainId, AuthSessionId sessionId);
+
+ ///
+ /// Retrieves all session chains belonging to the specified user within the tenant scope.
+ ///
+ /// The tenant identifier, or null.
+ /// The user whose chains are being retrieved.
+ /// A read-only list of session chains.
+ Task>> GetChainsByUserAsync(string? tenantId, TUserId userId);
+
+ ///
+ /// Retrieves the session root for the user, which represents the full set of chains and their associated security metadata.
+ /// Returns null if the root does not exist.
+ ///
+ /// The tenant identifier, or null.
+ /// The user identifier.
+ /// The session root or null.
+ Task?> GetSessionRootAsync(string? tenantId, TUserId userId);
+
+ ///
+ /// Persists a session root structure, usually after chain creation, rotation, or security operations.
+ ///
+ /// The tenant identifier, or null.
+ /// The session root to save.
+ Task SaveSessionRootAsync(string? tenantId, ISessionRoot root);
+
+ ///
+ /// Revokes the session root, invalidating all chains and sessions belonging to the specified user in the tenant scope.
+ ///
+ /// The tenant identifier, or null.
+ /// The user whose root should be revoked.
+ /// The UTC timestamp of revocation.
+ Task RevokeSessionRootAsync(string? tenantId, TUserId userId, DateTime at);
+
+ ///
+ /// Removes expired sessions from the store while leaving chains and session roots intact. Cleanup strategy is determined by the store implementation.
+ ///
+ /// The tenant identifier, or null.
+ /// The current UTC timestamp.
+ Task DeleteExpiredSessionsAsync(string? tenantId, DateTime now);
+
+ ///
+ /// Retrieves the chain identifier associated with the specified session.
+ ///
+ /// The tenant identifier, or null.
+ /// The session identifier.
+ /// The chain identifier or null.
+ Task GetChainIdBySessionAsync(string? tenantId, AuthSessionId sessionId);
+ }
+
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ITokenStore.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ITokenStore.cs
new file mode 100644
index 0000000..ee116ac
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ITokenStore.cs
@@ -0,0 +1,75 @@
+using CodeBeam.UltimateAuth.Core.Domain;
+
+namespace CodeBeam.UltimateAuth.Core.Abstractions
+{
+ ///
+ /// Provides persistence and validation support for issued tokens,
+ /// including refresh tokens and optional access token identifiers (jti).
+ ///
+ public interface ITokenStore
+ {
+ ///
+ /// Persists a refresh token hash associated with a session.
+ ///
+ Task StoreRefreshTokenAsync(
+ string? tenantId,
+ TUserId userId,
+ AuthSessionId sessionId,
+ string refreshTokenHash,
+ DateTimeOffset expiresAt);
+
+ ///
+ /// Validates a provided refresh token against the stored hash.
+ /// Returns true if valid and not expired or revoked.
+ ///
+ Task ValidateRefreshTokenAsync(
+ string? tenantId,
+ TUserId userId,
+ AuthSessionId sessionId,
+ string providedRefreshToken);
+
+ ///
+ /// Revokes the refresh token associated with the specified session.
+ ///
+ Task RevokeRefreshTokenAsync(
+ string? tenantId,
+ AuthSessionId sessionId,
+ DateTimeOffset at);
+
+ ///
+ /// Revokes all refresh tokens belonging to the user.
+ ///
+ Task RevokeAllRefreshTokensAsync(
+ string? tenantId,
+ TUserId userId,
+ DateTimeOffset at);
+
+ // ------------------------------------------------------------
+ // ACCESS TOKEN IDENTIFIERS (OPTIONAL)
+ // ------------------------------------------------------------
+
+ ///
+ /// Stores a JWT ID (jti) for replay detection or revocation.
+ /// Implementations may ignore this if not supported.
+ ///
+ Task StoreTokenIdAsync(
+ string? tenantId,
+ string jti,
+ DateTimeOffset expiresAt);
+
+ ///
+ /// Determines whether the specified token identifier has been revoked.
+ ///
+ Task IsTokenIdRevokedAsync(
+ string? tenantId,
+ string jti);
+
+ ///
+ /// Revokes a token identifier, preventing further usage.
+ ///
+ Task RevokeTokenIdAsync(
+ string? tenantId,
+ string jti,
+ DateTimeOffset at);
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/CodeBeam.UltimateAuth.Core.csproj b/src/CodeBeam.UltimateAuth.Core/CodeBeam.UltimateAuth.Core.csproj
index 34c4219..488b584 100644
--- a/src/CodeBeam.UltimateAuth.Core/CodeBeam.UltimateAuth.Core.csproj
+++ b/src/CodeBeam.UltimateAuth.Core/CodeBeam.UltimateAuth.Core.csproj
@@ -10,7 +10,7 @@
-
+
diff --git a/src/CodeBeam.UltimateAuth.Core/Contexts/Issued/IssuedAccessToken.cs b/src/CodeBeam.UltimateAuth.Core/Contexts/Issued/IssuedAccessToken.cs
new file mode 100644
index 0000000..32d37e6
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Contexts/Issued/IssuedAccessToken.cs
@@ -0,0 +1,29 @@
+namespace CodeBeam.UltimateAuth.Core.Contexts
+{
+ ///
+ /// Represents an issued access token (JWT or opaque).
+ ///
+ public sealed class IssuedAccessToken
+ {
+ ///
+ /// The actual token value sent to the client.
+ ///
+ public required string Token { get; init; }
+
+ ///
+ /// Token type: "jwt" or "opaque".
+ /// Used for diagnostics and middleware behavior.
+ ///
+ public required string TokenType { 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; }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Contexts/Issued/IssuedRefreshToken.cs b/src/CodeBeam.UltimateAuth.Core/Contexts/Issued/IssuedRefreshToken.cs
new file mode 100644
index 0000000..1f64804
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Contexts/Issued/IssuedRefreshToken.cs
@@ -0,0 +1,24 @@
+namespace CodeBeam.UltimateAuth.Core.Contexts
+{
+ ///
+ /// Represents an issued refresh token.
+ /// Always opaque and hashed at rest.
+ ///
+ public sealed class IssuedRefreshToken
+ {
+ ///
+ /// Plain refresh token value (returned to client once).
+ ///
+ public required string Token { get; init; }
+
+ ///
+ /// Hash of the refresh token to be persisted.
+ ///
+ public required string TokenHash { get; init; }
+
+ ///
+ /// Expiration time.
+ ///
+ public required DateTimeOffset ExpiresAt { get; init; }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Contexts/Issued/IssuedSession.cs b/src/CodeBeam.UltimateAuth.Core/Contexts/Issued/IssuedSession.cs
new file mode 100644
index 0000000..157663a
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Contexts/Issued/IssuedSession.cs
@@ -0,0 +1,27 @@
+using CodeBeam.UltimateAuth.Core.Domain;
+
+namespace CodeBeam.UltimateAuth.Core.Contexts
+{
+ ///
+ /// Represents the result of a session issuance operation.
+ ///
+ 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; }
+
+ ///
+ /// Indicates whether this issuance is metadata-only
+ /// (used in SemiHybrid mode).
+ ///
+ public bool IsMetadataOnly { get; init; }
+ }
+
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Contexts/SessionIssueContext.cs b/src/CodeBeam.UltimateAuth.Core/Contexts/SessionIssueContext.cs
new file mode 100644
index 0000000..d75473c
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Contexts/SessionIssueContext.cs
@@ -0,0 +1,23 @@
+using CodeBeam.UltimateAuth.Core.Domain;
+
+namespace CodeBeam.UltimateAuth.Core.Contexts
+{
+ ///
+ /// Represents the context in which a session is issued
+ /// (login, refresh, reauthentication).
+ ///
+ public sealed class SessionIssueContext
+ {
+ public required TUserId UserId { get; init; }
+ public string? TenantId { get; init; }
+
+ public required long SecurityVersion { get; init; }
+
+ public DeviceInfo Device { get; init; }
+ public IReadOnlyDictionary? ClaimsSnapshot { get; init; }
+
+ public DateTime Now { get; init; } = DateTime.UtcNow;
+
+ public ChainId? ChainId { get; init; }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Contexts/SessionStoreContext.cs b/src/CodeBeam.UltimateAuth.Core/Contexts/SessionStoreContext.cs
new file mode 100644
index 0000000..fa5a426
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Contexts/SessionStoreContext.cs
@@ -0,0 +1,43 @@
+using CodeBeam.UltimateAuth.Core.Domain;
+
+namespace CodeBeam.UltimateAuth.Core.Contexts
+{
+ ///
+ /// Context information required by the session store when
+ /// creating or rotating sessions.
+ ///
+ public sealed class SessionStoreContext
+ {
+ ///
+ /// The authenticated user identifier.
+ ///
+ public required TUserId UserId { 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 ChainId? ChainId { 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; }
+
+ ///
+ /// Optional device or client identifier.
+ ///
+ public DeviceInfo? DeviceInfo { get; init; }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Contexts/TokenIssueContext.cs b/src/CodeBeam.UltimateAuth.Core/Contexts/TokenIssueContext.cs
new file mode 100644
index 0000000..782d6a4
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Contexts/TokenIssueContext.cs
@@ -0,0 +1,16 @@
+using System.Security.Claims;
+
+namespace CodeBeam.UltimateAuth.Core.Contexts
+{
+ public sealed class TokenIssueContext
+ {
+ public required string UserId { get; init; }
+ public required string TenantId { get; init; }
+
+ public IReadOnlyCollection Claims { get; init; } = Array.Empty();
+
+ public string? SessionId { get; init; }
+
+ public DateTimeOffset IssuedAt { get; init; } = DateTimeOffset.UtcNow;
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Contexts/UserContext.cs b/src/CodeBeam.UltimateAuth.Core/Contexts/UserContext.cs
new file mode 100644
index 0000000..34f30a2
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Contexts/UserContext.cs
@@ -0,0 +1,14 @@
+using CodeBeam.UltimateAuth.Core.Abstractions;
+
+namespace CodeBeam.UltimateAuth.Core.Contexts
+{
+ public sealed class UserContext
+ {
+ public TUserId? UserId { get; init; }
+ public IUser? User { get; init; }
+
+ public bool IsAuthenticated => UserId is not null;
+
+ public static UserContext Anonymous() => new();
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Session/AuthSessionId.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Session/AuthSessionId.cs
index f5a8ba2..70b7372 100644
--- a/src/CodeBeam.UltimateAuth.Core/Domain/Session/AuthSessionId.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Domain/Session/AuthSessionId.cs
@@ -10,21 +10,18 @@
/// Initializes a new using the specified GUID value.
///
/// The underlying GUID representing the session identifier.
- public AuthSessionId(Guid value)
+ public AuthSessionId(string value)
{
+ if (string.IsNullOrWhiteSpace(value))
+ throw new ArgumentException("SessionId cannot be empty.", nameof(value));
+
Value = value;
}
///
/// Gets the underlying GUID value of the session identifier.
///
- public Guid Value { get; }
-
- ///
- /// Generates a new session identifier using a newly created GUID.
- ///
- /// A new instance.
- public static AuthSessionId New() => new AuthSessionId(Guid.NewGuid());
+ public string Value { get; }
///
/// Determines whether the specified is equal to the current instance.
@@ -43,19 +40,19 @@ public AuthSessionId(Guid value)
///
/// Returns a hash code based on the underlying GUID value.
///
- public override int GetHashCode() => Value.GetHashCode();
+ public override int GetHashCode() => StringComparer.Ordinal.GetHashCode(Value);
///
/// Returns the string representation of the underlying GUID value.
///
/// The GUID as a string.
- public override string ToString() => Value.ToString();
+ public override string ToString() => Value;
///
/// Converts the to its underlying .
///
/// The session identifier.
/// The underlying GUID value.
- public static implicit operator Guid(AuthSessionId id) => id.Value;
+ public static implicit operator string(AuthSessionId id) => id.Value;
}
}
diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Session/ISession.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Session/ISession.cs
index 878d6b2..349ae7e 100644
--- a/src/CodeBeam.UltimateAuth.Core/Domain/Session/ISession.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Domain/Session/ISession.cs
@@ -31,7 +31,7 @@ public interface ISession
/// Gets the timestamp of the last successful usage.
/// Used when evaluating sliding expiration policies.
///
- DateTime LastSeenAt { get; }
+ DateTime? LastSeenAt { get; }
///
/// Gets a value indicating whether this session has been explicitly revoked.
diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Session/ISessionChain.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Session/ISessionChain.cs
index b7034df..e408b17 100644
--- a/src/CodeBeam.UltimateAuth.Core/Domain/Session/ISessionChain.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Domain/Session/ISessionChain.cs
@@ -38,10 +38,9 @@ public interface ISessionChain
IReadOnlyDictionary? ClaimsSnapshot { get; }
///
- /// Gets the list of all rotated sessions created within this chain.
- /// The newest session is always considered the active one.
+ /// Gets the identifier of the currently active authentication session, if one exists.
///
- IReadOnlyList> Sessions { get; }
+ AuthSessionId? ActiveSessionId { get; }
///
/// Gets a value indicating whether this chain has been revoked.
diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Session/ISessionRoot.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Session/ISessionRoot.cs
index c3d3baf..afc7d14 100644
--- a/src/CodeBeam.UltimateAuth.Core/Domain/Session/ISessionRoot.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Domain/Session/ISessionRoot.cs
@@ -7,18 +7,18 @@
///
public interface ISessionRoot
{
- ///
- /// Gets the identifier of the user who owns this session root.
- /// Each user has one root per tenant.
- ///
- TUserId UserId { 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.
+ ///
+ TUserId UserId { get; }
+
///
/// Gets a value indicating whether the entire session root is revoked.
/// When true, all chains and sessions belonging to this root are invalid,
diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Session/SessionMetadata.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Session/SessionMetadata.cs
index a496381..20fe7fe 100644
--- a/src/CodeBeam.UltimateAuth.Core/Domain/Session/SessionMetadata.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Domain/Session/SessionMetadata.cs
@@ -7,6 +7,14 @@
///
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();
+
///
/// Gets the version of the client application that created the session.
/// Useful for enforcing upgrade policies or troubleshooting version-related issues.
diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSession.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSession.cs
new file mode 100644
index 0000000..07dab4e
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSession.cs
@@ -0,0 +1,112 @@
+namespace CodeBeam.UltimateAuth.Core.Domain.Session
+{
+ public sealed class UAuthSession : ISession
+ {
+ public AuthSessionId SessionId { get; }
+ public string? TenantId { get; }
+ public TUserId UserId { get; }
+ public DateTime CreatedAt { get; }
+ public DateTime ExpiresAt { get; }
+ public DateTime? LastSeenAt { get; }
+ public bool IsRevoked { get; }
+ public DateTime? RevokedAt { get; }
+ public long SecurityVersionAtCreation { get; }
+ public DeviceInfo Device { get; }
+ public SessionMetadata Metadata { get; }
+
+ private UAuthSession(
+ AuthSessionId sessionId,
+ string? tenantId,
+ TUserId userId,
+ DateTime createdAt,
+ DateTime expiresAt,
+ DateTime? lastSeenAt,
+ bool isRevoked,
+ DateTime? revokedAt,
+ long securityVersionAtCreation,
+ DeviceInfo device,
+ SessionMetadata metadata)
+ {
+ SessionId = sessionId;
+ TenantId = tenantId;
+ UserId = userId;
+ CreatedAt = createdAt;
+ ExpiresAt = expiresAt;
+ LastSeenAt = lastSeenAt;
+ IsRevoked = isRevoked;
+ RevokedAt = revokedAt;
+ SecurityVersionAtCreation = securityVersionAtCreation;
+ Device = device;
+ Metadata = metadata;
+ }
+
+ public static UAuthSession Create(
+ AuthSessionId sessionId,
+ string? tenantId,
+ TUserId userId,
+ DateTime now,
+ DateTime expiresAt,
+ long securityVersion,
+ DeviceInfo device,
+ SessionMetadata metadata)
+ {
+ return new UAuthSession(
+ sessionId,
+ tenantId,
+ userId,
+ createdAt: now,
+ expiresAt: expiresAt,
+ lastSeenAt: now,
+ isRevoked: false,
+ revokedAt: null,
+ securityVersionAtCreation: securityVersion,
+ device: device,
+ metadata: metadata
+ );
+ }
+
+ public UAuthSession WithLastSeen(DateTime now)
+ {
+ return new UAuthSession(
+ SessionId,
+ TenantId,
+ UserId,
+ CreatedAt,
+ ExpiresAt,
+ lastSeenAt: now,
+ IsRevoked,
+ RevokedAt,
+ SecurityVersionAtCreation,
+ Device,
+ Metadata
+ );
+ }
+
+ public UAuthSession Revoke(DateTime at)
+ {
+ if (IsRevoked) return this;
+
+ return new UAuthSession(
+ SessionId,
+ TenantId,
+ UserId,
+ CreatedAt,
+ ExpiresAt,
+ LastSeenAt,
+ isRevoked: true,
+ revokedAt: at,
+ SecurityVersionAtCreation,
+ Device,
+ Metadata
+ );
+ }
+
+ public SessionState GetState(DateTime now)
+ {
+ if (IsRevoked) return SessionState.Revoked;
+ if (now >= ExpiresAt) return SessionState.Expired;
+ return SessionState.Active;
+ }
+ }
+
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSessionChain.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSessionChain.cs
new file mode 100644
index 0000000..c775877
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSessionChain.cs
@@ -0,0 +1,94 @@
+namespace CodeBeam.UltimateAuth.Core.Domain
+{
+ public sealed class UAuthSessionChain : ISessionChain
+ {
+ public ChainId ChainId { get; }
+ public string? TenantId { get; }
+ public TUserId UserId { get; }
+ public int RotationCount { get; }
+ public long SecurityVersionAtCreation { get; }
+ public IReadOnlyDictionary? ClaimsSnapshot { get; }
+ public AuthSessionId? ActiveSessionId { get; }
+ public bool IsRevoked { get; }
+ public DateTime? RevokedAt { get; }
+
+ private UAuthSessionChain(
+ ChainId chainId,
+ string? tenantId,
+ TUserId userId,
+ int rotationCount,
+ long securityVersionAtCreation,
+ IReadOnlyDictionary? claimsSnapshot,
+ AuthSessionId? activeSessionId,
+ bool isRevoked,
+ DateTime? revokedAt)
+ {
+ ChainId = chainId;
+ TenantId = tenantId;
+ UserId = userId;
+ RotationCount = rotationCount;
+ SecurityVersionAtCreation = securityVersionAtCreation;
+ ClaimsSnapshot = claimsSnapshot;
+ ActiveSessionId = activeSessionId;
+ IsRevoked = isRevoked;
+ RevokedAt = revokedAt;
+ }
+
+ public static UAuthSessionChain Create(
+ ChainId chainId,
+ string? tenantId,
+ TUserId userId,
+ long securityVersion,
+ IReadOnlyDictionary? claimsSnapshot = null)
+ {
+ return new UAuthSessionChain(
+ chainId,
+ tenantId,
+ userId,
+ rotationCount: 0,
+ securityVersionAtCreation: securityVersion,
+ claimsSnapshot: claimsSnapshot,
+ activeSessionId: null,
+ isRevoked: false,
+ revokedAt: null
+ );
+ }
+
+ public UAuthSessionChain ActivateSession(AuthSessionId sessionId)
+ {
+ if (IsRevoked)
+ return this;
+
+ return new UAuthSessionChain(
+ ChainId,
+ TenantId,
+ UserId,
+ RotationCount + 1,
+ SecurityVersionAtCreation,
+ ClaimsSnapshot,
+ activeSessionId: sessionId,
+ isRevoked: false,
+ revokedAt: null
+ );
+ }
+
+ public UAuthSessionChain Revoke(DateTime at)
+ {
+ if (IsRevoked)
+ return this;
+
+ return new UAuthSessionChain(
+ ChainId,
+ TenantId,
+ UserId,
+ RotationCount,
+ SecurityVersionAtCreation,
+ ClaimsSnapshot,
+ ActiveSessionId,
+ isRevoked: true,
+ revokedAt: at
+ );
+ }
+
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSessionRoot.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSessionRoot.cs
new file mode 100644
index 0000000..0b0be80
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSessionRoot.cs
@@ -0,0 +1,64 @@
+namespace CodeBeam.UltimateAuth.Core.Domain
+{
+ public sealed class UAuthSessionRoot : ISessionRoot
+ {
+ public TUserId UserId { get; }
+ public string? TenantId { get; }
+ public bool IsRevoked { get; }
+ public DateTime? RevokedAt { get; }
+ public long SecurityVersion { get; }
+ public IReadOnlyList> Chains { get; }
+ public DateTime LastUpdatedAt { get; }
+
+ private UAuthSessionRoot(
+ string? tenantId,
+ TUserId userId,
+ bool isRevoked,
+ DateTime? revokedAt,
+ long securityVersion,
+ IReadOnlyList> chains,
+ DateTime lastUpdatedAt)
+ {
+ TenantId = tenantId;
+ UserId = userId;
+ IsRevoked = isRevoked;
+ RevokedAt = revokedAt;
+ SecurityVersion = securityVersion;
+ Chains = chains;
+ LastUpdatedAt = lastUpdatedAt;
+ }
+
+ public static UAuthSessionRoot Create(
+ string? tenantId,
+ TUserId userId,
+ DateTime issuedAt)
+ {
+ return new UAuthSessionRoot(
+ tenantId,
+ userId,
+ isRevoked: false,
+ revokedAt: null,
+ securityVersion: 0,
+ chains: Array.Empty>(),
+ lastUpdatedAt: issuedAt
+ );
+ }
+
+ public UAuthSessionRoot Revoke(DateTime at)
+ {
+ if (IsRevoked)
+ return this;
+
+ return new UAuthSessionRoot(
+ TenantId,
+ UserId,
+ isRevoked: true,
+ revokedAt: at,
+ securityVersion: SecurityVersion,
+ chains: Chains,
+ lastUpdatedAt: at
+ );
+ }
+
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Token/UAuthJwtTokenDescriptor.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Token/UAuthJwtTokenDescriptor.cs
new file mode 100644
index 0000000..84bc03c
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Domain/Token/UAuthJwtTokenDescriptor.cs
@@ -0,0 +1,24 @@
+using System.Security.Claims;
+
+namespace CodeBeam.UltimateAuth.Core.Domain
+{
+ ///
+ /// Framework-agnostic JWT description used by IJwtTokenGenerator.
+ ///
+ public sealed class UAuthJwtTokenDescriptor
+ {
+ public required ClaimsIdentity Subject { get; init; }
+
+ public required string Issuer { get; init; }
+
+ public required string Audience { get; init; }
+
+ public required DateTime Expires { get; init; }
+
+ ///
+ /// Signing key material (symmetric or asymmetric).
+ /// Interpretation is up to the generator implementation.
+ ///
+ public required object SigningKey { get; init; }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Enums/UAuthMode.cs b/src/CodeBeam.UltimateAuth.Core/Enums/UAuthMode.cs
new file mode 100644
index 0000000..941a93a
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Enums/UAuthMode.cs
@@ -0,0 +1,43 @@
+namespace CodeBeam.UltimateAuth.Core
+{
+ ///
+ /// Defines the authentication execution model for UltimateAuth.
+ /// Each mode represents a fundamentally different security
+ /// and lifecycle strategy.
+ ///
+ 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,
+
+ ///
+ /// 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,
+
+ ///
+ /// 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/Extensions/UltimateAuthServiceCollectionExtensions.cs b/src/CodeBeam.UltimateAuth.Core/Extensions/UltimateAuthServiceCollectionExtensions.cs
index 378585e..a8f872d 100644
--- a/src/CodeBeam.UltimateAuth.Core/Extensions/UltimateAuthServiceCollectionExtensions.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Extensions/UltimateAuthServiceCollectionExtensions.cs
@@ -32,7 +32,7 @@ public static class UltimateAuthServiceCollectionExtensions
///
public static IServiceCollection AddUltimateAuth(this IServiceCollection services, IConfiguration configurationSection)
{
- services.Configure(configurationSection);
+ services.Configure(configurationSection);
return services.AddUltimateAuthInternal();
}
@@ -41,7 +41,7 @@ public static IServiceCollection AddUltimateAuth(this IServiceCollection service
/// This is useful when settings are derived dynamically or are not stored
/// in appsettings.json.
///
- public static IServiceCollection AddUltimateAuth(this IServiceCollection services, Action configure)
+ public static IServiceCollection AddUltimateAuth(this IServiceCollection services, Action configure)
{
services.Configure(configure);
return services.AddUltimateAuthInternal();
@@ -54,14 +54,15 @@ public static IServiceCollection AddUltimateAuth(this IServiceCollection service
///
public static IServiceCollection AddUltimateAuth(this IServiceCollection services)
{
- services.Configure(_ => { });
+ services.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:
@@ -72,20 +73,17 @@ public static IServiceCollection AddUltimateAuth(this IServiceCollection service
///
private static IServiceCollection AddUltimateAuthInternal(this IServiceCollection services)
{
- services.AddSingleton, UltimateAuthOptionsValidator>();
- services.AddSingleton, SessionOptionsValidator>();
- services.AddSingleton, TokenOptionsValidator>();
- services.AddSingleton, PkceOptionsValidator>();
- services.AddSingleton, MultiTenantOptionsValidator>();
+ services.AddSingleton, UAuthOptionsValidator>();
+ services.AddSingleton, UAuthSessionOptionsValidator>();
+ services.AddSingleton, UAuthTokenOptionsValidator>();
+ services.AddSingleton, UAuthPkceOptionsValidator>();
+ services.AddSingleton, UAuthMultiTenantOptionsValidator>();
- // Binding of nested sub-options (Session, Token, etc.) is intentionally not done here.
- // These must be bound at the server level to allow configuration per-environment.
+ // Nested options are bound automatically by the options binder.
+ // Server layer may override or extend these settings.
services.AddSingleton();
- // Default factory throws until a real session store is registered.
- services.TryAddSingleton();
-
return services;
}
diff --git a/src/CodeBeam.UltimateAuth.Core/Extensions/UltimateAuthSessionStoreExtensions.cs b/src/CodeBeam.UltimateAuth.Core/Extensions/UltimateAuthSessionStoreExtensions.cs
index 2037e0b..3f9d93f 100644
--- a/src/CodeBeam.UltimateAuth.Core/Extensions/UltimateAuthSessionStoreExtensions.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Extensions/UltimateAuthSessionStoreExtensions.cs
@@ -5,7 +5,7 @@
namespace CodeBeam.UltimateAuth.Core.Extensions
{
///
- /// Provides extension methods for registering a concrete
+ /// Provides extension methods for registering a concrete
/// implementation into the application's dependency injection container.
///
/// UltimateAuth requires exactly one session store implementation that determines
@@ -34,16 +34,16 @@ public static IServiceCollection AddUltimateAuthSessionStore(this IServi
.GetInterfaces()
.FirstOrDefault(i =>
i.IsGenericType &&
- i.GetGenericTypeDefinition() == typeof(ISessionStore<>));
+ i.GetGenericTypeDefinition() == typeof(ISessionStoreKernel<>));
if (storeInterface is null)
{
throw new InvalidOperationException(
- $"{typeof(TStore).Name} must implement ISessionStore.");
+ $"{typeof(TStore).Name} must implement ISessionStoreKernel.");
}
var userIdType = storeInterface.GetGenericArguments()[0];
- var typedInterface = typeof(ISessionStore<>).MakeGenericType(userIdType);
+ var typedInterface = typeof(ISessionStoreKernel<>).MakeGenericType(userIdType);
services.TryAddScoped(typedInterface, typeof(TStore));
@@ -84,7 +84,7 @@ public GenericSessionStoreFactory(IServiceProvider sp, Type userIdType)
/// for the specified tenant and user ID type.
/// Throws if the requested TUserId does not match the registered store's type.
///
- public ISessionStore Create(string? tenantId)
+ public ISessionStoreKernel Create(string? tenantId)
{
if (typeof(TUserId) != _userIdType)
{
@@ -93,10 +93,10 @@ public ISessionStore Create(string? tenantId)
$"but requested with TUserId='{typeof(TUserId).Name}'.");
}
- var typed = typeof(ISessionStore<>).MakeGenericType(_userIdType);
+ var typed = typeof(ISessionStoreKernel<>).MakeGenericType(_userIdType);
var store = _sp.GetRequiredService(typed);
- return (ISessionStore)store;
+ return (ISessionStoreKernel)store;
}
}
}
diff --git a/src/CodeBeam.UltimateAuth.Core/Internal/UAuthSession.cs b/src/CodeBeam.UltimateAuth.Core/Internal/UAuthSession.cs
deleted file mode 100644
index 69a9cfd..0000000
--- a/src/CodeBeam.UltimateAuth.Core/Internal/UAuthSession.cs
+++ /dev/null
@@ -1,71 +0,0 @@
-using CodeBeam.UltimateAuth.Core.Domain;
-
-namespace CodeBeam.UltimateAuth.Core.Internal
-{
- internal sealed class UAuthSession : ISession
- {
- public UAuthSession(AuthSessionId sessionId, TUserId userId, DateTime createdAt, DateTime expiresAt, DateTime lastSeenAt,
- long securityVersionAtCreation, DeviceInfo device, SessionMetadata metadata, bool isRevoked = false,
- DateTime? revokedAt = null)
- {
- SessionId = sessionId;
- UserId = userId;
- CreatedAt = createdAt;
- ExpiresAt = expiresAt;
- LastSeenAt = lastSeenAt;
- SecurityVersionAtCreation = securityVersionAtCreation;
- Device = device;
- Metadata = metadata;
- IsRevoked = isRevoked;
- RevokedAt = revokedAt;
- }
-
- public AuthSessionId SessionId { get; }
- public TUserId UserId { get; }
-
- public DateTime CreatedAt { get; }
- public DateTime ExpiresAt { get; }
- public DateTime LastSeenAt { get; }
-
- public long SecurityVersionAtCreation { get; }
-
- public bool IsRevoked { get; }
- public DateTime? RevokedAt { get; }
-
- public DeviceInfo Device { get; }
- public SessionMetadata Metadata { get; }
-
- public SessionState GetState(DateTime now)
- {
- if (IsRevoked) return SessionState.Revoked;
- if (now >= ExpiresAt) return SessionState.Expired;
-
- return SessionState.Active;
- }
-
- public static UAuthSession CreateNew(TUserId userId, long rootSecurityVersion, DeviceInfo device, SessionMetadata metadata, DateTime now, TimeSpan lifetime)
- {
- return new UAuthSession(
- sessionId: AuthSessionId.New(),
- userId: userId,
- createdAt: now,
- expiresAt: now.Add(lifetime),
- lastSeenAt: now,
- securityVersionAtCreation: rootSecurityVersion,
- device: device,
- metadata: metadata
- );
- }
-
- // TODO: WithUpdatedLastSeenAt? Add as optionally used in session validation flow.
- public UAuthSession WithRevoked(DateTime at)
- {
- return new UAuthSession(
- SessionId, UserId, CreatedAt, ExpiresAt, LastSeenAt,
- SecurityVersionAtCreation, Device, Metadata,
- isRevoked: true,
- revokedAt: at
- );
- }
- }
-}
diff --git a/src/CodeBeam.UltimateAuth.Core/Internal/UAuthSessionChain.cs b/src/CodeBeam.UltimateAuth.Core/Internal/UAuthSessionChain.cs
deleted file mode 100644
index 4f10170..0000000
--- a/src/CodeBeam.UltimateAuth.Core/Internal/UAuthSessionChain.cs
+++ /dev/null
@@ -1,76 +0,0 @@
-using CodeBeam.UltimateAuth.Core.Domain;
-
-namespace CodeBeam.UltimateAuth.Core.Internal
-{
- internal sealed class UAuthSessionChain : ISessionChain
- {
- public UAuthSessionChain(ChainId chainId, TUserId userId, int rotationCount, long securityVersionAtCreation, IReadOnlyDictionary? claimsSnapshot,
- IReadOnlyList> sessions, bool isRevoked = false, DateTime? revokedAt = null)
- {
- ChainId = chainId;
- UserId = userId;
- RotationCount = rotationCount;
- SecurityVersionAtCreation = securityVersionAtCreation;
- ClaimsSnapshot = claimsSnapshot;
- Sessions = sessions;
- IsRevoked = isRevoked;
- RevokedAt = revokedAt;
- }
-
- public ChainId ChainId { get; }
- public TUserId UserId { get; }
-
- public int RotationCount { get; }
- public long SecurityVersionAtCreation { get; }
-
- public IReadOnlyDictionary? ClaimsSnapshot { get; }
- public IReadOnlyList> Sessions { get; }
-
- public bool IsRevoked { get; }
- public DateTime? RevokedAt { get; }
-
- public static UAuthSessionChain CreateNew(TUserId userId, long rootSecurityVersion, ISession initialSession, IReadOnlyDictionary? claimsSnapshot)
- {
- return new UAuthSessionChain(
- chainId: ChainId.New(),
- userId: userId,
- rotationCount: 0,
- securityVersionAtCreation: rootSecurityVersion,
- claimsSnapshot: claimsSnapshot,
- sessions: new[] { initialSession }
- );
- }
-
- public UAuthSessionChain AddRotatedSession(ISession session)
- {
- var newList = new List>(Sessions.Count + 1);
- newList.AddRange(Sessions);
- newList.Add(session);
-
- return new UAuthSessionChain(
- ChainId,
- UserId,
- RotationCount + 1,
- SecurityVersionAtCreation,
- ClaimsSnapshot,
- newList,
- IsRevoked,
- RevokedAt
- );
- }
-
- public UAuthSessionChain WithRevoked(DateTime at)
- {
- return new UAuthSessionChain(
- ChainId,
- UserId,
- RotationCount,
- SecurityVersionAtCreation,
- ClaimsSnapshot,
- Sessions,
- isRevoked: true,
- revokedAt: at
- );
- }
- }
-}
diff --git a/src/CodeBeam.UltimateAuth.Core/Internal/UAuthSessionRoot.cs b/src/CodeBeam.UltimateAuth.Core/Internal/UAuthSessionRoot.cs
deleted file mode 100644
index 9696f6c..0000000
--- a/src/CodeBeam.UltimateAuth.Core/Internal/UAuthSessionRoot.cs
+++ /dev/null
@@ -1,95 +0,0 @@
-using CodeBeam.UltimateAuth.Core.Domain;
-
-namespace CodeBeam.UltimateAuth.Core.Internal
-{
- internal sealed class UAuthSessionRoot : ISessionRoot
- {
- public UAuthSessionRoot(string? tenantId, TUserId userId, bool isRevoked, DateTime? revokedAt, long securityVersion, IReadOnlyList> chains, DateTime lastUpdatedAt)
- {
- TenantId = tenantId;
- UserId = userId;
- IsRevoked = isRevoked;
- RevokedAt = revokedAt;
- SecurityVersion = securityVersion;
- Chains = chains;
- LastUpdatedAt = lastUpdatedAt;
- }
-
- public string? TenantId { get; }
- public TUserId UserId { get; }
- public bool IsRevoked { get; }
- public DateTime? RevokedAt { get; }
- public long SecurityVersion { get; }
- public IReadOnlyList> Chains { get; }
- public DateTime LastUpdatedAt { get; }
-
- public static UAuthSessionRoot CreateNew(string? tenantId, TUserId userId, DateTime now)
- {
- return new UAuthSessionRoot(
- tenantId: tenantId,
- userId: userId,
- isRevoked: false,
- revokedAt: null,
- securityVersion: 1,
- chains: Array.Empty>(),
- lastUpdatedAt: now
- );
- }
-
- public UAuthSessionRoot AddChain(ISessionChain chain, DateTime now)
- {
- var newList = new List>(Chains.Count + 1);
- newList.AddRange(Chains);
- newList.Add(chain);
-
- return new UAuthSessionRoot(
- TenantId,
- UserId,
- IsRevoked,
- RevokedAt,
- SecurityVersion,
- newList,
- lastUpdatedAt: now
- );
- }
-
- public UAuthSessionRoot WithSecurityVersionIncrement(DateTime now)
- {
- return new UAuthSessionRoot(
- TenantId,
- UserId,
- IsRevoked,
- RevokedAt,
- securityVersion: SecurityVersion + 1,
- Chains,
- lastUpdatedAt: now
- );
- }
-
- public UAuthSessionRoot WithRevoked(DateTime at)
- {
- return new UAuthSessionRoot(
- TenantId,
- UserId,
- isRevoked: true,
- revokedAt: at,
- SecurityVersion,
- Chains,
- lastUpdatedAt: at
- );
- }
-
- public UAuthSessionRoot WithUnrevoked(DateTime now)
- {
- return new UAuthSessionRoot(
- TenantId,
- UserId,
- isRevoked: false,
- revokedAt: null,
- SecurityVersion,
- Chains,
- lastUpdatedAt: now
- );
- }
- }
-}
diff --git a/src/CodeBeam.UltimateAuth.Core/Internal/UAuthSessionService.cs b/src/CodeBeam.UltimateAuth.Core/Internal/UAuthSessionService.cs
deleted file mode 100644
index c8a5da2..0000000
--- a/src/CodeBeam.UltimateAuth.Core/Internal/UAuthSessionService.cs
+++ /dev/null
@@ -1,170 +0,0 @@
-using CodeBeam.UltimateAuth.Core.Abstractions;
-using CodeBeam.UltimateAuth.Core.Domain;
-using CodeBeam.UltimateAuth.Core.Models;
-using CodeBeam.UltimateAuth.Core.Options;
-
-namespace CodeBeam.UltimateAuth.Core.Internal
-{
- internal sealed class UAuthSessionService : ISessionService
- {
- private readonly ISessionStore _store;
- private readonly SessionOptions _options;
-
- public UAuthSessionService(ISessionStore store, SessionOptions options)
- {
- _store = store;
- _options = options;
- }
-
- public async Task> CreateLoginSessionAsync(string? tenantId, TUserId userId, DeviceInfo device, SessionMetadata? metadata, DateTime now)
- {
- var root = await _store.GetSessionRootAsync(tenantId, userId) ?? UAuthSessionRoot.CreateNew(tenantId, userId, now);
-
- var session = UAuthSession.CreateNew(
- userId,
- root.SecurityVersion,
- device,
- metadata ?? new SessionMetadata(),
- now,
- _options.Lifetime
- );
-
- var chain = UAuthSessionChain.CreateNew(
- userId,
- root.SecurityVersion,
- session,
- claimsSnapshot: null
- );
-
- var concreteRoot = (UAuthSessionRoot)root;
- var updatedRoot = concreteRoot.AddChain(chain, now);
-
- await _store.SaveSessionAsync(tenantId, session);
- await _store.SaveChainAsync(tenantId, chain);
- await _store.SaveSessionRootAsync(tenantId, updatedRoot);
- await _store.SetActiveSessionIdAsync(tenantId, chain.ChainId, session.SessionId);
-
- return new SessionResult
- {
- Session = session,
- Chain = chain,
- Root = updatedRoot
- };
- }
-
- public async Task> RefreshSessionAsync(string? tenantId, AuthSessionId currentSessionId, DateTime now)
- {
- var oldSession = await _store.GetSessionAsync(tenantId, currentSessionId) ?? throw new InvalidOperationException("Session not found");
-
- var chainId = await _store.GetChainIdBySessionAsync(tenantId, currentSessionId)
- ?? throw new InvalidOperationException("Chain not found");
-
- var chain = await _store.GetChainAsync(tenantId, chainId)
- ?? throw new InvalidOperationException("Chain missing");
-
- var root = await _store.GetSessionRootAsync(tenantId, oldSession.UserId)
- ?? throw new InvalidOperationException("Root missing");
-
- if (root.IsRevoked)
- throw new UnauthorizedAccessException("Root revoked");
-
- if (chain.IsRevoked)
- throw new UnauthorizedAccessException("Chain revoked");
-
- if (oldSession.SecurityVersionAtCreation != root.SecurityVersion)
- throw new UnauthorizedAccessException("SecurityVersion mismatch");
-
- if (now >= oldSession.ExpiresAt)
- throw new UnauthorizedAccessException("Session expired");
-
- var newSession = UAuthSession.CreateNew(
- oldSession.UserId,
- root.SecurityVersion,
- oldSession.Device,
- oldSession.Metadata,
- now,
- _options.Lifetime
- );
-
- var concreteChain = (UAuthSessionChain)chain;
- var rotatedChain = concreteChain.AddRotatedSession(newSession);
-
- await _store.SaveSessionAsync(tenantId, newSession);
- await _store.UpdateChainAsync(tenantId, rotatedChain);
- await _store.SetActiveSessionIdAsync(tenantId, chain.ChainId, newSession.SessionId);
-
- return new SessionResult
- {
- Session = newSession,
- Chain = rotatedChain,
- Root = root
- };
- }
-
- public async Task RevokeSessionAsync(string? tenantId, AuthSessionId sessionId, DateTime at)
- {
- await _store.RevokeSessionAsync(tenantId, sessionId, at);
- }
-
- public async Task RevokeChainAsync(string? tenantId, ChainId chainId, DateTime at)
- {
- await _store.RevokeChainAsync(tenantId, chainId, at);
- }
-
- public async Task RevokeRootAsync(string? tenantId, TUserId userId, DateTime at)
- {
- await _store.RevokeSessionRootAsync(tenantId, userId, at);
- }
-
- public async Task> ValidateSessionAsync(string? tenantId, AuthSessionId sessionId, DateTime now)
- {
- var session = await _store.GetSessionAsync(tenantId, sessionId);
-
- if (session == null)
- {
- return new SessionValidationResult
- {
- State = SessionState.Expired
- };
- }
-
- var chainId = await _store.GetChainIdBySessionAsync(tenantId, sessionId);
- var chain = chainId == null ? null : await _store.GetChainAsync(tenantId, chainId.Value);
- var root = await _store.GetSessionRootAsync(tenantId, session.UserId);
-
- var state = ComputeState(session, chain, root, now);
-
- return new SessionValidationResult
- {
- Session = session,
- Chain = chain,
- Root = root,
- State = state
- };
- }
-
- private SessionState ComputeState(ISession session, ISessionChain? chain, ISessionRoot? root, DateTime now)
- {
- if (root == null || chain == null)
- return SessionState.Expired;
-
- if (root.IsRevoked) return SessionState.RootRevoked;
- if (chain.IsRevoked) return SessionState.ChainRevoked;
-
- if (session.IsRevoked) return SessionState.Revoked;
- if (now >= session.ExpiresAt) return SessionState.Expired;
-
- if (session.SecurityVersionAtCreation != root.SecurityVersion)
- return SessionState.SecurityVersionMismatch;
-
- return SessionState.Active;
- }
-
- public Task>> GetChainsAsync(string? tenantId, TUserId userId)
- => _store.GetChainsByUserAsync(tenantId, userId);
-
- public Task>> GetSessionsAsync(string? tenantId, ChainId chainId)
- => _store.GetSessionsByChainAsync(tenantId, chainId);
-
- }
-}
diff --git a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/CompositeTenantResolver.cs b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/CompositeTenantResolver.cs
index 1dc03b9..af82c69 100644
--- a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/CompositeTenantResolver.cs
+++ b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/CompositeTenantResolver.cs
@@ -3,15 +3,15 @@
///
/// Executes multiple tenant resolvers in order; the first resolver returning a non-null tenant id wins.
///
- public sealed class CompositeTenantResolver : ITenantResolver
+ public sealed class CompositeTenantResolver : ITenantIdResolver
{
- private readonly IReadOnlyList _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)
+ public CompositeTenantResolver(IEnumerable resolvers)
{
_resolvers = resolvers.ToList();
}
diff --git a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/FixedTenantResolver.cs b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/FixedTenantResolver.cs
index 84b70d4..28b8506 100644
--- a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/FixedTenantResolver.cs
+++ b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/FixedTenantResolver.cs
@@ -3,7 +3,7 @@
///
/// Returns a constant tenant id for all resolution requests; useful for single-tenant or statically configured systems.
///
- public sealed class FixedTenantResolver : ITenantResolver
+ public sealed class FixedTenantResolver : ITenantIdResolver
{
private readonly string _tenantId;
diff --git a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/HeaderTenantResolver.cs b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/HeaderTenantResolver.cs
index bf33d31..e969f0d 100644
--- a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/HeaderTenantResolver.cs
+++ b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/HeaderTenantResolver.cs
@@ -5,7 +5,7 @@
/// Example: X-Tenant: foo → returns "foo".
/// Useful when multi-tenancy is controlled by API gateways or reverse proxies.
///
- public sealed class HeaderTenantResolver : ITenantResolver
+ public sealed class HeaderTenantResolver : ITenantIdResolver
{
private readonly string _headerName;
diff --git a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/HostTenantResolver.cs b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/HostTenantResolver.cs
index 00d5bd9..f411b8d 100644
--- a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/HostTenantResolver.cs
+++ b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/HostTenantResolver.cs
@@ -1,34 +1,30 @@
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 : ITenantResolver
+ 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/ITenantResolver.cs b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/ITenantIdResolver.cs
similarity index 93%
rename from src/CodeBeam.UltimateAuth.Core/MultiTenancy/ITenantResolver.cs
rename to src/CodeBeam.UltimateAuth.Core/MultiTenancy/ITenantIdResolver.cs
index bd448a1..cc7867c 100644
--- a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/ITenantResolver.cs
+++ b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/ITenantIdResolver.cs
@@ -5,7 +5,7 @@
/// Implementations may extract the tenant from headers, hostnames,
/// authentication tokens, or any other application-defined source.
///
- public interface ITenantResolver
+ public interface ITenantIdResolver
{
///
/// Attempts to resolve the tenant id given the contextual request data.
diff --git a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/PathTenantResolver.cs b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/PathTenantResolver.cs
index 11df974..38c3f77 100644
--- a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/PathTenantResolver.cs
+++ b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/PathTenantResolver.cs
@@ -4,7 +4,7 @@
/// Resolves the tenant id from the request path.
/// Example pattern: /t/{tenantId}/... → returns the extracted tenant id.
///
- public sealed class PathTenantResolver : ITenantResolver
+ public sealed class PathTenantResolver : ITenantIdResolver
{
private readonly string _prefix;
diff --git a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/TenantResolutionContext.cs b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/TenantResolutionContext.cs
index d5fd3a5..6ae1c48 100644
--- a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/TenantResolutionContext.cs
+++ b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/TenantResolutionContext.cs
@@ -31,7 +31,36 @@ public sealed class TenantResolutionContext
///
/// 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();
+
+ private TenantResolutionContext() { }
+
+ public static TenantResolutionContext Create(
+ IReadOnlyDictionary? headers = null,
+ IReadOnlyDictionary? Query = null,
+ string? host = null,
+ string? path = null,
+ object? rawContext = null)
+ {
+ return new TenantResolutionContext
+ {
+ 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
new file mode 100644
index 0000000..c33d8b7
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/TenantValidation.cs
@@ -0,0 +1,28 @@
+using System.Text.RegularExpressions;
+using CodeBeam.UltimateAuth.Core.Options;
+
+namespace CodeBeam.UltimateAuth.Core.MultiTenancy
+{
+ internal static class TenantValidation
+ {
+ public static UAuthTenantContext FromResolvedTenant(
+ string rawTenantId,
+ UAuthMultiTenantOptions options)
+ {
+ if (string.IsNullOrWhiteSpace(rawTenantId))
+ return UAuthTenantContext.NotResolved();
+
+ var tenantId = options.NormalizeToLowercase
+ ? rawTenantId.ToLowerInvariant()
+ : rawTenantId;
+
+ if (!Regex.IsMatch(tenantId, options.TenantIdRegex))
+ return UAuthTenantContext.NotResolved();
+
+ if (options.ReservedTenantIds.Contains(tenantId))
+ return UAuthTenantContext.NotResolved();
+
+ return UAuthTenantContext.Resolved(tenantId);
+ }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/UAuthTenantContext.cs b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/UAuthTenantContext.cs
new file mode 100644
index 0000000..9874068
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/UAuthTenantContext.cs
@@ -0,0 +1,23 @@
+namespace CodeBeam.UltimateAuth.Core.MultiTenancy
+{
+ ///
+ /// Represents the resolved tenant result for the current request.
+ ///
+ public sealed class UAuthTenantContext
+ {
+ public string? TenantId { get; }
+ public bool IsResolved { get; }
+
+ private UAuthTenantContext(string? tenantId, bool resolved)
+ {
+ TenantId = tenantId;
+ IsResolved = resolved;
+ }
+
+ public static UAuthTenantContext NotResolved()
+ => new(null, false);
+
+ public static UAuthTenantContext Resolved(string tenantId)
+ => new(tenantId, true);
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Options/LoginOptions.cs b/src/CodeBeam.UltimateAuth.Core/Options/UAuthLoginOptions.cs
similarity index 94%
rename from src/CodeBeam.UltimateAuth.Core/Options/LoginOptions.cs
rename to src/CodeBeam.UltimateAuth.Core/Options/UAuthLoginOptions.cs
index 320299f..46677d7 100644
--- a/src/CodeBeam.UltimateAuth.Core/Options/LoginOptions.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Options/UAuthLoginOptions.cs
@@ -4,7 +4,7 @@
/// Configuration settings related to interactive user login behavior,
/// including lockout policies and failed-attempt thresholds.
///
- public sealed class LoginOptions
+ public sealed class UAuthLoginOptions
{
///
/// Maximum number of consecutive failed login attempts allowed
diff --git a/src/CodeBeam.UltimateAuth.Core/Options/MultiTenantOptions.cs b/src/CodeBeam.UltimateAuth.Core/Options/UAuthMultiTenantOptions.cs
similarity index 79%
rename from src/CodeBeam.UltimateAuth.Core/Options/MultiTenantOptions.cs
rename to src/CodeBeam.UltimateAuth.Core/Options/UAuthMultiTenantOptions.cs
index 5fae413..6774eb7 100644
--- a/src/CodeBeam.UltimateAuth.Core/Options/MultiTenantOptions.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Options/UAuthMultiTenantOptions.cs
@@ -5,7 +5,7 @@
/// Controls whether tenants are required, how they are resolved,
/// and how tenant identifiers are normalized.
///
- public sealed class MultiTenantOptions
+ public sealed class UAuthMultiTenantOptions
{
///
/// Enables multi-tenant mode.
@@ -17,7 +17,7 @@ public sealed class MultiTenantOptions
/// If tenant cannot be resolved, this value is used.
/// If null and RequireTenant = true, request fails.
///
- public string? DefaultTenantId { get; set; } = "default";
+ public string? DefaultTenantId { get; set; }
///
/// If true, a resolved tenant id must always exist.
@@ -55,5 +55,16 @@ public sealed class MultiTenantOptions
/// 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;
+
+ // Header config
+ public string HeaderName { get; set; } = "X-Tenant";
}
}
diff --git a/src/CodeBeam.UltimateAuth.Core/Options/MultiTenantOptionsValidator.cs b/src/CodeBeam.UltimateAuth.Core/Options/UAuthMultiTenantOptionsValidator.cs
similarity index 89%
rename from src/CodeBeam.UltimateAuth.Core/Options/MultiTenantOptionsValidator.cs
rename to src/CodeBeam.UltimateAuth.Core/Options/UAuthMultiTenantOptionsValidator.cs
index b6b25e9..74828a1 100644
--- a/src/CodeBeam.UltimateAuth.Core/Options/MultiTenantOptionsValidator.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Options/UAuthMultiTenantOptionsValidator.cs
@@ -4,15 +4,15 @@
namespace CodeBeam.UltimateAuth.Core.Options
{
///
- /// Validates at application startup.
+ /// 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 MultiTenantOptionsValidator : IValidateOptions
+ internal sealed class UAuthMultiTenantOptionsValidator : IValidateOptions
{
///
- /// Performs validation on the provided instance.
+ /// Performs validation on the provided instance.
/// This method enforces:
/// - valid tenant id regex format,
/// - reserved tenant ids matching the regex,
@@ -25,7 +25,7 @@ internal sealed class MultiTenantOptionsValidator : IValidateOptions indicating success or the
/// specific configuration error encountered.
///
- public ValidateOptionsResult Validate(string? name, MultiTenantOptions options)
+ public ValidateOptionsResult Validate(string? name, UAuthMultiTenantOptions options)
{
// Multi-tenancy disabled → no validation needed
if (!options.Enabled)
diff --git a/src/CodeBeam.UltimateAuth.Core/Options/UltimateAuthOptions.cs b/src/CodeBeam.UltimateAuth.Core/Options/UAuthOptions.cs
similarity index 84%
rename from src/CodeBeam.UltimateAuth.Core/Options/UltimateAuthOptions.cs
rename to src/CodeBeam.UltimateAuth.Core/Options/UAuthOptions.cs
index fd015b4..2915ce2 100644
--- a/src/CodeBeam.UltimateAuth.Core/Options/UltimateAuthOptions.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Options/UAuthOptions.cs
@@ -11,31 +11,31 @@ namespace CodeBeam.UltimateAuth.Core.Options
/// All sub-options are resolved from configuration (appsettings.json)
/// or through inline setup in AddUltimateAuth().
///
- public sealed class UltimateAuthOptions
+ public sealed class UAuthOptions
{
///
/// Configuration settings for interactive login flows,
/// including lockout thresholds and failed-attempt policies.
///
- public LoginOptions Login { get; set; } = new();
+ public UAuthLoginOptions Login { get; set; } = new();
///
/// Settings that control session creation, refresh behavior,
/// sliding expiration, idle timeouts, device limits, and chain rules.
///
- public SessionOptions Session { get; set; } = new();
+ public UAuthSessionOptions Session { get; set; } = new();
///
/// Token issuance configuration, including JWT and opaque token
/// generation, lifetimes, signing keys, and audience/issuer values.
///
- public TokenOptions Token { get; set; } = new();
+ public UAuthTokenOptions Token { get; set; } = new();
///
/// PKCE (Proof Key for Code Exchange) configuration used for
/// browser-based login flows and WASM authentication.
///
- public PkceOptions Pkce { get; set; } = new();
+ public UAuthPkceOptions Pkce { get; set; } = new();
///
/// Event hooks raised during authentication lifecycle events
@@ -47,7 +47,7 @@ public sealed class UltimateAuthOptions
/// Multi-tenancy configuration controlling how tenants are resolved,
/// validated, and optionally enforced.
///
- public MultiTenantOptions MultiTenantOptions { get; set; } = new();
+ public UAuthMultiTenantOptions MultiTenantOptions { get; set; } = new();
///
/// Provides converters used to normalize and serialize TUserId
diff --git a/src/CodeBeam.UltimateAuth.Core/Options/UltimateAuthOptionsValidator.cs b/src/CodeBeam.UltimateAuth.Core/Options/UAuthOptionsValidator.cs
similarity index 88%
rename from src/CodeBeam.UltimateAuth.Core/Options/UltimateAuthOptionsValidator.cs
rename to src/CodeBeam.UltimateAuth.Core/Options/UAuthOptionsValidator.cs
index e313456..405681e 100644
--- a/src/CodeBeam.UltimateAuth.Core/Options/UltimateAuthOptionsValidator.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Options/UAuthOptionsValidator.cs
@@ -2,9 +2,9 @@
namespace CodeBeam.UltimateAuth.Core.Options
{
- internal sealed class UltimateAuthOptionsValidator : IValidateOptions
+ internal sealed class UAuthOptionsValidator : IValidateOptions
{
- public ValidateOptionsResult Validate(string? name, UltimateAuthOptions options)
+ public ValidateOptionsResult Validate(string? name, UAuthOptions options)
{
var errors = new List();
diff --git a/src/CodeBeam.UltimateAuth.Core/Options/PkceOptions.cs b/src/CodeBeam.UltimateAuth.Core/Options/UAuthPkceOptions.cs
similarity index 93%
rename from src/CodeBeam.UltimateAuth.Core/Options/PkceOptions.cs
rename to src/CodeBeam.UltimateAuth.Core/Options/UAuthPkceOptions.cs
index ce1a7bd..18af314 100644
--- a/src/CodeBeam.UltimateAuth.Core/Options/PkceOptions.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Options/UAuthPkceOptions.cs
@@ -5,7 +5,7 @@
/// authorization flows. Controls how long authorization codes remain
/// valid before they must be exchanged for tokens.
///
- public sealed class PkceOptions
+ public sealed class UAuthPkceOptions
{
///
/// Lifetime of a PKCE authorization code in seconds.
diff --git a/src/CodeBeam.UltimateAuth.Core/Options/PkceOptionsValidator.cs b/src/CodeBeam.UltimateAuth.Core/Options/UAuthPkceOptionsValidator.cs
similarity index 73%
rename from src/CodeBeam.UltimateAuth.Core/Options/PkceOptionsValidator.cs
rename to src/CodeBeam.UltimateAuth.Core/Options/UAuthPkceOptionsValidator.cs
index 6c95a63..744d81a 100644
--- a/src/CodeBeam.UltimateAuth.Core/Options/PkceOptionsValidator.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Options/UAuthPkceOptionsValidator.cs
@@ -2,9 +2,9 @@
namespace CodeBeam.UltimateAuth.Core.Options
{
- internal sealed class PkceOptionsValidator : IValidateOptions
+ internal sealed class UAuthPkceOptionsValidator : IValidateOptions
{
- public ValidateOptionsResult Validate(string? name, PkceOptions options)
+ public ValidateOptionsResult Validate(string? name, UAuthPkceOptions options)
{
var errors = new List();
diff --git a/src/CodeBeam.UltimateAuth.Core/Options/SessionOptions.cs b/src/CodeBeam.UltimateAuth.Core/Options/UAuthSessionOptions.cs
similarity index 92%
rename from src/CodeBeam.UltimateAuth.Core/Options/SessionOptions.cs
rename to src/CodeBeam.UltimateAuth.Core/Options/UAuthSessionOptions.cs
index 696a197..60776df 100644
--- a/src/CodeBeam.UltimateAuth.Core/Options/SessionOptions.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Options/UAuthSessionOptions.cs
@@ -8,7 +8,7 @@
/// These values influence how sessions are created, refreshed,
/// expired, revoked, and grouped into device chains.
///
- public sealed class SessionOptions
+ public sealed class UAuthSessionOptions
{
///
/// The standard lifetime of a session before it expires.
@@ -18,10 +18,10 @@ public sealed class SessionOptions
///
/// Maximum absolute lifetime a session may have, even when
- /// sliding expiration is enabled. If set to zero, no hard cap
+ /// sliding expiration is enabled. If null, no hard cap
/// is applied.
///
- public TimeSpan MaxLifetime { get; set; } = TimeSpan.Zero;
+ public TimeSpan? MaxLifetime { get; set; }
///
/// When enabled, each refresh extends the session's expiration,
@@ -33,7 +33,7 @@ public sealed class SessionOptions
/// Maximum allowed idle time before the session becomes invalid.
/// If null or zero, idle expiration is disabled entirely.
///
- public TimeSpan? IdleTimeout { get; set; } = TimeSpan.Zero;
+ public TimeSpan? IdleTimeout { get; set; }
///
/// Maximum number of device session chains a single user may have.
diff --git a/src/CodeBeam.UltimateAuth.Core/Options/SessionOptionsValidator.cs b/src/CodeBeam.UltimateAuth.Core/Options/UAuthSessionOptionsValidator.cs
similarity index 95%
rename from src/CodeBeam.UltimateAuth.Core/Options/SessionOptionsValidator.cs
rename to src/CodeBeam.UltimateAuth.Core/Options/UAuthSessionOptionsValidator.cs
index c38d642..1d81b1d 100644
--- a/src/CodeBeam.UltimateAuth.Core/Options/SessionOptionsValidator.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Options/UAuthSessionOptionsValidator.cs
@@ -2,9 +2,9 @@
namespace CodeBeam.UltimateAuth.Core.Options
{
- internal sealed class SessionOptionsValidator : IValidateOptions
+ internal sealed class UAuthSessionOptionsValidator : IValidateOptions
{
- public ValidateOptionsResult Validate(string? name, SessionOptions options)
+ public ValidateOptionsResult Validate(string? name, UAuthSessionOptions options)
{
var errors = new List();
diff --git a/src/CodeBeam.UltimateAuth.Core/Options/TokenOptions.cs b/src/CodeBeam.UltimateAuth.Core/Options/UAuthTokenOptions.cs
similarity index 88%
rename from src/CodeBeam.UltimateAuth.Core/Options/TokenOptions.cs
rename to src/CodeBeam.UltimateAuth.Core/Options/UAuthTokenOptions.cs
index 8771aef..764eaf2 100644
--- a/src/CodeBeam.UltimateAuth.Core/Options/TokenOptions.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Options/UAuthTokenOptions.cs
@@ -5,7 +5,7 @@
/// within UltimateAuth. Includes JWT and opaque token generation,
/// lifetimes, and cryptographic settings.
///
- public sealed class TokenOptions
+ public sealed class UAuthTokenOptions
{
///
/// Determines whether JWT-format access tokens should be issued.
@@ -53,5 +53,11 @@ public sealed class TokenOptions
/// 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;
}
}
diff --git a/src/CodeBeam.UltimateAuth.Core/Options/TokenOptionsValidator.cs b/src/CodeBeam.UltimateAuth.Core/Options/UAuthTokenOptionsValidator.cs
similarity index 91%
rename from src/CodeBeam.UltimateAuth.Core/Options/TokenOptionsValidator.cs
rename to src/CodeBeam.UltimateAuth.Core/Options/UAuthTokenOptionsValidator.cs
index 563c4c0..084c4da 100644
--- a/src/CodeBeam.UltimateAuth.Core/Options/TokenOptionsValidator.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Options/UAuthTokenOptionsValidator.cs
@@ -2,9 +2,9 @@
namespace CodeBeam.UltimateAuth.Core.Options
{
- internal sealed class TokenOptionsValidator : IValidateOptions
+ internal sealed class UAuthTokenOptionsValidator : IValidateOptions
{
- public ValidateOptionsResult Validate(string? name, TokenOptions options)
+ public ValidateOptionsResult Validate(string? name, UAuthTokenOptions options)
{
var errors = new List();
diff --git a/src/CodeBeam.UltimateAuth.Server/CodeBeam.UltimateAuth.Server.csproj b/src/CodeBeam.UltimateAuth.Server/CodeBeam.UltimateAuth.Server.csproj
index 8004a0d..d4e9395 100644
--- a/src/CodeBeam.UltimateAuth.Server/CodeBeam.UltimateAuth.Server.csproj
+++ b/src/CodeBeam.UltimateAuth.Server/CodeBeam.UltimateAuth.Server.csproj
@@ -7,4 +7,16 @@
true
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/ILoginEndpointHandler.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/ILoginEndpointHandler.cs
new file mode 100644
index 0000000..6b46450
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/ILoginEndpointHandler.cs
@@ -0,0 +1,9 @@
+using Microsoft.AspNetCore.Http;
+
+namespace CodeBeam.UltimateAuth.Server.Endpoints
+{
+ public interface ILoginEndpointHandler
+ {
+ Task LoginAsync(HttpContext ctx);
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/ILogoutEndpointHandler.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/ILogoutEndpointHandler.cs
new file mode 100644
index 0000000..3185a33
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/ILogoutEndpointHandler.cs
@@ -0,0 +1,9 @@
+using Microsoft.AspNetCore.Http;
+
+namespace CodeBeam.UltimateAuth.Server.Endpoints
+{
+ public interface ILogoutEndpointHandler
+ {
+ Task LogoutAsync(HttpContext ctx);
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IPkceEndpointHandler.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IPkceEndpointHandler.cs
new file mode 100644
index 0000000..d9324a4
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IPkceEndpointHandler.cs
@@ -0,0 +1,11 @@
+using Microsoft.AspNetCore.Http;
+
+namespace CodeBeam.UltimateAuth.Server.Endpoints
+{
+ public interface IPkceEndpointHandler
+ {
+ Task CreateAsync(HttpContext ctx);
+ Task VerifyAsync(HttpContext ctx);
+ Task ConsumeAsync(HttpContext ctx);
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IReauthEndpointHandler.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IReauthEndpointHandler.cs
new file mode 100644
index 0000000..4de1bae
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IReauthEndpointHandler.cs
@@ -0,0 +1,9 @@
+using Microsoft.AspNetCore.Http;
+
+namespace CodeBeam.UltimateAuth.Server.Endpoints
+{
+ public interface IReauthEndpointHandler
+ {
+ Task ReauthAsync(HttpContext ctx);
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/ISessionManagementHandler.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/ISessionManagementHandler.cs
new file mode 100644
index 0000000..1d5c928
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/ISessionManagementHandler.cs
@@ -0,0 +1,12 @@
+using Microsoft.AspNetCore.Http;
+
+namespace CodeBeam.UltimateAuth.Server.Endpoints
+{
+ public interface ISessionManagementHandler
+ {
+ Task GetCurrentSessionAsync(HttpContext ctx);
+ Task GetAllSessionsAsync(HttpContext ctx);
+ Task RevokeSessionAsync(string sessionId, HttpContext ctx);
+ Task RevokeAllAsync(HttpContext ctx);
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/ISessionRefreshEndpointHandler.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/ISessionRefreshEndpointHandler.cs
new file mode 100644
index 0000000..15261fd
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/ISessionRefreshEndpointHandler.cs
@@ -0,0 +1,9 @@
+using Microsoft.AspNetCore.Http;
+
+namespace CodeBeam.UltimateAuth.Server.Endpoints
+{
+ public interface ISessionRefreshEndpointHandler
+ {
+ Task RefreshSessionAsync(HttpContext ctx);
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/ITokenEndpointHandler.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/ITokenEndpointHandler.cs
new file mode 100644
index 0000000..e69a1e5
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/ITokenEndpointHandler.cs
@@ -0,0 +1,12 @@
+using Microsoft.AspNetCore.Http;
+
+namespace CodeBeam.UltimateAuth.Server.Endpoints
+{
+ public interface ITokenEndpointHandler
+ {
+ Task GetTokenAsync(HttpContext ctx);
+ Task RefreshTokenAsync(HttpContext ctx);
+ Task IntrospectAsync(HttpContext ctx);
+ Task RevokeAsync(HttpContext ctx);
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IUserInfoEndpointHandler.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IUserInfoEndpointHandler.cs
new file mode 100644
index 0000000..54c0eae
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IUserInfoEndpointHandler.cs
@@ -0,0 +1,11 @@
+using Microsoft.AspNetCore.Http;
+
+namespace CodeBeam.UltimateAuth.Server.Endpoints
+{
+ public interface IUserInfoEndpointHandler
+ {
+ Task GetUserInfoAsync(HttpContext ctx);
+ Task GetPermissionsAsync(HttpContext ctx);
+ Task CheckPermissionAsync(HttpContext ctx);
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultLoginEndpointHandler.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultLoginEndpointHandler.cs
new file mode 100644
index 0000000..2b94841
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultLoginEndpointHandler.cs
@@ -0,0 +1,12 @@
+using Microsoft.AspNetCore.Http;
+
+namespace CodeBeam.UltimateAuth.Server.Endpoints
+{
+ public class DefaultLoginEndpointHandler : ILoginEndpointHandler
+ {
+ public Task LoginAsync(HttpContext ctx)
+ {
+ return Task.FromResult(Results.StatusCode(StatusCodes.Status501NotImplemented));
+ }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultPkceEndpointHandler.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultPkceEndpointHandler.cs
new file mode 100644
index 0000000..873a6fb
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultPkceEndpointHandler.cs
@@ -0,0 +1,16 @@
+using Microsoft.AspNetCore.Http;
+
+namespace CodeBeam.UltimateAuth.Server.Endpoints
+{
+ public class DefaultPkceEndpointHandler : IPkceEndpointHandler
+ {
+ public Task CreateAsync(HttpContext ctx)
+ => Task.FromResult(Results.StatusCode(StatusCodes.Status501NotImplemented));
+
+ public Task VerifyAsync(HttpContext ctx)
+ => Task.FromResult(Results.StatusCode(StatusCodes.Status501NotImplemented));
+
+ public Task ConsumeAsync(HttpContext ctx)
+ => Task.FromResult(Results.StatusCode(StatusCodes.Status501NotImplemented));
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/EndpointEnablement.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/EndpointEnablement.cs
new file mode 100644
index 0000000..7863eae
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/EndpointEnablement.cs
@@ -0,0 +1,8 @@
+namespace CodeBeam.UltimateAuth.Server.Endpoints
+{
+ internal static class EndpointEnablement
+ {
+ public static bool Resolve(bool? overrideValue, bool modeDefault)
+ => overrideValue ?? modeDefault;
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/UAuthEndpointDefaults.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/UAuthEndpointDefaults.cs
new file mode 100644
index 0000000..3eea6f6
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/UAuthEndpointDefaults.cs
@@ -0,0 +1,15 @@
+namespace CodeBeam.UltimateAuth.Server
+{
+ ///
+ /// Represents which endpoint groups are enabled by default
+ /// for a given authentication mode.
+ ///
+ public sealed class UAuthEndpointDefaults
+ {
+ public bool Login { get; init; }
+ public bool Pkce { get; init; }
+ public bool Token { get; init; }
+ public bool Session { get; init; }
+ public bool UserInfo { get; init; }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/UAuthEndpointDefaultsMap.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/UAuthEndpointDefaultsMap.cs
new file mode 100644
index 0000000..0a7b6ff
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/UAuthEndpointDefaultsMap.cs
@@ -0,0 +1,56 @@
+using CodeBeam.UltimateAuth.Core;
+
+namespace CodeBeam.UltimateAuth.Server.Endpoints
+{
+ ///
+ /// Provides default endpoint enablement rules based on UAuthMode.
+ /// These defaults represent the secure and meaningful surface
+ /// for each authentication strategy.
+ ///
+ internal static class UAuthEndpointDefaultsMap
+ {
+ public static UAuthEndpointDefaults ForMode(UAuthMode mode)
+ {
+ return mode switch
+ {
+ UAuthMode.PureOpaque => new UAuthEndpointDefaults
+ {
+ Login = true,
+ Pkce = false,
+ Token = false,
+ Session = true,
+ UserInfo = true
+ },
+
+ UAuthMode.Hybrid => new UAuthEndpointDefaults
+ {
+ Login = true,
+ Pkce = true,
+ Token = true,
+ Session = true,
+ UserInfo = true
+ },
+
+ UAuthMode.SemiHybrid => new UAuthEndpointDefaults
+ {
+ Login = true,
+ Pkce = true,
+ Token = true,
+ Session = false,
+ UserInfo = true
+ },
+
+ UAuthMode.PureJwt => new UAuthEndpointDefaults
+ {
+ Login = true,
+ Pkce = false,
+ Token = true,
+ Session = false,
+ UserInfo = true
+ },
+
+ _ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null)
+ };
+ }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/UAuthEndpointRegistrar.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/UAuthEndpointRegistrar.cs
new file mode 100644
index 0000000..5169cd5
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/UAuthEndpointRegistrar.cs
@@ -0,0 +1,109 @@
+using CodeBeam.UltimateAuth.Server.Options;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing;
+
+namespace CodeBeam.UltimateAuth.Server.Endpoints
+{
+ public interface IAuthEndpointRegistrar
+ {
+ void MapEndpoints(RouteGroupBuilder rootGroup, UAuthServerOptions options);
+ }
+
+ public class UAuthEndpointRegistrar : IAuthEndpointRegistrar
+ {
+ public void MapEndpoints(RouteGroupBuilder rootGroup, UAuthServerOptions options)
+ {
+ var defaults = UAuthEndpointDefaultsMap.ForMode(options.Mode);
+
+ // Base: /auth
+ string basePrefix = options.RoutePrefix.TrimStart('/');
+
+ bool useRouteTenant =
+ options.MultiTenant.Enabled &&
+ options.MultiTenant.EnableRoute;
+
+ RouteGroupBuilder group = useRouteTenant
+ ? rootGroup.MapGroup("/{tenant}/" + basePrefix)
+ : rootGroup.MapGroup("/" + basePrefix);
+
+ if (EndpointEnablement.Resolve(options.EnablePkceEndpoints, defaults.Pkce))
+ {
+ var pkce = group.MapGroup("/pkce");
+
+ pkce.MapPost("/create", async (IPkceEndpointHandler h, HttpContext ctx)
+ => await h.CreateAsync(ctx));
+
+ pkce.MapPost("/verify", async (IPkceEndpointHandler h, HttpContext ctx)
+ => await h.VerifyAsync(ctx));
+
+ pkce.MapPost("/consume", async (IPkceEndpointHandler h, HttpContext ctx)
+ => await h.ConsumeAsync(ctx));
+ }
+
+ if (EndpointEnablement.Resolve(options.EnableLoginEndpoints, defaults.Login))
+ {
+ group.MapPost("/login", async (ILoginEndpointHandler h, HttpContext ctx)
+ => await h.LoginAsync(ctx));
+
+ group.MapPost("/logout", async (ILogoutEndpointHandler h, HttpContext ctx)
+ => await h.LogoutAsync(ctx));
+
+ group.MapPost("/refresh-session", async (ISessionRefreshEndpointHandler h, HttpContext ctx)
+ => await h.RefreshSessionAsync(ctx));
+
+ group.MapPost("/reauth", async (IReauthEndpointHandler h, HttpContext ctx)
+ => await h.ReauthAsync(ctx));
+ }
+
+ if (EndpointEnablement.Resolve(options.EnableTokenEndpoints, defaults.Token))
+ {
+ var token = group.MapGroup("");
+
+ token.MapPost("/token", async (ITokenEndpointHandler h, HttpContext ctx)
+ => await h.GetTokenAsync(ctx));
+
+ token.MapPost("/refresh-token", async (ITokenEndpointHandler h, HttpContext ctx)
+ => await h.RefreshTokenAsync(ctx));
+
+ token.MapPost("/introspect", async (ITokenEndpointHandler h, HttpContext ctx)
+ => await h.IntrospectAsync(ctx));
+
+ token.MapPost("/revoke", async (ITokenEndpointHandler h, HttpContext ctx)
+ => await h.RevokeAsync(ctx));
+ }
+
+ if (EndpointEnablement.Resolve(options.EnableSessionEndpoints, defaults.Session))
+ {
+ var session = group.MapGroup("/session");
+
+ session.MapGet("/current", async (ISessionManagementHandler h, HttpContext ctx)
+ => await h.GetCurrentSessionAsync(ctx));
+
+ session.MapGet("/list", async (ISessionManagementHandler h, HttpContext ctx)
+ => await h.GetAllSessionsAsync(ctx));
+
+ session.MapPost("/revoke/{sessionId}", async (ISessionManagementHandler h, string sessionId, HttpContext ctx)
+ => await h.RevokeSessionAsync(sessionId, ctx));
+
+ session.MapPost("/revoke-all", async (ISessionManagementHandler h, HttpContext ctx)
+ => await h.RevokeAllAsync(ctx));
+ }
+
+ if (EndpointEnablement.Resolve(options.EnableUserInfoEndpoints, defaults.UserInfo))
+ {
+ var user = group.MapGroup("");
+
+ user.MapGet("/userinfo", async (IUserInfoEndpointHandler h, HttpContext ctx)
+ => await h.GetUserInfoAsync(ctx));
+
+ user.MapGet("/permissions", async (IUserInfoEndpointHandler h, HttpContext ctx)
+ => await h.GetPermissionsAsync(ctx));
+
+ user.MapPost("/permissions/check", async (IUserInfoEndpointHandler h, HttpContext ctx)
+ => await h.CheckPermissionAsync(ctx));
+ }
+ }
+
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Extensions/HttpContextSessionExtensions.cs b/src/CodeBeam.UltimateAuth.Server/Extensions/HttpContextSessionExtensions.cs
new file mode 100644
index 0000000..474f783
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Extensions/HttpContextSessionExtensions.cs
@@ -0,0 +1,24 @@
+using CodeBeam.UltimateAuth.Server.Middlewares;
+using CodeBeam.UltimateAuth.Server.Sessions;
+using Microsoft.AspNetCore.Http;
+
+namespace CodeBeam.UltimateAuth.Server.Extensions
+{
+ public static class HttpContextSessionExtensions
+ {
+ public static SessionContext GetSessionContext(
+ this HttpContext context)
+ {
+ if (context.Items.TryGetValue(
+ SessionResolutionMiddleware.SessionContextKey,
+ out var value)
+ && value is SessionContext session)
+ {
+ return session;
+ }
+
+ return SessionContext.Anonymous();
+ }
+ }
+
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Extensions/HttpContextTenantExtensions.cs b/src/CodeBeam.UltimateAuth.Server/Extensions/HttpContextTenantExtensions.cs
new file mode 100644
index 0000000..6a74f8a
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Extensions/HttpContextTenantExtensions.cs
@@ -0,0 +1,27 @@
+using CodeBeam.UltimateAuth.Core.MultiTenancy;
+using CodeBeam.UltimateAuth.Server.Middlewares;
+using Microsoft.AspNetCore.Http;
+
+namespace CodeBeam.UltimateAuth.Server.Extensions
+{
+ public static class HttpContextTenantExtensions
+ {
+ public static string? GetTenantId(this HttpContext ctx)
+ {
+ return ctx.GetTenantContext().TenantId;
+ }
+
+ public static UAuthTenantContext GetTenantContext(this HttpContext ctx)
+ {
+ if (ctx.Items.TryGetValue(
+ TenantMiddleware.TenantContextKey,
+ out var value)
+ && value is UAuthTenantContext tenant)
+ {
+ return tenant;
+ }
+
+ return UAuthTenantContext.NotResolved();
+ }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Extensions/TenantResolutionContextExtensions.cs b/src/CodeBeam.UltimateAuth.Server/Extensions/TenantResolutionContextExtensions.cs
new file mode 100644
index 0000000..11a98d1
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Extensions/TenantResolutionContextExtensions.cs
@@ -0,0 +1,25 @@
+//using Microsoft.AspNetCore.Http;
+//using CodeBeam.UltimateAuth.Core.MultiTenancy;
+
+//namespace CodeBeam.UltimateAuth.Server.MultiTenancy
+//{
+// public static class TenantResolutionContextExtensions
+// {
+// public static TenantResolutionContext FromHttpContext(this HttpContext ctx)
+// {
+// var headers = ctx.Request.Headers
+// .ToDictionary(
+// h => h.Key,
+// h => h.Value.ToString(),
+// StringComparer.OrdinalIgnoreCase);
+
+// return new TenantResolutionContext
+// {
+// Headers = headers,
+// Host = ctx.Request.Host.Host,
+// Path = ctx.Request.Path.Value,
+// RawContext = ctx
+// };
+// }
+// }
+//}
diff --git a/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthApplicationBuilderExtensions.cs b/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthApplicationBuilderExtensions.cs
new file mode 100644
index 0000000..1d0c4fe
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthApplicationBuilderExtensions.cs
@@ -0,0 +1,19 @@
+using CodeBeam.UltimateAuth.Server.Middlewares;
+using Microsoft.AspNetCore.Builder;
+
+namespace CodeBeam.UltimateAuth.Server.Extensions
+{
+ public static class UltimateAuthApplicationBuilderExtensions
+ {
+ public static IApplicationBuilder UseUltimateAuthServer(
+ this IApplicationBuilder app)
+ {
+ app.UseMiddleware();
+ app.UseMiddleware();
+ app.UseMiddleware();
+
+ return app;
+ }
+ }
+
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthServerServiceCollectionExtensions.cs b/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthServerServiceCollectionExtensions.cs
new file mode 100644
index 0000000..8520014
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthServerServiceCollectionExtensions.cs
@@ -0,0 +1,105 @@
+using CodeBeam.UltimateAuth.Core.Extensions;
+using CodeBeam.UltimateAuth.Core.MultiTenancy;
+using CodeBeam.UltimateAuth.Core.Options;
+using CodeBeam.UltimateAuth.Server.Endpoints;
+using CodeBeam.UltimateAuth.Server.Issuers;
+using CodeBeam.UltimateAuth.Server.MultiTenancy;
+using CodeBeam.UltimateAuth.Server.Options;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Options;
+
+namespace CodeBeam.UltimateAuth.Server.Extensions
+{
+ public static class UAuthServerServiceCollectionExtensions
+ {
+ public static IServiceCollection AddUltimateAuthServer(
+ this IServiceCollection services)
+ {
+ services.AddUltimateAuth(); // Core
+ return services.AddUltimateAuthServerInternal();
+ }
+
+ public static IServiceCollection AddUltimateAuthServer(
+ this IServiceCollection services,
+ IConfiguration configuration)
+ {
+ services.AddUltimateAuth(configuration); // Core
+ services.Configure(
+ configuration.GetSection("UltimateAuth:Server"));
+
+ services.Configure(
+ configuration.GetSection("UltimateAuth:SessionResolution"));
+
+ return services.AddUltimateAuthServerInternal();
+ }
+
+ public static IServiceCollection AddUltimateAuthServer(
+ this IServiceCollection services,
+ Action configure)
+ {
+ services.AddUltimateAuth(); // Core
+ services.Configure(configure);
+
+ return services.AddUltimateAuthServerInternal();
+ }
+
+ private static IServiceCollection AddUltimateAuthServerInternal(
+ this IServiceCollection services)
+ {
+ // -----------------------------
+ // OPTIONS VALIDATION
+ // -----------------------------
+ services.TryAddEnumerable(
+ ServiceDescriptor.Singleton<
+ IValidateOptions,
+ UAuthServerOptionsValidator>());
+
+ // -----------------------------
+ // TENANT RESOLUTION
+ // -----------------------------
+ services.TryAddSingleton(sp =>
+ {
+ var opts = sp.GetRequiredService>().Value;
+
+ var resolvers = new List();
+
+ if (opts.EnableRoute)
+ resolvers.Add(new PathTenantResolver());
+
+ if (opts.EnableHeader)
+ resolvers.Add(new HeaderTenantResolver(opts.HeaderName));
+
+ if (opts.EnableDomain)
+ resolvers.Add(new HostTenantResolver());
+
+ return resolvers.Count switch
+ {
+ 0 => new FixedTenantResolver(opts.DefaultTenantId ?? "default"),
+ 1 => resolvers[0],
+ _ => new CompositeTenantResolver(resolvers)
+ };
+ });
+
+ services.TryAddScoped();
+
+ // -----------------------------
+ // SESSION / TOKEN ISSUERS
+ // -----------------------------
+ services.TryAddScoped(
+ typeof(UAuthSessionIssuer<>),
+ typeof(UAuthSessionIssuer<>));
+
+ services.TryAddScoped();
+
+ // -----------------------------
+ // ENDPOINTS
+ // -----------------------------
+ services.TryAddSingleton();
+
+ return services;
+ }
+
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Issuers/UAuthSessionIssuer.cs b/src/CodeBeam.UltimateAuth.Server/Issuers/UAuthSessionIssuer.cs
new file mode 100644
index 0000000..3a224fa
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Issuers/UAuthSessionIssuer.cs
@@ -0,0 +1,70 @@
+using CodeBeam.UltimateAuth.Core;
+using CodeBeam.UltimateAuth.Core.Abstractions;
+using CodeBeam.UltimateAuth.Core.Contexts;
+using CodeBeam.UltimateAuth.Core.Domain;
+using CodeBeam.UltimateAuth.Core.Domain.Session;
+using CodeBeam.UltimateAuth.Server.Options;
+using Microsoft.Extensions.Options;
+
+namespace CodeBeam.UltimateAuth.Server.Issuers
+{
+ ///
+ /// UltimateAuth session issuer responsible for creating
+ /// opaque authentication sessions.
+ ///
+ public sealed class UAuthSessionIssuer : ISessionIssuer
+ {
+ private readonly IOpaqueTokenGenerator _opaqueGenerator;
+ private readonly UAuthServerOptions _options;
+
+ public UAuthSessionIssuer(IOpaqueTokenGenerator opaqueGenerator, IOptions options)
+ {
+ _opaqueGenerator = opaqueGenerator;
+ _options = options.Value;
+ }
+
+ public Task> IssueAsync(
+ SessionIssueContext context,
+ UAuthSessionChain chain,
+ CancellationToken cancellationToken = default)
+ {
+ if (_options.Mode == UAuthMode.PureJwt)
+ {
+ throw new InvalidOperationException(
+ "Session issuer cannot be used in PureJwt mode.");
+ }
+
+ var opaqueSessionId = _opaqueGenerator.Generate();
+ var expiresAt = context.Now.Add(_options.Session.Lifetime);
+
+ if (_options.Session.MaxLifetime is not null)
+ {
+ var absoluteExpiry =
+ context.Now.Add(_options.Session.MaxLifetime.Value);
+
+ if (absoluteExpiry < expiresAt)
+ {
+ expiresAt = absoluteExpiry;
+ }
+ }
+
+ var session = UAuthSession.Create(
+ sessionId: new AuthSessionId(opaqueSessionId),
+ tenantId: context.TenantId,
+ userId: context.UserId,
+ now: context.Now,
+ expiresAt: expiresAt,
+ securityVersion: context.SecurityVersion,
+ device: context.Device,
+ metadata: SessionMetadata.Empty
+ );
+
+ return Task.FromResult(new IssuedSession
+ {
+ Session = session,
+ OpaqueSessionId = opaqueSessionId,
+ IsMetadataOnly = _options.Mode == UAuthMode.SemiHybrid
+ });
+ }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Issuers/UAuthTokenIssuer.cs b/src/CodeBeam.UltimateAuth.Server/Issuers/UAuthTokenIssuer.cs
new file mode 100644
index 0000000..46d6e3f
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Issuers/UAuthTokenIssuer.cs
@@ -0,0 +1,126 @@
+using CodeBeam.UltimateAuth.Core;
+using CodeBeam.UltimateAuth.Core.Abstractions;
+using CodeBeam.UltimateAuth.Core.Contexts;
+using CodeBeam.UltimateAuth.Core.Domain;
+using CodeBeam.UltimateAuth.Server.Options;
+using Microsoft.Extensions.Options;
+using System.Security.Claims;
+
+namespace CodeBeam.UltimateAuth.Server.Issuers
+{
+ ///
+ /// Default UltimateAuth token issuer.
+ /// Opinionated implementation of ITokenIssuer.
+ /// Mode-aware (PureOpaque, Hybrid, SemiHybrid, PureJwt).
+ ///
+ public sealed class UAuthTokenIssuer : ITokenIssuer
+ {
+ private readonly IOpaqueTokenGenerator _opaqueGenerator;
+ private readonly IJwtTokenGenerator _jwtGenerator;
+ private readonly ITokenHasher _tokenHasher;
+ private readonly UAuthServerOptions _options;
+
+ public UAuthTokenIssuer(IOpaqueTokenGenerator opaqueGenerator, IJwtTokenGenerator jwtGenerator, ITokenHasher tokenHasher, IOptions options)
+ {
+ _opaqueGenerator = opaqueGenerator;
+ _jwtGenerator = jwtGenerator;
+ _tokenHasher = tokenHasher;
+ _options = options.Value;
+ }
+
+ public Task IssueAccessTokenAsync(TokenIssueContext context, CancellationToken cancellationToken = default)
+ {
+ var now = DateTimeOffset.UtcNow;
+ var expires = now.Add(_options.Tokens.AccessTokenLifetime);
+
+ return _options.Mode switch
+ {
+ UAuthMode.PureOpaque => Task.FromResult(IssueOpaqueAccessToken(
+ expires,
+ context.SessionId)),
+
+ UAuthMode.Hybrid or
+ UAuthMode.SemiHybrid or
+ UAuthMode.PureJwt => Task.FromResult(IssueJwtAccessToken(
+ context,
+ expires)),
+
+ _ => throw new InvalidOperationException(
+ $"Unsupported auth mode: {_options.Mode}")
+ };
+ }
+
+ public Task IssueRefreshTokenAsync(TokenIssueContext context, CancellationToken cancellationToken = default)
+ {
+ if (_options.Mode == UAuthMode.PureOpaque)
+ return Task.FromResult(null);
+
+ var now = DateTimeOffset.UtcNow;
+ var expires = now.Add(_options.Tokens.RefreshTokenLifetime);
+
+ string token = _opaqueGenerator.Generate();
+ string hash = _tokenHasher.Hash(token);
+
+ return Task.FromResult(new IssuedRefreshToken
+ {
+ Token = token,
+ TokenHash = hash,
+ ExpiresAt = expires
+ });
+ }
+
+ private IssuedAccessToken IssueOpaqueAccessToken(DateTimeOffset expires, string? sessionId)
+ {
+ string token = _opaqueGenerator.Generate();
+
+ return new IssuedAccessToken
+ {
+ Token = token,
+ TokenType = "opaque",
+ ExpiresAt = expires,
+ SessionId = sessionId
+ };
+ }
+
+ private IssuedAccessToken IssueJwtAccessToken(TokenIssueContext context, DateTimeOffset expires)
+ {
+ var claims = new List
+ {
+ new Claim(ClaimTypes.NameIdentifier, context.UserId),
+ new Claim("tenant", context.TenantId)
+ };
+
+ claims.AddRange(context.Claims);
+
+ if (!string.IsNullOrWhiteSpace(context.SessionId))
+ {
+ claims.Add(new Claim("sid", context.SessionId));
+ }
+
+ if (_options.Tokens.AddJwtIdClaim)
+ {
+ string jti = _opaqueGenerator.Generate(16); // shorter is fine
+ claims.Add(new Claim("jti", jti));
+ }
+
+ var descriptor = new UAuthJwtTokenDescriptor
+ {
+ Subject = new ClaimsIdentity(claims),
+ Issuer = _options.Tokens.Issuer,
+ Audience = _options.Tokens.Audience,
+ Expires = expires.UtcDateTime,
+ SigningKey = _options.Tokens.SigningKey
+ };
+
+ string jwt = _jwtGenerator.CreateToken(descriptor);
+
+ return new IssuedAccessToken
+ {
+ Token = jwt,
+ TokenType = "jwt",
+ ExpiresAt = expires,
+ SessionId = context.SessionId
+ };
+ }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Middlewares/SessionResolutionMiddleware.cs b/src/CodeBeam.UltimateAuth.Server/Middlewares/SessionResolutionMiddleware.cs
new file mode 100644
index 0000000..3ec6a3b
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Middlewares/SessionResolutionMiddleware.cs
@@ -0,0 +1,38 @@
+using CodeBeam.UltimateAuth.Server.Extensions;
+using CodeBeam.UltimateAuth.Server.Sessions;
+using Microsoft.AspNetCore.Http;
+
+namespace CodeBeam.UltimateAuth.Server.Middlewares
+{
+ public sealed class SessionResolutionMiddleware
+ {
+ private readonly RequestDelegate _next;
+ private readonly ISessionIdResolver _sessionIdResolver;
+
+ public const string SessionContextKey = "__UAuthSession";
+
+ public SessionResolutionMiddleware(
+ RequestDelegate next,
+ ISessionIdResolver sessionIdResolver)
+ {
+ _next = next;
+ _sessionIdResolver = sessionIdResolver;
+ }
+
+ public async Task InvokeAsync(HttpContext context)
+ {
+ var tenant = context.GetTenantContext();
+ var sessionId = _sessionIdResolver.Resolve(context);
+
+ var sessionContext = sessionId is null
+ ? SessionContext.Anonymous()
+ : SessionContext.FromSessionId(
+ sessionId.Value,
+ tenant.TenantId);
+
+ context.Items[SessionContextKey] = sessionContext;
+
+ await _next(context);
+ }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Middlewares/TenantMiddleware.cs b/src/CodeBeam.UltimateAuth.Server/Middlewares/TenantMiddleware.cs
new file mode 100644
index 0000000..6650b1f
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Middlewares/TenantMiddleware.cs
@@ -0,0 +1,53 @@
+using CodeBeam.UltimateAuth.Core.MultiTenancy;
+using CodeBeam.UltimateAuth.Core.Options;
+using CodeBeam.UltimateAuth.Server.MultiTenancy;
+using Microsoft.AspNetCore.Http;
+
+namespace CodeBeam.UltimateAuth.Server.Middlewares
+{
+ public sealed class TenantMiddleware
+ {
+ private readonly RequestDelegate _next;
+ private readonly ITenantResolver _resolver;
+ private readonly UAuthMultiTenantOptions _options;
+
+ public const string TenantContextKey = "__UAuthTenant";
+
+ public TenantMiddleware(
+ RequestDelegate next,
+ ITenantResolver resolver,
+ UAuthMultiTenantOptions options)
+ {
+ _next = next;
+ _resolver = resolver;
+ _options = options;
+ }
+
+ public async Task InvokeAsync(HttpContext context)
+ {
+ UAuthTenantContext tenantContext;
+
+ if (!_options.Enabled)
+ {
+ // Single-tenant mode → tenant concept disabled
+ tenantContext = UAuthTenantContext.NotResolved();
+ }
+ else
+ {
+ tenantContext = await _resolver.ResolveAsync(context);
+
+ if (_options.RequireTenant && !tenantContext.IsResolved)
+ {
+ context.Response.StatusCode = StatusCodes.Status400BadRequest;
+ await context.Response.WriteAsync(
+ "Tenant is required but could not be resolved.");
+ return;
+ }
+ }
+
+ context.Items[TenantContextKey] = tenantContext;
+
+ await _next(context);
+ }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Middlewares/UserMiddleware.cs b/src/CodeBeam.UltimateAuth.Server/Middlewares/UserMiddleware.cs
new file mode 100644
index 0000000..aa87a46
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Middlewares/UserMiddleware.cs
@@ -0,0 +1,27 @@
+using CodeBeam.UltimateAuth.Server.Users;
+using Microsoft.AspNetCore.Http;
+
+namespace CodeBeam.UltimateAuth.Server.Middlewares
+{
+ public sealed class UserMiddleware
+ {
+ private readonly RequestDelegate _next;
+ private readonly IUserAccessor _userAccessor;
+
+ public const string UserContextKey = "__UAuthUser";
+
+ public UserMiddleware(
+ RequestDelegate next,
+ IUserAccessor userAccessor)
+ {
+ _next = next;
+ _userAccessor = userAccessor;
+ }
+
+ public async Task InvokeAsync(HttpContext context)
+ {
+ await _userAccessor.ResolveAsync(context);
+ await _next(context);
+ }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/MultiTenancy/DomainTenantAdapter.cs b/src/CodeBeam.UltimateAuth.Server/MultiTenancy/DomainTenantAdapter.cs
new file mode 100644
index 0000000..88b3c74
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/MultiTenancy/DomainTenantAdapter.cs
@@ -0,0 +1,34 @@
+using CodeBeam.UltimateAuth.Core.MultiTenancy;
+using CodeBeam.UltimateAuth.Core.Options;
+using Microsoft.AspNetCore.Http;
+
+namespace CodeBeam.UltimateAuth.Server.MultiTenancy
+{
+ public sealed class DomainTenantAdapter : ITenantResolver
+ {
+ private readonly ITenantIdResolver _coreResolver;
+ private readonly UAuthMultiTenantOptions _options;
+
+ public DomainTenantAdapter(
+ HostTenantResolver coreResolver,
+ UAuthMultiTenantOptions options)
+ {
+ _coreResolver = coreResolver;
+ _options = options;
+ }
+
+ public async Task ResolveAsync(HttpContext ctx)
+ {
+ if (!_options.Enabled)
+ return UAuthTenantContext.NotResolved();
+
+ var resolutionContext = TenantResolutionContextFactory.FromHttpContext(ctx);
+ var tenantId = await _coreResolver.ResolveTenantIdAsync(resolutionContext);
+
+ if (tenantId is null)
+ return UAuthTenantContext.NotResolved();
+
+ return UAuthTenantContextFactory.Create(tenantId, _options);
+ }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/MultiTenancy/HeaderTenantAdapter.cs b/src/CodeBeam.UltimateAuth.Server/MultiTenancy/HeaderTenantAdapter.cs
new file mode 100644
index 0000000..8250cdb
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/MultiTenancy/HeaderTenantAdapter.cs
@@ -0,0 +1,34 @@
+using CodeBeam.UltimateAuth.Core.MultiTenancy;
+using CodeBeam.UltimateAuth.Core.Options;
+using Microsoft.AspNetCore.Http;
+
+namespace CodeBeam.UltimateAuth.Server.MultiTenancy
+{
+ public sealed class HeaderTenantAdapter : ITenantResolver
+ {
+ private readonly ITenantIdResolver _coreResolver;
+ private readonly UAuthMultiTenantOptions _options;
+
+ public HeaderTenantAdapter(
+ HeaderTenantResolver coreResolver,
+ UAuthMultiTenantOptions options)
+ {
+ _coreResolver = coreResolver;
+ _options = options;
+ }
+
+ public async Task ResolveAsync(HttpContext ctx)
+ {
+ if (!_options.Enabled)
+ return UAuthTenantContext.NotResolved();
+
+ var resolutionContext = TenantResolutionContextFactory.FromHttpContext(ctx);
+
+ var tenantId = await _coreResolver.ResolveTenantIdAsync(resolutionContext);
+ if (tenantId is null)
+ return UAuthTenantContext.NotResolved();
+
+ return UAuthTenantContextFactory.Create(tenantId, _options);
+ }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/MultiTenancy/ITenantResolver.cs b/src/CodeBeam.UltimateAuth.Server/MultiTenancy/ITenantResolver.cs
new file mode 100644
index 0000000..60b1223
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/MultiTenancy/ITenantResolver.cs
@@ -0,0 +1,11 @@
+using CodeBeam.UltimateAuth.Core.MultiTenancy;
+using Microsoft.AspNetCore.Http;
+
+namespace CodeBeam.UltimateAuth.Server.MultiTenancy
+{
+ public interface ITenantResolver
+ {
+ Task ResolveAsync(HttpContext ctx);
+ }
+}
+
diff --git a/src/CodeBeam.UltimateAuth.Server/MultiTenancy/RouteTenantAdapter.cs b/src/CodeBeam.UltimateAuth.Server/MultiTenancy/RouteTenantAdapter.cs
new file mode 100644
index 0000000..e69719a
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/MultiTenancy/RouteTenantAdapter.cs
@@ -0,0 +1,32 @@
+using CodeBeam.UltimateAuth.Core.MultiTenancy;
+using CodeBeam.UltimateAuth.Core.Options;
+using CodeBeam.UltimateAuth.Server.MultiTenancy;
+using Microsoft.AspNetCore.Http;
+
+public sealed class RouteTenantAdapter : ITenantResolver
+{
+ private readonly ITenantIdResolver _coreResolver;
+ private readonly UAuthMultiTenantOptions _options;
+
+ public RouteTenantAdapter(
+ PathTenantResolver coreResolver,
+ UAuthMultiTenantOptions options)
+ {
+ _coreResolver = coreResolver;
+ _options = options;
+ }
+
+ public async Task ResolveAsync(HttpContext ctx)
+ {
+ if (!_options.Enabled)
+ return UAuthTenantContext.NotResolved();
+
+ var resolutionContext = TenantResolutionContextFactory.FromHttpContext(ctx);
+ var tenantId = await _coreResolver.ResolveTenantIdAsync(resolutionContext);
+
+ if (tenantId is null)
+ return UAuthTenantContext.NotResolved();
+
+ return UAuthTenantContextFactory.Create(tenantId, _options);
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/MultiTenancy/TenantResolutionContextFactory.cs b/src/CodeBeam.UltimateAuth.Server/MultiTenancy/TenantResolutionContextFactory.cs
new file mode 100644
index 0000000..ae7457a
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/MultiTenancy/TenantResolutionContextFactory.cs
@@ -0,0 +1,31 @@
+using CodeBeam.UltimateAuth.Core.MultiTenancy;
+using Microsoft.AspNetCore.Http;
+
+namespace CodeBeam.UltimateAuth.Server.MultiTenancy
+{
+ public static class TenantResolutionContextFactory
+ {
+ public static TenantResolutionContext FromHttpContext(HttpContext ctx)
+ {
+ var headers = ctx.Request.Headers
+ .ToDictionary(
+ h => h.Key,
+ h => h.Value.ToString(),
+ StringComparer.OrdinalIgnoreCase);
+
+ var query = ctx.Request.Query
+ .ToDictionary(
+ q => q.Key,
+ q => q.Value.ToString(),
+ StringComparer.OrdinalIgnoreCase);
+
+ return TenantResolutionContext.Create(
+ headers: headers,
+ Query: query,
+ host: ctx.Request.Host.Host,
+ path: ctx.Request.Path.Value,
+ rawContext: ctx
+ );
+ }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/MultiTenancy/UAuthTenantContextFactory.cs b/src/CodeBeam.UltimateAuth.Server/MultiTenancy/UAuthTenantContextFactory.cs
new file mode 100644
index 0000000..c9531f0
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/MultiTenancy/UAuthTenantContextFactory.cs
@@ -0,0 +1,22 @@
+using CodeBeam.UltimateAuth.Core.MultiTenancy;
+using CodeBeam.UltimateAuth.Core.Options;
+using System.Text.RegularExpressions;
+
+public static class UAuthTenantContextFactory
+{
+ public static UAuthTenantContext Create(
+ string tenantId,
+ UAuthMultiTenantOptions options)
+ {
+ if (options.NormalizeToLowercase)
+ tenantId = tenantId.ToLowerInvariant();
+
+ if (!Regex.IsMatch(tenantId, options.TenantIdRegex))
+ return UAuthTenantContext.NotResolved();
+
+ if (options.ReservedTenantIds.Contains(tenantId))
+ return UAuthTenantContext.NotResolved();
+
+ return UAuthTenantContext.Resolved(tenantId);
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/MultiTenancy/UAuthTenantResolver.cs b/src/CodeBeam.UltimateAuth.Server/MultiTenancy/UAuthTenantResolver.cs
new file mode 100644
index 0000000..2988d91
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/MultiTenancy/UAuthTenantResolver.cs
@@ -0,0 +1,75 @@
+using CodeBeam.UltimateAuth.Core.MultiTenancy;
+using CodeBeam.UltimateAuth.Core.Options;
+using Microsoft.AspNetCore.Http;
+
+namespace CodeBeam.UltimateAuth.Server.MultiTenancy
+{
+ ///
+ /// Server-level tenant resolver.
+ /// Responsible for executing core tenant id resolvers and
+ /// applying UltimateAuth tenant policies.
+ ///
+ public sealed class UAuthTenantResolver : ITenantResolver
+ {
+ private readonly ITenantIdResolver _idResolver;
+ private readonly UAuthMultiTenantOptions _options;
+
+ public UAuthTenantResolver(
+ ITenantIdResolver idResolver,
+ UAuthMultiTenantOptions options)
+ {
+ _idResolver = idResolver;
+ _options = options;
+ }
+
+ public async Task ResolveAsync(HttpContext context)
+ {
+ if (!_options.Enabled)
+ return UAuthTenantContext.NotResolved();
+
+ var resolutionContext =
+ TenantResolutionContextFactory.FromHttpContext(context);
+
+ var rawTenantId =
+ await _idResolver.ResolveTenantIdAsync(resolutionContext);
+
+ if (string.IsNullOrWhiteSpace(rawTenantId))
+ {
+ if (_options.RequireTenant)
+ return UAuthTenantContext.NotResolved();
+
+ if (_options.DefaultTenantId is null)
+ return UAuthTenantContext.NotResolved();
+
+ return UAuthTenantContext.Resolved(
+ Normalize(_options.DefaultTenantId));
+ }
+
+ var tenantId = Normalize(rawTenantId);
+
+ if (!IsValid(tenantId))
+ return UAuthTenantContext.NotResolved();
+
+ return UAuthTenantContext.Resolved(tenantId);
+ }
+
+ private string Normalize(string tenantId)
+ {
+ return _options.NormalizeToLowercase
+ ? tenantId.ToLowerInvariant()
+ : tenantId;
+ }
+
+ private bool IsValid(string tenantId)
+ {
+ if (!System.Text.RegularExpressions.Regex
+ .IsMatch(tenantId, _options.TenantIdRegex))
+ return false;
+
+ if (_options.ReservedTenantIds.Contains(tenantId))
+ return false;
+
+ return true;
+ }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Options/.gitkeep b/src/CodeBeam.UltimateAuth.Server/Options/.gitkeep
deleted file mode 100644
index 5f28270..0000000
--- a/src/CodeBeam.UltimateAuth.Server/Options/.gitkeep
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/CodeBeam.UltimateAuth.Server/Options/UAuthServerOptions.cs b/src/CodeBeam.UltimateAuth.Server/Options/UAuthServerOptions.cs
new file mode 100644
index 0000000..d094d53
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Options/UAuthServerOptions.cs
@@ -0,0 +1,105 @@
+using CodeBeam.UltimateAuth.Core;
+using CodeBeam.UltimateAuth.Core.Options;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace CodeBeam.UltimateAuth.Server.Options
+{
+ ///
+ /// Server-side configuration for UltimateAuth.
+ /// Does NOT duplicate Core options.
+ /// Instead, it composes SessionOptions, TokenOptions, PkceOptions, MultiTenantOptions
+ /// and adds server-only behaviors (routing, endpoint activation, policies).
+ ///
+ public sealed class UAuthServerOptions
+ {
+ ///
+ /// Defines how UltimateAuth executes authentication flows.
+ /// Default is Hybrid.
+ ///
+ public UAuthMode Mode { get; set; } = UAuthMode.Hybrid;
+
+ // -------------------------------------------------------
+ // ROUTING
+ // -------------------------------------------------------
+
+ ///
+ /// Base API route. Default: "/auth"
+ /// Changing this prevents conflicts with other auth systems.
+ ///
+ public string RoutePrefix { get; set; } = "/auth";
+
+
+ // -------------------------------------------------------
+ // CORE OPTION COMPOSITION
+ // (Server must NOT duplicate Core options)
+ // -------------------------------------------------------
+
+ ///
+ /// Session behavior (lifetime, sliding expiration, etc.)
+ /// Fully defined in Core.
+ ///
+ public UAuthSessionOptions Session { get; set; } = new();
+
+ ///
+ /// Token issuing behavior (lifetimes, refresh policies).
+ /// Fully defined in Core.
+ ///
+ public UAuthTokenOptions Tokens { get; set; } = new();
+
+ ///
+ /// PKCE configuration (required for WASM).
+ /// Fully defined in Core.
+ ///
+ public UAuthPkceOptions Pkce { get; set; } = new();
+
+ ///
+ /// Multi-tenancy behavior (resolver, normalization, etc.)
+ /// Fully defined in Core.
+ ///
+ public UAuthMultiTenantOptions MultiTenant { get; set; } = new();
+
+
+ // -------------------------------------------------------
+ // SERVER-ONLY BEHAVIOR
+ // -------------------------------------------------------
+
+ ///
+ /// Enables/disables specific endpoint groups.
+ /// Useful for API hardening.
+ ///
+ public bool? EnableLoginEndpoints { get; set; } = true;
+ public bool? EnablePkceEndpoints { get; set; } = true;
+ public bool? EnableTokenEndpoints { get; set; } = true;
+ public bool? EnableSessionEndpoints { get; set; } = true;
+ public bool? EnableUserInfoEndpoints { get; set; } = true;
+
+ ///
+ /// If true, server will add anti-forgery headers
+ /// and require proper request metadata.
+ ///
+ public bool EnableAntiCsrfProtection { get; set; } = true;
+
+ ///
+ /// If true, login attempts are rate-limited to prevent brute force attacks.
+ ///
+ public bool EnableLoginRateLimiting { get; set; } = true;
+
+
+ // -------------------------------------------------------
+ // CUSTOMIZATION HOOKS
+ // -------------------------------------------------------
+
+ ///
+ /// Allows developers to mutate endpoint routing AFTER UltimateAuth registers defaults.
+ /// Example: adding new routes, overriding authorization, adding filters.
+ ///
+ public Action? OnConfigureEndpoints { get; set; }
+
+ ///
+ /// Allows developers to add or replace server services before DI is built.
+ /// Example: overriding default ILoginService.
+ ///
+ public Action? ConfigureServices { get; set; }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Options/UAuthServerOptionsValidator.cs b/src/CodeBeam.UltimateAuth.Server/Options/UAuthServerOptionsValidator.cs
new file mode 100644
index 0000000..49b1d15
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Options/UAuthServerOptionsValidator.cs
@@ -0,0 +1,75 @@
+using CodeBeam.UltimateAuth.Core;
+using Microsoft.Extensions.Options;
+
+namespace CodeBeam.UltimateAuth.Server.Options
+{
+ public sealed class UAuthServerOptionsValidator
+ : IValidateOptions
+ {
+ public ValidateOptionsResult Validate(
+ string? name,
+ UAuthServerOptions options)
+ {
+ if (string.IsNullOrWhiteSpace(options.RoutePrefix))
+ {
+ return ValidateOptionsResult.Fail(
+ "RoutePrefix must be specified.");
+ }
+
+ if (!options.RoutePrefix.StartsWith("/"))
+ {
+ return ValidateOptionsResult.Fail(
+ "RoutePrefix must start with '/'.");
+ }
+
+ // -------------------------
+ // AUTH MODE VALIDATION
+ // -------------------------
+ if (!Enum.IsDefined(typeof(UAuthMode), options.Mode))
+ {
+ return ValidateOptionsResult.Fail(
+ $"Invalid UAuthMode: {options.Mode}");
+ }
+
+ // -------------------------
+ // SESSION VALIDATION
+ // -------------------------
+ if (options.Mode != UAuthMode.PureJwt)
+ {
+ if (options.Session.Lifetime <= TimeSpan.Zero)
+ {
+ return ValidateOptionsResult.Fail(
+ "Session.Lifetime must be greater than zero.");
+ }
+
+ if (options.Session.MaxLifetime is not null &&
+ options.Session.MaxLifetime <= TimeSpan.Zero)
+ {
+ return ValidateOptionsResult.Fail(
+ "Session.MaxLifetime must be greater than zero when specified.");
+ }
+ }
+
+ // -------------------------
+ // MULTI-TENANT VALIDATION
+ // -------------------------
+ if (options.MultiTenant.Enabled)
+ {
+ if (options.MultiTenant.RequireTenant &&
+ string.IsNullOrWhiteSpace(options.MultiTenant.DefaultTenantId))
+ {
+ // This is allowed, but warn-worthy logic
+ // We still allow it, middleware will reject requests
+ }
+
+ if (string.IsNullOrWhiteSpace(options.MultiTenant.TenantIdRegex))
+ {
+ return ValidateOptionsResult.Fail(
+ "MultiTenant.TenantIdRegex must be specified.");
+ }
+ }
+
+ return ValidateOptionsResult.Success;
+ }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Options/UAuthSessionResolutionOptions.cs b/src/CodeBeam.UltimateAuth.Server/Options/UAuthSessionResolutionOptions.cs
new file mode 100644
index 0000000..9bbac33
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Options/UAuthSessionResolutionOptions.cs
@@ -0,0 +1,24 @@
+namespace CodeBeam.UltimateAuth.Server.Options
+{
+ public sealed class UAuthSessionResolutionOptions
+ {
+ public bool EnableBearer { get; set; } = true;
+ public bool EnableHeader { get; set; } = true;
+ public bool EnableCookie { get; set; } = true;
+ public bool EnableQuery { get; set; } = false;
+
+ public string HeaderName { get; set; } = "X-UAuth-Session";
+ public string CookieName { get; set; } = "__uauth";
+ public string QueryParameterName { get; set; } = "session_id";
+
+ // Precedence order
+ // Example: Bearer, Header, Cookie, Query
+ public List Order { get; set; } = new()
+ {
+ "Bearer",
+ "Header",
+ "Cookie",
+ "Query"
+ };
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Session/.gitkeep b/src/CodeBeam.UltimateAuth.Server/Session/.gitkeep
deleted file mode 100644
index 5f28270..0000000
--- a/src/CodeBeam.UltimateAuth.Server/Session/.gitkeep
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/CodeBeam.UltimateAuth.Server/Sessions/BearerSessionIdResolver.cs b/src/CodeBeam.UltimateAuth.Server/Sessions/BearerSessionIdResolver.cs
new file mode 100644
index 0000000..49293e4
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Sessions/BearerSessionIdResolver.cs
@@ -0,0 +1,24 @@
+using CodeBeam.UltimateAuth.Core.Domain;
+using Microsoft.AspNetCore.Http;
+
+namespace CodeBeam.UltimateAuth.Server.Sessions
+{
+ public sealed class BearerSessionIdResolver : ISessionIdResolver
+ {
+ public AuthSessionId? Resolve(HttpContext context)
+ {
+ var header = context.Request.Headers.Authorization.ToString();
+ if (string.IsNullOrWhiteSpace(header))
+ return null;
+
+ if (!header.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
+ return null;
+
+ var raw = header["Bearer ".Length..].Trim();
+ if (string.IsNullOrWhiteSpace(raw))
+ return null;
+
+ return new AuthSessionId(raw);
+ }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Sessions/CompositeSessionIdResolver.cs b/src/CodeBeam.UltimateAuth.Server/Sessions/CompositeSessionIdResolver.cs
new file mode 100644
index 0000000..b69111b
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Sessions/CompositeSessionIdResolver.cs
@@ -0,0 +1,27 @@
+using CodeBeam.UltimateAuth.Core.Domain;
+using Microsoft.AspNetCore.Http;
+
+namespace CodeBeam.UltimateAuth.Server.Sessions
+{
+ public sealed class CompositeSessionIdResolver : ISessionIdResolver
+ {
+ private readonly IReadOnlyList _resolvers;
+
+ public CompositeSessionIdResolver(IEnumerable resolvers)
+ {
+ _resolvers = resolvers.ToList();
+ }
+
+ public AuthSessionId? Resolve(HttpContext context)
+ {
+ foreach (var r in _resolvers)
+ {
+ var id = r.Resolve(context);
+ if (id is not null)
+ return id;
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Sessions/CookieSessionIdResolver.cs b/src/CodeBeam.UltimateAuth.Server/Sessions/CookieSessionIdResolver.cs
new file mode 100644
index 0000000..cb33ac7
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Sessions/CookieSessionIdResolver.cs
@@ -0,0 +1,26 @@
+using CodeBeam.UltimateAuth.Core.Domain;
+using Microsoft.AspNetCore.Http;
+
+namespace CodeBeam.UltimateAuth.Server.Sessions
+{
+ public sealed class CookieSessionIdResolver : ISessionIdResolver
+ {
+ private readonly string _cookieName;
+
+ public CookieSessionIdResolver(string cookieName)
+ {
+ _cookieName = cookieName;
+ }
+
+ public AuthSessionId? Resolve(HttpContext context)
+ {
+ if (!context.Request.Cookies.TryGetValue(_cookieName, out var raw))
+ return null;
+
+ if (string.IsNullOrWhiteSpace(raw))
+ return null;
+
+ return new AuthSessionId(raw.Trim());
+ }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Sessions/HeaderSessionIdResolver.cs b/src/CodeBeam.UltimateAuth.Server/Sessions/HeaderSessionIdResolver.cs
new file mode 100644
index 0000000..aad25f4
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Sessions/HeaderSessionIdResolver.cs
@@ -0,0 +1,27 @@
+using CodeBeam.UltimateAuth.Core.Domain;
+using Microsoft.AspNetCore.Http;
+
+namespace CodeBeam.UltimateAuth.Server.Sessions
+{
+ public sealed class HeaderSessionIdResolver : ISessionIdResolver
+ {
+ private readonly string _headerName;
+
+ public HeaderSessionIdResolver(string headerName)
+ {
+ _headerName = headerName;
+ }
+
+ public AuthSessionId? Resolve(HttpContext context)
+ {
+ if (!context.Request.Headers.TryGetValue(_headerName, out var values))
+ return null;
+
+ var raw = values.FirstOrDefault();
+ if (string.IsNullOrWhiteSpace(raw))
+ return null;
+
+ return new AuthSessionId(raw.Trim());
+ }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Sessions/ISessionIdResolver.cs b/src/CodeBeam.UltimateAuth.Server/Sessions/ISessionIdResolver.cs
new file mode 100644
index 0000000..bddb506
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Sessions/ISessionIdResolver.cs
@@ -0,0 +1,10 @@
+using CodeBeam.UltimateAuth.Core.Domain;
+using Microsoft.AspNetCore.Http;
+
+namespace CodeBeam.UltimateAuth.Server.Sessions
+{
+ public interface ISessionIdResolver
+ {
+ AuthSessionId? Resolve(HttpContext context);
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Sessions/ISessionOrchestrator.cs b/src/CodeBeam.UltimateAuth.Server/Sessions/ISessionOrchestrator.cs
new file mode 100644
index 0000000..358b8fd
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Sessions/ISessionOrchestrator.cs
@@ -0,0 +1,34 @@
+using CodeBeam.UltimateAuth.Core.Contexts;
+using CodeBeam.UltimateAuth.Core.Domain;
+
+namespace CodeBeam.UltimateAuth.Server.Sessions
+{
+ ///
+ /// Orchestrates session, chain, and root lifecycles
+ /// according to UltimateAuth security rules.
+ ///
+ public interface ISessionOrchestrator
+ {
+ ///
+ /// Creates a new login session (initial authentication).
+ ///
+ Task> CreateLoginSessionAsync(
+ SessionIssueContext context);
+
+ ///
+ /// Revokes a single session.
+ ///
+ Task RevokeSessionAsync(
+ string? tenantId,
+ AuthSessionId sessionId,
+ DateTime at);
+
+ ///
+ /// Revokes all sessions of a user (global logout).
+ ///
+ Task RevokeAllSessionsAsync(
+ string? tenantId,
+ TUserId userId,
+ DateTime at);
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Sessions/QuerySessionIdResolver.cs b/src/CodeBeam.UltimateAuth.Server/Sessions/QuerySessionIdResolver.cs
new file mode 100644
index 0000000..3019d8c
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Sessions/QuerySessionIdResolver.cs
@@ -0,0 +1,27 @@
+using CodeBeam.UltimateAuth.Core.Domain;
+using Microsoft.AspNetCore.Http;
+
+namespace CodeBeam.UltimateAuth.Server.Sessions
+{
+ public sealed class QuerySessionIdResolver : ISessionIdResolver
+ {
+ private readonly string _parameterName;
+
+ public QuerySessionIdResolver(string parameterName)
+ {
+ _parameterName = parameterName;
+ }
+
+ public AuthSessionId? Resolve(HttpContext context)
+ {
+ if (!context.Request.Query.TryGetValue(_parameterName, out var values))
+ return null;
+
+ var raw = values.FirstOrDefault();
+ if (string.IsNullOrWhiteSpace(raw))
+ return null;
+
+ return new AuthSessionId(raw.Trim());
+ }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Sessions/SessionContext.cs b/src/CodeBeam.UltimateAuth.Server/Sessions/SessionContext.cs
new file mode 100644
index 0000000..3aad385
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Sessions/SessionContext.cs
@@ -0,0 +1,31 @@
+using CodeBeam.UltimateAuth.Core.Domain;
+
+namespace CodeBeam.UltimateAuth.Server.Sessions
+{
+ ///
+ /// 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 bool IsAnonymous => SessionId is null;
+
+ private SessionContext(AuthSessionId? sessionId, string? tenantId)
+ {
+ SessionId = sessionId;
+ TenantId = tenantId;
+ }
+
+ public static SessionContext Anonymous()
+ => new(null, null);
+
+ public static SessionContext FromSessionId(
+ AuthSessionId sessionId,
+ string? tenantId)
+ => new(sessionId, tenantId);
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Sessions/UAuthSessionIdResolver.cs b/src/CodeBeam.UltimateAuth.Server/Sessions/UAuthSessionIdResolver.cs
new file mode 100644
index 0000000..75afe79
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Sessions/UAuthSessionIdResolver.cs
@@ -0,0 +1,44 @@
+using CodeBeam.UltimateAuth.Core.Domain;
+using CodeBeam.UltimateAuth.Server.Options;
+using Microsoft.Extensions.Options;
+
+namespace CodeBeam.UltimateAuth.Server.Sessions
+{
+ public sealed class UAuthSessionIdResolver : ISessionIdResolver
+ {
+ private readonly ISessionIdResolver _inner;
+
+ public UAuthSessionIdResolver(IOptions options)
+ {
+ var o = options.Value;
+
+ var map = new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ ["Bearer"] = new BearerSessionIdResolver(),
+ ["Header"] = new HeaderSessionIdResolver(o.HeaderName),
+ ["Cookie"] = new CookieSessionIdResolver(o.CookieName),
+ ["Query"] = new QuerySessionIdResolver(o.QueryParameterName),
+ };
+
+ var list = new List();
+
+ foreach (var key in o.Order)
+ {
+ if (!map.TryGetValue(key, out var resolver))
+ continue;
+
+ if (key.Equals("Bearer", StringComparison.OrdinalIgnoreCase) && !o.EnableBearer) continue;
+ if (key.Equals("Header", StringComparison.OrdinalIgnoreCase) && !o.EnableHeader) continue;
+ if (key.Equals("Cookie", StringComparison.OrdinalIgnoreCase) && !o.EnableCookie) continue;
+ if (key.Equals("Query", StringComparison.OrdinalIgnoreCase) && !o.EnableQuery) continue;
+
+ list.Add(resolver);
+ }
+
+ _inner = new CompositeSessionIdResolver(list);
+ }
+
+ public AuthSessionId? Resolve(Microsoft.AspNetCore.Http.HttpContext context)
+ => _inner.Resolve(context);
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Sessions/UAuthSessionOrchestrator.cs b/src/CodeBeam.UltimateAuth.Server/Sessions/UAuthSessionOrchestrator.cs
new file mode 100644
index 0000000..d16e03f
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Sessions/UAuthSessionOrchestrator.cs
@@ -0,0 +1,235 @@
+using CodeBeam.UltimateAuth.Core;
+using CodeBeam.UltimateAuth.Core.Abstractions;
+using CodeBeam.UltimateAuth.Core.Contexts;
+using CodeBeam.UltimateAuth.Core.Domain;
+using CodeBeam.UltimateAuth.Server.Issuers;
+using CodeBeam.UltimateAuth.Server.Options;
+
+namespace CodeBeam.UltimateAuth.Server.Sessions
+{
+ ///
+ /// Default UltimateAuth session store implementation.
+ /// Handles session, chain, and root orchestration on top of a kernel store.
+ ///
+ public sealed class UAuthSessionOrchestrator : ISessionOrchestrator
+ {
+ private readonly ISessionStoreFactory _factory;
+ private readonly UAuthSessionIssuer _sessionIssuer;
+ private readonly UAuthServerOptions _serverOptions;
+
+ public UAuthSessionOrchestrator(ISessionStoreFactory factory, UAuthSessionIssuer sessionIssuer, UAuthServerOptions serverOptions)
+ {
+ _factory = factory;
+ _sessionIssuer = sessionIssuer;
+ _serverOptions = serverOptions;
+ }
+
+ public async Task> CreateLoginSessionAsync(SessionIssueContext context)
+ {
+ var kernel = _factory.Create(context.TenantId);
+
+ // 1️⃣ Load or create root
+ var root = await kernel.GetSessionRootAsync(
+ context.TenantId,
+ context.UserId);
+
+ if (root is null)
+ {
+ root = UAuthSessionRoot.Create(
+ context.TenantId,
+ context.UserId,
+ context.Now);
+ }
+ else if (root.IsRevoked)
+ {
+ throw new InvalidOperationException(
+ "User session root is revoked.");
+ }
+
+ // 2️⃣ Load or create chain (interface → concrete)
+ ISessionChain? loadedChain = null;
+
+ if (context.ChainId is not null)
+ {
+ loadedChain = await kernel.GetChainAsync(
+ context.TenantId,
+ context.ChainId.Value);
+ }
+
+ if (loadedChain is not null && loadedChain.IsRevoked)
+ {
+ throw new InvalidOperationException(
+ "Session chain is revoked.");
+ }
+
+ UAuthSessionChain chain;
+
+ if (loadedChain is null)
+ {
+ chain = UAuthSessionChain.Create(
+ ChainId.New(),
+ context.TenantId,
+ context.UserId,
+ root.SecurityVersion,
+ context.ClaimsSnapshot);
+ }
+ else if (loadedChain is UAuthSessionChain concreteChain)
+ {
+ chain = concreteChain;
+ }
+ else
+ {
+ throw new InvalidOperationException(
+ "Unsupported ISessionChain implementation. " +
+ "UltimateAuth requires SessionChain.");
+ }
+
+ // TODO: Add cancellation token support
+ var issuedSession = await _sessionIssuer.IssueAsync(
+ context,
+ chain);
+
+ // 4️⃣ Persist session
+ await kernel.SaveSessionAsync(
+ context.TenantId,
+ issuedSession.Session);
+
+ // 5️⃣ Update & persist chain
+ var updatedChain = chain.ActivateSession(
+ issuedSession.Session.SessionId);
+
+ await kernel.SaveChainAsync(
+ context.TenantId,
+ updatedChain);
+
+ // 6️⃣ Persist root (idempotent)
+ await kernel.SaveSessionRootAsync(
+ context.TenantId,
+ root);
+
+ return issuedSession;
+ }
+
+ public async Task> RotateSessionAsync(string? tenantId, AuthSessionId currentSessionId, SessionIssueContext context)
+ {
+ if (_serverOptions.Mode == UAuthMode.PureJwt)
+ throw new InvalidOperationException(
+ "Session rotation is not available in PureJwt mode.");
+
+ var kernel = _factory.Create(tenantId);
+
+ // 1️⃣ Load current session
+ var currentSession = await kernel.GetSessionAsync(
+ tenantId,
+ currentSessionId);
+
+ if (currentSession is null)
+ throw new InvalidOperationException("Session not found.");
+
+ if (currentSession.IsRevoked)
+ throw new InvalidOperationException("Session is revoked.");
+
+ if (currentSession.GetState(context.Now) != SessionState.Active)
+ throw new InvalidOperationException("Session is not active.");
+
+ // 2️⃣ Load chain id
+ var chainId = await kernel.GetChainIdBySessionAsync(
+ tenantId,
+ currentSessionId);
+
+ if (chainId is null)
+ throw new InvalidOperationException("Session chain not found.");
+
+ // 3️⃣ Load chain
+ var loadedChain = await kernel.GetChainAsync(
+ tenantId,
+ chainId.Value);
+
+ if (loadedChain is null || loadedChain.IsRevoked)
+ throw new InvalidOperationException("Session chain is revoked.");
+
+ if (loadedChain is not UAuthSessionChain chain)
+ throw new InvalidOperationException(
+ "Unsupported ISessionChain implementation.");
+
+ // 4️⃣ Load root
+ var root = await kernel.GetSessionRootAsync(
+ tenantId,
+ context.UserId);
+
+ if (root is null || root.IsRevoked)
+ throw new InvalidOperationException("Session root is revoked.");
+
+ // 5️⃣ Security version check
+ if (currentSession.SecurityVersionAtCreation != root.SecurityVersion)
+ throw new InvalidOperationException(
+ "Session security version mismatch.");
+
+ // TODO: Add cancellation token support
+ var issuedSession = await _sessionIssuer.IssueAsync(
+ context,
+ chain);
+
+ // 7️⃣ Persist new session
+ await kernel.SaveSessionAsync(
+ tenantId,
+ issuedSession.Session);
+
+ // 8️⃣ Revoke old session
+ await kernel.RevokeSessionAsync(
+ tenantId,
+ currentSessionId,
+ context.Now);
+
+ // 9️⃣ Activate new session in chain
+ var updatedChain = chain.ActivateSession(
+ issuedSession.Session.SessionId);
+
+ await kernel.SaveChainAsync(
+ tenantId,
+ updatedChain);
+
+ // 🔟 Root persistence (idempotent)
+ await kernel.SaveSessionRootAsync(
+ tenantId,
+ root);
+
+ return issuedSession;
+ }
+
+ public Task?> GetSessionAsync(
+ string? tenantId,
+ AuthSessionId sessionId)
+ {
+ var kernel = _factory.Create(tenantId);
+ return kernel.GetSessionAsync(tenantId, sessionId);
+ }
+
+ public async Task RevokeSessionAsync(
+ string? tenantId,
+ AuthSessionId sessionId,
+ DateTime at)
+ {
+ var kernel = _factory.Create(tenantId);
+ await kernel.RevokeSessionAsync(tenantId, sessionId, at);
+ }
+
+ public async Task RevokeAllSessionsAsync(
+ string? tenantId,
+ TUserId userId,
+ DateTime at)
+ {
+ var kernel = _factory.Create(tenantId);
+ await kernel.RevokeSessionRootAsync(tenantId, userId, at);
+ }
+
+ public async Task RevokeChainAsync(
+ string? tenantId,
+ ChainId chainId,
+ DateTime at)
+ {
+ var kernel = _factory.Create