Skip to content

Commit de8cc39

Browse files
feat: add Multiple Custom Domain Support
1 parent a915434 commit de8cc39

15 files changed

Lines changed: 2755 additions & 207 deletions

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ env/
1515

1616
#Build files
1717
dist
18-
docs
1918

2019
#testfile
2120
setup.py

README.md

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ This SDK provides comprehensive support for securing APIs with Auth0-issued acce
1717

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

283+
### 7. Multi-Custom Domain (MCD) Support
284+
285+
If your Auth0 tenant has multiple custom domains, or you're migrating between domains, the SDK can accept tokens from any of them:
286+
287+
#### Static Domain List
288+
289+
```python
290+
from auth0_api_python import ApiClient, ApiClientOptions
291+
292+
api_client = ApiClient(ApiClientOptions(
293+
domains=[
294+
"tenant.auth0.com",
295+
"auth.example.com",
296+
"auth.acme.org"
297+
],
298+
audience="https://api.example.com"
299+
))
300+
301+
# Tokens from any of the three domains are accepted
302+
claims = await api_client.verify_access_token(access_token)
303+
```
304+
305+
#### Dynamic Resolver
306+
307+
For runtime domain resolution based on request context:
308+
309+
```python
310+
from auth0_api_python import ApiClient, ApiClientOptions, DomainsResolverContext
311+
312+
def resolve_domains(context: DomainsResolverContext) -> list[str]:
313+
# Determine allowed domains based on the request
314+
return ["tenant.auth0.com", "auth.example.com"]
315+
316+
api_client = ApiClient(ApiClientOptions(
317+
domains=resolve_domains,
318+
audience="https://api.example.com"
319+
))
320+
```
321+
322+
For hybrid mode (migration scenarios), resolver patterns, error handling, and caching configuration, see the full guides:
323+
324+
- **[Multi-Custom Domain Guide](docs/MultipleCustomDomain.md)** - Configuration modes, resolver patterns, migration, error handling
325+
- **[Caching Guide](docs/Caching.md)** - Cache tuning, custom adapters (Redis, Memcached)
326+
282327
## Feedback
283328

284329
### Contributing

docs/Caching.md

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# Caching
2+
3+
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.
4+
5+
## Default Behavior
6+
7+
By default, the SDK uses an in-memory LRU cache with:
8+
9+
- **TTL**: 600 seconds (10 minutes), or the server's `Cache-Control: max-age` value - whichever is lower
10+
- **Max entries**: 100 per cache (discovery and JWKS caches are separate)
11+
- **Eviction**: Least Recently Used (LRU) when max entries is reached
12+
13+
No configuration is needed for the default cache. It works well for single-server deployments.
14+
15+
## Configuration
16+
17+
### TTL and Max Entries
18+
19+
```python
20+
from auth0_api_python import ApiClient, ApiClientOptions
21+
22+
api_client = ApiClient(ApiClientOptions(
23+
domains=["tenant.auth0.com", "auth.example.com"],
24+
audience="https://api.example.com",
25+
cache_ttl_seconds=300, # 5 minutes max TTL
26+
cache_max_entries=50 # 50 entries per cache
27+
))
28+
```
29+
30+
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.
31+
32+
### Custom Cache Adapter
33+
34+
For distributed deployments (multiple servers, containers), use a shared cache backend by implementing `CacheAdapter`:
35+
36+
```python
37+
import json
38+
from typing import Any, Optional
39+
from auth0_api_python import ApiClient, ApiClientOptions, CacheAdapter
40+
41+
class RedisCache(CacheAdapter):
42+
def __init__(self, redis_client):
43+
self.redis = redis_client
44+
45+
def get(self, key: str) -> Optional[Any]:
46+
value = self.redis.get(key)
47+
return json.loads(value) if value else None
48+
49+
def set(self, key: str, value: Any, ttl_seconds: Optional[int] = None) -> None:
50+
serialized = json.dumps(value)
51+
if ttl_seconds:
52+
self.redis.set(key, serialized, ex=ttl_seconds)
53+
else:
54+
self.redis.set(key, serialized)
55+
56+
def delete(self, key: str) -> None:
57+
self.redis.delete(key)
58+
59+
def clear(self) -> None:
60+
# Be careful: this clears the entire Redis database
61+
self.redis.flushdb()
62+
63+
# Usage
64+
import redis
65+
redis_client = redis.Redis(host="localhost", port=6379, db=0)
66+
67+
api_client = ApiClient(ApiClientOptions(
68+
domains=["tenant.auth0.com", "auth.example.com"],
69+
audience="https://api.example.com",
70+
cache_adapter=RedisCache(redis_client)
71+
))
72+
```
73+
74+
When a custom adapter is provided, both the discovery cache and JWKS cache use it. 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`).
75+
76+
## Tuning Recommendations
77+
78+
### TTL
79+
80+
- **Development**: Use a short TTL (e.g., `cache_ttl_seconds=10`) to pick up configuration changes quickly
81+
- **Production**: The default (600 seconds) is a reasonable upper bound. Auth0's `Cache-Control: max-age` headers will typically set a lower effective TTL
82+
83+
### Max Entries
84+
85+
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:
86+
87+
- **Static list with 3 domains**: `cache_max_entries=10` is more than enough
88+
- **Dynamic resolver with many issuers**: Set to `(expected_issuers * 2) + buffer`
89+
90+
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.
91+
92+
## CacheAdapter API
93+
94+
| Method | Signature | Description |
95+
|---|---|---|
96+
| `get` | `(key: str) -> Optional[Any]` | Return cached value or `None` if not found / expired |
97+
| `set` | `(key: str, value: Any, ttl_seconds: Optional[int]) -> None` | Store value with optional TTL |
98+
| `delete` | `(key: str) -> None` | Remove a single entry |
99+
| `clear` | `() -> None` | Remove all entries |
100+
101+
All methods are synchronous. The `value` passed to `set` is a dictionary (parsed JSON from Auth0's OIDC and JWKS endpoints).

0 commit comments

Comments
 (0)