You need to configure your app's code to obtain an access token from Microsoft Entra ID. The access token is issued on behalf of the bot app.
Note
If you've built your Teams app using Microsoft Teams Toolkit, you can enable SSO for your app using the instructions in the Tools and SDKs module. For more information, see Add single sign-on to Teams app. Teams Toolkit supports SSO for JavaScript and TypeScript apps in Visual Studio Code, and in Teams Toolkit 17.4 preview 3 for C# apps.
You've configured client secret and OAuth connection setting for the app in Microsoft Entra ID. You must configure the code with these values.
To update the development environment variables:
Open the bot app project.
Open the environment file for your project.
Update the following variables:
For MicrosoftAppId, update the bot ID from Microsoft Entra ID.
For MicrosoftAppPassword, update the client secret.
For ConnectionName, update the name of the OAuth connection you configured in Microsoft Entra ID.
For MicrosoftAppTenantId, update the tenant ID.
Save the file.
You've now configured the required environment variables for your bot app and SSO. Next, add the code for handling bot tokens.
Add code to handle an access token
The request to get the token is a POST message request using the existing message schema. It's included in the attachments of an OAuthCard. The schema for the OAuthCard class is defined in Microsoft Bot Schema 4.0. Teams refreshes the token if the TokenExchangeResource property is populated on the card. For the Teams channel, only the Id property, which uniquely identifies a token request, is honored.
Note
The Microsoft Bot Framework OAuthPrompt or the MultiProviderAuthDialog is supported for SSO authentication.
To update your app's code:
Add code snippet for TeamsSSOTokenExchangeMiddleware.
You might receive multiple responses for a given request if the user has multiple active endpoints. You must eliminate all duplicate or redundant responses with the token. For more information about signin/tokenExchange, see TeamsSSOTokenExchangeMiddleware Class.
Use the following code snippet for requesting a token.
After you add the AdapterWithErrorHandler.cs, the following code must appear:
public class AdapterWithErrorHandler : CloudAdapter
{
public AdapterWithErrorHandler(
IConfiguration configuration,
IHttpClientFactory httpClientFactory,
ILogger<IBotFrameworkHttpAdapter> logger,
IStorage storage,
ConversationState conversationState)
: base(configuration, httpClientFactory, logger)
{
base.Use(new TeamsSSOTokenExchangeMiddleware(storage, configuration["ConnectionName"]));
OnTurnError = async (turnContext, exception) =>
{
// Log any leaked exception from the application.
// NOTE: In production environment, you must consider logging this to
// Azure Application Insights. Visit https://learn.microsoft.com/azure/bot-service/bot-builder-telemetry?view=azure-bot-service-4.0&tabs=csharp to see how
// to add telemetry capture to your bot.
logger.LogError(exception, $"[OnTurnError] unhandled error : {exception.Message}");
// Send a message to the user.
await turnContext.SendActivityAsync("The bot encountered an error or bug.");
await turnContext.SendActivityAsync("To continue to run this bot, please fix the bot source code.");
if (conversationState != null)
{
try
{
// Delete the conversationState for the current conversation to prevent the
// bot from getting stuck in a error-loop caused by being in a bad state.
// conversationState must be thought of as similar to "cookie-state" in a Web pages.
await conversationState.DeleteAsync(turnContext);
}
catch (Exception e)
{
logger.LogError(e, $"Exception caught on attempting to Delete ConversationState : {e.Message}");
}
}
// Send a trace activity, which is displayed in the Bot Framework Emulator.
await turnContext.TraceActivityAsync(
"OnTurnError Trace",
exception.Message,
"https://www.botframework.com/schemas/error",
"TurnError");
};
}
}
After you add the code snippet for TeamsSSOTokenExchangeMiddleware, the following code must appear:
// index.js is used to setup and configure your bot.
// Import required packages
const path = require('path');
// Read botFilePath and botFileSecret from .env file.
const ENV_FILE = path.join(__dirname, '.env');
require('dotenv').config({ path: ENV_FILE });
const express = require("express");
// Import required bot services.
// See https://learn.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0 to learn more about the different parts of a bot.
const {
CloudAdapter,
ConversationState,
MemoryStorage,
UserState,
ConfigurationBotFrameworkAuthentication,
TeamsSSOTokenExchangeMiddleware
} = require('botbuilder');
const { TeamsBot } = require('./bots/teamsBot');
const { MainDialog } = require('./dialogs/mainDialog');
const { env } = require('process');
const botFrameworkAuthentication = new ConfigurationBotFrameworkAuthentication(process.env);
var conname = env.connectionName;
console.log(`\n${ conname } is the con name`);
// Create adapter.
// See https://learn.microsoft.com/javascript/api/botbuilder-core/botadapter?view=botbuilder-ts-latest to learn more about how bot adapter.
const adapter = new CloudAdapter(botFrameworkAuthentication);
const memoryStorage = new MemoryStorage();
const tokenExchangeMiddleware = new TeamsSSOTokenExchangeMiddleware(memoryStorage, env.connectionName);
adapter.use(tokenExchangeMiddleware);
adapter.onTurnError = async (context, error) => {
// This check writes out errors to console log .vs. app insights.
// NOTE: In production environment, you must consider logging this to Azure
// application insights. See https://learn.microsoft.com/azure/bot-service/bot-builder-telemetry?view=azure-bot-service-4.0&tabs=csharp for telemetry
// configuration instructions.
console.error(`\n [onTurnError] unhandled error: ${ error }`);
// Send a trace activity, which is displayed in Bot Framework Emulator.
await context.sendTraceActivity(
'OnTurnError Trace',
`${ error }`,
'https://www.botframework.com/schemas/error',
'TurnError'
);
// Send a message to the user.
await context.sendActivity('The bot encountered an error or bug.');
await context.sendActivity('To continue to run this bot, please fix the bot source code.');
// Clear out state.
await conversationState.delete(context);
};
// Define the state store for your bot.
// See https://aka.ms/about-bot-state to learn more about using MemoryStorage.
// A bot requires a state storage system to persist the dialog and user state between messages.
//const memoryStorage = new MemoryStorage();
// Create conversation and user state with in-memory storage provider.
const conversationState = new ConversationState(memoryStorage);
const userState = new UserState(memoryStorage);
// Create the main dialog.
const dialog = new MainDialog();
// Create the bot that will handle incoming messages.
const bot = new TeamsBot(conversationState, userState, dialog);
// Create express application.
const expressApp = express();
expressApp.use(express.json());
const server = expressApp.listen(process.env.port || process.env.PORT || 3978, () => {
console.log(`\n${ expressApp.name } listening to`, server.address());
console.log('\nGet Bot Framework Emulator: https://aka.ms/botframework-emulator');
console.log('\nTo talk to your bot, open the emulator select "Open Bot"');
});
// Listen for incoming requests.
expressApp.post('/api/messages', async (req, res) => {
// Route received a request to adapter for processing.
await adapter.process(req, res, (context) => bot.run(context));
});
Consent dialog for getting access token
If the app user is using the application for the first time and user consent is required, the following dialog box appears:
When the user selects Continue, one of the following events occurs:
If the bot UI has a sign-in button, the sign-in flow for bots is activated. You can determine the permissions that require app user's consent. Use this approach if your app requires Graph permissions other than openid.
If the bot doesn't have a sign-in button on the OAuth card, app user consent is required for a minimal set of permissions. This token is useful for basic authentication and to get the app user's email address.
The consent dialog that appears is for open-id scopes defined in Microsoft Entra ID. The app user must give consent only once. After consenting, the app user can access and use your bot app for the granted permissions and scopes.
Note
After the app user consents, they're not required to consent again for any other permissions. If the permissions defined in Microsoft Entra scope are modified, then the app user may need to consent again. If, however, the consent prompt fails to let the app user access, the bot app falls back to sign-in card.
Important
Scenarios where consent dialogs are not needed:
If the tenant administrator has granted consent on behalf of the tenant, app users don't need to be prompted for consent at all. This means that the app users don't see the consent dialogs and can access the app seamlessly.
If your Microsoft Entra app is registered in the same tenant from which you're requesting an authentication in Teams, the app user can't be asked to consent, and is granted an access token right away. App users consent to these permissions only if the Microsoft Entra app is registered in a different tenant.
The response with the token is sent through an invoke activity with the same schema as other invoke activities that the bots receive today. The only difference is the invoke name, sign in/tokenExchange, and the value field. The value field contains the Id, a string of the initial request to get the token and the token field, a string value including the token.
Use the following code snippet to invoke response:
public MainDialog(IConfiguration configuration, ILogger<MainDialog> logger)
: base(nameof(MainDialog), configuration["ConnectionName"])
{
AddDialog(new OAuthPrompt(
nameof(OAuthPrompt),
new OAuthPromptSettings
{
ConnectionName = ConnectionName,
Text = "Please Sign In",
Title = "Sign In",
Timeout = 300000, // User has 5 minutes to login (1000 * 60 * 5)
EndOnInvalidMessage = true
}));
AddDialog(new ConfirmPrompt(nameof(ConfirmPrompt)));
AddDialog(new WaterfallDialog(nameof(WaterfallDialog), new WaterfallStep[]
{
PromptStepAsync,
LoginStepAsync,
}));
// The initial child Dialog to run.
InitialDialogId = nameof(WaterfallDialog);
}
private async Task<DialogTurnResult> PromptStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
return await stepContext.BeginDialogAsync(nameof(OAuthPrompt), null, cancellationToken);
}
private async Task<DialogTurnResult> LoginStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
var tokenResponse = (TokenResponse)stepContext.Result;
if (tokenResponse?.Token != null)
{
var token = tokenResponse.Token;
// On successful login, the token contains sign in token.
}
else
{
await stepContext.Context.SendActivityAsync(MessageFactory.Text("Login was not successful please try again."), cancellationToken);
}
return await stepContext.EndDialogAsync(cancellationToken: cancellationToken);
}
class MainDialog {
this.addDialog(new OAuthPrompt(OAUTH_PROMPT, {
connectionName: process.env.connectionName,
text: 'Please Sign In',
title: 'Sign In',
timeout: 300000
}));
this.addDialog(new ConfirmPrompt(CONFIRM_PROMPT));
this.addDialog(new WaterfallDialog(MAIN_WATERFALL_DIALOG, [
this.promptStep.bind(this),
this.loginStep.bind(this),
]));
this.initialDialogId = MAIN_WATERFALL_DIALOG;
}
async promptStep(stepContext) {
try {
return await stepContext.beginDialog(OAUTH_PROMPT);
} catch (err) {
console.error(err);
}
}
async loginStep(stepContext) {
// Get the token from the previous step. Note that we could also have gotten the
// token directly from the prompt itself. There is an example of this in the next method.
const tokenResponse = stepContext.result;
if (!tokenResponse || !tokenResponse.token) {
await stepContext.context.sendActivity('Login was not successful please try again.');
} else {
const token = tokenResponse.token;
// On successful login, the token contains sign in token.
}
return await stepContext.endDialog();
}
OnTeamsMessagingExtensionQueryAsync and OnTeamsAppBasedLinkQueryAsync from the TeamsMessagingExtensionsSearchAuthConfigBot.cs file are the only SSO handlers that are supported. Other SSO handlers aren't supported.
You've configured client secret and OAuth connection setting for the app in Microsoft Entra ID. You must configure your app code with these variables.
To update the development environment variables:
Open the app project.
Open the ./env file for your project.
Update the following variables:
For MicrosoftAppId, update the Bot registration ID from Microsoft Entra ID.
For MicrosoftAppPassword, update the Bot registration client secret.
For ConnectionName, update the name of the OAuth connection you configured in Microsoft Entra ID.
For MicrosoftAppTenantId, update the tenant ID.
Save the file.
You've now configured the required environment variables for your bot app and SSO. Next, add the code for handling tokens.
Add code to request a token
The request to get the token is a POST message request using the existing message schema. It's included in the attachments of an OAuthCard. The schema for the OAuthCard class is defined in Microsoft Bot Schema 4.0. Teams refreshes the token if the TokenExchangeResource property is populated on the card. For the Teams channel, only the Id property, which uniquely identifies a token request, is honored.
Note
The Microsoft Bot Framework OAuthPrompt or the MultiProviderAuthDialog is supported for SSO authentication.
To update your app's code:
Add code snippet for TeamsSSOTokenExchangeMiddleware.
You might receive multiple responses for a given request if the user has multiple active endpoints. You must eliminate all duplicate or redundant responses with the token. For more information about signin/tokenExchange, see TeamsSSOTokenExchangeMiddleware Class.
Use the following code snippet for requesting a token.
After you add the AdapterWithErrorHandler.cs, the following code must appear:
public class AdapterWithErrorHandler : CloudAdapter
{
public AdapterWithErrorHandler(
IConfiguration configuration,
IHttpClientFactory httpClientFactory,
ILogger<IBotFrameworkHttpAdapter> logger,
IStorage storage,
ConversationState conversationState)
: base(configuration, httpClientFactory, logger)
{
base.Use(new TeamsSSOTokenExchangeMiddleware(storage, configuration["ConnectionName"]));
OnTurnError = async (turnContext, exception) =>
{
// Log any leaked exception from the application.
// NOTE: In production environment, you must consider logging this to
// Azure Application Insights. Visit https://learn.microsoft.com/en-us/azure/bot-service/bot-builder-telemetry?view=azure-bot-service-4.0&tabs=csharp to see how
// to add telemetry capture to your bot.
logger.LogError(exception, $"[OnTurnError] unhandled error : {exception.Message}");
// Send a message to the user.
await turnContext.SendActivityAsync("The bot encountered an error or bug.");
await turnContext.SendActivityAsync("To continue to run this bot, please fix the bot source code.");
if (conversationState != null)
{
try
{
// Delete the conversationState for the current conversation to prevent the
// bot from getting stuck in an error-loop caused by being in a bad state.
// ConversationState must be thought of as similar to "cookie-state" in a Web pages.
await conversationState.DeleteAsync(turnContext);
}
catch (Exception e)
{
logger.LogError(e, $"Exception caught on attempting to Delete ConversationState : {e.Message}");
}
}
// Send a trace activity, which will be displayed in the Bot Framework Emulator.
await turnContext.TraceActivityAsync(
"OnTurnError Trace",
exception.Message,
"https://www.botframework.com/schemas/error",
"TurnError");
};
}
}
After you add the code to index.js, the following code must appear:
// index.js is used to setup and configure your bot.
// Import required packages.
const path = require('path');
// Read botFilePath and botFileSecret from .env file.
const ENV_FILE = path.join(__dirname, '.env');
require('dotenv').config({ path: ENV_FILE });
const express = require("express");
// Import required bot services.
// See https://learn.microsoft.com/en-us/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0 to learn more about the different parts of a bot.
const {
CloudAdapter,
ConversationState,
MemoryStorage,
UserState,
ConfigurationBotFrameworkAuthentication,
TeamsSSOTokenExchangeMiddleware
} = require('botbuilder');
const { TeamsBot } = require('./bots/teamsBot');
const { MainDialog } = require('./dialogs/mainDialog');
const { env } = require('process');
const botFrameworkAuthentication = new ConfigurationBotFrameworkAuthentication(process.env);
var conname = env.connectionName;
console.log(`\n${ conname } is the con name`);
// Create adapter.
// See https://learn.microsoft.com/en-us/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0 to learn more about how bots work.
const adapter = new CloudAdapter(botFrameworkAuthentication);
const memoryStorage = new MemoryStorage();
const tokenExchangeMiddleware = new TeamsSSOTokenExchangeMiddleware(memoryStorage, env.connectionName);
adapter.use(tokenExchangeMiddleware);
adapter.onTurnError = async (context, error) => {
// This check writes out errors to console log .vs. app insights.
// NOTE: In production environment, you must consider logging this to Azure application insights. See https://learn.microsoft.com/en-us/azure/bot-service/bot-builder-telemetry?view=azure-bot-service-4.0&tabs=csharp for telemetry configuration instructions.
console.error(`\n [onTurnError] unhandled error: ${ error }`);
// Send a trace activity, which will be displayed in Bot Framework Emulator.
await context.sendTraceActivity(
'OnTurnError Trace',
`${ error }`,
'https://www.botframework.com/schemas/error',
'TurnError'
);
// Send a message to the user.
await context.sendActivity('The bot encountered an error or bug.');
await context.sendActivity('To continue to run this bot, please fix the bot source code.');
// Clear out state.
await conversationState.delete(context);
};
// Define the state store for your bot.
// See https://learn.microsoft.com/en-us/azure/bot-service/bot-builder-howto-v4-state?view=azure-bot-service-4.0&branch=live&tabs=csharp to learn more about using MemoryStorage.
// A bot requires a state storage system to persist the dialog and user state between messages.
//const memoryStorage = new MemoryStorage();
// Create conversation and user state with in-memory storage provider.
const conversationState = new ConversationState(memoryStorage);
const userState = new UserState(memoryStorage);
// Create the main dialog.
const dialog = new MainDialog();
// Create the bot that will handle incoming messages.
const bot = new TeamsBot(conversationState, userState, dialog);
// Create express application.
const expressApp = express();
expressApp.use(express.json());
const server = expressApp.listen(process.env.port || process.env.PORT || 3978, () => {
console.log(`\n${ expressApp.name } listening to`, server.address());
console.log('\nGet Bot Framework Emulator: https://aka.ms/botframework-emulator');
console.log('\nTo talk to your bot, open the emulator select "Open Bot"');
});
// Listen for incoming requests.
expressApp.post('/api/messages', async (req, res) => {
// Route received a request to adapter for processing.
await adapter.process(req, res, (context) => bot.run(context));
});
Consent dialog for getting access token
If the app user is using your app for the first time, they're required to consent for SSO authentication.
When the app user selects the user name, the permission is granted and they can use the app.
The consent dialog that appears is for open-id scopes defined in Microsoft Entra ID. The app user must give consent only once. After consenting, the app user can access and use your message extension app for the granted permissions and scopes.
Important
Scenarios where consent dialogs are not needed:
If the tenant administrator has granted consent on behalf of the tenant, app users don't need to be prompted for consent at all. This means that the app users don't see the consent dialogs and can access the app seamlessly.
The response with the token is sent through an invoke activity with the same schema as other invoke activities that the bots receive today. The only difference is the invoke name,
sign in/tokenExchange, and the value field. The value field contains the Id, a string of the initial request to get the token and the token field, a string value including the token.
Use the following code snippet example to invoke response:
public MainDialog(IConfiguration configuration, ILogger<MainDialog> logger)
: base(nameof(MainDialog), configuration["ConnectionName"])
{
AddDialog(new OAuthPrompt(
nameof(OAuthPrompt),
new OAuthPromptSettings
{
ConnectionName = ConnectionName,
Text = "Please Sign In",
Title = "Sign In",
Timeout = 300000, // User has 5 minutes to login (1000 * 60 * 5)
EndOnInvalidMessage = true
}));
AddDialog(new ConfirmPrompt(nameof(ConfirmPrompt)));
AddDialog(new WaterfallDialog(nameof(WaterfallDialog), new WaterfallStep[]
{
PromptStepAsync,
LoginStepAsync,
}));
// The initial child Dialog to run.
InitialDialogId = nameof(WaterfallDialog);
}
private async Task<DialogTurnResult> PromptStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
return await stepContext.BeginDialogAsync(nameof(OAuthPrompt), null, cancellationToken);
}
private async Task<DialogTurnResult> LoginStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
var tokenResponse = (TokenResponse)stepContext.Result;
if (tokenResponse?.Token != null)
{
var token = tokenResponse.Token;
// On successful login, the token contains sign in token.
}
else
{
await stepContext.Context.SendActivityAsync(MessageFactory.Text("Login was not successful please try again."), cancellationToken);
}
return await stepContext.EndDialogAsync(cancellationToken: cancellationToken);
}
class MainDialog {
this.addDialog(new OAuthPrompt(OAUTH_PROMPT, {
connectionName: process.env.connectionName,
text: 'Please Sign In',
title: 'Sign In',
timeout: 300000
}));
this.addDialog(new ConfirmPrompt(CONFIRM_PROMPT));
this.addDialog(new WaterfallDialog(MAIN_WATERFALL_DIALOG, [
this.promptStep.bind(this),
this.loginStep.bind(this),
]));
this.initialDialogId = MAIN_WATERFALL_DIALOG;
}
async promptStep(stepContext) {
try {
return await stepContext.beginDialog(OAUTH_PROMPT);
} catch (err) {
console.error(err);
}
}
async loginStep(stepContext) {
// Get the token from the previous step. Note that we could also have gotten the
// token directly from the prompt itself. There is an example of this in the next method.
const tokenResponse = stepContext.result;
if (!tokenResponse || !tokenResponse.token) {
await stepContext.context.sendActivity('Login was not successful please try again.');
} else {
const token = tokenResponse.token;
// On successful login, the token contains sign in token.
}
return await stepContext.endDialog();
}
You receive the token in OnTeamsMessagingExtensionQueryAsync handler in the turnContext.Activity.Value payload or in the OnTeamsAppBasedLinkQueryAsync, depending on which scenario you're enabling SSO for.
If you're using the OAuth connection, you must update or add the token in the Bot Framework Token store. Add the following code snippet example to TeamsMessagingExtensionsSearchAuthConfigBot.cs (or the equivalent file in your app's code) for updating or adding the token in the store:
The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see our contributor guide.
Platform Docs feedback
Platform Docs is an open source project. Select a link to provide feedback: