Starlette/FastAPI middleware and route builders for protecting HTTP APIs with Keycard OAuth
Project description
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")),
)
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
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(),
))
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.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file keycardai_starlette-0.4.0.tar.gz.
File metadata
- Download URL: keycardai_starlette-0.4.0.tar.gz
- Upload date:
- Size: 62.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.8 {"installer":{"name":"uv","version":"0.11.8","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}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
77257184afd10cac080a812357129cb82f307f3de2da2c47a654b6498818bf9b
|
|
| MD5 |
97410d70dd24bc6dcf39b851975a18f4
|
|
| BLAKE2b-256 |
587414c37b10c2321a765ba10272dc959669b8d059a0079d62eb7cbb803e7393
|
File details
Details for the file keycardai_starlette-0.4.0-py3-none-any.whl.
File metadata
- Download URL: keycardai_starlette-0.4.0-py3-none-any.whl
- Upload date:
- Size: 21.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.8 {"installer":{"name":"uv","version":"0.11.8","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}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
aed6aa2c97f7113e21692fc9bb24768b5efc1d01a6215435b3545842b01c44dc
|
|
| MD5 |
ff5723d08cd2b78fdf19281f32b782fc
|
|
| BLAKE2b-256 |
9e001fc92504eceb0b65c907dafb0a2c07b984f34fcd26b9163e1318a5c506c8
|