# SPA Authorization

In this article we will finally start to code the solution. Our environment was set up and we are ready to use the created entities (application, grant) to our advantage.

Before we jump to the code editor, let's look at the end result that we are aiming for:

The application we are about to build

The application will consist of six modules/boxes that we will be creating in this tutorial series. Each module will be crated to teach you some new concept in the process. In this article, we will focus on the first one, which you can see above - Authorization. Its role is to log the user in and display an access token for further inspection.

WARNING

In a real application it is not recommended to allow the users to view the access tokens. However, for our tutorial needs, it was desired.

# Architecture

Let's look briefly at a schema of the app that we will build to understand what are its building blocks:

Building blocks of the tutorial application

There seems to be a lot going on, however, the overall project is rather simple. Each "module" on th page is one component (there is 6 of them). Each component uses some services. All of them rely on the AuthService in order to retrieve an access token for communication with the API. The other services expose selected Ability API functionalities.

App component

The diagram skips the App component, which is at the root of the application.

In this article we will create AccessToken component and the AuthService class.

# Authorization Method

Ability Platform protects its APIs using the industry standard OAuth2 suite of protocols. The main idea behind it is that accessing API endpoints requires possession of an access token. Access token is basically an encoded string that allows the server to understand if you are authorized to do the operation that you requested (like getting some data, or updating it, etc.). OAuth2 defines a few authorization flows, each one targeting specific case. I.e., a Client Credentials flow is ideal for cases where the application should access some protected server without any user context. It could be some kind of web job running on schedule every hour, generating some reports based on data that it fetches from the server.

In this tutorial series we are building a SPA (Single Page App). These are web applications meant to be used in a browser with users logged in. Client Credentials flow would be a very bad choice here. Instead, we need to use some authorization flow which is suited for web apps that are accessed by users. We are going to go with Authorization Code with Proof Key for Code Exchange (PKCE) flow. It is currently the recommended flow to be used in the cases as ours. Additionally, using it will make you prepared for the upcoming OAuth2.1 update of the protocol, which is going to promote PKCE and drop support for some other flows, like implicit grant, which is considered legacy.

# App Initialization

As mentioned in the prerequisites, we will be using Vue.js framework to speed up the development of the app and skip the hassle of dealing with the HTML DOM elements. If you're familiar with any SPA component-based JS framework (i.e. Angular, React), you should be rather familiar with what we'll be doing.

TIP

One thing that needs to be made clear: the technologies that we're using during this tutorial (Node.js, Vue.js, etc.) were chosen due to their simplicity and clarity (in our humble opinion). We do not make any recommendations in this area - you are free to use any software development stack that your project requires and that your business department allows you to use.

# Vue CLI

Let's start off with installation of the Vue CLI, which will allow us to manage our app's dependencies through NPM. Additionally it will let us do things like:

  • defining and loading environment variables
  • running development server that reloads our app every time we change some code

Open your terminal and run the command:

npm install -g @vue/cli

This will install Vue CLI. You can verify that the installation was successful by running:

vue --version

If it returns a version, installation was successful. Navigate to the directory where you want the code files to be stored and run:

vue create ability-getting-started-spa

Obviously, you can call your app any way you like.

Select the "Default (Vue 3)" preset and continue (we will be using Vue 3). The whole process is described in detail here.

After these steps you should end up with a project structure as below:

Initial project structure

# Test run

Let's try to run the app as it is now. Run the following command in the terminal:

npm run serve

You should get a result similar to this one:

Vue CLI Running

When you follow the link http://localhost:8080 in your browser, you should see:

Vue running in the browser

If that works, you are on the right way. The command that you just run is very useful during development since it will automatically reload your app when you make changes to the source code.

# Packages

Let's install some packages that will be useful:

npm install @azure/msal-browser bootstrap

We'll be using msal-browser for authorization purposes. Bootstrap will allow us to not focus too much on CSS styling.

# Eslint Rule

Vue-CLI installed eslint package into your application (you can see that in package.json file). It is a JavaScript linter that statically analyzes your code and informs you if it finds any issues. By default, it is rather strict, i.e. it will throw an error (and disallow the application to run) if it finds some unused variables defined in your code. It might become problematic during the tutorial, and you might want to relax that rule. Here's an example of eslintConfig that we used in package.json:













 
 
 


