|
6 | 6 | - [Organizations](#organizations) |
7 | 7 | - [Extra parameters](#extra-parameters) |
8 | 8 | - [Roles](#roles) |
| 9 | +- [Multiple Custom Domain (MCD) Support](#multiple-custom-domain-mcd-support) |
9 | 10 | - [Backchannel Logout](#backchannel-logout) |
10 | 11 | - [Blazor Server](#blazor-server) |
11 | 12 |
|
@@ -314,6 +315,162 @@ public IActionResult Admin() |
314 | 315 | } |
315 | 316 | ``` |
316 | 317 |
|
| 318 | +## Multiple Custom Domain (MCD) Support |
| 319 | + |
| 320 | +Multiple Custom Domains (MCD) lets you resolve the Auth0 domain per request while keeping a single SDK instance. This is useful when one application serves multiple custom domains (for example, `brand-1.my-app.com` and `brand-2.my-app.com`), each mapped to a different `Auth0` custom domain. |
| 321 | + |
| 322 | +`MCD` is enabled by providing a `DomainResolver` function instead of a static domain string, enabling you to dynamically define the `Auth0` custom domain at run-time. |
| 323 | + |
| 324 | +Resolver mode is intended for the custom domains of a single `Auth0` tenant. It is not a supported way to connect multiple `Auth0` tenants to one application. |
| 325 | + |
| 326 | +### Dynamic Domain Resolver |
| 327 | + |
| 328 | +Provide a resolver function to select the domain at runtime. The resolver should return the `Auth0 Custom Domain` (for example, `brand-1.custom-domain.com`). Returning `null` or an empty value throws `InvalidOperationException`. |
| 329 | + |
| 330 | +### Configure with a DomainResolver |
| 331 | + |
| 332 | +Call `WithCustomDomains()` and provide a `DomainResolver` to resolve the domain dynamically based on the incoming request. The domain can be derived from a subdomain, request header, query parameter, or any other request attribute: |
| 333 | + |
| 334 | +```csharp |
| 335 | +services.AddAuth0WebAppAuthentication(options => |
| 336 | +{ |
| 337 | + options.Domain = Configuration["Auth0:Domain"]; |
| 338 | + options.ClientId = Configuration["Auth0:ClientId"]; |
| 339 | +}) |
| 340 | +.WithCustomDomains(options => |
| 341 | +{ |
| 342 | + // Example: resolve from a custom header |
| 343 | + options.DomainResolver = httpContext => |
| 344 | + { |
| 345 | + var tenant = httpContext.Request.Headers["X-Tenant-Domain"].FirstOrDefault(); |
| 346 | + return Task.FromResult(tenant ?? "default-tenant.auth0.com"); |
| 347 | + }; |
| 348 | +}); |
| 349 | +``` |
| 350 | + |
| 351 | +### Resolve domain from subdomain |
| 352 | + |
| 353 | +```csharp |
| 354 | +services.AddAuth0WebAppAuthentication(options => |
| 355 | +{ |
| 356 | + options.Domain = Configuration["Auth0:Domain"]; |
| 357 | + options.ClientId = Configuration["Auth0:ClientId"]; |
| 358 | +}) |
| 359 | +.WithCustomDomains(options => |
| 360 | +{ |
| 361 | + // e.g., "acme.myapp.com" -> "acme.auth0.com" |
| 362 | + options.DomainResolver = httpContext => |
| 363 | + { |
| 364 | + var host = httpContext.Request.Host.Host; |
| 365 | + var subdomain = host.Split('.')[0]; |
| 366 | + return Task.FromResult($"{subdomain}.auth0.com"); |
| 367 | + }; |
| 368 | +}); |
| 369 | +``` |
| 370 | + |
| 371 | +### Redirect URI requirements |
| 372 | + |
| 373 | +When using MCD, the `redirectUri` must be an **absolute URL**. In MCD deployments, you will typically resolve the redirect URI per request so each domain uses the correct callback URL: |
| 374 | + |
| 375 | +```csharp |
| 376 | +var authenticationProperties = new LoginAuthenticationPropertiesBuilder() |
| 377 | + // Resolve redirect URI based on the incoming request's host |
| 378 | + .WithRedirectUri($"{HttpContext.Request.Scheme}://{HttpContext.Request.Host}/callback") |
| 379 | + .Build(); |
| 380 | + |
| 381 | +await HttpContext.ChallengeAsync(Auth0Constants.AuthenticationScheme, authenticationProperties); |
| 382 | +``` |
| 383 | + |
| 384 | +You must validate the host and scheme safely for your deployment to prevent open redirect attacks. |
| 385 | + |
| 386 | +### Legacy sessions and migration |
| 387 | + |
| 388 | +When moving from a static domain setup to a `DomainResolver`, existing sessions can continue to work if the resolver returns the same Auth0 custom domain that was used for those legacy sessions. |
| 389 | + |
| 390 | +If the resolver returns a different domain, the SDK treats the session as missing and requires the user to sign in again. This is intentional to keep sessions isolated per domain. |
| 391 | + |
| 392 | +### Security requirements |
| 393 | + |
| 394 | +When configuring the `DomainResolver`, you are responsible for ensuring that all resolved domains are trusted. Mis-configuring the domain resolver is a critical security risk that can lead to authentication bypass on the relying party (RP) or expose the application to Server-Side Request Forgery (SSRF). |
| 395 | + |
| 396 | +**Single tenant limitation:** |
| 397 | +The `DomainResolver` is intended solely for multiple custom domains belonging to the same Auth0 tenant. It is not a supported mechanism for connecting multiple Auth0 tenants to a single application. |
| 398 | + |
| 399 | +**Secure proxy requirement:** |
| 400 | +When using MCD, your application must be deployed behind a secure edge or reverse proxy (e.g., Cloudflare, Nginx, or AWS ALB). The proxy must be configured to sanitize and overwrite `Host` and `X-Forwarded-Host` headers before they reach your application. |
| 401 | + |
| 402 | +Without a trusted proxy layer to validate these headers, an attacker can manipulate the domain resolution process. This can result in malicious redirects, where users are sent to unauthorized or fraudulent endpoints during the login and logout flows. |
| 403 | + |
| 404 | +### Configuration Manager Cache |
| 405 | + |
| 406 | +You can control how OpenID Connect configuration managers are cached per domain with `ConfigurationManagerCache`. |
| 407 | + |
| 408 | +By default, the SDK uses an in-memory cache with: |
| 409 | +- `maxSize: 100` entries |
| 410 | +- No expiration (entries remain until evicted by size pressure) |
| 411 | + |
| 412 | +The cache is keyed by the OIDC metadata endpoint URL (e.g., `https://brand-1.custom-domain.com/.well-known/openid-configuration`). Each distinct domain resolved by `DomainResolver` occupies one cache entry. |
| 413 | + |
| 414 | +Most applications can keep the defaults, but you may want to adjust them in the following cases: |
| 415 | +- Increase `maxSize` if one process may verify tokens for more than 100 distinct domains during its lifetime. |
| 416 | +- Decrease `maxSize` if memory usage matters more than avoiding repeated OIDC discovery setup. |
| 417 | +- Set `slidingExpiration` if you want entries that haven't been accessed within a given duration to be evicted automatically. |
| 418 | +- Use `NullConfigurationManagerCache` to disable caching entirely (not recommended for production). |
| 419 | + |
| 420 | +Rule of thumb: set `maxSize` to cover the number of distinct domains a single process is expected to serve, with some headroom. |
| 421 | + |
| 422 | +#### MemoryConfigurationManagerCache (Default) |
| 423 | + |
| 424 | +```csharp |
| 425 | +.WithCustomDomains(options => |
| 426 | +{ |
| 427 | + options.DomainResolver = httpContext => { /* ... */ }; |
| 428 | + |
| 429 | + options.ConfigurationManagerCache = new MemoryConfigurationManagerCache( |
| 430 | + maxSize: 100, // Maximum number of domains to cache |
| 431 | + slidingExpiration: TimeSpan.FromHours(1) // Optional: evict entries not accessed within 1 hour |
| 432 | + ); |
| 433 | +}); |
| 434 | +``` |
| 435 | + |
| 436 | +#### NullConfigurationManagerCache |
| 437 | + |
| 438 | +Disables caching entirely — a new configuration manager is created on every request (not recommended for production): |
| 439 | + |
| 440 | +```csharp |
| 441 | +.WithCustomDomains(options => |
| 442 | +{ |
| 443 | + options.DomainResolver = httpContext => { /* ... */ }; |
| 444 | + options.ConfigurationManagerCache = new NullConfigurationManagerCache(); |
| 445 | +}); |
| 446 | +``` |
| 447 | + |
| 448 | +#### Custom Cache Implementation |
| 449 | + |
| 450 | +Implement `IConfigurationManagerCache` for custom caching strategies (e.g., a distributed cache): |
| 451 | + |
| 452 | +```csharp |
| 453 | +public class MyCustomConfigurationManagerCache : IConfigurationManagerCache |
| 454 | +{ |
| 455 | + public IConfigurationManager<OpenIdConnectConfiguration> GetOrCreate( |
| 456 | + string metadataAddress, |
| 457 | + Func<string, IConfigurationManager<OpenIdConnectConfiguration>> factory) |
| 458 | + { |
| 459 | + // Return a cached instance or call factory(metadataAddress) to create one |
| 460 | + } |
| 461 | + |
| 462 | + public void Clear() { /* Evict all entries */ } |
| 463 | + public void Dispose() { /* Clean up resources */ } |
| 464 | +} |
| 465 | + |
| 466 | +// Usage |
| 467 | +.WithCustomDomains(options => |
| 468 | +{ |
| 469 | + options.DomainResolver = httpContext => { /* ... */ }; |
| 470 | + options.ConfigurationManagerCache = new MyCustomConfigurationManagerCache(); |
| 471 | +}); |
| 472 | +``` |
| 473 | + |
317 | 474 | ## Backchannel Logout |
318 | 475 |
|
319 | 476 | Backchannel logout can be configured by calling `WithBackchannelLogout()` when calling `AddAuth0WebAppAuthentication`. |
|
0 commit comments