Search code examples
c#.netblazordotnet-httpclient

.net services.AddHttpClient Automatic Access Token Handling


I am trying to write a Blazor app that uses client secret credentials to get an access token for the API. I wanted to encapsulate it in such a way that it handles the token fetching and refreshing behind the scenes. To achieve this, I created the following inherited class which uses IdentityModel Nuget package:

public class MPSHttpClient : HttpClient
{
    private readonly IConfiguration Configuration;
    private readonly TokenProvider Tokens;
    private readonly ILogger Logger;

    public MPSHttpClient(IConfiguration configuration, TokenProvider tokens, ILogger logger)
    {
        Configuration = configuration;
        Tokens = tokens;
        Logger = logger;
    }

    public async Task<bool> RefreshTokens()
    {
        if (Tokens.RefreshToken == null)
            return false;

        var client = new HttpClient();

        var disco = await client.GetDiscoveryDocumentAsync(Configuration["Settings:Authority"]);
        if (disco.IsError) throw new Exception(disco.Error);

        var result = await client.RequestRefreshTokenAsync(new RefreshTokenRequest
        {
            Address = disco.TokenEndpoint,
            ClientId = Configuration["Settings:ClientID"],
            RefreshToken = Tokens.RefreshToken
        });

        Logger.LogInformation("Refresh Token Result {0}", result.IsError);

        if (result.IsError)
        {
            Logger.LogError("Error: {0)", result.ErrorDescription);

            return false;
        }

        Tokens.RefreshToken = result.RefreshToken;
        Tokens.AccessToken = result.AccessToken;

        Logger.LogInformation("Access Token: {0}", result.AccessToken);
        Logger.LogInformation("Refresh Token: {0}" , result.RefreshToken);

        return true;
    }

    public async Task<bool> CheckTokens()
    {
        if (await RefreshTokens())
            return true;


        var client = new HttpClient();

        var disco = await client.GetDiscoveryDocumentAsync(Configuration["Settings:Authority"]);
        if (disco.IsError) throw new Exception(disco.Error);

        var result = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
        {
            Address = disco.TokenEndpoint,
            ClientId = Configuration["Settings:ClientID"],
            ClientSecret = Configuration["Settings:ClientSecret"]
        });

        if (result.IsError)
        {
            //Log("Error: " + result.Error);
            return false;
        }

        Tokens.AccessToken = result.AccessToken;
        Tokens.RefreshToken = result.RefreshToken;

        return true;
    }


    public new async Task<HttpResponseMessage> GetAsync(string requestUri)
    {
        DefaultRequestHeaders.Authorization =
                        new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", Tokens.AccessToken);

        var response = await base.GetAsync(requestUri);

        if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
        {
            if (await CheckTokens())
            {
                DefaultRequestHeaders.Authorization =
                                new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", Tokens.AccessToken);

                response = await base.GetAsync(requestUri);
            }
        }

        return response;
    }
}

The idea is to keep from having to write a bunch of redundant code to try the API, then request/refresh the token if you are unauthorized. I tried it at first using extension methods to HttpClient, but there was no good way to inject the Configuration into a static class.

So my Service code is written as this:

public interface IEngineListService
{
    Task<IEnumerable<EngineList>> GetEngineList();
}

public class EngineListService : IEngineListService
{
    private readonly MPSHttpClient _httpClient;

    public EngineListService(MPSHttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    async Task<IEnumerable<EngineList>> IEngineListService.GetEngineList()
    {
        return await JsonSerializer.DeserializeAsync<IEnumerable<EngineList>>
            (await _httpClient.GetStreamAsync($"api/EngineLists"), new JsonSerializerOptions() { PropertyNameCaseInsensitive = true });
    }
}

Everything compiles great. In my Startup, I have the following code:

        services.AddScoped<TokenProvider>();

        services.AddHttpClient<IEngineListService, EngineListService>(client =>
        {
            client.BaseAddress = new Uri(Configuration["Settings:ApiAddress"]);
        });

Just to be complete, Token Provider looks like this:

public class TokenProvider
{
    public string AccessToken { get; set; }
    public string RefreshToken { get; set; }
}

When I run the App, it complains that it can't find a suitable constructor for EngineListService in the call to services.AddHttpClient. Is there a way to pass AddHttpClient an actual instance of the IEngineListService. Any other way I might be able to achieve this?

Thanks, Jim


Solution

