Skip to content

Commit c51ef46

Browse files
committed
Update ReadMe and Examples
1 parent 4956f27 commit c51ef46

2 files changed

Lines changed: 94 additions & 98 deletions

File tree

EXAMPLES.md

Lines changed: 86 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
- [Organizations](#organizations)
77
- [Extra parameters](#extra-parameters)
88
- [Roles](#roles)
9-
- [Multiple Custom Domains](#multiple-custom-domains)
9+
- [Multiple Custom Domain (MCD) Support](#multiple-custom-domain-mcd-support)
1010
- [Backchannel Logout](#backchannel-logout)
1111
- [Blazor Server](#blazor-server)
1212

@@ -315,13 +315,21 @@ public IActionResult Admin()
315315
}
316316
```
317317

318-
## Multiple Custom Domains
318+
## Multiple Custom Domain (MCD) Support
319319

320-
The SDK supports scenarios where a single application needs to authenticate users against **multiple Auth0 tenants** or **custom domains**. This is useful for multi-tenant SaaS applications where each customer has their own Auth0 tenant.
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.
321321

322-
### Basic Configuration
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.
323323

324-
Enable Multiple Custom Domains by calling `WithCustomDomains()` and providing a `DomainResolver` function:
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:
325333

326334
```csharp
327335
services.AddAuth0WebAppAuthentication(options =>
@@ -331,91 +339,103 @@ services.AddAuth0WebAppAuthentication(options =>
331339
})
332340
.WithCustomDomains(options =>
333341
{
342+
// Example: resolve from a custom header
334343
options.DomainResolver = httpContext =>
335344
{
336-
var host = httpContext.Request.Host.Host;
337-
338-
// Route to different Auth0 tenants based on subdomain
339-
if (host.StartsWith("tenant-a."))
340-
{
341-
return Task.FromResult("tenant-a.us.auth0.com");
342-
}
343-
344-
if (host.StartsWith("tenant-b."))
345-
{
346-
return Task.FromResult("tenant-b.us.auth0.com");
347-
}
348-
349-
// Default domain
350-
return Task.FromResult("your-default-domain.us.auth0.com");
345+
var tenant = httpContext.Request.Headers["X-Tenant-Domain"].FirstOrDefault();
346+
return Task.FromResult(tenant ?? "default-tenant.auth0.com");
351347
};
352348
});
353349
```
354350

355-
### Configuring the DomainResolver
356-
357-
The `DomainResolver` function receives the `HttpContext` and returns the Auth0 domain as a string. You can inspect any aspect of the HTTP request. The resolver can perform simple lookups or complex operations like querying a database:
351+
### Resolve domain from subdomain
358352

359353
```csharp
360-
options.DomainResolver = httpContext =>
354+
services.AddAuth0WebAppAuthentication(options =>
361355
{
362-
// Option 1: Based on subdomain
363-
var host = httpContext.Request.Host.Host;
364-
var subdomain = host.Split('.')[0];
365-
return Task.FromResult($"{subdomain}.us.auth0.com");
366-
367-
// Option 2: Based on path
368-
var path = httpContext.Request.Path.Value;
369-
if (path.StartsWith("/tenant-a"))
370-
return Task.FromResult("tenant-a.us.auth0.com");
371-
372-
// Option 3: Based on custom header
373-
var tenantHeader = httpContext.Request.Headers["X-Tenant-Id"].FirstOrDefault();
374-
return Task.FromResult($"{tenantHeader}.us.auth0.com");
375-
376-
// Option 4: Based on cookie
377-
var tenantCookie = httpContext.Request.Cookies["tenant"];
378-
return Task.FromResult($"{tenantCookie}.us.auth0.com");
379-
};
380-
381-
// Option 5: Complex resolution with database lookup
382-
options.DomainResolver = async httpContext =>
356+
options.Domain = Configuration["Auth0:Domain"];
357+
options.ClientId = Configuration["Auth0:ClientId"];
358+
})
359+
.WithCustomDomains(options =>
383360
{
384-
var host = httpContext.Request.Host.Host;
385-
var tenantId = host.Split('.')[0];
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
386372

387-
// Look up tenant configuration from database
388-
var tenantService = httpContext.RequestServices
389-
.GetRequiredService<ITenantService>();
390-
var tenant = await tenantService.GetTenantAsync(tenantId);
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:
391374

392-
return tenant?.Auth0Domain ?? "default.us.auth0.com";
393-
};
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);
394382
```
395383

396-
### Caching Configuration
384+
You must validate the host and scheme safely for your deployment to prevent open redirect attacks.
397385

398-
Control how OpenID Connect configurations are cached for each domain using `ConfigurationManagerCache`.
386+
### Legacy sessions and migration
399387

400-
#### MemoryConfigurationManagerCache (Default)
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).
401419

402-
Caches configurations in memory with configurable size and expiration:
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)
403423

404424
```csharp
405425
.WithCustomDomains(options =>
406426
{
407427
options.DomainResolver = httpContext => { /* ... */ };
408428

409429
options.ConfigurationManagerCache = new MemoryConfigurationManagerCache(
410-
maxSize: 100, // Maximum number of domains to cache
411-
slidingExpiration: TimeSpan.FromHours(1) // Optional: cache entries expire after 1 hour of inactivity
430+
maxSize: 100, // Maximum number of domains to cache
431+
slidingExpiration: TimeSpan.FromHours(1) // Optional: evict entries not accessed within 1 hour
412432
);
413433
});
414434
```
415435

416436
#### NullConfigurationManagerCache
417437

418-
Disables caching entirely (not recommended for production):
438+
Disables caching entirely — a new configuration manager is created on every request (not recommended for production):
419439

420440
```csharp
421441
.WithCustomDomains(options =>
@@ -427,49 +447,30 @@ Disables caching entirely (not recommended for production):
427447

428448
#### Custom Cache Implementation
429449

430-
Implement `IConfigurationManagerCache` for custom caching strategies (e.g., Redis, distributed cache):
450+
Implement `IConfigurationManagerCache` for custom caching strategies (e.g., a distributed cache):
431451

432452
```csharp
433-
public class RedisConfigurationManagerCache : IConfigurationManagerCache
453+
public class MyCustomConfigurationManagerCache : IConfigurationManagerCache
434454
{
435-
private readonly IDistributedCache _cache;
436-
437-
public RedisConfigurationManagerCache(IDistributedCache cache)
438-
{
439-
_cache = cache;
440-
}
441-
442455
public IConfigurationManager<OpenIdConnectConfiguration> GetOrCreate(
443456
string metadataAddress,
444457
Func<string, IConfigurationManager<OpenIdConnectConfiguration>> factory)
445458
{
446-
// Implement Redis-based caching logic
459+
// Return a cached instance or call factory(metadataAddress) to create one
447460
}
448461

449-
public void Clear() => _cache.Remove("oidc-configs");
450-
public void Dispose() { }
462+
public void Clear() { /* Evict all entries */ }
463+
public void Dispose() { /* Clean up resources */ }
451464
}
452465

453466
// Usage
454467
.WithCustomDomains(options =>
455468
{
456469
options.DomainResolver = httpContext => { /* ... */ };
457-
options.ConfigurationManagerCache = new RedisConfigurationManagerCache(distributedCache);
470+
options.ConfigurationManagerCache = new MyCustomConfigurationManagerCache();
458471
});
459472
```
460473

461-
### Important Considerations
462-
463-
1. **Domain Resolution Timing**: The `DomainResolver` is called early in the request pipeline and the result is cached for the duration of the request.
464-
465-
2. **Callback URLs**: Each Auth0 tenant must have the appropriate callback URLs configured:
466-
- **Allowed Callback URLs**: `https://{your-domain}/callback`
467-
- **Allowed Logout URLs**: `https://{your-domain}/`
468-
469-
3. **Token Validation**: The SDK automatically validates that tokens come from the expected issuer (the domain returned by your `DomainResolver`).
470-
471-
4. **Performance**: Use the default `MemoryConfigurationManagerCache` for most scenarios. The cache significantly reduces the overhead of fetching OIDC configuration for each request.
472-
473474
## Backchannel Logout
474475

475476
Backchannel logout can be configured by calling `WithBackchannelLogout()` when calling `AddAuth0WebAppAuthentication`.

README.md

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,11 @@ For more code samples on how to integrate the **auth0-aspnetcore-authentication*
114114

115115
> This SDK also works with Blazor Server, for more info see [the Blazor Server section in our examples](https://github.com/auth0/auth0-aspnetcore-authentication/blob/main/EXAMPLES.md#blazor-server).
116116
117-
## Multiple Custom Domains
117+
## Multiple Custom Domain (MCD) Support
118118

119-
The SDK supports scenarios where a single application needs to authenticate users against **multiple Auth0 tenants** or **custom domains**. This is useful for multi-tenant SaaS applications where each customer has their own Auth0 tenant.
119+
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.
120+
121+
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.
120122

121123
### Configuration
122124

@@ -128,23 +130,16 @@ services.AddAuth0WebAppAuthentication(options =>
128130
})
129131
.WithCustomDomains(options =>
130132
{
133+
// Example: resolve from a custom header
131134
options.DomainResolver = httpContext =>
132135
{
133-
var host = httpContext.Request.Host.Host;
134-
135-
// Route to different Auth0 tenants based on subdomain
136-
if (host.StartsWith("tenant-a."))
137-
return Task.FromResult("tenant-a.us.auth0.com");
138-
139-
if (host.StartsWith("tenant-b."))
140-
return Task.FromResult("tenant-b.us.auth0.com");
141-
142-
return Task.FromResult("default.us.auth0.com");
136+
var tenant = httpContext.Request.Headers["X-Tenant-Domain"].FirstOrDefault();
137+
return Task.FromResult(tenant ?? "default-tenant.auth0.com");
143138
};
144139
});
145140
```
146141

147-
For detailed configuration options, caching strategies, and more examples, see the [Multiple Custom Domains Examples](EXAMPLES.md#multiple-custom-domains).
142+
For detailed configuration options, caching strategies, security requirements, and more examples, see the [Multiple Custom Domain (MCD) Examples](EXAMPLES.md#multiple-custom-domain-mcd-support).
148143

149144
## API reference
150145
Explore public API's available in auth0-aspnetcore-authentication.

0 commit comments

Comments
 (0)