ASP.NET Core 8+Angular迁移至Entra ID for Customers偶发令牌颁发者不匹配
将应用从Azure AD B2C升级到Entra ID for Customers(配置与单租户Azure AD应用基本一致)时,遇到以下核心错误:
消息:IDX40001: 颁发者'https://{tenantGuid}.ciamlogin.com/{tenantGuid}/'与应用配置的有效颁发者不匹配。
内部异常:IDX20803: 无法从'https://{tenantDomain}.ciamlogin.com/{tenantGuid}/.well-known/openid-configuration'获取配置,将在'xxxx'重试。
异常:System.IO.IOException: IDX20804: 无法从'https://{tenantDomain}.ciamlogin.com/common/discovery/keys'检索文档。
客户端收到的Www-authentication头信息:
Bearer error="invalid_token", error_description="The issuer '(null)' is invalid"
反常现象:按下Ctrl+Shift+R刷新3-10次后,问题有时会自行解决;重启应用或使用隐私浏览器测试,结果随机。从错误信息看,Microsoft.Identity.Web似乎在错误的/common/端点查找令牌密钥,而非appsettings中指定的租户ID。
已确认会话中所有API请求携带的Bearer令牌完全相同,令牌包含的iss声明为:
"iss": "https://{tenantGuid}.ciamlogin.com/{tenantGuid}/"
相关配置
appsettings.development.json
"AzureAd": { "Instance": "https://{tenantDomain}.ciamlogin.com/", "ClientId": "{serverAppRegistrationClientId}", "Domain": "{tenantDomain}.onmicrosoft.com", "TenantId": "{tenantGuid}", "CallbackPath": "/signin-oidc", "Scopes": { "Read": [ "{scope1}.Read", "{scope1}.ReadWrite" ], "Write": [ "{scope1}.ReadWrite" ] } },
program.cs(基于CleanArchitecture模板)
var builder = WebApplication.CreateBuilder(args); JwtSecurityTokenHandler.DefaultMapInboundClaims = false; builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) .AddMicrosoftIdentityWebApi(builder.Configuration, "AzureAd", OpenIdConnectDefaults.AuthenticationScheme); builder.Services.AddAuthorizationBuilder()... IdentityModelEventSource.ShowPII = true; builder.Services.AddMediatR(cfg => { cfg.RegisterServicesFromAssemblyContaining<Program>(); }); builder.Services.AddApplicationServices(builder.Configuration); builder.Services.AddInfrastructureServices(builder.Configuration); builder.Services.AddWebServices(); var modelBuilder = new ODataConventionModelBuilder(); modelBuilder.EntitySet<{entity1}>("{Entity1}"); builder.Services.AddControllers().AddOData(options => options.EnableQueryFeatures().AddRouteComponents( routePrefix: "api", model: modelBuilder.GetEdmModel()) ); builder.Services.AddControllers(); var app = builder.Build(); if (app.Environment.IsDevelopment()) { app.UseExceptionHandler("/error"); app.UseMigrationsEndPoint(); await app.InitialiseDatabaseAsync(); } else { app.UseExceptionHandler("/error"); app.UseHsts(); } app.UseSwagger(); app.UseSwaggerUI(options => { options.SwaggerEndpoint("/swagger/v1/swagger.json", "v1"); }); app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.UseODataRouteDebug(); app.MapControllers(); app.MapControllerRoute( name: "default", pattern: "api/{controller}/{action=Index}/{id?}"); app.MapFallbackToFile("index.html"); app.Run(); public partial class Program { }
auth-config.ts
import { LogLevel, Configuration, BrowserCacheLocation } from '@azure/msal-browser'; const isIE = window.navigator.userAgent.indexOf("MSIE ") > -1 || window.navigator.userAgent.indexOf("Trident/") > -1; export const msalConfig: Configuration = { auth: { clientId: '{clientAppRegistrationClientId}', authority: 'https://{tenantDomain}.ciamlogin.com', redirectUri: '/signin-oidc', postLogoutRedirectUri: '/', }, cache: { cacheLocation: BrowserCacheLocation.LocalStorage, storeAuthStateInCookie: isIE, }, system: { loggerOptions: { loggerCallback(logLevel: LogLevel, message: string) { console.log(message); }, logLevel: LogLevel.Verbose, piiLoggingEnabled: false } } } export const protectedResources = { apiScope1: { endpoint: "/api/entity1", scopes: { read: ["api://{serverAppRegistrationClientId}/{scope1}.Read"], write: ["api://{serverAppRegistrationClientId}/{scope1}.ReadWrite"] } } } export const loginRequest = { scopes: [] };
依赖版本
- .NET 8.0.1
- Microsoft.Identity.Web 2.17.4
- @azure/msal-angular 3.0.9
- @azure/msal-browser 3.6.0
已尝试方案
- 新建Entra ID for Customers租户
- 调整配置文件中的租户域名/ID、端点格式
- 参考微软官方示例配置
- 设置
JwtSecurityTokenHandler.DefaultMapInboundClaims = false - 切换认证方案为OpenIdConnect/JwtBearer
解决方案
修正后端配置中的Authority参数
Entra ID for Customers需要明确指定包含租户ID的Authority,避免自动构造时使用/common/端点。修改appsettings.development.json的AzureAd节点:"AzureAd": { "Instance": "https://{tenantDomain}.ciamlogin.com/", "ClientId": "{serverAppRegistrationClientId}", "Domain": "{tenantDomain}.onmicrosoft.com", "TenantId": "{tenantGuid}", "CallbackPath": "/signin-oidc", "Authority": "https://{tenantDomain}.ciamlogin.com/{tenantGuid}/", "Scopes": { "Read": [ "{scope1}.Read", "{scope1}.ReadWrite" ], "Write": [ "{scope1}.ReadWrite" ] } },修正前端MSAL的Authority配置
前端auth-config.ts中的authority必须包含租户ID,否则会默认指向/common/端点:export const msalConfig: Configuration = { auth: { clientId: '{clientAppRegistrationClientId}', authority: 'https://{tenantDomain}.ciamlogin.com/{tenantGuid}/', redirectUri: '/signin-oidc', postLogoutRedirectUri: '/', }, // 其他配置保持不变 }显式配置JWT验证逻辑
在program.cs中手动指定元数据地址和颁发者验证规则,避免自动发现错误:builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddMicrosoftIdentityWebApi(options => { builder.Configuration.Bind("AzureAd", options); // 强制验证颁发者 options.TokenValidationParameters.IssuerValidator = (issuer, token, parameters) => { var expectedIssuer = $"https://{builder.Configuration["AzureAd:TenantId"]}.ciamlogin.com/{builder.Configuration["AzureAd:TenantId"]}/"; if (issuer != expectedIssuer) { throw new SecurityTokenInvalidIssuerException($"无效颁发者:{issuer}"); } return issuer; }; // 手动指定元数据地址 options.MetadataAddress = $"https://{builder.Configuration["AzureAd:TenantDomain"]}.ciamlogin.com/{builder.Configuration["AzureAd:TenantId"]}/.well-known/openid-configuration"; }, options => { builder.Configuration.Bind("AzureAd", options); });切换到
JwtBearerDefaults.AuthenticationScheme更适配API场景,同时避免OpenIdConnect模式下的配置冲突。清除缓存并重启服务
重启应用后清除浏览器LocalStorage和服务器内存缓存,避免旧配置或令牌残留导致的随机问题。
内容的提问来源于stack exchange,提问作者samDTMSP

