Managing JWT Tokens with Polly

header manage token with polly

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:

  1. The token did not include any information about its expiration.
  2. The expiration was determined by the provider, who offered an endpoint to check if the token was still valid.
  3. 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:

    process workflow

    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:

    C#
    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:

    C#
    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:

    C#
    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)

    api call workflow

    To optimize resource usage, I used the IClientFactory interface, which was injected directly into the constructor.

    C#
     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):

    C#
    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:

    C#
    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:

    C#
    //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 of HttpClient.
    • 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:

    C#
        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!

      Share this article
      Shareable URL
      Prev Post

      .NET 9 Hybrid Cache

      Next Post

      Store passwords in a database

      Leave a Reply

      Your email address will not be published. Required fields are marked *

      Read next

      Discovering Span<T>

      With .NET Core 2.1 and C# 7.2, a new type of struct was introduced: Span<T> and ReadOnlySpan<T>.…

      .NET 9 Hybrid Cache

      In this article, we’ll delve into one of Microsoft’s latest innovations, currently in preview at the time of…

      HashSet in .NET 9

      What is a HashSet<T>? In .NET, a HashSet<T> is a collection that implements an unordered set of unique…

      LINQ Extension Method

      At the 2024 edition of Overnet’s WPC conference, I attended an insightful talk about LINQ extension…