Add code to enable SSO in your bot app

Before you add code to enable single sign-on (SSO), ensure that you've configured your app and bot resource in Microsoft Entra admin center.

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.

This section covers:

  1. Update development environment variables
  2. Add code to handle an access token
  3. Add code to receive the token
  4. Handle app user log out

Update development environment variables

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:

  1. Open the bot app project.

  2. Open the environment file for your project.

  3. 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.
  4. 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:

  1. Add code snippet for TeamsSSOTokenExchangeMiddleware.

    Add the following code snippet to AdapterWithErrorHandler.cs (or the equivalent class in your app's code):

    base.Use(new TeamsSSOTokenExchangeMiddleware(storage, configuration["ConnectionName"]));
    

    Note

    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.

  2. 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");
                };
            }
        }
    

If the app user is using the application for the first time and user consent is required, the following dialog box appears:

Consent dialog for bot SSO

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.

If you encounter any errors, see Troubleshoot SSO authentication in Teams.

Add code to receive the token

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);
        }

Note

The code snippets use the Waterfall Dialog. For more information, see About component and waterfall dialogs.

Validate the access token

Web APIs on your server must decode the access token and verify if it's sent from the client.

Note

If you use Bot Framework, it handles the access token validation. If you don't use Bot Framework, follow the guidelines given in this section.

For more information about validating access token, see Validate tokens.

There are a number of libraries available that can handle JWT validation. Basic validation includes:

  • Checking that the token is well-formed.
  • Checking that the token was issued by the intended authority.
  • Checking that the token is targeted to the web API.

Keep in mind the following guidelines when validating the token:

  • Valid SSO tokens are issued by Microsoft Entra ID. The iss claim in the token must start with this value.
  • The token's aud1 parameter is set to the app ID generated during Microsoft Entra app registration.
  • The token's scp parameter is set to access_as_user.

Example access token

The following code snippet is a typical decoded payload of an access token:

{
    aud: "2c3caa80-93f9-425e-8b85-0745f50c0d24",
    iss: "https://login.microsoftonline.com/fec4f964-8bc9-4fac-b972-1c1da35adbcd/v2.0",
    iat: 1521143967,
    nbf: 1521143967,
    exp: 1521147867,
    aio: "ATQAy/8GAAAA0agfnU4DTJUlEqGLisMtBk5q6z+6DB+sgiRjB/Ni73q83y0B86yBHU/WFJnlMQJ8",
    azp: "e4590ed6-62b3-5102-beff-bad2292ab01c",
    azpacr: "0",
    e_exp: 262800,
    name: "Mila Nikolova",
    oid: "6467882c-fdfd-4354-a1ed-4e13f064be25",
    preferred_username: "milan@contoso.com",
    scp: "access_as_user",
    sub: "XkjgWjdmaZ-_xDmhgN1BMP2vL2YOfeVxfPT_o8GRWaw",
    tid: "fec4f964-8bc9-4fac-b972-1c1da35adbcd",
    uti: "MICAQyhrH02ov54bCtIDAA",
    ver: "2.0"
}

Handle app user log out

Use the following code snippet to handle the access token in case the app user logs out:

    private async Task<DialogTurnResult> InterruptAsync(DialogContext innerDc, 
    CancellationToken cancellationToken = default(CancellationToken))
        {
            if (innerDc.Context.Activity.Type == ActivityTypes.Message)
            {
                var text = innerDc.Context.Activity.Text.ToLowerInvariant();

                // Allow logout anywhere in the command.
                if (text.IndexOf("logout") >= 0)
                {
                    // The UserTokenClient encapsulates the authentication processes.
                    var userTokenClient = innerDc.Context.TurnState.Get<UserTokenClient>();
                    await userTokenClient.SignOutUserAsync(
                    innerDc.Context.Activity.From.Id, 
                    ConnectionName, 
                    innerDc.Context.Activity.ChannelId, 
                    cancellationToken
                    ).ConfigureAwait(false);

                    await innerDc.Context.SendActivityAsync(MessageFactory.Text("You have been signed out."), cancellationToken);
                    return await innerDc.CancelAllDialogsAsync(cancellationToken);
                }
            }

            return null;
        }

