Generic OIDC Provider

OidcProvider is a JWT authentication provider for any standards-compliant OpenID Connect identity provider — Authentik, Keycloak, Auth0, Okta, or any other issuer that exposes a .well-known/openid-configuration discovery document and a JWKS endpoint. Unlike AWS Cognito Provider and Azure Entra ID Authentication Provider, it is not tied to a specific cloud vendor.

Overview

The OIDC provider handles:

  • JWKS discovery — the signing keys are fetched automatically from {issuer}/.well-known/openid-configuration (or an explicit jwks_uri), and cached the same way as the other providers.

  • Signature verification and validation of the exp/nbf claims (with configurable clock-skew leeway).

  • Issuer validation (iss) — always enforced.

  • Audience validation (aud) — enforced when audience is configured. Strongly recommended: without it, any token signed by the issuer for any client of that issuer is accepted.

  • Claim mapping — the claims used for the user’s display name and groups are configurable, since different providers name them differently (OIDC’s own convention is preferred_username and groups, which are also Authentik’s defaults).

Configuration

Basic Configuration

from auth_middleware import JwtAuthMiddleware
from auth_middleware.providers.oidc.oidc_provider import OidcProvider
from auth_middleware.providers.oidc.oidc_provider_settings import (
    OidcProviderSettings,
)

# Configure the OIDC settings — this example targets an Authentik
# application, but the same settings work for any OIDC-compliant IdP.
auth_settings = OidcProviderSettings(
    issuer="https://authentik.example.com/application/o/my-app/",
    audience="your-oidc-client-id",
)

# Create the provider
auth_provider = OidcProvider(settings=auth_settings)

# Add to FastAPI application
app.add_middleware(JwtAuthMiddleware, auth_provider=auth_provider)

Warning

audience is optional but strongly recommended, for the same reason as Cognito’s user_pool_client_id: without it, the provider only checks that the token was signed by the configured issuer, not which client it was issued for.

Explicit JWKS URL

By default the JWKS URL is discovered from the issuer’s OIDC discovery document on first use, and cached afterwards. If your IdP doesn’t expose standard discovery, or you want to skip that extra request, set jwks_uri explicitly:

auth_settings = OidcProviderSettings(
    issuer="https://authentik.example.com/application/o/my-app/",
    audience="your-oidc-client-id",
    jwks_uri="https://authentik.example.com/application/o/my-app/jwks/",
)

Claim Mapping

Different identity providers use different claim names for the user’s display name and group memberships. Configure them to match your IdP:

auth_settings = OidcProviderSettings(
    issuer="https://idp.example.com/",
    audience="your-client-id",
    username_claim="upn",       # default: "preferred_username"
    groups_claim="roles",       # default: "groups"
)

Set groups_claim=None to disable reading groups from the token claims entirely — useful if groups should come from a separate GroupsProvider instead (e.g. Groups Provider backed by SQL).

Clock-Skew Leeway

If the server clocks of your application and identity provider can drift, allow a small grace period when validating exp/nbf:

auth_settings = OidcProviderSettings(
    issuer="https://idp.example.com/",
    audience="your-client-id",
    jwt_leeway=30,  # seconds, default 0
)

Example: Authentik

See Authentik Infrastructure Setup for how to create the Authentik-side OAuth2/OIDC provider and application, and how to include group membership in the token.

from fastapi import FastAPI, Depends
from starlette.requests import Request
from auth_middleware import JwtAuthMiddleware
from auth_middleware.guards import require_user, require_groups
from auth_middleware.providers.oidc.oidc_provider import OidcProvider
from auth_middleware.providers.oidc.oidc_provider_settings import (
    OidcProviderSettings,
)

app = FastAPI(title="Authentik Example API")

auth_settings = OidcProviderSettings(
    issuer="https://authentik.example.com/application/o/my-app/",
    audience="your-oidc-client-id",
    groups_claim="groups",  # see the Authentik setup guide to enable this
)

app.add_middleware(
    JwtAuthMiddleware,
    auth_provider=OidcProvider(settings=auth_settings),
)

