m365-msteams-northwind-app-samples

Sample code for Microsoft Teams application with SSO, Bots, Messaging extensions and other capabilities.

Teams App Camp

Lab B03: Enable Azure AD Single Sign-On

This lab is part of Path B, which begins with a Northwind Orders application that does not use Azure AD authentication.

In this lab you will implement an identity mapping scheme to allow Northwind users to log in using Azure AD when they’re in Microsoft Teams, even as they continue to log into Northwind’s bespoke authentication system outside of Teams.

The completed solution can be found in the B03-TeamsSSO-IdMapping folder, but the instructions will guide you through modifying the app running in your working folder.

Note that as you complete the labs, the original app should still work outside of Teams! This is often a requirement of ISV’s who have an app in market and need to serve an existing customer base outside of Teams.

In this lab you will learn to:

Features

Project structure

The project structure when you start of this lab and end of this lab is as follows. Use this depiction for comparison. On your left is the contents of folder B02-after-teams-login and on your right is the contents of folder B03-after-teams-sso.

Project Structure Before Project Structure After
B02-after-teams-login
    ├── client
    │   ├── components
    │       ├── navigation.js
    │   └── identity
    │       ├── 🔺identityClient.js
    │       └── login.html
    │       └── 🔺login.js
    │       └── teamsLoginLauncher.html
    │       └── teamsLoginLauncher.js
    │       └── userPanel.js
    ├── modules
    │   └── env.js
    │   └── northwindDataService.js
    │   └── teamsHelpers.js
    ├── pages
    │   └── categories.html
    │   └── categories.js
    │   └── categoryDetails.html
    │   └── categoryDetails.js
    │   └── myOrders.html
    │   └── orderDetail.html
    │   └── orderDetail.js
    │   └── privacy.html
    │   └── productDetail.html
    │   └── productDetail.js
    │   └── termsofuse.html
    ├── index.html
    ├── index.js
    ├── northwind.css
    ├── manifest
    │   └── 🔺makePackage.js
    │   └── 🔺manifest.template.json
    │   └── northwind32.png
    │   └── northwind192.png
    ├── server
    │   └── constants.js
    │   └── 🔺identityService.js
    │   └── northwindDataService.js
    │   └── server.js
    ├── 🔺.env_Sample
    ├── .gitignore
    ├── 🔺package.json
    ├── README.md
B03-after-teams-sso
    ├── client
    │   ├── components
    │       ├── navigation.js
    │   └── identity
    │       ├── 🆕aadLogin.html
    │       └── 🆕aadLogin.js
    │       ├── 🔺identityClient.js
    │       └── login.html
    │       └── 🔺login.js
    │       └── teamsLoginLauncher.html
    │       └── teamsLoginLauncher.js
    │       └── userPanel.js
    ├── modules
    │   └── env.js
    │   └── northwindDataService.js
    │   └── teamsHelpers.js
    ├── pages
    │   └── categories.html
    │   └── categories.js
    │   └── categoryDetails.html
    │   └── categoryDetails.js
    │   └── myOrders.html
    │   └── orderDetail.html
    │   └── orderDetail.js
    │   └── privacy.html
    │   └── productDetail.html
    │   └── productDetail.js
    │   └── termsofuse.html
    ├── index.html
    ├── index.js
    ├── northwind.css
    ├── manifest
    │   └── 🔺makePackage.js
    │   └── 🔺manifest.template.json
    │   └── northwind32.png
    │   └── northwind192.png
    ├── server
    │   └── constants.js
    │   └── 🔺identityService.js
    │   └── northwindDataService.js
    │   └── server.js
    ├── 🔺.env_Sample
    ├── .gitignore
    ├── 🔺package.json
    ├── README.md

Exercise 1: Register your application with Azure AD

In order for users to log into your application with Azure AD, you need to register it. In this exercise you will register your application directly in the tenant you created in Lab B02, however we’ll set it up so it can be used from other tenants, such as those of customers who purchase your application in the Microsoft Teams store. To learn more about multitenant applications, see this video.

Step 1: Register your application in Azure Active Directory

Navigating to the M365 Admin site

