Authentication with ADAL in React Single Page Applications

One of the key features in Single Page Applications (SPAs) is a little thing known as authentication. The ability to login and make authenticated network requests to a backend API are often required, but not always easy to implement.

In the first part of this tutorial, we will cover how to implement basic authentication with Azure's Active Directory (AAD) and the Azure Directory Authentication Library (ADAL) for JavaScript, (also known as the adal-angular library on npm) in a Single Page Application (SPA) written with React JS.

In addition, we will make sure network requests to a (REST) API running on Azure always use a valid bearer token.

In the second part, we will take control of the session management without having to change the refresh token lifetime in Azure AD. We will enforce a login (so no automatic (re-)authentication with the refresh token) whenever the user has been inactive longer than our configured session time.

Our goal will be to create a SPA where a user needs to login before we render anything of the SPA, to reduce the session timeout to 30 minutes instead of the default 1 hour and to disable automatic login.

If your goals are different from these and, for example, you want to render something even though no user is logged in or keep automatic login enabled, or use the default session timeouts, don't fret. You will find that the steps in this tutorial are as independent of each other as possible, giving you the opportunity to use only what you need to build your own setup.

React vs Azure

Not covered in this tutorial

  • how to set up Azure AD
  • how to set up a client web app on Azure
  • how to set up an API backend on Azure

But I can give you this: you'll need to use OAuth2 implicit flow by setting oauth2AllowImplicitFlowto to "true" in the AAD manifest of the client application. And, you will need to use the BearerStrategy (as in, use an Authentication parameter in the request headers set to “Bearer <a valid access token>”) to authenticate network requests against the backend API.

Covered in this tutorial

  • First part:
    • how to set up and initialize the adal-angular library
    • how to set up axios(/apisauce)
    • authenticate calls to the backend
  • Second part:
    • how to disable automatic login by adal
    • how to automatically logout after 30 minutes (configurable)

