From 98628f7df8c2b3f49172f4f977d826e7a69cefd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Can=20Karag=C3=B6z?= Date: Wed, 11 Nov 2026 16:22:35 +0300 Subject: [PATCH 1/5] fix(security): prevent account enumeration through login failure responses --- .../Options/UAuthLoginOptions.cs | 7 +++++- .../Flows/Login/LoginOrchestrator.cs | 22 +++++++++++++------ .../Redirect/AuthRedirectResolver.cs | 12 +++++++++- 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src/CodeBeam.UltimateAuth.Core/Options/UAuthLoginOptions.cs b/src/CodeBeam.UltimateAuth.Core/Options/UAuthLoginOptions.cs index 4677358e..cfcbb601 100644 --- a/src/CodeBeam.UltimateAuth.Core/Options/UAuthLoginOptions.cs +++ b/src/CodeBeam.UltimateAuth.Core/Options/UAuthLoginOptions.cs @@ -21,8 +21,13 @@ public sealed class UAuthLoginOptions /// /// Gets or sets a value indicating whether detailed information about authentication failures (like locked until or /// remaining attempts) is included in responses. + /// Disabled by default to reduce account enumeration risks. /// - public bool IncludeFailureDetails { get; set; } = true; + /// + /// Enabling this option may reveal account existence or lockout state + /// to unauthenticated clients. Use only when this disclosure is acceptable. + /// + public bool IncludeFailureDetails { get; set; } /// /// Gets or sets the time interval during which failed login attempts are counted for lockout purposes. diff --git a/src/CodeBeam.UltimateAuth.Server/Flows/Login/LoginOrchestrator.cs b/src/CodeBeam.UltimateAuth.Server/Flows/Login/LoginOrchestrator.cs index 425e450b..41c26b73 100644 --- a/src/CodeBeam.UltimateAuth.Server/Flows/Login/LoginOrchestrator.cs +++ b/src/CodeBeam.UltimateAuth.Server/Flows/Login/LoginOrchestrator.cs @@ -91,14 +91,14 @@ public async Task LoginAsync(AuthFlowContext flow, LoginRequest req if (accountState.IsLocked(now)) { - return LoginResult.Failed(AuthFailureReason.LockedOut, accountState.LockedUntil, remainingAttempts: 0); + return CreateFailure(flow, AuthFailureReason.LockedOut, accountState.LockedUntil, remainingAttempts: 0); } factorState = await _authenticationSecurityManager.GetOrCreateFactorAsync(flow.Tenant, userKey.Value, request.CredentialType, ct); if (factorState.IsLocked(now)) { - return LoginResult.Failed(AuthFailureReason.LockedOut, factorState.LockedUntil, 0); + return CreateFailure(flow, AuthFailureReason.LockedOut, factorState.LockedUntil, remainingAttempts: 0); } foreach (var provider in _credentialProviders) @@ -193,16 +193,16 @@ public async Task LoginAsync(AuthFlowContext flow, LoginRequest req } } - - return LoginResult.Failed( + return CreateFailure( + flow, factorState.IsLocked(now) ? AuthFailureReason.LockedOut - : decision.FailureReason, + : decision.FailureReason ?? AuthFailureReason.Unknown, lockedUntil, remainingAttempts); } - return LoginResult.Failed(decision.FailureReason); + return CreateFailure(flow, decision.FailureReason); } @@ -215,7 +215,7 @@ public async Task LoginAsync(AuthFlowContext flow, LoginRequest req } if (!credentialsValid || userKey is null) - return LoginResult.Failed(AuthFailureReason.InvalidCredentials); + return CreateFailure(flow, AuthFailureReason.InvalidCredentials); // After this point, the login is successful. We can reset any failure counts and proceed to create a session. if (loginExecution.Mode == LoginExecutionMode.Preview) @@ -279,4 +279,12 @@ public async Task LoginAsync(AuthFlowContext flow, LoginRequest req return LoginResult.Success(issuedSession.Session.SessionId, tokens); } + + private static LoginResult CreateFailure(AuthFlowContext flow, AuthFailureReason reason, DateTimeOffset? lockedUntil = null, int? remainingAttempts = null) + { + if (!flow.Options.Login.IncludeFailureDetails) + return LoginResult.Failed(AuthFailureReason.InvalidCredentials); + + return LoginResult.Failed(reason, lockedUntil, remainingAttempts); + } } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Redirect/AuthRedirectResolver.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Redirect/AuthRedirectResolver.cs index efbaa8f7..1c0f5c91 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Redirect/AuthRedirectResolver.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Redirect/AuthRedirectResolver.cs @@ -68,12 +68,22 @@ private RedirectDecision Resolve(AuthFlowContext flow, HttpContext ctx, string? // Failure payload if (failureReason is not null) { + var publicReason = failureReason; + + if (flow.FlowType == AuthFlowType.Login && + !flow.Options.Login.IncludeFailureDetails && + loginResult is not null && + !loginResult.IsSuccess) + { + publicReason = AuthFailureReason.InvalidCredentials; + } + var payload = new AuthFlowPayload { V = 1, Flow = flow.FlowType, Status = "failed", - Reason = failureReason + Reason = publicReason }; if (flow.FlowType == AuthFlowType.Login && loginResult is not null && flow.Options.Login.IncludeFailureDetails) From 823b54d21fb4d8c4a6bf402cb5879dda2e977333 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Can=20Karag=C3=B6z?= Date: Wed, 11 Nov 2026 16:54:52 +0300 Subject: [PATCH 2/5] Fix Tests --- .../Flows/Login/LoginOrchestrator.cs | 4 +- .../LoginTests.cs | 60 +++++++++++++++---- .../Server/TryLoginTests.cs | 46 +++++++------- 3 files changed, 73 insertions(+), 37 deletions(-) diff --git a/src/CodeBeam.UltimateAuth.Server/Flows/Login/LoginOrchestrator.cs b/src/CodeBeam.UltimateAuth.Server/Flows/Login/LoginOrchestrator.cs index 41c26b73..89cfb72b 100644 --- a/src/CodeBeam.UltimateAuth.Server/Flows/Login/LoginOrchestrator.cs +++ b/src/CodeBeam.UltimateAuth.Server/Flows/Login/LoginOrchestrator.cs @@ -197,12 +197,12 @@ public async Task LoginAsync(AuthFlowContext flow, LoginRequest req flow, factorState.IsLocked(now) ? AuthFailureReason.LockedOut - : decision.FailureReason ?? AuthFailureReason.Unknown, + : decision.FailureReason ?? AuthFailureReason.InvalidCredentials, lockedUntil, remainingAttempts); } - return CreateFailure(flow, decision.FailureReason); + return CreateFailure(flow, decision.FailureReason ?? AuthFailureReason.InvalidCredentials); } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/LoginTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/LoginTests.cs index 5cdc5fc4..c5415251 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration/LoginTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/LoginTests.cs @@ -783,7 +783,9 @@ public async Task TryLogin_WithRepeatedInvalidCredentials_ShouldParticipateInLoc correctResult.Should().NotBeNull(); correctResult!.IsSuccess.Should().BeFalse(); - correctResult.Reason.Should().Be(AuthFailureReason.LockedOut); + correctResult.Reason.Should().Be(AuthFailureReason.InvalidCredentials); + correctResult.RemainingAttempts.Should().BeNull(); + correctResult.LockoutUntilUtc.Should().BeNull(); } [Fact] @@ -1575,11 +1577,14 @@ await LoginAsync( [Fact] public async Task Login_FailuresWithinFailureWindow_ShouldAccumulateTowardLockout() { - _factory.Clock.Reset(); + using var factory = CreateDetailedFailureFactory(); + factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var user = await factory.CreateLoginUserAsync( + ct: TestContext.Current.CancellationToken); using var client = CreateClient( + factory, $"failure-window-inside-{Guid.NewGuid():N}"); var first = await TryLoginAsync( @@ -1592,7 +1597,7 @@ public async Task Login_FailuresWithinFailureWindow_ShouldAccumulateTowardLockou first.RemainingAttempts.Should().Be(1); first.LockoutUntilUtc.Should().BeNull(); - _factory.Clock.Advance(TimeSpan.FromMinutes(14)); + factory.Clock.Advance(TimeSpan.FromMinutes(14)); var second = await TryLoginAsync( client, @@ -1608,11 +1613,14 @@ public async Task Login_FailuresWithinFailureWindow_ShouldAccumulateTowardLockou [Fact] public async Task Login_FailureDuringLockout_ShouldNotExtendLockout_WhenExtendLockOnFailureIsDisabled() { - _factory.Clock.Reset(); + using var factory = CreateDetailedFailureFactory(); + factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var user = await factory.CreateLoginUserAsync( + ct: TestContext.Current.CancellationToken); using var client = CreateClient( + factory, $"lockout-no-extension-{Guid.NewGuid():N}"); var first = await TryLoginAsync( @@ -1634,7 +1642,7 @@ public async Task Login_FailureDuringLockout_ShouldNotExtendLockout_WhenExtendLo var originalLockoutUntil = second.LockoutUntilUtc!.Value; - _factory.Clock.Advance(TimeSpan.FromSeconds(5)); + factory.Clock.Advance(TimeSpan.FromSeconds(5)); var duringLockout = await TryLoginAsync( client, @@ -1647,7 +1655,7 @@ public async Task Login_FailureDuringLockout_ShouldNotExtendLockout_WhenExtendLo duringLockout.LockoutUntilUtc .Should().Be(originalLockoutUntil); - _factory.Clock.Set( + factory.Clock.Set( originalLockoutUntil.AddMilliseconds(1)); var login = await LoginAsync( @@ -1667,14 +1675,18 @@ public async Task Login_FailureDuringLockout_ShouldNotExtendLockout_WhenExtendLo [Fact] public async Task ConcurrentLoginFailures_ShouldNotLoseFailureAttempts() { - _factory.Clock.Reset(); + using var factory = CreateDetailedFailureFactory(); + factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var user = await factory.CreateLoginUserAsync( + ct: TestContext.Current.CancellationToken); using var client1 = CreateClient( + factory, $"concurrent-failure-1-{Guid.NewGuid():N}"); using var client2 = CreateClient( + factory, $"concurrent-failure-2-{Guid.NewGuid():N}"); var task1 = TryLoginAsync( @@ -1692,6 +1704,7 @@ public async Task ConcurrentLoginFailures_ShouldNotLoseFailureAttempts() results.Should().OnlyContain(x => !x.IsSuccess); using var verificationClient = CreateClient( + factory, $"concurrent-failure-verification-{Guid.NewGuid():N}"); var verification = await TryLoginAsync( @@ -1838,10 +1851,20 @@ public async Task ConcurrentLogin_WithSamePreviewReceipt_ShouldNotAllowReceiptTo "another-wrong-password"); failureState.IsSuccess.Should().BeFalse(); - failureState.Reason.Should().Be(AuthFailureReason.LockedOut); + failureState.Reason.Should().Be(AuthFailureReason.InvalidCredentials); + failureState.RemainingAttempts.Should().BeNull(); + failureState.LockoutUntilUtc.Should().BeNull(); } + private static AuthServerFactory CreateDetailedFailureFactory() + { + return AuthServerFactory.Create(options => + { + options.Login.IncludeFailureDetails = true; + }); + } + private HttpClient CreateClient(string deviceId = "test-device-1234567890123456") { var client = _factory.CreateClient( @@ -1862,6 +1885,21 @@ private HttpClient CreateClient(string deviceId = "test-device-1234567890123456" return client; } + private static HttpClient CreateClient(AuthServerFactory factory, string deviceId) + { + var client = factory.CreateClient( + new WebApplicationFactoryClientOptions + { + AllowAutoRedirect = false, + HandleCookies = false + }); + + client.DefaultRequestHeaders.Add("Origin", "https://localhost:6130"); + client.DefaultRequestHeaders.Add("X-UDID", deviceId); + + return client; + } + private static Task LoginAsync(HttpClient client, string identifier, string secret) { return CsrfTestClient.PostJsonAsync(client, diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/TryLoginTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/TryLoginTests.cs index 56b021fe..c76dfcce 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/TryLoginTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/TryLoginTests.cs @@ -1,36 +1,34 @@ -using CodeBeam.UltimateAuth.Core.Contracts; +using AwesomeAssertions; +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Server.Flows; using CodeBeam.UltimateAuth.Tests.Unit.Helpers; -using AwesomeAssertions; using Microsoft.Extensions.DependencyInjection; -using System; -using System.Collections.Generic; -using System.Text; -namespace CodeBeam.UltimateAuth.Tests.Unit.Server +namespace CodeBeam.UltimateAuth.Tests.Unit.Server; + +public class TryLoginTests { - public class TryLoginTests + [Fact] + public async Task TryLogin_Should_Return_Preview_On_Failure() { - [Fact] - public async Task TryLogin_Should_Return_Preview_On_Failure() - { - var runtime = new TestAuthRuntime(); + var runtime = new TestAuthRuntime(); - using var scope = runtime.Services.CreateScope(); + using var scope = runtime.Services.CreateScope(); - var flow = await runtime.CreateLoginFlowAsync(); - var orchestrator = scope.ServiceProvider.GetRequiredService(); + var flow = await runtime.CreateLoginFlowAsync(); + var orchestrator = scope.ServiceProvider.GetRequiredService(); - var result = await orchestrator.LoginAsync(flow, new LoginRequest - { - Identifier = "user", - Secret = "wrong" - }, ct: TestContext.Current.CancellationToken); - - result.Should().NotBeNull(); - result.IsSuccess.Should().BeFalse(); + var result = await orchestrator.LoginAsync(flow, new LoginRequest + { + Identifier = "user", + Secret = "wrong" + }, ct: TestContext.Current.CancellationToken); - result.RemainingAttempts.Should().NotBeNull(); - } + result.Should().NotBeNull(); + result.IsSuccess.Should().BeFalse(); + result.FailureReason.Should().Be(AuthFailureReason.InvalidCredentials); + result.RemainingAttempts.Should().BeNull(); + result.LockoutUntilUtc.Should().BeNull(); } } From ec86494ebc3ad944950ec53c36a37303e7b4494f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Can=20Karag=C3=B6z?= Date: Wed, 11 Nov 2026 20:13:32 +0300 Subject: [PATCH 3/5] Add Tests --- .../Infrastructure/TestDevice.cs | 12 + .../LoginAccountEnumerationTests.cs | 356 ++++++++++++++++++ 2 files changed, 368 insertions(+) create mode 100644 tests/CodeBeam.UltimateAuth.Tests.Integration/Infrastructure/TestDevice.cs create mode 100644 tests/CodeBeam.UltimateAuth.Tests.Integration/LoginAccountEnumerationTests.cs diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/Infrastructure/TestDevice.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/Infrastructure/TestDevice.cs new file mode 100644 index 00000000..b11b81bb --- /dev/null +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/Infrastructure/TestDevice.cs @@ -0,0 +1,12 @@ +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Domain; + +namespace CodeBeam.UltimateAuth.Tests.Integration; + +internal static class TestDevice +{ + public static DeviceContext Default() => DeviceContext.Create(DeviceId.Create("test-device-000-000-000-000-01"), null, null, null, null, null); + public static DeviceContext Alternative() => DeviceContext.Create(DeviceId.Create("test-device-000-000-000-000-alternative"), null, null, null, null, null); + public static DeviceInfo DefaultDeviceInfo() => new DeviceInfo() { DeviceId = DeviceId.Create("test-device-info-000-000-000-000-01") }; + +} diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/LoginAccountEnumerationTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/LoginAccountEnumerationTests.cs new file mode 100644 index 00000000..73810efc --- /dev/null +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/LoginAccountEnumerationTests.cs @@ -0,0 +1,356 @@ +using AwesomeAssertions; +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Tests.Integration.Infrastructure; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using CodeBeam.UltimateAuth.Server.Options; +using System.Net; +using System.Net.Http.Json; +using System.Text; +using System.Text.Json; + +namespace CodeBeam.UltimateAuth.Tests.Integration; + +public sealed class LoginAccountEnumerationTests +{ + [Fact] + public void IncludeFailureDetails_ShouldBeDisabledByDefault() + { + using var factory = new AuthServerFactory(); + + var options = factory.Services + .GetRequiredService>() + .Value; + + options.Login.IncludeFailureDetails.Should().BeFalse(); + } + + [Fact] + public async Task TryLogin_UnknownUserAndWrongPassword_ShouldHaveSamePublicFailure() + { + using var factory = new AuthServerFactory(); + + var user = await factory.CreateLoginUserAsync( + ct: TestContext.Current.CancellationToken); + + using var client = CreateClient(factory); + + var unknown = await TryLoginAsync( + client, + $"unknown-{Guid.NewGuid():N}", + "wrong-password"); + + var existing = await TryLoginAsync( + client, + user.Identifier, + "wrong-password"); + + AssertGenericFailure(unknown); + AssertGenericFailure(existing); + } + + [Fact] + public async Task TryLogin_NewAndExistingLockout_ShouldNotExposeAccountState() + { + using var factory = new AuthServerFactory(); + + var user = await factory.CreateLoginUserAsync( + ct: TestContext.Current.CancellationToken); + + using var client = CreateClient(factory); + + var first = await TryLoginAsync( + client, user.Identifier, "wrong-password-1"); + + var second = await TryLoginAsync( + client, user.Identifier, "wrong-password-2"); + + // The second failure triggers the configured lockout. + AssertGenericFailure(first); + AssertGenericFailure(second); + + // Even a correct password must not reveal active lockout. + var locked = await TryLoginAsync( + client, user.Identifier, user.Secret); + + AssertGenericFailure(locked); + } + + [Fact] + public async Task Login_FailureRedirect_ShouldNotExposeLockoutInformation() + { + using var factory = new AuthServerFactory(); + + var user = await factory.CreateLoginUserAsync( + ct: TestContext.Current.CancellationToken); + + using var client = CreateClient(factory); + + var first = await LoginAsync( + client, user.Identifier, "wrong-password-1"); + + var second = await LoginAsync( + client, user.Identifier, "wrong-password-2"); + + var locked = await LoginAsync( + client, user.Identifier, user.Secret); + + AssertGenericRedirectFailure(first); + AssertGenericRedirectFailure(second); + AssertGenericRedirectFailure(locked); + } + + [Fact] + public async Task TryLogin_ExplicitDetailedMode_ShouldPreserveFailureDetails() + { + using var factory = AuthServerFactory.Create(options => + { + options.Login.IncludeFailureDetails = true; + }); + + var user = await factory.CreateLoginUserAsync( + ct: TestContext.Current.CancellationToken); + + using var client = CreateClient(factory); + + var first = await TryLoginAsync( + client, user.Identifier, "wrong-password-1"); + + first.IsSuccess.Should().BeFalse(); + first.Reason.Should().Be(AuthFailureReason.InvalidCredentials); + first.RemainingAttempts.Should().Be(1); + first.LockoutUntilUtc.Should().BeNull(); + + var second = await TryLoginAsync( + client, user.Identifier, "wrong-password-2"); + + second.IsSuccess.Should().BeFalse(); + second.Reason.Should().Be(AuthFailureReason.LockedOut); + second.RemainingAttempts.Should().Be(0); + second.LockoutUntilUtc.Should().NotBeNull(); + + var locked = await TryLoginAsync( + client, user.Identifier, user.Secret); + + locked.IsSuccess.Should().BeFalse(); + locked.Reason.Should().Be(AuthFailureReason.LockedOut); + locked.RemainingAttempts.Should().Be(0); + locked.LockoutUntilUtc.Should().Be(second.LockoutUntilUtc); + } + + [Fact] + public async Task PkcePreview_UnknownWrongPasswordAndLockedAccount_ShouldReturnGenericFailure() + { + using var factory = new AuthServerFactory(); + + var user = await factory.CreateLoginUserAsync( + ct: TestContext.Current.CancellationToken); + + using var client = CreateClient(factory); + + var unknown = await TryPkceLoginAsync( + client, + $"unknown-{Guid.NewGuid():N}", + "wrong-password"); + + AssertGenericPkceFailure(unknown); + + var first = await TryPkceLoginAsync( + client, + user.Identifier, + "wrong-password-1"); + + AssertGenericPkceFailure(first); + + // MaxFailedAttempts = 2 in the integration host. + var second = await TryPkceLoginAsync( + client, + user.Identifier, + "wrong-password-2"); + + AssertGenericPkceFailure(second); + + // Correct credentials must not expose an active lockout. + var locked = await TryPkceLoginAsync( + client, + user.Identifier, + user.Secret); + + AssertGenericPkceFailure(locked); + } + + private static void AssertGenericPkceFailure( + TryPkceLoginResult result) + { + result.IsSuccess.Should().BeFalse(); + result.Reason.Should().Be(AuthFailureReason.InvalidCredentials); + result.RemainingAttempts.Should().BeNull(); + result.LockoutUntilUtc.Should().BeNull(); + result.RequiresMfa.Should().BeFalse(); + result.RetryWithNewPkce.Should().BeFalse(); + } + + private static async Task TryPkceLoginAsync( + HttpClient client, + string identifier, + string secret) + { + // Each attempt uses its own valid PKCE authorization. + var verifier = WebEncoders.Base64UrlEncode( + System.Security.Cryptography.RandomNumberGenerator.GetBytes(32)); + + var challenge = WebEncoders.Base64UrlEncode( + System.Security.Cryptography.SHA256.HashData( + Encoding.ASCII.GetBytes(verifier))); + + using var authorizeResponse = await CsrfTestClient.PostJsonAsync( + client, + "/auth/pkce/authorize", + new + { + codeChallenge = challenge, + challengeMethod = "S256", + device = TestDevice.Default() + }); + + authorizeResponse.StatusCode.Should().Be(HttpStatusCode.OK); + + var authorization = await authorizeResponse.Content + .ReadFromJsonAsync( + cancellationToken: TestContext.Current.CancellationToken); + + authorization.Should().NotBeNull(); + authorization!.AuthorizationCode.Should().NotBeNullOrWhiteSpace(); + + using var response = await CsrfTestClient.PostJsonAsync( + client, + "/auth/pkce/try-complete", + new + { + authorization_code = authorization.AuthorizationCode, + code_verifier = verifier, + identifier, + secret + }); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + + var result = await response.Content + .ReadFromJsonAsync( + cancellationToken: TestContext.Current.CancellationToken); + + result.Should().NotBeNull(); + + return result!; + } + + + private static void AssertGenericFailure(TryLoginResult result) + { + result.IsSuccess.Should().BeFalse(); + result.Reason.Should().Be(AuthFailureReason.InvalidCredentials); + result.RemainingAttempts.Should().BeNull(); + result.LockoutUntilUtc.Should().BeNull(); + result.PreviewReceipt.Should().BeNullOrWhiteSpace(); + } + + private static void AssertGenericRedirectFailure( + HttpResponseMessage response) + { + response.StatusCode.Should().Be(HttpStatusCode.Found); + + response.Headers + .TryGetValues("Set-Cookie", out _) + .Should().BeFalse(); + + response.Headers.Location.Should().NotBeNull(); + + var location = response.Headers.Location!; + var query = QueryHelpers.ParseQuery(location.Query); + + query.ContainsKey("uauth").Should().BeTrue(); + + var encoded = query["uauth"].ToString(); + + var json = Encoding.UTF8.GetString( + WebEncoders.Base64UrlDecode(encoded)); + + using var document = JsonDocument.Parse(json); + + var root = document.RootElement; + + root.GetProperty("Status") + .GetString() + .Should().Be("failed"); + + root.GetProperty("Reason") + .GetInt32() + .Should().Be((int)AuthFailureReason.InvalidCredentials); + + root.TryGetProperty("RemainingAttempts", out _) + .Should().BeFalse(); + + root.TryGetProperty("LockoutUntil", out _) + .Should().BeFalse(); + } + + private static HttpClient CreateClient(AuthServerFactory factory) + { + var client = factory.CreateClient( + new WebApplicationFactoryClientOptions + { + AllowAutoRedirect = false, + HandleCookies = false + }); + + client.DefaultRequestHeaders.Add( + "Origin", "https://localhost:6130"); + + client.DefaultRequestHeaders.Add( + "X-UDID", $"enumeration-{Guid.NewGuid():N}"); + + return client; + } + + private static async Task TryLoginAsync( + HttpClient client, + string identifier, + string secret) + { + using var response = await CsrfTestClient.PostJsonAsync( + client, + "/auth/try-login", + new LoginRequest + { + Identifier = identifier, + Secret = secret + }); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + + var result = await response.Content + .ReadFromJsonAsync( + cancellationToken: TestContext.Current.CancellationToken); + + result.Should().NotBeNull(); + return result!; + } + + private static Task LoginAsync( + HttpClient client, + string identifier, + string secret) + { + return CsrfTestClient.PostJsonAsync( + client, + "/auth/login", + new LoginRequest + { + Identifier = identifier, + Secret = secret + }); + } +} From 38a4ab0c1efbe443051f7671f56b30a3954be1a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Can=20Karag=C3=B6z?= Date: Wed, 11 Nov 2026 22:10:30 +0300 Subject: [PATCH 4/5] Fix Authentication State Store Concurrency --- .../Data/UAuthAuthenticationModelBuilder.cs | 2 +- .../EfCoreAuthenticationSecurityStateStore.cs | 45 ++++++++++- .../AuthenticationSecurityPostgresTests.cs | 54 ++++++++++++++ ...timateAuth.Tests.Integration.EfCore.csproj | 2 + .../Infrastructure/PostgresTestDatabase.cs | 74 +++++++++++++++++++ 5 files changed, 174 insertions(+), 3 deletions(-) create mode 100644 tests/CodeBeam.UltimateAuth.Tests.Integration.EfCore/AuthenticationSecurityPostgresTests.cs create mode 100644 tests/CodeBeam.UltimateAuth.Tests.Integration.EfCore/Infrastructure/PostgresTestDatabase.cs diff --git a/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Data/UAuthAuthenticationModelBuilder.cs b/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Data/UAuthAuthenticationModelBuilder.cs index 4689db06..606fb6e4 100644 --- a/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Data/UAuthAuthenticationModelBuilder.cs +++ b/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Data/UAuthAuthenticationModelBuilder.cs @@ -65,7 +65,7 @@ private static void ConfigureAuthenticationSecurityState(ModelBuilder b) e.Property(x => x.ResetAttempts) .IsRequired(); - e.HasIndex(x => new { x.Tenant, x.UserKey, x.Scope, x.CredentialType }).IsUnique(); + e.HasIndex(x => new { x.Tenant, x.UserKey, x.Scope, x.CredentialType }).IsUnique().HasAnnotation("Npgsql:NullsDistinct", false); e.HasIndex(x => new { x.Tenant, x.UserKey }); e.HasIndex(x => new { x.Tenant, x.LockedUntil }); e.HasIndex(x => new { x.Tenant, x.ResetRequestedAt }); diff --git a/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Stores/EfCoreAuthenticationSecurityStateStore.cs b/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Stores/EfCoreAuthenticationSecurityStateStore.cs index 98f4ee94..fb9bbeee 100644 --- a/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Stores/EfCoreAuthenticationSecurityStateStore.cs +++ b/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Stores/EfCoreAuthenticationSecurityStateStore.cs @@ -4,6 +4,7 @@ using CodeBeam.UltimateAuth.Core.MultiTenancy; using CodeBeam.UltimateAuth.Core.Security; using Microsoft.EntityFrameworkCore; +using System.Data.Common; namespace CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore; @@ -57,7 +58,15 @@ public async Task AddAsync(AuthenticationSecurityState state, CancellationToken DbSet.Add(entity); - await _db.SaveChangesAsync(ct); + try + { + await _db.SaveChangesAsync(ct); + } + catch (DbUpdateException ex) when (IsUniqueViolation(ex)) + { + DetachEntries(ex); + throw new UAuthConflictException("security_state_already_exists"); + } } public async Task UpdateAsync(AuthenticationSecurityState state, long expectedVersion, CancellationToken ct = default) @@ -81,7 +90,16 @@ public async Task UpdateAsync(AuthenticationSecurityState state, long expectedVe AuthenticationSecurityStateMapper.UpdateProjection(state, entity); - await _db.SaveChangesAsync(ct); + try + { + await _db.SaveChangesAsync(ct); + } + catch (DbUpdateConcurrencyException ex) + { + DetachEntries(ex); + + throw new UAuthConflictException("security_state_version_conflict"); + } } public async Task DeleteAsync(UserKey userKey, AuthenticationSecurityScope scope, CredentialType? credentialType, CancellationToken ct = default) @@ -101,4 +119,27 @@ public async Task DeleteAsync(UserKey userKey, AuthenticationSecurityScope scope await _db.SaveChangesAsync(ct); } + + private static void DetachEntries(DbUpdateException exception) + { + foreach (var entry in exception.Entries) + { + entry.State = EntityState.Detached; + } + } + + private static bool IsUniqueViolation(DbUpdateException exception) + { + for (Exception? current = exception.InnerException; + current is not null; + current = current.InnerException) + { + if (current is DbException dbException && dbException.SqlState == "23505") + { + return true; + } + } + + return false; + } } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration.EfCore/AuthenticationSecurityPostgresTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration.EfCore/AuthenticationSecurityPostgresTests.cs new file mode 100644 index 00000000..b76c062d --- /dev/null +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration.EfCore/AuthenticationSecurityPostgresTests.cs @@ -0,0 +1,54 @@ +using CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using Npgsql; + +namespace CodeBeam.UltimateAuth.Tests.Integration.EfCore.Authentication; + +public sealed class AuthenticationSecurityPostgresTests(PostgresFixture postgres) : IClassFixture +{ + [Fact] + public async Task Account_state_must_be_unique_when_credential_type_is_null() + { + await using var database = await postgres.CreateDatabaseAsync(); + + var options = + new DbContextOptionsBuilder() + .UseNpgsql(database.ConnectionString) + .Options; + + await using (var db = new UAuthAuthenticationDbContext(options)) + { + await db.Database.EnsureCreatedAsync(TestContext.Current.CancellationToken); + } + + await using var connection = + new NpgsqlConnection(database.ConnectionString); + + await connection.OpenAsync(TestContext.Current.CancellationToken); + + const string insert = """ + INSERT INTO "UAuth_Authentication" + ("Id", "Tenant", "UserKey", "Scope", + "CredentialType", "FailedAttempts", + "RequiresReauthentication", "ResetAttempts", + "SecurityVersion") + VALUES + (@id, 'default', 'test-user', 0, + NULL, 0, FALSE, 0, 0); + """; + + async Task InsertAsync() + { + await using var command = new NpgsqlCommand(insert, connection); + command.Parameters.AddWithValue("id", Guid.NewGuid()); + await command.ExecuteNonQueryAsync(); + } + + await InsertAsync(); + + var exception = await Assert.ThrowsAsync( + InsertAsync); + + Assert.Equal(PostgresErrorCodes.UniqueViolation, exception.SqlState); + } +} diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration.EfCore/CodeBeam.UltimateAuth.Tests.Integration.EfCore.csproj b/tests/CodeBeam.UltimateAuth.Tests.Integration.EfCore/CodeBeam.UltimateAuth.Tests.Integration.EfCore.csproj index dc1e7447..b30b0606 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration.EfCore/CodeBeam.UltimateAuth.Tests.Integration.EfCore.csproj +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration.EfCore/CodeBeam.UltimateAuth.Tests.Integration.EfCore.csproj @@ -11,6 +11,8 @@ + + diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration.EfCore/Infrastructure/PostgresTestDatabase.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration.EfCore/Infrastructure/PostgresTestDatabase.cs new file mode 100644 index 00000000..abfa7e34 --- /dev/null +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration.EfCore/Infrastructure/PostgresTestDatabase.cs @@ -0,0 +1,74 @@ +using Npgsql; +using Testcontainers.PostgreSql; + +namespace CodeBeam.UltimateAuth.Tests.Integration.EfCore; + +public sealed class PostgresFixture : IAsyncLifetime +{ + private PostgreSqlContainer? _container; + + public string ConnectionString { get; private set; } = ""; + + public async ValueTask InitializeAsync() + { + ConnectionString = + Environment.GetEnvironmentVariable("UAUTH_TEST_POSTGRES") ?? ""; + + if (!string.IsNullOrWhiteSpace(ConnectionString)) + return; + + _container = new PostgreSqlBuilder("postgres:17-alpine") + .Build(); + + await _container.StartAsync(); + + ConnectionString = _container.GetConnectionString(); + } + + public async ValueTask DisposeAsync() + { + if (_container is not null) + await _container.DisposeAsync(); + } + + public async Task CreateDatabaseAsync() + { + var name = "uauth_test_" + Guid.NewGuid().ToString("N"); + + await using var connection = new NpgsqlConnection(ConnectionString); + await connection.OpenAsync(); + + await using var command = + new NpgsqlCommand($"CREATE DATABASE {name}", connection); + + await command.ExecuteNonQueryAsync(); + + var builder = new NpgsqlConnectionStringBuilder(ConnectionString) + { + Database = name, + Pooling = false + }; + + return new PostgresTestDatabase( + builder.ConnectionString, + ConnectionString, + name); + } +} + +public sealed class PostgresTestDatabase(string connectionString, string adminConnectionString, string name) : IAsyncDisposable +{ + public string ConnectionString => connectionString; + + public async ValueTask DisposeAsync() + { + await using var connection = + new NpgsqlConnection(adminConnectionString); + + await connection.OpenAsync(); + + await using var command = new NpgsqlCommand($"DROP DATABASE {name} WITH (FORCE)", connection); + + await command.ExecuteNonQueryAsync(); + } +} From 2b95629f0951567dc333bc9f2e479333c272e244 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Can=20Karag=C3=B6z?= Date: Wed, 11 Nov 2026 22:35:15 +0300 Subject: [PATCH 5/5] Add Migrations and Tests --- ...icationSecurityStateUniqueness.Designer.cs | 780 ++++++++++++++++++ ...ixAuthenticationSecurityStateUniqueness.cs | 39 + .../Migrations/UAuthDbContextModelSnapshot.cs | 4 +- ...icationSecurityStateUniqueness.Designer.cs | 766 +++++++++++++++++ ...ixAuthenticationSecurityStateUniqueness.cs | 22 + .../Migrations/UAuthDbContextModelSnapshot.cs | 5 +- .../uauthhub.db | Bin 4096 -> 339968 bytes .../uauthhub.db-shm | Bin 32768 -> 0 bytes .../uauthhub.db-wal | Bin 927032 -> 0 bytes ...icationSecurityStateUniqueness.Designer.cs | 766 +++++++++++++++++ ...ixAuthenticationSecurityStateUniqueness.cs | 22 + .../Migrations/UAuthDbContextModelSnapshot.cs | 5 +- .../uauth.db | Bin 4096 -> 339968 bytes .../uauth.db-shm | Bin 32768 -> 0 bytes .../uauth.db-wal | Bin 885832 -> 0 bytes .../EfCoreAuthenticationSecurityStateStore.cs | 12 +- .../AuthenticationSecurityPostgresTests.cs | 146 ++++ 17 files changed, 2559 insertions(+), 8 deletions(-) create mode 100644 apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Migrations/20261111192531_FixAuthenticationSecurityStateUniqueness.Designer.cs create mode 100644 apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Migrations/20261111192531_FixAuthenticationSecurityStateUniqueness.cs create mode 100644 samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/20261111192825_FixAuthenticationSecurityStateUniqueness.Designer.cs create mode 100644 samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/20261111192825_FixAuthenticationSecurityStateUniqueness.cs delete mode 100644 samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/uauthhub.db-shm delete mode 100644 samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/uauthhub.db-wal create mode 100644 samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20261111192647_FixAuthenticationSecurityStateUniqueness.Designer.cs create mode 100644 samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20261111192647_FixAuthenticationSecurityStateUniqueness.cs delete mode 100644 samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db-shm delete mode 100644 samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db-wal diff --git a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Migrations/20261111192531_FixAuthenticationSecurityStateUniqueness.Designer.cs b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Migrations/20261111192531_FixAuthenticationSecurityStateUniqueness.Designer.cs new file mode 100644 index 00000000..0708b188 --- /dev/null +++ b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Migrations/20261111192531_FixAuthenticationSecurityStateUniqueness.Designer.cs @@ -0,0 +1,780 @@ +// +using System; +using CodeBeam.UltimateAuth.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace CodeBeam.UltimateAuth.IdentityLab.Migrations +{ + [DbContext(typeof(UAuthDbContext))] + [Migration("20261111192531_FixAuthenticationSecurityStateUniqueness")] + partial class FixAuthenticationSecurityStateUniqueness + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.14") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore.AuthenticationSecurityStateProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CredentialType") + .HasColumnType("integer"); + + b.Property("FailedAttempts") + .HasColumnType("integer"); + + b.Property("LastFailedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LockedUntil") + .HasColumnType("timestamp with time zone"); + + b.Property("RequiresReauthentication") + .HasColumnType("boolean"); + + b.Property("ResetAttempts") + .HasColumnType("integer"); + + b.Property("ResetConsumedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ResetExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ResetRequestedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ResetTokenHash") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Scope") + .HasColumnType("integer"); + + b.Property("SecurityVersion") + .IsConcurrencyToken() + .HasColumnType("bigint"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "LockedUntil"); + + b.HasIndex("Tenant", "ResetRequestedAt"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "UserKey", "Scope"); + + b.HasIndex("Tenant", "UserKey", "Scope", "CredentialType") + .IsUnique(); + + NpgsqlIndexBuilderExtensions.AreNullsDistinct(b.HasIndex("Tenant", "UserKey", "Scope", "CredentialType"), false); + + b.ToTable("UAuth_Authentication", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.RolePermissionProjection", b => + { + b.Property("Tenant") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("Permission") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Tenant", "RoleId", "Permission"); + + b.HasIndex("Tenant", "Permission"); + + b.HasIndex("Tenant", "RoleId"); + + b.ToTable("UAuth_RolePermissions", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.RoleProjection", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "Id") + .IsUnique(); + + b.HasIndex("Tenant", "NormalizedName") + .IsUnique(); + + b.ToTable("UAuth_Roles", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.UserRoleProjection", b => + { + b.Property("Tenant") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("UserKey") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("AssignedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Tenant", "UserKey", "RoleId"); + + b.HasIndex("Tenant", "RoleId"); + + b.HasIndex("Tenant", "UserKey"); + + b.ToTable("UAuth_UserRoles", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore.PasswordCredentialProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastUsedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SecretHash") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("SecurityStamp") + .HasColumnType("uuid"); + + b.Property("Source") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "ExpiresAt"); + + b.HasIndex("Tenant", "Id") + .IsUnique(); + + b.HasIndex("Tenant", "RevokedAt"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "UserKey", "DeletedAt"); + + b.ToTable("UAuth_PasswordCredentials", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore.PasswordHistoryProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CredentialId") + .HasColumnType("uuid"); + + b.Property("ReplacedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SecretHash") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "CredentialId", "ReplacedAt", "Id"); + + b.ToTable("UAuth_PasswordHistory", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionChainProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AbsoluteExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ActiveSessionId") + .HasColumnType("text"); + + b.Property("ChainId") + .HasColumnType("uuid"); + + b.Property("ClaimsSnapshot") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Device") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("LastSeenAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RootId") + .HasColumnType("uuid"); + + b.Property("RotationCount") + .HasColumnType("integer"); + + b.Property("SecurityVersionAtCreation") + .HasColumnType("bigint"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("TouchCount") + .HasColumnType("integer"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "ChainId") + .IsUnique(); + + b.HasIndex("Tenant", "RootId"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "UserKey", "DeviceId"); + + b.ToTable("UAuth_SessionChains", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChainId") + .HasColumnType("uuid"); + + b.Property("Claims") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Device") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Metadata") + .IsRequired() + .HasColumnType("text"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SecurityVersionAtCreation") + .HasColumnType("bigint"); + + b.Property("SessionId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "ChainId"); + + b.HasIndex("Tenant", "ExpiresAt"); + + b.HasIndex("Tenant", "RevokedAt"); + + b.HasIndex("Tenant", "SessionId") + .IsUnique(); + + b.HasIndex("Tenant", "ChainId", "RevokedAt"); + + b.HasIndex("Tenant", "UserKey", "RevokedAt"); + + b.ToTable("UAuth_Sessions", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionRootProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RootId") + .HasMaxLength(128) + .HasColumnType("uuid"); + + b.Property("SecurityVersion") + .HasColumnType("bigint"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "RootId") + .IsUnique(); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "UserKey", "RevokedAt"); + + b.ToTable("UAuth_SessionRoots", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore.RefreshTokenProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChainId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReplacedByTokenHash") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SessionId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("TokenId") + .HasColumnType("uuid"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "ChainId"); + + b.HasIndex("Tenant", "ExpiresAt"); + + b.HasIndex("Tenant", "ReplacedByTokenHash"); + + b.HasIndex("Tenant", "SessionId"); + + b.HasIndex("Tenant", "TokenHash") + .IsUnique(); + + b.HasIndex("Tenant", "TokenId"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "ExpiresAt", "RevokedAt"); + + b.HasIndex("Tenant", "TokenHash", "RevokedAt"); + + b.ToTable("UAuth_RefreshTokens", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Users.EntityFrameworkCore.UserIdentifierProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("NormalizedValue") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("VerifiedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "NormalizedValue"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "Type", "NormalizedValue") + .IsUnique(); + + b.HasIndex("Tenant", "UserKey", "IsPrimary"); + + b.HasIndex("Tenant", "UserKey", "Type", "IsPrimary", "DeletedAt", "CreatedAt", "Id"); + + b.ToTable("UAuth_UserIdentifiers", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Users.EntityFrameworkCore.UserLifecycleProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SecurityVersion") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "UserKey") + .IsUnique(); + + b.HasIndex("Tenant", "DeletedAt", "CreatedAt", "Id"); + + b.HasIndex("Tenant", "Status", "DeletedAt", "CreatedAt", "Id"); + + b.ToTable("UAuth_UserLifecycles", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Users.EntityFrameworkCore.UserProfileProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Bio") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)"); + + b.Property("BirthDate") + .HasColumnType("date"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Culture") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("FirstName") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Gender") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Language") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("LastName") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Metadata") + .HasColumnType("text"); + + b.Property("ProfileKey") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("TimeZone") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "UserKey", "ProfileKey") + .IsUnique(); + + b.ToTable("UAuth_UserProfiles", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionChainProjection", b => + { + b.HasOne("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionRootProjection", null) + .WithMany() + .HasForeignKey("Tenant", "RootId") + .HasPrincipalKey("Tenant", "RootId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionProjection", b => + { + b.HasOne("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionChainProjection", null) + .WithMany() + .HasForeignKey("Tenant", "ChainId") + .HasPrincipalKey("Tenant", "ChainId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Migrations/20261111192531_FixAuthenticationSecurityStateUniqueness.cs b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Migrations/20261111192531_FixAuthenticationSecurityStateUniqueness.cs new file mode 100644 index 00000000..99fcee52 --- /dev/null +++ b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Migrations/20261111192531_FixAuthenticationSecurityStateUniqueness.cs @@ -0,0 +1,39 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CodeBeam.UltimateAuth.IdentityLab.Migrations +{ + /// + public partial class FixAuthenticationSecurityStateUniqueness : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_UAuth_Authentication_Tenant_UserKey_Scope_CredentialType", + table: "UAuth_Authentication"); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_Authentication_Tenant_UserKey_Scope_CredentialType", + table: "UAuth_Authentication", + columns: new[] { "Tenant", "UserKey", "Scope", "CredentialType" }, + unique: true) + .Annotation("Npgsql:NullsDistinct", false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_UAuth_Authentication_Tenant_UserKey_Scope_CredentialType", + table: "UAuth_Authentication"); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_Authentication_Tenant_UserKey_Scope_CredentialType", + table: "UAuth_Authentication", + columns: new[] { "Tenant", "UserKey", "Scope", "CredentialType" }, + unique: true); + } + } +} diff --git a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Migrations/UAuthDbContextModelSnapshot.cs b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Migrations/UAuthDbContextModelSnapshot.cs index 55d8efe3..2e3c58e4 100644 --- a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Migrations/UAuthDbContextModelSnapshot.cs +++ b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Migrations/UAuthDbContextModelSnapshot.cs @@ -17,7 +17,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "10.0.12") + .HasAnnotation("ProductVersion", "10.0.14") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -89,6 +89,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("Tenant", "UserKey", "Scope", "CredentialType") .IsUnique(); + NpgsqlIndexBuilderExtensions.AreNullsDistinct(b.HasIndex("Tenant", "UserKey", "Scope", "CredentialType"), false); + b.ToTable("UAuth_Authentication", (string)null); }); diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/20261111192825_FixAuthenticationSecurityStateUniqueness.Designer.cs b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/20261111192825_FixAuthenticationSecurityStateUniqueness.Designer.cs new file mode 100644 index 00000000..1ca04a07 --- /dev/null +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/20261111192825_FixAuthenticationSecurityStateUniqueness.Designer.cs @@ -0,0 +1,766 @@ +// +using System; +using CodeBeam.UltimateAuth.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore.Migrations +{ + [DbContext(typeof(UAuthDbContext))] + [Migration("20261111192825_FixAuthenticationSecurityStateUniqueness")] + partial class FixAuthenticationSecurityStateUniqueness + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.14"); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore.AuthenticationSecurityStateProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CredentialType") + .HasColumnType("INTEGER"); + + b.Property("FailedAttempts") + .HasColumnType("INTEGER"); + + b.Property("LastFailedAt") + .HasColumnType("TEXT"); + + b.Property("LockedUntil") + .HasColumnType("TEXT"); + + b.Property("RequiresReauthentication") + .HasColumnType("INTEGER"); + + b.Property("ResetAttempts") + .HasColumnType("INTEGER"); + + b.Property("ResetConsumedAt") + .HasColumnType("TEXT"); + + b.Property("ResetExpiresAt") + .HasColumnType("TEXT"); + + b.Property("ResetRequestedAt") + .HasColumnType("TEXT"); + + b.Property("ResetTokenHash") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Scope") + .HasColumnType("INTEGER"); + + b.Property("SecurityVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "LockedUntil"); + + b.HasIndex("Tenant", "ResetRequestedAt"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "UserKey", "Scope"); + + b.HasIndex("Tenant", "UserKey", "Scope", "CredentialType") + .IsUnique() + .HasAnnotation("Npgsql:NullsDistinct", false); + + b.ToTable("UAuth_Authentication", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.RolePermissionProjection", b => + { + b.Property("Tenant") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.Property("Permission") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Tenant", "RoleId", "Permission"); + + b.HasIndex("Tenant", "Permission"); + + b.HasIndex("Tenant", "RoleId"); + + b.ToTable("UAuth_RolePermissions", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.RoleProjection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "Id") + .IsUnique(); + + b.HasIndex("Tenant", "NormalizedName") + .IsUnique(); + + b.ToTable("UAuth_Roles", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.UserRoleProjection", b => + { + b.Property("Tenant") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UserKey") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.Property("AssignedAt") + .HasColumnType("TEXT"); + + b.HasKey("Tenant", "UserKey", "RoleId"); + + b.HasIndex("Tenant", "RoleId"); + + b.HasIndex("Tenant", "UserKey"); + + b.ToTable("UAuth_UserRoles", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore.PasswordCredentialProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("LastUsedAt") + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("SecretHash") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("Source") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "ExpiresAt"); + + b.HasIndex("Tenant", "Id") + .IsUnique(); + + b.HasIndex("Tenant", "RevokedAt"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "UserKey", "DeletedAt"); + + b.ToTable("UAuth_PasswordCredentials", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore.PasswordHistoryProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CredentialId") + .HasColumnType("TEXT"); + + b.Property("ReplacedAt") + .HasColumnType("TEXT"); + + b.Property("SecretHash") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "CredentialId", "ReplacedAt", "Id"); + + b.ToTable("UAuth_PasswordHistory", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionChainProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AbsoluteExpiresAt") + .HasColumnType("TEXT"); + + b.Property("ActiveSessionId") + .HasColumnType("TEXT"); + + b.Property("ChainId") + .HasColumnType("TEXT"); + + b.Property("ClaimsSnapshot") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Device") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("LastSeenAt") + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("RootId") + .HasColumnType("TEXT"); + + b.Property("RotationCount") + .HasColumnType("INTEGER"); + + b.Property("SecurityVersionAtCreation") + .HasColumnType("INTEGER"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("TouchCount") + .HasColumnType("INTEGER"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "ChainId") + .IsUnique(); + + b.HasIndex("Tenant", "RootId"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "UserKey", "DeviceId"); + + b.ToTable("UAuth_SessionChains", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChainId") + .HasColumnType("TEXT"); + + b.Property("Claims") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Device") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("Metadata") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("SecurityVersionAtCreation") + .HasColumnType("INTEGER"); + + b.Property("SessionId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "ChainId"); + + b.HasIndex("Tenant", "ExpiresAt"); + + b.HasIndex("Tenant", "RevokedAt"); + + b.HasIndex("Tenant", "SessionId") + .IsUnique(); + + b.HasIndex("Tenant", "ChainId", "RevokedAt"); + + b.HasIndex("Tenant", "UserKey", "RevokedAt"); + + b.ToTable("UAuth_Sessions", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionRootProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("RootId") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("SecurityVersion") + .HasColumnType("INTEGER"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "RootId") + .IsUnique(); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "UserKey", "RevokedAt"); + + b.ToTable("UAuth_SessionRoots", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore.RefreshTokenProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChainId") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("ReplacedByTokenHash") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("SessionId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("TokenId") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "ChainId"); + + b.HasIndex("Tenant", "ExpiresAt"); + + b.HasIndex("Tenant", "ReplacedByTokenHash"); + + b.HasIndex("Tenant", "SessionId"); + + b.HasIndex("Tenant", "TokenHash") + .IsUnique(); + + b.HasIndex("Tenant", "TokenId"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "ExpiresAt", "RevokedAt"); + + b.HasIndex("Tenant", "TokenHash", "RevokedAt"); + + b.ToTable("UAuth_RefreshTokens", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Users.EntityFrameworkCore.UserIdentifierProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("IsPrimary") + .HasColumnType("INTEGER"); + + b.Property("NormalizedValue") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("VerifiedAt") + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "NormalizedValue"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "Type", "NormalizedValue") + .IsUnique(); + + b.HasIndex("Tenant", "UserKey", "IsPrimary"); + + b.HasIndex("Tenant", "UserKey", "Type", "IsPrimary", "DeletedAt", "CreatedAt", "Id"); + + b.ToTable("UAuth_UserIdentifiers", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Users.EntityFrameworkCore.UserLifecycleProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("SecurityVersion") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "UserKey") + .IsUnique(); + + b.HasIndex("Tenant", "DeletedAt", "CreatedAt", "Id"); + + b.HasIndex("Tenant", "Status", "DeletedAt", "CreatedAt", "Id"); + + b.ToTable("UAuth_UserLifecycles", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Users.EntityFrameworkCore.UserProfileProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Bio") + .HasMaxLength(4096) + .HasColumnType("TEXT"); + + b.Property("BirthDate") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Culture") + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("FirstName") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Gender") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Language") + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("LastName") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Metadata") + .HasColumnType("TEXT"); + + b.Property("ProfileKey") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("TimeZone") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "UserKey", "ProfileKey") + .IsUnique(); + + b.ToTable("UAuth_UserProfiles", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionChainProjection", b => + { + b.HasOne("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionRootProjection", null) + .WithMany() + .HasForeignKey("Tenant", "RootId") + .HasPrincipalKey("Tenant", "RootId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionProjection", b => + { + b.HasOne("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionChainProjection", null) + .WithMany() + .HasForeignKey("Tenant", "ChainId") + .HasPrincipalKey("Tenant", "ChainId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/20261111192825_FixAuthenticationSecurityStateUniqueness.cs b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/20261111192825_FixAuthenticationSecurityStateUniqueness.cs new file mode 100644 index 00000000..b84b884e --- /dev/null +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/20261111192825_FixAuthenticationSecurityStateUniqueness.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore.Migrations +{ + /// + public partial class FixAuthenticationSecurityStateUniqueness : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/UAuthDbContextModelSnapshot.cs b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/UAuthDbContextModelSnapshot.cs index ceee4996..281591dd 100644 --- a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/UAuthDbContextModelSnapshot.cs +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/UAuthDbContextModelSnapshot.cs @@ -15,7 +15,7 @@ partial class UAuthDbContextModelSnapshot : ModelSnapshot protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "10.0.12"); + modelBuilder.HasAnnotation("ProductVersion", "10.0.14"); modelBuilder.Entity("CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore.AuthenticationSecurityStateProjection", b => { @@ -82,7 +82,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("Tenant", "UserKey", "Scope"); b.HasIndex("Tenant", "UserKey", "Scope", "CredentialType") - .IsUnique(); + .IsUnique() + .HasAnnotation("Npgsql:NullsDistinct", false); b.ToTable("UAuth_Authentication", (string)null); }); diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/uauthhub.db b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/uauthhub.db index c9cf4b4e788deb2a37a4a5eef5db1c380729a4e1..6bcf8d7a2b24af737be90fad112f266e7b9a0947 100644 GIT binary patch literal 339968 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 SfB*y_009U<00Izj2>gFh9Wycj delta 28 icmZp8Akv^PL7J73fq{W>vY>!Hkfk^E^tKNybNB&gi3g(q diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/uauthhub.db-shm b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/uauthhub.db-shm deleted file mode 100644 index 82b7d9ddd563f8b036f4aa20d54925f29f69cd02..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32768 zcmeI5=W`cT6owyAP-zK0p$3pHy#%SDhZaS#7wiqe-mqZrT>%jbHY}*1U>8ME1bY_* z6#NVP>L-0S!#WvK-2R549V7c)eKNZw!KGMzW6OKZ~EliJkJ?w-B3_bf8g{jj@t zw5NOj-8bpGO5anupVIH?en}ly_dVUO$(||^Rrk}L?j?IV-NRM)=HBd^^lv1~URA7L zZomz=0XN_V+<+T!18%?#xB)ld2Hb!fa070@4Y&a};0D})8*l?|zzw(oH{b@`fE#cF zZomz=0XN_V+<+T!18%?#xB)ld2Hb!fa070@4Y&a};0D})8*l?|zzw*83~~FCvau0=%l(PCi7EuCHwS#U7fOW z)ILq9sfQ0_drdv+fBu~IoXAyC%vjFPrhV3{JVVH|g^b~Y_$^~)-pe}h60h=l1RADr z3THEqYq^=b4En8Kd3u&XaB$>ceh*%g&jlS>!Ae#~F#J?b{az!ULMl$zwPUk!> z;(Bi5UW1++aX*UFA_c{+SiE?0DBQzUcG;0)$)F*k5K_ZjrumoAU8a67|(<+ z%H@hKyv>JEOkTmUTxQs56MivF4~b6BhW0Cqf#lTZIIiSeZp*8HPuOhPK<4b>Jj)B2 ztMgpgm3LSdg@D63j>`=@E#wKq^pNNgJjaV63vyfAjd%Gdis@%^JXaWY+LR{^(?g<1 z@_9&kj%&N~9v??B{}G(Te8Wzgv5e)0`PrjK@kRFHTvztsebz@|;7DF(6>H3TZO&6X zZ0%+i<|HtV$&Cvzs}b2T?|hgla{g2(brmNKu=f7)EgW9}h?f!56C zEaq|zH*sf%db^Twd}|jTRdOE|@px1nTQyr`6&##9p6`O!&Q|*T_1yYPStXbBR@WQ0_#22&%8?;(q|HBeNU; diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/uauthhub.db-wal b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/uauthhub.db-wal deleted file mode 100644 index f799821e7da6764016bb271d7c60adffe67f7585..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 927032 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 diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20261111192647_FixAuthenticationSecurityStateUniqueness.Designer.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20261111192647_FixAuthenticationSecurityStateUniqueness.Designer.cs new file mode 100644 index 00000000..3a5227a0 --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20261111192647_FixAuthenticationSecurityStateUniqueness.Designer.cs @@ -0,0 +1,766 @@ +// +using System; +using CodeBeam.UltimateAuth.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.Migrations +{ + [DbContext(typeof(UAuthDbContext))] + [Migration("20261111192647_FixAuthenticationSecurityStateUniqueness")] + partial class FixAuthenticationSecurityStateUniqueness + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.14"); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore.AuthenticationSecurityStateProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CredentialType") + .HasColumnType("INTEGER"); + + b.Property("FailedAttempts") + .HasColumnType("INTEGER"); + + b.Property("LastFailedAt") + .HasColumnType("TEXT"); + + b.Property("LockedUntil") + .HasColumnType("TEXT"); + + b.Property("RequiresReauthentication") + .HasColumnType("INTEGER"); + + b.Property("ResetAttempts") + .HasColumnType("INTEGER"); + + b.Property("ResetConsumedAt") + .HasColumnType("TEXT"); + + b.Property("ResetExpiresAt") + .HasColumnType("TEXT"); + + b.Property("ResetRequestedAt") + .HasColumnType("TEXT"); + + b.Property("ResetTokenHash") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Scope") + .HasColumnType("INTEGER"); + + b.Property("SecurityVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "LockedUntil"); + + b.HasIndex("Tenant", "ResetRequestedAt"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "UserKey", "Scope"); + + b.HasIndex("Tenant", "UserKey", "Scope", "CredentialType") + .IsUnique() + .HasAnnotation("Npgsql:NullsDistinct", false); + + b.ToTable("UAuth_Authentication", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.RolePermissionProjection", b => + { + b.Property("Tenant") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.Property("Permission") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Tenant", "RoleId", "Permission"); + + b.HasIndex("Tenant", "Permission"); + + b.HasIndex("Tenant", "RoleId"); + + b.ToTable("UAuth_RolePermissions", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.RoleProjection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "Id") + .IsUnique(); + + b.HasIndex("Tenant", "NormalizedName") + .IsUnique(); + + b.ToTable("UAuth_Roles", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.UserRoleProjection", b => + { + b.Property("Tenant") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UserKey") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.Property("AssignedAt") + .HasColumnType("TEXT"); + + b.HasKey("Tenant", "UserKey", "RoleId"); + + b.HasIndex("Tenant", "RoleId"); + + b.HasIndex("Tenant", "UserKey"); + + b.ToTable("UAuth_UserRoles", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore.PasswordCredentialProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("LastUsedAt") + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("SecretHash") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("Source") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "ExpiresAt"); + + b.HasIndex("Tenant", "Id") + .IsUnique(); + + b.HasIndex("Tenant", "RevokedAt"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "UserKey", "DeletedAt"); + + b.ToTable("UAuth_PasswordCredentials", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore.PasswordHistoryProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CredentialId") + .HasColumnType("TEXT"); + + b.Property("ReplacedAt") + .HasColumnType("TEXT"); + + b.Property("SecretHash") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "CredentialId", "ReplacedAt", "Id"); + + b.ToTable("UAuth_PasswordHistory", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionChainProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AbsoluteExpiresAt") + .HasColumnType("TEXT"); + + b.Property("ActiveSessionId") + .HasColumnType("TEXT"); + + b.Property("ChainId") + .HasColumnType("TEXT"); + + b.Property("ClaimsSnapshot") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Device") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("LastSeenAt") + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("RootId") + .HasColumnType("TEXT"); + + b.Property("RotationCount") + .HasColumnType("INTEGER"); + + b.Property("SecurityVersionAtCreation") + .HasColumnType("INTEGER"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("TouchCount") + .HasColumnType("INTEGER"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "ChainId") + .IsUnique(); + + b.HasIndex("Tenant", "RootId"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "UserKey", "DeviceId"); + + b.ToTable("UAuth_SessionChains", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChainId") + .HasColumnType("TEXT"); + + b.Property("Claims") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Device") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("Metadata") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("SecurityVersionAtCreation") + .HasColumnType("INTEGER"); + + b.Property("SessionId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "ChainId"); + + b.HasIndex("Tenant", "ExpiresAt"); + + b.HasIndex("Tenant", "RevokedAt"); + + b.HasIndex("Tenant", "SessionId") + .IsUnique(); + + b.HasIndex("Tenant", "ChainId", "RevokedAt"); + + b.HasIndex("Tenant", "UserKey", "RevokedAt"); + + b.ToTable("UAuth_Sessions", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionRootProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("RootId") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("SecurityVersion") + .HasColumnType("INTEGER"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "RootId") + .IsUnique(); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "UserKey", "RevokedAt"); + + b.ToTable("UAuth_SessionRoots", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore.RefreshTokenProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChainId") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("ReplacedByTokenHash") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("SessionId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("TokenId") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "ChainId"); + + b.HasIndex("Tenant", "ExpiresAt"); + + b.HasIndex("Tenant", "ReplacedByTokenHash"); + + b.HasIndex("Tenant", "SessionId"); + + b.HasIndex("Tenant", "TokenHash") + .IsUnique(); + + b.HasIndex("Tenant", "TokenId"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "ExpiresAt", "RevokedAt"); + + b.HasIndex("Tenant", "TokenHash", "RevokedAt"); + + b.ToTable("UAuth_RefreshTokens", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Users.EntityFrameworkCore.UserIdentifierProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("IsPrimary") + .HasColumnType("INTEGER"); + + b.Property("NormalizedValue") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("VerifiedAt") + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "NormalizedValue"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "Type", "NormalizedValue") + .IsUnique(); + + b.HasIndex("Tenant", "UserKey", "IsPrimary"); + + b.HasIndex("Tenant", "UserKey", "Type", "IsPrimary", "DeletedAt", "CreatedAt", "Id"); + + b.ToTable("UAuth_UserIdentifiers", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Users.EntityFrameworkCore.UserLifecycleProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("SecurityVersion") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "UserKey") + .IsUnique(); + + b.HasIndex("Tenant", "DeletedAt", "CreatedAt", "Id"); + + b.HasIndex("Tenant", "Status", "DeletedAt", "CreatedAt", "Id"); + + b.ToTable("UAuth_UserLifecycles", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Users.EntityFrameworkCore.UserProfileProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Bio") + .HasMaxLength(4096) + .HasColumnType("TEXT"); + + b.Property("BirthDate") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Culture") + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("FirstName") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Gender") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Language") + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("LastName") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Metadata") + .HasColumnType("TEXT"); + + b.Property("ProfileKey") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("TimeZone") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "UserKey", "ProfileKey") + .IsUnique(); + + b.ToTable("UAuth_UserProfiles", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionChainProjection", b => + { + b.HasOne("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionRootProjection", null) + .WithMany() + .HasForeignKey("Tenant", "RootId") + .HasPrincipalKey("Tenant", "RootId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionProjection", b => + { + b.HasOne("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionChainProjection", null) + .WithMany() + .HasForeignKey("Tenant", "ChainId") + .HasPrincipalKey("Tenant", "ChainId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20261111192647_FixAuthenticationSecurityStateUniqueness.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20261111192647_FixAuthenticationSecurityStateUniqueness.cs new file mode 100644 index 00000000..483767d9 --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20261111192647_FixAuthenticationSecurityStateUniqueness.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.Migrations +{ + /// + public partial class FixAuthenticationSecurityStateUniqueness : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} 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 8b10c53a..e8eee716 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 @@ -15,7 +15,7 @@ partial class UAuthDbContextModelSnapshot : ModelSnapshot protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "10.0.12"); + modelBuilder.HasAnnotation("ProductVersion", "10.0.14"); modelBuilder.Entity("CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore.AuthenticationSecurityStateProjection", b => { @@ -82,7 +82,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("Tenant", "UserKey", "Scope"); b.HasIndex("Tenant", "UserKey", "Scope", "CredentialType") - .IsUnique(); + .IsUnique() + .HasAnnotation("Npgsql:NullsDistinct", false); b.ToTable("UAuth_Authentication", (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 c9cf4b4e788deb2a37a4a5eef5db1c380729a4e1..2be20ba23fa7559f5dcb3c9addafca528c38a340 100644 GIT binary patch literal 339968 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 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 38ab800e0f4e6a3d1180e764cbbbe8e2ac02de41..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32768 zcmeI5_m5Ro6onTcAa#a5!Y~D-3s?auLm3!4f*?&01Qitl#V)<*C><1RASy*grGruw z6hssoMM;Ri$Ug$>Ot@xZ2%JMq_MF_clP~YRdF#Ht&v(zSb8gnfws6&ulmjx)^YuP5 zrTF-m?BSy;pUVBG=fND=8v`jP%*02xRIk-=mLare);OxEjiNhVpl%O-xV_S-l5okh=N zKV0n|UF*C5?wjOZCHE=WPs!(Gza);Y_c__GZcpWr();OJ-?=@V?BUXT^G5cKyWc7q zkO3Kx0U3}18IS=PkO3Kx0U3}18IS=PkO3Kx0U3}18IS=PkO3Kx0U3}18IS=PkO3Kx z0U3}18IS=PkO3Kx0U3}18IS=PkO3Kx0U3}18IS=PkO3L^e+{_*s8{(IXoOLC&L?P| zDZ)9^8OrgD33oz3*)MmNgv!T2HXg)FIPViR&h*8frW2IsDJu_{PTl_^Mh1A26C_s+p*gxYMkkhiI{@vn1jVwYA{QslGC#c z8V9#@CugaA4CLZ-?7$wMsBvZh9>(LCfw_1U%M50ziv9S;pl^6fclMad$3Px_^of;$ z%s@=S6L=c)@EVpI%u@|t;ah{g;Vs>1i7Foh`8eqlD+8H9n2aa!4CbR4D-7nTj#YTu zpl^7~j`%qIqEued1V2Tx?!kBjQ!x_@@H$o+%v=Mj@s2^?@RpshDg2^TUeOd^<46=M zAA-?%-f+^IhP}gDyR)HHJ_g*W&vBGHm)o*3aNM$eK=01jj86md>5VqS0UV8D`9m=V zFBndmi4P3BhqUg3E%+?tg4AZ4;~>6^V)er?7B3o3S_>NuyN9&yil0N`DbBXQA$%Xj z`tQehykaO&S@4<|z6hS?tY&9Hk+ zt=r%&ydTrRX0z7ELwFQVVHOtR4YMhH8u!HSJ~3tZq|LQhFAp9D8ejq*!!*psA}sO9 z+eLce54+}3b#9Aw*bvpkR>Qg{{Kip*I335J=2NyapCFb2+_Q<}sNOge$Drm@wl|+3 SmH`@KB%Y08*giOGI_G7^e5DWo diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db-wal b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db-wal deleted file mode 100644 index c3298258e9f80a744d9b5c7254ae4812f7fc1d22..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 885832 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 X;ZQ6d4aOsRvSexT*lZq~%l`iXxHUR9 diff --git a/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Stores/EfCoreAuthenticationSecurityStateStore.cs b/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Stores/EfCoreAuthenticationSecurityStateStore.cs index fb9bbeee..b60d9529 100644 --- a/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Stores/EfCoreAuthenticationSecurityStateStore.cs +++ b/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Stores/EfCoreAuthenticationSecurityStateStore.cs @@ -62,7 +62,7 @@ public async Task AddAsync(AuthenticationSecurityState state, CancellationToken { await _db.SaveChangesAsync(ct); } - catch (DbUpdateException ex) when (IsUniqueViolation(ex)) + catch (DbUpdateException ex) when (IsSecurityStateUniqueViolation(ex)) { DetachEntries(ex); throw new UAuthConflictException("security_state_already_exists"); @@ -128,13 +128,19 @@ private static void DetachEntries(DbUpdateException exception) } } - private static bool IsUniqueViolation(DbUpdateException exception) + private static bool IsSecurityStateUniqueViolation(DbUpdateException exception) { + if (exception.Entries.Count != 1 || exception.Entries[0].Entity is not AuthenticationSecurityStateProjection) + { + return false; + } + for (Exception? current = exception.InnerException; current is not null; current = current.InnerException) { - if (current is DbException dbException && dbException.SqlState == "23505") + if (current is DbException dbException && + dbException.SqlState == "23505") { return true; } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration.EfCore/AuthenticationSecurityPostgresTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration.EfCore/AuthenticationSecurityPostgresTests.cs index b76c062d..2968fa97 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration.EfCore/AuthenticationSecurityPostgresTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration.EfCore/AuthenticationSecurityPostgresTests.cs @@ -1,5 +1,12 @@ using CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore; +using CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore.Extensions; +using CodeBeam.UltimateAuth.Core.Abstractions; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.Errors; +using CodeBeam.UltimateAuth.Core.MultiTenancy; +using CodeBeam.UltimateAuth.Core.Security; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; using Npgsql; namespace CodeBeam.UltimateAuth.Tests.Integration.EfCore.Authentication; @@ -51,4 +58,143 @@ async Task InsertAsync() Assert.Equal(PostgresErrorCodes.UniqueViolation, exception.SqlState); } + + [Fact] + public async Task Concurrent_account_creation_should_persist_one_state() + { + await using var database = await postgres.CreateDatabaseAsync(); + await using var provider = await CreateProviderAsync( + database.ConnectionString); + + var tenant = TenantKey.FromExternal("concurrency-test"); + var user = UserKey.FromString("concurrent-user"); + + await using var scope1 = provider.CreateAsyncScope(); + await using var scope2 = provider.CreateAsyncScope(); + + var store1 = CreateStore(scope1.ServiceProvider, tenant); + var store2 = CreateStore(scope2.ServiceProvider, tenant); + + var ct = TestContext.Current.CancellationToken; + + // Both requests observe the state before creation. + Assert.Null(await store1.GetAsync( + user, AuthenticationSecurityScope.Account, null, ct)); + + Assert.Null(await store2.GetAsync( + user, AuthenticationSecurityScope.Account, null, ct)); + + var state1 = AuthenticationSecurityState.CreateAccount(tenant, user); + var state2 = AuthenticationSecurityState.CreateAccount(tenant, user); + + var outcomes = await Task.WhenAll( + AttemptAddAsync(store1, state1, ct), + AttemptAddAsync(store2, state2, ct)); + + Assert.Equal(1, outcomes.Count(x => x)); + Assert.Equal(1, outcomes.Count(x => !x)); + + await using var verification = provider.CreateAsyncScope(); + var stored = await CreateStore( + verification.ServiceProvider, tenant) + .GetAsync(user, AuthenticationSecurityScope.Account, null, ct); + + Assert.NotNull(stored); + } + + [Fact] + public async Task Stale_security_version_should_be_rejected() + { + await using var database = await postgres.CreateDatabaseAsync(); + await using var provider = await CreateProviderAsync( + database.ConnectionString); + + var tenant = TenantKey.FromExternal("version-test"); + var user = UserKey.FromString("version-user"); + var ct = TestContext.Current.CancellationToken; + + await using var scope1 = provider.CreateAsyncScope(); + await using var scope2 = provider.CreateAsyncScope(); + + var store1 = CreateStore(scope1.ServiceProvider, tenant); + var store2 = CreateStore(scope2.ServiceProvider, tenant); + + await store1.AddAsync( + AuthenticationSecurityState.CreateAccount(tenant, user), ct); + + var original1 = await store1.GetAsync( + user, AuthenticationSecurityScope.Account, null, ct); + + var original2 = await store2.GetAsync( + user, AuthenticationSecurityScope.Account, null, ct); + + Assert.NotNull(original1); + Assert.NotNull(original2); + + var now = DateTimeOffset.UtcNow; + + var first = original1!.RegisterFailure( + now, 10, TimeSpan.FromMinutes(15), + TimeSpan.FromMinutes(15)); + + await store1.UpdateAsync( + first, original1.SecurityVersion, ct); + + var second = original2!.RegisterFailure( + now, 10, TimeSpan.FromMinutes(15), + TimeSpan.FromMinutes(15)); + + await Assert.ThrowsAsync( + () => store2.UpdateAsync( + second, original2.SecurityVersion, ct)); + + await using var verification = provider.CreateAsyncScope(); + + var persisted = await CreateStore( + verification.ServiceProvider, tenant) + .GetAsync(user, AuthenticationSecurityScope.Account, null, ct); + + Assert.NotNull(persisted); + Assert.Equal(1, persisted!.FailedAttempts); + Assert.Equal(first.SecurityVersion, persisted.SecurityVersion); + } + + private static async Task AttemptAddAsync( + IAuthenticationSecurityStateStore store, + AuthenticationSecurityState state, + CancellationToken ct) + { + try + { + await store.AddAsync(state, ct); + return true; + } + catch (UAuthConflictException) + { + return false; + } + } + + + private static async Task CreateProviderAsync(string connectionString) + { + var services = new ServiceCollection(); + + services.AddUltimateAuthAuthenticationEntityFrameworkCore(options => options.UseNpgsql(connectionString)); + + var provider = services.BuildServiceProvider(validateScopes: true); + + await using var scope = provider.CreateAsyncScope(); + + var db = scope.ServiceProvider.GetRequiredService(); + + await db.Database.EnsureCreatedAsync(TestContext.Current.CancellationToken); + + return provider; + } + + private static IAuthenticationSecurityStateStore CreateStore(IServiceProvider services, TenantKey tenant) + { + return services.GetRequiredService().Create(tenant); + } }