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

Blazor Server集成Azure AD B2C调用API遇401未授权问题

Blazor Server (.NET 7) 集成Azure AD B2C调用API遇401及令牌请求错误

已完成的基础配置

  • 已创建客户端和API的应用注册
  • API应用注册已暴露Portal.User Scope
  • 客户端应用注册已获取该Scope权限并完成管理员同意
  • 可正常通过B2C登录验证,令牌中可见自定义Scope
  • 客户端(https://localhost:portA)与API(https://localhost:portB)部署在不同地址,符合需求

问题现象

  1. 按微软官方指南配置令牌获取与API调用传递后,调用API始终返回401 Unauthorized,已确认TokenHandler成功获取令牌并附加到请求头,API地址正确。
  2. 尝试添加EnableTokenAcquisitionToCallDownstreamApi配置时,出现错误:

Message contains error: 'invalid_request', error_description: 'AADB2C90146: The scope 'openid profile offline_access https://(mytenant).onmicrosoft.com/11012303-6e36-4c8a-a240-0b79d866dfb5/Portal.User 53625e8b-6d88-4061-9d47-e9821e0d744c' provided in request specifies more than one resource for an access token, which is not supported. Correlation ID: af4eef71-7c2e-473b-9fa8-3fa49726bbe2 Timestamp: 2023-05-09 02:51:42Z ', error_uri: 'error_uri is null'.


API端配置代码

Program.cs

builder.Services.AddAuthorization(options =>
{
// Create policy to check for the scope
options.AddPolicy("Portal.User",
    policy => policy.Requirements.Add(new ScopeRequirement("Portal.User")));
});

app.UseAuthentication();
app.UseAuthorization();

ScopeRequirement类

public class ScopeRequirement: AuthorizationHandler<ScopeRequirement>, IAuthorizationRequirement
{
    string[] _acceptedScopes;

    public ScopeRequirement(params string[] acceptedScopes)
    {
        _acceptedScopes = acceptedScopes;
    }

    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
                                                    ScopeRequirement requirement)
    {
        if (!context.User.Claims.Any(x => x.Type == ClaimConstants.Scope)
           && !context.User.Claims.Any(y => y.Type == ClaimConstants.Scp))
        {
            return Task.CompletedTask;
        }

        Claim scopeClaim = context?.User?.FindFirst(ClaimConstants.Scp);

        if (scopeClaim == null)
            scopeClaim = context?.User?.FindFirst(ClaimConstants.Scope);

        if (scopeClaim != null && scopeClaim.Value.Split(' ').Intersect(requirement._acceptedScopes).Any())
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
 }

客户端配置代码

Program.cs

// Configuration to sign-in users with Azure AD B2C.
var configuration = builder.Configuration;
// Add authentication with Microsoft identity platform
builder.Services.AddMicrosoftIdentityWebAppAuthentication(configuration, "AzureAdB2C");

...

builder.Services.Configure<OpenIdConnectOptions>(
    OpenIdConnectDefaults.AuthenticationScheme, options =>
    {
        options.ResponseType = OpenIdConnectResponseType.Code;
        options.SaveTokens = true;
        options.Scope.Add(options.ClientId);
        options.Scope.Add("offline_access");
        options.Scope.Add("openid");
    });

builder.Services.AddHttpContextAccessor();

builder.Services.AddScoped<TokenHandler>();
builder.Services.AddHttpClient("api", options =>
{
    options.BaseAddress = new Uri(configuration["API:BaseUri"] ?? "");
}).AddHttpMessageHandler<TokenHandler>();
builder.Services.AddTransient(sp => sp.GetRequiredService<IHttpClientFactory>()
    .CreateClient("api"));

builder.Services.AddScoped<TokenProvider>();

builder.Services.AddControllersWithViews(options =>
{
    var policy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build();
    options.Filters.Add(new AuthorizeFilter(policy));
}).AddMicrosoftIdentityUI();

builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor()
    .AddMicrosoftIdentityConsentHandler();

...
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.MapBlazorHub().RequireAuthorization(
    new AuthorizeAttribute
    {
        AuthenticationSchemes = OpenIdConnectDefaults.AuthenticationScheme
    });

TokenHandler.cs

public class TokenHandler : DelegatingHandler
{
    private readonly IHttpContextAccessor accessor;

    public TokenHandler(IHttpContextAccessor accessor) => this.accessor = accessor;

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var accessToken = await accessor.HttpContext.GetTokenAsync("access_token");
        request.Headers.Authorization =
            new AuthenticationHeaderValue("Bearer", accessToken);
        return await base.SendAsync(request, cancellationToken);
    }
}

