diff --git a/UltimateAuth.slnx b/UltimateAuth.slnx
index 21efc31..18a8f98 100644
--- a/UltimateAuth.slnx
+++ b/UltimateAuth.slnx
@@ -3,7 +3,7 @@
-
+
diff --git a/src/CodeBeam.UltimateAuth.AspNetCore/Abstractions/IUAuthUserManagementService.cs b/src/CodeBeam.UltimateAuth.AspNetCore/Abstractions/IUAuthUserManagementService.cs
new file mode 100644
index 0000000..44c57f1
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.AspNetCore/Abstractions/IUAuthUserManagementService.cs
@@ -0,0 +1,28 @@
+namespace CodeBeam.UltimateAuth.Server.Users
+{
+ ///
+ /// Administrative user management operations.
+ ///
+ public interface IUAuthUserManagementService
+ {
+ Task> GetByIdAsync(
+ TUserId userId,
+ CancellationToken ct = default);
+
+ Task>> GetAllAsync(
+ CancellationToken ct = default);
+
+ Task DisableAsync(
+ TUserId userId,
+ CancellationToken ct = default);
+
+ Task EnableAsync(
+ TUserId userId,
+ CancellationToken ct = default);
+
+ Task ResetPasswordAsync(
+ TUserId userId,
+ ResetPasswordRequest request,
+ CancellationToken ct = default);
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.AspNetCore/Abstractions/IUAuthUserProfileService.cs b/src/CodeBeam.UltimateAuth.AspNetCore/Abstractions/IUAuthUserProfileService.cs
new file mode 100644
index 0000000..68dedea
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.AspNetCore/Abstractions/IUAuthUserProfileService.cs
@@ -0,0 +1,23 @@
+namespace CodeBeam.UltimateAuth.Server.Users
+{
+ ///
+ /// User self-service operations (profile, password, MFA).
+ ///
+ public interface IUAuthUserProfileService
+ {
+ Task> GetCurrentAsync(
+ CancellationToken ct = default);
+
+ Task UpdateProfileAsync(
+ UpdateProfileRequest request,
+ CancellationToken ct = default);
+
+ Task ChangePasswordAsync(
+ ChangePasswordRequest request,
+ CancellationToken ct = default);
+
+ Task ConfigureMfaAsync(
+ ConfigureMfaRequest request,
+ CancellationToken ct = default);
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.AspNetCore/CodeBeam.UltimateAuth.AspNetCore.csproj b/src/CodeBeam.UltimateAuth.AspNetCore/CodeBeam.UltimateAuth.Server.Users.csproj
similarity index 100%
rename from src/CodeBeam.UltimateAuth.AspNetCore/CodeBeam.UltimateAuth.AspNetCore.csproj
rename to src/CodeBeam.UltimateAuth.AspNetCore/CodeBeam.UltimateAuth.Server.Users.csproj
diff --git a/src/CodeBeam.UltimateAuth.AspNetCore/Handlers/.gitkeep b/src/CodeBeam.UltimateAuth.AspNetCore/Handlers/.gitkeep
deleted file mode 100644
index 5f28270..0000000
--- a/src/CodeBeam.UltimateAuth.AspNetCore/Handlers/.gitkeep
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/CodeBeam.UltimateAuth.AspNetCore/Users/Models/AdminUserFilter.cs b/src/CodeBeam.UltimateAuth.AspNetCore/Users/Models/AdminUserFilter.cs
new file mode 100644
index 0000000..87cec2b
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.AspNetCore/Users/Models/AdminUserFilter.cs
@@ -0,0 +1,10 @@
+namespace CodeBeam.UltimateAuth.Server.Users
+{
+ public sealed class AdminUserFilter
+ {
+ public bool? IsActive { get; init; }
+ public bool? IsEmailConfirmed { get; init; }
+
+ public string? Search { get; init; }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.AspNetCore/Users/Models/ChangePasswordRequest.cs b/src/CodeBeam.UltimateAuth.AspNetCore/Users/Models/ChangePasswordRequest.cs
new file mode 100644
index 0000000..b53dfb6
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.AspNetCore/Users/Models/ChangePasswordRequest.cs
@@ -0,0 +1,13 @@
+namespace CodeBeam.UltimateAuth.Server.Users
+{
+ public sealed class ChangePasswordRequest
+ {
+ public required string CurrentPassword { get; init; }
+ public required string NewPassword { get; init; }
+
+ ///
+ /// If true, other sessions will be revoked.
+ ///
+ public bool RevokeOtherSessions { get; init; } = true;
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.AspNetCore/Users/Models/ConfigureMfaRequest.cs b/src/CodeBeam.UltimateAuth.AspNetCore/Users/Models/ConfigureMfaRequest.cs
new file mode 100644
index 0000000..ff2b1d4
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.AspNetCore/Users/Models/ConfigureMfaRequest.cs
@@ -0,0 +1,12 @@
+namespace CodeBeam.UltimateAuth.Server.Users
+{
+ public sealed class ConfigureMfaRequest
+ {
+ public bool Enable { get; init; }
+
+ ///
+ /// Optional verification code when enabling MFA.
+ ///
+ public string? VerificationCode { get; init; }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.AspNetCore/Users/Models/ResetPasswordRequest.cs b/src/CodeBeam.UltimateAuth.AspNetCore/Users/Models/ResetPasswordRequest.cs
new file mode 100644
index 0000000..672d176
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.AspNetCore/Users/Models/ResetPasswordRequest.cs
@@ -0,0 +1,12 @@
+namespace CodeBeam.UltimateAuth.Server.Users
+{
+ public sealed class ResetPasswordRequest
+ {
+ public required string NewPassword { get; init; }
+
+ ///
+ /// If true, all active sessions will be revoked.
+ ///
+ public bool RevokeSessions { get; init; } = true;
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.AspNetCore/Users/Models/UpdateProfileRequest.cs b/src/CodeBeam.UltimateAuth.AspNetCore/Users/Models/UpdateProfileRequest.cs
new file mode 100644
index 0000000..f7688c9
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.AspNetCore/Users/Models/UpdateProfileRequest.cs
@@ -0,0 +1,8 @@
+namespace CodeBeam.UltimateAuth.Server.Users
+{
+ public sealed class UpdateProfileRequest
+ {
+ public string? Username { get; init; }
+ public string? Email { get; init; }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.AspNetCore/Users/Models/UserDto.cs b/src/CodeBeam.UltimateAuth.AspNetCore/Users/Models/UserDto.cs
new file mode 100644
index 0000000..8aa7cb5
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.AspNetCore/Users/Models/UserDto.cs
@@ -0,0 +1,16 @@
+namespace CodeBeam.UltimateAuth.Server.Users
+{
+ public sealed class UserDto
+ {
+ public required TUserId UserId { get; init; }
+
+ public string? Username { get; init; }
+ public string? Email { get; init; }
+
+ public bool IsActive { get; init; }
+ public bool IsEmailConfirmed { get; init; }
+
+ public DateTime CreatedAt { get; init; }
+ public DateTime? LastLoginAt { get; init; }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.AspNetCore/Users/Models/UserProfileDto.cs b/src/CodeBeam.UltimateAuth.AspNetCore/Users/Models/UserProfileDto.cs
new file mode 100644
index 0000000..9b2496e
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.AspNetCore/Users/Models/UserProfileDto.cs
@@ -0,0 +1,14 @@
+namespace CodeBeam.UltimateAuth.Server.Users
+{
+ public sealed class UserProfileDto
+ {
+ public required TUserId UserId { get; init; }
+
+ public string? Username { get; init; }
+ public string? Email { get; init; }
+
+ public bool IsEmailConfirmed { get; init; }
+
+ public DateTime CreatedAt { get; init; }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Infrastructure/IClock.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Infrastructure/IClock.cs
new file mode 100644
index 0000000..ce4905a
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Infrastructure/IClock.cs
@@ -0,0 +1,11 @@
+namespace CodeBeam.UltimateAuth.Core.Abstractions
+{
+ ///
+ /// Provides an abstracted time source for the system.
+ /// Used to improve testability and ensure consistent time handling.
+ ///
+ public interface IClock
+ {
+ DateTime UtcNow { get; }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Issuers/ITokenHasher.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Infrastructure/ITokenHasher.cs
similarity index 100%
rename from src/CodeBeam.UltimateAuth.Core/Abstractions/Issuers/ITokenHasher.cs
rename to src/CodeBeam.UltimateAuth.Core/Abstractions/Infrastructure/ITokenHasher.cs
diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Infrastructure/IUAuthPasswordHasher.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Infrastructure/IUAuthPasswordHasher.cs
new file mode 100644
index 0000000..53246c1
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Infrastructure/IUAuthPasswordHasher.cs
@@ -0,0 +1,13 @@
+namespace CodeBeam.UltimateAuth.Core.Abstractions
+{
+ ///
+ /// Securely hashes and verifies user passwords.
+ /// Designed for slow, adaptive, memory-hard algorithms
+ /// such as Argon2 or bcrypt.
+ ///
+ public interface IUAuthPasswordHasher
+ {
+ string Hash(string password);
+ bool Verify(string password, string hash);
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Issuers/ISessionIssuer.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Issuers/ISessionIssuer.cs
index f19db3e..726bbfb 100644
--- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Issuers/ISessionIssuer.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Issuers/ISessionIssuer.cs
@@ -1,4 +1,4 @@
-using CodeBeam.UltimateAuth.Core.Contexts;
+using CodeBeam.UltimateAuth.Core.Contracts;
using CodeBeam.UltimateAuth.Core.Domain;
namespace CodeBeam.UltimateAuth.Core.Abstractions
@@ -8,6 +8,6 @@ namespace CodeBeam.UltimateAuth.Core.Abstractions
///
public interface ISessionIssuer
{
- Task> IssueAsync(SessionIssueContext context, UAuthSessionChain chain, CancellationToken cancellationToken = default);
+ Task> IssueAsync(AuthenticatedSessionContext context, ISessionChain chain, CancellationToken cancellationToken = default);
}
}
diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Issuers/ITokenIssuer.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Issuers/ITokenIssuer.cs
deleted file mode 100644
index 948bf3b..0000000
--- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Issuers/ITokenIssuer.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-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/Principals/IUserAuthenticator.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Principals/IUserAuthenticator.cs
new file mode 100644
index 0000000..0708937
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Principals/IUserAuthenticator.cs
@@ -0,0 +1,9 @@
+using CodeBeam.UltimateAuth.Core.Contracts;
+
+namespace CodeBeam.UltimateAuth.Core.Abstractions
+{
+ public interface IUserAuthenticator
+ {
+ Task> AuthenticateAsync(string? tenantId, string identifier, string secret, CancellationToken cancellationToken = default);
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/IUserIdConverter.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Principals/IUserIdConverter.cs
similarity index 94%
rename from src/CodeBeam.UltimateAuth.Core/Abstractions/IUserIdConverter.cs
rename to src/CodeBeam.UltimateAuth.Core/Abstractions/Principals/IUserIdConverter.cs
index a52f3ec..5564215 100644
--- a/src/CodeBeam.UltimateAuth.Core/Abstractions/IUserIdConverter.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Principals/IUserIdConverter.cs
@@ -31,6 +31,7 @@ public interface IUserIdConverter
/// Thrown when the input value cannot be parsed into a valid identifier.
///
TUserId FromString(string value);
+ bool TryFromString(string value, out TUserId userId);
///
/// Reconstructs a typed user identifier from its binary representation.
@@ -41,5 +42,6 @@ public interface IUserIdConverter
/// Thrown when the input binary value cannot be parsed into a valid identifier.
///
TUserId FromBytes(byte[] binary);
+ bool TryFromBytes(byte[] binary, out TUserId userId);
}
}
diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/IUserIdConverterResolver.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Principals/IUserIdConverterResolver.cs
similarity index 100%
rename from src/CodeBeam.UltimateAuth.Core/Abstractions/IUserIdConverterResolver.cs
rename to src/CodeBeam.UltimateAuth.Core/Abstractions/Principals/IUserIdConverterResolver.cs
diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Principals/IUserIdFactory.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Principals/IUserIdFactory.cs
new file mode 100644
index 0000000..b5d2715
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Principals/IUserIdFactory.cs
@@ -0,0 +1,16 @@
+namespace CodeBeam.UltimateAuth.Core.Abstractions
+{
+ ///
+ /// Responsible for creating new user identifiers.
+ /// This abstraction allows UltimateAuth to remain
+ /// independent from the concrete user ID type.
+ ///
+ /// User identifier type.
+ public interface IUserIdFactory
+ {
+ ///
+ /// Creates a new unique user identifier.
+ ///
+ TUserId Create();
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Services/ISessionService.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Services/ISessionService.cs
deleted file mode 100644
index c9ba215..0000000
--- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Services/ISessionService.cs
+++ /dev/null
@@ -1,93 +0,0 @@
-using CodeBeam.UltimateAuth.Core.Domain;
-using CodeBeam.UltimateAuth.Core.Models;
-
-namespace CodeBeam.UltimateAuth.Core.Abstractions
-{
- ///
- /// Provides high-level session lifecycle operations such as creation, refresh, validation, and revocation.
- ///
- /// The type used to uniquely identify the user.
- public interface ISessionService
- {
- ///
- /// Creates a new login session for the specified user.
- ///
- ///
- /// The tenant identifier. Use null for single-tenant applications.
- ///
- /// The user associated with the session.
- /// Information about the device initiating the session.
- /// Optional metadata describing the session context.
- /// The current UTC timestamp.
- ///
- /// A result containing the newly created session, chain, and session root.
- ///
- Task> CreateLoginSessionAsync(string? tenantId, TUserId userId, DeviceInfo deviceInfo, SessionMetadata? metadata, DateTime now);
-
- ///
- /// Rotates the specified session and issues a new one while preserving the session chain.
- ///
- /// The tenant identifier, or null.
- /// The active session identifier to be refreshed.
- /// The current UTC timestamp.
- ///
- /// A result containing the refreshed session and updated chain.
- ///
- ///
- /// Thrown if the session, its chain, or the user's session root is invalid.
- ///
- Task> RefreshSessionAsync(string? tenantId, AuthSessionId currentSessionId,DateTime now);
-
- ///
- /// Revokes a single session, preventing further use.
- ///
- /// The tenant identifier, or null.
- /// The session identifier to revoke.
- /// The UTC timestamp of the revocation.
- Task RevokeSessionAsync(string? tenantId, AuthSessionId sessionId, DateTime at);
-
- ///
- /// Revokes an entire session chain (device-level logout).
- ///
- /// The tenant identifier, or null.
- /// The session chain identifier to revoke.
- /// The UTC timestamp of the revocation.
- Task RevokeChainAsync(string? tenantId, ChainId chainId, DateTime at);
-
- ///
- /// Revokes the user's session root, invalidating all existing sessions across all chains.
- ///
- /// The tenant identifier, or null.
- /// The user whose root should be revoked.
- /// The UTC timestamp of the revocation.
- Task RevokeRootAsync(string? tenantId, TUserId userId, DateTime at);
-
- ///
- /// Validates a session and evaluates its current state, including expiration, revocation, and security version alignment.
- ///
- /// The tenant identifier, or null.
- /// The session identifier to validate.
- /// The current UTC timestamp.
- ///
- /// A detailed validation result describing the session, chain, root,
- /// and computed session state.
- ///
- Task> ValidateSessionAsync(string? tenantId, AuthSessionId sessionId, DateTime now);
-
- ///
- /// Retrieves all session chains belonging to the specified user.
- ///
- /// The tenant identifier, or null.
- /// The user whose session chains are requested.
- /// A read-only list of session chains.
- Task>> GetChainsAsync(string? tenantId, TUserId userId);
-
- ///
- /// Retrieves all sessions belonging to a specific session chain.
- ///
- /// The tenant identifier, or null.
- /// The session chain identifier.
- /// A read-only list of sessions contained within the chain.
- Task>> GetSessionsAsync(string? tenantId, ChainId chainId);
- }
-}
diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Services/IUAuthFlowService.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Services/IUAuthFlowService.cs
new file mode 100644
index 0000000..530bacd
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Services/IUAuthFlowService.cs
@@ -0,0 +1,33 @@
+using CodeBeam.UltimateAuth.Core.Contracts;
+
+namespace CodeBeam.UltimateAuth.Core.Abstractions
+{
+ ///
+ /// Handles authentication flows such as login,
+ /// logout, session refresh and reauthentication.
+ ///
+ public interface IUAuthFlowService
+ {
+ Task LoginAsync(LoginRequest request, CancellationToken ct = default);
+
+ Task ExternalLoginAsync(ExternalLoginRequest request, CancellationToken ct = default);
+
+ Task BeginMfaAsync(BeginMfaRequest request, CancellationToken ct = default);
+
+ Task CompleteMfaAsync(CompleteMfaRequest request, CancellationToken ct = default);
+
+ Task LogoutAsync(LogoutRequest request, CancellationToken ct = default);
+
+ Task LogoutAllAsync(LogoutAllRequest request, CancellationToken ct = default);
+
+ Task RefreshSessionAsync(SessionRefreshRequest request, CancellationToken ct = default);
+
+ Task ReauthenticateAsync(ReauthRequest request, CancellationToken ct = default);
+
+ Task CreatePkceChallengeAsync(PkceCreateRequest request, CancellationToken ct = default);
+
+ Task VerifyPkceAsync(PkceVerifyRequest request, CancellationToken ct = default);
+
+ Task ConsumePkceAsync(PkceConsumeRequest request, CancellationToken ct = default);
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Services/IUAuthService.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Services/IUAuthService.cs
new file mode 100644
index 0000000..3e3e0ac
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Services/IUAuthService.cs
@@ -0,0 +1,15 @@
+namespace CodeBeam.UltimateAuth.Core.Abstractions
+{
+ ///
+ /// High-level facade for UltimateAuth.
+ /// Provides access to authentication flows,
+ /// session lifecycle and user operations.
+ ///
+ public interface IUAuthService
+ {
+ IUAuthFlowService Flow { get; }
+ IUAuthSessionService Sessions { get; }
+ IUAuthTokenService Tokens { get; }
+ IUAuthUserService Users { get; }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Services/IUAuthSessionService.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Services/IUAuthSessionService.cs
new file mode 100644
index 0000000..25ad4f5
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Services/IUAuthSessionService.cs
@@ -0,0 +1,33 @@
+using CodeBeam.UltimateAuth.Core.Contracts;
+using CodeBeam.UltimateAuth.Core.Domain;
+
+namespace CodeBeam.UltimateAuth.Core.Abstractions
+{
+ ///
+ /// Provides high-level session lifecycle operations such as creation, refresh, validation, and revocation.
+ ///
+ /// The type used to uniquely identify the user.
+ public interface IUAuthSessionService
+ {
+ Task> ValidateSessionAsync(string? tenantId, AuthSessionId sessionId, DateTime at);
+
+ Task>> GetChainsAsync(string? tenantId, TUserId userId);
+
+ Task>> GetSessionsAsync(string? tenantId, ChainId chainId);
+
+ Task?> GetCurrentSessionAsync(string? tenantId, AuthSessionId sessionId);
+
+ Task RevokeSessionAsync(string? tenantId, AuthSessionId sessionId, DateTime at);
+
+ Task RevokeChainAsync(string? tenantId, ChainId chainId, DateTime at);
+
+ Task ResolveChainIdAsync(string? tenantId, AuthSessionId sessionId);
+
+ Task RevokeAllChainsAsync(string? tenantId, TUserId userId, ChainId? exceptChainId, DateTime at);
+
+ // Hard revoke - admin
+ Task RevokeRootAsync(string? tenantId, TUserId userId, DateTime at);
+
+ Task> IssueSessionAfterAuthenticationAsync(string? tenantId, AuthenticatedSessionContext context, CancellationToken cancellationToken = default);
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Services/IUAuthTokenService.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Services/IUAuthTokenService.cs
new file mode 100644
index 0000000..bca0bd6
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Services/IUAuthTokenService.cs
@@ -0,0 +1,27 @@
+using CodeBeam.UltimateAuth.Core.Contracts;
+
+namespace CodeBeam.UltimateAuth.Core.Abstractions
+{
+ ///
+ /// Issues, refreshes and validates access and refresh tokens.
+ /// Stateless or hybrid depending on auth mode.
+ ///
+ public interface IUAuthTokenService
+ {
+ ///
+ /// Issues access (and optionally refresh) tokens
+ /// for a validated session.
+ ///
+ Task CreateTokensAsync(TokenIssueContext context, CancellationToken cancellationToken = default);
+
+ ///
+ /// Refreshes tokens using a refresh token.
+ ///
+ Task RefreshAsync(TokenRefreshContext context, CancellationToken cancellationToken = default);
+
+ ///
+ /// Validates an access token (JWT or opaque).
+ ///
+ Task> ValidateAsync(string token, TokenType type, CancellationToken cancellationToken = default);
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Services/IUAuthUserService.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Services/IUAuthUserService.cs
new file mode 100644
index 0000000..ee74667
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Services/IUAuthUserService.cs
@@ -0,0 +1,20 @@
+using CodeBeam.UltimateAuth.Core.Contracts;
+
+namespace CodeBeam.UltimateAuth.Core.Abstractions
+{
+ ///
+ /// Minimal user operations required for authentication.
+ /// Does NOT include role or permission management.
+ /// For user management, CodeBeam.UltimateAuth.Users package is recommended.
+ ///
+ public interface IUAuthUserService
+ {
+ Task RegisterAsync(RegisterUserRequest request, CancellationToken cancellationToken = default);
+
+ Task DeleteAsync(TUserId userId, CancellationToken cancellationToken = default);
+
+ Task ValidateCredentialsAsync(ValidateCredentialsRequest request, CancellationToken cancellationToken = default);
+
+ Task> AuthenticateAsync(string? tenantId, string identifier, string secret, CancellationToken cancellationToken = default);
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/IOpaqueTokenStore.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/IOpaqueTokenStore.cs
new file mode 100644
index 0000000..2f5e141
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/IOpaqueTokenStore.cs
@@ -0,0 +1,11 @@
+using CodeBeam.UltimateAuth.Core.Contracts;
+
+namespace CodeBeam.UltimateAuth.Core.Abstractions
+{
+ public interface IOpaqueTokenStore
+ {
+ Task FindByHashAsync(
+ string tokenHash,
+ CancellationToken ct = default);
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ISessionStore.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ISessionStore.cs
index e25ba9c..5b65e7d 100644
--- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ISessionStore.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ISessionStore.cs
@@ -1,4 +1,4 @@
-using CodeBeam.UltimateAuth.Core.Contexts;
+using CodeBeam.UltimateAuth.Core.Contracts;
using CodeBeam.UltimateAuth.Core.Domain;
namespace CodeBeam.UltimateAuth.Core.Abstractions
diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ISessionStoreKernel.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ISessionStoreKernel.cs
index 9d8763b..d398eb1 100644
--- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ISessionStoreKernel.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ISessionStoreKernel.cs
@@ -56,13 +56,6 @@ public interface ISessionStoreKernel
/// 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.
///
@@ -135,6 +128,11 @@ public interface ISessionStoreKernel
/// The session identifier.
/// The chain identifier or null.
Task GetChainIdBySessionAsync(string? tenantId, AuthSessionId sessionId);
- }
+ ///
+ /// Executes multiple store operations as a single atomic unit.
+ /// Implementations must ensure transactional consistency where supported.
+ ///
+ Task ExecuteAsync(Func action);
+ }
}
diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/IUserStore.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/IUAuthUserStore.cs
similarity index 74%
rename from src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/IUserStore.cs
rename to src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/IUAuthUserStore.cs
index fcb268b..53f78c5 100644
--- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/IUserStore.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/IUAuthUserStore.cs
@@ -1,18 +1,20 @@
-namespace CodeBeam.UltimateAuth.Core.Abstractions
+using CodeBeam.UltimateAuth.Core.Domain;
+using CodeBeam.UltimateAuth.Core.Infrastructure;
+
+namespace CodeBeam.UltimateAuth.Core.Abstractions
{
///
/// Provides minimal user lookup and security metadata required for authentication.
/// This store does not manage user creation, claims, or profile data — these belong
/// to higher-level application services outside UltimateAuth.
///
- public interface IUserStore
+ public interface IUAuthUserStore
{
- ///
- /// Retrieves a user by identifier. Returns null if no such user exists.
- ///
- /// The identifier of the user.
- /// The user instance or null if not found.
- Task?> FindByIdAsync(TUserId userId);
+ Task?> FindByIdAsync(string? tenantId, TUserId userId, CancellationToken token = default);
+
+ Task?> FindByUsernameAsync(string? tenantId,
+ string username,
+ CancellationToken ct = default);
///
/// Retrieves a user by a login credential such as username or email.
@@ -33,7 +35,7 @@ public interface IUserStore
///
/// Updates the password hash for the specified user. This method is invoked by
- /// password management services and not by .
+ /// password management services and not by .
///
/// The user identifier.
/// The new password hash value.
@@ -54,5 +56,17 @@ public interface IUserStore
///
/// The user identifier.
Task IncrementSecurityVersionAsync(TUserId userId);
+
+ Task ExistsByUsernameAsync(
+ string username,
+ CancellationToken ct = default);
+
+ Task CreateAsync(
+ UserRecord user,
+ CancellationToken ct = default);
+
+ Task DeleteAsync(
+ TUserId userId,
+ CancellationToken ct = default);
}
}
diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/IUserStoreFactory.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/IUserStoreFactory.cs
index 06e7a0d..23f8551 100644
--- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/IUserStoreFactory.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/IUserStoreFactory.cs
@@ -16,11 +16,11 @@ public interface IUserStoreFactory
/// in single-tenant deployments.
///
///
- /// An implementation capable of user lookup and security metadata retrieval.
+ /// An implementation capable of user lookup and security metadata retrieval.
///
///
/// Thrown if no user store implementation has been registered for the given user ID type.
///
- IUserStore Create(string tenantId);
+ IUAuthUserStore Create(string tenantId);
}
}
diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Validators/ITokenValidator.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Validators/ITokenValidator.cs
new file mode 100644
index 0000000..d2010fb
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Validators/ITokenValidator.cs
@@ -0,0 +1,16 @@
+using CodeBeam.UltimateAuth.Core.Contracts;
+
+namespace CodeBeam.UltimateAuth.Core.Abstractions
+{
+ ///
+ /// Validates access tokens (JWT or opaque) and resolves
+ /// the authenticated user context.
+ ///
+ public interface ITokenValidator
+ {
+ Task> ValidateAsync(
+ string token,
+ TokenType type,
+ CancellationToken ct = default);
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/CodeBeam.UltimateAuth.Core.csproj b/src/CodeBeam.UltimateAuth.Core/CodeBeam.UltimateAuth.Core.csproj
index 488b584..c03f964 100644
--- a/src/CodeBeam.UltimateAuth.Core/CodeBeam.UltimateAuth.Core.csproj
+++ b/src/CodeBeam.UltimateAuth.Core/CodeBeam.UltimateAuth.Core.csproj
@@ -8,9 +8,9 @@
-
-
-
+
+
+
diff --git a/src/CodeBeam.UltimateAuth.Core/Contexts/SessionIssueContext.cs b/src/CodeBeam.UltimateAuth.Core/Contexts/SessionIssueContext.cs
deleted file mode 100644
index d75473c..0000000
--- a/src/CodeBeam.UltimateAuth.Core/Contexts/SessionIssueContext.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-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/TokenIssueContext.cs b/src/CodeBeam.UltimateAuth.Core/Contexts/TokenIssueContext.cs
deleted file mode 100644
index 782d6a4..0000000
--- a/src/CodeBeam.UltimateAuth.Core/Contexts/TokenIssueContext.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-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/Contracts/Login/ExternalLoginRequest.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Login/ExternalLoginRequest.cs
new file mode 100644
index 0000000..6126e7d
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Login/ExternalLoginRequest.cs
@@ -0,0 +1,11 @@
+namespace CodeBeam.UltimateAuth.Core.Contracts
+{
+ public sealed record ExternalLoginRequest
+ {
+ public string? TenantId { get; init; }
+ public string Provider { get; init; } = default!;
+ public string ExternalToken { get; init; } = default!;
+ public string? DeviceId { get; init; }
+ }
+
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Login/LoginContinuation.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Login/LoginContinuation.cs
new file mode 100644
index 0000000..ec5fb02
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Login/LoginContinuation.cs
@@ -0,0 +1,20 @@
+namespace CodeBeam.UltimateAuth.Core.Contracts
+{
+ public sealed record LoginContinuation
+ {
+ ///
+ /// Gets the type of login continuation required.
+ ///
+ public LoginContinuationType Type { get; init; }
+
+ ///
+ /// Opaque continuation token used to resume the login flow.
+ ///
+ public string ContinuationToken { get; init; } = default!;
+
+ ///
+ /// Optional hint for UX (e.g. "Enter MFA code", "Verify device").
+ ///
+ public string? Hint { get; init; }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Login/LoginContinuationType.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Login/LoginContinuationType.cs
new file mode 100644
index 0000000..662fbef
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Login/LoginContinuationType.cs
@@ -0,0 +1,9 @@
+namespace CodeBeam.UltimateAuth.Core.Contracts
+{
+ public enum LoginContinuationType
+ {
+ Mfa,
+ Pkce,
+ External
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Login/LoginRequest.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Login/LoginRequest.cs
new file mode 100644
index 0000000..97a75f0
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Login/LoginRequest.cs
@@ -0,0 +1,23 @@
+using CodeBeam.UltimateAuth.Core.Domain;
+
+namespace CodeBeam.UltimateAuth.Core.Contracts
+{
+ public sealed record LoginRequest
+ {
+ public string? TenantId { get; init; }
+ public string Identifier { get; init; } = default!; // username, email etc.
+ public string Secret { get; init; } = default!; // password
+ public DateTime? At { get; init; }
+ public DeviceInfo DeviceInfo { get; init; }
+ public IReadOnlyDictionary? Metadata { get; init; }
+
+ ///
+ /// Hint to request access/refresh tokens when the server mode supports it.
+ /// Server policy may still ignore this.
+ ///
+ public bool RequestTokens { get; init; }
+
+ // Optional
+ public ChainId? ChainId { get; init; }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Login/LoginResult.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Login/LoginResult.cs
new file mode 100644
index 0000000..cc0d44f
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Login/LoginResult.cs
@@ -0,0 +1,38 @@
+using CodeBeam.UltimateAuth.Core.Contracts;
+using CodeBeam.UltimateAuth.Core.Domain;
+
+namespace CodeBeam.UltimateAuth.Core.Contracts
+{
+ public sealed record LoginResult
+ {
+ public LoginStatus Status { get; init; }
+ public AuthSessionId? SessionId { get; init; }
+ public AccessToken? AccessToken { get; init; }
+ public RefreshToken? RefreshToken { get; init; }
+ public LoginContinuation? Continuation { get; init; }
+
+ // Helpers
+ public bool IsSuccess => Status == LoginStatus.Success;
+ public bool RequiresContinuation => Continuation is not null;
+ public bool RequiresMfa => Continuation?.Type == LoginContinuationType.Mfa;
+ public bool RequiresPkce => Continuation?.Type == LoginContinuationType.Pkce;
+
+ public static LoginResult Failed() => new() { Status = LoginStatus.Failed };
+
+ public static LoginResult Success(AuthSessionId sessionId, AuthTokens? tokens = null)
+ => new()
+ {
+ Status = LoginStatus.Success,
+ SessionId = sessionId,
+ AccessToken = tokens?.AccessToken,
+ RefreshToken = tokens?.RefreshToken
+ };
+
+ public static LoginResult Continue(LoginContinuation continuation)
+ => new()
+ {
+ Status = LoginStatus.RequiresContinuation,
+ Continuation = continuation
+ };
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Login/LoginStatus.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Login/LoginStatus.cs
new file mode 100644
index 0000000..94a3902
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Login/LoginStatus.cs
@@ -0,0 +1,9 @@
+namespace CodeBeam.UltimateAuth.Core.Contracts
+{
+ public enum LoginStatus
+ {
+ Success,
+ RequiresContinuation,
+ Failed
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Login/ReauthRequest.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Login/ReauthRequest.cs
new file mode 100644
index 0000000..b1d2565
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Login/ReauthRequest.cs
@@ -0,0 +1,11 @@
+using CodeBeam.UltimateAuth.Core.Domain;
+
+namespace CodeBeam.UltimateAuth.Core.Contracts
+{
+ public sealed record ReauthRequest
+ {
+ public string? TenantId { get; init; }
+ public AuthSessionId SessionId { get; init; }
+ public string Secret { get; init; } = default!;
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Login/ReauthResult.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Login/ReauthResult.cs
new file mode 100644
index 0000000..d14eb10
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Login/ReauthResult.cs
@@ -0,0 +1,7 @@
+namespace CodeBeam.UltimateAuth.Core.Contracts
+{
+ public sealed record ReauthResult
+ {
+ public bool Success { get; init; }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Logout/LogoutAllRequest.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Logout/LogoutAllRequest.cs
new file mode 100644
index 0000000..7bfa2da
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Logout/LogoutAllRequest.cs
@@ -0,0 +1,23 @@
+using CodeBeam.UltimateAuth.Core.Domain;
+
+namespace CodeBeam.UltimateAuth.Core.Contracts
+{
+ public sealed class LogoutAllRequest
+ {
+ public string? TenantId { get; init; }
+
+ ///
+ /// The current session initiating the logout-all operation.
+ /// Used to resolve the active chain when ExceptCurrent is true.
+ ///
+ public AuthSessionId? CurrentSessionId { get; init; }
+
+ ///
+ /// If true, the current session will NOT be revoked.
+ ///
+ public bool ExceptCurrent { get; init; }
+
+ public DateTime? At { get; init; }
+ }
+
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Logout/LogoutRequest.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Logout/LogoutRequest.cs
new file mode 100644
index 0000000..90e6973
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Logout/LogoutRequest.cs
@@ -0,0 +1,16 @@
+using CodeBeam.UltimateAuth.Core.Domain;
+
+namespace CodeBeam.UltimateAuth.Core.Contracts
+{
+ public sealed record LogoutRequest
+ {
+ public string? TenantId { get; init; }
+ public AuthSessionId SessionId { get; init; }
+
+ ///
+ /// Optional logical timestamp for the logout operation.
+ /// If not provided, the flow service will use DateTime.UtcNow.
+ ///
+ public DateTime? At { get; init; }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Mfa/BeginMfaRequest.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Mfa/BeginMfaRequest.cs
new file mode 100644
index 0000000..86af91a
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Mfa/BeginMfaRequest.cs
@@ -0,0 +1,7 @@
+namespace CodeBeam.UltimateAuth.Core.Contracts
+{
+ public sealed record BeginMfaRequest
+ {
+ public string MfaToken { get; init; } = default!;
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Mfa/CompleteMfaRequest.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Mfa/CompleteMfaRequest.cs
new file mode 100644
index 0000000..5d575d0
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Mfa/CompleteMfaRequest.cs
@@ -0,0 +1,8 @@
+namespace CodeBeam.UltimateAuth.Core.Contracts
+{
+ public sealed record CompleteMfaRequest
+ {
+ public string ChallengeId { get; init; } = default!;
+ public string Code { get; init; } = default!;
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Mfa/MfaChallengeResult.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Mfa/MfaChallengeResult.cs
new file mode 100644
index 0000000..9bb085c
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Mfa/MfaChallengeResult.cs
@@ -0,0 +1,8 @@
+namespace CodeBeam.UltimateAuth.Core.Contracts
+{
+ public sealed record MfaChallengeResult
+ {
+ public string ChallengeId { get; init; } = default!;
+ public string Method { get; init; } = default!; // totp, sms, email etc.
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Pkce/PkceChallengeResult.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Pkce/PkceChallengeResult.cs
new file mode 100644
index 0000000..1a4d986
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Pkce/PkceChallengeResult.cs
@@ -0,0 +1,8 @@
+namespace CodeBeam.UltimateAuth.Core.Contracts
+{
+ public sealed record PkceChallengeResult
+ {
+ public string Challenge { get; init; } = default!;
+ public string Method { get; init; } = "S256";
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Pkce/PkceConsumeRequest.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Pkce/PkceConsumeRequest.cs
new file mode 100644
index 0000000..153e865
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Pkce/PkceConsumeRequest.cs
@@ -0,0 +1,7 @@
+namespace CodeBeam.UltimateAuth.Core.Contracts
+{
+ public sealed record PkceConsumeRequest
+ {
+ public string Challenge { get; init; } = default!;
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Pkce/PkceCreateRequest.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Pkce/PkceCreateRequest.cs
new file mode 100644
index 0000000..bd8eb88
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Pkce/PkceCreateRequest.cs
@@ -0,0 +1,7 @@
+namespace CodeBeam.UltimateAuth.Core.Contracts
+{
+ public sealed record PkceCreateRequest
+ {
+ public string ClientId { get; init; } = default!;
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Pkce/PkceVerificationResult.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Pkce/PkceVerificationResult.cs
new file mode 100644
index 0000000..c094b0a
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Pkce/PkceVerificationResult.cs
@@ -0,0 +1,7 @@
+namespace CodeBeam.UltimateAuth.Core.Contracts
+{
+ public sealed record PkceVerificationResult
+ {
+ public bool IsValid { get; init; }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Pkce/PkceVerifyRequest.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Pkce/PkceVerifyRequest.cs
new file mode 100644
index 0000000..9a1d588
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Pkce/PkceVerifyRequest.cs
@@ -0,0 +1,8 @@
+namespace CodeBeam.UltimateAuth.Core.Contracts
+{
+ public sealed record PkceVerifyRequest
+ {
+ public string Challenge { get; init; } = default!;
+ public string Verifier { get; init; } = default!;
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/AuthenticatedSessionContext.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/AuthenticatedSessionContext.cs
new file mode 100644
index 0000000..6422fbb
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/AuthenticatedSessionContext.cs
@@ -0,0 +1,31 @@
+using CodeBeam.UltimateAuth.Core.Domain;
+
+namespace CodeBeam.UltimateAuth.Core.Contracts
+{
+ ///
+ /// Represents the context in which a session is issued
+ /// (login, refresh, reauthentication).
+ ///
+ public sealed class AuthenticatedSessionContext
+ {
+ public string? TenantId { get; init; }
+ public required TUserId UserId { get; init; }
+ public DeviceInfo DeviceInfo { get; init; }
+ public DateTime Now { get; init; }
+ public ClaimsSnapshot? Claims { get; init; }
+ public SessionMetadata Metadata { get; init; }
+
+ ///
+ /// Optional chain identifier.
+ /// If null, a new chain will be created.
+ /// If provided, session will be issued under the existing chain.
+ ///
+ public ChainId? ChainId { get; init; }
+
+ ///
+ /// Indicates that authentication has already been completed.
+ /// This context MUST NOT be constructed from raw credentials.
+ ///
+ public bool IsAuthenticated { get; init; } = true;
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Contexts/Issued/IssuedSession.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/IssuedSession.cs
similarity index 93%
rename from src/CodeBeam.UltimateAuth.Core/Contexts/Issued/IssuedSession.cs
rename to src/CodeBeam.UltimateAuth.Core/Contracts/Session/IssuedSession.cs
index 157663a..cc2f0f8 100644
--- a/src/CodeBeam.UltimateAuth.Core/Contexts/Issued/IssuedSession.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/IssuedSession.cs
@@ -1,6 +1,6 @@
using CodeBeam.UltimateAuth.Core.Domain;
-namespace CodeBeam.UltimateAuth.Core.Contexts
+namespace CodeBeam.UltimateAuth.Core.Contracts
{
///
/// Represents the result of a session issuance operation.
diff --git a/src/CodeBeam.UltimateAuth.Server/Sessions/SessionContext.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionContext.cs
similarity index 94%
rename from src/CodeBeam.UltimateAuth.Server/Sessions/SessionContext.cs
rename to src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionContext.cs
index 3aad385..03af37b 100644
--- a/src/CodeBeam.UltimateAuth.Server/Sessions/SessionContext.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionContext.cs
@@ -1,6 +1,6 @@
using CodeBeam.UltimateAuth.Core.Domain;
-namespace CodeBeam.UltimateAuth.Server.Sessions
+namespace CodeBeam.UltimateAuth.Core.Contracts
{
///
/// Lightweight session context resolved from the incoming request.
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionRefreshRequest.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionRefreshRequest.cs
new file mode 100644
index 0000000..9343883
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionRefreshRequest.cs
@@ -0,0 +1,8 @@
+namespace CodeBeam.UltimateAuth.Core.Contracts
+{
+ public sealed record SessionRefreshRequest
+ {
+ public string? TenantId { get; init; }
+ public string RefreshToken { get; init; } = default!;
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionRefreshResult.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionRefreshResult.cs
new file mode 100644
index 0000000..d207f59
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionRefreshResult.cs
@@ -0,0 +1,10 @@
+using CodeBeam.UltimateAuth.Core.Contracts;
+
+namespace CodeBeam.UltimateAuth.Core.Contracts
+{
+ public sealed record SessionRefreshResult
+ {
+ public AccessToken AccessToken { get; init; } = default!;
+ public RefreshToken? RefreshToken { get; init; }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Models/SessionResult.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionResult.cs
similarity index 97%
rename from src/CodeBeam.UltimateAuth.Core/Models/SessionResult.cs
rename to src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionResult.cs
index 5dbfb06..cb43f4e 100644
--- a/src/CodeBeam.UltimateAuth.Core/Models/SessionResult.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionResult.cs
@@ -1,6 +1,6 @@
using CodeBeam.UltimateAuth.Core.Domain;
-namespace CodeBeam.UltimateAuth.Core.Models
+namespace CodeBeam.UltimateAuth.Core.Contracts
{
// TODO: IsNewChain, IsNewRoot flags?
///
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionRotationContext.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionRotationContext.cs
new file mode 100644
index 0000000..2510afa
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionRotationContext.cs
@@ -0,0 +1,15 @@
+using CodeBeam.UltimateAuth.Core.Domain;
+
+namespace CodeBeam.UltimateAuth.Core.Contracts
+{
+ public sealed record SessionRotationContext
+ {
+ public string? TenantId { get; init; }
+ public AuthSessionId CurrentSessionId { get; init; }
+ public TUserId UserId { get; init; }
+ public DateTime Now { get; init; }
+ public DeviceInfo Device { get; init; }
+ public ClaimsSnapshot Claims { get; init; }
+ public SessionMetadata Metadata { get; init; }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Contexts/SessionStoreContext.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionStoreContext.cs
similarity index 96%
rename from src/CodeBeam.UltimateAuth.Core/Contexts/SessionStoreContext.cs
rename to src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionStoreContext.cs
index fa5a426..78910d4 100644
--- a/src/CodeBeam.UltimateAuth.Core/Contexts/SessionStoreContext.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionStoreContext.cs
@@ -1,6 +1,6 @@
using CodeBeam.UltimateAuth.Core.Domain;
-namespace CodeBeam.UltimateAuth.Core.Contexts
+namespace CodeBeam.UltimateAuth.Core.Contracts
{
///
/// Context information required by the session store when
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionValidationContext.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionValidationContext.cs
new file mode 100644
index 0000000..aa8b3dd
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionValidationContext.cs
@@ -0,0 +1,12 @@
+using CodeBeam.UltimateAuth.Core.Domain;
+
+namespace CodeBeam.UltimateAuth.Core.Contracts
+{
+ public sealed record SessionValidationContext
+ {
+ public string? TenantId { get; init; }
+ public AuthSessionId SessionId { get; init; }
+ public DateTime Now { get; init; }
+ public DeviceInfo Device { get; init; }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionValidationResult.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionValidationResult.cs
new file mode 100644
index 0000000..26e9020
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionValidationResult.cs
@@ -0,0 +1,44 @@
+using CodeBeam.UltimateAuth.Core.Domain;
+
+namespace CodeBeam.UltimateAuth.Core.Contracts
+{
+ public sealed class SessionValidationResult
+ {
+ public SessionState State { get; }
+ public ISession? Session { get; }
+ public ISessionChain? Chain { get; }
+ public ISessionRoot? Root { get; }
+
+ private SessionValidationResult(
+ SessionState state,
+ ISession? session,
+ ISessionChain? chain,
+ ISessionRoot? root)
+ {
+ State = state;
+ Session = session;
+ Chain = chain;
+ Root = root;
+ }
+
+ public bool IsValid => State == SessionState.Active;
+
+ public static SessionValidationResult Active(
+ ISession session,
+ ISessionChain chain,
+ ISessionRoot root)
+ => new(
+ SessionState.Active,
+ session,
+ chain,
+ root);
+
+ public static SessionValidationResult Invalid(
+ SessionState state)
+ => new(
+ state,
+ session: null,
+ chain: null,
+ root: null);
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Contexts/Issued/IssuedAccessToken.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/AccessToken.cs
similarity index 77%
rename from src/CodeBeam.UltimateAuth.Core/Contexts/Issued/IssuedAccessToken.cs
rename to src/CodeBeam.UltimateAuth.Core/Contracts/Token/AccessToken.cs
index 32d37e6..3284350 100644
--- a/src/CodeBeam.UltimateAuth.Core/Contexts/Issued/IssuedAccessToken.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/AccessToken.cs
@@ -1,20 +1,21 @@
-namespace CodeBeam.UltimateAuth.Core.Contexts
+namespace CodeBeam.UltimateAuth.Core.Contracts
{
///
/// Represents an issued access token (JWT or opaque).
///
- public sealed class IssuedAccessToken
+ public sealed class AccessToken
{
///
/// The actual token value sent to the client.
///
public required string Token { get; init; }
+ // TODO: TokenKind enum?
///
/// Token type: "jwt" or "opaque".
/// Used for diagnostics and middleware behavior.
///
- public required string TokenType { get; init; }
+ public TokenType Type { get; init; }
///
/// Expiration time of the token.
@@ -25,5 +26,7 @@ public sealed class IssuedAccessToken
/// Optional session id this token is bound to (Hybrid / SemiHybrid).
///
public string? SessionId { get; init; }
+
+ public string? Scope { get; init; }
}
}
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/AuthTokens.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/AuthTokens.cs
new file mode 100644
index 0000000..344fedd
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/AuthTokens.cs
@@ -0,0 +1,17 @@
+namespace CodeBeam.UltimateAuth.Core.Contracts
+{
+ ///
+ /// Represents a set of authentication tokens issued as a result of a successful login.
+ /// This model is intentionally extensible to support additional token types in the future.
+ ///
+ public sealed record AuthTokens
+ {
+ ///
+ /// The issued access token.
+ /// Always present when is returned.
+ ///
+ public AccessToken AccessToken { get; init; } = default!;
+
+ public RefreshToken? RefreshToken { get; init; }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/OpaqueTokenRecord.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/OpaqueTokenRecord.cs
new file mode 100644
index 0000000..ed13a6a
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/OpaqueTokenRecord.cs
@@ -0,0 +1,18 @@
+using CodeBeam.UltimateAuth.Core.Domain;
+using System.Security.Claims;
+
+namespace CodeBeam.UltimateAuth.Core.Contracts
+{
+ public sealed class OpaqueTokenRecord
+ {
+ public string TokenHash { get; init; } = default!;
+ public string UserId { get; init; } = default!;
+ public string? TenantId { get; init; }
+ public AuthSessionId? SessionId { get; init; }
+ public DateTimeOffset ExpiresAt { get; init; }
+ public bool IsRevoked { get; init; }
+ public DateTimeOffset? RevokedAt { get; init; }
+ public IReadOnlyCollection Claims { get; init; } = Array.Empty();
+ }
+
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Contexts/Issued/IssuedRefreshToken.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/RefreshToken.cs
similarity index 86%
rename from src/CodeBeam.UltimateAuth.Core/Contexts/Issued/IssuedRefreshToken.cs
rename to src/CodeBeam.UltimateAuth.Core/Contracts/Token/RefreshToken.cs
index 1f64804..1e9d87a 100644
--- a/src/CodeBeam.UltimateAuth.Core/Contexts/Issued/IssuedRefreshToken.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/RefreshToken.cs
@@ -1,10 +1,10 @@
-namespace CodeBeam.UltimateAuth.Core.Contexts
+namespace CodeBeam.UltimateAuth.Core.Contracts
{
///
/// Represents an issued refresh token.
/// Always opaque and hashed at rest.
///
- public sealed class IssuedRefreshToken
+ public sealed class RefreshToken
{
///
/// Plain refresh token value (returned to client once).
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenInvalidReason.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenInvalidReason.cs
new file mode 100644
index 0000000..96ce78b
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenInvalidReason.cs
@@ -0,0 +1,16 @@
+namespace CodeBeam.UltimateAuth.Core.Contracts
+{
+ public enum TokenInvalidReason
+ {
+ Invalid,
+ Expired,
+ Revoked,
+ Malformed,
+ SignatureInvalid,
+ AudienceMismatch,
+ IssuerMismatch,
+ MissingSubject,
+ Unknown,
+ NotImplemented
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenIssueContext.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenIssueContext.cs
new file mode 100644
index 0000000..ad3dc76
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenIssueContext.cs
@@ -0,0 +1,11 @@
+using CodeBeam.UltimateAuth.Core.Domain;
+
+namespace CodeBeam.UltimateAuth.Core.Contracts
+{
+ public sealed record TokenIssueContext
+ {
+ public string? TenantId { get; init; }
+ public ISession Session { get; init; } = default!;
+ public DateTime Now { get; init; }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenRefreshContext.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenRefreshContext.cs
new file mode 100644
index 0000000..9507442
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenRefreshContext.cs
@@ -0,0 +1,9 @@
+namespace CodeBeam.UltimateAuth.Core.Contracts
+{
+ public sealed record TokenRefreshContext
+ {
+ public string? TenantId { get; init; }
+
+ public string RefreshToken { get; init; } = default!;
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenType.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenType.cs
new file mode 100644
index 0000000..dc94f72
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenType.cs
@@ -0,0 +1,10 @@
+namespace CodeBeam.UltimateAuth.Core.Contracts
+{
+ public enum TokenType
+ {
+ Opaque,
+ Jwt,
+ Unknown
+ }
+
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenValidationResult.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenValidationResult.cs
new file mode 100644
index 0000000..b8e7c29
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenValidationResult.cs
@@ -0,0 +1,67 @@
+using CodeBeam.UltimateAuth.Core.Domain;
+using System.Security.Claims;
+
+namespace CodeBeam.UltimateAuth.Core.Contracts
+{
+ public sealed record TokenValidationResult
+ {
+ public bool IsValid { get; init; }
+ public TokenType Type { get; init; }
+ public string? TenantId { get; init; }
+ public TUserId? UserId { get; init; }
+ public AuthSessionId? SessionId { get; init; }
+ public IReadOnlyCollection Claims { get; init; } = Array.Empty();
+ public TokenInvalidReason? InvalidReason { get; init; }
+ public DateTime? ExpiresAt { get; set; }
+
+ private TokenValidationResult(
+ bool isValid,
+ TokenType type,
+ string? tenantId,
+ TUserId? userId,
+ AuthSessionId? sessionId,
+ IReadOnlyCollection? claims,
+ TokenInvalidReason? invalidReason,
+ DateTime? expiresAt
+ )
+ {
+ IsValid = isValid;
+ TenantId = tenantId;
+ UserId = userId;
+ SessionId = sessionId;
+ Claims = claims ?? Array.Empty();
+ InvalidReason = invalidReason;
+ ExpiresAt = expiresAt;
+ }
+
+ public static TokenValidationResult Valid(
+ TokenType type,
+ string? tenantId,
+ TUserId userId,
+ AuthSessionId? sessionId,
+ IReadOnlyCollection claims,
+ DateTime? expiresAt)
+ => new(
+ isValid: true,
+ type,
+ tenantId,
+ userId,
+ sessionId,
+ claims,
+ invalidReason: null,
+ expiresAt
+ );
+
+ public static TokenValidationResult Invalid(TokenType type, TokenInvalidReason reason)
+ => new(
+ isValid: false,
+ type,
+ tenantId: null,
+ userId: default,
+ sessionId: null,
+ claims: null,
+ invalidReason: reason,
+ expiresAt: null
+ );
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/User/RegisterUserRequest.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/User/RegisterUserRequest.cs
new file mode 100644
index 0000000..a5565b9
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/User/RegisterUserRequest.cs
@@ -0,0 +1,30 @@
+namespace CodeBeam.UltimateAuth.Core.Contracts
+{
+ ///
+ /// Request to register a new user with credentials.
+ ///
+ public sealed class RegisterUserRequest
+ {
+ ///
+ /// Unique user identifier (username, email, or external id).
+ /// Interpretation is application-specific.
+ ///
+ public required string Identifier { get; init; }
+
+ ///
+ /// Plain-text password.
+ /// Will be hashed by the configured password hasher.
+ ///
+ public required string Password { get; init; }
+
+ ///
+ /// Optional tenant identifier.
+ ///
+ public string? TenantId { get; init; }
+
+ ///
+ /// Optional initial claims or metadata.
+ ///
+ public IReadOnlyDictionary? Metadata { get; init; }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/User/UserAuthenticationResult.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/User/UserAuthenticationResult.cs
new file mode 100644
index 0000000..64e2c34
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/User/UserAuthenticationResult.cs
@@ -0,0 +1,26 @@
+using CodeBeam.UltimateAuth.Core.Domain;
+
+namespace CodeBeam.UltimateAuth.Core.Contracts
+{
+ public sealed class UserAuthenticationResult
+ {
+ public bool Succeeded { get; init; }
+
+ public TUserId? UserId { get; init; }
+
+ public ClaimsSnapshot? Claims { get; init; }
+
+ public bool RequiresMfa { get; init; }
+
+ public static UserAuthenticationResult Fail() => new() { Succeeded = false };
+
+ public static UserAuthenticationResult Success(TUserId userId, ClaimsSnapshot claims, bool requiresMfa = false)
+ => new()
+ {
+ Succeeded = true,
+ UserId = userId,
+ Claims = claims,
+ RequiresMfa = requiresMfa
+ };
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Contexts/UserContext.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/User/UserContext.cs
similarity index 74%
rename from src/CodeBeam.UltimateAuth.Core/Contexts/UserContext.cs
rename to src/CodeBeam.UltimateAuth.Core/Contracts/User/UserContext.cs
index 34f30a2..0c87265 100644
--- a/src/CodeBeam.UltimateAuth.Core/Contexts/UserContext.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/User/UserContext.cs
@@ -1,6 +1,6 @@
-using CodeBeam.UltimateAuth.Core.Abstractions;
+using CodeBeam.UltimateAuth.Core.Domain;
-namespace CodeBeam.UltimateAuth.Core.Contexts
+namespace CodeBeam.UltimateAuth.Core.Contracts
{
public sealed class UserContext
{
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/User/ValidateCredentialsRequest.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/User/ValidateCredentialsRequest.cs
new file mode 100644
index 0000000..fc1dd7e
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/User/ValidateCredentialsRequest.cs
@@ -0,0 +1,24 @@
+namespace CodeBeam.UltimateAuth.Core.Contracts
+{
+ ///
+ /// Request to validate user credentials.
+ /// Used during login flows.
+ ///
+ public sealed class ValidateCredentialsRequest
+ {
+ ///
+ /// User identifier (same value used during registration).
+ ///
+ public required string Identifier { get; init; }
+
+ ///
+ /// Plain-text password provided by the user.
+ ///
+ public required string Password { get; init; }
+
+ ///
+ /// Optional tenant identifier.
+ ///
+ public string? TenantId { get; init; }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Session/ClaimsSnapshot.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Session/ClaimsSnapshot.cs
new file mode 100644
index 0000000..9cf5ad5
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Domain/Session/ClaimsSnapshot.cs
@@ -0,0 +1,69 @@
+namespace CodeBeam.UltimateAuth.Core.Domain
+{
+ public sealed class ClaimsSnapshot
+ {
+ private readonly IReadOnlyDictionary _claims;
+
+ public ClaimsSnapshot(IReadOnlyDictionary claims)
+ {
+ _claims = new Dictionary(claims);
+ }
+
+ public IReadOnlyDictionary AsDictionary() => _claims;
+
+ public bool TryGet(string type, out string value) => _claims.TryGetValue(type, out value);
+
+ public string? Get(string type)
+ => _claims.TryGetValue(type, out var value)
+ ? value
+ : null;
+
+ public static ClaimsSnapshot Empty { get; } = new ClaimsSnapshot(new Dictionary());
+
+ public override bool Equals(object? obj)
+ {
+ if (obj is not ClaimsSnapshot other)
+ return false;
+
+ if (_claims.Count != other._claims.Count)
+ return false;
+
+ foreach (var kv in _claims)
+ {
+ if (!other._claims.TryGetValue(kv.Key, out var v))
+ return false;
+
+ if (!string.Equals(kv.Value, v, StringComparison.Ordinal))
+ return false;
+ }
+
+ return true;
+ }
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ int hash = 17;
+ foreach (var kv in _claims.OrderBy(x => x.Key))
+ {
+ hash = hash * 23 + kv.Key.GetHashCode();
+ hash = hash * 23 + kv.Value.GetHashCode();
+ }
+ return hash;
+ }
+ }
+
+ public static ClaimsSnapshot From(params (string Type, string Value)[] claims)
+ {
+ var dict = new Dictionary(StringComparer.Ordinal);
+ foreach (var (type, value) in claims)
+ dict[type] = value;
+
+ return new ClaimsSnapshot(dict);
+ }
+
+ // TODO: Add ToClaimsPrincipal and FromClaimsPrincipal methods
+
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Session/DeviceInfo.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Session/DeviceInfo.cs
index ca204c3..c70c7e5 100644
--- a/src/CodeBeam.UltimateAuth.Core/Domain/Session/DeviceInfo.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Domain/Session/DeviceInfo.cs
@@ -7,6 +7,11 @@
///
public sealed class DeviceInfo
{
+ ///
+ /// Gets the unique identifier for the device.
+ ///
+ public string DeviceId { get; init; } = default!;
+
///
/// Gets the high-level platform identifier, such as web, mobile,
/// tablet or iot.
@@ -55,6 +60,34 @@ public sealed class DeviceInfo
/// Gets optional custom metadata supplied by the application.
/// Allows additional device attributes not covered by standard fields.
///
- public Dictionary? Custom { get; init; }
+ public Dictionary? Custom { get; init; }
+
+ public static DeviceInfo Unknown { get; } = new()
+ {
+ DeviceId = "unknown",
+ Platform = null,
+ Browser = null,
+ IpAddress = null,
+ UserAgent = null,
+ IsTrusted = null
+ };
+
+ ///
+ /// Determines whether the current device information matches the specified device information based on device
+ /// identifiers.
+ ///
+ /// The device information to compare with the current instance. Cannot be null.
+ /// true if the device identifiers are equal; otherwise, false.
+ public bool Matches(DeviceInfo other)
+ {
+ if (other is null)
+ return false;
+
+ if (DeviceId != other.DeviceId)
+ return false;
+
+ // TODO: UA / IP drift policy
+ return true;
+ }
}
}
diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Session/ISession.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Session/ISession.cs
index 349ae7e..232e13a 100644
--- a/src/CodeBeam.UltimateAuth.Core/Domain/Session/ISession.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Domain/Session/ISession.cs
@@ -56,6 +56,8 @@ public interface ISession
///
DeviceInfo Device { get; }
+ ClaimsSnapshot Claims { get; }
+
///
/// Gets session-scoped metadata used for application-specific extensions,
/// such as tenant data, app version, locale, or CSRF tokens.
@@ -69,5 +71,9 @@ public interface ISession
/// Current timestamp used for comparisons.
/// The evaluated of this session.
SessionState GetState(DateTime now);
+
+ bool ShouldUpdateLastSeen(DateTime now);
+ ISession Touch(DateTime now);
+
}
}
diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Session/ISessionChain.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Session/ISessionChain.cs
index e408b17..358beb8 100644
--- a/src/CodeBeam.UltimateAuth.Core/Domain/Session/ISessionChain.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Domain/Session/ISessionChain.cs
@@ -35,7 +35,7 @@ public interface ISessionChain
/// Useful for offline clients, WASM apps, and environments where
/// full user lookup cannot be performed on each request.
///
- IReadOnlyDictionary? ClaimsSnapshot { get; }
+ ClaimsSnapshot ClaimsSnapshot { get; }
///
/// Gets the identifier of the currently active authentication session, if one exists.
@@ -53,6 +53,10 @@ public interface ISessionChain
/// Gets the timestamp when the chain was revoked, if applicable.
///
DateTime? RevokedAt { get; }
+
+ ISessionChain AttachSession(AuthSessionId sessionId);
+ ISessionChain RotateSession(AuthSessionId sessionId);
+ ISessionChain Revoke(DateTime at);
}
}
diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Session/SessionMetadata.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Session/SessionMetadata.cs
index 20fe7fe..ca81551 100644
--- a/src/CodeBeam.UltimateAuth.Core/Domain/Session/SessionMetadata.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Domain/Session/SessionMetadata.cs
@@ -27,12 +27,6 @@ public sealed class SessionMetadata
///
public string? Locale { get; init; }
- ///
- /// Gets the tenant identifier attached to this session, if applicable.
- /// This value may override or complement root-level multi-tenant resolution.
- ///
- public string? TenantId { get; init; }
-
///
/// Gets a Cross-Site Request Forgery token or other session-scoped secret
/// used for request integrity validation in web applications.
diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Session/SessionState.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Session/SessionState.cs
index 0faaf75..95a6af0 100644
--- a/src/CodeBeam.UltimateAuth.Core/Domain/Session/SessionState.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Domain/Session/SessionState.cs
@@ -6,39 +6,12 @@
///
public enum SessionState
{
- ///
- /// The session is valid, not expired, not revoked, and its security version
- /// matches the user's current security version.
- ///
- Active = 0,
-
- ///
- /// The session has passed its expiration time and is no longer valid.
- ///
- Expired = 1,
-
- ///
- /// The session was explicitly revoked by user action or administrative control.
- ///
- Revoked = 2,
-
- ///
- /// The session's parent chain has been revoked, typically representing a
- /// device-level logout or device ban.
- ///
- ChainRevoked = 3,
-
- ///
- /// The user's entire session root has been revoked. This invalidates all
- /// chains and sessions immediately across all devices.
- ///
- RootRevoked = 4,
-
- ///
- /// The session's stored SecurityVersionAtCreation does not match the user's
- /// current security version, indicating a password reset, MFA reset,
- /// or other critical security event.
- ///
- SecurityVersionMismatch = 5
+ Active,
+ Expired,
+ Revoked,
+ NotFound,
+ Invalid,
+ SecurityMismatch,
+ DeviceMismatch
}
}
diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSession.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSession.cs
index 07dab4e..a56fcd2 100644
--- a/src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSession.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSession.cs
@@ -12,20 +12,22 @@ public sealed class UAuthSession : ISession
public DateTime? RevokedAt { get; }
public long SecurityVersionAtCreation { get; }
public DeviceInfo Device { get; }
+ public ClaimsSnapshot Claims { 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)
+ AuthSessionId sessionId,
+ string? tenantId,
+ TUserId userId,
+ DateTime createdAt,
+ DateTime expiresAt,
+ DateTime? lastSeenAt,
+ bool isRevoked,
+ DateTime? revokedAt,
+ long securityVersionAtCreation,
+ DeviceInfo device,
+ ClaimsSnapshot claims,
+ SessionMetadata metadata)
{
SessionId = sessionId;
TenantId = tenantId;
@@ -37,6 +39,7 @@ private UAuthSession(
RevokedAt = revokedAt;
SecurityVersionAtCreation = securityVersionAtCreation;
Device = device;
+ Claims = claims;
Metadata = metadata;
}
@@ -46,11 +49,11 @@ public static UAuthSession Create(
TUserId userId,
DateTime now,
DateTime expiresAt,
- long securityVersion,
DeviceInfo device,
+ ClaimsSnapshot claims,
SessionMetadata metadata)
{
- return new UAuthSession(
+ return new(
sessionId,
tenantId,
userId,
@@ -59,25 +62,59 @@ public static UAuthSession Create(
lastSeenAt: now,
isRevoked: false,
revokedAt: null,
- securityVersionAtCreation: securityVersion,
+ securityVersionAtCreation: 0,
device: device,
+ claims: claims,
metadata: metadata
);
}
- public UAuthSession WithLastSeen(DateTime now)
+ public UAuthSession WithSecurityVersion(long version)
{
+ if (SecurityVersionAtCreation == version)
+ return this;
+
return new UAuthSession(
SessionId,
TenantId,
UserId,
CreatedAt,
ExpiresAt,
- lastSeenAt: now,
+ LastSeenAt,
+ IsRevoked,
+ RevokedAt,
+ version,
+ Device,
+ Claims,
+ Metadata
+ );
+ }
+
+ public bool ShouldUpdateLastSeen(DateTime now)
+ {
+ if (LastSeenAt is null)
+ return true;
+
+ return (now - LastSeenAt.Value) >= TimeSpan.FromMinutes(1);
+ }
+
+ public ISession Touch(DateTime now)
+ {
+ if (!ShouldUpdateLastSeen(now))
+ return this;
+
+ return new UAuthSession(
+ SessionId,
+ TenantId,
+ UserId,
+ CreatedAt,
+ ExpiresAt,
+ now,
IsRevoked,
RevokedAt,
SecurityVersionAtCreation,
Device,
+ Claims,
Metadata
);
}
@@ -86,17 +123,18 @@ public UAuthSession Revoke(DateTime at)
{
if (IsRevoked) return this;
- return new UAuthSession(
+ return new(
SessionId,
TenantId,
UserId,
CreatedAt,
ExpiresAt,
LastSeenAt,
- isRevoked: true,
- revokedAt: at,
+ true,
+ at,
SecurityVersionAtCreation,
Device,
+ Claims,
Metadata
);
}
diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSessionChain.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSessionChain.cs
index c775877..1afa3aa 100644
--- a/src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSessionChain.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSessionChain.cs
@@ -7,7 +7,7 @@ public sealed class UAuthSessionChain : ISessionChain
public TUserId UserId { get; }
public int RotationCount { get; }
public long SecurityVersionAtCreation { get; }
- public IReadOnlyDictionary? ClaimsSnapshot { get; }
+ public ClaimsSnapshot ClaimsSnapshot { get; }
public AuthSessionId? ActiveSessionId { get; }
public bool IsRevoked { get; }
public DateTime? RevokedAt { get; }
@@ -18,7 +18,7 @@ private UAuthSessionChain(
TUserId userId,
int rotationCount,
long securityVersionAtCreation,
- IReadOnlyDictionary? claimsSnapshot,
+ ClaimsSnapshot claimsSnapshot,
AuthSessionId? activeSessionId,
bool isRevoked,
DateTime? revokedAt)
@@ -39,7 +39,7 @@ public static UAuthSessionChain Create(
string? tenantId,
TUserId userId,
long securityVersion,
- IReadOnlyDictionary? claimsSnapshot = null)
+ ClaimsSnapshot claimsSnapshot)
{
return new UAuthSessionChain(
chainId,
@@ -54,7 +54,25 @@ public static UAuthSessionChain Create(
);
}
- public UAuthSessionChain ActivateSession(AuthSessionId sessionId)
+ public ISessionChain AttachSession(AuthSessionId sessionId)
+ {
+ if (IsRevoked)
+ return this;
+
+ return new UAuthSessionChain(
+ ChainId,
+ TenantId,
+ UserId,
+ RotationCount, // Unchanged on first attach
+ SecurityVersionAtCreation,
+ ClaimsSnapshot,
+ activeSessionId: sessionId,
+ isRevoked: false,
+ revokedAt: null
+ );
+ }
+
+ public ISessionChain RotateSession(AuthSessionId sessionId)
{
if (IsRevoked)
return this;
@@ -72,7 +90,7 @@ public UAuthSessionChain ActivateSession(AuthSessionId sessionId)
);
}
- public UAuthSessionChain Revoke(DateTime at)
+ public ISessionChain Revoke(DateTime at)
{
if (IsRevoked)
return this;
diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/IUser.cs b/src/CodeBeam.UltimateAuth.Core/Domain/User/IUser.cs
similarity index 93%
rename from src/CodeBeam.UltimateAuth.Core/Abstractions/IUser.cs
rename to src/CodeBeam.UltimateAuth.Core/Domain/User/IUser.cs
index b3fe970..5222ca9 100644
--- a/src/CodeBeam.UltimateAuth.Core/Abstractions/IUser.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Domain/User/IUser.cs
@@ -1,4 +1,4 @@
-namespace CodeBeam.UltimateAuth.Core.Abstractions
+namespace CodeBeam.UltimateAuth.Core.Domain
{
///
/// Represents the minimal user abstraction required by UltimateAuth.
diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/User/UserId.cs b/src/CodeBeam.UltimateAuth.Core/Domain/User/UserId.cs
new file mode 100644
index 0000000..962885f
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Domain/User/UserId.cs
@@ -0,0 +1,16 @@
+namespace CodeBeam.UltimateAuth.Core.Domain
+{
+ ///
+ /// Strongly typed identifier for a user.
+ /// Default user id implementation for UltimateAuth.
+ ///
+ public readonly record struct UserId(string Value)
+ {
+ public override string ToString() => Value;
+
+ public static UserId New() => new(Guid.NewGuid().ToString("N"));
+
+ public static implicit operator string(UserId id) => id.Value;
+ public static implicit operator UserId(string value) => new(value);
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Errors/Base/UAuthChainException.cs b/src/CodeBeam.UltimateAuth.Core/Errors/Base/UAuthChainException.cs
new file mode 100644
index 0000000..2238697
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Errors/Base/UAuthChainException.cs
@@ -0,0 +1,17 @@
+using CodeBeam.UltimateAuth.Core.Domain;
+
+namespace CodeBeam.UltimateAuth.Core.Errors
+{
+ public abstract class UAuthChainException : UAuthDomainException
+ {
+ public ChainId ChainId { get; }
+
+ protected UAuthChainException(
+ ChainId chainId,
+ string message)
+ : base(message)
+ {
+ ChainId = chainId;
+ }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthChainLinkMissingException.cs b/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthChainLinkMissingException.cs
new file mode 100644
index 0000000..68ce8bd
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthChainLinkMissingException.cs
@@ -0,0 +1,14 @@
+using CodeBeam.UltimateAuth.Core.Domain;
+
+namespace CodeBeam.UltimateAuth.Core.Errors
+{
+ public sealed class UAuthSessionChainLinkMissingException : UAuthSessionException
+ {
+ public UAuthSessionChainLinkMissingException(AuthSessionId sessionId)
+ : base(
+ sessionId,
+ $"Session '{sessionId}' is not associated with any session chain.")
+ {
+ }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionChainNotFoundException.cs b/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionChainNotFoundException.cs
new file mode 100644
index 0000000..758b7ef
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionChainNotFoundException.cs
@@ -0,0 +1,12 @@
+using CodeBeam.UltimateAuth.Core.Domain;
+
+namespace CodeBeam.UltimateAuth.Core.Errors
+{
+ public sealed class UAuthSessionChainNotFoundException : UAuthChainException
+ {
+ public UAuthSessionChainNotFoundException(ChainId chainId)
+ : base(chainId, $"Session chain '{chainId}' was not found.")
+ {
+ }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionChainRevokedException.cs b/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionChainRevokedException.cs
new file mode 100644
index 0000000..c755b93
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionChainRevokedException.cs
@@ -0,0 +1,14 @@
+using CodeBeam.UltimateAuth.Core.Domain;
+
+namespace CodeBeam.UltimateAuth.Core.Errors
+{
+ public sealed class UAuthSessionChainRevokedException : UAuthChainException
+ {
+ public ChainId ChainId { get; }
+
+ public UAuthSessionChainRevokedException(ChainId chainId)
+ : base(chainId, $"Session chain '{chainId}' has been revoked.")
+ {
+ }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionDeviceMismatchException.cs b/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionDeviceMismatchException.cs
new file mode 100644
index 0000000..bb8660f
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionDeviceMismatchException.cs
@@ -0,0 +1,22 @@
+using CodeBeam.UltimateAuth.Core.Domain;
+
+namespace CodeBeam.UltimateAuth.Core.Errors
+{
+ public sealed class UAuthSessionDeviceMismatchException : UAuthSessionException
+ {
+ public DeviceInfo Expected { get; }
+ public DeviceInfo Actual { get; }
+
+ public UAuthSessionDeviceMismatchException(
+ AuthSessionId sessionId,
+ DeviceInfo expected,
+ DeviceInfo actual)
+ : base(
+ sessionId,
+ $"Session '{sessionId}' device mismatch detected.")
+ {
+ Expected = expected;
+ Actual = actual;
+ }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Errors/UAuthSessionExpiredException.cs b/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionExpiredException.cs
similarity index 100%
rename from src/CodeBeam.UltimateAuth.Core/Errors/UAuthSessionExpiredException.cs
rename to src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionExpiredException.cs
diff --git a/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionInvalidStateException.cs b/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionInvalidStateException.cs
new file mode 100644
index 0000000..bd396bd
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionInvalidStateException.cs
@@ -0,0 +1,19 @@
+using CodeBeam.UltimateAuth.Core.Domain;
+
+namespace CodeBeam.UltimateAuth.Core.Errors
+{
+ public sealed class UAuthSessionInvalidStateException : UAuthSessionException
+ {
+ public SessionState State { get; }
+
+ public UAuthSessionInvalidStateException(
+ AuthSessionId sessionId,
+ SessionState state)
+ : base(
+ sessionId,
+ $"Session '{sessionId}' is in invalid state '{state}'.")
+ {
+ State = state;
+ }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Errors/UAuthSessionNotActiveException.cs b/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionNotActiveException.cs
similarity index 100%
rename from src/CodeBeam.UltimateAuth.Core/Errors/UAuthSessionNotActiveException.cs
rename to src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionNotActiveException.cs
diff --git a/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionNotFoundException.cs b/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionNotFoundException.cs
new file mode 100644
index 0000000..8cc0a59
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionNotFoundException.cs
@@ -0,0 +1,13 @@
+using CodeBeam.UltimateAuth.Core.Domain;
+
+namespace CodeBeam.UltimateAuth.Core.Errors
+{
+ public sealed class UAuthSessionNotFoundException
+ : UAuthSessionException
+ {
+ public UAuthSessionNotFoundException(AuthSessionId sessionId)
+ : base(sessionId, $"Session '{sessionId}' was not found.")
+ {
+ }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Errors/UAuthSessionRevokedException.cs b/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionRevokedException.cs
similarity index 100%
rename from src/CodeBeam.UltimateAuth.Core/Errors/UAuthSessionRevokedException.cs
rename to src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionRevokedException.cs
diff --git a/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionRootRevokedException.cs b/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionRootRevokedException.cs
new file mode 100644
index 0000000..f1c8978
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionRootRevokedException.cs
@@ -0,0 +1,13 @@
+namespace CodeBeam.UltimateAuth.Core.Errors
+{
+ public sealed class UAuthSessionRootRevokedException : Exception
+ {
+ public object UserId { get; }
+
+ public UAuthSessionRootRevokedException(object userId)
+ : base("All sessions for the user have been revoked.")
+ {
+ UserId = userId;
+ }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionSecurityMismatchException.cs b/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionSecurityMismatchException.cs
new file mode 100644
index 0000000..9ba17f1
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Errors/Session/UAuthSessionSecurityMismatchException.cs
@@ -0,0 +1,17 @@
+using CodeBeam.UltimateAuth.Core.Domain;
+using CodeBeam.UltimateAuth.Core.Errors;
+
+public sealed class UAuthSessionSecurityMismatchException : UAuthSessionException
+{
+ public long CurrentSecurityVersion { get; }
+
+ public UAuthSessionSecurityMismatchException(
+ AuthSessionId sessionId,
+ long currentSecurityVersion)
+ : base(
+ sessionId,
+ $"Session '{sessionId}' is invalid due to security version mismatch.")
+ {
+ CurrentSecurityVersion = currentSecurityVersion;
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Errors/UAuthSecurityVersionMismatchException.cs b/src/CodeBeam.UltimateAuth.Core/Errors/UAuthSecurityVersionMismatchException.cs
deleted file mode 100644
index b312dde..0000000
--- a/src/CodeBeam.UltimateAuth.Core/Errors/UAuthSecurityVersionMismatchException.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-namespace CodeBeam.UltimateAuth.Core.Errors
-{
- ///
- /// Represents a domain-level authentication failure caused by a mismatch
- /// between the session's stored security version and the user's current
- /// security version.
- /// A mismatch indicates that a critical security event has occurred
- /// after the session was created—such as a password reset, MFA reset,
- /// account recovery, or other action requiring all prior sessions
- /// to be invalidated.
- ///
- public sealed class UAuthSecurityVersionMismatchException : UAuthDomainException
- {
- ///
- /// Gets the security version captured when the session was created.
- ///
- public long SessionVersion { get; }
-
- ///
- /// Gets the user's current security version, which has increased
- /// since the session was issued.
- ///
- public long UserVersion { get; }
-
- ///
- /// Initializes a new instance of the class
- /// using the session's stored version and the user's current version.
- ///
- /// The security version value stored in the session.
- /// The user's current security version.
- public UAuthSecurityVersionMismatchException(long sessionVersion, long userVersion) : base($"Security version mismatch. Session={sessionVersion}, User={userVersion}")
- {
- SessionVersion = sessionVersion;
- UserVersion = userVersion;
- }
- }
-}
diff --git a/src/CodeBeam.UltimateAuth.Core/Extensions/UltimateAuthServiceCollectionExtensions.cs b/src/CodeBeam.UltimateAuth.Core/Extensions/UltimateAuthServiceCollectionExtensions.cs
index a8f872d..315d384 100644
--- a/src/CodeBeam.UltimateAuth.Core/Extensions/UltimateAuthServiceCollectionExtensions.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Extensions/UltimateAuthServiceCollectionExtensions.cs
@@ -1,9 +1,8 @@
using CodeBeam.UltimateAuth.Core.Abstractions;
using CodeBeam.UltimateAuth.Core.Options;
-using CodeBeam.UltimateAuth.Core.Utilities;
+using CodeBeam.UltimateAuth.Core.Infrastructure;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
namespace CodeBeam.UltimateAuth.Core.Extensions
diff --git a/src/CodeBeam.UltimateAuth.Core/Utilities/Base64Url.cs b/src/CodeBeam.UltimateAuth.Core/Infrastructure/Base64Url.cs
similarity index 96%
rename from src/CodeBeam.UltimateAuth.Core/Utilities/Base64Url.cs
rename to src/CodeBeam.UltimateAuth.Core/Infrastructure/Base64Url.cs
index 8b120f0..48fb6c8 100644
--- a/src/CodeBeam.UltimateAuth.Core/Utilities/Base64Url.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Infrastructure/Base64Url.cs
@@ -1,4 +1,4 @@
-namespace CodeBeam.UltimateAuth.Core.Utilities
+namespace CodeBeam.UltimateAuth.Core.Infrastructure
{
///
/// Provides Base64 URL-safe encoding and decoding utilities.
diff --git a/src/CodeBeam.UltimateAuth.Core/Infrastructure/GuidUserIdFactory.cs b/src/CodeBeam.UltimateAuth.Core/Infrastructure/GuidUserIdFactory.cs
new file mode 100644
index 0000000..afe906b
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Infrastructure/GuidUserIdFactory.cs
@@ -0,0 +1,9 @@
+using CodeBeam.UltimateAuth.Core.Abstractions;
+
+namespace CodeBeam.UltimateAuth.Core.Infrastructure
+{
+ public sealed class GuidUserIdFactory : IUserIdFactory
+ {
+ public Guid Create() => Guid.NewGuid();
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Utilities/RandomIdGenerator.cs b/src/CodeBeam.UltimateAuth.Core/Infrastructure/RandomIdGenerator.cs
similarity index 97%
rename from src/CodeBeam.UltimateAuth.Core/Utilities/RandomIdGenerator.cs
rename to src/CodeBeam.UltimateAuth.Core/Infrastructure/RandomIdGenerator.cs
index 5c63d26..b2faa23 100644
--- a/src/CodeBeam.UltimateAuth.Core/Utilities/RandomIdGenerator.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Infrastructure/RandomIdGenerator.cs
@@ -1,6 +1,6 @@
using System.Security.Cryptography;
-namespace CodeBeam.UltimateAuth.Core.Utilities
+namespace CodeBeam.UltimateAuth.Core.Infrastructure
{
///
/// Provides cryptographically secure random ID generation.
diff --git a/src/CodeBeam.UltimateAuth.Core/Infrastructure/StringUserIdFactory.cs b/src/CodeBeam.UltimateAuth.Core/Infrastructure/StringUserIdFactory.cs
new file mode 100644
index 0000000..a622edf
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Infrastructure/StringUserIdFactory.cs
@@ -0,0 +1,9 @@
+using CodeBeam.UltimateAuth.Core.Abstractions;
+
+namespace CodeBeam.UltimateAuth.Core.Infrastructure
+{
+ public sealed class StringUserIdFactory : IUserIdFactory
+ {
+ public string Create() => Guid.NewGuid().ToString("N");
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Utilities/UAuthUserIdConverter.cs b/src/CodeBeam.UltimateAuth.Core/Infrastructure/UAuthUserIdConverter.cs
similarity index 84%
rename from src/CodeBeam.UltimateAuth.Core/Utilities/UAuthUserIdConverter.cs
rename to src/CodeBeam.UltimateAuth.Core/Infrastructure/UAuthUserIdConverter.cs
index e0ebae5..01085ff 100644
--- a/src/CodeBeam.UltimateAuth.Core/Utilities/UAuthUserIdConverter.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Infrastructure/UAuthUserIdConverter.cs
@@ -4,7 +4,7 @@
using System.Text;
using System.Text.Json;
-namespace CodeBeam.UltimateAuth.Core.Utilities
+namespace CodeBeam.UltimateAuth.Core.Infrastructure
{
///
/// Default implementation of that provides
@@ -71,6 +71,20 @@ public TUserId FromString(string value)
};
}
+ public bool TryFromString(string value, out TUserId? id)
+ {
+ try
+ {
+ id = FromString(value);
+ return true;
+ }
+ catch
+ {
+ id = default;
+ return false;
+ }
+ }
+
///
/// Converts a UTF-8 encoded binary representation back into a user id.
///
@@ -79,5 +93,19 @@ public TUserId FromString(string value)
public TUserId FromBytes(byte[] binary) =>
FromString(Encoding.UTF8.GetString(binary));
+ public bool TryFromBytes(byte[] binary, out TUserId? id)
+ {
+ try
+ {
+ id = FromBytes(binary);
+ return true;
+ }
+ catch
+ {
+ id = default;
+ return false;
+ }
+ }
+
}
}
diff --git a/src/CodeBeam.UltimateAuth.Core/Utilities/UAuthUserIdConverterResolver.cs b/src/CodeBeam.UltimateAuth.Core/Infrastructure/UAuthUserIdConverterResolver.cs
similarity index 97%
rename from src/CodeBeam.UltimateAuth.Core/Utilities/UAuthUserIdConverterResolver.cs
rename to src/CodeBeam.UltimateAuth.Core/Infrastructure/UAuthUserIdConverterResolver.cs
index 8e2f079..8ac22a1 100644
--- a/src/CodeBeam.UltimateAuth.Core/Utilities/UAuthUserIdConverterResolver.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Infrastructure/UAuthUserIdConverterResolver.cs
@@ -1,7 +1,7 @@
using CodeBeam.UltimateAuth.Core.Abstractions;
using Microsoft.Extensions.DependencyInjection;
-namespace CodeBeam.UltimateAuth.Core.Utilities
+namespace CodeBeam.UltimateAuth.Core.Infrastructure
{
///
/// Resolves instances from the DI container.
diff --git a/src/CodeBeam.UltimateAuth.Core/Infrastructure/UserIdFactory.cs b/src/CodeBeam.UltimateAuth.Core/Infrastructure/UserIdFactory.cs
new file mode 100644
index 0000000..7a14b54
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Infrastructure/UserIdFactory.cs
@@ -0,0 +1,10 @@
+using CodeBeam.UltimateAuth.Core.Abstractions;
+using CodeBeam.UltimateAuth.Core.Domain;
+
+namespace CodeBeam.UltimateAuth.Core.Infrastructure
+{
+ public sealed class UserIdFactory : IUserIdFactory
+ {
+ public UserId Create() => new UserId(Guid.NewGuid().ToString("N"));
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Infrastructure/UserRecord.cs b/src/CodeBeam.UltimateAuth.Core/Infrastructure/UserRecord.cs
new file mode 100644
index 0000000..3f80324
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Infrastructure/UserRecord.cs
@@ -0,0 +1,16 @@
+using CodeBeam.UltimateAuth.Core.Domain;
+
+namespace CodeBeam.UltimateAuth.Core.Infrastructure
+{
+ public sealed class UserRecord
+ {
+ public required TUserId Id { get; init; }
+ public required string Username { get; init; }
+ public required string PasswordHash { get; init; }
+ public ClaimsSnapshot Claims { get; init; } = ClaimsSnapshot.Empty;
+ public bool RequiresMfa { get; init; }
+ public bool IsActive { get; init; } = true;
+ public DateTime CreatedAt { get; init; }
+ public bool IsDeleted { get; init; }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Models/SessionValidationResult.cs b/src/CodeBeam.UltimateAuth.Core/Models/SessionValidationResult.cs
deleted file mode 100644
index 31560b1..0000000
--- a/src/CodeBeam.UltimateAuth.Core/Models/SessionValidationResult.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-using CodeBeam.UltimateAuth.Core.Domain;
-
-namespace CodeBeam.UltimateAuth.Core.Models
-{
- ///
- /// Represents the outcome of validating a session, including the resolved session,
- /// its chain and root structures, and the computed validation state.
- ///
- ///
- /// Session, Chain and Root may be null if validation fails or if the session
- /// does not exist. State always indicates the final resolved status.
- ///
- public sealed class SessionValidationResult
- {
- ///
- /// The resolved session instance, or null if the session was not found.
- ///
- public ISession? Session { get; init; }
-
- ///
- /// The session chain that owns the session, or null if unavailable.
- ///
- public ISessionChain? Chain { get; init; }
-
- ///
- /// The session root associated with the user, or null if unavailable.
- ///
- public ISessionRoot? Root { get; init; }
-
- ///
- /// The final computed validation state for the session.
- ///
- public SessionState State { get; init; }
- }
-}
diff --git a/src/CodeBeam.UltimateAuth.Core/Enums/UAuthMode.cs b/src/CodeBeam.UltimateAuth.Core/Options/UAuthMode.cs
similarity index 100%
rename from src/CodeBeam.UltimateAuth.Core/Enums/UAuthMode.cs
rename to src/CodeBeam.UltimateAuth.Core/Options/UAuthMode.cs
diff --git a/src/CodeBeam.UltimateAuth.Server/Abstractions/IDeviceResolver.cs b/src/CodeBeam.UltimateAuth.Server/Abstractions/IDeviceResolver.cs
new file mode 100644
index 0000000..adb05d4
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Abstractions/IDeviceResolver.cs
@@ -0,0 +1,13 @@
+using Microsoft.AspNetCore.Http;
+using CodeBeam.UltimateAuth.Core.Domain;
+
+namespace CodeBeam.UltimateAuth.Server.Abstractions
+{
+ ///
+ /// Resolves device and client metadata from the current HTTP context.
+ ///
+ public interface IDeviceResolver
+ {
+ DeviceInfo Resolve(HttpContext context);
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/CodeBeam.UltimateAuth.Server.csproj b/src/CodeBeam.UltimateAuth.Server/CodeBeam.UltimateAuth.Server.csproj
index d4e9395..957c721 100644
--- a/src/CodeBeam.UltimateAuth.Server/CodeBeam.UltimateAuth.Server.csproj
+++ b/src/CodeBeam.UltimateAuth.Server/CodeBeam.UltimateAuth.Server.csproj
@@ -8,15 +8,19 @@
-
+
-
+
+
+
+
+
diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultLoginEndpointHandler.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultLoginEndpointHandler.cs
index 2b94841..cb63cef 100644
--- a/src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultLoginEndpointHandler.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultLoginEndpointHandler.cs
@@ -1,12 +1,60 @@
-using Microsoft.AspNetCore.Http;
+using CodeBeam.UltimateAuth.Core.Abstractions;
+using CodeBeam.UltimateAuth.Core.Contracts;
+using CodeBeam.UltimateAuth.Server.Abstractions;
+using CodeBeam.UltimateAuth.Server.Endpoints;
+using CodeBeam.UltimateAuth.Server.MultiTenancy;
+using Microsoft.AspNetCore.Http;
-namespace CodeBeam.UltimateAuth.Server.Endpoints
+public sealed class DefaultLoginEndpointHandler : ILoginEndpointHandler
{
- public class DefaultLoginEndpointHandler : ILoginEndpointHandler
+ private readonly IUAuthFlowService _flow;
+ private readonly IDeviceResolver _deviceResolver;
+ private readonly ITenantResolver _tenantResolver;
+ private readonly IClock _clock;
+
+ public DefaultLoginEndpointHandler(
+ IUAuthFlowService flow,
+ IDeviceResolver deviceResolver,
+ ITenantResolver tenantResolver,
+ IClock clock)
+ {
+ _flow = flow;
+ _deviceResolver = deviceResolver;
+ _tenantResolver = tenantResolver;
+ _clock = clock;
+ }
+
+ public async Task LoginAsync(HttpContext ctx)
{
- public Task LoginAsync(HttpContext ctx)
+ var request = await ctx.Request.ReadFromJsonAsync();
+ if (request is null)
+ return Results.BadRequest("Invalid login request.");
+
+ var tenantCtx = await _tenantResolver.ResolveAsync(ctx);
+
+ var flowRequest = request with
{
- return Task.FromResult(Results.StatusCode(StatusCodes.Status501NotImplemented));
- }
+ TenantId = tenantCtx.TenantId,
+ At = _clock.UtcNow,
+ DeviceInfo = _deviceResolver.Resolve(ctx)
+ };
+
+ var result = await _flow.LoginAsync(flowRequest, ctx.RequestAborted);
+
+ return result.Status switch
+ {
+ LoginStatus.Success => Results.Ok(new
+ {
+ sessionId = result.SessionId,
+ accessToken = result.AccessToken,
+ refreshToken = result.RefreshToken
+ }),
+
+ LoginStatus.RequiresContinuation => Results.Accepted(null, result.Continuation),
+
+ LoginStatus.Failed => Results.Unauthorized(),
+
+ _ => Results.StatusCode(500)
+ };
}
}
diff --git a/src/CodeBeam.UltimateAuth.Server/Events/.gitkeep b/src/CodeBeam.UltimateAuth.Server/Events/.gitkeep
deleted file mode 100644
index 5f28270..0000000
--- a/src/CodeBeam.UltimateAuth.Server/Events/.gitkeep
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/CodeBeam.UltimateAuth.Server/Extensions/ClaimsSnapshotExtensions.cs b/src/CodeBeam.UltimateAuth.Server/Extensions/ClaimsSnapshotExtensions.cs
new file mode 100644
index 0000000..bfdbee3
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Extensions/ClaimsSnapshotExtensions.cs
@@ -0,0 +1,14 @@
+using CodeBeam.UltimateAuth.Core.Domain;
+using System.Security.Claims;
+
+namespace CodeBeam.UltimateAuth.Server.Extensions
+{
+ public static class ClaimsSnapshotExtensions
+ {
+ public static IReadOnlyCollection AsClaims(
+ this ClaimsSnapshot snapshot)
+ => snapshot.AsDictionary()
+ .Select(kv => new Claim(kv.Key, kv.Value))
+ .ToArray();
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Extensions/HttpContextSessionExtensions.cs b/src/CodeBeam.UltimateAuth.Server/Extensions/HttpContextSessionExtensions.cs
index 474f783..4d3063f 100644
--- a/src/CodeBeam.UltimateAuth.Server/Extensions/HttpContextSessionExtensions.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Extensions/HttpContextSessionExtensions.cs
@@ -1,17 +1,14 @@
-using CodeBeam.UltimateAuth.Server.Middlewares;
-using CodeBeam.UltimateAuth.Server.Sessions;
+using CodeBeam.UltimateAuth.Core.Contracts;
+using CodeBeam.UltimateAuth.Server.Middlewares;
using Microsoft.AspNetCore.Http;
namespace CodeBeam.UltimateAuth.Server.Extensions
{
public static class HttpContextSessionExtensions
{
- public static SessionContext GetSessionContext(
- this HttpContext context)
+ public static SessionContext GetSessionContext(this HttpContext context)
{
- if (context.Items.TryGetValue(
- SessionResolutionMiddleware.SessionContextKey,
- out var value)
+ if (context.Items.TryGetValue(SessionResolutionMiddleware.SessionContextKey, out var value)
&& value is SessionContext session)
{
return session;
diff --git a/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthServerServiceCollectionExtensions.cs b/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthServerServiceCollectionExtensions.cs
index 8520014..c34701d 100644
--- a/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthServerServiceCollectionExtensions.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthServerServiceCollectionExtensions.cs
@@ -1,10 +1,13 @@
-using CodeBeam.UltimateAuth.Core.Extensions;
+using CodeBeam.UltimateAuth.Core.Abstractions;
+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 CodeBeam.UltimateAuth.Server.Services;
+using CodeBeam.UltimateAuth.Server.Users;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
@@ -84,6 +87,11 @@ private static IServiceCollection AddUltimateAuthServerInternal(
services.TryAddScoped();
+ services.AddScoped(typeof(IUAuthFlowService), typeof(UAuthFlowService<>));
+ services.AddScoped(typeof(IUAuthSessionService<>), typeof(UAuthSessionService<>));
+ services.AddScoped(typeof(IUAuthUserService<>), typeof(UAuthUserService<>));
+ services.AddScoped(typeof(IUAuthTokenService<>), typeof(UAuthTokenService<>));
+
// -----------------------------
// SESSION / TOKEN ISSUERS
// -----------------------------
diff --git a/src/CodeBeam.UltimateAuth.Server/Sessions/BearerSessionIdResolver.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/BearerSessionIdResolver.cs
similarity index 92%
rename from src/CodeBeam.UltimateAuth.Server/Sessions/BearerSessionIdResolver.cs
rename to src/CodeBeam.UltimateAuth.Server/Infrastructure/BearerSessionIdResolver.cs
index 49293e4..f2ffcec 100644
--- a/src/CodeBeam.UltimateAuth.Server/Sessions/BearerSessionIdResolver.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/BearerSessionIdResolver.cs
@@ -1,7 +1,7 @@
using CodeBeam.UltimateAuth.Core.Domain;
using Microsoft.AspNetCore.Http;
-namespace CodeBeam.UltimateAuth.Server.Sessions
+namespace CodeBeam.UltimateAuth.Server.Infrastructure
{
public sealed class BearerSessionIdResolver : ISessionIdResolver
{
diff --git a/src/CodeBeam.UltimateAuth.Server/Sessions/CompositeSessionIdResolver.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/CompositeSessionIdResolver.cs
similarity index 92%
rename from src/CodeBeam.UltimateAuth.Server/Sessions/CompositeSessionIdResolver.cs
rename to src/CodeBeam.UltimateAuth.Server/Infrastructure/CompositeSessionIdResolver.cs
index b69111b..dd5c6f6 100644
--- a/src/CodeBeam.UltimateAuth.Server/Sessions/CompositeSessionIdResolver.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/CompositeSessionIdResolver.cs
@@ -1,7 +1,7 @@
using CodeBeam.UltimateAuth.Core.Domain;
using Microsoft.AspNetCore.Http;
-namespace CodeBeam.UltimateAuth.Server.Sessions
+namespace CodeBeam.UltimateAuth.Server.Infrastructure
{
public sealed class CompositeSessionIdResolver : ISessionIdResolver
{
diff --git a/src/CodeBeam.UltimateAuth.Server/Sessions/CookieSessionIdResolver.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/CookieSessionIdResolver.cs
similarity index 92%
rename from src/CodeBeam.UltimateAuth.Server/Sessions/CookieSessionIdResolver.cs
rename to src/CodeBeam.UltimateAuth.Server/Infrastructure/CookieSessionIdResolver.cs
index cb33ac7..c7d533e 100644
--- a/src/CodeBeam.UltimateAuth.Server/Sessions/CookieSessionIdResolver.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/CookieSessionIdResolver.cs
@@ -1,7 +1,7 @@
using CodeBeam.UltimateAuth.Core.Domain;
using Microsoft.AspNetCore.Http;
-namespace CodeBeam.UltimateAuth.Server.Sessions
+namespace CodeBeam.UltimateAuth.Server.Infrastructure
{
public sealed class CookieSessionIdResolver : ISessionIdResolver
{
diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/DefaultUserAuthenticator.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/DefaultUserAuthenticator.cs
new file mode 100644
index 0000000..fbfdcfd
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/DefaultUserAuthenticator.cs
@@ -0,0 +1,40 @@
+using CodeBeam.UltimateAuth.Core.Abstractions;
+using CodeBeam.UltimateAuth.Core.Contracts;
+
+namespace CodeBeam.UltimateAuth.Server.Infrastructure
+{
+ public sealed class DefaultUserAuthenticator : IUserAuthenticator
+ {
+ private readonly IUAuthUserStore _userStore;
+ private readonly IUAuthPasswordHasher _passwordHasher;
+
+ public DefaultUserAuthenticator(IUAuthUserStore userStore, IUAuthPasswordHasher passwordHasher)
+ {
+ _userStore = userStore;
+ _passwordHasher = passwordHasher;
+ }
+
+ public async Task> AuthenticateAsync(
+ string? tenantId,
+ string username,
+ string secret,
+ CancellationToken cancellationToken = default)
+ {
+ var user = await _userStore.FindByUsernameAsync(
+ tenantId,
+ username,
+ cancellationToken);
+
+ if (user is null)
+ return UserAuthenticationResult.Fail();
+
+ if (!user.IsActive)
+ return UserAuthenticationResult.Fail();
+
+ if (!_passwordHasher.Verify(secret, user.PasswordHash))
+ return UserAuthenticationResult.Fail();
+
+ return UserAuthenticationResult.Success(user.Id, user.Claims, user.RequiresMfa);
+ }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Sessions/HeaderSessionIdResolver.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/HeaderSessionIdResolver.cs
similarity index 92%
rename from src/CodeBeam.UltimateAuth.Server/Sessions/HeaderSessionIdResolver.cs
rename to src/CodeBeam.UltimateAuth.Server/Infrastructure/HeaderSessionIdResolver.cs
index aad25f4..ca6521e 100644
--- a/src/CodeBeam.UltimateAuth.Server/Sessions/HeaderSessionIdResolver.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/HeaderSessionIdResolver.cs
@@ -1,7 +1,7 @@
using CodeBeam.UltimateAuth.Core.Domain;
using Microsoft.AspNetCore.Http;
-namespace CodeBeam.UltimateAuth.Server.Sessions
+namespace CodeBeam.UltimateAuth.Server.Infrastructure
{
public sealed class HeaderSessionIdResolver : ISessionIdResolver
{
diff --git a/src/CodeBeam.UltimateAuth.Server/Sessions/ISessionIdResolver.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/ISessionIdResolver.cs
similarity index 77%
rename from src/CodeBeam.UltimateAuth.Server/Sessions/ISessionIdResolver.cs
rename to src/CodeBeam.UltimateAuth.Server/Infrastructure/ISessionIdResolver.cs
index bddb506..46c063b 100644
--- a/src/CodeBeam.UltimateAuth.Server/Sessions/ISessionIdResolver.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/ISessionIdResolver.cs
@@ -1,7 +1,7 @@
using CodeBeam.UltimateAuth.Core.Domain;
using Microsoft.AspNetCore.Http;
-namespace CodeBeam.UltimateAuth.Server.Sessions
+namespace CodeBeam.UltimateAuth.Server.Infrastructure
{
public interface ISessionIdResolver
{
diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/ISessionOrchestrator.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/ISessionOrchestrator.cs
new file mode 100644
index 0000000..b388f55
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/ISessionOrchestrator.cs
@@ -0,0 +1,30 @@
+using CodeBeam.UltimateAuth.Core.Contracts;
+using CodeBeam.UltimateAuth.Core.Domain;
+
+namespace CodeBeam.UltimateAuth.Server.Infrastructure
+{
+ internal interface ISessionOrchestrator
+ {
+ Task> CreateLoginSessionAsync(AuthenticatedSessionContext context);
+
+ Task> RotateSessionAsync(SessionRotationContext context);
+
+ Task> ValidateSessionAsync(SessionValidationContext context);
+
+ Task?> GetSessionAsync(string? tenantId, AuthSessionId sessionId);
+
+ Task>> GetChainsAsync(string? tenantId, TUserId userId);
+
+ Task ResolveChainIdAsync(string? tenantId,AuthSessionId sessionId);
+
+ Task>> GetSessionsAsync(string? tenantId, ChainId chainId);
+
+ Task RevokeSessionAsync(string? tenantId, AuthSessionId sessionId, DateTime at);
+
+ Task RevokeChainAsync(string? tenantId, ChainId chainId, DateTime at);
+
+ Task RevokeAllChainsAsync(string? tenantId, TUserId userId, ChainId? exceptChainId,DateTime at);
+
+ Task RevokeRootAsync(string? tenantId, TUserId userId, DateTime at);
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/ITokenIssuer.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/ITokenIssuer.cs
new file mode 100644
index 0000000..803bb4b
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/ITokenIssuer.cs
@@ -0,0 +1,14 @@
+using CodeBeam.UltimateAuth.Core.Contracts;
+
+namespace CodeBeam.UltimateAuth.Server.Infrastructure
+{
+ ///
+ /// Issues access and refresh tokens according to the active auth mode.
+ /// Does not perform persistence or validation.
+ ///
+ public interface ITokenIssuer
+ {
+ Task IssueAccessTokenAsync(TokenIssuanceContext context, CancellationToken cancellationToken = default);
+ Task IssueRefreshTokenAsync(TokenIssuanceContext context, CancellationToken cancellationToken = default);
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Users/IUserAccessor.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/IUserAccessor.cs
similarity index 71%
rename from src/CodeBeam.UltimateAuth.Server/Users/IUserAccessor.cs
rename to src/CodeBeam.UltimateAuth.Server/Infrastructure/IUserAccessor.cs
index 05a0e5a..87de8da 100644
--- a/src/CodeBeam.UltimateAuth.Server/Users/IUserAccessor.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/IUserAccessor.cs
@@ -1,6 +1,6 @@
using Microsoft.AspNetCore.Http;
-namespace CodeBeam.UltimateAuth.Server.Users
+namespace CodeBeam.UltimateAuth.Server.Infrastructure
{
public interface IUserAccessor
{
diff --git a/src/CodeBeam.UltimateAuth.Server/Sessions/QuerySessionIdResolver.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/QuerySessionIdResolver.cs
similarity index 92%
rename from src/CodeBeam.UltimateAuth.Server/Sessions/QuerySessionIdResolver.cs
rename to src/CodeBeam.UltimateAuth.Server/Infrastructure/QuerySessionIdResolver.cs
index 3019d8c..237a9b1 100644
--- a/src/CodeBeam.UltimateAuth.Server/Sessions/QuerySessionIdResolver.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/QuerySessionIdResolver.cs
@@ -1,7 +1,7 @@
using CodeBeam.UltimateAuth.Core.Domain;
using Microsoft.AspNetCore.Http;
-namespace CodeBeam.UltimateAuth.Server.Sessions
+namespace CodeBeam.UltimateAuth.Server.Infrastructure
{
public sealed class QuerySessionIdResolver : ISessionIdResolver
{
diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/SystemClock.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/SystemClock.cs
new file mode 100644
index 0000000..5a3b064
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/SystemClock.cs
@@ -0,0 +1,9 @@
+using CodeBeam.UltimateAuth.Core.Abstractions;
+
+namespace CodeBeam.UltimateAuth.Server.Infrastructure
+{
+ public sealed class SystemClock : IClock
+ {
+ public DateTime UtcNow => DateTime.UtcNow;
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/TokenIssuanceContext.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/TokenIssuanceContext.cs
new file mode 100644
index 0000000..67e8ea6
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/TokenIssuanceContext.cs
@@ -0,0 +1,13 @@
+using System.Security.Claims;
+
+namespace CodeBeam.UltimateAuth.Server.Infrastructure
+{
+ public sealed record TokenIssuanceContext
+ {
+ public string UserId { get; init; } = default!;
+ public string? TenantId { get; init; }
+ public IReadOnlyCollection Claims { get; init; } = Array.Empty();
+ public string? SessionId { get; init; }
+ public DateTime IssuedAt { get; init; }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/UAuthDeviceResolver.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/UAuthDeviceResolver.cs
new file mode 100644
index 0000000..15ef29a
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/UAuthDeviceResolver.cs
@@ -0,0 +1,49 @@
+using CodeBeam.UltimateAuth.Core.Domain;
+using CodeBeam.UltimateAuth.Server.Abstractions;
+using Microsoft.AspNetCore.Http;
+
+namespace CodeBeam.UltimateAuth.Server.Infrastructure
+{
+ public sealed class DefaultDeviceResolver : IDeviceResolver
+ {
+ public DeviceInfo Resolve(HttpContext context)
+ {
+ var request = context.Request;
+
+ return new DeviceInfo
+ {
+ DeviceId = ResolveDeviceId(context),
+ Platform = ResolvePlatform(request),
+ OperatingSystem = null, // optional UA parsing later
+ Browser = request.Headers.UserAgent.ToString(),
+ IpAddress = context.Connection.RemoteIpAddress?.ToString(),
+ UserAgent = request.Headers.UserAgent.ToString(),
+ IsTrusted = null
+ };
+ }
+
+ private static string ResolveDeviceId(HttpContext context)
+ {
+ if (context.Request.Headers.TryGetValue("X-Device-Id", out var header))
+ return header.ToString();
+
+ if (context.Request.Cookies.TryGetValue("ua_device", out var cookie))
+ return cookie;
+
+ return "unknown";
+ }
+
+ private static string? ResolvePlatform(HttpRequest request)
+ {
+ var ua = request.Headers.UserAgent.ToString().ToLowerInvariant();
+
+ if (ua.Contains("android")) return "android";
+ if (ua.Contains("iphone") || ua.Contains("ipad")) return "ios";
+ if (ua.Contains("windows")) return "windows";
+ if (ua.Contains("mac os")) return "macos";
+ if (ua.Contains("linux")) return "linux";
+
+ return "web";
+ }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Sessions/UAuthSessionIdResolver.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/UAuthSessionIdResolver.cs
similarity index 96%
rename from src/CodeBeam.UltimateAuth.Server/Sessions/UAuthSessionIdResolver.cs
rename to src/CodeBeam.UltimateAuth.Server/Infrastructure/UAuthSessionIdResolver.cs
index 75afe79..9219aaa 100644
--- a/src/CodeBeam.UltimateAuth.Server/Sessions/UAuthSessionIdResolver.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/UAuthSessionIdResolver.cs
@@ -2,7 +2,7 @@
using CodeBeam.UltimateAuth.Server.Options;
using Microsoft.Extensions.Options;
-namespace CodeBeam.UltimateAuth.Server.Sessions
+namespace CodeBeam.UltimateAuth.Server.Infrastructure
{
public sealed class UAuthSessionIdResolver : ISessionIdResolver
{
diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/UAuthSessionOrchestrator.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/UAuthSessionOrchestrator.cs
new file mode 100644
index 0000000..7bcd741
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/UAuthSessionOrchestrator.cs
@@ -0,0 +1,337 @@
+using CodeBeam.UltimateAuth.Core.Abstractions;
+using CodeBeam.UltimateAuth.Core.Contracts;
+using CodeBeam.UltimateAuth.Core.Domain;
+using CodeBeam.UltimateAuth.Core.Errors;
+using CodeBeam.UltimateAuth.Server.Issuers;
+using CodeBeam.UltimateAuth.Server.Options;
+
+namespace CodeBeam.UltimateAuth.Server.Infrastructure
+{
+ ///
+ /// 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(AuthenticatedSessionContext context)
+ {
+ var kernel = _factory.Create(context.TenantId);
+
+ 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 UAuthSessionRootRevokedException(context.UserId!);
+ }
+
+ ISessionChain chain;
+
+ if (context.ChainId is not null)
+ {
+ chain = await kernel.GetChainAsync(
+ context.TenantId,
+ context.ChainId.Value)
+ ?? throw new UAuthSessionChainNotFoundException(
+ context.ChainId.Value);
+
+ if (chain.IsRevoked)
+ throw new UAuthSessionChainRevokedException(
+ chain.ChainId);
+ }
+ else
+ {
+ chain = UAuthSessionChain.Create(
+ ChainId.New(),
+ context.TenantId,
+ context.UserId,
+ root.SecurityVersion,
+ context.Claims);
+ }
+
+ var issuedSession = await _sessionIssuer.IssueAsync(
+ context,
+ chain);
+
+ await kernel.ExecuteAsync(async () =>
+ {
+ await kernel.SaveSessionAsync(
+ context.TenantId,
+ issuedSession.Session);
+
+ var updatedChain = chain.AttachSession(
+ issuedSession.Session.SessionId);
+
+ await kernel.SaveChainAsync(
+ context.TenantId,
+ updatedChain);
+
+ await kernel.SaveSessionRootAsync(
+ context.TenantId,
+ root);
+ });
+
+ return issuedSession;
+ }
+
+ public async Task> RotateSessionAsync(SessionRotationContext context)
+ {
+ var kernel = _factory.Create(context.TenantId);
+
+ var currentSession = await kernel.GetSessionAsync(
+ context.TenantId,
+ context.CurrentSessionId);
+
+ if (currentSession is null)
+ throw new UAuthSessionNotFoundException(context.CurrentSessionId);
+
+ if (currentSession.IsRevoked)
+ throw new UAuthSessionRevokedException(context.CurrentSessionId);
+
+ var state = currentSession.GetState(context.Now);
+ if (state != SessionState.Active)
+ throw new UAuthSessionInvalidStateException(
+ context.CurrentSessionId, state);
+
+ var chainId = await kernel.GetChainIdBySessionAsync(
+ context.TenantId,
+ context.CurrentSessionId);
+
+ if (chainId is null)
+ throw new UAuthSessionChainLinkMissingException(context.CurrentSessionId);
+
+ var chain = await kernel.GetChainAsync(
+ context.TenantId,
+ chainId.Value);
+
+ if (chain is null || chain.IsRevoked)
+ throw new UAuthSessionChainRevokedException(chainId.Value);
+
+ var root = await kernel.GetSessionRootAsync(
+ context.TenantId,
+ currentSession.UserId);
+
+ if (root is null || root.IsRevoked)
+ throw new UAuthSessionRootRevokedException(
+ currentSession.UserId!);
+
+ if (currentSession.SecurityVersionAtCreation != root.SecurityVersion)
+ throw new UAuthSessionSecurityMismatchException(
+ context.CurrentSessionId,
+ root.SecurityVersion);
+
+ var issueContext = new AuthenticatedSessionContext
+ {
+ TenantId = root.TenantId,
+ UserId = currentSession.UserId,
+ Now = context.Now,
+ DeviceInfo = context.Device,
+ Claims = context.Claims
+ };
+
+ var issuedSession = await _sessionIssuer.IssueAsync(
+ issueContext,
+ chain);
+
+ await kernel.ExecuteAsync(async () =>
+ {
+ await kernel.RevokeSessionAsync(
+ context.TenantId,
+ context.CurrentSessionId,
+ context.Now);
+
+ await kernel.SaveSessionAsync(
+ context.TenantId,
+ issuedSession.Session);
+
+ var rotatedChain = chain.RotateSession(
+ issuedSession.Session.SessionId);
+
+ await kernel.SaveChainAsync(
+ context.TenantId,
+ rotatedChain);
+
+ await kernel.SaveSessionRootAsync(
+ context.TenantId,
+ root);
+ });
+
+ return issuedSession;
+ }
+
+ public async Task> ValidateSessionAsync(
+ SessionValidationContext context)
+ {
+ var kernel = _factory.Create(context.TenantId);
+
+ // 1️⃣ Load session
+ var session = await kernel.GetSessionAsync(
+ context.TenantId,
+ context.SessionId);
+
+ if (session is null)
+ return SessionValidationResult.Invalid(SessionState.NotFound);
+
+ var state = session.GetState(context.Now);
+
+ if (state != SessionState.Active)
+ return SessionValidationResult.Invalid(state);
+
+ // 2️⃣ Resolve chain
+ var chainId = await kernel.GetChainIdBySessionAsync(
+ context.TenantId,
+ context.SessionId);
+
+ if (chainId is null)
+ return SessionValidationResult.Invalid(SessionState.Invalid);
+
+ var chain = await kernel.GetChainAsync(
+ context.TenantId,
+ chainId.Value);
+
+ if (chain is null || chain.IsRevoked)
+ return SessionValidationResult.Invalid(SessionState.Revoked);
+
+ // 3️⃣ Resolve root
+ var root = await kernel.GetSessionRootAsync(
+ context.TenantId,
+ session.UserId);
+
+ if (root is null || root.IsRevoked)
+ return SessionValidationResult.Invalid(SessionState.Revoked);
+
+ // 4️⃣ Security version check
+ if (session.SecurityVersionAtCreation != root.SecurityVersion)
+ return SessionValidationResult.Invalid(SessionState.SecurityMismatch);
+
+ // 5️⃣ Device check
+ if (!session.Device.Matches(context.Device))
+ return SessionValidationResult.Invalid(SessionState.DeviceMismatch);
+
+ // 6️⃣ Touch session (best-effort)
+ if (session.ShouldUpdateLastSeen(context.Now))
+ {
+ var updated = session.Touch(context.Now);
+ await kernel.SaveSessionAsync(context.TenantId, updated);
+ session = updated;
+ }
+
+ // 7️⃣ Success
+ return SessionValidationResult.Active(
+ session,
+ chain,
+ root);
+ }
+
+ public Task?> GetSessionAsync(string? tenantId, AuthSessionId sessionId)
+ {
+ var kernel = _factory.Create(tenantId);
+ return kernel.GetSessionAsync(tenantId, sessionId);
+ }
+
+ public Task>> GetSessionsAsync(string? tenantId, ChainId chainId)
+ {
+ var kernel = _factory.Create(tenantId);
+ return kernel.GetSessionsByChainAsync(tenantId, chainId);
+ }
+
+ public Task>> GetChainsAsync(string? tenantId, TUserId userId)
+ {
+ var kernel = _factory.Create(tenantId);
+ return kernel.GetChainsByUserAsync(tenantId, userId);
+ }
+
+ public async Task ResolveChainIdAsync(
+ string? tenantId,
+ AuthSessionId sessionId)
+ {
+ var kernel = _factory.Create(tenantId);
+ return await kernel.GetChainIdBySessionAsync(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(tenantId);
+ await kernel.RevokeChainAsync(tenantId, chainId, at);
+ }
+
+ public async Task RevokeAllChainsAsync(
+ string? tenantId,
+ TUserId userId,
+ ChainId? exceptChainId,
+ DateTime at)
+ {
+ var kernel = _factory.Create(tenantId);
+
+ var chains = await kernel.GetChainsByUserAsync(tenantId, userId);
+
+ await kernel.ExecuteAsync(async () =>
+ {
+ foreach (var chain in chains)
+ {
+ if (exceptChainId.HasValue &&
+ chain.ChainId.Equals(exceptChainId.Value))
+ {
+ continue;
+ }
+
+ if (!chain.IsRevoked)
+ {
+ await kernel.RevokeChainAsync(
+ tenantId,
+ chain.ChainId,
+ at);
+ }
+ }
+ });
+ }
+
+ public async Task RevokeRootAsync(string? tenantId, TUserId userId, DateTime at)
+ {
+ var kernel = _factory.Create(tenantId);
+
+ await kernel.ExecuteAsync(async () =>
+ {
+ await kernel.RevokeSessionRootAsync(
+ tenantId,
+ userId,
+ at);
+ });
+ }
+
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Users/UAuthUserAccessor.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/UAuthUserAccessor.cs
similarity index 85%
rename from src/CodeBeam.UltimateAuth.Server/Users/UAuthUserAccessor.cs
rename to src/CodeBeam.UltimateAuth.Server/Infrastructure/UAuthUserAccessor.cs
index 0769479..d9c85bb 100644
--- a/src/CodeBeam.UltimateAuth.Server/Users/UAuthUserAccessor.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/UAuthUserAccessor.cs
@@ -1,19 +1,19 @@
using CodeBeam.UltimateAuth.Core.Abstractions;
-using CodeBeam.UltimateAuth.Core.Contexts;
+using CodeBeam.UltimateAuth.Core.Contracts;
using CodeBeam.UltimateAuth.Server.Extensions;
using CodeBeam.UltimateAuth.Server.Middlewares;
using Microsoft.AspNetCore.Http;
-namespace CodeBeam.UltimateAuth.Server.Users
+namespace CodeBeam.UltimateAuth.Server.Infrastructure
{
public sealed class UAuthUserAccessor : IUserAccessor
{
private readonly ISessionStore _sessionStore;
- private readonly IUserStore _userStore;
+ private readonly IUAuthUserStore _userStore;
public UAuthUserAccessor(
ISessionStore sessionStore,
- IUserStore userStore)
+ IUAuthUserStore userStore)
{
_sessionStore = sessionStore;
_userStore = userStore;
@@ -43,7 +43,7 @@ public async Task ResolveAsync(HttpContext context)
}
// 👤 Load user
- var user = await _userStore.FindByIdAsync(session.UserId);
+ var user = await _userStore.FindByIdAsync(sessionCtx.TenantId, session.UserId);
if (user is null)
{
diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/UAuthUserId.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/UAuthUserId.cs
new file mode 100644
index 0000000..d42d35b
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/UAuthUserId.cs
@@ -0,0 +1,12 @@
+namespace CodeBeam.UltimateAuth.Server.Infrastructure
+{
+ public readonly record struct UAuthUserId(Guid Value)
+ {
+ public override string ToString() => Value.ToString("N");
+
+ public static UAuthUserId New() => new(Guid.NewGuid());
+
+ public static implicit operator Guid(UAuthUserId id) => id.Value;
+ public static implicit operator UAuthUserId(Guid value) => new(value);
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Internal/.gitkeep b/src/CodeBeam.UltimateAuth.Server/Internal/.gitkeep
deleted file mode 100644
index 5f28270..0000000
--- a/src/CodeBeam.UltimateAuth.Server/Internal/.gitkeep
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/CodeBeam.UltimateAuth.Server/Issuers/UAuthSessionIssuer.cs b/src/CodeBeam.UltimateAuth.Server/Issuers/UAuthSessionIssuer.cs
index 3a224fa..201d6b5 100644
--- a/src/CodeBeam.UltimateAuth.Server/Issuers/UAuthSessionIssuer.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Issuers/UAuthSessionIssuer.cs
@@ -1,6 +1,6 @@
using CodeBeam.UltimateAuth.Core;
using CodeBeam.UltimateAuth.Core.Abstractions;
-using CodeBeam.UltimateAuth.Core.Contexts;
+using CodeBeam.UltimateAuth.Core.Contracts;
using CodeBeam.UltimateAuth.Core.Domain;
using CodeBeam.UltimateAuth.Core.Domain.Session;
using CodeBeam.UltimateAuth.Server.Options;
@@ -23,10 +23,11 @@ public UAuthSessionIssuer(IOpaqueTokenGenerator opaqueGenerator, IOptions> IssueAsync(
- SessionIssueContext context,
- UAuthSessionChain chain,
- CancellationToken cancellationToken = default)
+ AuthenticatedSessionContext context,
+ ISessionChain chain,
+ CancellationToken cancellationToken = default)
{
if (_options.Mode == UAuthMode.PureJwt)
{
@@ -43,9 +44,7 @@ public Task> IssueAsync(
context.Now.Add(_options.Session.MaxLifetime.Value);
if (absoluteExpiry < expiresAt)
- {
expiresAt = absoluteExpiry;
- }
}
var session = UAuthSession.Create(
@@ -54,9 +53,9 @@ public Task> IssueAsync(
userId: context.UserId,
now: context.Now,
expiresAt: expiresAt,
- securityVersion: context.SecurityVersion,
- device: context.Device,
- metadata: SessionMetadata.Empty
+ claims: context.Claims,
+ device: context.DeviceInfo,
+ metadata: context.Metadata
);
return Task.FromResult(new IssuedSession
diff --git a/src/CodeBeam.UltimateAuth.Server/Issuers/UAuthTokenIssuer.cs b/src/CodeBeam.UltimateAuth.Server/Issuers/UAuthTokenIssuer.cs
index 46d6e3f..e965827 100644
--- a/src/CodeBeam.UltimateAuth.Server/Issuers/UAuthTokenIssuer.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Issuers/UAuthTokenIssuer.cs
@@ -1,7 +1,8 @@
using CodeBeam.UltimateAuth.Core;
using CodeBeam.UltimateAuth.Core.Abstractions;
-using CodeBeam.UltimateAuth.Core.Contexts;
+using CodeBeam.UltimateAuth.Core.Contracts;
using CodeBeam.UltimateAuth.Core.Domain;
+using CodeBeam.UltimateAuth.Server.Infrastructure;
using CodeBeam.UltimateAuth.Server.Options;
using Microsoft.Extensions.Options;
using System.Security.Claims;
@@ -28,7 +29,7 @@ public UAuthTokenIssuer(IOpaqueTokenGenerator opaqueGenerator, IJwtTokenGenerato
_options = options.Value;
}
- public Task IssueAccessTokenAsync(TokenIssueContext context, CancellationToken cancellationToken = default)
+ public Task IssueAccessTokenAsync(TokenIssuanceContext context, CancellationToken cancellationToken = default)
{
var now = DateTimeOffset.UtcNow;
var expires = now.Add(_options.Tokens.AccessTokenLifetime);
@@ -50,10 +51,10 @@ UAuthMode.SemiHybrid or
};
}
- public Task IssueRefreshTokenAsync(TokenIssueContext context, CancellationToken cancellationToken = default)
+ public Task IssueRefreshTokenAsync(TokenIssuanceContext context, CancellationToken cancellationToken = default)
{
if (_options.Mode == UAuthMode.PureOpaque)
- return Task.FromResult(null);
+ return Task.FromResult(null);
var now = DateTimeOffset.UtcNow;
var expires = now.Add(_options.Tokens.RefreshTokenLifetime);
@@ -61,7 +62,7 @@ UAuthMode.SemiHybrid or
string token = _opaqueGenerator.Generate();
string hash = _tokenHasher.Hash(token);
- return Task.FromResult(new IssuedRefreshToken
+ return Task.FromResult(new RefreshToken
{
Token = token,
TokenHash = hash,
@@ -69,20 +70,20 @@ UAuthMode.SemiHybrid or
});
}
- private IssuedAccessToken IssueOpaqueAccessToken(DateTimeOffset expires, string? sessionId)
+ private AccessToken IssueOpaqueAccessToken(DateTimeOffset expires, string? sessionId)
{
string token = _opaqueGenerator.Generate();
- return new IssuedAccessToken
+ return new AccessToken
{
Token = token,
- TokenType = "opaque",
+ Type = TokenType.Opaque,
ExpiresAt = expires,
SessionId = sessionId
};
}
- private IssuedAccessToken IssueJwtAccessToken(TokenIssueContext context, DateTimeOffset expires)
+ private AccessToken IssueJwtAccessToken(TokenIssuanceContext context, DateTimeOffset expires)
{
var claims = new List
{
@@ -114,10 +115,10 @@ private IssuedAccessToken IssueJwtAccessToken(TokenIssueContext context, DateTim
string jwt = _jwtGenerator.CreateToken(descriptor);
- return new IssuedAccessToken
+ return new AccessToken
{
Token = jwt,
- TokenType = "jwt",
+ Type = 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
index 3ec6a3b..2f5380e 100644
--- a/src/CodeBeam.UltimateAuth.Server/Middlewares/SessionResolutionMiddleware.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Middlewares/SessionResolutionMiddleware.cs
@@ -1,5 +1,6 @@
-using CodeBeam.UltimateAuth.Server.Extensions;
-using CodeBeam.UltimateAuth.Server.Sessions;
+using CodeBeam.UltimateAuth.Core.Contracts;
+using CodeBeam.UltimateAuth.Server.Extensions;
+using CodeBeam.UltimateAuth.Server.Infrastructure;
using Microsoft.AspNetCore.Http;
namespace CodeBeam.UltimateAuth.Server.Middlewares
diff --git a/src/CodeBeam.UltimateAuth.Server/Middlewares/UserMiddleware.cs b/src/CodeBeam.UltimateAuth.Server/Middlewares/UserMiddleware.cs
index aa87a46..079fbc8 100644
--- a/src/CodeBeam.UltimateAuth.Server/Middlewares/UserMiddleware.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Middlewares/UserMiddleware.cs
@@ -1,4 +1,4 @@
-using CodeBeam.UltimateAuth.Server.Users;
+using CodeBeam.UltimateAuth.Server.Infrastructure;
using Microsoft.AspNetCore.Http;
namespace CodeBeam.UltimateAuth.Server.Middlewares
diff --git a/src/CodeBeam.UltimateAuth.Server/MultiTenancy/DomainTenantAdapter.cs b/src/CodeBeam.UltimateAuth.Server/MultiTenancy/DomainTenantAdapter.cs
deleted file mode 100644
index 88b3c74..0000000
--- a/src/CodeBeam.UltimateAuth.Server/MultiTenancy/DomainTenantAdapter.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-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
deleted file mode 100644
index 8250cdb..0000000
--- a/src/CodeBeam.UltimateAuth.Server/MultiTenancy/HeaderTenantAdapter.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-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/RouteTenantAdapter.cs b/src/CodeBeam.UltimateAuth.Server/MultiTenancy/RouteTenantAdapter.cs
deleted file mode 100644
index e69719a..0000000
--- a/src/CodeBeam.UltimateAuth.Server/MultiTenancy/RouteTenantAdapter.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-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/Services/.gitkeep b/src/CodeBeam.UltimateAuth.Server/Services/.gitkeep
deleted file mode 100644
index 5f28270..0000000
--- a/src/CodeBeam.UltimateAuth.Server/Services/.gitkeep
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/CodeBeam.UltimateAuth.Server/Services/UAuthFlowService.cs b/src/CodeBeam.UltimateAuth.Server/Services/UAuthFlowService.cs
new file mode 100644
index 0000000..084e3b0
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Services/UAuthFlowService.cs
@@ -0,0 +1,149 @@
+using CodeBeam.UltimateAuth.Core.Abstractions;
+using CodeBeam.UltimateAuth.Core.Contracts;
+using CodeBeam.UltimateAuth.Core.Domain;
+
+namespace CodeBeam.UltimateAuth.Server.Services
+{
+ internal sealed class UAuthFlowService : IUAuthFlowService
+ {
+ private readonly IUAuthUserService _users;
+ private readonly IUAuthSessionService _sessions;
+ private readonly IUAuthTokenService _tokens;
+
+ public UAuthFlowService(
+ IUAuthUserService users,
+ IUAuthSessionService sessions,
+ IUAuthTokenService tokens)
+ {
+ _users = users;
+ _sessions = sessions;
+ _tokens = tokens;
+ }
+
+ public Task BeginMfaAsync(BeginMfaRequest request, CancellationToken ct = default)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task CompleteMfaAsync(CompleteMfaRequest request, CancellationToken ct = default)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task ConsumePkceAsync(PkceConsumeRequest request, CancellationToken ct = default)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task CreatePkceChallengeAsync(PkceCreateRequest request, CancellationToken ct = default)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task ExternalLoginAsync(ExternalLoginRequest request, CancellationToken ct = default)
+ {
+ throw new NotImplementedException();
+ }
+
+ public async Task LoginAsync(LoginRequest request, CancellationToken ct = default)
+ {
+ var now = request.At ?? DateTime.UtcNow;
+ var device = request.DeviceInfo ?? DeviceInfo.Unknown;
+
+ var authResult = await _users.AuthenticateAsync(request.TenantId, request.Identifier, request.Secret, ct);
+
+ if (!authResult.Succeeded)
+ {
+ return LoginResult.Failed();
+ }
+
+ var sessionResult = await _sessions.IssueSessionAfterAuthenticationAsync(request.TenantId,
+ new AuthenticatedSessionContext
+ {
+ TenantId = request.TenantId,
+ UserId = authResult.UserId!,
+ Now = now,
+ DeviceInfo = device,
+ Claims = authResult.Claims,
+ ChainId = request.ChainId
+ });
+
+ AuthTokens? tokens = null;
+
+ if (request.RequestTokens)
+ {
+ tokens = await _tokens.CreateTokensAsync(
+ new TokenIssueContext
+ {
+ TenantId = request.TenantId,
+ Session = sessionResult.Session,
+ Now = now
+ },
+ ct);
+ }
+
+ return LoginResult.Success(sessionResult.Session.SessionId, tokens);
+ }
+
+ public async Task LogoutAsync(LogoutRequest request, CancellationToken ct = default)
+ {
+ var at = request.At ?? DateTime.UtcNow;
+ await _sessions.RevokeSessionAsync(request.TenantId, request.SessionId, at);
+ }
+
+ public async Task LogoutAllAsync(LogoutAllRequest request, CancellationToken ct = default)
+ {
+ var at = request.At ?? DateTime.UtcNow;
+
+ if (request.CurrentSessionId is null)
+ throw new InvalidOperationException(
+ "CurrentSessionId must be provided for logout-all operation.");
+
+ var currentSessionId = request.CurrentSessionId.Value;
+
+ var validation = await _sessions.ValidateSessionAsync(
+ request.TenantId,
+ currentSessionId,
+ at);
+
+ if (validation.IsValid ||
+ validation.Session is null)
+ throw new InvalidOperationException("Current session is not valid.");
+
+ var userId = validation.Session.UserId;
+
+ ChainId? currentChainId = null;
+
+ if (request.ExceptCurrent)
+ {
+ if (request.CurrentSessionId is null)
+ throw new InvalidOperationException("CurrentSessionId must be provided when ExceptCurrent is true.");
+
+ currentChainId = await _sessions.ResolveChainIdAsync(
+ request.TenantId,
+ currentSessionId);
+
+ if (currentChainId is null)
+ throw new InvalidOperationException("Current session chain could not be resolved.");
+ }
+
+ await _sessions.RevokeAllChainsAsync(request.TenantId, userId, exceptChainId: currentChainId, at);
+ }
+
+ public Task ReauthenticateAsync(ReauthRequest request, CancellationToken ct = default)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task RefreshSessionAsync(SessionRefreshRequest request, CancellationToken ct = default)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task VerifyPkceAsync(PkceVerifyRequest request, CancellationToken ct = default)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Services/UAuthSessionService.cs b/src/CodeBeam.UltimateAuth.Server/Services/UAuthSessionService.cs
new file mode 100644
index 0000000..43cdf35
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Services/UAuthSessionService.cs
@@ -0,0 +1,125 @@
+using CodeBeam.UltimateAuth.Core.Abstractions;
+using CodeBeam.UltimateAuth.Core.Contracts;
+using CodeBeam.UltimateAuth.Core.Domain;
+using CodeBeam.UltimateAuth.Server.Infrastructure;
+
+namespace CodeBeam.UltimateAuth.Server.Services
+{
+ internal sealed class UAuthSessionService : IUAuthSessionService
+ {
+ private readonly ISessionOrchestrator _orchestrator;
+
+ public UAuthSessionService(ISessionOrchestrator orchestrator)
+ {
+ _orchestrator = orchestrator;
+ }
+
+ public Task> ValidateSessionAsync(
+ string? tenantId,
+ AuthSessionId sessionId,
+ DateTime now)
+ {
+ var context = new SessionValidationContext()
+ {
+ TenantId = tenantId,
+ Now = now
+ };
+
+ return _orchestrator.ValidateSessionAsync(context);
+ }
+
+ public Task>> GetChainsAsync(
+ string? tenantId,
+ TUserId userId)
+ => _orchestrator.GetChainsAsync(
+ tenantId,
+ userId);
+
+ public Task>> GetSessionsAsync(
+ string? tenantId,
+ ChainId chainId)
+ => _orchestrator.GetSessionsAsync(
+ tenantId,
+ chainId);
+
+ public Task?> GetSessionAsync(
+ string? tenantId,
+ AuthSessionId sessionId)
+ => _orchestrator.GetSessionAsync(
+ tenantId,
+ sessionId);
+
+ public Task RevokeSessionAsync(
+ string? tenantId,
+ AuthSessionId sessionId,
+ DateTime at)
+ => _orchestrator.RevokeSessionAsync(
+ tenantId,
+ sessionId,
+ at);
+
+ public Task ResolveChainIdAsync(
+ string? tenantId,
+ AuthSessionId sessionId)
+ => _orchestrator.ResolveChainIdAsync(tenantId, sessionId);
+
+ public Task RevokeAllChainsAsync(
+ string? tenantId,
+ TUserId userId,
+ ChainId? exceptChainId,
+ DateTime at)
+ => _orchestrator.RevokeAllChainsAsync(
+ tenantId,
+ userId,
+ exceptChainId,
+ at);
+
+ public Task RevokeChainAsync(
+ string? tenantId,
+ ChainId chainId,
+ DateTime at)
+ => _orchestrator.RevokeChainAsync(
+ tenantId,
+ chainId,
+ at);
+
+ public Task RevokeRootAsync(
+ string? tenantId,
+ TUserId userId,
+ DateTime at)
+ => _orchestrator.RevokeRootAsync(
+ tenantId,
+ userId,
+ at);
+
+ public Task?> GetCurrentSessionAsync(string? tenantId, AuthSessionId sessionId)
+ {
+ // TODO: Implement this method
+ throw new NotImplementedException();
+ }
+
+ public Task> IssueSessionAfterAuthenticationAsync(
+ string? tenantId,
+ AuthenticatedSessionContext context,
+ CancellationToken cancellationToken = default)
+ {
+ if (context.UserId is null)
+ throw new InvalidOperationException(
+ "Authenticated session context requires a valid user id.");
+
+ // Authenticated → IssueContext map
+ var issueContext = new AuthenticatedSessionContext
+ {
+ TenantId = tenantId,
+ UserId = context.UserId,
+ Now = context.Now,
+ DeviceInfo = context.DeviceInfo,
+ Claims = context.Claims,
+ ChainId = context.ChainId
+ };
+
+ return _orchestrator.CreateLoginSessionAsync(issueContext);
+ }
+
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Services/UAuthTokenService.cs b/src/CodeBeam.UltimateAuth.Server/Services/UAuthTokenService.cs
new file mode 100644
index 0000000..6d35fbf
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Services/UAuthTokenService.cs
@@ -0,0 +1,62 @@
+using CodeBeam.UltimateAuth.Core.Abstractions;
+using CodeBeam.UltimateAuth.Core.Contracts;
+using CodeBeam.UltimateAuth.Server.Extensions;
+using CodeBeam.UltimateAuth.Server.Infrastructure;
+
+namespace CodeBeam.UltimateAuth.Server.Services
+{
+ internal sealed class UAuthTokenService : IUAuthTokenService
+ {
+ private readonly ITokenIssuer _issuer;
+ private readonly ITokenValidator _validator;
+ private readonly IUserIdConverter _userIdConverter;
+
+ public UAuthTokenService(ITokenIssuer issuer, ITokenValidator validator, IUserIdConverterResolver converterResolver)
+ {
+ _issuer = issuer;
+ _validator = validator;
+ _userIdConverter = converterResolver.GetConverter();
+ }
+
+ public async Task