Skip to main content

OAuth2.0 and OpenID Connect Client Library

Project description

py-identity-model

Build License

OIDC/OAuth2.0 helper library for decoding JWTs and creating JWTs utilizing the client_credentials grant. This project has been used in production for years as the foundation of Flask/FastAPI middleware implementations.

Installation

pip install py-identity-model

Or with uv:

uv add py-identity-model

Requirements: Python 3.12 or higher

SSL Certificate Configuration

If you're working with custom SSL certificates (e.g., in corporate environments or with self-signed certificates), the library supports the following environment variables:

  • SSL_CERT_FILE - Recommended for new setups (httpx native)
  • CURL_CA_BUNDLE - Alternative option (also supported by httpx)
  • REQUESTS_CA_BUNDLE - Legacy support for backward compatibility
export SSL_CERT_FILE=/path/to/ca-bundle.crt
# OR
export REQUESTS_CA_BUNDLE=/path/to/ca-bundle.crt

Note: For backward compatibility, if you're upgrading from an older version that used requests, your existing REQUESTS_CA_BUNDLE environment variable will continue to work automatically.

See the Migration Guide for more details.

Compliance Status

  • OpenID Connect Discovery 1.0 - Implements all specification requirements
  • RFC 7517 (JSON Web Key) - Implements JWK/JWKS specification requirements
  • JWT Validation - Comprehensive validation with PyJWT integration
  • Client Credentials Flow - OAuth 2.0 client credentials grant support

The library currently supports:

  • ✅ Discovery endpoint with full validation
  • ✅ JWKS endpoint with RFC 7517 compliance
  • ✅ JWT token validation with auto-discovery
  • ✅ Authorization servers with multiple active keys
  • ✅ Client credentials token generation
  • ✅ UserInfo endpoint (OpenID Connect)
  • ✅ Comprehensive error handling and validation

For more information on token validation options, refer to the official PyJWT Docs

Note: Does not currently support opaque tokens.

Async/Await Support ⚡

NEW in v1.2.0: Full async/await support for all client operations!

py-identity-model now provides both synchronous and asynchronous APIs:

  • Synchronous API (default import): Traditional blocking I/O - perfect for scripts, CLIs, Flask, Django
  • Asynchronous API (from py_identity_model.aio import ...): Non-blocking I/O - perfect for FastAPI, Starlette, high-concurrency apps
# Sync (default) - works as before
from py_identity_model import get_discovery_document

response = get_discovery_document(request)

# Async (new!) - for async frameworks
from py_identity_model.aio import get_discovery_document

response = await get_discovery_document(request)

When to use async:

  • ✅ Async web frameworks (FastAPI, Starlette, aiohttp)
  • ✅ High-concurrency applications
  • ✅ Concurrent I/O operations
  • ✅ Applications already using asyncio

When to use sync:

  • ✅ Scripts and CLIs
  • ✅ Traditional web frameworks (Flask, Django)
  • ✅ Simple applications
  • ✅ Blocking I/O is acceptable

See examples/async_examples.py for complete async examples!

Thread Safety & Concurrency 🔒

py-identity-model is fully thread-safe and async-safe for use in multi-threaded, multi-worker, and async environments.

HTTP Client Management

The library uses different strategies for sync and async clients to ensure optimal performance and thread safety:

Synchronous API (Thread-Local Storage)

  • Each thread gets its own HTTP client using threading.local()
  • Thread-isolated connection pooling: Connections are reused within the same thread
  • No global state: Eliminates race conditions and lock contention
  • Automatic cleanup: Each thread manages its own client lifecycle
# Sync API - thread-safe by design
from py_identity_model import get_discovery_document, DiscoveryDocumentRequest

# Each thread gets its own client with connection pooling
response = get_discovery_document(DiscoveryDocumentRequest(address=url))

Perfect for:

  • Flask with threading (threaded=True)
  • Gunicorn/uWSGI with threaded workers
  • Messaging consumers (Kafka, RabbitMQ) with thread-per-message
  • Any multi-threaded application

