I found this answer to be woefully incomplete:
Refresh Token using Polly with Typed Client
Same question as in this post, except the accepted answer seems critically flawed. Every time you make a request, you're going to get a 401 error, then you obtain an access token, attach it to the request and try again. It works, but you take an error on every single message.
The only solution I see is to set the default authentication header, but to do that, you need the HttpClient
. So the original answer would need to do something like this:
services.AddHttpClient<TypedClient>()
.AddPolicyHandler((provider, request) =>
{
return Policy.HandleResult<HttpResponseMessage>(r => r.StatusCode == HttpStatusCode.Unauthorized)
.RetryAsync(1, (response, retryCount, context) =>
{
var httpClient = provider.GetRequiredService<HttpClient>();
var authService = provider.GetRequiredService<AuthService>();
httpClient.DefaultRequestHeaders.Authorization = authService.GetAccessToken());
});
});
});
So the rub is, how do you get the current HttpClient
in order to set the default access token so you don't call this handler every time you make a request? We only want to invoke this handler when the token has expired.
CodePudding user response:
As I have stated in the comments I've already created two solutions:
- one which utilizes a custom exception to trigger refresh and retry
- and another which uses Polly's Context to indicate the need for refresh
Both of the solutions are utilizing named clients. So, here I would focus only on that part which needs to be changed to use typed client.
The good news is that you need to change only a tiny part of the solutions.
Rewriting the custom exception based solution
Only the following code needs to be changed from this:
services.AddHttpClient("TestClient")
.AddPolicyHandler((provider, _) => GetTokenRefresher(provider))
.AddHttpMessageHandler<TokenFreshnessHandler>();
to this:
services.AddHttpClient<ITestClient, TestClient>
.AddPolicyHandler((provider, _) => GetTokenRefresher(provider))
.AddHttpMessageHandler<TokenFreshnessHandler>();
The ITestClient
and TestClient
entities are independent from the rest of the solution.
Rewriting the context based solution
Only the following code needs to be changed
services.AddHttpClient("TestClient")
.AddPolicyHandler((sp, request) => GetTokenRefresher(sp, request))
.AddHttpMessageHandler<TokenRetrievalHandler>()
to this:
services.AddHttpClient<ITestClient, TestClient>
.AddPolicyHandler((sp, request) => GetTokenRefresher(sp, request))
.AddHttpMessageHandler<TokenRetrievalHandler>()
So the rub is, how do you get the current
HttpClient
in order to set the default access token so you don't call this handler every time you make a request? We only want to invoke this handler when the token has expired.
With my proposed solution you don't need to access the HttpClient
to set the default header, because the currently active access token is stored in a singleton class (TokenService
). Inside the DelegatingHandler
(TokenFreshnessHandler
/TokenRetrievalHandler
) you retrieve the latest, greatest token and set it on the HttpRequestMessage
.
CodePudding user response:
The answer by Peter Csala is a great start. However, I found that the lifetime of the 'context' was only as long as the HttpRequestMessage, which is basically once per HttpClient function invocation (Put, Post, Get, Delete, Send). That is, it doesn't persist from function call to function call, so we are constantly asking the cache for a token. In MSAL, this function isn't completely trivial and involves a context switch. we should be able to just use a stored string until we get a 401 error, then it's reasonable to ask the MSAL cache for a new token.
I believe this is a more efficient version of the TokenRetrievalService handler:
public class TokenRetrievalHandler : DelegatingHandler
{
private readonly ITokenService tokenService;
public TokenRetrievalHandler(ITokenService tokenService)
{
this.tokenService = tokenService;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", this.tokenService.AccessToken);
return await base.SendAsync(request, cancellationToken);
}
}
and this is a more efficient version of the Token Service:
public class TokenService : ITokenService
{
public string AccessToken { get; set; }
public async Task RefreshAccessTokenAsync()
{
this.AccessToken = await <GetTokenFromPromptorCache>();
}
}
And the token refresher policy then looks much simpler:
private static IAsyncPolicy<HttpResponseMessage> GetTokenRefresher(IServiceProvider serviceProvider, HttpRequestMessage httpRequestMessage)
{
return Policy<HttpResponseMessage>
.HandleResult(response => response.StatusCode == HttpStatusCode.Unauthorized)
.RetryAsync(async (handler, retry) =>
{
await serviceProvider.GetRequiredService<ITokenService>().RefreshAccessTokenAsync();
});
}
Again, props to Peter Csala for the bulk of the work. This is a minor tweak that accomplishes the original goal of reusing the raw version of the token without bothering (for example) the MSAL cache on every call.