| name | webauthx |
|---|---|
| description | Set up production-ready WebAuthn passkey authentication using webauthx. Use when adding passkey auth, WebAuthn registration, or WebAuthn authentication to an app. |
Set up production-ready WebAuthn passkey ceremony orchestration using webauthx.
Read README.md for the full API reference. Read examples/hono/ for a simplified reference implementation. The example prioritizes clarity over production hardening, so follow this checklist for production.
Install webauthx and import from webauthx/server and webauthx/client.
- Server: call
Registration.getOptions({ name, rp }). Returns{ challenge, options }. - Store the
challengeserver-side (signed cookie, DB, or KV). Must be single-use and short-lived (≤5 min). - Client: pass
optionstoRegistration.create({ options }). Triggers the browser passkey prompt. Must be called from a user gesture (click/tap). - Client: POST the returned credential to the server.
- Server: consume the stored challenge, then call
Registration.verify(credential, { challenge, origin, rpId }). - Persist
result.credential.idandresult.credential.publicKeyassociated with the user.
- Server: call
Authentication.getOptions({ credentialId, rpId }). Returns{ challenge, options }. - Store the challenge. Same rules as registration: single-use, short-lived, server-side.
- Client: pass
optionstoAuthentication.sign({ options }). Must be user-initiated. - Client: POST the returned response to the server.
- Server: consume the stored challenge, look up the stored
publicKeybyresponse.id, then callAuthentication.verify(response, { challenge, publicKey, origin, rpId }). - On success, mint a new session (rotate any existing session ID).
- Keep challenge generation and verification as separate routes (e.g.
POST /register/options+POST /register/verify). Don't combine them into a single endpoint.
- Use separate cookies for challenges and sessions with different names and TTLs. Challenge cookies are short-lived (≤5 min) and consumed on verify. Session cookies are long-lived and persist across requests.
- Always set
HttpOnly,Secure,SameSite: 'Lax', andPath: '/'on both. Use signed cookies to prevent tampering. - Don't store user data in the session cookie. Store a session ID or credential ID, and look up the rest server-side. Keeps cookies small and avoids stale data.
- Protect authenticated routes with middleware that reads the session cookie, looks up the credential/user, and returns 401 if invalid.
- Keep it generic: check session, load user context, call next. Don't put business logic in the middleware.
- See
examples/hono/for a reference pattern.
Registration.createandAuthentication.signmust be called from a click/tap handler. Browsers reject unprompted WebAuthn calls.- Handle cancellation gracefully. Users can dismiss the passkey prompt at any time. Catch the error and let them retry without reloading the page.
- Let users register multiple passkeys so they aren't locked out if they lose a device.
- Show a passkey management UI where users can see their registered credentials (
credentialId,createdAt,lastUsedAt) and remove ones they don't recognize.
- Keep
rp.idstable. Changing your rpId invalidates all existing credentials. Pick it once.
Follow every item below for production deployments.
- WebAuthn only works over HTTPS (
localhostis the only exception). - Set
Strict-Transport-SecuritywithincludeSubDomainsandpreload. - Set
Cache-Control: no-storeon all ceremony endpoints.
- Hardcode
originandrpIdon the server. Never accept them from the client or derive from request headers. rpIdis a registrable domain (e.g.example.com), no scheme, no path. To share passkeys across subdomains, use the parent domain.originmust exactly match what the browser sends (e.g.https://app.example.com). For multiple origins, use a strict allowlist.
- Challenges are single-use. Consume (delete) the challenge on the first verification attempt, whether it succeeds or fails.
- Expire challenges after 1-5 minutes (the Hono example uses 300s).
- Store challenges server-side in a signed cookie, database, or KV store. Never trust the client to provide the challenge.
- Where possible, bind the challenge to the ceremony type (
registrationvsauthentication), the user, and the expected rpId/origin. - Issuing a new challenge should invalidate any prior outstanding challenge for the same session/operation.
If using cookies to store challenges (like the Hono example):
HttpOnly: trueto prevent JavaScript access.Secure: trueso it's only sent over HTTPS.SameSite: 'Lax'(or'Strict'if UX allows).Path: '/'or scoped narrowly to ceremony endpoints.- Short
maxAgematching your challenge TTL. - Signed cookies prevent tampering but not reading. Challenge secrecy isn't critical, but cookie theft enables replay within TTL, so keep TTLs short.
user.idmust be a stable, opaque, non-PII identifier (raw bytes, ≤64 bytes). Don't use email or username directly as the user handle since this leaks PII to authenticators.- Reuse the same
user.idfor the account across registrations.
- Explicitly set
userVerification: 'required'for passkey-grade assurance. Don't rely on defaults since they may vary across libraries and spec versions. - Set
attestation: 'none'unless you have a specific business need for device provenance. Attestation validation is complex and easy to get wrong. - Use
excludeCredentialIdsto prevent duplicate registration with the same authenticator. - If registering a credential for an existing account, require an authenticated session. Otherwise an attacker could attach their passkey to a victim's account.
- Explicitly set
userVerification: 'required'. - allowCredentials flow (username-first): set
credentialIdto scope the browser prompt. Account enumeration risk comes from whether the server reveals "user exists", so mitigate with generic responses and rate limiting. - Discoverable credentials (passkey-first): omit
credentialIdand mapresponse.idto a user server-side.
- Rotate the session ID on auth. After successful verification, mint a new session and invalidate the old one to prevent session fixation.
- Session cookies:
HttpOnly,Secure,SameSite, shortest acceptable TTL. - For sensitive actions, require recent WebAuthn verification (e.g. within 5 minutes).
- Logout:
- Stateful sessions (session ID mapped to server-side record): invalidate the record, not just the cookie.
- Stateless sessions (signed cookie): deleting the cookie is the only option. Stolen cookies remain valid until expiry. Consider a server-side revocation list for high-security apps.
- Lock down CORS on ceremony endpoints. Don't use
Access-Control-Allow-Origin: *with credentials. - If your frontend and API are on different origins, allowlist exact origins and require CSRF tokens (double-submit cookie or server-stored).
SameSitecookies alone are not sufficient for cross-origin credentialed requests.
- Set
Content-Security-Policy: frame-ancestors 'none'(or a strict allowlist) on pages that initiate ceremonies. - Consider
Permissions-Policy: publickey-credentials-create=(self), publickey-credentials-get=(self)to restrict where WebAuthn can be called.
- Return generic errors to clients. Don't distinguish between "unknown credential" and "verification failed" for unauthenticated callers since this leaks account enumeration info. Log specifics server-side only.
- Never log raw credential responses, challenges, or session cookies.
- Rate limit all ceremony endpoints per IP and per account.
- Use progressive backoff on repeated failures.
Persist per credential (minimum):
credentialId: stable identifierpublicKey: hex-encoded P-256 public key fromRegistration.verifyuserId: associated user accountcreatedAt/lastUsedAt: for audit and UX
Optional (not currently exposed by webauthx, use ox/webauthn directly if needed):
signCount: track and validate to detect cloned authenticatorstransports: for UX hints (USB, NFC, BLE, internal)backupEligible/backupState: for risk-scoring synced vs hardware-bound passkeys
- Encourage multiple credentials per account so users can register multiple passkeys/devices.
- Provide a recovery path that isn't weaker than your threat model (e.g. verified email + rate limiting, or recovery codes).