Implement passkey authentication and enhance security features#31
Implement passkey authentication and enhance security features#31
Conversation
- Added login page structure with navigation and buttons for login and registration. - Enhanced PasskeyEndpoints to handle passkey registration and login with improved error handling and tenant isolation. - Updated ApplicationServicesExtensions to configure passkey options including user verification and attestation preferences. - Modified Login.razor and Register.razor to support passkey login and registration, including validation and error handling. - Implemented ManagePasskeys.razor to manage user passkeys with proper credential handling. - Introduced PasskeyService to manage passkey creation and login options, returning structured payloads. - Updated JavaScript functions for WebAuthn to handle passkey creation and login with improved compatibility. - Added unit tests for passkey tenant isolation and validation of passkey operations.
- Implemented critical security checks for cross-tenant token theft and cloned authenticators in JwtAuthenticationEndpoints and PasskeyEndpoints. - Added logging for security violations including cross-tenant token theft and passkey counter mismatches. - Updated JWT token service to include security stamp in tokens and validate it on each request. - Introduced new error codes for security violations and passkey counter mismatches. - Created integration tests for passkey security features, including account lockout on cloned authenticator detection and token invalidation on security stamp changes. - Refactored passkey-related test helpers for better code reuse and clarity. - Ensured that refresh tokens are cleared upon passkey login to enhance security.
…d conflict detection
There was a problem hiding this comment.
Pull request overview
Adds and hardens passkey (WebAuthn) flows across the API, Blazor web app, and integration tests, including tenant isolation, token revocation via security-stamp validation, and security-violation handling.
Changes:
- Implement/adjust passkey registration + login flows, including discoverable-credential login options and frontend JSON handling.
- Add security controls: tenant validation, passkey counter mismatch handling, cross-tenant refresh-token theft detection, and security-stamp claim/validation.
- Add/expand integration tests for tenant isolation and security behaviors; refactor test helpers for Marten setup and passkey manipulation.
Reviewed changes
Copilot reviewed 23 out of 23 changed files in this pull request and generated 14 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/BookStore.AppHost.Tests/TestHelpers.cs | Adds Marten store/session helpers and register+login helper for tests. |
| tests/BookStore.AppHost.Tests/PasswordManagementTests.cs | Adjusts password removal verification to query DB directly (token invalidation). |
| tests/BookStore.AppHost.Tests/PasskeyTests.cs | Updates expectation for assertion-options endpoint to always return options (anti-enumeration). |
| tests/BookStore.AppHost.Tests/PasskeyTestHelpers.cs | New shared helpers for adding/updating passkeys in DB for integration tests. |
| tests/BookStore.AppHost.Tests/PasskeyTenantIsolationTests.cs | New tenant-scoping tests for passkey list/delete and creation options. |
| tests/BookStore.AppHost.Tests/PasskeySecurityTests.cs | New tests for security-stamp revocation, token theft detection, refresh-token clearing, sign-count scenarios. |
| tests/BookStore.AppHost.Tests/PasskeyRegistrationSecurityTests.cs | New tests for concurrent registration + user-id conflict masking. |
| tests/BookStore.AppHost.Tests/PasskeyDeletionTests.cs | Refactors DB setup using new helpers; verifies unsafe-id deletion. |
| src/BookStore.Web/wwwroot/js/passkeys.js | Uses PublicKeyCredential.parse*FromJSON when available; improves wrapped-response handling and transports capture. |
| src/BookStore.Web/Services/PasskeyService.cs | Returns typed payload for creation options (options JSON + userId), normalizes login options JSON serialization. |
| src/BookStore.Web/Program.cs | Custom DI for IPasskeyClient with cookie persistence for ceremony state. |
| src/BookStore.Web/Components/Pages/Register.razor | Switches to typed creation-options payload (no manual JSON parsing). |
| src/BookStore.Web/Components/Pages/ManagePasskeys.razor | Switches to typed creation-options payload. |
| src/BookStore.Web/Components/Pages/Login.razor | Passkey login UX tweaks (optional email, better aria label, error handling). |
| src/BookStore.Shared/Models/ErrorCodes.cs | Adds security-violation and counter-mismatch error codes. |
| src/BookStore.ApiService/Services/JwtTokenService.cs | Adds security_stamp claim to access tokens. |
| src/BookStore.ApiService/Infrastructure/Logging/Log.Users.cs | Adds structured logs for passkey and cross-tenant security events. |
| src/BookStore.ApiService/Infrastructure/Identity/MartenUserStore.cs | Refactors some lookups; adds (currently ineffective) tenant-isolation asserts. |
| src/BookStore.ApiService/Infrastructure/Extensions/ApplicationServicesExtensions.cs | Tightens passkey options, origin validation, JWT secret validation, and adds security-stamp validation hook. |
| src/BookStore.ApiService/Endpoints/PasskeyEndpoints.cs | Adds tenant validation, conflict detection before attestation, counter checks, refresh-token clearing, user-profile stream initialization. |
| src/BookStore.ApiService/Endpoints/JwtAuthenticationEndpoints.cs | Locks account + clears tokens on cross-tenant refresh-token usage. |
| login-page.md | Adds captured login-page accessibility snapshot/output. |
| AGENTS.md | Updates repo structure notes + requires dotnet format. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Defense-in-depth: Validate tenant isolation | ||
| // Marten's session should already filter by tenant_id, but verify for security | ||
| if (user != null) | ||
| { | ||
| System.Diagnostics.Debug.Assert( | ||
| _session.TenantId == _session.TenantId, | ||
| "Tenant isolation violation detected in FindByEmailAsync"); | ||
| } |
There was a problem hiding this comment.
This assert has the same issue (_session.TenantId == _session.TenantId): it can never fail and will emit a warning that breaks the build. Remove it or implement a real tenant-isolation check.
| // Defense-in-depth: Validate tenant isolation | |
| // Marten's session should already filter by tenant_id, but verify for security | |
| if (user != null) | |
| { | |
| System.Diagnostics.Debug.Assert( | |
| _session.TenantId == _session.TenantId, | |
| "Tenant isolation violation detected in FindByEmailAsync"); | |
| } | |
| // Defense-in-depth: tenant isolation is enforced by Marten's session tenant filtering. | |
| // If additional checks are needed, they should compare the user's tenant to the session tenant. |
| var secretKey = jwtSettings["SecretKey"]; | ||
| var environment = services.BuildServiceProvider().GetRequiredService<Microsoft.AspNetCore.Hosting.IWebHostEnvironment>(); | ||
|
|
There was a problem hiding this comment.
Avoid calling services.BuildServiceProvider() during service registration; it creates a second container (duplicated singletons, wrong scoped lifetimes, potential leaks). Prefer passing IWebHostEnvironment into this method (from the caller) or performing this validation via options validation/hosted service after the real provider is built.
| if (user != null) | ||
| { | ||
| System.Diagnostics.Debug.Assert( | ||
| _session.TenantId == _session.TenantId, |
There was a problem hiding this comment.
Same issue here: _session.TenantId == _session.TenantId is always true and will generate a warning (treated as error). Remove it or replace it with an actual tenant-isolation validation for passkey lookups.
| _session.TenantId == _session.TenantId, | |
| user.TenantId == _session.TenantId, |
|
|
||
| // Act - Simulate concurrent registration attempts with the SAME user ID | ||
| // This simulates a race condition where two clients try to register at the same time | ||
| var registrationResults = new List<(HttpStatusCode StatusCode, string Content)>(); |
There was a problem hiding this comment.
registrationResults is declared but never used. With TreatWarningsAsErrors=true this will fail the build; remove it or use it (e.g., to capture results instead of relying only on results).
| var registrationResults = new List<(HttpStatusCode StatusCode, string Content)>(); |
| using System.IdentityModel.Tokens.Jwt; | ||
| using System.Linq; | ||
| using System.Net; | ||
| using System.Net.Http.Json; | ||
| using BookStore.ApiService.Models; |
There was a problem hiding this comment.
Unused using directives here (e.g., System.IdentityModel.Tokens.Jwt and BookStore.ApiService.Models) will produce warnings that fail the build under TreatWarningsAsErrors=true. Remove unused usings or stop fully-qualifying JwtSecurityTokenHandler if you keep the import.
| using JasperFx; | ||
| using Marten; | ||
| using Microsoft.AspNetCore.Identity; | ||
| using Weasel.Core; |
There was a problem hiding this comment.
Unused using directives here (JasperFx, Microsoft.AspNetCore.Identity, Weasel.Core) will produce warnings that fail the build under TreatWarningsAsErrors=true. Remove any usings that aren’t needed.
| using JasperFx; | |
| using Marten; | |
| using Microsoft.AspNetCore.Identity; | |
| using Weasel.Core; | |
| using Marten; |
| var loginResponse1 = await login1.Content.ReadFromJsonAsync<TestHelpers.LoginResponse>(); | ||
|
|
||
| // Manually add the same refresh token with a DIFFERENT tenant ID to simulate cross-tenant token theft | ||
| // In a real scenario, this would require a security breach or bug that allows token reuse across tenants |
There was a problem hiding this comment.
loginResponse1 is assigned but never used, which will fail the build under TreatWarningsAsErrors=true. Either assert it’s not null / use it in the test, or remove the variable entirely.
| var loginResponse1 = await login1.Content.ReadFromJsonAsync<TestHelpers.LoginResponse>(); | |
| // Manually add the same refresh token with a DIFFERENT tenant ID to simulate cross-tenant token theft | |
| // In a real scenario, this would require a security breach or bug that allows token reuse across tenants | |
| // Manually add the same refresh token with a DIFFERENT tenant ID to simulate cross-tenant token theft | |
| // In a real scenario, this would require a security breach or bug that allows token reuse across tenants | |
| // In a real scenario, this would require a security breach or bug that allows token reuse across tenants |
| using Marten; | ||
| using Refit; | ||
| using Weasel.Core; |
There was a problem hiding this comment.
using Marten; / using Weasel.Core; appear unused in this test file and will fail the build with warnings-as-errors. Please remove unused usings.
| using Marten; | |
| using Refit; | |
| using Weasel.Core; | |
| using Refit; |
|
|
||
| // Act - Try to use a passkey with a DECREASING counter (cloned authenticator) | ||
| var client = TestHelpers.GetUnauthenticatedClient(tenantId); | ||
| var response = await client.PostAsJsonAsync("/account/assertion/result", new |
There was a problem hiding this comment.
response is assigned but never used; with warnings treated as errors this will break the build. Either assert on response (e.g., status code) or discard it.
| var response = await client.PostAsJsonAsync("/account/assertion/result", new | |
| await client.PostAsJsonAsync("/account/assertion/result", new |
| using BookStore.ApiService.Models; | ||
| using BookStore.Client; | ||
| using BookStore.Shared.Models; | ||
| using JasperFx; | ||
| using Marten; |
There was a problem hiding this comment.
At least BookStore.ApiService.Models and JasperFx are unused here and will fail the build under warnings-as-errors. Please remove unused usings.
| using BookStore.ApiService.Models; | |
| using BookStore.Client; | |
| using BookStore.Shared.Models; | |
| using JasperFx; | |
| using Marten; | |
| using BookStore.Client; | |
| using BookStore.Shared.Models; |
Description
Implement passkey authentication and registration flow, enhancing security with tenant validation and conflict detection. Introduce critical security checks and logging for passkey operations.
Type of Change
Related Issues
Fixes #
Changes Made
Testing
dotnet test)dotnet build)dotnet format --verify-no-changes)Checklist
Screenshots (if applicable)
Additional Notes