Microsoft Graph Toolkit in a Teams application with yo teams (and SSO)

Microsoft Graph Toolkit in a Teams application with yo teams (and SSO)

Recently in a Microsoft community call the question came up why there is no sample yet of using the Microsoft Graph Toolkit (MGT) with the yeoman generator for Teams. Okay, your question, my order. Let’s go for it.

Although I like to dig behind the scenes and implement things step by step to get a real understanding it also makes sense to simplify things, especially when they reoccur. This is why it’s also worth to consider the great capabilities of Microsoft Graph Toolkit (MGT) which makes your life a lot easier (of course after you deeply understand authentication and token generation inside Microsoft Teams 😉 )

Getting started with yo teams and the Microsoft Graph Toolkit is very easy. First you need to setup your Teams application. Although other frontend apps like an action based Messaging extension with task modules would work similar, here a simple Teams Tab is used:

yo teams for a mgt tab

Except the usage of SSO all special features are omitted to keep it simple.

Next we need to install two more packages for the Microsoft Graph Toolkit:

npm i @microsoft/mgt @microsoft/mgt-react

Then we need to setup an app registration in Azure AD. It needs a name, a redirect Uri (https://xxxxx.ngrok.io/auth.html for the moment), support multi-tenant, the implicit flow (allow ID and access token) and User.Read and People.Read delegated permissions for now.

Once that is available implementation can be started. From the “getting started” documentation we know that first a provider needs to be established. This can be done in the React component:

import * as React from "react";
import * as microsoftTeams from "@microsoft/teams-js";
import { Providers, TeamsProvider } from "@microsoft/mgt";
import { Login, People } from "@microsoft/mgt-react";

export const PeopleMgtTabLogin = (props) => {
    
    TeamsProvider.microsoftTeamsLib = microsoftTeams;
    Providers.globalProvider = new TeamsProvider ({
        clientId: process.env.TAB_APP_ID!,
        authPopupUrl: '/auth.html',
        scopes: ["User.Read", "People.Read"]
    });

    return (
        <div>           
            <div>
                <Login />
            </div>
            <div>
                <People showMax={5} />
            </div>
        </div>
    );
}

The provider is simply taken from the TeamsJS sdk. It receives the client ID from the app created above, an authentication page and the required permission scope. The rest in this component is the use of an mgt <Login /> component and the mgt <People /> component.

To establish the authentication there is the need to add the referred auth.html page which should be located together with root pages for the tab application (so it can be later reached via https://<HOSTNAME>/auth.html)

The auth.html page

The content of that page is pretty straightforward as per documentation.

<!DOCTYPE html>
<html>
  <head>
    <script src="https://unpkg.com/@microsoft/teams-js/dist/MicrosoftTeams.min.js" crossorigin="anonymous"></script>
    <script src="https://unpkg.com/@microsoft/mgt/dist/bundle/mgt-loader.js"></script>
  </head>

  <body>
    <script>
      mgt.TeamsProvider.handleAuth();
    </script>
  </body>
</html>

Having that all pieces for an initial run of the application are there. Solution can be run and once the Tab is rendered a “Sign In” component is present, on click a popup with authentication and permission consent is shown while finally the People component shows the recent 5 contacts of the signed in user.

That’s nice and was very easy so far but WE MISS something and that is SSO … because we do not really want the users to sign in if already in Teams.

As you might now from lots of my previous posts on Teams Development SSO for Microsoft Teams consists of two parts:

  1. Grabbing the ID token in frontend [described in yo teams wiki]
  2. Exchanging the ID token to an access token with the on-behalf flow in the backend [described by Wictor]

Number 1 is already there if the solution was setup as shown above but number 2 needs to be implemented AND should reside in the backend. Why? Because there is a need for an app secret which you would not provide to the frontend (user) for sure. But instead executing the Microsoft Graph call in the backend this will now take part in the frontend because we do not care for that anymore. Now Microsoft Graph Toolkit is responsible for this.

What needs to be done are the following steps:

  1. Slightly adjust our app registration for the on-behalf flow
  2. Implement a backend service to exchange the ID token from the frontend with the on-behalf flow and return that token to the frontend
  3. Use a custom mgt provider consuming that access token

App registration for the on-behalf flow

For the on-behalf flow the app needs a secret which we securely store and consume from Microsoft key vault as we are professional developers (only me and Chuck Norris are allowed to have it in the local .env file 😉 ). We can also tick off the implicit flow ID token and access token which is not needed anymore. Furthermore

  • Expose an API and give the Api URI a name like api://xxxx.ngrok.io/<Your App ID> (xxxx depends on your current ngrok Url)
  • Set scope name to access_as_user and provide Admin & User messages
  • Add Teams Client 1fec8e78-bce4-4aaf-ab1b-5451cc387264 and Teams Web Client 5e3ce6c0-2b1f-4285-8d4b-75ee78787346 IDs under “Add a client application”

Implement backend service for on-behalf flow

Same as in one of my previous posts this can be quickly established.

  • Install the following packages followed by a gulp build
    npm install passport passport-azure-ad --save
    npm install @types/passport @types/passport-azure-ad --save-dev
    npm install axios querystring --save
  • Under ./src/app/api create a tokenRouter.ts
  • Load that tokenRouter in your server.ts by
    express.use("/api", tokenRouter({}));
    and don’t forget to import

The tokenRouter.ts now can have the following content:

import express = require("express");
import passport = require("passport");
import { BearerStrategy, IBearerStrategyOption, ITokenPayload, VerifyCallback } from "passport-azure-ad";
import qs = require("querystring");
import Axios from "axios";
import * as debug from "debug";
const log = debug("msteams");

export const tokenRouter = (options: any): express.Router => {
    const router = express.Router();
    const pass = new passport.Passport();
    router.use(pass.initialize());

    const bearerStrategy = new BearerStrategy({
        identityMetadata: "https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration",
        clientID: process.env.TAB_APP_ID as string,
        audience: `api://${process.env.PUBLIC_HOSTNAME}/${process.env.TAB_APP_ID}` as string,
        loggingLevel: "warn",
        validateIssuer: false,
        passReqToCallback: false
    } as IBearerStrategyOption,
        (token: ITokenPayload, done: VerifyCallback) => {
            done(null, { tid: token.tid, name: token.name, upn: token.upn }, token);
        }
    );
    pass.use(bearerStrategy);

    const exchangeForToken = (tid: string, token: string, scopes: string[]): Promise<string> => {
        return new Promise((resolve, reject) => {
            const url = `https://login.microsoftonline.com/${tid}/oauth2/v2.0/token`;
            const params = {
                client_id: process.env.TAB_APP_ID,
                client_secret: process.env.TAB_APP_SECRET,
                grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
                assertion: token,
                requested_token_use: "on_behalf_of",
                scope: scopes.join(" ")
            };

            Axios.post(url,
                qs.stringify(params), {
                headers: {
                    "Accept": "application/json",
                    "Content-Type": "application/x-www-form-urlencoded"
                }
            }).then(result => {
                if (result.status !== 200) {
                    reject(result);
                } else {
                    resolve(result.data.access_token);
                }
            }).catch(err => {
                // error code 400 likely means you have not done an admin consent on the app
                reject(err);
            });
        });
    };

    router.get(
        "/accesstoken",
        pass.authenticate("oauth-bearer", { session: false }),        
        async (req: any, res: express.Response, next: express.NextFunction) => {
            const user: any = req.user;
            try {
                const accessToken = await exchangeForToken(user.tid,
                    req.header("Authorization")!.replace("Bearer ", "") as string,
                    ["https://graph.microsoft.com/user.read","https://graph.microsoft.com/people.read"]);
                
                res.json({ access_token: accessToken});
            } catch (err) {
                if (err.status) {
                    res.status(err.status).send(err.message);
                } else {
                    res.status(500).send(err);
                }
            }
        });
    return router;
};

As known from my previous samples maybe, the request comes in with the ssoToken generated from TeamsJS sdk. This token is taken for authentication first with the BearerStrategy and afterwards exchanged for an accessToken within the exchangeForToken function by calling the on-behalf flow. In my previous cases this accessToken was directly used for any requests towards Microsoft Graph inside this router but here we simply return it back to the client.

Use custom mgt provider to consume access token

The client component now looks slightly different than the first sample above:

import * as React from "react";
import { useState, useEffect } from "react";
import Axios from "axios";
import { Provider, Flex, Text, Header } from "@fluentui/react-northstar";
import { Providers, SimpleProvider, ProviderState } from "@microsoft/mgt";
import { People, Person, PersonViewType } from "@microsoft/mgt-react";
import { useTeams } from "msteams-react-base-component";
import * as microsoftTeams from "@microsoft/teams-js";
import jwtDecode from "jwt-decode";

export const PeopleMgtTabSSO = (props) => {
    const [{ inTeams, theme, context }] = useTeams();
    const [name, setName] = useState<string>();
    const [error, setError] = useState<string>();
    const [ssoToken, setSsoToken] = useState<string>();

    useEffect(() => {
        if (inTeams === true) {
            microsoftTeams.authentication.getAuthToken({
                successCallback: (token: string) => {
                    const decoded: { [key: string]: any; } = jwtDecode(token) as { [key: string]: any; };
                    setName(decoded!.name);
                    microsoftTeams.appInitialization.notifySuccess();
                    setSsoToken(token);                    
                },
                failureCallback: (message: string) => {
                    setError(message);
                    microsoftTeams.appInitialization.notifyFailure({
                        reason: microsoftTeams.appInitialization.FailedReason.AuthFailed,
                        message
                    });
                },
                resources: [`api://${process.env.PUBLIC_HOSTNAME}/${process.env.TAB_APP_ID}` as string]
            });
        }
    }, [inTeams]);

    useEffect(() => {
        if (ssoToken) {
            let provider = new SimpleProvider((scopes: string[]): Promise<string> => {
                return Axios.get(`https://${process.env.PUBLIC_HOSTNAME}/api/accesstoken`, {
                                responseType: "json",
                                headers: {
                                    Authorization: `Bearer ${ssoToken}`
                                }
                            }).then(result => {
                                const accessToken = result.data.access_token;                   
                                return accessToken
                            })
                            .catch((error) => {
                                console.log(error);
                                return "";
                            });
            });
            Providers.globalProvider = provider;
            Providers.globalProvider.setState(ProviderState.SignedIn);
        }
    },[ssoToken]);

    return (
        <Provider theme={theme}>
            <Flex fill={true} column styles={{
                padding: ".8rem 0 .8rem .5rem"
            }}>
                <Flex.Item>
                    <Header content="This is your tab" />
                </Flex.Item>
                <Flex.Item>
                <div>
                    <div>
                        <Text content={`Hello ${name}`} />
                    </div>
                    {error && <div><Text content={`An SSO error occurred ${error}`} /></div>}

                    <div>
                        <Person personQuery="me" view={PersonViewType.twolines} />
                    </div>
                    <div>
                        <People showMax={5} />
                    </div>
                </div>
                </Flex.Item>
            </Flex>
        </Provider>
    );
}

Let’s start at the bottom. The same <People /> component is used but the <Login /> isn’t needed anymore. It’s replaced by <Person /> to still have information visible for the current user. But how does the authentication work now so those components can retrieve and render data?

First inside the upper useEffect hook the microsoftTeams.authentication.getAuthToken function has a successHandler where the ssoToken in the frontend is generated. This is similar to all previous samples.

The lower useEffect hook now is fired on change of the ssoToken only. If this is available a new custom SimpleProvider is established. As parameter it gets a function how to retrieve the accessToken. This is in fact a call against our previously created /api/accesstoken endpoint.

After the SimpleProvider is instantiated it is set as the globalProvider (known from TeamsProvider above) AND very important the state of this provider is set to “SignedIn”. This is essential here because it is the signal for the components to start retrieving their data. And now when they do it behind the scenes the function to retrieve the accessToken is called also. This you will detect in that order once you start debugging the solution.

The final result in this “SSO” case will look like this:

The final result using SSO auth

Update: Meanwhile there is also a dedicated Teams SSO provider live as part of Microsoft Graph Toolkit v2.3. For details refer to this Microsoft blog post.

Using new TeamsMsal2Provider for SSO

Meanwhile also the new Msal2 based Teams-SSO provider was officially released. And as this sample before was not far away it’s easy to show it’s usage in this new final section.

First you need to have the Microsoft Graph Toolkit installed as mentioned above but at least in version 2.3. Next there is the need for another package, the TeamsMsal2Provider itself:

npm install @microsoft/mgt-teams-msal2-provider 

The frontend component would simply look like this:

import * as React from "react";
import { useState, useEffect } from "react";
import { Provider, Flex, Text, Header } from "@fluentui/react-northstar";
import { Providers, ProviderState } from "@microsoft/mgt";
import { HttpMethod, TeamsMsal2Provider } from "@microsoft/mgt-teams-msal2-provider";
import { People, Person, PersonViewType } from "@microsoft/mgt-react";
import { useTeams } from "msteams-react-base-component";
import * as microsoftTeams from "@microsoft/teams-js";

export const PeopleMgtTabTeamsSSO = (props) => {
    const [{ inTeams, theme, context }] = useTeams();
    const [error, setError] = useState<string>();

    TeamsMsal2Provider.microsoftTeamsLib = microsoftTeams;

    useEffect(() => {
        if (inTeams === true) {
            let provider = new TeamsMsal2Provider({
                clientId: `${process.env.TAB_APP_ID}`,
                authPopupUrl: '',
                scopes: ['User.Read','People.Read'],
                ssoUrl: `https://${process.env.PUBLIC_HOSTNAME}/api/token`,
                httpMethod: HttpMethod.POST
              });
            Providers.globalProvider = provider;
            Providers.globalProvider.setState(ProviderState.SignedIn);
        }
    }, [inTeams]);

    return (
        <Provider theme={theme}>
            <Flex fill={true} column styles={{
                padding: ".8rem 0 .8rem .5rem"
            }}>
                <Flex.Item>
                    <Header content="This is your tab" />
                </Flex.Item>
                <Flex.Item>
                <div>                    
                    {error && <div><Text content={`An SSO error occurred ${error}`} /></div>}

                    <div>
                        <Person personQuery="me" view={PersonViewType.twolines} />
                    </div>
                    <div>
                        <People showMax={5} />
                    </div>
                </div>
                </Flex.Item>
            </Flex>
        </Provider>
    );
}

It looks much simpler than the other SSO sample. The main difference is that it’s not necessary to care for the SSO token anymore. Only the Provider needs to be established and configured. 3 parameters need explanation. The authPopupUrl needs to be a link to a simple page for authentication and consent handling. I kept it blank as I gave admin consent to my permissions. If you do not want this, the simple documentation explains what this page needs. The ssoUrl is a link to a backend service which is repsonsible for the accessToken generation. It’s the same on-behalf flow used above. The only difference is we need to use it as httpMethod: HttpMethod.POST

Otherwise a GET would come in unauthenticated and having all parts in a query string. This does not fit to our existing service but with a POST we need a slightly different endpoint than before but most of it looks similar:

router.post(
        "/token",
        pass.authenticate("oauth-bearer", { session: false }),
        async (req: express.Request, res: express.Response, next: express.NextFunction) => {
            const user: any = req.user;
            try {
                const accessToken = await exchangeForToken(user.tid,
                    req.header("Authorization")!.replace("Bearer ", "") as string,
                    ["https://graph.microsoft.com/user.read","https://graph.microsoft.com/people.read"]);
                
                res.json({ access_token: accessToken});
            } catch (err) {
                if (err.status) {
                    res.status(err.status).send(err.message);
                } else {
                    res.status(500).send(err);
                }
            }
        });

That’s all you need for the implementation if the new TeamsMsal2Provider. I think it should now be the preferable way. But for comparison and background explanation I keep all 3 variants in this post and also in the source code which you can find in my GitHub repository. There I was encapsulating all three shown react components (the one with <Login /> and the two with SSO) in one parent component simply commenting out the non-wanted ones …

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.

One thought on “Microsoft Graph Toolkit in a Teams application with yo teams (and SSO)

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