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
397 changes: 195 additions & 202 deletions Examples.md

Large diffs are not rendered by default.

53 changes: 34 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,35 +33,41 @@ The recommended way to use the Management API is with the `ManagementClient` wra

##### Using ManagementClient (Recommended)

The `ManagementClient` wrapper handles token acquisition and refresh automatically using client credentials:
The `ManagementClient` wrapper abstracts token management through an `ITokenProvider`. Choose the built-in provider that fits your scenario, or implement the interface for full control.

**Client credentials** (recommended for server-to-server — tokens are acquired and refreshed automatically):

```csharp
var client = new ManagementClient(new ManagementClientOptions
{
Domain = "YOUR_AUTH0_DOMAIN",
ClientId = "YOUR_CLIENT_ID",
ClientSecret = "YOUR_CLIENT_SECRET"
TokenProvider = new ClientCredentialsTokenProvider(
domain: "YOUR_AUTH0_DOMAIN",
clientId: "YOUR_CLIENT_ID",
clientSecret: "YOUR_CLIENT_SECRET"
)
});

// Tokens are automatically acquired and refreshed
var users = await client.Users.GetAllAsync();
```

You can also use a static token or a dynamic token provider:
> **Note:** The domain is specified twice — once in `ManagementClientOptions` (to build the base API URL `https://{domain}/api/v2`) and once in `ClientCredentialsTokenProvider` (to build the token endpoint URL `https://{domain}/oauth/token`). Both must match your Auth0 tenant domain.

```csharp
// With a static token
var client = new ManagementClient(new ManagementClientOptions
{
Domain = "YOUR_AUTH0_DOMAIN",
Token = "your-access-token"
});
> **Already have a token?** Use `ManagementApiClient` directly:
> ```csharp
> var client = new ManagementApiClient(
> token: "your-access-token",
> clientOptions: new ClientOptions { BaseUrl = "https://YOUR_AUTH0_DOMAIN/api/v2" });
> ```

**Async delegate** (retrieve tokens from an external source such as a secret manager):

// With a dynamic token provider (e.g., from a vault or external service)
```csharp
var client = new ManagementClient(new ManagementClientOptions
{
Domain = "YOUR_AUTH0_DOMAIN",
TokenProvider = () => GetTokenFromVault()
TokenProvider = new DelegateTokenProvider(ct => secretManager.GetSecretAsync("auth0-token", ct))
});
```

Expand All @@ -71,9 +77,12 @@ Additional configuration options:
var client = new ManagementClient(new ManagementClientOptions
{
Domain = "YOUR_AUTH0_DOMAIN",
ClientId = "YOUR_CLIENT_ID",
ClientSecret = "YOUR_CLIENT_SECRET",
Audience = "https://custom-audience/", // Optional: defaults to https://{domain}/api/v2/
TokenProvider = new ClientCredentialsTokenProvider(
domain: "YOUR_AUTH0_DOMAIN",
clientId: "YOUR_CLIENT_ID",
clientSecret: "YOUR_CLIENT_SECRET",
audience: "https://custom-audience/" // Optional: defaults to https://{domain}/api/v2/
),
Timeout = TimeSpan.FromSeconds(60), // Optional: request timeout
MaxRetries = 3, // Optional: retry attempts
HttpClient = customHttpClient, // Optional: bring your own HttpClient
Expand All @@ -89,7 +98,10 @@ var client = new ManagementClient(new ManagementClientOptions
If you prefer to manage tokens yourself, you can use the `ManagementApiClient` directly. Generate a token for the API calls you wish to make (see [Access Tokens for the Management API](https://auth0.com/docs/api/management/v2/tokens)):

```csharp
var client = new ManagementApiClient("your token", new Uri("https://YOUR_AUTH0_DOMAIN/api/v2"));
var client = new ManagementApiClient(
token: "your-access-token",
clientOptions: new ClientOptions { BaseUrl = "https://YOUR_AUTH0_DOMAIN/api/v2" }
);
```

##### Making API Calls
Expand Down Expand Up @@ -238,8 +250,11 @@ services.AddSingleton<IManagementApiClient>(provider =>
return new ManagementClient(new ManagementClientOptions
{
Domain = "YOUR_AUTH0_DOMAIN",
ClientId = "YOUR_CLIENT_ID",
ClientSecret = "YOUR_CLIENT_SECRET"
TokenProvider = new ClientCredentialsTokenProvider(
domain: "YOUR_AUTH0_DOMAIN",
clientId: "YOUR_CLIENT_ID",
clientSecret: "YOUR_CLIENT_SECRET"
)
});
});
```
Expand Down
45 changes: 31 additions & 14 deletions V8_MIGRATION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,31 +91,39 @@ var client = new ManagementApiClient(token, new Uri("https://YOUR_DOMAIN/api/v2"
```csharp
using Auth0.ManagementApi;

// Automatic token acquisition and refresh
// Automatic token acquisition and refresh via client credentials
var client = new ManagementClient(new ManagementClientOptions
{
Domain = "YOUR_DOMAIN",
ClientId = "YOUR_CLIENT_ID",
ClientSecret = "YOUR_CLIENT_SECRET"
TokenProvider = new ClientCredentialsTokenProvider(
domain: "YOUR_DOMAIN",
clientId: "YOUR_CLIENT_ID",
clientSecret: "YOUR_CLIENT_SECRET"
)
});
```

**v8 (Alternative - with manual token):**
**v8 (Alternative - pre-obtained token, use `ManagementApiClient` directly):**
```csharp
using Auth0.ManagementApi;

// If you prefer to manage tokens yourself
// Use a pre-obtained access token directly — no wrapper needed
var client = new ManagementApiClient(
token: "YOUR_ACCESS_TOKEN",
clientOptions: new ClientOptions { BaseUrl = "https://YOUR_DOMAIN/api/v2" });

// Or retrieve tokens asynchronously (e.g., from a secret manager) via ManagementClient
var client = new ManagementClient(new ManagementClientOptions
{
Domain = "YOUR_DOMAIN",
Token = "YOUR_ACCESS_TOKEN"
TokenProvider = new DelegateTokenProvider(ct => secretManager.GetSecretAsync("auth0-token", ct))
});

// Or with a dynamic token provider
// Or implement ITokenProvider for any other strategy
var client = new ManagementClient(new ManagementClientOptions
{
Domain = "YOUR_DOMAIN",
TokenProvider = () => GetTokenFromVault()
TokenProvider = new MyCustomTokenProvider()
});
```

Expand All @@ -124,8 +132,11 @@ var client = new ManagementClient(new ManagementClientOptions
| Option | v7 | v8 |
|--------|----|----|
| Domain/URL | Constructor parameter (`Uri`) | `ManagementClientOptions.Domain` |
| Token | Constructor parameter | `ManagementClientOptions.Token` or `TokenProvider` |
| Client Credentials | Not supported (manual token) | `ManagementClientOptions.ClientId` + `ClientSecret` |
| Token | Constructor parameter (string) | `new ManagementApiClient(token: "…", clientOptions: …)` |
| Dynamic token | Not supported | `new DelegateTokenProvider(factory)` |
| Client Credentials | Not supported (manual token) | `new ClientCredentialsTokenProvider(...)` |
| Custom token strategy | Not supported | Implement `ITokenProvider` |
| Audience | N/A | `ClientCredentialsTokenProvider` constructor parameter |
| Timeout | Via `HttpClientManagementConnection` | `ManagementClientOptions.Timeout` |
| Max Retries | Via `HttpClientManagementConnection` | `ManagementClientOptions.MaxRetries` |
| Custom HttpClient | Via `HttpClientManagementConnection` | `ManagementClientOptions.HttpClient` |
Expand Down Expand Up @@ -378,8 +389,11 @@ var client = new ManagementApiClient(token, new Uri("https://YOUR_DOMAIN/api/v2"
var client = new ManagementClient(new ManagementClientOptions
{
Domain = "YOUR_DOMAIN",
ClientId = "YOUR_CLIENT_ID",
ClientSecret = "YOUR_CLIENT_SECRET"
TokenProvider = new ClientCredentialsTokenProvider(
domain: "YOUR_DOMAIN",
clientId: "YOUR_CLIENT_ID",
clientSecret: "YOUR_CLIENT_SECRET"
)
});
```

Expand Down Expand Up @@ -673,8 +687,11 @@ services.AddSingleton<IManagementApiClient>(provider =>
return new ManagementClient(new ManagementClientOptions
{
Domain = "YOUR_AUTH0_DOMAIN",
ClientId = "YOUR_CLIENT_ID",
ClientSecret = "YOUR_CLIENT_SECRET"
TokenProvider = new ClientCredentialsTokenProvider(
domain: "YOUR_AUTH0_DOMAIN",
clientId: "YOUR_CLIENT_ID",
clientSecret: "YOUR_CLIENT_SECRET"
)
});
});

Expand Down
4 changes: 4 additions & 0 deletions src/Auth0.ManagementApi/Auth0.ManagementApi.Custom.props
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Auth0.AuthenticationApi\Auth0.AuthenticationApi.csproj" />
</ItemGroup>

<PropertyGroup>
<TargetFrameworks>net462;netstandard2.0</TargetFrameworks>
</PropertyGroup>
Expand Down
125 changes: 125 additions & 0 deletions src/Auth0.ManagementApi/Wrapper/ClientCredentialsTokenProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
using Auth0.AuthenticationApi;
using Auth0.AuthenticationApi.Models;

namespace Auth0.ManagementApi;

/// <summary>
/// An <see cref="ITokenProvider"/> that automatically acquires and caches tokens
/// via the OAuth 2.0 client credentials grant.
///
/// Tokens are cached until they expire (with a 10-second leeway for safety) and
/// refreshed transparently on the next call.
/// </summary>
public sealed class ClientCredentialsTokenProvider : ITokenProvider, IDisposable
{
private readonly string _clientId;
private readonly string _clientSecret;
private readonly string _audience;
private readonly AuthenticationApiClient _authClient;
private readonly SemaphoreSlim _semaphore = new(1, 1);

private string? _accessToken;

private long _expiresAtTicks = DateTime.MinValue.Ticks;

private const int LeewaySeconds = 10;

/// <summary>
/// Creates a new <see cref="ClientCredentialsTokenProvider"/> for automatic token acquisition.
/// </summary>
/// <param name="domain">The Auth0 domain (e.g., "your-tenant.auth0.com").</param>
/// <param name="clientId">The client ID of the application.</param>
/// <param name="clientSecret">The client secret of the application.</param>
/// <param name="audience">
/// The API audience. Defaults to <c>https://{domain}/api/v2/</c>.
/// </param>
/// <param name="httpClient">
/// An optional <see cref="HttpClient"/> for making token requests.
/// If not provided, a new instance will be created and managed internally.
/// </param>
public ClientCredentialsTokenProvider(
string domain,
string clientId,
string clientSecret,
string? audience = null,
HttpClient? httpClient = null)
{
if (string.IsNullOrWhiteSpace(domain))
throw new ArgumentException("Domain must not be null, empty, or whitespace.", nameof(domain));
if (string.IsNullOrWhiteSpace(clientId))
throw new ArgumentException("Client ID must not be null, empty, or whitespace.", nameof(clientId));
if (string.IsNullOrWhiteSpace(clientSecret))
throw new ArgumentException("Client secret must not be null, empty, or whitespace.", nameof(clientSecret));

_clientId = clientId;
_clientSecret = clientSecret;
_audience = audience ?? $"https://{domain}/api/v2/";

var connection = httpClient != null
? new HttpClientAuthenticationConnection(httpClient)
: null;
_authClient = new AuthenticationApiClient(domain, connection);
}

/// <summary>
/// Returns a valid access token, fetching a new one from Auth0 if the cached
/// token has expired or does not yet exist. Thread-safe.
/// </summary>
/// <param name="cancellationToken">A token to cancel the operation.</param>
public async Task<string> GetTokenAsync(CancellationToken cancellationToken = default)
{
var cachedToken = Volatile.Read(ref _accessToken);
var expiresAt = new DateTime(Interlocked.Read(ref _expiresAtTicks), DateTimeKind.Utc);
if (cachedToken != null && DateTime.UtcNow < expiresAt.AddSeconds(-LeewaySeconds))
{
return cachedToken;
}

await _semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
// Double-check after acquiring the semaphore
cachedToken = Volatile.Read(ref _accessToken);
expiresAt = new DateTime(Interlocked.Read(ref _expiresAtTicks), DateTimeKind.Utc);
if (cachedToken != null && DateTime.UtcNow < expiresAt.AddSeconds(-LeewaySeconds))
{
return cachedToken;
}

await FetchTokenAsync(cancellationToken).ConfigureAwait(false);
}
finally
{
_semaphore.Release();
}

return Volatile.Read(ref _accessToken)!;
}

private async Task FetchTokenAsync(CancellationToken cancellationToken)
{
var response = await _authClient.GetTokenAsync(new ClientCredentialsTokenRequest
{
ClientId = _clientId,
ClientSecret = _clientSecret,
Audience = _audience
}, cancellationToken).ConfigureAwait(false);

var newExpiryTicks = DateTime.UtcNow.AddSeconds(response.ExpiresIn).Ticks;

// Write expiry first, then the token. The fast-path reader checks the token
// for null before examining the expiry, so this ordering ensures it never
// reads a fresh token paired with a stale expiry.
Interlocked.Exchange(ref _expiresAtTicks, newExpiryTicks);
Volatile.Write(ref _accessToken, response.AccessToken);
}

/// <summary>
/// Releases resources held by this provider.
/// </summary>
public void Dispose()
{
_semaphore.Dispose();
_authClient.Dispose();
}
}
34 changes: 34 additions & 0 deletions src/Auth0.ManagementApi/Wrapper/DelegateTokenProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
namespace Auth0.ManagementApi;

/// <summary>
/// An <see cref="ITokenProvider"/> backed by a user-supplied delegate.
/// Use this to retrieve tokens from an external source such as a secret manager,
/// cache, or any custom async operation.
/// The delegate is invoked on every call; no caching is performed by this class.
/// </summary>
public sealed class DelegateTokenProvider : ITokenProvider
{
private readonly Func<CancellationToken, Task<string>> _tokenFactory;

/// <summary>
/// Creates a new <see cref="DelegateTokenProvider"/> with a cancellable async token factory.
/// </summary>
/// <param name="tokenFactory">
/// An async function that accepts a <see cref="CancellationToken"/> and returns a valid access token.
/// </param>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="tokenFactory"/> is null.</exception>
public DelegateTokenProvider(Func<CancellationToken, Task<string>> tokenFactory)
{
_tokenFactory = tokenFactory ?? throw new ArgumentNullException(nameof(tokenFactory));
}

/// <inheritdoc />
public async Task<string> GetTokenAsync(CancellationToken cancellationToken = default)
{
var token = await _tokenFactory(cancellationToken).ConfigureAwait(false);
if (token == null)
throw new InvalidOperationException(
"The token factory returned null. Ensure the delegate returns a valid access token.");
return token;
}
}
14 changes: 14 additions & 0 deletions src/Auth0.ManagementApi/Wrapper/ITokenProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace Auth0.ManagementApi;

/// <summary>
/// Abstraction for supplying access tokens to the Management API client.
/// Implement this interface to support custom token strategies.
/// </summary>
public interface ITokenProvider
{
/// <summary>
/// Returns a valid access token, fetching or refreshing as needed.
/// </summary>
/// <param name="cancellationToken">A token to cancel the operation.</param>
Task<string> GetTokenAsync(CancellationToken cancellationToken = default);
}
Loading
Loading