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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 00:04:58