  • Special thanks to @pinkfloydx33 for helping me solve this. This link that he shared https://blog.joaograssi.com/typed-httpclient-with-messagehandler-getting-accesstokens-from-identityserver/ was everything I needed. The trick was that there exists a class called DelegatingHandler that you can inherit and override the OnSendAsync method and do all of your token-checking there before sending it to the final HttpHandler. So my new MPSHttpClient class is as so:

    public class MPSHttpClient : DelegatingHandler
    {
        private readonly IConfiguration Configuration;
        private readonly TokenProvider Tokens;
        private readonly ILogger<MPSHttpClient> Logger;
        private readonly HttpClient client;
    
        public MPSHttpClient(HttpClient httpClient, IConfiguration configuration, TokenProvider tokens, ILogger<MPSHttpClient> logger)
        {
            Configuration = configuration;
            Tokens = tokens;
            Logger = logger;
            client = httpClient;
        }
    
        public async Task<bool> CheckTokens()
        {
            var disco = await client.GetDiscoveryDocumentAsync(Configuration["Settings:Authority"]);
            if (disco.IsError) throw new Exception(disco.Error);
    
            var result = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
            {
                Address = disco.TokenEndpoint,
                ClientId = Configuration["Settings:ClientID"],
                ClientSecret = Configuration["Settings:ClientSecret"]
            });
    
            if (result.IsError)
            {
                //Log("Error: " + result.Error);
                return false;
            }
    
            Tokens.AccessToken = result.AccessToken;
            Tokens.RefreshToken = result.RefreshToken;
    
            return true;
        }
    
        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            request.SetBearerToken(Tokens.AccessToken);
    
            var response = await base.SendAsync(request, cancellationToken);
    
            if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
            {
                if (await CheckTokens())
                {
                    request.SetBearerToken(Tokens.AccessToken);
    
                    response = await base.SendAsync(request, cancellationToken);
                }
            }
    
            return response;
        }
    }
    

    The big changes here are the inheritance and I used DI to obtain the HttpClient much like @Rosco mentioned. I had tried to override OnGetAsync in my original version. When inheriting from DelegatingHandler, all you have to override is OnSendAsync. This will handle all of your get, put, post, and deletes from your HttpContext all in one method.

    My EngineList Service is written as if there were no tokens to be considered, which was my original goal:

    public interface IEngineListService
    {
        Task<IEnumerable<EngineList>> GetEngineList();
    }
    
    public class EngineListService : IEngineListService
    {
        private readonly HttpClient _httpClient;
    
        public EngineListService(HttpClient httpClient)
        {
            _httpClient = httpClient;
        }
    
        async Task<IEnumerable<EngineList>> IEngineListService.GetEngineList()
        {
            return await JsonSerializer.DeserializeAsync<IEnumerable<EngineList>>
                (await _httpClient.GetStreamAsync($"api/EngineLists"), new JsonSerializerOptions() { PropertyNameCaseInsensitive = true });
        }
    }
    

    The Token Provider stayed the same. I plan to add expirations and such to it, but it works as is:

    public class TokenProvider
    {
        public string AccessToken { get; set; }
        public string RefreshToken { get; set; }
    }
    

    The ConfigureServices code changed just a bit:

        public void ConfigureServices(IServiceCollection services)
        {
        ...
    
            services.AddScoped<TokenProvider>();
    
            services.AddTransient<MPSHttpClient>();
    
            services.AddHttpClient<IEngineListService, EngineListService>(client =>
            {
                client.BaseAddress = new Uri(Configuration["Settings:ApiAddress"]);
            }).AddHttpMessageHandler<MPSHttpClient>();
    
         ...
         }
    

    You instantiate MPSHttpClient as Transient, then reference it with the AddHttpMessageHandler call attached to the AddHttpClient call. I know this is different than how others implement HttpClients, but I learned this method of creating client services from a Pluralsight video and have been using it for everything. I create a separate Service for each entity in the database. If say I wanted to do tires, I would add the following to ConfigureServices:

            services.AddHttpClient<ITireListService, TireListService>(client =>
            {
                client.BaseAddress = new Uri(Configuration["Settings:ApiAddress"]);
            }).AddHttpMessageHandler<MPSHttpClient>();
    

    It will use the same DelegatingHandler so I can just keep adding services for each entity type while no longer worrying about tokens. Thanks to everyone that responded.

    Thanks, Jim