Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Embr × Foundry · Teams Outgoing Webhook bot

A Microsoft Teams bot fronted by Embr, backed by an Azure AI Foundry deployment via the OpenAI-compatible endpoint. Built with Node.js 20 + Express + TypeScript.

This sample is #3 in the Embr POC fleet and probes a specific platform finding: does Embr's ingress preserve request-body integrity faithfully enough for HMAC-SHA256 signature validation to succeed? Teams signs the raw bytes of every Outgoing Webhook callback; if any byte changes in flight (chunked re-encoding, Content-Length adjustment, JSON re-serialization, whitespace normalization), HMAC fails and the bot returns 401.

What the sample demonstrates

  • Teams Outgoing Webhook receiver at POST /api/teams
  • HMAC-SHA256 verification against the raw request body, base64-encoded, with constant-time comparison
  • Async-ack-then-process pattern — a 4.5s soft timeout on the Foundry call with a fallback "thinking…" response so we beat Teams' ~5s budget
  • Adaptive Card response shape for the success path; plain {type: "message"} for fallbacks and errors
  • Landing page + /api/diag to inspect the last received payload, raw body length, headers, and HMAC validation result — the diagnostic surface area for the platform finding

Required environment variables

Variable What Example
TEAMS_WEBHOOK_SECRET The base64 HMAC token from the Teams Outgoing Webhook setup UI <set-via-embr-variables-set>
FOUNDRY_BASE_URL OpenAI-compatible base URL of your Foundry project https://<resource>.services.ai.azure.com/api/projects/<project>/openai/v1
FOUNDRY_API_KEY API key for the Foundry project <set-via-embr-variables-set>
FOUNDRY_MODEL_DEPLOYMENT Deployed model name gpt-4o-mini

The Teams secret is provided in the Outgoing Webhook configuration UI and is already base64-encoded — paste it verbatim into the env var, the code decodes it to bytes before computing the HMAC.

Endpoints

Method Path Purpose
GET / HTML landing showing config + last request
GET /health Health check (used by Embr's healthCheck)
POST /api/teams Teams Outgoing Webhook callback (HMAC-validated)
GET /api/diag JSON snapshot of state + sanitized last request

Quickstart (local, with ngrok)

npm install
export TEAMS_WEBHOOK_SECRET='<paste from Teams Outgoing Webhook UI>'
export FOUNDRY_BASE_URL='https://<resource>.services.ai.azure.com/api/projects/<project>/openai/v1'
export FOUNDRY_API_KEY='<your key>'
export FOUNDRY_MODEL_DEPLOYMENT='gpt-4o-mini'
npm run start

# In another terminal:
ngrok http 8080

Copy the https://<id>.ngrok-free.app URL ngrok prints, append /api/teams, and paste it into the Teams Outgoing Webhook Callback URL field. Save, go to the Teams channel, and @your-bot-name hi. You should see an Adaptive Card reply.

If you see Unauthorized: hmac mismatch, hit GET /api/diag to see the exact raw body, headers, and computed-vs-provided HMAC values.

Deploy to Embr

embr quickstart deploy embr-devs/embr-foundry-teams-bot-sample -i 120233234

embr variables set TEAMS_WEBHOOK_SECRET '<paste from Teams UI>'
embr variables set FOUNDRY_BASE_URL 'https://<resource>.services.ai.azure.com/api/projects/<project>/openai/v1'
embr variables set FOUNDRY_API_KEY '<your key>'
embr variables set FOUNDRY_MODEL_DEPLOYMENT 'gpt-4o-mini'

Once Embr reports the deployment URL (e.g. https://teams-bot.example.embr.app), go back to the Teams Outgoing Webhook configuration UI and update the Callback URL to https://<embr-deployment-host>/api/teams. Save. Then @your-bot-name hi in Teams.

Embr platform findings

This sample is intentionally narrow. It surfaces three things:

1. HMAC raw-body integrity through Embr ingress (central finding)

Teams' HMAC is computed over the exact bytes of the request body. If the local quickstart works (HMAC ✓) but the deployed version returns Unauthorized: hmac mismatch, Embr's ingress is mutating the body between Teams and the app process. Likely culprits include:

  • Chunked transfer-encoding being unwrapped/re-wrapped with different framing
  • Content-Length recomputation that doesn't match the bytes delivered to the app
  • JSON normalization or whitespace stripping by an intermediary
  • TLS termination + body buffering that injects/strips a trailing newline

To file the finding, capture GET /api/diag from the deployed instance after a failing Teams call. Include rawBodyLength, contentLength, transferEncoding, the provided and expected HMAC values, and rawBodyPreview. That's enough to triage on the Embr side.

2. No first-class expose_as: webhook primitive in embr.yaml

Webhook integrations need a stable, predictable public URL across deploys — Teams (and most webhook providers) make you re-register the callback URL by hand whenever the host changes, which is friction. Today there's no declarative affordance in embr.yaml like expose_as: webhook that guarantees URL stability or even just signals intent. Speculative shape:

ingress:
  routes:
    - path: /api/teams
      stable: true       # never change the host across deploys
      raw_body: true     # do not mutate body before the app sees it

3. Teams-side limitation — NOT an Embr issue

Teams Outgoing Webhooks are synchronous-only with a hard ~5s response budget and no follow-up channel — once you 200 the response, you cannot send another message back into the conversation. For genuine conversational bots (proactive notifications, long-running tool calls, multi-turn context), you need the Bot Framework / Azure Bot Service, which would surface a different set of findings (multi-tenant Bot config, async outbound messaging, conversation references, JWT validation against Microsoft's JWKS rather than HMAC). We deliberately picked the Outgoing Webhook variant so the Embr finding is narrow and isolated to one question: does the body survive ingress?

The 4.5s soft-timeout fallback in teams-handler.ts is a partial mitigation of the Teams limitation — when Foundry is slow we send a "thinking…" message synchronously rather than time out — but the user has to send another message to retry. That's a Teams design, not an Embr gap.

File layout

src/
  server.ts          Express app + route wiring
  teams-auth.ts      HMAC verification + middleware
  teams-handler.ts   POST /api/teams handler, mention stripping, diag state
  foundry.ts         OpenAI SDK client pointed at Foundry's openai-compat endpoint
  landing.ts         GET / dark-themed HTML landing
embr.yaml            platform: nodejs 20, port 8080, /health
package.json         express, openai, dotenv, tsx
tsconfig.json        strict TypeScript, ES2022

About

Embr POC #3: Microsoft Teams Outgoing Webhook bot with HMAC verification, fronted by Embr

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages