Angular site hosted on an Azure storage account as a static website receives 500 when it's invoking an Azure B2C-protected Function App function. The function is receiving a 404.
Update
The original title for this question was "Angular app which invokes B2C-secured Functions App receives 401 Unauthorized
response". The solution was, as @Alex AIT suggested (below), to replace the https://<b2c_tenant_name>.b2clogin.com/<b2c_tenant_name>.onmicrosoft.com/v2.0/.well-known/openid-configuration?p=<SignUpAndSignInPolicyName>
in the Function App's Issuer URL with https://<b2c_tenant_name>.b2clogin.com/<b2c_tenant_name>.onmicrosoft.com/v2.0/
. I.e., remove the trailing .well-known/openid-configuration?p=<SignUpAndSignInPolicyName>
segments. In a subsequent chat session, Alex pointed out that the policy is part of the path such as https://<tenantname>.b2clogin.com/<tenantname>.onmicrosoft.com/<policyname>/v2.0
or https://<tenantname>.b2clogin.com/<tenantguid>/<policyname>/v2.0
. However either of those paths for the Function App's Issuer URL reverts to the 401 response.
After resolving the 401 issue, the Angular SPA app now receives 500. However, the invoked API function is receiving a 404. The Function App's log stream indicates 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.
So the policy is not getting attached.
My objective is to establish a secure, serverless Angular web application which is statically-hosted on an Azure Storage website (i.e., within the storage account's $web
container). There are two projects: a public SPA
Angular 7+ project and a protected API
Function App project. Because Azure storage account static websites only allow public anonymous access to all files, the Angular app-hosting blob container's files (the website's files) are not secured. But the Angular app's invocations of the Azure Functions API calls are secured. The Function App API
project is protected via Azure AD B2C authentication.
Toward this end, I have attempted to adapt the technique outlined in Single-Page Application built on MSAL.js with Azure AD B2C and Node.js Web API with Azure AD B2C. I was able to get these samples to run. And furthermore I was able to modify their settings to authenticate against my own Azure B2C tenant (rather than against Microsoft's B2C tenant) and run them locally. But I didn't attempt to deploy these sample projects to Azure and figure out the required tweaks for the settings. I skipped the deployment exercise because I'm not a Node.js developer.
But my subsequent adaptation of the code in those (Node.js) sample projects to my statically-hosted Angular SPA project and to my Azure Functions API project is yielding 401 Unauthorized
whenever the API is invoked from the SPA. So I would like to understand how to resolve this issue.
The setup
Assumptions/Prerequisites
- An Azure B2C Tenant has been created
- Identity providers have been configured for the B2C Tenant
- A
Sign-up and Sign-in
user flow policy has been configured for the B2C Tenant- Make a note of its name. We'll refer to it's name below as
<SignUpAndSignInPolicyName>
- Make a note of its name. We'll refer to it's name below as
- An Azure Storage account has been created with its Static website feature enabled
An Angular app has been created
- The
@azure/msal-angular
package has been installed In
app-routing.module.ts
,- the
useHash
option has been set:imports: [RouterModule.forRoot(routes, { useHash: true })],
- Hash-routing is necessary to accommodate static hosting
- A secure component has been created and a protected-route established
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' }, ];
- the
- The
An Azure Function App has been created
- Make a note of the Function App's URL
The following function has been created in the Function App for testing purposes. And it's been published to 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 Tenant
API Application- Create the
API
Application (i.e., name it “API”) - Make a note of its Application ID
- The Application ID will be used later in the Function App's AAD Auth settings
- Set Include Web App / Web API to Yes
- Set the Allow implicit flow to Yes
- Set the Reply URL to
https://<functionappname>.azurewebsites.net/.auth/login/aad/callback
- Suffix the Function App's URL with
/.auth/login/aad/callback
- Suffix the Function App's URL with
- Set the App ID URI segment to “API”
- Yields:
https://<b2c_tenant_name>.onmicrosoft.com/API
- Yields:
- Create the
SPA
Application (i.e., name it “SPA”) - Set Include Web App / Web API to Yes
- Set Allow implicit flow to Yes
- Set the Reply URL to http://localhost:4200
- In the API access tab, add the
API
application API- The only available scope “Access this app on behalf of the signed-in user (user_impersonation)” will be pre-selected
Primary (Non-B2C) Tenant
Function App- In the Authentication / Authorization blade,
- Set App Service Authentication to On
- Set Action to take when request is not authenticated to Log in with Azure Active Directory
- In the Authentication Providers section, configure the Azure Active Directory provider as follows:
- Set Management Mode to Advanced
- Set Client ID to the B2C
API
application's Application ID - Set the Issuer Url to
https://<b2c_tenant_name>.b2clogin.com/<b2c_tenant_name>.onmicrosoft.com/v2.0/.well-known/openid-configuration?p=<SignUpAndSignInPolicyName>
- Save these Authentication / Authorization settings
In the Angular application's
app.module.ts
NgModule
import property, set: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'], })
Create a component named
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>
Serve the app locally:
ng serve
- Click on the Secure link
- Which navigates to the
/secure
route - Which prompts the user to authenticate
- Which navigates to the
- Click on the
Fetch data from B2C-secured Azure function
button - Server returns a
401 Not Authorized
response - If the SPA app's
Reply URL
is updated to theSPA
static website URL and the SPA files are published, the401
likewise gets returned when the API function is called.
So I'm not sure what's configured wrong. Any ideas?
This is not the issuer of your tenant:
But if you open this URL in your browser, it will show your the issuer you search for.
It should be something like this:
It might also be a good idea to choose either b2clogin.com and login.microsoftonline.com for both the Azure Function and the Angular app. I don't think you can mix them like this.
If you still have issues, you could try this as a scope instead of
/user_impersonation
:Or try adding
https://<b2c_tenant_name>.onmicrosoft.com/API/user_impersonation
to the allowed audiences in the Azure Function.Had the same issue as you described an looking though the responses posted I was able to solve it by changing the authority to:
The standard one (
https://<b2c_tenant_name>.microsoftonline.com/tfp/<b2c_tenant_name>.onmicrosoft.com/<SignUpAndSignInPolicyName>
) was causing me to get a 401 when I tried to use the token on my Function appEdit : Adding code sample
While my code is using react environment variables it's all just JS and should work the same in an angular app.
The solution for me was to change the security for the azure function in the standard settings to Anonymous (from Function) ... It seems it was expecting also a function code in addition to the bearer token... Took me 5+ hours to find out, as all my focus was on what could be wrong with the JWT access token, or the AADB2C configs etc ...
Oops, maybe I posted this in the wrong thread, I was getting 401 actually ...