# WebAuthn and passkeys

> How the Web Authentication API and passkeys give a PWA phishing-resistant, passwordless sign-in — credential creation and assertion, discoverable credentials, platform vs roaming authenticators, and cross-device sync.

**In one line:** The Web Authentication API (WebAuthn) lets a site register and verify a
public-key credential bound to a device's authenticator, so users sign in with a fingerprint,
face, or device PIN instead of a password. A **passkey** is a discoverable WebAuthn
credential that typically syncs across a user's devices — phishing-resistant by design,
because the signature is bound to the site's origin and the private key is never exposed to
the site.

## What WebAuthn actually does

WebAuthn replaces the shared secret (a password) with a key pair. For a passkey
(discoverable credential), the **private key** is held by the authenticator (a secure
element, TPM, or platform keystore); for some non-discoverable credentials the private key
may instead be stored by the relying party in wrapped form, encrypted under a master key the
authenticator holds. Either way the **public key** is stored by your server and the private
key is never handed to the site. There are two ceremonies:

- **Registration** (`navigator.credentials.create()`) — the authenticator generates a new
  key pair for your origin and returns the public key plus an attestation. You store the
  public key and a credential ID against the user account.
- **Authentication** (`navigator.credentials.get()`) — the authenticator signs a
  server-issued challenge with the private key. Your server verifies the signature with the
  stored public key.

Because the signed data includes the origin and a fresh challenge, a credential phished on a
look-alike domain is useless, and a captured assertion can't be replayed.

## Passkeys vs "classic" WebAuthn

A passkey is a WebAuthn credential that is **discoverable** (a.k.a. resident) — the
authenticator stores enough state to find the credential without the server first naming a
credential ID. That's what enables "just tap to sign in" with no username typed first.
Combined with provider sync (iCloud Keychain, Google Password Manager, etc.), the same
passkey works across a user's devices.

| | Classic WebAuthn (second factor) | Passkey |
|---|---|---|
| Discoverable credential | Usually not required | Yes (`residentKey: "required"`) |
| User identifier needed first | Often (username + password, then key) | No — credential is discoverable |
| Typical role | Second factor on top of a password | Primary, passwordless sign-in |
| Cross-device | Roaming security key, per device | Synced across the user's devices |

## Platform vs roaming authenticators

- **Platform authenticator** — built into the device (Touch ID / Face ID, Windows Hello,
  Android biometrics). Request it with `authenticatorAttachment: "platform"`. Best for the
  primary passkey on a user's own device.
- **Roaming (cross-platform) authenticator** — a removable security key (USB/NFC/Bluetooth)
  or a phone used to sign in to another device. Use `"cross-platform"` for hardware keys or
  cross-device flows.

Use `PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()` to detect whether a
platform authenticator exists before offering a passkey path, and
`isConditionalMediationAvailable()` to enable autofill-style passkey suggestions.

## Conditional UI (passkey autofill)

Calling `navigator.credentials.get()` with `mediation: "conditional"` lets the browser offer
saved passkeys inline in the username field's autofill, instead of a modal. The request waits
silently until the user picks a passkey, so you can start it on page load without interrupting
users who'd rather type. Pair the input with `autocomplete="username webauthn"`.

## Browser & ecosystem support

WebAuthn is supported across current major browsers. Passkeys — discoverable credentials
with cross-device sync — are available on several major platforms, though coverage is still
filling in: the cited passkeys.dev device-support matrix tracks per-platform gaps (for
example, synced passkeys are listed as planned rather than shipped on some platforms). Where
supported, sync is handled by a platform credential manager such as Google Password Manager
or iCloud Keychain rather than the browser alone. Because the matrix shifts, feature-detect
(`PublicKeyCredential`, conditional mediation) rather than assuming.

## A PWA's responsibilities

WebAuthn is only as safe as the server-side verification. The browser API hands you data to
check, not a finished decision:

- Generate a cryptographically random **challenge** per ceremony and bind it to the session;
  reject a stale or reused challenge.
- Verify the **origin** and **RP ID** in the returned client data match your site.
- On registration, store the public key, credential ID, and **signature counter**; on each
  authentication, confirm the counter hasn't gone backwards (a clone signal).
- Treat passkeys as account credentials: let users enroll more than one, name them, and
  revoke a lost one.

## Practical checklist

- [ ] Feature-detect `window.PublicKeyCredential` before showing any passkey UI.
- [ ] Use `isUserVerifyingPlatformAuthenticatorAvailable()` to gate the platform-passkey path.
- [ ] Set `residentKey: "required"` and `userVerification: "preferred"` for true passkeys.
- [ ] Issue a fresh random challenge server-side and verify origin, RP ID, and signature.
- [ ] Persist and check the signature counter to detect cloned authenticators.
- [ ] Offer conditional-UI autofill with `autocomplete="username webauthn"` where supported.
- [ ] Allow multiple passkeys per account and a recovery path so a lost device isn't a lockout.