Skip to content

Guide: Passkeys

Add passkey (WebAuthn) sign-in. See examples/passkeys/ for a working example with browser templates.

Install

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

Server

from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastauth import FastAuth, FastAuthConfig
from fastauth.adapters.sqlalchemy import SQLAlchemyAdapter
from fastauth.providers.credentials import CredentialsProvider
from fastauth.providers.passkey import PasskeyProvider
from fastauth.session_backends.memory import MemorySessionBackend

adapter = SQLAlchemyAdapter(engine_url="sqlite+aiosqlite:///./auth.db")

config = FastAuthConfig(
    secret="change-me-in-production-min-32-bytes!!",
    providers=[
        CredentialsProvider(),                                          # users need a way to register first
        PasskeyProvider(rp_id="localhost", rp_name="Passkeys Demo",
                        origin="http://localhost:8000"),
    ],
    adapter=adapter.user,
    token_adapter=adapter.token,
    passkey_adapter=adapter.passkey,
    passkey_state_store=MemorySessionBackend(),                         # use Redis in production
    token_delivery="cookie",
    debug=True,                                                        # localhost only
)

auth = FastAuth(config)

@asynccontextmanager
async def lifespan(app: FastAPI):
    await adapter.create_tables()
    yield

app = FastAPI(lifespan=lifespan)
auth.mount(app)

Browser

// Register a new passkey against the authenticated account.
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 Touch ID" }),
});

// Authenticate (no prior auth needed).
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 }),
});

Notes

  • rp_id must match your site's hostname. localhost works in dev.
  • Use RedisSessionBackend in production — workers share challenge state.
  • on_signin is not fired in cookie mode for passkey complete — see Passkeys provider. Audit accordingly.
  • See Passkeys feature for the management surface.