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

ASP.NET Core 8+Angular迁移至Entra ID for Customers偶发令牌颁发者不匹配

问题:从Azure AD B2C升级到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

解决方案

  1. 修正后端配置中的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" ]
        }
    },
    
  2. 修正前端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: '/',
        },
        // 其他配置保持不变
    }
    
  3. 显式配置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模式下的配置冲突。

  4. 清除缓存并重启服务
    重启应用后清除浏览器LocalStorage和服务器内存缓存,避免旧配置或令牌残留导致的随机问题。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 10:21:00