"eslintConfig": {
  "root": true,
  "env": {
    "node": true
  },
  "extends": [
    "plugin:vue/essential",
    "eslint:recommended"
  ],
  "parserOptions": {
    "parser": "babel-eslint"
  },
  "rules": {
    "no-unused-vars": "off"
  }
}

It differs from the default configuration in a rule that we've added. Update your package.json with that small change and save the file. If your app is running, you might need to restart it for the new config to be applied (press Ctrl+C in your terminal window and rerun npm run serve). You will not see any difference in runtime, however, it might save you from some annoying errors while you're coding later on.

# Environment Variables

Our application will use a few values that are configuration kind of data. Instead of putting them all over our source code, let's keep them in one file, for easier access.

Create a file called .env (in the root directory of your project, the same one where the src folder lives).

TIP

We assume that you have bash shell installed, and the touch command is available. However, you can create the file in any other way you like.

touch .env

Inside of this file, add the following contents:

VUE_APP_AUTH_CLIENTID=
VUE_APP_AUTH_AUTHORITY=
VUE_APP_AUTH_B2C_DOMAIN=
VUE_APP_AUTH_SCOPE=
VUE_APP_AUTH_REDIRECT_URI=

All of the values above will be needed for the authorization process. For now, there are no values, we need to fill them in. Let's discuss each one of them.

Authorization Parameters

You can find a dedicated page on ADP that discusses OAuth 2.0 authorization parameters here.

# Client Id

The first parameter is VUE_APP_AUTH_CLIENTID. it is the ID of the application that you created in the Admin Portal, not so long ago. Go back to the Portal, navigate to your application and copy its Id:

ClientId in the Admin Portal

In the example above, the clientId is 89da7a96-cf57-492b-9abf-3c75f020c22c. Yours will be different.

# Authority

The VUE_APP_AUTH_AUTHORITY parameter is the URL of the authority endpoint for your tenant and solution. It has the following format:

https://<B2C_TENANT_NAME>.b2clogin.com/<B2C_TENANT_NAME>.onmicrosoft.com/B2C_1A_<TENANT_NAME>_RP

The B2C_TENANT_NAME is the name of the B2C tenant that is associated with your Ability Platform instance. You can find it in a table of platform parameters that you received when you got access to your platform instance.

Sandbox Users

Sandbox users can find the full B2C domain under "B2C Tenant Domain" parameter. The <TENANT_NAME> is under Tenant Name (if it has any spaces, replace them with dashes ("-")).

TIP

Azure Active Directory B2C is an identity management service that is used by Ability Platform. You can read more about B2C parameters here.

The TENANT_NAME is the name of the tenant in the Admin Portal that you are logging with (with spaces replaced with dashes). For most users it would be abb-ability. If you're a Sandbox user, go to the "Tenants" tab of the Admin Portal and copy the Name of your tenant (again, replace spaces with dashes).

An example of VUE_APP_AUTH_AUTHORITY could be:

https://Abilityrst15EunOps.b2clogin.com/Abilityrst15EunOps.onmicrosoft.com/B2C_1A_us-john-smith-tenant_RP

TIP

The above authority string will not work, you need to provide your own.

# B2C Domain

The VUE_APP_AUTH_B2C_DOMAIN parameter has the following format:

<B2C_TENANT_NAME>.b2clogin.com

Since you already had to use the <B2C_TENANT_NAME> value in the VUE_APP_AUTH_B2C_DOMAIN parameter, just reuse it here.

# Scope

Another string that you need to prepare is for the VUE_APP_AUTH_SCOPE parameter. Its format is:

https://<B2C_TENANT_NAME>.onmicrosoft.com/<SOLUTION_NAMESPACE>/instance

You should already know how to find the <B2C_TENANT_NAME>.

SOLUTION_NAMESPACE may be taken from the Admin Portal, in the "Details" view of your solution:

Solution namespace in the Admin Portal

The value is titled Namespace.

# Redirect URI

This one is simple. The value for VUE_APP_AUTH_REDIRECT_URI is the Redirect URI that you had to fill in while creating an application entity in the Admin Portal. It will be http://localhost:8080, since this is the default URL used by Vue CLI when running an app.

When you collect all of the values fill them into the .env file.

TIP

.env file is being read by Vue CLI when building your app and the values are being used in the appropriate places (which you will see in a moment). Make sure to restart the npm run serve command anytime you modify the .env file. Otherwise, the changes will not be picked up.

# Styles and main.js

Before we move on, let's create a style.css file in the src/ directory with the following contents:

