Make discoverable WebAuthn passkeys the primary sign-in
Decision
Sign users in with passkeys, using WebAuthn discoverable credentials through webauthn-rs. The user types no username; the device offers its matching passkey behind Face ID or Touch ID. A new account can be created with an email address and a passkey and no password. Email and password remain as a fallback where a passkey isn't available.
The server serves an apple-app-site-association file so iOS will offer passkeys for our domain. It keeps pending WebAuthn challenges in Postgres and deletes expired ones daily. Successful sign-in by either path issues the same device-bound PASETO token (ADR-0028).
Why
Passwords get reused, phished and stuffed. A passkey is scoped to its relying-party domain, so a phishing site can't obtain an assertion for ours, and the server stores no shared secret. Apple platforms support passkeys natively, including sync through iCloud Keychain, so the main sign-in path costs the user one biometric prompt.
Rejected alternatives
- Passwords first, with passkeys as an optional extra. The phishable secret would stay on the main path for every user who never enrols a passkey.
- Passkeys only. A user whose passkey sits on a lost device, or in a keychain the current device doesn't sync, would need an account-recovery flow we would have to build first.
- Email magic links. Each sign-in leaves the app for a mail client, and the link is itself a bearer secret that can be forwarded or intercepted.
Consequences
The main sign-in path has no secret a user can type into the wrong site. We maintain WebAuthn's challenge-and-response flow and credential storage, the associated-domains file, and the password path beside them. Passkeys need HTTPS on a real domain, so local development runs Caddy with a real certificate on port 443.