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.
Configuration from the environment
AuthProvider reads its zone and application credential from the environment
when they are not passed explicitly.
Zone: set KEYCARD_ZONE_URL to the full zone URL. KEYCARD_ZONE_ID and
KEYCARD_BASE_URL still work but emit a DeprecationWarning; migrate as
follows:
| Deprecated | Canonical replacement |
|---|---|
KEYCARD_ZONE_ID="abc1234" |
KEYCARD_ZONE_URL="https://abc1234.keycard.cloud" |
KEYCARD_ZONE_ID="abc1234" + KEYCARD_BASE_URL="https://custom.example.com" |
KEYCARD_ZONE_URL="https://abc1234.custom.example.com" |
Application credential: discovery is delegated to
keycardai.oauth.server.discover_credential. KEYCARD_APPLICATION_CREDENTIAL_TYPE
selects client_secret, workload_identity or web_identity
(eks_workload_identity is accepted as a legacy alias). Without a selector,
exactly one source may be configured: KEYCARD_CLIENT_ID +
KEYCARD_CLIENT_SECRET, an injected workload token file
(AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE, AWS_WEB_IDENTITY_TOKEN_FILE,
AZURE_FEDERATED_TOKEN_FILE or KEYCARD_EKS_WORKLOAD_IDENTITY_TOKEN_FILE), or
KEYCARD_WEB_IDENTITY_KEY_STORAGE_DIR. More than one source with no selector
fails at startup with an ambiguity error naming the selector as the remedy.
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.14.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.14.0.tar.gz | 72.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| keycardai_starlette-0.14.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 97.6 kB
Release files / keycardai_starlette-0.14.0.tar.gz
| Download URL | keycardai_starlette-0.14.0.tar.gz |
|---|---|
| Size | 72.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b066ae7978d3a0d6ee557a603af1ba35e85e7f0dcb557e16e4a5d94f2db8b649
|
|
BLAKE2b-256 checksum How to use checksums |
4c0b1e1523ae3744f558e61d8c5573d7ed1125c3514614b517f5118cf87a4b7a
|
| 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.14.0-py3-none-any.whl
| Download URL | keycardai_starlette-0.14.0-py3-none-any.whl |
|---|---|
| Size | 25.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
37fcd692a823625bda9bc086649a91927cb66c6bb2b9032f71f865aa537080ce
|
|
BLAKE2b-256 checksum How to use checksums |
4cbe02f5150802173c4e55f693933f90dd8b03c2a8b104654988df7c5b9a5224
|
| 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}
|