Azure 存储帐户上作为静态网站的角度网站在调用受 Azure B2C 保护的函数应用函数时会收到 500。该函数正在接收 404。
更新
这个问题的原始标题是"调用 B2C 保护函数应用的 Angular 应用收到401 Unauthorized
响应"。解决方案是,正如 AIT 建议@Alex(如下)所示,将函数应用的颁发者 URL中的https://<b2c_tenant_name>.b2clogin.com/<b2c_tenant_name>.onmicrosoft.com/v2.0/.well-known/openid-configuration?p=<SignUpAndSignInPolicyName>
替换为https://<b2c_tenant_name>.b2clogin.com/<b2c_tenant_name>.onmicrosoft.com/v2.0/
。即,删除尾随.well-known/openid-configuration?p=<SignUpAndSignInPolicyName>
段。在随后的聊天会话中,亚历克斯指出,该策略是路径的一部分,例如https://<tenantname>.b2clogin.com/<tenantname>.onmicrosoft.com/<policyname>/v2.0
或https://<tenantname>.b2clogin.com/<tenantguid>/<policyname>/v2.0
。但是,函数应用的颁发者 URL 的这些路径中的任何一个都会还原为 401 响应。
解决 401 问题后,Angular SPA 应用程序现在会收到 500。但是,调用的 API 函数正在接收 404。函数应用的日志流指示Failed to download OpenID configuration from 'https://<b2c_tenant_name>.b2clogin.com/<b2c_tenant_name>.onmicrosoft.com/v2.0/.well-known/openid-configuration': The remote server returned an error: (404) Not Found.
因此不会附加策略。
我的目标是建立一个安全的无服务器 Angular Web 应用程序,该应用程序静态托管在 Azure 存储网站上(即,在存储帐户的$web
容器内)。有两个项目:一个公共SPA
Angular 7+项目和一个受保护的API
函数应用项目。由于 Azure 存储帐户静态网站仅允许对所有文件进行公共匿名访问,因此Angular应用托管 blob 容器的文件(网站的文件)不受保护。但Angular应用对Azure FunctionsAPI 调用的调用是安全的。函数应用API
项目通过 Azure AD B2C 身份验证进行保护。
为此,我尝试采用基于 MSAL.js 构建的单页应用程序与 Azure AD B2C 和 Node.js Web API 与 Azure AD B2C 中概述的技术。我能够让这些样本运行。此外,我能够修改它们的设置,以针对我自己的 Azure B2C 租户(而不是针对Microsoft的 B2C 租户)进行身份验证,并在本地运行它们。但我并没有尝试将这些示例项目部署到 Azure 并找出设置所需的调整。我跳过了部署练习,因为我不是 Node.js 开发人员。
但是,每当从 SPA 调用 API 时,我随后将这些 (Node.js) 示例项目中的代码改编为静态托管的 Angular SPA 项目和 Azure Functions API 项目,都会产生401 Unauthorized
。所以我想了解如何解决这个问题。
设置的
假设/先决条件
- 已创建 AzureB2C 租户
- 已为B2C 租户配置标识提供者
- 已为B2C 租户配置了
Sign-up and Sign-in
用户流策略- 记下其名称。我们将在下面将其名称称为
<SignUpAndSignInPolicyName>
- 记下其名称。我们将在下面将其名称称为
- 已创建启用了静态网站功能的 Azure 存储帐户
已创建角度应用程序
已安装@azure/msal-angular
包在
已app-routing.module.ts
,- 设置
useHash
选项:imports: [RouterModule.forRoot(routes, { useHash: true })],
- 哈希路由对于容纳静态托管是必要的
- 已创建安全组件并建立受保护路由
const routes: Routes = [ { path: 'secure', component: SecureComponent, canActivate: [MsalGuard] }, { path: 'state', redirectTo: 'secure' }, // HACK/TODO { path: 'error', redirectTo: 'secure' }, // HACK/TODO { path: '', redirectTo: '', pathMatch: 'full' }, ];
- 设置
已创建 Azure函数应用
- 记下函数应用的URL
出于测试目的,已在函数应用中创建了以下函数。它已发布到 Azure:
using System; using System.IO; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Azure.WebJobs; using Microsoft.Azure.WebJobs.Extensions.Http; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Newtonsoft.Json; namespace SomeCompany.Functions { public static class HttpTriggerCSharp { [FunctionName("HttpTriggerCSharp")] public static async Task<IActionResult> Run( [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req, ILogger log) { log.LogInformation("C# HTTP trigger function processed a request."); string name = req.Query["name"]; string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); dynamic data = JsonConvert.DeserializeObject(requestBody); name = name ?? data?.name; return name != null ? (ActionResult)new OkObjectResult($"Hello, {name}") : new BadRequestObjectResult("Please pass a name on the query string or in the request body"); } } }
B2C 租户
接口应用- 创建
API
应用程序(即将其命名为">API") - 记下其应用程序
- ID 稍后将在函数应用的 AAD 身份验证设置中使用应用程序 ID
- 将"包括 Web 应用/Web API"设置为"是 "
- 将">允许隐式流"设置为"是 "
- 将回复URL 设置为
https://<functionappname>.azurewebsites.net/.auth/login/aad/callback
函数- 应用的URL 后缀
/.auth/login/aad/callback
- 应用的URL 后缀
- 将应用 ID URI段设置为"API">
- 产生:
https://<b2c_tenant_name>.onmicrosoft.com/API
- 产生:
- 创建
SPA
应用程序(即将其命名为">SPA") - 将"包括 Web 应用/Web API"设置为"是 "
- 将">允许隐式流">设置为"是 "
- 将回复 URL设置为http://localhost:4200
- 在 API访问选项卡中,添加
API
应用程序 API- 将预先选择唯一可用的范围"代表登录用户 (user_impersonation) 访问此应用
主(非 B2C)租户
函数应用- 在"身份验证/授权">边栏选项卡中,
- 将应用服务身份验证设置为"开">
- 将请求未通过身份验证时要执行的操作设置为使用 Azure 活动目录登录
- 在"身份验证提供程序"部分中,按如下所示配置Azure Active Directory提供程序:
- 将"管理模式"设置为"高级">
- 将客户端ID 设置为 B2C
API
应用程序的应用程序ID - 将颁发者 URL设置为
https://<b2c_tenant_name>.b2clogin.com/<b2c_tenant_name>.onmicrosoft.com/v2.0/.well-known/openid-configuration?p=<SignUpAndSignInPolicyName>
- 保存这些身份验证/授权设置
在Angular应用程序的
app.module.ts
NgModule
导入属性中,设置:MsalModule.forRoot({ clientID: '<B2C Tenant |> SPA Application |> Application ID>', // Note, for authority, the following doesn't work: // B2C Tenant |> User flows (policies) |> <SignUpAndSignInPolicyName> |> Run user flow |> URL at top of the `Run user flow` blade // I.e., `https://<b2c_tenant_name>.b2clogin.com/<b2c_tenant_name>.onmicrosoft.com/v2.0/.well-known/openid-configuration?p=<SignUpAndSignInPolicyName>` // Supposedly (according to various blog posts), that URL should be used as the `authority`. So, why doesn't it work?. // The following URL works. However, the B2C portal indicates that `login.microsoftonline.com` is to be deprecated soon authority: 'https://login.microsoftonline.com/tfp/<b2c_tenant_name>.onmicrosoft.com/<SignUpAndSignInPolicyName>', // B2C Tenant |> Applications |> API |> Published Scopes |> `user_impersonation` | FULL SCOPE VALUE consentScopes: ['https://<b2c_tenant_name>.onmicrosoft.com/API/user_impersonation'], })
创建名为
Secure
的组件ng g c Secure -s --skipTests
secure.component.ts
import { Component } from '@angular/core'; import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Subscription } from 'rxjs'; import { MsalService } from '@azure/msal-angular'; @Component({ selector: 'app-secure', templateUrl: './secure.component.html', }) export class SecureComponent { constructor(private http: HttpClient, private msalService: MsalService) { } azureTestFunctionResponse: string; callApiWithAccessToken(accessToken: string) { const url = 'https://<function_app_name>.azurewebsites.net/api/HttpTriggerCSharp?name=HelloFromAzureFunction'; const httpHeaders = new HttpHeaders({ Authorization: `Bearer ${accessToken}` }); const subscription: Subscription = this.http.get(url, { headers: httpHeaders , responseType: 'text'}).subscribe(_ => { this.azureTestFunctionResponse = _; subscription.unsubscribe(); }); } invokeB2cSecuredAzureFunction() { // B2C Tenant |> `API` Application |> Published Scopes |> `user_impersonation` scope |> Full Scope Value const tokenRequest: string[] = ['https://<b2c_tenant_name>.onmicrosoft.com/API/user_impersonation']; this.msalService.acquireTokenSilent(tokenRequest) .then(tokenResponse => { this.callApiWithAccessToken(tokenResponse); }) .catch(error1 => { this.msalService.acquireTokenPopup(tokenRequest) .then(tokenResponse => { this.callApiWithAccessToken(tokenResponse); }) .catch(error => { console.log('Error acquiring the access token to call the Web api:n' + error); }); }); } }
secure.component.html
<h4>Secure Component</h4> <button (click)="invokeB2cSecuredAzureFunction()">Fetch data from B2C-secured Azure functions</button> <hr /> <div>{{azureTestFunctionResponse}}</div>
app.component.html
<div style="text-align:center"> <h4> {{ title }} </h4> </div> <mat-card style="float: left;"> This site is a configuration demonstration of a secure, serverless Angular web application. The site is statically hosted on an <em>Azure Storage</em> website (<code>$web</code> container). The site's backend is secured by Azure <em>Business-to-Consumer</em> <span class="acronym">(B2C)</span> authentication. The site interacts with a secure <em>Azure Functions</em> <span class="acronym">API</span>. </mat-card> <p style="text-align: center;"><a routerLink="/" routerLinkActive="active">Home</a> <a routerLink="/secure" routerLinkActive="active">Secure</a></p> <p style="text-align: center;"><router-outlet></router-outlet></p>
在本地提供应用:
ng serve
- 单击安全链接
- 导航到
/secure
路线 - 提示用户进行身份验证
- 导航到
- 单击
Fetch data from B2C-secured Azure function
按钮 - 服务器返回
401 Not Authorized
响应 - 如果 SPA 应用的
Reply URL
更新为SPA
静态网站 URL 并发布 SPA 文件,则调用 API 函数时同样会返回401
。
所以我不确定配置错误。有什么想法吗?
这不是租户的颁发者:
https://<b2c_tenant_name>.b2clogin.com/<b2c_tenant_name>.onmicrosoft.com/v2.0/.well-known/openid-configuration?p=<SignUpAndSignInPolicyName>
但是,如果您在浏览器中打开此 URL,它将显示您是您搜索的颁发者。
它应该是这样的:
https://<b2c_tenant_name>.b2clogin.com/<b2c_tenant_name>.onmicrosoft.com/v2.0
https://<b2c_tenant_name>.b2clogin.com/<b2c_tenant_guid>.onmicrosoft.com/v2.0
https://<b2c_tenant_name>.b2clogin.com/<b2c_tenant_name>.onmicrosoft.com/SignUpAndSignInPolicyName/v2.0
https://login.microsoftonline.com/<b2c_tenant_name>.onmicrosoft.com/v2.0
为Azure 函数和 Angular 应用选择 b2clogin.com 和 login.microsoftonline.com 也可能是一个好主意。我不认为你可以像这样混合它们。
如果仍有问题,可以尝试将其作为范围而不是/user_impersonation
:
https://<b2c_tenant_name>.onmicrosoft.com/API/.default
或者尝试将https://<b2c_tenant_name>.onmicrosoft.com/API/user_impersonation
添加到 Azure 函数中允许的受众。
与您描述的相同问题 尽管发布了回复,但我能够通过将权限更改为:
https://<b2c_tenant_name>.b2clogin.com/tfp/<b2c_tenant_name>.onmicrosoft.com/<SignUpAndSignInPolicyName>
标准的一个(https://<b2c_tenant_name>.microsoftonline.com/tfp/<b2c_tenant_name>.onmicrosoft.com/<SignUpAndSignInPolicyName>
)导致我在尝试在我的函数应用上使用令牌时获得401
编辑:添加代码示例
虽然我的代码使用 react 环境变量,但它都只是 JS,并且在角度应用程序中应该工作相同。
import * as Msal from 'msal';
/** @type {import('msal').Configuration} */
const msalConfig = {
auth: {
clientId: process.env.REACT_APP_CLIENT_ID,
authority: 'https://<b2c_tenant_name>.b2clogin.com/tfp/<b2c_tenant_name>.onmicrosoft.com/<SignUpAndSignInPolicyName>',
validateAuthority: false,
navigateToLoginRequestUrl: false,
},
cache: {
cacheLocation: 'localStorage',
storeAuthStateInCookie: true,
},
};
/** @type {import('msal').AuthenticationParameters} */
const reqParams = {
scopes: [process.env.REACT_APP_SCOPE],
};
const clientApplication = new Msal.UserAgentApplication(msalConfig);
clientApplication.handleRedirectCallback((error, response) => {
if (error) {
if (error.message.indexOf('AADB2C90118') >= 0) {
//User clicked forgot password
clientApplication.authority = 'https://<b2c_tenant_name>.b2clogin.com/tfp/<b2c_tenant_name>.onmicrosoft.com/<ResetPasswordPolicyName>';
clientApplication.loginRedirect(reqParams);
return;
}
return console.error(error);
}
});
我的解决方案是将标准设置中 azure 函数的安全性更改为匿名(从函数)...似乎除了持有者令牌之外,它还期待一个功能代码......我花了 5+ 个小时才找到答案,因为我所有的注意力都集中在 JWT 访问令牌或 AADB2C 配置等可能出现问题的地方......
哎呀,也许我在错误的线程中发布了这个,我实际上得到了 401......
问题实际上出在 Web API (https://fabrikamb2chello.azurewebsites.net/hello) 上。该示例正在调用此受保护的资源,但该示例已对域login.microsoftonline.com
进行了硬编码,这会导致护照库(中间件)出现问题,该库提取并验证access_token,将access_token中的声明传播到验证回调,并让框架完成剩余的身份验证过程。
由于 Web API 使用不同的颁发机构,因此护照库无法验证令牌,因为login.microsoftonline.com
和{tenantName}.b2clogin.com
之间的颁发机构 URL 格式不同。例如,当使用{tenantName}.b2clogin.com
时,必须包含策略,但是当以前使用login.microsoftonline.com
时,没有必要在护照的URL中包含策略来验证令牌。
我们将尽快更新 Web API 和所有受影响的示例。您可以通过定位此分支来尝试 api 调用,该分支在 localhost:5000 上运行。在代码中,更新 api 终结点以命中ApiEndpoint = "http://localhost:5000/hello"
。修复示例后将在此处更新。