Navigating to the M365 Admin site

Opening App Registrations

Adding a registration

You will be presented with the “Register an application” form.

Register an application form

You will be presented with the application overview. There are two values on this screen you need to copy for use later on; those are the Application (client) ID 1️⃣ and the Directory (tenant) ID 2️⃣.

Application overview screen

When you’ve recorded these values, navigate to “Certificates & secrets” 3️⃣.

Adding a secret

Now you will create a client secret, which is like a password for your application to use when it needs to authenticate with Azure AD.

The secret will be displayed just this once on the “Certificates and secrets” screen. Copy it now and store it in a safe place.

Copy the app secret


😎 MANAGING APP SECRETS IS AN ONGOING RESPONSIBILITY. App secrets have a limited lifetime, and if they expire your application may stop working. You can have multiple secrets, so plan to roll them over as you would with a digital certificate.


😎 KEEP YOUR SECRETS SECRET. Give each developer a free developer tenant and register their apps in their tenants so each developer has his or her own app secrets. Limit who has access to app secrets for production. If you’re running in Microsoft Azure, a great place to store your secrets is Azure KeyVault. You could deploy an app just like this one and store sensitive application settings in Keyvault. See this article for more information.


Step 2: Grant your application permission to call the Microsoft Graph API

The app registration created an identity for your application; now we need to give it permission to call the Microsoft Graph API. The Microsoft Graph is a RESTful API that allows you to access data in Azure AD and Microsoft 365, including Microsoft Teams.

Confirm the permission

Grant consent

Step 3: Expose an API

The Northwind Orders app is a full stack application, with code running in the web browser and web server. The browser application accesses data by calling a web API on the server side. To allow this, we need to expose an API in our Azure AD application. This will allow the server to validate Azure AD access tokens from the web browser.

Click “Expose an API” 1️⃣ and then “Add a scope”2️⃣. Scopes expose an application’s permissions; what you’re doing here is adding a permission that your application’s browser code can use it when calling the server.

Expose an API

On the “Add a scope” flyout, edit the Application ID URI to include your ngrok URL between the “api://” and the client ID. Click the “Save and continue” button to proceed.

Set the App URI

Now that you’ve defined the application URI, the “Add a scope” flyout will allow you to set up the new permission scope. Fill in the form as follows:

Add the scope

Step 4: Authorize Microsoft Teams to log users into your application

Microsoft Teams provides a Single Sign-On (SSO) capability so users are silently logged into your application using the same credentials they used to log into Microsoft Teams. This requires giving Microsoft Teams permission to issue Azure AD tokens on behalf of your application. In this step, you’ll provide that permission.

Click “Expose an API” 1️⃣ and then “+ Add a client application” 2️⃣.

Open the Expose an API screen

Paste the ID for the Teams mobile or desktop app, 1fec8e78-bce4-4aaf-ab1b-5451cc387264 into the flyout 1️⃣ and check the scope you created earlier 2️⃣ to allow Teams to issue tokens for that scope. Then click “Add application” 3️⃣ to save your work.

Repeat the process for the Teams web application, 5e3ce6c0-2b1f-4285-8d4b-75ee78787346.

Add a client application

Exercise 2: Update the Teams application package

You’ve given Microsoft Teams permission to issue an Azure AD access token to your application (single sign-on), but Microsoft Teams doesn’t know about that. To provide the information Teams needs, we need to update the Teams Application package.

Step 1: Add the Azure AD information to the .env file

Open the .env file in your working directory and add these lines, filling in the information you saved during the app registration:

TENANT_ID=00000000-0000-0000-0000-000000000000
CLIENT_ID=00000000-0000-0000-0000-000000000000
CLIENT_SECRET=xxxxx

Step 2: Add the Azure AD information to your manifest

In your working directory, edit the manifest/manifest.template.json file.

Update the revision number to a larger version number:

  "version": "1.3.0",

Add a comma after the validDomains property and then add a new property, webApplicationInfo.

  "webApplicationInfo": {
      "id": "<CLIENT_ID>",
      "resource": "api://<HOSTNAME>/<CLIENT_ID>"
  }

