Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ManagementApiClient and other clients' constructors should accept Func<Task<string>> getToken or Func<ValueTask<string>> getToken #759

Open
5 tasks done
voroninp opened this issue Jan 6, 2025 · 1 comment
Labels
feature request A feature has been asked for or suggested by the community

Comments

@voroninp
Copy link

voroninp commented Jan 6, 2025

Checklist

  • I have looked into the Readme and have not found a suitable solution or answer.
  • I have looked into the API documentation and have not found a suitable solution or answer.
  • I have searched the issues and have not found a suitable solution or answer.
  • I have searched the Auth0 Community forums and have not found a suitable solution or answer.
  • I agree to the terms within the Auth0 Code of Conduct.

Describe the problem you'd like to have solved

Currently the constructor of ManagementApiClient requires a token. However, tokens are expirable. To avoid 401 Unauthorized errors the calling code should ensure token is still valid or recreate the instance of ManagementApiClient if token has expired.
IMO, this is an accidental complexity.

Describe the ideal solution

If I could provide a callback for acquiring a new token, things could be simplified as verifications become the responsibility of ManagementApiClient and clients it exposes (Organizations, Users, etc.)

Alternatives and current workarounds

As a workaround I pass an http client with a delegating handler which refreshes (if needed) the token and sets authorization header as Bearer <token>:

public static IServiceCollection AddAuth0Client(this IServiceCollection services, IConfiguration configuration, string configurationSection)
{
    services.AddOptions<Auth0Client.Options>()
        .Bind(configuration.GetSection(configurationSection))
        .ValidateDataAnnotations();

    services.AddScoped<Auth0Client.AuthenticatingHandler>();

    services.AddHttpClient("Auth0")
        .AddHttpMessageHandler<Auth0Client.AuthenticatingHandler>();

    services.AddScoped<IManagementApiClient>(s =>
    {
        var options = s.GetRequiredService<IOptions<Auth0Client.Options>>().Value;
        var httpClientFactory = s.GetRequiredService<IHttpClientFactory>();
        var httpClient = httpClientFactory.CreateClient("Auth0");
        var connection = new HttpClientManagementConnection(httpClient);

        return new ManagementApiClient(
            token: "Whatever. It does not matter. It will be replaced by the token acquired by Auth0Client.AuthenticatingHandler", 
            options.ManagementApiUri, connection);
    });
    services.AddScoped<IOrganizationsClient>(s => s.GetRequiredService<IManagementApiClient>().Organizations);
    services.AddScoped<IAuth0Client, Auth0Client>();

    return services;
}

Code of the handler:

public sealed class AuthenticatingHandler : DelegatingHandler
{
    private static readonly ManualResetEventSlim _event = new (initialState: true);
    private static volatile AccessTokenResponse? _tokenResponse = null;
    private static DateTime _tokenExpiresAt = DateTime.MaxValue;

    private readonly Options _options;
    private readonly TimeProvider _timeProvider;

    public AuthenticatingHandler(IOptions<Options> options, TimeProvider timeProvider)
    {
        _options = options.NotNull().Value;
        _timeProvider = timeProvider.NotNull();
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        await AddAccessToken(request, cancellationToken);

        try
        {
            return await base.SendAsync(request, cancellationToken);
        }
        // just retry authentication if request failed.
        catch (ErrorApiException ex) when (ex.StatusCode == HttpStatusCode.Unauthorized)
        {
            await AddAccessToken(request, cancellationToken);
            return await base.SendAsync(request, cancellationToken);
        }
    }

    private async Task AddAccessToken(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        _event.Wait(cancellationToken);
        try
        {
            if (_tokenResponse is null || _tokenExpiresAt <= _timeProvider.GetUtcNow().UtcDateTime)
            {
                var authClient = new AuthenticationApiClient(_options.BaseUri);
                var credentialsRequest = new ClientCredentialsTokenRequest
                {
                    Audience = _options.Audience,
                    ClientId = _options.ClientId,
                    ClientSecret = _options.ClientSecret,
                };
                _tokenResponse = await authClient.GetTokenAsync(credentialsRequest, cancellationToken);
                _tokenExpiresAt = _timeProvider.GetUtcNow().UtcDateTime + TimeSpan.FromSeconds(_tokenResponse.ExpiresIn) - TimeSpan.FromSeconds(1);
            }

            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _tokenResponse.AccessToken);
        }
        finally
        {
            _event.Set();
        }
    }
}

Additional context

No response

@voroninp voroninp added the feature request A feature has been asked for or suggested by the community label Jan 6, 2025
@Hawxy
Copy link
Contributor

Hawxy commented Jan 6, 2025

Just an FYI that I already support this scenario via my own package that's been around for a number of years: https://github.com/Hawxy/Auth0Net.DependencyInjection

This has been discussed a few times with the Auth0 team, but I don't think there's an appetite for internalizing the functionality.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature request A feature has been asked for or suggested by the community
Projects
None yet
Development

No branches or pull requests

2 participants