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.

5 thoughts on “Using MSAL.js 2.0 in SharePoint Framework (SPFx)

  1. Hey Markus,
    Great stuff. Was actually looking for someone who has implemented the msal 2.0.
    Just a question through, Microsoft now recommends the auth code grant flow with PKCE ( with the code challenge ). Did you ever try that out?

    Ashwin

    Like

  2. Hi Markus,
    Thanks a lot for this post and the github repo!
    As to the redirect URI, you can add a wildcard if you edit this through the manifest of the Azure AD App. And then you can use this in code:
    redirectUri: “https://”+ location.host + location.pathname

    Like

  3. Thanks so much for sharing! I’m struggling a little with whether to go the MSAL or MSGraphClient route. I’m building an SFPx web part that will use AD app-only access to search private M365 Group site name/description fields with the search query parameter on the MSGraph groups endpoint. I’m leaning towards the MSGraphClient simply because MSAL still feels a little beta-esque and I prefer to stick with more mature technologies.

    But you mentioned that ‘there are still issues in authentication and authorization with Azure AD applications such as Microsoft Graph’. Do you anticipate any issues in my scenario with the MSGraphClient?

    Thanks again for the all the helpful info! 🙂

    Like

    1. Hello Tracy,
      first of all thanks for the great feedback on my article. Much appreciated!
      Of course you should start with with integrated MSGraphClient if possible.
      I touched two issues inside the article:
      One was (at the time of writing) that the so-called “implict flow” which generated the token with MSGraphClient became more and more critical in working with new browsers (beginning with Safari). Microsoft is/was working on that to implement MSAL2.0 behind the scenes for SPFx as well.
      https://github.com/SharePoint/sp-dev-docs/issues/6135
      The second issue is, when you grant permissions for MSGraphClient, you grant it for all potential webparts/SPFx components in your tenants, not only for “your special webpart you implement at the moment”. You can overcome this by using “domain isolation” but that is not that easy. Or by using what I describe in my article.
      To explain this 2nd issue a bit more I like to fwd you to my colleage Wictor who has a great article on this:
      http://www.wictorwilen.se/sharepoint-framework-and-microsoft-graph-access-–-convenient-but-be-very-careful
      Hope this helps you a bit?

      Like

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