Skip to content

Add WebAuthn passkey login alongside email OTP#712

Merged
rzueger merged 1 commit intodevelopfrom
claude/add-passkey-auth-Uc7cj
Apr 19, 2026
Merged

Add WebAuthn passkey login alongside email OTP#712
rzueger merged 1 commit intodevelopfrom
claude/add-passkey-auth-Uc7cj

Conversation

@rzueger
Copy link
Copy Markdown
Member

@rzueger rzueger commented Apr 19, 2026

Summary

Adds passkeys (WebAuthn) as an optional, faster sign-in mechanism alongside the existing email OTP flow. OTP remains the baseline and recovery path; passkeys are purely additive and feature-flagged per-environment.

Server (Cloud Functions, functions/auth/)

  • generateRegistrationOptions / verifyRegistration — passkey enrolment
  • generateAuthenticationOptions / verifyAuthentication — passkey sign-in, mints a Firebase custom token on success (same mechanism the OTP flow uses)
  • removePasskey — user-initiated removal
  • cleanupExpiredWebauthnChallenges — scheduled (hourly) cleanup
  • webauthnHelpers — atomic challenge consume via RTDB transaction(), AuthError, RP config, ID-token verification

Client

  • src/util/webauthn.ts — thin wrapper around @simplewebauthn/browser; does NOT import firebase/auth (saga reuses the existing requestFirebaseAuthentication flow)
  • Login page: single "Mit Passkey anmelden" button (reused StyledOrContainer separator), conditionally rendered when __CONF__.passkeysEnabled && isPasskeySupported()
  • Profile page: PasskeyManager section — list with device name, created date, last used date; remove via styled modal confirmation (matches MovementDeleteConfirmationDialog pattern)
  • PostLoginPasskeyPrompt — post-login card shown to users without any passkey, styled identically to InstallCard; two-strike dismiss with "Später" → "Nicht mehr anzeigen" pattern; auto-labels from user agent (iPhone / iPad / Mac / Windows / Android / Linux)
  • New auth module slice handles REGISTER_PASSKEY, LOGIN_WITH_PASSKEY, LOAD_PASSKEYS, REMOVE_PASSKEY with success/failure actions

Infra

  • 3 new Realtime Database paths with locked-down rules: webauthnCredentials (read self/admin, writes server-only), webauthnChallenges (server-only), webauthnCredentialOwners (server-only index)
  • @simplewebauthn/browser v13 added to root; @simplewebauthn/server v10 added to functions
  • passkeysEnabled: false in projects/default.json; enabled via environments.<env>.passkeysEnabled: true (uses the env-merge capability from Merge selected environment config into __CONF__ #710)
  • Runtime RP config via firebase functions:config:set webauthn.rpid=... webauthn.origins=...

Security posture

  • Atomic challenge consume via RTDB .transaction() (prevents concurrent re-use)
  • Counter clone detection on every authentication, spec-compliant handling of non-counting authenticators (iCloud Keychain, Google Password Manager, 1Password)
  • Session-based auth for register/remove (valid ID token required); no step-up re-auth needed — low-stakes product with OTP fallback always available
  • User enumeration mitigated: generateAuthenticationOptions returns plausible empty-allowList options for unknown emails

Test plan

  • npm test — 1915 tests pass (includes new specs for webauthn.ts, PasskeyLoginButton, PasskeyManager, PostLoginPasskeyPrompt, all auth module additions)
  • cd functions && npm test — 278 tests pass (6 new spec files for the webauthn functions)
  • npm run typecheck clean
  • Manual QA on lszm-test: enable passkey login via profile, sign out, sign back in with passkey, remove passkey, verify OTP still works
  • Verify on an env with passkeysEnabled: false that the UI remains unchanged

Adds passkeys as a second, faster sign-in mechanism while OTP
remains the baseline and recovery path. Registered passkeys live
in Realtime Database; the server mints a Firebase custom token
via the same single line the OTP flow uses, so no new Firebase
Auth surface is introduced.

- Cloud Functions: generate/verify registration and authentication
  options, remove-credential, expired-challenge cleanup.
- Client util src/util/webauthn.ts does not import firebase/auth;
  saga reuses the existing requestFirebaseAuthentication action.
- Login page gains a passkey button; profile page gains a
  passkey manager; post-login banner prompts first-time setup.
- Gated behind a passkeysEnabled flag, default false.
@rzueger rzueger force-pushed the claude/add-passkey-auth-Uc7cj branch from c477813 to 9859a1e Compare April 19, 2026 09:01
@rzueger rzueger merged commit 34464a7 into develop Apr 19, 2026
2 checks passed
@rzueger rzueger deleted the claude/add-passkey-auth-Uc7cj branch April 19, 2026 09:15
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.

2 participants