Code sample

Sample name Description C# Node.js
Bot conversation SSO quick start This sample code shows how to get started with SSO in a bot for Microsoft Teams. View View

Note

OnTeamsMessagingExtensionQueryAsync and OnTeamsAppBasedLinkQueryAsync from the TeamsMessagingExtensionsSearchAuthConfigBot.cs file are the only SSO handlers that are supported. Other SSO handlers aren't supported.

This section covers:

  1. Update development environment variables
  2. Add code to request a token
  3. Add code to receive the token
  4. Add token to Bot Framework Token Store
  5. Handle app user log out

Update development environment variables

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:

  1. Open the app project.

  2. Open the ./env file for your project.

  3. 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.
  4. 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:

  1. Add code snippet for TeamsSSOTokenExchangeMiddleware.

    Add the following code snippet to AdapterWithErrorHandler.cs (or the equivalent class in your app's code):

    base.Use(new TeamsSSOTokenExchangeMiddleware(storage, configuration["ConnectionName"]));
    

    Note

    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.

  2. 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");
                };
            }
        }
    

If the app user is using your app for the first time, they're required to consent for SSO authentication.

SSO authentication for message extension app

When the app user selects the user name, the permission is granted and they can use the app.

SSO authentication completed for message extension 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.

If you encounter any errors, see Troubleshoot SSO authentication in Teams.

Add code to receive the token

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);
        }

Note

The code snippets use the Waterfall Dialog bot. For more information about Waterfall Dialog, see About component and waterfall dialogs.

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.

JObject valueObject=JObject.FromObject(turnContext.Activity.Value);
if(valueObject["authentication"] !=null)
 {
    JObject authenticationObject=JObject.FromObject(valueObject["authentication"]);
    if(authenticationObject["token"] !=null)
 }

Validate the access token

Web APIs on your server must decode the access token and verify if it's sent from the client.

Note

If you use Bot Framework, it handles the access token validation. If you don't use Bot Framework, follow the guidelines in this section.

For more information about validating access token, see Validate tokens.

There are a number of libraries available that can handle JWT validation. Basic validation includes:

  • Checking that the token is well-formed.
  • Checking that the token was issued by the intended authority.
  • Checking that the token is targeted to the web API.

Keep in mind the following guidelines when validating the token:

  • Valid SSO tokens are issued by Microsoft Entra ID. The iss claim in the token must start with this value.
  • The token's aud1 parameter is set to the app ID generated during Microsoft Entra app registration.
  • The token's scp parameter is set to access_as_user.

Example access token

The following code snippet is a typical decoded payload of an access token:

{
    aud: "2c3caa80-93f9-425e-8b85-0745f50c0d24",
    iss: "https://login.microsoftonline.com/fec4f964-8bc9-4fac-b972-1c1da35adbcd/v2.0",
    iat: 1521143967,
    nbf: 1521143967,
    exp: 1521147867,
    aio: "ATQAy/8GAAAA0agfnU4DTJUlEqGLisMtBk5q6z+6DB+sgiRjB/Ni73q83y0B86yBHU/WFJnlMQJ8",
    azp: "e4590ed6-62b3-5102-beff-bad2292ab01c",
    azpacr: "0",
    e_exp: 262800,
    name: "Mila Nikolova",
    oid: "6467882c-fdfd-4354-a1ed-4e13f064be25",
    preferred_username: "milan@contoso.com",
    scp: "access_as_user",
    sub: "XkjgWjdmaZ-_xDmhgN1BMP2vL2YOfeVxfPT_o8GRWaw",
    tid: "fec4f964-8bc9-4fac-b972-1c1da35adbcd",
    uti: "MICAQyhrH02ov54bCtIDAA",
    ver: "2.0"
}

Add token to Bot Framework Token Store

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:

Note

