Passkeys let users sign in with the fingerprint, face or PIN they already use to unlock their device. Under the hood they are public-key credentials defined by the WebAuthn standard, and they are resistant to phishing because each credential is bound to your website's domain.

How passkeys work

  • During registration, the user's device creates a key pair. The private key stays on the device (or syncs through the platform's password manager). Your server stores only the public key.
  • During sign-in, your server sends a random challenge. The device signs it with the private key after the user unlocks it. Your server verifies the signature with the stored public key.

Nothing secret is stored on your server, so a database leak does not expose credentials. And because the browser only offers a passkey to the domain it was created for, a look-alike phishing site cannot use it.

The registration flow

  1. Server creates a challenge and registration options.
  2. Browser calls navigator.credentials.create() with those options.
  3. Server verifies the response and stores the credential ID and public key.

Using a maintained library is strongly recommended. With SimpleWebAuthn in Node:

typescript
import { generateRegistrationOptions, verifyRegistrationResponse } from "@simplewebauthn/server";

const options = await generateRegistrationOptions({
  rpName: "Example App",
  rpID: "example.com",
  userName: user.email,
  attestationType: "none",
  authenticatorSelection: { residentKey: "preferred", userVerification: "preferred" },
});
await saveChallenge(user.id, options.challenge);
typescript
const verification = await verifyRegistrationResponse({
  response: body,
  expectedChallenge: await getChallenge(user.id),
  expectedOrigin: "https://example.com",
  expectedRPID: "example.com",
});
if (verification.verified) await storeCredential(user.id, verification.registrationInfo);

The sign-in flow

  1. Server generates authentication options with a fresh challenge.
  2. Browser calls navigator.credentials.get().
  3. Server verifies the signature against the stored public key and signs the user in.

Enable conditional UI (autofill) so passkeys appear in the username field's suggestions, which makes adoption much smoother.

Rolling out without breaking anything

  • Keep existing login methods at first. Offer "Create a passkey" after a successful sign-in.
  • Let users register multiple passkeys (phone, laptop, security key) and name them.
  • Provide a solid account recovery path, such as email verification plus a recovery code.
  • Show where passkeys are used and allow revoking them.

Common mistakes

  • Reusing challenges or not expiring them. Challenges must be single-use and short-lived.
  • Mismatched rpID and origin between environments. Configure them per environment.
  • Removing passwords before users have a passkey on more than one device.

Key takeaways

  • Passkeys are WebAuthn public-key credentials unlocked by device biometrics or PIN.
  • They are phishing-resistant and leave no secrets on your server.
  • Use a maintained library, single-use challenges and correct rpID settings.
  • Roll out alongside existing logins with multiple passkeys and good recovery.