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