Secure an Aurelia Single Page App with Azure Active Directory B2C / MSAL

If you create a modern web application with an API / REST backend and a Single Page Application (SPA) as your frontend, that you want to run in the internet, you definitely don’t want to handle security / user management on your own. You will want to use a service like Auth0 or Azure Active Directory to handle authentication and/or authorization.

In this example, we will develop a basic Aurelia frontend application, that will be secured via Microsoft Authentication Library (MSAL) for JavaScript backed by a custom Azure Active Directory (B2C). The B2C directory is the identity store where users of our application will be stored. The Aurelia app will login to the B2C directory via MSAL and use an ID token provided after a successful login to call the web API (authorization token in the request header).

23
Implicit Flow

Let’s start by creating the directory…

Create a B2C Directory

The creation of an Azure Active Directory is quite simple. Log in to your Azure account and select “New”. Search for “Azure Active Directory B2C” and select “Create” –> “Create a new Azure AD B2C Tenant”. Enter your desired organization and domain name and select the country/region you want the directory to be located. In my example, the configuration looks like that:

2

After the tenant is created, we have to add an application that can access the directory. Therefore, go to the newly created B2C directory and select “Applications” in the settings blade. Click “Add” and enter a name for the application. Also, be sure to select “Include web app / web API”, because we want to use the “implicit flow” as shown in the intro section above. In the reply URL enter “http://localhost:9000“, because this is the URL our Aurelia app will be running on. After a successful login, Azure AD will redirect the browser to that given URL.

4
B2C Application Settings

