Skip to content

feat: passkey login support with e2e tests#88

Merged
antosubash merged 25 commits intomainfrom
worktree-logical-frolicking-spark
Apr 6, 2026
Merged

feat: passkey login support with e2e tests#88
antosubash merged 25 commits intomainfrom
worktree-logical-frolicking-spark

Conversation

@antosubash
Copy link
Copy Markdown
Owner

Summary

  • Adds WebAuthn/FIDO2 passkey support: 6 API endpoints (register begin/complete, login begin/complete, list, delete), a Manage Passkeys view endpoint and React page, login page passkey button, and sidebar nav item
  • E2e tests covering passkey registration, deletion, sign-in via CDP virtual authenticator, and failure handling

Key implementation details

  • IdentityPasskeyOptions.ServerDomain gates the feature — passkey UI is hidden when unset
  • PasskeyHelpers.ToBase64Url shared between endpoints to avoid duplication
  • passkeyEnabled prop is re-computed in the login POST error path so the passkey button stays visible after a failed password attempt
  • Virtual authenticator credentials use explicit allowCredentials injection via route interception because ASP.NET Identity may not request residentKey: required, making discoverable-credential flow unreliable in CDP

Test plan

  • npx playwright test tests/smoke/users-account.spec.ts tests/flows/users-passkey.spec.ts — 9/9 pass
  • dotnet test — all integration tests pass
  • Manual: navigate to /Identity/Account/Manage/Passkeys, register a passkey, sign out, use passkey to sign in

- sm new project: scaffold vite.dev.config.ts so sm dev works out of the box
- sm dev: fix vite invocation (vite dev → vite) and add --configLoader runner
- sm doctor: fix PagesKeyPattern regex to match single-quoted Pages/index.ts keys
- moduleHmrPlugin: fall back to src/modules/ path for scaffolded user projects
- SM0052/SM0053 diagnostics: scope to SimpleModule.* host projects only so user
  projects with their own naming conventions are not falsely flagged
- AgentExtensionsEmitter: remove early return that broke empty projects
- SimpleModuleHostExtensions: register IHttpContextAccessor for EntityInterceptor
- Directory.Build.props: guard PackageReadmeFile with Exists() to fix pack
…upport

- Configure IdentityOptions.Stores.SchemaVersion = Version3 in UsersModule to opt into
  the AspNetUserPasskeys table (WebAuthn passkey support)
- Configure IdentityPasskeyOptions from appsettings "Passkeys" section in UsersModule
- Add Passkeys config sections to appsettings.json and appsettings.Development.json
- Fix HostDbContextFactory to expose a minimal service provider with IdentityOptions
  via UseApplicationServiceProvider so EF design-time model creation picks up SchemaVersion
- Add UseApplicationServiceProvider to AddModuleDbContext for runtime parity
- Add migration AddPasskeySupport creating Users_AspNetUserPasskeys table
- Add HostDbContextPasskeys.cs as a seam partial for future passkey customizations
…hanges

Add `protected override Version SchemaVersion => IdentitySchemaVersions.Version3`
to the HostDbContext partial — this compiles cleanly against Identity 10.0.3 and is
the correct way to opt into the AspNetUserPasskeys table. Because SchemaVersion is
now a compile-time constant on the class, the two IOptions workarounds are no longer
needed: remove UseApplicationServiceProvider(sp) from ModuleDbContextOptionsBuilder
(which would have applied the full app DI to every module DbContext) and revert the
ServiceCollection + BuildServiceProvider block from HostDbContextFactory (design-time).
Adds the POST /api/passkeys/register/begin endpoint that calls
MakePasskeyCreationOptionsAsync and returns WebAuthn creation options JSON.

Also fixes test infrastructure: UsersDbContext now overrides SchemaVersion
to Version3 so the EF model includes the AspNetUserPasskeys table, and the
shared test factory now calls UseApplicationServiceProvider so IdentityOptions
(including SchemaVersion) are accessible during model creation. Adds
appsettings.Testing.json to configure ServerDomain = "localhost" for tests.
Adds the POST /api/passkeys/register/complete endpoint that calls
PerformPasskeyAttestationAsync to validate WebAuthn attestation and stores
the passkey. Catches InvalidOperationException (no challenge cookie) and
PasskeyException (crypto failure) to return 400 instead of 500.

Also adds PasskeyApiEndpointTests integration test class covering all four
scenarios: RegisterBegin authenticated/unauthenticated and RegisterComplete
unauthenticated/invalid-credential. Tests seed a real user (passkey-test-user-id)
with CreateAuthenticatedClient targeting that user ID.
…isterCompleteEndpoint

Replace undisposed StreamReader with a using block (leaveOpen: true) to avoid
closing the framework-owned request body stream. PasskeyException confirmed
present in .NET 10 Identity — catch block retained as-is.
Add PasskeyLoginBeginEndpoint, PasskeyLoginCompleteEndpoint, GetPasskeysEndpoint,
and DeletePasskeyEndpoint with full integration test coverage (38 tests passing).
Adds transports field to the passkeys DTO and derives a human-readable
device type hint (e.g. 'Built-in sensor', 'Security key (USB)') shown
below the passkey name in the Manage Passkeys page.
The test-projects directory contains its own biome.json which conflicts
with the root configuration. Since test-projects is already excluded
from the files.includes scope, explicitly adding it as an ignore pattern
prevents the nested root config error.
…vigations

- serial mode + beforeEach cleanup eliminates shared-state races between tests
- migrate credential ID via CDP WebAuthn.getCredentials after registration so the
  sign-in assertion works whether or not the credential was stored as a resident key
- register route handlers before navigation so no request can slip through unintercepted
- replace page.waitForTimeout(500) with try/catch on waitForURL for deterministic error reporting
- fix auth.setup.ts to navigate directly to /Identity/Account/Login instead of
  clicking the missing "Log in" link on the landing page
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Apr 6, 2026

Deploying simplemodule-website with  Cloudflare Pages  Cloudflare Pages

Latest commit: 1fc0e69
Status: ✅  Deploy successful!
Preview URL: https://22bd129c.simplemodule-website.pages.dev
Branch Preview URL: https://worktree-logical-frolicking.simplemodule-website.pages.dev

View logs

@cloudflare-workers-and-pages
Copy link
Copy Markdown

Deploying simplemodule-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: 4bf1d6a
Status: ✅  Deploy successful!
Preview URL: https://9ce2c7cd.simplemodule.pages.dev
Branch Preview URL: https://worktree-logical-frolicking.simplemodule.pages.dev

View logs

Auto-merge of PR #87 (type-safe routes) placed the SM0054 endpoint/view
Route-const diagnostics outside the 'foreach (var module in data.Modules)'
loop, leaving 'module' out of scope. Move them back inside the loop.
@antosubash antosubash merged commit 161ba68 into main Apr 6, 2026
4 checks passed
@antosubash antosubash deleted the worktree-logical-frolicking-spark branch April 6, 2026 17:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant