From 55bc8f03cfa7047c0ad906e5b6d1eb21d6d1a2bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Can=20Karag=C3=B6z?= Date: Wed, 1 Apr 2026 20:15:31 +0300 Subject: [PATCH 1/8] Final Preview Review --- .../Components/Dialogs/UsersDialog.razor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) { From 919a2db904129694749bfd3a72ba2dcd95bbb941 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Can=20Karag=C3=B6z?= Date: Sat, 4 Apr 2026 12:31:49 +0300 Subject: [PATCH 2/8] Minor Polish --- .../Components/Dialogs/UsersDialog.razor.cs | 2 +- .../Options/Validators/UAuthTokenOptionsValidator.cs | 2 +- .../Infrastructure/UAuthRequestClient.cs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) 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/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/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; } From ec8a424f5c13a204e78843c56434ce8e541ddd50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Can=20Karag=C3=B6z?= Date: Mon, 6 Apr 2026 02:36:34 +0300 Subject: [PATCH 3/8] Added Multi Profile Support --- .../UserSeedContributor.cs | 4 +- .../Program.cs | 1 + .../20260405222857_MultiProfile.Designer.cs | 716 ++++++++++++++++++ .../Migrations/20260405222857_MultiProfile.cs | 49 ++ .../Migrations/UAuthDbContextModelSnapshot.cs | 8 +- .../uauth.db-shm | Bin 32768 -> 32768 bytes .../uauth.db-wal | Bin 716912 -> 881712 bytes .../Defaults/UAuthActions.cs | 4 + .../Auth/AuthStateSnapshotFactory.cs | 3 +- .../Abstractions/IUserEndpointHandler.cs | 6 + .../Endpoints/UAuthEndpointRegistrar.cs | 20 +- .../Options/UAuthServerOptions.cs | 3 + .../Options/UAuthUserProfileOptions.cs | 11 + .../Services/Abstractions/IUserClient.cs | 8 +- .../Services/UAuthUserClient.cs | 69 +- .../Domain/ProfileKey.cs | 66 ++ .../Domain/ProfileKeyJsonConverter.cs | 28 + .../Dtos/UserQuery.cs | 1 + .../Dtos/UserView.cs | 1 + .../Requests/CreateProfileRequest.cs | 22 + .../Requests/DeleteProfileRequest.cs | 6 + .../Requests/GetProfileRequest.cs | 6 + .../Requests/UpdateProfileRequest.cs | 1 + .../Data/UAuthUsersModelBuilder.cs | 9 +- .../Mappers/UserProfileMapper.cs | 4 +- .../Projections/UserProfileProjection.cs | 3 + .../Stores/EfCoreUserProfileStore.cs | 44 +- .../Stores/InMemoryUserProfileStore.cs | 43 +- .../Contracts/UserProfileQuery.cs | 2 + .../Domain/UserProfile.cs | 51 +- .../Domain/UserProfileKey.cs | 4 +- .../Endpoints/UserEndpointHandler.cs | 80 +- .../UserProfileSnapshotProvider.cs | 4 +- .../Mapping/UserProfileMapper.cs | 1 + .../Services/IUserApplicationService.cs | 6 +- .../Services/UserApplicationService.cs | 143 +++- .../Stores/IUserProfileStore.cs | 4 +- .../IUserProfileSnapshotProvider.cs | 2 +- .../LoginTests.cs | 7 +- .../UserProfileTests.cs | 213 ++++++ .../Client/UAuthClientUserTests.cs | 14 +- .../EfCoreUserProfileStoreTests.cs | 204 ++++- 42 files changed, 1806 insertions(+), 65 deletions(-) create mode 100644 samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20260405222857_MultiProfile.Designer.cs create mode 100644 samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20260405222857_MultiProfile.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Options/UAuthUserProfileOptions.cs create mode 100644 src/users/CodeBeam.UltimateAuth.Users.Contracts/Domain/ProfileKey.cs create mode 100644 src/users/CodeBeam.UltimateAuth.Users.Contracts/Domain/ProfileKeyJsonConverter.cs create mode 100644 src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/CreateProfileRequest.cs create mode 100644 src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/DeleteProfileRequest.cs create mode 100644 src/users/CodeBeam.UltimateAuth.Users.Contracts/Requests/GetProfileRequest.cs create mode 100644 tests/CodeBeam.UltimateAuth.Tests.Integration/UserProfileTests.cs 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/Migrations/20260405222857_MultiProfile.Designer.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20260405222857_MultiProfile.Designer.cs new file mode 100644 index 00000000..5077af89 --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20260405222857_MultiProfile.Designer.cs @@ -0,0 +1,716 @@ +// +using System; +using CodeBeam.UltimateAuth.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.Migrations +{ + [DbContext(typeof(UAuthDbContext))] + [Migration("20260405222857_MultiProfile")] + partial class MultiProfile + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.5"); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore.AuthenticationSecurityStateProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CredentialType") + .HasColumnType("INTEGER"); + + b.Property("FailedAttempts") + .HasColumnType("INTEGER"); + + b.Property("LastFailedAt") + .HasColumnType("TEXT"); + + b.Property("LockedUntil") + .HasColumnType("TEXT"); + + b.Property("RequiresReauthentication") + .HasColumnType("INTEGER"); + + b.Property("ResetAttempts") + .HasColumnType("INTEGER"); + + b.Property("ResetConsumedAt") + .HasColumnType("TEXT"); + + b.Property("ResetExpiresAt") + .HasColumnType("TEXT"); + + b.Property("ResetRequestedAt") + .HasColumnType("TEXT"); + + b.Property("ResetTokenHash") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Scope") + .HasColumnType("INTEGER"); + + b.Property("SecurityVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "LockedUntil"); + + b.HasIndex("Tenant", "ResetRequestedAt"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "UserKey", "Scope"); + + b.HasIndex("Tenant", "UserKey", "Scope", "CredentialType") + .IsUnique(); + + b.ToTable("UAuth_Authentication", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.RolePermissionProjection", b => + { + b.Property("Tenant") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.Property("Permission") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Tenant", "RoleId", "Permission"); + + b.HasIndex("Tenant", "Permission"); + + b.HasIndex("Tenant", "RoleId"); + + b.ToTable("UAuth_RolePermissions", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.RoleProjection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "Id") + .IsUnique(); + + b.HasIndex("Tenant", "NormalizedName") + .IsUnique(); + + b.ToTable("UAuth_Roles", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.UserRoleProjection", b => + { + b.Property("Tenant") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UserKey") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.Property("AssignedAt") + .HasColumnType("TEXT"); + + b.HasKey("Tenant", "UserKey", "RoleId"); + + b.HasIndex("Tenant", "RoleId"); + + b.HasIndex("Tenant", "UserKey"); + + b.ToTable("UAuth_UserRoles", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore.PasswordCredentialProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("LastUsedAt") + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("SecretHash") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("Source") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "ExpiresAt"); + + b.HasIndex("Tenant", "Id") + .IsUnique(); + + b.HasIndex("Tenant", "RevokedAt"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "UserKey", "DeletedAt"); + + b.ToTable("UAuth_PasswordCredentials", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionChainProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AbsoluteExpiresAt") + .HasColumnType("TEXT"); + + b.Property("ActiveSessionId") + .HasColumnType("TEXT"); + + b.Property("ChainId") + .HasColumnType("TEXT"); + + b.Property("ClaimsSnapshot") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Device") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("LastSeenAt") + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("RootId") + .HasColumnType("TEXT"); + + b.Property("RotationCount") + .HasColumnType("INTEGER"); + + b.Property("SecurityVersionAtCreation") + .HasColumnType("INTEGER"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("TouchCount") + .HasColumnType("INTEGER"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "ChainId") + .IsUnique(); + + b.HasIndex("Tenant", "RootId"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "UserKey", "DeviceId"); + + b.ToTable("UAuth_SessionChains", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChainId") + .HasColumnType("TEXT"); + + b.Property("Claims") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Device") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("Metadata") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("SecurityVersionAtCreation") + .HasColumnType("INTEGER"); + + b.Property("SessionId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "ChainId"); + + b.HasIndex("Tenant", "ExpiresAt"); + + b.HasIndex("Tenant", "RevokedAt"); + + b.HasIndex("Tenant", "SessionId") + .IsUnique(); + + b.HasIndex("Tenant", "ChainId", "RevokedAt"); + + b.HasIndex("Tenant", "UserKey", "RevokedAt"); + + b.ToTable("UAuth_Sessions", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionRootProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("RootId") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("SecurityVersion") + .HasColumnType("INTEGER"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "RootId") + .IsUnique(); + + b.HasIndex("Tenant", "UserKey") + .IsUnique(); + + b.ToTable("UAuth_SessionRoots", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore.RefreshTokenProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChainId") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("ReplacedByTokenHash") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("SessionId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("TokenId") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "ChainId"); + + b.HasIndex("Tenant", "ExpiresAt"); + + b.HasIndex("Tenant", "ReplacedByTokenHash"); + + b.HasIndex("Tenant", "SessionId"); + + b.HasIndex("Tenant", "TokenHash") + .IsUnique(); + + b.HasIndex("Tenant", "TokenId"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "ExpiresAt", "RevokedAt"); + + b.HasIndex("Tenant", "TokenHash", "RevokedAt"); + + b.ToTable("UAuth_RefreshTokens", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Users.EntityFrameworkCore.UserIdentifierProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("IsPrimary") + .HasColumnType("INTEGER"); + + b.Property("NormalizedValue") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("VerifiedAt") + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "NormalizedValue"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "Type", "NormalizedValue") + .IsUnique(); + + b.HasIndex("Tenant", "UserKey", "IsPrimary"); + + b.HasIndex("Tenant", "UserKey", "Type", "IsPrimary"); + + b.ToTable("UAuth_UserIdentifiers", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Users.EntityFrameworkCore.UserLifecycleProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("SecurityVersion") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "UserKey") + .IsUnique(); + + b.ToTable("UAuth_UserLifecycles", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Users.EntityFrameworkCore.UserProfileProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Bio") + .HasColumnType("TEXT"); + + b.Property("BirthDate") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Culture") + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .HasColumnType("TEXT"); + + b.Property("FirstName") + .HasColumnType("TEXT"); + + b.Property("Gender") + .HasColumnType("TEXT"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("LastName") + .HasColumnType("TEXT"); + + b.Property("Metadata") + .HasColumnType("TEXT"); + + b.Property("ProfileKey") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("TimeZone") + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "UserKey", "ProfileKey") + .IsUnique(); + + b.ToTable("UAuth_UserProfiles", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionChainProjection", b => + { + b.HasOne("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionRootProjection", null) + .WithMany() + .HasForeignKey("Tenant", "RootId") + .HasPrincipalKey("Tenant", "RootId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionProjection", b => + { + b.HasOne("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionChainProjection", null) + .WithMany() + .HasForeignKey("Tenant", "ChainId") + .HasPrincipalKey("Tenant", "ChainId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20260405222857_MultiProfile.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20260405222857_MultiProfile.cs new file mode 100644 index 00000000..049933f1 --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20260405222857_MultiProfile.cs @@ -0,0 +1,49 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.Migrations +{ + /// + public partial class MultiProfile : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_UAuth_UserProfiles_Tenant_UserKey", + table: "UAuth_UserProfiles"); + + migrationBuilder.AddColumn( + name: "ProfileKey", + table: "UAuth_UserProfiles", + type: "TEXT", + maxLength: 64, + nullable: false, + defaultValue: ""); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_UserProfiles_Tenant_UserKey_ProfileKey", + table: "UAuth_UserProfiles", + columns: new[] { "Tenant", "UserKey", "ProfileKey" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_UAuth_UserProfiles_Tenant_UserKey_ProfileKey", + table: "UAuth_UserProfiles"); + + migrationBuilder.DropColumn( + name: "ProfileKey", + table: "UAuth_UserProfiles"); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_UserProfiles_Tenant_UserKey", + table: "UAuth_UserProfiles", + columns: new[] { "Tenant", "UserKey" }); + } + } +} 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-shm b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db-shm index 9c25688eed7baaff75ab914efad771e9281bd466..0c7cbb9c2724fa7c43ad5af45e889a2cd2506d70 100644 GIT binary patch delta 948 zcmb7?M@&>v6o$Xc^??#6U_>#tCev-TU>r~^ z`0N$LhKgMlCT`rhapA(9(evK8aA&;9pL5RtfA8M=@}9>N(V}*$Omt6e2GezsnV<-{P=A>SU~RBr%_*tijC|cH3w$ZJ2DM6)A$A<04mVbQ}v<##%P9 zl|2?363R91(rBe-`2?p@Po?!cl3B=d)=|VZ_Iiuca`biXN#Zlh(gk{ELn&mjf^0l& zXP-FPs;##!w?+@*jOo&`N=MOTfkrAb*&tn^;b1>UrSs{*RM?Oa%?<8bR>*j!GKYNW z^UZG=o*?5qmoTkShw(_N-xxwAe$%C6lo7*C9*F7F6PYH}t-~p`p`lb!9e{dqi+T$R zNu!p_T$Mgwx1-Dg^)Q|b7ORX{Zqr~kWXYXRNAe#qlbT^~1{vJ9BhoE{j-60mbYPs#Lpp z9iW^X;$(n|WFCuIMIoEnX;bmM2~hnx%qg4le#)y=Z*AASSRkKiijaPtz`KA~>y{sE O2J6N3>CwEG>i!Lc3;rAc delta 758 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 delta 9399 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 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.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/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/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/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/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IUserClient.cs b/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IUserClient.cs index 3034f685..8f941e55 100644 --- a/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IUserClient.cs +++ b/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IUserClient.cs @@ -14,9 +14,13 @@ public interface IUserClient Task DeleteMeAsync(); Task> DeleteUserAsync(UserKey userKey, DeleteUserRequest request); - Task> GetMeAsync(); + Task> GetMeAsync(GetProfileRequest? request = null); Task UpdateMeAsync(UpdateProfileRequest request); + Task CreateMyProfileAsync(CreateProfileRequest request); + Task DeleteMyProfileAsync(ProfileKey profileKey); - Task> GetUserAsync(UserKey userKey); + Task> GetUserAsync(UserKey userKey, GetProfileRequest? request = null); Task UpdateUserAsync(UserKey userKey, UpdateProfileRequest request); + Task CreateUserProfileAsync(UserKey userKey, CreateProfileRequest request); + Task DeleteUserProfileAsync(UserKey userKey, ProfileKey profileKey); } 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/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/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/EfCoreUserProfileStore.cs b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserProfileStore.cs index 9623dc92..963d4dec 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; @@ -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/InMemoryUserProfileStore.cs b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserProfileStore.cs index 38195576..bb683ffd 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) { } + 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.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/UserProfileTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/UserProfileTests.cs new file mode 100644 index 00000000..671f0344 --- /dev/null +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/UserProfileTests.cs @@ -0,0 +1,213 @@ +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 cookie = login.Headers.GetValues("Set-Cookie").First(); + _client.DefaultRequestHeaders.Add("Cookie", cookie); + + var request = new CreateProfileRequest + { + ProfileKey = ProfileKey.Parse("business", null) + }; + + 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 Login() + { + var response = await _client.PostAsJsonAsync("/auth/login", new + { + identifier = "admin", + secret = "admin" + }); + + response.StatusCode.Should().Be(HttpStatusCode.Found); + + var cookie = response.Headers.GetValues("Set-Cookie").First(); + + _client.DefaultRequestHeaders.Remove("Cookie"); + _client.DefaultRequestHeaders.Add("Cookie", cookie); + + return response; + } +} diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthClientUserTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthClientUserTests.cs index eb2cbf2c..aa718bf4 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthClientUserTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthClientUserTests.cs @@ -18,12 +18,15 @@ public async Task GetMe_Should_Call_Correct_Endpoint() UserKey = UserKey.FromString("user-1") }; - Request.Setup(x => x.SendFormAsync(It.IsAny())) + Request.Setup(x => x.SendJsonAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(SuccessJson(response)); var client = CreateUserClient(); await client.Users.GetMeAsync(); - Request.Verify(x => x.SendFormAsync("/auth/me/get"), Times.Once); + + Request.Verify(x => x.SendJsonAsync( + "/auth/me/profile/get", + It.Is(o => o is GetProfileRequest && ((GetProfileRequest)o).ProfileKey == null)), Times.Once); } [Fact] @@ -155,12 +158,15 @@ public async Task GetUser_Should_Call_Admin_Endpoint() UserKey = userKey }; - Request.Setup(x => x.SendFormAsync(It.IsAny())) + Request.Setup(x => x.SendJsonAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(SuccessJson(response)); var client = CreateUserClient(); await client.Users.GetUserAsync(userKey); - Request.Verify(x => x.SendFormAsync($"/auth/admin/users/{userKey.Value}/profile/get"), Times.Once); + + Request.Verify(x => x.SendJsonAsync( + $"/auth/admin/users/{userKey.Value}/profile/get", + It.Is(o => o is GetProfileRequest && ((GetProfileRequest)o).ProfileKey == null)), Times.Once); } [Fact] diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserProfileStoreTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserProfileStoreTests.cs index 87d84078..69b79f4f 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserProfileStoreTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserProfileStoreTests.cs @@ -3,6 +3,7 @@ using CodeBeam.UltimateAuth.Core.Errors; using CodeBeam.UltimateAuth.Core.MultiTenancy; using CodeBeam.UltimateAuth.Tests.Unit.Helpers; +using CodeBeam.UltimateAuth.Users.Contracts; using CodeBeam.UltimateAuth.Users.EntityFrameworkCore; using CodeBeam.UltimateAuth.Users.Reference; using Microsoft.Data.Sqlite; @@ -31,6 +32,7 @@ public async Task Add_And_Get_Should_Work() Guid.NewGuid(), tenant, userKey, + ProfileKey.Default, DateTimeOffset.UtcNow, displayName: "display", firstName: "first", @@ -38,7 +40,7 @@ public async Task Add_And_Get_Should_Work() ); await store.AddAsync(profile); - var result = await store.GetAsync(new UserProfileKey(tenant, userKey)); + var result = await store.GetAsync(new UserProfileKey(tenant, userKey, ProfileKey.Default)); Assert.NotNull(result); Assert.Equal(userKey, result!.UserKey); @@ -59,6 +61,7 @@ public async Task Exists_Should_Return_True_When_Exists() Guid.NewGuid(), tenant, userKey, + ProfileKey.Default, DateTimeOffset.UtcNow, displayName: "display", firstName: "first", @@ -66,7 +69,7 @@ public async Task Exists_Should_Return_True_When_Exists() ); await store.AddAsync(profile); - var exists = await store.ExistsAsync(new UserProfileKey(tenant, userKey)); + var exists = await store.ExistsAsync(new UserProfileKey(tenant, userKey, ProfileKey.Default)); Assert.True(exists); } @@ -83,6 +86,7 @@ public async Task Save_Should_Increment_Version() Guid.NewGuid(), tenant, userKey, + ProfileKey.Default, DateTimeOffset.UtcNow, displayName: "display", firstName: "first", @@ -98,7 +102,7 @@ public async Task Save_Should_Increment_Version() await using (var db2 = CreateDb(connection)) { var store2 = new EfCoreUserProfileStore(db2, new TenantContext(tenant)); - var existing = await store2.GetAsync(new UserProfileKey(tenant, userKey)); + var existing = await store2.GetAsync(new UserProfileKey(tenant, userKey, ProfileKey.Default)); var updated = existing!.UpdateName(existing.FirstName, existing.LastName, "new", DateTimeOffset.UtcNow); await store2.SaveAsync(updated, expectedVersion: 0); } @@ -106,7 +110,7 @@ public async Task Save_Should_Increment_Version() await using (var db3 = CreateDb(connection)) { var store3 = new EfCoreUserProfileStore(db3, new TenantContext(tenant)); - var result = await store3.GetAsync(new UserProfileKey(tenant, userKey)); + var result = await store3.GetAsync(new UserProfileKey(tenant, userKey, ProfileKey.Default)); Assert.Equal(1, result!.Version); Assert.Equal("new", result.DisplayName); @@ -125,6 +129,7 @@ public async Task Save_With_Wrong_Version_Should_Throw() Guid.NewGuid(), tenant, userKey, + ProfileKey.Default, DateTimeOffset.UtcNow, displayName: "display", firstName: "first", @@ -140,7 +145,7 @@ public async Task Save_With_Wrong_Version_Should_Throw() await using (var db2 = CreateDb(connection)) { var store2 = new EfCoreUserProfileStore(db2, new TenantContext(tenant)); - var existing = await store2.GetAsync(new UserProfileKey(tenant, userKey)); + var existing = await store2.GetAsync(new UserProfileKey(tenant, userKey, ProfileKey.Default)); var updated = existing!.UpdateName(existing.FirstName, existing.LastName, "new", DateTimeOffset.UtcNow); await Assert.ThrowsAsync(() => @@ -166,6 +171,7 @@ public async Task Should_Not_See_Data_From_Other_Tenant() Guid.NewGuid(), tenant1, userKey, + ProfileKey.Default, DateTimeOffset.UtcNow, displayName: "display", firstName: "first", @@ -173,7 +179,7 @@ public async Task Should_Not_See_Data_From_Other_Tenant() ); await store1.AddAsync(profile); - var result = await store2.GetAsync(new UserProfileKey(tenant2, userKey)); + var result = await store2.GetAsync(new UserProfileKey(tenant2, userKey, ProfileKey.Default)); Assert.Null(result); } @@ -193,6 +199,7 @@ public async Task Soft_Delete_Should_Work() Guid.NewGuid(), tenant, userKey, + ProfileKey.Default, DateTimeOffset.UtcNow, displayName: "display", firstName: "first", @@ -202,14 +209,195 @@ public async Task Soft_Delete_Should_Work() await store.AddAsync(profile); await store.DeleteAsync( - new UserProfileKey(tenant, userKey), + new UserProfileKey(tenant, userKey, ProfileKey.Default), expectedVersion: 0, DeleteMode.Soft, DateTimeOffset.UtcNow); - var result = await store.GetAsync(new UserProfileKey(tenant, userKey)); + var result = await store.GetAsync(new UserProfileKey(tenant, userKey, ProfileKey.Default)); Assert.NotNull(result); Assert.NotNull(result!.DeletedAt); } + + [Fact] + public async Task Same_User_Can_Have_Multiple_Profiles() + { + using var connection = CreateOpenConnection(); + await using var db = CreateDb(connection); + + var tenant = TenantKeys.Single; + var store = new EfCoreUserProfileStore(db, new TenantContext(tenant)); + + var userKey = UserKey.FromGuid(Guid.NewGuid()); + + var defaultProfile = UserProfile.Create( + Guid.NewGuid(), + tenant, + userKey, + ProfileKey.Default, + DateTimeOffset.UtcNow, + displayName: "default"); + + var businessProfile = UserProfile.Create( + Guid.NewGuid(), + tenant, + userKey, + ProfileKey.Parse("business", null), + DateTimeOffset.UtcNow, + displayName: "business"); + + await store.AddAsync(defaultProfile); + await store.AddAsync(businessProfile); + + var p1 = await store.GetAsync(new UserProfileKey(tenant, userKey, ProfileKey.Default)); + var p2 = await store.GetAsync(new UserProfileKey(tenant, userKey, ProfileKey.Parse("business", null))); + + Assert.NotNull(p1); + Assert.NotNull(p2); + Assert.NotEqual(p1!.ProfileKey, p2!.ProfileKey); + } + + [Fact] + public async Task GetAsync_Should_Return_Correct_Profile_By_ProfileKey() + { + using var connection = CreateOpenConnection(); + await using var db = CreateDb(connection); + + var tenant = TenantKeys.Single; + var store = new EfCoreUserProfileStore(db, new TenantContext(tenant)); + + var userKey = UserKey.FromGuid(Guid.NewGuid()); + + await store.AddAsync(UserProfile.Create( + Guid.NewGuid(), + tenant, + userKey, + ProfileKey.Default, + DateTimeOffset.UtcNow, + displayName: "default")); + + await store.AddAsync(UserProfile.Create( + Guid.NewGuid(), + tenant, + userKey, + ProfileKey.Parse("business", null), + DateTimeOffset.UtcNow, + displayName: "business")); + + var result = await store.GetAsync( + new UserProfileKey(tenant, userKey, ProfileKey.Parse("business", null))); + + Assert.Equal("business", result!.DisplayName); + } + + [Fact] + public async Task GetByUsersAsync_Should_Filter_By_ProfileKey() + { + using var connection = CreateOpenConnection(); + await using var db = CreateDb(connection); + + var tenant = TenantKeys.Single; + var store = new EfCoreUserProfileStore(db, new TenantContext(tenant)); + + var userKey = UserKey.FromGuid(Guid.NewGuid()); + + await store.AddAsync(UserProfile.Create( + Guid.NewGuid(), + tenant, + userKey, + ProfileKey.Default, + DateTimeOffset.UtcNow, + displayName: "default")); + + await store.AddAsync(UserProfile.Create( + Guid.NewGuid(), + tenant, + userKey, + ProfileKey.Parse("business", null), + DateTimeOffset.UtcNow, + displayName: "business")); + + var results = await store.GetByUsersAsync( + new[] { userKey }, + ProfileKey.Default); + + Assert.Single(results); + Assert.Equal(ProfileKey.Default, results[0].ProfileKey); + } + + [Fact] + public async Task Should_Not_Allow_Duplicate_ProfileKey_For_Same_User() + { + using var connection = CreateOpenConnection(); + await using var db = CreateDb(connection); + + var tenant = TenantKeys.Single; + var store = new EfCoreUserProfileStore(db, new TenantContext(tenant)); + + var userKey = UserKey.FromGuid(Guid.NewGuid()); + + var profile1 = UserProfile.Create( + Guid.NewGuid(), + tenant, + userKey, + ProfileKey.Default, + DateTimeOffset.UtcNow); + + var profile2 = UserProfile.Create( + Guid.NewGuid(), + tenant, + userKey, + ProfileKey.Default, + DateTimeOffset.UtcNow); + + await store.AddAsync(profile1); + + await Assert.ThrowsAsync(() => + store.AddAsync(profile2)); + } + + [Fact] + public async Task Delete_Should_Not_Affect_Other_Profiles() + { + using var connection = CreateOpenConnection(); + await using var db = CreateDb(connection); + + var tenant = TenantKeys.Single; + var store = new EfCoreUserProfileStore(db, new TenantContext(tenant)); + + var userKey = UserKey.FromGuid(Guid.NewGuid()); + + var defaultProfile = UserProfile.Create( + Guid.NewGuid(), + tenant, + userKey, + ProfileKey.Default, + DateTimeOffset.UtcNow); + + var businessProfile = UserProfile.Create( + Guid.NewGuid(), + tenant, + userKey, + ProfileKey.Parse("business", null), + DateTimeOffset.UtcNow); + + await store.AddAsync(defaultProfile); + await store.AddAsync(businessProfile); + + await store.DeleteAsync( + new UserProfileKey(tenant, userKey, ProfileKey.Default), + 0, + DeleteMode.Soft, + DateTimeOffset.UtcNow); + + var defaultResult = await store.GetAsync( + new UserProfileKey(tenant, userKey, ProfileKey.Default)); + + var businessResult = await store.GetAsync( + new UserProfileKey(tenant, userKey, ProfileKey.Parse("business", null))); + + Assert.NotNull(defaultResult!.DeletedAt); + Assert.Null(businessResult!.DeletedAt); + } } From ce4e4c3f8cb1715303507dd1fb788e776e0cffa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Can=20Karag=C3=B6z?= Date: Mon, 6 Apr 2026 02:45:16 +0300 Subject: [PATCH 4/8] Fix Test --- .../UserProfileTests.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/UserProfileTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/UserProfileTests.cs index 671f0344..4859ea70 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration/UserProfileTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/UserProfileTests.cs @@ -119,12 +119,14 @@ 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 = ProfileKey.Parse("business", null) + ProfileKey = key }; var first = await _client.PostAsJsonAsync("/auth/me/profile/create", request); From e3cd617bb1a8bb5c2f40c9b36f5a262393930a98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Can=20Karag=C3=B6z?= Date: Mon, 6 Apr 2026 11:16:33 +0300 Subject: [PATCH 5/8] Fix Warnings & Added Refresh Integration Tests --- .../Contracts/Pkce/PkceCompleteRequest.cs | 4 +- .../Infrastructure/SessionValidationMapper.cs | 2 +- .../AspNetCore/UAuthPolicyProvider.cs | 4 +- .../ResourceApi/ResourceAuthContextFactory.cs | 12 +- .../Services/UAuthFlowClient.cs | 2 +- .../RefreshTests.cs | 200 ++++++++++++++++++ 6 files changed, 214 insertions(+), 10 deletions(-) create mode 100644 tests/CodeBeam.UltimateAuth.Tests.Integration/RefreshTests.cs 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/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.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/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/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/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); + } +} From 429d5e8027a980c999a4cc8b19486febbcf245f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Can=20Karag=C3=B6z?= Date: Mon, 6 Apr 2026 21:13:31 +0300 Subject: [PATCH 6/8] Final Cleanup --- .../{Principals => User}/IUserIdConverter.cs | 0 .../IUserIdConverterResolver.cs | 0 .../{Principals => User}/IUserIdFactory.cs | 0 .../Contracts/Login/ExternalLoginRequest.cs | 1 - .../Contracts/Login/ReauthRequest.cs | 1 - .../Contracts/Refresh/RefreshStrategy.cs | 10 ---- .../Refresh}/SessionRefreshStatus.cs | 2 +- .../Contracts/Session/Dtos/IdentityInfo.cs | 1 - .../Contracts/Session/SessionStoreContext.cs | 43 ------------- .../Contracts/Token/AccessToken.cs | 2 +- .../Contracts/Token/TokenFormat.cs | 5 +- .../Contracts/Token/TokenType.cs | 8 --- .../Contracts/Token/TokenValidationResult.cs | 13 ++-- .../Defaults/UAuthConstants.cs | 1 + .../Domain/Auth/AuthArtifactType.cs | 16 ++--- .../Domain/Auth/AuthFlowType.cs | 29 +++++++++ .../Domain/AuthFlowType.cs | 30 ---------- .../Domain/Hub/HubErrorCode.cs | 8 +-- .../Domain/Hub/HubFlowType.cs | 8 +-- .../Domain/Pkce/HubLoginArtifact.cs | 17 ------ .../Domain/Principals/AuthFailureReason.cs | 18 +++--- .../Domain/Principals/GrantKind.cs | 6 +- .../Domain/Principals/PrimaryGrantKind.cs | 7 --- .../Principals}/PrimaryTokenKind.cs | 2 +- .../Domain/Principals/ReauthBehavior.cs | 6 +- .../Security/AuthenticationSecurityScope.cs | 2 +- .../Domain/Security/CredentialType.cs | 8 +-- .../Domain/Session/RefreshOutcome.cs | 10 ++-- .../{ => Runtime}/UAuthConflictException.cs | 0 .../{ => Runtime}/UAuthForbiddenException.cs | 0 .../UAuthUnauthorizedException.cs | 0 .../{ => Runtime}/UAuthValidationException.cs | 0 .../Events/UAuthEvents.cs | 4 +- .../Infrastructure/Base64Url.cs | 1 - .../Converters/DeviceContextJsonConverter.cs | 1 - .../Converters/UAuthUserIdConverter.cs | 2 - .../MultiTenancy/CompositeTenantResolver.cs | 1 - .../MultiTenancy/HeaderTenantResolver.cs | 1 - ...ntContext.cs => TenantExecutionContext.cs} | 4 +- .../MultiTenancy/UAuthTenantContext.cs | 6 +- .../Options/HeaderTokenFormat.cs | 4 +- .../IPrimaryCredentialResolver.cs | 2 +- .../Auth/ClientProfileReader.cs | 6 -- .../Auth/EffectiveUAuthServerOptions.cs | 2 - .../Auth/IPrimaryTokenResolver.cs | 2 +- .../Auth/PrimaryTokenResolver.cs | 2 +- .../UAuthAuthenticationExtension.cs | 2 +- .../AuthenticationSecurityManager.cs | 4 +- .../AspNetCore/UAuthAuthorizationHandler.cs | 6 +- .../AddUltimateAuthServerExtensions.cs | 20 ------- .../Composition/UltimateAuthServerBuilder.cs | 13 ---- .../UltimateAuthServerBuilderValidation.cs | 23 ------- .../Contracts/ResolvedCredential.cs | 2 +- .../Diagnostics/UAuthDiagnostic.cs | 6 +- .../Bridges/LoginEndpointHandlerBridge.cs | 16 ----- .../Bridges/LogoutEndpointHandlerBridge.cs | 17 ------ .../Bridges/PkceEndpointHandlerBridge.cs | 18 ------ .../Bridges/RefreshEndpointHandlerBridge.cs | 15 ----- .../Bridges/ValidateEndpointHandlerBridge.cs | 15 ----- .../Endpoints/ValidateEndpointHandler.cs | 2 +- .../Extensions/ClaimsSnapshotExtensions.cs | 10 ---- .../HttpContextRequestExtensions.cs | 9 ++- .../Extensions/ServiceCollectionExtensions.cs | 2 +- .../Flows/Login/LoginDecisionKind.cs | 6 +- .../Flows/Login/LoginExecutionMode.cs | 2 +- .../Flows/Pkce/PkceChallengeMethod.cs | 2 +- .../Flows/Pkce/PkceValidationFailureReason.cs | 12 ++-- .../Flows/Refresh/RefreshDecision.cs | 4 +- .../Flows/Refresh/RefreshEvaluationResult.cs | 5 -- .../Flows/Refresh/RefreshStrategyResolver.cs | 20 ------- .../Infrastructure/AccessPolicyProvider.cs | 1 - .../AspNetCore/TransportCredentialKind.cs | 8 +-- .../Credentials/PrimaryCredentialResolver.cs | 2 +- .../Credentials/ValidateCredentialResolver.cs | 8 +-- .../Issuers/UAuthTokenIssuer.cs | 4 +- .../Infrastructure/Redirect/ReturnUrlKind.cs | 6 +- .../Infrastructure/User/UAuthUserAccessor.cs | 1 - .../Infrastructure/User/UAuthUserId.cs | 11 ---- .../Options/UAuthHubDeploymentMode.cs | 6 +- .../Options/UAuthPrimaryCredentialPolicy.cs | 4 +- .../Runtime/UAuthServerProductInfoProvider.cs | 3 +- .../Services/UAuthJwtValidator.cs | 8 +-- .../EfCoreAuthenticationSecurityStateStore.cs | 2 +- ...AuthenticationSecurityStateStoreFactory.cs | 2 +- ...nMemoryAuthenticationSecurityStateStore.cs | 2 +- ...AuthenticationSecurityStateStoreFactory.cs | 2 +- .../Stores/EfCoreRoleStore.cs | 2 +- .../Stores/EfCoreRoleStoreFactory.cs | 2 +- .../Stores/EfCoreUserRoleStore.cs | 2 +- .../Stores/EfCoreUserRoleStoreFactory.cs | 2 +- .../Stores/InMemoryRoleStore.cs | 2 +- .../Stores/InMemoryRoleStoreFactory.cs | 2 +- .../Stores/InMemoryUserRoleStore.cs | 2 +- .../Stores/InMemoryUserRoleStoreFactory.cs | 2 +- .../Stores/EfCorePasswordCredentialStore.cs | 2 +- .../EfCorePasswordCredentialStoreFactory.cs | 2 +- .../InMemoryPasswordCredentialStore.cs | 2 +- .../InMemoryPasswordCredentialStoreFactory.cs | 2 +- .../InMemoryTenantVersionedStore.cs | 4 +- ...timateAuthServerBuilderArgon2Extensions.cs | 12 ---- .../Stores/EfCoreSessionStore.cs | 2 +- .../Stores/EfCoreSessionStoreFactory.cs | 2 +- .../Stores/EfCoreRefreshTokenStore.cs | 2 +- .../Stores/EfCoreRefreshTokenStoreFactory.cs | 2 +- .../Dtos/IdentifierExistenceScope.cs | 6 +- .../Dtos/MfaMethod.cs | 8 +-- .../Dtos/UserIdentifierType.cs | 8 +-- .../Stores/EFCoreUserProfileStoreFactory.cs | 2 +- .../Stores/EfCoreUserIdentifierStore.cs | 2 +- .../EfCoreUserIdentifierStoreFactory.cs | 2 +- .../Stores/EfCoreUserLifecycleStore.cs | 2 +- .../Stores/EfCoreUserLifecycleStoreFactory.cs | 2 +- .../Stores/EfCoreUserProfileStore.cs | 2 +- .../Stores/InMemoryUserIdentifierStore.cs | 2 +- .../InMemoryUserIdentifierStoreFactory.cs | 2 +- .../Stores/InMemoryUserLifecycleStore.cs | 2 +- .../InMemoryUserLifecycleStoreFactory.cs | 2 +- .../Stores/InMemoryUserProfileStore.cs | 2 +- .../Stores/InMemoryUserProfileStoreFactory.cs | 2 +- .../EfCoreAuthenticationStoreTests.cs | 30 +++++----- .../EfCoreCredentialStoreTests.cs | 32 +++++----- .../EfCoreRoleStoreTests.cs | 32 +++++----- .../EfCoreSessionStoreTests.cs | 60 +++++++++---------- .../EfCoreTokenStoreTests.cs | 10 ++-- .../EfCoreUserIdentifierStoreTests.cs | 20 +++---- .../EfCoreUserLifecycleStoreTests.cs | 26 ++++---- .../EfCoreUserProfileStoreTests.cs | 30 +++++----- .../EfCoreUserRoleStoreTests.cs | 16 ++--- .../Users/IdentifierConcurrencyTests.cs | 16 ++--- 129 files changed, 325 insertions(+), 634 deletions(-) rename src/CodeBeam.UltimateAuth.Core/Abstractions/{Principals => User}/IUserIdConverter.cs (100%) rename src/CodeBeam.UltimateAuth.Core/Abstractions/{Principals => User}/IUserIdConverterResolver.cs (100%) rename src/CodeBeam.UltimateAuth.Core/Abstractions/{Principals => User}/IUserIdFactory.cs (100%) delete mode 100644 src/CodeBeam.UltimateAuth.Core/Contracts/Refresh/RefreshStrategy.cs rename src/CodeBeam.UltimateAuth.Core/{Domain/Session => Contracts/Refresh}/SessionRefreshStatus.cs (70%) delete mode 100644 src/CodeBeam.UltimateAuth.Core/Contracts/Session/SessionStoreContext.cs delete mode 100644 src/CodeBeam.UltimateAuth.Core/Contracts/Token/TokenType.cs create mode 100644 src/CodeBeam.UltimateAuth.Core/Domain/Auth/AuthFlowType.cs delete mode 100644 src/CodeBeam.UltimateAuth.Core/Domain/AuthFlowType.cs delete mode 100644 src/CodeBeam.UltimateAuth.Core/Domain/Pkce/HubLoginArtifact.cs delete mode 100644 src/CodeBeam.UltimateAuth.Core/Domain/Principals/PrimaryGrantKind.cs rename src/CodeBeam.UltimateAuth.Core/{Contracts/Token => Domain/Principals}/PrimaryTokenKind.cs (58%) rename src/CodeBeam.UltimateAuth.Core/Errors/{ => Runtime}/UAuthConflictException.cs (100%) rename src/CodeBeam.UltimateAuth.Core/Errors/{ => Runtime}/UAuthForbiddenException.cs (100%) rename src/CodeBeam.UltimateAuth.Core/Errors/{ => Runtime}/UAuthUnauthorizedException.cs (100%) rename src/CodeBeam.UltimateAuth.Core/Errors/{ => Runtime}/UAuthValidationException.cs (100%) rename src/CodeBeam.UltimateAuth.Core/MultiTenancy/{TenantContext.cs => TenantExecutionContext.cs} (62%) delete mode 100644 src/CodeBeam.UltimateAuth.Server/Composition/Extensions/AddUltimateAuthServerExtensions.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Composition/UltimateAuthServerBuilder.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Composition/UltimateAuthServerBuilderValidation.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Endpoints/Bridges/LoginEndpointHandlerBridge.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Endpoints/Bridges/LogoutEndpointHandlerBridge.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Endpoints/Bridges/PkceEndpointHandlerBridge.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Endpoints/Bridges/RefreshEndpointHandlerBridge.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Endpoints/Bridges/ValidateEndpointHandlerBridge.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Extensions/ClaimsSnapshotExtensions.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Flows/Refresh/RefreshEvaluationResult.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Flows/Refresh/RefreshStrategyResolver.cs delete mode 100644 src/CodeBeam.UltimateAuth.Server/Infrastructure/User/UAuthUserId.cs delete mode 100644 src/security/CodeBeam.UltimateAuth.Security.Argon2/UltimateAuthServerBuilderArgon2Extensions.cs 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/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/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/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/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.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/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/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/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/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/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/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/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/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/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.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 963d4dec..dbbe32fd 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserProfileStore.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserProfileStore.cs @@ -14,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; 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 bb683ffd..129d7e92 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserProfileStore.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Stores/InMemoryUserProfileStore.cs @@ -13,7 +13,7 @@ public sealed class InMemoryUserProfileStore : InMemoryTenantVersionedStore new(entity.Tenant, entity.UserKey, entity.ProfileKey); - public InMemoryUserProfileStore(TenantContext tenant) : base(tenant) + public InMemoryUserProfileStore(TenantExecutionContext tenant) : base(tenant) { } 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/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreAuthenticationStoreTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreAuthenticationStoreTests.cs index 35d5a069..79de0b8f 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreAuthenticationStoreTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreAuthenticationStoreTests.cs @@ -22,7 +22,7 @@ public async Task Add_And_Get_Should_Work() await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCoreAuthenticationSecurityStateStore(db, new TenantContext(tenant)); + var store = new EfCoreAuthenticationSecurityStateStore(db, new TenantExecutionContext(tenant)); var userKey = UserKey.FromGuid(Guid.NewGuid()); @@ -50,13 +50,13 @@ public async Task Update_With_RegisterFailure_Should_Increment_Version() await using (var db1 = CreateDb(connection)) { - var store = new EfCoreAuthenticationSecurityStateStore(db1, new TenantContext(tenant)); + var store = new EfCoreAuthenticationSecurityStateStore(db1, new TenantExecutionContext(tenant)); await store.AddAsync(state); } await using (var db2 = CreateDb(connection)) { - var store = new EfCoreAuthenticationSecurityStateStore(db2, new TenantContext(tenant)); + var store = new EfCoreAuthenticationSecurityStateStore(db2, new TenantExecutionContext(tenant)); var existing = await store.GetAsync(userKey, AuthenticationSecurityScope.Account, null); var updated = existing!.RegisterFailure( @@ -69,7 +69,7 @@ public async Task Update_With_RegisterFailure_Should_Increment_Version() await using (var db3 = CreateDb(connection)) { - var store = new EfCoreAuthenticationSecurityStateStore(db3, new TenantContext(tenant)); + var store = new EfCoreAuthenticationSecurityStateStore(db3, new TenantExecutionContext(tenant)); var result = await store.GetAsync(userKey, AuthenticationSecurityScope.Account, null); Assert.Equal(1, result!.SecurityVersion); @@ -84,7 +84,7 @@ public async Task Update_With_Wrong_Version_Should_Throw() await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCoreAuthenticationSecurityStateStore(db, new TenantContext(tenant)); + var store = new EfCoreAuthenticationSecurityStateStore(db, new TenantExecutionContext(tenant)); var userKey = UserKey.FromGuid(Guid.NewGuid()); var state = AuthenticationSecurityState.CreateAccount(tenant, userKey); @@ -107,13 +107,13 @@ public async Task RegisterSuccess_Should_Clear_Failures() await using (var db1 = CreateDb(connection)) { - var store = new EfCoreAuthenticationSecurityStateStore(db1, new TenantContext(tenant)); + var store = new EfCoreAuthenticationSecurityStateStore(db1, new TenantExecutionContext(tenant)); await store.AddAsync(state); } await using (var db2 = CreateDb(connection)) { - var store = new EfCoreAuthenticationSecurityStateStore(db2, new TenantContext(tenant)); + var store = new EfCoreAuthenticationSecurityStateStore(db2, new TenantExecutionContext(tenant)); var existing = await store.GetAsync(userKey, AuthenticationSecurityScope.Account, null); var updated = existing!.RegisterSuccess(); await store.UpdateAsync(updated, expectedVersion: 1); @@ -121,7 +121,7 @@ public async Task RegisterSuccess_Should_Clear_Failures() await using (var db3 = CreateDb(connection)) { - var store = new EfCoreAuthenticationSecurityStateStore(db3, new TenantContext(tenant)); + var store = new EfCoreAuthenticationSecurityStateStore(db3, new TenantExecutionContext(tenant)); var result = await store.GetAsync(userKey, AuthenticationSecurityScope.Account, null); Assert.Equal(0, result!.FailedAttempts); @@ -140,7 +140,7 @@ public async Task BeginReset_And_Consume_Should_Work() await using (var db1 = CreateDb(connection)) { - var store = new EfCoreAuthenticationSecurityStateStore(db1, new TenantContext(tenant)); + var store = new EfCoreAuthenticationSecurityStateStore(db1, new TenantExecutionContext(tenant)); await store.AddAsync(state); } @@ -148,7 +148,7 @@ public async Task BeginReset_And_Consume_Should_Work() await using (var db2 = CreateDb(connection)) { - var store = new EfCoreAuthenticationSecurityStateStore(db2, new TenantContext(tenant)); + var store = new EfCoreAuthenticationSecurityStateStore(db2, new TenantExecutionContext(tenant)); var existing = await store.GetAsync(userKey, AuthenticationSecurityScope.Account, null); var updated = existing!.BeginReset("hash", now, TimeSpan.FromMinutes(10)); await store.UpdateAsync(updated, expectedVersion: 0); @@ -156,7 +156,7 @@ public async Task BeginReset_And_Consume_Should_Work() await using (var db3 = CreateDb(connection)) { - var store = new EfCoreAuthenticationSecurityStateStore(db3, new TenantContext(tenant)); + var store = new EfCoreAuthenticationSecurityStateStore(db3, new TenantExecutionContext(tenant)); var existing = await store.GetAsync(userKey, AuthenticationSecurityScope.Account, null); var consumed = existing!.ConsumeReset(DateTimeOffset.UtcNow); await store.UpdateAsync(consumed, expectedVersion: 1); @@ -164,7 +164,7 @@ public async Task BeginReset_And_Consume_Should_Work() await using (var db4 = CreateDb(connection)) { - var store = new EfCoreAuthenticationSecurityStateStore(db4, new TenantContext(tenant)); + var store = new EfCoreAuthenticationSecurityStateStore(db4, new TenantExecutionContext(tenant)); var result = await store.GetAsync(userKey, AuthenticationSecurityScope.Account, null); Assert.NotNull(result!.ResetConsumedAt); } @@ -177,7 +177,7 @@ public async Task Delete_Should_Work() await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCoreAuthenticationSecurityStateStore(db, new TenantContext(tenant)); + var store = new EfCoreAuthenticationSecurityStateStore(db, new TenantExecutionContext(tenant)); var userKey = UserKey.FromGuid(Guid.NewGuid()); @@ -201,8 +201,8 @@ public async Task Should_Not_See_Data_From_Other_Tenant() var tenant1 = TenantKeys.Single; var tenant2 = TenantKey.FromInternal("tenant-2"); - var store1 = new EfCoreAuthenticationSecurityStateStore(db, new TenantContext(tenant1)); - var store2 = new EfCoreAuthenticationSecurityStateStore(db, new TenantContext(tenant2)); + var store1 = new EfCoreAuthenticationSecurityStateStore(db, new TenantExecutionContext(tenant1)); + var store2 = new EfCoreAuthenticationSecurityStateStore(db, new TenantExecutionContext(tenant2)); var userKey = UserKey.FromGuid(Guid.NewGuid()); var state = AuthenticationSecurityState.CreateAccount(tenant1, userKey); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreCredentialStoreTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreCredentialStoreTests.cs index 23e651d1..2bafdc7d 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreCredentialStoreTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreCredentialStoreTests.cs @@ -24,7 +24,7 @@ public async Task Add_And_Get_Should_Work() await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCorePasswordCredentialStore(db, new TenantContext(tenant)); + var store = new EfCorePasswordCredentialStore(db, new TenantExecutionContext(tenant)); var userKey = UserKey.FromGuid(Guid.NewGuid()); @@ -52,7 +52,7 @@ public async Task Exists_Should_Return_True_When_Exists() await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCorePasswordCredentialStore(db, new TenantContext(tenant)); + var store = new EfCorePasswordCredentialStore(db, new TenantExecutionContext(tenant)); var userKey = UserKey.FromGuid(Guid.NewGuid()); @@ -90,13 +90,13 @@ public async Task Save_Should_Increment_Version() await using (var db1 = CreateDb(connection)) { - var store1 = new EfCorePasswordCredentialStore(db1, new TenantContext(tenant)); + var store1 = new EfCorePasswordCredentialStore(db1, new TenantExecutionContext(tenant)); await store1.AddAsync(credential); } await using (var db2 = CreateDb(connection)) { - var store2 = new EfCorePasswordCredentialStore(db2, new TenantContext(tenant)); + var store2 = new EfCorePasswordCredentialStore(db2, new TenantExecutionContext(tenant)); var existing = await store2.GetAsync(new CredentialKey(tenant, credential.Id)); var updated = existing!.ChangeSecret("new_hash", DateTimeOffset.UtcNow); await store2.SaveAsync(updated, expectedVersion: 0); @@ -104,7 +104,7 @@ public async Task Save_Should_Increment_Version() await using (var db3 = CreateDb(connection)) { - var store3 = new EfCorePasswordCredentialStore(db3, new TenantContext(tenant)); + var store3 = new EfCorePasswordCredentialStore(db3, new TenantExecutionContext(tenant)); var result = await store3.GetAsync(new CredentialKey(tenant, credential.Id)); Assert.Equal(1, result!.Version); @@ -131,13 +131,13 @@ public async Task Save_With_Wrong_Version_Should_Throw() await using (var db1 = CreateDb(connection)) { - var store1 = new EfCorePasswordCredentialStore(db1, new TenantContext(tenant)); + var store1 = new EfCorePasswordCredentialStore(db1, new TenantExecutionContext(tenant)); await store1.AddAsync(credential); } await using (var db2 = CreateDb(connection)) { - var store2 = new EfCorePasswordCredentialStore(db2, new TenantContext(tenant)); + var store2 = new EfCorePasswordCredentialStore(db2, new TenantExecutionContext(tenant)); var existing = await store2.GetAsync(new CredentialKey(tenant, credential.Id)); var updated = existing!.ChangeSecret("new_hash", DateTimeOffset.UtcNow); @@ -156,8 +156,8 @@ public async Task Should_Not_See_Data_From_Other_Tenant() var tenant1 = TenantKeys.Single; var tenant2 = TenantKey.FromInternal("tenant-2"); - var store1 = new EfCorePasswordCredentialStore(db, new TenantContext(tenant1)); - var store2 = new EfCorePasswordCredentialStore(db, new TenantContext(tenant2)); + var store1 = new EfCorePasswordCredentialStore(db, new TenantExecutionContext(tenant1)); + var store2 = new EfCorePasswordCredentialStore(db, new TenantExecutionContext(tenant2)); var userKey = UserKey.FromGuid(Guid.NewGuid()); @@ -183,7 +183,7 @@ public async Task Soft_Delete_Should_Work() await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCorePasswordCredentialStore(db, new TenantContext(tenant)); + var store = new EfCorePasswordCredentialStore(db, new TenantExecutionContext(tenant)); var userKey = UserKey.FromGuid(Guid.NewGuid()); @@ -229,13 +229,13 @@ public async Task Revoke_Should_Persist() await using (var db1 = CreateDb(connection)) { - var store1 = new EfCorePasswordCredentialStore(db1, new TenantContext(tenant)); + var store1 = new EfCorePasswordCredentialStore(db1, new TenantExecutionContext(tenant)); await store1.AddAsync(credential); } await using (var db2 = CreateDb(connection)) { - var store2 = new EfCorePasswordCredentialStore(db2, new TenantContext(tenant)); + var store2 = new EfCorePasswordCredentialStore(db2, new TenantExecutionContext(tenant)); var existing = await store2.GetAsync(new CredentialKey(tenant, credential.Id)); var revoked = existing!.Revoke(DateTimeOffset.UtcNow); await store2.SaveAsync(revoked, expectedVersion: 0); @@ -243,7 +243,7 @@ public async Task Revoke_Should_Persist() await using (var db3 = CreateDb(connection)) { - var store3 = new EfCorePasswordCredentialStore(db3, new TenantContext(tenant)); + var store3 = new EfCorePasswordCredentialStore(db3, new TenantExecutionContext(tenant)); var result = await store3.GetAsync(new CredentialKey(tenant, credential.Id)); Assert.True(result!.IsRevoked); @@ -269,13 +269,13 @@ public async Task ChangeSecret_Should_Update_SecurityState() await using (var db1 = CreateDb(connection)) { - var store1 = new EfCorePasswordCredentialStore(db1, new TenantContext(tenant)); + var store1 = new EfCorePasswordCredentialStore(db1, new TenantExecutionContext(tenant)); await store1.AddAsync(credential); } await using (var db2 = CreateDb(connection)) { - var store2 = new EfCorePasswordCredentialStore(db2, new TenantContext(tenant)); + var store2 = new EfCorePasswordCredentialStore(db2, new TenantExecutionContext(tenant)); var existing = await store2.GetAsync(new CredentialKey(tenant, credential.Id)); var updated = existing!.ChangeSecret("new_hash", DateTimeOffset.UtcNow); @@ -284,7 +284,7 @@ public async Task ChangeSecret_Should_Update_SecurityState() await using (var db3 = CreateDb(connection)) { - var store3 = new EfCorePasswordCredentialStore(db3, new TenantContext(tenant)); + var store3 = new EfCorePasswordCredentialStore(db3, new TenantExecutionContext(tenant)); var result = await store3.GetAsync(new CredentialKey(tenant, credential.Id)); Assert.Equal("new_hash", result!.SecretHash); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreRoleStoreTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreRoleStoreTests.cs index 8418e86c..92147b93 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreRoleStoreTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreRoleStoreTests.cs @@ -24,7 +24,7 @@ public async Task Add_And_Get_Should_Work() await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCoreRoleStore(db, new TenantContext(tenant)); + var store = new EfCoreRoleStore(db, new TenantExecutionContext(tenant)); var role = Role.Create( null, @@ -49,7 +49,7 @@ public async Task Add_With_Duplicate_Name_Should_Throw() await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCoreRoleStore(db, new TenantContext(tenant)); + var store = new EfCoreRoleStore(db, new TenantExecutionContext(tenant)); var role1 = Role.Create(null, tenant, "admin", null, DateTimeOffset.UtcNow); var role2 = Role.Create(null, tenant, "ADMIN", null, DateTimeOffset.UtcNow); @@ -69,7 +69,7 @@ public async Task Save_Should_Increment_Version() await using (var db = CreateDb(connection)) { - var store = new EfCoreRoleStore(db, new TenantContext(tenant)); + var store = new EfCoreRoleStore(db, new TenantExecutionContext(tenant)); var role = Role.Create(null, tenant, "admin", null, DateTimeOffset.UtcNow); roleId = role.Id; await store.AddAsync(role); @@ -77,7 +77,7 @@ public async Task Save_Should_Increment_Version() await using (var db = CreateDb(connection)) { - var store = new EfCoreRoleStore(db, new TenantContext(tenant)); + var store = new EfCoreRoleStore(db, new TenantExecutionContext(tenant)); var existing = await store.GetAsync(new RoleKey(tenant, roleId)); var updated = existing!.Rename("admin2", DateTimeOffset.UtcNow); await store.SaveAsync(updated, expectedVersion: 0); @@ -85,7 +85,7 @@ public async Task Save_Should_Increment_Version() await using (var db = CreateDb(connection)) { - var store = new EfCoreRoleStore(db, new TenantContext(tenant)); + var store = new EfCoreRoleStore(db, new TenantExecutionContext(tenant)); var result = await store.GetAsync(new RoleKey(tenant, roleId)); Assert.Equal(1, result!.Version); @@ -103,7 +103,7 @@ public async Task Save_With_Wrong_Version_Should_Throw() await using (var db = CreateDb(connection)) { - var store = new EfCoreRoleStore(db, new TenantContext(tenant)); + var store = new EfCoreRoleStore(db, new TenantExecutionContext(tenant)); var role = Role.Create(null, tenant, "admin", null, DateTimeOffset.UtcNow); roleId = role.Id; await store.AddAsync(role); @@ -111,7 +111,7 @@ public async Task Save_With_Wrong_Version_Should_Throw() await using (var db = CreateDb(connection)) { - var store = new EfCoreRoleStore(db, new TenantContext(tenant)); + var store = new EfCoreRoleStore(db, new TenantExecutionContext(tenant)); var existing = await store.GetAsync(new RoleKey(tenant, roleId)); var updated = existing!.Rename("admin2", DateTimeOffset.UtcNow); @@ -131,7 +131,7 @@ public async Task Rename_To_Existing_Name_Should_Throw() await using (var db = CreateDb(connection)) { - var store = new EfCoreRoleStore(db, new TenantContext(tenant)); + var store = new EfCoreRoleStore(db, new TenantExecutionContext(tenant)); var role1 = Role.Create(null, tenant, "admin", null, DateTimeOffset.UtcNow); var role2 = Role.Create(null, tenant, "user", null, DateTimeOffset.UtcNow); role1Id = role1.Id; @@ -142,7 +142,7 @@ public async Task Rename_To_Existing_Name_Should_Throw() await using (var db = CreateDb(connection)) { - var store = new EfCoreRoleStore(db, new TenantContext(tenant)); + var store = new EfCoreRoleStore(db, new TenantExecutionContext(tenant)); var role = await store.GetAsync(new RoleKey(tenant, role2Id)); var updated = role!.Rename("admin", DateTimeOffset.UtcNow); @@ -160,7 +160,7 @@ public async Task Save_Should_Replace_Permissions() await using (var db = CreateDb(connection)) { - var store = new EfCoreRoleStore(db, new TenantContext(tenant)); + var store = new EfCoreRoleStore(db, new TenantExecutionContext(tenant)); var role = Role.Create( null, @@ -176,7 +176,7 @@ public async Task Save_Should_Replace_Permissions() await using (var db = CreateDb(connection)) { - var store = new EfCoreRoleStore(db, new TenantContext(tenant)); + var store = new EfCoreRoleStore(db, new TenantExecutionContext(tenant)); var existing = await store.GetAsync(new RoleKey(tenant, roleId)); var updated = existing!.SetPermissions( new[] @@ -189,7 +189,7 @@ public async Task Save_Should_Replace_Permissions() await using (var db = CreateDb(connection)) { - var store = new EfCoreRoleStore(db, new TenantContext(tenant)); + var store = new EfCoreRoleStore(db, new TenantExecutionContext(tenant)); var result = await store.GetAsync(new RoleKey(tenant, roleId)); Assert.Single(result!.Permissions); @@ -207,7 +207,7 @@ public async Task Soft_Delete_Should_Work() await using (var db = CreateDb(connection)) { - var store = new EfCoreRoleStore(db, new TenantContext(tenant)); + var store = new EfCoreRoleStore(db, new TenantExecutionContext(tenant)); var role = Role.Create(null, tenant, "admin", null, DateTimeOffset.UtcNow); roleId = role.Id; await store.AddAsync(role); @@ -215,13 +215,13 @@ public async Task Soft_Delete_Should_Work() await using (var db = CreateDb(connection)) { - var store = new EfCoreRoleStore(db, new TenantContext(tenant)); + var store = new EfCoreRoleStore(db, new TenantExecutionContext(tenant)); await store.DeleteAsync(new RoleKey(tenant, roleId), 0, DeleteMode.Soft, DateTimeOffset.UtcNow); } await using (var db = CreateDb(connection)) { - var store = new EfCoreRoleStore(db, new TenantContext(tenant)); + var store = new EfCoreRoleStore(db, new TenantExecutionContext(tenant)); var result = await store.GetAsync(new RoleKey(tenant, roleId)); Assert.NotNull(result!.DeletedAt); } @@ -234,7 +234,7 @@ public async Task Query_Should_Filter_And_Page() await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCoreRoleStore(db, new TenantContext(tenant)); + var store = new EfCoreRoleStore(db, new TenantExecutionContext(tenant)); await store.AddAsync(Role.Create(null, tenant, "admin", null, DateTimeOffset.UtcNow)); await store.AddAsync(Role.Create(null, tenant, "user", null, DateTimeOffset.UtcNow)); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreSessionStoreTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreSessionStoreTests.cs index ac3d32c5..6eb7d7c9 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreSessionStoreTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreSessionStoreTests.cs @@ -24,7 +24,7 @@ public async Task Create_And_Get_Session_Should_Work() await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var store = new EfCoreSessionStore(db, new TenantExecutionContext(tenant)); var userKey = UserKey.FromGuid(Guid.NewGuid()); @@ -90,7 +90,7 @@ public async Task Session_Should_Persist_DeviceContext() await using (var db = CreateDb(connection)) { - var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var store = new EfCoreSessionStore(db, new TenantExecutionContext(tenant)); var root = UAuthSessionRoot.Create(tenant, userKey, DateTimeOffset.UtcNow); @@ -127,7 +127,7 @@ await store.ExecuteAsync(async ct => await using (var db = CreateDb(connection)) { - var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var store = new EfCoreSessionStore(db, new TenantExecutionContext(tenant)); var result = await store.GetSessionAsync(sessionId); @@ -167,7 +167,7 @@ public async Task Session_Should_Persist_Claims_And_Metadata() await using (var db = CreateDb(connection)) { - var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var store = new EfCoreSessionStore(db, new TenantExecutionContext(tenant)); var root = UAuthSessionRoot.Create(tenant, userKey, DateTimeOffset.UtcNow); @@ -204,7 +204,7 @@ await store.ExecuteAsync(async ct => await using (var db = CreateDb(connection)) { - var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var store = new EfCoreSessionStore(db, new TenantExecutionContext(tenant)); var result = await store.GetSessionAsync(sessionId); @@ -234,7 +234,7 @@ public async Task Revoke_Session_Should_Work() await using (var db = CreateDb(connection)) { - var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var store = new EfCoreSessionStore(db, new TenantExecutionContext(tenant)); var root = UAuthSessionRoot.Create(tenant, userKey, DateTimeOffset.UtcNow); @@ -288,7 +288,7 @@ public async Task Should_Not_See_Session_From_Other_Tenant() await using (var db = CreateDb(connection)) { - var store1 = new EfCoreSessionStore(db, new TenantContext(tenant1)); + var store1 = new EfCoreSessionStore(db, new TenantExecutionContext(tenant1)); var root = UAuthSessionRoot.Create(tenant1, userKey, DateTimeOffset.UtcNow); @@ -325,7 +325,7 @@ await store1.ExecuteAsync(async ct => await using (var db = CreateDb(connection)) { - var store2 = new EfCoreSessionStore(db, new TenantContext(tenant2)); + var store2 = new EfCoreSessionStore(db, new TenantExecutionContext(tenant2)); var result = await store2.GetSessionAsync(sessionId); @@ -343,7 +343,7 @@ public async Task ExecuteAsync_Should_Rollback_On_Error() await using (var db = CreateDb(connection)) { - var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var store = new EfCoreSessionStore(db, new TenantExecutionContext(tenant)); await Assert.ThrowsAsync(async () => { @@ -368,7 +368,7 @@ public async Task GetSessionsByChain_Should_Return_Sessions() await using (var db = CreateDb(connection)) { - var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var store = new EfCoreSessionStore(db, new TenantExecutionContext(tenant)); var root = UAuthSessionRoot.Create(tenant, userKey, DateTimeOffset.UtcNow); chainId = SessionChainId.New(); @@ -405,7 +405,7 @@ await store.ExecuteAsync(async ct => await using (var db = CreateDb(connection)) { - var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var store = new EfCoreSessionStore(db, new TenantExecutionContext(tenant)); var sessions = await store.GetSessionsByChainAsync(chainId); Assert.Single(sessions); } @@ -423,7 +423,7 @@ public async Task ExecuteAsync_Should_Commit_Multiple_Operations() await using (var db = CreateDb(connection)) { - var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var store = new EfCoreSessionStore(db, new TenantExecutionContext(tenant)); await store.ExecuteAsync(async ct => { @@ -460,7 +460,7 @@ await store.ExecuteAsync(async ct => await using (var db = CreateDb(connection)) { - var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var store = new EfCoreSessionStore(db, new TenantExecutionContext(tenant)); var result = await store.GetSessionAsync(sessionId); @@ -480,7 +480,7 @@ public async Task ExecuteAsync_Should_Rollback_All_On_Failure() await using (var db = CreateDb(connection)) { - var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var store = new EfCoreSessionStore(db, new TenantExecutionContext(tenant)); await Assert.ThrowsAsync(async () => { @@ -516,7 +516,7 @@ public async Task RevokeChainCascade_Should_Revoke_All_Sessions() await using (var db = CreateDb(connection)) { - var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var store = new EfCoreSessionStore(db, new TenantExecutionContext(tenant)); var root = UAuthSessionRoot.Create(tenant, userKey, DateTimeOffset.UtcNow); chainId = SessionChainId.New(); @@ -554,7 +554,7 @@ await store.ExecuteAsync(async ct => await using (var db = CreateDb(connection)) { - var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var store = new EfCoreSessionStore(db, new TenantExecutionContext(tenant)); await store.ExecuteAsync(async ct => { @@ -564,7 +564,7 @@ await store.ExecuteAsync(async ct => await using (var db = CreateDb(connection)) { - var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var store = new EfCoreSessionStore(db, new TenantExecutionContext(tenant)); var sessions = await store.GetSessionsByChainAsync(chainId); Assert.All(sessions, s => Assert.True(s.IsRevoked)); @@ -584,7 +584,7 @@ public async Task SetActiveSession_Should_Work() await using (var db = CreateDb(connection)) { - var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var store = new EfCoreSessionStore(db, new TenantExecutionContext(tenant)); var root = UAuthSessionRoot.Create(tenant, userKey, DateTimeOffset.UtcNow); @@ -612,7 +612,7 @@ await store.ExecuteAsync(async ct => await using (var db = CreateDb(connection)) { - var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var store = new EfCoreSessionStore(db, new TenantExecutionContext(tenant)); var active = await store.GetActiveSessionIdAsync(chainId); Assert.Equal(sessionId, active); @@ -648,7 +648,7 @@ public async Task SaveSession_Should_Increment_Version() await using (var db = CreateDb(connection)) { - var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var store = new EfCoreSessionStore(db, new TenantExecutionContext(tenant)); var root = UAuthSessionRoot.Create(tenant, userKey, DateTimeOffset.UtcNow); chainId = SessionChainId.New(); @@ -686,7 +686,7 @@ await store.ExecuteAsync(async ct => await using (var db = CreateDb(connection)) { - var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var store = new EfCoreSessionStore(db, new TenantExecutionContext(tenant)); await store.ExecuteAsync(async ct => { @@ -699,7 +699,7 @@ await store.ExecuteAsync(async ct => await using (var db = CreateDb(connection)) { - var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var store = new EfCoreSessionStore(db, new TenantExecutionContext(tenant)); var result = await store.GetSessionAsync(sessionId); Assert.Equal(1, result!.Version); @@ -719,7 +719,7 @@ public async Task SaveSession_With_Wrong_Version_Should_Throw() await using (var db = CreateDb(connection)) { - var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var store = new EfCoreSessionStore(db, new TenantExecutionContext(tenant)); var root = UAuthSessionRoot.Create(tenant, userKey, DateTimeOffset.UtcNow); chainId = SessionChainId.New(); @@ -757,7 +757,7 @@ await store.ExecuteAsync(async ct => await using (var db = CreateDb(connection)) { - var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var store = new EfCoreSessionStore(db, new TenantExecutionContext(tenant)); await Assert.ThrowsAsync(async () => { @@ -784,7 +784,7 @@ public async Task SaveChain_Should_Increment_Version() await using (var db = CreateDb(connection)) { - var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var store = new EfCoreSessionStore(db, new TenantExecutionContext(tenant)); var root = UAuthSessionRoot.Create(tenant, userKey, DateTimeOffset.UtcNow); chainId = SessionChainId.New(); @@ -809,7 +809,7 @@ await store.ExecuteAsync(async ct => await using (var db = CreateDb(connection)) { - var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var store = new EfCoreSessionStore(db, new TenantExecutionContext(tenant)); await store.ExecuteAsync(async ct => { @@ -822,7 +822,7 @@ await store.ExecuteAsync(async ct => await using (var db = CreateDb(connection)) { - var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var store = new EfCoreSessionStore(db, new TenantExecutionContext(tenant)); var result = await store.GetChainAsync(chainId); Assert.Equal(1, result!.Version); @@ -839,7 +839,7 @@ public async Task SaveRoot_Should_Increment_Version() await using (var db = CreateDb(connection)) { - var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var store = new EfCoreSessionStore(db, new TenantExecutionContext(tenant)); var root = UAuthSessionRoot.Create( tenant, @@ -854,7 +854,7 @@ await store.ExecuteAsync(async ct => await using (var db = CreateDb(connection)) { - var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var store = new EfCoreSessionStore(db, new TenantExecutionContext(tenant)); await store.ExecuteAsync(async ct => { @@ -867,7 +867,7 @@ await store.ExecuteAsync(async ct => await using (var db = CreateDb(connection)) { - var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var store = new EfCoreSessionStore(db, new TenantExecutionContext(tenant)); var result = await store.GetRootByUserAsync(userKey); Assert.Equal(1, result!.Version); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreTokenStoreTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreTokenStoreTests.cs index c7bd8179..87445ef0 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreTokenStoreTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreTokenStoreTests.cs @@ -22,7 +22,7 @@ public async Task Store_And_Find_Should_Work() await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCoreRefreshTokenStore(db, new TenantContext(tenant)); + var store = new EfCoreRefreshTokenStore(db, new TenantExecutionContext(tenant)); AuthSessionId.TryCreate(ValidRaw, out var sessionId); var token = RefreshToken.Create( @@ -58,7 +58,7 @@ public async Task Revoke_Should_Set_RevokedAt() await using (var db = CreateDb(connection)) { - var store = new EfCoreRefreshTokenStore(db, new TenantContext(tenant)); + var store = new EfCoreRefreshTokenStore(db, new TenantExecutionContext(tenant)); var token = RefreshToken.Create( TokenId.From(Guid.NewGuid()), @@ -79,7 +79,7 @@ await store.ExecuteAsync(async ct => await using (var db = CreateDb(connection)) { - var store = new EfCoreRefreshTokenStore(db, new TenantContext(tenant)); + var store = new EfCoreRefreshTokenStore(db, new TenantExecutionContext(tenant)); await store.ExecuteAsync(async ct => { @@ -89,7 +89,7 @@ await store.ExecuteAsync(async ct => await using (var db = CreateDb(connection)) { - var store = new EfCoreRefreshTokenStore(db, new TenantContext(tenant)); + var store = new EfCoreRefreshTokenStore(db, new TenantExecutionContext(tenant)); var result = await store.FindByHashAsync(tokenHash); Assert.NotNull(result!.RevokedAt); @@ -103,7 +103,7 @@ public async Task Store_Outside_Transaction_Should_Throw() await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCoreRefreshTokenStore(db, new TenantContext(tenant)); + var store = new EfCoreRefreshTokenStore(db, new TenantExecutionContext(tenant)); AuthSessionId.TryCreate(ValidRaw, out var sessionId); var token = RefreshToken.Create( diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserIdentifierStoreTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserIdentifierStoreTests.cs index 4fd806af..c980329d 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserIdentifierStoreTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserIdentifierStoreTests.cs @@ -23,7 +23,7 @@ public async Task Add_And_Get_Should_Work() using var connection = CreateOpenConnection(); await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCoreUserIdentifierStore(db, new TenantContext(tenant)); + var store = new EfCoreUserIdentifierStore(db, new TenantExecutionContext(tenant)); var userKey = UserKey.FromGuid(Guid.NewGuid()); var identifier = UserIdentifier.Create( @@ -49,7 +49,7 @@ public async Task Exists_Should_Return_True_When_Exists() using var connection = CreateOpenConnection(); await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCoreUserIdentifierStore(db, new TenantContext(tenant)); + var store = new EfCoreUserIdentifierStore(db, new TenantExecutionContext(tenant)); var userKey = UserKey.FromGuid(Guid.NewGuid()); var identifier = UserIdentifier.Create( @@ -77,7 +77,7 @@ public async Task Save_With_Wrong_Version_Should_Throw() var userKey = UserKey.FromGuid(Guid.NewGuid()); await using var db1 = CreateDb(connection); - var store1 = new EfCoreUserIdentifierStore(db1, new TenantContext(tenant)); + var store1 = new EfCoreUserIdentifierStore(db1, new TenantExecutionContext(tenant)); var identifier = UserIdentifier.Create( Guid.NewGuid(), @@ -92,7 +92,7 @@ public async Task Save_With_Wrong_Version_Should_Throw() await store1.AddAsync(identifier); await using var db2 = CreateDb(connection); - var store2 = new EfCoreUserIdentifierStore(db2, new TenantContext(tenant)); + var store2 = new EfCoreUserIdentifierStore(db2, new TenantExecutionContext(tenant)); var updated = identifier.SetPrimary(DateTimeOffset.UtcNow); @@ -120,13 +120,13 @@ public async Task Save_Should_Increment_Version() await using (var db1 = CreateDb(connection)) { - var store1 = new EfCoreUserIdentifierStore(db1, new TenantContext(tenant)); + var store1 = new EfCoreUserIdentifierStore(db1, new TenantExecutionContext(tenant)); await store1.AddAsync(identifier); } await using (var db2 = CreateDb(connection)) { - var store2 = new EfCoreUserIdentifierStore(db2, new TenantContext(tenant)); + var store2 = new EfCoreUserIdentifierStore(db2, new TenantExecutionContext(tenant)); var existing = await store2.GetAsync(identifier.Id); var updated = existing!.SetPrimary(DateTimeOffset.UtcNow); await store2.SaveAsync(updated, expectedVersion: 0); @@ -134,7 +134,7 @@ public async Task Save_Should_Increment_Version() await using (var db3 = CreateDb(connection)) { - var store3 = new EfCoreUserIdentifierStore(db3, new TenantContext(tenant)); + var store3 = new EfCoreUserIdentifierStore(db3, new TenantExecutionContext(tenant)); var result = await store3.GetAsync(identifier.Id); Assert.Equal(1, result!.Version); } @@ -147,8 +147,8 @@ public async Task Should_Not_See_Data_From_Other_Tenant() await using var db = CreateDb(connection); var tenant1 = TenantKeys.Single; var tenant2 = TenantKey.FromInternal("tenant-2"); - var store1 = new EfCoreUserIdentifierStore(db, new TenantContext(tenant1)); - var store2 = new EfCoreUserIdentifierStore(db, new TenantContext(tenant2)); + var store1 = new EfCoreUserIdentifierStore(db, new TenantExecutionContext(tenant1)); + var store2 = new EfCoreUserIdentifierStore(db, new TenantExecutionContext(tenant2)); var userKey = UserKey.FromGuid(Guid.NewGuid()); @@ -174,7 +174,7 @@ public async Task Soft_Delete_Should_Work() using var connection = CreateOpenConnection(); await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCoreUserIdentifierStore(db, new TenantContext(tenant)); + var store = new EfCoreUserIdentifierStore(db, new TenantExecutionContext(tenant)); var userKey = UserKey.FromGuid(Guid.NewGuid()); var identifier = UserIdentifier.Create( diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserLifecycleStoreTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserLifecycleStoreTests.cs index 0c7c0333..f07ef861 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserLifecycleStoreTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserLifecycleStoreTests.cs @@ -23,7 +23,7 @@ public async Task Add_And_Get_Should_Work() await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCoreUserLifecycleStore(db, new TenantContext(tenant)); + var store = new EfCoreUserLifecycleStore(db, new TenantExecutionContext(tenant)); var userKey = UserKey.FromGuid(Guid.NewGuid()); @@ -48,7 +48,7 @@ public async Task Exists_Should_Return_True_When_Exists() await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCoreUserLifecycleStore(db, new TenantContext(tenant)); + var store = new EfCoreUserLifecycleStore(db, new TenantExecutionContext(tenant)); var userKey = UserKey.FromGuid(Guid.NewGuid()); @@ -78,13 +78,13 @@ public async Task Save_Should_Increment_Version() await using (var db1 = CreateDb(connection)) { - var store1 = new EfCoreUserLifecycleStore(db1, new TenantContext(tenant)); + var store1 = new EfCoreUserLifecycleStore(db1, new TenantExecutionContext(tenant)); await store1.AddAsync(lifecycle); } await using (var db2 = CreateDb(connection)) { - var store2 = new EfCoreUserLifecycleStore(db2, new TenantContext(tenant)); + var store2 = new EfCoreUserLifecycleStore(db2, new TenantExecutionContext(tenant)); var existing = await store2.GetAsync(new UserLifecycleKey(tenant, userKey)); var updated = existing!.ChangeStatus(DateTimeOffset.UtcNow, UserStatus.Suspended); await store2.SaveAsync(updated, expectedVersion: 0); @@ -92,7 +92,7 @@ public async Task Save_Should_Increment_Version() await using (var db3 = CreateDb(connection)) { - var store3 = new EfCoreUserLifecycleStore(db3, new TenantContext(tenant)); + var store3 = new EfCoreUserLifecycleStore(db3, new TenantExecutionContext(tenant)); var result = await store3.GetAsync(new UserLifecycleKey(tenant, userKey)); Assert.Equal(1, result!.Version); @@ -115,13 +115,13 @@ public async Task Save_With_Wrong_Version_Should_Throw() await using (var db1 = CreateDb(connection)) { - var store1 = new EfCoreUserLifecycleStore(db1, new TenantContext(tenant)); + var store1 = new EfCoreUserLifecycleStore(db1, new TenantExecutionContext(tenant)); await store1.AddAsync(lifecycle); } await using (var db2 = CreateDb(connection)) { - var store2 = new EfCoreUserLifecycleStore(db2, new TenantContext(tenant)); + var store2 = new EfCoreUserLifecycleStore(db2, new TenantExecutionContext(tenant)); var existing = await store2.GetAsync(new UserLifecycleKey(tenant, userKey)); var updated = existing!.ChangeStatus(DateTimeOffset.UtcNow, UserStatus.Suspended); @@ -139,8 +139,8 @@ public async Task Should_Not_See_Data_From_Other_Tenant() var tenant1 = TenantKeys.Single; var tenant2 = TenantKey.FromInternal("tenant-2"); - var store1 = new EfCoreUserLifecycleStore(db, new TenantContext(tenant1)); - var store2 = new EfCoreUserLifecycleStore(db, new TenantContext(tenant2)); + var store1 = new EfCoreUserLifecycleStore(db, new TenantExecutionContext(tenant1)); + var store2 = new EfCoreUserLifecycleStore(db, new TenantExecutionContext(tenant2)); var userKey = UserKey.FromGuid(Guid.NewGuid()); @@ -163,7 +163,7 @@ public async Task Soft_Delete_Should_Work() await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCoreUserLifecycleStore(db, new TenantContext(tenant)); + var store = new EfCoreUserLifecycleStore(db, new TenantExecutionContext(tenant)); var userKey = UserKey.FromGuid(Guid.NewGuid()); @@ -201,13 +201,13 @@ public async Task Delete_Should_Increment_SecurityVersion() await using (var db1 = CreateDb(connection)) { - var store1 = new EfCoreUserLifecycleStore(db1, new TenantContext(tenant)); + var store1 = new EfCoreUserLifecycleStore(db1, new TenantExecutionContext(tenant)); await store1.AddAsync(lifecycle); } await using (var db2 = CreateDb(connection)) { - var store2 = new EfCoreUserLifecycleStore(db2, new TenantContext(tenant)); + var store2 = new EfCoreUserLifecycleStore(db2, new TenantExecutionContext(tenant)); var existing = await store2.GetAsync(new UserLifecycleKey(tenant, userKey)); var deleted = existing!.MarkDeleted(DateTimeOffset.UtcNow); @@ -217,7 +217,7 @@ public async Task Delete_Should_Increment_SecurityVersion() await using (var db3 = CreateDb(connection)) { - var store3 = new EfCoreUserLifecycleStore(db3, new TenantContext(tenant)); + var store3 = new EfCoreUserLifecycleStore(db3, new TenantExecutionContext(tenant)); var result = await store3.GetAsync(new UserLifecycleKey(tenant, userKey)); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserProfileStoreTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserProfileStoreTests.cs index 69b79f4f..0a154f9b 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserProfileStoreTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserProfileStoreTests.cs @@ -24,7 +24,7 @@ public async Task Add_And_Get_Should_Work() await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCoreUserProfileStore(db, new TenantContext(tenant)); + var store = new EfCoreUserProfileStore(db, new TenantExecutionContext(tenant)); var userKey = UserKey.FromGuid(Guid.NewGuid()); @@ -53,7 +53,7 @@ public async Task Exists_Should_Return_True_When_Exists() await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCoreUserProfileStore(db, new TenantContext(tenant)); + var store = new EfCoreUserProfileStore(db, new TenantExecutionContext(tenant)); var userKey = UserKey.FromGuid(Guid.NewGuid()); @@ -95,13 +95,13 @@ public async Task Save_Should_Increment_Version() await using (var db1 = CreateDb(connection)) { - var store1 = new EfCoreUserProfileStore(db1, new TenantContext(tenant)); + var store1 = new EfCoreUserProfileStore(db1, new TenantExecutionContext(tenant)); await store1.AddAsync(profile); } await using (var db2 = CreateDb(connection)) { - var store2 = new EfCoreUserProfileStore(db2, new TenantContext(tenant)); + var store2 = new EfCoreUserProfileStore(db2, new TenantExecutionContext(tenant)); var existing = await store2.GetAsync(new UserProfileKey(tenant, userKey, ProfileKey.Default)); var updated = existing!.UpdateName(existing.FirstName, existing.LastName, "new", DateTimeOffset.UtcNow); await store2.SaveAsync(updated, expectedVersion: 0); @@ -109,7 +109,7 @@ public async Task Save_Should_Increment_Version() await using (var db3 = CreateDb(connection)) { - var store3 = new EfCoreUserProfileStore(db3, new TenantContext(tenant)); + var store3 = new EfCoreUserProfileStore(db3, new TenantExecutionContext(tenant)); var result = await store3.GetAsync(new UserProfileKey(tenant, userKey, ProfileKey.Default)); Assert.Equal(1, result!.Version); @@ -138,13 +138,13 @@ public async Task Save_With_Wrong_Version_Should_Throw() await using (var db1 = CreateDb(connection)) { - var store1 = new EfCoreUserProfileStore(db1, new TenantContext(tenant)); + var store1 = new EfCoreUserProfileStore(db1, new TenantExecutionContext(tenant)); await store1.AddAsync(profile); } await using (var db2 = CreateDb(connection)) { - var store2 = new EfCoreUserProfileStore(db2, new TenantContext(tenant)); + var store2 = new EfCoreUserProfileStore(db2, new TenantExecutionContext(tenant)); var existing = await store2.GetAsync(new UserProfileKey(tenant, userKey, ProfileKey.Default)); var updated = existing!.UpdateName(existing.FirstName, existing.LastName, "new", DateTimeOffset.UtcNow); @@ -162,8 +162,8 @@ public async Task Should_Not_See_Data_From_Other_Tenant() var tenant1 = TenantKeys.Single; var tenant2 = TenantKey.FromInternal("tenant-2"); - var store1 = new EfCoreUserProfileStore(db, new TenantContext(tenant1)); - var store2 = new EfCoreUserProfileStore(db, new TenantContext(tenant2)); + var store1 = new EfCoreUserProfileStore(db, new TenantExecutionContext(tenant1)); + var store2 = new EfCoreUserProfileStore(db, new TenantExecutionContext(tenant2)); var userKey = UserKey.FromGuid(Guid.NewGuid()); @@ -191,7 +191,7 @@ public async Task Soft_Delete_Should_Work() await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCoreUserProfileStore(db, new TenantContext(tenant)); + var store = new EfCoreUserProfileStore(db, new TenantExecutionContext(tenant)); var userKey = UserKey.FromGuid(Guid.NewGuid()); @@ -227,7 +227,7 @@ public async Task Same_User_Can_Have_Multiple_Profiles() await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCoreUserProfileStore(db, new TenantContext(tenant)); + var store = new EfCoreUserProfileStore(db, new TenantExecutionContext(tenant)); var userKey = UserKey.FromGuid(Guid.NewGuid()); @@ -265,7 +265,7 @@ public async Task GetAsync_Should_Return_Correct_Profile_By_ProfileKey() await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCoreUserProfileStore(db, new TenantContext(tenant)); + var store = new EfCoreUserProfileStore(db, new TenantExecutionContext(tenant)); var userKey = UserKey.FromGuid(Guid.NewGuid()); @@ -298,7 +298,7 @@ public async Task GetByUsersAsync_Should_Filter_By_ProfileKey() await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCoreUserProfileStore(db, new TenantContext(tenant)); + var store = new EfCoreUserProfileStore(db, new TenantExecutionContext(tenant)); var userKey = UserKey.FromGuid(Guid.NewGuid()); @@ -333,7 +333,7 @@ public async Task Should_Not_Allow_Duplicate_ProfileKey_For_Same_User() await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCoreUserProfileStore(db, new TenantContext(tenant)); + var store = new EfCoreUserProfileStore(db, new TenantExecutionContext(tenant)); var userKey = UserKey.FromGuid(Guid.NewGuid()); @@ -364,7 +364,7 @@ public async Task Delete_Should_Not_Affect_Other_Profiles() await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCoreUserProfileStore(db, new TenantContext(tenant)); + var store = new EfCoreUserProfileStore(db, new TenantExecutionContext(tenant)); var userKey = UserKey.FromGuid(Guid.NewGuid()); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserRoleStoreTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserRoleStoreTests.cs index e1062e34..5d9b1264 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserRoleStoreTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserRoleStoreTests.cs @@ -22,7 +22,7 @@ public async Task Assign_And_GetAssignments_Should_Work() await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCoreUserRoleStore(db, new TenantContext(tenant)); + var store = new EfCoreUserRoleStore(db, new TenantExecutionContext(tenant)); var userKey = UserKey.FromGuid(Guid.NewGuid()); var roleId = RoleId.New(); @@ -41,7 +41,7 @@ public async Task Assign_Duplicate_Should_Throw() await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCoreUserRoleStore(db, new TenantContext(tenant)); + var store = new EfCoreUserRoleStore(db, new TenantExecutionContext(tenant)); var userKey = UserKey.FromGuid(Guid.NewGuid()); var roleId = RoleId.New(); @@ -58,7 +58,7 @@ public async Task Remove_Should_Work() await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCoreUserRoleStore(db, new TenantContext(tenant)); + var store = new EfCoreUserRoleStore(db, new TenantExecutionContext(tenant)); var userKey = UserKey.FromGuid(Guid.NewGuid()); var roleId = RoleId.New(); @@ -77,7 +77,7 @@ public async Task Remove_NonExisting_Should_Not_Throw() await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCoreUserRoleStore(db, new TenantContext(tenant)); + var store = new EfCoreUserRoleStore(db, new TenantExecutionContext(tenant)); var userKey = UserKey.FromGuid(Guid.NewGuid()); var roleId = RoleId.New(); @@ -92,7 +92,7 @@ public async Task CountAssignments_Should_Return_Correct_Count() await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCoreUserRoleStore(db, new TenantContext(tenant)); + var store = new EfCoreUserRoleStore(db, new TenantExecutionContext(tenant)); var roleId = RoleId.New(); @@ -111,7 +111,7 @@ public async Task RemoveAssignmentsByRole_Should_Remove_All() await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCoreUserRoleStore(db, new TenantContext(tenant)); + var store = new EfCoreUserRoleStore(db, new TenantExecutionContext(tenant)); var roleId = RoleId.New(); @@ -137,8 +137,8 @@ public async Task Should_Not_See_Data_From_Other_Tenant() var tenant1 = TenantKeys.Single; var tenant2 = TenantKey.FromInternal("tenant-2"); - var store1 = new EfCoreUserRoleStore(db, new TenantContext(tenant1)); - var store2 = new EfCoreUserRoleStore(db, new TenantContext(tenant2)); + var store1 = new EfCoreUserRoleStore(db, new TenantExecutionContext(tenant1)); + var store2 = new EfCoreUserRoleStore(db, new TenantExecutionContext(tenant2)); var userKey = UserKey.FromGuid(Guid.NewGuid()); var roleId = RoleId.New(); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/IdentifierConcurrencyTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/IdentifierConcurrencyTests.cs index ef600fd8..7644b744 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/IdentifierConcurrencyTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/IdentifierConcurrencyTests.cs @@ -13,7 +13,7 @@ public class IdentifierConcurrencyTests [Fact] public async Task Save_should_increment_version() { - var store = new InMemoryUserIdentifierStore(new TenantContext(TenantKeys.Single)); + var store = new InMemoryUserIdentifierStore(new TenantExecutionContext(TenantKeys.Single)); var now = DateTimeOffset.UtcNow; var id = Guid.NewGuid(); @@ -34,7 +34,7 @@ public async Task Save_should_increment_version() [Fact] public async Task Delete_should_throw_when_version_conflicts() { - var store = new InMemoryUserIdentifierStore(new TenantContext(TenantKeys.Single)); + var store = new InMemoryUserIdentifierStore(new TenantExecutionContext(TenantKeys.Single)); var now = DateTimeOffset.UtcNow; var id = Guid.NewGuid(); @@ -57,7 +57,7 @@ await Assert.ThrowsAsync(async () => [Fact] public async Task Parallel_SetPrimary_should_conflict_deterministic() { - var store = new InMemoryUserIdentifierStore(new TenantContext(TenantKeys.Single)); + var store = new InMemoryUserIdentifierStore(new TenantExecutionContext(TenantKeys.Single)); var now = DateTimeOffset.UtcNow; var id = Guid.NewGuid(); @@ -103,7 +103,7 @@ public async Task Parallel_SetPrimary_should_conflict_deterministic() [Fact] public async Task Update_should_throw_concurrency_when_versions_conflict() { - var store = new InMemoryUserIdentifierStore(new TenantContext(TenantKeys.Single)); + var store = new InMemoryUserIdentifierStore(new TenantExecutionContext(TenantKeys.Single)); var id = Guid.NewGuid(); var now = DateTimeOffset.UtcNow; var tenant = TenantKey.Single; @@ -130,7 +130,7 @@ await Assert.ThrowsAsync(async () => [Fact] public async Task Parallel_updates_should_result_in_single_success_deterministic() { - var store = new InMemoryUserIdentifierStore(new TenantContext(TenantKeys.Single)); + var store = new InMemoryUserIdentifierStore(new TenantExecutionContext(TenantKeys.Single)); var now = DateTimeOffset.UtcNow; var tenant = TenantKey.Single; var id = Guid.NewGuid(); @@ -181,7 +181,7 @@ public async Task Parallel_updates_should_result_in_single_success_deterministic [Fact] public async Task High_contention_updates_should_allow_only_one_success() { - var store = new InMemoryUserIdentifierStore(new TenantContext(TenantKeys.Single)); + var store = new InMemoryUserIdentifierStore(new TenantExecutionContext(TenantKeys.Single)); var now = DateTimeOffset.UtcNow; var tenant = TenantKey.Single; var id = Guid.NewGuid(); @@ -227,7 +227,7 @@ public async Task High_contention_updates_should_allow_only_one_success() [Fact] public async Task High_contention_SetPrimary_should_allow_only_one_deterministic() { - var store = new InMemoryUserIdentifierStore(new TenantContext(TenantKeys.Single)); + var store = new InMemoryUserIdentifierStore(new TenantExecutionContext(TenantKeys.Single)); var now = DateTimeOffset.UtcNow; var tenant = TenantKey.Single; @@ -276,7 +276,7 @@ public async Task High_contention_SetPrimary_should_allow_only_one_deterministic [Fact] public async Task Two_identifiers_racing_for_primary_should_allow() { - var store = new InMemoryUserIdentifierStore(new TenantContext(TenantKeys.Single)); + var store = new InMemoryUserIdentifierStore(new TenantExecutionContext(TenantKeys.Single)); var now = DateTimeOffset.UtcNow; var tenant = TenantKey.Single; var user = TestUsers.Admin; From 44c92746be362ea6ad0f1dd0cc552191083872b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Can=20Karag=C3=B6z?= Date: Mon, 6 Apr 2026 22:21:19 +0300 Subject: [PATCH 7/8] PasswordHash Enhancement --- .../uauth.db-shm | Bin 32768 -> 32768 bytes .../uauth.db-wal | Bin 881712 -> 943512 bytes .../Infrastructure/IUAuthPasswordHasher.cs | 5 +- .../Domain/Security/PasswordHash.cs | 58 +++++++++ .../Security/PasswordHashJsonConverter.cs | 25 ++++ .../Default/PasswordAlgorithms.cs | 8 ++ .../Data/UAuthCredentialsModelBuilder.cs | 6 +- .../PasswordCredentialProjection.cs | 5 +- .../Domain/PasswordCredential.cs | 16 +-- .../Abstractions/ISecretCredential.cs | 6 +- .../Argon2PasswordHasher.cs | 64 +++++++--- .../EfCoreCredentialStoreTests.cs | 115 +++++++++++------- .../Helpers/TestPasswordHasher.cs | 30 ++++- .../Security/Argon2PasswordHasherTest.cs | 105 ++++++++++++---- 14 files changed, 346 insertions(+), 97 deletions(-) create mode 100644 src/CodeBeam.UltimateAuth.Core/Domain/Security/PasswordHash.cs create mode 100644 src/CodeBeam.UltimateAuth.Core/Domain/Security/PasswordHashJsonConverter.cs create mode 100644 src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/Default/PasswordAlgorithms.cs 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 index 0c7cbb9c2724fa7c43ad5af45e889a2cd2506d70..e6eec9c3b413da71e866355becc25403f23ff34c 100644 GIT binary patch delta 319 zcmZo@U}|V!s+V}A%K!ojK+MR%An+7O`vCDax34QVr8q9O-jOk}K$3k~-;OnWt9aLi zlBynP6c~Wa{f`8o!W-*jnN`_=99bZ?17Zgtb^>BnDCPpv(m*Ub`5?3O<|oX3Hk(&D z++bvUGWj8M_2z$0w-_fsVqU`T&Ty6C7Q<7ZVCCjNu1uWFw;AqiX7oSH$aoJZwsdnz zU<)(jH6U~G<~gByjLg>=?t?i6;SFqzcQ=2GohiZiVDm#}RmRO5%5N|--(YwM=8s}O)XI7Cc|T(&Po7Rj&42x delta 261 zcmZo@U}|V!s+V}A%K!p0K+MR%AaD&x`vCEx33m^C($@Q{_xJD$m!PxPq#};4efceM z8ma1mMu7py-2X@bD!j2imYGp}vm#5U&13@>m(8mjZZJ+(VyW5u&*>H;W+fIi#?2eb QZ!khw>#AU^HMJ0H0H|YNLjV8( 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 index 03c676f0252d1fef32150d041afec61655766db0..bd8778a5429f61c15daa1fe0077633b2a8425a82 100644 GIT binary patch delta 15113 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 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/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/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.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/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/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreCredentialStoreTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreCredentialStoreTests.cs index 2bafdc7d..094b9036 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreCredentialStoreTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreCredentialStoreTests.cs @@ -24,15 +24,20 @@ public async Task Add_And_Get_Should_Work() await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCorePasswordCredentialStore(db, new TenantExecutionContext(tenant)); + var store = new EfCorePasswordCredentialStore( + db, + new TenantExecutionContext(tenant)); var userKey = UserKey.FromGuid(Guid.NewGuid()); + var hasher = new TestPasswordHasher(); + var passwordHash = hasher.Hash("123456"); + var credential = PasswordCredential.Create( Guid.NewGuid(), tenant, userKey, - "hash", + passwordHash, CredentialSecurityState.Active(), new CredentialMetadata(), DateTimeOffset.UtcNow); @@ -42,7 +47,8 @@ public async Task Add_And_Get_Should_Work() var result = await store.GetAsync(new CredentialKey(tenant, credential.Id)); Assert.NotNull(result); - Assert.Equal("hash", result!.SecretHash); + + Assert.True(hasher.Verify(result.SecretHash, "123456")); } [Fact] @@ -52,63 +58,73 @@ public async Task Exists_Should_Return_True_When_Exists() await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCorePasswordCredentialStore(db, new TenantExecutionContext(tenant)); + var store = new EfCorePasswordCredentialStore( + db, new TenantExecutionContext(tenant)); var userKey = UserKey.FromGuid(Guid.NewGuid()); + var hasher = new TestPasswordHasher(); + var hash = hasher.Hash("123"); + var credential = PasswordCredential.Create( Guid.NewGuid(), tenant, userKey, - "hash", + hash, CredentialSecurityState.Active(), new CredentialMetadata(), DateTimeOffset.UtcNow); await store.AddAsync(credential); + var exists = await store.ExistsAsync(new CredentialKey(tenant, credential.Id)); Assert.True(exists); } [Fact] - public async Task Save_Should_Increment_Version() + public async Task Save_Should_Increment_Version_And_Update_Hash() { using var connection = CreateOpenConnection(); var tenant = TenantKeys.Single; var userKey = UserKey.FromGuid(Guid.NewGuid()); + var hasher = new TestPasswordHasher(); var credential = PasswordCredential.Create( Guid.NewGuid(), tenant, userKey, - "hash", + hasher.Hash("old"), CredentialSecurityState.Active(), new CredentialMetadata(), DateTimeOffset.UtcNow); await using (var db1 = CreateDb(connection)) { - var store1 = new EfCorePasswordCredentialStore(db1, new TenantExecutionContext(tenant)); - await store1.AddAsync(credential); + var store = new EfCorePasswordCredentialStore(db1, new TenantExecutionContext(tenant)); + await store.AddAsync(credential); } await using (var db2 = CreateDb(connection)) { - var store2 = new EfCorePasswordCredentialStore(db2, new TenantExecutionContext(tenant)); - var existing = await store2.GetAsync(new CredentialKey(tenant, credential.Id)); - var updated = existing!.ChangeSecret("new_hash", DateTimeOffset.UtcNow); - await store2.SaveAsync(updated, expectedVersion: 0); + var store = new EfCorePasswordCredentialStore(db2, new TenantExecutionContext(tenant)); + + var existing = await store.GetAsync(new CredentialKey(tenant, credential.Id)); + + var updated = existing!.ChangeSecret(hasher.Hash("new"), DateTimeOffset.UtcNow); + + await store.SaveAsync(updated, expectedVersion: 0); } await using (var db3 = CreateDb(connection)) { - var store3 = new EfCorePasswordCredentialStore(db3, new TenantExecutionContext(tenant)); - var result = await store3.GetAsync(new CredentialKey(tenant, credential.Id)); + var store = new EfCorePasswordCredentialStore(db3, new TenantExecutionContext(tenant)); + + var result = await store.GetAsync(new CredentialKey(tenant, credential.Id)); Assert.Equal(1, result!.Version); - Assert.Equal("new_hash", result.SecretHash); + Assert.True(hasher.Verify(result.SecretHash, "new")); } } @@ -119,31 +135,32 @@ public async Task Save_With_Wrong_Version_Should_Throw() var tenant = TenantKeys.Single; var userKey = UserKey.FromGuid(Guid.NewGuid()); + var hasher = new TestPasswordHasher(); var credential = PasswordCredential.Create( Guid.NewGuid(), tenant, userKey, - "hash", + hasher.Hash("123"), CredentialSecurityState.Active(), new CredentialMetadata(), DateTimeOffset.UtcNow); await using (var db1 = CreateDb(connection)) { - var store1 = new EfCorePasswordCredentialStore(db1, new TenantExecutionContext(tenant)); - await store1.AddAsync(credential); + var store = new EfCorePasswordCredentialStore(db1, new TenantExecutionContext(tenant)); + await store.AddAsync(credential); } await using (var db2 = CreateDb(connection)) { - var store2 = new EfCorePasswordCredentialStore(db2, new TenantExecutionContext(tenant)); + var store = new EfCorePasswordCredentialStore(db2, new TenantExecutionContext(tenant)); - var existing = await store2.GetAsync(new CredentialKey(tenant, credential.Id)); - var updated = existing!.ChangeSecret("new_hash", DateTimeOffset.UtcNow); + var existing = await store.GetAsync(new CredentialKey(tenant, credential.Id)); + var updated = existing!.ChangeSecret(hasher.Hash("new"), DateTimeOffset.UtcNow); await Assert.ThrowsAsync(() => - store2.SaveAsync(updated, expectedVersion: 999)); + store.SaveAsync(updated, expectedVersion: 999)); } } @@ -156,6 +173,8 @@ public async Task Should_Not_See_Data_From_Other_Tenant() var tenant1 = TenantKeys.Single; var tenant2 = TenantKey.FromInternal("tenant-2"); + var hasher = new TestPasswordHasher(); + var store1 = new EfCorePasswordCredentialStore(db, new TenantExecutionContext(tenant1)); var store2 = new EfCorePasswordCredentialStore(db, new TenantExecutionContext(tenant2)); @@ -165,12 +184,13 @@ public async Task Should_Not_See_Data_From_Other_Tenant() Guid.NewGuid(), tenant1, userKey, - "hash", + hasher.Hash("123"), CredentialSecurityState.Active(), new CredentialMetadata(), DateTimeOffset.UtcNow); await store1.AddAsync(credential); + var result = await store2.GetAsync(new CredentialKey(tenant2, credential.Id)); Assert.Null(result); @@ -183,6 +203,8 @@ public async Task Soft_Delete_Should_Work() await using var db = CreateDb(connection); var tenant = TenantKeys.Single; + var hasher = new TestPasswordHasher(); + var store = new EfCorePasswordCredentialStore(db, new TenantExecutionContext(tenant)); var userKey = UserKey.FromGuid(Guid.NewGuid()); @@ -191,7 +213,7 @@ public async Task Soft_Delete_Should_Work() Guid.NewGuid(), tenant, userKey, - "hash", + hasher.Hash("123"), CredentialSecurityState.Active(), new CredentialMetadata(), DateTimeOffset.UtcNow); @@ -217,34 +239,38 @@ public async Task Revoke_Should_Persist() var tenant = TenantKeys.Single; var userKey = UserKey.FromGuid(Guid.NewGuid()); + var hasher = new TestPasswordHasher(); var credential = PasswordCredential.Create( Guid.NewGuid(), tenant, userKey, - "hash", + hasher.Hash("123"), CredentialSecurityState.Active(), new CredentialMetadata(), DateTimeOffset.UtcNow); await using (var db1 = CreateDb(connection)) { - var store1 = new EfCorePasswordCredentialStore(db1, new TenantExecutionContext(tenant)); - await store1.AddAsync(credential); + var store = new EfCorePasswordCredentialStore(db1, new TenantExecutionContext(tenant)); + await store.AddAsync(credential); } await using (var db2 = CreateDb(connection)) { - var store2 = new EfCorePasswordCredentialStore(db2, new TenantExecutionContext(tenant)); - var existing = await store2.GetAsync(new CredentialKey(tenant, credential.Id)); + var store = new EfCorePasswordCredentialStore(db2, new TenantExecutionContext(tenant)); + + var existing = await store.GetAsync(new CredentialKey(tenant, credential.Id)); var revoked = existing!.Revoke(DateTimeOffset.UtcNow); - await store2.SaveAsync(revoked, expectedVersion: 0); + + await store.SaveAsync(revoked, expectedVersion: 0); } await using (var db3 = CreateDb(connection)) { - var store3 = new EfCorePasswordCredentialStore(db3, new TenantExecutionContext(tenant)); - var result = await store3.GetAsync(new CredentialKey(tenant, credential.Id)); + var store = new EfCorePasswordCredentialStore(db3, new TenantExecutionContext(tenant)); + + var result = await store.GetAsync(new CredentialKey(tenant, credential.Id)); Assert.True(result!.IsRevoked); } @@ -257,37 +283,40 @@ public async Task ChangeSecret_Should_Update_SecurityState() var tenant = TenantKeys.Single; var userKey = UserKey.FromGuid(Guid.NewGuid()); + var hasher = new TestPasswordHasher(); var credential = PasswordCredential.Create( Guid.NewGuid(), tenant, userKey, - "hash", + hasher.Hash("old"), CredentialSecurityState.Active(), new CredentialMetadata(), DateTimeOffset.UtcNow); await using (var db1 = CreateDb(connection)) { - var store1 = new EfCorePasswordCredentialStore(db1, new TenantExecutionContext(tenant)); - await store1.AddAsync(credential); + var store = new EfCorePasswordCredentialStore(db1, new TenantExecutionContext(tenant)); + await store.AddAsync(credential); } await using (var db2 = CreateDb(connection)) { - var store2 = new EfCorePasswordCredentialStore(db2, new TenantExecutionContext(tenant)); - var existing = await store2.GetAsync(new CredentialKey(tenant, credential.Id)); - var updated = existing!.ChangeSecret("new_hash", DateTimeOffset.UtcNow); + var store = new EfCorePasswordCredentialStore(db2, new TenantExecutionContext(tenant)); - await store2.SaveAsync(updated, expectedVersion: 0); + var existing = await store.GetAsync(new CredentialKey(tenant, credential.Id)); + var updated = existing!.ChangeSecret(hasher.Hash("new"), DateTimeOffset.UtcNow); + + await store.SaveAsync(updated, expectedVersion: 0); } await using (var db3 = CreateDb(connection)) { - var store3 = new EfCorePasswordCredentialStore(db3, new TenantExecutionContext(tenant)); - var result = await store3.GetAsync(new CredentialKey(tenant, credential.Id)); + var store = new EfCorePasswordCredentialStore(db3, new TenantExecutionContext(tenant)); + + var result = await store.GetAsync(new CredentialKey(tenant, credential.Id)); - Assert.Equal("new_hash", result!.SecretHash); + Assert.True(hasher.Verify(result!.SecretHash, "new")); Assert.NotNull(result.UpdatedAt); } } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Helpers/TestPasswordHasher.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Helpers/TestPasswordHasher.cs index c6b363ec..a4daef84 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Helpers/TestPasswordHasher.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Helpers/TestPasswordHasher.cs @@ -1,9 +1,33 @@ -using CodeBeam.UltimateAuth.Core.Abstractions; +using CodeBeam.UltimateAuth.Core; +using CodeBeam.UltimateAuth.Core.Abstractions; +using CodeBeam.UltimateAuth.Core.Errors; +using CodeBeam.UltimateAuth.Credentials.Contracts; namespace CodeBeam.UltimateAuth.Tests.Unit.Helpers; internal sealed class TestPasswordHasher : IUAuthPasswordHasher { - public string Hash(string password) => $"HASH::{password}"; - public bool Verify(string hashedPassword, string providedPassword) => hashedPassword == $"HASH::{providedPassword}"; + public PasswordHash Hash(string password) + { + if (string.IsNullOrWhiteSpace(password)) + throw new UAuthValidationException("password_required"); + + return PasswordHash.Create(PasswordAlgorithms.Legacy, $"TEST::{password}"); + } + + public bool Verify(PasswordHash hash, string secret) + { + if (string.IsNullOrWhiteSpace(secret) || string.IsNullOrWhiteSpace(hash.Hash)) + return false; + + if (hash.Algorithm != PasswordAlgorithms.Legacy) + return false; + + return hash.Hash == $"TEST::{secret}"; + } + + public bool NeedsRehash(PasswordHash hash) + { + return false; + } } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Security/Argon2PasswordHasherTest.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Security/Argon2PasswordHasherTest.cs index 506c90f1..95474a45 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Security/Argon2PasswordHasherTest.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Security/Argon2PasswordHasherTest.cs @@ -1,5 +1,8 @@ -using CodeBeam.UltimateAuth.Core.Errors; +using CodeBeam.UltimateAuth.Core; +using CodeBeam.UltimateAuth.Core.Errors; +using CodeBeam.UltimateAuth.Credentials.Contracts; using CodeBeam.UltimateAuth.Security.Argon2; +using FluentAssertions; using Microsoft.Extensions.Options; namespace CodeBeam.UltimateAuth.Tests.Unit; @@ -13,74 +16,132 @@ private Argon2PasswordHasher CreateHasher() } [Fact] - public void Hash_ShouldReturn_NonEmptyString() + public void Hash_Should_Return_Valid_PasswordHash() { var hasher = CreateHasher(); + var result = hasher.Hash("password123"); - Assert.False(string.IsNullOrWhiteSpace(result)); - Assert.Contains(".", result); + result.Should().NotBeNull(); + result.Algorithm.Should().Be(PasswordAlgorithms.Argon2); + result.Hash.Should().NotBeNullOrWhiteSpace(); + + var parts = result.Hash.Split('.'); + parts.Length.Should().Be(5); } [Fact] - public void Verify_ShouldReturn_True_ForValidPassword() + public void Verify_Should_Return_True_For_Correct_Password() { var hasher = CreateHasher(); + var hash = hasher.Hash("password123"); + var result = hasher.Verify(hash, "password123"); - Assert.True(result); + result.Should().BeTrue(); } [Fact] - public void Verify_ShouldReturn_False_ForInvalidPassword() + public void Verify_Should_Return_False_For_Wrong_Password() { var hasher = CreateHasher(); + var hash = hasher.Hash("password123"); - var result = hasher.Verify(hash, "wrong-password"); - Assert.False(result); + var result = hasher.Verify(hash, "wrong"); + + result.Should().BeFalse(); } [Fact] - public void Verify_ShouldReturn_False_ForInvalidFormat() + public void Verify_Should_Return_False_For_Invalid_Format() { var hasher = CreateHasher(); - var result = hasher.Verify("invalid-format", "password"); - Assert.False(result); + var invalid = PasswordHash.Create(PasswordAlgorithms.Argon2, "invalid"); + + var result = hasher.Verify(invalid, "password"); + + result.Should().BeFalse(); } [Fact] - public void Hash_ShouldThrow_WhenPasswordIsEmpty() + public void Hash_Should_Throw_When_Password_Is_Empty() { var hasher = CreateHasher(); + Assert.Throws(() => hasher.Hash("")); } [Fact] - public void Hash_ShouldProduce_DifferentHashes_ForSamePassword() + public void Hash_Should_Produce_Different_Hashes_For_Same_Password() { var hasher = CreateHasher(); + var hash1 = hasher.Hash("password123"); var hash2 = hasher.Hash("password123"); - Assert.NotEqual(hash1, hash2); + hash1.Hash.Should().NotBe(hash2.Hash); } [Fact] - public void Verify_ShouldUse_SameSalt_FromHash() + public void Verify_Should_Use_Embedded_Salt_And_Parameters() { var hasher = CreateHasher(); + var hash = hasher.Hash("password123"); - var parts = hash.Split('.'); - Assert.Equal(2, parts.Length); + // parametreleri değiştir (simulate config drift) + var differentOptions = Options.Create(new Argon2Options + { + Iterations = 999, + MemorySizeKb = 999, + Parallelism = 1, + SaltSize = 16, + HashSize = 32 + }); - var salt1 = parts[0]; - var hash2 = hasher.Hash("password123"); - var salt2 = hash2.Split('.')[0]; + var differentHasher = new Argon2PasswordHasher(differentOptions); + + // 🔥 yine de doğrulamalı + var result = differentHasher.Verify(hash, "password123"); + + result.Should().BeTrue(); + } + + [Fact] + public void NeedsRehash_Should_Return_True_When_Parameters_Changed() + { + var hasher = CreateHasher(); + + var hash = hasher.Hash("password123"); + + var differentOptions = Options.Create(new Argon2Options + { + Iterations = 999, + MemorySizeKb = 999, + Parallelism = 1, + SaltSize = 16, + HashSize = 32 + }); + + var differentHasher = new Argon2PasswordHasher(differentOptions); + + var result = differentHasher.NeedsRehash(hash); + + result.Should().BeTrue(); + } + + [Fact] + public void NeedsRehash_Should_Return_False_When_Parameters_Match() + { + var hasher = CreateHasher(); + + var hash = hasher.Hash("password123"); + + var result = hasher.NeedsRehash(hash); - Assert.NotEqual(salt1, salt2); // random salt doğrulama + result.Should().BeFalse(); } } From 6567f11d5f178a6374e10b30e61b9df43e94f12e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Can=20Karag=C3=B6z?= Date: Mon, 6 Apr 2026 23:33:23 +0300 Subject: [PATCH 8/8] Added Some XML Comments --- ...0260327184128_InitUltimateAuth.Designer.cs | 710 ------------------ .../Migrations/20260405222857_MultiProfile.cs | 49 -- ...260406192328_InitUltimateAuth.Designer.cs} | 4 +- ....cs => 20260406192328_InitUltimateAuth.cs} | 6 +- .../uauth.db | Bin 4096 -> 311296 bytes .../uauth.db-shm | Bin 32768 -> 0 bytes .../uauth.db-wal | Bin 943512 -> 0 bytes .../Options/UAuthClientOptions.cs | 95 ++- .../Abstractions/IAuthorizationClient.cs | 92 +++ .../Abstractions/ICredentialClient.cs | 94 +++ .../Services/Abstractions/IFlowClient.cs | 115 ++- .../Services/Abstractions/ISessionClient.cs | 101 +++ .../Services/Abstractions/IUAuthClient.cs | 54 ++ .../Services/Abstractions/IUserClient.cs | 116 +++ .../Abstractions/IUserIdentifierClient.cs | 101 +++ 15 files changed, 770 insertions(+), 767 deletions(-) delete mode 100644 samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20260327184128_InitUltimateAuth.Designer.cs delete mode 100644 samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20260405222857_MultiProfile.cs rename samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/{20260405222857_MultiProfile.Designer.cs => 20260406192328_InitUltimateAuth.Designer.cs} (99%) rename samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/{20260327184128_InitUltimateAuth.cs => 20260406192328_InitUltimateAuth.cs} (99%) delete mode 100644 samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db-shm delete mode 100644 samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db-wal 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/20260327184128_InitUltimateAuth.Designer.cs deleted file mode 100644 index 079580bb..00000000 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20260327184128_InitUltimateAuth.Designer.cs +++ /dev/null @@ -1,710 +0,0 @@ -// -using System; -using CodeBeam.UltimateAuth.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.Migrations -{ - [DbContext(typeof(UAuthDbContext))] - [Migration("20260327184128_InitUltimateAuth")] - partial class InitUltimateAuth - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "10.0.5"); - - modelBuilder.Entity("CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore.AuthenticationSecurityStateProjection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); - - b.Property("CredentialType") - .HasColumnType("INTEGER"); - - b.Property("FailedAttempts") - .HasColumnType("INTEGER"); - - b.Property("LastFailedAt") - .HasColumnType("TEXT"); - - b.Property("LockedUntil") - .HasColumnType("TEXT"); - - b.Property("RequiresReauthentication") - .HasColumnType("INTEGER"); - - b.Property("ResetAttempts") - .HasColumnType("INTEGER"); - - b.Property("ResetConsumedAt") - .HasColumnType("TEXT"); - - b.Property("ResetExpiresAt") - .HasColumnType("TEXT"); - - b.Property("ResetRequestedAt") - .HasColumnType("TEXT"); - - b.Property("ResetTokenHash") - .HasMaxLength(512) - .HasColumnType("TEXT"); - - b.Property("Scope") - .HasColumnType("INTEGER"); - - b.Property("SecurityVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("Tenant") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("TEXT"); - - b.Property("UserKey") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Tenant", "LockedUntil"); - - b.HasIndex("Tenant", "ResetRequestedAt"); - - b.HasIndex("Tenant", "UserKey"); - - b.HasIndex("Tenant", "UserKey", "Scope"); - - b.HasIndex("Tenant", "UserKey", "Scope", "CredentialType") - .IsUnique(); - - b.ToTable("UAuth_Authentication", (string)null); - }); - - modelBuilder.Entity("CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.RolePermissionProjection", b => - { - b.Property("Tenant") - .HasMaxLength(128) - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("TEXT"); - - b.Property("Permission") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Tenant", "RoleId", "Permission"); - - b.HasIndex("Tenant", "Permission"); - - b.HasIndex("Tenant", "RoleId"); - - b.ToTable("UAuth_RolePermissions", (string)null); - }); - - modelBuilder.Entity("CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.RoleProjection", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("CreatedAt") - .HasColumnType("TEXT"); - - b.Property("DeletedAt") - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("TEXT"); - - b.Property("Tenant") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("TEXT"); - - b.Property("UpdatedAt") - .HasColumnType("TEXT"); - - b.Property("Version") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("Tenant", "Id") - .IsUnique(); - - b.HasIndex("Tenant", "NormalizedName") - .IsUnique(); - - b.ToTable("UAuth_Roles", (string)null); - }); - - modelBuilder.Entity("CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.UserRoleProjection", b => - { - b.Property("Tenant") - .HasMaxLength(128) - .HasColumnType("TEXT"); - - b.Property("UserKey") - .HasMaxLength(128) - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("TEXT"); - - b.Property("AssignedAt") - .HasColumnType("TEXT"); - - b.HasKey("Tenant", "UserKey", "RoleId"); - - b.HasIndex("Tenant", "RoleId"); - - b.HasIndex("Tenant", "UserKey"); - - b.ToTable("UAuth_UserRoles", (string)null); - }); - - modelBuilder.Entity("CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore.PasswordCredentialProjection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); - - b.Property("CreatedAt") - .HasColumnType("TEXT"); - - b.Property("DeletedAt") - .HasColumnType("TEXT"); - - b.Property("ExpiresAt") - .HasColumnType("TEXT"); - - b.Property("LastUsedAt") - .HasColumnType("TEXT"); - - b.Property("RevokedAt") - .HasColumnType("TEXT"); - - b.Property("SecretHash") - .IsRequired() - .HasMaxLength(512) - .HasColumnType("TEXT"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("Source") - .HasMaxLength(128) - .HasColumnType("TEXT"); - - b.Property("Tenant") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("TEXT"); - - b.Property("UpdatedAt") - .HasColumnType("TEXT"); - - b.Property("UserKey") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("TEXT"); - - b.Property("Version") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("Tenant", "ExpiresAt"); - - b.HasIndex("Tenant", "Id") - .IsUnique(); - - b.HasIndex("Tenant", "RevokedAt"); - - b.HasIndex("Tenant", "UserKey"); - - b.HasIndex("Tenant", "UserKey", "DeletedAt"); - - b.ToTable("UAuth_PasswordCredentials", (string)null); - }); - - modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionChainProjection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AbsoluteExpiresAt") - .HasColumnType("TEXT"); - - b.Property("ActiveSessionId") - .HasColumnType("TEXT"); - - b.Property("ChainId") - .HasColumnType("TEXT"); - - b.Property("ClaimsSnapshot") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("CreatedAt") - .HasColumnType("TEXT"); - - b.Property("Device") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("DeviceId") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("TEXT"); - - b.Property("LastSeenAt") - .HasColumnType("TEXT"); - - b.Property("RevokedAt") - .HasColumnType("TEXT"); - - b.Property("RootId") - .HasColumnType("TEXT"); - - b.Property("RotationCount") - .HasColumnType("INTEGER"); - - b.Property("SecurityVersionAtCreation") - .HasColumnType("INTEGER"); - - b.Property("Tenant") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("TEXT"); - - b.Property("TouchCount") - .HasColumnType("INTEGER"); - - b.Property("UserKey") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("TEXT"); - - b.Property("Version") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("Tenant", "ChainId") - .IsUnique(); - - b.HasIndex("Tenant", "RootId"); - - b.HasIndex("Tenant", "UserKey"); - - b.HasIndex("Tenant", "UserKey", "DeviceId"); - - b.ToTable("UAuth_SessionChains", (string)null); - }); - - modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionProjection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChainId") - .HasColumnType("TEXT"); - - b.Property("Claims") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("CreatedAt") - .HasColumnType("TEXT"); - - b.Property("Device") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("ExpiresAt") - .HasColumnType("TEXT"); - - b.Property("Metadata") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("RevokedAt") - .HasColumnType("TEXT"); - - b.Property("SecurityVersionAtCreation") - .HasColumnType("INTEGER"); - - b.Property("SessionId") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Tenant") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("TEXT"); - - b.Property("UserKey") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("TEXT"); - - b.Property("Version") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("Tenant", "ChainId"); - - b.HasIndex("Tenant", "ExpiresAt"); - - b.HasIndex("Tenant", "RevokedAt"); - - b.HasIndex("Tenant", "SessionId") - .IsUnique(); - - b.HasIndex("Tenant", "ChainId", "RevokedAt"); - - b.HasIndex("Tenant", "UserKey", "RevokedAt"); - - b.ToTable("UAuth_Sessions", (string)null); - }); - - modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionRootProjection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CreatedAt") - .HasColumnType("TEXT"); - - b.Property("RevokedAt") - .HasColumnType("TEXT"); - - b.Property("RootId") - .HasMaxLength(128) - .HasColumnType("TEXT"); - - b.Property("SecurityVersion") - .HasColumnType("INTEGER"); - - b.Property("Tenant") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("TEXT"); - - b.Property("UpdatedAt") - .HasColumnType("TEXT"); - - b.Property("UserKey") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("TEXT"); - - b.Property("Version") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("Tenant", "RootId") - .IsUnique(); - - b.HasIndex("Tenant", "UserKey") - .IsUnique(); - - b.ToTable("UAuth_SessionRoots", (string)null); - }); - - modelBuilder.Entity("CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore.RefreshTokenProjection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChainId") - .HasColumnType("TEXT"); - - b.Property("CreatedAt") - .HasColumnType("TEXT"); - - b.Property("ExpiresAt") - .HasColumnType("TEXT"); - - b.Property("ReplacedByTokenHash") - .HasMaxLength(128) - .HasColumnType("TEXT"); - - b.Property("RevokedAt") - .HasColumnType("TEXT"); - - b.Property("SessionId") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Tenant") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("TEXT"); - - b.Property("TokenHash") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("TEXT"); - - b.Property("TokenId") - .HasColumnType("TEXT"); - - b.Property("UserKey") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("TEXT"); - - b.Property("Version") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("Tenant", "ChainId"); - - b.HasIndex("Tenant", "ExpiresAt"); - - b.HasIndex("Tenant", "ReplacedByTokenHash"); - - b.HasIndex("Tenant", "SessionId"); - - b.HasIndex("Tenant", "TokenHash") - .IsUnique(); - - b.HasIndex("Tenant", "TokenId"); - - b.HasIndex("Tenant", "UserKey"); - - b.HasIndex("Tenant", "ExpiresAt", "RevokedAt"); - - b.HasIndex("Tenant", "TokenHash", "RevokedAt"); - - b.ToTable("UAuth_RefreshTokens", (string)null); - }); - - modelBuilder.Entity("CodeBeam.UltimateAuth.Users.EntityFrameworkCore.UserIdentifierProjection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); - - b.Property("CreatedAt") - .HasColumnType("TEXT"); - - b.Property("DeletedAt") - .HasColumnType("TEXT"); - - b.Property("IsPrimary") - .HasColumnType("INTEGER"); - - b.Property("NormalizedValue") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("Tenant") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.Property("UpdatedAt") - .HasColumnType("TEXT"); - - b.Property("UserKey") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("TEXT"); - - b.Property("Value") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("VerifiedAt") - .HasColumnType("TEXT"); - - b.Property("Version") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("Tenant", "NormalizedValue"); - - b.HasIndex("Tenant", "UserKey"); - - b.HasIndex("Tenant", "Type", "NormalizedValue") - .IsUnique(); - - b.HasIndex("Tenant", "UserKey", "IsPrimary"); - - b.HasIndex("Tenant", "UserKey", "Type", "IsPrimary"); - - b.ToTable("UAuth_UserIdentifiers", (string)null); - }); - - modelBuilder.Entity("CodeBeam.UltimateAuth.Users.EntityFrameworkCore.UserLifecycleProjection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); - - b.Property("CreatedAt") - .HasColumnType("TEXT"); - - b.Property("DeletedAt") - .HasColumnType("TEXT"); - - b.Property("SecurityVersion") - .HasColumnType("INTEGER"); - - b.Property("Status") - .HasColumnType("INTEGER"); - - b.Property("Tenant") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("TEXT"); - - b.Property("UpdatedAt") - .HasColumnType("TEXT"); - - b.Property("UserKey") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("TEXT"); - - b.Property("Version") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("Tenant", "UserKey") - .IsUnique(); - - b.ToTable("UAuth_UserLifecycles", (string)null); - }); - - modelBuilder.Entity("CodeBeam.UltimateAuth.Users.EntityFrameworkCore.UserProfileProjection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); - - b.Property("Bio") - .HasColumnType("TEXT"); - - b.Property("BirthDate") - .HasColumnType("TEXT"); - - b.Property("CreatedAt") - .HasColumnType("TEXT"); - - b.Property("Culture") - .HasColumnType("TEXT"); - - b.Property("DeletedAt") - .HasColumnType("TEXT"); - - b.Property("DisplayName") - .HasColumnType("TEXT"); - - b.Property("FirstName") - .HasColumnType("TEXT"); - - b.Property("Gender") - .HasColumnType("TEXT"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LastName") - .HasColumnType("TEXT"); - - b.Property("Metadata") - .HasColumnType("TEXT"); - - b.Property("Tenant") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("TEXT"); - - b.Property("TimeZone") - .HasColumnType("TEXT"); - - b.Property("UpdatedAt") - .HasColumnType("TEXT"); - - b.Property("UserKey") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("TEXT"); - - b.Property("Version") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("Tenant", "UserKey"); - - b.ToTable("UAuth_UserProfiles", (string)null); - }); - - modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionChainProjection", b => - { - b.HasOne("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionRootProjection", null) - .WithMany() - .HasForeignKey("Tenant", "RootId") - .HasPrincipalKey("Tenant", "RootId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - }); - - modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionProjection", b => - { - b.HasOne("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionChainProjection", null) - .WithMany() - .HasForeignKey("Tenant", "ChainId") - .HasPrincipalKey("Tenant", "ChainId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20260405222857_MultiProfile.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20260405222857_MultiProfile.cs deleted file mode 100644 index 049933f1..00000000 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20260405222857_MultiProfile.cs +++ /dev/null @@ -1,49 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.Migrations -{ - /// - public partial class MultiProfile : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropIndex( - name: "IX_UAuth_UserProfiles_Tenant_UserKey", - table: "UAuth_UserProfiles"); - - migrationBuilder.AddColumn( - name: "ProfileKey", - table: "UAuth_UserProfiles", - type: "TEXT", - maxLength: 64, - nullable: false, - defaultValue: ""); - - migrationBuilder.CreateIndex( - name: "IX_UAuth_UserProfiles_Tenant_UserKey_ProfileKey", - table: "UAuth_UserProfiles", - columns: new[] { "Tenant", "UserKey", "ProfileKey" }, - unique: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropIndex( - name: "IX_UAuth_UserProfiles_Tenant_UserKey_ProfileKey", - table: "UAuth_UserProfiles"); - - migrationBuilder.DropColumn( - name: "ProfileKey", - table: "UAuth_UserProfiles"); - - migrationBuilder.CreateIndex( - name: "IX_UAuth_UserProfiles_Tenant_UserKey", - table: "UAuth_UserProfiles", - columns: new[] { "Tenant", "UserKey" }); - } - } -} diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20260405222857_MultiProfile.Designer.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20260406192328_InitUltimateAuth.Designer.cs similarity index 99% rename from samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20260405222857_MultiProfile.Designer.cs rename to samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20260406192328_InitUltimateAuth.Designer.cs index 5077af89..1302213b 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20260405222857_MultiProfile.Designer.cs +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20260406192328_InitUltimateAuth.Designer.cs @@ -11,8 +11,8 @@ namespace CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.Migrations { [DbContext(typeof(UAuthDbContext))] - [Migration("20260405222857_MultiProfile")] - partial class MultiProfile + [Migration("20260406192328_InitUltimateAuth")] + partial class InitUltimateAuth { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) 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/uauth.db b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db index 4e86411b5803e34b1e4767ce981907694f15c1ed..b911a93d76e1fb9aec1adf6fdede1e589bee09df 100644 GIT binary patch literal 311296 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 delta 27 hcmZo@5N=SIAkE6iz`(#bSx~?p$kJ=m+|)3K9{^Fz1{nYV 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 e6eec9c3b413da71e866355becc25403f23ff34c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32768 zcmeI5=W~@s6o(&BhysDo6KVkIU{{b(5=y9cv3F6iWA9zCqu6^xQ4mp4G4|ej?+xWo z@U#Bb;dSmfQFi@sPu_XY&OCGP&7Qr#v(LL=4q-kga1Ix9g+VuMbcfLH2D#~x z;r<{WF$VgvoJV*(rqsA*CWmqq3pkfcxYD4Pwz@rQ4};wF$Z&I(j~D~}S-~n+$CMh^ z%;GSP=0wh8Ay*mn(vGKip4a$}?+wyZBE$V-K4J{i@)keF6q?t}W**0I66dpss|~to z&l+AZ$W4z7wX#GIm z;VXX1!R~itKTb33vyyAM*)TsodMx+F7Z^_J1uxT@1`v%Z1qr{<(0zjK4s%wxSI!37MRse z;61bKd<75VIbO|I%SPRHaW5^gE%?o&42jW<%t4QnUyfnj={ zqDOEYx8|u~vtGONrdf7ca6KQUEhudDNUrC$!tOq0<2^WtBRGz;xRA?I7M8uXfse|b zna{vc+`#SmY`x4~_vB!X&5hhq(7mT^yE?7huzCug z@HM{}c956o@axJxVhq$UH>Ruw)J)}5*0z8?T7C=Rcg20g80f`5oDx%LUNeo)_=aE2 G`uG!v*Rym0 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 bd8778a5429f61c15daa1fe0077633b2a8425a82..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 943512 zcmeI*31A!5o%nIdw{s0gf(S=zD?nlrIXW#VkOoPX6BFA>96J{UuVPPPCAQ>9auRc{ z6DY9lc1v5@(uH#L-ojFPE$w!@+lz%#7U;G>TcD-1|1K@N3*9Z-ZMWtBo}{rxvLs76 zZRa}#C!XWYd!P4aH1pG&dF~r(4jC*zJKJD5%^;^s9v-}HWO3~$c5V3ku5Z2V{(dpb zsGon!mv4RE{?*fdx_MJaIH5M|ipGXRi3W$*x}w5xSwn+Zw<7(&+)Pe#`>N!>Qa_UE z>3+}m&k95HJDTQ;B}U~^gZRUQ00IagfB*srAbWnTp+qg+Bo(K(( zm>YV6+eA|7{fe=#p<+JTN-TMRd(wto2%LDw%MF6`*N#wmDMV;oAn&Qr*3)Qv%Tkhw{?o= z2=vbl$hd<3_tKvuP@IO!1q;M;1Pc}{cxBqakr4zCKmY**5I_I{1Q0;r6c!lYR#9tm zyN!o7_La}-*>V0_<@czV7sG@6Jef6wWVua(r6x2*CWxK1{b zS@Ik~r7Vl~9D(ETVb||3|I;6MjzFp~A%Fk^2q1s}0tg_000IagFzW?moTEyM3skq% zoIiBa`&ZAA#ZJAdaMlA*Km-s#009ILKmY**5I_I{1WsXrtmg<0{q9rUH@yDFlT$oL z(4q5MCH{Rx&kO+yelkvpY+-C&OYPtwF{DOoxFKTvTsdlrOj$wc30_+ zMicSLbCm2}HIrA!bg3!jt&{n-*Y%`n@b&g=XzvpH>$cVjuluMh^e83I^Zj~^~gZ>a`75XE#+=?e{`>YTivqFYX4{~oEV$v2$cIS zPA!qCR?xq))3?5FGG1S*^nIF8Z&&-K-eAKrb6RCZgE_sZN%TJoC^e?7wnF`n@}!<_ zM*0?V+M#XpYE4U)7;ikFYeU`9AvN82lK-5auhk@%WH*OS=GU0omlUs`B!_l|>2n|u4*sZX3T-X+pOv;b4W(ZrQ(dK> zjk~iG&^zZn9VfF_u(FFbljNrBLh-@;4Z&Ree4RcI72I`TFcgktmgN83B`{48yjnCO>5Q|Z{DP93mZf6_F<+~#k zPl)X4E&=ojL`P%&6CWbF1yi+?-XhlxNBY^YskX+Xt||W6kf*k^Pl`N?>TWuts@CLk z8E;*yYj*N-QeH>)CtpRLI>ThotIpThvR5g)o#k$RjGI`0bVSr+p{uaW5!N%7wVtlZ z+d|@7!^uOHYjaf?Hs2|M!wly9q1LlLwS~Vi%%4Jg6UR6S+!7jcin1S zO~}o}JK77h;^ZpDj+5jH0rA@T(cxlA6gzPylb9MuJ0gpn3)fcHn67XYZ|B*qMQh^O zb8C2xAUT%6EPakZ+*~^^5kKB~vG}p@9P#7K1>#56a|Gdk|M7XZykWdA_1?i}49YWN z4JHH-KmY**5I_I{1Q0*~0R#}3K?2K+mFpMFcMe(|cDK#rwAnpzt=RWnXXT3pxfwCRh z009ILKmY**5I_I{1Q0*~fte^!FEg#h1^zJd(Bh%jKmJAf3(Ul-B+m#SfB*srAbaqJ0R#|0009ILKmY**5I|ri3M9J*XmNo>KiU7zZ{520zv(Y96RVOuBY*$` z2q1s}0tg_000IagP`1DVxrG)Nc>FydkMG}p`-kZ-P_`o*AbcRPn)3Cs}+n`*!V8Mb{=33`}yY9<#UZ{DX z=8Eb-)$c0rs@!GVZ8RI675^Qpsr~dC)0J+cAsiV{561Tmg%fIDXfzQGAFCaRbPDc6 z?z6AgH<}pi>y4|i_5n4L2=5B3v3Q>?@9Z-IN{wl&El+lJNo`i+@o+TK9gQX?($UX5 z?eMh=YE3qq@#ZCo(9R(>wI}@=ljkUb?x3$H*wEwicLW>ElUFb|ESo>4K}_cM0dqrp zS5I(lu)CpaLr+6jZ%0Q%U_;mDo^GF*-(cRjK3!)0F3k-ayW2Z`-P;@12e&u)dV4ms zcZq#=2D^Gzq!X3}l8N^Ow~18KDQgRR)JQ0jD6mlCu2;tjEDgleP(mH>72JAcAbYte zkha@yb$@iPep}tL&1(N>ESwnIBI;Ar$7B^Pk*QYD>N44bGsK$oF$%8wqZgjKf9a=P}*5q~@54Y#kl5V9;ed%V-EmU*5bux8W zvb@Q&!oxh6|AA6b(`FDT_`@7zxK?F*Q-7O%zj|R3p0D)ILBrCqly` z1=?FjD4r16(|vmD6Nrw+`X@dObPJ|xC%r{J@{jZ+zp1vyq^>Fcv74v1v@5|pi|TGV zqpH^Aav5)3t7~@ha#CJL_J4@8wg? zc;eT^`3{hYrI{n7=-L;*(~VxUfl|eDrfng?zP5GQ?AU z@@1+nv$3xH+EH^9Rn-=b#S{9ljQ*NST#^-7)EbV9`@*pTOa0+kVz5;N@3g~Mm*`qm z1Xp5t7Wl)_Jaaojk=>)A-T6|IA80$Gk$j5+qeF?&SiU8>Cm3DMPR7kiVA0fZWqsq) zw@|G~oPBu78Ohs`o5DGVe@A$i+CSEx)l4(<78Po!nN^Ocsp>aBwvAQXkS0cp+(YDi zR878g(hiuDQPpeDocj7)R&|C?U)hg@;2i@qI^u%MWlPqTbZXYQ`C3rcWleVb$tBIb3(B*L ztlOacSM1vLS^le0?R%ZPm#Rncp*vMGkg<@>sv0R6FZJz=M~6le>ZI#>-N#w#B7D*% zhIR@p;TnB&Bs3Bqj28NS&exv^?^h??^XRhB9Tg{Rai_){bE{CxN>6mOe{hO* za!&1~+=UdosVV11BKNDm^p$w3{`)3cP@XUUv}H}{lIkyQb8k=5sb)1}+5RSOQMATb z=nh5urm^r1N-{ZdRO#+dCN|1@dm?V{7s*?EEiQ2PcH_qH-uld?ykFo%1wPmh0tg_0 z00IagfB*srAb<0k^5I_I{1Q0*~0R#|00D+S#aJJmL78ls) ze&l_-#X`W_WftlU*JUXvmXQyKmY**5I_I{1Q0*~0R&E} zzlNYHLr40#Vy8f8(uQBC>s`BH~(+v{c&D!ZhX$8bIz|1r7}Cd?7~{pR)=wXb2u`f z9&F#%*XtWi4EA-ayJBj5uqV1#jl}zU)JQ0j=nD*n!jblYJZm%sx`V!+U_*OXYj9hG zxuj&w4I8=|%y|+sH!L%!_Gn(wU{3GU+%z7#pw@I?jq%Xp?5u7K#p4H}v4KEL9Z(~Q zaA;^E!@Y4ewq70UYgLETggW3$r1q9)i)950kY`m*V^ZS!0_E8*yIAS1WeF#8C(3x) zl3G*$8sqrha?4n-WJ`~@WJgq_OV^H3B>B64jRg>gW$oDusGt2 z93mH#Uk4M3mQ{kxR!!q$Z>=+pSGtUcg4vZIPg3$=?++!y(TGN+9g>^-qa$ix=7i8Q zHln5uknHW26t9%*l}p))zMR=x7d-ltc}cq@F5~!U*+tJ+R&#;s z%g$!rLLPCcn#Nb0FODHmhOK3kAz$trk0kfy=DTTp?RmAPy&}8Ul#<*=chB)S8Ddp5Lp zjeDB~se2M>cXUYIsK$oFsT^uYP-a#t&pE5v3z+7tHaRuDY0k>o=a@_e#JOadrS!|Z z^jS2WZT*Fytx%@*D`{EQu5e1b%+uBuSzS?jR+HDK+NW;THLJCptfrI6qSY5E8&+Gp zX?(--T2s_vJXD{3kY!~=<4A6`E))T3S!=H>JKNbwmQw_|rTpNT$d70zu8V6; zSBTqza5*K)(^8t#o<*e zEK4<=$`Uf%t&R+Z`qcsd*hC}BL%yP%tmjFx#6slTQXC0OOFt4OuJ4K-3CqhVPM#!7 zEY8H1xkp0tMWwf>iA3^L%f&}jjzx83fB*srAb;-CIk>b009ILKmY**5I_I{1ZKU!QdvSRF7VS8 z4}9<+-??-Sae-N1?-URL1Q0*~0R#|0009ILKmY+LuuT5Y;sV~Tzv}ztCC!f#7mzAU z2q1s}0tg_000IagfB*sr%zA+)Swbx?@SQV1vh{|AU%ZOAz^t!#3Wxv#2q1s}0tg_0 z00IagfPfUZSpLxB0*l_b^389(IJ|zx83fB*srAb1KXC!6!h`?<2q1s}0tg_000IagfWWL5SSd@W z#RWXIOH5mrzT?Bh1!jG{Q$Pd|KmY**5I_I{1Q0*~0R*IgRsPW80{``AgMIgW@`eu) z7mzAU2q1s}0tg_000IagfB*sr%z6QvETI+`So()wJ$rri|3!!k%=&t#fCwOf00Iag zfB*srAbk8gIn)>B6c5ffmvVg6c7Of5I_I{1Q0*~0R#|000AlBls~k%!0U!zzc?J;^`ZH5 zZZMo~{H;N0UT}E+bMw!uf3m)#?&G!pRlC0Cp6X|-S5#eJ`OC_c74J3v*65$tIQJ66 zyXL%KrgJ=|aYvxmwB2PKSHqD3^L0?a>p|`7jQ*W@Ly{k32t-)Mcdgg`=T@B`ZshS&>nN#~VuMlg9Oo`o_ zo5rn|)tatw8^^Dhoa^=hHIfML3ahcg+19NSOy@gahIOmu2;}Biw`QR%pL0pAX;YJN zd~J4?H>>e@I2xJAYqz>Tx>p_WB~l#P;>!wTQ(La(Z(?(O4z-oCvzA%4Y5bg3C1oyn za3ma4OHlI3knN^#{UEW$#NXQKrmkSJr!jblYzKL>W3%;$yOioH>>ddA$ zkxg`B=c17=^wgRU)N3=2Uzs;s-O*@bqA{iqxU8%t zSGY8ty#hyjy20g3D^E_7t7W&%%#psVrPj1r+)tb*+_C_`&!lg;eItI*Qr%k6>kE?)60|f)CT#TMa$K4A{y>8XRT?g zD9Gm03zDUt+LQMP@?U1Qo5nA6h{i0kx}k)u=4l_PoO;U1X~CwE&v&A2thA>{#VJbv zF`GV6Qk!b$@Z@iKn}oZhmYg@W5@pl-7LDKnn~+^H=`_(D)m;yCMPtLEq43q}Kv!s3 zO|^mK3N6JmS|kyDK9j3uH`wewifkIJwWie~o0e?YtUYL!$(gP)verH@MRvprP2-l8 z;u1<+jBK5pg^g-#IGpnVF97u!(yihs&<sez`)f^O zZsVaPy1b_^S9RB5IXmg=85>di+T$Bz;o(qBcloN@WNneW>sBkCa&BpK8|E*NIPc25 z$t#D6?1>h&$yaNNic;2>RmzDJQU{_$(`BIT$O<@-=#i8yvvJdSXicqYbjq3=_cn`* zy`07=D!9NQkZJ6hlTT(|++Lu}Q>LabY|hHr6`9$YsJ3O6yq}xh>Mn87JlG?y>oeE+ z=_Z}dwtfv;p-k&n%4vsL`PHwNo!iWDv}{G`Sxshq;*_ON-C9mo)5(;WRc-C2@eRwx z!Qhy5FyvKDZnZ8H7Y!!+J}PVNm1So;JIQj2Ah*o9F%b9hE_pAn#Ra~*<$qRvApEJb z=r3?QA{?ZN00IagfB*srAbF)}Im=n0EC<* z5B%z}`tMv%T;O<6lO_TPAb>gC?^DDDrd>UeAp{UW009ILKmY**5I_I{1dfNm zYMH1O7dU^n|HG^Qa{n&k0>^`zG!Z}m0R#|0009ILKmY**5SVrXZ<9IF;sQVY^n+U# zjQr*E#092ZJ&_><5I_I{1Q0*~0R#|0009J!hrk+{s1_Ia%~fmcFGpYALtNl^P?IJC z2q1s}0tg_000IagfB*v1PT(?`BP}lQ{Nl&{(nJ6O1Q0*~0R#|0009ILKw#Pl_+^f? zxWIcazPIKx*RT3Hae--9PhYH@)-z2m;0#XflL zdx;Ai4{Fjx009ILKmY**5I_I{1Q0-A+6lDE9BFX@XUBaXxwPr04-*%dcJ)Mt5I_I{ z1Q0*~0R#|0009ILI35B)nWz>QFm!zT_dD!!zC>K$cu(nJ6O1Q0*~0R#|0 z009ILKw#PlTrP8@#RabX*7tTTf9d4}ae--9PhKBw63rL<`12pwGhfV`Tl+hM^7aKEoBzXkzn$lw8<}%U{rBpZ*S$~7W+AK6CI2ua4y`?Fe)S zeLca3_O908wg&T*o0}UpbTycB5-~R{GpF`qULh8WJ&PTgo5mLfYfYUZYip*+n#^bW zKu)5UmzuNG)+J;ty<*e&1+BHFUYl`z!{mHzj74{ai>YZVv=_;qwoVRxZWguG3h_At zwWjSZ)XI3nf>wbT&Y0Parv}AIw z+XvK0BD^cC#&TO_U(eWx+SeZ67z+=FV!B4E+hlDKaouV;1;|M`w+?h0<}Z+F&@ykb zVNYaFH0@2kT2oY%vc9ZRPNa}&>$(iI9a#Y<5u%!71eT0N!60Gecs{)%B!B7Bug$*?siS%uGO`sP?K?dIJ+pD)p$G{jZ8Wz zbgTQLd({D7B2@})HGhHZYsTLBRSoUEcHGSv!tJYPOMEx@J;rN0l+e|ME$)HHuiQ$VRP zZM79|N&2ekOE)9^yweVCn^$XEvc!1f0bLtPel3@3Jjs8~&(~^_OR}3wR*mLwE9tFt zjYQKcTJv3@VYNWDW`ejyRyvxt(W(xqS?x;KHVRg`s2|t-8dLj{;`Ni{&?+YR%Ys9T z=G20OI&vofojWpi(zJN1b(XD@o#Olt9Y8|!OK%aaLg?AM6od5k%7G;%@z{`mD6m-&tp^ zk*TiI&&D|k=$-SPj+5CdSlLCJNpjP5q4;3_hA{C(d;arv`aD!{*MY%MIFebaJL!0`#(Jh#&ofNxnIMUCCO|>;9bxrZlhCH>UozU_u zs=Mios#=rFWxRE*uGz_+HnK}Ye<&d?T~Z$#+4HLNHMZs&1@*`Ty_wKk_qrX$)fzWwbLzC(GI zc8gCGd4lOyLs_*@cz4}uTusQ$)Oeu_j^rxEj+5jH0r6e+=x~7pA-P7e6K8UTsd2O; zve>zBZFP<53Rm%Vp50osCZ0XFW_(*k@~Y_2hGgsR>kGDZhIhx3%{sm=98W}JV|iy* zX1DLW>tq`EM4Fk;odUN$1ro~B)I~3B z`r0GmM6Xy;eE+Y?a8PPZQK;m1{aSy4fd9Jde!AtEXXSGQ70N#tlo!MgCIk>b009IL zKmY**5I_I{1Q3{h0&lGh7>mRRD$JGZ>yyh1M+Zcq7RrKYaRKWSiHgOyk8PFx1?Cxc z8s`42{-yd4)PAzQ)3CF4XH8x0l6gz3zh3c*+~(+}o2nKjdt2P<(Dl5M-Q=|Q=cM{w z>F1cTds|FiA-lIlR+o|7t`_+=&eIL0Xiv`Ko%-6sv3Nq?yIS7|N_3Gbu&6a07yUHH z3M}=9V~N355!%Vqb!x3DUKJI~v%nvY=9$|OitHW@?ar5y>|nAZ8p*dPAc9b%v3yH9 z)kH`(QEoW5|rM6#U}eOjbd z-5<{MN6pz+U?>zG7MfWHzSe0fw|VAlJJHcBw~tnOdwu`>ww%XM;@OY%B)ecwy!B?H zLw4JS?qK`cuH+*=`iE1r6`C5lgKfd?U{@fxIp-mxREnaB>E6ed*-X5;tu@#Y6t8>h z4vOcD+5IaeJ+S2iHD#yHe0#{>Z!!7G!UD&qhMd1@MF;l8Xpuh0Ij54zT`jfKz)AQhYFc~d z)ctv7@iY4R%FfI^>sUSpdHre0uE95-o6~Z1k5FbRRyXtXLTyL4&Ji`9tnGD&qE>T9 zan{J`)1G}+FehIaSD;m9o|7xKR_1kkle+k7xu2@tiO(PT3dQ_SVH}T#DUQj5kT-8R zJNX3HyT$RDd-at1SV^^^tPhoQ^R=L?HL}}J?k95oOZl7xbPXu~1GU=cXa4hz_AqJQ zONpasiBmNL`Rv4IRgDyUzRI^V9vvD@sFSX{baxl2i=;{CZLKMma1FIN5*mpQMhiU@ z?CVd2_p52~%zkuP=#GjLwz!fDL`NecmE0=SveFYB?H`FdS0y3GLII48l zj}sf^y?hk+_U-ciUW*It-hI~_Uwvit>%4dH=%OBMjQ|1&Abk;#!5 z#)@$Y({KHpasWlG>xgLopJbfEtRX2gT^w?nX>QxhteC%bHjOh*Vd2pjC%P$Q&Lp}{ z7mQPweF*Dl3N<#yDa;-CGpD~y-iF3Fg@b+Rz7w+Bu6S$>9Q}kT31=EqIipB5w*C$n+xkN`QDp1+htt9sJzP{{xBhc z00IagfB*srAbMd@6ThQWkT0ItDtIKW) z1Z=jT-RAQI+B|)I@o;4KklNQ5nqrz99%yM6gBi5AyrNiV$)ag-fv;Gs&-~Tr|1SA{ z0i*JqLHuDt009ILKmY**5I_I{1Q0*~ff**Sw6eFpl-PnO);jV3#QO#Qx^=K!V`qKoJqbjyOtNTNe}Rqv{lw${ zv|{IdspkkH1|>363Qj%|KmY**5I_I{1Q0*~0R#}3?gHbA@l5fkfWfddom2V9z_|{W z!yoXp`7Ab<-D7du9jz9R$KkVh99FN}?(zq1wlba>@C^)yBjI==7D_~8g&!aoP-H^X|UvUw?Z|s=olw5v1#hzYstG0R#|0009ILKmY**5SUQ{@;QQ1 z;tiru?c)E5=Lin|kR;sTEul*eXNvB@j~2q1s}0tg_000IagfB*ugra((| zW9PYLJV21?FL1g9)A|d1`*Umm=iS%7za{k?!Bqz3s#BAbQX_x>0tg_000IagfB*sr zAaL{o<8L+2mqQGgO!YqT^gvyp#oc1_iRT1-?lzZrV!-dV_*~wArPbwhw6yu$UROC! z420wp1IZtS9~UURWMZ7cGh{inxWE$ASF38iz2F?4BRKlnCj|r$KmY**5I_I{1Q0*~ z0R&E#fP9XilqiJAUqI$jiwpeAC)Pal;op4bYpJ-vs|MxOlT`xtivR)$Ab#sAWO{N>B3xWLl}<>`}@TXu^8 z0tg_000IagfB*srAbGG|0)$1c*>wW zRel!O0s#aNKmY**5I_I{1Q0*~0R&Etz)CSJL4A2~fwdy16XO&v{>aT6_P+h$>Qr3d zNrUp_$teeWMF0T=5I_I{1Q0*~0R#|00D-avtTkfvgEHd+S>qJ?9=!7WkDcS6E5|9U zSa63Jr%?Q0LI42-5I_I{1Q0*~0R#|0;6w|I`zz!Se@3r2HN;%K1g3sfi%8k7f>=T3Chvp)n7KmY**5I_I{ z1Q0*~0R#}pE3mAx*H}t~q~b`&DV+Q3>Tmz3?~i?{xWFS~oI>T1yg6Ye0tg_000Iag zfB*srAb6w)#NyZ;x8ry5I_I{1Q0*~0R#|00D)N`Fn)1ma_GOt#?;V%{-7%u zY;pK49$TBu;}EUKmY**5I_I{1Q0*~f#W40haW5@N>h2<$0_tHd(N9TxaIw+xWN4e z<$gvgJYKa*H_pX6VuZ+rLH%{R-KaITWD=(Vg zmx>E~GC5A6^2u^?$+`$2fB*srAbfv_dfcvDHRt8C&wvN!pG8pv=Bf50R#|0009ILKmY**5I6+|${uuZVP)By9N9R9 zi)3wR;}P7t^n;h$_J1!T$0@8(&Ne9T5*{<4L!HC!@>{(Qr^Vmqv09vgpv&S3dRi?3d#k<0<_y>zK4)e;$DLD5`9=skp$s$@dPP^iq=n1Q0*~0R#|0009IL zKmY**PE~=N_YO|c8DZxUym!zfX|=e(pB8=QJNrAA{w(<%!Gh-vN<{o%LI42-5I_I{ z1Q0*~0R#|0UPX#!<7LQ*%9}x7| ztgQ~WH{kO6Gd=XDx)0x+2t@`$v4Mu(xEjmXT|f1Rz<|0dG&+V z3$%UrH@`pQwWgaUo+EfpJWRlZ00IagfB*srAb@B99`WoM@10=F5I$CTTqL&3=d0tg_000IagfB*srAbEfzzd{)?eU;FU+&8eQG@U96{Cm4uf*lf;;BF zD*nNQ00IagfB*srAb8nQie}?Ounw)nawKd;+u0FJ90uMh0+sTn@M0>9cw~w$d{@CnS5@ zUnaWthlfK6H8h$SZ0?T^=bu|RtA&@O3-MNCvPGCosX_!=+$}brcuv6QZgYtz2K;V| z&*cqRT3t>@OPkN_b(QnPKr*+5ac zDNzVfo`5Wq78hv#%oi?NJn;4pr{V%1GAQ>euPToyA1bTdtc(Bx2q1s}0tg_000Iag zfB*tVC2(d{<09SD1hS(@)#9qgM%}zjuZ+qIs~Ww!CHcEORERW9`X>!!mkE){R+&*P zF7U+bJNNB-+b=$tiVM8gp!`~SukuahlSfq;Hbwve1Q0*~0R#|0009ILKmdUf1r}B} z)=dftoLk*!)W-yL4{cN~70dKdfdbD+RLF$$#RY;gV_IC`?7!O+`NP7^!BkwpXHcG2 z?p5|FVk8G91Q0*~0R#|0009ILKmY**5SS#evP!(MA@gE`sooM%m9zd-Z*8b7bNz~D zksp1hfGKt$%F`yxq{Ri!f2`%9UvBC;I~5mLZBU+4?p7ko>Ph8h1_B5mfB*srAbdg;uGT(DsieF0R#|0 z009ILKmY**5I_I{1df5g1yzl1-Af7bc3P=ES>qJm`nlh2{nPK~otBCV?A698+{;LX z$Drm(0s#aNKmY**5I_I{1Q0;rR2H~Mj3=lM2;_gZrTXNKQ~2Ru-S(lcuKU7oQgMOP zC&npMl5Zh6m8*}kBY*$`2q1s}0tg_000Ib{5&}8n0!}sVV0D@6Pxo;O|7PR`*ZW>R z@IWdqFmGaJZwPN7nD3O5XjjsOA(AblHFN*-rB^?HiyWu0X72O4aSFvROb8%=00IagfB*srATZ+u#xJf(4*l2Im>T-e z)9ML$Y%Z6@8VI&ooNlMv;`6t*TC5Id!0K-E*jrlKN+0@fq1WSXaXB4!n>F*V!bABL zUXmWtjd2Q#-;T=n)5-q}Ll$0=Ccd~bt3CLGu3)gm;kS5fZ8nQjv@QHG%uS6`I9GXx@*E=-&bSW% zGK>HM2q1s}0tg_000IagP>z5cRj`!MSbh>#cPz}BY*$`2q1s}0tg_000IagFqOc$Vk|;^I6(KXP0frLr|{;-pZ)La z9{YbkOU4BjyrdhaQ2fG#00IagfB*srAbey}HRWzlUG4@m zV4T8B{&?u3=+)kE+c>0uXO%xsh1Q0*~0R#|0009ILKwxGFls)L+!kV%- znO)-)*2;>#Zi z|y}Eh1Z!i=bXt~@uxOLE>wrm}-_N?=%>)P8pLaO)b z@Zj1_JsXGDbu8bzeADW-R-4t?+G4i^oi>*kY|`bncw9ce#TxY4-Ck=;OE3_eJbB7|KNVUDis%a!k|1+I4PDOfB*sr zAbvDMeVc36(s=ia-2>z2#AMnZcMW81gv4aP@@cH1I5Bi_yp z2Um7^2b^Bto^AWK^z@{kEn4I`b@>gwH#gUFv*!#qluN{2<#lK3$1zr;46e=%Ghk}y_1Q0*~0R#|0 z009ILKmY**5I6z>MT|>m%=G1`nEc+stZ@nlcHH%u|NG^-J5zCiJH$AJ${k0rS2jTa z0R#|0009ILKmY**5I_KdqY-e3F$Z<`^IAuMqMZbe!8nEYeedFTeDrg+H|Q^LG?mA; z2q1s}0tg_000IagfB*srOcQ~r`wL`^Q+U^_TMym$Pr=SqT;N_YPN8z|G${$$KmY** z5I_I{1Q0*~0R#|0;M5aXDaIYt^&6Ptm5w_rj&hvB&2w(@f4lNs%TsZIyTv$#%H5|P zIweK`0R#|0009ILKmY**5I|sB2w3H?gXM$;j$oX^xw52}JUn=t9FO49vtNG8X@QHs zAjc^*Dvuk)A0`A4KmY**5I_I{1Q0*~0R#}3{sIo8cqM_7dBvbDJz3=6IX|>z7mH%e zlSR|w0)PC^$S3Ar&~&5hFF;&C$}k~-00IagfB*srAbFdUBfT07gj>{fe=#p<+JTN-TMRd(wto4eU=x4PXfuRvQM z;k5n&pS>_~-&_Csi_fL{3p{5~o|~nt6bk_a5I_I{1Q0*~0R#|00D)O7u(YzbUJDfz z3nK^uyJXPf0zXmTe$^MZzW979F7TQ`d2JRWQ8WY)KmY**5I_I{1Q0*~0R&Dl0WoZ$ z{#gV0egTIh(BcAjzH#>DcYpp{|1T96c*3AOaf$&^Tm%q6009ILKmY**5I_I{1ZJv$ zUA%a(%ohxZqPk>pwYb2i9*pi^f5rcOh`7K^Z3N^S0R#|0009ILKmY**5J2FR6*#iE zK#TC|(BcAn&i>A?hrV>nKq@ZqszG`6l+~o*2q1s}0tg_000IagfB*sr%qoF1s~SzZ z?gYtK5{d$5jYr`6?{`0U&AtEow{m8M(k8|u5I>j@KmY**5I_I{1Q0*~0R#|0poqYZ z3NdEDqD6Ad0DF3}$iH)bXv;EV2K2_&SnuXwcaaeTPV9IDcYW&BmVRV2X%ANDB+(L<^F?YRkk9FPA&j;sV2o zmsY*r{{f-=W0?e)(|! zd0XDN{pwU);Aw;M^mHj4`9J^x1Q0*~0R#|0009ILKmdU$1)8fGJIm-(AkxcvzrdCE z{ouKOx?t6v^8Er8N`rX6fcU|L00IagfB*srAbR9xT=@qPj2j&cglx(FbE00IagfB*srAbKAU{MfcT3E0R#|0009ILKmY** z5I_KdX(14)%z55_=M+;ZU$hr{-v1PPzrf+fhp+v~oHH+y{RJwOisbtR#9vGZAb;F0e28egS3Q zi9li>2q1s}0tg_000IagfB*sroB{$l?+ci!cS2>(`crj0ID+>JR7>?s9v-}HWO3~$ z?!4?bz5n_j*Z)GkU%;r`V-SCs5I_I{1Q0*~0R#|0009ILK;R?^TxC41x3RO%P-l>T z#E-hRHd~v|;d5AQ*0zAfX$^QSeyhi4v9?(qeuv#}Z*vFw`r_fp?jg0WZ>l(gNU}zz ztHlNW;>RC9`=j%WSIPbYM&&t!_``$%0tg_000IagfB*srAb+E!ae>z!{mMDPKO8)VxPVk)LI42-5I_I{1Q0*~0R#|0V3r9?jtex(5^8aQ z^FH=c>%G6;FrJDFJT2ZkILpdUaS%WN0R#|0009ILKmY**5SYyZ%~g$^y8ZwqykEd9 zIkdRInZNw-Z+`L5fdi?yz*7e0so9K0*$_Yg0R#|0009ILKmY**5SU#8E33s@3d@TN zED-`7T3q0pmET-*^-n+kLMkrsq(OOdcKJ{m1Q0*~0R#|0009ILKmY**X0d>^Mh;$3 zUR)q+oWif1we|7gAH1|lj#F5ntP$fBiXTh}AbTLlTc1}i2wo!Ab(lCybR{zxh=aIYArP`URcmw??PfB*srAbI$qB;|}V+@1N?0 z1Qka)PT`tG<9A*7;YXiL#RWbh#wk=ja_Zt!as&`S009ILKmY**5I_I{1Wu-aO^iBd zEF&r~r{ZYGDQq?_c;B_3eNXzmgLjH?3Y9xgrU3g!009ILKmY**5I_I{1Q0;rR28s` zK?jX>WrYQfbezHt?fn9;y{qTHFZ*0&GvgGVs;!Q4BY*$`2q1s}0tg_000Ib6MIWGP7ELfonF;`{?6Oeq@P^3mBDm8pIza1Q0*~0R#|0009ILKmY**5IFt< zapN3AZ)0b`V5qC}`MhoZmey8_%hlFmakd767JrMk&C+HII9(2BtKZ=cmfq{YZnd{q ztWJxyrNQQ1Ww)-f+nTK{R)?*{CW>{MESeS<@L&3U^BWs`cF9fzM&${E_``$%0tg_0 z00IagfB*srAba+T_0W}l{dIM9hI)@T;B`3sL5thr3|gGQ zR=>sD;`3SBTH5S(o73fYx$LFfbY^Z*r+SXQ$T!+0EV4N!eGfjod#z`m-RTHy_IlUt z_BjTEn|8)M2iym|d#ron?cuKQx=n4W%^MiowE7zJfVw~2ueJ}ESDDM%(7YnOWzX1% zDz+U^<9id)5ixsYD3sV0jSXkbjE<i0aKUw0_gU*LAb{M%=#6~#gT0R#|0009ILKmY**rnA6}gT~Vh{mF0f4RwdEJUoAV zeq9;g0he+=cuc>_&+~o#4Ezq>JQ@-&@LQEoBcVt_eD7|~7@5VF@`H)Q$f}hqcPME9x_i~C38iKytUAQxXJ4g3L00IagfB*srAb)$EdmH2fB*srAb>g74`lh(^3WZ|3qY?WBj^-AZ%hA$ocQ&td z@A8cH?ugm1Zt3wH>~0Txy&>PG)vKF_$JTBR#d~(_@OSvOS_iJ~*tE^RckO0>&zAmR zsAb*dLxamVtq&crZtM1}cG}$@zr*3Q1RS1Li__z)!?eGBs;@-P|>z2#AMnZcMW81gv4aP@@cH1I5Bi_yp2Um7^ z2b^Bto^AWK^zE^jf?wo7WO-v$c9WfuPe9Y@JkU0b;kg z#HR8|`!{sWK$Kdm>YNsk{8d|)DRueHU|@%JyQyd6@VbuWdzWuo-PUTeI$K-pmY~z- zvN)|SQGS=tZ?Oh_cDL8s(h>{=^OZW?3`D8Vmj{H_U*P_Cjkg^B<;CBbFXkENUu_V7 zm=Hh!0R#|0009ILKmY**j*GxemB!Nz{o+nE5D3&Ay7KV+@%i;-bO0~qW%H%qv`)Hn z%)FUSTa@Reb=sndu!FSuQCtsUhCOscRB)_z20@ZeU5?P zrk!!m0rvs#9_yZXd$=pSZd03T^9IH?tv0VnZ`m_8qKa(?)cD>+bVSS^844wKMPtKR zGovGFER+xtn$2VJgesRE2uB8@2jXIboiXvZ8WZ#S2V>D;Rm=*H_yz`IYCJAxu3BX? zUw4hUe<&0l7QbI(9u18q23IB2NGOspue!pV;WJ;k!aSHrjI3I@GTuL^4u|5+!{Po| zG#=fRXzq^=uRIW6X}4NEE3GXn!vkt05l)P)OeMTB79A4hxWeok7!F577J6ltM0%;c ziR=%DfiD`AFV3uz zlUoE3KmY**5I_I{1Q0*~0R(2fz~$oQg8DuKQ@z@tcz=Ov;WhEz!P_5SzcjjUu3Nre zph8(aKmx zC>0lYn~V!2-#>8bRzD?2009ILKmY**5I_I{1Q0mI1&YK4{wy6bn7C^{)**w+6A%}W zj|@z_U*PA@T(`0BZ5!{Eae+$Z9}LPX%G2U6CIk>b009ILKmY**5I_I{1Q3{R0%ufK zHW_<;VibkG&0>rNF{nYj;;f2FM}5}3z+fmGi7zr%+Op?&N27_jDAKvISlW9BA3J>M z?HwQQTb>%H@J++~H>X>n$qxbuAbOoZ)t6{xLj>57H4Z9Xz{mr+bnIifYar0w)!3JVCmi2Cv~NtJWgWf zrFouar`PVaIkb)PjG$P~$cQs=NJZUXhGN9S?BNTqyl&6%z;I-H$Ii{WHpZL-?OR-G zZ+J^^ptaq#Zu{QLx9%U=YZozU%FXZoZpE+1lFt#$J>4+> zYVm^!0R#|0009ILKmdVLP2k2|6{%t6eLh2aSoyj#F0@Ozr8_1phpO#Rj$kL~J-?o}ri_m9RC(c#qM>%{4OwLHaZae?PwJL?-4zVXn2e7K-Oxx=75 zA$~9+fB*srAb+lWFX-z%ua1s=Hb z-){Torx$)E6&JYHpj@jwbYct6-Vi_l0R#|0009ILKmY**5I8jioYjrJhH~%yyTnHN zF$zlw7!>K(A*9zxZ7nV^^!s~$`*`0y_od1MO+MtvOAbpxy0HJm4|$C{cx2Qy)8+v}Wch5mDmHWT~~d!0iuwU|`-W)zMU3 z;1PrJi1LhF&V&F02q1s}0tg_000IagfB*v1RY0j~)OS266c@xZ=G2=eq7`2XmClUe`( 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 8f941e55..ed06416b 100644 --- a/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IUserClient.cs +++ b/src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IUserClient.cs @@ -4,23 +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); + + /// + /// 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); + + /// + /// 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); }