Implementing Single Sign-On (SSO) in Your Microsoft Teams Bot App [Part II]

afrazkhan

Afraz Khan

Posted on September 8, 2024

Implementing Single Sign-On (SSO) in Your Microsoft Teams Bot App [Part II]

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.

Bot App SSO

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.

Admin Consent

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 use userState, 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's onMessage 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 🚀!!!


💖 💪 🙅 🚩
afrazkhan
Afraz Khan

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