You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何通过OIDC在Azure AD/AWS Cognito中透明使用Refresh Token?

问题描述

我开发的系统包含两个姊妹Web应用,每个应用由Razor前端和Minimal API后端组成:

  • 第一个应用使用Azure AD认证企业员工
  • 第二个应用使用AWS Cognito用户池认证客户

两个应用均已正确对接各自的身份提供商(IdP),通过令牌向后端做身份验证。现在遇到的问题是:令牌过期后,认证Cookie仍有效,网站显示用户已认证,但转发给后端的令牌已经过期。

我希望通过refresh_token自动获取新令牌,因此在CookieAuthenticationOptions的OnValidatePrincipal事件中注入了自定义逻辑。目前GetFreshTokenAsync能成功获取新令牌,但更新令牌后,控制器/端点请求HttpClient时,context.GetTokenAsync("id_token")仍返回旧令牌,导致后端返回401错误。


认证流程配置

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);
});

解决方案

问题核心在于:OnValidatePrincipal中更新的context.Properties是要写入到响应Cookie中的,但当前请求的HttpContext里的认证属性并没有同步更新,所以当前请求后续调用GetTokenAsync拿到的还是旧令牌。

方案一:同步更新当前请求的认证属性

在OnValidatePrincipalAsync方法中,更新完context.Properties后,手动将新令牌同步到当前HttpContext的认证信息中:

// 更新完context.Properties之后添加以下代码
var authResult = AuthenticateResult.Success(new AuthenticationTicket(context.Principal, context.Properties, context.Scheme.Name));
context.HttpContext.User = authResult.Principal;
context.HttpContext.Items[typeof(AuthenticateResult)] = authResult;

这样当前请求后续调用GetTokenAsync时,就能从更新后的AuthenticationTicket中获取新令牌。

方案二:直接从Cookie属性获取令牌

修改HttpClient的配置代码,跳过GetTokenAsync,直接从AuthenticationProperties中取更新后的令牌:

services.AddHttpClient(AdminApiName, (sp, http) =>
{
    var context = sp.GetRequiredService<IHttpContextAccessor>().HttpContext!;
    // 直接从认证属性中获取更新后的令牌
    var authResult = context.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme).Result;
    var idToken = authResult.Properties.GetTokenValue("id_token");

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

注:这里使用.Result是因为AddHttpClient的配置委托不支持异步,若要避免同步等待,可改用IHttpMessageHandlerBuilderFilter实现异步令牌获取。

额外优化点

  1. 令牌过期判断时建议留缓冲时间,避免网络延迟导致令牌在请求过程中过期:
var bufferTime = TimeSpan.FromMinutes(5);
if (jwt.ValidTo.ToLocalTime() < DateTime.Now.Add(bufferTime))
{
    // 执行刷新逻辑
}
  1. 确保TokenResponse类的属性与IdP返回的字段完全匹配(如expires_in对应ExpiresIn),避免序列化失败。

内容的提问来源于stack exchange,提问作者Kralizek

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.07 00:44:58