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.

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:

  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.14.2

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.14.2
File Size Uploaded
keycardai_starlette-0.14.2.tar.gz 72.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for keycardai-starlette 0.14.2
File Interpreter ABI Platform
keycardai_starlette-0.14.2-py3-none-any.whl Python 3 none any Details

Total release size: 98.7 kB

Release files / keycardai_starlette-0.14.2.tar.gz

Download URL keycardai_starlette-0.14.2.tar.gz
Size 72.9 kB
Tags Source
SHA-256 checksum
How to use checksums
c5cd5225f0aa51f91c636f8b22acae37ca3d1cbc4be7a55590da621c5587b2b5
BLAKE2b-256 checksum
How to use checksums
ecba4ba43b669ec93a0ce2348f63ffaf8cf5b478534e29d9adf5f464b89c2ef0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.15 {"installer":{"name":"uv","version":"0.12.15","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.2-py3-none-any.whl

Download URL keycardai_starlette-0.14.2-py3-none-any.whl
Size 25.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
0d304c83f015c7983c8c655591b1218fb5c865546cb0d4c48db90f3d90c8a7ad
BLAKE2b-256 checksum
How to use checksums
900b1a61b7c984f1aaec7a7c6ac8f804708a54a479f388137c5e195eb0bc06df
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.15 {"installer":{"name":"uv","version":"0.12.15","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

This release

0.14.2 This release

2 release files

0.14.1

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