body {
    background: #EBEBEB;
    color: #1F1F1F;
}
.app {
    display: flex;
    flex-direction: column;
    min-height: 100vh;
}
main {
    flex: 1;
}
.abb {
    color: rgb(234, 51, 40)
}
.card {
    box-shadow: 0 .125rem .25rem rgba(0,0,0,.075);
    transition: box-shadow 300ms;
}
.card:hover {
    box-shadow: 0 .5rem 1rem rgba(0,0,0,.15);
}

These styles will make our app look just a little bit better.

Additionally, modify the src/main.js file so that it looks like this:

import { createApp } from 'vue'
import App from './App.vue'
import 'bootstrap/dist/css/bootstrap.min.css';
import './style.css';

createApp(App).mount('#app')

Basically, we imported bootstrap together with our custom CSS file.

# AuthService Class

We'll start the actual coding from the AuthService class. This class will make use of the msal-browser package that we installed. AuthService will also make use of authConfig.json that we need to define.

TIP

Our AuthService is a simplified version of an example provided by Microsoft at GitHub.

Create a new file src/auth/authConfig.js:

const msalConfig = {
  auth: {
    clientId: process.env.VUE_APP_AUTH_CLIENTID,
    authority: process.env.VUE_APP_AUTH_AUTHORITY,
    knownAuthorities: [process.env.VUE_APP_AUTH_B2C_DOMAIN]
  },
  cache: {
    cacheLocation: "sessionStorage", // This configures where your cache will be stored
    storeAuthStateInCookie: false, // Set this to "true" if you are having issues on IE11 or Edge
  }
}

const loginRequest = {
  scopes: [process.env.VUE_APP_AUTH_SCOPE, "openid"],
  redirectUrl: process.env.VUE_APP_AUTH_REDIRECT_URI
}

const tokenRequest = {
  scopes: [process.env.VUE_APP_AUTH_SCOPE],
  forceRefresh: false // Set this to "true" to skip a cached token and go to the server to get a new token
}

export { msalConfig, loginRequest, tokenRequest }

This file uses the values that were defined by us in the .env file. Each time process.env.* is there, the environment variable is used.

Next, create the file src/auth/AuthService.js:

import * as msal from "@azure/msal-browser";
import { msalConfig, loginRequest, tokenRequest } from "./authConfig";

class AuthService {
  static instance

  constructor() {
    this.msalApp = new msal.PublicClientApplication(msalConfig)
  }

  static getInstance() {
    if (AuthService.instance) {
      return AuthService.instance
    }

    AuthService.instance = new AuthService()
    return AuthService.instance
  }

  async signIn() {
    return await this.msalApp.loginPopup(loginRequest).then((resp) => {
      if (resp !== null) {
        this.accountId = resp.account.homeAccountId
      } else {
        const currentAccounts = this.msalApp.getAllAccounts()
        if (!currentAccounts || currentAccounts.length < 1) {
          return
        } else {
          this.accountId = currentAccounts[0].homeAccountId
        }
      }
    })
  }

  isAuthenticated() {
    const accounts = this.msalApp.getAllAccounts()
    return (accounts && accounts.length > 0)
  }

  getAccount() {
    if (this.accountId) {
      return this.msalApp.getAccountByHomeId(this.accountId)
    } else if (this.msalApp.getAllAccounts() && this.msalApp.getAllAccounts().length !== 0) {
      return this.msalApp.getAllAccounts()[0]
    }
    return null
  }

  async getToken() {
    const request = {
      ...tokenRequest,
      account: this.getAccount()
    }

    return await this.msalApp.acquireTokenSilent(request).catch(async (error) => {
      console.log("silent token acquisition fails.")
      console.log("acquiring token using popup")
      return await this.msalApp.acquireTokenPopup(request)
    })
  }
}

export { AuthService as default }

As you can see, this class is a singleton. Just one instance of it will be created during the runtime and it will be used by all of our components. As you can see, we are using popup as a method to acquire a token. The usage of msal-browser makes this class rather consise considering that this is a core service of our application.

Let's move on to create our first Vue.js component.

# AccessToken Vue.js Component

Create a new file src/auth/AccessToken.vue:

<template>
  <div class="card" style="height: 100%">
    <div class="card-body">
      <h5 class="card-title">1. Authorization</h5>
      <h6 class="card-subtitle text-muted mb-4">Get an access token</h6>
      <div class="row">
        <div class="container">
          <div class="col-12">
            <button
              type="button"
              class="btn btn-primary mb-3"
              @click="getAccessToken"
            >Sign in to get an access token</button>
          </div>
          <div class="col-12">
            <label for="accessTokenTextArea">Access token</label>
            <textarea id="accessTokenTextArea" readonly v-model="result" class="form-control mb-2" rows="5"></textarea>

            <small class="form-text text-muted">
              Visit
              <a target="_blank" href="https://jwt.ms">JWT.MS</a> to decode and inspect the contents of your token.
            </small>
          </div>

        </div>
      </div>
    </div>
  </div>
</template>

<script>
import AuthService from "./AuthService";

export default {
  name: "AccessToken",
  data: function () {
    return {
      authService: null,
      result: null,
    };
  },
  methods: {
    getAccessToken: async function () {
      try {
        this.result = null
        await this.authService.signIn();
        const { accessToken } = await this.authService.getToken();
        this.result = accessToken;
      } catch (error) {
        this.result = error;
      }
    },
  },
  created: function () {
    this.authService = AuthService.getInstance();
  },
};
</script>

If this is the first time you're seeing a Vue.js code, let's break it down a bit. We've got both the HTML and the script that controls it in the same file. The whole file is like an HTML template file that contains the various parts in separate "containers". template is used for HTML markup, script contains JavaScript and, optionally, there could also be a style container for CSS.

Every Vue.js component needs to export an object that will contain the component's private data and methods. In the case of AccessToken component we have two fields in the data object:

  • authService - a singleton instance of our AuthService class
  • result - a variable that will store the access token. It is used in the HTML markup to display its content in the textarea (v-model="result").

We've got just a single method called getAccessToken. It is called when the user pressed a button. It makes use of our AuthService instance to get the token.

The last piece in the created function. It is one of the Vue.js lifecycle hooks. It is run automatically when our component's instance is created. We use it to obtain the singleton instance of AuthService. It is done the same way in every component that we create in this tutorial series.

# App Component

Now that we have the AccessToken component ready, let's try to put it on the screen. We need to modify the src/App.vue file that was created automatically for us by the Vue CLI. If you look into it you will see that it displays an HelloWorld component (another one that was created automatically for us). You can delete the /src/components/HelloWorld.vue - we will not need it. Now, let's update the contents of src/App.vue:















 















 




 




<template>
  <div class="app">
    <header class="mb-4">
      <nav class="navbar navbar-light bg-white border-bottom">
        <div class="container">
          <span class="navbar-brand mb-0 h1 text-wrap">
          Getting Started with <span class="abb font-weight-bold">ABB Ability™ Platform
          </span></span>
        </div>
      </nav>
    </header>

    <main class="container">
      <div class="row">
        <div class="col-12 mb-4"><AccessToken /></div>
      </div>
    </main>

    <footer class="footer mt-4 p-3 bg-white border-top">
      <div class="container">
        <span class="text-muted">This application is part of the Ability Getting Started 
        tutorial series that can be found on 
        the <a target="_blank" href="https://clientsuccess.ability.abb">
        Ability Developer Portal</a>.</span>
      </div>
    </footer>
  </div>
</template>

<script>
import AccessToken from './auth/AccessToken';

export default {
  name: 'App',
  components: {
    AccessToken
  }
}
</script>

Save the file and run the command:

npm run serve

Navigate to http://localhost:8080 in your browser. You should see the following result:

Our application after completing the first module

The final test is to press the "Sign in" button and see if you are able to get an access token. if something fails in this process, there are a few things that could go wrong. Make sure that you check well these items:

  • correct configuration values in the .env file
  • correctly assigned grant to yourself and your application in the Admin Portal

TIP

If the app does not work as you'd expect, you will most likely find some errors in the console of your browser. Possibly, you just need to restart the Vue.js server and run it again with npm run serve. If you cannot find any issue in your code, feel free to drop us an email. We'll have a look.

# Summary

In this module of the tutorial we were dealing with all things related to authorization of our application. Hopefully you were able to get an access token. Just below the token area there is a link to jwt.ms. Try to paste your token in there to see what is actually encoded in it. You should see the four permissions that you selected in the Admin Portal. This is how the API server knows what your app is allowed to do!

An access token by itself is not very exciting. We wanted to have a device and an SPA to interact with the APIs. Continue to the next article to get closer to that end goal.

Author: Marcin Jahn
Last updated: 1/10/2022, 11:05:26 AM
Feedback