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 B2C和Node.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
。因此,我想了解如何解决此问题。
Sign-up and Sign-in
用户流策略
<SignUpAndSignInPolicyName>
已创建 Angular 应用
@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 Function App
已在 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");
}
}
}
API
应用程序(即,将其命名为“ API ”) https://<functionappname>.azurewebsites.net/.auth/login/aad/callback
/.auth/login/aad/callback
https://<b2c_tenant_name>.onmicrosoft.com/API
SPA
应用程序(即,将其命名为“ SPA ”) API
应用API
API
应用程序的应用程序ID https://<b2c_tenant_name>.b2clogin.com/<b2c_tenant_name>.onmicrosoft.com/v2.0/.well-known/openid-configuration?p=<SignUpAndSignInPolicyName>
在 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'],
})
创建一个名为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
响应Reply URL
更新为SPA
静态网站URL并发布了SPA文件,则调用API函数时同样会返回401
。所以我不确定配置错误。有什么想法吗?
答案 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"
。修复示例后,将在此处更新。