ODC with Bots for Teams - User Authentication


In this final part of the ODC with Bots for Teams series, we explore a way to authenticate a user within a Microsoft Teams conversation. Specifically, we examine how to quietly exchange a user's access token, issued when logging into Microsoft Teams, for an access token of our bot application. This token will have specific Microsoft Graph API permissions that we will use to send an email to the user.
Authenticating a user within a Teams conversation is a significant topic. Besides the token exchange using Microsoft Entra, which is covered in this article, you can also authenticate a user with any OpenID Connect-compatible Identity Provider.
But when should you authenticate a user from within a conversation?
Explicitly asking a user to authenticate is not always necessary. You only need to require authentication when you need an access token to perform actions on behalf of the user (meaning with permissions associated with the user) on resources. Examples include:
Interacting with the Microsoft Graph API, such as sending an email on behalf of a user. In this case, you need an access token issued by Microsoft Entra and permissions to send emails.
Interacting with AWS Simple Storage Service. Here, you would authenticate a user with AWS Cognito to get an access token that you can exchange for AWS credentials via AWS STS.
You don't need to require user authentication just to know who the user is. The information sent as part of an Activity in the From and Recipient properties can be sufficient.
For more information on Bot Authentication, visit User authentication in the Azure AI Bot Service - Bot Service | Microsoft Learn.
So, without further delay, let's begin by preparing the prerequisites.
Demo Application
This article series includes a demo application called "ODC with Bots for Teams Demo," available on ODC Forge. Be sure to download the version of the application that matches each article in this series.
For this article, you need to install Version 0.5 from ODC Forge.
In the ODC Portal, go to Forge - All Assets.
Search for "ODC with Bots for Teams Demo".
Click on the Asset (Do not click on Install on the tile!).
Switch to the Version tab and click on Install next to Version 0.5.
Version 0.5 depends on other Forge components:
OAuthTokenExchange - An external logic library that helps retrieve access tokens easily.
Bot Framework Service API - A connector library for using Bot Connector API endpoints.
LiquidFluid - An external logic library used to render handlebar templates based on the Shopify Liquid Templating syntax.
Microsoft Entra Application Registration
In Entra Application Registration, we will configure permissions, scope, and authorized clients, an additional client secret and a redirect URI.
Our demo bot needs to send an email on behalf of the logged-in user. To send emails, it requires the Mail.Send permission.
Additionally, we need to create a custom scope and add the Microsoft Teams client application as an authorized client. This ensures that only these trusted clients can request tokens from Entra on behalf of the user, which will be used in our bot to send an email.
We create a new client secret to be used by the Bot Framework token service to acquire the user token during authentication. You already have one secret configured to retrieve an access token for interacting with the Connector API. You could use the same secret, but creating a separate one makes the purpose of each client secret clearer.
Finally, we add a redirect URI to our application registration. This is where the authorization code will be sent after a user successfully signs in from Teams. The entire authentication process is managed by the Azure Bot Framework Service, specifically the Token Service, and the redirect URI will direct to that service.
In Azure Portal switch to the Entra application registration that is associated with your configured bot.
Grant Send Mail Permission
Under the Manage menu, select API permissions.
Click Add a permission.
In the Request API permissions sidebar, select Microsoft Graph.
Choose Delegated Permissions (Permissions on behalf of a user).
Type Mail.Send in the search box and check the box next to Mail.Send (Send mail as a user).
Click Add permissions.
Repeat this step to add the following additional permissions
openid
email
profile
Finally click on Grant admin consent for <your domain>.
Configure Scope
Under the Manage menu, select Expose an API.
Click the Add link next to Application ID URI.
For the value, enter
api://<your Microsoft Tenant FQDN>/botid-<copied Application (client) ID>
.Click Save.
In the Scopes defined by this API section, click on Add a scope.
Scope name - access_as_user
Who can consent - Admins and users
Admin consent display name - Allow Teams to access the user profile and use the bot on behalf of the user
Admin consent description - Allow Microsoft Teams to access the user profile and call the application's API on behalf of the user
User consent display name - Allow Teams to access the user profile and use the bot on behalf of the user
User consent description - Allow Microsoft Teams to access the user profile and call the application's API on behalf of the user
Add Authorized Client Applications
Still in the same menu.
In the Authorized client applications section, click on Add a client application.
Each Microsoft M365 application, such as Teams, Teams for Web, and Outlook, has a unique client ID. We are now allowing the Teams Desktop and Web applications to access our application API (ODC Exposed REST API) on behalf of the logged-in user. You can find a complete list of Microsoft First-Party apps with their identifiers here: Verify first-party Microsoft applications in sign-in reports | Microsoft Learn.
For the Microsoft Teams Desktop and Mobile applications:
Client ID - 1fec8e78-bce4-4aaf-ab1b-5451cc387264
Select the checkbox next to the scope you created earlier
Click Add application
For the Microsoft Teams for Web application:
Client ID - 5e3ce6c0-2b1f-4285-8d4b-75ee78787346
Select the checkbox next to the scope you created earlier
Click Add application
Add Client Secret
Under the Manage menu select Certificates & secrets
Select the Client secrets tab and click on New client secret
Description - Bot User Authentication
Expires - Recommended: 180 days (6 months)
Click Add
Immediately after adding the new client secret, copy the Value and save it for later. It will only be displayed once.
Redirect URI
The final configuration step is to add a Redirect URI to the application registration.
Under the Manage menu select Authentication.
In the Platform configurations section click Add a platform
Select Web
Redirect URIs - https://token.botframework.com/.auth/web/redirect
Click Configure
With our Entra application registration set up, we can now add some configuration details to our Azure Bot resource.
Azure Bot Resource
Under the Settings menu select Configuration
Click Add OAuth Connection Settings
Name: Default
Service Provider: Azure Active Directory v2
Client id: <Application (client) ID> from Entra application registration
Client secret: <Client secret you created you in the previous step>
Token Exchange URL: leave empty
Tenant ID: common
Scopes: Mail.Send openid email profile
Click Save
After saving the connection, click on the newly created connection again. In the top right corner of the sidebar, click on Test connection. Complete the sign-in dialog and in the next dialog, you will receive the issued token. You can copy the value and inspect the token at jwt.io.
App Manifest
The configuration above lets us require users to authenticate through Azure Entra. With Microsoft Teams, we can use a "silent" token exchange feature that swaps a user's Teams access token for an access token issued for the application registration linked to our bot. To enable this "silent" exchange, we need to modify our Teams app manifest and add some extra properties.
{
...,
"permissions": ["messageTeamMembers", "identity"],
"validDomains": ["token.botframework.com"],
"webApplicationInfo": {
"id": "<Application (client) ID>",
"resource": "api://<domain name>/BotId-<Application (client) ID>"
}
permissions - identity: This permission allows the bot to access user identity information. It lets the bot get user details, which is important for personalized interactions and implementing SSO.
validDomains: The domain token.botframework.com is included to let the bot use the Bot Framework OAuth flow for authentication. By specifying this domain, the bot can securely interact with the OAuth token service provided by the Bot Framework, which is necessary for handling user authentication and authorization.
webApplicationInfo - id: This is the Application (client) ID of the bot registered in Microsoft Entra ID. It uniquely identifies the bot application and is used during the authentication process to request tokens.
webApplicationInfo - resource: This is the Application ID URI that represents the bot's API. It specifies the intended audience for the authentication tokens.
After you modify the app manifest with the new values, package it, and reupload the Teams app to Microsoft Teams as described here.
With our prerequisites complete, we can now explore the implementation details. Open ODC Studio and the demo application.
Bot Handler
This version of the demo application includes a new handler called BotHandlerSimpleAuthentication, which is set up as the in-app event handler for the OnBotActivity event. In the Logic tab, open Bots - BotHandlerSimpleAuthentication.
Compared to our previous event handler we made the following additions.
TryGetUserToken
This action checks the TokenStore to see if there is already a valid, non-expired access token available for the sender of the message and returns it.
SendOAuthPrompt
Back in the main flow and if there is no non-expired access token available for the user our flow prepares an OAuthCard message and sends it to the user.
An OAuthCard is used to facilitate OAuth authentication. You will find the OAuthCard template under Data - Resources - Cards.
{
"type": "message",
"attachments": [
{
"contentType": "application/vnd.microsoft.card.oauth",
"content": {
"connectionName": "{{connectionName}}",
"tokenExchangeResource": {
"id": "{{activity}}"
}
}
}
],
"recipient": {
"id": "{{recipient}}"
}
}
RenderOAuthCard replaces the placeholder values with actual data. It uses the LiquidFluid external logic component to create the final card message.
connectionName - The configured OAuth connection name in the Azure Bot resource (Default).
activity - The Activity ID of the last activity that triggered the authentication flow. We will discuss why this value is important later. For now, remember that we use this ID to track the last message a user sent in the conversation.
recipient - The ID of the recipient.
The rendered OAuthCard is then sent to the conversation just like any other message.
Summary
In our bot handler, we first try to get a valid access token for the user from the TokenStore entity. If the user doesn't have an access token, or if the access token has expired, we send an OAuthCard to the user in the Teams conversation to trigger a token exchange.
Messaging Endpoint
As soon as Microsoft Teams receives the OAuthCard message, it sends an Activity of type invoke to the bot's configured messaging endpoint. This Activity contains the user's Microsoft Teams client access token, which we need to exchange for a Bot application token using the Azure Bot Token API.
Open the Messages endpoint in Logic - Integrations - REST - MessagingEndpoint.
In the Messages endpoint we handle this special activity of type invoke and a name of signin/tokenExchange.
DeserializeInboundTokenExchange
This action deserializes the request in to a InvokeTokenExchange structure, a format the Azure Bot Token API expects to exchange a token.
TryExchangeUserToken
This action first gets an access token to interact with the Azure Bot Token API and then calls the ExchangeToken endpoint using our InvokeTokenExchange values. If successful, it saves the access token in the TokenStore entity for future use.
Original Message Playback
Now comes the tricky part. The user's original message hasn't been processed because our bot handler started an OAuth prompt flow, so the original message is essentially lost. Fortunately, all inbound activities are stored in the Activity entity.
When we created the OAuthCard, we added the original message's Activity Id to the card. This Id is sent back to us as part of the invoke - signin/tokenExchange activity.
{
"name": "signin/tokenExchange",
"type": "invoke",
...
"value": {
"id": "<Activity ID>",
"token": "<Teams client token>",
"connectionName": "Default"
},
...
}
We now use this Activity ID to get the original message from the Activity entity (GetOriginActivity), deserialize the payload (DeserializeOriginActivity), and trigger an OnBotActivity with the values from the original activity.
This method replays the original message, and since we now have a valid access token, the bot handler will process the inbound message.
Summary
Our Messaging Endpoint manages the token exchange. It gets the Microsoft Teams client access token and swaps it for an access token for our registered Bot application using the Azure Bot Token Service API. Then, it retrieves the original user message from the Activity Store and triggers an OnBotActivity with the original message payload. This step is essential because the original message isn't processed if the user doesn't have a valid access token.
Send Mail via Graph API
Back in our bot handler (BotHandlerSimpleAuthentication) lets review the part when our action flow could successfully retrieve a users access token from the Token Store.
GetProfile
This action queries the Me endpoint of the Microsoft Graph API to retrieve the basic user profile using the user's access token. This step is needed to get the User Principal Name and the email address (in most cases, the UPN will be the same as the primary email address).
For this operation, the User.Read permission on the Bot application registration in Entra is required.
SendGraphEmail
This action sends a basic email using the Graph API. In our example, the email is sent from the user to themselves, but it's just for demonstration purposes.
For this operation, the Mail.Send permission on the Bot application registration in Entra is necessary.
Summary
To send an email, we use the cached user access token to first get the basic user profile, and then we send an email to that user via the Graph API. This shows how a Microsoft Teams client access token can be successfully exchanged for a bot-specific access token with granted permissions in the Entra application registration.
Bonus Excerise
After testing your bot implementation, you'll quickly notice some delay between sending a message and receiving a response from your bot. As an additional exercise, add a typing activity (an activity of type typing) to:
The messaging endpoint before triggering the OnBotActivity event.
The bot handler right after the condition that checks if the activity type is a message.
Sending a typing activity in a conversation improves the user experience by providing feedback.
The End of the ODC with Bots for Teams series
This is the final tutorial in my series on ODC with Bots for Microsoft Teams. As you can imagine, there is a lot more to the topic than what we covered in these tutorials, but I hope the series gives you a good start on your journey to turning OutSystems Developer Cloud into a full-fledged, multi-bot environment.
I hope you enjoyed this tutorial and the entire series. For questions and feedback, please leave a comment here.
Subscribe to my newsletter
Read articles from Stefan Weber directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Stefan Weber
Stefan Weber
As a seasoned Senior Director at Telelink Business Services EAD, a leading IT full-service provider headquartered in Sofia, Bulgaria, I lead the charge in our Application Services Practice. In this role, I spearhead the development of tailored software solutions using no-code/low-code platforms and cutting-edge cloud-ready/cloud-native solutions based on the Microsoft .NET stack. Throughout my diverse career, I've accumulated a wealth of experience in various capacities, both technically and personally. The constant desire to create innovative software solutions led me to the world of Low-Code and the OutSystems platform. I remain captivated by how closely OutSystems aligns with traditional software development, offering a seamless experience devoid of limitations. While my managerial responsibilities primarily revolve around leading and inspiring my teams, my passion for solution development with OutSystems remains unwavering. My personal focus extends to integrating our solutions with leading technologies such as Amazon Web Services, Microsoft 365, Azure, and more. In 2023, I earned recognition as an OutSystems Most Valuable Professional, one of only 80 worldwide, and concurrently became an AWS Community Builder.