This is a thread-safe implementation of a DelegatingHandler
, available as Nuget package, that automatically refreshes the access token when the access token expires whilst not serializing all requests. Most implementations use a lock internally which essentially makes all async actions synchronous again. This implementation only blocks during the actual refresh. Inspired by Bryan Helms' Thread-Safe Auth Token Store Using ConcurrentDictionary and AsyncLazy.
var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<TokenOptions>(configuration.GetRequiredSection("MyClient"))
.AddTransient<TokenDelegatingHandler>()
.AddHttpClient<ITokenAuthenticationService, TokenAuthenticationService>()
.ConfigureHttpClient(c => c.BaseAddress = new Uri("http://auth.myservice.com")).Services
builder.Services.AddHttpClient<MyService>()
.ConfigureHttpClient(c => c.BaseAddress = new Uri("http://myservice.com"))
.AddHttpMessageHandler<TokenDelegatingHandler>();
public class MyService(HttpClient client)
{
public Task<IEnumerable<MyFoo>> GetFoos() => client.GetFromJsonAsync<IEnumerable<Foo>>("/api/v1/foos");
}
...or using Refit:
// ...or Refit:
builder.Services.AddRefitClient<IMyService>()
.ConfigureHttpClient(c => c.BaseAddress = new Uri("http://myservice.com"))
.AddHttpMessageHandler<TokenDelegatingHandler>();
public interface IMyService
{
[Get("/api/v1/foos")]
Task<IEnumerable<Foo>> GetFooss();
}
Adding Polly to the mix:
builder.Services.AddHttpClient<MyService>()
.ConfigureHttpClient(c => c.BaseAddress = new Uri("http://myservice.com"))
.AddHttpMessageHandler<TokenDelegatingHandler>();
.AddPolicyHandler(HttpPolicyExtensions
.HandleTransientHttpError()
.WaitAndRetryAsync(Backoff.DecorrelatedJitterBackoffV2(TimeSpan.FromSeconds(1), 3))
).Services
{
"MyClient": {
"ClientId": "myclient",
"ClientSecret": "clientsecretclientsecretclientsecret",
"TokenEndpoint": "http://auth.myservice.com/realms/myapi/protocol/openid-connect/token"
}
}
{
"MyClient": {
"ClientId": "myclient",
"ClientSecret": "clientsecretclientsecretclientsecret",
"Username": "admin",
"Password": "mysup3rs4f3p455w0rd",
"TokenEndpoint": "http://auth.myservice.com/realms/myapi/protocol/openid-connect/token"
}
}
Icon by Freepik