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