Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jobs:
- name: Install Poetry
uses: snok/install-poetry@v1
with:
version: latest
version: 2.2.1
virtualenvs-create: true
virtualenvs-in-project: true
installer-parallel: true
Expand Down
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ env/

#Build files
dist
docs

#testfile
setup.py
Expand Down
47 changes: 46 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ This SDK provides comprehensive support for securing APIs with Auth0-issued acce

### **Core Features**
- **Unified Entry Point**: `verify_request()` - automatically detects and validates Bearer or DPoP schemes
- **OIDC Discovery** - Automatic fetching of Auth0 metadata and JWKS
- **Multi-Custom Domain (MCD)** - Accept tokens from multiple Auth0 domains with static lists or dynamic resolvers
- **OIDC Discovery** - Automatic fetching of Auth0 metadata and JWKS with per-issuer caching
- **JWT Validation** - Complete RS256 signature verification with claim validation
- **DPoP Proof Verification** - Full RFC 9449 compliance with ES256 signature validation
- **Flexible Configuration** - Support for both "Allowed" and "Required" DPoP modes
Expand Down Expand Up @@ -279,6 +280,50 @@ api_client = ApiClient(ApiClientOptions(
))
```

### 7. Multi-Custom Domain (MCD) Support

If your Auth0 tenant has multiple custom domains, or you're migrating between domains, the SDK can accept tokens from any of them:

#### Static Domain List

```python
from auth0_api_python import ApiClient, ApiClientOptions

api_client = ApiClient(ApiClientOptions(
domains=[
"tenant.auth0.com",
"auth.example.com",
"auth.acme.org"
],
audience="https://api.example.com"
))

# Tokens from any of the three domains are accepted
claims = await api_client.verify_access_token(access_token)
```

#### Dynamic Resolver

For runtime domain resolution based on request context:

```python
from auth0_api_python import ApiClient, ApiClientOptions, DomainsResolverContext

def resolve_domains(context: DomainsResolverContext) -> list[str]:
# Determine allowed domains based on the request
return ["tenant.auth0.com", "auth.example.com"]

api_client = ApiClient(ApiClientOptions(
domains=resolve_domains,
audience="https://api.example.com"
))
```

For hybrid mode (migration scenarios), resolver patterns, error handling, and caching configuration, see the full guides:

- **[Multi-Custom Domain Guide](docs/MultipleCustomDomain.md)** - Configuration modes, resolver patterns, migration, error handling
- **[Caching Guide](docs/Caching.md)** - Cache tuning, custom adapters (Redis, Memcached)

## Feedback

### Contributing
Expand Down
103 changes: 103 additions & 0 deletions docs/Caching.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Caching

The SDK caches OIDC discovery metadata and JWKS (JSON Web Key Sets) to avoid redundant network calls on every token verification. In MCD mode, each issuer domain gets its own cache entries.

## Default Behavior

By default, the SDK uses an in-memory LRU cache with:

- **TTL**: 600 seconds (10 minutes), or the server's `Cache-Control: max-age` value - whichever is lower
- **Max entries**: 100 per cache (discovery and JWKS caches are separate)
- **Eviction**: Least Recently Used (LRU) when max entries is reached

No configuration is needed for the default cache. It works well for single-server deployments.

## Configuration

### TTL and Max Entries

```python
from auth0_api_python import ApiClient, ApiClientOptions

api_client = ApiClient(ApiClientOptions(
domains=["tenant.auth0.com", "auth.example.com"],
audience="https://api.example.com",
cache_ttl_seconds=300, # 5 minutes max TTL
cache_max_entries=50 # 50 entries per cache
))
```

The effective TTL for each entry is `min(server_max_age, cache_ttl_seconds)`. Auth0 typically sends `Cache-Control: max-age=15` for discovery metadata, so the effective TTL will be 15 seconds even if you configure a higher value.

### Custom Cache Adapter

For distributed deployments (multiple servers, containers), use a shared cache backend by implementing `CacheAdapter`:

```python
import json
from typing import Any, Optional
from auth0_api_python import ApiClient, ApiClientOptions, CacheAdapter

class RedisCache(CacheAdapter):
def __init__(self, redis_client):
self.redis = redis_client

def get(self, key: str) -> Optional[Any]:
value = self.redis.get(key)
return json.loads(value) if value else None

def set(self, key: str, value: Any, ttl_seconds: Optional[int] = None) -> None:
serialized = json.dumps(value)
if ttl_seconds:
self.redis.set(key, serialized, ex=ttl_seconds)
else:
self.redis.set(key, serialized)

def delete(self, key: str) -> None:
self.redis.delete(key)

def clear(self) -> None:
# Be careful: this clears the entire Redis database
self.redis.flushdb()

# Usage
import redis
redis_client = redis.Redis(host="localhost", port=6379, db=0)

api_client = ApiClient(ApiClientOptions(
domains=["tenant.auth0.com", "auth.example.com"],
audience="https://api.example.com",
cache_adapter=RedisCache(redis_client)
))
```

When a custom adapter is provided, both the discovery cache and JWKS cache use the same adapter instance. Cache keys are inherently distinct β€” discovery keys are normalized issuer URLs (e.g., `https://tenant.auth0.com/`) and JWKS keys are `jwks_uri` values (e.g., `https://tenant.auth0.com/.well-known/jwks.json`).

**Note:** Because both caches share one adapter, entries share the same LRU eviction pool. A JWKS entry could evict a discovery entry (or vice versa) under memory pressure. Set `cache_max_entries` accordingly β€” recommended: `number_of_issuers Γ— 3`. With the default `InMemoryCache`, discovery and JWKS caches are separate and each gets its own `max_entries` budget.

## Tuning Recommendations

### TTL

- **Development**: Use a short TTL (e.g., `cache_ttl_seconds=10`) to pick up configuration changes quickly
- **Production**: The default (600 seconds) is a reasonable upper bound. Auth0's `Cache-Control: max-age` headers will typically set a lower effective TTL

### Max Entries

Each issuer domain consumes **2 cache entries** (one for discovery metadata, one for JWKS). Size the cache based on the number of distinct issuers you expect:

- **Static list with 3 domains**: `cache_max_entries=10` is more than enough
- **Dynamic resolver with many issuers**: Set to `(expected_issuers * 2) + buffer`

When the cache is full, the least recently used entry is evicted. A cache miss triggers a network fetch on the next verification for that issuer.

## CacheAdapter API

| Method | Signature | Description |
|---|---|---|
| `get` | `(key: str) -> Optional[Any]` | Return cached value or `None` if not found / expired |
| `set` | `(key: str, value: Any, ttl_seconds: Optional[int]) -> None` | Store value with optional TTL |
| `delete` | `(key: str) -> None` | Remove a single entry |
| `clear` | `() -> None` | Remove all entries |

All methods are synchronous. The `value` passed to `set` is a dictionary (parsed JSON from Auth0's OIDC and JWKS endpoints).
Loading