Needed requirements

  • An Active Directory (AD) in Azure with some users to test with
  • A client web app in Azure to host the SPA on
  • An API backend on Azure with an endpoint or two to test with
  • A React JS project (we used create-react-app to reduce setup and in this case to be able to test implementations faster)
  • Plugins/libraries: (to be installed with npm or yarn)
    • adal-angular (at the time of writing this tutorial the version was v1.0.17)
    • axios (at the time of writing this tutorial the version was v0.18.0)
    • Optional: apisauce (at the time of writing this tutorial the version was v1.0.0) (also contains the axios package so there's no need to install axios separately if you install and use ApiSauce)

You can also use this sample repo to follow the tutorial.

It's a setup!

  1. Set up an AD in Azure with a user or two to test with
  2. Set up the necessary application project(s) in Azure, of which we will use the tenant ID, the application ID of the client web app, and the application ID of the API app.
  3. Set up a config file in your React application where we can place our Azure IDs and other configuration parameters.
// src/config/AdalConfig.js
export default {
 clientId: 'ENTER THE APPLICATION ID OF THE REGISTERED WEB APP ON AZURE',
 endpoints: {
 // Necessary for CORS requests, for more info see https://github.com/AzureAD/azure-activedirectory-library-for-js/wiki/CORS-usage
 api: "ENTER THE APPLICATION ID OF THE REGISTERED API APP ON AZURE" 
 },
 // 'tenant' is the Azure AD instance.
 tenant: 'ENTER YOUR TENANT ID',
 // 'cacheLocation' is set to 'sessionStorage' by default, for more info see https://github.com/AzureAD/azure-activedirectory-library-for-js/wiki/Config-authentication-context#configurable-options
 // We change it to'localStorage' because 'sessionStorage' does not work when our app is served on 'localhost' in development.
 cacheLocation: 'localStorage'
}
TIP: Use custom environment variables here! See Create React App's guide for more information.

Initialize Adal Instance

Next up, we will initialize the adal instance with the config we just defined.

First, some necessary imports:

// src/services/Auth.js
import AuthenticationContext from 'adal-angular'
import AdalConfig from '../config/AdalConfig'

Second, we add some code so that the adal library can log to console.

// We use this to enable logging in the adal library. When you're building for production, you should know that it's best to disable the logging.
window.Logging.log = function(message) {
 console.log(message); // this enables logging to the console
}
window.Logging.level = 2 // 0 = only error, 1 = up to warnings, 2 = up to info, 3 = up to verbose

Then, we initialize the adal instance by combining the AuthenticationContext class, exported from the adal library, with the AdalConfig we defined in the previous step.

// Initialize the authentication
export default new AuthenticationContext(AdalConfig)
Don't worry if export default new AuthenticationContext(AdalConfig) would initialize a new instance each time you import it -> webpack will build all our javascript code in one file and imports will reference to single instances respectively.

Initialize axios instance

To make sure our network requests use the correct base url of the API, we create a config file with a certain baseURL parameter which we'll later use to initialize an axios instance.

// src/config/ApiConfig.js
export default {
  baseURL: "ENTER BASE URL OF API HERE" // something like "http://my-host-name.xyz/api"
}

Next, use the ApiConfig to initialize an axios instance like so:

// src/services/Api.js
import axios from 'axios'

import ApiConfig from '../config/ApiConfig'

const instance = axios.create(ApiConfig)

export default instance

Finally, we will import the axios instance, in whichever components we need it, to make api calls, for example in the componentDidMount section of the 'App' component:

// src/App.js
import Api from './services/Api'

class App extends Component {
  componentDidMount() {
    // Perform a network request on mount to easily test our setup
    Api.get('/todos')
  }

Show me what you got

After we've initialized everything we need, we can start coding the logic to successfully render the React application or to redirect the user to Microsoft's login page.

In index.js, import the AuthContext from our authentication service and the AdalConfig to be able to use the IDs.

// src/index.js
import AdalConfig from './config/AdalConfig'
import AuthContext from './services/Auth'

Add the following code to let the adal library handle any possible callbacks after logging in or (re-)acquiring tokens:

// Handle possible callbacks on id_token or access_token
AuthContext.handleWindowCallback()

Then we'll add some extra logic that we will only run when we are on the parent window and not in an iframe. If we were to allow it to run in iframes, which are used by adal to acquire tokens, then we would be stuck with multiple instances of our React app and we don't want that.

If we have no logged in user then we will redirect the user to Microsoft's login page. If we have a logged in user then we will acquire an access token for our API to see that everything works, and we will render our React application.

This results in the following code:

// extra callback logic, only in the actual application, not in iFrames in the app
if ((window === window.parent) && window === window.top && !AuthContext.isCallback(window.location.hash)) {
  // Having both of these checks is to prevent having a token in local-storage, but no user.
  if (!AuthContext.getCachedToken(AdalConfig.clientId) || !AuthContext.getCachedUser()) {
    AuthContext.login()
    // or render something that everyone can see
    // ReactDOM.render(<publicpartofapp>, document.getElementById('root'))
  } else {
    AuthContext.acquireToken(AdalConfig.endpoints.api, (message, token, msg) => {
      if (token) {
        ReactDOM.render(<app>, document.getElementById('root'))
      }
    })
  }
}
As you can see in the comments in the code, you can also choose to show a public page even when there is no logged in user. Instead of calling AuthContext.login() you can also render another React instance, for example a public landing page which has a specific button to be able to login later on...

Add interceptor to network requests

To make sure our network requests to the backend API remain authenticated, we will add some logic to our axios instance.

We will call adal's acquireToken function each time a network request is made. Adal will return the valid access token or it will asynchronously fetch a new one if it is invalid. Once the token is available we will add it to the Authorization header of the network request.

First, don't forget to add the necessary imports:

// src/services/Api.js
import AdalConfig from '../config/AdalConfig'
import AuthContext from './Auth'

Then we can place the re-acquiring of tokens in a request interceptor of the axios instance like so:

// src/services/Api.js
// Add a request interceptor
instance.interceptors.request.use((config) => {
  // Check and acquire a token before the request is sent
  return new Promise((resolve, reject) => {
    AuthContext.acquireToken(AdalConfig.endpoints.api, (message, token, msg) => {
      if (!!token) {
        config.headers.Authorization = `Bearer ${token}`
        resolve(config)
      } else {
          // Do something with error of acquiring the token
          reject(config)
      }
    })
  })
}, function(error) {
  // Do something with error of the request
  return Promise.reject(error)
})

And that's it! At this moment, your React SPA is ready to use authentication with the adal-angular library and Azure's Active Directory!

Sample

A sample project with authentication as described in this tutorial can be found on our GitHub space here.

Special thanks to

I would like to take a brief moment to thank magnuf for his example on github, originally helping me on my way figuring all of this out.

Want to do adjustments to the session timeout?

Then follow this link to part 2 of this tutorial, where I explain how to add session management!