4
\$\begingroup\$

I have a .NET 6 web API that does some requests to another third party API, this API has a token that expires after a few hours, so I want to cache this token and reuse in the requests.

Here how I implemented it:

public class TokenInformation
{
    public string Token { get; set; }
    public DateTime ExpiresIn { get; set; }
}
public static class TokenManagement
{
    private static object _lock = new object();
    private static TokenInformation _token;

    // HttpClient already have credentials and other default headers
    public static async Task<string> GetToken(HttpClient httpClient)
    {
        if (_token is null || _token.ExpiresIn <= DateTime.Now)
        {
            var response = await httpClient.GetAsync("/token");
            if (!response.IsSuccessStatusCode)
                throw new Exception($"Authentication Failure: {response.StatusCode}");

            var token = await response.Content.ReadAsAsync<TokenInformation>();

            lock (_lock)
            {
                _token = token;
            }
        }

        return _token.Token;
    }
}
public class EmployeeServiceClient
{
    private HttpClient _httpClient;

    public EmployeeServiceClient(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task<string> GetEmployeeDetails(int employeeId)
    {
        using var message = new HttpRequestMessage(HttpMethod.Get, $"/employee/{employeeId}/details");
        message.Headers.Authorization = 
            new AuthenticationHeaderValue("Bearer", await TokenManagement.GetToken(_httpClient));

        var result = await _httpClient.SendAsync(message);
        return await result.Content.ReadAsStringAsync();
    }
}

My biggest doubt is if I really need the lock since this is an API with multiple threads and requests. I think so, what you guys think, any tips? Also, should yje lock be wider? If so, since contains async calls should I use .Result ?

Obs. Each class is in a separate file, some class names were changed to hide vendor name, everything else is real code.

\$\endgroup\$
6
  • 2
    \$\begingroup\$ You need the lock. And further, you need the lock to be broader - around the if since it accesses _token as well. \$\endgroup\$ Dec 4, 2022 at 1:11
  • 1
    \$\begingroup\$ Have you considered to use a 3rd party in-memory cache like: MemoryCache or LazyCache? \$\endgroup\$ Dec 4, 2022 at 7:06
  • \$\begingroup\$ Thanks @JesseC.Slicer, the problem of a broader lock is that the method contains async calls, do you think I should turn it into sync calls with .Result ? \$\endgroup\$ Dec 4, 2022 at 16:01
  • \$\begingroup\$ Why not use Microsoft's in-memory cache? \$\endgroup\$
    – BCdotWEB
    Dec 5, 2022 at 9:44
  • 2
    \$\begingroup\$ @VanderleiMorais No. .Result is almost always a bad idea. \$\endgroup\$ Dec 5, 2022 at 15:43

1 Answer 1

3
\$\begingroup\$

Generally speaking there are two ways to solve the auto-refresh token problem:

  • Proactive / Preventive / Pessimistic: Try to prevent to use an expired token by asking a new one before calling the secured endpoint
  • Reactive / Mitigative / Optimistic: Calling the secured endpoint with the token and whenever the server indicates token expiry then ask for a new one

Proactive approach

Your solution falls under this category. The token server issues a token and indicates the expiry of it. And your application proactively checks this expiry before making any http calls.

The problem with this preventive approach is that the token server time and your application server time might not be synced. Your application server's system clock might be earlier or later in time compared to the token server's system clock. Let's see what could happen around the token expiry. Let's suppose that the token expires at 10:05. Then depending on the time skew the result is different:

  • The application server thinks it is 10:04, but the secured endpoint's server's time is 10:06
    • Your code will not ask for a new token but the secured endpoint will reject your call since the token is expired
  • The application server thinks it is 10:06, but the secured endpoint's server's time is 10:04
    • Your code will ask a new token even though it was not expired yet

The former case is problematic. This problem can be solved by adding some error-margin to the expiry check (like token.Expiry < Now - 5m). The actual value of the margin depends on the validity length of a token.

(P.S. Please try to prefer UTC kind over local in case of DateTime)

Reactive approach

In this case the application always assumes that the token is valid. It does not do any expiry check. Rather the secured endpoint call is wrapped into a retry handler. If the endpoint call fails because the token was expired (for example the response's status code is 401) then you ask for a new token and issue a new retry attempt with the new token.

If the token is shared and the concurrency is high then it might happen that multiple requests will fail with 401 at the same time and try to get a new token. Either you write a code which ensures that only a single thread is asking for a new token. Or your code uses the last one wins strategy in your cache.

In the later case there is an intermittent period when multiple tokens are used concurrently by your application but eventually it will consolidate into a single one. So, it is wasting resources but still a good approach.

Here I have proposed two solutions for this mitigative technique.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.