Microsoft Teams bot
Erato can answer in Microsoft Teams personal chats, group chats and channels. Chats started in Teams are ordinary Erato chats: they appear in the web app and the Teams tab, keep their history, and use the same assistants, models, policies, MCP tools and SharePoint integration as the web app.
Overview
- Personal chats stream the answer while it is generated.
- Group chats and channels answer when the bot is @mentioned, reply in the thread and mention the requester. The preceding messages of the conversation are attached as context.
- Files uploaded in a personal chat, pasted images, and SharePoint/OneDrive files shared in a conversation become chat files.
- Tool approvals appear as an Adaptive Card; only the person who asked can decide.
- Background tasks post their result back into the Teams conversation once they finish.
/newstarts a fresh Erato chat in the current conversation.
In the chat list of the web app and the add-ins, the Source filter shows only the chats started in Teams, or hides them. It covers the bot and the Teams tab.
Users are recognized by their Entra object ID, which Erato records whenever they sign in to the web app or the Teams tab. People who have never opened Erato are asked to do so once.
Branding
The Teams tab, Office task panes and web frontend can share one theme mount. Teams package icon source settings select PNGs from that theme while retaining existing ZIP entry names and manifest URLs. See the shared branding configuration. Installed Teams clients display packaged icons until the app package is updated.
Prerequisites
- Erato signs users in with Microsoft Entra ID.
- The Teams app (
integrations.ms_office.teams) is enabled, so the bot can be shipped in the same app package as the tab. - A messaging endpoint that Microsoft’s cloud can reach. Add-ins and the Teams tab are loaded by the Outlook or Teams client on the user’s device, so they work on an internal-only host. The bot works the other way round: Teams hands every message to Azure Bot Service, which posts it to
https://<host>/api/integrations/ms_teams/messages, server to server. That path therefore needs a public hostname with a certificate from a publicly trusted CA, and TLS 1.2 must stay enabled. For an internal-only deployment, publish only this path, for example through a reverse proxy or web application firewall in a DMZ, or Azure Application Gateway over an existing ExpressRoute or VPN connection. Teams has no mode in which the bot fetches messages itself. See Security and network. - Outbound HTTPS from Erato to
login.microsoftonline.com,login.botframework.com,*.botframework.com,smba.trafficmanager.net,graph.microsoft.com(identity and groups) and*.sharepoint.com(files people attach). - A customer administrator who can create resources in an Azure resource group (and register the
Microsoft.BotServiceprovider if the subscription never used it), update the Entra app registration and grant consent (Cloud Application Administrator), and deploy the Teams package: Global Administrator in Microsoft 365 admin center’s Integrated apps, or Teams Administrator in Teams admin center.
Setup
The bot uses the deployment’s existing Entra app registration: the same app as the add-ins, the Teams tab and usually the web login. Erato needs no values that only exist after the Azure setup, so the order is:
- The Erato operator enables the bot and deploys once.
- Customer IT runs one Cloud Shell command from the setup page. It creates or updates everything in Azure and Entra.
- Customer IT uploads the Teams package and sends the bot a message.
Nothing has to be handed back to the operator, and Erato is not redeployed.
1. Enable the bot in Erato
[integrations.ms_office.teams.bot]
enabled = true
# Only when integrations.ms_office.addin.msal_authority does not name the tenant:
tenant_id = "TENANT_ID"
# Only when Azure reaches Erato on a different, public hostname:
messaging_endpoint = "https://teams-bot.example.com"Everything else has defaults: app_id and sso_app_id come from the add-in’s msal_client_id, oauth_connection_name is graph-sso, and sso_resource is api://<host>/botid-<app_id> for the host the Teams package is downloaded from. Explicit values still win. Erato logs at startup which settings it filled in; the setup page shows their values.
Upgrading an existing bot without single sign-on: single sign-on is now on by default, so the Teams package rendered after the upgrade points webApplicationInfo at the derived sso_resource. The installed package changes only when you upload a new one; users then accept the update, and the OAuth connection must have that token exchange URL. Run the guided setup before uploading, or set sso_enabled = false to keep the package as it was.
The bot authenticates to Microsoft with its app registration’s client secret (app_password). With the default app_id, that is the secret oauth2-proxy already uses for the web login; no new credential is needed. With the Helm chart:
msTeamsBot:
# Pass the existing oauth2-proxy client secret to the backend.
appPassword:
existingSecret:
name: erato-secrets
key: OAUTH2_PROXY_CLIENT_SECRET
# Route the messaging endpoint around oauth2-proxy on the main host ...
directRoute:
enabled: true
ingressFrom:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx
# ... and/or publish only that path on a separate public hostname.
publicIngress:
enabled: true
host: teams-bot.example.com
className: nginx-public
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: 256k
tls:
enabled: true
secretName: teams-bot-tlsIf the Teams tab is already installed, increase integrations.ms_office.teams.manifest.version in the same change: the package gains the bot, and Teams only accepts an update with a higher version.
The /office-addin/setup → Teams page now shows the bot section.
2. Guided setup with Azure Cloud Shell
On the deployment’s Teams setup page:
-
Read Before you start: the messaging endpoint shown there is the one Azure will call. If it is the internal address the page was opened at and Azure cannot reach it, the Erato operator sets
messaging_endpointfirst. -
The Tenant ID is prefilled. Enter the Subscription ID. To create the Azure Bot, also enter an existing Resource group and a name for the new bot; leave both empty if a bot for this deployment’s app registration already exists.
-
Select Open Azure Cloud Shell, choose PowerShell, and sign in to the tenant. First-time Cloud Shell use can require registering the
Microsoft.CloudShellresource provider. Persistent storage is optional. -
Select Copy command, paste it into Cloud Shell, and press Enter. The command fetches a specific PowerShell release from
erato.chat, checks its SHA-256 checksum, and performs a read-only check. No local installation, download, file upload, or private repository access is needed. -
Read the results: PASS means that Azure setting is present; MISSING means a step remains. Access errors and the wrong tenant stop the check. Under Preview and apply changes, copy the preview command. It uses PowerShell’s
-WhatIf; nothing is changed. -
Review the preview, then copy the separate apply command when ready.
-Applydisplays the target and proposed changes and asks for confirmation. It:- creates a single-tenant Azure Bot (Free tier,
global) for the existing app registration, or corrects an existing bot’s messaging endpoint and enables its Teams channel, - adds SSO settings to the existing app registration (a v2 access-token setting, the
access_as_userscope, Teams client pre-authorization, the Bot Framework redirect and delegated Graph permissions), - creates a separate OAuth connection with a dedicated credential that never leaves Azure,
- grants tenant-wide consent for the delegated Graph permissions. Without the required role, everything else stays applied and the report links the admin consent page.
-SkipConsentleaves consent to a separate process.
It never changes a bot of another tenant, type or app registration, preserves existing settings, and refuses to overwrite a connection with conflicting settings.
- creates a single-tenant Azure Bot (Free tier,
View script shows the exact PowerShell source inside the setup page before execution. View command exposes every parameter and the expected checksum. The command contains public deployment identifiers, never credentials. Every check, preview and apply command independently verifies the script before running it. If verification fails, nothing executes; reopen the setup page and contact Erato support if it persists. Do not remove the checksum check to continue.
Without a resource group and bot name, the helper finds the existing bot for the app registration within the selected subscription; it does not search other subscriptions or switch tenants. If several bots use that app ID, enter the resource group and bot name to select one. Deployment details and connection names lists the identifiers. An existing bot configured before this guided setup may have a graph connection; the helper keeps it and creates graph-sso next to it.
The check also reads the tenant’s app management policies and reports them before any change: a cap on secret lifetime (the dedicated OAuth credential then gets that lifetime instead of 365 days), a policy that blocks new client secrets on the app (Apply stops before writing), and Entra’s restriction on Application ID URIs, which exempts apps that issue v2 access tokens: Apply saves that setting on its own before adding the URI. Reading the policies needs Policy.Read.All; without it the check says so and Apply still runs. Errors name the step that failed. Record the credential’s reported key ID and expiry, and rerun the helper before it expires; secret values are never printed. If apply fails after a partial change, the helper identifies the added credential when available. Inspect the connection and credential before retrying. Process environments sharing an app registration sequentially.
For local administration, the same reviewed script runs with PowerShell 7.2+ and Azure CLI. Browser-based Cloud Shell is the default path. -Json produces a sanitized support report. Exit 0 means checks pass, 1 means settings such as admin consent remain, and 2 means the operation stopped or failed. An apply can complete its writes and still exit 1 because consent remains outstanding. The helper never deploys Erato or installs a Teams package.
References: Cloud Shell overview , PowerShell preview and confirmation .
3. Upload the Teams package and test
- On the setup page, download the Teams package. In Microsoft 365 admin center, Settings → Integrated apps, upload it as a Teams app (or update the existing Erato app) and deploy it to its users: deployed apps are installed for them and update without asking. Alternatively, upload it in Teams admin center and install it with an app setup policy. Adding a bot or changing single sign-on otherwise requires every user to accept an update.
- When the app is installed for someone, the bot greets them in their personal chat. Users Erato does not know yet are asked to open the Erato tab (or the web app, with
public_base_url) once, so Erato records their Entra object ID. Send the bot a message. - The setup page’s status shows whether a Teams message reached Erato (recorded in the database, so on any replica) and whether Microsoft Entra accepts Erato’s bot credential. The credential check works right after the Erato deploy, before any Azure Bot exists, and asks Entra at most once a minute per instance. It distinguishes a rejected secret, an app Entra does not find in the tenant, and Entra being unreachable. Select Refresh status after sending.
- Verify that sign-in completes without selecting Sign in, subject to the tenant’s consent and Conditional Access requirements. Also test the Teams web client, the tab and the other Office add-ins, then an @mention in a group chat or channel and a file.
Security and network
The messaging endpoint is the only Erato route that must be reachable from the internet. Microsoft does not support restricting it by IP address: there is no published list of Bot Connector addresses, and Teams traffic has been observed outside the AzureBotService service tag. Anyone can also install a single-tenant bot’s package in another tenant. Microsoft’s guidance is that the bot must refuse what it should not process, so the protection is layered:
- Erato (the real gate). Before reading the request body, Erato verifies the Bot Connector token: RS256 signature from Microsoft’s Bot Framework key set, a key endorsed for Teams, issuer
https://api.botframework.com, audience equal to the bot’s app ID, and its validity period with five minutes of clock skew. Only then does it read at most 256 KB, and the activity must come from the Teams channel, match the token’sserviceurl, and belong totenant_id. Signing keys are refreshed at most once a minute, and known keys stay valid if Microsoft is unreachable, so forged tokens cannot make Erato flood Microsoft’s endpoints. Attachments are only downloaded over HTTPS from Teams and SharePoint hosts, re-checked on every redirect. Rejections are logged with their reason, never with the token. Users must be known to Erato, and their data is accessed with their own Teams sign-in. - Edge. Route only
/api/integrations/ms_teams/messages, preferably on a separate hostname (msTeamsBot.publicIngress); everything else on that host returns 404. Limit request bodies to about 256 KB, keep TLS 1.2 enabled, and apply an overall rate limit rather than a per-IP one, because Microsoft’s sending addresses are shared. An IP allowlist based on the service tag is unsupported and can break without notice; use it in monitoring mode at most. A web application firewall should use a policy for this path only: known false positives are CRS rules 920300 and 931130, and the SQL injection and XSS rules trip on message text such as pasted code. Never log theAuthorizationheader: a captured token can be replayed until it expires. - Cluster. The network policy lets only the ingress controller reach the backend port; only the exact messaging path bypasses oauth2-proxy.
- Microsoft 365 and Azure. Teams app permission policies decide who can use the bot. New Azure Bots get the Web Chat and Direct Line channels enabled; Erato refuses their activities, and you can remove them. Do not delete the Azure Bot while keeping the app registration: only one bot can use an app ID.
To verify the gate from outside, without a token, both requests must fail:
curl -i -X POST https://<public host>/api/integrations/ms_teams/messages \
-H 'Content-Type: application/json' -d '{}' # 401 Unauthorized
curl -i https://<public host>/api/integrations/ms_teams/messages # 405 Method Not AllowedManual setup
Use these steps if your organization manages Azure and Entra through another process, or to understand what the guided setup does.
1. Record the identities and deployment URL
| Name used below | Meaning |
|---|---|
TENANT_ID | Customer’s Entra tenant |
BOT_APP_ID | Entra client ID registered as the bot’s messaging identity |
AUTH_APP_ID | Entra client ID used by the add-in, OAuth connection and bot SSO |
TEAMS_APP_ID | Package ID in integrations.ms_office.teams.app_id; keep it stable |
ERATO_HOST | Public Erato hostname, without https:// or a path |
SSO_RESOURCE | Exact Application ID URI exposed by AUTH_APP_ID |
For a new deployment, BOT_APP_ID and AUTH_APP_ID can both be the existing
customer-owned single-tenant add-in registration. A bot authenticates as a server
and needs a client secret even though browser MSAL does not use one.
For an existing deployment with separate bot registrations, retain BOT_APP_ID
and its working credentials. The OAuth connection can use AUTH_APP_ID separately.
For our combined tab/bot package, keeping AUTH_APP_ID equal to the current tab’s
client ID avoids changing its NAA identity. The generated manifest has one
webApplicationInfo.id; bots[].botId identifies the messaging bot.
Microsoft documents separate bot and authentication registrations .
2. Extend the existing Entra authentication registration
Open Entra admin center → App registrations → the application with AUTH_APP_ID.
Keep its existing redirect URIs, scopes, permissions, and credentials.
- Under Manifest, set
api.requestedAccessTokenVersionto2. This concerns access tokens issued for this application’s exposed API; review existing API consumers if the registration already exposes one. - Under Expose an API, configure the Application ID URI. For a combined
bot/tab app, use
api://ERATO_HOST/botid-BOT_APP_ID. Record this asSSO_RESOURCEwithout/access_as_userat the end. The suffix must use the messaging bot’sBOT_APP_ID, even whenAUTH_APP_IDis a separate registration. Teams can otherwise reportresourcematchfailedand show no reply. - Add an enabled delegated scope named
access_as_user. Use clear admin/user consent labels such as “Access Erato as the signed-in user”. - Under Authorized client applications, add both clients below and select
access_as_userfor each.
| Client | Application ID |
|---|---|
| Teams desktop and mobile | 1fec8e78-bce4-4aaf-ab1b-5451cc387264 |
| Teams web | 5e3ce6c0-2b1f-4285-8d4b-75ee78787346 |
-
Under Authentication → Add a platform → Web, add
https://token.botframework.com/.auth/web/redirect. Keep the existing Web callbacks and SPAbrk-multihub://ERATO_HOSTredirect. -
Under API permissions → Microsoft Graph → Delegated permissions, include the following permissions, together with any additional scopes Erato uses:
openid profile email offline_access User.Read GroupMember.Read.All Files.Read.All Sites.Read.All Chat.Read ChannelMessage.Read.All -
Have an appropriately privileged tenant administrator grant admin consent. Configuring permissions alone does not grant consent. Consent on an old bot registration does not transfer to the shared authentication registration.
-
Under Certificates & secrets, add a dedicated OAuth connection credential. Use its Value, not its Secret ID, in step 4. Keep existing credentials.
If staging and production share an authentication registration, retain both
environment-specific resource URIs in its identifierUris array (the Entra
manifest editor or Graph API can edit this array). Use the same access_as_user
scope and Teams client pre-authorizations. Process the environments sequentially.
References: Entra SSO configuration , combined bot/tab manifest , Application ID URI policy .
3. Create or inspect the Azure Bot resource
Open Azure Portal → All resources and find the existing Azure Bot.
If none exists, create an Azure Bot in the customer’s subscription/resource group,
select Single Tenant → Use existing app registration, and supply BOT_APP_ID
and TENANT_ID. Record the resource name, subscription and resource group.
Under Configuration, set the messaging endpoint to the publicly reachable
address (the messaging_endpoint host when Erato itself is internal-only):
https://ERATO_HOST/api/integrations/ms_teams/messagesUnder Channels, enable Microsoft Teams. The bot credential corresponding
to BOT_APP_ID belongs in the Erato deployment’s secret storage. This is separate
from the OAuth credential when BOT_APP_ID and AUTH_APP_ID differ.
4. Configure the OAuth connection
On the Azure Bot resource, open Configuration → OAuth Connection Settings.
For a staged rollout, create a new connection named graph-sso and keep any
existing graph connection until validation is complete.
If helper version 1.0.0 configured graph-sso with an authentication-app-ID URI,
the current setup command previews and repairs that specific mismatch. It keeps
the existing credential and old Entra URIs, and refuses the repair if the
connection’s authentication app, tenant, provider, or required permissions differ.
After applying, update Erato’s sso_resource and the deployed Teams package as
described below; changing Azure alone does not update the installed manifest.
| Field | Value |
|---|---|
| Name | graph-sso |
| Service provider | Azure Active Directory v2 |
| Client ID | AUTH_APP_ID |
| Client secret | The dedicated credential value from step 2 |
| Tenant ID | TENANT_ID |
| Token Exchange URL | SSO_RESOURCE, exactly as exposed in Entra |
| Scopes | The space-separated delegated Graph scopes listed in step 2 |
Save and use Test Connection. A successful test verifies interactive OAuth; it does not establish that Teams silent SSO works. That requires the package and client checks below. See Microsoft’s OAuth connection configuration .
5. Configure the Erato deployment
With BOT_APP_ID and AUTH_APP_ID both equal to the add-in’s msal_client_id,
the connection named graph-sso and SSO_RESOURCE equal to
api://ERATO_HOST/botid-BOT_APP_ID, the defaults from
step 1 of the guided setup already match. Otherwise
set the values that differ:
[integrations.ms_office.teams.bot]
enabled = true
app_id = "BOT_APP_ID"
tenant_id = "TENANT_ID"
oauth_connection_name = "graph-sso"
sso_app_id = "AUTH_APP_ID"
sso_resource = "SSO_RESOURCE"
# Supply app_password through the deployment's secret storage.Keep the add-in’s msal_client_id = "AUTH_APP_ID". Keep TEAMS_APP_ID and the
existing bot identity stable. Increase the Teams manifest version so the updated
package is distinguishable from the installed version.
The exact path /api/integrations/ms_teams/messages must reach the backend without
oauth2-proxy stripping its Bot Connector Authorization header. The Erato Helm chart
supports msTeamsBot.directRoute; configure the ingress/network-policy allowance
for the actual controller. For example:
msTeamsBot:
directRoute:
enabled: true
ingressFrom:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginxThe backend validates this route’s authentication.
Apply the deployment using the environment’s normal deployment procedure. The Cloud Shell helper does not modify Helm, Kubernetes, or Erato configuration.
6. Update the Teams package and verify the user experience
- Open
https://ERATO_HOST/office-addin/setup, select Teams, and download the package. Inspectmanifest.jsoninside the ZIP:bots[0].botIdisBOT_APP_ID.webApplicationInfo.idisAUTH_APP_ID.webApplicationInfo.resourceis exactlySSO_RESOURCE.nestedAppAuthInforetains the tab’s NAA redirect.validDomainsincludes exactlytoken.botframework.com.
- Upload the package as an update to the existing app through the customer’s Teams/Microsoft 365 app-management process. Downloading a new manifest does not update the installed app.
- Open the Erato web app or Teams tab once with the test user to record their Entra object ID. Then test a fresh personal bot conversation.
- Verify that user authentication completes without clicking Sign in, subject to the tenant’s consent and Conditional Access requirements. Also test the Teams web client, existing tab login, and the other Office add-ins.
- Test an @mention and a file operation after the personal-chat check succeeds.
The bot resumes the original message after successful silent or interactive
sign-in. Teams can hide the OAuth card when resource matching fails, leaving the
conversation silent. For resourcematchfailed, verify both exact URI agreement
and that its botid- suffix uses the messaging bot’s ID. Distribute an updated
Teams package after changing the manifest. Channels use a personal conversation
for authentication.
If necessary, roll back the deployment to oauth_connection_name = "graph", remove
the SSO overrides, and distribute a higher-version package with the prior
authentication manifest. Keep the original connection and credentials during
rollout. Additive changes to a shared Entra app also affect the other environments
using it; remove only additions whose consumers you have checked.
Configuration Options
| Option | Type | Default | Description |
|---|---|---|---|
enabled | boolean | false | Whether the Teams bot is enabled |
app_id | string | add-in msal_client_id | Bot app registration ID |
app_password | string | Bot client secret; with the default app_id, the web login’s (oauth2-proxy) secret | |
tenant_id | string | tenant of a tenant-specific msal_authority | Entra tenant whose activities are accepted |
oauth_connection_name | string | "graph-sso" | Azure Bot OAuth connection that yields the user’s Graph token |
token_service_url | string | "https://token.botframework.com" | Bot Framework token service |
sso_enabled | boolean | true | Teams single sign-on; false keeps an existing bot without SSO and its manifest unchanged |
sso_app_id | string | add-in msal_client_id | App ID for silent sign-in, rendered into webApplicationInfo.id |
sso_resource | string | api://<host>/botid-<app_id> | Token exchange URL for silent sign-in; <host> is where the Teams package is downloaded from |
public_base_url | string | Address users open Erato at, for links in bot messages; an internal host is fine | |
messaging_endpoint | string | setup page address | Public URL Azure Bot Service posts messages to, shown on the setup page and passed to its helper |
assistant_id | string | Assistant that new Teams chats are bound to | |
context_message_count | integer | 20 | Preceding group chat or channel messages attached as context (0 disables) |
streaming | boolean | true | Stream answers in personal chats |
security_groups_only | boolean | true | Resolve only security groups; match the web login’s groups claim setup |
Limitations
- MCP servers that forward the user’s OIDC ID token receive none on Teams requests; servers using access-token forwarding get the Graph token.
- Client tools that run in the desktop sidecar are not available in Teams. The answer links to the chat in Erato instead.
- In group chats and channels, answers built from the requester’s data are visible to everyone in the conversation.