Blazor Server集成Azure AD B2C调用API遇401未授权问题
已完成的基础配置
- 已创建客户端和API的应用注册
- API应用注册已暴露
Portal.UserScope - 客户端应用注册已获取该Scope权限并完成管理员同意
- 可正常通过B2C登录验证,令牌中可见自定义Scope
- 客户端(
https://localhost:portA)与API(https://localhost:portB)部署在不同地址,符合需求
问题现象
- 按微软官方指南配置令牌获取与API调用传递后,调用API始终返回
401 Unauthorized,已确认TokenHandler成功获取令牌并附加到请求头,API地址正确。 - 尝试添加
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同时包含多资源权限。
解决步骤:
- 移除客户端配置中
options.Scope.Add(options.ClientId)代码,客户端ClientId无需加入access_token的Scope列表。 - 调整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 });
- 正确配置令牌获取:
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

