← Recipes

阅读中文版本

Register a Passkey with WebAuthn

Offer a WebAuthn registration ceremony, store the credential public key and counter, and verify assertions on login without ever storing a shared secret.

~50 minwebauthnauthenticationsecurity

Task

Add passwordless passkey registration and authentication to a web app.

When to use this

Passwords are the weak link, and a passkey integration is feared to be complex, so the app keeps relying on reusable secrets.

Prerequisites

  • A browser with WebAuthn support
  • HTTPS origin
  • A server library for WebAuthn verification

Steps

1. Generate a registration challenge server-side and pass the options to the browser.

Expected output: The ceremony is bound to a fresh challenge.

2. Call navigator.credentials.create on the client and send the attestation to the server.

Expected output: The browser creates a key pair on the authenticator.

3. Verify the attestation and store the credential id, public key, and counter.

Expected output: The server holds only a public key, never a secret.

4. On login, verify the assertion and check that the counter increased.

curl -s https://app.example.com/webauthn/register/options | jq .

Expected output: Cloned authenticators are detected.

Troubleshooting

Problem

Login fails after syncing to a new device

Solution

Handle multiple credentials per user and let them register several authenticators.

Problem

Counter not checked

Solution

Some authenticators always report 0; treat a decreasing non-zero counter as suspicious but allow 0.

Limitations

  • Passkeys require a secure context and compatible authenticator, so a fallback must remain for some users.
  • Account recovery becomes critical because loss of all authenticators can lock a user out.

Context

Add passwordless passkey registration and authentication to a web app. Passwords are the weak link, and a passkey integration is feared to be complex, so the app keeps relying on reusable secrets.

Steps

  1. Generate a registration challenge server-side and pass the options to the browser. Expected: The ceremony is bound to a fresh challenge.
  2. Call navigator.credentials.create on the client and send the attestation to the server. Expected: The browser creates a key pair on the authenticator.
  3. Verify the attestation and store the credential id, public key, and counter. Expected: The server holds only a public key, never a secret.
  4. On login, verify the assertion and check that the counter increased. Expected: Cloned authenticators are detected.
const cred = await navigator.credentials.create({
  publicKey: {
    challenge, rp: { name: 'Acme' }, user: { id, name },
    pubKeyCredParams: [{ type: 'public-key', alg: -7 }],
  },
});
await fetch('/webauthn/register/verify', { method: 'POST', body: JSON.stringify(cred) });

Verify

  • The ceremony is bound to a fresh challenge.
  • The browser creates a key pair on the authenticator.
  • The server holds only a public key, never a secret.
  • Cloned authenticators are detected.

Limits

  • Passkeys require a secure context and compatible authenticator, so a fallback must remain for some users.
  • Account recovery becomes critical because loss of all authenticators can lock a user out.