Asynchronous API (Singleton with Lock Protection)

  • Single async HTTP client per process created lazily
  • Thread-safe initialization: Protected by threading.Lock()
  • Shared connection pool: All async operations share connections efficiently
  • Optimal for async: No locks during I/O operations
# Async API - async-safe with efficient connection sharing
from py_identity_model.aio import get_discovery_document, DiscoveryDocumentRequest

# All async operations share a single client and connection pool
response = await get_discovery_document(DiscoveryDocumentRequest(address=url))

Perfect for:

  • FastAPI with async endpoints
  • Starlette applications
  • aiohttp servers
  • Any asyncio-based application

Caching Strategy

  • Discovery documents: Cached per process with functools.lru_cache (sync) and async_lru.alru_cache (async)
  • JWKS keys: Cached per process for fast validation
  • SSL configuration: Thread-safe with lock protection
  • Response bodies: Always fully consumed and closed

Safe for Production

Works seamlessly with:

  • ✅ FastAPI with multiple workers (uvicorn --workers N)
  • ✅ Gunicorn with threading or async workers
  • ✅ Django with multiple worker threads
  • ✅ Flask with threading enabled
  • ✅ Celery/messaging workers
  • ✅ Concurrent request handling

Performance Benefits

  1. Connection pooling: HTTP connections are reused for better performance
  2. Thread-local clients (sync): No lock contention between threads
  3. Shared async client: Efficient connection sharing in async code
  4. Cached discovery/JWKS: Reduces redundant network calls
  5. Explicit resource cleanup: Responses are closed to prevent connection leaks
# Example: Concurrent token validation in threaded environment
from concurrent.futures import ThreadPoolExecutor
from py_identity_model import validate_token, TokenValidationConfig

def validate_request(token: str) -> dict:
    config = TokenValidationConfig(perform_disco=True, audience="my-api")
    # Each thread uses its own HTTP client with connection pooling
    return validate_token(token, config, "https://issuer.example.com")

# Safe to use with multiple threads - no shared state
with ThreadPoolExecutor(max_workers=10) as executor:
    futures = [executor.submit(validate_request, token) for token in tokens]
    results = [f.result() for f in futures]
# Example: Concurrent async token validation
import asyncio
from py_identity_model.aio import validate_token, TokenValidationConfig

async def validate_request(token: str) -> dict:
    config = TokenValidationConfig(perform_disco=True, audience="my-api")
    # All async calls share a single client and connection pool
    return await validate_token(token, config, "https://issuer.example.com")

# Safe concurrent async operations
async def main():
    results = await asyncio.gather(*[validate_request(token) for token in tokens])

Key Architectural Decisions

Component Sync API Async API
HTTP Client Thread-local (one per thread) Singleton (one per process)
Connection Pooling Per-thread pools Shared process pool
Thread Safety Isolation via thread-local Lock-protected initialization
Best For Multi-threaded apps Async/await apps

This library inspired by Duende.IdentityModel

From Duende.IdentityModel

It provides an object model to interact with the endpoints defined in the various OAuth and OpenId Connect specifications in the form of:

  • types to represent the requests and responses
  • extension methods to invoke requests
  • constants defined in the specifications, such as standard scope, claim, and parameter names
  • other convenience methods for performing common identity related operations

This library aims to provide the same features in Python.

Documentation

Full documentation is available at jamescrowley321.github.io/py-identity-model.

Compliance Documentation

Configuration

Environment Variables

The library supports the following environment variables for configuration:

HTTP Client Configuration

  • HTTP_TIMEOUT - HTTP request timeout in seconds (default: 30.0)

    export HTTP_TIMEOUT=60.0  # Increase timeout to 60 seconds
    
  • HTTP_RETRY_COUNT - Number of retries for rate-limited requests (default: 3)

    export HTTP_RETRY_COUNT=5  # Retry up to 5 times
    
  • HTTP_RETRY_BASE_DELAY - Base delay in seconds for exponential backoff (default: 1.0)

    export HTTP_RETRY_BASE_DELAY=2.0  # Start with 2-second delay
    

SSL/TLS Certificate Configuration

