0
(0)

Most Identity Providers like Azure AD use OAuth 2.0 as an open standard for authentication and authorization, In this blog post I will show you how to implement the OAuth 2.0 flow for Microsoft Teams Tabs using SharePoint Framework and the Teams Client SDK. A basic understanding of OAuth 2.0 is required for reading this post, so if you are new to this concept, please read this post or this good overview which is easier to follow and understand it.

Source code for this sample is available here in Github ๐Ÿ™‚

Microsoft Teams Tabs Development

It’s important to know that Tabs are simple iframes point to domains declared in the app manifest, iframes do have stronger security, which can be useful for you and for the end use, so if you try to redirect the end user to the login page of a provider like Box.com you will get an error, but fortunately the Teams client SDK provides us a simple way to overcome this issue.

Before implementing the OAuth flow, I want to talk about different ways of developing a Microsoft Teams Tab as each approach has its pros and cons.

SharePoint Framework

From SharePoint Framework 1.8, you can build Tabs and host them in SharePoint, so your application will surface using a web part experience, below you can find the pros and cons of using SharePoint Framework to build Tabs:

Advantages

  • Automatic hosting
  • Automatic deployment
  • Single sign-on (through SharePoint)
  • CDN optimized
  • Easy debugging (through workbench)
  • Access to the SharePoint context
  • Consume Microsoft Graph API and other Azure AD secured Rest APIs (facilitated by AadHttpClientFactory class)
  • Custom properties to configure the tab
  • Reusability (can be a web part in SharePoint and also a tab in Microsoft Teams)

Disadvantages

  • Static tabs are not supported yet (but it’s in their roadmap and is planned to be supported in the future)
  • Only Tabs supported in a SharePoint framework solution (Bots, Connectors and Messaging Extensions are not supported)
  • Not easy to add them to a channel or a team programmatically as the URL is dynamic and can be hosted in different SharePoint team site
  • No server-side code (You can use Azure Functions as server-side code which you have to pay for the service)

Node.js/ASP.NET

You can also create a solution using ASP.Net (MVC or Core) or Node.js and host it in Azure, AWS or in anywhere on the internet. You can also use the Yeoman Generator for Microsoft Teams which makes it easier for you to develop your Teams solution.

Advantages

  • Support all project types such as Bots, Messaging Extensions, Tabs, Connectors and Outgoing Webhooks
  • Support server-side code (like Express for node.js project)
  • Support routing
  • Static page URL
  • Provisioning is simple
  • Configurable (through query string parameters for example)

Disadvantages

  • Manual deployment
  • Not easy to debug (you have to use ngrok for node.js projects and pay for a subscription if you don’t want to change some settings each time you run the project)
  • SSO is not supported (Yeoman generator is recently added this feature but I haven’t tried it yet)
  • You will need a custom domain as Azurewebsite.net domain doesn’t support for SSO (as it might be a security risk)
  • Hosting is not free (as it needs to be deployed in a cloud service or a custom hosting)

For this project, I will show you how to implement it with SharePoint Framework as it gives us more benefits.

Register Your App

I use Box.com as an example because it has pre-build user interface components such as UI Elements which helps us to focus on the code rather than the UI, but you can use any other providers. The first step is to register your app, each provider has different way of registering the app or even different name, for example Application Registration in Azure or Connected App in salesforce, for Box.com you can navigate to the developer console and select the Create a new app button. You have different options for creating your app such as Enterprise integration or Partner Integration, but let’s select Custom App and then select Standard OAuth 2.0 (User Authentication).

Give it a name and create the custom app. Navigate to the configuration page and here you can find the Client ID and the Client secret, copy the values as we need them later (you may see different names for some providers, for example when you create a connected app in Salesforce, instead of Client ID and Client Secret you will see Consumer Key and Consumer Secret but they do the same thing).

If you scroll down you will find the Application scopes, based on your application you can select or deselect the scopes, for this sample, let’s stick with the default scope which is Read and write files and folders stored in Box.

