One of the most common tasks for backend developers is integrating with third party APIs. And most of the time we end up solving the same problems over and over again, like automatically refreshing the auth token, which eventually expires.
Let’s consider the following scenario. There is a third party API at https://denispavlov.net
, which requires authentication. We know that there is a GET /auth
endpoint which returns an access token valid for 30 minutes. And we’re interested in a resource GET /api
, which requires that token to be sent in request header as Authorization: Bearer {token}
.
To refresh the JWT, I never like to rely on token expiration timestamp coming from a third party server, even when it’s available, because it’s not reliable imho (server clocks might be wrong, etc). Instead, I prefer to wait for the first 401 Unauthorized
response to be my indicator that it’s time for token renewal. Let’s consider the following code to accomplish this scenario.
using System.Net;
public class HttpService
{
private const string _authUrl = "https://denispavlov.net/auth";
private readonly HttpClient _client;
private readonly string _authHeader = HttpRequestHeader.Authorization.ToString();
private static string _jwt = string.Empty;
public HttpService(HttpClient client)
{
_client = client;
SetJwt();
}
private void SetJwt()
{
using var request = new HttpRequestMessage(HttpMethod.Get, _authUrl);
using var response = _client.Send(request);
using var stream = response.Content.ReadAsStream();
using var reader = new StreamReader(stream);
_jwt = reader.ReadToEnd();
}
public async Task<string> GetContentAsync(string url)
{
var response = await SendAsync(HttpMethod.Get, url);
if (!response.IsSuccessStatusCode)
{
SetJwt();
response = await SendAsync(HttpMethod.Get, url);
}
return await response.Content.ReadAsStringAsync();
}
private async Task<HttpResponseMessage> SendAsync(HttpMethod method, string url)
{
var request = new HttpRequestMessage(method, url);
request.Headers.Add(_authHeader, $"Bearer {_jwt}");
return await _client.SendAsync(request);
}
}
At the first glance, it looks fine. We inject HttpClient
using HttpClientFactory
Typed Client pattern, we get the access token and store it in a field. When consumer of this class calls GetContentAsync and first response isn’t successful, refresh access token and retry.
This solution will work in a single threaded application where requests come in one at a time. Unfortunately, that is not the reality for most production applications, and this code may have some issues when handling many concurrent requests. The problem is that our token refresh method is not thread safe, therefore you may end up with multiple threads trying to refresh the token simultaneously (potentially making extra unnecessary calls to the third party API, or worse, getting throttled because the GET /auth
may have request rate limit). Let’s consider a better thread-safe solution.
Important Observation #1: The SetJwt
method has been modified to lock
the critical section of code to ensures that only one thread can enter at a time. Hence, avoiding potential simultaneous calls to refresh the token.
Important Observation #2: The two if
statements if (string.IsNullOrEmpty(_jwt))
before and after the lock. This ensures that if there is a race condition where the first thread just executed line 28 and the second thread already entered the first if
statement on line 21, but hasn’t entered the lock
, there is a secondary check to avoid the second renewal.
Important Observation #3: The new private ExecuteWithRetriesAsync
method is a wrapper for all API calls that require access token. It utilizes Polly
library that is now included in .NET to automatically refresh JWT and retry the failed request once. Line 53 has an important if
statement to ensure that we’re clearing out the expired JWT, in case another thread refreshed it.
NOTE: If you anticipate very low traffic on this service, you may move the SetJwt
call from the constructor to the beginning of GetContentAsync
, to avoid a situation where the token expired before you got a chance to use it. Make sure any other methods you want to expose that invoke HttpClient
are wrapped in ExecuteWithRetriesAsync
as done in my GetContentAsync
example.