Reputation: 539
I don't know how to implement the client part of the Client Credential Flow for two APIs.
Scenario: An API A obtains a JWT token (your own token) from an identity server (keycloak) to access an API B.
Technology: .NET Core with C #
How to store the token in API A to be used in requests for API B? How to manage token expiration so that it is refreshed?
Is it a good option to use a singleton instance of a class that stores the token and uses a Timer to refresh the token? Is it better to have a worker service (background) to manage token expiration?
Are there other ways to store and manage token expiration in this scenario?
Is possible to use IdentityModel
with keycloak?
Upvotes: 1
Views: 1326
Reputation: 7019
You can use a singleton similar to this:
public class TokenStore
{
public string Token { get; set; }
public string RefreshToken { get; set; }
}
Next, you can create a System.Net.Http.DelegatingHandler
to take care of the whole Token process. It would be something like this:
public class MyTokenHandler : DelegatingHandler
{
private readonly TokenStore _tokenStore;
private TaskCompletionSource<object> updateTokenTaskCompletionSource;
public MyTokenHandler(TokenStore tokenStore)
{
_tokenStore = tokenStore;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
// check if token is already being fetched
if (updateTokenTaskCompletionSource != null)
await updateTokenTaskCompletionSource.Task;
var httpResponse = await InternalSendAsync(request, cancellationToken);
if (httpResponse.StatusCode == System.Net.HttpStatusCode.Unauthorized)
// you can add conditions such as excluding Paths and checking response message to avoid recursion
// you can also verify token expiary based on time
{
// intentionally not passing in the refresh token
// at this point we know there is an expired token. So, we can update token regardless of the main request being cancelled
await UpdateTokenAsync();
httpResponse = await InternalSendAsync(request, cancellationToken);
}
return httpResponse;
}
private async Task UpdateTokenAsync(CancellationToken cancellationToken = default)
{
// taskCompletionSource handles multiple requests attempting to refresh token at the same time
if (updateTokenTaskCompletionSource is null)
{
updateTokenTaskCompletionSource = new TaskCompletionSource<object>();
try
{
var refreshRequest = new HttpRequestMessage(HttpMethod.Post, "/token");
var refreshResponse = await base.SendAsync(refreshRequest, cancellationToken);
_tokenStore.Token = "updated token here";
}
catch (Exception e)
{
updateTokenTaskCompletionSource.TrySetException(e);
updateTokenTaskCompletionSource = null;
throw new Exception("Failed fetching token", e);
}
updateTokenTaskCompletionSource.TrySetResult(null);
updateTokenTaskCompletionSource = null;
}
else
await updateTokenTaskCompletionSource.Task;
}
private async Task<HttpResponseMessage> InternalSendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _tokenStore.Token);
return await base.SendAsync(request, cancellationToken);
}
}
Finally, in your startup.cs you can add this code:
services.AddHttpClient<MyClientService>(httpClient =>
{
httpClient.BaseAddress = new Uri("Your base URL");
}).AddHttpMessageHandler<MyTokenHandler>();
Note that I haven't accounted for setting the RefreshToken
. You can set it based on the requirement. Here is the whole sample project: https://github.com/neville-nazerane/httpclient-refresh/
Upvotes: 1