Skip to main content

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:

  1. Adds starlette.middleware.authentication.AuthenticationMiddleware wired to a KeycardAuthBackend so every request gets a populated request.user and request.auth.
  2. 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

  • AuthenticationMiddleware with KeycardAuthBackend
  • /.well-known/oauth-protected-resource (RFC 9728)
  • /.well-known/oauth-authorization-server (RFC 8414)
  • /.well-known/jwks.json (only when WebIdentity is 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)

Source distribution for keycardai-starlette 0.13.0
File Size Uploaded
keycardai_starlette-0.13.0.tar.gz 70.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for keycardai-starlette 0.13.0
File Interpreter ABI Platform
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}

Release history Release notifications | RSS feed

0.14.2

2 release files

0.14.1

2 release files

This release

0.13.0 This release

2 release files

0.12.0

2 release files

0.11.0

2 release files

0.10.0

2 release files

0.9.0

2 release files

0.8.0

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page