Note down the Application ID and the domain name (https://{yourtenantname}.onmicrosoft.com – in my case https://aureliab2c.onmicrosoft.com), as we will need the values when we configure the authentication of our Web API.

Next, to give users the ability to register for the application, you have to create a policy within the directory that allows users to create an account. You also need a policy to sign in and – if you want to – a policy to reset the password for a user.

In this example, we will only use “local accounts” (login with username and password). You can also add further identity providers like Google or Facebook to add social login capabilities to your application. To keep it simple, we only support local accounts at the moment.

Fortunately, we can create a policy that allows users to add an account and configure the login process in one single configuration. It’s called “Sign-up or sign-in policy”.

Click on the corresponding entry in the configuration blade and add a new one.

Next, you have to enter information about the identity provider to use (in our case “Local Account”, the signup attributes (fields a user can enter during signup like “Given Name” and “Surname”) and application claims (information that will be sent with the identity token after login). In our application, the configuration is as follows:

5
Identity Provider
6
Signup Attributes
7
Application Claims

Of course, you can adjust a lot more settings, but for our case, this is enough in the first place.

After you have created the policy, you can test it by clicking on the “Run Now” button and create a user for yourself.

19
Login Page / B2C
20
Signup Page

As a last step, note down the name of the policy you have just created – in our case: B2C_1_signupin. We need it later on…

Our directory is now ready to go, so let’s head over to the API…

Create REST API

The REST API we want to develop is based on ASP.NET Core 2.0 and will be secured by the Azure AD we just created. To achieve this in a very convenient way, you can enter the connection to the Azure AD during project setup.

In Visual Studio, open “File –> New — Project” and select “ASP.NET Core Web Application” and in the following wizard, select “Web API”.

Create Web API
Create Web API

To enter the B2C data you noted down in the last steps, click on the “Change Authentication” button, select “Individual User Accounts” and enter the following details:

  • the domain name of your B2C tenant
  • the application ID that corresponds to the API
  • the policy to use
11
Auth Configuration

The wizard adds settings to you appsettings.json file and configures you startup class to use the B2C information you just entered.

{
"AzureAdB2C": {
"Instance": "https://login.microsoftonline.com/tfp/",
"ClientId": "8b2b1e43-a2f7-4538-afbf-9b7ac293ea1f",
"Domain": "aureliab2c.onmicrosoft.com",
"SignUpSignInPolicyId": "B2C_1_signupin"
},
"Logging": {
"IncludeScopes": false,
"Debug": {
"LogLevel": {
"Default": "Warning"
}
},
"Console": {
"LogLevel": {
"Default": "Warning"
}
}
}
}
view raw apsettings.json hosted with ❤ by GitHub
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddAzureAdB2CBearer(options => Configuration.Bind("AzureAdB2C", options));
services.AddMvc();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseCors(builder =>
{
builder.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod();
});
app.UseAuthentication();
app.UseMvc();
}
view raw startup.cs hosted with ❤ by GitHub

While we want to focus on the authentication process and securing the backend, the API will only provide one controller that can be called on the path “people“. The GET request on that path will return a list of characters (from StarWars đŸ˜‰ – well, just sample data). As the controller is annotated with the Authorize attribute, requests will only be successful, if the caller adds the Authorization header with a valid ID / Bearer token. The token will be provided by the AAD B2C directory after a successful login. More one that later when we implement the Aurelia app.

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Backend.Controllers
{
[Authorize]
[Route("api/[controller]")]
public class PeopleController : Controller
{
[HttpGet]
public IActionResult Get()
{
return Ok(SampleData.PeopleList);
}
}
}
using System.Collections.Generic;
namespace Backend
{
public static class SampleData
{
public static List<object> PeopleList = new List<object>()
{
new
{
Name = "Master Yoda",
Email = "master@yoda.com",
Id = 1
},
new
{
Name = "Luke Skywalker",
Email = "luke@skywalker.com",
Id = 2
},
new
{
Name = "Darth Vader",
Email = "darth@vader.com",
Id = 3
},
new
{
Name = "Leia Organa",
Email = "leia@organa.com",
Id = 4
},
};
}
}
view raw SampleData.cs hosted with ❤ by GitHub

So, as I said…if you want to call the people controller without a valid token, you will receive a 401 status code (Unauthorized) – the way we want it. To test that behavior, we can use Postman.

13
401 Unauthorized / Call API without a Bearer Token

The backend is now secured by the Azure B2C directory…so let’s add the frontend.

Create the Aurelia SPA

Before we can start developing the frontend application with Aurelia, we have to install the command line interface (CLI). To do this, open a command prompt an enter the following command:

npm install -g aurelia-cli@latest

This command will install the Aurelia CLI globally on your machine. After the command returns, we can create a new Aurelia application via

au new frontend

The command (btw. frontend is the name of our application) will start a wizard where you can select different options, e.g. if you want to use plain JavaScript or TypeScript.

15
Start the wizard
16
Select Options

We will use TypeScript for our application.

After you have entered all the necessary information, you can finish the wizard and install the dependencies (you will be ask by the wizard) afterwards.

The result will be an Aurelia application with minimal dependencies, built with TypeScript. You can test the application by running…

au run --watch

and opening the browser, pointing to http://localhost:9000.

To be able to call the API, request an ID token at the B2C directory and display the results from the people controller, we need a few more dependencies.

First, we need the current version of the MSAL JS library, which is – at the time of writing – still in preview for JavaScript. You can install it via NPM, but the current version available (0.11) has a few bugs. Therefore, we will take the current dev version directly from the GitHub repository đŸ™‚

You can download the version from https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/out/msal.js and save it in a “lib” folder in the root directory of the Aurelia app.

Next, we need the Aurelia fetch library and jwt_decode (to decode the ID token we receive after login from the B2C directory). Install both dependencies via

npm install aurelia-fetch-client@latest jwt_decode@latest --save

Because we are “compiling” the application via the Aurelia CLI, we also need to add these additional libraries to the aurelia.json file located in the “aurelia_project” folder. Insert entries for the MSAL lib (line 4), aurelia-fetch-client (line 12) and jwt_decode (starting line 47) in the dependencies section of the JSON file.

"prepend": [
"node_modules/bluebird/js/browser/bluebird.core.js",
"node_modules/aurelia-cli/lib/resources/scripts/configure-bluebird.js",
"node_modules/requirejs/require.js",
"lib/msal.js"
],
"dependencies": [
"aurelia-binding",
"aurelia-bootstrapper",
"aurelia-dependency-injection",
"aurelia-event-aggregator",
"aurelia-fetch-client",
"aurelia-framework",
"aurelia-history",
"aurelia-history-browser",
"aurelia-loader",
"aurelia-loader-default",
"aurelia-logging",
"aurelia-logging-console",
"aurelia-metadata",
"aurelia-pal",
"aurelia-pal-browser",
"aurelia-path",
"aurelia-polyfills",
"aurelia-route-recognizer",
"aurelia-router",
"aurelia-task-queue",
"aurelia-templating",
"aurelia-templating-binding",
{
"name": "aurelia-templating-resources",
"path": "../node_modules/aurelia-templating-resources/dist/amd",
"main": "aurelia-templating-resources"
},
{
"name": "aurelia-templating-router",
"path": "../node_modules/aurelia-templating-router/dist/amd",
"main": "aurelia-templating-router"
},
{
"name": "aurelia-testing",
"path": "../node_modules/aurelia-testing/dist/amd",
"main": "aurelia-testing",
"env": "dev"
},
"text",
{
"name": "jwt-decode",
"path": "../node_modules/jwt-decode/lib",
"main": "index"
}
]
view raw aurelia.json hosted with ❤ by GitHub

Now we are ready to implement the application. As we want to focus on the authentication process, I will concentrate on the pieces that are important for our case.

  1. Create a settings file, where we can access the B2C settings during runtime.
  2. Add a class (in auth.ts file) that is responsible for handling the authentication process
    1. Check, if user is already authenticated
    2. Get the current ID / JWT token
    3. Login via B2C directory
    4. Logout
  3. Add a class (HttpConfig) that will configure the Aurelia HttpClient class to intercept each request and insert the Authentication header (Bearer token)
import {HttpClient} from 'aurelia-fetch-client';
import {inject} from 'aurelia-framework';
import settings from './settings';
import * as jwt_decode from 'jwt-decode';
declare const Msal : any;
@inject(HttpClient)
export class Auth {
private httpClient : HttpClient;
public authenticated : boolean;
private clientApplication : any;
constructor(httpClient) {
this.httpClient = httpClient;
this.authenticated = false;
this.clientApplication = new Msal.UserAgentApplication(settings.clientId, settings.authority, (errorDesc, token, error, tokenType) => {
if (token) {
this.authenticated = true;
} else {
this.login();
}
}, {cacheLocation: 'localStorage', postLogoutRedirectUri: 'http://localhost:9000/#&#39; });
}
public login() {
window.location.hash = '';
this
.clientApplication
.loginRedirect(['openid']);
}
public logout() {
this.authenticated = false;
this
.clientApplication
.logout();
}
public getToken() : string {
if (this.authenticated) {
return this._getTokentInternal();
}
return null;
}
public getDecodedToken() {
let token = this.getToken();
return jwt_decode(token);
}
private _getTokentInternal() : string {
let user = this
.clientApplication
.getUser();
let ar = new Msal.AuthenticationRequestParameters(this.clientApplication.authorityInstance,
this.clientApplication.clientId, [settings.clientId],
'id_token', this.clientApplication.redirectUri);
let token = this
.clientApplication
.getCachedToken(ar, user);
return token.token;
}
isAuthenticated() {
return new Promise((resolve, reject) => {
let cachedUser = this
.clientApplication
.getUser();
if (cachedUser == null) {
this.authenticated = false;
return reject();
}
let token = this._getTokentInternal();
if (token) {
this.authenticated = true;
return resolve();
} else {
return reject();
}
});
}
}
view raw auth.ts hosted with ❤ by GitHub
import {Auth} from './auth';
import {HttpClient} from 'aurelia-fetch-client';
import {inject} from 'aurelia-framework';
@inject(HttpClient, Auth)
export class HttpConfig {
private http : HttpClient;
private auth : Auth;
constructor(http, auth) {
this.http = http;
this.auth = auth;
}
configure() {
let a = this.auth;
this
.http
.configure(httpConfig => {
httpConfig
.withDefaults({
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
})
.withInterceptor({
request(request) {
if (a.authenticated) {
let token = a.getToken();
token = `Bearer ${token}`;
request
.headers
.append('Authorization', token);
}
return request;
},
response(response) {
if (response.status === 401) {
a.login();
}
return response;
}
});
});
}
}
view raw http-config.ts hosted with ❤ by GitHub
let config = {
service: 'http://localhost:7079/api/&#39;,
clientId: '8b2b1e43-a2f7-4538-afbf-9b7ac293ea1f',
authority: 'https://login.microsoftonline.com/tfp/aureliab2c.onmicrosoft.com/B2C_1_signupin&#39;
};
export default config;
view raw settings.ts hosted with ❤ by GitHub

The Auth class is the “main actor” in our frontend example. It uses the MSAL class UserAgentApplication to login & logout…as well as getting the ID token after a successful login attempt.

As we want to secure the complete frontend application and not only a few path/routes, we use the Auth class in the bootstrapping process of the Aurelia app – in the main.ts file.

aurelia
.start()
.then((a) => {
// get auth object from Aurelia container
let auth : Auth = a
.container
.get(Auth);
setTimeout(() => {
auth
.isAuthenticated()
.then(() => {
a.setRoot();
return;
})
.catch(() => {
auth.login();
});
}, 200);
});
view raw main.ts hosted with ❤ by GitHub

As you can see, after loading the Aurelia framework, we check if the user is already logged in (by calling isAuthenticated on the auth object). If the promise is rejected (user is not logged in), we redirect to the AAD login page by invoking auth.login. After entering the username and password and submitting the form, we will be redirected with an ID token as URL parameter (id_token) to our app. The MSAL library picks up the id_token and handles expiration checking and storing all the necessary data for us. If everything is ok, the next call to isAuthenticated will successfully resolve and the app loads the main class App (in app.ts). While loading the view and view-model, a call to the web API is invoked (in method activate, line 4 – one of the lifecycle methods of an Aurelia view. activate is called before the view is visible/attached to the DOM).

activate() {
return this
.httpClient
.fetch(settings.service + 'people')
.then((response) => {
return response
.json()
.then((data) => {
this.people = data;
this.decodedToken = this
.auth
.getDecodedToken();
let exp = this.decodedToken['exp'];
this.expires = new Date(0);
this.expires.setUTCSeconds(exp);
});
});
}
view raw app.ts hosted with ❤ by GitHub

In the .NET Core application, the request headers are checked for a valid Bearer token. If the token provided is valid, the call is routed to the people controller and the results are sent to the Aurelia app, which in turn displays the list of…StarWars characters đŸ™‚

To see the contents of the ID token in the frontend, we decode the token in the App class (see above) and display the results (given name, surname, email and token expiration date) in a message box.

22
Final Application

If the Logout button in the top menu is clicked, auth.logout is called, the MSAL library clears stored data about the ID token and endpoints and redirects to http://localhost:9000/#.

Wrap Up

As you have seen, it is possible to secure a REST API and a corresponding Single Page Application – in our case an Aurelia app – with Azure Active Direct B2C. Of course, the app is far away from “production-ready”, but it is a good example to get an impression of how you can integrate an OpenID Connect flow in your own application and secure a SPA in a very simple way.

You can find the complete sample on GitHub: https://github.com/cdennig/azure-b2c-aurelia-example

Have fun with it…

Cheers!

13 Replies to “Secure an Aurelia Single Page App with Azure Active Directory B2C / MSAL”

  1. Great walkthrough, thanks for putting this together. The last 2 days I’ve been trying to do something similar to this with a React SPA, .NET Core 2 Web API and Azure AD. I’ve followed your example, but couldn’t quite get the Web API to accept the token that I pass it.

    The new MSAL library seems a bit different, there is an aquireToken method, do you need to acquire a new token for the call to the web api?

    Like

    1. Hi Daniel,

      the method you are referring to, is to retrieve an access_token with user interaction (as far as I remember). If you log in via this.clientApplication.loginRedirect([‘openid’]); you should be able to retrieve an id_token via the callback you register when creating the Msal.UserAgentApplication object. But you are right, there is a change in the new version of MSAL…it’s not possible (at least I did not manage) to retrieve the id_token from the cache. You can workaround this problem, by getting the id_token from localStorage (let token = localStorage.getItem(‘msal.idtoken’);). But be aware…you have to check, if the token is still valid (not expired). You can use the npm package “jwt-decode” e.g. for this…

      If your API still doesn’t accept the token, then there is a misconfiguration in the authentication middleware on ASP.NET core and you should have a look there…

      Cheers,
      Christian

      Like

  2. Christian, thanks tons for this walk through — it was a big help to me. I’m curious about a couple things. First, why did you need to include the setTimeout in main.js — is that necessary? In my case, I’m actually using Msal to target our organizational AAD, so I’m using loginPopup() in my login() function. One thing I’ve noticed is that my logout function pops up a window asking which user to log out — even though there is only one user logged in — is that normal or is there something wrong in the way I’m configured?

    Like

    1. Hi Rodd,

      I’m glad to hear it helped you! To be honest, the setTimeout in main.js is just a workaround, to “ensure” that the login callback in auth.ts has been called, before I invoke isAuthenticated() in main.ts. There where situations I ran into this problem…well, definitely nothing you would do in a prod application of course…

      The logout issue seems to be normal, currently. But hey, MS engineering is listening đŸ™‚ feel free to open an issue in the Github repo. I’m sure they appreciate it.

      Cheers!

      Like

      1. Hi, I implemented the Msal library for js in my Aurelia project following your example and I am having the issue you mentioned with the callback function not being called before the isAuthenticated(). In my case the code works fine on Edge and Chrome however in Firefox the callback function does not seem to be called at all, even if I wait for a few seconds. I believe that this might be caused due to something specific in my project, for your code from Github runs ok on Firefox. Do you have any idea what would prevent the callback function from being called or potentially what would cause that call to be delayed for too long?

        Like

  3. …strange thing is, if i alternate the value for changeLocation btw sessionStorage and localStorage, the code is likely to work…

    Like

  4. Christian, this worked with MSAL v0.1.1 but doesn’t work when using the v.0.1.3 library. Have you tried integrating the v.0.1.3 library with Aurelia at all? I’ve tried but am having a terrible time getting things working — especially if I try using loginPopup. Would love to know if you’ve ever got that working.

    Like

    1. Hi Rodd,

      I’ve another project running on the current MSAL version, but with redirecting to the identity provider as in this example. Do you defintetly need the “popup flow”? Redirect should be preferred, especially when it comes to using the web app on a mobile.

      What kind of errors do you see?

      Like

  5. I finally did implement the redirect instead of the Popup and I have things working. loginPopup was opening the popup, I’d log in, the popup would close and another would open and sometimes even a third. Sometimes the app would begin loading in one of the popups. It was extremely weird behavior.

    Did you have to change the _getTokenInternal() function of your Auth class? I seem to remember getting errors when trying to instantiate AuthenticationRequestParameters. I’ll have to try that again — I tried so many different things that I may have had something else going on as well.

    Like

    1. Yes, you are right. I had to adjust the token determination due to the same errors. I saved the access and id token in properties of the auth-manager class after getting them from the MSAL lib…so basically I avoided using AuthenticationRequestParameters instances…

      Like

      1. Since you’re storing the token, will your app handle token expiration? I thought the idea was to use acquireTokenSilent so that MSAL would handle requesting new tokens if the token expired. If I’m thinking about this correctly, your code would just require the user to log in again if the token is expired?

        Like

      2. Thanks for your feedback. I’m kind of new to MSAL so I wanted to make sure I was understanding what was going on. Sure appreciate this example and your willingness to give feedback!

        Like

Leave a Reply to Christian Dennig Cancel reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: