Skip to content

Feat/mcd support#75

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

Feat/mcd support#75
kishore7snehil wants to merge 2 commits intomainfrom
feat/mcd-support

Conversation

@kishore7snehil
Copy link
Contributor

📋 Changes

This PR implements Multiple Custom Domain (MCD) support for auth0-fastapi-api, enabling FastAPI APIs to accept tokens from multiple Auth0 custom domains with static lists, dynamic resolvers, and hybrid mode for zero-downtime domain migrations.

✨ Features

  • Multiple Custom Domain Verification: Accept tokens from multiple Auth0 domains via a domains parameter (static list or callable resolver) on Auth0FastAPI initialization
  • Dynamic Resolver Support: Runtime domain resolution via DomainsResolver callable with request context (DomainsResolverContext)
  • Hybrid Mode: Use domain and domains together for migration scenarios - domain drives client-initiated flows (token exchange, connection tokens), domains drives token verification
  • Per-Issuer Caching: OIDC discovery metadata and JWKS cached per issuer domain with configurable TTL, max entries, and LRU eviction via auth0-api-python core SDK
  • Pluggable Cache Backends: Support for custom cache backends (Redis, Memcached, etc.) through CacheAdapter interface with default InMemoryCache implementation
  • DPoP + MCD Compatibility: Full DPoP (Demonstrating Proof-of-Possession) support across multiple custom domains

🔧 API Changes

  • Extended Auth0FastAPI constructor with MCD parameters: domains, cache_ttl_seconds, cache_max_entries, cache_adapter
  • All MCD parameters passed through to underlying auth0-api-python SDK's ApiClientOptions
  • New type exports from fastapi_plugin: DomainsResolverContext, DomainsResolver, ConfigurationError, DomainsResolverError, CacheAdapter, InMemoryCache
  • Request context automatically passed to core SDK via verify_request() for resolver support

📖 Documentation

  • Updated README.md with comprehensive MCD section showing static lists, dynamic resolvers, hybrid mode, and cache configuration
  • Updated EXAMPLES.md with detailed MCD examples:

🧪 Testing

  • This change adds test coverage
  • This change has been tested on the latest version of the platform/language
Manual Integration Testing

Requires an Auth0 tenant with multiple custom domains configured and a machine-to-machine application with client credentials grant enabled.

import asyncio
import httpx
from fastapi import FastAPI, Depends
from fastapi_plugin import Auth0FastAPI

DOMAIN_DEFAULT = "<AUTH0_DOMAIN>"         # e.g. "dev-tenant.us.auth0.com"
DOMAIN_CD1 = "<CUSTOM_DOMAIN_1>"          # e.g. "auth.example.com"
DOMAIN_CD2 = "<CUSTOM_DOMAIN_2>"          # e.g. "auth.acme.org"
ALL_DOMAINS = [DOMAIN_DEFAULT, DOMAIN_CD1, DOMAIN_CD2]

CLIENT_ID = "<CLIENT_ID>"
CLIENT_SECRET = "<CLIENT_SECRET>"
AUDIENCE = "<API_AUDIENCE>"

# Helper: get an access token from a specific domain via client_credentials grant
async def get_token(domain: str) -> str:
    async with httpx.AsyncClient() as client:
        response = await client.post(
            f"https://{domain}/oauth/token",
            json={
                "grant_type": "client_credentials",
                "client_id": CLIENT_ID,
                "client_secret": CLIENT_SECRET,
                "audience": AUDIENCE
            }
        )
        return response.json()["access_token"]

async def test_multiple_custom_domain():
    # Setup FastAPI app with MCD
    app = FastAPI()
    auth0 = Auth0FastAPI(
        domains=ALL_DOMAINS,
        audience=AUDIENCE
    )
    
    @app.get("/protected")
    async def protected_route(claims=Depends(auth0.require_auth())):
        return {"user": claims["sub"], "issuer": claims["iss"]}
    
    from starlette.testclient import TestClient
    client = TestClient(app)
    
    # Test tokens from all configured domains
    for domain in ALL_DOMAINS:
        token = await get_token(domain)
        response = client.get(
            "/protected",
            headers={"Authorization": f"Bearer {token}"}
        )
        assert response.status_code == 200
        data = response.json()
        print(f"[PASS] {domain} -> iss={data['issuer']}, user={data['user']}")

asyncio.run(test_multiple_custom_domain())

Expected: All three domains succeed. Each token's iss matches its issuing domain and verification passes.

Contributor Checklist

@kishore7snehil kishore7snehil requested a review from a team as a code owner March 2, 2026 14:58
@@ -1,5 +1,18 @@
from typing import Dict, List

Check notice

Code scanning / CodeQL

Unused import Note test

Import of 'Dict' is not used.

Copilot Autofix

AI 2 days ago

To fix the problem, remove the unused symbol Dict from the typing import while keeping the used symbol List. This eliminates the unnecessary dependency and clarifies which types are actually needed.

Concretely, in tests/conftest.py, on line 1, change from typing import Dict, List to from typing import List. No other lines need modification, as List remains in use for the domains parameter annotation of setup_mcd_mocks. No additional methods, imports, or definitions are required.

Suggested changeset 1
tests/conftest.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/tests/conftest.py b/tests/conftest.py
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,4 +1,4 @@
-from typing import Dict, List
+from typing import List
 
 from pytest_httpx import HTTPXMock
 from .test_utils import PUBLIC_DPOP_JWK, PRIVATE_JWK
EOF
@@ -1,4 +1,4 @@
from typing import Dict, List
from typing import List

from pytest_httpx import HTTPXMock
from .test_utils import PUBLIC_DPOP_JWK, PRIVATE_JWK
Copilot is powered by AI and may make mistakes. Always verify output.
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