Skip to content

Passkeys (WebAuthn) feature

For setup and the provider constructor, see Passkeys provider. This page covers the management surface and security notes.

Endpoints

Method Path Auth Request Response
POST /auth/passkeys/register/begin yes WebAuthn registration options (JSON)
POST /auth/passkeys/register/complete yes {credential, name?} 201 {id, name, created_at}
GET /auth/passkeys yes [{id,name,aaguid,created_at,last_used_at}]
DELETE /auth/passkeys/{credential_id} yes 204
POST /auth/passkeys/authenticate/begin {email?} (optional) WebAuthn auth options (JSON)
POST /auth/passkeys/authenticate/complete {credential} cookie: MessageResponse; json: TokenResponse

Managing passkeys

A user registers passkeys against their authenticated account:

# Begin — fetch registration options, then call navigator.credentials.create on the client
curl -X POST http://localhost:8000/auth/passkeys/register/begin -H "Authorization: Bearer $ACCESS"

# Complete — submit the credential, optionally with a friendly name
curl -X POST http://localhost:8000/auth/passkeys/register/complete \
  -H "Authorization: Bearer $ACCESS" -H "Content-Type: application/json" \
  -d '{"credential": {...}, "name": "MacBook Touch ID"}'

# List
curl http://localhost:8000/auth/passkeys -H "Authorization: Bearer $ACCESS"

# Delete
curl -X DELETE http://localhost:8000/auth/passkeys/<credential_id> -H "Authorization: Bearer $ACCESS"

Hooks: on_passkey_registered, on_passkey_deleted.

Security notes

  • Passkeys store a per-credential sign_count. FastAuth updates it after every authenticate/complete; a higher sign_count on the authenticator than in the DB is a signal the credential was used on a different authenticator. FastAuth does not auto-revoke in this case — monitor and revoke manually if you care.
  • Registration is gated by require_auth. Users cannot register a passkey to someone else's account. Authentication (/authenticate/begin, /authenticate/complete) is unauthenticated.
  • authenticate/begin with email optional. If your frontend knows the user, send the email to scope allowCredentials (interoperable with non-discoverable authenticators). If omitted, a discoverable (resident-key) flow is used.
  • on_signin cookie-mode caveat — see Passkeys provider. Use JSON mode if you need audit logging on every sign-in.

Browser integration

A minimal client-side sketch:

// register/begin
const opts = await fetch("/auth/passkeys/register/begin", {
  method: "POST", credentials: "include",
}).then(r => r.json());
const cred = await navigator.credentials.create({ publicKey: opts });
await fetch("/auth/passkeys/register/complete", {
  method: "POST", credentials: "include",
  headers: {"Content-Type": "application/json"},
  body: JSON.stringify({ credential: cred.response, name: "MacBook" }),
});

// authenticate/begin → complete
const opts = await fetch("/auth/passkeys/authenticate/begin", {
  method: "POST", credentials: "include",
}).then(r => r.json());
const cred = await navigator.credentials.get({ publicKey: opts });
await fetch("/auth/passkeys/authenticate/complete", {
  method: "POST", credentials: "include",
  headers: {"Content-Type": "application/json"},
  body: JSON.stringify({ credential: cred.response }),
});

See examples/passkeys/ for a working example with templates.