JavaScript WebAuthn (Passkeys) Table

PieceWhat it doesField note
credentials.create()RegistrationKeypair born in the authenticator, bound to rpId; server stores the PUBLIC key only
credentials.get()AuthenticationSigns the server challenge - assertion returns authenticatorData + signature + userHandle
challengeThe freshness proofServer-generated, single-use, expiring - replay resistance lives here; clients never invent it
UV vs UPThe pin pointPresence = a hand touched; Verification = biometrics/PIN proved the person - server checks the flag
platform / cross-platformThe attachmentBuilt-in Touch/Face ID vs roaming keys (YubiKey, QR hybrid flow) - one account holds both
conditional UIThe discoverabilityautocomplete="username webauthn" + mediation:'conditional' = passkeys in the autofill dropdown
attestation vs assertionThe two signaturesAttestation = who made the key (registration, usually 'none'); assertion = proof of possession (every login)
server verificationThe trust boundarySignature vs stored public key + origin + rpId + counter - client promise resolving is UX, not auth
Reference: the MDN Web Authentication API. WebAuthn replaces the password's shared-secret problem with origin-bound public keys: the private key never leaves the device, a lookalike domain cannot obtain a usable signature (the authenticator refuses to sign for an rpId it did not register), and the server's database holds nothing a breach can replay.
Bottom line: two ceremonies, one rule - the server verifies. Challenge is server-generated and single-use; the assertion signature is checked against the stored public key along with origin, rpId, and the sign counter; and UV is a policy the server sets AND checks in the flags - requesting 'preferred' and hoping is the silent downgrade that ships most failed WebAuthn deployments. Recovery is your remaining product surface: multiple passkeys per account, cross-device enrollment, one slow rate-limited fallback.
Related tools: the Web Crypto table (where challenges and nonces come from), the localStorage table (what NOT to store secrets in), the fetch table (moving assertions to the server), and the Permissions table (querying what the browser will allow).

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

  1. 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.
  2. 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.
  3. 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).
  4. 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.
  5. 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.

Related tools