Effortless Token Refresh with Refit in C#: A Step-by-Step Guide

mohyusufz
3 min readJan 4, 2025

--

generated by DALL-E

Introduction

When working with REST APIs in C#, managing JWT authentication tokens can become a challenge, especially when the access token expires during concurrent API calls. This article demonstrates how to implement a seamless token refresh mechanism using Refit, a lightweight HTTP client library for .NET. We’ll handle token expiration elegantly, even in concurrent scenarios, and ensure retry logic is robust.

Why Use Refit?

Refit simplifies API consumption by allowing you to define interfaces for your endpoints. It abstracts away the complexity of HTTP client setup and lets you focus on business logic. Combining Refit with an effective token refresh strategy ensures a clean and maintainable codebase.

Problem: Token Expiration and Concurrent API Calls

When multiple API calls are made simultaneously using an expired access token, refreshing the token once and ensuring all calls are retried is crucial. Without proper concurrency management, redundant refresh calls may occur, leading to inconsistent behavior.

Solution: Token Refresh with Concurrency Handling

We’ll use a custom AuthHeaderHandler to handle token expiration and refresh logic. The handler will integrate with an IAuthService interface generated using Refit, making the solution modular and testable.

Step 1: Define IAuthService with Refit

public interface IAuthService
{
[Post("/auth/refresh")]
Task<TokenResponse> RefreshAccessTokenAsync([Body(BodySerializationMethod.UrlEncoded)] Dictionary<string, string> request);
}

This interface defines the API endpoint for refreshing the token.

Step 2: Implement the AuthHeaderHandler

public sealed class AuthHeaderHandler : DelegatingHandler
{
private readonly IAuthService _authService;
private static readonly SemaphoreSlim _refreshLock = new(1, 1);
private static bool _isTokenRefreshing = false;

public AuthHeaderHandler(IAuthService authService)
{
_authService = authService ?? throw new ArgumentNullException(nameof(authService));
}

protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
if (!string.IsNullOrEmpty(TokenStore.AccessToken))
{
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", TokenStore.AccessToken);
}

var response = await base.SendAsync(request, cancellationToken);

if (response.StatusCode == HttpStatusCode.Unauthorized)
{
await _refreshLock.WaitAsync(cancellationToken);
try
{
if (!_isTokenRefreshing)
{
_isTokenRefreshing = true;

var refreshRequest = new Dictionary<string, string>
{
{ "refresh_token", TokenStore.RefreshToken }
};

var tokenResponse = await _authService.RefreshAccessTokenAsync(refreshRequest);
TokenStore.AccessToken = tokenResponse.AccessToken;
TokenStore.RefreshToken = tokenResponse.RefreshToken;
}
}
finally
{
_isTokenRefreshing = false;
_refreshLock.Release();
}

request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", TokenStore.AccessToken);
response = await base.SendAsync(request, cancellationToken);
}

return response;
}
}

The handler:

  • Attaches the access token to requests.
  • Refreshes the token if the server responds with 401 Unauthorized.
  • Ensures only one refresh operation occurs at a time using SemaphoreSlim.

Step 3: Configure ConsumerBuilder

internal static class ConsumerBuilder
{
internal static T Create<T>() where T : class
{
// Create a dedicated HttpClient for IAuthService
var authHttpClient = new HttpClient
{
BaseAddress = new Uri(ConsumerConfiguration.Instance.EntryPoint),
Timeout = TimeSpan.FromMilliseconds(ConsumerConfiguration.Instance.MiliSecondTimeOut)
};

var authService = RestService.For<IAuthService>(authHttpClient);

var httpClient = new HttpClient(new AuthHeaderHandler(authService)
{
InnerHandler = new HttpClientHandler()
})
{
BaseAddress = new Uri(ConsumerConfiguration.Instance.EntryPoint),
Timeout = TimeSpan.FromMilliseconds(ConsumerConfiguration.Instance.MiliSecondTimeOut)
};

var settings = new RefitSettings
{
ContentSerializer = new NewtonsoftJsonContentSerializer(
new JsonSerializerSettings
{
ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver()
})
};

return RestService.For<T>(httpClient, settings);
}
}

Step 4: Token Storage and Model

Token Store

public static class TokenStore
{
public static string AccessToken { get; set; }
public static string RefreshToken { get; set; }
}

Token Response Model

public class TokenResponse
{
[JsonProperty("access_token")]
public string AccessToken { get; set; }

[JsonProperty("refresh_token")]
public string RefreshToken { get; set; }
}

Step 5: How It Works

  1. Attach Token: The AuthHeaderHandler attaches the access token to each request.
  2. Refresh Token: When a 401 Unauthorized response is received, the handler triggers the token refresh logic.
  3. Concurrency Management: Using SemaphoreSlim, only one thread refreshes the token while others wait.
  4. Retry Logic: After refreshing the token, the handler retries the original request.

Benefits of This Approach

  1. Clean Architecture: Encapsulates authentication logic in AuthHeaderHandler and IAuthService.
  2. Concurrency Safe: Manages token refresh efficiently in concurrent scenarios.
  3. Scalable: Works seamlessly for multiple Refit clients.
  4. Testable: Both the handler and the IAuthService can be mocked for unit testing.

Conclusion

With this approach, you can handle token expiration in a clean and efficient manner while leveraging the power of Refit. Whether you’re building a small-scale project or a large enterprise solution, this pattern ensures robustness and scalability.

Have thoughts or questions? Let’s discuss in the comments below!

--

--

mohyusufz
mohyusufz

Written by mohyusufz

Writing out of curiosity, one post at a time. Exploring knowledge and embracing lifelong growth.

No responses yet