Integrate a chat bot in SharePoint portal using an SPFx extension, MSAL, LUIS and the Bot Framework

With the democratization of AI, integrate chat bots inside SharePoint portals may be more and more common in the near future. According to this trend, I wanted to explore this specific usage and especially the authentication mechanisms allowing deep integration in SharePoint based intranet solutions. Actually, to be truly efficient, a bot must propose personalized answers to the users by calling protected APIs like Microsoft Graph or custom APIs and it is precisely the pupose of this sample. The complete code, deployment and debuging instructions are available in the PnP repository here if you want to try this out:

Basically, this solution shows how to integrate a bot web chat (the React web chat component) inside a modern SharePoint site using an SPFx extension, the MSAL library and the Bot Framework. User intents are determined by a LUIS model and mapped to specific Microsoft graph queries in the bot logic. This is obviously a dummy example and in a real world scenario, you may want to use more complex dialogs/queries to help your users but the concept is still valid.

Create chat bots consuming Azure AD protected APIs

The main problematic: Let’s say you want to integrate a bot in your SharePoint portal. Depending of what you’re trying to achieve, you will probably need to call protected back-end APIs to retrieve relevant data. I’ve listed here multiple solutions that can be used to implement such a scenario in a chat bot context:

Method

Concept

Pros/Cons

The lazy way

Use application permissions instead delegated permissions in the Azure AD application. In this case, you don’t care about the user permissions since your application will use its own predefined permissions to perform queries.

Fairly easy to implement

Since you don’t care about the talking user, this solution is only suitable for very few cases (like administration automation operations). Otherwise, this solution can present obvious security risks.

The easy way

Use the bot back channel and perform queries in the SPFx code (or client side code in general), not in the bot itself. This is basically the approach shown in this article. In this kind of solution, your bot does not call any protected APIs. Instead, it uses the Bot Framework back channel mechanism to simply notify the client side code what queries to execute according to the user intents. Since SPFx provides an GraphHttpClient with seamless authentication, you don’t need to handle it yourself. In return, the client side code notify the bot through back channel (again) with the query result so they can be displayed in the chat window.

Implement the OAuth2 implicit grant flow to perform queries under the current user identity using his permissions. In this scenario the authentication flow is handled by specialized libraries like MSAL (Azure AD v2) or ADAL (Azure AD v1). The access token is passed through the back channel from the client side code to the bot. Then queries are performed in the bot code using this token.

Can be used with any Azure AD protected APIs, not only Microsoft Graph.

Implement the OAuth2 authorization code flow to perform queries under the current user identity using his permission. In this scenario, the authentication flow is handled by the bot itself. A sign-in link is generated and displayed in the channel interface (can be a web chat, Teams, Skype, etc…) redirecting the user to its login page. Once logged, the bot handles access token retrieval and use it to perform queries.

Quick words on the MSAL library usage with SharePoint and SPFx

In this sample, I chose the MSAL option instead of ADAL since it targets the Azure AD v2 endpoint. Actually, using either option will give the same results in this sample, but since Microsoft moves to MSAL, I decided to try it out starting from the PnP react-msal-msgraph sample. The MSAL library for JavaScript is available here: https://github.com/AzureAD/microsoft-authentication-library-for-js. The main difference between v1 and v2 is in v2, you can now define dynamic permissions through the « scope » parameter.

JavaScript

1

2

constscopes=["Directory.Read.All","User.Read"];

...

In this way, you don’t have to specify permissions directly in your app anymore. This approach gives you more flexibility in your implementation. However, using this library presents some caveats (as of 30/12/2017) listed as follows:

No Single-Sign-On. You will always see the login popup for the first time even if you are already connected. Unfortunately, this behaviour is hard coded in the library via the « prompt=select_account » query parameter (see this StackOverflow thread for more information). To get a SSO like, you will have to use, for now, ADAL.js instead. A good starting point on how to use it with SPFx can be found in the react-aad-implicitflow PnP Sample.

