fastapi-oidc-guard
Strict OIDC bearer-token authentication for FastAPI resource servers.
The library validates externally issued JWT user access tokens using OIDC discovery, cached JWKS, PyJWT, and a mandatory UserInfo request. Authentication is opt-in through a typed FastAPI dependency. It does not implement browser login, authorization-code flows, sessions, PKCE, machine-to-machine tokens, or providers whose JWT access tokens cannot call UserInfo.
Installation
pip install fastapi-oidc-guard
Python 3.12 or newer is required.
Usage
import contextlib
import dataclasses
from fastapi import FastAPI
from fastapi_oidc_guard import Authenticated, OidcConfig, VerifiedIdentity, authentication_context
@dataclasses.dataclass(frozen=True, slots=True)
class User:
id: str
name: str
async def resolve_user(identity: VerifiedIdentity) -> User | None:
userinfo = identity.userinfo
if userinfo.extra.get('role') != 'myrole':
return None
return User(id=identity.sub, name=userinfo.name or '')
@contextlib.asynccontextmanager
async def lifespan(app: FastAPI):
async with authentication_context(
app, OidcConfig(issuer='https://issuer.example', audience='project-id'), resolve_user
):
yield
app = FastAPI(lifespan=lifespan)
@app.get('/public')
async def public_endpoint():
return {'message': 'This could be anyone'}
@app.get('/authenticated')
async def authenticated_endpoint(user: Authenticated[User]):
return {'message': f'Welcome back {user.name}!'}
authentication_context eagerly downloads and validates discovery metadata and JWKS during
application startup. Discovery must advertise authorization_endpoint, token_endpoint,
jwks_uri, and userinfo_endpoint, and it must support the authorization-code flow. The resulting
values seed the same TTL caches used while requests are served. Startup fails when the provider is
unavailable or its metadata is invalid.
The user resolver must be async. It receives a fully verified VerifiedIdentity and returns the
application's concrete user object. Returning None denies access with HTTP 403.
Verified Identity
VerifiedIdentity retains information needed by downstream authorization policies:
sub,issued_at,expires_at,issuer, andaudiences- the verified signing
kidandalgorithm - all verified JWT claims as a recursively immutable mapping
- typed, non-optional
userinfo
Known UserInfo fields are available as attributes. Additional non-null JSON claims are retained
in userinfo.extra.
Configuration
from datetime import timedelta
from fastapi_oidc_guard import OidcConfig
config = OidcConfig(
issuer='https://issuer.example',
audience='project-id',
allowed_algorithms=(
'RS256',
'RS384',
'RS512',
'PS256',
'PS384',
'PS512',
'ES256',
'ES384',
'ES512',
'EdDSA',
),
discovery_cache_ttl=timedelta(minutes=15),
jwks_cache_ttl=timedelta(minutes=15),
userinfo_cache_ttl=timedelta(minutes=5),
userinfo_cache_max_entries=1024,
leeway=timedelta(seconds=30),
max_token_lifetime=None,
expected_token_type=None,
openapi_scopes=None,
swagger_ui_client_id=None,
http_timeout=timedelta(seconds=5),
allow_insecure_http=False,
)
Supported algorithms are RS256, RS384, RS512, PS256, PS384, PS512, ES256,
ES384, ES512, and EdDSA. All are enabled by default. Only configured algorithms are
accepted; provider metadata cannot expand this allowlist. Applications can narrow it to the exact
algorithm or algorithms used by their provider.
Set expected_token_type='at+jwt' when the provider emits RFC 9068 access tokens. It remains
optional because many providers omit typ or use a provider-specific value.
HTTP is rejected by default. allow_insecure_http=True exists for explicit local-development
setups and should not be enabled in production.
OpenAPI
Routes using Authenticated[...] reference an OidcBearer OAuth2 authorization-code security
scheme. Its authorization URL, token URL, and scopes come from the cached OIDC discovery response,
so generating OpenAPI does not make another provider request. When discovery omits the optional
scopes_supported field, the scheme advertises only openid.
Set openapi_scopes to replace the discovered scope list:
config = OidcConfig(
issuer='https://issuer.example',
audience='project-id',
openapi_scopes=('openid', 'email', 'profile', 'urn:example:custom'),
)
The override must contain unique OAuth2 scope names and include openid. It affects only the
generated OpenAPI authorization flow; it does not validate a token's scope claim or grant
permissions. Enforce application authorization in the async identity mapper.
Set swagger_ui_client_id for a public documentation client:
config = OidcConfig(
issuer='https://issuer.example', audience='project-id', swagger_ui_client_id='swagger-ui'
)
The configured client ID replaces any clientId already present in FastAPI's
swagger_ui_init_oauth. Other application-owned Swagger settings are preserved, and PKCE is
enabled unless the application explicitly configures usePkceWithAuthorizationCodeGrant. Client
secrets are intentionally unsupported because Swagger UI is a browser-based public client.
Validation Policy
The hardened policy is not configurable down to unsafe compatibility behavior:
iss,aud,sub,iat, andexpare required.iat,exp, and optionalnbfmust be JSON integers; booleans, floats, and strings fail.- Discovery's issuer must exactly match the configured issuer.
- JWT header
alg, the local allowlist, and an explicit JWKalgmust agree. - Symmetric, private, weak RSA, incompatible EC/OKP, and non-verification JWKs are rejected.
- Duplicate JWK IDs are rejected.
- An unknown
kidis rejected without causing an outbound request. - UserInfo
submust exactly match the verified tokensub. - Raw bearer tokens and Authorization headers are never included in library errors.
UserInfo is cached by a SHA-256 token fingerprint, not by subject. The bounded five-minute cache is capped by token expiration and deduplicates concurrent fetches.
Key Rotation
JWKS is fetched only at application startup and when jwks_cache_ttl expires. Token-controlled
values, including an unknown kid, never trigger a network refresh.
For rotation without rejected tokens, the identity provider must:
- Publish the next public key at least one complete JWKS TTL before using it to sign tokens.
- Keep an old public key published until all tokens signed by it have expired, including leeway.
- Avoid changing
jwks_uriwithout allowing both discovery and JWKS caches to refresh first.
Emergency or unannounced rotations can cause authentication failures until the cache expires. This is intentional: provider key management, rather than attacker-controlled token headers, determines when network refreshes occur.
UserInfo Contract
Every accepted token must be a user access token that the discovered userinfo_endpoint accepts.
The library does not support providers without that endpoint, client-credentials tokens, or JWTs
issued for an API audience that cannot also call UserInfo. Appropriate provider scopes, commonly
openid, profile, and email, must be granted when the corresponding claims are required.
UserInfo is fetched successfully and its sub is matched against the JWT before the application
mapper runs. Therefore VerifiedIdentity.userinfo is never None. UserInfo failure rejects the
request instead of producing a partial identity.
Configuration Parsing
OidcConfig uses Pydantic's default extra-field behavior, which ignores unknown fields. It does
not impose an extra-field policy on an application's root configuration. Pydantic treats nested
models as separate configuration boundaries, so applications that need another policy can
subclass it:
class StrictOidcConfig(OidcConfig, extra='forbid', frozen=True):
pass
Errors
| Condition | Response |
|---|---|
| Missing credentials | 401, WWW-Authenticate: Bearer |
| Malformed or invalid token | 401, WWW-Authenticate: Bearer error="invalid_token" |
Resolver returns None |
403 |
| Malformed provider response | 502 |
| Provider timeout, rate limit, or 5xx | 503 |
| Missing lifespan or mapped-user type mismatch | 500 |
Bodies use FastAPI's stable {'detail': '...'} format without PyJWT, provider, token, claim, or
key details.
Typing
For Pyright, Authenticated[User] is the same static type as User. At runtime it expands to a
normal Annotated[User, Depends(...)] dependency and validates the returned value with
isinstance.
The type argument must therefore be a concrete runtime-checkable class. Unions, parameterized
generics, Any, TypedDict, and non-runtime-checkable protocols are not supported.
Development
uv sync
uv run pyright
uv run ruff format --check .
uv run ruff check .
uv run pytest
uv run behave
uv build
Pyright runs in strict mode using its Node.js extra. Ruff checks all rules except return
annotations, docstrings, security, and lazy-import rules; formatting uses spaces, LF line endings,
a 100-character line length, single quotes, and no magic trailing comma. Pytest covers technical
components and API-level behavior, while Behave covers business-level authentication outcomes.
GitLab CI runs the same quality checks and builds the package from .gitlab-ci.yml.
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 fastapi_oidc_guard-0.1.0.tar.gz.
File metadata
- Download URL: fastapi_oidc_guard-0.1.0.tar.gz
- Upload date:
- Size: 20.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4fee88483c8485f4aeb91e5de8c1f79b8df74377f268c9a6d44875afd8c68170
|
|
| MD5 |
a4399c7c3d4de2d195eca4d0d1269c97
|
|
| BLAKE2b-256 |
b77e402f775d8d61744dce1e38834f386b56c6c52275a968e4a2285638ce6a00
|
File details
Details for the file fastapi_oidc_guard-0.1.0-py3-none-any.whl.
File metadata
- Download URL: fastapi_oidc_guard-0.1.0-py3-none-any.whl
- Upload date:
- Size: 25.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c8e720473257b57f03a296bfbe3391dd0ef8ca8a289336e1085e2ab4be73efbc
|
|
| MD5 |
cfa5abccf7333a5d23d8ab45367ae980
|
|
| BLAKE2b-256 |
33d35cd3b5fe91b92650c2961ead503ea4c7245ddcebfc67b1c715d56e273652
|