Skip to content

Commit ac24ef7

Browse files
feat: add Multiple Custom Domains (MCD) support and fix JWT verification
1 parent fe57431 commit ac24ef7

7 files changed

Lines changed: 1778 additions & 110 deletions

File tree

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,6 @@ test.py
2424
test-script.py
2525
.coverage
2626
coverage.xml
27-
27+
examples/mcd-poc
28+
IMPLEMENTATION_NOTES.md
29+
examples/MCD_DEVELOPER_GUIDE.md

examples/MCD.md

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
# Multiple Custom Domains (MCD) Guide
2+
3+
This guide explains how to implement Multiple Custom Domain (MCD) support using the Auth0 Python SDKs.
4+
5+
## What is MCD?
6+
7+
Multiple Custom Domains (MCD) allows your application to serve different organizations or tenants from different hostnames, each mapping to a different Auth0 tenant/domain.
8+
9+
**Example:**
10+
- `https://acme.yourapp.com` → Auth0 tenant: `acme.auth0.com`
11+
- `https://globex.yourapp.com` → Auth0 tenant: `globex.auth0.com`
12+
13+
Each tenant gets its own branded login experience while using a single application codebase.
14+
15+
## Configuration Methods
16+
17+
### Method 1: Static Domain (Single Tenant)
18+
19+
For applications with a single Auth0 domain:
20+
21+
```python
22+
from auth0_server_python import ServerClient
23+
24+
client = ServerClient(
25+
domain="your-tenant.auth0.com", # Static string
26+
client_id="your_client_id",
27+
client_secret="your_client_secret",
28+
secret="your_encryption_secret"
29+
)
30+
```
31+
32+
### Method 2: Dynamic Domain Resolver (MCD)
33+
34+
For MCD support, provide a domain resolver function that receives a `DomainResolverContext`:
35+
36+
```python
37+
from auth0_server_python import ServerClient
38+
from auth0_server_python.auth_types import DomainResolverContext
39+
40+
# Map your app hostnames to Auth0 domains
41+
DOMAIN_MAP = {
42+
"acme.yourapp.com": "acme.auth0.com",
43+
"globex.yourapp.com": "globex.auth0.com",
44+
}
45+
DEFAULT_DOMAIN = "default.auth0.com"
46+
47+
async def domain_resolver(context: DomainResolverContext) -> str:
48+
"""
49+
Resolve Auth0 domain based on request hostname.
50+
51+
Args:
52+
context: Contains request_url and request_headers
53+
54+
Returns:
55+
Auth0 domain string (e.g., "acme.auth0.com")
56+
"""
57+
# Extract hostname from request headers
58+
if not context.request_headers:
59+
return DEFAULT_DOMAIN
60+
61+
host = context.request_headers.get('host', DEFAULT_DOMAIN)
62+
host_without_port = host.split(':')[0]
63+
64+
# Look up Auth0 domain
65+
return DOMAIN_MAP.get(host_without_port, DEFAULT_DOMAIN)
66+
67+
client = ServerClient(
68+
domain=domain_resolver, # Callable function
69+
client_id="your_client_id",
70+
client_secret="your_client_secret",
71+
secret="your_encryption_secret"
72+
)
73+
```
74+
75+
## DomainResolverContext
76+
77+
The `DomainResolverContext` object provides request information to your resolver:
78+
79+
| Property | Type | Description |
80+
|----------|------|-------------|
81+
| `request_url` | `Optional[str]` | Full request URL (e.g., "https://acme.yourapp.com/auth/login") |
82+
| `request_headers` | `Optional[dict[str, str]]` | Request headers dictionary |
83+
84+
**Common headers:**
85+
- `host`: Request hostname (e.g., "acme.yourapp.com")
86+
- `x-forwarded-host`: Original host when behind proxy/load balancer
87+
88+
**Example usage:**
89+
90+
```python
91+
async def domain_resolver(context: DomainResolverContext) -> str:
92+
# Check if we have request headers
93+
if not context.request_headers:
94+
return DEFAULT_DOMAIN
95+
96+
# Use x-forwarded-host if behind proxy, otherwise use host
97+
host = (context.request_headers.get('x-forwarded-host') or
98+
context.request_headers.get('host', ''))
99+
100+
# Remove port number if present
101+
hostname = host.split(':')[0].lower()
102+
103+
# Look up in mapping
104+
return DOMAIN_MAP.get(hostname, DEFAULT_DOMAIN)
105+
```
106+
107+
## Error Handling
108+
109+
### DomainResolverError
110+
111+
The domain resolver should return a valid Auth0 domain string. Invalid returns will raise `DomainResolverError`:
112+
113+
```python
114+
from auth0_server_python.error import DomainResolverError
115+
116+
async def domain_resolver(context: DomainResolverContext) -> str:
117+
try:
118+
domain = lookup_domain_from_db(context)
119+
120+
if not domain:
121+
# Return default instead of None
122+
return DEFAULT_DOMAIN
123+
124+
return domain # Must be a non-empty string
125+
126+
except Exception as e:
127+
# Log error and return default
128+
logger.error(f"Domain resolution failed: {e}")
129+
return DEFAULT_DOMAIN
130+
```
131+
132+
**Invalid return values that raise `DomainResolverError`:**
133+
- `None`
134+
- Empty string `""`
135+
- Non-string types (int, list, dict, etc.)
136+
137+
**Exceptions raised by your resolver:**
138+
- Automatically wrapped in `DomainResolverError`
139+
- Original exception accessible via `.original_error`

0 commit comments

Comments
 (0)