Sample code for Microsoft Teams application with SSO, Bots, Messaging extensions and other capabilities.
This lab is part of extending with capabilities for your teams app which begins with a Northwind Orders core application using the AAD
path.
Complete labs A01-A03 to get the Northwind Orders core application ready for this lab.
In this lab you will perform five exercises.
Over the course of these exercises you will complete the following lab goals.
This lab requires the following prerequisites.
OPTIONAL: If you want to run or modify these applications locally, you may find the following tools helpful.
📃NOTE: During installation, select the following modules to be added to Visual Studio.
Microsoft Graph PowerShell SDK
Install-Module Microsoft.Graph -AllowClobber -Force
Install-Module -Name Az -Scope CurrentUser -Repository PSGallery -AllowClobber -Force
To complete this lab you’ll deploy the following to Azure.
You’ll create the three applications and their supporting infrastructure using automated deployment scripts called ARM templates.
Download the source code needed for these services
In this exercise you will deploy resources into your Azure subscription using an ARM template. These resources will all share the same resource group. They include the three web applications and a SQL server instance with a database.
office-add-in-saas-monetization-sample/Deployment_SaaS_Resources/
in your text editor.ARMParameters.json
file and note the following parameters.
- webAppSiteName
- webApiSiteName
- resourceMockWebSiteName
- domainName
- directoryId (Directory (tenant) ID)
- sqlAdministratorLogin
- sqlAdministratorLoginPassword
- sqlMockDatabaseName
- sqlSampleDatabaseName
Enter a unique name for each web app and web site in the parameter list shown below because each one must have a unique name across all of Azure. All of the parameters that correspond to web apps and sites in the following list end in SiteName.
If you need assistance findign you domainName and directoryId, please refer to this article.
Based on the subscription you are using, you may change the location where your azure resources are deployed. To change this, find the
DeployTemplate.ps1
file and search for variable$location
. By default it iscentralus
but you can change it toeastus
.
Leave the rest of the configuration in file ARMParameters.json
as is, this will be automatically filled in after scripts deploy the resources.
Connect-Graph -Scopes "Application.ReadWrite.All, Directory.AccessAsUser.All DelegatedPermissionGrant.ReadWrite.All Directory.ReadWrite.All"
Once accepted, the browser will redirect and show the below message. You can close the browser and continue with the PowerShell command line.
In the same PowerShell terminal window run .\InstallApps.ps1
.
This step adds
Microsoft Graph PowerShell
in Azure Active Directory under Enterprise Applications with the necessary permissions so we can create the needed applications for this particular exercise using its commands.
⚠️ You might get an error as shown below. It depends on the execution policy settings in PowerShell. If you do get the error, move to Step 2. If you do not get the error keep going.
env
file for deploying add-ins. These values will also be pre-populated in ARMParameters.json
. Do not change this file.ARMParameters.json
file is now updated with the values of applications deployed.⚠️ This step is only needed if the previous step ended in an error.
The error you experienced above is likely due to the execution policy of your PowerShell terminal. Here you will set the PowerShell execution policy to be less restrictive and then re-run the install script.
You will set the execution policy to Bypass
for now. Read more on execution policies here.
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
Now re-run .\InstallApps.ps1
The script should now run to create all three applications in Azure AD. At the end of the script, your command line should display below information.:
ARMParameters.json
. Do not change this file.ARMParameters.json
file is now updated with the values of applications deployed.Connect-AzAccount
. This will redirect you to login page..\DeployTemplate.ps1
.
When prompted, enter the name of the resource group to create.Your resources will start to get deployed one after the other and you’ll see the output as shown below if everything is okay.
You’ll get a message on the command line that the ARM Template deployment was successfully as shown below.
App registrations
in Azure AD in Azure portal. Use this link to navigate to it.Under All applications, filter with Display name Contoso Monetization
.
You should see three apps as shown in the screen below:
Here you’ll deploy the server side code for the three applications.
.\MonetizationCodeSample
directory..\PublishSaaSApps.ps1
.📃 NOTE: You may see some warnings about file expiration, please ignore.
The final messages may look like the image below.
A01-begin-app
directory you worked in for Labs A01-A03..env
file.
SAAS_API=https://(webApiSiteName).azurewebsites.net/api/Subscriptions/CheckOrActivateLicense
SAAS_SCOPES=api://(webApiClientId)/user_impersonation
OFFER_ID=contoso_o365_addin
ARMParameters.json
file.https://(webAppSiteName).azurewebsites.net
; you should be able to log in using your tenant administrator account. Don’t purchase a subscription yet, however!Here you will grant the Northwind Orders app permission to call the licensing service in Azure.
In this exercise and the next, you will connect the Northwind Orders application to the sample licensing service you just installed. This will allow you to simulate purchasing the Northwind Orders application in the **AppSource simulator** and enforcing the licenses in Microsoft Teams.
The licensing web service is secured using Azure AD. To call it the Northwind Orders app will acquire an access token to call the licensing service on behalf of the logged in user.
⚠️If you were able to complete Step 2, then you don’t need this. Move on to Step 3.
If you were unable to find the licensing service in Step 2 above, then it’s probably registered in a different tenant (different Azure AD instance). Now you will set up a cross-tenant consent!
http://localhost
as the Redirect URI.This allows the administrator of your M365 tenant to log in using a web browser for the purpose of consent. In a real application, you would create a web page that acknowledges the consent instead of using http://localhost, which will send the admin to an error page but only after doing the initial consent.
https://login.microsoftonline.com/<m365-tenant-id>/adminconsent?client_id=<license-service-client-id>&redirect_uri=http://localhost
Substitute your M365 tenant ID and the license service client ID (from the other tenant) in this URL and browse there.
You have added the permission but nobody has consented to it. Fortunately you’re an administrator and can grant your consent from this same screen!
The message 3️⃣ Granted for tenant should be displayed for each permission.
In this exercise, you will update the Northwind Orders app to call the licensing service in Azure.
/server/validateLicenseService.js
import aad from 'azure-ad-jwt';
import fetch from 'node-fetch';
export async function validateLicense(thisAppAccessToken) {
const audience = `api://${process.env.HOSTNAME}/${process.env.CLIENT_ID}`;
return new Promise((resolve, reject) => {
aad.verify(thisAppAccessToken, { audience: audience }, async (err, result) => {
if (result) {
const licensingAppUrl = `${process.env.SAAS_API}/${process.env.OFFER_ID}`
const licensingAppAccessToken = await getOboAccessToken(thisAppAccessToken);
if (licensingAppAccessToken === "interaction_required") {
reject({ "status":401, "message": "Interaction required"});
}
const licensingResponse = await fetch(licensingAppUrl, {
method: "POST",
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
"Authorization" :`Bearer ${licensingAppAccessToken}`
}
});
if (licensingResponse.ok) {
const licensingData = await licensingResponse.json();
console.log(licensingData.reason);
resolve(licensingData);
} else {
reject({ "status": licensingResponse.status, "message": licensingResponse.statusText });
}
} else {
reject({ "status": 401, "message": "Invalid client access token in northwindLicenseService.js"});
}
});
});
}
// TODO: Securely cache the results of this function for the lifetime of the resulting token
async function getOboAccessToken(clientSideToken) {
const tenantId = process.env.TENANT_ID;
const clientId = process.env.CLIENT_ID;
const clientSecret = process.env.CLIENT_SECRET;
const scopes = process.env.SAAS_SCOPES;
// Use On Behalf Of flow to exchange the client-side token for an
// access token with the needed permissions
const INTERACTION_REQUIRED_STATUS_TEXT = "interaction_required";
const url = "https://login.microsoftonline.com/" + tenantId + "/oauth2/v2.0/token";
const params = {
client_id: clientId,
client_secret: clientSecret,
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
assertion: clientSideToken,
requested_token_use: "on_behalf_of",
scope: scopes
};
const accessTokenQueryParams = new URLSearchParams(params).toString();
try {
const oboResponse = await fetch(url, {
method: "POST",
body: accessTokenQueryParams,
headers: {
Accept: "application/json",
"Content-Type": "application/x-www-form-urlencoded"
}
});
const oboData = await oboResponse.json();
if (oboResponse.status !== 200) {
// We got an error on the OBO request. Check if it is consent required.
if (oboData.error.toLowerCase() === 'invalid_grant' ||
oboData.error.toLowerCase() === 'interaction_required') {
throw (INTERACTION_REQUIRED_STATUS_TEXT);
} else {
console.log(`Error returned in OBO: ${JSON.stringify(oboData)}`);
throw (`Error in OBO exchange ${oboResponse.status}: ${oboResponse.statusText}`);
}
}
return oboData.access_token;
} catch (error) {
return error;
}
}
In Lab A03, you called the Microsoft Graph API using application permissions. The above code calls the licensing service using delegated permissions, meaning that the application is acting on behalf of the user.
To do this, the code uses the On Behalf of Flow to exchange the incoming access token (targeted for the Northwind Orders app) for a new access token that is targeted for the licensing service application.
Now that you have a function that checks the user’s license on the server side, you need to add a POST request to the service that calls the function.
server/server.js
and open it in your code editor.import aad from 'azure-ad-jwt';
import { validateLicense } from './validateLicenseService.js';
await initializeIdentityService()
, add the following code.// Web service validates a user's license
app.post('/api/validateLicense', async (req, res) => {
try {
const token = req.headers['authorization'].split(' ')[1];
try {
let hasLicense = await validateLicense(token);
res.send(JSON.stringify({ "validLicense" : hasLicense }));
}
catch (error) {
console.log (`Error ${error.status} in validateLicense(): ${error.message}`);
res.status(error.status).send(error.message);
}
}
catch (error) {
console.log(`Error in /api/validateAadLogin handling: ${error}`);
res.status(500).json({ status: 500, statusText: error });
}
});
The client needs to be able to show if there is a licensing error. This step adds that code to the client.
client/pages/needLicense.html
.<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<title>Northwind Privacy</title>
<link rel="icon" href="data:;base64,="> <!-- Suppress favicon error -->
<link rel="stylesheet" href="/northwind.css" />
</head>
<body class="ms-Fabric" dir="ltr">
<h1>Sorry you need a valid license to use this application</h1>
<p>Please purchase a license from the Microsoft Teams store.
</p>
<div id="errorMsg"></div>
<script type="module" src="needLicense.js"></script>
</body>
</html>
The HTML page needs some JavaScript to work properly.
/client/pages/needLicense.js
.const searchParams = new URLSearchParams(window.location.search);
if (searchParams.has('error')) {
const error = searchParams.get('error');
const displayElementError = document.getElementById('errorMsg');
displayElementError.innerHTML = error;
}
In this step, you will add client side function to check if the user has a license
client/modules/northwindLicensing.js
.This code calls the server-side API we just added using an Azure AD token obtained using Microsoft Teams SSO.
import 'https://statics.teams.cdn.office.net/sdk/v1.11.0/js/MicrosoftTeams.min.js';
export async function hasValidLicense() {
await new Promise((resolve, reject) => {
microsoftTeams.initialize(() => { resolve(); });
});
const authToken = await new Promise((resolve, reject) => {
microsoftTeams.authentication.getAuthToken({
successCallback: (result) => { resolve(result); },
failureCallback: (error) => { reject(error); }
});
});
const response = await fetch(`/api/validateLicense`, {
"method": "post",
"headers": {
"content-type": "application/json",
"authorization": `Bearer ${authToken}`
},
"cache": "no-cache"
});
if (response.ok) {
const data = await response.json();
return data.validLicense;
} else {
const error = await response.json();
console.log(`ERROR: ${error}`);
}
}
In this step, you’l add client side code that checks the user’s license on every request.
client/identity/userPanel.js
in your code editor.
This is a web component that displays the user’s picture and name on every page, so it’s an easy place to check the license.import { hasValidLicense } from '../modules/northwindLicensing.js';
else
clause within the connectedCallback()
function. if (await inTeams()) {
const validLicense = await hasValidLicense();
if (validLicense.status && validLicense.status.toString().toLowerCase()==="failure") {
window.location.href =`/pages/needLicense.html?error=${validLicense.reason}`;
}
}
The completed userPanel.js
should look like the following code.
import {
getLoggedInEmployee,
logoff
} from './identityClient.js';
import { inTeams } from '../modules/teamsHelpers.js';
import { hasValidLicense } from '../modules/northwindLicensing.js';
class northwindUserPanel extends HTMLElement {
async connectedCallback() {
const employee = await getLoggedInEmployee();
if (!employee) {
logoff();
} else {
if (await inTeams()) {
const validLicense = await hasValidLicense();
if (validLicense.status && validLicense.status.toString().toLowerCase()==="failure") {
window.location.href =`/pages/needLicense.html?error=${validLicense.reason}`;
}
}
this.innerHTML = `<div class="userPanel">
<img src="data:image/bmp;base64,${employee.photo}"></img>
<p>${employee.displayName}</p>
<p>${employee.jobTitle}</p>
<hr />
<button id="logout">Log out</button>
</div>
`;
const logoutButton = document.getElementById('logout');
logoutButton.addEventListener('click', async ev => {
logoff();
});
}
}
}
// Define the web component and insert an instance at the top of the page
customElements.define('northwind-user-panel', northwindUserPanel);
const panel = document.createElement('northwind-user-panel');
document.body.insertBefore(panel, document.body.firstChild);
📃 NOTE: There are many ways to make the license check more robust, such as checking it on every web service call and caching this on the server side to avoid excessive calls to the licensing server. However this is just a lab so we wanted to keep it simple.
Now that all the pieces are in place, it’s time to run the application you’ve set up.
In this initial step, you’ll run the application without a user license to see how the application behaves.
📃 NOTE: The sample application checks the license in JavaScript, which is convenient for this lab but it would be easy for someone to bypass the license check. In a real application you’d probably check the license on all accesses to your application web site.
The Teams Store is also listed in the Microsoft **AppSource portal. Users can purchase your app in either location. For this lab you will use an **AppSource simulator which you installed earlier in this lab. Just know that Teams users can purchase apps directly from the user interface when they’re listed in the Teams app store.
📃NOTE: The AppSource simulator’s background color is green to make it easy to see when you are redirected to your app’s landing page, which has a blue background.
📃 NOTE: The AppSource simulator has a mock offer name, “Contoso Apps”, rather than showing the “Northwind Orders” app. This is just a constant defined in the monetization project’s
SaasOfferMockData/Offers.cs
file. The real AppSource web page will show the application name and other information you configured in Partner Center.
The AppSource simulator displays the plans available for the offer. The simulator has two hard-coded plans, “SeatBasedPlan” (which uses a per-user pricing model), and a “SiteBasedPlan” (which uses a flat-rate pricing model).
The real AppSource would show the plans you had defined in Partner Center, the publisher’s portal for defining and publishing AppSource offers.
Microsoft Teams only supports the per-user pricing model
The simulated purchase is now complete, so you will be redirected to the app’s landing page.
You will need to supply a page like this as part of your application; it needs to interpret a token sent by AppSource and log the user in with Azure Active Directory. This token can be sent to the SaaS Fulfillment API v2, which will respond with the details about what the customer has purchased.
You can find the code for this in the Monetization repo’s SaaSSampleWebApp project under /Services/SubscriptionService.cs
.
The landing page gives the app a chance to interact with the user and capture any configuration information it needs. Users who purchase the app in the Teams store would be brought to this same page. The sample app’s landing page allows the user to select a region; the app stores this information in its own database based on the Microsoft 365 tenant ID.
Once the region has been selected, the sample app shows a welcome page with the user’s name, which is obtained by reading the user’s profile with the Microsoft Graph API.
On this screen you can add individual user licenses using the Add User button, or you can set a policy that allows users to claim licenses on a first come, first served basis. Turn on the First come first served switch to enable this option.
📃NOTE: Everything on this screen is defined by this application. It’s intended to be flexible since publishers have a wide range of licensing approaches. Apps can tell who’s logging in via Azure AD and use the user ID and tenant ID to authorize access, provide personalization, etc.
Now that you’ve purchased a subscription, you can see the Northwind Orders application in Teams.
Return to the licensing application.
If you’ve closed the tab, you can find it at https://(webAppSiteName).azurewebsites.net where webAppSiteName
is the name you chose earlier in this lab.
Notice that your username has been assigned a license. The sample app stored this in a SQL Server database. When the Teams application called the licensing service, the access token contained the tenant ID and user ID, enabling the licensing service to determine that the user has a license.
For the latest issues, or to file a bug report, see the github issues list for this repository.
After completing this lab, you may continue with any of the following labs.