Microsoft Teams
Bot Framework messaging with JWT + JWKS-validated inbound activities. Multi-tenant or single-tenant.
🟡 Outbound-ready, inbound JWT-verified, talks to Teams via the Bot Connector REST API. Inbound activities are signed by Microsoft and verified against a dynamically-discovered JWKS, so a spoofed POST returns 401.
What you get
- Outbound text via Bot Framework Conversation API:
POST {serviceUrl}/v3/conversations/{conversationId}/activitieswith{type:"message", text}. Threads (replyToActivityId) are supported. - Inbound webhook at
/webhooks/teams. Microsoft signs every activity with a JWT; Minara discovers the JWKS via the OpenID metadata endpoint (https://login.botframework.com/v1/.well-known/openidconfiguration) and caches both for 24 hours. - OAuth client-credentials token cache for outbound, 1-hour TTL with 5-minute refresh headroom.
- Text only this PR. Adaptive Cards, Office 365 connectors, and file uploads are a future enhancement.
- 28000-char text limit.
Setup
1. Register a bot (Azure)
- In the Azure portal, create an Azure Bot resource (single region; pricing tier "F0 free" is enough for development)
- After provisioning, the "Configuration" blade shows the Microsoft App ID (a GUID) and lets you create a Client Secret (the App Password). Save both.
- Decide tenant scope: "Multi Tenant" (use
commonfor the tenant id) or "Single Tenant" (use your Azure AD tenant GUID).
2. Connect the bot to Teams
- Same Azure Bot blade → "Channels" → "Microsoft Teams". Accept the Terms of Service and enable.
- Create a Teams app manifest (or use the Developer Portal for Teams) that points to your bot's Microsoft App ID. Side-load into your tenant for testing.
- Add the bot to a team or DM it from a test user. The first
inbound activity carries the
conversation.idandserviceUrlMinara needs.
3. Set the bot's messaging endpoint
In the Azure Bot blade → "Configuration" → "Messaging endpoint",
set the URL to https://<your-host>/webhooks/teams.
4. Configure Minara
minara auth messaging add
# pick `teams` from the list; paste App ID, App Password, tenant id
# (or "common"), the default conversation id and service URL.Or set env vars:
TEAMS_BOT_APP_ID=<bot Microsoft App ID GUID>
TEAMS_BOT_APP_PASSWORD=<bot Microsoft App Password>
TEAMS_BOT_TENANT_ID=common
TEAMS_DEFAULT_CONVERSATION_ID=<conversation id from a captured inbound activity>
TEAMS_DEFAULT_SERVICE_URL=https://smba.trafficmanager.net/teams/The TEAMS_DEFAULT_SERVICE_URL value above is the production multi-
tenant route. For single-tenant or sovereign-cloud deployments,
override with whatever serviceUrl appeared in the first inbound
activity from your tenant.
5. Test
minara auth messaging test teamsInbound webhook
Bot Framework signs inbound activities with a JWT in the
Authorization: Bearer ... header. Minara verifies:
| Claim | Expected value |
|---|---|
iss (issuer) | https://api.botframework.com |
aud (audience) | TEAMS_BOT_APP_ID (multi-tenant) or TEAMS_BOT_TENANT_ID GUID (single-tenant) |
| Signature | RS256, key discovered from https://login.botframework.com/v1/.well-known/openidconfiguration |
| Clock skew | ±5 minutes |
JWKS + OpenID metadata are cached for 24 hours. Microsoft rotates
keys periodically; on a kid miss the cache is invalidated and
refetched once.
Activity types accepted: message. Other types (conversationUpdate,
typing, installationUpdate) are dropped.
Limits & caveats
serviceUrlis per-conversation. The Bot Framework documentation says you must captureserviceUrlfrom each inbound activity and persist it per-conversation. Minara honours the inboundserviceUrlif present; the env default is only the bootstrap fallback for cold push.- Multi-tenant audience. With
TEAMS_BOT_TENANT_ID=commonthe bot accepts activities from any tenant. The JWT audience equalsTEAMS_BOT_APP_ID. For single-tenant, set the tenant GUID and the audience becomes the tenant GUID. - Adaptive Cards. Not yet supported; the bot speaks plain text.
- JWKS discovery requires network access at first inbound POST.
If
login.botframework.comis firewalled, validation fails closed (401).
Troubleshooting
"401 on every inbound"
- Clock skew > 5 minutes between your host and Microsoft.
TEAMS_BOT_APP_IDvalue does not match the bot's Microsoft App ID in Azure (a frequent paste mistake; the Azure Bot has both an Application ID and a resource ID, you need Application ID).- For single-tenant:
TEAMS_BOT_TENANT_IDdoes not match the audience in the inbound JWT.
"401 on outbound (token endpoint)"
TEAMS_BOT_APP_PASSWORDwas rotated or revoked. Generate a fresh Client Secret in the Azure Bot configuration blade.
"Conversation not found (404 on outbound)"
- The default
TEAMS_DEFAULT_CONVERSATION_IDis stale (user uninstalled the bot, or the channel was deleted). Capture a fresh conversation id from a recent inbound activity.
Reference
- Env vars:
TEAMS_* - Outbound:
apps/agent/src/messaging/teams.ts - Inbound spec:
apps/agent/src/messaging/inbound/specs/teams.ts - JWT helper:
apps/agent/src/messaging/_shared/jwt-verify.ts - Bot Connector authentication: learn.microsoft.com