Skip to content

Passkeys (WebAuthn)

Sign in with Touch ID, Face ID, Windows Hello, or a hardware key. Requires the webauthn extra.

pip install "sreekarnv-fastauth[standard,webauthn]"

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_id must match the host of your deployed site (e.g. example.com). Browsers reject credentials whose RP ID does not match the origin.
  • origin is the HTTPS origin(s) allowed for the WebAuthn ceremony. Local development may use http://localhost:8000.
  • passkey_state_store holds registration and authentication challenges (two key families: pk_reg:{challenge} and pk_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

  1. Authenticated user calls POST /auth/passkeys/register/begin. FastAuth returns WebAuthn registration options including excluded credentials (existing passkeys for the user). Challenge is stored at pk_reg:{challenge} keyed by {user_id, challenge}.
  2. Browser calls navigator.credentials.create({publicKey: options}).
  3. Send the resulting credential JSON to /auth/passkeys/register/complete with an optional friendly name. FastAuth verifies the challenge (stored.user_id == user.id), stores the public key + sign count + aaguid, and fires on_passkey_registered.

Authentication flow

  1. POST /auth/passkeys/authenticate/begin with optional {email}. If email is supplied, FastAuth sends an allowCredentials list scoped to that user's passkeys; if not, a discoverable (resident-key) ceremony is used.
  2. Browser calls navigator.credentials.get({publicKey: options}).
  3. Send {credential} to /auth/passkeys/authenticate/complete. FastAuth verifies, updates sign_count and last_used_at, and issues tokens. allow_signin runs; returning False403.

Hook reach caveat: on_signin is currently fired only in JSON token mode for passkey complete. In cookie mode the cookies are set and a MessageResponse returned without on_signin. If you rely on on_signin for audit logging, either use JSON mode or log inside on_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)False403
  • on_signin — JSON mode only (see caveat above)

Common mistakes

  • rp_id ≠ site host — browsers reject the ceremony with SecurityError.
  • HTTP in production — WebAuthn requires a secure context. Browsers treat localhost as 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 raises ConfigError at mount time if passkey_adapter or passkey_state_store is None.