Passkeys in 2026 — WebAuthn Basics for Web Devs

WebAuthn passkeys security

Passkeys are not “passwords stored in the cloud with better branding.” They are WebAuthn credentials: public-key pairs scoped to your site’s relying party ID, unlocked by local biometrics or device PIN, often synced via platform vendors (iCloud Keychain, Google Password Manager, Windows Hello). In 2026 you can ship them without inventing crypto — but the ceremony and RP ID rules still bite teams who treat them like another OAuth button.

Ceremony in one page

Registration (create)

  1. User is authenticated (or in a signup flow).
  2. Server sends PublicKeyCredentialCreationOptions (challenge, rp.id, user id, algorithms).
  3. Browser / authenticator creates a credential; returns attestation/attested data.
  4. Server verifies the challenge and stores the credential id + public key.

Authentication (get)

  1. Server sends PublicKeyCredentialRequestOptions (challenge, allowed credential ids optional).
  2. Authenticator signs the challenge.
  3. Server verifies signature with the stored public key and checks challenge/RP ID/origin.

The private key never hits your server. Phishing resistance comes from the browser binding the credential to the RP ID / origin — a lookalike domain cannot complete the ceremony for example.com.

RP ID and origin footguns

// server options (shape)
{
  rp: { name: "Acme", id: "example.com" }, // eTLD+1 typically
  user: { id: userIdBytes, name: email, displayName },
  challenge: randomBytes,
  pubKeyCredParams: [{ alg: -7, type: "public-key" }, { alg: -257, type: "public-key" }],
}
MistakeResult
RP ID www.example.com but users also use apexCredentials do not work across hosts unless carefully designed
Testing on localhost then shipping different RP IDDev credentials useless in prod (expected) — plan separate envs
IP address as originWebAuthn support is painful; use real hostnames
HTTP in productionSecure context required

Subdomains: credentials registered under RP ID example.com can work for app.example.com if the browser accepts that RP ID relationship. Do not casually change RP ID after launch — you orphan passkeys.

UX gotchas that generate support tickets

  • Cross-device login: User registered on iPhone, opens desktop without sync / without QR hybrid flow. Offer discoverable credentials + hybrid transport, and keep a recovery path (email magic link, not “just reset password” if you removed passwords).
  • Enterprise browsers / MDM: Security keys required; platform passkeys blocked. Detect failures and show “try security key” copy.
  • Autofill conditional UI: mediation: 'conditional' for passkey autofill in the username field — great when supported, silent no-op in older browsers; feature-detect.
  • Account recovery: Passkeys can be lost with the Apple ID / Google account. Ship backup codes or second factor enrollment day one.

Server verification essentials

Use a maintained library (@simplewebauthn/server, etc.). Manually parsing CBOR once is a CVE farm. Always:

  1. Consume challenges (single use, short TTL).
  2. Verify origin and rpIdHash.
  3. Update signature counters if the authenticator provides them (clone detection signal).
  4. Store credentials per user; allow multiple passkeys (phone + laptop).

Minimal product checklist

  1. Feature-detect PublicKeyCredential and isUserVerifyingPlatformAuthenticatorAvailable.
  2. Separate RP IDs / origins per environment.
  3. Multi-device story documented before marketing “passwordless.”
  4. Fallback auth path tested under “lost phone.”
  5. Logging for ceremony step failures (without logging raw authenticator blobs into public channels).

Passkeys remove shared secrets from your database. They do not remove product design. Get RP ID and recovery right, and the crypto libraries will do the rest.