34

I am using IHttpClientFactory for sending requests and receiving HTTP responses from two external APIs using Net Core 2.2.

I am looking for a good strategy to get a new access token using a refresh token that has been stored in the appsettings.json. The new access token needs to be requested when the current request returns 403 or 401 errors, When the new access and refresh token have been obtained, the appsettings.json needs to be updated with the new values in order to be used in subsequent requests.

I am using two clients to send requests to two different APIs but only one of them use token authentication mechanism.

I have implemented something simple that works but i am looking for a more elegant solution that can update the header dynamically when the current token has expired :

I have registered the IHttpClientFactory in the Startup.ConfigureServices method as follows:

services.AddHttpClient();

Once registered i am using it in two different methods to call two different APIs, the first method is:

   public async Task<AirCallRequest> GetInformationAsync(AirCallModel model)
    {
        try
        {


            CandidateResults modelCandidateResult = null;

            var request = new HttpRequestMessage(HttpMethod.Get,
            "https://*******/v2/*****");
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _appSettings.Value.Token);


            var clientJAAPI = _httpClientFactory.CreateClient();
            var responseclientJAAPI = await clientJAAPI.SendAsync(request);


            if (responseclientJAAPI.IsSuccessStatusCode)
            {
                modelCandidateResult = await responseclientJAAPI.Content
                   .ReadAsAsync<CandidateResults>();

                ....
            }


            if ((responseclientJAAPI .StatusCode.ToString() == "Unauthorized")
            {                    

                await RefreshAccessToken();

               //Calls recursively this method again
                return await GetInformationAsync(model);

            }

            return null;
        }
        catch (Exception e)
        {
            return null;

        }

    }

The refresh Token method looks like that:

private async Task RefreshAccessToken()
    {


        var valuesRequest = new List<KeyValuePair<string, string>>();
        valuesRequest.Add(new KeyValuePair<string, string>("client_id", "*****"));
        valuesRequest.Add(new KeyValuePair<string, string>("client_secret","****"));
        valuesRequest.Add(new KeyValuePair<string, string>("grant_type", "refresh_token"));
        valuesRequest.Add(new KeyValuePair<string, string>("refresh_token", "*****"));


        RefreshTokenResponse refreshTokenResponse = null;

        var request = new HttpRequestMessage(HttpMethod.Post,
        "https://*****/connect/token");

        request.Content = new FormUrlEncodedContent(valuesRequest);

        var clientJAAPI = _httpClientFactory.CreateClient();
        var responseclientJAAPI = await clientJAAPI.SendAsync(request);

        if (responseclientJAAPI.IsSuccessStatusCode)
        {
            refreshTokenResponse = await responseclientJAAPI.Content.ReadAsAsync<RefreshTokenResponse>();

            //this updates the POCO object representing the configuration but not the appsettings.json :
            _appSettings.Value.Token = refreshTokenResponse.access_token;

        }

    }

Notice that I am updating the POCO object representing the configuration but not the appsettings.json, so the new values are stored in memory. I want to update the appsettings.json for subsequent requests.

If the solution proposed require to define the main settings for the Httpclient in the Startup.ConfigureService, it needs to allow to create different instances of the HttpClien, because one of the HttpClient instances (use in another method to call a second API) doesn't require a token to send the requests.

Peter Csala
  • 17,736
  • 16
  • 35
  • 75
D.B
  • 4,009
  • 14
  • 46
  • 83

2 Answers2

67

Looks like you need DelegatingHandler. In two words you can "intercept" your http request and add the Authorization header, then try to execute it and if token was not valid, refresh token and retry one more time. Something like:

public class AuthenticationDelegatingHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var token = await GetTokenAsync();
        request.Headers.Authorization = new AuthenticationHeaderValue(token.Scheme, token.AccessToken);
        var response = await base.SendAsync(request, cancellationToken);

        if (response.StatusCode == HttpStatusCode.Unauthorized || response.StatusCode == HttpStatusCode.Forbidden)
        {
            token = await RefreshTokenAsync();
            request.Headers.Authorization = new AuthenticationHeaderValue(token.Scheme, token.AccessToken);
            response = await base.SendAsync(request, cancellationToken);
        }

        return response;
    }
}

You register this delegating handler in Startup.cs like that:

services.AddTransient<AuthenticationDelegatingHandler>();
services.AddHttpClient("MySecuredClient", client =>
    {
        client.BaseAddress = new Uri("https://baseUrl.com/");
    })
    .AddHttpMessageHandler<AuthenticationDelegatingHandler>();

And use like that:

var securedClient = _httpClientFactory.CreateClient("MySecuredClient");
securedClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, "v2/relativeUrl"));

Regarding storing refresh token in appsetting.json. I don't think it's a good idea as refresh token doesn't have expiration time. If you can use credentials to obtain new token for the first time, use it, and then store refresh token in-memory for further refreshes.

