Skip to content

feat: Add Multiple Custom Domains (MCD) support#106

Open
kishore7snehil wants to merge 4 commits intomainfrom
feat/mcd-support
Open

feat: Add Multiple Custom Domains (MCD) support#106
kishore7snehil wants to merge 4 commits intomainfrom
feat/mcd-support

Conversation

@kishore7snehil
Copy link
Contributor

@kishore7snehil kishore7snehil commented Feb 11, 2026

This PR adds Multiple Custom Domains (MCD) support to auth0-fastapi, enabling FastAPI applications to use multiple custom domains configured on the same Auth0 tenant. This wrapper leverages the MCD implementation in auth0-server-python while handling FastAPI-specific concerns like dynamic redirect URI construction.

✨ Features

1. Dynamic Domain Configuration

  • Callable Domain Support: Accept Callable as domain parameter in Auth0Config
  • Dynamic Redirect URI: Automatically builds redirect_uri from request host when MCD enabled
  • Backward Compatible: Static string domains continue to work unchanged

Example:

from auth0_server_python.auth_types import DomainResolverContext

async def domain_resolver(context: DomainResolverContext) -> str:
    host = context.request_headers.get('host', '').split(':')[0]
    return DOMAIN_MAP.get(host, DEFAULT_DOMAIN)

config = Auth0Config(
    domain=domain_resolver,  # Callable triggers MCD mode
    client_id="...",
    client_secret="...",
    app_base_url="https://myapp.com",
    secret="...",
)

2. Request Base URL Builder

  • Proxy Header Support: Handles x-forwarded-host and x-forwarded-proto
  • Port Normalization: Removes standard ports (80, 443) from URLs
  • MCD Ready: Different custom domains get different redirect URIs

Example:

# Request to https://auth.acme.com/auth/login
# → redirect_uri = https://acme.myapp.com/auth/callback

# Request to https://auth.globex.com/auth/login
# → redirect_uri = https://globex.myapp.com/auth/callback

3. Route Handler Updates

  • Login: Builds dynamic redirect_uri when domain is callable
  • Callback: Uses dynamic base URL for safe redirects
  • Logout: Builds dynamic returnTo URL for MCD scenarios

🔄 Compatibility

** Backward Compatible** - No breaking changes for existing users.

Existing Usage (Unchanged)

# Static domain - works exactly as before
config = Auth0Config(domain="auth.acme.com", ...)

New Usage (Optional)

# Dynamic MCD - new opt-in feature
config = Auth0Config(domain=domain_resolver, ...)

📊 Testing

Unit Tests

14 MCD-specific tests in TestMCDSupport covering URL building, login/callback/logout routes, and dependency injection.

Manual Integration Testing

Prerequisites:

  • One Auth0 tenant with two custom domains configured (e.g., auth.acme.com and auth.globex.com)
  • A Regular Web Application in that tenant
  • Add local hostnames to /etc/hosts:
    127.0.0.1 acme.myapp.com globex.myapp.com
    

Setup:

  1. Configure two custom domains in your Auth0 tenant (Settings → Custom Domains)

  2. Register callback URLs in the application:

    • http://acme.myapp.com:3000/auth/callback
    • http://globex.myapp.com:3000/auth/callback
  3. Create a FastAPI app with MCD:

import os
import uvicorn
from fastapi import FastAPI, Request, Response
from fastapi.responses import HTMLResponse
from starlette.middleware.sessions import SessionMiddleware

from auth0_fastapi.config import Auth0Config
from auth0_fastapi.auth.auth_client import AuthClient
from auth0_fastapi.server.routes import router, register_auth_routes
from auth0_server_python.auth_types import DomainResolverContext

# Two custom domains on the same Auth0 tenant
DOMAIN_MAP = {
    "acme.myapp.com": "auth.acme.com",
    "globex.myapp.com": "auth.globex.com",
}
DEFAULT_DOMAIN = "auth.acme.com"

async def domain_resolver(context: DomainResolverContext) -> str:
    host = (context.request_headers or {}).get("x-forwarded-host") or \
           (context.request_headers or {}).get("host", "")
    host = host.split(":")[0]
    return DOMAIN_MAP.get(host, DEFAULT_DOMAIN)

app = FastAPI()
app.add_middleware(SessionMiddleware, secret_key=os.environ["APP_SECRET"])

config = Auth0Config(
    domain=domain_resolver,
    clientId=os.environ["AUTH0_CLIENT_ID"],
    clientSecret=os.environ["AUTH0_CLIENT_SECRET"],
    appBaseUrl="http://localhost:3000",
    secret=os.environ["APP_SECRET"],
    mount_routes=True,
    authorization_params={"scope": "openid profile email"},
)

app.state.config = config
auth_client = AuthClient(config)
app.state.auth_client = auth_client

register_auth_routes(router, config)
app.include_router(router)

@app.get("/", response_class=HTMLResponse)
async def home(request: Request):
    user = await auth_client.client.get_user(store_options={"request": request})
    host = request.headers.get("host", "localhost")
    domain = DOMAIN_MAP.get(host.split(":")[0], DEFAULT_DOMAIN)
    if user:
        name = user.get("name", user.get("email", "Unknown"))
        return f"<p>Host: {host} | Domain: {domain}</p><p>Logged in as: {name}</p><a href='/auth/logout'>Logout</a>"
    return f"<p>Host: {host} | Domain: {domain}</p><p>Not logged in</p><a href='/auth/login'>Login</a>"

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=3000)
  1. Start the app: python server.py

Test cases:

# Test Steps Expected
1 Login via custom domain A Visit http://acme.myapp.com:3000/auth/login Redirects to auth.acme.com/authorize
2 Login via custom domain B Visit http://globex.myapp.com:3000/auth/login Redirects to auth.globex.com/authorize
3 Callback validates issuer Complete login via domain A Callback succeeds, token issuer matches auth.acme.com
4 Session bound to origin domain Log in via domain A, then visit http://globex.myapp.com:3000/ Shows "Not logged in" (session bound to domain A)
5 Independent sessions per domain Log in via domain B separately Both sessions exist independently on the same tenant
6 Dynamic redirect_uri Log in via each domain redirect_uri in authorize URL matches the request host, not appBaseUrl
7 Logout is domain-specific Log out via domain A Only domain A session cleared; domain B session remains
8 Static domain backward compat Create Auth0Config with domain="auth.acme.com" (string) All flows work as before without any resolver

📚 Documentation

Document Description
examples/MultipleCustomDomains.md Developer guide with usage patterns

@kishore7snehil kishore7snehil requested a review from a team as a code owner February 11, 2026 13:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant