Hey there,
I’m quite new to using Auth0, so sorry in advance if I misunderstood something in the documentation.
I’m building a small library to manage roles and permissions for my application through a secure backend. I figured the Management API would be the best approach for this. After reading through the documentation, I created a testing Management API access token, and that works perfectly.
For production, I understand that requesting an access token only when needed is recommended. So, I wrote the following class to manage ManagementApiClient
instances, ensuring it works when an access token is already configured (in my case, through .NET User Secrets):
My Implementation
using Auth0.ManagementApi;
using InfiniLore.Credentials.Auth0.Contracts;
using Microsoft.Extensions.Options;
using RestSharp;
using System.Text.Json;
namespace InfiniLore.Credentials.Auth0;
public class Auth0ClientManager(IOptions<Auth0Options> options) : IAuth0ClientManager {
private readonly Auth0Options _settings = options.Value;
private string AccessToken { get; set; } = options.Value.AccessToken ?? string.Empty ;
private DateTime _tokenExpiry = DateTime.MinValue;
private string? _scopes;
private string? _tokenType;
private IManagementApiClient? Client { get; set; }
// -----------------------------------------------------------------------------------------------------------------
// Methods
// -----------------------------------------------------------------------------------------------------------------
public void ResetClient() {
AccessToken = string.Empty;
_tokenExpiry = DateTime.MinValue;
_scopes = null;
_tokenType = null;
Client = null;
}
public async ValueTask<IManagementApiClient> GetClientAsync(CancellationToken ct = default) {
if (_tokenExpiry != DateTime.MinValue && DateTime.UtcNow - TimeSpan.FromMinutes(5) > _tokenExpiry)
ResetClient(); // Ensure a fresh client if token expired
return Client ??= await ClientFactory(ct);
}
private async Task<IManagementApiClient> ClientFactory(CancellationToken ct = default) {
if (string.IsNullOrWhiteSpace(_settings.Domain)) throw new InvalidOperationException("Domain is required and cannot be empty.");
if (!Uri.IsWellFormedUriString($"https://{_settings.Domain}", UriKind.Absolute))
throw new InvalidOperationException($"Domain '{_settings.Domain}' is not a valid URI.");
if (AccessToken.IsNullOrWhiteSpace()) await GetNewAccessToken(ct);
if (_tokenExpiry != DateTime.MinValue && DateTime.UtcNow > _tokenExpiry) await GetNewAccessToken(ct);
return new ManagementApiClient(AccessToken, _settings.Domain);
}
// ReSharper disable once JoinNullCheckWithUsage
private async Task GetNewAccessToken(CancellationToken ct = default) {
using var client = new RestClient($"https://{_settings.Domain}");
var request = new RestRequest("/oauth/token", Method.Post);
request.AddHeader("Content-Type", "application/x-www-form-urlencoded");
request.AddParameter("grant_type", "client_credentials", ParameterType.GetOrPost);
request.AddParameter("client_id", _settings.ClientId, ParameterType.GetOrPost);
request.AddParameter("client_secret", _settings.ClientSecret, ParameterType.GetOrPost);
request.AddParameter("audience", $"https://{_settings.Domain}/api/v2/", ParameterType.GetOrPost);
// request.AddParameter("scope", "read:users update:users read:resource_servers", ParameterType.GetOrPost);
RestResponse response = await client.ExecuteAsync(request, ct);
if (!response.IsSuccessful) throw new Exception($"Error: {response.StatusCode} - {response.Content}");
if(response.Content.IsNullOrWhiteSpace()) throw new Exception("Error: Access token is empty");
// Deserialize JSON response
var jsonResponse = JsonSerializer.Deserialize<Auth0TokenResponse>(response.Content);
if (jsonResponse?.AccessToken is null) throw new Exception("Failed to retrieve access token.");
// Store relevant values
AccessToken = jsonResponse.AccessToken;
_tokenExpiry = DateTime.UtcNow.AddSeconds(jsonResponse.ExpiresIn);
_scopes = jsonResponse.Scope;
_tokenType = jsonResponse.TokenType;
}
}
public class Auth0TokenResponse {
[JsonPropertyName("access_token")]
public string? AccessToken { get; set; }
[JsonPropertyName("expires_in")]
public int ExpiresIn { get; set; }
[JsonPropertyName("scope")]
public string? Scope { get; set; }
[JsonPropertyName("token_type")]
public string? TokenType { get; set; }
}
The Issue
Everything works when I use my testing access token in my config file, but as soon as that isn’t defined and I fetch a new access token, things go wrong.
- The token request succeeds, but in debugging, I noticed that the
scope
field returnsnull
. - When I try to use this access token in a request, I get the following error:
Auth0.Core.Exceptions.ErrorApiException: {"statusCode":401,"error":"Unauthorized","message":"Invalid token","attributes":{"error":"Invalid token"}}
This makes me think that I need to explicitly request scopes when fetching the access token, like in the commented-out line:
request.AddParameter("scope", "read:users update:users read:resource_servers");
But when I try that, I get this error when requesting the token:
Error: Forbidden - {"error":"access_denied","error_description":"Client has not been granted scopes: read:user
I’m not sure how to proceed from here. How do I correctly request and use an access token with scopes for the Management API?
Any help would be greatly appreciated!
(For clarity sake, the repo with the code snippet I posted above can be found here : GitHub - InfiniLore/credentials.cs: Auto generate permissions )