调用 B2C 保护的函数应用 API 的 Angular 应用接收 500,函数接收 404


托管在

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.c‌​om/<policyname>/v2.0https://<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容器内)。有两个项目:一个公共SPAAngular 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。所以我想了解如何解决这个问题。

设置的

假设/先决条件

  1. 已创建 AzureB2C 租户
  2. 已为B2C 租户配置标识提供者
  3. 已为B2C 租户配置了Sign-up and Sign-in用户流策略
    • 记下其名称。我们将在下面将其名称称为<SignUpAndSignInPolicyName>
  4. 已创建启用了静态网站功能的 Azure 存储帐户
  5. 已创建角度应用程序

    已安装
    1. @azure/msal-angular
    2. 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' },
      ];
      
  6. 已创建 Azure函数应用

    • 记下函数应用的URL
  7. 出于测试目的,已在函数应用中创建了以下函数。它已发布到 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 租户

接口应用
  1. 创建API应用程序(即将其命名为">API")
  2. 记下其应用程序
    • ID 稍后将在函数应用的 AAD 身份验证设置中使用应用程序 ID
  3. "包括 Web 应用/Web API"设置为"是
  4. "
  5. 将">允许隐式流"设置为"是
  6. "
  7. 回复URL 设置为https://<functionappname>.azurewebsites.net/.auth/login/aad/callback函数
    • 应用的URL 后缀/.auth/login/aad/callback
  8. 应用 ID URI段设置为"API">
    • 产生:https://<b2c_tenant_name>.onmicrosoft.com/API
水疗申请
  1. 创建SPA应用程序(即将其命名为">SPA")
  2. "包括 Web 应用/Web API"设置为"是
  3. "
  4. 将">允许隐式流">设置为"是
  5. "
  6. 回复 URL设置为http://localhost:4200
  7. 在 API访问选项卡中,添加API应用程序 API
    • 将预先选择唯一可用的范围"代表登录用户 (user_impersonation) 访问此应用

主(非 B2C)租户

函数应用
  1. "身份验证/授权">边栏选项卡中,
    • 应用服务身份验证设置为"开">
    • 请求未通过身份验证时要执行的操作设置为使用 Azure 活动目录登录
    • "身份验证提供程序"部分中,按如下所示配置Azure Active Directory提供程序:
      • "管理模式"设置为"高级">
      • 客户端ID 设置为 B2CAPI应用程序的应用程序ID
      • 颁发者 URL设置为https://<b2c_tenant_name>.b2clogin.com/<b2c_tenant_name>.onmicrosoft.com/v2.0/.well-known/openid-configuration?p=<SignUpAndSignInPolicyName>
  2. 保存这些身份验证/授权设置
Azure Application
  1. Angular应用程序的app.module.tsNgModule导入属性中,设置:

    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'],
    })
    
  2. 创建名为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>
      
  3. 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>&nbsp;<span class="acronym">(B2C)</span> authentication. The site interacts with a secure
    <em>Azure Functions</em>&nbsp;<span class="acronym">API</span>.
    </mat-card>
    <p style="text-align: center;"><a routerLink="/" routerLinkActive="active">Home</a>&nbsp;&nbsp;<a routerLink="/secure" routerLinkActive="active">Secure</a></p>
    <p style="text-align: center;"><router-outlet></router-outlet></p>
    
  4. 在本地提供应用:ng serve

  5. 单击安全链接
    • 导航到/secure路线
    • 提示用户进行身份验证
  6. 单击Fetch data from B2C-secured Azure function按钮
  7. 服务器返回401 Not Authorized响应
  8. 如果 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"。修复示例后将在此处更新。

最新更新