@@ -35,7 +38,10 @@
- Preparing
+ Home
+ Docs
+ Samples
+ Donate
diff --git a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/Pages/DocsLandingPage.razor b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/Pages/DocsLandingPage.razor
new file mode 100644
index 00000000..be6a91bc
--- /dev/null
+++ b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/Pages/DocsLandingPage.razor
@@ -0,0 +1,30 @@
+@page "/docs"
+
+
+
+
+ UltimateAuth Docs
+ The modern way to understand authentication — unified, simple and powerful.
+
+
+
+ This page is preparing.
+
+
+ But the documentation is currently available in markdown format and covers core concepts, architecture, flows and integration guides.
+
+
+
+ Start exploring the docs to understand how UltimateAuth simplifies authentication across sessions, cookies and tokens.
+
+
+
+ Read Documentation
+
+
+
+ Full documentation site is coming soon.
+
+
+
+
\ No newline at end of file
diff --git a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/Pages/Home.razor b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/Pages/Home.razor
index b6b11844..5be34301 100644
--- a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/Pages/Home.razor
+++ b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/Pages/Home.razor
@@ -92,12 +92,13 @@
+
- UltimateAuth is and will always forever free.
+ UltimateAuth is, and will always remain free.No licenses. No tiers. No hidden limits. Full features as a self-hosted framework.
But building and maintaining a secure, production-ready Auth framework takes time, effort and care.
@@ -135,7 +136,13 @@
"Override any part of the pipeline without breaking security boundaries."),
new(Icons.Material.Outlined.Category, "Developer First",
- "Clean APIs, Blazor native design and seamless integration with .NET ecosystem.")
+ "Clean APIs, Blazor native design and seamless integration with .NET ecosystem."),
+
+ new(Icons.Material.Outlined.AccountTree, "Enterprise-Ready Foundations",
+ "Built-in multi-tenancy, soft delete and optimistic concurrency support. Designed with real-world data integrity and scalability in mind."),
+
+ new(Icons.Material.Outlined.Groups, "Advanced User Management",
+ "Support for multi-profile and multi-identifier users with a full lifecycle model. Go beyond basic identity systems with flexible, real-world user modeling.")
};
record Principle(string Icon, string Title, string Description);
diff --git a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/Pages/NotFound.razor b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/Pages/NotFound.razor
index 917ada1d..99f00c84 100644
--- a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/Pages/NotFound.razor
+++ b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/Pages/NotFound.razor
@@ -1,5 +1,11 @@
@page "/not-found"
@layout MainLayout
-
Not Found
-
Sorry, the content you are looking for does not exist.
\ No newline at end of file
+
+
+
+
Not Found
+
Sorry, the content you are looking for does not exist.
+
+
+
diff --git a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/Pages/Samples.razor b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/Pages/Samples.razor
new file mode 100644
index 00000000..a1974cbd
--- /dev/null
+++ b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/Pages/Samples.razor
@@ -0,0 +1,73 @@
+@page "/samples"
+
+
+
+
+ Samples
+ Explore working examples and see UltimateAuth in action.
+ Interactive online samples are coming soon.
+
+
+
+
+ All samples are available on GitHub. Clone the repository, open the solution
+ and run the projects to play with real-world UltimateAuth implementation.
+
+
+
+
+
+ Blazor Server (EF Core)
+ Full-featured sample with persistent storage using EF Core.
+ View Source Code on GitHub
+
+
+
+
+
+ Blazor Server (InMemory)
+ Lightweight sample for quick setup without persistence.
+ View Source Code on GitHub
+
+
+
+
+
+ Blazor WASM (Standalone)
+ Client-side sample using Hybrid authentication model.
+ View Source Code on GitHub
+
+
+
+
+
+ UAuthHub
+ Complete Auth server with UI, working together with WASM client.
+ View Source Code on GitHub
+
+
+
+
+
+ Resource API
+ Example of securing application APIs using UltimateAuth.
+ View Source Code on GitHub
+
+
+
+
+
+ WASM Sample Setup:
+ Start the following projects together:
+ (1) UAuthHub
+ (2) ResourceApi
+ (3) BlazorStandaloneWasm
+ After opening the solution, configure multiple startup projects in Visual Studio.
+
+
+
+ Each sample refreshes (clear + seed) it's data on each restart. When you close application it will be reset.
+
+
+
+
diff --git a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.csproj b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.csproj
index d53a6763..300b60ef 100644
--- a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.csproj
+++ b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.csproj
@@ -1,15 +1,16 @@
-
- net10.0
- enable
- enable
- true
-
+
+ net10.0
+ enable
+ enable
+ true
+ false
+
-
-
-
-
+
+
+
+
diff --git a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm/Components/App.razor b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm/Components/App.razor
index fd0d0c18..4c724313 100644
--- a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm/Components/App.razor
+++ b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm/Components/App.razor
@@ -5,6 +5,22 @@
+
+ UltimateAuth | Modern Auth Framework for .NET
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@* *@
diff --git a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm/wwwroot/app.css b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm/wwwroot/app.css
index 32c66ecc..9eede6fb 100644
--- a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm/wwwroot/app.css
+++ b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm/wwwroot/app.css
@@ -95,7 +95,6 @@ h1:focus {
display: flex;
justify-content: flex-end;
align-items: center;
- gap: 4px;
}
.ua-gradient {
diff --git a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm/wwwroot/favicon.png b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm/wwwroot/favicon.png
deleted file mode 100644
index 8422b596..00000000
Binary files a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm/wwwroot/favicon.png and /dev/null differ
diff --git a/samples/CodeBeam.UltimateAuth.Sample.Seed/UserSeedContributor.cs b/samples/CodeBeam.UltimateAuth.Sample.Seed/UserSeedContributor.cs
index 5d285880..3dde6696 100644
--- a/samples/CodeBeam.UltimateAuth.Sample.Seed/UserSeedContributor.cs
+++ b/samples/CodeBeam.UltimateAuth.Sample.Seed/UserSeedContributor.cs
@@ -59,11 +59,11 @@ await lifecycleStore.AddAsync(
ct);
}
- var profileKey = new UserProfileKey(tenant, userKey);
+ var profileKey = new UserProfileKey(tenant, userKey, ProfileKey.Default);
if (!await profileStore.ExistsAsync(profileKey, ct))
{
await profileStore.AddAsync(
- UserProfile.Create(Guid.NewGuid(), tenant, userKey, now, displayName: displayName),
+ UserProfile.Create(Guid.NewGuid(), tenant, userKey, ProfileKey.Default, now, displayName: displayName),
ct);
}
diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Program.cs b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Program.cs
index 9108573e..9784acb8 100644
--- a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Program.cs
+++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Program.cs
@@ -38,6 +38,7 @@
o.Login.MaxFailedAttempts = 2;
o.Login.LockoutDuration = TimeSpan.FromSeconds(10);
o.Identifiers.AllowMultipleUsernames = true;
+ o.UserProfile.EnableMultiProfile = true;
})
.AddUltimateAuthInMemory()
.AddUAuthHub(o => o.AllowedClientOrigins.Add("https://localhost:6130")); // Client sample's URL
diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/UsersDialog.razor.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/UsersDialog.razor.cs
index e6807ab6..3f963991 100644
--- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/UsersDialog.razor.cs
+++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/UsersDialog.razor.cs
@@ -144,7 +144,7 @@ Are you sure you want to delete {user.DisplayName ?? user.UserName ?? user.Pr
Mode = DeleteMode.Soft
};
- var result = await UAuthClient.Users.DeleteUserAsync(UserKey.Parse(user.UserKey, null), req);
+ var result = await UAuthClient.Users.DeleteUserAsync(user.UserKey, req);
if (result.IsSuccess)
{
diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20260327184128_InitUltimateAuth.Designer.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20260406192328_InitUltimateAuth.Designer.cs
similarity index 98%
rename from samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20260327184128_InitUltimateAuth.Designer.cs
rename to samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20260406192328_InitUltimateAuth.Designer.cs
index 079580bb..1302213b 100644
--- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20260327184128_InitUltimateAuth.Designer.cs
+++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20260406192328_InitUltimateAuth.Designer.cs
@@ -11,7 +11,7 @@
namespace CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.Migrations
{
[DbContext(typeof(UAuthDbContext))]
- [Migration("20260327184128_InitUltimateAuth")]
+ [Migration("20260406192328_InitUltimateAuth")]
partial class InitUltimateAuth
{
///
@@ -658,6 +658,11 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder)
b.Property("Metadata")
.HasColumnType("TEXT");
+ b.Property("ProfileKey")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
b.Property("Tenant")
.IsRequired()
.HasMaxLength(128)
@@ -680,7 +685,8 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder)
b.HasKey("Id");
- b.HasIndex("Tenant", "UserKey");
+ b.HasIndex("Tenant", "UserKey", "ProfileKey")
+ .IsUnique();
b.ToTable("UAuth_UserProfiles", (string)null);
});
diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20260327184128_InitUltimateAuth.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20260406192328_InitUltimateAuth.cs
similarity index 99%
rename from samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20260327184128_InitUltimateAuth.cs
rename to samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20260406192328_InitUltimateAuth.cs
index 9f373138..bd4101fd 100644
--- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20260327184128_InitUltimateAuth.cs
+++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20260406192328_InitUltimateAuth.cs
@@ -182,6 +182,7 @@ protected override void Up(MigrationBuilder migrationBuilder)
Id = table.Column(type: "TEXT", nullable: false),
Tenant = table.Column(type: "TEXT", maxLength: 128, nullable: false),
UserKey = table.Column(type: "TEXT", maxLength: 128, nullable: false),
+ ProfileKey = table.Column(type: "TEXT", maxLength: 64, nullable: false),
FirstName = table.Column(type: "TEXT", nullable: true),
LastName = table.Column(type: "TEXT", nullable: true),
DisplayName = table.Column(type: "TEXT", nullable: true),
@@ -498,9 +499,10 @@ protected override void Up(MigrationBuilder migrationBuilder)
unique: true);
migrationBuilder.CreateIndex(
- name: "IX_UAuth_UserProfiles_Tenant_UserKey",
+ name: "IX_UAuth_UserProfiles_Tenant_UserKey_ProfileKey",
table: "UAuth_UserProfiles",
- columns: new[] { "Tenant", "UserKey" });
+ columns: new[] { "Tenant", "UserKey", "ProfileKey" },
+ unique: true);
migrationBuilder.CreateIndex(
name: "IX_UAuth_UserRoles_Tenant_RoleId",
diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/UAuthDbContextModelSnapshot.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/UAuthDbContextModelSnapshot.cs
index 211ef12e..af13077f 100644
--- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/UAuthDbContextModelSnapshot.cs
+++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/UAuthDbContextModelSnapshot.cs
@@ -655,6 +655,11 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.Property("Metadata")
.HasColumnType("TEXT");
+ b.Property("ProfileKey")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
b.Property("Tenant")
.IsRequired()
.HasMaxLength(128)
@@ -677,7 +682,8 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.HasKey("Id");
- b.HasIndex("Tenant", "UserKey");
+ b.HasIndex("Tenant", "UserKey", "ProfileKey")
+ .IsUnique();
b.ToTable("UAuth_UserProfiles", (string)null);
});
diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db
index 4e86411b..b911a93d 100644
Binary files a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db and b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db differ
diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db-shm b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db-shm
deleted file mode 100644
index 9c25688e..00000000
Binary files a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db-shm and /dev/null differ
diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db-wal b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db-wal
deleted file mode 100644
index 42498f65..00000000
Binary files a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db-wal and /dev/null differ
diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/UsersDialog.razor.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/UsersDialog.razor.cs
index 3168085d..5d184fe7 100644
--- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/UsersDialog.razor.cs
+++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Dialogs/UsersDialog.razor.cs
@@ -144,7 +144,7 @@ Are you sure you want to delete {user.DisplayName ?? user.UserName ?? user.Pr
Mode = DeleteMode.Soft
};
- var result = await UAuthClient.Users.DeleteUserAsync(UserKey.Parse(user.UserKey, null), req);
+ var result = await UAuthClient.Users.DeleteUserAsync(user.UserKey, req);
if (result.IsSuccess)
{
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 06ddc3f2..a77dd098 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
@@ -4,7 +4,6 @@
net10.0enableenable
- 0.1.0false
diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Infrastructure/IUAuthPasswordHasher.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/Infrastructure/IUAuthPasswordHasher.cs
index 039a8216..82946e79 100644
--- a/src/CodeBeam.UltimateAuth.Core/Abstractions/Infrastructure/IUAuthPasswordHasher.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Abstractions/Infrastructure/IUAuthPasswordHasher.cs
@@ -7,6 +7,7 @@
///
public interface IUAuthPasswordHasher
{
- string Hash(string password);
- bool Verify(string hash, string secret);
+ PasswordHash Hash(string password);
+ bool Verify(PasswordHash hash, string secret);
+ bool NeedsRehash(PasswordHash hash);
}
diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Principals/IUserIdConverter.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/User/IUserIdConverter.cs
similarity index 100%
rename from src/CodeBeam.UltimateAuth.Core/Abstractions/Principals/IUserIdConverter.cs
rename to src/CodeBeam.UltimateAuth.Core/Abstractions/User/IUserIdConverter.cs
diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Principals/IUserIdConverterResolver.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/User/IUserIdConverterResolver.cs
similarity index 100%
rename from src/CodeBeam.UltimateAuth.Core/Abstractions/Principals/IUserIdConverterResolver.cs
rename to src/CodeBeam.UltimateAuth.Core/Abstractions/User/IUserIdConverterResolver.cs
diff --git a/src/CodeBeam.UltimateAuth.Core/Abstractions/Principals/IUserIdFactory.cs b/src/CodeBeam.UltimateAuth.Core/Abstractions/User/IUserIdFactory.cs
similarity index 100%
rename from src/CodeBeam.UltimateAuth.Core/Abstractions/Principals/IUserIdFactory.cs
rename to src/CodeBeam.UltimateAuth.Core/Abstractions/User/IUserIdFactory.cs
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Login/ExternalLoginRequest.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Login/ExternalLoginRequest.cs
index 32da871e..64f2be08 100644
--- a/src/CodeBeam.UltimateAuth.Core/Contracts/Login/ExternalLoginRequest.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Login/ExternalLoginRequest.cs
@@ -1,5 +1,4 @@
using CodeBeam.UltimateAuth.Core.Domain;
-using CodeBeam.UltimateAuth.Core.MultiTenancy;
namespace CodeBeam.UltimateAuth.Core.Contracts;
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Login/ReauthRequest.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Login/ReauthRequest.cs
index f5089e7a..9ffe9ab8 100644
--- a/src/CodeBeam.UltimateAuth.Core/Contracts/Login/ReauthRequest.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Login/ReauthRequest.cs
@@ -1,5 +1,4 @@
using CodeBeam.UltimateAuth.Core.Domain;
-using CodeBeam.UltimateAuth.Core.MultiTenancy;
namespace CodeBeam.UltimateAuth.Core.Contracts;
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Pkce/PkceCompleteRequest.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Pkce/PkceCompleteRequest.cs
index b951e1ec..7c057ff2 100644
--- a/src/CodeBeam.UltimateAuth.Core/Contracts/Pkce/PkceCompleteRequest.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Pkce/PkceCompleteRequest.cs
@@ -15,8 +15,8 @@ public sealed record PkceCompleteRequest
public required string Secret { get; init; }
[JsonPropertyName("return_url")]
- public string ReturnUrl { get; init; }
+ public string? ReturnUrl { get; init; }
[JsonPropertyName("hub_session_id")]
- public string HubSessionId { get; init; }
+ public string? HubSessionId { get; init; }
}
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Refresh/RefreshStrategy.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Refresh/RefreshStrategy.cs
deleted file mode 100644
index 731248f6..00000000
--- a/src/CodeBeam.UltimateAuth.Core/Contracts/Refresh/RefreshStrategy.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-namespace CodeBeam.UltimateAuth.Core.Contracts;
-
-public enum RefreshStrategy
-{
- NotSupported = 0,
- SessionOnly = 10, // PureOpaque
- TokenOnly = 20, // PureJwt
- TokenWithSessionCheck = 30, // SemiHybrid
- SessionAndToken = 40 // Hybrid
-}
diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Session/SessionRefreshStatus.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Refresh/SessionRefreshStatus.cs
similarity index 70%
rename from src/CodeBeam.UltimateAuth.Core/Domain/Session/SessionRefreshStatus.cs
rename to src/CodeBeam.UltimateAuth.Core/Contracts/Refresh/SessionRefreshStatus.cs
index 16f23a04..41ed8a4e 100644
--- a/src/CodeBeam.UltimateAuth.Core/Domain/Session/SessionRefreshStatus.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Refresh/SessionRefreshStatus.cs
@@ -1,4 +1,4 @@
-namespace CodeBeam.UltimateAuth.Core.Domain;
+namespace CodeBeam.UltimateAuth.Core.Contracts;
public enum SessionRefreshStatus
{
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/Dtos/IdentityInfo.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/Dtos/IdentityInfo.cs
index 4f008972..c7488e64 100644
--- a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/Dtos/IdentityInfo.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/Dtos/IdentityInfo.cs
@@ -7,5 +7,4 @@ public sealed class IdentityInfo
public string? UserKey { get; set; }
public DateTimeOffset? AuthenticatedAt { get; set; }
-
}
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionStoreContext.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionStoreContext.cs
deleted file mode 100644
index 3bcda73b..00000000
--- a/src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionStoreContext.cs
+++ /dev/null
@@ -1,43 +0,0 @@
-using CodeBeam.UltimateAuth.Core.Domain;
-using CodeBeam.UltimateAuth.Core.MultiTenancy;
-
-namespace CodeBeam.UltimateAuth.Core.Contracts;
-
-///
-/// Context information required by the session store when
-/// creating or rotating sessions.
-///
-public sealed class SessionStoreContext
-{
- ///
- /// The authenticated user identifier.
- ///
- public required UserKey UserKey { get; init; }
-
- ///
- /// The tenant identifier, if multi-tenancy is enabled.
- ///
- public TenantKey Tenant { get; init; }
-
- ///
- /// Optional chain identifier.
- /// If null, a new chain should be created.
- ///
- public SessionChainId? ChainId { get; init; }
-
- ///
- /// Indicates whether the session is metadata-only
- /// (used in SemiHybrid mode).
- ///
- public bool IsMetadataOnly { get; init; }
-
- ///
- /// The UTC timestamp when the session was issued.
- ///
- public DateTimeOffset IssuedAt { get; init; }
-
- ///
- /// Optional device or client identifier.
- ///
- public required DeviceContext Device { get; init; }
-}
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/AccessToken.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/AccessToken.cs
index 459c8d0d..8c7169c3 100644
--- a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/AccessToken.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/AccessToken.cs
@@ -15,7 +15,7 @@ public sealed class AccessToken
/// Token type: "jwt" or "opaque".
/// Used for diagnostics and middleware behavior.
///
- public TokenType Type { get; init; }
+ public TokenFormat Format { get; init; }
///
/// Expiration time of the token.
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenFormat.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenFormat.cs
index a50157ce..b748de6c 100644
--- a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenFormat.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenFormat.cs
@@ -1,9 +1,8 @@
namespace CodeBeam.UltimateAuth.Core.Contracts;
-// TODO: It's same as TokenType
-// It's not primary token kind, it's about transport format.
public enum TokenFormat
{
Opaque = 0,
- Jwt = 10
+ Jwt = 10,
+ Unknown = 100
}
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenType.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenType.cs
deleted file mode 100644
index da231a01..00000000
--- a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenType.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace CodeBeam.UltimateAuth.Core.Contracts;
-
-public enum TokenType
-{
- Opaque = 0,
- Jwt = 10,
- Unknown = 100
-}
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenValidationResult.cs b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenValidationResult.cs
index f0247ddf..372fd4fd 100644
--- a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenValidationResult.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenValidationResult.cs
@@ -7,7 +7,7 @@ namespace CodeBeam.UltimateAuth.Core.Contracts;
public sealed record TokenValidationResult
{
public bool IsValid { get; init; }
- public TokenType Type { get; init; }
+ public TokenFormat Format { get; init; }
public TenantKey? Tenant { get; init; }
public TUserId? UserId { get; init; }
public AuthSessionId? SessionId { get; init; }
@@ -17,7 +17,7 @@ public sealed record TokenValidationResult
private TokenValidationResult(
bool isValid,
- TokenType type,
+ TokenFormat format,
TenantKey? tenant,
TUserId? userId,
AuthSessionId? sessionId,
@@ -27,6 +27,7 @@ private TokenValidationResult(
)
{
IsValid = isValid;
+ Format = format;
Tenant = tenant;
UserId = userId;
SessionId = sessionId;
@@ -36,7 +37,7 @@ private TokenValidationResult(
}
public static TokenValidationResult Valid(
- TokenType type,
+ TokenFormat format,
TenantKey tenant,
TUserId userId,
AuthSessionId? sessionId,
@@ -44,7 +45,7 @@ public static TokenValidationResult Valid(
DateTimeOffset? expiresAt)
=> new(
isValid: true,
- type,
+ format,
tenant,
userId,
sessionId,
@@ -53,10 +54,10 @@ public static TokenValidationResult Valid(
expiresAt
);
- public static TokenValidationResult Invalid(TokenType type, TokenInvalidReason reason)
+ public static TokenValidationResult Invalid(TokenFormat format, TokenInvalidReason reason)
=> new(
isValid: false,
- type: type,
+ format: format,
tenant: null,
userId: default,
sessionId: null,
diff --git a/src/CodeBeam.UltimateAuth.Core/Defaults/UAuthActions.cs b/src/CodeBeam.UltimateAuth.Core/Defaults/UAuthActions.cs
index 58fc735b..337cc47c 100644
--- a/src/CodeBeam.UltimateAuth.Core/Defaults/UAuthActions.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Defaults/UAuthActions.cs
@@ -68,8 +68,12 @@ public static class UserProfiles
public const string GetSelf = "users.profile.get.self";
public const string UpdateSelf = "users.profile.update.self";
+ public const string CreateSelf = "users.profile.add.self";
+ public const string CreateAdmin = "users.profile.add.admin";
public const string GetAdmin = "users.profile.get.admin";
public const string UpdateAdmin = "users.profile.update.admin";
+ public const string DeleteSelf = "users.profile.delete.self";
+ public const string DeleteAdmin = "users.profile.delete.admin";
}
public static class UserIdentifiers
diff --git a/src/CodeBeam.UltimateAuth.Core/Defaults/UAuthConstants.cs b/src/CodeBeam.UltimateAuth.Core/Defaults/UAuthConstants.cs
index c6761ad6..37881b9d 100644
--- a/src/CodeBeam.UltimateAuth.Core/Defaults/UAuthConstants.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Defaults/UAuthConstants.cs
@@ -31,6 +31,7 @@ public static class Form
public const string ReturnUrl = "return_url";
public const string Device = "__uauth_device";
public const string ClientProfile = "__uauth_client_profile";
+ public const string FormCacheKey = "__uauth_form";
}
public static class Query
diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Auth/AuthArtifactType.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Auth/AuthArtifactType.cs
index 85157ad7..a7d9bef7 100644
--- a/src/CodeBeam.UltimateAuth.Core/Domain/Auth/AuthArtifactType.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Domain/Auth/AuthArtifactType.cs
@@ -2,13 +2,13 @@
public enum AuthArtifactType
{
- PkceAuthorizationCode,
- HubFlow,
- LoginPreview,
- HubLogin,
- MfaChallenge,
- PasswordReset,
- MagicLink,
- OAuthState,
+ PkceAuthorizationCode = 0,
+ HubFlow = 10,
+ LoginPreview = 20,
+ HubLogin = 30,
+ MfaChallenge = 40,
+ PasswordReset = 50,
+ MagicLink = 60,
+ OAuthState = 100,
Custom = 1000
}
diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Auth/AuthFlowType.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Auth/AuthFlowType.cs
new file mode 100644
index 00000000..b1202880
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Domain/Auth/AuthFlowType.cs
@@ -0,0 +1,29 @@
+namespace CodeBeam.UltimateAuth.Core.Domain;
+
+public enum AuthFlowType
+{
+ Login = 0,
+ Reauthentication = 10,
+ Logout = 20,
+
+ RefreshSession = 100,
+ ValidateSession = 110,
+ QuerySession = 120,
+ RevokeSession = 130,
+
+ IssueToken = 200,
+ RefreshToken = 210,
+ IntrospectToken = 220,
+ RevokeToken = 230,
+
+ UserInfo = 300,
+ PermissionQuery = 310,
+
+ UserManagement = 400,
+ UserProfileManagement = 410,
+ UserIdentifierManagement = 420,
+ CredentialManagement = 430,
+ AuthorizationManagement = 440,
+
+ ApiAccess = 500
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/AuthFlowType.cs b/src/CodeBeam.UltimateAuth.Core/Domain/AuthFlowType.cs
deleted file mode 100644
index 905d54df..00000000
--- a/src/CodeBeam.UltimateAuth.Core/Domain/AuthFlowType.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-namespace CodeBeam.UltimateAuth.Core.Domain;
-
-public enum AuthFlowType
-{
- Login,
- Reauthentication,
-
- Logout,
- RefreshSession,
- ValidateSession,
-
- IssueToken,
- RefreshToken,
- IntrospectToken,
- RevokeToken,
-
- QuerySession,
- RevokeSession,
-
- UserInfo,
- PermissionQuery,
-
- UserManagement,
- UserProfileManagement,
- UserIdentifierManagement,
- CredentialManagement,
- AuthorizationManagement,
-
- ApiAccess
-}
diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Hub/HubErrorCode.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Hub/HubErrorCode.cs
index c185ab21..ca09b19b 100644
--- a/src/CodeBeam.UltimateAuth.Core/Domain/Hub/HubErrorCode.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Domain/Hub/HubErrorCode.cs
@@ -3,8 +3,8 @@
public enum HubErrorCode
{
None = 0,
- InvalidCredentials,
- LockedOut,
- RequiresMfa,
- Unknown
+ InvalidCredentials = 10,
+ LockedOut = 20,
+ RequiresMfa = 30,
+ Unknown = 100
}
diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Hub/HubFlowType.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Hub/HubFlowType.cs
index 3d3980c7..fe4ea93c 100644
--- a/src/CodeBeam.UltimateAuth.Core/Domain/Hub/HubFlowType.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Domain/Hub/HubFlowType.cs
@@ -4,10 +4,10 @@ public enum HubFlowType
{
None = 0,
- Login = 1,
- Mfa = 2,
- Reauthentication = 3,
- Consent = 4,
+ Login = 10,
+ Mfa = 20,
+ Reauthentication = 30,
+ Consent = 40,
Custom = 1000
}
diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Pkce/HubLoginArtifact.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Pkce/HubLoginArtifact.cs
deleted file mode 100644
index c6ceb415..00000000
--- a/src/CodeBeam.UltimateAuth.Core/Domain/Pkce/HubLoginArtifact.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-//namespace CodeBeam.UltimateAuth.Core.Domain;
-
-//public sealed class HubLoginArtifact : AuthArtifact
-//{
-// public string AuthorizationCode { get; }
-// public string CodeVerifier { get; }
-
-// public HubLoginArtifact(
-// string authorizationCode,
-// string codeVerifier,
-// DateTimeOffset expiresAt)
-// : base(AuthArtifactType.HubLogin, expiresAt)
-// {
-// AuthorizationCode = authorizationCode;
-// CodeVerifier = codeVerifier;
-// }
-//}
diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Principals/AuthFailureReason.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Principals/AuthFailureReason.cs
index b0b148ed..23510367 100644
--- a/src/CodeBeam.UltimateAuth.Core/Domain/Principals/AuthFailureReason.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Domain/Principals/AuthFailureReason.cs
@@ -2,13 +2,13 @@
public enum AuthFailureReason
{
- InvalidCredentials,
- LockedOut,
- RequiresMfa,
- SessionExpired,
- SessionRevoked,
- TenantDisabled,
- Unauthorized,
- ReauthenticationRequired,
- Unknown
+ InvalidCredentials = 0,
+ LockedOut = 10,
+ RequiresMfa = 20,
+ ReauthenticationRequired = 30,
+ Unauthorized = 40,
+ SessionExpired = 100,
+ SessionRevoked = 110,
+ TenantDisabled = 120,
+ Unknown = 1000
}
diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Principals/GrantKind.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Principals/GrantKind.cs
index 601b18f8..162dac79 100644
--- a/src/CodeBeam.UltimateAuth.Core/Domain/Principals/GrantKind.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Domain/Principals/GrantKind.cs
@@ -2,7 +2,7 @@
public enum GrantKind
{
- Session,
- AccessToken,
- RefreshToken
+ Session = 0,
+ AccessToken = 10,
+ RefreshToken = 20
}
diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Principals/PrimaryGrantKind.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Principals/PrimaryGrantKind.cs
deleted file mode 100644
index 9e12f09d..00000000
--- a/src/CodeBeam.UltimateAuth.Core/Domain/Principals/PrimaryGrantKind.cs
+++ /dev/null
@@ -1,7 +0,0 @@
-namespace CodeBeam.UltimateAuth.Core.Domain;
-
-public enum PrimaryGrantKind
-{
- Stateful,
- Stateless
-}
diff --git a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/PrimaryTokenKind.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Principals/PrimaryTokenKind.cs
similarity index 58%
rename from src/CodeBeam.UltimateAuth.Core/Contracts/Token/PrimaryTokenKind.cs
rename to src/CodeBeam.UltimateAuth.Core/Domain/Principals/PrimaryTokenKind.cs
index 06cd52af..7b3ae60e 100644
--- a/src/CodeBeam.UltimateAuth.Core/Contracts/Token/PrimaryTokenKind.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Domain/Principals/PrimaryTokenKind.cs
@@ -1,4 +1,4 @@
-namespace CodeBeam.UltimateAuth.Core.Contracts;
+namespace CodeBeam.UltimateAuth.Core.Domain;
public enum PrimaryTokenKind
{
diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Principals/ReauthBehavior.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Principals/ReauthBehavior.cs
index 44262fe4..077f8f96 100644
--- a/src/CodeBeam.UltimateAuth.Core/Domain/Principals/ReauthBehavior.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Domain/Principals/ReauthBehavior.cs
@@ -2,7 +2,7 @@
public enum ReauthBehavior
{
- Redirect,
- None,
- RaiseEvent
+ Redirect = 0,
+ None = 10,
+ RaiseEvent = 20
}
diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Security/AuthenticationSecurityScope.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Security/AuthenticationSecurityScope.cs
index cd0c6c31..052c9a64 100644
--- a/src/CodeBeam.UltimateAuth.Core/Domain/Security/AuthenticationSecurityScope.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Domain/Security/AuthenticationSecurityScope.cs
@@ -3,5 +3,5 @@
public enum AuthenticationSecurityScope
{
Account = 0,
- Factor = 1
+ Factor = 10
}
diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Security/CredentialType.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Security/CredentialType.cs
index 35226f1b..1972b9b4 100644
--- a/src/CodeBeam.UltimateAuth.Core/Domain/Security/CredentialType.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Domain/Security/CredentialType.cs
@@ -12,12 +12,12 @@ public enum CredentialType
Totp = 30,
// Modern
- Passkey = 40,
+ Passkey = 100,
// Machine / system
- Certificate = 50,
- ApiKey = 60,
+ Certificate = 200,
+ ApiKey = 210,
// External / Federated // TODO: Add Microsoft, Google, GitHub etc.
- External = 70
+ External = 1000
}
diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Security/PasswordHash.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Security/PasswordHash.cs
new file mode 100644
index 00000000..a95107d6
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Domain/Security/PasswordHash.cs
@@ -0,0 +1,58 @@
+using CodeBeam.UltimateAuth.Core.Errors;
+
+namespace CodeBeam.UltimateAuth.Core;
+public readonly record struct PasswordHash : IParsable
+{
+ public string Algorithm { get; }
+ public string Hash { get; }
+
+ private PasswordHash(string algorithm, string hash)
+ {
+ Algorithm = algorithm;
+ Hash = hash;
+ }
+
+ public static PasswordHash Create(string algorithm, string hash)
+ {
+ if (string.IsNullOrWhiteSpace(algorithm))
+ throw new UAuthValidationException("hash_algorithm_required");
+
+ if (string.IsNullOrWhiteSpace(hash))
+ throw new UAuthValidationException("hash_required");
+
+ return new PasswordHash(algorithm, hash);
+ }
+
+ public static PasswordHash Parse(string s, IFormatProvider? provider)
+ {
+ if (!TryParse(s, provider, out var result))
+ throw new FormatException("Invalid PasswordHash format.");
+
+ return result;
+ }
+
+ public static bool TryParse(string? s, IFormatProvider? provider, out PasswordHash result)
+ {
+ if (string.IsNullOrWhiteSpace(s))
+ {
+ result = default;
+ return false;
+ }
+
+ var parts = s.Split('$', 2);
+
+ if (parts.Length != 2)
+ {
+ // backward compatibility
+ result = new PasswordHash("legacy", s);
+ return true;
+ }
+
+ result = new PasswordHash(parts[0], parts[1]);
+ return true;
+ }
+
+ public override string ToString() => $"{Algorithm}${Hash}";
+
+ public static implicit operator string(PasswordHash value) => value.ToString();
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Security/PasswordHashJsonConverter.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Security/PasswordHashJsonConverter.cs
new file mode 100644
index 00000000..f501a39f
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Core/Domain/Security/PasswordHashJsonConverter.cs
@@ -0,0 +1,25 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace CodeBeam.UltimateAuth.Core;
+
+public sealed class PasswordHashJsonConverter : JsonConverter
+{
+ public override PasswordHash Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType != JsonTokenType.String)
+ throw new JsonException("PasswordHash must be a string.");
+
+ var value = reader.GetString();
+
+ if (!PasswordHash.TryParse(value, null, out var result))
+ throw new JsonException($"Invalid PasswordHash: '{value}'");
+
+ return result;
+ }
+
+ public override void Write(Utf8JsonWriter writer, PasswordHash value, JsonSerializerOptions options)
+ {
+ writer.WriteStringValue(value.ToString());
+ }
+}
diff --git a/src/CodeBeam.UltimateAuth.Core/Domain/Session/RefreshOutcome.cs b/src/CodeBeam.UltimateAuth.Core/Domain/Session/RefreshOutcome.cs
index 27229e1f..da64faf0 100644
--- a/src/CodeBeam.UltimateAuth.Core/Domain/Session/RefreshOutcome.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Domain/Session/RefreshOutcome.cs
@@ -2,9 +2,9 @@
public enum RefreshOutcome
{
- Success, // minimal transport
- NoOp,
- Touched,
- Rotated,
- ReauthRequired
+ Success = 0, // minimal transport
+ NoOp = 10,
+ Touched = 20,
+ Rotated = 30,
+ ReauthRequired = 100
}
diff --git a/src/CodeBeam.UltimateAuth.Core/Errors/UAuthConflictException.cs b/src/CodeBeam.UltimateAuth.Core/Errors/Runtime/UAuthConflictException.cs
similarity index 100%
rename from src/CodeBeam.UltimateAuth.Core/Errors/UAuthConflictException.cs
rename to src/CodeBeam.UltimateAuth.Core/Errors/Runtime/UAuthConflictException.cs
diff --git a/src/CodeBeam.UltimateAuth.Core/Errors/UAuthForbiddenException.cs b/src/CodeBeam.UltimateAuth.Core/Errors/Runtime/UAuthForbiddenException.cs
similarity index 100%
rename from src/CodeBeam.UltimateAuth.Core/Errors/UAuthForbiddenException.cs
rename to src/CodeBeam.UltimateAuth.Core/Errors/Runtime/UAuthForbiddenException.cs
diff --git a/src/CodeBeam.UltimateAuth.Core/Errors/UAuthUnauthorizedException.cs b/src/CodeBeam.UltimateAuth.Core/Errors/Runtime/UAuthUnauthorizedException.cs
similarity index 100%
rename from src/CodeBeam.UltimateAuth.Core/Errors/UAuthUnauthorizedException.cs
rename to src/CodeBeam.UltimateAuth.Core/Errors/Runtime/UAuthUnauthorizedException.cs
diff --git a/src/CodeBeam.UltimateAuth.Core/Errors/UAuthValidationException.cs b/src/CodeBeam.UltimateAuth.Core/Errors/Runtime/UAuthValidationException.cs
similarity index 100%
rename from src/CodeBeam.UltimateAuth.Core/Errors/UAuthValidationException.cs
rename to src/CodeBeam.UltimateAuth.Core/Errors/Runtime/UAuthValidationException.cs
diff --git a/src/CodeBeam.UltimateAuth.Core/Events/UAuthEvents.cs b/src/CodeBeam.UltimateAuth.Core/Events/UAuthEvents.cs
index 9162c9fb..8a17a5d8 100644
--- a/src/CodeBeam.UltimateAuth.Core/Events/UAuthEvents.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Events/UAuthEvents.cs
@@ -1,6 +1,4 @@
-using CodeBeam.UltimateAuth.Core.Options;
-
-namespace CodeBeam.UltimateAuth.Core.Events;
+namespace CodeBeam.UltimateAuth.Core.Events;
///
/// Provides an optional, application-wide event hook system for UltimateAuth.
diff --git a/src/CodeBeam.UltimateAuth.Core/Infrastructure/Base64Url.cs b/src/CodeBeam.UltimateAuth.Core/Infrastructure/Base64Url.cs
index 14b2de0d..9cdfc1f5 100644
--- a/src/CodeBeam.UltimateAuth.Core/Infrastructure/Base64Url.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Infrastructure/Base64Url.cs
@@ -39,5 +39,4 @@ public static byte[] Decode(string input)
return Convert.FromBase64String(padded);
}
-
}
diff --git a/src/CodeBeam.UltimateAuth.Core/Infrastructure/Converters/DeviceContextJsonConverter.cs b/src/CodeBeam.UltimateAuth.Core/Infrastructure/Converters/DeviceContextJsonConverter.cs
index e0e8b013..03c27f1b 100644
--- a/src/CodeBeam.UltimateAuth.Core/Infrastructure/Converters/DeviceContextJsonConverter.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Infrastructure/Converters/DeviceContextJsonConverter.cs
@@ -14,7 +14,6 @@ public override DeviceContext Read(ref Utf8JsonReader reader, Type typeToConvert
using var doc = JsonDocument.ParseValue(ref reader);
var root = doc.RootElement;
- // DeviceId
DeviceId? deviceId = null;
if (root.TryGetProperty("deviceId", out var deviceIdProp))
{
diff --git a/src/CodeBeam.UltimateAuth.Core/Infrastructure/Converters/UAuthUserIdConverter.cs b/src/CodeBeam.UltimateAuth.Core/Infrastructure/Converters/UAuthUserIdConverter.cs
index 9949db9b..152cf026 100644
--- a/src/CodeBeam.UltimateAuth.Core/Infrastructure/Converters/UAuthUserIdConverter.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Infrastructure/Converters/UAuthUserIdConverter.cs
@@ -1,6 +1,5 @@
using CodeBeam.UltimateAuth.Core.Abstractions;
using CodeBeam.UltimateAuth.Core.Domain;
-using CodeBeam.UltimateAuth.Core.Errors;
using System.Globalization;
using System.Text;
using System.Text.Json;
@@ -107,5 +106,4 @@ public bool TryFromBytes(byte[] binary, out TUserId id)
return false;
}
}
-
}
diff --git a/src/CodeBeam.UltimateAuth.Core/Infrastructure/SessionValidationMapper.cs b/src/CodeBeam.UltimateAuth.Core/Infrastructure/SessionValidationMapper.cs
index 0e0ab036..f46ced1f 100644
--- a/src/CodeBeam.UltimateAuth.Core/Infrastructure/SessionValidationMapper.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Infrastructure/SessionValidationMapper.cs
@@ -10,7 +10,7 @@ public static SessionValidationResult ToDomain(SessionValidationInfo dto)
{
var state = (SessionState)dto.State;
- if (!dto.IsValid || dto.Snapshot.Identity is null)
+ if (!dto.IsValid || dto.Snapshot?.Identity is null)
{
return SessionValidationResult.Invalid(state);
}
diff --git a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/CompositeTenantResolver.cs b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/CompositeTenantResolver.cs
index ffc9040e..035b90b9 100644
--- a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/CompositeTenantResolver.cs
+++ b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/CompositeTenantResolver.cs
@@ -32,5 +32,4 @@ public CompositeTenantResolver(IEnumerable resolvers)
return null;
}
-
}
diff --git a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/HeaderTenantResolver.cs b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/HeaderTenantResolver.cs
index 512ef583..3782dd31 100644
--- a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/HeaderTenantResolver.cs
+++ b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/HeaderTenantResolver.cs
@@ -33,5 +33,4 @@ public HeaderTenantResolver(string headerName)
return Task.FromResult(null);
}
-
}
diff --git a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/TenantContext.cs b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/TenantExecutionContext.cs
similarity index 62%
rename from src/CodeBeam.UltimateAuth.Core/MultiTenancy/TenantContext.cs
rename to src/CodeBeam.UltimateAuth.Core/MultiTenancy/TenantExecutionContext.cs
index 17e51cbc..262fa6e5 100644
--- a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/TenantContext.cs
+++ b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/TenantExecutionContext.cs
@@ -1,11 +1,11 @@
namespace CodeBeam.UltimateAuth.Core.MultiTenancy;
-public sealed class TenantContext
+public sealed class TenantExecutionContext
{
public TenantKey Tenant { get; }
public bool IsGlobal { get; }
- public TenantContext(TenantKey tenant, bool isGlobal = false)
+ public TenantExecutionContext(TenantKey tenant, bool isGlobal = false)
{
Tenant = tenant;
IsGlobal = isGlobal;
diff --git a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/UAuthTenantContext.cs b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/UAuthTenantContext.cs
index 9f73a2c9..11907b6e 100644
--- a/src/CodeBeam.UltimateAuth.Core/MultiTenancy/UAuthTenantContext.cs
+++ b/src/CodeBeam.UltimateAuth.Core/MultiTenancy/UAuthTenantContext.cs
@@ -7,9 +7,9 @@ public sealed class UAuthTenantContext
{
public TenantKey Tenant { get; }
- private UAuthTenantContext(TenantKey tenant)
+ private UAuthTenantContext(TenantKey tenant, bool allowUnresolved = false)
{
- if (tenant.IsUnresolved)
+ if (!allowUnresolved && tenant.IsUnresolved)
throw new InvalidOperationException("Runtime tenant context cannot be unresolved.");
Tenant = tenant;
@@ -20,6 +20,6 @@ private UAuthTenantContext(TenantKey tenant)
public static UAuthTenantContext SingleTenant() => new(TenantKey.Single);
public static UAuthTenantContext System() => new(TenantKey.System);
- public static UAuthTenantContext Unresolved() => new(TenantKey.Unresolved);
+ public static UAuthTenantContext Unresolved() => new(TenantKey.Unresolved, allowUnresolved: true);
public static UAuthTenantContext Resolved(TenantKey tenant) => new(tenant);
}
diff --git a/src/CodeBeam.UltimateAuth.Core/Options/HeaderTokenFormat.cs b/src/CodeBeam.UltimateAuth.Core/Options/HeaderTokenFormat.cs
index 826703c8..85b9c8ce 100644
--- a/src/CodeBeam.UltimateAuth.Core/Options/HeaderTokenFormat.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Options/HeaderTokenFormat.cs
@@ -2,6 +2,6 @@
public enum HeaderTokenFormat
{
- Bearer,
- Raw
+ Bearer = 0,
+ Raw = 10
}
diff --git a/src/CodeBeam.UltimateAuth.Core/Options/Validators/UAuthTokenOptionsValidator.cs b/src/CodeBeam.UltimateAuth.Core/Options/Validators/UAuthTokenOptionsValidator.cs
index 33881151..9d774c7d 100644
--- a/src/CodeBeam.UltimateAuth.Core/Options/Validators/UAuthTokenOptionsValidator.cs
+++ b/src/CodeBeam.UltimateAuth.Core/Options/Validators/UAuthTokenOptionsValidator.cs
@@ -38,7 +38,7 @@ public ValidateOptionsResult Validate(string? name, UAuthTokenOptions options)
errors.Add("Token.OpaqueIdBytes must be at least 16 bytes (128-bit entropy).");
if (options.OpaqueIdBytes > 128)
- errors.Add("Token.OpaqueIdBytes must not exceed 64 bytes.");
+ errors.Add("Token.OpaqueIdBytes must not exceed 128 bytes.");
}
return errors.Count == 0
diff --git a/src/CodeBeam.UltimateAuth.Server/Abstractions/IPrimaryCredentialResolver.cs b/src/CodeBeam.UltimateAuth.Server/Abstractions/IPrimaryCredentialResolver.cs
index d4bfbfca..4c4a1911 100644
--- a/src/CodeBeam.UltimateAuth.Server/Abstractions/IPrimaryCredentialResolver.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Abstractions/IPrimaryCredentialResolver.cs
@@ -5,5 +5,5 @@ namespace CodeBeam.UltimateAuth.Server.Abstractions;
public interface IPrimaryCredentialResolver
{
- PrimaryGrantKind Resolve(HttpContext context);
+ PrimaryTokenKind Resolve(HttpContext context);
}
diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/AuthStateSnapshotFactory.cs b/src/CodeBeam.UltimateAuth.Server/Auth/AuthStateSnapshotFactory.cs
index 76e977f3..cf182877 100644
--- a/src/CodeBeam.UltimateAuth.Server/Auth/AuthStateSnapshotFactory.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Auth/AuthStateSnapshotFactory.cs
@@ -1,6 +1,7 @@
using CodeBeam.UltimateAuth.Core.Abstractions;
using CodeBeam.UltimateAuth.Core.Contracts;
using CodeBeam.UltimateAuth.Users;
+using CodeBeam.UltimateAuth.Users.Contracts;
namespace CodeBeam.UltimateAuth.Server.Auth
{
@@ -23,7 +24,7 @@ public AuthStateSnapshotFactory(IPrimaryUserIdentifierProvider identifier, IUser
return null;
var identifiers = await _identifier.GetAsync(validation.Tenant, validation.UserKey.Value, ct);
- var profile = await _profile.GetAsync(validation.Tenant, validation.UserKey.Value, ct);
+ var profile = await _profile.GetAsync(validation.Tenant, validation.UserKey.Value, ProfileKey.Default, ct);
var lifecycle = await _lifecycle.GetAsync(validation.Tenant, validation.UserKey.Value, ct);
var identity = new AuthIdentitySnapshot
diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/ClientProfileReader.cs b/src/CodeBeam.UltimateAuth.Server/Auth/ClientProfileReader.cs
index 92fe58c1..671f608e 100644
--- a/src/CodeBeam.UltimateAuth.Server/Auth/ClientProfileReader.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Auth/ClientProfileReader.cs
@@ -21,12 +21,6 @@ public async Task ReadAsync(HttpContext context)
return formProfile;
}
- //if (context.Request.HasFormContentType && context.Request.Form.TryGetValue(UAuthConstants.Form.ClientProfile, out var formValue) &&
- // TryParse(formValue, out var formProfile))
- //{
- // return formProfile;
- //}
-
return UAuthClientProfile.NotSpecified;
}
diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/EffectiveUAuthServerOptions.cs b/src/CodeBeam.UltimateAuth.Server/Auth/EffectiveUAuthServerOptions.cs
index f17a63a6..5ddb0666 100644
--- a/src/CodeBeam.UltimateAuth.Server/Auth/EffectiveUAuthServerOptions.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Auth/EffectiveUAuthServerOptions.cs
@@ -11,6 +11,4 @@ public sealed class EffectiveUAuthServerOptions
/// Cloned, per-request server options
///
public UAuthServerOptions Options { get; init; } = default!;
-
- public UAuthResponseOptions AuthResponse => Options.AuthResponse;
}
diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/IPrimaryTokenResolver.cs b/src/CodeBeam.UltimateAuth.Server/Auth/IPrimaryTokenResolver.cs
index b53d9ef8..ba50be05 100644
--- a/src/CodeBeam.UltimateAuth.Server/Auth/IPrimaryTokenResolver.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Auth/IPrimaryTokenResolver.cs
@@ -1,5 +1,5 @@
using CodeBeam.UltimateAuth.Core;
-using CodeBeam.UltimateAuth.Core.Contracts;
+using CodeBeam.UltimateAuth.Core.Domain;
namespace CodeBeam.UltimateAuth.Server.Auth;
diff --git a/src/CodeBeam.UltimateAuth.Server/Auth/PrimaryTokenResolver.cs b/src/CodeBeam.UltimateAuth.Server/Auth/PrimaryTokenResolver.cs
index 8e5baa88..5daee211 100644
--- a/src/CodeBeam.UltimateAuth.Server/Auth/PrimaryTokenResolver.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Auth/PrimaryTokenResolver.cs
@@ -1,5 +1,5 @@
using CodeBeam.UltimateAuth.Core;
-using CodeBeam.UltimateAuth.Core.Contracts;
+using CodeBeam.UltimateAuth.Core.Domain;
namespace CodeBeam.UltimateAuth.Server.Auth;
diff --git a/src/CodeBeam.UltimateAuth.Server/Authentication/AspNetCore/UAuthAuthenticationExtension.cs b/src/CodeBeam.UltimateAuth.Server/Authentication/AspNetCore/UAuthAuthenticationExtension.cs
index e2b4ed54..27b8e3ca 100644
--- a/src/CodeBeam.UltimateAuth.Server/Authentication/AspNetCore/UAuthAuthenticationExtension.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Authentication/AspNetCore/UAuthAuthenticationExtension.cs
@@ -5,7 +5,7 @@ namespace CodeBeam.UltimateAuth.Server.Authentication;
public static class UAuthAuthenticationExtensions
{
- public static AuthenticationBuilder AddUAuthCookies(this AuthenticationBuilder builder, Action? configure = null)
+ public static AuthenticationBuilder AddUAuthScheme(this AuthenticationBuilder builder, Action? configure = null)
{
return builder.AddScheme(UAuthConstants.SchemeDefaults.GlobalScheme,
options =>
diff --git a/src/CodeBeam.UltimateAuth.Server/Authentication/AuthenticationSecurityManager.cs b/src/CodeBeam.UltimateAuth.Server/Authentication/AuthenticationSecurityManager.cs
index 59de49b3..f64ebecd 100644
--- a/src/CodeBeam.UltimateAuth.Server/Authentication/AuthenticationSecurityManager.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Authentication/AuthenticationSecurityManager.cs
@@ -10,12 +10,10 @@ namespace CodeBeam.UltimateAuth.Server.Security;
internal sealed class AuthenticationSecurityManager : IAuthenticationSecurityManager
{
private readonly IAuthenticationSecurityStateStoreFactory _storeFactory;
- private readonly UAuthServerOptions _options;
- public AuthenticationSecurityManager(IAuthenticationSecurityStateStoreFactory storeFactory, IOptions options)
+ public AuthenticationSecurityManager(IAuthenticationSecurityStateStoreFactory storeFactory)
{
_storeFactory = storeFactory;
- _options = options.Value;
}
public async Task GetOrCreateAccountAsync(TenantKey tenant, UserKey userKey, CancellationToken ct = default)
diff --git a/src/CodeBeam.UltimateAuth.Server/Authorization/AspNetCore/UAuthAuthorizationHandler.cs b/src/CodeBeam.UltimateAuth.Server/Authorization/AspNetCore/UAuthAuthorizationHandler.cs
index 3df4852a..9adc5db6 100644
--- a/src/CodeBeam.UltimateAuth.Server/Authorization/AspNetCore/UAuthAuthorizationHandler.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Authorization/AspNetCore/UAuthAuthorizationHandler.cs
@@ -1,8 +1,4 @@
-using CodeBeam.UltimateAuth.Core;
-using CodeBeam.UltimateAuth.Core.Contracts;
-using CodeBeam.UltimateAuth.Core.Errors;
-using CodeBeam.UltimateAuth.Server.Auth;
-using CodeBeam.UltimateAuth.Server.Extensions;
+using CodeBeam.UltimateAuth.Core.Errors;
using CodeBeam.UltimateAuth.Server.Infrastructure;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
diff --git a/src/CodeBeam.UltimateAuth.Server/Authorization/AspNetCore/UAuthPolicyProvider.cs b/src/CodeBeam.UltimateAuth.Server/Authorization/AspNetCore/UAuthPolicyProvider.cs
index 147b87dc..45491c25 100644
--- a/src/CodeBeam.UltimateAuth.Server/Authorization/AspNetCore/UAuthPolicyProvider.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Authorization/AspNetCore/UAuthPolicyProvider.cs
@@ -12,13 +12,13 @@ public UAuthPolicyProvider(IOptions options)
_fallback = new DefaultAuthorizationPolicyProvider(options);
}
- public Task GetPolicyAsync(string policyName)
+ public Task GetPolicyAsync(string policyName)
{
var policy = new AuthorizationPolicyBuilder()
.AddRequirements(new UAuthActionRequirement(policyName))
.Build();
- return Task.FromResult(policy);
+ return Task.FromResult(policy);
}
public Task GetDefaultPolicyAsync()
diff --git a/src/CodeBeam.UltimateAuth.Server/Composition/Extensions/AddUltimateAuthServerExtensions.cs b/src/CodeBeam.UltimateAuth.Server/Composition/Extensions/AddUltimateAuthServerExtensions.cs
deleted file mode 100644
index 91617a35..00000000
--- a/src/CodeBeam.UltimateAuth.Server/Composition/Extensions/AddUltimateAuthServerExtensions.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-//using CodeBeam.UltimateAuth.Core.Extensions;
-//using CodeBeam.UltimateAuth.Server.Extensions;
-//using CodeBeam.UltimateAuth.Server.Options;
-//using Microsoft.Extensions.Configuration;
-//using Microsoft.Extensions.DependencyInjection;
-
-//namespace CodeBeam.UltimateAuth.Server.Composition.Extensions;
-
-//public static class AddUltimateAuthServerExtensions
-//{
-// public static UltimateAuthServerBuilder AddUltimateAuthServer(this IServiceCollection services, IConfiguration configuration)
-// {
-// services.AddUltimateAuth(configuration); // Core
-// services.AddUAuthServerInfrastructure(); // issuer, flow, endpoints
-
-// services.Configure(configuration.GetSection("UltimateAuth:Server"));
-
-// return new UltimateAuthServerBuilder(services);
-// }
-//}
diff --git a/src/CodeBeam.UltimateAuth.Server/Composition/UltimateAuthServerBuilder.cs b/src/CodeBeam.UltimateAuth.Server/Composition/UltimateAuthServerBuilder.cs
deleted file mode 100644
index 1a119390..00000000
--- a/src/CodeBeam.UltimateAuth.Server/Composition/UltimateAuthServerBuilder.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-using Microsoft.Extensions.DependencyInjection;
-
-namespace CodeBeam.UltimateAuth.Server.Composition;
-
-public sealed class UltimateAuthServerBuilder
-{
- internal UltimateAuthServerBuilder(IServiceCollection services)
- {
- Services = services;
- }
-
- public IServiceCollection Services { get; }
-}
diff --git a/src/CodeBeam.UltimateAuth.Server/Composition/UltimateAuthServerBuilderValidation.cs b/src/CodeBeam.UltimateAuth.Server/Composition/UltimateAuthServerBuilderValidation.cs
deleted file mode 100644
index 8370bce1..00000000
--- a/src/CodeBeam.UltimateAuth.Server/Composition/UltimateAuthServerBuilderValidation.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using CodeBeam.UltimateAuth.Core.Abstractions;
-using Microsoft.Extensions.DependencyInjection;
-
-namespace CodeBeam.UltimateAuth.Server.Composition;
-
-public static class UltimateAuthServerBuilderValidationExtensions
-{
- public static IServiceCollection Build(this UltimateAuthServerBuilder builder)
- {
- var services = builder.Services;
-
- if (!services.Any(sd => sd.ServiceType == typeof(IUAuthPasswordHasher)))
- throw new InvalidOperationException("No IUAuthPasswordHasher registered. Call UseArgon2() or another hasher.");
-
- //if (!services.Any(sd => sd.ServiceType.IsAssignableTo(typeof(IUAuthUserStore<>))))
- // throw new InvalidOperationException("No credential store registered.");
-
- if (!services.Any(sd => sd.ServiceType.IsAssignableTo(typeof(ISessionStore))))
- throw new InvalidOperationException("No session store registered.");
-
- return services;
- }
-}
diff --git a/src/CodeBeam.UltimateAuth.Server/Contracts/ResolvedCredential.cs b/src/CodeBeam.UltimateAuth.Server/Contracts/ResolvedCredential.cs
index 98c509f8..b6939cd5 100644
--- a/src/CodeBeam.UltimateAuth.Server/Contracts/ResolvedCredential.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Contracts/ResolvedCredential.cs
@@ -6,7 +6,7 @@ namespace CodeBeam.UltimateAuth.Server.Contracts;
public sealed record ResolvedCredential
{
- public PrimaryGrantKind Kind { get; init; }
+ public PrimaryTokenKind Kind { get; init; }
///
/// Raw credential value (session id / jwt / opaque)
diff --git a/src/CodeBeam.UltimateAuth.Server/Diagnostics/UAuthDiagnostic.cs b/src/CodeBeam.UltimateAuth.Server/Diagnostics/UAuthDiagnostic.cs
index b993991a..386388b0 100644
--- a/src/CodeBeam.UltimateAuth.Server/Diagnostics/UAuthDiagnostic.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Diagnostics/UAuthDiagnostic.cs
@@ -4,7 +4,7 @@ public sealed record UAuthDiagnostic(string code, string message, UAuthDiagnosti
public enum UAuthDiagnosticSeverity
{
- Info,
- Warning,
- Error
+ Info = 0,
+ Warning = 10,
+ Error = 20
}
diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IUserEndpointHandler.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IUserEndpointHandler.cs
index 8e43a97b..fa21263e 100644
--- a/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IUserEndpointHandler.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/Abstractions/IUserEndpointHandler.cs
@@ -19,6 +19,12 @@ public interface IUserEndpointHandler
Task GetUserAsync(UserKey userKey, HttpContext ctx);
Task UpdateUserAsync(UserKey userKey, HttpContext ctx);
+ Task CreateProfileSelfAsync(HttpContext ctx);
+ Task DeleteProfileSelfAsync(HttpContext ctx);
+
+ Task CreateProfileAdminAsync(UserKey userKey, HttpContext ctx);
+ Task DeleteProfileAdminAsync(UserKey userKey, HttpContext ctx);
+
Task GetMyIdentifiersAsync(HttpContext ctx);
Task IdentifierExistsSelfAsync(HttpContext ctx);
Task AddUserIdentifierSelfAsync(HttpContext ctx);
diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/Bridges/LoginEndpointHandlerBridge.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/Bridges/LoginEndpointHandlerBridge.cs
deleted file mode 100644
index 9639a42f..00000000
--- a/src/CodeBeam.UltimateAuth.Server/Endpoints/Bridges/LoginEndpointHandlerBridge.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-//using CodeBeam.UltimateAuth.Core.Domain;
-//using Microsoft.AspNetCore.Http;
-
-//namespace CodeBeam.UltimateAuth.Server.Endpoints;
-
-//internal sealed class LoginEndpointHandlerBridge : ILoginEndpointHandler
-//{
-// private readonly LoginEndpointHandler _inner;
-
-// public LoginEndpointHandlerBridge(LoginEndpointHandler inner)
-// {
-// _inner = inner;
-// }
-
-// public Task LoginAsync(HttpContext ctx) => _inner.LoginAsync(ctx);
-//}
diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/Bridges/LogoutEndpointHandlerBridge.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/Bridges/LogoutEndpointHandlerBridge.cs
deleted file mode 100644
index 710cf06e..00000000
--- a/src/CodeBeam.UltimateAuth.Server/Endpoints/Bridges/LogoutEndpointHandlerBridge.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-//using CodeBeam.UltimateAuth.Core.Domain;
-//using Microsoft.AspNetCore.Http;
-
-//namespace CodeBeam.UltimateAuth.Server.Endpoints;
-
-//internal sealed class LogoutEndpointHandlerBridge : ILogoutEndpointHandler
-//{
-// private readonly LogoutEndpointHandler _inner;
-
-// public LogoutEndpointHandlerBridge(LogoutEndpointHandler inner)
-// {
-// _inner = inner;
-// }
-
-// public Task LogoutAsync(HttpContext ctx)
-// => _inner.LogoutAsync(ctx);
-//}
diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/Bridges/PkceEndpointHandlerBridge.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/Bridges/PkceEndpointHandlerBridge.cs
deleted file mode 100644
index 1b5aef95..00000000
--- a/src/CodeBeam.UltimateAuth.Server/Endpoints/Bridges/PkceEndpointHandlerBridge.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-//using CodeBeam.UltimateAuth.Core.Domain;
-//using Microsoft.AspNetCore.Http;
-
-//namespace CodeBeam.UltimateAuth.Server.Endpoints;
-
-//internal sealed class PkceEndpointHandlerBridge : IPkceEndpointHandler
-//{
-// private readonly PkceEndpointHandler _inner;
-
-// public PkceEndpointHandlerBridge(PkceEndpointHandler inner)
-// {
-// _inner = inner;
-// }
-
-// public Task AuthorizeAsync(HttpContext ctx) => _inner.AuthorizeAsync(ctx);
-
-// public Task CompleteAsync(HttpContext ctx) => _inner.CompleteAsync(ctx);
-//}
diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/Bridges/RefreshEndpointHandlerBridge.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/Bridges/RefreshEndpointHandlerBridge.cs
deleted file mode 100644
index 9a23cc1b..00000000
--- a/src/CodeBeam.UltimateAuth.Server/Endpoints/Bridges/RefreshEndpointHandlerBridge.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-using Microsoft.AspNetCore.Http;
-
-namespace CodeBeam.UltimateAuth.Server.Endpoints;
-
-internal sealed class RefreshEndpointHandlerBridge : IRefreshEndpointHandler
-{
- private readonly RefreshEndpointHandler _inner;
-
- public RefreshEndpointHandlerBridge(RefreshEndpointHandler inner)
- {
- _inner = inner;
- }
-
- public Task RefreshAsync(HttpContext ctx) => _inner.RefreshAsync(ctx);
-}
diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/Bridges/ValidateEndpointHandlerBridge.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/Bridges/ValidateEndpointHandlerBridge.cs
deleted file mode 100644
index 412bcef8..00000000
--- a/src/CodeBeam.UltimateAuth.Server/Endpoints/Bridges/ValidateEndpointHandlerBridge.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-using Microsoft.AspNetCore.Http;
-
-namespace CodeBeam.UltimateAuth.Server.Endpoints;
-
-internal sealed class ValidateEndpointHandlerBridge : IValidateEndpointHandler
-{
- private readonly ValidateEndpointHandler _inner;
-
- public ValidateEndpointHandlerBridge(ValidateEndpointHandler inner)
- {
- _inner = inner;
- }
-
- public Task ValidateAsync(HttpContext context, CancellationToken ct = default) => _inner.ValidateAsync(context, ct);
-}
diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/UAuthEndpointRegistrar.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/UAuthEndpointRegistrar.cs
index fa1d0120..4ba6e369 100644
--- a/src/CodeBeam.UltimateAuth.Server/Endpoints/UAuthEndpointRegistrar.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/UAuthEndpointRegistrar.cs
@@ -220,21 +220,37 @@ public void MapEndpoints(RouteGroupBuilder rootGroup, UAuthServerOptions options
if (options.Endpoints.UserProfile != false)
{
if (Enabled(UAuthActions.UserProfiles.GetSelf))
- self.MapPost("/get", async ([FromServices] IUserEndpointHandler h, HttpContext ctx)
+ self.MapPost("/profile/get", async ([FromServices] IUserEndpointHandler h, HttpContext ctx)
=> await h.GetMeAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserProfileManagement));
+ if (Enabled(UAuthActions.UserProfiles.CreateSelf))
+ self.MapPost("/profile/create", async (IUserEndpointHandler h, HttpContext ctx)
+ => await h.CreateProfileSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserProfileManagement));
+
if (Enabled(UAuthActions.UserProfiles.UpdateSelf))
- self.MapPost("/update", async ([FromServices] IUserEndpointHandler h, HttpContext ctx)
+ self.MapPost("/profile/update", async ([FromServices] IUserEndpointHandler h, HttpContext ctx)
=> await h.UpdateMeAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserProfileManagement));
+ if (Enabled(UAuthActions.UserProfiles.DeleteSelf))
+ self.MapPost("/profile/delete", async (IUserEndpointHandler h, HttpContext ctx)
+ => await h.DeleteProfileSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserProfileManagement));
+
if (Enabled(UAuthActions.UserProfiles.GetAdmin))
adminUsers.MapPost("/{userKey}/profile/get", async ([FromServices] IUserEndpointHandler h, UserKey userKey, HttpContext ctx)
=> await h.GetUserAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserProfileManagement));
+ if (Enabled(UAuthActions.UserProfiles.CreateAdmin))
+ adminUsers.MapPost("/{userKey}/profile/create", async (IUserEndpointHandler h, UserKey userKey, HttpContext ctx)
+ => await h.CreateProfileAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserProfileManagement));
+
if (Enabled(UAuthActions.UserProfiles.UpdateAdmin))
adminUsers.MapPost("/{userKey}/profile/update", async ([FromServices] IUserEndpointHandler h, UserKey userKey, HttpContext ctx)
=> await h.UpdateUserAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserProfileManagement));
+
+ if (Enabled(UAuthActions.UserProfiles.DeleteAdmin))
+ adminUsers.MapPost("/{userKey}/profile/delete", async (IUserEndpointHandler h, UserKey userKey, HttpContext ctx)
+ => await h.DeleteProfileAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserProfileManagement));
}
if (options.Endpoints.UserIdentifier != false)
diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/ValidateEndpointHandler.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/ValidateEndpointHandler.cs
index 9473ee3a..f95fd6fa 100644
--- a/src/CodeBeam.UltimateAuth.Server/Endpoints/ValidateEndpointHandler.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/ValidateEndpointHandler.cs
@@ -46,7 +46,7 @@ public async Task ValidateAsync(HttpContext context, CancellationToken
);
}
- if (credential.Kind == PrimaryGrantKind.Stateful)
+ if (credential.Kind == PrimaryTokenKind.Session)
{
if (!AuthSessionId.TryCreate(credential.Value, out var sessionId))
{
diff --git a/src/CodeBeam.UltimateAuth.Server/Extensions/ClaimsSnapshotExtensions.cs b/src/CodeBeam.UltimateAuth.Server/Extensions/ClaimsSnapshotExtensions.cs
deleted file mode 100644
index d0e17f40..00000000
--- a/src/CodeBeam.UltimateAuth.Server/Extensions/ClaimsSnapshotExtensions.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-using CodeBeam.UltimateAuth.Core.Domain;
-using System.Security.Claims;
-
-namespace CodeBeam.UltimateAuth.Server.Extensions;
-
-public static class ClaimsSnapshotExtensions
-{
- public static IReadOnlyCollection AsClaims(this ClaimsSnapshot snapshot)
- => snapshot.AsDictionary().Select(kv => new Claim(kv.Key, kv.Value)).ToArray();
-}
diff --git a/src/CodeBeam.UltimateAuth.Server/Extensions/HttpContext/HttpContextRequestExtensions.cs b/src/CodeBeam.UltimateAuth.Server/Extensions/HttpContext/HttpContextRequestExtensions.cs
index 480e882b..46b06bf4 100644
--- a/src/CodeBeam.UltimateAuth.Server/Extensions/HttpContext/HttpContextRequestExtensions.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Extensions/HttpContext/HttpContextRequestExtensions.cs
@@ -1,17 +1,16 @@
-using Microsoft.AspNetCore.Http;
+using CodeBeam.UltimateAuth.Core.Defaults;
+using Microsoft.AspNetCore.Http;
namespace CodeBeam.UltimateAuth.Server.Extensions;
internal static class HttpContextRequestExtensions
{
- private const string FormCacheKey = "__uauth_form";
-
public static async Task GetCachedFormAsync(this HttpContext ctx)
{
if (!ctx.Request.HasFormContentType)
return null;
- if (ctx.Items.TryGetValue(FormCacheKey, out var existing) && existing is IFormCollection cached)
+ if (ctx.Items.TryGetValue(UAuthConstants.Form.FormCacheKey, out var existing) && existing is IFormCollection cached)
return cached;
try
@@ -19,7 +18,7 @@ internal static class HttpContextRequestExtensions
ctx.Request.EnableBuffering();
var form = await ctx.Request.ReadFormAsync();
ctx.Request.Body.Position = 0;
- ctx.Items[FormCacheKey] = form;
+ ctx.Items[UAuthConstants.Form.FormCacheKey] = form;
return form;
}
catch (IOException)
diff --git a/src/CodeBeam.UltimateAuth.Server/Extensions/ServiceCollectionExtensions.cs b/src/CodeBeam.UltimateAuth.Server/Extensions/ServiceCollectionExtensions.cs
index a9da44d6..b9cf26dc 100644
--- a/src/CodeBeam.UltimateAuth.Server/Extensions/ServiceCollectionExtensions.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Extensions/ServiceCollectionExtensions.cs
@@ -279,7 +279,7 @@ private static IServiceCollection AddUltimateAuthServerInternal(this IServiceCol
options.DefaultChallengeScheme ??= UAuthConstants.SchemeDefaults.GlobalScheme;
});
- services.AddAuthentication().AddUAuthCookies();
+ services.AddAuthentication().AddUAuthScheme();
services.AddAuthorization();
services.AddSingleton();
diff --git a/src/CodeBeam.UltimateAuth.Server/Flows/Login/LoginDecisionKind.cs b/src/CodeBeam.UltimateAuth.Server/Flows/Login/LoginDecisionKind.cs
index e4044bd7..2c8a0c4c 100644
--- a/src/CodeBeam.UltimateAuth.Server/Flows/Login/LoginDecisionKind.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Flows/Login/LoginDecisionKind.cs
@@ -2,7 +2,7 @@
public enum LoginDecisionKind
{
- Allow = 1,
- Deny = 2,
- Challenge = 3
+ Allow = 0,
+ Deny = 10,
+ Challenge = 20
}
diff --git a/src/CodeBeam.UltimateAuth.Server/Flows/Login/LoginExecutionMode.cs b/src/CodeBeam.UltimateAuth.Server/Flows/Login/LoginExecutionMode.cs
index 56892530..2987ec57 100644
--- a/src/CodeBeam.UltimateAuth.Server/Flows/Login/LoginExecutionMode.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Flows/Login/LoginExecutionMode.cs
@@ -3,5 +3,5 @@
internal enum LoginExecutionMode
{
Preview = 0,
- Commit = 1
+ Commit = 10
}
diff --git a/src/CodeBeam.UltimateAuth.Server/Flows/Pkce/PkceChallengeMethod.cs b/src/CodeBeam.UltimateAuth.Server/Flows/Pkce/PkceChallengeMethod.cs
index 287d8406..4d550d37 100644
--- a/src/CodeBeam.UltimateAuth.Server/Flows/Pkce/PkceChallengeMethod.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Flows/Pkce/PkceChallengeMethod.cs
@@ -2,5 +2,5 @@
public enum PkceChallengeMethod
{
- S256
+ S256 = 0
}
diff --git a/src/CodeBeam.UltimateAuth.Server/Flows/Pkce/PkceValidationFailureReason.cs b/src/CodeBeam.UltimateAuth.Server/Flows/Pkce/PkceValidationFailureReason.cs
index 29a6eef5..a77e1bb2 100644
--- a/src/CodeBeam.UltimateAuth.Server/Flows/Pkce/PkceValidationFailureReason.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Flows/Pkce/PkceValidationFailureReason.cs
@@ -2,10 +2,10 @@
public enum PkceValidationFailureReason
{
- None,
- ArtifactExpired,
- MaxAttemptsExceeded,
- UnsupportedChallengeMethod,
- InvalidVerifier,
- ContextMismatch
+ None = 0,
+ ArtifactExpired = 10,
+ MaxAttemptsExceeded = 20,
+ UnsupportedChallengeMethod = 30,
+ InvalidVerifier = 40,
+ ContextMismatch = 50
}
diff --git a/src/CodeBeam.UltimateAuth.Server/Flows/Refresh/RefreshDecision.cs b/src/CodeBeam.UltimateAuth.Server/Flows/Refresh/RefreshDecision.cs
index 00c9eb6d..8620026f 100644
--- a/src/CodeBeam.UltimateAuth.Server/Flows/Refresh/RefreshDecision.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Flows/Refresh/RefreshDecision.cs
@@ -17,7 +17,7 @@ public enum RefreshDecision
/// No access / refresh token issued.
/// (PureOpaque)
///
- SessionTouch = 1,
+ SessionTouch = 10,
///
/// Refresh token is rotated and
@@ -25,5 +25,5 @@ public enum RefreshDecision
/// Session MAY also be touched depending on policy.
/// (Hybrid, SemiHybrid, PureJwt)
///
- TokenRotation = 2
+ TokenRotation = 20
}
diff --git a/src/CodeBeam.UltimateAuth.Server/Flows/Refresh/RefreshEvaluationResult.cs b/src/CodeBeam.UltimateAuth.Server/Flows/Refresh/RefreshEvaluationResult.cs
deleted file mode 100644
index f5a7f856..00000000
--- a/src/CodeBeam.UltimateAuth.Server/Flows/Refresh/RefreshEvaluationResult.cs
+++ /dev/null
@@ -1,5 +0,0 @@
-using CodeBeam.UltimateAuth.Core.Domain;
-
-namespace CodeBeam.UltimateAuth.Server.Flows;
-
-internal sealed record RefreshEvaluationResult(RefreshOutcome Outcome);
diff --git a/src/CodeBeam.UltimateAuth.Server/Flows/Refresh/RefreshStrategyResolver.cs b/src/CodeBeam.UltimateAuth.Server/Flows/Refresh/RefreshStrategyResolver.cs
deleted file mode 100644
index 52da1aa4..00000000
--- a/src/CodeBeam.UltimateAuth.Server/Flows/Refresh/RefreshStrategyResolver.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-using CodeBeam.UltimateAuth.Core;
-using CodeBeam.UltimateAuth.Core.Contracts;
-using System.Security;
-
-namespace CodeBeam.UltimateAuth.Server.Flows;
-
-public class RefreshStrategyResolver
-{
- public static RefreshStrategy Resolve(UAuthMode mode)
- {
- return mode switch
- {
- UAuthMode.PureOpaque => RefreshStrategy.SessionOnly,
- UAuthMode.PureJwt => RefreshStrategy.TokenOnly,
- UAuthMode.SemiHybrid => RefreshStrategy.TokenWithSessionCheck,
- UAuthMode.Hybrid => RefreshStrategy.SessionAndToken,
- _ => throw new SecurityException("Unsupported refresh mode")
- };
- }
-}
diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/AccessPolicyProvider.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/AccessPolicyProvider.cs
index 0a991193..2c19190b 100644
--- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/AccessPolicyProvider.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/AccessPolicyProvider.cs
@@ -17,5 +17,4 @@ public AccessPolicyProvider(CompiledAccessPolicySet set, IServiceProvider servic
}
public IReadOnlyCollection GetPolicies(AccessContext context) => _set.Resolve(context, _services);
-
}
diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/AspNetCore/TransportCredentialKind.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/AspNetCore/TransportCredentialKind.cs
index a33ad8bd..870a458e 100644
--- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/AspNetCore/TransportCredentialKind.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/AspNetCore/TransportCredentialKind.cs
@@ -2,8 +2,8 @@
public enum TransportCredentialKind
{
- Session,
- AccessToken,
- RefreshToken,
- Hub
+ Session = 0,
+ AccessToken = 10,
+ RefreshToken = 20,
+ Hub = 30
}
diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/PrimaryCredentialResolver.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/PrimaryCredentialResolver.cs
index 3ba31678..0744a460 100644
--- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/PrimaryCredentialResolver.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/PrimaryCredentialResolver.cs
@@ -16,7 +16,7 @@ public PrimaryCredentialResolver(IOptions options)
_options = options.Value;
}
- public PrimaryGrantKind Resolve(HttpContext context)
+ public PrimaryTokenKind Resolve(HttpContext context)
{
if (IsApiRequest(context))
return _options.PrimaryCredential.Api;
diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/ValidateCredentialResolver.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/ValidateCredentialResolver.cs
index 1923e5c3..85a6c22c 100644
--- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/ValidateCredentialResolver.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Credentials/ValidateCredentialResolver.cs
@@ -23,8 +23,8 @@ public ValidateCredentialResolver(IPrimaryCredentialResolver primaryResolver)
return kind switch
{
- PrimaryGrantKind.Stateful => await ResolveSession(context, response),
- PrimaryGrantKind.Stateless => await ResolveAccessToken(context, response),
+ PrimaryTokenKind.Session => await ResolveSession(context, response),
+ PrimaryTokenKind.AccessToken => await ResolveAccessToken(context, response),
_ => null
};
@@ -49,7 +49,7 @@ public ValidateCredentialResolver(IPrimaryCredentialResolver primaryResolver)
return new ResolvedCredential
{
- Kind = PrimaryGrantKind.Stateful,
+ Kind = PrimaryTokenKind.Session,
Value = raw.Trim(),
Tenant = context.GetTenant(),
Device = await context.GetDeviceAsync()
@@ -81,7 +81,7 @@ public ValidateCredentialResolver(IPrimaryCredentialResolver primaryResolver)
return new ResolvedCredential
{
- Kind = PrimaryGrantKind.Stateless,
+ Kind = PrimaryTokenKind.AccessToken,
Value = value,
Tenant = context.GetTenant(),
Device = await context.GetDeviceAsync()
diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Issuers/UAuthTokenIssuer.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Issuers/UAuthTokenIssuer.cs
index cb0b5955..d6986a1e 100644
--- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Issuers/UAuthTokenIssuer.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Issuers/UAuthTokenIssuer.cs
@@ -95,7 +95,7 @@ private AccessToken IssueOpaqueAccessToken(DateTimeOffset expires, string? sessi
return new AccessToken
{
Token = token,
- Type = TokenType.Opaque,
+ Format = TokenFormat.Opaque,
ExpiresAt = expires,
SessionId = sessionId
};
@@ -135,7 +135,7 @@ private AccessToken IssueJwtAccessToken(TokenIssuanceContext context, UAuthToken
return new AccessToken
{
Token = jwt,
- Type = TokenType.Jwt,
+ Format = TokenFormat.Jwt,
ExpiresAt = expires,
SessionId = context.SessionId.ToString()
};
diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Redirect/ReturnUrlKind.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Redirect/ReturnUrlKind.cs
index fa82e13d..52989cb5 100644
--- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Redirect/ReturnUrlKind.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Redirect/ReturnUrlKind.cs
@@ -2,7 +2,7 @@
public enum ReturnUrlKind
{
- None,
- Relative,
- Absolute
+ None = 0,
+ Relative = 10,
+ Absolute = 20
}
diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/User/UAuthUserAccessor.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/User/UAuthUserAccessor.cs
index d02e74b3..de1841c1 100644
--- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/User/UAuthUserAccessor.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/User/UAuthUserAccessor.cs
@@ -3,7 +3,6 @@
using CodeBeam.UltimateAuth.Core.Defaults;
using CodeBeam.UltimateAuth.Core.MultiTenancy;
using CodeBeam.UltimateAuth.Server.Extensions;
-using CodeBeam.UltimateAuth.Server.Middlewares;
using Microsoft.AspNetCore.Http;
namespace CodeBeam.UltimateAuth.Server.Infrastructure;
diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/User/UAuthUserId.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/User/UAuthUserId.cs
deleted file mode 100644
index caf8cb45..00000000
--- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/User/UAuthUserId.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-namespace CodeBeam.UltimateAuth.Server.Infrastructure;
-
-public readonly record struct UAuthUserId(Guid Value)
-{
- public override string ToString() => Value.ToString("N");
-
- public static UAuthUserId New() => new(Guid.NewGuid());
-
- public static implicit operator Guid(UAuthUserId id) => id.Value;
- public static implicit operator UAuthUserId(Guid value) => new(value);
-}
diff --git a/src/CodeBeam.UltimateAuth.Server/Options/UAuthHubDeploymentMode.cs b/src/CodeBeam.UltimateAuth.Server/Options/UAuthHubDeploymentMode.cs
index 2b90ae92..7fe26afd 100644
--- a/src/CodeBeam.UltimateAuth.Server/Options/UAuthHubDeploymentMode.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Options/UAuthHubDeploymentMode.cs
@@ -10,17 +10,17 @@ public enum UAuthHubDeploymentMode
/// UAuthHub is embedded in the same application and same origin.
/// Example: Blazor Server app hosting auth endpoints internally.
///
- Embedded,
+ Embedded = 0,
///
/// UAuthHub is hosted separately but within the same site boundary.
/// Example: auth.company.com and app.company.com behind same-site policy.
///
- Integrated,
+ Integrated = 10,
///
/// UAuthHub is hosted on a different site / domain.
/// Example: auth.vendor.com used by app.company.com.
///
- External
+ External = 20
}
diff --git a/src/CodeBeam.UltimateAuth.Server/Options/UAuthPrimaryCredentialPolicy.cs b/src/CodeBeam.UltimateAuth.Server/Options/UAuthPrimaryCredentialPolicy.cs
index 685bd2a3..b6993a08 100644
--- a/src/CodeBeam.UltimateAuth.Server/Options/UAuthPrimaryCredentialPolicy.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Options/UAuthPrimaryCredentialPolicy.cs
@@ -7,12 +7,12 @@ public sealed class UAuthPrimaryCredentialPolicy
///
/// Default primary credential for UI-style requests.
///
- public PrimaryGrantKind Ui { get; set; } = PrimaryGrantKind.Stateful;
+ public PrimaryTokenKind Ui { get; set; } = PrimaryTokenKind.Session;
///
/// Default primary credential for API requests.
///
- public PrimaryGrantKind Api { get; set; } = PrimaryGrantKind.Stateless;
+ public PrimaryTokenKind Api { get; set; } = PrimaryTokenKind.AccessToken;
internal UAuthPrimaryCredentialPolicy Clone() => new()
{
diff --git a/src/CodeBeam.UltimateAuth.Server/Options/UAuthServerOptions.cs b/src/CodeBeam.UltimateAuth.Server/Options/UAuthServerOptions.cs
index 6706f0d4..97652d24 100644
--- a/src/CodeBeam.UltimateAuth.Server/Options/UAuthServerOptions.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Options/UAuthServerOptions.cs
@@ -95,6 +95,8 @@ public sealed class UAuthServerOptions
public UAuthLoginIdentifierOptions LoginIdentifiers { get; set; } = new();
+ public UAuthUserProfileOptions UserProfile { get; set; } = new();
+
public UAuthNavigationOptions Navigation { get; set; } = new();
@@ -148,6 +150,7 @@ internal UAuthServerOptions Clone()
Identifiers = Identifiers.Clone(),
IdentifierValidation = IdentifierValidation.Clone(),
LoginIdentifiers = LoginIdentifiers.Clone(),
+ UserProfile = UserProfile.Clone(),
Endpoints = Endpoints.Clone(),
Navigation = Navigation.Clone(),
diff --git a/src/CodeBeam.UltimateAuth.Server/Options/UAuthUserProfileOptions.cs b/src/CodeBeam.UltimateAuth.Server/Options/UAuthUserProfileOptions.cs
new file mode 100644
index 00000000..0a6ab660
--- /dev/null
+++ b/src/CodeBeam.UltimateAuth.Server/Options/UAuthUserProfileOptions.cs
@@ -0,0 +1,11 @@
+namespace CodeBeam.UltimateAuth.Server.Options;
+
+public class UAuthUserProfileOptions
+{
+ public bool EnableMultiProfile { get; set; } = false;
+
+ internal UAuthUserProfileOptions Clone() => new()
+ {
+ EnableMultiProfile = EnableMultiProfile
+ };
+}
diff --git a/src/CodeBeam.UltimateAuth.Server/ResourceApi/ResourceAuthContextFactory.cs b/src/CodeBeam.UltimateAuth.Server/ResourceApi/ResourceAuthContextFactory.cs
index 7569b9e7..7d0ced82 100644
--- a/src/CodeBeam.UltimateAuth.Server/ResourceApi/ResourceAuthContextFactory.cs
+++ b/src/CodeBeam.UltimateAuth.Server/ResourceApi/ResourceAuthContextFactory.cs
@@ -25,6 +25,10 @@ public AuthContext Create(DateTimeOffset? at = null)
var result = ctx.Items[UAuthConstants.HttpItems.SessionValidationResult] as SessionValidationResult;
+ DeviceContext device = result?.BoundDeviceId is { } deviceId
+ ? DeviceContext.Create(DeviceId.Create(deviceId.Value))
+ : DeviceContext.Anonymous();
+
if (result is null || !result.IsValid)
{
return new AuthContext
@@ -33,7 +37,7 @@ public AuthContext Create(DateTimeOffset? at = null)
Operation = AuthOperation.ResourceAccess,
Mode = UAuthMode.PureOpaque,
ClientProfile = UAuthClientProfile.Api,
- Device = DeviceContext.Create(DeviceId.Create(result.BoundDeviceId.Value.Value)),
+ Device = device,
At = at ?? _clock.UtcNow,
Session = null
};
@@ -43,15 +47,15 @@ public AuthContext Create(DateTimeOffset? at = null)
{
Tenant = result.Tenant,
Operation = AuthOperation.ResourceAccess,
- Mode = UAuthMode.PureOpaque, // sonra resolver yapılabilir
+ Mode = UAuthMode.PureOpaque, // TODO: Think about resolver.
ClientProfile = UAuthClientProfile.Api,
- Device = DeviceContext.Create(DeviceId.Create(result.BoundDeviceId.Value.Value)),
+ Device = device,
At = at ?? _clock.UtcNow,
Session = new SessionSecurityContext
{
UserKey = result.UserKey,
- SessionId = result.SessionId.Value,
+ SessionId = result.SessionId!.Value,
State = result.State,
ChainId = result.ChainId,
BoundDeviceId = result.BoundDeviceId
diff --git a/src/CodeBeam.UltimateAuth.Server/Runtime/UAuthServerProductInfoProvider.cs b/src/CodeBeam.UltimateAuth.Server/Runtime/UAuthServerProductInfoProvider.cs
index de9a7e0a..57605781 100644
--- a/src/CodeBeam.UltimateAuth.Server/Runtime/UAuthServerProductInfoProvider.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Runtime/UAuthServerProductInfoProvider.cs
@@ -1,5 +1,4 @@
-using CodeBeam.UltimateAuth.Core.Options;
-using CodeBeam.UltimateAuth.Server.Options;
+using CodeBeam.UltimateAuth.Server.Options;
using Microsoft.Extensions.Options;
using System.Reflection;
diff --git a/src/CodeBeam.UltimateAuth.Server/Services/UAuthJwtValidator.cs b/src/CodeBeam.UltimateAuth.Server/Services/UAuthJwtValidator.cs
index ab4b6d4b..18adbfcd 100644
--- a/src/CodeBeam.UltimateAuth.Server/Services/UAuthJwtValidator.cs
+++ b/src/CodeBeam.UltimateAuth.Server/Services/UAuthJwtValidator.cs
@@ -27,7 +27,7 @@ public async Task> ValidateAsync(string
if (!result.IsValid)
{
- return TokenValidationResult.Invalid(TokenType.Jwt, MapJwtError(result.Exception));
+ return TokenValidationResult.Invalid(TokenFormat.Jwt, MapJwtError(result.Exception));
}
var jwt = (JsonWebToken)result.SecurityToken;
@@ -38,7 +38,7 @@ public async Task> ValidateAsync(string
var userIdString = jwt.GetClaim(ClaimTypes.NameIdentifier)?.Value ?? jwt.GetClaim("sub")?.Value;
if (string.IsNullOrWhiteSpace(userIdString))
{
- return TokenValidationResult.Invalid(TokenType.Jwt, TokenInvalidReason.MissingSubject);
+ return TokenValidationResult.Invalid(TokenFormat.Jwt, TokenInvalidReason.MissingSubject);
}
TUserId userId;
@@ -48,7 +48,7 @@ public async Task> ValidateAsync(string
}
catch
{
- return TokenValidationResult.Invalid(TokenType.Jwt, TokenInvalidReason.Malformed);
+ return TokenValidationResult.Invalid(TokenFormat.Jwt, TokenInvalidReason.Malformed);
}
var tenantId = jwt.GetClaim("tenant")?.Value ?? jwt.GetClaim("tid")?.Value;
@@ -60,7 +60,7 @@ public async Task> ValidateAsync(string
}
return TokenValidationResult.Valid(
- type: TokenType.Jwt,
+ format: TokenFormat.Jwt,
tenant: TenantKey.FromExternal(tenantId),
userId,
sessionId: sessionId,
diff --git a/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Stores/EfCoreAuthenticationSecurityStateStore.cs b/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Stores/EfCoreAuthenticationSecurityStateStore.cs
index fe12bfcd..d7c52993 100644
--- a/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Stores/EfCoreAuthenticationSecurityStateStore.cs
+++ b/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Stores/EfCoreAuthenticationSecurityStateStore.cs
@@ -12,7 +12,7 @@ internal sealed class EfCoreAuthenticationSecurityStateStore : IAuth
private readonly TDbContext _db;
private readonly TenantKey _tenant;
- public EfCoreAuthenticationSecurityStateStore(TDbContext db, TenantContext tenant)
+ public EfCoreAuthenticationSecurityStateStore(TDbContext db, TenantExecutionContext tenant)
{
_db = db;
_tenant = tenant.Tenant;
diff --git a/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Stores/EfCoreAuthenticationSecurityStateStoreFactory.cs b/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Stores/EfCoreAuthenticationSecurityStateStoreFactory.cs
index 5f897cf7..d254316c 100644
--- a/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Stores/EfCoreAuthenticationSecurityStateStoreFactory.cs
+++ b/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Stores/EfCoreAuthenticationSecurityStateStoreFactory.cs
@@ -15,6 +15,6 @@ public EfCoreAuthenticationSecurityStateStoreFactory(TDbContext db)
public IAuthenticationSecurityStateStore Create(TenantKey tenant)
{
- return new EfCoreAuthenticationSecurityStateStore(_db, new TenantContext(tenant));
+ return new EfCoreAuthenticationSecurityStateStore(_db, new TenantExecutionContext(tenant));
}
}
diff --git a/src/authentication/CodeBeam.UltimateAuth.Authentication.InMemory/InMemoryAuthenticationSecurityStateStore.cs b/src/authentication/CodeBeam.UltimateAuth.Authentication.InMemory/InMemoryAuthenticationSecurityStateStore.cs
index 6a191aeb..503ed884 100644
--- a/src/authentication/CodeBeam.UltimateAuth.Authentication.InMemory/InMemoryAuthenticationSecurityStateStore.cs
+++ b/src/authentication/CodeBeam.UltimateAuth.Authentication.InMemory/InMemoryAuthenticationSecurityStateStore.cs
@@ -14,7 +14,7 @@ internal sealed class InMemoryAuthenticationSecurityStateStore : IAuthentication
private readonly ConcurrentDictionary _byId = new();
private readonly ConcurrentDictionary<(UserKey, AuthenticationSecurityScope, CredentialType?), Guid> _index = new();
- public InMemoryAuthenticationSecurityStateStore(TenantContext tenant)
+ public InMemoryAuthenticationSecurityStateStore(TenantExecutionContext tenant)
{
_tenant = tenant.Tenant;
}
diff --git a/src/authentication/CodeBeam.UltimateAuth.Authentication.InMemory/InMemoryAuthenticationSecurityStateStoreFactory.cs b/src/authentication/CodeBeam.UltimateAuth.Authentication.InMemory/InMemoryAuthenticationSecurityStateStoreFactory.cs
index dfc34430..72dca09e 100644
--- a/src/authentication/CodeBeam.UltimateAuth.Authentication.InMemory/InMemoryAuthenticationSecurityStateStoreFactory.cs
+++ b/src/authentication/CodeBeam.UltimateAuth.Authentication.InMemory/InMemoryAuthenticationSecurityStateStoreFactory.cs
@@ -10,6 +10,6 @@ internal sealed class InMemoryAuthenticationSecurityStateStoreFactory : IAuthent
public IAuthenticationSecurityStateStore Create(TenantKey tenant)
{
- return _stores.GetOrAdd(tenant, t => new InMemoryAuthenticationSecurityStateStore(new TenantContext(t)));
+ return _stores.GetOrAdd(tenant, t => new InMemoryAuthenticationSecurityStateStore(new TenantExecutionContext(t)));
}
}
diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Stores/EfCoreRoleStore.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Stores/EfCoreRoleStore.cs
index f5151afa..3a0b5a30 100644
--- a/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Stores/EfCoreRoleStore.cs
+++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Stores/EfCoreRoleStore.cs
@@ -11,7 +11,7 @@ internal sealed class EfCoreRoleStore : IRoleStore where TDbContext
private readonly TDbContext _db;
private readonly TenantKey _tenant;
- public EfCoreRoleStore(TDbContext db, TenantContext tenant)
+ public EfCoreRoleStore(TDbContext db, TenantExecutionContext tenant)
{
_db = db;
_tenant = tenant.Tenant;
diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Stores/EfCoreRoleStoreFactory.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Stores/EfCoreRoleStoreFactory.cs
index ed02cd5e..d6bf6afe 100644
--- a/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Stores/EfCoreRoleStoreFactory.cs
+++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Stores/EfCoreRoleStoreFactory.cs
@@ -14,6 +14,6 @@ public EfCoreRoleStoreFactory(TDbContext db)
public IRoleStore Create(TenantKey tenant)
{
- return new EfCoreRoleStore(_db, new TenantContext(tenant));
+ return new EfCoreRoleStore(_db, new TenantExecutionContext(tenant));
}
}
diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Stores/EfCoreUserRoleStore.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Stores/EfCoreUserRoleStore.cs
index a99234f0..a7f46971 100644
--- a/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Stores/EfCoreUserRoleStore.cs
+++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Stores/EfCoreUserRoleStore.cs
@@ -11,7 +11,7 @@ internal sealed class EfCoreUserRoleStore : IUserRoleStore where TDb
private readonly TDbContext _db;
private readonly TenantKey _tenant;
- public EfCoreUserRoleStore(TDbContext db, TenantContext tenant)
+ public EfCoreUserRoleStore(TDbContext db, TenantExecutionContext tenant)
{
_db = db;
_tenant = tenant.Tenant;
diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Stores/EfCoreUserRoleStoreFactory.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Stores/EfCoreUserRoleStoreFactory.cs
index 74132289..a4920062 100644
--- a/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Stores/EfCoreUserRoleStoreFactory.cs
+++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Stores/EfCoreUserRoleStoreFactory.cs
@@ -14,6 +14,6 @@ public EfCoreUserRoleStoreFactory(TDbContext db)
public IUserRoleStore Create(TenantKey tenant)
{
- return new EfCoreUserRoleStore(_db, new TenantContext(tenant));
+ return new EfCoreUserRoleStore(_db, new TenantExecutionContext(tenant));
}
}
diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Stores/InMemoryRoleStore.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Stores/InMemoryRoleStore.cs
index 1c47b32f..11bde034 100644
--- a/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Stores/InMemoryRoleStore.cs
+++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Stores/InMemoryRoleStore.cs
@@ -10,7 +10,7 @@ internal sealed class InMemoryRoleStore : InMemoryTenantVersionedStore new(entity.Tenant, entity.Id);
- public InMemoryRoleStore(TenantContext tenant) : base(tenant)
+ public InMemoryRoleStore(TenantExecutionContext tenant) : base(tenant)
{
}
diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Stores/InMemoryRoleStoreFactory.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Stores/InMemoryRoleStoreFactory.cs
index 570d507b..7b5b2df3 100644
--- a/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Stores/InMemoryRoleStoreFactory.cs
+++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Stores/InMemoryRoleStoreFactory.cs
@@ -9,6 +9,6 @@ public sealed class InMemoryRoleStoreFactory : IRoleStoreFactory
public IRoleStore Create(TenantKey tenant)
{
- return _stores.GetOrAdd(tenant, t => new InMemoryRoleStore(new TenantContext(t)));
+ return _stores.GetOrAdd(tenant, t => new InMemoryRoleStore(new TenantExecutionContext(t)));
}
}
diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Stores/InMemoryUserRoleStore.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Stores/InMemoryUserRoleStore.cs
index 8027360b..d681d2a0 100644
--- a/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Stores/InMemoryUserRoleStore.cs
+++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Stores/InMemoryUserRoleStore.cs
@@ -11,7 +11,7 @@ internal sealed class InMemoryUserRoleStore : IUserRoleStore
private readonly TenantKey _tenant;
private readonly ConcurrentDictionary> _assignments = new();
- public InMemoryUserRoleStore(TenantContext tenant)
+ public InMemoryUserRoleStore(TenantExecutionContext tenant)
{
_tenant = tenant.Tenant;
}
diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Stores/InMemoryUserRoleStoreFactory.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Stores/InMemoryUserRoleStoreFactory.cs
index 9e8c8723..745ab6ef 100644
--- a/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Stores/InMemoryUserRoleStoreFactory.cs
+++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Stores/InMemoryUserRoleStoreFactory.cs
@@ -9,6 +9,6 @@ public sealed class InMemoryUserRoleStoreFactory : IUserRoleStoreFactory
public IUserRoleStore Create(TenantKey tenant)
{
- return _stores.GetOrAdd(tenant, t => new InMemoryUserRoleStore(new TenantContext(t)));
+ return _stores.GetOrAdd(tenant, t => new InMemoryUserRoleStore(new TenantExecutionContext(t)));
}
}
diff --git a/src/client/CodeBeam.UltimateAuth.Client.Blazor/Infrastructure/UAuthRequestClient.cs b/src/client/CodeBeam.UltimateAuth.Client.Blazor/Infrastructure/UAuthRequestClient.cs
index bc66f3cb..63662572 100644
--- a/src/client/CodeBeam.UltimateAuth.Client.Blazor/Infrastructure/UAuthRequestClient.cs
+++ b/src/client/CodeBeam.UltimateAuth.Client.Blazor/Infrastructure/UAuthRequestClient.cs
@@ -58,8 +58,8 @@ public async Task SendFormAsync(string endpoint, IDictiona
if (result.Status == 0)
throw new UAuthTransportException("Network error.");
- if (result.Status >= 500)
- throw new UAuthTransportException($"Server error {result.Status}", (HttpStatusCode)result.Status);
+ //if (result.Status >= 500)
+ // throw new UAuthTransportException($"Server error {result.Status}", (HttpStatusCode)result.Status);
return result;
}
diff --git a/src/client/CodeBeam.UltimateAuth.Client/Options/UAuthClientOptions.cs b/src/client/CodeBeam.UltimateAuth.Client/Options/UAuthClientOptions.cs
index 7931121d..f08a09a3 100644
--- a/src/client/CodeBeam.UltimateAuth.Client/Options/UAuthClientOptions.cs
+++ b/src/client/CodeBeam.UltimateAuth.Client/Options/UAuthClientOptions.cs
@@ -2,26 +2,117 @@
namespace CodeBeam.UltimateAuth.Client.Options;
+///
+/// Represents client-side configuration for UltimateAuth.
+///
+///
+///
+/// This class defines how the client application interacts with the UltimateAuth server,
+/// including authentication flows, endpoints, client behavior, and multi-tenant support.
+///
+///
+///
+/// The most important concept is , which determines
+/// how authentication behaves depending on the client type (e.g., Blazor Server, WASM, API).
+///
+///
+///
+/// Key areas:
+///
+/// Client Profile: Controls auth mode (session vs token, PKCE, etc.)
+/// Flows: Login, PKCE, refresh, and reauthentication behavior
+/// Endpoints: Server route configuration
+/// State Events: Client-side state change notifications
+/// Multi-Tenancy: Tenant resolution and propagation
+///
+///
+///
+///
+/// Important:
+///
+/// If is enabled, the profile is inferred automatically.
+/// If disabled, must be explicitly set.
+/// Different profiles may result in different authentication modes (e.g., PureOpaque vs Hybrid).
+///
+///
+///
public sealed class UAuthClientOptions
{
+ ///
+ /// Specifies the client profile used for authentication behavior.
+ ///
+ ///
+ /// Determines how authentication flows are executed (e.g., session-based, PKCE, token-based).
+ ///
public UAuthClientProfile ClientProfile { get; set; } = UAuthClientProfile.NotSpecified;
+
+ ///
+ /// Enables automatic detection of the client profile.
+ ///
+ ///
+ /// When enabled, UltimateAuth infers the client type (e.g., Blazor Server, WASM).
+ /// Disable this to guarantee explicitly control behavior via .
+ ///
public bool AutoDetectClientProfile { get; set; } = true;
///
- /// Global fallback return URL used by interactive authentication flows
- /// when no flow-specific return URL is provided.
+ /// Default return URL used for interactive authentication flows.
///
+ ///
+ /// Used when no return URL is explicitly provided in login or PKCE flows.
+ ///
public string? DefaultReturnUrl { get; set; }
+ ///
+ /// Configures client-side state change events.
+ ///
+ ///
+ /// Controls how authentication-related events (e.g., login, logout, profile changes) are propagated and handled within the client.
+ ///
public UAuthStateEventOptions StateEvents { get; set; } = new();
+
+ ///
+ /// Defines server endpoint paths used by the client.
+ ///
+ ///
+ /// Allows customization of API routes for authentication and user operations.
+ ///
public UAuthClientEndpointOptions Endpoints { get; set; } = new();
+
+ ///
+ /// Options related to login flow behavior.
+ ///
+ ///
+ /// Controls how login requests are executed and handled on the client.
+ ///
public UAuthClientLoginFlowOptions Login { get; set; } = new();
///
/// Options related to PKCE-based login flows.
///
public UAuthClientPkceLoginFlowOptions Pkce { get; set; } = new();
+
+ ///
+ /// Configures automatic session/token refresh behavior.
+ ///
+ ///
+ /// Determines how and when refresh operations are triggered.
+ ///
public UAuthClientAutoRefreshOptions AutoRefresh { get; set; } = new();
+
+ ///
+ /// Options for reauthentication behavior.
+ ///
+ ///
+ /// Used when session becomes invalid and user interaction is required again.
+ ///
public UAuthClientReauthOptions Reauth { get; init; } = new();
+
+ ///
+ /// Configures multi-tenant behavior for the client.
+ ///
+ ///
+ /// Controls how tenant information is resolved and included in requests.
+ ///
public UAuthClientMultiTenantOptions MultiTenant { get; set; } = new();
}
diff --git a/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IAuthorizationClient.cs b/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IAuthorizationClient.cs
index 78b6ffd9..3e848763 100644
--- a/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IAuthorizationClient.cs
+++ b/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IAuthorizationClient.cs
@@ -4,17 +4,109 @@
namespace CodeBeam.UltimateAuth.Client.Services;
+///
+/// Provides authorization and role management operations for the current application.
+///
+///
+///
+/// This client is responsible for evaluating permissions, managing roles,
+/// and assigning authorization policies to users.
+///
+///
+///
+/// Key capabilities:
+///
+/// Permission checks via
+/// User role management (assign/remove roles)
+/// Role lifecycle management (create, rename, delete)
+/// Permission assignment to roles
+///
+///
+///
+///
+/// Important:
+///
+/// Authorization decisions are evaluated on the server and may depend on current session context.
+/// Role changes may not take effect immediately for active sessions depending on caching strategy.
+/// Multi-tenant isolation is enforced; all operations are scoped to the current tenant.
+///
+///
+///
public interface IAuthorizationClient
{
+ ///
+ /// Evaluates whether the current user is authorized to perform a specific action.
+ ///
+ ///
+ /// This method performs a server-side authorization check based on the current session,
+ /// roles, and assigned permissions.
+ ///
Task> CheckAsync(AuthorizationCheckRequest request);
+
+ ///
+ /// Retrieves roles assigned to the current user.
+ ///
+ ///
+ /// Results may be paginated. Use to control paging behavior.
+ ///
Task> GetMyRolesAsync(PageRequest? request = null);
+
+ ///
+ /// Retrieves roles assigned to a specific user.
+ ///
+ ///
+ /// Requires appropriate administrative permissions.
+ ///
Task> GetUserRolesAsync(UserKey userKey, PageRequest? request = null);
+
+ ///
+ /// Assigns a role to a user.
+ ///
+ ///
+ /// The role must exist and the caller must have sufficient privileges.
+ ///
Task AssignRoleToUserAsync(AssignRoleRequest request);
+
+ ///
+ /// Removes a role from a user.
+ ///
Task RemoveRoleFromUserAsync(RemoveRoleRequest request);
+
+ ///
+ /// Creates a new role.
+ ///
+ ///
+ /// Role names must be unique (case-insensitive) within a tenant.
+ ///
Task> CreateRoleAsync(CreateRoleRequest request);
+
+ ///
+ /// Queries roles with filtering and pagination.
+ ///
Task>> QueryRolesAsync(RoleQuery request);
+
+ ///
+ /// Renames an existing role.
+ ///
+ ///
+ /// May fail if the target name already exists.
+ ///
Task RenameRoleAsync(RenameRoleRequest request);
+
+ ///
+ /// Sets the permissions associated with a role.
+ ///
+ ///
+ /// This operation replaces existing permissions.
+ ///
Task SetRolePermissionsAsync(SetRolePermissionsRequest request);
+
+ ///
+ /// Deletes a role.
+ ///
+ ///
+ /// Deleting a role removes it from all users.
+ ///
Task> DeleteRoleAsync(DeleteRoleRequest request);
}
diff --git a/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/ICredentialClient.cs b/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/ICredentialClient.cs
index b9241513..5715505d 100644
--- a/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/ICredentialClient.cs
+++ b/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/ICredentialClient.cs
@@ -4,18 +4,112 @@
namespace CodeBeam.UltimateAuth.Client.Services;
+///
+/// Provides credential management operations such as password creation, update, revocation, and reset flows.
+///
+///
+///
+/// This client handles both self-service and administrative credential operations.
+///
+///
+///
+/// Key capabilities:
+///
+/// Credential creation and update
+/// Credential revocation
+/// Credential reset flows (begin/complete)
+///
+///
+///
+///
+/// Important:
+///
+/// Reset operations are typically multi-step (begin → complete).
+/// Self methods operate on the current authenticated user.
+/// User methods require administrative privileges.
+/// Credential changes may invalidate active sessions depending on security policy.
+///
+///
+///
public interface ICredentialClient
{
+ ///
+ /// Adds a new credential for the current user.
+ ///
+ ///
+ /// Typically used for initial password setup or adding alternative credentials.
+ ///
Task> AddMyAsync(AddCredentialRequest request);
+
+ ///
+ /// Changes the credential of the current user.
+ ///
+ ///
+ /// May require the current credential for verification depending on policy.
+ ///
Task> ChangeMyAsync(ChangeCredentialRequest request);
+
+ ///
+ /// Revokes the current user's credential.
+ ///
+ ///
+ /// Revocation may invalidate active sessions.
+ ///
Task RevokeMyAsync(RevokeCredentialRequest request);
+
+ ///
+ /// Starts the credential reset process for the current user.
+ ///
+ ///
+ /// This typically issues a verification step (e.g., email or OTP).
+ ///
Task> BeginResetMyAsync(BeginResetCredentialRequest request);
+
+ ///
+ /// Completes the credential reset process for the current user.
+ ///
+ ///
+ /// Must be called after a successful .
+ ///
Task> CompleteResetMyAsync(CompleteResetCredentialRequest request);
+
+ ///
+ /// Adds a credential for a specific user.
+ ///
+ ///
+ /// Requires administrative privileges.
+ ///
Task> AddUserAsync(UserKey userKey, AddCredentialRequest request);
+
+ ///
+ /// Changes the credential of a specific user.
+ ///
+ ///
+ /// Typically used for administrative resets or overrides.
+ ///
Task> ChangeUserAsync(UserKey userKey, ChangeCredentialRequest request);
+
+ ///
+ /// Revokes a credential for a specific user.
+ ///
Task RevokeUserAsync(UserKey userKey, RevokeCredentialRequest request);
+
+ ///
+ /// Starts the credential reset process for a specific user.
+ ///
Task> BeginResetUserAsync(UserKey userKey, BeginResetCredentialRequest request);
+
+ ///
+ /// Completes the credential reset process for a specific user.
+ ///
Task> CompleteResetUserAsync(UserKey userKey, CompleteResetCredentialRequest request);
+
+ ///
+ /// Deletes a credential associated with a specific user.
+ ///
+ ///
+ /// This removes softly or permanently the credential from the system.
+ ///
Task DeleteUserAsync(UserKey userKey, DeleteCredentialRequest request);
}
diff --git a/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IFlowClient.cs b/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IFlowClient.cs
index f2cda9e5..2387d852 100644
--- a/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IFlowClient.cs
+++ b/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IFlowClient.cs
@@ -6,24 +6,135 @@
// TODO: Add ReauthAsync
namespace CodeBeam.UltimateAuth.Client.Services;
+///
+/// Provides authentication flow operations such as login, logout, session validation,
+/// refresh, and PKCE-based authentication.
+///
+///
+///
+/// This client is responsible for managing the full authentication lifecycle.
+/// It abstracts different auth modes (e.g., session-based, token-based, PKCE).
+///
+///
+///
+/// Key capabilities:
+///
+/// Login and logout flows
+/// Session validation and refresh
+/// PKCE-based authentication (for WASM and public clients)
+/// Device and session revocation
+///
+///
+///
+///
+/// Important:
+///
+/// Behavior depends on client profile (e.g., Blazor Server vs WASM).
+/// Login may result in redirects or cookie/token updates.
+/// Refresh behavior differs between PureOpaque and Hybrid modes.
+/// Session state is managed server-side and may expire independently.
+///
+///
+///
public interface IFlowClient
{
+ ///
+ /// Performs a login operation.
+ ///
+ ///
+ /// This method triggers redirects depending on the client profile and configuration.
+ ///
Task LoginAsync(LoginRequest request, string? returnUrl = null);
+
+ ///
+ /// Attempts to log in and returns a structured result instead of throwing. UltimateAuth suggestion as better UX.
+ ///
+ ///
+ /// Redirects only on successful login if mode is TryAndCommit. In TryOnly mode, it returns the result without redirecting.
+ /// DirectCommit mode behaves same as LoginAsync.
+ ///
Task TryLoginAsync(LoginRequest request, UAuthSubmitMode mode, string? returnUrl = null);
+ ///
+ /// Logs out the current user.
+ ///
+ ///
+ /// This clears the current session and may trigger redirects.
+ ///
Task LogoutAsync();
+
+ ///
+ /// Refreshes the current session or tokens.
+ ///
+ ///
+ /// Behavior depends on auth mode:
+ ///
+ /// PureOpaque: touches session
+ /// Hybrid: rotates tokens
+ ///
+ ///
Task RefreshAsync(bool isAuto = false);
- //Task ReauthAsync();
+
+ ///
+ /// Validates the current authentication state.
+ ///
+ ///
+ /// Can be used to check if the current session is still valid. For UI refresh, consider using IUAuthStateManager instead.
+ ///
Task ValidateAsync();
+ ///
+ /// Starts a PKCE authentication flow and navigates to UAuthHub.
+ ///
+ ///
+ /// Typically used in public clients such as Blazor WASM.
+ ///
Task BeginPkceAsync(string? returnUrl = null);
- Task TryCompletePkceLoginAsync(PkceCompleteRequest request, UAuthSubmitMode mode);
+
+ ///
+ /// Completes a PKCE login flow.
+ ///
+ ///
+ /// Must be called after .
+ ///
Task CompletePkceLoginAsync(PkceCompleteRequest request);
+ ///
+ /// Attempts to complete a PKCE login flow.
+ ///
+ ///
+ /// Redirects only on successful PKCE login if mode is TryAndCommit. In TryOnly mode, it returns the result without redirecting.
+ /// DirectCommit mode behaves same as CompletePkceLoginAsync.
+ ///
+ Task TryCompletePkceLoginAsync(PkceCompleteRequest request, UAuthSubmitMode mode);
+
+ ///
+ /// Logs out the given device session of current user.
+ ///
Task> LogoutMyDeviceAsync(LogoutDeviceRequest request);
+
+ ///
+ /// Logs out all other sessions except the current one of current user.
+ ///
Task LogoutMyOtherDevicesAsync();
+
+ ///
+ /// Logs out all sessions of the current user.
+ ///
Task LogoutAllMyDevicesAsync();
+
+ ///
+ /// Logs out a specific device session for a user.
+ ///
Task> LogoutUserDeviceAsync(UserKey userKey, LogoutDeviceRequest request);
+
+ ///
+ /// Logs out all other sessions for a user. Only given chain remains active.
+ ///
Task LogoutUserOtherDevicesAsync(UserKey userKey, LogoutOtherDevicesRequest request);
+
+ ///
+ /// Logs out all sessions for a user.
+ ///
Task LogoutAllUserDevicesAsync(UserKey userKey);
}
diff --git a/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/ISessionClient.cs b/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/ISessionClient.cs
index 1ecb6eb4..582f3243 100644
--- a/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/ISessionClient.cs
+++ b/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/ISessionClient.cs
@@ -3,19 +3,120 @@
namespace CodeBeam.UltimateAuth.Client.Services;
+///
+/// Provides session and device management operations for the current user or administrators.
+///
+///
+///
+/// This client exposes the session model of UltimateAuth, which is based on:
+///
+/// Root: Represents the user's global session authority.
+/// Chain: Represents a device or client context.
+/// Session: Represents an individual authentication instance.
+///
+///
+///
+///
+/// Key capabilities:
+///
+/// List active sessions (by device)
+/// Inspect session details
+/// Revoke sessions at different levels (session, chain, root)
+///
+///
+///
+///
+/// Important:
+///
+/// Revoking is different with logout. Revoke removes trust on device and new login creates a new chain instead of continue on current.
+/// Revoking a root is the ultimate tool which should use on security-critital situations.
+/// Session state is server-controlled and may expire independently.
+/// Administrative methods require elevated permissions.
+///
+///
+///
public interface ISessionClient
{
+ ///
+ /// Retrieves chains (devices) summary for the current user.
+ ///
+ ///
+ /// Each chain represents a device or client context.
+ ///
Task>> GetMyChainsAsync(PageRequest? request = null);
+
+ ///
+ /// Retrieves detailed information about a specific session chain.
+ ///
+ ///
+ /// Includes session history and device-related information.
+ ///
Task> GetMyChainDetailAsync(SessionChainId chainId);
+
+ ///
+ /// Revokes a specific session chain (device).
+ ///
+ ///
+ /// This logs out the user from the specified device.
+ ///
Task> RevokeMyChainAsync(SessionChainId chainId);
+
+ ///
+ /// Revokes all session chains except the current one.
+ ///
+ ///
+ /// Useful for "log out from other devices" scenarios.
+ ///
Task RevokeMyOtherChainsAsync();
+
+ ///
+ /// Revokes all session chains for the current user.
+ ///
+ ///
+ /// This logs out the user from all devices with clearing all device trusts.
+ ///
Task RevokeAllMyChainsAsync();
+ ///
+ /// Retrieves session chains (devices) for a specific user.
+ ///
+ ///
+ /// Requires administrative privileges.
+ ///
Task>> GetUserChainsAsync(UserKey userKey, PageRequest? request = null);
+
+ ///
+ /// Retrieves detailed session chain information for a specific user.
+ ///
Task> GetUserChainDetailAsync(UserKey userKey, SessionChainId chainId);
+
+ ///
+ /// Revokes a specific session instance for a user.
+ ///
+ ///
+ /// This invalidates a single session without affecting the entire device (chain).
+ ///
Task RevokeUserSessionAsync(UserKey userKey, AuthSessionId sessionId);
+
+ ///
+ /// Revokes a session chain (device) for a user.
+ ///
Task> RevokeUserChainAsync(UserKey userKey, SessionChainId chainId);
+
+ ///
+ /// Revokes the root session for a user.
+ ///
+ ///
+ /// This invalidates all sessions and chains for the user.
+ ///
Task RevokeUserRootAsync(UserKey userKey);
+
+ ///
+ /// Revokes all session chains (devices) for a user.
+ ///
+ ///
+ /// Equivalent to logging the user out from all devices.
+ ///
Task RevokeAllUserChainsAsync(UserKey userKey);
}
diff --git a/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IUAuthClient.cs b/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IUAuthClient.cs
index 54dcf6bd..54cabf73 100644
--- a/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IUAuthClient.cs
+++ b/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IUAuthClient.cs
@@ -2,12 +2,66 @@
namespace CodeBeam.UltimateAuth.Client;
+///
+/// Entry point for interacting with UltimateAuth from client applications.
+/// Provides access to all authentication, user, session, and authorization operations.
+///
+///
+///
+/// This client is designed to work across different client profiles (Blazor Server, WASM, MAUI, MVC, API).
+/// Behavior may vary depending on the configured ClientProfile.
+///
+///
+///
+/// Key components:
+///
+/// : Handles login, logout, refresh and auth flows.
+/// : Manages session lifecycle and validation.
+/// : User profile and account operations.
+/// : Email, username, phone management.
+/// : Password and credential operations.
+/// : Permission and policy checks.
+///
+///
+///
+///
+/// Important:
+///
+/// Session-based flows may rely on cookies (Blazor Server) or tokens (WASM).
+/// State changes (login, logout, profile updates) may trigger client events.
+/// Multi-tenant behavior depends on client configuration.
+///
+///
+///
public interface IUAuthClient
{
+ ///
+ /// Provides authentication flow operations such as login, logout, and refresh.
+ ///
IFlowClient Flows { get; }
+
+ ///
+ /// Provides access to session lifecycle operations.
+ ///
ISessionClient Sessions { get; }
+
+ ///
+ /// Provides user profile and account management operations.
+ ///
IUserClient Users { get; }
+
+ ///
+ /// Manages user identifiers such as email, username, and phone.
+ ///
IUserIdentifierClient Identifiers { get; }
+
+ ///
+ /// Provides credential operations such as password management.
+ ///
ICredentialClient Credentials { get; }
+
+ ///
+ /// Provides authorization and policy evaluation operations.
+ ///
IAuthorizationClient Authorization { get; }
}
diff --git a/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IUserClient.cs b/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IUserClient.cs
index 3034f685..ed06416b 100644
--- a/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IUserClient.cs
+++ b/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IUserClient.cs
@@ -4,19 +4,139 @@
namespace CodeBeam.UltimateAuth.Client.Services;
+///
+/// Provides user management and profile operations for both self-service and administrative scenarios.
+///
+///
+///
+/// This client handles user lifecycle operations as well as profile management.
+///
+///
+///
+/// UltimateAuth supports multi-identifier and multi-profile per user:
+///
+/// Each user can have multiple profiles (e.g., "default", "business"). You can enable it with server options.
+/// Profile selection is controlled via .
+/// If no profile is specified, the default profile is used.
+///
+///
+///
+///
+/// Key capabilities:
+///
+/// User creation and deletion
+/// User status management
+/// Profile retrieval and updates
+/// Multi-profile creation and deletion
+///
+///
+///
+///
+/// Important:
+///
+/// Self methods operate on the current authenticated user.
+/// Admin methods require elevated permissions.
+/// Deleting a user removes all associated profiles.
+/// Default profile cannot be deleted.
+///
+///
+///
public interface IUserClient
{
+ ///
+ /// Queries users with filtering and pagination.
+ ///
Task>> QueryAsync(UserQuery query);
+
+ ///
+ /// Creates a new user as the current user context.
+ ///
+ ///
+ /// Behavior may vary depending on client permissions and configuration.
+ /// Self creation may be allowed or restricted based on server settings.
+ ///
Task> CreateAsync(CreateUserRequest request);
+
+ ///
+ /// Creates a new user with administrative privileges.
+ ///
Task> CreateAsAdminAsync(CreateUserRequest request);
+
+ ///
+ /// Changes the status of the current user.
+ ///
Task> ChangeMyStatusAsync(ChangeUserStatusSelfRequest request);
+
+ ///
+ /// Changes the status of a specific user.
+ ///
Task> ChangeUserStatusAsync(UserKey userKey, ChangeUserStatusAdminRequest request);
+
+ ///
+ /// Deletes the current user. This is a soft-delete operation. Only administrators can restore deleted users or permanently removes soft deleted users.
+ ///
+ ///
+ /// This operation removes all associated profiles and sessions.
+ ///
Task DeleteMeAsync();
+
+ ///
+ /// Deletes a specific user.
+ ///
Task> DeleteUserAsync(UserKey userKey, DeleteUserRequest request);
- Task> GetMeAsync();
+
+ ///
+ /// Retrieves the current user's profile.
+ ///
+ ///
+ /// If is null, the default profile is returned.
+ ///
+ Task> GetMeAsync(GetProfileRequest? request = null);
+
+ ///
+ /// Updates the current user's profile.
+ ///
+ ///
+ /// The target profile is determined by .
+ ///
Task UpdateMeAsync(UpdateProfileRequest request);
- Task> GetUserAsync(UserKey userKey);
+ ///
+ /// Creates a new profile for the current user.
+ ///
+ ///
+ /// Profile keys must be unique per user.
+ /// Default profile is automatically created on user creation and cannot be duplicated.
+ ///
+ Task CreateMyProfileAsync(CreateProfileRequest request);
+
+ ///
+ /// Deletes a profile of the current user.
+ ///
+ ///
+ /// The default profile cannot be deleted.
+ ///
+ Task DeleteMyProfileAsync(ProfileKey profileKey);
+
+
+ ///
+ /// Retrieves a profile of a specific user.
+ ///
+ Task> GetUserAsync(UserKey userKey, GetProfileRequest? request = null);
+
+ ///
+ /// Updates a profile of a specific user.
+ ///
Task UpdateUserAsync(UserKey userKey, UpdateProfileRequest request);
+
+ ///
+ /// Creates a profile for a specific user.
+ ///
+ Task CreateUserProfileAsync(UserKey userKey, CreateProfileRequest request);
+
+ ///
+ /// Deletes a profile of a specific user.
+ ///
+ Task DeleteUserProfileAsync(UserKey userKey, ProfileKey profileKey);
}
diff --git a/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IUserIdentifierClient.cs b/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IUserIdentifierClient.cs
index 6ce76868..96dfd2bf 100644
--- a/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IUserIdentifierClient.cs
+++ b/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IUserIdentifierClient.cs
@@ -4,21 +4,122 @@
namespace CodeBeam.UltimateAuth.Client.Services;
+///
+/// Provides operations for managing user identifiers such as email, username, and phone.
+///
+///
+///
+/// Identifiers represent login and contact points for a user (e.g., email, username, phone).
+/// Each identifier has a type, value, and verification state.
+///
+///
+///
+/// Key capabilities:
+///
+/// Add, update, and delete identifiers
+/// Mark identifiers as primary
+/// Verify identifiers (e.g., email or phone verification)
+///
+///
+///
+///
+/// Important:
+///
+/// Identifier values are normalized and must be unique per tenant.
+/// Only one primary identifier per type is allowed.
+/// Verification is required for sensitive operations depending on policy.
+/// Self methods operate on the current user; user methods require administrative privileges.
+///
+///
+///
public interface IUserIdentifierClient
{
+ ///
+ /// Retrieves identifiers of the current user.
+ ///
Task>> GetMyAsync(PageRequest? request = null);
+
+ ///
+ /// Adds a new identifier to the current user.
+ ///
+ ///
+ /// The identifier must be unique within the tenant.
+ ///
Task AddMyAsync(AddUserIdentifierRequest request);
+
+ ///
+ /// Updates an existing identifier of the current user.
+ ///
+ ///
+ /// May require re-verification depending on the change.
+ ///
Task UpdateMyAsync(UpdateUserIdentifierRequest request);
+
+ ///
+ /// Marks an identifier as primary for the current user.
+ ///
+ ///
+ /// Only one primary identifier per type is allowed.
+ ///
Task SetMyPrimaryAsync(SetPrimaryUserIdentifierRequest request);
+
+ ///
+ /// Removes the primary designation from an identifier.
+ ///
+ ///
+ /// At least one primary identifier may be required depending on system policy.
+ ///
Task UnsetMyPrimaryAsync(UnsetPrimaryUserIdentifierRequest request);
+
+ ///
+ /// Verifies an identifier of the current user.
+ ///
+ ///
+ /// Typically used for email or phone verification flows.
+ ///
Task VerifyMyAsync(VerifyUserIdentifierRequest request);
+
+ ///
+ /// Deletes an identifier of the current user.
+ ///
+ ///
+ /// Primary identifiers may need to be reassigned before deletion.
+ ///
Task DeleteMyAsync(DeleteUserIdentifierRequest request);
+
+ ///
+ /// Retrieves identifiers of a specific user.
+ ///
Task>> GetUserAsync(UserKey userKey, PageRequest? request = null);
+
+ ///
+ /// Adds an identifier to a specific user.
+ ///
Task AddUserAsync(UserKey userKey, AddUserIdentifierRequest request);
+
+ ///
+ /// Updates an identifier of a specific user.
+ ///
Task UpdateUserAsync(UserKey userKey, UpdateUserIdentifierRequest request);
+
+ ///
+ /// Marks an identifier as primary for a specific user.
+ ///
Task SetUserPrimaryAsync(UserKey userKey, SetPrimaryUserIdentifierRequest request);
+
+ ///
+ /// Removes the primary designation from an identifier of a specific user.
+ ///
Task UnsetUserPrimaryAsync(UserKey userKey, UnsetPrimaryUserIdentifierRequest request);
+
+ ///
+ /// Verifies an identifier for a specific user.
+ ///
Task VerifyUserAsync(UserKey userKey, VerifyUserIdentifierRequest request);
+
+ ///
+ /// Deletes an identifier of a specific user.
+ ///
Task DeleteUserAsync(UserKey userKey, DeleteUserIdentifierRequest request);
}
diff --git a/src/client/CodeBeam.UltimateAuth.Client/Services/UAuthFlowClient.cs b/src/client/CodeBeam.UltimateAuth.Client/Services/UAuthFlowClient.cs
index 988c0ab2..cf9bb88e 100644
--- a/src/client/CodeBeam.UltimateAuth.Client/Services/UAuthFlowClient.cs
+++ b/src/client/CodeBeam.UltimateAuth.Client/Services/UAuthFlowClient.cs
@@ -330,7 +330,7 @@ public async Task CompletePkceLoginAsync(PkceCompleteRequest request)
{
["authorization_code"] = request.AuthorizationCode,
["code_verifier"] = request.CodeVerifier,
- ["return_url"] = request.ReturnUrl,
+ ["return_url"] = request.ReturnUrl ?? string.Empty,
["Identifier"] = request.Identifier ?? string.Empty,
["Secret"] = request.Secret ?? string.Empty,
diff --git a/src/client/CodeBeam.UltimateAuth.Client/Services/UAuthUserClient.cs b/src/client/CodeBeam.UltimateAuth.Client/Services/UAuthUserClient.cs
index 001afe2e..14c67252 100644
--- a/src/client/CodeBeam.UltimateAuth.Client/Services/UAuthUserClient.cs
+++ b/src/client/CodeBeam.UltimateAuth.Client/Services/UAuthUserClient.cs
@@ -23,15 +23,16 @@ public UAuthUserClient(IUAuthRequestClient request, IUAuthClientEvents events, I
private string Url(string path) => UAuthUrlBuilder.Build(_options.Endpoints.BasePath, path, _options.MultiTenant);
- public async Task> GetMeAsync()
+ public async Task> GetMeAsync(GetProfileRequest? request = null)
{
- var raw = await _request.SendFormAsync(Url("/me/get"));
+ request ??= new GetProfileRequest();
+ var raw = await _request.SendJsonAsync(Url("/me/profile/get"), request);
return UAuthResultMapper.FromJson(raw);
}
public async Task UpdateMeAsync(UpdateProfileRequest request)
{
- var raw = await _request.SendJsonAsync(Url("/me/update"), request);
+ var raw = await _request.SendJsonAsync(Url("/me/profile/update"), request);
if (raw.Ok)
{
await _events.PublishAsync(new UAuthStateEventArgs(UAuthStateEvent.ProfileChanged, _options.StateEvents.HandlingMode, request));
@@ -56,12 +57,69 @@ public async Task>> QueryAsync(UserQuery qu
return UAuthResultMapper.FromJson>(raw);
}
+ public async Task CreateMyProfileAsync(CreateProfileRequest request)
+ {
+ var raw = await _request.SendJsonAsync(Url("/me/profile/create"), request);
+
+ if (raw.Ok)
+ {
+ await _events.PublishAsync(
+ new UAuthStateEventArgs(
+ UAuthStateEvent.ProfileChanged,
+ _options.StateEvents.HandlingMode,
+ request));
+ }
+
+ return UAuthResultMapper.From(raw);
+ }
+
+ public async Task CreateUserProfileAsync(UserKey userKey, CreateProfileRequest request)
+ {
+ var raw = await _request.SendJsonAsync(Url($"/admin/users/{userKey.Value}/profile/create"), request);
+ return UAuthResultMapper.From(raw);
+ }
+
public async Task> CreateAsync(CreateUserRequest request)
{
var raw = await _request.SendJsonAsync(Url("/users/create"), request);
return UAuthResultMapper.FromJson(raw);
}
+ public async Task DeleteMyProfileAsync(ProfileKey profileKey)
+ {
+ var request = new DeleteProfileRequest
+ {
+ ProfileKey = profileKey
+ };
+
+ var raw = await _request.SendJsonAsync(Url("/me/profile/delete"), request);
+
+ if (raw.Ok)
+ {
+ await _events.PublishAsync(
+ new UAuthStateEventArgs(
+ UAuthStateEvent.ProfileChanged,
+ _options.StateEvents.HandlingMode,
+ profileKey));
+ }
+
+ return UAuthResultMapper.From(raw);
+ }
+
+ public async Task DeleteUserProfileAsync(UserKey userKey, ProfileKey profileKey)
+ {
+ var request = new DeleteProfileRequest
+ {
+ ProfileKey = profileKey
+ };
+
+ var raw = await _request.SendJsonAsync(
+ Url($"/admin/users/{userKey.Value}/profile/delete"),
+ request);
+
+ return UAuthResultMapper.From(raw);
+ }
+
public async Task> CreateAsAdminAsync(CreateUserRequest request)
{
var raw = await _request.SendJsonAsync(Url("/admin/users/create"), request);
@@ -90,9 +148,10 @@ public async Task> DeleteUserAsync(UserKey userKey
return UAuthResultMapper.FromJson(raw);
}
- public async Task> GetUserAsync(UserKey userKey)
+ public async Task> GetUserAsync(UserKey userKey, GetProfileRequest? request = null)
{
- var raw = await _request.SendFormAsync(Url($"/admin/users/{userKey.Value}/profile/get"));
+ request = request ?? new GetProfileRequest();
+ var raw = await _request.SendJsonAsync(Url($"/admin/users/{userKey.Value}/profile/get"), request);
return UAuthResultMapper.FromJson(raw);
}
diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Default/PasswordAlgorithms.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Default/PasswordAlgorithms.cs
new file mode 100644
index 00000000..4514b954
--- /dev/null
+++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Default/PasswordAlgorithms.cs
@@ -0,0 +1,8 @@
+namespace CodeBeam.UltimateAuth.Credentials.Contracts;
+
+public static class PasswordAlgorithms
+{
+ public const string Argon2 = "argon2";
+ public const string Bcrypt = "bcrypt";
+ public const string Legacy = "legacy";
+}
diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Data/UAuthCredentialsModelBuilder.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Data/UAuthCredentialsModelBuilder.cs
index 67415b62..5b1b3ab5 100644
--- a/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Data/UAuthCredentialsModelBuilder.cs
+++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Data/UAuthCredentialsModelBuilder.cs
@@ -1,4 +1,5 @@
-using CodeBeam.UltimateAuth.Core.Domain;
+using CodeBeam.UltimateAuth.Core;
+using CodeBeam.UltimateAuth.Core.Domain;
using CodeBeam.UltimateAuth.Core.MultiTenancy;
using CodeBeam.UltimateAuth.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
@@ -34,6 +35,9 @@ private static void ConfigurePasswordCredentials(ModelBuilder b)
.IsRequired();
e.Property(x => x.SecretHash)
+ .HasConversion(
+ v => v.ToString(),
+ v => PasswordHash.Parse(v, null))
.HasMaxLength(512)
.IsRequired();
diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Projections/PasswordCredentialProjection.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Projections/PasswordCredentialProjection.cs
index ae4a8dee..84726176 100644
--- a/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Projections/PasswordCredentialProjection.cs
+++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Projections/PasswordCredentialProjection.cs
@@ -1,4 +1,5 @@
-using CodeBeam.UltimateAuth.Core.Domain;
+using CodeBeam.UltimateAuth.Core;
+using CodeBeam.UltimateAuth.Core.Domain;
using CodeBeam.UltimateAuth.Core.MultiTenancy;
namespace CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore;
@@ -11,7 +12,7 @@ public sealed class PasswordCredentialProjection
public UserKey UserKey { get; set; }
- public string SecretHash { get; set; } = default!;
+ public PasswordHash SecretHash { get; set; }
public DateTimeOffset? RevokedAt { get; set; }
diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Stores/EfCorePasswordCredentialStore.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Stores/EfCorePasswordCredentialStore.cs
index 383bd7e0..00bbef97 100644
--- a/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Stores/EfCorePasswordCredentialStore.cs
+++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Stores/EfCorePasswordCredentialStore.cs
@@ -13,7 +13,7 @@ internal sealed class EfCorePasswordCredentialStore : IPasswordCrede
private readonly TDbContext _db;
private readonly TenantKey _tenant;
- public EfCorePasswordCredentialStore(TDbContext db, TenantContext tenant)
+ public EfCorePasswordCredentialStore(TDbContext db, TenantExecutionContext tenant)
{
_db = db;
_tenant = tenant.Tenant;
diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Stores/EfCorePasswordCredentialStoreFactory.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Stores/EfCorePasswordCredentialStoreFactory.cs
index 13a0a4a7..4efffb30 100644
--- a/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Stores/EfCorePasswordCredentialStoreFactory.cs
+++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Stores/EfCorePasswordCredentialStoreFactory.cs
@@ -15,6 +15,6 @@ public EfCorePasswordCredentialStoreFactory(TDbContext db)
public IPasswordCredentialStore Create(TenantKey tenant)
{
- return new EfCorePasswordCredentialStore(_db, new TenantContext(tenant));
+ return new EfCorePasswordCredentialStore(_db, new TenantExecutionContext(tenant));
}
}
diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/InMemoryPasswordCredentialStore.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/InMemoryPasswordCredentialStore.cs
index 0d1d7981..4d524d37 100644
--- a/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/InMemoryPasswordCredentialStore.cs
+++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/InMemoryPasswordCredentialStore.cs
@@ -13,7 +13,7 @@ internal sealed class InMemoryPasswordCredentialStore : InMemoryTenantVersionedS
protected override CredentialKey GetKey(PasswordCredential entity)
=> new(entity.Tenant, entity.Id);
- public InMemoryPasswordCredentialStore(TenantContext tenant) : base(tenant)
+ public InMemoryPasswordCredentialStore(TenantExecutionContext tenant) : base(tenant)
{
}
diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/InMemoryPasswordCredentialStoreFactory.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/InMemoryPasswordCredentialStoreFactory.cs
index 6258724a..fb48648d 100644
--- a/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/InMemoryPasswordCredentialStoreFactory.cs
+++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/InMemoryPasswordCredentialStoreFactory.cs
@@ -10,6 +10,6 @@ public sealed class InMemoryPasswordCredentialStoreFactory : IPasswordCredential
public IPasswordCredentialStore Create(TenantKey tenant)
{
- return _stores.GetOrAdd(tenant, t => new InMemoryPasswordCredentialStore(new TenantContext(t)));
+ return _stores.GetOrAdd(tenant, t => new InMemoryPasswordCredentialStore(new TenantExecutionContext(t)));
}
}
diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Domain/PasswordCredential.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Domain/PasswordCredential.cs
index 6e24cb6c..fd88eedc 100644
--- a/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Domain/PasswordCredential.cs
+++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/Domain/PasswordCredential.cs
@@ -1,8 +1,10 @@
-using CodeBeam.UltimateAuth.Core.Abstractions;
+using CodeBeam.UltimateAuth.Core;
+using CodeBeam.UltimateAuth.Core.Abstractions;
using CodeBeam.UltimateAuth.Core.Domain;
using CodeBeam.UltimateAuth.Core.Errors;
using CodeBeam.UltimateAuth.Core.MultiTenancy;
using CodeBeam.UltimateAuth.Credentials.Contracts;
+using System.Text.Json.Serialization;
namespace CodeBeam.UltimateAuth.Credentials.Reference;
@@ -13,8 +15,8 @@ public sealed class PasswordCredential : ISecretCredential, ITenantEntity, IVers
public UserKey UserKey { get; init; }
public CredentialType Type => CredentialType.Password;
- // TODO: Add hash algorithm (PasswordHash object with hash and algorithm properties)
- public string SecretHash { get; private set; } = default!;
+ [JsonConverter(typeof(PasswordHashJsonConverter))]
+ public PasswordHash SecretHash { get; private set; }
public CredentialSecurityState Security { get; private set; } = CredentialSecurityState.Active();
public CredentialMetadata Metadata { get; private set; } = new CredentialMetadata();
@@ -34,7 +36,7 @@ private PasswordCredential(
Guid id,
TenantKey tenant,
UserKey userKey,
- string secretHash,
+ PasswordHash secretHash,
CredentialSecurityState security,
CredentialMetadata metadata,
DateTimeOffset createdAt,
@@ -80,7 +82,7 @@ public static PasswordCredential Create(
Guid? id,
TenantKey tenant,
UserKey userKey,
- string secretHash,
+ PasswordHash secretHash,
CredentialSecurityState security,
CredentialMetadata metadata,
DateTimeOffset now)
@@ -98,7 +100,7 @@ public static PasswordCredential Create(
0);
}
- public PasswordCredential ChangeSecret(string newSecretHash, DateTimeOffset now)
+ public PasswordCredential ChangeSecret(PasswordHash newSecretHash, DateTimeOffset now)
{
if (string.IsNullOrWhiteSpace(newSecretHash))
throw new UAuthValidationException("credential_secret_required");
@@ -156,7 +158,7 @@ public static PasswordCredential FromProjection(
Guid id,
TenantKey tenant,
UserKey userKey,
- string secretHash,
+ PasswordHash secretHash,
CredentialSecurityState security,
CredentialMetadata metadata,
DateTimeOffset createdAt,
diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials/Abstractions/ISecretCredential.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials/Abstractions/ISecretCredential.cs
index 314d8395..4eed1f27 100644
--- a/src/credentials/CodeBeam.UltimateAuth.Credentials/Abstractions/ISecretCredential.cs
+++ b/src/credentials/CodeBeam.UltimateAuth.Credentials/Abstractions/ISecretCredential.cs
@@ -1,6 +1,8 @@
-namespace CodeBeam.UltimateAuth.Credentials;
+using CodeBeam.UltimateAuth.Core;
+
+namespace CodeBeam.UltimateAuth.Credentials;
public interface ISecretCredential : ICredential
{
- string SecretHash { get; }
+ PasswordHash SecretHash { get; }
}
diff --git a/src/persistence/CodeBeam.UltimateAuth.InMemory/InMemoryTenantVersionedStore.cs b/src/persistence/CodeBeam.UltimateAuth.InMemory/InMemoryTenantVersionedStore.cs
index c3d3d5c3..f43531e4 100644
--- a/src/persistence/CodeBeam.UltimateAuth.InMemory/InMemoryTenantVersionedStore.cs
+++ b/src/persistence/CodeBeam.UltimateAuth.InMemory/InMemoryTenantVersionedStore.cs
@@ -9,9 +9,9 @@ public abstract class InMemoryTenantVersionedStore : InMemoryVers
where TEntity : class, IVersionedEntity, IEntitySnapshot, ITenantEntity
where TKey : notnull, IEquatable
{
- private readonly TenantContext _tenant;
+ private readonly TenantExecutionContext _tenant;
- protected InMemoryTenantVersionedStore(TenantContext tenant)
+ protected InMemoryTenantVersionedStore(TenantExecutionContext tenant)
{
_tenant = tenant;
}
diff --git a/src/security/CodeBeam.UltimateAuth.Security.Argon2/Argon2PasswordHasher.cs b/src/security/CodeBeam.UltimateAuth.Security.Argon2/Argon2PasswordHasher.cs
index 2ec21417..7e3b7875 100644
--- a/src/security/CodeBeam.UltimateAuth.Security.Argon2/Argon2PasswordHasher.cs
+++ b/src/security/CodeBeam.UltimateAuth.Security.Argon2/Argon2PasswordHasher.cs
@@ -1,10 +1,13 @@
-using System.Security.Cryptography;
-using System.Text;
+using CodeBeam.UltimateAuth.Core;
using CodeBeam.UltimateAuth.Core.Abstractions;
using CodeBeam.UltimateAuth.Core.Errors;
+using CodeBeam.UltimateAuth.Credentials.Contracts;
using Konscious.Security.Cryptography;
using Microsoft.Extensions.Options;
+using System.Security.Cryptography;
+using System.Text;
+// TODO: Add rehashing support (rehash on login if options have changed, or if hash is malformed). This is important to ensure that password hashes stay up-to-date with the latest security standards and configurations. It also allows for seamless upgrades to the hashing algorithm or parameters without forcing users to reset their passwords.
namespace CodeBeam.UltimateAuth.Security.Argon2;
internal sealed class Argon2PasswordHasher : IUAuthPasswordHasher
@@ -16,37 +19,49 @@ public Argon2PasswordHasher(IOptions options)
_options = options.Value;
}
- public string Hash(string password)
+ public PasswordHash Hash(string password)
{
if (string.IsNullOrEmpty(password))
throw new UAuthValidationException("Password cannot be null or empty.");
var salt = RandomNumberGenerator.GetBytes(_options.SaltSize);
-
var argon2 = CreateArgon2(password, salt);
-
var hash = argon2.GetBytes(_options.HashSize);
- // format:
- // {salt}.{hash}
- return $"{Convert.ToBase64String(salt)}.{Convert.ToBase64String(hash)}";
+ var encoded = $"{_options.Iterations}.{_options.MemorySizeKb}.{_options.Parallelism}.{Convert.ToBase64String(salt)}.{Convert.ToBase64String(hash)}";
+ return PasswordHash.Create(PasswordAlgorithms.Argon2, encoded);
}
- public bool Verify(string hash, string secret)
+ public bool Verify(PasswordHash hash, string secret)
{
- if (string.IsNullOrWhiteSpace(secret) || string.IsNullOrWhiteSpace(hash))
+ if (string.IsNullOrWhiteSpace(secret) || string.IsNullOrWhiteSpace(hash.Hash))
return false;
- var parts = hash.Split('.');
- if (parts.Length != 2)
+ if (hash.Algorithm != PasswordAlgorithms.Argon2)
return false;
+ var parts = hash.Hash.Split('.', StringSplitOptions.RemoveEmptyEntries);
+ if (parts.Length != 5)
+ return false;
+
+ if (!int.TryParse(parts[0], out var iterations) ||
+ !int.TryParse(parts[1], out var memory) ||
+ !int.TryParse(parts[2], out var parallelism))
+ return false;
+
+ var salt = Convert.FromBase64String(parts[3]);
+ var expectedHash = Convert.FromBase64String(parts[4]);
+
try
{
- var salt = Convert.FromBase64String(parts[0]);
- var expectedHash = Convert.FromBase64String(parts[1]);
+ var argon2 = new Argon2id(Encoding.UTF8.GetBytes(secret))
+ {
+ Salt = salt,
+ Iterations = iterations,
+ MemorySize = memory,
+ DegreeOfParallelism = parallelism
+ };
- var argon2 = CreateArgon2(secret, salt);
var actualHash = argon2.GetBytes(expectedHash.Length);
return CryptographicOperations.FixedTimeEquals(actualHash, expectedHash);
@@ -57,6 +72,25 @@ public bool Verify(string hash, string secret)
}
}
+ public bool NeedsRehash(PasswordHash hash)
+ {
+ if (hash.Algorithm != PasswordAlgorithms.Argon2)
+ return true;
+
+ var parts = hash.Hash.Split('.', StringSplitOptions.RemoveEmptyEntries);
+ if (parts.Length != 5)
+ return true;
+
+ if (!int.TryParse(parts[0], out var iterations) ||
+ !int.TryParse(parts[1], out var memory) ||
+ !int.TryParse(parts[2], out var parallelism))
+ return true;
+
+ return iterations != _options.Iterations ||
+ memory != _options.MemorySizeKb ||
+ parallelism != _options.Parallelism;
+ }
+
private Argon2id CreateArgon2(string password, byte[] salt)
{
return new Argon2id(Encoding.UTF8.GetBytes(password))
diff --git a/src/security/CodeBeam.UltimateAuth.Security.Argon2/UltimateAuthServerBuilderArgon2Extensions.cs b/src/security/CodeBeam.UltimateAuth.Security.Argon2/UltimateAuthServerBuilderArgon2Extensions.cs
deleted file mode 100644
index cbce83da..00000000
--- a/src/security/CodeBeam.UltimateAuth.Security.Argon2/UltimateAuthServerBuilderArgon2Extensions.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using CodeBeam.UltimateAuth.Security.Argon2;
-
-namespace CodeBeam.UltimateAuth.Server.Composition.Extensions;
-
-public static class UltimateAuthServerBuilderArgon2Extensions
-{
- public static UltimateAuthServerBuilder UseArgon2(this UltimateAuthServerBuilder builder, Action? configure = null)
- {
- builder.Services.AddUltimateAuthArgon2(configure);
- return builder;
- }
-}
diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Stores/EfCoreSessionStore.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Stores/EfCoreSessionStore.cs
index ca86ff54..a1108ca4 100644
--- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Stores/EfCoreSessionStore.cs
+++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Stores/EfCoreSessionStore.cs
@@ -12,7 +12,7 @@ internal sealed class EfCoreSessionStore : ISessionStore where TDbCo
private readonly TDbContext _db;
private readonly TenantKey _tenant;
- public EfCoreSessionStore(TDbContext db, TenantContext tenant)
+ public EfCoreSessionStore(TDbContext db, TenantExecutionContext tenant)
{
_db = db;
_tenant = tenant.Tenant;
diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Stores/EfCoreSessionStoreFactory.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Stores/EfCoreSessionStoreFactory.cs
index 363e3738..75b4aba7 100644
--- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Stores/EfCoreSessionStoreFactory.cs
+++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Stores/EfCoreSessionStoreFactory.cs
@@ -15,6 +15,6 @@ public EfCoreSessionStoreFactory(TDbContext db)
public ISessionStore Create(TenantKey tenant)
{
- return new EfCoreSessionStore(_db, new TenantContext(tenant));
+ return new EfCoreSessionStore(_db, new TenantExecutionContext(tenant));
}
}
diff --git a/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/Stores/EfCoreRefreshTokenStore.cs b/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/Stores/EfCoreRefreshTokenStore.cs
index b4be2ef5..94bbe0c1 100644
--- a/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/Stores/EfCoreRefreshTokenStore.cs
+++ b/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/Stores/EfCoreRefreshTokenStore.cs
@@ -11,7 +11,7 @@ internal sealed class EfCoreRefreshTokenStore : IRefreshTokenStore w
private readonly TenantKey _tenant;
private bool _inTransaction;
- public EfCoreRefreshTokenStore(TDbContext db, TenantContext tenant)
+ public EfCoreRefreshTokenStore(TDbContext db, TenantExecutionContext tenant)
{
_db = db;
_tenant = tenant.Tenant;
diff --git a/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/Stores/EfCoreRefreshTokenStoreFactory.cs b/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/Stores/EfCoreRefreshTokenStoreFactory.cs
index 9cc94371..cd7e8bf9 100644
--- a/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/Stores/EfCoreRefreshTokenStoreFactory.cs
+++ b/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/Stores/EfCoreRefreshTokenStoreFactory.cs
@@ -15,6 +15,6 @@ public EfCoreRefreshTokenStoreFactory(TDbContext db)
public IRefreshTokenStore Create(TenantKey tenant)
{
- return new EfCoreRefreshTokenStore(_db, new TenantContext(tenant));
+ return new EfCoreRefreshTokenStore(_db, new TenantExecutionContext(tenant));
}
}
diff --git a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Domain/ProfileKey.cs b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Domain/ProfileKey.cs
new file mode 100644
index 00000000..180a1172
--- /dev/null
+++ b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Domain/ProfileKey.cs
@@ -0,0 +1,66 @@
+using System.Text.Json.Serialization;
+
+namespace CodeBeam.UltimateAuth.Users.Contracts;
+
+[JsonConverter(typeof(ProfileKeyJsonConverter))]
+public readonly record struct ProfileKey : IParsable
+{
+ public string Value { get; }
+
+ private ProfileKey(string value)
+ {
+ Value = value;
+ }
+
+ public static ProfileKey Default => new("default");
+
+ public static bool TryCreate(string? raw, out ProfileKey key)
+ {
+ if (IsValid(raw))
+ {
+ key = new ProfileKey(Normalize(raw!));
+ return true;
+ }
+
+ key = default;
+ return false;
+ }
+
+ public static ProfileKey Parse(string s, IFormatProvider? provider)
+ {
+ if (TryParse(s, provider, out var key))
+ return key;
+
+ throw new FormatException("Invalid ProfileKey.");
+ }
+
+ public static bool TryParse(string? s, IFormatProvider? provider, out ProfileKey result)
+ {
+ if (IsValid(s))
+ {
+ result = new ProfileKey(Normalize(s!));
+ return true;
+ }
+
+ result = default;
+ return false;
+ }
+
+ private static bool IsValid(string? value)
+ {
+ if (string.IsNullOrWhiteSpace(value))
+ return false;
+
+ if (value.Length > 64)
+ return false;
+
+ return true;
+ }
+
+ private static string Normalize(string value)
+ => value.Trim().ToLowerInvariant();
+
+ public override string ToString() => Value;
+
+ public static implicit operator string(ProfileKey key) => key.Value;
+}
\ No newline at end of file
diff --git a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Domain/ProfileKeyJsonConverter.cs b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Domain/ProfileKeyJsonConverter.cs
new file mode 100644
index 00000000..31976379
--- /dev/null
+++ b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Domain/ProfileKeyJsonConverter.cs
@@ -0,0 +1,28 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace CodeBeam.UltimateAuth.Users.Contracts;
+
+public sealed class ProfileKeyJsonConverter : JsonConverter
+{
+ public override ProfileKey Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType == JsonTokenType.Null)
+ return default;
+
+ if (reader.TokenType != JsonTokenType.String)
+ throw new JsonException("ProfileKey must be a string.");
+
+ var value = reader.GetString();
+
+ if (!ProfileKey.TryCreate(value, out var key))
+ throw new JsonException($"Invalid ProfileKey value: '{value}'");
+
+ return key;
+ }
+
+ public override void Write(Utf8JsonWriter writer, ProfileKey value, JsonSerializerOptions options)
+ {
+ writer.WriteStringValue(value.Value);
+ }
+}
diff --git a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/IdentifierExistenceScope.cs b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/IdentifierExistenceScope.cs
index ba2d8f33..92934904 100644
--- a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/IdentifierExistenceScope.cs
+++ b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/IdentifierExistenceScope.cs
@@ -5,15 +5,15 @@ public enum IdentifierExistenceScope
///
/// Checks only within the same user.
///
- WithinUser,
+ WithinUser = 0,
///
/// Checks within tenant but only primary identifiers.
///
- TenantPrimaryOnly,
+ TenantPrimaryOnly = 10,
///
/// Checks within tenant regardless of primary flag.
///
- TenantAny
+ TenantAny = 20
}
diff --git a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/MfaMethod.cs b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/MfaMethod.cs
index f207ae2c..1c86080e 100644
--- a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/MfaMethod.cs
+++ b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/MfaMethod.cs
@@ -2,8 +2,8 @@
public enum MfaMethod
{
- Totp = 10,
- Sms = 20,
- Email = 30,
- Passkey = 40
+ Totp = 0,
+ Sms = 10,
+ Email = 20,
+ Passkey = 30
}
diff --git a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserIdentifierType.cs b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserIdentifierType.cs
index 7267c203..3db0c4c1 100644
--- a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserIdentifierType.cs
+++ b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserIdentifierType.cs
@@ -2,8 +2,8 @@
public enum UserIdentifierType
{
- Username,
- Email,
- Phone,
- Custom
+ Username = 0,
+ Email = 10,
+ Phone = 20,
+ Custom = 100
}
diff --git a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserQuery.cs b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserQuery.cs
index 43e1fcf8..596f089d 100644
--- a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserQuery.cs
+++ b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserQuery.cs
@@ -7,4 +7,5 @@ public sealed record UserQuery : PageRequest
public string? Search { get; set; }
public UserStatus? Status { get; set; }
public bool IncludeDeleted { get; set; }
+ public ProfileKey? ProfileKey { get; set; }
}
diff --git a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserView.cs b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserView.cs
index b245402d..7db9293b 100644
--- a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserView.cs
+++ b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserView.cs
@@ -12,6 +12,7 @@ public sealed record UserView
public string? PrimaryEmail { get; init; }
public string? PrimaryPhone { get; init; }
+ public ProfileKey ProfileKey { get; set; }
public string? FirstName { get; init; }
public string? LastName { get; init; }
public string? DisplayName { get; init; }
diff --git a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/CreateProfileRequest.cs b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/CreateProfileRequest.cs
new file mode 100644
index 00000000..b1ed0059
--- /dev/null
+++ b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/CreateProfileRequest.cs
@@ -0,0 +1,22 @@
+namespace CodeBeam.UltimateAuth.Users.Contracts;
+
+public sealed class CreateProfileRequest
+{
+ public required ProfileKey ProfileKey { get; init; }
+
+ public ProfileKey? CloneFrom { get; init; }
+
+ public string? FirstName { get; init; }
+ public string? LastName { get; init; }
+ public string? DisplayName { get; init; }
+
+ public DateOnly? BirthDate { get; init; }
+ public string? Gender { get; init; }
+ public string? Bio { get; init; }
+
+ public string? Language { get; init; }
+ public string? TimeZone { get; init; }
+ public string? Culture { get; init; }
+
+ public Dictionary? Metadata { get; init; }
+}
diff --git a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/DeleteProfileRequest.cs b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/DeleteProfileRequest.cs
new file mode 100644
index 00000000..ca822e34
--- /dev/null
+++ b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/DeleteProfileRequest.cs
@@ -0,0 +1,6 @@
+namespace CodeBeam.UltimateAuth.Users.Contracts;
+
+public sealed class DeleteProfileRequest
+{
+ public required ProfileKey ProfileKey { get; init; }
+}
diff --git a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/GetProfileRequest.cs b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/GetProfileRequest.cs
new file mode 100644
index 00000000..5d730eab
--- /dev/null
+++ b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/GetProfileRequest.cs
@@ -0,0 +1,6 @@
+namespace CodeBeam.UltimateAuth.Users.Contracts;
+
+public sealed class GetProfileRequest
+{
+ public ProfileKey? ProfileKey { get; init; }
+}
diff --git a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/UpdateProfileRequest.cs b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/UpdateProfileRequest.cs
index 018aac82..73093aa2 100644
--- a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/UpdateProfileRequest.cs
+++ b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/UpdateProfileRequest.cs
@@ -2,6 +2,7 @@
public sealed record UpdateProfileRequest
{
+ public ProfileKey? ProfileKey { get; set; }
public string? FirstName { get; init; }
public string? LastName { get; init; }
public string? DisplayName { get; init; }
diff --git a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Data/UAuthUsersModelBuilder.cs b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Data/UAuthUsersModelBuilder.cs
index edbf278c..d5d6763d 100644
--- a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Data/UAuthUsersModelBuilder.cs
+++ b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Data/UAuthUsersModelBuilder.cs
@@ -1,6 +1,8 @@
using CodeBeam.UltimateAuth.Core.Domain;
using CodeBeam.UltimateAuth.Core.MultiTenancy;
using CodeBeam.UltimateAuth.EntityFrameworkCore;
+using CodeBeam.UltimateAuth.Users.Contracts;
+using CodeBeam.UltimateAuth.Users.Reference;
using Microsoft.EntityFrameworkCore;
namespace CodeBeam.UltimateAuth.Users.EntityFrameworkCore;
@@ -108,6 +110,11 @@ private static void ConfigureProfiles(ModelBuilder b)
.HasMaxLength(128)
.IsRequired();
+ e.Property(x => x.ProfileKey)
+ .HasConversion(v => v.Value, v => ProfileKey.Parse(v, null))
+ .HasMaxLength(64)
+ .IsRequired();
+
e.Property(x => x.Metadata)
.HasConversion(new NullableJsonValueConverter>())
.Metadata.SetValueComparer(JsonValueComparers.Create>());
@@ -116,7 +123,7 @@ private static void ConfigureProfiles(ModelBuilder b)
e.Property(x => x.UpdatedAt).HasNullableUtcDateTimeOffsetConverter();
e.Property(x => x.DeletedAt).HasNullableUtcDateTimeOffsetConverter();
- e.HasIndex(x => new { x.Tenant, x.UserKey });
+ e.HasIndex(x => new { x.Tenant, x.UserKey, x.ProfileKey }).IsUnique();
});
}
}
\ No newline at end of file
diff --git a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Mappers/UserProfileMapper.cs b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Mappers/UserProfileMapper.cs
index aaa2addb..8063234a 100644
--- a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Mappers/UserProfileMapper.cs
+++ b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Mappers/UserProfileMapper.cs
@@ -10,6 +10,7 @@ public static UserProfile ToDomain(this UserProfileProjection p)
p.Id,
p.Tenant,
p.UserKey,
+ p.ProfileKey,
p.FirstName,
p.LastName,
p.DisplayName,
@@ -33,6 +34,7 @@ public static UserProfileProjection ToProjection(this UserProfile d)
Id = d.Id,
Tenant = d.Tenant,
UserKey = d.UserKey,
+ ProfileKey = d.ProfileKey,
FirstName = d.FirstName,
LastName = d.LastName,
DisplayName = d.DisplayName,
@@ -66,7 +68,7 @@ public static void UpdateProjection(this UserProfile source, UserProfileProjecti
target.Culture = source.Culture;
// Version store-owned
- // Id / Tenant / UserKey / CreatedAt immutable
+ // Id / Tenant / UserKey / ProfileKey / CreatedAt immutable
}
}
diff --git a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Projections/UserProfileProjection.cs b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Projections/UserProfileProjection.cs
index 90dfed20..0698c205 100644
--- a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Projections/UserProfileProjection.cs
+++ b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Projections/UserProfileProjection.cs
@@ -1,5 +1,6 @@
using CodeBeam.UltimateAuth.Core.Domain;
using CodeBeam.UltimateAuth.Core.MultiTenancy;
+using CodeBeam.UltimateAuth.Users.Contracts;
namespace CodeBeam.UltimateAuth.Users.EntityFrameworkCore;
@@ -11,6 +12,8 @@ public sealed class UserProfileProjection
public UserKey UserKey { get; set; } = default!;
+ public ProfileKey ProfileKey { get; set; } = ProfileKey.Default;
+
public string? FirstName { get; set; }
public string? LastName { get; set; }
diff --git a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EFCoreUserProfileStoreFactory.cs b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EFCoreUserProfileStoreFactory.cs
index e3c4147e..68669465 100644
--- a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EFCoreUserProfileStoreFactory.cs
+++ b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EFCoreUserProfileStoreFactory.cs
@@ -15,6 +15,6 @@ public EfCoreUserProfileStoreFactory(TDbContext db)
public IUserProfileStore Create(TenantKey tenant)
{
- return new EfCoreUserProfileStore(_db, new TenantContext(tenant));
+ return new EfCoreUserProfileStore(_db, new TenantExecutionContext(tenant));
}
}
diff --git a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserIdentifierStore.cs b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserIdentifierStore.cs
index e52bb943..14e4de4f 100644
--- a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserIdentifierStore.cs
+++ b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserIdentifierStore.cs
@@ -13,7 +13,7 @@ internal sealed class EfCoreUserIdentifierStore : IUserIdentifierSto
private readonly TDbContext _db;
private readonly TenantKey _tenant;
- public EfCoreUserIdentifierStore(TDbContext db, TenantContext tenant)
+ public EfCoreUserIdentifierStore(TDbContext db, TenantExecutionContext tenant)
{
_db = db;
_tenant = tenant.Tenant;
diff --git a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserIdentifierStoreFactory.cs b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserIdentifierStoreFactory.cs
index 2d343b42..cd0be9fc 100644
--- a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserIdentifierStoreFactory.cs
+++ b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserIdentifierStoreFactory.cs
@@ -15,6 +15,6 @@ public EfCoreUserIdentifierStoreFactory(TDbContext db)
public IUserIdentifierStore Create(TenantKey tenant)
{
- return new EfCoreUserIdentifierStore(_db, new TenantContext(tenant));
+ return new EfCoreUserIdentifierStore(_db, new TenantExecutionContext(tenant));
}
}
diff --git a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserLifecycleStore.cs b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserLifecycleStore.cs
index 63c8ef35..9994d412 100644
--- a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserLifecycleStore.cs
+++ b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserLifecycleStore.cs
@@ -11,7 +11,7 @@ internal sealed class EfCoreUserLifecycleStore : IUserLifecycleStore
private readonly TDbContext _db;
private readonly TenantKey _tenant;
- public EfCoreUserLifecycleStore(TDbContext db, TenantContext tenant)
+ public EfCoreUserLifecycleStore(TDbContext db, TenantExecutionContext tenant)
{
_db = db;
_tenant = tenant.Tenant;
diff --git a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserLifecycleStoreFactory.cs b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserLifecycleStoreFactory.cs
index 7e5c4d44..38fb3af7 100644
--- a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserLifecycleStoreFactory.cs
+++ b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserLifecycleStoreFactory.cs
@@ -15,6 +15,6 @@ public EfCoreUserLifecycleStoreFactory(TDbContext db)
public IUserLifecycleStore Create(TenantKey tenant)
{
- return new EfCoreUserLifecycleStore(_db, new TenantContext(tenant));
+ return new EfCoreUserLifecycleStore(_db, new TenantExecutionContext(tenant));
}
}
diff --git a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserProfileStore.cs b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserProfileStore.cs
index 9623dc92..dbbe32fd 100644
--- a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserProfileStore.cs
+++ b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserProfileStore.cs
@@ -2,8 +2,10 @@
using CodeBeam.UltimateAuth.Core.Domain;
using CodeBeam.UltimateAuth.Core.Errors;
using CodeBeam.UltimateAuth.Core.MultiTenancy;
+using CodeBeam.UltimateAuth.Users.Contracts;
using CodeBeam.UltimateAuth.Users.Reference;
using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Internal;
namespace CodeBeam.UltimateAuth.Users.EntityFrameworkCore;
@@ -12,7 +14,7 @@ internal sealed class EfCoreUserProfileStore : IUserProfileStore whe
private readonly TDbContext _db;
private readonly TenantKey _tenant;
- public EfCoreUserProfileStore(TDbContext db, TenantContext tenant)
+ public EfCoreUserProfileStore(TDbContext db, TenantExecutionContext tenant)
{
_db = db;
_tenant = tenant.Tenant;
@@ -28,7 +30,8 @@ public EfCoreUserProfileStore(TDbContext db, TenantContext tenant)
.AsNoTracking()
.SingleOrDefaultAsync(x =>
x.Tenant == _tenant &&
- x.UserKey == key.UserKey,
+ x.UserKey == key.UserKey &&
+ x.ProfileKey == key.ProfileKey.Value,
ct);
return projection?.ToDomain();
@@ -41,7 +44,8 @@ public async Task ExistsAsync(UserProfileKey key, CancellationToken ct = d
return await DbSet
.AnyAsync(x =>
x.Tenant == _tenant &&
- x.UserKey == key.UserKey,
+ x.UserKey == key.UserKey &&
+ x.ProfileKey == key.ProfileKey.Value,
ct);
}
@@ -54,6 +58,16 @@ public async Task AddAsync(UserProfile entity, CancellationToken ct = default)
if (entity.Version != 0)
throw new InvalidOperationException("New profile must have version 0.");
+ var exists = await DbSet
+ .AnyAsync(x =>
+ x.Tenant == entity.Tenant.Value &&
+ x.UserKey == entity.UserKey.Value &&
+ x.ProfileKey == entity.ProfileKey.Value,
+ ct);
+
+ if (exists)
+ throw new UAuthConflictException("profile_already_exists");
+
DbSet.Add(projection);
await _db.SaveChangesAsync(ct);
@@ -66,7 +80,8 @@ public async Task SaveAsync(UserProfile entity, long expectedVersion, Cancellati
var existing = await DbSet
.SingleOrDefaultAsync(x =>
x.Tenant == _tenant &&
- x.UserKey == entity.UserKey,
+ x.UserKey == entity.UserKey &&
+ x.ProfileKey == entity.ProfileKey.Value,
ct);
if (existing is null)
@@ -88,7 +103,8 @@ public async Task DeleteAsync(UserProfileKey key, long expectedVersion, DeleteMo
var projection = await DbSet
.SingleOrDefaultAsync(x =>
x.Tenant == _tenant &&
- x.UserKey == key.UserKey,
+ x.UserKey == key.UserKey &&
+ x.ProfileKey == key.ProfileKey.Value,
ct);
if (projection is null)
@@ -120,6 +136,11 @@ public async Task> QueryAsync(UserProfileQuery query, C
.AsNoTracking()
.Where(x => x.Tenant == _tenant);
+ if (query.ProfileKey != null)
+ {
+ baseQuery = baseQuery.Where(x => x.ProfileKey == query.ProfileKey.Value);
+ }
+
if (!query.IncludeDeleted)
baseQuery = baseQuery.Where(x => x.DeletedAt == null);
@@ -164,7 +185,7 @@ public async Task> QueryAsync(UserProfileQuery query, C
query.Descending);
}
- public async Task> GetByUsersAsync(IReadOnlyList userKeys, CancellationToken ct = default)
+ public async Task> GetByUsersAsync(IReadOnlyList userKeys, ProfileKey profileKey, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
@@ -172,9 +193,22 @@ public async Task> GetByUsersAsync(IReadOnlyList x.Tenant == _tenant)
.Where(x => userKeys.Contains(x.UserKey))
+ .Where(x => x.ProfileKey == profileKey.Value)
.Where(x => x.DeletedAt == null)
.ToListAsync(ct);
return projections.Select(x => x.ToDomain()).ToList();
}
+
+ public async Task> GetAllProfilesByUserAsync(UserKey userKey, CancellationToken ct = default)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var projections = await DbSet
+ .AsNoTracking()
+ .Where(x => x.Tenant == _tenant)
+ .Where(x => x.UserKey == userKey)
+ .ToListAsync(ct);
+ return projections.Select(x => x.ToDomain()).ToList();
+ }
}
diff --git a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserIdentifierStore.cs b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserIdentifierStore.cs
index cd624dbc..698836dd 100644
--- a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserIdentifierStore.cs
+++ b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserIdentifierStore.cs
@@ -13,7 +13,7 @@ public sealed class InMemoryUserIdentifierStore : InMemoryTenantVersionedStore entity.Id;
private readonly object _primaryLock = new();
- public InMemoryUserIdentifierStore(TenantContext tenant) : base(tenant)
+ public InMemoryUserIdentifierStore(TenantExecutionContext tenant) : base(tenant)
{
}
diff --git a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserIdentifierStoreFactory.cs b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserIdentifierStoreFactory.cs
index 828fcc51..c6145e2a 100644
--- a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserIdentifierStoreFactory.cs
+++ b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserIdentifierStoreFactory.cs
@@ -20,7 +20,7 @@ public IUserIdentifierStore Create(TenantKey tenant)
return _stores.GetOrAdd(tenant, t =>
{
Console.WriteLine("New Store Added");
- var tenantContext = new TenantContext(tenant);
+ var tenantContext = new TenantExecutionContext(tenant);
return ActivatorUtilities.CreateInstance(_provider, tenantContext);
});
}
diff --git a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserLifecycleStore.cs b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserLifecycleStore.cs
index 4546acfc..0cd4633b 100644
--- a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserLifecycleStore.cs
+++ b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserLifecycleStore.cs
@@ -10,7 +10,7 @@ public sealed class InMemoryUserLifecycleStore : InMemoryTenantVersionedStore new(entity.Tenant, entity.UserKey);
- public InMemoryUserLifecycleStore(TenantContext tenant) : base(tenant)
+ public InMemoryUserLifecycleStore(TenantExecutionContext tenant) : base(tenant)
{
}
diff --git a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserLifecycleStoreFactory.cs b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserLifecycleStoreFactory.cs
index f19359f7..6794d694 100644
--- a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserLifecycleStoreFactory.cs
+++ b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserLifecycleStoreFactory.cs
@@ -19,7 +19,7 @@ public IUserLifecycleStore Create(TenantKey tenant)
{
return _stores.GetOrAdd(tenant, t =>
{
- var tenantContext = new TenantContext(tenant);
+ var tenantContext = new TenantExecutionContext(tenant);
return ActivatorUtilities.CreateInstance(_provider, tenantContext);
});
}
diff --git a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserProfileStore.cs b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserProfileStore.cs
index 38195576..129d7e92 100644
--- a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserProfileStore.cs
+++ b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserProfileStore.cs
@@ -1,7 +1,9 @@
using CodeBeam.UltimateAuth.Core.Contracts;
using CodeBeam.UltimateAuth.Core.Domain;
+using CodeBeam.UltimateAuth.Core.Errors;
using CodeBeam.UltimateAuth.Core.MultiTenancy;
using CodeBeam.UltimateAuth.InMemory;
+using CodeBeam.UltimateAuth.Users.Contracts;
using CodeBeam.UltimateAuth.Users.Reference;
namespace CodeBeam.UltimateAuth.Users.InMemory;
@@ -9,12 +11,23 @@ namespace CodeBeam.UltimateAuth.Users.InMemory;
public sealed class InMemoryUserProfileStore : InMemoryTenantVersionedStore, IUserProfileStore
{
protected override UserProfileKey GetKey(UserProfile entity)
- => new(entity.Tenant, entity.UserKey);
+ => new(entity.Tenant, entity.UserKey, entity.ProfileKey);
- public InMemoryUserProfileStore(TenantContext tenant) : base(tenant)
+ public InMemoryUserProfileStore(TenantExecutionContext tenant) : base(tenant)
{
}
+ protected override void BeforeAdd(UserProfile entity)
+ {
+ var exists = TenantValues()
+ .Any(x =>
+ x.UserKey == entity.UserKey &&
+ x.ProfileKey == entity.ProfileKey);
+
+ if (exists)
+ throw new UAuthConflictException("profile_already_exists");
+ }
+
public Task> QueryAsync(UserProfileQuery query, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
@@ -22,6 +35,11 @@ public Task> QueryAsync(UserProfileQuery query, Cancell
var normalized = query.Normalize();
var baseQuery = TenantValues().AsQueryable();
+ if (query.ProfileKey != null)
+ {
+ baseQuery = baseQuery.Where(x => x.ProfileKey == query.ProfileKey);
+ }
+
if (!query.IncludeDeleted)
baseQuery = baseQuery.Where(x => !x.IsDeleted);
@@ -69,19 +87,36 @@ public Task> QueryAsync(UserProfileQuery query, Cancell
query.Descending));
}
- public Task> GetByUsersAsync(IReadOnlyList userKeys, CancellationToken ct = default)
+ public Task> GetByUsersAsync(IReadOnlyList userKeys, ProfileKey profileKey, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
var set = userKeys.ToHashSet();
- var result = TenantValues()
+ var query = TenantValues()
.Where(x => set.Contains(x.UserKey))
- .Where(x => !x.IsDeleted)
+ .Where(x => x.ProfileKey == profileKey)
+ .Where(x => !x.IsDeleted);
+
+ var result = query
.Select(x => x.Snapshot())
.ToList()
.AsReadOnly();
return Task.FromResult>(result);
}
+
+ public Task> GetAllProfilesByUserAsync(UserKey userKey, CancellationToken ct = default)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var query = TenantValues()
+ .Where(x => x.UserKey == userKey)
+ .Where(x => !x.IsDeleted);
+ var result = query
+ .Select(x => x.Snapshot())
+ .ToList()
+ .AsReadOnly();
+ return Task.FromResult>(result);
+ }
}
\ No newline at end of file
diff --git a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserProfileStoreFactory.cs b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserProfileStoreFactory.cs
index b6f49bd4..0502c922 100644
--- a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserProfileStoreFactory.cs
+++ b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserProfileStoreFactory.cs
@@ -19,7 +19,7 @@ public IUserProfileStore Create(TenantKey tenant)
{
return _stores.GetOrAdd(tenant, t =>
{
- var tenantContext = new TenantContext(t);
+ var tenantContext = new TenantExecutionContext(t);
return ActivatorUtilities.CreateInstance(_provider, tenantContext);
});
}
diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Contracts/UserProfileQuery.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Contracts/UserProfileQuery.cs
index 3c3835e6..6e122100 100644
--- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Contracts/UserProfileQuery.cs
+++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Contracts/UserProfileQuery.cs
@@ -1,8 +1,10 @@
using CodeBeam.UltimateAuth.Core.Contracts;
+using CodeBeam.UltimateAuth.Users.Contracts;
namespace CodeBeam.UltimateAuth.Users.Reference;
public sealed record UserProfileQuery : PageRequest
{
public bool IncludeDeleted { get; init; }
+ public ProfileKey? ProfileKey { get; set; }
}
diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Domain/UserProfile.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Domain/UserProfile.cs
index 9bc18905..5830a22d 100644
--- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Domain/UserProfile.cs
+++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Domain/UserProfile.cs
@@ -1,10 +1,10 @@
using CodeBeam.UltimateAuth.Core.Abstractions;
using CodeBeam.UltimateAuth.Core.Domain;
using CodeBeam.UltimateAuth.Core.MultiTenancy;
+using CodeBeam.UltimateAuth.Users.Contracts;
namespace CodeBeam.UltimateAuth.Users.Reference;
-// TODO: Multi profile (e.g., public profiles, private profiles, profiles per application, etc. with ProfileKey)
public sealed class UserProfile : ITenantEntity, IVersionedEntity, ISoftDeletable, IEntitySnapshot
{
private UserProfile() { }
@@ -13,6 +13,7 @@ private UserProfile() { }
public TenantKey Tenant { get; private set; }
public UserKey UserKey { get; init; } = default!;
+ public ProfileKey ProfileKey { get; set; } = ProfileKey.Default;
public string? FirstName { get; private set; }
public string? LastName { get; private set; }
@@ -44,6 +45,7 @@ public UserProfile Snapshot()
Id = Id,
Tenant = Tenant,
UserKey = UserKey,
+ ProfileKey = ProfileKey,
FirstName = FirstName,
LastName = LastName,
DisplayName = DisplayName,
@@ -65,6 +67,7 @@ public static UserProfile Create(
Guid? id,
TenantKey tenant,
UserKey userKey,
+ ProfileKey? profileKey,
DateTimeOffset createdAt,
string? firstName = null,
string? lastName = null,
@@ -81,6 +84,7 @@ public static UserProfile Create(
Id = id ?? Guid.NewGuid(),
Tenant = tenant,
UserKey = userKey,
+ ProfileKey = profileKey ?? ProfileKey.Default,
FirstName = firstName,
LastName = lastName,
DisplayName = displayName,
@@ -169,6 +173,7 @@ public static UserProfile FromProjection(
Guid id,
TenantKey tenant,
UserKey userKey,
+ ProfileKey profileKey,
string? firstName,
string? lastName,
string? displayName,
@@ -189,6 +194,7 @@ public static UserProfile FromProjection(
Id = id,
Tenant = tenant,
UserKey = userKey,
+ ProfileKey = profileKey,
FirstName = firstName,
LastName = lastName,
DisplayName = displayName,
@@ -205,4 +211,47 @@ public static UserProfile FromProjection(
Version = version
};
}
+
+ public UserProfile CloneTo(
+ Guid? newId,
+ ProfileKey newProfileKey,
+ DateTimeOffset now,
+ Action? mutate = null)
+ {
+ if (IsDeleted)
+ throw new InvalidOperationException("cannot_clone_deleted_profile");
+
+ var clone = new UserProfile
+ {
+ Id = newId ?? Guid.NewGuid(),
+ Tenant = Tenant,
+ UserKey = UserKey,
+ ProfileKey = newProfileKey,
+
+ FirstName = FirstName,
+ LastName = LastName,
+ DisplayName = DisplayName,
+
+ BirthDate = BirthDate,
+ Gender = Gender,
+ Bio = Bio,
+
+ Language = Language,
+ TimeZone = TimeZone,
+ Culture = Culture,
+
+ Metadata = Metadata is null
+ ? null
+ : new Dictionary(Metadata),
+
+ CreatedAt = now,
+ UpdatedAt = null,
+ DeletedAt = null,
+ Version = 0
+ };
+
+ mutate?.Invoke(clone);
+
+ return clone;
+ }
}
diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Domain/UserProfileKey.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Domain/UserProfileKey.cs
index c197d94f..2aa643b6 100644
--- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Domain/UserProfileKey.cs
+++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Domain/UserProfileKey.cs
@@ -1,8 +1,10 @@
using CodeBeam.UltimateAuth.Core.Domain;
using CodeBeam.UltimateAuth.Core.MultiTenancy;
+using CodeBeam.UltimateAuth.Users.Contracts;
namespace CodeBeam.UltimateAuth.Users.Reference;
public readonly record struct UserProfileKey(
TenantKey Tenant,
- UserKey UserKey);
+ UserKey UserKey,
+ ProfileKey ProfileKey);
diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Endpoints/UserEndpointHandler.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Endpoints/UserEndpointHandler.cs
index b48a4ecf..7ec8f92d 100644
--- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Endpoints/UserEndpointHandler.cs
+++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Endpoints/UserEndpointHandler.cs
@@ -120,13 +120,15 @@ public async Task GetMeAsync(HttpContext ctx)
if (!flow.IsAuthenticated)
return Results.Unauthorized();
+ var request = await ctx.ReadJsonAsync(ctx.RequestAborted);
+
var accessContext = await _accessContextFactory.CreateAsync(
authFlow: flow,
action: UAuthActions.UserProfiles.GetSelf,
resource: "users",
resourceId: flow?.UserKey?.Value);
- var profile = await _users.GetMeAsync(accessContext, ctx.RequestAborted);
+ var profile = await _users.GetMeAsync(accessContext, request?.ProfileKey, ctx.RequestAborted);
return Results.Ok(profile);
}
@@ -136,13 +138,15 @@ public async Task GetUserAsync(UserKey userKey, HttpContext ctx)
if (!flow.IsAuthenticated)
return Results.Unauthorized();
+ var request = await ctx.ReadJsonAsync(ctx.RequestAborted);
+
var accessContext = await _accessContextFactory.CreateAsync(
authFlow: flow,
action: UAuthActions.UserProfiles.GetAdmin,
resource: "users",
resourceId: userKey.Value);
- var profile = await _users.GetUserProfileAsync(accessContext, ctx.RequestAborted);
+ var profile = await _users.GetUserProfileAsync(accessContext, request?.ProfileKey, ctx.RequestAborted);
return Results.Ok(profile);
}
@@ -220,6 +224,78 @@ public async Task DeleteAsync(UserKey userKey, HttpContext ctx)
return Results.Ok();
}
+ public async Task CreateProfileSelfAsync(HttpContext ctx)
+ {
+ var flow = _authFlow.Current;
+ if (!flow.IsAuthenticated)
+ return Results.Unauthorized();
+
+ var request = await ctx.ReadJsonAsync(ctx.RequestAborted);
+
+ var accessContext = await _accessContextFactory.CreateAsync(
+ authFlow: flow,
+ action: UAuthActions.UserProfiles.CreateSelf,
+ resource: "users",
+ resourceId: flow.UserKey!.Value);
+
+ await _users.CreateProfileAsync(accessContext, request, ctx.RequestAborted);
+ return Results.Ok();
+ }
+
+ public async Task CreateProfileAdminAsync(UserKey userKey, HttpContext ctx)
+ {
+ var flow = _authFlow.Current;
+ if (!flow.IsAuthenticated)
+ return Results.Unauthorized();
+
+ var request = await ctx.ReadJsonAsync(ctx.RequestAborted);
+
+ var accessContext = await _accessContextFactory.CreateAsync(
+ authFlow: flow,
+ action: UAuthActions.UserProfiles.CreateAdmin,
+ resource: "users",
+ resourceId: userKey.Value);
+
+ await _users.CreateProfileAsync(accessContext, request, ctx.RequestAborted);
+ return Results.Ok();
+ }
+
+ public async Task DeleteProfileSelfAsync(HttpContext ctx)
+ {
+ var flow = _authFlow.Current;
+ if (!flow.IsAuthenticated)
+ return Results.Unauthorized();
+
+ var request = await ctx.ReadJsonAsync(ctx.RequestAborted);
+
+ var accessContext = await _accessContextFactory.CreateAsync(
+ authFlow: flow,
+ action: UAuthActions.UserProfiles.DeleteSelf,
+ resource: "users",
+ resourceId: flow.UserKey!.Value);
+
+ await _users.DeleteProfileAsync(accessContext, request.ProfileKey, ctx.RequestAborted);
+ return Results.Ok();
+ }
+
+ public async Task DeleteProfileAdminAsync(UserKey userKey, HttpContext ctx)
+ {
+ var flow = _authFlow.Current;
+ if (!flow.IsAuthenticated)
+ return Results.Unauthorized();
+
+ var request = await ctx.ReadJsonAsync(ctx.RequestAborted);
+
+ var accessContext = await _accessContextFactory.CreateAsync(
+ authFlow: flow,
+ action: UAuthActions.UserProfiles.DeleteAdmin,
+ resource: "users",
+ resourceId: userKey.Value);
+
+ await _users.DeleteProfileAsync(accessContext, request.ProfileKey, ctx.RequestAborted);
+ return Results.Ok();
+ }
+
public async Task GetMyIdentifiersAsync(HttpContext ctx)
{
var flow = _authFlow.Current;
diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Infrastructure/UserProfileSnapshotProvider.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Infrastructure/UserProfileSnapshotProvider.cs
index 72cbe134..f924f3aa 100644
--- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Infrastructure/UserProfileSnapshotProvider.cs
+++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Infrastructure/UserProfileSnapshotProvider.cs
@@ -13,10 +13,10 @@ public UserProfileSnapshotProvider(IUserProfileStoreFactory storeFactory)
_storeFactory = storeFactory;
}
- public async Task GetAsync(TenantKey tenant, UserKey userKey, CancellationToken ct = default)
+ public async Task GetAsync(TenantKey tenant, UserKey userKey, ProfileKey profileKey, CancellationToken ct = default)
{
var store = _storeFactory.Create(tenant);
- var profile = await store.GetAsync(new UserProfileKey(tenant, userKey), ct);
+ var profile = await store.GetAsync(new UserProfileKey(tenant, userKey, profileKey), ct);
if (profile is null || profile.IsDeleted)
return null;
diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Mapping/UserProfileMapper.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Mapping/UserProfileMapper.cs
index e7a95f69..6f3cdf43 100644
--- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Mapping/UserProfileMapper.cs
+++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Mapping/UserProfileMapper.cs
@@ -8,6 +8,7 @@ public static UserView ToDto(UserProfile profile)
=> new()
{
UserKey = profile.UserKey,
+ ProfileKey = profile.ProfileKey,
FirstName = profile.FirstName,
LastName = profile.LastName,
DisplayName = profile.DisplayName,
diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Services/IUserApplicationService.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Services/IUserApplicationService.cs
index 5c9157f1..035eead1 100644
--- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Services/IUserApplicationService.cs
+++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Services/IUserApplicationService.cs
@@ -5,15 +5,17 @@ namespace CodeBeam.UltimateAuth.Users.Reference;
public interface IUserApplicationService
{
- Task GetMeAsync(AccessContext context, CancellationToken ct = default);
- Task GetUserProfileAsync(AccessContext context, CancellationToken ct = default);
+ Task GetMeAsync(AccessContext context, ProfileKey? profileKey = null, CancellationToken ct = default);
+ Task GetUserProfileAsync(AccessContext context, ProfileKey? profileKey = null, CancellationToken ct = default);
Task> QueryUsersAsync(AccessContext context, UserQuery query, CancellationToken ct = default);
Task CreateUserAsync(AccessContext context, CreateUserRequest request, CancellationToken ct = default);
Task ChangeUserStatusAsync(AccessContext context, object request, CancellationToken ct = default);
+ Task CreateProfileAsync(AccessContext context, CreateProfileRequest request, CancellationToken ct = default);
Task UpdateUserProfileAsync(AccessContext context, UpdateProfileRequest request, CancellationToken ct = default);
+ Task DeleteProfileAsync(AccessContext context, ProfileKey profileKey, CancellationToken ct = default);
Task> GetIdentifiersByUserAsync(AccessContext context, UserIdentifierQuery query, CancellationToken ct = default);
diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Services/UserApplicationService.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Services/UserApplicationService.cs
index aa354974..27ee484a 100644
--- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Services/UserApplicationService.cs
+++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Services/UserApplicationService.cs
@@ -75,6 +75,7 @@ await profileStore.AddAsync(
Guid.NewGuid(),
context.ResourceTenant,
userKey,
+ ProfileKey.Default,
now,
firstName: request.FirstName,
lastName: request.LastName,
@@ -195,15 +196,16 @@ public async Task DeleteMeAsync(AccessContext context, CancellationToken ct = de
var profileStore = _profileStoreFactory.Create(context.ResourceTenant);
var identifierStore = _identifierStoreFactory.Create(context.ResourceTenant);
- var profileKey = new UserProfileKey(context.ResourceTenant, userKey);
- var profile = await profileStore.GetAsync(profileKey, innerCt);
await lifecycleStore.DeleteAsync(lifecycleKey, lifecycle.Version, DeleteMode.Soft, now, innerCt);
await identifierStore.DeleteByUserAsync(userKey, DeleteMode.Soft, now, innerCt);
- if (profile is not null)
+ var profiles = await profileStore.GetAllProfilesByUserAsync(userKey, innerCt);
+
+ foreach (var profile in profiles)
{
- await profileStore.DeleteAsync(profileKey, profile.Version, DeleteMode.Soft, now, innerCt);
+ var key = new UserProfileKey(context.ResourceTenant, userKey, profile.ProfileKey);
+ await profileStore.DeleteAsync(key, profile.Version, DeleteMode.Soft, now, innerCt);
}
foreach (var integration in _integrations)
@@ -233,14 +235,15 @@ public async Task DeleteUserAsync(AccessContext context, DeleteUserRequest reque
var profileStore = _profileStoreFactory.Create(context.ResourceTenant);
var identifierStore = _identifierStoreFactory.Create(context.ResourceTenant);
- var profileKey = new UserProfileKey(context.ResourceTenant, targetUserKey);
- var profile = await profileStore.GetAsync(profileKey, innerCt);
await lifecycleStore.DeleteAsync(userLifecycleKey, lifecycle.Version, request.Mode, now, innerCt);
await identifierStore.DeleteByUserAsync(targetUserKey, request.Mode, now, innerCt);
- if (profile is not null)
+ var profiles = await profileStore.GetAllProfilesByUserAsync(targetUserKey, innerCt);
+
+ foreach (var profile in profiles)
{
- await profileStore.DeleteAsync(profileKey, profile.Version, request.Mode, now, innerCt);
+ var key = new UserProfileKey(context.ResourceTenant, profile.UserKey, profile.ProfileKey);
+ await profileStore.DeleteAsync(key, profile.Version, DeleteMode.Soft, now, innerCt);
}
foreach (var integration in _integrations)
@@ -257,31 +260,99 @@ public async Task DeleteUserAsync(AccessContext context, DeleteUserRequest reque
#region User Profile
- public async Task GetMeAsync(AccessContext context, CancellationToken ct = default)
+ public async Task GetMeAsync(AccessContext context, ProfileKey? profileKey = null, CancellationToken ct = default)
{
var command = new AccessCommand(async innerCt =>
{
+ var effectiveProfileKey = profileKey ?? ProfileKey.Default;
+
if (context.ActorUserKey is null)
throw new UnauthorizedAccessException();
- return await BuildUserViewAsync(context.ResourceTenant, context.ActorUserKey.Value, innerCt);
+ if (!_options.UserProfile.EnableMultiProfile && effectiveProfileKey != ProfileKey.Default)
+ throw new UAuthConflictException("multi_profile_disabled");
+
+ return await BuildUserViewAsync(context.ResourceTenant, context.ActorUserKey.Value, effectiveProfileKey, innerCt);
});
return await _accessOrchestrator.ExecuteAsync(context, command, ct);
}
- public async Task GetUserProfileAsync(AccessContext context, CancellationToken ct = default)
+ public async Task GetUserProfileAsync(AccessContext context, ProfileKey? profileKey = null, CancellationToken ct = default)
{
var command = new AccessCommand(async innerCt =>
{
+ var effectiveProfileKey = profileKey ?? ProfileKey.Default;
+
+ if (!_options.UserProfile.EnableMultiProfile && effectiveProfileKey != ProfileKey.Default)
+ throw new UAuthConflictException("multi_profile_disabled");
+
var targetUserKey = context.GetTargetUserKey();
- return await BuildUserViewAsync(context.ResourceTenant, targetUserKey, innerCt);
+ return await BuildUserViewAsync(context.ResourceTenant, targetUserKey, effectiveProfileKey, innerCt);
});
return await _accessOrchestrator.ExecuteAsync(context, command, ct);
}
+ public async Task CreateProfileAsync(AccessContext context, CreateProfileRequest request, CancellationToken ct = default)
+ {
+ var command = new AccessCommand(async innerCt =>
+ {
+ var tenant = context.ResourceTenant;
+ var userKey = context.GetTargetUserKey();
+ var now = _clock.UtcNow;
+
+ var profileKey = request.ProfileKey;
+
+ if (!_options.UserProfile.EnableMultiProfile)
+ throw new UAuthConflictException("multi_profile_disabled");
+
+ if (profileKey == ProfileKey.Default)
+ throw new UAuthConflictException("default_profile_already_exists");
+
+ var store = _profileStoreFactory.Create(tenant);
+
+ var exists = await store.ExistsAsync(new UserProfileKey(tenant, userKey, profileKey), innerCt);
+
+ if (exists)
+ throw new UAuthConflictException("profile_already_exists");
+
+ UserProfile profile;
+ if (request.CloneFrom is ProfileKey cloneFromKey)
+ {
+ var source = await store.GetAsync(new UserProfileKey(tenant, userKey, cloneFromKey), innerCt);
+
+ if (source == null)
+ throw new UAuthNotFoundException("source_profile_not_found");
+
+ profile = source.CloneTo(Guid.NewGuid(), profileKey, now);
+ }
+ else
+ {
+ profile = UserProfile.Create(
+ Guid.NewGuid(),
+ tenant,
+ userKey,
+ profileKey,
+ now,
+ firstName: request.FirstName,
+ lastName: request.LastName,
+ displayName: request.DisplayName,
+ birthDate: request.BirthDate,
+ gender: request.Gender,
+ bio: request.Bio,
+ language: request.Language,
+ timezone: request.TimeZone,
+ culture: request.Culture);
+ }
+
+ await store.AddAsync(profile, innerCt);
+ });
+
+ await _accessOrchestrator.ExecuteAsync(context, command, ct);
+ }
+
public async Task UpdateUserProfileAsync(AccessContext context, UpdateProfileRequest request, CancellationToken ct = default)
{
var command = new AccessCommand(async innerCt =>
@@ -290,13 +361,17 @@ public async Task UpdateUserProfileAsync(AccessContext context, UpdateProfileReq
var userKey = context.GetTargetUserKey();
var now = _clock.UtcNow;
- var key = new UserProfileKey(tenant, userKey);
+ var profileKey = request.ProfileKey ?? ProfileKey.Default;
+ var key = new UserProfileKey(tenant, userKey, profileKey);
var profileStore = _profileStoreFactory.Create(tenant);
var profile = await profileStore.GetAsync(key, innerCt);
if (profile is null)
throw new UAuthNotFoundException();
+ if (!_options.UserProfile.EnableMultiProfile && profileKey != ProfileKey.Default)
+ throw new UAuthConflictException("multi_profile_disabled");
+
var expectedVersion = profile.Version;
profile
@@ -311,6 +386,39 @@ public async Task UpdateUserProfileAsync(AccessContext context, UpdateProfileReq
await _accessOrchestrator.ExecuteAsync(context, command, ct);
}
+ public async Task DeleteProfileAsync(AccessContext context, ProfileKey profileKey, CancellationToken ct = default)
+ {
+ var command = new AccessCommand(async innerCt =>
+ {
+ var tenant = context.ResourceTenant;
+ var userKey = context.GetTargetUserKey();
+ var now = _clock.UtcNow;
+
+ if (!_options.UserProfile.EnableMultiProfile)
+ throw new UAuthConflictException("multi_profile_disabled");
+
+ if (profileKey == ProfileKey.Default)
+ throw new UAuthConflictException("cannot_delete_default_profile");
+
+ var store = _profileStoreFactory.Create(tenant);
+
+ var key = new UserProfileKey(tenant, userKey, profileKey);
+ var profile = await store.GetAsync(key, innerCt);
+
+ if (profile is null || profile.IsDeleted)
+ throw new UAuthNotFoundException("user_profile_not_found");
+
+ var profiles = await store.GetAllProfilesByUserAsync(userKey, innerCt);
+
+ if (profiles.Count <= 1)
+ throw new UAuthConflictException("cannot_delete_last_profile");
+
+ await store.DeleteAsync(key, profile.Version, DeleteMode.Soft, now, innerCt);
+ });
+
+ await _accessOrchestrator.ExecuteAsync(context, command, ct);
+ }
+
#endregion
@@ -658,13 +766,15 @@ public async Task DeleteUserIdentifierAsync(AccessContext context, DeleteUserIde
#region Helpers
- private async Task BuildUserViewAsync(TenantKey tenant, UserKey userKey, CancellationToken ct)
+ private async Task BuildUserViewAsync(TenantKey tenant, UserKey userKey, ProfileKey? profileKey, CancellationToken ct)
{
+ var effectiveProfileKey = profileKey ?? ProfileKey.Default;
+
var lifecycleStore = _lifecycleStoreFactory.Create(tenant);
var identifierStore = _identifierStoreFactory.Create(tenant);
var profileStore = _profileStoreFactory.Create(tenant);
var lifecycle = await lifecycleStore.GetAsync(new UserLifecycleKey(tenant, userKey));
- var profile = await profileStore.GetAsync(new UserProfileKey(tenant, userKey), ct);
+ var profile = await profileStore.GetAsync(new UserProfileKey(tenant, userKey, effectiveProfileKey), ct);
if (lifecycle is null || lifecycle.IsDeleted)
throw new UAuthNotFoundException("user_not_found");
@@ -751,6 +861,7 @@ public async Task> QueryUsersAsync(AccessContext contex
var command = new AccessCommand>(async innerCt =>
{
query ??= new UserQuery();
+ var effectiveProfileKey = query.ProfileKey ?? ProfileKey.Default;
var lifecycleQuery = new UserLifecycleQuery
{
@@ -778,7 +889,7 @@ public async Task> QueryUsersAsync(AccessContext contex
var userKeys = lifecycles.Select(x => x.UserKey).ToList();
var profileStore = _profileStoreFactory.Create(context.ResourceTenant);
var identifierStore = _identifierStoreFactory.Create(context.ResourceTenant);
- var profiles = await profileStore.GetByUsersAsync(userKeys, innerCt);
+ var profiles = await profileStore.GetByUsersAsync(userKeys, effectiveProfileKey, innerCt);
var identifiers = await identifierStore.GetByUsersAsync(userKeys, innerCt);
var profileMap = profiles.ToDictionary(x => x.UserKey);
var identifierGroups = identifiers.GroupBy(x => x.UserKey).ToDictionary(x => x.Key, x => x.ToList());
diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Stores/IUserProfileStore.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Stores/IUserProfileStore.cs
index 5d63cb60..0b6aff75 100644
--- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Stores/IUserProfileStore.cs
+++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Stores/IUserProfileStore.cs
@@ -1,11 +1,13 @@
using CodeBeam.UltimateAuth.Core.Abstractions;
using CodeBeam.UltimateAuth.Core.Contracts;
using CodeBeam.UltimateAuth.Core.Domain;
+using CodeBeam.UltimateAuth.Users.Contracts;
namespace CodeBeam.UltimateAuth.Users.Reference;
public interface IUserProfileStore : IVersionedStore
{
Task> QueryAsync(UserProfileQuery query, CancellationToken ct = default);
- Task> GetByUsersAsync(IReadOnlyList userKeys, CancellationToken ct = default);
+ Task> GetByUsersAsync(IReadOnlyList userKeys, ProfileKey profileKey, CancellationToken ct = default);
+ Task> GetAllProfilesByUserAsync(UserKey userKey, CancellationToken ct = default);
}
diff --git a/src/users/CodeBeam.UltimateAuth.Users/Abstractions/IUserProfileSnapshotProvider.cs b/src/users/CodeBeam.UltimateAuth.Users/Abstractions/IUserProfileSnapshotProvider.cs
index 38b2f00c..8a1789b7 100644
--- a/src/users/CodeBeam.UltimateAuth.Users/Abstractions/IUserProfileSnapshotProvider.cs
+++ b/src/users/CodeBeam.UltimateAuth.Users/Abstractions/IUserProfileSnapshotProvider.cs
@@ -6,5 +6,5 @@ namespace CodeBeam.UltimateAuth.Users;
public interface IUserProfileSnapshotProvider
{
- Task GetAsync(TenantKey tenant, UserKey userKey, CancellationToken ct = default);
+ Task GetAsync(TenantKey tenant, UserKey userKey, ProfileKey profileKey, CancellationToken ct = default);
}
diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/LoginTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/LoginTests.cs
index a06b2b12..5d8a86af 100644
--- a/tests/CodeBeam.UltimateAuth.Tests.Integration/LoginTests.cs
+++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/LoginTests.cs
@@ -1,4 +1,5 @@
-using FluentAssertions;
+using CodeBeam.UltimateAuth.Users.Contracts;
+using FluentAssertions;
using Microsoft.AspNetCore.Mvc.Testing;
using System.Net;
using System.Net.Http.Json;
@@ -83,14 +84,14 @@ public async Task Authenticated_User_Should_Access_Me_Endpoint()
var cookie = loginResponse.Headers.GetValues("Set-Cookie").First();
_client.DefaultRequestHeaders.Add("Cookie", cookie);
- var response = await _client.PostAsync("/auth/me/get", null);
+ var response = await _client.PostAsJsonAsync("/auth/me/profile/get", new GetProfileRequest() { ProfileKey = null });
response.StatusCode.Should().Be(HttpStatusCode.OK);
}
[Fact]
public async Task Anonymous_Should_Not_Access_Me()
{
- var response = await _client.PostAsync("/auth/me/get", null);
+ var response = await _client.PostAsync("/auth/me/profile/get", null);
response.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
}
}
\ No newline at end of file
diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/RefreshTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/RefreshTests.cs
new file mode 100644
index 00000000..dfd73b27
--- /dev/null
+++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/RefreshTests.cs
@@ -0,0 +1,200 @@
+using FluentAssertions;
+using Microsoft.AspNetCore.Mvc.Testing;
+using System.Net;
+using System.Net.Http.Json;
+
+namespace CodeBeam.UltimateAuth.Tests.Integration;
+
+public class RefreshTests : IClassFixture
+{
+ private readonly HttpClient _client;
+
+ public RefreshTests(AuthServerFactory factory)
+ {
+ _client = factory.CreateClient(new WebApplicationFactoryClientOptions
+ {
+ AllowAutoRedirect = false,
+ HandleCookies = false
+ });
+
+ _client.DefaultRequestHeaders.Add("Origin", "https://localhost:6130");
+ _client.DefaultRequestHeaders.Add("X-UDID", "test-device-1234567890123456");
+ }
+
+ [Fact]
+ public async Task Refresh_PureOpaque_Should_Touch_Session()
+ {
+ await LoginAsync("BlazorServer");
+ var response = await RefreshAsync();
+
+ response.StatusCode.Should().Be(HttpStatusCode.NoContent);
+ response.Headers.TryGetValues("Set-Cookie", out var cookies).Should().BeTrue();
+ }
+
+ [Fact]
+ public async Task Refresh_PureOpaque_Invalid_Should_Return_Unauthorized()
+ {
+ SetClientProfile("BlazorServer");
+ var response = await RefreshAsync();
+
+ response.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
+ }
+
+ [Fact]
+ public async Task Refresh_Hybrid_Should_Rotate_Tokens()
+ {
+ await LoginAsync("BlazorWasm");
+
+ var response = await RefreshAsync();
+
+ response.StatusCode.Should().Be(HttpStatusCode.NoContent);
+
+ response.Headers.TryGetValues("Set-Cookie", out var cookies).Should().BeTrue();
+ cookies.Should().NotBeEmpty();
+ }
+
+ [Fact]
+ public async Task Refresh_Hybrid_Should_Fail_On_Reuse()
+ {
+ await LoginAsync("BlazorWasm");
+
+ var first = await RefreshAsync();
+ first.StatusCode.Should().Be(HttpStatusCode.NoContent);
+
+ var second = await RefreshAsync();
+
+ second.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
+ }
+
+ [Fact]
+ public async Task Refresh_PureOpaque_Should_Not_Touch_Immediately()
+ {
+ await LoginAsync("BlazorServer");
+
+ var first = await RefreshAsync();
+ first.StatusCode.Should().Be(HttpStatusCode.NoContent);
+
+ var second = await RefreshAsync();
+
+ second.StatusCode.Should().Be(HttpStatusCode.NoContent);
+ }
+
+ [Fact]
+ public async Task Refresh_Hybrid_Should_Fail_When_Session_Mismatch()
+ {
+ var factory = new AuthServerFactory();
+
+ var client1 = factory.CreateClient(new WebApplicationFactoryClientOptions
+ {
+ AllowAutoRedirect = false,
+ HandleCookies = false
+ });
+
+ var client2 = factory.CreateClient(new WebApplicationFactoryClientOptions
+ {
+ AllowAutoRedirect = false,
+ HandleCookies = false
+ });
+
+ var cookie1 = await LoginAsync(client1, "BlazorWasm", "device-1-1234567890123456");
+ var cookie2 = await LoginAsync(client2, "BlazorWasm", "device-2-1234567890123456");
+
+ cookie1.Should().NotBeNullOrWhiteSpace();
+ cookie2.Should().NotBeNullOrWhiteSpace();
+ cookie1.Should().NotBe(cookie2);
+
+ client2.DefaultRequestHeaders.Remove("Cookie");
+ client2.DefaultRequestHeaders.Add("Cookie", cookie1);
+ var response = await client2.PostAsync("/auth/refresh", null);
+
+ response.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
+ }
+
+ [Fact]
+ public async Task Refresh_Hybrid_Should_Fail_Without_RefreshToken()
+ {
+ await LoginAsync("BlazorWasm");
+ var cookies = _client.DefaultRequestHeaders.GetValues("Cookie").First();
+ var onlySession = string.Join("; ", cookies.Split("; ").Where(x => x.StartsWith("uas=")));
+
+ _client.DefaultRequestHeaders.Remove("Cookie");
+ _client.DefaultRequestHeaders.Add("Cookie", onlySession);
+
+ var response = await _client.PostAsync("/auth/refresh", null);
+
+ response.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
+ }
+
+
+ private async Task LoginAsync(string profile)
+ {
+ SetClientProfile(profile);
+
+ var response = await _client.PostAsJsonAsync("/auth/login", new
+ {
+ identifier = "admin",
+ secret = "admin"
+ });
+
+ response.StatusCode.Should().Be(HttpStatusCode.Found);
+
+ var cookies = response.Headers.GetValues("Set-Cookie")
+ .Select(x => x.Split(';')[0]);
+
+ var cookieHeader = string.Join("; ", cookies);
+
+ _client.DefaultRequestHeaders.Remove("Cookie");
+ _client.DefaultRequestHeaders.Add("Cookie", cookieHeader);
+ }
+
+ private async Task LoginAsync(HttpClient client, string profile, string udid = "test-device-1234567890123456")
+ {
+ client.DefaultRequestHeaders.Remove("Origin");
+ client.DefaultRequestHeaders.Add("Origin", "https://localhost:6130");
+
+ client.DefaultRequestHeaders.Remove("X-UDID");
+ client.DefaultRequestHeaders.Add("X-UDID", udid);
+
+ SetClientProfile(client, profile);
+
+ var response = await client.PostAsJsonAsync("/auth/login", new
+ {
+ identifier = "admin",
+ secret = "admin"
+ });
+
+ response.StatusCode.Should().Be(HttpStatusCode.Found);
+
+ var cookieHeader = BuildCookieHeader(response);
+
+ client.DefaultRequestHeaders.Remove("Cookie");
+ client.DefaultRequestHeaders.Add("Cookie", cookieHeader);
+
+ return cookieHeader;
+ }
+
+ private void SetClientProfile(string profile)
+ {
+ _client.DefaultRequestHeaders.Remove("X-UAuth-ClientProfile");
+ _client.DefaultRequestHeaders.Add("X-UAuth-ClientProfile", profile);
+ }
+
+ private void SetClientProfile(HttpClient client, string profile)
+ {
+ client.DefaultRequestHeaders.Remove("X-UAuth-ClientProfile");
+ client.DefaultRequestHeaders.Add("X-UAuth-ClientProfile", profile);
+ }
+
+ private static string BuildCookieHeader(HttpResponseMessage response)
+ {
+ var cookies = response.Headers.GetValues("Set-Cookie")
+ .Select(x => x.Split(';')[0]);
+
+ return string.Join("; ", cookies);
+ }
+
+ private Task RefreshAsync()
+ {
+ return _client.PostAsync("/auth/refresh", null);
+ }
+}
diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/UserProfileTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/UserProfileTests.cs
new file mode 100644
index 00000000..4859ea70
--- /dev/null
+++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/UserProfileTests.cs
@@ -0,0 +1,215 @@
+using CodeBeam.UltimateAuth.Users.Contracts;
+using FluentAssertions;
+using Microsoft.AspNetCore.Mvc.Testing;
+using System.Net;
+using System.Net.Http.Json;
+
+namespace CodeBeam.UltimateAuth.Tests.Integration;
+
+public class UserProfileTests : IClassFixture
+{
+ private readonly HttpClient _client;
+
+ public UserProfileTests(AuthServerFactory factory)
+ {
+ _client = factory.CreateClient(new WebApplicationFactoryClientOptions
+ {
+ AllowAutoRedirect = false,
+ HandleCookies = false
+ });
+
+ _client.DefaultRequestHeaders.Add("Origin", "https://localhost:6130");
+ _client.DefaultRequestHeaders.Add("X-UDID", "test-device-1234567890123456");
+ }
+
+ [Fact]
+ public async Task Profile_Switch_Should_Return_Correct_Profile_Data()
+ {
+ var loginResponse = await _client.PostAsJsonAsync("/auth/login", new
+ {
+ identifier = "admin",
+ secret = "admin"
+ });
+
+ var cookie = loginResponse.Headers.GetValues("Set-Cookie").First();
+ _client.DefaultRequestHeaders.Add("Cookie", cookie);
+
+ var defaultResponse = await _client.PostAsJsonAsync("/auth/me/profile/get", new { });
+ defaultResponse.StatusCode.Should().Be(HttpStatusCode.OK);
+
+ var defaultProfile = await defaultResponse.Content.ReadFromJsonAsync();
+ defaultProfile.Should().NotBeNull();
+
+ var createResponse = await _client.PostAsJsonAsync("/auth/me/profile/create", new CreateProfileRequest
+ {
+ ProfileKey = ProfileKey.Parse("business", null)
+ });
+
+ createResponse.StatusCode.Should().Be(HttpStatusCode.OK);
+
+ var updateResponse = await _client.PostAsJsonAsync("/auth/me/profile/update", new UpdateProfileRequest()
+ {
+ ProfileKey = ProfileKey.Parse("business", null),
+ DisplayName = "Updated Business Name"
+ });
+
+ updateResponse.StatusCode.Should().Be(HttpStatusCode.OK);
+
+ var businessResponse = await _client.PostAsJsonAsync("/auth/me/profile/get", new GetProfileRequest()
+ {
+ ProfileKey = ProfileKey.Parse("business", null)
+ });
+
+ businessResponse.StatusCode.Should().Be(HttpStatusCode.OK);
+
+ var businessProfile = await businessResponse.Content.ReadFromJsonAsync();
+
+ businessProfile.Should().NotBeNull();
+ businessProfile!.DisplayName.Should().Be("Updated Business Name");
+
+ var defaultAgainResponse = await _client.PostAsJsonAsync("/auth/me/profile/get", new { });
+
+ var defaultAgain = await defaultAgainResponse.Content.ReadFromJsonAsync();
+
+ defaultAgain!.DisplayName.Should().Be(defaultProfile!.DisplayName);
+ }
+
+ [Fact]
+ public async Task GetMe_Without_ProfileKey_Should_Return_Default_Profile()
+ {
+ var login = await _client.PostAsJsonAsync("/auth/login", new
+ {
+ identifier = "admin",
+ secret = "admin"
+ });
+
+ var cookie = login.Headers.GetValues("Set-Cookie").First();
+ _client.DefaultRequestHeaders.Add("Cookie", cookie);
+
+ var response = await _client.PostAsJsonAsync("/auth/me/profile/get", new { });
+
+ var profile = await response.Content.ReadFromJsonAsync();
+
+ profile.Should().NotBeNull();
+ profile!.ProfileKey.Value.Should().Be("default");
+ }
+
+ [Fact]
+ public async Task Should_Not_Found_NonDefault_Profile_When_Not_Created()
+ {
+ var login = await _client.PostAsJsonAsync("/auth/login", new
+ {
+ identifier = "admin",
+ secret = "admin"
+ });
+
+ var cookie = login.Headers.GetValues("Set-Cookie").First();
+ _client.DefaultRequestHeaders.Add("Cookie", cookie);
+
+ var response = await _client.PostAsJsonAsync("/auth/me/profile/get", new
+ {
+ profileKey = "business"
+ });
+
+ response.StatusCode.Should().Be(HttpStatusCode.NotFound);
+ }
+
+ [Fact]
+ public async Task Should_Not_Create_Duplicate_Profile()
+ {
+ var login = await Login();
+
+ var key = ProfileKey.Parse($"business-{Guid.NewGuid()}", null);
+
+ var cookie = login.Headers.GetValues("Set-Cookie").First();
+ _client.DefaultRequestHeaders.Add("Cookie", cookie);
+
+ var request = new CreateProfileRequest
+ {
+ ProfileKey = key
+ };
+
+ var first = await _client.PostAsJsonAsync("/auth/me/profile/create", request);
+ first.StatusCode.Should().Be(HttpStatusCode.OK);
+
+ var second = await _client.PostAsJsonAsync("/auth/me/profile/create", request);
+ second.StatusCode.Should().Be(HttpStatusCode.Conflict);
+ }
+
+ [Fact]
+ public async Task Should_Not_Delete_Default_Profile()
+ {
+ var login = await Login();
+ var cookie = login.Headers.GetValues("Set-Cookie").First();
+ _client.DefaultRequestHeaders.Add("Cookie", cookie);
+
+ var response = await _client.PostAsJsonAsync("/auth/me/profile/delete", new
+ {
+ profileKey = "default"
+ });
+
+ response.StatusCode.Should().Be(HttpStatusCode.Conflict);
+ }
+
+ [Fact]
+ public async Task Should_Not_Update_NonExisting_Profile()
+ {
+ var login = await Login();
+ var cookie = login.Headers.GetValues("Set-Cookie").First();
+ _client.DefaultRequestHeaders.Add("Cookie", cookie);
+
+ var response = await _client.PostAsJsonAsync("/auth/me/profile/update", new UpdateProfileRequest
+ {
+ ProfileKey = ProfileKey.Parse("ghost", null),
+ DisplayName = "Should Fail"
+ });
+
+ response.StatusCode.Should().Be(HttpStatusCode.NotFound);
+ }
+
+ [Fact]
+ public async Task Deleted_Profile_Should_Not_Be_Returned()
+ {
+ var login = await Login();
+ var cookie = login.Headers.GetValues("Set-Cookie").First();
+ _client.DefaultRequestHeaders.Add("Cookie", cookie);
+
+ var key = ProfileKey.Parse("business", null);
+
+ await _client.PostAsJsonAsync("/auth/me/profile/create", new CreateProfileRequest
+ {
+ ProfileKey = key
+ });
+
+ await _client.PostAsJsonAsync("/auth/me/profile/delete", new
+ {
+ profileKey = key.Value
+ });
+
+ var response = await _client.PostAsJsonAsync("/auth/me/profile/get", new GetProfileRequest
+ {
+ ProfileKey = key
+ });
+
+ response.StatusCode.Should().Be(HttpStatusCode.NotFound);
+ }
+
+
+ private async Task