在ASP.NET Core的OpenIdConnect事件中调用Entra Id保护的Web API遇问题
解决Blazor Web应用中OnTokenValidated事件调用Entra ID保护API的MSAL异常问题
问题原因
在OnTokenValidated事件触发时,MSAL尚未将用户账户信息持久化到本地缓存中。此时调用ITokenAcquisition.GetAccessTokenForUserAsync会因为找不到缓存的用户账户,触发MsalUiRequiredException(提示"No account or login hint was passed to the AcquireTokenSilent call")。这个阶段用户的认证流程尚未完全完成,无法支持静默获取令牌的操作。
替代解决方案
方案1:使用IClaimsTransformation扩展Claims(推荐)
IClaimsTransformation是ASP.NET Core官方提供的扩展Claims Principal的机制,会在认证流程完全完成后(用户账户已写入MSAL缓存)执行,此时可以安全调用ITokenAcquisition获取令牌。
- 实现自定义Claims转换器:
public class CustomClaimsTransformer : IClaimsTransformation { private readonly ITokenAcquisition _tokenAcquisition; private readonly ApiOptions _apiOptions; public CustomClaimsTransformer(ITokenAcquisition tokenAcquisition, IOptions<ApiOptions> apiOptions) { _tokenAcquisition = tokenAcquisition; _apiOptions = apiOptions.Value; } public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal) { // 避免重复处理自定义Claims if (principal.HasClaim(c => c.Type == "CustomApiClaim")) { return principal; } try { // 静默获取API访问令牌 string accessToken = await _tokenAcquisition.GetAccessTokenForUserAsync(_apiOptions.Scopes, user: principal); // 调用受保护的Web API获取用户额外信息 using var httpClient = new HttpClient(); httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); var userDetails = await httpClient.GetFromJsonAsync<UserDetails>("https://your-api-endpoint/api/user/profile"); // 向ClaimsIdentity添加自定义Claim var identity = principal.Identity as ClaimsIdentity; identity.AddClaim(new Claim("CustomApiClaim", userDetails.UserSpecificData)); return new ClaimsPrincipal(identity); } catch (MsalUiRequiredException) { // 令牌获取失败时返回原Principal,可根据业务需求添加重定向等逻辑 return principal; } } // 定义API返回的用户信息模型 private class UserDetails { public string UserSpecificData { get; set; } } }
- 在Program.cs中注册转换器:
builder.Services.AddScoped<IClaimsTransformation, CustomClaimsTransformer>();
方案2:在登录回调页面处理
将API调用逻辑移到登录成功后的回调页面(比如指定的RedirectUri页面),此时用户认证状态已完全建立,MSAL缓存也已写入。
示例Login.razor页面:
@page "/login-callback" @inject ITokenAcquisition TokenAcquisition @inject IOptions<ApiOptions> ApiOptions @inject NavigationManager NavigationManager @inject AuthenticationStateProvider AuthStateProvider @code { protected override async Task OnInitializedAsync() { try { var accessToken = await TokenAcquisition.GetAccessTokenForUserAsync(ApiOptions.Value.Scopes); // 调用API获取用户信息 using var httpClient = new HttpClient(); httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); var userDetails = await httpClient.GetFromJsonAsync<UserDetails>("https://your-api-endpoint/api/user/profile"); // 更新用户Claims(可通过自定义AuthenticationStateProvider实现) var customAuthProvider = (CustomAuthenticationStateProvider)AuthStateProvider; await customAuthProvider.UpdateUserClaims(userDetails.UserSpecificData); // 重定向到主页 NavigationManager.NavigateTo("/"); } catch (MsalUiRequiredException) { NavigationManager.NavigateTo("/login-failed"); } } private class UserDetails { public string UserSpecificData { get; set; } } }
方案3:在AuthorizationCodeReceived事件中直接获取令牌
利用授权码直接请求访问令牌,不依赖MSAL的用户缓存,适合需要在认证流程中立即获取令牌的场景:
public override async Task AuthorizationCodeReceived(AuthorizationCodeReceivedContext context) { await base.AuthorizationCodeReceived(context); // 用授权码直接请求访问令牌 var tokenClient = new TokenClient( context.Options.TokenEndpoint, context.Options.ClientId, context.Options.ClientSecret); var tokenResponse = await tokenClient.RequestAuthorizationCodeTokenAsync( context.ProtocolMessage.Code, context.Options.RedirectUri); string accessToken = tokenResponse.AccessToken; // 调用API获取用户信息并添加到Claims using var httpClient = new HttpClient(); httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); var userDetails = await httpClient.GetFromJsonAsync<UserDetails>("https://your-api-endpoint/api/user/profile"); context.Principal.Identities.First().AddClaim(new Claim("CustomApiClaim", userDetails.UserSpecificData)); } private class UserDetails { public string UserSpecificData { get; set; } }
总结
优先选择方案1(IClaimsTransformation),它符合ASP.NET Core的认证扩展规范,代码可维护性更高,且能自动处理Claims的缓存和更新。方案2和3适合有特殊业务场景需求的情况。
内容的提问来源于stack exchange,提问作者TheGrumpyDev
相关产品推荐
相关产品推荐

