I came across this old thread. The problem I had was that I know to use a static HttpClient, but my token needs refreshing every 59 minutes.
So I could have used HttpClientFactory, but because one of my projects was still in .NET 4.8, I created a class that inherited from HttpClient so I have similar code in all projects. A secret is needed to be able to get the token (I'm using identityserver4).
I then set that as a singleton in DI (I'm using Ninject here):
Bind<MyHttpClient>().ToMethod(c => { var accessKey = ConfigurationManager.AppSettings["AccessKey"]; var client = new MyHttpClient(accessKey) { BaseAddress = new Uri(MyUrls.MyApiBaseUrl) }; client.DefaultRequestHeaders.Accept.Clear(); client.DefaultRequestHeaders.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json")); return client; }).InSingletonScope();
Then the class itself - named after the API it is used to access:
public class MyHttpClient : BaseHttpClient { private static readonly HttpClient _authHttpClient = new HttpClient(); private string _secret; public MyHttpClient(string secret) { _secret = secret; } /// <summary> /// Add the token to each and every request, cached for 1 minute less than the token's lifetime /// </summary> /// <param name="request"></param> /// <param name="cancellationToken"></param> /// <returns></returns> /// <exception cref="Exception"></exception> public override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); var cacheSeconds = 3600 - 60; // Default of 59 minutes var token = CacheHelper<string>.Get("MyToken", cacheSeconds * 60, () => { var authorityUrl = MyUrls.AuthServerUrl; // discover endpoints from metadata DiscoveryDocumentResponse disco; disco = _authHttpClient.GetDiscoveryDocumentAsync(authorityUrl).Result; if (disco.IsError) { throw new Exception("Error getting discovery document: " + disco.Error); } // request token var tokenResponse = _authHttpClient.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest { Address = disco.TokenEndpoint, ClientId = "myapp", ClientSecret = _secret, Scope = "myapi" }).Result; if (tokenResponse.IsError) { throw new Exception("Error getting token: " + tokenResponse.Error); } if (tokenResponse.ExpiresIn < cacheSeconds + 60) { throw new Exception($"Token expires in {tokenResponse.ExpiresIn}s, which is less than {cacheSeconds + 60}"); } if (tokenResponse.ExpiresIn > cacheSeconds + 60) { Log.Warn().Message($"Token expiry in {tokenResponse.ExpiresIn}s, which is greater than {cacheSeconds}").Write(); } return tokenResponse.AccessToken; }); // THIS IS THE BIT - Assign this inside a SendAsync override and you are done! request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); return base.SendAsync(request, cancellationToken); } }
Finally just for completeness, my CacheHelper class looks like this:
public static class CacheHelper<T> { private static readonly object _locker = new object(); public static T Get(string cacheName, int cacheTimeoutSeconds, Func<T> func) { var obj = MemoryCache.Default.Get(cacheName, null); if (obj != null) return (T)obj; lock (_locker) { obj = MemoryCache.Default.Get(cacheName, null); if (obj == null) { obj = func(); var cip = new CacheItemPolicy { AbsoluteExpiration = new DateTimeOffset(DateTime.UtcNow.AddSeconds(cacheTimeoutSeconds)) }; MemoryCache.Default.Set(cacheName, obj, cip); } } return (T)obj; } }