Identity and users#
Authentication answers who called. Route declarations answer whether proof is required. Keep both explicit, and make forgotten declarations a startup error.
app.pypython
from wreath import Request, Wreath
from wreath.auth import BearerTokenBackend, Identity, authenticated, public
identities = {
"dev-alice": Identity("alice", roles=frozenset({"member"})),
"dev-operator": Identity("operator", roles=frozenset({"operator"})),
}
def verify_token(token: str) -> Identity | None:
return identities.get(token)
app = Wreath(require_access_declarations=True)
app.configure_auth(BearerTokenBackend(verify_token))
@app.get("/health")
@public()
async def health(request: Request) -> dict:
return {"status": "ok"}
@app.get("/me")
@authenticated()
async def me(request: Request) -> dict:
return {
"id": request.identity.id,
"roles": sorted(request.identity.roles),
}test_app.pypython
from wreath.testing import TestClient
from app import app
async def test_public_work_does_not_need_credentials() -> None:
async with TestClient(app) as client:
response = await client.get("/health")
assert response.status == 200
async def test_private_work_challenges_then_publishes_identity() -> None:
async with TestClient(app) as client:
missing = await client.get("/me")
invalid = await client.get(
"/me",
headers={"authorization": "Bearer unknown"},
)
allowed = await client.get(
"/me",
headers={"authorization": "Bearer dev-operator"},
)
assert missing.status == invalid.status == 401
assert allowed.json() == {"id": "operator", "roles": ["operator"]}Replace the development mapping with a constant-time token lookup, JWT verifier or session backend. Do not put a database query in public routes: Wreath only invokes the backend when a route's compiled access requirement needs identity.
For registration, verification, reset, sessions, TOTP and WebAuthn, mount user_router and second_factor_router; the complete path is exercised in the serious API story. SAML, OIDC, SCIM, Cedar and tenant boundaries live in the identity reference.