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 highersign_counton 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/beginwithemailoptional. If your frontend knows the user, send the email to scopeallowCredentials(interoperable with non-discoverable authenticators). If omitted, a discoverable (resident-key) flow is used.on_signincookie-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.