@app.get("/profile", dependencies=[Depends(require_user())])
async def get_profile(request: Request):
    user = request.state.current_user
    return {"user": user.name, "email": user.email, "groups": await user.groups}

@app.get("/admin", dependencies=[Depends(require_groups(["admins"]))])
async def admin_only():
    return {"message": "Admin access granted"}

Example: Keycloak

See Keycloak Infrastructure Setup for how to create the realm and client in Keycloak, and how to map group membership into the token.

from fastapi import FastAPI, Depends
from starlette.requests import Request
from auth_middleware import JwtAuthMiddleware
from auth_middleware.guards import require_user, require_groups
from auth_middleware.providers.oidc.oidc_provider import OidcProvider
from auth_middleware.providers.oidc.oidc_provider_settings import (
    OidcProviderSettings,
)

app = FastAPI(title="Keycloak Example API")

auth_settings = OidcProviderSettings(
    issuer="https://keycloak.example.com/realms/myapp",
    audience="my-app",
    groups_claim="groups",  # see the Keycloak setup guide to enable this
)

app.add_middleware(
    JwtAuthMiddleware,
    auth_provider=OidcProvider(settings=auth_settings),
)

@app.get("/profile", dependencies=[Depends(require_user())])
async def get_profile(request: Request):
    user = request.state.current_user
    return {"user": user.name, "email": user.email, "groups": await user.groups}

@app.get("/admin", dependencies=[Depends(require_groups(["admins"]))])
async def admin_only():
    return {"message": "Admin access granted"}

Building Your Own Provider

If your identity provider isn’t OIDC-compliant, or you need behavior OidcProvider doesn’t cover, subclass the JWTProvider contract directly and implement load_jwks, verify_token, and create_user_from_token — this is exactly how CognitoProvider, EntraIDProvider, and OidcProvider itself are built. See Extending Authorization Providers for the equivalent pattern on the authorization side (groups/roles/permissions providers).

API Reference

Generic OpenID Connect JWT provider.

Works with any standards-compliant OIDC identity provider — Authentik, Keycloak, Auth0, Okta, or any other issuer that exposes a .well-known/openid-configuration discovery document and a JWKS endpoint — instead of being tied to a specific cloud vendor.

class auth_middleware.providers.oidc.oidc_provider.OidcProvider(settings: OidcProviderSettings, permissions_provider: PermissionsProvider | None = None, groups_provider: GroupsProvider | None = None, roles_provider: RolesProvider | None = None)[source]

Bases: JWTProvider

JWT authentication provider for any standards-compliant OIDC issuer.

Example

from auth_middleware.providers.oidc.oidc_provider import OidcProvider
from auth_middleware.providers.oidc.oidc_provider_settings import (
    OidcProviderSettings,
)

settings = OidcProviderSettings(
    issuer="https://authentik.example.com/application/o/my-app/",
    audience="my-client-id",
)
auth_provider = OidcProvider(settings=settings)
__init__(settings: OidcProviderSettings, permissions_provider: PermissionsProvider | None = None, groups_provider: GroupsProvider | None = None, roles_provider: RolesProvider | None = None) None[source]
async create_user_from_token(token: JWTAuthorizationCredentials) User[source]

Initializes a domain User object with data recovered from a JWT token issued by the configured OIDC provider.

Parameters:

token (JWTAuthorizationCredentials) – the verified token.

Returns:

Domain object.

Return type:

User

async get_keys() list[dict[str, Any]][source]

Fetch the JWKS keys from the OIDC identity provider.

Returns:

a list of JWK keys

Return type:

List[JWK]

async load_jwks() JWKS[source]

Load JWKS credentials from the OIDC identity provider.

Returns:

cached key set with refresh metadata.

Return type:

JWKS

