React JS Application with Azure AD B2C

Introduction 

 
Azure AD B2C is Microsoft’s identity provider for social and enterprise logins. It allows you to, for example, unify the login process across Azure AD. It involves rooting around through multiple samples, the ADAL library, and the MSAL library. It makes use of MSAL underneath and the core of it (other than protecting routes) will probably work with other frameworks too, but I use React at the moment.
 
Step 1 - Create a React app
  • npx create-react-app loginPage-app
  • cd loginPage-app
  • npm start
React JS project will get executed in the default browser.
 
Step 2 - Login Azure AD B2C portal
 
https://portal.azure.com/
 
In the below URL, you will get the steps on how to create an Azure ad b2c portal account.
 
https://docs.microsoft.com/en-us/azure/active-directory-b2c/tutorial-create-tenant 
 
Step 3 - Installation & Coding in an App
 

Using msal.js library with Azure ID

 
React AAD MSAL is a library that allows you to easily integrate auth using Azure Active Directory into your React application. The library focuses on flexibility, allowing you to define how you want to interact with logins and logouts. The latest version is msal.js v1.0.2.
 
npm install msal --save
 

Key differences of msal from oid-client.js

  1.  msal.js is opinionated on caching and renewing your access token and offers no event handling around access token length. Instead, 'session-length' is tied directly to the chosen cache lifetime and user-actions. For instance, if session Storage is chosen, the session lifetime is tied directly to the length of the browser process (viz. tab or window) or code is called to deliberately log the user out. Choosing local Storage will instead create indefinite session life unless code is called to deliberately log the user out. Microsoft recommends using session Storage which we will do here.
  2. The issuer is verified against a 'hard-coded' list inside the library code, as the library is meant only to be used against AAD. The library skips using the public keys to verify the token signature and instead relies on the audience to validate the token signature.
  3. All scopes provided in the configuration of its interface, UserAgentApplication, must belong to the same audience - ergo there must be a 1-to-1 relationship (or 1-to-many, however that would likely be indicative of redundancy or over-segmentation of the application) of audiences to UserAgentApplication instances.

    npminstalloidc-client --save 

Configurations

 
Next, we need to create a configuration for our JavaScript application. Here is what we are currently running: 
 
configuration.json
  1. "scopes": [  
  2.  "api://<api-application-id>/<scope-name>"  
  3. ],  
  4. "auth": {  
  5.  "clientId""<registered client app id>",  
  6.  "authority""https://login.microsoftonline.com/reactapplicationdev.onmicrosoft.com",  
  7.  "validateAuthority"true,  
  8.  "redirectUri""http://localhost:3000",  
  9.  "postLogoutRedirectUri""http://localhost:3000",  
  10.  "navigateToLoginRequestUrl"true  
  11. },  
  12. "cache": {  
  13.  "cacheLocation""sessionStorage",  
  14.  "storeAuthStateInCookie"true  
  15. }  
  16. },  
  1.  ClientID: Required. The ClientID of your application, you should get this from the application registration portal.
  2. authority: Optional. A URL indicating a directory that MSAL can request tokens from. Default value is: https://login.microsoftonline.com/common
  3. redirectUri: Optional. The redirect URI of your app, where authentication responses can be sent and received by your app. It must exactly match one of the redirect URIs you registered in the portal, except that it must be URL encoded. Defaults to window.location.href.
  4. cacheLocation: Optional. Sets browser storage to either localStorage or sessionStorage. The default is sessionStorage.
    Abstracting UserAgentApplication.
Whenever you add an external dependency that can be easily encapsulated (e.g. an authentication library)a it is useful to move it behind an interface to reduce areas in your application that you will need to change in the scenario that you swap to a different provider for that particular dependency. In this section, we will encapsulate our UserAgentApplication inside a custom authService JavaScript class. There is an example of such a class below.
 
