Re-Authorize Efficiently Using Polly And .NET HttpClientFactory
In today’s world, we are using a lot of APIs to build our applications. To make your .NET based applications more ressilient and fault tolerant, go to solution is to use Polly. Probably, you are already using it. So,I’m not going to explain how to use it in general. You can find some excellent documentation here.
However, In this post I’m going to discuss about one of the typical scenario where we need to refresh the authentication token when using Polly. Let’s get started.
UPDATE: If you are using Polly v8 and .NET 8, you can find the updated post here
The Problem
To handle such situation what we do is retry on Unauthorized failure and call a method to reauthorize. Something like below from the Polly documentation -
1 |
|
The above pattern works by sharing the httpClient variable across closures, but a significant drawback is that you have to declare and use the policy in the same scope. This makes impossible to use dependency injection approach where you define policies centrally on startup, then provide them by DI to the point of use. Using DI with Polly in this way is a powerful pattern for separation of concerns, and allows easy stubbing out of Polly in unit testing.
From Polly v5.1.0, with Context available as a state variable to every delegate, the policy declaration can be rewritten:
1 |
|
And the usage (elsewhere in the codebase):
1 |
|
Passing context as state-data parameters in the different parts of policy execution allows policy declaration and usage now to be separate.
The above example and explanation is taken from the Polly documentation. You can find the same here.
IMO, the above approach is not very clean as we are passing httpClient as a state variable. Instead if we can pass the Token as a state variable, it will be more cleaner. Let’s try to implement the same.
The Solution
The solution could be to make use of the Context , DelegationHandler and HttpClientFactory. Define a communication between them as below -
In our scenario, we are using Auth0 as Token Provider and our Downstream system (Todo API)is protected by it.
First, we will focus on Auth0 Service. This service is responsible for getting the actual Token from Auth0 and refresh it when it is expired. We will start by defining Token DTO as below -
1 |
|
We will also define a Auth0Service which will be responsible for the actual hard work -
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public interface IAuth0Service
{
Task<Token> GetTokenAsync();
}
public class Auth0Service : IAuth0Service
{
private readonly HttpClient _httpClient;
private readonly Auth0Option _settings;
public Auth0Service(HttpClient client, Auth0Option settings)
{
_httpClient = client;
_settings = settings;
}
public async Task<Token> GetTokenAsync()
{
var content = new FormUrlEncodedContent(new Dictionary<string, string>
{
["client_id"] = _settings.ClientId,
["client_secret"] = _settings.ClientSecret,
["audience"] = _settings.Audience,
["grant_type"] = _settings.GrantType,
["scope"] = _settings.Scope
});
var result= await _httpClient.PostAsync(_settings.TokenUrl, content);
if (!result.IsSuccessStatusCode)
{
return Token.Empty;
}
var token = await result.Content.ReadFromJsonAsync<Token>();
return token!;
}
}
Next, we will focus on Token Service. This service is responsible for getting the cached Token from memory and refresh it from Auth0 Service when it is expired. Below is the implementation -
1 |
|
Here, couple of things to note -
-
We are using ValueTask for GetToken. This is because we know that the Hot path will be to get the Token from the memory cache and very few scenraio(e.g very first time when Downstream is getting hit) it will actualy call the Auth0 Service. So, we are using ValueTask to avoid the overhead of allocating a new Task in the heap.
-
We are using SetAbsoluteExpiration and SetSlidingExpiration both to make sure we are not overwhelming by storing the unused Token in the memory cache. Also to avoid edge case when the Token is expired but not yet refreshed we are making sure it removed from the cache before it’s actual expiry time.
Time to rewrite our Policy for getting the refreshed Token -
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static IAsyncPolicy<HttpResponseMessage> GetTokenRefresher(IServiceProvider provider, HttpRequestMessage request)
{
var delay = Backoff.ConstantBackoff(TimeSpan.FromMilliseconds(100), retryCount: 3);
return Policy<HttpResponseMessage>
.HandleResult(msg => msg.StatusCode == System.Net.HttpStatusCode.Unauthorized)
.WaitAndRetryAsync(delay, async (_, _, _, _) =>
{
await provider.GetRequiredService<ITokenService>().RefreshToken();
request.SetPolicyExecutionContext(new Context());
});
}
public static IAsyncPolicy<HttpResponseMessage> GetConstantBackofffPolicy()
{
var delay = Backoff.ConstantBackoff(TimeSpan.FromMilliseconds(100), retryCount: 3);
return HttpPolicyExtensions
.HandleTransientHttpError()
.OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.NotFound)
.WaitAndRetryAsync(delay);
}
Very simple and clean. We are making sure, after refreshing the token, the context is clear so that in DelegationHandler we can get the updated token. Below is the implementation of custom handler -
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class TokenRetrievalHandler : DelegatingHandler
{
private readonly ITokenService tokenService;
private const string TokenRetrieval = nameof(TokenRetrieval);
private const string TokenKey = nameof(TokenKey);
public TokenRetrievalHandler(ITokenService service)
{
tokenService = service;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var context = request.GetPolicyExecutionContext();
if (context.Count == 0)
{
context = new Context(TokenRetrieval, new Dictionary<string, object> { { TokenKey, await tokenService.GetToken() } });
request.SetPolicyExecutionContext(context);
}
var token = (Token)context[TokenKey];
if(token!= Token.Empty)
request.Headers.Authorization = new AuthenticationHeaderValue(token.Scheme, token.AccessToken);
return await base.SendAsync(request, cancellationToken);
}
}
We are intercepting the actual call and making sure,the updated token is added in header always.
We have all the pieces in place. Now, we just need to orchestrate the flow. We will also make use of PolicyWrap to combine the same type of policies to make our application fault tolerant.
We will create ServiceCollection extension to keep our StartUp.cs clear and concise. First we will create the extension for Auth0 Service as below
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static IServiceCollection AddAuth0Service(this IServiceCollection services, HostBuilderContext context)
{
var auth0Option = context.Configuration.GetSection(Auth0Option.ConfigKey).Get<Auth0Option>()!;
services.AddSingleton(auth0Option);
services
.AddHttpClient<IAuth0Service, Auth0Service>(client =>
{
client.BaseAddress = new Uri(auth0Option.BaseAddress);
client.DefaultRequestHeaders.TryAddWithoutValidation("Content-Type", "application/json");
})
.AddPolicyHandler(PollyRetryPolicies.GetConstantBackofffPolicy());
return services;
}
Now, the intersting Typed Client for DownStream System which is in our case TodoService. Below is the extension method for the same -
1 |
|
We have added the TokenRetrievalHandler to intercept any call from this Typed Client and attach the Token in the header. Also, we have added the PolicyWrap to make sure we are retrying 3 times with a constant backoff of 100ms in case of any transient error and also refresh the token in case of Unauthorized error.
And the last bit, The Console from where we are calling the API -
1 |
|
Here, we have configured the services for MemoryCache, Auth0Service and TodoService. Also, we have added the appsettings.json file to get the configuration for the same.
That’s it. We are done. Now, we can run the application.
Conclusion
IMO, the above approach is more cleaner and easy to understand. Also, it is more flexible and can be extended easily.Also, each service has it’s own responsibility and can be tested independently. I have used Auth0 as Token Provider but you can use any other provider as well with any other Downstream system.I have skipped the part for Auth0 configuration but let me know if you want me to explain it in seperate post. You can also use the same approach for Named Client as well. This post is inspired from below post
As always, You can find the source code here.