This provides the Azure AD app registration information to Microsoft Teams for use in the SSO process. The finished manifest.template.json file is here for your reference.

Step 3: Modify the packaging code to include the CLIENT_ID

Open the file manifest/makePackage.js in your code editor. Notice that the code only makes a few of the environment variables available when it creates manifest.json. We just added some references to CLIENT_ID, so we need to handle them in the makePackage.js code.

In the if statement where the code checks for the TEAMS_APP_ID and HOSTNAME environment variables, add the CLIENT_ID as well like this:

    Object.keys(process.env).forEach((key) => {
        if (key.indexOf('TEAMS_APP_ID') === 0 ||
            key.indexOf('HOSTNAME') === 0 ||
            key.indexOf('CLIENT_ID') === 0) {
            data = data.split(`<${key}>`).join(process.env[key]);
            console.log (`Inserted ${key} value of ${process.env[key]}`);
        }
    });

Step 4: Rebuild the package

Now you can build a new package at any time with this command:

npm run package

Go ahead and run it, and two files, manifest.json and northwind.zip (the app package) should be updated in your manifest folder.

Exercise 3: Update the application source code

Step 1: Update your package.json file

The updated code will use two new npm packages on the server side:

To accommodate this, open the package.json file in your working directory and update the dependencies property to include these packages like this:

  "dependencies": {
    "@azure/msal-node": "^1.5.0",
    "azure-ad-jwt": "^1.1.0",
    "cookie-parser": "^1.4.6",
    "dotenv": "^10.0.0",
    "express": "^4.17.1",
    "node-fetch": "^3.1.1",
    "request": "^2.88.0"
  },

Then, from a command line in your working directory, install the package by typing

npm install

Step 2: Add an identity mapping screen

When a user logs into the app for the first time, we will log them into both Azure AD and the Northwind login scheme and save their Northwind employee ID in the Azure AD user profile. This is just one of many approaches; an application could save the Azure AD user ID and another system’s user ID in a database, or it could save the Azure AD user ID in the user’s profile within the other authentication. Any way you do it, the idea is to create a linkage between each Azure AD user and their user profile in the other identity service.

In this step, you’ll add a new web page to handle this identity mapping. If the application reads the Azure AD user’s employee ID and it’s blank, the user will be redirected here.

In your working folder, create a file /client/identity/aadLogin.html and paste in this markup:

<!doctype html>
<html>

<head>
    <meta charset="UTF-8" />
    <title>Northwind Login Page</title>
    <link rel="stylesheet" href="/northwind.css" />
    <link rel="icon" href="data:;base64,="> <!-- Suppress favicon error -->
</head>

<body>

    <div id="teamsLoginLauncher" style="display:none">
        <h2>Northwind Login (AAD Mapping)</h2>
        <br />
        <p>Please log in using your Northwind employee login it to link your Microsoft Teams login.</p>
        <button id="teamsLoginLauncherButton">Log in</button>
    </div>
    <div id="message" class="errorMessage"></div>

    <script type="module" src="/identity/aadLogin.js"></script>

</body>

</html>

Now create another file, /client/identity/aadLogin.js and insert this code:

import 'https://statics.teams.cdn.office.net/sdk/v1.11.0/js/MicrosoftTeams.min.js';
import {
    setLoggedinEmployeeId
} from './identityClient.js';

const teamsLoginLauncher = document.getElementById('teamsLoginLauncher');
const teamsLoginLauncherButton = document.getElementById('teamsLoginLauncherButton');

