Pedro Fernandes Filho
Pedro Fernandes Filho

Reputation: 539

How to store and manage tokens in Client Credential Flow between APIs?

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

Answers (1)

Neville Nazerane
Neville Nazerane

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

Related Questions