authService.js
  1. import * as Msal from 'msal';  
  2. import configuration from './configuration.json';  
  3.   
  4. const msalConfig = {  
  5.  ...configuration.msalConfig  
  6. };  
  7.   
  8. class AuthService {  
  9.  constructor() {  
  10.   this.userAgentApplication = new Msal.UserAgentApplication(msalConfig);  
  11.   
  12.   this.userAgentApplication.handleRedirectCallback((error, response) => {});  
  13.  };  
  14.   
  15.  fetchAccessToken = () => {  
  16.   const accessTokenRequest = {  
  17.    scopes: [...msalConfig.scopes]  
  18.   };  
  19.   
  20.   return this.userAgentApplication.acquireTokenSilent(accessTokenRequest)  
  21.    .then((accessTokenResponse) => (accessTokenResponse.accessToken))  
  22.    .catch((error) => {  
  23.     console.log(error);  
  24.     if (error.errorMessage.indexOf("interaction_required") !== -1) {  
  25.      this.userAgentApplication.acquireTokenRedirect(accessTokenRequest);  
  26.     } else {  
  27.      this.login();  
  28.     }  
  29.    });  
  30.  };  
  31.   
  32.  getUser = () => {  
  33.   return this.userAgentApplication.getAccount();  
  34.  };  
  35.   
  36.  login = () => {  
  37.   return this.userAgentApplication.loginRedirect({  
  38.    scopes: [...msalConfig.scopes],  
  39.    prompt: 'select_account'  
  40.   });  
  41.  };  
  42.   
  43.  logout = () => {  
  44.   store.dispatch(authActions.resetApp);  
  45.   persistor.purge().then(() => {  
  46.    this.userAgentApplication.logout();  
  47.   });  
  48.  };  
  49. }  
  50.   
  51. const authService = new AuthService();  
  52. export default authService;  
  1. We first import the msal library and our configuration for our UserAgentApplication.
  2. We provide handles to our application to use the UserAgentApplication's key methods so that our app can work: login, logout, get a user, get the access token.
     
    Note: we use the same scopes during login as we do for fetching an access token - you should stay consistent within a given instance of your implementation. Which brings us to the last point, you could make this more reusable by confusing a configuration as a parameter during the construction of your instance. Rather, here we hard code the configuration reference and instantiate our only needed instance for exporting here. This is because our example is quite simple and only needs access to one audience and thus one set of scopes and configuration. However, it is relatively easy to imagine a large scale application relying on multiple audiences which requires the use of multiple userAgentApplication instances in order to access a variety of resources that would benefit from a tweaked abstraction over-top the UserAgentApplication to allow for a more re-usable class. If readers would like to see an example of what that might look like, please indicate so in the comments below.

Login

 
Our login method uses login Redirect and passes in scopes and a special prompt parameter. We are using this parameter so that people are given the choice to select which Microsoft account they want to use every time, for customers with more than one account this is highly convenient. You may note that our scopes are the same as used for fetching the access token and the interaction method is the same. Do not mix-and-match redirect and pop-up methods as it is against Microsoft recommendation and more confusing for users. The login functionality is essentially a functional way of making an authorization request to the tenant for an id_token. Once login is completed successfully, you should be able to check the logged-in user by retrieving information from the identity token with getAccount.
 

Fetch Access Token

 
Our fetchAccessToken method is the most complex piece of code. It returns a promise of an access token from the access token response and if it fails, it will attempt an interactive access token request or if it fails in an unknown way - sends them back to login. Again, recall that we use the same scopes as we did in login as well as the same interaction method, redirect. The acquireTokenSilent method which lies at the core of this functionality will try to get a cached access token from either session or localStorage depending upon your configurations above if it fails to find one or the access token is close to expiring/has expired, it will request a new one if authentication fails when requesting the new access token due to the session expiring in AAD's back end, it will indicate "interaction required" at which point our code makes an interactive request, essentially requiring the user to re-enter their credentials to keep the session alive. An access token lifetime is an hour while an AAD session maxes out at 24 hours.
 

