diff --git a/Examples.md b/Examples.md
index 43c905600..70423cec9 100644
--- a/Examples.md
+++ b/Examples.md
@@ -290,6 +290,9 @@ public async Task LoginWithClientCredentialsAndMonitorClientQuota()
- [4.3 Get a specific Network ACL configuration](#43-get-a-specific-network-acl-configuration)
- [4.4 Update Network ACL with a PATCH request](#44-update-network-acl-with-a-patch-request)
- [4.5 Update Network ACL with a PUT request](#45-update-network-acl-with-a-put-request)
+- [5. Using a Custom Domain with the Management API](#5-using-a-custom-domain-with-the-management-api)
+ - [5.1 Let the client manage the connection (simplest)](#51-let-the-client-manage-the-connection-simplest)
+ - [5.2 Bring your own connection](#52-bring-your-own-connection)
## 1. Management Client Initialization
@@ -570,3 +573,76 @@ public async void UpdateNetworkAcl() {
```
⬆️ [Go to Top](#)
+
+## 5. Using a Custom Domain with the Management API
+
+When your tenant uses a [custom domain](https://auth0.com/docs/customize/custom-domains), certain Management API endpoints accept an `Auth0-Custom-Domain` header so that generated URLs (e.g. in email verification tickets or password-change tickets) use your custom domain instead of the default Auth0 domain. The SDK can add this header automatically on the [supported endpoints](#supported-endpoints).
+
+### 5.1 Let the client manage the connection (simplest)
+
+Pass `customDomain` to the `ManagementApiClient` constructor and omit the `connection` argument. The client creates its own `HttpClientManagementConnection` internally with the custom domain already wired in.
+
+```csharp
+public async Task InitializeWithCustomDomain()
+{
+ var authClient = new AuthenticationApiClient("tenant.auth0.com");
+
+ var accessTokenResponse = await authClient.GetTokenAsync(new ClientCredentialsTokenRequest()
+ {
+ Audience = "https://tenant.auth0.com/api/v2/",
+ ClientId = "clientId",
+ ClientSecret = "clientSecret",
+ });
+
+ // No explicit connection — the client creates one internally and wires in the custom domain.
+ var managementClient = new ManagementApiClient(
+ accessTokenResponse.AccessToken,
+ "tenant.auth0.com",
+ connection: null,
+ customDomain: "login.example.com");
+}
+```
+
+### 5.2 Bring your own connection
+
+If you already supply an `HttpClientManagementConnection` (e.g. to configure retries or inject an `HttpClient`), pass the custom domain directly to the connection constructor instead.
+
+```csharp
+public async Task InitializeWithCustomDomainAndOwnConnection()
+{
+ var authClient = new AuthenticationApiClient("tenant.auth0.com");
+
+ var accessTokenResponse = await authClient.GetTokenAsync(new ClientCredentialsTokenRequest()
+ {
+ Audience = "https://tenant.auth0.com/api/v2/",
+ ClientId = "clientId",
+ ClientSecret = "clientSecret",
+ });
+
+ var connection = new HttpClientManagementConnection(
+ httpClient: null,
+ options: new HttpClientManagementConnectionOptions { NumberOfHttpRetries = 5 },
+ customDomain: "login.example.com");
+
+ var managementClient = new ManagementApiClient(
+ accessTokenResponse.AccessToken,
+ "tenant.auth0.com",
+ connection);
+}
+```
+
+### Supported endpoints
+
+The `Auth0-Custom-Domain` header is automatically included only on the following endpoints. It is silently omitted from all others.
+
+| Endpoint |
+|---|
+| `POST /api/v2/jobs/verification-email` |
+| `POST /api/v2/tickets/email-verification` |
+| `POST /api/v2/tickets/password-change` |
+| `GET/POST /api/v2/organizations/{id}/invitations` |
+| `GET/POST /api/v2/users` and `GET /api/v2/users/{id}` |
+| `POST /api/v2/guardian/enrollments/ticket` |
+| `POST /api/v2/self-service-profiles/{id}/sso-ticket` |
+
+⬆️ [Go to Top](#)
diff --git a/src/Auth0.ManagementApi/AssemblyInfo.cs b/src/Auth0.ManagementApi/AssemblyInfo.cs
index 6f15ab4f6..752e9e340 100644
--- a/src/Auth0.ManagementApi/AssemblyInfo.cs
+++ b/src/Auth0.ManagementApi/AssemblyInfo.cs
@@ -1,3 +1,4 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Auth0.ManagementApi.IntegrationTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100453e57bfa7549abf0f1775df9384d2f279d25c2ab4c78d5a69d7e6da9567d2b984da533229a0d530a3b75c7f5a12c341799b448102995b8a123d1288aa12ca3c1c354c3da97e64626d1223ca7c6e95cba845bce6edcee8b326c2cd015cc84995e5b630ef5c7fa69928dea64a53ee71a493267de7e18d0e9f31e1e00bb8e01cae")]
+[assembly: InternalsVisibleTo("Auth0.Core.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100453e57bfa7549abf0f1775df9384d2f279d25c2ab4c78d5a69d7e6da9567d2b984da533229a0d530a3b75c7f5a12c341799b448102995b8a123d1288aa12ca3c1c354c3da97e64626d1223ca7c6e95cba845bce6edcee8b326c2cd015cc84995e5b630ef5c7fa69928dea64a53ee71a493267de7e18d0e9f31e1e00bb8e01cae")]
diff --git a/src/Auth0.ManagementApi/CustomDomainHeader.cs b/src/Auth0.ManagementApi/CustomDomainHeader.cs
new file mode 100644
index 000000000..8c26189c2
--- /dev/null
+++ b/src/Auth0.ManagementApi/CustomDomainHeader.cs
@@ -0,0 +1,16 @@
+namespace Auth0.ManagementApi;
+
+///
+/// Provides constants related to the Auth0 custom domain HTTP header.
+///
+///
+/// Use this when constructing a with a custom domain.
+/// The header is automatically included on allowed endpoints only.
+///
+public static class CustomDomainHeader
+{
+ ///
+ /// The HTTP header name used to specify an Auth0 custom domain.
+ ///
+ public const string HeaderName = "Auth0-Custom-Domain";
+}
diff --git a/src/Auth0.ManagementApi/HttpClientManagementConnection.cs b/src/Auth0.ManagementApi/HttpClientManagementConnection.cs
index c28edbfba..b0714c749 100644
--- a/src/Auth0.ManagementApi/HttpClientManagementConnection.cs
+++ b/src/Auth0.ManagementApi/HttpClientManagementConnection.cs
@@ -6,6 +6,7 @@
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
+using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Auth0.Core;
@@ -19,8 +20,20 @@ public class HttpClientManagementConnection : IManagementConnection, IDisposable
{
private static readonly JsonSerializerSettings jsonSerializerSettings = new() { NullValueHandling = NullValueHandling.Ignore, DateParseHandling = DateParseHandling.DateTime };
+ private static readonly Regex[] CustomDomainAllowlist = new[]
+ {
+ new Regex(@"^jobs/verification-email/?$", RegexOptions.Compiled | RegexOptions.IgnoreCase),
+ new Regex(@"^tickets/email-verification/?$", RegexOptions.Compiled | RegexOptions.IgnoreCase),
+ new Regex(@"^tickets/password-change/?$", RegexOptions.Compiled | RegexOptions.IgnoreCase),
+ new Regex(@"^organizations/[^/]+/invitations/?$", RegexOptions.Compiled | RegexOptions.IgnoreCase),
+ new Regex(@"^users(/[^/]+/?)?$", RegexOptions.Compiled | RegexOptions.IgnoreCase),
+ new Regex(@"^guardian/enrollments/ticket/?$", RegexOptions.Compiled | RegexOptions.IgnoreCase),
+ new Regex(@"^self-service-profiles/[^/]+/sso-ticket/?$", RegexOptions.Compiled | RegexOptions.IgnoreCase),
+ };
+
private readonly HttpClient httpClient;
private readonly HttpClientManagementConnectionOptions options;
+ internal readonly string? customDomain;
private bool ownHttpClient;
private readonly ConcurrentRandom random = new();
@@ -38,16 +51,42 @@ public class HttpClientManagementConnection : IManagementConnection, IDisposable
/// be created and be used for all requests made by this instance.
/// Optional to use.
public HttpClientManagementConnection(HttpClient httpClient = null, HttpClientManagementConnectionOptions options = null)
+ : this(httpClient, options, null)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Optional to use. If not specified one will
+ /// be created and be used for all requests made by this instance.
+ /// Optional to use.
+ /// Optional Auth0 custom domain. When set, the Auth0-Custom-Domain
+ /// header is automatically added to requests targeting allowed endpoints.
+ /// Thrown when contains a URI scheme or whitespace.
+ public HttpClientManagementConnection(HttpClient? httpClient, HttpClientManagementConnectionOptions? options, string? customDomain)
{
+ if (customDomain != null)
+ {
+ customDomain = customDomain.Trim();
+ if (string.IsNullOrWhiteSpace(customDomain))
+ throw new ArgumentException("Custom domain must not be empty or whitespace.", nameof(customDomain));
+ if (customDomain.Contains("://"))
+ throw new ArgumentException("Custom domain must be a domain name without a URI scheme (e.g., 'login.example.com').", nameof(customDomain));
+ if (customDomain.Contains('/'))
+ throw new ArgumentException("Custom domain must be a domain name without a path (e.g., 'login.example.com').", nameof(customDomain));
+ }
+
ownHttpClient = httpClient == null;
this.httpClient = httpClient ?? new HttpClient();
this.options = options ?? new HttpClientManagementConnectionOptions();
+ this.customDomain = customDomain;
}
///
/// Initializes a new instance of the class.
///
- /// to use with the managed
+ /// to use with the managed
/// that will be created and used for all requests made
/// by this instance.
/// Optional to use.
@@ -74,6 +113,7 @@ private async Task GetAsyncInternal(Uri uri, IDictionary h
using (var request = new HttpRequestMessage(HttpMethod.Get, uri))
{
ApplyHeaders(request.Headers, headers);
+ ApplyCustomDomainHeader(request.Headers, uri);
return await SendRequest(request, converters, cancellationToken).ConfigureAwait(false);
}
}
@@ -83,6 +123,7 @@ private async Task SendAsyncInternal(HttpMethod method, Uri uri, object bo
using (var request = new HttpRequestMessage(method, uri) { Content = BuildMessageContent(body, files) })
{
ApplyHeaders(request.Headers, headers);
+ ApplyCustomDomainHeader(request.Headers, uri);
return await SendRequest(request, converters, cancellationToken).ConfigureAwait(false);
}
}
@@ -138,6 +179,27 @@ internal void ApplyHeaders(HttpHeaders current, IDictionary inpu
current.Add(pair.Key, pair.Value);
}
+ private void ApplyCustomDomainHeader(HttpHeaders current, Uri uri)
+ {
+ if (!string.IsNullOrWhiteSpace(customDomain) && IsCustomDomainAllowed(uri))
+ current.Add(CustomDomainHeader.HeaderName, customDomain);
+ }
+
+ private static bool IsCustomDomainAllowed(Uri uri)
+ {
+ var path = uri.AbsolutePath;
+ const string apiPrefix = "/api/v2/";
+ var index = path.IndexOf(apiPrefix, StringComparison.OrdinalIgnoreCase);
+ if (index < 0) return false;
+ var relativePath = path.Substring(index + apiPrefix.Length);
+
+ foreach (var pattern in CustomDomainAllowlist)
+ if (pattern.IsMatch(relativePath))
+ return true;
+
+ return false;
+ }
+
private HttpContent BuildMessageContent(object body, IList files = null)
{
if (body == null)
diff --git a/src/Auth0.ManagementApi/ManagementApiClient.cs b/src/Auth0.ManagementApi/ManagementApiClient.cs
index 2b0887b41..6a3004b5b 100644
--- a/src/Auth0.ManagementApi/ManagementApiClient.cs
+++ b/src/Auth0.ManagementApi/ManagementApiClient.cs
@@ -192,10 +192,27 @@ public class ManagementApiClient : IManagementApiClient
/// of the tenant to manage.
/// to facilitate communication with server.
public ManagementApiClient(string token, Uri baseUri, IManagementConnection managementConnection = null)
+ : this(token, baseUri, managementConnection, null)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// A valid Auth0 Management API v2 token.
+ /// of the tenant to manage.
+ /// to facilitate communication with server.
+ ///
+ /// Optional Auth0 custom domain to include via the Auth0-Custom-Domain header on
+ /// allowed endpoints. When set, the header is automatically sent on requests to
+ /// endpoints that support it (e.g., /api/v2/users, /api/v2/tickets/email-verification).
+ /// It is silently omitted from all other endpoints.
+ ///
+ public ManagementApiClient(string token, Uri baseUri, IManagementConnection managementConnection, string? customDomain)
{
if (managementConnection == null)
{
- var ownedManagementConnection = new HttpClientManagementConnection();
+ var ownedManagementConnection = new HttpClientManagementConnection(null, null, customDomain);
managementConnection = ownedManagementConnection;
connectionToDispose = ownedManagementConnection;
}
@@ -246,9 +263,26 @@ public ManagementApiClient(string token, Uri baseUri, IManagementConnection mana
///
/// A valid Auth0 Management API v2 token.
/// Your Auth0 domain. tenant.auth0.com
- ///
+ /// to facilitate communication with server.
public ManagementApiClient(string token, string domain, IManagementConnection connection = null)
- : this(token, new Uri($"https://{domain}/api/v2"), connection)
+ : this(token, new Uri($"https://{domain}/api/v2"), connection, null)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// A valid Auth0 Management API v2 token.
+ /// Your Auth0 domain. tenant.auth0.com
+ /// to facilitate communication with server.
+ ///
+ /// Optional Auth0 custom domain to include via the Auth0-Custom-Domain header on
+ /// allowed endpoints. When set, the header is automatically sent on requests to
+ /// endpoints that support it (e.g., /api/v2/users, /api/v2/tickets/email-verification).
+ /// It is silently omitted from all other endpoints.
+ ///
+ public ManagementApiClient(string token, string domain, IManagementConnection connection, string? customDomain)
+ : this(token, new Uri($"https://{domain}/api/v2"), connection, customDomain)
{
}
diff --git a/tests/Auth0.Core.UnitTests/HttpClientManagementConnectionTests.cs b/tests/Auth0.Core.UnitTests/HttpClientManagementConnectionTests.cs
index 9a3736e75..4735480d6 100644
--- a/tests/Auth0.Core.UnitTests/HttpClientManagementConnectionTests.cs
+++ b/tests/Auth0.Core.UnitTests/HttpClientManagementConnectionTests.cs
@@ -4,8 +4,11 @@
using Moq;
using Moq.Protected;
using System;
+using System.Collections.Generic;
+using System.Linq;
using System.Net;
using System.Net.Http;
+using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@@ -138,6 +141,313 @@ public void Should_Not_Retry_When_Limit_Set_To_Zero()
amountOfTimesCalled.Should().Be(1);
}
+ [Theory]
+ [InlineData("https://tenant.auth0.com/api/v2/jobs/verification-email", "GET")]
+ [InlineData("https://tenant.auth0.com/api/v2/jobs/verification-email", "POST")]
+ [InlineData("https://tenant.auth0.com/api/v2/tickets/email-verification", "GET")]
+ [InlineData("https://tenant.auth0.com/api/v2/tickets/email-verification", "POST")]
+ [InlineData("https://tenant.auth0.com/api/v2/tickets/password-change", "GET")]
+ [InlineData("https://tenant.auth0.com/api/v2/tickets/password-change", "POST")]
+ [InlineData("https://tenant.auth0.com/api/v2/organizations/org_123/invitations", "GET")]
+ [InlineData("https://tenant.auth0.com/api/v2/organizations/org_123/invitations", "POST")]
+ [InlineData("https://tenant.auth0.com/api/v2/users", "GET")]
+ [InlineData("https://tenant.auth0.com/api/v2/users", "POST")]
+ [InlineData("https://tenant.auth0.com/api/v2/users/auth0|123", "GET")]
+ [InlineData("https://tenant.auth0.com/api/v2/users/auth0|123", "POST")]
+ [InlineData("https://tenant.auth0.com/api/v2/guardian/enrollments/ticket", "GET")]
+ [InlineData("https://tenant.auth0.com/api/v2/guardian/enrollments/ticket", "POST")]
+ [InlineData("https://tenant.auth0.com/api/v2/self-service-profiles/ssp_123/sso-ticket", "GET")]
+ [InlineData("https://tenant.auth0.com/api/v2/self-service-profiles/ssp_123/sso-ticket", "POST")]
+ public async Task Custom_domain_header_sent_on_allowed_endpoint(string url, string httpMethod)
+ {
+ HttpRequestMessage capturedRequest = null;
+ var mockHandler = new Mock(MockBehavior.Strict);
+ mockHandler.Protected()
+ .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny())
+ .Callback((req, _) => capturedRequest = req)
+ .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("", Encoding.UTF8, "application/json") });
+
+ var connection = new HttpClientManagementConnection(new HttpClient(mockHandler.Object), null, "custom.example.com");
+ var method = new HttpMethod(httpMethod);
+ if (method == HttpMethod.Get)
+ await connection.GetAsync