Automatically Refreshing Auth Tokens in .NET.

March 2, 2022 - 7 minutes to read

This post shows a clean and unobtrusive way to send auth headers when working with HttpClient. I will show how to add access tokens to the headers of your requests without cluttering your client code and how to refresh the access token on expiry automatically.

What is an access token?

When using an auth mechanism such as OAuth2, the auth server will issue both an access token and a refresh token on login. When a client application needs to access a secured resource on behalf of the user, the access token is sent as part of the request to show that it has been granted access by the user to the resource. You can think of it as a key to your front door.

This token is therefore called a bearer token. Anyone bearing this token can access the secured resources on behalf of the original user, i.e. they can unlock your front door. For this reason, it’s vital to mitigate any security issues if this token was to be obtained by a third party.

One way to increase security is to give the token a short expiry time, usually in the range of minutes or hours. Think of this as your house key self-destructing an hour after it was cut for you by a locksmith. If your key is stolen, there’s a much smaller chance of someone finding your home and using it in time. While this is great from a security point of view, it’s not very convenient for you to visit the locksmith every hour.

Refresh tokens to the rescue

A refresh token is a credential that allows the application to obtain a new access token without forcing the user to log in again. The app can continue to use this refresh token repeatedly for as long as it is valid. Refresh tokens typically have a much longer lifespan, sometimes with no expiry at all.

Refresh tokens are very powerful and must be kept safe. This is why several OAuth flows exist, some of which issue refresh tokens and some which don’t. There’s no easy way of securely storing a refresh token within a browser, so they are usually reserved for flows in which a backend server is involved, where the token can be securely cached.

In this post, we will be looking at how to automatically refresh the access token when it has expired, using the stored refresh token.

Setting up the HttpClient

To begin with, we will register an IHttpClientFactory by calling AddHttpClient. This will allow us to resolve an HttpClient from the dependency injection container when required. See the HttpClientFactory documents if you are not familiar with this pattern.

In a more complex application, you will likely require more advanced configurations for your HTTP clients and named or typed clients may be more appropriate. The approach demonstrated here will work for all registrations.

var builder = WebApplication.CreateBuilder(args);
builder.services.AddHttpClient();
...

We can now resolve an HttpClient from the DI container. The runtime will manage the pooling and lifetime of the underlying HttpClientMessageHandlers instances. The next step is to add our message handler to deal with auth headers.

Adding an Auth Header Handler

HttpClient has the concept of a pipeline, which will be a familiar concept to anyone who has worked with middleware in ASP.NET Core. These handlers can manage a range of crosscutting concerns such as caching, logging, error handling and, in this case, authorisation.

Let’s first assume that you have a way of obtaining the access and refresh tokens for a specific user, such as querying your application’s cache. I’d imagine the interface to this would be something like:

public interface ITokenProvider
{
    Task<string> GetAccessTokenAsync(string userId);
    Task<string> RefreshTokensAsync(string userId);
}

We can now create a DelegatingHandler, one step in the pipeline.

public class TokenAuthHeaderHandler: DelegatingHandler
{
    private readonly ITokenProvider _tokenProvider;    

    public TokenAuthHeaderHandler(ITokenProvider tokenProvider) => _tokenProvider = tokenProvider;

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 
    {
        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await _tokenProvider.GetAccessTokenAsync("myUserId"));
        return await base.SendAsync(request, cancellationToken);
    }
}

This handler will intercept every request made from the HttpClient, retrieve the access token for the user myUserId and then add it to the request headers. This allows us to make requests without worrying about the crosscutting auth concern across our application. Let’s go back and update Program.cs to register this handler with our pipeline.

var builder = WebApplication.CreateBuilder(args);
builder.services
    .AddHttpClient()
    .AddHttpMessageHandler<TokenAuthHeaderHandler>();
...

Refreshing tokens on expiry

Eventually, this token will expire, and our request will fail. In some cases, it can make sense to pre-emptively refresh the token using the expiry timestamp, for example, in an application where the same token is used a high number of times. In other applications, accessing resources on behalf of the user might happen days apart, so continually refreshing the token before expiry is wasteful. It’s good to handle failed calls due to expired tokens in either design, so we will do this next.

