diff --git a/UltimateAuth.slnx b/UltimateAuth.slnx
index ee6b5d9..f86cfc5 100644
--- a/UltimateAuth.slnx
+++ b/UltimateAuth.slnx
@@ -12,15 +12,26 @@
+
+
+
+
-
+
+
+
+
+
+
+
+
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..00bebb1 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,19 +78,19 @@ 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
};
- await UAuthClient.CompletePkceLoginAsync(request);
+ await UAuthClient.Flows.CompletePkceLoginAsync(request);
}
private async Task StartNewPkceAsync()
{
var returnUrl = await ResolveReturnUrlAsync();
- await UAuthClient.BeginPkceAsync(returnUrl);
+ await UAuthClient.Flows.BeginPkceAsync(returnUrl);
}
private async Task ResolveReturnUrlAsync()
diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Program.cs b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Program.cs
index 67c4055..53d75b8 100644
--- a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Program.cs
+++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Program.cs
@@ -1,16 +1,24 @@
+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.Defaults;
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 +54,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 +86,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..0ebd5cf 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
@@ -8,18 +8,28 @@
+
+
+
+
+
+
+
+
+
+
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..6919b0a 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
@@ -18,7 +18,7 @@
@inject IClock Clock
@inject IUAuthCookieManager CookieManager
@inject IHttpContextAccessor HttpContextAccessor
-@inject IUAuthClient UAuthClient
+@inject IUAuthClient UAuth
@inject NavigationManager Nav
@inject IUAuthProductInfoProvider ProductInfo
@inject AuthenticationStateProvider AuthStateProvider
@@ -45,6 +45,8 @@
Programmatic Login
+ GetMe
+ Change User Inactive
@@ -64,6 +66,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..80c99fe 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
@@ -2,6 +2,7 @@
using CodeBeam.UltimateAuth.Client.Device;
using CodeBeam.UltimateAuth.Core.Contracts;
using CodeBeam.UltimateAuth.Core.Domain;
+using CodeBeam.UltimateAuth.Users.Contracts;
using Microsoft.AspNetCore.Components.Authorization;
using MudBlazor;
@@ -41,17 +42,17 @@ 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);
+ await UAuth.Flows.LoginAsync(request);
_authState = await AuthStateProvider.GetAuthenticationStateAsync();
}
private async Task ValidateAsync()
{
- var result = await UAuthClient.ValidateAsync();
+ var result = await UAuth.Flows.ValidateAsync();
Snackbar.Add(
result.IsValid ? "Session is valid ✅" : $"Session invalid ❌ ({result.State})",
@@ -60,13 +61,45 @@ private async Task ValidateAsync()
private async Task LogoutAsync()
{
- await UAuthClient.LogoutAsync();
+ await UAuth.Flows.LogoutAsync();
Snackbar.Add("Logged out", Severity.Success);
}
private async Task RefreshAsync()
{
- await UAuthClient.RefreshAsync();
+ await UAuth.Flows.RefreshAsync();
+ }
+
+ private async Task HandleGetMe()
+ {
+ var profileResult = await UAuth.Users.GetMeAsync();
+ if (profileResult.Ok)
+ {
+ var profile = profileResult.Value;
+ Snackbar.Add($"User Profile: {profile?.UserName} ({profile?.DisplayName})", Severity.Info);
+ }
+ else
+ {
+ Snackbar.Add($"Failed to get profile: {profileResult.Error}", Severity.Error);
+ }
+ }
+
+ private async Task ChangeUserInactive()
+ {
+ ChangeUserStatusRequest request = new ChangeUserStatusRequest
+ {
+ UserKey = UserKey.FromString("user"),
+ NewStatus = UserStatus.Disabled
+ };
+ var result = await UAuth.Users.ChangeStatusAsync(request);
+ if (result.Ok)
+ {
+ Snackbar.Add($"User is disabled.", Severity.Info);
+ }
+ else
+ {
+ Snackbar.Add($"Failed to change user status.", Severity.Error);
+ }
}
protected override void OnAfterRender(bool firstRender)
diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Program.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Program.cs
index e449e4e..cff2558 100644
--- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Program.cs
+++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Program.cs
@@ -1,16 +1,25 @@
+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.Defaults;
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;
+using Scalar.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
@@ -24,6 +33,8 @@
builder.Services.AddMudServices();
builder.Services.AddMudExtensions();
+builder.Services.AddEndpointsApiExplorer();
+builder.Services.AddOpenApi();
builder.Services
.AddAuthentication(options =>
@@ -44,7 +55,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 +96,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())
{
@@ -87,6 +114,11 @@
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
+else
+{
+ app.MapOpenApi();
+ app.MapScalarApiReference();
+}
app.UseHttpsRedirection();
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..44c873a 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,22 +46,22 @@ 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);
+ await UAuthClient.Flows.LoginAsync(request);
}
private async Task StartPkceLogin()
{
- await UAuthClient.BeginPkceAsync();
+ await UAuthClient.Flows.BeginPkceAsync();
//await UAuthClient.NavigateToHubLoginAsync(Nav.Uri);
}
private async Task ValidateAsync()
{
- var result = await UAuthClient.ValidateAsync();
+ var result = await UAuthClient.Flows.ValidateAsync();
Snackbar.Add(
result.IsValid ? "Session is valid ✅" : $"Session invalid ❌ ({result.State})",
@@ -70,13 +70,13 @@ private async Task ValidateAsync()
private async Task LogoutAsync()
{
- await UAuthClient.LogoutAsync();
+ await UAuthClient.Flows.LogoutAsync();
Snackbar.Add("Logged out", Severity.Success);
}
private async Task RefreshAsync()
{
- await UAuthClient.RefreshAsync();
+ await UAuthClient.Flows.RefreshAsync();
}
private async Task RefreshAuthState()
diff --git a/src/CodeBeam.UltimateAuth.Client/Abstractions/IBrowserPostClient.cs b/src/CodeBeam.UltimateAuth.Client/Abstractions/IBrowserPostClient.cs
deleted file mode 100644
index efded6e..0000000
--- a/src/CodeBeam.UltimateAuth.Client/Abstractions/IBrowserPostClient.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-using CodeBeam.UltimateAuth.Client.Contracts;
-
-namespace CodeBeam.UltimateAuth.Client.Abstractions
-{
- public interface IBrowserPostClient
- {
- ///
- /// Sends a POST request to the specified endpoint with the provided form data and navigates to the resulting. Submits a form.
- /// location asynchronously.
- ///
- /// The relative or absolute URI of the endpoint to which the POST request is sent. Cannot be null or empty.
- /// An optional collection of key-value pairs representing form data to include in the POST request. If null, no
- /// form data is sent.
- /// A task that represents the asynchronous navigation operation.
- Task NavigatePostAsync(string endpoint, IDictionary? data = null);
-
- ///
- /// Background POST request with JS fetch.
- ///
- ///
- ///
- Task FetchPostAsync(string endpoint, IDictionary? data = null);
-
- //Task> FetchPostJsonAsync(string url, IDictionary? data = null);
-
- Task FetchPostJsonRawAsync(string endpoint, IDictionary? data = null);
- }
-}
diff --git a/src/CodeBeam.UltimateAuth.Client/Authentication/DefaultUAuthStateManager.cs b/src/CodeBeam.UltimateAuth.Client/Authentication/DefaultUAuthStateManager.cs
index fd9dbae..1eb5b37 100644
--- a/src/CodeBeam.UltimateAuth.Client/Authentication/DefaultUAuthStateManager.cs
+++ b/src/CodeBeam.UltimateAuth.Client/Authentication/DefaultUAuthStateManager.cs
@@ -24,7 +24,7 @@ public async Task EnsureAsync(CancellationToken ct = default)
return;
await _bootstrapper.EnsureStartedAsync();
- var result = await _client.ValidateAsync();
+ var result = await _client.Flows.ValidateAsync();
if (!result.IsValid)
{
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.Client/Contracts/BrowserPostJsonResult.cs b/src/CodeBeam.UltimateAuth.Client/Contracts/BrowserPostJsonResult.cs
deleted file mode 100644
index 643423d..0000000
--- a/src/CodeBeam.UltimateAuth.Client/Contracts/BrowserPostJsonResult.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-namespace CodeBeam.UltimateAuth.Client.Contracts
-{
- public sealed record BrowserPostJsonResult
- {
- public bool Ok { get; init; }
- public int Status { get; init; }
- public string? RefreshOutcome { get; init; }
- public T? Body { get; init; }
- }
-}
diff --git a/src/CodeBeam.UltimateAuth.Client/Contracts/BrowserPostResult.cs b/src/CodeBeam.UltimateAuth.Client/Contracts/BrowserPostResult.cs
deleted file mode 100644
index 1b48e99..0000000
--- a/src/CodeBeam.UltimateAuth.Client/Contracts/BrowserPostResult.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-namespace CodeBeam.UltimateAuth.Client.Contracts
-{
- public sealed record BrowserPostResult
- {
- public bool Ok { get; init; }
- public int Status { get; init; }
- public string? RefreshOutcome { get; init; }
- }
-}
diff --git a/src/CodeBeam.UltimateAuth.Client/Contracts/BrowserPostRawResult.cs b/src/CodeBeam.UltimateAuth.Client/Contracts/UAuthTransportResult.cs
similarity index 85%
rename from src/CodeBeam.UltimateAuth.Client/Contracts/BrowserPostRawResult.cs
rename to src/CodeBeam.UltimateAuth.Client/Contracts/UAuthTransportResult.cs
index 446b982..867cfe8 100644
--- a/src/CodeBeam.UltimateAuth.Client/Contracts/BrowserPostRawResult.cs
+++ b/src/CodeBeam.UltimateAuth.Client/Contracts/UAuthTransportResult.cs
@@ -2,7 +2,7 @@
namespace CodeBeam.UltimateAuth.Client.Contracts
{
- public sealed class BrowserPostRawResult
+ public sealed class UAuthTransportResult
{
public bool Ok { get; init; }
public int Status { get; init; }
diff --git a/src/CodeBeam.UltimateAuth.Client/Extensions/UltimateAuthClientServiceCollectionExtensions.cs b/src/CodeBeam.UltimateAuth.Client/Extensions/UltimateAuthClientServiceCollectionExtensions.cs
index 752739e..fab6aae 100644
--- a/src/CodeBeam.UltimateAuth.Client/Extensions/UltimateAuthClientServiceCollectionExtensions.cs
+++ b/src/CodeBeam.UltimateAuth.Client/Extensions/UltimateAuthClientServiceCollectionExtensions.cs
@@ -6,6 +6,7 @@
using CodeBeam.UltimateAuth.Client.Infrastructure;
using CodeBeam.UltimateAuth.Client.Options;
using CodeBeam.UltimateAuth.Client.Runtime;
+using CodeBeam.UltimateAuth.Client.Services;
using CodeBeam.UltimateAuth.Client.Utilities;
using CodeBeam.UltimateAuth.Core.Abstractions;
using CodeBeam.UltimateAuth.Core.Options;
@@ -96,8 +97,10 @@ private static IServiceCollection AddUltimateAuthClientInternal(this IServiceCol
o.Refresh.Interval ??= TimeSpan.FromMinutes(5);
});
- services.AddScoped();
+ services.AddScoped();
services.AddScoped();
+ services.AddScoped();
+ services.TryAddScoped();
services.AddScoped(sp =>
{
diff --git a/src/CodeBeam.UltimateAuth.Client/Infrastructure/BlazorServerSessionCoordinator.cs b/src/CodeBeam.UltimateAuth.Client/Infrastructure/BlazorServerSessionCoordinator.cs
index f722864..0b0d06e 100644
--- a/src/CodeBeam.UltimateAuth.Client/Infrastructure/BlazorServerSessionCoordinator.cs
+++ b/src/CodeBeam.UltimateAuth.Client/Infrastructure/BlazorServerSessionCoordinator.cs
@@ -48,7 +48,7 @@ private async Task RunAsync(CancellationToken ct)
while (await _timer!.WaitForNextTickAsync(ct))
{
_diagnostics.MarkAutomaticRefresh();
- var result = await _client.RefreshAsync(isAuto: true);
+ var result = await _client.Flows.RefreshAsync(isAuto: true);
switch (result.Outcome)
{
diff --git a/src/CodeBeam.UltimateAuth.Client/Infrastructure/BrowserPostClient.cs b/src/CodeBeam.UltimateAuth.Client/Infrastructure/BrowserPostClient.cs
deleted file mode 100644
index 02bc7bc..0000000
--- a/src/CodeBeam.UltimateAuth.Client/Infrastructure/BrowserPostClient.cs
+++ /dev/null
@@ -1,75 +0,0 @@
-using CodeBeam.UltimateAuth.Client.Abstractions;
-using CodeBeam.UltimateAuth.Client.Contracts;
-using CodeBeam.UltimateAuth.Core.Options;
-using Microsoft.Extensions.Options;
-using Microsoft.JSInterop;
-
-namespace CodeBeam.UltimateAuth.Client.Infrastructure
-{
- internal sealed class BrowserPostClient : IBrowserPostClient
- {
- private readonly IJSRuntime _js;
- private UAuthOptions _coreOptions;
-
- public BrowserPostClient(IJSRuntime js, IOptions coreOptions)
- {
- _js = js;
- _coreOptions = coreOptions.Value;
- }
-
- public Task NavigatePostAsync(string endpoint, IDictionary? data = null)
- {
- return _js.InvokeVoidAsync("uauth.post", new
- {
- url = endpoint,
- mode = "navigate",
- data = data,
- clientProfile = _coreOptions.ClientProfile.ToString()
- }).AsTask();
- }
-
- public async Task FetchPostAsync(string endpoint, IDictionary? data = null)
- {
- var result = await _js.InvokeAsync("uauth.post", new
- {
- url = endpoint,
- mode = "fetch",
- expectJson = false,
- data = data,
- clientProfile = _coreOptions.ClientProfile.ToString()
- });
-
- return result;
- }
-
- public async Task FetchPostJsonRawAsync(string endpoint, IDictionary? data = null)
- {
- var postData = data ?? new Dictionary();
- return await _js.InvokeAsync("uauth.post",
- new
- {
- url = endpoint,
- mode = "fetch",
- expectJson = true,
- data = postData,
- clientProfile = _coreOptions.ClientProfile.ToString()
- });
- }
-
-
- //public async Task> FetchPostJsonAsync(string endpoint, IDictionary? data = null)
- //{
- // var result = await _js.InvokeAsync>("uauth.post", new
- // {
- // url = endpoint,
- // mode = "fetch",
- // expectJson = true,
- // data = data,
- // clientProfile = _coreOptions.ClientProfile.ToString()
- // });
-
- // return result;
- //}
-
- }
-}
diff --git a/src/CodeBeam.UltimateAuth.Client/Infrastructure/IUAuthRequestClient.cs b/src/CodeBeam.UltimateAuth.Client/Infrastructure/IUAuthRequestClient.cs
new file mode 100644
index 0000000..f93dd98
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Client/Infrastructure/IUAuthRequestClient.cs
@@ -0,0 +1,15 @@
+using CodeBeam.UltimateAuth.Client.Contracts;
+
+namespace CodeBeam.UltimateAuth.Client.Infrastructure
+{
+ public interface IUAuthRequestClient
+ {
+ Task NavigateAsync(string endpoint, IDictionary? form = null, CancellationToken ct = default);
+
+ Task SendFormAsync(string endpoint, IDictionary? form = null, CancellationToken ct = default);
+
+ Task SendFormForJsonAsync(string endpoint, IDictionary? form = null, CancellationToken ct = default);
+
+ Task SendJsonAsync(string endpoint, object? payload = null, CancellationToken ct = default);
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthRequestClient.cs b/src/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthRequestClient.cs
new file mode 100644
index 0000000..beb15f8
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthRequestClient.cs
@@ -0,0 +1,79 @@
+using CodeBeam.UltimateAuth.Client.Abstractions;
+using CodeBeam.UltimateAuth.Client.Contracts;
+using CodeBeam.UltimateAuth.Core.Options;
+using Microsoft.Extensions.Options;
+using Microsoft.JSInterop;
+
+// TODO: Add fluent helper API like RequiredOk
+namespace CodeBeam.UltimateAuth.Client.Infrastructure
+{
+ internal sealed class UAuthRequestClient : IUAuthRequestClient
+ {
+ private readonly IJSRuntime _js;
+ private UAuthOptions _coreOptions;
+
+ public UAuthRequestClient(IJSRuntime js, IOptions coreOptions)
+ {
+ _js = js;
+ _coreOptions = coreOptions.Value;
+ }
+
+ public Task NavigateAsync(string endpoint, IDictionary? form = null, CancellationToken ct = default)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ return _js.InvokeVoidAsync("uauth.post", ct, new
+ {
+ url = endpoint,
+ mode = "navigate",
+ data = form,
+ clientProfile = _coreOptions.ClientProfile.ToString()
+ }).AsTask();
+ }
+
+ public async Task SendFormAsync(string endpoint, IDictionary? form = null, CancellationToken ct = default)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var result = await _js.InvokeAsync("uauth.post", ct, new
+ {
+ url = endpoint,
+ mode = "fetch",
+ expectJson = false,
+ data = form,
+ clientProfile = _coreOptions.ClientProfile.ToString()
+ });
+
+ return result;
+ }
+
+ public async Task SendFormForJsonAsync(string endpoint, IDictionary? form = null, CancellationToken ct = default)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var postData = form ?? new Dictionary();
+ return await _js.InvokeAsync("uauth.post", ct,
+ new
+ {
+ url = endpoint,
+ mode = "fetch",
+ expectJson = true,
+ data = postData,
+ clientProfile = _coreOptions.ClientProfile.ToString()
+ });
+ }
+
+ public async Task SendJsonAsync(string endpoint, object? payload = default, CancellationToken ct = default)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ return await _js.InvokeAsync("uauth.postJson", ct, new
+ {
+ url = endpoint,
+ payload = payload,
+ clientProfile = _coreOptions.ClientProfile.ToString()
+ });
+ }
+
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthResultMapper.cs b/src/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthResultMapper.cs
new file mode 100644
index 0000000..245575e
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthResultMapper.cs
@@ -0,0 +1,51 @@
+using CodeBeam.UltimateAuth.Client.Contracts;
+using CodeBeam.UltimateAuth.Core.Contracts;
+using System.Text.Json;
+
+namespace CodeBeam.UltimateAuth.Client.Infrastructure
+{
+ internal static class UAuthResultMapper
+ {
+ public static UAuthResult FromJson(UAuthTransportResult raw)
+ {
+ if (!raw.Ok)
+ {
+ return new UAuthResult
+ {
+ Ok = false,
+ Status = raw.Status
+ };
+ }
+
+ if (raw.Body is null)
+ {
+ return new UAuthResult
+ {
+ Ok = true,
+ Status = raw.Status,
+ Value = default
+ };
+ }
+
+ var value = raw.Body.Value.Deserialize(
+ new JsonSerializerOptions
+ {
+ PropertyNameCaseInsensitive = true
+ });
+
+ return new UAuthResult
+ {
+ Ok = true,
+ Status = raw.Status,
+ Value = value
+ };
+ }
+
+ public static UAuthResult FromStatus(UAuthTransportResult raw)
+ => new()
+ {
+ Ok = raw.Ok,
+ Status = raw.Status
+ };
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Client/Options/UAuthClientOptions.cs b/src/CodeBeam.UltimateAuth.Client/Options/UAuthClientOptions.cs
index 97a8aee..94f0af8 100644
--- a/src/CodeBeam.UltimateAuth.Client/Options/UAuthClientOptions.cs
+++ b/src/CodeBeam.UltimateAuth.Client/Options/UAuthClientOptions.cs
@@ -15,15 +15,15 @@ public sealed class AuthEndpointOptions
///
/// Base URL of UAuthHub (e.g. https://localhost:6110)
///
- public string Authority { get; set; } = string.Empty;
+ public string Authority { get; set; } = "/auth";
- public string Login { get; set; } = "/auth/login";
- public string Logout { get; set; } = "/auth/logout";
- public string Refresh { get; set; } = "/auth/refresh";
- public string Reauth { get; set; } = "/auth/reauth";
- public string Validate { get; set; } = "/auth/validate";
- public string PkceAuthorize { get; set; } = "/auth/pkce/authorize";
- public string PkceComplete { get; set; } = "/auth/pkce/complete";
+ public string Login { get; set; } = "/login";
+ public string Logout { get; set; } = "/logout";
+ public string Refresh { get; set; } = "/refresh";
+ public string Reauth { get; set; } = "/reauth";
+ public string Validate { get; set; } = "/validate";
+ public string PkceAuthorize { get; set; } = "/pkce/authorize";
+ public string PkceComplete { get; set; } = "/pkce/complete";
public string HubLoginPath { get; set; } = "/uauthhub/login";
}
diff --git a/src/CodeBeam.UltimateAuth.Client/Services/DefaultFlowClient.cs b/src/CodeBeam.UltimateAuth.Client/Services/DefaultFlowClient.cs
new file mode 100644
index 0000000..cda6ed4
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Client/Services/DefaultFlowClient.cs
@@ -0,0 +1,221 @@
+using CodeBeam.UltimateAuth.Client.Abstractions;
+using CodeBeam.UltimateAuth.Client.Contracts;
+using CodeBeam.UltimateAuth.Client.Diagnostics;
+using CodeBeam.UltimateAuth.Client.Extensions;
+using CodeBeam.UltimateAuth.Client.Infrastructure;
+using CodeBeam.UltimateAuth.Client.Options;
+using CodeBeam.UltimateAuth.Core.Contracts;
+using CodeBeam.UltimateAuth.Core.Domain;
+using CodeBeam.UltimateAuth.Core.Options;
+using Microsoft.AspNetCore.Components;
+using Microsoft.Extensions.Options;
+using System.Security.Cryptography;
+using System.Text;
+using System.Text.Json;
+
+namespace CodeBeam.UltimateAuth.Client.Services
+{
+ internal class DefaultFlowClient : IFlowClient
+ {
+ private readonly IUAuthRequestClient _post;
+ private readonly UAuthClientOptions _options;
+ private readonly UAuthOptions _coreOptions;
+ private readonly UAuthClientDiagnostics _diagnostics;
+ private readonly NavigationManager _nav;
+
+ public DefaultFlowClient(
+ IUAuthRequestClient post,
+ IOptions options,
+ IOptions coreOptions,
+ UAuthClientDiagnostics diagnostics,
+ NavigationManager nav)
+ {
+ _post = post;
+ _options = options.Value;
+ _coreOptions = coreOptions.Value;
+ _diagnostics = diagnostics;
+ _nav = nav;
+ }
+
+ public async Task LoginAsync(LoginRequest request)
+ {
+ var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, _options.Endpoints.Login);
+ await _post.NavigateAsync(url, request.ToDictionary());
+ }
+
+ public async Task LogoutAsync()
+ {
+ var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, _options.Endpoints.Logout);
+ await _post.NavigateAsync(url);
+ }
+
+ public async Task RefreshAsync(bool isAuto = false)
+ {
+ if (isAuto == false)
+ {
+ _diagnostics.MarkManualRefresh();
+ }
+
+ var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, _options.Endpoints.Refresh);
+ var result = await _post.SendFormAsync(url);
+ var refreshOutcome = RefreshOutcomeParser.Parse(result.RefreshOutcome);
+ switch (refreshOutcome)
+ {
+ case RefreshOutcome.NoOp:
+ _diagnostics.MarkRefreshNoOp();
+ break;
+ case RefreshOutcome.Touched:
+ _diagnostics.MarkRefreshTouched();
+ break;
+ case RefreshOutcome.ReauthRequired:
+ _diagnostics.MarkRefreshReauthRequired();
+ break;
+ case RefreshOutcome.None:
+ _diagnostics.MarkRefreshUnknown();
+ break;
+ }
+
+ return new RefreshResult
+ {
+ Ok = result.Ok,
+ Status = result.Status,
+ Outcome = refreshOutcome
+ };
+ }
+
+ public async Task ReauthAsync()
+ {
+ var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, _options.Endpoints.Reauth);
+ await _post.NavigateAsync(_options.Endpoints.Reauth);
+ }
+
+ public async Task ValidateAsync()
+ {
+ var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, _options.Endpoints.Validate);
+ var raw = await _post.SendFormForJsonAsync(url);
+
+ if (!raw.Ok || raw.Body is null)
+ {
+ return new AuthValidationResult
+ {
+ IsValid = false,
+ State = "transport"
+ };
+ }
+
+ var body = raw.Body.Value.Deserialize(
+ new JsonSerializerOptions
+ {
+ PropertyNameCaseInsensitive = true
+ });
+
+ return body ?? new AuthValidationResult
+ {
+ IsValid = false,
+ State = "deserialize"
+ };
+ }
+
+ public async Task BeginPkceAsync(string? returnUrl = null)
+ {
+ var pkce = _options.Login.Pkce;
+
+ if (!pkce.Enabled)
+ throw new InvalidOperationException("PKCE login is disabled by configuration.");
+
+ var verifier = CreateVerifier();
+ var challenge = CreateChallenge(verifier);
+
+ var authorizeUrl = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, _options.Endpoints.PkceAuthorize);
+
+ var raw = await _post.SendFormForJsonAsync(
+ authorizeUrl,
+ new Dictionary
+ {
+ ["code_challenge"] = challenge,
+ ["challenge_method"] = "S256"
+ });
+
+ if (!raw.Ok || raw.Body is null)
+ throw new InvalidOperationException("PKCE authorize failed.");
+
+ var response = raw.Body.Value.Deserialize(
+ new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
+
+ if (response is null || string.IsNullOrWhiteSpace(response.AuthorizationCode))
+ throw new InvalidOperationException("Invalid PKCE authorize response.");
+
+ if (pkce.OnAuthorized is not null)
+ await pkce.OnAuthorized(response);
+
+ var resolvedReturnUrl = returnUrl
+ ?? pkce.ReturnUrl
+ ?? _options.Login.DefaultReturnUrl
+ ?? _nav.Uri;
+
+ if (pkce.AutoRedirect)
+ {
+ await NavigateToHubLoginAsync(response.AuthorizationCode, verifier, resolvedReturnUrl);
+ }
+ }
+
+ public async Task CompletePkceLoginAsync(PkceLoginRequest request)
+ {
+ if (request is null)
+ throw new ArgumentNullException(nameof(request));
+
+ var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, _options.Endpoints.PkceComplete);
+
+ var payload = new Dictionary
+ {
+ ["authorization_code"] = request.AuthorizationCode,
+ ["code_verifier"] = request.CodeVerifier,
+ ["return_url"] = request.ReturnUrl,
+
+ ["Identifier"] = request.Identifier ?? string.Empty,
+ ["Secret"] = request.Secret ?? string.Empty
+ };
+
+ await _post.NavigateAsync(url, payload);
+ }
+
+ private Task NavigateToHubLoginAsync(string authorizationCode, string codeVerifier, string returnUrl)
+ {
+ var hubLoginUrl = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, _options.Endpoints.HubLoginPath);
+
+ var data = new Dictionary
+ {
+ ["authorization_code"] = authorizationCode,
+ ["code_verifier"] = codeVerifier,
+ ["return_url"] = returnUrl,
+ ["client_profile"] = _coreOptions.ClientProfile.ToString()
+ };
+
+ return _post.NavigateAsync(hubLoginUrl, data);
+ }
+
+
+ // ---------------- PKCE CRYPTO ----------------
+
+ private static string CreateVerifier()
+ {
+ var bytes = RandomNumberGenerator.GetBytes(32);
+ return Base64UrlEncode(bytes);
+ }
+
+ private static string CreateChallenge(string verifier)
+ {
+ using var sha256 = SHA256.Create();
+ var hash = sha256.ComputeHash(Encoding.ASCII.GetBytes(verifier));
+ return Base64UrlEncode(hash);
+ }
+
+ private static string Base64UrlEncode(byte[] input)
+ {
+ return Convert.ToBase64String(input)
+ .TrimEnd('=')
+ .Replace('+', '-')
+ .Replace('/', '_');
+ }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Client/Services/DefaultUserClient.cs b/src/CodeBeam.UltimateAuth.Client/Services/DefaultUserClient.cs
new file mode 100644
index 0000000..62818e8
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Client/Services/DefaultUserClient.cs
@@ -0,0 +1,70 @@
+using CodeBeam.UltimateAuth.Client.Infrastructure;
+using CodeBeam.UltimateAuth.Client.Options;
+using CodeBeam.UltimateAuth.Core.Contracts;
+using CodeBeam.UltimateAuth.Core.Domain;
+using CodeBeam.UltimateAuth.Users.Contracts;
+using Microsoft.Extensions.Options;
+
+namespace CodeBeam.UltimateAuth.Client.Services
+{
+ internal sealed class DefaultUserClient : IUserClient
+ {
+ private readonly IUAuthRequestClient _request;
+ private readonly UAuthClientOptions _options;
+
+ public DefaultUserClient(IUAuthRequestClient request, IOptions options)
+ {
+ _request = request;
+ _options = options.Value;
+ }
+
+ public async Task> GetMeAsync()
+ {
+ var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, "/users/me/get");
+ var raw = await _request.SendFormForJsonAsync(url);
+ return UAuthResultMapper.FromJson(raw);
+ }
+
+ public async Task UpdateMeAsync(UpdateProfileRequest request)
+ {
+ var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, "/users/me/update");
+ var raw = await _request.SendJsonAsync(url, request);
+ return UAuthResultMapper.FromStatus(raw);
+ }
+
+ public async Task> CreateAsync(CreateUserRequest request)
+ {
+ var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, "/users/create");
+ var raw = await _request.SendJsonAsync(url, request);
+ return UAuthResultMapper.FromJson(raw);
+ }
+
+ public async Task> ChangeStatusAsync(ChangeUserStatusRequest request)
+ {
+ var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, "/users/status");
+ var raw = await _request.SendJsonAsync(url, request);
+ return UAuthResultMapper.FromJson(raw);
+ }
+
+ public async Task> DeleteAsync(DeleteUserRequest request)
+ {
+ var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, "/users/delete");
+ var raw = await _request.SendJsonAsync(url, request);
+ return UAuthResultMapper.FromJson(raw);
+ }
+
+ public async Task> GetProfileAsync(UserKey userKey)
+ {
+ var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, $"/admin/users/{userKey}/profile/get");
+ var raw = await _request.SendFormForJsonAsync(url);
+ return UAuthResultMapper.FromJson(raw);
+ }
+
+ public async Task UpdateProfileAsync(UserKey userKey, UpdateProfileRequest request)
+ {
+ var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, $"/admin/users/{userKey}/profile/update");
+ var raw = await _request.SendJsonAsync(url, request);
+ return UAuthResultMapper.FromStatus(raw);
+ }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Client/Services/IFlowClient.cs b/src/CodeBeam.UltimateAuth.Client/Services/IFlowClient.cs
new file mode 100644
index 0000000..c99d145
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Client/Services/IFlowClient.cs
@@ -0,0 +1,16 @@
+using CodeBeam.UltimateAuth.Client.Contracts;
+using CodeBeam.UltimateAuth.Core.Contracts;
+
+namespace CodeBeam.UltimateAuth.Client.Services;
+
+public interface IFlowClient
+{
+ Task LoginAsync(LoginRequest request);
+ Task LogoutAsync();
+ Task RefreshAsync(bool isAuto = false);
+ Task ReauthAsync();
+ Task ValidateAsync();
+
+ Task BeginPkceAsync(string? returnUrl = null);
+ Task CompletePkceLoginAsync(PkceLoginRequest request);
+}
diff --git a/src/CodeBeam.UltimateAuth.Client/Services/IUAuthClient.cs b/src/CodeBeam.UltimateAuth.Client/Services/IUAuthClient.cs
index 5bdcfe5..e6bddb4 100644
--- a/src/CodeBeam.UltimateAuth.Client/Services/IUAuthClient.cs
+++ b/src/CodeBeam.UltimateAuth.Client/Services/IUAuthClient.cs
@@ -1,18 +1,10 @@
-using CodeBeam.UltimateAuth.Client.Contracts;
-using CodeBeam.UltimateAuth.Core.Contracts;
+using CodeBeam.UltimateAuth.Client.Services;
namespace CodeBeam.UltimateAuth.Client
{
public interface IUAuthClient
{
- Task LoginAsync(LoginRequest request);
- Task LogoutAsync();
- Task RefreshAsync(bool isAuto = false);
- Task ReauthAsync();
-
- Task ValidateAsync();
-
- Task BeginPkceAsync(string? returnUrl = null);
- Task CompletePkceLoginAsync(PkceLoginRequest request);
+ IFlowClient Flows { get; }
+ IUserClient Users { get; }
}
}
diff --git a/src/CodeBeam.UltimateAuth.Client/Services/IUserClient.cs b/src/CodeBeam.UltimateAuth.Client/Services/IUserClient.cs
new file mode 100644
index 0000000..79d6bd9
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Client/Services/IUserClient.cs
@@ -0,0 +1,19 @@
+using CodeBeam.UltimateAuth.Core.Contracts;
+using CodeBeam.UltimateAuth.Core.Domain;
+using CodeBeam.UltimateAuth.Users.Contracts;
+
+namespace CodeBeam.UltimateAuth.Client.Services
+{
+ public interface IUserClient
+ {
+ Task> CreateAsync(CreateUserRequest request);
+ Task> ChangeStatusAsync(ChangeUserStatusRequest request);
+ Task> DeleteAsync(DeleteUserRequest request);
+
+ Task> GetMeAsync();
+ Task UpdateMeAsync(UpdateProfileRequest request);
+
+ Task> GetProfileAsync(UserKey userKey);
+ Task UpdateProfileAsync(UserKey userKey, UpdateProfileRequest request);
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Client/Services/UAuthClient.cs b/src/CodeBeam.UltimateAuth.Client/Services/UAuthClient.cs
index 1cf53eb..021de20 100644
--- a/src/CodeBeam.UltimateAuth.Client/Services/UAuthClient.cs
+++ b/src/CodeBeam.UltimateAuth.Client/Services/UAuthClient.cs
@@ -1,224 +1,15 @@
-using CodeBeam.UltimateAuth.Client.Abstractions;
-using CodeBeam.UltimateAuth.Client.Authentication;
-using CodeBeam.UltimateAuth.Client.Contracts;
-using CodeBeam.UltimateAuth.Client.Diagnostics;
-using CodeBeam.UltimateAuth.Client.Extensions;
-using CodeBeam.UltimateAuth.Client.Infrastructure;
-using CodeBeam.UltimateAuth.Client.Options;
-using CodeBeam.UltimateAuth.Core.Abstractions;
-using CodeBeam.UltimateAuth.Core.Contracts;
-using CodeBeam.UltimateAuth.Core.Domain;
-using CodeBeam.UltimateAuth.Core.Options;
-using Microsoft.AspNetCore.Components;
-using Microsoft.Extensions.Options;
-using System.Security.Cryptography;
-using System.Text;
-using System.Text.Json;
+using CodeBeam.UltimateAuth.Client.Services;
-namespace CodeBeam.UltimateAuth.Client
-{
- internal sealed class UAuthClient : IUAuthClient
- {
- private readonly IBrowserPostClient _post;
- private readonly UAuthClientOptions _options;
- private readonly UAuthOptions _coreOptions;
- private readonly UAuthClientDiagnostics _diagnostics;
- private readonly NavigationManager _nav;
-
- public UAuthClient(
- IBrowserPostClient post,
- IOptions options,
- IOptions coreOptions,
- UAuthClientDiagnostics diagnostics,
- NavigationManager nav)
- {
- _post = post;
- _options = options.Value;
- _coreOptions = coreOptions.Value;
- _diagnostics = diagnostics;
- _nav = nav;
- }
-
- public async Task LoginAsync(LoginRequest request)
- {
- var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, _options.Endpoints.Login);
- await _post.NavigatePostAsync(url, request.ToDictionary());
- }
-
- public async Task LogoutAsync()
- {
- var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, _options.Endpoints.Logout);
- await _post.NavigatePostAsync(url);
- }
-
- public async Task RefreshAsync(bool isAuto = false)
- {
- if (isAuto == false)
- {
- _diagnostics.MarkManualRefresh();
- }
-
- var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, _options.Endpoints.Refresh);
- var result = await _post.FetchPostAsync(url);
- var refreshOutcome = RefreshOutcomeParser.Parse(result.RefreshOutcome);
- switch (refreshOutcome)
- {
- case RefreshOutcome.NoOp:
- _diagnostics.MarkRefreshNoOp();
- break;
- case RefreshOutcome.Touched:
- _diagnostics.MarkRefreshTouched();
- break;
- case RefreshOutcome.ReauthRequired:
- _diagnostics.MarkRefreshReauthRequired();
- break;
- case RefreshOutcome.None:
- _diagnostics.MarkRefreshUnknown();
- break;
- }
-
- return new RefreshResult
- {
- Ok = result.Ok,
- Status = result.Status,
- Outcome = refreshOutcome
- };
- }
-
- public async Task ReauthAsync()
- {
- var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, _options.Endpoints.Reauth);
- await _post.NavigatePostAsync(_options.Endpoints.Reauth);
- }
-
- public async Task ValidateAsync()
- {
- var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, _options.Endpoints.Validate);
- var raw = await _post.FetchPostJsonRawAsync(url);
-
- if (!raw.Ok || raw.Body is null)
- {
- return new AuthValidationResult
- {
- IsValid = false,
- State = "transport"
- };
- }
-
- var body = raw.Body.Value.Deserialize(
- new JsonSerializerOptions
- {
- PropertyNameCaseInsensitive = true
- });
-
- return body ?? new AuthValidationResult
- {
- IsValid = false,
- State = "deserialize"
- };
- }
-
- public async Task BeginPkceAsync(string? returnUrl = null)
- {
- var pkce = _options.Login.Pkce;
-
- if (!pkce.Enabled)
- throw new InvalidOperationException("PKCE login is disabled by configuration.");
-
- var verifier = CreateVerifier();
- var challenge = CreateChallenge(verifier);
-
- var authorizeUrl = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, _options.Endpoints.PkceAuthorize);
-
- var raw = await _post.FetchPostJsonRawAsync(
- authorizeUrl,
- new Dictionary
- {
- ["code_challenge"] = challenge,
- ["challenge_method"] = "S256"
- });
-
- if (!raw.Ok || raw.Body is null)
- throw new InvalidOperationException("PKCE authorize failed.");
+namespace CodeBeam.UltimateAuth.Client;
- var response = raw.Body.Value.Deserialize(
- new JsonSerializerOptions{ PropertyNameCaseInsensitive = true });
-
- if (response is null || string.IsNullOrWhiteSpace(response.AuthorizationCode))
- throw new InvalidOperationException("Invalid PKCE authorize response.");
-
- if (pkce.OnAuthorized is not null)
- await pkce.OnAuthorized(response);
-
- var resolvedReturnUrl = returnUrl
- ?? pkce.ReturnUrl
- ?? _options.Login.DefaultReturnUrl
- ?? _nav.Uri;
-
- if (pkce.AutoRedirect)
- {
- await NavigateToHubLoginAsync(response.AuthorizationCode, verifier, resolvedReturnUrl);
- }
- }
-
- public async Task CompletePkceLoginAsync(PkceLoginRequest request)
- {
- if (request is null)
- throw new ArgumentNullException(nameof(request));
-
- var url = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, _options.Endpoints.PkceComplete);
-
- var payload = new Dictionary
- {
- ["authorization_code"] = request.AuthorizationCode,
- ["code_verifier"] = request.CodeVerifier,
- ["return_url"] = request.ReturnUrl,
-
- ["Identifier"] = request.Identifier ?? string.Empty,
- ["Secret"] = request.Secret ?? string.Empty
- };
-
- await _post.NavigatePostAsync(url, payload);
- }
-
- private Task NavigateToHubLoginAsync(string authorizationCode, string codeVerifier, string returnUrl)
- {
- var hubLoginUrl = UAuthUrlBuilder.Combine(_options.Endpoints.Authority, _options.Endpoints.HubLoginPath);
-
- var data = new Dictionary
- {
- ["authorization_code"] = authorizationCode,
- ["code_verifier"] = codeVerifier,
- ["return_url"] = returnUrl,
- ["client_profile"] = _coreOptions.ClientProfile.ToString()
- };
-
- return _post.NavigatePostAsync(hubLoginUrl, data);
- }
-
-
- // ---------------- PKCE CRYPTO ----------------
-
- private static string CreateVerifier()
- {
- var bytes = RandomNumberGenerator.GetBytes(32);
- return Base64UrlEncode(bytes);
- }
-
- private static string CreateChallenge(string verifier)
- {
- using var sha256 = SHA256.Create();
- var hash = sha256.ComputeHash(Encoding.ASCII.GetBytes(verifier));
- return Base64UrlEncode(hash);
- }
-
- private static string Base64UrlEncode(byte[] input)
- {
- return Convert.ToBase64String(input)
- .TrimEnd('=')
- .Replace('+', '-')
- .Replace('/', '_');
- }
+internal sealed class UAuthClient : IUAuthClient
+{
+ public IFlowClient Flows { get; }
+ public IUserClient Users { get; }
+ public UAuthClient(IFlowClient flows, IUserClient users)
+ {
+ Flows = flows;
+ Users = users;
}
}
diff --git a/src/CodeBeam.UltimateAuth.Client/wwwroot/uauth.js b/src/CodeBeam.UltimateAuth.Client/wwwroot/uauth.js
index 94c26bc..0aa48f7 100644
--- a/src/CodeBeam.UltimateAuth.Client/wwwroot/uauth.js
+++ b/src/CodeBeam.UltimateAuth.Client/wwwroot/uauth.js
@@ -100,7 +100,8 @@ window.uauth.post = async function (options) {
}
const headers = {
"X-UDID": window.uauth.deviceId,
- "X-UAuth-ClientProfile": clientProfile
+ "X-UAuth-ClientProfile": clientProfile,
+ "X-Requested-With": "UAuth"
};
if (data) {
@@ -136,6 +137,47 @@ window.uauth.post = async function (options) {
};
};
+window.uauth.postJson = async function (options) {
+ const {
+ url,
+ payload,
+ clientProfile
+ } = options;
+
+ if (!window.uauth.deviceId) {
+ throw new Error("UAuth deviceId is not initialized.");
+ }
+
+ const headers = {
+ "Content-Type": "application/json",
+ "X-UDID": window.uauth.deviceId,
+ "X-UAuth-ClientProfile": clientProfile ?? "",
+ "X-Requested-With": "UAuth"
+ };
+
+ const response = await fetch(url, {
+ method: "POST",
+ credentials: "include",
+ headers: headers,
+ body: payload ? JSON.stringify(payload) : null
+ });
+
+ let responseBody = null;
+
+ try {
+ responseBody = await response.json();
+ } catch {
+ responseBody = null;
+ }
+
+ return {
+ ok: response.ok,
+ status: response.status,
+ refreshOutcome: response.headers.get("X-UAuth-Refresh"),
+ body: responseBody
+ };
+};
+
window.uauth.setDeviceId = function (value) {
window.uauth.deviceId = value;
};
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..dd008ec
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Authority/AccessContext.cs
@@ -0,0 +1,47 @@
+using CodeBeam.UltimateAuth.Core.Domain;
+using System.Collections;
+
+namespace CodeBeam.UltimateAuth.Core.Contracts
+{
+ public sealed class AccessContext
+ {
+ // Actor
+ public UserKey? ActorUserKey { get; init; }
+ public string? ActorTenantId { get; init; }
+ public bool IsAuthenticated { get; init; }
+ public bool IsSystemActor { get; init; }
+
+ // Target
+ public string? Resource { get; init; }
+ public string? ResourceId { get; init; }
+ public string? ResourceTenantId { get; init; }
+
+ public string Action { get; init; } = default!;
+ public IReadOnlyDictionary Attributes { get; init; } = EmptyAttributes.Instance;
+
+ public bool IsCrossTenant => ActorTenantId != null && ResourceTenantId != null && !string.Equals(ActorTenantId, ResourceTenantId, StringComparison.Ordinal);
+ public bool IsSelfAction => ActorUserKey != null && ResourceId != null && string.Equals(ActorUserKey.Value, ResourceId, StringComparison.Ordinal);
+ public bool HasActor => ActorUserKey != null;
+ public bool HasTarget => ResourceId != null;
+ }
+
+ internal sealed class EmptyAttributes : IReadOnlyDictionary
+ {
+ public static readonly EmptyAttributes Instance = new();
+
+ private EmptyAttributes() { }
+
+ public IEnumerable Keys => Array.Empty();
+ public IEnumerable
- 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/Auth/Context/DefaultAccessContextFactory.cs b/src/CodeBeam.UltimateAuth.Server/Auth/Context/DefaultAccessContextFactory.cs
new file mode 100644
index 0000000..9ff7b68
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Auth/Context/DefaultAccessContextFactory.cs
@@ -0,0 +1,53 @@
+using CodeBeam.UltimateAuth.Authorization;
+using CodeBeam.UltimateAuth.Core.Contracts;
+using System.Collections.ObjectModel;
+
+namespace CodeBeam.UltimateAuth.Server.Auth
+{
+ internal sealed class DefaultAccessContextFactory : IAccessContextFactory
+ {
+ private readonly IUserRoleStore _roleStore;
+
+ public DefaultAccessContextFactory(IUserRoleStore roleStore)
+ {
+ _roleStore = roleStore;
+ }
+
+ public async Task CreateAsync(AuthFlowContext authFlow, string action, string resource, string? resourceId = null, string? resourceTenantId = null, IDictionary? attributes = null, CancellationToken ct = default)
+ {
+ if (string.IsNullOrWhiteSpace(action))
+ throw new ArgumentException("Action is required.", nameof(action));
+
+ if (string.IsNullOrWhiteSpace(resource))
+ throw new ArgumentException("Resource is required.", nameof(resource));
+
+ var attrs = attributes is not null
+ ? new Dictionary(attributes)
+ : new Dictionary();
+
+ if (authFlow.IsAuthenticated && authFlow.UserKey is not null)
+ {
+ var roles = await _roleStore.GetRolesAsync(authFlow.TenantId, authFlow.UserKey.Value, ct);
+ attrs["roles"] = roles;
+ }
+
+ return new AccessContext
+ {
+ ActorUserKey = authFlow.UserKey,
+ ActorTenantId = authFlow.TenantId,
+ IsAuthenticated = authFlow.IsAuthenticated,
+ IsSystemActor = false,
+
+ Resource = resource,
+ ResourceId = resourceId,
+ ResourceTenantId = resourceTenantId ?? authFlow.TenantId,
+
+ Action = action,
+
+ Attributes = attrs.Count > 0
+ ? new ReadOnlyDictionary(attrs)
+ : EmptyAttributes.Instance
+ };
+ }
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/Context/DefaultAuthContextFactory.cs b/src/CodeBeam.UltimateAuth.Server/Auth/Context/DefaultAuthContextFactory.cs
new file mode 100644
index 0000000..8d11f2b
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Auth/Context/DefaultAuthContextFactory.cs
@@ -0,0 +1,24 @@
+using CodeBeam.UltimateAuth.Core.Abstractions;
+using CodeBeam.UltimateAuth.Core.Contracts;
+using CodeBeam.UltimateAuth.Server.Extensions;
+
+namespace CodeBeam.UltimateAuth.Server.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/Auth/Context/DefaultAuthFlow.cs b/src/CodeBeam.UltimateAuth.Server/Auth/Context/DefaultAuthFlow.cs
index ca13cf1..2340fd3 100644
--- a/src/CodeBeam.UltimateAuth.Server/Auth/Context/DefaultAuthFlow.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Auth/Context/DefaultAuthFlow.cs
@@ -9,10 +9,7 @@ internal sealed class DefaultAuthFlow : IAuthFlow
private readonly IAuthFlowContextFactory _factory;
private readonly DefaultAuthFlowContextAccessor _accessor;
- public DefaultAuthFlow(
- IHttpContextAccessor http,
- IAuthFlowContextFactory factory,
- IAuthFlowContextAccessor accessor)
+ public DefaultAuthFlow(IHttpContextAccessor http, IAuthFlowContextFactory factory, IAuthFlowContextAccessor accessor)
{
_http = http;
_factory = factory;
diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/Context/IAccessContextFactory.cs b/src/CodeBeam.UltimateAuth.Server/Auth/Context/IAccessContextFactory.cs
new file mode 100644
index 0000000..4bc5aaa
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Auth/Context/IAccessContextFactory.cs
@@ -0,0 +1,9 @@
+using CodeBeam.UltimateAuth.Core.Contracts;
+
+namespace CodeBeam.UltimateAuth.Server.Auth
+{
+ public interface IAccessContextFactory
+ {
+ Task CreateAsync(AuthFlowContext authFlow, string action, string resource, string? resourceId = null, string? resourceTenantId = null, IDictionary? attributes = null, CancellationToken ct = default);
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Authentication/UAuthAuthenticationExtension.cs b/src/CodeBeam.UltimateAuth.Server/Authentication/UAuthAuthenticationExtension.cs
index 5f47858..a2fcc73 100644
--- a/src/CodeBeam.UltimateAuth.Server/Authentication/UAuthAuthenticationExtension.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Authentication/UAuthAuthenticationExtension.cs
@@ -1,4 +1,5 @@
-using Microsoft.AspNetCore.Authentication;
+using CodeBeam.UltimateAuth.Server.Defaults;
+using Microsoft.AspNetCore.Authentication;
namespace CodeBeam.UltimateAuth.Server.Authentication;
diff --git a/src/CodeBeam.UltimateAuth.Server/Authentication/UAuthAuthenticationHandler.cs b/src/CodeBeam.UltimateAuth.Server/Authentication/UAuthAuthenticationHandler.cs
index bccddfa..107c95d 100644
--- a/src/CodeBeam.UltimateAuth.Server/Authentication/UAuthAuthenticationHandler.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Authentication/UAuthAuthenticationHandler.cs
@@ -1,8 +1,10 @@
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.Defaults;
using CodeBeam.UltimateAuth.Server.Infrastructure;
+using CodeBeam.UltimateAuth.Server.Services;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ -55,32 +57,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..a834035 100644
--- a/src/CodeBeam.UltimateAuth.Server/CodeBeam.UltimateAuth.Server.csproj
+++ b/src/CodeBeam.UltimateAuth.Server/CodeBeam.UltimateAuth.Server.csproj
@@ -18,11 +18,14 @@
+
+
-
-
-
-
+
+
+
+
+
diff --git a/src/CodeBeam.UltimateAuth.Server/Defaults/UAuthActions.cs b/src/CodeBeam.UltimateAuth.Server/Defaults/UAuthActions.cs
new file mode 100644
index 0000000..111ab05
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Defaults/UAuthActions.cs
@@ -0,0 +1,49 @@
+namespace CodeBeam.UltimateAuth.Server.Defaults
+{
+ public static class UAuthActions
+ {
+ public static class Users
+ {
+ public const string Create = "users.create";
+ public const string Delete = "users.delete";
+ public const string ChangeStatus = "users.status.change";
+ }
+
+ public static class UserProfiles
+ {
+ public const string GetSelf = "users.profile.get.self";
+ public const string UpdateSelf = "users.profile.update.self";
+ public const string GetAdmin = "users.profile.get.admin";
+ public const string UpdateAdmin = "users.profile.update.admin";
+ }
+
+ public static class UserIdentifiers
+ {
+ public const string Get = "users.identifiers.get";
+ public const string Change = "users.identifiers.change";
+ public const string Verify = "users.identifiers.verify";
+ public const string Delete = "users.identifiers.delete";
+ }
+
+ public static class Credentials
+ {
+ public const string List = "credentials.list";
+ public const string Add = "credentials.add";
+ public const string Change = "credentials.change";
+ public const string Revoke = "credentials.revoke";
+ public const string Activate = "credentials.activate";
+ public const string Delete = "credentials.delete";
+ }
+
+ public static class Authorization
+ {
+ public static class Roles
+ {
+ public const string Read = "authorization.roles.read";
+ public const string Assign = "authorization.roles.assign";
+ public const string Remove = "authorization.roles.remove";
+ }
+ }
+
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Authentication/UAuthCookieDefaults.cs b/src/CodeBeam.UltimateAuth.Server/Defaults/UAuthCookieDefaults.cs
similarity index 65%
rename from src/CodeBeam.UltimateAuth.Server/Authentication/UAuthCookieDefaults.cs
rename to src/CodeBeam.UltimateAuth.Server/Defaults/UAuthCookieDefaults.cs
index 0fe1e11..3f745cf 100644
--- a/src/CodeBeam.UltimateAuth.Server/Authentication/UAuthCookieDefaults.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Defaults/UAuthCookieDefaults.cs
@@ -1,4 +1,4 @@
-namespace CodeBeam.UltimateAuth.Server.Authentication;
+namespace CodeBeam.UltimateAuth.Server.Defaults;
public static class UAuthCookieDefaults
{
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..352e65e
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/ICredentialEndpointHandler.cs
@@ -0,0 +1,14 @@
+using Microsoft.AspNetCore.Http;
+
+namespace CodeBeam.UltimateAuth.Server.Endpoints
+{
+ public interface ICredentialEndpointHandler
+ {
+ Task GetAllAsync(HttpContext ctx);
+ Task AddAsync(HttpContext ctx);
+ Task ChangeAsync(string type, HttpContext ctx);
+ Task RevokeAsync(string type, HttpContext ctx);
+ Task ActivateAsync(string type, HttpContext ctx);
+ Task DeleteAsync(string type, HttpContext ctx);
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IUserLifecycleEndpointHandler.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IUserLifecycleEndpointHandler.cs
new file mode 100644
index 0000000..08cdbe6
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IUserLifecycleEndpointHandler.cs
@@ -0,0 +1,11 @@
+using Microsoft.AspNetCore.Http;
+
+namespace CodeBeam.UltimateAuth.Server.Endpoints
+{
+ public interface IUserLifecycleEndpointHandler
+ {
+ 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..aed3688 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;
@@ -79,10 +79,10 @@ public void MapEndpoints(RouteGroupBuilder rootGroup, UAuthServerOptions options
{
var session = group.MapGroup("/session");
- session.MapGet("/current", async ([FromServices] ISessionManagementHandler h, HttpContext ctx)
+ session.MapPost("/current", async ([FromServices] ISessionManagementHandler h, HttpContext ctx)
=> await h.GetCurrentSessionAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.QuerySession));
- session.MapGet("/list", async ([FromServices] ISessionManagementHandler h, HttpContext ctx)
+ session.MapPost("/list", async ([FromServices] ISessionManagementHandler h, HttpContext ctx)
=> await h.GetAllSessionsAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.QuerySession));
session.MapPost("/revoke/{sessionId}", async ([FromServices] ISessionManagementHandler h, string sessionId, HttpContext ctx)
@@ -96,15 +96,93 @@ public void MapEndpoints(RouteGroupBuilder rootGroup, UAuthServerOptions options
{
var user = group.MapGroup("");
- user.MapGet("/userinfo", async ([FromServices] IUserInfoEndpointHandler h, HttpContext ctx)
+ user.MapPost("/userinfo", async ([FromServices] IUserInfoEndpointHandler h, HttpContext ctx)
=> await h.GetUserInfoAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserInfo));
- user.MapGet("/permissions", async ([FromServices] IUserInfoEndpointHandler h, HttpContext ctx)
+ user.MapPost("/permissions", async ([FromServices] IUserInfoEndpointHandler h, HttpContext ctx)
=> await h.GetPermissionsAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.PermissionQuery));
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("/create", async ([FromServices] IUserLifecycleEndpointHandler h, HttpContext ctx)
+ => await h.CreateAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserManagement));
+
+ users.MapPost("/status", async ([FromServices] IUserLifecycleEndpointHandler h, HttpContext ctx)
+ => await h.ChangeStatusAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserManagement));
+
+ // Post is intended for Auth
+ users.MapPost("/delete", async ([FromServices] IUserLifecycleEndpointHandler h, HttpContext ctx)
+ => await h.DeleteAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserManagement));
+ }
+
+ if (options.EnableUserProfileEndpoints)
+ {
+ var userProfile = group.MapGroup("/users");
+
+ userProfile.MapPost("/me/get", async ([FromServices] IUserProfileEndpointHandler h, HttpContext ctx)
+ => await h.GetAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserProfile));
+
+ userProfile.MapPost("/me/update", 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.MapPost("/{userKey}/profile/get", async ([FromServices] IUserProfileAdminEndpointHandler h, UserKey userKey, HttpContext ctx)
+ => await h.GetAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserManagement));
+
+ admin.MapPost("/{userKey}/profile/update", 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("/get", async ([FromServices] ICredentialEndpointHandler h, HttpContext ctx)
+ => await h.GetAllAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement));
+
+ credentials.MapPost("/post", async ([FromServices] ICredentialEndpointHandler h, HttpContext ctx)
+ => await h.AddAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement));
+
+ credentials.MapPost("/update/{type}", async ([FromServices] ICredentialEndpointHandler h, string type, HttpContext ctx)
+ => await h.ChangeAsync(type, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement));
+
+ credentials.MapPost("/{type}/revoke", async ([FromServices] ICredentialEndpointHandler h, string type, HttpContext ctx)
+ => await h.RevokeAsync(type, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement));
+
+ credentials.MapPost("/{type}/activate", async ([FromServices] ICredentialEndpointHandler h, string type, HttpContext ctx)
+ => await h.ActivateAsync(type, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement));
+
+ credentials.MapPost("/delete/{type}", async ([FromServices] ICredentialEndpointHandler h, string type, HttpContext ctx)
+ => await h.DeleteAsync(type, 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/get", async ([FromServices] IAuthorizationEndpointHandler h, UserKey userKey, HttpContext ctx)
+ => await h.GetRolesAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.AuthorizationManagement));
+
+ authz.MapPost("/users/{userKey}/roles/post", async ([FromServices] IAuthorizationEndpointHandler h, UserKey userKey, HttpContext ctx)
+ => await h.AssignRoleAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.AuthorizationManagement));
+
+ authz.MapPost("/users/{userKey}/roles/delete", 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..2dc154b 100644
--- a/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthServerServiceCollectionExtensions.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthServerServiceCollectionExtensions.cs
@@ -1,9 +1,15 @@
-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.Policies.Abstractions;
+using CodeBeam.UltimateAuth.Policies.Defaults;
+using CodeBeam.UltimateAuth.Policies.Registry;
using CodeBeam.UltimateAuth.Server.Abstactions;
using CodeBeam.UltimateAuth.Server.Abstractions;
using CodeBeam.UltimateAuth.Server.Auth;
@@ -13,11 +19,12 @@
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 Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
@@ -31,12 +38,18 @@ public static class UAuthServerServiceCollectionExtensions
public static IServiceCollection AddUltimateAuthServer(this IServiceCollection services)
{
services.AddUltimateAuth();
+ AddUsersInternal(services);
+ AddCredentialsInternal(services);
+ AddUltimateAuthPolicies(services);
return services.AddUltimateAuthServerInternal();
}
public static IServiceCollection AddUltimateAuthServer(this IServiceCollection services, IConfiguration configuration)
{
services.AddUltimateAuth(configuration);
+ AddUsersInternal(services);
+ AddCredentialsInternal(services);
+ AddUltimateAuthPolicies(services);
services.Configure(configuration.GetSection("UltimateAuth:Server"));
return services.AddUltimateAuthServerInternal();
@@ -45,6 +58,9 @@ public static IServiceCollection AddUltimateAuthServer(this IServiceCollection s
public static IServiceCollection AddUltimateAuthServer(this IServiceCollection services, Action configure)
{
services.AddUltimateAuth();
+ AddUsersInternal(services);
+ AddCredentialsInternal(services);
+ AddUltimateAuthPolicies(services);
services.Configure(configure);
return services.AddUltimateAuthServerInternal();
@@ -123,16 +139,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 +169,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 +196,124 @@ 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.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