使用IConfidentialClientApplication获取安全Web API令牌报401错误咨询
C#控制台应用调用Azure AD保护的Web API返回401错误问题说明
场景1:前端应用正常流程
- 用户在React应用中输入账号凭证
- 调用https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize端点
- 成功获取id token
- 已认证用户请求私有Web API时,携带上一步的id token调用https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token端点
- 获取新的JWT令牌
- 将该JWT令牌附加到Web API请求头中
- React应用收到Web API返回的响应
以上是React应用与Web API交互的常规流程,对应OAuth2认证流程。
场景2:C#控制台应用模拟流程
使用MSAL.NET库编写测试代码如下:
private static async Task RunAsync() { string clientId = "client Id of the application that I have registered using azure app registration in Azure B2C"; string clientSecret = "client secret of the application that I have registered using azure app registration in Azure B2C"; string instance = "https://login.microsoftonline.com/{0}/"; string tenantId = "Tenant Id that I can see when I open the application that I have registered using azure app registration in Azure B2C"; string webAppUri = "web app domain"; // For Web applications that use OpenID Connect Authorization Code flow, use IConfidentialClientApplication IConfidentialClientApplication app; app = ConfidentialClientApplicationBuilder .Create(clientId) .WithClientSecret(clientSecret) .WithAuthority(new Uri($"https://login.microsoftonline.com/{tenantId}")) .WithLegacyCacheCompatibility(false) .Build(); // For confidential clients, this value should use a format similar to {Application ID URI}/.default. string[] scopes = new string[] { $"{webAppUri}/.default" }; AuthenticationResult result = null; try { result = await app.AcquireTokenForClient(scopes).ExecuteAsync(); Console.ForegroundColor = ConsoleColor.Green; Console.WriteLine("Token acquired"); Console.WriteLine($"{result.AccessToken}"); Console.ResetColor(); } catch (MsalServiceException ex) when (ex.Message.Contains("AADSTS70011")) { // Invalid scope. The scope has to be of the form "https://resourceurl/.default" // Mitigation: change the scope to be as expected Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine("Scope provided is not supported"); Console.ResetColor(); } if (result != null) { var httpClient = new HttpClient(); var apiCaller = new ProtectedApiCallHelper(httpClient); string webApiUrl = "http://localhost:12345/mycustomwebapi/list"; var defaultRequetHeaders = httpClient.DefaultRequestHeaders; if (defaultRequetHeaders.Accept == null || !defaultRequetHeaders.Accept.Any(m => m.MediaType == "application/json")) { httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); } defaultRequetHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken); HttpResponseMessage response = await httpClient.GetAsync(webApiUrl); if (response.IsSuccessStatusCode) { string json = await response.Content.ReadAsStringAsync(); var jsonResult = JsonConvert.DeserializeObject<List<JObject>>(json); } else { Console.WriteLine($"Failed to call the Web Api: {response.StatusCode}"); string content = await response.Content.ReadAsStringAsync(); } Console.ResetColor(); } }
当前问题
上述代码可以获取格式正常的JWT令牌,附加到请求头后调用自定义安全Web API时,返回401 Unauthorized响应。
疑问点
- 对比场景1的步骤,C#代码是否缺失了某个环节?
- 已在Azure AD注册的Web API是否需要配置额外的访问权限?
- 是否需要在C#中复刻场景1的步骤:先调用https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize端点,再调用https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token端点获取有效JWT令牌?
补充信息
通过https://jwt.io对比C#获取的令牌和前端获取的令牌,两者包含的属性存在差异。
问题排查与解决方案
问题核心是两种场景使用的OAuth2认证流不一致,导致令牌属性和校验逻辑不匹配:
- 你的C#代码没有缺失逻辑环节,只是使用了错误的认证流:前端流程走的是带用户身份的授权码流,下发的令牌会携带用户相关的
oid、scp等委托权限声明;而你代码里AcquireTokenForClient走的是无用户参与的客户端凭证流,下发的令牌携带的是应用身份的roles等应用权限声明,如果你的Web API只校验用户身份的委托权限令牌,就会直接返回401。 - 如果你要保留客户端凭证流的实现,需要给Web API额外配置权限:首先在Azure AD的Web API注册条目里新增应用权限(注意不是委托权限),然后给控制台应用的注册条目分配这个应用权限并完成管理员同意,同时需要调整Web API的校验逻辑,支持识别应用权限的
roles声明。 - 如果你要完全复刻前端的用户身份流程,需要改用公共客户端流实现:比如设备代码流、用户名密码流(仅测试可用,不建议生产环境使用),这两类流都会走先校验身份、再获取令牌的逻辑,拿到的令牌属性和前端一致,可以直接被现有的Web API校验逻辑接受。
内容的提问来源于stack exchange,提问作者MikePR
相关产品推荐
相关产品推荐