We will be using Polly, a .NET resilience and transient-fault-handling library, to help us with this. If you haven’t used Polly before, I highly recommend checking out their documentation. They have many beneficial policies for ensuring your systems are highly available and scalable.

To refresh tokens, we will be using their Retry policy to capture Unauthorised (401) and Forbidden (403) error code responses. Check which error codes the API you are working with returns when your access token has expired, and alter accordingly.

Update the TokenAuthHeaderHandler above with the following.

public class TokenAuthHeaderHandler: DelegatingHandler
{
    private readonly IAppAccessTokenProvider _tokenProvider;
    private readonly AsyncRetryPolicy<HttpResponseMessage> _policy;

    public TokenAuthHeaderHandler(IAppAccessTokenProvider tokenProvider)
    {
        _tokenProvider = tokenProvider;

        _policy = Polly.Policy
            .HandleResult<HttpResponseMessage>(r => r.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden)
            .RetryAsync((_, _) => tokenProvider.RefreshTokensAsync());
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 
        => await _policy.ExecuteAsync(async () =>
        {
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await _tokenProvider.GetAccessTokenAsync("myUserId"));
            return await base.SendAsync(request, cancellationToken);
        });
}

We have now wrapped our code inside SendAsync within a Polly execution policy and defined this policy in the constructor. The policy states that when this action returns an HttpResponseMessage with a 401 or 403 status code, the policy will refresh the tokens using the token provider and then retry the call. GetAccessTokenAsync will yield the newly refreshed token the second time around, and our call will succeed.

Passing the User Id to the pipeline

We now have a working pipeline for our HttpClient, which automatically adds tokens to our headers and refreshes them when required. At the same time, the main application code using the HttpClient remains blissfully unaware. However, you will have noticed that we have used myUserId as the user’s identifier so far.

There are several possible ways to surface the real id within the DelegatingHandler, and which one you choose depends on the architecture of your solution. If you are making this call while handling a web request into your app, you could inject the HttpContext and read the user’s identity from it. Another option is to add the id to the RequestOptions dictionary, attached to the HttpRequestMessage provided to the SendAsync method.

Whichever method you choose to go with, you need to provide the user id to the Polly policy so that Polly can access it as part of the retry execution. To do this, create a dictionary to hold the id, pass it to _policy.ExecuteAsync and then read from it during the retry.

public class TokenAuthHeaderHandler: DelegatingHandler
{
    private readonly IAppAccessTokenProvider _tokenProvider;
    private readonly AsyncRetryPolicy<HttpResponseMessage> _policy;

    public TokenAuthHeaderHandler(IAppAccessTokenProvider tokenProvider)
    {
           _tokenProvider = tokenProvider;
           
           _policy = Polly.Policy
               .HandleResult<HttpResponseMessage>(r => r.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden)
               .RetryAsync((_, _, context) => tokenProvider.RefreshTokensAsync(context["UserId"].ToString()));       
            });
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var myRealUserId = // <The Relevant ID>
        var pollyContext = new Dictionary<string, object> { { "UserId", myRealUserId } };

        return await _policy.ExecuteAsync(async _ =>
        {
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await _tokenProvider.GetAccessTokenAsync(myRealUserId));
            return await base.SendAsync(request, cancellationToken);
        }, pollyContext);
    }
}

Summary

With HTTP APIs being so prevalent in today’s software ecosystem, the web application you are building will likely need to contact another API at some point. We must integrate with protocols such as OAuth correctly while keeping the crosscutting code in the background and not littered through business logic.

Using DelegatingHandlers to create pipelines for outgoing HTTP calls is a powerful technique to achieve this and is often underused. Hopefully, you can start to imagine other stages that you can add to this pipeline, such as telemetry, logging and failure handling.

I would also urge you to read more about the Polly library if you are not familiar with it and the common problems that it can solve. It has a lot of fantastic integration points with ASP.NET Core and is recommended by Microsoft as the go-to library for retries, circuit-breakers and other policies that are so important now in this era of web-based applications and distributed systems.