我正在开发的应用程序由两个姐妹 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。
如何解决?
谢谢!
如果在 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 配置委托中。