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

在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获取令牌。

  1. 实现自定义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; }
    }
}
  1. 在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.12 16:12:28