Blazor Web App(.NET 8)调用受保护API的Microsoft Identity认证问题
我有一个已在Microsoft Entra ID中注册的.NET Core API测试项目,返回天气预报数据,可通过Postman正常调用。同时搭建了一个使用Microsoft Identity实现用户注册与登录的.NET 8 Blazor Web App,program.cs部分代码如下:
// Add services to the container. builder.Services.AddRazorComponents() .AddInteractiveServerComponents() .AddMicrosoftIdentityConsentHandler(); // set the client secret for now var clientSecret = "xxxxxxxxx"; // come back to this later and fix - maybe use Azure Key Vault builder.Services.AddCascadingAuthenticationState(); //builder.Services.AddTokenAcquisition(); // This is where you wire up to events to detect when a user Log in builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) .AddMicrosoftIdentityWebApp(options => { builder.Configuration.Bind("AzureAd", options); options.ClientSecret = clientSecret; options.Events = new OpenIdConnectEvents { OnRedirectToIdentityProvider = async ctxt => { // Invoked before redirecting to the identity provider to authenticate. // This can be used to set ProtocolMessage.State // that will be persisted through the authentication process. // The ProtocolMessage can also be used to add or customize // parameters sent to the identity provider. ctxt.ProtocolMessage.PostLogoutRedirectUri = ctxt.Request.Scheme + "://" + ctxt.Request.Host + "/"; await Task.Yield(); }, OnAuthenticationFailed = async ctxt => { // They tried to log in but it failed await Task.Yield(); }, OnSignedOutCallbackRedirect = async ctxt => { ctxt.HttpContext.Response.Redirect(ctxt.Options.SignedOutRedirectUri); ctxt.HandleResponse(); await Task.Yield(); }, OnTicketReceived = async ctxt => { if (ctxt.Principal != null) { if (ctxt.Principal.Identity is ClaimsIdentity identity) { var colClaims = await ctxt.Principal.Claims.ToDynamicListAsync(); var IdentityProvider = colClaims.FirstOrDefault( c => c.Type == "http://schemas.microsoft.com/identity/claims/identityprovider")?.Value; var Objectidentifier = colClaims.FirstOrDefault( c => c.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier")?.Value; var EmailAddress = colClaims.FirstOrDefault( c => c.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress")?.Value; var FirstName = colClaims.FirstOrDefault( c => c.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname")?.Value; var LastName = colClaims.FirstOrDefault( c => c.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname")?.Value; var AzureB2CFlow = colClaims.FirstOrDefault( c => c.Type == "http://schemas.microsoft.com/claims/authnclassreference")?.Value; var auth_time = colClaims.FirstOrDefault( c => c.Type == "auth_time")?.Value; var DisplayName = colClaims.FirstOrDefault( c => c.Type == "name")?.Value; var idp_access_token = colClaims.FirstOrDefault( c => c.Type == "idp_access_token")?.Value; } } await Task.Yield(); }, OnTokenValidated = async context => { var idToken = context.SecurityToken as JwtSecurityToken; if (idToken != null) { // Exchange ID token for an access token var accessToken = await context.HttpContext.GetTokenAsync("access_token"); // Now you have the access token, you can store it or use it as per your requirement. // For example, you can store it in the user s claims for later use. var claimsIdentity = context.Principal.Identity as ClaimsIdentity; claimsIdentity?.AddClaim(new Claim("id_token", idToken.RawData)); } await Task.Yield(); } }; }) .EnableTokenAcquisitionToCallDownstreamApi(new string[] { "api://xxxxxx/Access.Read" }) .AddInMemoryTokenCaches();
矛盾错误情况
- 配置客户端密钥(硬编码、appsettings或用户机密中)时,触发错误:
MsalServiceException: A configuration issue is preventing authentication - check the error message from the server for details. You can modify the configuration in the application registration portal. Original exception: AADSTS700025: Client is public so neither 'client_assertion' nor 'client_secret' should be presented.
- 不配置客户端密钥时,触发错误:
MsalClientException: One client credential type required either: ClientSecret, Certificate, ClientAssertion or AppTokenProvider must be defined when creating a Confidential Client. Only specify one.
注释掉EnableTokenAcquisitionToCallDownstreamApi后用户可正常登录,但ID Token不包含Scope信息,无法调用API。项目必须使用Microsoft生态,无法更换认证提供商。
解决方案
1. 修正应用注册类型(核心原因)
你的Blazor Web App当前被注册为公共客户端,但调用下游API需使用机密客户端。需在Microsoft Entra ID中修改应用注册:
- 进入Entra ID应用注册,找到你的Blazor应用
- 前往「管理 > 认证」页面,在「高级设置」中,将「允许公共客户端流」设置为否
- 保存更改后,该应用将被视为机密客户端,可正常使用客户端密钥
2. 正确配置客户端密钥与权限
- 在应用注册的「管理 > 证书和密码」页面,生成新的客户端密钥(或使用现有有效密钥),将其值存入
appsettings.json(推荐使用用户机密或Azure Key Vault,避免硬编码) - 确保
appsettings.json的AzureAd配置包含ClientSecret字段:
"AzureAd": { "Instance": "https://login.microsoftonline.com/", "Domain": "your-domain.onmicrosoft.com", "TenantId": "your-tenant-id", "ClientId": "your-client-id", "ClientSecret": "your-client-secret", "CallbackPath": "/signin-oidc", "SignedOutRedirectUri": "/" }
- 确认应用注册中已添加下游API权限:前往「管理 > API权限」,添加目标API的
Access.Read权限,并点击「授予管理员同意」
3. 优化Program.cs配置
移除硬编码的clientSecret,直接通过配置绑定读取,简化不必要的事件逻辑:
// Add services to the container. builder.Services.AddRazorComponents() .AddInteractiveServerComponents() .AddMicrosoftIdentityConsentHandler(); builder.Services.AddCascadingAuthenticationState(); builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd")) .EnableTokenAcquisitionToCallDownstreamApi(new string[] { "api://xxxxxx/Access.Read" }) .AddInMemoryTokenCaches(); // 其他服务配置...
4. 获取并使用访问令牌调用API
在Blazor组件中,通过ITokenAcquisition服务获取访问令牌后调用API:
@inject ITokenAcquisition TokenAcquisition @inject HttpClient HttpClient private async Task CallWeatherApi() { var accessToken = await TokenAcquisition.GetAccessTokenForUserAsync(new[] { "api://xxxxxx/Access.Read" }); HttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); var weatherData = await HttpClient.GetFromJsonAsync<WeatherForecast[]>("/weatherforecast"); // 处理返回数据 }
内容的提问来源于stack exchange,提问作者AnnR

