通过 OIDC 连接到 Azure AD 或 AWS Cognito 时如何透明地使用刷新令牌?

问题描述 投票:0回答:1

我正在开发的应用程序由两个姐妹 Web 应用程序组成,每个应用程序由前端 Razor Web 应用程序和 Minimal API 后端组成。 第一个使用 Azure AD 对公司员工进行身份验证。第二个使用 AWS Cognito 用户池对客户进行身份验证。

两个 Web 应用程序都正确建立与其 IdP 的连接,并使用令牌向各自的后端应用程序验证自己的身份。

我面临的问题是,令牌最终会过期,而身份验证 cookie 仍然有效,所以我看到我仍然在网站上进行了身份验证,但我转发到后端的令牌已过期。

我希望系统使用

refresh_token
自动获取新令牌,并且我使用
CookieAuthenticationOptions
OnValidatePrincipal
事件来挂钩我的代码。

这是我到目前为止得到的:

我如何设置身份验证工作流程:

builder.Services
    .AddAuthentication(options =>
    {
        options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    })
    .AddCookie(options =>
    {
        options.Events = new()
        {
            OnValidatePrincipal = OnValidatePrincipalAsync
        };
    })
    .AddOpenIdConnect(options =>
    {
        var azureAdOptions = new AzureAdOptions();
        builder.Configuration.GetSection("Authentication").Bind(azureAdOptions);
        builder.Configuration.GetSection("Authentication").Bind(options);
        
        options.Scope.Add("openid");
        options.Scope.Add("profile");
        options.Scope.Add("offline_access");
        
        options.GetClaimsFromUserInfoEndpoint = true;
        options.MapInboundClaims = false;

        options.TokenValidationParameters = new()
        {
            NameClaimType = "name"
        };
    });

OnValidatePrincipalAsync
方法及其依赖项

async Task OnValidatePrincipalAsync(CookieValidatePrincipalContext context)
{
    if (context.Principal?.Identity?.IsAuthenticated ?? false)
    {
        var idToken = context.Properties.GetTokenValue("id_token");
        var jwt = new JwtSecurityToken(idToken);
        
        if (jwt.ValidTo.ToLocalTime() < DateTime.Now)
        {
            var refreshToken = context.Properties.GetTokenValue("refresh_token");

            if (refreshToken is null)
            {
                context.RejectPrincipal();

                return;
            }

            var tokenResponse = await GetFreshTokenAsync(context.HttpContext, refreshToken);

            if (tokenResponse is null)
            {
                context.RejectPrincipal();

                return;
            }

            var expirationValue = DateTime.Now.ToLocalTime().AddSeconds(tokenResponse.ExpiresIn).ToString("o", CultureInfo.InvariantCulture);

            context.Properties.UpdateTokenValue("refresh_token", tokenResponse.RefreshToken);
            context.Properties.UpdateTokenValue("id_token", tokenResponse.IdToken);
            context.Properties.UpdateTokenValue("access_token", tokenResponse.AccessToken);
            context.Properties.UpdateTokenValue("expires_at", expirationValue);

            context.ShouldRenew = true;
        }
    }
}

async Task<TokenResponse?> GetFreshTokenAsync(HttpContext httpContext, string refreshToken)
{
    var serviceProvider = httpContext.RequestServices;
    var cancellationToken = httpContext.RequestAborted;
    
    var oidcOptions = serviceProvider.GetRequiredService<IOptionsSnapshot<OpenIdConnectOptions>>().Get(OpenIdConnectDefaults.AuthenticationScheme);

    var configuration = oidcOptions.Configuration ?? await oidcOptions.ConfigurationManager!.GetConfigurationAsync(cancellationToken).ConfigureAwait(false);
    
    var httpClientFactory = serviceProvider.GetRequiredService<IHttpClientFactory>();

    var address = configuration.TokenEndpoint;
    var clientId = oidcOptions.ClientId!;
    var clientSecret = oidcOptions.ClientSecret!;

    using var httpClient = httpClientFactory.CreateClient();

    var requestBody = new Dictionary<string, string>()
    {
        ["grant_type"] = "refresh_token",
        ["client_id"] = clientId,
        ["refresh_token"] = refreshToken,
        ["scope"] = string.Join(" ", oidcOptions.Scope),
        ["client_secret"] = clientSecret
    };

    using var httpRequest = new HttpRequestMessage(HttpMethod.Post, address);
    httpRequest.Content = new FormUrlEncodedContent(requestBody);

    using var httpResponse = await httpClient.SendAsync(httpRequest, cancellationToken);

    if (!httpResponse.IsSuccessStatusCode)
    {
        return null;
    }
    
    var tokenResponse = await httpResponse.Content.ReadFromJsonAsync<TokenResponse>(cancellationToken);

    return tokenResponse;
}