microsoftTeams.initialize(async () => {

    const authToken = await new Promise((resolve, reject) => {
        microsoftTeams.authentication.getAuthToken({
            successCallback: (result) => { resolve(result); },
            failureCallback: (error) => { reject(error); }
        });
    });

    const response = await fetch(`/api/validateAadLogin`, {
        "method": "post",
        "headers": {
            "content-type": "application/json",
            "authorization": `Bearer ${authToken}`
        },
        "body": JSON.stringify({
            "employeeId": 0
        }),
        "cache": "no-cache"
    });
    if (response.ok) {
        const data = await response.json();
        if (data.employeeId) {
            // If here, AAD user was mapped to a Northwind employee ID
            setLoggedinEmployeeId(data.employeeId);
            window.location.href = document.referrer;
        }
    } else if (response.status === 404) {

        // If here, AAD user logged in but there was no mapping to an employee ID. Get one now.
        teamsLoginLauncherButton.addEventListener('click', async ev => {
            microsoftTeams.authentication.authenticate({
                url: `${window.location.origin}/identity/login.html?teams=true`,
                width: 600,
                height: 535,
                successCallback: async (northwindCredentials) => {
                    const response = await fetch(`/api/validateAadLogin`, {
                        "method": "post",
                        "headers": {
                            "content-type": "application/json",
                            "authorization": `Bearer ${authToken}`
                        },
                        "body": JSON.stringify({
                            "username": northwindCredentials.username,
                            "password": northwindCredentials.password
                        }),
                        "cache": "no-cache"
                    });
                    setLoggedinEmployeeId(northwindCredentials.employeeId);
                    window.location.href = document.referrer;
                },
                failureCallback: (reason) => {
                    throw `Error in teams.authentication.authenticate: ${reason}`
                }
            });
        });
        teamsLoginLauncher.style.display = "inline";

    } else {
        console.log(`Error ${response.status} on /api/validateAadLogin: ${response.statusText}`);
    }
});

This code uses the Teams JavaScript SDK to obtain an Azure AD token using microsoftTeams.authentication.getAuthToken(), and then it calls the server side at /api/validateAadLogin using this token. The server will read the user’s employeeId and return it. If the employee ID is not found, the server returns an HTTP 404 error and the code prompts the user to log in via the Northwind login page. When the user logs in, the code passes his or her credentials back to /api/validateAadLogin, which looks up the employeeId and writes it to the user’s profile.

Step 3: Return the username and password from the login page

Open the file /client/identity/login.js in your code editor, and find the call to notifySuccess(). Modify it to return the username and password so the aadLogin.html page can provide it to the server.

    microsoftTeams.authentication.notifySuccess({
        username: usernameInput.value,
        password: passwordInput.value,
        employeeId: employeeId
    });

The completed login script is here at B03-after-teams-sso/client/identity/login.js

Step 4: Modify the logoff code

Logging off in this new version of the Teams app will be a very transitory thing, because the user will immediately be logged back on again! However we’ll leave it in place for testing purposes.

Open the file client/identity/identityClient.js in your code editor and change the line:

    window.location.href = "/identity/teamsLoginLauncher.html";

to:

    window.location.href = "/identity/aadLogin.html";

Step 5: Nodify the identity server code

Now that the client code has been updated, it’s time to modify the server code to handle the /api/validateAadLogin call from the client. To do that, open the file /server/identityService.js in your code editor and add these lines at the top:

import aad from 'azure-ad-jwt';
import { dbService } from '../northwindDB/dbService.js';
const db = new dbService();

This will import two node packages:

Then locate the function initializeIdentityService() and add these lines near the bottom of the function, just above the app.use('/api/', validateApiRequest); statement.

    // Web service validates an Azure AD login
    app.post('/api/validateAadLogin', async (req, res) => {

        try {
            const employeeId = await validateAndMapAadLogin(req, res);
            if (employeeId) {
                res.send(JSON.stringify({ "employeeId": employeeId }));
            } else {
                res.status(401).send('Unknown authentication failure');
            }
        }
        catch (error) {
            console.log(`Error in /api/validateAadLogin handling: ${error.statusMessage}`);
            res.status(error.status).json({ status: error.status, statusText: error.statusMessage });
        }

    });

This code will handle the call to /api/validateAadLogin but the real work is done in another function, validateAndMapAadLogin(). Next, add this code at the bottom of identityService.js.

