Guide
Passkeys and WebAuthn explained
Passwords leak in breaches, get reused across sites, and fail against convincing phishing pages that look identical to the real login form. Passkeys — the consumer-friendly name for FIDO2 credentials accessed through WebAuthn in the browser — replace shared secrets with public-key cryptography bound to your domain. The private key never leaves the authenticator (your phone's Secure Enclave, Windows Hello, or a hardware key). This guide explains how registration and login ceremonies work, platform vs roaming authenticators, RP ID scoping, recovery trade-offs, and what to implement before you ship passwordless auth alongside OAuth or JWT sessions.
What passkeys are (and what they are not)
A passkey is a discoverable FIDO2 credential: a key pair where the public half is stored on your server and the private half lives in an authenticator device. "Discoverable" means the authenticator can list credentials for a given relying party (your website) without the user typing a username first — the browser shows a picker of saved passkeys for your domain.
Passkeys are not passwords stored in iCloud Keychain with a different label. They are not magic links, SMS one-time codes, or TOTP authenticator apps — those still rely on a shared secret or channel that phishers can intercept. They are also not the same as a crypto wallet signature: WebAuthn signs a challenge with a credential scoped to your RP ID, not an arbitrary message for on-chain settlement.
The standards stack:
- WebAuthn — browser JavaScript API (
navigator.credentials.create()andget()). - CTAP2 — wire protocol between the browser and the authenticator (USB, NFC, BLE).
- FIDO2 — umbrella spec combining WebAuthn + CTAP for passwordless and second-factor use cases.
Apple, Google, and Microsoft sync passkeys across devices tied to the same account (iCloud Keychain, Google Password Manager, Windows Hello). That convenience trades some pure hardware isolation for usability — still far stronger than password + SMS for most consumer apps.
Platform vs roaming authenticators
Authenticators fall into two families, and your UX should account for both:
- Platform authenticators — built into the device OS: Touch ID, Face ID, Windows Hello, Android fingerprint. Private keys live in a secure element or TPM. Users unlock with biometrics or device PIN. This is what most people mean by "passkey on my phone."
- Roaming authenticators — external hardware: YubiKey, Feitian, etc. Plug in USB, tap NFC, or pair over BLE. Keys travel with the device; useful for high-assurance admin accounts and users who refuse cloud sync.
WebAuthn lets you request either via authenticatorAttachment:
"platform" nudges users toward built-in biometrics;
"cross-platform" targets roaming keys. Omitting the constraint allows
both — usually the right default for a "Sign in with passkey" button.
User verification (userVerification: "required") forces
biometric or PIN proof before signing. Prefer "preferred" only when you
accept security downgrade on older devices — most production apps should require it.
Registration ceremony (creating a passkey)
Registration binds a new credential to a user account. Never trust the client to generate keys server-side — the authenticator must create the key pair locally.
- Server generates a challenge — at least 16 random bytes, stored server-side (session or short-lived cache) with the intended username and RP ID.
- Browser calls
navigator.credentials.create()withpublicKeyoptions:challenge,rp(name + id),user(id, name, displayName),pubKeyCredParams(usually ES256 = -7),authenticatorSelection, and optionalexcludeCredentialsto prevent duplicate registrations. - User verifies — biometric or PIN prompt from the OS.
- Authenticator returns
PublicKeyCredentialwithrawId,attestationObject, andclientDataJSON. - Server verifies — challenge matches, origin and RP ID hash are correct, signature over attestation is valid, credential ID is new, then stores credential ID + public key + sign counter + transports for the user.
The RP ID is critical: it must be a registrable domain suffix of the
page origin. If your RP ID is solana.garden, credentials work on
https://solana.garden and subdomains you explicitly allow — but
not on evil-solana.garden or
solana.garden.phishing.com. That domain binding is what makes passkeys
phishing-resistant: a fake site cannot trigger your real credential because the RP
ID will not match.
Use libraries (SimpleWebAuthn, @passwordless-id/webauthn, Auth0/Firebase passkey SDKs) rather than parsing CBOR attestation by hand unless you have a compliance reason to inspect attestation certificates.
Assertion ceremony (signing in)
Login reuses the stored public key. The flow mirrors registration:
- Server issues a fresh challenge and optionally lists allowed
allowCredentials(credential IDs for known users) or leaves the list empty for discoverable (usernameless) login. - Browser calls
navigator.credentials.get()with the challenge and RP ID. - Authenticator signs
authenticatorData+clientDataJSONwith the private key. - Server looks up the credential ID, verifies the signature with the stored public key, checks challenge freshness, origin, RP ID, and the sign counter (monotonic — a cloned key replaying an old counter should fail).
Usernameless login omits allowCredentials so the
authenticator offers all passkeys for your RP ID. Better UX, but you identify the
user only after verifying the assertion (credential ID maps to account). Hybrid
flows show email first, then passkey — easier migration from passwords.
After verification, issue your normal session — a signed JWT or opaque server session cookie over HTTPS. WebAuthn replaces the password check, not your entire auth stack.
Attestation, AAGUID, and enterprise policy
During registration, authenticators may include an attestation statement proving the make and model (YubiKey 5, Apple Anonymous, etc.). Consumer passkey products often use none attestation or privacy-preserving formats to avoid tracking users across sites.
Enterprise deployments sometimes require attestation policy — only allow keys from approved vendors, stored in an MDM-controlled secure enclave. For a typical SaaS app, verify attestation loosely or skip it; store the AAGUID if present for support diagnostics, not as a hard gate.
Resident keys (discoverable credentials) must be requested via
residentKey: "required" in authenticatorSelection for
usernameless flows. Each resident key consumes limited authenticator storage — warn
power users who register dozens of test accounts.
Recovery, account loss, and migration
Passkeys solve credential theft; they do not solve device loss by themselves. Plan explicit recovery:
- Synced passkeys — iCloud/Google account recovery becomes your recovery path. Document this for users; offer a fallback email magic link only from a hardened, rate-limited flow.
- Multiple credentials per user — let users register a phone passkey and a backup hardware key. Store several credential IDs per account.
- Legacy password + passkey — during migration, keep password login with MFA until passkey enrollment is confirmed, then deprecate passwords for enrolled users.
- Admin break-glass — support tickets verifying identity out-of-band; never a "reset passkey" link that bypasses verification.
Crypto wallet users expect seed-phrase recovery; passkey users expect Apple/Google account recovery. Set expectations in your UI copy — "Your passkey is saved to iCloud Keychain" vs "This security key does not sync — store a backup key."
Security properties and limits
Passkeys defend against:
- Phishing — credentials are origin-bound; fake domains cannot sign.
- Database leaks — servers store public keys only; no password hash to crack.
- Replay — challenges are single-use; sign counters detect cloned authenticators.
They do not stop:
- Session hijacking after login — still need HttpOnly secure cookies, CSRF tokens, short TTLs.
- XSS exfiltrating session tokens — pair with strict CSP.
- Malware on the device — can abuse an unlocked authenticator while the user is present.
- Account takeover of the sync provider — weaker than stolen passwords but not zero risk.
WebAuthn challenges should be generated with a CSPRNG — the same discipline as cryptographic secrets elsewhere in your stack. Expire challenges within 60–300 seconds.
Implementation pitfalls
- Wrong RP ID — using
www.example.comwhen users land on apex (or vice versa) breaks credentials. Pick one canonical host. - HTTP in production — WebAuthn requires secure context (HTTPS or localhost). No exceptions on live sites.
- Base64url encoding mistakes — credential IDs and challenges must be base64url, not standard base64, when serializing JSON.
- Not storing sign counter — enables clone detection bypass on supported authenticators.
- Skipping origin verification — clientDataJSON includes the origin; verify it server-side, do not trust a client field.
- iframes — WebAuthn in cross-origin iframes requires
Permissions-Policy: publickey-credentials-get=(self)and user activation. - Safari quirks — test iOS and macOS; conditional UI (autofill passkeys) needs
mediation: "conditional"and associated domain files for native apps.
Production checklist
- Enforce HTTPS with HSTS; WebAuthn will not run on plain HTTP.
- Choose a stable RP ID matching your canonical domain.
- Use a maintained WebAuthn server library; verify challenges, origin, RP ID, and signatures.
- Require user verification for high-value accounts.
- Support multiple credentials per user plus optional hardware backup.
- Store credential ID, public key COSE, sign counter, transports, and AAGUID.
- Offer passwordless and username-first flows; measure enrollment conversion separately.
- Rate-limit registration and assertion endpoints like any auth API.
- Document recovery paths before marketing "passwordless only."
- Test Chrome, Safari, Firefox on desktop and mobile; include conditional UI where supported.
Key takeaways
- Passkeys are FIDO2 discoverable credentials exposed through the WebAuthn browser API.
- Private keys never leave the authenticator; servers store public keys and verify signatures over fresh challenges.
- RP ID domain binding is what makes login phishing-resistant — not user training.
- Platform authenticators (biometrics) are default; roaming keys suit high-assurance backup.
- Plan recovery and multi-credential enrollment before dropping passwords entirely.
- WebAuthn replaces the password step; you still need secure sessions, CSP, and HTTPS after login.
Related reading
- OAuth 2.0 and OpenID Connect explained — social login and delegated authorization alongside passkeys
- JWT explained — issuing session tokens after WebAuthn verification
- TLS and HTTPS explained — secure context requirement for WebAuthn
- Content Security Policy explained — limiting XSS after authenticated sessions