To be generic, you will have no choice to use an SPFx extension. I explain: actually, to get the authentication works properly with the MSAL library, you will need to respect at least two constraints:

The redirect URI you specify in the client-side code must match the one set in the Azure AD application (an error will be raised otherwise). To use the MSAL library, your code must instantiate an UserAgentApplication object as follows:

// This URL should be the same as the AAD app registered in registration portal

// This is this parameter getting login popup window to close

redirectUri:this.props.context.pageContext.site.absoluteUrl,

});

You have the choice here to set your own redirect URI. If you don’t specify any value, the current URL will be taken. In this case, it means you will need to add a redirect URL in your AAD app for every page in your portal where this code is used to respect this constraint. Not very convenient in a real world scenario…

For the login popup window to close, the page pointed by the redirect URL in your Azure AD application must have an instance of UserApplicationAgent. It means you need a custom component in that specific page. It can be a Web Part, a JavaScript snippet or, more appropriated, an SPFx extension (of ApplicationCustomizer type). For information, this behaviour is not an issue and it’s by design see https://github.com/AzureAD/microsoft-authentication-library-for-js/issues/174. I take this opportunity to mention the PnP react-msal-msgraph sample will only work in your workbench page for this reason.

The solution I found to respect these two constraints and still be generic is to set the site collection root URL as a redirect URI, both in the UserAgentApplication objectand in the Azure AD application:

Although it works, this behaviour can be very misleading because it means the redirect URl won’t be the final URL where the token is transmitted. This URL is only here to actually close the popup window…

Bot state and conversation history

Once logged, the access token is sent to the bot using the Bot Framework back channel via the « userAuthenticated » event and using the user mail as unique identifier to get corresponding data afterwards:

Client side code (SPFx extension)

JavaScript

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

private_sendAccessTokenToBot(token:string):void{

// Using the backchannel to pass the auth token retrieved from OAuth2 implicit grant flow

this._botConnection.postActivity({

type:"event",

value:{

accessToken:token,

userDisplayName:this.props.context.pageContext.user.displayName// For the welcome message

},

from:{

// IMPORTANT (1 of 2): USE THE SAME USER ID FOR BOT STATE TO BE ABLE TO GET USER SPECIFIC DATA

id:this.props.context.pageContext.user.email

},

name:"userAuthenticated"// Custom name to identify this event in the bot

})

.subscribe(

id=>{

// Show the panel only if the event has been well received by the bot (RxJs format)

In this sample and for development purpose, I used the « In-memory » bot storage (the data is cleared each time the bot is restarted). However, it is possible to use an Azure Table, CosmosDb or SQL Azure to store your information in a production scenario.

Also to keep conversation history, I store its id in the browser local storage using PnP utilities (15 minutes expiration). I use local storage instead session storage simply to be able to keep the conversation in multiple tabs (not possible with session storage). If you don’t do that, a new conversation will be created every time and previous data will be lost (not really in the back end, but for the user, yes). It can be frustrating so keeping the history is a crucial point:

JavaScript

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

// Get the conversation id if there is one. Otherwise, a new one will be created

constconversationId=pnp.storage.local.get(this.CONVERSATION_ID_KEY);

// Initialize the bot connection direct line

this._botConnection=newDirectLine({

secret:this._directLineSecret,

webSocket:false,// Needed to be able to retrieve history

conversationId:conversationId?conversationId:null,

});

this._botConnection.connectionStatus$

.subscribe((connectionStatus)=>{

switch(connectionStatus){

// Successfully connected to the converstaion.

caseConnectionStatus.Online:

if(!conversationId){

// Store the current conversation id in the browser session storage

// with 15 minutes expiration

pnp.storage.local.put(

this.CONVERSATION_ID_KEY,this._botConnection["conversationId"],

pnp.util.dateAdd(newDate(),"minute",15)

);

}

break;

}

});

Hope this quick sample can help you to create awesome chat bot solutions!