For working with custom SSL certificates (corporate environments, self-signed certificates):

  • SSL_CERT_FILE - Path to CA bundle file (recommended, httpx native)
  • CURL_CA_BUNDLE - Alternative CA bundle path (also supported by httpx)
  • REQUESTS_CA_BUNDLE - Legacy support for backward compatibility
# Recommended approach
export SSL_CERT_FILE=/path/to/ca-bundle.crt

# OR use legacy variable (backward compatible)
export REQUESTS_CA_BUNDLE=/path/to/ca-bundle.crt

Priority Order: SSL_CERT_FILEREQUESTS_CA_BUNDLECURL_CA_BUNDLE → System defaults

See the Migration Guide for more details.

Quick Examples

Discovery

Only a subset of fields is currently mapped.

import os

from py_identity_model import DiscoveryDocumentRequest, get_discovery_document

DISCO_ADDRESS = os.environ["DISCO_ADDRESS"]

disco_doc_request = DiscoveryDocumentRequest(address=DISCO_ADDRESS)
disco_doc_response = get_discovery_document(disco_doc_request)

if disco_doc_response.is_successful:
    print(f"Issuer: {disco_doc_response.issuer}")
    print(f"Token Endpoint: {disco_doc_response.token_endpoint}")
    print(f"JWKS URI: {disco_doc_response.jwks_uri}")
else:
    print(f"Error: {disco_doc_response.error}")

JWKs

import os

from py_identity_model import (
    DiscoveryDocumentRequest,
    get_discovery_document,
    JwksRequest,
    get_jwks,
)

DISCO_ADDRESS = os.environ["DISCO_ADDRESS"]

disco_doc_request = DiscoveryDocumentRequest(address=DISCO_ADDRESS)
disco_doc_response = get_discovery_document(disco_doc_request)

if disco_doc_response.is_successful:
    jwks_request = JwksRequest(address=disco_doc_response.jwks_uri)
    jwks_response = get_jwks(jwks_request)

    if jwks_response.is_successful:
        print(f"Found {len(jwks_response.keys)} keys")
        for key in jwks_response.keys:
            print(f"Key ID: {key.kid}, Type: {key.kty}")
    else:
        print(f"Error: {jwks_response.error}")

Basic Token Validation

Token validation validates the signature of a JWT against the values provided from an OIDC discovery document. The function will raise a PyIdentityModelException if the token is expired or signature validation fails.

Token validation utilizes PyJWT for work related to JWT validation. The configuration object is mapped to the input parameters of jwt.decode.

import os

from py_identity_model import (
    PyIdentityModelException,
    TokenValidationConfig,
    validate_token,
)

DISCO_ADDRESS = os.environ["DISCO_ADDRESS"]
TEST_AUDIENCE = os.environ["TEST_AUDIENCE"]

token = get_token()  # Get the token in the manner best suited to your application

validation_options = {
    "verify_signature": True,
    "verify_aud": True,
    "verify_iat": True,
    "verify_exp": True,
    "verify_nbf": True,
    "verify_iss": True,
    "verify_sub": True,
    "verify_jti": True,
    "verify_at_hash": True,
    "require_aud": False,
    "require_iat": False,
    "require_exp": False,
    "require_nbf": False,
    "require_iss": False,
    "require_sub": False,
    "require_jti": False,
    "require_at_hash": False,
    "leeway": 0,
}

validation_config = TokenValidationConfig(
    perform_disco=True,
    audience=TEST_AUDIENCE,
    options=validation_options
)

try:
    claims = validate_token(
        jwt=token,
        token_validation_config=validation_config,
        disco_doc_address=DISCO_ADDRESS
    )
    print(f"Token validated successfully for subject: {claims.get('sub')}")
except PyIdentityModelException as e:
    print(f"Token validation failed: {e}")

Token Generation

The only current supported flow is the client_credentials flow. Load configuration parameters in the method your application supports. Environment variables are used here for demonstration purposes.

import os

from py_identity_model import (
    ClientCredentialsTokenRequest,
    ClientCredentialsTokenResponse,
    DiscoveryDocumentRequest,
    get_discovery_document,
    request_client_credentials_token,
)

