Passkeys (WebAuthn)¶
Sign in with Touch ID, Face ID, Windows Hello, or a hardware key. Requires the webauthn extra.
Setup¶
from fastauth.providers.passkey import PasskeyProvider
from fastauth.session_backends.memory import MemorySessionBackend
config = FastAuthConfig(
...,
providers=[
CredentialsProvider(), # users need a way to register first
PasskeyProvider(
rp_id="example.com", # your domain (no scheme, no port)
rp_name="My App",
origin="https://example.com", # may be a list[str]
),
],
passkey_adapter=adapter.passkey,
passkey_state_store=MemorySessionBackend(), # use Redis in production
)
rp_idmust match the host of your deployed site (e.g.example.com). Browsers reject credentials whose RP ID does not match the origin.originis the HTTPS origin(s) allowed for the WebAuthn ceremony. Local development may usehttp://localhost:8000.passkey_state_storeholds registration and authentication challenges (two key families:pk_reg:{challenge}andpk_auth:{challenge}, TTL 300s). Multi-worker deployments must use a shared backend.
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 |
Registration flow¶
- Authenticated user calls
POST /auth/passkeys/register/begin. FastAuth returns WebAuthn registration options including excluded credentials (existing passkeys for the user). Challenge is stored atpk_reg:{challenge}keyed by{user_id, challenge}. - Browser calls
navigator.credentials.create({publicKey: options}). - Send the resulting credential JSON to
/auth/passkeys/register/completewith an optional friendlyname. FastAuth verifies the challenge (stored.user_id == user.id), stores the public key + sign count + aaguid, and fireson_passkey_registered.
Authentication flow¶
POST /auth/passkeys/authenticate/beginwith optional{email}. Ifemailis supplied, FastAuth sends anallowCredentialslist scoped to that user's passkeys; if not, a discoverable (resident-key) ceremony is used.- Browser calls
navigator.credentials.get({publicKey: options}). - Send
{credential}to/auth/passkeys/authenticate/complete. FastAuth verifies, updatessign_countandlast_used_at, and issues tokens.allow_signinruns; returningFalse→403.
Hook reach caveat:
on_signinis currently fired only in JSON token mode for passkey complete. In cookie mode the cookies are set and aMessageResponsereturned withouton_signin. If you rely onon_signinfor audit logging, either use JSON mode or log insideon_passkey_registered/ your own middleware. (needs maintainer clarification on whether this is intentional)
Hooks¶
on_passkey_registered(user, passkey)on_passkey_deleted(user, passkey)allow_signin(user, provider)—False→403on_signin— JSON mode only (see caveat above)
Common mistakes¶
rp_id≠ site host — browsers reject the ceremony withSecurityError.- HTTP in production — WebAuthn requires a secure context. Browsers treat
localhostas a secure context even over HTTP, for development. - Multi-worker +
MemorySessionBackend— challenge written by worker A is invisible to worker B → ceremony fails. Use Redis. - Forgetting
passkey_adapter=adapter.passkey— the passkeys router raisesConfigErrorat mount time ifpasskey_adapterorpasskey_state_storeisNone.