async verify_token(token: JWTAuthorizationCredentials) bool[source]
class auth_middleware.providers.oidc.oidc_provider_settings.OidcProviderSettings(_case_sensitive: bool | None = None, _nested_model_default_partial_update: bool | None = None, _env_prefix: str | None = None, _env_prefix_target: EnvPrefixTarget | None = None, _env_file: DotenvType | None = PosixPath('.'), _env_file_encoding: str | None = None, _env_ignore_empty: bool | None = None, _env_nested_delimiter: str | None = None, _env_nested_max_split: int | None = None, _env_parse_none_str: str | None = None, _env_parse_enums: bool | None = None, _cli_prog_name: str | None = None, _cli_parse_args: bool | list[str] | tuple[str, ...] | None = None, _cli_settings_source: CliSettingsSource[Any] | None = None, _cli_parse_none_str: str | None = None, _cli_hide_none_type: bool | None = None, _cli_avoid_json: bool | None = None, _cli_enforce_required: bool | None = None, _cli_use_class_docs_for_groups: bool | None = None, _cli_exit_on_error: bool | None = None, _cli_prefix: str | None = None, _cli_flag_prefix_char: str | None = None, _cli_implicit_flags: bool | Literal['dual', 'toggle'] | None = None, _cli_ignore_unknown_args: bool | None = None, _cli_kebab_case: bool | Literal['all', 'no_enums'] | None = None, _cli_shortcuts: Mapping[str, str | list[str]] | None = None, _secrets_dir: PathType | None = None, _build_sources: tuple[tuple[PydanticBaseSettingsSource, ...], dict[str, Any]] | None = None, *, jwt_secret_key: str | None = None, jwt_algorithm: str | None = 'HS256', jwt_token_verification_disabled: bool | None = False, jwt_leeway: ~typing.Annotated[int, ~annotated_types.Ge(ge=0)] = 0, jwks_cache_strategy: ~typing.Literal['time', 'usage', 'both'] = 'both', jwks_background_refresh: bool = True, jwks_background_refresh_threshold: ~typing.Annotated[float, ~annotated_types.Ge(ge=0.0), ~annotated_types.Le(le=1.0)] = 0.8, issuer: str, audience: str | None = None, jwks_uri: str | None = None, discovery_url: str | None = None, algorithms: list[str] = <factory>, username_claim: str = 'preferred_username', groups_claim: str | None = 'groups', jwks_cache_interval: int | None = 20, jwks_cache_usages: int | None = 1000)[source]

Bases: JWTProviderSettings

Settings for a generic OpenID Connect provider.

Works with any standards-compliant OIDC identity provider (Authentik, Keycloak, Auth0, Okta, …): the JWKS is discovered from the issuer’s .well-known/openid-configuration document unless jwks_uri is set explicitly.

algorithms: list[str]
audience: str | None
discovery_url: str | None
groups_claim: str | None
issuer: str
jwks_cache_interval: int | None
jwks_cache_usages: int | None
jwks_uri: str | None
model_config = {'arbitrary_types_allowed': True, 'case_sensitive': False, 'cli_avoid_json': False, 'cli_enforce_required': False, 'cli_exit_on_error': True, 'cli_flag_prefix_char': '-', 'cli_hide_none_type': False, 'cli_ignore_unknown_args': False, 'cli_implicit_flags': False, 'cli_kebab_case': False, 'cli_parse_args': None, 'cli_parse_none_str': None, 'cli_prefix': '', 'cli_prog_name': None, 'cli_shortcuts': None, 'cli_use_class_docs_for_groups': False, 'enable_decoding': True, 'env_file': '.env', 'env_file_encoding': 'utf-8', 'env_ignore_empty': False, 'env_nested_delimiter': None, 'env_nested_max_split': None, 'env_parse_enums': None, 'env_parse_none_str': None, 'env_prefix': '', 'env_prefix_target': 'variable', 'extra': 'ignore', 'frozen': True, 'json_file': None, 'json_file_encoding': None, 'nested_model_default_partial_update': False, 'protected_namespaces': ('model_validate', 'model_dump', 'settings_customise_sources'), 'secrets_dir': None, 'toml_file': None, 'validate_default': True, 'yaml_config_section': None, 'yaml_file': None, 'yaml_file_encoding': None}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

username_claim: str

For more information about setting up an identity provider or other authentication providers, see: