Несколько запросов для одновременного обновления токена доступа в OIDC - PullRequest
1 голос
/ 28 июня 2019

У меня есть немного проблем с обновлением токенов обновления в определенной ситуации, когда приложение на одной странице выполняет несколько вызовов API одновременно. У меня есть SPA, который имеет стек, который состоит из следующего.

Html / JS SPA -> Приложение MVC -> WebAPI

Я использую гибридный поток, когда пользователь входит на страницу, где я сохраняю id_token, access_token и refresh_token в файле cookie сеанса.

Я использую HttpClient, который имеет два DelegatingHandlers, чтобы общаться с веб-API. Один из делегирующих обработчиков просто добавляет токен доступа в заголовок авторизации. Другой выполняется до этого и проверяет время жизни, оставшееся на токене доступа. Если токену доступа осталось ограниченное время, то refresh_token используется для получения новых учетных данных и их сохранения в моем сеансе.

Вот код для 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);
    }
}

Проблема в том, что многие звонки достигают этого примерно в одно и то же время. Все эти вызовы будут одновременно попадать в конечную точку обновления. Все они получат новые действительные токены доступа, и приложение продолжит работу. Однако, если 3 запроса происходят одновременно, будут созданы три новых токена обновления, и только один из них будет действительным. Из-за асинхронного характера приложения я не могу гарантировать, что токен обновления, сохраненный в моем сеансе, является фактически последним токеном обновления. В следующий раз, когда мне понадобится обновить токен обновления, он может быть недействительным (и часто это так).

Мои мысли о возможных решениях до сих пор.

  • Блокировка в точке проверки токена доступа с помощью мьютекса или подобного. Однако он может блокироваться, когда он используется другим пользователем с другим сеансом (насколько мне известно). Это также не работает, если мое приложение MVC установлено в нескольких экземплярах.

  • Измените, чтобы токены обновления оставались действительными после использования. Так что не имеет значения, кто из трех привыкнет.

Любые мысли о том, что из вышеперечисленного лучше или у кого-нибудь есть действительно умная альтернатива.

Большое спасибо!

1 Ответ

0 голосов
/ 28 июня 2019

Когда все ваши запросы поступают из одного и того же SPA, лучше всего синхронизировать их в браузере и избавиться от проблемной стороны сервера.Каждый раз, когда ваш код клиента требует токен, возвращайте обещание.Один и тот же экземпляр обещания для всех запросов, поэтому все они разрешаются с помощью единственного запроса к серверу.

К сожалению, если вы проксируете все запросы через локальный API и никогда не передадите свой носитель в SPA, моя идея не будетне работает.

Но если вы сохраняете свой токен обновления абсолютно безопасным (никогда не отправляйте его на фронт), я не вижу никаких проблем, чтобы сделать его многоразовым.В этом случае вы можете включить скользящую опцию , как отлично описано здесь , чтобы выполнять меньше запросов на обновление.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...