调用受B2C保护的功能的Angular应用会收到“ 401未经授权”响应

时间:2019-06-02 02:46:01

标签: angular azure-active-directory azure-ad-b2c msal msal.js

Azure存储帐户上作为静态网站托管的

Angular 网站在调用受Azure B2C保护的Azure时收到Not Authorized (401) Function App function

我的目标是建立一个安全的,无服务器的Angular Web应用程序,该应用程序静态托管在Azure存储网站上(即在存储帐户的$web容器内)。有两个项目:一个 public SPA Angular 7 + 项目和一个受保护的 API Function App 项目。因为Azure存储帐户静态网站仅允许公共匿名访问所有文件,所以不保护 Angular 应用程序托管Blob容器的文件(网站的文件)。但是 Angular 应用程序对 Azure函数 API调用的调用是安全的。 Function App API项目通过Azure Active Directory(AAD)企业对消费者(B2C)身份验证得到保护。

为此,我尝试采用Single-Page Application built on MSAL.js with Azure AD B2CNode.js Web API with Azure AD B2C中概述的技术。我能够运行这些样本。而且,我能够修改它们的设置以针对我自己的Azure B2C租户(而不是针对Microsoft的B2C租户)进行身份验证并在本地运行它们。但是我没有尝试将这些示例项目部署到Azure并找出设置所需的调整。我不是Node.js开发人员,因此跳过了部署练习。

但是,这些(Node.js)示例项目中后来对我的静态托管的Angular SPA项目以及我的Azure Functions API项目进行的代码修改,无论何时从SPA调用API,都会产生401 Unauthorized。因此,我想了解如何解决此问题。

设置

假设/先决条件

  1. 已创建Azure B2C租户
  2. 已为 B2C租户 配置了
  3. 身份提供者
  4. 已为 B2C租户配置了Sign-up and Sign-in 用户流策略
    • 记下其名称。我们在下面将其称为<SignUpAndSignInPolicyName>
  5. 已创建一个Azure存储帐户并启用了静态网站功能
  6. 已创建 Angular 应用

    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' },
             ];
      
  7. 已创建一个Azure Function App

    • 记下 Function App 的URL
  8. 已在 Function App 中创建了以下功能用于测试。它已发布到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应用
  1. 创建API应用程序(即,将其命名为 API ”)
  2. 记下其应用程序ID
    • 应用程序ID 稍后将在Function App的AAD身份验证设置中使用
  3. Include Web App / Web API 设置为
  4. 允许隐式流设置为
  5. 答复URL 设置为 https://<functionappname>.azurewebsites.net/.auth/login/aad/callback
    • 在{em> Function App 的URL后加上/.auth/login/aad/callback
  6. 应用程序ID URI 段设置为 “ API”
    • 收益率:https://<b2c_tenant_name>.onmicrosoft.com/API
SPA应用
  1. 创建SPA应用程序(即,将其命名为 SPA ”)
  2. Include Web App / Web API 设置为
  3. 允许隐式流设置为
  4. 答复URL 设置为 http://localhost:4200
  5. API访问标签中,添加API应用API
    • 将唯一选择范围“代表已登录用户(user_impersonation)访问此应用”

主要(非B2C)租户

功能应用
  1. “身份验证/授权” 刀片中,
    • App服务身份验证设置为
    • 未经身份验证的请求采取的操作设置为 使用Azure Active Directory登录
    • 身份验证提供程序部分中,如下配置 Azure Active Directory 提供程序:
      • 管理模式设置为 高级
      • 客户端ID 设置为B2C API应用程序的应用程序ID
      • 发卡人网址设置为 https://<b2c_tenant_name>.b2clogin.com/<b2c_tenant_name>.onmicrosoft.com/v2.0/.well-known/openid-configuration?p=<SignUpAndSignInPolicyName>
  2. 保存这些身份验证/授权设置
Azure应用程序
  1. Angular 应用程序的app.module.ts NgModule import 属性中,设置:

    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

所以我不确定配置错误。有什么想法吗?

4 个答案:

答案 0 :(得分:0)

这不是您租户的发行人:

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 Function和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函数中允许的受众。

答案 1 :(得分:0)

发生了与您描述的问题相同的问题,尽管发布的回复我可以通过将权限更改为来解决它:

  

https://<b2c_tenant_name>.b2clogin.com/tfp/<b2c_tenant_name>.onmicrosoft.com/<SignUpAndSignInPolicyName>

当我尝试在我的Function应用程序上使用令牌时,标准的(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);
    }
});

答案 2 :(得分:0)

对我来说,解决方案是将标准设置中azure函数的安全性更改为Anonymous(来自Function)...似乎除了承载令牌之外,还期望有一个功能代码... Took me 5 +几个小时来找出答案,因为我的全部精力都放在了JWT访问令牌或AADB2C配置等可能出错的地方上……

糟糕,也许我将其发布在错误的线程中,实际上我正在获取401 ...

答案 3 :(得分:0)

问题实际上出在网络API(https://fabrikamb2chello.azurewebsites.net/hello)上。该示例正在调用此受保护的资源,但是该示例具有hard coded the domain login.microsoftonline.com,该示例随后导致护照库(中间件)出现问题,该护照库提取并验证access_token,并将access_token中的声明传播至验证回调并让框架完成剩余的身份验证过程。

由于Web api使用的权限不同,因此护照库无法验证令牌,因为login.microsoftonline.com{tenantName}.b2clogin.com之间的授权URL格式不同。例如,在使用{tenantName}.b2clogin.com时,必须包括该策略,但是在先前使用login.microsoftonline.com时,则不必在用于护照的URL中包括该策略以验证令牌。

我们将尽快更新网络api和所有受影响的示例。您可以通过targeting this branch试用api调用,该调用在localhost:5000上运行。在代码中,更新api端点以打ApiEndpoint = "http://localhost:5000/hello"。修复示例后,将在此处更新。