You can find the sample TeamsMessagingExtensionsSearchAuthConfigBot.cs in Tab, Bot, and Message Extension (ME) SSO.

protected override async Task<InvokeResponse> OnInvokeActivityAsync(ITurnContext<IInvokeActivity> turnContext, CancellationToken cancellationToken)
     {
         JObject valueObject = JObject.FromObject(turnContext.Activity.Value);
         if (valueObject["authentication"] != null)
         {
             JObject authenticationObject = JObject.FromObject(valueObject["authentication"]);
             if (authenticationObject["token"] != null)
             {
                 //If the token is NOT exchangeable, then return 412 to require user consent.
                 if (await TokenIsExchangeable(turnContext, cancellationToken))
                 {
                     return await base.OnInvokeActivityAsync(turnContext, cancellationToken).ConfigureAwait(false);
                 }
                 else
                 {
                     var response = new InvokeResponse();
                     response.Status = 412;
                     return response;
                 }
             }
         }
         return await base.OnInvokeActivityAsync(turnContext, cancellationToken).ConfigureAwait(false);
     }
     private async Task<bool> TokenIsExchangeable(ITurnContext turnContext, CancellationToken cancellationToken)
     {
         TokenResponse tokenExchangeResponse = null;
         try
         {
             JObject valueObject = JObject.FromObject(turnContext.Activity.Value);
             var tokenExchangeRequest =
             ((JObject)valueObject["authentication"])?.ToObject<TokenExchangeInvokeRequest>();
             var userTokenClient = turnContext.TurnState.Get<UserTokenClient>();
             tokenExchangeResponse = await userTokenClient.ExchangeTokenAsync(
                             turnContext.Activity.From.Id,
                              _connectionName,
                              turnContext.Activity.ChannelId,
                              new TokenExchangeRequest
              {
                  Token = tokenExchangeRequest.Token,
              },
               cancellationToken).ConfigureAwait(false);
         }
 #pragma warning disable CA1031 //Do not catch general exception types (ignoring, see comment below)
         catch
 #pragma warning restore CA1031 //Do not catch general exception types
         {
             //ignore exceptions.
             //if token exchange failed for any reason, tokenExchangeResponse above remains null, and a failure invoke response is sent to the caller.
             //This ensures the caller knows that the invoke has failed.
         }
         if (tokenExchangeResponse == null || string.IsNullOrEmpty(tokenExchangeResponse.Token))
         {
             return false;
         }
         return true;
     }

Handle app user log out

Use the following code snippet to handle the access token in case the app user logs out:

    private async Task<DialogTurnResult> InterruptAsync(DialogContext innerDc, 
    CancellationToken cancellationToken = default(CancellationToken))
        {
            if (innerDc.Context.Activity.Type == ActivityTypes.Message)
            {
                var text = innerDc.Context.Activity.Text.ToLowerInvariant();

                // Allow logout anywhere in the command.
                if (text.IndexOf("logout") >= 0)
                {
                    // The UserTokenClient encapsulates the authentication processes.
                    var userTokenClient = innerDc.Context.TurnState.Get<UserTokenClient>();
                    await userTokenClient.SignOutUserAsync(
    innerDc.Context.Activity.From.Id, 
    ConnectionName, 
    innerDc.Context.Activity.ChannelId, 
    cancellationToken
    ).ConfigureAwait(false);

                    await innerDc.Context.SendActivityAsync(MessageFactory.Text("You have been signed out."), cancellationToken);
                    return await innerDc.CancelAllDialogsAsync(cancellationToken);
                }
            }

            return null;
        }

Code sample

This section provides bot authentication v3 SDK sample.

Sample name Description .NET Node.js Python Manifest
Bot authentication This sample shows how to get started with authentication in a bot for Teams. View View View View
Tab, bot, and Message extension (ME) SSO This sample shows SSO for tab, bot, and message extension - search, action, link unfurl. View View NA View
Tab, bot, and Message extension This sample shows how to check authentication in bot, tab, and message extension with SSO View View NA View

Next step