DISCO_ADDRESS = os.environ["DISCO_ADDRESS"]
CLIENT_ID = os.environ["CLIENT_ID"]
CLIENT_SECRET = os.environ["CLIENT_SECRET"]
SCOPE = os.environ["SCOPE"]

# First, get the discovery document to find the token endpoint
disco_doc_response = get_discovery_document(
    DiscoveryDocumentRequest(address=DISCO_ADDRESS)
)

if disco_doc_response.is_successful:
    # Request an access token using client credentials
    client_creds_req = ClientCredentialsTokenRequest(
        client_id=CLIENT_ID,
        client_secret=CLIENT_SECRET,
        address=disco_doc_response.token_endpoint,
        scope=SCOPE,
    )
    client_creds_token = request_client_credentials_token(client_creds_req)

    if client_creds_token.is_successful:
        print(f"Access Token: {client_creds_token.token['access_token']}")
        print(f"Token Type: {client_creds_token.token['token_type']}")
        print(f"Expires In: {client_creds_token.token['expires_in']} seconds")
    else:
        print(f"Token request failed: {client_creds_token.error}")
else:
    print(f"Discovery failed: {disco_doc_response.error}")

Features Status

✅ Completed Features

  • Discovery Endpoint - Implements OpenID Connect Discovery 1.0
  • JWKS Endpoint - Implements RFC 7517 (JSON Web Key)
  • Token Validation - JWT validation with auto-discovery and PyJWT integration
  • Token Endpoint - Client credentials grant type
  • UserInfo Endpoint - OpenID Connect UserInfo with sync and async support
  • Token-to-Principal Conversion - Convert JWTs to ClaimsPrincipal objects
  • Protocol Constants - OIDC and OAuth 2.0 constants
  • Comprehensive Type Hints - Full type safety throughout
  • Error Handling - Structured exceptions and validation
  • Async/Await Support - Full async API via py_identity_model.aio module (v1.2.0)
  • Modular Architecture - Clean separation between HTTP layer and business logic (v1.2.0)

🚧 Upcoming Features

  • Token Introspection Endpoint (RFC 7662)
  • Token Revocation Endpoint (RFC 7009)
  • Dynamic Client Registration (RFC 7591)
  • Device Authorization Endpoint
  • Additional grant types (authorization code, refresh token, device flow)
  • Opaque tokens support

For detailed development plans, see the Project Roadmap.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

py_identity_model-2.17.1.tar.gz (81.2 kB view details)

Uploaded Source

Built Distribution

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

py_identity_model-2.17.1-py3-none-any.whl (113.6 kB view details)

Uploaded Python 3

File details

Details for the file py_identity_model-2.17.1.tar.gz.

File metadata

  • Download URL: py_identity_model-2.17.1.tar.gz
  • Upload date:
  • Size: 81.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.2 {"installer":{"name":"uv","version":"0.11.2","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}

File hashes

Hashes for py_identity_model-2.17.1.tar.gz
Algorithm Hash digest
SHA256 fa032467a32c1590da02691cef7ec351edf44dddccd60b117070102a83f18f2d
MD5 30ec7021ce7cfdbbe84c9e330bd1cd03
BLAKE2b-256 287275ff3b35ed7a6d0632a5671b12ab1441dfddc3b496f833f699631570ed3b

See more details on using hashes here.

File details

Details for the file py_identity_model-2.17.1-py3-none-any.whl.

File metadata

  • Download URL: py_identity_model-2.17.1-py3-none-any.whl
  • Upload date:
  • Size: 113.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.2 {"installer":{"name":"uv","version":"0.11.2","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}

File hashes

Hashes for py_identity_model-2.17.1-py3-none-any.whl
Algorithm Hash digest
SHA256 6123b6287d499bb3717b451dd8a3e7eb239a98f2621c3826cf2deb7ae373f539
MD5 d51ee422aa5af4bbfda26009238719c7
BLAKE2b-256 2bff9a06286b37b211a2eefd0515f125f0a92dd60486f8e7c93618e9548d3898

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page