keycardai-starlette
Starlette/FastAPI integration for Keycard. Plugs into Starlette's standard
authentication framework: an AuthenticationBackend populates request.user
and request.auth, the @requires decorator gates routes, and
@auth.grant(resource) performs delegated OAuth 2.0 token exchange.
Installation
pip install keycardai-starlette
Quick Start
from fastapi import FastAPI, Request
from keycardai.starlette import AuthProvider, KeycardUser, requires
from keycardai.oauth.server import AccessContext, ClientSecret
auth = AuthProvider(
zone_id="your-zone-id",
application_credential=ClientSecret(("client_id", "client_secret")),
# Resource indicator (RFC 8707) Keycard mints tokens for. The verifier
# rejects tokens whose "aud" claim does not include this value.
audience="https://your-api.example.com",
)
app = FastAPI()
auth.install(app) # AuthenticationMiddleware + /.well-known/* routes
@app.get("/health")
async def health():
return {"ok": True} # public, no decorator
@app.get("/api/me")
@requires("authenticated") # standard Starlette gating
async def me(request: Request):
user: KeycardUser = request.user
return {"client_id": user.client_id, "scopes": list(request.auth.scopes)}
@app.get("/api/data")
@requires("authenticated")
@auth.grant("https://api.example.com") # delegated token exchange (RFC 8693)
async def get_data(request: Request, access: AccessContext):
token = access.access("https://api.example.com").access_token
Leaving audience unset disables the audience check: the verifier accepts any
token minted by the zone regardless of its aud claim.
How it integrates with Starlette
AuthProvider.install(app) does two things:
- Adds
starlette.middleware.authentication.AuthenticationMiddlewarewired to aKeycardAuthBackendso every request gets a populatedrequest.userandrequest.auth. - Mounts the OAuth discovery endpoints under
/.well-known/.
Routes you do not decorate stay public: the backend returns None (anonymous
user) when no Authorization header is present, exactly like
starlette.authentication.UnauthenticatedUser. Routes that need a verified
caller use the @requires(...) decorator.
Decorators
@requires(scopes)
keycardai.starlette.requires is a drop-in for
starlette.authentication.requires with one difference: anonymous requests
get an RFC 6750 401 response with a
WWW-Authenticate: Bearer ... resource_metadata="..." header (RFC 9728)
instead of stock HTTPException(403). Scope checks behave the same.
@requires("authenticated") # any verified caller
@requires(["authenticated", "admin"]) # additional scope check
AuthProvider.requires is exposed as a static-method alias if you prefer
accessing the decorator via the provider instance:
@auth.requires("authenticated")
@auth.grant(resource)
Performs OAuth 2.0 delegated token exchange (RFC 8693) for one or more
downstream resources and injects an AccessContext parameter into the
endpoint. Mirrors the @grant() decorator from keycardai-mcp so the
decorator name is consistent across packages.
@app.get("/api/calendar")
@requires("authenticated")
@auth.grant("https://graph.microsoft.com")
async def calendar(request: Request, access: AccessContext):
token = access.access("https://graph.microsoft.com").access_token
Errors from the exchange are stored per-resource on the AccessContext
rather than raised: call access.has_errors() / access.get_errors() to
decide how to respond. The AccessContext parameter is hidden from FastAPI
introspection via __signature__ rewriting, so it never appears in the
generated OpenAPI schema.
Other entry points
protected_router()
Mount any ASGI app behind Keycard authentication and the /.well-known/*
metadata routes in one call. Useful when every route under some prefix needs
the same protection (for example an MCP transport, an internal admin app).
from keycardai.starlette import protected_router
from starlette.applications import Starlette
inner = build_my_api() # any ASGI app
app = Starlette(routes=protected_router(
issuer=auth.issuer,
app=inner,
verifier=auth.get_token_verifier(),
))
Opaque sub-apps: require_authentication=True
@requires(...) only gates routes you decorate. A mounted sub-app that
handles its own routing (an MCP JSONRPC dispatcher, a gRPC handler, any
non-Starlette ASGI app) bypasses route decorators, so anonymous requests
would fall through to it. Pass require_authentication=True to make the
backend itself the gate: requests without an Authorization header get an
RFC 6750 401 challenge instead of reaching the sub-app anonymously.
app = Starlette(routes=protected_router(
issuer=auth.issuer,
app=inner,
verifier=auth.get_token_verifier(),
require_authentication=True, # every request to `inner` needs a token
))
The same flag exists on KeycardAuthBackend(verifier, require_authentication=True)
when you register the middleware yourself. OAuth metadata paths under
/.well-known/ stay public either way (RFC 9728 §2, RFC 8414 §3).
AuthenticationMiddleware directly
For full control over middleware ordering, register the standard Starlette middleware yourself:
from starlette.middleware.authentication import AuthenticationMiddleware
from keycardai.starlette import KeycardAuthBackend, keycard_on_error
app.add_middleware(
AuthenticationMiddleware,
backend=KeycardAuthBackend(auth.get_token_verifier()),
on_error=keycard_on_error,
)
What install() adds
AuthenticationMiddlewarewithKeycardAuthBackend/.well-known/oauth-protected-resource(RFC 9728)/.well-known/oauth-authorization-server(RFC 8414)/.well-known/jwks.json(only whenWebIdentityis configured)
The middleware never gates access on its own; it just populates
request.user / request.auth. Routes you do not decorate stay public.
Release files for keycardai-starlette 0.13.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| keycardai_starlette-0.13.0.tar.gz | 70.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| keycardai_starlette-0.13.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 95.6 kB
Release files / keycardai_starlette-0.13.0.tar.gz
| Download URL | keycardai_starlette-0.13.0.tar.gz |
|---|---|
| Size | 70.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
2140b6c5f793273d8ef0b48fab0edb4c037179d61f57fe61fd6f0ceeb787a0c3
|
|
BLAKE2b-256 checksum How to use checksums |
d72aed80e3241570fa48c65b7c569d572a9fc84a6179f080b955db58ee30cc46
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.10 {"installer":{"name":"uv","version":"0.12.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / keycardai_starlette-0.13.0-py3-none-any.whl
| Download URL | keycardai_starlette-0.13.0-py3-none-any.whl |
|---|---|
| Size | 25.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
39f63fc10ee7cccb9c0b4d87ac67375502fbe0d3ba1e038375b9651b3a7bdbc3
|
|
BLAKE2b-256 checksum How to use checksums |
17c3bd3e85f6fae6f63ff9514a06054178c98f5d3da33c9f76565274d9f88ed9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.10 {"installer":{"name":"uv","version":"0.12.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|