Skip to content

Token Validation

JWT validation with optional OpenID Connect auto-discovery.

Configuration

TokenValidationConfig(perform_disco, key=None, audience=None, algorithms=None, issuer=None, subject=None, options=None, claims_validator=None, require_https=True, leeway=None) dataclass

Configuration for JWT token validation.

Attributes:

Name Type Description
perform_disco bool

Whether to perform discovery to fetch JWKS and issuer

key dict | None

Public key for JWT verification (dict with 'kty', 'n', 'e' for RSA)

audience str | None

Expected audience claim(s) in the token

algorithms list[str] | None

List of allowed signing algorithms (e.g., ['RS256'])

issuer str | list[str] | None

Expected issuer claim. A single string or a list of accepted issuers for multi-tenant validation.

subject str | None

Expected sub claim. Validated after decoding.

options dict | None

Additional PyJWT decode options (e.g., {'require': ['sub']}). Security-critical options (verify_signature, verify_exp, verify_nbf, verify_iat) cannot be disabled and will raise ConfigurationException if set to False.

claims_validator Callable | None

Optional callable for custom claims validation. Can be sync: Callable[[dict], None] or async: Callable[[dict], Awaitable[None]] (in async context) Should raise an exception if validation fails. The decoded token claims dict is passed as the only argument.

leeway float | None

Clock skew tolerance in seconds for exp and nbf claims. Useful when clocks between the issuer and this server are not perfectly synchronized.

Examples:

>>> # Multi-tenant with clock skew tolerance
>>> config = TokenValidationConfig(
...     perform_disco=True,
...     audience="my-api",
...     issuer=[
...         "https://idp1.example.com",
...         "https://idp2.example.com",
...     ],
...     leeway=30,
... )
>>> # Custom claims validation
>>> def validate_custom_claims(claims: dict) -> None:
...     if claims.get("role") != "admin":
...         raise ValueError("User is not an admin")
>>>
>>> config = TokenValidationConfig(
...     perform_disco=True,
...     claims_validator=validate_custom_claims,
... )

Sync API

validate_token(jwt, token_validation_config, disco_doc_address=None, http_client=None)

Validate a JWT token.

Parameters:

Name Type Description Default
jwt str

The JWT token to validate

required
token_validation_config TokenValidationConfig

Token validation configuration

required
disco_doc_address str | None

Discovery document address (required if perform_disco=True)

None
http_client HTTPClient | None

Optional managed HTTP client. When None (the default), uses the thread-local default with the full TTL cache + cooldown stack. When provided, all of the following are bypassed:

  • Discovery document cache — every call re-fetches the .well-known/openid-configuration document.
  • JWKS cache — every call re-fetches the JWKS.
  • Kid-miss cooldown — an attacker forging tokens with unknown kid headers drives 1:1 upstream JWKS fetches with no rate limit.
  • Signature-failure cooldown — an attacker forging signatures against cached kids drives 1:1 upstream JWKS fetches on the retry path with no rate limit.

The injected-client path is appropriate for one-off validations (CLI tooling, tests) and for callers that have implemented their own caching layer over the HTTP client. It is not appropriate for high-volume request paths exposed to untrusted JWTs.

A logger.warning is emitted the first time an injected client is used in the process so accidental opt-out is detectable in production logs.

None

Returns:

Name Type Description
dict dict

Decoded token claims

Raises:

Type Description
TokenValidationException

If token validation fails

ConfigurationException

If configuration is invalid

Async API

validate_token(jwt, token_validation_config, disco_doc_address=None, http_client=None) async

Validate a JWT token (async).

Parameters:

Name Type Description Default
jwt str

The JWT token to validate

required
token_validation_config TokenValidationConfig

Token validation configuration

required
disco_doc_address str | None

Discovery document address (required if perform_disco=True)

None
http_client AsyncHTTPClient | None

Optional managed HTTP client. When None (the default), uses the module-level singleton with the full TTL cache + cooldown stack. When provided, all of the following are bypassed:

  • Discovery document cache — every call re-fetches the .well-known/openid-configuration document.
  • JWKS cache — every call re-fetches the JWKS.
  • Kid-miss cooldown — an attacker forging tokens with unknown kid headers drives 1:1 upstream JWKS fetches with no rate limit.
  • Signature-failure cooldown — an attacker forging signatures against cached kids drives 1:1 upstream JWKS fetches on the retry path with no rate limit.

The injected-client path is appropriate for one-off validations (CLI tooling, tests) and for callers that have implemented their own caching layer over the HTTP client. It is not appropriate for high-volume request paths exposed to untrusted JWTs.

A logger.warning is emitted the first time an injected client is used in the process so accidental opt-out is detectable in production logs.

None

Returns:

Name Type Description
dict dict

Decoded token claims

Raises:

Type Description
TokenValidationException

If token validation fails

ConfigurationException

If configuration is invalid