Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
76 changes: 76 additions & 0 deletions Examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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](#)
1 change: 1 addition & 0 deletions src/Auth0.ManagementApi/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("Auth0.ManagementApi.IntegrationTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100453e57bfa7549abf0f1775df9384d2f279d25c2ab4c78d5a69d7e6da9567d2b984da533229a0d530a3b75c7f5a12c341799b448102995b8a123d1288aa12ca3c1c354c3da97e64626d1223ca7c6e95cba845bce6edcee8b326c2cd015cc84995e5b630ef5c7fa69928dea64a53ee71a493267de7e18d0e9f31e1e00bb8e01cae")]
[assembly: InternalsVisibleTo("Auth0.Core.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100453e57bfa7549abf0f1775df9384d2f279d25c2ab4c78d5a69d7e6da9567d2b984da533229a0d530a3b75c7f5a12c341799b448102995b8a123d1288aa12ca3c1c354c3da97e64626d1223ca7c6e95cba845bce6edcee8b326c2cd015cc84995e5b630ef5c7fa69928dea64a53ee71a493267de7e18d0e9f31e1e00bb8e01cae")]
16 changes: 16 additions & 0 deletions src/Auth0.ManagementApi/CustomDomainHeader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace Auth0.ManagementApi;

/// <summary>
/// Provides constants related to the Auth0 custom domain HTTP header.
/// </summary>
/// <remarks>
/// Use this when constructing a <see cref="ManagementApiClient"/> with a custom domain.
/// The header is automatically included on allowed endpoints only.
/// </remarks>
public static class CustomDomainHeader
{
Comment thread
kishore7snehil marked this conversation as resolved.
/// <summary>
/// The HTTP header name used to specify an Auth0 custom domain.
/// </summary>
public const string HeaderName = "Auth0-Custom-Domain";
}
64 changes: 63 additions & 1 deletion src/Auth0.ManagementApi/HttpClientManagementConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
Expand All @@ -38,16 +51,42 @@ public class HttpClientManagementConnection : IManagementConnection, IDisposable
/// be created and be used for all requests made by this instance.</param>
/// <param name="options">Optional <see cref="HttpClientManagementConnectionOptions"/> to use.</param>
public HttpClientManagementConnection(HttpClient httpClient = null, HttpClientManagementConnectionOptions options = null)
: this(httpClient, options, null)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="HttpClientManagementConnection"/> class.
/// </summary>
/// <param name="httpClient">Optional <see cref="HttpClient"/> to use. If not specified one will
/// be created and be used for all requests made by this instance.</param>
/// <param name="options">Optional <see cref="HttpClientManagementConnectionOptions"/> to use.</param>
/// <param name="customDomain">Optional Auth0 custom domain. When set, the <c>Auth0-Custom-Domain</c>
/// header is automatically added to requests targeting allowed endpoints.</param>
/// <exception cref="ArgumentException">Thrown when <paramref name="customDomain"/> contains a URI scheme or whitespace.</exception>
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('/'))
Comment thread
kishore7snehil marked this conversation as resolved.
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;
}

/// <summary>
/// Initializes a new instance of the <see cref="HttpClientManagementConnection"/> class.
/// </summary>
/// <param name="handler"><see cref="HttpMessageHandler"/> to use with the managed
/// <param name="handler"><see cref="HttpMessageHandler"/> to use with the managed
/// <see cref="HttpClient"/> that will be created and used for all requests made
/// by this instance.</param>
/// <param name="options">Optional <see cref="HttpClientManagementConnectionOptions"/> to use.</param>
Expand All @@ -74,6 +113,7 @@ private async Task<T> GetAsyncInternal<T>(Uri uri, IDictionary<string, string> h
using (var request = new HttpRequestMessage(HttpMethod.Get, uri))
{
ApplyHeaders(request.Headers, headers);
ApplyCustomDomainHeader(request.Headers, uri);
return await SendRequest<T>(request, converters, cancellationToken).ConfigureAwait(false);
}
}
Expand All @@ -83,6 +123,7 @@ private async Task<T> SendAsyncInternal<T>(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<T>(request, converters, cancellationToken).ConfigureAwait(false);
}
}
Expand Down Expand Up @@ -138,6 +179,27 @@ internal void ApplyHeaders(HttpHeaders current, IDictionary<string, string> 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<FileUploadParameter> files = null)
{
if (body == null)
Expand Down
40 changes: 37 additions & 3 deletions src/Auth0.ManagementApi/ManagementApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -192,10 +192,27 @@ public class ManagementApiClient : IManagementApiClient
/// <param name="baseUri"><see cref="Uri"/> of the tenant to manage.</param>
/// <param name="managementConnection"><see cref="IManagementConnection"/> to facilitate communication with server.</param>
public ManagementApiClient(string token, Uri baseUri, IManagementConnection managementConnection = null)
: this(token, baseUri, managementConnection, null)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="ManagementApiClient"/> class.
/// </summary>
/// <param name="token">A valid Auth0 Management API v2 token.</param>
/// <param name="baseUri"><see cref="Uri"/> of the tenant to manage.</param>
/// <param name="managementConnection"><see cref="IManagementConnection"/> to facilitate communication with server.</param>
/// <param name="customDomain">
/// Optional Auth0 custom domain to include via the <c>Auth0-Custom-Domain</c> header on
/// allowed endpoints. When set, the header is automatically sent on requests to
/// endpoints that support it (e.g., <c>/api/v2/users</c>, <c>/api/v2/tickets/email-verification</c>).
/// It is silently omitted from all other endpoints.
/// </param>
public ManagementApiClient(string token, Uri baseUri, IManagementConnection managementConnection, string? customDomain)
{
if (managementConnection == null)
Comment thread
kishore7snehil marked this conversation as resolved.
{
var ownedManagementConnection = new HttpClientManagementConnection();
var ownedManagementConnection = new HttpClientManagementConnection(null, null, customDomain);
managementConnection = ownedManagementConnection;
connectionToDispose = ownedManagementConnection;
}
Expand Down Expand Up @@ -246,9 +263,26 @@ public ManagementApiClient(string token, Uri baseUri, IManagementConnection mana
/// </summary>
/// <param name="token">A valid Auth0 Management API v2 token.</param>
/// <param name="domain">Your Auth0 domain. <example>tenant.auth0.com</example></param>
/// <param name="connection"></param>
/// <param name="connection"><see cref="IManagementConnection"/> to facilitate communication with server.</param>
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)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="ManagementApiClient"/> class.
/// </summary>
/// <param name="token">A valid Auth0 Management API v2 token.</param>
/// <param name="domain">Your Auth0 domain. <example>tenant.auth0.com</example></param>
/// <param name="connection"><see cref="IManagementConnection"/> to facilitate communication with server.</param>
/// <param name="customDomain">
/// Optional Auth0 custom domain to include via the <c>Auth0-Custom-Domain</c> header on
/// allowed endpoints. When set, the header is automatically sent on requests to
/// endpoints that support it (e.g., <c>/api/v2/users</c>, <c>/api/v2/tickets/email-verification</c>).
/// It is silently omitted from all other endpoints.
/// </param>
public ManagementApiClient(string token, string domain, IManagementConnection connection, string? customDomain)
: this(token, new Uri($"https://{domain}/api/v2"), connection, customDomain)
{
}

Expand Down
Loading
Loading