// validateAndMapAadLogin() - Returns an employee ID of the logged in user based
// on an existing mapping OR the username/password passed from a client login.
// If there is no existing mapping and no username/password is specified, it will throw
// an exception.
async function validateAndMapAadLogin(req, res) {

    const audience = `api://${process.env.HOSTNAME}/${process.env.CLIENT_ID}`;
    const token = req.headers['authorization'].split(' ')[1];

    const aadUserId = await new Promise((resolve, reject) => {
        aad.verify(token, { audience: audience }, async (err, result) => {
            if (result) {
                resolve(result.oid);
            } else {
                reject(err);
            }
        });
    });

    if (aadUserId) {
        // If here, user is logged into Azure AD
        let employeeId = await getEmployeeIdForUser(aadUserId);
        if (employeeId) {
            // We found the employee ID for the AAD user
            return employeeId;
        } else if (req.body.username) {
            // We did not find an employee ID for this user, try to 
            // get one using the legacy authentication
            const username = req.body.username;
            const password = req.body.password;
            const employeeId = await validateEmployeeLogin(username, password);
            if (employeeId) {
                // If here, user is logged into both Azure AD and the legacy
                // authentication. Save the employee ID in the user's AAD
                // profile for future use.
                await setEmployeeIdForUser(aadUserId, employeeId);
                return employeeId;
            } else {
                // If here, the employee login failed; throw an exception
                throw ({ status: 401, statusMessage: "Employee login failed" });
            }
        } else {
            // If here we don't have an employee ID and employee credentials were
            // not provided.
            throw ({ status: 404, statusMessage: "Employee ID not found for this user" });
        }
    } else {
        res.status(401).send('Invalid AAD token');
    }
}

async function getEmployeeIdForUser(aadUserId) {

    const idMapDB = await db.getTable("IdentityMap", "aadUserId");
    const identity = idMapDB.item(aadUserId);
    return identity.employeeId;
}

async function setEmployeeIdForUser(aadUserId, employeeId) {
    try {

        const identityMap = await db.getTable("IdentityMap", "aadUserId");
        if (identityMap.item(aadUserId)) {
            // User already mapped (shouldn't happen but handle it anyway)
            const item = identityMap.item(aadUserId);
            item.employeeId = employeeId;
        } else {
            identityMap.addItem({
                "aadUserId": aadUserId,
                "employeeId": employeeId
            });
        }
        await identityMap.save();

    }
    catch (error) {
        console.log(`Error updating user mapping ${error}`);
    }
}

validateAndMapAadLogin() validates the Azure AD access token sent by the client and obtains the Azure AD user ID. It then calls getEmployeeIdForUser() to get the employee ID for that user. getEmployeeIdForUser() uses MSAL to obtain an Azure AD access token for the application and then it calls the Microsoft Graph with that token.

If validateAndMapAadLogin() fails to get an employee ID, and a username and password were provided, it looks up the employee ID and uses setEmployeeIdForUser() to write it to the JSON database.

The finished server/identityService.js file is here.

Exercise 4: Test your application in Microsoft Teams


😎 LOGIN STATE IS STORED IN A BROWSER COOKIE: The sample login scheme uses a browser session cookie to establish who is logged in. It is not secure - do not use this in a real application! Also be aware during testing that your login will persist until you close all instances of your web browser. For example if you leave your browser logged in after an earlier lab and then run Microsoft Teams in another instance of the same browser, your user will already be logged in.


Step 1: Start the application

Now it’s time to run your updated application and run it in Microsoft Teams. Start the application with this command:

npm start

Step 2: Upload the app package

In the Teams web or desktop UI, click “Apps” in the sidebar 1️⃣, then “Manage your apps” 2️⃣. At this point you have three choices:

In this case, choose the first option 3️⃣.

Upload the app

Navigate to the Northwind.zip file in your manifest directory and upload it. Although the application is already installed, you are providing a newer version so it will update the application. Click the “Add” button to install it for your personal use.

Upload the app

Step 3: Run the application

The application should appear without any login prompt. The app’s navigation should not be displayed; instead users can navigate to “My Orders” or “Products” using the tabs in the Teams app.

Run the app


CHALLENGE: Notice the logout button doesn’t do anything in Teams! If you wish, hide the logout button just as you hid the navigation bar. The code is in client/identity/userPanel.js.

Known issues

For the latest issues, or to file a bug report, see the github issues list for this repository.

References

Single sign-on (SSO) support for Tabs