Using MSAL.js 2.0 in SharePoint Framework (SPFx)

Using MSAL.js 2.0 in SharePoint Framework (SPFx)

Since Jul-20 this year the MSAL.js 2.0 library is generally available (GA). This is a big step forward as there are still issues in authentication and authorization with Azure AD applications such as Microsoft Graph. For instance using Safari Browser and the Intelligent Tracking Protection (ITP) which will not work with implicit flow or iFrames as used by SharePoint Framework (SPFx). The ‘AADSTS50058: A silent sign-in request was sent but no user is signed in.‘ is a typical error in this scenario.

The alternative is MSAL.js 2.0 using the authorization code flow where this small little post wants to show how to implement it.

Preparation

Once we have setup our standard webpart we need to install MSAL.js 2.0 by

npm install @azure/msal-browser

Next we need to register an application in Azure AD.

What’s important here, is to use “Single-page-application (SPA) and give it a Redirect URI. This should match your page later where you want to instantiate your webpart (Hey this sounds like showstopper?) and is case sensitive but in fact it must only match your request (incl. wildcard option) and the Uri needs to be a valid one otherwise you would receive a timeout.

In case you have third party cookies disabled AND cannot use a popup to verify user login you would need a redirect: The Url must match your page with your webpart.

For details on redirect Uri restrictions click here.

From the app registration the app id, redirect uri and tenant url will be needed later on. Last point here is granting some permissions. In this demo case Mail.Read is sufficient.

To achieve a 100% silent retrieval an admin consent for the permission is already granted. The code of this solution does not handle user consent.

Basic implementation

Now implementation can start. First, some values, needed for MSAL, can be made configurable. Therefore webpart properties are adjusted:

export interface IMyMailsWebPartProps {
  applicationID: string;
  redirectUri: string;
  tenantUrl: string;
}
// ...
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
    return {
      pages: [
        {
          header: {
            description: strings.PropertyPaneDescription
          },
          groups: [
            {
              groupName: strings.BasicGroupName,
              groupFields: [
                PropertyPaneTextField('applicationID', {
                  label: strings.ApplicationIDFieldLabel
                }),
                PropertyPaneTextField('redirectUri', {
                  label: strings.RedirectUriFieldLabel
                }),
                PropertyPaneTextField('tenantUrl', {
                  label: strings.TenantUrlFieldLabel
                })
              ]
            }
          ]
        }
      ]
    };
  }

This properties also need to be given to the top level React component but additionally needed are current user’s email and the HttpClient used to retrieve Microsoft Graph data.

import { HttpClient } from "@microsoft/sp-http";

export interface IMyMailsProps {
  applicationID: string;
  redirectUri: string;
  tenantUrl: string;
  userMail: string;
  httpClient: HttpClient;
}

First in the constructor the MSAL client can be established:

private myMSALObj: PublicClientApplication;

  constructor(props) {
    super(props);

    const msalConfig = {
      auth: {
        authority: `https://login.microsoftonline.com/${this.props.tenantUrl}`,
        clientId: this.props.applicationID,
        redirectUri: this.props.redirectUri
      }
    };
    
    this.myMSALObj = new PublicClientApplication(msalConfig);
    
    this.state = {
      mails: []
    };
    ....
  }

Now everything is ready to use. At first a check is made if there are any users already authenticated. If so only an access token needs to be acquired to retreive mails. If not, a login will be tried on several options and then the request for current user’s emails is executed towards Microsoft Graph.

private loadMails = () => {
    const accounts = this.myMSALObj.getAllAccounts();
    if (accounts !== null) {
      this.handleLoggedInUser(accounts);
    }
    else {
      this.loginForAccessTokenByMSAL()
      .then((token) => {
        this.getMailsFromGraph(token).then(mails => {
          this.setState(() => {
            return { mails: mails };
          });      
        });
      });
    }    
  }

Authenticated user: Acquire token

In case a user was already authenticated (and is stored in cache, by default in sessionStorage but this is configurable in “our” msalConfig) just a check is needed to evaluate the right one and then the access token for that case can be acquired (from cache as well or refreshed, MSAL will handle). With a given access token user’s mails can be retrieved.

private handleLoggedInUser(currentAccounts: AccountInfo[]) {
    let accountObj = null;
    if (currentAccounts === null) {
      // No user signed in
      return;
    } else if (currentAccounts.length > 1) {
        // More than one user is authenticated, get current one 
        accountObj = this.myMSALObj.getAccountByUsername(this.props.userMail);
    } else {
        accountObj = currentAccounts[0];
    }
    if (accountObj === null) {
      this.acquireAccessToken(this.ssoRequest, accountObj)
      .then((accessToken) => {
        this.getMailsFromGraph(accessToken).then(mails => {
          this.setState(() => {
            return { mails: mails };
          });      
        });
      });
    }    
  }

No authenticated user: Login

If no authentication took place so far the loginForAccessTokenByMSAL function is tried silently. If this is not successful cause “InteractionRequired” another attempt by a popup is tried, finally also the redirect option can be tried (see below).

private ssoRequest: AuthorizationUrlRequest = {
    scopes: ["https://graph.microsoft.com/Mail.Read"]    
  };

private async loginForAccessTokenByMSAL(): Promise<string> {
    this.ssoRequest.loginHint = this.props.userMail;  
    return this.myMSALObj.ssoSilent(this.ssoRequest).then((response) => {
      return response.accessToken;  
    }).catch((error) => {  
        console.log(error);
        if (error instanceof InteractionRequiredAuthError) {
          return this.myMSALObj.loginPopup(this.ssoRequest)
          .then((response) => {
            return response.accessToken;
          }) 
          .catch(error => {
            if (error.message.indexOf('popup_window_error') > -1) { // Popups are blocked
              return this.redirectLogin(this.ssoRequest);
            }            
          });
        } else {
            return null;
        }
    });  
  }

But even in private sessions or generally with third party cookies turned off, as a login is in place inside SPFx a popup or redirect window do not need interaction but close automatically within 1s. This is because SPFx already established a user session.
Once the token is available either from silent or popup login a request towards Microsoft Graph is nothing special anymore. Only the Header with bearer token authorization needs to be constructed and then the correct endpoint can be used:

private getMailsFromGraph = async (accessToken: string): Promise<any> => {
    if (accessToken !== null) {
      const graphMailEndpoint: string = "https://graph.microsoft.com/v1.0/me/messages";
      return this.props.httpClient
        .get(graphMailEndpoint, HttpClient.configurations.v1,
          {
            headers: [
              ['Authorization', `Bearer ${accessToken}`]
            ]
          })
        .then((res: HttpClientResponse): Promise<any> => {
          return res.json();
        })
        .then((response: any) => {
          let mails: IMail[] = [];
          response.value.forEach((m) => {
            mails.push({from: m.from.emailAddress.address, subject: m.subject});
          });
          return mails;
        });
      }
      else {
        console.log("Error retrieving token");
        return [];
      }
  }

The webpart

That’s all. The simple ‘Hello World’ webpart could be adapted now like this:

Finally a click on the button ‘Get mails’ will execute the loadMails function from above and some mails will be rendered:

Login by page redirect

A redirect works slightly different. The function initiates a redirect to a Microsoft login page. But as said a user session is already there with SPFx so it gets directly back to the given redirectURI which is now essential in this case that it gets back to the correct SP page. Now the situation needs to be handled, which could be done in the constructor as such a redirect totally reloads the page (!!).

constructor(props) {
    ...
    this.myMSALObj.handleRedirectPromise().then((tokenResponse) => {      
      if (tokenResponse !== null) {
        const access_token = tokenResponse.accessToken;
        this.getMailsByMSAL(access_token).then(mails => {
          this.setState(() => {
            return { mails: mails };
          });      
        });
      } else 
      {
         // In case we would like to directly load data in case of NO redirect:
        // const currentAccounts = this.myMSALObj.getAllAccounts();
        // this.handleLoggedInUser(currentAccounts);
      }      
    }).catch((error) => {
        console.log(error);
        return null;
    });
  }

So what happens exactly in this “handleRedirectPromise” and where does the token come from? If a debugger would stop right here or this code wouldn’t exist (comment out for instance) in the browser window could be detected the following in the Url:

Page redirect – The authorization code

What’s retrieved by the redirect url as an anchor is the so called authorization code. MSAL will in a next step use this to retrieve an access token. So after the code is finished a “normal” url can be detected:

Page redirect – After authorization code handled

SPA vs webpart on portal pages considerations

Originally MSAL.js 2.0 is dedicated to single page applications (SPA). So what about the scenario we have multiple webparts (same or different) on a same page using that scenario? Especially with the above described redirect login it can cause strange situations having several webparts or instances on the same page. Try it out with this example.

You would need to insure that each page only has one “primary” webpart which is responsible for establishing the login. All the other webparts consuming MSAL tokens then should be enforced to only “acquire” an access token. Furthermore if there are different webparts on the page with different scopes and it’s controllable it would make sense to summarize required permissions under the same app id as only then the scenario (one webpart only responsible for login) makes sense.

Hope this post helps a bit to understand and implement even more complex scenarios with SPFx and MSAL.js 2.0. Of course it’s not the one and only answer on issues with the standard way of SPFx accessing 3rd party Apis with MSGraphClient  or AadHttpClient but in many scenarios it might be a suitable solution.
For your reference there is also a github repository with the full code example.

Markus is a SharePoint architect and technical consultant with focus on latest technology stack in Microsoft 365 and SharePoint Online development. He loves the new SharePoint Framework as well as some backend stuff around Azure Automation or Azure Functions and also has a passion for Microsoft Graph.
He works for Avanade as an expert for Microsoft 365 Dev and is based in Munich.
Although if partially inspired by his daily work opinions are always personal.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s