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.
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
- Generate a registration challenge server-side and pass the options to the browser. Expected: The ceremony is bound to a fresh challenge.
- Call navigator.credentials.create on the client and send the attestation to the server. Expected: The browser creates a key pair on the authenticator.
- Verify the attestation and store the credential id, public key, and counter. Expected: The server holds only a public key, never a secret.
- 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.