Skip to main content

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, and audiences
  • the verified signing kid and algorithm
  • 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, and exp are required.
  • iat, exp, and optional nbf must 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 JWK alg must agree.
  • Symmetric, private, weak RSA, incompatible EC/OKP, and non-verification JWKs are rejected.
  • Duplicate JWK IDs are rejected.
  • An unknown kid is rejected without causing an outbound request.
  • UserInfo sub must exactly match the verified token sub.
  • 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:

  1. Publish the next public key at least one complete JWKS TTL before using it to sign tokens.
  2. Keep an old public key published until all tokens signed by it have expired, including leeway.
  3. Avoid changing jwks_uri without 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

fastapi_oidc_guard-0.1.0.tar.gz (20.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

fastapi_oidc_guard-0.1.0-py3-none-any.whl (25.5 kB view details)

Uploaded Python 3

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

Hashes for fastapi_oidc_guard-0.1.0.tar.gz
Algorithm Hash digest
SHA256 4fee88483c8485f4aeb91e5de8c1f79b8df74377f268c9a6d44875afd8c68170
MD5 a4399c7c3d4de2d195eca4d0d1269c97
BLAKE2b-256 b77e402f775d8d61744dce1e38834f386b56c6c52275a968e4a2285638ce6a00

See more details on using hashes here.

File details

Details for the file fastapi_oidc_guard-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for fastapi_oidc_guard-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 c8e720473257b57f03a296bfbe3391dd0ef8ca8a289336e1085e2ab4be73efbc
MD5 cfa5abccf7333a5d23d8ab45367ae980
BLAKE2b-256 33d35cd3b5fe91b92650c2961ead503ea4c7245ddcebfc67b1c715d56e273652

See more details on using hashes here.

Release history Release notifications | RSS feed

0.4.0

2 files

0.3.0

2 files

0.2.0

2 files

This release

0.1.0 This release

2 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