Multiple Requests to Refresh Access Token At the Same Time In OIDC

Viewed 2101

I have a bit of a head-scratcher for updating a refresh tokens in a certain situation with a single page application making multiple api calls at the same time. I have an SPA which has a stack that consists of the following.

Html/JS SPA -> MVC Application -> WebAPI

I make use of the Hybrid flow, when a user logs onto the page I store the id_token the access_token and the refresh_token in the session cookie.

I use a HttpClient which has two DelegatingHandlers to talk to the web API. One of the delegating handlers simply adds the access token to the Authorization header. The other one runs before this and checks the lifetime left on the access token. If the access token has a limited amount of time left the refresh_token is used to get new credentials and save them back to my session.

Here is the code for the OidcTokenRefreshHandler.

public class OidcTokenRefreshHandler : DelegatingHandler
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly OidcTokenRefreshHandlerParams _handlerParams;

    public OidcTokenRefreshHandler(IHttpContextAccessor httpContextAccessor, OidcTokenRefreshHandlerParams handlerParams)
    {
        _httpContextAccessor = httpContextAccessor;
        _handlerParams = handlerParams;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        var accessToken = await _httpContextAccessor.HttpContext.GetTokenAsync("access_token");
        var handler = new JwtSecurityTokenHandler();
        var accessTokenObj = handler.ReadJwtToken(accessToken);
        var expiry = accessTokenObj.ValidTo;

        if (expiry - TimeSpan.FromMinutes(_handlerParams.AccessTokenThresholdTimeInMinutes) < DateTime.UtcNow )
        {
            await RefreshTokenAsync(cancellationToken);
        }

        return await base.SendAsync(request, cancellationToken);
    }

    private async Task RefreshTokenAsync(CancellationToken cancellationToken)
    {
        var client = new HttpClient();

        var discoveryResponse = await client.GetDiscoveryDocumentAsync(_handlerParams.OidcAuthorityUrl, cancellationToken);
        if (discoveryResponse.IsError)
        {
            throw new Exception(discoveryResponse.Error);
        }

        var refreshToken = await _httpContextAccessor.HttpContext.GetTokenAsync(OpenIdConnectParameterNames.RefreshToken);
        var tokenResponse = await client.RequestRefreshTokenAsync(new RefreshTokenRequest
        {
            Address = discoveryResponse.TokenEndpoint,
            ClientId = _handlerParams.OidcClientId,
            ClientSecret = _handlerParams.OidcClientSecret,
            RefreshToken = refreshToken
        }, cancellationToken);
        if (tokenResponse.IsError)
        {
            throw new Exception(tokenResponse.Error);
        }

        var tokens = new List<AuthenticationToken>
        {
            new AuthenticationToken
            {
                Name = OpenIdConnectParameterNames.IdToken,
                Value = tokenResponse.IdentityToken
            },
            new AuthenticationToken
            {
                Name = OpenIdConnectParameterNames.AccessToken,
                Value = tokenResponse.AccessToken
            },
            new AuthenticationToken
            {
                Name = OpenIdConnectParameterNames.RefreshToken,
                Value = tokenResponse.RefreshToken
            }
        };

        // Sign in the user with a new refresh_token and new access_token.
        var info = await _httpContextAccessor.HttpContext.AuthenticateAsync("Cookies");
        info.Properties.StoreTokens(tokens);
        await _httpContextAccessor.HttpContext.SignInAsync("Cookies", info.Principal, info.Properties);
    }
}

The problem is that many calls hit this at roughly the same time. All of these calls will then hit the refresh endpoint at the same time. They will all retrieve new valid access tokens and the application will continue to work. However if 3 requests happen at the same time, three new refresh tokens will be created and only one of these will be valid. Due to the asynchronous nature of the application I have no guarantee that the refresh token stored in my session is actually the latest refresh token. The next time I need to refresh the refresh token may be invalid (and often is).

My thoughts on possible solutions so far.

  • Lock at the point of checking the access token with a Mutex or similar. However this has the potential to block when it is being used by a different user with a different session (to the best of my knowledge). It also doesn't work if my MVC app is across multiple instances.

  • Change so the refresh tokens remain valid after use. So it doesn't matter which one of the three gets used.

Any thoughts on which of the above is better or has anyone got a really clever alternative.

Many Thanks!

1 Answers

When all your requests come from the same SPA, the best should be to sync them in the browser and get rid of the problem serverside. Each time your client code requires a token, return a promise. The same promise instance to all requests, so they all get resolved with the only request to the server.

Unfortunately if you proxy all the requests through your local API and never pass your bearer to the SPA, my idea wouldn't work.

But if you keep your refresh token absolutely secure (never send it to the front), I can't see any problem to make it reusable. In that case you can switch on sliding option as excellently described here to perform less renewal requests.

Related