← automation
sherpa-api#466 Done ⏱ 1h 3m
Passkey (WebAuthn) login
⎇ auto/api-466-passkey-webauthn-login
Portal localhost:3003Embed localhost:3103API localhost:4003
🔍 iterative-review ✓ (main-pass) 🧪 test:api failed 226 passed · 5 failed · 0 flaky · 12 skipped · report 🧑‍🔬 QA ✅ PASS

Feedback

Pull requests

loading…

QA evaluation

✅ PASS

PASS. All four acceptance criteria were independently verified through the REAL Customer Portal UI in headless Chromium with a CDP virtual authenticator (platform + roaming key) - not via API/curl/DB. I wrote my own spec (sherpa-test/tests/eval-passkey-qe.spec.ts) that self-registers a fresh customer, password-logs-in, then drives enrol -> passwordless login -> add 2nd -> revoke -> revoked-rejected -> survivor-login, capturing fresh eval-*.png proof. The 'Customer Portal' the ticket targets is served by the portal app on :3003 under the customer tenant (test.localhost:3003, portalType=customer); the bare localhost:3003 origin is the CRM/back-office org (Azure AD, portalType=crm) with no password form - but it too ships the 'Sign in with a passkey' button (eval-7). Tests: test:api reported 5 failures, but all 5 are pre-existing plus-insurance count/state assertions (renewals, ledger, policy-order, suspension preview, translation coverage) from shared-DB drift, zero relation to passkeys/auth; the identical 5-failed signature appears on unrelated ticket #456, so tests_ok=true (no new regressions). Feature is genuinely usable end-to-end in the Customer Portal UI.

4/4 acceptance criteria passed
  • Register a passkey (sign-up or Profile) and log in with the "Sign in with a passkey" buttonCustomer portal (test.localhost:3003), real UI. Enrolled 'QE Device A' from Profile -> Security -> Passkeys (Add passkey -> Name this passkey -> Create passkey); 'Passkey added' toast, list showed 1, account-recovery copy visible. Logged out, clicked 'Sign in with a passkey' on /login with NO email typed (discoverable flow) -> landed authenticated on the customer dashboard ('Hi QE,' / 'My Policies'). eval-1, eval-2.
  • Register a second passkey from the management screen (list shows 2)Added 'QE Device B' on a roaming (USB) authenticator from the management screen. List showed 2 entries: 'QE Device B' (Last used Never) and 'QE Device A' (Last used May 31, 2026 - from the passkey login above). eval-3.
  • Revoke one passkey from the management screen (list shows 1)Revoked 'QE Device A' via the inline Revoke -> Remove confirm. 'Passkey revoked' toast shown; the row disappeared and the list dropped to 1 ('QE Device B' only). eval-4.
  • The revoked passkey can no longer authenticate (login attempt rejected)With ONLY the revoked Device A present, clicking 'Sign in with a passkey' was rejected: red 'Unrecognized passkey' error, stayed on /login. Re-presenting ONLY the surviving Device B then logged in successfully -> the rejection is credential-specific, not a broken flow. Backend confirms: discoverable login looks up by credentialId (deleted on revoke) and persists newCounter for replay protection. eval-5, eval-6.
tests_ok: true · quality: Production-grade. Portal: passkeys.ts wraps all 7 endpoints via @simplewebauthn/browser, feature-detects PublicKeyCredential, delegates token persistence to the single AuthProvider.login path. api.ts adds ONLY the two passkey LOGIN paths to NO_AUTH_PATHS (so a revoked/unknown-passkey 401 is a login failure, not a session-end) while register/list/rename/revoke stay authenticated. PasskeysSection serializes ceremonies via a busy flag, confirm-before-revoke, recovery copy, empty state. Backend (sherpa-api) is additive: passkeyController/Routes/UserPasskey model + migrations (User|Customer ownership via nullable userId/customerId + CHECK constraint); management ops scoped by ownerWhere (no IDOR), login persists signature counter (clone/replay protection), tokens issued owner-scoped. No severe issues. Embed correctly out of scope: anonymous quote->checkout funnel, no login/passkey/webauthn surface (empty diff vs development).