Обновление токенов и MicrosoftIdentityWebApp

В настоящее время мы используем подход AddMicrosoftIdentityWebApp для включения аутентификации с помощью Azure AD B2C. В частности, мы используем следующий код:

services.AddAuthentication()
.AddCookie(FRONTEND_COOKIE_SCHEME) // special cookie due to having a separate back-end auth protocol
.AddMicrosoftIdentityWebApp(options =>
{  
    // config info removed to be concise
    options.SignInScheme = FRONTEND_COOKIE_SCHEME;
    options.SignOutScheme = FRONTEND_COOKIE_SCHEME;
    options.UseTokenLifetime = true;
    options.SaveTokens = true;
    options.TokenValidationParameters = new TokenValidationParameters
    {
        NameClaimType = ClaimTypes.NameIdentifier
    };
})
.EnableTokenAcquisitionToCallDownstreamApi()
.AddInMemoryTokenCaches();

Мы хотим реализовать процесс обновления токена (ссылка: https://learn.microsoft.com/en-us/azure/active-directory-b2c/authorization-code-flow). Я могу подтвердить, что конечная точка .well-known указывает на то, что эта функция включена:

    "response_types_supported": [
        "code",
        "code id_token",
        "code token",
        "code id_token token",
        "id_token",
        "id_token token",
        "token",
        "token id_token"
    ],

Я также могу подтвердить, что запускается шаг 1 этого процесса — при достижении конечной точки аутентификации я перенаправляюсь на oauth2/v2.0/authorize?client_id = {clientId}&redirect_uri = {redirectUri}&response_type=code&scope=openid%20profile%20offline_access&code_challenge = {codeChallenge}&code_challenge_method=S256&response_mode=form_post&nonce = {nonce}&state = {state}&ui_locales=en. Однако, глядя на полученный ответ на шаге OnTokenValidated, я замечаю, что не вижу code. Я решил, что это, вероятно, связано с тем, что code вместо этого используется на этапе OnAuthorizationCodeReceived, и собирался начать реализацию процесса получения токена обновления там.

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

Мой вопрос, в частности, заключается в том, нужно ли нам, как потребителям процесса MicrosoftIdentityWebApp, на самом деле явно вызывать процесс Refresh Token, или это что-то, что происходит изначально, и в этом случае, возможно, нам просто нужно включить параметр конфигурации. ?

ОБНОВЛЕНИЕ 1

Итак, я немного покопался и обнаружил, что MicrosoftIdentityWebApp действительно обрабатывает процесс обмена токенами. Однако теперь я столкнулся с двумя новыми проблемами:

  1. Чтобы получить access_token как часть рабочего процесса по умолчанию, мне нужно было изменить response_type и scope, в частности, чтобы перейти от response_type=code&scope=openid profile offline_access к response_type=code id_token token&scope=openid profile offline_access {clientId} (не уверен, нужно ли мне специально добавлять id_token, но решил, что это не повредит). Однако установка этих параметров во время настройки AddMicrosoftIdentityWebApp, в частности путем установки options.ResponseType и options.Scope, похоже, вообще ничего не дала - при отправке запроса он по-прежнему по умолчанию выполнял только то, что было настроено изначально. Чтобы это заработало, мне нужно было перехватить действие OnRedirectToIdentityProvider и добавить его напрямую. Есть идеи, почему? Меня это не особо беспокоит, просто любопытно, почему это не работает на уровне конфигурации.

  2. Несмотря на добавление token к ResponseType, я, похоже, не получаю refresh_token, как указано в документации (https://learn.microsoft.com/en-us/azure/active-directory-b2c/authorization-code- поток №2-получить токен доступа). Глядя на сам запрос токена в действии OnAuthorizationCodeReceived, я вижу, что выполняемый запрос выглядит корректно, с одной странной странностью — несмотря на то, что offline_access было включено ранее, свойство scope, похоже, здесь не установлено. Не уверены, что именно это является причиной проблемы?

  1. Я попробовал удалить id_token из списка ResponseType и оставить только code token - как ни странно, когда я это делаю, я больше не получаю обратно access_token, а id_token все равно получаю.
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
0
77
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Итак, в конце концов мы решили эту проблему, используя следующий подход:

  1. В OnRedirectToIdentityProvider для решения проблем, перечисленных в пункте 1 обновления, мы добавили следующий код. Я до сих пор не совсем понимаю, зачем нам это нужно было, но, похоже, другого способа сделать правильный 302-редирект на authorize не было.
var defaultPolicy = _configuration.GetValue<string>($"{_policySectionName}");
if (!context.Properties.Items.TryGetValue("policy", out var policy) ||
    policy.Equals(defaultPolicy))
{
    var clientId = _configuration.GetValue<string>($"{_clientIdSectionName}");
    context.ProtocolMessage.ResponseType = OpenIdConnectResponseType.CodeIdTokenToken;
    context.ProtocolMessage.Scope += $" {clientId}";
}
  1. В OnAuthorizationCodeReceived мы добавили следующий код, чтобы получить refresh_token:
var data = new Dictionary<string, string>()
        {
            { "grant_type", _authorizationCodeTypeName},
            { "client_id", context.TokenEndpointRequest.ClientId },
            { "client_secret", context.TokenEndpointRequest.ClientSecret },
            { "code", context.TokenEndpointRequest.Code },
            { "redirect_uri", context.TokenEndpointRequest.RedirectUri },
            { "code_verifier", context.TokenEndpointRequest.Parameters["code_verifier"] }
        };

var instance = _configuration.GetValue<string>($"{_instanceSectionName}");
var domain = _configuration.GetValue<string>($"{_domainSectionName}");
var policy = _configuration.GetValue<string>($"{_policySectionName}");
var tokenUrl = $"{instance}/{domain}/{policy}/{_tokenPath}";

var refreshTokenRequest = new HttpRequestMessage(HttpMethod.Post, tokenUrl)
{
    Content = new FormUrlEncodedContent(data)
};

var response = await _client.SendAsync(refreshTokenRequest);

if (response.IsSuccessStatusCode)
{
    var retVal = await response.Content.ReadFromJsonAsync<RefreshTokenResponse>();

    if (retVal?.AccessToken != null && retVal?.RefreshToken != null && retVal?.IdToken != null)
    {
        identity.AddClaims(new List<Claim> {
            new(_accessTokenClaimName, retVal.AccessToken),
            new(_refreshTokenClaimName, retVal.RefreshToken)
        });

        if (context.Properties != null)
        {
            var accessToken = new JwtSecurityToken(retVal.AccessToken);

            context.Properties.IsPersistent = true;
            context.Properties.ExpiresUtc = accessToken.ValidTo;
        }

        context.HandleCodeRedemption(retVal.AccessToken, retVal.IdToken);
    }
}
  1. В пользовательском ActionFilterAttribute мы добавили следующий код для обновления токена:
var accessTokenClaim = claimsIdentity.FindFirst(Constants.Authentication.AccessTokenClaimName);

if (accessTokenClaim != null)
{
    var jwt = new JwtSecurityToken(accessTokenClaim.Value);

    if (jwt.ValidTo < DateTime.UtcNow)
    {
        var refreshTokenClaim = claimsIdentity.FindFirst(Constants.Authentication.RefreshTokenClaimName);

        if (refreshTokenClaim != null)
        {
            var configuration = _configuration.Value;

            var data = new Dictionary<string, string>()
                    {
                        { "grant_type", Constants.Authentication.RefreshTokenTypeName},
                        { "client_id", configuration.GetValue<string>($"{Constants.Authentication.AzureAdB2cSectionName}:{Constants.Authentication.ClientIdSectionName}") },
                        { "client_secret", configuration.GetValue<string>($"{Constants.Authentication.AzureAdB2cSectionName}:{Constants.Authentication.ClientSecretSectionName}") },
                        { Constants.Authentication.RefreshTokenTypeName, refreshTokenClaim.Value }
                    };

            var instance = configuration.GetValue<string>($"{Constants.Authentication.AzureAdB2cSectionName}:{Constants.Authentication.InstanceSectionName}");
            var domain = configuration.GetValue<string>($"{Constants.Authentication.AzureAdB2cSectionName}:{Constants.Authentication.DomainSectionName}");
            var policy = configuration.GetValue<string>($"{Constants.Authentication.AzureAdB2cSectionName}:{Constants.Authentication.SignUpSignInPolicyIdSectionName}");
            var tokenUrl = $"{instance}/{domain}/{policy}/{Constants.Authentication.TokenEndpointPath}";

            var refreshTokenRequest = new HttpRequestMessage(HttpMethod.Post, tokenUrl)
            {
                Content = new FormUrlEncodedContent(data)
            };

            try
            {
                var response = await _client.SendAsync(refreshTokenRequest);

                if (response.IsSuccessStatusCode)
                {
                    var retVal = await response.Content.ReadFromJsonAsync<RefreshTokenResponse>();

                    if (retVal?.AccessToken != null && retVal?.RefreshToken != null)
                    {
                        claimsIdentity.RemoveClaim(accessTokenClaim);
                        claimsIdentity.RemoveClaim(refreshTokenClaim);
                        claimsIdentity.AddClaim(new(Constants.Authentication.AccessTokenClaimName, retVal.AccessToken));
                        claimsIdentity.AddClaim(new(Constants.Authentication.RefreshTokenClaimName, retVal.RefreshToken));
                    }
                    else
                        context.Result = logoutRedirect;
                }
                else
                    context.Result = logoutRedirect;
            }
            catch (Exception)
            {
                context.Result = logoutRedirect;
            }
        }
        else
            context.Result = logoutRedirect;
    }
}

ОБНОВЛЯТЬ

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

  1. Что касается токена обновления, вместо того, чтобы хранить его как утверждение, мы сохраняем токен обновления в виде файла cookie. Всякий раз, когда пользователь просматривает сайт, мы проверяем, аутентифицирован ли его IPrincipal, и, если да, мы проверяем, не истек ли срок действия его токена доступа. Если да, мы вызываем конечную точку /token, используя токен обновления, и получаем новый токен обновления и токен доступа.
  2. Изначально IPrincipal выходит из системы из-за неактивности. Когда это происходит, если есть токен обновления, мы удаляем токен обновления и принудительно выходим из системы, чтобы очистить все остальные оставшиеся части.

Другие вопросы по теме