Next, we need to create a configuration for our JavaScript application. Here is what we are currently running:
configuration.json
- "scopes": [
- "api://<api-application-id>/<scope-name>"
- ],
- "auth": {
- "clientId": "<registered client app id>",
- "authority": "https://login.microsoftonline.com/reactapplicationdev.onmicrosoft.com",
- "validateAuthority": true,
- "redirectUri": "http://localhost:3000",
- "postLogoutRedirectUri": "http://localhost:3000",
- "navigateToLoginRequestUrl": true
- },
- "cache": {
- "cacheLocation": "sessionStorage",
- "storeAuthStateInCookie": true
- }
- },
- ClientID: Required. The ClientID of your application, you should get this from the application registration portal.
- authority: Optional. A URL indicating a directory that MSAL can request tokens from. Default value is: https://login.microsoftonline.com/common
- 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.
-
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
- import * as Msal from 'msal';
- import configuration from './configuration.json';
-
- const msalConfig = {
- ...configuration.msalConfig
- };
-
- class AuthService {
- constructor() {
- this.userAgentApplication = new Msal.UserAgentApplication(msalConfig);
-
- this.userAgentApplication.handleRedirectCallback((error, response) => {});
- };
-
- fetchAccessToken = () => {
- const accessTokenRequest = {
- scopes: [...msalConfig.scopes]
- };
-
- return this.userAgentApplication.acquireTokenSilent(accessTokenRequest)
- .then((accessTokenResponse) => (accessTokenResponse.accessToken))
- .catch((error) => {
- console.log(error);
- if (error.errorMessage.indexOf("interaction_required") !== -1) {
- this.userAgentApplication.acquireTokenRedirect(accessTokenRequest);
- } else {
- this.login();
- }
- });
- };
-
- getUser = () => {
- return this.userAgentApplication.getAccount();
- };
-
- login = () => {
- return this.userAgentApplication.loginRedirect({
- scopes: [...msalConfig.scopes],
- prompt: 'select_account'
- });
- };
-
- logout = () => {
- store.dispatch(authActions.resetApp);
- persistor.purge().then(() => {
- this.userAgentApplication.logout();
- });
- };
- }
-
- const authService = new AuthService();
- export default authService;
- We first import the msal library and our configuration for our UserAgentApplication.
-
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
- import axios from 'axios'
- import configuration from '../configuration.json';
- import authService from '../authService';
- var apiURL = configuration.apigateway;
-
- export const client = axios.create({
- baseURL: apiURL,
- rejectUnauthorized: false,
- crossDomain: true,
- headers: {
- 'Ocp-Apim-Subscription-Key': configuration.Applications
- }
- });
-
-
-
-
- const apirequest = (options) => {
- return authService.fetchAccessToken()
- .then((accessToken) => {
- if (!options.headers)
- options.headers = {};
-
- options.headers['Authorization'] = `bearer ${accessToken}`;
-
- return client(options)
- .then(response => {
- return response.data;
- })
- .catch(error => Promise.reject(error.response || error.message));
- });
- }
-
- export const request = function(options) {
- return authService.fetchAccessToken()
- .then((accessToken) => {
- if (!options.headers)
- options.headers = {};
-
- options.headers['Authorization'] = `bearer ${accessToken}`;
-
- return client(options)
- .then(response => {
- return response.data;
- })
- .catch(error => Promise.reject(error.response || error.message));
- });
- }
-
- import axios from 'axios'
- import configuration from '../configuration.json';
- import authService from '../authService';
-
- var apiURL = configuration.apigateway;
-
- export const client = axios.create({
- baseURL: apiURL,
- rejectUnauthorized: false,
- crossDomain: true,
- headers: {
- 'Ocp-Apim-Subscription-Key': configuration.Applications
- }
- });
-
-
-
-
- const apirequest = (options) => {
- return authService.fetchAccessToken()
- .then((accessToken) => {
- if (!options.headers)
- options.headers = {};
-
- options.headers['Authorization'] = `bearer ${accessToken}`;
-
- return client(options)
- .then(response => {
- return response.data;
- })
- .catch(error => Promise.reject(error.response || error.message));
- });
- }
-
- export const request = function(options) {
- return authService.fetchAccessToken()
- .then((accessToken) => {
- if (!options.headers)
- options.headers = {};
-
- options.headers['Authorization'] = `bearer ${accessToken}`;
-
- return client(options)
- .then(response => {
- return response.data;
- })
- .catch(error => Promise.reject(error.response || error.message));
- });
- }
-
- 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
- import apirequest from "../api/ApiRequest";
- import authService from '../authService';
- import {
- call,
- put,
- takeLatest
- } from 'redux-saga/effects';
- export const GET_USER = 'GET_USER';
- export const GET_USER_COMPLETE = 'GET_USER_COMPLETE';
- export const LOGIN = 'LOGIN';
- export const LOGOUT = 'LOGOUT';
- const initialState = {
- user: null
- }
- const reducer = (state = initialState, action) => {
- switch (action.type) {
- case LOGOUT:
- return {
- ...state,
- logout: true
- };
- case GET_USER_COMPLETE:
- const user = action.payload;
- if (!user)
- return {
- ...state
- };
- return {
- ...state,
- user: user
- };
- default:
- return state;
- }
- }
- export const getUser = (user) => {
- type: GET_USER,
- payload: user
- };
- export const login = () => {
- type: LOGIN
- };
- export const logout = () => {
- type: LOGOUT
- };
-
- function* loginSaga() {
- yield authService.login();
- }
-
- function* logoutSaga() {
- yield authService.logout();
- }
-
- function* getUserSaga() {
- const user = yield authService.getUser();
- if (user)
- yield put({
- type: GET_USER_COMPLETE,
- payload: user
- });
- else
- yield put({
- type: LOGIN
- });
- }
- export const sagas = [
- takeLatest(GET_USER, getUserSaga),
- takeLatest(LOGIN, loginSaga),
- takeLatest(LOGOUT, logoutSaga)
- ];
- 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
- import React, {
- Component
- } from 'react';
- import * as auth from './redux/auth';
- import {
- connect
- } from 'react-redux';
- class App extends Component {
- componentDidMount() {
- this.props.getUser();
- }
- render() {
- let appContent = < div > Your app content here. < /div>
- return ( <
- div > {
- (!this.props.user) && ( < div style = {
- {
- bottom: '50%',
- position: 'absolute',
- right: '50%'
- }
- } >
- <
- i style = {
- {
- display: 'inline-block',
- }
- }
- className = "fas fa-spinner fa-spin fa-5x" > < /i> <
- /div>)} {
- (!!this.props.user && appContent)
- } <
- /div>
- );
- }
- }
- const mapStateToProps = state => {
- return {
- user: state.auth.user
- };
- };
- const mapDispatchToProps = dispatch => {
- return {
- getUser: () => dispatch(auth.getUser())
- };
- };
- export default connect(mapStateToProps, mapDispatchToProps) App;