We also need to change the Redirect URI and the CORS domains, but we will get back to them later.

Create a SharePoint Framework solution

To create a SharePoint Framework solution (SPFx) please follow this instruction, here is the settings I set for this solution.

Install dependencies

Here are the dependencies we need to install (they are all box-ui-elements dependencies)

npm i node-sass axios classnames draft-js form-serialize jsuri pikaday react-animate-height react-intl@2.8.0 react-measure react-process-string react-router-dom react-tether react-textarea-autosize scroll-into-view-if-needed tabbable react-virtualized react-modal js-sha1 @popperjs/core moment query-string filesize postcss-loader sass-loader box-ui-elements --save-dev

Step 1: User interaction (login in button)

First we need to build an UI for the user to interact with which is commonly a button called “Sign in” or “Log in“.

I added a function component to the solution to handle the login process:

import * as React from "react";
import { PrimaryButton } from "office-ui-fabric-react";
import { boxService } from "../../../services/services";
import styles from "./BoxContentExplorer.module.scss";
import Loading from "../../../utilities/loading";
const loadingImage: any = require("./assets/loading.gif");

const logo: any = require("./assets/boxLogo.png");

const BoxLogin = () => {
  const [isLoading, setIsLoading] = React.useState(true);

  const openLoginPopUp = async () => {
    setIsLoading(true);
    boxService.GetBoxAccessToken()
    .then(res => setIsLoading(false))
    .catch(err=>setIsLoading(false));
  };

  return (
    <div>
      <Loading imageSrc={loadingImage} hidden={isLoading} />
      <div className={styles.login} hidden={!isLoading}>
        <div className={styles.loginContainer}>
          <img src={logo} alt="Box logo" />
          <p>Please log in to access to this folder</p>
          <PrimaryButton
            className={styles.loginButton}
            text="Log in"
            onClick={openLoginPopUp}
          />
        </div>
      </div>
    </div>
  );
};

export default BoxLogin;

It would look like this:

Step 2: Authentication start page and complete page

We need two pages, one for redirecting the user to the authorization page of the identity provider to sign in and get consent for the scopes required for our apps, and one for check the returned state and return the code to the success callback function (commonly known as Redirect page and must be an absolute URI).

You can host those pages anywhere like Azure, AWS or your private host, but for this sample I’m going to host them in SharePoint, also bear in mind that both pages must be on the same domain otherwise you will get an error.

Start page

The start page generates random state data, saves it to the local storage for future validation and redirects to the identity provider’s /authorize endpoint which in our sample is https://account.box.com/api/oauth2/authorize.

microsoftTeams.initialize();
microsoftTeams.getContext(function (context) {
    // Generate random state string and store it, so we can verify it in the callback
    let state = newGuid();
    var callbackUrl ="https://ramindev.sharepoint.com//SitePages/loginComplete.aspx";
    
    var clientId = getUrlParameter("clientId");
    var authorizeUrl = getUrlParameter("authorizationUrl");
    localStorage.setItem("bt.state", state);
    localStorage.removeItem("simple.error");
    
    let queryParams = {
      client_id: clientId,
      response_type: "code",
      redirect_uri: callbackUrl,
      state: state,
    };
    // Go to the Box.com authorization endpoint.
    let authorizeEndpoint = `${authorizeUrl}?${toQueryString(queryParams)}`;
    window.location.assign(authorizeEndpoint);
});
Sign in page in the identity provider’s /authorize endpoint
Grant access to the tab

Complete page (Redirect page)

When the user signed in and granted access to the tab, the provider takes the user to the redirect page with an access token or code, then it checks that the returned state value matches what was saved earlier, and calls the successCallback function if the state is valid and the code is returned, or calls the failureCallback function if something went wrong.

var code = getUrlParameter('code');
var state = getUrlParameter('state');

microsoftTeams.initialize();

let expectedState = localStorage.getItem("bt.state");
if (expectedState !== state ) {
    // State does not match, report error
    microsoftTeams.authentication.notifyFailure("StateDoesNotMatch");
} else {
    // Success: return token information to the tab
    microsoftTeams.authentication.notifySuccess(code);
}

