axa-fr-oidc
A Python library for OpenID Connect (OIDC) authentication with DPoP (Demonstrating Proof-of-Possession) support, featuring JWT validation, token caching, and both sync/async operations.
Supported Python versions: 3.10 to 3.15.
Table of Contents (ToC)
- Table of Contents (ToC)
- Features
- Installation
- Quick Start
- Advanced Usage (Low-Level API)
- API Reference
- Advanced Configuration
- Development
- Contributing
Features
- 🔐 OIDC Authentication - Full OpenID Connect authentication support
- 🔑 DPoP Support - Demonstrating Proof-of-Possession for enhanced security
- ✅ JWT Validation - Comprehensive token validation with JWKS
- 💾 Token Caching - Built-in memory cache for tokens and JWKS
- ⚡ Async/Sync - Supports both synchronous and asynchronous operations
- 🎯 Type Safe - Fully typed with Python type hints
- 🔒 Flexible Auth Methods -
client_secret_jwt,client_secret_post, andclient_secret_basicwith automatic fallback
Installation
Using uv (recommended)
uv add axa-fr-oidc
Using pip
pip install axa-fr-oidc
Quick Start
Simple Usage with OidcClient (Recommended)
The public surface is split across two focused, single-responsibility classes:
OidcClient— retrieve and exchange access tokens.OidcValidator— validate access tokens (and DPoP proofs).
This split keeps each class small and easy to test. When a single program
needs both, the two instances can share the same http_service and
memory_cache.
from axa_fr_oidc import OidcClient, OidcValidator
# Create a client with client credentials (token retrieval)
client = OidcClient(
issuer="https://issuer.url",
client_id="your-client-id",
client_secret="your-client-secret",
scopes=["openid", "profile"],
)
# Get an access token (automatically cached and refreshed)
access_token = client.get_access_token()
# Force a fresh token from the authorization server (bypasses cache)
fresh_token = client.get_access_token(force_renew_token=True)
# Validate a token using the dedicated validator
validator = OidcValidator(
issuer="https://issuer.url",
audience="your-api-audience",
)
result = validator.validate_token(access_token)
if result.success:
print(f"Token is valid! Subject: {result.payload['sub']}")
else:
print(f"Token is invalid: {result.error}")
# Clean up resources
client.close_sync()
validator.close_sync()
Using Context Managers
from axa_fr_oidc import OidcClient
# Sync context manager
with OidcClient(
issuer="https://issuer.url",
client_id="your-client-id",
client_secret="your-client-secret",
) as client:
token = client.get_access_token()
# Async context manager
async with OidcClient(
issuer="https://issuer.url",
client_id="your-client-id",
client_secret="your-client-secret",
) as client:
token = await client.get_access_token_async()
Async Operations
import asyncio
from axa_fr_oidc import OidcClient, OidcValidator
async def main():
async with OidcClient(
issuer="https://issuer.url",
client_id="your-client-id",
client_secret="your-client-secret",
) as client, OidcValidator(
issuer="https://issuer.url",
audience="your-api-audience",
) as validator:
# Async token retrieval
token = await client.get_access_token_async()
# Async token validation
result = await validator.validate_token_async(token)
print(result.success, result.payload)
asyncio.run(main())
Private Key Authentication
from axa_fr_oidc import OidcClient
# Load your private key
with open("private_key.pem", "r") as f:
private_key_pem = f.read()
client = OidcClient(
issuer="https://issuer.url",
client_id="your-client-id",
private_key=private_key_pem,
algorithm="RS256",
scopes=["openid", "profile"],
)
token = client.get_access_token()
Validating DPoP Tokens
from axa_fr_oidc import OidcValidator
validator = OidcValidator(
issuer="https://issuer.url",
audience="your-api-audience",
)
# Validate a DPoP-bound token
result = validator.validate_token(
token=access_token,
dpop=dpop_proof,
path="/api/resource",
http_method="POST",
)
print(result.success, result.error)
Token Exchange
from axa_fr_oidc import OidcClient
client = OidcClient(
issuer="https://issuer.url",
client_id="your-client-id",
client_secret="your-client-secret",
)
# Exchange a token (RFC 8693)
new_token = client.token_exchange(
subject_token=user_token,
requested_token_type="urn:ietf:params:oauth:token-type:access_token",
)
Custom HTTP Configuration (Proxies, SSL, Timeouts)
The client supports custom HTTP configurations including proxy settings, SSL verification, and timeouts:
from axa_fr_oidc import OidcClient
# Using a proxy
client = OidcClient(
issuer="https://issuer.url",
client_id="your-client-id",
client_secret="your-client-secret",
proxy="http://proxy.example.com:8080",
)
# Using an HTTPS proxy
client = OidcClient(
issuer="https://issuer.url",
client_id="your-client-id",
client_secret="your-client-secret",
proxy="https://secure-proxy.example.com:8443",
)
# Disable SSL verification (not recommended for production)
client = OidcClient(
issuer="https://issuer.url",
client_id="your-client-id",
client_secret="your-client-secret",
verify=False,
)
# Set custom timeout (in seconds)
client = OidcClient(
issuer="https://issuer.url",
client_id="your-client-id",
client_secret="your-client-secret",
timeout=30.0,
)
# Combine multiple HTTP configurations
client = OidcClient(
issuer="https://issuer.url",
client_id="your-client-id",
client_secret="your-client-secret",
proxy="http://proxy.example.com:8080",
verify=True,
timeout=10.0,
)
token = client.get_access_token()
Client Secret Authentication Methods
When using client_secret, you can control how the credentials are sent to the
token endpoint via the auth_method parameter.
This setting affects token retrieval (get_access_token() /
get_access_token_async()) and RFC 8693 token exchange (token_exchange()). It
does not affect token validation.
auth_method |
Behaviour |
|---|---|
"client_secret_jwt" (default) |
Signs an HS256 JWT assertion (RFC 7523). Automatically falls back to client_secret_post on 401 if the server does not have this method enabled for the client, then reuses client_secret_post for later renewals in the same client instance. |
"client_secret_post" |
Sends client_id + client_secret in the POST body. Broadly supported. |
"client_secret_basic" |
Sends credentials as an HTTP Basic Auth header. Broadly supported. |
After a successful automatic fallback, the retrieved access token is still cached
normally. When that token must be renewed later, the same OidcClient/
OpenIdConnect instance skips the failing JWT round-trip and directly uses
client_secret_post. The same effective method is reused by later
token_exchange() calls. If token_exchange() itself receives a 401
invalid_client response while using client_secret_jwt, it also retries once
with client_secret_post.
from axa_fr_oidc import OidcClient
from axa_fr_oidc.constants import (
CLIENT_SECRET_AUTH_METHOD_JWT, # "client_secret_jwt" (default)
CLIENT_SECRET_AUTH_METHOD_POST, # "client_secret_post"
CLIENT_SECRET_AUTH_METHOD_BASIC, # "client_secret_basic"
)
# Default: tries client_secret_jwt, falls back to client_secret_post on 401
client = OidcClient(
issuer="https://issuer.url",
client_id="your-client-id",
client_secret="your-client-secret",
)
# Explicitly use client_secret_post (no fallback overhead)
client = OidcClient(
issuer="https://issuer.url",
client_id="your-client-id",
client_secret="your-client-secret",
auth_method=CLIENT_SECRET_AUTH_METHOD_POST,
)
# Explicitly use client_secret_basic
client = OidcClient(
issuer="https://issuer.url",
client_id="your-client-id",
client_secret="your-client-secret",
auth_method=CLIENT_SECRET_AUTH_METHOD_BASIC,
)
token = client.get_access_token()
For more details, see the Client Secret Auth Methods Guide.
Extract Properties from a JWT Token
from axa_fr_oidc import JWTAuthorization
authorization_header = "<your-jwt-token>"
jwt_auth = JWTAuthorization(authorization_header)
print(jwt_auth.get_property("sub")) # Print the subject of the token
print(jwt_auth.get_property("exp")) # Print the expiration time of the token
Advanced Usage (Low-Level API)
For users who need more control over the authentication process, the library provides low-level components that can be customized individually.
Using OpenIdConnect and OidcAuthentication Directly
from axa_fr_oidc import OidcAuthentication, OpenIdConnect, MemoryCache, XHttpServiceGet
from httpx import AsyncClient, Client
# Create HTTP clients
http_client = Client()
http_async_client = AsyncClient()
# Create HTTP service
http_service = XHttpServiceGet(
http_client=http_client,
http_async_client=http_async_client
)
# Create cache
memory_cache = MemoryCache()
# Create authentication handler
auth = OidcAuthentication(
issuer="https://issuer.url",
scopes=["openid", "profile"],
api_audience="your-api-audience",
service=http_service,
memory_cache=memory_cache,
algorithms=["RS256", "ES256"],
)
# Create OpenID Connect client
oidc = OpenIdConnect(
authentication=auth,
memory_cache=memory_cache,
client_id="your-client-id",
client_secret="your-client-secret"
)
# Get access token
access_token = oidc.get_access_token()
# Validate token
result = auth.validate(access_token, None, None, None)
print(result.success, result.error)
Async Operations with Low-Level API
All low-level components support async/await:
from axa_fr_oidc import OidcAuthentication, OpenIdConnect, MemoryCache, XHttpServiceGet
from httpx import AsyncClient, Client
async def main():
http_service = XHttpServiceGet(
http_client=Client(),
http_async_client=AsyncClient()
)
memory_cache = MemoryCache()
auth = OidcAuthentication(
issuer="https://issuer.url",
scopes=["openid", "profile"],
api_audience="your-api-audience",
service=http_service,
memory_cache=memory_cache
)
oidc = OpenIdConnect(
authentication=auth,
memory_cache=memory_cache,
client_id="your-client-id",
client_secret="your-client-secret"
)
# Async token retrieval
access_token = await oidc.get_access_token_async()
# Async token validation
result = await auth.validate_async(access_token, None, None, None)
print(result.success, result.payload)
# Run with asyncio
import asyncio
asyncio.run(main())
Using Private Key Authentication (Low-Level)
For client credentials flow with private key (JWT bearer) using the low-level API:
from axa_fr_oidc import OidcAuthentication, OpenIdConnect, MemoryCache, XHttpServiceGet
from httpx import AsyncClient, Client
# Load your private key
with open("private_key.pem", "r") as f:
private_key_pem = f.read()
http_service = XHttpServiceGet(
http_client=Client(),
http_async_client=AsyncClient()
)
memory_cache = MemoryCache()
auth = OidcAuthentication(
issuer="https://issuer.url",
scopes=["openid", "profile"],
api_audience="your-api-audience",
service=http_service,
memory_cache=memory_cache
)
oidc = OpenIdConnect(
authentication=auth,
memory_cache=memory_cache,
client_id="your-client-id",
private_key=private_key_pem,
algorithm="RS256" # or other supported algorithms
)
access_token = oidc.get_access_token()
Note: For most use cases, consider using the simpler
OidcClientinstead. See the Quick Start section for examples.
Custom Configuration
You can customize various timeouts and cache settings:
Using OidcClient
from axa_fr_oidc import OidcClient
client = OidcClient(
issuer="https://issuer.url",
client_id="your-client-id",
client_secret="your-client-secret",
scopes=["openid", "profile"],
issuer_cache_expiration_seconds=7200, # Cache token_endpoint for 2 hours (default: 3600)
token_expiration_margin_seconds=90, # Renew cached access tokens 90 seconds before exp
)
token_expiration_margin_seconds controls only access tokens retrieved and
cached by OidcClient. It defaults to 90 seconds and can be set to 0 to keep
tokens until their actual exp claim. It does not change how OidcValidator
validates incoming tokens.
Using OidcValidator
from axa_fr_oidc import OidcValidator
validator = OidcValidator(
issuer="https://issuer.url",
audience="your-api-audience",
algorithms=["RS256", "ES256"], # Allowed algorithms for validation
issuer_cache_expiration_seconds=7200, # Cache JWKS for 2 hours (default: 3600)
)
Using Low-Level API
from axa_fr_oidc import (
OidcAuthentication,
MemoryCache,
XHttpServiceGet,
)
from httpx import AsyncClient, Client
auth = OidcAuthentication(
issuer="https://issuer.url",
scopes=["openid", "profile"],
api_audience="your-api-audience",
service=XHttpServiceGet(
http_client=Client(),
http_async_client=AsyncClient()
),
memory_cache=MemoryCache(),
algorithms=["RS256", "ES256"], # Supported algorithms
)
Custom Token Validation with handle_validation
The handle_validation parameter allows you to dynamically customize which scopes and audience are checked during token validation, based on the token payload itself.
Note:
handle_validationis invoked before the token signature and standard claims are validated. Treatpayloadas untrusted until validation succeeds, and avoid side effects based on it. This is useful when:
- Different tokens require different scopes depending on their claims
- The audience to validate depends on the token content
- You need multi-tenant validation logic
Use it with the low-level API:
from axa_fr_oidc import OidcAuthentication, MemoryCache, XHttpServiceGet
from axa_fr_oidc.oidc.oidc_authentication import HandleValidationResult
from httpx import AsyncClient, Client
def custom_validation(payload: dict) -> HandleValidationResult:
"""Return aud=None to skip audience validation (unless an audience override is provided)."""
return HandleValidationResult(scopes=payload.get("scope", "").split(), aud=None)
auth = OidcAuthentication(
issuer="https://issuer.url",
scopes=["default-scope"], # Ignored when handle_validation is provided
api_audience="default-audience", # Ignored when handle_validation is provided
service=XHttpServiceGet(
http_client=Client(),
http_async_client=AsyncClient()
),
memory_cache=MemoryCache(),
handle_validation=custom_validation,
)
result = auth.validate(access_token, None)
The HandleValidationResult dataclass has the following fields:
scopes(list[str]) — The list of scopes to require in the token.aud(str | None) — The audience to validate. Set toNoneto skip audience validation.
For more details, see the examples above.
API Reference
High-Level Client (Recommended)
-
OidcClient— High-level client for token retrieval and exchange.get_access_token(force_renew_token=False)/get_access_token_async(force_renew_token=False)— Get an access token (setforce_renew_token=Trueto bypass cache)token_exchange()— Exchange tokens (RFC 8693)get_token_endpoint()/get_token_endpoint_async()— Get the token endpoint URLclear_cache()— Clear all cached dataclose()/close_sync()— Release resources- Supports context managers (
with/async with)
-
OidcValidator— High-level validator for access tokens and DPoP proofs.validate_token(token, dpop=None, path=None, http_method=None, audience=None)/validate_token_async(...)— Validate an access tokenget_token_endpoint()/get_token_endpoint_async()— Get the token endpoint URLclear_cache()— Clear cached JWKS / discovery dataclose()/close_sync()— Release resources- Supports context managers (
with/async with) - Accepts a
handle_validationcallback to derive scopes / audience from the token payload at runtime
Breaking change (since the previous release):
OidcClientno longer exposesvalidate_token()/validate_token_async(). Move any validation calls toOidcValidator.
Low-Level Classes
OidcAuthentication- OIDC token validation and JWKS managementOpenIdConnect- Client for obtaining access tokensMemoryCache- In-memory cache for tokens and JWKSXHttpServiceGet- HTTP service wrapper for sync/async requestsJWTAuthorization- Utility for extracting JWT claimsAuthenticationResult- Result object from validation operationsHandleValidationResult(fromaxa_fr_oidc.oidc.oidc_authentication) - Result object returned by customhandle_validationcallbacks
Interfaces
All main classes have corresponding interfaces for dependency injection:
IOidcAuthentication- Interface for OidcAuthenticationIOpenIdConnect- Interface for OpenIdConnectIMemoryCache- Interface for MemoryCacheIHttpServiceGet- Interface for XHttpServiceGetIGenericAuthorization- Interface for JWTAuthorization
Constants
The library exports useful constants for configuration:
from axa_fr_oidc import (
DEFAULT_DPOP_MAX_AGE_SECONDS, # 300 (5 minutes)
DEFAULT_CLOCK_SKEW_SECONDS, # 300 (5 minutes)
DEFAULT_JTI_LIFETIME_SECONDS, # 300 (5 minutes)
DEFAULT_JWT_ALGORITHM, # "RS256"
DEFAULT_JWT_EXPIRATION_SECONDS, # 300 (5 minutes)
DEFAULT_HTTP_TIMEOUT_SECONDS, # 5 seconds
SUPPORTED_ALGORITHMS, # ["RS256", "HS256"]
DPOP_TOKEN_TYPE, # "dpop+jwt"
GRANT_TYPE_CLIENT_CREDENTIALS, # "client_credentials"
CLIENT_ASSERTION_TYPE_JWT_BEARER, # "urn:ietf:params:oauth:..."
CONTENT_TYPE_FORM_URLENCODED, # "application/x-www-form-urlencoded"
OIDC_WELL_KNOWN_PATH, # "/.well-known/openid-configuration"
CLIENT_SECRET_AUTH_METHOD_JWT, # "client_secret_jwt"
CLIENT_SECRET_AUTH_METHOD_POST, # "client_secret_post"
CLIENT_SECRET_AUTH_METHOD_BASIC, # "client_secret_basic"
)
Advanced Configuration
Client Secret Authentication Methods
For detailed information on configuring the client-secret auth method
(client_secret_jwt, client_secret_post, client_secret_basic) and the
automatic fallback behaviour, see the
Client Secret Auth Methods Guide.
Proxy, SSL, and Timeout Configuration
For detailed information on configuring HTTP proxies, SSL verification, and timeouts, see the Proxy Configuration Guide.
Quick examples:
# Using a proxy
client = OidcClient(
issuer="https://auth.example.com",
client_id="your-client-id",
client_secret="your-client-secret",
proxy="http://proxy.example.com:8080",
)
# With custom timeout
client = OidcClient(
issuer="https://auth.example.com",
client_id="your-client-id",
client_secret="your-client-secret",
timeout=30.0,
)
Development
Setup Development Environment
# Clone the repository
git clone https://github.com/your-org/axa-fr-oidc.git
cd axa-fr-oidc
# Install uv if not already installed
curl -LsSf https://astral.sh/uv/install.sh | sh
# Install dependencies
uv sync --group dev
Using the Makefile
The project includes a Makefile for convenient development commands:
# Show all available commands
make help
# Install dependencies
make install # Production dependencies only
make install-dev # All development dependencies
make install-quality # Quality check tools only
make install-test # Test dependencies only
# Code quality
make lint # Run ruff linter (includes docstring checks)
make lint-fix # Run ruff linter with auto-fix
make format # Run ruff formatter
make format-check # Check formatting without changes
make type-check # Run mypy type checker
# Security
make security # Run bandit security checks
make security-audit # Run pip-audit for dependency vulnerabilities
# Testing
make test # Run tests
make test-cov # Run tests with coverage report
# Combined commands
make quality # Run all quality checks (lint, format, type-check, security)
make all # Run quality checks and tests
# Cleanup
make clean # Remove build artifacts and cache files
Running Tests
# Using make
make test
# Or directly with uv
uv run pytest
Running Quality Checks
# Run all quality checks at once
make quality
# Or run individual checks
make lint
make type-check
make security
Installing Specific Dependency Groups
# Install only test dependencies
uv sync --group test
# Install only linting tools
uv sync --group lint
# Install only security tools
uv sync --group security
# Install everything for development
uv sync --group dev
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Metadata
Release files for axa-fr-oidc 2.1.5
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| axa_fr_oidc-2.1.5.tar.gz | 33.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| axa_fr_oidc-2.1.5-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 69.0 kB
Release files / axa_fr_oidc-2.1.5.tar.gz
| Download URL | axa_fr_oidc-2.1.5.tar.gz |
|---|---|
| Size | 33.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
8903f905ebeaeac5c948410a3bf7732133f77c24b231fbae848eea2d495b7e48
|
|
BLAKE2b-256 checksum How to use checksums |
2d09c58089bfaa9a2641d8aedbeb98a02b2c2b46df001dd1a9e511c1eef0f538
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.14 {"installer":{"name":"uv","version":"0.12.14","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 / axa_fr_oidc-2.1.5-py3-none-any.whl
| Download URL | axa_fr_oidc-2.1.5-py3-none-any.whl |
|---|---|
| Size | 35.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
5f95ebd6f413b17d779456fed2498a86b76206ada9e540dfa4b0402fc39418c4
|
|
BLAKE2b-256 checksum How to use checksums |
95a9b7dd8946bf5b0af0d42a03665e0ccae9579959e8da6bae6a7ac80bfeacd2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.14 {"installer":{"name":"uv","version":"0.12.14","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}
|