Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
f5fc39b
Adds Auth0CustomDomainsStartupFilter to resolve the custom domain usi…
kailash-b Jan 8, 2026
90a5ba0
Adds Auth0CustomDomainsCookieManager for domain scoped cookie management
kailash-b Jan 9, 2026
2cdaeea
Adds Auth0CustomDomainsOpenIdConnectConfigurationManager for domain s…
kailash-b Jan 12, 2026
7642428
Adds Auth0CustomDomainsOpenIdConnectPostConfigureOptions
kailash-b Jan 12, 2026
60adbfd
Update OpenIdConnectEvents to handle multiple custom domains
kailash-b Jan 13, 2026
c133861
Adds extensions and configuration for users to setup custom domains
kailash-b Jan 13, 2026
2cbe41b
Removes custom cookie manager
kailash-b Jan 13, 2026
63ff187
Refactoring to extract common code to Utils
kailash-b Jan 14, 2026
5bb1971
Handles BackChannelLogout scenario
kailash-b Jan 14, 2026
00b2f68
Adds unit tests on Auth0CustomDomainsOpenIdConnectConfigurationManager
kailash-b Jan 14, 2026
af45f56
Adds unit tests on Auth0CustomDomainStartupFiler and PostConfigureOpt…
kailash-b Jan 14, 2026
ef527af
Refactoring, test case addition and docs changes
kailash-b Jan 14, 2026
0814dad
Address review comments
kailash-b Jan 15, 2026
7a02d69
Improve caching behaviour
kailash-b Jan 21, 2026
b87848b
Updates ReadMe and Examples.md
kailash-b Feb 17, 2026
6c83889
Add test cases for edge cases
kailash-b Mar 24, 2026
09aca42
Address Security review comments
kailash-b Mar 31, 2026
4956f27
Handle back channel logout when multiple custom-domains are enabled
kailash-b Apr 7, 2026
c51ef46
Update ReadMe and Examples
kailash-b Apr 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 157 additions & 0 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- [Organizations](#organizations)
- [Extra parameters](#extra-parameters)
- [Roles](#roles)
- [Multiple Custom Domain (MCD) Support](#multiple-custom-domain-mcd-support)
- [Backchannel Logout](#backchannel-logout)
- [Blazor Server](#blazor-server)

Expand Down Expand Up @@ -314,6 +315,162 @@ public IActionResult Admin()
}
```

## Multiple Custom Domain (MCD) Support

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.

`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.

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.

### Dynamic Domain Resolver

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`.

### Configure with a DomainResolver

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:

```csharp
services.AddAuth0WebAppAuthentication(options =>
{
options.Domain = Configuration["Auth0:Domain"];
options.ClientId = Configuration["Auth0:ClientId"];
})
.WithCustomDomains(options =>
{
// Example: resolve from a custom header
options.DomainResolver = httpContext =>
{
var tenant = httpContext.Request.Headers["X-Tenant-Domain"].FirstOrDefault();
return Task.FromResult(tenant ?? "default-tenant.auth0.com");
};
});
```

### Resolve domain from subdomain

```csharp
services.AddAuth0WebAppAuthentication(options =>
{
options.Domain = Configuration["Auth0:Domain"];
options.ClientId = Configuration["Auth0:ClientId"];
})
.WithCustomDomains(options =>
{
// e.g., "acme.myapp.com" -> "acme.auth0.com"
options.DomainResolver = httpContext =>
{
var host = httpContext.Request.Host.Host;
var subdomain = host.Split('.')[0];
return Task.FromResult($"{subdomain}.auth0.com");
};
});
```

### Redirect URI requirements

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:

```csharp
var authenticationProperties = new LoginAuthenticationPropertiesBuilder()
// Resolve redirect URI based on the incoming request's host
.WithRedirectUri($"{HttpContext.Request.Scheme}://{HttpContext.Request.Host}/callback")
.Build();

await HttpContext.ChallengeAsync(Auth0Constants.AuthenticationScheme, authenticationProperties);
```

You must validate the host and scheme safely for your deployment to prevent open redirect attacks.

### Legacy sessions and migration

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.

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.

### Security requirements

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).

**Single tenant limitation:**
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.

**Secure proxy requirement:**
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.

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.

### Configuration Manager Cache

You can control how OpenID Connect configuration managers are cached per domain with `ConfigurationManagerCache`.

By default, the SDK uses an in-memory cache with:
- `maxSize: 100` entries
- No expiration (entries remain until evicted by size pressure)

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.

Most applications can keep the defaults, but you may want to adjust them in the following cases:
- Increase `maxSize` if one process may verify tokens for more than 100 distinct domains during its lifetime.
- Decrease `maxSize` if memory usage matters more than avoiding repeated OIDC discovery setup.
- Set `slidingExpiration` if you want entries that haven't been accessed within a given duration to be evicted automatically.
- Use `NullConfigurationManagerCache` to disable caching entirely (not recommended for production).

Rule of thumb: set `maxSize` to cover the number of distinct domains a single process is expected to serve, with some headroom.

#### MemoryConfigurationManagerCache (Default)

```csharp
.WithCustomDomains(options =>
{
options.DomainResolver = httpContext => { /* ... */ };

options.ConfigurationManagerCache = new MemoryConfigurationManagerCache(
maxSize: 100, // Maximum number of domains to cache
slidingExpiration: TimeSpan.FromHours(1) // Optional: evict entries not accessed within 1 hour
);
});
```

#### NullConfigurationManagerCache

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

```csharp
.WithCustomDomains(options =>
{
options.DomainResolver = httpContext => { /* ... */ };
options.ConfigurationManagerCache = new NullConfigurationManagerCache();
});
```

#### Custom Cache Implementation

Implement `IConfigurationManagerCache` for custom caching strategies (e.g., a distributed cache):

```csharp
public class MyCustomConfigurationManagerCache : IConfigurationManagerCache
{
public IConfigurationManager<OpenIdConnectConfiguration> GetOrCreate(
string metadataAddress,
Func<string, IConfigurationManager<OpenIdConnectConfiguration>> factory)
{
// Return a cached instance or call factory(metadataAddress) to create one
}

public void Clear() { /* Evict all entries */ }
public void Dispose() { /* Clean up resources */ }
}

// Usage
.WithCustomDomains(options =>
{
options.DomainResolver = httpContext => { /* ... */ };
options.ConfigurationManagerCache = new MyCustomConfigurationManagerCache();
});
```

## Backchannel Logout

Backchannel logout can be configured by calling `WithBackchannelLogout()` when calling `AddAuth0WebAppAuthentication`.
Expand Down
35 changes: 31 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ A library based on `Microsoft.AspNetCore.Authentication.OpenIdConnect` to make i
![Downloads](https://img.shields.io/nuget/dt/auth0.aspnetcore.authentication)
[![License](https://img.shields.io/:license-MIT-blue.svg?style=flat)](https://opensource.org/licenses/MIT)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/auth0/auth0-aspnetcore-authentication)
![AzureDevOps](https://img.shields.io/azure-devops/build/Auth0SDK/Auth0.AspNetCore.Authentication/8)
[![Build and Test](https://github.com/auth0/auth0-aspnetcore-authentication/actions/workflows/build.yml/badge.svg)](https://github.com/auth0/auth0-aspnetcore-authentication/actions/workflows/build.yml)

:books: [Documentation](#documentation) - :rocket: [Getting Started](#getting-started) - :computer: [API Reference](#api-reference) - :speech_balloon: [Feedback](#feedback)

Expand All @@ -15,12 +15,12 @@ A library based on `Microsoft.AspNetCore.Authentication.OpenIdConnect` to make i
- [Quickstart](https://auth0.com/docs/quickstart/webapp/aspnet-core) - our interactive guide for quickly adding login, logout and user information to an ASP.NET MVC application using Auth0.
- [Sample App](https://github.com/auth0-samples/auth0-aspnetcore-mvc-samples/tree/master/Quickstart/Sample) - a full-fledged ASP.NET MVC application integrated with Auth0.
- [Examples](https://github.com/auth0/auth0-aspnetcore-authentication/blob/main/EXAMPLES.md) - code samples for common ASP.NET MVC authentication scenario's.
- [Docs site](https://www.auth0.com/docs) - explore our docs site and learn more about
- [Docs site](https://www.auth0.com/docs) - explore our docs site and learn more about Auth0.

## Getting started
### Requirements

This library supports .NET 6.0 and above.
This library supports .NET 6.0, 7.0, 8.0, and 10.0.

### Installation

Expand Down Expand Up @@ -114,6 +114,33 @@ For more code samples on how to integrate the **auth0-aspnetcore-authentication*

> 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).

## Multiple Custom Domain (MCD) Support

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.

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.

### Configuration

```csharp
services.AddAuth0WebAppAuthentication(options =>
{
options.Domain = Configuration["Auth0:Domain"];
options.ClientId = Configuration["Auth0:ClientId"];
})
.WithCustomDomains(options =>
{
// Example: resolve from a custom header
options.DomainResolver = httpContext =>
{
var tenant = httpContext.Request.Headers["X-Tenant-Domain"].FirstOrDefault();
return Task.FromResult(tenant ?? "default-tenant.auth0.com");
};
});
```

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).

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

Expand Down Expand Up @@ -152,4 +179,4 @@ Please do not report security vulnerabilities on the public GitHub issue tracker
</p>
<p align="center">Auth0 is an easy to implement, adaptable authentication and authorization platform. To learn more checkout <a href="https://auth0.com/why-auth0">Why Auth0?</a></p>
<p align="center">
This project is licensed under the MIT license. See the <a href="https://github.com/auth0/auth0-aspnetcore-authentication/blob/main/LICENSE"> LICENSE</a> file for more info.</p>
This project is licensed under the MIT license. See the <a href="https://github.com/auth0/auth0-aspnetcore-authentication/blob/main/LICENSE">LICENSE</a> file for more info.</p>
5 changes: 5 additions & 0 deletions src/Auth0.AspNetCore.Authentication/Auth0Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,10 @@ public class Auth0Constants
/// The callback path to which Auth0 should redirect back, used when configuring OpenIdConnect
/// </summary>
internal static string DefaultCallbackPath = "/callback";

/// <summary>
/// Key used to store the resolved domain in the authentication properties.
/// </summary>
internal static readonly string ResolvedDomainKey = "auth0:resolved-domain";
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using System;
using System.Threading.Tasks;
using Auth0.AspNetCore.Authentication.BackchannelLogout;
using Auth0.AspNetCore.Authentication.CustomDomains;
using Microsoft.AspNetCore.Hosting;

namespace Auth0.AspNetCore.Authentication
{
Expand Down Expand Up @@ -64,6 +67,49 @@ public Auth0WebAppAuthenticationBuilder WithBackchannelLogout()
return this;
}

/// <summary>
/// Configures support for multiple Auth0 custom domains with dynamic domain resolution.
/// </summary>
/// <param name="configureOptions">A delegate used to configure the <see cref="Auth0CustomDomainsOptions"/></param>
/// <returns>An instance of <see cref="Auth0WebAppAuthenticationBuilder"/></returns>
public Auth0WebAppAuthenticationBuilder WithCustomDomains(Action<Auth0CustomDomainsOptions> configureOptions)
{
EnableCustomDomains(configureOptions);
return this;
}

private void EnableCustomDomains(Action<Auth0CustomDomainsOptions> configureOptions)
{
var customDomainsOptions = new Auth0CustomDomainsOptions();
configureOptions(customDomainsOptions);

// Validate that DomainResolver is configured
if (customDomainsOptions.DomainResolver == null)
{
throw new InvalidOperationException(
$"DomainResolver must be configured when using {nameof(WithCustomDomains)}. " +
$"Set the {nameof(Auth0CustomDomainsOptions.DomainResolver)} property to provide a function that resolves the Auth0 domain for each request.");
}

// Register the options for this authentication scheme
_services.Configure(_authenticationScheme, configureOptions);

// Register HttpContextAccessor - required for domain resolution
_services.AddHttpContextAccessor();

// Register HttpClient - required for fetching OIDC configuration per domain
_services.AddHttpClient();

// Register the startup filter to resolve domain early in the request pipeline
_services.TryAddEnumerable(
ServiceDescriptor.Singleton<IStartupFilter, Auth0CustomDomainStartupFilter>(
_ => new Auth0CustomDomainStartupFilter(_authenticationScheme)));

// Register the post-configure options to set up custom ConfigurationManager
_services.TryAddEnumerable(
ServiceDescriptor.Singleton<IPostConfigureOptions<OpenIdConnectOptions>, Auth0CustomDomainsOpenIdConnectPostConfigureOptions>());
}

private void EnableWithAccessToken(Action<Auth0WebAppWithAccessTokenOptions> configureOptions)
{
var auth0WithAccessTokensOptions = new Auth0WebAppWithAccessTokenOptions();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,10 +156,19 @@
{
await VerifyBackchannelLogoutSupport(context.HttpContext, oidcOptions);

var issuer = $"https://{options.Domain}/";
// Prefer issuer from the authenticated principal
var resolvedIssuer = context.HttpContext.User?.FindFirst("iss")?.Value;

// Fall back to the domain resolved by StartupFilter (cached in HttpContext.Items)
if (string.IsNullOrWhiteSpace(resolvedIssuer))
{
resolvedIssuer = context.HttpContext.GetResolvedDomain();
}

var issuer = Utils.ToAuthority(resolvedIssuer ?? $"https://{options.Domain}/");
var sid = context.Principal?.FindFirst("sid")?.Value;

var isLoggedOut = await logoutTokenHandler.IsLoggedOutAsync(issuer, sid);

Check warning on line 171 in src/Auth0.AspNetCore.Authentication/AuthenticationBuilderExtensions.cs

View workflow job for this annotation

GitHub Actions / build (net7.0)

Possible null reference argument for parameter 'sid' in 'Task<bool> ILogoutTokenHandler.IsLoggedOutAsync(string issuer, string sid)'.

Check warning on line 171 in src/Auth0.AspNetCore.Authentication/AuthenticationBuilderExtensions.cs

View workflow job for this annotation

GitHub Actions / build (net7.0)

Possible null reference argument for parameter 'sid' in 'Task<bool> ILogoutTokenHandler.IsLoggedOutAsync(string issuer, string sid)'.

Check warning on line 171 in src/Auth0.AspNetCore.Authentication/AuthenticationBuilderExtensions.cs

View workflow job for this annotation

GitHub Actions / build (net8.0)

Possible null reference argument for parameter 'sid' in 'Task<bool> ILogoutTokenHandler.IsLoggedOutAsync(string issuer, string sid)'.

Check warning on line 171 in src/Auth0.AspNetCore.Authentication/AuthenticationBuilderExtensions.cs

View workflow job for this annotation

GitHub Actions / build (net8.0)

Possible null reference argument for parameter 'sid' in 'Task<bool> ILogoutTokenHandler.IsLoggedOutAsync(string issuer, string sid)'.

Check warning on line 171 in src/Auth0.AspNetCore.Authentication/AuthenticationBuilderExtensions.cs

View workflow job for this annotation

GitHub Actions / build (net10.0)

Possible null reference argument for parameter 'sid' in 'Task<bool> ILogoutTokenHandler.IsLoggedOutAsync(string issuer, string sid)'.

Check warning on line 171 in src/Auth0.AspNetCore.Authentication/AuthenticationBuilderExtensions.cs

View workflow job for this annotation

GitHub Actions / build (net10.0)

Possible null reference argument for parameter 'sid' in 'Task<bool> ILogoutTokenHandler.IsLoggedOutAsync(string issuer, string sid)'.

Check warning on line 171 in src/Auth0.AspNetCore.Authentication/AuthenticationBuilderExtensions.cs

View workflow job for this annotation

GitHub Actions / build (net6.0)

Possible null reference argument for parameter 'sid' in 'Task<bool> ILogoutTokenHandler.IsLoggedOutAsync(string issuer, string sid)'.

Check warning on line 171 in src/Auth0.AspNetCore.Authentication/AuthenticationBuilderExtensions.cs

View workflow job for this annotation

GitHub Actions / build (net6.0)

Possible null reference argument for parameter 'sid' in 'Task<bool> ILogoutTokenHandler.IsLoggedOutAsync(string issuer, string sid)'.

if (isLoggedOut)
{
Expand Down Expand Up @@ -196,7 +205,7 @@

if (isExpired && !string.IsNullOrWhiteSpace(refreshToken))
{
var result = await RefreshTokens(options, refreshToken, oidcOptions.Backchannel);
var result = await RefreshTokens(context.HttpContext, options, refreshToken, oidcOptions.Backchannel);

if (result != null)
{
Expand Down Expand Up @@ -239,17 +248,21 @@
}
}

private static async Task<AccessTokenResponse?> RefreshTokens(Auth0WebAppOptions options, string refreshToken, HttpClient httpClient)
private static async Task<AccessTokenResponse?> RefreshTokens(HttpContext httpContext, Auth0WebAppOptions options, string refreshToken, HttpClient httpClient)
{
var tokenClient = new TokenClient(httpClient);
return await tokenClient.Refresh(options, refreshToken);

// Get the resolved domain from HttpContext if available (for multiple custom domains)
var resolvedDomain = httpContext.GetResolvedDomain();

return await tokenClient.Refresh(options, refreshToken, resolvedDomain);
}

private static async Task VerifyBackchannelLogoutSupport(HttpContext context, OpenIdConnectOptions oidcOptions)
{
if (oidcOptions.Configuration == null)
{
oidcOptions.Configuration = await oidcOptions.ConfigurationManager.GetConfigurationAsync(context.RequestAborted);

Check warning on line 265 in src/Auth0.AspNetCore.Authentication/AuthenticationBuilderExtensions.cs

View workflow job for this annotation

GitHub Actions / build (net7.0)

Dereference of a possibly null reference.

Check warning on line 265 in src/Auth0.AspNetCore.Authentication/AuthenticationBuilderExtensions.cs

View workflow job for this annotation

GitHub Actions / build (net7.0)

Dereference of a possibly null reference.

Check warning on line 265 in src/Auth0.AspNetCore.Authentication/AuthenticationBuilderExtensions.cs

View workflow job for this annotation

GitHub Actions / build (net8.0)

Dereference of a possibly null reference.

Check warning on line 265 in src/Auth0.AspNetCore.Authentication/AuthenticationBuilderExtensions.cs

View workflow job for this annotation

GitHub Actions / build (net8.0)

Dereference of a possibly null reference.

Check warning on line 265 in src/Auth0.AspNetCore.Authentication/AuthenticationBuilderExtensions.cs

View workflow job for this annotation

GitHub Actions / build (net10.0)

Dereference of a possibly null reference.

Check warning on line 265 in src/Auth0.AspNetCore.Authentication/AuthenticationBuilderExtensions.cs

View workflow job for this annotation

GitHub Actions / build (net10.0)

Dereference of a possibly null reference.

Check warning on line 265 in src/Auth0.AspNetCore.Authentication/AuthenticationBuilderExtensions.cs

View workflow job for this annotation

GitHub Actions / build (net6.0)

Dereference of a possibly null reference.

Check warning on line 265 in src/Auth0.AspNetCore.Authentication/AuthenticationBuilderExtensions.cs

View workflow job for this annotation

GitHub Actions / build (net6.0)

Dereference of a possibly null reference.
}

var additionalConfiguration = oidcOptions.Configuration.AdditionalData;
Expand Down
Loading
Loading