Implementing Single Sign-On (SSO) in Your Microsoft Teams Bot App [Part II]
Afraz Khan
Posted on September 8, 2024
In the previous article, we explored the process of setting up a simple echo bot app in Teams. In this article, we will examine the Single Sign-On (SSO) options for enabling authentication in a Teams bot app.
Teams Bot App SSO
Teams apps are available in the Teams client either through the app store or via Admin-approved apps section "Build for your org". It means users must be logged into Teams before accessing the apps.
So by default, no additional login is required for users to access the app. However, if you want to enable authentication for your app, SSO is one of the options available. Users will be prompted to consent to the scopes defined by the app. Once consent is given, the user is granted access to the app.
Technically, a token is generated for each bot message request. This token can be used in the bot backend to make API calls to various Microsoft services (e.g., Skype, OneNote) on behalf of the user.
For more information, visit here.
Let’s dive into it.
SSO Setup
To configure SSO for your Teams bot app, you need to update three components:
- Microsoft Entra ID App
- Bot Registration in Azure Bot Service
- Minor Updates in the Teams Developer Portal App
(We created all these resources in the previous article)
I won’t detail the configuration steps for these components here, as comprehensive guides are available from Microsoft on their official documentation platforms. You can refer to the following resources:
OAuth Dialog Flow
Once the initial setup is complete, it’s time to implement some code changes. You need to integrate an OAuth dialog flow into your existing bot app, which will enable the Bot Framework to handle the complete SSO authentication flow.
For each bot message received, this dialog flow is executed. Its responsibility is to fetch the token from the Bot Framework Token Service and make it available in the message context. For more details, please refer to the SSO architecture discussed in the first section of this article.
No User Consent
You can implement this OAuth dialog flow according to your use case. If you prefer not to have users provide explicit consent and only want to acquire the token directly, you should enable "Admin Consent" for all the scopes/permissions in the Microsoft Entra ID app.
If there is no Admin Consent then users are shown a consent screen everytime there is request for new token generation from the Bot Framework.
Code Changes for a Node.js Bot Backend
To integrate SSO into your existing bot app, you'll need to restructure and add some components to the Bot backend:
-
Inject SSO Middleware
Integrate the SSO middleware into your bot to handle authentication flow.
js const {TeamsSSOTokenExchangeMiddleware} = require('botbuilder'); const tokenExchangeMiddleware = new TeamsSSOTokenExchangeMiddleware(memoryStorage, env.connectionName); adapter.use(tokenExchangeMiddleware);
-
Implement OAuth Dialog
I’m pasting the smallest possible version of the dialog code below. Feel free to tweak it as needed.
js // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. const { ConfirmPrompt, DialogSet, DialogTurnStatus, OAuthPrompt, WaterfallDialog } = require('botbuilder-dialogs'); const { LogoutDialog } = require('./logoutDialog'); const CONFIRM_PROMPT = 'ConfirmPrompt'; const MAIN_DIALOG = 'MainDialog'; const MAIN_WATERFALL_DIALOG = 'MainWaterfallDialog'; const OAUTH_PROMPT = 'OAuthPrompt'; class MainDialog extends LogoutDialog { constructor(userState) { super(MAIN_DIALOG, process.env.OAuthConnectionName); this.addDialog(new OAuthPrompt(OAUTH_PROMPT, { connectionName: process.env.OAuthConnectionName, 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.processTokenStep.bind(this) ])); this.initialDialogId = MAIN_WATERFALL_DIALOG; this.userState = userState; this.authTokenAccessor = this.userState.createProperty('AuthToken'); } /** * The run method handles the incoming activity (in the form of a DialogContext) and passes it through the dialog system. * If no dialog is active, it will start the default dialog. * @param {*} dialogContext */ async run(context, accessor) { const dialogSet = new DialogSet(accessor); dialogSet.add(this); const dialogContext = await dialogSet.createContext(context); const results = await dialogContext.continueDialog(); if (results.status === DialogTurnStatus.empty) { await dialogContext.beginDialog(this.id); } } async promptStep(stepContext) { try { return await stepContext.beginDialog(OAUTH_PROMPT); } catch (err) { console.error(err); } } async processTokenStep(stepContext) { const tokenResponse = stepContext.result; if (!tokenResponse || !tokenResponse.token) { await stepContext.context.sendActivity('Authentication was not successful please try again.'); } else { this.authTokenAccessor.set(stepContext.context, { token: tokenResponse.token }); } return await stepContext.endDialog(); } } module.exports.MainDialog = MainDialog;
-
Attach User State, Conversation State, and OAuth Dialog
Configure your bot to useuserState
,conversationState
, and the OAuth dialog to manage user interactions and authentication.const { CloudAdapter, ConversationState, MemoryStorage, UserState, ConfigurationBotFrameworkAuthentication, TeamsSSOTokenExchangeMiddleware } = require('botbuilder'); // 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);
Cache the Token
Implement logic to save the token for future use. The recommended approach is to persist the token in the Bot user state cache. See the Dialog logic above. For more details on managing cache and state in the bot backend, refer to the official guide.-
Access the Token in the Bot
Once token is saved in the cache via th OAuth Dialog. Make changes in your Bot'sonMessage
endpoint to load it.class DialogBot extends TeamsActivityHandler { constructor(conversationState, userState, dialog) { super(); if (!conversationState) { throw new Error('[DialogBot]: Missing parameter. conversationState is required'); } if (!userState) { throw new Error('[DialogBot]: Missing parameter. userState is required'); } if (!dialog) { throw new Error('[DialogBot]: Missing parameter. dialog is required'); } this.conversationState = conversationState; this.userState = userState; this.dialog = dialog; this.dialogState = this.conversationState.createProperty('DialogState'); this.authTokenAccessor = this.userState.createProperty('AuthToken'); // See https://aka.ms/about-bot-activity-message to learn more about the message and other activity types. this.onMessage(async (context, next) => { console.info('OnMessage: New message is received.'); const processingMessage = await context.sendActivity('Processing your request...'); // Dialog is executed await this.dialog.run(context, this.dialogState); // Load the token from the cache const authToken = await this.authTokenAccessor.get(context, {}); console.log(`Token ==> ${ authToken }`); const memberData = await this.getTeamsMemberInfo(context); console.info(`OnMessage: member ==> "${ memberData.aadObjectId }".`); await context.updateActivity({ id: processingMessage.id, type: 'message', text: 'Thanks' }); // By calling next() you ensure that the next BotHandler is run. await next(); }); } /** * Override the ActivityHandler.run() method to save state changes after the bot logic completes. */ async run(context) { await super.run(context); // Save any state changes. The load happened during the execution of the Dialog. await this.conversationState.saveChanges(context, false); await this.userState.saveChanges(context, false); } async getTeamsMemberInfo(context) { const response = await TeamsInfo.getMember(context, context.activity.from.id); return response; } } module.exports.DialogBot = DialogBot;
And you are done 😍. Now SSO is enabled for the bot app.
All of these changes are mentioned on the official documentation and also refer to the SSO sample. Again, You can play around with this sample as per your usecase.
Happy learning 🚀!!!
Posted on September 8, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
September 8, 2024