Guide: Passkeys¶
Add passkey (WebAuthn) sign-in. See examples/passkeys/ for a working example with browser templates.
Install¶
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_idmust match your site's hostname.localhostworks in dev.- Use
RedisSessionBackendin production — workers share challenge state. on_signinis not fired in cookie mode for passkey complete — see Passkeys provider. Audit accordingly.- See Passkeys feature for the management surface.