diff --git a/UltimateAuth.slnx b/UltimateAuth.slnx
index ee6b5d9..1f15cee 100644
--- a/UltimateAuth.slnx
+++ b/UltimateAuth.slnx
@@ -12,15 +12,25 @@
+
+
+
+
-
+
+
+
+
+
+
+
diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.csproj b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.csproj
index f0dc5ec..fac1f58 100644
--- a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.csproj
+++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.csproj
@@ -1,4 +1,4 @@
-
+
net10.0
@@ -14,13 +14,21 @@
+
+
+
+
+
+
+
+
diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/Pages/Home.razor.cs b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/Pages/Home.razor.cs
index 7ae27ac..a2c2e59 100644
--- a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/Pages/Home.razor.cs
+++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/Pages/Home.razor.cs
@@ -78,8 +78,8 @@ private async Task ProgrammaticPkceLogin()
var request = new PkceLoginRequest
{
- Identifier = "Admin",
- Secret = "Password!",
+ Identifier = "admin",
+ Secret = "admin",
AuthorizationCode = credentials?.AuthorizationCode ?? string.Empty,
CodeVerifier = credentials?.CodeVerifier ?? string.Empty,
ReturnUrl = _state?.ReturnUrl ?? string.Empty
diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Program.cs b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Program.cs
index 67c4055..c6aef00 100644
--- a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Program.cs
+++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Program.cs
@@ -1,16 +1,23 @@
+using CodeBeam.UltimateAuth.Authorization.InMemory;
+using CodeBeam.UltimateAuth.Authorization.InMemory.Extensions;
+using CodeBeam.UltimateAuth.Authorization.Reference.Extensions;
using CodeBeam.UltimateAuth.Client.Extensions;
-using CodeBeam.UltimateAuth.Core.Abstractions;
using CodeBeam.UltimateAuth.Core.Domain;
using CodeBeam.UltimateAuth.Core.Extensions;
+using CodeBeam.UltimateAuth.Core.Infrastructure;
using CodeBeam.UltimateAuth.Core.Runtime;
-using CodeBeam.UltimateAuth.Credentials.InMemory;
+using CodeBeam.UltimateAuth.Credentials.InMemory.Extensions;
+using CodeBeam.UltimateAuth.Credentials.Reference;
using CodeBeam.UltimateAuth.Sample.UAuthHub.Components;
using CodeBeam.UltimateAuth.Security.Argon2;
using CodeBeam.UltimateAuth.Server.Authentication;
using CodeBeam.UltimateAuth.Server.Extensions;
-using CodeBeam.UltimateAuth.Server.Infrastructure;
using CodeBeam.UltimateAuth.Sessions.InMemory;
using CodeBeam.UltimateAuth.Tokens.InMemory;
+using CodeBeam.UltimateAuth.Users;
+using CodeBeam.UltimateAuth.Users.InMemory.Extensions;
+using CodeBeam.UltimateAuth.Users.Reference;
+using CodeBeam.UltimateAuth.Users.Reference.Extensions;
using MudBlazor.Services;
using MudExtensions.Services;
@@ -46,7 +53,12 @@
//o.Session.TouchInterval = TimeSpan.FromSeconds(9);
//o.Session.IdleTimeout = TimeSpan.FromSeconds(15);
})
- .AddInMemoryCredentials()
+ .AddUltimateAuthUsersInMemory()
+ .AddUltimateAuthUsersReference()
+ .AddUltimateAuthCredentialsInMemory()
+ .AddUltimateAuthCredentialsReference()
+ .AddUltimateAuthAuthorizationInMemory()
+ .AddUltimateAuthAuthorizationReference()
.AddUltimateAuthInMemorySessions()
.AddUltimateAuthInMemoryTokens()
.AddUltimateAuthArgon2();
@@ -73,6 +85,19 @@
var app = builder.Build();
+using (var scope = app.Services.CreateScope())
+{
+ scope.ServiceProvider.GetRequiredService();
+ scope.ServiceProvider.GetRequiredService();
+ scope.ServiceProvider.GetRequiredService>();
+
+ var seeder = scope.ServiceProvider.GetService();
+ //if (seeder is not null)
+ // await seeder.SeedAsync();
+
+
+}
+
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/CodeBeam.UltimateAuth.Sample.BlazorServer.csproj b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/CodeBeam.UltimateAuth.Sample.BlazorServer.csproj
index 69effd5..e54c2fa 100644
--- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/CodeBeam.UltimateAuth.Sample.BlazorServer.csproj
+++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/CodeBeam.UltimateAuth.Sample.BlazorServer.csproj
@@ -13,13 +13,21 @@
+
+
+
+
+
+
+
+
diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Home.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Home.razor
index c36676a..a36836b 100644
--- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Home.razor
+++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Home.razor
@@ -64,6 +64,12 @@
Not Authorized context is shown.
+
+
+
+ This is Admin content.
+
+
diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Home.razor.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Home.razor.cs
index 806ffd9..2c46268 100644
--- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Home.razor.cs
+++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Home.razor.cs
@@ -41,8 +41,8 @@ private async Task ProgrammaticLogin()
var deviceId = await DeviceIdProvider.GetOrCreateAsync();
var request = new LoginRequest
{
- Identifier = "Admin",
- Secret = "Password!",
+ Identifier = "admin",
+ Secret = "admin",
Device = DeviceContext.FromDeviceId(deviceId),
};
await UAuthClient.LoginAsync(request);
diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Program.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Program.cs
index e449e4e..b16df96 100644
--- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Program.cs
+++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Program.cs
@@ -1,13 +1,20 @@
+using CodeBeam.UltimateAuth.Authorization.InMemory;
+using CodeBeam.UltimateAuth.Authorization.InMemory.Extensions;
+using CodeBeam.UltimateAuth.Authorization.Reference.Extensions;
using CodeBeam.UltimateAuth.Client.Extensions;
using CodeBeam.UltimateAuth.Core.Domain;
using CodeBeam.UltimateAuth.Core.Extensions;
-using CodeBeam.UltimateAuth.Credentials.InMemory;
+using CodeBeam.UltimateAuth.Credentials.InMemory.Extensions;
+using CodeBeam.UltimateAuth.Credentials.Reference;
using CodeBeam.UltimateAuth.Sample.BlazorServer.Components;
using CodeBeam.UltimateAuth.Security.Argon2;
using CodeBeam.UltimateAuth.Server.Authentication;
using CodeBeam.UltimateAuth.Server.Extensions;
using CodeBeam.UltimateAuth.Sessions.InMemory;
using CodeBeam.UltimateAuth.Tokens.InMemory;
+using CodeBeam.UltimateAuth.Users.InMemory.Extensions;
+using CodeBeam.UltimateAuth.Users.Reference;
+using CodeBeam.UltimateAuth.Users.Reference.Extensions;
using Microsoft.AspNetCore.Components;
using MudBlazor.Services;
using MudExtensions.Services;
@@ -44,7 +51,12 @@
//o.Session.TouchInterval = TimeSpan.FromSeconds(9);
//o.Session.IdleTimeout = TimeSpan.FromSeconds(15);
})
- .AddInMemoryCredentials()
+ .AddUltimateAuthUsersInMemory()
+ .AddUltimateAuthUsersReference()
+ .AddUltimateAuthCredentialsInMemory()
+ .AddUltimateAuthCredentialsReference()
+ .AddUltimateAuthAuthorizationInMemory()
+ .AddUltimateAuthAuthorizationReference()
.AddUltimateAuthInMemorySessions()
.AddUltimateAuthInMemoryTokens()
.AddUltimateAuthArgon2();
@@ -80,6 +92,17 @@
var app = builder.Build();
+using (var scope = app.Services.CreateScope())
+{
+ scope.ServiceProvider.GetRequiredService();
+ //scope.ServiceProvider.GetRequiredService();
+ //scope.ServiceProvider.GetRequiredService>();
+
+ var seeder = scope.ServiceProvider.GetService();
+ //if (seeder is not null)
+ // await seeder.SeedAsync();
+}
+
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
diff --git a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm.csproj b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm.csproj
index d1f3837..36d7dae 100644
--- a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm.csproj
+++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm.csproj
@@ -17,8 +17,11 @@
+
+
+
diff --git a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Home.razor b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Home.razor
index e2f5e68..da54578 100644
--- a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Home.razor
+++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Home.razor
@@ -59,6 +59,10 @@
Not Authorized context is shown.
+
+
+ This is Admin content.
+
diff --git a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Home.razor.cs b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Home.razor.cs
index f4a6fef..fc4d08d 100644
--- a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Home.razor.cs
+++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Home.razor.cs
@@ -46,8 +46,8 @@ private async Task ProgrammaticLogin()
var device = await DeviceIdProvider.GetOrCreateAsync();
var request = new LoginRequest
{
- Identifier = "Admin",
- Secret = "Password!",
+ Identifier = "admin",
+ Secret = "admin",
Device = DeviceContext.FromDeviceId(device),
};
await UAuthClient.LoginAsync(request);
diff --git a/src/CodeBeam.UltimateAuth.Client/CodeBeam.UltimateAuth.Client.csproj b/src/CodeBeam.UltimateAuth.Client/CodeBeam.UltimateAuth.Client.csproj
index 3d97996..5ead5c5 100644
--- a/src/CodeBeam.UltimateAuth.Client/CodeBeam.UltimateAuth.Client.csproj
+++ b/src/CodeBeam.UltimateAuth.Client/CodeBeam.UltimateAuth.Client.csproj
@@ -30,6 +30,8 @@
+
+
diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Auth/IAuthContextFactory.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Auth/IAuthContextFactory.cs
new file mode 100644
index 0000000..1dc892e
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Auth/IAuthContextFactory.cs
@@ -0,0 +1,8 @@
+using CodeBeam.UltimateAuth.Core.Contracts;
+
+namespace CodeBeam.UltimateAuth.Core.Abstractions;
+
+public interface IAuthContextFactory
+{
+ AuthContext Create(DateTimeOffset? at = null);
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAccessAuthority.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAccessAuthority.cs
new file mode 100644
index 0000000..bf61d88
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAccessAuthority.cs
@@ -0,0 +1,10 @@
+using CodeBeam.UltimateAuth.Core.Contracts;
+
+namespace CodeBeam.UltimateAuth.Core.Abstractions
+{
+ public interface IAccessAuthority
+ {
+ AccessDecision Decide(AccessContext context, IEnumerable runtimePolicies);
+ }
+
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAccessInvariant.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAccessInvariant.cs
new file mode 100644
index 0000000..806d6c9
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAccessInvariant.cs
@@ -0,0 +1,9 @@
+using CodeBeam.UltimateAuth.Core.Contracts;
+
+namespace CodeBeam.UltimateAuth.Core.Abstractions
+{
+ public interface IAccessInvariant
+ {
+ AccessDecision Decide(AccessContext context);
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAccessPolicy.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAccessPolicy.cs
new file mode 100644
index 0000000..487072f
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAccessPolicy.cs
@@ -0,0 +1,10 @@
+using CodeBeam.UltimateAuth.Core.Contracts;
+
+namespace CodeBeam.UltimateAuth.Core.Abstractions
+{
+ public interface IAccessPolicy
+ {
+ bool AppliesTo(AccessContext context);
+ AccessDecision Decide(AccessContext context);
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAuthAuthority.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAuthAuthority.cs
index 9da4a8f..9a29458 100644
--- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAuthAuthority.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAuthAuthority.cs
@@ -4,7 +4,6 @@ namespace CodeBeam.UltimateAuth.Core.Abstractions
{
public interface IAuthAuthority
{
- AuthorizationResult Decide(AuthContext context);
+ AccessDecisionResult Decide(AuthContext context, IEnumerable? policies = null);
}
-
}
diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAuthorityInvariant.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAuthorityInvariant.cs
index 32ce7dc..dc0cc0a 100644
--- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAuthorityInvariant.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAuthorityInvariant.cs
@@ -4,6 +4,6 @@ namespace CodeBeam.UltimateAuth.Core.Abstractions
{
public interface IAuthorityInvariant
{
- AuthorizationResult Decide(AuthContext context);
+ AccessDecisionResult Decide(AuthContext context);
}
}
diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAuthorityPolicy.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAuthorityPolicy.cs
index 235ea3d..2b2021a 100644
--- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAuthorityPolicy.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAuthorityPolicy.cs
@@ -5,6 +5,6 @@ namespace CodeBeam.UltimateAuth.Core.Abstractions
public interface IAuthorityPolicy
{
bool AppliesTo(AuthContext context);
- AuthorizationResult Decide(AuthContext context);
+ AccessDecisionResult Decide(AuthContext context);
}
}
diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Infrastructure/IUAuthPasswordHasher.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Infrastructure/IUAuthPasswordHasher.cs
index 53246c1..d6596c9 100644
--- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Infrastructure/IUAuthPasswordHasher.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Infrastructure/IUAuthPasswordHasher.cs
@@ -8,6 +8,6 @@
public interface IUAuthPasswordHasher
{
string Hash(string password);
- bool Verify(string password, string hash);
+ bool Verify(string hash, string secret);
}
}
diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Principals/IUserAuthenticator.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Principals/IUserAuthenticator.cs
deleted file mode 100644
index 21ee5ad..0000000
--- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Principals/IUserAuthenticator.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-using CodeBeam.UltimateAuth.Core.Contracts;
-
-namespace CodeBeam.UltimateAuth.Core.Abstractions
-{
- public interface IUserAuthenticator
- {
- Task> AuthenticateAsync(string? tenantId, AuthenticationContext context, CancellationToken ct = default);
- }
-}
diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Principals/IUserClaimsProvider.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Principals/IUserClaimsProvider.cs
new file mode 100644
index 0000000..3be15c9
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Principals/IUserClaimsProvider.cs
@@ -0,0 +1,8 @@
+using CodeBeam.UltimateAuth.Core.Domain;
+
+namespace CodeBeam.UltimateAuth.Core;
+
+public interface IUserClaimsProvider
+{
+ Task GetClaimsAsync(string? tenantId, UserKey userKey, CancellationToken ct = default);
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Services/ISessionService.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Services/ISessionService.cs
new file mode 100644
index 0000000..9dc9df9
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Services/ISessionService.cs
@@ -0,0 +1,12 @@
+using CodeBeam.UltimateAuth.Core.Contracts;
+using CodeBeam.UltimateAuth.Core.Domain;
+
+namespace CodeBeam.UltimateAuth.Core.Abstractions
+{
+ public interface ISessionService
+ {
+ Task RevokeAllAsync(AuthContext authContext, UserKey userKey, CancellationToken ct = default);
+ Task RevokeAllExceptChainAsync(AuthContext authContext, UserKey userKey, SessionChainId exceptChainId, CancellationToken ct = default);
+ Task RevokeRootAsync(AuthContext authContext, UserKey userKey, CancellationToken ct = default);
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Services/IUAuthService.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Services/IUAuthService.cs
index c2c3423..9486398 100644
--- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Services/IUAuthService.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Services/IUAuthService.cs
@@ -5,11 +5,11 @@
/// Provides access to authentication flows,
/// session lifecycle and user operations.
///
- public interface IUAuthService
- {
- //IUAuthFlowService Flow { get; }
- IUAuthSessionManager Sessions { get; }
- //IUAuthTokenService Tokens { get; }
- IUAuthUserService Users { get; }
- }
+ //public interface IUAuthService
+ //{
+ // //IUAuthFlowService Flow { get; }
+ // IUAuthSessionManager Sessions { get; }
+ // //IUAuthTokenService Tokens { get; }
+ // IUAuthUserService Users { get; }
+ //}
}
diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Services/IUAuthUserService.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Services/IUAuthUserService.cs
index af6e3ba..d546e55 100644
--- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Services/IUAuthUserService.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Services/IUAuthUserService.cs
@@ -1,15 +1,15 @@
-using CodeBeam.UltimateAuth.Core.Contracts;
+//using CodeBeam.UltimateAuth.Core.Contracts;
-namespace CodeBeam.UltimateAuth.Core.Abstractions
-{
- ///
- /// Defines the minimal user authentication contract expected by UltimateAuth.
- /// This service does not manage sessions, tokens, or transport concerns.
- /// For user management, CodeBeam.UltimateAuth.Users package is recommended.
- ///
- public interface IUAuthUserService
- {
- Task> AuthenticateAsync(string? tenantId, string identifier, string secret, CancellationToken cancellationToken = default);
- Task ValidateCredentialsAsync(ValidateCredentialsRequest request, CancellationToken cancellationToken = default);
- }
-}
+//namespace CodeBeam.UltimateAuth.Core.Abstractions
+//{
+// ///
+// /// Defines the minimal user authentication contract expected by UltimateAuth.
+// /// This service does not manage sessions, tokens, or transport concerns.
+// /// For user management, CodeBeam.UltimateAuth.Users package is recommended.
+// ///
+// public interface IUAuthUserService
+// {
+// Task> AuthenticateAsync(string? tenantId, string identifier, string secret, CancellationToken cancellationToken = default);
+// Task ValidateCredentialsAsync(ValidateCredentialsRequest request, CancellationToken cancellationToken = default);
+// }
+//}
diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/IUAuthUserStore.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/IUAuthUserStore.cs
index 96f4f54..b97c47e 100644
--- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/IUAuthUserStore.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/IUAuthUserStore.cs
@@ -10,16 +10,16 @@ namespace CodeBeam.UltimateAuth.Core.Abstractions
///
public interface IUAuthUserStore
{
- Task?> FindByIdAsync(string? tenantId, TUserId userId, CancellationToken token = default);
+ Task?> FindByIdAsync(string? tenantId, TUserId userId, CancellationToken token = default);
- Task?> FindByUsernameAsync(string? tenantId, string username, CancellationToken ct = default);
+ Task?> FindByUsernameAsync(string? tenantId, string username, CancellationToken ct = default);
///
/// Retrieves a user by a login credential such as username or email.
/// Returns null if no matching user exists.
///
/// The user instance or null if not found.
- Task?> FindByLoginAsync(string? tenantId, string login, CancellationToken token = default);
+ Task?> FindByLoginAsync(string? tenantId, string login, CancellationToken token = default);
///
/// Returns the password hash for the specified user, if the user participates
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Authority/AccessContext.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Authority/AccessContext.cs
new file mode 100644
index 0000000..74f8416
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Authority/AccessContext.cs
@@ -0,0 +1,14 @@
+using CodeBeam.UltimateAuth.Core.Domain;
+
+namespace CodeBeam.UltimateAuth.Core.Contracts
+{
+ public sealed class AccessContext
+ {
+ public string? TenantId { get; init; }
+ public UserKey? ActorUserKey { get; init; }
+ public string Action { get; init; } = default!;
+ public string? Resource { get; init; }
+ public string? ResourceId { get; init; }
+ public IReadOnlyDictionary? Attributes { get; init; }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Authority/AccessDecision.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Authority/AccessDecision.cs
new file mode 100644
index 0000000..2320615
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Authority/AccessDecision.cs
@@ -0,0 +1,40 @@
+namespace CodeBeam.UltimateAuth.Core.Contracts
+{
+ public sealed record AccessDecision
+ {
+ public bool IsAllowed { get; }
+ public bool RequiresReauthentication { get; }
+ public string? DenyReason { get; }
+
+ private AccessDecision(
+ bool isAllowed,
+ bool requiresReauthentication,
+ string? denyReason)
+ {
+ IsAllowed = isAllowed;
+ RequiresReauthentication = requiresReauthentication;
+ DenyReason = denyReason;
+ }
+
+ public static AccessDecision Allow()
+ => new(
+ isAllowed: true,
+ requiresReauthentication: false,
+ denyReason: null);
+
+ public static AccessDecision Deny(string reason)
+ => new(
+ isAllowed: false,
+ requiresReauthentication: false,
+ denyReason: reason);
+
+ public static AccessDecision ReauthenticationRequired(string? reason = null)
+ => new(
+ isAllowed: false,
+ requiresReauthentication: true,
+ denyReason: reason);
+
+ public bool IsDenied =>
+ !IsAllowed && !RequiresReauthentication;
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Authority/AuthorizationResult.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Authority/AccessDecisionResult.cs
similarity index 68%
rename from src/CodeBeam.UltimateAuth.Core/Contracts/Authority/AuthorizationResult.cs
rename to src/CodeBeam.UltimateAuth.Core/Contracts/Authority/AccessDecisionResult.cs
index 09af255..e157c94 100644
--- a/src/CodeBeam.UltimateAuth.Core/Contracts/Authority/AuthorizationResult.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Authority/AccessDecisionResult.cs
@@ -1,23 +1,23 @@
namespace CodeBeam.UltimateAuth.Core.Contracts
{
- public sealed class AuthorizationResult
+ public sealed class AccessDecisionResult
{
public AuthorizationDecision Decision { get; }
public string? Reason { get; }
- private AuthorizationResult(AuthorizationDecision decision, string? reason)
+ private AccessDecisionResult(AuthorizationDecision decision, string? reason)
{
Decision = decision;
Reason = reason;
}
- public static AuthorizationResult Allow()
+ public static AccessDecisionResult Allow()
=> new(AuthorizationDecision.Allow, null);
- public static AuthorizationResult Deny(string reason)
+ public static AccessDecisionResult Deny(string reason)
=> new(AuthorizationDecision.Deny, reason);
- public static AuthorizationResult Challenge(string reason)
+ public static AccessDecisionResult Challenge(string reason)
=> new(AuthorizationDecision.Challenge, reason);
// Developer happiness helpers
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Authority/AuthenticationContext.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Authority/AuthenticationContext.cs
deleted file mode 100644
index 53027da..0000000
--- a/src/CodeBeam.UltimateAuth.Core/Contracts/Authority/AuthenticationContext.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-using CodeBeam.UltimateAuth.Core.Domain;
-
-namespace CodeBeam.UltimateAuth.Core.Contracts
-{
- public sealed record AuthenticationContext
- {
- public string? TenantId { get; init; }
- public string Identifier { get; init; } = default!;
- public string Secret { get; init; } = default!;
- public AuthOperation Operation { get; init; } // Login, Reauth, Validate
- public DeviceContext? Device { get; init; }
- public string CredentialType { get; init; } = "password";
- }
-}
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Common/DeleteMode.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Common/DeleteMode.cs
new file mode 100644
index 0000000..e28fa7b
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Common/DeleteMode.cs
@@ -0,0 +1,8 @@
+namespace CodeBeam.UltimateAuth.Core.Contracts
+{
+ public enum DeleteMode
+ {
+ Soft,
+ Hard
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Login/LoginResult.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Login/LoginResult.cs
index f9be49f..8324739 100644
--- a/src/CodeBeam.UltimateAuth.Core/Contracts/Login/LoginResult.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Login/LoginResult.cs
@@ -18,7 +18,12 @@ public sealed record LoginResult
public bool RequiresMfa => Continuation?.Type == LoginContinuationType.Mfa;
public bool RequiresPkce => Continuation?.Type == LoginContinuationType.Pkce;
- public static LoginResult Failed() => new() { Status = LoginStatus.Failed };
+ public static LoginResult Failed(AuthFailureReason? reason = null)
+ => new()
+ {
+ Status = LoginStatus.Failed,
+ FailureReason = reason
+ };
public static LoginResult Success(AuthSessionId sessionId, AuthTokens? tokens = null)
=> new()
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/User/AuthUserSnapshot.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/User/AuthUserSnapshot.cs
index 62b8fd1..ef8cdcc 100644
--- a/src/CodeBeam.UltimateAuth.Core/Contracts/User/AuthUserSnapshot.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/User/AuthUserSnapshot.cs
@@ -1,6 +1,16 @@
namespace CodeBeam.UltimateAuth.Core.Contracts
{
// This is for AuthFlowContext, with minimal data and no db access
+ ///
+ /// Represents the minimal authentication state of the current request.
+ /// This type is request-scoped and contains no domain or persistence data.
+ ///
+ /// AuthUserSnapshot answers only the question:
+ /// "Is there an authenticated user associated with this execution context?"
+ ///
+ /// It must not be used for user discovery, lifecycle decisions,
+ /// or authorization policies.
+ ///
public sealed class AuthUserSnapshot
{
public bool IsAuthenticated { get; }
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/User/UserContext.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/User/UserContext.cs
index 0c87265..2063d0c 100644
--- a/src/CodeBeam.UltimateAuth.Core/Contracts/User/UserContext.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/User/UserContext.cs
@@ -5,7 +5,7 @@ namespace CodeBeam.UltimateAuth.Core.Contracts
public sealed class UserContext
{
public TUserId? UserId { get; init; }
- public IUser? User { get; init; }
+ public IAuthSubject? User { get; init; }
public bool IsAuthenticated => UserId is not null;
diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/AuthFlowType.cs b/src/CodeBeam.UltimateAuth.Core/Domain/AuthFlowType.cs
index ed0ca74..e18593a 100644
--- a/src/CodeBeam.UltimateAuth.Core/Domain/AuthFlowType.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Domain/AuthFlowType.cs
@@ -20,6 +20,11 @@ public enum AuthFlowType
UserInfo,
PermissionQuery,
+ UserManagement,
+ UserProfile,
+ CredentialManagement,
+ AuthorizationManagement,
+
ApiAccess
}
}
diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/ICurrentUser.cs b/src/CodeBeam.UltimateAuth.Core/Domain/ICurrentUser.cs
new file mode 100644
index 0000000..399cad5
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Domain/ICurrentUser.cs
@@ -0,0 +1,9 @@
+using CodeBeam.UltimateAuth.Core.Domain;
+
+namespace CodeBeam.UltimateAuth.Core;
+
+public interface ICurrentUser
+{
+ bool IsAuthenticated { get; }
+ UserKey UserKey { get; }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Principals/ClaimsSnapshotBuilder.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Principals/ClaimsSnapshotBuilder.cs
new file mode 100644
index 0000000..6ceca57
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Domain/Principals/ClaimsSnapshotBuilder.cs
@@ -0,0 +1,39 @@
+using System.Security.Claims;
+
+namespace CodeBeam.UltimateAuth.Core.Domain
+{
+ public sealed class ClaimsSnapshotBuilder
+ {
+ private readonly Dictionary> _claims = new(StringComparer.Ordinal);
+
+ public ClaimsSnapshotBuilder Add(string type, string value)
+ {
+ if (!_claims.TryGetValue(type, out var set))
+ {
+ set = new HashSet(StringComparer.Ordinal);
+ _claims[type] = set;
+ }
+
+ set.Add(value);
+ return this;
+ }
+
+ public ClaimsSnapshotBuilder AddMany(string type, IEnumerable values)
+ {
+ foreach (var v in values)
+ Add(type, v);
+
+ return this;
+ }
+
+ public ClaimsSnapshotBuilder AddRole(string role) => Add(ClaimTypes.Role, role);
+
+ public ClaimsSnapshotBuilder AddPermission(string permission) => Add("uauth:permission", permission);
+
+ public ClaimsSnapshot Build()
+ {
+ var frozen = _claims.ToDictionary(kv => kv.Key, kv => (IReadOnlyCollection)kv.Value.ToArray(), StringComparer.Ordinal);
+ return new ClaimsSnapshot(frozen);
+ }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Session/ClaimsSnapshot.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Session/ClaimsSnapshot.cs
index 2190461..c6f5f7b 100644
--- a/src/CodeBeam.UltimateAuth.Core/Domain/Session/ClaimsSnapshot.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Domain/Session/ClaimsSnapshot.cs
@@ -5,24 +5,60 @@ namespace CodeBeam.UltimateAuth.Core.Domain
{
public sealed class ClaimsSnapshot
{
- public IReadOnlyDictionary Claims { get; }
+ private readonly IReadOnlyDictionary> _claims;
+ public IReadOnlyDictionary> Claims => _claims;
[JsonConstructor]
- public ClaimsSnapshot(IReadOnlyDictionary claims)
+ public ClaimsSnapshot(IReadOnlyDictionary> claims)
{
- Claims = new Dictionary(claims);
+ _claims = claims;
}
- public IReadOnlyDictionary AsDictionary() => Claims;
+ public static ClaimsSnapshot Empty { get; } = new(new Dictionary>());
- public bool TryGet(string type, out string value) => Claims.TryGetValue(type, out value);
+ public string? Get(string type) => _claims.TryGetValue(type, out var values) ? values.FirstOrDefault() : null;
+ public bool TryGet(string type, out string value)
+ {
+ value = null!;
+
+ if (!Claims.TryGetValue(type, out var values))
+ return false;
+
+ var first = values.FirstOrDefault();
+ if (first is null)
+ return false;
+
+ value = first;
+ return true;
+ }
+ public IReadOnlyCollection GetAll(string type) => _claims.TryGetValue(type, out var values) ? values : Array.Empty();
+
+ public bool Has(string type) => _claims.ContainsKey(type);
+ public bool HasValue(string type, string value) => _claims.TryGetValue(type, out var values) && values.Contains(value);
+
+ public IReadOnlyCollection Roles => GetAll(ClaimTypes.Role);
+ public IReadOnlyCollection Permissions => GetAll("uauth:permission");
+
+ public bool IsInRole(string role) => HasValue(ClaimTypes.Role, role);
+ public bool HasPermission(string permission) => HasValue("uauth:permission", permission);
+
+ ///
+ /// Flattens claims by taking the first value of each claim.
+ /// Useful for logging, diagnostics, or legacy consumers.
+ ///
+ public IReadOnlyDictionary AsDictionary()
+ {
+ var dict = new Dictionary(StringComparer.Ordinal);
- public string? Get(string type)
- => Claims.TryGetValue(type, out var value)
- ? value
- : null;
+ foreach (var (type, values) in Claims)
+ {
+ var first = values.FirstOrDefault();
+ if (first is not null)
+ dict[type] = first;
+ }
- public static ClaimsSnapshot Empty { get; } = new ClaimsSnapshot(new Dictionary());
+ return dict;
+ }
public override bool Equals(object? obj)
{
@@ -32,12 +68,15 @@ public override bool Equals(object? obj)
if (Claims.Count != other.Claims.Count)
return false;
- foreach (var kv in Claims)
+ foreach (var (type, values) in Claims)
{
- if (!other.Claims.TryGetValue(kv.Key, out var v))
+ if (!other.Claims.TryGetValue(type, out var otherValues))
+ return false;
+
+ if (values.Count != otherValues.Count)
return false;
- if (!string.Equals(kv.Value, v, StringComparison.Ordinal))
+ if (!values.All(v => otherValues.Contains(v)))
return false;
}
@@ -49,22 +88,37 @@ public override int GetHashCode()
unchecked
{
int hash = 17;
- foreach (var kv in Claims.OrderBy(x => x.Key))
+
+ foreach (var (type, values) in Claims.OrderBy(x => x.Key))
{
- hash = hash * 23 + kv.Key.GetHashCode();
- hash = hash * 23 + kv.Value.GetHashCode();
+ hash = hash * 23 + type.GetHashCode();
+
+ foreach (var value in values.OrderBy(v => v))
+ {
+ hash = hash * 23 + value.GetHashCode();
+ }
}
+
return hash;
}
}
public static ClaimsSnapshot From(params (string Type, string Value)[] claims)
{
- var dict = new Dictionary(StringComparer.Ordinal);
+ var dict = new Dictionary>(StringComparer.Ordinal);
+
foreach (var (type, value) in claims)
- dict[type] = value;
+ {
+ if (!dict.TryGetValue(type, out var set))
+ {
+ set = new HashSet(StringComparer.Ordinal);
+ dict[type] = set;
+ }
- return new ClaimsSnapshot(dict);
+ set.Add(value);
+ }
+
+ return new ClaimsSnapshot(dict.ToDictionary(kv => kv.Key, kv => (IReadOnlyCollection)kv.Value.ToArray(), StringComparer.Ordinal));
}
public ClaimsSnapshot With(params (string Type, string Value)[] claims)
@@ -72,14 +126,20 @@ public ClaimsSnapshot With(params (string Type, string Value)[] claims)
if (claims.Length == 0)
return this;
- var dict = new Dictionary(Claims, StringComparer.Ordinal);
+ var dict = Claims.ToDictionary(kv => kv.Key, kv => new HashSet(kv.Value, StringComparer.Ordinal), StringComparer.Ordinal);
foreach (var (type, value) in claims)
{
- dict[type] = value;
+ if (!dict.TryGetValue(type, out var set))
+ {
+ set = new HashSet(StringComparer.Ordinal);
+ dict[type] = set;
+ }
+
+ set.Add(value);
}
- return new ClaimsSnapshot(dict);
+ return new ClaimsSnapshot(dict.ToDictionary(kv => kv.Key, kv => (IReadOnlyCollection)kv.Value.ToArray(), StringComparer.Ordinal));
}
public ClaimsSnapshot Merge(ClaimsSnapshot other)
@@ -90,44 +150,24 @@ public ClaimsSnapshot Merge(ClaimsSnapshot other)
if (Claims.Count == 0)
return other;
- var dict = new Dictionary(Claims, StringComparer.Ordinal);
+ var dict = Claims.ToDictionary(kv => kv.Key, kv => new HashSet(kv.Value, StringComparer.Ordinal), StringComparer.Ordinal);
- foreach (var kv in other.Claims)
+ foreach (var (type, values) in other.Claims)
{
- dict[kv.Key] = kv.Value;
- }
-
- return new ClaimsSnapshot(dict);
- }
-
- public static ClaimsSnapshot FromClaimsPrincipal(ClaimsPrincipal principal)
- {
- if (principal is null)
- return Empty;
-
- if (principal.Identity?.IsAuthenticated != true)
- return Empty;
-
- var dict = new Dictionary(StringComparer.Ordinal);
+ if (!dict.TryGetValue(type, out var set))
+ {
+ set = new HashSet(StringComparer.Ordinal);
+ dict[type] = set;
+ }
- foreach (var claim in principal.Claims)
- {
- dict[claim.Type] = claim.Value;
+ foreach (var value in values)
+ set.Add(value);
}
- return new ClaimsSnapshot(dict);
+ return new ClaimsSnapshot(dict.ToDictionary(kv => kv.Key, kv => (IReadOnlyCollection)kv.Value.ToArray(), StringComparer.Ordinal));
}
- public ClaimsPrincipal ToClaimsPrincipal(string authenticationType = "UltimateAuth")
- {
- if (Claims.Count == 0)
- return new ClaimsPrincipal(new ClaimsIdentity());
-
- var claims = Claims.Select(kv => new Claim(kv.Key, kv.Value));
- var identity = new ClaimsIdentity(claims, authenticationType);
-
- return new ClaimsPrincipal(identity);
- }
+ public static ClaimsSnapshotBuilder Create() => new ClaimsSnapshotBuilder();
}
}
diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/User/IUser.cs b/src/CodeBeam.UltimateAuth.Core/Domain/User/IAuthSubject.cs
similarity index 94%
rename from src/CodeBeam.UltimateAuth.Core/Domain/User/IUser.cs
rename to src/CodeBeam.UltimateAuth.Core/Domain/User/IAuthSubject.cs
index 5222ca9..97eec36 100644
--- a/src/CodeBeam.UltimateAuth.Core/Domain/User/IUser.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Domain/User/IAuthSubject.cs
@@ -5,7 +5,7 @@
/// Includes the unique user identifier and an optional set of claims that
/// may be used during authentication or session creation.
///
- public interface IUser
+ public interface IAuthSubject
{
///
/// Gets the unique identifier of the user.
diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/User/UserKey.cs b/src/CodeBeam.UltimateAuth.Core/Domain/User/UserKey.cs
index f696d21..3e42de9 100644
--- a/src/CodeBeam.UltimateAuth.Core/Domain/User/UserKey.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Domain/User/UserKey.cs
@@ -1,6 +1,10 @@
-namespace CodeBeam.UltimateAuth.Core.Domain
+using CodeBeam.UltimateAuth.Core.Infrastructure;
+using System.Text.Json.Serialization;
+
+namespace CodeBeam.UltimateAuth.Core.Domain
{
- public readonly record struct UserKey
+ [JsonConverter(typeof(UserKeyJsonConverter))]
+ public readonly record struct UserKey : IParsable
{
public string Value { get; }
@@ -31,6 +35,32 @@ public static UserKey FromString(string value)
///
public static UserKey New() => FromGuid(Guid.NewGuid());
+ public static bool TryParse(string? s, IFormatProvider? provider, out UserKey result)
+ {
+ if (string.IsNullOrWhiteSpace(s))
+ {
+ result = default;
+ return false;
+ }
+
+ if (Guid.TryParse(s, out var guid))
+ {
+ result = FromGuid(guid);
+ return true;
+ }
+
+ result = FromString(s);
+ return true;
+ }
+
+ public static UserKey Parse(string s, IFormatProvider? provider)
+ {
+ if (!TryParse(s, provider, out var result))
+ throw new FormatException($"Invalid UserKey value: '{s}'");
+
+ return result;
+ }
+
public override string ToString() => Value;
public static implicit operator string(UserKey key) => key.Value;
diff --git a/src/CodeBeam.UltimateAuth.Core/Extensions/ClaimSnapshotExtensions.cs b/src/CodeBeam.UltimateAuth.Core/Extensions/ClaimSnapshotExtensions.cs
deleted file mode 100644
index 2a8688d..0000000
--- a/src/CodeBeam.UltimateAuth.Core/Extensions/ClaimSnapshotExtensions.cs
+++ /dev/null
@@ -1,45 +0,0 @@
-using System.Security.Claims;
-using CodeBeam.UltimateAuth.Core.Domain;
-
-namespace CodeBeam.UltimateAuth.Core.Extensions
-{
- public static class ClaimsSnapshotExtensions
- {
- public static ClaimsPrincipal ToClaimsPrincipal(this ClaimsSnapshot snapshot, string authenticationType = "UltimateAuth")
- {
- var claims = snapshot
- .AsDictionary()
- .Select(kv => new Claim(kv.Key, kv.Value));
-
- var identity = new ClaimsIdentity(claims, authenticationType);
- return new ClaimsPrincipal(identity);
- }
-
- public static IReadOnlyCollection ToClaims(this ClaimsSnapshot snapshot)
- {
- if (snapshot == null)
- return Array.Empty();
-
- return snapshot
- .AsDictionary()
- .Select(kv => new Claim(kv.Key, kv.Value))
- .ToArray();
- }
-
- public static ClaimsSnapshot ToSnapshot(this IEnumerable claims)
- {
- if (claims == null)
- return ClaimsSnapshot.Empty;
-
- return new ClaimsSnapshot(
- claims
- .GroupBy(c => c.Type)
- .ToDictionary(
- g => g.Key,
- g => g.Last().Value,
- StringComparer.Ordinal
- )
- );
- }
- }
-}
diff --git a/src/CodeBeam.UltimateAuth.Core/Extensions/ClaimsSnapshotExtensions.cs b/src/CodeBeam.UltimateAuth.Core/Extensions/ClaimsSnapshotExtensions.cs
new file mode 100644
index 0000000..fa7bc2f
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Extensions/ClaimsSnapshotExtensions.cs
@@ -0,0 +1,61 @@
+using CodeBeam.UltimateAuth.Core.Domain;
+using System.Security.Claims;
+
+namespace CodeBeam.UltimateAuth.Core.Extensions
+{
+ public static class ClaimsSnapshotExtensions
+ {
+ ///
+ /// Converts a ClaimsSnapshot into an ASP.NET Core ClaimsPrincipal.
+ ///
+ public static ClaimsPrincipal ToClaimsPrincipal(this ClaimsSnapshot snapshot, string authenticationType = "UltimateAuth")
+ {
+ if (snapshot == null)
+ return new ClaimsPrincipal(new ClaimsIdentity());
+
+ var claims = snapshot.Claims.SelectMany(kv => kv.Value.Select(value => new Claim(kv.Key, value)));
+
+ var identity = new ClaimsIdentity(claims, authenticationType);
+ return new ClaimsPrincipal(identity);
+ }
+
+ ///
+ /// Converts an ASP.NET Core ClaimsPrincipal into a ClaimsSnapshot.
+ ///
+ public static ClaimsSnapshot ToClaimsSnapshot(this ClaimsPrincipal principal)
+ {
+ if (principal is null)
+ return ClaimsSnapshot.Empty;
+
+ if (principal.Identity?.IsAuthenticated != true)
+ return ClaimsSnapshot.Empty;
+
+ var dict = new Dictionary>(StringComparer.Ordinal);
+
+ foreach (var claim in principal.Claims)
+ {
+ if (!dict.TryGetValue(claim.Type, out var set))
+ {
+ set = new HashSet(StringComparer.Ordinal);
+ dict[claim.Type] = set;
+ }
+
+ set.Add(claim.Value);
+ }
+
+ return new ClaimsSnapshot(dict.ToDictionary(kv => kv.Key, kv => (IReadOnlyCollection)kv.Value.ToArray(), StringComparer.Ordinal));
+ }
+
+ public static IEnumerable ToClaims(this ClaimsSnapshot snapshot)
+ {
+ foreach (var (type, values) in snapshot.Claims)
+ {
+ foreach (var value in values)
+ {
+ yield return new Claim(type, value);
+ }
+ }
+ }
+
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Infrastructure/AuthUserRecord.cs b/src/CodeBeam.UltimateAuth.Core/Infrastructure/AuthUserRecord.cs
new file mode 100644
index 0000000..79885c2
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Infrastructure/AuthUserRecord.cs
@@ -0,0 +1,50 @@
+namespace CodeBeam.UltimateAuth.Core.Infrastructure
+{
+ ///
+ /// Represents the minimal, immutable user snapshot required by the UltimateAuth Core
+ /// during authentication discovery and subject binding.
+ ///
+ /// This type is NOT a domain user model.
+ /// It contains only normalized, opinionless fields that determine whether
+ /// a user can participate in authentication flows.
+ ///
+ /// AuthUserRecord is produced by the Users domain as a boundary projection
+ /// and is never mutated by the Core.
+ ///
+ public sealed record AuthUserRecord
+ {
+ ///
+ /// Application-level user identifier.
+ ///
+ public required TUserId Id { get; init; }
+
+ ///
+ /// Primary login identifier (username, email, etc).
+ /// Used only for discovery and uniqueness checks.
+ ///
+ public required string Identifier { get; init; }
+
+ ///
+ /// Indicates whether the user is considered active for authentication purposes.
+ /// Domain-specific statuses are normalized into this flag by the Users domain.
+ ///
+ public required bool IsActive { get; init; }
+
+ ///
+ /// Indicates whether the user is deleted.
+ /// Deleted users are never eligible for authentication.
+ ///
+ public required bool IsDeleted { get; init; }
+
+ ///
+ /// The timestamp when the user was originally created.
+ /// Provided for invariant validation and auditing purposes.
+ ///
+ public required DateTimeOffset CreatedAt { get; init; }
+
+ ///
+ /// The timestamp when the user was deleted, if applicable.
+ ///
+ public DateTimeOffset? DeletedAt { get; init; }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/DefaultAuthAuthority.cs b/src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/DefaultAuthAuthority.cs
index 830ebfe..1b82692 100644
--- a/src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/DefaultAuthAuthority.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/DefaultAuthAuthority.cs
@@ -14,9 +14,8 @@ public DefaultAuthAuthority(IEnumerable invariants, IEnumer
_policies = policies ?? Array.Empty();
}
- public AuthorizationResult Decide(AuthContext context)
+ public AccessDecisionResult Decide(AuthContext context, IEnumerable? policies = null)
{
- // 1. Invariants
foreach (var invariant in _invariants)
{
var result = invariant.Decide(context);
@@ -24,10 +23,11 @@ public AuthorizationResult Decide(AuthContext context)
return result;
}
- // 2. Policies
bool challenged = false;
- foreach (var policy in _policies)
+ var effectivePolicies = _policies.Concat(policies ?? Enumerable.Empty());
+
+ foreach (var policy in effectivePolicies)
{
if (!policy.AppliesTo(context))
continue;
@@ -42,8 +42,9 @@ public AuthorizationResult Decide(AuthContext context)
}
return challenged
- ? AuthorizationResult.Challenge("Additional verification required.")
- : AuthorizationResult.Allow();
+ ? AccessDecisionResult.Challenge("Additional verification required.")
+ : AccessDecisionResult.Allow();
}
+
}
}
diff --git a/src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/DeviceMismatchPolicy.cs b/src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/DeviceMismatchPolicy.cs
index d8472f8..1d53f38 100644
--- a/src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/DeviceMismatchPolicy.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/DeviceMismatchPolicy.cs
@@ -8,7 +8,7 @@ public sealed class DeviceMismatchPolicy : IAuthorityPolicy
public bool AppliesTo(AuthContext context)
=> context.Device is not null;
- public AuthorizationResult Decide(AuthContext context)
+ public AccessDecisionResult Decide(AuthContext context)
{
var device = context.Device;
@@ -18,14 +18,14 @@ public AuthorizationResult Decide(AuthContext context)
return context.Operation switch
{
AuthOperation.Access =>
- AuthorizationResult.Deny("Access from unknown device."),
+ AccessDecisionResult.Deny("Access from unknown device."),
AuthOperation.Refresh =>
- AuthorizationResult.Challenge("Device verification required."),
+ AccessDecisionResult.Challenge("Device verification required."),
- AuthOperation.Login => AuthorizationResult.Allow(), // login establishes device
+ AuthOperation.Login => AccessDecisionResult.Allow(), // login establishes device
- _ => AuthorizationResult.Allow()
+ _ => AccessDecisionResult.Allow()
};
}
}
diff --git a/src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/DevicePresenceInvariant.cs b/src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/DevicePresenceInvariant.cs
index a2fc709..5bcd732 100644
--- a/src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/DevicePresenceInvariant.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/DevicePresenceInvariant.cs
@@ -5,15 +5,15 @@ namespace CodeBeam.UltimateAuth.Core.Infrastructure
{
public sealed class DevicePresenceInvariant : IAuthorityInvariant
{
- public AuthorizationResult Decide(AuthContext context)
+ public AccessDecisionResult Decide(AuthContext context)
{
if (context.Operation is AuthOperation.Login or AuthOperation.Refresh)
{
if (context.Device is null)
- return AuthorizationResult.Deny("Device information is required.");
+ return AccessDecisionResult.Deny("Device information is required.");
}
- return AuthorizationResult.Allow();
+ return AccessDecisionResult.Allow();
}
}
diff --git a/src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/ExpiredSessionInvariant.cs b/src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/ExpiredSessionInvariant.cs
index a3eedf6..cb9e14c 100644
--- a/src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/ExpiredSessionInvariant.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/ExpiredSessionInvariant.cs
@@ -6,22 +6,22 @@ namespace CodeBeam.UltimateAuth.Core.Infrastructure
{
public sealed class ExpiredSessionInvariant : IAuthorityInvariant
{
- public AuthorizationResult Decide(AuthContext context)
+ public AccessDecisionResult Decide(AuthContext context)
{
if (context.Operation == AuthOperation.Login)
- return AuthorizationResult.Allow();
+ return AccessDecisionResult.Allow();
var session = context.Session;
if (session is null)
- return AuthorizationResult.Allow();
+ return AccessDecisionResult.Allow();
if (session.State == SessionState.Expired)
{
- return AuthorizationResult.Deny("Session has expired.");
+ return AccessDecisionResult.Deny("Session has expired.");
}
- return AuthorizationResult.Allow();
+ return AccessDecisionResult.Allow();
}
}
}
diff --git a/src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/InvalidOrRevokedSessionInvariant.cs b/src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/InvalidOrRevokedSessionInvariant.cs
index 1929bb5..7d8fe9a 100644
--- a/src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/InvalidOrRevokedSessionInvariant.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/InvalidOrRevokedSessionInvariant.cs
@@ -6,15 +6,15 @@ namespace CodeBeam.UltimateAuth.Core.Infrastructure
{
public sealed class InvalidOrRevokedSessionInvariant : IAuthorityInvariant
{
- public AuthorizationResult Decide(AuthContext context)
+ public AccessDecisionResult Decide(AuthContext context)
{
if (context.Operation == AuthOperation.Login)
- return AuthorizationResult.Allow();
+ return AccessDecisionResult.Allow();
var session = context.Session;
if (session is null)
- return AuthorizationResult.Deny("Session is required for this operation.");
+ return AccessDecisionResult.Deny("Session is required for this operation.");
if (session.State == SessionState.Invalid ||
session.State == SessionState.NotFound ||
@@ -22,10 +22,10 @@ public AuthorizationResult Decide(AuthContext context)
session.State == SessionState.SecurityMismatch ||
session.State == SessionState.DeviceMismatch)
{
- return AuthorizationResult.Deny($"Session state is invalid: {session.State}");
+ return AccessDecisionResult.Deny($"Session state is invalid: {session.State}");
}
- return AuthorizationResult.Allow();
+ return AccessDecisionResult.Allow();
}
}
}
diff --git a/src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/UAuthModeOperationPolicy.cs b/src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/UAuthModeOperationPolicy.cs
index 5096041..459e4ca 100644
--- a/src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/UAuthModeOperationPolicy.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/UAuthModeOperationPolicy.cs
@@ -7,33 +7,33 @@ public sealed class AuthModeOperationPolicy : IAuthorityPolicy
{
public bool AppliesTo(AuthContext context) => true; // Applies to all contexts
- public AuthorizationResult Decide(AuthContext context)
+ public AccessDecisionResult Decide(AuthContext context)
{
return context.Mode switch
{
UAuthMode.PureOpaque => DecideForPureOpaque(context),
UAuthMode.PureJwt => DecideForPureJwt(context),
- UAuthMode.Hybrid => AuthorizationResult.Allow(),
- UAuthMode.SemiHybrid => AuthorizationResult.Allow(),
+ UAuthMode.Hybrid => AccessDecisionResult.Allow(),
+ UAuthMode.SemiHybrid => AccessDecisionResult.Allow(),
- _ => AuthorizationResult.Deny("Unsupported authentication mode.")
+ _ => AccessDecisionResult.Deny("Unsupported authentication mode.")
};
}
- private static AuthorizationResult DecideForPureOpaque(AuthContext context)
+ private static AccessDecisionResult DecideForPureOpaque(AuthContext context)
{
if (context.Operation == AuthOperation.Refresh)
- return AuthorizationResult.Deny("Refresh operation is not supported in PureOpaque mode.");
+ return AccessDecisionResult.Deny("Refresh operation is not supported in PureOpaque mode.");
- return AuthorizationResult.Allow();
+ return AccessDecisionResult.Allow();
}
- private static AuthorizationResult DecideForPureJwt(AuthContext context)
+ private static AccessDecisionResult DecideForPureJwt(AuthContext context)
{
if (context.Operation == AuthOperation.Access)
- return AuthorizationResult.Deny("Session-based access is not supported in PureJwt mode.");
+ return AccessDecisionResult.Deny("Session-based access is not supported in PureJwt mode.");
- return AuthorizationResult.Allow();
+ return AccessDecisionResult.Allow();
}
}
}
diff --git a/src/CodeBeam.UltimateAuth.Core/Infrastructure/IInMemoryUserIdProvider.cs b/src/CodeBeam.UltimateAuth.Core/Infrastructure/IInMemoryUserIdProvider.cs
new file mode 100644
index 0000000..3c61b2f
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Infrastructure/IInMemoryUserIdProvider.cs
@@ -0,0 +1,8 @@
+namespace CodeBeam.UltimateAuth.Core.Infrastructure
+{
+ public interface IInMemoryUserIdProvider
+ {
+ TUserId GetAdminUserId();
+ TUserId GetUserUserId();
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Infrastructure/UAuthUserIdConverter.cs b/src/CodeBeam.UltimateAuth.Core/Infrastructure/UAuthUserIdConverter.cs
index 26d0d1e..44465ac 100644
--- a/src/CodeBeam.UltimateAuth.Core/Infrastructure/UAuthUserIdConverter.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Infrastructure/UAuthUserIdConverter.cs
@@ -33,11 +33,14 @@ public string ToString(TUserId id)
{
return id switch
{
- int v => v.ToString(CultureInfo.InvariantCulture),
- long v => v.ToString(CultureInfo.InvariantCulture),
+ UserKey v => v.Value,
Guid v => v.ToString("N"),
string v => v,
- _ => JsonSerializer.Serialize(id)
+ int v => v.ToString(CultureInfo.InvariantCulture),
+ long v => v.ToString(CultureInfo.InvariantCulture),
+
+ _ => throw new InvalidOperationException($"Unsupported UserId type: {typeof(TUserId).FullName}. " +
+ "Provide a custom IUserIdConverter.")
};
}
@@ -62,11 +65,11 @@ public TUserId FromString(string value)
{
return typeof(TUserId) switch
{
- Type t when t == typeof(int) => (TUserId)(object)int.Parse(value, CultureInfo.InvariantCulture),
- Type t when t == typeof(long) => (TUserId)(object)long.Parse(value, CultureInfo.InvariantCulture),
+ Type t when t == typeof(UserKey) => (TUserId)(object)UserKey.FromString(value),
Type t when t == typeof(Guid) => (TUserId)(object)Guid.Parse(value),
Type t when t == typeof(string) => (TUserId)(object)value,
- Type t when t == typeof(UserKey) => (TUserId)(object)UserKey.FromString(value),
+ Type t when t == typeof(int) => (TUserId)(object)int.Parse(value, CultureInfo.InvariantCulture),
+ Type t when t == typeof(long) => (TUserId)(object)long.Parse(value, CultureInfo.InvariantCulture),
_ => JsonSerializer.Deserialize(value)
?? throw new UAuthInternalException("Cannot deserialize TUserId")
@@ -92,8 +95,7 @@ public bool TryFromString(string value, out TUserId? id)
///
/// Binary data representing the user id.
/// The reconstructed user id.
- public TUserId FromBytes(byte[] binary) =>
- FromString(Encoding.UTF8.GetString(binary));
+ public TUserId FromBytes(byte[] binary) => FromString(Encoding.UTF8.GetString(binary));
public bool TryFromBytes(byte[] binary, out TUserId? id)
{
diff --git a/src/CodeBeam.UltimateAuth.Core/Infrastructure/UserKeyJsonConverter.cs b/src/CodeBeam.UltimateAuth.Core/Infrastructure/UserKeyJsonConverter.cs
new file mode 100644
index 0000000..21f731e
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Infrastructure/UserKeyJsonConverter.cs
@@ -0,0 +1,22 @@
+using CodeBeam.UltimateAuth.Core.Domain;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace CodeBeam.UltimateAuth.Core.Infrastructure
+{
+ public sealed class UserKeyJsonConverter : JsonConverter
+ {
+ public override UserKey Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType != JsonTokenType.String)
+ throw new JsonException("UserKey must be a string.");
+
+ return UserKey.FromString(reader.GetString()!);
+ }
+
+ public override void Write(Utf8JsonWriter writer, UserKey value, JsonSerializerOptions options)
+ {
+ writer.WriteStringValue(value.Value);
+ }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Infrastructure/UserRecord.cs b/src/CodeBeam.UltimateAuth.Core/Infrastructure/UserRecord.cs
deleted file mode 100644
index a5ad9a1..0000000
--- a/src/CodeBeam.UltimateAuth.Core/Infrastructure/UserRecord.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using CodeBeam.UltimateAuth.Core.Domain;
-
-namespace CodeBeam.UltimateAuth.Core.Infrastructure
-{
- public sealed class UserRecord
- {
- public required TUserId Id { get; init; }
- public required string Username { get; init; }
- public required string PasswordHash { get; init; }
- public ClaimsSnapshot Claims { get; init; } = ClaimsSnapshot.Empty;
- public bool RequiresMfa { get; init; }
- public bool IsActive { get; init; } = true;
- public DateTimeOffset CreatedAt { get; init; }
- public bool IsDeleted { get; init; }
- }
-}
diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthFlowContextFactory.cs b/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthFlowContextFactory.cs
index b2c02f8..6b3e618 100644
--- a/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthFlowContextFactory.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Auth/Context/AuthFlowContextFactory.cs
@@ -4,6 +4,7 @@
using CodeBeam.UltimateAuth.Server.Extensions;
using CodeBeam.UltimateAuth.Server.Infrastructure;
using CodeBeam.UltimateAuth.Server.Options;
+using CodeBeam.UltimateAuth.Server.Services;
using Microsoft.AspNetCore.Http;
namespace CodeBeam.UltimateAuth.Server.Auth
diff --git a/src/CodeBeam.UltimateAuth.Server/Authentication/UAuthAuthenticationHandler.cs b/src/CodeBeam.UltimateAuth.Server/Authentication/UAuthAuthenticationHandler.cs
index bccddfa..cc23e9b 100644
--- a/src/CodeBeam.UltimateAuth.Server/Authentication/UAuthAuthenticationHandler.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Authentication/UAuthAuthenticationHandler.cs
@@ -1,8 +1,9 @@
using CodeBeam.UltimateAuth.Core.Abstractions;
using CodeBeam.UltimateAuth.Core.Contracts;
using CodeBeam.UltimateAuth.Core.Domain;
-using CodeBeam.UltimateAuth.Server.Auth;
+using CodeBeam.UltimateAuth.Core.Extensions;
using CodeBeam.UltimateAuth.Server.Infrastructure;
+using CodeBeam.UltimateAuth.Server.Services;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ -55,32 +56,47 @@ protected override async Task HandleAuthenticateAsync()
if (!result.IsValid || result.UserKey is null)
return AuthenticateResult.NoResult();
- var principal = CreatePrincipal(result);
- var ticket = new AuthenticationTicket(principal,UAuthCookieDefaults.AuthenticationScheme);
+ var principal = result.Claims.ToClaimsPrincipal(UAuthCookieDefaults.AuthenticationScheme);
+ return AuthenticateResult.Success(new AuthenticationTicket(principal, UAuthCookieDefaults.AuthenticationScheme));
- return AuthenticateResult.Success(ticket);
+
+ //var principal = CreatePrincipal(result);
+ //var ticket = new AuthenticationTicket(principal,UAuthCookieDefaults.AuthenticationScheme);
+
+ //return AuthenticateResult.Success(ticket);
}
private static ClaimsPrincipal CreatePrincipal(SessionValidationResult result)
{
- var claims = new List
- {
- new Claim(ClaimTypes.NameIdentifier, result.UserKey.Value),
- new Claim("uauth:session_id", result.SessionId.ToString())
- };
-
- if (!string.IsNullOrEmpty(result.TenantId))
- {
- claims.Add(new Claim("uauth:tenant", result.TenantId));
- }
-
- // Session claims (snapshot)
- foreach (var (key, value) in result.Claims.AsDictionary())
- {
- claims.Add(new Claim(key, value));
- }
-
- var identity = new ClaimsIdentity(claims, UAuthCookieDefaults.AuthenticationScheme);
+ //var claims = new List
+ //{
+ // new Claim(ClaimTypes.NameIdentifier, result.UserKey.Value),
+ // new Claim("uauth:session_id", result.SessionId.ToString())
+ //};
+
+ //if (!string.IsNullOrEmpty(result.TenantId))
+ //{
+ // claims.Add(new Claim("uauth:tenant", result.TenantId));
+ //}
+
+ //// Session claims (snapshot)
+ //foreach (var (key, value) in result.Claims.AsDictionary())
+ //{
+ // if (key == "http://schemas.microsoft.com/ws/2008/06/identity/claims/role")
+ // {
+ // foreach (var role in value.Split(','))
+ // claims.Add(new Claim(ClaimTypes.Role, role));
+ // }
+ // else
+ // {
+ // claims.Add(new Claim(key, value));
+ // }
+ //}
+
+ //var identity = new ClaimsIdentity(claims, UAuthCookieDefaults.AuthenticationScheme);
+ //return new ClaimsPrincipal(identity);
+
+ var identity = new ClaimsIdentity(result.Claims.ToClaims(), UAuthCookieDefaults.AuthenticationScheme);
return new ClaimsPrincipal(identity);
}
diff --git a/src/CodeBeam.UltimateAuth.Server/CodeBeam.UltimateAuth.Server.csproj b/src/CodeBeam.UltimateAuth.Server/CodeBeam.UltimateAuth.Server.csproj
index 442f314..f572a09 100644
--- a/src/CodeBeam.UltimateAuth.Server/CodeBeam.UltimateAuth.Server.csproj
+++ b/src/CodeBeam.UltimateAuth.Server/CodeBeam.UltimateAuth.Server.csproj
@@ -18,11 +18,13 @@
+
+
-
-
-
-
+
+
+
+
diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IAuthorizationEndpointHandler.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IAuthorizationEndpointHandler.cs
new file mode 100644
index 0000000..53460a6
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IAuthorizationEndpointHandler.cs
@@ -0,0 +1,13 @@
+using CodeBeam.UltimateAuth.Core.Domain;
+using Microsoft.AspNetCore.Http;
+
+namespace CodeBeam.UltimateAuth.Server.Endpoints
+{
+ public interface IAuthorizationEndpointHandler
+ {
+ Task CheckAsync(HttpContext ctx);
+ Task GetRolesAsync(UserKey userKey, HttpContext ctx);
+ Task AssignRoleAsync(UserKey userKey, HttpContext ctx);
+ Task RemoveRoleAsync(UserKey userKey, HttpContext ctx);
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/ICredentialEndpointHandler.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/ICredentialEndpointHandler.cs
new file mode 100644
index 0000000..2b1ac88
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/ICredentialEndpointHandler.cs
@@ -0,0 +1,11 @@
+using Microsoft.AspNetCore.Http;
+
+namespace CodeBeam.UltimateAuth.Server.Endpoints
+{
+ public interface ICredentialEndpointHandler
+ {
+ Task SetInitialAsync(HttpContext ctx);
+ Task ResetPasswordAsync(HttpContext ctx);
+ Task RevokeAllAsync(HttpContext ctx);
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IUserEndpointHandler.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IUserEndpointHandler.cs
new file mode 100644
index 0000000..bc96fc7
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IUserEndpointHandler.cs
@@ -0,0 +1,11 @@
+using Microsoft.AspNetCore.Http;
+
+namespace CodeBeam.UltimateAuth.Server.Endpoints
+{
+ public interface IUserEndpointHandler
+ {
+ Task CreateAsync(HttpContext ctx);
+ Task ChangeStatusAsync(HttpContext ctx);
+ Task DeleteAsync(HttpContext ctx);
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IUserProfileAdminEndpointHandler.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IUserProfileAdminEndpointHandler.cs
new file mode 100644
index 0000000..7717e26
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IUserProfileAdminEndpointHandler.cs
@@ -0,0 +1,11 @@
+using CodeBeam.UltimateAuth.Core.Domain;
+using Microsoft.AspNetCore.Http;
+
+namespace CodeBeam.UltimateAuth.Server.Endpoints
+{
+ public interface IUserProfileAdminEndpointHandler
+ {
+ Task GetAsync(UserKey userKey, HttpContext ctx);
+ Task UpdateAsync(UserKey userKey, HttpContext ctx);
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IUserProfileEndpointHandler.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IUserProfileEndpointHandler.cs
new file mode 100644
index 0000000..afc9e36
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IUserProfileEndpointHandler.cs
@@ -0,0 +1,10 @@
+using Microsoft.AspNetCore.Http;
+
+namespace CodeBeam.UltimateAuth.Server.Endpoints
+{
+ public interface IUserProfileEndpointHandler
+ {
+ Task GetAsync(HttpContext ctx);
+ Task UpdateAsync(HttpContext ctx);
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultLoginEndpointHandler.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultLoginEndpointHandler.cs
index 9131458..bb215f4 100644
--- a/src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultLoginEndpointHandler.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultLoginEndpointHandler.cs
@@ -53,13 +53,11 @@ public async Task LoginAsync(HttpContext ctx)
if (string.IsNullOrWhiteSpace(identifier) || string.IsNullOrWhiteSpace(secret))
return RedirectFailure(ctx, AuthFailureReason.InvalidCredentials, authFlow.OriginalOptions);
- var tenantCtx = ctx.GetTenantContext();
-
var flowRequest = new LoginRequest
{
Identifier = identifier,
Secret = secret,
- TenantId = tenantCtx.TenantId,
+ TenantId = authFlow.TenantId,
At = _clock.UtcNow,
Device = authFlow.Device,
RequestTokens = shouldIssueTokens
diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultValidateEndpointHandler.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultValidateEndpointHandler.cs
index 5bd10ac..f47f8a6 100644
--- a/src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultValidateEndpointHandler.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/DefaultValidateEndpointHandler.cs
@@ -4,6 +4,7 @@
using CodeBeam.UltimateAuth.Core.Extensions;
using CodeBeam.UltimateAuth.Server.Auth;
using CodeBeam.UltimateAuth.Server.Infrastructure;
+using CodeBeam.UltimateAuth.Server.Services;
using Microsoft.AspNetCore.Http;
namespace CodeBeam.UltimateAuth.Server.Endpoints
diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/UAuthEndpointRegistrar.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/UAuthEndpointRegistrar.cs
index 0d3ccc9..2815489 100644
--- a/src/CodeBeam.UltimateAuth.Server/Endpoints/UAuthEndpointRegistrar.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/UAuthEndpointRegistrar.cs
@@ -19,7 +19,7 @@ public class UAuthEndpointRegistrar : IAuthEndpointRegistrar
{
public void MapEndpoints(RouteGroupBuilder rootGroup, UAuthServerOptions options)
{
- // Base: /auth
+ // Default base: /auth
string basePrefix = options.RoutePrefix.TrimStart('/');
bool useRouteTenant = options.MultiTenant.Enabled && options.MultiTenant.EnableRoute;
@@ -105,6 +105,72 @@ public void MapEndpoints(RouteGroupBuilder rootGroup, UAuthServerOptions options
user.MapPost("/permissions/check", async ([FromServices] IUserInfoEndpointHandler h, HttpContext ctx)
=> await h.CheckPermissionAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.PermissionQuery));
}
+
+ if (options.EnableUserLifecycleEndpoints)
+ {
+ var users = group.MapGroup("/users");
+
+ users.MapPost("", async ([FromServices] IUserEndpointHandler h, HttpContext ctx)
+ => await h.CreateAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserManagement));
+
+ users.MapPost("/status", async ([FromServices] IUserEndpointHandler h, HttpContext ctx)
+ => await h.ChangeStatusAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserManagement));
+
+ // Post is intended here
+ users.MapPost("/delete", async ([FromServices] IUserEndpointHandler h, HttpContext ctx)
+ => await h.DeleteAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserManagement));
+ }
+
+ if (options.EnableUserProfileEndpoints)
+ {
+ var userProfile = group.MapGroup("/users");
+
+ userProfile.MapGet("/me", async ([FromServices] IUserProfileEndpointHandler h, HttpContext ctx)
+ => await h.GetAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserProfile));
+
+ userProfile.MapPut("/me", async ([FromServices] IUserProfileEndpointHandler h, HttpContext ctx)
+ => await h.UpdateAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserInfo));
+ }
+
+ if (options.EnableAdminChangeUserProfileEndpoints)
+ {
+ var admin = group.MapGroup("/admin/users");
+
+ admin.MapGet("/{userKey}/profile", async ([FromServices] IUserProfileAdminEndpointHandler h, UserKey userKey, HttpContext ctx)
+ => await h.GetAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserManagement));
+
+ admin.MapPut("/{userKey}/profile", async ([FromServices] IUserProfileAdminEndpointHandler h, UserKey userKey, HttpContext ctx)
+ => await h.UpdateAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserManagement));
+ }
+
+ if (options.EnableCredentialsEndpoints)
+ {
+ var credentials = group.MapGroup("/credentials");
+
+ credentials.MapPost("/initial", async ([FromServices] ICredentialEndpointHandler h, HttpContext ctx)
+ => await h.SetInitialAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement));
+
+ credentials.MapPost("/password/reset", async ([FromServices] ICredentialEndpointHandler h, HttpContext ctx)
+ => await h.ResetPasswordAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement));
+
+ credentials.MapPost("/revoke-all", async ([FromServices] ICredentialEndpointHandler h, HttpContext ctx)
+ => await h.RevokeAllAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement));
+ }
+
+ if (options.EnableAuthorizationEndpoints)
+ {
+ var authz = group.MapGroup("/authorization");
+
+ authz.MapPost("/check", async ([FromServices] IAuthorizationEndpointHandler h, HttpContext ctx)
+ => await h.CheckAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.AuthorizationManagement));
+
+ authz.MapPost("/users/{userKey}/roles", async ([FromServices] IAuthorizationEndpointHandler h, UserKey userKey, HttpContext ctx)
+ => await h.AssignRoleAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.AuthorizationManagement));
+
+ authz.MapDelete("/users/{userKey}/roles", async ([FromServices] IAuthorizationEndpointHandler h, UserKey userKey, HttpContext ctx)
+ => await h.RemoveRoleAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.AuthorizationManagement));
+ }
+
}
}
diff --git a/src/CodeBeam.UltimateAuth.Server/Extensions/HttpContextJsonExtensions.cs b/src/CodeBeam.UltimateAuth.Server/Extensions/HttpContextJsonExtensions.cs
new file mode 100644
index 0000000..4b2c9b1
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Extensions/HttpContextJsonExtensions.cs
@@ -0,0 +1,26 @@
+using System.Text.Json;
+using Microsoft.AspNetCore.Http;
+
+namespace CodeBeam.UltimateAuth.Server.Extensions
+{
+ public static class HttpContextJsonExtensions
+ {
+ private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
+
+ public static async Task ReadJsonAsync(this HttpContext ctx, CancellationToken ct = default)
+ {
+ if (!ctx.Request.HasJsonContentType())
+ throw new InvalidOperationException("Request content type must be application/json.");
+
+ if (ctx.Request.Body is null)
+ throw new InvalidOperationException("Request body is empty.");
+
+ var result = await JsonSerializer.DeserializeAsync(ctx.Request.Body, JsonOptions, ct);
+
+ if (result is null)
+ throw new InvalidOperationException("Request body could not be deserialized.");
+
+ return result;
+ }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthServerServiceCollectionExtensions.cs b/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthServerServiceCollectionExtensions.cs
index daba2ba..9b771a3 100644
--- a/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthServerServiceCollectionExtensions.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthServerServiceCollectionExtensions.cs
@@ -1,23 +1,29 @@
-using CodeBeam.UltimateAuth.Core.Abstractions;
+using CodeBeam.UltimateAuth.Authorization;
+using CodeBeam.UltimateAuth.Core;
+using CodeBeam.UltimateAuth.Core.Abstractions;
using CodeBeam.UltimateAuth.Core.Domain;
using CodeBeam.UltimateAuth.Core.Extensions;
using CodeBeam.UltimateAuth.Core.Infrastructure;
using CodeBeam.UltimateAuth.Core.MultiTenancy;
using CodeBeam.UltimateAuth.Core.Options;
+using CodeBeam.UltimateAuth.Credentials;
using CodeBeam.UltimateAuth.Server.Abstactions;
using CodeBeam.UltimateAuth.Server.Abstractions;
using CodeBeam.UltimateAuth.Server.Auth;
using CodeBeam.UltimateAuth.Server.Cookies;
using CodeBeam.UltimateAuth.Server.Endpoints;
using CodeBeam.UltimateAuth.Server.Infrastructure;
+using CodeBeam.UltimateAuth.Server.Infrastructure.Auth;
using CodeBeam.UltimateAuth.Server.Infrastructure.Hub;
using CodeBeam.UltimateAuth.Server.Infrastructure.Session;
using CodeBeam.UltimateAuth.Server.Issuers;
+using CodeBeam.UltimateAuth.Server.Login;
+using CodeBeam.UltimateAuth.Server.Login.Orchestrators;
using CodeBeam.UltimateAuth.Server.MultiTenancy;
using CodeBeam.UltimateAuth.Server.Options;
using CodeBeam.UltimateAuth.Server.Services;
using CodeBeam.UltimateAuth.Server.Stores;
-using CodeBeam.UltimateAuth.Server.Users;
+using CodeBeam.UltimateAuth.Users;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
@@ -31,12 +37,16 @@ public static class UAuthServerServiceCollectionExtensions
public static IServiceCollection AddUltimateAuthServer(this IServiceCollection services)
{
services.AddUltimateAuth();
+ AddUsersInternal(services);
+ AddCredentialsInternal(services);
return services.AddUltimateAuthServerInternal();
}
public static IServiceCollection AddUltimateAuthServer(this IServiceCollection services, IConfiguration configuration)
{
services.AddUltimateAuth(configuration);
+ AddUsersInternal(services);
+ AddCredentialsInternal(services);
services.Configure(configuration.GetSection("UltimateAuth:Server"));
return services.AddUltimateAuthServerInternal();
@@ -45,6 +55,8 @@ public static IServiceCollection AddUltimateAuthServer(this IServiceCollection s
public static IServiceCollection AddUltimateAuthServer(this IServiceCollection services, Action configure)
{
services.AddUltimateAuth();
+ AddUsersInternal(services);
+ AddCredentialsInternal(services);
services.Configure(configure);
return services.AddUltimateAuthServerInternal();
@@ -123,16 +135,15 @@ private static IServiceCollection AddUltimateAuthServerInternal(this IServiceCol
services.AddScoped();
// Public resolver
- services.AddScoped();
+ services.TryAddScoped();
services.TryAddScoped();
- services.AddScoped(typeof(IUAuthFlowService<>), typeof(UAuthFlowService<>));
- services.AddScoped(typeof(IRefreshFlowService), typeof(DefaultRefreshFlowService));
- services.AddScoped(typeof(IUAuthSessionManager), typeof(UAuthSessionManager));
- services.AddScoped(typeof(IUAuthUserService<>), typeof(UAuthUserService<>));
+ services.TryAddScoped(typeof(IUAuthFlowService<>), typeof(UAuthFlowService<>));
+ services.TryAddScoped(typeof(IRefreshFlowService), typeof(DefaultRefreshFlowService));
+ services.TryAddScoped(typeof(IUAuthSessionManager), typeof(UAuthSessionManager));
- services.AddSingleton();
+ services.TryAddSingleton();
// TODO: Allow custom cookie manager via options
//services.AddSingleton();
@@ -154,20 +165,25 @@ private static IServiceCollection AddUltimateAuthServerInternal(this IServiceCol
services.TryAddScoped(typeof(IUserAccessor), typeof(UAuthUserAccessor));
services.TryAddScoped();
- services.TryAddScoped(typeof(IUserAuthenticator<>), typeof(DefaultUserAuthenticator<>));
+ //services.TryAddScoped(typeof(IUserAuthenticator<>), typeof(DefaultUserAuthenticator<>));
services.TryAddScoped(typeof(ISessionOrchestrator), typeof(UAuthSessionOrchestrator));
+ services.TryAddScoped(typeof(ILoginOrchestrator<>), typeof(DefaultLoginOrchestrator<>));
+ services.TryAddScoped();
+ services.TryAddScoped();
services.TryAddScoped();
+ services.TryAddScoped();
+ services.TryAddScoped();
services.TryAddScoped(typeof(ISessionQueryService), typeof(UAuthSessionQueryService));
services.TryAddScoped(typeof(IRefreshTokenResolver), typeof(DefaultRefreshTokenResolver));
services.TryAddScoped(typeof(ISessionTouchService), typeof(DefaultSessionTouchService));
services.TryAddScoped();
services.TryAddScoped();
services.TryAddScoped();
- services.AddScoped();
+ services.TryAddScoped();
services.TryAddScoped();
services.TryAddScoped();
services.TryAddScoped();
- services.AddScoped();
+ services.TryAddScoped();
services.TryAddSingleton();
services.TryAddSingleton();
@@ -176,64 +192,97 @@ private static IServiceCollection AddUltimateAuthServerInternal(this IServiceCol
services.TryAddSingleton();
services.TryAddScoped();
services.TryAddSingleton();
- services.AddScoped();
+ services.TryAddScoped();
- services.AddScoped(typeof(IRefreshTokenValidator), typeof(DefaultRefreshTokenValidator));
- services.AddScoped();
- services.AddScoped(typeof(IRefreshTokenRotationService), typeof(RefreshTokenRotationService));
+ services.TryAddScoped(typeof(IRefreshTokenValidator), typeof(DefaultRefreshTokenValidator));
+ services.TryAddScoped();
+ services.TryAddScoped(typeof(IRefreshTokenRotationService), typeof(RefreshTokenRotationService));
- services.AddSingleton();
- services.AddSingleton();
- services.AddSingleton();
- services.AddScoped();
- services.AddSingleton();
+ services.TryAddSingleton();
+ services.TryAddSingleton();
+ services.TryAddSingleton();
+ services.TryAddScoped();
- services.AddScoped();
+ services.TryAddScoped();
- services.AddSingleton();
- services.AddScoped();
- services.AddScoped();
- services.AddScoped();
+ services.TryAddSingleton();
+ services.TryAddScoped();
+ services.TryAddScoped();
+ services.TryAddScoped();
+ services.TryAddScoped();
+ services.TryAddScoped();
- services.AddScoped();
+ services.TryAddScoped();
// -----------------------------
// ENDPOINTS
// -----------------------------
services.AddHttpContextAccessor();
- services.AddScoped();
- services.AddScoped();
- services.AddScoped();
+ services.TryAddScoped();
+ services.TryAddScoped();
+ services.TryAddScoped();
- services.AddScoped();
+ services.TryAddScoped();
services.TryAddSingleton();
// Endpoint handlers
- services.AddScoped>();
+ services.TryAddScoped>();
services.TryAddScoped();
- services.AddScoped();
+ services.TryAddScoped();
services.TryAddScoped();
- services.AddScoped>();
+ services.TryAddScoped>();
services.TryAddScoped();
- services.AddScoped();
+ services.TryAddScoped();
services.TryAddScoped();
- services.AddScoped>();
+ services.TryAddScoped>();
services.TryAddScoped();
//services.TryAddScoped();
//services.TryAddScoped();
//services.TryAddScoped();
+ services.ConfigureHttpJsonOptions(o =>
+ {
+ o.SerializerOptions.Converters.Add(new UserKeyJsonConverter());
+ });
return services;
}
+ // =========================
+ // USERS (FRAMEWORK-REQUIRED)
+ // =========================
+ internal static IServiceCollection AddUsersInternal(IServiceCollection services)
+ {
+ // Core user abstractions
+ services.TryAddScoped(typeof(IUserAccessor), typeof(UAuthUserAccessor));
+ services.TryAddScoped();
+
+ // Security state
+ //services.TryAddScoped(typeof(IUserSecurityEvents<>), typeof(DefaultUserSecurityEvents<>));
+
+ // TODO: Move this into AddAuthorizaionInternal method
+ services.TryAddScoped(typeof(IUserClaimsProvider), typeof(DefaultAuthorizationClaimsProvider));
+
+ return services;
+ }
+
+ // =========================
+ // CREDENTIALS (FRAMEWORK-REQUIRED)
+ // =========================
+ internal static IServiceCollection AddCredentialsInternal(IServiceCollection services)
+ {
+ services.TryAddScoped();
+ return services;
+ }
+
+
public static IServiceCollection AddUAuthServerInfrastructure(this IServiceCollection services)
{
// Flow orchestration
@@ -243,9 +292,6 @@ public static IServiceCollection AddUAuthServerInfrastructure(this IServiceColle
services.TryAddScoped(typeof(ISessionIssuer), typeof(UAuthSessionIssuer));
services.TryAddScoped(typeof(ITokenIssuer), typeof(UAuthTokenIssuer));
- // User service
- services.TryAddScoped(typeof(IUAuthUserService<>), typeof(UAuthUserService<>));
-
// Endpoints
services.TryAddSingleton();
diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Auth/DefaultAuthContextFactory.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Auth/DefaultAuthContextFactory.cs
new file mode 100644
index 0000000..00dde0d
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Auth/DefaultAuthContextFactory.cs
@@ -0,0 +1,25 @@
+using CodeBeam.UltimateAuth.Core.Abstractions;
+using CodeBeam.UltimateAuth.Core.Contracts;
+using CodeBeam.UltimateAuth.Server.Auth;
+using CodeBeam.UltimateAuth.Server.Extensions;
+
+namespace CodeBeam.UltimateAuth.Server.Infrastructure.Auth
+{
+ internal sealed class DefaultAuthContextFactory : IAuthContextFactory
+ {
+ private readonly IAuthFlowContextAccessor _flow;
+ private readonly IClock _clock;
+
+ public DefaultAuthContextFactory(IAuthFlowContextAccessor flow, IClock clock)
+ {
+ _flow = flow;
+ _clock = clock;
+ }
+
+ public AuthContext Create(DateTimeOffset? at = null)
+ {
+ var flow = _flow.Current;
+ return flow.ToAuthContext(at ?? _clock.UtcNow);
+ }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/DefaultCredentialResponseWriter.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/DefaultCredentialResponseWriter.cs
similarity index 100%
rename from src/CodeBeam.UltimateAuth.Server/Infrastructure/DefaultCredentialResponseWriter.cs
rename to src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/DefaultCredentialResponseWriter.cs
diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/DefaultPrimaryCredentialResolver.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/DefaultPrimaryCredentialResolver.cs
similarity index 100%
rename from src/CodeBeam.UltimateAuth.Server/Infrastructure/DefaultPrimaryCredentialResolver.cs
rename to src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/DefaultPrimaryCredentialResolver.cs
diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/DefaultUserAuthenticator.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/DefaultUserAuthenticator.cs
deleted file mode 100644
index a2e7b7a..0000000
--- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/DefaultUserAuthenticator.cs
+++ /dev/null
@@ -1,50 +0,0 @@
-using CodeBeam.UltimateAuth.Core.Abstractions;
-using CodeBeam.UltimateAuth.Core.Contracts;
-using CodeBeam.UltimateAuth.Core.Domain;
-using System.Security.Claims;
-
-namespace CodeBeam.UltimateAuth.Server.Infrastructure
-{
- public sealed class DefaultUserAuthenticator : IUserAuthenticator
- {
- private readonly IUAuthUserStore _userStore;
- private readonly IUAuthPasswordHasher _passwordHasher;
-
- public DefaultUserAuthenticator(IUAuthUserStore userStore, IUAuthPasswordHasher passwordHasher)
- {
- _userStore = userStore;
- _passwordHasher = passwordHasher;
- }
-
- public async Task> AuthenticateAsync(string? tenantId, AuthenticationContext context, CancellationToken ct = default)
- {
- if (context is null)
- throw new ArgumentNullException(nameof(context));
-
- if (!string.Equals(context.CredentialType, "password", StringComparison.Ordinal))
- return UserAuthenticationResult.Fail();
-
- var user = await _userStore.FindByUsernameAsync(tenantId, context.Identifier, ct);
-
- if (user is null || !user.IsActive)
- return UserAuthenticationResult.Fail();
-
- if (!_passwordHasher.Verify(context.Secret, user.PasswordHash))
- return UserAuthenticationResult.Fail();
-
- var claims = (user.Claims ?? ClaimsSnapshot.Empty)
- .With(
- (ClaimTypes.NameIdentifier, user.Id.ToString()!),
- (ClaimTypes.Name, user.Username),
- ("uauth:username", user.Username)
- );
-
- return UserAuthenticationResult.Success(
- user.Id,
- claims,
- user.RequiresMfa
- );
-
- }
- }
-}
diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/DefaultAccessAuthority.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/DefaultAccessAuthority.cs
new file mode 100644
index 0000000..3f4f539
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/DefaultAccessAuthority.cs
@@ -0,0 +1,60 @@
+using CodeBeam.UltimateAuth.Core.Abstractions;
+using CodeBeam.UltimateAuth.Core.Contracts;
+
+namespace CodeBeam.UltimateAuth.Server.Infrastructure
+{
+ public sealed class DefaultAccessAuthority : IAccessAuthority
+ {
+ private readonly IEnumerable _invariants;
+ private readonly IEnumerable _globalPolicies;
+
+ public DefaultAccessAuthority(IEnumerable invariants, IEnumerable globalPolicies)
+ {
+ _invariants = invariants ?? Array.Empty();
+ _globalPolicies = globalPolicies ?? Array.Empty();
+ }
+
+ public AccessDecision Decide(AccessContext context, IEnumerable runtimePolicies)
+ {
+ foreach (var invariant in _invariants)
+ {
+ var result = invariant.Decide(context);
+ if (!result.IsAllowed)
+ return result;
+ }
+
+ foreach (var policy in _globalPolicies)
+ {
+ if (!policy.AppliesTo(context))
+ continue;
+
+ var result = policy.Decide(context);
+
+ if (!result.IsAllowed)
+ return result;
+
+ // Allow here means "no objection", NOT permission
+ }
+
+ bool requiresReauth = false;
+
+ foreach (var policy in runtimePolicies)
+ {
+ if (!policy.AppliesTo(context))
+ continue;
+
+ var result = policy.Decide(context);
+
+ if (!result.IsAllowed)
+ return result;
+
+ if (result.RequiresReauthentication)
+ requiresReauth = true;
+ }
+
+ return requiresReauth
+ ? AccessDecision.ReauthenticationRequired()
+ : AccessDecision.Allow();
+ }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/IAccessCommand.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/IAccessCommand.cs
new file mode 100644
index 0000000..f72a257
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/IAccessCommand.cs
@@ -0,0 +1,12 @@
+using CodeBeam.UltimateAuth.Core.Abstractions;
+using CodeBeam.UltimateAuth.Core.Contracts;
+
+namespace CodeBeam.UltimateAuth.Server.Infrastructure
+{
+ public interface IAccessCommand
+ {
+ IEnumerable GetPolicies(AccessContext context);
+ Task ExecuteAsync(CancellationToken ct = default);
+ }
+
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/IAccessOrchestrator.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/IAccessOrchestrator.cs
new file mode 100644
index 0000000..337bf3e
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/IAccessOrchestrator.cs
@@ -0,0 +1,9 @@
+using CodeBeam.UltimateAuth.Core.Contracts;
+
+namespace CodeBeam.UltimateAuth.Server.Infrastructure
+{
+ public interface IAccessOrchestrator
+ {
+ Task ExecuteAsync(AccessContext context, IAccessCommand command, CancellationToken ct = default);
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeAllSessionsCommand.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeAllSessionsCommand.cs
new file mode 100644
index 0000000..cdffd57
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeAllSessionsCommand.cs
@@ -0,0 +1,24 @@
+using CodeBeam.UltimateAuth.Core.Abstractions;
+using CodeBeam.UltimateAuth.Core.Contracts;
+using CodeBeam.UltimateAuth.Core.Domain;
+
+namespace CodeBeam.UltimateAuth.Server.Infrastructure
+{
+ public sealed class RevokeAllUserSessionsCommand : ISessionCommand
+ {
+ public UserKey UserKey { get; }
+
+ public RevokeAllUserSessionsCommand(UserKey userKey)
+ {
+ UserKey = userKey;
+ }
+
+ // TODO: This method should call its own logic. Not revoke root.
+ public async Task ExecuteAsync(AuthContext context, ISessionIssuer issuer, CancellationToken ct)
+ {
+ await issuer.RevokeRootAsync(context.TenantId, UserKey, context.At, ct);
+ return Unit.Value;
+ }
+
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeRootCommand.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeRootCommand.cs
index 4f0d752..a4f272a 100644
--- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeRootCommand.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/RevokeRootCommand.cs
@@ -13,17 +13,9 @@ public RevokeRootCommand(UserKey userKey)
UserKey = userKey;
}
- public async Task ExecuteAsync(
- AuthContext context,
- ISessionIssuer issuer,
- CancellationToken ct)
+ public async Task ExecuteAsync(AuthContext context, ISessionIssuer issuer, CancellationToken ct)
{
- await issuer.RevokeRootAsync(
- context.TenantId,
- UserKey,
- context.At,
- ct);
-
+ await issuer.RevokeRootAsync(context.TenantId, UserKey, context.At, ct);
return Unit.Value;
}
}
diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/UAuthAccessOrchestrator.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/UAuthAccessOrchestrator.cs
new file mode 100644
index 0000000..5b7920b
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/UAuthAccessOrchestrator.cs
@@ -0,0 +1,36 @@
+using CodeBeam.UltimateAuth.Core.Abstractions;
+using CodeBeam.UltimateAuth.Core.Contracts;
+using CodeBeam.UltimateAuth.Core.Errors;
+
+namespace CodeBeam.UltimateAuth.Server.Infrastructure
+{
+ public sealed class UAuthAccessOrchestrator : IAccessOrchestrator
+ {
+ private readonly IAccessAuthority _authority;
+ private bool _executed;
+
+ public UAuthAccessOrchestrator(IAccessAuthority authority)
+ {
+ _authority = authority;
+ }
+
+ public async Task ExecuteAsync(AccessContext context, IAccessCommand command, CancellationToken ct = default)
+ {
+ if (_executed)
+ throw new InvalidOperationException("Access orchestrator can only be executed once.");
+
+ _executed = true;
+
+ var policies = command.GetPolicies(context) ?? Array.Empty();
+ var decision = _authority.Decide(context, policies);
+
+ if (!decision.IsAllowed)
+ throw new UAuthAuthorizationException(decision.DenyReason);
+
+ if (decision.RequiresReauthentication)
+ throw new InvalidOperationException("Requires reuthenticate.");
+
+ await command.ExecuteAsync(ct);
+ }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/IInnerSessionIdResolver.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/SessionId/IInnerSessionIdResolver.cs
similarity index 100%
rename from src/CodeBeam.UltimateAuth.Server/Infrastructure/IInnerSessionIdResolver.cs
rename to src/CodeBeam.UltimateAuth.Server/Infrastructure/SessionId/IInnerSessionIdResolver.cs
diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/ISessionIdResolver.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/SessionId/ISessionIdResolver.cs
similarity index 100%
rename from src/CodeBeam.UltimateAuth.Server/Infrastructure/ISessionIdResolver.cs
rename to src/CodeBeam.UltimateAuth.Server/Infrastructure/SessionId/ISessionIdResolver.cs
diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/User/HttpContextCurrentUser.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/User/HttpContextCurrentUser.cs
new file mode 100644
index 0000000..dcd8c17
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/User/HttpContextCurrentUser.cs
@@ -0,0 +1,25 @@
+using CodeBeam.UltimateAuth.Core;
+using CodeBeam.UltimateAuth.Core.Contracts;
+using CodeBeam.UltimateAuth.Core.Domain;
+using CodeBeam.UltimateAuth.Server.Middlewares;
+using Microsoft.AspNetCore.Http;
+
+namespace CodeBeam.UltimateAuth.Server.Infrastructure
+{
+ internal sealed class HttpContextCurrentUser : ICurrentUser
+ {
+ private readonly IHttpContextAccessor _http;
+
+ public HttpContextCurrentUser(IHttpContextAccessor http)
+ {
+ _http = http;
+ }
+
+ public bool IsAuthenticated => Snapshot?.IsAuthenticated == true;
+
+ public UserKey UserKey => Snapshot?.UserId ?? throw new InvalidOperationException("Current user is not authenticated.");
+
+ private AuthUserSnapshot? Snapshot => _http.HttpContext?.Items[UserMiddleware.UserContextKey] as AuthUserSnapshot;
+ }
+
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/IUserAccessor.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/User/IUserAccessor.cs
similarity index 100%
rename from src/CodeBeam.UltimateAuth.Server/Infrastructure/IUserAccessor.cs
rename to src/CodeBeam.UltimateAuth.Server/Infrastructure/User/IUserAccessor.cs
diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/UAuthUserAccessor.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/User/UAuthUserAccessor.cs
similarity index 100%
rename from src/CodeBeam.UltimateAuth.Server/Infrastructure/UAuthUserAccessor.cs
rename to src/CodeBeam.UltimateAuth.Server/Infrastructure/User/UAuthUserAccessor.cs
diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/UAuthUserId.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/User/UAuthUserId.cs
similarity index 100%
rename from src/CodeBeam.UltimateAuth.Server/Infrastructure/UAuthUserId.cs
rename to src/CodeBeam.UltimateAuth.Server/Infrastructure/User/UAuthUserId.cs
diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/UserAccessorBridge.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/User/UserAccessorBridge.cs
similarity index 100%
rename from src/CodeBeam.UltimateAuth.Server/Infrastructure/UserAccessorBridge.cs
rename to src/CodeBeam.UltimateAuth.Server/Infrastructure/User/UserAccessorBridge.cs
diff --git a/src/CodeBeam.UltimateAuth.Server/Login/DefaultLoginAuthority.cs b/src/CodeBeam.UltimateAuth.Server/Login/DefaultLoginAuthority.cs
new file mode 100644
index 0000000..fd02425
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Login/DefaultLoginAuthority.cs
@@ -0,0 +1,42 @@
+namespace CodeBeam.UltimateAuth.Server.Login
+{
+ ///
+ /// Default implementation of the login authority.
+ /// Applies basic security checks for login attempts.
+ ///
+ public sealed class DefaultLoginAuthority : ILoginAuthority
+ {
+ public LoginDecision Decide(LoginDecisionContext context)
+ {
+ // 1. Credentials must be valid
+ if (!context.CredentialsValid)
+ {
+ return LoginDecision.Deny("Invalid credentials.");
+ }
+
+ // 2. User must exist
+ if (!context.UserExists || context.UserKey is null)
+ {
+ // Deliberately vague to prevent user enumeration
+ return LoginDecision.Deny("Invalid credentials.");
+ }
+
+ // 3. User must be active and not locked
+ if (context.SecurityState is not null)
+ {
+ if (context.SecurityState.IsLocked)
+ {
+ return LoginDecision.Deny("User account is locked.");
+ }
+
+ if (context.SecurityState.RequiresReauthentication)
+ {
+ return LoginDecision.Challenge("Reauthentication required.");
+ }
+ }
+
+ // 4. All checks passed
+ return LoginDecision.Allow();
+ }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Login/DefaultLoginOrchestrator.cs b/src/CodeBeam.UltimateAuth.Server/Login/DefaultLoginOrchestrator.cs
new file mode 100644
index 0000000..f878641
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Login/DefaultLoginOrchestrator.cs
@@ -0,0 +1,168 @@
+using CodeBeam.UltimateAuth.Core.Abstractions;
+using CodeBeam.UltimateAuth.Core.Contracts;
+using CodeBeam.UltimateAuth.Core.Domain;
+using CodeBeam.UltimateAuth.Core.Options;
+using CodeBeam.UltimateAuth.Server.Abstactions;
+using CodeBeam.UltimateAuth.Server.Auth;
+using CodeBeam.UltimateAuth.Credentials;
+using CodeBeam.UltimateAuth.Server.Extensions;
+using CodeBeam.UltimateAuth.Server.Infrastructure;
+using CodeBeam.UltimateAuth.Users;
+
+using CodeBeam.UltimateAuth.Core;
+
+namespace CodeBeam.UltimateAuth.Server.Login.Orchestrators
+{
+ internal sealed class DefaultLoginOrchestrator : ILoginOrchestrator
+ {
+ private readonly ICredentialStore _credentialStore;
+ private readonly ICredentialValidator _credentialValidator;
+ private readonly IUserStore _users;
+ private readonly IUserSecurityStateProvider _userSecurityStateProvider;
+ private readonly ILoginAuthority _authority;
+ private readonly ISessionOrchestrator _sessionOrchestrator;
+ private readonly ITokenIssuer _tokens;
+ private readonly IUserClaimsProvider _claimsProvider;
+ private readonly IUserIdConverterResolver _userIdConverterResolver;
+
+ public DefaultLoginOrchestrator(
+ ICredentialStore credentialStore,
+ ICredentialValidator credentialValidator,
+ IUserStore users,
+ IUserSecurityStateProvider userSecurityStateProvider,
+ ILoginAuthority authority,
+ ISessionOrchestrator sessionOrchestrator,
+ ITokenIssuer tokens,
+ IUserClaimsProvider claimsProvider,
+ IUserIdConverterResolver userIdConverterResolver)
+ {
+ _credentialStore = credentialStore;
+ _credentialValidator = credentialValidator;
+ _users = users;
+ _userSecurityStateProvider = userSecurityStateProvider;
+ _authority = authority;
+ _sessionOrchestrator = sessionOrchestrator;
+ _tokens = tokens;
+ _claimsProvider = claimsProvider;
+ _userIdConverterResolver = userIdConverterResolver;
+ }
+
+ public async Task LoginAsync(AuthFlowContext flow, LoginRequest request, CancellationToken ct = default)
+ {
+ var now = request.At ?? DateTimeOffset.UtcNow;
+
+ // 1. Validate credentials (Credentials domain)
+ var credential = await _credentialStore.FindByLoginAsync(request.TenantId, request.Identifier, ct);
+ bool credentialsValid = false;
+ TUserId? userId = default;
+
+ if (credential is not null)
+ {
+ var credentialValidationResult = await _credentialValidator.ValidateAsync(credential, request.Secret, ct);
+ credentialsValid = credentialValidationResult.IsValid;
+ userId = credential.UserId;
+ }
+
+ bool userExists = userId is not null;
+
+ // 2. Resolve user security state (Users domain)
+ IUserSecurityState? securityState = null;
+ UserKey? userKey = null;
+
+ if (userExists)
+ {
+ securityState = await _userSecurityStateProvider.GetAsync(request.TenantId, userId!, ct);
+ var converter = _userIdConverterResolver.GetConverter();
+ userKey = UserKey.FromString(converter.ToString(userId!));
+ }
+
+ // 3. Authority decision (Login domain)
+ var decisionContext = new LoginDecisionContext
+ {
+ TenantId = request.TenantId,
+ Identifier = request.Identifier,
+ CredentialsValid = credentialsValid,
+ UserExists = userExists,
+ UserKey = userKey,
+ SecurityState = securityState,
+ IsChained = request.ChainId is not null
+ };
+
+ var decision = _authority.Decide(decisionContext);
+
+ switch (decision.Kind)
+ {
+ case LoginDecisionKind.Deny:
+ return LoginResult.Failed();
+
+ case LoginDecisionKind.Challenge:
+ {
+ // Orchestrator decides HOW to continue
+ var continuation = new LoginContinuation
+ {
+ Type = LoginContinuationType.Mfa,
+ // TODO: Add here
+ //ContinuationToken = _continuationTokenService.Create(
+ // request.TenantId,
+ // userKey!,
+ // request.ChainId),
+ Hint = decision.Reason
+ };
+
+ return LoginResult.Continue(continuation);
+ }
+
+ case LoginDecisionKind.Allow:
+ break;
+ }
+
+ if (userKey is not UserKey validUserKey)
+ {
+ return LoginResult.Failed();
+ }
+
+ var claims = await _claimsProvider.GetClaimsAsync(request.TenantId, validUserKey, ct);
+
+ // 4. Create authenticated session
+ var sessionContext = new AuthenticatedSessionContext
+ {
+ TenantId = request.TenantId,
+ UserKey = validUserKey,
+ Now = now,
+ Device = request.Device,
+ Claims = claims,
+ ChainId = request.ChainId,
+ Metadata = SessionMetadata.Empty
+ };
+
+ var authContext = flow.ToAuthContext(now);
+ var issuedSession = await _sessionOrchestrator.ExecuteAsync(authContext, new CreateLoginSessionCommand(sessionContext), ct);
+
+ // 6. Issue tokens if requested
+ AuthTokens? tokens = null;
+
+ if (request.RequestTokens)
+ {
+ var tokenContext = new TokenIssuanceContext
+ {
+ TenantId = request.TenantId,
+ UserKey = validUserKey,
+ SessionId = issuedSession.Session.SessionId,
+ ChainId = request.ChainId,
+ Claims = claims.AsDictionary()
+ };
+
+ var access = await _tokens.IssueAccessTokenAsync(flow, tokenContext, ct);
+ var refresh = await _tokens.IssueRefreshTokenAsync(flow, tokenContext, RefreshTokenPersistence.Persist, ct);
+
+ tokens = new AuthTokens
+ {
+ AccessToken = access,
+ RefreshToken = refresh
+ };
+ }
+
+ return LoginResult.Success(issuedSession.Session.SessionId, tokens);
+ }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Login/ILoginAuthority.cs b/src/CodeBeam.UltimateAuth.Server/Login/ILoginAuthority.cs
new file mode 100644
index 0000000..d05fac6
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Login/ILoginAuthority.cs
@@ -0,0 +1,17 @@
+namespace CodeBeam.UltimateAuth.Server.Login
+{
+ ///
+ /// Represents the authority responsible for making login decisions.
+ /// This authority determines whether a login attempt is allowed,
+ /// denied, or requires additional verification (e.g. MFA).
+ ///
+ public interface ILoginAuthority
+ {
+ ///
+ /// Evaluates a login attempt based on the provided decision context.
+ ///
+ /// The login decision context.
+ /// The login decision.
+ LoginDecision Decide(LoginDecisionContext context);
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Login/ILoginOrchestrator.cs b/src/CodeBeam.UltimateAuth.Server/Login/ILoginOrchestrator.cs
new file mode 100644
index 0000000..d22d605
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Login/ILoginOrchestrator.cs
@@ -0,0 +1,16 @@
+using CodeBeam.UltimateAuth.Core.Contracts;
+using CodeBeam.UltimateAuth.Server.Auth;
+
+namespace CodeBeam.UltimateAuth.Server.Login
+{
+ ///
+ /// Orchestrates the login flow.
+ /// Responsible for executing the login process by coordinating
+ /// credential validation, user resolution, authority decision,
+ /// and session creation.
+ ///
+ public interface ILoginOrchestrator
+ {
+ Task LoginAsync(AuthFlowContext flow, LoginRequest request, CancellationToken ct = default);
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Login/LoginDecision.cs b/src/CodeBeam.UltimateAuth.Server/Login/LoginDecision.cs
new file mode 100644
index 0000000..625d311
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Login/LoginDecision.cs
@@ -0,0 +1,26 @@
+namespace CodeBeam.UltimateAuth.Server.Login
+{
+ ///
+ /// Represents the outcome of a login decision.
+ ///
+ public sealed class LoginDecision
+ {
+ public LoginDecisionKind Kind { get; }
+ public string? Reason { get; }
+
+ private LoginDecision(LoginDecisionKind kind, string? reason = null)
+ {
+ Kind = kind;
+ Reason = reason;
+ }
+
+ public static LoginDecision Allow()
+ => new(LoginDecisionKind.Allow);
+
+ public static LoginDecision Deny(string reason)
+ => new(LoginDecisionKind.Deny, reason);
+
+ public static LoginDecision Challenge(string reason)
+ => new(LoginDecisionKind.Challenge, reason);
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Login/LoginDecisionContext.cs b/src/CodeBeam.UltimateAuth.Server/Login/LoginDecisionContext.cs
new file mode 100644
index 0000000..695d19d
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Login/LoginDecisionContext.cs
@@ -0,0 +1,50 @@
+using CodeBeam.UltimateAuth.Core.Domain;
+using CodeBeam.UltimateAuth.Users;
+
+namespace CodeBeam.UltimateAuth.Server.Login
+{
+ ///
+ /// Represents all information required by the login authority
+ /// to make a login decision.
+ ///
+ public sealed class LoginDecisionContext
+ {
+ ///
+ /// Gets the tenant identifier.
+ ///
+ public string? TenantId { get; init; }
+
+ ///
+ /// Gets the login identifier (e.g. username or email).
+ ///
+ public required string Identifier { get; init; }
+
+ ///
+ /// Indicates whether the provided credentials were successfully validated.
+ ///
+ public bool CredentialsValid { get; init; }
+
+ ///
+ /// Gets the resolved user identifier if available.
+ ///
+ public UserKey? UserKey { get; init; }
+
+ ///
+ /// Gets the user security state if the user could be resolved.
+ ///
+ public IUserSecurityState? SecurityState { get; init; }
+
+ ///
+ /// Indicates whether the user exists.
+ /// This allows the authority to distinguish between
+ /// invalid credentials and non-existent users.
+ ///
+ public bool UserExists { get; init; }
+
+ ///
+ /// Indicates whether this login attempt is part of a chained flow
+ /// (e.g. reauthentication, MFA completion).
+ ///
+ public bool IsChained { get; init; }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Login/LoginDecisionKind.cs b/src/CodeBeam.UltimateAuth.Server/Login/LoginDecisionKind.cs
new file mode 100644
index 0000000..c1086d0
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Login/LoginDecisionKind.cs
@@ -0,0 +1,9 @@
+namespace CodeBeam.UltimateAuth.Server.Login
+{
+ public enum LoginDecisionKind
+ {
+ Allow = 1,
+ Deny = 2,
+ Challenge = 3
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Options/UAuthServerOptions.cs b/src/CodeBeam.UltimateAuth.Server/Options/UAuthServerOptions.cs
index 1388620..6e3da9d 100644
--- a/src/CodeBeam.UltimateAuth.Server/Options/UAuthServerOptions.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Options/UAuthServerOptions.cs
@@ -108,6 +108,14 @@ public void ReplaceSessionCookieManager() where T : class, IUAuthCookieManage
public bool? EnableSessionEndpoints { get; set; } = true;
public bool? EnableUserInfoEndpoints { get; set; } = true;
+ public bool EnableUserLifecycleEndpoints { get; set; } = true;
+ public bool EnableUserProfileEndpoints { get; set; } = true;
+ public bool EnableAdminChangeUserProfileEndpoints { get; set; } = false;
+ public bool EnableCredentialsEndpoints { get; set; } = true;
+ public bool EnableAuthorizationEndpoints { get; set; } = true;
+
+ public UserIdentifierOptions UserIdentifiers { get; set; } = new();
+
///
/// If true, server will add anti-forgery headers
/// and require proper request metadata.
@@ -137,7 +145,7 @@ public void ReplaceSessionCookieManager() where T : class, IUAuthCookieManage
public Action? ConfigureServices { get; set; }
- internal Dictionary> ModeConfigurations { get; } = new();
+ internal Dictionary> ModeConfigurations { get; set; } = new();
internal UAuthServerOptions Clone()
@@ -159,16 +167,23 @@ internal UAuthServerOptions Clone()
AuthResponse = AuthResponse.Clone(),
Hub = Hub.Clone(),
SessionResolution = SessionResolution.Clone(),
+ UserIdentifiers = UserIdentifiers.Clone(),
EnableLoginEndpoints = EnableLoginEndpoints,
EnablePkceEndpoints = EnablePkceEndpoints,
EnableTokenEndpoints = EnableTokenEndpoints,
EnableSessionEndpoints = EnableSessionEndpoints,
EnableUserInfoEndpoints = EnableUserInfoEndpoints,
+ EnableUserLifecycleEndpoints = EnableUserLifecycleEndpoints,
+ EnableUserProfileEndpoints = EnableUserProfileEndpoints,
+ EnableAdminChangeUserProfileEndpoints = EnableAdminChangeUserProfileEndpoints,
+ EnableCredentialsEndpoints = EnableCredentialsEndpoints,
+ EnableAuthorizationEndpoints = EnableAuthorizationEndpoints,
EnableAntiCsrfProtection = EnableAntiCsrfProtection,
EnableLoginRateLimiting = EnableLoginRateLimiting,
+ ModeConfigurations = ModeConfigurations,
OnConfigureEndpoints = OnConfigureEndpoints,
ConfigureServices = ConfigureServices,
CustomCookieManagerType = CustomCookieManagerType
diff --git a/src/CodeBeam.UltimateAuth.Server/Options/UserIdentifierOptions.cs b/src/CodeBeam.UltimateAuth.Server/Options/UserIdentifierOptions.cs
new file mode 100644
index 0000000..0f63cc9
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Options/UserIdentifierOptions.cs
@@ -0,0 +1,29 @@
+namespace CodeBeam.UltimateAuth.Server.Options
+{
+ public sealed class UserIdentifierOptions
+ {
+ public bool AllowUsernameChange { get; set; } = true;
+ public bool AllowMultipleUsernames { get; set; } = false;
+ public bool AllowMultipleEmail { get; set; } = true;
+ public bool AllowMultiplePhone { get; set; } = true;
+
+ public bool RequireEmailVerification { get; set; } = false;
+ public bool RequirePhoneVerification { get; set; } = false;
+
+ public bool AllowAdminOverride { get; set; } = true;
+ public bool AllowUserOverride { get; set; } = true;
+
+ internal UserIdentifierOptions Clone() => new()
+ {
+ AllowUsernameChange = AllowUsernameChange,
+ AllowMultipleUsernames = AllowMultipleUsernames,
+ AllowMultipleEmail = AllowMultipleEmail,
+ AllowMultiplePhone = AllowMultiplePhone,
+ RequireEmailVerification = RequireEmailVerification,
+ RequirePhoneVerification = RequirePhoneVerification,
+ AllowAdminOverride = AllowAdminOverride,
+ AllowUserOverride = AllowUserOverride
+ };
+ }
+
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Services/DefaultSessionService.cs b/src/CodeBeam.UltimateAuth.Server/Services/DefaultSessionService.cs
new file mode 100644
index 0000000..7e444b2
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Services/DefaultSessionService.cs
@@ -0,0 +1,35 @@
+using CodeBeam.UltimateAuth.Core.Abstractions;
+using CodeBeam.UltimateAuth.Core.Contracts;
+using CodeBeam.UltimateAuth.Core.Domain;
+using CodeBeam.UltimateAuth.Server.Infrastructure;
+using CodeBeam.UltimateAuth.Server.Infrastructure.Orchestrator;
+
+namespace CodeBeam.UltimateAuth.Server.Services
+{
+ internal sealed class DefaultSessionService : ISessionService
+ {
+ private readonly ISessionOrchestrator _orchestrator;
+ private readonly IClock _clock;
+
+ public DefaultSessionService(ISessionOrchestrator orchestrator, IClock clock)
+ {
+ _orchestrator = orchestrator;
+ _clock = clock;
+ }
+
+ public Task RevokeAllAsync(AuthContext authContext, UserKey userKey, CancellationToken ct)
+ {
+ return _orchestrator.ExecuteAsync(authContext, new RevokeAllUserSessionsCommand(userKey), ct);
+ }
+
+ public Task RevokeAllExceptChainAsync(AuthContext authContext, UserKey userKey, SessionChainId exceptChainId, CancellationToken ct)
+ {
+ return _orchestrator.ExecuteAsync(authContext, new RevokeAllChainsCommand(userKey, exceptChainId), ct);
+ }
+
+ public Task RevokeRootAsync(AuthContext authContext, UserKey userKey, CancellationToken ct)
+ {
+ return _orchestrator.ExecuteAsync(authContext, new RevokeRootCommand(userKey), ct);
+ }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/ISessionQueryService.cs b/src/CodeBeam.UltimateAuth.Server/Services/ISessionQueryService.cs
similarity index 93%
rename from src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/ISessionQueryService.cs
rename to src/CodeBeam.UltimateAuth.Server/Services/ISessionQueryService.cs
index 64a9773..fa67421 100644
--- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/ISessionQueryService.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Services/ISessionQueryService.cs
@@ -1,7 +1,7 @@
using CodeBeam.UltimateAuth.Core.Contracts;
using CodeBeam.UltimateAuth.Core.Domain;
-namespace CodeBeam.UltimateAuth.Server.Infrastructure
+namespace CodeBeam.UltimateAuth.Server.Services
{
public interface ISessionQueryService
{
diff --git a/src/CodeBeam.UltimateAuth.Server/Services/UAuthFlowService.cs b/src/CodeBeam.UltimateAuth.Server/Services/UAuthFlowService.cs
index 833c7b1..6864311 100644
--- a/src/CodeBeam.UltimateAuth.Server/Services/UAuthFlowService.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Services/UAuthFlowService.cs
@@ -6,13 +6,15 @@
using CodeBeam.UltimateAuth.Server.Auth;
using CodeBeam.UltimateAuth.Server.Extensions;
using CodeBeam.UltimateAuth.Server.Infrastructure;
+using CodeBeam.UltimateAuth.Server.Login;
+using System;
namespace CodeBeam.UltimateAuth.Server.Services
{
internal sealed class UAuthFlowService : IUAuthFlowService
{
private readonly IAuthFlowContextAccessor _authFlow;
- private readonly IUAuthUserService _users;
+ private readonly ILoginOrchestrator _loginOrchestrator;
private readonly ISessionOrchestrator _orchestrator;
private readonly ISessionQueryService _queries;
private readonly ITokenIssuer _tokens;
@@ -21,7 +23,7 @@ internal sealed class UAuthFlowService : IUAuthFlowService
public UAuthFlowService(
IAuthFlowContextAccessor authFlow,
- IUAuthUserService users,
+ ILoginOrchestrator loginOrchestrator,
ISessionOrchestrator orchestrator,
ISessionQueryService queries,
ITokenIssuer tokens,
@@ -29,7 +31,7 @@ public UAuthFlowService(
IRefreshTokenValidator tokenValidator)
{
_authFlow = authFlow;
- _users = users;
+ _loginOrchestrator = loginOrchestrator;
_orchestrator = orchestrator;
_queries = queries;
_tokens = tokens;
@@ -52,112 +54,17 @@ public Task ExternalLoginAsync(ExternalLoginRequest request, Cancel
throw new NotImplementedException();
}
- public async Task LoginAsync(AuthFlowContext flow, LoginRequest request, CancellationToken ct = default)
+ public Task LoginAsync(AuthFlowContext flow, LoginRequest request, CancellationToken ct = default)
{
- var now = request.At ?? DateTimeOffset.UtcNow;
-
- var auth = await _users.AuthenticateAsync(request.TenantId, request.Identifier, request.Secret, ct);
-
- if (!auth.Succeeded)
- return LoginResult.Failed();
-
- var converter = _userIdConverterResolver.GetConverter();
- var userKey = UserKey.FromString(converter.ToString(auth.UserId!));
- var sessionContext = new AuthenticatedSessionContext
- {
- TenantId = request.TenantId,
- UserKey = userKey,
- Now = now,
- Device = request.Device,
- Claims = auth.Claims,
- ChainId = request.ChainId,
- Metadata = SessionMetadata.Empty // TODO: Check all SessionMetadata.Empty statements
- };
-
- var authContext = flow.ToAuthContext(now);
- var issuedSession = await _orchestrator.ExecuteAsync(authContext, new CreateLoginSessionCommand(sessionContext), ct);
-
- bool shouldIssueTokens = request.RequestTokens;
-
- AuthTokens? tokens = null;
-
- if (shouldIssueTokens)
- {
- var tokenContext = new TokenIssuanceContext
- {
- TenantId = request.TenantId,
- UserKey = userKey,
- SessionId = issuedSession.Session.SessionId,
- ChainId = request.ChainId,
- Claims = auth.Claims.AsDictionary()
- };
-
- var access = await _tokens.IssueAccessTokenAsync(flow, tokenContext, ct);
- var refresh = await _tokens.IssueRefreshTokenAsync(flow, tokenContext, RefreshTokenPersistence.Persist, ct);
-
- tokens = new AuthTokens { AccessToken = access, RefreshToken = refresh };
- }
-
- return LoginResult.Success(issuedSession.Session.SessionId, tokens);
+ return _loginOrchestrator.LoginAsync(flow, request, ct);
}
- public async Task LoginAsync(AuthFlowContext flow, AuthExecutionContext execution, LoginRequest request, CancellationToken ct = default)
+ public Task LoginAsync(AuthFlowContext flow, AuthExecutionContext execution, LoginRequest request, CancellationToken ct = default)
{
- var now = request.At ?? DateTimeOffset.UtcNow;
-
- var auth = await _users.AuthenticateAsync(request.TenantId, request.Identifier, request.Secret, ct);
-
- if (!auth.Succeeded)
- return LoginResult.Failed();
-
- var converter = _userIdConverterResolver.GetConverter();
- var userKey = UserKey.FromString(converter.ToString(auth.UserId!));
- var sessionContext = new AuthenticatedSessionContext
- {
- TenantId = request.TenantId,
- UserKey = userKey,
- Now = now,
- Device = request.Device,
- Claims = auth.Claims,
- ChainId = request.ChainId,
- Metadata = SessionMetadata.Empty
- };
-
- var authContext = flow.ToAuthContext(now);
- var issuedSession = await _orchestrator.ExecuteAsync(authContext, new CreateLoginSessionCommand(sessionContext), ct);
-
- bool shouldIssueTokens = request.RequestTokens;
-
- AuthTokens? tokens = null;
-
- if (shouldIssueTokens)
- {
- var tokenContext = new TokenIssuanceContext
- {
- TenantId = request.TenantId,
- UserKey = userKey,
- SessionId = issuedSession.Session.SessionId,
- ChainId = request.ChainId,
- Claims = auth.Claims.AsDictionary()
- };
-
-
- var effectiveFlow = execution.EffectiveClientProfile is null
- ? flow
- : flow.WithClientProfile((UAuthClientProfile)execution.EffectiveClientProfile);
-
- var access = await _tokens.IssueAccessTokenAsync(effectiveFlow, tokenContext, ct);
-
- var refresh = await _tokens.IssueRefreshTokenAsync(effectiveFlow, tokenContext, RefreshTokenPersistence.Persist, ct);
-
- tokens = new AuthTokens
- {
- AccessToken = access,
- RefreshToken = refresh
- };
- }
-
- return LoginResult.Success(issuedSession.Session.SessionId, tokens);
+ var effectiveFlow = execution.EffectiveClientProfile is null
+ ? flow
+ : flow.WithClientProfile((UAuthClientProfile)execution.EffectiveClientProfile);
+ return _loginOrchestrator.LoginAsync(effectiveFlow, request, ct);
}
public Task LogoutAsync(LogoutRequest request, CancellationToken ct = default)
diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/UAuthSessionQueryService.cs b/src/CodeBeam.UltimateAuth.Server/Services/UAuthSessionQueryService.cs
similarity index 86%
rename from src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/UAuthSessionQueryService.cs
rename to src/CodeBeam.UltimateAuth.Server/Services/UAuthSessionQueryService.cs
index 9bd0336..a6736a0 100644
--- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/UAuthSessionQueryService.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Services/UAuthSessionQueryService.cs
@@ -1,19 +1,24 @@
-using CodeBeam.UltimateAuth.Core.Abstractions;
+using CodeBeam.UltimateAuth.Authorization;
+using CodeBeam.UltimateAuth.Core;
+using CodeBeam.UltimateAuth.Core.Abstractions;
using CodeBeam.UltimateAuth.Core.Contracts;
using CodeBeam.UltimateAuth.Core.Domain;
using CodeBeam.UltimateAuth.Server.Options;
using Microsoft.Extensions.Options;
+using System.ComponentModel.DataAnnotations;
-namespace CodeBeam.UltimateAuth.Server.Infrastructure
+namespace CodeBeam.UltimateAuth.Server.Services
{
public sealed class UAuthSessionQueryService : ISessionQueryService
{
private readonly ISessionStoreKernelFactory _storeFactory;
+ private readonly IUserClaimsProvider _claimsProvider;
private readonly UAuthServerOptions _options;
- public UAuthSessionQueryService(ISessionStoreKernelFactory storeFactory, IOptions options)
+ public UAuthSessionQueryService(ISessionStoreKernelFactory storeFactory, IUserClaimsProvider claimsProvider, IOptions options)
{
_storeFactory = storeFactory;
+ _claimsProvider = claimsProvider;
_options = options.Value;
}
@@ -45,7 +50,8 @@ public async Task ValidateSessionAsync(SessionValidatio
//if (!session.Device.Matches(context.Device) && _options.Session.DeviceMismatchBehavior == DeviceMismatchBehavior.Reject)
// return SessionValidationResult.Invalid(SessionState.DeviceMismatch);
- return SessionValidationResult.Active(context.TenantId, session.UserKey, session.SessionId, session.ChainId, root.RootId, session.Claims, boundDeviceId: session.Device.DeviceId);
+ var claims = await _claimsProvider.GetClaimsAsync(context.TenantId, session.UserKey, ct);
+ return SessionValidationResult.Active(context.TenantId, session.UserKey, session.SessionId, session.ChainId, root.RootId, claims, boundDeviceId: session.Device.DeviceId);
}
public Task GetSessionAsync(string? tenantId, AuthSessionId sessionId, CancellationToken ct = default)
diff --git a/src/CodeBeam.UltimateAuth.Server/Services/UAuthUserService.cs b/src/CodeBeam.UltimateAuth.Server/Services/UAuthUserService.cs
deleted file mode 100644
index f76f804..0000000
--- a/src/CodeBeam.UltimateAuth.Server/Services/UAuthUserService.cs
+++ /dev/null
@@ -1,41 +0,0 @@
-using CodeBeam.UltimateAuth.Core.Abstractions;
-using CodeBeam.UltimateAuth.Core.Contracts;
-
-namespace CodeBeam.UltimateAuth.Server.Users
-{
- internal sealed class UAuthUserService : IUAuthUserService
- {
- private readonly IUserAuthenticator _authenticator;
-
- public UAuthUserService(IUserAuthenticator authenticator)
- {
- _authenticator = authenticator;
- }
-
- public async Task> AuthenticateAsync(string? tenantId, string identifier, string secret, CancellationToken ct = default)
- {
- var context = new AuthenticationContext
- {
- Identifier = identifier,
- Secret = secret,
- CredentialType = "password"
- };
-
- return await _authenticator.AuthenticateAsync(tenantId, context, ct);
- }
-
- // This method must not issue sessions or tokens
- public async Task ValidateCredentialsAsync(ValidateCredentialsRequest request, CancellationToken ct = default)
- {
- var context = new AuthenticationContext
- {
- Identifier = request.Identifier,
- Secret = request.Password,
- CredentialType = "password"
- };
-
- var result = await _authenticator.AuthenticateAsync(request.TenantId,context, ct);
- return result.Succeeded;
- }
- }
-}
diff --git a/src/CodeBeam.UltimateAuth.Users/Abstractions/IUAuthUserManagementService.cs b/src/CodeBeam.UltimateAuth.Users/Abstractions/IUAuthUserManagementService.cs
deleted file mode 100644
index b03c400..0000000
--- a/src/CodeBeam.UltimateAuth.Users/Abstractions/IUAuthUserManagementService.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-using CodeBeam.UltimateAuth.Server.Users.Contracts;
-
-namespace CodeBeam.UltimateAuth.Server.Users
-{
- ///
- /// Administrative user management operations.
- ///
- public interface IUAuthUserManagementService
- {
- Task RegisterAsync(RegisterUserRequest request, CancellationToken cancellationToken = default);
-
- Task DeleteAsync(TUserId userId, CancellationToken cancellationToken = default);
-
- Task> GetByIdAsync(
- TUserId userId,
- CancellationToken ct = default);
-
- Task>> GetAllAsync(
- CancellationToken ct = default);
-
- Task DisableAsync(
- TUserId userId,
- CancellationToken ct = default);
-
- Task EnableAsync(
- TUserId userId,
- CancellationToken ct = default);
-
- Task ResetPasswordAsync(
- TUserId userId,
- ResetPasswordRequest request,
- CancellationToken ct = default);
-
- // TODO: Change password, Update user info, etc.
- }
-}
diff --git a/src/CodeBeam.UltimateAuth.Users/Abstractions/IUAuthUserProfileService.cs b/src/CodeBeam.UltimateAuth.Users/Abstractions/IUAuthUserProfileService.cs
deleted file mode 100644
index 68dedea..0000000
--- a/src/CodeBeam.UltimateAuth.Users/Abstractions/IUAuthUserProfileService.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-namespace CodeBeam.UltimateAuth.Server.Users
-{
- ///
- /// User self-service operations (profile, password, MFA).
- ///
- public interface IUAuthUserProfileService
- {
- Task> GetCurrentAsync(
- CancellationToken ct = default);
-
- Task UpdateProfileAsync(
- UpdateProfileRequest request,
- CancellationToken ct = default);
-
- Task ChangePasswordAsync(
- ChangePasswordRequest request,
- CancellationToken ct = default);
-
- Task ConfigureMfaAsync(
- ConfigureMfaRequest request,
- CancellationToken ct = default);
- }
-}
diff --git a/src/CodeBeam.UltimateAuth.Users/Extensions/.gitkeep b/src/CodeBeam.UltimateAuth.Users/Extensions/.gitkeep
deleted file mode 100644
index 5f28270..0000000
--- a/src/CodeBeam.UltimateAuth.Users/Extensions/.gitkeep
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/CodeBeam.UltimateAuth.Users/Middlewares/.gitkeep b/src/CodeBeam.UltimateAuth.Users/Middlewares/.gitkeep
deleted file mode 100644
index 5f28270..0000000
--- a/src/CodeBeam.UltimateAuth.Users/Middlewares/.gitkeep
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/CodeBeam.UltimateAuth.Users/Options/.gitkeep b/src/CodeBeam.UltimateAuth.Users/Options/.gitkeep
deleted file mode 100644
index 5f28270..0000000
--- a/src/CodeBeam.UltimateAuth.Users/Options/.gitkeep
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/CodeBeam.UltimateAuth.Users/Services/.gitkeep b/src/CodeBeam.UltimateAuth.Users/Services/.gitkeep
deleted file mode 100644
index 5f28270..0000000
--- a/src/CodeBeam.UltimateAuth.Users/Services/.gitkeep
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/CodeBeam.UltimateAuth.Users/Users/Models/AdminUserFilter.cs b/src/CodeBeam.UltimateAuth.Users/Users/Models/AdminUserFilter.cs
deleted file mode 100644
index 87cec2b..0000000
--- a/src/CodeBeam.UltimateAuth.Users/Users/Models/AdminUserFilter.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-namespace CodeBeam.UltimateAuth.Server.Users
-{
- public sealed class AdminUserFilter
- {
- public bool? IsActive { get; init; }
- public bool? IsEmailConfirmed { get; init; }
-
- public string? Search { get; init; }
- }
-}
diff --git a/src/CodeBeam.UltimateAuth.Users/Users/Models/ChangePasswordRequest.cs b/src/CodeBeam.UltimateAuth.Users/Users/Models/ChangePasswordRequest.cs
deleted file mode 100644
index b53dfb6..0000000
--- a/src/CodeBeam.UltimateAuth.Users/Users/Models/ChangePasswordRequest.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-namespace CodeBeam.UltimateAuth.Server.Users
-{
- public sealed class ChangePasswordRequest
- {
- public required string CurrentPassword { get; init; }
- public required string NewPassword { get; init; }
-
- ///
- /// If true, other sessions will be revoked.
- ///
- public bool RevokeOtherSessions { get; init; } = true;
- }
-}
diff --git a/src/CodeBeam.UltimateAuth.Users/Users/Models/ConfigureMfaRequest.cs b/src/CodeBeam.UltimateAuth.Users/Users/Models/ConfigureMfaRequest.cs
deleted file mode 100644
index ff2b1d4..0000000
--- a/src/CodeBeam.UltimateAuth.Users/Users/Models/ConfigureMfaRequest.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-namespace CodeBeam.UltimateAuth.Server.Users
-{
- public sealed class ConfigureMfaRequest
- {
- public bool Enable { get; init; }
-
- ///
- /// Optional verification code when enabling MFA.
- ///
- public string? VerificationCode { get; init; }
- }
-}
diff --git a/src/CodeBeam.UltimateAuth.Users/Users/Models/ResetPasswordRequest.cs b/src/CodeBeam.UltimateAuth.Users/Users/Models/ResetPasswordRequest.cs
deleted file mode 100644
index 672d176..0000000
--- a/src/CodeBeam.UltimateAuth.Users/Users/Models/ResetPasswordRequest.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-namespace CodeBeam.UltimateAuth.Server.Users
-{
- public sealed class ResetPasswordRequest
- {
- public required string NewPassword { get; init; }
-
- ///
- /// If true, all active sessions will be revoked.
- ///
- public bool RevokeSessions { get; init; } = true;
- }
-}
diff --git a/src/CodeBeam.UltimateAuth.Users/Users/Models/UpdateProfileRequest.cs b/src/CodeBeam.UltimateAuth.Users/Users/Models/UpdateProfileRequest.cs
deleted file mode 100644
index f7688c9..0000000
--- a/src/CodeBeam.UltimateAuth.Users/Users/Models/UpdateProfileRequest.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace CodeBeam.UltimateAuth.Server.Users
-{
- public sealed class UpdateProfileRequest
- {
- public string? Username { get; init; }
- public string? Email { get; init; }
- }
-}
diff --git a/src/CodeBeam.UltimateAuth.Users/Users/Models/UserDto.cs b/src/CodeBeam.UltimateAuth.Users/Users/Models/UserDto.cs
deleted file mode 100644
index 5a8de73..0000000
--- a/src/CodeBeam.UltimateAuth.Users/Users/Models/UserDto.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-namespace CodeBeam.UltimateAuth.Server.Users
-{
- public sealed class UserDto
- {
- public required TUserId UserId { get; init; }
-
- public string? Username { get; init; }
- public string? Email { get; init; }
-
- public bool IsActive { get; init; }
- public bool IsEmailConfirmed { get; init; }
-
- public DateTimeOffset CreatedAt { get; init; }
- public DateTimeOffset? LastLoginAt { get; init; }
- }
-}
diff --git a/src/CodeBeam.UltimateAuth.Users/Users/Models/UserProfileDto.cs b/src/CodeBeam.UltimateAuth.Users/Users/Models/UserProfileDto.cs
deleted file mode 100644
index 141d34a..0000000
--- a/src/CodeBeam.UltimateAuth.Users/Users/Models/UserProfileDto.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-namespace CodeBeam.UltimateAuth.Server.Users
-{
- public sealed class UserProfileDto
- {
- public required TUserId UserId { get; init; }
-
- public string? Username { get; init; }
- public string? Email { get; init; }
-
- public bool IsEmailConfirmed { get; init; }
-
- public DateTimeOffset CreatedAt { get; init; }
- }
-}
diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.Contracts/CodeBeam.UltimateAuth.Authorization.Contracts.csproj b/src/authorization/CodeBeam.UltimateAuth.Authorization.Contracts/CodeBeam.UltimateAuth.Authorization.Contracts.csproj
new file mode 100644
index 0000000..e03d745
--- /dev/null
+++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.Contracts/CodeBeam.UltimateAuth.Authorization.Contracts.csproj
@@ -0,0 +1,17 @@
+
+
+
+ net8.0;net9.0;net10.0
+ enable
+ enable
+ 0.0.1-preview
+ 0.0.1-preview
+ true
+ $(NoWarn);1591
+
+
+
+
+
+
+
diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.Contracts/Dtos/PermissionDto.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.Contracts/Dtos/PermissionDto.cs
new file mode 100644
index 0000000..10baf33
--- /dev/null
+++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.Contracts/Dtos/PermissionDto.cs
@@ -0,0 +1,6 @@
+namespace CodeBeam.UltimateAuth.Authorization.Contracts;
+
+public sealed record PermissionDto
+{
+ public required string Value { get; init; }
+}
diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.Contracts/Dtos/RoleDto.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.Contracts/Dtos/RoleDto.cs
new file mode 100644
index 0000000..cc50b19
--- /dev/null
+++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.Contracts/Dtos/RoleDto.cs
@@ -0,0 +1,6 @@
+namespace CodeBeam.UltimateAuth.Authorization.Contracts;
+
+public sealed record RoleDto
+{
+ public required string Name { get; init; }
+}
diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.Contracts/Requests/AssignRoleRequest.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.Contracts/Requests/AssignRoleRequest.cs
new file mode 100644
index 0000000..a4bf928
--- /dev/null
+++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.Contracts/Requests/AssignRoleRequest.cs
@@ -0,0 +1,7 @@
+namespace CodeBeam.UltimateAuth.Authorization.Contracts
+{
+ public sealed class AssignRoleRequest
+ {
+ public required string Role { get; init; }
+ }
+}
diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.Contracts/Requests/AuthorizationCheckRequest.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.Contracts/Requests/AuthorizationCheckRequest.cs
new file mode 100644
index 0000000..078a504
--- /dev/null
+++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.Contracts/Requests/AuthorizationCheckRequest.cs
@@ -0,0 +1,9 @@
+namespace CodeBeam.UltimateAuth.Authorization.Contracts
+{
+ public sealed class AuthorizationCheckRequest
+ {
+ public required string Action { get; init; }
+ public string? Resource { get; init; }
+ public string? ResourceId { get; init; }
+ }
+}
diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.Contracts/Requests/AuthorizationRequest.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.Contracts/Requests/AuthorizationRequest.cs
new file mode 100644
index 0000000..4e3f0f3
--- /dev/null
+++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.Contracts/Requests/AuthorizationRequest.cs
@@ -0,0 +1,24 @@
+namespace CodeBeam.UltimateAuth.Authorization.Contracts;
+
+public sealed record AuthorizationRequest
+{
+ ///
+ /// Logical operation being requested (e.g. "orders.read").
+ ///
+ public required string Operation { get; init; }
+
+ ///
+ /// Optional resource identifier (row, entity, aggregate, etc).
+ ///
+ public string? Resource { get; init; }
+
+ ///
+ /// Optional resource identifier.
+ ///
+ public string? ResourceId { get; init; }
+
+ ///
+ /// Optional contextual attributes for fine-grained access decisions.
+ ///
+ public IReadOnlyDictionary? Attributes { get; init; }
+}
diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.Contracts/Responses/AuthorizationResult.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.Contracts/Responses/AuthorizationResult.cs
new file mode 100644
index 0000000..1c3d78d
--- /dev/null
+++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.Contracts/Responses/AuthorizationResult.cs
@@ -0,0 +1,25 @@
+namespace CodeBeam.UltimateAuth.Authorization.Contracts;
+
+public sealed record AuthorizationResult
+{
+ public required bool IsAllowed { get; init; }
+
+ ///
+ /// Indicates whether re-authentication is required.
+ ///
+ public bool RequiresReauthentication { get; init; }
+
+ ///
+ /// Optional reason code for denial.
+ ///
+ public string? DenyReason { get; init; }
+
+ public static AuthorizationResult Allow()
+ => new() { IsAllowed = true };
+
+ public static AuthorizationResult Deny(string? reason = null)
+ => new() { IsAllowed = false, DenyReason = reason };
+
+ public static AuthorizationResult ReauthRequired()
+ => new() { IsAllowed = false, RequiresReauthentication = true };
+}
diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.Contracts/Responses/UserRolesResponse.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.Contracts/Responses/UserRolesResponse.cs
new file mode 100644
index 0000000..17afba3
--- /dev/null
+++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.Contracts/Responses/UserRolesResponse.cs
@@ -0,0 +1,11 @@
+using CodeBeam.UltimateAuth.Core.Domain;
+
+namespace CodeBeam.UltimateAuth.Authorization.Contracts
+{
+ public sealed record UserRolesResponse
+ {
+ public required UserKey UserKey { get; init; }
+ public required IReadOnlyCollection Roles { get; init; }
+ }
+
+}
diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/CodeBeam.UltimateAuth.Authorization.InMemory.csproj b/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/CodeBeam.UltimateAuth.Authorization.InMemory.csproj
new file mode 100644
index 0000000..c80fec6
--- /dev/null
+++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/CodeBeam.UltimateAuth.Authorization.InMemory.csproj
@@ -0,0 +1,19 @@
+
+
+
+ net8.0;net9.0;net10.0
+ enable
+ enable
+ 0.0.1-preview
+ 0.0.1-preview
+ true
+ $(NoWarn);1591
+
+
+
+
+
+
+
+
+
diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Extensions/AuthorizationInMemoryExtensions.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Extensions/AuthorizationInMemoryExtensions.cs
new file mode 100644
index 0000000..3cbbf12
--- /dev/null
+++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Extensions/AuthorizationInMemoryExtensions.cs
@@ -0,0 +1,16 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+
+namespace CodeBeam.UltimateAuth.Authorization.InMemory.Extensions
+{
+ public static class AuthorizationInMemoryExtensions
+ {
+ public static IServiceCollection AddUltimateAuthAuthorizationInMemory(this IServiceCollection services)
+ {
+ services.TryAddScoped();
+ services.TryAddScoped();
+
+ return services;
+ }
+ }
+}
diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/IAuthorizationSeeder.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/IAuthorizationSeeder.cs
new file mode 100644
index 0000000..e7d0561
--- /dev/null
+++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/IAuthorizationSeeder.cs
@@ -0,0 +1,7 @@
+namespace CodeBeam.UltimateAuth.Authorization.InMemory
+{
+ public interface IAuthorizationSeeder
+ {
+ Task SeedAsync(CancellationToken ct = default);
+ }
+}
diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/InMemoryAuthorizationSeeder.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/InMemoryAuthorizationSeeder.cs
new file mode 100644
index 0000000..82af6b3
--- /dev/null
+++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/InMemoryAuthorizationSeeder.cs
@@ -0,0 +1,23 @@
+using CodeBeam.UltimateAuth.Core.Domain;
+using CodeBeam.UltimateAuth.Core.Infrastructure;
+
+namespace CodeBeam.UltimateAuth.Authorization.InMemory
+{
+ internal sealed class InMemoryAuthorizationSeeder : IAuthorizationSeeder
+ {
+ private readonly IUserRoleStore _roles;
+ private readonly IInMemoryUserIdProvider _ids;
+
+ public InMemoryAuthorizationSeeder(IUserRoleStore roles, IInMemoryUserIdProvider ids)
+ {
+ _roles = roles;
+ _ids = ids;
+ }
+
+ public async Task SeedAsync(CancellationToken ct = default)
+ {
+ var key = _ids.GetAdminUserId();
+ await _roles.AssignAsync(null, key, "Admin", ct);
+ }
+ }
+}
diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Stores/InMemoryUserRoleStore.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Stores/InMemoryUserRoleStore.cs
new file mode 100644
index 0000000..e10afdf
--- /dev/null
+++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Stores/InMemoryUserRoleStore.cs
@@ -0,0 +1,50 @@
+using CodeBeam.UltimateAuth.Core.Domain;
+using CodeBeam.UltimateAuth.Core.Infrastructure;
+using System.Collections.Concurrent;
+
+namespace CodeBeam.UltimateAuth.Authorization.InMemory
+{
+ internal sealed class InMemoryUserRoleStore : IUserRoleStore
+ {
+ private readonly ConcurrentDictionary> _roles = new();
+
+ public InMemoryUserRoleStore(IInMemoryUserIdProvider ids)
+ {
+ var key = ids.GetAdminUserId();
+ _roles[key] = new HashSet
+ {
+ "Admin"
+ };
+ }
+
+ public Task> GetRolesAsync(string? tenantId, UserKey userKey, CancellationToken ct = default)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (_roles.TryGetValue(userKey, out var set))
+ return Task.FromResult>(set.ToArray());
+
+ return Task.FromResult>(Array.Empty());
+ }
+
+ public Task AssignAsync(string? tenantId, UserKey userKey, string role, CancellationToken ct = default)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var set = _roles.GetOrAdd(userKey, _ => new HashSet(StringComparer.OrdinalIgnoreCase));
+ set.Add(role);
+ return Task.CompletedTask;
+ }
+
+ public Task RemoveAsync(string? tenantId, UserKey userKey, string role, CancellationToken ct = default)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (_roles.TryGetValue(userKey, out var set))
+ set.Remove(role);
+
+ return Task.CompletedTask;
+ }
+ }
+
+}
diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/CodeBeam.UltimateAuth.Authorization.Reference.csproj b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/CodeBeam.UltimateAuth.Authorization.Reference.csproj
new file mode 100644
index 0000000..f42a8e7
--- /dev/null
+++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/CodeBeam.UltimateAuth.Authorization.Reference.csproj
@@ -0,0 +1,19 @@
+
+
+
+ net8.0;net9.0;net10.0
+ enable
+ enable
+ 0.0.1-preview
+ 0.0.1-preview
+ true
+ $(NoWarn);1591
+
+
+
+
+
+
+
+
+
diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Domain/Role.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Domain/Role.cs
new file mode 100644
index 0000000..2550b22
--- /dev/null
+++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Domain/Role.cs
@@ -0,0 +1,7 @@
+using CodeBeam.UltimateAuth.Authorization.Domain;
+
+public sealed class Role
+{
+ public required string Name { get; init; }
+ public IReadOnlyCollection Permissions { get; init; } = Array.Empty();
+}
diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Endpoints/DefaultAuthorizationEndpointHandler.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Endpoints/DefaultAuthorizationEndpointHandler.cs
new file mode 100644
index 0000000..09df3ba
--- /dev/null
+++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/Endpoints/DefaultAuthorizationEndpointHandler.cs
@@ -0,0 +1,90 @@
+using CodeBeam.UltimateAuth.Authorization.Contracts;
+using CodeBeam.UltimateAuth.Core.Contracts;
+using CodeBeam.UltimateAuth.Core.Domain;
+using CodeBeam.UltimateAuth.Server.Auth;
+using CodeBeam.UltimateAuth.Server.Endpoints;
+using CodeBeam.UltimateAuth.Server.Extensions;
+using Microsoft.AspNetCore.Http;
+
+namespace CodeBeam.UltimateAuth.Authorization.Reference
+{
+ public sealed class DefaultAuthorizationEndpointHandler : IAuthorizationEndpointHandler
+ {
+ private readonly IAuthFlowContextAccessor _authFlow;
+ private readonly IAuthorizationService _authorization;
+ private readonly IUserRoleService _roles;
+
+ public DefaultAuthorizationEndpointHandler(IAuthFlowContextAccessor authFlow, IAuthorizationService authorization, IUserRoleService roles)
+ {
+ _authFlow = authFlow;
+ _authorization = authorization;
+ _roles = roles;
+ }
+
+ public async Task CheckAsync(HttpContext ctx)
+ {
+ var flow = _authFlow.Current;
+
+ if (!flow.IsAuthenticated)
+ return Results.Unauthorized();
+
+ var req = await ctx.ReadJsonAsync