Msal-Token-Interceptor is a request interceptor which uses Microsoft MSAL to handle OAuth 2.0 token exchange with Microsoft Identity Platform during a fetch request. This is part of fetch-interceptor family.
Example syntax:
fetch
.with(new MsalTokenHandler({ scopes: [ 'User.Read' ]}))
(
"https://graph.microsoft.com/v1.0/me", {
method: 'GET'
});
- Requires fetch-interceptor and MSAL and knowledge how to use the 2 libraries.
- Knowledge on OAuth 2.0, Bearer Token and OpenId Connect.
- Knowledge on Microsoft Identity Platform - Authorization Code Grant.
NOTE: Authentication is a difficult subject and this README does not cover prerequisite authentication subject such as OpenId Connect, OAuth 2.0 and Microsoft Identity Platform. You need to understand these subjects before able to make correct use of both MSAL and this library. The assumption is that if you're looking for bearer token helper using MSAL when making a fetch request, chances are you should already have knowledge of the mentioned subjects. Otherwise, please read and understand the mentioned subjects before continue. You may have difficulty to follow through if you do not understand those subjects.
NOTE: MSAL library has a specific version for React (mainly to expose MSAL functions into React component via React Hooks). This library does not require the MSAL React version but uses the standard Browser version. Therefore, this library can be used in any JavaScript application (e.g. React, Angular...etc.).
Msal-Token-Interceptor is available as NPM package.
npm install @appzmonster/msal-token-interceptor
Typically in a MSAL enabled application, you have the following in your .src/index.js or at the entry point of the application to initialize your MSAL instance.
// Msal package.
import { PublicClientApplication } from '@azure/msal-browser';
import msalConfiguration from './configuration';
...
const msalInstance = new PublicClientApplication(msalConfiguration);
NOTE: You can refer to MSAL initialization here.
Once the MSAL instance is initialized, you can use it to login, logout and acquire token to send authenticated request to the token protected backend service API.
For such an application, there's always a repetitive logic which you need to code, that is to prepare a token before making a service request to the backend service API. Reason being, the backend service API will reject request without a valid token. Usually, the token is stored in a local cache and will expire. If it is expired or near expire, you're responsible to get a new token from authorization server (the authorization server in this case is Microsoft Identity Platform). MSAL did a good job providing many handy functions for most tasks except it does not provide a handy request interceptor to automatically handle token exchange during an in-flight (outgoing) request. Reason why Microsoft omits this is understandable because you as the developer have many choices to code your XHR request. The 2 most popular choices are Axios and Fetch API. My opinion is Microsoft does not want to make opinionated decision how to take care of token exchange when making a fetch request.
This MSAL token interceptor uses the fetch-interceptor library which extends the Fetch API.
The following example shows how you can easily make a Fetch request with all token handling logic taken care for you:
// Msal package.
import { PublicClientApplication } from '@azure/msal-browser';
import msalConfiguration from './configuration';
// Fetch-interceptor package
import { initialize } from '@appzmonster/fetch-interceptor';
initialize(); // Enable fetch interceptor support on Fetch API.
...
const msalInstance = new PublicClientApplication(msalConfiguration);
let response = await fetch
.with(new MsalTokenHandler({ scopes: [ 'User.Read' ]}, msalInstance))
(
"https://graph.microsoft.com/v1.0/me", {
method: 'GET'
});
The MsalTokenHandler
has the following arguments:
-
tokenRequest
(Required)A token request type. MSAL uses this token request type for the
acquireTokenSilent
function. Refer here for information aboutacquireTokenSilent
. -
msalInstance
(Optional if fallback default MSAL is registered)The MSAL instance which you want the
MsalTokenHandler
to use internally. Typically, this is the MSAL instance initialized at the beginning of the application (an instance of MSALPublicClientApplication
).NOTE: You can omit this argument for every fetch request if you have registered a default fallback MSAL instance. When such registration exists and no MSAL instance is provided in the fetch request, the
MsalTokenHandler
in the fetch request will fallback to the registered instance. We will talk about default fallback MSAL later below. -
setAccount
(Optional)This function is invoked when the internal MSAL instance (
msalInstance
argument) is setting the account for the OAuth 2.0 token request. A token request requires the user account information in order for the auth server to verify its identity. An array of user accounts is passed to this function and expects the function to select one account and return. If this argument is omitted, MSAL token interceptor will always use the first user account.NOTE: An array of user accounts is meant for a browser with multiple active user account sessions. At the time of writing, Microsoft is looking into this (refer here). Before that happens, the assumption is the active account is always the first account in the list. Only use this function when it's necessary to check the number of active accounts.
In some cases, the application may have a different redirect uri for silent request (silent request is made via a hidden iframe and eventually lands on a specific uri in the application). Getting a token for service API is one such silent request. According to OAuth 2.0 standard, it is recommended for the application to have an empty landing page for silent request to redirect to. Let's assume we do the same in our application:
NOTE: Both redirect uri and silent redirect uri must be registered in your application authentication uri. For Microsoft Identity Platform, refer here.
NOTE: The following is an example written using React:
./src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { PublicClientApplication } from '@azure/msal-browser';
import msalConfiguration from './configuration';
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import './index.css';
import App from './App';
...
const silentRedirectUri = "https://localhost:3000/oidc/response-silent";
const msalInstance = new PublicClientApplication(msalConfiguration);
const silentRedirectRoutePath = new URL(silentRedirectUri).pathname;
ReactDOM.render(
<React.StrictMode>
<MsalProvider instance={msalInstance}>
<Router>
<Switch>
{/* Empty page for iframe silent authentication */}
<Route path={silentRedirectRoutePath} render={() => null} />
{/* Render actual app */}
<Route render={() => <App />}/>
</Switch>
</Router>
</MsalProvider>
</React.StrictMode>,
document.getElementById('root')
);
You can set the redirectUri
to the "silent redirect uri" of a MSALTokenHandler
in a fetch request with the following:
...
let response = await fetch
.with(new MsalTokenHandler({
scopes: [ 'User.Read' ], redirectUri: silentRedirectUri // Redirect to silent redirect uri (iframe)
}, msalInstance))
(
"https://graph.microsoft.com/v1.0/me", {
method: 'GET'
});
If for some reason the application needs to retrieve the last sent token for a fetch request, you can create a reference to the MsalTokenHandler
and invoke the token
function to get the last sent token after the fetch request returns.
...
let msalTokenHandler = new MsalTokenHandler({
scopes: [ 'User.Read' ],
redirectUri: silentRedirectUri
}, msalInstance);
let response = await fetch.with(msalTokenHandler)("https://graph.microsoft.com/v1.0/me", {
method: 'GET'
});
if (response.ok)
{
console.log("last sent token", msalTokenHandler.token());
}
If the current session is no longer valid or token can no longer be renewed, MSAL will throw error such as InteractionRequiredAuthError
requesting the user to re-login. There are many other types of error and you can refer here for information.
MsalTokenHandler
does not suppress the error or attempt to redirect the user to login should any of these errors occur during the token exchange. Instead, it will return the error to the caller or to the previous request interceptor for decision. This design is intentional so to allow the developer to take charge the decision what to do with the error. MsalTokenHandler
does not take an opinionated approach to handle the error on behalf of the developer.
In most cases, all fetch requests will be using the same instance of MSAL (the MSAL instance initialized in index.js) to exchange token with auth server. Also, configuration such as silent redirect uri for silent token request is usually the same for all fetch requests (can't think of a reason why you need more than 1 silent redirect uri). With such use case, the MsalTokenHandler
provides a static function to allow registration of a default MSAL instance and a default set of token request.
First, import the MsalTokenHandler
as per normal, typically in index.js.
./src/index.js
import { MsalTokenHandler } from '@appzmonster/msal-token-interceptor';
Next, after the MSAL instance is initialized and other token request configuration is ready, register these objects to MsalTokenHandler
via the registerDefault
static function:
./src/index.js
import { MsalTokenHandler } from '@appzmonster/msal-token-interceptor';
// Initialize msal.
const { msal, ...extensions } = configuration;
const silentRedirectRoutePath = new URL(extensions.silentRedirectUri).pathname;
const msalInstance = new PublicClientApplication(msal);
...
// Register MsalTokenHandler default fallback.
MsalTokenHandler.registerDefault(msalInstance, {
redirectUri: extensions.silentRedirectUri
});
MsalTokenHandler.registerDefault
arguments:
-
defaultMsalInstance
An instance of MSAL
PublicClientApplication
. If themsalInstance
argument of theMsalTokenHandler
in a fetch request is not set, it will fallback using thisdefaultMsalInstance
. -
defaultTokenRequest
A token request type. MSAL uses this for the
acquireTokenSilent
function.NOTE: This library only process
redirectUri
for fallback at the tine of writing. Property such asscopes
fallback is not implemented. I've omitted handlingscopes
fallback since every request requires different scopes, should not need a defaultscopes
in place (except login which MSAL automatically injectsopenid
andprofile
, i think).
You have to code the following for each fetch request without fallback more or less:
...
// Inject msal and silent redirect uri.
const { msal } = useMsal();
const msalInstance = msal.instance;
const { silentRedirectUri } = useSilentRedirectUri();
// Fetch request.
let response = await fetch.with(new MsalTokenHandler({
scopes: [ 'User.Read' ], redirectUri: silentRedirectUri
}, msalInstance))
(
"https://graph.microsoft.com/v1.0/me", {
method: 'GET'
});
With fallback, you code the following:
// No more injection required.
let response = await fetch.with(new MsalTokenHandler({
scopes: [ 'User.Read' ]}))
(
"https://graph.microsoft.com/v1.0/me", {
method: 'GET'
});
Fetch-interceptor Dependency
MsalTokenHandler
uses fetch-interceptor. Therefore, you need to make sure to enable request interceptor support on Fetch API before using MsalTokenHandler
. You'll see console error if MsalTokenHandler
fails to detect fetch-interceptor.
Enable request interceptor feature on Fetch API:
// Fetch-interceptor package
import { initialize } from '@appzmonster/fetch-interceptor';
// Enable fetch interceptor support on Fetch API.
initialize();
...
// Now you can use MsalTokenHandler in fetch api.
let response = await fetch
.with(new MsalTokenHandler({ scopes: [ 'User.Read' ]}))
(
"https://graph.microsoft.com/v1.0/me", {
method: 'GET'
});
Only Support Fetch API
MsalTokenHandler
uses fetch-interceptor
and fetch-interceptor
extends Fetch API to support request interceptor. Other XHR API such as Axios cannot use this library.
There are many OAuth 2.0 and OpenId Connect compliant clients out there and most are developed to work specifically for specific auth server (Identity Provider). For example, Google has its own client for Google Identity Platform. A more agnostic client and OpenId Connect compliant client is oidc-client. This is the best generic i've come across so far to my knowledge. I've tried using oidc-client to authenticate with Microsoft Identity Platform directly without going through Identity Server and it works but only limited to 1 single Azure tenant and not the "common" or "organization" multiplexer. The argument is OpenId Connect spec mentions the authority and token issuer must be the same. For Microsoft Identity Platform, it is more advance such that it supports sort of multiplexer "common" or "organizations" authority which is different from the token issuer (token issuer is the actual tenant itself), thus failing the token validation in oidc-client. If we want to extend this library to support "one-stop" solution, the best approach is to create an interface and use adapter design pattern to adapt all different clients against the interface specification. Probably the community can contribute on that.
Copyright (c) 2021 Jimmy Leong (Github: appzmonster). Licensed under the MIT License.