排查与解决思路

一、先解决AADB2C90146令牌请求错误

这个错误是因为请求的Scope包含多个资源标识符:API的Scope和客户端自身的ClientId,而Azure AD B2C不允许一个access_token同时包含多资源权限。

解决步骤:

  1. 移除客户端配置中options.Scope.Add(options.ClientId)代码,客户端ClientId无需加入access_token的Scope列表。
  2. 调整OpenIdConnect配置,只保留必要的Scope:
builder.Services.Configure<OpenIdConnectOptions>(
    OpenIdConnectDefaults.AuthenticationScheme, options =>
    {
        options.ResponseType = OpenIdConnectResponseType.Code;
        options.SaveTokens = true;
        options.Scope.Add("openid");
        options.Scope.Add("profile");
        options.Scope.Add("offline_access");
        options.Scope.Add(configuration["API:Scope"]); // 仅添加API的完整Scope
    });
  1. 正确配置令牌获取:
builder.Services.AddMicrosoftIdentityWebAppAuthentication(configuration, "AzureAdB2C")
    .EnableTokenAcquisitionToCallDownstreamApi(new string[] { configuration["API:Scope"] })
    .AddInMemoryTokenCaches();

二、排查API端401 Unauthorized问题

解决令牌请求错误后,若仍出现401,按以下步骤排查:

1. 补全API端身份验证配置

当前API仅配置了授权策略,缺少Azure AD B2C身份验证中间件,无法验证令牌有效性:

// 在AddAuthorization之前添加
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(configuration, "AzureAdB2C");

同时确保appsettings.json包含API的B2C配置:

"AzureAdB2C": {
    "Instance": "https://{你的租户名}.b2clogin.com/",
    "ClientId": "{API应用注册的ClientId}",
    "Domain": "{你的租户名}.onmicrosoft.com",
    "SignUpSignInPolicyId": "{你的注册登录策略名}"
}

2. 验证令牌的aud(受众)是否匹配

用JWT解析工具查看access_token的aud字段,必须等于API应用注册的ClientId。若不匹配,检查客户端配置的API Scope是否为完整格式:https://{租户}.onmicrosoft.com/{API-ClientId}/{ScopeName}。

3. 修正ScopeRequirement逻辑

Azure AD B2C颁发的access_token中,Scope声明的键是scp(小写),简化验证逻辑:

protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ScopeRequirement requirement)
{
    var scopeClaim = context.User.FindFirst("scp");
    if (scopeClaim != null && scopeClaim.Value.Split(' ').Any(s => s.Equals(requirement._acceptedScopes.First(), StringComparison.OrdinalIgnoreCase)))
    {
        context.Succeed(requirement);
    }
    return Task.CompletedTask;
}

4. 确保授权策略被应用

在API的控制器或Action上添加[Authorize(Policy = "Portal.User")],确保授权策略生效。

5. 配置CORS策略

因客户端与API跨域,API端需添加CORS配置:

builder.Services.AddCors(options =>
{
    options.AddPolicy("AllowBlazorClient",
        policy => policy.WithOrigins("https://localhost:portA")
                        .AllowAnyHeader()
                        .AllowAnyMethod());
});

// 在UseAuthentication之后添加
app.UseCors("AllowBlazorClient");

三、客户端令牌获取优化

使用微软官方的BearerTokenAuthorizationMessageHandler替代自定义TokenHandler,自动处理令牌的获取、缓存与刷新:

builder.Services.AddHttpClient("api", options =>
{
    options.BaseAddress = new Uri(configuration["API:BaseUri"] ?? "");
}).AddHttpMessageHandler(sp =>
{
    var handler = sp.GetRequiredService<BearerTokenAuthorizationMessageHandler>();
    handler.Scopes.Add(configuration["API:Scope"]);
    return handler;
});

同时移除自定义TokenHandler的相关注册代码。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.22 09:57:02