JavaScript WebAuthn (Passkeys) Table
| Piece | What it does | Field note |
|---|---|---|
credentials.create() | Registration | Keypair born in the authenticator, bound to rpId; server stores the PUBLIC key only |
credentials.get() | Authentication | Signs the server challenge - assertion returns authenticatorData + signature + userHandle |
challenge | The freshness proof | Server-generated, single-use, expiring - replay resistance lives here; clients never invent it |
UV vs UP | The pin point | Presence = a hand touched; Verification = biometrics/PIN proved the person - server checks the flag |
platform / cross-platform | The attachment | Built-in Touch/Face ID vs roaming keys (YubiKey, QR hybrid flow) - one account holds both |
conditional UI | The discoverability | autocomplete="username webauthn" + mediation:'conditional' = passkeys in the autofill dropdown |
attestation vs assertion | The two signatures | Attestation = who made the key (registration, usually 'none'); assertion = proof of possession (every login) |
server verification | The trust boundary | Signature vs stored public key + origin + rpId + counter - client promise resolving is UX, not auth |
WebAuthn is the passkeys API: at registration, the user's device generates a keypair bound to your origin (the rpId) - the private key never leaves the authenticator, and the server stores only the public key. At login, the server sends a challenge and the device signs it. That is the entire cryptographic story, and it replaces the password's phishing surface with origin-bound math: a lookalike domain cannot ask your authenticator to sign for it, because the authenticator refuses to make a signature for an rpId it did not register.
Bottom line: two ceremonies, one rule - the server verifies. The challenge must be server-generated, single-use, and expiring; the assertion's signature is checked against the stored public key along with origin, rpId, and the signature counter; userVerification is a policy the SERVER sets and checks in the flags, not a UI prompt it hopes happened. Client-side success (the promise resolving) means the user touched something - it is UX, not authentication.
The honest part: recovery is still your product. A passkey-bound account with an email-magic-link recovery path is exactly as phishable as that email path - so ship multiple passkeys per account, cross-device enrollment (the hybrid QR flow), and a deliberately slow, rate-limited recovery story. Also know that attestation is usually 'none' in practice: for sync passkeys especially, you mostly cannot tell which vendor made a key - by privacy design, and your threat model should not depend on it.
How to use
- Register: server builds the creation options ({ challenge, rp, user, pubKeyCredParams, excludeCredentials, authenticatorSelection }); call navigator.credentials.create({ publicKey: options }); ship the attestation response to the server, which verifies it and stores credentialId, the public key, and the sign counter. excludeCredentials prevents duplicate keys per account - pass it the existing credential ids.
- Authenticate: server sends { challenge, allowCredentials, userVerification }; call navigator.credentials.get({ publicKey: options }); the assertion response (authenticatorData, signature, clientDataJSON, userHandle) goes to the server, which verifies the signature against the stored public key, checks the counter increased, and validates origin and rpIdHash. Allow credentials makes the ceremony single-device; omit it for discoverable (resident) passkeys.
- Set verification policy deliberately: authenticatorSelection.userVerification controls what the ceremony demands - 'required' forces biometrics/PIN, 'preferred' asks nicely, 'discouraged' accepts a bare touch. Then the server checks the UV flag in authenticatorData, because the client's promise resolving proves presence (a touch), not verification (the right human).
- Add conditional UI for discoverability: an <input autocomplete="username webauthn"> field plus credentials.get({ mediation: 'conditional', publicKey: ... }) makes passkey suggestions appear in the username autofill dropdown - the ceremony runs only if the user picks one. This is the layer that makes passkeys feel native instead of a modal ceremony users flee.
- Design the recovery story on purpose: encourage multiple passkeys (laptop + phone), support the hybrid cross-device flow (QR + Bluetooth proximity) for new devices, and keep exactly one slow, rate-limited, human-reviewed fallback for total loss. The security of a passkey system is the minimum of its ceremonies and its recovery path - attackers go where the friction is low.
Frequently asked questions
Are passkeys really phishing-proof?
The credential is - by construction: the key pair is bound to your rpId (registrable domain), and an authenticator will not produce a signature for a different rpId no matter how convincing the fake site looks. There is no reusable secret to type into the wrong box; the attacker gets a challenge-bound signature for THEIR origin, which your server would reject and theirs cannot use. What absolutely remains phishable is everything around the credential: the recovery email link, the one-time code fallback, the support-agent social engineering. Passkeys move the moat to the origin; they do not defend the drawbridge you left open behind it.
What is the difference between user presence and user verification?
User presence (UP): a hand touched the device - proves possession of an unlocked phone, which a thief also has. User verification (UV): the device verified A PERSON through biometric or PIN - proves the right human is present. The server declares which it requires via userVerification and then checks the UV flag in authenticatorData; the classic silent downgrade is requesting 'preferred' (users can dismiss the prompt) and never checking the flag - the ceremony succeeds at presence level while the security doc claims verification. For high-value actions, require UV and verify the flag; the distinction is one bit in a byte, and it is the bit your risk policy lives on.
Where does the biometric data go?
Now near your server - that is the privacy architecture. Biometrics are enrolled in the device's secure hardware (Secure Enclave, TPM) and used locally to unlock the authenticator; the relying party sees only a signature, flags, and a credential id. There is no face or fingerprint data in the protocol at all, so there is nothing biometric to leak from your database, subpoena, or breach. What you do hold - public keys, credential ids, sign counters, and metadata like last-used timestamps - is by design non-secret and identifies nothing about the user's body.
Attestation vs assertion - do I need attestation?
Assertion, always - it is the proof-of-possession signature on every login and it is the authentication. Attestation (registration-time evidence about WHO MADE the key and how) is usually skippable: browsers default to 'none' for privacy, sync passkeys (iCloud Keychain, Google Password Manager) are software authenticators with no meaningful hardware attestation anyway, and gating registration on specific vendor certificate chains locks out exactly the users who adopted passkeys early. Request attestation only with a concrete, current threat model (e.g., corporate policy requiring managed keys) - and even then treat it as policy input, not a security boundary.