Here you can see how I manage client credential token refreshes and try to make it work for your scenario.


Update:

Here you can find same idea but implemented by professionals and available in nuget. The usage is very simple:

services.AddAccessTokenManagement(options =>
{
    options.Client.Clients.Add("identityserver", new ClientCredentialsTokenRequest
    {
        Address = "https://demo.identityserver.io/connect/token",
        ClientId = "m2m.short",
        ClientSecret = "secret",
        Scope = "api" // optional
    });
});

services.AddHttpClient<MyClient>(client =>
{
    client.BaseAddress = new Uri("https://demo.identityserver.io/api/");
})
.AddClientAccessTokenHandler();

Requests sent by MyClient will always have valid bearer token. The refresh performed automatically.

Artur
  • 4,595
  • 25
  • 38
  • Thanks. It is a good solution. Just a couple of things were missing. First i needed to add services.AddTransient(); before services.AddHttpClient("name").AddHttpMessageHandler(); cos i was getting a Service not Registered exception. Secondly, I do not like the idea get a new token for every request to the server. Cos if i keep it in memory after the response is sent to the client it is going to be destroyed. I will need to cache the refresh token or something like that. – D.B May 19 '19 at 09:06
  • You are right, I forgot about the registration, will update my answer. Regarding you second point, look at the link I gave at the end. Specially at 'AccessTokensCacheManager' and its usage in 'AuthenticationDelegatingHandler'. It reuses access token and obtaining new one only when required. Here you can implement any logic you want. – Artur May 19 '19 at 09:48
  • I have been looking at your link. Just wondering for how long the token you stored into the ConcurrentDictionary will stay available? what is the life cycle of that token, isn't it destroyed when a response is sent to the client? – D.B May 19 '19 at 12:37
  • The token will be alive as long as `AccessTokensCacheManager` is alive and it registered as Singlton. Hence it will be alive as long as application is running. I'm taking half of expiration period as valid period for token, once it's expired I'm obtaining new token and replace the old one in ConcurrentDictionary. – Artur May 20 '19 at 04:13
  • Right I understand.That is a great solution. Just one questions remains in the code I can't see how you register the AccessTokensCacheManager as singleton. Should i do that using the services.AddScoped in the Startup.cs? – D.B May 20 '19 at 13:32
  • There is an extension method `AddAuthentication` in class `HttpClientBuilderExtensions` (Extensions/Core/HttpClientBuilderExtensions.cs). The `IHttpClientBuilder` interface is returned by `AddHttpClient` method that should be called in `Startup.cs`. – Artur May 20 '19 at 17:39
  • I was trying to implement something similar to your cache approach, I am using a clientId, and the clientId is a parameter that is sent to the API controller, that Action Method is the one that uses the SendAsync. Wondering How I can read that parameter withing the DelegatingHandler, knowing that the DelegatingHandler is instantiated in the startup class. I created another question for it, if u want have a look at it: https://stackoverflow.com/questions/56456954/how-to-read-parameters-sent-to-an-action-method-webapi-within-a-delegatinghand – D.B Jun 05 '19 at 12:58
  • @Artur Did you switch over to use IdentityServer nuget? Did I read docs/sample right that you need to have a IDistributedCache (redis?)? – Terry Jun 04 '23 at 14:32
  • 1
    @Terry I never used this library. But looking into [source code](https://github.com/IdentityModel/IdentityModel.AspNetCore/blob/72479bf781eac07b5f7f568ae45e498b5ba9ed69/src/AccessTokenManagement/TokenManagementServiceCollectionExtensions.cs#L385), it adds an in-memory cache implementation by default. IMHO, it makes sense to use remote cache only in serverless scenarios. In a simple scenario, every service instance can obtain its own token, store it in memory, and avoid remote calls overhead on every token retrieval. – Artur Jun 07 '23 at 08:01
  • 1
    BTW the lib was moved here https://github.com/DuendeSoftware/Duende.AccessTokenManagement/wiki – Artur Jun 07 '23 at 08:02
2

I like the DelegatingHandler idea of Artur. But that solution has code duplication:

request.Headers.Authorization = new AuthenticationHeaderValue(token.Scheme, token.AccessToken);
response = await base.SendAsync(request, cancellationToken);

Also the GetTokenAsync and RefreshTokenAsync methods are part of the DelegatingHandler class, which might be a suboptimal.


If you want to avoid these you can use a combination of retry policy of Polly, DelegatingHandler and a token management service.

Here is sequence diagram which depicts the communication flow.

refresh token in case of 401

The related sample code is posted here.


UPDATE #1

An alternative version (which separates the responsibilities better) is available here.

refreshing token

Peter Csala
  • 17,736
  • 16
  • 35
  • 75