最后,当我需要调用后端时如何请求令牌:

services.AddHttpClient(AdminApiName, async (sp, http) =>
{
    var context = sp.GetRequiredService<IHttpContextAccessor>().HttpContext!;

    var idToken = await context.GetTokenAsync("id_token");

    http.BaseAddress = configuration.GetServiceUri("admin-api");
    http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", idToken);
});

GetFreshTokenAsync
获得一个新的令牌,但我无法使用它来替换旧方法,因此当控制器/端点请求新的 HttpClient 时,
context.GetTokenAsync("id_token")
调用将返回旧的令牌。由于过期了,后端返回401。

如何解决?

谢谢!

asp.net-core azure-active-directory amazon-cognito openid-connect
1个回答
0
投票

如果在 HttpClient 已获取旧令牌后刷新令牌,则 HttpClient 将不知道刷新的令牌,并将继续使用过时的令牌。

当您在

OnValidatePrincipalAsync
中更新令牌时,您正确设置了
context.ShouldRenew = true;
,它应该使用新的令牌信息更新 cookie。确保在任何 HttpClient 尝试使用令牌之前发生这种情况。

您可以尝试以下代码:

services.AddHttpClient(AdminApiName, async (sp, http) =>
{
    var context = sp.GetRequiredService<IHttpContextAccessor>().HttpContext!;
    var auth = context.RequestServices.GetRequiredService<IAuthenticationService>();
    var authenticateResult = await auth.AuthenticateAsync(context, CookieAuthenticationDefaults.AuthenticationScheme);

    if (!authenticateResult.Succeeded)
    {
        throw new InvalidOperationException("Authentication failed.");
    }

    var idToken = authenticateResult.Properties.GetTokenValue("id_token");
    
    if (TokenNeedsRefresh(authenticateResult.Properties))
    {
        // Manually trigger the OnValidatePrincipalAsync event or abstract the refresh logic into a reusable method
        // You could trigger it by calling context.SignInAsync(authenticateResult.Principal, authenticateResult.Properties);
        // However, you may need to abstract the refresh token logic and call it directly here.
    }

    idToken = authenticateResult.Properties.GetTokenValue("id_token"); // Get the refreshed token

    http.BaseAddress = configuration.GetServiceUri("admin-api");
    http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", idToken);
});

刷新

OnValidatePrincipalAsync
中的令牌后,请确保使用新令牌值更新身份验证 cookie:

    context.Properties.StoreTokens(new List<AuthenticationToken>
    {
        new AuthenticationToken { Name = "id_token", Value = tokenResponse.IdToken },
        new AuthenticationToken { Name = "access_token", Value = tokenResponse.AccessToken },
        new AuthenticationToken { Name = "refresh_token", Value = tokenResponse.RefreshToken }
    });

await context.HttpContext.SignInAsync(context.Principal, context.Properties);

始终检查令牌是否即将过期,不仅是已经过期,还可能在运输或处理过程中过期。

private bool TokenNeedsRefresh(AuthenticationProperties properties)
{
    var expirationValue = properties.GetTokenValue("expires_at");
    if (DateTime.TryParse(expirationValue, out var expiresAt))
    {
        return expiresAt.AddMinutes(-5) < DateTime.UtcNow; // Checking 5 minutes before actual expiration
    }
    return true;
}

考虑创建一个管理令牌生命周期的服务,这样您就有了获取和刷新令牌的单点责任。将此服务注入到您的 HttpClient 配置委托中。

© www.soinside.com 2019 - 2024. All rights reserved.