Use with React/Redux/Redux-Sagas/Axios

 
Using our above authService, let's us show how we can use it in combination with React, Redux, Redux-Sagas, and Axios to build the basis of an application. Axios, being our HTTP request client, is the easiest place to start. Here is a simplistic way of including an access token on every request. We create a client at the start, you can see that we have an Azure API product subscription key in our headers as a fun side-note. On each api request wrapper, we request an access token and in the resolution of that promise, we make our request, ensured that we have the latest access token to complete the request.
 
apiRequest.js
  1. import axios from 'axios'  
  2. import configuration from '../configuration.json';  
  3. import authService from '../authService';  
  4. var apiURL = configuration.apigateway;  
  5.   
  6. export const client = axios.create({  
  7.  baseURL: apiURL,  
  8.  rejectUnauthorized: false,  
  9.  crossDomain: true,  
  10.  headers: {  
  11.   'Ocp-Apim-Subscription-Key': configuration.Applications  
  12.  }  
  13. });  
  14.   
  15. /** 
  16.  * ApiRequest Wrapper with default success/error actions 
  17.  */  
  18. const apirequest = (options) => {  
  19.  return authService.fetchAccessToken()  
  20.   .then((accessToken) => {  
  21.    if (!options.headers)  
  22.     options.headers = {};  
  23.   
  24.    options.headers['Authorization'] = `bearer ${accessToken}`;  
  25.   
  26.    return client(options)  
  27.     .then(response => {  
  28.      return response.data;  
  29.     })  
  30.     .catch(error => Promise.reject(error.response || error.message));  
  31.   });  
  32. }  
  33.   
  34. export const request = function(options) {  
  35.  return authService.fetchAccessToken()  
  36.   .then((accessToken) => {  
  37.    if (!options.headers)  
  38.     options.headers = {};  
  39.   
  40.    options.headers['Authorization'] = `bearer ${accessToken}`;  
  41.   
  42.    return client(options)  
  43.     .then(response => {  
  44.      return response.data;  
  45.     })  
  46.     .catch(error => Promise.reject(error.response || error.message));  
  47.   });  
  48. }  
  49.   
  50. import axios from 'axios'  
  51. import configuration from '../configuration.json';  
  52. import authService from '../authService';  
  53.   
  54. var apiURL = configuration.apigateway;  
  55.   
  56. export const client = axios.create({  
  57.  baseURL: apiURL,  
  58.  rejectUnauthorized: false,  
  59.  crossDomain: true,  
  60.  headers: {  
  61.   'Ocp-Apim-Subscription-Key': configuration.Applications  
  62.  }  
  63. });  
  64.   
  65. /** 
  66.  * ApiRequest Wrapper with default success/error actions 
  67.  */  
  68. const apirequest = (options) => {  
  69.  return authService.fetchAccessToken()  
  70.   .then((accessToken) => {  
  71.    if (!options.headers)  
  72.     options.headers = {};  
  73.   
  74.    options.headers['Authorization'] = `bearer ${accessToken}`;  
  75.   
  76.    return client(options)  
  77.     .then(response => {  
  78.      return response.data;  
  79.     })  
  80.     .catch(error => Promise.reject(error.response || error.message));  
  81.   });  
  82. }  
  83.   
  84. export const request = function(options) {  
  85.  return authService.fetchAccessToken()  
  86.   .then((accessToken) => {  
  87.    if (!options.headers)  
  88.     options.headers = {};  
  89.   
  90.    options.headers['Authorization'] = `bearer ${accessToken}`;  
  91.   
  92.    return client(options)  
  93.     .then(response => {  
  94.      return response.data;  
  95.     })  
  96.     .catch(error => Promise.reject(error.response || error.message));  
  97.   });  
  98. }  
  99.   
  100. export default apirequest;  
In order to get an access token though, our app needs to be authenticated vis-a-vis login. If our application is using redux to manage state, it makes sense to take our abstraction of authService a step further and handle those methods using a reducer and actions. Here is a reducer for the areas our application might want to use (ideally you might break this up into smaller files of actions, reducer, and sagas, but for the sake of brevity and legibility I am including them all here).
 
