In this article, I want to share how I tackled the management of a JWT token during an integration project with a telecommunications service provider.
Context
Let’s start by understanding the context: I needed to develop a .NET 9 application to interact with a telecommunications service provider. The provider exposed REST APIs authenticated via a JWT token, which had the following characteristics:
- The token did not include any information about its expiration.
- The expiration was determined by the provider, who offered an endpoint to check if the token was still valid.
- The provider could revoke the token at any time, requiring a new authentication API call to obtain a new token.
Additionally, the application had to handle high traffic, so making frequent API calls to validate the token was not the best choise due to performance and call limits.
Workflow
This article focuses on the core elements I used to manage the token effectively. Here’s the workflow I followed:
Defining a policy
To manage retries, I used the Polly library. For those unfamiliar, Polly is a .NET library that simplifies implementing resilience policies such as retries, circuit breakers, timeouts, and fallbacks. To install it, I added these packages to the project:
dotnet add package Polly
dotnet add package Polly.Extensions.Http
In the class where I used Polly, I defined a single policy applied to all methods. Multiple policies can also be created to handle specific scenarios. Here’s how I defined it:
private const int RETRY_COUNT = 2;
//Define Policy
private static readonly AsyncRetryPolicy<HttpResponseMessage> retryPolicy = HttpPolicyExtensions
.HandleTransientHttpError()
.OrResult( r =>
r.StatusCode != HttpStatusCode.OK &&
r.StatusCode != HttpStatusCode.NoContent
)
.Or<HttpRequestException>(ex =>
ex.InnerException is TimeoutException
)
.RetryAsync(
RETRY_COUNT,
async (response, retryCount, context) =>
{
//Retry implementation
});
How the Policy Works:
.HandleTransientHttpError(): detects automatically transient HTTP errors (e.g., server 5xx errors or temporary network issues)
.OrResult( … ) row 6: retries when the status code is not 200 OK
or No Content
.
.Or(ex => ex.InnerException is TimeoutException ): retries on timeout exceptions.
Here is an example:
private const int RETRY_COUNT = 2;
//Define Policy
private static readonly AsyncRetryPolicy<HttpResponseMessage> retryPolicy = HttpPolicyExtensions
.HandleTransientHttpError()
.OrResult( r =>
r.StatusCode != HttpStatusCode.OK &&
r.StatusCode != HttpStatusCode.NoContent
)
.Or<HttpRequestException>(ex =>
ex.InnerException is TimeoutException
)
.RetryAsync(
RETRY_COUNT,
async (response, retryCount, context) =>
{
var integration = (PhoneServiceIntegration)context["phoneService"];
//=============== Unauthorized ===============
if (response.Result?.StatusCode == HttpStatusCode.Unauthorized)
{
//Clean token from cache
integration.InvalidateToken();
//Get new token and update it in header
var token = await integration.GetTokenAsync();
integration.AddOrUpdateHeaderToken(token);
}
//=============== Timeout ===============
else if (response.Exception is HttpRequestException && response.Exception.InnerException is TimeoutException)
{
integration._logger.Warning($"Timeout, retry number {retryCount}", "");
}
//=============== Other ===============
else
{
//manage other type of responses
}
});
The class where I’m using Polly is called PhoneServiceIntegration
. On line 17, I inject the service instance into the policy, allowing access to methods and services provided via Dependency Injection.
From lines 20 to 29, I handle the case of an invalid or missing token. Whenever a 401 Unauthorized
error occurs, the token is invalidated and regenerated.
From lines 30 to 34, I address timeout errors. In such cases, the request will be retried until the maximum number of attempts is reached, which in my case is set to 2 (defined by the RETRY_COUNT
constant).
On line 36, I handle other types of errors. Of course, this depends on the context and your application. In my case, I implemented specific scenarios defined by the telecommunications provider, which I won’t include here as they are not relevant to this article.
Using the policy
Now that we’ve defined our policy, let’s look at how to use it. My project was designed to handle concurrent usage by around a hundred users, most of whom performed operations involving calls to multiple provider endpoints (for example, the GetQueues
endpoint)
To optimize resource usage, I used the IClientFactory
interface, which was injected directly into the constructor.
public ComsyIntegration(
IConfiguration config,
ICache cache,
IHttpClientFactory _clientFactory)
{
_cache = cache;
_client = _clientFactory.CreateClient("PhoneProviderClient");
_comsySettings = comsySettings;
}
I defined in the Program.cs (in my case in this project since it is an API project):
services
.AddHttpClient<ComsyIntegration>("PhoneProviderClient")
.AddHttpMessageHandler<LoggingHandler>();
I then defined a generic method to obtain an instance of an HTTP client, retrieve the token, and add it to the header:
public async Task<HttpClient> GetHttpClientAsync()
{
var token = await GetTokenAsync();
AddOrUpdateComsyToken(token);
return _client;
}
The GetTokenAsync()
method searches for the token in the cache. If it’s not found, it makes an authentication API call to the provider. If the token had expired, the previously configured policy would handle removing it from the cache and making a new authentication call.
The AddOrUpdateComsyToken(token)
method simply adds the token to the request header. At this point, I created a generic function to handle HTTP verbs such as GET, POST, PUT, and DELETE. Below is an example of the method used to handle GET requests:
//Custom implementation for http GET
public async Task<T?> GetAsync<T>(string endpoint)
{
//Build client
var client = await GetHttpClientAsync();
//Get response from endpoint
using var response = await retryPolicy.ExecuteAsync(async (context) =>
await client.GetAsync(endpoint)
, new Context { ["phoneService"] = this });
//Parse content and deserialize response
string responseContent = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<T>(responseContent)
}
Let’s clarify the implementation.
var client = await GetHttpClientAsync()
: this calls the method that returns an instance ofHttpClient
.- The magic happens between lines 7 and 9! The HTTP call is made using our policy. Note that the actual HTTP call happens on line 8, while on the same line, the service context is passed as a parameter. If the HTTP call on line 8 fails (according to the logic we defined), the policy will trigger. This could involve retrying the request in case of a timeout or generating a new token if a
401 Unauthorized
error occurs.
Here is the complete code:
public partial class PhoneProviderIntegration: IPhoneProviderIntegration, IDisposable
{
private HttpClient _client;
private readonly ICxLogger _logger;
private readonly ICache _cache;
//Retry counter if http call fail
private const int RETRY_COUNT = 2;
#region Polly Policy
//Define Policy
private static readonly AsyncRetryPolicy<HttpResponseMessage> retryPolicy = HttpPolicyExtensions
.HandleTransientHttpError()
.OrResult( r =>
r.StatusCode != HttpStatusCode.OK &&
r.StatusCode != HttpStatusCode.NoContent
)
.Or<HttpRequestException>(ex =>
ex.InnerException is TimeoutException
)
.RetryAsync(
RETRY_COUNT,
async (response, retryCount, context) =>
{
var integration = (PhoneServiceIntegration)context["phoneService"];
//=============== Unauthorized ===============
if (response.Result?.StatusCode == HttpStatusCode.Unauthorized)
{
//Clean token from cache
integration.InvalidateToken();
//Get new token and update it in header
var token = await integration.GetTokenAsync();
integration.AddOrUpdateHeaderToken(token);
}
//=============== Timeout ===============
else if (response.Exception is HttpRequestException && response.Exception.InnerException is TimeoutException)
{
integration._logger.Warning($"Timeout, retry number {retryCount}", "");
}
//=============== Other ===============
else
{
//manage other type of responses
}
});
#endregion
//Example method
public async Task<List<QueueDTO>> GetQueuesAsync()
{
//Example of endpoint
string endpoint = $"https://endpoint.example/api/v1/queues";
//http get from endpoint
var queues = await GetAsync<List<QueueDTO>>(endpoint);
return queues;
}
#region Private
//Prepare and get Http client
public async Task<HttpClient> GetHttpClientAsync()
{
var token = await GetTokenAsync();
AddOrUpdateComsyToken(token);
return _client;
}
//Get token from cache or provider auth endpoint
public async Task<string> GetTokenAsync()
{
...
}
//Add Bearer JWT token to header
public async Task AddOrUpdateComsyToken(string token)
{
...
}
//Custom implementation for http GET
public async Task<T?> GetAsync<T>(string endpoint)
{
//Build client
var client = await GetHttpClientAsync();
//Get response from endpoint
using var response = await retryPolicy.ExecuteAsync(async (context) =>
await client.GetAsync(endpoint)
, new Context { ["phoneService"] = this });
//Parse content and deserialize response
string responseContent = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<T>(responseContent)
}
#endregion
}
Conclusion
In this article, I demonstrated a real-world use case for managing token expiration using the Polly library.
When the application starts, it doesn’t have a token in the cache and makes an authentication API call to retrieve one. Subsequent requests reuse the token until it expires or is invalidated by the provider. At that point, the policy detects a 401 Unauthorized
, clears the cache, fetches a new token, and retries the failed request.
This approach ensures a clean and linear execution without requiring additional error-handling code. The policy also handles timeouts and server errors (e.g., 500
), making the application more resilient.
Polly can also be configured to increase the wait time between retries (not covered here but detailed in the official documentation).
I hope you found this article helpful. See you next time!