Now we can upload these pages to SharePoint (I uploaded them in the root site collection) and make sure everyone has access to them.

Update your app’s configuration

We need to update the OAuth redirect URI in the app’s configuration page we setup earlier:

And also the CORS domains:

Step 3:Authenticate method

The Teams client SDK provides a method which opens the start page in an iframe in a pop-up window and you can register the successCallBack and failureCallback function with this method:

private Authenticate(
    ClientId: string,
    AuthorizationUrl: string
  ): Promise<any> {
    const authenticationUrl = `${location.protocol}//${location.hostname}/sitepages/${config.authenticatePage}`;
    return new Promise((resolve, reject) => {
      const url = `${authenticationUrl}?clientId=${ClientId}&authorizationUrl=${AuthorizationUrl}`;
      this.teamsContext.teamsJs.authentication.authenticate({
        url,
        width: 500,
        height: 600,
        successCallback: (result) => resolve(result),
        failureCallback: (reason) => {
          this.errorMessage.set("Authentication failed, please try again!");
          reject(reason);
        },
      });
    });
  }

Step 4: Get access token

Once we get the authorization code, we need to exchange the authorization code for an access token (the authorization code is only valid for one minute).

private async GetAccessToken(code: string) {
    const data = {
      grant_type: "authorization_code",
      client_id: config.boxClientId,
      client_secret: config.boxClientSecret,
      code: code,
    };
    const response = await axios({
      url: config.boxGetTokenUrl,
      method: "POST",
      headers: { "content-type": "application/json" },
      data: JSON.stringify(data),
    });
    return response.data;
}

Then we can store it local or session storage to avoid this process in each refresh.

this.token={
  AccessToken:tokenObject["access_token"],
  ExpiresIn:tokenObject["refresh_token"],
  RefreshToken:tokenObject["expires_in"]
};
this.boxToken.set(this.token.AccessToken);
localStorage.setItem(
  `${userObjectId}-boxAccessToken`,
  JSON.stringify(this.token)
);

This token is only valid for certain amount of time (usually 1 hour) and you have to revoke the token, hard coding client secret and keeping the tokens in the client side is not recommended and you should secure them (in another post I will show you how securely keep them in Azure Key Vault and use Azure function to set/get them), but for this sample I tried to keep it simple and give you the idea of how to implement the flow.

Step 5: Update the UI with the access token

Once we get the access token we can make other calls to get data from the resources we are allowed to, in this sample as box-ui-elements gives us everything as a simple component we don’t need to be worry about the UI, you can simply set the access token and the folder Id, and it renders the UI for you.

<ContentExplorer
  language="en-US"
  messages={messages}
  token={boxToken}
  contentPreviewProps={{
    contentSidebarProps: {
      hasActivityFeed: true,
      hasSkills: true,
      hasMetadata: true,
      detailsSidebarProps: {
        hasProperties: true,
        hasNotices: true,
        hasAccessStats: true,
        hasVersions: true,
      },
    },
  }}
  rootFolderId={props.folderId ? props.folderId : 0}
/>

Deploy the app

To be able to use this app in Microsoft Teams we need to update the supportedHost in the manifest file:

"supportedHosts": ["SharePointWebPart","TeamsPersonalApp","TeamsTab"]

Now we can bundle and upload the package to the app catalog, after upload and deploy the package you can see a button in the ribbon which syncs your app to Teams:

Now open Microsoft Teams, go to the Apps, and you can find your app in the organization apps:

You can add it as a personal or teams tab.

Here is the final look:

Summary

What we have seen above is a simple Teams Tab uses the Teams Client SDK to implement the OAuth 2.0 flow. As Teams Tabs are iframes, it’s not possible to redirect users directly within the tab’s content, however the Teams Client SDK provides us a safe method to overcome this problem.

How useful was this post?

Click on a star to rate it!

Average rating 0 / 5. Vote count: 0

No votes so far! Be the first to rate this post.