authReducer.js
  1. import apirequest from "../api/ApiRequest";  
  2. import authService from '../authService';  
  3. import {  
  4.  call,  
  5.  put,  
  6.  takeLatest  
  7. } from 'redux-saga/effects';  
  8. export const GET_USER = 'GET_USER';  
  9. export const GET_USER_COMPLETE = 'GET_USER_COMPLETE';  
  10. export const LOGIN = 'LOGIN';  
  11. export const LOGOUT = 'LOGOUT';  
  12. const initialState = {  
  13.  user: null  
  14. }  
  15. const reducer = (state = initialState, action) => {  
  16.  switch (action.type) {  
  17.   case LOGOUT:  
  18.    return {  
  19.     ...state,  
  20.     logout: true  
  21.    };  
  22.   case GET_USER_COMPLETE:  
  23.    const user = action.payload;  
  24.    if (!user)  
  25.     return {  
  26.      ...state  
  27.     };  
  28.    return {  
  29.     ...state,  
  30.     user: user  
  31.    };  
  32.   default:  
  33.    return state;  
  34.  }  
  35. }  
  36. export const getUser = (user) => {  
  37.  type: GET_USER,  
  38.  payload: user  
  39. };  
  40. export const login = () => {  
  41.  type: LOGIN  
  42. };  
  43. export const logout = () => {  
  44.  type: LOGOUT  
  45. };  
  46.   
  47. function* loginSaga() {  
  48.  yield authService.login();  
  49. }  
  50.   
  51. function* logoutSaga() {  
  52.  yield authService.logout();  
  53. }  
  54.   
  55. function* getUserSaga() {  
  56.  const user = yield authService.getUser();  
  57.  if (user)  
  58.   yield put({  
  59.    type: GET_USER_COMPLETE,  
  60.    payload: user  
  61.   });  
  62.  else  
  63.   yield put({  
  64.    type: LOGIN  
  65.   });  
  66. }  
  67. export const sagas = [  
  68.  takeLatest(GET_USER, getUserSaga),  
  69.  takeLatest(LOGIN, loginSaga),  
  70.  takeLatest(LOGOUT, logoutSaga)  
  71. ];  
  72. export default reducer;  
Next, we can simply throw our authentication check in our app like so and it will show a spinner until it redirects the user to login whereupon they can attempt to login. On success, they come back and the app recognizes the user and renders the rest of the app content.
 
App.js
  1. import React, {  
  2.  Component  
  3. } from 'react';  
  4. import * as auth from './redux/auth';  
  5. import {  
  6.  connect  
  7. } from 'react-redux';  
  8. class App extends Component {  
  9.  componentDidMount() {  
  10.   this.props.getUser();  
  11.  }  
  12.  render() {  
  13.    let appContent = < div > Your app content here. < /div>  
  14.    return ( <  
  15.     div > {  
  16.      (!this.props.user) && ( < div style = {  
  17.        {  
  18.         bottom: '50%',  
  19.         position: 'absolute',  
  20.         right: '50%'  
  21.        }  
  22.       } >  
  23.       <  
  24.       i style = {  
  25.        {  
  26.         display: 'inline-block',  
  27.        }  
  28.       }  
  29.       className = "fas fa-spinner fa-spin fa-5x" > < /i> <  
  30.       /div>)} {  
  31.        (!!this.props.user && appContent)  
  32.       } <  
  33.       /div>  
  34.      );  
  35.     }  
  36.    }  
  37.    const mapStateToProps = state => {  
  38.     return {  
  39.      user: state.auth.user  
  40.     };  
  41.    };  
  42.    const mapDispatchToProps = dispatch => {  
  43.     return {  
  44.      getUser: () => dispatch(auth.getUser())  
  45.     };  
  46.    };  
  47.    export default connect(mapStateToProps, mapDispatchToProps) App;