From 829a79921869d3c26528ea26415981d852922468 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Can=20Karag=C3=B6z?= <78308169+mckaragoz@users.noreply.github.com> Date: Wed, 4 Nov 2026 23:04:02 +0300 Subject: [PATCH 01/11] feat(docs): showcase Identity Lab and enhance landing page experience (#143) --- .../Program.cs | 3 +- .../Layout/MainLayout.razor | 20 +- .../Pages/Home.razor | 142 +++++++++++ .../Pages/Home.razor.css | 221 ++++++++++++++++++ .../Pages/Samples.razor | 6 +- .../wwwroot/app.css | 38 ++- 6 files changed, 414 insertions(+), 16 deletions(-) create mode 100644 docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/Pages/Home.razor.css diff --git a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Program.cs b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Program.cs index 73d5b374..2a34f7a9 100644 --- a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Program.cs +++ b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Program.cs @@ -142,6 +142,8 @@ var app = builder.Build(); +app.UseForwardedHeaders(); + activityEventBridge.Initialize(app.Services); // Configure the HTTP request pipeline. @@ -152,7 +154,6 @@ app.UseHsts(); } app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true); -app.UseForwardedHeaders(); app.UseHttpsRedirection(); app.UseUltimateAuthWithAspNetCore(); diff --git a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/Layout/MainLayout.razor b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/Layout/MainLayout.razor index 6301b33e..852c35a8 100644 --- a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/Layout/MainLayout.razor +++ b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/Layout/MainLayout.razor @@ -25,6 +25,10 @@
Docs Samples + + Identity Lab + Donate
@@ -68,6 +72,7 @@ Samples + Identity Lab Donate @@ -79,9 +84,7 @@ - + @@ -89,17 +92,14 @@ - - UltimateAuth - - - Open-source unified Auth for modern .NET. - + UltimateAuth + Open-source unified Auth for modern .NET. Docs Samples + Identity Lab GitHub Discord @@ -124,7 +124,7 @@ -
+
An unhandled error has occurred. diff --git a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/Pages/Home.razor b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/Pages/Home.razor index e00fb76c..65b41518 100644 --- a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/Pages/Home.razor +++ b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/Pages/Home.razor @@ -90,6 +90,148 @@ +
+ + + +
+ + +
+ +
+
+ +
+ +
+ +
+ + LIVE INTERACTIVE SANDBOX +
+ + + Meet Identity Lab. + Authentication, experienced. + + + + Go beyond documentation and code samples. + Explore UltimateAuth inside a real, + interactive Blazor application. + + + + Create your own sandbox, manage identities, + inspect sessions, experiment with credentials + and explore authorization — all powered by + UltimateAuth. + + +
+ + + + +
+ +
+ Identity Management +
+
+ Explore users, profiles and identifiers. +
+
+
+ + +
+ +
+ Session Lifecycle +
+
+ Discover device-aware sessions and + session lineage. +
+
+
+ + +
+ +
+ Credentials +
+
+ Experience credential management + and account security. +
+
+
+ + +
+ +
+ Authorization +
+
+ Explore roles, permissions and + policy-driven access. +
+
+
+ +
+ +
+ + + Launch Identity Lab + + + + Explore Documentation + + +
+ +
+ + + Identity Lab is a demonstration environment. + Use test data only. Sandbox data may be reset. + +
+ +
+
+ +
+
+
diff --git a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/Pages/Home.razor.css b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/Pages/Home.razor.css new file mode 100644 index 00000000..d556ad95 --- /dev/null +++ b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/Pages/Home.razor.css @@ -0,0 +1,221 @@ +.ua-lab-section { + position: relative; + padding: 96px 0; +} + +.ua-lab-showcase { + position: relative; + overflow: hidden; + isolation: isolate; + border-radius: 28px; + border: 1px solid rgba(130, 155, 255, 0.22); + background: radial-gradient( ellipse at 80% 0%, rgba(88, 91, 240, 0.22), transparent 55% ), linear-gradient( 145deg, #10182d 0%, #0b1120 55%, #111a30 100% ); + color: #f5f7ff; + box-shadow: 0 28px 90px rgba(5, 12, 30, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.08); +} + +.ua-lab-content { + position: relative; + z-index: 1; + padding: clamp(32px, 6vw, 80px); +} + +.ua-lab-ambient { + position: absolute; + z-index: 0; + pointer-events: none; + border-radius: 50%; + filter: blur(70px); +} + +.ua-lab-ambient-one { + width: 360px; + height: 360px; + top: -180px; + right: -100px; + background: rgba(110, 97, 255, 0.18); +} + +.ua-lab-ambient-two { + width: 280px; + height: 280px; + bottom: -160px; + left: -90px; + background: rgba(42, 170, 220, 0.12); +} + +.ua-lab-heading { + max-width: 820px; +} + +.ua-lab-eyebrow { + display: inline-flex; + align-items: center; + gap: 10px; + padding: 8px 14px; + border: 1px solid rgba(155, 174, 255, 0.25); + border-radius: 100px; + background: rgba(255, 255, 255, 0.045); + color: #aebfff; + font-size: 0.72rem; + font-weight: 700; + letter-spacing: 0.13em; + margin-bottom: 28px; +} + +.ua-lab-status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: #57e4ae; + box-shadow: 0 0 12px rgba(87, 228, 174, 0.6); +} + +.ua-lab-title { + font-weight: 800 !important; + letter-spacing: -0.045em !important; + line-height: 1.12 !important; + color: #fff !important; + margin-bottom: 28px !important; + overflow-wrap: break-word; + min-width: 0; +} + + .ua-lab-title span { + display: block; + background: linear-gradient( 90deg, #b5c3ff, #8ee5ff, #c5b6ff ); + background-clip: text; + -webkit-background-clip: text; + color: transparent; + } + +.ua-lab-description { + max-width: 690px; + color: #b6c2d9 !important; + line-height: 1.85 !important; + margin-bottom: 14px !important; +} + +.ua-lab-features { + margin-top: 44px; +} + +.ua-lab-feature { + height: 100%; + min-height: 185px; + padding: 24px 20px; + border-radius: 16px; + border: 1px solid rgba(175, 190, 255, 0.14); + background: rgba(255, 255, 255, 0.035); + transition: border-color 0.2s ease, background 0.2s ease, transform 0.2s ease; +} + + .ua-lab-feature:hover { + transform: translateY(-3px); + border-color: rgba(155, 180, 255, 0.38); + background: rgba(255, 255, 255, 0.065); + } + + .ua-lab-feature .mud-icon-root { + color: #a9baff; + margin-bottom: 18px; + } + +.ua-lab-feature-title { + font-size: 0.95rem; + font-weight: 700; + color: #f7f9ff; + margin-bottom: 10px; +} + +.ua-lab-feature-text { + color: #aab8d1; + font-size: 0.85rem; + line-height: 1.65; +} + +.ua-lab-actions { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 16px; + margin-top: 44px; +} + +.ua-lab-primary-button { + border-radius: 12px !important; + padding: 12px 26px !important; + font-weight: 700 !important; + box-shadow: 0 8px 28px rgba(100, 110, 255, 0.2); +} + +.ua-lab-secondary-button { + color: #dbe4ff !important; +} + +.ua-lab-footnote { + display: flex; + align-items: flex-start; + gap: 10px; + margin-top: 34px; + color: #91a0bd; + font-size: 0.78rem; + line-height: 1.6; +} + + .ua-lab-footnote .mud-icon-root { + flex-shrink: 0; + font-size: 17px; + } + +@media (max-width: 600px) { + .ua-lab-section { + padding: 52px 0; + } + + .ua-lab-showcase { + border-radius: 20px; + } + + .ua-lab-title { + font-size: clamp(1.75rem, 7vw, 2.5rem) !important; + line-height: 1.18 !important; + letter-spacing: -0.035em !important; + } + + .ua-lab-title span { + font-size: 0.9em; + overflow-wrap: anywhere; + } + + .ua-lab-features { + margin-top: 24px; + } + + .ua-lab-feature { + min-height: auto; + } + + .ua-lab-actions { + align-items: stretch; + flex-direction: column; + } + + .ua-lab-actions .mud-button-root { + width: 100%; + } + + .ua-lab-content { + padding: 32px 24px; + } +} + +@media (prefers-reduced-motion: reduce) { + .ua-lab-feature { + transition: none; + } + + .ua-lab-feature:hover { + transform: none; + } +} diff --git a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/Pages/Samples.razor b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/Pages/Samples.razor index dee6fad4..e69e4bc9 100644 --- a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/Pages/Samples.razor +++ b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/Pages/Samples.razor @@ -94,7 +94,7 @@ Follow the project combinations below to run them locally. - + Blazor Server @@ -108,7 +108,7 @@ - + Blazor Standalone WASM @@ -127,7 +127,7 @@ - + Blazor Interactive WASM diff --git a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm/wwwroot/app.css b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm/wwwroot/app.css index f03bac21..a699d2cd 100644 --- a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm/wwwroot/app.css +++ b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm/wwwroot/app.css @@ -183,20 +183,54 @@ h1:focus { transform: scale(1.15); } +/* Vision spotlight — Light theme */ .ua-global-overlay { position: fixed; inset: 0; z-index: 9998; pointer-events: none; opacity: 0; - transition: opacity 0.35s ease; - background: radial-gradient( circle at 50% 40%, transparent 90px, rgba(0, 0, 0, 0.99) 600px ); + transition: opacity 0.65s ease; + background: radial-gradient( ellipse at 50% 50%, rgba(12, 18, 38, 0.08) 0%, rgba(12, 18, 38, 0.32) 48%, rgba(12, 18, 38, 0.72) 100% ); } + /* Vision spotlight — Dark theme */ + .ua-global-overlay.ua-overlay-dark { + background: radial-gradient( ellipse at 50% 43%, rgba(120, 140, 255, 0.18) 0%, rgba(85, 95, 210, 0.13) 35%, transparent 72% ), radial-gradient( ellipse at 50% 50%, transparent 15%, rgba(5, 9, 25, 0.16) 65%, rgba(5, 9, 25, 0.42) 100% ); + } + + /* Activate overlay */ .ua-global-overlay.active { opacity: 1; } +/* Vision content stays above the overlay */ +.ua-vision { + position: relative; + z-index: 9999; +} + +/* Bulb glow */ +.ua-vision-icon-wrapper { + position: relative; + z-index: 1; + border-radius: 50%; + transition: transform 0.5s ease, filter 0.5s ease; +} + + .ua-vision-icon-wrapper:hover { + transform: scale(1.18); + filter: drop-shadow(0 0 14px rgba(125, 165, 255, 0.8)) drop-shadow(0 0 42px rgba(125, 165, 255, 0.35)); + } + +/* Accessibility */ +@media (prefers-reduced-motion: reduce) { + .ua-global-overlay, + .ua-vision-icon-wrapper { + transition: none; + } +} + .ua-vision { position: relative; z-index: 9999; From 0af981ad79177c72a77aa2740375b8c7dcb2ef45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Can=20Karag=C3=B6z?= <78308169+mckaragoz@users.noreply.github.com> Date: Fri, 6 Nov 2026 00:58:29 +0300 Subject: [PATCH 02/11] Add GitHub Actions workflow for JavaScript tests --- .github/workflows/javascript-tests.yml | 36 ++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 .github/workflows/javascript-tests.yml diff --git a/.github/workflows/javascript-tests.yml b/.github/workflows/javascript-tests.yml new file mode 100644 index 00000000..3646148e --- /dev/null +++ b/.github/workflows/javascript-tests.yml @@ -0,0 +1,36 @@ +name: JavaScript Client Tests + +on: + pull_request: + paths: + - 'src/client/CodeBeam.UltimateAuth.Client.Blazor/TScripts/**' + - 'tests/CodeBeam.UltimateAuth.Tests.Unit/javascript/**' + - '.github/workflows/javascript-tests.yml' + + push: + branches: + - dev + - master + paths: + - 'src/client/CodeBeam.UltimateAuth.Client.Blazor/TScripts/**' + - 'tests/CodeBeam.UltimateAuth.Tests.Unit/javascript/**' + - '.github/workflows/javascript-tests.yml' + + workflow_dispatch: + +jobs: + javascript-tests: + name: Node.js Client Tests + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Run JavaScript tests + run: node --test tests/CodeBeam.UltimateAuth.Tests.Unit/javascript/*.test.cjs From abad249bfe26e42a0fd279755e668d52fba1ddf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Can=20Karag=C3=B6z?= <78308169+mckaragoz@users.noreply.github.com> Date: Fri, 6 Nov 2026 01:03:28 +0300 Subject: [PATCH 03/11] Enhance JavaScript tests workflow with change detection Updated workflow to include permissions and detect changes step. --- .github/workflows/javascript-tests.yml | 36 ++++++++++++++++++-------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/.github/workflows/javascript-tests.yml b/.github/workflows/javascript-tests.yml index 3646148e..afe34b2b 100644 --- a/.github/workflows/javascript-tests.yml +++ b/.github/workflows/javascript-tests.yml @@ -2,25 +2,39 @@ name: JavaScript Client Tests on: pull_request: - paths: - - 'src/client/CodeBeam.UltimateAuth.Client.Blazor/TScripts/**' - - 'tests/CodeBeam.UltimateAuth.Tests.Unit/javascript/**' - - '.github/workflows/javascript-tests.yml' - push: branches: - dev - master - paths: - - 'src/client/CodeBeam.UltimateAuth.Client.Blazor/TScripts/**' - - 'tests/CodeBeam.UltimateAuth.Tests.Unit/javascript/**' - - '.github/workflows/javascript-tests.yml' - - workflow_dispatch: + +permissions: + contents: read jobs: + detect-changes: + name: Detect JavaScript Changes + runs-on: ubuntu-latest + outputs: + javascript: ${{ steps.filter.outputs.javascript }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Detect changed files + id: filter + uses: dorny/paths-filter@v3 + with: + filters: | + javascript: + - 'src/client/CodeBeam.UltimateAuth.Client.Blazor/TScripts/**' + - 'tests/CodeBeam.UltimateAuth.Tests.Unit/javascript/**' + - '.github/workflows/javascript-tests.yml' + javascript-tests: name: Node.js Client Tests + needs: detect-changes + if: needs.detect-changes.outputs.javascript == 'true' runs-on: ubuntu-latest steps: From 491f9b122878f10a4d62c5e3c159f3b5f9c9d62e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Can=20Karag=C3=B6z?= <78308169+mckaragoz@users.noreply.github.com> Date: Fri, 6 Nov 2026 09:17:10 +0300 Subject: [PATCH 04/11] Merge commit from fork * advisory-fix-1 * Fix Reviewed Issues * Fix Conflicts & Tests --- .../AssemblyVisibility.cs | 1 + .../Endpoints/UAuthEndpointRegistrar.cs | 142 +++--- .../Extensions/ServiceCollectionExtensions.cs | 11 +- .../SessionId/CompositeSessionIdResolver.cs | 32 +- .../SessionId/ISessionIdResolver.cs | 2 +- .../SessionResolutionMiddleware.cs | 2 +- .../Options/UAuthAntiforgeryOptions.cs | 20 + .../Options/UAuthServerOptions.cs | 8 +- .../RateLimiting/UAuthEndpoint.cs | 1 + .../UAuthEndpointClassification.cs | 1 + .../RateLimiting/UAuthRateLimitPolicy.cs | 23 +- .../ResourceCompositionSessionIdResolver.cs | 54 +++ .../Security/UAuthAntiforgeryEndpoint.cs | 24 + .../Security/UAuthCsrfEndpointFilter.cs | 35 ++ .../Security/UAuthCsrfMetadata.cs | 3 + .../Security/UAuthCsrfOperation.cs | 8 + .../Security/UAuthCsrfPolicyEvaluator.cs | 26 + .../Security/UAuthCsrfResponseInspector.cs | 14 + .../Security/UAuthCsrfTransport.cs | 18 + .../Security/UAuthCsrfValidator.cs | 20 + .../Infrastructure/BrowserUAuthBridge.cs | 11 + .../TScripts/uauth.js | 124 ++++- .../wwwroot/uauth.min.js | 2 +- .../Infrastructure/IBrowserUAuthBridge.cs | 2 + .../Infrastructure/UAuthClientBootstrapper.cs | 32 +- .../Infrastructure/UAuthUrlBuilder.cs | 3 + .../Options/UAuthClientEndpointOptions.cs | 5 + .../AuthorizationManagementTests.cs | 70 ++- .../ClientRateLimitingIntegrationTests.cs | 8 +- .../CredentialManagementTests.cs | 15 +- .../Infrastructure/CsrfTestClient.cs | 138 ++++++ .../LoginTests.cs | 109 ++--- .../LogoutAdminTests.cs | 62 +-- .../LogoutTests.cs | 191 +++----- .../PkceFlowIntegrationTests.cs | 2 +- .../RateLimitingFlowTests.cs | 8 +- .../RateLimitingProofFlowTests.cs | 30 +- .../RateLimitingTests.cs | 5 +- .../RefreshTests.cs | 189 ++++---- .../SdkEnpointContractTests.cs | 27 +- .../Security/CsrfAttackTests.cs | 345 ++++++++++++++ .../Security/UAuthAntiforgeryEndpointTests.cs | 204 ++++++++ .../UAuthCsrfEndpointCoverageTests.cs | 184 ++++++++ .../SessionAdminTests.cs | 55 +-- .../SessionSlidingExpirationTests.cs | 51 +- .../SessionTests.cs | 32 +- .../UserIdentifierTests.cs | 21 +- .../UserLifecycleTests.cs | 15 +- .../UserProfileTests.cs | 102 ++-- .../Client/UAuthClientBootstrapperTests.cs | 202 +++++++- .../Security/UAuthCsrfPolicyEvaluatorTests.cs | 73 +++ .../CompositeSessionIdResolverTests.cs | 443 ++++++++---------- .../javascript/uauth.test.cjs | 185 +++++++- 53 files changed, 2463 insertions(+), 927 deletions(-) create mode 100644 src/CodeBeam.UltimateAuth.Server/Options/UAuthAntiforgeryOptions.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/ResourceApi/ResourceCompositionSessionIdResolver.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Security/UAuthAntiforgeryEndpoint.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Security/UAuthCsrfEndpointFilter.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Security/UAuthCsrfMetadata.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Security/UAuthCsrfOperation.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Security/UAuthCsrfPolicyEvaluator.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Security/UAuthCsrfResponseInspector.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Security/UAuthCsrfTransport.cs create mode 100644 src/CodeBeam.UltimateAuth.Server/Security/UAuthCsrfValidator.cs create mode 100644 tests/CodeBeam.UltimateAuth.Tests.Integration/Infrastructure/CsrfTestClient.cs create mode 100644 tests/CodeBeam.UltimateAuth.Tests.Integration/Security/CsrfAttackTests.cs create mode 100644 tests/CodeBeam.UltimateAuth.Tests.Integration/Security/UAuthAntiforgeryEndpointTests.cs create mode 100644 tests/CodeBeam.UltimateAuth.Tests.Integration/Security/UAuthCsrfEndpointCoverageTests.cs create mode 100644 tests/CodeBeam.UltimateAuth.Tests.Unit/Security/UAuthCsrfPolicyEvaluatorTests.cs diff --git a/src/CodeBeam.UltimateAuth.Server/AssemblyVisibility.cs b/src/CodeBeam.UltimateAuth.Server/AssemblyVisibility.cs index 247b2327..7992a8a8 100644 --- a/src/CodeBeam.UltimateAuth.Server/AssemblyVisibility.cs +++ b/src/CodeBeam.UltimateAuth.Server/AssemblyVisibility.cs @@ -2,4 +2,5 @@ [assembly: InternalsVisibleTo("CodeBeam.UltimateAuth.Credentials.Reference")] [assembly: InternalsVisibleTo("CodeBeam.UltimateAuth.Tests.Unit")] +[assembly: InternalsVisibleTo("CodeBeam.UltimateAuth.Tests.Integration")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] diff --git a/src/CodeBeam.UltimateAuth.Server/Endpoints/UAuthEndpointRegistrar.cs b/src/CodeBeam.UltimateAuth.Server/Endpoints/UAuthEndpointRegistrar.cs index ac01f945..3bf2fe0c 100644 --- a/src/CodeBeam.UltimateAuth.Server/Endpoints/UAuthEndpointRegistrar.cs +++ b/src/CodeBeam.UltimateAuth.Server/Endpoints/UAuthEndpointRegistrar.cs @@ -1,9 +1,10 @@ -using CodeBeam.UltimateAuth.Server.RateLimiting; +using CodeBeam.UltimateAuth.Server.RateLimiting; using CodeBeam.UltimateAuth.Authorization.Contracts; using CodeBeam.UltimateAuth.Core.Defaults; using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Server.Auth; using CodeBeam.UltimateAuth.Server.Options; +using CodeBeam.UltimateAuth.Server.Security; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -17,9 +18,12 @@ namespace CodeBeam.UltimateAuth.Server.Endpoints; public class UAuthEndpointRegistrar : IAuthEndpointRegistrar { private readonly UAuthServerEndpointOptions _options; + private readonly UAuthAntiforgeryOptions _antiforgeryOptions; + public UAuthEndpointRegistrar(IOptions options) { _options = options.Value.Endpoints; + _antiforgeryOptions = options.Value.Antiforgery; } bool Enabled(string action) => !_options.DisabledActions.Contains(action); @@ -38,12 +42,18 @@ public void MapEndpoints(RouteGroupBuilder rootGroup, UAuthServerOptions options : rootGroup.MapGroup("/" + basePrefix); group.AddEndpointFilter(); + group.AddEndpointFilter(); + + group.MapGet(_antiforgeryOptions.TokenEndpoint, UAuthAntiforgeryEndpoint.GetToken) + .WithUAuthRateLimit(UAuthEndpoint.AntiforgeryToken); + ((IEndpointConventionBuilder)group).Finally(endpoint => { if (!endpoint.Metadata.OfType().Any()) throw new InvalidOperationException("Every UltimateAuth endpoint must declare a rate limiting classification."); }); + //var user = group.MapGroup(""); var self = group.MapGroup("/me"); var admin = group.MapGroup("/admin"); @@ -51,32 +61,34 @@ public void MapEndpoints(RouteGroupBuilder rootGroup, UAuthServerOptions options if (options.Endpoints.Authentication != false) { group.MapPost("/login", async ([FromServices] ILoginEndpointHandler h, HttpContext ctx) - => await h.LoginAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Login)).WithUAuthRateLimit(UAuthEndpoint.Login); + => await h.LoginAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Login), new UAuthCsrfMetadata(UAuthCsrfOperation.Login)).WithUAuthRateLimit(UAuthEndpoint.Login); group.MapPost("/try-login", async ([FromServices] ILoginEndpointHandler h, HttpContext ctx) - => await h.TryLoginAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Login)).WithUAuthRateLimit(UAuthEndpoint.TryLogin); + => await h.TryLoginAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Login), new UAuthCsrfMetadata(UAuthCsrfOperation.Login)).WithUAuthRateLimit(UAuthEndpoint.TryLogin); + group.MapPost("/validate", async ([FromServices] IValidateEndpointHandler h, HttpContext ctx) => await h.ValidateAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.ValidateSession)).WithUAuthRateLimit(UAuthEndpoint.Validate); if (Enabled(UAuthActions.Flows.LogoutSelf)) group.MapPost("/logout", async ([FromServices] ILogoutEndpointHandler h, HttpContext ctx) - => await h.LogoutAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Logout)).WithUAuthRateLimit(UAuthEndpoint.Logout); + => await h.LogoutAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Logout), new UAuthCsrfMetadata(UAuthCsrfOperation.AuthenticatedMutation)).WithUAuthRateLimit(UAuthEndpoint.Logout); if (Enabled(UAuthActions.Flows.LogoutDeviceSelf)) self.MapPost("/logout-device", async ([FromServices] ILogoutEndpointHandler h, HttpContext ctx) - => await h.LogoutDeviceSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Logout)).WithUAuthRateLimit(UAuthEndpoint.LogoutDeviceSelf); + => await h.LogoutDeviceSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Logout), new UAuthCsrfMetadata(UAuthCsrfOperation.AuthenticatedMutation)).WithUAuthRateLimit(UAuthEndpoint.LogoutDeviceSelf); if (Enabled(UAuthActions.Flows.LogoutOthersSelf)) self.MapPost("/logout-others", async ([FromServices] ILogoutEndpointHandler h, HttpContext ctx) - => await h.LogoutOthersSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Logout)).WithUAuthRateLimit(UAuthEndpoint.LogoutOthersSelf); + => await h.LogoutOthersSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Logout), new UAuthCsrfMetadata(UAuthCsrfOperation.AuthenticatedMutation)).WithUAuthRateLimit(UAuthEndpoint.LogoutOthersSelf); if (Enabled(UAuthActions.Flows.LogoutAllSelf)) self.MapPost("/logout-all", async ([FromServices] ILogoutEndpointHandler h, HttpContext ctx) - => await h.LogoutAllSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Logout)).WithUAuthRateLimit(UAuthEndpoint.LogoutAllSelf); + => await h.LogoutAllSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Logout), new UAuthCsrfMetadata(UAuthCsrfOperation.AuthenticatedMutation)).WithUAuthRateLimit(UAuthEndpoint.LogoutAllSelf); group.MapPost("/refresh", async ([FromServices] IRefreshEndpointHandler h, HttpContext ctx) - => await h.RefreshAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.RefreshSession)).WithUAuthRateLimit(UAuthEndpoint.Refresh); + => await h.RefreshAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.RefreshSession), new UAuthCsrfMetadata(UAuthCsrfOperation.AuthenticatedMutation)).WithUAuthRateLimit(UAuthEndpoint.Refresh); + //group.MapPost("/reauth", async ([FromServices] IReauthEndpointHandler h, HttpContext ctx) // => await h.ReauthAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Reauthentication)); @@ -84,15 +96,16 @@ public void MapEndpoints(RouteGroupBuilder rootGroup, UAuthServerOptions options if (Enabled(UAuthActions.Flows.LogoutDeviceAdmin)) admin.MapPost("/users/{userKey}/logout-device", async ([FromServices] ILogoutEndpointHandler h, UserKey userKey, HttpContext ctx) - => await h.LogoutDeviceAdminAsync(ctx, userKey)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Logout)).WithUAuthRateLimit(UAuthEndpoint.LogoutDeviceAdmin); + => await h.LogoutDeviceAdminAsync(ctx, userKey)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Logout), new UAuthCsrfMetadata(UAuthCsrfOperation.AuthenticatedMutation)).WithUAuthRateLimit(UAuthEndpoint.LogoutDeviceAdmin); if (Enabled(UAuthActions.Flows.LogoutOthersAdmin)) admin.MapPost("/users/{userKey}/logout-others", async ([FromServices] ILogoutEndpointHandler h, UserKey userKey, HttpContext ctx) - => await h.LogoutOthersAdminAsync(ctx, userKey)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Logout)).WithUAuthRateLimit(UAuthEndpoint.LogoutOthersAdmin); + => await h.LogoutOthersAdminAsync(ctx, userKey)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Logout), new UAuthCsrfMetadata(UAuthCsrfOperation.AuthenticatedMutation)).WithUAuthRateLimit(UAuthEndpoint.LogoutOthersAdmin); if (Enabled(UAuthActions.Flows.LogoutAllAdmin)) admin.MapPost("/users/{userKey}/logout-all", async ([FromServices] ILogoutEndpointHandler h, UserKey userKey, HttpContext ctx) - => await h.LogoutAllAdminAsync(ctx, userKey)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Logout)).WithUAuthRateLimit(UAuthEndpoint.LogoutAllAdmin); + => await h.LogoutAllAdminAsync(ctx, userKey)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Logout), new UAuthCsrfMetadata(UAuthCsrfOperation.AuthenticatedMutation)).WithUAuthRateLimit(UAuthEndpoint.LogoutAllAdmin); + } if (options.Endpoints.Pkce != false) @@ -103,10 +116,11 @@ public void MapEndpoints(RouteGroupBuilder rootGroup, UAuthServerOptions options => await h.AuthorizeAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Login)).WithUAuthRateLimit(UAuthEndpoint.PkceAuthorize); pkce.MapPost("/complete", async ([FromServices] IPkceEndpointHandler h, HttpContext ctx) - => await h.CompleteAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Login)).WithUAuthRateLimit(UAuthEndpoint.PkceComplete); + => await h.CompleteAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Login), new UAuthCsrfMetadata(UAuthCsrfOperation.PkceCompletion)).WithUAuthRateLimit(UAuthEndpoint.PkceComplete); pkce.MapPost("/try-complete", async ([FromServices] IPkceEndpointHandler h, HttpContext ctx) - => await h.TryCompleteAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Login)).WithUAuthRateLimit(UAuthEndpoint.PkceTryComplete); + => await h.TryCompleteAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.Login), new UAuthCsrfMetadata(UAuthCsrfOperation.PkceCompletion)).WithUAuthRateLimit(UAuthEndpoint.PkceTryComplete); + } //if (options.Endpoints.Token != false) @@ -141,15 +155,16 @@ public void MapEndpoints(RouteGroupBuilder rootGroup, UAuthServerOptions options if (Enabled(UAuthActions.Sessions.RevokeChainSelf)) selfSession.MapPost("/chains/{chainId}/revoke", async ([FromServices] ISessionEndpointHandler h, SessionChainId chainId, HttpContext ctx) - => await h.RevokeMyChainAsync(chainId, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.RevokeSession)).WithUAuthRateLimit(UAuthEndpoint.RevokeMyChain); + => await h.RevokeMyChainAsync(chainId, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.RevokeSession), new UAuthCsrfMetadata(UAuthCsrfOperation.AuthenticatedMutation)).WithUAuthRateLimit(UAuthEndpoint.RevokeMyChain); if (Enabled(UAuthActions.Sessions.RevokeOtherChainsSelf)) selfSession.MapPost("/revoke-others",async ([FromServices] ISessionEndpointHandler h, HttpContext ctx) - => await h.RevokeOtherChainsAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.RevokeSession)).WithUAuthRateLimit(UAuthEndpoint.RevokeOtherChains); + => await h.RevokeOtherChainsAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.RevokeSession), new UAuthCsrfMetadata(UAuthCsrfOperation.AuthenticatedMutation)).WithUAuthRateLimit(UAuthEndpoint.RevokeOtherChains); if (Enabled(UAuthActions.Sessions.RevokeAllChainsSelf)) selfSession.MapPost("/revoke-all", async ([FromServices] ISessionEndpointHandler h, HttpContext ctx) - => await h.RevokeAllMyChainsAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.RevokeSession)).WithUAuthRateLimit(UAuthEndpoint.RevokeAllMyChains); + => await h.RevokeAllMyChainsAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.RevokeSession), new UAuthCsrfMetadata(UAuthCsrfOperation.AuthenticatedMutation)).WithUAuthRateLimit(UAuthEndpoint.RevokeAllMyChains); + if (Enabled(UAuthActions.Sessions.ListChainsAdmin)) @@ -162,19 +177,20 @@ public void MapEndpoints(RouteGroupBuilder rootGroup, UAuthServerOptions options if (Enabled(UAuthActions.Sessions.RevokeSessionAdmin)) adminSession.MapPost("/{sessionId}/revoke", async ([FromServices] ISessionEndpointHandler h, UserKey userKey, AuthSessionId sessionId, HttpContext ctx) - => await h.RevokeUserSessionAsync(userKey, sessionId, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.RevokeSession)).WithUAuthRateLimit(UAuthEndpoint.RevokeUserSession); + => await h.RevokeUserSessionAsync(userKey, sessionId, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.RevokeSession), new UAuthCsrfMetadata(UAuthCsrfOperation.AuthenticatedMutation)).WithUAuthRateLimit(UAuthEndpoint.RevokeUserSession); if (Enabled(UAuthActions.Sessions.RevokeChainAdmin)) adminSession.MapPost("/chains/{chainId}/revoke", async ([FromServices] ISessionEndpointHandler h, UserKey userKey, SessionChainId chainId, HttpContext ctx) - => await h.RevokeUserChainAsync(userKey, chainId, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.RevokeSession)).WithUAuthRateLimit(UAuthEndpoint.RevokeUserChain); + => await h.RevokeUserChainAsync(userKey, chainId, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.RevokeSession), new UAuthCsrfMetadata(UAuthCsrfOperation.AuthenticatedMutation)).WithUAuthRateLimit(UAuthEndpoint.RevokeUserChain); if (Enabled(UAuthActions.Sessions.RevokeRootAdmin)) adminSession.MapPost("/revoke-root", async ([FromServices] ISessionEndpointHandler h, UserKey userKey, HttpContext ctx) - => await h.RevokeRootAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.RevokeSession)).WithUAuthRateLimit(UAuthEndpoint.RevokeRoot); + => await h.RevokeRootAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.RevokeSession), new UAuthCsrfMetadata(UAuthCsrfOperation.AuthenticatedMutation)).WithUAuthRateLimit(UAuthEndpoint.RevokeRoot); if (Enabled(UAuthActions.Sessions.RevokeAllChainsAdmin)) adminSession.MapPost("/revoke-all", async ([FromServices] ISessionEndpointHandler h, UserKey userKey, HttpContext ctx) - => await h.RevokeAllChainsAsync(userKey, null, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.RevokeSession)).WithUAuthRateLimit(UAuthEndpoint.RevokeAllChains); + => await h.RevokeAllChainsAsync(userKey, null, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.RevokeSession), new UAuthCsrfMetadata(UAuthCsrfOperation.AuthenticatedMutation)).WithUAuthRateLimit(UAuthEndpoint.RevokeAllChains); + } //if (options.EnableUserInfoEndpoints != false) @@ -199,11 +215,12 @@ public void MapEndpoints(RouteGroupBuilder rootGroup, UAuthServerOptions options if (Enabled(UAuthActions.Users.ChangeStatusSelf)) self.MapPost("/status", async ([FromServices] IUserEndpointHandler h, HttpContext ctx) - => await h.ChangeStatusSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserManagement)).WithUAuthRateLimit(UAuthEndpoint.ChangeStatusSelf); + => await h.ChangeStatusSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserManagement), new UAuthCsrfMetadata(UAuthCsrfOperation.AuthenticatedMutation)).WithUAuthRateLimit(UAuthEndpoint.ChangeStatusSelf); if (Enabled(UAuthActions.Users.DeleteSelf)) self.MapPost("/delete", async ([FromServices] IUserEndpointHandler h, HttpContext ctx) - => await h.DeleteMeAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserManagement)).WithUAuthRateLimit(UAuthEndpoint.UserDeleteSelf); + => await h.DeleteMeAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserManagement), new UAuthCsrfMetadata(UAuthCsrfOperation.AuthenticatedMutation)).WithUAuthRateLimit(UAuthEndpoint.UserDeleteSelf); + if (Enabled(UAuthActions.Users.QueryAdmin)) @@ -216,15 +233,16 @@ public void MapEndpoints(RouteGroupBuilder rootGroup, UAuthServerOptions options if (Enabled(UAuthActions.Users.CreateAdmin)) adminUsers.MapPost("/create", async ([FromServices] IUserEndpointHandler h, HttpContext ctx) - => await h.CreateAdminAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserManagement)).WithUAuthRateLimit(UAuthEndpoint.UserCreateAdmin); + => await h.CreateAdminAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserManagement), new UAuthCsrfMetadata(UAuthCsrfOperation.AuthenticatedMutation)).WithUAuthRateLimit(UAuthEndpoint.UserCreateAdmin); if (Enabled(UAuthActions.Users.ChangeStatusAdmin)) adminUsers.MapPost("/{userKey}/status", async ([FromServices] IUserEndpointHandler h, UserKey userKey, HttpContext ctx) - => await h.ChangeStatusAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserManagement)).WithUAuthRateLimit(UAuthEndpoint.ChangeStatusAdmin); + => await h.ChangeStatusAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserManagement), new UAuthCsrfMetadata(UAuthCsrfOperation.AuthenticatedMutation)).WithUAuthRateLimit(UAuthEndpoint.ChangeStatusAdmin); if (Enabled(UAuthActions.Users.DeleteAdmin)) adminUsers.MapPost("/{userKey}/delete", async ([FromServices] IUserEndpointHandler h, UserKey userKey, HttpContext ctx) - => await h.DeleteAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserManagement)).WithUAuthRateLimit(UAuthEndpoint.UserDeleteAdmin); + => await h.DeleteAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserManagement), new UAuthCsrfMetadata(UAuthCsrfOperation.AuthenticatedMutation)).WithUAuthRateLimit(UAuthEndpoint.UserDeleteAdmin); + } if (options.Endpoints.UserProfile != false) @@ -235,15 +253,16 @@ public void MapEndpoints(RouteGroupBuilder rootGroup, UAuthServerOptions options if (Enabled(UAuthActions.UserProfiles.CreateSelf)) self.MapPost("/profile/create", async (IUserEndpointHandler h, HttpContext ctx) - => await h.CreateProfileSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserProfileManagement)).WithUAuthRateLimit(UAuthEndpoint.CreateProfileSelf); + => await h.CreateProfileSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserProfileManagement), new UAuthCsrfMetadata(UAuthCsrfOperation.AuthenticatedMutation)).WithUAuthRateLimit(UAuthEndpoint.CreateProfileSelf); if (Enabled(UAuthActions.UserProfiles.UpdateSelf)) self.MapPost("/profile/update", async ([FromServices] IUserEndpointHandler h, HttpContext ctx) - => await h.UpdateMeAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserProfileManagement)).WithUAuthRateLimit(UAuthEndpoint.ProfileUpdateSelf); + => await h.UpdateMeAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserProfileManagement), new UAuthCsrfMetadata(UAuthCsrfOperation.AuthenticatedMutation)).WithUAuthRateLimit(UAuthEndpoint.ProfileUpdateSelf); if (Enabled(UAuthActions.UserProfiles.DeleteSelf)) self.MapPost("/profile/delete", async (IUserEndpointHandler h, HttpContext ctx) - => await h.DeleteProfileSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserProfileManagement)).WithUAuthRateLimit(UAuthEndpoint.DeleteProfileSelf); + => await h.DeleteProfileSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserProfileManagement), new UAuthCsrfMetadata(UAuthCsrfOperation.AuthenticatedMutation)).WithUAuthRateLimit(UAuthEndpoint.DeleteProfileSelf); + if (Enabled(UAuthActions.UserProfiles.GetAdmin)) @@ -252,15 +271,16 @@ public void MapEndpoints(RouteGroupBuilder rootGroup, UAuthServerOptions options if (Enabled(UAuthActions.UserProfiles.CreateAdmin)) adminUsers.MapPost("/{userKey}/profile/create", async (IUserEndpointHandler h, UserKey userKey, HttpContext ctx) - => await h.CreateProfileAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserProfileManagement)).WithUAuthRateLimit(UAuthEndpoint.CreateProfileAdmin); + => await h.CreateProfileAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserProfileManagement), new UAuthCsrfMetadata(UAuthCsrfOperation.AuthenticatedMutation)).WithUAuthRateLimit(UAuthEndpoint.CreateProfileAdmin); if (Enabled(UAuthActions.UserProfiles.UpdateAdmin)) adminUsers.MapPost("/{userKey}/profile/update", async ([FromServices] IUserEndpointHandler h, UserKey userKey, HttpContext ctx) - => await h.UpdateUserAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserProfileManagement)).WithUAuthRateLimit(UAuthEndpoint.ProfileUpdateAdmin); + => await h.UpdateUserAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserProfileManagement), new UAuthCsrfMetadata(UAuthCsrfOperation.AuthenticatedMutation)).WithUAuthRateLimit(UAuthEndpoint.ProfileUpdateAdmin); if (Enabled(UAuthActions.UserProfiles.DeleteAdmin)) adminUsers.MapPost("/{userKey}/profile/delete", async (IUserEndpointHandler h, UserKey userKey, HttpContext ctx) - => await h.DeleteProfileAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserProfileManagement)).WithUAuthRateLimit(UAuthEndpoint.DeleteProfileAdmin); + => await h.DeleteProfileAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserProfileManagement), new UAuthCsrfMetadata(UAuthCsrfOperation.AuthenticatedMutation)).WithUAuthRateLimit(UAuthEndpoint.DeleteProfileAdmin); + } if (options.Endpoints.UserIdentifier != false) @@ -271,27 +291,28 @@ public void MapEndpoints(RouteGroupBuilder rootGroup, UAuthServerOptions options if (Enabled(UAuthActions.UserIdentifiers.AddSelf)) self.MapPost("/identifiers/add", async ([FromServices] IUserEndpointHandler h, HttpContext ctx) - => await h.AddUserIdentifierSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement)).WithUAuthRateLimit(UAuthEndpoint.AddUserIdentifierSelf); + => await h.AddUserIdentifierSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement), new UAuthCsrfMetadata(UAuthCsrfOperation.AuthenticatedMutation)).WithUAuthRateLimit(UAuthEndpoint.AddUserIdentifierSelf); if (Enabled(UAuthActions.UserIdentifiers.UpdateSelf)) self.MapPost("/identifiers/update", async ([FromServices] IUserEndpointHandler h, HttpContext ctx) - => await h.UpdateUserIdentifierSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement)).WithUAuthRateLimit(UAuthEndpoint.UpdateUserIdentifierSelf); + => await h.UpdateUserIdentifierSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement), new UAuthCsrfMetadata(UAuthCsrfOperation.AuthenticatedMutation)).WithUAuthRateLimit(UAuthEndpoint.UpdateUserIdentifierSelf); if (Enabled(UAuthActions.UserIdentifiers.SetPrimarySelf)) self.MapPost("/identifiers/set-primary",async ([FromServices] IUserEndpointHandler h, HttpContext ctx) - => await h.SetPrimaryUserIdentifierSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement)).WithUAuthRateLimit(UAuthEndpoint.SetPrimaryUserIdentifierSelf); + => await h.SetPrimaryUserIdentifierSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement), new UAuthCsrfMetadata(UAuthCsrfOperation.AuthenticatedMutation)).WithUAuthRateLimit(UAuthEndpoint.SetPrimaryUserIdentifierSelf); if (Enabled(UAuthActions.UserIdentifiers.UnsetPrimarySelf)) self.MapPost("/identifiers/unset-primary", async ([FromServices] IUserEndpointHandler h, HttpContext ctx) - => await h.UnsetPrimaryUserIdentifierSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement)).WithUAuthRateLimit(UAuthEndpoint.UnsetPrimaryUserIdentifierSelf); + => await h.UnsetPrimaryUserIdentifierSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement), new UAuthCsrfMetadata(UAuthCsrfOperation.AuthenticatedMutation)).WithUAuthRateLimit(UAuthEndpoint.UnsetPrimaryUserIdentifierSelf); if (Enabled(UAuthActions.UserIdentifiers.VerifySelf)) self.MapPost("/identifiers/verify", async ([FromServices] IUserEndpointHandler h, HttpContext ctx) - => await h.VerifyUserIdentifierSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement)).WithUAuthRateLimit(UAuthEndpoint.VerifyUserIdentifierSelf); + => await h.VerifyUserIdentifierSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement), new UAuthCsrfMetadata(UAuthCsrfOperation.AuthenticatedMutation)).WithUAuthRateLimit(UAuthEndpoint.VerifyUserIdentifierSelf); if (Enabled(UAuthActions.UserIdentifiers.DeleteSelf)) self.MapPost("/identifiers/delete", async ([FromServices] IUserEndpointHandler h, HttpContext ctx) - => await h.DeleteUserIdentifierSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement)).WithUAuthRateLimit(UAuthEndpoint.DeleteUserIdentifierSelf); + => await h.DeleteUserIdentifierSelfAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement), new UAuthCsrfMetadata(UAuthCsrfOperation.AuthenticatedMutation)).WithUAuthRateLimit(UAuthEndpoint.DeleteUserIdentifierSelf); + if (Enabled(UAuthActions.UserIdentifiers.GetAdmin)) @@ -300,27 +321,28 @@ public void MapEndpoints(RouteGroupBuilder rootGroup, UAuthServerOptions options if (Enabled(UAuthActions.UserIdentifiers.AddAdmin)) adminUsers.MapPost("/{userKey}/identifiers/add", async ([FromServices] IUserEndpointHandler h, UserKey userKey, HttpContext ctx) - => await h.AddUserIdentifierAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement)).WithUAuthRateLimit(UAuthEndpoint.AddUserIdentifierAdmin); + => await h.AddUserIdentifierAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement), new UAuthCsrfMetadata(UAuthCsrfOperation.AuthenticatedMutation)).WithUAuthRateLimit(UAuthEndpoint.AddUserIdentifierAdmin); if (Enabled(UAuthActions.UserIdentifiers.UpdateAdmin)) adminUsers.MapPost("/{userKey}/identifiers/update", async ([FromServices] IUserEndpointHandler h, UserKey userKey, HttpContext ctx) - => await h.UpdateUserIdentifierAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement)).WithUAuthRateLimit(UAuthEndpoint.UpdateUserIdentifierAdmin); + => await h.UpdateUserIdentifierAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement), new UAuthCsrfMetadata(UAuthCsrfOperation.AuthenticatedMutation)).WithUAuthRateLimit(UAuthEndpoint.UpdateUserIdentifierAdmin); if (Enabled(UAuthActions.UserIdentifiers.SetPrimaryAdmin)) adminUsers.MapPost("/{userKey}/identifiers/set-primary", async ([FromServices] IUserEndpointHandler h, UserKey userKey, HttpContext ctx) - => await h.SetPrimaryUserIdentifierAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement)).WithUAuthRateLimit(UAuthEndpoint.SetPrimaryUserIdentifierAdmin); + => await h.SetPrimaryUserIdentifierAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement), new UAuthCsrfMetadata(UAuthCsrfOperation.AuthenticatedMutation)).WithUAuthRateLimit(UAuthEndpoint.SetPrimaryUserIdentifierAdmin); if (Enabled(UAuthActions.UserIdentifiers.UnsetPrimaryAdmin)) adminUsers.MapPost("/{userKey}/identifiers/unset-primary", async ([FromServices] IUserEndpointHandler h, UserKey userKey, HttpContext ctx) - => await h.UnsetPrimaryUserIdentifierAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement)).WithUAuthRateLimit(UAuthEndpoint.UnsetPrimaryUserIdentifierAdmin); + => await h.UnsetPrimaryUserIdentifierAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement), new UAuthCsrfMetadata(UAuthCsrfOperation.AuthenticatedMutation)).WithUAuthRateLimit(UAuthEndpoint.UnsetPrimaryUserIdentifierAdmin); if (Enabled(UAuthActions.UserIdentifiers.VerifyAdmin)) adminUsers.MapPost("/{userKey}/identifiers/verify", async ([FromServices] IUserEndpointHandler h, UserKey userKey, HttpContext ctx) - => await h.VerifyUserIdentifierAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement)).WithUAuthRateLimit(UAuthEndpoint.VerifyUserIdentifierAdmin); + => await h.VerifyUserIdentifierAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement), new UAuthCsrfMetadata(UAuthCsrfOperation.AuthenticatedMutation)).WithUAuthRateLimit(UAuthEndpoint.VerifyUserIdentifierAdmin); if (Enabled(UAuthActions.UserIdentifiers.DeleteAdmin)) adminUsers.MapPost("/{userKey}/identifiers/delete", async ([FromServices] IUserEndpointHandler h, UserKey userKey, HttpContext ctx) - => await h.DeleteUserIdentifierAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement)).WithUAuthRateLimit(UAuthEndpoint.DeleteUserIdentifierAdmin); + => await h.DeleteUserIdentifierAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.UserIdentifierManagement), new UAuthCsrfMetadata(UAuthCsrfOperation.AuthenticatedMutation)).WithUAuthRateLimit(UAuthEndpoint.DeleteUserIdentifierAdmin); + if (Enabled(UAuthActions.UserIdentifiers.CheckAvailability)) self.MapPost("/identifiers/check-availability", async ([FromServices] IUserEndpointHandler h, HttpContext ctx) @@ -338,15 +360,16 @@ public void MapEndpoints(RouteGroupBuilder rootGroup, UAuthServerOptions options if (Enabled(UAuthActions.Credentials.AddSelf)) selfCredentials.MapPost("/add", async ([FromServices] ICredentialEndpointHandler h, HttpContext ctx) - => await h.AddAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement)).WithUAuthRateLimit(UAuthEndpoint.CredentialAddSelf); + => await h.AddAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement), new UAuthCsrfMetadata(UAuthCsrfOperation.AuthenticatedMutation)).WithUAuthRateLimit(UAuthEndpoint.CredentialAddSelf); if (Enabled(UAuthActions.Credentials.ChangeSelf)) selfCredentials.MapPost("/change", async ([FromServices] ICredentialEndpointHandler h, HttpContext ctx) - => await h.ChangeSecretAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement)).WithUAuthRateLimit(UAuthEndpoint.CredentialChangeSelf); + => await h.ChangeSecretAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement), new UAuthCsrfMetadata(UAuthCsrfOperation.AuthenticatedMutation)).WithUAuthRateLimit(UAuthEndpoint.CredentialChangeSelf); if (Enabled(UAuthActions.Credentials.RevokeSelf)) selfCredentials.MapPost("/revoke", async ([FromServices] ICredentialEndpointHandler h, HttpContext ctx) - => await h.RevokeAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement)).WithUAuthRateLimit(UAuthEndpoint.CredentialRevokeSelf); + => await h.RevokeAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement), new UAuthCsrfMetadata(UAuthCsrfOperation.AuthenticatedMutation)).WithUAuthRateLimit(UAuthEndpoint.CredentialRevokeSelf); + if (Enabled(UAuthActions.Credentials.BeginResetAnonymous)) selfCredentials.MapPost("/reset/begin", async ([FromServices] ICredentialEndpointHandler h, HttpContext ctx) @@ -359,27 +382,28 @@ public void MapEndpoints(RouteGroupBuilder rootGroup, UAuthServerOptions options if (Enabled(UAuthActions.Credentials.AddAdmin)) adminCredentials.MapPost("/add", async ([FromServices] ICredentialEndpointHandler h, UserKey userKey, HttpContext ctx) - => await h.AddAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement)).WithUAuthRateLimit(UAuthEndpoint.CredentialAddAdmin); + => await h.AddAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement), new UAuthCsrfMetadata(UAuthCsrfOperation.AuthenticatedMutation)).WithUAuthRateLimit(UAuthEndpoint.CredentialAddAdmin); if (Enabled(UAuthActions.Credentials.ChangeAdmin)) adminCredentials.MapPost("/change", async ([FromServices] ICredentialEndpointHandler h, UserKey userKey, HttpContext ctx) - => await h.ChangeSecretAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement)).WithUAuthRateLimit(UAuthEndpoint.CredentialChangeAdmin); + => await h.ChangeSecretAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement), new UAuthCsrfMetadata(UAuthCsrfOperation.AuthenticatedMutation)).WithUAuthRateLimit(UAuthEndpoint.CredentialChangeAdmin); if (Enabled(UAuthActions.Credentials.RevokeAdmin)) adminCredentials.MapPost("/revoke", async ([FromServices] ICredentialEndpointHandler h, UserKey userKey, HttpContext ctx) - => await h.RevokeAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement)).WithUAuthRateLimit(UAuthEndpoint.CredentialRevokeAdmin); + => await h.RevokeAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement), new UAuthCsrfMetadata(UAuthCsrfOperation.AuthenticatedMutation)).WithUAuthRateLimit(UAuthEndpoint.CredentialRevokeAdmin); if (Enabled(UAuthActions.Credentials.BeginResetAdmin)) adminCredentials.MapPost("/reset/begin", async ([FromServices] ICredentialEndpointHandler h, UserKey userKey, HttpContext ctx) - => await h.BeginResetAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement)).WithUAuthRateLimit(UAuthEndpoint.CredentialResetBeginAdmin); + => await h.BeginResetAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement), new UAuthCsrfMetadata(UAuthCsrfOperation.AuthenticatedMutation)).WithUAuthRateLimit(UAuthEndpoint.CredentialResetBeginAdmin); if (Enabled(UAuthActions.Credentials.CompleteResetAdmin)) adminCredentials.MapPost("/reset/complete", async ([FromServices] ICredentialEndpointHandler h, UserKey userKey, HttpContext ctx) - => await h.CompleteResetAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement)).WithUAuthRateLimit(UAuthEndpoint.CredentialResetCompleteAdmin); + => await h.CompleteResetAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement), new UAuthCsrfMetadata(UAuthCsrfOperation.AuthenticatedMutation)).WithUAuthRateLimit(UAuthEndpoint.CredentialResetCompleteAdmin); if (Enabled(UAuthActions.Credentials.DeleteAdmin)) adminCredentials.MapPost("/delete", async ([FromServices] ICredentialEndpointHandler h, UserKey userKey, HttpContext ctx) - => await h.DeleteAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement)).WithUAuthRateLimit(UAuthEndpoint.CredentialDeleteAdmin); + => await h.DeleteAdminAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.CredentialManagement), new UAuthCsrfMetadata(UAuthCsrfOperation.AuthenticatedMutation)).WithUAuthRateLimit(UAuthEndpoint.CredentialDeleteAdmin); + } if (options.Endpoints.Authorization != false) @@ -406,15 +430,16 @@ public void MapEndpoints(RouteGroupBuilder rootGroup, UAuthServerOptions options if (Enabled(UAuthActions.Authorization.Roles.AssignAdmin)) adminAuthz.MapPost("/users/{userKey}/roles/assign", async ([FromServices] IAuthorizationEndpointHandler h, UserKey userKey, HttpContext ctx) - => await h.AssignRoleAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.AuthorizationManagement)).WithUAuthRateLimit(UAuthEndpoint.AssignRole); + => await h.AssignRoleAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.AuthorizationManagement), new UAuthCsrfMetadata(UAuthCsrfOperation.AuthenticatedMutation)).WithUAuthRateLimit(UAuthEndpoint.AssignRole); if (Enabled(UAuthActions.Authorization.Roles.RemoveAdmin)) adminAuthz.MapPost("/users/{userKey}/roles/remove", async ([FromServices] IAuthorizationEndpointHandler h, UserKey userKey, HttpContext ctx) - => await h.RemoveRoleAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.AuthorizationManagement)).WithUAuthRateLimit(UAuthEndpoint.RemoveRole); + => await h.RemoveRoleAsync(userKey, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.AuthorizationManagement), new UAuthCsrfMetadata(UAuthCsrfOperation.AuthenticatedMutation)).WithUAuthRateLimit(UAuthEndpoint.RemoveRole); if (Enabled(UAuthActions.Authorization.Roles.CreateAdmin)) adminAuthz.MapPost("/roles/create",async ([FromServices] IAuthorizationEndpointHandler h, HttpContext ctx) - => await h.CreateRoleAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.AuthorizationManagement)).WithUAuthRateLimit(UAuthEndpoint.CreateRole); + => await h.CreateRoleAsync(ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.AuthorizationManagement), new UAuthCsrfMetadata(UAuthCsrfOperation.AuthenticatedMutation)).WithUAuthRateLimit(UAuthEndpoint.CreateRole); + if (Enabled(UAuthActions.Authorization.Roles.QueryAdmin)) adminAuthz.MapPost("/roles/query", async ([FromServices] IAuthorizationEndpointHandler h, HttpContext ctx) @@ -422,15 +447,16 @@ public void MapEndpoints(RouteGroupBuilder rootGroup, UAuthServerOptions options if (Enabled(UAuthActions.Authorization.Roles.RenameAdmin)) adminAuthz.MapPost("/roles/{roleId}/rename", async ([FromServices] IAuthorizationEndpointHandler h, RoleId roleId, HttpContext ctx) - => await h.RenameRoleAsync(roleId, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.AuthorizationManagement)).WithUAuthRateLimit(UAuthEndpoint.RenameRole); + => await h.RenameRoleAsync(roleId, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.AuthorizationManagement), new UAuthCsrfMetadata(UAuthCsrfOperation.AuthenticatedMutation)).WithUAuthRateLimit(UAuthEndpoint.RenameRole); if (Enabled(UAuthActions.Authorization.Roles.SetPermissionsAdmin)) adminAuthz.MapPost("/roles/{roleId}/permissions", async ([FromServices] IAuthorizationEndpointHandler h, RoleId roleId, HttpContext ctx) - => await h.SetRolePermissionsAsync(roleId, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.AuthorizationManagement)).WithUAuthRateLimit(UAuthEndpoint.SetRolePermissions); + => await h.SetRolePermissionsAsync(roleId, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.AuthorizationManagement), new UAuthCsrfMetadata(UAuthCsrfOperation.AuthenticatedMutation)).WithUAuthRateLimit(UAuthEndpoint.SetRolePermissions); if (Enabled(UAuthActions.Authorization.Roles.DeleteAdmin)) adminAuthz.MapPost("/roles/{roleId}/delete", async ([FromServices] IAuthorizationEndpointHandler h, RoleId roleId, HttpContext ctx) - => await h.DeleteRoleAsync(roleId, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.AuthorizationManagement)).WithUAuthRateLimit(UAuthEndpoint.DeleteRole); + => await h.DeleteRoleAsync(roleId, ctx)).WithMetadata(new AuthFlowMetadata(AuthFlowType.AuthorizationManagement), new UAuthCsrfMetadata(UAuthCsrfOperation.AuthenticatedMutation)).WithUAuthRateLimit(UAuthEndpoint.DeleteRole); + } // IMPORTANT: diff --git a/src/CodeBeam.UltimateAuth.Server/Extensions/ServiceCollectionExtensions.cs b/src/CodeBeam.UltimateAuth.Server/Extensions/ServiceCollectionExtensions.cs index 11305e4b..ce14a9e6 100644 --- a/src/CodeBeam.UltimateAuth.Server/Extensions/ServiceCollectionExtensions.cs +++ b/src/CodeBeam.UltimateAuth.Server/Extensions/ServiceCollectionExtensions.cs @@ -315,6 +315,15 @@ private static IServiceCollection AddUltimateAuthServerInternal(this IServiceCol opt.CustomResolversFirst = true; }); + services.AddAntiforgery(options => + { + options.HeaderName = "X-UAuth-CSRF"; + options.FormFieldName = "__RequestVerificationToken"; + }); + + services.AddScoped(); + services.AddScoped(); + return services; } @@ -448,7 +457,7 @@ private static IServiceCollection AddUltimateAuthResourceInternal(this IServiceC services.AddScoped(); services.AddScoped(); - services.TryAddScoped(); + services.TryAddScoped(); services.TryAddScoped(); services.TryAddScoped(); diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/SessionId/CompositeSessionIdResolver.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/SessionId/CompositeSessionIdResolver.cs index 8a72f7d5..6d0c5731 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/SessionId/CompositeSessionIdResolver.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/SessionId/CompositeSessionIdResolver.cs @@ -1,4 +1,5 @@ using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Server.Auth; using CodeBeam.UltimateAuth.Server.Options; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; @@ -9,25 +10,34 @@ namespace CodeBeam.UltimateAuth.Server.Infrastructure; public sealed class CompositeSessionIdResolver : ISessionIdResolver { private readonly IReadOnlyDictionary _resolvers; - private readonly UAuthSessionResolutionOptions _options; + private readonly IClientProfileReader _clientProfileReader; + private readonly IEffectiveServerOptionsProvider _effectiveOptionsProvider; - public CompositeSessionIdResolver(IEnumerable resolvers, IOptions options) + public CompositeSessionIdResolver(IEnumerable resolvers, IClientProfileReader clientProfileReader, IEffectiveServerOptionsProvider effectiveOptionsProvider) { - _options = options.Value.SessionResolution; _resolvers = resolvers.ToDictionary(r => r.Name, StringComparer.OrdinalIgnoreCase); + _clientProfileReader = clientProfileReader; + _effectiveOptionsProvider = effectiveOptionsProvider; } - public AuthSessionId? Resolve(HttpContext context) + public async ValueTask ResolveAsync(HttpContext context, CancellationToken ct = default) { - foreach (var name in _options.Order) + ct.ThrowIfCancellationRequested(); + + var clientProfile = await _clientProfileReader.ReadAsync(context); + var effective = _effectiveOptionsProvider.GetEffective(context, clientProfile); + var options = effective.Options.SessionResolution; + + foreach (var name in options.Order) { - if (!IsEnabled(name)) + if (!IsEnabled(name, options)) continue; if (!_resolvers.TryGetValue(name, out var resolver)) continue; var id = resolver.Resolve(context); + if (id is not null) return id; } @@ -35,19 +45,19 @@ public CompositeSessionIdResolver(IEnumerable resolvers return null; } - private bool IsEnabled(string name) + private static bool IsEnabled(string name, UAuthSessionResolutionOptions options) { if (name.Equals("Bearer", StringComparison.OrdinalIgnoreCase)) - return _options.EnableBearer; + return options.EnableBearer; if (name.Equals("Header", StringComparison.OrdinalIgnoreCase)) - return _options.EnableHeader; + return options.EnableHeader; if (name.Equals("Cookie", StringComparison.OrdinalIgnoreCase)) - return _options.EnableCookie; + return options.EnableCookie; if (name.Equals("Query", StringComparison.OrdinalIgnoreCase)) - return _options.EnableQuery; + return options.EnableQuery; return false; } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/SessionId/ISessionIdResolver.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/SessionId/ISessionIdResolver.cs index f46bbff3..55eec7da 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/SessionId/ISessionIdResolver.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/SessionId/ISessionIdResolver.cs @@ -5,5 +5,5 @@ namespace CodeBeam.UltimateAuth.Server.Infrastructure; public interface ISessionIdResolver { - AuthSessionId? Resolve(HttpContext context); + ValueTask ResolveAsync(HttpContext context, CancellationToken ct = default); } diff --git a/src/CodeBeam.UltimateAuth.Server/Middlewares/SessionResolutionMiddleware.cs b/src/CodeBeam.UltimateAuth.Server/Middlewares/SessionResolutionMiddleware.cs index 7e7d94d3..dd7f36ac 100644 --- a/src/CodeBeam.UltimateAuth.Server/Middlewares/SessionResolutionMiddleware.cs +++ b/src/CodeBeam.UltimateAuth.Server/Middlewares/SessionResolutionMiddleware.cs @@ -21,7 +21,7 @@ public async Task InvokeAsync(HttpContext context) var sessionIdResolver = context.RequestServices.GetRequiredService(); var tenant = context.GetTenant(); - var sessionId = sessionIdResolver.Resolve(context); + var sessionId = await sessionIdResolver.ResolveAsync(context); var sessionContext = sessionId is null ? SessionContext.Anonymous() diff --git a/src/CodeBeam.UltimateAuth.Server/Options/UAuthAntiforgeryOptions.cs b/src/CodeBeam.UltimateAuth.Server/Options/UAuthAntiforgeryOptions.cs new file mode 100644 index 00000000..f7f35eb3 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Options/UAuthAntiforgeryOptions.cs @@ -0,0 +1,20 @@ +namespace CodeBeam.UltimateAuth.Server.Options; + +/// +/// Configures CSRF protection for UltimateAuth endpoints. +/// +public sealed class UAuthAntiforgeryOptions +{ + // TODO: Implement form field and header name configuration for antiforgery token submission. Check uauth.js file carefully to ensure that the form field and header names are consistent with the client-side implementation. + + /// + /// Path segment used to retrieve a request token. + /// Resolved under the configured UltimateAuth base path. + /// + public string TokenEndpoint { get; set; } = "/antiforgery/token"; + + public UAuthAntiforgeryOptions Clone() => new() + { + TokenEndpoint = TokenEndpoint + }; +} diff --git a/src/CodeBeam.UltimateAuth.Server/Options/UAuthServerOptions.cs b/src/CodeBeam.UltimateAuth.Server/Options/UAuthServerOptions.cs index ed510713..0e8a6e8d 100644 --- a/src/CodeBeam.UltimateAuth.Server/Options/UAuthServerOptions.cs +++ b/src/CodeBeam.UltimateAuth.Server/Options/UAuthServerOptions.cs @@ -119,12 +119,7 @@ public sealed class UAuthServerOptions public UAuthNavigationOptions Navigation { get; set; } = new(); - - ///// - ///// If true, server will add anti-forgery headers - ///// and require proper request metadata. - ///// - //public bool EnableAntiCsrfProtection { get; set; } = true; + public UAuthAntiforgeryOptions Antiforgery { get; set; } = new(); ///// ///// If true, login attempts are rate-limited to prevent brute force attacks. @@ -178,6 +173,7 @@ internal UAuthServerOptions Clone() UserProfile = UserProfile.Clone(), Endpoints = Endpoints.Clone(), Navigation = Navigation.Clone(), + Antiforgery = Antiforgery.Clone(), //EnableAntiCsrfProtection = EnableAntiCsrfProtection, //EnableLoginRateLimiting = EnableLoginRateLimiting, diff --git a/src/CodeBeam.UltimateAuth.Server/RateLimiting/UAuthEndpoint.cs b/src/CodeBeam.UltimateAuth.Server/RateLimiting/UAuthEndpoint.cs index 8331894c..f440185b 100644 --- a/src/CodeBeam.UltimateAuth.Server/RateLimiting/UAuthEndpoint.cs +++ b/src/CodeBeam.UltimateAuth.Server/RateLimiting/UAuthEndpoint.cs @@ -83,6 +83,7 @@ public enum UAuthEndpoint DeleteRole, HubEntry, PasswordPolicy, + AntiforgeryToken, } internal enum UAuthRateLimitGroup { Sensitive, Standard, Frequent } diff --git a/src/CodeBeam.UltimateAuth.Server/RateLimiting/UAuthEndpointClassification.cs b/src/CodeBeam.UltimateAuth.Server/RateLimiting/UAuthEndpointClassification.cs index 6929c13a..cae51dc2 100644 --- a/src/CodeBeam.UltimateAuth.Server/RateLimiting/UAuthEndpointClassification.cs +++ b/src/CodeBeam.UltimateAuth.Server/RateLimiting/UAuthEndpointClassification.cs @@ -86,6 +86,7 @@ internal static class UAuthEndpointClassification UAuthEndpoint.DeleteRole => new(endpoint, UAuthRateLimitGroup.Standard, false), UAuthEndpoint.HubEntry => new(endpoint, UAuthRateLimitGroup.Sensitive, true), UAuthEndpoint.PasswordPolicy => new(endpoint, UAuthRateLimitGroup.Standard, true), + UAuthEndpoint.AntiforgeryToken => new(endpoint, UAuthRateLimitGroup.Frequent, false), _ => throw new ArgumentOutOfRangeException(nameof(endpoint)) }; } diff --git a/src/CodeBeam.UltimateAuth.Server/RateLimiting/UAuthRateLimitPolicy.cs b/src/CodeBeam.UltimateAuth.Server/RateLimiting/UAuthRateLimitPolicy.cs index 435e4f21..ff2ca672 100644 --- a/src/CodeBeam.UltimateAuth.Server/RateLimiting/UAuthRateLimitPolicy.cs +++ b/src/CodeBeam.UltimateAuth.Server/RateLimiting/UAuthRateLimitPolicy.cs @@ -66,14 +66,20 @@ internal static T WithUAuthRateLimit(this T builder, UAuthEndpoint endpoint) builder.Finally(b => { // Includes policies inherited from application route groups. Native Disable also wins. - if (b.Metadata.OfType().Any() || - b.Metadata.OfType().Any()) return; + if (b.Metadata.OfType().Any() || b.Metadata.OfType().Any()) + return; var options = b.ApplicationServices.GetRequiredService>().Value.RateLimiting; var (rule, budget) = options.Resolve(metadata); - if (!options.Enabled || !rule.Enabled) return; + + if (!options.Enabled || !rule.Enabled) + return; + var immediate = new ImmediateConventionBuilder(b); - if (rule.PolicyName is not null) immediate.RequireRateLimiting(rule.PolicyName); - else immediate.RequireRateLimiting(new UAuthRateLimitPolicy(metadata, rule, budget)); + + if (rule.PolicyName is not null) + immediate.RequireRateLimiting(rule.PolicyName); + else + immediate.RequireRateLimiting(new UAuthRateLimitPolicy(metadata, rule, budget)); }); return builder; } @@ -94,10 +100,13 @@ internal static class UAuthRateLimitValidation return "RateLimiting.Endpoints contains an unknown endpoint."; foreach (var rule in new[] { options.Sensitive, options.Standard, options.Frequent, options.Login }.Concat(options.Endpoints.Values)) { - if (rule is null) return "RateLimiting rules cannot be null."; + if (rule is null) + return "RateLimiting rules cannot be null."; + if (rule.PolicyName is not null) { - if (string.IsNullOrWhiteSpace(rule.PolicyName)) return "RateLimiting.PolicyName cannot be blank."; + if (string.IsNullOrWhiteSpace(rule.PolicyName)) + return "RateLimiting.PolicyName cannot be blank."; } else if (rule.PermitLimit <= 0 || rule.Window.Ticks < 6) return "RateLimiting requires a positive PermitLimit and a window of at least six ticks."; diff --git a/src/CodeBeam.UltimateAuth.Server/ResourceApi/ResourceCompositionSessionIdResolver.cs b/src/CodeBeam.UltimateAuth.Server/ResourceApi/ResourceCompositionSessionIdResolver.cs new file mode 100644 index 00000000..cd986c9a --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/ResourceApi/ResourceCompositionSessionIdResolver.cs @@ -0,0 +1,54 @@ +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Server.Options; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; + +namespace CodeBeam.UltimateAuth.Server.Infrastructure; + +public sealed class ResourceCompositeSessionIdResolver : ISessionIdResolver +{ + private readonly IReadOnlyDictionary _resolvers; + private readonly UAuthSessionResolutionOptions _options; + + public ResourceCompositeSessionIdResolver(IEnumerable resolvers, IOptions options) + { + _options = options.Value.SessionResolution; + _resolvers = resolvers.ToDictionary(r => r.Name, StringComparer.OrdinalIgnoreCase); + } + + public ValueTask ResolveAsync(HttpContext context, CancellationToken ct = default) + { + foreach (var name in _options.Order) + { + if (!IsEnabled(name)) + continue; + + if (!_resolvers.TryGetValue(name, out var resolver)) + continue; + + var id = resolver.Resolve(context); + + if (id is not null) + return ValueTask.FromResult(id); + } + + return ValueTask.FromResult(null); + } + + private bool IsEnabled(string name) + { + if (name.Equals("Bearer", StringComparison.OrdinalIgnoreCase)) + return _options.EnableBearer; + + if (name.Equals("Header", StringComparison.OrdinalIgnoreCase)) + return _options.EnableHeader; + + if (name.Equals("Cookie", StringComparison.OrdinalIgnoreCase)) + return _options.EnableCookie; + + if (name.Equals("Query", StringComparison.OrdinalIgnoreCase)) + return _options.EnableQuery; + + return false; + } +} diff --git a/src/CodeBeam.UltimateAuth.Server/Security/UAuthAntiforgeryEndpoint.cs b/src/CodeBeam.UltimateAuth.Server/Security/UAuthAntiforgeryEndpoint.cs new file mode 100644 index 00000000..380bb729 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Security/UAuthAntiforgeryEndpoint.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Antiforgery; +using Microsoft.AspNetCore.Http; + +namespace CodeBeam.UltimateAuth.Server.Security; + +internal static class UAuthAntiforgeryEndpoint +{ + public static IResult GetToken(HttpContext context, IAntiforgery antiforgery) + { + context.Response.Headers.CacheControl = "no-store, no-cache"; + + context.Response.Headers.Pragma = "no-cache"; + context.Response.Headers["X-Content-Type-Options"] = "nosniff"; + + var tokens = antiforgery.GetAndStoreTokens(context); + + return Results.Ok(new + { + requestToken = tokens.RequestToken, + headerName = tokens.HeaderName, + formFieldName = tokens.FormFieldName + }); + } +} diff --git a/src/CodeBeam.UltimateAuth.Server/Security/UAuthCsrfEndpointFilter.cs b/src/CodeBeam.UltimateAuth.Server/Security/UAuthCsrfEndpointFilter.cs new file mode 100644 index 00000000..39cca5e8 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Security/UAuthCsrfEndpointFilter.cs @@ -0,0 +1,35 @@ +using CodeBeam.UltimateAuth.Server.Auth; +using Microsoft.AspNetCore.Http; + +namespace CodeBeam.UltimateAuth.Server.Security; + +internal sealed class UAuthCsrfEndpointFilter(IAuthFlowContextAccessor authContext, UAuthCsrfPolicyEvaluator policy, UAuthCsrfValidator validator) : IEndpointFilter +{ + public async ValueTask InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next) + { + var http = context.HttpContext; + + var metadata = http.GetEndpoint()?.Metadata.GetMetadata(); + + if (metadata is null) + return await next(context); + + var auth = authContext.Current; + + if (!policy.RequiresProtection(auth, metadata.Operation)) + { + return await next(context); + } + + if (!await validator.ValidateAsync(http)) + { + return Results.BadRequest( + new + { + error = "invalid_antiforgery_token" + }); + } + + return await next(context); + } +} diff --git a/src/CodeBeam.UltimateAuth.Server/Security/UAuthCsrfMetadata.cs b/src/CodeBeam.UltimateAuth.Server/Security/UAuthCsrfMetadata.cs new file mode 100644 index 00000000..155bcac2 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Security/UAuthCsrfMetadata.cs @@ -0,0 +1,3 @@ +using CodeBeam.UltimateAuth.Server.Security; + +internal sealed record UAuthCsrfMetadata(UAuthCsrfOperation Operation); diff --git a/src/CodeBeam.UltimateAuth.Server/Security/UAuthCsrfOperation.cs b/src/CodeBeam.UltimateAuth.Server/Security/UAuthCsrfOperation.cs new file mode 100644 index 00000000..d419cd91 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Security/UAuthCsrfOperation.cs @@ -0,0 +1,8 @@ +namespace CodeBeam.UltimateAuth.Server.Security; + +public enum UAuthCsrfOperation +{ + Login = 0, + PkceCompletion = 10, + AuthenticatedMutation = 20 +} diff --git a/src/CodeBeam.UltimateAuth.Server/Security/UAuthCsrfPolicyEvaluator.cs b/src/CodeBeam.UltimateAuth.Server/Security/UAuthCsrfPolicyEvaluator.cs new file mode 100644 index 00000000..e506e770 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Security/UAuthCsrfPolicyEvaluator.cs @@ -0,0 +1,26 @@ +using CodeBeam.UltimateAuth.Server.Auth; + +namespace CodeBeam.UltimateAuth.Server.Security; + +internal sealed class UAuthCsrfPolicyEvaluator +{ + public bool RequiresProtection(AuthFlowContext auth, UAuthCsrfOperation operation) + { + return RequiresProtection(operation, + UAuthCsrfResponseInspector.CanIssueAuthenticationCookie(auth.Response), + auth.Options.SessionResolution.EnableCookie); + } + + internal static bool RequiresProtection(UAuthCsrfOperation operation, bool canIssueCookie, bool canAcceptCookie) + { + return operation switch + { + UAuthCsrfOperation.Login + or UAuthCsrfOperation.PkceCompletion + or UAuthCsrfOperation.AuthenticatedMutation + => canIssueCookie || canAcceptCookie, + + _ => true + }; + } +} diff --git a/src/CodeBeam.UltimateAuth.Server/Security/UAuthCsrfResponseInspector.cs b/src/CodeBeam.UltimateAuth.Server/Security/UAuthCsrfResponseInspector.cs new file mode 100644 index 00000000..8a92cd23 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Security/UAuthCsrfResponseInspector.cs @@ -0,0 +1,14 @@ +using CodeBeam.UltimateAuth.Core.Options; +using CodeBeam.UltimateAuth.Server.Auth; + +namespace CodeBeam.UltimateAuth.Server.Security; + +internal static class UAuthCsrfResponseInspector +{ + public static bool CanIssueAuthenticationCookie(EffectiveAuthResponse response) + { + return response.SessionIdDelivery.Mode == TokenResponseMode.Cookie + || response.AccessTokenDelivery.Mode == TokenResponseMode.Cookie + || response.RefreshTokenDelivery.Mode == TokenResponseMode.Cookie; + } +} diff --git a/src/CodeBeam.UltimateAuth.Server/Security/UAuthCsrfTransport.cs b/src/CodeBeam.UltimateAuth.Server/Security/UAuthCsrfTransport.cs new file mode 100644 index 00000000..632927b1 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Security/UAuthCsrfTransport.cs @@ -0,0 +1,18 @@ +namespace CodeBeam.UltimateAuth.Server.Security; + +internal readonly record struct UAuthCsrfTransport(bool CanIssueAuthenticationCookie, bool CanConsumeAuthenticationCookie, bool IsVerifiedHeaderOnly) +{ + public bool RequiresProtection(UAuthCsrfOperation operation) + { + return operation switch + { + UAuthCsrfOperation.Login or UAuthCsrfOperation.PkceCompletion + => CanIssueAuthenticationCookie, + + UAuthCsrfOperation.AuthenticatedMutation + => CanConsumeAuthenticationCookie || CanIssueAuthenticationCookie, + + _ => true + }; + } +} diff --git a/src/CodeBeam.UltimateAuth.Server/Security/UAuthCsrfValidator.cs b/src/CodeBeam.UltimateAuth.Server/Security/UAuthCsrfValidator.cs new file mode 100644 index 00000000..7ba7d563 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Security/UAuthCsrfValidator.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Antiforgery; +using Microsoft.AspNetCore.Http; + +namespace CodeBeam.UltimateAuth.Server.Security; + +internal sealed class UAuthCsrfValidator(IAntiforgery antiforgery) +{ + public async Task ValidateAsync(HttpContext context) + { + try + { + await antiforgery.ValidateRequestAsync(context); + return true; + } + catch (AntiforgeryValidationException) + { + return false; + } + } +} diff --git a/src/client/CodeBeam.UltimateAuth.Client.Blazor/Infrastructure/BrowserUAuthBridge.cs b/src/client/CodeBeam.UltimateAuth.Client.Blazor/Infrastructure/BrowserUAuthBridge.cs index 31d28d09..57c31373 100644 --- a/src/client/CodeBeam.UltimateAuth.Client.Blazor/Infrastructure/BrowserUAuthBridge.cs +++ b/src/client/CodeBeam.UltimateAuth.Client.Blazor/Infrastructure/BrowserUAuthBridge.cs @@ -16,4 +16,15 @@ public ValueTask SetDeviceIdAsync(string deviceId) { return _js.InvokeVoidAsync("uauth.setDeviceId", deviceId); } + + public ValueTask ConfigureAntiforgeryAsync(string tokenUrl, string? trustedOrigin = null) + { + return _js.InvokeVoidAsync( + "uauth.antiforgery.configure", + new + { + tokenUrl, + trustedOrigin + }); + } } diff --git a/src/client/CodeBeam.UltimateAuth.Client.Blazor/TScripts/uauth.js b/src/client/CodeBeam.UltimateAuth.Client.Blazor/TScripts/uauth.js index f711c7fa..b2710b91 100644 --- a/src/client/CodeBeam.UltimateAuth.Client.Blazor/TScripts/uauth.js +++ b/src/client/CodeBeam.UltimateAuth.Client.Blazor/TScripts/uauth.js @@ -34,7 +34,10 @@ window.uauth.storage = { } }; -window.uauth.submitForm = function (form) { +window.uauth.submitForm = async function ( + form, + trustedOrigin = null) { + if (!form) return; @@ -42,15 +45,31 @@ window.uauth.submitForm = function (form) { throw new Error("UAuth deviceId is not initialized."); } - let udid = form.querySelector("input[name='__uauth_device']"); + const validatedUrl = window.uauth.validateAuthUrl( + form.action, + trustedOrigin + ); + + form.action = validatedUrl; + + let udid = form.querySelector( + "input[name='__uauth_device']" + ); + if (!udid) { udid = document.createElement("input"); udid.type = "hidden"; udid.name = "__uauth_device"; form.appendChild(udid); } + udid.value = window.uauth.deviceId; + await window.uauth.antiforgery.appendToForm( + form, + trustedOrigin + ); + form.submit(); }; @@ -158,7 +177,13 @@ window.uauth.tryAndCommit = async function (options) { form.appendChild(udid); document.body.appendChild(form); - form.submit(); + try { + await window.uauth.submitForm(form, trustedOrigin); + } + catch (error) { + form.remove(); + throw error; + } } return normalized; @@ -206,14 +231,20 @@ window.uauth.post = async function (options) { } document.body.appendChild(form); - form.submit(); + try { + await window.uauth.submitForm(form, trustedOrigin); + } + catch (error) { + form.remove(); + throw error; + } return null; } const isLogout = mode === "logout" || mode === "logout-observe"; - + return window.uauth.request({ url: validatedUrl, method: "POST", @@ -222,6 +253,8 @@ window.uauth.post = async function (options) { contentType: "form", credentials: "include", allowCrossOriginCredentials: true, + antiforgery: true, + trustedOrigin, navigateOnSuccess: mode === "logout", headers: isLogout ? { "X-UAuth-Logout-Mode": "observe" } @@ -242,7 +275,9 @@ window.uauth.postJson = function (options) { clientProfile: options.clientProfile, contentType: "json", credentials: "include", - allowCrossOriginCredentials: true + allowCrossOriginCredentials: true, + antiforgery: true, + trustedOrigin: options.trustedOrigin }); }; @@ -321,6 +356,20 @@ window.uauth.request = async function (options) { } } + if (requestMethod !== "GET" && options.antiforgery === true) { + const tokenUrl = window.uauth.validateAuthUrl( + window.uauth.antiforgery.tokenUrl, + options.trustedOrigin + ); + + if (new URL(tokenUrl).origin !== requestUrl.origin) { + throw new Error("UAuth antiforgery token origin differs from request origin."); + } + + const token = await window.uauth.antiforgery.getToken(options.trustedOrigin); + headers[token.headerName] = token.requestToken; + } + let response; try { @@ -597,6 +646,69 @@ window.uauth.validateAuthUrl = function (url, trustedOrigin = null) { return target.href; }; +window.uauth.antiforgery = { + tokenUrl: null, + + configure: function (options) { + if (!options?.tokenUrl) { + throw new Error("UAuth antiforgery token URL is required."); + } + + this.tokenUrl = window.uauth.validateAuthUrl( + options.tokenUrl, + options.trustedOrigin + ); + }, + + getToken: async function (trustedOrigin) { + if (!this.tokenUrl) { + throw new Error("UAuth antiforgery is not configured."); + } + + const url = window.uauth.validateAuthUrl( + this.tokenUrl, + trustedOrigin + ); + + const response = await fetch(url, { + method: "GET", + credentials: "include", + cache: "no-store", + redirect: "error" + }); + + if (!response.ok) { + throw new Error(`Unable to obtain UAuth antiforgery token: HTTP ${response.status}.`); + } + + const token = await response.json(); + + if (!token?.requestToken || + !token?.headerName || + !token?.formFieldName) { + throw new Error("Invalid UAuth antiforgery response."); + } + + return token; + }, + + appendToForm: async function (form, trustedOrigin) { + const token = await this.getToken(trustedOrigin); + + let input = Array.from(form.elements) + .find(element => element.name === token.formFieldName); + + if (!input) { + input = document.createElement("input"); + input.type = "hidden"; + input.name = token.formFieldName; + form.appendChild(input); + } + + input.value = token.requestToken; + } +}; + window.uauth.setDeviceId = function (value) { window.uauth.deviceId = value; }; diff --git a/src/client/CodeBeam.UltimateAuth.Client.Blazor/wwwroot/uauth.min.js b/src/client/CodeBeam.UltimateAuth.Client.Blazor/wwwroot/uauth.min.js index a15a206d..9c32b1a2 100644 --- a/src/client/CodeBeam.UltimateAuth.Client.Blazor/wwwroot/uauth.min.js +++ b/src/client/CodeBeam.UltimateAuth.Client.Blazor/wwwroot/uauth.min.js @@ -1 +1 @@ -window.uauth=window.uauth||{};window.uauth.storage={set:function(n,t,i){const r=n==="local"?window.localStorage:window.sessionStorage;r.setItem(t,i)},get:function(n,t){const i=n==="local"?window.localStorage:window.sessionStorage;return i.getItem(t)},remove:function(n,t){const i=n==="local"?window.localStorage:window.sessionStorage;i.removeItem(t)},exists:function(n,t){const i=n==="local"?window.localStorage:window.sessionStorage;return i.getItem(t)!==null}};window.uauth.submitForm=function(n){if(!n)return;if(!window.uauth.deviceId)throw new Error("UAuth deviceId is not initialized.");let t=n.querySelector("input[name='__uauth_device']");t||(t=document.createElement("input"),t.type="hidden",t.name="__uauth_device",n.appendChild(t));t.value=window.uauth.deviceId;n.submit()};window.uauth.tryAndCommit=async function(n){const{tryUrl:c,commitUrl:l,data:f,clientProfile:o,trustedOrigin:e}=n,a=window.uauth.validateAuthUrl(c,e),v=window.uauth.validateAuthUrl(l,e),s=window.uauth.deviceId;if(!s)throw new Error("UAuth deviceId is not initialized.");const r=await window.uauth.postJson({url:a,payload:f,clientProfile:o,trustedOrigin:e});if(!r||!Number.isInteger(r.status))throw new Error("Invalid UAuth transport response.");const i=r.status;if(i===429)return{isSuccess:!1,status:429,errorCode:"rate_limited",retryAfter:r.retryAfter??null};const y=i>=200&&i<300,h=i>=400&&i<500;if(!y&&!h)throw new Error(`Unexpected UAuth try response: HTTP ${i}.`);const t=r.body;if(!t||typeof t!="object"||Array.isArray(t)||typeof t.isSuccess!="boolean")throw new Error("Invalid UAuth try response body.");if(h&&t.isSuccess)throw new Error("Successful authentication result returned with HTTP error.");const u={isSuccess:t.isSuccess,reason:t.reason??null,remainingAttempts:t.remainingAttempts??null,lockoutUntilUtc:t.lockoutUntilUtc??null,requiresMfa:t.requiresMfa??!1,retryWithNewPkce:t.retryWithNewPkce??!1,previewReceipt:t.previewReceipt??null};if(u.isSuccess){const n=document.createElement("form");n.method="POST";n.action=v;for(const t of Object.keys(f)){const i=document.createElement("input");i.type="hidden";i.name=t;i.value=f[t]??"";n.appendChild(i)}if(u.previewReceipt){const t=document.createElement("input");t.type="hidden";t.name="PreviewReceipt";t.value=u.previewReceipt;n.appendChild(t)}const t=document.createElement("input");t.type="hidden";t.name="__uauth_client_profile";t.value=o??"";n.appendChild(t);const i=document.createElement("input");i.type="hidden";i.name="__uauth_device";i.value=s;n.appendChild(i);document.body.appendChild(n);n.submit()}return u};window.uauth.post=async function(n){const{url:f,mode:t,data:i,clientProfile:r,trustedOrigin:e}=n,u=window.uauth.validateAuthUrl(f,e);if(t==="navigate"){const n=document.createElement("form");n.method="POST";n.action=u;const t=document.createElement("input");t.type="hidden";t.name="__uauth_client_profile";t.value=r??"";n.appendChild(t);const f=document.createElement("input");if(f.type="hidden",f.name="__uauth_device",f.value=window.uauth.deviceId,n.appendChild(f),i)for(const t in i){const r=document.createElement("input");r.type="hidden";r.name=t;r.value=i[t];n.appendChild(r)}return document.body.appendChild(n),n.submit(),null}const o=t==="logout"||t==="logout-observe";return window.uauth.request({url:u,method:"POST",payload:i,clientProfile:r,contentType:"form",credentials:"include",allowCrossOriginCredentials:!0,navigateOnSuccess:t==="logout",headers:o?{"X-UAuth-Logout-Mode":"observe"}:{}})};window.uauth.postJson=function(n){const t=window.uauth.validateAuthUrl(n.url,n.trustedOrigin);return window.uauth.request({url:t,method:"POST",payload:n.payload,clientProfile:n.clientProfile,contentType:"json",credentials:"include",allowCrossOriginCredentials:!0})};window.uauth.request=async function(n){const{followRedirects:a=!1,navigateOnSuccess:v=!1,url:h,method:y="GET",payload:i=null,clientProfile:p="",contentType:c="json",credentials:e="same-origin",headers:w={}}=n;if(!h)throw new Error("Request URL is required.");const o=new URL(h,document.baseURI);if(!["http:","https:"].includes(o.protocol))throw new Error("Unsupported request URL protocol.");const r=y.toUpperCase();if(!["GET","POST","PUT","PATCH","DELETE"].includes(r))throw new Error(`Unsupported HTTP method: ${r}`);if(!["same-origin","include","omit"].includes(e))throw new Error("Invalid credentials policy.");if(e==="include"&&o.origin!==window.location.origin&&!n.allowCrossOriginCredentials)throw new Error("Cross-origin credentials require explicit authorization.");if(!window.uauth.deviceId)throw new Error("UAuth deviceId is not initialized.");const s={...w,"X-UDID":window.uauth.deviceId,"X-UAuth-ClientProfile":p,"X-Requested-With":"UAuth"};let u=null;if(i!==null&&i!==undefined){if(r==="GET")throw new Error("GET requests cannot contain a body.");if(c==="json")s["Content-Type"]="application/json",u=JSON.stringify(i);else if(c==="form"){s["Content-Type"]="application/x-www-form-urlencoded";u=new URLSearchParams;for(const[n,t]of Object.entries(i))u.append(n,t??"")}else throw new Error("Unsupported content type.");}let t;try{t=await fetch(o.href,{method:r,credentials:e,redirect:a?"follow":"error",headers:s,body:u})}catch(l){if(l instanceof TypeError)return{ok:!1,status:0,refreshOutcome:null,body:null};throw l;}let f=null;if(t.status!==204&&t.status!==205){const n=await t.text();if(n)try{f=JSON.parse(n)}catch{f=n}}if(v&&t.ok){const n=f?.redirectUrl;if(typeof n=="string"&&n.length>0){const t=new URL(n,window.location.origin);if(!["http:","https:"].includes(t.protocol))throw new Error("Invalid logout redirect protocol.");window.location.assign(t.href)}}return{retryAfter:t.headers.get("Retry-After"),ok:t.ok,status:t.status,refreshOutcome:t.headers.get("X-UAuth-Refresh"),body:f}};window.uauth.resourceRequest=async function(n){const{url:o,baseAddress:h,method:v="GET",payload:f=null,clientProfile:y="",credentials:p="Auto",requestId:u}=n;if(!o||typeof o!="string")throw new Error("Resource request URL is required.");const i=h?new URL(h):new URL(window.location.origin+"/");if(!["http:","https:"].includes(i.protocol))throw new Error("Invalid Resource API protocol.");if(i.username||i.password||i.search||i.hash)throw new Error("Invalid Resource API base address.");const t=new URL(o,i);if(t.origin!==i.origin)throw new Error("Resource request target differs from configured API origin.");if(t.username||t.password)throw new Error("Resource URL cannot contain credentials.");if(!["http:","https:"].includes(t.protocol))throw new Error("Unsupported Resource API protocol.");const s=v.toUpperCase();if(!["GET","POST","PUT","PATCH","DELETE"].includes(s))throw new Error("Unsupported Resource API method.");if(s==="GET"&&f!==null)throw new Error("GET requests cannot contain a body.");let r;switch(p.toLowerCase()){case"auto":r=t.origin===window.location.origin?"same-origin":"include";break;case"sameorigin":r="same-origin";break;case"include":r="include";break;case"omit":r="omit";break;default:throw new Error("Invalid Resource credentials mode.");}if(r==="include"&&t.protocol!=="https:"&&!["localhost","127.0.0.1","[::1]"].includes(t.hostname))throw new Error("Cross-origin credential requests require HTTPS.");if(!window.uauth.deviceId)throw new Error("UAuth deviceId is not initialized.");const c={"X-UDID":window.uauth.deviceId,"X-UAuth-ClientProfile":y,"X-Requested-With":"UAuth"};let l=null;f!==null&&f!==undefined&&(c["Content-Type"]="application/json",l=JSON.stringify(f));const a=new AbortController;if(u){if(window.uauth.resourceRequests.has(u))throw new Error("Duplicate Resource request ID.");window.uauth.resourceRequests.set(u,a)}try{const n=await fetch(t.href,{method:s,credentials:r,redirect:"error",headers:c,body:l,signal:a.signal});let i=null;if(n.status!==204&&n.status!==205){const t=await n.text();t.length>0&&(i=t)}return{retryAfter:n.headers.get("Retry-After"),ok:n.ok,status:n.status,body:i,contentType:n.headers.get("Content-Type")}}catch(e){if(e?.name==="AbortError")throw e;if(e instanceof TypeError)return{ok:!1,status:0,body:null,contentType:null};throw e;}finally{u&&window.uauth.resourceRequests.delete(u)}};window.uauth.resourceRequests=new Map;window.uauth.cancelResourceRequest=function(n){const t=window.uauth.resourceRequests.get(n);return t?(t.abort(),!0):!1};window.uauth.validateAuthUrl=function(n,t=null){if(!n||typeof n!="string")throw new Error("Authentication URL is required.");const r=t?new URL(t,window.location.origin):new URL(window.location.origin),i=new URL(n,document.baseURI);if(!["http:","https:"].includes(r.protocol))throw new Error("Unsupported trusted origin protocol.");if(!["http:","https:"].includes(i.protocol))throw new Error("Unsupported authentication URL protocol.");if(r.username||r.password)throw new Error("Trusted origin cannot contain credentials.");if(i.username||i.password)throw new Error("Authentication URL cannot contain credentials.");if(i.origin!==r.origin)throw new Error("Authentication request target differs from the trusted UAuthHub origin.");return i.href};window.uauth.setDeviceId=function(n){window.uauth.deviceId=n};window.uauth.getDeviceInfo=function(){return{userAgent:navigator.userAgent,platform:navigator.platform,language:navigator.language}} \ No newline at end of file +window.uauth=window.uauth||{};window.uauth.storage={set:function(n,t,i){const r=n==="local"?window.localStorage:window.sessionStorage;r.setItem(t,i)},get:function(n,t){const i=n==="local"?window.localStorage:window.sessionStorage;return i.getItem(t)},remove:function(n,t){const i=n==="local"?window.localStorage:window.sessionStorage;i.removeItem(t)},exists:function(n,t){const i=n==="local"?window.localStorage:window.sessionStorage;return i.getItem(t)!==null}};window.uauth.submitForm=async function(n,t=null){if(!n)return;if(!window.uauth.deviceId)throw new Error("UAuth deviceId is not initialized.");const r=window.uauth.validateAuthUrl(n.action,t);n.action=r;let i=n.querySelector("input[name='__uauth_device']");i||(i=document.createElement("input"),i.type="hidden",i.name="__uauth_device",n.appendChild(i));i.value=window.uauth.deviceId;await window.uauth.antiforgery.appendToForm(n,t);n.submit()};window.uauth.tryAndCommit=async function(n){const{tryUrl:c,commitUrl:l,data:e,clientProfile:o,trustedOrigin:u}=n,a=window.uauth.validateAuthUrl(c,u),v=window.uauth.validateAuthUrl(l,u),s=window.uauth.deviceId;if(!s)throw new Error("UAuth deviceId is not initialized.");const r=await window.uauth.postJson({url:a,payload:e,clientProfile:o,trustedOrigin:u});if(!r||!Number.isInteger(r.status))throw new Error("Invalid UAuth transport response.");const i=r.status;if(i===429)return{isSuccess:!1,status:429,errorCode:"rate_limited",retryAfter:r.retryAfter??null};const y=i>=200&&i<300,h=i>=400&&i<500;if(!y&&!h)throw new Error(`Unexpected UAuth try response: HTTP ${i}.`);const t=r.body;if(!t||typeof t!="object"||Array.isArray(t)||typeof t.isSuccess!="boolean")throw new Error("Invalid UAuth try response body.");if(h&&t.isSuccess)throw new Error("Successful authentication result returned with HTTP error.");const f={isSuccess:t.isSuccess,reason:t.reason??null,remainingAttempts:t.remainingAttempts??null,lockoutUntilUtc:t.lockoutUntilUtc??null,requiresMfa:t.requiresMfa??!1,retryWithNewPkce:t.retryWithNewPkce??!1,previewReceipt:t.previewReceipt??null};if(f.isSuccess){const n=document.createElement("form");n.method="POST";n.action=v;for(const t of Object.keys(e)){const i=document.createElement("input");i.type="hidden";i.name=t;i.value=e[t]??"";n.appendChild(i)}if(f.previewReceipt){const t=document.createElement("input");t.type="hidden";t.name="PreviewReceipt";t.value=f.previewReceipt;n.appendChild(t)}const t=document.createElement("input");t.type="hidden";t.name="__uauth_client_profile";t.value=o??"";n.appendChild(t);const i=document.createElement("input");i.type="hidden";i.name="__uauth_device";i.value=s;n.appendChild(i);document.body.appendChild(n);try{await window.uauth.submitForm(n,u)}catch(r){n.remove();throw r;}}return f};window.uauth.post=async function(n){const{url:e,mode:t,data:i,clientProfile:u,trustedOrigin:r}=n,f=window.uauth.validateAuthUrl(e,r);if(t==="navigate"){const n=document.createElement("form");n.method="POST";n.action=f;const t=document.createElement("input");t.type="hidden";t.name="__uauth_client_profile";t.value=u??"";n.appendChild(t);const e=document.createElement("input");if(e.type="hidden",e.name="__uauth_device",e.value=window.uauth.deviceId,n.appendChild(e),i)for(const t in i){const r=document.createElement("input");r.type="hidden";r.name=t;r.value=i[t];n.appendChild(r)}document.body.appendChild(n);try{await window.uauth.submitForm(n,r)}catch(o){n.remove();throw o;}return null}const o=t==="logout"||t==="logout-observe";return window.uauth.request({url:f,method:"POST",payload:i,clientProfile:u,contentType:"form",credentials:"include",allowCrossOriginCredentials:!0,antiforgery:!0,trustedOrigin:r,navigateOnSuccess:t==="logout",headers:o?{"X-UAuth-Logout-Mode":"observe"}:{}})};window.uauth.postJson=function(n){const t=window.uauth.validateAuthUrl(n.url,n.trustedOrigin);return window.uauth.request({url:t,method:"POST",payload:n.payload,clientProfile:n.clientProfile,contentType:"json",credentials:"include",allowCrossOriginCredentials:!0,antiforgery:!0,trustedOrigin:n.trustedOrigin})};window.uauth.request=async function(n){const{followRedirects:a=!1,navigateOnSuccess:v=!1,url:h,method:y="GET",payload:r=null,clientProfile:p="",contentType:c="json",credentials:s="same-origin",headers:w={}}=n;if(!h)throw new Error("Request URL is required.");const u=new URL(h,document.baseURI);if(!["http:","https:"].includes(u.protocol))throw new Error("Unsupported request URL protocol.");const i=y.toUpperCase();if(!["GET","POST","PUT","PATCH","DELETE"].includes(i))throw new Error(`Unsupported HTTP method: ${i}`);if(!["same-origin","include","omit"].includes(s))throw new Error("Invalid credentials policy.");if(s==="include"&&u.origin!==window.location.origin&&!n.allowCrossOriginCredentials)throw new Error("Cross-origin credentials require explicit authorization.");if(!window.uauth.deviceId)throw new Error("UAuth deviceId is not initialized.");const f={...w,"X-UDID":window.uauth.deviceId,"X-UAuth-ClientProfile":p,"X-Requested-With":"UAuth"};let e=null;if(r!==null&&r!==undefined){if(i==="GET")throw new Error("GET requests cannot contain a body.");if(c==="json")f["Content-Type"]="application/json",e=JSON.stringify(r);else if(c==="form"){f["Content-Type"]="application/x-www-form-urlencoded";e=new URLSearchParams;for(const[n,t]of Object.entries(r))e.append(n,t??"")}else throw new Error("Unsupported content type.");}if(i!=="GET"&&n.antiforgery===!0){const i=window.uauth.validateAuthUrl(window.uauth.antiforgery.tokenUrl,n.trustedOrigin);if(new URL(i).origin!==u.origin)throw new Error("UAuth antiforgery token origin differs from request origin.");const t=await window.uauth.antiforgery.getToken(n.trustedOrigin);f[t.headerName]=t.requestToken}let t;try{t=await fetch(u.href,{method:i,credentials:s,redirect:a?"follow":"error",headers:f,body:e})}catch(l){if(l instanceof TypeError)return{ok:!1,status:0,refreshOutcome:null,body:null};throw l;}let o=null;if(t.status!==204&&t.status!==205){const n=await t.text();if(n)try{o=JSON.parse(n)}catch{o=n}}if(v&&t.ok){const n=o?.redirectUrl;if(typeof n=="string"&&n.length>0){const t=new URL(n,window.location.origin);if(!["http:","https:"].includes(t.protocol))throw new Error("Invalid logout redirect protocol.");window.location.assign(t.href)}}return{retryAfter:t.headers.get("Retry-After"),ok:t.ok,status:t.status,refreshOutcome:t.headers.get("X-UAuth-Refresh"),body:o}};window.uauth.resourceRequest=async function(n){const{url:o,baseAddress:h,method:v="GET",payload:f=null,clientProfile:y="",credentials:p="Auto",requestId:u}=n;if(!o||typeof o!="string")throw new Error("Resource request URL is required.");const i=h?new URL(h):new URL(window.location.origin+"/");if(!["http:","https:"].includes(i.protocol))throw new Error("Invalid Resource API protocol.");if(i.username||i.password||i.search||i.hash)throw new Error("Invalid Resource API base address.");const t=new URL(o,i);if(t.origin!==i.origin)throw new Error("Resource request target differs from configured API origin.");if(t.username||t.password)throw new Error("Resource URL cannot contain credentials.");if(!["http:","https:"].includes(t.protocol))throw new Error("Unsupported Resource API protocol.");const s=v.toUpperCase();if(!["GET","POST","PUT","PATCH","DELETE"].includes(s))throw new Error("Unsupported Resource API method.");if(s==="GET"&&f!==null)throw new Error("GET requests cannot contain a body.");let r;switch(p.toLowerCase()){case"auto":r=t.origin===window.location.origin?"same-origin":"include";break;case"sameorigin":r="same-origin";break;case"include":r="include";break;case"omit":r="omit";break;default:throw new Error("Invalid Resource credentials mode.");}if(r==="include"&&t.protocol!=="https:"&&!["localhost","127.0.0.1","[::1]"].includes(t.hostname))throw new Error("Cross-origin credential requests require HTTPS.");if(!window.uauth.deviceId)throw new Error("UAuth deviceId is not initialized.");const c={"X-UDID":window.uauth.deviceId,"X-UAuth-ClientProfile":y,"X-Requested-With":"UAuth"};let l=null;f!==null&&f!==undefined&&(c["Content-Type"]="application/json",l=JSON.stringify(f));const a=new AbortController;if(u){if(window.uauth.resourceRequests.has(u))throw new Error("Duplicate Resource request ID.");window.uauth.resourceRequests.set(u,a)}try{const n=await fetch(t.href,{method:s,credentials:r,redirect:"error",headers:c,body:l,signal:a.signal});let i=null;if(n.status!==204&&n.status!==205){const t=await n.text();t.length>0&&(i=t)}return{retryAfter:n.headers.get("Retry-After"),ok:n.ok,status:n.status,body:i,contentType:n.headers.get("Content-Type")}}catch(e){if(e?.name==="AbortError")throw e;if(e instanceof TypeError)return{ok:!1,status:0,body:null,contentType:null};throw e;}finally{u&&window.uauth.resourceRequests.delete(u)}};window.uauth.resourceRequests=new Map;window.uauth.cancelResourceRequest=function(n){const t=window.uauth.resourceRequests.get(n);return t?(t.abort(),!0):!1};window.uauth.validateAuthUrl=function(n,t=null){if(!n||typeof n!="string")throw new Error("Authentication URL is required.");const r=t?new URL(t,window.location.origin):new URL(window.location.origin),i=new URL(n,document.baseURI);if(!["http:","https:"].includes(r.protocol))throw new Error("Unsupported trusted origin protocol.");if(!["http:","https:"].includes(i.protocol))throw new Error("Unsupported authentication URL protocol.");if(r.username||r.password)throw new Error("Trusted origin cannot contain credentials.");if(i.username||i.password)throw new Error("Authentication URL cannot contain credentials.");if(i.origin!==r.origin)throw new Error("Authentication request target differs from the trusted UAuthHub origin.");return i.href};window.uauth.antiforgery={tokenUrl:null,configure:function(n){if(!n?.tokenUrl)throw new Error("UAuth antiforgery token URL is required.");this.tokenUrl=window.uauth.validateAuthUrl(n.tokenUrl,n.trustedOrigin)},getToken:async function(n){if(!this.tokenUrl)throw new Error("UAuth antiforgery is not configured.");const r=window.uauth.validateAuthUrl(this.tokenUrl,n),i=await fetch(r,{method:"GET",credentials:"include",cache:"no-store",redirect:"error"});if(!i.ok)throw new Error(`Unable to obtain UAuth antiforgery token: HTTP ${i.status}.`);const t=await i.json();if(!t?.requestToken||!t?.headerName||!t?.formFieldName)throw new Error("Invalid UAuth antiforgery response.");return t},appendToForm:async function(n,t){const r=await this.getToken(t);let i=Array.from(n.elements).find(n=>n.name===r.formFieldName);i||(i=document.createElement("input"),i.type="hidden",i.name=r.formFieldName,n.appendChild(i));i.value=r.requestToken}};window.uauth.setDeviceId=function(n){window.uauth.deviceId=n};window.uauth.getDeviceInfo=function(){return{userAgent:navigator.userAgent,platform:navigator.platform,language:navigator.language}} \ No newline at end of file diff --git a/src/client/CodeBeam.UltimateAuth.Client/Infrastructure/IBrowserUAuthBridge.cs b/src/client/CodeBeam.UltimateAuth.Client/Infrastructure/IBrowserUAuthBridge.cs index 54798cf1..5cbe9fc3 100644 --- a/src/client/CodeBeam.UltimateAuth.Client/Infrastructure/IBrowserUAuthBridge.cs +++ b/src/client/CodeBeam.UltimateAuth.Client/Infrastructure/IBrowserUAuthBridge.cs @@ -9,4 +9,6 @@ public interface IBrowserUAuthBridge /// Sets the device ID in the browser's local storage or cookies. /// ValueTask SetDeviceIdAsync(string deviceId); + + ValueTask ConfigureAntiforgeryAsync(string tokenUrl, string? trustedOrigin = null); } diff --git a/src/client/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthClientBootstrapper.cs b/src/client/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthClientBootstrapper.cs index c70a0375..39203317 100644 --- a/src/client/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthClientBootstrapper.cs +++ b/src/client/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthClientBootstrapper.cs @@ -1,6 +1,12 @@ -namespace CodeBeam.UltimateAuth.Client.Infrastructure; +using CodeBeam.UltimateAuth.Client.Options; +using Microsoft.Extensions.Options; + +namespace CodeBeam.UltimateAuth.Client.Infrastructure; // TODO: Add device id auto creation for MVC, this is only for blazor. +// TODO: Support dynamic tenant changes during the application lifetime. +// Antiforgery configuration is currently initialized only once. +// Reconfigure the token URL when the active tenant changes. internal sealed class UAuthClientBootstrapper : IUAuthClientBootstrapper { private readonly SemaphoreSlim _gate = new(1, 1); @@ -8,13 +14,15 @@ internal sealed class UAuthClientBootstrapper : IUAuthClientBootstrapper private readonly IDeviceIdProvider _deviceIdProvider; private readonly IBrowserUAuthBridge _browser; + private readonly UAuthClientOptions _options; public bool IsStarted => _started; - public UAuthClientBootstrapper(IDeviceIdProvider deviceIdProvider, IBrowserUAuthBridge browser) + public UAuthClientBootstrapper(IDeviceIdProvider deviceIdProvider, IBrowserUAuthBridge browser, IOptions options) { _deviceIdProvider = deviceIdProvider; _browser = browser; + _options = options.Value; } public async Task EnsureStartedAsync(CancellationToken ct = default) @@ -23,14 +31,34 @@ public async Task EnsureStartedAsync(CancellationToken ct = default) return; await _gate.WaitAsync(ct); + try { if (_started) return; var deviceId = await _deviceIdProvider.GetOrCreateAsync(); + + var basePath = _options.Endpoints.BasePath; + + var tokenUrl = UAuthUrlBuilder.Build(basePath, _options.Endpoints.AntiforgeryToken, _options.MultiTenant); + + string? trustedOrigin = null; + + if (Uri.TryCreate(basePath, UriKind.Absolute, out var hubUri)) + { + if (hubUri.Scheme != Uri.UriSchemeHttp && hubUri.Scheme != Uri.UriSchemeHttps) + { + throw new InvalidOperationException("UAuthHub URL must use HTTP or HTTPS."); + } + + trustedOrigin = hubUri.GetLeftPart(UriPartial.Authority); + } + await _browser.SetDeviceIdAsync(deviceId.Value); + await _browser.ConfigureAntiforgeryAsync(tokenUrl, trustedOrigin); + _started = true; } finally diff --git a/src/client/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthUrlBuilder.cs b/src/client/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthUrlBuilder.cs index 41ff2612..b17402d6 100644 --- a/src/client/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthUrlBuilder.cs +++ b/src/client/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthUrlBuilder.cs @@ -20,6 +20,9 @@ public static string Build(string authority, string relativePath, UAuthClientMul { var baseAuthority = authority.TrimEnd('/'); + // TODO: Correct route-based tenant URL construction when authority + // is an absolute URL (e.g. https://localhost:6110/auth). + // Preserve the scheme and host, and insert the tenant into the path. if (tenant.Enabled && tenant.Transport == TenantTransport.Route) { if (string.IsNullOrWhiteSpace(tenant.Tenant)) diff --git a/src/client/CodeBeam.UltimateAuth.Client/Options/UAuthClientEndpointOptions.cs b/src/client/CodeBeam.UltimateAuth.Client/Options/UAuthClientEndpointOptions.cs index 63cffc75..00545954 100644 --- a/src/client/CodeBeam.UltimateAuth.Client/Options/UAuthClientEndpointOptions.cs +++ b/src/client/CodeBeam.UltimateAuth.Client/Options/UAuthClientEndpointOptions.cs @@ -55,6 +55,11 @@ public sealed class UAuthClientEndpointOptions /// public string PkceComplete { get; set; } = "/pkce/complete"; + /// + /// Path for obtaining an antiforgery request token. + /// + public string AntiforgeryToken { get; set; } = "/antiforgery/token"; + /// /// Path for the UAuthHub login endpoint (e.g. /uauthhub/entry) /// diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/AuthorizationManagementTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/AuthorizationManagementTests.cs index 9e1a9ff9..a4171484 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration/AuthorizationManagementTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/AuthorizationManagementTests.cs @@ -39,7 +39,7 @@ public async Task AssignRole_TargetMismatch_ShouldRejectWithoutMutation() using var client = await AuthenticatedClientAsync(factory, admin); - using var response = await client.PostAsJsonAsync( + using var response = await CsrfTestClient.PostJsonAsync(client, UserRolesUrl(target.UserKey, "assign"), new AssignRoleRequest { @@ -206,7 +206,7 @@ await GrantAsync(factory, admin, using var client = await AuthenticatedClientAsync(factory, admin); - using var response = await client.PostAsJsonAsync( + using var response = await CsrfTestClient.PostJsonAsync(client, $"{AdminBase}/roles/create", new CreateRoleRequest { @@ -238,7 +238,8 @@ await GrantAsync(factory, admin, using var client = await AuthenticatedClientAsync(factory, admin); - using var response = await client.PostAsJsonAsync( + using var response = await CsrfTestClient.PostJsonAsync( + client, RoleUrl(role.Id, "rename"), new RenameRoleRequest { @@ -277,7 +278,8 @@ await GrantAsync( using var client = await AuthenticatedClientAsync(factory, admin); - using var response = await client.PostAsJsonAsync( + using var response = await CsrfTestClient.PostJsonAsync( + client, RoleUrl(role.Id, "permissions"), new SetRolePermissionsRequest { @@ -317,7 +319,7 @@ await GrantAsync(factory, admin, using var client = await AuthenticatedClientAsync(factory, admin); - using var response = await client.PostAsJsonAsync( + using var response = await CsrfTestClient.PostJsonAsync(client, UserRolesUrl(target.UserKey, "assign"), new AssignRoleRequest { @@ -348,7 +350,8 @@ await GrantAsync(factory, admin, using var client = await AuthenticatedClientAsync(factory, admin); - using var response = await client.PostAsJsonAsync( + using var response = await CsrfTestClient.PostJsonAsync( + client, UserRolesUrl(target.UserKey, "remove"), new RemoveRoleRequest { @@ -379,7 +382,7 @@ await GrantAsync(factory, admin, using var client = await AuthenticatedClientAsync(factory, admin); - using var response = await client.PostAsJsonAsync( + using var response = await CsrfTestClient.PostJsonAsync(client, RoleUrl(role.Id, "delete"), new DeleteRoleRequest { @@ -508,7 +511,7 @@ public async Task AssignRole_Anonymous_ShouldReject() using var client = CreateClient(factory); - using var response = await client.PostAsJsonAsync( + using var response = await CsrfTestClient.PostJsonAsync(client, UserRolesUrl(target.UserKey, "assign"), new AssignRoleRequest { @@ -533,7 +536,7 @@ public async Task AssignRole_WithoutPermission_ShouldNotMutate() using var client = await AuthenticatedClientAsync(factory, actor); - using var response = await client.PostAsJsonAsync( + using var response = await CsrfTestClient.PostJsonAsync(client, UserRolesUrl(target.UserKey, "assign"), new AssignRoleRequest { @@ -557,7 +560,7 @@ public async Task CreateRole_WithoutPermission_ShouldNotCreateRole() using var client = await AuthenticatedClientAsync(factory, actor); - using var response = await client.PostAsJsonAsync( + using var response = await CsrfTestClient.PostJsonAsync(client, $"{AdminBase}/roles/create", new CreateRoleRequest { @@ -585,7 +588,7 @@ await GrantAsync( using var client = await AuthenticatedClientAsync(factory, user); - using var response = await client.PostAsJsonAsync( + using var response = await CsrfTestClient.PostJsonAsync(client, $"{SelfBase}/check", new AuthorizationCheckRequest { @@ -611,7 +614,7 @@ public async Task AuthorizationCheck_WithoutPermission_ShouldReturnDenial() using var client = await AuthenticatedClientAsync(factory, user); - using var response = await client.PostAsJsonAsync( + using var response = await CsrfTestClient.PostJsonAsync(client, $"{SelfBase}/check", new AuthorizationCheckRequest { @@ -775,7 +778,7 @@ await SeedAssignmentAsync( using var client = await AuthenticatedClientAsync(factory, user); // First request: the user has the original permission. - using (var response = await client.PostAsJsonAsync( + using (var response = await CsrfTestClient.PostJsonAsync(client, $"{SelfBase}/permissions/get", new { })) { @@ -902,7 +905,7 @@ public async Task GetMyPermissions_WhenNoRoles_ReturnsEmpty() using var client = await AuthenticatedClientAsync(factory, user); - using var response = await client.PostAsJsonAsync( + using var response = await CsrfTestClient.PostJsonAsync(client, $"{SelfBase}/permissions/get", new { }); @@ -936,7 +939,7 @@ await SeedAssignmentAsync( using var client = await AuthenticatedClientAsync(factory, user); - using var response = await client.PostAsJsonAsync( + using var response = await CsrfTestClient.PostJsonAsync(client, $"{SelfBase}/permissions/get", new { }); @@ -967,15 +970,10 @@ private static string UniqueRoleName() private static string RoleUrl(RoleId id, string operation) => $"{AdminBase}/roles/{id.Value}/{operation}"; - private static string UserRolesUrl( - UserKey userKey, - string operation) + private static string UserRolesUrl(UserKey userKey, string operation) => $"{AdminBase}/users/{userKey.Value}/roles/{operation}"; - private static async Task GrantAsync( - AuthServerFactory factory, - IntegrationTestUser user, - params string[] permissions) + private static async Task GrantAsync(AuthServerFactory factory, IntegrationTestUser user, params string[] permissions) { await factory.GrantPermissionsAsync( user.UserKey, @@ -1002,13 +1000,11 @@ private static HttpClient CreateClient(AuthServerFactory factory) return client; } - private static async Task AuthenticatedClientAsync( - AuthServerFactory factory, - IntegrationTestUser user) + private static async Task AuthenticatedClientAsync(AuthServerFactory factory, IntegrationTestUser user) { var client = CreateClient(factory); - using var response = await client.PostAsJsonAsync( + using var response = await CsrfTestClient.PostJsonAsync(client, "/auth/login", new { @@ -1032,9 +1028,7 @@ private static async Task AuthenticatedClientAsync( return client; } - private static async Task SeedRoleAsync( - AuthServerFactory factory, - IEnumerable? permissions = null) + private static async Task SeedRoleAsync(AuthServerFactory factory, IEnumerable? permissions = null) { using var scope = factory.Services.CreateScope(); @@ -1055,9 +1049,7 @@ await storeFactory return role; } - private static async Task ReadRoleAsync( - AuthServerFactory factory, - RoleId roleId) + private static async Task ReadRoleAsync(AuthServerFactory factory, RoleId roleId) { using var scope = factory.Services.CreateScope(); @@ -1069,9 +1061,7 @@ await storeFactory .GetAsync(new RoleKey(TenantKeys.Single, roleId)); } - private static async Task FindRoleByNameAsync( - AuthServerFactory factory, - string name) + private static async Task FindRoleByNameAsync(AuthServerFactory factory, string name) { using var scope = factory.Services.CreateScope(); @@ -1083,10 +1073,7 @@ await storeFactory .GetByNameAsync(name.ToUpperInvariant()); } - private static async Task SeedAssignmentAsync( - AuthServerFactory factory, - UserKey userKey, - RoleId roleId) + private static async Task SeedAssignmentAsync(AuthServerFactory factory, UserKey userKey, RoleId roleId) { using var scope = factory.Services.CreateScope(); @@ -1098,10 +1085,7 @@ await storeFactory .AssignAsync(userKey, roleId, factory.Clock.UtcNow); } - private static async Task HasRoleAsync( - AuthServerFactory factory, - UserKey userKey, - RoleId roleId) + private static async Task HasRoleAsync(AuthServerFactory factory, UserKey userKey, RoleId roleId) { using var scope = factory.Services.CreateScope(); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/ClientRateLimitingIntegrationTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/ClientRateLimitingIntegrationTests.cs index 3baccf5c..f4f61084 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration/ClientRateLimitingIntegrationTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/ClientRateLimitingIntegrationTests.cs @@ -70,7 +70,7 @@ public async Task NativeRefresh429PreservesCredentials_AndSuppressesImmediateRef Assert.Equal(credential, http.DefaultRequestHeaders.GetValues("Cookie").Single()); await flow.RefreshAsync(isAuto: true); Assert.Equal(2, transport.Count); - var session = await http.PostAsync("/auth/validate", null); + var session = await CsrfTestClient.PostAsync(http, "/auth/validate", null); Assert.Equal(HttpStatusCode.OK, session.StatusCode); Assert.True((await session.Content.ReadFromJsonAsync())!.IsValid); } @@ -81,7 +81,7 @@ private static async Task Login(AuthServerFactory factory, Integrati http.DefaultRequestHeaders.Add("Origin", "https://localhost:6130"); http.DefaultRequestHeaders.Add("X-UDID", "client-rate-device-123456789"); http.DefaultRequestHeaders.Add("X-UAuth-ClientProfile", "BlazorServer"); - var login = await http.PostAsJsonAsync("/auth/login", new { identifier = user.Identifier, secret = user.Secret }); + var login = await CsrfTestClient.PostJsonAsync(http, "/auth/login", new { identifier = user.Identifier, secret = user.Secret }); Assert.True(login.Headers.TryGetValues("Set-Cookie", out var cookies)); http.DefaultRequestHeaders.Add("Cookie", string.Join("; ", cookies!.Select(c => c.Split(';')[0]))); return http; @@ -113,13 +113,13 @@ private sealed class HttpTransport(HttpClient http) : IUAuthRequestClient, IUAut public async Task SendFormAsync(string endpoint, IDictionary? form = null, CancellationToken ct = default) { Count++; - using var response = await http.PostAsync(endpoint, form is null ? null : new FormUrlEncodedContent(form), ct); + using var response = await CsrfTestClient.PostAsync(http, endpoint, form is null ? null : new FormUrlEncodedContent(form)); return await Read(response, ct); } public async Task SendJsonAsync(string endpoint, object? payload = null, CancellationToken ct = default) { Count++; - using var response = await http.PostAsJsonAsync(endpoint, payload, ct); + using var response = await CsrfTestClient.PostJsonAsync(http, endpoint, payload); return await Read(response, ct); } private static async Task Read(HttpResponseMessage response, CancellationToken ct) diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/CredentialManagementTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/CredentialManagementTests.cs index 95540fef..fbe24202 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration/CredentialManagementTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/CredentialManagementTests.cs @@ -247,8 +247,7 @@ await factory.GrantPermissionsAsync( using var client = CreateClient(factory); await AuthenticateAsync(client, admin); - var url = - $"/auth/admin/users/{target.UserKey.Value}/credentials/reset"; + var url = $"/auth/admin/users/{target.UserKey.Value}/credentials/reset"; var begin = await BeginResetAsync( client, @@ -698,7 +697,7 @@ public async Task SelfChange_ShouldRespectConfiguredSessionPolicy(CredentialSess var newPassword = $"Changed-{Guid.NewGuid():N}!"; - var response = await currentClient.PostAsJsonAsync( + var response = await CsrfTestClient.PostJsonAsync(currentClient, "/auth/me/credentials/change", new ChangeCredentialRequest { @@ -753,7 +752,8 @@ await factory.GrantPermissionsAsync( var newPassword = $"AdminChanged-{Guid.NewGuid():N}!"; - var response = await adminClient.PostAsJsonAsync( + var response = await CsrfTestClient.PostJsonAsync( + adminClient, $"/auth/admin/users/{target.UserKey.Value}/credentials/change", new ChangeCredentialRequest { @@ -1001,7 +1001,7 @@ private static AuthServerFactory CreateFactory(TestResetNotifier notifier) private static Task BeginResetAsync(HttpClient client, string url, string identifier) { - return client.PostAsJsonAsync( + return CsrfTestClient.PostJsonAsync(client, url, new BeginResetCredentialRequest { @@ -1013,7 +1013,7 @@ private static Task BeginResetAsync(HttpClient client, stri private static Task CompleteResetAsync(HttpClient client, string url, string identifier, string token, string newPassword) { - return client.PostAsJsonAsync( + return CsrfTestClient.PostJsonAsync(client, url, new CompleteResetCredentialRequest { @@ -1026,7 +1026,8 @@ private static Task CompleteResetAsync(HttpClient client, s private static async Task AuthenticateAsync(HttpClient client, IntegrationTestUser user) { - var response = await client.PostAsJsonAsync( + var response = await CsrfTestClient.PostJsonAsync( + client, "/auth/login", new { diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/Infrastructure/CsrfTestClient.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/Infrastructure/CsrfTestClient.cs new file mode 100644 index 00000000..2aca4bdd --- /dev/null +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/Infrastructure/CsrfTestClient.cs @@ -0,0 +1,138 @@ +using System.Net.Http.Json; + +namespace CodeBeam.UltimateAuth.Tests.Integration.Infrastructure; + +internal static class CsrfTestClient +{ + private sealed record TokenResponse(string RequestToken, string HeaderName, string FormFieldName); + + public static Task PostJsonAsync( + HttpClient client, + string url, + T payload, + string tokenUrl = "/auth/antiforgery/token") + { + return SendAsync( + client, + url, + JsonContent.Create(payload), + tokenUrl); + } + + public static Task PostAsync( + HttpClient client, + string url, + HttpContent? content = null, + string tokenUrl = "/auth/antiforgery/token") + { + return SendAsync(client, url, content, tokenUrl); + } + + private static async Task SendAsync( + HttpClient client, + string url, + HttpContent? content, + string tokenUrl) + { + using var tokenResponse = await client.GetAsync(tokenUrl); + + tokenResponse.EnsureSuccessStatusCode(); + + var token = await tokenResponse.Content + .ReadFromJsonAsync(); + + if (token is null || + string.IsNullOrWhiteSpace(token.RequestToken) || + string.IsNullOrWhiteSpace(token.HeaderName)) + { + throw new InvalidOperationException( + "Invalid antiforgery token response."); + } + + var antiforgeryCookie = tokenResponse.Headers + .GetValues("Set-Cookie") + .Select(value => value.Split(';', 2)[0]) + .FirstOrDefault(value => + value.TrimStart().StartsWith( + ".AspNetCore.Antiforgery.", + StringComparison.OrdinalIgnoreCase)); + + if (string.IsNullOrWhiteSpace(antiforgeryCookie)) + { + throw new InvalidOperationException( + "Antiforgery cookie was not issued."); + } + + using var request = new HttpRequestMessage( + HttpMethod.Post, + url) + { + Content = content + }; + + request.Headers.TryAddWithoutValidation( + token.HeaderName, + token.RequestToken); + + // Preserve existing manually managed session cookies. + var existingCookies = client.DefaultRequestHeaders.TryGetValues( + "Cookie", + out var values) + ? string.Join("; ", values) + : string.Empty; + + var cookieHeader = string.IsNullOrWhiteSpace(existingCookies) + ? antiforgeryCookie + : $"{existingCookies}; {antiforgeryCookie}"; + + request.Headers.TryAddWithoutValidation( + "Cookie", + cookieHeader); + + return await client.SendAsync(request); + } + + public static async Task SendAsync(HttpClient client, HttpRequestMessage request, string tokenUrl = "/auth/antiforgery/token") + { + using var tokenResponse = await client.GetAsync(tokenUrl); + tokenResponse.EnsureSuccessStatusCode(); + + var token = await tokenResponse.Content + .ReadFromJsonAsync(); + + if (token is null || + string.IsNullOrWhiteSpace(token.RequestToken) || + string.IsNullOrWhiteSpace(token.HeaderName)) + { + throw new InvalidOperationException( + "Invalid antiforgery token response."); + } + + request.Headers.TryAddWithoutValidation( + token.HeaderName, + token.RequestToken); + + var antiforgeryCookie = tokenResponse.Headers + .GetValues("Set-Cookie") + .Select(x => x.Split(';', 2)[0]) + .First(x => x.StartsWith( + ".AspNetCore.Antiforgery.", + StringComparison.OrdinalIgnoreCase)); + + var existingCookies = + request.Headers.TryGetValues("Cookie", out var requestCookies) + ? string.Join("; ", requestCookies) + : client.DefaultRequestHeaders.TryGetValues("Cookie", out var clientCookies) + ? string.Join("; ", clientCookies) + : string.Empty; + + request.Headers.Remove("Cookie"); + request.Headers.TryAddWithoutValidation( + "Cookie", + string.IsNullOrWhiteSpace(existingCookies) + ? antiforgeryCookie + : $"{existingCookies}; {antiforgeryCookie}"); + + return await client.SendAsync(request); + } +} diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/LoginTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/LoginTests.cs index 71fae4e0..60dc1bfd 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration/LoginTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/LoginTests.cs @@ -29,7 +29,7 @@ public async Task Login_WithValidCredentials_ShouldIssueSessionCredential() using var client = CreateClient($"valid-session-{Guid.NewGuid():N}"); - var response = await client.PostAsJsonAsync( + var response = await CsrfTestClient.PostJsonAsync(client, "/auth/login", new { @@ -60,7 +60,7 @@ public async Task Login_WithValidCredentials_ShouldCreateUsableAuthenticatedSess using var authenticatedClient = CreateClient(deviceId); authenticatedClient.DefaultRequestHeaders.Add("Cookie", cookie); - var response = await authenticatedClient.PostAsJsonAsync( + var response = await CsrfTestClient.PostJsonAsync(authenticatedClient, "/auth/me/sessions/chains", new PageRequest { @@ -136,7 +136,7 @@ public async Task Login_WithUnsupportedContentType_ShouldNotAuthenticateUser() using var content = new StringContent( "identifier=admin&secret=admin"); - var response = await client.PostAsync( + var response = await CsrfTestClient.PostAsync(client, LoginEndpoint, content); @@ -289,7 +289,7 @@ public async Task TryLogin_WithValidCredentials_ShouldReturnSuccessfulPreview() using var client = CreateClient( $"try-login-valid-{Guid.NewGuid():N}"); - var response = await client.PostAsJsonAsync( + var response = await CsrfTestClient.PostJsonAsync(client, "/auth/try-login", new { @@ -319,7 +319,7 @@ public async Task TryLogin_WithValidCredentials_ShouldNotAuthenticateUser() using var client = CreateClient( "try-login-device-2222222222222222"); - var response = await client.PostAsJsonAsync("/auth/try-login", new + var response = await CsrfTestClient.PostJsonAsync(client, "/auth/try-login", new { identifier = ValidIdentifier, secret = ValidSecret @@ -350,7 +350,7 @@ public async Task TryLogin_WithInvalidCredentials_ShouldReturnFailedPreviewWitho using var client = CreateClient( $"try-invalid-{Guid.NewGuid():N}"); - var response = await client.PostAsJsonAsync( + var response = await CsrfTestClient.PostJsonAsync(client, "/auth/try-login", new { @@ -379,7 +379,7 @@ public async Task TryLogin_WithMissingCredentials_ShouldReturnFailedPreview() using var client = CreateClient( "try-login-device-4444444444444444"); - var response = await client.PostAsJsonAsync("/auth/try-login", new + var response = await CsrfTestClient.PostJsonAsync(client, "/auth/try-login", new { identifier = "", secret = "" @@ -403,7 +403,7 @@ public async Task Login_WithValidPreviewReceipt_ShouldAuthenticateUser() using var client = CreateClient( $"preview-valid-{Guid.NewGuid():N}"); - var previewResponse = await client.PostAsJsonAsync( + var previewResponse = await CsrfTestClient.PostJsonAsync(client, "/auth/try-login", new { @@ -420,7 +420,7 @@ public async Task Login_WithValidPreviewReceipt_ShouldAuthenticateUser() preview!.IsSuccess.Should().BeTrue(); preview.PreviewReceipt.Should().NotBeNullOrWhiteSpace(); - var loginResponse = await client.PostAsJsonAsync( + var loginResponse = await CsrfTestClient.PostJsonAsync(client, LoginEndpoint, new { @@ -446,7 +446,7 @@ public async Task Login_WithPreviewReceiptFromDifferentDevice_ShouldNotTrustRece using var previewClient = CreateClient( $"receipt-owner-{Guid.NewGuid():N}"); - var previewResponse = await previewClient.PostAsJsonAsync( + var previewResponse = await CsrfTestClient.PostJsonAsync(previewClient, "/auth/try-login", new { @@ -489,7 +489,7 @@ public async Task Login_WithPreviewReceiptAndDifferentSecret_ShouldNotAuthentica using var client = CreateClient( $"preview-different-secret-{Guid.NewGuid():N}"); - var previewResponse = await client.PostAsJsonAsync( + var previewResponse = await CsrfTestClient.PostJsonAsync(client, "/auth/try-login", new { @@ -528,7 +528,7 @@ public async Task Login_WithPreviewReceiptAndDifferentIdentifier_ShouldFallBackT $"preview-different-identifier-{Guid.NewGuid():N}"); // Create receipt for user A. - var previewResponse = await client.PostAsJsonAsync( + var previewResponse = await CsrfTestClient.PostJsonAsync(client, "/auth/try-login", new { @@ -549,7 +549,7 @@ public async Task Login_WithPreviewReceiptAndDifferentIdentifier_ShouldFallBackT // // The receipt must NOT be trusted for user B, but it also must not // prevent user B from authenticating with valid credentials. - var loginResponse = await client.PostAsJsonAsync( + var loginResponse = await CsrfTestClient.PostJsonAsync(client, LoginEndpoint, new { @@ -579,7 +579,7 @@ public async Task Login_WithPreviewReceiptFromDifferentIdentifier_ShouldNotBypas using var client = CreateClient( $"preview-cross-user-{Guid.NewGuid():N}"); - var previewResponse = await client.PostAsJsonAsync( + var previewResponse = await CsrfTestClient.PostJsonAsync(client, "/auth/try-login", new { @@ -596,7 +596,7 @@ public async Task Login_WithPreviewReceiptFromDifferentIdentifier_ShouldNotBypas // Receipt belongs to user A. // Attempt user B authentication with INVALID credentials. - var loginResponse = await client.PostAsJsonAsync( + var loginResponse = await CsrfTestClient.PostJsonAsync(client, LoginEndpoint, new { @@ -618,7 +618,7 @@ public async Task Login_WithUnknownPreviewReceipt_ShouldFallBackToNormalLoginVal using var client = CreateClient( $"unknown-receipt-{Guid.NewGuid():N}"); - var response = await client.PostAsJsonAsync( + var response = await CsrfTestClient.PostJsonAsync(client, LoginEndpoint, new { @@ -644,7 +644,7 @@ public async Task PreviewReceipt_AfterSuccessfulCommit_ShouldBeConsumed() using var client = CreateClient( $"preview-consume-{Guid.NewGuid():N}"); - var previewResponse = await client.PostAsJsonAsync( + var previewResponse = await CsrfTestClient.PostJsonAsync(client, "/auth/try-login", new { @@ -659,7 +659,7 @@ public async Task PreviewReceipt_AfterSuccessfulCommit_ShouldBeConsumed() preview!.IsSuccess.Should().BeTrue(); preview.PreviewReceipt.Should().NotBeNullOrWhiteSpace(); - var firstCommit = await client.PostAsJsonAsync( + var firstCommit = await CsrfTestClient.PostJsonAsync(client, LoginEndpoint, new { @@ -680,7 +680,7 @@ public async Task PreviewReceipt_AfterSuccessfulCommit_ShouldBeConsumed() // // Reusing it must not grant any special trust. The request should // simply fall back to normal credential validation. - var secondCommit = await client.PostAsJsonAsync( + var secondCommit = await CsrfTestClient.PostJsonAsync(client, LoginEndpoint, new { @@ -712,7 +712,7 @@ public async Task Login_WithRepeatedInvalidCredentials_ShouldLockAccount() .TryGetValues("Set-Cookie", out _) .Should().BeFalse(); - var second = await client.PostAsJsonAsync( + var second = await CsrfTestClient.PostJsonAsync(client, "/auth/login", new { @@ -744,7 +744,7 @@ public async Task TryLogin_WithRepeatedInvalidCredentials_ShouldParticipateInLoc using var client = CreateClient( "try-lockout-device-222222222222222"); - var first = await client.PostAsJsonAsync( + var first = await CsrfTestClient.PostJsonAsync(client, "/auth/try-login", new { @@ -758,7 +758,7 @@ public async Task TryLogin_WithRepeatedInvalidCredentials_ShouldParticipateInLoc firstResult.Should().NotBeNull(); firstResult!.IsSuccess.Should().BeFalse(); - var second = await client.PostAsJsonAsync( + var second = await CsrfTestClient.PostJsonAsync(client, "/auth/try-login", new { @@ -766,14 +766,13 @@ public async Task TryLogin_WithRepeatedInvalidCredentials_ShouldParticipateInLoc secret = "wrong-password-2" }); - var secondResult = - await second.Content.ReadFromJsonAsync(); + var secondResult = await second.Content.ReadFromJsonAsync(); secondResult.Should().NotBeNull(); secondResult!.IsSuccess.Should().BeFalse(); // Account should now be locked. - var correctAttempt = await client.PostAsJsonAsync( + var correctAttempt = await CsrfTestClient.PostJsonAsync(client, "/auth/try-login", new { @@ -797,7 +796,7 @@ public async Task TryLogin_WithValidCredentials_ShouldNotConsumeFailureAttempt() using var client = CreateClient( $"try-no-failure-{Guid.NewGuid():N}"); - var preview = await client.PostAsJsonAsync( + var preview = await CsrfTestClient.PostJsonAsync(client, "/auth/try-login", new { @@ -811,7 +810,7 @@ public async Task TryLogin_WithValidCredentials_ShouldNotConsumeFailureAttempt() previewResult.Should().NotBeNull(); previewResult!.IsSuccess.Should().BeTrue(); - var failure = await client.PostAsJsonAsync( + var failure = await CsrfTestClient.PostJsonAsync(client, "/auth/try-login", new { @@ -835,7 +834,7 @@ public async Task PreviewReceipt_WithDifferentSecret_ShouldNotSuppressFailureAcc using var client = CreateClient( $"receipt-accounting-{Guid.NewGuid():N}"); - var previewResponse = await client.PostAsJsonAsync( + var previewResponse = await CsrfTestClient.PostJsonAsync(client, "/auth/try-login", new { @@ -903,7 +902,7 @@ public async Task PreviewReceipt_FromDifferentDevice_ShouldNotSuppressFailureAcc using var receiptClient = CreateClient( $"receipt-device-a-{Guid.NewGuid():N}"); - var previewResponse = await receiptClient.PostAsJsonAsync( + var previewResponse = await CsrfTestClient.PostJsonAsync(receiptClient, "/auth/try-login", new { @@ -972,7 +971,7 @@ public async Task SuccessfulLogin_ShouldResetPreviousFailureAccounting() $"success-reset-{Guid.NewGuid():N}"); // Failure #1 - var firstFailure = await client.PostAsJsonAsync( + var firstFailure = await CsrfTestClient.PostJsonAsync(client, LoginEndpoint, new { @@ -985,7 +984,7 @@ public async Task SuccessfulLogin_ShouldResetPreviousFailureAccounting() .Should().BeFalse(); // Successful authentication must reset previous failure accounting. - var success = await client.PostAsJsonAsync( + var success = await CsrfTestClient.PostJsonAsync(client, LoginEndpoint, new { @@ -1001,7 +1000,7 @@ public async Task SuccessfulLogin_ShouldResetPreviousFailureAccounting() // If the previous failure was reset, this is failure #1 again, // not failure #2 / lockout. - var failureAfterSuccess = await client.PostAsJsonAsync( + var failureAfterSuccess = await CsrfTestClient.PostJsonAsync(client, "/auth/try-login", new { @@ -1009,8 +1008,7 @@ public async Task SuccessfulLogin_ShouldResetPreviousFailureAccounting() secret = "wrong-password-2" }); - var result = - await failureAfterSuccess.Content.ReadFromJsonAsync(); + var result = await failureAfterSuccess.Content.ReadFromJsonAsync(); result.Should().NotBeNull(); result!.IsSuccess.Should().BeFalse(); @@ -1072,7 +1070,7 @@ public async Task Login_WithFormPayload_ShouldAuthenticateUser() ["Secret"] = user.Secret }); - var response = await client.PostAsync( + var response = await CsrfTestClient.PostAsync(client, LoginEndpoint, content); @@ -1100,7 +1098,7 @@ public async Task TryLogin_WithFormPayload_ShouldReturnSuccessfulPreview() ["Secret"] = user.Secret }); - var response = await client.PostAsync( + var response = await CsrfTestClient.PostAsync(client, "/auth/try-login", content); @@ -1125,14 +1123,11 @@ public async Task TryLogin_WithFormPayload_ShouldReturnSuccessfulPreview() [InlineData("admin", " ")] [InlineData("", "")] [InlineData(" ", " ")] - public async Task Login_WithMissingOrWhitespaceCredentials_ShouldNotAuthenticate( - string identifier, - string secret) + public async Task Login_WithMissingOrWhitespaceCredentials_ShouldNotAuthenticate(string identifier, string secret) { - using var client = CreateClient( - $"invalid-input-device-{Guid.NewGuid():N}"); + using var client = CreateClient($"invalid-input-device-{Guid.NewGuid():N}"); - var response = await client.PostAsJsonAsync( + var response = await CsrfTestClient.PostJsonAsync(client, "/auth/login", new { @@ -1152,14 +1147,11 @@ public async Task Login_WithMissingOrWhitespaceCredentials_ShouldNotAuthenticate [InlineData("admin", " ")] [InlineData("", "")] [InlineData(" ", " ")] - public async Task TryLogin_WithMissingOrWhitespaceCredentials_ShouldReturnInvalidCredentials( - string identifier, - string secret) + public async Task TryLogin_WithMissingOrWhitespaceCredentials_ShouldReturnInvalidCredentials(string identifier, string secret) { - using var client = CreateClient( - $"invalid-preview-device-{Guid.NewGuid():N}"); + using var client = CreateClient($"invalid-preview-device-{Guid.NewGuid():N}"); - var response = await client.PostAsJsonAsync( + var response = await CsrfTestClient.PostJsonAsync(client, "/auth/try-login", new { @@ -1224,7 +1216,7 @@ public async Task TryLogin_WithEmptyJsonObject_ShouldReturnInvalidCredentials() using var client = CreateClient( "empty-json-device-555555555555555"); - var response = await client.PostAsJsonAsync( + var response = await CsrfTestClient.PostJsonAsync(client, "/auth/try-login", new { }); @@ -1270,7 +1262,7 @@ public async Task Login_WithJsonPropertyNamesUsingDifferentCasing_ShouldAuthenti ["SECRET"] = user.Secret }); - var response = await client.PostAsync( + var response = await CsrfTestClient.PostAsync(client, LoginEndpoint, content); @@ -1337,7 +1329,7 @@ public async Task TryLogin_WithUnknownIdentifier_ShouldReturnSameFailureReasonAs using var existingClient = CreateClient( $"existing-{Guid.NewGuid():N}"); - var unknownResponse = await unknownClient.PostAsJsonAsync( + var unknownResponse = await CsrfTestClient.PostJsonAsync(unknownClient, "/auth/try-login", new { @@ -1345,7 +1337,7 @@ public async Task TryLogin_WithUnknownIdentifier_ShouldReturnSameFailureReasonAs secret = "wrong-password" }); - var existingResponse = await existingClient.PostAsJsonAsync( + var existingResponse = await CsrfTestClient.PostJsonAsync(existingClient, "/auth/try-login", new { @@ -1384,7 +1376,7 @@ public async Task TryLogin_WithUnknownIdentifier_ShouldNotIssuePreviewReceipt() using var client = CreateClient( $"unknown-receipt-{Guid.NewGuid():N}"); - var response = await client.PostAsJsonAsync( + var response = await CsrfTestClient.PostJsonAsync(client, "/auth/try-login", new { @@ -1416,7 +1408,7 @@ public async Task TryLogin_WithWrongPassword_ShouldNotIssuePreviewReceipt() using var client = CreateClient( $"wrong-password-{Guid.NewGuid():N}"); - var response = await client.PostAsJsonAsync( + var response = await CsrfTestClient.PostJsonAsync(client, "/auth/try-login", new { @@ -1852,8 +1844,7 @@ public async Task ConcurrentLogin_WithSamePreviewReceipt_ShouldNotAllowReceiptTo } - private HttpClient CreateClient( - string deviceId = "test-device-1234567890123456") + private HttpClient CreateClient(string deviceId = "test-device-1234567890123456") { var client = _factory.CreateClient( new WebApplicationFactoryClientOptions @@ -1875,7 +1866,7 @@ private HttpClient CreateClient( private static Task LoginAsync(HttpClient client, string identifier, string secret) { - return client.PostAsJsonAsync( + return CsrfTestClient.PostJsonAsync(client, LoginEndpoint, new { @@ -1886,7 +1877,7 @@ private static Task LoginAsync(HttpClient client, string id private static async Task TryLoginAsync(HttpClient client, string identifier, string secret) { - var response = await client.PostAsJsonAsync( + var response = await CsrfTestClient.PostJsonAsync(client, "auth/try-login", new LoginRequest { @@ -1905,7 +1896,7 @@ private static async Task TryLoginAsync(HttpClient client, strin private static Task LoginWithPreviewReceiptAsync(HttpClient client, string identifier, string secret, string previewReceipt) { - return client.PostAsJsonAsync("/auth/login", new + return CsrfTestClient.PostJsonAsync(client, "/auth/login", new { identifier, secret, diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/LogoutAdminTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/LogoutAdminTests.cs index 2b34bdc5..fb873146 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration/LogoutAdminTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/LogoutAdminTests.cs @@ -54,7 +54,7 @@ await LoginAsync( using var anonymousClient = CreateClient( $"admin-logout-device-anonymous-{Guid.NewGuid():N}"); - var response = await anonymousClient.PostAsJsonAsync( + var response = await CsrfTestClient.PostJsonAsync(anonymousClient, $"/auth/admin/users/{target.UserKey.Value}/logout-device", new LogoutDeviceRequest { @@ -119,7 +119,7 @@ await LoginAsync( var targetChain = targetChains!.Items .Single(x => x.IsCurrentDevice); - var response = await actorClient.PostAsJsonAsync( + var response = await CsrfTestClient.PostJsonAsync(actorClient, $"/auth/admin/users/{target.UserKey.Value}/logout-device", new LogoutDeviceRequest { @@ -165,7 +165,7 @@ await LoginAsync( using var anonymousClient = CreateClient( $"admin-logout-all-anonymous-{Guid.NewGuid():N}"); - var response = await anonymousClient.PostAsync( + var response = await CsrfTestClient.PostAsync(anonymousClient, $"/auth/admin/users/{target.UserKey.Value}/logout-all", null); @@ -214,7 +214,7 @@ await LoginAsync( "Cookie", targetCookie); - var response = await actorClient.PostAsync( + var response = await CsrfTestClient.PostAsync(actorClient, $"/auth/admin/users/{target.UserKey.Value}/logout-all", null); @@ -299,7 +299,7 @@ await LoginAsync( // // Admin logs out target device. // - var response = await adminClient.PostAsJsonAsync( + var response = await CsrfTestClient.PostJsonAsync(adminClient, $"/auth/admin/users/{target.UserKey.Value}/logout-device", new LogoutDeviceRequest { @@ -401,7 +401,7 @@ await LoginAsync( // // Admin terminates only device 1. // - var response = await adminClient.PostAsJsonAsync( + var response = await CsrfTestClient.PostJsonAsync(adminClient, $"/auth/admin/users/{target.UserKey.Value}/logout-device", new LogoutDeviceRequest { @@ -487,9 +487,7 @@ await LoginAsync( "Cookie", targetBCookie); - // // Obtain B's chain. - // var targetBChainsResponse = await GetChainsAsync(targetBClient); @@ -504,10 +502,8 @@ await LoginAsync( var targetBChain = targetBChains!.Items .Single(x => x.IsCurrentDevice); - // // URL says target A, but supplied chain belongs to B. - // - var response = await adminClient.PostAsJsonAsync( + var response = await CsrfTestClient.PostJsonAsync(adminClient, $"/auth/admin/users/{targetA.UserKey.Value}/logout-device", new LogoutDeviceRequest { @@ -518,24 +514,18 @@ await LoginAsync( HttpStatusCode.Forbidden, HttpStatusCode.NotFound); - // // Most important assertion: // B's session must NOT have been mutated. - // (await GetChainsAsync(targetBClient)) .StatusCode.Should() .Be(HttpStatusCode.OK); - // // A must also remain untouched. - // (await GetChainsAsync(targetAClient)) .StatusCode.Should() .Be(HttpStatusCode.OK); - // // Admin remains authenticated. - // (await GetChainsAsync(adminClient)) .StatusCode.Should() .Be(HttpStatusCode.OK); @@ -586,7 +576,7 @@ await _factory.GrantPermissionsAsync( var currentChain = chains!.Items.Single(x => x.IsCurrentDevice); - var response = await adminClient.PostAsJsonAsync( + var response = await CsrfTestClient.PostJsonAsync(adminClient, $"/auth/admin/users/{target.UserKey.Value}/logout-others", new LogoutOtherDevicesRequest { @@ -654,7 +644,7 @@ await LoginAsync( var currentChain = chains!.Items .Single(x => x.IsCurrentDevice); - var response = await actorClient.PostAsJsonAsync( + var response = await CsrfTestClient.PostJsonAsync(actorClient, $"/auth/admin/users/{target.UserKey.Value}/logout-others", new LogoutOtherDevicesRequest { @@ -754,7 +744,7 @@ await LoginAsync( // URL target = target // CurrentChainId = otherUser's chain // - var response = await adminClient.PostAsJsonAsync( + var response = await CsrfTestClient.PostJsonAsync(adminClient, $"/auth/admin/users/{target.UserKey.Value}/logout-others", new LogoutOtherDevicesRequest { @@ -839,7 +829,7 @@ await LoginAsync( var currentChain = chains!.Items .Single(x => x.IsCurrentDevice); - var response = await adminClient.PostAsJsonAsync( + var response = await CsrfTestClient.PostJsonAsync(adminClient, $"/auth/admin/users/{target.UserKey.Value}/logout-others", new LogoutOtherDevicesRequest { @@ -937,14 +927,14 @@ await LoginAsync( var url = $"/auth/admin/users/{target.UserKey.Value}/logout-others"; - var first = await adminClient.PostAsJsonAsync( + var first = await CsrfTestClient.PostJsonAsync(adminClient, url, request); first.StatusCode.Should() .Be(HttpStatusCode.OK); - var second = await adminClient.PostAsJsonAsync( + var second = await CsrfTestClient.PostJsonAsync(adminClient, url, request); @@ -1057,7 +1047,7 @@ await LoginAsync( // // Admin logs out every device belonging to target. // - var response = await adminClient.PostAsync( + var response = await CsrfTestClient.PostAsync(adminClient, $"/auth/admin/users/{target.UserKey.Value}/logout-all", null); @@ -1120,7 +1110,7 @@ await LoginAsync( "Cookie", targetCookie); - var response = await adminClient.PostAsync( + var response = await CsrfTestClient.PostAsync(adminClient, $"/auth/admin/users/{target.UserKey.Value}/logout-all", null); @@ -1205,7 +1195,7 @@ await LoginAsync( // Actor is authenticated but does NOT have // flows.logoutall.admin. // - var response = await actorClient.PostAsync( + var response = await CsrfTestClient.PostAsync(actorClient, $"/auth/admin/users/{target.UserKey.Value}/logout-all", null); @@ -1284,26 +1274,23 @@ await LoginAsync( "Cookie", targetCookie2); - var url = - $"/auth/admin/users/{target.UserKey.Value}/logout-all"; + var url = $"/auth/admin/users/{target.UserKey.Value}/logout-all"; - var first = await adminClient.PostAsync( + var first = await CsrfTestClient.PostAsync(adminClient, url, null); first.StatusCode.Should() .Be(HttpStatusCode.OK); - var second = await adminClient.PostAsync( + var second = await CsrfTestClient.PostAsync(adminClient, url, null); second.StatusCode.Should() .Be(HttpStatusCode.OK); - // // Sessions must remain logged out. - // (await GetChainsAsync(targetClient1)) .StatusCode.Should() .Be(HttpStatusCode.Unauthorized); @@ -1312,10 +1299,7 @@ await LoginAsync( .StatusCode.Should() .Be(HttpStatusCode.Unauthorized); - // - // Repeating the operation must not affect - // the administrative actor. - // + // Repeating the operation must not affect the administrative actor. (await GetChainsAsync(adminClient)) .StatusCode.Should() .Be(HttpStatusCode.OK); @@ -1375,7 +1359,7 @@ await LoginAsync( "Cookie", unrelatedCookie); - var response = await adminClient.PostAsync( + var response = await CsrfTestClient.PostAsync(adminClient, $"/auth/admin/users/{target.UserKey.Value}/logout-all", null); @@ -1431,7 +1415,7 @@ private HttpClient CreateClient(string deviceId) private static async Task LoginAsync(HttpClient client, string identifier, string secret) { - return await client.PostAsJsonAsync( + return await CsrfTestClient.PostJsonAsync(client, "/auth/login", new { @@ -1442,7 +1426,7 @@ private static async Task LoginAsync(HttpClient client, str private static async Task GetChainsAsync(HttpClient client) { - return await client.PostAsJsonAsync( + return await CsrfTestClient.PostJsonAsync(client, "/auth/me/sessions/chains", new PageRequest { diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/LogoutTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/LogoutTests.cs index ba1b5f25..4d960c24 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration/LogoutTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/LogoutTests.cs @@ -39,7 +39,7 @@ public async Task Logout_WithAuthenticatedSession_ShouldSucceed() client.DefaultRequestHeaders.Add("Cookie", cookie); - var logoutResponse = await client.PostAsync( + var logoutResponse = await CsrfTestClient.PostAsync(client, "/auth/logout", null); @@ -65,7 +65,7 @@ public async Task Logout_ShouldDeleteSessionCookie() client.DefaultRequestHeaders.Add("Cookie", cookie); - var logoutResponse = await client.PostAsync( + var logoutResponse = await CsrfTestClient.PostAsync(client, "/auth/logout", null); @@ -110,10 +110,9 @@ public async Task Logout_ShouldInvalidateSessionOnServer() var beforeLogout = await GetChainsAsync(client); beforeLogout.StatusCode.Should().Be(HttpStatusCode.OK); - var logoutResponse = await client.PostAsync("/auth/logout", null); + var logoutResponse = await CsrfTestClient.PostAsync(client, "/auth/logout", null); logoutResponse.StatusCode.Should().Be(HttpStatusCode.Found); - // // IMPORTANT: // Deliberately keep sending the old cookie. // @@ -121,7 +120,6 @@ public async Task Logout_ShouldInvalidateSessionOnServer() // magically remove our manually supplied Cookie header. // // Therefore this proves server-side invalidation. - // var afterLogout = await GetChainsAsync(client); @@ -149,7 +147,7 @@ public async Task Logout_OldSessionCookie_ShouldNotAuthenticateFromNewClient() "Cookie", cookie); - var logoutResponse = await originalClient.PostAsync( + var logoutResponse = await CsrfTestClient.PostAsync(originalClient, "/auth/logout", null); @@ -215,7 +213,7 @@ public async Task Logout_ShouldNotInvalidateOtherDeviceSession() before1.StatusCode.Should().Be(HttpStatusCode.OK); before2.StatusCode.Should().Be(HttpStatusCode.OK); - var logout = await client1.PostAsync( + var logout = await CsrfTestClient.PostAsync(client1, "/auth/logout", null); @@ -268,7 +266,7 @@ public async Task Logout_ShouldNotInvalidateAnotherUsersSession() "Cookie", cookie2); - var logout = await client1.PostAsync( + var logout = await CsrfTestClient.PostAsync(client1, "/auth/logout", null); @@ -323,7 +321,7 @@ public async Task Logout_ShouldDetachActiveSessionFromCurrentChain() var chainId = currentBefore.ChainId; var sessionId = currentBefore.ActiveSessionId; - var logout = await client.PostAsync("/auth/logout", null); + var logout = await CsrfTestClient.PostAsync(client, "/auth/logout", null); logout.StatusCode.Should().Be(HttpStatusCode.Found); @@ -375,7 +373,7 @@ public async Task Logout_OldSession_ShouldRemainInvalidAfterLoginAgain() firstClient.DefaultRequestHeaders.Add("Cookie", oldCookie); - var logout = await firstClient.PostAsync("/auth/logout", null); + var logout = await CsrfTestClient.PostAsync(firstClient, "/auth/logout", null); logout.StatusCode.Should().Be(HttpStatusCode.Found); @@ -429,7 +427,7 @@ public async Task Logout_ShouldDetachOnlyCurrentDeviceSession() client1.DefaultRequestHeaders.Add("Cookie", cookie1); client2.DefaultRequestHeaders.Add("Cookie", cookie2); - var beforeResponse = await client2.PostAsJsonAsync( + var beforeResponse = await CsrfTestClient.PostJsonAsync(client2, "/auth/me/sessions/chains", new PageRequest { @@ -451,17 +449,17 @@ public async Task Logout_ShouldDetachOnlyCurrentDeviceSession() device2SessionId.Should().NotBeNull(); - var logout = await client1.PostAsync("/auth/logout", null); + var logout = await CsrfTestClient.PostAsync(client1, "/auth/logout", null); logout.StatusCode.Should().Be(HttpStatusCode.Found); - var device1Result = await client1.PostAsJsonAsync( + var device1Result = await CsrfTestClient.PostJsonAsync(client1, "/auth/me/sessions/chains", new PageRequest()); device1Result.StatusCode.Should().Be(HttpStatusCode.Unauthorized); - var device2Result = await client2.PostAsJsonAsync( + var device2Result = await CsrfTestClient.PostJsonAsync(client2, "/auth/me/sessions/chains", new PageRequest { @@ -500,7 +498,7 @@ public async Task Logout_ShouldNotRevokeDeviceChain() var cookie = GetSessionCookie(login); client.DefaultRequestHeaders.Add("Cookie", cookie); - var beforeResponse = await client.PostAsJsonAsync( + var beforeResponse = await CsrfTestClient.PostJsonAsync(client, "/auth/me/sessions/chains", new PageRequest { @@ -516,7 +514,7 @@ public async Task Logout_ShouldNotRevokeDeviceChain() var chainBefore = before!.Items.Single(x => x.IsCurrentDevice); var chainId = chainBefore.ChainId; - await client.PostAsync("/auth/logout", null); + await CsrfTestClient.PostAsync(client, "/auth/logout", null); using var reloginClient = CreateClient(device); @@ -531,7 +529,7 @@ public async Task Logout_ShouldNotRevokeDeviceChain() "Cookie", newCookie); - var afterResponse = await reloginClient.PostAsJsonAsync( + var afterResponse = await CsrfTestClient.PostJsonAsync(reloginClient, "/auth/me/sessions/chains", new PageRequest { @@ -559,11 +557,11 @@ public async Task Logout_WithoutAuthentication_ShouldBeSafeAndIdempotent() using var client = CreateClient( $"logout-anonymous-{Guid.NewGuid():N}"); - var first = await client.PostAsync( + var first = await CsrfTestClient.PostAsync(client, "/auth/logout", null); - var second = await client.PostAsync( + var second = await CsrfTestClient.PostAsync(client, "/auth/logout", null); @@ -594,19 +592,19 @@ public async Task Logout_WithAlreadyLoggedOutSession_ShouldRemainIdempotent() "Cookie", cookie); - var firstLogout = await client.PostAsync( + var firstLogout = await CsrfTestClient.PostAsync(client, "/auth/logout", null); firstLogout.StatusCode.Should().Be(HttpStatusCode.Found); - var secondLogout = await client.PostAsync( + var secondLogout = await CsrfTestClient.PostAsync(client, "/auth/logout", null); secondLogout.StatusCode.Should().BeOneOf(HttpStatusCode.Unauthorized, HttpStatusCode.Found); - var authenticatedRequest = await client.PostAsJsonAsync( + var authenticatedRequest = await CsrfTestClient.PostJsonAsync(client, "/auth/me/sessions/chains", new PageRequest()); @@ -650,7 +648,7 @@ public async Task Logout_WithInvalidSessionCookie_ShouldNotAffectValidSession() "Cookie", $"uas={Guid.NewGuid():N}"); - var invalidLogout = await invalidClient.PostAsync( + var invalidLogout = await CsrfTestClient.PostAsync(invalidClient, "/auth/logout", null); @@ -659,7 +657,7 @@ public async Task Logout_WithInvalidSessionCookie_ShouldNotAffectValidSession() // // Existing legitimate session must be untouched. // - var validSession = await validClient.PostAsJsonAsync( + var validSession = await CsrfTestClient.PostJsonAsync(validClient, "/auth/me/sessions/chains", new PageRequest { @@ -706,7 +704,7 @@ public async Task Logout_WithOldSessionAfterRelogin_ShouldNotLogoutNewSession() "Cookie", oldCookie); - var firstLogout = await firstClient.PostAsync( + var firstLogout = await CsrfTestClient.PostAsync(firstClient, "/auth/logout", null); @@ -732,7 +730,7 @@ public async Task Logout_WithOldSessionAfterRelogin_ShouldNotLogoutNewSession() "Cookie", newCookie); - var beforeReplay = await currentClient.PostAsJsonAsync( + var beforeReplay = await CsrfTestClient.PostJsonAsync(currentClient, "/auth/me/sessions/chains", new PageRequest { @@ -764,7 +762,7 @@ public async Task Logout_WithOldSessionAfterRelogin_ShouldNotLogoutNewSession() "Cookie", oldCookie); - var staleLogout = await staleClient.PostAsync( + var staleLogout = await CsrfTestClient.PostAsync(staleClient, "/auth/logout", null); @@ -773,7 +771,7 @@ public async Task Logout_WithOldSessionAfterRelogin_ShouldNotLogoutNewSession() // // Session B MUST still be valid. // - var afterReplay = await currentClient.PostAsJsonAsync( + var afterReplay = await CsrfTestClient.PostJsonAsync(currentClient, "/auth/me/sessions/chains", new PageRequest { @@ -824,7 +822,7 @@ public async Task RepeatedLogoutLoginCycles_ShouldPreserveChainAndRotateSessions "Cookie", cookie); - var chainsResponse = await client.PostAsJsonAsync( + var chainsResponse = await CsrfTestClient.PostJsonAsync(client, "/auth/me/sessions/chains", new PageRequest { @@ -861,7 +859,7 @@ public async Task RepeatedLogoutLoginCycles_ShouldPreserveChainAndRotateSessions // if (i < 2) { - var logout = await client.PostAsync( + var logout = await CsrfTestClient.PostAsync(client, "/auth/logout", null); @@ -889,9 +887,7 @@ public async Task Logout_WithStaleCredential_ShouldNotAffectOtherDevice() var device2 = $"logout-stale-device-2-{Guid.NewGuid():N}"; - // // Device 1 login. - // using var device1Client = CreateClient(device1); var login1 = await LoginAsync( @@ -905,9 +901,7 @@ public async Task Logout_WithStaleCredential_ShouldNotAffectOtherDevice() "Cookie", staleCookie); - // // Device 2 login. - // using var device2Client = CreateClient(device2); var login2 = await LoginAsync( @@ -922,7 +916,7 @@ public async Task Logout_WithStaleCredential_ShouldNotAffectOtherDevice() device2Cookie); var device2BeforeResponse = - await device2Client.PostAsJsonAsync( + await CsrfTestClient.PostJsonAsync(device2Client, "/auth/me/sessions/chains", new PageRequest { @@ -947,7 +941,7 @@ await device2Client.PostAsJsonAsync( device2SessionId.Should().NotBeNull(); - var logout1 = await device1Client.PostAsync("/auth/logout", null); + var logout1 = await CsrfTestClient.PostAsync(device1Client, "/auth/logout", null); logout1.StatusCode.Should().Be(HttpStatusCode.Found); @@ -955,12 +949,12 @@ await device2Client.PostAsJsonAsync( staleClient.DefaultRequestHeaders.Add("Cookie", staleCookie); - var staleLogout = await staleClient.PostAsync("/auth/logout", null); + var staleLogout = await CsrfTestClient.PostAsync(staleClient, "/auth/logout", null); staleLogout.StatusCode.Should().BeOneOf(HttpStatusCode.Unauthorized, HttpStatusCode.Found); var device2AfterResponse = - await device2Client.PostAsJsonAsync( + await CsrfTestClient.PostJsonAsync(device2Client, "/auth/me/sessions/chains", new PageRequest { @@ -1006,15 +1000,15 @@ public async Task Logout_WithAlreadyLoggedOutSession_ShouldRemainSafe() client.DefaultRequestHeaders.Add("Cookie", cookie); - var firstLogout = await client.PostAsync("/auth/logout", null); + var firstLogout = await CsrfTestClient.PostAsync(client, "/auth/logout", null); firstLogout.StatusCode.Should().Be(HttpStatusCode.Found); - var secondLogout = await client.PostAsync("/auth/logout", null); + var secondLogout = await CsrfTestClient.PostAsync(client, "/auth/logout", null); secondLogout.StatusCode.Should().BeOneOf(HttpStatusCode.Unauthorized, HttpStatusCode.Found); - var authenticatedRequest = await client.PostAsJsonAsync( + var authenticatedRequest = await CsrfTestClient.PostJsonAsync(client, "/auth/me/sessions/chains", new PageRequest { @@ -1048,7 +1042,7 @@ public async Task Logout_ShouldNotReturnAuthenticationCredentials() "Cookie", sessionCookie); - var logout = await client.PostAsync( + var logout = await CsrfTestClient.PostAsync(client, "/auth/logout", null); @@ -1105,7 +1099,7 @@ public async Task Logout_ShouldDeleteSessionCookieWithRootPath() "Cookie", sessionCookie); - var logout = await client.PostAsync( + var logout = await CsrfTestClient.PostAsync(client, "/auth/logout", null); @@ -1151,7 +1145,7 @@ public async Task Logout_ShouldNotReissueSessionCookieAfterServerSideInvalidatio "Cookie", oldCookie); - var logout = await client.PostAsync( + var logout = await CsrfTestClient.PostAsync(client, "/auth/logout", null); @@ -1227,7 +1221,7 @@ public async Task Logout_CookieDeletion_ShouldNotContainOriginalSessionCredentia "Cookie", sessionCookie); - var logout = await client.PostAsync( + var logout = await CsrfTestClient.PostAsync(client, "/auth/logout", null); @@ -1279,8 +1273,8 @@ public async Task ConcurrentLogout_WithSameSession_ShouldLeaveSessionInvalid() client2.DefaultRequestHeaders.Add("Cookie", cookie); var responses = await Task.WhenAll( - client1.PostAsync("/auth/logout", null), - client2.PostAsync("/auth/logout", null)); + CsrfTestClient.PostAsync(client1, "/auth/logout", null), + CsrfTestClient.PostAsync(client2, "/auth/logout", null)); // // Depending on where stale-session rejection occurs, @@ -1351,8 +1345,8 @@ public async Task ConcurrentLogout_FromDifferentDevices_ShouldInvalidateBothSess cookie2); var logouts = await Task.WhenAll( - client1.PostAsync("/auth/logout", null), - client2.PostAsync("/auth/logout", null)); + CsrfTestClient.PostAsync(client1, "/auth/logout", null), + CsrfTestClient.PostAsync(client2, "/auth/logout", null)); logouts.Should().OnlyContain( x => x.StatusCode == HttpStatusCode.Found); @@ -1414,7 +1408,7 @@ public async Task ConcurrentRequests_WithSessionBeingLoggedOut_ShouldNeverLeaveC // Intentionally race an authenticated operation against logout. // var logoutTask = - logoutClient.PostAsync("/auth/logout", null); + CsrfTestClient.PostAsync(logoutClient, "/auth/logout", null); var authenticatedRequestTask = GetChainsAsync(requestClient); @@ -1489,7 +1483,7 @@ public async Task ConcurrentLogoutAndRelogin_OnSameDevice_ShouldLeaveChainInCons oldCookie); var logoutTask = - logoutClient.PostAsync("/auth/logout", null); + CsrfTestClient.PostAsync(logoutClient, "/auth/logout", null); var loginTask = LoginAsync( @@ -1625,7 +1619,7 @@ public async Task LogoutDeviceSelf_ShouldRevokeTargetDeviceSession() !x.IsCurrentDevice && x.ActiveSessionId is not null); - var logoutResponse = await client1.PostAsJsonAsync( + var logoutResponse = await CsrfTestClient.PostJsonAsync(client1, "/auth/me/logout-device", new LogoutDeviceRequest { @@ -1676,7 +1670,7 @@ public async Task LogoutDeviceSelf_ShouldNotInvalidateCurrentDevice_WhenAnotherD !x.IsCurrentDevice && x.ActiveSessionId is not null); - var logout = await client1.PostAsJsonAsync( + var logout = await CsrfTestClient.PostJsonAsync(client1, "/auth/me/logout-device", new LogoutDeviceRequest { @@ -1741,7 +1735,7 @@ public async Task LogoutDeviceSelf_ShouldDetachActiveSessionWithoutRevokingChain !x.IsCurrentDevice && x.ActiveSessionId is not null); - var logout = await client1.PostAsJsonAsync( + var logout = await CsrfTestClient.PostJsonAsync(client1, "/auth/me/logout-device", new LogoutDeviceRequest { @@ -1773,7 +1767,7 @@ public async Task LogoutDeviceSelf_WithoutAuthentication_ShouldReturnUnauthorize using var client = CreateClient( $"logout-device-anonymous-{Guid.NewGuid():N}"); - var response = await client.PostAsJsonAsync( + var response = await CsrfTestClient.PostJsonAsync(client, "/auth/me/logout-device", new LogoutDeviceRequest { @@ -1840,7 +1834,7 @@ await LoginAsync( // // Attacker tries to target victim's ChainId. // - var attack = await attackerClient.PostAsJsonAsync( + var attack = await CsrfTestClient.PostJsonAsync(attackerClient, "/auth/me/logout-device", new LogoutDeviceRequest { @@ -1894,7 +1888,7 @@ public async Task LogoutDeviceSelf_ShouldAllowCurrentDeviceToLogoutItself() currentChain.ActiveSessionId.Should().NotBeNull(); - var logout = await client.PostAsJsonAsync( + var logout = await CsrfTestClient.PostJsonAsync(client, "/auth/me/logout-device", new LogoutDeviceRequest { @@ -1933,7 +1927,7 @@ public async Task LogoutDeviceSelf_WithUnknownChain_ShouldNotAffectCurrentSessio "Cookie", cookie); - var response = await client.PostAsJsonAsync( + var response = await CsrfTestClient.PostJsonAsync(client, "/auth/me/logout-device", new LogoutDeviceRequest { @@ -2005,7 +1999,7 @@ await LoginAsync( !x.IsCurrentDevice && x.ActiveSessionId is not null); - var first = await actorClient.PostAsJsonAsync( + var first = await CsrfTestClient.PostJsonAsync(actorClient, "/auth/me/logout-device", new LogoutDeviceRequest { @@ -2017,7 +2011,7 @@ await LoginAsync( // // Same operation against an already detached chain. // - var second = await actorClient.PostAsJsonAsync( + var second = await CsrfTestClient.PostJsonAsync(actorClient, "/auth/me/logout-device", new LogoutDeviceRequest { @@ -2104,7 +2098,7 @@ await LoginAsync( var originalChainId = targetChain.ChainId; var originalSessionId = targetChain.ActiveSessionId; - var logout = await actorClient.PostAsJsonAsync( + var logout = await CsrfTestClient.PostJsonAsync(actorClient, "/auth/me/logout-device", new LogoutDeviceRequest { @@ -2206,9 +2200,7 @@ public async Task LogoutOthersSelf_ShouldInvalidateAllOtherDeviceSessions() client2.DefaultRequestHeaders.Add("Cookie", cookie2); client3.DefaultRequestHeaders.Add("Cookie", cookie3); - // // All three sessions must initially be usable. - // (await GetChainsAsync(client1)) .StatusCode.Should().Be(HttpStatusCode.OK); @@ -2218,24 +2210,18 @@ public async Task LogoutOthersSelf_ShouldInvalidateAllOtherDeviceSessions() (await GetChainsAsync(client3)) .StatusCode.Should().Be(HttpStatusCode.OK); - // // Device 1 logs out every OTHER device. - // - var logout = await client1.PostAsync( + var logout = await CsrfTestClient.PostAsync(client1, "/auth/me/logout-others", null); logout.StatusCode.Should().Be(HttpStatusCode.OK); - // // Actor survives. - // (await GetChainsAsync(client1)) .StatusCode.Should().Be(HttpStatusCode.OK); - // // Every other authority must be dead. - // (await GetChainsAsync(client2)) .StatusCode.Should().Be(HttpStatusCode.Unauthorized); @@ -2301,7 +2287,7 @@ await LoginAsync( currentSessionId.Should().NotBeNull(); - var logout = await currentClient.PostAsync( + var logout = await CsrfTestClient.PostAsync(currentClient, "/auth/me/logout-others", null); @@ -2414,7 +2400,7 @@ await LoginAsync( .Select(x => x.ChainId) .ToHashSet(); - var logout = await actorClient.PostAsync( + var logout = await CsrfTestClient.PostAsync(actorClient, "/auth/me/logout-others", null); @@ -2516,7 +2502,7 @@ await LoginAsync( (await GetChainsAsync(victimClient)) .StatusCode.Should().Be(HttpStatusCode.OK); - var logout = await actorClient1.PostAsync( + var logout = await CsrfTestClient.PostAsync(actorClient1, "/auth/me/logout-others", null); @@ -2547,7 +2533,7 @@ public async Task LogoutOthersSelf_WithoutAuthentication_ShouldReturnUnauthorize using var client = CreateClient( $"logout-others-anonymous-{Guid.NewGuid():N}"); - var response = await client.PostAsync( + var response = await CsrfTestClient.PostAsync(client, "/auth/me/logout-others", null); @@ -2596,19 +2582,15 @@ await LoginAsync( sessionId.Should().NotBeNull(); - // // There is nothing to terminate. - // - var logout = await client.PostAsync( + var logout = await CsrfTestClient.PostAsync(client, "/auth/me/logout-others", null); logout.StatusCode.Should() .Be(HttpStatusCode.OK); - // // No-op must not damage current authority. - // var afterResponse = await GetChainsAsync(client); @@ -2663,32 +2645,26 @@ await LoginAsync( "Cookie", targetCookie); - var first = await actorClient.PostAsync( + var first = await CsrfTestClient.PostAsync(actorClient, "/auth/me/logout-others", null); first.StatusCode.Should() .Be(HttpStatusCode.OK); - // // Nothing remains to logout except actor itself. - // - var second = await actorClient.PostAsync( + var second = await CsrfTestClient.PostAsync(actorClient, "/auth/me/logout-others", null); second.StatusCode.Should() .Be(HttpStatusCode.OK); - // // Actor survives repeated calls. - // (await GetChainsAsync(actorClient)) .StatusCode.Should().Be(HttpStatusCode.OK); - // // Previously invalidated authority stays invalid. - // (await GetChainsAsync(targetClient)) .StatusCode.Should().Be(HttpStatusCode.Unauthorized); } @@ -2729,7 +2705,7 @@ await LoginAsync( "Cookie", oldTargetCookie); - var logout = await actorClient.PostAsync( + var logout = await CsrfTestClient.PostAsync(actorClient, "/auth/me/logout-others", null); @@ -2817,7 +2793,7 @@ await LoginAsync( var originalSessionId = targetBefore.ActiveSessionId; - var logout = await actorClient.PostAsync( + var logout = await CsrfTestClient.PostAsync(actorClient, "/auth/me/logout-others", null); @@ -2947,11 +2923,11 @@ await LoginAsync( actorCookie); var responses = await Task.WhenAll( - request1.PostAsync( + CsrfTestClient.PostAsync(request1, "/auth/me/logout-others", null), - request2.PostAsync( + CsrfTestClient.PostAsync(request2, "/auth/me/logout-others", null)); @@ -3028,9 +3004,7 @@ public async Task LogoutAllSelf_ShouldInvalidateAllDeviceSessions() client2.DefaultRequestHeaders.Add("Cookie", cookie2); client3.DefaultRequestHeaders.Add("Cookie", cookie3); - // // Sanity check: every authority is initially valid. - // (await GetChainsAsync(client1)) .StatusCode.Should().Be(HttpStatusCode.OK); @@ -3040,19 +3014,14 @@ public async Task LogoutAllSelf_ShouldInvalidateAllDeviceSessions() (await GetChainsAsync(client3)) .StatusCode.Should().Be(HttpStatusCode.OK); - // - // Device 1 logs out ALL sessions belonging to the user, - // including its own session. - // - var logout = await client1.PostAsync( + // Device 1 logs out ALL sessions belonging to the user, including its own session. + var logout = await CsrfTestClient.PostAsync(client1, "/auth/me/logout-all", null); logout.StatusCode.Should().Be(HttpStatusCode.OK); - // // Every previously issued authority must now be dead. - // (await GetChainsAsync(client1)) .StatusCode.Should().Be(HttpStatusCode.Unauthorized); @@ -3096,7 +3065,7 @@ await LoginAsync( // // Unlike logout-others, logout-all includes the actor. // - var logout = await client.PostAsync( + var logout = await CsrfTestClient.PostAsync(client, "/auth/me/logout-all", null); @@ -3189,7 +3158,7 @@ await LoginAsync( x => x.ChainId, x => x.ActiveSessionId); - var logout = await client1.PostAsync( + var logout = await CsrfTestClient.PostAsync(client1, "/auth/me/logout-all", null); @@ -3320,26 +3289,22 @@ await LoginAsync( (await GetChainsAsync(victimClient)) .StatusCode.Should().Be(HttpStatusCode.OK); - var logout = await actorClient1.PostAsync( + var logout = await CsrfTestClient.PostAsync(actorClient1, "/auth/me/logout-all", null); logout.StatusCode.Should() .Be(HttpStatusCode.OK); - // // Both actor sessions must die. - // (await GetChainsAsync(actorClient1)) .StatusCode.Should().Be(HttpStatusCode.Unauthorized); (await GetChainsAsync(actorClient2)) .StatusCode.Should().Be(HttpStatusCode.Unauthorized); - // // Critical ownership invariant: // User B must remain completely unaffected. - // var victimVerification = await GetChainsAsync(victimClient); @@ -3355,7 +3320,7 @@ public async Task LogoutAllSelf_WithoutAuthentication_ShouldReturnUnauthorized() using var client = CreateClient( $"logout-all-anonymous-{Guid.NewGuid():N}"); - var response = await client.PostAsync( + var response = await CsrfTestClient.PostAsync(client, "/auth/me/logout-all", null); @@ -3415,7 +3380,7 @@ await LoginAsync( originalChains.Should().HaveCount(2); - var logout = await client1.PostAsync( + var logout = await CsrfTestClient.PostAsync(client1, "/auth/me/logout-all", null); @@ -3526,11 +3491,11 @@ await LoginAsync( actorCookie); var responses = await Task.WhenAll( - request1.PostAsync( + CsrfTestClient.PostAsync(request1, "/auth/me/logout-all", null), - request2.PostAsync( + CsrfTestClient.PostAsync(request2, "/auth/me/logout-all", null)); @@ -3594,7 +3559,7 @@ private HttpClient CreateClient(string deviceId) private static Task LoginAsync(HttpClient client, string identifier, string secret) { - return client.PostAsJsonAsync( + return CsrfTestClient.PostJsonAsync(client, "/auth/login", new { @@ -3605,7 +3570,7 @@ private static Task LoginAsync(HttpClient client, string id private static Task GetChainsAsync(HttpClient client) { - return client.PostAsJsonAsync( + return CsrfTestClient.PostJsonAsync(client, "/auth/me/sessions/chains", new PageRequest { diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/PkceFlowIntegrationTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/PkceFlowIntegrationTests.cs index ae851ccf..3ba88eeb 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration/PkceFlowIntegrationTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/PkceFlowIntegrationTests.cs @@ -557,7 +557,7 @@ private static Task CompleteAsync(HttpClient client, string returnUrl ?? string.Empty }; - return client.PostAsync( + return CsrfTestClient.PostAsync(client, CompleteEndpoint, new FormUrlEncodedContent(values)); } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/RateLimitingFlowTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/RateLimitingFlowTests.cs index d5883975..3463c6a5 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration/RateLimitingFlowTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/RateLimitingFlowTests.cs @@ -35,11 +35,11 @@ HttpClient Client(string device) using var second = Client("rate-limit-flow-second-123456"); foreach (var client in new[] { first, second }) { - var login = await client.PostAsJsonAsync("/auth/login", new { identifier = user.Identifier, secret = user.Secret }); + var login = await CsrfTestClient.PostJsonAsync(client, "/auth/login", new { identifier = user.Identifier, secret = user.Secret }); Assert.True(login.Headers.TryGetValues("Set-Cookie", out var cookies)); client.DefaultRequestHeaders.Add("Cookie", cookies!.First()); } - var rejected = await first.PostAsJsonAsync("/auth/login", new { identifier = user.Identifier, secret = "wrong" }); + var rejected = await CsrfTestClient.PostJsonAsync(first, "/auth/login", new { identifier = user.Identifier, secret = "wrong" }); Assert.Equal(HttpStatusCode.TooManyRequests, rejected.StatusCode); Assert.False(rejected.Headers.Contains("Set-Cookie")); using (var scope = factory.Services.CreateScope()) @@ -50,11 +50,11 @@ HttpClient Client(string device) } Assert.NotEqual(HttpStatusCode.TooManyRequests, (await first.PostAsync("/auth/logout", null)).StatusCode); // Two validated sessions of the same actor share logout capacity. - var logout = await second.PostAsync("/auth/logout", null); + var logout = await CsrfTestClient.PostAsync(second, "/auth/logout", null); Assert.Equal(HttpStatusCode.TooManyRequests, logout.StatusCode); Assert.False(logout.Headers.Contains("Set-Cookie")); // The rejected logout did not revoke the second session. - var validation = await second.PostAsync("/auth/validate", null); + var validation = await CsrfTestClient.PostAsync(second, "/auth/validate", null); Assert.Equal(HttpStatusCode.OK, validation.StatusCode); var result = await validation.Content.ReadFromJsonAsync(); Assert.True(result?.IsValid); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/RateLimitingProofFlowTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/RateLimitingProofFlowTests.cs index 2b9910fd..0e81c547 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration/RateLimitingProofFlowTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/RateLimitingProofFlowTests.cs @@ -30,14 +30,14 @@ public async Task PkceCompletion_RejectedRequestPreservesAuthorizationProof() using var client = Client(factory); static string Encode(byte[] value) => Convert.ToBase64String(value).TrimEnd('=').Replace('+', '-').Replace('/', '_'); var verifier = Encode(RandomNumberGenerator.GetBytes(32)); - var authorize = await client.PostAsync("/auth/pkce/authorize", new FormUrlEncodedContent(new Dictionary + var authorize = await CsrfTestClient.PostAsync(client, "/auth/pkce/authorize", new FormUrlEncodedContent(new Dictionary { ["code_challenge"] = Encode(SHA256.HashData(Encoding.ASCII.GetBytes(verifier))), ["challenge_method"] = "S256", ["redirect_uri"] = "/callback" })); Assert.Equal(HttpStatusCode.OK, authorize.StatusCode); var proof = await authorize.Content.ReadFromJsonAsync(); Assert.NotNull(proof); // Malformed payload consumes request quota without supplying the valid authorization proof. - Assert.Equal(HttpStatusCode.BadRequest, (await client.PostAsync("/auth/pkce/complete", new FormUrlEncodedContent(new Dictionary()))).StatusCode); - Task Complete() => client.PostAsync("/auth/pkce/complete", new FormUrlEncodedContent(new Dictionary + Assert.Equal(HttpStatusCode.BadRequest, (await CsrfTestClient.PostAsync(client, "/auth/pkce/complete", new FormUrlEncodedContent(new Dictionary()))).StatusCode); + Task Complete() => CsrfTestClient.PostAsync(client, "/auth/pkce/complete", new FormUrlEncodedContent(new Dictionary { ["authorization_code"] = proof.AuthorizationCode, ["code_verifier"] = verifier, @@ -61,7 +61,7 @@ public async Task Refresh_RejectedRequestPreservesRefreshToken_ThenSameTokenRota var user = await factory.CreateLoginUserAsync(); using var client = Client(factory); client.DefaultRequestHeaders.Add("X-UAuth-ClientProfile", "BlazorWasm"); - var login = await client.PostAsJsonAsync("/auth/login", new { identifier = user.Identifier, secret = user.Secret }); + var login = await CsrfTestClient.PostJsonAsync(client, "/auth/login", new { identifier = user.Identifier, secret = user.Secret }); var cookies = Cookies(login); var session = cookies.Split("; ").Single(c => c.StartsWith("uas=")); var refresh = cookies.Split("; ").Single(c => c.StartsWith("uar="))[4..]; @@ -71,14 +71,14 @@ public async Task Refresh_RejectedRequestPreservesRefreshToken_ThenSameTokenRota var before = await store.FindByHashAsync(hash); Assert.NotNull(before); client.DefaultRequestHeaders.Add("Cookie", session); - Assert.Equal(HttpStatusCode.Unauthorized, (await client.PostAsync("/auth/refresh", null)).StatusCode); + Assert.Equal(HttpStatusCode.Unauthorized, (await CsrfTestClient.PostAsync(client, "/auth/refresh", null)).StatusCode); client.DefaultRequestHeaders.Remove("Cookie"); client.DefaultRequestHeaders.Add("Cookie", cookies); - var rejected = await client.PostAsync("/auth/refresh", null); + var rejected = await CsrfTestClient.PostAsync(client, "/auth/refresh", null); Assert.Equal(HttpStatusCode.TooManyRequests, rejected.StatusCode); Assert.False(rejected.Headers.Contains("Set-Cookie")); Assert.Equal(before, await store.FindByHashAsync(hash)); - var renewed = await AfterReplenishment(() => client.PostAsync("/auth/refresh", null)); + var renewed = await AfterReplenishment(() => CsrfTestClient.PostAsync(client, "/auth/refresh", null)); Assert.Equal(HttpStatusCode.NoContent, renewed.StatusCode); Assert.True(renewed.Headers.Contains("Set-Cookie")); Assert.False((await store.FindByHashAsync(hash))!.IsActive(factory.Clock.UtcNow)); @@ -93,19 +93,19 @@ public async Task Recovery_RejectedRequestPreservesResetProof_ThenSameProofChang { services.RemoveAll(); services.AddSingleton(notifier); }); var user = await factory.CreateLoginUserAsync(); using var client = Client(factory); - var begin = await client.PostAsJsonAsync("/auth/me/credentials/reset/begin", new BeginResetCredentialRequest + var begin = await CsrfTestClient.PostJsonAsync(client, "/auth/me/credentials/reset/begin", new BeginResetCredentialRequest { Identifier = user.Identifier, CredentialType = CredentialType.Password, ResetCodeType = ResetCodeType.Token }); Assert.Equal(HttpStatusCode.OK, begin.StatusCode); Assert.NotNull(notifier.Last); // Consume quota using an unknown account, never presenting the issued proof. - await client.PostAsJsonAsync("/auth/me/credentials/reset/complete", new CompleteResetCredentialRequest + await CsrfTestClient.PostJsonAsync(client, "/auth/me/credentials/reset/complete", new CompleteResetCredentialRequest { Identifier = "unknown-account", CredentialType = CredentialType.Password, ResetToken = "invalid", NewSecret = "Changed-password-123!" }); var request = new CompleteResetCredentialRequest { Identifier = user.Identifier, CredentialType = CredentialType.Password, ResetToken = notifier.Last.Token, NewSecret = "Changed-password-123!" }; - Assert.Equal(HttpStatusCode.TooManyRequests, (await client.PostAsJsonAsync("/auth/me/credentials/reset/complete", request)).StatusCode); - var completed = await AfterReplenishment(() => client.PostAsJsonAsync("/auth/me/credentials/reset/complete", request)); + Assert.Equal(HttpStatusCode.TooManyRequests, (await CsrfTestClient.PostJsonAsync(client, "/auth/me/credentials/reset/complete", request)).StatusCode); + var completed = await AfterReplenishment(() => CsrfTestClient.PostJsonAsync(client, "/auth/me/credentials/reset/complete", request)); Assert.Equal(HttpStatusCode.OK, completed.StatusCode); - var login = await client.PostAsJsonAsync("/auth/login", new { identifier = user.Identifier, secret = request.NewSecret }); + var login = await CsrfTestClient.PostJsonAsync(client, "/auth/login", new { identifier = user.Identifier, secret = request.NewSecret }); Assert.True(login.Headers.Contains("Set-Cookie")); } @@ -119,12 +119,12 @@ public async Task AuthenticatedUsersOnSameConnectionIpHaveIndependentValidationB foreach (var client in new[] { first, second }) { var user = await factory.CreateLoginUserAsync(); - var login = await client.PostAsJsonAsync("/auth/login", new { identifier = user.Identifier, secret = user.Secret }); + var login = await CsrfTestClient.PostJsonAsync(client, "/auth/login", new { identifier = user.Identifier, secret = user.Secret }); client.DefaultRequestHeaders.Add("Cookie", Cookies(login)); - var validation = await client.PostAsync("/auth/validate", null); + var validation = await CsrfTestClient.PostAsync(client, "/auth/validate", null); Assert.Equal(HttpStatusCode.OK, validation.StatusCode); Assert.True((await validation.Content.ReadFromJsonAsync())!.IsValid); - Assert.Equal(HttpStatusCode.TooManyRequests, (await client.PostAsync("/auth/validate", null)).StatusCode); + Assert.Equal(HttpStatusCode.TooManyRequests, (await CsrfTestClient.PostAsync(client, "/auth/validate", null)).StatusCode); } } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/RateLimitingTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/RateLimitingTests.cs index ee55d5a5..da4ffeef 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration/RateLimitingTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/RateLimitingTests.cs @@ -178,12 +178,15 @@ public async Task EveryMappedEndpointHasOneClassification() /auth/uauthhub/entry|HubEntry|Sensitive /auth/users/create|UserCreateAnonymous|Sensitive /auth/validate|Validate|Frequent +/auth/antiforgery/token|AntiforgeryToken|Frequent """; var actual = routes.Cast().Select(endpoint => { var metadata = Assert.Single(endpoint.Metadata, m => m.GetType().Name == "UAuthRateLimitMetadata"); var type = metadata.GetType(); - Assert.Equal(new[] { "POST" }, endpoint.Metadata.GetMetadata()!.HttpMethods); + var expectedMethods = endpoint.RoutePattern.RawText == "/auth/antiforgery/token" ? new[] { "GET" } : new[] { "POST" }; + + Assert.Equal(expectedMethods, endpoint.Metadata.GetMetadata()!.HttpMethods); return $"{endpoint.RoutePattern.RawText}|{type.GetProperty("Endpoint")!.GetValue(metadata)}|{type.GetProperty("Group")!.GetValue(metadata)}"; }).OrderBy(x => x, StringComparer.Ordinal).ToArray(); Assert.Equal(expected.Split('\n').Select(x => x.Trim()).OrderBy(x => x, StringComparer.Ordinal), actual); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/RefreshTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/RefreshTests.cs index 6ea68db0..8725fb56 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration/RefreshTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/RefreshTests.cs @@ -394,7 +394,7 @@ await LoginAsync( cookie); var response = - await otherClient.PostAsync( + await CsrfTestClient.PostAsync(otherClient, "/auth/refresh", null); @@ -660,7 +660,7 @@ await LoginAsync( $"{sessionB}; {refreshA}"); var response = - await clientB.PostAsync( + await CsrfTestClient.PostAsync(clientB, "/auth/refresh", null); @@ -697,7 +697,7 @@ await LoginAsync( cookie); var response = - await foreignClient.PostAsync( + await CsrfTestClient.PostAsync(foreignClient, "/auth/refresh", null); @@ -709,7 +709,7 @@ await foreignClient.PostAsync( // the legitimate credential set. // var legitimateRefresh = - await originalClient.PostAsync( + await CsrfTestClient.PostAsync(originalClient, "/auth/refresh", null); @@ -831,8 +831,8 @@ await LoginAsync( var responses = await Task.WhenAll( - clientA.PostAsync("/auth/refresh", null), - clientB.PostAsync("/auth/refresh", null)); + CsrfTestClient.PostAsync(clientA, "/auth/refresh", null), + CsrfTestClient.PostAsync(clientB, "/auth/refresh", null)); responses.Count(x => x.StatusCode == HttpStatusCode.NoContent) @@ -889,8 +889,8 @@ await LoginAsync( var responses = await Task.WhenAll( - clientA.PostAsync("/auth/refresh", null), - clientB.PostAsync("/auth/refresh", null)); + CsrfTestClient.PostAsync(clientA, "/auth/refresh", null), + CsrfTestClient.PostAsync(clientB, "/auth/refresh", null)); var winner = responses.Single(x => @@ -920,7 +920,7 @@ await Task.WhenAll( winningCredentials); var result = - await continuation.PostAsync( + await CsrfTestClient.PostAsync(continuation, "/auth/refresh", null); @@ -967,8 +967,8 @@ await LoginAsync( var responses = await Task.WhenAll( - clientA.PostAsync("/auth/refresh", null), - clientB.PostAsync("/auth/refresh", null)); + CsrfTestClient.PostAsync(clientA, "/auth/refresh", null), + CsrfTestClient.PostAsync(clientB, "/auth/refresh", null)); responses.Count(x => x.StatusCode == HttpStatusCode.NoContent) @@ -990,7 +990,7 @@ await Task.WhenAll( originalCookies); var replay = - await replayClient.PostAsync( + await CsrfTestClient.PostAsync(replayClient, "/auth/refresh", null); @@ -1040,8 +1040,8 @@ await LoginAsync( var responses = await Task.WhenAll( - clientA.PostAsync("/auth/refresh", null), - clientB.PostAsync("/auth/refresh", null)); + CsrfTestClient.PostAsync(clientA, "/auth/refresh", null), + CsrfTestClient.PostAsync(clientB, "/auth/refresh", null)); var success = responses.Single(x => @@ -1121,7 +1121,7 @@ await store.ExecuteAsync( client.DefaultRequestHeaders.Add("Cookie", cookies); var response = - await client.PostAsync("/auth/refresh", null); + await CsrfTestClient.PostAsync(client, "/auth/refresh", null); response.StatusCode.Should() .Be(HttpStatusCode.Unauthorized); @@ -1163,8 +1163,7 @@ await store.ExecuteAsync( client.DefaultRequestHeaders.Remove("Cookie"); client.DefaultRequestHeaders.Add("Cookie", cookies); - var response = - await client.PostAsync("/auth/refresh", null); + var response = await CsrfTestClient.PostAsync(client, "/auth/refresh", null); response.StatusCode.Should() .Be(HttpStatusCode.Unauthorized); @@ -1208,8 +1207,7 @@ await store.ExecuteAsync( client.DefaultRequestHeaders.Remove("Cookie"); client.DefaultRequestHeaders.Add("Cookie", cookies); - var response = - await client.PostAsync("/auth/refresh", null); + var response = await CsrfTestClient.PostAsync(client, "/auth/refresh", null); response.StatusCode.Should() .Be(HttpStatusCode.Unauthorized); @@ -1231,20 +1229,16 @@ await LoginAsync( user, "BlazorWasm"); - // // R0 -> R1 - // var first = - await loginClient.PostAsync( + await CsrfTestClient.PostAsync(loginClient, "/auth/refresh", null); first.StatusCode.Should() .Be(HttpStatusCode.NoContent); - // // Deliberately replay R0. - // using var replayClient = CreateClient(deviceId); @@ -1257,7 +1251,7 @@ await loginClient.PostAsync( originalCookies); var replay = - await replayClient.PostAsync( + await CsrfTestClient.PostAsync(replayClient, "/auth/refresh", null); @@ -1291,11 +1285,9 @@ await LoginAsync( user, "BlazorWasm"); - // // R0 -> R1 - // var rotation = - await client.PostAsync( + await CsrfTestClient.PostAsync(client, "/auth/refresh", null); @@ -1307,15 +1299,11 @@ await client.PostAsync( originalCookies, rotation); - // // Move OUTSIDE the concurrency tolerance window. - // factory.Clock.Advance( TimeSpan.FromSeconds(3)); - // // Replay R0. - // using var attacker = CreateClient(factory, deviceId); @@ -1328,7 +1316,7 @@ await client.PostAsync( originalCookies); var replay = - await attacker.PostAsync( + await CsrfTestClient.PostAsync(attacker, "/auth/refresh", null); @@ -1351,7 +1339,7 @@ await attacker.PostAsync( replacementCookies); var legitimateRefresh = - await legitimateClient.PostAsync( + await CsrfTestClient.PostAsync(legitimateClient, "/auth/refresh", null); @@ -1378,7 +1366,7 @@ public async Task Refresh_Hybrid_DuplicateWithinConcurrencyWindow_ShouldNotInval var originalCookies = await LoginAsync(client, user, "BlazorWasm"); // R0 -> R1 - var rotation = await client.PostAsync("/auth/refresh", null); + var rotation = await CsrfTestClient.PostAsync(client, "/auth/refresh", null); rotation.StatusCode.Should().Be(HttpStatusCode.NoContent); @@ -1392,7 +1380,7 @@ public async Task Refresh_Hybrid_DuplicateWithinConcurrencyWindow_ShouldNotInval duplicate.DefaultRequestHeaders.Add("Cookie", originalCookies); - var duplicateResponse = await duplicate.PostAsync("/auth/refresh", null); + var duplicateResponse = await CsrfTestClient.PostAsync(duplicate, "/auth/refresh", null); duplicateResponse.StatusCode.Should().Be(HttpStatusCode.Unauthorized); @@ -1404,7 +1392,7 @@ public async Task Refresh_Hybrid_DuplicateWithinConcurrencyWindow_ShouldNotInval legitimate.DefaultRequestHeaders.Add("Cookie", replacementCookies); - var continuation = await legitimate.PostAsync("/auth/refresh", null); + var continuation = await CsrfTestClient.PostAsync(legitimate, "/auth/refresh", null); continuation.StatusCode.Should().Be(HttpStatusCode.NoContent); } @@ -1450,21 +1438,17 @@ await LoginAsync( chainA.ChainId.Should() .NotBe(chainB.ChainId); - // // Rotate device A: // A:R0 -> A:R1 - // var rotationA = - await clientA.PostAsync( + await CsrfTestClient.PostAsync(clientA, "/auth/refresh", null); rotationA.StatusCode.Should() .Be(HttpStatusCode.NoContent); - // // Replay A:R0. - // using var attacker = CreateClient(deviceA); @@ -1477,17 +1461,15 @@ await clientA.PostAsync( originalCookiesA); var replay = - await attacker.PostAsync( + await CsrfTestClient.PostAsync(attacker, "/auth/refresh", null); replay.StatusCode.Should() .Be(HttpStatusCode.Unauthorized); - // // Device B belongs to another chain. // Compromise of A must not kill B. - // using var safeClient = CreateClient(deviceB); @@ -1500,7 +1482,7 @@ await attacker.PostAsync( originalCookiesB); var safeRefresh = - await safeClient.PostAsync( + await CsrfTestClient.PostAsync(safeClient, "/auth/refresh", null); @@ -1529,7 +1511,7 @@ public async Task Refresh_Hybrid_JustBeforeRefreshTokenExpiry_ShouldSucceed() client.DefaultRequestHeaders.Remove("Cookie"); client.DefaultRequestHeaders.Add("Cookie", cookies); - var response = await client.PostAsync( + var response = await CsrfTestClient.PostAsync(client, "/auth/refresh", null); @@ -1558,7 +1540,7 @@ public async Task Refresh_Hybrid_AtRefreshTokenExpiry_ShouldReturnUnauthorized() client.DefaultRequestHeaders.Remove("Cookie"); client.DefaultRequestHeaders.Add("Cookie", cookies); - var response = await client.PostAsync( + var response = await CsrfTestClient.PostAsync(client, "/auth/refresh", null); @@ -1587,7 +1569,7 @@ public async Task Refresh_Hybrid_AfterRefreshTokenExpiry_ShouldReturnUnauthorize client.DefaultRequestHeaders.Remove("Cookie"); client.DefaultRequestHeaders.Add("Cookie", cookies); - var response = await client.PostAsync( + var response = await CsrfTestClient.PostAsync(client, "/auth/refresh", null); @@ -1632,7 +1614,7 @@ await LoginAsync( cookies); var response = - await client.PostAsync( + await CsrfTestClient.PostAsync(client, "/auth/refresh", null); @@ -1677,7 +1659,7 @@ await LoginAsync( cookies); var response = - await client.PostAsync( + await CsrfTestClient.PostAsync(client, "/auth/refresh", null); @@ -1723,7 +1705,7 @@ await LoginAsync( cookies); var response = - await client.PostAsync( + await CsrfTestClient.PostAsync(client, "/auth/refresh", null); @@ -1768,7 +1750,7 @@ await LoginAsync( cookies); var response = - await client.PostAsync( + await CsrfTestClient.PostAsync(client, "/auth/refresh", null); @@ -1804,7 +1786,7 @@ await LoginAsync( GetCookie(originalCookies, "uar"); var response = - await client.PostAsync( + await CsrfTestClient.PostAsync(client, "/auth/refresh", null); @@ -1848,17 +1830,15 @@ await LoginAsync( GetCookie(originalCookies, "uas"); var response = - await client.PostAsync( + await CsrfTestClient.PostAsync(client, "/auth/refresh", null); response.StatusCode.Should() .Be(HttpStatusCode.NoContent); - // // Hybrid refresh rotates the refresh credential. // Session identity itself must remain stable. - // var rotatedCredentials = BuildHybridCredentialAfterRefresh( originalCookies, @@ -1891,9 +1871,7 @@ await LoginAsync( user, "BlazorWasm"); - // // Remove refresh credential while preserving session. - // var session = GetCookie(originalCookies, "uas"); @@ -1904,7 +1882,7 @@ await LoginAsync( session); var response = - await client.PostAsync( + await CsrfTestClient.PostAsync(client, "/auth/refresh", null); @@ -1945,20 +1923,16 @@ await LoginAsync( user, "BlazorWasm"); - // // R0 -> R1 - // var rotation = - await client.PostAsync( + await CsrfTestClient.PostAsync(client, "/auth/refresh", null); rotation.StatusCode.Should() .Be(HttpStatusCode.NoContent); - // // Replay R0. - // using var replayClient = CreateClient(deviceId); @@ -1971,7 +1945,7 @@ await client.PostAsync( originalCookies); var replay = - await replayClient.PostAsync( + await CsrfTestClient.PostAsync(replayClient, "/auth/refresh", null); @@ -2036,10 +2010,10 @@ await LoginAsync( var responses = await Task.WhenAll( - clientA.PostAsync( + CsrfTestClient.PostAsync(clientA, "/auth/refresh", null), - clientB.PostAsync( + CsrfTestClient.PostAsync(clientB, "/auth/refresh", null)); @@ -2191,9 +2165,7 @@ await LoginAsync( userA, "BlazorWasm"); - // // Same credential is now presented under Tenant B. - // using var tenantBClient = CreateClient( factory, @@ -2212,7 +2184,7 @@ await LoginAsync( cookiesA); var response = - await tenantBClient.PostAsync( + await CsrfTestClient.PostAsync(tenantBClient, "/auth/refresh", null); @@ -2258,9 +2230,7 @@ await LoginAsync( userA, "BlazorWasm"); - // // Attack / accidental cross-tenant presentation. - // using var tenantBClient = CreateClient( factory, @@ -2279,7 +2249,7 @@ await LoginAsync( cookiesA); var crossTenant = - await tenantBClient.PostAsync( + await CsrfTestClient.PostAsync(tenantBClient, "/auth/refresh", null); @@ -2307,7 +2277,7 @@ await tenantBClient.PostAsync( cookiesA); var legitimate = - await tenantAClient.PostAsync( + await CsrfTestClient.PostAsync(tenantAClient, "/auth/refresh", null); @@ -2372,10 +2342,10 @@ await LoginAsync( var responses = await Task.WhenAll( - clientA.PostAsync( + CsrfTestClient.PostAsync(clientA, "/auth/refresh", null), - clientB.PostAsync( + CsrfTestClient.PostAsync(clientB, "/auth/refresh", null)); @@ -2461,7 +2431,7 @@ await LoginAsync( mixedCredential); var response = - await attacker.PostAsync( + await CsrfTestClient.PostAsync(attacker, "/auth/refresh", null); @@ -2481,8 +2451,9 @@ public async Task Refresh_Hybrid_Maui_HeaderCredentials_ShouldSupportConsecutive SetClientProfile(client, "Maui"); - // 1. Login - using var login = await client.PostAsJsonAsync( + // Login with CSRF protection. + using var login = await CsrfTestClient.PostJsonAsync( + client, "/auth/login", new { @@ -2498,42 +2469,55 @@ public async Task Refresh_Hybrid_Maui_HeaderCredentials_ShouldSupportConsecutive sessionId.Should().NotBeNullOrWhiteSpace(); refreshToken.Should().NotBeNullOrWhiteSpace(); - // 2. First refresh using the credentials delivered by login + // First refresh using MAUI header credentials. using var firstRequest = new HttpRequestMessage( - HttpMethod.Post, "/auth/refresh"); + HttpMethod.Post, + "/auth/refresh"); firstRequest.Headers.TryAddWithoutValidation( - "X-UAuth-Session", sessionId); + "X-UAuth-Session", + sessionId); firstRequest.Headers.TryAddWithoutValidation( - "X-Refresh-Token", refreshToken); + "X-Refresh-Token", + refreshToken); - using var first = await client.SendAsync(firstRequest); + using var first = await CsrfTestClient.SendAsync( + client, + firstRequest); - first.StatusCode.Should().Be(HttpStatusCode.NoContent); + first.StatusCode.Should().Be( + HttpStatusCode.NoContent, + $"first refresh failed: {await first.Content.ReadAsStringAsync()}"); var rotatedRefreshToken = ReadCredential(first, "uar"); rotatedRefreshToken.Should().NotBeNullOrWhiteSpace(); rotatedRefreshToken.Should().NotBe(refreshToken); - // Session ID may be reissued depending on the refresh policy. var nextSessionId = TryReadCredential(first, "uas") ?? sessionId; - // 3. Second refresh using the newly delivered refresh token + // Second refresh using rotated credentials. using var secondRequest = new HttpRequestMessage( - HttpMethod.Post, "/auth/refresh"); + HttpMethod.Post, + "/auth/refresh"); secondRequest.Headers.TryAddWithoutValidation( - "X-UAuth-Session", nextSessionId); + "X-UAuth-Session", + nextSessionId); secondRequest.Headers.TryAddWithoutValidation( - "X-Refresh-Token", rotatedRefreshToken); + "X-Refresh-Token", + rotatedRefreshToken); - using var second = await client.SendAsync(secondRequest); + using var second = await CsrfTestClient.SendAsync( + client, + secondRequest); - second.StatusCode.Should().Be(HttpStatusCode.NoContent); + second.StatusCode.Should().Be( + HttpStatusCode.NoContent, + $"second refresh failed: {await second.Content.ReadAsStringAsync()}"); } @@ -2542,7 +2526,7 @@ private async Task LoginAsync(IntegrationTestUser user, string profile = { SetClientProfile(profile); - var response = await _client.PostAsJsonAsync( + var response = await CsrfTestClient.PostJsonAsync(_client, "/auth/login", new { @@ -2578,7 +2562,7 @@ private static async Task LoginAsync(HttpClient client, IntegrationTestU profile); var response = - await client.PostAsJsonAsync( + await CsrfTestClient.PostJsonAsync(client, "/auth/login", new { @@ -2599,7 +2583,7 @@ await client.PostAsJsonAsync( private Task RefreshAsync() { - return _client.PostAsync("/auth/refresh", null); + return CsrfTestClient.PostAsync(_client, "/auth/refresh", null); } private ISessionStore GetSessionStore() @@ -2644,7 +2628,7 @@ private static async Task GetCurrentChainAsync(HttpClient c private static async Task GetChainsAsync(HttpClient client) { - return await client.PostAsJsonAsync( + return await CsrfTestClient.PostJsonAsync(client, "/auth/me/sessions/chains", new PageRequest { @@ -2751,14 +2735,9 @@ private async Task RevokeRootDirectlyAsync(UserKey userKey) private static string BuildHybridCredentialAfterRefresh(string originalCookies, HttpResponseMessage refreshResponse) { - var session = - GetCookie(originalCookies, "uas"); - - var refreshed = - BuildCookieHeader(refreshResponse); - - var refresh = - GetCookie(refreshed, "uar"); + var session = GetCookie(originalCookies, "uas"); + var refreshed = BuildCookieHeader(refreshResponse); + var refresh = GetCookie(refreshed, "uar"); return $"{session}; {refresh}"; } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/SdkEnpointContractTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/SdkEnpointContractTests.cs index 2dbcd496..5f4d16da 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration/SdkEnpointContractTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/SdkEnpointContractTests.cs @@ -27,7 +27,7 @@ public async Task ChangeMyStatus_ShouldReturnTypedJson_NotEmpty200() var user = await factory.CreateLoginUserAsync(); using var client = CreateClient(factory); await AuthenticateAsync(client, user); - using var response = await client.PostAsJsonAsync("/auth/me/status", + using var response = await CsrfTestClient.PostJsonAsync(client, "/auth/me/status", new ChangeUserStatusSelfRequest { NewStatus = SelfAssignableUserStatus.SelfSuspended }); var result = await AssertTypedSuccessAsync(response); result.IsSuccess.Should().BeTrue(); @@ -44,7 +44,8 @@ public async Task ChangeUserStatus_ShouldReturnTypedJson_NotEmpty200() await factory.GrantPermissionsAsync(admin.UserKey, [UAuthActions.Users.ChangeStatusAdmin]); using var client = CreateClient(factory); await AuthenticateAsync(client, admin); - using var response = await client.PostAsJsonAsync( + using var response = await CsrfTestClient.PostJsonAsync( + client, $"/auth/admin/users/{target.UserKey.Value}/status", new ChangeUserStatusAdminRequest { NewStatus = AdminAssignableUserStatus.Suspended }); var result = await AssertTypedSuccessAsync(response); @@ -64,9 +65,9 @@ public async Task ChangeUserStatus_AlreadyInRequestedStatus_ShouldReturnTypedJso await AuthenticateAsync(client, admin); var url = $"/auth/admin/users/{target.UserKey.Value}/status"; var request = new ChangeUserStatusAdminRequest { NewStatus = AdminAssignableUserStatus.Suspended }; - using var first = await client.PostAsJsonAsync(url, request); + using var first = await CsrfTestClient.PostJsonAsync(client, url, request); (await AssertTypedSuccessAsync(first)).IsSuccess.Should().BeTrue(); - using var second = await client.PostAsJsonAsync(url, request); + using var second = await CsrfTestClient.PostJsonAsync(client, url, request); var result = await AssertTypedSuccessAsync(second); result.IsSuccess.Should().BeTrue(); result.PreviousStatus.Should().Be(UserStatus.Suspended); @@ -82,7 +83,8 @@ public async Task DeleteUser_ShouldReturnTypedJson_WithRequestedMode() await factory.GrantPermissionsAsync(admin.UserKey, [UAuthActions.Users.DeleteAdmin]); using var client = CreateClient(factory); await AuthenticateAsync(client, admin); - using var response = await client.PostAsJsonAsync( + using var response = await CsrfTestClient.PostJsonAsync( + client, $"/auth/admin/users/{target.UserKey.Value}/delete", new DeleteUserRequest { Mode = DeleteMode.Soft }); var result = await AssertTypedSuccessAsync(response); @@ -100,7 +102,8 @@ public async Task AdminBeginReset_ShouldReturnTypedJson_WithoutProof() await factory.GrantPermissionsAsync(admin.UserKey, [UAuthActions.Credentials.BeginResetAdmin]); using var client = CreateClient(factory); await AuthenticateAsync(client, admin); - using var response = await client.PostAsJsonAsync( + using var response = await CsrfTestClient.PostJsonAsync( + client, $"/auth/admin/users/{target.UserKey.Value}/credentials/reset/begin", BeginResetRequest(target)); var result = await AssertTypedSuccessAsync(response); result.ExpiresAt.Should().BeAfter(factory.Clock.UtcNow); @@ -121,11 +124,11 @@ await factory.GrantPermissionsAsync(admin.UserKey, using var client = CreateClient(factory); await AuthenticateAsync(client, admin); var url = $"/auth/admin/users/{target.UserKey.Value}/credentials/reset"; - using var begin = await client.PostAsJsonAsync($"{url}/begin", BeginResetRequest(target)); + using var begin = await CsrfTestClient.PostJsonAsync(client, $"{url}/begin", BeginResetRequest(target)); await AssertTypedSuccessAsync(begin); var token = notifier.Notifications.Single(x => x.UserKey == target.UserKey).Token; var newSecret = $"Changed-{Guid.NewGuid():N}!"; - using var complete = await client.PostAsJsonAsync($"{url}/complete", + using var complete = await CsrfTestClient.PostJsonAsync(client, $"{url}/complete", new CompleteResetCredentialRequest { Identifier = target.Identifier, @@ -161,7 +164,7 @@ public async Task RevokeMy_UnknownCredential_ShouldNotReturnSuccessful204() var user = await factory.CreateLoginUserAsync(); using var client = CreateClient(factory); await AuthenticateAsync(client, user); - using var response = await client.PostAsJsonAsync("/auth/me/credentials/revoke", + using var response = await CsrfTestClient.PostJsonAsync(client, "/auth/me/credentials/revoke", new RevokeCredentialRequest { Id = Guid.NewGuid() }); response.StatusCode.Should().Be(HttpStatusCode.NotFound); } @@ -175,7 +178,7 @@ public async Task RevokeAdmin_UnknownCredential_ShouldNotReturnSuccessful204() await factory.GrantPermissionsAsync(admin.UserKey, [UAuthActions.Credentials.RevokeAdmin]); using var client = CreateClient(factory); await AuthenticateAsync(client, admin); - using var response = await client.PostAsJsonAsync( + using var response = await CsrfTestClient.PostJsonAsync(client, $"/auth/admin/users/{target.UserKey.Value}/credentials/revoke", new RevokeCredentialRequest { Id = Guid.NewGuid() }); response.StatusCode.Should().Be(HttpStatusCode.NotFound); @@ -190,7 +193,7 @@ public async Task DeleteCredentialAdmin_UnknownCredential_ShouldNotReturnSuccess await factory.GrantPermissionsAsync(admin.UserKey, [UAuthActions.Credentials.DeleteAdmin]); using var client = CreateClient(factory); await AuthenticateAsync(client, admin); - using var response = await client.PostAsJsonAsync( + using var response = await CsrfTestClient.PostJsonAsync(client, $"/auth/admin/users/{target.UserKey.Value}/credentials/delete", new DeleteCredentialRequest { Id = Guid.NewGuid(), Mode = DeleteMode.Soft }); response.StatusCode.Should().Be(HttpStatusCode.NotFound); @@ -247,7 +250,7 @@ private static HttpClient CreateClient(AuthServerFactory factory) private static async Task AuthenticateAsync(HttpClient client, IntegrationTestUser user) { - using var response = await client.PostAsJsonAsync("/auth/login", new + using var response = await CsrfTestClient.PostJsonAsync(client, "/auth/login", new { identifier = user.Identifier, secret = user.Secret diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/Security/CsrfAttackTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/Security/CsrfAttackTests.cs new file mode 100644 index 00000000..1050c53a --- /dev/null +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/Security/CsrfAttackTests.cs @@ -0,0 +1,345 @@ +using System.Net; +using System.Net.Http.Json; +using System.Text.Json; +using AwesomeAssertions; +using CodeBeam.UltimateAuth.Tests.Integration.Infrastructure; +using Microsoft.AspNetCore.Mvc.Testing; + +namespace CodeBeam.UltimateAuth.Tests.Integration.Security; + +public sealed class CsrfAttackTests +{ + [Fact] + public async Task Login_WithoutCsrfToken_MustNotCreateSession() + { + await using var factory = new AuthServerFactory(); + var attacker = await factory.CreateLoginUserAsync(); + + using var victimBrowser = CreateBrowser(factory); + + using var response = await victimBrowser.PostAsJsonAsync( + "/auth/login", + new + { + identifier = attacker.Identifier, + secret = attacker.Secret + }); + + await AssertCsrfRejected(response); + AssertNoSessionCookie(response); + } + + [Fact] + public async Task Login_WithForgedCsrfToken_MustNotCreateSession() + { + await using var factory = new AuthServerFactory(); + var attacker = await factory.CreateLoginUserAsync(); + + using var victimBrowser = CreateBrowser(factory); + + using var request = new HttpRequestMessage( + HttpMethod.Post, "/auth/login") + { + Content = JsonContent.Create(new + { + identifier = attacker.Identifier, + secret = attacker.Secret + }) + }; + + request.Headers.TryAddWithoutValidation( + "RequestVerificationToken", + "attacker-forged-token"); + + using var response = await victimBrowser.SendAsync(request); + + await AssertCsrfRejected(response); + AssertNoSessionCookie(response); + } + + [Fact] + public async Task Login_WithTokenFromAnotherBrowser_MustBeRejected() + { + await using var factory = new AuthServerFactory(); + var attacker = await factory.CreateLoginUserAsync(); + + using var attackerBrowser = CreateBrowser(factory); + using var victimBrowser = CreateBrowser(factory); + + var attackerTokens = await GetTokens(attackerBrowser); + var victimTokens = await GetTokens(victimBrowser); + + // Victim browser has its own antiforgery cookie, + // but attacker supplies a token from a different browser. + using var request = new HttpRequestMessage( + HttpMethod.Post, "/auth/login") + { + Content = JsonContent.Create(new + { + identifier = attacker.Identifier, + secret = attacker.Secret + }) + }; + + request.Headers.TryAddWithoutValidation( + attackerTokens.HeaderName, + attackerTokens.RequestToken); + + request.Headers.TryAddWithoutValidation( + "Cookie", + victimTokens.Cookie); + + using var response = await victimBrowser.SendAsync(request); + + await AssertCsrfRejected(response); + AssertNoSessionCookie(response); + } + + [Fact] + public async Task Login_WithValidCsrfToken_ShouldSucceed() + { + await using var factory = new AuthServerFactory(); + var user = await factory.CreateLoginUserAsync(); + + using var browser = CreateBrowser(factory); + var tokens = await GetTokens(browser); + + using var request = new HttpRequestMessage( + HttpMethod.Post, "/auth/login") + { + Content = JsonContent.Create(new + { + identifier = user.Identifier, + secret = user.Secret + }) + }; + + request.Headers.TryAddWithoutValidation( + tokens.HeaderName, + tokens.RequestToken); + + request.Headers.TryAddWithoutValidation( + "Cookie", + tokens.Cookie); + + using var response = await browser.SendAsync(request); + + response.StatusCode.Should().Be(HttpStatusCode.Found); + + response.Headers.TryGetValues( + "Set-Cookie", out var cookies).Should().BeTrue(); + + cookies!.Should().Contain(x => + x.StartsWith("uas=", StringComparison.OrdinalIgnoreCase)); + } + + + [Fact] + public async Task RevokeOthers_CsrfAttack_MustNotRevokeSessions() + { + await using var factory = new AuthServerFactory(); + var user = await factory.CreateLoginUserAsync(); + + using var currentBrowser = CreateBrowser(factory); + using var otherBrowser = CreateBrowser(factory); + + // Create two independent authenticated sessions. + var currentCookie = await LoginAndGetSessionCookie( + currentBrowser, user); + + var otherCookie = await LoginAndGetSessionCookie( + otherBrowser, user); + + SetSessionCookie(currentBrowser, currentCookie); + SetSessionCookie(otherBrowser, otherCookie); + + // Verify both sessions are initially usable. + await AssertAuthenticated(currentBrowser); + await AssertAuthenticated(otherBrowser); + + // ATTACK 1: Authenticated request without CSRF token. + using (var missingToken = await currentBrowser.PostAsync( + "/auth/me/sessions/revoke-others", null)) + { + await AssertCsrfRejected(missingToken); + } + + // The attack must not revoke the other session. + await AssertAuthenticated(otherBrowser); + + // ATTACK 2: Forged antiforgery token. + using (var forgedRequest = new HttpRequestMessage( + HttpMethod.Post, + "/auth/me/sessions/revoke-others")) + { + forgedRequest.Headers.TryAddWithoutValidation( + "RequestVerificationToken", + "forged-token"); + + using var forged = await currentBrowser.SendAsync( + forgedRequest); + + await AssertCsrfRejected(forged); + } + + // The second attack must not revoke the other session. + await AssertAuthenticated(otherBrowser); + + // CONTROL: Valid CSRF token must allow the operation. + using (var valid = await CsrfTestClient.PostAsync( + currentBrowser, + "/auth/me/sessions/revoke-others")) + { + valid.IsSuccessStatusCode.Should().BeTrue( + $"valid revoke failed: " + + await valid.Content.ReadAsStringAsync()); + } + + // The current session must survive revoke-others. + await AssertAuthenticated(currentBrowser); + + // The other session must now be revoked. + using var revokedCheck = await CsrfTestClient.PostJsonAsync( + otherBrowser, + "/auth/me/identifiers/get", + new { }); + + revokedCheck.StatusCode.Should().Be( + HttpStatusCode.Unauthorized); + } + + private static async Task LoginAndGetSessionCookie( + HttpClient browser, + IntegrationTestUser user) + { + var tokens = await GetTokens(browser); + + using var request = new HttpRequestMessage( + HttpMethod.Post, "/auth/login") + { + Content = JsonContent.Create(new + { + identifier = user.Identifier, + secret = user.Secret + }) + }; + + request.Headers.TryAddWithoutValidation( + tokens.HeaderName, + tokens.RequestToken); + + request.Headers.TryAddWithoutValidation( + "Cookie", + tokens.Cookie); + + using var response = await browser.SendAsync(request); + + response.StatusCode.Should().Be(HttpStatusCode.Found); + + response.Headers.TryGetValues( + "Set-Cookie", out var cookies).Should().BeTrue(); + + return cookies! + .Select(x => x.Split(';', 2)[0]) + .Single(x => x.StartsWith( + "uas=", + StringComparison.OrdinalIgnoreCase)); + } + + private static void SetSessionCookie( + HttpClient browser, + string sessionCookie) + { + browser.DefaultRequestHeaders.Remove("Cookie"); + browser.DefaultRequestHeaders.TryAddWithoutValidation( + "Cookie", + sessionCookie); + } + + private static async Task AssertAuthenticated( + HttpClient browser) + { + using var response = await CsrfTestClient.PostJsonAsync( + browser, + "/auth/me/identifiers/get", + new { }); + + response.StatusCode.Should().Be( + HttpStatusCode.OK, + $"session should remain authenticated: " + + await response.Content.ReadAsStringAsync()); + } + + + + private static HttpClient CreateBrowser(AuthServerFactory factory) + { + var client = factory.CreateClient( + new WebApplicationFactoryClientOptions + { + AllowAutoRedirect = false, + HandleCookies = false + }); + + client.DefaultRequestHeaders.Add( + "Origin", "https://localhost:6130"); + + client.DefaultRequestHeaders.Add( + "X-UDID", $"csrf-attack-{Guid.NewGuid():N}"); + + return client; + } + + private static async Task GetTokens(HttpClient client) + { + using var response = await client.GetAsync( + "/auth/antiforgery/token"); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + + using var json = JsonDocument.Parse( + await response.Content.ReadAsStringAsync()); + + var token = json.RootElement + .GetProperty("requestToken").GetString()!; + + var headerName = json.RootElement + .GetProperty("headerName").GetString()!; + + response.Headers.TryGetValues( + "Set-Cookie", out var cookies).Should().BeTrue(); + + var cookie = cookies! + .Select(x => x.Split(';', 2)[0]) + .Single(x => x.StartsWith( + ".AspNetCore.Antiforgery.", + StringComparison.OrdinalIgnoreCase)); + + return new CsrfTokens(token, headerName, cookie); + } + + private static async Task AssertCsrfRejected( + HttpResponseMessage response) + { + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + + var body = await response.Content.ReadAsStringAsync(); + + body.Should().Contain("invalid_antiforgery_token"); + } + + private static void AssertNoSessionCookie( + HttpResponseMessage response) + { + if (!response.Headers.TryGetValues("Set-Cookie", out var cookies)) + return; + + cookies.Should().NotContain(x => + x.StartsWith("uas=", StringComparison.OrdinalIgnoreCase)); + } + + private sealed record CsrfTokens( + string RequestToken, + string HeaderName, + string Cookie); +} diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/Security/UAuthAntiforgeryEndpointTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/Security/UAuthAntiforgeryEndpointTests.cs new file mode 100644 index 00000000..a926f892 --- /dev/null +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/Security/UAuthAntiforgeryEndpointTests.cs @@ -0,0 +1,204 @@ +using System.Net; +using System.Net.Http.Json; +using System.Text.Json; +using CodeBeam.UltimateAuth.Tests.Integration.Infrastructure; +using Microsoft.AspNetCore.Mvc.Testing; + +namespace CodeBeam.UltimateAuth.Tests.Integration.Security; + +public sealed class UAuthAntiforgeryEndpointTests +{ + private sealed record TokenResponse( + string RequestToken, + string HeaderName, + string FormFieldName); + + [Fact] + public async Task TokenEndpoint_ReturnsValidTokenAndNoStore() + { + await using var factory = new AuthServerFactory(); + + using var client = factory.CreateClient(); + + using var response = await client.GetAsync( + "/auth/antiforgery/token"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + Assert.Contains( + "no-store", + response.Headers.CacheControl?.ToString() ?? ""); + + var token = await response.Content + .ReadFromJsonAsync(); + + Assert.NotNull(token); + Assert.False(string.IsNullOrWhiteSpace(token.RequestToken)); + Assert.False(string.IsNullOrWhiteSpace(token.HeaderName)); + Assert.False(string.IsNullOrWhiteSpace(token.FormFieldName)); + + Assert.Contains( + response.Headers.TryGetValues("Set-Cookie", out var cookies) + ? cookies + : Array.Empty(), + cookie => cookie.Contains("Antiforgery")); + } + + [Fact] + public async Task Login_WithoutToken_IsRejected() + { + await using var factory = new AuthServerFactory(); + + using var client = factory.CreateClient( + new WebApplicationFactoryClientOptions + { + AllowAutoRedirect = false, + HandleCookies = true + }); + + using var response = await client.PostAsJsonAsync( + "/auth/login", + new + { + identifier = "csrf-test-user", + secret = "irrelevant" + }); + + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + + Assert.Contains( + "invalid_antiforgery_token", + body); + } + + [Fact] + public async Task Login_WithInvalidToken_IsRejected() + { + await using var factory = new AuthServerFactory(); + + using var client = factory.CreateClient( + new WebApplicationFactoryClientOptions + { + AllowAutoRedirect = false, + HandleCookies = true + }); + + await client.GetAsync("/auth/antiforgery/token"); + + using var request = new HttpRequestMessage( + HttpMethod.Post, + "/auth/login") + { + Content = JsonContent.Create(new + { + identifier = "csrf-test-user", + secret = "irrelevant" + }) + }; + + request.Headers.Add( + "X-UAuth-CSRF", + "invalid-token"); + + using var response = await client.SendAsync(request); + + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + } + + [Fact] + public async Task Login_WithValidToken_PassesCsrfValidation() + { + await using var factory = new AuthServerFactory(); + + using var client = factory.CreateClient( + new WebApplicationFactoryClientOptions + { + AllowAutoRedirect = false, + HandleCookies = true + }); + + var token = await client.GetFromJsonAsync( + "/auth/antiforgery/token"); + + Assert.NotNull(token); + + using var request = new HttpRequestMessage( + HttpMethod.Post, + "/auth/login") + { + Content = JsonContent.Create(new + { + identifier = "nonexistent-user", + secret = "invalid-password" + }) + }; + + request.Headers.Add( + token.HeaderName, + token.RequestToken); + + using var response = await client.SendAsync(request); + + var body = await response.Content.ReadAsStringAsync(); + + Assert.DoesNotContain( + "invalid_antiforgery_token", + body); + } + + [Fact] + public async Task TokenFromAnotherClient_IsRejected() + { + await using var factory = new AuthServerFactory(); + + using var firstClient = factory.CreateClient( + new WebApplicationFactoryClientOptions + { + HandleCookies = true, + AllowAutoRedirect = false + }); + + using var secondClient = factory.CreateClient( + new WebApplicationFactoryClientOptions + { + HandleCookies = true, + AllowAutoRedirect = false + }); + + var firstToken = + await firstClient.GetFromJsonAsync( + "/auth/antiforgery/token"); + + // Ensure the second client has its own antiforgery cookie. + await secondClient.GetAsync("/auth/antiforgery/token"); + + Assert.NotNull(firstToken); + + using var request = new HttpRequestMessage( + HttpMethod.Post, + "/auth/login") + { + Content = JsonContent.Create(new + { + identifier = "csrf-test-user", + secret = "irrelevant" + }) + }; + + request.Headers.Add( + firstToken.HeaderName, + firstToken.RequestToken); + + using var response = await secondClient.SendAsync(request); + + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + + Assert.Contains( + "invalid_antiforgery_token", + body); + } +} diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/Security/UAuthCsrfEndpointCoverageTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/Security/UAuthCsrfEndpointCoverageTests.cs new file mode 100644 index 00000000..9632cb6b --- /dev/null +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/Security/UAuthCsrfEndpointCoverageTests.cs @@ -0,0 +1,184 @@ +using AwesomeAssertions; +using CodeBeam.UltimateAuth.Server.Security; +using CodeBeam.UltimateAuth.Tests.Integration.Infrastructure; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace CodeBeam.UltimateAuth.Tests.Integration; + +public sealed class CsrfEndpointCoverageTests +{ + [Fact] + public void AllAuthenticationAndMutationEndpoints_MustDeclareCsrfProtection() + { + using var factory = new AuthServerFactory(); + + // Ensure the application and endpoint routing are initialized. + using var client = factory.CreateClient( + new WebApplicationFactoryClientOptions + { + AllowAutoRedirect = false + }); + + var endpoints = factory.Services + .GetRequiredService() + .Endpoints + .OfType() + .Where(e => e.RoutePattern.RawText?.TrimStart('/').StartsWith("auth/", StringComparison.OrdinalIgnoreCase) == true) + .ToArray(); + + endpoints.Should().NotBeEmpty(); + + var expected = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + // Authentication + ["auth/login"] = UAuthCsrfOperation.Login, + ["auth/try-login"] = UAuthCsrfOperation.Login, + ["auth/pkce/complete"] = UAuthCsrfOperation.PkceCompletion, + ["auth/pkce/try-complete"] = UAuthCsrfOperation.PkceCompletion, + + // Session and logout + ["auth/logout"] = UAuthCsrfOperation.AuthenticatedMutation, + ["auth/refresh"] = UAuthCsrfOperation.AuthenticatedMutation, + ["auth/me/logout-device"] = UAuthCsrfOperation.AuthenticatedMutation, + ["auth/me/logout-others"] = UAuthCsrfOperation.AuthenticatedMutation, + ["auth/me/logout-all"] = UAuthCsrfOperation.AuthenticatedMutation, + ["auth/me/sessions/revoke-others"] = UAuthCsrfOperation.AuthenticatedMutation, + ["auth/me/sessions/revoke-all"] = UAuthCsrfOperation.AuthenticatedMutation, + ["auth/me/sessions/chains/{chainId}/revoke"] = UAuthCsrfOperation.AuthenticatedMutation, + + // User lifecycle + ["auth/me/status"] = UAuthCsrfOperation.AuthenticatedMutation, + ["auth/me/delete"] = UAuthCsrfOperation.AuthenticatedMutation, + ["auth/admin/users/create"] = UAuthCsrfOperation.AuthenticatedMutation, + ["auth/admin/users/{userKey}/status"] = UAuthCsrfOperation.AuthenticatedMutation, + ["auth/admin/users/{userKey}/delete"] = UAuthCsrfOperation.AuthenticatedMutation, + + // Profiles + ["auth/me/profile/create"] = UAuthCsrfOperation.AuthenticatedMutation, + ["auth/me/profile/update"] = UAuthCsrfOperation.AuthenticatedMutation, + ["auth/me/profile/delete"] = UAuthCsrfOperation.AuthenticatedMutation, + ["auth/admin/users/{userKey}/profile/create"] = UAuthCsrfOperation.AuthenticatedMutation, + ["auth/admin/users/{userKey}/profile/update"] = UAuthCsrfOperation.AuthenticatedMutation, + ["auth/admin/users/{userKey}/profile/delete"] = UAuthCsrfOperation.AuthenticatedMutation, + + // Self identifiers + ["auth/me/identifiers/add"] = UAuthCsrfOperation.AuthenticatedMutation, + ["auth/me/identifiers/update"] = UAuthCsrfOperation.AuthenticatedMutation, + ["auth/me/identifiers/set-primary"] = UAuthCsrfOperation.AuthenticatedMutation, + ["auth/me/identifiers/unset-primary"] = UAuthCsrfOperation.AuthenticatedMutation, + ["auth/me/identifiers/verify"] = UAuthCsrfOperation.AuthenticatedMutation, + ["auth/me/identifiers/delete"] = UAuthCsrfOperation.AuthenticatedMutation, + + // Admin identifiers + ["auth/admin/users/{userKey}/identifiers/add"] = UAuthCsrfOperation.AuthenticatedMutation, + ["auth/admin/users/{userKey}/identifiers/update"] = UAuthCsrfOperation.AuthenticatedMutation, + ["auth/admin/users/{userKey}/identifiers/set-primary"] = UAuthCsrfOperation.AuthenticatedMutation, + ["auth/admin/users/{userKey}/identifiers/unset-primary"] = UAuthCsrfOperation.AuthenticatedMutation, + ["auth/admin/users/{userKey}/identifiers/verify"] = UAuthCsrfOperation.AuthenticatedMutation, + ["auth/admin/users/{userKey}/identifiers/delete"] = UAuthCsrfOperation.AuthenticatedMutation, + + // Self credentials + ["auth/me/credentials/add"] = UAuthCsrfOperation.AuthenticatedMutation, + ["auth/me/credentials/change"] = UAuthCsrfOperation.AuthenticatedMutation, + ["auth/me/credentials/revoke"] = UAuthCsrfOperation.AuthenticatedMutation, + + // Admin credentials + ["auth/admin/users/{userKey}/credentials/add"] = UAuthCsrfOperation.AuthenticatedMutation, + ["auth/admin/users/{userKey}/credentials/change"] = UAuthCsrfOperation.AuthenticatedMutation, + ["auth/admin/users/{userKey}/credentials/revoke"] = UAuthCsrfOperation.AuthenticatedMutation, + ["auth/admin/users/{userKey}/credentials/reset/begin"] = UAuthCsrfOperation.AuthenticatedMutation, + ["auth/admin/users/{userKey}/credentials/reset/complete"] = UAuthCsrfOperation.AuthenticatedMutation, + ["auth/admin/users/{userKey}/credentials/delete"] = UAuthCsrfOperation.AuthenticatedMutation, + + // Authorization + ["auth/admin/authorization/users/{userKey}/roles/assign"] = UAuthCsrfOperation.AuthenticatedMutation, + ["auth/admin/authorization/users/{userKey}/roles/remove"] = UAuthCsrfOperation.AuthenticatedMutation, + ["auth/admin/authorization/roles/create"] = UAuthCsrfOperation.AuthenticatedMutation, + ["auth/admin/authorization/roles/{roleId}/rename"] = UAuthCsrfOperation.AuthenticatedMutation, + ["auth/admin/authorization/roles/{roleId}/permissions"] = UAuthCsrfOperation.AuthenticatedMutation, + ["auth/admin/authorization/roles/{roleId}/delete"] = UAuthCsrfOperation.AuthenticatedMutation, + + // Admin session operations + ["auth/admin/users/{userKey}/logout-device"] = UAuthCsrfOperation.AuthenticatedMutation, + ["auth/admin/users/{userKey}/logout-others"] = UAuthCsrfOperation.AuthenticatedMutation, + ["auth/admin/users/{userKey}/logout-all"] = UAuthCsrfOperation.AuthenticatedMutation, + ["auth/admin/users/{userKey}/sessions/{sessionId}/revoke"] = UAuthCsrfOperation.AuthenticatedMutation, + ["auth/admin/users/{userKey}/sessions/chains/{chainId}/revoke"] = UAuthCsrfOperation.AuthenticatedMutation, + ["auth/admin/users/{userKey}/sessions/revoke-root"] = UAuthCsrfOperation.AuthenticatedMutation, + ["auth/admin/users/{userKey}/sessions/revoke-all"] = UAuthCsrfOperation.AuthenticatedMutation + }; + + var actual = endpoints + .Where(e => e.Metadata.GetMetadata()?.HttpMethods.Contains("POST") == true) + .ToDictionary( + e => e.RoutePattern.RawText!.TrimStart('/'), + e => e.Metadata.GetMetadata()?.Operation, + StringComparer.OrdinalIgnoreCase); + + // Every expected protected endpoint must exist and carry + // the exact expected CSRF operation. + foreach (var (route, operation) in expected) + { + actual.Should().ContainKey(route, $"protected endpoint {route} must be registered"); + actual[route].Should().Be(operation, $"endpoint {route} must declare {operation}"); + } + + // Explicitly unprotected POST endpoints must be reviewed. + // These are read-only or anonymous flows with separate security semantics. + var explicitlyExcluded = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "auth/validate", + "auth/password-policy", + "auth/pkce/authorize", + "auth/users/create", + "auth/me/credentials/reset/begin", + "auth/me/credentials/reset/complete", + + "auth/me/sessions/chains", + "auth/me/sessions/chains/{chainId}", + "auth/admin/users/{userKey}/sessions/chains", + "auth/admin/users/{userKey}/sessions/chains/{chainId}", + + "auth/admin/users/query", + "auth/admin/users/get-by-keys", + "auth/me/profile/get", + "auth/admin/users/{userKey}/profile/get", + "auth/me/identifiers/get", + "auth/admin/users/{userKey}/identifiers/get", + "auth/me/identifiers/check-availability", + + "auth/me/authorization/check", + "auth/me/authorization/roles/get", + "auth/me/authorization/permissions/get", + "auth/admin/authorization/users/{userKey}/roles/get", + "auth/admin/authorization/roles/query", + + // UAuthHub bootstrap endpoint: + // Creates a short-lived HubFlowArtifact only. + // Does not authenticate the user or issue session credentials. + // PKCE completion performs the actual authentication. + "auth/uauthhub/entry", + }; + + var unexpected = actual.Keys + .Except(expected.Keys, StringComparer.OrdinalIgnoreCase) + .Except(explicitlyExcluded, StringComparer.OrdinalIgnoreCase) + .OrderBy(x => x) + .ToArray(); + + unexpected.Should().BeEmpty( + "new POST endpoints require an explicit CSRF security classification"); + + // Exclusions must not silently acquire a different behavior. + foreach (var route in explicitlyExcluded) + { + if (actual.TryGetValue(route, out var metadata)) + { + metadata.Should().BeNull( + $"excluded endpoint {route} must be reviewed before changing its CSRF classification"); + } + } + } +} diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/SessionAdminTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/SessionAdminTests.cs index 14f710a9..8784d760 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration/SessionAdminTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/SessionAdminTests.cs @@ -415,23 +415,19 @@ await LoginAsync( var sessionId = chain.ActiveSessionId!.Value; - var revoke = await adminClient.PostAsync( + var revoke = await CsrfTestClient.PostAsync(adminClient, $"/auth/admin/users/{target.UserKey.Value}/sessions/{sessionId.Value}/revoke", null); revoke.StatusCode.Should().Be(HttpStatusCode.OK); - // // Revoked session can no longer authenticate. - // var targetVerification = await GetChainsAsync(targetClient); targetVerification.StatusCode.Should() .Be(HttpStatusCode.Unauthorized); - // // Admin's own authority must remain intact. - // var adminVerification = await GetChainsAsync(adminClient); adminVerification.StatusCode.Should() @@ -568,7 +564,7 @@ await LoginAsync( sessionId.Should().NotBeNull(); - var revoke = await actorClient.PostAsync( + var revoke = await CsrfTestClient.PostAsync(actorClient, $"/auth/admin/users/{target.UserKey.Value}/sessions/{sessionId!.Value.Value}/revoke", null); @@ -644,7 +640,7 @@ await LoginAsync( chain1.ActiveSessionId.Should().NotBeNull(); - var revoke = await adminClient.PostAsync( + var revoke = await CsrfTestClient.PostAsync(adminClient, $"/auth/admin/users/{target.UserKey.Value}/sessions/{chain1.ActiveSessionId!.Value.Value}/revoke", null); @@ -722,7 +718,7 @@ await LoginAsync( var targetChain = page!.Items.Single(x => x.IsCurrentDevice); - var revoke = await adminClient.PostAsync( + var revoke = await CsrfTestClient.PostAsync(adminClient, $"/auth/admin/users/{target.UserKey.Value}/sessions/chains/{targetChain.ChainId.Value}/revoke", null); @@ -903,7 +899,7 @@ await LoginAsync( var chain1 = page.Items.Single(x => x.IsCurrentDevice); - var revoke = await adminClient.PostAsync( + var revoke = await CsrfTestClient.PostAsync(adminClient, $"/auth/admin/users/{target.UserKey.Value}/sessions/chains/{chain1.ChainId.Value}/revoke", null); @@ -978,7 +974,7 @@ await LoginAsync( var targetChain = page!.Items.Single(x => x.IsCurrentDevice); - var response = await actorClient.PostAsync( + var response = await CsrfTestClient.PostAsync(actorClient, $"/auth/admin/users/{target.UserKey.Value}/sessions/chains/{targetChain.ChainId.Value}/revoke", null); @@ -1048,7 +1044,7 @@ await LoginAsync( (await GetChainsAsync(targetClient2)) .StatusCode.Should().Be(HttpStatusCode.OK); - var revoke = await adminClient.PostAsync( + var revoke = await CsrfTestClient.PostAsync(adminClient, $"/auth/admin/users/{target.UserKey.Value}/sessions/revoke-all", null); @@ -1114,7 +1110,7 @@ await LoginAsync( targetClient.DefaultRequestHeaders.Add("Cookie", targetCookie); unrelatedClient.DefaultRequestHeaders.Add("Cookie", unrelatedCookie); - var revoke = await adminClient.PostAsync( + var revoke = await CsrfTestClient.PostAsync(adminClient, $"/auth/admin/users/{target.UserKey.Value}/sessions/revoke-all", null); @@ -1178,7 +1174,7 @@ await LoginAsync( targetClient1.DefaultRequestHeaders.Add("Cookie", cookie1); targetClient2.DefaultRequestHeaders.Add("Cookie", cookie2); - var response = await actorClient.PostAsync( + var response = await CsrfTestClient.PostAsync(actorClient, $"/auth/admin/users/{target.UserKey.Value}/sessions/revoke-all", null); @@ -1219,16 +1215,14 @@ await LoginAsync( "Cookie", targetCookie); - var response = await anonymousClient.PostAsync( + var response = await CsrfTestClient.PostAsync(anonymousClient, $"/auth/admin/users/{target.UserKey.Value}/sessions/revoke-all", null); response.StatusCode.Should() .Be(HttpStatusCode.Unauthorized); - // // Anonymous request must produce zero mutation. - // var verification = await GetChainsAsync(targetClient); @@ -1285,24 +1279,20 @@ await LoginAsync( (await GetChainsAsync(targetClient2)) .StatusCode.Should().Be(HttpStatusCode.OK); - var revoke = await adminClient.PostAsync( + var revoke = await CsrfTestClient.PostAsync(adminClient, $"/auth/admin/users/{target.UserKey.Value}/sessions/revoke-root", null); revoke.StatusCode.Should().Be(HttpStatusCode.OK); - // // Root cascade must invalidate every existing target session. - // (await GetChainsAsync(targetClient1)) .StatusCode.Should().Be(HttpStatusCode.Unauthorized); (await GetChainsAsync(targetClient2)) .StatusCode.Should().Be(HttpStatusCode.Unauthorized); - // // Actor remains valid. - // (await GetChainsAsync(adminClient)) .StatusCode.Should().Be(HttpStatusCode.OK); } @@ -1351,7 +1341,7 @@ await LoginAsync( targetClient.DefaultRequestHeaders.Add("Cookie", targetCookie); unrelatedClient.DefaultRequestHeaders.Add("Cookie", unrelatedCookie); - var revoke = await adminClient.PostAsync( + var revoke = await CsrfTestClient.PostAsync(adminClient, $"/auth/admin/users/{target.UserKey.Value}/sessions/revoke-root", null); @@ -1392,7 +1382,7 @@ await LoginAsync( actorClient.DefaultRequestHeaders.Add("Cookie", actorCookie); targetClient.DefaultRequestHeaders.Add("Cookie", targetCookie); - var response = await actorClient.PostAsync($"/auth/admin/users/{target.UserKey.Value}/sessions/revoke-root", null); + var response = await CsrfTestClient.PostAsync(actorClient, $"/auth/admin/users/{target.UserKey.Value}/sessions/revoke-root", null); response.StatusCode.Should().BeOneOf(HttpStatusCode.Forbidden, HttpStatusCode.Unauthorized); @@ -1412,7 +1402,7 @@ public async Task RevokeRootAdmin_ShouldRejectUnauthenticatedRequestWithoutMutat var targetCookie = GetSessionCookie(await LoginAsync(targetClient, target.Identifier, target.Secret)); targetClient.DefaultRequestHeaders.Add("Cookie", targetCookie); - var response = await anonymousClient.PostAsync($"/auth/admin/users/{target.UserKey.Value}/sessions/revoke-root", null); + var response = await CsrfTestClient.PostAsync(anonymousClient, $"/auth/admin/users/{target.UserKey.Value}/sessions/revoke-root", null); response.StatusCode.Should().Be(HttpStatusCode.Unauthorized); (await GetChainsAsync(targetClient)).StatusCode.Should().Be(HttpStatusCode.OK); @@ -1455,7 +1445,7 @@ public async Task RevokeRootAdmin_ShouldAllowSubsequentLoginWithNewAuthenticatio beforeRevoke.StatusCode.Should().Be(HttpStatusCode.OK); - var revoke = await adminClient.PostAsync($"/auth/admin/users/{target.UserKey.Value}/sessions/revoke-root", null); + var revoke = await CsrfTestClient.PostAsync(adminClient, $"/auth/admin/users/{target.UserKey.Value}/sessions/revoke-root", null); revoke.StatusCode.Should().Be(HttpStatusCode.OK); @@ -1502,9 +1492,7 @@ await _factory.GrantPermissionsAsync( using var initialTargetClient = CreateClient($"root-concurrent-initial-{Guid.NewGuid():N}"); - // // Establish initial authentication root. - // var adminLogin = await LoginAsync( adminClient, admin.Identifier, @@ -1529,10 +1517,8 @@ await _factory.GrantPermissionsAsync( "Cookie", initialCookie); - // // Revoke the target's current authentication root. - // - var revoke = await adminClient.PostAsync( + var revoke = await CsrfTestClient.PostAsync(adminClient, $"/auth/admin/users/{target.UserKey.Value}/sessions/revoke-root", null); @@ -1544,10 +1530,7 @@ await _factory.GrantPermissionsAsync( oldSessionVerification.StatusCode .Should().Be(HttpStatusCode.Unauthorized); - // - // Two devices authenticate concurrently after there is - // no active root. - // + // Two devices authenticate concurrently after there is no active root. using var clientA = CreateClient($"root-concurrent-a-{Guid.NewGuid():N}"); @@ -1667,7 +1650,7 @@ private HttpClient CreateClient(string deviceId) private static Task LoginAsync(HttpClient client, string identifier, string secret) { - return client.PostAsJsonAsync( + return CsrfTestClient.PostJsonAsync(client, "/auth/login", new { @@ -1696,7 +1679,7 @@ private static string GetSessionCookie(HttpResponseMessage response) private static Task GetChainsAsync(HttpClient client) { - return client.PostAsJsonAsync( + return CsrfTestClient.PostJsonAsync(client, "/auth/me/sessions/chains", new PageRequest { diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/SessionSlidingExpirationTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/SessionSlidingExpirationTests.cs index fb0fc9dd..3a914d4f 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration/SessionSlidingExpirationTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/SessionSlidingExpirationTests.cs @@ -141,25 +141,25 @@ public async Task IdentifierGet_ExpiredIdleSession_ShouldReturnUnauthorized(stri HandleCookies = false }); - client.DefaultRequestHeaders.Add( - "Origin", "https://localhost:6130"); - client.DefaultRequestHeaders.Add( - "X-UDID", $"idle-regression-{Guid.NewGuid():N}"); - client.DefaultRequestHeaders.Add( - "X-UAuth-ClientProfile", profile); - - using var login = await client.PostAsJsonAsync( + client.DefaultRequestHeaders.Add("Origin", "https://localhost:6130"); + client.DefaultRequestHeaders.Add("X-UDID", $"idle-regression-{Guid.NewGuid():N}"); + client.DefaultRequestHeaders.Add("X-UAuth-ClientProfile", profile); + + using var login = await CsrfTestClient.PostJsonAsync( + client, "/auth/login", - new { identifier = user.Identifier, secret = user.Secret }); + new + { + identifier = user.Identifier, + secret = user.Secret + }); login.StatusCode.Should().Be(HttpStatusCode.Found); login.Headers.TryGetValues("Set-Cookie", out var cookies) .Should().BeTrue(); - client.DefaultRequestHeaders.Add( - "Cookie", - string.Join("; ", cookies!.Select(x => x.Split(';')[0]))); + ApplyCookies(client, login); // Same session must be accepted before IdleTimeout. using var before = await client.PostAsJsonAsync( @@ -388,11 +388,9 @@ public async Task Hybrid_ConcurrentRefresh_ShouldNotLeaveBrokenSessionGraph() using var secondClient = CloneClientWithCookies( factory, client, "BlazorWasm"); - using var first = await firstClient.PostAsync( - "/auth/refresh", null); + using var first = await CsrfTestClient.PostAsync(firstClient, "/auth/refresh", null); - using var second = await secondClient.PostAsync( - "/auth/refresh", null); + using var second = await CsrfTestClient.PostAsync(secondClient, "/auth/refresh", null); // A rotated refresh credential must not be reusable. first.StatusCode.Should().Be(HttpStatusCode.NoContent); @@ -470,17 +468,29 @@ private static HttpClient CreateClient(AuthServerFactory factory, string profile private static async Task Login(AuthServerFactory factory, HttpClient client) { var user = await factory.CreateLoginUserAsync(); - using var response = await client.PostAsJsonAsync("/auth/login", - new { identifier = user.Identifier, secret = user.Secret }); + + using var response = await CsrfTestClient.PostJsonAsync( + client, + "/auth/login", + new + { + identifier = user.Identifier, + secret = user.Secret + }); + response.StatusCode.Should().Be(HttpStatusCode.Found); ApplyCookies(client, response); } private static async Task Refresh(HttpClient client, string profile) { - var response = await client.PostAsync("/auth/refresh", null); + var response = await CsrfTestClient.PostAsync( + client, + "/auth/refresh"); + if (response.IsSuccessStatusCode && profile == "BlazorWasm") ApplyCookies(client, response); + return response; } @@ -508,8 +518,7 @@ private static string Cookie(HttpClient client, string name) => .Split("; ", StringSplitOptions.RemoveEmptyEntries) .Single(x => x.StartsWith(name + "=", StringComparison.Ordinal)); - private static async Task<(UAuthSession Session, UAuthSessionChain Chain)> ReadCurrent( - AuthServerFactory factory, HttpClient client) + private static async Task<(UAuthSession Session, UAuthSessionChain Chain)> ReadCurrent(AuthServerFactory factory, HttpClient client) { using var response = await client.PostAsJsonAsync("/auth/me/sessions/chains", new PageRequest { PageNumber = 1, PageSize = 50 }); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/SessionTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/SessionTests.cs index 65bb06c6..9b5efad1 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration/SessionTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/SessionTests.cs @@ -221,24 +221,20 @@ await LoginAsync( var targetChain = targetChains!.Items.Single(x => x.IsCurrentDevice); - var revoke = await currentClient.PostAsync( + var revoke = await CsrfTestClient.PostAsync(currentClient, $"/auth/me/sessions/chains/{targetChain.ChainId.Value}/revoke", null); revoke.StatusCode.Should().Be(HttpStatusCode.OK); - // // Target session must immediately stop working. - // var targetVerification = await GetChainsAsync(targetClient); targetVerification.StatusCode.Should() .Be(HttpStatusCode.Unauthorized); - // // Actor's own chain must remain usable. - // var actorVerification = await GetChainsAsync(currentClient); @@ -327,7 +323,7 @@ public async Task SessionSelfEndpoints_ShouldRejectUnauthenticatedRequests() using var client = CreateClient( $"session-anonymous-{Guid.NewGuid():N}"); - var list = await client.PostAsJsonAsync( + var list = await CsrfTestClient.PostJsonAsync(client, "/auth/me/sessions/chains", new PageRequest()); @@ -343,21 +339,21 @@ public async Task SessionSelfEndpoints_ShouldRejectUnauthenticatedRequests() detail.StatusCode.Should() .Be(HttpStatusCode.Unauthorized); - var revoke = await client.PostAsync( + var revoke = await CsrfTestClient.PostAsync(client, $"/auth/me/sessions/chains/{randomChain.Value}/revoke", null); revoke.StatusCode.Should() .Be(HttpStatusCode.Unauthorized); - var revokeOthers = await client.PostAsync( + var revokeOthers = await CsrfTestClient.PostAsync(client, "/auth/me/sessions/revoke-others", null); revokeOthers.StatusCode.Should() .Be(HttpStatusCode.Unauthorized); - var revokeAll = await client.PostAsync( + var revokeAll = await CsrfTestClient.PostAsync(client, "/auth/me/sessions/revoke-all", null); @@ -411,7 +407,7 @@ await LoginAsync( "Cookie", otherCookie2); - var revoke = await currentClient.PostAsync( + var revoke = await CsrfTestClient.PostAsync(currentClient, "/auth/me/sessions/revoke-others", null); @@ -489,7 +485,7 @@ await LoginAsync( "Cookie", unrelatedCookie); - var revoke = await currentClient.PostAsync( + var revoke = await CsrfTestClient.PostAsync(currentClient, "/auth/me/sessions/revoke-others", null); @@ -561,10 +557,8 @@ await LoginAsync( client2.DefaultRequestHeaders.Add("Cookie", cookie2); client3.DefaultRequestHeaders.Add("Cookie", cookie3); - // // Client1 revokes every chain, including itself. - // - var revoke = await client1.PostAsync( + var revoke = await CsrfTestClient.PostAsync(client1, "/auth/me/sessions/revoke-all", null); @@ -618,7 +612,7 @@ await LoginAsync( "Cookie", unrelatedCookie); - var revoke = await userClient.PostAsync( + var revoke = await CsrfTestClient.PostAsync(userClient, "/auth/me/sessions/revoke-all", null); @@ -664,12 +658,10 @@ private HttpClient CreateClient(string deviceId) return client; } - private static async Task LoginAsync( - HttpClient client, - string identifier, - string secret) + private static Task LoginAsync(HttpClient client, string identifier, string secret) { - return await client.PostAsJsonAsync( + return CsrfTestClient.PostJsonAsync( + client, "/auth/login", new { diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/UserIdentifierTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/UserIdentifierTests.cs index 82ce3a0f..7bd0af39 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration/UserIdentifierTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/UserIdentifierTests.cs @@ -200,8 +200,7 @@ await response.Content [InlineData("unset-primary", true)] [InlineData("verify", true)] [InlineData("delete", true)] - public async Task IdentifierMutation_WhenIdBelongsToAnotherUser_ShouldReturnNotFoundAndPreserveRecords( - string operation, bool admin) + public async Task IdentifierMutation_WhenIdBelongsToAnotherUser_ShouldReturnNotFoundAndPreserveRecords(string operation, bool admin) { var actor = await _factory.CreateLoginUserAsync(); var victim = await _factory.CreateLoginUserAsync(); @@ -245,12 +244,13 @@ public async Task IdentifierMutation_WhenIdBelongsToAnotherUser_ShouldReturnNotF path = $"/auth/admin/users/{target.UserKey.Value}/identifiers/{operation}"; } - using var response = await client.PostAsJsonAsync(path, new - { - id = identifier.Id, - newValue = $"changed-{Guid.NewGuid():N}@example.com", - mode = 0 // DeleteMode.Soft - }); + using var response = await CsrfTestClient.PostJsonAsync(client, path, + new + { + id = identifier.Id, + newValue = $"changed-{Guid.NewGuid():N}@example.com", + mode = 0 + }); var after = await store.GetByUserAsync(victim.UserKey); using var assertions = new AwesomeAssertions.Execution.AssertionScope(); @@ -302,7 +302,8 @@ public async Task IdentifierMutation_WhenIdBelongsToSelf_ShouldSucceed(string op "X-UDID", $"identifier-owner-{Guid.NewGuid():N}"); await AuthenticateIdentifierClientAsync(client, owner.Identifier, owner.Secret); var newValue = $"updated-{Guid.NewGuid():N}@example.com"; - using var response = await client.PostAsJsonAsync( + using var response = await CsrfTestClient.PostJsonAsync( + client, $"/auth/me/identifiers/{operation}", new { @@ -361,7 +362,7 @@ private HttpClient CreateClient() private static async Task AuthenticateIdentifierClientAsync(HttpClient client, string identifier, string secret) { - using var login = await client.PostAsJsonAsync("/auth/login", new { identifier, secret }); + using var login = await CsrfTestClient.PostJsonAsync(client, "/auth/login", new { identifier, secret }); login.StatusCode.Should().Be(HttpStatusCode.Found); login.Headers.TryGetValues("Set-Cookie", out var cookies).Should().BeTrue(); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/UserLifecycleTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/UserLifecycleTests.cs index 119109a7..2723e5f7 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration/UserLifecycleTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/UserLifecycleTests.cs @@ -57,7 +57,8 @@ public async Task CreateUser_ShouldCreateUsableUser() // // The aggregate must be usable, not merely persisted. // - var login = await client.PostAsJsonAsync( + var login = await CsrfTestClient.PostJsonAsync( + client, "/auth/login", new { @@ -65,6 +66,8 @@ public async Task CreateUser_ShouldCreateUsableUser() secret }); + login.StatusCode.Should().Be(HttpStatusCode.Found); + login.StatusCode.Should() .Be(HttpStatusCode.Found); } @@ -355,7 +358,8 @@ await CreateUserAsync( // lifecycle integration produced a credential that // the authentication pipeline can consume. // - var login = await client.PostAsJsonAsync( + var login = await CsrfTestClient.PostJsonAsync( + client, "/auth/login", new { @@ -363,6 +367,8 @@ await CreateUserAsync( secret }); + login.StatusCode.Should().Be(HttpStatusCode.Found); + login.StatusCode.Should() .Be(HttpStatusCode.Found); @@ -651,7 +657,8 @@ public async Task AdminCreate_WhenAnonymous_ShouldReturnUnauthorized() using var client = CreateClient(); - var response = await client.PostAsJsonAsync( + var response = await CsrfTestClient.PostJsonAsync( + client, "/auth/admin/users/create", new CreateUserRequest { @@ -751,4 +758,4 @@ private static UserKey GetUserKey(UserCreateResult result) return UserKey.FromString(result.UserKey!); } -} \ No newline at end of file +} diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/UserProfileTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/UserProfileTests.cs index 45dd55d9..791c76f3 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration/UserProfileTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/UserProfileTests.cs @@ -20,35 +20,28 @@ public UserProfileTests(AuthServerFactory factory) }); _client.DefaultRequestHeaders.Add("Origin", "https://localhost:6130"); - _client.DefaultRequestHeaders.Add("X-UDID", "test-device-1234567890123456"); + _client.DefaultRequestHeaders.Add("X-UDID", $"profile-test-{Guid.NewGuid():N}"); } [Fact] public async Task Profile_Switch_Should_Return_Correct_Profile_Data() { - var loginResponse = await _client.PostAsJsonAsync("/auth/login", new - { - identifier = "admin", - secret = "admin" - }); - - var cookie = loginResponse.Headers.GetValues("Set-Cookie").First(); - _client.DefaultRequestHeaders.Add("Cookie", cookie); + await Login(); - var defaultResponse = await _client.PostAsJsonAsync("/auth/me/profile/get", new { }); + var defaultResponse = await PostProfileAsync("/auth/me/profile/get", new { }); defaultResponse.StatusCode.Should().Be(HttpStatusCode.OK); var defaultProfile = await defaultResponse.Content.ReadFromJsonAsync(); defaultProfile.Should().NotBeNull(); - var createResponse = await _client.PostAsJsonAsync("/auth/me/profile/create", new CreateProfileRequest + var createResponse = await PostProfileAsync("/auth/me/profile/create", new CreateProfileRequest { ProfileKey = ProfileKey.Parse("business", null) }); createResponse.StatusCode.Should().Be(HttpStatusCode.OK); - var updateResponse = await _client.PostAsJsonAsync("/auth/me/profile/update", new UpdateProfileRequest() + var updateResponse = await PostProfileAsync("/auth/me/profile/update", new UpdateProfileRequest() { ProfileKey = ProfileKey.Parse("business", null), DisplayName = "Updated Business Name" @@ -56,7 +49,7 @@ public async Task Profile_Switch_Should_Return_Correct_Profile_Data() updateResponse.StatusCode.Should().Be(HttpStatusCode.OK); - var businessResponse = await _client.PostAsJsonAsync("/auth/me/profile/get", new GetProfileRequest() + var businessResponse = await PostProfileAsync("/auth/me/profile/get", new GetProfileRequest() { ProfileKey = ProfileKey.Parse("business", null) }); @@ -68,7 +61,7 @@ public async Task Profile_Switch_Should_Return_Correct_Profile_Data() businessProfile.Should().NotBeNull(); businessProfile!.DisplayName.Should().Be("Updated Business Name"); - var defaultAgainResponse = await _client.PostAsJsonAsync("/auth/me/profile/get", new { }); + var defaultAgainResponse = await PostProfileAsync("/auth/me/profile/get", new { }); var defaultAgain = await defaultAgainResponse.Content.ReadFromJsonAsync(); @@ -78,14 +71,7 @@ public async Task Profile_Switch_Should_Return_Correct_Profile_Data() [Fact] public async Task GetMe_Without_ProfileKey_Should_Return_Default_Profile() { - var login = await _client.PostAsJsonAsync("/auth/login", new - { - identifier = "admin", - secret = "admin" - }); - - var cookie = login.Headers.GetValues("Set-Cookie").First(); - _client.DefaultRequestHeaders.Add("Cookie", cookie); + await Login(); var response = await _client.PostAsJsonAsync("/auth/me/profile/get", new { }); @@ -98,16 +84,9 @@ public async Task GetMe_Without_ProfileKey_Should_Return_Default_Profile() [Fact] public async Task Should_Not_Found_NonDefault_Profile_When_Not_Created() { - var login = await _client.PostAsJsonAsync("/auth/login", new - { - identifier = "admin", - secret = "admin" - }); + await Login(); - var cookie = login.Headers.GetValues("Set-Cookie").First(); - _client.DefaultRequestHeaders.Add("Cookie", cookie); - - var response = await _client.PostAsJsonAsync("/auth/me/profile/get", new + var response = await PostProfileAsync("/auth/me/profile/get", new { profileKey = "business" }); @@ -130,21 +109,19 @@ public async Task Should_Not_Create_Duplicate_Profile() ProfileKey = key }; - var first = await _client.PostAsJsonAsync("/auth/me/profile/create", request); + var first = await PostProfileAsync("/auth/me/profile/create", request); first.StatusCode.Should().Be(HttpStatusCode.OK); - var second = await _client.PostAsJsonAsync("/auth/me/profile/create", request); + var second = await PostProfileAsync("/auth/me/profile/create", request); second.StatusCode.Should().Be(HttpStatusCode.Conflict); } [Fact] public async Task Should_Not_Delete_Default_Profile() { - var login = await Login(); - var cookie = login.Headers.GetValues("Set-Cookie").First(); - _client.DefaultRequestHeaders.Add("Cookie", cookie); + await Login(); - var response = await _client.PostAsJsonAsync("/auth/me/profile/delete", new + var response = await PostProfileAsync("/auth/me/profile/delete", new { profileKey = "default" }); @@ -155,11 +132,9 @@ public async Task Should_Not_Delete_Default_Profile() [Fact] public async Task Should_Not_Update_NonExisting_Profile() { - var login = await Login(); - var cookie = login.Headers.GetValues("Set-Cookie").First(); - _client.DefaultRequestHeaders.Add("Cookie", cookie); + await Login(); - var response = await _client.PostAsJsonAsync("/auth/me/profile/update", new UpdateProfileRequest + var response = await PostProfileAsync("/auth/me/profile/update", new UpdateProfileRequest { ProfileKey = ProfileKey.Parse("ghost", null), DisplayName = "Should Fail" @@ -171,23 +146,21 @@ public async Task Should_Not_Update_NonExisting_Profile() [Fact] public async Task Deleted_Profile_Should_Not_Be_Returned() { - var login = await Login(); - var cookie = login.Headers.GetValues("Set-Cookie").First(); - _client.DefaultRequestHeaders.Add("Cookie", cookie); + await Login(); var key = ProfileKey.Parse("business", null); - await _client.PostAsJsonAsync("/auth/me/profile/create", new CreateProfileRequest + await PostProfileAsync("/auth/me/profile/create", new CreateProfileRequest { ProfileKey = key }); - await _client.PostAsJsonAsync("/auth/me/profile/delete", new + await PostProfileAsync("/auth/me/profile/delete", new { profileKey = key.Value }); - var response = await _client.PostAsJsonAsync("/auth/me/profile/get", new GetProfileRequest + var response = await PostProfileAsync("/auth/me/profile/get", new GetProfileRequest { ProfileKey = key }); @@ -198,19 +171,40 @@ public async Task Deleted_Profile_Should_Not_Be_Returned() private async Task Login() { - var response = await _client.PostAsJsonAsync("/auth/login", new - { - identifier = "admin", - secret = "admin" - }); + var response = await CsrfTestClient.PostJsonAsync( + _client, + "/auth/login", + new + { + identifier = "admin", + secret = "admin" + }); response.StatusCode.Should().Be(HttpStatusCode.Found); - var cookie = response.Headers.GetValues("Set-Cookie").First(); + response.Headers.TryGetValues( + "Set-Cookie", + out var cookies).Should().BeTrue(); + + var sessionCookie = cookies! + .Single(x => x.StartsWith( + "uas=", + StringComparison.OrdinalIgnoreCase)) + .Split(';', 2)[0]; _client.DefaultRequestHeaders.Remove("Cookie"); - _client.DefaultRequestHeaders.Add("Cookie", cookie); + _client.DefaultRequestHeaders.Add("Cookie", sessionCookie); return response; } + + private Task PostProfileAsync( + string url, + T request) + { + return CsrfTestClient.PostJsonAsync( + _client, + url, + request); + } } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthClientBootstrapperTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthClientBootstrapperTests.cs index c16255aa..ce53fef8 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthClientBootstrapperTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthClientBootstrapperTests.cs @@ -1,7 +1,9 @@ -using CodeBeam.UltimateAuth.Client; +using AwesomeAssertions; +using CodeBeam.UltimateAuth.Client; using CodeBeam.UltimateAuth.Client.Infrastructure; -using CodeBeam.UltimateAuth.Client.Runtime; +using CodeBeam.UltimateAuth.Client.Options; using CodeBeam.UltimateAuth.Tests.Unit.Helpers; +using Microsoft.Extensions.Options; using Moq; namespace CodeBeam.UltimateAuth.Tests.Unit; @@ -11,38 +13,204 @@ public class UAuthClientBootstrapperTests [Fact] public async Task EnsureStartedAsync_should_initialize_only_once() { - var deviceId = TestDevice.Default().DeviceId!.Value; - - var provider = new Mock(); - provider.Setup(x => x.GetOrCreateAsync(default)).ReturnsAsync(deviceId); + var deviceId = TestDevice.Default().DeviceId!.Value.Value; + var provider = CreateDeviceProvider(deviceId); var browser = new Mock(); - var bootstrapper = new UAuthClientBootstrapper(provider.Object, browser.Object); + var bootstrapper = CreateBootstrapper( + provider, + browser, + new UAuthClientOptions()); await bootstrapper.EnsureStartedAsync(); await bootstrapper.EnsureStartedAsync(); - provider.Verify(x => x.GetOrCreateAsync(default), Times.Once); - browser.Verify(x => x.SetDeviceIdAsync(deviceId.Value), Times.Once); + bootstrapper.IsStarted.Should().BeTrue(); + + provider.Verify( + x => x.GetOrCreateAsync(default), + Times.Once); + + browser.Verify( + x => x.SetDeviceIdAsync(deviceId), + Times.Once); + + browser.Verify( + x => x.ConfigureAntiforgeryAsync( + "/auth/antiforgery/token", + null), + Times.Once); } [Fact] public async Task EnsureStartedAsync_should_be_thread_safe() { - var deviceId = TestDevice.Default().DeviceId!.Value; - - var provider = new Mock(); - provider.Setup(x => x.GetOrCreateAsync(default)).ReturnsAsync(deviceId); + var deviceId = TestDevice.Default().DeviceId!.Value.Value; + var provider = CreateDeviceProvider(deviceId); var browser = new Mock(); - var bootstrapper = new UAuthClientBootstrapper(provider.Object, browser.Object); - var tasks = Enumerable.Range(0, 10).Select(_ => bootstrapper.EnsureStartedAsync()); + var bootstrapper = CreateBootstrapper( + provider, + browser, + new UAuthClientOptions()); + + var tasks = Enumerable.Range(0, 10) + .Select(_ => bootstrapper.EnsureStartedAsync()); await Task.WhenAll(tasks); - provider.Verify(x => x.GetOrCreateAsync(default), Times.Once); - browser.Verify(x => x.SetDeviceIdAsync(deviceId.Value), Times.Once); + bootstrapper.IsStarted.Should().BeTrue(); + + provider.Verify( + x => x.GetOrCreateAsync(default), + Times.Once); + + browser.Verify( + x => x.SetDeviceIdAsync(deviceId), + Times.Once); + + browser.Verify( + x => x.ConfigureAntiforgeryAsync( + "/auth/antiforgery/token", + null), + Times.Once); + } + + [Fact] + public async Task EnsureStartedAsync_should_configure_same_origin_antiforgery() + { + var deviceId = TestDevice.Default().DeviceId!.Value.Value; + + var provider = CreateDeviceProvider(deviceId); + var browser = new Mock(); + + var options = new UAuthClientOptions + { + Endpoints = new UAuthClientEndpointOptions + { + BasePath = "/identity", + AntiforgeryToken = "/antiforgery/token" + } + }; + + var bootstrapper = CreateBootstrapper( + provider, + browser, + options); + + await bootstrapper.EnsureStartedAsync(); + + browser.Verify( + x => x.ConfigureAntiforgeryAsync( + "/identity/antiforgery/token", + null), + Times.Once); + } + + [Fact] + public async Task EnsureStartedAsync_should_configure_cross_origin_antiforgery() + { + var deviceId = TestDevice.Default().DeviceId!.Value.Value; + + var provider = CreateDeviceProvider(deviceId); + var browser = new Mock(); + + var options = new UAuthClientOptions + { + Endpoints = new UAuthClientEndpointOptions + { + BasePath = "https://localhost:6110/auth", + AntiforgeryToken = "/antiforgery/token" + } + }; + + var bootstrapper = CreateBootstrapper( + provider, + browser, + options); + + await bootstrapper.EnsureStartedAsync(); + + browser.Verify( + x => x.ConfigureAntiforgeryAsync( + "https://localhost:6110/auth/antiforgery/token", + "https://localhost:6110"), + Times.Once); + } + + [Fact] + public async Task EnsureStartedAsync_should_retry_when_antiforgery_configuration_fails() + { + var deviceId = TestDevice.Default().DeviceId!.Value.Value; + + var provider = CreateDeviceProvider(deviceId); + var browser = new Mock(); + + var attempts = 0; + + browser + .Setup(x => x.ConfigureAntiforgeryAsync( + It.IsAny(), + It.IsAny())) + .Returns(() => + { + attempts++; + + if (attempts == 1) + { + return new ValueTask( + Task.FromException( + new InvalidOperationException( + "JS initialization failed."))); + } + + return ValueTask.CompletedTask; + }); + + var bootstrapper = CreateBootstrapper( + provider, + browser, + new UAuthClientOptions()); + + await Assert.ThrowsAsync( + () => bootstrapper.EnsureStartedAsync()); + + bootstrapper.IsStarted.Should().BeFalse(); + + await bootstrapper.EnsureStartedAsync(); + + bootstrapper.IsStarted.Should().BeTrue(); + + browser.Verify( + x => x.ConfigureAntiforgeryAsync( + "/auth/antiforgery/token", + null), + Times.Exactly(2)); + } + + private static Mock CreateDeviceProvider( + string deviceId) + { + var provider = new Mock(); + + provider + .Setup(x => x.GetOrCreateAsync(default)) + .ReturnsAsync( + TestDevice.Default().DeviceId!.Value); + + return provider; + } + + private static UAuthClientBootstrapper CreateBootstrapper( + Mock provider, + Mock browser, + UAuthClientOptions options) + { + return new UAuthClientBootstrapper( + provider.Object, + browser.Object, + Options.Create(options)); } } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Security/UAuthCsrfPolicyEvaluatorTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Security/UAuthCsrfPolicyEvaluatorTests.cs new file mode 100644 index 00000000..c78b8d27 --- /dev/null +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Security/UAuthCsrfPolicyEvaluatorTests.cs @@ -0,0 +1,73 @@ +using CodeBeam.UltimateAuth.Server.Security; + +namespace CodeBeam.UltimateAuth.Tests.Unit.Security; + +public sealed class UAuthCsrfPolicyEvaluatorTests +{ + [Theory] + [InlineData(UAuthCsrfOperation.Login)] + [InlineData(UAuthCsrfOperation.PkceCompletion)] + [InlineData(UAuthCsrfOperation.AuthenticatedMutation)] + public void CookieIssuance_RequiresProtection(UAuthCsrfOperation operation) + { + var result = UAuthCsrfPolicyEvaluator.RequiresProtection( + operation, + canIssueCookie: true, + canAcceptCookie: false); + + Assert.True(result); + } + + [Theory] + [InlineData(UAuthCsrfOperation.Login)] + [InlineData(UAuthCsrfOperation.PkceCompletion)] + [InlineData(UAuthCsrfOperation.AuthenticatedMutation)] + public void CookieAcceptance_RequiresProtection(UAuthCsrfOperation operation) + { + var result = UAuthCsrfPolicyEvaluator.RequiresProtection( + operation, + canIssueCookie: false, + canAcceptCookie: true); + + Assert.True(result); + } + + [Theory] + [InlineData(UAuthCsrfOperation.Login)] + [InlineData(UAuthCsrfOperation.PkceCompletion)] + [InlineData(UAuthCsrfOperation.AuthenticatedMutation)] + public void HeaderOnlyTransport_DoesNotRequireProtection(UAuthCsrfOperation operation) + { + var result = UAuthCsrfPolicyEvaluator.RequiresProtection( + operation, + canIssueCookie: false, + canAcceptCookie: false); + + Assert.False(result); + } + + [Theory] + [InlineData(UAuthCsrfOperation.Login)] + [InlineData(UAuthCsrfOperation.PkceCompletion)] + [InlineData(UAuthCsrfOperation.AuthenticatedMutation)] + public void BothCookieCapabilities_RequireProtection(UAuthCsrfOperation operation) + { + var result = UAuthCsrfPolicyEvaluator.RequiresProtection( + operation, + canIssueCookie: true, + canAcceptCookie: true); + + Assert.True(result); + } + + [Fact] + public void UnknownOperation_FailsClosed() + { + var result = UAuthCsrfPolicyEvaluator.RequiresProtection( + (UAuthCsrfOperation)999, + canIssueCookie: false, + canAcceptCookie: false); + + Assert.True(result); + } +} diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Sessions/CompositeSessionIdResolverTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Sessions/CompositeSessionIdResolverTests.cs index a6b16ad8..d466699e 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Sessions/CompositeSessionIdResolverTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Sessions/CompositeSessionIdResolverTests.cs @@ -1,350 +1,297 @@ -using CodeBeam.UltimateAuth.Core.Domain; +using AwesomeAssertions; +using CodeBeam.UltimateAuth.Core; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.Options; +using CodeBeam.UltimateAuth.Server.Auth; using CodeBeam.UltimateAuth.Server.Infrastructure; +using CodeBeam.UltimateAuth.Server.Options; using CodeBeam.UltimateAuth.Tests.Unit.Helpers; -using AwesomeAssertions; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Options; using Moq; namespace CodeBeam.UltimateAuth.Tests.Unit; public sealed class CompositeSessionIdResolverTests { + private const UAuthClientProfile Profile = UAuthClientProfile.BlazorServer; + [Fact] - public void Resolve_ShouldUseFirstSuccessfulResolverAccordingToConfiguredOrder() + public async Task ResolveAsync_ShouldUseFirstSuccessfulResolverAccordingToConfiguredOrder() { - var firstId = - CreateSessionId( - "first-session-00000000000000000000000001"); - - var secondId = - CreateSessionId( - "second-session-0000000000000000000000001"); + var firstId = CreateSessionId("first-session-00000000000000000000000001"); + var secondId = CreateSessionId("second-session-0000000000000000000000001"); - var bearer = - CreateResolver( - "bearer", - firstId); + var bearer = CreateResolver("Bearer", firstId); + var cookie = CreateResolver("Cookie", secondId); - var cookie = - CreateResolver( - "cookie", - secondId); + var options = CreateOptions("Bearer", "Cookie"); - var options = TestServerOptions.Default(); + var (sut, context) = CreateSut( + options, + bearer.Object, + cookie.Object); - options.SessionResolution.Order = - new List { "Bearer", "Cookie" }; + var result = await sut.ResolveAsync(context); - options.SessionResolution.EnableBearer = true; - options.SessionResolution.EnableCookie = true; + result.Should().Be(firstId); + bearer.Verify(x => x.Resolve(context), Times.Once); + cookie.Verify(x => x.Resolve(context), Times.Never); + } - var sut = - new CompositeSessionIdResolver( - new[] - { - bearer.Object, - cookie.Object - }, - Options.Create(options)); + [Fact] + public async Task ResolveAsync_WhenFirstResolverReturnsNull_ShouldContinueToNextResolver() + { + var expected = CreateSessionId("cookie-session-0000000000000000000000002"); - var context = new DefaultHttpContext(); + var bearer = CreateResolver("Bearer", null); + var cookie = CreateResolver("Cookie", expected); - var result = sut.Resolve(context); + var options = CreateOptions("Bearer", "Cookie"); - result.Should().Be(firstId); + var (sut, context) = CreateSut( + options, + bearer.Object, + cookie.Object); - bearer.Verify( - x => x.Resolve(context), - Times.Once); + var result = await sut.ResolveAsync(context); - cookie.Verify( - x => x.Resolve(context), - Times.Never); + result.Should().Be(expected); + bearer.Verify(x => x.Resolve(context), Times.Once); + cookie.Verify(x => x.Resolve(context), Times.Once); } [Fact] - public void Resolve_WhenFirstResolverReturnsNull_ShouldContinueToNextResolver() + public async Task ResolveAsync_WhenResolverIsDisabled_ShouldSkipIt() { - var expected = - CreateSessionId( - "cookie-session-0000000000000000000000002"); - - var bearer = - CreateResolver( - "bearer", - null); + var bearerId = CreateSessionId("bearer-session-0000000000000000000000002"); + var cookieId = CreateSessionId("cookie-session-0000000000000000000000003"); - var cookie = - CreateResolver( - "cookie", - expected); + var bearer = CreateResolver("Bearer", bearerId); + var cookie = CreateResolver("Cookie", cookieId); - var options = TestServerOptions.Default(); + var options = CreateOptions("Bearer", "Cookie"); + options.SessionResolution.EnableBearer = false; - options.SessionResolution.Order = - new List { "Bearer", "Cookie" }; + var (sut, context) = CreateSut( + options, + bearer.Object, + cookie.Object); - options.SessionResolution.EnableBearer = true; - options.SessionResolution.EnableCookie = true; + var result = await sut.ResolveAsync(context); - var sut = - new CompositeSessionIdResolver( - new[] - { - bearer.Object, - cookie.Object - }, - Options.Create(options)); + result.Should().Be(cookieId); + bearer.Verify(x => x.Resolve(It.IsAny()), Times.Never); + cookie.Verify(x => x.Resolve(context), Times.Once); + } - var context = new DefaultHttpContext(); + [Fact] + public async Task ResolveAsync_WhenConfiguredResolverDoesNotExist_ShouldContinue() + { + var expected = CreateSessionId("cookie-session-0000000000000000000000004"); - var result = sut.Resolve(context); + var cookie = CreateResolver("Cookie", expected); + var options = CreateOptions("Bearer", "Cookie"); - result.Should().Be(expected); + var (sut, context) = CreateSut(options, cookie.Object); - bearer.Verify( - x => x.Resolve(context), - Times.Once); + var result = await sut.ResolveAsync(context); - cookie.Verify( - x => x.Resolve(context), - Times.Once); + result.Should().Be(expected); + cookie.Verify(x => x.Resolve(context), Times.Once); } [Fact] - public void Resolve_WhenResolverIsDisabled_ShouldSkipIt() + public async Task ResolveAsync_WhenOrderUsesDifferentCasing_ShouldResolveCaseInsensitively() { - var bearerId = - CreateSessionId( - "bearer-session-0000000000000000000000002"); + var expected = CreateSessionId("bearer-session-0000000000000000000000005"); - var cookieId = - CreateSessionId( - "cookie-session-0000000000000000000000003"); + var bearer = CreateResolver("bearer", expected); + var options = CreateOptions("bEaReR"); - var bearer = - CreateResolver( - "bearer", - bearerId); + var (sut, context) = CreateSut(options, bearer.Object); - var cookie = - CreateResolver( - "cookie", - cookieId); + var result = await sut.ResolveAsync(context); - var options = TestServerOptions.Default(); - - options.SessionResolution.Order = - new List { "Bearer", "Cookie" }; - - options.SessionResolution.EnableBearer = false; - options.SessionResolution.EnableCookie = true; - - var sut = - new CompositeSessionIdResolver( - new[] - { - bearer.Object, - cookie.Object - }, - Options.Create(options)); + result.Should().Be(expected); + bearer.Verify(x => x.Resolve(context), Times.Once); + } - var context = new DefaultHttpContext(); + [Fact] + public async Task ResolveAsync_WhenNoResolverProducesSessionId_ShouldReturnNull() + { + var bearer = CreateResolver("Bearer", null); + var cookie = CreateResolver("Cookie", null); - var result = sut.Resolve(context); + var options = CreateOptions("Bearer", "Cookie"); - result.Should().Be(cookieId); + var (sut, context) = CreateSut( + options, + bearer.Object, + cookie.Object); - bearer.Verify( - x => x.Resolve( - It.IsAny()), - Times.Never); + var result = await sut.ResolveAsync(context); - cookie.Verify( - x => x.Resolve(context), - Times.Once); + result.Should().BeNull(); + bearer.Verify(x => x.Resolve(context), Times.Once); + cookie.Verify(x => x.Resolve(context), Times.Once); } [Fact] - public void Resolve_WhenConfiguredResolverDoesNotExist_ShouldContinue() + public async Task ResolveAsync_WhenOrderChanges_ShouldRespectNewPrecedence() { - var expected = - CreateSessionId( - "cookie-session-0000000000000000000000004"); + var bearerId = CreateSessionId("bearer-session-0000000000000000000000006"); + var cookieId = CreateSessionId("cookie-session-0000000000000000000000006"); - var cookie = - CreateResolver( - "cookie", - expected); + var bearer = CreateResolver("Bearer", bearerId); + var cookie = CreateResolver("Cookie", cookieId); - var options = TestServerOptions.Default(); - - options.SessionResolution.Order = - new List { "Bearer", "Cookie" }; - - options.SessionResolution.EnableBearer = true; - options.SessionResolution.EnableCookie = true; + var options = CreateOptions("Cookie", "Bearer"); - var sut = - new CompositeSessionIdResolver( - new[] - { - cookie.Object - }, - Options.Create(options)); + var (sut, context) = CreateSut( + options, + bearer.Object, + cookie.Object); - var result = - sut.Resolve( - new DefaultHttpContext()); + var result = await sut.ResolveAsync(context); - result.Should().Be(expected); + result.Should().Be(cookieId); + cookie.Verify(x => x.Resolve(context), Times.Once); + bearer.Verify(x => x.Resolve(context), Times.Never); } - [Fact] - public void Resolve_WhenOrderUsesDifferentCasing_ShouldResolveCaseInsensitively() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ResolveAsync_ShouldRespectEffectiveCookieSetting( + bool effectiveCookieEnabled) { - var expected = - CreateSessionId( - "bearer-session-0000000000000000000000005"); - - var bearer = - CreateResolver( - "bearer", - expected); - - var options = TestServerOptions.Default(); + var expected = CreateSessionId("cookie-session-000000000000000000000001"); - options.SessionResolution.Order = - new List { "bEaReR" }; + var cookie = CreateResolver("Cookie", expected); - options.SessionResolution.EnableBearer = true; + var options = CreateOptions("Cookie"); + options.SessionResolution.EnableCookie = effectiveCookieEnabled; - var sut = - new CompositeSessionIdResolver( - new[] - { - bearer.Object - }, - Options.Create(options)); + var (sut, context) = CreateSut(options, cookie.Object); - var result = - sut.Resolve( - new DefaultHttpContext()); + var result = await sut.ResolveAsync(context); - result.Should().Be(expected); + if (effectiveCookieEnabled) + { + result.Should().Be(expected); + cookie.Verify(x => x.Resolve(context), Times.Once); + } + else + { + result.Should().BeNull(); + cookie.Verify(x => x.Resolve(context), Times.Never); + } } [Fact] - public void Resolve_WhenNoResolverProducesSessionId_ShouldReturnNull() + public async Task ResolveAsync_ShouldReadClientProfileAndUseItsEffectiveOptions() { - var bearer = - CreateResolver( - "bearer", - null); + var expected = CreateSessionId("cookie-session-000000000000000000000002"); - var cookie = - CreateResolver( - "cookie", - null); + var cookie = CreateResolver("Cookie", expected); - var options = TestServerOptions.Default(); + var options = CreateOptions("Cookie"); - options.SessionResolution.Order = - new List { "Bearer", "Cookie" }; + var profileReader = new Mock(); + var effectiveProvider = new Mock(); - options.SessionResolution.EnableBearer = true; - options.SessionResolution.EnableCookie = true; + var context = new DefaultHttpContext(); - var sut = - new CompositeSessionIdResolver( - new[] - { - bearer.Object, - cookie.Object - }, - Options.Create(options)); + profileReader + .Setup(x => x.ReadAsync(context)) + .ReturnsAsync(Profile); - var result = - sut.Resolve( - new DefaultHttpContext()); + effectiveProvider + .Setup(x => x.GetEffective(context, Profile)) + .Returns(new EffectiveUAuthServerOptions + { + Mode = UAuthMode.PureOpaque, + Options = options + }); - result.Should().BeNull(); - } + var sut = new CompositeSessionIdResolver( + new[] { cookie.Object }, + profileReader.Object, + effectiveProvider.Object); - [Fact] - public void Resolve_WhenOrderChanges_ShouldRespectNewPrecedence() - { - var bearerId = - CreateSessionId( - "bearer-session-0000000000000000000000006"); + var result = await sut.ResolveAsync(context); - var cookieId = - CreateSessionId( - "cookie-session-0000000000000000000000006"); + result.Should().Be(expected); - var bearer = - CreateResolver( - "bearer", - bearerId); + profileReader.Verify( + x => x.ReadAsync(context), + Times.Once); - var cookie = - CreateResolver( - "cookie", - cookieId); + effectiveProvider.Verify( + x => x.GetEffective(context, Profile), + Times.Once); - var options = TestServerOptions.Default(); + cookie.Verify( + x => x.Resolve(context), + Times.Once); + } - options.SessionResolution.Order = - new List { "Cookie", "Bearer" }; + private static UAuthServerOptions CreateOptions(params string[] order) + { + var options = TestServerOptions.Default(); + options.SessionResolution.Order = order.ToList(); options.SessionResolution.EnableBearer = true; + options.SessionResolution.EnableHeader = true; options.SessionResolution.EnableCookie = true; + options.SessionResolution.EnableQuery = true; - var sut = - new CompositeSessionIdResolver( - new[] - { - bearer.Object, - cookie.Object - }, - Options.Create(options)); + return options; + } - var result = - sut.Resolve( - new DefaultHttpContext()); + private static (CompositeSessionIdResolver Sut, DefaultHttpContext Context) + CreateSut( + UAuthServerOptions effectiveOptions, + params IInnerSessionIdResolver[] resolvers) + { + var context = new DefaultHttpContext(); - result.Should().Be(cookieId); + var profileReader = new Mock(); + profileReader + .Setup(x => x.ReadAsync(context)) + .ReturnsAsync(Profile); + + var effectiveProvider = new Mock(); + effectiveProvider + .Setup(x => x.GetEffective(context, Profile)) + .Returns(new EffectiveUAuthServerOptions + { + Mode = UAuthMode.PureOpaque, + Options = effectiveOptions + }); + + var sut = new CompositeSessionIdResolver( + resolvers, + profileReader.Object, + effectiveProvider.Object); + + return (sut, context); } - private static Mock CreateResolver( - string name, - AuthSessionId? result) + private static Mock CreateResolver(string name, AuthSessionId? result) { - var resolver = - new Mock(); + var resolver = new Mock(); - resolver - .SetupGet(x => x.Name) - .Returns(name); + resolver.SetupGet(x => x.Name).Returns(name); - resolver - .Setup(x => x.Resolve( - It.IsAny())) - .Returns(result); + resolver.Setup(x => x.Resolve(It.IsAny())).Returns(result); return resolver; } - private static AuthSessionId CreateSessionId( - string value) + private static AuthSessionId CreateSessionId(string value) { - AuthSessionId.TryCreate( - value, - out var id) - .Should() - .BeTrue(); - + AuthSessionId.TryCreate(value, out var id).Should().BeTrue(); return id; } } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/javascript/uauth.test.cjs b/tests/CodeBeam.UltimateAuth.Tests.Unit/javascript/uauth.test.cjs index e3b56edb..bb289e9d 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/javascript/uauth.test.cjs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/javascript/uauth.test.cjs @@ -18,7 +18,7 @@ function createEnvironment(fetchImpl = async () => ({ get: () => "application/json" }, text: async () => '{"id":1}' -}), onNavigate = () => {}) { +}), onNavigate = () => { }) { const memoryStorage = () => { const values = new Map(); return { setItem: (key, value) => values.set(key, value), getItem: key => values.get(key) ?? null, removeItem: key => values.delete(key) }; @@ -55,6 +55,47 @@ function createEnvironment(fetchImpl = async () => ({ return window.uauth; } +const HUB_ORIGIN = "https://localhost:6110"; + +function configureAntiforgery(uauth) { + uauth.antiforgery.configure({ + tokenUrl: `${HUB_ORIGIN}/auth/antiforgery/token`, + trustedOrigin: HUB_ORIGIN + }); +} + +function authUrl(path) { + return `${HUB_ORIGIN}${path}`; +} + + +function csrfAwareFetch(postHandler) { + return async (url, options = {}) => { + if (url === authUrl("/auth/antiforgery/token")) { + assert.equal(options.method, "GET"); + assert.equal(options.credentials, "include"); + + return { + ok: true, + status: 200, + json: async () => ({ + requestToken: "csrf-test-token", + headerName: "X-CSRF-TOKEN", + formFieldName: "__RequestVerificationToken" + }) + }; + } + + assert.equal(options.method, "POST"); + assert.equal( + options.headers["X-CSRF-TOKEN"], + "csrf-test-token" + ); + + return postHandler(url, options); + }; +} + test("validateAuthUrl accepts trusted origin", () => { const uauth = createEnvironment(); @@ -328,13 +369,105 @@ test("cancelResourceRequest aborts active request", async () => { ); }); + +test("postJson sends antiforgery token to trusted cross-origin UAuthHub", async () => { + const calls = []; + + const uauth = createEnvironment(async (url, options) => { + calls.push({ url, options }); + + if (url === "https://localhost:6110/auth/antiforgery/token") { + return { + ok: true, + status: 200, + json: async () => ({ + requestToken: "csrf-test-token", + headerName: "X-CSRF-TOKEN", + formFieldName: "__RequestVerificationToken" + }) + }; + } + + return { + ok: true, + status: 200, + headers: { get: () => null }, + text: async () => JSON.stringify({ isSuccess: true }) + }; + }); + + uauth.antiforgery.configure({ + tokenUrl: "https://localhost:6110/auth/antiforgery/token", + trustedOrigin: "https://localhost:6110" + }); + + const result = await uauth.postJson({ + url: "https://localhost:6110/auth/try-login", + payload: { username: "test" }, + clientProfile: "BlazorWasm", + trustedOrigin: "https://localhost:6110" + }); + + assert.equal(result.status, 200); + assert.equal(calls.length, 2); + + assert.equal( + calls[0].url, + "https://localhost:6110/auth/antiforgery/token" + ); + assert.equal(calls[0].options.method, "GET"); + assert.equal(calls[0].options.credentials, "include"); + + assert.equal( + calls[1].url, + "https://localhost:6110/auth/try-login" + ); + assert.equal(calls[1].options.method, "POST"); + assert.equal(calls[1].options.credentials, "include"); + assert.equal( + calls[1].options.headers["X-CSRF-TOKEN"], + "csrf-test-token" + ); +}); + + +test("postJson fails closed when antiforgery is not configured", async () => { + let fetchCalls = 0; + + const uauth = createEnvironment(async () => { + fetchCalls++; + throw new Error("Fetch must not be called"); + }); + + await assert.rejects( + uauth.postJson({ + url: authUrl("/auth/try-login"), + payload: { username: "test" }, + clientProfile: "BlazorServer", + trustedOrigin: HUB_ORIGIN + }), + /Authentication URL is required|antiforgery is not configured/i + ); + + assert.equal(fetchCalls, 0); +}); + test("429 try-and-commit returns throttle metadata without committing or reading an auth-result body", async () => { let calls = 0; - const uauth = createEnvironment(async () => { + const uauth = createEnvironment(csrfAwareFetch(async () => { calls++; return { ok: false, status: 429, headers: { get: name => name === "Retry-After" ? "90" : null }, text: async () => "not-json" }; + })); + + configureAntiforgery(uauth); + + const result = await uauth.tryAndCommit({ + tryUrl: authUrl("/auth/try-login"), + commitUrl: authUrl("/auth/login"), + data: {}, + clientProfile: "BlazorServer", + trustedOrigin: HUB_ORIGIN }); - const result = await uauth.tryAndCommit({ tryUrl: "/auth/try-login", commitUrl: "/auth/login", data: {}, clientProfile: "BlazorServer" }); assert.equal(result.status, 429); assert.equal(result.errorCode, "rate_limited"); assert.equal(result.retryAfter, "90"); @@ -345,7 +478,7 @@ test("429 try-and-commit returns throttle metadata without committing or reading test("logout 429 is observable and never navigates or changes stored tokens", async () => { let calls = 0; - const uauth = createEnvironment(async (_url, request) => { + const uauth = createEnvironment(csrfAwareFetch(async (_url, request) => { calls++; assert.equal(request.redirect, "error"); @@ -363,15 +496,18 @@ test("logout 429 is observable and never navigates or changes stored tokens", as }, text: async () => "" }; - }, () => assert.fail("Rejected logout must not navigate")); + }), () => assert.fail("Rejected logout must not navigate")); + + configureAntiforgery(uauth); uauth.storage.set("local", "access", "still-valid-access"); uauth.storage.set("session", "refresh", "still-valid-refresh"); const result = await uauth.post({ - url: "/auth/logout", + url: authUrl("/auth/logout"), mode: "logout", - clientProfile: "BlazorServer" + clientProfile: "BlazorServer", + trustedOrigin: HUB_ORIGIN }); assert.equal(result.status, 429); @@ -393,26 +529,29 @@ test("resource 429 preserves Retry-After without automatic retry", async () => { assert.equal(calls, 1); }); - test("refresh 429 leaves stored credentials untouched and sends only once", async () => { +test("refresh 429 leaves stored credentials untouched and sends only once", async () => { let calls = 0; - const uauth = createEnvironment(async () => { + const uauth = createEnvironment(csrfAwareFetch(async () => { calls++; return { ok: false, status: 429, headers: { get: () => null }, text: async () => "" }; - }); + })); + + configureAntiforgery(uauth); + uauth.storage.set("local", "access", "valid-access"); uauth.storage.set("session", "refresh", "valid-refresh"); - const result = await uauth.post({ url: "/auth/refresh", mode: "fetch", clientProfile: "BlazorWasm" }); + const result = await uauth.post({ url: authUrl("/auth/refresh"), mode: "fetch", clientProfile: "BlazorWasm", trustedOrigin: HUB_ORIGIN }); assert.equal(result.status, 429); assert.equal(uauth.storage.get("local", "access"), "valid-access"); assert.equal(uauth.storage.get("session", "refresh"), "valid-refresh"); assert.equal(calls, 1); - }); +}); test("successful observed logout navigates after confirmed success without replaying POST", async () => { let calls = 0; let navigation = null; - const uauth = createEnvironment(async (_url, request) => { + const uauth = createEnvironment(csrfAwareFetch(async (_url, request) => { calls++; assert.equal(request.redirect, "error"); @@ -431,12 +570,15 @@ test("successful observed logout navigates after confirmed success without repla redirectUrl: "https://localhost:6130/login" }) }; - }, url => navigation = url); + }), url => navigation = url); + + configureAntiforgery(uauth); const result = await uauth.post({ - url: "/auth/logout", + url: authUrl("/auth/logout"), mode: "logout", - clientProfile: "BlazorWasm" + clientProfile: "BlazorWasm", + trustedOrigin: HUB_ORIGIN }); assert.equal(result.status, 200); @@ -447,7 +589,7 @@ test("successful observed logout navigates after confirmed success without repla test("observed logout rejects unsafe redirect protocols", async () => { let navigation = null; - const uauth = createEnvironment(async () => ({ + const uauth = createEnvironment(csrfAwareFetch(async () => ({ ok: true, status: 200, redirected: false, @@ -456,13 +598,16 @@ test("observed logout rejects unsafe redirect protocols", async () => { isSuccess: true, redirectUrl: "javascript:alert(1)" }) - }), url => navigation = url); + })), url => navigation = url); + + configureAntiforgery(uauth); await assert.rejects( uauth.post({ - url: "/auth/logout", + url: authUrl("/auth/logout"), mode: "logout", - clientProfile: "BlazorWasm" + clientProfile: "BlazorWasm", + trustedOrigin: HUB_ORIGIN }), /Invalid logout redirect protocol/ ); From 7fdba58b043fd73bcb4bc503744eed5aecbfbd40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Can=20Karag=C3=B6z?= <78308169+mckaragoz@users.noreply.github.com> Date: Fri, 6 Nov 2026 14:12:11 +0300 Subject: [PATCH 05/11] test: Add CSRF regression coverage and update endpoint classification (#144) * test: Add CSRF regression coverage and update endpoint classification * Fix TrustedOrigin in Request Client and Client Bootstrapper --- .../Program.cs | 1 + .../Infrastructure/UAuthRequestClient.cs | 14 ++- .../Infrastructure/UAuthClientBootstrapper.cs | 8 +- .../LoginCsrfExploitProofTests.cs | 112 ++++++++++++++++++ .../Server/RateLimitingPartitionTests.cs | 5 +- 5 files changed, 126 insertions(+), 14 deletions(-) create mode 100644 tests/CodeBeam.UltimateAuth.Tests.Integration/LoginCsrfExploitProofTests.cs diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Program.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Program.cs index b6184b9a..8ce3a825 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Program.cs +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Program.cs @@ -14,6 +14,7 @@ using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Infrastructure; using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.Extensions.Options; using MudBlazor.Services; using MudExtensions.Services; using Scalar.AspNetCore; diff --git a/src/client/CodeBeam.UltimateAuth.Client.Blazor/Infrastructure/UAuthRequestClient.cs b/src/client/CodeBeam.UltimateAuth.Client.Blazor/Infrastructure/UAuthRequestClient.cs index eee577e8..f1802b21 100644 --- a/src/client/CodeBeam.UltimateAuth.Client.Blazor/Infrastructure/UAuthRequestClient.cs +++ b/src/client/CodeBeam.UltimateAuth.Client.Blazor/Infrastructure/UAuthRequestClient.cs @@ -135,6 +135,7 @@ public async Task TryAndCommitAsync(string tryEndpoint, if (response.ValueKind != JsonValueKind.Object) throw new UAuthProtocolException("Invalid tryAndCommit response."); + if (response.TryGetProperty("status", out var status) && status.ValueKind == JsonValueKind.Number && status.TryGetInt32(out var code) && code == 429) throw UAuthRateLimit.Exception(response.TryGetProperty("retryAfter", out var retry) && retry.ValueKind == JsonValueKind.String ? retry.GetString() : null); try @@ -145,16 +146,17 @@ public async Task TryAndCommitAsync(string tryEndpoint, catch (JsonException ex) { throw new UAuthProtocolException("Invalid tryAndCommit response.", ex); } } + private string? ResolveTrustedOrigin() { var basePath = _options.Endpoints.BasePath; - if (!Uri.TryCreate(basePath, UriKind.Absolute, out var uri)) - return null; - - if (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps) - return null; + if (Uri.TryCreate(basePath, UriKind.Absolute, out var hubUri) && + (hubUri.Scheme == Uri.UriSchemeHttp || hubUri.Scheme == Uri.UriSchemeHttps)) + { + return hubUri.GetLeftPart(UriPartial.Authority); + } - return uri.GetLeftPart(UriPartial.Authority); + return null; } } diff --git a/src/client/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthClientBootstrapper.cs b/src/client/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthClientBootstrapper.cs index 39203317..6cee1b18 100644 --- a/src/client/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthClientBootstrapper.cs +++ b/src/client/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthClientBootstrapper.cs @@ -45,13 +45,9 @@ public async Task EnsureStartedAsync(CancellationToken ct = default) string? trustedOrigin = null; - if (Uri.TryCreate(basePath, UriKind.Absolute, out var hubUri)) + if (Uri.TryCreate(basePath, UriKind.Absolute, out var hubUri) && + (hubUri.Scheme == Uri.UriSchemeHttp || hubUri.Scheme == Uri.UriSchemeHttps)) { - if (hubUri.Scheme != Uri.UriSchemeHttp && hubUri.Scheme != Uri.UriSchemeHttps) - { - throw new InvalidOperationException("UAuthHub URL must use HTTP or HTTPS."); - } - trustedOrigin = hubUri.GetLeftPart(UriPartial.Authority); } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/LoginCsrfExploitProofTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/LoginCsrfExploitProofTests.cs new file mode 100644 index 00000000..dc7374bd --- /dev/null +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/LoginCsrfExploitProofTests.cs @@ -0,0 +1,112 @@ +using System.Net; +using System.Net.Http.Json; +using AwesomeAssertions; +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Tests.Integration.Infrastructure; +using Microsoft.AspNetCore.Mvc.Testing; + +namespace CodeBeam.UltimateAuth.Tests.Integration.Security; + +public sealed class LoginCsrfExploitProofTests +{ + [Fact] + public async Task CrossOriginLogin_WithoutCsrfProof_MustNotAuthenticateVictimAsAttacker() + { + await using var factory = new AuthServerFactory(); + + var attacker = await factory.CreateLoginUserAsync(); + var deviceId = $"csrf-victim-{Guid.NewGuid():N}"; + + using var victim = CreateBrowser(factory, deviceId); + + // The victim starts unauthenticated. + await AssertUnauthenticatedAsync(victim); + + // The attacker submits valid credentials without CSRF proof. + using var request = new HttpRequestMessage( + HttpMethod.Post, + "/auth/login") + { + Content = JsonContent.Create(new + { + identifier = attacker.Identifier, + secret = attacker.Secret + }) + }; + + request.Headers.TryAddWithoutValidation( + "Origin", + "https://localhost:6130"); + + using var response = await victim.SendAsync(request); + + var body = await response.Content.ReadAsStringAsync(); + + // Prove that the rejection came from CSRF validation. + response.StatusCode.Should().Be( + HttpStatusCode.BadRequest, + $"unexpected login response: {body}"); + + body.Should().Contain( + "invalid_antiforgery_token"); + + // The rejected login must not issue a session cookie. + var sessionCookie = GetSessionCookie(response); + + sessionCookie.Should().BeNull( + "a rejected CSRF login must not establish a session"); + + // The victim must remain unauthenticated. + await AssertUnauthenticatedAsync(victim); + } + + private static async Task AssertUnauthenticatedAsync( + HttpClient client) + { + using var response = await client.PostAsJsonAsync( + "/auth/me/sessions/chains", + new PageRequest + { + PageNumber = 1, + PageSize = 10 + }); + + response.StatusCode.Should().Be( + HttpStatusCode.Unauthorized); + } + + private static HttpClient CreateBrowser( + AuthServerFactory factory, + string deviceId) + { + var client = factory.CreateClient( + new WebApplicationFactoryClientOptions + { + AllowAutoRedirect = false, + HandleCookies = false + }); + + client.DefaultRequestHeaders.TryAddWithoutValidation( + "X-UDID", + deviceId); + + return client; + } + + private static string? GetSessionCookie( + HttpResponseMessage response) + { + if (!response.Headers.TryGetValues( + "Set-Cookie", + out var cookies)) + { + return null; + } + + return cookies + .Select(x => x.Split(';', 2)[0]) + .FirstOrDefault(x => x.StartsWith( + "uas=", + StringComparison.OrdinalIgnoreCase)); + } +} diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/RateLimitingPartitionTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/RateLimitingPartitionTests.cs index fef13244..5a6cea5b 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/RateLimitingPartitionTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/RateLimitingPartitionTests.cs @@ -39,13 +39,14 @@ public async Task RejectionForwardsNativeRetryMetadata() public void EveryEndpointHasTheReviewedClassification() { var all = Enum.GetValues().Select(UAuthEndpointClassification.Get).ToArray(); - Assert.Equal(80, all.Length); + Assert.Equal(81, all.Length); Assert.Equal(20, all.Count(x => x.Group == UAuthRateLimitGroup.Sensitive)); Assert.Equal(56, all.Count(x => x.Group == UAuthRateLimitGroup.Standard)); - Assert.Equal(4, all.Count(x => x.Group == UAuthRateLimitGroup.Frequent)); + Assert.Equal(5, all.Count(x => x.Group == UAuthRateLimitGroup.Frequent)); Assert.Equal(UAuthRateLimitGroup.Sensitive, UAuthEndpointClassification.Get(UAuthEndpoint.CredentialResetBeginAnonymous).Group); Assert.Equal(UAuthRateLimitGroup.Standard, UAuthEndpointClassification.Get(UAuthEndpoint.VerifyUserIdentifierAdmin).Group); Assert.Equal(UAuthRateLimitGroup.Frequent, UAuthEndpointClassification.Get(UAuthEndpoint.Logout).Group); + Assert.Equal(UAuthRateLimitGroup.Frequent, UAuthEndpointClassification.Get(UAuthEndpoint.AntiforgeryToken).Group); } [Fact] From 94938309b392b2deb639fcb38bd4944de936ab2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Can=20Karag=C3=B6z?= <78308169+mckaragoz@users.noreply.github.com> Date: Fri, 6 Nov 2026 15:40:00 +0300 Subject: [PATCH 06/11] Fix UAuthUrlBuilder when Tenant Specified (#145) * Fix UAuthUrlBuilder when Tenant Specified * Fix Flaky Logout Test * More Fixes * More Fixes --- .../Infrastructure/UAuthUrlBuilder.cs | 43 +++++-- ...Beam.UltimateAuth.IdentityLab.Tests.csproj | 4 +- ...timateAuth.Tests.Integration.EfCore.csproj | 2 +- ...Beam.UltimateAuth.Tests.Integration.csproj | 2 +- .../LogoutTests.cs | 106 +++++++----------- ...th.Tests.RateLimiting.Compatibility.csproj | 4 + .../Client/UAuthUrlBuilderTests.cs | 84 ++++++++++++++ .../CodeBeam.UltimateAuth.Tests.Unit.csproj | 2 +- 8 files changed, 168 insertions(+), 79 deletions(-) create mode 100644 tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthUrlBuilderTests.cs diff --git a/src/client/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthUrlBuilder.cs b/src/client/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthUrlBuilder.cs index b17402d6..4b6de185 100644 --- a/src/client/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthUrlBuilder.cs +++ b/src/client/CodeBeam.UltimateAuth.Client/Infrastructure/UAuthUrlBuilder.cs @@ -19,20 +19,45 @@ public static class UAuthUrlBuilder public static string Build(string authority, string relativePath, UAuthClientMultiTenantOptions tenant) { var baseAuthority = authority.TrimEnd('/'); + var endpoint = relativePath.TrimStart('/'); - // TODO: Correct route-based tenant URL construction when authority - // is an absolute URL (e.g. https://localhost:6110/auth). - // Preserve the scheme and host, and insert the tenant into the path. - if (tenant.Enabled && tenant.Transport == TenantTransport.Route) + if (!tenant.Enabled || tenant.Transport != TenantTransport.Route) + return baseAuthority + "/" + endpoint; + + if (string.IsNullOrWhiteSpace(tenant.Tenant)) + throw new InvalidOperationException("Tenant is enabled for route transport but no tenant value is provided."); + + var tenantId = tenant.Tenant.Trim('/'); + + if (tenantId.Length == 0 || + tenantId is "." or ".." || + tenantId.Contains('/') || + tenantId.Contains('\\') || + tenantId.Contains('?') || + tenantId.Contains('#')) { - if (string.IsNullOrWhiteSpace(tenant.Tenant)) + throw new ArgumentException("Invalid tenant route segment.", nameof(tenant)); + } + + var segment = Uri.EscapeDataString(tenantId); + + if (baseAuthority.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || baseAuthority.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) + { + if (Uri.TryCreate(baseAuthority, UriKind.Absolute, out var uri)) { - throw new InvalidOperationException("Tenant is enabled for route transport but no tenant value is provided."); - } + if (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps) + throw new ArgumentException("Authority must use HTTP or HTTPS.", nameof(authority)); + + if (!string.IsNullOrEmpty(uri.Query) || !string.IsNullOrEmpty(uri.Fragment)) + throw new ArgumentException("Authority cannot contain a query or fragment.", nameof(authority)); - baseAuthority = "/" + tenant.Tenant.Trim('/') + baseAuthority; + var origin = uri.GetLeftPart(UriPartial.Authority); + var path = uri.AbsolutePath.TrimEnd('/'); + + return origin + "/" + segment + path + "/" + endpoint; + } } - return baseAuthority + "/" + relativePath.TrimStart('/'); + return "/" + segment + "/" + baseAuthority.TrimStart('/') + "/" + endpoint; } } diff --git a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/CodeBeam.UltimateAuth.IdentityLab.Tests.csproj b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/CodeBeam.UltimateAuth.IdentityLab.Tests.csproj index 8adc52da..85ebd04e 100644 --- a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/CodeBeam.UltimateAuth.IdentityLab.Tests.csproj +++ b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/CodeBeam.UltimateAuth.IdentityLab.Tests.csproj @@ -1,4 +1,4 @@ - + net10.0 false @@ -8,7 +8,7 @@ - + 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 d282dbc5..ed85da6c 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 @@ -8,7 +8,7 @@ - + diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/CodeBeam.UltimateAuth.Tests.Integration.csproj b/tests/CodeBeam.UltimateAuth.Tests.Integration/CodeBeam.UltimateAuth.Tests.Integration.csproj index 956c4a59..b876ff76 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration/CodeBeam.UltimateAuth.Tests.Integration.csproj +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/CodeBeam.UltimateAuth.Tests.Integration.csproj @@ -8,7 +8,7 @@ - + diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/LogoutTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/LogoutTests.cs index 4d960c24..850f7be3 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration/LogoutTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/LogoutTests.cs @@ -514,7 +514,8 @@ public async Task Logout_ShouldNotRevokeDeviceChain() var chainBefore = before!.Items.Single(x => x.IsCurrentDevice); var chainId = chainBefore.ChainId; - await CsrfTestClient.PostAsync(client, "/auth/logout", null); + var logout = await CsrfTestClient.PostAsync(client, "/auth/logout", null); + logout.StatusCode.Should().Be(HttpStatusCode.Found); using var reloginClient = CreateClient(device); @@ -979,46 +980,6 @@ await CsrfTestClient.PostJsonAsync(device2Client, device2After.IsRevoked.Should().BeFalse(); } - [Fact] - public async Task Logout_WithAlreadyLoggedOutSession_ShouldRemainSafe() - { - _factory.Clock.Reset(); - - var user = await _factory.CreateLoginUserAsync(); - var device = $"logout-idempotent-{Guid.NewGuid():N}"; - - using var client = CreateClient(device); - - var login = await LoginAsync( - client, - user.Identifier, - user.Secret); - - login.StatusCode.Should().Be(HttpStatusCode.Found); - - var cookie = GetSessionCookie(login); - - client.DefaultRequestHeaders.Add("Cookie", cookie); - - var firstLogout = await CsrfTestClient.PostAsync(client, "/auth/logout", null); - - firstLogout.StatusCode.Should().Be(HttpStatusCode.Found); - - var secondLogout = await CsrfTestClient.PostAsync(client, "/auth/logout", null); - - secondLogout.StatusCode.Should().BeOneOf(HttpStatusCode.Unauthorized, HttpStatusCode.Found); - - var authenticatedRequest = await CsrfTestClient.PostJsonAsync(client, - "/auth/me/sessions/chains", - new PageRequest - { - PageNumber = 1, - PageSize = 50 - }); - - authenticatedRequest.StatusCode.Should().Be(HttpStatusCode.Unauthorized); - } - [Fact] public async Task Logout_ShouldNotReturnAuthenticationCredentials() { @@ -1276,15 +1237,23 @@ public async Task ConcurrentLogout_WithSameSession_ShouldLeaveSessionInvalid() CsrfTestClient.PostAsync(client1, "/auth/logout", null), CsrfTestClient.PostAsync(client2, "/auth/logout", null)); - // - // Depending on where stale-session rejection occurs, - // one request may observe the session after the other revoked it. - // - // What must never happen is an infrastructure failure. - // - responses.Should().OnlyContain(x => - x.StatusCode == HttpStatusCode.Found || - x.StatusCode == HttpStatusCode.Unauthorized); + foreach (var response in responses) + { + if (response.StatusCode == HttpStatusCode.BadRequest) + { + var body = await response.Content.ReadAsStringAsync(); + + body.Should().Contain("invalid_antiforgery_token"); + continue; + } + + response.StatusCode.Should().BeOneOf( + HttpStatusCode.Found, + HttpStatusCode.Unauthorized); + } + + responses.Should().Contain(x => + x.StatusCode == HttpStatusCode.Found); // // Final state is the actual security invariant. @@ -3475,10 +3444,7 @@ await LoginAsync( user.Identifier, user.Secret)); - // - // Two independent HTTP requests start with the same - // authenticated actor authority. - // + // Two independent HTTP requests start with the same authenticated actor authority. using var request1 = CreateClient(actorDevice); using var request2 = CreateClient(actorDevice); @@ -3499,22 +3465,30 @@ await LoginAsync( "/auth/me/logout-all", null)); - // - // Depending on where authentication is linearized, - // the second request may observe either the pre-logout - // or post-logout authority. - // - responses.Should().OnlyContain(x => - x.StatusCode == HttpStatusCode.OK || - x.StatusCode == HttpStatusCode.Unauthorized); + foreach (var response in responses) + { + if (response.StatusCode == HttpStatusCode.BadRequest) + { + var body = await response.Content + .ReadFromJsonAsync>(); + + body.Should().NotBeNull(); + body.Should().ContainKey("error") + .WhoseValue.Should().Be("invalid_antiforgery_token"); + + continue; + } + + response.StatusCode.Should().BeOneOf( + HttpStatusCode.OK, + HttpStatusCode.Unauthorized); + } responses.Should().Contain(x => x.StatusCode == HttpStatusCode.OK); - // // Final state is the security invariant that matters: // no old authority may survive. - // using var actorVerification = CreateClient(actorDevice); @@ -3588,7 +3562,9 @@ private static string GetSessionCookie(HttpResponseMessage response) cookies.Should().NotBeNullOrEmpty(); - var cookie = cookies!.First(); + var cookie = cookies! + .Select(x => x.Split(';', 2)[0]) + .First(x => x.StartsWith("uas=", StringComparison.OrdinalIgnoreCase)); cookie.Should().NotBeNullOrWhiteSpace(); return cookie; diff --git a/tests/CodeBeam.UltimateAuth.Tests.RateLimiting.Compatibility/CodeBeam.UltimateAuth.Tests.RateLimiting.Compatibility.csproj b/tests/CodeBeam.UltimateAuth.Tests.RateLimiting.Compatibility/CodeBeam.UltimateAuth.Tests.RateLimiting.Compatibility.csproj index 039804bc..7fe0d607 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.RateLimiting.Compatibility/CodeBeam.UltimateAuth.Tests.RateLimiting.Compatibility.csproj +++ b/tests/CodeBeam.UltimateAuth.Tests.RateLimiting.Compatibility/CodeBeam.UltimateAuth.Tests.RateLimiting.Compatibility.csproj @@ -8,6 +8,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthUrlBuilderTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthUrlBuilderTests.cs new file mode 100644 index 00000000..0a7a7054 --- /dev/null +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthUrlBuilderTests.cs @@ -0,0 +1,84 @@ +using CodeBeam.UltimateAuth.Client.Contracts; +using CodeBeam.UltimateAuth.Client.Infrastructure; +using CodeBeam.UltimateAuth.Client.Options; + +namespace CodeBeam.UltimateAuth.Tests.Unit.Client; + +public class UAuthUrlBuilderTests +{ + [Theory] + [InlineData("/auth", "/login", "/acme/auth/login")] + [InlineData("/auth/", "/login", "/acme/auth/login")] + [InlineData("https://localhost:6110/auth", "/login", "https://localhost:6110/acme/auth/login")] + [InlineData("https://example.com/api/auth", "/refresh", "https://example.com/acme/api/auth/refresh")] + [InlineData("https://example.com/auth/", "/pkce/authorize", "https://example.com/acme/auth/pkce/authorize")] + public void Build_RouteTenant_InsertsTenantBeforeBasePath(string basePath, string endpoint, string expected) + { + var tenant = new UAuthClientMultiTenantOptions + { + Enabled = true, + Tenant = "acme", + Transport = TenantTransport.Route + }; + + var result = UAuthUrlBuilder.Build( + basePath, endpoint, tenant); + + Assert.Equal(expected, result); + } + + [Theory] + [InlineData(false, TenantTransport.Route)] + [InlineData(true, TenantTransport.Header)] + [InlineData(true, TenantTransport.None)] + public void Build_WithoutRouteTenant_PreservesUrl( + bool enabled, TenantTransport transport) + { + var tenant = new UAuthClientMultiTenantOptions + { + Enabled = enabled, + Tenant = "acme", + Transport = transport + }; + + var result = UAuthUrlBuilder.Build( + "https://example.com/auth", "/login", tenant); + + Assert.Equal("https://example.com/auth/login", result); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void Build_RouteTenantWithoutValue_Throws(string? value) + { + var tenant = new UAuthClientMultiTenantOptions + { + Enabled = true, + Tenant = value, + Transport = TenantTransport.Route + }; + + Assert.Throws(() => + UAuthUrlBuilder.Build("/auth", "/login", tenant)); + } + + [Theory] + [InlineData("foo/bar")] + [InlineData("../admin")] + [InlineData("foo?bar")] + [InlineData("foo#bar")] + public void Build_InvalidTenantSegment_Throws(string value) + { + var tenant = new UAuthClientMultiTenantOptions + { + Enabled = true, + Tenant = value, + Transport = TenantTransport.Route + }; + + Assert.Throws(() => + UAuthUrlBuilder.Build("/auth", "/login", tenant)); + } +} diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/CodeBeam.UltimateAuth.Tests.Unit.csproj b/tests/CodeBeam.UltimateAuth.Tests.Unit/CodeBeam.UltimateAuth.Tests.Unit.csproj index 6940539a..d90a6ba2 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/CodeBeam.UltimateAuth.Tests.Unit.csproj +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/CodeBeam.UltimateAuth.Tests.Unit.csproj @@ -9,7 +9,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive From 95d75d47a38395747ec8230dda0f93645e51d71a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Can=20Karag=C3=B6z?= <78308169+mckaragoz@users.noreply.github.com> Date: Sat, 7 Nov 2026 00:47:44 +0300 Subject: [PATCH 07/11] Modernize Dependencies, Test Infrastructure and CI Workflows (#146) * Update Dependencies & Identity Lab Arrangements * test: propagate xUnit v3 cancellation tokens Pass the current xUnit test cancellation token to ordinary asynchronous test operations in unit, integration, and Identity Lab tests. Keep exact token matching in the snapshot factory mock and qualify the xUnit context where bUnit also defines TestContext. Preserve explicit cancellation sources and non-cancellable operations. Leave the two transaction cleanup rollbacks uncancellable, with explanatory comments. Validation: xUnit1051 decreases from 2,403 to 2 intentional cleanup exceptions. * ci: migrate .NET 10 tests to Microsoft.Testing.Platform * build: centralize supported frameworks and derive CI compatibility matrix * ci: standardize workflow checks and conditional PR validation * Some Additional Changes --- .docker/Dockerfile | 4 +- .docker/Dockerfile.identitylab | 4 +- .github/workflows/deploy.yml | 55 ++++- .github/workflows/identitylab-image.yml | 68 ++++--- .github/workflows/javascript-tests.yml | 26 +-- .github/workflows/nuget-publish.yml | 21 +- .github/workflows/ultimateauth-ci.yml | 187 ++++++++++++++--- CONTRIBUTING.md | 39 ++++ Directory.Build.props | 6 + README.md | 43 ++-- .../CodeBeam.UltimateAuth.IdentityLab.csproj | 8 +- .../Pages/Dashboard/MyAccessPage.razor | 2 +- ...eBeam.UltimateAuth.Docs.Wasm.Client.csproj | 6 +- .../Layout/MainLayout.razor | 2 +- .../Pages/Samples.razor | 2 +- .../CodeBeam.UltimateAuth.Docs.Wasm.csproj | 2 +- global.json | 5 + .../CodeBeam.UltimateAuth.Sample.Seed.csproj | 2 +- ...Beam.UltimateAuth.Sample.Shared.Hub.csproj | 5 +- ...CodeBeam.UltimateAuth.Sample.Shared.csproj | 14 +- ...UltimateAuth.Sample.UAuthHub.EFCore.csproj | 10 +- ...deBeam.UltimateAuth.Sample.UAuthHub.csproj | 4 +- ...mateAuth.Sample.BlazorServer.EFCore.csproj | 10 +- ...am.UltimateAuth.Sample.BlazorServer.csproj | 4 +- ...ateAuth.Sample.BlazorStandaloneWasm.csproj | 8 +- ...odeBeam.UAuth.Sample.IntWasm.Client.csproj | 4 +- .../CodeBeam.UAuth.Sample.IntWasm.csproj | 2 +- ...imateAuth.Sample.ResourceApi.EfCore.csproj | 2 +- ...eam.UltimateAuth.Sample.ResourceApi.csproj | 2 +- .../CodeBeam.UltimateAuth.Core.csproj | 2 +- .../CodeBeam.UltimateAuth.Server.csproj | 4 +- ....Authentication.EntityFrameworkCore.csproj | 2 +- ...ltimateAuth.Authentication.InMemory.csproj | 2 +- ...ltimateAuth.Authorization.Contracts.csproj | 2 +- ...h.Authorization.EntityFrameworkCore.csproj | 2 +- ...UltimateAuth.Authorization.InMemory.csproj | 2 +- ...ltimateAuth.Authorization.Reference.csproj | 2 +- ...CodeBeam.UltimateAuth.Authorization.csproj | 2 +- ...mateAuth.EntityFrameworkCore.Bundle.csproj | 2 +- ...deBeam.UltimateAuth.InMemory.Bundle.csproj | 2 +- ...eBeam.UltimateAuth.Reference.Bundle.csproj | 2 +- ...Beam.UltimateAuth.Client.AspNetCore.csproj | 2 +- ...CodeBeam.UltimateAuth.Client.Blazor.csproj | 28 +-- ...Beam.UltimateAuth.Client.JsMinifier.csproj | 2 +- .../CodeBeam.UltimateAuth.Client.csproj | 2 +- ....UltimateAuth.Credentials.Contracts.csproj | 2 +- ...uth.Credentials.EntityFrameworkCore.csproj | 2 +- ...m.UltimateAuth.Credentials.InMemory.csproj | 2 +- ....UltimateAuth.Credentials.Reference.csproj | 2 +- .../CodeBeam.UltimateAuth.Credentials.csproj | 2 +- ...am.UltimateAuth.EntityFrameworkCore.csproj | 14 +- .../CodeBeam.UltimateAuth.InMemory.csproj | 2 +- .../CodeBeam.UltimateAuth.Policies.csproj | 2 +- ...deBeam.UltimateAuth.Security.Argon2.csproj | 2 +- ...teAuth.Sessions.EntityFrameworkCore.csproj | 2 +- ...Beam.UltimateAuth.Sessions.InMemory.csproj | 2 +- ...mateAuth.Tokens.EntityFrameworkCore.csproj | 2 +- ...deBeam.UltimateAuth.Tokens.InMemory.csproj | 2 +- ...deBeam.UltimateAuth.Users.Contracts.csproj | 2 +- ...imateAuth.Users.EntityFrameworkCore.csproj | 2 +- ...odeBeam.UltimateAuth.Users.InMemory.csproj | 2 +- ...deBeam.UltimateAuth.Users.Reference.csproj | 2 +- .../CodeBeam.UltimateAuth.Users.csproj | 2 +- .../CodeBeam.UltimateAuth.DocsBuilder.csproj | 4 +- .../AssemblyBehavior.cs | 4 +- .../Authorization/SandboxPolicyTests.cs | 18 +- ...Beam.UltimateAuth.IdentityLab.Tests.csproj | 16 +- .../Infrastructure/PostgresFixture.cs | 4 +- .../RegistrationIntegrationTests.cs | 71 ++++--- .../Integration/SandboxPersistenceTests.cs | 50 ++--- .../Integration/UserWorkflowTests.cs | 62 +++--- .../Provisioning/MemberProvisioningTests.cs | 6 +- .../Provisioning/ProvisioningPolicyTests.cs | 2 +- .../Provisioning/RegistrationTests.cs | 4 +- .../README.md | 8 +- .../Regression/SystemRoleIdentityTests.cs | 2 +- .../Security/AdminGuardTests.cs | 4 +- .../Security/RolePolicyTests.cs | 8 +- .../Services/AccountDeletionTests.cs | 10 +- .../Services/DashboardTests.cs | 14 +- .../Services/DemoRoleServiceTests.cs | 8 +- .../Services/DemoStoreTests.cs | 8 +- .../Services/RepairTests.cs | 18 +- .../Services/SandboxAdminTests.cs | 38 ++-- .../identity-lab.runsettings | 14 -- ...timateAuth.Tests.Integration.EfCore.csproj | 10 +- .../UserCreationAtomicityTests.cs | 8 +- .../UserQueryPaginationTests.cs | 10 +- .../AssemblyBehavior.cs | 4 +- .../AuthServerTests.cs | 1 - .../AuthorizationManagementTests.cs | 130 ++++++------ .../ClientRateLimitingIntegrationTests.cs | 16 +- ...Beam.UltimateAuth.Tests.Integration.csproj | 14 +- .../CredentialManagementTests.cs | 104 +++++----- .../HttpContextJsonContractTests.cs | 20 +- .../LoginCsrfExploitProofTests.cs | 6 +- .../LoginTests.cs | 172 ++++++++-------- .../LogoutAdminTests.cs | 112 +++++----- .../LogoutTests.cs | 178 ++++++++-------- .../PkceFlowIntegrationTests.cs | 14 +- .../RateLimitingFlowTests.cs | 8 +- .../RateLimitingProofFlowTests.cs | 18 +- .../RateLimitingTests.cs | 2 +- .../RefreshTests.cs | 128 ++++++------ .../ResourceApiAuthenticationTests.cs | 16 +- .../ResourceApiAuthorizationTests.cs | 20 +- .../ResourceApiTenantIsolationTests.cs | 14 +- .../SdkEnpointContractTests.cs | 58 +++--- .../Security/CsrfAttackTests.cs | 24 +-- .../Security/UAuthAntiforgeryEndpointTests.cs | 26 +-- .../SessionAdminTests.cs | 176 ++++++++-------- .../SessionSlidingExpirationTests.cs | 6 +- .../SessionTests.cs | 48 ++--- .../UAuthHubSampleSmokeTests.cs | 2 +- .../UserCreationAtomicityTests.cs | 8 +- .../UserIdentifierTests.cs | 48 ++--- .../UserLifecycleTests.cs | 36 ++-- .../UserProfileTests.cs | 24 ++- ...th.Tests.RateLimiting.Compatibility.csproj | 9 +- .../AssemblyBehavior.cs | 4 +- ...ticationSecurityStateStoreContractTests.cs | 88 ++++---- .../AuthorizationServiceTests.cs | 4 +- .../Authorization/RoleServiceTests.cs | 10 +- .../Store/RoleStoreContractTests.cs | 110 +++++----- .../Store/UserRoleStoreContractTests.cs | 98 ++++----- .../UserPermissionServiceTests.cs | 6 +- .../Authorization/UserRoleServiceTests.cs | 36 ++-- .../Client/AuthStateSnapshotFactoryTests.cs | 6 +- .../Client/Blazor/UAuthRequestClientTests.cs | 12 +- .../Client/ClientRateLimitingTests.cs | 8 +- .../Client/SessionCoordinatorTests.cs | 2 +- .../Client/UAuthClientBootstrapperTests.cs | 12 +- .../Client/UAuthResourceClientTests.cs | 20 +- .../Client/UAuthResourceRequestClientTests.cs | 12 +- .../Client/UAuthStateManagerTests.cs | 60 +++--- .../CodeBeam.UltimateAuth.Tests.Unit.csproj | 20 +- .../Core/RefreshTokenValidatorTests.cs | 26 +-- .../Credentials/ChangePasswordTests.cs | 14 +- .../CredentialManagementServiceTests.cs | 26 +-- .../EfCorePasswordHistoryAtomicityTests.cs | 17 +- .../PasswordCredentialWriterTests.cs | 16 +- .../PasswordValidationPipelineTests.cs | 10 +- .../Credentials/ResetPasswordTests.cs | 24 +-- .../PasswordCredentialStoreContractTests.cs | 102 +++++----- .../PasswordHistoryStoreContractTests.cs | 124 +++++------ .../UAuthPasswordHistoryValidatorTests.cs | 14 +- .../EfCoreAuthenticationStoreTests.cs | 46 ++--- .../EfCoreCredentialAtomicityTests.cs | 44 ++-- .../EfCoreCredentialStoreTests.cs | 48 ++--- .../EfCoreRoleStoreTests.cs | 52 ++--- .../EfCoreSessionStoreTests.cs | 69 +++---- .../EfCoreTokenStoreTests.cs | 32 +-- .../EfCoreUserIdentifierStoreTests.cs | 30 +-- .../EfCoreUserLifecycleStoreTests.cs | 40 ++-- .../EfCoreUserProfileStoreTests.cs | 66 +++--- .../EfCoreUserRoleStoreTests.cs | 34 ++-- .../InMemory/InMemoryVersionedStoreTests.cs | 50 ++--- .../Policies/AccessPoliciesTests.cs | 54 ++--- .../RemoteSessionValidatorTests.cs | 18 +- .../Server/AccessContextFactoryTests.cs | 12 +- .../Server/AccessPipelineTests.cs | 20 +- .../Server/AuthAuthorityTests.cs | 4 +- .../AuthenticationSecurityManagerTests.cs | 26 +-- .../Server/HubCredentialResolverTests.cs | 22 +- .../Server/HubFlowReaderTests.cs | 22 +- .../Server/HubFlowServiceTests.cs | 20 +- .../Server/LoginOrchestratorTests.cs | 94 ++++----- .../Server/PkceServiceTests.cs | 24 +-- .../Server/PkceTests.cs | 20 +- .../Server/RateLimitingPartitionTests.cs | 2 +- .../Server/RefreshFlowServiceTests.cs | 30 +-- .../RefreshTokenRotationServiceTests.cs | 20 +- .../ResourceAccessContextBuilderTests.cs | 16 +- .../Server/SessionApplicationServiceTests.cs | 178 ++++++++-------- .../Server/SessionTouchServiceTests.cs | 56 ++--- .../Server/TryLoginTests.cs | 2 +- .../Server/UAuthFlowServiceTests.cs | 22 +- .../UAuthResourceAccessOrchestratorTests.cs | 14 +- .../Server/UAuthSessionValidatorTests.cs | 28 +-- .../CompositeSessionIdResolverTests.cs | 18 +- .../Sessions/SessionTests.cs | 34 ++-- .../Store/SessionStoreChainContractTests.cs | 32 +-- .../SessionStoreLifecycleContractTests.cs | 192 +++++++++--------- .../Store/SessionStoreRootContractTests.cs | 44 ++-- .../Store/SessionStoreSessionContractTests.cs | 42 ++-- .../Sessions/UAuthSessionIssuerTests.cs | 58 +++--- .../Tokens/RefreshTokenStoreContractTests.cs | 124 +++++------ .../Tokens/UAuthTokenIssuerTests.cs | 58 +++--- .../Users/IdentifierConcurrencyTests.cs | 54 ++--- .../UserSummaryQueryStoreContractTests.cs | 62 +++--- .../Store/UserIdentifierStoreContractTests.cs | 168 +++++++-------- .../Store/UserLifecycleStoreContractTests.cs | 112 +++++----- .../Store/UserProfileStoreContractTests.cs | 130 ++++++------ .../Users/UserApplicationServiceTests.cs | 54 ++--- .../Users/UserCreateValidatorTests.cs | 14 +- .../UserIdentifierApplicationServiceTests.cs | 66 +++--- .../Users/UserIdentifierValidationTests.cs | 22 +- .../Users/UserProfileValidatorTests.cs | 2 +- tests/Directory.Build.props | 13 ++ 199 files changed, 3078 insertions(+), 2815 deletions(-) create mode 100644 global.json delete mode 100644 tests/CodeBeam.UltimateAuth.IdentityLab.Tests/identity-lab.runsettings create mode 100644 tests/Directory.Build.props diff --git a/.docker/Dockerfile b/.docker/Dockerfile index d7011ee0..550d2a57 100644 --- a/.docker/Dockerfile +++ b/.docker/Dockerfile @@ -7,12 +7,12 @@ ENV CI=true COPY docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm/*.csproj CodeBeam.Website/ COPY docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/*.csproj CodeBeam.Website.Client/ -RUN dotnet restore CodeBeam.Website/CodeBeam.UltimateAuth.Docs.Wasm.csproj +RUN dotnet restore CodeBeam.Website/CodeBeam.UltimateAuth.Docs.Wasm.csproj -p:UltimateAuthBuildTargetFrameworks=net10.0 COPY . . RUN dotnet publish docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.csproj \ - -c Release -o /app/publish + -c Release -p:UltimateAuthBuildTargetFrameworks=net10.0 -o /app/publish # RUNTIME FROM mcr.microsoft.com/dotnet/aspnet:10.0 diff --git a/.docker/Dockerfile.identitylab b/.docker/Dockerfile.identitylab index 24974c06..b786705d 100644 --- a/.docker/Dockerfile.identitylab +++ b/.docker/Dockerfile.identitylab @@ -5,11 +5,13 @@ WORKDIR /src COPY . . RUN dotnet restore \ - apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/CodeBeam.UltimateAuth.IdentityLab.csproj + apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/CodeBeam.UltimateAuth.IdentityLab.csproj \ + -p:UltimateAuthBuildTargetFrameworks=net10.0 RUN dotnet publish \ apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/CodeBeam.UltimateAuth.IdentityLab.csproj \ -c Release \ + -p:UltimateAuthBuildTargetFrameworks=net10.0 \ --no-restore \ -o /app/publish diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 69e6789d..cebd62a9 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,13 +1,15 @@ -name: Deploy UltimateAuth +name: Docs Site on: + pull_request: push: branches: - master workflow_dispatch: concurrency: - group: ultimateauth-deploy + # PR updates may cancel their own validation, never a production deployment. + group: ${{ github.event_name == 'pull_request' && format('docs-site-pr-{0}', github.event.pull_request.number) || 'ultimateauth-deploy' }} cancel-in-progress: true permissions: @@ -15,35 +17,74 @@ permissions: packages: write jobs: - build-and-deploy: + changes: + name: Changes + if: github.event_name == 'pull_request' runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + outputs: + site: ${{ steps.filter.outputs.site }} + steps: + - name: Detect changed files + id: filter + uses: dorny/paths-filter@v3 + with: + filters: | + site: + - 'docs/**' + - 'src/utilities/CodeBeam.UltimateAuth.DocsBuilder/**' + - 'Directory.Build.props' + - 'Directory.Build.targets' + - 'Directory.Packages.props' + - 'global.json' + - 'NuGet.Config' + - 'nuget.config' + - '.dockerignore' + - '.docker/Dockerfile' + - '.docker/docker-compose.yml' + - '.github/workflows/deploy.yml' + build: + name: Docker Build + needs: changes + # Master and manual builds do not depend on a PR-only detector succeeding. + if: >- + !cancelled() && + (github.event_name != 'pull_request' || + (needs.changes.result == 'success' && needs.changes.outputs.site == 'true')) + runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - - name: Login to GitHub Container Registry + if: github.event_name != 'pull_request' && github.ref == 'refs/heads/master' uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Setup Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Build and push image uses: docker/build-push-action@v6 with: context: . file: .docker/Dockerfile - push: true + push: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/master' }} tags: | ghcr.io/codebeamorg/ultimateauth:master ghcr.io/codebeamorg/ultimateauth:${{ github.sha }} cache-from: type=gha cache-to: type=gha,mode=max + deploy: + name: Deploy + needs: build + if: github.event_name != 'pull_request' && github.ref == 'refs/heads/master' + runs-on: ubuntu-latest + steps: - name: Deploy to Server uses: appleboy/ssh-action@v1.0.3 with: diff --git a/.github/workflows/identitylab-image.yml b/.github/workflows/identitylab-image.yml index d0df3b0f..fe17a6f3 100644 --- a/.github/workflows/identitylab-image.yml +++ b/.github/workflows/identitylab-image.yml @@ -1,26 +1,10 @@ -name: Identity Lab Docker Image +name: Identity Lab on: pull_request: - paths: - - "apps/identity-lab/**" - - "src/**" - - "samples/**" - - ".docker/Dockerfile.identitylab" - - ".docker/docker-compose.identitylab.yml" - - ".github/workflows/identitylab-image.yml" - push: branches: - lab - paths: - - "apps/identity-lab/**" - - "src/**" - - "samples/**" - - ".docker/Dockerfile.identitylab" - - ".docker/docker-compose.identitylab.yml" - - ".github/workflows/identitylab-image.yml" - workflow_dispatch: permissions: @@ -28,20 +12,59 @@ permissions: packages: write concurrency: - group: identitylab-deploy + # PR validation cannot block or replace a production deployment run. + group: ${{ github.event_name == 'pull_request' && format('identitylab-pr-{0}', github.event.pull_request.number) || 'identitylab-deploy' }} cancel-in-progress: false jobs: - build: + changes: + name: Changes + if: github.event_name != 'workflow_dispatch' runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + outputs: + image: ${{ steps.filter.outputs.image }} + steps: + - name: Checkout for push comparison + if: github.event_name != 'pull_request' + uses: actions/checkout@v4 + - name: Detect changed files + id: filter + uses: dorny/paths-filter@v3 + with: + base: ${{ github.ref }} + filters: | + image: + - 'apps/identity-lab/**' + - 'src/**' + - 'samples/**' + - 'Directory.Build.props' + - 'Directory.Build.targets' + - 'Directory.Packages.props' + - 'global.json' + - 'NuGet.Config' + - 'nuget.config' + - '.dockerignore' + - '.docker/Dockerfile.identitylab' + - '.docker/docker-compose.identitylab.yml' + - '.github/workflows/identitylab-image.yml' + build: + name: Docker Build + needs: changes + # Manual image builds must also run when the PR/push detector is skipped. + if: >- + !cancelled() && + (github.event_name == 'workflow_dispatch' || + (needs.changes.result == 'success' && needs.changes.outputs.image == 'true')) + runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Log in to GHCR if: github.event_name != 'pull_request' uses: docker/login-action@v3 @@ -49,7 +72,6 @@ jobs: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Build and push Identity Lab uses: docker/build-push-action@v6 with: @@ -61,7 +83,7 @@ jobs: cache-to: type=gha,mode=max deploy: - name: Deploy Identity Lab + name: Deploy needs: build if: github.event_name != 'pull_request' && github.ref == 'refs/heads/lab' runs-on: ubuntu-latest diff --git a/.github/workflows/javascript-tests.yml b/.github/workflows/javascript-tests.yml index afe34b2b..d791c003 100644 --- a/.github/workflows/javascript-tests.yml +++ b/.github/workflows/javascript-tests.yml @@ -1,4 +1,4 @@ -name: JavaScript Client Tests +name: JS Client on: pull_request: @@ -11,40 +11,40 @@ permissions: contents: read jobs: - detect-changes: - name: Detect JavaScript Changes + changes: + name: Changes runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read outputs: javascript: ${{ steps.filter.outputs.javascript }} - steps: - - name: Checkout + - name: Checkout for push comparison + if: github.event_name != 'pull_request' uses: actions/checkout@v4 - - name: Detect changed files id: filter uses: dorny/paths-filter@v3 with: + base: ${{ github.ref }} filters: | javascript: - 'src/client/CodeBeam.UltimateAuth.Client.Blazor/TScripts/**' - 'tests/CodeBeam.UltimateAuth.Tests.Unit/javascript/**' - '.github/workflows/javascript-tests.yml' - javascript-tests: - name: Node.js Client Tests - needs: detect-changes - if: needs.detect-changes.outputs.javascript == 'true' + tests: + name: Tests + needs: changes + if: needs.changes.outputs.javascript == 'true' runs-on: ubuntu-latest - steps: - name: Checkout uses: actions/checkout@v4 - - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '22' - - name: Run JavaScript tests run: node --test tests/CodeBeam.UltimateAuth.Tests.Unit/javascript/*.test.cjs diff --git a/.github/workflows/nuget-publish.yml b/.github/workflows/nuget-publish.yml index f7822605..5bbdf1d2 100644 --- a/.github/workflows/nuget-publish.yml +++ b/.github/workflows/nuget-publish.yml @@ -24,13 +24,26 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Resolve supported SDKs + id: frameworks + env: + DOTNET_NOLOGO: '1' + shell: bash + run: | + frameworks=$(dotnet msbuild src/CodeBeam.UltimateAuth.Core/CodeBeam.UltimateAuth.Core.csproj -nologo -getProperty:UltimateAuthTargetFrameworks) + { + echo 'sdks<> "$GITHUB_OUTPUT" + - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: | - 8.0.x - 9.0.x - 10.0.x + dotnet-version: ${{ steps.frameworks.outputs.sdks }} - name: Verify package version shell: bash diff --git a/.github/workflows/ultimateauth-ci.yml b/.github/workflows/ultimateauth-ci.yml index 0b38ddf7..92d96423 100644 --- a/.github/workflows/ultimateauth-ci.yml +++ b/.github/workflows/ultimateauth-ci.yml @@ -1,4 +1,4 @@ -name: UltimateAuth CI +name: UAuth CI on: push: @@ -8,54 +8,185 @@ on: tags: - 'v*' pull_request: - branches: - - dev - - 'release/**' workflow_dispatch: jobs: - build-test-coverage: - name: Build, Test & Coverage + frameworks: + name: Framework Validation runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.policy.outputs.matrix }} + latest: ${{ steps.policy.outputs.latest }} + latest-display: ${{ steps.policy.outputs.latest-display }} + sdks: ${{ steps.policy.outputs.sdks }} + solution-frameworks: ${{ steps.policy.outputs.solution-frameworks }} + steps: + - uses: actions/checkout@v4 + - name: Audit projects and generate compatibility matrix + id: policy + env: + DOTNET_NOLOGO: '1' + shell: bash + run: | + python3 - <<'PYTHON' + import concurrent.futures + import json + import os + from pathlib import Path + import re + import subprocess + import xml.etree.ElementTree as ET + + + def properties(project, names): + return json.loads(subprocess.check_output( + ["dotnet", "msbuild", str(project), "-nologo", "-getProperty:" + ",".join(names)], + text=True))["Properties"] + + config = properties("src/CodeBeam.UltimateAuth.Core/CodeBeam.UltimateAuth.Core.csproj", + ["UltimateAuthTargetFrameworks", "UltimateAuthLatestTargetFramework"]) + frameworks = config["UltimateAuthTargetFrameworks"].split(";") + assert frameworks and all(re.fullmatch(r"net\d+\.\d+", f) for f in frameworks), "Invalid supported frameworks" + version = lambda f: tuple(map(int, f[3:].split("."))) + assert frameworks == sorted(set(frameworks), key=version), "Frameworks must be unique and ascending" + latest = config["UltimateAuthLatestTargetFramework"] + assert latest == max(frameworks, key=version), "Incorrect latest test target" + projects = sorted(p for base in ("src", "samples", "apps", "docs", "tests") + for p in Path(base).rglob("*.csproj") if not {"obj", "bin"}.intersection(p.parts)) + with concurrent.futures.ThreadPoolExecutor(max_workers=4) as pool: + metadata = dict(zip(projects, pool.map(lambda p: properties( + p, ["TargetFramework", "TargetFrameworks", "IsPackable"]), projects))) + targets = lambda info: (info["TargetFrameworks"] or info["TargetFramework"]).split(";") + solution = {p.attrib["Path"].replace("\\", "/") for p in ET.parse("UltimateAuth.slnx").iter("Project")} + assert {p.as_posix() for p in projects} <= solution, "Projects are missing from the full solution build" + expected = {p for p in projects if p.parts[0] == "src" and "utilities" not in p.parts + and p.parent.name != "CodeBeam.UltimateAuth.Client.JsMinifier"} + expected.add(Path("samples/CodeBeam.UltimateAuth.Sample.Seed/CodeBeam.UltimateAuth.Sample.Seed.csproj")) + packages = {p: info for p, info in metadata.items() if info["IsPackable"].lower() == "true"} + assert expected <= packages.keys(), "An expected library is missing or no longer packable" + for p in expected: + assert targets(packages[p]) == frameworks, f"Missing supported library target: {p}" + tests = [p for p in projects if p.parts[0] == "tests"] + assert tests, "No test projects discovered" + for p in tests: + assert targets(metadata[p]) == [latest], f"Tests must target only {latest}: {p}" + required = {latest} | {info["TargetFramework"] for info in metadata.values() if info["TargetFramework"]} + assert required <= set(frameworks), "Retarget single-target exceptions before dropping their library framework" + matrix = {"include": []} + for framework in frameworks: + included = [p.as_posix() for p, info in packages.items() if framework in targets(info)] + assert included, f"No packages selected for {framework}" + matrix["include"].append({"framework": framework, "display": framework[3:].removesuffix(".0"), + "sdk": framework[3:] + ".x", + "projects": "\n".join(included)}) + for p, info in packages.items(): + assert set(targets(info)) <= set(frameworks), f"Uncovered package framework: {p}" + with open(os.environ["GITHUB_OUTPUT"], "a") as output: + output.write("matrix=" + json.dumps(matrix, separators=(",", ":")) + "\n") + output.write("latest=" + latest + "\n") + output.write("latest-display=" + latest[3:].removesuffix(".0") + "\n") + output.write("solution-frameworks=" + "%3B".join(sorted(required, key=version)) + "\n") + output.write("sdks<- - dotnet test tests/CodeBeam.UltimateAuth.IdentityLab.Tests + dotnet test --project tests/CodeBeam.UltimateAuth.IdentityLab.Tests/CodeBeam.UltimateAuth.IdentityLab.Tests.csproj --configuration Release - --collect:"XPlat Code Coverage" - --logger "trx;LogFileName=identity-lab.trx" + --framework ${{ needs.frameworks.outputs.latest }} + -p:UltimateAuthBuildTargetFrameworks=${{ needs.frameworks.outputs.solution-frameworks }} + --coverlet + --coverlet-output-format cobertura + --coverlet-output-format json + --coverlet-include "[CodeBeam.UltimateAuth.IdentityLab]*" + --coverlet-exclude-by-file "**/Migrations/**/*.cs" + --report-trx + --report-trx-filename identity-lab.trx --results-directory TestResults/IdentityLab - --settings tests/CodeBeam.UltimateAuth.IdentityLab.Tests/identity-lab.runsettings - name: Generate Identity Lab coverage summary if: always() @@ -87,7 +224,7 @@ jobs: import xml.etree.ElementTree as ET reports = glob.glob( - "TestResults/IdentityLab/**/coverage.cobertura.xml", + "TestResults/IdentityLab/**/coverage.cobertura*.xml", recursive=True ) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 67cbcb91..130e1ec1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -99,3 +99,42 @@ Once the CLA process is implemented, contributors may be required to sign the ap Be respectful, assume good intentions, and help others learn. Different experience levels and perspectives make the project better. **Thank you for helping build UltimateAuth.** + +## Framework targeting and CI + +`UltimateAuthTargetFrameworks` in `Directory.Build.props` is the supported library +framework list. Keep it unique and in ascending version order. The last entry +becomes `UltimateAuthLatestTargetFramework`, which all five test projects inherit +from `tests/Directory.Build.props`. Tests, including rate-limiting compatibility +tests, run once on that latest framework; older frameworks receive package build +verification rather than repeated test runs. + +The 33 library projects under `src/` and the packable `Sample.Seed` project use +`UltimateAuthBuildTargetFrameworks`, which defaults to the supported list. CI can +select a supported build subset through that property without changing the +frameworks of single-target applications. `Sample.Shared.Hub` is a separate, +packable net10-only Razor library. Applications, other samples, docs, and the two +build tools retain their explicit net10 targets. Docker builds also restrict library +builds to their net10 application target, so adding a newer library framework +does not require upgrading those container SDKs. + +The CI policy job queries evaluated MSBuild properties for every project. It +checks solution membership, all expected library targets, and latest-only test +targets, then generates the compatibility matrix and an exact package/framework +table in the job summary. Each matrix job pins `global.json` **in its own checkout** +to the SDK installed for its framework, so a newer preinstalled SDK cannot hide +an older-SDK build failure. The committed `global.json` still selects MTP without +pinning an SDK. + +The test job retains the full solution build, but builds libraries only for the +latest framework and the frameworks needed by intentional single-target projects. +Identity Lab keeps its separate PostgreSQL job, TRX output, and assembly-scoped +coverage. The publishing workflow also derives its SDK list from the central +property. + +For example, changing the central list to `net10.0;net11.0` automatically generates +two SDK build jobs and moves all regular tests to net11. No workflow matrix edits +are needed. Framework-specific package references (notably Blazor, EF Core, and +TestHost) still need compatible versions when adding a new framework. Retarget +net10-only application/tool exceptions before eventually removing net10 itself; +CI rejects that omission rather than silently excluding projects. diff --git a/Directory.Build.props b/Directory.Build.props index 28108cd4..941fc4da 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,5 +1,11 @@ + + net8.0;net9.0;net10.0 + $([System.Text.RegularExpressions.Regex]::Match('$(UltimateAuthTargetFrameworks)', '[^;]+$').Value) + + $(UltimateAuthTargetFrameworks) + 0.2.0-preview.1 $(NoWarn);1591 diff --git a/README.md b/README.md index abaaeb25..a157e2c6 100644 --- a/README.md +++ b/README.md @@ -34,27 +34,40 @@ UltimateAuth is an open-source auth framework with platform-level capabilities t --- ## Roadmap -| Phase | Version | Scope | Status | Release Date | -| ----------------------- | ------------- | ----------------------------------------- | -------------- | ------------ | -| First Preview | 0.1.0-preview | "Stable" Preview Core | ✅ Completed | 07.04.2026 | +| Phase | Version | Scope | Status | Release Date | +| ----------------------- | ------------- | ----------------------------------------- | -------------- | ------------ | +| First Preview | 0.1.0-preview | Preview Core | ✅ Completed | 07.04.2026 | | First Release* | 0.1.0 | Documented & Quality Tested (Client Public API Locked) | ✅ Completed | 12.10.2026 | -| Product Expansion | 0.2.0 | Full Auth Modes, Rate Limiting | 🟡 In Progress | Q4 2026 | -| Security Expansion | 0.3.0 | MFA, Reauth | 🟡 In Progress | Q4 2026 | -| Infrastructure Expansion| 0.4.0 | Redis, Distributed Cache, Password Hasher | 🔜 Planned | Q1 2027 | -| Multi-Tenant Expansion | 0.5.0 | Multi tenant management | 🔜 Planned | Q1 2027 | -| Extensibility Expansion | 0.6.0 | Audit, events, hooks | 🔜 Planned | Q1 2027 | -| Performance Expansion | 0.7.0 | Benchmarks, caching | 🔜 Planned | Q1 2027 | -| Ecosystem Expansion | 0.8.0 | Migration tools | 🔜 Planned | Q2 2027 | -| v1.0 | 1.0.0 | Locked API, align with .NET 11 | 🔜 Planned | Q2 2027 | +| Product Expansion | 0.2.0 | Full Auth Modes, Rate Limiting | 🟡 In Progress | Q4 2026 | +| Security Expansion | 0.3.0 | MFA, Reauth | 🟡 In Progress | Q1 2027 | +| Infrastructure Expansion| 0.4.0 | Redis, Distributed Cache, Password Hashers | 🔜 Planned | Q1 2027 | +| Multi-Tenant Expansion | 0.5.0 | Multi tenant management | 🔜 Planned | Q2 2027 | +| Extensibility Expansion | 0.6.0 | Audit, events, hooks | 🔜 Planned | Q2 2027 | +| Performance Expansion | 0.7.0 | Benchmarks, caching | 🔜 Planned | Q3 2027 | +| Ecosystem Expansion | 0.8.0 | Migration tools | 🔜 Planned | Q3 2027 | +| v1.0 | 1.0.0 | Locked API, align with .NET 12 | 🔜 Planned | Q4 2027 | -*v 0.1.0 already provides a skeleton and extensibility points of multi tenancy, MFA, reauth etc. Expansion releases will enhance these areas. +> **UltimateAuth was architected from its first release with extensibility points and foundational support for advanced capabilities, including multi-tenancy, MFA, and reauthentication. Subsequent releases build upon this existing architecture, expanding implementations, improvements and production-ready functionality rather than introducing these concepts from scratch.* > The project roadmap is actively maintained as a GitHub issue: - -👉 https://github.com/CodeBeamOrg/UltimateAuth/issues/8 +> +> [![GitHub Roadmap](https://img.shields.io/badge/GitHub-View_Roadmap-181717?logo=github&logoColor=white&style=flat-square)](https://github.com/CodeBeamOrg/UltimateAuth/issues/8) We keep it up-to-date with current priorities, planned features, and progress. Feel free to follow, comment, or contribute ideas. +### Project Milestones + +UltimateAuth is designed to deliver a simple, consistent developer experience from its very first release. Each milestone expands its capabilities and supported scenarios, rather than introducing usability or architectural maturity from scratch. + +| Milestone | Version | Target | +|---|---|---| +| **Developer Foundation** | 0.1.x | A functional, security-conscious authentication framework with a complete core architecture, extensibility, and a developer-first API. Suitable for development, early adoption, and production use following application-specific evaluation. | +| **Production Expansion** | 0.2.0 | Ready to power real-world applications, particularly small and medium-sized products, with expanded authentication modes, built-in rate limiting, and a proven integration experience. Larger applications can also adopt UltimateAuth when its available capabilities meet their requirements. | +| **Advanced Security & Scale** | 0.4.0 | Extends production capabilities with MFA, reauthentication, distributed infrastructure, and additional security options for applications with more demanding requirements. Builds on the same simple developer experience. | +| **Migration & Adoption** | 0.8.0 | Makes adopting UltimateAuth in existing applications easier through migration tools, guidance, and established integration patterns, without requiring new applications to wait for this milestone. | + +**The UltimateAuth philosophy:** Simple from day one, more capable with every release. Version milestones describe expanding capabilities — not a progression from difficult to easy, or from unusable to usable. + --- ### 💜 Join the UltimateAuth Community @@ -347,7 +360,7 @@ Create test accounts, manage identities and sessions, explore roles and permissi **Powered by UltimateAuth, not simulated.** The demonstrated identity capabilities use the framework itself, so you can see what UltimateAuth can do before integrating it into your own application. -🧪 **Identity Lab — Interactive Sandbox** *(Link coming soon)* +🧪 **[Identity Lab](https://lab.ultimateauth.com)** --- diff --git a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/CodeBeam.UltimateAuth.IdentityLab.csproj b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/CodeBeam.UltimateAuth.IdentityLab.csproj index 5987c73f..de41bbeb 100644 --- a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/CodeBeam.UltimateAuth.IdentityLab.csproj +++ b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/CodeBeam.UltimateAuth.IdentityLab.csproj @@ -10,14 +10,14 @@ - - + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/Dashboard/MyAccessPage.razor b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/Dashboard/MyAccessPage.razor index 2f27fb8b..5a23e614 100644 --- a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/Dashboard/MyAccessPage.razor +++ b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/Dashboard/MyAccessPage.razor @@ -211,7 +211,7 @@ @foreach (var group in Scenarios.GroupBy(x => x.Group)) { - + @foreach (var scenario in group) { diff --git a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/CodeBeam.UltimateAuth.Docs.Wasm.Client.csproj b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/CodeBeam.UltimateAuth.Docs.Wasm.Client.csproj index 69a55d41..a4e8ea48 100644 --- a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/CodeBeam.UltimateAuth.Docs.Wasm.Client.csproj +++ b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/CodeBeam.UltimateAuth.Docs.Wasm.Client.csproj @@ -11,10 +11,10 @@ - + - - + + diff --git a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/Layout/MainLayout.razor b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/Layout/MainLayout.razor index 852c35a8..8b993cce 100644 --- a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/Layout/MainLayout.razor +++ b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/Layout/MainLayout.razor @@ -110,7 +110,7 @@ - © @DateTime.Now.Year CodeBeam. + © @DateTime.Now.Year CodeBeam. diff --git a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/Pages/Samples.razor b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/Pages/Samples.razor index e69e4bc9..668e5ba0 100644 --- a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/Pages/Samples.razor +++ b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/Pages/Samples.razor @@ -5,7 +5,7 @@ Samples Explore working examples and see UltimateAuth in action. - Identity Lab (hosted live demo) is coming soon. + Identity Lab (live sandbox) is also available. diff --git a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.csproj b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.csproj index be947b19..d84cbca3 100644 --- a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.csproj +++ b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.csproj @@ -10,7 +10,7 @@ - + diff --git a/global.json b/global.json new file mode 100644 index 00000000..3140116d --- /dev/null +++ b/global.json @@ -0,0 +1,5 @@ +{ + "test": { + "runner": "Microsoft.Testing.Platform" + } +} diff --git a/samples/CodeBeam.UltimateAuth.Sample.Seed/CodeBeam.UltimateAuth.Sample.Seed.csproj b/samples/CodeBeam.UltimateAuth.Sample.Seed/CodeBeam.UltimateAuth.Sample.Seed.csproj index cc80e9e8..d961b7f3 100644 --- a/samples/CodeBeam.UltimateAuth.Sample.Seed/CodeBeam.UltimateAuth.Sample.Seed.csproj +++ b/samples/CodeBeam.UltimateAuth.Sample.Seed/CodeBeam.UltimateAuth.Sample.Seed.csproj @@ -1,7 +1,7 @@  - net8.0;net9.0;net10.0 + $(UltimateAuthBuildTargetFrameworks) $(NoWarn);1591 CodeBeam.UltimateAuth.Sample.Seed diff --git a/samples/CodeBeam.UltimateAuth.Sample.Shared.Hub/CodeBeam.UltimateAuth.Sample.Shared.Hub.csproj b/samples/CodeBeam.UltimateAuth.Sample.Shared.Hub/CodeBeam.UltimateAuth.Sample.Shared.Hub.csproj index 5aa2b7e0..967a1f52 100644 --- a/samples/CodeBeam.UltimateAuth.Sample.Shared.Hub/CodeBeam.UltimateAuth.Sample.Shared.Hub.csproj +++ b/samples/CodeBeam.UltimateAuth.Sample.Shared.Hub/CodeBeam.UltimateAuth.Sample.Shared.Hub.csproj @@ -1,9 +1,10 @@ - + net10.0 enable enable + false @@ -12,7 +13,7 @@ - + diff --git a/samples/CodeBeam.UltimateAuth.Sample.Shared/CodeBeam.UltimateAuth.Sample.Shared.csproj b/samples/CodeBeam.UltimateAuth.Sample.Shared/CodeBeam.UltimateAuth.Sample.Shared.csproj index f1d71e85..b1b27775 100644 --- a/samples/CodeBeam.UltimateAuth.Sample.Shared/CodeBeam.UltimateAuth.Sample.Shared.csproj +++ b/samples/CodeBeam.UltimateAuth.Sample.Shared/CodeBeam.UltimateAuth.Sample.Shared.csproj @@ -1,4 +1,4 @@ - + net10.0 @@ -13,12 +13,12 @@ - - - - - - + + + + + + diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore.csproj b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore.csproj index afc22ae3..c82d7e9a 100644 --- a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore.csproj +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore.csproj @@ -9,17 +9,17 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.csproj b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.csproj index 2012f030..6106524c 100644 --- a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.csproj +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.csproj @@ -9,8 +9,8 @@ - - + + diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.csproj b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.csproj index 9f0beb00..e15aaf63 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.csproj +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.csproj @@ -9,17 +9,17 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/CodeBeam.UltimateAuth.Sample.BlazorServer.csproj b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/CodeBeam.UltimateAuth.Sample.BlazorServer.csproj index cfe21912..a582eeab 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/CodeBeam.UltimateAuth.Sample.BlazorServer.csproj +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/CodeBeam.UltimateAuth.Sample.BlazorServer.csproj @@ -8,8 +8,8 @@ - - + + diff --git a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm.csproj b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm.csproj index 1ef3e632..07ea416a 100644 --- a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm.csproj +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm.csproj @@ -8,10 +8,10 @@ - - - - + + + + diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/CodeBeam.UAuth.Sample.IntWasm.Client.csproj b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/CodeBeam.UAuth.Sample.IntWasm.Client.csproj index 1dafa9a8..321b6185 100644 --- a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/CodeBeam.UAuth.Sample.IntWasm.Client.csproj +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.Client/CodeBeam.UAuth.Sample.IntWasm.Client.csproj @@ -11,8 +11,8 @@ - - + + diff --git a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.csproj b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.csproj index 2154aec8..2afcbaee 100644 --- a/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.csproj +++ b/samples/int-wasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm/CodeBeam.UAuth.Sample.IntWasm.csproj @@ -11,7 +11,7 @@ - + diff --git a/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore.csproj b/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore.csproj index cde66c01..3d660c31 100644 --- a/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore.csproj +++ b/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore/CodeBeam.UltimateAuth.Sample.ResourceApi.EfCore.csproj @@ -8,7 +8,7 @@ - + diff --git a/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi/CodeBeam.UltimateAuth.Sample.ResourceApi.csproj b/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi/CodeBeam.UltimateAuth.Sample.ResourceApi.csproj index cde66c01..3d660c31 100644 --- a/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi/CodeBeam.UltimateAuth.Sample.ResourceApi.csproj +++ b/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi/CodeBeam.UltimateAuth.Sample.ResourceApi.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/CodeBeam.UltimateAuth.Core/CodeBeam.UltimateAuth.Core.csproj b/src/CodeBeam.UltimateAuth.Core/CodeBeam.UltimateAuth.Core.csproj index c59475f7..f34b9a2a 100644 --- a/src/CodeBeam.UltimateAuth.Core/CodeBeam.UltimateAuth.Core.csproj +++ b/src/CodeBeam.UltimateAuth.Core/CodeBeam.UltimateAuth.Core.csproj @@ -1,7 +1,7 @@  - net8.0;net9.0;net10.0 + $(UltimateAuthBuildTargetFrameworks) $(NoWarn);1591 CodeBeam.UltimateAuth.Core diff --git a/src/CodeBeam.UltimateAuth.Server/CodeBeam.UltimateAuth.Server.csproj b/src/CodeBeam.UltimateAuth.Server/CodeBeam.UltimateAuth.Server.csproj index 6f21fab2..6ece6b73 100644 --- a/src/CodeBeam.UltimateAuth.Server/CodeBeam.UltimateAuth.Server.csproj +++ b/src/CodeBeam.UltimateAuth.Server/CodeBeam.UltimateAuth.Server.csproj @@ -1,7 +1,7 @@  - net8.0;net9.0;net10.0 + $(UltimateAuthBuildTargetFrameworks) $(NoWarn);1591 CodeBeam.UltimateAuth.Server @@ -22,7 +22,7 @@ - + diff --git a/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore.csproj b/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore.csproj index 4437d969..ceab34cf 100644 --- a/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore.csproj +++ b/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore.csproj @@ -1,7 +1,7 @@  - net8.0;net9.0;net10.0 + $(UltimateAuthBuildTargetFrameworks) $(NoWarn);1591 CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore diff --git a/src/authentication/CodeBeam.UltimateAuth.Authentication.InMemory/CodeBeam.UltimateAuth.Authentication.InMemory.csproj b/src/authentication/CodeBeam.UltimateAuth.Authentication.InMemory/CodeBeam.UltimateAuth.Authentication.InMemory.csproj index c47f0f33..59cd70f1 100644 --- a/src/authentication/CodeBeam.UltimateAuth.Authentication.InMemory/CodeBeam.UltimateAuth.Authentication.InMemory.csproj +++ b/src/authentication/CodeBeam.UltimateAuth.Authentication.InMemory/CodeBeam.UltimateAuth.Authentication.InMemory.csproj @@ -1,7 +1,7 @@  - net8.0;net9.0;net10.0 + $(UltimateAuthBuildTargetFrameworks) $(NoWarn);1591 CodeBeam.UltimateAuth.Authentication.InMemory diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.Contracts/CodeBeam.UltimateAuth.Authorization.Contracts.csproj b/src/authorization/CodeBeam.UltimateAuth.Authorization.Contracts/CodeBeam.UltimateAuth.Authorization.Contracts.csproj index 46f73334..7911a019 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.Contracts/CodeBeam.UltimateAuth.Authorization.Contracts.csproj +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.Contracts/CodeBeam.UltimateAuth.Authorization.Contracts.csproj @@ -1,7 +1,7 @@  - net8.0;net9.0;net10.0 + $(UltimateAuthBuildTargetFrameworks) $(NoWarn);1591 CodeBeam.UltimateAuth.Authorization.Contracts diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.csproj b/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.csproj index 76cfca96..f85a3966 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.csproj +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.csproj @@ -1,7 +1,7 @@  - net8.0;net9.0;net10.0 + $(UltimateAuthBuildTargetFrameworks) $(NoWarn);1591 CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/CodeBeam.UltimateAuth.Authorization.InMemory.csproj b/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/CodeBeam.UltimateAuth.Authorization.InMemory.csproj index a7723fcd..627076aa 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/CodeBeam.UltimateAuth.Authorization.InMemory.csproj +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/CodeBeam.UltimateAuth.Authorization.InMemory.csproj @@ -1,7 +1,7 @@  - net8.0;net9.0;net10.0 + $(UltimateAuthBuildTargetFrameworks) $(NoWarn);1591 CodeBeam.UltimateAuth.Authorization.InMemory diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/CodeBeam.UltimateAuth.Authorization.Reference.csproj b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/CodeBeam.UltimateAuth.Authorization.Reference.csproj index 82976ca3..38c3e64f 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/CodeBeam.UltimateAuth.Authorization.Reference.csproj +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.Reference/CodeBeam.UltimateAuth.Authorization.Reference.csproj @@ -1,7 +1,7 @@  - net8.0;net9.0;net10.0 + $(UltimateAuthBuildTargetFrameworks) $(NoWarn);1591 CodeBeam.UltimateAuth.Authorization.Reference diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization/CodeBeam.UltimateAuth.Authorization.csproj b/src/authorization/CodeBeam.UltimateAuth.Authorization/CodeBeam.UltimateAuth.Authorization.csproj index 00dad5a1..d83ec60d 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization/CodeBeam.UltimateAuth.Authorization.csproj +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization/CodeBeam.UltimateAuth.Authorization.csproj @@ -1,7 +1,7 @@  - net8.0;net9.0;net10.0 + $(UltimateAuthBuildTargetFrameworks) $(NoWarn);1591 CodeBeam.UltimateAuth.Authorization diff --git a/src/bundle/CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle/CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle.csproj b/src/bundle/CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle/CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle.csproj index 17220e06..9926c359 100644 --- a/src/bundle/CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle/CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle.csproj +++ b/src/bundle/CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle/CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle.csproj @@ -1,7 +1,7 @@  - net8.0;net9.0;net10.0 + $(UltimateAuthBuildTargetFrameworks) $(NoWarn);1591 CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle diff --git a/src/bundle/CodeBeam.UltimateAuth.InMemory.Bundle/CodeBeam.UltimateAuth.InMemory.Bundle.csproj b/src/bundle/CodeBeam.UltimateAuth.InMemory.Bundle/CodeBeam.UltimateAuth.InMemory.Bundle.csproj index c0c22ad8..a64f2137 100644 --- a/src/bundle/CodeBeam.UltimateAuth.InMemory.Bundle/CodeBeam.UltimateAuth.InMemory.Bundle.csproj +++ b/src/bundle/CodeBeam.UltimateAuth.InMemory.Bundle/CodeBeam.UltimateAuth.InMemory.Bundle.csproj @@ -1,7 +1,7 @@  - net8.0;net9.0;net10.0 + $(UltimateAuthBuildTargetFrameworks) $(NoWarn);1591 CodeBeam.UltimateAuth.InMemory.Bundle diff --git a/src/bundle/CodeBeam.UltimateAuth.Reference.Bundle/CodeBeam.UltimateAuth.Reference.Bundle.csproj b/src/bundle/CodeBeam.UltimateAuth.Reference.Bundle/CodeBeam.UltimateAuth.Reference.Bundle.csproj index 01f6356a..48919bb1 100644 --- a/src/bundle/CodeBeam.UltimateAuth.Reference.Bundle/CodeBeam.UltimateAuth.Reference.Bundle.csproj +++ b/src/bundle/CodeBeam.UltimateAuth.Reference.Bundle/CodeBeam.UltimateAuth.Reference.Bundle.csproj @@ -1,7 +1,7 @@  - net8.0;net9.0;net10.0 + $(UltimateAuthBuildTargetFrameworks) $(NoWarn);1591 CodeBeam.UltimateAuth.Reference.Bundle diff --git a/src/client/CodeBeam.UltimateAuth.Client.AspNetCore/CodeBeam.UltimateAuth.Client.AspNetCore.csproj b/src/client/CodeBeam.UltimateAuth.Client.AspNetCore/CodeBeam.UltimateAuth.Client.AspNetCore.csproj index a6b3ab90..fcb04456 100644 --- a/src/client/CodeBeam.UltimateAuth.Client.AspNetCore/CodeBeam.UltimateAuth.Client.AspNetCore.csproj +++ b/src/client/CodeBeam.UltimateAuth.Client.AspNetCore/CodeBeam.UltimateAuth.Client.AspNetCore.csproj @@ -1,7 +1,7 @@  - net8.0;net9.0;net10.0 + $(UltimateAuthBuildTargetFrameworks) $(NoWarn);1591 CodeBeam.UltimateAuth.Client.AspNetCore diff --git a/src/client/CodeBeam.UltimateAuth.Client.Blazor/CodeBeam.UltimateAuth.Client.Blazor.csproj b/src/client/CodeBeam.UltimateAuth.Client.Blazor/CodeBeam.UltimateAuth.Client.Blazor.csproj index 73d5c800..a2e8969e 100644 --- a/src/client/CodeBeam.UltimateAuth.Client.Blazor/CodeBeam.UltimateAuth.Client.Blazor.csproj +++ b/src/client/CodeBeam.UltimateAuth.Client.Blazor/CodeBeam.UltimateAuth.Client.Blazor.csproj @@ -1,7 +1,7 @@  - net8.0;net9.0;net10.0 + $(UltimateAuthBuildTargetFrameworks) CodeBeam.UltimateAuth.Client.Blazor @@ -16,25 +16,25 @@ - - - - + + + + - - - - + + + + - - - - + + + + - + diff --git a/src/client/CodeBeam.UltimateAuth.Client.JsMinifier/CodeBeam.UltimateAuth.Client.JsMinifier.csproj b/src/client/CodeBeam.UltimateAuth.Client.JsMinifier/CodeBeam.UltimateAuth.Client.JsMinifier.csproj index cdd30716..bc6e3e8f 100644 --- a/src/client/CodeBeam.UltimateAuth.Client.JsMinifier/CodeBeam.UltimateAuth.Client.JsMinifier.csproj +++ b/src/client/CodeBeam.UltimateAuth.Client.JsMinifier/CodeBeam.UltimateAuth.Client.JsMinifier.csproj @@ -7,7 +7,7 @@ - + diff --git a/src/client/CodeBeam.UltimateAuth.Client/CodeBeam.UltimateAuth.Client.csproj b/src/client/CodeBeam.UltimateAuth.Client/CodeBeam.UltimateAuth.Client.csproj index f7f329c1..7f13e4e3 100644 --- a/src/client/CodeBeam.UltimateAuth.Client/CodeBeam.UltimateAuth.Client.csproj +++ b/src/client/CodeBeam.UltimateAuth.Client/CodeBeam.UltimateAuth.Client.csproj @@ -1,7 +1,7 @@  - net8.0;net9.0;net10.0 + $(UltimateAuthBuildTargetFrameworks) CodeBeam.UltimateAuth.Client diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/CodeBeam.UltimateAuth.Credentials.Contracts.csproj b/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/CodeBeam.UltimateAuth.Credentials.Contracts.csproj index 376e6b7a..1186eb80 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/CodeBeam.UltimateAuth.Credentials.Contracts.csproj +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.Contracts/CodeBeam.UltimateAuth.Credentials.Contracts.csproj @@ -1,7 +1,7 @@  - net8.0;net9.0;net10.0 + $(UltimateAuthBuildTargetFrameworks) $(NoWarn);1591 CodeBeam.UltimateAuth.Credentials.Contracts diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore.csproj b/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore.csproj index 978ce78c..92f06217 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore.csproj +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore.csproj @@ -1,7 +1,7 @@  - net8.0;net9.0;net10.0 + $(UltimateAuthBuildTargetFrameworks) $(NoWarn);1591 CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/CodeBeam.UltimateAuth.Credentials.InMemory.csproj b/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/CodeBeam.UltimateAuth.Credentials.InMemory.csproj index 7bc21c6a..0351770a 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/CodeBeam.UltimateAuth.Credentials.InMemory.csproj +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/CodeBeam.UltimateAuth.Credentials.InMemory.csproj @@ -1,7 +1,7 @@  - net8.0;net9.0;net10.0 + $(UltimateAuthBuildTargetFrameworks) $(NoWarn);1591 CodeBeam.UltimateAuth.Credentials.InMemory diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/CodeBeam.UltimateAuth.Credentials.Reference.csproj b/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/CodeBeam.UltimateAuth.Credentials.Reference.csproj index ec99e9ab..2656c92f 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/CodeBeam.UltimateAuth.Credentials.Reference.csproj +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.Reference/CodeBeam.UltimateAuth.Credentials.Reference.csproj @@ -1,7 +1,7 @@  - net8.0;net9.0;net10.0 + $(UltimateAuthBuildTargetFrameworks) $(NoWarn);1591 CodeBeam.UltimateAuth.Credentials.Reference diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials/CodeBeam.UltimateAuth.Credentials.csproj b/src/credentials/CodeBeam.UltimateAuth.Credentials/CodeBeam.UltimateAuth.Credentials.csproj index 53803f40..83ac2e7a 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials/CodeBeam.UltimateAuth.Credentials.csproj +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials/CodeBeam.UltimateAuth.Credentials.csproj @@ -1,7 +1,7 @@  - net8.0;net9.0;net10.0 + $(UltimateAuthBuildTargetFrameworks) $(NoWarn);1591 CodeBeam.UltimateAuth.Credentials diff --git a/src/persistence/CodeBeam.UltimateAuth.EntityFrameworkCore/CodeBeam.UltimateAuth.EntityFrameworkCore.csproj b/src/persistence/CodeBeam.UltimateAuth.EntityFrameworkCore/CodeBeam.UltimateAuth.EntityFrameworkCore.csproj index 11ada993..cf84ac1b 100644 --- a/src/persistence/CodeBeam.UltimateAuth.EntityFrameworkCore/CodeBeam.UltimateAuth.EntityFrameworkCore.csproj +++ b/src/persistence/CodeBeam.UltimateAuth.EntityFrameworkCore/CodeBeam.UltimateAuth.EntityFrameworkCore.csproj @@ -1,7 +1,7 @@  - net8.0;net9.0;net10.0 + $(UltimateAuthBuildTargetFrameworks) $(NoWarn);1591 CodeBeam.UltimateAuth.EntityFrameworkCore @@ -18,16 +18,16 @@ - - + + - - + + - - + + diff --git a/src/persistence/CodeBeam.UltimateAuth.InMemory/CodeBeam.UltimateAuth.InMemory.csproj b/src/persistence/CodeBeam.UltimateAuth.InMemory/CodeBeam.UltimateAuth.InMemory.csproj index ca100aa8..8ef46540 100644 --- a/src/persistence/CodeBeam.UltimateAuth.InMemory/CodeBeam.UltimateAuth.InMemory.csproj +++ b/src/persistence/CodeBeam.UltimateAuth.InMemory/CodeBeam.UltimateAuth.InMemory.csproj @@ -1,7 +1,7 @@  - net8.0;net9.0;net10.0 + $(UltimateAuthBuildTargetFrameworks) $(NoWarn);1591 CodeBeam.UltimateAuth.InMemory diff --git a/src/policies/CodeBeam.UltimateAuth.Policies/CodeBeam.UltimateAuth.Policies.csproj b/src/policies/CodeBeam.UltimateAuth.Policies/CodeBeam.UltimateAuth.Policies.csproj index 6f703658..13c2f88f 100644 --- a/src/policies/CodeBeam.UltimateAuth.Policies/CodeBeam.UltimateAuth.Policies.csproj +++ b/src/policies/CodeBeam.UltimateAuth.Policies/CodeBeam.UltimateAuth.Policies.csproj @@ -1,7 +1,7 @@  - net8.0;net9.0;net10.0 + $(UltimateAuthBuildTargetFrameworks) $(NoWarn);1591 CodeBeam.UltimateAuth.Policies diff --git a/src/security/CodeBeam.UltimateAuth.Security.Argon2/CodeBeam.UltimateAuth.Security.Argon2.csproj b/src/security/CodeBeam.UltimateAuth.Security.Argon2/CodeBeam.UltimateAuth.Security.Argon2.csproj index 1ed21519..d7b51946 100644 --- a/src/security/CodeBeam.UltimateAuth.Security.Argon2/CodeBeam.UltimateAuth.Security.Argon2.csproj +++ b/src/security/CodeBeam.UltimateAuth.Security.Argon2/CodeBeam.UltimateAuth.Security.Argon2.csproj @@ -1,7 +1,7 @@  - net8.0;net9.0;net10.0 + $(UltimateAuthBuildTargetFrameworks) $(NoWarn);1591 CodeBeam.UltimateAuth.Security.Argon2 diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.csproj b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.csproj index 345afa17..69308198 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.csproj +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.csproj @@ -1,7 +1,7 @@  - net8.0;net9.0;net10.0 + $(UltimateAuthBuildTargetFrameworks) $(NoWarn);1591 CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/CodeBeam.UltimateAuth.Sessions.InMemory.csproj b/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/CodeBeam.UltimateAuth.Sessions.InMemory.csproj index 09a0ea22..e2a8e517 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/CodeBeam.UltimateAuth.Sessions.InMemory.csproj +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/CodeBeam.UltimateAuth.Sessions.InMemory.csproj @@ -1,7 +1,7 @@  - net8.0;net9.0;net10.0 + $(UltimateAuthBuildTargetFrameworks) $(NoWarn);1591 CodeBeam.UltimateAuth.Sessions.InMemory diff --git a/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore.csproj b/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore.csproj index 6d1c5e19..cb3f075e 100644 --- a/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore.csproj +++ b/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore.csproj @@ -1,7 +1,7 @@  - net8.0;net9.0;net10.0 + $(UltimateAuthBuildTargetFrameworks) $(NoWarn);1591 CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore diff --git a/src/tokens/CodeBeam.UltimateAuth.Tokens.InMemory/CodeBeam.UltimateAuth.Tokens.InMemory.csproj b/src/tokens/CodeBeam.UltimateAuth.Tokens.InMemory/CodeBeam.UltimateAuth.Tokens.InMemory.csproj index 4be63678..708c17cd 100644 --- a/src/tokens/CodeBeam.UltimateAuth.Tokens.InMemory/CodeBeam.UltimateAuth.Tokens.InMemory.csproj +++ b/src/tokens/CodeBeam.UltimateAuth.Tokens.InMemory/CodeBeam.UltimateAuth.Tokens.InMemory.csproj @@ -1,7 +1,7 @@  - net8.0;net9.0;net10.0 + $(UltimateAuthBuildTargetFrameworks) $(NoWarn);1591 CodeBeam.UltimateAuth.Tokens.InMemory diff --git a/src/users/CodeBeam.UltimateAuth.Users.Contracts/CodeBeam.UltimateAuth.Users.Contracts.csproj b/src/users/CodeBeam.UltimateAuth.Users.Contracts/CodeBeam.UltimateAuth.Users.Contracts.csproj index 8ff8aa91..3bcc4e6e 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Contracts/CodeBeam.UltimateAuth.Users.Contracts.csproj +++ b/src/users/CodeBeam.UltimateAuth.Users.Contracts/CodeBeam.UltimateAuth.Users.Contracts.csproj @@ -1,7 +1,7 @@  - net8.0;net9.0;net10.0 + $(UltimateAuthBuildTargetFrameworks) $(NoWarn);1591 CodeBeam.UltimateAuth.Users.Contracts diff --git a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/CodeBeam.UltimateAuth.Users.EntityFrameworkCore.csproj b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/CodeBeam.UltimateAuth.Users.EntityFrameworkCore.csproj index ab0912de..6ffa4e97 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/CodeBeam.UltimateAuth.Users.EntityFrameworkCore.csproj +++ b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/CodeBeam.UltimateAuth.Users.EntityFrameworkCore.csproj @@ -1,7 +1,7 @@  - net8.0;net9.0;net10.0 + $(UltimateAuthBuildTargetFrameworks) $(NoWarn);1591 CodeBeam.UltimateAuth.Users.EntityFrameworkCore diff --git a/src/users/CodeBeam.UltimateAuth.Users.InMemory/CodeBeam.UltimateAuth.Users.InMemory.csproj b/src/users/CodeBeam.UltimateAuth.Users.InMemory/CodeBeam.UltimateAuth.Users.InMemory.csproj index 0d7ed5fb..8581ea8a 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.InMemory/CodeBeam.UltimateAuth.Users.InMemory.csproj +++ b/src/users/CodeBeam.UltimateAuth.Users.InMemory/CodeBeam.UltimateAuth.Users.InMemory.csproj @@ -1,7 +1,7 @@  - net8.0;net9.0;net10.0 + $(UltimateAuthBuildTargetFrameworks) $(NoWarn);1591 CodeBeam.UltimateAuth.Users.InMemory diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/CodeBeam.UltimateAuth.Users.Reference.csproj b/src/users/CodeBeam.UltimateAuth.Users.Reference/CodeBeam.UltimateAuth.Users.Reference.csproj index 3b4b5061..db838103 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Reference/CodeBeam.UltimateAuth.Users.Reference.csproj +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/CodeBeam.UltimateAuth.Users.Reference.csproj @@ -1,7 +1,7 @@  - net8.0;net9.0;net10.0 + $(UltimateAuthBuildTargetFrameworks) $(NoWarn);1591 CodeBeam.UltimateAuth.Users.Reference diff --git a/src/users/CodeBeam.UltimateAuth.Users/CodeBeam.UltimateAuth.Users.csproj b/src/users/CodeBeam.UltimateAuth.Users/CodeBeam.UltimateAuth.Users.csproj index 9dd91208..4a454f08 100644 --- a/src/users/CodeBeam.UltimateAuth.Users/CodeBeam.UltimateAuth.Users.csproj +++ b/src/users/CodeBeam.UltimateAuth.Users/CodeBeam.UltimateAuth.Users.csproj @@ -1,7 +1,7 @@  - net8.0;net9.0;net10.0 + $(UltimateAuthBuildTargetFrameworks) $(NoWarn);1591 CodeBeam.UltimateAuth.Users diff --git a/src/utilities/CodeBeam.UltimateAuth.DocsBuilder/CodeBeam.UltimateAuth.DocsBuilder.csproj b/src/utilities/CodeBeam.UltimateAuth.DocsBuilder/CodeBeam.UltimateAuth.DocsBuilder.csproj index cdce659b..bcef26b9 100644 --- a/src/utilities/CodeBeam.UltimateAuth.DocsBuilder/CodeBeam.UltimateAuth.DocsBuilder.csproj +++ b/src/utilities/CodeBeam.UltimateAuth.DocsBuilder/CodeBeam.UltimateAuth.DocsBuilder.csproj @@ -9,8 +9,8 @@ - - + + diff --git a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/AssemblyBehavior.cs b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/AssemblyBehavior.cs index c7fc3b1f..0378b5a6 100644 --- a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/AssemblyBehavior.cs +++ b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/AssemblyBehavior.cs @@ -1 +1,3 @@ -[assembly: CollectionBehavior(DisableTestParallelization = true)] +using Xunit.v3; + +[assembly: Parallelization(Mode = Xunit.Sdk.ParallelMode.None)] diff --git a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Authorization/SandboxPolicyTests.cs b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Authorization/SandboxPolicyTests.cs index 818b4c05..91aef379 100644 --- a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Authorization/SandboxPolicyTests.cs +++ b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Authorization/SandboxPolicyTests.cs @@ -26,11 +26,11 @@ public async Task Owner_cannot_access_foreign_or_missing_targets(string action) foreach (var target in new UserKey?[] { LabDatabase.MemberB, LabDatabase.OwnerB, UserKey.FromString("nonexistent"), null }) { - var denied = await policy.DecideAsync(Contexts.Create(action, LabDatabase.OwnerA, target)); + var denied = await policy.DecideAsync(Contexts.Create(action, LabDatabase.OwnerA, target), TestContext.Current.CancellationToken); Assert.True(denied.IsDenied); } Assert.True((await policy.DecideAsync(Contexts.Create(action, - LabDatabase.OwnerA, LabDatabase.MemberA))).IsAllowed); + LabDatabase.OwnerA, LabDatabase.MemberA), TestContext.Current.CancellationToken)).IsAllowed); Assert.Equal(before, await db.SnapshotAsync()); } @@ -44,9 +44,9 @@ public async Task Inactive_sandbox_blocks_writes_and_suspension_also_blocks_read await db.SetStatusAsync(status); var policy = new SandboxUAuthAccessPolicy(db, new LabRootResolver(), new()); Assert.True((await policy.DecideAsync(Contexts.Create(UAuthActions.UserProfiles.UpdateAdmin, - LabDatabase.OwnerA, LabDatabase.MemberA))).IsDenied); + LabDatabase.OwnerA, LabDatabase.MemberA), TestContext.Current.CancellationToken)).IsDenied); Assert.Equal(readDenied, (await policy.DecideAsync(Contexts.Create(UAuthActions.UserProfiles.GetAdmin, - LabDatabase.OwnerA, LabDatabase.MemberA))).IsDenied); + LabDatabase.OwnerA, LabDatabase.MemberA), TestContext.Current.CancellationToken)).IsDenied); } [Fact] @@ -56,11 +56,11 @@ public async Task Members_cannot_manage_users_and_owners_cannot_be_deleted() await db.SeedAsync(); var policy = new SandboxUAuthAccessPolicy(db, new LabRootResolver(), new()); Assert.Equal("sandbox_owner_required", (await policy.DecideAsync(Contexts.Create( - UAuthActions.Users.DeleteAdmin, LabDatabase.MemberA, LabDatabase.OwnerA))).DenyReason); + UAuthActions.Users.DeleteAdmin, LabDatabase.MemberA, LabDatabase.OwnerA), TestContext.Current.CancellationToken)).DenyReason); Assert.Equal("sandbox_owner_cannot_be_deleted", (await policy.DecideAsync(Contexts.Create( - UAuthActions.Users.DeleteAdmin, LabDatabase.OwnerA, LabDatabase.OwnerA))).DenyReason); + UAuthActions.Users.DeleteAdmin, LabDatabase.OwnerA, LabDatabase.OwnerA), TestContext.Current.CancellationToken)).DenyReason); Assert.Equal("sandbox_owner_cannot_delete_self", (await policy.DecideAsync(Contexts.Create( - UAuthActions.Users.DeleteSelf, LabDatabase.OwnerA, LabDatabase.OwnerA))).DenyReason); + UAuthActions.Users.DeleteSelf, LabDatabase.OwnerA, LabDatabase.OwnerA), TestContext.Current.CancellationToken)).DenyReason); } [Fact] @@ -71,8 +71,8 @@ public async Task Batch_read_checks_every_target_and_requires_nonempty_targets() var policy = new SandboxUAuthAccessPolicy(db, new LabRootResolver(), new()); foreach (var targets in new[] { Array.Empty(), new[] { LabDatabase.MemberA, LabDatabase.MemberB } }) Assert.True((await policy.DecideAsync(Contexts.Create(UAuthActions.Users.GetByKeysAdmin, - LabDatabase.OwnerA, targets: targets))).IsDenied); + LabDatabase.OwnerA, targets: targets), TestContext.Current.CancellationToken)).IsDenied); Assert.True((await policy.DecideAsync(Contexts.Create(UAuthActions.Users.GetByKeysAdmin, - LabDatabase.OwnerA, targets: new[] { LabDatabase.MemberA, LabDatabase.OwnerA }))).IsAllowed); + LabDatabase.OwnerA, targets: new[] { LabDatabase.MemberA, LabDatabase.OwnerA }), TestContext.Current.CancellationToken)).IsAllowed); } } diff --git a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/CodeBeam.UltimateAuth.IdentityLab.Tests.csproj b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/CodeBeam.UltimateAuth.IdentityLab.Tests.csproj index 85ebd04e..7f863920 100644 --- a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/CodeBeam.UltimateAuth.IdentityLab.Tests.csproj +++ b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/CodeBeam.UltimateAuth.IdentityLab.Tests.csproj @@ -1,20 +1,16 @@  - net10.0 false true - - - - - - - - - + + + + + + diff --git a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Infrastructure/PostgresFixture.cs b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Infrastructure/PostgresFixture.cs index bf45c506..3f2936e0 100644 --- a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Infrastructure/PostgresFixture.cs +++ b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Infrastructure/PostgresFixture.cs @@ -12,7 +12,7 @@ public sealed class PostgresFixture : IAsyncLifetime { private PostgreSqlContainer? container; private string connectionString = ""; - public async Task InitializeAsync() + public async ValueTask InitializeAsync() { connectionString = Environment.GetEnvironmentVariable("IDENTITYLAB_TEST_POSTGRES") ?? ""; if (string.IsNullOrWhiteSpace(connectionString)) @@ -36,7 +36,7 @@ public async Task CreateAsync() await db.Database.MigrateAsync(); return database; } - public async Task DisposeAsync() + public async ValueTask DisposeAsync() { if (container is not null) await container.DisposeAsync(); } diff --git a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Integration/RegistrationIntegrationTests.cs b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Integration/RegistrationIntegrationTests.cs index 20a9b7d7..67f6757d 100644 --- a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Integration/RegistrationIntegrationTests.cs +++ b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Integration/RegistrationIntegrationTests.cs @@ -26,33 +26,33 @@ public async Task Registration_persists_account_owner_role_and_sandbox_without_l var registration = new LabRegistrationService(ActorProvider.For(null), sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService(), new(db), new(db, NullLogger.Instance)); - var result = await registration.RegisterAsync(new("alice", "alice@example.test", "test-password-8!")); + var result = await registration.RegisterAsync(new("alice", "alice@example.test", "test-password-8!"), TestContext.Current.CancellationToken); Assert.True(result.IsSuccess, result.ErrorMessage); UserKey owner; await using (var verify = db.CreateDbContext()) { - var sandbox = await verify.Sandboxes.Include(x => x.Members).SingleAsync(); + var sandbox = await verify.Sandboxes.Include(x => x.Members).SingleAsync(TestContext.Current.CancellationToken); owner = sandbox.OwnerUserKey; Assert.Equal(owner, Assert.Single(sandbox.Members).UserKey); } // Query actual framework stores, using a new scope to avoid tracked state. await using var verification = runtime.Services.CreateAsyncScope(); var assignments = await verification.ServiceProvider.GetRequiredService() - .Create(TenantKeys.Single).GetAssignmentsAsync(owner); + .Create(TenantKeys.Single).GetAssignmentsAsync(owner, TestContext.Current.CancellationToken); Assert.Contains(assignments, x => x.RoleId == LabRoleIds.Owner); Assert.DoesNotContain(assignments, x => x.RoleId == LabRoleIds.Admin); var lifecycle = await verification.ServiceProvider.GetRequiredService() - .Create(TenantKeys.Single).GetAsync(new UserLifecycleKey(TenantKeys.Single, owner)); + .Create(TenantKeys.Single).GetAsync(new UserLifecycleKey(TenantKeys.Single, owner), TestContext.Current.CancellationToken); Assert.NotNull(lifecycle); var duplicate = await Record.ExceptionAsync(async () => { - var duplicateResult = await registration.RegisterAsync(new("alice", "alice@example.test", "test-password-8!")); + var duplicateResult = await registration.RegisterAsync(new("alice", "alice@example.test", "test-password-8!"), TestContext.Current.CancellationToken); Assert.False(duplicateResult.IsSuccess); }); // Duplicate validation may throw or return failure depending on the framework contract. if (duplicate is not null) Assert.IsType(duplicate); await using var final = db.CreateDbContext(); - Assert.Equal(1, await final.Sandboxes.CountAsync()); + Assert.Equal(1, await final.Sandboxes.CountAsync(TestContext.Current.CancellationToken)); } [Fact] @@ -62,14 +62,14 @@ public async Task Trusted_seed_is_repeatable_and_assigns_ladmin_only_to_fixed_ad await using var runtime = await LabRuntime.CreateAsync(db); await using var scope = runtime.Services.CreateAsyncScope(); var sp = scope.ServiceProvider; - await ActivatorUtilities.CreateInstance(sp).SeedAsync(TenantKeys.Single); - await ActivatorUtilities.CreateInstance(sp).SeedAsync(TenantKeys.Single); + await ActivatorUtilities.CreateInstance(sp).SeedAsync(TenantKeys.Single, TestContext.Current.CancellationToken); + await ActivatorUtilities.CreateInstance(sp).SeedAsync(TenantKeys.Single, TestContext.Current.CancellationToken); var store = sp.GetRequiredService().Create(TenantKeys.Single); - var assignments = await store.GetAssignmentsAsync(LabAdminIdentity.UserKey); + var assignments = await store.GetAssignmentsAsync(LabAdminIdentity.UserKey, TestContext.Current.CancellationToken); Assert.Single(assignments, x => x.RoleId == LabRoleIds.Admin); - Assert.Equal(1, await store.CountAssignmentsAsync(LabRoleIds.Admin)); + Assert.Equal(1, await store.CountAssignmentsAsync(LabRoleIds.Admin, TestContext.Current.CancellationToken)); var roleStore = sp.GetRequiredService().Create(TenantKeys.Single); - var manager = await roleStore.GetByNameAsync("MANAGER"); + var manager = await roleStore.GetByNameAsync("MANAGER", TestContext.Current.CancellationToken); Assert.NotNull(manager); Assert.Equal(LabRoleIds.Manager, manager.Id); Assert.True(manager.Permissions.Select(x => x.Value).ToHashSet().SetEquals(LabDemoPermissions.Manager)); @@ -85,14 +85,14 @@ public async Task Actual_role_service_denies_user_escalation_without_changing_pe await using var scope = runtime.Services.CreateAsyncScope(); var service = scope.ServiceProvider.GetRequiredService(); var store = scope.ServiceProvider.GetRequiredService().Create(TenantKeys.Single); - var before = await store.GetAssignmentsAsync(LabDatabase.MemberA); + var before = await store.GetAssignmentsAsync(LabDatabase.MemberA, TestContext.Current.CancellationToken); var context = Contexts.Create(CodeBeam.UltimateAuth.Core.Defaults.UAuthActions.Authorization.Roles.AssignAdmin, LabDatabase.MemberA, LabDatabase.MemberA); await Assert.ThrowsAsync(() => - service.AssignAsync(context, LabDatabase.MemberA, role)); + service.AssignAsync(context, LabDatabase.MemberA, role, TestContext.Current.CancellationToken)); await using var verification = runtime.Services.CreateAsyncScope(); var after = await verification.ServiceProvider.GetRequiredService() - .Create(TenantKeys.Single).GetAssignmentsAsync(LabDatabase.MemberA); + .Create(TenantKeys.Single).GetAssignmentsAsync(LabDatabase.MemberA, TestContext.Current.CancellationToken); Assert.Equal(before.Select(x => x.RoleId).OrderBy(x => x.Value), after.Select(x => x.RoleId).OrderBy(x => x.Value)); } @@ -108,31 +108,31 @@ public async Task Owner_can_provision_member_and_assign_demo_role_but_cannot_del var assignments = sp.GetRequiredService(); var registration = new LabRegistrationService(ActorProvider.For(null), users, roles, assignments, new(db), new(db, NullLogger.Instance)); - Assert.True((await registration.RegisterAsync(new("owner", "owner@example.test", "test-password-8!"))).IsSuccess); + Assert.True((await registration.RegisterAsync(new("owner", "owner@example.test", "test-password-8!"), TestContext.Current.CancellationToken)).IsSuccess); UserKey owner; - await using (var verify = db.CreateDbContext()) owner = (await verify.Sandboxes.SingleAsync()).OwnerUserKey; + await using (var verify = db.CreateDbContext()) owner = (await verify.Sandboxes.SingleAsync(TestContext.Current.CancellationToken)).OwnerUserKey; var provider = ActorProvider.For(owner); var provisioning = new LabUserProvisioningService(provider, new(db), new(db), users, roles, assignments, sp.GetRequiredService(), NullLogger.Instance); var result = await provisioning.CreateMemberAsync(owner, new CreateUserRequest - { UserName = "member", Email = "member@example.test", Password = "test-password-8!" }); + { UserName = "member", Email = "member@example.test", Password = "test-password-8!" }, TestContext.Current.CancellationToken); Assert.True(result.IsSuccess, result.FailureReason); var member = UserKey.FromString(result.UserKey!); var demos = new LabDemoRoleService(provider, db, roles, assignments); - await demos.SetRoleAsync(member, "Manager", true); - await Assert.ThrowsAsync(() => demos.SetRoleAsync(member, "Ladmin", true)); + await demos.SetRoleAsync(member, "Manager", true, TestContext.Current.CancellationToken); + await Assert.ThrowsAsync(() => demos.SetRoleAsync(member, "Ladmin", true, ct: TestContext.Current.CancellationToken)); var roleService = sp.GetRequiredService(); await Assert.ThrowsAsync(() => roleService.AssignAsync(Contexts.Create(CodeBeam.UltimateAuth.Core.Defaults.UAuthActions.Authorization.Roles.AssignAdmin, - owner, member), member, "Ladmin")); + owner, member), member, "Ladmin", TestContext.Current.CancellationToken)); await using var fresh = runtime.Services.CreateAsyncScope(); var persisted = await fresh.ServiceProvider.GetRequiredService() - .Create(TenantKeys.Single).GetAssignmentsAsync(member); + .Create(TenantKeys.Single).GetAssignmentsAsync(member, TestContext.Current.CancellationToken); Assert.Contains(persisted, x => x.RoleId == LabRoleIds.User); Assert.Contains(persisted, x => x.RoleId == LabRoleIds.Manager); Assert.DoesNotContain(persisted, x => x.RoleId == LabRoleIds.Admin || x.RoleId == LabRoleIds.Owner); await using var final = db.CreateDbContext(); - Assert.Equal(member, (await final.SandboxMembers.SingleAsync(x => x.UserKey == member)).UserKey); + Assert.Equal(member, (await final.SandboxMembers.SingleAsync(x => x.UserKey == member, TestContext.Current.CancellationToken)).UserKey); } [Fact] @@ -144,21 +144,21 @@ public async Task Owner_cannot_rename_delete_or_change_permissions_of_system_rol await using var scope = runtime.Services.CreateAsyncScope(); var roles = scope.ServiceProvider.GetRequiredService(); var store = scope.ServiceProvider.GetRequiredService().Create(TenantKeys.Single); - var before = await store.GetByNameAsync("LADMIN"); + var before = await store.GetByNameAsync("LADMIN", TestContext.Current.CancellationToken); Assert.NotNull(before); CodeBeam.UltimateAuth.Core.Contracts.AccessContext Context(string action) => Contexts.Create(action, LabDatabase.OwnerA); await Assert.ThrowsAsync(() => - roles.RenameAsync(Context("authorization.roles.rename.admin"), LabRoleIds.Admin, "Manager")); + roles.RenameAsync(Context("authorization.roles.rename.admin"), LabRoleIds.Admin, "Manager", TestContext.Current.CancellationToken)); await Assert.ThrowsAsync(() => roles.DeleteAsync(Context("authorization.roles.delete.admin"), LabRoleIds.Admin, - CodeBeam.UltimateAuth.Core.Contracts.DeleteMode.Hard)); + CodeBeam.UltimateAuth.Core.Contracts.DeleteMode.Hard, TestContext.Current.CancellationToken)); await Assert.ThrowsAsync(() => roles.SetPermissionsAsync(Context("authorization.roles.permissions.admin"), LabRoleIds.Admin, - Array.Empty())); + Array.Empty(), TestContext.Current.CancellationToken)); await using var fresh = runtime.Services.CreateAsyncScope(); var after = await fresh.ServiceProvider.GetRequiredService() - .Create(TenantKeys.Single).GetByNameAsync("LADMIN"); + .Create(TenantKeys.Single).GetByNameAsync("LADMIN", TestContext.Current.CancellationToken); Assert.NotNull(after); Assert.Equal(before.Id, after.Id); Assert.Equal(before.Version, after.Version); @@ -179,9 +179,9 @@ public async Task Provisioning_assignment_fault_hard_deletes_real_partial_accoun var assignments = sp.GetRequiredService(); var registration = new LabRegistrationService(ActorProvider.For(null), users, roles, assignments, new(db), new(db, NullLogger.Instance)); - Assert.True((await registration.RegisterAsync(new("owner", "owner@example.test", "test-password-8!"))).IsSuccess); + Assert.True((await registration.RegisterAsync(new("owner", "owner@example.test", "test-password-8!"), TestContext.Current.CancellationToken)).IsSuccess); UserKey owner; - await using (var verify = db.CreateDbContext()) owner = (await verify.Sandboxes.SingleAsync()).OwnerUserKey; + await using (var verify = db.CreateDbContext()) owner = (await verify.Sandboxes.SingleAsync(cancellationToken: TestContext.Current.CancellationToken)).OwnerUserKey; var before = await db.SnapshotAsync(); var failedStore = new Moq.Mock(); failedStore.Setup(x => x.GetAssignmentsAsync(Moq.It.IsAny(), Moq.It.IsAny())) @@ -192,14 +192,14 @@ public async Task Provisioning_assignment_fault_hard_deletes_real_partial_accoun roles, failedFactory.Object, sp.GetRequiredService(), NullLogger.Instance); var failure = await Assert.ThrowsAsync(() => provisioning.CreateMemberAsync(owner, - new CreateUserRequest { UserName = "failed-member", Email = "failed-member@example.test", Password = "Member-test-password-8!" })); + new CreateUserRequest { UserName = "failed-member", Email = "failed-member@example.test", Password = "Member-test-password-8!" }, TestContext.Current.CancellationToken)); var partialKey = UserKey.FromString(failure.CreatedUserKey); await using var fresh = runtime.Services.CreateAsyncScope(); var lifecycle = await fresh.ServiceProvider.GetRequiredService() - .Create(TenantKeys.Single).GetAsync(new UserLifecycleKey(TenantKeys.Single, partialKey)); + .Create(TenantKeys.Single).GetAsync(new UserLifecycleKey(TenantKeys.Single, partialKey), TestContext.Current.CancellationToken); Assert.Null(lifecycle); var remainingRoles = await fresh.ServiceProvider.GetRequiredService() - .Create(TenantKeys.Single).GetAssignmentsAsync(partialKey); + .Create(TenantKeys.Single).GetAssignmentsAsync(partialKey, TestContext.Current.CancellationToken); Assert.Empty(remainingRoles); Assert.Equal(before, await db.SnapshotAsync()); } @@ -213,19 +213,18 @@ public async Task Actual_context_factory_replaces_forged_ladmin_attributes_with_ await using var scope = runtime.Services.CreateAsyncScope(); var sp = scope.ServiceProvider; await new LabRoleService(sp.GetRequiredService(), sp.GetRequiredService()) - .AssignAsync(TenantKeys.Single, LabDatabase.OwnerA, "Owner"); + .AssignAsync(TenantKeys.Single, LabDatabase.OwnerA, "Owner", TestContext.Current.CancellationToken); var context = await sp.GetRequiredService() .CreateAsync(Contexts.AuthenticatedFlow(LabDatabase.OwnerA), "users.delete.admin", "users", - LabDatabase.MemberB.Value, new Dictionary { ["roles"] = new[] { LabRoleIds.Admin } }); + LabDatabase.MemberB.Value, new Dictionary { ["roles"] = new[] { LabRoleIds.Admin } }, TestContext.Current.CancellationToken); Assert.False(new LabRootResolver().IsLadmin(context)); Assert.Equal(LabDatabase.OwnerA, context.ActorUserKey); var resolved = Assert.IsAssignableFrom>(context.Attributes["roles"]); Assert.Contains(LabRoleIds.Owner, resolved); Assert.DoesNotContain(LabRoleIds.Admin, resolved); var decision = await new CodeBeam.UltimateAuth.IdentityLab.Policy.SandboxUAuthAccessPolicy(db, - new LabRootResolver(), sp.GetRequiredService()).DecideAsync(context); + new LabRootResolver(), sp.GetRequiredService()).DecideAsync(context, TestContext.Current.CancellationToken); Assert.True(decision.IsDenied); Assert.Equal("sandbox_target_out_of_scope", decision.DenyReason); } - } diff --git a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Integration/SandboxPersistenceTests.cs b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Integration/SandboxPersistenceTests.cs index 982ff58a..f84054aa 100644 --- a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Integration/SandboxPersistenceTests.cs +++ b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Integration/SandboxPersistenceTests.cs @@ -11,9 +11,9 @@ public sealed class SandboxPersistenceTests(PostgresFixture postgres) : IClassFi public async Task Creation_persists_owner_membership_and_settings_together() { await using var db = await postgres.CreateAsync(); - var sandbox = await new SandboxService(db).CreateAsync(LabDatabase.OwnerA, " Sandbox A "); + var sandbox = await new SandboxService(db).CreateAsync(LabDatabase.OwnerA, " Sandbox A ", TestContext.Current.CancellationToken); await using var verify = db.CreateDbContext(); - var actual = await verify.Sandboxes.Include(x => x.Members).Include(x => x.Settings).SingleAsync(); + var actual = await verify.Sandboxes.Include(x => x.Members).Include(x => x.Settings).SingleAsync(cancellationToken: TestContext.Current.CancellationToken); Assert.Equal(sandbox.Id, actual.Id); Assert.Equal("Sandbox A", actual.Name); Assert.Equal(LabDatabase.OwnerA, actual.OwnerUserKey); @@ -30,14 +30,14 @@ public async Task Cross_sandbox_member_removal_and_owner_removal_leave_database_ await db.SeedAsync(); var before = await db.SnapshotAsync(); var service = new SandboxService(db); - Assert.False(await service.RemoveMemberAsync(LabDatabase.OwnerA, LabDatabase.MemberB)); - await Assert.ThrowsAsync(() => service.RemoveMemberAsync(LabDatabase.OwnerA, LabDatabase.OwnerA)); - await Assert.ThrowsAsync(() => service.RemoveMemberAsync(LabDatabase.MemberA, LabDatabase.MemberA)); + Assert.False(await service.RemoveMemberAsync(LabDatabase.OwnerA, LabDatabase.MemberB, TestContext.Current.CancellationToken)); + await Assert.ThrowsAsync(() => service.RemoveMemberAsync(LabDatabase.OwnerA, LabDatabase.OwnerA, TestContext.Current.CancellationToken)); + await Assert.ThrowsAsync(() => service.RemoveMemberAsync(LabDatabase.MemberA, LabDatabase.MemberA, TestContext.Current.CancellationToken)); Assert.Equal(before, await db.SnapshotAsync()); - Assert.True(await service.RemoveMemberAsync(LabDatabase.OwnerA, LabDatabase.MemberA)); + Assert.True(await service.RemoveMemberAsync(LabDatabase.OwnerA, LabDatabase.MemberA, TestContext.Current.CancellationToken)); await using var verify = db.CreateDbContext(); - Assert.False(await verify.SandboxMembers.AnyAsync(x => x.UserKey == LabDatabase.MemberA)); - Assert.True(await verify.SandboxMembers.AnyAsync(x => x.UserKey == LabDatabase.MemberB)); + Assert.False(await verify.SandboxMembers.AnyAsync(x => x.UserKey == LabDatabase.MemberA, cancellationToken: TestContext.Current.CancellationToken)); + Assert.True(await verify.SandboxMembers.AnyAsync(x => x.UserKey == LabDatabase.MemberB, cancellationToken: TestContext.Current.CancellationToken)); } [Fact] @@ -46,7 +46,7 @@ public async Task Duplicate_owner_creation_rolls_back_all_new_sandbox_state() await using var db = await postgres.CreateAsync(); await db.SeedAsync(); var before = await db.SnapshotAsync(); - await Assert.ThrowsAsync(() => new SandboxService(db).CreateAsync(LabDatabase.OwnerA, "Duplicate")); + await Assert.ThrowsAsync(() => new SandboxService(db).CreateAsync(LabDatabase.OwnerA, "Duplicate", TestContext.Current.CancellationToken)); Assert.Equal(before, await db.SnapshotAsync()); } @@ -57,9 +57,9 @@ public async Task Concurrent_member_additions_cannot_exceed_capacity() await db.SeedAsync(); await using (var setup = db.CreateDbContext()) { - var settings = await setup.SandboxSettings.SingleAsync(x => x.Sandbox.OwnerUserKey == LabDatabase.OwnerA); + var settings = await setup.SandboxSettings.SingleAsync(x => x.Sandbox.OwnerUserKey == LabDatabase.OwnerA, cancellationToken: TestContext.Current.CancellationToken); settings.MaxUsers = 3; - await setup.SaveChangesAsync(); + await setup.SaveChangesAsync(TestContext.Current.CancellationToken); } var service = new SandboxService(db); async Task Add(string key) => await Record.ExceptionAsync(async () => @@ -68,8 +68,8 @@ public async Task Concurrent_member_additions_cannot_exceed_capacity() var outcomes = await Task.WhenAll(Add("candidate-1"), Add("candidate-2")); Assert.Single(outcomes, x => x is null); await using var verify = db.CreateDbContext(); - Assert.Equal(3, await verify.SandboxMembers.CountAsync(x => x.Sandbox.OwnerUserKey == LabDatabase.OwnerA)); - Assert.Equal(2, await verify.SandboxMembers.CountAsync(x => x.Sandbox.OwnerUserKey == LabDatabase.OwnerB)); + Assert.Equal(3, await verify.SandboxMembers.CountAsync(x => x.Sandbox.OwnerUserKey == LabDatabase.OwnerA, cancellationToken: TestContext.Current.CancellationToken)); + Assert.Equal(2, await verify.SandboxMembers.CountAsync(x => x.Sandbox.OwnerUserKey == LabDatabase.OwnerB, cancellationToken: TestContext.Current.CancellationToken)); } [Fact] @@ -78,7 +78,7 @@ public async Task Member_cannot_be_added_to_second_sandbox() await using var db = await postgres.CreateAsync(); await db.SeedAsync(); var before = await db.SnapshotAsync(); - await Assert.ThrowsAsync(() => new SandboxService(db).AddMemberAsync(LabDatabase.OwnerA, LabDatabase.MemberB)); + await Assert.ThrowsAsync(() => new SandboxService(db).AddMemberAsync(LabDatabase.OwnerA, LabDatabase.MemberB, TestContext.Current.CancellationToken)); Assert.Equal(before, await db.SnapshotAsync()); } [Fact] @@ -91,9 +91,9 @@ public async Task Concurrent_creation_cannot_create_two_sandboxes_for_one_owner( var outcomes = await Task.WhenAll(Create("First"), Create("Second")); Assert.Single(outcomes, x => x is null); await using var verify = db.CreateDbContext(); - Assert.Equal(1, await verify.Sandboxes.CountAsync()); - Assert.Equal(1, await verify.SandboxMembers.CountAsync()); - Assert.Equal(1, await verify.SandboxSettings.CountAsync()); + Assert.Equal(1, await verify.Sandboxes.CountAsync(cancellationToken: TestContext.Current.CancellationToken)); + Assert.Equal(1, await verify.SandboxMembers.CountAsync(cancellationToken: TestContext.Current.CancellationToken)); + Assert.Equal(1, await verify.SandboxSettings.CountAsync(cancellationToken: TestContext.Current.CancellationToken)); } [Theory] @@ -104,32 +104,32 @@ public async Task Ensure_restores_owner_relationship_without_changing_existing_s await using var db = await postgres.CreateAsync(); await db.SeedAsync(); var service = new SandboxService(db); - var original = (await service.GetByUserAsync(LabDatabase.OwnerA))!; + var original = (await service.GetByUserAsync(LabDatabase.OwnerA, TestContext.Current.CancellationToken))!; await using (var setup = db.CreateDbContext()) { - var owner = await setup.SandboxMembers.SingleAsync(x => x.UserKey == LabDatabase.OwnerA); + var owner = await setup.SandboxMembers.SingleAsync(x => x.UserKey == LabDatabase.OwnerA, cancellationToken: TestContext.Current.CancellationToken); if (missingMembership) setup.SandboxMembers.Remove(owner); else owner.Role = SandboxMemberRole.Member; - await setup.SaveChangesAsync(); + await setup.SaveChangesAsync(TestContext.Current.CancellationToken); } - var repaired = await service.EnsureAsync(LabDatabase.OwnerA, "Must not replace existing name"); + var repaired = await service.EnsureAsync(LabDatabase.OwnerA, "Must not replace existing name", TestContext.Current.CancellationToken); Assert.Equal(original.Id, repaired.Id); Assert.Equal(original.Name, repaired.Name); Assert.Equal(original.Settings.MaxUsers, repaired.Settings.MaxUsers); var beforeRepeat = await db.SnapshotAsync(); - await service.EnsureAsync(LabDatabase.OwnerA, "Still must not rename"); + await service.EnsureAsync(LabDatabase.OwnerA, "Still must not rename", TestContext.Current.CancellationToken); Assert.Equal(beforeRepeat, await db.SnapshotAsync()); await using var verify = db.CreateDbContext(); - var members = await verify.SandboxMembers.ToListAsync(); + var members = await verify.SandboxMembers.ToListAsync(cancellationToken: TestContext.Current.CancellationToken); Assert.Equal(4, members.Count); Assert.Equal(SandboxMemberRole.Owner, Assert.Single(members, x => x.UserKey == LabDatabase.OwnerA).Role); Assert.Contains(members, x => x.UserKey == LabDatabase.MemberA && x.SandboxId == original.Id); Assert.Contains(members, x => x.UserKey == LabDatabase.OwnerB && x.Role == SandboxMemberRole.Owner); Assert.Contains(members, x => x.UserKey == LabDatabase.MemberB && x.SandboxId != original.Id); - Assert.Equal(2, await verify.Sandboxes.CountAsync()); - Assert.Equal(2, await verify.SandboxSettings.CountAsync()); + Assert.Equal(2, await verify.Sandboxes.CountAsync(cancellationToken: TestContext.Current.CancellationToken)); + Assert.Equal(2, await verify.SandboxSettings.CountAsync(cancellationToken: TestContext.Current.CancellationToken)); } } diff --git a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Integration/UserWorkflowTests.cs b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Integration/UserWorkflowTests.cs index 7a26a678..79926be4 100644 --- a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Integration/UserWorkflowTests.cs +++ b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Integration/UserWorkflowTests.cs @@ -53,15 +53,15 @@ public async Task Created_member_is_queryable_capacity_rejection_leaves_no_accou var sp = scope.ServiceProvider; var owner = await RegisterAsync(sp, db, "owner"); var foreignOwner = await RegisterAsync(sp, db, "foreign"); - var foreign = await Provisioning(sp, db, foreignOwner).CreateMemberAsync(foreignOwner, Request("foreign-member")); + var foreign = await Provisioning(sp, db, foreignOwner).CreateMemberAsync(foreignOwner, Request("foreign-member"), TestContext.Current.CancellationToken); Assert.True(foreign.IsSuccess, foreign.FailureReason); var foreignKey = UserKey.FromString(foreign.UserKey!); await using (var setup = db.CreateDbContext()) { - var settings = await setup.SandboxSettings.SingleAsync(x => x.Sandbox.OwnerUserKey == owner); + var settings = await setup.SandboxSettings.SingleAsync(x => x.Sandbox.OwnerUserKey == owner, cancellationToken: TestContext.Current.CancellationToken); settings.MaxUsers = 2; - await setup.SaveChangesAsync(); + await setup.SaveChangesAsync(TestContext.Current.CancellationToken); } var client = new Mock(MockBehavior.Strict); @@ -69,23 +69,23 @@ public async Task Created_member_is_queryable_capacity_rejection_leaves_no_accou client.SetupGet(x => x.Users).Returns(userClient.Object); var service = UserService(sp, db, owner, client.Object); - await service.CreateAsync(owner, Request("member")); - var visible = await service.GetUsersAsync(owner); + await service.CreateAsync(owner, Request("member"), TestContext.Current.CancellationToken); + var visible = await service.GetUsersAsync(owner, TestContext.Current.CancellationToken); Assert.NotNull(visible); var member = Assert.Single(visible.Users, x => !x.IsOwner).UserKey; Assert.Equal(2, visible.Users.Count); Assert.DoesNotContain(visible.Users, x => x.UserKey == foreignKey); var beforeRejected = await db.SnapshotAsync(); // The facade reports failures; assert persistence rather than dialog wording. - await service.CreateAsync(owner, Request("rejected-member")); + await service.CreateAsync(owner, Request("rejected-member"), TestContext.Current.CancellationToken); Assert.Equal(beforeRejected, await db.SnapshotAsync()); await using (var fresh = runtime.Services.CreateAsyncScope()) { var summaries = fresh.ServiceProvider.GetRequiredService().Create(TenantKeys.Single); - var all = await summaries.QueryAsync(new UserQuery { PageSize = 100, IncludeDeleted = true }); + var all = await summaries.QueryAsync(new UserQuery { PageSize = 100, IncludeDeleted = true }, TestContext.Current.CancellationToken); Assert.DoesNotContain(all.Items, x => x.UserName == "rejected-member"); } - Assert.False((await service.DeleteAsync(owner, foreignKey)).Success); + Assert.False((await service.DeleteAsync(owner, foreignKey, TestContext.Current.CancellationToken)).Success); Assert.Equal(beforeRejected, await db.SnapshotAsync()); userClient.VerifyNoOtherCalls(); var application = sp.GetRequiredService(); @@ -98,7 +98,7 @@ public async Task Created_member_is_queryable_capacity_rejection_leaves_no_accou return new UAuthResult { IsSuccess = true }; }); - Assert.True(await new SandboxAccountDeletionService(db, client.Object, NullLogger.Instance).DeleteMemberAsync(member)); + Assert.True(await new SandboxAccountDeletionService(db, client.Object, NullLogger.Instance).DeleteMemberAsync(member, TestContext.Current.CancellationToken)); userClient.Verify(x => x.DeleteMeAsync(), Times.Once); } else @@ -109,29 +109,29 @@ public async Task Created_member_is_queryable_capacity_rejection_leaves_no_accou var result = await application.DeleteUserAsync(Contexts.Create("users.delete.admin", owner, target), request); return new UAuthResult { IsSuccess = result.IsSuccess, Value = result }; }); - Assert.True((await service.DeleteAsync(owner, member)).Success); + Assert.True((await service.DeleteAsync(owner, member, TestContext.Current.CancellationToken)).Success); userClient.Verify(x => x.DeleteUserAsync(member, It.IsAny()), Times.Once); } await using var verification = runtime.Services.CreateAsyncScope(); var lifecycleStore = verification.ServiceProvider.GetRequiredService().Create(TenantKeys.Single); - var lifecycle = await lifecycleStore.GetAsync(new UserLifecycleKey(TenantKeys.Single, member)); + var lifecycle = await lifecycleStore.GetAsync(new UserLifecycleKey(TenantKeys.Single, member), TestContext.Current.CancellationToken); if (selfDelete) { Assert.NotNull(lifecycle); Assert.True(lifecycle.IsDeleted); } else Assert.Null(lifecycle); - Assert.NotNull(await lifecycleStore.GetAsync(new UserLifecycleKey(TenantKeys.Single, foreignKey))); - var remaining = await service.GetUsersAsync(owner); + Assert.NotNull(await lifecycleStore.GetAsync(new UserLifecycleKey(TenantKeys.Single, foreignKey), TestContext.Current.CancellationToken)); + var remaining = await service.GetUsersAsync(owner, TestContext.Current.CancellationToken); Assert.NotNull(remaining); Assert.Equal(owner, Assert.Single(remaining.Users).UserKey); // Optional dashboard calls may fail; its persisted count must still be usable. - var dashboard = await new LabDashboardService(new(db), client.Object).GetAsync(owner); + var dashboard = await new LabDashboardService(new(db), client.Object).GetAsync(owner, TestContext.Current.CancellationToken); Assert.NotNull(dashboard); Assert.Equal(1, dashboard.MemberCount); Assert.Equal(1, dashboard.RemainingCapacity); - await service.CreateAsync(owner, Request("replacement")); - Assert.Equal(2, (await service.GetUsersAsync(owner))!.Users.Count); + await service.CreateAsync(owner, Request("replacement"), TestContext.Current.CancellationToken); + Assert.Equal(2, (await service.GetUsersAsync(owner, TestContext.Current.CancellationToken))!.Users.Count); await using var final = db.CreateDbContext(); - Assert.False(await final.SandboxMembers.AnyAsync(x => x.UserKey == member)); - Assert.Equal(2, await final.SandboxMembers.CountAsync(x => x.Sandbox.OwnerUserKey == foreignOwner)); + Assert.False(await final.SandboxMembers.AnyAsync(x => x.UserKey == member, cancellationToken: TestContext.Current.CancellationToken)); + Assert.Equal(2, await final.SandboxMembers.CountAsync(x => x.Sandbox.OwnerUserKey == foreignOwner, cancellationToken: TestContext.Current.CancellationToken)); } [Theory] @@ -145,49 +145,49 @@ public async Task Owner_and_admin_recovery_restore_missing_membership_without_du var sp = scope.ServiceProvider; var owner = await RegisterAsync(sp, db, "repair-owner"); var other = await RegisterAsync(sp, db, "other-owner"); - var member = await Provisioning(sp, db, owner).CreateMemberAsync(owner, Request("retained-member")); + var member = await Provisioning(sp, db, owner).CreateMemberAsync(owner, Request("retained-member"), TestContext.Current.CancellationToken); Assert.True(member.IsSuccess, member.FailureReason); var roles = sp.GetRequiredService(); var assignments = sp.GetRequiredService(); await using (var setup = db.CreateDbContext()) { - setup.SandboxMembers.Remove(await setup.SandboxMembers.SingleAsync(x => x.UserKey == owner)); - await setup.SaveChangesAsync(); + setup.SandboxMembers.Remove(await setup.SandboxMembers.SingleAsync(x => x.UserKey == owner, cancellationToken: TestContext.Current.CancellationToken)); + await setup.SaveChangesAsync(TestContext.Current.CancellationToken); } if (adminRepair) { var context = Contexts.Create("identitylab.repair", LabAdminIdentity.UserKey); var service = new LabRepairService(db, sp.GetRequiredService(), roles, assignments, new LabAdminGuard(roles, assignments), NullLogger.Instance); - var snapshot = await service.GetSnapshotAsync(context); + var snapshot = await service.GetSnapshotAsync(context, TestContext.Current.CancellationToken); Assert.True(snapshot.UserScanComplete); Assert.Contains(snapshot.OrphanUsers, x => x.UserKey == owner && x.OwnedSandboxId != null); Assert.DoesNotContain(snapshot.OrphanUsers, x => x.UserKey == other); - Assert.True((await service.RepairOwnerAsync(context, owner)).Success); + Assert.True((await service.RepairOwnerAsync(context, owner, TestContext.Current.CancellationToken)).Success); var repaired = await db.SnapshotAsync(); - Assert.False((await service.RepairOwnerAsync(context, owner)).Success); + Assert.False((await service.RepairOwnerAsync(context, owner, TestContext.Current.CancellationToken)).Success); Assert.Equal(repaired, await db.SnapshotAsync()); - Assert.DoesNotContain((await service.GetSnapshotAsync(context)).OrphanUsers, x => x.UserKey == owner); + Assert.DoesNotContain((await service.GetSnapshotAsync(context, TestContext.Current.CancellationToken)).OrphanUsers, x => x.UserKey == owner); } else { var context = Contexts.Create("identitylab.repair", owner); var service = new LabOwnerRepairService(db, sp.GetRequiredService(), roles, assignments, NullLogger.Instance); - Assert.Equal(OwnerRepairState.CanRepair, (await service.GetStatusAsync(context)).State); - Assert.True((await service.RepairAsync(context)).Success); + Assert.Equal(OwnerRepairState.CanRepair, (await service.GetStatusAsync(context, TestContext.Current.CancellationToken)).State); + Assert.True((await service.RepairAsync(context, TestContext.Current.CancellationToken)).Success); var repaired = await db.SnapshotAsync(); - Assert.Equal(OwnerRepairState.NotRequired, (await service.GetStatusAsync(context)).State); - Assert.False((await service.RepairAsync(context)).Success); + Assert.Equal(OwnerRepairState.NotRequired, (await service.GetStatusAsync(context, TestContext.Current.CancellationToken)).State); + Assert.False((await service.RepairAsync(context, TestContext.Current.CancellationToken)).Success); Assert.Equal(repaired, await db.SnapshotAsync()); } await using var final = db.CreateDbContext(); - var members = await final.SandboxMembers.ToListAsync(); + var members = await final.SandboxMembers.ToListAsync(cancellationToken: TestContext.Current.CancellationToken); Assert.Equal(3, members.Count); var restored = Assert.Single(members, x => x.UserKey == owner); Assert.Equal(CodeBeam.UltimateAuth.IdentityLab.Domain.SandboxMemberRole.Owner, restored.Role); Assert.Contains(members, x => x.UserKey.Value == member.UserKey && x.SandboxId == restored.SandboxId); Assert.Contains(members, x => x.UserKey == other && x.SandboxId != restored.SandboxId); - Assert.Equal(2, await final.Sandboxes.CountAsync()); + Assert.Equal(2, await final.Sandboxes.CountAsync(cancellationToken: TestContext.Current.CancellationToken)); } } diff --git a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Provisioning/MemberProvisioningTests.cs b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Provisioning/MemberProvisioningTests.cs index c1a93792..3bb5675f 100644 --- a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Provisioning/MemberProvisioningTests.cs +++ b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Provisioning/MemberProvisioningTests.cs @@ -31,7 +31,7 @@ public async Task Forged_actor_or_regular_member_cannot_provision(string request new(db), new(db), users.Object, roles.Object, assignments.Object, new(), NullLogger.Instance); await Assert.ThrowsAsync(() => service.CreateMemberAsync( - UserKey.FromString(requestedActor), new CreateUserRequest())); + UserKey.FromString(requestedActor), new CreateUserRequest(), TestContext.Current.CancellationToken)); Assert.Equal(before, await db.SnapshotAsync()); users.VerifyNoOtherCalls(); roles.VerifyNoOtherCalls(); @@ -83,14 +83,14 @@ public async Task Failed_role_assignment_rolls_back_only_created_user_and_token_ var service = new LabUserProvisioningService(ActorProvider.For(LabDatabase.OwnerA), new(db), new(db), users.Object, roles.Object, assignments.Object, gate, NullLogger.Instance); await Assert.ThrowsAsync(() => service.CreateMemberAsync( - LabDatabase.OwnerA, new CreateUserRequest())); + LabDatabase.OwnerA, new CreateUserRequest(), TestContext.Current.CancellationToken)); Assert.NotNull(rollbackContext); Assert.Equal(createdKey, rollbackContext!.TargetUserKey); Assert.Equal(DeleteMode.Hard, rollbackMode); Assert.True(unrelatedDenied); Assert.True(rollbackAllowed); Assert.True(replayDenied); - Assert.True((await policy.DecideAsync(rollbackContext!)).IsDenied); + Assert.True((await policy.DecideAsync(rollbackContext!, TestContext.Current.CancellationToken)).IsDenied); users.Verify(x => x.DeleteUserAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); Assert.Equal(before, await db.SnapshotAsync()); } diff --git a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Provisioning/ProvisioningPolicyTests.cs b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Provisioning/ProvisioningPolicyTests.cs index ff1c440e..1911204f 100644 --- a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Provisioning/ProvisioningPolicyTests.cs +++ b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Provisioning/ProvisioningPolicyTests.cs @@ -26,7 +26,7 @@ public async Task Client_values_cannot_forge_provisioning_or_rollback_tokens(obj UAuthActions.Users.DeleteAdmin }) { var result = await policy.DecideAsync(Contexts.Create(action, LabDatabase.OwnerA, - UserKey.FromString("unrelated"), attributes: attributes)); + UserKey.FromString("unrelated"), attributes: attributes), TestContext.Current.CancellationToken); Assert.True(result.IsDenied); } Assert.Equal(before, await db.SnapshotAsync()); diff --git a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Provisioning/RegistrationTests.cs b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Provisioning/RegistrationTests.cs index 38f108bf..507cbfab 100644 --- a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Provisioning/RegistrationTests.cs +++ b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Provisioning/RegistrationTests.cs @@ -43,10 +43,10 @@ public async Task Registration_propagates_trusted_context_and_persists_matching_ assignments.Setup(x => x.Create(It.IsAny())).Returns(userStore.Object); var service = new LabRegistrationService(ActorProvider.For(null), users.Object, roles.Object, assignments.Object, new(db), new(db, NullLogger.Instance)); - var result = await service.RegisterAsync(new LabRegistrationRequest(" alice ", "alice@example.test", "test-password")); + var result = await service.RegisterAsync(new LabRegistrationRequest(" alice ", "alice@example.test", "test-password"), TestContext.Current.CancellationToken); Assert.True(result.IsSuccess); await using var verify = db.CreateDbContext(); - var sandbox = await verify.Sandboxes.Include(x => x.Members).SingleAsync(); + var sandbox = await verify.Sandboxes.Include(x => x.Members).SingleAsync(cancellationToken: TestContext.Current.CancellationToken); Assert.Equal(LabDatabase.OwnerA, sandbox.OwnerUserKey); Assert.Equal(LabDatabase.OwnerA, Assert.Single(sandbox.Members).UserKey); userStore.Verify(x => x.AssignAsync(LabDatabase.OwnerA, LabRoleIds.Owner, diff --git a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/README.md b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/README.md index 20985388..2b424ee9 100644 --- a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/README.md +++ b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/README.md @@ -3,9 +3,11 @@ The security and integration tests require .NET 10 SDK and Docker with permission to start containers. Run from the repository root: ```sh -dotnet test tests/CodeBeam.UltimateAuth.IdentityLab.Tests -c Release --logger trx +dotnet test --project tests/CodeBeam.UltimateAuth.IdentityLab.Tests/CodeBeam.UltimateAuth.IdentityLab.Tests.csproj -c Release --report-trx ``` +The repository selects Microsoft.Testing.Platform in `global.json`. Use `--report-trx` for TRX output and `--filter-trait "Category=Render"` for trait filtering. CI collects coverage with `coverlet.MTP`; the Identity Lab job preserves the assembly include and migration-file exclusion through `--coverlet-include` and `--coverlet-exclude-by-file`. + By default, each database test class starts PostgreSQL 17 through Testcontainers. Every test gets a new database, applies the actual LabDbContext migrations, and drops its database on disposal. Security and integration tests never use EF InMemory. Nothing connects to application production databases. Alternatively set `IDENTITYLAB_TEST_POSTGRES` to a connection string for a **dedicated test PostgreSQL instance**. Its login needs CREATE DATABASE and permission to drop the databases it creates. Only databases named `identitylab_test_` are created/dropped. This setting bypasses Docker; it does not bypass relational tests. Missing infrastructure fails fixture initialization rather than silently skipping tests. @@ -14,14 +16,14 @@ Tests replace authenticated-context sources and selected framework store/client `Regression/SystemRoleIdentityTests.cs` records an expected rejection that the inspected implementation does not enforce. It is intentionally neither skipped nor inverted to accept unsafe behavior. The `KnownDefect=SystemRoleIdentity` trait identifies it; normal local and CI runs include it. Expect that regression to fail until separately approved production work addresses the issue. Do not enable this suite as a required green check before defect triage. -See [coverage matrix](docs/security-coverage.md) and [execution report](docs/execution-report.md). The project is included in the solution. Repository CI excludes it from the general matrix test step and runs it in a dedicated .NET 10 PostgreSQL job; the dedicated job uploads TRX results even on failure. +See [coverage matrix](docs/security-coverage.md) and [execution report](docs/execution-report.md). The project is included in the solution. Repository CI selects framework test projects separately and runs it in a dedicated .NET 10 PostgreSQL job; the dedicated job uploads TRX results even on failure. ## Page render suite The `Rendering` tests use bUnit and need only the .NET 10 SDK; no Docker or PostgreSQL is required: ```sh -dotnet test tests/CodeBeam.UltimateAuth.IdentityLab.Tests/CodeBeam.UltimateAuth.IdentityLab.Tests.csproj -c Release --filter Category=Render +dotnet test --project tests/CodeBeam.UltimateAuth.IdentityLab.Tests/CodeBeam.UltimateAuth.IdentityLab.Tests.csproj -c Release --filter-trait "Category=Render" ``` The real pages and child components render with MudBlazor services and a popover provider. External client calls use explicit Moq responses. Dashboard initialization uses real Lab services with a uniquely named EF InMemory LabDbContext store; this verifies component initialization and navigation branches, not relational behavior. Existing service regression tests and their PostgreSQL fixtures are unchanged. Direct page rendering does not exercise router authorization or layouts. diff --git a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Regression/SystemRoleIdentityTests.cs b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Regression/SystemRoleIdentityTests.cs index a6d147a6..c78ccaf4 100644 --- a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Regression/SystemRoleIdentityTests.cs +++ b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Regression/SystemRoleIdentityTests.cs @@ -30,7 +30,7 @@ public async Task Owner_name_with_admin_id_must_be_rejected_before_assignment() assignments.Setup(x => x.Create(It.IsAny())).Returns(userStore.Object); var service = new LabRoleService(roles.Object, assignments.Object); await Assert.ThrowsAsync(() => service.AssignAsync( - TenantKeys.Single, LabDatabase.MemberA, "Owner")); + TenantKeys.Single, LabDatabase.MemberA, "Owner", TestContext.Current.CancellationToken)); userStore.Verify(x => x.AssignAsync(LabDatabase.MemberA, LabRoleIds.Admin, It.IsAny(), It.IsAny()), Times.Never); } diff --git a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Security/AdminGuardTests.cs b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Security/AdminGuardTests.cs index fd2fe01d..270db14e 100644 --- a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Security/AdminGuardTests.cs +++ b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Security/AdminGuardTests.cs @@ -26,7 +26,7 @@ public async Task Admin_guard_uses_store_assignment_even_when_attributes_claim_l users.Setup(x => x.Create(It.IsAny())).Returns(userStore.Object); var context = Contexts.Create("identitylab.admin", LabDatabase.OwnerA, attributes: new Dictionary { ["roles"] = new[] { LabRoleIds.Admin } }); - Assert.Equal(assigned, await new LabAdminGuard(roles.Object, users.Object).IsAdminAsync(context)); + Assert.Equal(assigned, await new LabAdminGuard(roles.Object, users.Object).IsAdminAsync(context, TestContext.Current.CancellationToken)); } [Theory] @@ -39,7 +39,7 @@ public async Task Provisioning_role_helper_rejects_roles_outside_owner_and_user( var roles = new Mock(MockBehavior.Strict); var users = new Mock(MockBehavior.Strict); await Assert.ThrowsAsync(() => new CodeBeam.UltimateAuth.IdentityLab.Services.LabRoleService( - roles.Object, users.Object).AssignAsync(TenantKeys.Single, LabDatabase.MemberA, name)); + roles.Object, users.Object).AssignAsync(TenantKeys.Single, LabDatabase.MemberA, name, TestContext.Current.CancellationToken)); roles.VerifyNoOtherCalls(); users.VerifyNoOtherCalls(); } diff --git a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Security/RolePolicyTests.cs b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Security/RolePolicyTests.cs index 4bef7f72..865b7ca9 100644 --- a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Security/RolePolicyTests.cs +++ b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Security/RolePolicyTests.cs @@ -15,7 +15,7 @@ public async Task Public_role_assignment_is_denied_even_when_context_claims_admi var context = Contexts.Create(UAuthActions.Authorization.Roles.AssignAdmin, CodeBeam.UltimateAuth.Core.Domain.UserKey.FromString(actor), LabDatabase.MemberA, attributes: new Dictionary { ["roles"] = new[] { LabRoleIds.Admin } }); - var result = await new LabApplicationPolicy().DecideAsync(context); + var result = await new LabApplicationPolicy().DecideAsync(context, TestContext.Current.CancellationToken); Assert.True(result.IsDenied); Assert.Equal("lab_role_management_restricted", result.DenyReason); } @@ -25,11 +25,11 @@ public async Task Trusted_system_role_assignment_and_authenticated_read_remain_a { var policy = new LabApplicationPolicy(); Assert.True((await policy.DecideAsync(Contexts.Create( - UAuthActions.Authorization.Roles.AssignAdmin, system: true))).IsAllowed); + UAuthActions.Authorization.Roles.AssignAdmin, system: true), TestContext.Current.CancellationToken)).IsAllowed); Assert.True((await policy.DecideAsync(Contexts.Create( - UAuthActions.Authorization.Roles.GetSelf, LabDatabase.MemberA))).IsAllowed); + UAuthActions.Authorization.Roles.GetSelf, LabDatabase.MemberA), TestContext.Current.CancellationToken)).IsAllowed); Assert.True((await policy.DecideAsync(Contexts.Create( - UAuthActions.Authorization.Roles.GetSelf))).IsDenied); + UAuthActions.Authorization.Roles.GetSelf), TestContext.Current.CancellationToken)).IsDenied); } [Theory] diff --git a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Services/AccountDeletionTests.cs b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Services/AccountDeletionTests.cs index dd0e29a3..0d3d99ee 100644 --- a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Services/AccountDeletionTests.cs +++ b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Services/AccountDeletionTests.cs @@ -22,11 +22,11 @@ public async Task Membership_is_removed_only_after_framework_self_deletion_succe var client = new Mock(MockBehavior.Strict); client.SetupGet(x => x.Users).Returns(users.Object); var service = new SandboxAccountDeletionService(db, client.Object, NullLogger.Instance); - Assert.Equal(success, await service.DeleteMemberAsync(LabDatabase.MemberA)); + Assert.Equal(success, await service.DeleteMemberAsync(LabDatabase.MemberA, TestContext.Current.CancellationToken)); await using var verify = db.CreateDbContext(); - Assert.Equal(!success, await verify.SandboxMembers.AnyAsync(x => x.UserKey == LabDatabase.MemberA)); - Assert.True(await verify.SandboxMembers.AnyAsync(x => x.UserKey == LabDatabase.MemberB)); - Assert.True(await verify.SandboxMembers.AnyAsync(x => x.UserKey == LabDatabase.OwnerA)); + Assert.Equal(!success, await verify.SandboxMembers.AnyAsync(x => x.UserKey == LabDatabase.MemberA, cancellationToken: TestContext.Current.CancellationToken)); + Assert.True(await verify.SandboxMembers.AnyAsync(x => x.UserKey == LabDatabase.MemberB, cancellationToken: TestContext.Current.CancellationToken)); + Assert.True(await verify.SandboxMembers.AnyAsync(x => x.UserKey == LabDatabase.OwnerA, cancellationToken: TestContext.Current.CancellationToken)); } [Fact] @@ -37,7 +37,7 @@ public async Task Owner_self_deletion_never_calls_framework_client() var before = await db.SnapshotAsync(); var client = new Mock(MockBehavior.Strict); var service = new SandboxAccountDeletionService(db, client.Object, NullLogger.Instance); - Assert.False(await service.DeleteMemberAsync(LabDatabase.OwnerA)); + Assert.False(await service.DeleteMemberAsync(LabDatabase.OwnerA, TestContext.Current.CancellationToken)); client.VerifyNoOtherCalls(); Assert.Equal(before, await db.SnapshotAsync()); } diff --git a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Services/DashboardTests.cs b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Services/DashboardTests.cs index 5baaab3d..7a698f9b 100644 --- a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Services/DashboardTests.cs +++ b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Services/DashboardTests.cs @@ -21,9 +21,9 @@ public async Task Overview_uses_own_membership_and_effective_capacity_when_optio await db.SeedAsync(); await using (var setup = db.CreateDbContext()) { - var settings = await setup.SandboxSettings.SingleAsync(x => x.Sandbox.OwnerUserKey == LabDatabase.OwnerA); + var settings = await setup.SandboxSettings.SingleAsync(x => x.Sandbox.OwnerUserKey == LabDatabase.OwnerA, cancellationToken: TestContext.Current.CancellationToken); settings.MaxUsers = configuredLimit; - await setup.SaveChangesAsync(); + await setup.SaveChangesAsync(TestContext.Current.CancellationToken); } var users = new Mock(MockBehavior.Strict); users.Setup(x => x.GetMeAsync(null)).ThrowsAsync(new InvalidOperationException("Profile unavailable")); @@ -39,7 +39,7 @@ public async Task Overview_uses_own_membership_and_effective_capacity_when_optio client.SetupGet(x => x.Authorization).Returns(authorization.Object); var service = new LabDashboardService(new(db), client.Object); - var owner = await service.GetAsync(LabDatabase.OwnerA); + var owner = await service.GetAsync(LabDatabase.OwnerA, TestContext.Current.CancellationToken); Assert.NotNull(owner); Assert.Equal("Sandbox A", owner.SandboxName); Assert.True(owner.IsOwner); @@ -53,14 +53,14 @@ public async Task Overview_uses_own_membership_and_effective_capacity_when_optio sessions.VerifyAll(); authorization.VerifyAll(); - var member = await service.GetAsync(LabDatabase.MemberA); + var member = await service.GetAsync(LabDatabase.MemberA, TestContext.Current.CancellationToken); Assert.NotNull(member); Assert.Equal(owner.SandboxId, member.SandboxId); Assert.False(member.IsOwner); Assert.Equal(owner.MemberCount, member.MemberCount); - Assert.True(await new SandboxService(db).RemoveMemberAsync(LabDatabase.OwnerA, LabDatabase.MemberA)); - var after = await service.GetAsync(LabDatabase.OwnerA); + Assert.True(await new SandboxService(db).RemoveMemberAsync(LabDatabase.OwnerA, LabDatabase.MemberA, TestContext.Current.CancellationToken)); + var after = await service.GetAsync(LabDatabase.OwnerA, TestContext.Current.CancellationToken); Assert.NotNull(after); Assert.Equal(1, after.MemberCount); Assert.Equal(expectedLimit - 1, after.RemainingCapacity); @@ -72,7 +72,7 @@ public async Task Missing_membership_returns_no_dashboard_without_requesting_opt await using var db = await postgres.CreateAsync(); await db.SeedAsync(); var client = new Mock(MockBehavior.Strict); - Assert.Null(await new LabDashboardService(new(db), client.Object).GetAsync(UserKey.FromString("orphan"))); + Assert.Null(await new LabDashboardService(new(db), client.Object).GetAsync(UserKey.FromString("orphan"), TestContext.Current.CancellationToken)); client.VerifyNoOtherCalls(); } } diff --git a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Services/DemoRoleServiceTests.cs b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Services/DemoRoleServiceTests.cs index 5077f1a9..bb5ec8fa 100644 --- a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Services/DemoRoleServiceTests.cs +++ b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Services/DemoRoleServiceTests.cs @@ -26,7 +26,7 @@ public async Task Demo_operations_reject_system_and_forged_names_without_accessi var roles = new Mock(MockBehavior.Strict); var assignments = new Mock(MockBehavior.Strict); var service = new LabDemoRoleService(ActorProvider.For(LabDatabase.OwnerA), db, roles.Object, assignments.Object); - await Assert.ThrowsAsync(() => service.SetRoleAsync(LabDatabase.MemberA, role, true)); + await Assert.ThrowsAsync(() => service.SetRoleAsync(LabDatabase.MemberA, role, true, TestContext.Current.CancellationToken)); Assert.Equal(before, await db.SnapshotAsync()); roles.VerifyNoOtherCalls(); assignments.VerifyNoOtherCalls(); @@ -48,7 +48,7 @@ public async Task Assignment_requires_active_persisted_owner_and_same_sandbox_re var roles = new Mock(MockBehavior.Strict); var assignments = new Mock(MockBehavior.Strict); var service = new LabDemoRoleService(ActorProvider.For(UserKey.FromString(actor)), db, roles.Object, assignments.Object); - await Assert.ThrowsAsync(() => service.SetRoleAsync(UserKey.FromString(target), LabRoles.Manager, true)); + await Assert.ThrowsAsync(() => service.SetRoleAsync(UserKey.FromString(target), LabRoles.Manager, true, TestContext.Current.CancellationToken)); Assert.Equal(before, await db.SnapshotAsync()); roles.VerifyNoOtherCalls(); assignments.VerifyNoOtherCalls(); @@ -80,12 +80,12 @@ public async Task Demo_role_id_must_match_fixed_identity_before_any_assignment(b var service = new LabDemoRoleService(ActorProvider.For(LabDatabase.OwnerA), db, roles.Object, assignments.Object); if (forged) { - await Assert.ThrowsAsync(() => service.SetRoleAsync(LabDatabase.MemberA, "Manager", true)); + await Assert.ThrowsAsync(() => service.SetRoleAsync(LabDatabase.MemberA, "Manager", true, TestContext.Current.CancellationToken)); userStore.VerifyNoOtherCalls(); } else { - await service.SetRoleAsync(LabDatabase.MemberA, "Manager", true); + await service.SetRoleAsync(LabDatabase.MemberA, "Manager", true, TestContext.Current.CancellationToken); userStore.Verify(x => x.AssignAsync(LabDatabase.MemberA, LabRoleIds.Manager, It.IsAny(), It.IsAny()), Times.Once); } diff --git a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Services/DemoStoreTests.cs b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Services/DemoStoreTests.cs index f3ada7e1..1800303b 100644 --- a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Services/DemoStoreTests.cs +++ b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Services/DemoStoreTests.cs @@ -37,11 +37,11 @@ public async Task Demo_store_only_accepts_exact_demo_permissions_from_fixed_role var roles = new Mock(); roles.Setup(x => x.Create(It.IsAny())).Returns(store.Object); var service = new LabDemoStoreService(ActorProvider.For(LabDatabase.MemberA), db, roles.Object, users.Object); - var actual = await service.GetPermissionsAsync(); + var actual = await service.GetPermissionsAsync(TestContext.Current.CancellationToken); Assert.True(actual.SetEquals(expected)); - await service.RequireAsync(LabDemoPermissions.ProductsRead); - await Assert.ThrowsAsync(() => service.RequireAsync("users.delete.admin")); + await service.RequireAsync(LabDemoPermissions.ProductsRead, TestContext.Current.CancellationToken); + await Assert.ThrowsAsync(() => service.RequireAsync("users.delete.admin", TestContext.Current.CancellationToken)); await db.SetStatusAsync(SandboxStatus.Suspended); - Assert.Empty(await service.GetPermissionsAsync()); + Assert.Empty(await service.GetPermissionsAsync(TestContext.Current.CancellationToken)); } } diff --git a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Services/RepairTests.cs b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Services/RepairTests.cs index fcf6fb76..df6481e0 100644 --- a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Services/RepairTests.cs +++ b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Services/RepairTests.cs @@ -20,11 +20,11 @@ public async Task Repair_removes_only_own_stale_members_and_preserves_both_owner var stores = new Mock(); stores.Setup(x => x.Create(It.IsAny())).Returns(store.Object); var service = new SandboxRepairService(db, stores.Object, NullLogger.Instance); - await Assert.ThrowsAsync(() => service.RepairAsync(LabDatabase.MemberA)); - var result = await service.RepairAsync(LabDatabase.OwnerA); + await Assert.ThrowsAsync(() => service.RepairAsync(LabDatabase.MemberA, TestContext.Current.CancellationToken)); + var result = await service.RepairAsync(LabDatabase.OwnerA, TestContext.Current.CancellationToken); Assert.Equal(1, result.RemovedMemberships); await using var verify = db.CreateDbContext(); - var keys = await verify.SandboxMembers.Select(x => x.UserKey).ToListAsync(); + var keys = await verify.SandboxMembers.Select(x => x.UserKey).ToListAsync(cancellationToken: TestContext.Current.CancellationToken); Assert.DoesNotContain(LabDatabase.MemberA, keys); Assert.Contains(LabDatabase.MemberB, keys); Assert.Contains(LabDatabase.OwnerA, keys); @@ -36,7 +36,7 @@ public async Task Preview_matches_repair_and_repeated_repair_preserves_valid_and await using var db = await postgres.CreateAsync(); await db.SeedAsync(); var validKey = CodeBeam.UltimateAuth.Core.Domain.UserKey.FromString("valid-member"); - await new SandboxService(db).AddMemberAsync(LabDatabase.OwnerA, validKey); + await new SandboxService(db).AddMemberAsync(LabDatabase.OwnerA, validKey, TestContext.Current.CancellationToken); var store = new Mock(MockBehavior.Strict); store.Setup(x => x.GetByKeysAsync( It.IsAny>(), It.IsAny())) @@ -45,15 +45,15 @@ public async Task Preview_matches_repair_and_repeated_repair_preserves_valid_and stores.Setup(x => x.Create(It.IsAny())).Returns(store.Object); var service = new SandboxRepairService(db, stores.Object, NullLogger.Instance); var before = await db.SnapshotAsync(); - Assert.Equal(new SandboxRepairPreview(3, 1), await service.PreviewAsync(LabDatabase.OwnerA)); + Assert.Equal(new SandboxRepairPreview(3, 1), await service.PreviewAsync(LabDatabase.OwnerA, TestContext.Current.CancellationToken)); Assert.Equal(before, await db.SnapshotAsync()); - Assert.Equal(1, (await service.RepairAsync(LabDatabase.OwnerA)).RemovedMemberships); + Assert.Equal(1, (await service.RepairAsync(LabDatabase.OwnerA, TestContext.Current.CancellationToken)).RemovedMemberships); var repaired = await db.SnapshotAsync(); - Assert.Equal(new SandboxRepairPreview(2, 0), await service.PreviewAsync(LabDatabase.OwnerA)); - Assert.Equal(0, (await service.RepairAsync(LabDatabase.OwnerA)).RemovedMemberships); + Assert.Equal(new SandboxRepairPreview(2, 0), await service.PreviewAsync(LabDatabase.OwnerA, TestContext.Current.CancellationToken)); + Assert.Equal(0, (await service.RepairAsync(LabDatabase.OwnerA, TestContext.Current.CancellationToken)).RemovedMemberships); Assert.Equal(repaired, await db.SnapshotAsync()); await using var verify = db.CreateDbContext(); - var keys = await verify.SandboxMembers.Select(x => x.UserKey).ToListAsync(); + var keys = await verify.SandboxMembers.Select(x => x.UserKey).ToListAsync(cancellationToken: TestContext.Current.CancellationToken); Assert.Equal(4, keys.Count); Assert.Contains(validKey, keys); Assert.Contains(LabDatabase.OwnerA, keys); diff --git a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Services/SandboxAdminTests.cs b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Services/SandboxAdminTests.cs index 9ecc085e..e631298c 100644 --- a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Services/SandboxAdminTests.cs +++ b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Services/SandboxAdminTests.cs @@ -26,20 +26,20 @@ public async Task Capacity_changes_respect_existing_members_and_control_subseque await using var db = await postgres.CreateAsync(); await db.SeedAsync(); var sandboxes = new SandboxService(db); - var sandbox = (await sandboxes.GetByUserAsync(LabDatabase.OwnerA))!; + var sandbox = (await sandboxes.GetByUserAsync(LabDatabase.OwnerA, TestContext.Current.CancellationToken))!; var service = Create(db); var before = await db.SnapshotAsync(); - await Assert.ThrowsAsync(() => service.SetMaxUsersAsync(Admin, sandbox.Id, 1)); + await Assert.ThrowsAsync(() => service.SetMaxUsersAsync(Admin, sandbox.Id, 1, TestContext.Current.CancellationToken)); Assert.Equal(before, await db.SnapshotAsync()); - Assert.True(await service.SetMaxUsersAsync(Admin, sandbox.Id, 2)); - await Assert.ThrowsAsync(() => sandboxes.AddMemberAsync(LabDatabase.OwnerA, UserKey.FromString("new-member"))); - Assert.True(await service.SetMaxUsersAsync(Admin, sandbox.Id, 3)); - await sandboxes.AddMemberAsync(LabDatabase.OwnerA, UserKey.FromString("new-member")); - var summary = await service.GetAsync(Admin, sandbox.Id); + Assert.True(await service.SetMaxUsersAsync(Admin, sandbox.Id, 2, TestContext.Current.CancellationToken)); + await Assert.ThrowsAsync(() => sandboxes.AddMemberAsync(LabDatabase.OwnerA, UserKey.FromString("new-member"), TestContext.Current.CancellationToken)); + Assert.True(await service.SetMaxUsersAsync(Admin, sandbox.Id, 3, TestContext.Current.CancellationToken)); + await sandboxes.AddMemberAsync(LabDatabase.OwnerA, UserKey.FromString("new-member"), TestContext.Current.CancellationToken); + var summary = await service.GetAsync(Admin, sandbox.Id, TestContext.Current.CancellationToken); Assert.NotNull(summary); Assert.Equal(3, summary.MemberCount); Assert.Equal(3, summary.MaxUsers); - Assert.Equal(2, (await sandboxes.GetMembersAsync((await sandboxes.GetByUserAsync(LabDatabase.OwnerB))!.Id)).Count); + Assert.Equal(2, (await sandboxes.GetMembersAsync((await sandboxes.GetByUserAsync(LabDatabase.OwnerB, TestContext.Current.CancellationToken))!.Id, TestContext.Current.CancellationToken)).Count); } [Theory] @@ -50,10 +50,10 @@ public async Task Search_treats_postgres_pattern_characters_literally(string lit { await using var db = await postgres.CreateAsync(); await db.SeedAsync(); - var sandbox = (await new SandboxService(db).GetByUserAsync(LabDatabase.OwnerA))!; + var sandbox = (await new SandboxService(db).GetByUserAsync(LabDatabase.OwnerA, TestContext.Current.CancellationToken))!; var service = Create(db); - Assert.True(await service.RenameAsync(Admin, sandbox.Id, $" Lab {literal} special ")); - var result = await service.QueryAsync(Admin, new SandboxQuery { Search = $" {literal} " }); + Assert.True(await service.RenameAsync(Admin, sandbox.Id, $" Lab {literal} special ", TestContext.Current.CancellationToken)); + var result = await service.QueryAsync(Admin, new SandboxQuery { Search = $" {literal} " }, TestContext.Current.CancellationToken); Assert.Equal(1, result.TotalCount); Assert.Equal(sandbox.Id, Assert.Single(result.Items).Id); Assert.Equal($"Lab {literal} special", result.Items[0].Name); @@ -65,15 +65,15 @@ public async Task Statistics_follow_status_transitions_and_membership_removal() await using var db = await postgres.CreateAsync(); await db.SeedAsync(); var sandboxes = new SandboxService(db); - var a = (await sandboxes.GetByUserAsync(LabDatabase.OwnerA))!; - var b = (await sandboxes.GetByUserAsync(LabDatabase.OwnerB))!; + var a = (await sandboxes.GetByUserAsync(LabDatabase.OwnerA, TestContext.Current.CancellationToken))!; + var b = (await sandboxes.GetByUserAsync(LabDatabase.OwnerB, TestContext.Current.CancellationToken))!; var service = Create(db); - Assert.Equal(new SandboxStatistics(2, 2, 0, 0, 4), await service.GetStatisticsAsync(Admin)); - Assert.True(await sandboxes.RemoveMemberAsync(LabDatabase.OwnerA, LabDatabase.MemberA)); - Assert.True(await service.SetStatusAsync(Admin, a.Id, SandboxStatus.Passive)); - Assert.True(await service.SetStatusAsync(Admin, b.Id, SandboxStatus.Suspended)); - Assert.Equal(new SandboxStatistics(2, 0, 1, 1, 3), await service.GetStatisticsAsync(Admin)); - var suspended = await service.QueryAsync(Admin, new SandboxQuery { Status = SandboxStatus.Suspended }); + Assert.Equal(new SandboxStatistics(2, 2, 0, 0, 4), await service.GetStatisticsAsync(Admin, TestContext.Current.CancellationToken)); + Assert.True(await sandboxes.RemoveMemberAsync(LabDatabase.OwnerA, LabDatabase.MemberA, TestContext.Current.CancellationToken)); + Assert.True(await service.SetStatusAsync(Admin, a.Id, SandboxStatus.Passive, TestContext.Current.CancellationToken)); + Assert.True(await service.SetStatusAsync(Admin, b.Id, SandboxStatus.Suspended, TestContext.Current.CancellationToken)); + Assert.Equal(new SandboxStatistics(2, 0, 1, 1, 3), await service.GetStatisticsAsync(Admin, TestContext.Current.CancellationToken)); + var suspended = await service.QueryAsync(Admin, new SandboxQuery { Status = SandboxStatus.Suspended }, TestContext.Current.CancellationToken); Assert.Equal(1, suspended.TotalCount); Assert.Equal(b.Id, Assert.Single(suspended.Items).Id); } diff --git a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/identity-lab.runsettings b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/identity-lab.runsettings deleted file mode 100644 index 87dfcfab..00000000 --- a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/identity-lab.runsettings +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - [CodeBeam.UltimateAuth.IdentityLab]* - **/Migrations/**/*.cs - cobertura,json - - - - - 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 ed85da6c..dc1e7447 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 @@ -1,20 +1,16 @@  - net10.0 enable enable false - - - - - - + + + diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration.EfCore/UserCreationAtomicityTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration.EfCore/UserCreationAtomicityTests.cs index 61190150..36553b50 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration.EfCore/UserCreationAtomicityTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration.EfCore/UserCreationAtomicityTests.cs @@ -56,7 +56,7 @@ await EfCoreTestRuntime.CreateAsync( }; var exception = - await Assert.ThrowsAsync(() => service.CreateUserAsync(context, request)); + await Assert.ThrowsAsync(() => service.CreateUserAsync(context, request, TestContext.Current.CancellationToken)); exception.Message.Should().Be("simulated_identifier_store_failure"); @@ -96,17 +96,17 @@ await lifecycleFactory .GetAsync( new UserLifecycleKey( tenant, - userKey)); + userKey), TestContext.Current.CancellationToken); var profiles = await profileFactory .Create(tenant) - .GetAllProfilesByUserAsync(userKey); + .GetAllProfilesByUserAsync(userKey, TestContext.Current.CancellationToken); var identifiers = await identifierFactory .Create(tenant) - .GetByUserAsync(userKey); + .GetByUserAsync(userKey, TestContext.Current.CancellationToken); // // Atomicity contract: diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration.EfCore/UserQueryPaginationTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration.EfCore/UserQueryPaginationTests.cs index 67b0454c..6e75fc0c 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration.EfCore/UserQueryPaginationTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration.EfCore/UserQueryPaginationTests.cs @@ -54,7 +54,7 @@ public async Task QueryUsersAsync_AppliesConfiguredPaginationAndSorting() { UserName = $"{prefix}-{name}", DisplayName = $"{prefix}-{name}" - }); + }, TestContext.Current.CancellationToken); result.IsSuccess.Should().BeTrue(); } @@ -81,7 +81,7 @@ public async Task QueryUsersAsync_AppliesConfiguredPaginationAndSorting() PageSize = 0, Search = prefix, SortBy = nameof(UserSummary.DisplayName) - }); + }, TestContext.Current.CancellationToken); firstPage.TotalCount.Should().Be(5); firstPage.PageNumber.Should().Be(1); @@ -106,7 +106,7 @@ public async Task QueryUsersAsync_AppliesConfiguredPaginationAndSorting() PageSize = 100, Search = prefix, SortBy = nameof(UserSummary.DisplayName) - }); + }, TestContext.Current.CancellationToken); secondPage.TotalCount.Should().Be(5); secondPage.PageNumber.Should().Be(2); @@ -163,7 +163,7 @@ public async Task QueryUsersAsync_AcrossPages_ReturnsEveryUserExactlyOnce() { UserName = $"{prefix}-{name}", DisplayName = $"{prefix}-{name}" - }); + }, TestContext.Current.CancellationToken); result.IsSuccess.Should().BeTrue(); } @@ -191,7 +191,7 @@ public async Task QueryUsersAsync_AcrossPages_ReturnsEveryUserExactlyOnce() PageSize = 2, Search = prefix, SortBy = nameof(UserSummary.DisplayName) - }); + }, TestContext.Current.CancellationToken); pages.Add(result); } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/AssemblyBehavior.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/AssemblyBehavior.cs index c7fc3b1f..0378b5a6 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration/AssemblyBehavior.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/AssemblyBehavior.cs @@ -1 +1,3 @@ -[assembly: CollectionBehavior(DisableTestParallelization = true)] +using Xunit.v3; + +[assembly: Parallelization(Mode = Xunit.Sdk.ParallelMode.None)] diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/AuthServerTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/AuthServerTests.cs index 46245237..419a181e 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration/AuthServerTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/AuthServerTests.cs @@ -1,5 +1,4 @@ using Microsoft.AspNetCore.Mvc.Testing; -using Microsoft.VisualStudio.TestPlatform.TestHost; namespace CodeBeam.UltimateAuth.Tests.Integration; diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/AuthorizationManagementTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/AuthorizationManagementTests.cs index a4171484..73de9bf3 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration/AuthorizationManagementTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/AuthorizationManagementTests.cs @@ -29,9 +29,9 @@ public async Task AssignRole_TargetMismatch_ShouldRejectWithoutMutation() { using var factory = NewFactory(); - var admin = await factory.CreateLoginUserAsync(); - var target = await factory.CreateLoginUserAsync(); - var other = await factory.CreateLoginUserAsync(); + var admin = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var target = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var other = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await GrantAsync(factory, admin, UAuthActions.Authorization.Roles.AssignAdmin); @@ -61,9 +61,9 @@ public async Task RemoveRole_TargetMismatch_ShouldRejectWithoutMutation() { using var factory = NewFactory(); - var admin = await factory.CreateLoginUserAsync(); - var target = await factory.CreateLoginUserAsync(); - var other = await factory.CreateLoginUserAsync(); + var admin = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var target = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var other = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await GrantAsync(factory, admin, UAuthActions.Authorization.Roles.RemoveAdmin); @@ -81,7 +81,7 @@ await GrantAsync(factory, admin, { UserKey = other.UserKey, RoleName = role.Name - }); + }, cancellationToken: TestContext.Current.CancellationToken); response.StatusCode.Should().Be(HttpStatusCode.BadRequest); @@ -97,7 +97,7 @@ public async Task RenameRole_TargetMismatch_ShouldRejectBothTargets() { using var factory = NewFactory(); - var admin = await factory.CreateLoginUserAsync(); + var admin = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await GrantAsync(factory, admin, UAuthActions.Authorization.Roles.RenameAdmin); @@ -113,7 +113,7 @@ await GrantAsync(factory, admin, { Id = other.Id, Name = "unexpected-name" - }); + }, cancellationToken: TestContext.Current.CancellationToken); response.StatusCode.Should().Be(HttpStatusCode.BadRequest); @@ -129,7 +129,7 @@ public async Task SetPermissions_TargetMismatch_ShouldNotModifyEitherRole() { using var factory = NewFactory(); - var admin = await factory.CreateLoginUserAsync(); + var admin = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await GrantAsync(factory, admin, UAuthActions.Authorization.Roles.SetPermissionsAdmin); @@ -145,7 +145,7 @@ await GrantAsync(factory, admin, { RoleId = other.Id, Permissions = [Permission.From("test.resource.write")] - }); + }, cancellationToken: TestContext.Current.CancellationToken); response.StatusCode.Should().Be(HttpStatusCode.BadRequest); @@ -161,7 +161,7 @@ public async Task DeleteRole_TargetMismatch_ShouldPreserveBothRoles() { using var factory = NewFactory(); - var admin = await factory.CreateLoginUserAsync(); + var admin = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await GrantAsync(factory, admin, UAuthActions.Authorization.Roles.DeleteAdmin); @@ -176,7 +176,7 @@ await GrantAsync(factory, admin, new DeleteRoleRequest { Id = other.Id - }); + }, cancellationToken: TestContext.Current.CancellationToken); response.StatusCode.Should().Be(HttpStatusCode.BadRequest); @@ -196,7 +196,7 @@ public async Task CreateRole_ValidRequest_ShouldPersistRole() { using var factory = NewFactory(); - var admin = await factory.CreateLoginUserAsync(); + var admin = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await GrantAsync(factory, admin, UAuthActions.Authorization.Roles.CreateAdmin); @@ -228,7 +228,7 @@ public async Task RenameRole_ValidRequest_ShouldPersistNewName() { using var factory = NewFactory(); - var admin = await factory.CreateLoginUserAsync(); + var admin = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await GrantAsync(factory, admin, UAuthActions.Authorization.Roles.RenameAdmin); @@ -262,7 +262,7 @@ public async Task SetPermissions_ValidRequest_ShouldReplacePermissions() { using var factory = NewFactory(); - var admin = await factory.CreateLoginUserAsync(); + var admin = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await GrantAsync( factory, @@ -309,8 +309,8 @@ public async Task AssignRole_ValidRequest_ShouldPersistAssignment() { using var factory = NewFactory(); - var admin = await factory.CreateLoginUserAsync(); - var target = await factory.CreateLoginUserAsync(); + var admin = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var target = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await GrantAsync(factory, admin, UAuthActions.Authorization.Roles.AssignAdmin); @@ -338,8 +338,8 @@ public async Task RemoveRole_ValidRequest_ShouldRemoveAssignment() { using var factory = NewFactory(); - var admin = await factory.CreateLoginUserAsync(); - var target = await factory.CreateLoginUserAsync(); + var admin = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var target = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await GrantAsync(factory, admin, UAuthActions.Authorization.Roles.RemoveAdmin); @@ -370,8 +370,8 @@ public async Task DeleteRole_ValidRequest_ShouldRemoveAssignments() { using var factory = NewFactory(); - var admin = await factory.CreateLoginUserAsync(); - var target = await factory.CreateLoginUserAsync(); + var admin = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var target = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await GrantAsync(factory, admin, UAuthActions.Authorization.Roles.DeleteAdmin); @@ -412,8 +412,8 @@ public async Task GetUserRoles_ShouldReturnAssignedRole() { using var factory = NewFactory(); - var admin = await factory.CreateLoginUserAsync(); - var target = await factory.CreateLoginUserAsync(); + var admin = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var target = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await GrantAsync(factory, admin, UAuthActions.Authorization.Roles.GetAdmin); @@ -426,12 +426,12 @@ await GrantAsync(factory, admin, using var response = await client.PostAsJsonAsync( UserRolesUrl(target.UserKey, "get"), - new RoleQuery()); + new RoleQuery(), cancellationToken: TestContext.Current.CancellationToken); response.StatusCode.Should().Be(HttpStatusCode.OK); var result = await response.Content - .ReadFromJsonAsync(); + .ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); result.Should().NotBeNull(); result!.UserKey.Should().Be(target.UserKey); @@ -444,7 +444,7 @@ public async Task GetMyRoles_ShouldReturnOwnRole() { using var factory = NewFactory(); - var user = await factory.CreateLoginUserAsync(); + var user = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var role = await SeedRoleAsync(factory); await SeedAssignmentAsync(factory, user.UserKey, role.Id); @@ -453,12 +453,12 @@ public async Task GetMyRoles_ShouldReturnOwnRole() using var response = await client.PostAsJsonAsync( $"{SelfBase}/roles/get", - new RoleQuery()); + new RoleQuery(), cancellationToken: TestContext.Current.CancellationToken); response.StatusCode.Should().Be(HttpStatusCode.OK); var result = await response.Content - .ReadFromJsonAsync(); + .ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); result.Should().NotBeNull(); result!.UserKey.Should().Be(user.UserKey); @@ -471,7 +471,7 @@ public async Task QueryRoles_ShouldReturnCreatedRole() { using var factory = NewFactory(); - var admin = await factory.CreateLoginUserAsync(); + var admin = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await GrantAsync( factory, @@ -484,12 +484,12 @@ await GrantAsync( using var response = await client.PostAsJsonAsync( $"{AdminBase}/roles/query", - new RoleQuery()); + new RoleQuery(), cancellationToken: TestContext.Current.CancellationToken); response.StatusCode.Should().Be(HttpStatusCode.OK); var result = await response.Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(cancellationToken: TestContext.Current.CancellationToken); result.Should().NotBeNull(); @@ -506,7 +506,7 @@ public async Task AssignRole_Anonymous_ShouldReject() { using var factory = NewFactory(); - var target = await factory.CreateLoginUserAsync(); + var target = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var role = await SeedRoleAsync(factory); using var client = CreateClient(factory); @@ -530,8 +530,8 @@ public async Task AssignRole_WithoutPermission_ShouldNotMutate() { using var factory = NewFactory(); - var actor = await factory.CreateLoginUserAsync(); - var target = await factory.CreateLoginUserAsync(); + var actor = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var target = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var role = await SeedRoleAsync(factory); using var client = await AuthenticatedClientAsync(factory, actor); @@ -555,7 +555,7 @@ public async Task CreateRole_WithoutPermission_ShouldNotCreateRole() { using var factory = NewFactory(); - var actor = await factory.CreateLoginUserAsync(); + var actor = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var name = UniqueRoleName(); using var client = await AuthenticatedClientAsync(factory, actor); @@ -579,7 +579,7 @@ public async Task AuthorizationCheck_WithPermission_ShouldAllow() { using var factory = NewFactory(); - var user = await factory.CreateLoginUserAsync(); + var user = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await GrantAsync( factory, @@ -599,7 +599,7 @@ await GrantAsync( response.StatusCode.Should().Be(HttpStatusCode.OK); var result = await response.Content - .ReadFromJsonAsync(); + .ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); result.Should().NotBeNull(); result!.IsAllowed.Should().BeTrue(); @@ -610,7 +610,7 @@ public async Task AuthorizationCheck_WithoutPermission_ShouldReturnDenial() { using var factory = NewFactory(); - var user = await factory.CreateLoginUserAsync(); + var user = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var client = await AuthenticatedClientAsync(factory, user); @@ -625,7 +625,7 @@ public async Task AuthorizationCheck_WithoutPermission_ShouldReturnDenial() response.StatusCode.Should().Be(HttpStatusCode.OK); var result = await response.Content - .ReadFromJsonAsync(); + .ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); result.Should().NotBeNull(); result!.IsAllowed.Should().BeFalse(); @@ -637,9 +637,9 @@ public async Task AssignRole_ServiceTargetMismatch_ShouldReject() { using var factory = NewFactory(); - var admin = await factory.CreateLoginUserAsync(); - var authorizedTarget = await factory.CreateLoginUserAsync(); - var other = await factory.CreateLoginUserAsync(); + var admin = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var authorizedTarget = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var other = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await GrantAsync( factory, @@ -678,9 +678,9 @@ public async Task RemoveRole_ServiceTargetMismatch_ShouldReject() { using var factory = NewFactory(); - var admin = await factory.CreateLoginUserAsync(); - var authorizedTarget = await factory.CreateLoginUserAsync(); - var other = await factory.CreateLoginUserAsync(); + var admin = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var authorizedTarget = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var other = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await GrantAsync( factory, @@ -744,13 +744,13 @@ public async Task RoleStore_ShouldIsolateRolesBetweenTenants() permissions: [], now: factory.Clock.UtcNow); - await storeFactory.Create(tenantA).AddAsync(role); + await storeFactory.Create(tenantA).AddAsync(role, TestContext.Current.CancellationToken); var fromA = await storeFactory.Create(tenantA) - .GetAsync(new RoleKey(tenantA, role.Id)); + .GetAsync(new RoleKey(tenantA, role.Id), TestContext.Current.CancellationToken); var fromB = await storeFactory.Create(tenantB) - .GetAsync(new RoleKey(tenantB, role.Id)); + .GetAsync(new RoleKey(tenantB, role.Id), TestContext.Current.CancellationToken); fromA.Should().NotBeNull(); fromB.Should().BeNull(); @@ -761,7 +761,7 @@ public async Task GetMyPermissions_WhenRolePermissionsChange_ReturnsUpdatedPermi { using var factory = NewFactory(); - var user = await factory.CreateLoginUserAsync(); + var user = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var oldPermission = Permission.From("reports.read"); var newPermission = Permission.From("reports.export"); @@ -785,7 +785,7 @@ await SeedAssignmentAsync( response.StatusCode.Should().Be(HttpStatusCode.OK); var result = await response.Content - .ReadFromJsonAsync(); + .ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); result.Should().NotBeNull(); @@ -801,7 +801,7 @@ await SeedAssignmentAsync( var store = roleStoreFactory.Create(TenantKeys.Single); - var stored = await store.GetAsync(new RoleKey(TenantKeys.Single, role.Id)); + var stored = await store.GetAsync(new RoleKey(TenantKeys.Single, role.Id), TestContext.Current.CancellationToken); stored.Should().NotBeNull(); @@ -815,11 +815,11 @@ await SeedAssignmentAsync( stored.Permissions.Should().Contain(newPermission); stored.Permissions.Should().NotContain(oldPermission); - await store.SaveAsync(stored, expectedVersion); + await store.SaveAsync(stored, expectedVersion, TestContext.Current.CancellationToken); // Check persistence using a fresh read. var saved = await store.GetAsync( - new RoleKey(TenantKeys.Single, role.Id)); + new RoleKey(TenantKeys.Single, role.Id), TestContext.Current.CancellationToken); saved.Should().NotBeNull(); saved!.Permissions.Should().Contain(newPermission); @@ -843,7 +843,7 @@ await SeedAssignmentAsync( // 1. Was the role updated? var updatedRole = await roleStore.GetAsync( - new RoleKey(TenantKeys.Single, role.Id)); + new RoleKey(TenantKeys.Single, role.Id), TestContext.Current.CancellationToken); updatedRole.Should().NotBeNull(); @@ -852,14 +852,14 @@ await SeedAssignmentAsync( // 2. Does the user still have the role? var assignments = await userRoleStore.GetAssignmentsAsync( - user.UserKey); + user.UserKey, TestContext.Current.CancellationToken); assignments.Should().Contain(x => x.RoleId == role.Id); // 3. Can the permission store resolve the updated permission? var permissions = await permissionStore.GetPermissionsAsync( TenantKeys.Single, - user.UserKey); + user.UserKey, TestContext.Current.CancellationToken); permissions.Should().Contain(newPermission); permissions.Should().NotContain(oldPermission); @@ -868,12 +868,12 @@ await SeedAssignmentAsync( // Second request: the new permission must be visible. using (var response = await client.PostAsJsonAsync( $"{SelfBase}/permissions/get", - new { })) + new { }, cancellationToken: TestContext.Current.CancellationToken)) { response.StatusCode.Should().Be(HttpStatusCode.OK); var result = await response.Content - .ReadFromJsonAsync(); + .ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); result.Should().NotBeNull(); @@ -891,7 +891,7 @@ public async Task GetMyPermissions_Anonymous_ShouldReturnUnauthorized() using var response = await client.PostAsJsonAsync( $"{SelfBase}/permissions/get", - new { }); + new { }, cancellationToken: TestContext.Current.CancellationToken); response.StatusCode.Should().Be(HttpStatusCode.Unauthorized); } @@ -901,7 +901,7 @@ public async Task GetMyPermissions_WhenNoRoles_ReturnsEmpty() { using var factory = NewFactory(); - var user = await factory.CreateLoginUserAsync(); + var user = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var client = await AuthenticatedClientAsync(factory, user); @@ -912,7 +912,7 @@ public async Task GetMyPermissions_WhenNoRoles_ReturnsEmpty() response.StatusCode.Should().Be(HttpStatusCode.OK); var result = await response.Content - .ReadFromJsonAsync(); + .ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); result.Should().NotBeNull(); result!.Permissions.Should().BeEmpty(); @@ -923,8 +923,8 @@ public async Task GetMyPermissions_DoesNotReturnOtherUsersPermissions() { using var factory = NewFactory(); - var user = await factory.CreateLoginUserAsync(); - var other = await factory.CreateLoginUserAsync(); + var user = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var other = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var permission = Permission.From("reports.export"); @@ -946,7 +946,7 @@ await SeedAssignmentAsync( response.StatusCode.Should().Be(HttpStatusCode.OK); var result = await response.Content - .ReadFromJsonAsync(); + .ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); result.Should().NotBeNull(); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/ClientRateLimitingIntegrationTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/ClientRateLimitingIntegrationTests.cs index f4f61084..5d6a180a 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration/ClientRateLimitingIntegrationTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/ClientRateLimitingIntegrationTests.cs @@ -26,27 +26,27 @@ public async Task NativeValidation429PreservesClientState_AndLaterRevocationStil { using var factory = AuthServerFactory.Create(o => { o.RateLimiting.Enabled = true; o.RateLimiting.Endpoints[UAuthEndpoint.Validate] = new() { PermitLimit = 1 }; }); - var user = await factory.CreateLoginUserAsync(); + var user = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var http = await Login(factory, user); var transport = new HttpTransport(http); using var services = ClientServices(factory, transport); using var scope = services.CreateScope(); var manager = scope.ServiceProvider.GetRequiredService(); - await manager.EnsureAsync(); + await manager.EnsureAsync(ct: TestContext.Current.CancellationToken); Assert.True(manager.State.IsAuthenticated); var validatedAt = manager.State.LastValidatedAt; - await manager.EnsureAsync(force: true); - await manager.EnsureAsync(force: true); + await manager.EnsureAsync(force: true, ct: TestContext.Current.CancellationToken); + await manager.EnsureAsync(force: true, ct: TestContext.Current.CancellationToken); Assert.True(manager.State.IsAuthenticated); Assert.Equal(validatedAt, manager.State.LastValidatedAt); Assert.Equal(2, transport.Count); using (var serverScope = factory.Services.CreateScope()) { var store = serverScope.ServiceProvider.GetRequiredService().Create(TenantKey.Single); - await store.ExecuteAsync(ct => store.RevokeRootCascadeAsync(user.UserKey, factory.Clock.UtcNow, ct)); + await store.ExecuteAsync(ct => store.RevokeRootCascadeAsync(user.UserKey, factory.Clock.UtcNow, ct), TestContext.Current.CancellationToken); } factory.Clock.Advance(TimeSpan.FromMinutes(2)); - await manager.EnsureAsync(force: true); + await manager.EnsureAsync(force: true, ct: TestContext.Current.CancellationToken); Assert.False(manager.State.IsAuthenticated); } @@ -55,7 +55,7 @@ public async Task NativeRefresh429PreservesCredentials_AndSuppressesImmediateRef { using var factory = AuthServerFactory.Create(o => { o.RateLimiting.Enabled = true; o.RateLimiting.Endpoints[UAuthEndpoint.Refresh] = new() { PermitLimit = 1 }; }); - var user = await factory.CreateLoginUserAsync(); + var user = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var http = await Login(factory, user); var credential = http.DefaultRequestHeaders.GetValues("Cookie").Single(); var transport = new HttpTransport(http); @@ -72,7 +72,7 @@ public async Task NativeRefresh429PreservesCredentials_AndSuppressesImmediateRef Assert.Equal(2, transport.Count); var session = await CsrfTestClient.PostAsync(http, "/auth/validate", null); Assert.Equal(HttpStatusCode.OK, session.StatusCode); - Assert.True((await session.Content.ReadFromJsonAsync())!.IsValid); + Assert.True((await session.Content.ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken))!.IsValid); } private static async Task Login(AuthServerFactory factory, IntegrationTestUser user) diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/CodeBeam.UltimateAuth.Tests.Integration.csproj b/tests/CodeBeam.UltimateAuth.Tests.Integration/CodeBeam.UltimateAuth.Tests.Integration.csproj index b876ff76..b6a5b7a8 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration/CodeBeam.UltimateAuth.Tests.Integration.csproj +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/CodeBeam.UltimateAuth.Tests.Integration.csproj @@ -1,22 +1,18 @@  - net10.0 enable enable false - - - - - - - - + + + + + diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/CredentialManagementTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/CredentialManagementTests.cs index fbe24202..1075fc27 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration/CredentialManagementTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/CredentialManagementTests.cs @@ -33,7 +33,7 @@ public async Task AnonymousReset_WithValidToken_ChangesPassword() factory.Clock.Reset(); - var user = await factory.CreateLoginUserAsync(); + var user = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var client = CreateClient(factory); @@ -44,7 +44,7 @@ public async Task AnonymousReset_WithValidToken_ChangesPassword() Identifier = user.Identifier, CredentialType = CredentialType.Password, ResetCodeType = ResetCodeType.Token - }); + }, cancellationToken: TestContext.Current.CancellationToken); beginResponse.StatusCode.Should().Be(HttpStatusCode.OK); @@ -63,7 +63,7 @@ public async Task AnonymousReset_WithValidToken_ChangesPassword() CredentialType = CredentialType.Password, ResetToken = notification.Token, NewSecret = newPassword - }); + }, cancellationToken: TestContext.Current.CancellationToken); completeResponse.StatusCode.Should().Be(HttpStatusCode.OK); @@ -78,7 +78,7 @@ public async Task AnonymousReset_WithValidToken_ChangesPassword() var store = storeFactory.Create(notification.Tenant); - var credentials = await store.GetByUserAsync(user.UserKey); + var credentials = await store.GetByUserAsync(user.UserKey, TestContext.Current.CancellationToken); var password = credentials .OfType() @@ -119,13 +119,13 @@ public async Task AdminBeginReset_TargetMismatch_ShouldRejectWithoutNotification using var factory = CreateFactory(notifier); factory.Clock.Reset(); - var admin = await factory.CreateLoginUserAsync(); - var target = await factory.CreateLoginUserAsync(); - var other = await factory.CreateLoginUserAsync(); + var admin = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var target = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var other = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await factory.GrantPermissionsAsync( admin.UserKey, - [UAuthActions.Credentials.BeginResetAdmin]); + [UAuthActions.Credentials.BeginResetAdmin], TestContext.Current.CancellationToken); using var client = CreateClient(factory); await AuthenticateAsync(client, admin); @@ -149,12 +149,12 @@ public async Task AdminBeginReset_ValidTarget_ShouldNotifyTarget() using var factory = CreateFactory(notifier); factory.Clock.Reset(); - var admin = await factory.CreateLoginUserAsync(); - var target = await factory.CreateLoginUserAsync(); + var admin = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var target = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await factory.GrantPermissionsAsync( admin.UserKey, - [UAuthActions.Credentials.BeginResetAdmin]); + [UAuthActions.Credentials.BeginResetAdmin], TestContext.Current.CancellationToken); using var client = CreateClient(factory); await AuthenticateAsync(client, admin); @@ -167,7 +167,7 @@ await factory.GrantPermissionsAsync( response.StatusCode.Should().Be(HttpStatusCode.OK); var result = await response.Content - .ReadFromJsonAsync(); + .ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); result.Should().NotBeNull(); result!.ExpiresAt.Should().BeAfter(factory.Clock.UtcNow); @@ -175,7 +175,7 @@ await factory.GrantPermissionsAsync( notifier.Notifications.Should() .ContainSingle(x => x.UserKey == target.UserKey); - var body = await response.Content.ReadAsStringAsync(); + var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); body.Should().NotContain( notifier.Notifications.Single().Token); @@ -189,13 +189,13 @@ public async Task AdminCompleteReset_TargetMismatch_ShouldNotChangeOtherPassword using var factory = CreateFactory(notifier); factory.Clock.Reset(); - var admin = await factory.CreateLoginUserAsync(); - var target = await factory.CreateLoginUserAsync(); - var other = await factory.CreateLoginUserAsync(); + var admin = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var target = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var other = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await factory.GrantPermissionsAsync( admin.UserKey, - [UAuthActions.Credentials.CompleteResetAdmin]); + [UAuthActions.Credentials.CompleteResetAdmin], TestContext.Current.CancellationToken); // B için gerçekten geçerli bir reset token'ı üret. using var anonymousClient = CreateClient(factory); @@ -234,15 +234,15 @@ public async Task AdminCompleteReset_ValidTarget_ShouldChangePassword() using var factory = CreateFactory(notifier); factory.Clock.Reset(); - var admin = await factory.CreateLoginUserAsync(); - var target = await factory.CreateLoginUserAsync(); + var admin = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var target = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await factory.GrantPermissionsAsync( admin.UserKey, [ UAuthActions.Credentials.BeginResetAdmin, UAuthActions.Credentials.CompleteResetAdmin - ]); + ], TestContext.Current.CancellationToken); using var client = CreateClient(factory); await AuthenticateAsync(client, admin); @@ -257,7 +257,7 @@ await factory.GrantPermissionsAsync( begin.StatusCode.Should().Be(HttpStatusCode.OK); var beginResult = await begin.Content - .ReadFromJsonAsync(); + .ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); beginResult.Should().NotBeNull(); @@ -277,7 +277,7 @@ await factory.GrantPermissionsAsync( complete.StatusCode.Should().Be(HttpStatusCode.OK); var completeResult = await complete.Content - .ReadFromJsonAsync(); + .ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); completeResult.Should().NotBeNull(); completeResult!.IsSuccess.Should().BeTrue(); @@ -294,7 +294,7 @@ public async Task AnonymousCompleteReset_ReusedToken_ShouldReject() using var factory = CreateFactory(notifier); factory.Clock.Reset(); - var user = await factory.CreateLoginUserAsync(); + var user = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var client = CreateClient(factory); @@ -340,8 +340,8 @@ public async Task AdminBeginReset_WithoutPermission_ShouldNotNotify() using var factory = CreateFactory(notifier); factory.Clock.Reset(); - var actor = await factory.CreateLoginUserAsync(); - var target = await factory.CreateLoginUserAsync(); + var actor = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var target = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var client = CreateClient(factory); await AuthenticateAsync(client, actor); @@ -363,7 +363,7 @@ public async Task AdminBeginReset_Unauthenticated_ShouldReject() using var factory = CreateFactory(notifier); factory.Clock.Reset(); - var target = await factory.CreateLoginUserAsync(); + var target = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var client = CreateClient(factory); @@ -385,7 +385,7 @@ public async Task AnonymousCompleteReset_InvalidProof_ShouldNotRevealIdentifierE using var factory = CreateFactory(notifier); factory.Clock.Reset(); - var existingUser = await factory.CreateLoginUserAsync(); + var existingUser = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var client = CreateClient(factory); @@ -411,8 +411,8 @@ public async Task AnonymousCompleteReset_InvalidProof_ShouldNotRevealIdentifierE .Be(unknownResponse.StatusCode); // Same observable response body. - var existingBody = await existingResponse.Content.ReadAsStringAsync(); - var unknownBody = await unknownResponse.Content.ReadAsStringAsync(); + var existingBody = await existingResponse.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); + var unknownBody = await unknownResponse.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); existingBody.Should().Be(unknownBody); @@ -431,7 +431,7 @@ public async Task AnonymousRecovery_DefaultPolicy_RevokesAllExistingChains() using var factory = CreateFactory(notifier); factory.Clock.Reset(); - var user = await factory.CreateLoginUserAsync(); + var user = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); // Two independent clients with different X-UDID values. using var firstClient = CreateClient(factory); @@ -448,7 +448,7 @@ public async Task AnonymousRecovery_DefaultPolicy_RevokesAllExistingChains() var store = sessionFactory.Create(TenantKeys.Single); - var chains = await store.GetChainsByUserAsync(user.UserKey); + var chains = await store.GetChainsByUserAsync(user.UserKey, ct: TestContext.Current.CancellationToken); chains.Should().HaveCount(2); chains.Should().OnlyContain(c => !c.IsRevoked); @@ -491,7 +491,7 @@ public async Task AnonymousRecovery_DefaultPolicy_RevokesAllExistingChains() var store = sessionFactory.Create(TenantKeys.Single); - var chains = await store.GetChainsByUserAsync(user.UserKey); + var chains = await store.GetChainsByUserAsync(user.UserKey, ct: TestContext.Current.CancellationToken); chains.Should().HaveCount(2); chains.Should().OnlyContain(c => c.IsRevoked); @@ -518,7 +518,7 @@ public async Task Recovery_ShouldRespectConfiguredSessionPolicy(CredentialSessio factory.Clock.Reset(); - var user = await factory.CreateLoginUserAsync(); + var user = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var firstClient = CreateClient(factory); using var secondClient = CreateClient(factory); @@ -578,8 +578,8 @@ public async Task Recovery_RevokeAll_ShouldNotAffectOtherUsers() using var factory = CreateFactory(notifier); factory.Clock.Reset(); - var target = await factory.CreateLoginUserAsync(); - var other = await factory.CreateLoginUserAsync(); + var target = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var other = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var targetClient = CreateClient(factory); using var otherClient = CreateClient(factory); @@ -630,7 +630,7 @@ public async Task Recovery_InvalidToken_ShouldNotRevokeExistingChains() using var factory = CreateFactory(notifier); factory.Clock.Reset(); - var user = await factory.CreateLoginUserAsync(); + var user = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var authenticatedClient = CreateClient(factory); await AuthenticateAsync(authenticatedClient, user); @@ -675,7 +675,7 @@ public async Task SelfChange_ShouldRespectConfiguredSessionPolicy(CredentialSess factory.Clock.Reset(); - var user = await factory.CreateLoginUserAsync(); + var user = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var currentClient = CreateClient(factory); using var otherClient = CreateClient(factory); @@ -731,12 +731,12 @@ public async Task AdminChange_ShouldRespectConfiguredSessionPolicy(CredentialSes factory.Clock.Reset(); - var admin = await factory.CreateLoginUserAsync(); - var target = await factory.CreateLoginUserAsync(); + var admin = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var target = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await factory.GrantPermissionsAsync( admin.UserKey, - [UAuthActions.Credentials.ChangeAdmin]); + [UAuthActions.Credentials.ChangeAdmin], TestContext.Current.CancellationToken); using var adminClient = CreateClient(factory); using var targetClient1 = CreateClient(factory); @@ -782,7 +782,7 @@ public async Task AnonymousBeginReset_AlreadyActive_ShouldNotRevealResetState() using var factory = CreateFactory(notifier); factory.Clock.Reset(); - var user = await factory.CreateLoginUserAsync(); + var user = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var client = CreateClient(factory); @@ -807,8 +807,8 @@ public async Task AnonymousBeginReset_AlreadyActive_ShouldNotRevealResetState() active.StatusCode.Should().Be(unknown.StatusCode); - (await active.Content.ReadAsStringAsync()) - .Should().Be(await unknown.Content.ReadAsStringAsync()); + (await active.Content.ReadAsStringAsync(TestContext.Current.CancellationToken)) + .Should().Be(await unknown.Content.ReadAsStringAsync(TestContext.Current.CancellationToken)); notifier.Notifications.Should().HaveCount(notificationCount); } @@ -821,7 +821,7 @@ public async Task AnonymousCompleteReset_ExpiredToken_ShouldNotRevealResetState( using var factory = CreateFactory(notifier); factory.Clock.Reset(); - var user = await factory.CreateLoginUserAsync(); + var user = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var client = CreateClient(factory); @@ -854,8 +854,8 @@ public async Task AnonymousCompleteReset_ExpiredToken_ShouldNotRevealResetState( expired.StatusCode.Should().Be(unknown.StatusCode); - (await expired.Content.ReadAsStringAsync()) - .Should().Be(await unknown.Content.ReadAsStringAsync()); + (await expired.Content.ReadAsStringAsync(TestContext.Current.CancellationToken)) + .Should().Be(await unknown.Content.ReadAsStringAsync(TestContext.Current.CancellationToken)); await AssertPasswordAsync(factory, user, user.Secret); } @@ -868,7 +868,7 @@ public async Task AnonymousBeginReset_InvalidCodeType_ShouldRejectConsistently() using var factory = CreateFactory(notifier); factory.Clock.Reset(); - var user = await factory.CreateLoginUserAsync(); + var user = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var client = CreateClient(factory); @@ -892,8 +892,8 @@ async Task SendAsync(string identifier) existing.StatusCode.Should().Be(HttpStatusCode.BadRequest); unknown.StatusCode.Should().Be(HttpStatusCode.BadRequest); - using var existingJson = JsonDocument.Parse(await existing.Content.ReadAsStringAsync()); - using var unknownJson = JsonDocument.Parse(await unknown.Content.ReadAsStringAsync()); + using var existingJson = JsonDocument.Parse(await existing.Content.ReadAsStringAsync(TestContext.Current.CancellationToken)); + using var unknownJson = JsonDocument.Parse(await unknown.Content.ReadAsStringAsync(TestContext.Current.CancellationToken)); existingJson.RootElement.GetProperty("status").GetInt32() .Should().Be(unknownJson.RootElement.GetProperty("status").GetInt32()); @@ -915,7 +915,7 @@ public async Task AnonymousCompleteReset_SamePassword_ShouldNotRevealCredentialS using var factory = CreateFactory(notifier); factory.Clock.Reset(); - var user = await factory.CreateLoginUserAsync(); + var user = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var client = CreateClient(factory); @@ -944,8 +944,8 @@ public async Task AnonymousCompleteReset_SamePassword_ShouldNotRevealCredentialS samePassword.StatusCode.Should().Be(unknown.StatusCode); - (await samePassword.Content.ReadAsStringAsync()) - .Should().Be(await unknown.Content.ReadAsStringAsync()); + (await samePassword.Content.ReadAsStringAsync(TestContext.Current.CancellationToken)) + .Should().Be(await unknown.Content.ReadAsStringAsync(TestContext.Current.CancellationToken)); await AssertPasswordAsync(factory, user, user.Secret); } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/HttpContextJsonContractTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/HttpContextJsonContractTests.cs index 22ea0305..47f833fb 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration/HttpContextJsonContractTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/HttpContextJsonContractTests.cs @@ -65,7 +65,7 @@ public async Task InvalidJsonRequest_ShouldReturn400ProblemDetails( request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); - using var response = await host.Client.SendAsync(request); + using var response = await host.Client.SendAsync(request, TestContext.Current.CancellationToken); response.StatusCode.Should().Be(HttpStatusCode.BadRequest); @@ -73,7 +73,7 @@ public async Task InvalidJsonRequest_ShouldReturn400ProblemDetails( .MediaType.Should().Be("application/problem+json"); var problem = await response.Content - .ReadFromJsonAsync(); + .ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); problem.Should().NotBeNull(); problem!.Status.Should().Be(400); @@ -89,12 +89,12 @@ public async Task ValidJsonRequest_ShouldReturn200WithTypedResult() using var response = await host.Client.PostAsJsonAsync( Endpoint, - new { name = "UltimateAuth" }); + new { name = "UltimateAuth" }, cancellationToken: TestContext.Current.CancellationToken); response.StatusCode.Should().Be(HttpStatusCode.OK); var result = await response.Content - .ReadFromJsonAsync(); + .ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); result.Should().NotBeNull(); result!.Name.Should().Be("UltimateAuth"); @@ -113,12 +113,12 @@ public async Task MissingContentType_ShouldReturn400ProblemDetails() Encoding.UTF8.GetBytes("""{"name":"test"}""")) }; - using var response = await host.Client.SendAsync(request); + using var response = await host.Client.SendAsync(request, TestContext.Current.CancellationToken); response.StatusCode.Should().Be(HttpStatusCode.BadRequest); var problem = await response.Content - .ReadFromJsonAsync(); + .ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); problem!.Detail.Should().Be("request_content_type_invalid"); } @@ -130,12 +130,12 @@ public async Task JsonReader_ShouldRewindBodyAfterSuccess() using var response = await host.Client.PostAsJsonAsync( "/test/json-rewind", - new { name = "UltimateAuth" }); + new { name = "UltimateAuth" }, cancellationToken: TestContext.Current.CancellationToken); response.StatusCode.Should().Be(HttpStatusCode.OK); var result = await response.Content - .ReadFromJsonAsync(); + .ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); result.Should().NotBeNull(); result!.Name.Should().Be("UltimateAuth"); @@ -161,7 +161,7 @@ public async Task InvalidPermission_ShouldReturn400ProblemDetails() using var response = await host.Client.PostAsync( "/test/json-permission", - content); + content, TestContext.Current.CancellationToken); response.StatusCode.Should().Be(HttpStatusCode.BadRequest); @@ -169,7 +169,7 @@ public async Task InvalidPermission_ShouldReturn400ProblemDetails() .MediaType.Should().Be("application/problem+json"); var problem = await response.Content - .ReadFromJsonAsync(); + .ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); problem.Should().NotBeNull(); problem!.Status.Should().Be(400); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/LoginCsrfExploitProofTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/LoginCsrfExploitProofTests.cs index dc7374bd..13f6b469 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration/LoginCsrfExploitProofTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/LoginCsrfExploitProofTests.cs @@ -14,7 +14,7 @@ public async Task CrossOriginLogin_WithoutCsrfProof_MustNotAuthenticateVictimAsA { await using var factory = new AuthServerFactory(); - var attacker = await factory.CreateLoginUserAsync(); + var attacker = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var deviceId = $"csrf-victim-{Guid.NewGuid():N}"; using var victim = CreateBrowser(factory, deviceId); @@ -38,9 +38,9 @@ public async Task CrossOriginLogin_WithoutCsrfProof_MustNotAuthenticateVictimAsA "Origin", "https://localhost:6130"); - using var response = await victim.SendAsync(request); + using var response = await victim.SendAsync(request, TestContext.Current.CancellationToken); - var body = await response.Content.ReadAsStringAsync(); + var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); // Prove that the rejection came from CSRF validation. response.StatusCode.Should().Be( diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/LoginTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/LoginTests.cs index 60dc1bfd..5cdc5fc4 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration/LoginTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/LoginTests.cs @@ -9,23 +9,21 @@ namespace CodeBeam.UltimateAuth.Tests.Integration; -public class LoginTests : IClassFixture +public class LoginTests : IDisposable { private const string LoginEndpoint = "/auth/login"; private const string ValidIdentifier = "admin"; private const string ValidSecret = "admin"; - private readonly AuthServerFactory _factory; + // Each test owns its host so clock changes and account state cannot leak. + private readonly AuthServerFactory _factory = new(); - public LoginTests(AuthServerFactory factory) - { - _factory = factory; - } + public void Dispose() => _factory.Dispose(); [Fact] public async Task Login_WithValidCredentials_ShouldIssueSessionCredential() { - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var client = CreateClient($"valid-session-{Guid.NewGuid():N}"); @@ -49,7 +47,7 @@ public async Task Login_WithValidCredentials_ShouldIssueSessionCredential() [Fact] public async Task Login_WithValidCredentials_ShouldCreateUsableAuthenticatedSession() { - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var deviceId = $"usable-session-{Guid.NewGuid():N}"; using var client = CreateClient(deviceId); var loginResponse = await LoginAsync(client, user.Identifier, user.Secret); @@ -70,7 +68,7 @@ public async Task Login_WithValidCredentials_ShouldCreateUsableAuthenticatedSess response.StatusCode.Should().Be(HttpStatusCode.OK); - var result = await response.Content.ReadFromJsonAsync>(); + var result = await response.Content.ReadFromJsonAsync>(cancellationToken: TestContext.Current.CancellationToken); result.Should().NotBeNull(); result!.Items.Should().NotBeEmpty(); @@ -239,7 +237,7 @@ public async Task Login_AfterLockoutExpires_ShouldAllowCorrectCredentials() { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var client = CreateClient( $"lockout-expiry-{Guid.NewGuid():N}"); @@ -284,7 +282,7 @@ await LoginAsync( [Fact] public async Task TryLogin_WithValidCredentials_ShouldReturnSuccessfulPreview() { - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var client = CreateClient( $"try-login-valid-{Guid.NewGuid():N}"); @@ -300,7 +298,7 @@ public async Task TryLogin_WithValidCredentials_ShouldReturnSuccessfulPreview() response.StatusCode.Should().Be(HttpStatusCode.OK); var result = - await response.Content.ReadFromJsonAsync(); + await response.Content.ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); result.Should().NotBeNull(); @@ -327,7 +325,7 @@ public async Task TryLogin_WithValidCredentials_ShouldNotAuthenticateUser() response.StatusCode.Should().Be(HttpStatusCode.OK); - var result = await response.Content.ReadFromJsonAsync(); + var result = await response.Content.ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); result.Should().NotBeNull(); result!.IsSuccess.Should().BeTrue(); @@ -337,7 +335,7 @@ public async Task TryLogin_WithValidCredentials_ShouldNotAuthenticateUser() new GetProfileRequest { ProfileKey = null - }); + }, cancellationToken: TestContext.Current.CancellationToken); meResponse.StatusCode.Should().Be(HttpStatusCode.Unauthorized); } @@ -345,7 +343,7 @@ public async Task TryLogin_WithValidCredentials_ShouldNotAuthenticateUser() [Fact] public async Task TryLogin_WithInvalidCredentials_ShouldReturnFailedPreviewWithoutSession() { - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var client = CreateClient( $"try-invalid-{Guid.NewGuid():N}"); @@ -361,7 +359,7 @@ public async Task TryLogin_WithInvalidCredentials_ShouldReturnFailedPreviewWitho response.StatusCode.Should().Be(HttpStatusCode.OK); var result = - await response.Content.ReadFromJsonAsync(); + await response.Content.ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); result.Should().NotBeNull(); result!.IsSuccess.Should().BeFalse(); @@ -387,7 +385,7 @@ public async Task TryLogin_WithMissingCredentials_ShouldReturnFailedPreview() response.StatusCode.Should().Be(HttpStatusCode.OK); - var result = await response.Content.ReadFromJsonAsync(); + var result = await response.Content.ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); result.Should().NotBeNull(); result!.IsSuccess.Should().BeFalse(); @@ -398,7 +396,7 @@ public async Task TryLogin_WithMissingCredentials_ShouldReturnFailedPreview() [Fact] public async Task Login_WithValidPreviewReceipt_ShouldAuthenticateUser() { - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var client = CreateClient( $"preview-valid-{Guid.NewGuid():N}"); @@ -414,7 +412,7 @@ public async Task Login_WithValidPreviewReceipt_ShouldAuthenticateUser() previewResponse.StatusCode.Should().Be(HttpStatusCode.OK); var preview = - await previewResponse.Content.ReadFromJsonAsync(); + await previewResponse.Content.ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); preview.Should().NotBeNull(); preview!.IsSuccess.Should().BeTrue(); @@ -441,7 +439,7 @@ public async Task Login_WithValidPreviewReceipt_ShouldAuthenticateUser() [Fact] public async Task Login_WithPreviewReceiptFromDifferentDevice_ShouldNotTrustReceipt() { - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var previewClient = CreateClient( $"receipt-owner-{Guid.NewGuid():N}"); @@ -457,7 +455,7 @@ public async Task Login_WithPreviewReceiptFromDifferentDevice_ShouldNotTrustRece previewResponse.StatusCode.Should().Be(HttpStatusCode.OK); var preview = await previewResponse.Content - .ReadFromJsonAsync(); + .ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); preview.Should().NotBeNull(); preview!.IsSuccess.Should().BeTrue(); @@ -474,7 +472,7 @@ public async Task Login_WithPreviewReceiptFromDifferentDevice_ShouldNotTrustRece identifier = user.Identifier, secret = "wrong-password", previewReceipt = preview.PreviewReceipt - }); + }, cancellationToken: TestContext.Current.CancellationToken); response.Headers .TryGetValues("Set-Cookie", out _) @@ -484,7 +482,7 @@ public async Task Login_WithPreviewReceiptFromDifferentDevice_ShouldNotTrustRece [Fact] public async Task Login_WithPreviewReceiptAndDifferentSecret_ShouldNotAuthenticate() { - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var client = CreateClient( $"preview-different-secret-{Guid.NewGuid():N}"); @@ -498,7 +496,7 @@ public async Task Login_WithPreviewReceiptAndDifferentSecret_ShouldNotAuthentica }); var preview = - await previewResponse.Content.ReadFromJsonAsync(); + await previewResponse.Content.ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); preview.Should().NotBeNull(); preview!.IsSuccess.Should().BeTrue(); @@ -511,7 +509,7 @@ public async Task Login_WithPreviewReceiptAndDifferentSecret_ShouldNotAuthentica identifier = user.Identifier, secret = "different-secret", previewReceipt = preview.PreviewReceipt - }); + }, cancellationToken: TestContext.Current.CancellationToken); loginResponse.Headers .TryGetValues("Set-Cookie", out _) @@ -521,8 +519,8 @@ public async Task Login_WithPreviewReceiptAndDifferentSecret_ShouldNotAuthentica [Fact] public async Task Login_WithPreviewReceiptAndDifferentIdentifier_ShouldFallBackToNormalAuthentication() { - var receiptOwner = await _factory.CreateLoginUserAsync(); - var otherUser = await _factory.CreateLoginUserAsync(); + var receiptOwner = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var otherUser = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var client = CreateClient( $"preview-different-identifier-{Guid.NewGuid():N}"); @@ -539,7 +537,7 @@ public async Task Login_WithPreviewReceiptAndDifferentIdentifier_ShouldFallBackT previewResponse.StatusCode.Should().Be(HttpStatusCode.OK); var preview = - await previewResponse.Content.ReadFromJsonAsync(); + await previewResponse.Content.ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); preview.Should().NotBeNull(); preview!.IsSuccess.Should().BeTrue(); @@ -573,8 +571,8 @@ public async Task Login_WithPreviewReceiptAndDifferentIdentifier_ShouldFallBackT [Fact] public async Task Login_WithPreviewReceiptFromDifferentIdentifier_ShouldNotBypassCredentialValidation() { - var receiptOwner = await _factory.CreateLoginUserAsync(); - var otherUser = await _factory.CreateLoginUserAsync(); + var receiptOwner = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var otherUser = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var client = CreateClient( $"preview-cross-user-{Guid.NewGuid():N}"); @@ -588,7 +586,7 @@ public async Task Login_WithPreviewReceiptFromDifferentIdentifier_ShouldNotBypas }); var preview = - await previewResponse.Content.ReadFromJsonAsync(); + await previewResponse.Content.ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); preview.Should().NotBeNull(); preview!.IsSuccess.Should().BeTrue(); @@ -613,7 +611,7 @@ public async Task Login_WithPreviewReceiptFromDifferentIdentifier_ShouldNotBypas [Fact] public async Task Login_WithUnknownPreviewReceipt_ShouldFallBackToNormalLoginValidation() { - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var client = CreateClient( $"unknown-receipt-{Guid.NewGuid():N}"); @@ -639,7 +637,7 @@ public async Task Login_WithUnknownPreviewReceipt_ShouldFallBackToNormalLoginVal [Fact] public async Task PreviewReceipt_AfterSuccessfulCommit_ShouldBeConsumed() { - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var client = CreateClient( $"preview-consume-{Guid.NewGuid():N}"); @@ -653,7 +651,7 @@ public async Task PreviewReceipt_AfterSuccessfulCommit_ShouldBeConsumed() }); var preview = - await previewResponse.Content.ReadFromJsonAsync(); + await previewResponse.Content.ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); preview.Should().NotBeNull(); preview!.IsSuccess.Should().BeTrue(); @@ -706,7 +704,7 @@ public async Task Login_WithRepeatedInvalidCredentials_ShouldLockAccount() { identifier = ValidIdentifier, secret = "wrong-password-1" - }); + }, cancellationToken: TestContext.Current.CancellationToken); first.Headers .TryGetValues("Set-Cookie", out _) @@ -731,7 +729,7 @@ public async Task Login_WithRepeatedInvalidCredentials_ShouldLockAccount() { identifier = ValidIdentifier, secret = ValidSecret - }); + }, cancellationToken: TestContext.Current.CancellationToken); correctLogin.Headers .TryGetValues("Set-Cookie", out _) @@ -753,7 +751,7 @@ public async Task TryLogin_WithRepeatedInvalidCredentials_ShouldParticipateInLoc }); var firstResult = - await first.Content.ReadFromJsonAsync(); + await first.Content.ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); firstResult.Should().NotBeNull(); firstResult!.IsSuccess.Should().BeFalse(); @@ -766,7 +764,7 @@ public async Task TryLogin_WithRepeatedInvalidCredentials_ShouldParticipateInLoc secret = "wrong-password-2" }); - var secondResult = await second.Content.ReadFromJsonAsync(); + var secondResult = await second.Content.ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); secondResult.Should().NotBeNull(); secondResult!.IsSuccess.Should().BeFalse(); @@ -781,7 +779,7 @@ public async Task TryLogin_WithRepeatedInvalidCredentials_ShouldParticipateInLoc }); var correctResult = - await correctAttempt.Content.ReadFromJsonAsync(); + await correctAttempt.Content.ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); correctResult.Should().NotBeNull(); correctResult!.IsSuccess.Should().BeFalse(); @@ -791,7 +789,7 @@ public async Task TryLogin_WithRepeatedInvalidCredentials_ShouldParticipateInLoc [Fact] public async Task TryLogin_WithValidCredentials_ShouldNotConsumeFailureAttempt() { - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var client = CreateClient( $"try-no-failure-{Guid.NewGuid():N}"); @@ -805,7 +803,7 @@ public async Task TryLogin_WithValidCredentials_ShouldNotConsumeFailureAttempt() }); var previewResult = - await preview.Content.ReadFromJsonAsync(); + await preview.Content.ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); previewResult.Should().NotBeNull(); previewResult!.IsSuccess.Should().BeTrue(); @@ -819,7 +817,7 @@ public async Task TryLogin_WithValidCredentials_ShouldNotConsumeFailureAttempt() }); var failureResult = - await failure.Content.ReadFromJsonAsync(); + await failure.Content.ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); failureResult.Should().NotBeNull(); failureResult!.IsSuccess.Should().BeFalse(); @@ -829,7 +827,7 @@ public async Task TryLogin_WithValidCredentials_ShouldNotConsumeFailureAttempt() [Fact] public async Task PreviewReceipt_WithDifferentSecret_ShouldNotSuppressFailureAccounting() { - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var client = CreateClient( $"receipt-accounting-{Guid.NewGuid():N}"); @@ -845,7 +843,7 @@ public async Task PreviewReceipt_WithDifferentSecret_ShouldNotSuppressFailureAcc previewResponse.StatusCode.Should().Be(HttpStatusCode.OK); var preview = - await previewResponse.Content.ReadFromJsonAsync(); + await previewResponse.Content.ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); preview.Should().NotBeNull(); preview!.IsSuccess.Should().BeTrue(); @@ -860,7 +858,7 @@ public async Task PreviewReceipt_WithDifferentSecret_ShouldNotSuppressFailureAcc identifier = user.Identifier, secret = "wrong-password-1", previewReceipt = preview.PreviewReceipt - }); + }, cancellationToken: TestContext.Current.CancellationToken); firstFailure.Headers .TryGetValues("Set-Cookie", out _) @@ -874,7 +872,7 @@ public async Task PreviewReceipt_WithDifferentSecret_ShouldNotSuppressFailureAcc { identifier = user.Identifier, secret = "wrong-password-2" - }); + }, cancellationToken: TestContext.Current.CancellationToken); secondFailure.Headers .TryGetValues("Set-Cookie", out _) @@ -887,7 +885,7 @@ public async Task PreviewReceipt_WithDifferentSecret_ShouldNotSuppressFailureAcc { identifier = user.Identifier, secret = user.Secret - }); + }, cancellationToken: TestContext.Current.CancellationToken); correctLogin.Headers .TryGetValues("Set-Cookie", out _) @@ -897,7 +895,7 @@ public async Task PreviewReceipt_WithDifferentSecret_ShouldNotSuppressFailureAcc [Fact] public async Task PreviewReceipt_FromDifferentDevice_ShouldNotSuppressFailureAccounting() { - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var receiptClient = CreateClient( $"receipt-device-a-{Guid.NewGuid():N}"); @@ -911,7 +909,7 @@ public async Task PreviewReceipt_FromDifferentDevice_ShouldNotSuppressFailureAcc }); var preview = - await previewResponse.Content.ReadFromJsonAsync(); + await previewResponse.Content.ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); preview.Should().NotBeNull(); preview!.IsSuccess.Should().BeTrue(); @@ -929,7 +927,7 @@ public async Task PreviewReceipt_FromDifferentDevice_ShouldNotSuppressFailureAcc identifier = user.Identifier, secret = "wrong-password-1", previewReceipt = preview.PreviewReceipt - }); + }, cancellationToken: TestContext.Current.CancellationToken); firstFailure.Headers .TryGetValues("Set-Cookie", out _) @@ -943,7 +941,7 @@ public async Task PreviewReceipt_FromDifferentDevice_ShouldNotSuppressFailureAcc { identifier = user.Identifier, secret = "wrong-password-2" - }); + }, cancellationToken: TestContext.Current.CancellationToken); secondFailure.Headers .TryGetValues("Set-Cookie", out _) @@ -955,7 +953,7 @@ public async Task PreviewReceipt_FromDifferentDevice_ShouldNotSuppressFailureAcc { identifier = user.Identifier, secret = user.Secret - }); + }, cancellationToken: TestContext.Current.CancellationToken); correctLogin.Headers .TryGetValues("Set-Cookie", out _) @@ -965,7 +963,7 @@ public async Task PreviewReceipt_FromDifferentDevice_ShouldNotSuppressFailureAcc [Fact] public async Task SuccessfulLogin_ShouldResetPreviousFailureAccounting() { - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var client = CreateClient( $"success-reset-{Guid.NewGuid():N}"); @@ -1008,7 +1006,7 @@ public async Task SuccessfulLogin_ShouldResetPreviousFailureAccounting() secret = "wrong-password-2" }); - var result = await failureAfterSuccess.Content.ReadFromJsonAsync(); + var result = await failureAfterSuccess.Content.ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); result.Should().NotBeNull(); result!.IsSuccess.Should().BeFalse(); @@ -1027,7 +1025,7 @@ await attackerDevice.PostAsJsonAsync( { identifier = ValidIdentifier, secret = "wrong-password-1" - }); + }, cancellationToken: TestContext.Current.CancellationToken); await attackerDevice.PostAsJsonAsync( "/auth/login", @@ -1035,7 +1033,7 @@ await attackerDevice.PostAsJsonAsync( { identifier = ValidIdentifier, secret = "wrong-password-2" - }); + }, cancellationToken: TestContext.Current.CancellationToken); using var differentDevice = CreateClient( "lockout-other-device-000000000000000"); @@ -1046,7 +1044,7 @@ await attackerDevice.PostAsJsonAsync( { identifier = ValidIdentifier, secret = ValidSecret - }); + }, cancellationToken: TestContext.Current.CancellationToken); response.Headers .TryGetValues("Set-Cookie", out _) @@ -1058,7 +1056,7 @@ await attackerDevice.PostAsJsonAsync( [Fact] public async Task Login_WithFormPayload_ShouldAuthenticateUser() { - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var client = CreateClient( $"form-login-{Guid.NewGuid():N}"); @@ -1086,7 +1084,7 @@ public async Task Login_WithFormPayload_ShouldAuthenticateUser() [Fact] public async Task TryLogin_WithFormPayload_ShouldReturnSuccessfulPreview() { - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var client = CreateClient( $"form-try-login-{Guid.NewGuid():N}"); @@ -1105,7 +1103,7 @@ public async Task TryLogin_WithFormPayload_ShouldReturnSuccessfulPreview() response.StatusCode.Should().Be(HttpStatusCode.OK); var result = - await response.Content.ReadFromJsonAsync(); + await response.Content.ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); result.Should().NotBeNull(); result!.IsSuccess.Should().BeTrue(); @@ -1161,7 +1159,7 @@ public async Task TryLogin_WithMissingOrWhitespaceCredentials_ShouldReturnInvali response.StatusCode.Should().Be(HttpStatusCode.OK); - var result = await response.Content.ReadFromJsonAsync(); + var result = await response.Content.ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); result.Should().NotBeNull(); result!.IsSuccess.Should().BeFalse(); @@ -1180,7 +1178,7 @@ public async Task TryLogin_WithUnsupportedContentType_ShouldReturnBadRequest() System.Text.Encoding.UTF8, "text/plain"); - var response = await client.PostAsync("/auth/try-login", content); + var response = await client.PostAsync("/auth/try-login", content, TestContext.Current.CancellationToken); response.StatusCode.Should().Be(HttpStatusCode.BadRequest); @@ -1200,7 +1198,7 @@ public async Task Login_WithUnsupportedContentType_ShouldNotAuthenticate() System.Text.Encoding.UTF8, "text/plain"); - var response = await client.PostAsync("/auth/login", content); + var response = await client.PostAsync("/auth/login", content, TestContext.Current.CancellationToken); response.Headers .TryGetValues("Set-Cookie", out _) @@ -1222,7 +1220,7 @@ public async Task TryLogin_WithEmptyJsonObject_ShouldReturnInvalidCredentials() response.StatusCode.Should().Be(HttpStatusCode.OK); - var result = await response.Content.ReadFromJsonAsync(); + var result = await response.Content.ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); result.Should().NotBeNull(); result!.IsSuccess.Should().BeFalse(); @@ -1238,7 +1236,7 @@ public async Task Login_WithEmptyJsonObject_ShouldNotAuthenticate() var response = await client.PostAsJsonAsync( "/auth/login", - new { }); + new { }, cancellationToken: TestContext.Current.CancellationToken); response.Headers .TryGetValues("Set-Cookie", out _) @@ -1251,7 +1249,7 @@ public async Task Login_WithEmptyJsonObject_ShouldNotAuthenticate() [Fact] public async Task Login_WithJsonPropertyNamesUsingDifferentCasing_ShouldAuthenticate() { - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var client = CreateClient( $"json-casing-{Guid.NewGuid():N}"); @@ -1297,7 +1295,7 @@ public async Task Login_WithUnknownIdentifier_ShouldNotRevealWhetherUserExists() { identifier = $"unknown-{Guid.NewGuid():N}", secret = "wrong-password" - }); + }, cancellationToken: TestContext.Current.CancellationToken); var existingResponse = await existingUserClient.PostAsJsonAsync( "/auth/login", @@ -1305,7 +1303,7 @@ public async Task Login_WithUnknownIdentifier_ShouldNotRevealWhetherUserExists() { identifier = ValidIdentifier, secret = "wrong-password" - }); + }, cancellationToken: TestContext.Current.CancellationToken); unknownResponse.StatusCode.Should().Be(existingResponse.StatusCode); @@ -1321,7 +1319,7 @@ public async Task Login_WithUnknownIdentifier_ShouldNotRevealWhetherUserExists() [Fact] public async Task TryLogin_WithUnknownIdentifier_ShouldReturnSameFailureReasonAsWrongPassword() { - var existingUser = await _factory.CreateLoginUserAsync(); + var existingUser = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var unknownClient = CreateClient( $"unknown-{Guid.NewGuid():N}"); @@ -1349,10 +1347,10 @@ public async Task TryLogin_WithUnknownIdentifier_ShouldReturnSameFailureReasonAs existingResponse.StatusCode.Should().Be(HttpStatusCode.OK); var unknownResult = - await unknownResponse.Content.ReadFromJsonAsync(); + await unknownResponse.Content.ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); var existingResult = - await existingResponse.Content.ReadFromJsonAsync(); + await existingResponse.Content.ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); unknownResult.Should().NotBeNull(); existingResult.Should().NotBeNull(); @@ -1387,7 +1385,7 @@ public async Task TryLogin_WithUnknownIdentifier_ShouldNotIssuePreviewReceipt() response.StatusCode.Should().Be(HttpStatusCode.OK); var result = - await response.Content.ReadFromJsonAsync(); + await response.Content.ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); result.Should().NotBeNull(); @@ -1403,7 +1401,7 @@ public async Task TryLogin_WithUnknownIdentifier_ShouldNotIssuePreviewReceipt() [Fact] public async Task TryLogin_WithWrongPassword_ShouldNotIssuePreviewReceipt() { - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var client = CreateClient( $"wrong-password-{Guid.NewGuid():N}"); @@ -1419,7 +1417,7 @@ public async Task TryLogin_WithWrongPassword_ShouldNotIssuePreviewReceipt() response.StatusCode.Should().Be(HttpStatusCode.OK); var result = - await response.Content.ReadFromJsonAsync(); + await response.Content.ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); result.Should().NotBeNull(); @@ -1444,7 +1442,7 @@ public async Task Login_WithUnknownIdentifier_ShouldNotReturnAuthenticationCrede { identifier = $"unknown-{Guid.NewGuid():N}", secret = ValidSecret - }); + }, cancellationToken: TestContext.Current.CancellationToken); response.Headers .TryGetValues("Set-Cookie", out _) @@ -1461,7 +1459,7 @@ public async Task Login_FailuresOutsideFailureWindow_ShouldNotAccumulateTowardLo { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var client = CreateClient( $"failure-window-{Guid.NewGuid():N}"); @@ -1511,7 +1509,7 @@ public async Task Login_BeforeLockoutExpires_ShouldRemainLocked() { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var client = CreateClient( $"active-lockout-{Guid.NewGuid():N}"); @@ -1544,7 +1542,7 @@ public async Task Login_AtLockoutExpirationBoundary_ShouldAllowCorrectCredential { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var client = CreateClient( $"lockout-boundary-{Guid.NewGuid():N}"); @@ -1579,7 +1577,7 @@ public async Task Login_FailuresWithinFailureWindow_ShouldAccumulateTowardLockou { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var client = CreateClient( $"failure-window-inside-{Guid.NewGuid():N}"); @@ -1612,7 +1610,7 @@ public async Task Login_FailureDuringLockout_ShouldNotExtendLockout_WhenExtendLo { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var client = CreateClient( $"lockout-no-extension-{Guid.NewGuid():N}"); @@ -1671,7 +1669,7 @@ public async Task ConcurrentLoginFailures_ShouldNotLoseFailureAttempts() { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var client1 = CreateClient( $"concurrent-failure-1-{Guid.NewGuid():N}"); @@ -1712,7 +1710,7 @@ public async Task ConcurrentSuccessfulLogins_FromDifferentDevices_ShouldCreateUs { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var device1 = $"concurrent-success-1-{Guid.NewGuid():N}"; var device2 = $"concurrent-success-2-{Guid.NewGuid():N}"; @@ -1745,7 +1743,7 @@ public async Task ConcurrentSuccessfulLogins_FromDifferentDevices_ShouldCreateUs { PageNumber = 1, PageSize = 10 - }), + }, cancellationToken: TestContext.Current.CancellationToken), authenticatedClient2.PostAsJsonAsync( "/auth/me/sessions/chains", @@ -1753,7 +1751,7 @@ public async Task ConcurrentSuccessfulLogins_FromDifferentDevices_ShouldCreateUs { PageNumber = 1, PageSize = 10 - })); + }, cancellationToken: TestContext.Current.CancellationToken)); sessionResponses.Should().OnlyContain( x => x.StatusCode == HttpStatusCode.OK); @@ -1761,12 +1759,12 @@ public async Task ConcurrentSuccessfulLogins_FromDifferentDevices_ShouldCreateUs var result1 = await sessionResponses[0] .Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(cancellationToken: TestContext.Current.CancellationToken); var result2 = await sessionResponses[1] .Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(cancellationToken: TestContext.Current.CancellationToken); result1.Should().NotBeNull(); result2.Should().NotBeNull(); @@ -1787,7 +1785,7 @@ public async Task ConcurrentLogin_WithSamePreviewReceipt_ShouldNotAllowReceiptTo { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var device = $"preview-concurrent-{Guid.NewGuid():N}"; diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/LogoutAdminTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/LogoutAdminTests.cs index fb873146..1f18607e 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration/LogoutAdminTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/LogoutAdminTests.cs @@ -23,7 +23,7 @@ public async Task LogoutDeviceAdmin_WithoutAuthentication_ShouldReturnUnauthoriz { _factory.Clock.Reset(); - var target = await _factory.CreateLoginUserAsync(); + var target = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var targetClient = CreateClient( $"admin-logout-device-target-{Guid.NewGuid():N}"); @@ -44,7 +44,7 @@ await LoginAsync( .Be(HttpStatusCode.OK); var chains = await chainsResponse.Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(cancellationToken: TestContext.Current.CancellationToken); chains.Should().NotBeNull(); @@ -76,8 +76,8 @@ public async Task LogoutDeviceAdmin_WithoutRequiredPermission_ShouldReturnForbid { _factory.Clock.Reset(); - var actor = await _factory.CreateLoginUserAsync(); - var target = await _factory.CreateLoginUserAsync(); + var actor = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var target = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var actorClient = CreateClient( $"admin-logout-device-unprivileged-{Guid.NewGuid():N}"); @@ -112,7 +112,7 @@ await LoginAsync( .Be(HttpStatusCode.OK); var targetChains = await targetChainsResponse.Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(cancellationToken: TestContext.Current.CancellationToken); targetChains.Should().NotBeNull(); @@ -147,7 +147,7 @@ public async Task LogoutAllAdmin_WithoutAuthentication_ShouldReturnUnauthorized( { _factory.Clock.Reset(); - var target = await _factory.CreateLoginUserAsync(); + var target = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var targetClient = CreateClient( $"admin-logout-all-target-{Guid.NewGuid():N}"); @@ -185,8 +185,8 @@ public async Task LogoutAllAdmin_WithoutRequiredPermission_ShouldReturnForbidden { _factory.Clock.Reset(); - var actor = await _factory.CreateLoginUserAsync(); - var target = await _factory.CreateLoginUserAsync(); + var actor = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var target = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var actorClient = CreateClient( $"admin-logout-all-unprivileged-{Guid.NewGuid():N}"); @@ -238,14 +238,14 @@ public async Task LogoutDeviceAdmin_WithPermission_ShouldInvalidateTargetDeviceS { _factory.Clock.Reset(); - var admin = await _factory.CreateLoginUserAsync(); - var target = await _factory.CreateLoginUserAsync(); + var admin = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var target = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await _factory.GrantPermissionsAsync( admin.UserKey, [ UAuthActions.Flows.LogoutDeviceAdmin - ]); + ], TestContext.Current.CancellationToken); var adminDevice = $"logout-admin-{Guid.NewGuid():N}"; @@ -289,7 +289,7 @@ await LoginAsync( .Be(HttpStatusCode.OK); var targetChains = await targetChainsResponse.Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(cancellationToken: TestContext.Current.CancellationToken); targetChains.Should().NotBeNull(); @@ -333,14 +333,14 @@ public async Task LogoutDeviceAdmin_ShouldNotInvalidateTargetUsersOtherDevice() { _factory.Clock.Reset(); - var admin = await _factory.CreateLoginUserAsync(); - var target = await _factory.CreateLoginUserAsync(); + var admin = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var target = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await _factory.GrantPermissionsAsync( admin.UserKey, [ UAuthActions.Flows.LogoutDeviceAdmin - ]); + ], TestContext.Current.CancellationToken); using var adminClient = CreateClient( $"logout-admin-{Guid.NewGuid():N}"); @@ -391,7 +391,7 @@ await LoginAsync( .Be(HttpStatusCode.OK); var chains = await chainsResponse.Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(cancellationToken: TestContext.Current.CancellationToken); chains.Should().NotBeNull(); @@ -438,15 +438,15 @@ public async Task LogoutDeviceAdmin_ShouldNotAllowChainFromDifferentTargetUser() { _factory.Clock.Reset(); - var admin = await _factory.CreateLoginUserAsync(); - var targetA = await _factory.CreateLoginUserAsync(); - var targetB = await _factory.CreateLoginUserAsync(); + var admin = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var targetA = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var targetB = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await _factory.GrantPermissionsAsync( admin.UserKey, [ UAuthActions.Flows.LogoutDeviceAdmin - ]); + ], TestContext.Current.CancellationToken); using var adminClient = CreateClient( $"logout-admin-{Guid.NewGuid():N}"); @@ -495,7 +495,7 @@ await LoginAsync( .Be(HttpStatusCode.OK); var targetBChains = await targetBChainsResponse.Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(cancellationToken: TestContext.Current.CancellationToken); targetBChains.Should().NotBeNull(); @@ -536,14 +536,14 @@ public async Task LogoutOthersAdmin_ShouldLogoutAllTargetDevicesExceptSpecifiedC { _factory.Clock.Reset(); - var admin = await _factory.CreateLoginUserAsync(); - var target = await _factory.CreateLoginUserAsync(); + var admin = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var target = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await _factory.GrantPermissionsAsync( admin.UserKey, [ UAuthActions.Flows.LogoutOthersAdmin - ]); + ], TestContext.Current.CancellationToken); using var adminClient = CreateClient($"logout-others-admin-{Guid.NewGuid():N}"); @@ -570,7 +570,7 @@ await _factory.GrantPermissionsAsync( chainsResponse.StatusCode.Should().Be(HttpStatusCode.OK); - var chains = await chainsResponse.Content.ReadFromJsonAsync>(); + var chains = await chainsResponse.Content.ReadFromJsonAsync>(cancellationToken: TestContext.Current.CancellationToken); chains.Should().NotBeNull(); @@ -599,8 +599,8 @@ public async Task LogoutOthersAdmin_WithoutRequiredPermission_ShouldReturnForbid { _factory.Clock.Reset(); - var actor = await _factory.CreateLoginUserAsync(); - var target = await _factory.CreateLoginUserAsync(); + var actor = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var target = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var actorClient = CreateClient( $"logout-others-actor-{Guid.NewGuid():N}"); @@ -637,7 +637,7 @@ await LoginAsync( await GetChainsAsync(targetClient1); var chains = await chainsResponse.Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(cancellationToken: TestContext.Current.CancellationToken); chains.Should().NotBeNull(); @@ -675,15 +675,15 @@ public async Task LogoutOthersAdmin_ShouldRejectCurrentChainFromDifferentUser() { _factory.Clock.Reset(); - var admin = await _factory.CreateLoginUserAsync(); - var target = await _factory.CreateLoginUserAsync(); - var otherUser = await _factory.CreateLoginUserAsync(); + var admin = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var target = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var otherUser = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await _factory.GrantPermissionsAsync( admin.UserKey, [ UAuthActions.Flows.LogoutOthersAdmin - ]); + ], TestContext.Current.CancellationToken); using var adminClient = CreateClient( $"logout-others-admin-{Guid.NewGuid():N}"); @@ -733,7 +733,7 @@ await LoginAsync( await GetChainsAsync(otherClient); var otherChains = await otherChainsResponse.Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(cancellationToken: TestContext.Current.CancellationToken); otherChains.Should().NotBeNull(); @@ -780,14 +780,14 @@ public async Task LogoutOthersAdmin_WithSingleTargetDevice_ShouldPreserveThatDev { _factory.Clock.Reset(); - var admin = await _factory.CreateLoginUserAsync(); - var target = await _factory.CreateLoginUserAsync(); + var admin = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var target = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await _factory.GrantPermissionsAsync( admin.UserKey, [ UAuthActions.Flows.LogoutOthersAdmin - ]); + ], TestContext.Current.CancellationToken); using var adminClient = CreateClient( $"logout-others-admin-{Guid.NewGuid():N}"); @@ -822,7 +822,7 @@ await LoginAsync( .Be(HttpStatusCode.OK); var chains = await chainsResponse.Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(cancellationToken: TestContext.Current.CancellationToken); chains.Should().NotBeNull(); @@ -860,14 +860,14 @@ public async Task LogoutOthersAdmin_WhenRepeated_ShouldRemainIdempotent() { _factory.Clock.Reset(); - var admin = await _factory.CreateLoginUserAsync(); - var target = await _factory.CreateLoginUserAsync(); + var admin = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var target = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await _factory.GrantPermissionsAsync( admin.UserKey, [ UAuthActions.Flows.LogoutOthersAdmin - ]); + ], TestContext.Current.CancellationToken); using var adminClient = CreateClient( $"logout-others-admin-{Guid.NewGuid():N}"); @@ -912,7 +912,7 @@ await LoginAsync( await GetChainsAsync(targetClient1); var chains = await chainsResponse.Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(cancellationToken: TestContext.Current.CancellationToken); chains.Should().NotBeNull(); @@ -968,14 +968,14 @@ public async Task LogoutAllAdmin_ShouldLogoutAllTargetDevices() { _factory.Clock.Reset(); - var admin = await _factory.CreateLoginUserAsync(); - var target = await _factory.CreateLoginUserAsync(); + var admin = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var target = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await _factory.GrantPermissionsAsync( admin.UserKey, [ UAuthActions.Flows.LogoutAllAdmin - ]); + ], TestContext.Current.CancellationToken); using var adminClient = CreateClient( $"logout-all-admin-{Guid.NewGuid():N}"); @@ -1075,14 +1075,14 @@ public async Task LogoutAllAdmin_ShouldNotInvalidateAdminSession() { _factory.Clock.Reset(); - var admin = await _factory.CreateLoginUserAsync(); - var target = await _factory.CreateLoginUserAsync(); + var admin = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var target = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await _factory.GrantPermissionsAsync( admin.UserKey, [ UAuthActions.Flows.LogoutAllAdmin - ]); + ], TestContext.Current.CancellationToken); using var adminClient = CreateClient( $"logout-all-admin-{Guid.NewGuid():N}"); @@ -1138,8 +1138,8 @@ public async Task LogoutAllAdmin_WithoutRequiredPermission_ShouldReturnForbidden { _factory.Clock.Reset(); - var actor = await _factory.CreateLoginUserAsync(); - var target = await _factory.CreateLoginUserAsync(); + var actor = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var target = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var actorClient = CreateClient( $"logout-all-actor-{Guid.NewGuid():N}"); @@ -1226,14 +1226,14 @@ public async Task LogoutAllAdmin_WhenRepeated_ShouldRemainIdempotent() { _factory.Clock.Reset(); - var admin = await _factory.CreateLoginUserAsync(); - var target = await _factory.CreateLoginUserAsync(); + var admin = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var target = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await _factory.GrantPermissionsAsync( admin.UserKey, [ UAuthActions.Flows.LogoutAllAdmin - ]); + ], TestContext.Current.CancellationToken); using var adminClient = CreateClient( $"logout-all-admin-{Guid.NewGuid():N}"); @@ -1310,15 +1310,15 @@ public async Task LogoutAllAdmin_ShouldNotAffectUnrelatedUser() { _factory.Clock.Reset(); - var admin = await _factory.CreateLoginUserAsync(); - var target = await _factory.CreateLoginUserAsync(); - var unrelated = await _factory.CreateLoginUserAsync(); + var admin = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var target = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var unrelated = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await _factory.GrantPermissionsAsync( admin.UserKey, [ UAuthActions.Flows.LogoutAllAdmin - ]); + ], TestContext.Current.CancellationToken); using var adminClient = CreateClient( $"logout-all-admin-{Guid.NewGuid():N}"); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/LogoutTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/LogoutTests.cs index 850f7be3..76abc819 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration/LogoutTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/LogoutTests.cs @@ -23,7 +23,7 @@ public async Task Logout_WithAuthenticatedSession_ShouldSucceed() { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var deviceId = $"logout-basic-{Guid.NewGuid():N}"; using var client = CreateClient(deviceId); @@ -51,7 +51,7 @@ public async Task Logout_ShouldDeleteSessionCookie() { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var deviceId = $"logout-cookie-{Guid.NewGuid():N}"; using var client = CreateClient(deviceId); @@ -93,7 +93,7 @@ public async Task Logout_ShouldInvalidateSessionOnServer() { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var deviceId = $"logout-invalidation-{Guid.NewGuid():N}"; using var client = CreateClient(deviceId); @@ -131,7 +131,7 @@ public async Task Logout_OldSessionCookie_ShouldNotAuthenticateFromNewClient() { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var deviceId = $"logout-replay-{Guid.NewGuid():N}"; using var originalClient = CreateClient(deviceId); @@ -175,7 +175,7 @@ public async Task Logout_ShouldNotInvalidateOtherDeviceSession() { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var device1 = $"logout-device-1-{Guid.NewGuid():N}"; var device2 = $"logout-device-2-{Guid.NewGuid():N}"; @@ -236,8 +236,8 @@ public async Task Logout_ShouldNotInvalidateAnotherUsersSession() { _factory.Clock.Reset(); - var user1 = await _factory.CreateLoginUserAsync(); - var user2 = await _factory.CreateLoginUserAsync(); + var user1 = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var user2 = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var client1 = CreateClient( $"logout-user-1-{Guid.NewGuid():N}"); @@ -289,7 +289,7 @@ public async Task Logout_ShouldDetachActiveSessionFromCurrentChain() { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var device = $"logout-detach-{Guid.NewGuid():N}"; using var client = CreateClient(device); @@ -305,11 +305,11 @@ public async Task Logout_ShouldDetachActiveSessionFromCurrentChain() { PageNumber = 1, PageSize = 50 - }); + }, cancellationToken: TestContext.Current.CancellationToken); beforeResponse.StatusCode.Should().Be(HttpStatusCode.OK); - var before = await beforeResponse.Content.ReadFromJsonAsync>(); + var before = await beforeResponse.Content.ReadFromJsonAsync>(cancellationToken: TestContext.Current.CancellationToken); before.Should().NotBeNull(); @@ -325,7 +325,7 @@ public async Task Logout_ShouldDetachActiveSessionFromCurrentChain() logout.StatusCode.Should().Be(HttpStatusCode.Found); - var oldSessionResponse = await client.PostAsJsonAsync("/auth/me/sessions/chains", new PageRequest()); + var oldSessionResponse = await client.PostAsJsonAsync("/auth/me/sessions/chains", new PageRequest(), cancellationToken: TestContext.Current.CancellationToken); oldSessionResponse.StatusCode.Should().Be(HttpStatusCode.Unauthorized); @@ -341,11 +341,11 @@ public async Task Logout_ShouldDetachActiveSessionFromCurrentChain() { PageNumber = 1, PageSize = 50 - }); + }, cancellationToken: TestContext.Current.CancellationToken); afterResponse.StatusCode.Should().Be(HttpStatusCode.OK); - var after = await afterResponse.Content.ReadFromJsonAsync>(); + var after = await afterResponse.Content.ReadFromJsonAsync>(cancellationToken: TestContext.Current.CancellationToken); after.Should().NotBeNull(); var currentAfter = after!.Items.Single(x => x.IsCurrentDevice); @@ -363,7 +363,7 @@ public async Task Logout_OldSession_ShouldRemainInvalidAfterLoginAgain() { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var device = $"logout-old-session-{Guid.NewGuid():N}"; using var firstClient = CreateClient(device); @@ -387,14 +387,14 @@ public async Task Logout_OldSession_ShouldRemainInvalidAfterLoginAgain() secondClient.DefaultRequestHeaders.Add("Cookie", newCookie); - var newSessionResponse = await secondClient.PostAsJsonAsync("/auth/me/sessions/chains", new PageRequest()); + var newSessionResponse = await secondClient.PostAsJsonAsync("/auth/me/sessions/chains", new PageRequest(), cancellationToken: TestContext.Current.CancellationToken); newSessionResponse.StatusCode.Should().Be(HttpStatusCode.OK); using var replayClient = CreateClient(device); replayClient.DefaultRequestHeaders.Add("Cookie", oldCookie); - var replayResponse = await replayClient.PostAsJsonAsync("/auth/me/sessions/chains", new PageRequest()); + var replayResponse = await replayClient.PostAsJsonAsync("/auth/me/sessions/chains", new PageRequest(), cancellationToken: TestContext.Current.CancellationToken); replayResponse.StatusCode.Should().Be(HttpStatusCode.Unauthorized); } @@ -403,7 +403,7 @@ public async Task Logout_ShouldDetachOnlyCurrentDeviceSession() { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var device1 = $"logout-device-1-{Guid.NewGuid():N}"; var device2 = $"logout-device-2-{Guid.NewGuid():N}"; @@ -438,7 +438,7 @@ public async Task Logout_ShouldDetachOnlyCurrentDeviceSession() beforeResponse.StatusCode.Should().Be(HttpStatusCode.OK); var before = await beforeResponse.Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(cancellationToken: TestContext.Current.CancellationToken); before.Should().NotBeNull(); @@ -469,7 +469,7 @@ public async Task Logout_ShouldDetachOnlyCurrentDeviceSession() device2Result.StatusCode.Should().Be(HttpStatusCode.OK); - var after = await device2Result.Content.ReadFromJsonAsync>(); + var after = await device2Result.Content.ReadFromJsonAsync>(cancellationToken: TestContext.Current.CancellationToken); after.Should().NotBeNull(); @@ -485,7 +485,7 @@ public async Task Logout_ShouldNotRevokeDeviceChain() { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var device = $"logout-not-revoke-{Guid.NewGuid():N}"; using var client = CreateClient(device); @@ -507,7 +507,7 @@ public async Task Logout_ShouldNotRevokeDeviceChain() }); var before = await beforeResponse.Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(cancellationToken: TestContext.Current.CancellationToken); before.Should().NotBeNull(); @@ -539,7 +539,7 @@ public async Task Logout_ShouldNotRevokeDeviceChain() }); var after = await afterResponse.Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(cancellationToken: TestContext.Current.CancellationToken); after.Should().NotBeNull(); @@ -575,7 +575,7 @@ public async Task Logout_WithAlreadyLoggedOutSession_ShouldRemainIdempotent() { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var device = $"logout-idempotent-{Guid.NewGuid():N}"; using var client = CreateClient(device); @@ -618,7 +618,7 @@ public async Task Logout_WithInvalidSessionCookie_ShouldNotAffectValidSession() { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var validDevice = $"logout-valid-{Guid.NewGuid():N}"; @@ -669,7 +669,7 @@ public async Task Logout_WithInvalidSessionCookie_ShouldNotAffectValidSession() validSession.StatusCode.Should().Be(HttpStatusCode.OK); var chains = await validSession.Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(cancellationToken: TestContext.Current.CancellationToken); chains.Should().NotBeNull(); @@ -684,7 +684,7 @@ public async Task Logout_WithOldSessionAfterRelogin_ShouldNotLogoutNewSession() { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var device = $"logout-stale-{Guid.NewGuid():N}"; // @@ -742,7 +742,7 @@ public async Task Logout_WithOldSessionAfterRelogin_ShouldNotLogoutNewSession() beforeReplay.StatusCode.Should().Be(HttpStatusCode.OK); var beforeChains = await beforeReplay.Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(cancellationToken: TestContext.Current.CancellationToken); beforeChains.Should().NotBeNull(); @@ -783,7 +783,7 @@ public async Task Logout_WithOldSessionAfterRelogin_ShouldNotLogoutNewSession() afterReplay.StatusCode.Should().Be(HttpStatusCode.OK); var afterChains = await afterReplay.Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(cancellationToken: TestContext.Current.CancellationToken); afterChains.Should().NotBeNull(); @@ -800,7 +800,7 @@ public async Task RepeatedLogoutLoginCycles_ShouldPreserveChainAndRotateSessions { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var device = $"logout-cycle-{Guid.NewGuid():N}"; SessionChainId? expectedChainId = null; @@ -834,7 +834,7 @@ public async Task RepeatedLogoutLoginCycles_ShouldPreserveChainAndRotateSessions chainsResponse.StatusCode.Should().Be(HttpStatusCode.OK); var chains = await chainsResponse.Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(cancellationToken: TestContext.Current.CancellationToken); chains.Should().NotBeNull(); @@ -880,7 +880,7 @@ public async Task Logout_WithStaleCredential_ShouldNotAffectOtherDevice() { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var device1 = $"logout-stale-device-1-{Guid.NewGuid():N}"; @@ -930,7 +930,7 @@ await CsrfTestClient.PostJsonAsync(device2Client, .Be(HttpStatusCode.OK); var before = await device2BeforeResponse.Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(cancellationToken: TestContext.Current.CancellationToken); before.Should().NotBeNull(); @@ -968,7 +968,7 @@ await CsrfTestClient.PostJsonAsync(device2Client, .Be(HttpStatusCode.OK); var after = await device2AfterResponse.Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(cancellationToken: TestContext.Current.CancellationToken); after.Should().NotBeNull(); @@ -985,7 +985,7 @@ public async Task Logout_ShouldNotReturnAuthenticationCredentials() { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var device = $"logout-no-credentials-{Guid.NewGuid():N}"; using var client = CreateClient(device); @@ -1042,7 +1042,7 @@ public async Task Logout_ShouldDeleteSessionCookieWithRootPath() { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var device = $"logout-cookie-path-{Guid.NewGuid():N}"; using var client = CreateClient(device); @@ -1088,7 +1088,7 @@ public async Task Logout_ShouldNotReissueSessionCookieAfterServerSideInvalidatio { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var device = $"logout-no-reissue-{Guid.NewGuid():N}"; using var client = CreateClient(device); @@ -1158,7 +1158,7 @@ public async Task Logout_CookieDeletion_ShouldNotContainOriginalSessionCredentia { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var device = $"logout-cookie-value-{Guid.NewGuid():N}"; using var client = CreateClient(device); @@ -1210,7 +1210,7 @@ public async Task ConcurrentLogout_WithSameSession_ShouldLeaveSessionInvalid() { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var device = $"logout-concurrent-same-{Guid.NewGuid():N}"; using var loginClient = CreateClient(device); @@ -1241,7 +1241,7 @@ public async Task ConcurrentLogout_WithSameSession_ShouldLeaveSessionInvalid() { if (response.StatusCode == HttpStatusCode.BadRequest) { - var body = await response.Content.ReadAsStringAsync(); + var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); body.Should().Contain("invalid_antiforgery_token"); continue; @@ -1275,7 +1275,7 @@ public async Task ConcurrentLogout_FromDifferentDevices_ShouldInvalidateBothSess { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var device1 = $"logout-concurrent-device-1-{Guid.NewGuid():N}"; @@ -1347,7 +1347,7 @@ public async Task ConcurrentRequests_WithSessionBeingLoggedOut_ShouldNeverLeaveC { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var device = $"logout-concurrent-request-{Guid.NewGuid():N}"; @@ -1420,7 +1420,7 @@ public async Task ConcurrentLogoutAndRelogin_OnSameDevice_ShouldLeaveChainInCons { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var device = $"logout-relogin-race-{Guid.NewGuid():N}"; @@ -1558,7 +1558,7 @@ public async Task LogoutDeviceSelf_ShouldRevokeTargetDeviceSession() { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var device1 = $"logout-device-self-1-{Guid.NewGuid():N}"; var device2 = $"logout-device-self-2-{Guid.NewGuid():N}"; @@ -1580,7 +1580,7 @@ public async Task LogoutDeviceSelf_ShouldRevokeTargetDeviceSession() chainsResponse.StatusCode.Should().Be(HttpStatusCode.OK); var page = await chainsResponse.Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(TestContext.Current.CancellationToken); page.Should().NotBeNull(); @@ -1611,7 +1611,7 @@ public async Task LogoutDeviceSelf_ShouldNotInvalidateCurrentDevice_WhenAnotherD { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var device1 = $"logout-device-keep-{Guid.NewGuid():N}"; var device2 = $"logout-device-target-{Guid.NewGuid():N}"; @@ -1631,7 +1631,7 @@ public async Task LogoutDeviceSelf_ShouldNotInvalidateCurrentDevice_WhenAnotherD var chainsResponse = await GetChainsAsync(client1); var page = await chainsResponse.Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(TestContext.Current.CancellationToken); page.Should().NotBeNull(); @@ -1679,7 +1679,7 @@ public async Task LogoutDeviceSelf_ShouldDetachActiveSessionWithoutRevokingChain { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var device1 = $"logout-device-observer-{Guid.NewGuid():N}"; var device2 = $"logout-device-revoked-{Guid.NewGuid():N}"; @@ -1696,7 +1696,7 @@ public async Task LogoutDeviceSelf_ShouldDetachActiveSessionWithoutRevokingChain var beforeResponse = await GetChainsAsync(client1); - var before = await beforeResponse.Content.ReadFromJsonAsync>(); + var before = await beforeResponse.Content.ReadFromJsonAsync>(TestContext.Current.CancellationToken); before.Should().NotBeNull(); @@ -1717,7 +1717,7 @@ public async Task LogoutDeviceSelf_ShouldDetachActiveSessionWithoutRevokingChain afterResponse.StatusCode.Should().Be(HttpStatusCode.OK); - var after = await afterResponse.Content.ReadFromJsonAsync>(); + var after = await afterResponse.Content.ReadFromJsonAsync>(TestContext.Current.CancellationToken); after.Should().NotBeNull(); @@ -1751,8 +1751,8 @@ public async Task LogoutDeviceSelf_ShouldNotAllowUserToLogoutAnotherUsersDevice( { _factory.Clock.Reset(); - var attacker = await _factory.CreateLoginUserAsync(); - var victim = await _factory.CreateLoginUserAsync(); + var attacker = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var victim = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var attackerDevice = $"logout-device-attacker-{Guid.NewGuid():N}"; @@ -1793,7 +1793,7 @@ await LoginAsync( .Be(HttpStatusCode.OK); var victimChains = await victimChainsResponse.Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(TestContext.Current.CancellationToken); victimChains.Should().NotBeNull(); @@ -1825,7 +1825,7 @@ public async Task LogoutDeviceSelf_ShouldAllowCurrentDeviceToLogoutItself() { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var device = $"logout-device-current-{Guid.NewGuid():N}"; using var client = CreateClient(device); @@ -1848,7 +1848,7 @@ public async Task LogoutDeviceSelf_ShouldAllowCurrentDeviceToLogoutItself() chainsResponse.StatusCode.Should().Be(HttpStatusCode.OK); var chains = await chainsResponse.Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(TestContext.Current.CancellationToken); chains.Should().NotBeNull(); @@ -1880,7 +1880,7 @@ public async Task LogoutDeviceSelf_WithUnknownChain_ShouldNotAffectCurrentSessio { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var device = $"logout-device-unknown-{Guid.NewGuid():N}"; using var client = CreateClient(device); @@ -1926,7 +1926,7 @@ public async Task LogoutDeviceSelf_AlreadyLoggedOutDevice_ShouldRemainSafe() { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var actorDevice = $"logout-device-repeat-actor-{Guid.NewGuid():N}"; @@ -1960,7 +1960,7 @@ await LoginAsync( var chainsResponse = await GetChainsAsync(actorClient); var chains = await chainsResponse.Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(TestContext.Current.CancellationToken); chains.Should().NotBeNull(); @@ -2021,7 +2021,7 @@ public async Task LogoutDeviceSelf_LoggedOutDevice_ShouldBeAbleToLoginAgain() { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var actorDevice = $"logout-device-relogin-actor-{Guid.NewGuid():N}"; @@ -2056,7 +2056,7 @@ await LoginAsync( await GetChainsAsync(actorClient); var chains = await chainsResponse.Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(TestContext.Current.CancellationToken); chains.Should().NotBeNull(); @@ -2122,7 +2122,7 @@ await LoginAsync( .Be(HttpStatusCode.OK); var after = await afterResponse.Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(TestContext.Current.CancellationToken); after.Should().NotBeNull(); @@ -2146,7 +2146,7 @@ public async Task LogoutOthersSelf_ShouldInvalidateAllOtherDeviceSessions() { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var device1 = $"logout-others-current-{Guid.NewGuid():N}"; var device2 = $"logout-others-other-1-{Guid.NewGuid():N}"; @@ -2203,7 +2203,7 @@ public async Task LogoutOthersSelf_ShouldKeepCurrentSessionValid() { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var currentDevice = $"logout-others-keep-current-{Guid.NewGuid():N}"; @@ -2244,7 +2244,7 @@ await LoginAsync( .Be(HttpStatusCode.OK); var before = await beforeResponse.Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(TestContext.Current.CancellationToken); before.Should().NotBeNull(); @@ -2272,7 +2272,7 @@ await LoginAsync( .Be(HttpStatusCode.OK); var after = await afterResponse.Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(TestContext.Current.CancellationToken); after.Should().NotBeNull(); @@ -2301,7 +2301,7 @@ public async Task LogoutOthersSelf_ShouldDetachOtherSessionsWithoutRevokingChain { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var actorDevice = $"logout-others-observer-{Guid.NewGuid():N}"; @@ -2353,7 +2353,7 @@ await LoginAsync( .Be(HttpStatusCode.OK); var before = await beforeResponse.Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(TestContext.Current.CancellationToken); before.Should().NotBeNull(); @@ -2385,7 +2385,7 @@ await LoginAsync( .Be(HttpStatusCode.OK); var after = await afterResponse.Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(TestContext.Current.CancellationToken); after.Should().NotBeNull(); @@ -2420,8 +2420,8 @@ public async Task LogoutOthersSelf_ShouldNotAffectAnotherUsersSessions() { _factory.Clock.Reset(); - var actor = await _factory.CreateLoginUserAsync(); - var victim = await _factory.CreateLoginUserAsync(); + var actor = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var victim = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); // // Actor owns two devices so logout-others actually has work to do. @@ -2515,7 +2515,7 @@ public async Task LogoutOthersSelf_WithNoOtherSessions_ShouldSucceed() { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var device = $"logout-others-single-{Guid.NewGuid():N}"; @@ -2539,7 +2539,7 @@ await LoginAsync( .Be(HttpStatusCode.OK); var before = await beforeResponse.Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(TestContext.Current.CancellationToken); before.Should().NotBeNull(); @@ -2567,7 +2567,7 @@ await LoginAsync( .Be(HttpStatusCode.OK); var after = await afterResponse.Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(TestContext.Current.CancellationToken); after.Should().NotBeNull(); @@ -2586,7 +2586,7 @@ public async Task LogoutOthersSelf_RepeatedCall_ShouldRemainSafe() { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var actorClient = CreateClient( $"logout-others-repeat-actor-{Guid.NewGuid():N}"); @@ -2643,7 +2643,7 @@ public async Task LogoutOthersSelf_OldCredentialsFromOtherDevices_ShouldRemainIn { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var actorDevice = $"logout-others-replay-actor-{Guid.NewGuid():N}"; @@ -2710,7 +2710,7 @@ public async Task LogoutOthersSelf_LoggedOutDevices_ShouldBeAbleToLoginAgain() { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var actorDevice = $"logout-others-relogin-actor-{Guid.NewGuid():N}"; @@ -2748,7 +2748,7 @@ await LoginAsync( .Be(HttpStatusCode.OK); var before = await beforeResponse.Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(TestContext.Current.CancellationToken); before.Should().NotBeNull(); @@ -2813,7 +2813,7 @@ await LoginAsync( .Be(HttpStatusCode.OK); var after = await afterResponse.Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(TestContext.Current.CancellationToken); after.Should().NotBeNull(); @@ -2847,7 +2847,7 @@ public async Task ConcurrentLogoutOthersSelf_ShouldLeaveOnlyActorSessionUsable() { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var actorDevice = $"logout-others-concurrent-actor-{Guid.NewGuid():N}"; @@ -2950,7 +2950,7 @@ public async Task LogoutAllSelf_ShouldInvalidateAllDeviceSessions() { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var device1 = $"logout-all-device-1-{Guid.NewGuid():N}"; var device2 = $"logout-all-device-2-{Guid.NewGuid():N}"; @@ -3006,7 +3006,7 @@ public async Task LogoutAllSelf_ShouldInvalidateCallingSession() { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var device = $"logout-all-current-{Guid.NewGuid():N}"; @@ -3070,7 +3070,7 @@ public async Task LogoutAllSelf_ShouldDetachSessionsWithoutRevokingChains() { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var device1 = $"logout-all-chain-1-{Guid.NewGuid():N}"; @@ -3112,7 +3112,7 @@ await LoginAsync( .Be(HttpStatusCode.OK); var before = await beforeResponse.Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(TestContext.Current.CancellationToken); before.Should().NotBeNull(); @@ -3171,7 +3171,7 @@ await LoginAsync( .Be(HttpStatusCode.OK); var after = await afterResponse.Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(TestContext.Current.CancellationToken); after.Should().NotBeNull(); @@ -3210,8 +3210,8 @@ public async Task LogoutAllSelf_ShouldNotAffectAnotherUsersSessions() { _factory.Clock.Reset(); - var actor = await _factory.CreateLoginUserAsync(); - var victim = await _factory.CreateLoginUserAsync(); + var actor = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var victim = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var actorClient1 = CreateClient( $"logout-all-actor-1-{Guid.NewGuid():N}"); @@ -3302,7 +3302,7 @@ public async Task LogoutAllSelf_LoggedOutDevices_ShouldBeAbleToLoginAgain() { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var device1 = $"logout-all-relogin-1-{Guid.NewGuid():N}"; @@ -3337,7 +3337,7 @@ await LoginAsync( await GetChainsAsync(client1); var before = await beforeResponse.Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(TestContext.Current.CancellationToken); before.Should().NotBeNull(); @@ -3421,7 +3421,7 @@ public async Task ConcurrentLogoutAllSelf_ShouldConvergeToAllSessionsInvalid() { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var actorDevice = $"logout-all-concurrent-actor-{Guid.NewGuid():N}"; @@ -3470,7 +3470,7 @@ await LoginAsync( if (response.StatusCode == HttpStatusCode.BadRequest) { var body = await response.Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(TestContext.Current.CancellationToken); body.Should().NotBeNull(); body.Should().ContainKey("error") diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/PkceFlowIntegrationTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/PkceFlowIntegrationTests.cs index 3ba88eeb..a73d90ef 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration/PkceFlowIntegrationTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/PkceFlowIntegrationTests.cs @@ -31,7 +31,7 @@ public async Task Pkce_WithValidVerifierAndCredentials_ShouldAuthenticate() _factory.Clock.Reset(); var user = - await _factory.CreateLoginUserAsync(); + await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var client = CreateClient( @@ -76,7 +76,7 @@ public async Task Pkce_AfterSuccessfulCompletion_ShouldRejectReplayOfAuthorizati _factory.Clock.Reset(); var user = - await _factory.CreateLoginUserAsync(); + await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var client = CreateClient( @@ -127,7 +127,7 @@ public async Task Pkce_WithInvalidVerifier_ShouldConsumeAuthorizationCode() _factory.Clock.Reset(); var user = - await _factory.CreateLoginUserAsync(); + await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var client = CreateClient( @@ -181,7 +181,7 @@ public async Task Pkce_AfterAuthorizationCodeExpires_ShouldRejectCompletion() _factory.Clock.Reset(); var user = - await _factory.CreateLoginUserAsync(); + await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var client = CreateClient( @@ -225,7 +225,7 @@ public async Task Pkce_WithValidProofButInvalidCredentials_ShouldNotAuthenticate _factory.Clock.Reset(); var user = - await _factory.CreateLoginUserAsync(); + await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var client = CreateClient( @@ -260,7 +260,7 @@ public async Task Pkce_WithInvalidProof_ShouldNotAuthenticateEvenWithValidCreden _factory.Clock.Reset(); var user = - await _factory.CreateLoginUserAsync(); + await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var client = CreateClient( @@ -379,7 +379,7 @@ public async Task Pkce_WithValidProofAndRepeatedInvalidCredentials_ShouldReachCr _factory.Clock.Reset(); var user = - await _factory.CreateLoginUserAsync(); + await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var client = CreateClient( diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/RateLimitingFlowTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/RateLimitingFlowTests.cs index 3463c6a5..b9d185a4 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration/RateLimitingFlowTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/RateLimitingFlowTests.cs @@ -22,7 +22,7 @@ public async Task RejectedLoginDoesNotLockAccount_AndRejectedLogoutDoesNotRevoke o.RateLimiting.Endpoints[UAuthEndpoint.Logout] = new() { PermitLimit = 1 }; o.Login.MaxFailedAttempts = 1; }); - var user = await factory.CreateLoginUserAsync(); + var user = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); HttpClient Client(string device) { var client = factory.CreateClient(new WebApplicationFactoryClientOptions @@ -45,10 +45,10 @@ HttpClient Client(string device) using (var scope = factory.Services.CreateScope()) { var security = scope.ServiceProvider.GetRequiredService(); - var factor = await security.GetOrCreateFactorAsync(TenantKey.Single, user.UserKey, CredentialType.Password); + var factor = await security.GetOrCreateFactorAsync(TenantKey.Single, user.UserKey, CredentialType.Password, TestContext.Current.CancellationToken); Assert.Equal(0, factor.FailedAttempts); } - Assert.NotEqual(HttpStatusCode.TooManyRequests, (await first.PostAsync("/auth/logout", null)).StatusCode); + Assert.NotEqual(HttpStatusCode.TooManyRequests, (await first.PostAsync("/auth/logout", null, TestContext.Current.CancellationToken)).StatusCode); // Two validated sessions of the same actor share logout capacity. var logout = await CsrfTestClient.PostAsync(second, "/auth/logout", null); Assert.Equal(HttpStatusCode.TooManyRequests, logout.StatusCode); @@ -56,7 +56,7 @@ HttpClient Client(string device) // The rejected logout did not revoke the second session. var validation = await CsrfTestClient.PostAsync(second, "/auth/validate", null); Assert.Equal(HttpStatusCode.OK, validation.StatusCode); - var result = await validation.Content.ReadFromJsonAsync(); + var result = await validation.Content.ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); Assert.True(result?.IsValid); Assert.NotNull(result!.Snapshot); } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/RateLimitingProofFlowTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/RateLimitingProofFlowTests.cs index 0e81c547..0ba5a65a 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration/RateLimitingProofFlowTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/RateLimitingProofFlowTests.cs @@ -26,14 +26,14 @@ public async Task PkceCompletion_RejectedRequestPreservesAuthorizationProof() { using var factory = AuthServerFactory.Create(o => { o.RateLimiting.Enabled = true; o.RateLimiting.Login = new() { PermitLimit = 1, Window = Window }; }); - var user = await factory.CreateLoginUserAsync(); + var user = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var client = Client(factory); static string Encode(byte[] value) => Convert.ToBase64String(value).TrimEnd('=').Replace('+', '-').Replace('/', '_'); var verifier = Encode(RandomNumberGenerator.GetBytes(32)); var authorize = await CsrfTestClient.PostAsync(client, "/auth/pkce/authorize", new FormUrlEncodedContent(new Dictionary { ["code_challenge"] = Encode(SHA256.HashData(Encoding.ASCII.GetBytes(verifier))), ["challenge_method"] = "S256", ["redirect_uri"] = "/callback" })); Assert.Equal(HttpStatusCode.OK, authorize.StatusCode); - var proof = await authorize.Content.ReadFromJsonAsync(); + var proof = await authorize.Content.ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); Assert.NotNull(proof); // Malformed payload consumes request quota without supplying the valid authorization proof. Assert.Equal(HttpStatusCode.BadRequest, (await CsrfTestClient.PostAsync(client, "/auth/pkce/complete", new FormUrlEncodedContent(new Dictionary()))).StatusCode); @@ -58,7 +58,7 @@ public async Task Refresh_RejectedRequestPreservesRefreshToken_ThenSameTokenRota { using var factory = AuthServerFactory.Create(o => { o.RateLimiting.Enabled = true; o.RateLimiting.Endpoints[UAuthEndpoint.Refresh] = new() { PermitLimit = 1, Window = Window }; }); - var user = await factory.CreateLoginUserAsync(); + var user = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var client = Client(factory); client.DefaultRequestHeaders.Add("X-UAuth-ClientProfile", "BlazorWasm"); var login = await CsrfTestClient.PostJsonAsync(client, "/auth/login", new { identifier = user.Identifier, secret = user.Secret }); @@ -68,7 +68,7 @@ public async Task Refresh_RejectedRequestPreservesRefreshToken_ThenSameTokenRota using var scope = factory.Services.CreateScope(); var store = scope.ServiceProvider.GetRequiredService().Create(TenantKey.Single); var hash = scope.ServiceProvider.GetRequiredService().Hash(refresh); - var before = await store.FindByHashAsync(hash); + var before = await store.FindByHashAsync(hash, TestContext.Current.CancellationToken); Assert.NotNull(before); client.DefaultRequestHeaders.Add("Cookie", session); Assert.Equal(HttpStatusCode.Unauthorized, (await CsrfTestClient.PostAsync(client, "/auth/refresh", null)).StatusCode); @@ -77,11 +77,11 @@ public async Task Refresh_RejectedRequestPreservesRefreshToken_ThenSameTokenRota var rejected = await CsrfTestClient.PostAsync(client, "/auth/refresh", null); Assert.Equal(HttpStatusCode.TooManyRequests, rejected.StatusCode); Assert.False(rejected.Headers.Contains("Set-Cookie")); - Assert.Equal(before, await store.FindByHashAsync(hash)); + Assert.Equal(before, await store.FindByHashAsync(hash, TestContext.Current.CancellationToken)); var renewed = await AfterReplenishment(() => CsrfTestClient.PostAsync(client, "/auth/refresh", null)); Assert.Equal(HttpStatusCode.NoContent, renewed.StatusCode); Assert.True(renewed.Headers.Contains("Set-Cookie")); - Assert.False((await store.FindByHashAsync(hash))!.IsActive(factory.Clock.UtcNow)); + Assert.False((await store.FindByHashAsync(hash, TestContext.Current.CancellationToken))!.IsActive(factory.Clock.UtcNow)); } [Fact] @@ -91,7 +91,7 @@ public async Task Recovery_RejectedRequestPreservesResetProof_ThenSameProofChang using var factory = AuthServerFactory.Create(o => { o.RateLimiting.Enabled = true; o.RateLimiting.Endpoints[UAuthEndpoint.CredentialResetCompleteAnonymous] = new() { PermitLimit = 1, Window = Window }; }, services => { services.RemoveAll(); services.AddSingleton(notifier); }); - var user = await factory.CreateLoginUserAsync(); + var user = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var client = Client(factory); var begin = await CsrfTestClient.PostJsonAsync(client, "/auth/me/credentials/reset/begin", new BeginResetCredentialRequest { Identifier = user.Identifier, CredentialType = CredentialType.Password, ResetCodeType = ResetCodeType.Token }); @@ -118,12 +118,12 @@ public async Task AuthenticatedUsersOnSameConnectionIpHaveIndependentValidationB using var second = Client(factory); foreach (var client in new[] { first, second }) { - var user = await factory.CreateLoginUserAsync(); + var user = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var login = await CsrfTestClient.PostJsonAsync(client, "/auth/login", new { identifier = user.Identifier, secret = user.Secret }); client.DefaultRequestHeaders.Add("Cookie", Cookies(login)); var validation = await CsrfTestClient.PostAsync(client, "/auth/validate", null); Assert.Equal(HttpStatusCode.OK, validation.StatusCode); - Assert.True((await validation.Content.ReadFromJsonAsync())!.IsValid); + Assert.True((await validation.Content.ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken))!.IsValid); Assert.Equal(HttpStatusCode.TooManyRequests, (await CsrfTestClient.PostAsync(client, "/auth/validate", null)).StatusCode); } } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/RateLimitingTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/RateLimitingTests.cs index da4ffeef..7fa8f5e0 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration/RateLimitingTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/RateLimitingTests.cs @@ -28,7 +28,7 @@ public async Task LoginAliasesShareBudget_AndMaintenanceIsIndependent() if (response.Headers.RetryAfter is not null) Assert.True(response.Headers.RetryAfter.Delta > TimeSpan.Zero); Assert.True(response.Headers.CacheControl?.NoStore); - Assert.Contains("rate_limited", await response.Content.ReadAsStringAsync()); + Assert.Contains("rate_limited", await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken)); } foreach (var route in new[] { "/auth/validate", "/auth/refresh", "/auth/logout" }) Assert.Equal(HttpStatusCode.NoContent, (await host.Post(route)).StatusCode); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/RefreshTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/RefreshTests.cs index 8725fb56..47e9b25a 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration/RefreshTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/RefreshTests.cs @@ -40,7 +40,7 @@ public async Task Refresh_PureOpaque_BeforeTouchInterval_ShouldNotMutateChain() { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await LoginAsync(user, "BlazorServer"); @@ -74,7 +74,7 @@ public async Task Refresh_PureOpaque_AfterTouchInterval_ShouldTouchChain() { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await LoginAsync(user, "BlazorServer"); @@ -114,7 +114,7 @@ public async Task Refresh_PureOpaque_AfterTouch_ShouldNotTouchAgainBeforeNextInt { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await LoginAsync(user, "BlazorServer"); @@ -159,7 +159,7 @@ public async Task Refresh_PureOpaque_ShouldPreserveSessionAndChainIdentity() { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await LoginAsync(user, "BlazorServer"); @@ -208,7 +208,7 @@ public async Task Refresh_PureOpaque_WithRevokedSession_ShouldReturnUnauthorized _factory.Clock.Reset(); var user = - await _factory.CreateLoginUserAsync(); + await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await LoginAsync( user, @@ -230,10 +230,10 @@ await store.ExecuteAsync( ct => store.RevokeSessionAsync( sessionId, _factory.Clock.UtcNow, - ct)); + ct), TestContext.Current.CancellationToken); var persisted = - await store.GetSessionAsync(sessionId); + await store.GetSessionAsync(sessionId, TestContext.Current.CancellationToken); persisted.Should().NotBeNull(); persisted!.IsRevoked.Should().BeTrue(); @@ -251,7 +251,7 @@ public async Task Refresh_PureOpaque_WithRevokedChain_ShouldReturnUnauthorized() _factory.Clock.Reset(); var user = - await _factory.CreateLoginUserAsync(); + await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await LoginAsync( user, @@ -267,11 +267,11 @@ await store.ExecuteAsync( ct => store.RevokeChainCascadeAsync( current.ChainId, _factory.Clock.UtcNow, - ct)); + ct), TestContext.Current.CancellationToken); var persistedChain = await store.GetChainAsync( - current.ChainId); + current.ChainId, TestContext.Current.CancellationToken); persistedChain.Should().NotBeNull(); persistedChain!.IsRevoked.Should().BeTrue(); @@ -289,7 +289,7 @@ public async Task Refresh_PureOpaque_WithRevokedRoot_ShouldReturnUnauthorized() _factory.Clock.Reset(); var user = - await _factory.CreateLoginUserAsync(); + await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await LoginAsync( user, @@ -303,7 +303,7 @@ await LoginAsync( var rootBefore = await store.GetActiveRootByUserAsync( - user.UserKey); + user.UserKey, TestContext.Current.CancellationToken); rootBefore.Should().NotBeNull(); rootBefore!.IsRevoked.Should().BeFalse(); @@ -312,14 +312,14 @@ await store.ExecuteAsync( ct => store.RevokeRootCascadeAsync( user.UserKey, _factory.Clock.UtcNow, - ct)); + ct), TestContext.Current.CancellationToken); // // Active lookup must no longer return the revoked root. // var activeRootAfter = await store.GetActiveRootByUserAsync( - user.UserKey); + user.UserKey, TestContext.Current.CancellationToken); activeRootAfter.Should().BeNull(); @@ -328,7 +328,7 @@ await store.GetActiveRootByUserAsync( // var historicalRoot = await store.GetRootByIdAsync( - rootBefore.RootId); + rootBefore.RootId, TestContext.Current.CancellationToken); historicalRoot.Should().NotBeNull(); historicalRoot!.IsRevoked.Should().BeTrue(); @@ -338,7 +338,7 @@ await store.GetRootByIdAsync( // var chainAfter = await store.GetChainAsync( - current.ChainId); + current.ChainId, TestContext.Current.CancellationToken); chainAfter.Should().NotBeNull(); chainAfter!.IsRevoked.Should().BeTrue(); @@ -356,7 +356,7 @@ public async Task Refresh_PureOpaque_FromDifferentDevice_ShouldReturnUnauthorize _factory.Clock.Reset(); var user = - await _factory.CreateLoginUserAsync(); + await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var cookie = await LoginAsync( @@ -427,7 +427,7 @@ public async Task Refresh_Hybrid_ShouldRotateCredentials() _factory.Clock.Reset(); var user = - await _factory.CreateLoginUserAsync(); + await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var beforeCookie = await LoginAsync( @@ -474,7 +474,7 @@ public async Task Refresh_Hybrid_ShouldPreserveChainIdentity() _factory.Clock.Reset(); var user = - await _factory.CreateLoginUserAsync(); + await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await LoginAsync( user, @@ -507,7 +507,7 @@ public async Task Refresh_Hybrid_RotatedCredential_ShouldBeUsableForNextRefresh( _factory.Clock.Reset(); var user = - await _factory.CreateLoginUserAsync(); + await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await LoginAsync( user, @@ -548,7 +548,7 @@ public async Task Refresh_Hybrid_ReusingPreviousCredential_ShouldReturnUnauthori _factory.Clock.Reset(); var user = - await _factory.CreateLoginUserAsync(); + await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var originalCookie = await LoginAsync( @@ -593,7 +593,7 @@ public async Task Refresh_Hybrid_WithoutRefreshToken_ShouldReturnUnauthorized() _factory.Clock.Reset(); var user = - await _factory.CreateLoginUserAsync(); + await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var cookie = await LoginAsync( @@ -622,7 +622,7 @@ public async Task Refresh_Hybrid_WithRefreshTokenFromDifferentSession_ShouldRetu _factory.Clock.Reset(); var user = - await _factory.CreateLoginUserAsync(); + await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var clientA = CreateClient( @@ -674,7 +674,7 @@ public async Task Refresh_Hybrid_FromDifferentDevice_ShouldReturnUnauthorized() _factory.Clock.Reset(); var user = - await _factory.CreateLoginUserAsync(); + await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var originalClient = CreateClient( @@ -723,7 +723,7 @@ public async Task Refresh_Hybrid_WithRevokedSession_ShouldReturnUnauthorized() _factory.Clock.Reset(); var user = - await _factory.CreateLoginUserAsync(); + await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await LoginAsync( user, @@ -751,7 +751,7 @@ public async Task Refresh_Hybrid_WithRevokedChain_ShouldReturnUnauthorized() _factory.Clock.Reset(); var user = - await _factory.CreateLoginUserAsync(); + await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await LoginAsync( user, @@ -776,7 +776,7 @@ public async Task Refresh_Hybrid_WithRevokedRoot_ShouldReturnUnauthorized() _factory.Clock.Reset(); var user = - await _factory.CreateLoginUserAsync(); + await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await LoginAsync( user, @@ -798,7 +798,7 @@ public async Task Refresh_Hybrid_ConcurrentReuseOfSameRefreshToken_ShouldAllowEx _factory.Clock.Reset(); var user = - await _factory.CreateLoginUserAsync(); + await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var deviceId = $"hybrid-race-{Guid.NewGuid():N}"; @@ -856,7 +856,7 @@ public async Task Refresh_Hybrid_ConcurrentDuplicate_ShouldLeaveWinningCredentia factory.Clock.Reset(); - var user = await factory.CreateLoginUserAsync(); + var user = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var deviceId = $"hybrid-concurrent-{Guid.NewGuid():N}"; @@ -934,7 +934,7 @@ public async Task Refresh_Hybrid_AfterConcurrentRotation_OriginalRefreshToken_Sh _factory.Clock.Reset(); var user = - await _factory.CreateLoginUserAsync(); + await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var deviceId = $"hybrid-race-replay-{Guid.NewGuid():N}"; @@ -1004,7 +1004,7 @@ public async Task Refresh_Hybrid_ConcurrentRotation_ShouldNotCreateAnotherChain( _factory.Clock.Reset(); var user = - await _factory.CreateLoginUserAsync(); + await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var deviceId = $"hybrid-race-chain-{Guid.NewGuid():N}"; @@ -1083,7 +1083,7 @@ public async Task Refresh_Hybrid_AfterSessionRevoked_ShouldReturnUnauthorized() { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var deviceId = $"refresh-revoke-session-{Guid.NewGuid():N}"; using var client = CreateClient(deviceId); @@ -1114,7 +1114,7 @@ await store.ExecuteAsync( ct => store.RevokeSessionAsync( before.ActiveSessionId!.Value, _factory.Clock.UtcNow, - ct)); + ct), TestContext.Current.CancellationToken); } client.DefaultRequestHeaders.Remove("Cookie"); @@ -1132,7 +1132,7 @@ public async Task Refresh_Hybrid_AfterChainRevoked_ShouldReturnUnauthorized() { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var deviceId = $"refresh-revoke-chain-{Guid.NewGuid():N}"; using var client = CreateClient(deviceId); @@ -1157,7 +1157,7 @@ await store.ExecuteAsync( ct => store.RevokeChainCascadeAsync( before.ChainId, _factory.Clock.UtcNow, - ct)); + ct), TestContext.Current.CancellationToken); } client.DefaultRequestHeaders.Remove("Cookie"); @@ -1174,7 +1174,7 @@ public async Task Refresh_Hybrid_AfterRootRevoked_ShouldReturnUnauthorized() { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var deviceId = $"refresh-revoke-root-{Guid.NewGuid():N}"; using var client = CreateClient(deviceId); @@ -1201,7 +1201,7 @@ await store.ExecuteAsync( ct => store.RevokeRootCascadeAsync( user.UserKey, _factory.Clock.UtcNow, - ct)); + ct), TestContext.Current.CancellationToken); } client.DefaultRequestHeaders.Remove("Cookie"); @@ -1218,7 +1218,7 @@ public async Task Refresh_Hybrid_ReusingRotatedToken_ShouldReturnUnauthorized() { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var deviceId = $"refresh-replay-{Guid.NewGuid():N}"; using var loginClient = CreateClient(deviceId); @@ -1271,7 +1271,7 @@ public async Task Refresh_Hybrid_ReplayAfterConcurrencyWindow_ShouldInvalidateRe factory.Clock.Reset(); var user = - await factory.CreateLoginUserAsync(); + await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var deviceId = $"refresh-replay-family-{Guid.NewGuid():N}"; @@ -1357,7 +1357,7 @@ public async Task Refresh_Hybrid_DuplicateWithinConcurrencyWindow_ShouldNotInval factory.Clock.Reset(); - var user = await factory.CreateLoginUserAsync(); + var user = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var deviceId = $"refresh-duplicate-window-{Guid.NewGuid():N}"; @@ -1403,7 +1403,7 @@ public async Task Refresh_Hybrid_ReplayOnOneChain_ShouldNotAffectOtherDeviceChai _factory.Clock.Reset(); var user = - await _factory.CreateLoginUserAsync(); + await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var deviceA = $"refresh-compromised-{Guid.NewGuid():N}"; @@ -1495,7 +1495,7 @@ public async Task Refresh_Hybrid_JustBeforeRefreshTokenExpiry_ShouldSucceed() { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var deviceId = $"hybrid-expiry-before-{Guid.NewGuid():N}"; using var client = CreateClient(deviceId); @@ -1524,7 +1524,7 @@ public async Task Refresh_Hybrid_AtRefreshTokenExpiry_ShouldReturnUnauthorized() { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var deviceId = $"hybrid-expiry-exact-{Guid.NewGuid():N}"; using var client = CreateClient(deviceId); @@ -1553,7 +1553,7 @@ public async Task Refresh_Hybrid_AfterRefreshTokenExpiry_ShouldReturnUnauthorize { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var deviceId = $"hybrid-expiry-after-{Guid.NewGuid():N}"; using var client = CreateClient(deviceId); @@ -1590,7 +1590,7 @@ public async Task Refresh_Hybrid_WhenSessionExpired_ShouldReturnUnauthorized() factory.Clock.Reset(); var user = - await factory.CreateLoginUserAsync(); + await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var deviceId = $"hybrid-session-expiry-{Guid.NewGuid():N}"; @@ -1636,7 +1636,7 @@ public async Task Refresh_Hybrid_WhenChainIdleTimeoutExceeded_ShouldReturnUnauth factory.Clock.Reset(); var user = - await factory.CreateLoginUserAsync(); + await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var deviceId = $"hybrid-idle-expiry-{Guid.NewGuid():N}"; @@ -1681,7 +1681,7 @@ public async Task Refresh_Hybrid_JustBeforeChainIdleTimeout_ShouldSucceed() factory.Clock.Reset(); var user = - await factory.CreateLoginUserAsync(); + await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var deviceId = $"hybrid-idle-before-{Guid.NewGuid():N}"; @@ -1727,7 +1727,7 @@ public async Task Refresh_Hybrid_AtChainIdleTimeout_ShouldReturnUnauthorized() factory.Clock.Reset(); var user = - await factory.CreateLoginUserAsync(); + await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var deviceId = $"hybrid-idle-exact-{Guid.NewGuid():N}"; @@ -1768,7 +1768,7 @@ public async Task Refresh_Hybrid_OnSuccess_ShouldRotateRefreshCookie() _factory.Clock.Reset(); var user = - await _factory.CreateLoginUserAsync(); + await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var deviceId = $"hybrid-cookie-rotate-{Guid.NewGuid():N}"; @@ -1812,7 +1812,7 @@ public async Task Refresh_Hybrid_OnSuccess_ShouldNotRotateSessionCookie() _factory.Clock.Reset(); var user = - await _factory.CreateLoginUserAsync(); + await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var deviceId = $"hybrid-cookie-session-{Guid.NewGuid():N}"; @@ -1857,7 +1857,7 @@ public async Task Refresh_Hybrid_OnUnauthorized_ShouldNotIssueReplacementRefresh _factory.Clock.Reset(); var user = - await _factory.CreateLoginUserAsync(); + await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var deviceId = $"hybrid-cookie-failure-{Guid.NewGuid():N}"; @@ -1909,7 +1909,7 @@ public async Task Refresh_Hybrid_OnReplayDetection_ShouldNotIssueReplacementRefr _factory.Clock.Reset(); var user = - await _factory.CreateLoginUserAsync(); + await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var deviceId = $"hybrid-cookie-replay-{Guid.NewGuid():N}"; @@ -1972,7 +1972,7 @@ public async Task Refresh_Hybrid_ConcurrentLoser_ShouldNotReceiveReplacementRefr _factory.Clock.Reset(); var user = - await _factory.CreateLoginUserAsync(); + await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var deviceId = $"hybrid-cookie-race-{Guid.NewGuid():N}"; @@ -2063,7 +2063,7 @@ public async Task Refresh_PureOpaque_OnSuccess_ShouldNotIssueRefreshCookie() _factory.Clock.Reset(); var user = - await _factory.CreateLoginUserAsync(); + await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await LoginAsync( user, @@ -2095,7 +2095,7 @@ public async Task Refresh_PureOpaque_OnSuccess_ShouldPreserveSessionCookie() _factory.Clock.Reset(); var user = - await _factory.CreateLoginUserAsync(); + await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var originalCookies = await LoginAsync( @@ -2148,7 +2148,7 @@ public async Task Refresh_Hybrid_CredentialFromTenantA_ShouldNotBeUsableInTenant var userA = await factory.CreateLoginUserAsync( - tenant: tenantA); + tenant: tenantA, ct: TestContext.Current.CancellationToken); using var loginClient = CreateClient( @@ -2210,7 +2210,7 @@ public async Task Refresh_Hybrid_CrossTenantAttempt_ShouldNotInvalidateSourceTen var userA = await factory.CreateLoginUserAsync( - tenant: tenantA); + tenant: tenantA, ct: TestContext.Current.CancellationToken); var deviceId = $"tenant-isolation-{Guid.NewGuid():N}"; @@ -2306,11 +2306,11 @@ public async Task Refresh_Hybrid_DifferentTenants_ShouldRotateIndependently() var userA = await factory.CreateLoginUserAsync( - tenant: tenantA); + tenant: tenantA, ct: TestContext.Current.CancellationToken); var userB = await factory.CreateLoginUserAsync( - tenant: tenantB); + tenant: tenantB, ct: TestContext.Current.CancellationToken); using var clientA = CreateClient( @@ -2379,11 +2379,11 @@ public async Task Refresh_Hybrid_SessionFromTenantAAndRefreshFromTenantB_ShouldB var userA = await factory.CreateLoginUserAsync( - tenant: tenantA); + tenant: tenantA, ct: TestContext.Current.CancellationToken); var userB = await factory.CreateLoginUserAsync( - tenant: tenantB); + tenant: tenantB, ct: TestContext.Current.CancellationToken); using var clientA = CreateClient(factory, deviceId); @@ -2444,7 +2444,7 @@ public async Task Refresh_Hybrid_Maui_HeaderCredentials_ShouldSupportConsecutive { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var client = CreateClient( $"maui-refresh-{Guid.NewGuid():N}"); @@ -2488,7 +2488,7 @@ public async Task Refresh_Hybrid_Maui_HeaderCredentials_ShouldSupportConsecutive first.StatusCode.Should().Be( HttpStatusCode.NoContent, - $"first refresh failed: {await first.Content.ReadAsStringAsync()}"); + $"first refresh failed: {await first.Content.ReadAsStringAsync(TestContext.Current.CancellationToken)}"); var rotatedRefreshToken = ReadCredential(first, "uar"); @@ -2517,7 +2517,7 @@ public async Task Refresh_Hybrid_Maui_HeaderCredentials_ShouldSupportConsecutive second.StatusCode.Should().Be( HttpStatusCode.NoContent, - $"second refresh failed: {await second.Content.ReadAsStringAsync()}"); + $"second refresh failed: {await second.Content.ReadAsStringAsync(TestContext.Current.CancellationToken)}"); } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/ResourceApi/ResourceApiAuthenticationTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/ResourceApi/ResourceApiAuthenticationTests.cs index 9d4c94b3..3a34937f 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration/ResourceApi/ResourceApiAuthenticationTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/ResourceApi/ResourceApiAuthenticationTests.cs @@ -36,7 +36,7 @@ await ResourceApiTestHost.CreateAsync( validator.Object); var response = await host.Client.GetAsync( - "/__tests/resource/anonymous"); + "/__tests/resource/anonymous", TestContext.Current.CancellationToken); response.StatusCode.Should().Be(HttpStatusCode.OK); @@ -61,7 +61,7 @@ await ResourceApiTestHost.CreateAsync( validator.Object); var response = await host.Client.GetAsync( - "/__tests/resource/authenticated"); + "/__tests/resource/authenticated", TestContext.Current.CancellationToken); response.StatusCode.Should() .Be(HttpStatusCode.Unauthorized); @@ -97,7 +97,7 @@ await ResourceApiTestHost.CreateAsync( AddSession(host.Client); var response = await host.Client.GetAsync( - "/__tests/resource/authenticated"); + "/__tests/resource/authenticated", TestContext.Current.CancellationToken); response.StatusCode.Should() .Be(HttpStatusCode.Unauthorized); @@ -126,7 +126,7 @@ await ResourceApiTestHost.CreateAsync( AddSession(host.Client); var response = await host.Client.GetAsync( - "/__tests/resource/authenticated"); + "/__tests/resource/authenticated", TestContext.Current.CancellationToken); response.StatusCode.Should() .Be(HttpStatusCode.OK); @@ -150,7 +150,7 @@ await ResourceApiTestHost.CreateAsync( AddSession(host.Client); var response = await host.Client.GetAsync( - "/__tests/resource/admin"); + "/__tests/resource/admin", TestContext.Current.CancellationToken); response.StatusCode.Should() .Be(HttpStatusCode.Forbidden); @@ -174,7 +174,7 @@ await ResourceApiTestHost.CreateAsync( AddSession(host.Client); var response = await host.Client.GetAsync( - "/__tests/resource/admin"); + "/__tests/resource/admin", TestContext.Current.CancellationToken); response.StatusCode.Should() .Be(HttpStatusCode.OK); @@ -200,14 +200,14 @@ await ResourceApiTestHost.CreateAsync( AddSession(host.Client); var response = await host.Client.GetAsync( - "/__tests/resource/identity"); + "/__tests/resource/identity", TestContext.Current.CancellationToken); response.StatusCode.Should() .Be(HttpStatusCode.OK); var body = await response.Content - .ReadFromJsonAsync(); + .ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); body.Should().NotBeNull(); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/ResourceApi/ResourceApiAuthorizationTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/ResourceApi/ResourceApiAuthorizationTests.cs index afc84bd6..013999c3 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration/ResourceApi/ResourceApiAuthorizationTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/ResourceApi/ResourceApiAuthorizationTests.cs @@ -50,7 +50,7 @@ public async Task PolicyEndpoint_WithoutCredential_ShouldReturnUnauthorized() await ResourceApiTestHost.CreateAsync(validator.Object); var response = await host.Client.GetAsync( - "/__tests/resource/products/read"); + "/__tests/resource/products/read", TestContext.Current.CancellationToken); response.StatusCode.Should() .Be(HttpStatusCode.Unauthorized); @@ -75,7 +75,7 @@ public async Task PolicyEndpoint_WithInvalidSession_ShouldReturnUnauthorized() AddSession(host.Client); var response = await host.Client.GetAsync( - "/__tests/resource/products/read"); + "/__tests/resource/products/read", TestContext.Current.CancellationToken); response.StatusCode.Should() .Be(HttpStatusCode.Unauthorized); @@ -99,7 +99,7 @@ await CreateHostAsync( AddSession(host.Client); var response = await host.Client.GetAsync( - "/__tests/resource/products/read"); + "/__tests/resource/products/read", TestContext.Current.CancellationToken); response.StatusCode.Should() .Be(HttpStatusCode.OK); @@ -129,7 +129,7 @@ await CreateHostAsync( AddSession(host.Client); var response = await host.Client.GetAsync( - "/__tests/resource/products/read"); + "/__tests/resource/products/read", TestContext.Current.CancellationToken); response.StatusCode.Should() .Be(HttpStatusCode.Forbidden); @@ -166,7 +166,7 @@ await CreateHostAsync( AddSession(host.Client); var response = await host.Client.GetAsync( - "/__tests/resource/products/read"); + "/__tests/resource/products/read", TestContext.Current.CancellationToken); response.StatusCode.Should() .Be(HttpStatusCode.OK); @@ -224,7 +224,7 @@ await CreateHostAsync( AddSession(host.Client); var response = await host.Client.GetAsync( - "/__tests/resource/products/read"); + "/__tests/resource/products/read", TestContext.Current.CancellationToken); response.StatusCode.Should() .Be(HttpStatusCode.OK); @@ -267,7 +267,7 @@ await CreateHostAsync( AddSession(host.Client); var response = await host.Client.GetAsync( - "/__tests/resource/products/read"); + "/__tests/resource/products/read", TestContext.Current.CancellationToken); response.StatusCode.Should() .Be(HttpStatusCode.OK); @@ -316,7 +316,7 @@ await CreateHostAsync( HttpMethod.Put, "/__tests/resource/products/42"); - var response = await host.Client.SendAsync(request); + var response = await host.Client.SendAsync(request, TestContext.Current.CancellationToken); response.StatusCode.Should() .Be(HttpStatusCode.OK); @@ -356,7 +356,7 @@ public async Task PolicyEndpoint_WhenReauthenticationRequired_ShouldReturn401Pro AddSession(host.Client); using var response = await host.Client.GetAsync( - "/__tests/resource/products/read"); + "/__tests/resource/products/read", TestContext.Current.CancellationToken); response.StatusCode.Should().Be(HttpStatusCode.Unauthorized); @@ -364,7 +364,7 @@ public async Task PolicyEndpoint_WhenReauthenticationRequired_ShouldReturn401Pro .MediaType.Should().Be("application/problem+json"); var problem = await response.Content - .ReadFromJsonAsync(); + .ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); problem.Should().NotBeNull(); problem!.Status.Should().Be(401); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/ResourceApi/ResourceApiTenantIsolationTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/ResourceApi/ResourceApiTenantIsolationTests.cs index bd199cd8..0610401b 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration/ResourceApi/ResourceApiTenantIsolationTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/ResourceApi/ResourceApiTenantIsolationTests.cs @@ -61,7 +61,7 @@ public async Task RequestTenant_ShouldBePassedToSessionValidation() AddTenant(host.Client, "tenant-a"); var response = await host.Client.GetAsync( - "/__tests/resource/authenticated"); + "/__tests/resource/authenticated", TestContext.Current.CancellationToken); response.StatusCode.Should() .Be(HttpStatusCode.OK); @@ -91,7 +91,7 @@ public async Task TenantA_WithTenantASession_ShouldAuthenticate() AddTenant(host.Client, "tenant-a"); var response = await host.Client.GetAsync( - "/__tests/resource/authenticated"); + "/__tests/resource/authenticated", TestContext.Current.CancellationToken); response.StatusCode.Should() .Be(HttpStatusCode.OK); @@ -118,7 +118,7 @@ public async Task TenantB_WithTenantASession_ShouldNotAuthenticate() AddTenant(host.Client, "tenant-b"); var response = await host.Client.GetAsync( - "/__tests/resource/authenticated"); + "/__tests/resource/authenticated", TestContext.Current.CancellationToken); response.StatusCode.Should() .Be(HttpStatusCode.Unauthorized); @@ -156,7 +156,7 @@ public async Task SameSessionId_ShouldRemainTenantScoped() AddTenant(host.Client, "tenant-a"); var tenantAResponse = await host.Client.GetAsync( - "/__tests/resource/authenticated"); + "/__tests/resource/authenticated", TestContext.Current.CancellationToken); tenantAResponse.StatusCode.Should() .Be(HttpStatusCode.OK); @@ -165,7 +165,7 @@ public async Task SameSessionId_ShouldRemainTenantScoped() AddTenant(host.Client, "tenant-b"); var tenantBResponse = await host.Client.GetAsync( - "/__tests/resource/authenticated"); + "/__tests/resource/authenticated", TestContext.Current.CancellationToken); tenantBResponse.StatusCode.Should() .Be(HttpStatusCode.Unauthorized); @@ -199,14 +199,14 @@ public async Task ResolvedTenant_ShouldBePreservedInValidationContext() AddTenant(host.Client, "tenant-a"); var tenantAResponse = await host.Client.GetAsync( - "/__tests/resource/authenticated"); + "/__tests/resource/authenticated", TestContext.Current.CancellationToken); host.Client.DefaultRequestHeaders.Remove(TenantHeader); AddTenant(host.Client, "tenant-b"); var tenantBResponse = await host.Client.GetAsync( - "/__tests/resource/authenticated"); + "/__tests/resource/authenticated", TestContext.Current.CancellationToken); tenantAResponse.StatusCode.Should() .Be(HttpStatusCode.OK); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/SdkEnpointContractTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/SdkEnpointContractTests.cs index 5f4d16da..b0200497 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration/SdkEnpointContractTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/SdkEnpointContractTests.cs @@ -24,7 +24,7 @@ public sealed class SdkEndpointContractTests public async Task ChangeMyStatus_ShouldReturnTypedJson_NotEmpty200() { using var factory = new AuthServerFactory(); - var user = await factory.CreateLoginUserAsync(); + var user = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var client = CreateClient(factory); await AuthenticateAsync(client, user); using var response = await CsrfTestClient.PostJsonAsync(client, "/auth/me/status", @@ -39,9 +39,9 @@ public async Task ChangeMyStatus_ShouldReturnTypedJson_NotEmpty200() public async Task ChangeUserStatus_ShouldReturnTypedJson_NotEmpty200() { using var factory = new AuthServerFactory(); - var admin = await factory.CreateLoginUserAsync(); - var target = await factory.CreateLoginUserAsync(); - await factory.GrantPermissionsAsync(admin.UserKey, [UAuthActions.Users.ChangeStatusAdmin]); + var admin = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var target = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + await factory.GrantPermissionsAsync(admin.UserKey, [UAuthActions.Users.ChangeStatusAdmin], TestContext.Current.CancellationToken); using var client = CreateClient(factory); await AuthenticateAsync(client, admin); using var response = await CsrfTestClient.PostJsonAsync( @@ -58,9 +58,9 @@ public async Task ChangeUserStatus_ShouldReturnTypedJson_NotEmpty200() public async Task ChangeUserStatus_AlreadyInRequestedStatus_ShouldReturnTypedJson() { using var factory = new AuthServerFactory(); - var admin = await factory.CreateLoginUserAsync(); - var target = await factory.CreateLoginUserAsync(); - await factory.GrantPermissionsAsync(admin.UserKey, [UAuthActions.Users.ChangeStatusAdmin]); + var admin = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var target = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + await factory.GrantPermissionsAsync(admin.UserKey, [UAuthActions.Users.ChangeStatusAdmin], TestContext.Current.CancellationToken); using var client = CreateClient(factory); await AuthenticateAsync(client, admin); var url = $"/auth/admin/users/{target.UserKey.Value}/status"; @@ -78,9 +78,9 @@ public async Task ChangeUserStatus_AlreadyInRequestedStatus_ShouldReturnTypedJso public async Task DeleteUser_ShouldReturnTypedJson_WithRequestedMode() { using var factory = new AuthServerFactory(); - var admin = await factory.CreateLoginUserAsync(); - var target = await factory.CreateLoginUserAsync(); - await factory.GrantPermissionsAsync(admin.UserKey, [UAuthActions.Users.DeleteAdmin]); + var admin = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var target = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + await factory.GrantPermissionsAsync(admin.UserKey, [UAuthActions.Users.DeleteAdmin], TestContext.Current.CancellationToken); using var client = CreateClient(factory); await AuthenticateAsync(client, admin); using var response = await CsrfTestClient.PostJsonAsync( @@ -97,9 +97,9 @@ public async Task AdminBeginReset_ShouldReturnTypedJson_WithoutProof() { var notifier = new CapturingResetNotifier(); using var factory = CreateResetFactory(notifier); - var admin = await factory.CreateLoginUserAsync(); - var target = await factory.CreateLoginUserAsync(); - await factory.GrantPermissionsAsync(admin.UserKey, [UAuthActions.Credentials.BeginResetAdmin]); + var admin = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var target = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + await factory.GrantPermissionsAsync(admin.UserKey, [UAuthActions.Credentials.BeginResetAdmin], TestContext.Current.CancellationToken); using var client = CreateClient(factory); await AuthenticateAsync(client, admin); using var response = await CsrfTestClient.PostJsonAsync( @@ -107,7 +107,7 @@ public async Task AdminBeginReset_ShouldReturnTypedJson_WithoutProof() $"/auth/admin/users/{target.UserKey.Value}/credentials/reset/begin", BeginResetRequest(target)); var result = await AssertTypedSuccessAsync(response); result.ExpiresAt.Should().BeAfter(factory.Clock.UtcNow); - var body = await response.Content.ReadAsStringAsync(); + var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); var notification = notifier.Notifications.Should().ContainSingle(x => x.UserKey == target.UserKey).Subject; body.Should().NotContain(notification.Token, "reset proof must only be sent through the notifier"); } @@ -117,10 +117,10 @@ public async Task AdminCompleteReset_ShouldReturnTypedJson_AndChangeCredential() { var notifier = new CapturingResetNotifier(); using var factory = CreateResetFactory(notifier); - var admin = await factory.CreateLoginUserAsync(); - var target = await factory.CreateLoginUserAsync(); + var admin = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var target = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await factory.GrantPermissionsAsync(admin.UserKey, - [UAuthActions.Credentials.BeginResetAdmin, UAuthActions.Credentials.CompleteResetAdmin]); + [UAuthActions.Credentials.BeginResetAdmin, UAuthActions.Credentials.CompleteResetAdmin], TestContext.Current.CancellationToken); using var client = CreateClient(factory); await AuthenticateAsync(client, admin); var url = $"/auth/admin/users/{target.UserKey.Value}/credentials/reset"; @@ -145,14 +145,14 @@ public async Task AdminBeginReset_TargetMismatch_ShouldReturn400_AndNotNotify() { var notifier = new CapturingResetNotifier(); using var factory = CreateResetFactory(notifier); - var admin = await factory.CreateLoginUserAsync(); - var target = await factory.CreateLoginUserAsync(); - var other = await factory.CreateLoginUserAsync(); - await factory.GrantPermissionsAsync(admin.UserKey, [UAuthActions.Credentials.BeginResetAdmin]); + var admin = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var target = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var other = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + await factory.GrantPermissionsAsync(admin.UserKey, [UAuthActions.Credentials.BeginResetAdmin], TestContext.Current.CancellationToken); using var client = CreateClient(factory); await AuthenticateAsync(client, admin); using var response = await client.PostAsJsonAsync( - $"/auth/admin/users/{target.UserKey.Value}/credentials/reset/begin", BeginResetRequest(other)); + $"/auth/admin/users/{target.UserKey.Value}/credentials/reset/begin", BeginResetRequest(other), cancellationToken: TestContext.Current.CancellationToken); response.StatusCode.Should().Be(HttpStatusCode.BadRequest); notifier.Notifications.Should().BeEmpty(); } @@ -161,7 +161,7 @@ public async Task AdminBeginReset_TargetMismatch_ShouldReturn400_AndNotNotify() public async Task RevokeMy_UnknownCredential_ShouldNotReturnSuccessful204() { using var factory = new AuthServerFactory(); - var user = await factory.CreateLoginUserAsync(); + var user = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var client = CreateClient(factory); await AuthenticateAsync(client, user); using var response = await CsrfTestClient.PostJsonAsync(client, "/auth/me/credentials/revoke", @@ -173,9 +173,9 @@ public async Task RevokeMy_UnknownCredential_ShouldNotReturnSuccessful204() public async Task RevokeAdmin_UnknownCredential_ShouldNotReturnSuccessful204() { using var factory = new AuthServerFactory(); - var admin = await factory.CreateLoginUserAsync(); - var target = await factory.CreateLoginUserAsync(); - await factory.GrantPermissionsAsync(admin.UserKey, [UAuthActions.Credentials.RevokeAdmin]); + var admin = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var target = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + await factory.GrantPermissionsAsync(admin.UserKey, [UAuthActions.Credentials.RevokeAdmin], TestContext.Current.CancellationToken); using var client = CreateClient(factory); await AuthenticateAsync(client, admin); using var response = await CsrfTestClient.PostJsonAsync(client, @@ -188,9 +188,9 @@ public async Task RevokeAdmin_UnknownCredential_ShouldNotReturnSuccessful204() public async Task DeleteCredentialAdmin_UnknownCredential_ShouldNotReturnSuccessful204() { using var factory = new AuthServerFactory(); - var admin = await factory.CreateLoginUserAsync(); - var target = await factory.CreateLoginUserAsync(); - await factory.GrantPermissionsAsync(admin.UserKey, [UAuthActions.Credentials.DeleteAdmin]); + var admin = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var target = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + await factory.GrantPermissionsAsync(admin.UserKey, [UAuthActions.Credentials.DeleteAdmin], TestContext.Current.CancellationToken); using var client = CreateClient(factory); await AuthenticateAsync(client, admin); using var response = await CsrfTestClient.PostJsonAsync(client, diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/Security/CsrfAttackTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/Security/CsrfAttackTests.cs index 1050c53a..52d4e724 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration/Security/CsrfAttackTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/Security/CsrfAttackTests.cs @@ -13,7 +13,7 @@ public sealed class CsrfAttackTests public async Task Login_WithoutCsrfToken_MustNotCreateSession() { await using var factory = new AuthServerFactory(); - var attacker = await factory.CreateLoginUserAsync(); + var attacker = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var victimBrowser = CreateBrowser(factory); @@ -23,7 +23,7 @@ public async Task Login_WithoutCsrfToken_MustNotCreateSession() { identifier = attacker.Identifier, secret = attacker.Secret - }); + }, cancellationToken: TestContext.Current.CancellationToken); await AssertCsrfRejected(response); AssertNoSessionCookie(response); @@ -33,7 +33,7 @@ public async Task Login_WithoutCsrfToken_MustNotCreateSession() public async Task Login_WithForgedCsrfToken_MustNotCreateSession() { await using var factory = new AuthServerFactory(); - var attacker = await factory.CreateLoginUserAsync(); + var attacker = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var victimBrowser = CreateBrowser(factory); @@ -51,7 +51,7 @@ public async Task Login_WithForgedCsrfToken_MustNotCreateSession() "RequestVerificationToken", "attacker-forged-token"); - using var response = await victimBrowser.SendAsync(request); + using var response = await victimBrowser.SendAsync(request, TestContext.Current.CancellationToken); await AssertCsrfRejected(response); AssertNoSessionCookie(response); @@ -61,7 +61,7 @@ public async Task Login_WithForgedCsrfToken_MustNotCreateSession() public async Task Login_WithTokenFromAnotherBrowser_MustBeRejected() { await using var factory = new AuthServerFactory(); - var attacker = await factory.CreateLoginUserAsync(); + var attacker = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var attackerBrowser = CreateBrowser(factory); using var victimBrowser = CreateBrowser(factory); @@ -89,7 +89,7 @@ public async Task Login_WithTokenFromAnotherBrowser_MustBeRejected() "Cookie", victimTokens.Cookie); - using var response = await victimBrowser.SendAsync(request); + using var response = await victimBrowser.SendAsync(request, TestContext.Current.CancellationToken); await AssertCsrfRejected(response); AssertNoSessionCookie(response); @@ -99,7 +99,7 @@ public async Task Login_WithTokenFromAnotherBrowser_MustBeRejected() public async Task Login_WithValidCsrfToken_ShouldSucceed() { await using var factory = new AuthServerFactory(); - var user = await factory.CreateLoginUserAsync(); + var user = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var browser = CreateBrowser(factory); var tokens = await GetTokens(browser); @@ -122,7 +122,7 @@ public async Task Login_WithValidCsrfToken_ShouldSucceed() "Cookie", tokens.Cookie); - using var response = await browser.SendAsync(request); + using var response = await browser.SendAsync(request, TestContext.Current.CancellationToken); response.StatusCode.Should().Be(HttpStatusCode.Found); @@ -138,7 +138,7 @@ public async Task Login_WithValidCsrfToken_ShouldSucceed() public async Task RevokeOthers_CsrfAttack_MustNotRevokeSessions() { await using var factory = new AuthServerFactory(); - var user = await factory.CreateLoginUserAsync(); + var user = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var currentBrowser = CreateBrowser(factory); using var otherBrowser = CreateBrowser(factory); @@ -159,7 +159,7 @@ public async Task RevokeOthers_CsrfAttack_MustNotRevokeSessions() // ATTACK 1: Authenticated request without CSRF token. using (var missingToken = await currentBrowser.PostAsync( - "/auth/me/sessions/revoke-others", null)) + "/auth/me/sessions/revoke-others", null, TestContext.Current.CancellationToken)) { await AssertCsrfRejected(missingToken); } @@ -177,7 +177,7 @@ public async Task RevokeOthers_CsrfAttack_MustNotRevokeSessions() "forged-token"); using var forged = await currentBrowser.SendAsync( - forgedRequest); + forgedRequest, TestContext.Current.CancellationToken); await AssertCsrfRejected(forged); } @@ -192,7 +192,7 @@ public async Task RevokeOthers_CsrfAttack_MustNotRevokeSessions() { valid.IsSuccessStatusCode.Should().BeTrue( $"valid revoke failed: " + - await valid.Content.ReadAsStringAsync()); + await valid.Content.ReadAsStringAsync(TestContext.Current.CancellationToken)); } // The current session must survive revoke-others. diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/Security/UAuthAntiforgeryEndpointTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/Security/UAuthAntiforgeryEndpointTests.cs index a926f892..c9c4cc07 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration/Security/UAuthAntiforgeryEndpointTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/Security/UAuthAntiforgeryEndpointTests.cs @@ -21,7 +21,7 @@ public async Task TokenEndpoint_ReturnsValidTokenAndNoStore() using var client = factory.CreateClient(); using var response = await client.GetAsync( - "/auth/antiforgery/token"); + "/auth/antiforgery/token", TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -30,7 +30,7 @@ public async Task TokenEndpoint_ReturnsValidTokenAndNoStore() response.Headers.CacheControl?.ToString() ?? ""); var token = await response.Content - .ReadFromJsonAsync(); + .ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); Assert.NotNull(token); Assert.False(string.IsNullOrWhiteSpace(token.RequestToken)); @@ -62,11 +62,11 @@ public async Task Login_WithoutToken_IsRejected() { identifier = "csrf-test-user", secret = "irrelevant" - }); + }, cancellationToken: TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); - var body = await response.Content.ReadAsStringAsync(); + var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); Assert.Contains( "invalid_antiforgery_token", @@ -85,7 +85,7 @@ public async Task Login_WithInvalidToken_IsRejected() HandleCookies = true }); - await client.GetAsync("/auth/antiforgery/token"); + await client.GetAsync("/auth/antiforgery/token", TestContext.Current.CancellationToken); using var request = new HttpRequestMessage( HttpMethod.Post, @@ -102,7 +102,7 @@ public async Task Login_WithInvalidToken_IsRejected() "X-UAuth-CSRF", "invalid-token"); - using var response = await client.SendAsync(request); + using var response = await client.SendAsync(request, TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } @@ -120,7 +120,7 @@ public async Task Login_WithValidToken_PassesCsrfValidation() }); var token = await client.GetFromJsonAsync( - "/auth/antiforgery/token"); + "/auth/antiforgery/token", cancellationToken: TestContext.Current.CancellationToken); Assert.NotNull(token); @@ -139,9 +139,9 @@ public async Task Login_WithValidToken_PassesCsrfValidation() token.HeaderName, token.RequestToken); - using var response = await client.SendAsync(request); + using var response = await client.SendAsync(request, TestContext.Current.CancellationToken); - var body = await response.Content.ReadAsStringAsync(); + var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); Assert.DoesNotContain( "invalid_antiforgery_token", @@ -169,10 +169,10 @@ public async Task TokenFromAnotherClient_IsRejected() var firstToken = await firstClient.GetFromJsonAsync( - "/auth/antiforgery/token"); + "/auth/antiforgery/token", cancellationToken: TestContext.Current.CancellationToken); // Ensure the second client has its own antiforgery cookie. - await secondClient.GetAsync("/auth/antiforgery/token"); + await secondClient.GetAsync("/auth/antiforgery/token", TestContext.Current.CancellationToken); Assert.NotNull(firstToken); @@ -191,11 +191,11 @@ await firstClient.GetFromJsonAsync( firstToken.HeaderName, firstToken.RequestToken); - using var response = await secondClient.SendAsync(request); + using var response = await secondClient.SendAsync(request, TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); - var body = await response.Content.ReadAsStringAsync(); + var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); Assert.Contains( "invalid_antiforgery_token", diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/SessionAdminTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/SessionAdminTests.cs index 8784d760..c6452ad3 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration/SessionAdminTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/SessionAdminTests.cs @@ -23,15 +23,15 @@ public async Task ListChainsAdmin_ShouldReturnTargetUsersChains() { _factory.Clock.Reset(); - var admin = await _factory.CreateLoginUserAsync(); - var target = await _factory.CreateLoginUserAsync(); - var unrelated = await _factory.CreateLoginUserAsync(); + var admin = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var target = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var unrelated = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await _factory.GrantPermissionsAsync( admin.UserKey, [ UAuthActions.Sessions.ListChainsAdmin - ]); + ], TestContext.Current.CancellationToken); using var adminClient = CreateClient( $"session-admin-list-{Guid.NewGuid():N}"); @@ -76,12 +76,12 @@ await LoginAsync( { PageNumber = 1, PageSize = 50 - }); + }, cancellationToken: TestContext.Current.CancellationToken); response.StatusCode.Should().Be(HttpStatusCode.OK); var page = await response.Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(cancellationToken: TestContext.Current.CancellationToken); page.Should().NotBeNull(); @@ -103,14 +103,14 @@ public async Task GetChainAdmin_ShouldReturnTargetUsersOwnedChain() { _factory.Clock.Reset(); - var admin = await _factory.CreateLoginUserAsync(); - var target = await _factory.CreateLoginUserAsync(); + var admin = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var target = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await _factory.GrantPermissionsAsync( admin.UserKey, [ UAuthActions.Sessions.GetChainAdmin - ]); + ], TestContext.Current.CancellationToken); using var adminClient = CreateClient( $"session-admin-detail-{Guid.NewGuid():N}"); @@ -143,7 +143,7 @@ await LoginAsync( targetChains.StatusCode.Should().Be(HttpStatusCode.OK); var page = await targetChains.Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(cancellationToken: TestContext.Current.CancellationToken); page.Should().NotBeNull(); @@ -152,12 +152,12 @@ await LoginAsync( var response = await adminClient.PostAsync( $"/auth/admin/users/{target.UserKey.Value}/sessions/chains/{targetChain.ChainId.Value}", - null); + null, TestContext.Current.CancellationToken); response.StatusCode.Should().Be(HttpStatusCode.OK); var detail = await response.Content - .ReadFromJsonAsync(); + .ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); detail.Should().NotBeNull(); @@ -176,16 +176,16 @@ public async Task GetChainAdmin_ShouldRejectChainOwnedByDifferentUser() { _factory.Clock.Reset(); - var admin = await _factory.CreateLoginUserAsync(); + var admin = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); - var targetA = await _factory.CreateLoginUserAsync(); - var targetB = await _factory.CreateLoginUserAsync(); + var targetA = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var targetB = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await _factory.GrantPermissionsAsync( admin.UserKey, [ UAuthActions.Sessions.GetChainAdmin - ]); + ], TestContext.Current.CancellationToken); using var adminClient = CreateClient( $"session-admin-cross-user-{Guid.NewGuid():N}"); @@ -233,7 +233,7 @@ await LoginAsync( await GetChainsAsync(targetBClient); var targetBPage = await targetBChains.Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(cancellationToken: TestContext.Current.CancellationToken); targetBPage.Should().NotBeNull(); @@ -245,7 +245,7 @@ await LoginAsync( // var response = await adminClient.PostAsync( $"/auth/admin/users/{targetA.UserKey.Value}/sessions/chains/{targetBChain.ChainId.Value}", - null); + null, TestContext.Current.CancellationToken); response.StatusCode.Should().BeOneOf( HttpStatusCode.BadRequest, @@ -274,8 +274,8 @@ public async Task SessionAdminQueryEndpoints_ShouldRequireAdminPermission() { _factory.Clock.Reset(); - var actor = await _factory.CreateLoginUserAsync(); - var target = await _factory.CreateLoginUserAsync(); + var actor = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var target = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var actorClient = CreateClient( $"session-admin-no-permission-{Guid.NewGuid():N}"); @@ -307,7 +307,7 @@ await LoginAsync( await GetChainsAsync(targetClient); var targetPage = await targetChains.Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(cancellationToken: TestContext.Current.CancellationToken); targetPage.Should().NotBeNull(); @@ -316,7 +316,7 @@ await LoginAsync( var list = await actorClient.PostAsJsonAsync( $"/auth/admin/users/{target.UserKey.Value}/sessions/chains", - new PageRequest()); + new PageRequest(), cancellationToken: TestContext.Current.CancellationToken); list.StatusCode.Should().BeOneOf( HttpStatusCode.Forbidden, @@ -324,7 +324,7 @@ await LoginAsync( var detail = await actorClient.PostAsync( $"/auth/admin/users/{target.UserKey.Value}/sessions/chains/{targetChain.ChainId.Value}", - null); + null, TestContext.Current.CancellationToken); detail.StatusCode.Should().BeOneOf( HttpStatusCode.Forbidden, @@ -343,7 +343,7 @@ await LoginAsync( [Fact] public async Task SessionAdminQueryEndpoints_ShouldRejectUnauthenticatedRequests() { - var target = await _factory.CreateLoginUserAsync(); + var target = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var anonymous = CreateClient( $"session-admin-anonymous-{Guid.NewGuid():N}"); @@ -352,14 +352,14 @@ public async Task SessionAdminQueryEndpoints_ShouldRejectUnauthenticatedRequests var list = await anonymous.PostAsJsonAsync( $"/auth/admin/users/{target.UserKey.Value}/sessions/chains", - new PageRequest()); + new PageRequest(), cancellationToken: TestContext.Current.CancellationToken); list.StatusCode.Should() .Be(HttpStatusCode.Unauthorized); var detail = await anonymous.PostAsync( $"/auth/admin/users/{target.UserKey.Value}/sessions/chains/{randomChain.Value}", - null); + null, TestContext.Current.CancellationToken); detail.StatusCode.Should() .Be(HttpStatusCode.Unauthorized); @@ -370,14 +370,14 @@ public async Task RevokeSessionAdmin_ShouldRevokeTargetUsersSession() { _factory.Clock.Reset(); - var admin = await _factory.CreateLoginUserAsync(); - var target = await _factory.CreateLoginUserAsync(); + var admin = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var target = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await _factory.GrantPermissionsAsync( admin.UserKey, [ UAuthActions.Sessions.RevokeSessionAdmin - ]); + ], TestContext.Current.CancellationToken); using var adminClient = CreateClient( $"session-revoke-admin-{Guid.NewGuid():N}"); @@ -405,7 +405,7 @@ await LoginAsync( chainsResponse.StatusCode.Should().Be(HttpStatusCode.OK); var page = await chainsResponse.Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(cancellationToken: TestContext.Current.CancellationToken); page.Should().NotBeNull(); @@ -439,15 +439,15 @@ public async Task RevokeSessionAdmin_ShouldRejectSessionOwnedByDifferentUser() { _factory.Clock.Reset(); - var admin = await _factory.CreateLoginUserAsync(); - var targetA = await _factory.CreateLoginUserAsync(); - var targetB = await _factory.CreateLoginUserAsync(); + var admin = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var targetA = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var targetB = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await _factory.GrantPermissionsAsync( admin.UserKey, [ UAuthActions.Sessions.RevokeSessionAdmin - ]); + ], TestContext.Current.CancellationToken); using var adminClient = CreateClient( $"session-revoke-cross-admin-{Guid.NewGuid():N}"); @@ -488,7 +488,7 @@ await LoginAsync( bChainsResponse.StatusCode.Should().Be(HttpStatusCode.OK); var bPage = await bChainsResponse.Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(cancellationToken: TestContext.Current.CancellationToken); bPage.Should().NotBeNull(); @@ -503,7 +503,7 @@ await LoginAsync( // var attack = await adminClient.PostAsync( $"/auth/admin/users/{targetA.UserKey.Value}/sessions/{bSessionId.Value}/revoke", - null); + null, TestContext.Current.CancellationToken); attack.StatusCode.Should().BeOneOf( HttpStatusCode.BadRequest, @@ -527,8 +527,8 @@ public async Task RevokeSessionAdmin_ShouldRequirePermission() { _factory.Clock.Reset(); - var actor = await _factory.CreateLoginUserAsync(); - var target = await _factory.CreateLoginUserAsync(); + var actor = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var target = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var actorClient = CreateClient( $"session-revoke-no-permission-{Guid.NewGuid():N}"); @@ -554,7 +554,7 @@ await LoginAsync( var chainsResponse = await GetChainsAsync(targetClient); var page = await chainsResponse.Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(cancellationToken: TestContext.Current.CancellationToken); page.Should().NotBeNull(); @@ -586,14 +586,14 @@ public async Task RevokeSessionAdmin_ShouldNotAffectOtherSessionOfSameUser() { _factory.Clock.Reset(); - var admin = await _factory.CreateLoginUserAsync(); - var target = await _factory.CreateLoginUserAsync(); + var admin = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var target = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await _factory.GrantPermissionsAsync( admin.UserKey, [ UAuthActions.Sessions.RevokeSessionAdmin - ]); + ], TestContext.Current.CancellationToken); using var adminClient = CreateClient( $"session-single-admin-{Guid.NewGuid():N}"); @@ -632,7 +632,7 @@ await LoginAsync( var chainsResponse = await GetChainsAsync(targetClient1); var page = await chainsResponse.Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(cancellationToken: TestContext.Current.CancellationToken); page.Should().NotBeNull(); @@ -676,14 +676,14 @@ public async Task RevokeChainAdmin_ShouldRevokeTargetChain() { _factory.Clock.Reset(); - var admin = await _factory.CreateLoginUserAsync(); - var target = await _factory.CreateLoginUserAsync(); + var admin = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var target = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await _factory.GrantPermissionsAsync( admin.UserKey, [ UAuthActions.Sessions.RevokeChainAdmin - ]); + ], TestContext.Current.CancellationToken); using var adminClient = CreateClient( $"revoke-chain-admin-{Guid.NewGuid():N}"); @@ -711,7 +711,7 @@ await LoginAsync( chainsResponse.StatusCode.Should().Be(HttpStatusCode.OK); var page = await chainsResponse.Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(cancellationToken: TestContext.Current.CancellationToken); page.Should().NotBeNull(); @@ -748,15 +748,15 @@ public async Task RevokeChainAdmin_ShouldRejectChainOwnedByDifferentUser() { _factory.Clock.Reset(); - var admin = await _factory.CreateLoginUserAsync(); - var targetA = await _factory.CreateLoginUserAsync(); - var targetB = await _factory.CreateLoginUserAsync(); + var admin = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var targetA = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var targetB = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await _factory.GrantPermissionsAsync( admin.UserKey, [ UAuthActions.Sessions.RevokeChainAdmin - ]); + ], TestContext.Current.CancellationToken); using var adminClient = CreateClient( $"revoke-chain-cross-admin-{Guid.NewGuid():N}"); @@ -796,7 +796,7 @@ await LoginAsync( .Be(HttpStatusCode.OK); var bPage = await bChainsResponse.Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(cancellationToken: TestContext.Current.CancellationToken); bPage.Should().NotBeNull(); @@ -809,7 +809,7 @@ await LoginAsync( // var attack = await adminClient.PostAsync( $"/auth/admin/users/{targetA.UserKey.Value}/sessions/chains/{bChain.ChainId.Value}/revoke", - null); + null, TestContext.Current.CancellationToken); attack.StatusCode.Should().BeOneOf( HttpStatusCode.BadRequest, @@ -843,14 +843,14 @@ public async Task RevokeChainAdmin_ShouldNotAffectOtherChainOfSameUser() { _factory.Clock.Reset(); - var admin = await _factory.CreateLoginUserAsync(); - var target = await _factory.CreateLoginUserAsync(); + var admin = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var target = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await _factory.GrantPermissionsAsync( admin.UserKey, [ UAuthActions.Sessions.RevokeChainAdmin - ]); + ], TestContext.Current.CancellationToken); using var adminClient = CreateClient( $"revoke-chain-isolation-admin-{Guid.NewGuid():N}"); @@ -890,7 +890,7 @@ await LoginAsync( await GetChainsAsync(targetClient1); var page = await chainsResponse.Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(cancellationToken: TestContext.Current.CancellationToken); page.Should().NotBeNull(); @@ -939,8 +939,8 @@ public async Task RevokeChainAdmin_ShouldRequirePermissionWithoutMutation() { _factory.Clock.Reset(); - var actor = await _factory.CreateLoginUserAsync(); - var target = await _factory.CreateLoginUserAsync(); + var actor = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var target = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var actorClient = CreateClient( $"revoke-chain-denied-actor-{Guid.NewGuid():N}"); @@ -967,7 +967,7 @@ await LoginAsync( await GetChainsAsync(targetClient); var page = await chainsResponse.Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(cancellationToken: TestContext.Current.CancellationToken); page.Should().NotBeNull(); @@ -997,12 +997,12 @@ public async Task RevokeAllChainsAdmin_ShouldRevokeAllTargetUsersChains() { _factory.Clock.Reset(); - var admin = await _factory.CreateLoginUserAsync(); - var target = await _factory.CreateLoginUserAsync(); + var admin = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var target = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await _factory.GrantPermissionsAsync( admin.UserKey, - [UAuthActions.Sessions.RevokeAllChainsAdmin]); + [UAuthActions.Sessions.RevokeAllChainsAdmin], TestContext.Current.CancellationToken); using var adminClient = CreateClient( $"revoke-all-admin-{Guid.NewGuid():N}"); @@ -1071,13 +1071,13 @@ public async Task RevokeAllChainsAdmin_ShouldNotAffectDifferentUser() { _factory.Clock.Reset(); - var admin = await _factory.CreateLoginUserAsync(); - var target = await _factory.CreateLoginUserAsync(); - var unrelated = await _factory.CreateLoginUserAsync(); + var admin = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var target = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var unrelated = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await _factory.GrantPermissionsAsync( admin.UserKey, - [UAuthActions.Sessions.RevokeAllChainsAdmin]); + [UAuthActions.Sessions.RevokeAllChainsAdmin], TestContext.Current.CancellationToken); using var adminClient = CreateClient( $"revoke-all-isolation-admin-{Guid.NewGuid():N}"); @@ -1140,8 +1140,8 @@ public async Task RevokeAllChainsAdmin_ShouldRequirePermissionWithoutMutation() { _factory.Clock.Reset(); - var actor = await _factory.CreateLoginUserAsync(); - var target = await _factory.CreateLoginUserAsync(); + var actor = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var target = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var actorClient = CreateClient( $"revoke-all-denied-actor-{Guid.NewGuid():N}"); @@ -1197,7 +1197,7 @@ public async Task RevokeAllChainsAdmin_ShouldRejectUnauthenticatedRequestWithout { _factory.Clock.Reset(); - var target = await _factory.CreateLoginUserAsync(); + var target = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var anonymousClient = CreateClient( $"revoke-all-anonymous-{Guid.NewGuid():N}"); @@ -1235,12 +1235,12 @@ public async Task RevokeRootAdmin_ShouldInvalidateAllExistingTargetSessions() { _factory.Clock.Reset(); - var admin = await _factory.CreateLoginUserAsync(); - var target = await _factory.CreateLoginUserAsync(); + var admin = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var target = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await _factory.GrantPermissionsAsync( admin.UserKey, - [UAuthActions.Sessions.RevokeRootAdmin]); + [UAuthActions.Sessions.RevokeRootAdmin], TestContext.Current.CancellationToken); using var adminClient = CreateClient( $"root-admin-{Guid.NewGuid():N}"); @@ -1302,13 +1302,13 @@ public async Task RevokeRootAdmin_ShouldNotAffectDifferentUser() { _factory.Clock.Reset(); - var admin = await _factory.CreateLoginUserAsync(); - var target = await _factory.CreateLoginUserAsync(); - var unrelated = await _factory.CreateLoginUserAsync(); + var admin = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var target = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var unrelated = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await _factory.GrantPermissionsAsync( admin.UserKey, - [UAuthActions.Sessions.RevokeRootAdmin]); + [UAuthActions.Sessions.RevokeRootAdmin], TestContext.Current.CancellationToken); using var adminClient = CreateClient( $"root-isolation-admin-{Guid.NewGuid():N}"); @@ -1362,8 +1362,8 @@ public async Task RevokeRootAdmin_ShouldRequirePermissionWithoutMutation() { _factory.Clock.Reset(); - var actor = await _factory.CreateLoginUserAsync(); - var target = await _factory.CreateLoginUserAsync(); + var actor = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var target = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var actorClient = CreateClient( $"root-denied-actor-{Guid.NewGuid():N}"); @@ -1394,7 +1394,7 @@ public async Task RevokeRootAdmin_ShouldRejectUnauthenticatedRequestWithoutMutat { _factory.Clock.Reset(); - var target = await _factory.CreateLoginUserAsync(); + var target = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var anonymousClient = CreateClient($"root-anonymous-{Guid.NewGuid():N}"); using var targetClient = CreateClient($"root-anonymous-target-{Guid.NewGuid():N}"); @@ -1413,10 +1413,10 @@ public async Task RevokeRootAdmin_ShouldAllowSubsequentLoginWithNewAuthenticatio { _factory.Clock.Reset(); - var admin = await _factory.CreateLoginUserAsync(); - var target = await _factory.CreateLoginUserAsync(); + var admin = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var target = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); - await _factory.GrantPermissionsAsync(admin.UserKey, [UAuthActions.Sessions.RevokeRootAdmin]); + await _factory.GrantPermissionsAsync(admin.UserKey, [UAuthActions.Sessions.RevokeRootAdmin], TestContext.Current.CancellationToken); var adminDevice = $"root-recreate-admin-{Guid.NewGuid():N}"; @@ -1479,12 +1479,12 @@ public async Task RevokeRootAdmin_ConcurrentSubsequentLogins_ShouldCreateSingleA { _factory.Clock.Reset(); - var admin = await _factory.CreateLoginUserAsync(); - var target = await _factory.CreateLoginUserAsync(); + var admin = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var target = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await _factory.GrantPermissionsAsync( admin.UserKey, - [UAuthActions.Sessions.RevokeRootAdmin]); + [UAuthActions.Sessions.RevokeRootAdmin], TestContext.Current.CancellationToken); using var adminClient = CreateClient($"root-concurrent-admin-{Guid.NewGuid():N}"); @@ -1579,7 +1579,7 @@ await _factory.GrantPermissionsAsync( verificationB.StatusCode.Should().Be(HttpStatusCode.OK); var chainsA = await verificationA.Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(cancellationToken: TestContext.Current.CancellationToken); chainsA.Should().NotBeNull(); @@ -1592,7 +1592,7 @@ await _factory.GrantPermissionsAsync( .Should().Be(1); var chainsB = await verificationB.Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(cancellationToken: TestContext.Current.CancellationToken); chainsB.Should().NotBeNull(); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/SessionSlidingExpirationTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/SessionSlidingExpirationTests.cs index 3a914d4f..5f84aba7 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration/SessionSlidingExpirationTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/SessionSlidingExpirationTests.cs @@ -132,7 +132,7 @@ public async Task IdentifierGet_ExpiredIdleSession_ShouldReturnUnauthorized(stri o.Session.TouchInterval = TimeSpan.FromSeconds(5); }); - var user = await factory.CreateLoginUserAsync(); + var user = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var client = factory.CreateClient( new WebApplicationFactoryClientOptions @@ -163,7 +163,7 @@ public async Task IdentifierGet_ExpiredIdleSession_ShouldReturnUnauthorized(stri // Same session must be accepted before IdleTimeout. using var before = await client.PostAsJsonAsync( - "/auth/me/identifiers/get", new { }); + "/auth/me/identifiers/get", new { }, cancellationToken: TestContext.Current.CancellationToken); before.StatusCode.Should().Be(HttpStatusCode.OK); @@ -172,7 +172,7 @@ public async Task IdentifierGet_ExpiredIdleSession_ShouldReturnUnauthorized(stri // Keep the original cookie deliberately. using var after = await client.PostAsJsonAsync( - "/auth/me/identifiers/get", new { }); + "/auth/me/identifiers/get", new { }, cancellationToken: TestContext.Current.CancellationToken); after.StatusCode.Should().Be(HttpStatusCode.Unauthorized, "an expired session must not remain authenticated " + diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/SessionTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/SessionTests.cs index 9b5efad1..c029a1a3 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration/SessionTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/SessionTests.cs @@ -23,8 +23,8 @@ public async Task ListChainsSelf_ShouldReturnOnlyAuthenticatedUsersChains() { _factory.Clock.Reset(); - var user1 = await _factory.CreateLoginUserAsync(); - var user2 = await _factory.CreateLoginUserAsync(); + var user1 = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var user2 = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var user1Device1 = CreateClient( $"session-user1-device1-{Guid.NewGuid():N}"); @@ -57,7 +57,7 @@ await LoginAsync( response.StatusCode.Should().Be(HttpStatusCode.OK); var page = await response.Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(cancellationToken: TestContext.Current.CancellationToken); page.Should().NotBeNull(); @@ -70,7 +70,7 @@ public async Task GetChainSelf_ShouldReturnRequestedOwnedChain() { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var client = CreateClient( $"session-detail-{Guid.NewGuid():N}"); @@ -90,7 +90,7 @@ await LoginAsync( chainsResponse.StatusCode.Should().Be(HttpStatusCode.OK); var page = await chainsResponse.Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(cancellationToken: TestContext.Current.CancellationToken); page.Should().NotBeNull(); @@ -98,12 +98,12 @@ await LoginAsync( var detailResponse = await client.PostAsync( $"/auth/me/sessions/chains/{chain.ChainId.Value}", - null); + null, TestContext.Current.CancellationToken); detailResponse.StatusCode.Should().Be(HttpStatusCode.OK); var detail = await detailResponse.Content - .ReadFromJsonAsync(); + .ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); detail.Should().NotBeNull(); detail!.ChainId.Should().Be(chain.ChainId); @@ -116,8 +116,8 @@ public async Task GetChainSelf_ShouldNotAllowReadingAnotherUsersChain() { _factory.Clock.Reset(); - var attacker = await _factory.CreateLoginUserAsync(); - var victim = await _factory.CreateLoginUserAsync(); + var attacker = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var victim = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var attackerClient = CreateClient( $"session-attacker-{Guid.NewGuid():N}"); @@ -149,7 +149,7 @@ await LoginAsync( await GetChainsAsync(victimClient); var victimChains = await victimChainsResponse.Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(cancellationToken: TestContext.Current.CancellationToken); victimChains.Should().NotBeNull(); @@ -158,7 +158,7 @@ await LoginAsync( var attack = await attackerClient.PostAsync( $"/auth/me/sessions/chains/{victimChain.ChainId.Value}", - null); + null, TestContext.Current.CancellationToken); attack.StatusCode.Should().BeOneOf( HttpStatusCode.BadRequest, @@ -179,7 +179,7 @@ public async Task RevokeChainSelf_ShouldRevokeOwnedTargetChain() { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var currentDevice = $"session-current-{Guid.NewGuid():N}"; @@ -214,7 +214,7 @@ await LoginAsync( await GetChainsAsync(targetClient); var targetChains = await targetChainsResponse.Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(cancellationToken: TestContext.Current.CancellationToken); targetChains.Should().NotBeNull(); @@ -247,8 +247,8 @@ public async Task RevokeChainSelf_ShouldNotAllowRevokingAnotherUsersChain() { _factory.Clock.Reset(); - var attacker = await _factory.CreateLoginUserAsync(); - var victim = await _factory.CreateLoginUserAsync(); + var attacker = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var victim = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var attackerClient = CreateClient( $"session-revoke-attacker-{Guid.NewGuid():N}"); @@ -280,7 +280,7 @@ await LoginAsync( await GetChainsAsync(victimClient); var victimChains = await victimChainsResponse.Content - .ReadFromJsonAsync>(); + .ReadFromJsonAsync>(cancellationToken: TestContext.Current.CancellationToken); victimChains.Should().NotBeNull(); @@ -289,7 +289,7 @@ await LoginAsync( var attack = await attackerClient.PostAsync( $"/auth/me/sessions/chains/{victimChain.ChainId.Value}/revoke", - null); + null, TestContext.Current.CancellationToken); attack.StatusCode.Should().BeOneOf( HttpStatusCode.BadRequest, @@ -334,7 +334,7 @@ public async Task SessionSelfEndpoints_ShouldRejectUnauthenticatedRequests() var detail = await client.PostAsync( $"/auth/me/sessions/chains/{randomChain.Value}", - null); + null, TestContext.Current.CancellationToken); detail.StatusCode.Should() .Be(HttpStatusCode.Unauthorized); @@ -366,7 +366,7 @@ public async Task RevokeOthersSelf_ShouldRevokeAllOtherChainsButKeepCurrentChain { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var currentClient = CreateClient( $"revoke-others-current-{Guid.NewGuid():N}"); @@ -443,8 +443,8 @@ public async Task RevokeOthersSelf_ShouldNotAffectAnotherUsersChains() { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); - var unrelatedUser = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var unrelatedUser = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var currentClient = CreateClient( $"revoke-others-owner-current-{Guid.NewGuid():N}"); @@ -524,7 +524,7 @@ public async Task RevokeAllSelf_ShouldRevokeCurrentAndAllOtherChains() { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var client1 = CreateClient( $"revoke-all-1-{Guid.NewGuid():N}"); @@ -583,8 +583,8 @@ public async Task RevokeAllSelf_ShouldNotAffectAnotherUser() { _factory.Clock.Reset(); - var user = await _factory.CreateLoginUserAsync(); - var unrelatedUser = await _factory.CreateLoginUserAsync(); + var user = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var unrelatedUser = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var userClient = CreateClient( $"revoke-all-owner-{Guid.NewGuid():N}"); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/UAuthHubSampleSmokeTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/UAuthHubSampleSmokeTests.cs index f657bc1d..8ee068c6 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration/UAuthHubSampleSmokeTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/UAuthHubSampleSmokeTests.cs @@ -20,7 +20,7 @@ public UAuthHubSampleSmokeTests(AuthServerFactory factory) [InlineData("/login")] public async Task CriticalPages_ShouldRenderWithoutServerError(string path) { - var response = await _client.GetAsync(path); + var response = await _client.GetAsync(path, TestContext.Current.CancellationToken); ((int)response.StatusCode) .Should() diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/UserCreationAtomicityTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/UserCreationAtomicityTests.cs index 071e052c..10e528a9 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration/UserCreationAtomicityTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/UserCreationAtomicityTests.cs @@ -72,7 +72,7 @@ public async Task CreateUser_WhenPersistenceFails_ShouldRollbackAllUserState() var exception = await Assert.ThrowsAsync( () => service.CreateUserAsync( context, - request)); + request, TestContext.Current.CancellationToken)); exception.Message.Should() .Be("simulated_identifier_store_failure"); @@ -103,11 +103,11 @@ public async Task CreateUser_WhenPersistenceFails_ShouldRollbackAllUserState() var identifierStore = identifierFactory.Create(tenant); - var lifecycle = await lifecycleStore.GetAsync(new UserLifecycleKey(tenant, userKey)); + var lifecycle = await lifecycleStore.GetAsync(new UserLifecycleKey(tenant, userKey), TestContext.Current.CancellationToken); - var profiles = await profileStore.GetAllProfilesByUserAsync(userKey); + var profiles = await profileStore.GetAllProfilesByUserAsync(userKey, TestContext.Current.CancellationToken); - var identifiers = await identifierStore.GetByUserAsync(userKey); + var identifiers = await identifierStore.GetByUserAsync(userKey, TestContext.Current.CancellationToken); lifecycle.Should().BeNull("failed user creation must not leave a lifecycle aggregate"); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/UserIdentifierTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/UserIdentifierTests.cs index 7bd0af39..ab0d052a 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration/UserIdentifierTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/UserIdentifierTests.cs @@ -35,11 +35,11 @@ public async Task Availability_WhenUsernameDoesNotExist_ShouldReturnAvailable() { Type = UserIdentifierType.Username, Value = username - }); + }, cancellationToken: TestContext.Current.CancellationToken); response.StatusCode.Should().Be(HttpStatusCode.OK); - var result = await response.Content.ReadFromJsonAsync(); + var result = await response.Content.ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); result.Should().NotBeNull(); @@ -64,7 +64,7 @@ public async Task Availability_WhenUsernameAlreadyExists_ShouldReturnUnavailable UserName = username, Password = $"Test-{Guid.NewGuid():N}!", DisplayName = username - }); + }, cancellationToken: TestContext.Current.CancellationToken); create.StatusCode.Should().Be(HttpStatusCode.OK); @@ -74,11 +74,11 @@ public async Task Availability_WhenUsernameAlreadyExists_ShouldReturnUnavailable { Type = UserIdentifierType.Username, Value = username - }); + }, cancellationToken: TestContext.Current.CancellationToken); response.StatusCode.Should().Be(HttpStatusCode.OK); - var result = await response.Content.ReadFromJsonAsync(); + var result = await response.Content.ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); result.Should().NotBeNull(); @@ -105,7 +105,7 @@ public async Task Availability_ShouldUseNormalizedIdentifier() Email = email, Password = $"Test-{Guid.NewGuid():N}!", DisplayName = "Availability Test" - }); + }, cancellationToken: TestContext.Current.CancellationToken); create.StatusCode.Should().Be(HttpStatusCode.OK); @@ -116,11 +116,11 @@ public async Task Availability_ShouldUseNormalizedIdentifier() { Type = UserIdentifierType.Email, Value = $" {email.ToUpperInvariant()} " - }); + }, cancellationToken: TestContext.Current.CancellationToken); response.StatusCode.Should().Be(HttpStatusCode.OK); - var result = await response.Content.ReadFromJsonAsync(); + var result = await response.Content.ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); result.Should().NotBeNull(); @@ -144,14 +144,14 @@ public async Task Availability_WhenIdentifierIsInvalid_ShouldReturnValidationRes { Type = UserIdentifierType.Email, Value = "not-an-email" - }); + }, cancellationToken: TestContext.Current.CancellationToken); response.StatusCode.Should() .Be(HttpStatusCode.OK); var result = await response.Content - .ReadFromJsonAsync(); + .ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); result.Should().NotBeNull(); @@ -173,14 +173,14 @@ public async Task Availability_WhenIdentifierIsEmpty_ShouldReturnValidationResul { Type = UserIdentifierType.Username, Value = " " - }); + }, cancellationToken: TestContext.Current.CancellationToken); response.StatusCode.Should() .Be(HttpStatusCode.OK); var result = await response.Content - .ReadFromJsonAsync(); + .ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); result.Should().NotBeNull(); @@ -202,10 +202,10 @@ await response.Content [InlineData("delete", true)] public async Task IdentifierMutation_WhenIdBelongsToAnotherUser_ShouldReturnNotFoundAndPreserveRecords(string operation, bool admin) { - var actor = await _factory.CreateLoginUserAsync(); - var victim = await _factory.CreateLoginUserAsync(); + var actor = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var victim = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); if (admin) - await _factory.GrantPermissionsAsync(actor.UserKey, new[] { "*" }); + await _factory.GrantPermissionsAsync(actor.UserKey, new[] { "*" }, TestContext.Current.CancellationToken); using var scope = _factory.Services.CreateScope(); var services = scope.ServiceProvider; @@ -220,16 +220,16 @@ public async Task IdentifierMutation_WhenIdBelongsToAnotherUser_ShouldReturnNotF _factory.Clock.UtcNow, isPrimary: operation == "unset-primary", verifiedAt: operation == "verify" ? null : _factory.Clock.UtcNow); - await store.AddAsync(identifier); + await store.AddAsync(identifier, TestContext.Current.CancellationToken); - var before = (await store.GetByUserAsync(victim.UserKey)) + var before = (await store.GetByUserAsync(victim.UserKey, TestContext.Current.CancellationToken)) .Select(x => x.Snapshot()).ToArray(); using var client = CreateClient(); await AuthenticateIdentifierClientAsync(client, actor.Identifier, actor.Secret); // Prove authentication works before exercising the ownership boundary. using var authenticated = await client.PostAsJsonAsync( - "/auth/me/identifiers/get", new { }); + "/auth/me/identifiers/get", new { }, cancellationToken: TestContext.Current.CancellationToken); authenticated.StatusCode.Should().Be(HttpStatusCode.OK); var path = admin @@ -240,7 +240,7 @@ public async Task IdentifierMutation_WhenIdBelongsToAnotherUser_ShouldReturnNotF // Use a separate route target whose identifiers are NOT the victim's. if (admin) { - var target = await _factory.CreateLoginUserAsync(); + var target = await _factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); path = $"/auth/admin/users/{target.UserKey.Value}/identifiers/{operation}"; } @@ -252,7 +252,7 @@ public async Task IdentifierMutation_WhenIdBelongsToAnotherUser_ShouldReturnNotF mode = 0 }); - var after = await store.GetByUserAsync(victim.UserKey); + var after = await store.GetByUserAsync(victim.UserKey, TestContext.Current.CancellationToken); using var assertions = new AwesomeAssertions.Execution.AssertionScope(); response.StatusCode.Should().Be(HttpStatusCode.NotFound); after.Should().BeEquivalentTo(before, @@ -274,7 +274,7 @@ public async Task IdentifierMutation_WhenIdBelongsToSelf_ShouldSucceed(string op var factory = configuredFactory ?? _factory; - var owner = await factory.CreateLoginUserAsync(); + var owner = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var scope = factory.Services.CreateScope(); var services = scope.ServiceProvider; var store = services.GetRequiredService().Create(TenantKeys.Single); @@ -287,7 +287,7 @@ public async Task IdentifierMutation_WhenIdBelongsToSelf_ShouldSucceed(string op factory.Clock.UtcNow, isPrimary: operation == "unset-primary", verifiedAt: operation == "verify" ? null : factory.Clock.UtcNow); - await store.AddAsync(identifier); + await store.AddAsync(identifier, TestContext.Current.CancellationToken); using var client = factory.CreateClient( new WebApplicationFactoryClientOptions { @@ -315,8 +315,8 @@ public async Task IdentifierMutation_WhenIdBelongsToSelf_ShouldSucceed(string op : null }); response.IsSuccessStatusCode.Should().BeTrue( - $"own identifier operation should succeed: {await response.Content.ReadAsStringAsync()}"); - var saved = await store.GetByIdAsync(identifier.Id); + $"own identifier operation should succeed: {await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken)}"); + var saved = await store.GetByIdAsync(identifier.Id, TestContext.Current.CancellationToken); saved.Should().NotBeNull(); switch (operation) { diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/UserLifecycleTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/UserLifecycleTests.cs index 2723e5f7..f659f431 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration/UserLifecycleTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/UserLifecycleTests.cs @@ -42,13 +42,13 @@ public async Task CreateUser_ShouldCreateUsableUser() UserName = username, Password = secret, DisplayName = "Integration User" - }); + }, cancellationToken: TestContext.Current.CancellationToken); response.StatusCode.Should() .Be(HttpStatusCode.OK); var result = - await response.Content.ReadFromJsonAsync(); + await response.Content.ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); result.Should().NotBeNull(); result!.IsSuccess.Should().BeTrue(); @@ -100,7 +100,7 @@ public async Task CreateUser_ShouldCreateLifecycle() var lifecycle = await store.GetAsync( new UserLifecycleKey( TenantKeys.Single, - userKey)); + userKey), TestContext.Current.CancellationToken); lifecycle.Should().NotBeNull(); @@ -151,7 +151,7 @@ public async Task CreateUser_ShouldCreateDefaultProfile() new UserProfileKey( TenantKeys.Single, userKey, - ProfileKey.Default)); + ProfileKey.Default), TestContext.Current.CancellationToken); profile.Should().NotBeNull(); @@ -196,7 +196,7 @@ public async Task CreateUser_WithUsername_ShouldCreatePrimaryUsername() var identifiers = await store.GetByUserAsync( - userKey); + userKey, TestContext.Current.CancellationToken); var identifier = identifiers .Single(x => @@ -233,7 +233,7 @@ public async Task CreateUser_WithVerifiedEmail_ShouldNotPersistEmailAsVerified() using var scope = _factory.Services.CreateScope(); var factory = scope.ServiceProvider.GetRequiredService(); var store = factory.Create(TenantKeys.Single); - var identifiers = await store.GetByUserAsync(userKey); + var identifiers = await store.GetByUserAsync(userKey, TestContext.Current.CancellationToken); var identifier = identifiers.Single(x => x.Type == UserIdentifierType.Email); identifier.Value.Should().Be(email); @@ -275,7 +275,7 @@ public async Task CreateUser_WithUnverifiedEmail_ShouldPersistEmailAsUnverified( var identifiers = await store.GetByUserAsync( - userKey); + userKey, TestContext.Current.CancellationToken); var identifier = identifiers .Single(x => @@ -320,7 +320,7 @@ public async Task CreateUser_WithUsernameAndEmail_ShouldCreateBothIdentifiers() var identifiers = await store.GetByUserAsync( - userKey); + userKey, TestContext.Current.CancellationToken); identifiers.Should() .ContainSingle(x => @@ -394,7 +394,7 @@ public async Task CreateUser_WithExistingUsername_ShouldNotCreateSecondUser() var firstResult = await firstResponse.Content - .ReadFromJsonAsync(); + .ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); firstResult.Should().NotBeNull(); firstResult!.IsSuccess.Should().BeTrue(); @@ -415,7 +415,7 @@ await CreateUserResponseAsync( ((int)secondResponse.StatusCode).Should().BeInRange(400, 499); - var problem = await secondResponse.Content.ReadFromJsonAsync(); + var problem = await secondResponse.Content.ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); problem.Should().NotBeNull(); @@ -446,7 +446,7 @@ await CreateUserResponseAsync( var identifier = await store.GetAsync( UserIdentifierType.Username, - normalized.Normalized); + normalized.Normalized, TestContext.Current.CancellationToken); identifier.Should().NotBeNull(); @@ -495,7 +495,7 @@ await lifecycleFactory .GetAsync( new UserLifecycleKey( TenantKeys.Single, - userKey)); + userKey), TestContext.Current.CancellationToken); var profile = await profileFactory @@ -504,13 +504,13 @@ await profileFactory new UserProfileKey( TenantKeys.Single, userKey, - ProfileKey.Default)); + ProfileKey.Default), TestContext.Current.CancellationToken); var identifiers = await identifierFactory .Create(TenantKeys.Single) .GetByUserAsync( - userKey); + userKey, TestContext.Current.CancellationToken); lifecycle.Should().NotBeNull(); profile.Should().NotBeNull(); @@ -568,7 +568,7 @@ public async Task QueryUsersAsync_AppliesConfiguredPaginationAndSorting() foreach (var name in names) { - var user = await factory.CreateLoginUserAsync(); + var user = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); using var seedScope = factory.Services.CreateScope(); @@ -583,7 +583,7 @@ await profileFactory user.UserKey, ProfileKey.Default, factory.Clock.UtcNow, - displayName: $"{prefix}-{name}")); + displayName: $"{prefix}-{name}"), TestContext.Current.CancellationToken); } using var scope = factory.Services.CreateScope(); @@ -616,7 +616,7 @@ await profileFactory PageSize = 0, Search = prefix, SortBy = nameof(UserSummary.DisplayName) - }); + }, TestContext.Current.CancellationToken); firstPage.TotalCount.Should().Be(5); firstPage.PageNumber.Should().Be(1); @@ -637,7 +637,7 @@ await profileFactory PageSize = 100, Search = prefix, SortBy = nameof(UserSummary.DisplayName) - }); + }, TestContext.Current.CancellationToken); secondPage.TotalCount.Should().Be(5); secondPage.PageNumber.Should().Be(2); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/UserProfileTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/UserProfileTests.cs index 791c76f3..5fa6cb94 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration/UserProfileTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/UserProfileTests.cs @@ -7,13 +7,15 @@ namespace CodeBeam.UltimateAuth.Tests.Integration; -public class UserProfileTests : IClassFixture +public class UserProfileTests : IDisposable { + // Profiles created by one test must not exist in another test's host. + private readonly AuthServerFactory _factory = new(); private readonly HttpClient _client; - public UserProfileTests(AuthServerFactory factory) + public UserProfileTests() { - _client = factory.CreateClient(new WebApplicationFactoryClientOptions + _client = _factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false, HandleCookies = false @@ -23,6 +25,12 @@ public UserProfileTests(AuthServerFactory factory) _client.DefaultRequestHeaders.Add("X-UDID", $"profile-test-{Guid.NewGuid():N}"); } + public void Dispose() + { + _client.Dispose(); + _factory.Dispose(); + } + [Fact] public async Task Profile_Switch_Should_Return_Correct_Profile_Data() { @@ -31,7 +39,7 @@ public async Task Profile_Switch_Should_Return_Correct_Profile_Data() var defaultResponse = await PostProfileAsync("/auth/me/profile/get", new { }); defaultResponse.StatusCode.Should().Be(HttpStatusCode.OK); - var defaultProfile = await defaultResponse.Content.ReadFromJsonAsync(); + var defaultProfile = await defaultResponse.Content.ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); defaultProfile.Should().NotBeNull(); var createResponse = await PostProfileAsync("/auth/me/profile/create", new CreateProfileRequest @@ -56,14 +64,14 @@ public async Task Profile_Switch_Should_Return_Correct_Profile_Data() businessResponse.StatusCode.Should().Be(HttpStatusCode.OK); - var businessProfile = await businessResponse.Content.ReadFromJsonAsync(); + var businessProfile = await businessResponse.Content.ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); businessProfile.Should().NotBeNull(); businessProfile!.DisplayName.Should().Be("Updated Business Name"); var defaultAgainResponse = await PostProfileAsync("/auth/me/profile/get", new { }); - var defaultAgain = await defaultAgainResponse.Content.ReadFromJsonAsync(); + var defaultAgain = await defaultAgainResponse.Content.ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); defaultAgain!.DisplayName.Should().Be(defaultProfile!.DisplayName); } @@ -73,9 +81,9 @@ public async Task GetMe_Without_ProfileKey_Should_Return_Default_Profile() { await Login(); - var response = await _client.PostAsJsonAsync("/auth/me/profile/get", new { }); + var response = await _client.PostAsJsonAsync("/auth/me/profile/get", new { }, cancellationToken: TestContext.Current.CancellationToken); - var profile = await response.Content.ReadFromJsonAsync(); + var profile = await response.Content.ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); profile.Should().NotBeNull(); profile!.ProfileKey.Value.Should().Be("default"); diff --git a/tests/CodeBeam.UltimateAuth.Tests.RateLimiting.Compatibility/CodeBeam.UltimateAuth.Tests.RateLimiting.Compatibility.csproj b/tests/CodeBeam.UltimateAuth.Tests.RateLimiting.Compatibility/CodeBeam.UltimateAuth.Tests.RateLimiting.Compatibility.csproj index 7fe0d607..104a4c4e 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.RateLimiting.Compatibility/CodeBeam.UltimateAuth.Tests.RateLimiting.Compatibility.csproj +++ b/tests/CodeBeam.UltimateAuth.Tests.RateLimiting.Compatibility/CodeBeam.UltimateAuth.Tests.RateLimiting.Compatibility.csproj @@ -1,17 +1,10 @@ - net8.0;net9.0;net10.0 false - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/AssemblyBehavior.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/AssemblyBehavior.cs index c7fc3b1f..0378b5a6 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/AssemblyBehavior.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/AssemblyBehavior.cs @@ -1 +1,3 @@ -[assembly: CollectionBehavior(DisableTestParallelization = true)] +using Xunit.v3; + +[assembly: Parallelization(Mode = Xunit.Sdk.ParallelMode.None)] diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Authentication/AuthenticationSecurityStateStoreContractTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Authentication/AuthenticationSecurityStateStoreContractTests.cs index b3523c18..25117b45 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Authentication/AuthenticationSecurityStateStoreContractTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Authentication/AuthenticationSecurityStateStoreContractTests.cs @@ -31,12 +31,12 @@ public async Task AddAsync_WhenValid_PersistsState() var state = AuthenticationSecurityState.CreateAccount(Tenant, user); - await store.AddAsync(state); + await store.AddAsync(state, ct: TestContext.Current.CancellationToken); var result = await store.GetAsync( user, AuthenticationSecurityScope.Account, - null); + null, ct: TestContext.Current.CancellationToken); result.Should().NotBeNull(); result!.Id.Should().Be(state.Id); @@ -64,18 +64,18 @@ public async Task AddAsync_SameUserWithAccountAndFactorStates_IsAllowed() user, CredentialType.Password); - await store.AddAsync(account); - await store.AddAsync(factor); + await store.AddAsync(account, ct: TestContext.Current.CancellationToken); + await store.AddAsync(factor, ct: TestContext.Current.CancellationToken); var accountResult = await store.GetAsync( user, AuthenticationSecurityScope.Account, - null); + null, ct: TestContext.Current.CancellationToken); var factorResult = await store.GetAsync( user, AuthenticationSecurityScope.Factor, - CredentialType.Password); + CredentialType.Password, ct: TestContext.Current.CancellationToken); accountResult.Should().NotBeNull(); factorResult.Should().NotBeNull(); @@ -100,7 +100,7 @@ public async Task AddAsync_WhenSameLogicalStateAlreadyExists_ThrowsConflict() Tenant, user); - await store.AddAsync(first); + await store.AddAsync(first, ct: TestContext.Current.CancellationToken); var act = () => store.AddAsync(second); @@ -137,7 +137,7 @@ public async Task GetAsync_WhenStateDoesNotExist_ReturnsNull() var result = await store.GetAsync( UserKey.New(), AuthenticationSecurityScope.Account, - null); + null, ct: TestContext.Current.CancellationToken); result.Should().BeNull(); } @@ -158,18 +158,18 @@ public async Task AddAsync_SameUserWithDifferentScope_IsAllowed() var second = AuthenticationSecurityState.CreateFactor(Tenant, user, CredentialType.Password); - await store.AddAsync(first); - await store.AddAsync(second); + await store.AddAsync(first, ct: TestContext.Current.CancellationToken); + await store.AddAsync(second, ct: TestContext.Current.CancellationToken); var firstResult = await store.GetAsync( user, first.Scope, - first.CredentialType); + first.CredentialType, ct: TestContext.Current.CancellationToken); var secondResult = await store.GetAsync( user, second.Scope, - second.CredentialType); + second.CredentialType, ct: TestContext.Current.CancellationToken); firstResult.Should().NotBeNull(); secondResult.Should().NotBeNull(); @@ -190,18 +190,18 @@ public async Task AddAsync_SameUserAndScopeWithDifferentCredentialType_IsAllowed var second = AuthenticationSecurityState.CreateFactor(Tenant, user, CredentialType.Passkey); - await store.AddAsync(first); - await store.AddAsync(second); + await store.AddAsync(first, ct: TestContext.Current.CancellationToken); + await store.AddAsync(second, ct: TestContext.Current.CancellationToken); var password = await store.GetAsync( user, AuthenticationSecurityScope.Factor, - CredentialType.Password); + CredentialType.Password, ct: TestContext.Current.CancellationToken); var passkey = await store.GetAsync( user, AuthenticationSecurityScope.Factor, - CredentialType.Passkey); + CredentialType.Passkey, ct: TestContext.Current.CancellationToken); password.Should().NotBeNull(); passkey.Should().NotBeNull(); @@ -226,7 +226,7 @@ public async Task UpdateAsync_WhenExpectedVersionMatches_PersistsChanges() Tenant, user); - await store.AddAsync(original); + await store.AddAsync(original, ct: TestContext.Current.CancellationToken); var updated = original.RegisterFailure( Now, @@ -236,12 +236,12 @@ public async Task UpdateAsync_WhenExpectedVersionMatches_PersistsChanges() await store.UpdateAsync( updated, - expectedVersion: original.SecurityVersion); + expectedVersion: original.SecurityVersion, ct: TestContext.Current.CancellationToken); var result = await store.GetAsync( user, AuthenticationSecurityScope.Account, - null); + null, ct: TestContext.Current.CancellationToken); result.Should().NotBeNull(); @@ -262,7 +262,7 @@ public async Task UpdateAsync_WhenExpectedVersionIsStale_ThrowsConflict() Tenant, UserKey.New()); - await store.AddAsync(original); + await store.AddAsync(original, ct: TestContext.Current.CancellationToken); var updated = original.RegisterFailure( Now, @@ -291,7 +291,7 @@ public async Task UpdateAsync_WhenResetBegins_PersistsResetState() user, CredentialType.Password); - await store.AddAsync(original); + await store.AddAsync(original, ct: TestContext.Current.CancellationToken); var updated = original.BeginReset( "hashed-reset-token", @@ -300,12 +300,12 @@ public async Task UpdateAsync_WhenResetBegins_PersistsResetState() await store.UpdateAsync( updated, - original.SecurityVersion); + original.SecurityVersion, ct: TestContext.Current.CancellationToken); var result = await store.GetAsync( user, AuthenticationSecurityScope.Factor, - CredentialType.Password); + CredentialType.Password, ct: TestContext.Current.CancellationToken); result.Should().NotBeNull(); @@ -351,17 +351,17 @@ public async Task DeleteAsync_WhenStateExists_RemovesState() var state = AuthenticationSecurityState.CreateAccount(Tenant, user); - await store.AddAsync(state); + await store.AddAsync(state, ct: TestContext.Current.CancellationToken); await store.DeleteAsync( user, state.Scope, - state.CredentialType); + state.CredentialType, ct: TestContext.Current.CancellationToken); var result = await store.GetAsync( user, state.Scope, - state.CredentialType); + state.CredentialType, ct: TestContext.Current.CancellationToken); result.Should().BeNull(); } @@ -383,23 +383,23 @@ public async Task DeleteAsync_AccountState_DoesNotDeleteFactorState() user, CredentialType.Password); - await store.AddAsync(account); - await store.AddAsync(factor); + await store.AddAsync(account, ct: TestContext.Current.CancellationToken); + await store.AddAsync(factor, ct: TestContext.Current.CancellationToken); await store.DeleteAsync( user, AuthenticationSecurityScope.Account, - null); + null, ct: TestContext.Current.CancellationToken); var accountResult = await store.GetAsync( user, AuthenticationSecurityScope.Account, - null); + null, ct: TestContext.Current.CancellationToken); var factorResult = await store.GetAsync( user, AuthenticationSecurityScope.Factor, - CredentialType.Password); + CredentialType.Password, ct: TestContext.Current.CancellationToken); accountResult.Should().BeNull(); factorResult.Should().NotBeNull(); @@ -430,23 +430,23 @@ public async Task DeleteAsync_RemovesOnlyExactLogicalState() var password = AuthenticationSecurityState.CreateFactor(Tenant, user, CredentialType.Password); var passkey = AuthenticationSecurityState.CreateFactor(Tenant, user, CredentialType.Passkey); - await store.AddAsync(password); - await store.AddAsync(passkey); + await store.AddAsync(password, ct: TestContext.Current.CancellationToken); + await store.AddAsync(passkey, ct: TestContext.Current.CancellationToken); await store.DeleteAsync( user, AuthenticationSecurityScope.Factor, - CredentialType.Password); + CredentialType.Password, ct: TestContext.Current.CancellationToken); var passwordResult = await store.GetAsync( user, AuthenticationSecurityScope.Factor, - CredentialType.Password); + CredentialType.Password, ct: TestContext.Current.CancellationToken); var passkeyResult = await store.GetAsync( user, AuthenticationSecurityScope.Factor, - CredentialType.Passkey); + CredentialType.Passkey, ct: TestContext.Current.CancellationToken); passwordResult.Should().BeNull(); passkeyResult.Should().NotBeNull(); @@ -473,17 +473,17 @@ public async Task GetAsync_IsTenantIsolated() tenantA, user); - await storeA.AddAsync(state); + await storeA.AddAsync(state, ct: TestContext.Current.CancellationToken); var fromA = await storeA.GetAsync( user, AuthenticationSecurityScope.Account, - null); + null, ct: TestContext.Current.CancellationToken); var fromB = await storeB.GetAsync( user, AuthenticationSecurityScope.Account, - null); + null, ct: TestContext.Current.CancellationToken); fromA.Should().NotBeNull(); fromA!.Tenant.Should().Be(tenantA); @@ -512,23 +512,23 @@ public async Task DeleteAsync_IsTenantIsolated() tenantB, user); - await storeA.AddAsync(stateA); - await storeB.AddAsync(stateB); + await storeA.AddAsync(stateA, ct: TestContext.Current.CancellationToken); + await storeB.AddAsync(stateB, ct: TestContext.Current.CancellationToken); await storeA.DeleteAsync( user, AuthenticationSecurityScope.Account, - null); + null, ct: TestContext.Current.CancellationToken); var fromA = await storeA.GetAsync( user, AuthenticationSecurityScope.Account, - null); + null, ct: TestContext.Current.CancellationToken); var fromB = await storeB.GetAsync( user, AuthenticationSecurityScope.Account, - null); + null, ct: TestContext.Current.CancellationToken); fromA.Should().BeNull(); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Authorization/AuthorizationServiceTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Authorization/AuthorizationServiceTests.cs index 727c3797..4caa7741 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Authorization/AuthorizationServiceTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Authorization/AuthorizationServiceTests.cs @@ -25,7 +25,7 @@ public async Task AuthorizeAsync_WhenAccessOrchestratorAllows_ReturnsAllowed() .Returns, CancellationToken>( async (_, command, ct) => await command.ExecuteAsync(ct)); - var result = await f.Sut.AuthorizeAsync(context); + var result = await f.Sut.AuthorizeAsync(context, ct: TestContext.Current.CancellationToken); result.IsAllowed.Should().BeTrue(); result.RequiresReauthentication.Should().BeFalse(); @@ -54,7 +54,7 @@ public async Task AuthorizeAsync_WhenAccessOrchestratorDenies_ReturnsDenied() It.IsAny())) .ThrowsAsync(exception); - var result = await f.Sut.AuthorizeAsync(context); + var result = await f.Sut.AuthorizeAsync(context, ct: TestContext.Current.CancellationToken); result.IsAllowed.Should().BeFalse(); result.RequiresReauthentication.Should().BeFalse(); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Authorization/RoleServiceTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Authorization/RoleServiceTests.cs index f8c0caae..d0b698d8 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Authorization/RoleServiceTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Authorization/RoleServiceTests.cs @@ -45,7 +45,7 @@ public async Task CreateAsync_WhenValid_CreatesAndPersistsRole() var result = await f.Sut.CreateAsync( context, " Administrators ", - permissions); + permissions, ct: TestContext.Current.CancellationToken); result.Should().BeSameAs(captured); @@ -144,7 +144,7 @@ public async Task RenameAsync_WhenValid_RenamesAndSavesWithOriginalVersion() await f.Sut.RenameAsync( context, role.Id, - " New Name "); + " New Name ", ct: TestContext.Current.CancellationToken); role.Name.Should().Be("New Name"); role.NormalizedName.Should().Be("NEW NAME"); @@ -224,7 +224,7 @@ public async Task DeleteAsync_WhenValid_RemovesAssignmentsAndUsesRequestedMode( var result = await f.Sut.DeleteAsync( context, role.Id, - mode); + mode, ct: TestContext.Current.CancellationToken); result.RoleId.Should().Be(role.Id); result.RemovedAssignments.Should().Be(13); @@ -322,7 +322,7 @@ public async Task SetPermissionsAsync_WhenValid_UpdatesAndSavesWithOriginalVersi await f.Sut.SetPermissionsAsync( context, role.Id, - permissions); + permissions, ct: TestContext.Current.CancellationToken); role.Permissions.Should().ContainSingle().Which.Should().Be(Permission.From("users.read")); @@ -376,7 +376,7 @@ public async Task QueryAsync_ForwardsQueryAndReturnsStoreResult() It.IsAny())) .ReturnsAsync(expected); - var result = await f.Sut.QueryAsync(context, query); + var result = await f.Sut.QueryAsync(context, query, ct: TestContext.Current.CancellationToken); result.Should().BeSameAs(expected); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Authorization/Store/RoleStoreContractTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Authorization/Store/RoleStoreContractTests.cs index b76b6146..484ee8ed 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Authorization/Store/RoleStoreContractTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Authorization/Store/RoleStoreContractTests.cs @@ -27,10 +27,10 @@ public async Task AddAsync_WhenValid_PersistsRole() var role = CreateRole("Administrators"); - await store.AddAsync(role); + await store.AddAsync(role, ct: TestContext.Current.CancellationToken); var result = await store.GetAsync( - new RoleKey(Tenant, role.Id)); + new RoleKey(Tenant, role.Id), ct: TestContext.Current.CancellationToken); result.Should().NotBeNull(); result!.Id.Should().Be(role.Id); @@ -49,7 +49,7 @@ public async Task AddAsync_WhenActiveNormalizedNameAlreadyExists_ThrowsConflict( var first = CreateRole("Administrators"); var duplicate = CreateRole(" administrators "); - await store.AddAsync(first); + await store.AddAsync(first, ct: TestContext.Current.CancellationToken); var act = () => store.AddAsync(duplicate); @@ -65,13 +65,13 @@ public async Task AddAsync_WhenDeletedRoleHasSameNormalizedName_ThrowsConflict() var first = CreateRole("Administrators"); - await store.AddAsync(first); + await store.AddAsync(first, ct: TestContext.Current.CancellationToken); await store.DeleteAsync( new RoleKey(Tenant, first.Id), first.Version, DeleteMode.Soft, - Now); + Now, ct: TestContext.Current.CancellationToken); var replacement = CreateRole("Administrators"); @@ -89,13 +89,13 @@ public async Task AddAsync_WhenPreviousRoleWasHardDeleted_AllowsNameReuse() var first = CreateRole("Administrators"); - await store.AddAsync(first); + await store.AddAsync(first, ct: TestContext.Current.CancellationToken); await store.DeleteAsync( new RoleKey(Tenant, first.Id), first.Version, DeleteMode.Hard, - Now); + Now, ct: TestContext.Current.CancellationToken); var replacement = CreateRole("Administrators"); @@ -104,7 +104,7 @@ await store.DeleteAsync( await act.Should().NotThrowAsync(); var result = await store.GetByNameAsync( - replacement.NormalizedName); + replacement.NormalizedName, ct: TestContext.Current.CancellationToken); result.Should().NotBeNull(); result!.Id.Should().Be(replacement.Id); @@ -122,10 +122,10 @@ public async Task GetByNameAsync_WhenRoleExists_ReturnsRole() var role = CreateRole("Administrators"); - await store.AddAsync(role); + await store.AddAsync(role, ct: TestContext.Current.CancellationToken); var result = await store.GetByNameAsync( - role.NormalizedName); + role.NormalizedName, ct: TestContext.Current.CancellationToken); result.Should().NotBeNull(); result!.Id.Should().Be(role.Id); @@ -140,16 +140,16 @@ public async Task GetByNameAsync_WhenRoleIsDeleted_ReturnsNull() var role = CreateRole("Administrators"); - await store.AddAsync(role); + await store.AddAsync(role, ct: TestContext.Current.CancellationToken); await store.DeleteAsync( new RoleKey(Tenant, role.Id), role.Version, DeleteMode.Soft, - Now); + Now, ct: TestContext.Current.CancellationToken); var result = await store.GetByNameAsync( - role.NormalizedName); + role.NormalizedName, ct: TestContext.Current.CancellationToken); result.Should().BeNull(); } @@ -168,12 +168,12 @@ public async Task GetByIdsAsync_ReturnsOnlyRequestedRoles() var operatorRole = CreateRole("Operators"); var unrelated = CreateRole("Auditors"); - await store.AddAsync(admin); - await store.AddAsync(operatorRole); - await store.AddAsync(unrelated); + await store.AddAsync(admin, ct: TestContext.Current.CancellationToken); + await store.AddAsync(operatorRole, ct: TestContext.Current.CancellationToken); + await store.AddAsync(unrelated, ct: TestContext.Current.CancellationToken); var result = await store.GetByIdsAsync( - [admin.Id, operatorRole.Id]); + [admin.Id, operatorRole.Id], ct: TestContext.Current.CancellationToken); result.Select(x => x.Id) .Should() @@ -192,17 +192,17 @@ public async Task GetByIdsAsync_DoesNotReturnDeletedRoles() var active = CreateRole("Active"); var deleted = CreateRole("Deleted"); - await store.AddAsync(active); - await store.AddAsync(deleted); + await store.AddAsync(active, ct: TestContext.Current.CancellationToken); + await store.AddAsync(deleted, ct: TestContext.Current.CancellationToken); await store.DeleteAsync( new RoleKey(Tenant, deleted.Id), deleted.Version, DeleteMode.Soft, - Now); + Now, ct: TestContext.Current.CancellationToken); var result = await store.GetByIdsAsync( - [active.Id, deleted.Id]); + [active.Id, deleted.Id], ct: TestContext.Current.CancellationToken); result.Should().ContainSingle(); result.Single().Id.Should().Be(active.Id); @@ -220,10 +220,10 @@ public async Task SaveAsync_WhenRoleIsChanged_PersistsChanges() var role = CreateRole("Administrators"); - await store.AddAsync(role); + await store.AddAsync(role, ct: TestContext.Current.CancellationToken); var persisted = await store.GetAsync( - new RoleKey(Tenant, role.Id)); + new RoleKey(Tenant, role.Id), ct: TestContext.Current.CancellationToken); persisted.Should().NotBeNull(); @@ -233,10 +233,10 @@ public async Task SaveAsync_WhenRoleIsChanged_PersistsChanges() await store.SaveAsync( persisted, - expectedVersion); + expectedVersion, ct: TestContext.Current.CancellationToken); var result = await store.GetAsync( - new RoleKey(Tenant, role.Id)); + new RoleKey(Tenant, role.Id), ct: TestContext.Current.CancellationToken); result.Should().NotBeNull(); result!.Name.Should().Be("Operators"); @@ -251,10 +251,10 @@ public async Task SaveAsync_WhenExpectedVersionIsStale_ThrowsConcurrency() var role = CreateRole("Administrators"); - await store.AddAsync(role); + await store.AddAsync(role, ct: TestContext.Current.CancellationToken); var persisted = await store.GetAsync( - new RoleKey(Tenant, role.Id)); + new RoleKey(Tenant, role.Id), ct: TestContext.Current.CancellationToken); persisted.Should().NotBeNull(); @@ -281,11 +281,11 @@ public async Task SaveAsync_WhenRenamedToExistingActiveRole_ThrowsConflict() var admin = CreateRole("Administrators"); var operators = CreateRole("Operators"); - await store.AddAsync(admin); - await store.AddAsync(operators); + await store.AddAsync(admin, ct: TestContext.Current.CancellationToken); + await store.AddAsync(operators, ct: TestContext.Current.CancellationToken); var persisted = await store.GetAsync( - new RoleKey(Tenant, operators.Id)); + new RoleKey(Tenant, operators.Id), ct: TestContext.Current.CancellationToken); persisted.Should().NotBeNull(); @@ -315,7 +315,7 @@ public async Task DeleteAsync_WhenSoftDeleted_KeepsRoleButMarksDeleted() var role = CreateRole("Administrators"); - await store.AddAsync(role); + await store.AddAsync(role, ct: TestContext.Current.CancellationToken); var deletedAt = Now.AddMinutes(1); @@ -323,10 +323,10 @@ await store.DeleteAsync( new RoleKey(Tenant, role.Id), role.Version, DeleteMode.Soft, - deletedAt); + deletedAt, ct: TestContext.Current.CancellationToken); var result = await store.GetAsync( - new RoleKey(Tenant, role.Id)); + new RoleKey(Tenant, role.Id), ct: TestContext.Current.CancellationToken); result.Should().NotBeNull(); result!.IsDeleted.Should().BeTrue(); @@ -341,21 +341,21 @@ public async Task DeleteAsync_WhenHardDeleted_RemovesRole() var role = CreateRole("Administrators"); - await store.AddAsync(role); + await store.AddAsync(role, ct: TestContext.Current.CancellationToken); await store.DeleteAsync( new RoleKey(Tenant, role.Id), role.Version, DeleteMode.Hard, - Now); + Now, ct: TestContext.Current.CancellationToken); var result = await store.GetAsync( - new RoleKey(Tenant, role.Id)); + new RoleKey(Tenant, role.Id), ct: TestContext.Current.CancellationToken); result.Should().BeNull(); var exists = await store.ExistsAsync( - new RoleKey(Tenant, role.Id)); + new RoleKey(Tenant, role.Id), ct: TestContext.Current.CancellationToken); exists.Should().BeFalse(); } @@ -368,7 +368,7 @@ public async Task DeleteAsync_WhenExpectedVersionIsStale_ThrowsConcurrency() var role = CreateRole("Administrators"); - await store.AddAsync(role); + await store.AddAsync(role, ct: TestContext.Current.CancellationToken); var act = () => store.DeleteAsync( new RoleKey(Tenant, role.Id), @@ -393,20 +393,20 @@ public async Task QueryAsync_WhenIncludeDeletedIsFalse_ExcludesDeletedRoles() var active = CreateRole("Active"); var deleted = CreateRole("Deleted"); - await store.AddAsync(active); - await store.AddAsync(deleted); + await store.AddAsync(active, ct: TestContext.Current.CancellationToken); + await store.AddAsync(deleted, ct: TestContext.Current.CancellationToken); await store.DeleteAsync( new RoleKey(Tenant, deleted.Id), deleted.Version, DeleteMode.Soft, - Now); + Now, ct: TestContext.Current.CancellationToken); var result = await store.QueryAsync( new RoleQuery { IncludeDeleted = false - }); + }, ct: TestContext.Current.CancellationToken); result.Items.Should() .Contain(x => x.Id == active.Id); @@ -424,20 +424,20 @@ public async Task QueryAsync_WhenIncludeDeletedIsTrue_IncludesDeletedRoles() var active = CreateRole("Active"); var deleted = CreateRole("Deleted"); - await store.AddAsync(active); - await store.AddAsync(deleted); + await store.AddAsync(active, ct: TestContext.Current.CancellationToken); + await store.AddAsync(deleted, ct: TestContext.Current.CancellationToken); await store.DeleteAsync( new RoleKey(Tenant, deleted.Id), deleted.Version, DeleteMode.Soft, - Now); + Now, ct: TestContext.Current.CancellationToken); var result = await store.QueryAsync( new RoleQuery { IncludeDeleted = true - }); + }, ct: TestContext.Current.CancellationToken); result.Items.Select(x => x.Id) .Should() @@ -453,14 +453,14 @@ public async Task QueryAsync_WhenSearchSpecified_SearchesNormalizedName() var admin = CreateRole("Global Administrators"); var operators = CreateRole("Operators"); - await store.AddAsync(admin); - await store.AddAsync(operators); + await store.AddAsync(admin, ct: TestContext.Current.CancellationToken); + await store.AddAsync(operators, ct: TestContext.Current.CancellationToken); var result = await store.QueryAsync( new RoleQuery { Search = " admin " - }); + }, ct: TestContext.Current.CancellationToken); result.Items.Should().ContainSingle(); result.Items.Single().Id.Should().Be(admin.Id); @@ -481,7 +481,7 @@ public async Task QueryAsync_SortsBeforeApplyingPagination() "Delta" }) { - await store.AddAsync(CreateRole(name)); + await store.AddAsync(CreateRole(name), ct: TestContext.Current.CancellationToken); } var result = await store.QueryAsync( @@ -491,7 +491,7 @@ public async Task QueryAsync_SortsBeforeApplyingPagination() PageSize = 2, SortBy = nameof(Role.Name), Descending = false - }); + }, ct: TestContext.Current.CancellationToken); result.TotalCount.Should().Be(5); result.PageNumber.Should().Be(2); @@ -522,13 +522,13 @@ public async Task Store_IsTenantIsolated() "Administrators", tenantA); - await storeA.AddAsync(roleA); + await storeA.AddAsync(roleA, ct: TestContext.Current.CancellationToken); var fromA = await storeA.GetAsync( - new RoleKey(tenantA, roleA.Id)); + new RoleKey(tenantA, roleA.Id), ct: TestContext.Current.CancellationToken); var fromB = await storeB.GetAsync( - new RoleKey(tenantB, roleA.Id)); + new RoleKey(tenantB, roleA.Id), ct: TestContext.Current.CancellationToken); fromA.Should().NotBeNull(); fromB.Should().BeNull(); @@ -537,7 +537,7 @@ public async Task Store_IsTenantIsolated() new RoleQuery { IncludeDeleted = true - }); + }, ct: TestContext.Current.CancellationToken); queryB.Items.Should() .NotContain(x => x.Id == roleA.Id); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Authorization/Store/UserRoleStoreContractTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Authorization/Store/UserRoleStoreContractTests.cs index 1723e3bf..700b5327 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Authorization/Store/UserRoleStoreContractTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Authorization/Store/UserRoleStoreContractTests.cs @@ -30,9 +30,9 @@ public async Task AssignAsync_WhenValid_PersistsAssignment() var roleId = RoleId.New(); var assignedAt = Now.AddMinutes(-10); - await store.AssignAsync(user, roleId, assignedAt); + await store.AssignAsync(user, roleId, assignedAt, ct: TestContext.Current.CancellationToken); - var result = await store.GetAssignmentsAsync(user); + var result = await store.GetAssignmentsAsync(user, ct: TestContext.Current.CancellationToken); result.Should().ContainSingle(); @@ -53,7 +53,7 @@ public async Task AssignAsync_WhenSameRoleAlreadyAssigned_ThrowsConflict() var user = UserKey.New(); var roleId = RoleId.New(); - await store.AssignAsync(user, roleId, Now); + await store.AssignAsync(user, roleId, Now, ct: TestContext.Current.CancellationToken); var act = () => store.AssignAsync( user, @@ -75,10 +75,10 @@ public async Task AssignAsync_SameUserCanHaveMultipleRoles() var roleA = RoleId.New(); var roleB = RoleId.New(); - await store.AssignAsync(user, roleA, Now); - await store.AssignAsync(user, roleB, Now.AddMinutes(1)); + await store.AssignAsync(user, roleA, Now, ct: TestContext.Current.CancellationToken); + await store.AssignAsync(user, roleB, Now.AddMinutes(1), ct: TestContext.Current.CancellationToken); - var result = await store.GetAssignmentsAsync(user); + var result = await store.GetAssignmentsAsync(user, ct: TestContext.Current.CancellationToken); result.Should().HaveCount(2); @@ -97,11 +97,11 @@ public async Task AssignAsync_SameRoleCanBeAssignedToMultipleUsers() var userB = UserKey.New(); var roleId = RoleId.New(); - await store.AssignAsync(userA, roleId, Now); - await store.AssignAsync(userB, roleId, Now.AddMinutes(1)); + await store.AssignAsync(userA, roleId, Now, ct: TestContext.Current.CancellationToken); + await store.AssignAsync(userB, roleId, Now.AddMinutes(1), ct: TestContext.Current.CancellationToken); - var assignmentsA = await store.GetAssignmentsAsync(userA); - var assignmentsB = await store.GetAssignmentsAsync(userB); + var assignmentsA = await store.GetAssignmentsAsync(userA, ct: TestContext.Current.CancellationToken); + var assignmentsB = await store.GetAssignmentsAsync(userB, ct: TestContext.Current.CancellationToken); assignmentsA.Should().ContainSingle(); assignmentsB.Should().ContainSingle(); @@ -126,10 +126,10 @@ public async Task GetAssignmentsAsync_ReturnsOnlyRequestedUserAssignments() var roleA = RoleId.New(); var roleB = RoleId.New(); - await store.AssignAsync(userA, roleA, Now); - await store.AssignAsync(userB, roleB, Now); + await store.AssignAsync(userA, roleA, Now, ct: TestContext.Current.CancellationToken); + await store.AssignAsync(userB, roleB, Now, ct: TestContext.Current.CancellationToken); - var result = await store.GetAssignmentsAsync(userA); + var result = await store.GetAssignmentsAsync(userA, ct: TestContext.Current.CancellationToken); result.Should().ContainSingle(); result.Single().UserKey.Should().Be(userA); @@ -142,7 +142,7 @@ public async Task GetAssignmentsAsync_WhenUserHasNoAssignments_ReturnsEmpty() await using var db = await CreateDatabaseAsync(); var store = db.CreateStore(Tenant); - var result = await store.GetAssignmentsAsync(UserKey.New()); + var result = await store.GetAssignmentsAsync(UserKey.New(), ct: TestContext.Current.CancellationToken); result.Should().BeEmpty(); } @@ -162,12 +162,12 @@ public async Task RemoveAsync_WhenAssignmentExists_RemovesOnlyRequestedAssignmen var roleA = RoleId.New(); var roleB = RoleId.New(); - await store.AssignAsync(user, roleA, Now); - await store.AssignAsync(user, roleB, Now); + await store.AssignAsync(user, roleA, Now, ct: TestContext.Current.CancellationToken); + await store.AssignAsync(user, roleB, Now, ct: TestContext.Current.CancellationToken); - await store.RemoveAsync(user, roleA); + await store.RemoveAsync(user, roleA, ct: TestContext.Current.CancellationToken); - var result = await store.GetAssignmentsAsync(user); + var result = await store.GetAssignmentsAsync(user, ct: TestContext.Current.CancellationToken); result.Should().ContainSingle(); result.Single().RoleId.Should().Be(roleB); @@ -186,7 +186,7 @@ public async Task RemoveAsync_WhenAssignmentDoesNotExist_IsIdempotent() await act.Should().NotThrowAsync(); - var result = await store.GetAssignmentsAsync(user); + var result = await store.GetAssignmentsAsync(user, ct: TestContext.Current.CancellationToken); result.Should().BeEmpty(); } @@ -201,13 +201,13 @@ public async Task RemoveAsync_DoesNotRemoveSameRoleFromOtherUser() var userB = UserKey.New(); var roleId = RoleId.New(); - await store.AssignAsync(userA, roleId, Now); - await store.AssignAsync(userB, roleId, Now); + await store.AssignAsync(userA, roleId, Now, ct: TestContext.Current.CancellationToken); + await store.AssignAsync(userB, roleId, Now, ct: TestContext.Current.CancellationToken); - await store.RemoveAsync(userA, roleId); + await store.RemoveAsync(userA, roleId, ct: TestContext.Current.CancellationToken); - var assignmentsA = await store.GetAssignmentsAsync(userA); - var assignmentsB = await store.GetAssignmentsAsync(userB); + var assignmentsA = await store.GetAssignmentsAsync(userA, ct: TestContext.Current.CancellationToken); + var assignmentsB = await store.GetAssignmentsAsync(userB, ct: TestContext.Current.CancellationToken); assignmentsA.Should().BeEmpty(); @@ -231,14 +231,14 @@ public async Task RemoveAssignmentsByRoleAsync_RemovesRoleFromAllUsers() var targetRole = RoleId.New(); var otherRole = RoleId.New(); - await store.AssignAsync(userA, targetRole, Now); - await store.AssignAsync(userA, otherRole, Now); - await store.AssignAsync(userB, targetRole, Now); + await store.AssignAsync(userA, targetRole, Now, ct: TestContext.Current.CancellationToken); + await store.AssignAsync(userA, otherRole, Now, ct: TestContext.Current.CancellationToken); + await store.AssignAsync(userB, targetRole, Now, ct: TestContext.Current.CancellationToken); - await store.RemoveAssignmentsByRoleAsync(targetRole); + await store.RemoveAssignmentsByRoleAsync(targetRole, ct: TestContext.Current.CancellationToken); - var assignmentsA = await store.GetAssignmentsAsync(userA); - var assignmentsB = await store.GetAssignmentsAsync(userB); + var assignmentsA = await store.GetAssignmentsAsync(userA, ct: TestContext.Current.CancellationToken); + var assignmentsB = await store.GetAssignmentsAsync(userB, ct: TestContext.Current.CancellationToken); assignmentsA.Should().ContainSingle(); assignmentsA.Single().RoleId.Should().Be(otherRole); @@ -271,12 +271,12 @@ public async Task CountAssignmentsAsync_ReturnsNumberOfAssignmentsForRole() var roleA = RoleId.New(); var roleB = RoleId.New(); - await store.AssignAsync(UserKey.New(), roleA, Now); - await store.AssignAsync(UserKey.New(), roleA, Now); - await store.AssignAsync(UserKey.New(), roleA, Now); - await store.AssignAsync(UserKey.New(), roleB, Now); + await store.AssignAsync(UserKey.New(), roleA, Now, ct: TestContext.Current.CancellationToken); + await store.AssignAsync(UserKey.New(), roleA, Now, ct: TestContext.Current.CancellationToken); + await store.AssignAsync(UserKey.New(), roleA, Now, ct: TestContext.Current.CancellationToken); + await store.AssignAsync(UserKey.New(), roleB, Now, ct: TestContext.Current.CancellationToken); - var result = await store.CountAssignmentsAsync(roleA); + var result = await store.CountAssignmentsAsync(roleA, ct: TestContext.Current.CancellationToken); result.Should().Be(3); } @@ -288,7 +288,7 @@ public async Task CountAssignmentsAsync_WhenNoAssignmentsExist_ReturnsZero() var store = db.CreateStore(Tenant); var result = - await store.CountAssignmentsAsync(RoleId.New()); + await store.CountAssignmentsAsync(RoleId.New(), ct: TestContext.Current.CancellationToken); result.Should().Be(0); } @@ -311,10 +311,10 @@ public async Task GetAssignmentsAsync_IsTenantIsolated() var user = UserKey.New(); var roleId = RoleId.New(); - await storeA.AssignAsync(user, roleId, Now); + await storeA.AssignAsync(user, roleId, Now, ct: TestContext.Current.CancellationToken); - var fromA = await storeA.GetAssignmentsAsync(user); - var fromB = await storeB.GetAssignmentsAsync(user); + var fromA = await storeA.GetAssignmentsAsync(user, ct: TestContext.Current.CancellationToken); + var fromB = await storeB.GetAssignmentsAsync(user, ct: TestContext.Current.CancellationToken); fromA.Should().ContainSingle(); fromB.Should().BeEmpty(); @@ -333,13 +333,13 @@ public async Task CountAssignmentsAsync_IsTenantIsolated() var roleId = RoleId.New(); - await storeA.AssignAsync(UserKey.New(), roleId, Now); - await storeA.AssignAsync(UserKey.New(), roleId, Now); + await storeA.AssignAsync(UserKey.New(), roleId, Now, ct: TestContext.Current.CancellationToken); + await storeA.AssignAsync(UserKey.New(), roleId, Now, ct: TestContext.Current.CancellationToken); - await storeB.AssignAsync(UserKey.New(), roleId, Now); + await storeB.AssignAsync(UserKey.New(), roleId, Now, ct: TestContext.Current.CancellationToken); - var countA = await storeA.CountAssignmentsAsync(roleId); - var countB = await storeB.CountAssignmentsAsync(roleId); + var countA = await storeA.CountAssignmentsAsync(roleId, ct: TestContext.Current.CancellationToken); + var countB = await storeB.CountAssignmentsAsync(roleId, ct: TestContext.Current.CancellationToken); countA.Should().Be(2); countB.Should().Be(1); @@ -361,16 +361,16 @@ public async Task RemoveAssignmentsByRoleAsync_IsTenantIsolated() var userA = UserKey.New(); var userB = UserKey.New(); - await storeA.AssignAsync(userA, roleId, Now); - await storeB.AssignAsync(userB, roleId, Now); + await storeA.AssignAsync(userA, roleId, Now, ct: TestContext.Current.CancellationToken); + await storeB.AssignAsync(userB, roleId, Now, ct: TestContext.Current.CancellationToken); - await storeA.RemoveAssignmentsByRoleAsync(roleId); + await storeA.RemoveAssignmentsByRoleAsync(roleId, ct: TestContext.Current.CancellationToken); var assignmentsA = - await storeA.GetAssignmentsAsync(userA); + await storeA.GetAssignmentsAsync(userA, ct: TestContext.Current.CancellationToken); var assignmentsB = - await storeB.GetAssignmentsAsync(userB); + await storeB.GetAssignmentsAsync(userB, ct: TestContext.Current.CancellationToken); assignmentsA.Should().BeEmpty(); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Authorization/UserPermissionServiceTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Authorization/UserPermissionServiceTests.cs index 39fd8b08..051c3e84 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Authorization/UserPermissionServiceTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Authorization/UserPermissionServiceTests.cs @@ -37,7 +37,7 @@ public async Task GetMyPermissionsAsync_ReturnsDistinctSortedPermissions() Permission.From("orders.update") ]); - var result = await f.Sut.GetMyPermissionsAsync(f.Context); + var result = await f.Sut.GetMyPermissionsAsync(f.Context, ct: TestContext.Current.CancellationToken); result.Permissions.Select(x => x.Value) .Should().Equal( @@ -65,7 +65,7 @@ public async Task GetMyPermissionsAsync_WhenNoPermissions_ReturnsEmpty() It.IsAny())) .ReturnsAsync(Array.Empty()); - var result = await f.Sut.GetMyPermissionsAsync(f.Context); + var result = await f.Sut.GetMyPermissionsAsync(f.Context, ct: TestContext.Current.CancellationToken); result.Permissions.Should().BeEmpty(); } @@ -82,7 +82,7 @@ public async Task GetMyPermissionsAsync_UsesResourceTenantAndActor() It.IsAny())) .ReturnsAsync(Array.Empty()); - await f.Sut.GetMyPermissionsAsync(f.Context); + await f.Sut.GetMyPermissionsAsync(f.Context, ct: TestContext.Current.CancellationToken); f.Permissions.Verify(x => x.GetPermissionsAsync( f.Context.ResourceTenant, diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Authorization/UserRoleServiceTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Authorization/UserRoleServiceTests.cs index b1ea60c5..dbc71e6a 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Authorization/UserRoleServiceTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Authorization/UserRoleServiceTests.cs @@ -49,7 +49,7 @@ public async Task AssignAsync_WhenRoleExists_NormalizesRoleNameAndAssignsTargetU await f.Sut.AssignAsync( context, target, - " administrators "); + " administrators ", ct: TestContext.Current.CancellationToken); f.RoleStore.Verify(x => x.GetByNameAsync( "ADMINISTRATORS", @@ -156,7 +156,7 @@ public async Task AssignAsync_UsesResourceTenantForBothStores() await f.Sut.AssignAsync( context, target, - "Admin"); + "Admin", ct: TestContext.Current.CancellationToken); f.RoleFactory.Verify( x => x.Create(context.ResourceTenant), @@ -196,7 +196,7 @@ public async Task RemoveAsync_WhenRoleExists_NormalizesRoleNameAndRemovesAssignm await f.Sut.RemoveAsync( context, target, - " operators "); + " operators ", ct: TestContext.Current.CancellationToken); f.UserRoleStore.Verify(x => x.RemoveAsync( target, @@ -259,7 +259,7 @@ public async Task RemoveAsync_WhenRoleIsDeleted_StillRemovesAssignment() await f.Sut.RemoveAsync( context, target, - "legacy"); + "legacy", ct: TestContext.Current.CancellationToken); f.UserRoleStore.Verify(x => x.RemoveAsync( target, @@ -309,7 +309,7 @@ public async Task GetRolesAsync_WhenAssignmentsExist_JoinsAssignmentsWithRoles() var result = await f.Sut.GetRolesAsync( context, target, - new PageRequest()); + new PageRequest(), ct: TestContext.Current.CancellationToken); result.TotalCount.Should().Be(2); result.Items.Should().HaveCount(2); @@ -373,7 +373,7 @@ public async Task GetRolesAsync_WhenAssignmentReferencesMissingRole_IgnoresOrpha var result = await f.Sut.GetRolesAsync( context, target, - new PageRequest()); + new PageRequest(), ct: TestContext.Current.CancellationToken); result.TotalCount.Should().Be(1); result.Items.Should().ContainSingle(); @@ -419,7 +419,7 @@ public async Task GetRolesAsync_AppliesPaginationAfterJoin() { PageNumber = 2, PageSize = 1 - }); + }, ct: TestContext.Current.CancellationToken); result.TotalCount.Should().Be(3); result.PageNumber.Should().Be(2); @@ -457,7 +457,7 @@ public async Task GetRolesAsync_NormalizesInvalidPagingValues() { PageNumber = -10, PageSize = 0 - }); + }, ct: TestContext.Current.CancellationToken); result.PageNumber.Should().Be(1); result.PageSize.Should().Be(250); @@ -491,7 +491,7 @@ public async Task GetRolesAsync_WhenPageSizeExceedsMaximum_ClampsPageSize() { PageNumber = 1, PageSize = 5000 - }); + }, ct: TestContext.Current.CancellationToken); result.PageSize.Should().Be(1000); } @@ -524,7 +524,7 @@ public async Task GetRolesAsync_PreservesPagingMetadata() PageSize = 20, SortBy = "name", Descending = true - }); + }, ct: TestContext.Current.CancellationToken); result.PageNumber.Should().Be(3); result.PageSize.Should().Be(20); @@ -554,7 +554,7 @@ public async Task GetRolesAsync_UsesResourceTenantForBothStores() await f.Sut.GetRolesAsync( context, target, - new PageRequest()); + new PageRequest(), ct: TestContext.Current.CancellationToken); f.RoleFactory.Verify( x => x.Create(context.ResourceTenant), @@ -620,7 +620,7 @@ public async Task GetRolesAsync_WhenSortByNameAscending_SortsByName() { SortBy = nameof(UserRoleInfo.Name), Descending = false - }); + }, ct: TestContext.Current.CancellationToken); result.Items .Select(x => x.Name) @@ -655,7 +655,7 @@ public async Task GetRolesAsync_WhenSortByNameDescending_SortsByNameDescending() { SortBy = nameof(UserRoleInfo.Name), Descending = true - }); + }, ct: TestContext.Current.CancellationToken); result.Items .Select(x => x.Name) @@ -694,7 +694,7 @@ public async Task GetRolesAsync_WhenSortByAssignedAtAscending_SortsByAssignedAt( { SortBy = nameof(UserRoleInfo.AssignedAt), Descending = false - }); + }, ct: TestContext.Current.CancellationToken); result.Items .Select(x => x.AssignedAt) @@ -733,7 +733,7 @@ public async Task GetRolesAsync_WhenSortByAssignedAtDescending_SortsByAssignedAt { SortBy = nameof(UserRoleInfo.AssignedAt), Descending = true - }); + }, ct: TestContext.Current.CancellationToken); result.Items .Select(x => x.AssignedAt) @@ -764,7 +764,7 @@ public async Task GetRolesAsync_WhenSortByIsNotSpecified_DefaultsToNameAscending var result = await f.Sut.GetRolesAsync( context, target, - new PageRequest()); + new PageRequest(), ct: TestContext.Current.CancellationToken); result.Items .Select(x => x.Name) @@ -816,7 +816,7 @@ public async Task GetRolesAsync_SortsBeforeApplyingPagination() PageNumber = 2, PageSize = 2, SortBy = nameof(UserRoleInfo.Name) - }); + }, ct: TestContext.Current.CancellationToken); result.TotalCount.Should().Be(5); result.PageNumber.Should().Be(2); @@ -874,7 +874,7 @@ public async Task GetRolesAsync_WhenNamesAreEqual_UsesRoleIdAsDeterministicTieBr new PageRequest { SortBy = nameof(UserRoleInfo.Name) - }); + }, ct: TestContext.Current.CancellationToken); result.Items.Select(x => x.RoleId) .Should() diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/AuthStateSnapshotFactoryTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/AuthStateSnapshotFactoryTests.cs index 973c98fc..fa7af2e6 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/AuthStateSnapshotFactoryTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/AuthStateSnapshotFactoryTests.cs @@ -21,7 +21,7 @@ public async Task CreateAsync_should_return_snapshot_when_valid() var pprovider = new Mock(); var lprovider = new Mock(); - provider.Setup(x => x.GetAsync(It.IsAny(), It.IsAny(), default)) + provider.Setup(x => x.GetAsync(It.IsAny(), It.IsAny(), TestContext.Current.CancellationToken)) .ReturnsAsync(new PrimaryUserIdentifiers { UserName = "admin" @@ -39,7 +39,7 @@ public async Task CreateAsync_should_return_snapshot_when_valid() DateTimeOffset.UtcNow ); - var snapshot = await factory.CreateAsync(validation); + var snapshot = await factory.CreateAsync(validation, ct: TestContext.Current.CancellationToken); snapshot.Should().NotBeNull(); snapshot!.Identity.PrimaryUserName.Should().Be("admin"); @@ -55,7 +55,7 @@ public async Task CreateAsync_should_return_null_when_invalid() var factory = new AuthStateSnapshotFactory(provider.Object, pprovider.Object, lprovider.Object); var validation = SessionValidationResult.Invalid(SessionState.NotFound); - var snapshot = await factory.CreateAsync(validation); + var snapshot = await factory.CreateAsync(validation, ct: TestContext.Current.CancellationToken); snapshot.Should().BeNull(); } } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/Blazor/UAuthRequestClientTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/Blazor/UAuthRequestClientTests.cs index 821df048..e6148acd 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/Blazor/UAuthRequestClientTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/Blazor/UAuthRequestClientTests.cs @@ -41,7 +41,7 @@ private UAuthRequestClient CreateSut(UAuthClientProfile profile = UAuthClientPro public async Task NavigateAsync_InvokesUAuthPost() { var sut = CreateSut(); - await sut.NavigateAsync("/login"); + await sut.NavigateAsync("/login", ct: Xunit.TestContext.Current.CancellationToken); var invocation = JSInterop.Invocations.Single(x => x.Identifier == "uauth.post"); invocation.Arguments.Should().ContainSingle(); @@ -61,7 +61,7 @@ public async Task SendFormAsync_ReturnsTransportResult() .SetResult(expected); var sut = CreateSut(); - var result = await sut.SendFormAsync("/login"); + var result = await sut.SendFormAsync("/login", ct: Xunit.TestContext.Current.CancellationToken); result.Should().BeSameAs(expected); } @@ -153,7 +153,7 @@ public async Task TryAndCommitThrottleKeepsStatusAndRetryGuidance() JSInterop.Setup("uauth.tryAndCommit", _ => true) .SetResult(System.Text.Json.JsonSerializer.SerializeToElement(new { status = 429, retryAfter = "45", isSuccess = false })); var error = await Assert.ThrowsAsync(() => - CreateSut().TryAndCommitAsync("/try", "/commit", new { })); + CreateSut().TryAndCommitAsync("/try", "/commit", new { }, ct: Xunit.TestContext.Current.CancellationToken)); Assert.Equal(System.Net.HttpStatusCode.TooManyRequests, error.StatusCode); Assert.Equal("rate_limited", error.Code); Assert.Equal(TimeSpan.FromSeconds(45), error.RetryAfter); @@ -164,7 +164,7 @@ public async Task TryAndCommitStillDeserializesSuccessfulPreview() { JSInterop.Setup("uauth.tryAndCommit", _ => true) .SetResult(System.Text.Json.JsonSerializer.SerializeToElement(new { isSuccess = true, previewReceipt = "receipt" })); - var result = await CreateSut().TryAndCommitAsync("/try", "/commit", new { }); + var result = await CreateSut().TryAndCommitAsync("/try", "/commit", new { }, ct: Xunit.TestContext.Current.CancellationToken); Assert.True(result.IsSuccess); Assert.Equal("receipt", result.PreviewReceipt); } @@ -174,7 +174,7 @@ public async Task ObservedLogoutReturnsHttpRejectionRatherThanSubmittingAnUnobse { JSInterop.Setup("uauth.post", _ => true) .SetResult(new UAuthTransportResult { Status = 429, RetryAfter = "15" }); - var result = await CreateSut().SendFormAsync("/auth/logout", null, navigateOnSuccess: true); + var result = await CreateSut().SendFormAsync("/auth/logout", null, navigateOnSuccess: true, ct: Xunit.TestContext.Current.CancellationToken); Assert.Equal(429, result.Status); Assert.Equal("15", result.RetryAfter); var invocation = JSInterop.Invocations.Single(x => x.Identifier == "uauth.post"); @@ -196,7 +196,7 @@ public async Task SendFormAsync_ResolvesTrustedOriginCorrectly(string basePath, var sut = CreateSut(basePath: basePath); // Act - await sut.SendFormAsync("/auth/validate"); + await sut.SendFormAsync("/auth/validate", ct: Xunit.TestContext.Current.CancellationToken); // Assert var invocation = JSInterop.Invocations diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/ClientRateLimitingTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/ClientRateLimitingTests.cs index e3ee0872..527a1367 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/ClientRateLimitingTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/ClientRateLimitingTests.cs @@ -64,16 +64,16 @@ public async Task ValidationThrottlePreservesStateAndBlazorPrincipal_ThenRevocat .ReturnsAsync(new UAuthTransportResult { Status = 200, Ok = true, Body = JsonSerializer.SerializeToElement(new AuthValidationResult { State = SessionState.Revoked }) }); using var manager = new UAuthStateManager(CreateClient(flows: CreateFlowClient()), Events.Object, clock); var provider = new UAuthAuthenticationStateProvider(manager); - await manager.EnsureAsync(); + await manager.EnsureAsync(ct: TestContext.Current.CancellationToken); var lastValidated = manager.State.LastValidatedAt; - await manager.EnsureAsync(force: true); - await manager.EnsureAsync(force: true); + await manager.EnsureAsync(force: true, ct: TestContext.Current.CancellationToken); + await manager.EnsureAsync(force: true, ct: TestContext.Current.CancellationToken); Assert.True(manager.State.IsAuthenticated); Assert.True((await provider.GetAuthenticationStateAsync()).User.Identity!.IsAuthenticated); Assert.Equal(lastValidated, manager.State.LastValidatedAt); Request.Verify(x => x.SendFormAsync("/auth/validate", null, It.IsAny()), Times.Exactly(2)); clock.Advance(TimeSpan.FromMinutes(2)); - await manager.EnsureAsync(force: true); + await manager.EnsureAsync(force: true, ct: TestContext.Current.CancellationToken); Assert.False(manager.State.IsAuthenticated); Assert.False((await provider.GetAuthenticationStateAsync()).User.Identity!.IsAuthenticated); } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/SessionCoordinatorTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/SessionCoordinatorTests.cs index c3338855..d4924138 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/SessionCoordinatorTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/SessionCoordinatorTests.cs @@ -30,7 +30,7 @@ public async Task StartAsync_should_not_start_when_auto_refresh_disabled() }); var coordinator = new SessionCoordinator(client.Object, nav.Object, options, diagnostics, clock); - await coordinator.StartAsync(); + await coordinator.StartAsync(cancellationToken: TestContext.Current.CancellationToken); Assert.False(diagnostics.IsRunning); } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthClientBootstrapperTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthClientBootstrapperTests.cs index ce53fef8..68151e9e 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthClientBootstrapperTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthClientBootstrapperTests.cs @@ -23,8 +23,8 @@ public async Task EnsureStartedAsync_should_initialize_only_once() browser, new UAuthClientOptions()); - await bootstrapper.EnsureStartedAsync(); - await bootstrapper.EnsureStartedAsync(); + await bootstrapper.EnsureStartedAsync(ct: TestContext.Current.CancellationToken); + await bootstrapper.EnsureStartedAsync(ct: TestContext.Current.CancellationToken); bootstrapper.IsStarted.Should().BeTrue(); @@ -100,7 +100,7 @@ public async Task EnsureStartedAsync_should_configure_same_origin_antiforgery() browser, options); - await bootstrapper.EnsureStartedAsync(); + await bootstrapper.EnsureStartedAsync(ct: TestContext.Current.CancellationToken); browser.Verify( x => x.ConfigureAntiforgeryAsync( @@ -131,7 +131,7 @@ public async Task EnsureStartedAsync_should_configure_cross_origin_antiforgery() browser, options); - await bootstrapper.EnsureStartedAsync(); + await bootstrapper.EnsureStartedAsync(ct: TestContext.Current.CancellationToken); browser.Verify( x => x.ConfigureAntiforgeryAsync( @@ -175,11 +175,11 @@ public async Task EnsureStartedAsync_should_retry_when_antiforgery_configuration new UAuthClientOptions()); await Assert.ThrowsAsync( - () => bootstrapper.EnsureStartedAsync()); + () => bootstrapper.EnsureStartedAsync(ct: TestContext.Current.CancellationToken)); bootstrapper.IsStarted.Should().BeFalse(); - await bootstrapper.EnsureStartedAsync(); + await bootstrapper.EnsureStartedAsync(ct: TestContext.Current.CancellationToken); bootstrapper.IsStarted.Should().BeTrue(); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthResourceClientTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthResourceClientTests.cs index 781e1b8f..e9680496 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthResourceClientTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthResourceClientTests.cs @@ -40,19 +40,19 @@ public async Task Resource_Methods_Should_Forward_Correct_Method(string method) switch (method) { case "GET": - await client.GetAsync("/api/products"); + await client.GetAsync("/api/products", ct: TestContext.Current.CancellationToken); break; case "POST": - await client.PostAsync("/api/products", payload); + await client.PostAsync("/api/products", payload, ct: TestContext.Current.CancellationToken); break; case "PUT": - await client.PutAsync("/api/products", payload); + await client.PutAsync("/api/products", payload, ct: TestContext.Current.CancellationToken); break; case "PATCH": - await client.PatchAsync("/api/products", payload); + await client.PatchAsync("/api/products", payload, ct: TestContext.Current.CancellationToken); break; } @@ -82,7 +82,7 @@ public async Task Get_Should_Deserialize_Success_Response() }); var result = await CreateClient() - .GetAsync("/api/products/1"); + .GetAsync("/api/products/1", ct: TestContext.Current.CancellationToken); result.IsSuccess.Should().BeTrue(); result.Status.Should().Be(200); @@ -108,7 +108,7 @@ public async Task Get_Should_Return_Default_When_Body_Is_Empty() }); var result = await CreateClient() - .GetAsync("/api/products/1"); + .GetAsync("/api/products/1", ct: TestContext.Current.CancellationToken); result.IsSuccess.Should().BeTrue(); result.Status.Should().Be(204); @@ -138,7 +138,7 @@ public async Task Get_Should_Parse_Problem_On_Failure() }); var result = await CreateClient() - .GetAsync("/api/products"); + .GetAsync("/api/products", ct: TestContext.Current.CancellationToken); result.IsSuccess.Should().BeFalse(); result.Status.Should().Be(403); @@ -164,7 +164,7 @@ public async Task Get_Should_Handle_Invalid_Problem_Json() }); var result = await CreateClient() - .GetAsync("/api/products"); + .GetAsync("/api/products", ct: TestContext.Current.CancellationToken); result.IsSuccess.Should().BeFalse(); result.Status.Should().Be(500); @@ -187,7 +187,7 @@ public async Task Delete_Should_Return_Success_Without_Body() }); var result = await CreateClient() - .DeleteAsync("/api/products/1"); + .DeleteAsync("/api/products/1", ct: TestContext.Current.CancellationToken); result.IsSuccess.Should().BeTrue(); result.Status.Should().Be(204); @@ -211,7 +211,7 @@ public async Task Delete_Should_Parse_Problem_On_Failure() }); var result = await CreateClient() - .DeleteAsync("/api/products/1"); + .DeleteAsync("/api/products/1", ct: TestContext.Current.CancellationToken); result.IsSuccess.Should().BeFalse(); result.Status.Should().Be(404); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthResourceRequestClientTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthResourceRequestClientTests.cs index 06c5ede3..99c7c126 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthResourceRequestClientTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthResourceRequestClientTests.cs @@ -65,7 +65,7 @@ public async Task Send_Should_Ensure_Bootstrap_Before_JS_Request() var client = CreateClient(); - var task = client.SendAsync("GET", "/api/products"); + var task = client.SendAsync("GET", "/api/products", ct: TestContext.Current.CancellationToken); _bootstrapper.Verify(x => x.EnsureStartedAsync( It.IsAny()), Times.Once); @@ -111,7 +111,7 @@ public async Task Send_Should_Pass_Correct_Arguments_To_JS() await client.SendAsync( "POST", "/api/products", - payload); + payload, ct: TestContext.Current.CancellationToken); capturedOptions.Should().NotBeNull(); @@ -159,7 +159,7 @@ public async Task Send_Should_Return_Success_Response() SetupResponse(response); var result = await CreateClient() - .SendAsync("GET", "/api/products/1"); + .SendAsync("GET", "/api/products/1", ct: TestContext.Current.CancellationToken); result.Should().BeSameAs(response); } @@ -177,7 +177,7 @@ public async Task Send_Should_Return_Http_Error_Response() SetupResponse(response); var result = await CreateClient() - .SendAsync("GET", "/api/products"); + .SendAsync("GET", "/api/products", ct: TestContext.Current.CancellationToken); result.Should().BeSameAs(response); result.Status.Should().Be(403); @@ -332,8 +332,8 @@ public async Task Send_Should_Generate_Unique_RequestIds() var client = CreateClient(); - await client.SendAsync("GET", "/api/products/1"); - await client.SendAsync("GET", "/api/products/2"); + await client.SendAsync("GET", "/api/products/1", ct: TestContext.Current.CancellationToken); + await client.SendAsync("GET", "/api/products/2", ct: TestContext.Current.CancellationToken); requestIds.Should().HaveCount(2); requestIds.Distinct().Should().HaveCount(2); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthStateManagerTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthStateManagerTests.cs index 8f25e4a3..5e1bc430 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthStateManagerTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthStateManagerTests.cs @@ -45,8 +45,8 @@ public async Task EnsureAsync_should_not_validate_when_authenticated_and_not_sta var manager = new UAuthStateManager(client.Object, events.Object, clock.Object); - await manager.EnsureAsync(); - await manager.EnsureAsync(); + await manager.EnsureAsync(ct: TestContext.Current.CancellationToken); + await manager.EnsureAsync(ct: TestContext.Current.CancellationToken); flowClient.Verify(x => x.ValidateAsync(), Times.Once); } @@ -70,9 +70,9 @@ public async Task EnsureAsync_Should_Deduplicate_InFlight_Calls_But_Allow_Later_ Snapshot = CreateSnapshot() }); - var first = manager.EnsureAsync(force: true); - var second = manager.EnsureAsync(force: true); - var third = manager.EnsureAsync(force: true); + var first = manager.EnsureAsync(force: true, ct: TestContext.Current.CancellationToken); + var second = manager.EnsureAsync(force: true, ct: TestContext.Current.CancellationToken); + var third = manager.EnsureAsync(force: true, ct: TestContext.Current.CancellationToken); first.IsCompleted.Should().BeFalse(); @@ -93,7 +93,7 @@ public async Task EnsureAsync_Should_Deduplicate_InFlight_Calls_But_Allow_Later_ manager.State.IsAuthenticated.Should().BeTrue(); - await manager.EnsureAsync(force: true); + await manager.EnsureAsync(force: true, ct: TestContext.Current.CancellationToken); flows.Verify( x => x.ValidateAsync(), @@ -115,7 +115,7 @@ public async Task EnsureAsync_invalid_should_clear_state() }); var manager = new UAuthStateManager(client.Object, events.Object, clock.Object); - await manager.EnsureAsync(); + await manager.EnsureAsync(ct: TestContext.Current.CancellationToken); manager.State.IsAuthenticated.Should().BeFalse(); } @@ -127,7 +127,7 @@ public async Task OnLogin_Should_Mark_Authenticated_State_Stale() { SetupValidSnapshot(flows); - await manager.EnsureAsync(); + await manager.EnsureAsync(ct: TestContext.Current.CancellationToken); manager.State.IsAuthenticated.Should().BeTrue(); manager.State.IsStale.Should().BeFalse(); @@ -160,7 +160,7 @@ public async Task OnLogout_Should_Clear_Authenticated_State() { SetupValidSnapshot(flows); - await manager.EnsureAsync(); + await manager.EnsureAsync(ct: TestContext.Current.CancellationToken); await manager.OnLogoutAsync(); manager.State.IsAuthenticated.Should().BeFalse(); @@ -178,11 +178,11 @@ public async Task MarkStale_Should_Cause_Next_Ensure_To_Validate() { SetupValidSnapshot(flows); - await manager.EnsureAsync(); + await manager.EnsureAsync(ct: TestContext.Current.CancellationToken); manager.MarkStale(); - await manager.EnsureAsync(); + await manager.EnsureAsync(ct: TestContext.Current.CancellationToken); flows.Verify(x => x.ValidateAsync(), Times.Exactly(2)); manager.State.IsStale.Should().BeFalse(); @@ -197,14 +197,14 @@ public async Task Clear_Should_Require_Next_Validation() { SetupValidSnapshot(flows); - await manager.EnsureAsync(); + await manager.EnsureAsync(ct: TestContext.Current.CancellationToken); manager.Clear(); manager.State.IsAuthenticated.Should().BeFalse(); manager.NeedsValidation.Should().BeTrue(); - await manager.EnsureAsync(); + await manager.EnsureAsync(ct: TestContext.Current.CancellationToken); flows.Verify(x => x.ValidateAsync(), Times.Exactly(2)); } @@ -222,7 +222,7 @@ public async Task State_Invalidating_Events_Should_Clear_State(UAuthStateEvent e { SetupValidSnapshot(flows); - await manager.EnsureAsync(); + await manager.EnsureAsync(ct: TestContext.Current.CancellationToken); await events.PublishAsync( new UAuthStateEventArgsEmpty( @@ -242,7 +242,7 @@ public async Task ProfileChanged_Should_Patch_Current_Identity() { SetupValidSnapshot(flows); - await manager.EnsureAsync(); + await manager.EnsureAsync(ct: TestContext.Current.CancellationToken); await events.PublishAsync( new UAuthStateEventArgs( @@ -268,7 +268,7 @@ public async Task UserStatusChanged_Should_Patch_Current_Identity() { SetupValidSnapshot(flows); - await manager.EnsureAsync(); + await manager.EnsureAsync(ct: TestContext.Current.CancellationToken); await events.PublishAsync( new UAuthStateEventArgs( @@ -294,7 +294,7 @@ public async Task RefreshMode_None_Should_Ignore_Event() { SetupValidSnapshot(flows); - await manager.EnsureAsync(); + await manager.EnsureAsync(ct: TestContext.Current.CancellationToken); await events.PublishAsync( new UAuthStateEventArgsEmpty( @@ -314,7 +314,7 @@ public async Task RefreshMode_Validate_Should_Force_Validation() { SetupValidSnapshot(flows); - await manager.EnsureAsync(); + await manager.EnsureAsync(ct: TestContext.Current.CancellationToken); await events.PublishAsync( new UAuthStateEventArgsEmpty( @@ -333,7 +333,7 @@ public async Task ValidationCalled_With_Patch_Should_Mark_Validated() { SetupValidSnapshot(flows); - await manager.EnsureAsync(); + await manager.EnsureAsync(ct: TestContext.Current.CancellationToken); manager.MarkStale(); @@ -362,7 +362,7 @@ public async Task AuthorizationChanged_With_Patch_Should_Mark_Stale() { SetupValidSnapshot(flows); - await manager.EnsureAsync(); + await manager.EnsureAsync(ct: TestContext.Current.CancellationToken); await events.PublishAsync( new UAuthStateEventArgsEmpty( @@ -381,7 +381,7 @@ public async Task Dispose_Should_Unsubscribe_From_State_Events() SetupValidSnapshot(flows); - await manager.EnsureAsync(); + await manager.EnsureAsync(ct: TestContext.Current.CancellationToken); manager.Dispose(); @@ -401,9 +401,9 @@ public async Task EnsureAsync_Should_Allow_Revalidation_After_Synchronous_Comple using (manager) { SetupValidSnapshot(flows); - await manager.EnsureAsync(force: true); - await manager.EnsureAsync(force: true); - await manager.EnsureAsync(force: true); + await manager.EnsureAsync(force: true, ct: TestContext.Current.CancellationToken); + await manager.EnsureAsync(force: true, ct: TestContext.Current.CancellationToken); + await manager.EnsureAsync(force: true, ct: TestContext.Current.CancellationToken); flows.Verify(x => x.ValidateAsync(), Times.Exactly(3)); } } @@ -424,7 +424,7 @@ public async Task EnsureAsync_When_Session_Is_Not_Active_Should_Clear_State(Sess { SetupValidSnapshot(flows); - await manager.EnsureAsync(); + await manager.EnsureAsync(ct: TestContext.Current.CancellationToken); manager.State.IsAuthenticated.Should().BeTrue(); @@ -434,7 +434,7 @@ public async Task EnsureAsync_When_Session_Is_Not_Active_Should_Clear_State(Sess State = sessionState }); - await manager.EnsureAsync(force: true); + await manager.EnsureAsync(force: true, ct: TestContext.Current.CancellationToken); manager.State.IsAuthenticated.Should().BeFalse(); manager.State.Identity.Should().BeNull(); @@ -471,7 +471,7 @@ await act.Should() manager.State.IsAuthenticated.Should().BeFalse(); - await manager.EnsureAsync(force: true); + await manager.EnsureAsync(force: true, ct: TestContext.Current.CancellationToken); manager.State.IsAuthenticated.Should().BeTrue(); manager.State.IsStale.Should().BeFalse(); @@ -491,7 +491,7 @@ public async Task EnsureAsync_When_Network_Fails_Should_Preserve_Authenticated_S { SetupValidSnapshot(flows); - await manager.EnsureAsync(); + await manager.EnsureAsync(ct: TestContext.Current.CancellationToken); var originalIdentity = manager.State.Identity; var originalValidatedAt = manager.State.LastValidatedAt; @@ -520,7 +520,7 @@ public async Task EnsureAsync_When_Active_Result_Has_No_Snapshot_Should_Mark_Sta { SetupValidSnapshot(flows); - await manager.EnsureAsync(); + await manager.EnsureAsync(ct: TestContext.Current.CancellationToken); flows.Setup(x => x.ValidateAsync()) .ReturnsAsync(new AuthValidationResult @@ -529,7 +529,7 @@ public async Task EnsureAsync_When_Active_Result_Has_No_Snapshot_Should_Mark_Sta Snapshot = null }); - await manager.EnsureAsync(force: true); + await manager.EnsureAsync(force: true, ct: TestContext.Current.CancellationToken); manager.State.IsAuthenticated.Should().BeTrue(); manager.State.IsStale.Should().BeTrue(); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/CodeBeam.UltimateAuth.Tests.Unit.csproj b/tests/CodeBeam.UltimateAuth.Tests.Unit/CodeBeam.UltimateAuth.Tests.Unit.csproj index d90a6ba2..bf71f5b0 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/CodeBeam.UltimateAuth.Tests.Unit.csproj +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/CodeBeam.UltimateAuth.Tests.Unit.csproj @@ -1,28 +1,18 @@  - net10.0 enable enable false - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - + + + + diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Core/RefreshTokenValidatorTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Core/RefreshTokenValidatorTests.cs index 4ce16d91..b506366e 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Core/RefreshTokenValidatorTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Core/RefreshTokenValidatorTests.cs @@ -38,7 +38,7 @@ public async Task Invalid_When_Token_Not_Found() RefreshToken = "non-existing", Now = DateTimeOffset.UtcNow, Device = DeviceContext.Create(DeviceId.Create(ValidDeviceId), null, null, null, null, null), - }); + }, ct: TestContext.Current.CancellationToken); Assert.False(result.IsValid); Assert.Equal(RefreshTokenValidationState.NotFound, result.State); @@ -69,7 +69,7 @@ public async Task Invalid_When_Token_Is_Revoked_Without_Replacement() now.AddMinutes(5)); await store.StoreAsync( - token.Revoke(now)); + token.Revoke(now), ct: TestContext.Current.CancellationToken); var result = await validator.ValidateAsync( new RefreshTokenValidationContext @@ -84,7 +84,7 @@ await store.StoreAsync( null, null, null) - }); + }, ct: TestContext.Current.CancellationToken); Assert.False(result.IsValid); Assert.Equal( @@ -119,7 +119,7 @@ public async Task Consumed_When_Token_Was_Replaced_By_Rotation() await store.StoreAsync( token.Revoke( now, - "replacement-refresh-token-hash")); + "replacement-refresh-token-hash"), ct: TestContext.Current.CancellationToken); var result = await validator.ValidateAsync( new RefreshTokenValidationContext @@ -134,7 +134,7 @@ await store.StoreAsync( null, null, null) - }); + }, ct: TestContext.Current.CancellationToken); Assert.False(result.IsValid); Assert.Equal( @@ -180,7 +180,7 @@ public async Task Invalid_When_Expected_Session_Id_Does_Not_Match() now, now.AddMinutes(10)); - await store.StoreAsync(token); + await store.StoreAsync(token, ct: TestContext.Current.CancellationToken); var result = await validator.ValidateAsync( new RefreshTokenValidationContext @@ -198,7 +198,7 @@ public async Task Invalid_When_Expected_Session_Id_Does_Not_Match() null, null, null), - }); + }, ct: TestContext.Current.CancellationToken); Assert.False(result.IsValid); @@ -238,7 +238,7 @@ public async Task Invalid_When_Token_Is_Expired() now.AddMinutes(-10), now.AddMinutes(-1)); - await store.StoreAsync(token); + await store.StoreAsync(token, ct: TestContext.Current.CancellationToken); var result = await validator.ValidateAsync( new RefreshTokenValidationContext @@ -253,7 +253,7 @@ public async Task Invalid_When_Token_Is_Expired() null, null, null), - }); + }, ct: TestContext.Current.CancellationToken); Assert.False(result.IsValid); @@ -286,7 +286,7 @@ public async Task Valid_When_Token_Is_Active() now, now.AddMinutes(10)); - await store.StoreAsync(token); + await store.StoreAsync(token, ct: TestContext.Current.CancellationToken); var result = await validator.ValidateAsync( new RefreshTokenValidationContext @@ -295,7 +295,7 @@ public async Task Valid_When_Token_Is_Active() RefreshToken = raw, Now = now, Device = DeviceContext.Create(DeviceId.Create(ValidDeviceId), null, null, null, null, null), - }); + }, ct: TestContext.Current.CancellationToken); Assert.True(result.IsValid); Assert.Equal(RefreshTokenValidationState.Valid, result.State); @@ -325,7 +325,7 @@ public async Task Reuse_Detected_When_Old_Token_Is_Reused_After_Rotation() now, now.AddMinutes(10)); - await store.StoreAsync(token.Revoke(now, "new-hash")); + await store.StoreAsync(token.Revoke(now, "new-hash"), ct: TestContext.Current.CancellationToken); var result = await validator.ValidateAsync( new RefreshTokenValidationContext @@ -334,7 +334,7 @@ public async Task Reuse_Detected_When_Old_Token_Is_Reused_After_Rotation() RefreshToken = raw, Now = now, Device = DeviceContext.Create(DeviceId.Create(ValidDeviceId), null, null, null, null, null), - }); + }, ct: TestContext.Current.CancellationToken); Assert.False(result.IsValid); Assert.Equal(RefreshTokenValidationState.Consumed, result.State); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Credentials/ChangePasswordTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Credentials/ChangePasswordTests.cs index 1af5ece3..4609b239 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Credentials/ChangePasswordTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Credentials/ChangePasswordTests.cs @@ -24,7 +24,7 @@ public async Task Change_password_with_correct_current_should_succeed() { CurrentSecret = "user", NewSecret = "newpass123" - }); + }, ct: TestContext.Current.CancellationToken); result.IsSuccess.Should().BeTrue(); } @@ -75,7 +75,7 @@ public async Task Change_password_should_increment_version() var context = TestAccessContext.ForUser(TestUsers.User, UAuthActions.Credentials.ChangeSelf); - var before = await service.GetAllAsync(context); + var before = await service.GetAllAsync(context, ct: TestContext.Current.CancellationToken); var versionBefore = before.Credentials.Single().Version; await service.ChangeSecretAsync(context, @@ -83,9 +83,9 @@ await service.ChangeSecretAsync(context, { CurrentSecret = "user", NewSecret = "newpass123" - }); + }, ct: TestContext.Current.CancellationToken); - var after = await service.GetAllAsync(context); + var after = await service.GetAllAsync(context, ct: TestContext.Current.CancellationToken); var versionAfter = after.Credentials.Single().Version; versionAfter.Should().Be(versionBefore + 1); @@ -106,14 +106,14 @@ await service.ChangeSecretAsync(context, { CurrentSecret = "user", NewSecret = "newpass123" - }); + }, ct: TestContext.Current.CancellationToken); var oldLogin = await orchestrator.LoginAsync(flow, new LoginRequest { Identifier = "user", Secret = "user" - }); + }, ct: TestContext.Current.CancellationToken); oldLogin.IsSuccess.Should().BeFalse(); @@ -122,7 +122,7 @@ await service.ChangeSecretAsync(context, { Identifier = "user", Secret = "newpass123" - }); + }, ct: TestContext.Current.CancellationToken); newLogin.IsSuccess.Should().BeTrue(); } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Credentials/CredentialManagementServiceTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Credentials/CredentialManagementServiceTests.cs index 4c9850f2..d4ffdc60 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Credentials/CredentialManagementServiceTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Credentials/CredentialManagementServiceTests.cs @@ -54,7 +54,7 @@ public async Task GetAllAsync_ReturnsCredentialsForTargetUser() It.IsAny())) .ReturnsAsync(new[] { credential }); - var result = await f.Sut.GetAllAsync(context); + var result = await f.Sut.GetAllAsync(context, ct: TestContext.Current.CancellationToken); result.Credentials.Should().ContainSingle(); @@ -98,7 +98,7 @@ public async Task AddAsync_HashesSecretAndPersistsCredentialForTargetUser() new AddCredentialRequest { Secret = "new-password" - }); + }, ct: TestContext.Current.CancellationToken); result.IsSuccess.Should().BeTrue(); @@ -341,7 +341,7 @@ public async Task ChangeSecretAsync_Self_WhenValid_SavesAndRevokesOtherChains() { CurrentSecret = "current-password", NewSecret = "new-password" - }); + }, ct: TestContext.Current.CancellationToken); result.IsSuccess.Should().BeTrue(); @@ -387,7 +387,7 @@ public async Task ChangeSecretAsync_SelfWithoutActorChain_RevokesAllChains() { CurrentSecret = "current-password", NewSecret = "new-password" - }); + }, ct: TestContext.Current.CancellationToken); result.IsSuccess.Should().BeTrue(); @@ -455,7 +455,7 @@ public async Task ChangeSecretAsync_Admin_WhenValid_DoesNotRequireCurrentSecret_ { CurrentSecret = null, NewSecret = "new-password" - }); + }, ct: TestContext.Current.CancellationToken); result.IsSuccess.Should().BeTrue(); @@ -520,7 +520,7 @@ public async Task RevokeAsync_WhenCredentialBelongsToDifferentUser_ReturnsNotFou new RevokeCredentialRequest { Id = id - }); + }, ct: TestContext.Current.CancellationToken); result.IsSuccess.Should().BeFalse(); result.Error.Should().Be("credential_not_found"); @@ -570,7 +570,7 @@ public async Task RevokeAsync_WhenValid_RevokesAndSavesExpectedVersion() new RevokeCredentialRequest { Id = id - }); + }, ct: TestContext.Current.CancellationToken); result.IsSuccess.Should().BeTrue(); } @@ -626,7 +626,7 @@ public async Task DeleteAsync_WhenValid_UsesRequestedDeleteMode(DeleteMode mode) { Id = id, Mode = mode - }); + }, ct: TestContext.Current.CancellationToken); result.IsSuccess.Should().BeTrue( $"delete should succeed but returned '{result.Error}'"); @@ -673,7 +673,7 @@ public async Task DeleteAsync_WhenCredentialBelongsToDifferentUser_ReturnsNotFou { Id = id, Mode = DeleteMode.Hard - }); + }, ct: TestContext.Current.CancellationToken); result.IsSuccess.Should().BeFalse(); result.Error.Should().Be("credential_not_found"); @@ -833,7 +833,7 @@ public async Task BeginResetAsync_Anonymous_UnknownIdentifier_DoesNotRequireTarg Identifier = identifier, CredentialType = CredentialType.Password, ResetCodeType = ResetCodeType.Token - }); + }, ct: TestContext.Current.CancellationToken); result.ExpiresAt.Should().BeAfter(Now); @@ -876,7 +876,7 @@ public async Task CompleteResetAsync_Anonymous_UnknownIdentifier_DoesNotRequireT CredentialType = CredentialType.Password, ResetToken = "some-token", NewSecret = "new-password" - }); + }, ct: TestContext.Current.CancellationToken); result.IsSuccess.Should().BeTrue(); @@ -1053,7 +1053,7 @@ public async Task DeleteAsync_Hard_RemovesPasswordHistory() { Id = id, Mode = DeleteMode.Hard - }); + }, ct: TestContext.Current.CancellationToken); result.IsSuccess.Should().BeTrue(); @@ -1105,7 +1105,7 @@ await f.Sut.AddAsync( new AddCredentialRequest { Secret = "ValidPassword123!" - }); + }, ct: TestContext.Current.CancellationToken); f.PasswordPipeline.Verify( x => x.ValidateAsync( diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Credentials/EfCorePasswordHistoryAtomicityTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Credentials/EfCorePasswordHistoryAtomicityTests.cs index 9f5a7626..4fa079f5 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Credentials/EfCorePasswordHistoryAtomicityTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Credentials/EfCorePasswordHistoryAtomicityTests.cs @@ -41,7 +41,7 @@ public async Task FailedHistoryOperation_RollsBackCredentialAndHistory( await using var connection = new SqliteConnection("Data Source=:memory:"); - await connection.OpenAsync(); + await connection.OpenAsync(cancellationToken: TestContext.Current.CancellationToken); var dbOptions = new DbContextOptionsBuilder() .UseSqlite(connection) @@ -52,7 +52,7 @@ public async Task FailedHistoryOperation_RollsBackCredentialAndHistory( await using (var db = new UAuthCredentialDbContext(dbOptions)) { - await db.Database.EnsureCreatedAsync(); + await db.Database.EnsureCreatedAsync(cancellationToken: TestContext.Current.CancellationToken); var user = UserKey.New(); @@ -73,7 +73,7 @@ public async Task FailedHistoryOperation_RollsBackCredentialAndHistory( var history = new EfCorePasswordHistoryStore( db, Tenant); - await credentials.AddAsync(credential); + await credentials.AddAsync(credential, ct: TestContext.Current.CancellationToken); originalHistoryId = Guid.NewGuid(); @@ -85,7 +85,7 @@ await history.AddAsync(new PasswordHistoryEntry CredentialKey = new CredentialKey(Tenant, credentialId), SecretHash = Hash("older"), ReplacedAt = Now.AddDays(-1) - }); + }, ct: TestContext.Current.CancellationToken); var pipeline = new Mock(); pipeline.Setup(x => x.ValidateAsync( @@ -137,7 +137,7 @@ await history.AddAsync(new PasswordHistoryEntry hasher.Object); await using var transaction = - await db.Database.BeginTransactionAsync(); + await db.Database.BeginTransactionAsync(cancellationToken: TestContext.Current.CancellationToken); await Assert.ThrowsAsync(() => writer.ChangeAsync( @@ -149,8 +149,9 @@ await Assert.ThrowsAsync(() => }, Options(2), Now.AddMinutes(1), - default)); + TestContext.Current.CancellationToken)); + // Cleanup must complete even when the test is cancelled. await transaction.RollbackAsync(); } @@ -159,7 +160,7 @@ await Assert.ThrowsAsync(() => { var savedCredential = await verifyDb.PasswordCredentials .AsNoTracking() - .SingleAsync(x => x.Id == credentialId); + .SingleAsync(x => x.Id == credentialId, cancellationToken: TestContext.Current.CancellationToken); Assert.Equal(Hash("current"), savedCredential.SecretHash); Assert.Equal(0, savedCredential.Version); @@ -167,7 +168,7 @@ await Assert.ThrowsAsync(() => var history = await verifyDb.PasswordHistory .AsNoTracking() .Where(x => x.CredentialId == credentialId) - .ToListAsync(); + .ToListAsync(cancellationToken: TestContext.Current.CancellationToken); Assert.Single(history); Assert.Equal(originalHistoryId, history[0].Id); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Credentials/PasswordCredentialWriterTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Credentials/PasswordCredentialWriterTests.cs index 98f820ae..e2fefb40 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Credentials/PasswordCredentialWriterTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Credentials/PasswordCredentialWriterTests.cs @@ -102,7 +102,7 @@ public async Task Create_ValidatesAndPersistsHashedPassword() var result = await f.Writer.CreateAsync( Tenant, user, "new-password", - Context(), Options(3), Now, default); + Context(), Options(3), Now, TestContext.Current.CancellationToken); Assert.Same(result, saved); Assert.Equal(user, result.UserKey); @@ -157,7 +157,7 @@ public async Task Change_WithHistory_StoresOldHashAndPrunes() var updated = await f.Writer.ChangeAsync( credential, "new-password", Context(), Options(3), - Now.AddMinutes(1), default); + Now.AddMinutes(1), TestContext.Current.CancellationToken); Assert.Equal(Hash("new-password"), updated.SecretHash); Assert.NotNull(added); @@ -193,7 +193,7 @@ public async Task Change_WithoutHistory_PrunesAllButDoesNotAdd() await f.Writer.ChangeAsync( credential, "new-password", Context(), Options(0), - Now.AddMinutes(1), default); + Now.AddMinutes(1), TestContext.Current.CancellationToken); f.History.Verify(x => x.AddAsync( It.IsAny(), @@ -227,13 +227,13 @@ public async Task ValidationFailure_PreventsPersistence( { await f.Writer.ChangeAsync( Credential(), "reused", - Context(), Options(3), Now, default); + Context(), Options(3), Now, TestContext.Current.CancellationToken); } else { await f.Writer.CreateAsync( Tenant, UserKey.New(), "reused", - Context(), Options(3), Now, default); + Context(), Options(3), Now, TestContext.Current.CancellationToken); } }); @@ -266,7 +266,7 @@ public async Task Change_WhenCredentialSaveFails_DoesNotWriteHistory() await Assert.ThrowsAsync(() => f.Writer.ChangeAsync( Credential(), "new-password", - Context(), Options(3), Now, default)); + Context(), Options(3), Now, TestContext.Current.CancellationToken)); f.HistoryFactory.Verify( x => x.Create(It.IsAny()), @@ -286,7 +286,7 @@ public async Task Change_WhenHistoryAddFails_DoesNotPrune() await Assert.ThrowsAsync(() => f.Writer.ChangeAsync( Credential(), "new-password", - Context(), Options(3), Now, default)); + Context(), Options(3), Now, TestContext.Current.CancellationToken)); f.History.Verify(x => x.PruneAsync( It.IsAny(), @@ -308,7 +308,7 @@ public async Task Change_WhenPruneFails_PropagatesFailure() await Assert.ThrowsAsync(() => f.Writer.ChangeAsync( Credential(), "new-password", - Context(), Options(3), Now, default)); + Context(), Options(3), Now, TestContext.Current.CancellationToken)); f.History.Verify(x => x.AddAsync( It.IsAny(), diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Credentials/PasswordValidationPipelineTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Credentials/PasswordValidationPipelineTests.cs index e6cab3bf..e2f0bb94 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Credentials/PasswordValidationPipelineTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Credentials/PasswordValidationPipelineTests.cs @@ -98,7 +98,7 @@ public async Task AbsoluteLimit_CannotBeBypassedByCustomStrengthPolicy() var result = await pipeline.ValidateAsync( CreateRequest(new string('a', 257)), - CreateOptions()); + CreateOptions(), ct: TestContext.Current.CancellationToken); Assert.False(result.Succeeded); @@ -119,7 +119,7 @@ public async Task NewCredential_DoesNotRequireHistory() var result = await pipeline.ValidateAsync( CreateRequest("NewPassword123!"), - CreateOptions(rememberLast: 5)); + CreateOptions(rememberLast: 5), ct: TestContext.Current.CancellationToken); Assert.True(result.Succeeded); Assert.Equal(1, strength.CallCount); @@ -142,7 +142,7 @@ public async Task ExistingCredential_AlwaysValidatesHistory_WhenRememberLastIsZe var result = await pipeline.ValidateAsync( CreateRequest("CurrentPassword123!", key), - CreateOptions(rememberLast: 0)); + CreateOptions(rememberLast: 0), ct: TestContext.Current.CancellationToken); Assert.False(result.Succeeded); @@ -164,7 +164,7 @@ public async Task ExistingCredential_PassesRememberLastToHistoryValidator() var result = await pipeline.ValidateAsync( CreateRequest("NewPassword123!", key), - CreateOptions(rememberLast: 5)); + CreateOptions(rememberLast: 5), ct: TestContext.Current.CancellationToken); Assert.True(result.Succeeded); Assert.Equal(1, history.CallCount); @@ -189,7 +189,7 @@ public async Task ExistingCredential_PropagatesHistoryValidationFailure() var result = await pipeline.ValidateAsync( CreateRequest("PreviouslyUsed123!", key), - CreateOptions(rememberLast: 3)); + CreateOptions(rememberLast: 3), ct: TestContext.Current.CancellationToken); Assert.False(result.Succeeded); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Credentials/ResetPasswordTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Credentials/ResetPasswordTests.cs index c2b92ed7..abd0ee70 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Credentials/ResetPasswordTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Credentials/ResetPasswordTests.cs @@ -24,7 +24,7 @@ public async Task Begin_reset_with_token_should_notify_without_exposing_token() var result = await service.BeginResetAsync( BeginContext(), - BeginRequest(ResetCodeType.Token)); + BeginRequest(ResetCodeType.Token), ct: TestContext.Current.CancellationToken); var notification = notifier.Notifications.Should() .ContainSingle().Subject; @@ -67,7 +67,7 @@ public async Task Begin_reset_with_code_should_notify_numeric_code() await runtime.GetCredentialManagementService().BeginResetAsync( BeginContext(), - BeginRequest(ResetCodeType.Code)); + BeginRequest(ResetCodeType.Code), ct: TestContext.Current.CancellationToken); var notification = notifier.Notifications.Should() .ContainSingle().Subject; @@ -86,7 +86,7 @@ public async Task Begin_reset_for_unknown_user_should_not_notify() BeginContext(), BeginRequest( ResetCodeType.Token, - "unknown@test.com")); + "unknown@test.com"), ct: TestContext.Current.CancellationToken); result.Should().NotBeNull(); result.ExpiresAt.Should().BeAfter(runtime.Clock.UtcNow); @@ -104,7 +104,7 @@ public async Task Reset_password_with_valid_token_should_succeed() var result = await service.CompleteResetAsync( CompleteContext(), - CompleteRequest(token, "newpass123")); + CompleteRequest(token, "newpass123"), ct: TestContext.Current.CancellationToken); result.IsSuccess.Should().BeTrue(); } @@ -124,7 +124,7 @@ public async Task Reset_password_with_same_password_should_not_consume_token() // reset must return a generic success response. var samePassword = await service.CompleteResetAsync( CompleteContext(), - CompleteRequest(token, "admin")); + CompleteRequest(token, "admin"), ct: TestContext.Current.CancellationToken); samePassword.IsSuccess.Should().BeTrue(); @@ -134,7 +134,7 @@ public async Task Reset_password_with_same_password_should_not_consume_token() // A policy failure must not consume the reset token. var retry = await service.CompleteResetAsync( CompleteContext(), - CompleteRequest(token, "DifferentPassword123!")); + CompleteRequest(token, "DifferentPassword123!"), ct: TestContext.Current.CancellationToken); retry.IsSuccess.Should().BeTrue(); @@ -144,7 +144,7 @@ await AssertPasswordAsync( // A successfully consumed token must not be reusable. var replay = await service.CompleteResetAsync( CompleteContext(), - CompleteRequest(token, "AnotherPassword123!")); + CompleteRequest(token, "AnotherPassword123!"), ct: TestContext.Current.CancellationToken); replay.IsSuccess.Should().BeTrue(); @@ -174,7 +174,7 @@ public async Task Reset_token_should_lock_after_max_attempts() { var result = await service.CompleteResetAsync( CompleteContext(), - CompleteRequest(wrongToken, "newpass123")); + CompleteRequest(wrongToken, "newpass123"), ct: TestContext.Current.CancellationToken); result.IsSuccess.Should().BeTrue(); @@ -184,7 +184,7 @@ public async Task Reset_token_should_lock_after_max_attempts() // Even the correct token must no longer work. var afterLockout = await service.CompleteResetAsync( CompleteContext(), - CompleteRequest(token, "newpass123")); + CompleteRequest(token, "newpass123"), ct: TestContext.Current.CancellationToken); afterLockout.IsSuccess.Should().BeTrue(); @@ -205,7 +205,7 @@ public async Task Reset_token_should_be_single_use() var first = await service.CompleteResetAsync( CompleteContext(), - CompleteRequest(token, "newpass123")); + CompleteRequest(token, "newpass123"), ct: TestContext.Current.CancellationToken); first.IsSuccess.Should().BeTrue(); @@ -213,7 +213,7 @@ public async Task Reset_token_should_be_single_use() var replay = await service.CompleteResetAsync( CompleteContext(), - CompleteRequest(token, "anotherpass")); + CompleteRequest(token, "anotherpass"), ct: TestContext.Current.CancellationToken); replay.IsSuccess.Should().BeTrue(); @@ -241,7 +241,7 @@ public async Task Reset_token_should_not_change_password_if_expired() var result = await service.CompleteResetAsync( CompleteContext(), - CompleteRequest(token, "newpass123")); + CompleteRequest(token, "newpass123"), ct: TestContext.Current.CancellationToken); // Generic anonymous response, not proof of a password change. result.IsSuccess.Should().BeTrue(); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Credentials/Store/PasswordCredentialStoreContractTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Credentials/Store/PasswordCredentialStoreContractTests.cs index c5901281..b74ac3e2 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Credentials/Store/PasswordCredentialStoreContractTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Credentials/Store/PasswordCredentialStoreContractTests.cs @@ -56,13 +56,13 @@ public async Task AddAsync_PersistsCredential() var user = UserKey.New(); var credential = CreateCredential(TenantA, user, "credential-1"); - await store.AddAsync(credential); + await store.AddAsync(credential, ct: TestContext.Current.CancellationToken); var key = new CredentialKey( credential.Tenant, credential.Id); - var persisted = await store.GetAsync(key); + var persisted = await store.GetAsync(key, ct: TestContext.Current.CancellationToken); persisted.Should().NotBeNull(); persisted!.Id.Should().Be(credential.Id); @@ -82,11 +82,11 @@ public async Task ExistsAsync_WhenCredentialExists_ReturnsTrue() UserKey.New(), "credential-1"); - await store.AddAsync(credential); + await store.AddAsync(credential, ct: TestContext.Current.CancellationToken); var key = new CredentialKey(TenantA, credential.Id); - (await store.ExistsAsync(key)).Should().BeTrue(); + (await store.ExistsAsync(key, ct: TestContext.Current.CancellationToken)).Should().BeTrue(); } [Fact] @@ -102,7 +102,7 @@ public async Task ExistsAsync_WhenCredentialDoesNotExist_ReturnsFalse() var key = new CredentialKey(TenantA, credential.Id); - (await store.ExistsAsync(key)).Should().BeFalse(); + (await store.ExistsAsync(key, ct: TestContext.Current.CancellationToken)).Should().BeFalse(); } [Fact] @@ -117,7 +117,7 @@ public async Task GetAsync_WhenCredentialDoesNotExist_ReturnsNull() "missing"); var result = await store.GetAsync( - new CredentialKey(TenantA, credential.Id)); + new CredentialKey(TenantA, credential.Id), ct: TestContext.Current.CancellationToken); result.Should().BeNull(); } @@ -140,7 +140,7 @@ public async Task AddAsync_WhenUserAlreadyHasActivePasswordCredential_ThrowsConf var second = CreateCredential( TenantA, user, "second"); - await store.AddAsync(first); + await store.AddAsync(first, ct: TestContext.Current.CancellationToken); var act = () => store.AddAsync(second); @@ -159,13 +159,13 @@ public async Task AddAsync_AfterSoftDeletingExistingCredential_AllowsReplacement var first = CreateCredential( TenantA, user, "first"); - await store.AddAsync(first); + await store.AddAsync(first, ct: TestContext.Current.CancellationToken); await store.DeleteAsync( new CredentialKey(TenantA, first.Id), first.Version, DeleteMode.Soft, - Now); + Now, ct: TestContext.Current.CancellationToken); var replacement = CreateCredential( TenantA, user, "replacement"); @@ -174,7 +174,7 @@ await store.DeleteAsync( await act.Should().NotThrowAsync(); - var active = await store.GetByUserAsync(user); + var active = await store.GetByUserAsync(user, ct: TestContext.Current.CancellationToken); active.Should().ContainSingle(); active.Single().Id.Should().Be(replacement.Id); @@ -195,7 +195,7 @@ public async Task SaveAsync_WhenExpectedVersionMatches_PersistsChanges() UserKey.New(), "original"); - await store.AddAsync(credential); + await store.AddAsync(credential, ct: TestContext.Current.CancellationToken); var changedAt = Now.AddMinutes(10); @@ -205,10 +205,10 @@ public async Task SaveAsync_WhenExpectedVersionMatches_PersistsChanges() await store.SaveAsync( credential, - expectedVersion: 0); + expectedVersion: 0, ct: TestContext.Current.CancellationToken); var persisted = await store.GetAsync( - new CredentialKey(TenantA, credential.Id)); + new CredentialKey(TenantA, credential.Id), ct: TestContext.Current.CancellationToken); persisted.Should().NotBeNull(); @@ -251,14 +251,14 @@ public async Task SaveAsync_WhenExpectedVersionIsStale_ThrowsConcurrency() UserKey.New(), "credential"); - await store.AddAsync(credential); + await store.AddAsync(credential, ct: TestContext.Current.CancellationToken); var changed = credential.Revoke( Now.AddMinutes(5)); await store.SaveAsync( changed, - credential.Version); + credential.Version, ct: TestContext.Current.CancellationToken); var act = () => store.SaveAsync( changed, @@ -283,17 +283,17 @@ public async Task RevokeAsync_RevokesCredential() UserKey.New(), "credential"); - await store.AddAsync(credential); + await store.AddAsync(credential, ct: TestContext.Current.CancellationToken); var revokedAt = Now.AddMinutes(10); await store.RevokeAsync( new CredentialKey(TenantA, credential.Id), revokedAt, - credential.Version); + credential.Version, ct: TestContext.Current.CancellationToken); var persisted = await store.GetAsync( - new CredentialKey(TenantA, credential.Id)); + new CredentialKey(TenantA, credential.Id), ct: TestContext.Current.CancellationToken); persisted.Should().NotBeNull(); persisted!.Security.RevokedAt.Should().Be(revokedAt); @@ -331,12 +331,12 @@ public async Task RevokeAsync_WhenExpectedVersionIsStale_ThrowsConcurrency() UserKey.New(), "credential"); - await store.AddAsync(credential); + await store.AddAsync(credential, ct: TestContext.Current.CancellationToken); await store.RevokeAsync( new CredentialKey(TenantA, credential.Id), Now, - 0); + 0, ct: TestContext.Current.CancellationToken); var act = () => store.RevokeAsync( new CredentialKey(TenantA, credential.Id), @@ -361,15 +361,15 @@ public async Task DeleteAsync_SoftDelete_HidesCredentialFromGetByUser() var credential = CreateCredential( TenantA, user, "credential"); - await store.AddAsync(credential); + await store.AddAsync(credential, ct: TestContext.Current.CancellationToken); await store.DeleteAsync( new CredentialKey(TenantA, credential.Id), credential.Version, DeleteMode.Soft, - Now); + Now, ct: TestContext.Current.CancellationToken); - var credentials = await store.GetByUserAsync(user); + var credentials = await store.GetByUserAsync(user, ct: TestContext.Current.CancellationToken); credentials.Should().BeEmpty(); } @@ -385,16 +385,16 @@ public async Task DeleteAsync_SoftDelete_PreservesCredentialRecord() UserKey.New(), "credential"); - await store.AddAsync(credential); + await store.AddAsync(credential, ct: TestContext.Current.CancellationToken); await store.DeleteAsync( new CredentialKey(TenantA, credential.Id), 0, DeleteMode.Soft, - Now); + Now, ct: TestContext.Current.CancellationToken); var persisted = await store.GetAsync( - new CredentialKey(TenantA, credential.Id)); + new CredentialKey(TenantA, credential.Id), ct: TestContext.Current.CancellationToken); persisted.Should().NotBeNull(); persisted!.IsDeleted.Should().BeTrue(); @@ -413,7 +413,7 @@ public async Task DeleteAsync_HardDelete_RemovesCredential() UserKey.New(), "credential"); - await store.AddAsync(credential); + await store.AddAsync(credential, ct: TestContext.Current.CancellationToken); var key = new CredentialKey( TenantA, @@ -423,10 +423,10 @@ await store.DeleteAsync( key, credential.Version, DeleteMode.Hard, - Now); + Now, ct: TestContext.Current.CancellationToken); - (await store.GetAsync(key)).Should().BeNull(); - (await store.ExistsAsync(key)).Should().BeFalse(); + (await store.GetAsync(key, ct: TestContext.Current.CancellationToken)).Should().BeNull(); + (await store.ExistsAsync(key, ct: TestContext.Current.CancellationToken)).Should().BeFalse(); } [Fact] @@ -440,12 +440,12 @@ public async Task DeleteAsync_WhenExpectedVersionIsStale_ThrowsConcurrency() UserKey.New(), "credential"); - await store.AddAsync(credential); + await store.AddAsync(credential, ct: TestContext.Current.CancellationToken); await store.RevokeAsync( new CredentialKey(TenantA, credential.Id), Now, - 0); + 0, ct: TestContext.Current.CancellationToken); var act = () => store.DeleteAsync( new CredentialKey(TenantA, credential.Id), @@ -476,10 +476,10 @@ public async Task GetByUserAsync_ReturnsOnlyActiveNonDeletedCredentialsForUser() var credentialB = CreateCredential( TenantA, userB, "b"); - await store.AddAsync(credentialA); - await store.AddAsync(credentialB); + await store.AddAsync(credentialA, ct: TestContext.Current.CancellationToken); + await store.AddAsync(credentialB, ct: TestContext.Current.CancellationToken); - var result = await store.GetByUserAsync(userA); + var result = await store.GetByUserAsync(userA, ct: TestContext.Current.CancellationToken); result.Should().ContainSingle(); result.Single().Id.Should().Be(credentialA.Id); @@ -500,18 +500,18 @@ public async Task DeleteByUserAsync_SoftDelete_DeletesUsersCredentials() var credential = CreateCredential( TenantA, user, "credential"); - await store.AddAsync(credential); + await store.AddAsync(credential, ct: TestContext.Current.CancellationToken); await store.DeleteByUserAsync( user, DeleteMode.Soft, - Now); + Now, ct: TestContext.Current.CancellationToken); - var active = await store.GetByUserAsync(user); + var active = await store.GetByUserAsync(user, ct: TestContext.Current.CancellationToken); active.Should().BeEmpty(); var persisted = await store.GetAsync( - new CredentialKey(TenantA, credential.Id)); + new CredentialKey(TenantA, credential.Id), ct: TestContext.Current.CancellationToken); persisted.Should().NotBeNull(); persisted!.IsDeleted.Should().BeTrue(); @@ -530,15 +530,15 @@ public async Task DeleteByUserAsync_HardDelete_RemovesUsersCredentials() var credential = CreateCredential( TenantA, user, "credential"); - await store.AddAsync(credential); + await store.AddAsync(credential, ct: TestContext.Current.CancellationToken); await store.DeleteByUserAsync( user, DeleteMode.Hard, - Now); + Now, ct: TestContext.Current.CancellationToken); var persisted = await store.GetAsync( - new CredentialKey(TenantA, credential.Id)); + new CredentialKey(TenantA, credential.Id), ct: TestContext.Current.CancellationToken); persisted.Should().BeNull(); } @@ -558,20 +558,20 @@ public async Task DeleteByUserAsync_DoesNotAffectOtherUsers() var other = CreateCredential( TenantA, otherUser, "other"); - await store.AddAsync(target); - await store.AddAsync(other); + await store.AddAsync(target, ct: TestContext.Current.CancellationToken); + await store.AddAsync(other, ct: TestContext.Current.CancellationToken); await store.DeleteByUserAsync( targetUser, DeleteMode.Hard, - Now); + Now, ct: TestContext.Current.CancellationToken); (await store.GetAsync( - new CredentialKey(TenantA, target.Id))) + new CredentialKey(TenantA, target.Id), ct: TestContext.Current.CancellationToken)) .Should().BeNull(); (await store.GetAsync( - new CredentialKey(TenantA, other.Id))) + new CredentialKey(TenantA, other.Id), ct: TestContext.Current.CancellationToken)) .Should().NotBeNull(); } @@ -595,11 +595,11 @@ public async Task GetByUserAsync_IsTenantIsolated() var credentialB = CreateCredential( TenantB, user, "b"); - await storeA.AddAsync(credentialA); - await storeB.AddAsync(credentialB); + await storeA.AddAsync(credentialA, ct: TestContext.Current.CancellationToken); + await storeB.AddAsync(credentialB, ct: TestContext.Current.CancellationToken); - var fromA = await storeA.GetByUserAsync(user); - var fromB = await storeB.GetByUserAsync(user); + var fromA = await storeA.GetByUserAsync(user, ct: TestContext.Current.CancellationToken); + var fromB = await storeB.GetByUserAsync(user, ct: TestContext.Current.CancellationToken); fromA.Should().ContainSingle(); fromA.Single().Id.Should().Be(credentialA.Id); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Credentials/Store/PasswordHistoryStoreContractTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Credentials/Store/PasswordHistoryStoreContractTests.cs index a42a4918..f023807e 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Credentials/Store/PasswordHistoryStoreContractTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Credentials/Store/PasswordHistoryStoreContractTests.cs @@ -47,9 +47,9 @@ public async Task AddAsync_PersistsAllFields() var key = Key(TenantA); var entry = Entry(key, UserKey.New()); - await store.AddAsync(entry); + await store.AddAsync(entry, ct: TestContext.Current.CancellationToken); - var stored = (await store.GetRecentAsync(key, 10)) + var stored = (await store.GetRecentAsync(key, 10, ct: TestContext.Current.CancellationToken)) .Should().ContainSingle().Subject; stored.Id.Should().Be(entry.Id); @@ -72,11 +72,11 @@ public async Task GetRecentAsync_ReturnsNewestFirst() var newest = Entry(key, user, 3); var middle = Entry(key, user, 2); - await store.AddAsync(newest); - await store.AddAsync(oldest); - await store.AddAsync(middle); + await store.AddAsync(newest, ct: TestContext.Current.CancellationToken); + await store.AddAsync(oldest, ct: TestContext.Current.CancellationToken); + await store.AddAsync(middle, ct: TestContext.Current.CancellationToken); - var result = await store.GetRecentAsync(key, 2); + var result = await store.GetRecentAsync(key, 2, ct: TestContext.Current.CancellationToken); result.Select(x => x.Id) .Should().Equal(newest.Id, middle.Id); @@ -95,10 +95,10 @@ public async Task GetRecentAsync_EqualDates_UsesIdDescending() var high = Entry(key, user, id: Guid.Parse( "00000000-0000-0000-0000-000000000002")); - await store.AddAsync(low); - await store.AddAsync(high); + await store.AddAsync(low, ct: TestContext.Current.CancellationToken); + await store.AddAsync(high, ct: TestContext.Current.CancellationToken); - var result = await store.GetRecentAsync(key, 2); + var result = await store.GetRecentAsync(key, 2, ct: TestContext.Current.CancellationToken); result.Select(x => x.Id) .Should().Equal(high.Id, low.Id); @@ -111,9 +111,9 @@ public async Task GetRecentAsync_Zero_ReturnsEmpty() var store = db.CreateStore(TenantA); var key = Key(TenantA); - await store.AddAsync(Entry(key, UserKey.New())); + await store.AddAsync(Entry(key, UserKey.New()), ct: TestContext.Current.CancellationToken); - (await store.GetRecentAsync(key, 0)) + (await store.GetRecentAsync(key, 0, ct: TestContext.Current.CancellationToken)) .Should().BeEmpty(); } @@ -124,9 +124,9 @@ public async Task GetRecentAsync_CountGreaterThanAvailable_ReturnsAll() var store = db.CreateStore(TenantA); var key = Key(TenantA); - await store.AddAsync(Entry(key, UserKey.New())); + await store.AddAsync(Entry(key, UserKey.New()), ct: TestContext.Current.CancellationToken); - (await store.GetRecentAsync(key, 100)) + (await store.GetRecentAsync(key, 100, ct: TestContext.Current.CancellationToken)) .Should().ContainSingle(); } @@ -148,7 +148,7 @@ public async Task GetRecentAsync_MissingCredential_ReturnsEmpty() await using var db = await CreateDatabaseAsync(); var store = db.CreateStore(TenantA); - (await store.GetRecentAsync(Key(TenantA), 5)) + (await store.GetRecentAsync(Key(TenantA), 5, ct: TestContext.Current.CancellationToken)) .Should().BeEmpty(); } @@ -162,10 +162,10 @@ public async Task GetRecentAsync_DoesNotMixCredentials() var user = UserKey.New(); var a = Entry(keyA, user); - await store.AddAsync(a); - await store.AddAsync(Entry(keyB, user)); + await store.AddAsync(a, ct: TestContext.Current.CancellationToken); + await store.AddAsync(Entry(keyB, user), ct: TestContext.Current.CancellationToken); - var result = await store.GetRecentAsync(keyA, 10); + var result = await store.GetRecentAsync(keyA, 10, ct: TestContext.Current.CancellationToken); result.Should().ContainSingle(); result.Single().Id.Should().Be(a.Id); @@ -189,11 +189,11 @@ public async Task PruneAsync_KeepsExactlyNewestEntries( .ToArray(); foreach (var entry in entries.Reverse()) - await store.AddAsync(entry); + await store.AddAsync(entry, ct: TestContext.Current.CancellationToken); - await store.PruneAsync(key, keepLast); + await store.PruneAsync(key, keepLast, ct: TestContext.Current.CancellationToken); - var result = await store.GetRecentAsync(key, 20); + var result = await store.GetRecentAsync(key, 20, ct: TestContext.Current.CancellationToken); var expected = entries .OrderByDescending(x => x.ReplacedAt) .Take(keepLast) @@ -211,14 +211,14 @@ public async Task PruneAsync_DoesNotAffectOtherCredentials() var keyB = Key(TenantA); var user = UserKey.New(); - await store.AddAsync(Entry(keyA, user)); + await store.AddAsync(Entry(keyA, user), ct: TestContext.Current.CancellationToken); var other = Entry(keyB, user); - await store.AddAsync(other); + await store.AddAsync(other, ct: TestContext.Current.CancellationToken); - await store.PruneAsync(keyA, 0); + await store.PruneAsync(keyA, 0, ct: TestContext.Current.CancellationToken); - (await store.GetRecentAsync(keyA, 10)).Should().BeEmpty(); - (await store.GetRecentAsync(keyB, 10)) + (await store.GetRecentAsync(keyA, 10, ct: TestContext.Current.CancellationToken)).Should().BeEmpty(); + (await store.GetRecentAsync(keyB, 10, ct: TestContext.Current.CancellationToken)) .Should().ContainSingle(x => x.Id == other.Id); } @@ -242,14 +242,14 @@ public async Task PruneAsync_IsIdempotent() var key = Key(TenantA); var user = UserKey.New(); - await store.AddAsync(Entry(key, user, 1)); - await store.AddAsync(Entry(key, user, 2)); + await store.AddAsync(Entry(key, user, 1), ct: TestContext.Current.CancellationToken); + await store.AddAsync(Entry(key, user, 2), ct: TestContext.Current.CancellationToken); - await store.PruneAsync(key, 1); - var first = await store.GetRecentAsync(key, 10); + await store.PruneAsync(key, 1, ct: TestContext.Current.CancellationToken); + var first = await store.GetRecentAsync(key, 10, ct: TestContext.Current.CancellationToken); - await store.PruneAsync(key, 1); - var second = await store.GetRecentAsync(key, 10); + await store.PruneAsync(key, 1, ct: TestContext.Current.CancellationToken); + var second = await store.GetRecentAsync(key, 10, ct: TestContext.Current.CancellationToken); second.Should().BeEquivalentTo(first); } @@ -263,15 +263,15 @@ public async Task DeleteByCredentialAsync_RemovesOnlyTarget() var keyB = Key(TenantA); var user = UserKey.New(); - await store.AddAsync(Entry(keyA, user)); - await store.AddAsync(Entry(keyA, user)); + await store.AddAsync(Entry(keyA, user), ct: TestContext.Current.CancellationToken); + await store.AddAsync(Entry(keyA, user), ct: TestContext.Current.CancellationToken); var other = Entry(keyB, user); - await store.AddAsync(other); + await store.AddAsync(other, ct: TestContext.Current.CancellationToken); - await store.DeleteByCredentialAsync(keyA); + await store.DeleteByCredentialAsync(keyA, ct: TestContext.Current.CancellationToken); - (await store.GetRecentAsync(keyA, 10)).Should().BeEmpty(); - (await store.GetRecentAsync(keyB, 10)) + (await store.GetRecentAsync(keyA, 10, ct: TestContext.Current.CancellationToken)).Should().BeEmpty(); + (await store.GetRecentAsync(keyB, 10, ct: TestContext.Current.CancellationToken)) .Should().ContainSingle(x => x.Id == other.Id); } @@ -283,10 +283,10 @@ public async Task DeleteByCredentialAsync_Missing_IsIdempotent() var key = Key(TenantA); - await store.DeleteByCredentialAsync(key); - await store.DeleteByCredentialAsync(key); + await store.DeleteByCredentialAsync(key, ct: TestContext.Current.CancellationToken); + await store.DeleteByCredentialAsync(key, ct: TestContext.Current.CancellationToken); - (await store.GetRecentAsync(key, 10)).Should().BeEmpty(); + (await store.GetRecentAsync(key, 10, ct: TestContext.Current.CancellationToken)).Should().BeEmpty(); } [Fact] @@ -300,16 +300,16 @@ public async Task DeleteByUserAsync_RemovesAllUsersCredentials() var keyA2 = Key(TenantA); var keyB = Key(TenantA); - await store.AddAsync(Entry(keyA1, userA)); - await store.AddAsync(Entry(keyA2, userA)); + await store.AddAsync(Entry(keyA1, userA), ct: TestContext.Current.CancellationToken); + await store.AddAsync(Entry(keyA2, userA), ct: TestContext.Current.CancellationToken); var other = Entry(keyB, userB); - await store.AddAsync(other); + await store.AddAsync(other, ct: TestContext.Current.CancellationToken); - await store.DeleteByUserAsync(userA); + await store.DeleteByUserAsync(userA, ct: TestContext.Current.CancellationToken); - (await store.GetRecentAsync(keyA1, 10)).Should().BeEmpty(); - (await store.GetRecentAsync(keyA2, 10)).Should().BeEmpty(); - (await store.GetRecentAsync(keyB, 10)) + (await store.GetRecentAsync(keyA1, 10, ct: TestContext.Current.CancellationToken)).Should().BeEmpty(); + (await store.GetRecentAsync(keyA2, 10, ct: TestContext.Current.CancellationToken)).Should().BeEmpty(); + (await store.GetRecentAsync(keyB, 10, ct: TestContext.Current.CancellationToken)) .Should().ContainSingle(x => x.Id == other.Id); } @@ -321,8 +321,8 @@ public async Task DeleteByUserAsync_Missing_IsIdempotent() var user = UserKey.New(); - await store.DeleteByUserAsync(user); - await store.DeleteByUserAsync(user); + await store.DeleteByUserAsync(user, ct: TestContext.Current.CancellationToken); + await store.DeleteByUserAsync(user, ct: TestContext.Current.CancellationToken); } [Fact] @@ -337,18 +337,18 @@ public async Task TenantIsolation_GetRecentAndPrune() var keyA = new CredentialKey(TenantA, credentialId); var keyB = new CredentialKey(TenantB, credentialId); - await storeA.AddAsync(Entry(keyA, user)); - await storeB.AddAsync(Entry(keyB, user)); + await storeA.AddAsync(Entry(keyA, user), ct: TestContext.Current.CancellationToken); + await storeB.AddAsync(Entry(keyB, user), ct: TestContext.Current.CancellationToken); - (await storeA.GetRecentAsync(keyA, 10)) + (await storeA.GetRecentAsync(keyA, 10, ct: TestContext.Current.CancellationToken)) .Should().ContainSingle(); - (await storeB.GetRecentAsync(keyB, 10)) + (await storeB.GetRecentAsync(keyB, 10, ct: TestContext.Current.CancellationToken)) .Should().ContainSingle(); - await storeA.PruneAsync(keyA, 0); + await storeA.PruneAsync(keyA, 0, ct: TestContext.Current.CancellationToken); - (await storeA.GetRecentAsync(keyA, 10)).Should().BeEmpty(); - (await storeB.GetRecentAsync(keyB, 10)) + (await storeA.GetRecentAsync(keyA, 10, ct: TestContext.Current.CancellationToken)).Should().BeEmpty(); + (await storeB.GetRecentAsync(keyB, 10, ct: TestContext.Current.CancellationToken)) .Should().ContainSingle(); } @@ -364,13 +364,13 @@ public async Task TenantIsolation_DeleteByUser() var keyA = new CredentialKey(TenantA, credentialId); var keyB = new CredentialKey(TenantB, credentialId); - await storeA.AddAsync(Entry(keyA, user)); - await storeB.AddAsync(Entry(keyB, user)); + await storeA.AddAsync(Entry(keyA, user), ct: TestContext.Current.CancellationToken); + await storeB.AddAsync(Entry(keyB, user), ct: TestContext.Current.CancellationToken); - await storeA.DeleteByUserAsync(user); + await storeA.DeleteByUserAsync(user, ct: TestContext.Current.CancellationToken); - (await storeA.GetRecentAsync(keyA, 10)).Should().BeEmpty(); - (await storeB.GetRecentAsync(keyB, 10)) + (await storeA.GetRecentAsync(keyA, 10, ct: TestContext.Current.CancellationToken)).Should().BeEmpty(); + (await storeB.GetRecentAsync(keyB, 10, ct: TestContext.Current.CancellationToken)) .Should().ContainSingle(); } @@ -456,7 +456,7 @@ public async Task AddAsync_DuplicateId_Throws() var key = Key(TenantA); var entry = Entry(key, UserKey.New()); - await store.AddAsync(entry); + await store.AddAsync(entry, ct: TestContext.Current.CancellationToken); var act = () => store.AddAsync(entry); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Credentials/UAuthPasswordHistoryValidatorTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Credentials/UAuthPasswordHistoryValidatorTests.cs index a1c1631e..128150bf 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Credentials/UAuthPasswordHistoryValidatorTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Credentials/UAuthPasswordHistoryValidatorTests.cs @@ -109,7 +109,7 @@ public async Task CurrentPassword_IsAlwaysRejected( var f = Create(); var result = await f.Validator.ValidateAsync( - f.Key, "CurrentPassword123!", rememberLast); + f.Key, "CurrentPassword123!", rememberLast, ct: TestContext.Current.CancellationToken); result.Succeeded.Should().BeFalse(); result.Violations.Should().ContainSingle( @@ -126,7 +126,7 @@ public async Task RememberLastZero_AllowsDifferentPassword_WithoutHistoryLookup( var f = Create(); var result = await f.Validator.ValidateAsync( - f.Key, "DifferentPassword123!", 0); + f.Key, "DifferentPassword123!", 0, ct: TestContext.Current.CancellationToken); result.Succeeded.Should().BeTrue(); @@ -151,7 +151,7 @@ public async Task HistoricalPassword_IsRejected() }); var result = await f.Validator.ValidateAsync( - f.Key, "Previous2!", 3); + f.Key, "Previous2!", 3, ct: TestContext.Current.CancellationToken); result.Succeeded.Should().BeFalse(); result.Violations.Should().ContainSingle( @@ -173,7 +173,7 @@ public async Task NewPassword_NotPresentInHistory_IsAllowed() }); var result = await f.Validator.ValidateAsync( - f.Key, "EntirelyNewPassword123!", 2); + f.Key, "EntirelyNewPassword123!", 2, ct: TestContext.Current.CancellationToken); result.Succeeded.Should().BeTrue(); } @@ -189,7 +189,7 @@ public async Task EmptyHistory_AllowsDifferentPassword() .ReturnsAsync(Array.Empty()); var result = await f.Validator.ValidateAsync( - f.Key, "NewPassword123!", 5); + f.Key, "NewPassword123!", 5, ct: TestContext.Current.CancellationToken); result.Succeeded.Should().BeTrue(); } @@ -205,7 +205,7 @@ public async Task HistoryLookup_UsesRequestedLimit() .ReturnsAsync(Array.Empty()); await f.Validator.ValidateAsync( - f.Key, "NewPassword123!", 7); + f.Key, "NewPassword123!", 7, ct: TestContext.Current.CancellationToken); f.HistoryStore.Verify( x => x.GetRecentAsync( @@ -268,7 +268,7 @@ public async Task HistoryVerification_StopsAtFirstMatch() }); var result = await f.Validator.ValidateAsync( - f.Key, "Match123!", 3); + f.Key, "Match123!", 3, ct: TestContext.Current.CancellationToken); result.Succeeded.Should().BeFalse(); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreAuthenticationStoreTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreAuthenticationStoreTests.cs index 6239983e..313366d1 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreAuthenticationStoreTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreAuthenticationStoreTests.cs @@ -30,9 +30,9 @@ public async Task Add_And_Get_Should_Work() tenant, userKey); - await store.AddAsync(state); + await store.AddAsync(state, ct: TestContext.Current.CancellationToken); - var result = await store.GetAsync(userKey, AuthenticationSecurityScope.Account, null); + var result = await store.GetAsync(userKey, AuthenticationSecurityScope.Account, null, ct: TestContext.Current.CancellationToken); Assert.NotNull(result); Assert.Equal(state.Id, result!.Id); @@ -51,13 +51,13 @@ public async Task Update_With_RegisterFailure_Should_Increment_Version() await using (var db1 = CreateDb(connection)) { var store = new EfCoreAuthenticationSecurityStateStore(db1, new TenantExecutionContext(tenant)); - await store.AddAsync(state); + await store.AddAsync(state, ct: TestContext.Current.CancellationToken); } await using (var db2 = CreateDb(connection)) { var store = new EfCoreAuthenticationSecurityStateStore(db2, new TenantExecutionContext(tenant)); - var existing = await store.GetAsync(userKey, AuthenticationSecurityScope.Account, null); + var existing = await store.GetAsync(userKey, AuthenticationSecurityScope.Account, null, ct: TestContext.Current.CancellationToken); var updated = existing!.RegisterFailure( DateTimeOffset.UtcNow, @@ -65,13 +65,13 @@ public async Task Update_With_RegisterFailure_Should_Increment_Version() failureWindow: TimeSpan.FromMinutes(5), lockoutDuration: TimeSpan.FromMinutes(5)); - await store.UpdateAsync(updated, expectedVersion: 0); + await store.UpdateAsync(updated, expectedVersion: 0, ct: TestContext.Current.CancellationToken); } await using (var db3 = CreateDb(connection)) { var store = new EfCoreAuthenticationSecurityStateStore(db3, new TenantExecutionContext(tenant)); - var result = await store.GetAsync(userKey, AuthenticationSecurityScope.Account, null); + var result = await store.GetAsync(userKey, AuthenticationSecurityScope.Account, null, ct: TestContext.Current.CancellationToken); Assert.Equal(1, result!.SecurityVersion); Assert.Equal(1, result.FailedAttempts); @@ -89,10 +89,10 @@ public async Task Update_With_Wrong_Version_Should_Throw() var userKey = UserKey.FromGuid(Guid.NewGuid()); var state = AuthenticationSecurityState.CreateAccount(tenant, userKey); - await store.AddAsync(state); + await store.AddAsync(state, ct: TestContext.Current.CancellationToken); var updated = state.RegisterFailure(DateTimeOffset.UtcNow, 3, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5)); - await Assert.ThrowsAsync(() => store.UpdateAsync(updated, expectedVersion: 999)); + await Assert.ThrowsAsync(() => store.UpdateAsync(updated, expectedVersion: 999, ct: TestContext.Current.CancellationToken)); } [Fact] @@ -109,21 +109,21 @@ public async Task RegisterSuccess_Should_Clear_Failures() await using (var db1 = CreateDb(connection)) { var store = new EfCoreAuthenticationSecurityStateStore(db1, new TenantExecutionContext(tenant)); - await store.AddAsync(state); + await store.AddAsync(state, ct: TestContext.Current.CancellationToken); } await using (var db2 = CreateDb(connection)) { var store = new EfCoreAuthenticationSecurityStateStore(db2, new TenantExecutionContext(tenant)); - var existing = await store.GetAsync(userKey, AuthenticationSecurityScope.Account, null); + var existing = await store.GetAsync(userKey, AuthenticationSecurityScope.Account, null, ct: TestContext.Current.CancellationToken); var updated = existing!.RegisterSuccess(); - await store.UpdateAsync(updated, expectedVersion: 1); + await store.UpdateAsync(updated, expectedVersion: 1, ct: TestContext.Current.CancellationToken); } await using (var db3 = CreateDb(connection)) { var store = new EfCoreAuthenticationSecurityStateStore(db3, new TenantExecutionContext(tenant)); - var result = await store.GetAsync(userKey, AuthenticationSecurityScope.Account, null); + var result = await store.GetAsync(userKey, AuthenticationSecurityScope.Account, null, ct: TestContext.Current.CancellationToken); Assert.Equal(0, result!.FailedAttempts); Assert.Null(result.LockedUntil); @@ -142,7 +142,7 @@ public async Task BeginReset_And_Consume_Should_Work() await using (var db1 = CreateDb(connection)) { var store = new EfCoreAuthenticationSecurityStateStore(db1, new TenantExecutionContext(tenant)); - await store.AddAsync(state); + await store.AddAsync(state, ct: TestContext.Current.CancellationToken); } DateTimeOffset now = DateTimeOffset.UtcNow; @@ -150,23 +150,23 @@ public async Task BeginReset_And_Consume_Should_Work() await using (var db2 = CreateDb(connection)) { var store = new EfCoreAuthenticationSecurityStateStore(db2, new TenantExecutionContext(tenant)); - var existing = await store.GetAsync(userKey, AuthenticationSecurityScope.Account, null); + var existing = await store.GetAsync(userKey, AuthenticationSecurityScope.Account, null, ct: TestContext.Current.CancellationToken); var updated = existing!.BeginReset("hash", now, TimeSpan.FromMinutes(10)); - await store.UpdateAsync(updated, expectedVersion: 0); + await store.UpdateAsync(updated, expectedVersion: 0, ct: TestContext.Current.CancellationToken); } await using (var db3 = CreateDb(connection)) { var store = new EfCoreAuthenticationSecurityStateStore(db3, new TenantExecutionContext(tenant)); - var existing = await store.GetAsync(userKey, AuthenticationSecurityScope.Account, null); + var existing = await store.GetAsync(userKey, AuthenticationSecurityScope.Account, null, ct: TestContext.Current.CancellationToken); var consumed = existing!.ConsumeReset(DateTimeOffset.UtcNow); - await store.UpdateAsync(consumed, expectedVersion: 1); + await store.UpdateAsync(consumed, expectedVersion: 1, ct: TestContext.Current.CancellationToken); } await using (var db4 = CreateDb(connection)) { var store = new EfCoreAuthenticationSecurityStateStore(db4, new TenantExecutionContext(tenant)); - var result = await store.GetAsync(userKey, AuthenticationSecurityScope.Account, null); + var result = await store.GetAsync(userKey, AuthenticationSecurityScope.Account, null, ct: TestContext.Current.CancellationToken); Assert.NotNull(result!.ResetConsumedAt); } } @@ -184,11 +184,11 @@ public async Task Delete_Should_Work() var state = AuthenticationSecurityState.CreateAccount(tenant, userKey); - await store.AddAsync(state); + await store.AddAsync(state, ct: TestContext.Current.CancellationToken); - await store.DeleteAsync(userKey, AuthenticationSecurityScope.Account, null); + await store.DeleteAsync(userKey, AuthenticationSecurityScope.Account, null, ct: TestContext.Current.CancellationToken); - var result = await store.GetAsync(userKey, AuthenticationSecurityScope.Account, null); + var result = await store.GetAsync(userKey, AuthenticationSecurityScope.Account, null, ct: TestContext.Current.CancellationToken); Assert.Null(result); } @@ -207,8 +207,8 @@ public async Task Should_Not_See_Data_From_Other_Tenant() var userKey = UserKey.FromGuid(Guid.NewGuid()); var state = AuthenticationSecurityState.CreateAccount(tenant1, userKey); - await store1.AddAsync(state); - var result = await store2.GetAsync(userKey, AuthenticationSecurityScope.Account, null); + await store1.AddAsync(state, ct: TestContext.Current.CancellationToken); + var result = await store2.GetAsync(userKey, AuthenticationSecurityScope.Account, null, ct: TestContext.Current.CancellationToken); Assert.Null(result); } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreCredentialAtomicityTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreCredentialAtomicityTests.cs index b06267a9..b695ae8d 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreCredentialAtomicityTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreCredentialAtomicityTests.cs @@ -49,7 +49,7 @@ public async Task ChangeSecret_WhenSessionRevocationFails_ShouldRollbackCredenti db, new TenantExecutionContext(tenant)); - await store.AddAsync(credential); + await store.AddAsync(credential, ct: TestContext.Current.CancellationToken); } // Act: Change password, then simulate a session failure. @@ -60,7 +60,7 @@ public async Task ChangeSecret_WhenSessionRevocationFails_ShouldRollbackCredenti new TenantExecutionContext(tenant)); var existing = await store.GetAsync( - new CredentialKey(tenant, credential.Id)); + new CredentialKey(tenant, credential.Id), ct: TestContext.Current.CancellationToken); Assert.NotNull(existing); @@ -68,7 +68,7 @@ await Assert.ThrowsAsync( async () => { await using var transaction = - await db.Database.BeginTransactionAsync(); + await db.Database.BeginTransactionAsync(cancellationToken: TestContext.Current.CancellationToken); var updated = existing.ChangeSecret( hasher.Hash("new-password"), @@ -76,7 +76,7 @@ await Assert.ThrowsAsync( await store.SaveAsync( updated, - existing.Version); + existing.Version, ct: TestContext.Current.CancellationToken); // Simulate a failure during session revocation. throw new InvalidOperationException( @@ -92,7 +92,7 @@ await store.SaveAsync( new TenantExecutionContext(tenant)); var persisted = await store.GetAsync( - new CredentialKey(tenant, credential.Id)); + new CredentialKey(tenant, credential.Id), ct: TestContext.Current.CancellationToken); Assert.NotNull(persisted); @@ -135,7 +135,7 @@ public async Task AtomicExecutor_WhenOperationSucceeds_ShouldCommitCredential() db, new TenantExecutionContext(tenant)); - await store.AddAsync(credential); + await store.AddAsync(credential, ct: TestContext.Current.CancellationToken); } await using (var db = CreateDb(connection)) @@ -173,7 +173,7 @@ await store.SaveAsync( ct); Assert.NotNull(db.Database.CurrentTransaction); - }); + }, ct: TestContext.Current.CancellationToken); } await using (var db = CreateDb(connection)) @@ -183,7 +183,7 @@ await store.SaveAsync( new TenantExecutionContext(tenant)); var persisted = await store.GetAsync( - new CredentialKey(tenant, credential.Id)); + new CredentialKey(tenant, credential.Id), ct: TestContext.Current.CancellationToken); Assert.NotNull(persisted); Assert.True( @@ -217,7 +217,7 @@ public async Task AtomicExecutor_WhenOperationFails_ShouldRollbackCredential() db, new TenantExecutionContext(tenant)); - await store.AddAsync(credential); + await store.AddAsync(credential, ct: TestContext.Current.CancellationToken); } await using (var db = CreateDb(connection)) @@ -258,7 +258,7 @@ await store.SaveAsync( throw new InvalidOperationException( "Simulated session revocation failure"); - })); + }, ct: TestContext.Current.CancellationToken)); } await using (var db = CreateDb(connection)) @@ -268,7 +268,7 @@ await store.SaveAsync( new TenantExecutionContext(tenant)); var persisted = await store.GetAsync( - new CredentialKey(tenant, credential.Id)); + new CredentialKey(tenant, credential.Id), ct: TestContext.Current.CancellationToken); Assert.NotNull(persisted); @@ -343,14 +343,14 @@ public async Task AtomicExecutor_WhenSessionOperationFails_ShouldRollbackBothSto db, new TenantExecutionContext(tenant)); - await credentialStore.AddAsync(credential); + await credentialStore.AddAsync(credential, ct: TestContext.Current.CancellationToken); await sessionStore.ExecuteAsync(async ct => { await sessionStore.CreateRootAsync(root, ct); await sessionStore.CreateChainAsync(chain, ct); await sessionStore.CreateSessionAsync(session, ct); - }); + }, ct: TestContext.Current.CancellationToken); } // Act: Change password and revoke session within one atomic operation. @@ -409,7 +409,7 @@ await sessionStore.RevokeAllChainsAsync( // Session operation succeeded, but the outer operation fails. throw new InvalidOperationException( "Simulated failure before outer commit"); - })); + }, ct: TestContext.Current.CancellationToken)); } // Assert: Both credential and session changes must be rolled back. @@ -426,7 +426,7 @@ await sessionStore.RevokeAllChainsAsync( new TenantExecutionContext(tenant)); var persisted = await credentialStore.GetAsync( - new CredentialKey(tenant, credential.Id)); + new CredentialKey(tenant, credential.Id), ct: TestContext.Current.CancellationToken); Assert.NotNull(persisted); @@ -437,7 +437,7 @@ await sessionStore.RevokeAllChainsAsync( Assert.Equal(0, persisted.Version); - var persistedSession = await sessionStore.GetSessionAsync(sessionId); + var persistedSession = await sessionStore.GetSessionAsync(sessionId, ct: TestContext.Current.CancellationToken); Assert.NotNull(persistedSession); Assert.False(persistedSession.IsRevoked); @@ -481,9 +481,9 @@ public async Task AtomicReset_WhenSessionRevocationFails_ShouldRollbackSecurityS db, new TenantExecutionContext(tenant)); - await credentialStore.AddAsync(credential); + await credentialStore.AddAsync(credential, ct: TestContext.Current.CancellationToken); - await securityStore.AddAsync(securityState); + await securityStore.AddAsync(securityState, ct: TestContext.Current.CancellationToken); var activeReset = securityState.BeginReset( resetTokenHash, @@ -492,7 +492,7 @@ public async Task AtomicReset_WhenSessionRevocationFails_ShouldRollbackSecurityS await securityStore.UpdateAsync( activeReset, - securityState.SecurityVersion); + securityState.SecurityVersion, ct: TestContext.Current.CancellationToken); } // Act: Consume reset token and change password, @@ -572,7 +572,7 @@ await sessionStore.RevokeAllChainsAsync( throw new InvalidOperationException( "Simulated failure before reset commit"); - })); + }, ct: TestContext.Current.CancellationToken)); } // Assert: Both security state and credential must be unchanged. @@ -589,7 +589,7 @@ await sessionStore.RevokeAllChainsAsync( new TenantExecutionContext(tenant)); var persistedCredential = await credentialStore.GetAsync( - new CredentialKey(tenant, credential.Id)); + new CredentialKey(tenant, credential.Id), ct: TestContext.Current.CancellationToken); Assert.NotNull(persistedCredential); @@ -608,7 +608,7 @@ await sessionStore.RevokeAllChainsAsync( var persistedState = await securityStore.GetAsync( user, AuthenticationSecurityScope.Account, - null); + null, ct: TestContext.Current.CancellationToken); Assert.NotNull(persistedState); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreCredentialStoreTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreCredentialStoreTests.cs index 094b9036..d4da21a4 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreCredentialStoreTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreCredentialStoreTests.cs @@ -42,9 +42,9 @@ public async Task Add_And_Get_Should_Work() new CredentialMetadata(), DateTimeOffset.UtcNow); - await store.AddAsync(credential); + await store.AddAsync(credential, ct: TestContext.Current.CancellationToken); - var result = await store.GetAsync(new CredentialKey(tenant, credential.Id)); + var result = await store.GetAsync(new CredentialKey(tenant, credential.Id), ct: TestContext.Current.CancellationToken); Assert.NotNull(result); @@ -75,9 +75,9 @@ public async Task Exists_Should_Return_True_When_Exists() new CredentialMetadata(), DateTimeOffset.UtcNow); - await store.AddAsync(credential); + await store.AddAsync(credential, ct: TestContext.Current.CancellationToken); - var exists = await store.ExistsAsync(new CredentialKey(tenant, credential.Id)); + var exists = await store.ExistsAsync(new CredentialKey(tenant, credential.Id), ct: TestContext.Current.CancellationToken); Assert.True(exists); } @@ -103,25 +103,25 @@ public async Task Save_Should_Increment_Version_And_Update_Hash() await using (var db1 = CreateDb(connection)) { var store = new EfCorePasswordCredentialStore(db1, new TenantExecutionContext(tenant)); - await store.AddAsync(credential); + await store.AddAsync(credential, ct: TestContext.Current.CancellationToken); } await using (var db2 = CreateDb(connection)) { var store = new EfCorePasswordCredentialStore(db2, new TenantExecutionContext(tenant)); - var existing = await store.GetAsync(new CredentialKey(tenant, credential.Id)); + var existing = await store.GetAsync(new CredentialKey(tenant, credential.Id), ct: TestContext.Current.CancellationToken); var updated = existing!.ChangeSecret(hasher.Hash("new"), DateTimeOffset.UtcNow); - await store.SaveAsync(updated, expectedVersion: 0); + await store.SaveAsync(updated, expectedVersion: 0, ct: TestContext.Current.CancellationToken); } await using (var db3 = CreateDb(connection)) { var store = new EfCorePasswordCredentialStore(db3, new TenantExecutionContext(tenant)); - var result = await store.GetAsync(new CredentialKey(tenant, credential.Id)); + var result = await store.GetAsync(new CredentialKey(tenant, credential.Id), ct: TestContext.Current.CancellationToken); Assert.Equal(1, result!.Version); Assert.True(hasher.Verify(result.SecretHash, "new")); @@ -149,18 +149,18 @@ public async Task Save_With_Wrong_Version_Should_Throw() await using (var db1 = CreateDb(connection)) { var store = new EfCorePasswordCredentialStore(db1, new TenantExecutionContext(tenant)); - await store.AddAsync(credential); + await store.AddAsync(credential, ct: TestContext.Current.CancellationToken); } await using (var db2 = CreateDb(connection)) { var store = new EfCorePasswordCredentialStore(db2, new TenantExecutionContext(tenant)); - var existing = await store.GetAsync(new CredentialKey(tenant, credential.Id)); + var existing = await store.GetAsync(new CredentialKey(tenant, credential.Id), ct: TestContext.Current.CancellationToken); var updated = existing!.ChangeSecret(hasher.Hash("new"), DateTimeOffset.UtcNow); await Assert.ThrowsAsync(() => - store.SaveAsync(updated, expectedVersion: 999)); + store.SaveAsync(updated, expectedVersion: 999, ct: TestContext.Current.CancellationToken)); } } @@ -189,9 +189,9 @@ public async Task Should_Not_See_Data_From_Other_Tenant() new CredentialMetadata(), DateTimeOffset.UtcNow); - await store1.AddAsync(credential); + await store1.AddAsync(credential, ct: TestContext.Current.CancellationToken); - var result = await store2.GetAsync(new CredentialKey(tenant2, credential.Id)); + var result = await store2.GetAsync(new CredentialKey(tenant2, credential.Id), ct: TestContext.Current.CancellationToken); Assert.Null(result); } @@ -218,15 +218,15 @@ public async Task Soft_Delete_Should_Work() new CredentialMetadata(), DateTimeOffset.UtcNow); - await store.AddAsync(credential); + await store.AddAsync(credential, ct: TestContext.Current.CancellationToken); await store.DeleteAsync( new CredentialKey(tenant, credential.Id), expectedVersion: 0, DeleteMode.Soft, - DateTimeOffset.UtcNow); + DateTimeOffset.UtcNow, ct: TestContext.Current.CancellationToken); - var result = await store.GetAsync(new CredentialKey(tenant, credential.Id)); + var result = await store.GetAsync(new CredentialKey(tenant, credential.Id), ct: TestContext.Current.CancellationToken); Assert.NotNull(result); Assert.NotNull(result!.DeletedAt); @@ -253,24 +253,24 @@ public async Task Revoke_Should_Persist() await using (var db1 = CreateDb(connection)) { var store = new EfCorePasswordCredentialStore(db1, new TenantExecutionContext(tenant)); - await store.AddAsync(credential); + await store.AddAsync(credential, ct: TestContext.Current.CancellationToken); } await using (var db2 = CreateDb(connection)) { var store = new EfCorePasswordCredentialStore(db2, new TenantExecutionContext(tenant)); - var existing = await store.GetAsync(new CredentialKey(tenant, credential.Id)); + var existing = await store.GetAsync(new CredentialKey(tenant, credential.Id), ct: TestContext.Current.CancellationToken); var revoked = existing!.Revoke(DateTimeOffset.UtcNow); - await store.SaveAsync(revoked, expectedVersion: 0); + await store.SaveAsync(revoked, expectedVersion: 0, ct: TestContext.Current.CancellationToken); } await using (var db3 = CreateDb(connection)) { var store = new EfCorePasswordCredentialStore(db3, new TenantExecutionContext(tenant)); - var result = await store.GetAsync(new CredentialKey(tenant, credential.Id)); + var result = await store.GetAsync(new CredentialKey(tenant, credential.Id), ct: TestContext.Current.CancellationToken); Assert.True(result!.IsRevoked); } @@ -297,24 +297,24 @@ public async Task ChangeSecret_Should_Update_SecurityState() await using (var db1 = CreateDb(connection)) { var store = new EfCorePasswordCredentialStore(db1, new TenantExecutionContext(tenant)); - await store.AddAsync(credential); + await store.AddAsync(credential, ct: TestContext.Current.CancellationToken); } await using (var db2 = CreateDb(connection)) { var store = new EfCorePasswordCredentialStore(db2, new TenantExecutionContext(tenant)); - var existing = await store.GetAsync(new CredentialKey(tenant, credential.Id)); + var existing = await store.GetAsync(new CredentialKey(tenant, credential.Id), ct: TestContext.Current.CancellationToken); var updated = existing!.ChangeSecret(hasher.Hash("new"), DateTimeOffset.UtcNow); - await store.SaveAsync(updated, expectedVersion: 0); + await store.SaveAsync(updated, expectedVersion: 0, ct: TestContext.Current.CancellationToken); } await using (var db3 = CreateDb(connection)) { var store = new EfCorePasswordCredentialStore(db3, new TenantExecutionContext(tenant)); - var result = await store.GetAsync(new CredentialKey(tenant, credential.Id)); + var result = await store.GetAsync(new CredentialKey(tenant, credential.Id), ct: TestContext.Current.CancellationToken); Assert.True(hasher.Verify(result!.SecretHash, "new")); Assert.NotNull(result.UpdatedAt); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreRoleStoreTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreRoleStoreTests.cs index 79d725a7..263a1d93 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreRoleStoreTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreRoleStoreTests.cs @@ -35,9 +35,9 @@ public async Task Add_And_Get_Should_Work() new[] { Permission.From("read"), Permission.From("write") }, DateTimeOffset.UtcNow); - await store.AddAsync(role); + await store.AddAsync(role, ct: TestContext.Current.CancellationToken); - var result = await store.GetAsync(new RoleKey(tenant, role.Id)); + var result = await store.GetAsync(new RoleKey(tenant, role.Id), ct: TestContext.Current.CancellationToken); Assert.NotNull(result); Assert.Equal("admin", result!.Name); @@ -56,9 +56,9 @@ public async Task Add_With_Duplicate_Name_Should_Throw() var role1 = Role.Create(null, tenant, "admin", null, DateTimeOffset.UtcNow); var role2 = Role.Create(null, tenant, "ADMIN", null, DateTimeOffset.UtcNow); - await store.AddAsync(role1); + await store.AddAsync(role1, ct: TestContext.Current.CancellationToken); - await Assert.ThrowsAsync(() => store.AddAsync(role2)); + await Assert.ThrowsAsync(() => store.AddAsync(role2, ct: TestContext.Current.CancellationToken)); } [Fact] @@ -74,21 +74,21 @@ public async Task Save_Should_Increment_Version() var store = new EfCoreRoleStore(db, new TenantExecutionContext(tenant), new UAuthPaginationOptions()); var role = Role.Create(null, tenant, "admin", null, DateTimeOffset.UtcNow); roleId = role.Id; - await store.AddAsync(role); + await store.AddAsync(role, ct: TestContext.Current.CancellationToken); } await using (var db = CreateDb(connection)) { var store = new EfCoreRoleStore(db, new TenantExecutionContext(tenant), new UAuthPaginationOptions()); - var existing = await store.GetAsync(new RoleKey(tenant, roleId)); + var existing = await store.GetAsync(new RoleKey(tenant, roleId), ct: TestContext.Current.CancellationToken); var updated = existing!.Rename("admin2", DateTimeOffset.UtcNow); - await store.SaveAsync(updated, expectedVersion: 0); + await store.SaveAsync(updated, expectedVersion: 0, ct: TestContext.Current.CancellationToken); } await using (var db = CreateDb(connection)) { var store = new EfCoreRoleStore(db, new TenantExecutionContext(tenant), new UAuthPaginationOptions()); - var result = await store.GetAsync(new RoleKey(tenant, roleId)); + var result = await store.GetAsync(new RoleKey(tenant, roleId), ct: TestContext.Current.CancellationToken); Assert.Equal(1, result!.Version); Assert.Equal("admin2", result.Name); @@ -108,16 +108,16 @@ public async Task Save_With_Wrong_Version_Should_Throw() var store = new EfCoreRoleStore(db, new TenantExecutionContext(tenant), new UAuthPaginationOptions()); var role = Role.Create(null, tenant, "admin", null, DateTimeOffset.UtcNow); roleId = role.Id; - await store.AddAsync(role); + await store.AddAsync(role, ct: TestContext.Current.CancellationToken); } await using (var db = CreateDb(connection)) { var store = new EfCoreRoleStore(db, new TenantExecutionContext(tenant), new UAuthPaginationOptions()); - var existing = await store.GetAsync(new RoleKey(tenant, roleId)); + var existing = await store.GetAsync(new RoleKey(tenant, roleId), ct: TestContext.Current.CancellationToken); var updated = existing!.Rename("admin2", DateTimeOffset.UtcNow); - await Assert.ThrowsAsync(() => store.SaveAsync(updated, expectedVersion: 999)); + await Assert.ThrowsAsync(() => store.SaveAsync(updated, expectedVersion: 999, ct: TestContext.Current.CancellationToken)); } } @@ -138,17 +138,17 @@ public async Task Rename_To_Existing_Name_Should_Throw() var role2 = Role.Create(null, tenant, "user", null, DateTimeOffset.UtcNow); role1Id = role1.Id; role2Id = role2.Id; - await store.AddAsync(role1); - await store.AddAsync(role2); + await store.AddAsync(role1, ct: TestContext.Current.CancellationToken); + await store.AddAsync(role2, ct: TestContext.Current.CancellationToken); } await using (var db = CreateDb(connection)) { var store = new EfCoreRoleStore(db, new TenantExecutionContext(tenant), new UAuthPaginationOptions()); - var role = await store.GetAsync(new RoleKey(tenant, role2Id)); + var role = await store.GetAsync(new RoleKey(tenant, role2Id), ct: TestContext.Current.CancellationToken); var updated = role!.Rename("admin", DateTimeOffset.UtcNow); - await Assert.ThrowsAsync(() => store.SaveAsync(updated, 0)); + await Assert.ThrowsAsync(() => store.SaveAsync(updated, 0, ct: TestContext.Current.CancellationToken)); } } @@ -173,26 +173,26 @@ public async Task Save_Should_Replace_Permissions() roleId = role.Id; - await store.AddAsync(role); + await store.AddAsync(role, ct: TestContext.Current.CancellationToken); } await using (var db = CreateDb(connection)) { var store = new EfCoreRoleStore(db, new TenantExecutionContext(tenant), new UAuthPaginationOptions()); - var existing = await store.GetAsync(new RoleKey(tenant, roleId)); + var existing = await store.GetAsync(new RoleKey(tenant, roleId), ct: TestContext.Current.CancellationToken); var updated = existing!.SetPermissions( new[] { Permission.From(UAuthActions.Authorization.Roles.SetPermissionsAdmin) }, DateTimeOffset.UtcNow); - await store.SaveAsync(updated, 0); + await store.SaveAsync(updated, 0, ct: TestContext.Current.CancellationToken); } await using (var db = CreateDb(connection)) { var store = new EfCoreRoleStore(db, new TenantExecutionContext(tenant), new UAuthPaginationOptions()); - var result = await store.GetAsync(new RoleKey(tenant, roleId)); + var result = await store.GetAsync(new RoleKey(tenant, roleId), ct: TestContext.Current.CancellationToken); Assert.Single(result!.Permissions); Assert.Contains(result.Permissions, p => p.Value == UAuthActions.Authorization.Roles.SetPermissionsAdmin); @@ -212,19 +212,19 @@ public async Task Soft_Delete_Should_Work() var store = new EfCoreRoleStore(db, new TenantExecutionContext(tenant), new UAuthPaginationOptions()); var role = Role.Create(null, tenant, "admin", null, DateTimeOffset.UtcNow); roleId = role.Id; - await store.AddAsync(role); + await store.AddAsync(role, ct: TestContext.Current.CancellationToken); } await using (var db = CreateDb(connection)) { var store = new EfCoreRoleStore(db, new TenantExecutionContext(tenant), new UAuthPaginationOptions()); - await store.DeleteAsync(new RoleKey(tenant, roleId), 0, DeleteMode.Soft, DateTimeOffset.UtcNow); + await store.DeleteAsync(new RoleKey(tenant, roleId), 0, DeleteMode.Soft, DateTimeOffset.UtcNow, ct: TestContext.Current.CancellationToken); } await using (var db = CreateDb(connection)) { var store = new EfCoreRoleStore(db, new TenantExecutionContext(tenant), new UAuthPaginationOptions()); - var result = await store.GetAsync(new RoleKey(tenant, roleId)); + var result = await store.GetAsync(new RoleKey(tenant, roleId), ct: TestContext.Current.CancellationToken); Assert.NotNull(result!.DeletedAt); } } @@ -238,16 +238,16 @@ public async Task Query_Should_Filter_And_Page() var tenant = TenantKeys.Single; var store = new EfCoreRoleStore(db, new TenantExecutionContext(tenant), new UAuthPaginationOptions()); - await store.AddAsync(Role.Create(null, tenant, "admin", null, DateTimeOffset.UtcNow)); - await store.AddAsync(Role.Create(null, tenant, "user", null, DateTimeOffset.UtcNow)); - await store.AddAsync(Role.Create(null, tenant, "guest", null, DateTimeOffset.UtcNow)); + await store.AddAsync(Role.Create(null, tenant, "admin", null, DateTimeOffset.UtcNow), ct: TestContext.Current.CancellationToken); + await store.AddAsync(Role.Create(null, tenant, "user", null, DateTimeOffset.UtcNow), ct: TestContext.Current.CancellationToken); + await store.AddAsync(Role.Create(null, tenant, "guest", null, DateTimeOffset.UtcNow), ct: TestContext.Current.CancellationToken); var result = await store.QueryAsync(new RoleQuery { Search = "us", PageNumber = 1, PageSize = 10 - }); + }, ct: TestContext.Current.CancellationToken); Assert.Single(result.Items); Assert.Equal("user", result.Items.First().Name); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreSessionStoreTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreSessionStoreTests.cs index dd6d81da..7c0caceb 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreSessionStoreTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreSessionStoreTests.cs @@ -64,9 +64,9 @@ await store.ExecuteAsync(async ct => await store.CreateRootAsync(root, ct); await store.CreateChainAsync(chain, ct); await store.CreateSessionAsync(session, ct); - }); + }, ct: TestContext.Current.CancellationToken); - var result = await store.GetSessionAsync(session.SessionId); + var result = await store.GetSessionAsync(session.SessionId, ct: TestContext.Current.CancellationToken); Assert.NotNull(result); Assert.Equal(session.SessionId, result!.SessionId); @@ -123,14 +123,14 @@ await store.ExecuteAsync(async ct => await store.CreateRootAsync(root, ct); await store.CreateChainAsync(chain, ct); await store.CreateSessionAsync(session, ct); - }); + }, ct: TestContext.Current.CancellationToken); } await using (var db = CreateDb(connection)) { var store = new EfCoreSessionStore(db, new TenantExecutionContext(tenant)); - var result = await store.GetSessionAsync(sessionId); + var result = await store.GetSessionAsync(sessionId, ct: TestContext.Current.CancellationToken); Assert.NotNull(result); Assert.NotNull(result!.Device.DeviceId); @@ -200,14 +200,14 @@ await store.ExecuteAsync(async ct => await store.CreateRootAsync(root, ct); await store.CreateChainAsync(chain, ct); await store.CreateSessionAsync(session, ct); - }); + }, ct: TestContext.Current.CancellationToken); } await using (var db = CreateDb(connection)) { var store = new EfCoreSessionStore(db, new TenantExecutionContext(tenant)); - var result = await store.GetSessionAsync(sessionId); + var result = await store.GetSessionAsync(sessionId, ct: TestContext.Current.CancellationToken); Assert.NotNull(result); @@ -269,12 +269,12 @@ await store.ExecuteAsync(async ct => await store.CreateRootAsync(root, ct); await store.CreateChainAsync(chain, ct); await store.CreateSessionAsync(session, ct); - }); + }, ct: TestContext.Current.CancellationToken); await store.ExecuteAsync(async ct => { revoked = await store.RevokeSessionAsync(sessionId, DateTimeOffset.UtcNow); - }); + }, ct: TestContext.Current.CancellationToken); Assert.True(revoked); } @@ -326,14 +326,14 @@ await store1.ExecuteAsync(async ct => await store1.CreateRootAsync(root, ct); await store1.CreateChainAsync(chain, ct); await store1.CreateSessionAsync(session, ct); - }); + }, ct: TestContext.Current.CancellationToken); } await using (var db = CreateDb(connection)) { var store2 = new EfCoreSessionStore(db, new TenantExecutionContext(tenant2)); - var result = await store2.GetSessionAsync(sessionId); + var result = await store2.GetSessionAsync(sessionId, ct: TestContext.Current.CancellationToken); Assert.Null(result); } @@ -356,7 +356,7 @@ await Assert.ThrowsAsync(async () => await store.ExecuteAsync(async ct => { throw new InvalidOperationException("boom"); - }); + }, ct: TestContext.Current.CancellationToken); }); } } @@ -406,13 +406,13 @@ await store.ExecuteAsync(async ct => await store.CreateRootAsync(root, ct); await store.CreateChainAsync(chain, ct); await store.CreateSessionAsync(session, ct); - }); + }, ct: TestContext.Current.CancellationToken); } await using (var db = CreateDb(connection)) { var store = new EfCoreSessionStore(db, new TenantExecutionContext(tenant)); - var sessions = await store.GetSessionsByChainAsync(chainId); + var sessions = await store.GetSessionsByChainAsync(chainId, ct: TestContext.Current.CancellationToken); Assert.Single(sessions); } } @@ -461,14 +461,14 @@ await store.ExecuteAsync(async ct => await store.CreateRootAsync(root, ct); await store.CreateChainAsync(chain, ct); await store.CreateSessionAsync(session, ct); - }); + }, ct: TestContext.Current.CancellationToken); } await using (var db = CreateDb(connection)) { var store = new EfCoreSessionStore(db, new TenantExecutionContext(tenant)); - var result = await store.GetSessionAsync(sessionId); + var result = await store.GetSessionAsync(sessionId, ct: TestContext.Current.CancellationToken); Assert.NotNull(result); } @@ -498,7 +498,7 @@ await store.ExecuteAsync(async ct => // 💥 simulate failure throw new InvalidOperationException("boom"); - }); + }, ct: TestContext.Current.CancellationToken); }); } @@ -555,7 +555,7 @@ await store.ExecuteAsync(async ct => await store.CreateRootAsync(root, ct); await store.CreateChainAsync(chain, ct); await store.CreateSessionAsync(session, ct); - }); + }, ct: TestContext.Current.CancellationToken); } await using (var db = CreateDb(connection)) @@ -565,13 +565,13 @@ await store.ExecuteAsync(async ct => await store.ExecuteAsync(async ct => { await store.RevokeChainCascadeAsync(chainId, DateTimeOffset.UtcNow, ct); - }); + }, ct: TestContext.Current.CancellationToken); } await using (var db = CreateDb(connection)) { var store = new EfCoreSessionStore(db, new TenantExecutionContext(tenant)); - var sessions = await store.GetSessionsByChainAsync(chainId); + var sessions = await store.GetSessionsByChainAsync(chainId, ct: TestContext.Current.CancellationToken); Assert.All(sessions, s => Assert.True(s.IsRevoked)); } @@ -612,14 +612,14 @@ await store.ExecuteAsync(async ct => await store.CreateRootAsync(root, ct); await store.CreateChainAsync(chain, ct); await store.SetActiveSessionIdAsync(chainId, sessionId); - }); + }, ct: TestContext.Current.CancellationToken); } await using (var db = CreateDb(connection)) { var store = new EfCoreSessionStore(db, new TenantExecutionContext(tenant)); - var active = await store.GetActiveSessionIdAsync(chainId); + var active = await store.GetActiveSessionIdAsync(chainId, ct: TestContext.Current.CancellationToken); Assert.Equal(sessionId, active); } @@ -687,7 +687,7 @@ await store.ExecuteAsync(async ct => await store.CreateRootAsync(root, ct); await store.CreateChainAsync(chain, ct); await store.CreateSessionAsync(session, ct); - }); + }, ct: TestContext.Current.CancellationToken); } await using (var db = CreateDb(connection)) @@ -700,13 +700,13 @@ await store.ExecuteAsync(async ct => var updated = existing!.Revoke(DateTimeOffset.UtcNow); await store.SaveSessionAsync(updated, expectedVersion: 0, ct); - }); + }, ct: TestContext.Current.CancellationToken); } await using (var db = CreateDb(connection)) { var store = new EfCoreSessionStore(db, new TenantExecutionContext(tenant)); - var result = await store.GetSessionAsync(sessionId); + var result = await store.GetSessionAsync(sessionId, ct: TestContext.Current.CancellationToken); Assert.Equal(1, result!.Version); } @@ -758,7 +758,7 @@ await store.ExecuteAsync(async ct => await store.CreateRootAsync(root, ct); await store.CreateChainAsync(chain, ct); await store.CreateSessionAsync(session, ct); - }); + }, ct: TestContext.Current.CancellationToken); } await using (var db = CreateDb(connection)) @@ -773,7 +773,7 @@ await store.ExecuteAsync(async ct => var updated = existing!.Revoke(DateTimeOffset.UtcNow); await store.SaveSessionAsync(updated, expectedVersion: 999, ct); - }); + }, ct: TestContext.Current.CancellationToken); }); } } @@ -810,7 +810,7 @@ await store.ExecuteAsync(async ct => { await store.CreateRootAsync(root, ct); await store.CreateChainAsync(chain, ct); - }); + }, ct: TestContext.Current.CancellationToken); } await using (var db = CreateDb(connection)) @@ -823,13 +823,13 @@ await store.ExecuteAsync(async ct => var updated = existing!.Revoke(DateTimeOffset.UtcNow); await store.SaveChainAsync(updated, expectedVersion: 0, ct); - }); + }, ct: TestContext.Current.CancellationToken); } await using (var db = CreateDb(connection)) { var store = new EfCoreSessionStore(db, new TenantExecutionContext(tenant)); - var result = await store.GetChainAsync(chainId); + var result = await store.GetChainAsync(chainId, ct: TestContext.Current.CancellationToken); Assert.Equal(1, result!.Version); } @@ -861,7 +861,7 @@ public async Task SaveRoot_Should_Increment_Version() await store.ExecuteAsync(async ct => { await store.CreateRootAsync(root, ct); - }); + }, ct: TestContext.Current.CancellationToken); } await using (var db = CreateDb(connection)) @@ -884,7 +884,7 @@ await store.SaveRootAsync( updated, expectedVersion: existing.Version, ct); - }); + }, ct: TestContext.Current.CancellationToken); } await using (var db = CreateDb(connection)) @@ -894,7 +894,7 @@ await store.SaveRootAsync( new TenantExecutionContext(tenant)); var result = - await store.GetRootByIdAsync(rootId); + await store.GetRootByIdAsync(rootId, ct: TestContext.Current.CancellationToken); result.Should().NotBeNull(); result!.Version.Should().Be(1); @@ -913,7 +913,7 @@ public async Task ExecuteAsync_Should_Join_Existing_Transaction() new TenantExecutionContext(TenantKeys.Single)); await using var transaction = - await db.Database.BeginTransactionAsync(); + await db.Database.BeginTransactionAsync(cancellationToken: TestContext.Current.CancellationToken); var originalTransaction = db.Database.CurrentTransaction; @@ -924,12 +924,13 @@ await store.ExecuteAsync(async ct => db.Database.CurrentTransaction); await Task.CompletedTask; - }); + }, ct: TestContext.Current.CancellationToken); Assert.Same( originalTransaction, db.Database.CurrentTransaction); + // Cleanup must complete even when the test is cancelled. await transaction.RollbackAsync(); } } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreTokenStoreTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreTokenStoreTests.cs index 7e36b9ff..612c7397 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreTokenStoreTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreTokenStoreTests.cs @@ -41,9 +41,9 @@ public async Task Store_And_Find_Should_Work() await store.ExecuteAsync(async ct => { await store.StoreAsync(token, ct); - }); + }, ct: TestContext.Current.CancellationToken); - var result = await store.FindByHashAsync("hash"); + var result = await store.FindByHashAsync("hash", ct: TestContext.Current.CancellationToken); Assert.NotNull(result); Assert.Equal("hash", result!.TokenHash); @@ -76,7 +76,7 @@ public async Task Revoke_Should_Set_RevokedAt() await store.ExecuteAsync(async ct => { await store.StoreAsync(token, ct); - }); + }, ct: TestContext.Current.CancellationToken); } await using (var db = CreateDb(connection)) @@ -86,13 +86,13 @@ await store.ExecuteAsync(async ct => await store.ExecuteAsync(async ct => { await store.RevokeAsync(tokenHash, DateTimeOffset.UtcNow, null, ct); - }); + }, ct: TestContext.Current.CancellationToken); } await using (var db = CreateDb(connection)) { var store = new EfCoreRefreshTokenStore(db, new TenantExecutionContext(tenant)); - var result = await store.FindByHashAsync(tokenHash); + var result = await store.FindByHashAsync(tokenHash, ct: TestContext.Current.CancellationToken); Assert.NotNull(result!.RevokedAt); } @@ -119,7 +119,7 @@ public async Task Store_Outside_Transaction_Should_Throw() DateTimeOffset.UtcNow.AddHours(1) ); - await Assert.ThrowsAsync(() => store.StoreAsync(token)); + await Assert.ThrowsAsync(() => store.StoreAsync(token, ct: TestContext.Current.CancellationToken)); } [Fact] @@ -160,7 +160,7 @@ await store.StoreAsync( db, new TenantExecutionContext(tenant)); - var persisted = await store.FindByHashAsync(hash); + var persisted = await store.FindByHashAsync(hash, ct: TestContext.Current.CancellationToken); Assert.NotNull(persisted); } @@ -194,7 +194,7 @@ await store.StoreAsync( throw new InvalidOperationException( "Simulated outer transaction failure"); - })); + }, ct: TestContext.Current.CancellationToken)); }); } @@ -204,7 +204,7 @@ await store.StoreAsync( db, new TenantExecutionContext(tenant)); - var persisted = await store.FindByHashAsync(hash); + var persisted = await store.FindByHashAsync(hash, ct: TestContext.Current.CancellationToken); Assert.Null(persisted); } @@ -248,7 +248,7 @@ await store.StoreAsync( db, new TenantExecutionContext(tenant)); - Assert.NotNull(await store.FindByHashAsync(hash)); + Assert.NotNull(await store.FindByHashAsync(hash, ct: TestContext.Current.CancellationToken)); } } @@ -265,17 +265,17 @@ public async Task ExecuteAsync_AfterFailure_ShouldResetExecutionState() await Assert.ThrowsAsync( () => store.ExecuteAsync(_ => - throw new InvalidOperationException("Simulated failure"))); + throw new InvalidOperationException("Simulated failure"), ct: TestContext.Current.CancellationToken)); await store.ExecuteAsync(async ct => { await store.StoreAsync( CreateToken("after-failure", tenant), ct); - }); + }, ct: TestContext.Current.CancellationToken); Assert.NotNull( - await store.FindByHashAsync("after-failure")); + await store.FindByHashAsync("after-failure", ct: TestContext.Current.CancellationToken)); } [Fact] @@ -295,7 +295,7 @@ await Assert.ThrowsAsync( await store.ExecuteAsync( _ => Task.CompletedTask, ct); - })); + }, ct: TestContext.Current.CancellationToken)); // The failed nested call must not leave the store locked. await store.ExecuteAsync(async ct => @@ -303,10 +303,10 @@ await store.ExecuteAsync(async ct => await store.StoreAsync( CreateToken("after-nested-failure", tenant), ct); - }); + }, ct: TestContext.Current.CancellationToken); Assert.NotNull( - await store.FindByHashAsync("after-nested-failure")); + await store.FindByHashAsync("after-nested-failure", ct: TestContext.Current.CancellationToken)); } private static RefreshToken CreateToken(string hash, TenantKey tenant) diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserIdentifierStoreTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserIdentifierStoreTests.cs index f06f5f60..aa6b4f65 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserIdentifierStoreTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserIdentifierStoreTests.cs @@ -37,8 +37,8 @@ public async Task Add_And_Get_Should_Work() DateTimeOffset.UtcNow, isPrimary: true); - await store.AddAsync(identifier); - var result = await store.GetAsync(identifier.Id); + await store.AddAsync(identifier, ct: TestContext.Current.CancellationToken); + var result = await store.GetAsync(identifier.Id, ct: TestContext.Current.CancellationToken); Assert.NotNull(result); Assert.Equal(identifier.Id, result!.Id); @@ -63,8 +63,8 @@ public async Task Exists_Should_Return_True_When_Exists() DateTimeOffset.UtcNow, isPrimary: true); - await store.AddAsync(identifier); - var exists = await store.ExistsAsync(identifier.Id); + await store.AddAsync(identifier, ct: TestContext.Current.CancellationToken); + var exists = await store.ExistsAsync(identifier.Id, ct: TestContext.Current.CancellationToken); Assert.True(exists); } @@ -90,7 +90,7 @@ public async Task Save_With_Wrong_Version_Should_Throw() DateTimeOffset.UtcNow, isPrimary: true); - await store1.AddAsync(identifier); + await store1.AddAsync(identifier, ct: TestContext.Current.CancellationToken); await using var db2 = CreateDb(connection); var store2 = new EfCoreUserIdentifierStore(db2, new TenantExecutionContext(tenant), new UAuthPaginationOptions()); @@ -98,7 +98,7 @@ public async Task Save_With_Wrong_Version_Should_Throw() var updated = identifier.SetPrimary(DateTimeOffset.UtcNow); await Assert.ThrowsAsync(() => - store2.SaveAsync(updated, expectedVersion: 999)); + store2.SaveAsync(updated, expectedVersion: 999, ct: TestContext.Current.CancellationToken)); } [Fact] @@ -122,21 +122,21 @@ public async Task Save_Should_Increment_Version() await using (var db1 = CreateDb(connection)) { var store1 = new EfCoreUserIdentifierStore(db1, new TenantExecutionContext(tenant), new UAuthPaginationOptions()); - await store1.AddAsync(identifier); + await store1.AddAsync(identifier, ct: TestContext.Current.CancellationToken); } await using (var db2 = CreateDb(connection)) { var store2 = new EfCoreUserIdentifierStore(db2, new TenantExecutionContext(tenant), new UAuthPaginationOptions()); - var existing = await store2.GetAsync(identifier.Id); + var existing = await store2.GetAsync(identifier.Id, ct: TestContext.Current.CancellationToken); var updated = existing!.SetPrimary(DateTimeOffset.UtcNow); - await store2.SaveAsync(updated, expectedVersion: 0); + await store2.SaveAsync(updated, expectedVersion: 0, ct: TestContext.Current.CancellationToken); } await using (var db3 = CreateDb(connection)) { var store3 = new EfCoreUserIdentifierStore(db3, new TenantExecutionContext(tenant), new UAuthPaginationOptions()); - var result = await store3.GetAsync(identifier.Id); + var result = await store3.GetAsync(identifier.Id, ct: TestContext.Current.CancellationToken); Assert.Equal(1, result!.Version); } } @@ -163,8 +163,8 @@ public async Task Should_Not_See_Data_From_Other_Tenant() DateTimeOffset.UtcNow, isPrimary: true); - await store1.AddAsync(identifier); - var result = await store2.GetAsync(identifier.Id); + await store1.AddAsync(identifier, ct: TestContext.Current.CancellationToken); + var result = await store2.GetAsync(identifier.Id, ct: TestContext.Current.CancellationToken); Assert.Null(result); } @@ -188,9 +188,9 @@ public async Task Soft_Delete_Should_Work() DateTimeOffset.UtcNow, isPrimary: true); - await store.AddAsync(identifier); - await store.DeleteAsync(identifier.Id, 0, DeleteMode.Soft, DateTimeOffset.UtcNow); - var result = await store.GetAsync(identifier.Id); + await store.AddAsync(identifier, ct: TestContext.Current.CancellationToken); + await store.DeleteAsync(identifier.Id, 0, DeleteMode.Soft, DateTimeOffset.UtcNow, ct: TestContext.Current.CancellationToken); + var result = await store.GetAsync(identifier.Id, ct: TestContext.Current.CancellationToken); Assert.NotNull(result); Assert.NotNull(result!.DeletedAt); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserLifecycleStoreTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserLifecycleStoreTests.cs index 9c97641c..8c5f0d0a 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserLifecycleStoreTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserLifecycleStoreTests.cs @@ -33,9 +33,9 @@ public async Task Add_And_Get_Should_Work() userKey, DateTimeOffset.UtcNow); - await store.AddAsync(lifecycle); + await store.AddAsync(lifecycle, ct: TestContext.Current.CancellationToken); - var result = await store.GetAsync(new UserLifecycleKey(tenant, userKey)); + var result = await store.GetAsync(new UserLifecycleKey(tenant, userKey), ct: TestContext.Current.CancellationToken); Assert.NotNull(result); Assert.Equal(userKey, result!.UserKey); @@ -58,8 +58,8 @@ public async Task Exists_Should_Return_True_When_Exists() userKey, DateTimeOffset.UtcNow); - await store.AddAsync(lifecycle); - var exists = await store.ExistsAsync(new UserLifecycleKey(tenant, userKey)); + await store.AddAsync(lifecycle, ct: TestContext.Current.CancellationToken); + var exists = await store.ExistsAsync(new UserLifecycleKey(tenant, userKey), ct: TestContext.Current.CancellationToken); Assert.True(exists); } @@ -80,21 +80,21 @@ public async Task Save_Should_Increment_Version() await using (var db1 = CreateDb(connection)) { var store1 = new EfCoreUserLifecycleStore(db1, new TenantExecutionContext(tenant), new UAuthPaginationOptions()); - await store1.AddAsync(lifecycle); + await store1.AddAsync(lifecycle, ct: TestContext.Current.CancellationToken); } await using (var db2 = CreateDb(connection)) { var store2 = new EfCoreUserLifecycleStore(db2, new TenantExecutionContext(tenant), new UAuthPaginationOptions()); - var existing = await store2.GetAsync(new UserLifecycleKey(tenant, userKey)); + var existing = await store2.GetAsync(new UserLifecycleKey(tenant, userKey), ct: TestContext.Current.CancellationToken); var updated = existing!.ChangeStatus(DateTimeOffset.UtcNow, UserStatus.Suspended); - await store2.SaveAsync(updated, expectedVersion: 0); + await store2.SaveAsync(updated, expectedVersion: 0, ct: TestContext.Current.CancellationToken); } await using (var db3 = CreateDb(connection)) { var store3 = new EfCoreUserLifecycleStore(db3, new TenantExecutionContext(tenant), new UAuthPaginationOptions()); - var result = await store3.GetAsync(new UserLifecycleKey(tenant, userKey)); + var result = await store3.GetAsync(new UserLifecycleKey(tenant, userKey), ct: TestContext.Current.CancellationToken); Assert.Equal(1, result!.Version); Assert.Equal(UserStatus.Suspended, result.Status); @@ -117,17 +117,17 @@ public async Task Save_With_Wrong_Version_Should_Throw() await using (var db1 = CreateDb(connection)) { var store1 = new EfCoreUserLifecycleStore(db1, new TenantExecutionContext(tenant), new UAuthPaginationOptions()); - await store1.AddAsync(lifecycle); + await store1.AddAsync(lifecycle, ct: TestContext.Current.CancellationToken); } await using (var db2 = CreateDb(connection)) { var store2 = new EfCoreUserLifecycleStore(db2, new TenantExecutionContext(tenant), new UAuthPaginationOptions()); - var existing = await store2.GetAsync(new UserLifecycleKey(tenant, userKey)); + var existing = await store2.GetAsync(new UserLifecycleKey(tenant, userKey), ct: TestContext.Current.CancellationToken); var updated = existing!.ChangeStatus(DateTimeOffset.UtcNow, UserStatus.Suspended); await Assert.ThrowsAsync(() => - store2.SaveAsync(updated, expectedVersion: 999)); + store2.SaveAsync(updated, expectedVersion: 999, ct: TestContext.Current.CancellationToken)); } } @@ -150,9 +150,9 @@ public async Task Should_Not_See_Data_From_Other_Tenant() userKey, DateTimeOffset.UtcNow); - await store1.AddAsync(lifecycle); + await store1.AddAsync(lifecycle, ct: TestContext.Current.CancellationToken); - var result = await store2.GetAsync(new UserLifecycleKey(tenant2, userKey)); + var result = await store2.GetAsync(new UserLifecycleKey(tenant2, userKey), ct: TestContext.Current.CancellationToken); Assert.Null(result); } @@ -173,15 +173,15 @@ public async Task Soft_Delete_Should_Work() userKey, DateTimeOffset.UtcNow); - await store.AddAsync(lifecycle); + await store.AddAsync(lifecycle, ct: TestContext.Current.CancellationToken); await store.DeleteAsync( new UserLifecycleKey(tenant, userKey), expectedVersion: 0, DeleteMode.Soft, - DateTimeOffset.UtcNow); + DateTimeOffset.UtcNow, ct: TestContext.Current.CancellationToken); - var result = await store.GetAsync(new UserLifecycleKey(tenant, userKey)); + var result = await store.GetAsync(new UserLifecycleKey(tenant, userKey), ct: TestContext.Current.CancellationToken); Assert.NotNull(result); Assert.NotNull(result!.DeletedAt); @@ -203,24 +203,24 @@ public async Task Delete_Should_Increment_SecurityVersion() await using (var db1 = CreateDb(connection)) { var store1 = new EfCoreUserLifecycleStore(db1, new TenantExecutionContext(tenant), new UAuthPaginationOptions()); - await store1.AddAsync(lifecycle); + await store1.AddAsync(lifecycle, ct: TestContext.Current.CancellationToken); } await using (var db2 = CreateDb(connection)) { var store2 = new EfCoreUserLifecycleStore(db2, new TenantExecutionContext(tenant), new UAuthPaginationOptions()); - var existing = await store2.GetAsync(new UserLifecycleKey(tenant, userKey)); + var existing = await store2.GetAsync(new UserLifecycleKey(tenant, userKey), ct: TestContext.Current.CancellationToken); var deleted = existing!.MarkDeleted(DateTimeOffset.UtcNow); - await store2.SaveAsync(deleted, expectedVersion: 0); + await store2.SaveAsync(deleted, expectedVersion: 0, ct: TestContext.Current.CancellationToken); } await using (var db3 = CreateDb(connection)) { var store3 = new EfCoreUserLifecycleStore(db3, new TenantExecutionContext(tenant), new UAuthPaginationOptions()); - var result = await store3.GetAsync(new UserLifecycleKey(tenant, userKey)); + var result = await store3.GetAsync(new UserLifecycleKey(tenant, userKey), ct: TestContext.Current.CancellationToken); Assert.Equal(1, result!.SecurityVersion); } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserProfileStoreTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserProfileStoreTests.cs index 7da74d8d..0e8cba83 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserProfileStoreTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserProfileStoreTests.cs @@ -40,8 +40,8 @@ public async Task Add_And_Get_Should_Work() lastName: "last" ); - await store.AddAsync(profile); - var result = await store.GetAsync(new UserProfileKey(tenant, userKey, ProfileKey.Default)); + await store.AddAsync(profile, ct: TestContext.Current.CancellationToken); + var result = await store.GetAsync(new UserProfileKey(tenant, userKey, ProfileKey.Default), ct: TestContext.Current.CancellationToken); Assert.NotNull(result); Assert.Equal(userKey, result!.UserKey); @@ -69,8 +69,8 @@ public async Task Exists_Should_Return_True_When_Exists() lastName: "last" ); - await store.AddAsync(profile); - var exists = await store.ExistsAsync(new UserProfileKey(tenant, userKey, ProfileKey.Default)); + await store.AddAsync(profile, ct: TestContext.Current.CancellationToken); + var exists = await store.ExistsAsync(new UserProfileKey(tenant, userKey, ProfileKey.Default), ct: TestContext.Current.CancellationToken); Assert.True(exists); } @@ -97,21 +97,21 @@ public async Task Save_Should_Increment_Version() await using (var db1 = CreateDb(connection)) { var store1 = new EfCoreUserProfileStore(db1, new TenantExecutionContext(tenant), new UAuthPaginationOptions()); - await store1.AddAsync(profile); + await store1.AddAsync(profile, ct: TestContext.Current.CancellationToken); } await using (var db2 = CreateDb(connection)) { var store2 = new EfCoreUserProfileStore(db2, new TenantExecutionContext(tenant), new UAuthPaginationOptions()); - var existing = await store2.GetAsync(new UserProfileKey(tenant, userKey, ProfileKey.Default)); + var existing = await store2.GetAsync(new UserProfileKey(tenant, userKey, ProfileKey.Default), ct: TestContext.Current.CancellationToken); var updated = existing!.UpdateName(existing.FirstName, existing.LastName, "new", DateTimeOffset.UtcNow); - await store2.SaveAsync(updated, expectedVersion: 0); + await store2.SaveAsync(updated, expectedVersion: 0, ct: TestContext.Current.CancellationToken); } await using (var db3 = CreateDb(connection)) { var store3 = new EfCoreUserProfileStore(db3, new TenantExecutionContext(tenant), new UAuthPaginationOptions()); - var result = await store3.GetAsync(new UserProfileKey(tenant, userKey, ProfileKey.Default)); + var result = await store3.GetAsync(new UserProfileKey(tenant, userKey, ProfileKey.Default), ct: TestContext.Current.CancellationToken); Assert.Equal(1, result!.Version); Assert.Equal("new", result.DisplayName); @@ -140,17 +140,17 @@ public async Task Save_With_Wrong_Version_Should_Throw() await using (var db1 = CreateDb(connection)) { var store1 = new EfCoreUserProfileStore(db1, new TenantExecutionContext(tenant), new UAuthPaginationOptions()); - await store1.AddAsync(profile); + await store1.AddAsync(profile, ct: TestContext.Current.CancellationToken); } await using (var db2 = CreateDb(connection)) { var store2 = new EfCoreUserProfileStore(db2, new TenantExecutionContext(tenant), new UAuthPaginationOptions()); - var existing = await store2.GetAsync(new UserProfileKey(tenant, userKey, ProfileKey.Default)); + var existing = await store2.GetAsync(new UserProfileKey(tenant, userKey, ProfileKey.Default), ct: TestContext.Current.CancellationToken); var updated = existing!.UpdateName(existing.FirstName, existing.LastName, "new", DateTimeOffset.UtcNow); await Assert.ThrowsAsync(() => - store2.SaveAsync(updated, expectedVersion: 999)); + store2.SaveAsync(updated, expectedVersion: 999, ct: TestContext.Current.CancellationToken)); } } @@ -179,8 +179,8 @@ public async Task Should_Not_See_Data_From_Other_Tenant() lastName: "last" ); - await store1.AddAsync(profile); - var result = await store2.GetAsync(new UserProfileKey(tenant2, userKey, ProfileKey.Default)); + await store1.AddAsync(profile, ct: TestContext.Current.CancellationToken); + var result = await store2.GetAsync(new UserProfileKey(tenant2, userKey, ProfileKey.Default), ct: TestContext.Current.CancellationToken); Assert.Null(result); } @@ -207,15 +207,15 @@ public async Task Soft_Delete_Should_Work() lastName: "last" ); - await store.AddAsync(profile); + await store.AddAsync(profile, ct: TestContext.Current.CancellationToken); await store.DeleteAsync( new UserProfileKey(tenant, userKey, ProfileKey.Default), expectedVersion: 0, DeleteMode.Soft, - DateTimeOffset.UtcNow); + DateTimeOffset.UtcNow, ct: TestContext.Current.CancellationToken); - var result = await store.GetAsync(new UserProfileKey(tenant, userKey, ProfileKey.Default)); + var result = await store.GetAsync(new UserProfileKey(tenant, userKey, ProfileKey.Default), ct: TestContext.Current.CancellationToken); Assert.NotNull(result); Assert.NotNull(result!.DeletedAt); @@ -248,11 +248,11 @@ public async Task Same_User_Can_Have_Multiple_Profiles() DateTimeOffset.UtcNow, displayName: "business"); - await store.AddAsync(defaultProfile); - await store.AddAsync(businessProfile); + await store.AddAsync(defaultProfile, ct: TestContext.Current.CancellationToken); + await store.AddAsync(businessProfile, ct: TestContext.Current.CancellationToken); - var p1 = await store.GetAsync(new UserProfileKey(tenant, userKey, ProfileKey.Default)); - var p2 = await store.GetAsync(new UserProfileKey(tenant, userKey, ProfileKey.Parse("business", null))); + var p1 = await store.GetAsync(new UserProfileKey(tenant, userKey, ProfileKey.Default), ct: TestContext.Current.CancellationToken); + var p2 = await store.GetAsync(new UserProfileKey(tenant, userKey, ProfileKey.Parse("business", null)), ct: TestContext.Current.CancellationToken); Assert.NotNull(p1); Assert.NotNull(p2); @@ -276,7 +276,7 @@ await store.AddAsync(UserProfile.Create( userKey, ProfileKey.Default, DateTimeOffset.UtcNow, - displayName: "default")); + displayName: "default"), ct: TestContext.Current.CancellationToken); await store.AddAsync(UserProfile.Create( Guid.NewGuid(), @@ -284,10 +284,10 @@ await store.AddAsync(UserProfile.Create( userKey, ProfileKey.Parse("business", null), DateTimeOffset.UtcNow, - displayName: "business")); + displayName: "business"), ct: TestContext.Current.CancellationToken); var result = await store.GetAsync( - new UserProfileKey(tenant, userKey, ProfileKey.Parse("business", null))); + new UserProfileKey(tenant, userKey, ProfileKey.Parse("business", null)), ct: TestContext.Current.CancellationToken); Assert.Equal("business", result!.DisplayName); } @@ -309,7 +309,7 @@ await store.AddAsync(UserProfile.Create( userKey, ProfileKey.Default, DateTimeOffset.UtcNow, - displayName: "default")); + displayName: "default"), ct: TestContext.Current.CancellationToken); await store.AddAsync(UserProfile.Create( Guid.NewGuid(), @@ -317,11 +317,11 @@ await store.AddAsync(UserProfile.Create( userKey, ProfileKey.Parse("business", null), DateTimeOffset.UtcNow, - displayName: "business")); + displayName: "business"), ct: TestContext.Current.CancellationToken); var results = await store.GetByUsersAsync( new[] { userKey }, - ProfileKey.Default); + ProfileKey.Default, ct: TestContext.Current.CancellationToken); Assert.Single(results); Assert.Equal(ProfileKey.Default, results[0].ProfileKey); @@ -352,10 +352,10 @@ public async Task Should_Not_Allow_Duplicate_ProfileKey_For_Same_User() ProfileKey.Default, DateTimeOffset.UtcNow); - await store.AddAsync(profile1); + await store.AddAsync(profile1, ct: TestContext.Current.CancellationToken); await Assert.ThrowsAsync(() => - store.AddAsync(profile2)); + store.AddAsync(profile2, ct: TestContext.Current.CancellationToken)); } [Fact] @@ -383,20 +383,20 @@ public async Task Delete_Should_Not_Affect_Other_Profiles() ProfileKey.Parse("business", null), DateTimeOffset.UtcNow); - await store.AddAsync(defaultProfile); - await store.AddAsync(businessProfile); + await store.AddAsync(defaultProfile, ct: TestContext.Current.CancellationToken); + await store.AddAsync(businessProfile, ct: TestContext.Current.CancellationToken); await store.DeleteAsync( new UserProfileKey(tenant, userKey, ProfileKey.Default), 0, DeleteMode.Soft, - DateTimeOffset.UtcNow); + DateTimeOffset.UtcNow, ct: TestContext.Current.CancellationToken); var defaultResult = await store.GetAsync( - new UserProfileKey(tenant, userKey, ProfileKey.Default)); + new UserProfileKey(tenant, userKey, ProfileKey.Default), ct: TestContext.Current.CancellationToken); var businessResult = await store.GetAsync( - new UserProfileKey(tenant, userKey, ProfileKey.Parse("business", null))); + new UserProfileKey(tenant, userKey, ProfileKey.Parse("business", null)), ct: TestContext.Current.CancellationToken); Assert.NotNull(defaultResult!.DeletedAt); Assert.Null(businessResult!.DeletedAt); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserRoleStoreTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserRoleStoreTests.cs index 5d9b1264..212052d6 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserRoleStoreTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserRoleStoreTests.cs @@ -27,8 +27,8 @@ public async Task Assign_And_GetAssignments_Should_Work() var userKey = UserKey.FromGuid(Guid.NewGuid()); var roleId = RoleId.New(); - await store.AssignAsync(userKey, roleId, DateTimeOffset.UtcNow); - var result = await store.GetAssignmentsAsync(userKey); + await store.AssignAsync(userKey, roleId, DateTimeOffset.UtcNow, ct: TestContext.Current.CancellationToken); + var result = await store.GetAssignmentsAsync(userKey, ct: TestContext.Current.CancellationToken); Assert.Single(result); Assert.Equal(roleId, result.First().RoleId); @@ -46,9 +46,9 @@ public async Task Assign_Duplicate_Should_Throw() var userKey = UserKey.FromGuid(Guid.NewGuid()); var roleId = RoleId.New(); - await store.AssignAsync(userKey, roleId, DateTimeOffset.UtcNow); + await store.AssignAsync(userKey, roleId, DateTimeOffset.UtcNow, ct: TestContext.Current.CancellationToken); - await Assert.ThrowsAsync(() => store.AssignAsync(userKey, roleId, DateTimeOffset.UtcNow)); + await Assert.ThrowsAsync(() => store.AssignAsync(userKey, roleId, DateTimeOffset.UtcNow, ct: TestContext.Current.CancellationToken)); } [Fact] @@ -63,9 +63,9 @@ public async Task Remove_Should_Work() var userKey = UserKey.FromGuid(Guid.NewGuid()); var roleId = RoleId.New(); - await store.AssignAsync(userKey, roleId, DateTimeOffset.UtcNow); - await store.RemoveAsync(userKey, roleId); - var result = await store.GetAssignmentsAsync(userKey); + await store.AssignAsync(userKey, roleId, DateTimeOffset.UtcNow, ct: TestContext.Current.CancellationToken); + await store.RemoveAsync(userKey, roleId, ct: TestContext.Current.CancellationToken); + var result = await store.GetAssignmentsAsync(userKey, ct: TestContext.Current.CancellationToken); Assert.Empty(result); } @@ -82,7 +82,7 @@ public async Task Remove_NonExisting_Should_Not_Throw() var userKey = UserKey.FromGuid(Guid.NewGuid()); var roleId = RoleId.New(); - await store.RemoveAsync(userKey, roleId); // should not throw + await store.RemoveAsync(userKey, roleId, ct: TestContext.Current.CancellationToken); // should not throw } [Fact] @@ -96,10 +96,10 @@ public async Task CountAssignments_Should_Return_Correct_Count() var roleId = RoleId.New(); - await store.AssignAsync(UserKey.FromGuid(Guid.NewGuid()), roleId, DateTimeOffset.UtcNow); - await store.AssignAsync(UserKey.FromGuid(Guid.NewGuid()), roleId, DateTimeOffset.UtcNow); + await store.AssignAsync(UserKey.FromGuid(Guid.NewGuid()), roleId, DateTimeOffset.UtcNow, ct: TestContext.Current.CancellationToken); + await store.AssignAsync(UserKey.FromGuid(Guid.NewGuid()), roleId, DateTimeOffset.UtcNow, ct: TestContext.Current.CancellationToken); - var count = await store.CountAssignmentsAsync(roleId); + var count = await store.CountAssignmentsAsync(roleId, ct: TestContext.Current.CancellationToken); Assert.Equal(2, count); } @@ -118,12 +118,12 @@ public async Task RemoveAssignmentsByRole_Should_Remove_All() var user1 = UserKey.FromGuid(Guid.NewGuid()); var user2 = UserKey.FromGuid(Guid.NewGuid()); - await store.AssignAsync(user1, roleId, DateTimeOffset.UtcNow); - await store.AssignAsync(user2, roleId, DateTimeOffset.UtcNow); + await store.AssignAsync(user1, roleId, DateTimeOffset.UtcNow, ct: TestContext.Current.CancellationToken); + await store.AssignAsync(user2, roleId, DateTimeOffset.UtcNow, ct: TestContext.Current.CancellationToken); - await store.RemoveAssignmentsByRoleAsync(roleId); + await store.RemoveAssignmentsByRoleAsync(roleId, ct: TestContext.Current.CancellationToken); - var count = await store.CountAssignmentsAsync(roleId); + var count = await store.CountAssignmentsAsync(roleId, ct: TestContext.Current.CancellationToken); Assert.Equal(0, count); } @@ -143,9 +143,9 @@ public async Task Should_Not_See_Data_From_Other_Tenant() var userKey = UserKey.FromGuid(Guid.NewGuid()); var roleId = RoleId.New(); - await store1.AssignAsync(userKey, roleId, DateTimeOffset.UtcNow); + await store1.AssignAsync(userKey, roleId, DateTimeOffset.UtcNow, ct: TestContext.Current.CancellationToken); - var result = await store2.GetAssignmentsAsync(userKey); + var result = await store2.GetAssignmentsAsync(userKey, ct: TestContext.Current.CancellationToken); Assert.Empty(result); } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/InMemory/InMemoryVersionedStoreTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/InMemory/InMemoryVersionedStoreTests.cs index 73385ead..53bf5919 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/InMemory/InMemoryVersionedStoreTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/InMemory/InMemoryVersionedStoreTests.cs @@ -15,9 +15,9 @@ public async Task AddAsync_WithoutAtomicContext_ShouldPersistEntity() var entity = TestEntity.Create("entity-1", "initial"); - await store.AddAsync(entity); + await store.AddAsync(entity, ct: TestContext.Current.CancellationToken); - var stored = await store.GetAsync("entity-1"); + var stored = await store.GetAsync("entity-1", ct: TestContext.Current.CancellationToken); stored.Should().NotBeNull(); stored!.Value.Should().Be("initial"); @@ -35,15 +35,15 @@ public async Task AddAsync_WithAtomicContext_WhenRolledBack_ShouldRemoveEntity() var store = new TestStore(accessor); await store.AddAsync( - TestEntity.Create("entity-1", "initial")); + TestEntity.Create("entity-1", "initial"), ct: TestContext.Current.CancellationToken); - (await store.ExistsAsync("entity-1")) + (await store.ExistsAsync("entity-1", ct: TestContext.Current.CancellationToken)) .Should() .BeTrue(); atomic.Rollback(); - (await store.ExistsAsync("entity-1")) + (await store.ExistsAsync("entity-1", ct: TestContext.Current.CancellationToken)) .Should() .BeFalse(); } @@ -55,27 +55,27 @@ public async Task SaveAsync_WithAtomicContext_WhenRolledBack_ShouldRestorePrevio var store = new TestStore(accessor); await store.AddAsync( - TestEntity.Create("entity-1", "initial")); + TestEntity.Create("entity-1", "initial"), ct: TestContext.Current.CancellationToken); var atomic = new InMemoryAtomicContext(); accessor.Current = atomic; - var entity = await store.GetAsync("entity-1"); + var entity = await store.GetAsync("entity-1", ct: TestContext.Current.CancellationToken); entity!.Value = "updated"; await store.SaveAsync( entity, - expectedVersion: 0); + expectedVersion: 0, ct: TestContext.Current.CancellationToken); - var updated = await store.GetAsync("entity-1"); + var updated = await store.GetAsync("entity-1", ct: TestContext.Current.CancellationToken); updated!.Value.Should().Be("updated"); updated.Version.Should().Be(1); atomic.Rollback(); - var restored = await store.GetAsync("entity-1"); + var restored = await store.GetAsync("entity-1", ct: TestContext.Current.CancellationToken); restored.Should().NotBeNull(); restored!.Value.Should().Be("initial"); @@ -89,7 +89,7 @@ public async Task DeleteAsync_Hard_WithAtomicContext_WhenRolledBack_ShouldRestor var store = new TestStore(accessor); await store.AddAsync( - TestEntity.Create("entity-1", "initial")); + TestEntity.Create("entity-1", "initial"), ct: TestContext.Current.CancellationToken); var atomic = new InMemoryAtomicContext(); accessor.Current = atomic; @@ -98,15 +98,15 @@ await store.DeleteAsync( "entity-1", expectedVersion: 0, DeleteMode.Hard, - DateTimeOffset.UtcNow); + DateTimeOffset.UtcNow, ct: TestContext.Current.CancellationToken); - (await store.ExistsAsync("entity-1")) + (await store.ExistsAsync("entity-1", ct: TestContext.Current.CancellationToken)) .Should() .BeFalse(); atomic.Rollback(); - var restored = await store.GetAsync("entity-1"); + var restored = await store.GetAsync("entity-1", ct: TestContext.Current.CancellationToken); restored.Should().NotBeNull(); restored!.Value.Should().Be("initial"); @@ -121,7 +121,7 @@ public async Task DeleteAsync_Soft_WithAtomicContext_WhenRolledBack_ShouldRestor var store = new TestStore(accessor); await store.AddAsync( - TestEntity.Create("entity-1", "initial")); + TestEntity.Create("entity-1", "initial"), ct: TestContext.Current.CancellationToken); var atomic = new InMemoryAtomicContext(); accessor.Current = atomic; @@ -136,9 +136,9 @@ await store.DeleteAsync( "entity-1", expectedVersion: 0, DeleteMode.Soft, - deletedAt); + deletedAt, ct: TestContext.Current.CancellationToken); - var deleted = await store.GetAsync("entity-1"); + var deleted = await store.GetAsync("entity-1", ct: TestContext.Current.CancellationToken); deleted.Should().NotBeNull(); deleted!.IsDeleted.Should().BeTrue(); @@ -147,7 +147,7 @@ await store.DeleteAsync( atomic.Rollback(); - var restored = await store.GetAsync("entity-1"); + var restored = await store.GetAsync("entity-1", ct: TestContext.Current.CancellationToken); restored.Should().NotBeNull(); restored!.IsDeleted.Should().BeFalse(); @@ -162,41 +162,41 @@ public async Task MultipleOperations_WhenRolledBack_ShouldRestoreOriginalState() var store = new TestStore(accessor); await store.AddAsync( - TestEntity.Create("existing", "original")); + TestEntity.Create("existing", "original"), ct: TestContext.Current.CancellationToken); var atomic = new InMemoryAtomicContext(); accessor.Current = atomic; // ADD await store.AddAsync( - TestEntity.Create("new", "created")); + TestEntity.Create("new", "created"), ct: TestContext.Current.CancellationToken); // UPDATE - var existing = await store.GetAsync("existing"); + var existing = await store.GetAsync("existing", ct: TestContext.Current.CancellationToken); existing!.Value = "updated"; await store.SaveAsync( existing, - expectedVersion: 0); + expectedVersion: 0, ct: TestContext.Current.CancellationToken); // DELETE newly-created entity await store.DeleteAsync( "new", expectedVersion: 0, DeleteMode.Hard, - DateTimeOffset.UtcNow); + DateTimeOffset.UtcNow, ct: TestContext.Current.CancellationToken); atomic.Rollback(); var restoredExisting = - await store.GetAsync("existing"); + await store.GetAsync("existing", ct: TestContext.Current.CancellationToken); restoredExisting.Should().NotBeNull(); restoredExisting!.Value.Should().Be("original"); restoredExisting.Version.Should().Be(0); - (await store.ExistsAsync("new")) + (await store.ExistsAsync("new", ct: TestContext.Current.CancellationToken)) .Should() .BeFalse(); } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Policies/AccessPoliciesTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Policies/AccessPoliciesTests.cs index 4889b9a5..cfb317dd 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Policies/AccessPoliciesTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Policies/AccessPoliciesTests.cs @@ -44,7 +44,7 @@ public async Task Decide_UnauthenticatedActor_ShouldDeny() var context = TestAccessContext.WithAction("users.get.self"); - var result = await sut.DecideAsync(context); + var result = await sut.DecideAsync(context, ct: TestContext.Current.CancellationToken); result.IsAllowed.Should().BeFalse(); result.DenyReason.Should().Be("unauthenticated"); @@ -59,7 +59,7 @@ public async Task Decide_AuthenticatedActor_ShouldAllow() UserKey.New(), "users.get.self"); - var result = await sut.DecideAsync(context); + var result = await sut.DecideAsync(context, ct: TestContext.Current.CancellationToken); result.IsAllowed.Should().BeTrue(); } @@ -85,7 +85,7 @@ public async Task Decide_SameTenant_ShouldAllow() var context = TestAccessContext.ForUser(UserKey.New(), "users.get.self"); - var result = await sut.DecideAsync(context); + var result = await sut.DecideAsync(context, ct: TestContext.Current.CancellationToken); result.IsAllowed.Should().BeTrue(); } @@ -112,7 +112,7 @@ public async Task Decide_CrossTenant_ShouldDeny() clientProfile: UAuthClientProfile.NotSpecified, attributes: EmptyAttributes.Instance); - var result = await sut.DecideAsync(context); + var result = await sut.DecideAsync(context, ct: TestContext.Current.CancellationToken); result.IsAllowed.Should().BeFalse(); result.DenyReason.Should().Be("cross_tenant_access_denied"); @@ -153,7 +153,7 @@ public async Task Decide_UnauthenticatedActor_ShouldDeny() var context = TestAccessContext.WithAction("users.update.self"); - var result = await sut.DecideAsync(context); + var result = await sut.DecideAsync(context, ct: TestContext.Current.CancellationToken); result.IsAllowed.Should().BeFalse(); result.DenyReason.Should().Be("unauthenticated"); @@ -171,7 +171,7 @@ public async Task Decide_WhenActorIsTarget_ShouldAllow() userKey, "users.update.self"); - var result = await sut.DecideAsync(context); + var result = await sut.DecideAsync(context, ct: TestContext.Current.CancellationToken); result.IsAllowed.Should().BeTrue(); result.DenyReason.Should().BeNull(); @@ -191,7 +191,7 @@ public async Task Decide_WhenActorIsNotTarget_ShouldDeny() target, "users.update.self"); - var result = await sut.DecideAsync(context); + var result = await sut.DecideAsync(context, ct: TestContext.Current.CancellationToken); result.IsAllowed.Should().BeFalse(); result.DenyReason.Should().Be("not_self"); @@ -232,7 +232,7 @@ public async Task Decide_NormalActor_ShouldDeny() var context = TestAccessContext.WithAction("users.repair.system"); - var result = await sut.DecideAsync(context); + var result = await sut.DecideAsync(context, ct: TestContext.Current.CancellationToken); result.IsAllowed.Should().BeFalse(); result.DenyReason.Should().Be("system_actor_required"); @@ -257,7 +257,7 @@ public async Task Decide_SystemActor_ShouldAllow() clientProfile: UAuthClientProfile.NotSpecified, attributes: EmptyAttributes.Instance); - var result = await sut.DecideAsync(context); + var result = await sut.DecideAsync(context, ct: TestContext.Current.CancellationToken); result.IsAllowed.Should().BeTrue(); result.DenyReason.Should().BeNull(); @@ -344,7 +344,7 @@ public async Task Decide_UnauthenticatedActor_ShouldDeny() var context = TestAccessContext.WithAction("users.update.admin"); - var result = await sut.DecideAsync(context); + var result = await sut.DecideAsync(context, ct: TestContext.Current.CancellationToken); result.IsAllowed.Should().BeFalse(); result.DenyReason.Should().Be("unauthenticated"); @@ -363,7 +363,7 @@ public async Task Decide_AdminModifyingOwnAccount_ShouldDeny() userKey, "users.update.admin"); - var result = await sut.DecideAsync(context); + var result = await sut.DecideAsync(context, ct: TestContext.Current.CancellationToken); result.IsAllowed.Should().BeFalse(); result.DenyReason.Should() @@ -383,7 +383,7 @@ public async Task Decide_AdminDeletingOwnAccount_ShouldDeny() userKey, "users.delete.admin"); - var result = await sut.DecideAsync(context); + var result = await sut.DecideAsync(context, ct: TestContext.Current.CancellationToken); result.IsAllowed.Should().BeFalse(); result.DenyReason.Should() @@ -404,7 +404,7 @@ public async Task Decide_AdminModifyingDifferentUser_ShouldAllow() target, "users.update.admin"); - var result = await sut.DecideAsync(context); + var result = await sut.DecideAsync(context, ct: TestContext.Current.CancellationToken); result.IsAllowed.Should().BeTrue(); result.DenyReason.Should().BeNull(); @@ -511,7 +511,7 @@ public async Task Decide_ShouldDelegateToInnerPolicy() var context = TestAccessContext.WithAction("users.get.self"); - var result = await sut.DecideAsync(context); + var result = await sut.DecideAsync(context, ct: TestContext.Current.CancellationToken); result.IsAllowed.Should().BeFalse(); result.DenyReason.Should().Be("inner_denied"); @@ -534,7 +534,7 @@ public async Task Decide_ShouldReturnInnerAllowDecision() var context = TestAccessContext.WithAction("users.get.self"); - var result = await sut.DecideAsync(context); + var result = await sut.DecideAsync(context, ct: TestContext.Current.CancellationToken); result.IsAllowed.Should().BeTrue(); inner.DecideCallCount.Should().Be(1); @@ -619,7 +619,7 @@ public async Task Decide_WhenPermissionsAttributeIsMissing_ShouldDeny() var context = TestAccessContext.WithAction("users.update.admin"); - var result = await sut.DecideAsync(context); + var result = await sut.DecideAsync(context, ct: TestContext.Current.CancellationToken); result.IsAllowed.Should().BeFalse(); result.DenyReason.Should().Be("missing_permission"); @@ -632,7 +632,7 @@ public async Task Decide_WhenPermissionsAttributeHasWrongType_ShouldDeny() var context = TestAccessContext.WithAction("users.update.admin") .WithAttribute(UAuthConstants.Access.Permissions, "invalid-permissions"); - var result = await sut.DecideAsync(context); + var result = await sut.DecideAsync(context, ct: TestContext.Current.CancellationToken); result.IsAllowed.Should().BeFalse(); result.DenyReason.Should().Be("missing_permission"); @@ -654,7 +654,7 @@ public async Task Decide_WhenPermissionAllowsAction_ShouldAllow() UAuthConstants.Access.Permissions, permissions); - var result = await sut.DecideAsync(context); + var result = await sut.DecideAsync(context, ct: TestContext.Current.CancellationToken); result.IsAllowed.Should().BeTrue(); } @@ -674,7 +674,7 @@ public async Task Decide_WhenPermissionDoesNotAllowAction_ShouldDeny() UAuthConstants.Access.Permissions, permissions); - var result = await sut.DecideAsync(context); + var result = await sut.DecideAsync(context, ct: TestContext.Current.CancellationToken); result.IsAllowed.Should().BeFalse(); result.DenyReason.Should().Be("missing_permission"); @@ -695,7 +695,7 @@ public async Task Decide_WhenPermissionAllowsExactAction_ShouldAllow() UAuthConstants.Access.Permissions, permissions); - var result = await sut.DecideAsync(context); + var result = await sut.DecideAsync(context, ct: TestContext.Current.CancellationToken); result.IsAllowed.Should().BeTrue(); result.DenyReason.Should().BeNull(); @@ -781,7 +781,7 @@ public async Task Decide_WhenActorIsMissing_ShouldDeny() var context = TestAccessContext.WithAction("users.get.self"); - var result = await sut.DecideAsync(context); + var result = await sut.DecideAsync(context, ct: TestContext.Current.CancellationToken); result.IsAllowed.Should().BeFalse(); result.DenyReason.Should().Be("missing_actor"); @@ -801,7 +801,7 @@ public async Task Decide_WhenRuntimeStateIsMissing_ShouldDeny() var context = TestAccessContext.ForUser(UserKey.New(), "users.get.self"); - var result = await sut.DecideAsync(context); + var result = await sut.DecideAsync(context, ct: TestContext.Current.CancellationToken); result.IsAllowed.Should().BeFalse(); result.DenyReason.Should().Be("user_not_found"); @@ -822,7 +822,7 @@ public async Task Decide_WhenUserDoesNotExist_ShouldDeny() var context = TestAccessContext.ForUser(UserKey.New(), "users.get.self"); - var result = await sut.DecideAsync(context); + var result = await sut.DecideAsync(context, ct: TestContext.Current.CancellationToken); result.IsAllowed.Should().BeFalse(); result.DenyReason.Should().Be("user_not_found"); @@ -845,7 +845,7 @@ public async Task Decide_WhenUserIsDeleted_ShouldDeny() UserKey.New(), "users.get.self"); - var result = await sut.DecideAsync(context); + var result = await sut.DecideAsync(context, ct: TestContext.Current.CancellationToken); result.IsAllowed.Should().BeFalse(); result.DenyReason.Should().Be("user_not_found"); @@ -868,7 +868,7 @@ public async Task Decide_WhenUserIsInactive_ShouldDeny() UserKey.New(), "users.get.self"); - var result = await sut.DecideAsync(context); + var result = await sut.DecideAsync(context, ct: TestContext.Current.CancellationToken); result.IsAllowed.Should().BeFalse(); result.DenyReason.Should().Be("user_not_active"); @@ -891,7 +891,7 @@ public async Task Decide_WhenUserIsActive_ShouldAllow() UserKey.New(), "users.get.self"); - var result = await sut.DecideAsync(context); + var result = await sut.DecideAsync(context, ct: TestContext.Current.CancellationToken); result.IsAllowed.Should().BeTrue(); result.DenyReason.Should().BeNull(); @@ -915,7 +915,7 @@ public async Task Decide_ShouldQueryRuntimeUsingActorTenantAndUserKey() var sut = new RequireActiveUserPolicy(runtime); var context = TestAccessContext.ForUser(userKey, "users.get.self", tenant); - var result = await sut.DecideAsync(context); + var result = await sut.DecideAsync(context, ct: TestContext.Current.CancellationToken); runtime.LastTenant.Should().Be(tenant); runtime.LastUserKey.Should().Be(userKey); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/ResourceApi/RemoteSessionValidatorTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/ResourceApi/RemoteSessionValidatorTests.cs index 2b184062..040f6718 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/ResourceApi/RemoteSessionValidatorTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/ResourceApi/RemoteSessionValidatorTests.cs @@ -32,7 +32,7 @@ public async Task ValidateSessionAsync_ShouldPostSessionAndTenantToValidationEnd var context = CreateValidationContext(); - await sut.ValidateSessionAsync(context); + await sut.ValidateSessionAsync(context, ct: TestContext.Current.CancellationToken); handler.Method.Should().Be(HttpMethod.Post); @@ -74,7 +74,7 @@ public async Task ValidateSessionAsync_WhenIncomingRequestContainsCookie_ShouldF httpContext); await sut.ValidateSessionAsync( - CreateValidationContext()); + CreateValidationContext(), ct: TestContext.Current.CancellationToken); handler.Cookie.Should() .Be("uauth_session=session-cookie; other=value"); @@ -91,7 +91,7 @@ public async Task ValidateSessionAsync_WhenIncomingRequestDoesNotContainCookie_S var sut = CreateSut(handler); await sut.ValidateSessionAsync( - CreateValidationContext()); + CreateValidationContext(), ct: TestContext.Current.CancellationToken); handler.Cookie.Should().BeNull(); } @@ -116,7 +116,7 @@ public async Task ValidateSessionAsync_WhenHubReturnsNonSuccess_ShouldReturnNotF var context = CreateValidationContext(); var result = - await sut.ValidateSessionAsync(context); + await sut.ValidateSessionAsync(context, ct: TestContext.Current.CancellationToken); result.IsValid.Should().BeFalse(); result.State.Should().Be(SessionState.NotFound); @@ -141,7 +141,7 @@ public async Task ValidateSessionAsync_WhenHubReturnsJsonNull_ShouldReturnNotFou var context = CreateValidationContext(); var result = - await sut.ValidateSessionAsync(context); + await sut.ValidateSessionAsync(context, ct: TestContext.Current.CancellationToken); result.IsValid.Should().BeFalse(); result.State.Should().Be(SessionState.NotFound); @@ -171,7 +171,7 @@ public async Task ValidateSessionAsync_WhenHubReturnsInvalidResult_ShouldMapResu var result = await sut.ValidateSessionAsync( - CreateValidationContext()); + CreateValidationContext(), ct: TestContext.Current.CancellationToken); result.IsValid.Should().BeFalse(); result.State.Should().Be(SessionState.Revoked); @@ -249,7 +249,7 @@ public async Task ValidateSessionAsync_WhenHubReturnsValidSnapshot_ShouldReturnA var sut = CreateSut(handler); var result = await sut.ValidateSessionAsync( - CreateValidationContext()); + CreateValidationContext(), ct: TestContext.Current.CancellationToken); result.IsValid.Should().BeTrue(); result.State.Should().Be(SessionState.Active); @@ -294,7 +294,7 @@ public async Task ValidateSessionAsync_WhenHubReturnsValidWithoutIdentity_Should var sut = CreateSut(handler); var result = await sut.ValidateSessionAsync( - CreateValidationContext()); + CreateValidationContext(), ct: TestContext.Current.CancellationToken); result.IsValid.Should().BeFalse(); } @@ -322,7 +322,7 @@ public async Task ValidateSessionAsync_WhenHubReturnsValidWithoutSnapshot_Should var sut = CreateSut(handler); var result = await sut.ValidateSessionAsync( - CreateValidationContext()); + CreateValidationContext(), ct: TestContext.Current.CancellationToken); result.IsValid.Should().BeFalse(); } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/AccessContextFactoryTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/AccessContextFactoryTests.cs index 4f627fb1..38023698 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/AccessContextFactoryTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/AccessContextFactoryTests.cs @@ -89,7 +89,7 @@ public async Task CreateAsync_ShouldResolveTrustedActorRoles() var context = await sut.CreateAsync( flow, "users.get.self", - "users"); + "users", ct: TestContext.Current.CancellationToken); // Only active roles are exposed. context.ActorRoles.Should().HaveCount(2); @@ -161,7 +161,7 @@ public async Task CreateAsync_ShouldIgnoreUntrustedRolesForAnonymousActor() flow, "users.get.anonymous", "users", - attributes: attributes); + attributes: attributes, ct: TestContext.Current.CancellationToken); // Anonymous actors must never receive trusted roles. context.ActorRoles.Should().BeEmpty(); @@ -203,7 +203,7 @@ public async Task CreateForUserTargetsAsync_ShouldNormalizeAndPreserveTargets() flow, "users.getbykeys.admin", "users", - new[] { first, second, first }); + new[] { first, second, first }, ct: TestContext.Current.CancellationToken); context.TargetUserKeys.Should().HaveCount(2); context.TargetUserKeys.Should().ContainInOrder(first, second); @@ -236,7 +236,7 @@ public async Task CreateForUserTargetsAsync_ShouldSnapshotInputCollection() flow, "users.getbykeys.admin", "users", - input); + input, ct: TestContext.Current.CancellationToken); input.Add(second); @@ -263,7 +263,7 @@ public async Task CreateForUserTargetsAsync_WithAttribute_ShouldPreserveTargets( flow, "users.getbykeys.admin", "users", - new[] { first, second }); + new[] { first, second }, ct: TestContext.Current.CancellationToken); var updated = context.WithAttribute("source", "test"); @@ -289,7 +289,7 @@ public async Task CreateAsync_SingleTargetContext_ShouldHaveEmptyTargetUserKeys( var context = await sut.CreateAsync( flow, "users.query.admin", - "users"); + "users", ct: TestContext.Current.CancellationToken); context.TargetUserKeys.Should().BeEmpty(); } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/AccessPipelineTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/AccessPipelineTests.cs index b7ffd6e5..37160702 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/AccessPipelineTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/AccessPipelineTests.cs @@ -30,7 +30,7 @@ public async Task Allowed_pipeline_should_execute_command_once() await pipeline.Orchestrator.ExecuteAsync( TestAccessContext.WithAction("users.read"), - command); + command, ct: TestContext.Current.CancellationToken); executions.Should().Be(1); } @@ -49,7 +49,7 @@ public async Task Generic_pipeline_should_return_command_result() var result = await pipeline.Orchestrator.ExecuteAsync( TestAccessContext.WithAction("users.read"), - command); + command, ct: TestContext.Current.CancellationToken); result.Should().Be("expected-result"); executions.Should().Be(1); @@ -169,7 +169,7 @@ await pipeline.Orchestrator.ExecuteAsync( { executed = true; return Task.CompletedTask; - })); + }), ct: TestContext.Current.CancellationToken); executed.Should().BeTrue(); @@ -317,7 +317,7 @@ public async Task Runtime_reauthentication_should_be_reported_by_authority() var decision = await authority.DecideAsync( TestAccessContext.WithAction("users.update"), - new[] { first.Object, second.Object }); + new[] { first.Object, second.Object }, ct: TestContext.Current.CancellationToken); decision.RequiresReauthentication.Should().BeTrue(); decision.IsAllowed.Should().BeFalse(); @@ -340,7 +340,7 @@ public async Task Runtime_denial_should_take_priority_over_earlier_reauthenticat var decision = await authority.DecideAsync( TestAccessContext.WithAction("users.update"), - new[] { reauth.Object, deny.Object }); + new[] { reauth.Object, deny.Object }, ct: TestContext.Current.CancellationToken); decision.IsDenied.Should().BeTrue(); decision.DenyReason.Should().Be("explicit_denial"); @@ -397,7 +397,7 @@ public async Task Authenticated_actor_should_receive_compiled_permissions() await pipeline.Orchestrator.ExecuteAsync( context, - new AccessCommand(_ => Task.CompletedTask)); + new AccessCommand(_ => Task.CompletedTask), ct: TestContext.Current.CancellationToken); captured.Should().NotBeNull(); captured.Should().NotBeSameAs(context); @@ -454,7 +454,7 @@ public async Task Authority_should_receive_same_enriched_context_as_policy_provi await pipeline.Orchestrator.ExecuteAsync( context, - new AccessCommand(_ => Task.CompletedTask)); + new AccessCommand(_ => Task.CompletedTask), ct: TestContext.Current.CancellationToken); providerContext.Should().NotBeNull(); authorityContext.Should().BeSameAs(providerContext); @@ -476,7 +476,7 @@ public async Task Anonymous_actor_should_skip_permission_store() await pipeline.Orchestrator.ExecuteAsync( context, - new AccessCommand(_ => Task.CompletedTask)); + new AccessCommand(_ => Task.CompletedTask), ct: TestContext.Current.CancellationToken); pipeline.PermissionStore.Verify( s => s.GetPermissionsAsync( @@ -615,7 +615,7 @@ public async Task Authority_should_allow_when_no_policies_exist() var decision = await authority.DecideAsync( TestAccessContext.WithAction("users.delete"), - Array.Empty()); + Array.Empty(), ct: TestContext.Current.CancellationToken); decision.IsAllowed.Should().BeTrue(); } @@ -629,7 +629,7 @@ public async Task Authority_should_treat_null_global_collections_as_empty() var decision = await authority.DecideAsync( TestAccessContext.WithAction("users.read"), - Array.Empty()); + Array.Empty(), ct: TestContext.Current.CancellationToken); decision.IsAllowed.Should().BeTrue(); } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/AuthAuthorityTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/AuthAuthorityTests.cs index d6c0eaba..fe29c217 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/AuthAuthorityTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/AuthAuthorityTests.cs @@ -16,7 +16,7 @@ public async Task DecideAsync_WithLegacyInvariant_ShouldWork() new IAuthorityInvariant[] { invariant }, Array.Empty()); - var result = await authority.DecideAsync(CreateContext()); + var result = await authority.DecideAsync(CreateContext(), ct: TestContext.Current.CancellationToken); result.Decision.Should().Be(AuthorizationDecision.Deny); result.Reason.Should().Be("legacy_invariant_denied"); @@ -32,7 +32,7 @@ public async Task DecideAsync_ShouldAwaitAsyncPolicy() Array.Empty(), new IAuthorityPolicy[] { policy }); - var evaluation = authority.DecideAsync(CreateContext()); + var evaluation = authority.DecideAsync(CreateContext(), ct: TestContext.Current.CancellationToken); // Ensure the policy has started evaluating. await policy.Started; diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/AuthenticationSecurityManagerTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/AuthenticationSecurityManagerTests.cs index 2e7664b9..af993204 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/AuthenticationSecurityManagerTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/AuthenticationSecurityManagerTests.cs @@ -60,7 +60,7 @@ public async Task GetOrCreateAccountAsync_WhenStateExists_ShouldReturnExistingSt var result = await _sut.GetOrCreateAccountAsync( _tenant, - _userKey); + _userKey, ct: TestContext.Current.CancellationToken); result.Should().BeSameAs(existing); @@ -94,7 +94,7 @@ public async Task GetOrCreateAccountAsync_WhenStateDoesNotExist_ShouldCreateStat var result = await _sut.GetOrCreateAccountAsync( _tenant, - _userKey); + _userKey, ct: TestContext.Current.CancellationToken); result.Should().NotBeNull(); result.Tenant.Should().Be(_tenant); @@ -139,7 +139,7 @@ public async Task GetOrCreateAccountAsync_WhenConcurrentCreationOccurs_ShouldRet var result = await _sut.GetOrCreateAccountAsync( _tenant, - _userKey); + _userKey, ct: TestContext.Current.CancellationToken); result.Should().BeSameAs(existing); @@ -203,7 +203,7 @@ public async Task GetOrCreateFactorAsync_WhenStateExists_ShouldReturnExistingSta var result = await _sut.GetOrCreateFactorAsync( _tenant, _userKey, - CredentialType.Password); + CredentialType.Password, ct: TestContext.Current.CancellationToken); result.Should().BeSameAs(existing); @@ -238,7 +238,7 @@ public async Task GetOrCreateFactorAsync_WhenStateDoesNotExist_ShouldCreateCorre var result = await _sut.GetOrCreateFactorAsync( _tenant, _userKey, - CredentialType.Password); + CredentialType.Password, ct: TestContext.Current.CancellationToken); result.Tenant.Should().Be(_tenant); result.UserKey.Should().Be(_userKey); @@ -287,7 +287,7 @@ public async Task GetOrCreateFactorAsync_WhenConcurrentCreationOccurs_ShouldRetu var result = await _sut.GetOrCreateFactorAsync( _tenant, _userKey, - CredentialType.Password); + CredentialType.Password, ct: TestContext.Current.CancellationToken); result.Should().BeSameAs(existing); } @@ -315,7 +315,7 @@ public async Task MutateAccountAsync_WhenMutationReturnsSameInstance_ShouldNotUp var result = await _sut.MutateAccountAsync( _tenant, _userKey, - state => state); + state => state, ct: TestContext.Current.CancellationToken); result.Should().BeSameAs(existing); @@ -355,7 +355,7 @@ public async Task MutateAccountAsync_WhenUpdateSucceeds_ShouldUseCurrentSecurity var result = await _sut.MutateAccountAsync( _tenant, _userKey, - _ => updated); + _ => updated, ct: TestContext.Current.CancellationToken); result.Should().BeSameAs(updated); @@ -425,7 +425,7 @@ public async Task MutateAccountAsync_WhenUpdateConflicts_ShouldReloadAndReapplyM { mutationCall++; return CreateMutatedState(state); - }); + }, ct: TestContext.Current.CancellationToken); result.Should().NotBeNull(); @@ -506,7 +506,7 @@ public async Task MutateFactorAsync_WhenMutationReturnsSameInstance_ShouldNotUpd _tenant, _userKey, CredentialType.Password, - state => state); + state => state, ct: TestContext.Current.CancellationToken); result.Should().BeSameAs(existing); @@ -568,7 +568,7 @@ public async Task MutateFactorAsync_WhenUpdateConflicts_ShouldReloadAndReapplyMu { mutationCalls++; return CreateMutatedState(state); - }); + }, ct: TestContext.Current.CancellationToken); result.Should().NotBeNull(); @@ -601,7 +601,7 @@ public async Task UpdateAsync_ShouldResolveStoreUsingUpdatedStateTenant() await _sut.UpdateAsync( state, - expectedVersion: 7); + expectedVersion: 7, ct: TestContext.Current.CancellationToken); _storeFactory.Verify( x => x.Create(otherTenant), @@ -632,7 +632,7 @@ await _sut.DeleteAsync( otherTenant, _userKey, AuthenticationSecurityScope.Factor, - CredentialType.Password); + CredentialType.Password, ct: TestContext.Current.CancellationToken); _storeFactory.Verify( x => x.Create(otherTenant), diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/HubCredentialResolverTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/HubCredentialResolverTests.cs index 98ad80d4..807bc3e1 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/HubCredentialResolverTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/HubCredentialResolverTests.cs @@ -27,7 +27,7 @@ public async Task ResolveAsync_WhenArtifactDoesNotExist_ShouldReturnNull() It.IsAny())) .ReturnsAsync((AuthArtifact?)null); - var result = await fixture.Sut.ResolveAsync(hubSessionId); + var result = await fixture.Sut.ResolveAsync(hubSessionId, ct: TestContext.Current.CancellationToken); result.Should().BeNull(); } @@ -47,7 +47,7 @@ public async Task ResolveAsync_WhenArtifactIsNotHubFlow_ShouldReturnNull() It.IsAny())) .ReturnsAsync(artifact); - var result = await fixture.Sut.ResolveAsync(hubSessionId); + var result = await fixture.Sut.ResolveAsync(hubSessionId, ct: TestContext.Current.CancellationToken); result.Should().BeNull(); } @@ -63,7 +63,7 @@ public async Task ResolveAsync_WhenFlowIsCompleted_ShouldReturnNull() SetupArtifact(fixture, artifact); var result = await fixture.Sut.ResolveAsync( - artifact.HubSessionId); + artifact.HubSessionId, ct: TestContext.Current.CancellationToken); result.Should().BeNull(); } @@ -81,7 +81,7 @@ public async Task ResolveAsync_WhenAuthorizationCodeIsMissing_ShouldReturnNull() SetupArtifact(fixture, artifact); var result = await fixture.Sut.ResolveAsync( - artifact.HubSessionId); + artifact.HubSessionId, ct: TestContext.Current.CancellationToken); result.Should().BeNull(); } @@ -100,7 +100,7 @@ public async Task ResolveAsync_WhenAuthorizationCodeIsEmpty_ShouldReturnNull() SetupArtifact(fixture, artifact); var result = await fixture.Sut.ResolveAsync( - artifact.HubSessionId); + artifact.HubSessionId, ct: TestContext.Current.CancellationToken); result.Should().BeNull(); } @@ -118,7 +118,7 @@ public async Task ResolveAsync_WhenCodeVerifierIsMissing_ShouldReturnNull() SetupArtifact(fixture, artifact); var result = await fixture.Sut.ResolveAsync( - artifact.HubSessionId); + artifact.HubSessionId, ct: TestContext.Current.CancellationToken); result.Should().BeNull(); } @@ -137,7 +137,7 @@ public async Task ResolveAsync_WhenCodeVerifierIsEmpty_ShouldReturnNull() SetupArtifact(fixture, artifact); var result = await fixture.Sut.ResolveAsync( - artifact.HubSessionId); + artifact.HubSessionId, ct: TestContext.Current.CancellationToken); result.Should().BeNull(); } @@ -153,7 +153,7 @@ public async Task ResolveAsync_WhenCredentialsAreValid_ShouldReturnCredentials() SetupArtifact(fixture, artifact); var result = await fixture.Sut.ResolveAsync( - artifact.HubSessionId); + artifact.HubSessionId, ct: TestContext.Current.CancellationToken); result.Should().NotBeNull(); @@ -171,7 +171,7 @@ public async Task ResolveAsync_ShouldReadArtifactUsingHubSessionId() SetupArtifact(fixture, artifact); await fixture.Sut.ResolveAsync( - artifact.HubSessionId); + artifact.HubSessionId, ct: TestContext.Current.CancellationToken); fixture.Store.Verify( x => x.GetAsync( @@ -218,7 +218,7 @@ public async Task ResolveAsync_WhenFlowIsExpired_ShouldReturnNull() SetupArtifact(fixture, artifact); var result = await fixture.Sut.ResolveAsync( - artifact.HubSessionId); + artifact.HubSessionId, ct: TestContext.Current.CancellationToken); result.Should().BeNull(); } @@ -234,7 +234,7 @@ public async Task ResolveAsync_WhenFlowExpiresExactlyNow_ShouldReturnNull() SetupArtifact(fixture, artifact); var result = await fixture.Sut.ResolveAsync( - artifact.HubSessionId); + artifact.HubSessionId, ct: TestContext.Current.CancellationToken); result.Should().BeNull(); } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/HubFlowReaderTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/HubFlowReaderTests.cs index a32b437e..f891464d 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/HubFlowReaderTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/HubFlowReaderTests.cs @@ -34,7 +34,7 @@ public async Task GetStateAsync_WhenArtifactDoesNotExist_ShouldReturnNull() It.IsAny())) .ReturnsAsync((AuthArtifact?)null); - var result = await fixture.Sut.GetStateAsync(hubSessionId); + var result = await fixture.Sut.GetStateAsync(hubSessionId, ct: TestContext.Current.CancellationToken); result.Should().BeNull(); } @@ -54,7 +54,7 @@ public async Task GetStateAsync_WhenArtifactIsNotHubFlow_ShouldReturnNull() new TestAuthArtifact( Now.AddMinutes(5))); - var result = await fixture.Sut.GetStateAsync(hubSessionId); + var result = await fixture.Sut.GetStateAsync(hubSessionId, ct: TestContext.Current.CancellationToken); result.Should().BeNull(); } @@ -74,7 +74,7 @@ public async Task GetStateAsync_WhenFlowIsActive_ShouldReturnActiveState() SetupArtifact(fixture, artifact); var result = await fixture.Sut.GetStateAsync( - artifact.HubSessionId); + artifact.HubSessionId, ct: TestContext.Current.CancellationToken); result.Should().NotBeNull(); @@ -99,7 +99,7 @@ public async Task GetStateAsync_WhenFlowIsExpired_ShouldReturnExpiredState() SetupArtifact(fixture, artifact); var result = await fixture.Sut.GetStateAsync( - artifact.HubSessionId); + artifact.HubSessionId, ct: TestContext.Current.CancellationToken); result.Should().NotBeNull(); @@ -120,7 +120,7 @@ public async Task GetStateAsync_WhenFlowExpiresExactlyNow_ShouldReturnExpiredSta SetupArtifact(fixture, artifact); var result = await fixture.Sut.GetStateAsync( - artifact.HubSessionId); + artifact.HubSessionId, ct: TestContext.Current.CancellationToken); result.Should().NotBeNull(); @@ -147,7 +147,7 @@ public async Task GetStateAsync_WhenFlowIsCompleted_ShouldReturnCompletedState() SetupArtifact(fixture, artifact); var result = await fixture.Sut.GetStateAsync( - artifact.HubSessionId); + artifact.HubSessionId, ct: TestContext.Current.CancellationToken); result.Should().NotBeNull(); @@ -170,7 +170,7 @@ public async Task GetStateAsync_WhenFlowIsExpiredAndCompleted_ShouldPreserveBoth SetupArtifact(fixture, artifact); var result = await fixture.Sut.GetStateAsync( - artifact.HubSessionId); + artifact.HubSessionId, ct: TestContext.Current.CancellationToken); result.Should().NotBeNull(); @@ -197,7 +197,7 @@ public async Task GetStateAsync_ShouldMapHubFlowState() SetupArtifact(fixture, artifact); var result = await fixture.Sut.GetStateAsync( - artifact.HubSessionId); + artifact.HubSessionId, ct: TestContext.Current.CancellationToken); result.Should().NotBeNull(); @@ -234,7 +234,7 @@ public async Task GetStateAsync_ShouldMapErrorAndAttemptCount() SetupArtifact(fixture, artifact); var result = await fixture.Sut.GetStateAsync( - artifact.HubSessionId); + artifact.HubSessionId, ct: TestContext.Current.CancellationToken); result.Should().NotBeNull(); @@ -260,7 +260,7 @@ public async Task GetStateAsync_ShouldPreserveMultipleAttempts() SetupArtifact(fixture, artifact); var result = await fixture.Sut.GetStateAsync( - artifact.HubSessionId); + artifact.HubSessionId, ct: TestContext.Current.CancellationToken); result.Should().NotBeNull(); @@ -282,7 +282,7 @@ public async Task GetStateAsync_ShouldReadArtifactUsingHubSessionId() SetupArtifact(fixture, artifact); await fixture.Sut.GetStateAsync( - artifact.HubSessionId); + artifact.HubSessionId, ct: TestContext.Current.CancellationToken); fixture.Store.Verify( x => x.GetAsync( diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/HubFlowServiceTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/HubFlowServiceTests.cs index 41fe0264..334623f6 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/HubFlowServiceTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/HubFlowServiceTests.cs @@ -53,7 +53,7 @@ public async Task BeginLoginAsync_ShouldCreateAndStoreHubFlowArtifact() }) .Returns(Task.CompletedTask); - var result = await fixture.Sut.BeginLoginAsync(request); + var result = await fixture.Sut.BeginLoginAsync(request, ct: TestContext.Current.CancellationToken); result.Should().NotBeNull(); result.HubSessionId.Should().NotBeNullOrWhiteSpace(); @@ -96,7 +96,7 @@ public async Task BeginLoginAsync_ShouldStoreAuthorizationCodeAndCodeVerifier() .Returns(Task.CompletedTask); await fixture.Sut.BeginLoginAsync( - CreateBeginRequest()); + CreateBeginRequest(), ct: TestContext.Current.CancellationToken); captured.Should().NotBeNull(); @@ -129,7 +129,7 @@ public async Task BeginLoginAsync_ShouldSetExpirationFromClockAndConfiguredLifet .Returns(Task.CompletedTask); await fixture.Sut.BeginLoginAsync( - CreateBeginRequest()); + CreateBeginRequest(), ct: TestContext.Current.CancellationToken); captured.Should().NotBeNull(); captured!.ExpiresAt.Should().Be(Now.Add(FlowLifetime)); @@ -145,7 +145,7 @@ public async Task BeginLoginAsync_WhenPreviousHubSessionIdExists_ShouldConsumePr var request = CreateBeginRequest( previousHubSessionId: previousId); - await fixture.Sut.BeginLoginAsync(request); + await fixture.Sut.BeginLoginAsync(request, ct: TestContext.Current.CancellationToken); fixture.Store.Verify( x => x.ConsumeAsync( @@ -167,7 +167,7 @@ public async Task BeginLoginAsync_WhenPreviousHubSessionIdIsMissing_ShouldNotCon var request = CreateBeginRequest( previousHubSessionId: previousHubSessionId); - await fixture.Sut.BeginLoginAsync(request); + await fixture.Sut.BeginLoginAsync(request, ct: TestContext.Current.CancellationToken); fixture.Store.Verify( x => x.ConsumeAsync( @@ -234,7 +234,7 @@ public async Task ContinuePkceAsync_ShouldReplacePkceCredentials() await fixture.Sut.ContinuePkceAsync( artifact.HubSessionId.Value, newAuthorizationCode, - newCodeVerifier); + newCodeVerifier, ct: TestContext.Current.CancellationToken); artifact.Payload .GetRequired("authorization_code") @@ -264,7 +264,7 @@ public async Task ContinuePkceAsync_ShouldClearExistingError() await fixture.Sut.ContinuePkceAsync( artifact.HubSessionId.Value, "new-code", - "new-verifier"); + "new-verifier", ct: TestContext.Current.CancellationToken); artifact.Error.Should().BeNull(); @@ -285,7 +285,7 @@ public async Task ContinuePkceAsync_ShouldStoreUpdatedArtifact() await fixture.Sut.ContinuePkceAsync( artifact.HubSessionId.Value, "new-code", - "new-verifier"); + "new-verifier", ct: TestContext.Current.CancellationToken); fixture.Store.Verify( x => x.StoreAsync( @@ -527,7 +527,7 @@ public async Task ConsumeAsync_WhenHubSessionIdIsEmpty_ShouldDoNothing( { var fixture = CreateFixture(); - await fixture.Sut.ConsumeAsync(hubSessionId); + await fixture.Sut.ConsumeAsync(hubSessionId, ct: TestContext.Current.CancellationToken); fixture.Store.Verify( x => x.ConsumeAsync( @@ -543,7 +543,7 @@ public async Task ConsumeAsync_WhenHubSessionIdExists_ShouldConsumeArtifact() const string hubSessionId = "hub-session"; - await fixture.Sut.ConsumeAsync(hubSessionId); + await fixture.Sut.ConsumeAsync(hubSessionId, ct: TestContext.Current.CancellationToken); fixture.Store.Verify( x => x.ConsumeAsync( diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/LoginOrchestratorTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/LoginOrchestratorTests.cs index 3402774d..058f45fe 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/LoginOrchestratorTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/LoginOrchestratorTests.cs @@ -29,7 +29,7 @@ public async Task Successful_login_should_return_success_result() { Identifier = "user", Secret = "user", - }); + }, ct: TestContext.Current.CancellationToken); result.IsSuccess.Should().BeTrue(); } @@ -46,7 +46,7 @@ public async Task Successful_login_should_create_session() { Identifier = "user", Secret = "user", - }); + }, ct: TestContext.Current.CancellationToken); result.SessionId.Should().NotBeNull(); } @@ -67,11 +67,11 @@ await orchestrator.LoginAsync(flow, { Identifier = "user", Secret = "wrong", - }); + }, ct: TestContext.Current.CancellationToken); var factory = runtime.Services.GetRequiredService(); var store = factory.Create(TenantKeys.Single); - var state = await store.GetAsync(TestUsers.User, AuthenticationSecurityScope.Factor, CredentialType.Password); + var state = await store.GetAsync(TestUsers.User, AuthenticationSecurityScope.Factor, CredentialType.Password, ct: TestContext.Current.CancellationToken); state?.FailedAttempts.Should().Be(1); } @@ -91,18 +91,18 @@ await orchestrator.LoginAsync(flow, { Identifier = "user", Secret = "wrong", - }); + }, ct: TestContext.Current.CancellationToken); await orchestrator.LoginAsync(flow, new LoginRequest { Identifier = "user", Secret = "user", // valid password - }); + }, ct: TestContext.Current.CancellationToken); var factory = runtime.Services.GetRequiredService(); var store = factory.Create(TenantKeys.Single); - var state = await store.GetAsync(TestUsers.User, AuthenticationSecurityScope.Factor, CredentialType.Password); + var state = await store.GetAsync(TestUsers.User, AuthenticationSecurityScope.Factor, CredentialType.Password, ct: TestContext.Current.CancellationToken); state?.FailedAttempts.Should().Be(0); } @@ -118,7 +118,7 @@ public async Task Invalid_password_should_fail_login() { Identifier = "user", Secret = "wrong", - }); + }, ct: TestContext.Current.CancellationToken); result.IsSuccess.Should().BeFalse(); } @@ -135,7 +135,7 @@ public async Task Non_existent_user_should_fail_login_gracefully() { Identifier = "ghost", Secret = "whatever", - }); + }, ct: TestContext.Current.CancellationToken); result.IsSuccess.Should().BeFalse(); } @@ -156,11 +156,11 @@ await orchestrator.LoginAsync(flow, { Identifier = "user", Secret = "wrong", - }); + }, ct: TestContext.Current.CancellationToken); var factory = runtime.Services.GetRequiredService(); var store = factory.Create(TenantKeys.Single); - var state = await store.GetAsync(TestUsers.User, AuthenticationSecurityScope.Factor, CredentialType.Password); + var state = await store.GetAsync(TestUsers.User, AuthenticationSecurityScope.Factor, CredentialType.Password, ct: TestContext.Current.CancellationToken); state!.IsLocked(DateTimeOffset.UtcNow).Should().BeTrue(); } @@ -181,14 +181,14 @@ await orchestrator.LoginAsync(flow, { Identifier = "user", Secret = "wrong", - }); + }, ct: TestContext.Current.CancellationToken); var result = await orchestrator.LoginAsync(flow, new LoginRequest { Identifier = "user", Secret = "user", - }); + }, ct: TestContext.Current.CancellationToken); result.IsSuccess.Should().BeFalse(); } @@ -209,19 +209,19 @@ await orchestrator.LoginAsync(flow, { Identifier = "user", Secret = "wrong", - }); + }, ct: TestContext.Current.CancellationToken); var factory = runtime.Services.GetRequiredService(); var store = factory.Create(TenantKeys.Single); - var state1 = await store.GetAsync(TestUsers.User, AuthenticationSecurityScope.Factor, CredentialType.Password); + var state1 = await store.GetAsync(TestUsers.User, AuthenticationSecurityScope.Factor, CredentialType.Password, ct: TestContext.Current.CancellationToken); await orchestrator.LoginAsync(flow, new LoginRequest { Identifier = "user", - }); + }, ct: TestContext.Current.CancellationToken); - var state2 = await store.GetAsync(TestUsers.User, AuthenticationSecurityScope.Factor, CredentialType.Password); + var state2 = await store.GetAsync(TestUsers.User, AuthenticationSecurityScope.Factor, CredentialType.Password, ct: TestContext.Current.CancellationToken); state2?.FailedAttempts.Should().Be(state1!.FailedAttempts); } @@ -243,12 +243,12 @@ await orchestrator.LoginAsync(flow, { Identifier = "user", Secret = "wrong", - }); + }, ct: TestContext.Current.CancellationToken); } var factory = runtime.Services.GetRequiredService(); var store = factory.Create(TenantKeys.Single); - var state = await store.GetAsync(TestUsers.User, AuthenticationSecurityScope.Factor, CredentialType.Password); + var state = await store.GetAsync(TestUsers.User, AuthenticationSecurityScope.Factor, CredentialType.Password, ct: TestContext.Current.CancellationToken); state?.IsLocked(DateTimeOffset.UtcNow).Should().BeFalse(); state?.FailedAttempts.Should().Be(5); @@ -271,11 +271,11 @@ await orchestrator.LoginAsync(flow, { Identifier = "user", Secret = "wrong", - }); + }, ct: TestContext.Current.CancellationToken); var factory = runtime.Services.GetRequiredService(); var store = factory.Create(TenantKeys.Single); - var state1 = await store.GetAsync(TestUsers.User, AuthenticationSecurityScope.Factor, CredentialType.Password); + var state1 = await store.GetAsync(TestUsers.User, AuthenticationSecurityScope.Factor, CredentialType.Password, ct: TestContext.Current.CancellationToken); var lockedUntil = state1!.LockedUntil; @@ -284,9 +284,9 @@ await orchestrator.LoginAsync(flow, { Identifier = "user", Secret = "wrong", - }); + }, ct: TestContext.Current.CancellationToken); - var state2 = await store.GetAsync(TestUsers.User, AuthenticationSecurityScope.Factor, CredentialType.Password); + var state2 = await store.GetAsync(TestUsers.User, AuthenticationSecurityScope.Factor, CredentialType.Password, ct: TestContext.Current.CancellationToken); state2?.LockedUntil.Should().Be(lockedUntil); } @@ -311,7 +311,7 @@ public async Task Login_success_should_trigger_UserLoggedIn_event() { Identifier = "user", Secret = "user", - }); + }, ct: TestContext.Current.CancellationToken); captured.Should().NotBeNull(); captured!.UserKey.Should().Be(TestUsers.User); @@ -338,7 +338,7 @@ public async Task Login_success_should_trigger_OnAnyEvent() { Identifier = "user", Secret = "user", - }); + }, ct: TestContext.Current.CancellationToken); count.Should().BeGreaterThan(0); } @@ -358,7 +358,7 @@ public async Task Event_handler_exception_should_not_break_login_flow() { Identifier = "user", Secret = "user", - }); + }, ct: TestContext.Current.CancellationToken); result.IsSuccess.Should().BeTrue(); } @@ -395,13 +395,13 @@ public async Task Login_With_Execution_Context_Should_Create_Session_For_Overrid result.SessionId.Should().NotBeNull(); var sessionStore = sessionStoreFactory.Create(TenantKeys.Single); - var session = await sessionStore.GetSessionAsync(result.SessionId!.Value); + var session = await sessionStore.GetSessionAsync(result.SessionId!.Value, ct: TestContext.Current.CancellationToken); session.Should().NotBeNull(); session!.Device.DeviceId.Should().Be(overriddenDevice.DeviceId); var chainStore = sessionStoreFactory.Create(TenantKeys.Single); - var chain = await chainStore.GetChainByDeviceAsync(TestUsers.User, (DeviceId)overriddenDevice.DeviceId!); + var chain = await chainStore.GetChainByDeviceAsync(TestUsers.User, (DeviceId)overriddenDevice.DeviceId!, ct: TestContext.Current.CancellationToken); chain.Should().NotBeNull(); chain!.Device.DeviceId.Should().Be(overriddenDevice.DeviceId); @@ -429,7 +429,7 @@ public async Task Internal_Login_Should_Respect_LoginExecutionOptions() Identifier = "user", Secret = "user" }, - options); + options, ct: TestContext.Current.CancellationToken); result.IsSuccess.Should().BeTrue(); } @@ -438,10 +438,10 @@ public async Task Internal_Login_Should_Respect_LoginExecutionOptions() public async Task Concurrent_First_Logins_FromDifferentDevices_ShouldCreateSingleRootAndDifferentChains() { var runtime = new TestAuthRuntime(); - var user = await runtime.CreateLoginUserAsync(); + var user = await runtime.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var storeFactory = runtime.Services.GetRequiredService(); var store = storeFactory.Create(TenantKeys.Single); - var rootBefore = await store.GetActiveRootByUserAsync(user.UserKey); + var rootBefore = await store.GetActiveRootByUserAsync(user.UserKey, ct: TestContext.Current.CancellationToken); rootBefore.Should().BeNull(); @@ -472,8 +472,8 @@ public async Task Concurrent_First_Logins_FromDifferentDevices_ShouldCreateSingl Secret = user.Secret }; - var taskA = runtime.LoginAsync(flowA, executionA, requestA); - var taskB = runtime.LoginAsync(flowB, executionB, requestB); + var taskA = runtime.LoginAsync(flowA, executionA, requestA, ct: TestContext.Current.CancellationToken); + var taskB = runtime.LoginAsync(flowB, executionB, requestB, ct: TestContext.Current.CancellationToken); var results = await Task.WhenAll(taskA, taskB); @@ -486,11 +486,11 @@ public async Task Concurrent_First_Logins_FromDifferentDevices_ShouldCreateSingl var sessionA = await store.GetSessionAsync( - results[0].SessionId!.Value); + results[0].SessionId!.Value, ct: TestContext.Current.CancellationToken); var sessionB = await store.GetSessionAsync( - results[1].SessionId!.Value); + results[1].SessionId!.Value, ct: TestContext.Current.CancellationToken); sessionA.Should().NotBeNull(); sessionB.Should().NotBeNull(); @@ -500,11 +500,11 @@ await store.GetSessionAsync( var chainA = await store.GetChainAsync( - sessionA.ChainId); + sessionA.ChainId, ct: TestContext.Current.CancellationToken); var chainB = await store.GetChainAsync( - sessionB.ChainId); + sessionB.ChainId, ct: TestContext.Current.CancellationToken); chainA.Should().NotBeNull(); chainB.Should().NotBeNull(); @@ -514,7 +514,7 @@ await store.GetChainAsync( var activeRoot = await store.GetActiveRootByUserAsync( - user.UserKey); + user.UserKey, ct: TestContext.Current.CancellationToken); activeRoot.Should().NotBeNull(); @@ -534,7 +534,7 @@ public async Task Concurrent_First_Logins_FromSameDevice_ShouldCreateSingleRootA // Fresh user: no Root, Chain or Session exists. // var user = - await runtime.CreateLoginUserAsync(); + await runtime.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); var storeFactory = runtime.Services.GetRequiredService(); @@ -544,7 +544,7 @@ public async Task Concurrent_First_Logins_FromSameDevice_ShouldCreateSingleRootA var rootBefore = await store.GetActiveRootByUserAsync( - user.UserKey); + user.UserKey, ct: TestContext.Current.CancellationToken); rootBefore.Should().BeNull(); @@ -595,13 +595,13 @@ await store.GetActiveRootByUserAsync( runtime.LoginAsync( flowA, executionA, - requestA); + requestA, ct: TestContext.Current.CancellationToken); var taskB = runtime.LoginAsync( flowB, executionB, - requestB); + requestB, ct: TestContext.Current.CancellationToken); var results = await Task.WhenAll( @@ -616,11 +616,11 @@ await Task.WhenAll( var sessionA = await store.GetSessionAsync( - results[0].SessionId!.Value); + results[0].SessionId!.Value, ct: TestContext.Current.CancellationToken); var sessionB = await store.GetSessionAsync( - results[1].SessionId!.Value); + results[1].SessionId!.Value, ct: TestContext.Current.CancellationToken); sessionA.Should().NotBeNull(); sessionB.Should().NotBeNull(); @@ -633,7 +633,7 @@ await store.GetSessionAsync( var chain = await store.GetChainAsync( - sessionA.ChainId); + sessionA.ChainId, ct: TestContext.Current.CancellationToken); chain.Should().NotBeNull(); @@ -642,7 +642,7 @@ await store.GetChainAsync( // var activeRoot = await store.GetActiveRootByUserAsync( - user.UserKey); + user.UserKey, ct: TestContext.Current.CancellationToken); activeRoot.Should().NotBeNull(); @@ -700,7 +700,7 @@ public async Task Unsupported_credential_type_should_not_affect_password_lockout var state = await store.GetAsync( TestUsers.User, AuthenticationSecurityScope.Factor, - CredentialType.Password); + CredentialType.Password, ct: TestContext.Current.CancellationToken); (state?.FailedAttempts ?? 0).Should().Be(0); } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/PkceServiceTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/PkceServiceTests.cs index 845460e6..c8921eb7 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/PkceServiceTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/PkceServiceTests.cs @@ -103,7 +103,7 @@ public async Task AuthorizeAsync_ShouldStorePkceAuthorizationArtifact() .Returns(Task.CompletedTask); var result = - await fixture.Sut.AuthorizeAsync(command); + await fixture.Sut.AuthorizeAsync(command, ct: TestContext.Current.CancellationToken); capturedKey.Should().NotBeNull(); capturedArtifact.Should().NotBeNull(); @@ -182,7 +182,7 @@ public async Task CompleteAsync_WhenAuthorizationArtifactDoesNotExist_ShouldRetu var result = await fixture.Sut.CompleteAsync( auth, - request); + request, ct: TestContext.Current.CancellationToken); result.InvalidPkce.Should().BeTrue(); @@ -224,7 +224,7 @@ public async Task CompleteAsync_ShouldConsumeAuthorizationCodeUsingRequestCode() await fixture.Sut.CompleteAsync( AuthFlowTestFactory.New(), - request); + request, ct: TestContext.Current.CancellationToken); fixture.Store.Verify( x => x.ConsumeAsync( @@ -257,7 +257,7 @@ public async Task CompleteAsync_WhenPkceValidationFails_ShouldNotAttemptLogin() var result = await fixture.Sut.CompleteAsync( AuthFlowTestFactory.New(), - CreateCompleteRequest()); + CreateCompleteRequest(), ct: TestContext.Current.CancellationToken); result.IsSuccess.Should().BeFalse(); result.FailureReason @@ -309,7 +309,7 @@ public async Task CompleteAsync_WhenValidationSucceeds_ShouldPassCredentialsToLo await fixture.Sut.CompleteAsync( AuthFlowTestFactory.New(), - request); + request, ct: TestContext.Current.CancellationToken); captured.Should().NotBeNull(); @@ -357,7 +357,7 @@ public async Task CompleteAsync_WhenValidationSucceeds_ShouldUseArtifactExecutio await fixture.Sut.CompleteAsync( AuthFlowTestFactory.New(), - CreateCompleteRequest()); + CreateCompleteRequest(), ct: TestContext.Current.CancellationToken); captured.Should().NotBeNull(); @@ -423,7 +423,7 @@ public async Task RefreshAsync_WhenOldAuthorizationCodeExists_ShouldConsumeIt() var hub = CreateHubArtifact( authorizationCode: "old-code"); - await fixture.Sut.RefreshAsync(hub); + await fixture.Sut.RefreshAsync(hub, ct: TestContext.Current.CancellationToken); fixture.Store.Verify( x => x.ConsumeAsync( @@ -446,7 +446,7 @@ public async Task RefreshAsync_WhenOldAuthorizationCodeDoesNotExist_ShouldNotCon var hub = CreateHubArtifact( authorizationCode: oldCode); - await fixture.Sut.RefreshAsync(hub); + await fixture.Sut.RefreshAsync(hub, ct: TestContext.Current.CancellationToken); fixture.Store.Verify( x => x.ConsumeAsync( @@ -483,7 +483,7 @@ public async Task RefreshAsync_ShouldCreateNewPkceArtifactWithHubContext() .Returns(Task.CompletedTask); var result = - await fixture.Sut.RefreshAsync(hub); + await fixture.Sut.RefreshAsync(hub, ct: TestContext.Current.CancellationToken); captured.Should().NotBeNull(); @@ -536,7 +536,7 @@ public async Task RefreshAsync_ReturnedVerifier_ShouldMatchStoredS256Challenge() .Returns(Task.CompletedTask); var result = - await fixture.Sut.RefreshAsync(hub); + await fixture.Sut.RefreshAsync(hub, ct: TestContext.Current.CancellationToken); captured.Should().NotBeNull(); @@ -600,7 +600,7 @@ public async Task CompleteAsync_WhenLoginFails_ShouldMapLoginFailure() var result = await fixture.Sut.CompleteAsync( AuthFlowTestFactory.New(), - CreateCompleteRequest()); + CreateCompleteRequest(), ct: TestContext.Current.CancellationToken); result.IsSuccess.Should().BeFalse(); @@ -635,7 +635,7 @@ public async Task CompleteAsync_WhenLoginSucceeds_ShouldReturnLoginResult() var result = await fixture.Sut.CompleteAsync( AuthFlowTestFactory.New(), - CreateCompleteRequest()); + CreateCompleteRequest(), ct: TestContext.Current.CancellationToken); result.IsSuccess.Should().BeTrue(); result.FailureReason.Should().BeNull(); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/PkceTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/PkceTests.cs index c5119182..4be8a36e 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/PkceTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/PkceTests.cs @@ -61,11 +61,11 @@ public async Task Refresh_Should_Generate_New_AuthorizationCode() var (artifact, _) = TestPkceFactory.Create(); - await store.StoreAsync(artifact.AuthorizationCode, artifact); + await store.StoreAsync(artifact.AuthorizationCode, artifact, cancellationToken: TestContext.Current.CancellationToken); var hub = TestHubFactory.Create(artifact); - var refreshed = await pkceService.RefreshAsync(hub); + var refreshed = await pkceService.RefreshAsync(hub, ct: TestContext.Current.CancellationToken); refreshed.AuthorizationCode.Should().NotBe(artifact.AuthorizationCode.Value); } @@ -82,13 +82,13 @@ public async Task Refresh_Should_Store_New_PkceArtifact() var (artifact, _) = TestPkceFactory.Create(); - await store.StoreAsync(artifact.AuthorizationCode, artifact); + await store.StoreAsync(artifact.AuthorizationCode, artifact, cancellationToken: TestContext.Current.CancellationToken); var hub = TestHubFactory.Create(artifact); - var refreshed = await pkceService.RefreshAsync(hub); + var refreshed = await pkceService.RefreshAsync(hub, ct: TestContext.Current.CancellationToken); - var stored = await store.GetAsync(new AuthArtifactKey(refreshed.AuthorizationCode)); + var stored = await store.GetAsync(new AuthArtifactKey(refreshed.AuthorizationCode), cancellationToken: TestContext.Current.CancellationToken); stored.Should().NotBeNull(); } @@ -102,10 +102,10 @@ public async Task Refresh_Should_Invalidate_Old_Code() var pkceService = scope.ServiceProvider.GetRequiredService(); var (artifact, _) = TestPkceFactory.Create(); - await store.StoreAsync(artifact.AuthorizationCode, artifact); + await store.StoreAsync(artifact.AuthorizationCode, artifact, cancellationToken: TestContext.Current.CancellationToken); var hub = TestHubFactory.Create(artifact); - var refreshed = await pkceService.RefreshAsync(hub); - var old = await store.GetAsync(artifact.AuthorizationCode); + var refreshed = await pkceService.RefreshAsync(hub, ct: TestContext.Current.CancellationToken); + var old = await store.GetAsync(artifact.AuthorizationCode, cancellationToken: TestContext.Current.CancellationToken); old.Should().BeNull(); } @@ -119,10 +119,10 @@ public async Task Refresh_Should_Preserve_Context() var (artifact, _) = TestPkceFactory.Create(); var hub = TestHubFactory.Create(artifact); - var refreshed = await pkceService.RefreshAsync(hub); + var refreshed = await pkceService.RefreshAsync(hub, ct: TestContext.Current.CancellationToken); var store = scope.ServiceProvider.GetRequiredService(); - var newArtifact = await store.GetAsync(new AuthArtifactKey(refreshed.AuthorizationCode)) as PkceAuthorizationArtifact; + var newArtifact = await store.GetAsync(new AuthArtifactKey(refreshed.AuthorizationCode), cancellationToken: TestContext.Current.CancellationToken) as PkceAuthorizationArtifact; newArtifact!.Context.Device.Should().BeEquivalentTo(artifact.Context.Device); } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/RateLimitingPartitionTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/RateLimitingPartitionTests.cs index 5a6cea5b..971422f2 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/RateLimitingPartitionTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/RateLimitingPartitionTests.cs @@ -29,7 +29,7 @@ public async Task RejectionForwardsNativeRetryMetadata() context.Response.Body = new MemoryStream(); var policy = new UAuthRateLimitPolicy(UAuthEndpointClassification.Get(UAuthEndpoint.Login), new(), "login"); await policy.OnRejected!(new Microsoft.AspNetCore.RateLimiting.OnRejectedContext - { HttpContext = context, Lease = rejected }, default); + { HttpContext = context, Lease = rejected }, TestContext.Current.CancellationToken); Assert.Equal(429, context.Response.StatusCode); Assert.Equal("no-store", context.Response.Headers.CacheControl.ToString()); Assert.True(int.Parse(context.Response.Headers.RetryAfter.ToString()) > 0); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/RefreshFlowServiceTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/RefreshFlowServiceTests.cs index 76be15d8..19bf170d 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/RefreshFlowServiceTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/RefreshFlowServiceTests.cs @@ -29,7 +29,7 @@ public async Task RefreshAsync_PureOpaque_WhenSessionIdMissing_ReturnsReauthWith var result = await sut.RefreshAsync( CreateFlow(UAuthMode.PureOpaque), - CreateRequest(sessionId: null)); + CreateRequest(sessionId: null), ct: TestContext.Current.CancellationToken); AssertReauth(result); validator.VerifyNoOtherCalls(); @@ -54,7 +54,7 @@ public async Task RefreshAsync_PureOpaque_WhenSessionInvalid_ReturnsReauthWithou var result = await sut.RefreshAsync( CreateFlow(UAuthMode.PureOpaque), - CreateRequest(SessionId)); + CreateRequest(SessionId), ct: TestContext.Current.CancellationToken); AssertReauth(result); touch.VerifyNoOtherCalls(); @@ -88,7 +88,7 @@ public async Task RefreshAsync_PureOpaque_WhenSessionRefreshSucceeds_ReturnsExpe var result = await sut.RefreshAsync( CreateFlow(UAuthMode.PureOpaque), - CreateRequest(SessionId)); + CreateRequest(SessionId), ct: TestContext.Current.CancellationToken); result.IsSuccess.Should().BeTrue(); result.Outcome.Should().Be(expectedOutcome); @@ -117,7 +117,7 @@ public async Task RefreshAsync_PureOpaque_WhenTouchFails_ReturnsReauth() var result = await sut.RefreshAsync( CreateFlow(UAuthMode.PureOpaque), - CreateRequest(SessionId)); + CreateRequest(SessionId), ct: TestContext.Current.CancellationToken); AssertReauth(result); rotation.VerifyNoOtherCalls(); @@ -130,7 +130,7 @@ public async Task RefreshAsync_PureJwt_WhenRefreshTokenMissing_ReturnsReauthWith var result = await sut.RefreshAsync( CreateFlow(UAuthMode.PureJwt), - CreateRequest(refreshToken: " ")); + CreateRequest(refreshToken: " "), ct: TestContext.Current.CancellationToken); AssertReauth(result); validator.VerifyNoOtherCalls(); @@ -157,7 +157,7 @@ public async Task RefreshAsync_PureJwt_WhenRotationSucceeds_ReturnsRotatedTokens var result = await sut.RefreshAsync( CreateFlow(UAuthMode.PureJwt), - CreateRequest(refreshToken: "refresh-token")); + CreateRequest(refreshToken: "refresh-token"), ct: TestContext.Current.CancellationToken); result.IsSuccess.Should().BeTrue(); result.Outcome.Should().Be(RefreshOutcome.Rotated); @@ -185,7 +185,7 @@ public async Task RefreshAsync_PureJwt_WhenRotationFails_ReturnsReauth() var result = await sut.RefreshAsync( CreateFlow(UAuthMode.PureJwt), - CreateRequest(refreshToken: "refresh-token")); + CreateRequest(refreshToken: "refresh-token"), ct: TestContext.Current.CancellationToken); AssertReauth(result); validator.VerifyNoOtherCalls(); @@ -204,7 +204,7 @@ public async Task RefreshAsync_Hybrid_WhenRequiredCredentialMissing_ReturnsReaut var result = await sut.RefreshAsync( CreateFlow(UAuthMode.Hybrid), - CreateRequest(sessionMarker is null ? null : SessionId, refreshToken)); + CreateRequest(sessionMarker is null ? null : SessionId, refreshToken), ct: TestContext.Current.CancellationToken); AssertReauth(result); validator.VerifyNoOtherCalls(); @@ -223,7 +223,7 @@ public async Task RefreshAsync_Hybrid_WhenSessionInvalid_ReturnsReauthWithoutRot var result = await sut.RefreshAsync( CreateFlow(UAuthMode.Hybrid), - CreateRequest(SessionId, "refresh-token")); + CreateRequest(SessionId, "refresh-token"), ct: TestContext.Current.CancellationToken); AssertReauth(result); rotation.VerifyNoOtherCalls(); @@ -266,7 +266,7 @@ public async Task RefreshAsync_Hybrid_WhenRotationFails_ReturnsReauthAfterSucces var result = await sut.RefreshAsync( CreateFlow(UAuthMode.Hybrid), - CreateRequest(SessionId, "refresh-token")); + CreateRequest(SessionId, "refresh-token"), ct: TestContext.Current.CancellationToken); AssertReauth(result); @@ -314,7 +314,7 @@ public async Task RefreshAsync_Hybrid_WhenAllStepsSucceed_ReturnsRotatedResultAn var result = await sut.RefreshAsync( CreateFlow(UAuthMode.Hybrid), - CreateRequest(SessionId, "refresh-token")); + CreateRequest(SessionId, "refresh-token"), ct: TestContext.Current.CancellationToken); result.IsSuccess.Should().BeTrue(); result.Outcome.Should().Be(RefreshOutcome.Rotated); @@ -346,7 +346,7 @@ public async Task RefreshAsync_Hybrid_WhenTouchFails_DoesNotRotateRefreshToken() var result = await sut.RefreshAsync( CreateFlow(UAuthMode.Hybrid), - CreateRequest(SessionId, "refresh-token")); + CreateRequest(SessionId, "refresh-token"), ct: TestContext.Current.CancellationToken); AssertReauth(result); @@ -376,7 +376,7 @@ public async Task RefreshAsync_SemiHybrid_WhenSessionInvalid_ReturnsReauthWithou var result = await sut.RefreshAsync( CreateFlow(UAuthMode.SemiHybrid), - CreateRequest(SessionId, "refresh-token")); + CreateRequest(SessionId, "refresh-token"), ct: TestContext.Current.CancellationToken); AssertReauth(result); rotation.VerifyNoOtherCalls(); @@ -405,7 +405,7 @@ public async Task RefreshAsync_SemiHybrid_WhenRotationSucceeds_ReturnsRotatedWit var result = await sut.RefreshAsync( CreateFlow(UAuthMode.SemiHybrid), - CreateRequest(SessionId, "refresh-token")); + CreateRequest(SessionId, "refresh-token"), ct: TestContext.Current.CancellationToken); result.IsSuccess.Should().BeTrue(); result.Outcome.Should().Be(RefreshOutcome.Rotated); @@ -437,7 +437,7 @@ public async Task RefreshAsync_SemiHybrid_WhenRotationFails_ReturnsReauthWithout var result = await sut.RefreshAsync( CreateFlow(UAuthMode.SemiHybrid), - CreateRequest(SessionId, "refresh-token")); + CreateRequest(SessionId, "refresh-token"), ct: TestContext.Current.CancellationToken); AssertReauth(result); touch.VerifyNoOtherCalls(); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/RefreshTokenRotationServiceTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/RefreshTokenRotationServiceTests.cs index 8d26537d..ffd2d6c3 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/RefreshTokenRotationServiceTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/RefreshTokenRotationServiceTests.cs @@ -35,7 +35,7 @@ public async Task RotateAsync_WhenValidationIsInvalid_ReturnsFailedWithoutCreati var sut = new RefreshTokenRotationService(validator.Object, storeFactory.Object, issuer.Object, claimsProvider.Object); - var result = await sut.RotateAsync(CreateFlow(), CreateContext()); + var result = await sut.RotateAsync(CreateFlow(), CreateContext(), ct: TestContext.Current.CancellationToken); result.Result.IsSuccess.Should().BeFalse(); result.Result.ReauthRequired.Should().BeTrue(); @@ -73,7 +73,7 @@ public async Task RotateAsync_WhenTokenIsNotFound_ReturnsFailedWithoutCreatingSt var result = await sut.RotateAsync( CreateFlow(), - CreateContext()); + CreateContext(), ct: TestContext.Current.CancellationToken); result.Result.IsSuccess.Should().BeFalse(); result.Result.ReauthRequired.Should().BeTrue(); @@ -131,7 +131,7 @@ public async Task RotateAsync_WhenTokenIsExpired_ReturnsFailedWithoutMutatingSto var result = await sut.RotateAsync( CreateFlow(tenant, multiTenant: true), - CreateContext(token.SessionId)); + CreateContext(token.SessionId), ct: TestContext.Current.CancellationToken); result.Result.IsSuccess.Should().BeFalse(); @@ -225,7 +225,7 @@ await sut.RotateAsync( CreateFlow( tenant, multiTenant: true), - CreateContext(sessionId)); + CreateContext(sessionId), ct: TestContext.Current.CancellationToken); result.Result.IsSuccess.Should() .BeFalse(); @@ -288,7 +288,7 @@ public async Task RotateAsync_WhenRefreshTokenCannotBeIssued_ReturnsFailedWithou var result = await sut.RotateAsync( CreateFlow(tenant, multiTenant: true), - CreateContext(validation.SessionId)); + CreateContext(validation.SessionId), ct: TestContext.Current.CancellationToken); result.Result.IsSuccess.Should().BeFalse(); result.Result.ReauthRequired.Should().BeTrue(); @@ -345,7 +345,7 @@ public async Task RotateAsync_WhenValid_AtomicallyRotatesRefreshToken() var sut = new RefreshTokenRotationService(validator.Object, storeFactory.Object, issuer.Object, claimsProvider.Object); - var result = await sut.RotateAsync(CreateFlow(tenant, multiTenant: true), CreateContext(validation.SessionId)); + var result = await sut.RotateAsync(CreateFlow(tenant, multiTenant: true), CreateContext(validation.SessionId), ct: TestContext.Current.CancellationToken); result.Result.IsSuccess.Should().BeTrue(); result.Result.AccessToken.Should().BeSameAs(accessToken); @@ -420,7 +420,7 @@ public async Task RotateAsync_WhenMultiTenantEnabled_UsesValidatedTenantForToken CreateFlow( tenant, multiTenant: true), - CreateContext(validation.SessionId)); + CreateContext(validation.SessionId), ct: TestContext.Current.CancellationToken); result.Result.IsSuccess.Should().BeTrue(); @@ -462,7 +462,7 @@ public async Task RotateAsync_WhenMultiTenantDisabled_UsesSingleTenantForTokenIs CreateFlow( TenantKey.Single, multiTenant: false), - CreateContext(validation.SessionId)); + CreateContext(validation.SessionId), ct: TestContext.Current.CancellationToken); result.Result.IsSuccess.Should().BeTrue(); @@ -519,7 +519,7 @@ public async Task RotateAsync_WhenAtomicConsumeLosesRace_ReturnsFailedWithoutSto var result = await sut.RotateAsync( CreateFlow(tenant, multiTenant: true), - CreateContext(validation.SessionId)); + CreateContext(validation.SessionId), ct: TestContext.Current.CancellationToken); result.Result.IsSuccess.Should().BeFalse(); result.Result.ReauthRequired.Should().BeTrue(); @@ -639,7 +639,7 @@ public async Task RotateAsync_WhenIssueRefreshDisabled_ShouldFailWithoutValidati var result = await sut.RotateAsync( flow, - CreateContext()); + CreateContext(), ct: TestContext.Current.CancellationToken); result.Result.IsSuccess.Should().BeFalse(); result.Result.ReauthRequired.Should().BeTrue(); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/ResourceAccessContextBuilderTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/ResourceAccessContextBuilderTests.cs index 3938678b..5ec03c83 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/ResourceAccessContextBuilderTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/ResourceAccessContextBuilderTests.cs @@ -27,7 +27,7 @@ public async Task Create_WithAuthenticatedUser_ShouldSetActor() var http = CreateHttpContext(authenticated: true, userKey); - var result = await ResourceAccessContextBuilder.CreateAsync(http, "users.read"); + var result = await ResourceAccessContextBuilder.CreateAsync(http, "users.read", ct: TestContext.Current.CancellationToken); result.IsAuthenticated.Should().BeTrue(); result.ActorUserKey.Should().Be(userKey); @@ -40,7 +40,7 @@ public async Task Create_WithAnonymousUser_ShouldNotSetActorUserKey() { var http = CreateHttpContext(authenticated: false); - var result = await ResourceAccessContextBuilder.CreateAsync(http, "users.read"); + var result = await ResourceAccessContextBuilder.CreateAsync(http, "users.read", ct: TestContext.Current.CancellationToken); result.IsAuthenticated.Should().BeFalse(); result.ActorUserKey.Should().BeNull(); @@ -56,7 +56,7 @@ public async Task Create_ShouldDeriveResourceFromAction(string action, string ex { var http = CreateHttpContext(authenticated: false); - var result = await ResourceAccessContextBuilder.CreateAsync(http, action); + var result = await ResourceAccessContextBuilder.CreateAsync(http, action, ct: TestContext.Current.CancellationToken); result.Resource.Should().Be(expectedResource); result.Action.Should().Be(action); @@ -67,7 +67,7 @@ public async Task Create_ShouldNotTreatCurrentUserAsSystemActor() { var http = CreateHttpContext(authenticated: true, CreateUserKey()); - var result = await ResourceAccessContextBuilder.CreateAsync(http, "users.read"); + var result = await ResourceAccessContextBuilder.CreateAsync(http, "users.read", ct: TestContext.Current.CancellationToken); result.IsSystemActor.Should().BeFalse(); } @@ -77,7 +77,7 @@ public async Task Create_ShouldNotSetTargetUser() { var http = CreateHttpContext(authenticated: true, CreateUserKey()); - var result = await ResourceAccessContextBuilder.CreateAsync(http, "users.read"); + var result = await ResourceAccessContextBuilder.CreateAsync(http, "users.read", ct: TestContext.Current.CancellationToken); result.TargetUserKey.Should().BeNull(); } @@ -89,7 +89,7 @@ public async Task Create_ShouldPropagateTenantToActorAndResource() var http = CreateHttpContext(authenticated: true, userKey: CreateUserKey(), tenant: tenant); - var result = await ResourceAccessContextBuilder.CreateAsync(http, "users.read"); + var result = await ResourceAccessContextBuilder.CreateAsync(http, "users.read", ct: TestContext.Current.CancellationToken); result.ActorTenant.Should().Be(tenant); result.ResourceTenant.Should().Be(tenant); @@ -179,7 +179,7 @@ public async Task Create_WithResourceRuntime_ShouldResolveTrustedActorRolesFromC // Act var context = await ResourceAccessContextBuilder.CreateAsync( http, - "users.read"); + "users.read", ct: TestContext.Current.CancellationToken); // Assert context.IsAuthenticated.Should().BeTrue(); @@ -255,7 +255,7 @@ public async Task Create_WithResourceRuntime_ShouldIgnoreRolesFromOtherIdentitie // Act var context = await ResourceAccessContextBuilder.CreateAsync( http, - "users.read"); + "users.read", ct: TestContext.Current.CancellationToken); // Assert context.ActorRoles.Should().ContainSingle(); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/SessionApplicationServiceTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/SessionApplicationServiceTests.cs index 48c01bd4..80fed14b 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/SessionApplicationServiceTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/SessionApplicationServiceTests.cs @@ -31,13 +31,13 @@ public async Task GetUserChainsAsync_ReturnsRequestedPage() var root = await CreateRootAsync(store, user); await store.CreateChainAsync( - CreateChain(root, Now.AddMinutes(-30))); + CreateChain(root, Now.AddMinutes(-30)), ct: TestContext.Current.CancellationToken); await store.CreateChainAsync( - CreateChain(root, Now.AddMinutes(-20))); + CreateChain(root, Now.AddMinutes(-20)), ct: TestContext.Current.CancellationToken); await store.CreateChainAsync( - CreateChain(root, Now.AddMinutes(-10))); + CreateChain(root, Now.AddMinutes(-10)), ct: TestContext.Current.CancellationToken); var result = await sut.GetUserChainsAsync( Context(user), @@ -46,7 +46,7 @@ await store.CreateChainAsync( { PageNumber = 2, PageSize = 2 - }); + }, ct: TestContext.Current.CancellationToken); result.TotalCount.Should().Be(3); result.PageNumber.Should().Be(2); @@ -70,8 +70,8 @@ public async Task GetUserChainsAsync_MarksActorChainAsCurrentDevice() root, Now.AddMinutes(-5)); - await store.CreateChainAsync(actorChain); - await store.CreateChainAsync(otherChain); + await store.CreateChainAsync(actorChain, ct: TestContext.Current.CancellationToken); + await store.CreateChainAsync(otherChain, ct: TestContext.Current.CancellationToken); var result = await sut.GetUserChainsAsync( Context(user, actorChain.ChainId), @@ -80,7 +80,7 @@ public async Task GetUserChainsAsync_MarksActorChainAsCurrentDevice() { PageNumber = 1, PageSize = 10 - }); + }, ct: TestContext.Current.CancellationToken); result.Items.Should().HaveCount(2); @@ -111,9 +111,9 @@ public async Task GetUserChainsAsync_SortByChainIdAscending_SortsByChainId() var second = CreateChain(root, Now.AddMinutes(-20)); var third = CreateChain(root, Now.AddMinutes(-10)); - await store.CreateChainAsync(first); - await store.CreateChainAsync(second); - await store.CreateChainAsync(third); + await store.CreateChainAsync(first, ct: TestContext.Current.CancellationToken); + await store.CreateChainAsync(second, ct: TestContext.Current.CancellationToken); + await store.CreateChainAsync(third, ct: TestContext.Current.CancellationToken); var result = await sut.GetUserChainsAsync( Context(user), @@ -124,7 +124,7 @@ public async Task GetUserChainsAsync_SortByChainIdAscending_SortsByChainId() PageSize = 10, SortBy = nameof(SessionChainSummary.ChainId), Descending = false - }); + }, ct: TestContext.Current.CancellationToken); result.Items .Select(x => x.ChainId.Value) @@ -141,13 +141,13 @@ public async Task GetUserChainsAsync_SortByChainIdDescending_SortsByChainId() var root = await CreateRootAsync(store, user); await store.CreateChainAsync( - CreateChain(root, Now.AddMinutes(-30))); + CreateChain(root, Now.AddMinutes(-30)), ct: TestContext.Current.CancellationToken); await store.CreateChainAsync( - CreateChain(root, Now.AddMinutes(-20))); + CreateChain(root, Now.AddMinutes(-20)), ct: TestContext.Current.CancellationToken); await store.CreateChainAsync( - CreateChain(root, Now.AddMinutes(-10))); + CreateChain(root, Now.AddMinutes(-10)), ct: TestContext.Current.CancellationToken); var result = await sut.GetUserChainsAsync( Context(user), @@ -158,7 +158,7 @@ await store.CreateChainAsync( PageSize = 10, SortBy = nameof(SessionChainSummary.ChainId), Descending = true - }); + }, ct: TestContext.Current.CancellationToken); result.Items .Select(x => x.ChainId.Value) @@ -176,13 +176,13 @@ public async Task GetUserChainsAsync_SortByCreatedAtAscending_SortsByCreatedAt() // Deliberately insert in non-chronological order. await store.CreateChainAsync( - CreateChain(root, Now.AddMinutes(-5))); + CreateChain(root, Now.AddMinutes(-5)), ct: TestContext.Current.CancellationToken); await store.CreateChainAsync( - CreateChain(root, Now.AddMinutes(-30))); + CreateChain(root, Now.AddMinutes(-30)), ct: TestContext.Current.CancellationToken); await store.CreateChainAsync( - CreateChain(root, Now.AddMinutes(-15))); + CreateChain(root, Now.AddMinutes(-15)), ct: TestContext.Current.CancellationToken); var result = await sut.GetUserChainsAsync( Context(user), @@ -193,7 +193,7 @@ await store.CreateChainAsync( PageSize = 10, SortBy = nameof(SessionChainSummary.CreatedAt), Descending = false - }); + }, ct: TestContext.Current.CancellationToken); result.Items .Select(x => x.CreatedAt) @@ -210,13 +210,13 @@ public async Task GetUserChainsAsync_SortByCreatedAtDescending_SortsByCreatedAt( var root = await CreateRootAsync(store, user); await store.CreateChainAsync( - CreateChain(root, Now.AddMinutes(-5))); + CreateChain(root, Now.AddMinutes(-5)), ct: TestContext.Current.CancellationToken); await store.CreateChainAsync( - CreateChain(root, Now.AddMinutes(-30))); + CreateChain(root, Now.AddMinutes(-30)), ct: TestContext.Current.CancellationToken); await store.CreateChainAsync( - CreateChain(root, Now.AddMinutes(-15))); + CreateChain(root, Now.AddMinutes(-15)), ct: TestContext.Current.CancellationToken); var result = await sut.GetUserChainsAsync( Context(user), @@ -227,7 +227,7 @@ await store.CreateChainAsync( PageSize = 10, SortBy = nameof(SessionChainSummary.CreatedAt), Descending = true - }); + }, ct: TestContext.Current.CancellationToken); result.Items .Select(x => x.CreatedAt) @@ -268,7 +268,7 @@ public async Task GetUserChainDetailAsync_WhenChainBelongsToDifferentUser_Throws ownerRoot, Now.AddMinutes(-10)); - await store.CreateChainAsync(chain); + await store.CreateChainAsync(chain, ct: TestContext.Current.CancellationToken); var act = () => sut.GetUserChainDetailAsync( Context(requestedUser), @@ -291,7 +291,7 @@ public async Task GetUserChainDetailAsync_ReturnsSessionsNewestFirst() root, Now.AddHours(-1)); - await store.CreateChainAsync(chain); + await store.CreateChainAsync(chain, ct: TestContext.Current.CancellationToken); var older = CreateSession( user, @@ -305,13 +305,13 @@ public async Task GetUserChainDetailAsync_ReturnsSessionsNewestFirst() Now.AddMinutes(-10), "newer-session"); - await store.CreateSessionAsync(older); - await store.CreateSessionAsync(newer); + await store.CreateSessionAsync(older, ct: TestContext.Current.CancellationToken); + await store.CreateSessionAsync(newer, ct: TestContext.Current.CancellationToken); var result = await sut.GetUserChainDetailAsync( Context(user), user, - chain.ChainId); + chain.ChainId, ct: TestContext.Current.CancellationToken); result.ChainId.Should().Be(chain.ChainId); @@ -345,16 +345,16 @@ public async Task RevokeUserSessionAsync_WhenSessionBelongsToUser_RevokesSession Now.AddMinutes(-30), "session-to-revoke"); - await store.CreateChainAsync(chain); - await store.CreateSessionAsync(session); + await store.CreateChainAsync(chain, ct: TestContext.Current.CancellationToken); + await store.CreateSessionAsync(session, ct: TestContext.Current.CancellationToken); await sut.RevokeUserSessionAsync( Context(user), user, - session.SessionId); + session.SessionId, ct: TestContext.Current.CancellationToken); var persisted = - await store.GetSessionAsync(session.SessionId); + await store.GetSessionAsync(session.SessionId, ct: TestContext.Current.CancellationToken); persisted.Should().NotBeNull(); persisted!.IsRevoked.Should().BeTrue(); @@ -381,8 +381,8 @@ public async Task RevokeUserSessionAsync_WhenSessionBelongsToDifferentUser_DoesN Now.AddMinutes(-30), "foreign-session"); - await store.CreateChainAsync(chain); - await store.CreateSessionAsync(session); + await store.CreateChainAsync(chain, ct: TestContext.Current.CancellationToken); + await store.CreateSessionAsync(session, ct: TestContext.Current.CancellationToken); var act = () => sut.RevokeUserSessionAsync( Context(caller), @@ -391,7 +391,7 @@ public async Task RevokeUserSessionAsync_WhenSessionBelongsToDifferentUser_DoesN await act.Should().ThrowAsync(); - var persisted = await store.GetSessionAsync(session.SessionId); + var persisted = await store.GetSessionAsync(session.SessionId, ct: TestContext.Current.CancellationToken); persisted.Should().NotBeNull(); persisted!.IsRevoked.Should().BeFalse(); } @@ -412,18 +412,18 @@ public async Task RevokeUserChainAsync_WhenChainBelongsToUser_RevokesChain() root, Now.AddHours(-1)); - await store.CreateChainAsync(chain); + await store.CreateChainAsync(chain, ct: TestContext.Current.CancellationToken); var result = await sut.RevokeUserChainAsync( Context(user), user, - chain.ChainId); + chain.ChainId, ct: TestContext.Current.CancellationToken); result.CurrentChain.Should().BeFalse(); result.RootRevoked.Should().BeFalse(); var persisted = - await store.GetChainAsync(chain.ChainId); + await store.GetChainAsync(chain.ChainId, ct: TestContext.Current.CancellationToken); persisted.Should().NotBeNull(); persisted!.IsRevoked.Should().BeTrue(); @@ -442,18 +442,18 @@ public async Task RevokeUserChainAsync_WhenRevokingActorChain_ReportsCurrentChai root, Now.AddHours(-1)); - await store.CreateChainAsync(chain); + await store.CreateChainAsync(chain, ct: TestContext.Current.CancellationToken); var result = await sut.RevokeUserChainAsync( Context(user, chain.ChainId), user, - chain.ChainId); + chain.ChainId, ct: TestContext.Current.CancellationToken); result.CurrentChain.Should().BeTrue(); result.RootRevoked.Should().BeFalse(); var persisted = - await store.GetChainAsync(chain.ChainId); + await store.GetChainAsync(chain.ChainId, ct: TestContext.Current.CancellationToken); persisted.Should().NotBeNull(); persisted!.IsRevoked.Should().BeTrue(); @@ -487,7 +487,7 @@ public async Task RevokeUserChainAsync_WhenChainBelongsToDifferentUser_MustNotRe ownerRoot, Now.AddHours(-1)); - await store.CreateChainAsync(chain); + await store.CreateChainAsync(chain, ct: TestContext.Current.CancellationToken); var act = () => sut.RevokeUserChainAsync( Context(caller), @@ -498,7 +498,7 @@ await act.Should() .ThrowAsync(); var persisted = - await store.GetChainAsync(chain.ChainId); + await store.GetChainAsync(chain.ChainId, ct: TestContext.Current.CancellationToken); persisted.Should().NotBeNull(); persisted!.IsRevoked.Should().BeFalse(); @@ -528,23 +528,23 @@ public async Task RevokeAllChainsAsync_WithExceptChain_LeavesExceptedChainActive root, Now.AddMinutes(-10)); - await store.CreateChainAsync(keep); - await store.CreateChainAsync(revoke1); - await store.CreateChainAsync(revoke2); + await store.CreateChainAsync(keep, ct: TestContext.Current.CancellationToken); + await store.CreateChainAsync(revoke1, ct: TestContext.Current.CancellationToken); + await store.CreateChainAsync(revoke2, ct: TestContext.Current.CancellationToken); await sut.RevokeAllChainsAsync( Context(user), user, - keep.ChainId); + keep.ChainId, ct: TestContext.Current.CancellationToken); var persistedKeep = - await store.GetChainAsync(keep.ChainId); + await store.GetChainAsync(keep.ChainId, ct: TestContext.Current.CancellationToken); var persistedRevoke1 = - await store.GetChainAsync(revoke1.ChainId); + await store.GetChainAsync(revoke1.ChainId, ct: TestContext.Current.CancellationToken); var persistedRevoke2 = - await store.GetChainAsync(revoke2.ChainId); + await store.GetChainAsync(revoke2.ChainId, ct: TestContext.Current.CancellationToken); persistedKeep!.IsRevoked.Should().BeFalse(); persistedRevoke1!.IsRevoked.Should().BeTrue(); @@ -567,18 +567,18 @@ public async Task RevokeAllChainsAsync_WithoutExceptChain_RevokesAllChains() root, Now.AddMinutes(-10)); - await store.CreateChainAsync(first); - await store.CreateChainAsync(second); + await store.CreateChainAsync(first, ct: TestContext.Current.CancellationToken); + await store.CreateChainAsync(second, ct: TestContext.Current.CancellationToken); await sut.RevokeAllChainsAsync( Context(user), user, - exceptChainId: null); + exceptChainId: null, ct: TestContext.Current.CancellationToken); - (await store.GetChainAsync(first.ChainId))! + (await store.GetChainAsync(first.ChainId, ct: TestContext.Current.CancellationToken))! .IsRevoked.Should().BeTrue(); - (await store.GetChainAsync(second.ChainId))! + (await store.GetChainAsync(second.ChainId, ct: TestContext.Current.CancellationToken))! .IsRevoked.Should().BeTrue(); } @@ -598,11 +598,11 @@ public async Task LogoutDeviceAsync_WhenActorChainMatches_ReportsCurrentChain() root, Now.AddHours(-1)); - await store.CreateChainAsync(chain); + await store.CreateChainAsync(chain, ct: TestContext.Current.CancellationToken); var result = await sut.LogoutDeviceAsync( Context(user, chain.ChainId), - chain.ChainId); + chain.ChainId, ct: TestContext.Current.CancellationToken); result.CurrentChain.Should().BeTrue(); result.RootRevoked.Should().BeFalse(); @@ -624,8 +624,8 @@ public async Task LogoutOtherDevicesAsync_RevokesSessionsOnOtherChains() root, Now.AddHours(-1)); - await store.CreateChainAsync(currentChain); - await store.CreateChainAsync(otherChain); + await store.CreateChainAsync(currentChain, ct: TestContext.Current.CancellationToken); + await store.CreateChainAsync(otherChain, ct: TestContext.Current.CancellationToken); var currentSession = CreateSession( user, @@ -639,19 +639,19 @@ public async Task LogoutOtherDevicesAsync_RevokesSessionsOnOtherChains() Now.AddMinutes(-20), "other-session"); - await store.CreateSessionAsync(currentSession); - await store.CreateSessionAsync(otherSession); + await store.CreateSessionAsync(currentSession, ct: TestContext.Current.CancellationToken); + await store.CreateSessionAsync(otherSession, ct: TestContext.Current.CancellationToken); await sut.LogoutOtherDevicesAsync( Context(user, currentChain.ChainId), user, - currentChain.ChainId); + currentChain.ChainId, ct: TestContext.Current.CancellationToken); var persistedCurrent = - await store.GetSessionAsync(currentSession.SessionId); + await store.GetSessionAsync(currentSession.SessionId, ct: TestContext.Current.CancellationToken); var persistedOther = - await store.GetSessionAsync(otherSession.SessionId); + await store.GetSessionAsync(otherSession.SessionId, ct: TestContext.Current.CancellationToken); persistedCurrent!.IsRevoked.Should().BeFalse(); persistedOther!.IsRevoked.Should().BeTrue(); @@ -673,8 +673,8 @@ public async Task LogoutAllDevicesAsync_RevokesAllUserSessions() root, Now.AddHours(-1)); - await store.CreateChainAsync(firstChain); - await store.CreateChainAsync(secondChain); + await store.CreateChainAsync(firstChain, ct: TestContext.Current.CancellationToken); + await store.CreateChainAsync(secondChain, ct: TestContext.Current.CancellationToken); var firstSession = CreateSession( user, @@ -688,17 +688,17 @@ public async Task LogoutAllDevicesAsync_RevokesAllUserSessions() Now.AddMinutes(-20), "second-session"); - await store.CreateSessionAsync(firstSession); - await store.CreateSessionAsync(secondSession); + await store.CreateSessionAsync(firstSession, ct: TestContext.Current.CancellationToken); + await store.CreateSessionAsync(secondSession, ct: TestContext.Current.CancellationToken); await sut.LogoutAllDevicesAsync( Context(user), - user); + user, ct: TestContext.Current.CancellationToken); - (await store.GetSessionAsync(firstSession.SessionId))! + (await store.GetSessionAsync(firstSession.SessionId, ct: TestContext.Current.CancellationToken))! .IsRevoked.Should().BeTrue(); - (await store.GetSessionAsync(secondSession.SessionId))! + (await store.GetSessionAsync(secondSession.SessionId, ct: TestContext.Current.CancellationToken))! .IsRevoked.Should().BeTrue(); } @@ -718,23 +718,23 @@ public async Task RevokeRootAsync_RevokesRootAndItsChains() var secondChain = CreateChain(root, Now.AddMinutes(-10)); - await store.CreateChainAsync(firstChain); - await store.CreateChainAsync(secondChain); + await store.CreateChainAsync(firstChain, ct: TestContext.Current.CancellationToken); + await store.CreateChainAsync(secondChain, ct: TestContext.Current.CancellationToken); - await sut.RevokeRootAsync(Context(user), user); + await sut.RevokeRootAsync(Context(user), user, ct: TestContext.Current.CancellationToken); - var activeRoot = await store.GetActiveRootByUserAsync(user); + var activeRoot = await store.GetActiveRootByUserAsync(user, ct: TestContext.Current.CancellationToken); activeRoot.Should().BeNull(); - var persistedRoot = await store.GetRootByIdAsync(root.RootId); + var persistedRoot = await store.GetRootByIdAsync(root.RootId, ct: TestContext.Current.CancellationToken); persistedRoot.Should().NotBeNull(); persistedRoot!.IsRevoked.Should().BeTrue(); persistedRoot.RevokedAt.Should().Be(Now); - (await store.GetChainAsync(firstChain.ChainId))!.IsRevoked.Should().BeTrue(); - (await store.GetChainAsync(secondChain.ChainId))!.IsRevoked.Should().BeTrue(); + (await store.GetChainAsync(firstChain.ChainId, ct: TestContext.Current.CancellationToken))!.IsRevoked.Should().BeTrue(); + (await store.GetChainAsync(secondChain.ChainId, ct: TestContext.Current.CancellationToken))!.IsRevoked.Should().BeTrue(); } [Fact] @@ -747,8 +747,8 @@ public async Task GetUserChainsAsync_ReturnsTotalSessionCountPerChain() var firstChain = CreateChain(root, Now.AddMinutes(-30)); var secondChain = CreateChain(root, Now.AddMinutes(-20)); - await store.CreateChainAsync(firstChain); - await store.CreateChainAsync(secondChain); + await store.CreateChainAsync(firstChain, ct: TestContext.Current.CancellationToken); + await store.CreateChainAsync(secondChain, ct: TestContext.Current.CancellationToken); var firstSession = CreateSession( user, firstChain.ChainId, Now.AddMinutes(-25), "first-1"); @@ -759,14 +759,14 @@ public async Task GetUserChainsAsync_ReturnsTotalSessionCountPerChain() var secondSession = CreateSession( user, secondChain.ChainId, Now.AddMinutes(-10), "second-1"); - await store.CreateSessionAsync(firstSession); - await store.CreateSessionAsync(rotatedSession); - await store.CreateSessionAsync(secondSession); + await store.CreateSessionAsync(firstSession, ct: TestContext.Current.CancellationToken); + await store.CreateSessionAsync(rotatedSession, ct: TestContext.Current.CancellationToken); + await store.CreateSessionAsync(secondSession, ct: TestContext.Current.CancellationToken); var result = await sut.GetUserChainsAsync( Context(user), user, - new PageRequest { PageNumber = 1, PageSize = 10 }); + new PageRequest { PageNumber = 1, PageSize = 10 }, ct: TestContext.Current.CancellationToken); result.Items.Single(x => x.ChainId == firstChain.ChainId) .SessionCount.Should().Be(2); @@ -783,18 +783,18 @@ public async Task GetUserChainsAsync_RevokedSessionsRemainCounted() var root = await CreateRootAsync(store, user); var chain = CreateChain(root, Now.AddMinutes(-30)); - await store.CreateChainAsync(chain); + await store.CreateChainAsync(chain, ct: TestContext.Current.CancellationToken); var first = CreateSession(user, chain.ChainId, Now.AddMinutes(-20), "revoked-1"); var second = CreateSession(user, chain.ChainId, Now.AddMinutes(-10), "revoked-2"); - await store.CreateSessionAsync(first); - await store.CreateSessionAsync(second); + await store.CreateSessionAsync(first, ct: TestContext.Current.CancellationToken); + await store.CreateSessionAsync(second, ct: TestContext.Current.CancellationToken); - await store.RevokeChainCascadeAsync(chain.ChainId, Now); + await store.RevokeChainCascadeAsync(chain.ChainId, Now, ct: TestContext.Current.CancellationToken); - var result = await sut.GetUserChainsAsync(Context(user), user, new PageRequest { PageNumber = 1, PageSize = 10 }); + var result = await sut.GetUserChainsAsync(Context(user), user, new PageRequest { PageNumber = 1, PageSize = 10 }, ct: TestContext.Current.CancellationToken); result.Items.Single(x => x.ChainId == chain.ChainId).SessionCount.Should().Be(2); } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/SessionTouchServiceTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/SessionTouchServiceTests.cs index cae98eea..abf83db7 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/SessionTouchServiceTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/SessionTouchServiceTests.cs @@ -25,7 +25,7 @@ public async Task RefreshAsync_WhenValidationIsInvalid_ReturnsReauthRequired() SessionValidationResult.Invalid(SessionState.Revoked), Policy(), SessionTouchMode.IfNeeded, - Now); + Now, ct: TestContext.Current.CancellationToken); result.IsSuccess.Should().BeFalse(); result.RequiresReauth.Should().BeTrue(); @@ -50,7 +50,7 @@ public async Task RefreshAsync_WhenChainIdIsMissing_ReturnsReauthRequired() validation, Policy(), SessionTouchMode.IfNeeded, - Now); + Now, ct: TestContext.Current.CancellationToken); result.IsSuccess.Should().BeFalse(); result.RequiresReauth.Should().BeTrue(); @@ -71,7 +71,7 @@ public async Task RefreshAsync_WhenChainDoesNotExist_ReturnsReauthRequired() validation, Policy(), SessionTouchMode.IfNeeded, - Now); + Now, ct: TestContext.Current.CancellationToken); result.IsSuccess.Should().BeFalse(); result.RequiresReauth.Should().BeTrue(); @@ -86,7 +86,7 @@ public async Task RefreshAsync_WhenSessionDoesNotExist_ReturnsReauthRequired() var store = factory.Create(validation.Tenant); var chain = CreateChain(validation, Now.AddMinutes(-2)); - await store.CreateChainAsync(chain); + await store.CreateChainAsync(chain, ct: TestContext.Current.CancellationToken); var sut = new SessionTouchService(factory); @@ -94,7 +94,7 @@ public async Task RefreshAsync_WhenSessionDoesNotExist_ReturnsReauthRequired() validation, Policy(), SessionTouchMode.IfNeeded, - Now); + Now, ct: TestContext.Current.CancellationToken); result.IsSuccess.Should().BeFalse(); result.RequiresReauth.Should().BeTrue(); @@ -109,7 +109,7 @@ public async Task RefreshAsync_WhenChainIsRevoked_ReturnsReauthRequired() var store = factory.Create(validation.Tenant); var revoked = chain.Revoke(Now.AddMinutes(-1)); - await store.SaveChainAsync(revoked, chain.Version); + await store.SaveChainAsync(revoked, chain.Version, ct: TestContext.Current.CancellationToken); var sut = new SessionTouchService(factory); @@ -117,13 +117,13 @@ public async Task RefreshAsync_WhenChainIsRevoked_ReturnsReauthRequired() validation, Policy(), SessionTouchMode.IfNeeded, - Now); + Now, ct: TestContext.Current.CancellationToken); result.IsSuccess.Should().BeFalse(); result.RequiresReauth.Should().BeTrue(); result.DidTouch.Should().BeFalse(); - var persisted = await store.GetChainAsync(chain.ChainId); + var persisted = await store.GetChainAsync(chain.ChainId, ct: TestContext.Current.CancellationToken); persisted!.IsRevoked.Should().BeTrue(); persisted.TouchCount.Should().Be(revoked.TouchCount); @@ -139,10 +139,10 @@ public async Task RefreshAsync_WhenSessionIsRevoked_ReturnsReauthRequired() var store = factory.Create(validation.Tenant); var revoked = session.Revoke(Now.AddMinutes(-1)); - await store.SaveSessionAsync(revoked, session.Version); + await store.SaveSessionAsync(revoked, session.Version, ct: TestContext.Current.CancellationToken); var result = await new SessionTouchService(factory).RefreshAsync( - validation, Policy(), SessionTouchMode.IfNeeded, Now); + validation, Policy(), SessionTouchMode.IfNeeded, Now, ct: TestContext.Current.CancellationToken); result.IsSuccess.Should().BeFalse(); result.RequiresReauth.Should().BeTrue(); @@ -158,7 +158,7 @@ await CreateValidSessionAsync( sessionExpiresAt: Now); var result = await new SessionTouchService(factory).RefreshAsync( - validation, Policy(), SessionTouchMode.Force, Now); + validation, Policy(), SessionTouchMode.Force, Now, ct: TestContext.Current.CancellationToken); result.IsSuccess.Should().BeFalse(); result.RequiresReauth.Should().BeTrue(); @@ -175,7 +175,7 @@ public async Task RefreshAsync_WhenChainIdleTimeoutElapsed_ReturnsReauthRequired validation, Policy(idleTimeout: TimeSpan.FromMinutes(10)), SessionTouchMode.Force, - Now); + Now, ct: TestContext.Current.CancellationToken); result.IsSuccess.Should().BeFalse(); result.RequiresReauth.Should().BeTrue(); @@ -189,15 +189,15 @@ public async Task RefreshAsync_WhenTouchIntervalHasNotElapsed_ReturnsSuccessWith await CreateValidSessionAsync(Now.AddMinutes(-4)); var result = await new SessionTouchService(factory).RefreshAsync( - validation, Policy(), SessionTouchMode.IfNeeded, Now); + validation, Policy(), SessionTouchMode.IfNeeded, Now, ct: TestContext.Current.CancellationToken); result.IsSuccess.Should().BeTrue(); result.RequiresReauth.Should().BeFalse(); result.DidTouch.Should().BeFalse(); var store = factory.Create(validation.Tenant); - var persistedChain = await store.GetChainAsync(chain.ChainId); - var persistedSession = await store.GetSessionAsync(session.SessionId); + var persistedChain = await store.GetChainAsync(chain.ChainId, ct: TestContext.Current.CancellationToken); + var persistedSession = await store.GetSessionAsync(session.SessionId, ct: TestContext.Current.CancellationToken); persistedChain!.LastSeenAt.Should().Be(chain.LastSeenAt); persistedChain.TouchCount.Should().Be(0); @@ -214,14 +214,14 @@ public async Task RefreshAsync_WhenTouchIntervalHasElapsed_TouchesAndSavesChain( await CreateValidSessionAsync(Now.AddMinutes(-6)); var result = await new SessionTouchService(factory).RefreshAsync( - validation, Policy(), SessionTouchMode.IfNeeded, Now); + validation, Policy(), SessionTouchMode.IfNeeded, Now, ct: TestContext.Current.CancellationToken); result.IsSuccess.Should().BeTrue(); result.DidTouch.Should().BeTrue(); result.SessionId.Should().Be(validation.SessionId); var persisted = await factory.Create(validation.Tenant) - .GetChainAsync(chain.ChainId); + .GetChainAsync(chain.ChainId, ct: TestContext.Current.CancellationToken); persisted!.LastSeenAt.Should().Be(Now); persisted.TouchCount.Should().Be(1); @@ -235,13 +235,13 @@ public async Task RefreshAsync_WhenTouchIntervalExactlyElapsed_TouchesChain() await CreateValidSessionAsync(Now.AddMinutes(-5)); var result = await new SessionTouchService(factory).RefreshAsync( - validation, Policy(), SessionTouchMode.IfNeeded, Now); + validation, Policy(), SessionTouchMode.IfNeeded, Now, ct: TestContext.Current.CancellationToken); result.IsSuccess.Should().BeTrue(); result.DidTouch.Should().BeTrue(); var persisted = await factory.Create(validation.Tenant) - .GetChainAsync(chain.ChainId); + .GetChainAsync(chain.ChainId, ct: TestContext.Current.CancellationToken); persisted!.LastSeenAt.Should().Be(Now); persisted.TouchCount.Should().Be(1); @@ -256,13 +256,13 @@ public async Task RefreshAsync_WhenTouchIntervalIsNull_TouchesEveryRefresh() var policy = PolicyWithoutTouchInterval(); var result = await new SessionTouchService(factory).RefreshAsync( - validation, policy, SessionTouchMode.IfNeeded, Now); + validation, policy, SessionTouchMode.IfNeeded, Now, ct: TestContext.Current.CancellationToken); result.IsSuccess.Should().BeTrue(); result.DidTouch.Should().BeTrue(); var persisted = await factory.Create(validation.Tenant) - .GetChainAsync(chain.ChainId); + .GetChainAsync(chain.ChainId, ct: TestContext.Current.CancellationToken); persisted!.LastSeenAt.Should().Be(Now); persisted.TouchCount.Should().Be(1); @@ -275,13 +275,13 @@ public async Task RefreshAsync_WhenForceIsUsed_TouchesBeforeInterval() await CreateValidSessionAsync(Now.AddMinutes(-1)); var result = await new SessionTouchService(factory).RefreshAsync( - validation, Policy(), SessionTouchMode.Force, Now); + validation, Policy(), SessionTouchMode.Force, Now, ct: TestContext.Current.CancellationToken); result.IsSuccess.Should().BeTrue(); result.DidTouch.Should().BeTrue(); var persisted = await factory.Create(validation.Tenant) - .GetChainAsync(chain.ChainId); + .GetChainAsync(chain.ChainId, ct: TestContext.Current.CancellationToken); persisted!.LastSeenAt.Should().Be(Now); } @@ -302,14 +302,14 @@ await CreateValidSessionAsync( slidingExpiration: true, lifetime: TimeSpan.FromHours(1)), SessionTouchMode.IfNeeded, - Now); + Now, ct: TestContext.Current.CancellationToken); result.IsSuccess.Should().BeTrue(); result.DidTouch.Should().BeTrue(); var store = factory.Create(validation.Tenant); - var persistedSession = await store.GetSessionAsync(session.SessionId); - var persistedChain = await store.GetChainAsync(chain.ChainId); + var persistedSession = await store.GetSessionAsync(session.SessionId, ct: TestContext.Current.CancellationToken); + var persistedChain = await store.GetChainAsync(chain.ChainId, ct: TestContext.Current.CancellationToken); persistedSession!.ExpiresAt.Should().Be(Now.AddHours(1)); persistedSession.Version.Should().Be(session.Version + 1); @@ -330,13 +330,13 @@ await CreateValidSessionAsync( validation, Policy(slidingExpiration: false), SessionTouchMode.IfNeeded, - Now); + Now, ct: TestContext.Current.CancellationToken); result.IsSuccess.Should().BeTrue(); result.DidTouch.Should().BeTrue(); var persisted = await factory.Create(validation.Tenant) - .GetSessionAsync(session.SessionId); + .GetSessionAsync(session.SessionId, ct: TestContext.Current.CancellationToken); persisted!.ExpiresAt.Should().Be(originalExpiry); persisted.Version.Should().Be(session.Version); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/TryLoginTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/TryLoginTests.cs index c8bcf379..56b021fe 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/TryLoginTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/TryLoginTests.cs @@ -25,7 +25,7 @@ public async Task TryLogin_Should_Return_Preview_On_Failure() { Identifier = "user", Secret = "wrong" - }); + }, ct: TestContext.Current.CancellationToken); result.Should().NotBeNull(); result.IsSuccess.Should().BeFalse(); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/UAuthFlowServiceTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/UAuthFlowServiceTests.cs index 2a49a93f..f0e8f9f2 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/UAuthFlowServiceTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/UAuthFlowServiceTests.cs @@ -51,7 +51,7 @@ public async Task LoginAsync_DelegatesToLoginOrchestrator() var result = await sut.LoginAsync( flow, - request); + request, ct: TestContext.Current.CancellationToken); result.Should().BeSameAs(expected); @@ -107,7 +107,7 @@ public async Task LoginAsync_WithClientProfileOverride_RecreatesFlowBeforeLogin( var result = await sut.LoginAsync( originalFlow, execution, - request); + request, ct: TestContext.Current.CancellationToken); result.Should().BeSameAs(expected); @@ -171,7 +171,7 @@ public async Task LoginAsync_WithDeviceOverride_RecreatesFlowBeforeLogin() var result = await sut.LoginAsync( originalFlow, execution, - request); + request, ct: TestContext.Current.CancellationToken); result.Should().BeSameAs(expected); @@ -237,7 +237,7 @@ public async Task LoginAsync_WithClientProfileAndDeviceOverride_AppliesOverrides var result = await sut.LoginAsync( originalFlow, execution, - request); + request, ct: TestContext.Current.CancellationToken); result.Should().BeSameAs(expected); @@ -292,7 +292,7 @@ public async Task InternalLoginAsync_DelegatesToInternalLoginOrchestrator() var result = await sut.LoginAsync( flow, request, - options); + options, ct: TestContext.Current.CancellationToken); result.Should().BeSameAs(expected); @@ -364,7 +364,7 @@ public async Task InternalLoginAsync_WithExecutionOverrides_UsesRecreatedFlow() originalFlow, execution, request, - options); + options, ct: TestContext.Current.CancellationToken); result.Should().BeSameAs(expected); } @@ -411,7 +411,7 @@ await sut.LogoutAsync( new LogoutRequest { SessionId = sessionId - }); + }, ct: TestContext.Current.CancellationToken); eventRaised.Should().BeFalse(); } @@ -454,7 +454,7 @@ await sut.LogoutAsync( new LogoutRequest { SessionId = sessionId - }); + }, ct: TestContext.Current.CancellationToken); captured.Should().NotBeNull(); @@ -506,7 +506,7 @@ await sut.LogoutAsync( new LogoutRequest { SessionId = sessionId - }); + }, ct: TestContext.Current.CancellationToken); eventRaised.Should().BeFalse(); } @@ -583,7 +583,7 @@ await sut.LogoutAllAsync( new LogoutAllRequest { ExceptCurrent = false - }); + }, ct: TestContext.Current.CancellationToken); captured.Should().NotBeNull(); captured!.UserKey.Should().Be(user); @@ -637,7 +637,7 @@ await sut.LogoutAllAsync( new LogoutAllRequest { ExceptCurrent = true - }); + }, ct: TestContext.Current.CancellationToken); captured.Should().NotBeNull(); captured!.UserKey.Should().Be(user); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/UAuthResourceAccessOrchestratorTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/UAuthResourceAccessOrchestratorTests.cs index b8cb0db4..6384dbca 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/UAuthResourceAccessOrchestratorTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/UAuthResourceAccessOrchestratorTests.cs @@ -57,7 +57,7 @@ public async Task ExecuteAsync_WhenAllowed_ShouldExecuteCommandExactlyOnce() return Task.CompletedTask; }); - await _sut.ExecuteAsync(context, command); + await _sut.ExecuteAsync(context, command, ct: TestContext.Current.CancellationToken); executions.Should().Be(1); } @@ -179,7 +179,7 @@ public async Task ExecuteAsync_ShouldPassEnrichedContextToPolicyProvider() await _sut.ExecuteAsync( original, - new AccessCommand(_ => Task.CompletedTask)); + new AccessCommand(_ => Task.CompletedTask), ct: TestContext.Current.CancellationToken); captured.Should().NotBeNull(); @@ -207,7 +207,7 @@ public async Task ExecuteAsync_ShouldCompileWildcardPermissionClaims() await _sut.ExecuteAsync( CreateContext(), - new AccessCommand(_ => Task.CompletedTask)); + new AccessCommand(_ => Task.CompletedTask), ct: TestContext.Current.CancellationToken); var compiled = GetCompiledPermissions(captured!); @@ -233,7 +233,7 @@ public async Task ExecuteAsync_WithNoPermissionClaims_ShouldProvideEmptyPermissi await _sut.ExecuteAsync( CreateContext(), - new AccessCommand(_ => Task.CompletedTask)); + new AccessCommand(_ => Task.CompletedTask), ct: TestContext.Current.CancellationToken); var compiled = GetCompiledPermissions(captured!); @@ -267,7 +267,7 @@ public async Task ExecuteAsync_ShouldPassEnrichedContextToAuthority() await _sut.ExecuteAsync( CreateContext(), - new AccessCommand(_ => Task.CompletedTask)); + new AccessCommand(_ => Task.CompletedTask), ct: TestContext.Current.CancellationToken); authorityContext.Should().NotBeNull(); @@ -291,7 +291,7 @@ public async Task ExecuteAsyncOfT_WhenAllowed_ShouldReturnCommandResult() var command = new AccessCommand( _ => Task.FromResult("result")); - var result = await _sut.ExecuteAsync(context, command); + var result = await _sut.ExecuteAsync(context, command, ct: TestContext.Current.CancellationToken); result.Should().Be("result"); } @@ -441,7 +441,7 @@ public async Task ExecuteAsync_ShouldAwaitAuthorityBeforeExecutingCommand() var executionTask = _sut.ExecuteAsync( CreateContext(), - command); + command, ct: TestContext.Current.CancellationToken); executed.Should().BeFalse(); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/UAuthSessionValidatorTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/UAuthSessionValidatorTests.cs index e781ebcf..270ce6ea 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/UAuthSessionValidatorTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/UAuthSessionValidatorTests.cs @@ -32,7 +32,7 @@ public async Task ValidateSessionAsync_WhenSessionDoesNotExist_ReturnsNotFound() It.IsAny())) .ReturnsAsync((UAuthSession?)null); - var result = await fixture.Sut.ValidateSessionAsync(fixture.Context); + var result = await fixture.Sut.ValidateSessionAsync(fixture.Context, ct: TestContext.Current.CancellationToken); result.IsValid.Should().BeFalse(); result.State.Should().Be(SessionState.NotFound); @@ -60,7 +60,7 @@ public async Task ValidateSessionAsync_WhenSessionIsExpired_ReturnsExpired() SetupSession(fixture); - var result = await fixture.Sut.ValidateSessionAsync(fixture.Context); + var result = await fixture.Sut.ValidateSessionAsync(fixture.Context, ct: TestContext.Current.CancellationToken); result.IsValid.Should().BeFalse(); result.State.Should().Be(SessionState.Expired); @@ -88,7 +88,7 @@ public async Task ValidateSessionAsync_WhenSessionIsRevoked_ReturnsRevoked() It.IsAny())) .ReturnsAsync(revoked); - var result = await fixture.Sut.ValidateSessionAsync(fixture.Context); + var result = await fixture.Sut.ValidateSessionAsync(fixture.Context, ct: TestContext.Current.CancellationToken); result.IsValid.Should().BeFalse(); result.State.Should().Be(SessionState.Revoked); @@ -120,7 +120,7 @@ public async Task ValidateSessionAsync_WhenChainDoesNotExist_ReturnsRevoked() It.IsAny())) .ReturnsAsync((UAuthSessionChain?)null); - var result = await fixture.Sut.ValidateSessionAsync(fixture.Context); + var result = await fixture.Sut.ValidateSessionAsync(fixture.Context, ct: TestContext.Current.CancellationToken); result.IsValid.Should().BeFalse(); result.State.Should().Be(SessionState.Revoked); @@ -150,7 +150,7 @@ public async Task ValidateSessionAsync_WhenChainIsRevoked_ReturnsRevoked() It.IsAny())) .ReturnsAsync(revokedChain); - var result = await fixture.Sut.ValidateSessionAsync(fixture.Context); + var result = await fixture.Sut.ValidateSessionAsync(fixture.Context, ct: TestContext.Current.CancellationToken); result.IsValid.Should().BeFalse(); result.State.Should().Be(SessionState.Revoked); @@ -170,7 +170,7 @@ public async Task ValidateSessionAsync_WhenChainIsAbsolutelyExpired_ReturnsExpir SetupSessionAndChain(fixture); - var result = await fixture.Sut.ValidateSessionAsync(fixture.Context); + var result = await fixture.Sut.ValidateSessionAsync(fixture.Context, ct: TestContext.Current.CancellationToken); result.IsValid.Should().BeFalse(); result.State.Should().Be(SessionState.Expired); @@ -191,7 +191,7 @@ public async Task ValidateSessionAsync_WhenChainIdleTimeoutIsExceeded_ReturnsExp SetupSessionAndChain(fixture); - var result = await fixture.Sut.ValidateSessionAsync(fixture.Context); + var result = await fixture.Sut.ValidateSessionAsync(fixture.Context, ct: TestContext.Current.CancellationToken); result.IsValid.Should().BeFalse(); result.State.Should().Be(SessionState.Expired); @@ -220,7 +220,7 @@ public async Task ValidateSessionAsync_WhenRootDoesNotExist_ReturnsRevoked() It.IsAny())) .ReturnsAsync((UAuthSessionRoot?)null); - var result = await fixture.Sut.ValidateSessionAsync(fixture.Context); + var result = await fixture.Sut.ValidateSessionAsync(fixture.Context, ct: TestContext.Current.CancellationToken); result.IsValid.Should().BeFalse(); result.State.Should().Be(SessionState.Revoked); @@ -249,7 +249,7 @@ public async Task ValidateSessionAsync_WhenRootIsRevoked_ReturnsRevoked() It.IsAny())) .ReturnsAsync(revokedRoot); - var result = await fixture.Sut.ValidateSessionAsync(fixture.Context); + var result = await fixture.Sut.ValidateSessionAsync(fixture.Context, ct: TestContext.Current.CancellationToken); result.IsValid.Should().BeFalse(); result.State.Should().Be(SessionState.Revoked); @@ -281,7 +281,7 @@ public async Task ValidateSessionAsync_WhenChainBelongsToDifferentRoot_ReturnsSe It.IsAny())) .ReturnsAsync(differentRoot); - var result = await fixture.Sut.ValidateSessionAsync(fixture.Context); + var result = await fixture.Sut.ValidateSessionAsync(fixture.Context, ct: TestContext.Current.CancellationToken); result.IsValid.Should().BeFalse(); result.State.Should().Be(SessionState.SecurityMismatch); @@ -311,7 +311,7 @@ public async Task ValidateSessionAsync_WhenSecurityVersionDoesNotMatch_ReturnsSe It.IsAny())) .ReturnsAsync(updatedRoot); - var result = await fixture.Sut.ValidateSessionAsync(fixture.Context); + var result = await fixture.Sut.ValidateSessionAsync(fixture.Context, ct: TestContext.Current.CancellationToken); result.IsValid.Should().BeFalse(); result.State.Should().Be(SessionState.SecurityMismatch); @@ -338,7 +338,7 @@ public async Task ValidateSessionAsync_WhenDeviceDoesNotMatchAndBehaviorIsReject SetupValidAggregate(fixture); - var result = await fixture.Sut.ValidateSessionAsync(fixture.Context); + var result = await fixture.Sut.ValidateSessionAsync(fixture.Context, ct: TestContext.Current.CancellationToken); result.IsValid.Should().BeFalse(); result.State.Should().Be(SessionState.DeviceMismatch); @@ -373,7 +373,7 @@ public async Task ValidateSessionAsync_WhenRequestHasNoDeviceId_AllowsValidation It.IsAny())) .ReturnsAsync(claims); - var result = await fixture.Sut.ValidateSessionAsync(fixture.Context); + var result = await fixture.Sut.ValidateSessionAsync(fixture.Context, ct: TestContext.Current.CancellationToken); result.IsValid.Should().BeTrue(); result.State.Should().Be(SessionState.Active); @@ -401,7 +401,7 @@ public async Task ValidateSessionAsync_WhenAggregateIsValid_ReturnsActiveResult( It.IsAny())) .ReturnsAsync(claims); - var result = await fixture.Sut.ValidateSessionAsync(fixture.Context); + var result = await fixture.Sut.ValidateSessionAsync(fixture.Context, ct: TestContext.Current.CancellationToken); result.IsValid.Should().BeTrue(); result.State.Should().Be(SessionState.Active); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Sessions/CompositeSessionIdResolverTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Sessions/CompositeSessionIdResolverTests.cs index d466699e..c89a0ee3 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Sessions/CompositeSessionIdResolverTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Sessions/CompositeSessionIdResolverTests.cs @@ -31,7 +31,7 @@ public async Task ResolveAsync_ShouldUseFirstSuccessfulResolverAccordingToConfig bearer.Object, cookie.Object); - var result = await sut.ResolveAsync(context); + var result = await sut.ResolveAsync(context, ct: TestContext.Current.CancellationToken); result.Should().Be(firstId); bearer.Verify(x => x.Resolve(context), Times.Once); @@ -53,7 +53,7 @@ public async Task ResolveAsync_WhenFirstResolverReturnsNull_ShouldContinueToNext bearer.Object, cookie.Object); - var result = await sut.ResolveAsync(context); + var result = await sut.ResolveAsync(context, ct: TestContext.Current.CancellationToken); result.Should().Be(expected); bearer.Verify(x => x.Resolve(context), Times.Once); @@ -77,7 +77,7 @@ public async Task ResolveAsync_WhenResolverIsDisabled_ShouldSkipIt() bearer.Object, cookie.Object); - var result = await sut.ResolveAsync(context); + var result = await sut.ResolveAsync(context, ct: TestContext.Current.CancellationToken); result.Should().Be(cookieId); bearer.Verify(x => x.Resolve(It.IsAny()), Times.Never); @@ -94,7 +94,7 @@ public async Task ResolveAsync_WhenConfiguredResolverDoesNotExist_ShouldContinue var (sut, context) = CreateSut(options, cookie.Object); - var result = await sut.ResolveAsync(context); + var result = await sut.ResolveAsync(context, ct: TestContext.Current.CancellationToken); result.Should().Be(expected); cookie.Verify(x => x.Resolve(context), Times.Once); @@ -110,7 +110,7 @@ public async Task ResolveAsync_WhenOrderUsesDifferentCasing_ShouldResolveCaseIns var (sut, context) = CreateSut(options, bearer.Object); - var result = await sut.ResolveAsync(context); + var result = await sut.ResolveAsync(context, ct: TestContext.Current.CancellationToken); result.Should().Be(expected); bearer.Verify(x => x.Resolve(context), Times.Once); @@ -129,7 +129,7 @@ public async Task ResolveAsync_WhenNoResolverProducesSessionId_ShouldReturnNull( bearer.Object, cookie.Object); - var result = await sut.ResolveAsync(context); + var result = await sut.ResolveAsync(context, ct: TestContext.Current.CancellationToken); result.Should().BeNull(); bearer.Verify(x => x.Resolve(context), Times.Once); @@ -152,7 +152,7 @@ public async Task ResolveAsync_WhenOrderChanges_ShouldRespectNewPrecedence() bearer.Object, cookie.Object); - var result = await sut.ResolveAsync(context); + var result = await sut.ResolveAsync(context, ct: TestContext.Current.CancellationToken); result.Should().Be(cookieId); cookie.Verify(x => x.Resolve(context), Times.Once); @@ -174,7 +174,7 @@ public async Task ResolveAsync_ShouldRespectEffectiveCookieSetting( var (sut, context) = CreateSut(options, cookie.Object); - var result = await sut.ResolveAsync(context); + var result = await sut.ResolveAsync(context, ct: TestContext.Current.CancellationToken); if (effectiveCookieEnabled) { @@ -219,7 +219,7 @@ public async Task ResolveAsync_ShouldReadClientProfileAndUseItsEffectiveOptions( profileReader.Object, effectiveProvider.Object); - var result = await sut.ResolveAsync(context); + var result = await sut.ResolveAsync(context, ct: TestContext.Current.CancellationToken); result.Should().Be(expected); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Sessions/SessionTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Sessions/SessionTests.cs index cd433dba..c26fe638 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Sessions/SessionTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Sessions/SessionTests.cs @@ -26,9 +26,9 @@ public async Task Login_should_cleanup_old_sessions_when_limit_exceeded() } var store = runtime.Services.GetRequiredService().Create(TenantKey.Single); - var chains = await store.GetChainsByUserAsync(TestUsers.User); + var chains = await store.GetChainsByUserAsync(TestUsers.User, ct: TestContext.Current.CancellationToken); var chain = chains.First(); - var sessions = await store.GetSessionsByChainAsync(chain.ChainId); + var sessions = await store.GetSessionsByChainAsync(chain.ChainId, ct: TestContext.Current.CancellationToken); sessions.Count.Should().BeLessThanOrEqualTo(3); } @@ -44,17 +44,17 @@ public async Task Logout_device_should_revoke_sessions_but_keep_chain() { Identifier = "user", Secret = "user" - }); + }, ct: TestContext.Current.CancellationToken); var store = runtime.Services.GetRequiredService().Create(TenantKey.Single); - var chainId = await store.GetChainIdBySessionAsync(result.SessionId!.Value); - await store.LogoutChainAsync(chainId!.Value, runtime.Clock.UtcNow); - var chain = await store.GetChainAsync(chainId.Value); + var chainId = await store.GetChainIdBySessionAsync(result.SessionId!.Value, ct: TestContext.Current.CancellationToken); + await store.LogoutChainAsync(chainId!.Value, runtime.Clock.UtcNow, ct: TestContext.Current.CancellationToken); + var chain = await store.GetChainAsync(chainId.Value, ct: TestContext.Current.CancellationToken); chain.Should().NotBeNull(); chain!.ActiveSessionId.Should().BeNull(); - var sessions = await store.GetSessionsByChainAsync(chainId.Value); + var sessions = await store.GetSessionsByChainAsync(chainId.Value, ct: TestContext.Current.CancellationToken); sessions.All(x => x.IsRevoked).Should().BeTrue(); } @@ -74,13 +74,13 @@ public async Task Logout_other_devices_should_keep_current_chain() var store = runtime.Services.GetRequiredService() .Create(TenantKey.Single); - var chains = await store.GetChainsByUserAsync(TestUsers.User); + var chains = await store.GetChainsByUserAsync(TestUsers.User, ct: TestContext.Current.CancellationToken); var current = chains.First(); - await store.RevokeOtherSessionsAsync(current.UserKey, current.ChainId, runtime.Clock.UtcNow); + await store.RevokeOtherSessionsAsync(current.UserKey, current.ChainId, runtime.Clock.UtcNow, ct: TestContext.Current.CancellationToken); - var updatedChains = await store.GetChainsByUserAsync(current.UserKey); + var updatedChains = await store.GetChainsByUserAsync(current.UserKey, ct: TestContext.Current.CancellationToken); updatedChains.Count(x => x.State == SessionChainState.Active).Should().Be(1); } @@ -97,13 +97,13 @@ public async Task Get_chain_detail_should_return_sessions() { Identifier = "user", Secret = "user" - }); + }, ct: TestContext.Current.CancellationToken); var service = runtime.Services.GetRequiredService(); var context = TestAccessContext.ForUser(TestUsers.User, "session.query"); var store = runtime.Services.GetRequiredService().Create(TenantKey.Single); - var chains = await store.GetChainsByUserAsync(TestUsers.User); - var result = await service.GetUserChainDetailAsync(context, chains.First().UserKey, chains.First().ChainId); + var chains = await store.GetChainsByUserAsync(TestUsers.User, ct: TestContext.Current.CancellationToken); + var result = await service.GetUserChainDetailAsync(context, chains.First().UserKey, chains.First().ChainId, ct: TestContext.Current.CancellationToken); result.Should().NotBeNull(); result.Sessions.Should().NotBeEmpty(); @@ -121,12 +121,12 @@ public async Task Revoke_chain_should_revoke_all_sessions() { Identifier = "user", Secret = "user" - }); + }, ct: TestContext.Current.CancellationToken); var store = runtime.Services.GetRequiredService().Create(TenantKey.Single); - var chains = await store.GetChainsByUserAsync(TestUsers.User); - await store.RevokeChainCascadeAsync(chains.First().ChainId, runtime.Clock.UtcNow); - var sessions = await store.GetSessionsByChainAsync(chains.First().ChainId); + var chains = await store.GetChainsByUserAsync(TestUsers.User, ct: TestContext.Current.CancellationToken); + await store.RevokeChainCascadeAsync(chains.First().ChainId, runtime.Clock.UtcNow, ct: TestContext.Current.CancellationToken); + var sessions = await store.GetSessionsByChainAsync(chains.First().ChainId, ct: TestContext.Current.CancellationToken); sessions.All(x => x.IsRevoked).Should().BeTrue(); } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Sessions/Store/SessionStoreChainContractTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Sessions/Store/SessionStoreChainContractTests.cs index ef08049b..4b632fba 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Sessions/Store/SessionStoreChainContractTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Sessions/Store/SessionStoreChainContractTests.cs @@ -83,7 +83,7 @@ public async Task CreateChainAsync_PersistsChain() await CreateChainGraphAsync(store, Tenant); var result = - await store.GetChainAsync(chain.ChainId); + await store.GetChainAsync(chain.ChainId, ct: TestContext.Current.CancellationToken); result.Should().NotBeNull(); @@ -107,7 +107,7 @@ public async Task GetChainAsync_WhenMissing_ReturnsNull() var result = await store.GetChainAsync( - SessionChainId.New()); + SessionChainId.New(), ct: TestContext.Current.CancellationToken); result.Should().BeNull(); } @@ -126,7 +126,7 @@ public async Task CreateChainAsync_WhenVersionIsNotZero_IsRejected() Now); await store.ExecuteAsync( - ct => store.CreateRootAsync(root, ct)); + ct => store.CreateRootAsync(root, ct), ct: TestContext.Current.CancellationToken); var chain = CreateChain( root, @@ -196,11 +196,11 @@ await store.ExecuteAsync( ct => store.SaveChainAsync( updated, expectedVersion: chain.Version, - ct)); + ct), ct: TestContext.Current.CancellationToken); var result = await store.GetChainAsync( - chain.ChainId); + chain.ChainId, ct: TestContext.Current.CancellationToken); result.Should().NotBeNull(); result!.LastSeenAt.Should().Be( @@ -283,11 +283,11 @@ await store.ExecuteAsync( ct => store.RevokeChainAsync( chain.ChainId, Now.AddMinutes(10), - ct)); + ct), ct: TestContext.Current.CancellationToken); var result = await store.GetChainAsync( - chain.ChainId); + chain.ChainId, ct: TestContext.Current.CancellationToken); result.Should().NotBeNull(); result!.IsRevoked.Should().BeTrue(); @@ -326,17 +326,17 @@ await store.ExecuteAsync( ct => store.RevokeChainAsync( chain.ChainId, Now.AddMinutes(5), - ct)); + ct), ct: TestContext.Current.CancellationToken); await store.ExecuteAsync( ct => store.RevokeChainAsync( chain.ChainId, Now.AddMinutes(10), - ct)); + ct), ct: TestContext.Current.CancellationToken); var result = await store.GetChainAsync( - chain.ChainId); + chain.ChainId, ct: TestContext.Current.CancellationToken); result.Should().NotBeNull(); result!.RevokedAt.Should().Be( @@ -372,7 +372,7 @@ await CreateChainGraphAsync( await store.ExecuteAsync( ct => store.CreateChainAsync( secondChainA, - ct)); + ct), ct: TestContext.Current.CancellationToken); var (_, chainB) = await CreateChainGraphAsync( @@ -382,7 +382,7 @@ await CreateChainGraphAsync( var result = await store.GetChainsByRootAsync( - rootA.RootId); + rootA.RootId, ct: TestContext.Current.CancellationToken); result.Should().HaveCount(2); @@ -421,10 +421,10 @@ await CreateChainGraphAsync( await store.ExecuteAsync( ct => store.CreateChainAsync( second, - ct)); + ct), ct: TestContext.Current.CancellationToken); var result = - await store.GetChainsByUserAsync(user); + await store.GetChainsByUserAsync(user, ct: TestContext.Current.CancellationToken); result.Select(x => x.ChainId) .Should() @@ -459,11 +459,11 @@ await CreateChainGraphAsync( var fromA = await storeA.GetChainAsync( - chain.ChainId); + chain.ChainId, ct: TestContext.Current.CancellationToken); var fromB = await storeB.GetChainAsync( - chain.ChainId); + chain.ChainId, ct: TestContext.Current.CancellationToken); fromA.Should().NotBeNull(); fromB.Should().BeNull(); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Sessions/Store/SessionStoreLifecycleContractTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Sessions/Store/SessionStoreLifecycleContractTests.cs index 9d6eca75..3dccddd6 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Sessions/Store/SessionStoreLifecycleContractTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Sessions/Store/SessionStoreLifecycleContractTests.cs @@ -230,13 +230,13 @@ public async Task Graph_HasExpectedActiveSessions() var graph = await CreateGraphAsync(store); var chainA1 = await store.GetChainAsync( - graph.ChainA1.ChainId); + graph.ChainA1.ChainId, ct: TestContext.Current.CancellationToken); var chainA2 = await store.GetChainAsync( - graph.ChainA2.ChainId); + graph.ChainA2.ChainId, ct: TestContext.Current.CancellationToken); var chainB1 = await store.GetChainAsync( - graph.ChainB1.ChainId); + graph.ChainB1.ChainId, ct: TestContext.Current.CancellationToken); chainA1!.ActiveSessionId .Should().Be(graph.SessionA12.SessionId); @@ -266,10 +266,10 @@ await store.ExecuteAsync( ct => store.RevokeSessionAsync( graph.SessionA11.SessionId, revokedAt, - ct)); + ct), ct: TestContext.Current.CancellationToken); var session = await store.GetSessionAsync( - graph.SessionA11.SessionId); + graph.SessionA11.SessionId, ct: TestContext.Current.CancellationToken); session.Should().NotBeNull(); session!.IsRevoked.Should().BeTrue(); @@ -288,10 +288,10 @@ await store.ExecuteAsync( ct => store.RevokeSessionAsync( graph.SessionA11.SessionId, Now.AddMinutes(20), - ct)); + ct), ct: TestContext.Current.CancellationToken); var chain = await store.GetChainAsync( - graph.ChainA1.ChainId); + graph.ChainA1.ChainId, ct: TestContext.Current.CancellationToken); chain!.ActiveSessionId .Should().Be(graph.SessionA12.SessionId); @@ -309,13 +309,13 @@ await store.ExecuteAsync( ct => store.RevokeSessionAsync( graph.SessionA12.SessionId, Now.AddMinutes(20), - ct)); + ct), ct: TestContext.Current.CancellationToken); var session = await store.GetSessionAsync( - graph.SessionA12.SessionId); + graph.SessionA12.SessionId, ct: TestContext.Current.CancellationToken); var chain = await store.GetChainAsync( - graph.ChainA1.ChainId); + graph.ChainA1.ChainId, ct: TestContext.Current.CancellationToken); session.Should().NotBeNull(); session!.IsRevoked.Should().BeTrue(); @@ -337,13 +337,13 @@ await store.ExecuteAsync( ct => store.RevokeSessionAsync( graph.SessionA12.SessionId, Now.AddMinutes(20), - ct)); + ct), ct: TestContext.Current.CancellationToken); var a21 = await store.GetSessionAsync( - graph.SessionA21.SessionId); + graph.SessionA21.SessionId, ct: TestContext.Current.CancellationToken); var b11 = await store.GetSessionAsync( - graph.SessionB11.SessionId); + graph.SessionB11.SessionId, ct: TestContext.Current.CancellationToken); a21!.IsRevoked.Should().BeFalse(); b11!.IsRevoked.Should().BeFalse(); @@ -364,10 +364,10 @@ public async Task RemoveSessionAsync_WhenSessionIsNotActive_RemovesSession() await store.ExecuteAsync( ct => store.RemoveSessionAsync( graph.SessionA11.SessionId, - ct)); + ct), ct: TestContext.Current.CancellationToken); var session = await store.GetSessionAsync( - graph.SessionA11.SessionId); + graph.SessionA11.SessionId, ct: TestContext.Current.CancellationToken); session.Should().BeNull(); } @@ -383,10 +383,10 @@ public async Task RemoveSessionAsync_WhenSessionIsNotActive_PreservesActiveSessi await store.ExecuteAsync( ct => store.RemoveSessionAsync( graph.SessionA11.SessionId, - ct)); + ct), ct: TestContext.Current.CancellationToken); var chain = await store.GetChainAsync( - graph.ChainA1.ChainId); + graph.ChainA1.ChainId, ct: TestContext.Current.CancellationToken); chain!.ActiveSessionId .Should().Be(graph.SessionA12.SessionId); @@ -403,13 +403,13 @@ public async Task RemoveSessionAsync_WhenSessionIsActive_DoesNotLeaveDanglingRef await store.ExecuteAsync( ct => store.RemoveSessionAsync( graph.SessionA12.SessionId, - ct)); + ct), ct: TestContext.Current.CancellationToken); var session = await store.GetSessionAsync( - graph.SessionA12.SessionId); + graph.SessionA12.SessionId, ct: TestContext.Current.CancellationToken); var chain = await store.GetChainAsync( - graph.ChainA1.ChainId); + graph.ChainA1.ChainId, ct: TestContext.Current.CancellationToken); session.Should().BeNull(); @@ -436,13 +436,13 @@ await store.ExecuteAsync( ct => store.LogoutChainAsync( graph.ChainA1.ChainId, at, - ct)); + ct), ct: TestContext.Current.CancellationToken); var a11 = await store.GetSessionAsync( - graph.SessionA11.SessionId); + graph.SessionA11.SessionId, ct: TestContext.Current.CancellationToken); var a12 = await store.GetSessionAsync( - graph.SessionA12.SessionId); + graph.SessionA12.SessionId, ct: TestContext.Current.CancellationToken); a11!.IsRevoked.Should().BeTrue(); a12!.IsRevoked.Should().BeTrue(); @@ -463,10 +463,10 @@ await store.ExecuteAsync( ct => store.LogoutChainAsync( graph.ChainA1.ChainId, Now.AddMinutes(20), - ct)); + ct), ct: TestContext.Current.CancellationToken); var chain = await store.GetChainAsync( - graph.ChainA1.ChainId); + graph.ChainA1.ChainId, ct: TestContext.Current.CancellationToken); chain.Should().NotBeNull(); chain!.ActiveSessionId.Should().BeNull(); @@ -484,10 +484,10 @@ await store.ExecuteAsync( ct => store.LogoutChainAsync( graph.ChainA1.ChainId, Now.AddMinutes(20), - ct)); + ct), ct: TestContext.Current.CancellationToken); var chain = await store.GetChainAsync( - graph.ChainA1.ChainId); + graph.ChainA1.ChainId, ct: TestContext.Current.CancellationToken); chain.Should().NotBeNull(); chain!.IsRevoked.Should().BeFalse(); @@ -505,19 +505,19 @@ await store.ExecuteAsync( ct => store.LogoutChainAsync( graph.ChainA1.ChainId, Now.AddMinutes(20), - ct)); + ct), ct: TestContext.Current.CancellationToken); var chainA2 = await store.GetChainAsync( - graph.ChainA2.ChainId); + graph.ChainA2.ChainId, ct: TestContext.Current.CancellationToken); var sessionA21 = await store.GetSessionAsync( - graph.SessionA21.SessionId); + graph.SessionA21.SessionId, ct: TestContext.Current.CancellationToken); var chainB1 = await store.GetChainAsync( - graph.ChainB1.ChainId); + graph.ChainB1.ChainId, ct: TestContext.Current.CancellationToken); var sessionB11 = await store.GetSessionAsync( - graph.SessionB11.SessionId); + graph.SessionB11.SessionId, ct: TestContext.Current.CancellationToken); chainA2!.IsRevoked.Should().BeFalse(); chainA2.ActiveSessionId.Should().Be( @@ -550,10 +550,10 @@ await store.ExecuteAsync( ct => store.RevokeChainCascadeAsync( graph.ChainA1.ChainId, at, - ct)); + ct), ct: TestContext.Current.CancellationToken); var chain = await store.GetChainAsync( - graph.ChainA1.ChainId); + graph.ChainA1.ChainId, ct: TestContext.Current.CancellationToken); chain.Should().NotBeNull(); chain!.IsRevoked.Should().BeTrue(); @@ -572,13 +572,13 @@ await store.ExecuteAsync( ct => store.RevokeChainCascadeAsync( graph.ChainA1.ChainId, Now.AddMinutes(20), - ct)); + ct), ct: TestContext.Current.CancellationToken); var a11 = await store.GetSessionAsync( - graph.SessionA11.SessionId); + graph.SessionA11.SessionId, ct: TestContext.Current.CancellationToken); var a12 = await store.GetSessionAsync( - graph.SessionA12.SessionId); + graph.SessionA12.SessionId, ct: TestContext.Current.CancellationToken); a11!.IsRevoked.Should().BeTrue(); a12!.IsRevoked.Should().BeTrue(); @@ -596,19 +596,19 @@ await store.ExecuteAsync( ct => store.RevokeChainCascadeAsync( graph.ChainA1.ChainId, Now.AddMinutes(20), - ct)); + ct), ct: TestContext.Current.CancellationToken); var chainA2 = await store.GetChainAsync( - graph.ChainA2.ChainId); + graph.ChainA2.ChainId, ct: TestContext.Current.CancellationToken); var sessionA21 = await store.GetSessionAsync( - graph.SessionA21.SessionId); + graph.SessionA21.SessionId, ct: TestContext.Current.CancellationToken); var chainB1 = await store.GetChainAsync( - graph.ChainB1.ChainId); + graph.ChainB1.ChainId, ct: TestContext.Current.CancellationToken); var sessionB11 = await store.GetSessionAsync( - graph.SessionB11.SessionId); + graph.SessionB11.SessionId, ct: TestContext.Current.CancellationToken); chainA2!.IsRevoked.Should().BeFalse(); sessionA21!.IsRevoked.Should().BeFalse(); @@ -633,16 +633,16 @@ await store.ExecuteAsync( ct => store.RevokeAllSessionsAsync( graph.UserA, Now.AddMinutes(20), - ct)); + ct), ct: TestContext.Current.CancellationToken); var a11 = await store.GetSessionAsync( - graph.SessionA11.SessionId); + graph.SessionA11.SessionId, ct: TestContext.Current.CancellationToken); var a12 = await store.GetSessionAsync( - graph.SessionA12.SessionId); + graph.SessionA12.SessionId, ct: TestContext.Current.CancellationToken); var a21 = await store.GetSessionAsync( - graph.SessionA21.SessionId); + graph.SessionA21.SessionId, ct: TestContext.Current.CancellationToken); a11!.IsRevoked.Should().BeTrue(); a12!.IsRevoked.Should().BeTrue(); @@ -661,13 +661,13 @@ await store.ExecuteAsync( ct => store.RevokeAllSessionsAsync( graph.UserA, Now.AddMinutes(20), - ct)); + ct), ct: TestContext.Current.CancellationToken); var chainA1 = await store.GetChainAsync( - graph.ChainA1.ChainId); + graph.ChainA1.ChainId, ct: TestContext.Current.CancellationToken); var chainA2 = await store.GetChainAsync( - graph.ChainA2.ChainId); + graph.ChainA2.ChainId, ct: TestContext.Current.CancellationToken); chainA1!.ActiveSessionId.Should().BeNull(); chainA2!.ActiveSessionId.Should().BeNull(); @@ -685,13 +685,13 @@ await store.ExecuteAsync( ct => store.RevokeAllSessionsAsync( graph.UserA, Now.AddMinutes(20), - ct)); + ct), ct: TestContext.Current.CancellationToken); var chainB1 = await store.GetChainAsync( - graph.ChainB1.ChainId); + graph.ChainB1.ChainId, ct: TestContext.Current.CancellationToken); var sessionB11 = await store.GetSessionAsync( - graph.SessionB11.SessionId); + graph.SessionB11.SessionId, ct: TestContext.Current.CancellationToken); chainB1!.IsRevoked.Should().BeFalse(); @@ -722,19 +722,19 @@ await store.ExecuteAsync( graph.UserA, graph.ChainA1.ChainId, Now.AddMinutes(20), - ct)); + ct), ct: TestContext.Current.CancellationToken); var a11 = await store.GetSessionAsync( - graph.SessionA11.SessionId); + graph.SessionA11.SessionId, ct: TestContext.Current.CancellationToken); var a12 = await store.GetSessionAsync( - graph.SessionA12.SessionId); + graph.SessionA12.SessionId, ct: TestContext.Current.CancellationToken); a11!.IsRevoked.Should().BeFalse(); a12!.IsRevoked.Should().BeFalse(); var chainA1 = await store.GetChainAsync( - graph.ChainA1.ChainId); + graph.ChainA1.ChainId, ct: TestContext.Current.CancellationToken); chainA1!.ActiveSessionId .Should().Be(graph.SessionA12.SessionId); @@ -753,10 +753,10 @@ await store.ExecuteAsync( graph.UserA, graph.ChainA1.ChainId, Now.AddMinutes(20), - ct)); + ct), ct: TestContext.Current.CancellationToken); var a21 = await store.GetSessionAsync( - graph.SessionA21.SessionId); + graph.SessionA21.SessionId, ct: TestContext.Current.CancellationToken); a21.Should().NotBeNull(); a21!.IsRevoked.Should().BeTrue(); @@ -775,13 +775,13 @@ await store.ExecuteAsync( graph.UserA, graph.ChainA1.ChainId, Now.AddMinutes(20), - ct)); + ct), ct: TestContext.Current.CancellationToken); var kept = await store.GetChainAsync( - graph.ChainA1.ChainId); + graph.ChainA1.ChainId, ct: TestContext.Current.CancellationToken); var affected = await store.GetChainAsync( - graph.ChainA2.ChainId); + graph.ChainA2.ChainId, ct: TestContext.Current.CancellationToken); kept!.ActiveSessionId .Should().Be(graph.SessionA12.SessionId); @@ -803,13 +803,13 @@ await store.ExecuteAsync( graph.UserA, graph.ChainA1.ChainId, Now.AddMinutes(20), - ct)); + ct), ct: TestContext.Current.CancellationToken); var b11 = await store.GetSessionAsync( - graph.SessionB11.SessionId); + graph.SessionB11.SessionId, ct: TestContext.Current.CancellationToken); var chainB1 = await store.GetChainAsync( - graph.ChainB1.ChainId); + graph.ChainB1.ChainId, ct: TestContext.Current.CancellationToken); b11!.IsRevoked.Should().BeFalse(); @@ -836,13 +836,13 @@ await store.ExecuteAsync( ct => store.RevokeAllChainsAsync( graph.UserA, Now.AddMinutes(20), - ct)); + ct), ct: TestContext.Current.CancellationToken); var a1 = await store.GetChainAsync( - graph.ChainA1.ChainId); + graph.ChainA1.ChainId, ct: TestContext.Current.CancellationToken); var a2 = await store.GetChainAsync( - graph.ChainA2.ChainId); + graph.ChainA2.ChainId, ct: TestContext.Current.CancellationToken); a1!.IsRevoked.Should().BeTrue(); a2!.IsRevoked.Should().BeTrue(); @@ -860,10 +860,10 @@ await store.ExecuteAsync( ct => store.RevokeAllChainsAsync( graph.UserA, Now.AddMinutes(20), - ct)); + ct), ct: TestContext.Current.CancellationToken); var b1 = await store.GetChainAsync( - graph.ChainB1.ChainId); + graph.ChainB1.ChainId, ct: TestContext.Current.CancellationToken); b1!.IsRevoked.Should().BeFalse(); } @@ -885,10 +885,10 @@ await store.ExecuteAsync( graph.UserA, graph.ChainA1.ChainId, Now.AddMinutes(20), - ct)); + ct), ct: TestContext.Current.CancellationToken); var a1 = await store.GetChainAsync( - graph.ChainA1.ChainId); + graph.ChainA1.ChainId, ct: TestContext.Current.CancellationToken); a1.Should().NotBeNull(); a1!.IsRevoked.Should().BeFalse(); @@ -907,10 +907,10 @@ await store.ExecuteAsync( graph.UserA, graph.ChainA1.ChainId, Now.AddMinutes(20), - ct)); + ct), ct: TestContext.Current.CancellationToken); var a2 = await store.GetChainAsync( - graph.ChainA2.ChainId); + graph.ChainA2.ChainId, ct: TestContext.Current.CancellationToken); a2.Should().NotBeNull(); a2!.IsRevoked.Should().BeTrue(); @@ -929,10 +929,10 @@ await store.ExecuteAsync( graph.UserA, graph.ChainA1.ChainId, Now.AddMinutes(20), - ct)); + ct), ct: TestContext.Current.CancellationToken); var b1 = await store.GetChainAsync( - graph.ChainB1.ChainId); + graph.ChainB1.ChainId, ct: TestContext.Current.CancellationToken); b1!.IsRevoked.Should().BeFalse(); } @@ -955,17 +955,17 @@ await store.ExecuteAsync( ct => store.RevokeRootCascadeAsync( graph.UserA, at, - ct)); + ct), ct: TestContext.Current.CancellationToken); var activeRoot = await store.GetActiveRootByUserAsync( - graph.UserA); + graph.UserA, ct: TestContext.Current.CancellationToken); activeRoot.Should().BeNull(); var root = await store.GetRootByIdAsync( - graph.RootA.RootId); + graph.RootA.RootId, ct: TestContext.Current.CancellationToken); root.Should().NotBeNull(); @@ -985,17 +985,17 @@ await store.ExecuteAsync( ct => store.RevokeRootCascadeAsync( graph.UserA, Now.AddMinutes(20), - ct)); + ct), ct: TestContext.Current.CancellationToken); var activeRoot = await store.GetActiveRootByUserAsync( - graph.UserA); + graph.UserA, ct: TestContext.Current.CancellationToken); activeRoot.Should().BeNull(); var root = await store.GetRootByIdAsync( - graph.RootA.RootId); + graph.RootA.RootId, ct: TestContext.Current.CancellationToken); root.Should().NotBeNull(); @@ -1018,13 +1018,13 @@ await store.ExecuteAsync( ct => store.RevokeRootCascadeAsync( graph.UserA, Now.AddMinutes(20), - ct)); + ct), ct: TestContext.Current.CancellationToken); var a1 = await store.GetChainAsync( - graph.ChainA1.ChainId); + graph.ChainA1.ChainId, ct: TestContext.Current.CancellationToken); var a2 = await store.GetChainAsync( - graph.ChainA2.ChainId); + graph.ChainA2.ChainId, ct: TestContext.Current.CancellationToken); a1!.IsRevoked.Should().BeTrue(); a2!.IsRevoked.Should().BeTrue(); @@ -1042,16 +1042,16 @@ await store.ExecuteAsync( ct => store.RevokeRootCascadeAsync( graph.UserA, Now.AddMinutes(20), - ct)); + ct), ct: TestContext.Current.CancellationToken); var a11 = await store.GetSessionAsync( - graph.SessionA11.SessionId); + graph.SessionA11.SessionId, ct: TestContext.Current.CancellationToken); var a12 = await store.GetSessionAsync( - graph.SessionA12.SessionId); + graph.SessionA12.SessionId, ct: TestContext.Current.CancellationToken); var a21 = await store.GetSessionAsync( - graph.SessionA21.SessionId); + graph.SessionA21.SessionId, ct: TestContext.Current.CancellationToken); a11!.IsRevoked.Should().BeTrue(); a12!.IsRevoked.Should().BeTrue(); @@ -1070,16 +1070,16 @@ await store.ExecuteAsync( ct => store.RevokeRootCascadeAsync( graph.UserA, Now.AddMinutes(20), - ct)); + ct), ct: TestContext.Current.CancellationToken); var rootB = await store.GetActiveRootByUserAsync( - graph.UserB); + graph.UserB, ct: TestContext.Current.CancellationToken); var chainB1 = await store.GetChainAsync( - graph.ChainB1.ChainId); + graph.ChainB1.ChainId, ct: TestContext.Current.CancellationToken); var sessionB11 = await store.GetSessionAsync( - graph.SessionB11.SessionId); + graph.SessionB11.SessionId, ct: TestContext.Current.CancellationToken); rootB.Should().NotBeNull(); chainB1.Should().NotBeNull(); @@ -1109,10 +1109,10 @@ await store.ExecuteAsync( ct => store.LogoutChainAsync( graph.ChainA1.ChainId, Now.AddMinutes(20), - ct)); + ct), ct: TestContext.Current.CancellationToken); var sessions = await store.GetSessionsByChainAsync( - graph.ChainA1.ChainId); + graph.ChainA1.ChainId, ct: TestContext.Current.CancellationToken); sessions.Should().HaveCount(2); sessions.Should().OnlyContain(x => x.IsRevoked); @@ -1130,11 +1130,11 @@ await store.ExecuteAsync( ct => store.RevokeRootCascadeAsync( graph.UserA, Now.AddMinutes(20), - ct)); + ct), ct: TestContext.Current.CancellationToken); var chains = await store.GetChainsByUserAsync( graph.UserA, - includeHistoricalRoots: true); + includeHistoricalRoots: true, ct: TestContext.Current.CancellationToken); chains.Should().HaveCount(2); chains.Should().OnlyContain(x => x.IsRevoked); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Sessions/Store/SessionStoreRootContractTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Sessions/Store/SessionStoreRootContractTests.cs index 91a75392..4f2fc72e 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Sessions/Store/SessionStoreRootContractTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Sessions/Store/SessionStoreRootContractTests.cs @@ -50,10 +50,10 @@ public async Task CreateRootAsync_PersistsRoot() Now); await store.ExecuteAsync( - ct => store.CreateRootAsync(root, ct)); + ct => store.CreateRootAsync(root, ct), ct: TestContext.Current.CancellationToken); var result = - await store.GetActiveRootByUserAsync(user); + await store.GetActiveRootByUserAsync(user, ct: TestContext.Current.CancellationToken); result.Should().NotBeNull(); @@ -81,7 +81,7 @@ public async Task GetRootByUserAsync_WhenMissing_ReturnsNull() var result = await store.GetActiveRootByUserAsync( - UserKey.New()); + UserKey.New(), ct: TestContext.Current.CancellationToken); result.Should().BeNull(); } @@ -99,7 +99,7 @@ await CreateRootAsync( var result = await store.GetRootByIdAsync( - root.RootId); + root.RootId, ct: TestContext.Current.CancellationToken); result.Should().NotBeNull(); @@ -122,7 +122,7 @@ public async Task GetRootByIdAsync_WhenMissing_ReturnsNull() var result = await store.GetRootByIdAsync( - missingRoot.RootId); + missingRoot.RootId, ct: TestContext.Current.CancellationToken); result.Should().BeNull(); } @@ -205,11 +205,11 @@ await store.ExecuteAsync( ct => store.SaveRootAsync( updated, expectedVersion: root.Version, - ct)); + ct), ct: TestContext.Current.CancellationToken); var result = await store.GetActiveRootByUserAsync( - root.UserKey); + root.UserKey, ct: TestContext.Current.CancellationToken); result.Should().NotBeNull(); @@ -302,19 +302,19 @@ await store.ExecuteAsync( ct => store.RevokeRootAsync( root.UserKey, revokedAt, - ct)); + ct), ct: TestContext.Current.CancellationToken); // Revoked root is no longer active. var active = await store.GetActiveRootByUserAsync( - root.UserKey); + root.UserKey, ct: TestContext.Current.CancellationToken); active.Should().BeNull(); // But the historical root must still exist. var result = await store.GetRootByIdAsync( - root.RootId); + root.RootId, ct: TestContext.Current.CancellationToken); result.Should().NotBeNull(); @@ -364,11 +364,11 @@ await store.ExecuteAsync( ct => store.RevokeRootAsync( root.UserKey, firstRevokedAt, - ct)); + ct), ct: TestContext.Current.CancellationToken); var afterFirst = await store.GetRootByIdAsync( - root.RootId); + root.RootId, ct: TestContext.Current.CancellationToken); afterFirst.Should().NotBeNull(); afterFirst!.IsRevoked.Should().BeTrue(); @@ -377,11 +377,11 @@ await store.ExecuteAsync( ct => store.RevokeRootAsync( root.UserKey, Now.AddMinutes(10), - ct)); + ct), ct: TestContext.Current.CancellationToken); var afterSecond = await store.GetRootByIdAsync( - root.RootId); + root.RootId, ct: TestContext.Current.CancellationToken); afterSecond.Should().NotBeNull(); afterSecond!.IsRevoked.Should().BeTrue(); @@ -428,10 +428,10 @@ await CreateRootAsync( user); var fromA = - await storeA.GetActiveRootByUserAsync(user); + await storeA.GetActiveRootByUserAsync(user, ct: TestContext.Current.CancellationToken); var fromB = - await storeB.GetActiveRootByUserAsync(user); + await storeB.GetActiveRootByUserAsync(user, ct: TestContext.Current.CancellationToken); fromA.Should().NotBeNull(); fromA!.RootId.Should().Be(root.RootId); @@ -460,11 +460,11 @@ await CreateRootAsync( var fromA = await storeA.GetRootByIdAsync( - root.RootId); + root.RootId, ct: TestContext.Current.CancellationToken); var fromB = await storeB.GetRootByIdAsync( - root.RootId); + root.RootId, ct: TestContext.Current.CancellationToken); fromA.Should().NotBeNull(); fromB.Should().BeNull(); @@ -554,7 +554,7 @@ await store.RevokeRootCascadeAsync( user, Now.AddMinutes(1), ct); - }); + }, ct: TestContext.Current.CancellationToken); var secondRoot = UAuthSessionRoot.Create( Tenant, @@ -566,17 +566,17 @@ await store.ExecuteAsync(async ct => await store.CreateRootAsync( secondRoot, ct); - }); + }, ct: TestContext.Current.CancellationToken); var active = - await store.GetActiveRootByUserAsync(user); + await store.GetActiveRootByUserAsync(user, ct: TestContext.Current.CancellationToken); active.Should().NotBeNull(); active!.RootId.Should().Be(secondRoot.RootId); active.IsRevoked.Should().BeFalse(); var historical = - await store.GetRootByIdAsync(firstRoot.RootId); + await store.GetRootByIdAsync(firstRoot.RootId, ct: TestContext.Current.CancellationToken); historical.Should().NotBeNull(); historical!.IsRevoked.Should().BeTrue(); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Sessions/Store/SessionStoreSessionContractTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Sessions/Store/SessionStoreSessionContractTests.cs index f0323c8d..03ba7bf2 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Sessions/Store/SessionStoreSessionContractTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Sessions/Store/SessionStoreSessionContractTests.cs @@ -119,7 +119,7 @@ await CreateSessionGraphAsync( Tenant); var result = await store.GetSessionAsync( - session.SessionId); + session.SessionId, ct: TestContext.Current.CancellationToken); result.Should().NotBeNull(); @@ -141,7 +141,7 @@ public async Task GetSessionAsync_WhenSessionDoesNotExist_ReturnsNull() var store = db.CreateStore(Tenant); var result = await store.GetSessionAsync( - NewSessionId()); + NewSessionId(), ct: TestContext.Current.CancellationToken); result.Should().BeNull(); } @@ -221,11 +221,11 @@ await store.ExecuteAsync( ct => store.SaveSessionAsync( updated, expectedVersion: session.Version, - ct)); + ct), ct: TestContext.Current.CancellationToken); var result = await store.GetSessionAsync( - session.SessionId); + session.SessionId, ct: TestContext.Current.CancellationToken); result.Should().NotBeNull(); result!.IsRevoked.Should().BeTrue(); @@ -306,12 +306,12 @@ await store.ExecuteAsync(async ct => session.SessionId, Now.AddMinutes(10), ct); - }); + }, ct: TestContext.Current.CancellationToken); revoked.Should().BeTrue(); var result = await store.GetSessionAsync( - session.SessionId); + session.SessionId, ct: TestContext.Current.CancellationToken); result.Should().NotBeNull(); result!.IsRevoked.Should().BeTrue(); @@ -333,7 +333,7 @@ await store.ExecuteAsync(async ct => NewSessionId(), Now, ct); - }); + }, ct: TestContext.Current.CancellationToken); result.Should().BeFalse(); } @@ -353,7 +353,7 @@ await store.ExecuteAsync( ct => store.RevokeSessionAsync( session.SessionId, Now.AddMinutes(5), - ct)); + ct), ct: TestContext.Current.CancellationToken); bool secondResult = true; @@ -364,13 +364,13 @@ await store.RevokeSessionAsync( session.SessionId, Now.AddMinutes(10), ct); - }); + }, ct: TestContext.Current.CancellationToken); secondResult.Should().BeFalse(); var result = await store.GetSessionAsync( - session.SessionId); + session.SessionId, ct: TestContext.Current.CancellationToken); result.Should().NotBeNull(); result!.RevokedAt.Should().Be( @@ -396,11 +396,11 @@ await CreateSessionGraphAsync( await store.ExecuteAsync( ct => store.RemoveSessionAsync( session.SessionId, - ct)); + ct), ct: TestContext.Current.CancellationToken); var result = await store.GetSessionAsync( - session.SessionId); + session.SessionId, ct: TestContext.Current.CancellationToken); result.Should().BeNull(); } @@ -437,7 +437,7 @@ await CreateSessionGraphAsync( var result = await store.GetChainIdBySessionAsync( - session.SessionId); + session.SessionId, ct: TestContext.Current.CancellationToken); result.Should().Be(chain.ChainId); } @@ -449,7 +449,7 @@ public async Task GetChainIdBySessionAsync_WhenMissing_ReturnsNull() var store = db.CreateStore(Tenant); var result = await store.GetChainIdBySessionAsync( - NewSessionId()); + NewSessionId(), ct: TestContext.Current.CancellationToken); result.Should().BeNull(); } @@ -474,10 +474,10 @@ await CreateSessionGraphAsync( chainA.ChainId); await store.ExecuteAsync( - ct => store.CreateSessionAsync(a2, ct)); + ct => store.CreateSessionAsync(a2, ct), ct: TestContext.Current.CancellationToken); var root = - await store.GetActiveRootByUserAsync(user); + await store.GetActiveRootByUserAsync(user, ct: TestContext.Current.CancellationToken); root.Should().NotBeNull(); @@ -493,7 +493,7 @@ await store.ExecuteAsync( root.SecurityVersion); await store.ExecuteAsync( - ct => store.CreateChainAsync(chainB, ct)); + ct => store.CreateChainAsync(chainB, ct), ct: TestContext.Current.CancellationToken); var b1 = CreateSession( Tenant, @@ -501,11 +501,11 @@ await store.ExecuteAsync( chainB.ChainId); await store.ExecuteAsync( - ct => store.CreateSessionAsync(b1, ct)); + ct => store.CreateSessionAsync(b1, ct), ct: TestContext.Current.CancellationToken); var result = await store.GetSessionsByChainAsync( - chainA.ChainId); + chainA.ChainId, ct: TestContext.Current.CancellationToken); result.Should().HaveCount(2); @@ -542,11 +542,11 @@ await CreateSessionGraphAsync( var fromA = await storeA.GetSessionAsync( - session.SessionId); + session.SessionId, ct: TestContext.Current.CancellationToken); var fromB = await storeB.GetSessionAsync( - session.SessionId); + session.SessionId, ct: TestContext.Current.CancellationToken); fromA.Should().NotBeNull(); fromB.Should().BeNull(); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Sessions/UAuthSessionIssuerTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Sessions/UAuthSessionIssuerTests.cs index 04224572..badf5f7c 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Sessions/UAuthSessionIssuerTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Sessions/UAuthSessionIssuerTests.cs @@ -103,7 +103,7 @@ public async Task IssueSessionAsync_WhenActiveRootDoesNotExist_ShouldCreateRoot( SetupEmptySessions(fixture); SetupSessionAndChainPersistence(fixture); - await fixture.Sut.IssueSessionAsync(context); + await fixture.Sut.IssueSessionAsync(context, ct: TestContext.Current.CancellationToken); createdRoot.Should().NotBeNull(); createdRoot!.Tenant.Should().Be(context.Tenant); @@ -136,7 +136,7 @@ public async Task IssueSessionAsync_WhenActiveRootExists_ShouldReuseRoot() SetupSessionAndChainPersistence(fixture); var result = - await fixture.Sut.IssueSessionAsync(context); + await fixture.Sut.IssueSessionAsync(context, ct: TestContext.Current.CancellationToken); result.Session.Should().NotBeNull(); @@ -178,7 +178,7 @@ public async Task IssueSessionAsync_WhenChainIdIsNotProvided_ShouldCreateNewChai SetupChainSave(fixture); var result = - await fixture.Sut.IssueSessionAsync(context); + await fixture.Sut.IssueSessionAsync(context, ct: TestContext.Current.CancellationToken); createdChain.Should().NotBeNull(); @@ -227,7 +227,7 @@ public async Task IssueSessionAsync_WhenExistingChainIsProvided_ShouldReuseChain SetupChainSave(fixture); var result = - await fixture.Sut.IssueSessionAsync(context); + await fixture.Sut.IssueSessionAsync(context, ct: TestContext.Current.CancellationToken); result.Session.ChainId.Should() .Be(chain.ChainId); @@ -279,7 +279,7 @@ public async Task IssueSessionAsync_WhenExplicitChainDoesNotExist_CurrentlyCreat SetupChainSave(fixture); var result = - await fixture.Sut.IssueSessionAsync(context); + await fixture.Sut.IssueSessionAsync(context, ct: TestContext.Current.CancellationToken); createdChain.Should().NotBeNull(); @@ -429,7 +429,7 @@ public async Task IssueSessionAsync_ShouldUseConfiguredSessionLifetime() SetupNewIssuance(fixture, context); var result = - await fixture.Sut.IssueSessionAsync(context); + await fixture.Sut.IssueSessionAsync(context, ct: TestContext.Current.CancellationToken); result.Session.ExpiresAt.Should() .Be(Now.AddHours(4)); @@ -447,7 +447,7 @@ public async Task IssueSessionAsync_WhenMaxLifetimeIsShorter_ShouldCapExpiration SetupNewIssuance(fixture, context); var result = - await fixture.Sut.IssueSessionAsync(context); + await fixture.Sut.IssueSessionAsync(context, ct: TestContext.Current.CancellationToken); result.Session.ExpiresAt.Should() .Be(Now.AddHours(2)); @@ -465,7 +465,7 @@ public async Task IssueSessionAsync_WhenMaxLifetimeIsLonger_ShouldUseNormalLifet SetupNewIssuance(fixture, context); var result = - await fixture.Sut.IssueSessionAsync(context); + await fixture.Sut.IssueSessionAsync(context, ct: TestContext.Current.CancellationToken); result.Session.ExpiresAt.Should() .Be(Now.AddHours(4)); @@ -520,7 +520,7 @@ public async Task IssueSessionAsync_ShouldPersistSessionAndAttachItToChain() .Returns(Task.CompletedTask); var result = - await fixture.Sut.IssueSessionAsync(context); + await fixture.Sut.IssueSessionAsync(context, ct: TestContext.Current.CancellationToken); createdSession.Should().NotBeNull(); savedChain.Should().NotBeNull(); @@ -546,7 +546,7 @@ public async Task IssueSessionAsync_WhenModeIsSemiHybrid_ShouldMarkResultAsMetad SetupNewIssuance(fixture, context); var result = - await fixture.Sut.IssueSessionAsync(context); + await fixture.Sut.IssueSessionAsync(context, ct: TestContext.Current.CancellationToken); result.IsMetadataOnly.Should().BeTrue(); } @@ -562,7 +562,7 @@ public async Task IssueSessionAsync_WhenModeIsHybrid_ShouldNotMarkResultAsMetada SetupNewIssuance(fixture, context); var result = - await fixture.Sut.IssueSessionAsync(context); + await fixture.Sut.IssueSessionAsync(context, ct: TestContext.Current.CancellationToken); result.IsMetadataOnly.Should().BeFalse(); } @@ -638,7 +638,7 @@ public async Task IssueSessionAsync_WhenSessionLimitReached_ShouldRemoveOldNonAc (id, _) => removed.Add(id)) .Returns(Task.CompletedTask); - await fixture.Sut.IssueSessionAsync(context); + await fixture.Sut.IssueSessionAsync(context, ct: TestContext.Current.CancellationToken); removed.Should().HaveCount(2); @@ -662,7 +662,7 @@ public async Task IssueSessionAsync_WhenBelowSessionLimit_ShouldNotRemoveSession SetupNewIssuance(fixture, context); - await fixture.Sut.IssueSessionAsync(context); + await fixture.Sut.IssueSessionAsync(context, ct: TestContext.Current.CancellationToken); fixture.Store.Verify( x => x.RemoveSessionAsync( @@ -1060,7 +1060,7 @@ public async Task RotateSessionAsync_ShouldCreateNewSessionOnSameChain() .Returns(Task.CompletedTask); var result = - await fixture.Sut.RotateSessionAsync(context); + await fixture.Sut.RotateSessionAsync(context, ct: TestContext.Current.CancellationToken); persistedSession.Should().NotBeNull(); @@ -1096,7 +1096,7 @@ public async Task RotateSessionAsync_ShouldRotateChainActiveSession() (chain, _, _) => savedChain = chain) .Returns(Task.CompletedTask); - await fixture.Sut.RotateSessionAsync(context); + await fixture.Sut.RotateSessionAsync(context, ct: TestContext.Current.CancellationToken); savedChain.Should().NotBeNull(); @@ -1134,7 +1134,7 @@ public async Task RotateSessionAsync_ShouldRevokeOldSession() }) .Returns(Task.CompletedTask); - await fixture.Sut.RotateSessionAsync(context); + await fixture.Sut.RotateSessionAsync(context, ct: TestContext.Current.CancellationToken); savedOldSession.Should().NotBeNull(); savedOldSession!.IsRevoked.Should().BeTrue(); @@ -1165,7 +1165,7 @@ public async Task RotateSessionAsync_ShouldReturnSameBoundSessionThatWasPersiste persistedSession = session) .Returns(Task.CompletedTask); - var result = await fixture.Sut.RotateSessionAsync(context); + var result = await fixture.Sut.RotateSessionAsync(context, ct: TestContext.Current.CancellationToken); persistedSession.Should().NotBeNull(); persistedSession!.ChainId.Should().Be(setup.Chain.ChainId); @@ -1187,7 +1187,7 @@ public async Task RotateSessionAsync_WhenModeIsSemiHybrid_ShouldMarkResultAsMeta context); var result = - await fixture.Sut.RotateSessionAsync(context); + await fixture.Sut.RotateSessionAsync(context, ct: TestContext.Current.CancellationToken); result.IsMetadataOnly.Should().BeTrue(); } @@ -1215,7 +1215,7 @@ public async Task RotateSessionAsync_WhenMaxLifetimeIsShorter_ShouldCapNewSessio (session, _) => persisted = session) .Returns(Task.CompletedTask); - await fixture.Sut.RotateSessionAsync(context); + await fixture.Sut.RotateSessionAsync(context, ct: TestContext.Current.CancellationToken); persisted.Should().NotBeNull(); @@ -1246,7 +1246,7 @@ public async Task RevokeSessionAsync_ShouldDelegateToTenantStoreAndReturnResult( await fixture.Sut.RevokeSessionAsync( Tenant, sessionId, - Now); + Now, ct: TestContext.Current.CancellationToken); result.Should().BeTrue(); @@ -1274,7 +1274,7 @@ public async Task RevokeChainAsync_WhenChainDoesNotExist_ShouldDoNothing() await fixture.Sut.RevokeChainAsync( Tenant, chainId, - Now); + Now, ct: TestContext.Current.CancellationToken); fixture.Store.Verify( x => x.RevokeChainCascadeAsync( @@ -1305,7 +1305,7 @@ public async Task RevokeChainAsync_WhenChainExists_ShouldCascadeRevoke() await fixture.Sut.RevokeChainAsync( Tenant, chain.ChainId, - Now); + Now, ct: TestContext.Current.CancellationToken); fixture.Store.Verify( x => x.RevokeChainCascadeAsync( @@ -1348,7 +1348,7 @@ await fixture.Sut.RevokeAllChainsAsync( Tenant, user, exceptChainId: null, - Now); + Now, ct: TestContext.Current.CancellationToken); fixture.Store.Verify( x => x.RevokeChainCascadeAsync( @@ -1394,7 +1394,7 @@ await fixture.Sut.RevokeAllChainsAsync( Tenant, user, keep.ChainId, - Now); + Now, ct: TestContext.Current.CancellationToken); fixture.Store.Verify( x => x.RevokeChainCascadeAsync( @@ -1431,7 +1431,7 @@ public async Task RevokeRootAsync_ShouldCascadeRevokeRoot() await fixture.Sut.RevokeRootAsync( Tenant, user, - Now); + Now, ct: TestContext.Current.CancellationToken); fixture.Store.Verify( x => x.RevokeRootCascadeAsync( @@ -1465,7 +1465,7 @@ public async Task GetChainIdBySessionAsync_ShouldReturnStoreResult() var result = await fixture.Sut.GetChainIdBySessionAsync( Tenant, - sessionId); + sessionId, ct: TestContext.Current.CancellationToken); result.Should().Be(chainId); } @@ -1490,7 +1490,7 @@ public async Task LogoutChainAsync_WhenChainDoesNotExist_ShouldReturnFalse() await fixture.Sut.LogoutChainAsync( Tenant, chainId, - Now); + Now, ct: TestContext.Current.CancellationToken); result.Should().BeFalse(); @@ -1522,7 +1522,7 @@ public async Task LogoutChainAsync_WhenChainIsRevoked_ShouldReturnFalse() await fixture.Sut.LogoutChainAsync( Tenant, chain.ChainId, - Now); + Now, ct: TestContext.Current.CancellationToken); result.Should().BeFalse(); @@ -1556,7 +1556,7 @@ public async Task LogoutChainAsync_WhenChainIsActive_ShouldLogoutAndReturnTrue() await fixture.Sut.LogoutChainAsync( Tenant, chain.ChainId, - Now); + Now, ct: TestContext.Current.CancellationToken); result.Should().BeTrue(); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Tokens/RefreshTokenStoreContractTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Tokens/RefreshTokenStoreContractTests.cs index bf95ebf9..ff3ede7d 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Tokens/RefreshTokenStoreContractTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Tokens/RefreshTokenStoreContractTests.cs @@ -35,10 +35,10 @@ public async Task StoreAsync_PersistsToken() "hash-a11"); await store.ExecuteAsync( - ct => store.StoreAsync(token, ct)); + ct => store.StoreAsync(token, ct), ct: TestContext.Current.CancellationToken); var persisted = await store.FindByHashAsync( - token.TokenHash); + token.TokenHash, ct: TestContext.Current.CancellationToken); persisted.Should().NotBeNull(); @@ -60,7 +60,7 @@ public async Task FindByHashAsync_WhenMissing_ReturnsNull() var store = db.CreateStore(TenantA); var result = await store.FindByHashAsync( - "missing-token-hash"); + "missing-token-hash", ct: TestContext.Current.CancellationToken); result.Should().BeNull(); } @@ -95,13 +95,13 @@ public async Task FindByHashAsync_IsTenantIsolated() "tenant-isolation-token"); await storeA.ExecuteAsync( - ct => storeA.StoreAsync(token, ct)); + ct => storeA.StoreAsync(token, ct), ct: TestContext.Current.CancellationToken); var fromA = await storeA.FindByHashAsync( - token.TokenHash); + token.TokenHash, ct: TestContext.Current.CancellationToken); var fromB = await storeB.FindByHashAsync( - token.TokenHash); + token.TokenHash, ct: TestContext.Current.CancellationToken); fromA.Should().NotBeNull(); fromB.Should().BeNull(); @@ -129,10 +129,10 @@ await store.ExecuteAsync( ct => store.RevokeAsync( token.TokenHash, revokedAt, - ct: ct)); + ct: ct), ct: TestContext.Current.CancellationToken); var persisted = await store.FindByHashAsync( - token.TokenHash); + token.TokenHash, ct: TestContext.Current.CancellationToken); persisted.Should().NotBeNull(); persisted!.IsRevoked.Should().BeTrue(); @@ -159,10 +159,10 @@ await store.ExecuteAsync( token.TokenHash, revokedAt, "rotation-new", - ct)); + ct), ct: TestContext.Current.CancellationToken); var persisted = await store.FindByHashAsync( - token.TokenHash); + token.TokenHash, ct: TestContext.Current.CancellationToken); persisted.Should().NotBeNull(); persisted!.IsRevoked.Should().BeTrue(); @@ -206,17 +206,17 @@ await store.ExecuteAsync( token.TokenHash, firstRevocation, "replacement-1", - ct)); + ct), ct: TestContext.Current.CancellationToken); await store.ExecuteAsync( ct => store.RevokeAsync( token.TokenHash, Now.AddMinutes(20), "replacement-2", - ct)); + ct), ct: TestContext.Current.CancellationToken); var persisted = await store.FindByHashAsync( - token.TokenHash); + token.TokenHash, ct: TestContext.Current.CancellationToken); persisted.Should().NotBeNull(); @@ -250,12 +250,12 @@ public async Task TryConsumeAsync_WhenTokenIsActive_ConsumesTokenAndReturnsTrue( token.TokenHash, consumedAt, "replacement-token", - ct)); + ct), ct: TestContext.Current.CancellationToken); consumed.Should().BeTrue(); var persisted = - await store.FindByHashAsync(token.TokenHash); + await store.FindByHashAsync(token.TokenHash, ct: TestContext.Current.CancellationToken); persisted.Should().NotBeNull(); persisted!.IsRevoked.Should().BeTrue(); @@ -284,7 +284,7 @@ public async Task TryConsumeAsync_WhenTokenAlreadyConsumed_ReturnsFalseAndDoesNo token.TokenHash, firstConsumedAt, "replacement-1", - ct)); + ct), ct: TestContext.Current.CancellationToken); first.Should().BeTrue(); @@ -293,12 +293,12 @@ public async Task TryConsumeAsync_WhenTokenAlreadyConsumed_ReturnsFalseAndDoesNo token.TokenHash, Now.AddMinutes(20), "replacement-2", - ct)); + ct), ct: TestContext.Current.CancellationToken); second.Should().BeFalse(); var persisted = - await store.FindByHashAsync(token.TokenHash); + await store.FindByHashAsync(token.TokenHash, ct: TestContext.Current.CancellationToken); persisted.Should().NotBeNull(); @@ -320,7 +320,7 @@ public async Task TryConsumeAsync_WhenTokenDoesNotExist_ReturnsFalse() "missing-token", Now, "replacement-token", - ct)); + ct), ct: TestContext.Current.CancellationToken); result.Should().BeFalse(); } @@ -349,15 +349,15 @@ public async Task TryConsumeAsync_IsTenantIsolated() tokenA.TokenHash, Now.AddMinutes(10), "replacement-a", - ct)); + ct), ct: TestContext.Current.CancellationToken); consumed.Should().BeTrue(); var persistedA = - await storeA.FindByHashAsync(tokenA.TokenHash); + await storeA.FindByHashAsync(tokenA.TokenHash, ct: TestContext.Current.CancellationToken); var persistedB = - await storeB.FindByHashAsync(tokenB.TokenHash); + await storeB.FindByHashAsync(tokenB.TokenHash, ct: TestContext.Current.CancellationToken); persistedA!.IsRevoked.Should().BeTrue(); @@ -387,14 +387,14 @@ public async Task TryConsumeAsync_WhenCalledConcurrently_AllowsExactlyOneConsume token.TokenHash, consumedAt, "replacement-a", - ct)); + ct), ct: TestContext.Current.CancellationToken); var taskB = storeB.ExecuteAsync( ct => storeB.TryConsumeAsync( token.TokenHash, consumedAt, "replacement-b", - ct)); + ct), ct: TestContext.Current.CancellationToken); var results = await Task.WhenAll( taskA, @@ -408,7 +408,7 @@ public async Task TryConsumeAsync_WhenCalledConcurrently_AllowsExactlyOneConsume var persisted = await storeA.FindByHashAsync( - token.TokenHash); + token.TokenHash, ct: TestContext.Current.CancellationToken); persisted.Should().NotBeNull(); persisted!.IsRevoked.Should().BeTrue(); @@ -438,7 +438,7 @@ public async Task TryConsumeAsync_WhenAlreadyConsumed_DoesNotOverwriteWinningRep token.TokenHash, winningTime, "winner-replacement", - ct)); + ct), ct: TestContext.Current.CancellationToken); winner.Should().BeTrue(); @@ -447,12 +447,12 @@ public async Task TryConsumeAsync_WhenAlreadyConsumed_DoesNotOverwriteWinningRep token.TokenHash, Now.AddMinutes(20), "loser-replacement", - ct)); + ct), ct: TestContext.Current.CancellationToken); loser.Should().BeFalse(); var persisted = - await store.FindByHashAsync(token.TokenHash); + await store.FindByHashAsync(token.TokenHash, ct: TestContext.Current.CancellationToken); persisted!.RevokedAt.Should() .Be(winningTime); @@ -495,12 +495,12 @@ await store.ExecuteAsync( ct => store.RevokeBySessionAsync( session, Now.AddMinutes(10), - ct)); + ct), ct: TestContext.Current.CancellationToken); - (await store.FindByHashAsync(token1.TokenHash))! + (await store.FindByHashAsync(token1.TokenHash, ct: TestContext.Current.CancellationToken))! .IsRevoked.Should().BeTrue(); - (await store.FindByHashAsync(token2.TokenHash))! + (await store.FindByHashAsync(token2.TokenHash, ct: TestContext.Current.CancellationToken))! .IsRevoked.Should().BeTrue(); } @@ -539,12 +539,12 @@ await store.ExecuteAsync( ct => store.RevokeBySessionAsync( targetSession, Now.AddMinutes(10), - ct)); + ct), ct: TestContext.Current.CancellationToken); - (await store.FindByHashAsync(target.TokenHash))! + (await store.FindByHashAsync(target.TokenHash, ct: TestContext.Current.CancellationToken))! .IsRevoked.Should().BeTrue(); - (await store.FindByHashAsync(other.TokenHash))! + (await store.FindByHashAsync(other.TokenHash, ct: TestContext.Current.CancellationToken))! .IsRevoked.Should().BeFalse(); } @@ -581,12 +581,12 @@ await store.ExecuteAsync( ct => store.RevokeByChainAsync( chain, Now.AddMinutes(10), - ct)); + ct), ct: TestContext.Current.CancellationToken); - (await store.FindByHashAsync(token1.TokenHash))! + (await store.FindByHashAsync(token1.TokenHash, ct: TestContext.Current.CancellationToken))! .IsRevoked.Should().BeTrue(); - (await store.FindByHashAsync(token2.TokenHash))! + (await store.FindByHashAsync(token2.TokenHash, ct: TestContext.Current.CancellationToken))! .IsRevoked.Should().BeTrue(); } @@ -621,12 +621,12 @@ await store.ExecuteAsync( ct => store.RevokeByChainAsync( targetChain, Now.AddMinutes(10), - ct)); + ct), ct: TestContext.Current.CancellationToken); - (await store.FindByHashAsync(target.TokenHash))! + (await store.FindByHashAsync(target.TokenHash, ct: TestContext.Current.CancellationToken))! .IsRevoked.Should().BeTrue(); - (await store.FindByHashAsync(other.TokenHash))! + (await store.FindByHashAsync(other.TokenHash, ct: TestContext.Current.CancellationToken))! .IsRevoked.Should().BeFalse(); } @@ -662,12 +662,12 @@ await store.ExecuteAsync( ct => store.RevokeAllForUserAsync( user, Now.AddMinutes(10), - ct)); + ct), ct: TestContext.Current.CancellationToken); - (await store.FindByHashAsync(token1.TokenHash))! + (await store.FindByHashAsync(token1.TokenHash, ct: TestContext.Current.CancellationToken))! .IsRevoked.Should().BeTrue(); - (await store.FindByHashAsync(token2.TokenHash))! + (await store.FindByHashAsync(token2.TokenHash, ct: TestContext.Current.CancellationToken))! .IsRevoked.Should().BeTrue(); } @@ -696,12 +696,12 @@ await store.ExecuteAsync( ct => store.RevokeAllForUserAsync( targetUser, Now.AddMinutes(10), - ct)); + ct), ct: TestContext.Current.CancellationToken); - (await store.FindByHashAsync(target.TokenHash))! + (await store.FindByHashAsync(target.TokenHash, ct: TestContext.Current.CancellationToken))! .IsRevoked.Should().BeTrue(); - (await store.FindByHashAsync(other.TokenHash))! + (await store.FindByHashAsync(other.TokenHash, ct: TestContext.Current.CancellationToken))! .IsRevoked.Should().BeFalse(); } @@ -737,13 +737,13 @@ await storeA.ExecuteAsync( ct => storeA.RevokeAsync( tokenA.TokenHash, Now.AddMinutes(10), - ct: ct)); + ct: ct), ct: TestContext.Current.CancellationToken); var persistedA = - await storeA.FindByHashAsync(tokenA.TokenHash); + await storeA.FindByHashAsync(tokenA.TokenHash, ct: TestContext.Current.CancellationToken); var persistedB = - await storeB.FindByHashAsync(tokenB.TokenHash); + await storeB.FindByHashAsync(tokenB.TokenHash, ct: TestContext.Current.CancellationToken); persistedA!.IsRevoked.Should().BeTrue(); persistedB!.IsRevoked.Should().BeFalse(); @@ -777,12 +777,12 @@ await storeA.ExecuteAsync( ct => storeA.RevokeBySessionAsync( session, Now.AddMinutes(10), - ct)); + ct), ct: TestContext.Current.CancellationToken); - (await storeA.FindByHashAsync(tokenA.TokenHash))! + (await storeA.FindByHashAsync(tokenA.TokenHash, ct: TestContext.Current.CancellationToken))! .IsRevoked.Should().BeTrue(); - (await storeB.FindByHashAsync(tokenB.TokenHash))! + (await storeB.FindByHashAsync(tokenB.TokenHash, ct: TestContext.Current.CancellationToken))! .IsRevoked.Should().BeFalse(); } @@ -813,12 +813,12 @@ await storeA.ExecuteAsync( ct => storeA.RevokeByChainAsync( chain, Now.AddMinutes(10), - ct)); + ct), ct: TestContext.Current.CancellationToken); - (await storeA.FindByHashAsync(tokenA.TokenHash))! + (await storeA.FindByHashAsync(tokenA.TokenHash, ct: TestContext.Current.CancellationToken))! .IsRevoked.Should().BeTrue(); - (await storeB.FindByHashAsync(tokenB.TokenHash))! + (await storeB.FindByHashAsync(tokenB.TokenHash, ct: TestContext.Current.CancellationToken))! .IsRevoked.Should().BeFalse(); } @@ -849,12 +849,12 @@ await storeA.ExecuteAsync( ct => storeA.RevokeAllForUserAsync( user, Now.AddMinutes(10), - ct)); + ct), ct: TestContext.Current.CancellationToken); - (await storeA.FindByHashAsync(tokenA.TokenHash))! + (await storeA.FindByHashAsync(tokenA.TokenHash, ct: TestContext.Current.CancellationToken))! .IsRevoked.Should().BeTrue(); - (await storeB.FindByHashAsync(tokenB.TokenHash))! + (await storeB.FindByHashAsync(tokenB.TokenHash, ct: TestContext.Current.CancellationToken))! .IsRevoked.Should().BeFalse(); } @@ -884,16 +884,16 @@ await store.ExecuteAsync( token.TokenHash, firstRevocation, "replacement-token", - ct)); + ct), ct: TestContext.Current.CancellationToken); await store.ExecuteAsync( ct => store.RevokeAllForUserAsync( user, Now.AddMinutes(20), - ct)); + ct), ct: TestContext.Current.CancellationToken); var persisted = - await store.FindByHashAsync(token.TokenHash); + await store.FindByHashAsync(token.TokenHash, ct: TestContext.Current.CancellationToken); persisted!.RevokedAt.Should().Be(firstRevocation); @@ -962,7 +962,7 @@ public async Task StoreAsync_WhenChainIdIsNull_PersistsToken() await StoreAsync(store, token); var persisted = - await store.FindByHashAsync(token.TokenHash); + await store.FindByHashAsync(token.TokenHash, ct: TestContext.Current.CancellationToken); persisted.Should().NotBeNull(); persisted!.ChainId.Should().BeNull(); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Tokens/UAuthTokenIssuerTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Tokens/UAuthTokenIssuerTests.cs index 453fe9cc..730fa53c 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Tokens/UAuthTokenIssuerTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Tokens/UAuthTokenIssuerTests.cs @@ -51,7 +51,7 @@ public async Task IssueAccessTokenAsync_WhenModeIsPureOpaque_ShouldIssueOpaqueTo var result = await fixture.Sut.IssueAccessTokenAsync( flow, - context); + context, ct: TestContext.Current.CancellationToken); result.Format.Should().Be(TokenFormat.Opaque); result.Token.Should().Be(OpaqueToken); @@ -79,7 +79,7 @@ public async Task IssueAccessTokenAsync_WhenModeIsHybrid_CurrentlyIssuesOpaqueTo var result = await fixture.Sut.IssueAccessTokenAsync( flow, - context); + context, ct: TestContext.Current.CancellationToken); // Characterizes current Hybrid behavior. // Production code explicitly marks JWT Hybrid as a future decision. @@ -104,7 +104,7 @@ public async Task IssueAccessTokenAsync_WhenOpaque_ShouldUseConfiguredLifetime() var result = await fixture.Sut.IssueAccessTokenAsync( flow, - CreateTokenContext()); + CreateTokenContext(), ct: TestContext.Current.CancellationToken); result.ExpiresAt.Should() .Be(Now.AddMinutes(17)); @@ -128,7 +128,7 @@ public async Task IssueAccessTokenAsync_WhenModeUsesJwt_ShouldIssueJwt( var result = await fixture.Sut.IssueAccessTokenAsync( flow, - context); + context, ct: TestContext.Current.CancellationToken); result.Format.Should().Be(TokenFormat.Jwt); result.Token.Should().Be(Jwt); @@ -169,7 +169,7 @@ public async Task IssueAccessTokenAsync_WhenJwt_ShouldBuildDescriptorFromContext var result = await fixture.Sut.IssueAccessTokenAsync( flow, - context); + context, ct: TestContext.Current.CancellationToken); captured.Should().NotBeNull(); @@ -227,7 +227,7 @@ public async Task IssueAccessTokenAsync_WhenJwt_ShouldIncludeSubjectAndTenantCla await fixture.Sut.IssueAccessTokenAsync( flow, - context); + context, ct: TestContext.Current.CancellationToken); captured.Should().NotBeNull(); @@ -270,7 +270,7 @@ public async Task IssueAccessTokenAsync_WhenJwtAndSessionExists_ShouldIncludeSes var result = await fixture.Sut.IssueAccessTokenAsync( flow, - context); + context, ct: TestContext.Current.CancellationToken); captured!.Claims.Should() .ContainKey("sid"); @@ -304,7 +304,7 @@ public async Task IssueAccessTokenAsync_WhenJwtAndSessionDoesNotExist_ShouldNotI var result = await fixture.Sut.IssueAccessTokenAsync( flow, - context); + context, ct: TestContext.Current.CancellationToken); captured!.Claims.Should() .NotContainKey("sid"); @@ -332,7 +332,7 @@ public async Task IssueAccessTokenAsync_WhenJwtIdEnabled_ShouldGenerateJti() await fixture.Sut.IssueAccessTokenAsync( flow, - CreateTokenContext()); + CreateTokenContext(), ct: TestContext.Current.CancellationToken); fixture.OpaqueGenerator.Verify( x => x.GenerateJwtId(), @@ -365,7 +365,7 @@ public async Task IssueAccessTokenAsync_WhenJwtIdDisabled_ShouldNotGenerateJti() await fixture.Sut.IssueAccessTokenAsync( flow, - CreateTokenContext()); + CreateTokenContext(), ct: TestContext.Current.CancellationToken); fixture.OpaqueGenerator.Verify( x => x.GenerateJwtId(), @@ -391,7 +391,7 @@ public async Task IssueRefreshTokenAsync_WhenModeIsPureOpaque_ShouldReturnNull() await fixture.Sut.IssueRefreshTokenAsync( flow, CreateTokenContext(), - RefreshTokenPersistence.Persist); + RefreshTokenPersistence.Persist, ct: TestContext.Current.CancellationToken); result.Should().BeNull(); @@ -426,7 +426,7 @@ public async Task IssueRefreshTokenAsync_WhenSessionIdIsMissing_ShouldReturnNull await fixture.Sut.IssueRefreshTokenAsync( flow, context, - RefreshTokenPersistence.Persist); + RefreshTokenPersistence.Persist, ct: TestContext.Current.CancellationToken); result.Should().BeNull(); @@ -459,7 +459,7 @@ public async Task IssueRefreshTokenAsync_ShouldGenerateAndHashRawToken() var context = CreateTokenContext(sessionId: sessionId); - var result = await fixture.Sut.IssueRefreshTokenAsync(flow, context, RefreshTokenPersistence.DoNotPersist); + var result = await fixture.Sut.IssueRefreshTokenAsync(flow, context, RefreshTokenPersistence.DoNotPersist, ct: TestContext.Current.CancellationToken); result.Should().NotBeNull(); result!.Token.Should().Be(RefreshToken); @@ -484,7 +484,7 @@ public async Task IssueRefreshTokenAsync_ShouldUseConfiguredRefreshLifetime() await fixture.Sut.IssueRefreshTokenAsync( flow, CreateTokenContext(sessionId: sessionId), - RefreshTokenPersistence.DoNotPersist); + RefreshTokenPersistence.DoNotPersist, ct: TestContext.Current.CancellationToken); result!.ExpiresAt.Should() .Be(Now.AddDays(14)); @@ -525,7 +525,7 @@ public async Task IssueRefreshTokenAsync_WhenPersistenceIsPersist_ShouldStoreHas await fixture.Sut.IssueRefreshTokenAsync( flow, context, - RefreshTokenPersistence.Persist); + RefreshTokenPersistence.Persist, ct: TestContext.Current.CancellationToken); captured.Should().NotBeNull(); @@ -577,7 +577,7 @@ public async Task IssueRefreshTokenAsync_WhenPersistenceIsDoNotPersist_ShouldNot await fixture.Sut.IssueRefreshTokenAsync( flow, CreateTokenContext(sessionId: sessionId), - RefreshTokenPersistence.DoNotPersist); + RefreshTokenPersistence.DoNotPersist, ct: TestContext.Current.CancellationToken); result.Should().NotBeNull(); @@ -606,7 +606,7 @@ public async Task IssueRefreshTokenAsync_WhenPersisting_ShouldExecuteInsideStore await fixture.Sut.IssueRefreshTokenAsync( flow, CreateTokenContext(sessionId: sessionId), - RefreshTokenPersistence.Persist); + RefreshTokenPersistence.Persist, ct: TestContext.Current.CancellationToken); fixture.Store.Verify( x => x.ExecuteAsync( @@ -702,7 +702,7 @@ public async Task IssueAccessTokenAsync_WhenCustomClaimsContainReservedClaims_Sh await fixture.Sut.IssueAccessTokenAsync( flow, - context); + context, ct: TestContext.Current.CancellationToken); captured.Should().NotBeNull(); @@ -841,7 +841,7 @@ public async Task IssueAccessTokenAsync_WhenMultipleRolesExist_ShouldPreserveAll .Callback(d => captured = d) .Returns(Jwt); - await fixture.Sut.IssueAccessTokenAsync(flow, context); + await fixture.Sut.IssueAccessTokenAsync(flow, context, ct: TestContext.Current.CancellationToken); captured.Should().NotBeNull(); captured!.Claims.Should().ContainKey("role"); @@ -872,7 +872,7 @@ public async Task IssueAccessTokenAsync_WhenMultiplePermissionsExist_ShouldPrese .Callback(d => captured = d) .Returns(Jwt); - await fixture.Sut.IssueAccessTokenAsync(flow, context); + await fixture.Sut.IssueAccessTokenAsync(flow, context, ct: TestContext.Current.CancellationToken); var permissions = captured!.Claims!["uauth:permission"] .Should().BeAssignableTo().Subject; @@ -898,7 +898,7 @@ public async Task IssueAccessTokenAsync_WhenSingleClaimExists_ShouldUseStringVal .Callback(d => captured = d) .Returns(Jwt); - await fixture.Sut.IssueAccessTokenAsync(flow, context); + await fixture.Sut.IssueAccessTokenAsync(flow, context, ct: TestContext.Current.CancellationToken); captured!.Claims!["department"] .Should().Be("legal"); @@ -926,7 +926,7 @@ public async Task IssueAccessTokenAsync_WhenClaimsAreEmpty_ShouldOnlyIncludeFram descriptor => captured = descriptor) .Returns(Jwt); - await fixture.Sut.IssueAccessTokenAsync(flow, context); + await fixture.Sut.IssueAccessTokenAsync(flow, context, ct: TestContext.Current.CancellationToken); captured.Should().NotBeNull(); @@ -971,7 +971,7 @@ public async Task IssueAccessTokenAsync_WhenCustomClaimIsReserved_ShouldIgnoreIt .Callback(d => captured = d) .Returns(Jwt); - await fixture.Sut.IssueAccessTokenAsync(flow, context); + await fixture.Sut.IssueAccessTokenAsync(flow, context, ct: TestContext.Current.CancellationToken); captured.Should().NotBeNull(); @@ -1003,7 +1003,7 @@ public async Task IssueAccessTokenAsync_WhenCustomSidExists_ShouldUseFrameworkSe .Callback(d => captured = d) .Returns(Jwt); - await fixture.Sut.IssueAccessTokenAsync(flow, context); + await fixture.Sut.IssueAccessTokenAsync(flow, context, ct: TestContext.Current.CancellationToken); captured!.Claims!["sid"] .Should().Be(sessionId.Value); @@ -1029,7 +1029,7 @@ public async Task IssueAccessTokenAsync_WhenCustomJtiExists_ShouldUseGeneratedJt .Callback(d => captured = d) .Returns(Jwt); - await fixture.Sut.IssueAccessTokenAsync(flow, context); + await fixture.Sut.IssueAccessTokenAsync(flow, context, ct: TestContext.Current.CancellationToken); captured!.Claims!["jti"] .Should().Be(JwtId); @@ -1059,7 +1059,7 @@ public async Task IssueAccessTokenAsync_WhenJwtIdDisabled_ShouldRejectCustomJti( .Callback(d => captured = d) .Returns(Jwt); - await fixture.Sut.IssueAccessTokenAsync(flow, context); + await fixture.Sut.IssueAccessTokenAsync(flow, context, ct: TestContext.Current.CancellationToken); captured!.Claims.Should().NotContainKey("jti"); @@ -1091,10 +1091,10 @@ public async Task IssueTokens_ShouldUseEffectiveLifetimes() "effective-session-0000000000000000000001")); var access = await fixture.Sut.IssueAccessTokenAsync( - flow, context); + flow, context, ct: TestContext.Current.CancellationToken); var refresh = await fixture.Sut.IssueRefreshTokenAsync( - flow, context, RefreshTokenPersistence.DoNotPersist); + flow, context, RefreshTokenPersistence.DoNotPersist, ct: TestContext.Current.CancellationToken); access.ExpiresAt.Should().Be(Now.AddMinutes(5)); refresh.Should().NotBeNull(); @@ -1115,7 +1115,7 @@ public async Task IssueRefreshTokenAsync_WhenIssueRefreshDisabled_ShouldReturnNu var result = await fixture.Sut.IssueRefreshTokenAsync( flow, CreateTokenContext(sessionId: null), - RefreshTokenPersistence.Persist); + RefreshTokenPersistence.Persist, ct: TestContext.Current.CancellationToken); result.Should().BeNull(); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/IdentifierConcurrencyTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/IdentifierConcurrencyTests.cs index 557935bc..497d72a1 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/IdentifierConcurrencyTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/IdentifierConcurrencyTests.cs @@ -20,15 +20,15 @@ public async Task Save_should_increment_version() var id = Guid.NewGuid(); var identifier = UserIdentifier.Create(id, TenantKey.Single, TestUsers.Admin, UserIdentifierType.Email, "a@test.com", "a@test.com", now); - await store.AddAsync(identifier); + await store.AddAsync(identifier, ct: TestContext.Current.CancellationToken); - var copy = await store.GetByIdAsync(id); + var copy = await store.GetByIdAsync(id, ct: TestContext.Current.CancellationToken); var expected = copy!.Version; copy.ChangeValue("b@test.com", "b@test.com", now); - await store.SaveAsync(copy, expected); + await store.SaveAsync(copy, expected, ct: TestContext.Current.CancellationToken); - var updated = await store.GetByIdAsync(id); + var updated = await store.GetByIdAsync(id, ct: TestContext.Current.CancellationToken); Assert.Equal(expected + 1, updated!.Version); } @@ -41,18 +41,18 @@ public async Task Delete_should_throw_when_version_conflicts() var id = Guid.NewGuid(); var identifier = UserIdentifier.Create(id, TenantKey.Single, TestUsers.Admin, UserIdentifierType.Email, "a@test.com", "a@test.com", now); - await store.AddAsync(identifier); + await store.AddAsync(identifier, ct: TestContext.Current.CancellationToken); - var copy1 = await store.GetByIdAsync(id); - var copy2 = await store.GetByIdAsync(id); + var copy1 = await store.GetByIdAsync(id, ct: TestContext.Current.CancellationToken); + var copy2 = await store.GetByIdAsync(id, ct: TestContext.Current.CancellationToken); var expected1 = copy1!.Version; copy1.MarkDeleted(now); - await store.SaveAsync(copy1, expected1); + await store.SaveAsync(copy1, expected1, ct: TestContext.Current.CancellationToken); await Assert.ThrowsAsync(async () => { - await store.DeleteAsync(copy2!.Id, copy2!.Version, DeleteMode.Soft, now); + await store.DeleteAsync(copy2!.Id, copy2!.Version, DeleteMode.Soft, now, ct: TestContext.Current.CancellationToken); }); } @@ -64,7 +64,7 @@ public async Task Parallel_SetPrimary_should_conflict_deterministic() var id = Guid.NewGuid(); var identifier = UserIdentifier.Create(id, TenantKey.Single, TestUsers.Admin, UserIdentifierType.Email, "a@test.com", "a@test.com", now); - await store.AddAsync(identifier); + await store.AddAsync(identifier, ct: TestContext.Current.CancellationToken); int success = 0; int conflicts = 0; @@ -97,7 +97,7 @@ public async Task Parallel_SetPrimary_should_conflict_deterministic() Assert.Equal(1, success); Assert.Equal(1, conflicts); - var final = await store.GetByIdAsync(id); + var final = await store.GetByIdAsync(id, ct: TestContext.Current.CancellationToken); Assert.True(final!.IsPrimary); Assert.Equal(1, final.Version); } @@ -111,21 +111,21 @@ public async Task Update_should_throw_concurrency_when_versions_conflict() var tenant = TenantKey.Single; var identifier = UserIdentifier.Create(id, TenantKey.Single, TestUsers.Admin, UserIdentifierType.Email, "a@test.com", "a@test.com", now); - await store.AddAsync(identifier); + await store.AddAsync(identifier, ct: TestContext.Current.CancellationToken); - var copy1 = await store.GetByIdAsync(id); - var copy2 = await store.GetByIdAsync(id); + var copy1 = await store.GetByIdAsync(id, ct: TestContext.Current.CancellationToken); + var copy2 = await store.GetByIdAsync(id, ct: TestContext.Current.CancellationToken); var expected1 = copy1!.Version; copy1.ChangeValue("b@test.com", "b@test.com", now); - await store.SaveAsync(copy1, expected1); + await store.SaveAsync(copy1, expected1, ct: TestContext.Current.CancellationToken); var expected2 = copy2!.Version; copy2.ChangeValue("c@test.com", "c@test.com", now); await Assert.ThrowsAsync(async () => { - await store.SaveAsync(copy2, expected2); + await store.SaveAsync(copy2, expected2, ct: TestContext.Current.CancellationToken); }); } @@ -138,7 +138,7 @@ public async Task Parallel_updates_should_result_in_single_success_deterministic var id = Guid.NewGuid(); var identifier = UserIdentifier.Create(id, TenantKey.Single, TestUsers.Admin, UserIdentifierType.Email, "a@test.com", "a@test.com", now); - await store.AddAsync(identifier); + await store.AddAsync(identifier, ct: TestContext.Current.CancellationToken); int success = 0; int conflicts = 0; @@ -174,7 +174,7 @@ public async Task Parallel_updates_should_result_in_single_success_deterministic Assert.Equal(1, success); Assert.Equal(1, conflicts); - var final = await store.GetByIdAsync(id); + var final = await store.GetByIdAsync(id, ct: TestContext.Current.CancellationToken); Assert.NotNull(final); Assert.Equal(1, final!.Version); @@ -189,7 +189,7 @@ public async Task High_contention_updates_should_allow_only_one_success() var id = Guid.NewGuid(); var identifier = UserIdentifier.Create(id, TenantKey.Single, TestUsers.Admin, UserIdentifierType.Email, "initial@test.com", "initial@test.com", now); - await store.AddAsync(identifier); + await store.AddAsync(identifier, ct: TestContext.Current.CancellationToken); int success = 0; int conflicts = 0; @@ -222,7 +222,7 @@ public async Task High_contention_updates_should_allow_only_one_success() Assert.True(success >= 1); Assert.Equal(20, success + conflicts); - var final = await store.GetByIdAsync(id); + var final = await store.GetByIdAsync(id, ct: TestContext.Current.CancellationToken); Assert.Equal(success, final!.Version); } @@ -236,7 +236,7 @@ public async Task High_contention_SetPrimary_should_allow_only_one_deterministic var id = Guid.NewGuid(); var identifier = UserIdentifier.Create(id, TenantKey.Single, TestUsers.Admin, UserIdentifierType.Email, "primary@test.com", "primary@test.com", now); - await store.AddAsync(identifier); + await store.AddAsync(identifier, ct: TestContext.Current.CancellationToken); int success = 0; int conflicts = 0; @@ -269,7 +269,7 @@ public async Task High_contention_SetPrimary_should_allow_only_one_deterministic Assert.Equal(1, success); Assert.Equal(19, conflicts); - var final = await store.GetByIdAsync(id); + var final = await store.GetByIdAsync(id, ct: TestContext.Current.CancellationToken); Assert.True(final!.IsPrimary); Assert.Equal(1, final.Version); @@ -289,8 +289,8 @@ public async Task Two_identifiers_racing_for_primary_should_allow() var identifier1 = UserIdentifier.Create(id1, TenantKey.Single, TestUsers.Admin, UserIdentifierType.Email, "a@test.com", "a@test.com", now); var identifier2 = UserIdentifier.Create(id2, TenantKey.Single, TestUsers.Admin, UserIdentifierType.Email, "b@test.com", "b@test.com", now); - await store.AddAsync(identifier1); - await store.AddAsync(identifier2); + await store.AddAsync(identifier1, ct: TestContext.Current.CancellationToken); + await store.AddAsync(identifier2, ct: TestContext.Current.CancellationToken); int success = 0; int conflicts = 0; @@ -318,7 +318,7 @@ public async Task Two_identifiers_racing_for_primary_should_allow() { Interlocked.Increment(ref conflicts); } - }), + }, cancellationToken: TestContext.Current.CancellationToken), Task.Run(async () => { try @@ -338,7 +338,7 @@ public async Task Two_identifiers_racing_for_primary_should_allow() { Interlocked.Increment(ref conflicts); } - }) + }, cancellationToken: TestContext.Current.CancellationToken) }; await Task.WhenAll(tasks); @@ -346,7 +346,7 @@ public async Task Two_identifiers_racing_for_primary_should_allow() Assert.Equal(2, success); Assert.Equal(0, conflicts); - var all = await store.GetByUserAsync(user); + var all = await store.GetByUserAsync(user, ct: TestContext.Current.CancellationToken); var primaries = all .Where(x => x.Type == UserIdentifierType.Email && x.IsPrimary) diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/Store/Query/UserSummaryQueryStoreContractTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/Store/Query/UserSummaryQueryStoreContractTests.cs index 860497da..1b8df684 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/Store/Query/UserSummaryQueryStoreContractTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/Store/Query/UserSummaryQueryStoreContractTests.cs @@ -28,7 +28,7 @@ public async Task QueryAsync_WhenEmpty_ReturnsEmptyPage() var store = db.CreateStore(TenantA); - var result = await store.QueryAsync(new UserQuery()); + var result = await store.QueryAsync(new UserQuery(), ct: TestContext.Current.CancellationToken); result.Items.Should().BeEmpty(); result.TotalCount.Should().Be(0); @@ -53,10 +53,10 @@ await db.SeedAsync(TenantB, ]); var resultA = await db.CreateStore(TenantA) - .QueryAsync(new UserQuery()); + .QueryAsync(new UserQuery(), ct: TestContext.Current.CancellationToken); var resultB = await db.CreateStore(TenantB) - .QueryAsync(new UserQuery()); + .QueryAsync(new UserQuery(), ct: TestContext.Current.CancellationToken); resultA.TotalCount.Should().Be(1); resultB.TotalCount.Should().Be(1); @@ -87,7 +87,7 @@ public async Task QueryAsync_With1501Users_ReturnsCorrectLastPage() { PageNumber = 7, PageSize = 250 - }); + }, ct: TestContext.Current.CancellationToken); result.TotalCount.Should().Be(1501); result.Items.Should().ContainSingle(); @@ -119,7 +119,7 @@ public async Task QueryAsync_SearchesBeyondFirst1000Users() { Search = "target@example.com", PageSize = 50 - }); + }, ct: TestContext.Current.CancellationToken); result.TotalCount.Should().Be(1); result.Items.Should().ContainSingle(); @@ -151,7 +151,7 @@ public async Task QueryAsync_SearchesSupportedFields( await db.SeedAsync(TenantA, [target]); var result = await db.CreateStore(TenantA) - .QueryAsync(new UserQuery { Search = search }); + .QueryAsync(new UserQuery { Search = search }, ct: TestContext.Current.CancellationToken); result.TotalCount.Should().Be(1); result.Items.Should().ContainSingle(); @@ -173,7 +173,7 @@ await db.SeedAsync(TenantA, .QueryAsync(new UserQuery { Search = "does-not-exist" - }); + }, ct: TestContext.Current.CancellationToken); result.TotalCount.Should().Be(0); result.Items.Should().BeEmpty(); @@ -195,7 +195,7 @@ await db.SeedAsync(TenantA, ]); var result = await db.CreateStore(TenantA) - .QueryAsync(new UserQuery { Search = search }); + .QueryAsync(new UserQuery { Search = search }, ct: TestContext.Current.CancellationToken); result.TotalCount.Should().Be(2); } @@ -224,7 +224,7 @@ public async Task QueryAsync_PaginationHasNoMissingOrDuplicateUsers() { PageNumber = page, PageSize = 10 - }); + }, ct: TestContext.Current.CancellationToken); result.TotalCount.Should().Be(105); collected.AddRange(result.Items.Select(x => x.UserKey)); @@ -254,7 +254,7 @@ public async Task QueryAsync_PageBeyondLast_ReturnsEmptyButCorrectTotal() { PageNumber = 10, PageSize = 10 - }); + }, ct: TestContext.Current.CancellationToken); result.TotalCount.Should().Be(15); result.Items.Should().BeEmpty(); @@ -276,7 +276,7 @@ await db.SeedAsync(TenantA, { PageNumber = int.MaxValue, PageSize = 1000 - }); + }, ct: TestContext.Current.CancellationToken); result.Items.Should().BeEmpty(); result.TotalCount.Should().Be(1); @@ -304,7 +304,7 @@ public async Task QueryAsync_FiltersByStatus() .QueryAsync(new UserQuery { Status = UserStatus.Active - }); + }, ct: TestContext.Current.CancellationToken); result.TotalCount.Should().Be(1); result.Items.Single().UserKey.Should().Be(active.UserKey); @@ -328,9 +328,9 @@ public async Task QueryAsync_ExcludesDeletedUsersByDefault() var store = db.CreateStore(TenantA); - var normal = await store.QueryAsync(new UserQuery()); + var normal = await store.QueryAsync(new UserQuery(), ct: TestContext.Current.CancellationToken); var includingDeleted = await store.QueryAsync( - new UserQuery { IncludeDeleted = true }); + new UserQuery { IncludeDeleted = true }, ct: TestContext.Current.CancellationToken); normal.TotalCount.Should().Be(1); normal.Items.Single().UserKey.Should().Be(active.UserKey); @@ -352,7 +352,7 @@ public async Task QueryAsync_UserWithoutProfileIsStillReturned() await db.SeedAsync(TenantA, [user]); var result = await db.CreateStore(TenantA) - .QueryAsync(new UserQuery()); + .QueryAsync(new UserQuery(), ct: TestContext.Current.CancellationToken); result.Items.Should().ContainSingle(); result.Items[0].UserKey.Should().Be(user.UserKey); @@ -379,13 +379,13 @@ await db.SeedAsync(TenantA, var store = db.CreateStore(TenantA); var defaultResult = await store.QueryAsync( - new UserQuery()); + new UserQuery(), ct: TestContext.Current.CancellationToken); var businessResult = await store.QueryAsync( new UserQuery { ProfileKey = customProfile - }); + }, ct: TestContext.Current.CancellationToken); defaultResult.TotalCount.Should().Be(1); defaultResult.Items.Single().DisplayName.Should().BeNull(); @@ -415,7 +415,7 @@ public async Task QueryAsync_SearchTreatsSqlWildcardsLiterally( .QueryAsync(new UserQuery { Search = search - }); + }, ct: TestContext.Current.CancellationToken); result.TotalCount.Should().Be(1); result.Items.Single().UserKey.Should().Be(target.UserKey); @@ -457,7 +457,7 @@ await db.SeedAsync(TenantA, .QueryAsync(new UserQuery { Search = key.Value - }); + }, ct: TestContext.Current.CancellationToken); result.TotalCount.Should().Be(1); result.Items.Single().UserKey.Should().Be(key); @@ -479,7 +479,7 @@ await db.SeedAsync(TenantA, .QueryAsync(new UserQuery { Search = "user-key-12345" - }); + }, ct: TestContext.Current.CancellationToken); result.TotalCount.Should().Be(0); result.Items.Should().BeEmpty(); @@ -491,7 +491,7 @@ public async Task GetByKeysAsync_WhenEmpty_ReturnsEmpty() await using var db = await CreateDatabaseAsync(); var result = await db.CreateStore(TenantA) - .GetByKeysAsync(Array.Empty()); + .GetByKeysAsync(Array.Empty(), ct: TestContext.Current.CancellationToken); result.Should().BeEmpty(); } @@ -511,7 +511,7 @@ await db.SeedAsync(TenantA, ]); var result = await db.CreateStore(TenantA) - .GetByKeysAsync([first, first, second, second]); + .GetByKeysAsync([first, first, second, second], ct: TestContext.Current.CancellationToken); result.Should().HaveCount(2); @@ -533,7 +533,7 @@ await db.SeedAsync(TenantA, ]); var result = await db.CreateStore(TenantA) - .GetByKeysAsync([existing, missing]); + .GetByKeysAsync([existing, missing], ct: TestContext.Current.CancellationToken); result.Should().ContainSingle(); @@ -559,7 +559,7 @@ await db.SeedAsync(TenantA, ]); var result = await db.CreateStore(TenantA) - .GetByKeysAsync([active, deleted]); + .GetByKeysAsync([active, deleted], ct: TestContext.Current.CancellationToken); result.Should().ContainSingle(); @@ -595,10 +595,10 @@ await db.SeedAsync(TenantB, }; var resultA = await db.CreateStore(TenantA) - .GetByKeysAsync(requested); + .GetByKeysAsync(requested, ct: TestContext.Current.CancellationToken); var resultB = await db.CreateStore(TenantB) - .GetByKeysAsync(requested); + .GetByKeysAsync(requested, ct: TestContext.Current.CancellationToken); resultA.Should().HaveCount(2); resultB.Should().HaveCount(2); @@ -638,7 +638,7 @@ await db.SeedAsync(TenantA, ]); var result = await db.CreateStore(TenantA) - .GetByKeysAsync([key]); + .GetByKeysAsync([key], ct: TestContext.Current.CancellationToken); var user = result.Should().ContainSingle().Subject; @@ -667,7 +667,7 @@ await db.SeedAsync(TenantA, ]); var result = await db.CreateStore(TenantA) - .GetByKeysAsync([key]); + .GetByKeysAsync([key], ct: TestContext.Current.CancellationToken); result.Should().ContainSingle(); @@ -693,7 +693,7 @@ await db.SeedAsync(TenantA, ]); var result = await db.CreateStore(TenantA) - .GetByKeysAsync([key]); + .GetByKeysAsync([key], ct: TestContext.Current.CancellationToken); result.Should().ContainSingle(); result[0].DisplayName.Should().BeNull(); @@ -718,7 +718,7 @@ public async Task GetByKeysAsync_With250Keys_ReturnsAllUsers() .ToArray(); var result = await db.CreateStore(TenantA) - .GetByKeysAsync(keys); + .GetByKeysAsync(keys, ct: TestContext.Current.CancellationToken); result.Should().HaveCount(250); @@ -748,7 +748,7 @@ public async Task GetByKeysAsync_FindsUsersBeyondFirstLifecyclePage() }; var result = await db.CreateStore(TenantA) - .GetByKeysAsync(requested); + .GetByKeysAsync(requested, ct: TestContext.Current.CancellationToken); result.Should().HaveCount(3); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/Store/UserIdentifierStoreContractTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/Store/UserIdentifierStoreContractTests.cs index 38d11f3a..fa8a4bfc 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/Store/UserIdentifierStoreContractTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/Store/UserIdentifierStoreContractTests.cs @@ -55,9 +55,9 @@ public async Task AddAsync_PersistsIdentifier() TenantA, isPrimary: true); - await store.AddAsync(identifier); + await store.AddAsync(identifier, ct: TestContext.Current.CancellationToken); - var persisted = await store.GetByIdAsync(identifier.Id); + var persisted = await store.GetByIdAsync(identifier.Id, ct: TestContext.Current.CancellationToken); persisted.Should().NotBeNull(); persisted!.Id.Should().Be(identifier.Id); @@ -77,7 +77,7 @@ public async Task GetByIdAsync_WhenMissing_ReturnsNull() await using var db = await CreateDatabaseAsync(); var store = db.CreateStore(TenantA); - var result = await store.GetByIdAsync(Guid.NewGuid()); + var result = await store.GetByIdAsync(Guid.NewGuid(), ct: TestContext.Current.CancellationToken); result.Should().BeNull(); } @@ -90,11 +90,11 @@ public async Task GetAsync_ByTypeAndNormalizedValue_ReturnsIdentifier() var identifier = CreateIdentifier(TenantA); - await store.AddAsync(identifier); + await store.AddAsync(identifier, ct: TestContext.Current.CancellationToken); var result = await store.GetAsync( UserIdentifierType.Email, - "USER@EXAMPLE.COM"); + "USER@EXAMPLE.COM", ct: TestContext.Current.CancellationToken); result.Should().NotBeNull(); result!.Id.Should().Be(identifier.Id); @@ -108,17 +108,17 @@ public async Task GetAsync_ByTypeAndNormalizedValue_ExcludesDeleted() var identifier = CreateIdentifier(TenantA); - await store.AddAsync(identifier); + await store.AddAsync(identifier, ct: TestContext.Current.CancellationToken); await store.DeleteAsync( identifier.Id, expectedVersion: 0, DeleteMode.Soft, - Now.AddHours(1)); + Now.AddHours(1), ct: TestContext.Current.CancellationToken); var result = await store.GetAsync( identifier.Type, - identifier.NormalizedValue); + identifier.NormalizedValue, ct: TestContext.Current.CancellationToken); result.Should().BeNull(); } @@ -151,17 +151,17 @@ public async Task GetByUserAsync_ReturnsOnlyActiveIdentifiersForUser() value: "b@example.com", normalizedValue: "B@EXAMPLE.COM"); - await store.AddAsync(first); - await store.AddAsync(second); - await store.AddAsync(otherUser); + await store.AddAsync(first, ct: TestContext.Current.CancellationToken); + await store.AddAsync(second, ct: TestContext.Current.CancellationToken); + await store.AddAsync(otherUser, ct: TestContext.Current.CancellationToken); await store.DeleteAsync( second.Id, 0, DeleteMode.Soft, - Now.AddHours(1)); + Now.AddHours(1), ct: TestContext.Current.CancellationToken); - var result = await store.GetByUserAsync(userA); + var result = await store.GetByUserAsync(userA, ct: TestContext.Current.CancellationToken); result.Should().ContainSingle(); result.Single().Id.Should().Be(first.Id); @@ -179,7 +179,7 @@ public async Task SaveAsync_PersistsChangesAndIncrementsVersion() var identifier = CreateIdentifier(TenantA); - await store.AddAsync(identifier); + await store.AddAsync(identifier, ct: TestContext.Current.CancellationToken); var changedAt = Now.AddMinutes(10); @@ -190,9 +190,9 @@ public async Task SaveAsync_PersistsChangesAndIncrementsVersion() await store.SaveAsync( identifier, - expectedVersion: 0); + expectedVersion: 0, ct: TestContext.Current.CancellationToken); - var persisted = await store.GetByIdAsync(identifier.Id); + var persisted = await store.GetByIdAsync(identifier.Id, ct: TestContext.Current.CancellationToken); persisted.Should().NotBeNull(); persisted!.Value.Should().Be("changed@example.com"); @@ -223,10 +223,10 @@ public async Task SaveAsync_WhenVersionIsStale_ThrowsConcurrency() var identifier = CreateIdentifier(TenantA); - await store.AddAsync(identifier); + await store.AddAsync(identifier, ct: TestContext.Current.CancellationToken); identifier.MarkVerified(Now.AddMinutes(1)); - await store.SaveAsync(identifier, 0); + await store.SaveAsync(identifier, 0, ct: TestContext.Current.CancellationToken); var act = () => store.SaveAsync( identifier, @@ -262,11 +262,11 @@ public async Task AddAsync_PrimaryIdentifier_UnsetsExistingPrimaryOfSameType() normalizedValue: "SECOND@EXAMPLE.COM", isPrimary: true); - await store.AddAsync(first); - await store.AddAsync(second); + await store.AddAsync(first, ct: TestContext.Current.CancellationToken); + await store.AddAsync(second, ct: TestContext.Current.CancellationToken); - var persistedFirst = await store.GetByIdAsync(first.Id); - var persistedSecond = await store.GetByIdAsync(second.Id); + var persistedFirst = await store.GetByIdAsync(first.Id, ct: TestContext.Current.CancellationToken); + var persistedSecond = await store.GetByIdAsync(second.Id, ct: TestContext.Current.CancellationToken); persistedFirst!.IsPrimary.Should().BeFalse(); persistedSecond!.IsPrimary.Should().BeTrue(); @@ -296,13 +296,13 @@ public async Task AddAsync_PrimaryIdentifier_DoesNotUnsetPrimaryOfDifferentType( "ALICE", isPrimary: true); - await store.AddAsync(email); - await store.AddAsync(username); + await store.AddAsync(email, ct: TestContext.Current.CancellationToken); + await store.AddAsync(username, ct: TestContext.Current.CancellationToken); - (await store.GetByIdAsync(email.Id))! + (await store.GetByIdAsync(email.Id, ct: TestContext.Current.CancellationToken))! .IsPrimary.Should().BeTrue(); - (await store.GetByIdAsync(username.Id))! + (await store.GetByIdAsync(username.Id, ct: TestContext.Current.CancellationToken))! .IsPrimary.Should().BeTrue(); } @@ -327,17 +327,17 @@ public async Task SaveAsync_SetPrimary_UnsetsExistingPrimaryOfSameType() value: "second@example.com", normalizedValue: "SECOND@EXAMPLE.COM"); - await store.AddAsync(first); - await store.AddAsync(second); + await store.AddAsync(first, ct: TestContext.Current.CancellationToken); + await store.AddAsync(second, ct: TestContext.Current.CancellationToken); second.SetPrimary(Now.AddMinutes(1)); - await store.SaveAsync(second, 0); + await store.SaveAsync(second, 0, ct: TestContext.Current.CancellationToken); - (await store.GetByIdAsync(first.Id))! + (await store.GetByIdAsync(first.Id, ct: TestContext.Current.CancellationToken))! .IsPrimary.Should().BeFalse(); - (await store.GetByIdAsync(second.Id))! + (await store.GetByIdAsync(second.Id, ct: TestContext.Current.CancellationToken))! .IsPrimary.Should().BeTrue(); } @@ -355,7 +355,7 @@ public async Task DeleteAsync_Soft_PreservesRecordAndClearsPrimary() TenantA, isPrimary: true); - await store.AddAsync(identifier); + await store.AddAsync(identifier, ct: TestContext.Current.CancellationToken); var deletedAt = Now.AddHours(1); @@ -363,9 +363,9 @@ await store.DeleteAsync( identifier.Id, 0, DeleteMode.Soft, - deletedAt); + deletedAt, ct: TestContext.Current.CancellationToken); - var persisted = await store.GetByIdAsync(identifier.Id); + var persisted = await store.GetByIdAsync(identifier.Id, ct: TestContext.Current.CancellationToken); persisted.Should().NotBeNull(); persisted!.IsDeleted.Should().BeTrue(); @@ -382,15 +382,15 @@ public async Task DeleteAsync_Hard_RemovesIdentifier() var identifier = CreateIdentifier(TenantA); - await store.AddAsync(identifier); + await store.AddAsync(identifier, ct: TestContext.Current.CancellationToken); await store.DeleteAsync( identifier.Id, 0, DeleteMode.Hard, - Now); + Now, ct: TestContext.Current.CancellationToken); - (await store.GetByIdAsync(identifier.Id)) + (await store.GetByIdAsync(identifier.Id, ct: TestContext.Current.CancellationToken)) .Should().BeNull(); } @@ -402,10 +402,10 @@ public async Task DeleteAsync_WhenVersionIsStale_ThrowsConcurrency() var identifier = CreateIdentifier(TenantA); - await store.AddAsync(identifier); + await store.AddAsync(identifier, ct: TestContext.Current.CancellationToken); identifier.MarkVerified(Now.AddMinutes(1)); - await store.SaveAsync(identifier, 0); + await store.SaveAsync(identifier, 0, ct: TestContext.Current.CancellationToken); var act = () => store.DeleteAsync( identifier.Id, @@ -444,7 +444,7 @@ public async Task SaveAsync_WhenEntityBelongsToDifferentTenant_IsRejected() var storeB = db.CreateStore(TenantB); var identifier = CreateIdentifier(TenantB); - await storeB.AddAsync(identifier); + await storeB.AddAsync(identifier, ct: TestContext.Current.CancellationToken); var act = () => storeA.SaveAsync( identifier, @@ -463,9 +463,9 @@ public async Task GetByIdAsync_IsTenantIsolated() var storeB = db.CreateStore(TenantB); var identifier = CreateIdentifier(TenantB); - await storeB.AddAsync(identifier); + await storeB.AddAsync(identifier, ct: TestContext.Current.CancellationToken); - var result = await storeA.GetByIdAsync(identifier.Id); + var result = await storeA.GetByIdAsync(identifier.Id, ct: TestContext.Current.CancellationToken); result.Should().BeNull(); } @@ -535,13 +535,13 @@ public async Task ExistsAsync_TenantAny_FindsMatchingIdentifier() var store = db.CreateStore(TenantA); var identifier = CreateIdentifier(TenantA); - await store.AddAsync(identifier); + await store.AddAsync(identifier, ct: TestContext.Current.CancellationToken); var result = await store.ExistsAsync( new IdentifierExistenceQuery( identifier.Type, identifier.NormalizedValue, - IdentifierExistenceScope.TenantAny)); + IdentifierExistenceScope.TenantAny), ct: TestContext.Current.CancellationToken); result.Exists.Should().BeTrue(); result.OwnerUserKey.Should().Be(identifier.UserKey); @@ -555,14 +555,14 @@ public async Task ExistsAsync_WithinUser_DoesNotMatchOtherUser() var store = db.CreateStore(TenantA); var identifier = CreateIdentifier(TenantA); - await store.AddAsync(identifier); + await store.AddAsync(identifier, ct: TestContext.Current.CancellationToken); var result = await store.ExistsAsync( new IdentifierExistenceQuery( identifier.Type, identifier.NormalizedValue, IdentifierExistenceScope.WithinUser, - UserKey: UserKey.New())); + UserKey: UserKey.New()), ct: TestContext.Current.CancellationToken); result.Exists.Should().BeFalse(); } @@ -577,13 +577,13 @@ public async Task ExistsAsync_TenantPrimaryOnly_IgnoresNonPrimary() TenantA, isPrimary: false); - await store.AddAsync(identifier); + await store.AddAsync(identifier, ct: TestContext.Current.CancellationToken); var result = await store.ExistsAsync( new IdentifierExistenceQuery( identifier.Type, identifier.NormalizedValue, - IdentifierExistenceScope.TenantPrimaryOnly)); + IdentifierExistenceScope.TenantPrimaryOnly), ct: TestContext.Current.CancellationToken); result.Exists.Should().BeFalse(); } @@ -595,14 +595,14 @@ public async Task ExistsAsync_ExcludeIdentifierId_ExcludesCurrentIdentifier() var store = db.CreateStore(TenantA); var identifier = CreateIdentifier(TenantA); - await store.AddAsync(identifier); + await store.AddAsync(identifier, ct: TestContext.Current.CancellationToken); var result = await store.ExistsAsync( new IdentifierExistenceQuery( identifier.Type, identifier.NormalizedValue, IdentifierExistenceScope.TenantAny, - ExcludeIdentifierId: identifier.Id)); + ExcludeIdentifierId: identifier.Id), ct: TestContext.Current.CancellationToken); result.Exists.Should().BeFalse(); } @@ -623,26 +623,26 @@ public async Task ExistsAsync_SoftDeletedIdentifier_RemainsReserved(UserIdentifi normalizedValue: normalizedValue, isPrimary: true); - await store.AddAsync(identifier); + await store.AddAsync(identifier, ct: TestContext.Current.CancellationToken); await store.DeleteAsync( identifier.Id, expectedVersion: 0, DeleteMode.Soft, - Now.AddHours(1)); + Now.AddHours(1), ct: TestContext.Current.CancellationToken); // A deleted identifier still reserves its normalized value. var exists = await store.ExistsAsync( new IdentifierExistenceQuery( type, normalizedValue, - IdentifierExistenceScope.TenantAny)); + IdentifierExistenceScope.TenantAny), ct: TestContext.Current.CancellationToken); exists.Exists.Should().BeTrue(); exists.OwnerIdentifierId.Should().Be(identifier.Id); // Ordinary lookup must not resolve deleted identifiers. - var lookup = await store.GetAsync(type, normalizedValue); + var lookup = await store.GetAsync(type, normalizedValue, ct: TestContext.Current.CancellationToken); lookup.Should().BeNull(); // Excluding the matching identifier must still work. @@ -651,7 +651,7 @@ await store.DeleteAsync( type, normalizedValue, IdentifierExistenceScope.TenantAny, - ExcludeIdentifierId: identifier.Id)); + ExcludeIdentifierId: identifier.Id), ct: TestContext.Current.CancellationToken); excluded.Exists.Should().BeFalse(); } @@ -681,12 +681,12 @@ public async Task GetByUsersAsync_ReturnsActiveIdentifiersForRequestedUsersOnly( value: "c@example.com", normalizedValue: "C@EXAMPLE.COM"); - await store.AddAsync(a); - await store.AddAsync(b); - await store.AddAsync(c); + await store.AddAsync(a, ct: TestContext.Current.CancellationToken); + await store.AddAsync(b, ct: TestContext.Current.CancellationToken); + await store.AddAsync(c, ct: TestContext.Current.CancellationToken); var result = await store.GetByUsersAsync( - [userA, userB]); + [userA, userB], ct: TestContext.Current.CancellationToken); result.Should().HaveCount(2); result.Select(x => x.UserKey) @@ -721,23 +721,23 @@ public async Task DeleteByUserAsync_Soft_DeletesOnlySpecifiedUsersIdentifiers() value: "b@example.com", normalizedValue: "B@EXAMPLE.COM"); - await store.AddAsync(a1); - await store.AddAsync(a2); - await store.AddAsync(b); + await store.AddAsync(a1, ct: TestContext.Current.CancellationToken); + await store.AddAsync(a2, ct: TestContext.Current.CancellationToken); + await store.AddAsync(b, ct: TestContext.Current.CancellationToken); var deletedAt = Now.AddHours(1); await store.DeleteByUserAsync( userA, DeleteMode.Soft, - deletedAt); + deletedAt, ct: TestContext.Current.CancellationToken); var query = await store.QueryAsync( new UserIdentifierQuery { UserKey = userA, IncludeDeleted = true - }); + }, ct: TestContext.Current.CancellationToken); query.Items.Should().HaveCount(2); query.Items.Should().OnlyContain(x => @@ -745,7 +745,7 @@ await store.DeleteByUserAsync( x.DeletedAt == deletedAt && !x.IsPrimary); - (await store.GetByUserAsync(userB)) + (await store.GetByUserAsync(userB, ct: TestContext.Current.CancellationToken)) .Should() .ContainSingle(); } @@ -769,16 +769,16 @@ public async Task DeleteByUserAsync_Hard_RemovesOnlySpecifiedUsersIdentifiers() value: "b@example.com", normalizedValue: "B@EXAMPLE.COM"); - await store.AddAsync(a); - await store.AddAsync(b); + await store.AddAsync(a, ct: TestContext.Current.CancellationToken); + await store.AddAsync(b, ct: TestContext.Current.CancellationToken); await store.DeleteByUserAsync( userA, DeleteMode.Hard, - Now); + Now, ct: TestContext.Current.CancellationToken); - (await store.GetByIdAsync(a.Id)).Should().BeNull(); - (await store.GetByIdAsync(b.Id)).Should().NotBeNull(); + (await store.GetByIdAsync(a.Id, ct: TestContext.Current.CancellationToken)).Should().BeNull(); + (await store.GetByIdAsync(b.Id, ct: TestContext.Current.CancellationToken)).Should().NotBeNull(); } [Fact] @@ -812,20 +812,20 @@ public async Task QueryAsync_ByDefault_ExcludesDeleted() value: "deleted@example.com", normalizedValue: "DELETED@EXAMPLE.COM"); - await store.AddAsync(active); - await store.AddAsync(deleted); + await store.AddAsync(active, ct: TestContext.Current.CancellationToken); + await store.AddAsync(deleted, ct: TestContext.Current.CancellationToken); await store.DeleteAsync( deleted.Id, 0, DeleteMode.Soft, - Now.AddHours(1)); + Now.AddHours(1), ct: TestContext.Current.CancellationToken); var result = await store.QueryAsync( new UserIdentifierQuery { UserKey = user - }); + }, ct: TestContext.Current.CancellationToken); result.Items.Should().ContainSingle(); result.Items.Single().Id.Should().Be(active.Id); @@ -850,21 +850,21 @@ public async Task QueryAsync_IncludeDeleted_ReturnsDeletedIdentifiers() value: "deleted@example.com", normalizedValue: "DELETED@EXAMPLE.COM"); - await store.AddAsync(active); - await store.AddAsync(deleted); + await store.AddAsync(active, ct: TestContext.Current.CancellationToken); + await store.AddAsync(deleted, ct: TestContext.Current.CancellationToken); await store.DeleteAsync( deleted.Id, 0, DeleteMode.Soft, - Now.AddHours(1)); + Now.AddHours(1), ct: TestContext.Current.CancellationToken); var result = await store.QueryAsync( new UserIdentifierQuery { UserKey = user, IncludeDeleted = true - }); + }, ct: TestContext.Current.CancellationToken); result.Items.Should().HaveCount(2); result.TotalCount.Should().Be(2); @@ -886,7 +886,7 @@ await store.AddAsync( user, value: $"user{i}@example.com", normalizedValue: $"USER{i}@EXAMPLE.COM", - createdAt: Now.AddMinutes(i))); + createdAt: Now.AddMinutes(i)), ct: TestContext.Current.CancellationToken); } var result = await store.QueryAsync( @@ -896,7 +896,7 @@ await store.AddAsync( PageNumber = 2, PageSize = 2, SortBy = nameof(UserIdentifier.CreatedAt) - }); + }, ct: TestContext.Current.CancellationToken); result.TotalCount.Should().Be(5); result.Items.Should().HaveCount(2); @@ -918,24 +918,24 @@ public async Task QueryAsync_SortByNormalizedValue_IsSupported() await store.AddAsync(CreateIdentifier( TenantA, user, value: "c@example.com", - normalizedValue: "C@EXAMPLE.COM")); + normalizedValue: "C@EXAMPLE.COM"), ct: TestContext.Current.CancellationToken); await store.AddAsync(CreateIdentifier( TenantA, user, value: "a@example.com", - normalizedValue: "A@EXAMPLE.COM")); + normalizedValue: "A@EXAMPLE.COM"), ct: TestContext.Current.CancellationToken); await store.AddAsync(CreateIdentifier( TenantA, user, value: "b@example.com", - normalizedValue: "B@EXAMPLE.COM")); + normalizedValue: "B@EXAMPLE.COM"), ct: TestContext.Current.CancellationToken); var result = await store.QueryAsync( new UserIdentifierQuery { UserKey = user, SortBy = nameof(UserIdentifier.NormalizedValue) - }); + }, ct: TestContext.Current.CancellationToken); result.Items.Select(x => x.NormalizedValue) .Should() diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/Store/UserLifecycleStoreContractTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/Store/UserLifecycleStoreContractTests.cs index 9b3bb133..ee20b966 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/Store/UserLifecycleStoreContractTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/Store/UserLifecycleStoreContractTests.cs @@ -44,10 +44,10 @@ public async Task AddAsync_PersistsLifecycle() var lifecycle = CreateLifecycle(TenantA); - await store.AddAsync(lifecycle); + await store.AddAsync(lifecycle, ct: TestContext.Current.CancellationToken); var persisted = await store.GetAsync( - new UserLifecycleKey(TenantA, lifecycle.UserKey)); + new UserLifecycleKey(TenantA, lifecycle.UserKey), ct: TestContext.Current.CancellationToken); persisted.Should().NotBeNull(); persisted!.Id.Should().Be(lifecycle.Id); @@ -66,7 +66,7 @@ public async Task GetAsync_WhenMissing_ReturnsNull() var store = db.CreateStore(TenantA); var result = await store.GetAsync( - new UserLifecycleKey(TenantA, UserKey.New())); + new UserLifecycleKey(TenantA, UserKey.New()), ct: TestContext.Current.CancellationToken); result.Should().BeNull(); } @@ -78,10 +78,10 @@ public async Task ExistsAsync_WhenPresent_ReturnsTrue() var store = db.CreateStore(TenantA); var lifecycle = CreateLifecycle(TenantA); - await store.AddAsync(lifecycle); + await store.AddAsync(lifecycle, ct: TestContext.Current.CancellationToken); var result = await store.ExistsAsync( - new UserLifecycleKey(TenantA, lifecycle.UserKey)); + new UserLifecycleKey(TenantA, lifecycle.UserKey), ct: TestContext.Current.CancellationToken); result.Should().BeTrue(); } @@ -93,7 +93,7 @@ public async Task ExistsAsync_WhenMissing_ReturnsFalse() var store = db.CreateStore(TenantA); var result = await store.ExistsAsync( - new UserLifecycleKey(TenantA, UserKey.New())); + new UserLifecycleKey(TenantA, UserKey.New()), ct: TestContext.Current.CancellationToken); result.Should().BeFalse(); } @@ -106,7 +106,7 @@ public async Task AddAsync_WhenSameUserAlreadyExists_ThrowsConflict() var user = UserKey.New(); - await store.AddAsync(CreateLifecycle(TenantA, user)); + await store.AddAsync(CreateLifecycle(TenantA, user), ct: TestContext.Current.CancellationToken); var act = () => store.AddAsync( CreateLifecycle(TenantA, user)); @@ -141,7 +141,7 @@ public async Task SaveAsync_PersistsDomainChanges_AndIncrementsVersion() var store = db.CreateStore(TenantA); var lifecycle = CreateLifecycle(TenantA); - await store.AddAsync(lifecycle); + await store.AddAsync(lifecycle, ct: TestContext.Current.CancellationToken); var changedAt = Now.AddMinutes(10); @@ -153,10 +153,10 @@ public async Task SaveAsync_PersistsDomainChanges_AndIncrementsVersion() await store.SaveAsync( lifecycle, - expectedVersion: 0); + expectedVersion: 0, ct: TestContext.Current.CancellationToken); var persisted = await store.GetAsync( - new UserLifecycleKey(TenantA, lifecycle.UserKey)); + new UserLifecycleKey(TenantA, lifecycle.UserKey), ct: TestContext.Current.CancellationToken); persisted.Should().NotBeNull(); persisted!.Status.Should().Be(UserStatus.Suspended); @@ -188,11 +188,11 @@ public async Task SaveAsync_WhenVersionIsStale_ThrowsConcurrency() var store = db.CreateStore(TenantA); var lifecycle = CreateLifecycle(TenantA); - await store.AddAsync(lifecycle); + await store.AddAsync(lifecycle, ct: TestContext.Current.CancellationToken); lifecycle.IncrementSecurityVersion(); - await store.SaveAsync(lifecycle, 0); + await store.SaveAsync(lifecycle, 0, ct: TestContext.Current.CancellationToken); lifecycle.IncrementSecurityVersion(); @@ -217,15 +217,15 @@ public async Task DeleteAsync_Soft_PreservesRecordAndDeletionState() var lifecycle = CreateLifecycle(TenantA); var key = new UserLifecycleKey(TenantA, lifecycle.UserKey); - await store.AddAsync(lifecycle); + await store.AddAsync(lifecycle, ct: TestContext.Current.CancellationToken); await store.DeleteAsync( key, expectedVersion: 0, DeleteMode.Soft, - Now.AddHours(1)); + Now.AddHours(1), ct: TestContext.Current.CancellationToken); - var persisted = await store.GetAsync(key); + var persisted = await store.GetAsync(key, ct: TestContext.Current.CancellationToken); persisted.Should().NotBeNull(); persisted!.IsDeleted.Should().BeTrue(); @@ -242,16 +242,16 @@ public async Task DeleteAsync_Hard_RemovesRecord() var lifecycle = CreateLifecycle(TenantA); var key = new UserLifecycleKey(TenantA, lifecycle.UserKey); - await store.AddAsync(lifecycle); + await store.AddAsync(lifecycle, ct: TestContext.Current.CancellationToken); await store.DeleteAsync( key, expectedVersion: 0, DeleteMode.Hard, - Now); + Now, ct: TestContext.Current.CancellationToken); - (await store.GetAsync(key)).Should().BeNull(); - (await store.ExistsAsync(key)).Should().BeFalse(); + (await store.GetAsync(key, ct: TestContext.Current.CancellationToken)).Should().BeNull(); + (await store.ExistsAsync(key, ct: TestContext.Current.CancellationToken)).Should().BeFalse(); } [Fact] @@ -279,10 +279,10 @@ public async Task DeleteAsync_WhenVersionIsStale_ThrowsConcurrency() var lifecycle = CreateLifecycle(TenantA); var key = new UserLifecycleKey(TenantA, lifecycle.UserKey); - await store.AddAsync(lifecycle); + await store.AddAsync(lifecycle, ct: TestContext.Current.CancellationToken); lifecycle.IncrementSecurityVersion(); - await store.SaveAsync(lifecycle, 0); + await store.SaveAsync(lifecycle, 0, ct: TestContext.Current.CancellationToken); var act = () => store.DeleteAsync( key, @@ -321,7 +321,7 @@ public async Task SaveAsync_WhenEntityBelongsToDifferentTenant_IsRejected() var storeB = db.CreateStore(TenantB); var lifecycle = CreateLifecycle(TenantB); - await storeB.AddAsync(lifecycle); + await storeB.AddAsync(lifecycle, ct: TestContext.Current.CancellationToken); var act = () => storeA.SaveAsync( lifecycle, @@ -339,14 +339,14 @@ public async Task QueryAsync_IsTenantIsolated() var storeA = db.CreateStore(TenantA); var storeB = db.CreateStore(TenantB); - await storeA.AddAsync(CreateLifecycle(TenantA)); - await storeB.AddAsync(CreateLifecycle(TenantB)); + await storeA.AddAsync(CreateLifecycle(TenantA), ct: TestContext.Current.CancellationToken); + await storeB.AddAsync(CreateLifecycle(TenantB), ct: TestContext.Current.CancellationToken); var resultA = await storeA.QueryAsync( - new UserLifecycleQuery()); + new UserLifecycleQuery(), ct: TestContext.Current.CancellationToken); var resultB = await storeB.QueryAsync( - new UserLifecycleQuery()); + new UserLifecycleQuery(), ct: TestContext.Current.CancellationToken); resultA.Items.Should().ContainSingle(); resultA.Items.Single().Tenant.Should().Be(TenantA); @@ -368,17 +368,17 @@ public async Task QueryAsync_ByDefault_ExcludesDeleted() var active = CreateLifecycle(TenantA); var deleted = CreateLifecycle(TenantA); - await store.AddAsync(active); - await store.AddAsync(deleted); + await store.AddAsync(active, ct: TestContext.Current.CancellationToken); + await store.AddAsync(deleted, ct: TestContext.Current.CancellationToken); await store.DeleteAsync( new UserLifecycleKey(TenantA, deleted.UserKey), 0, DeleteMode.Soft, - Now.AddHours(1)); + Now.AddHours(1), ct: TestContext.Current.CancellationToken); var result = await store.QueryAsync( - new UserLifecycleQuery()); + new UserLifecycleQuery(), ct: TestContext.Current.CancellationToken); result.Items.Should().ContainSingle(); result.Items.Single().UserKey.Should().Be(active.UserKey); @@ -394,20 +394,20 @@ public async Task QueryAsync_IncludeDeleted_ReturnsDeletedRecords() var active = CreateLifecycle(TenantA); var deleted = CreateLifecycle(TenantA); - await store.AddAsync(active); - await store.AddAsync(deleted); + await store.AddAsync(active, ct: TestContext.Current.CancellationToken); + await store.AddAsync(deleted, ct: TestContext.Current.CancellationToken); await store.DeleteAsync( new UserLifecycleKey(TenantA, deleted.UserKey), 0, DeleteMode.Soft, - Now.AddHours(1)); + Now.AddHours(1), ct: TestContext.Current.CancellationToken); var result = await store.QueryAsync( new UserLifecycleQuery { IncludeDeleted = true - }); + }, ct: TestContext.Current.CancellationToken); result.Items.Should().HaveCount(2); result.TotalCount.Should().Be(2); @@ -433,14 +433,14 @@ public async Task QueryAsync_Status_FiltersResults() Now.AddMinutes(1), UserStatus.Suspended); - await store.AddAsync(active); - await store.AddAsync(suspended); + await store.AddAsync(active, ct: TestContext.Current.CancellationToken); + await store.AddAsync(suspended, ct: TestContext.Current.CancellationToken); var result = await store.QueryAsync( new UserLifecycleQuery { Status = UserStatus.Suspended - }); + }, ct: TestContext.Current.CancellationToken); result.Items.Should().ContainSingle(); result.Items.Single().UserKey.Should().Be(suspended.UserKey); @@ -462,7 +462,7 @@ public async Task QueryAsync_AppliesPagination() await store.AddAsync( CreateLifecycle( TenantA, - createdAt: Now.AddMinutes(i))); + createdAt: Now.AddMinutes(i)), ct: TestContext.Current.CancellationToken); } var result = await store.QueryAsync( @@ -471,7 +471,7 @@ await store.AddAsync( PageNumber = 2, PageSize = 2, SortBy = nameof(UserLifecycle.CreatedAt) - }); + }, ct: TestContext.Current.CancellationToken); result.Items.Should().HaveCount(2); result.TotalCount.Should().Be(5); @@ -504,16 +504,16 @@ public async Task QueryAsync_SortsCreatedAtAscending() TenantA, createdAt: Now.AddMinutes(2)); - await store.AddAsync(third); - await store.AddAsync(first); - await store.AddAsync(second); + await store.AddAsync(third, ct: TestContext.Current.CancellationToken); + await store.AddAsync(first, ct: TestContext.Current.CancellationToken); + await store.AddAsync(second, ct: TestContext.Current.CancellationToken); var result = await store.QueryAsync( new UserLifecycleQuery { SortBy = nameof(UserLifecycle.CreatedAt), Descending = false - }); + }, ct: TestContext.Current.CancellationToken); result.Items.Select(x => x.CreatedAt) .Should() @@ -530,20 +530,20 @@ public async Task QueryAsync_SortsCreatedAtDescending() var store = db.CreateStore(TenantA); await store.AddAsync( - CreateLifecycle(TenantA, createdAt: Now.AddMinutes(1))); + CreateLifecycle(TenantA, createdAt: Now.AddMinutes(1)), ct: TestContext.Current.CancellationToken); await store.AddAsync( - CreateLifecycle(TenantA, createdAt: Now.AddMinutes(3))); + CreateLifecycle(TenantA, createdAt: Now.AddMinutes(3)), ct: TestContext.Current.CancellationToken); await store.AddAsync( - CreateLifecycle(TenantA, createdAt: Now.AddMinutes(2))); + CreateLifecycle(TenantA, createdAt: Now.AddMinutes(2)), ct: TestContext.Current.CancellationToken); var result = await store.QueryAsync( new UserLifecycleQuery { SortBy = nameof(UserLifecycle.CreatedAt), Descending = true - }); + }, ct: TestContext.Current.CancellationToken); result.Items.Select(x => x.CreatedAt) .Should() @@ -563,16 +563,16 @@ public async Task QueryAsync_SortByUserKey_IsSupported() var secondUser = UserKey.FromString("user-b"); var thirdUser = UserKey.FromString("user-c"); - await store.AddAsync(CreateLifecycle(TenantA, thirdUser)); - await store.AddAsync(CreateLifecycle(TenantA, firstUser)); - await store.AddAsync(CreateLifecycle(TenantA, secondUser)); + await store.AddAsync(CreateLifecycle(TenantA, thirdUser), ct: TestContext.Current.CancellationToken); + await store.AddAsync(CreateLifecycle(TenantA, firstUser), ct: TestContext.Current.CancellationToken); + await store.AddAsync(CreateLifecycle(TenantA, secondUser), ct: TestContext.Current.CancellationToken); var result = await store.QueryAsync( new UserLifecycleQuery { SortBy = nameof(UserLifecycle.UserKey), Descending = false - }); + }, ct: TestContext.Current.CancellationToken); result.Items.Select(x => x.UserKey.Value) .Should() @@ -591,20 +591,20 @@ public async Task QueryAsync_SortByDeletedAt_IsSupported() var first = CreateLifecycle(TenantA); var second = CreateLifecycle(TenantA); - await store.AddAsync(first); - await store.AddAsync(second); + await store.AddAsync(first, ct: TestContext.Current.CancellationToken); + await store.AddAsync(second, ct: TestContext.Current.CancellationToken); await store.DeleteAsync( new UserLifecycleKey(TenantA, first.UserKey), 0, DeleteMode.Soft, - Now.AddHours(1)); + Now.AddHours(1), ct: TestContext.Current.CancellationToken); await store.DeleteAsync( new UserLifecycleKey(TenantA, second.UserKey), 0, DeleteMode.Soft, - Now.AddHours(2)); + Now.AddHours(2), ct: TestContext.Current.CancellationToken); var result = await store.QueryAsync( new UserLifecycleQuery @@ -612,7 +612,7 @@ await store.DeleteAsync( IncludeDeleted = true, SortBy = nameof(UserLifecycle.DeletedAt), Descending = false - }); + }, ct: TestContext.Current.CancellationToken); result.Items.Select(x => x.DeletedAt) .Should() diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/Store/UserProfileStoreContractTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/Store/UserProfileStoreContractTests.cs index 522d4c29..eed54630 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/Store/UserProfileStoreContractTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/Store/UserProfileStoreContractTests.cs @@ -59,14 +59,14 @@ public async Task AddAsync_PersistsProfile() var profile = CreateProfile(TenantA); - await store.AddAsync(profile); + await store.AddAsync(profile, ct: TestContext.Current.CancellationToken); var key = new UserProfileKey( TenantA, profile.UserKey, profile.ProfileKey); - var persisted = await store.GetAsync(key); + var persisted = await store.GetAsync(key, ct: TestContext.Current.CancellationToken); persisted.Should().NotBeNull(); persisted!.Id.Should().Be(profile.Id); @@ -91,7 +91,7 @@ public async Task GetAsync_WhenMissing_ReturnsNull() new UserProfileKey( TenantA, UserKey.New(), - ProfileKey.Default)); + ProfileKey.Default), ct: TestContext.Current.CancellationToken); result.Should().BeNull(); } @@ -103,13 +103,13 @@ public async Task ExistsAsync_WhenPresent_ReturnsTrue() var store = db.CreateStore(TenantA); var profile = CreateProfile(TenantA); - await store.AddAsync(profile); + await store.AddAsync(profile, ct: TestContext.Current.CancellationToken); var result = await store.ExistsAsync( new UserProfileKey( TenantA, profile.UserKey, - profile.ProfileKey)); + profile.ProfileKey), ct: TestContext.Current.CancellationToken); result.Should().BeTrue(); } @@ -124,7 +124,7 @@ public async Task ExistsAsync_WhenMissing_ReturnsFalse() new UserProfileKey( TenantA, UserKey.New(), - ProfileKey.Default)); + ProfileKey.Default), ct: TestContext.Current.CancellationToken); result.Should().BeFalse(); } @@ -138,7 +138,7 @@ public async Task AddAsync_WhenSameUserAndProfileKeyAlreadyExists_ThrowsConflict var user = UserKey.New(); await store.AddAsync( - CreateProfile(TenantA, user)); + CreateProfile(TenantA, user), ct: TestContext.Current.CancellationToken); var duplicate = CreateProfile( TenantA, @@ -171,10 +171,10 @@ public async Task SameUser_CanHaveDifferentProfileKeys() user, secondaryKey); - await store.AddAsync(defaultProfile); - await store.AddAsync(workProfile); + await store.AddAsync(defaultProfile, ct: TestContext.Current.CancellationToken); + await store.AddAsync(workProfile, ct: TestContext.Current.CancellationToken); - var profiles = await store.GetAllProfilesByUserAsync(user); + var profiles = await store.GetAllProfilesByUserAsync(user, ct: TestContext.Current.CancellationToken); profiles.Should().HaveCount(2); profiles.Select(x => x.ProfileKey) @@ -209,7 +209,7 @@ public async Task SaveAsync_PersistsAllMutableFields_AndIncrementsVersion() var profile = CreateProfile(TenantA); - await store.AddAsync(profile); + await store.AddAsync(profile, ct: TestContext.Current.CancellationToken); var changedAt = Now.AddMinutes(10); @@ -241,13 +241,13 @@ public async Task SaveAsync_PersistsAllMutableFields_AndIncrementsVersion() await store.SaveAsync( profile, - expectedVersion: 0); + expectedVersion: 0, ct: TestContext.Current.CancellationToken); var persisted = await store.GetAsync( new UserProfileKey( TenantA, profile.UserKey, - profile.ProfileKey)); + profile.ProfileKey), ct: TestContext.Current.CancellationToken); persisted.Should().NotBeNull(); @@ -294,7 +294,7 @@ public async Task SaveAsync_WhenVersionIsStale_ThrowsConcurrency() var store = db.CreateStore(TenantA); var profile = CreateProfile(TenantA); - await store.AddAsync(profile); + await store.AddAsync(profile, ct: TestContext.Current.CancellationToken); profile.UpdateName( "First", @@ -302,7 +302,7 @@ public async Task SaveAsync_WhenVersionIsStale_ThrowsConcurrency() "First Update", Now.AddMinutes(1)); - await store.SaveAsync(profile, 0); + await store.SaveAsync(profile, 0, ct: TestContext.Current.CancellationToken); profile.UpdateName( "Second", @@ -329,7 +329,7 @@ public async Task DeleteAsync_Soft_PreservesProfileAndDeletionState() var store = db.CreateStore(TenantA); var profile = CreateProfile(TenantA); - await store.AddAsync(profile); + await store.AddAsync(profile, ct: TestContext.Current.CancellationToken); var deletedAt = Now.AddHours(1); @@ -340,13 +340,13 @@ await store.DeleteAsync( profile.ProfileKey), expectedVersion: 0, DeleteMode.Soft, - deletedAt); + deletedAt, ct: TestContext.Current.CancellationToken); var persisted = await store.GetAsync( new UserProfileKey( TenantA, profile.UserKey, - profile.ProfileKey)); + profile.ProfileKey), ct: TestContext.Current.CancellationToken); persisted.Should().NotBeNull(); persisted!.IsDeleted.Should().BeTrue(); @@ -366,16 +366,16 @@ public async Task DeleteAsync_Hard_RemovesProfile() profile.UserKey, profile.ProfileKey); - await store.AddAsync(profile); + await store.AddAsync(profile, ct: TestContext.Current.CancellationToken); await store.DeleteAsync( key, 0, DeleteMode.Hard, - Now); + Now, ct: TestContext.Current.CancellationToken); - (await store.GetAsync(key)).Should().BeNull(); - (await store.ExistsAsync(key)).Should().BeFalse(); + (await store.GetAsync(key, ct: TestContext.Current.CancellationToken)).Should().BeNull(); + (await store.ExistsAsync(key, ct: TestContext.Current.CancellationToken)).Should().BeFalse(); } [Fact] @@ -409,7 +409,7 @@ public async Task DeleteAsync_WhenVersionIsStale_ThrowsConcurrency() profile.UserKey, profile.ProfileKey); - await store.AddAsync(profile); + await store.AddAsync(profile, ct: TestContext.Current.CancellationToken); profile.UpdateName( "Changed", @@ -417,7 +417,7 @@ public async Task DeleteAsync_WhenVersionIsStale_ThrowsConcurrency() null, Now.AddMinutes(1)); - await store.SaveAsync(profile, 0); + await store.SaveAsync(profile, 0, ct: TestContext.Current.CancellationToken); var act = () => store.DeleteAsync( key, @@ -456,7 +456,7 @@ public async Task SaveAsync_WhenEntityBelongsToDifferentTenant_IsRejected() var storeB = db.CreateStore(TenantB); var profile = CreateProfile(TenantB); - await storeB.AddAsync(profile); + await storeB.AddAsync(profile, ct: TestContext.Current.CancellationToken); var act = () => storeA.SaveAsync( profile, @@ -474,14 +474,14 @@ public async Task QueryAsync_IsTenantIsolated() var storeA = db.CreateStore(TenantA); var storeB = db.CreateStore(TenantB); - await storeA.AddAsync(CreateProfile(TenantA)); - await storeB.AddAsync(CreateProfile(TenantB)); + await storeA.AddAsync(CreateProfile(TenantA), ct: TestContext.Current.CancellationToken); + await storeB.AddAsync(CreateProfile(TenantB), ct: TestContext.Current.CancellationToken); var resultA = await storeA.QueryAsync( - new UserProfileQuery()); + new UserProfileQuery(), ct: TestContext.Current.CancellationToken); var resultB = await storeB.QueryAsync( - new UserProfileQuery()); + new UserProfileQuery(), ct: TestContext.Current.CancellationToken); resultA.Items.Should().ContainSingle(); resultA.Items.Single().Tenant.Should().Be(TenantA); @@ -514,8 +514,8 @@ public async Task GetAllProfilesByUserAsync_ReturnsActiveProfilesOnly() user, secondaryKey); - await store.AddAsync(first); - await store.AddAsync(second); + await store.AddAsync(first, ct: TestContext.Current.CancellationToken); + await store.AddAsync(second, ct: TestContext.Current.CancellationToken); await store.DeleteAsync( new UserProfileKey( @@ -524,10 +524,10 @@ await store.DeleteAsync( secondaryKey), 0, DeleteMode.Soft, - Now.AddHours(1)); + Now.AddHours(1), ct: TestContext.Current.CancellationToken); var result = - await store.GetAllProfilesByUserAsync(user); + await store.GetAllProfilesByUserAsync(user, ct: TestContext.Current.CancellationToken); result.Should().ContainSingle(); result.Single().ProfileKey.Should().Be(ProfileKey.Default); @@ -542,11 +542,11 @@ public async Task GetAllProfilesByUserAsync_DoesNotReturnOtherUsersProfiles() var userA = UserKey.New(); var userB = UserKey.New(); - await store.AddAsync(CreateProfile(TenantA, userA)); - await store.AddAsync(CreateProfile(TenantA, userB)); + await store.AddAsync(CreateProfile(TenantA, userA), ct: TestContext.Current.CancellationToken); + await store.AddAsync(CreateProfile(TenantA, userB), ct: TestContext.Current.CancellationToken); var result = - await store.GetAllProfilesByUserAsync(userA); + await store.GetAllProfilesByUserAsync(userA, ct: TestContext.Current.CancellationToken); result.Should().ContainSingle(); result.Single().UserKey.Should().Be(userA); @@ -562,13 +562,13 @@ public async Task GetByUsersAsync_ReturnsRequestedUsersForProfileKey() var userB = UserKey.New(); var userC = UserKey.New(); - await store.AddAsync(CreateProfile(TenantA, userA)); - await store.AddAsync(CreateProfile(TenantA, userB)); - await store.AddAsync(CreateProfile(TenantA, userC)); + await store.AddAsync(CreateProfile(TenantA, userA), ct: TestContext.Current.CancellationToken); + await store.AddAsync(CreateProfile(TenantA, userB), ct: TestContext.Current.CancellationToken); + await store.AddAsync(CreateProfile(TenantA, userC), ct: TestContext.Current.CancellationToken); var result = await store.GetByUsersAsync( [userA, userB], - ProfileKey.Default); + ProfileKey.Default, ct: TestContext.Current.CancellationToken); result.Should().HaveCount(2); @@ -589,8 +589,8 @@ public async Task GetByUsersAsync_ExcludesDeletedProfiles() var first = CreateProfile(TenantA, userA); var second = CreateProfile(TenantA, userB); - await store.AddAsync(first); - await store.AddAsync(second); + await store.AddAsync(first, ct: TestContext.Current.CancellationToken); + await store.AddAsync(second, ct: TestContext.Current.CancellationToken); await store.DeleteAsync( new UserProfileKey( @@ -599,11 +599,11 @@ await store.DeleteAsync( ProfileKey.Default), 0, DeleteMode.Soft, - Now.AddHours(1)); + Now.AddHours(1), ct: TestContext.Current.CancellationToken); var result = await store.GetByUsersAsync( [userA, userB], - ProfileKey.Default); + ProfileKey.Default, ct: TestContext.Current.CancellationToken); result.Should().ContainSingle(); result.Single().UserKey.Should().Be(userA); @@ -622,8 +622,8 @@ public async Task QueryAsync_ByDefault_ExcludesDeleted() var active = CreateProfile(TenantA); var deleted = CreateProfile(TenantA); - await store.AddAsync(active); - await store.AddAsync(deleted); + await store.AddAsync(active, ct: TestContext.Current.CancellationToken); + await store.AddAsync(deleted, ct: TestContext.Current.CancellationToken); await store.DeleteAsync( new UserProfileKey( @@ -632,10 +632,10 @@ await store.DeleteAsync( deleted.ProfileKey), 0, DeleteMode.Soft, - Now.AddHours(1)); + Now.AddHours(1), ct: TestContext.Current.CancellationToken); var result = await store.QueryAsync( - new UserProfileQuery()); + new UserProfileQuery(), ct: TestContext.Current.CancellationToken); result.Items.Should().ContainSingle(); result.Items.Single().Id.Should().Be(active.Id); @@ -651,8 +651,8 @@ public async Task QueryAsync_IncludeDeleted_ReturnsDeletedProfiles() var active = CreateProfile(TenantA); var deleted = CreateProfile(TenantA); - await store.AddAsync(active); - await store.AddAsync(deleted); + await store.AddAsync(active, ct: TestContext.Current.CancellationToken); + await store.AddAsync(deleted, ct: TestContext.Current.CancellationToken); await store.DeleteAsync( new UserProfileKey( @@ -661,13 +661,13 @@ await store.DeleteAsync( deleted.ProfileKey), 0, DeleteMode.Soft, - Now.AddHours(1)); + Now.AddHours(1), ct: TestContext.Current.CancellationToken); var result = await store.QueryAsync( new UserProfileQuery { IncludeDeleted = true - }); + }, ct: TestContext.Current.CancellationToken); result.Items.Should().HaveCount(2); result.TotalCount.Should().Be(2); @@ -686,19 +686,19 @@ public async Task QueryAsync_ProfileKey_FiltersProfiles() await store.AddAsync( CreateProfile( TenantA, - profileKey: ProfileKey.Default)); + profileKey: ProfileKey.Default), ct: TestContext.Current.CancellationToken); var work = CreateProfile( TenantA, profileKey: workKey); - await store.AddAsync(work); + await store.AddAsync(work, ct: TestContext.Current.CancellationToken); var result = await store.QueryAsync( new UserProfileQuery { ProfileKey = workKey - }); + }, ct: TestContext.Current.CancellationToken); result.Items.Should().ContainSingle(); result.Items.Single().Id.Should().Be(work.Id); @@ -716,7 +716,7 @@ await store.AddAsync( CreateProfile( TenantA, createdAt: Now.AddMinutes(i), - displayName: $"User {i}")); + displayName: $"User {i}"), ct: TestContext.Current.CancellationToken); } var result = await store.QueryAsync( @@ -725,7 +725,7 @@ await store.AddAsync( PageNumber = 2, PageSize = 2, SortBy = nameof(UserProfile.CreatedAt) - }); + }, ct: TestContext.Current.CancellationToken); result.Items.Should().HaveCount(2); result.TotalCount.Should().Be(5); @@ -754,27 +754,27 @@ await store.AddAsync( TenantA, firstName: "Charlie", lastName: "Zulu", - displayName: "Mike")); + displayName: "Mike"), ct: TestContext.Current.CancellationToken); await store.AddAsync( CreateProfile( TenantA, firstName: "Alice", lastName: "Alpha", - displayName: "Alpha")); + displayName: "Alpha"), ct: TestContext.Current.CancellationToken); await store.AddAsync( CreateProfile( TenantA, firstName: "Bob", lastName: "Mike", - displayName: "Zulu")); + displayName: "Zulu"), ct: TestContext.Current.CancellationToken); var result = await store.QueryAsync( new UserProfileQuery { SortBy = sortBy - }); + }, ct: TestContext.Current.CancellationToken); result.Items.Should().HaveCount(3); @@ -809,24 +809,24 @@ public async Task QueryAsync_SortsCreatedAtDescending() await store.AddAsync( CreateProfile( TenantA, - createdAt: Now.AddMinutes(1))); + createdAt: Now.AddMinutes(1)), ct: TestContext.Current.CancellationToken); await store.AddAsync( CreateProfile( TenantA, - createdAt: Now.AddMinutes(3))); + createdAt: Now.AddMinutes(3)), ct: TestContext.Current.CancellationToken); await store.AddAsync( CreateProfile( TenantA, - createdAt: Now.AddMinutes(2))); + createdAt: Now.AddMinutes(2)), ct: TestContext.Current.CancellationToken); var result = await store.QueryAsync( new UserProfileQuery { SortBy = nameof(UserProfile.CreatedAt), Descending = true - }); + }, ct: TestContext.Current.CancellationToken); result.Items.Select(x => x.CreatedAt) .Should() diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/UserApplicationServiceTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/UserApplicationServiceTests.cs index d4675dc9..7be1eed9 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/UserApplicationServiceTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/UserApplicationServiceTests.cs @@ -103,7 +103,7 @@ public async Task CreateUserAsync_WhenValid_CreatesLifecycleAndDefaultProfile() var result = await f.Sut.CreateUserAsync( context, - request); + request, ct: TestContext.Current.CancellationToken); result.IsSuccess.Should().BeTrue(); @@ -156,7 +156,7 @@ public async Task CreateUserAsync_CreatesConfiguredIdentifiersForSameUser() var result = await f.Sut.CreateUserAsync( context, - request); + request, ct: TestContext.Current.CancellationToken); result.IsSuccess.Should().BeTrue(); @@ -201,7 +201,7 @@ public async Task CreateUserAsync_InvokesLifecycleIntegrations() It.IsAny())) .Returns(Task.CompletedTask); - await f.Sut.CreateUserAsync(context, request); + await f.Sut.CreateUserAsync(context, request, ct: TestContext.Current.CancellationToken); integration.Verify(x => x.OnUserCreatedAsync( context.ResourceTenant, @@ -286,7 +286,7 @@ public async Task ChangeUserStatusAsync_Self_AllowsActiveToSelfSuspended() await f.Sut.ChangeUserStatusAsync( context, - request); + request, ct: TestContext.Current.CancellationToken); lifecycle.Status.Should().Be(UserStatus.SelfSuspended); @@ -429,7 +429,7 @@ public async Task DeleteMeAsync_SoftDeletesUserData_AndRevokesAllChains() It.IsAny())) .Returns(Task.CompletedTask); - await f.Sut.DeleteMeAsync(context); + await f.Sut.DeleteMeAsync(context, ct: TestContext.Current.CancellationToken); f.LifecycleStore.Verify(x => x.DeleteAsync( It.IsAny(), @@ -496,7 +496,7 @@ public async Task DeleteMeAsync_InvokesIntegrationWithSoftDelete() It.IsAny())) .Returns(Task.CompletedTask); - await f.Sut.DeleteMeAsync(context); + await f.Sut.DeleteMeAsync(context, ct: TestContext.Current.CancellationToken); integration.Verify(x => x.OnUserDeletedAsync( context.ResourceTenant, @@ -574,7 +574,7 @@ public async Task DeleteUserAsync_UsesRequestedDeleteMode( await f.Sut.DeleteUserAsync( context, - request); + request, ct: TestContext.Current.CancellationToken); f.LifecycleStore.Verify(x => x.DeleteAsync( It.IsAny(), @@ -1025,7 +1025,7 @@ await f.Sut.VerifyUserIdentifierAsync( { Id = identifier.Id, Proof = proof - }); + }, ct: TestContext.Current.CancellationToken); identifier.IsVerified.Should().BeTrue(); identifier.VerifiedAt.Should().Be(Now); @@ -1189,7 +1189,7 @@ await f.Sut.DeleteUserIdentifierAsync( { Id = target.Id, Mode = DeleteMode.Soft - }); + }, ct: TestContext.Current.CancellationToken); target.IsDeleted.Should().BeTrue(); target.DeletedAt.Should().Be(Now); @@ -1257,7 +1257,7 @@ await f.Sut.DeleteUserIdentifierAsync( { Id = target.Id, Mode = DeleteMode.Hard - }); + }, ct: TestContext.Current.CancellationToken); f.IdentifierStore.Verify(x => x.DeleteAsync( target.Id, @@ -1356,7 +1356,7 @@ await f.Sut.UpdateUserIdentifierAsync( { Id = identifier.Id, NewValue = "new@example.com" - }); + }, ct: TestContext.Current.CancellationToken); identifier.Value.Should().Be("new@example.com"); identifier.NormalizedValue.Should().Be("new@example.com"); @@ -1405,7 +1405,7 @@ await f.Sut.SetPrimaryUserIdentifierAsync( new SetPrimaryUserIdentifierRequest { Id = identifier.Id - }); + }, ct: TestContext.Current.CancellationToken); identifier.IsPrimary.Should().BeTrue(); identifier.UpdatedAt.Should().Be(Now); @@ -1500,7 +1500,7 @@ await f.Sut.UnsetPrimaryUserIdentifierAsync( new UnsetPrimaryUserIdentifierRequest { Id = target.Id - }); + }, ct: TestContext.Current.CancellationToken); target.IsPrimary.Should().BeFalse(); target.UpdatedAt.Should().Be(Now); @@ -1573,7 +1573,7 @@ await f.Sut.DeleteUserAsync( new DeleteUserRequest { Mode = mode - }); + }, ct: TestContext.Current.CancellationToken); f.SessionStore.Verify(x => x.RevokeAllChainsAsync( target, @@ -1672,7 +1672,7 @@ await f.Sut.DeleteUserAsync( new DeleteUserRequest { Mode = mode - }); + }, ct: TestContext.Current.CancellationToken); f.ProfileStore.Verify(x => x.DeleteAsync( new UserProfileKey( @@ -1719,7 +1719,7 @@ public async Task GetIdentifierAsync_WhenValueInvalid_ReturnsNullWithoutStoreAcc "invalid", "", false, "identifier_invalid")); var result = await f.Sut.GetIdentifierAsync( - context, UserIdentifierType.Email, "invalid"); + context, UserIdentifierType.Email, "invalid", ct: TestContext.Current.CancellationToken); result.Should().BeNull(); @@ -1751,7 +1751,7 @@ public async Task GetIdentifierAsync_WhenNotFound_ReturnsNull() .ReturnsAsync((UserIdentifier?)null); var result = await f.Sut.GetIdentifierAsync( - context, UserIdentifierType.Email, "ALICE@EXAMPLE.COM"); + context, UserIdentifierType.Email, "ALICE@EXAMPLE.COM", ct: TestContext.Current.CancellationToken); result.Should().BeNull(); @@ -1787,7 +1787,7 @@ public async Task GetIdentifierAsync_WhenFound_ReturnsMappedIdentifier() .ReturnsAsync(identifier); var result = await f.Sut.GetIdentifierAsync( - context, UserIdentifierType.Email, "alice@example.com"); + context, UserIdentifierType.Email, "alice@example.com", ct: TestContext.Current.CancellationToken); result.Should().NotBeNull(); result!.Id.Should().Be(identifier.Id); @@ -1808,7 +1808,7 @@ public async Task UserIdentifierExistsAsync_WhenValueInvalid_ReturnsFalseWithout "invalid", "", false, "identifier_invalid")); var result = await f.Sut.UserIdentifierExistsAsync( - context, UserIdentifierType.Email, "invalid"); + context, UserIdentifierType.Email, "invalid", ct: TestContext.Current.CancellationToken); result.Should().BeFalse(); @@ -1848,7 +1848,7 @@ public async Task UserIdentifierExistsAsync_TenantScope_DoesNotPassUserKey( context, UserIdentifierType.Email, "ALICE@EXAMPLE.COM", - scope); + scope, ct: TestContext.Current.CancellationToken); result.Should().BeTrue(); @@ -1884,7 +1884,7 @@ public async Task UserIdentifierExistsAsync_WithinUser_PassesTargetUserKey() context, UserIdentifierType.Username, "Alice", - IdentifierExistenceScope.WithinUser); + IdentifierExistenceScope.WithinUser, ct: TestContext.Current.CancellationToken); result.Should().BeTrue(); } @@ -1912,7 +1912,7 @@ public async Task UserIdentifierExistsAsync_WhenStoreReportsMissing_ReturnsFalse .ReturnsAsync(new UserIdentifierExistenceResult(Exists: false)); var result = await f.Sut.UserIdentifierExistsAsync( - context, UserIdentifierType.Username, "missing"); + context, UserIdentifierType.Username, "missing", ct: TestContext.Current.CancellationToken); result.Should().BeFalse(); } @@ -1948,7 +1948,7 @@ public async Task GetUsersByKeysAsync_WhenEmpty_ReturnsEmptyWithoutStoreAccess() var result = await f.Sut.GetUsersByKeysAsync( context, - Array.Empty()); + Array.Empty(), ct: TestContext.Current.CancellationToken); result.Should().BeEmpty(); @@ -1996,7 +1996,7 @@ public async Task GetUsersByKeysAsync_ShouldNormalizeKeysAndUseResourceTenant() var result = await f.Sut.GetUsersByKeysAsync( context, - [first, first, second]); + [first, first, second], ct: TestContext.Current.CancellationToken); result.Should().BeEquivalentTo(expected); @@ -2033,7 +2033,7 @@ public async Task GetUsersByKeysAsync_When250DistinctKeys_ExecutesSuccessfully() var result = await f.Sut.GetUsersByKeysAsync( context, - keys); + keys, ct: TestContext.Current.CancellationToken); result.Should().BeEmpty(); @@ -2162,7 +2162,7 @@ public async Task CreateUserAsync_WhenIdentifierCountEqualsLimit_Succeeds() SetupValidCreate(f, context, request); SetupSuccessfulCreatePersistence(f); - var result = await f.Sut.CreateUserAsync(context, request); + var result = await f.Sut.CreateUserAsync(context, request, ct: TestContext.Current.CancellationToken); result.IsSuccess.Should().BeTrue(); @@ -2201,7 +2201,7 @@ public async Task CreateUserAsync_WithDefaultLimit_AllowsThreeInitialIdentifiers SetupValidCreate(f, context, request); SetupSuccessfulCreatePersistence(f); - var result = await f.Sut.CreateUserAsync(context, request); + var result = await f.Sut.CreateUserAsync(context, request, ct: TestContext.Current.CancellationToken); result.IsSuccess.Should().BeTrue(); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/UserCreateValidatorTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/UserCreateValidatorTests.cs index 64aad5cd..307482f8 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/UserCreateValidatorTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/UserCreateValidatorTests.cs @@ -25,7 +25,7 @@ public async Task ValidateAsync_WhenNoIdentifierProvided_ShouldReturnIdentifierR var result = await fixture.Sut.ValidateAsync( fixture.Context, - request); + request, ct: TestContext.Current.CancellationToken); result.IsValid.Should().BeFalse(); @@ -72,7 +72,7 @@ public async Task ValidateAsync_WhenIdentifierIsInvalid_ShouldNotCheckAvailabili var result = await fixture.Sut.ValidateAsync( fixture.Context, - CreateRequest(type, "invalid-value")); + CreateRequest(type, "invalid-value"), ct: TestContext.Current.CancellationToken); result.IsValid.Should().BeFalse(); @@ -121,7 +121,7 @@ public async Task ValidateAsync_WhenIdentifierIsUnavailable_ShouldReturnExpected var result = await fixture.Sut.ValidateAsync( fixture.Context, - CreateRequest(type, "value")); + CreateRequest(type, "value"), ct: TestContext.Current.CancellationToken); result.IsValid.Should().BeFalse(); @@ -155,7 +155,7 @@ public async Task ValidateAsync_WhenIdentifierIsValidAndAvailable_ShouldSucceed( var result = await fixture.Sut.ValidateAsync( fixture.Context, - CreateRequest(type, "value")); + CreateRequest(type, "value"), ct: TestContext.Current.CancellationToken); result.IsValid.Should().BeTrue(); result.Errors.Should().BeEmpty(); @@ -190,7 +190,7 @@ public async Task ValidateAsync_WhenAllIdentifiersProvided_ShouldValidateAllIden UserName = "john", Email = "john@example.com", Phone = "1234567890" - }); + }, ct: TestContext.Current.CancellationToken); result.IsValid.Should().BeTrue(); @@ -238,7 +238,7 @@ await fixture.Sut.ValidateAsync( UserName = "john", Email = "john@example.com", Phone = "1234567890" - }); + }, ct: TestContext.Current.CancellationToken); fixture.IdentifierValidator.Verify( x => x.ValidateAsync( @@ -339,7 +339,7 @@ public async Task ValidateAsync_WhenOneIdentifierIsInvalid_ShouldContinueValidat { UserName = "invalid", Email = "john@example.com" - }); + }, ct: TestContext.Current.CancellationToken); result.IsValid.Should().BeFalse(); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/UserIdentifierApplicationServiceTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/UserIdentifierApplicationServiceTests.cs index 4f31ff61..8cf080e5 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/UserIdentifierApplicationServiceTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/UserIdentifierApplicationServiceTests.cs @@ -27,9 +27,9 @@ await service.AddUserIdentifierAsync(context, Type = UserIdentifierType.Email, Value = "new@example.com", IsPrimary = true - }); + }, ct: TestContext.Current.CancellationToken); - var identifiers = await service.GetIdentifiersByUserAsync(context, new UserIdentifierQuery()); + var identifiers = await service.GetIdentifiersByUserAsync(context, new UserIdentifierQuery(), ct: TestContext.Current.CancellationToken); identifiers.Items.Where(x => x.Type == UserIdentifierType.Email).Should().ContainSingle(x => x.IsPrimary); @@ -61,14 +61,14 @@ await identifierService.AddUserIdentifierAsync(context, Type = UserIdentifierType.Phone, Value = "+905551111111", IsPrimary = true - }); + }, ct: TestContext.Current.CancellationToken); var result = await loginOrchestrator.LoginAsync(flow, new LoginRequest { Identifier = "+905551111111", Secret = "user", - }); + }, ct: TestContext.Current.CancellationToken); result.IsSuccess.Should().BeFalse(); } @@ -81,7 +81,7 @@ public async Task Adding_non_primary_should_not_affect_existing_primary() var context = TestAccessContext.ForUser(TestUsers.User, UserIdentifiers.AddSelf); - var before = await service.GetIdentifiersByUserAsync(context, new UserIdentifierQuery()); + var before = await service.GetIdentifiersByUserAsync(context, new UserIdentifierQuery(), ct: TestContext.Current.CancellationToken); var existingPrimaryEmail = before.Items.Single(x => x.Type == UserIdentifierType.Email && x.IsPrimary); await service.AddUserIdentifierAsync(context, @@ -90,9 +90,9 @@ await service.AddUserIdentifierAsync(context, Type = UserIdentifierType.Email, Value = "secondary@example.com", IsPrimary = false - }); + }, ct: TestContext.Current.CancellationToken); - var after = await service.GetIdentifiersByUserAsync(context, new UserIdentifierQuery()); + var after = await service.GetIdentifiersByUserAsync(context, new UserIdentifierQuery(), ct: TestContext.Current.CancellationToken); after.Items.Single(x => x.Type == UserIdentifierType.Email && x.IsPrimary) .Id.Should().Be(existingPrimaryEmail.Id); @@ -106,16 +106,16 @@ public async Task Updating_value_should_reset_verification() var context = TestAccessContext.ForUser(TestUsers.User, UserIdentifiers.UpdateSelf); - var email = (await service.GetIdentifiersByUserAsync(context, new UserIdentifierQuery())).Items.Single(x => x.Type == UserIdentifierType.Email); + var email = (await service.GetIdentifiersByUserAsync(context, new UserIdentifierQuery(), ct: TestContext.Current.CancellationToken)).Items.Single(x => x.Type == UserIdentifierType.Email); await service.UpdateUserIdentifierAsync(context, new UpdateUserIdentifierRequest { Id = email.Id, NewValue = "updated@example.com" - }); + }, ct: TestContext.Current.CancellationToken); - var updated = (await service.GetIdentifiersByUserAsync(context, new UserIdentifierQuery())).Items + var updated = (await service.GetIdentifiersByUserAsync(context, new UserIdentifierQuery(), ct: TestContext.Current.CancellationToken)).Items .Single(x => x.Id == email.Id); updated.IsVerified.Should().BeFalse(); @@ -146,7 +146,7 @@ await service.AddUserIdentifierAsync( Type = UserIdentifierType.Email, Value = "shared@example.com", IsPrimary = false - }); + }, ct: TestContext.Current.CancellationToken); Func act = () => service.AddUserIdentifierAsync( @@ -185,7 +185,7 @@ await service.AddUserIdentifierAsync( { Type = UserIdentifierType.Email, Value = "unique@example.com" - }); + }, ct: TestContext.Current.CancellationToken); Func act = () => service.AddUserIdentifierAsync( @@ -225,7 +225,7 @@ await service.AddUserIdentifierAsync( { Type = UserIdentifierType.Email, Value = "shared@example.com" - }); + }, ct: TestContext.Current.CancellationToken); Func act = () => service.AddUserIdentifierAsync( @@ -254,7 +254,7 @@ public async Task Unsetting_last_login_identifier_should_fail() var context = TestAccessContext.ForUser(TestUsers.User, UserIdentifiers.UnsetPrimarySelf); - var email = (await service.GetIdentifiersByUserAsync(context, new UserIdentifierQuery())).Items.Single(x => x.Type == UserIdentifierType.Email && x.IsPrimary); + var email = (await service.GetIdentifiersByUserAsync(context, new UserIdentifierQuery(), ct: TestContext.Current.CancellationToken)).Items.Single(x => x.Type == UserIdentifierType.Email && x.IsPrimary); Func act = async () => await service.UnsetPrimaryUserIdentifierAsync(context, @@ -279,7 +279,7 @@ await service.AddUserIdentifierAsync(context, { Type = UserIdentifierType.Email, Value = "Test@Example.com" - }); + }, ct: TestContext.Current.CancellationToken); Func act = async () => await service.AddUserIdentifierAsync(context, @@ -309,9 +309,9 @@ await service.AddUserIdentifierAsync(context, { Type = UserIdentifierType.Username, Value = "UserName" - }); + }, ct: TestContext.Current.CancellationToken); - var identifiers = await service.GetIdentifiersByUserAsync(context, new UserIdentifierQuery()); + var identifiers = await service.GetIdentifiersByUserAsync(context, new UserIdentifierQuery(), ct: TestContext.Current.CancellationToken); identifiers.Items.Should().Contain(x => x.Value == "UserName"); } @@ -333,7 +333,7 @@ await service.AddUserIdentifierAsync(context, { Type = UserIdentifierType.Username, Value = "UserName" - }); + }, ct: TestContext.Current.CancellationToken); Func act = async () => await service.AddUserIdentifierAsync(context, @@ -359,9 +359,9 @@ await service.AddUserIdentifierAsync(context, { Type = UserIdentifierType.Phone, Value = "+90 (555) 123-45-67" - }); + }, ct: TestContext.Current.CancellationToken); - var identifiers = await service.GetIdentifiersByUserAsync(context, new UserIdentifierQuery()); + var identifiers = await service.GetIdentifiersByUserAsync(context, new UserIdentifierQuery(), ct: TestContext.Current.CancellationToken); identifiers.Items.Should().Contain(x => x.Type == UserIdentifierType.Phone && @@ -381,16 +381,16 @@ await service.AddUserIdentifierAsync(context, { Type = UserIdentifierType.Email, Value = "one@example.com" - }); + }, ct: TestContext.Current.CancellationToken); await service.AddUserIdentifierAsync(context, new AddUserIdentifierRequest { Type = UserIdentifierType.Email, Value = "two@example.com" - }); + }, ct: TestContext.Current.CancellationToken); - var identifiers = await service.GetIdentifiersByUserAsync(context, new UserIdentifierQuery()); + var identifiers = await service.GetIdentifiersByUserAsync(context, new UserIdentifierQuery(), ct: TestContext.Current.CancellationToken); var second = identifiers.Items.Single(x => x.Value == "two@example.com"); Func act = async () => @@ -420,7 +420,7 @@ public async Task Availability_should_return_available_for_unused_identifier() { Type = UserIdentifierType.Username, Value = "unused-user-name" - }); + }, ct: TestContext.Current.CancellationToken); result.IsValid.Should().BeTrue(); result.IsAvailable.Should().BeTrue(); @@ -439,7 +439,7 @@ public async Task Availability_should_return_unavailable_for_existing_identifier var identifiers = await service.GetIdentifiersByUserAsync( context, - new UserIdentifierQuery()); + new UserIdentifierQuery(), ct: TestContext.Current.CancellationToken); var existing = identifiers.Items.First(); @@ -449,7 +449,7 @@ await service.GetIdentifiersByUserAsync( { Type = existing.Type, Value = existing.Value - }); + }, ct: TestContext.Current.CancellationToken); result.IsValid.Should().BeTrue(); result.IsAvailable.Should().BeFalse(); @@ -460,11 +460,11 @@ public async Task Availability_should_use_normalized_identifier() { var runtime = new TestAuthRuntime(); - var user = await runtime.CreateLoginUserAsync(); + var user = await runtime.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); const string storedEmail = "availability@example.com"; - await runtime.AddIdentifierAsync(user.UserKey, UserIdentifierType.Email, storedEmail); + await runtime.AddIdentifierAsync(user.UserKey, UserIdentifierType.Email, storedEmail, ct: TestContext.Current.CancellationToken); var service = runtime.GetUserApplicationService(); @@ -475,7 +475,7 @@ public async Task Availability_should_use_normalized_identifier() { Type = UserIdentifierType.Email, Value = " AVAILABILITY@EXAMPLE.COM " - }); + }, ct: TestContext.Current.CancellationToken); result.IsValid.Should().BeTrue(); result.IsAvailable.Should().BeFalse(); @@ -498,7 +498,7 @@ public async Task Availability_should_return_validation_errors_for_invalid_ident { Type = UserIdentifierType.Email, Value = "invalid" - }); + }, ct: TestContext.Current.CancellationToken); result.IsValid.Should().BeFalse(); result.IsAvailable.Should().BeFalse(); @@ -511,12 +511,12 @@ public async Task Email_availability_should_be_case_insensitive_by_default() var runtime = new TestAuthRuntime(); var user = - await runtime.CreateLoginUserAsync(); + await runtime.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); await runtime.AddIdentifierAsync( user.UserKey, UserIdentifierType.Email, - "availability@example.com"); + "availability@example.com", ct: TestContext.Current.CancellationToken); var service = runtime.GetUserApplicationService(); @@ -532,7 +532,7 @@ await service.CheckIdentifierAvailabilityAsync( { Type = UserIdentifierType.Email, Value = "AVAILABILITY@EXAMPLE.COM" - }); + }, ct: TestContext.Current.CancellationToken); result.IsValid.Should().BeTrue(); result.IsAvailable.Should().BeFalse(); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/UserIdentifierValidationTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/UserIdentifierValidationTests.cs index 3b17c43d..5162951a 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/UserIdentifierValidationTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/UserIdentifierValidationTests.cs @@ -20,7 +20,7 @@ public async Task ValidateAsync_WhenIdentifierIsEmpty_ShouldReturnIdentifierEmpt { Type = UserIdentifierType.Username, Value = " " - }); + }, ct: TestContext.Current.CancellationToken); result.IsValid.Should().BeFalse(); result.Errors.Should().ContainSingle(x => @@ -38,7 +38,7 @@ public async Task ValidateAsync_ShouldTrimIdentifierBeforeValidation() Value = " valid_user " }; - var result = await sut.ValidateAsync(null!, identifier); + var result = await sut.ValidateAsync(null!, identifier, ct: TestContext.Current.CancellationToken); result.IsValid.Should().BeTrue(); identifier.Value.Should().Be("valid_user"); @@ -58,7 +58,7 @@ public async Task ValidateAsync_WhenUsernameIsTooShort_ShouldReturnUsernameTooSh { Type = UserIdentifierType.Username, Value = "abc" - }); + }, ct: TestContext.Current.CancellationToken); result.IsValid.Should().BeFalse(); result.Errors.Should().Contain(x => @@ -79,7 +79,7 @@ public async Task ValidateAsync_WhenUsernameIsTooLong_ShouldReturnUsernameTooLon { Type = UserIdentifierType.Username, Value = "abcdef" - }); + }, ct: TestContext.Current.CancellationToken); result.IsValid.Should().BeFalse(); result.Errors.Should().Contain(x => @@ -101,7 +101,7 @@ public async Task ValidateAsync_WhenUsernameDoesNotMatchAllowedRegex_ShouldRetur { Type = UserIdentifierType.Username, Value = "user123" - }); + }, ct: TestContext.Current.CancellationToken); result.IsValid.Should().BeFalse(); result.Errors.Should().Contain(x => @@ -123,7 +123,7 @@ public async Task ValidateAsync_WhenUsernameValidationIsDisabled_ShouldSkipUsern { Type = UserIdentifierType.Username, Value = "a" - }); + }, ct: TestContext.Current.CancellationToken); result.IsValid.Should().BeTrue(); } @@ -139,7 +139,7 @@ public async Task ValidateAsync_WhenEmailDoesNotContainAt_ShouldReturnInvalidFor { Type = UserIdentifierType.Email, Value = "not-an-email" - }); + }, ct: TestContext.Current.CancellationToken); result.IsValid.Should().BeFalse(); result.Errors.Should().Contain(x => @@ -160,7 +160,7 @@ public async Task ValidateAsync_WhenEmailIsTooShort_ShouldReturnEmailTooShort() { Type = UserIdentifierType.Email, Value = "a@b.co" - }); + }, ct: TestContext.Current.CancellationToken); result.Errors.Should().Contain(x => x.Code == "email_too_short"); @@ -180,7 +180,7 @@ public async Task ValidateAsync_WhenEmailIsTooLong_ShouldReturnEmailTooLong() { Type = UserIdentifierType.Email, Value = "abc@example.com" - }); + }, ct: TestContext.Current.CancellationToken); result.Errors.Should().Contain(x => x.Code == "email_too_long"); @@ -200,7 +200,7 @@ public async Task ValidateAsync_WhenPhoneIsTooShort_ShouldReturnPhoneTooShort() { Type = UserIdentifierType.Phone, Value = "123" - }); + }, ct: TestContext.Current.CancellationToken); result.Errors.Should().Contain(x => x.Code == "phone_too_short"); @@ -220,7 +220,7 @@ public async Task ValidateAsync_WhenPhoneIsTooLong_ShouldReturnPhoneTooLong() { Type = UserIdentifierType.Phone, Value = "123456789" - }); + }, ct: TestContext.Current.CancellationToken); result.Errors.Should().Contain(x => x.Code == "phone_too_long"); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/UserProfileValidatorTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/UserProfileValidatorTests.cs index f54acbb6..0bd77a0f 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/UserProfileValidatorTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/UserProfileValidatorTests.cs @@ -14,7 +14,7 @@ public async Task ValidateAsync_CurrentImplementation_ShouldSucceed() var result = await sut.ValidateAsync( null!, - new UserProfileInfo()); + new UserProfileInfo(), ct: TestContext.Current.CancellationToken); result.IsValid.Should().BeTrue(); result.Errors.Should().BeEmpty(); diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props new file mode 100644 index 00000000..b17f63d7 --- /dev/null +++ b/tests/Directory.Build.props @@ -0,0 +1,13 @@ + + + + $(UltimateAuthLatestTargetFramework) + + Exe + true + + + + + + From dfc17fcb0f93d987560bde48d788769c33b98327 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Can=20Karag=C3=B6z?= <78308169+mckaragoz@users.noreply.github.com> Date: Sat, 7 Nov 2026 14:25:01 +0300 Subject: [PATCH 08/11] Enforce Device Binding in Session Validation (#147) * Enforce Device Binding in Session Validation * Fix Antiforgery Behavior on Device Hardening --- .../ResourceApi/RemoteSessionValidator.cs | 5 + .../Services/UAuthSessionValidator.cs | 19 +-- .../TScripts/uauth.js | 5 +- .../wwwroot/uauth.min.js | 2 +- .../LogoutTests.cs | 127 ++++++++++++++++++ .../RemoteSessionValidatorTests.cs | 52 ++++++- .../Server/UAuthSessionValidatorTests.cs | 100 ++++++++++++-- .../javascript/uauth.test.cjs | 50 +++++++ 8 files changed, 333 insertions(+), 27 deletions(-) diff --git a/src/CodeBeam.UltimateAuth.Server/ResourceApi/RemoteSessionValidator.cs b/src/CodeBeam.UltimateAuth.Server/ResourceApi/RemoteSessionValidator.cs index 14429d4c..ac1f2f71 100644 --- a/src/CodeBeam.UltimateAuth.Server/ResourceApi/RemoteSessionValidator.cs +++ b/src/CodeBeam.UltimateAuth.Server/ResourceApi/RemoteSessionValidator.cs @@ -36,6 +36,11 @@ public async Task ValidateSessionAsync(SessionValidatio request.Headers.Add("Cookie", cookie.ToString()); } + if (context.Device.DeviceId is { } deviceId) + { + request.Headers.TryAddWithoutValidation("X-UDID", deviceId.Value); + } + var response = await _http.SendAsync(request, ct); if (!response.IsSuccessStatusCode) diff --git a/src/CodeBeam.UltimateAuth.Server/Services/UAuthSessionValidator.cs b/src/CodeBeam.UltimateAuth.Server/Services/UAuthSessionValidator.cs index c46cceaa..ad9617f0 100644 --- a/src/CodeBeam.UltimateAuth.Server/Services/UAuthSessionValidator.cs +++ b/src/CodeBeam.UltimateAuth.Server/Services/UAuthSessionValidator.cs @@ -16,7 +16,6 @@ public UAuthSessionValidator(ISessionStoreFactory storeFactory, IUserClaimsProvi _claimsProvider = claimsProvider; } - // TODO: Improve Device binding // Validate runs before AuthFlowContext is set, do not call _authFlow here. // TODO: Add GetSessionAggregate store method to 1 call instead of 3 calls of root chain session @@ -61,21 +60,13 @@ public async Task ValidateSessionAsync(SessionValidatio if (session.SecurityVersionAtCreation != root.SecurityVersion) return SessionValidationResult.Invalid(SessionState.SecurityMismatch, session.UserKey, session.SessionId, session.ChainId, root.RootId); - if (chain.Device.HasDeviceId && context.Device.HasDeviceId) - { - if (!Equals(chain.Device.DeviceId, context.Device.DeviceId)) - { - if (options.DeviceMismatchBehavior == DeviceMismatchBehavior.Reject) - return SessionValidationResult.Invalid(SessionState.DeviceMismatch, chain.UserKey, session.SessionId, chain.ChainId, root.RootId); + // TODO: Implement device mismatch behavior based on options, e.g., allow, reject or rebind. + var deviceMismatch = !chain.Device.HasDeviceId || !context.Device.HasDeviceId || !Equals(chain.Device.DeviceId, context.Device.DeviceId); - //if (options.Session.DeviceMismatchBehavior == AllowAndRebind) - } + if (deviceMismatch && options.DeviceMismatchBehavior == DeviceMismatchBehavior.Reject) + { + return SessionValidationResult.Invalid(SessionState.DeviceMismatch, chain.UserKey, session.SessionId, chain.ChainId, root.RootId); } - //else - //{ - // // Add SessionValidatorOrigin to seperate UserRequest or background task - // return SessionValidationResult.Invalid(SessionState.DeviceMismatch, chain.UserKey, session.SessionId, chain.ChainId, root.RootId); - //} var claims = await _claimsProvider.GetClaimsAsync(context.Tenant, session.UserKey, ct); return SessionValidationResult.Active(context.Tenant, session.UserKey, session.SessionId, session.ChainId, root.RootId, claims, session.CreatedAt, chain.Device.DeviceId); diff --git a/src/client/CodeBeam.UltimateAuth.Client.Blazor/TScripts/uauth.js b/src/client/CodeBeam.UltimateAuth.Client.Blazor/TScripts/uauth.js index b2710b91..237bb830 100644 --- a/src/client/CodeBeam.UltimateAuth.Client.Blazor/TScripts/uauth.js +++ b/src/client/CodeBeam.UltimateAuth.Client.Blazor/TScripts/uauth.js @@ -674,7 +674,10 @@ window.uauth.antiforgery = { method: "GET", credentials: "include", cache: "no-store", - redirect: "error" + redirect: "error", + headers: { + "X-UDID": window.uauth.deviceId + } }); if (!response.ok) { diff --git a/src/client/CodeBeam.UltimateAuth.Client.Blazor/wwwroot/uauth.min.js b/src/client/CodeBeam.UltimateAuth.Client.Blazor/wwwroot/uauth.min.js index 9c32b1a2..5c205de1 100644 --- a/src/client/CodeBeam.UltimateAuth.Client.Blazor/wwwroot/uauth.min.js +++ b/src/client/CodeBeam.UltimateAuth.Client.Blazor/wwwroot/uauth.min.js @@ -1 +1 @@ -window.uauth=window.uauth||{};window.uauth.storage={set:function(n,t,i){const r=n==="local"?window.localStorage:window.sessionStorage;r.setItem(t,i)},get:function(n,t){const i=n==="local"?window.localStorage:window.sessionStorage;return i.getItem(t)},remove:function(n,t){const i=n==="local"?window.localStorage:window.sessionStorage;i.removeItem(t)},exists:function(n,t){const i=n==="local"?window.localStorage:window.sessionStorage;return i.getItem(t)!==null}};window.uauth.submitForm=async function(n,t=null){if(!n)return;if(!window.uauth.deviceId)throw new Error("UAuth deviceId is not initialized.");const r=window.uauth.validateAuthUrl(n.action,t);n.action=r;let i=n.querySelector("input[name='__uauth_device']");i||(i=document.createElement("input"),i.type="hidden",i.name="__uauth_device",n.appendChild(i));i.value=window.uauth.deviceId;await window.uauth.antiforgery.appendToForm(n,t);n.submit()};window.uauth.tryAndCommit=async function(n){const{tryUrl:c,commitUrl:l,data:e,clientProfile:o,trustedOrigin:u}=n,a=window.uauth.validateAuthUrl(c,u),v=window.uauth.validateAuthUrl(l,u),s=window.uauth.deviceId;if(!s)throw new Error("UAuth deviceId is not initialized.");const r=await window.uauth.postJson({url:a,payload:e,clientProfile:o,trustedOrigin:u});if(!r||!Number.isInteger(r.status))throw new Error("Invalid UAuth transport response.");const i=r.status;if(i===429)return{isSuccess:!1,status:429,errorCode:"rate_limited",retryAfter:r.retryAfter??null};const y=i>=200&&i<300,h=i>=400&&i<500;if(!y&&!h)throw new Error(`Unexpected UAuth try response: HTTP ${i}.`);const t=r.body;if(!t||typeof t!="object"||Array.isArray(t)||typeof t.isSuccess!="boolean")throw new Error("Invalid UAuth try response body.");if(h&&t.isSuccess)throw new Error("Successful authentication result returned with HTTP error.");const f={isSuccess:t.isSuccess,reason:t.reason??null,remainingAttempts:t.remainingAttempts??null,lockoutUntilUtc:t.lockoutUntilUtc??null,requiresMfa:t.requiresMfa??!1,retryWithNewPkce:t.retryWithNewPkce??!1,previewReceipt:t.previewReceipt??null};if(f.isSuccess){const n=document.createElement("form");n.method="POST";n.action=v;for(const t of Object.keys(e)){const i=document.createElement("input");i.type="hidden";i.name=t;i.value=e[t]??"";n.appendChild(i)}if(f.previewReceipt){const t=document.createElement("input");t.type="hidden";t.name="PreviewReceipt";t.value=f.previewReceipt;n.appendChild(t)}const t=document.createElement("input");t.type="hidden";t.name="__uauth_client_profile";t.value=o??"";n.appendChild(t);const i=document.createElement("input");i.type="hidden";i.name="__uauth_device";i.value=s;n.appendChild(i);document.body.appendChild(n);try{await window.uauth.submitForm(n,u)}catch(r){n.remove();throw r;}}return f};window.uauth.post=async function(n){const{url:e,mode:t,data:i,clientProfile:u,trustedOrigin:r}=n,f=window.uauth.validateAuthUrl(e,r);if(t==="navigate"){const n=document.createElement("form");n.method="POST";n.action=f;const t=document.createElement("input");t.type="hidden";t.name="__uauth_client_profile";t.value=u??"";n.appendChild(t);const e=document.createElement("input");if(e.type="hidden",e.name="__uauth_device",e.value=window.uauth.deviceId,n.appendChild(e),i)for(const t in i){const r=document.createElement("input");r.type="hidden";r.name=t;r.value=i[t];n.appendChild(r)}document.body.appendChild(n);try{await window.uauth.submitForm(n,r)}catch(o){n.remove();throw o;}return null}const o=t==="logout"||t==="logout-observe";return window.uauth.request({url:f,method:"POST",payload:i,clientProfile:u,contentType:"form",credentials:"include",allowCrossOriginCredentials:!0,antiforgery:!0,trustedOrigin:r,navigateOnSuccess:t==="logout",headers:o?{"X-UAuth-Logout-Mode":"observe"}:{}})};window.uauth.postJson=function(n){const t=window.uauth.validateAuthUrl(n.url,n.trustedOrigin);return window.uauth.request({url:t,method:"POST",payload:n.payload,clientProfile:n.clientProfile,contentType:"json",credentials:"include",allowCrossOriginCredentials:!0,antiforgery:!0,trustedOrigin:n.trustedOrigin})};window.uauth.request=async function(n){const{followRedirects:a=!1,navigateOnSuccess:v=!1,url:h,method:y="GET",payload:r=null,clientProfile:p="",contentType:c="json",credentials:s="same-origin",headers:w={}}=n;if(!h)throw new Error("Request URL is required.");const u=new URL(h,document.baseURI);if(!["http:","https:"].includes(u.protocol))throw new Error("Unsupported request URL protocol.");const i=y.toUpperCase();if(!["GET","POST","PUT","PATCH","DELETE"].includes(i))throw new Error(`Unsupported HTTP method: ${i}`);if(!["same-origin","include","omit"].includes(s))throw new Error("Invalid credentials policy.");if(s==="include"&&u.origin!==window.location.origin&&!n.allowCrossOriginCredentials)throw new Error("Cross-origin credentials require explicit authorization.");if(!window.uauth.deviceId)throw new Error("UAuth deviceId is not initialized.");const f={...w,"X-UDID":window.uauth.deviceId,"X-UAuth-ClientProfile":p,"X-Requested-With":"UAuth"};let e=null;if(r!==null&&r!==undefined){if(i==="GET")throw new Error("GET requests cannot contain a body.");if(c==="json")f["Content-Type"]="application/json",e=JSON.stringify(r);else if(c==="form"){f["Content-Type"]="application/x-www-form-urlencoded";e=new URLSearchParams;for(const[n,t]of Object.entries(r))e.append(n,t??"")}else throw new Error("Unsupported content type.");}if(i!=="GET"&&n.antiforgery===!0){const i=window.uauth.validateAuthUrl(window.uauth.antiforgery.tokenUrl,n.trustedOrigin);if(new URL(i).origin!==u.origin)throw new Error("UAuth antiforgery token origin differs from request origin.");const t=await window.uauth.antiforgery.getToken(n.trustedOrigin);f[t.headerName]=t.requestToken}let t;try{t=await fetch(u.href,{method:i,credentials:s,redirect:a?"follow":"error",headers:f,body:e})}catch(l){if(l instanceof TypeError)return{ok:!1,status:0,refreshOutcome:null,body:null};throw l;}let o=null;if(t.status!==204&&t.status!==205){const n=await t.text();if(n)try{o=JSON.parse(n)}catch{o=n}}if(v&&t.ok){const n=o?.redirectUrl;if(typeof n=="string"&&n.length>0){const t=new URL(n,window.location.origin);if(!["http:","https:"].includes(t.protocol))throw new Error("Invalid logout redirect protocol.");window.location.assign(t.href)}}return{retryAfter:t.headers.get("Retry-After"),ok:t.ok,status:t.status,refreshOutcome:t.headers.get("X-UAuth-Refresh"),body:o}};window.uauth.resourceRequest=async function(n){const{url:o,baseAddress:h,method:v="GET",payload:f=null,clientProfile:y="",credentials:p="Auto",requestId:u}=n;if(!o||typeof o!="string")throw new Error("Resource request URL is required.");const i=h?new URL(h):new URL(window.location.origin+"/");if(!["http:","https:"].includes(i.protocol))throw new Error("Invalid Resource API protocol.");if(i.username||i.password||i.search||i.hash)throw new Error("Invalid Resource API base address.");const t=new URL(o,i);if(t.origin!==i.origin)throw new Error("Resource request target differs from configured API origin.");if(t.username||t.password)throw new Error("Resource URL cannot contain credentials.");if(!["http:","https:"].includes(t.protocol))throw new Error("Unsupported Resource API protocol.");const s=v.toUpperCase();if(!["GET","POST","PUT","PATCH","DELETE"].includes(s))throw new Error("Unsupported Resource API method.");if(s==="GET"&&f!==null)throw new Error("GET requests cannot contain a body.");let r;switch(p.toLowerCase()){case"auto":r=t.origin===window.location.origin?"same-origin":"include";break;case"sameorigin":r="same-origin";break;case"include":r="include";break;case"omit":r="omit";break;default:throw new Error("Invalid Resource credentials mode.");}if(r==="include"&&t.protocol!=="https:"&&!["localhost","127.0.0.1","[::1]"].includes(t.hostname))throw new Error("Cross-origin credential requests require HTTPS.");if(!window.uauth.deviceId)throw new Error("UAuth deviceId is not initialized.");const c={"X-UDID":window.uauth.deviceId,"X-UAuth-ClientProfile":y,"X-Requested-With":"UAuth"};let l=null;f!==null&&f!==undefined&&(c["Content-Type"]="application/json",l=JSON.stringify(f));const a=new AbortController;if(u){if(window.uauth.resourceRequests.has(u))throw new Error("Duplicate Resource request ID.");window.uauth.resourceRequests.set(u,a)}try{const n=await fetch(t.href,{method:s,credentials:r,redirect:"error",headers:c,body:l,signal:a.signal});let i=null;if(n.status!==204&&n.status!==205){const t=await n.text();t.length>0&&(i=t)}return{retryAfter:n.headers.get("Retry-After"),ok:n.ok,status:n.status,body:i,contentType:n.headers.get("Content-Type")}}catch(e){if(e?.name==="AbortError")throw e;if(e instanceof TypeError)return{ok:!1,status:0,body:null,contentType:null};throw e;}finally{u&&window.uauth.resourceRequests.delete(u)}};window.uauth.resourceRequests=new Map;window.uauth.cancelResourceRequest=function(n){const t=window.uauth.resourceRequests.get(n);return t?(t.abort(),!0):!1};window.uauth.validateAuthUrl=function(n,t=null){if(!n||typeof n!="string")throw new Error("Authentication URL is required.");const r=t?new URL(t,window.location.origin):new URL(window.location.origin),i=new URL(n,document.baseURI);if(!["http:","https:"].includes(r.protocol))throw new Error("Unsupported trusted origin protocol.");if(!["http:","https:"].includes(i.protocol))throw new Error("Unsupported authentication URL protocol.");if(r.username||r.password)throw new Error("Trusted origin cannot contain credentials.");if(i.username||i.password)throw new Error("Authentication URL cannot contain credentials.");if(i.origin!==r.origin)throw new Error("Authentication request target differs from the trusted UAuthHub origin.");return i.href};window.uauth.antiforgery={tokenUrl:null,configure:function(n){if(!n?.tokenUrl)throw new Error("UAuth antiforgery token URL is required.");this.tokenUrl=window.uauth.validateAuthUrl(n.tokenUrl,n.trustedOrigin)},getToken:async function(n){if(!this.tokenUrl)throw new Error("UAuth antiforgery is not configured.");const r=window.uauth.validateAuthUrl(this.tokenUrl,n),i=await fetch(r,{method:"GET",credentials:"include",cache:"no-store",redirect:"error"});if(!i.ok)throw new Error(`Unable to obtain UAuth antiforgery token: HTTP ${i.status}.`);const t=await i.json();if(!t?.requestToken||!t?.headerName||!t?.formFieldName)throw new Error("Invalid UAuth antiforgery response.");return t},appendToForm:async function(n,t){const r=await this.getToken(t);let i=Array.from(n.elements).find(n=>n.name===r.formFieldName);i||(i=document.createElement("input"),i.type="hidden",i.name=r.formFieldName,n.appendChild(i));i.value=r.requestToken}};window.uauth.setDeviceId=function(n){window.uauth.deviceId=n};window.uauth.getDeviceInfo=function(){return{userAgent:navigator.userAgent,platform:navigator.platform,language:navigator.language}} \ No newline at end of file +window.uauth=window.uauth||{};window.uauth.storage={set:function(n,t,i){const r=n==="local"?window.localStorage:window.sessionStorage;r.setItem(t,i)},get:function(n,t){const i=n==="local"?window.localStorage:window.sessionStorage;return i.getItem(t)},remove:function(n,t){const i=n==="local"?window.localStorage:window.sessionStorage;i.removeItem(t)},exists:function(n,t){const i=n==="local"?window.localStorage:window.sessionStorage;return i.getItem(t)!==null}};window.uauth.submitForm=async function(n,t=null){if(!n)return;if(!window.uauth.deviceId)throw new Error("UAuth deviceId is not initialized.");const r=window.uauth.validateAuthUrl(n.action,t);n.action=r;let i=n.querySelector("input[name='__uauth_device']");i||(i=document.createElement("input"),i.type="hidden",i.name="__uauth_device",n.appendChild(i));i.value=window.uauth.deviceId;await window.uauth.antiforgery.appendToForm(n,t);n.submit()};window.uauth.tryAndCommit=async function(n){const{tryUrl:c,commitUrl:l,data:e,clientProfile:o,trustedOrigin:u}=n,a=window.uauth.validateAuthUrl(c,u),v=window.uauth.validateAuthUrl(l,u),s=window.uauth.deviceId;if(!s)throw new Error("UAuth deviceId is not initialized.");const r=await window.uauth.postJson({url:a,payload:e,clientProfile:o,trustedOrigin:u});if(!r||!Number.isInteger(r.status))throw new Error("Invalid UAuth transport response.");const i=r.status;if(i===429)return{isSuccess:!1,status:429,errorCode:"rate_limited",retryAfter:r.retryAfter??null};const y=i>=200&&i<300,h=i>=400&&i<500;if(!y&&!h)throw new Error(`Unexpected UAuth try response: HTTP ${i}.`);const t=r.body;if(!t||typeof t!="object"||Array.isArray(t)||typeof t.isSuccess!="boolean")throw new Error("Invalid UAuth try response body.");if(h&&t.isSuccess)throw new Error("Successful authentication result returned with HTTP error.");const f={isSuccess:t.isSuccess,reason:t.reason??null,remainingAttempts:t.remainingAttempts??null,lockoutUntilUtc:t.lockoutUntilUtc??null,requiresMfa:t.requiresMfa??!1,retryWithNewPkce:t.retryWithNewPkce??!1,previewReceipt:t.previewReceipt??null};if(f.isSuccess){const n=document.createElement("form");n.method="POST";n.action=v;for(const t of Object.keys(e)){const i=document.createElement("input");i.type="hidden";i.name=t;i.value=e[t]??"";n.appendChild(i)}if(f.previewReceipt){const t=document.createElement("input");t.type="hidden";t.name="PreviewReceipt";t.value=f.previewReceipt;n.appendChild(t)}const t=document.createElement("input");t.type="hidden";t.name="__uauth_client_profile";t.value=o??"";n.appendChild(t);const i=document.createElement("input");i.type="hidden";i.name="__uauth_device";i.value=s;n.appendChild(i);document.body.appendChild(n);try{await window.uauth.submitForm(n,u)}catch(r){n.remove();throw r;}}return f};window.uauth.post=async function(n){const{url:e,mode:t,data:i,clientProfile:u,trustedOrigin:r}=n,f=window.uauth.validateAuthUrl(e,r);if(t==="navigate"){const n=document.createElement("form");n.method="POST";n.action=f;const t=document.createElement("input");t.type="hidden";t.name="__uauth_client_profile";t.value=u??"";n.appendChild(t);const e=document.createElement("input");if(e.type="hidden",e.name="__uauth_device",e.value=window.uauth.deviceId,n.appendChild(e),i)for(const t in i){const r=document.createElement("input");r.type="hidden";r.name=t;r.value=i[t];n.appendChild(r)}document.body.appendChild(n);try{await window.uauth.submitForm(n,r)}catch(o){n.remove();throw o;}return null}const o=t==="logout"||t==="logout-observe";return window.uauth.request({url:f,method:"POST",payload:i,clientProfile:u,contentType:"form",credentials:"include",allowCrossOriginCredentials:!0,antiforgery:!0,trustedOrigin:r,navigateOnSuccess:t==="logout",headers:o?{"X-UAuth-Logout-Mode":"observe"}:{}})};window.uauth.postJson=function(n){const t=window.uauth.validateAuthUrl(n.url,n.trustedOrigin);return window.uauth.request({url:t,method:"POST",payload:n.payload,clientProfile:n.clientProfile,contentType:"json",credentials:"include",allowCrossOriginCredentials:!0,antiforgery:!0,trustedOrigin:n.trustedOrigin})};window.uauth.request=async function(n){const{followRedirects:a=!1,navigateOnSuccess:v=!1,url:h,method:y="GET",payload:r=null,clientProfile:p="",contentType:c="json",credentials:s="same-origin",headers:w={}}=n;if(!h)throw new Error("Request URL is required.");const u=new URL(h,document.baseURI);if(!["http:","https:"].includes(u.protocol))throw new Error("Unsupported request URL protocol.");const i=y.toUpperCase();if(!["GET","POST","PUT","PATCH","DELETE"].includes(i))throw new Error(`Unsupported HTTP method: ${i}`);if(!["same-origin","include","omit"].includes(s))throw new Error("Invalid credentials policy.");if(s==="include"&&u.origin!==window.location.origin&&!n.allowCrossOriginCredentials)throw new Error("Cross-origin credentials require explicit authorization.");if(!window.uauth.deviceId)throw new Error("UAuth deviceId is not initialized.");const f={...w,"X-UDID":window.uauth.deviceId,"X-UAuth-ClientProfile":p,"X-Requested-With":"UAuth"};let e=null;if(r!==null&&r!==undefined){if(i==="GET")throw new Error("GET requests cannot contain a body.");if(c==="json")f["Content-Type"]="application/json",e=JSON.stringify(r);else if(c==="form"){f["Content-Type"]="application/x-www-form-urlencoded";e=new URLSearchParams;for(const[n,t]of Object.entries(r))e.append(n,t??"")}else throw new Error("Unsupported content type.");}if(i!=="GET"&&n.antiforgery===!0){const i=window.uauth.validateAuthUrl(window.uauth.antiforgery.tokenUrl,n.trustedOrigin);if(new URL(i).origin!==u.origin)throw new Error("UAuth antiforgery token origin differs from request origin.");const t=await window.uauth.antiforgery.getToken(n.trustedOrigin);f[t.headerName]=t.requestToken}let t;try{t=await fetch(u.href,{method:i,credentials:s,redirect:a?"follow":"error",headers:f,body:e})}catch(l){if(l instanceof TypeError)return{ok:!1,status:0,refreshOutcome:null,body:null};throw l;}let o=null;if(t.status!==204&&t.status!==205){const n=await t.text();if(n)try{o=JSON.parse(n)}catch{o=n}}if(v&&t.ok){const n=o?.redirectUrl;if(typeof n=="string"&&n.length>0){const t=new URL(n,window.location.origin);if(!["http:","https:"].includes(t.protocol))throw new Error("Invalid logout redirect protocol.");window.location.assign(t.href)}}return{retryAfter:t.headers.get("Retry-After"),ok:t.ok,status:t.status,refreshOutcome:t.headers.get("X-UAuth-Refresh"),body:o}};window.uauth.resourceRequest=async function(n){const{url:o,baseAddress:h,method:v="GET",payload:f=null,clientProfile:y="",credentials:p="Auto",requestId:u}=n;if(!o||typeof o!="string")throw new Error("Resource request URL is required.");const i=h?new URL(h):new URL(window.location.origin+"/");if(!["http:","https:"].includes(i.protocol))throw new Error("Invalid Resource API protocol.");if(i.username||i.password||i.search||i.hash)throw new Error("Invalid Resource API base address.");const t=new URL(o,i);if(t.origin!==i.origin)throw new Error("Resource request target differs from configured API origin.");if(t.username||t.password)throw new Error("Resource URL cannot contain credentials.");if(!["http:","https:"].includes(t.protocol))throw new Error("Unsupported Resource API protocol.");const s=v.toUpperCase();if(!["GET","POST","PUT","PATCH","DELETE"].includes(s))throw new Error("Unsupported Resource API method.");if(s==="GET"&&f!==null)throw new Error("GET requests cannot contain a body.");let r;switch(p.toLowerCase()){case"auto":r=t.origin===window.location.origin?"same-origin":"include";break;case"sameorigin":r="same-origin";break;case"include":r="include";break;case"omit":r="omit";break;default:throw new Error("Invalid Resource credentials mode.");}if(r==="include"&&t.protocol!=="https:"&&!["localhost","127.0.0.1","[::1]"].includes(t.hostname))throw new Error("Cross-origin credential requests require HTTPS.");if(!window.uauth.deviceId)throw new Error("UAuth deviceId is not initialized.");const c={"X-UDID":window.uauth.deviceId,"X-UAuth-ClientProfile":y,"X-Requested-With":"UAuth"};let l=null;f!==null&&f!==undefined&&(c["Content-Type"]="application/json",l=JSON.stringify(f));const a=new AbortController;if(u){if(window.uauth.resourceRequests.has(u))throw new Error("Duplicate Resource request ID.");window.uauth.resourceRequests.set(u,a)}try{const n=await fetch(t.href,{method:s,credentials:r,redirect:"error",headers:c,body:l,signal:a.signal});let i=null;if(n.status!==204&&n.status!==205){const t=await n.text();t.length>0&&(i=t)}return{retryAfter:n.headers.get("Retry-After"),ok:n.ok,status:n.status,body:i,contentType:n.headers.get("Content-Type")}}catch(e){if(e?.name==="AbortError")throw e;if(e instanceof TypeError)return{ok:!1,status:0,body:null,contentType:null};throw e;}finally{u&&window.uauth.resourceRequests.delete(u)}};window.uauth.resourceRequests=new Map;window.uauth.cancelResourceRequest=function(n){const t=window.uauth.resourceRequests.get(n);return t?(t.abort(),!0):!1};window.uauth.validateAuthUrl=function(n,t=null){if(!n||typeof n!="string")throw new Error("Authentication URL is required.");const r=t?new URL(t,window.location.origin):new URL(window.location.origin),i=new URL(n,document.baseURI);if(!["http:","https:"].includes(r.protocol))throw new Error("Unsupported trusted origin protocol.");if(!["http:","https:"].includes(i.protocol))throw new Error("Unsupported authentication URL protocol.");if(r.username||r.password)throw new Error("Trusted origin cannot contain credentials.");if(i.username||i.password)throw new Error("Authentication URL cannot contain credentials.");if(i.origin!==r.origin)throw new Error("Authentication request target differs from the trusted UAuthHub origin.");return i.href};window.uauth.antiforgery={tokenUrl:null,configure:function(n){if(!n?.tokenUrl)throw new Error("UAuth antiforgery token URL is required.");this.tokenUrl=window.uauth.validateAuthUrl(n.tokenUrl,n.trustedOrigin)},getToken:async function(n){if(!this.tokenUrl)throw new Error("UAuth antiforgery is not configured.");const r=window.uauth.validateAuthUrl(this.tokenUrl,n),i=await fetch(r,{method:"GET",credentials:"include",cache:"no-store",redirect:"error",headers:{"X-UDID":window.uauth.deviceId}});if(!i.ok)throw new Error(`Unable to obtain UAuth antiforgery token: HTTP ${i.status}.`);const t=await i.json();if(!t?.requestToken||!t?.headerName||!t?.formFieldName)throw new Error("Invalid UAuth antiforgery response.");return t},appendToForm:async function(n,t){const r=await this.getToken(t);let i=Array.from(n.elements).find(n=>n.name===r.formFieldName);i||(i=document.createElement("input"),i.type="hidden",i.name=r.formFieldName,n.appendChild(i));i.value=r.requestToken}};window.uauth.setDeviceId=function(n){window.uauth.deviceId=n};window.uauth.getDeviceInfo=function(){return{userAgent:navigator.userAgent,platform:navigator.platform,language:navigator.language}} \ No newline at end of file diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/LogoutTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/LogoutTests.cs index 76abc819..f1b757ed 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration/LogoutTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/LogoutTests.cs @@ -3511,6 +3511,114 @@ await LoginAsync( x.StatusCode == HttpStatusCode.Unauthorized); } + [Fact] + public async Task Logout_AntiforgeryTokenMustUseAuthenticatedDeviceContext() + { + _factory.Clock.Reset(); + + var ct = TestContext.Current.CancellationToken; + var user = await _factory.CreateLoginUserAsync(ct: ct); + var deviceId = $"csrf-device-{Guid.NewGuid():N}"; + + using var client = CreateClient(deviceId); + + using var login = await LoginAsync( + client, + user.Identifier, + user.Secret); + + login.StatusCode.Should().Be(HttpStatusCode.Found); + + var sessionCookie = GetSessionCookie(login); + + client.DefaultRequestHeaders.Add("Cookie", sessionCookie); + + // First reproduce the old JS behavior: + // the antiforgery GET does not carry X-UDID. + client.DefaultRequestHeaders.Remove("X-UDID"); + + using var anonymousTokenResponse = await client.GetAsync( + "/auth/antiforgery/token", ct); + + anonymousTokenResponse.StatusCode.Should().Be(HttpStatusCode.OK); + + var anonymousToken = await anonymousTokenResponse.Content + .ReadFromJsonAsync( + cancellationToken: ct); + + anonymousToken.Should().NotBeNull(); + + var antiforgeryCookie = GetAntiforgeryCookie( + anonymousTokenResponse); + + // The POST has the correct device, but its CSRF token + // was obtained in a different authentication context. + using var rejectedRequest = new HttpRequestMessage( + HttpMethod.Post, "/auth/logout"); + + rejectedRequest.Headers.Add("X-UDID", deviceId); + rejectedRequest.Headers.Add( + anonymousToken!.HeaderName, + anonymousToken.RequestToken); + rejectedRequest.Headers.Add( + "Cookie", + $"{sessionCookie}; {antiforgeryCookie}"); + rejectedRequest.Headers.Add( + "X-UAuth-Logout-Mode", "observe"); + + using var rejected = await client.SendAsync( + rejectedRequest, ct); + + rejected.StatusCode.Should().Be(HttpStatusCode.BadRequest); + + var rejectedBody = await rejected.Content.ReadAsStringAsync(ct); + rejectedBody.Should().Contain("invalid_antiforgery_token"); + + // Correct behavior: obtain the token using the same + // Device ID that will be used by the logout POST. + using var tokenRequest = new HttpRequestMessage( + HttpMethod.Get, "/auth/antiforgery/token"); + + tokenRequest.Headers.Add("X-UDID", deviceId); + tokenRequest.Headers.Add("Cookie", sessionCookie); + + using var tokenResponse = await client.SendAsync( + tokenRequest, ct); + + tokenResponse.StatusCode.Should().Be(HttpStatusCode.OK); + + var token = await tokenResponse.Content + .ReadFromJsonAsync( + cancellationToken: ct); + + token.Should().NotBeNull(); + + var validAntiforgeryCookie = GetAntiforgeryCookie( + tokenResponse); + + using var logoutRequest = new HttpRequestMessage( + HttpMethod.Post, "/auth/logout"); + + logoutRequest.Headers.Add("X-UDID", deviceId); + logoutRequest.Headers.Add( + token!.HeaderName, + token.RequestToken); + logoutRequest.Headers.Add( + "Cookie", + $"{sessionCookie}; {validAntiforgeryCookie}"); + logoutRequest.Headers.Add( + "X-UAuth-Logout-Mode", "observe"); + + using var logout = await client.SendAsync( + logoutRequest, ct); + + var logoutBody = await logout.Content.ReadAsStringAsync(ct); + + logout.StatusCode.Should().Be( + HttpStatusCode.OK, + logoutBody); + } + // ------------------------------------------------------------ // Helpers @@ -3592,4 +3700,23 @@ private static string GetSessionCookie(HttpResponseMessage response) return firstSegment[(separator + 1)..]; } + + private sealed record AntiforgeryTokenResponse( + string RequestToken, + string HeaderName, + string FormFieldName); + + private static string GetAntiforgeryCookie( + HttpResponseMessage response) + { + response.Headers + .TryGetValues("Set-Cookie", out var cookies) + .Should().BeTrue(); + + return cookies! + .Select(x => x.Split(';', 2)[0]) + .First(x => x.TrimStart().StartsWith( + ".AspNetCore.Antiforgery.", + StringComparison.OrdinalIgnoreCase)); + } } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/ResourceApi/RemoteSessionValidatorTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/ResourceApi/RemoteSessionValidatorTests.cs index 040f6718..afde4863 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/ResourceApi/RemoteSessionValidatorTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/ResourceApi/RemoteSessionValidatorTests.cs @@ -1,10 +1,11 @@ -using CodeBeam.UltimateAuth.Core.Contracts; +using AwesomeAssertions; +using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Core.Infrastructure; using CodeBeam.UltimateAuth.Core.MultiTenancy; using CodeBeam.UltimateAuth.Core.Options; using CodeBeam.UltimateAuth.Server.ResourceApi; -using AwesomeAssertions; +using CodeBeam.UltimateAuth.Tests.Unit.Helpers; using Microsoft.AspNetCore.Http; using System.Net; using System.Net.Http.Json; @@ -371,6 +372,51 @@ public void ToDomain_WhenIsValidTrueButStateIsRevoked_ShouldFailClosed() } + [Fact] + public async Task ValidateSessionAsync_WhenDeviceIdExists_ShouldForwardDeviceId() + { + var handler = new TestHttpMessageHandler + { + Response = CreateFailureResponse(HttpStatusCode.Unauthorized) + }; + + var sut = CreateSut(handler); + + var context = CreateValidationContext() with + { + Device = TestDevice.Default() + }; + + await sut.ValidateSessionAsync( + context, + ct: TestContext.Current.CancellationToken); + + handler.DeviceId.Should().Be(context.Device.DeviceId!.Value.Value); + } + + [Fact] + public async Task ValidateSessionAsync_WhenDeviceIdIsMissing_ShouldNotForwardDeviceId() + { + var handler = new TestHttpMessageHandler + { + Response = CreateFailureResponse(HttpStatusCode.Unauthorized) + }; + + var sut = CreateSut(handler); + + var context = CreateValidationContext() with + { + Device = DeviceContext.Anonymous() + }; + + await sut.ValidateSessionAsync( + context, + ct: TestContext.Current.CancellationToken); + + handler.DeviceId.Should().BeNull(); + } + + private static RemoteSessionValidator CreateSut(TestHttpMessageHandler handler, HttpContext? httpContext = null) { var httpClient = new HttpClient(handler) @@ -418,11 +464,13 @@ private sealed class TestHttpMessageHandler : HttpMessageHandler public Uri? RequestUri { get; private set; } public string? Body { get; private set; } public string? Cookie { get; private set; } + public string? DeviceId { get; private set; } protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { Method = request.Method; RequestUri = request.RequestUri; + DeviceId = request.Headers.TryGetValues("X-UDID", out var values) ? values.SingleOrDefault() : null; if (request.Content is not null) { diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/UAuthSessionValidatorTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/UAuthSessionValidatorTests.cs index 270ce6ea..e7a39459 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/UAuthSessionValidatorTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Server/UAuthSessionValidatorTests.cs @@ -356,13 +356,66 @@ public async Task ValidateSessionAsync_WhenDeviceDoesNotMatchAndBehaviorIsReject } [Fact] - public async Task ValidateSessionAsync_WhenRequestHasNoDeviceId_AllowsValidation() + public async Task ValidateSessionAsync_WhenRequestHasNoDeviceIdAndBehaviorIsReject_ReturnsDeviceMismatch() + { + var fixture = CreateFixture(requestDevice: DeviceContext.Anonymous(), deviceMismatchBehavior: DeviceMismatchBehavior.Reject); + + SetupValidAggregate(fixture); + + var result = await fixture.Sut.ValidateSessionAsync( + fixture.Context, + ct: TestContext.Current.CancellationToken); + + result.IsValid.Should().BeFalse(); + result.State.Should().Be(SessionState.DeviceMismatch); + result.UserKey.Should().Be(fixture.User); + result.SessionId.Should().Be(fixture.Session.SessionId); + result.ChainId.Should().Be(fixture.Chain.ChainId); + result.RootId.Should().Be(fixture.Root.RootId); + + fixture.ClaimsProvider.Verify( + x => x.GetClaimsAsync( + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Never); + } + + + [Theory] + [InlineData(DeviceMismatchBehavior.Allow)] + [InlineData(DeviceMismatchBehavior.AllowAndRebind)] + public async Task ValidateSessionAsync_WhenDeviceDoesNotMatchAndBehaviorAllows_ReturnsActive( + DeviceMismatchBehavior behavior) { var fixture = CreateFixture( - requestDevice: DeviceContext.Anonymous()); + requestDevice: TestDevice.Alternative(), + deviceMismatchBehavior: behavior); - var claims = ClaimsSnapshot.From( - ("uauth:permission", "orders.read")); + SetupValidAggregate(fixture); + + fixture.ClaimsProvider + .Setup(x => x.GetClaimsAsync( + fixture.Tenant, + fixture.User, + It.IsAny())) + .ReturnsAsync(ClaimsSnapshot.Empty); + + var result = await fixture.Sut.ValidateSessionAsync( + fixture.Context, + ct: TestContext.Current.CancellationToken); + + result.IsValid.Should().BeTrue(); + result.State.Should().Be(SessionState.Active); + result.BoundDeviceId.Should().Be(fixture.Chain.Device.DeviceId); + } + + [Theory] + [InlineData(DeviceMismatchBehavior.Allow)] + [InlineData(DeviceMismatchBehavior.AllowAndRebind)] + public async Task ValidateSessionAsync_WhenRequestHasNoDeviceIdAndBehaviorAllows_ReturnsActive(DeviceMismatchBehavior behavior) + { + var fixture = CreateFixture(requestDevice: DeviceContext.Anonymous(), deviceMismatchBehavior: behavior); SetupValidAggregate(fixture); @@ -371,9 +424,11 @@ public async Task ValidateSessionAsync_WhenRequestHasNoDeviceId_AllowsValidation fixture.Tenant, fixture.User, It.IsAny())) - .ReturnsAsync(claims); + .ReturnsAsync(ClaimsSnapshot.Empty); - var result = await fixture.Sut.ValidateSessionAsync(fixture.Context, ct: TestContext.Current.CancellationToken); + var result = await fixture.Sut.ValidateSessionAsync( + fixture.Context, + ct: TestContext.Current.CancellationToken); result.IsValid.Should().BeTrue(); result.State.Should().Be(SessionState.Active); @@ -417,6 +472,33 @@ public async Task ValidateSessionAsync_WhenAggregateIsValid_ReturnsActiveResult( result.BoundDeviceId.Should().Be(fixture.Chain.Device.DeviceId); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ValidateSessionAsync_WhenStoredDeviceIdIsMissingAndBehaviorIsReject_ReturnsDeviceMismatch(bool requestDeviceMissing) + { + var fixture = CreateFixture( + storedDevice: DeviceContext.Anonymous(), + requestDevice: requestDeviceMissing + ? DeviceContext.Anonymous() + : TestDevice.Default(), + deviceMismatchBehavior: DeviceMismatchBehavior.Reject); + + SetupValidAggregate(fixture); + + var result = await fixture.Sut.ValidateSessionAsync(fixture.Context, ct: TestContext.Current.CancellationToken); + + result.IsValid.Should().BeFalse(); + result.State.Should().Be(SessionState.DeviceMismatch); + + fixture.ClaimsProvider.Verify( + x => x.GetClaimsAsync( + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Never); + } + [Fact] public async Task ValidateSessionAsync_PropagatesCancellationToken() { @@ -514,8 +596,8 @@ private static Fixture CreateFixture( DateTimeOffset? chainExpiresAt = null, TimeSpan? idleTimeout = null, DeviceContext? requestDevice = null, - DeviceMismatchBehavior deviceMismatchBehavior = - DeviceMismatchBehavior.Reject) + DeviceMismatchBehavior deviceMismatchBehavior = DeviceMismatchBehavior.Reject, + DeviceContext? storedDevice = null) { var tenant = TenantKey.Single; var user = UserKey.New(); @@ -532,7 +614,7 @@ private static Fixture CreateFixture( user, chainCreatedAt ?? Now.AddHours(-1), chainExpiresAt ?? Now.AddDays(7), - TestDevice.Default(), + storedDevice ?? TestDevice.Default(), ClaimsSnapshot.Empty, root.SecurityVersion); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/javascript/uauth.test.cjs b/tests/CodeBeam.UltimateAuth.Tests.Unit/javascript/uauth.test.cjs index bb289e9d..d3582c73 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/javascript/uauth.test.cjs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/javascript/uauth.test.cjs @@ -614,3 +614,53 @@ test("observed logout rejects unsafe redirect protocols", async () => { assert.equal(navigation, null); }); + +test("authenticated logout uses the same Device ID for CSRF token and POST", async () => { + const deviceId = "test-device"; + let tokenDeviceId = null; + let logoutDeviceId = null; + + const uauth = createEnvironment(async (url, options) => { + if (url === authUrl("/auth/antiforgery/token")) { + tokenDeviceId = options.headers?.["X-UDID"] ?? null; + + return { + ok: true, + status: 200, + json: async () => ({ + requestToken: "csrf-test-token", + headerName: "X-UAuth-CSRF", + formFieldName: "__RequestVerificationToken" + }) + }; + } + + assert.equal(url, authUrl("/auth/logout")); + + logoutDeviceId = options.headers?.["X-UDID"] ?? null; + + return { + ok: true, + status: 200, + redirected: false, + headers: { get: () => null }, + text: async () => JSON.stringify({ + isSuccess: true, + redirectUrl: null + }) + }; + }); + + configureAntiforgery(uauth); + + const result = await uauth.post({ + url: authUrl("/auth/logout"), + mode: "logout-observe", + clientProfile: "BlazorServer", + trustedOrigin: HUB_ORIGIN + }); + + assert.equal(result.status, 200); + assert.equal(logoutDeviceId, deviceId); + assert.equal(tokenDeviceId, deviceId); +}); From 2fedceddb8e1376763cbcd45876d66a2970175f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Can=20Karag=C3=B6z?= <78308169+mckaragoz@users.noreply.github.com> Date: Sat, 7 Nov 2026 15:09:21 +0300 Subject: [PATCH 09/11] Fix External Hub Antiforgery Cookies and Strengthen CSRF Regression Tests (#148) --- .../Extensions/ServiceCollectionExtensions.cs | 25 ++++- .../Options/UAuthAntiforgeryOptions.cs | 13 ++- .../Security/AntiforgeryCookiePolicyTests.cs | 94 +++++++++++++++++++ .../Security/CsrfAttackTests.cs | 55 ++++++----- 4 files changed, 154 insertions(+), 33 deletions(-) create mode 100644 tests/CodeBeam.UltimateAuth.Tests.Integration/Security/AntiforgeryCookiePolicyTests.cs diff --git a/src/CodeBeam.UltimateAuth.Server/Extensions/ServiceCollectionExtensions.cs b/src/CodeBeam.UltimateAuth.Server/Extensions/ServiceCollectionExtensions.cs index ce14a9e6..fba91eff 100644 --- a/src/CodeBeam.UltimateAuth.Server/Extensions/ServiceCollectionExtensions.cs +++ b/src/CodeBeam.UltimateAuth.Server/Extensions/ServiceCollectionExtensions.cs @@ -1,5 +1,4 @@ -using Microsoft.AspNetCore.Builder; -using CodeBeam.UltimateAuth.Authorization; +using CodeBeam.UltimateAuth.Authorization; using CodeBeam.UltimateAuth.Core; using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Defaults; @@ -11,6 +10,7 @@ using CodeBeam.UltimateAuth.Core.Options; using CodeBeam.UltimateAuth.Core.Runtime; using CodeBeam.UltimateAuth.Credentials; +using CodeBeam.UltimateAuth.Credentials.Extensions; using CodeBeam.UltimateAuth.Policies.Abstractions; using CodeBeam.UltimateAuth.Policies.Defaults; using CodeBeam.UltimateAuth.Policies.Registry; @@ -30,13 +30,15 @@ using CodeBeam.UltimateAuth.Server.Stores; using CodeBeam.UltimateAuth.Users; using CodeBeam.UltimateAuth.Users.Contracts; +using Microsoft.AspNetCore.Antiforgery; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; -using CodeBeam.UltimateAuth.Credentials.Extensions; namespace CodeBeam.UltimateAuth.Server.Extensions; @@ -321,6 +323,23 @@ private static IServiceCollection AddUltimateAuthServerInternal(this IServiceCol options.FormFieldName = "__RequestVerificationToken"; }); + services.AddOptions().PostConfigure>((antiforgery, serverOptions) => + { + var server = serverOptions.Value; + + var sameSite = server.Antiforgery.CookieSameSite + ?? (server.HubDeploymentMode == UAuthHubDeploymentMode.External + ? SameSiteMode.None + : SameSiteMode.Strict); + + antiforgery.Cookie.SameSite = sameSite; + + if (sameSite == SameSiteMode.None) + { + antiforgery.Cookie.SecurePolicy = CookieSecurePolicy.Always; + } + }); + services.AddScoped(); services.AddScoped(); diff --git a/src/CodeBeam.UltimateAuth.Server/Options/UAuthAntiforgeryOptions.cs b/src/CodeBeam.UltimateAuth.Server/Options/UAuthAntiforgeryOptions.cs index f7f35eb3..739e4c81 100644 --- a/src/CodeBeam.UltimateAuth.Server/Options/UAuthAntiforgeryOptions.cs +++ b/src/CodeBeam.UltimateAuth.Server/Options/UAuthAntiforgeryOptions.cs @@ -1,4 +1,6 @@ -namespace CodeBeam.UltimateAuth.Server.Options; +using Microsoft.AspNetCore.Http; + +namespace CodeBeam.UltimateAuth.Server.Options; /// /// Configures CSRF protection for UltimateAuth endpoints. @@ -13,8 +15,15 @@ public sealed class UAuthAntiforgeryOptions /// public string TokenEndpoint { get; set; } = "/antiforgery/token"; + /// + /// Overrides the antiforgery cookie SameSite policy. + /// When null, External Hub deployments use None, other deployments retain ASP.NET Core's Strict default. + /// + public SameSiteMode? CookieSameSite { get; set; } + public UAuthAntiforgeryOptions Clone() => new() { - TokenEndpoint = TokenEndpoint + TokenEndpoint = TokenEndpoint, + CookieSameSite = CookieSameSite }; } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/Security/AntiforgeryCookiePolicyTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/Security/AntiforgeryCookiePolicyTests.cs new file mode 100644 index 00000000..0d17113c --- /dev/null +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/Security/AntiforgeryCookiePolicyTests.cs @@ -0,0 +1,94 @@ + +using AwesomeAssertions; +using CodeBeam.UltimateAuth.Core; +using CodeBeam.UltimateAuth.Server.Options; +using CodeBeam.UltimateAuth.Tests.Integration.Infrastructure; +using Microsoft.AspNetCore.Antiforgery; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace CodeBeam.UltimateAuth.Tests.Integration.Security; + +public sealed class AntiforgeryCookiePolicyTests +{ + [Theory] + [InlineData(UAuthHubDeploymentMode.Embedded, SameSiteMode.Strict)] + [InlineData(UAuthHubDeploymentMode.Integrated, SameSiteMode.Strict)] + [InlineData(UAuthHubDeploymentMode.External, SameSiteMode.None)] + public async Task AntiforgeryCookie_UsesDeploymentPolicy( + UAuthHubDeploymentMode deployment, + SameSiteMode expected) + { + await using var factory = AuthServerFactory.Create( + options => options.HubDeploymentMode = deployment); + + var antiforgery = factory.Services + .GetRequiredService>() + .Value; + + antiforgery.Cookie.SameSite.Should().Be(expected); + + if (expected == SameSiteMode.None) + { + antiforgery.Cookie.SecurePolicy + .Should().Be(CookieSecurePolicy.Always); + } + } + + [Fact] + public async Task ExternalHub_AntiforgeryTokenCookie_IsSecureAndCrossSite() + { + await using var factory = AuthServerFactory.Create( + options => options.HubDeploymentMode = + UAuthHubDeploymentMode.External); + + using var client = factory.CreateClient( + new WebApplicationFactoryClientOptions + { + BaseAddress = new Uri("https://localhost"), + AllowAutoRedirect = false + }); + + using var response = await client.GetAsync( + "/auth/antiforgery/token", + TestContext.Current.CancellationToken); + + response.EnsureSuccessStatusCode(); + + response.Headers.TryGetValues("Set-Cookie", out var cookies) + .Should().BeTrue(); + + var cookie = cookies!.Single(x => + x.StartsWith( + ".AspNetCore.Antiforgery.", + StringComparison.OrdinalIgnoreCase)); + + cookie.Contains( + "samesite=none", + StringComparison.OrdinalIgnoreCase).Should().BeTrue(); + + cookie.Split(';', StringSplitOptions.TrimEntries) + .Any(x => x.Equals( + "secure", + StringComparison.OrdinalIgnoreCase)) + .Should().BeTrue(); + } + + [Fact] + public async Task ExplicitAntiforgeryCookiePolicy_OverridesDeploymentDefault() + { + await using var factory = AuthServerFactory.Create(options => + { + options.HubDeploymentMode = UAuthHubDeploymentMode.External; + options.Antiforgery.CookieSameSite = SameSiteMode.Lax; + }); + + var antiforgery = factory.Services + .GetRequiredService>() + .Value; + + antiforgery.Cookie.SameSite.Should().Be(SameSiteMode.Lax); + } +} diff --git a/tests/CodeBeam.UltimateAuth.Tests.Integration/Security/CsrfAttackTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Integration/Security/CsrfAttackTests.cs index 52d4e724..e11c38a3 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Integration/Security/CsrfAttackTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Integration/Security/CsrfAttackTests.cs @@ -33,25 +33,29 @@ public async Task Login_WithoutCsrfToken_MustNotCreateSession() public async Task Login_WithForgedCsrfToken_MustNotCreateSession() { await using var factory = new AuthServerFactory(); - var attacker = await factory.CreateLoginUserAsync(ct: TestContext.Current.CancellationToken); + var ct = TestContext.Current.CancellationToken; + + var user = await factory.CreateLoginUserAsync(ct: ct); using var victimBrowser = CreateBrowser(factory); - using var request = new HttpRequestMessage( - HttpMethod.Post, "/auth/login") + // Obtain a real antiforgery cookie and request token. + var tokens = await GetTokens(victimBrowser); + + using var request = new HttpRequestMessage(HttpMethod.Post, "/auth/login") { Content = JsonContent.Create(new { - identifier = attacker.Identifier, - secret = attacker.Secret + identifier = user.Identifier, + secret = user.Secret }) }; - request.Headers.TryAddWithoutValidation( - "RequestVerificationToken", - "attacker-forged-token"); + // Keep the legitimate cookie, but forge the request token. + request.Headers.TryAddWithoutValidation(tokens.HeaderName, "attacker-forged-token"); + request.Headers.TryAddWithoutValidation("Cookie", tokens.Cookie); - using var response = await victimBrowser.SendAsync(request, TestContext.Current.CancellationToken); + using var response = await victimBrowser.SendAsync(request, ct); await AssertCsrfRejected(response); AssertNoSessionCookie(response); @@ -167,28 +171,26 @@ public async Task RevokeOthers_CsrfAttack_MustNotRevokeSessions() // The attack must not revoke the other session. await AssertAuthenticated(otherBrowser); - // ATTACK 2: Forged antiforgery token. - using (var forgedRequest = new HttpRequestMessage( - HttpMethod.Post, - "/auth/me/sessions/revoke-others")) + // ATTACK 2: Valid antiforgery cookie with a forged request token. + var forgedTokens = await GetTokens(currentBrowser); + + using (var forgedRequest = new HttpRequestMessage(HttpMethod.Post, "/auth/me/sessions/revoke-others")) { - forgedRequest.Headers.TryAddWithoutValidation( - "RequestVerificationToken", - "forged-token"); + forgedRequest.Headers.TryAddWithoutValidation(forgedTokens.HeaderName, "attacker-forged-token"); - using var forged = await currentBrowser.SendAsync( - forgedRequest, TestContext.Current.CancellationToken); + // Preserve the authenticated session cookie and + // include the legitimate antiforgery cookie. + forgedRequest.Headers.TryAddWithoutValidation("Cookie", $"{currentCookie}; {forgedTokens.Cookie}"); + using var forged = await currentBrowser.SendAsync(forgedRequest, TestContext.Current.CancellationToken); await AssertCsrfRejected(forged); } - // The second attack must not revoke the other session. + // A rejected CSRF request must not revoke the other session. await AssertAuthenticated(otherBrowser); // CONTROL: Valid CSRF token must allow the operation. - using (var valid = await CsrfTestClient.PostAsync( - currentBrowser, - "/auth/me/sessions/revoke-others")) + using (var valid = await CsrfTestClient.PostAsync(currentBrowser, "/auth/me/sessions/revoke-others")) { valid.IsSuccessStatusCode.Should().BeTrue( $"valid revoke failed: " + @@ -199,15 +201,12 @@ public async Task RevokeOthers_CsrfAttack_MustNotRevokeSessions() await AssertAuthenticated(currentBrowser); // The other session must now be revoked. - using var revokedCheck = await CsrfTestClient.PostJsonAsync( - otherBrowser, - "/auth/me/identifiers/get", - new { }); + using var revokedCheck = await CsrfTestClient.PostJsonAsync(otherBrowser, "/auth/me/identifiers/get", new { }); - revokedCheck.StatusCode.Should().Be( - HttpStatusCode.Unauthorized); + revokedCheck.StatusCode.Should().Be(HttpStatusCode.Unauthorized); } + private static async Task LoginAndGetSessionCookie( HttpClient browser, IntegrationTestUser user) From b88448d4ffeb6c985c544ad8e2d6e3e795a99fd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Can=20Karag=C3=B6z?= <78308169+mckaragoz@users.noreply.github.com> Date: Sat, 7 Nov 2026 20:52:15 +0300 Subject: [PATCH 10/11] Improve Identity Lab Login UX and Sidebar Footer (#149) * Improve Identity Lab Login UX and Sidebar Footer * Completed Implementations --- README.md | 3 +- .../Components/Layout/DashboardLayout.razor | 145 +++++++------ .../Layout/DashboardLayout.razor.css | 116 ++++++++++- .../Components/Pages/LoginPage.razor | 28 ++- docs/content/migration/index.md | 82 +++++++- docs/content/migration/v0.1.4.md | 194 ++++++++++++++++++ docs/content/migration/v0.2.0.md | 159 ++++++++++++++ .../wwwroot/docs/docs-index.json | 14 ++ .../wwwroot/docs/migration/index.json | 38 +++- .../wwwroot/docs/migration/v0.1.4.json | 54 +++++ .../wwwroot/docs/migration/v0.2.0.json | 39 ++++ .../Components/UAuthLoginForm.razor.cs | 30 ++- 12 files changed, 812 insertions(+), 90 deletions(-) create mode 100644 docs/content/migration/v0.1.4.md create mode 100644 docs/content/migration/v0.2.0.md create mode 100644 docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/migration/v0.1.4.json create mode 100644 docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/migration/v0.2.0.json diff --git a/README.md b/README.md index a157e2c6..37dd42fc 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ --- -UltimateAuth is an open-source auth framework with platform-level capabilities that unifies secure session, cookie and token based Auth, modern PKCE flows, Blazor/Maui-ready client experiences - eliminating the complexity of traditional Auth systems while providing a clean, lightweight, extensible and developer-first architecture. +UltimateAuth is an open-source auth framework with platform-level capabilities that unifies secure session, cookie and token based Auth, modern PKCE flows, Blazor/MAUI-ready client experiences - eliminating the complexity of traditional Auth systems while providing a clean, lightweight, extensible and developer-first architecture. --- ## Roadmap @@ -70,6 +70,7 @@ UltimateAuth is designed to deliver a simple, consistent developer experience fr --- + ### 💜 Join the UltimateAuth Community UltimateAuth is more than a framework — it's a growing open-source community. diff --git a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Layout/DashboardLayout.razor b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Layout/DashboardLayout.razor index fbb8cedb..54c68294 100644 --- a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Layout/DashboardLayout.razor +++ b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Layout/DashboardLayout.razor @@ -118,85 +118,110 @@
- -
WORKSPACE
+
+ +
WORKSPACE
+ + + + Platform Administration + + - - - Platform Administration + + Overview - - - Overview - +
PERSONAL
-
PERSONAL
+ + My Identity + - - My Identity - + + My Sessions + - - My Sessions - + + Session Diagnostics + - - Session Diagnostics - + + My Access + - - My Access - + + Authorization Lab + - - Authorization Lab - + @if (ShowManagement) + { +
SANDBOX MANAGEMENT
- @if (ShowManagement) - { -
SANDBOX MANAGEMENT
+ + Users + - - Users - + + Authorization + - - Authorization - + + Activity + - - Activity - + + Settings + + } - - Settings - - } + + + Sign Out + + - - - Sign Out +
PLAYGROUND
+ + + Demo Store
+
-
PLAYGROUND
- - - Demo Store - -
- - - -
- - Powered by UltimateAuth + - - v @UltimateAuthVersion - .NET @LabVersionInfo.DotNetVersion -
diff --git a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Layout/DashboardLayout.razor.css b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Layout/DashboardLayout.razor.css index 00097497..efe385e9 100644 --- a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Layout/DashboardLayout.razor.css +++ b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Layout/DashboardLayout.razor.css @@ -54,8 +54,9 @@ .lab-sidebar-content { display: flex; flex-direction: column; - min-height: 100%; - padding: 20px 12px; + height: 100%; + min-height: 0; + padding: 20px 12px 12px; } .lab-sidebar-sandbox { @@ -117,15 +118,118 @@ font-weight: 650; } -.lab-sidebar-bottom { + +/* Scrollable navigation */ + +.lab-sidebar-navigation { + flex: 1 1 auto; + min-height: 0; + overflow-y: auto; + overflow-x: hidden; + scrollbar-width: thin; + scrollbar-color: var(--mud-palette-lines-default) transparent; +} + +/* Product footer */ + +.lab-sidebar-footer { + flex-shrink: 0; + padding-top: 12px; + margin-top: 12px; + border-top: 1px solid var(--mud-palette-lines-default); +} + +.lab-sidebar-product { + padding: 12px; + border: 1px solid var(--mud-palette-lines-default); + border-radius: 12px; + background: var(--mud-palette-surface); +} + +.lab-sidebar-product-brand { + display: flex; + align-items: center; + gap: 9px; + color: var(--mud-palette-text-primary); + text-decoration: none; + min-width: 0; +} + +.lab-sidebar-product-icon { + width: 36px; + height: 36px; + flex-shrink: 0; display: flex; align-items: center; - gap: 8px; - padding: 18px 12px 4px; + justify-content: center; + border-radius: 9px; + background: var(--mud-palette-primary-hover); +} + +.lab-sidebar-product-info { + display: flex; + flex: 1; + flex-direction: column; + gap: 3px; + min-width: 0; +} + + .lab-sidebar-product-info strong { + font-size: 0.75rem; + font-weight: 700; + line-height: 1.3; + white-space: nowrap; + } + + .lab-sidebar-product-info small { + font-size: 0.66rem; + color: var(--mud-palette-text-secondary); + } + +.lab-sidebar-product-external { + flex-shrink: 0; color: var(--mud-palette-text-secondary); - font-size: .72rem; + opacity: 0.65; +} + +.lab-sidebar-product-brand:hover .lab-sidebar-product-external { + color: var(--mud-palette-primary); + opacity: 1; } +/* Version information */ + +.lab-sidebar-product-meta { + display: flex; + align-items: center; + justify-content: space-between; + gap: 6px; + margin-top: 12px; + padding-top: 10px; + border-top: 1px solid var(--mud-palette-lines-default); +} + + .lab-sidebar-product-meta a, + .lab-sidebar-product-runtime { + display: inline-flex; + align-items: center; + gap: 4px; + min-width: 0; + color: var(--mud-palette-text-secondary); + font-size: 0.65rem; + white-space: nowrap; + text-decoration: none; + } + + .lab-sidebar-product-meta .mud-icon-root { + font-size: 14px; + } + + .lab-sidebar-product-meta a:hover { + color: var(--mud-palette-primary); + } + + .lab-dashboard-main { min-height: calc(100vh - var(--mud-appbar-height)); } diff --git a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/LoginPage.razor b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/LoginPage.razor index fc4a05b5..d512cd87 100644 --- a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/LoginPage.razor +++ b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Components/Pages/LoginPage.razor @@ -107,7 +107,7 @@ - + @if (IsLockedOut) { @@ -127,10 +127,7 @@ @LockoutCountdown - + Try again after @@ -187,7 +184,7 @@ MaxLength="256" Required="true" RequiredError="Email or username is required." - Disabled="IsLockedOut" + Disabled="@_loginBusy" Immediate="true" /> - @(IsLockedOut ? "Temporarily locked" : "Sign In") - + @@ -315,8 +314,15 @@ ?? AuthState.Identity?.PrimaryUserName ?? "User"; - private string? CurrentUserName => - AuthState.Identity?.PrimaryUserName; + private string? CurrentUserName => AuthState.Identity?.PrimaryUserName; + + private bool _loginBusy; + + private async Task HandleSubmittingChanged(bool submitting) + { + _loginBusy = submitting; + await InvokeAsync(StateHasChanged); + } private void ContinueToDashboard() { diff --git a/docs/content/migration/index.md b/docs/content/migration/index.md index 0cd8f9d0..f890b36a 100644 --- a/docs/content/migration/index.md +++ b/docs/content/migration/index.md @@ -6,10 +6,84 @@ group: migration # Migration Guides -Version-specific instructions for upgrading UltimateAuth applications. +Keep your UltimateAuth applications up to date with clear, version-specific upgrade guidance. -Migration guides focus on breaking changes, behavior changes, and required application updates. +This page provides an overview of stable UltimateAuth releases, their migration impact, and any actions required when upgrading. -## Available Guides +**Most updates require little or no application code changes.** When an update requires attention, a dedicated migration guide explains what changed and how to adapt your application. -- [Migrating to v0.1.1](/docs/migration/v0.1.1) — Blazor route authorization and component lifecycle changes. +## Version Migration Overview + +The table below lists stable releases in descending order, with the newest version first. + +Preview and release candidate versions are excluded. + +| Version | Migration Impact | What's Changed | Migration Guide | +|---|---|---|---| +| **v0.2.0 (In Progress)** | **Medium (Provisional)** | Async authorization APIs, authorization checks, and integration changes | [View Guide](/docs/migration/v0.2.0) | +| **v0.1.4** | **Low** | CSRF protection, antiforgery integration, and interface changes | [View Guide](/docs/migration/v0.1.4) | +| **v0.1.3** | **None** | Session deletion, revocation, and refresh consistency fixes | Not required | +| **v0.1.2** | **None** | Password change transaction and session revocation fixes | Not required | +| **v0.1.1** | **Low** | Blazor route authorization and component lifecycle changes | [View Guide](/docs/migration/v0.1.1) | +| **v0.1.0** | **N/A** | Initial stable release | Not applicable | + +**Migration impact describes the expected developer effort and affected integration surface, not the size of the code change or the significance of the release.** A small change to a widely used authentication API may have a higher migration impact than a large internal refactor. + +> Individual preview and release candidate versions are not listed separately. Migration guidance for an in-progress release may change before its stable release. + +Security and reliability fixes may have an impact of **None** while still being strongly recommended. + +## Understanding Migration Impact + +| Impact | Meaning | Expected Action | +|---|---|---| +| **None** | No application code or configuration changes are required for standard integrations. | Update packages and run existing tests. | +| **Low** | Small, localized changes may be required, typically for specific integrations or customizations. | Review the migration guide and update affected code. | +| **Medium** | Changes affect commonly used APIs, core integration points, or multiple application areas. Even a small code change is classified as Medium when developers must update functionality they use directly and extensively. | Review the migration guide, update affected integrations, and perform targeted regression testing. | +| **High** | Significant architectural, API, persistence, or behavioral changes require a planned migration. | Review all migration steps, test thoroughly, and plan deployment carefully. | + +Migration impact is evaluated using both **the effort required and how directly the changes affect application developers**. + +A change does not qualify as Low simply because it requires only a few lines of code. Breaking changes to frequently used authentication, authorization, session management, or client integration APIs may be classified as Medium even when the implementation changes are relatively small. + +Applications using only unaffected functionality may require fewer changes than the stated impact level suggests. + +## How to Upgrade + +1. **Identify your current version.** Check the installed `CodeBeam.UltimateAuth.*` NuGet packages. +2. **Review every version between your current and target versions.** Migration requirements are cumulative when skipping releases. +3. **Follow the linked migration guides.** Apply any required API, configuration, or behavioral changes in version order. +4. **Update your UltimateAuth packages together.** Keep related packages on compatible versions. +5. **Build and test your application.** Verify login, logout, refresh, authorization, and session management flows relevant to your integration. + +For releases marked **None**, no dedicated migration guide is necessary. You should still review the release notes and run your application tests. + +## Available Migration Guides + +### [Migrating to v0.1.4](/docs/migration/v0.1.4) + +**Impact: Low** + +Introduces antiforgery protection for cookie-based authentication flows, updates browser client integration, and changes selected session resolution and browser bridge interfaces. + +Applications using the standard UltimateAuth browser client generally receive antiforgery handling automatically. + +Custom HTTP clients, manually implemented browser bridges, and custom session resolvers should review the guide. + +### [Migrating to v0.1.1](/docs/migration/v0.1.1) + +**Impact: Low** + +Introduces routing-level enforcement for `[UAuthAuthorize]` and updates the `UAuthComponentBase` lifecycle. + +Most applications using the built-in `UAuthApp` router require no changes. Applications with custom routers, component lifecycle overrides, or specialized session requirements should review the guide. + +## Release History + +For complete release notes, bug fixes, and security updates, visit the [UltimateAuth GitHub Releases](https://github.com/CodeBeamOrg/UltimateAuth/releases) page. + +Migration guides focus on changes developers must make to their applications. Release notes provide the complete history of changes in each version. + +--- + +**Upgrading across multiple versions?** Start with the earliest version after your currently installed release and review each migration entry through your target version. \ No newline at end of file diff --git a/docs/content/migration/v0.1.4.md b/docs/content/migration/v0.1.4.md new file mode 100644 index 00000000..c197caea --- /dev/null +++ b/docs/content/migration/v0.1.4.md @@ -0,0 +1,194 @@ +--- +title: Migrating to v0.1.4 +order: 3 +group: migration +--- + +# Migrating to v0.1.4 + +**Migration Impact: Low** + +UltimateAuth v0.1.4 introduces antiforgery protection for cookie-based authentication flows and updates several client and server integration interfaces. + +The release strengthens protection against Cross-Site Request Forgery (CSRF) while preserving the standard UltimateAuth developer experience. + +**Most applications using the built-in UltimateAuth browser client require no application code changes.** Antiforgery token handling is integrated into the client. + +Applications using custom HTTP requests, custom browser bridges, or custom session resolvers should review the changes below. + +## 1. Antiforgery Protection for Cookie-Based Authentication + +UltimateAuth now validates antiforgery tokens for protected operations when cookie-based authentication is applicable. + +This includes relevant authentication operations and authenticated state-changing endpoints. + +Requests that fail antiforgery validation are rejected with HTTP `400 Bad Request` and the following error: + +```json +{ + "error": "invalid_antiforgery_token" +} +``` + +### Using the built-in browser client + +The UltimateAuth browser client now supports retrieving and submitting antiforgery tokens automatically. + +If your application uses the standard UltimateAuth client integration, no additional antiforgery implementation is normally required. + +Make sure the server and client packages are updated together. + +### Using custom HTTP requests + +If your application calls protected UltimateAuth endpoints directly from a browser, you must obtain an antiforgery token and submit it with the request. + +The default token endpoint is: + +```http +GET /auth/antiforgery/token +``` + +The response contains: + +```json +{ + "requestToken": "...", + "headerName": "X-UAuth-CSRF", + "formFieldName": "__RequestVerificationToken" +} +``` + +The endpoint also issues an antiforgery cookie. + +For subsequent protected requests: + +1. Obtain a token from the configured antiforgery endpoint. +2. Preserve the associated antiforgery cookie. +3. Send the request token using the returned `headerName`. +4. Include the appropriate authentication credentials for the request. + +For example: + +```http +POST /auth/logout +X-UAuth-CSRF: +Cookie: +``` + +The request token and antiforgery cookie must belong to the same antiforgery context. + +**Do not hardcode or fabricate antiforgery tokens.** Always obtain them from the server. + +The default endpoint path assumes the standard `/auth` base path. Applications using custom routing or multi-tenancy must use their configured endpoint URL. + +### Custom server-side integrations + +Applications using custom HTTP clients or server-to-server integrations should review whether their requests use cookie-based authentication. + +Antiforgery requirements depend on the authentication transport and operation. They are not automatically equivalent for every bearer-token or header-based request. + +Do not disable antiforgery validation to resolve an integration failure. Update the affected request flow instead. + +## 2. ISessionIdResolver Is Now Asynchronous + +The `ISessionIdResolver` interface has changed to support asynchronous session resolution. + +If you implement this interface yourself, update the implementation. + +**Before (v0.1.3):** + +```csharp +public AuthSessionId? Resolve(HttpContext context) +{ + // Resolve the session identifier. +} +``` + +**After (v0.1.4):** + +```csharp +public ValueTask ResolveAsync(HttpContext context, CancellationToken ct = default) +{ + ct.ThrowIfCancellationRequested(); + + // Resolve the session identifier. + return ValueTask.FromResult(null); +} +``` + +Replace the placeholder return with your existing resolution logic. + +If your resolver performs asynchronous operations, use an `async ValueTask` implementation and await those operations. + +Update any direct calls from `Resolve(...)` to `ResolveAsync(...)` and await the result. + +Applications using the built-in session resolver do not need to make this change. + +## 3. Custom Browser Bridges + +The `IBrowserUAuthBridge` interface now includes: + +```csharp +ValueTask ConfigureAntiforgeryAsync(string tokenUrl, string? trustedOrigin = null); +``` + +This allows the UltimateAuth client bootstrapper to configure antiforgery handling for browser requests. + +If you provide a custom implementation of `IBrowserUAuthBridge`, implement this new method. + +Your implementation must configure the browser-side antiforgery integration using the supplied token endpoint and optional trusted origin. + +The exact implementation depends on your browser integration. + +**Do not add an empty implementation unless antiforgery handling is already provided by your custom bridge.** Doing so may leave protected browser requests without the required token. + +Applications using the built-in UltimateAuth browser bridge do not need to make this change. + +## 4. Antiforgery Endpoint Configuration + +v0.1.4 introduces a server-side antiforgery configuration section: + +```csharp +options.Antiforgery.TokenEndpoint = "/antiforgery/token"; +``` + +The client also exposes the corresponding endpoint setting: + +```csharp +options.Endpoints.AntiforgeryToken = "/antiforgery/token"; +``` + +The default endpoint path is `/antiforgery/token`, resolved under the configured UltimateAuth base path. + +No configuration change is required when using the defaults. + +If you customize the endpoint, ensure the server and client configurations match. + +## 5. Verify Your Application + +After upgrading to v0.1.4, verify the following flows where applicable: + +- Login and logout using the built-in browser client. +- Session refresh and validation. +- Authenticated account and session management operations. +- Custom requests to UltimateAuth endpoints. +- Custom session resolver implementations. +- Custom browser bridge implementations. +- External Hub and multi-tenant configurations. + +For applications using custom browser requests, verify that valid antiforgery tokens allow protected operations and missing or invalid tokens are rejected. + +## Migration Checklist + +- [ ] Update UltimateAuth packages to compatible v0.1.4 versions. +- [ ] Update custom `ISessionIdResolver` implementations, if any. +- [ ] Update custom `IBrowserUAuthBridge` implementations, if any. +- [ ] Add antiforgery token handling to custom browser HTTP requests, if applicable. +- [ ] Verify custom antiforgery endpoint configuration, if applicable. +- [ ] Run authentication, authorization, and session regression tests. + +--- + +**Using the standard UltimateAuth client and built-in session resolution?** No application code changes are normally required. Update your packages and verify your existing authentication flows. + +For the complete list of changes, see [UltimateAuth v0.1.4 Release](https://github.com/CodeBeamOrg/UltimateAuth/releases/tag/v0.1.4). diff --git a/docs/content/migration/v0.2.0.md b/docs/content/migration/v0.2.0.md new file mode 100644 index 00000000..08c132d9 --- /dev/null +++ b/docs/content/migration/v0.2.0.md @@ -0,0 +1,159 @@ +--- +title: Migrating to v0.2.0 +order: 4 +group: migration +--- + +# Migrating to v0.2.0 + +**Migration Impact: Medium — In Progress** + +**Latest published prerelease reviewed: v0.2.0-preview.2** + +This guide covers migration requirements for the upcoming UltimateAuth v0.2.0 release. + +The guide is provisional and will be updated as new preview and release candidate versions become available. The final migration requirements may change before the stable v0.2.0 release. + +## 1. Asynchronous Authorization Policies + +**Available since preview.1** + +Custom implementations of `IAccessPolicy`, `IAccessInvariant`, and `IAccessAuthority` should migrate to `DecideAsync`. + +**Before:** + +```csharp +public AccessDecision Decide(AccessContext context) +{ + return AccessDecision.Allow(); +} +``` + +**After:** + +```csharp +public ValueTask DecideAsync(AccessContext context, CancellationToken ct = default) +{ + return ValueTask.FromResult(AccessDecision.Allow()); +} +``` + +For `IAccessAuthority`, preserve the existing `runtimePolicies` parameter and add the cancellation token. + +Synchronous `Decide` methods are obsolete. Existing synchronous implementations retain compatibility through the interface defaults, but new implementations should use `DecideAsync`. + +If your application invokes an authority directly, switch to `DecideAsync` and await the result. + +## 2. Authorization Check Results + +**Available since preview.1** + +`CheckAsync` now returns an explicit authorization decision in the response body. + +If your application previously interpreted HTTP `403` as the result of a successful authorization evaluation that denied access, update that handling. + +```csharp +var result = await UAuthClient.Authorization.CheckAsync(request); + +if (result.IsSuccess && result.Value is { IsAllowed: true }) +{ + // Authorized +} +``` + +An HTTP-successful authorization check does not necessarily mean that access was granted. + +Inspect the returned authorization decision rather than relying solely on the HTTP status. + +## 3. Custom User Lifecycle Integrations + +**Available since preview.2** + +The `IUserLifecycleIntegration.OnUserCreatedAsync` signature now includes `UAuthClientProfile`. + +**Before:** + +```csharp +Task OnUserCreatedAsync( + TenantKey tenant, + UserKey userKey, + object request, + CancellationToken ct = default); +``` + +**After:** + +```csharp +Task OnUserCreatedAsync( + TenantKey tenant, + UserKey userKey, + object request, + UAuthClientProfile clientProfile, + CancellationToken ct = default); +``` + +Update custom implementations to accept the additional client profile parameter. + +Applications using built-in lifecycle integrations do not need to make this change. + +## 4. Identifier and Profile Limits + +**Available since preview.2** + +The reference Users domain now enforces additional identifier and profile safety limits. + +The default maximum is 10 active identifiers per user. Applications requiring a different capacity should configure `Identifiers.Behavior.MaxIdentifiersPerUser`. + +Existing profile creation and update operations must also respect the new field-length and metadata limits. + +If you use EF Core persistence: + +1. Review existing profile data for values exceeding the new limits. +2. Add or apply the appropriate EF Core database migration for your application's DbContext. +3. Verify that applications handling profile validation errors use the returned error codes. + +See [Profile Validation & Limits](/docs/plugin-domains/profile-validation-limits) for the supported limits. + +## 5. Password Policies and History + +**Available since preview.2** + +The reference password credential implementation now applies configurable password policies, with Standard as the default level. + +Review custom registration, password-change, and password-reset flows to ensure that they handle server-side password validation errors. + +Applications using persistent password-history storage must apply the required database migration before enabling password history. + +Password history is disabled by default (`RememberLast = 0`). + +See [Password Policy & History](/docs/security/password-policy) for configuration and persistence requirements. + +## 6. Rate Limiting + +**Available since preview.2** + +Built-in endpoint rate limiting is enabled by default when using: + +```csharp +app.UseUltimateAuthWithAspNetCore(); +``` + +If your application already registers its own rate limiting middleware, avoid installing a duplicate limiter. + +To retain application-owned rate limiting: + +```csharp +app.UseUltimateAuthWithAspNetCore(useRateLimiting: false); +``` + +Then configure your existing `UseRateLimiter()` middleware in the appropriate pipeline position. + +Applications using `UseUltimateAuth()` without the integrated ASP.NET Core helper do not enable UltimateAuth rate limiting by default. + +Review custom HTTP clients and authentication flows for HTTP `429 Too Many Requests` handling. + +See [Rate Limiting](/docs/security/rate-limiting) for middleware integration and defaults. + +--- + +This page will continue to evolve through the v0.2.0 preview and RC series and will become the stable migration guide when v0.2.0 is released. diff --git a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/docs-index.json b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/docs-index.json index 0a8a077b..e1c3c2ec 100644 --- a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/docs-index.json +++ b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/docs-index.json @@ -320,5 +320,19 @@ "Order": 2, "Group": "migration", "GroupOrder": 10 + }, + { + "Title": "Migrating to v0.1.4", + "Slug": "migration/v0.1.4", + "Order": 3, + "Group": "migration", + "GroupOrder": 10 + }, + { + "Title": "Migrating to v0.2.0", + "Slug": "migration/v0.2.0", + "Order": 4, + "Group": "migration", + "GroupOrder": 10 } ] \ No newline at end of file diff --git a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/migration/index.json b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/migration/index.json index c595f397..8f6b4d5a 100644 --- a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/migration/index.json +++ b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/migration/index.json @@ -1,14 +1,44 @@ { "Slug": "migration/index", "Title": "Migration Guides", - "Html": "\n\u003Cp\u003EVersion-specific instructions for upgrading UltimateAuth applications.\u003C/p\u003E\n\u003Cp\u003EMigration guides focus on breaking changes, behavior changes, and required application updates.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022available-guides\u0022\u003EAvailable Guides\u003C/h2\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ca href=\u0022/docs/migration/v0.1.1\u0022\u003EMigrating to v0.1.1\u003C/a\u003E \u2014 Blazor route authorization and component lifecycle changes.\u003C/li\u003E\n\u003C/ul\u003E\n", + "Html": "\n\u003Cp\u003EKeep your UltimateAuth applications up to date with clear, version-specific upgrade guidance.\u003C/p\u003E\n\u003Cp\u003EThis page provides an overview of stable UltimateAuth releases, their migration impact, and any actions required when upgrading.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EMost updates require little or no application code changes.\u003C/strong\u003E When an update requires attention, a dedicated migration guide explains what changed and how to adapt your application.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022version-migration-overview\u0022\u003EVersion Migration Overview\u003C/h2\u003E\n\u003Cp\u003EThe table below lists stable releases in descending order, with the newest version first.\u003C/p\u003E\n\u003Cp\u003EPreview and release candidate versions are excluded.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EVersion\u003C/th\u003E\n\u003Cth\u003EMigration Impact\u003C/th\u003E\n\u003Cth\u003EWhat\u0027s Changed\u003C/th\u003E\n\u003Cth\u003EMigration Guide\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003Ev0.2.0 (In Progress)\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Cstrong\u003EMedium (Provisional)\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003EAsync authorization APIs, authorization checks, and integration changes\u003C/td\u003E\n\u003Ctd\u003E\u003Ca href=\u0022/docs/migration/v0.2.0\u0022\u003EView Guide\u003C/a\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003Ev0.1.4\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Cstrong\u003ELow\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003ECSRF protection, antiforgery integration, and interface changes\u003C/td\u003E\n\u003Ctd\u003E\u003Ca href=\u0022/docs/migration/v0.1.4\u0022\u003EView Guide\u003C/a\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003Ev0.1.3\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Cstrong\u003ENone\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003ESession deletion, revocation, and refresh consistency fixes\u003C/td\u003E\n\u003Ctd\u003ENot required\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003Ev0.1.2\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Cstrong\u003ENone\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003EPassword change transaction and session revocation fixes\u003C/td\u003E\n\u003Ctd\u003ENot required\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003Ev0.1.1\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Cstrong\u003ELow\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003EBlazor route authorization and component lifecycle changes\u003C/td\u003E\n\u003Ctd\u003E\u003Ca href=\u0022/docs/migration/v0.1.1\u0022\u003EView Guide\u003C/a\u003E\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003Ev0.1.0\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003E\u003Cstrong\u003EN/A\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003EInitial stable release\u003C/td\u003E\n\u003Ctd\u003ENot applicable\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Cstrong\u003EMigration impact describes the expected developer effort and affected integration surface, not the size of the code change or the significance of the release.\u003C/strong\u003E A small change to a widely used authentication API may have a higher migration impact than a large internal refactor.\u003C/p\u003E\n\u003Cblockquote\u003E\n\u003Cp\u003EIndividual preview and release candidate versions are not listed separately. Migration guidance for an in-progress release may change before its stable release.\u003C/p\u003E\n\u003C/blockquote\u003E\n\u003Cp\u003ESecurity and reliability fixes may have an impact of \u003Cstrong\u003ENone\u003C/strong\u003E while still being strongly recommended.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022understanding-migration-impact\u0022\u003EUnderstanding Migration Impact\u003C/h2\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EImpact\u003C/th\u003E\n\u003Cth\u003EMeaning\u003C/th\u003E\n\u003Cth\u003EExpected Action\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003ENone\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003ENo application code or configuration changes are required for standard integrations.\u003C/td\u003E\n\u003Ctd\u003EUpdate packages and run existing tests.\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003ELow\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003ESmall, localized changes may be required, typically for specific integrations or customizations.\u003C/td\u003E\n\u003Ctd\u003EReview the migration guide and update affected code.\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003EMedium\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003EChanges affect commonly used APIs, core integration points, or multiple application areas. Even a small code change is classified as Medium when developers must update functionality they use directly and extensively.\u003C/td\u003E\n\u003Ctd\u003EReview the migration guide, update affected integrations, and perform targeted regression testing.\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Cstrong\u003EHigh\u003C/strong\u003E\u003C/td\u003E\n\u003Ctd\u003ESignificant architectural, API, persistence, or behavioral changes require a planned migration.\u003C/td\u003E\n\u003Ctd\u003EReview all migration steps, test thoroughly, and plan deployment carefully.\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EMigration impact is evaluated using both \u003Cstrong\u003Ethe effort required and how directly the changes affect application developers\u003C/strong\u003E.\u003C/p\u003E\n\u003Cp\u003EA change does not qualify as Low simply because it requires only a few lines of code. Breaking changes to frequently used authentication, authorization, session management, or client integration APIs may be classified as Medium even when the implementation changes are relatively small.\u003C/p\u003E\n\u003Cp\u003EApplications using only unaffected functionality may require fewer changes than the stated impact level suggests.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022how-to-upgrade\u0022\u003EHow to Upgrade\u003C/h2\u003E\n\u003Col\u003E\n\u003Cli\u003E\u003Cstrong\u003EIdentify your current version.\u003C/strong\u003E Check the installed \u003Ccode\u003ECodeBeam.UltimateAuth.*\u003C/code\u003E NuGet packages.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003EReview every version between your current and target versions.\u003C/strong\u003E Migration requirements are cumulative when skipping releases.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003EFollow the linked migration guides.\u003C/strong\u003E Apply any required API, configuration, or behavioral changes in version order.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003EUpdate your UltimateAuth packages together.\u003C/strong\u003E Keep related packages on compatible versions.\u003C/li\u003E\n\u003Cli\u003E\u003Cstrong\u003EBuild and test your application.\u003C/strong\u003E Verify login, logout, refresh, authorization, and session management flows relevant to your integration.\u003C/li\u003E\n\u003C/ol\u003E\n\u003Cp\u003EFor releases marked \u003Cstrong\u003ENone\u003C/strong\u003E, no dedicated migration guide is necessary. You should still review the release notes and run your application tests.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022available-migration-guides\u0022\u003EAvailable Migration Guides\u003C/h2\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022migrating-to-v0.1.4\u0022\u003E\u003Ca href=\u0022/docs/migration/v0.1.4\u0022\u003EMigrating to v0.1.4\u003C/a\u003E\u003C/h3\u003E\n\u003Cp\u003E\u003Cstrong\u003EImpact: Low\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EIntroduces antiforgery protection for cookie-based authentication flows, updates browser client integration, and changes selected session resolution and browser bridge interfaces.\u003C/p\u003E\n\u003Cp\u003EApplications using the standard UltimateAuth browser client generally receive antiforgery handling automatically.\u003C/p\u003E\n\u003Cp\u003ECustom HTTP clients, manually implemented browser bridges, and custom session resolvers should review the guide.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022migrating-to-v0.1.1\u0022\u003E\u003Ca href=\u0022/docs/migration/v0.1.1\u0022\u003EMigrating to v0.1.1\u003C/a\u003E\u003C/h3\u003E\n\u003Cp\u003E\u003Cstrong\u003EImpact: Low\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EIntroduces routing-level enforcement for \u003Ccode\u003E[UAuthAuthorize]\u003C/code\u003E and updates the \u003Ccode\u003EUAuthComponentBase\u003C/code\u003E lifecycle.\u003C/p\u003E\n\u003Cp\u003EMost applications using the built-in \u003Ccode\u003EUAuthApp\u003C/code\u003E router require no changes. Applications with custom routers, component lifecycle overrides, or specialized session requirements should review the guide.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022release-history\u0022\u003ERelease History\u003C/h2\u003E\n\u003Cp\u003EFor complete release notes, bug fixes, and security updates, visit the \u003Ca href=\u0022https://github.com/CodeBeamOrg/UltimateAuth/releases\u0022\u003EUltimateAuth GitHub Releases\u003C/a\u003E page.\u003C/p\u003E\n\u003Cp\u003EMigration guides focus on changes developers must make to their applications. Release notes provide the complete history of changes in each version.\u003C/p\u003E\n\u003Chr\u003E\n\u003Cp\u003E\u003Cstrong\u003EUpgrading across multiple versions?\u003C/strong\u003E Start with the earliest version after your currently installed release and review each migration entry through your target version.\u003C/p\u003E\n", "Headings": [ { - "Id": "available-guides", - "Text": "Available Guides", + "Id": "version-migration-overview", + "Text": "Version Migration Overview", + "Level": 0 + }, + { + "Id": "understanding-migration-impact", + "Text": "Understanding Migration Impact", + "Level": 0 + }, + { + "Id": "how-to-upgrade", + "Text": "How to Upgrade", + "Level": 0 + }, + { + "Id": "available-migration-guides", + "Text": "Available Migration Guides", + "Level": 0 + }, + { + "Id": "migrating-to-v0.1.4", + "Text": "Migrating to v0.1.4", + "Level": 1 + }, + { + "Id": "migrating-to-v0.1.1", + "Text": "Migrating to v0.1.1", + "Level": 1 + }, + { + "Id": "release-history", + "Text": "Release History", "Level": 0 } ], - "SourceHash": "FA6FAFE02BA9EF2656EBA1C8531CEC3F43A2666D05A61B6E2979BD96B3593D8B", + "SourceHash": "DD4CBD449E209E19EED5EE80BEE6DEA683168AF0485EA7C6CF76F5D80A8A3B76", "BuilderVersion": "1" } \ No newline at end of file diff --git a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/migration/v0.1.4.json b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/migration/v0.1.4.json new file mode 100644 index 00000000..fefe0679 --- /dev/null +++ b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/migration/v0.1.4.json @@ -0,0 +1,54 @@ +{ + "Slug": "migration/v0.1.4", + "Title": "Migrating to v0.1.4", + "Html": "\n\u003Cp\u003E\u003Cstrong\u003EMigration Impact: Low\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EUltimateAuth v0.1.4 introduces antiforgery protection for cookie-based authentication flows and updates several client and server integration interfaces.\u003C/p\u003E\n\u003Cp\u003EThe release strengthens protection against Cross-Site Request Forgery (CSRF) while preserving the standard UltimateAuth developer experience.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EMost applications using the built-in UltimateAuth browser client require no application code changes.\u003C/strong\u003E Antiforgery token handling is integrated into the client.\u003C/p\u003E\n\u003Cp\u003EApplications using custom HTTP requests, custom browser bridges, or custom session resolvers should review the changes below.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022antiforgery-protection-for-cookie-based-authentication\u0022\u003E1. Antiforgery Protection for Cookie-Based Authentication\u003C/h2\u003E\n\u003Cp\u003EUltimateAuth now validates antiforgery tokens for protected operations when cookie-based authentication is applicable.\u003C/p\u003E\n\u003Cp\u003EThis includes relevant authentication operations and authenticated state-changing endpoints.\u003C/p\u003E\n\u003Cp\u003ERequests that fail antiforgery validation are rejected with HTTP \u003Ccode\u003E400 Bad Request\u003C/code\u003E and the following error:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EJSON\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-json\u0022\u003E{\n \u0026quot;error\u0026quot;: \u0026quot;invalid_antiforgery_token\u0026quot;\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022using-the-built-in-browser-client\u0022\u003EUsing the built-in browser client\u003C/h3\u003E\n\u003Cp\u003EThe UltimateAuth browser client now supports retrieving and submitting antiforgery tokens automatically.\u003C/p\u003E\n\u003Cp\u003EIf your application uses the standard UltimateAuth client integration, no additional antiforgery implementation is normally required.\u003C/p\u003E\n\u003Cp\u003EMake sure the server and client packages are updated together.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022using-custom-http-requests\u0022\u003EUsing custom HTTP requests\u003C/h3\u003E\n\u003Cp\u003EIf your application calls protected UltimateAuth endpoints directly from a browser, you must obtain an antiforgery token and submit it with the request.\u003C/p\u003E\n\u003Cp\u003EThe default token endpoint is:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EHTTP\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-http\u0022\u003EGET /auth/antiforgery/token\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe response contains:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EJSON\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-json\u0022\u003E{\n \u0026quot;requestToken\u0026quot;: \u0026quot;...\u0026quot;,\n \u0026quot;headerName\u0026quot;: \u0026quot;X-UAuth-CSRF\u0026quot;,\n \u0026quot;formFieldName\u0026quot;: \u0026quot;__RequestVerificationToken\u0026quot;\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe endpoint also issues an antiforgery cookie.\u003C/p\u003E\n\u003Cp\u003EFor subsequent protected requests:\u003C/p\u003E\n\u003Col\u003E\n\u003Cli\u003EObtain a token from the configured antiforgery endpoint.\u003C/li\u003E\n\u003Cli\u003EPreserve the associated antiforgery cookie.\u003C/li\u003E\n\u003Cli\u003ESend the request token using the returned \u003Ccode\u003EheaderName\u003C/code\u003E.\u003C/li\u003E\n\u003Cli\u003EInclude the appropriate authentication credentials for the request.\u003C/li\u003E\n\u003C/ol\u003E\n\u003Cp\u003EFor example:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EHTTP\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-http\u0022\u003EPOST /auth/logout\nX-UAuth-CSRF: \u0026lt;request-token\u0026gt;\nCookie: \u0026lt;authentication-and-antiforgery-cookies\u0026gt;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe request token and antiforgery cookie must belong to the same antiforgery context.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EDo not hardcode or fabricate antiforgery tokens.\u003C/strong\u003E Always obtain them from the server.\u003C/p\u003E\n\u003Cp\u003EThe default endpoint path assumes the standard \u003Ccode\u003E/auth\u003C/code\u003E base path. Applications using custom routing or multi-tenancy must use their configured endpoint URL.\u003C/p\u003E\n\u003Ch3 class=\u0022mud-scrollspy-section\u0022 id=\u0022custom-server-side-integrations\u0022\u003ECustom server-side integrations\u003C/h3\u003E\n\u003Cp\u003EApplications using custom HTTP clients or server-to-server integrations should review whether their requests use cookie-based authentication.\u003C/p\u003E\n\u003Cp\u003EAntiforgery requirements depend on the authentication transport and operation. They are not automatically equivalent for every bearer-token or header-based request.\u003C/p\u003E\n\u003Cp\u003EDo not disable antiforgery validation to resolve an integration failure. Update the affected request flow instead.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022isessionidresolver-is-now-asynchronous\u0022\u003E2. ISessionIdResolver Is Now Asynchronous\u003C/h2\u003E\n\u003Cp\u003EThe \u003Ccode\u003EISessionIdResolver\u003C/code\u003E interface has changed to support asynchronous session resolution.\u003C/p\u003E\n\u003Cp\u003EIf you implement this interface yourself, update the implementation.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EBefore (v0.1.3):\u003C/strong\u003E\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Epublic AuthSessionId? Resolve(HttpContext context)\n{\n // Resolve the session identifier.\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Cstrong\u003EAfter (v0.1.4):\u003C/strong\u003E\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Epublic ValueTask\u0026lt;AuthSessionId?\u0026gt; ResolveAsync(HttpContext context, CancellationToken ct = default)\n{\n ct.ThrowIfCancellationRequested();\n\n // Resolve the session identifier.\n return ValueTask.FromResult\u0026lt;AuthSessionId?\u0026gt;(null);\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EReplace the placeholder return with your existing resolution logic.\u003C/p\u003E\n\u003Cp\u003EIf your resolver performs asynchronous operations, use an \u003Ccode\u003Easync ValueTask\u0026lt;AuthSessionId?\u0026gt;\u003C/code\u003E implementation and await those operations.\u003C/p\u003E\n\u003Cp\u003EUpdate any direct calls from \u003Ccode\u003EResolve(...)\u003C/code\u003E to \u003Ccode\u003EResolveAsync(...)\u003C/code\u003E and await the result.\u003C/p\u003E\n\u003Cp\u003EApplications using the built-in session resolver do not need to make this change.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022custom-browser-bridges\u0022\u003E3. Custom Browser Bridges\u003C/h2\u003E\n\u003Cp\u003EThe \u003Ccode\u003EIBrowserUAuthBridge\u003C/code\u003E interface now includes:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003EValueTask ConfigureAntiforgeryAsync(string tokenUrl, string? trustedOrigin = null);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThis allows the UltimateAuth client bootstrapper to configure antiforgery handling for browser requests.\u003C/p\u003E\n\u003Cp\u003EIf you provide a custom implementation of \u003Ccode\u003EIBrowserUAuthBridge\u003C/code\u003E, implement this new method.\u003C/p\u003E\n\u003Cp\u003EYour implementation must configure the browser-side antiforgery integration using the supplied token endpoint and optional trusted origin.\u003C/p\u003E\n\u003Cp\u003EThe exact implementation depends on your browser integration.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EDo not add an empty implementation unless antiforgery handling is already provided by your custom bridge.\u003C/strong\u003E Doing so may leave protected browser requests without the required token.\u003C/p\u003E\n\u003Cp\u003EApplications using the built-in UltimateAuth browser bridge do not need to make this change.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022antiforgery-endpoint-configuration\u0022\u003E4. Antiforgery Endpoint Configuration\u003C/h2\u003E\n\u003Cp\u003Ev0.1.4 introduces a server-side antiforgery configuration section:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eoptions.Antiforgery.TokenEndpoint = \u0026quot;/antiforgery/token\u0026quot;;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe client also exposes the corresponding endpoint setting:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eoptions.Endpoints.AntiforgeryToken = \u0026quot;/antiforgery/token\u0026quot;;\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThe default endpoint path is \u003Ccode\u003E/antiforgery/token\u003C/code\u003E, resolved under the configured UltimateAuth base path.\u003C/p\u003E\n\u003Cp\u003ENo configuration change is required when using the defaults.\u003C/p\u003E\n\u003Cp\u003EIf you customize the endpoint, ensure the server and client configurations match.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022verify-your-application\u0022\u003E5. Verify Your Application\u003C/h2\u003E\n\u003Cp\u003EAfter upgrading to v0.1.4, verify the following flows where applicable:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003ELogin and logout using the built-in browser client.\u003C/li\u003E\n\u003Cli\u003ESession refresh and validation.\u003C/li\u003E\n\u003Cli\u003EAuthenticated account and session management operations.\u003C/li\u003E\n\u003Cli\u003ECustom requests to UltimateAuth endpoints.\u003C/li\u003E\n\u003Cli\u003ECustom session resolver implementations.\u003C/li\u003E\n\u003Cli\u003ECustom browser bridge implementations.\u003C/li\u003E\n\u003Cli\u003EExternal Hub and multi-tenant configurations.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EFor applications using custom browser requests, verify that valid antiforgery tokens allow protected operations and missing or invalid tokens are rejected.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022migration-checklist\u0022\u003EMigration Checklist\u003C/h2\u003E\n\u003Cul class=\u0022contains-task-list\u0022\u003E\n\u003Cli class=\u0022task-list-item\u0022\u003E\u003Cinput disabled=\u0022disabled\u0022 type=\u0022checkbox\u0022\u003E Update UltimateAuth packages to compatible v0.1.4 versions.\u003C/li\u003E\n\u003Cli class=\u0022task-list-item\u0022\u003E\u003Cinput disabled=\u0022disabled\u0022 type=\u0022checkbox\u0022\u003E Update custom \u003Ccode\u003EISessionIdResolver\u003C/code\u003E implementations, if any.\u003C/li\u003E\n\u003Cli class=\u0022task-list-item\u0022\u003E\u003Cinput disabled=\u0022disabled\u0022 type=\u0022checkbox\u0022\u003E Update custom \u003Ccode\u003EIBrowserUAuthBridge\u003C/code\u003E implementations, if any.\u003C/li\u003E\n\u003Cli class=\u0022task-list-item\u0022\u003E\u003Cinput disabled=\u0022disabled\u0022 type=\u0022checkbox\u0022\u003E Add antiforgery token handling to custom browser HTTP requests, if applicable.\u003C/li\u003E\n\u003Cli class=\u0022task-list-item\u0022\u003E\u003Cinput disabled=\u0022disabled\u0022 type=\u0022checkbox\u0022\u003E Verify custom antiforgery endpoint configuration, if applicable.\u003C/li\u003E\n\u003Cli class=\u0022task-list-item\u0022\u003E\u003Cinput disabled=\u0022disabled\u0022 type=\u0022checkbox\u0022\u003E Run authentication, authorization, and session regression tests.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Chr\u003E\n\u003Cp\u003E\u003Cstrong\u003EUsing the standard UltimateAuth client and built-in session resolution?\u003C/strong\u003E No application code changes are normally required. Update your packages and verify your existing authentication flows.\u003C/p\u003E\n\u003Cp\u003EFor the complete list of changes, see \u003Ca href=\u0022https://github.com/CodeBeamOrg/UltimateAuth/releases/tag/v0.1.4\u0022\u003EUltimateAuth v0.1.4 Release\u003C/a\u003E.\u003C/p\u003E\n", + "Headings": [ + { + "Id": "antiforgery-protection-for-cookie-based-authentication", + "Text": "1. Antiforgery Protection for Cookie-Based Authentication", + "Level": 0 + }, + { + "Id": "using-the-built-in-browser-client", + "Text": "Using the built-in browser client", + "Level": 1 + }, + { + "Id": "using-custom-http-requests", + "Text": "Using custom HTTP requests", + "Level": 1 + }, + { + "Id": "custom-server-side-integrations", + "Text": "Custom server-side integrations", + "Level": 1 + }, + { + "Id": "isessionidresolver-is-now-asynchronous", + "Text": "2. ISessionIdResolver Is Now Asynchronous", + "Level": 0 + }, + { + "Id": "custom-browser-bridges", + "Text": "3. Custom Browser Bridges", + "Level": 0 + }, + { + "Id": "antiforgery-endpoint-configuration", + "Text": "4. Antiforgery Endpoint Configuration", + "Level": 0 + }, + { + "Id": "verify-your-application", + "Text": "5. Verify Your Application", + "Level": 0 + }, + { + "Id": "migration-checklist", + "Text": "Migration Checklist", + "Level": 0 + } + ], + "SourceHash": "F8FA4FDA0E64B56BADDBA75519538C1ACBFDF88567EF487D30011FAB944B67CC", + "BuilderVersion": "1" +} \ No newline at end of file diff --git a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/migration/v0.2.0.json b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/migration/v0.2.0.json new file mode 100644 index 00000000..a0206e85 --- /dev/null +++ b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/migration/v0.2.0.json @@ -0,0 +1,39 @@ +{ + "Slug": "migration/v0.2.0", + "Title": "Migrating to v0.2.0", + "Html": "\n\u003Cp\u003E\u003Cstrong\u003EMigration Impact: Medium \u2014 In Progress\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003ELatest published prerelease reviewed: v0.2.0-preview.2\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EThis guide covers migration requirements for the upcoming UltimateAuth v0.2.0 release.\u003C/p\u003E\n\u003Cp\u003EThe guide is provisional and will be updated as new preview and release candidate versions become available. The final migration requirements may change before the stable v0.2.0 release.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022asynchronous-authorization-policies\u0022\u003E1. Asynchronous Authorization Policies\u003C/h2\u003E\n\u003Cp\u003E\u003Cstrong\u003EAvailable since preview.1\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003ECustom implementations of \u003Ccode\u003EIAccessPolicy\u003C/code\u003E, \u003Ccode\u003EIAccessInvariant\u003C/code\u003E, and \u003Ccode\u003EIAccessAuthority\u003C/code\u003E should migrate to \u003Ccode\u003EDecideAsync\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EBefore:\u003C/strong\u003E\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Epublic AccessDecision Decide(AccessContext context)\n{\n return AccessDecision.Allow();\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Cstrong\u003EAfter:\u003C/strong\u003E\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Epublic ValueTask\u0026lt;AccessDecision\u0026gt; DecideAsync(AccessContext context, CancellationToken ct = default)\n{\n return ValueTask.FromResult(AccessDecision.Allow());\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EFor \u003Ccode\u003EIAccessAuthority\u003C/code\u003E, preserve the existing \u003Ccode\u003EruntimePolicies\u003C/code\u003E parameter and add the cancellation token.\u003C/p\u003E\n\u003Cp\u003ESynchronous \u003Ccode\u003EDecide\u003C/code\u003E methods are obsolete. Existing synchronous implementations retain compatibility through the interface defaults, but new implementations should use \u003Ccode\u003EDecideAsync\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EIf your application invokes an authority directly, switch to \u003Ccode\u003EDecideAsync\u003C/code\u003E and await the result.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022authorization-check-results\u0022\u003E2. Authorization Check Results\u003C/h2\u003E\n\u003Cp\u003E\u003Cstrong\u003EAvailable since preview.1\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003E\u003Ccode\u003ECheckAsync\u003C/code\u003E now returns an explicit authorization decision in the response body.\u003C/p\u003E\n\u003Cp\u003EIf your application previously interpreted HTTP \u003Ccode\u003E403\u003C/code\u003E as the result of a successful authorization evaluation that denied access, update that handling.\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Evar result = await UAuthClient.Authorization.CheckAsync(request);\n\nif (result.IsSuccess \u0026amp;\u0026amp; result.Value is { IsAllowed: true })\n{\n // Authorized\n}\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EAn HTTP-successful authorization check does not necessarily mean that access was granted.\u003C/p\u003E\n\u003Cp\u003EInspect the returned authorization decision rather than relying solely on the HTTP status.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022custom-user-lifecycle-integrations\u0022\u003E3. Custom User Lifecycle Integrations\u003C/h2\u003E\n\u003Cp\u003E\u003Cstrong\u003EAvailable since preview.2\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EThe \u003Ccode\u003EIUserLifecycleIntegration.OnUserCreatedAsync\u003C/code\u003E signature now includes \u003Ccode\u003EUAuthClientProfile\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003E\u003Cstrong\u003EBefore:\u003C/strong\u003E\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003ETask OnUserCreatedAsync(\n TenantKey tenant,\n UserKey userKey,\n object request,\n CancellationToken ct = default);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003E\u003Cstrong\u003EAfter:\u003C/strong\u003E\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003ETask OnUserCreatedAsync(\n TenantKey tenant,\n UserKey userKey,\n object request,\n UAuthClientProfile clientProfile,\n CancellationToken ct = default);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EUpdate custom implementations to accept the additional client profile parameter.\u003C/p\u003E\n\u003Cp\u003EApplications using built-in lifecycle integrations do not need to make this change.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022identifier-and-profile-limits\u0022\u003E4. Identifier and Profile Limits\u003C/h2\u003E\n\u003Cp\u003E\u003Cstrong\u003EAvailable since preview.2\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EThe reference Users domain now enforces additional identifier and profile safety limits.\u003C/p\u003E\n\u003Cp\u003EThe default maximum is 10 active identifiers per user. Applications requiring a different capacity should configure \u003Ccode\u003EIdentifiers.Behavior.MaxIdentifiersPerUser\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EExisting profile creation and update operations must also respect the new field-length and metadata limits.\u003C/p\u003E\n\u003Cp\u003EIf you use EF Core persistence:\u003C/p\u003E\n\u003Col\u003E\n\u003Cli\u003EReview existing profile data for values exceeding the new limits.\u003C/li\u003E\n\u003Cli\u003EAdd or apply the appropriate EF Core database migration for your application\u0027s DbContext.\u003C/li\u003E\n\u003Cli\u003EVerify that applications handling profile validation errors use the returned error codes.\u003C/li\u003E\n\u003C/ol\u003E\n\u003Cp\u003ESee \u003Ca href=\u0022/docs/plugin-domains/profile-validation-limits\u0022\u003EProfile Validation \u0026amp; Limits\u003C/a\u003E for the supported limits.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022password-policies-and-history\u0022\u003E5. Password Policies and History\u003C/h2\u003E\n\u003Cp\u003E\u003Cstrong\u003EAvailable since preview.2\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EThe reference password credential implementation now applies configurable password policies, with Standard as the default level.\u003C/p\u003E\n\u003Cp\u003EReview custom registration, password-change, and password-reset flows to ensure that they handle server-side password validation errors.\u003C/p\u003E\n\u003Cp\u003EApplications using persistent password-history storage must apply the required database migration before enabling password history.\u003C/p\u003E\n\u003Cp\u003EPassword history is disabled by default (\u003Ccode\u003ERememberLast = 0\u003C/code\u003E).\u003C/p\u003E\n\u003Cp\u003ESee \u003Ca href=\u0022/docs/security/password-policy\u0022\u003EPassword Policy \u0026amp; History\u003C/a\u003E for configuration and persistence requirements.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022rate-limiting\u0022\u003E6. Rate Limiting\u003C/h2\u003E\n\u003Cp\u003E\u003Cstrong\u003EAvailable since preview.2\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003EBuilt-in endpoint rate limiting is enabled by default when using:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eapp.UseUltimateAuthWithAspNetCore();\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EIf your application already registers its own rate limiting middleware, avoid installing a duplicate limiter.\u003C/p\u003E\n\u003Cp\u003ETo retain application-owned rate limiting:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-code-wrapper\u0022\u003E\u003Cdiv class=\u0022docs-code-header\u0022\u003E\u003Cspan class=\u0022docs-code-language\u0022\u003EC#\u003C/span\u003E\u003Cbutton type=\u0022button\u0022 class=\u0022docs-code-copy\u0022 aria-label=\u0022Copy code\u0022\u003ECopy\u003C/button\u003E\u003C/div\u003E\u003Cpre\u003E\u003Ccode class=\u0022language-csharp\u0022\u003Eapp.UseUltimateAuthWithAspNetCore(useRateLimiting: false);\n\u003C/code\u003E\u003C/pre\u003E\u003C/div\u003E\n\u003Cp\u003EThen configure your existing \u003Ccode\u003EUseRateLimiter()\u003C/code\u003E middleware in the appropriate pipeline position.\u003C/p\u003E\n\u003Cp\u003EApplications using \u003Ccode\u003EUseUltimateAuth()\u003C/code\u003E without the integrated ASP.NET Core helper do not enable UltimateAuth rate limiting by default.\u003C/p\u003E\n\u003Cp\u003EReview custom HTTP clients and authentication flows for HTTP \u003Ccode\u003E429 Too Many Requests\u003C/code\u003E handling.\u003C/p\u003E\n\u003Cp\u003ESee \u003Ca href=\u0022/docs/security/rate-limiting\u0022\u003ERate Limiting\u003C/a\u003E for middleware integration and defaults.\u003C/p\u003E\n\u003Chr\u003E\n\u003Cp\u003EThis page will continue to evolve through the v0.2.0 preview and RC series and will become the stable migration guide when v0.2.0 is released.\u003C/p\u003E\n", + "Headings": [ + { + "Id": "asynchronous-authorization-policies", + "Text": "1. Asynchronous Authorization Policies", + "Level": 0 + }, + { + "Id": "authorization-check-results", + "Text": "2. Authorization Check Results", + "Level": 0 + }, + { + "Id": "custom-user-lifecycle-integrations", + "Text": "3. Custom User Lifecycle Integrations", + "Level": 0 + }, + { + "Id": "identifier-and-profile-limits", + "Text": "4. Identifier and Profile Limits", + "Level": 0 + }, + { + "Id": "password-policies-and-history", + "Text": "5. Password Policies and History", + "Level": 0 + }, + { + "Id": "rate-limiting", + "Text": "6. Rate Limiting", + "Level": 0 + } + ], + "SourceHash": "99DF0B4E30A8578E3B2E809B38606FC0041D6A54F05EECDC98D95E8FD78DCBBA", + "BuilderVersion": "1" +} \ No newline at end of file diff --git a/src/client/CodeBeam.UltimateAuth.Client.Blazor/Components/UAuthLoginForm.razor.cs b/src/client/CodeBeam.UltimateAuth.Client.Blazor/Components/UAuthLoginForm.razor.cs index 7ff71a56..d9c54663 100644 --- a/src/client/CodeBeam.UltimateAuth.Client.Blazor/Components/UAuthLoginForm.razor.cs +++ b/src/client/CodeBeam.UltimateAuth.Client.Blazor/Components/UAuthLoginForm.razor.cs @@ -95,6 +95,13 @@ public partial class UAuthLoginForm [Parameter] public EventCallback OnTryResult { get; set; } + private bool _isSubmitting; + + [Parameter] + public EventCallback OnSubmittingChanged { get; set; } + + public bool IsSubmitting => _isSubmitting; + private ElementReference _form; private HubCredentials? _credentials; @@ -174,16 +181,31 @@ public async Task ReloadAsync() /// Thrown if the form has not been rendered. Call this method only after the OnAfterRender lifecycle event. public async Task SubmitAsync() { + if (_isSubmitting) + return; + if (_form.Context is null) throw new InvalidOperationException("Form is not yet rendered. Call SubmitAsync after OnAfterRender."); - if (LoginType == UAuthLoginType.Pkce) + _isSubmitting = true; + + try { - await SubmitPkceAsync(); + await OnSubmittingChanged.InvokeAsync(true); + + if (LoginType == UAuthLoginType.Pkce) + { + await SubmitPkceAsync(); + } + else + { + await SubmitPasswordAsync(); + } } - else + finally { - await SubmitPasswordAsync(); + _isSubmitting = false; + await OnSubmittingChanged.InvokeAsync(false); } } From 40d32a9cd8b82c196a24f9f11e325984bb31e737 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Can=20Karag=C3=B6z?= Date: Sat, 7 Nov 2026 21:00:48 +0300 Subject: [PATCH 11/11] Update 0.2.0-preview.2 --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 941fc4da..742ac4ab 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -6,7 +6,7 @@ $(UltimateAuthTargetFrameworks) - 0.2.0-preview.1 + 0.2.0-preview.2 $(NoWarn);1591 CodeBeam