From 474d1e812bf3fe1a1f75b2ab06f477c631bf62c3 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Fri, 8 May 2026 09:41:38 -0700 Subject: [PATCH] docs(msgraph): webhook listener setup page + env var reference Second docs slice shipped alongside the webhook listener code so users can actually wire up the endpoint the moment this PR lands. - website/docs/user-guide/messaging/msgraph-webhook.md: new page covering what the listener is (change-notification ingress, distinct from the teams chat adapter), quick-start YAML + env-var config, full config table, security hardening (clientState + timing-safe compare, source-IP allowlisting against Microsoft's published egress ranges, TLS termination at the reverse proxy, response hygiene), status-code table, troubleshooting, and cross-links to the Azure app registration guide. - website/docs/reference/environment-variables.md: new Microsoft Graph Webhook Listener subsection with MSGRAPH_WEBHOOK_ENABLED, _PORT, _CLIENT_STATE, _ACCEPTED_RESOURCES, _ALLOWED_SOURCE_CIDRS. - website/sidebars.ts: wire the new page into Messaging Platforms, right after the teams chat adapter so the two related pages are adjacent in the sidebar. The pipeline runtime / operator CLI / outbound delivery pages still land with their matching PRs. With this PR merged, an operator can get the listener running end-to-end, register a Graph subscription manually, and receive validation handshake plus notification POSTs against the configured client_state. Verified via npm run build: new page routes at /docs/user-guide/messaging/msgraph-webhook, sidebar wires correctly, no new warnings or errors. --- .../docs/reference/environment-variables.md | 12 ++ .../user-guide/messaging/msgraph-webhook.md | 137 ++++++++++++++++++ website/sidebars.ts | 1 + 3 files changed, 150 insertions(+) create mode 100644 website/docs/user-guide/messaging/msgraph-webhook.md diff --git a/website/docs/reference/environment-variables.md b/website/docs/reference/environment-variables.md index 078e1ff5b7..8ff9a5cbf4 100644 --- a/website/docs/reference/environment-variables.md +++ b/website/docs/reference/environment-variables.md @@ -418,6 +418,18 @@ App-only credentials for the Microsoft Graph REST client used by the upcoming Te | `MSGRAPH_SCOPE` | OAuth2 scope for the client-credentials token request (default: `https://graph.microsoft.com/.default`). | | `MSGRAPH_AUTHORITY_URL` | Microsoft identity platform authority (default: `https://login.microsoftonline.com`). Override only for national/sovereign clouds (e.g. `https://login.microsoftonline.us` for GCC High). | +### Microsoft Graph Webhook Listener + +Inbound change-notification listener for Graph events (Teams meetings, calendar, chat, etc.). See [Microsoft Graph Webhook Listener](/docs/user-guide/messaging/msgraph-webhook) for setup and security hardening. + +| Variable | Description | +|----------|-------------| +| `MSGRAPH_WEBHOOK_ENABLED` | Enable the `msgraph_webhook` gateway platform (`true`/`1`/`yes`). | +| `MSGRAPH_WEBHOOK_PORT` | Port the listener binds to (default: `8646`). | +| `MSGRAPH_WEBHOOK_CLIENT_STATE` | Shared secret Graph echoes in every notification; compared with `hmac.compare_digest`. Generate with `openssl rand -hex 32`. | +| `MSGRAPH_WEBHOOK_ACCEPTED_RESOURCES` | Comma-separated allowlist of Graph resource paths/patterns (e.g. `communications/onlineMeetings,chats/*/messages`). Trailing `*` is prefix-matching. Empty = accept all. | +| `MSGRAPH_WEBHOOK_ALLOWED_SOURCE_CIDRS` | Comma-separated CIDR ranges allowed to POST to the listener (e.g. `52.96.0.0/14,52.104.0.0/14`). Empty = allow all (default). Restrict to Microsoft Graph's published egress ranges in production. | + ### Advanced Messaging Tuning Advanced per-platform knobs for throttling the outbound message batcher. Most users never need to touch these; defaults are set to respect each platform's rate limits without feeling sluggish. diff --git a/website/docs/user-guide/messaging/msgraph-webhook.md b/website/docs/user-guide/messaging/msgraph-webhook.md new file mode 100644 index 0000000000..da2aa45773 --- /dev/null +++ b/website/docs/user-guide/messaging/msgraph-webhook.md @@ -0,0 +1,137 @@ +--- +sidebar_position: 23 +title: "Microsoft Graph Webhook Listener" +description: "Receive Microsoft Graph change notifications (meetings, calendar, chat, etc.) in Hermes" +--- + +# Microsoft Graph Webhook Listener + +The `msgraph_webhook` gateway platform is an inbound event listener. It's how Hermes receives **change notifications** from Microsoft Graph — "a Teams meeting ended," "a new message landed in this chat," "this calendar event was updated." Different from the `teams` platform (which is a chat bot users type to) — this one is M365 telling Hermes something happened, not a person. + +Right now the primary consumer is the Teams meeting summary pipeline: Graph notifies when a meeting produces a transcript, the pipeline fetches it, and Hermes posts a summary back into Teams. Other Graph resources (`/chats/.../messages`, `/users/.../events`) use the same listener — the pipeline consumers land with their own PRs. + +## Prerequisites + +- Microsoft Graph application credentials — [Register a Microsoft Graph Application](/docs/guides/microsoft-graph-app-registration) +- A **public HTTPS URL** that Microsoft Graph can reach (Graph does not call private endpoints). A dev tunnel works for testing; production needs a real domain with a valid certificate. +- A strong shared secret to use as the `clientState` value. Generate with `openssl rand -hex 32` and put it in `~/.hermes/.env` as `MSGRAPH_WEBHOOK_CLIENT_STATE`. + +## Quick Start + +Minimum `~/.hermes/config.yaml`: + +```yaml +platforms: + msgraph_webhook: + enabled: true + extra: + port: 8646 + client_state: "replace-with-a-strong-secret" + accepted_resources: + - "communications/onlineMeetings" +``` + +Or via env vars in `~/.hermes/.env` (auto-merged on startup): + +```bash +MSGRAPH_WEBHOOK_ENABLED=true +MSGRAPH_WEBHOOK_PORT=8646 +MSGRAPH_WEBHOOK_CLIENT_STATE= +MSGRAPH_WEBHOOK_ACCEPTED_RESOURCES=communications/onlineMeetings +``` + +Start the gateway: `hermes gateway run`. The listener exposes: + +- `POST /msgraph/webhook` — change notifications from Graph +- `GET /msgraph/webhook?validationToken=...` — Graph subscription validation handshake +- `GET /health` — readiness probe with accepted/duplicate counters + +Expose the listener publicly (reverse proxy, dev tunnel, ingress). Your notification URL for Graph subscriptions is your public HTTPS origin followed by `/msgraph/webhook`: + +``` +https://ops.example.com/msgraph/webhook +``` + +## Configuration + +All settings go under `platforms.msgraph_webhook.extra`: + +| Setting | Default | Description | +|---------|---------|-------------| +| `host` | `0.0.0.0` | Bind address for the HTTP listener. | +| `port` | `8646` | Bind port. | +| `webhook_path` | `/msgraph/webhook` | URL path Graph POSTs to. | +| `health_path` | `/health` | Readiness endpoint. | +| `client_state` | — | Shared secret Graph echoes in every notification. Compared with `hmac.compare_digest` — generate with `openssl rand -hex 32`. | +| `accepted_resources` | `[]` (accept all) | Allowlist of Graph resource paths/patterns. Trailing `*` acts as prefix match. Leading `/` is tolerated. Example: `["communications/onlineMeetings", "chats/*/messages"]`. | +| `max_seen_receipts` | `5000` | Dedupe cache size for notification IDs. Oldest entries evicted when the cap is hit. | +| `allowed_source_cidrs` | `[]` (allow all) | Optional source-IP allowlist. See below. | + +Each setting also has an equivalent env var (`MSGRAPH_WEBHOOK_*`) that merges into the config at gateway startup — see the [environment variables reference](/docs/reference/environment-variables#microsoft-graph-teams-meetings). + +## Security Hardening + +### clientState is the primary auth check + +Every Graph notification includes the `clientState` string your subscription registered with. The listener rejects any notification whose `clientState` doesn't match, using timing-safe comparison. This is Microsoft's documented mechanism — treat the value as a strong shared secret. + +If `client_state` is unset, the listener accepts every well-formed POST. **Don't run without it in production.** + +### Source-IP allowlisting (production deployments) + +For production, restrict the listener to Microsoft's published Graph webhook source IP ranges. Microsoft documents the egress ranges under the [Office 365 IP Address and URL Web service](https://learn.microsoft.com/en-us/microsoft-365/enterprise/urls-and-ip-address-ranges). Configure them as: + +```yaml +platforms: + msgraph_webhook: + enabled: true + extra: + client_state: "..." + allowed_source_cidrs: + - "52.96.0.0/14" + - "52.104.0.0/14" + # ...add the current Microsoft 365 "Common" + "Teams" category egress ranges +``` + +Or as an env var: + +```bash +MSGRAPH_WEBHOOK_ALLOWED_SOURCE_CIDRS="52.96.0.0/14,52.104.0.0/14" +``` + +Empty allowlist = accept from anywhere (default; preserves dev-tunnel workflows). Invalid CIDR strings log a warning and are ignored. **Review the Microsoft IP list quarterly** — it changes. + +### HTTPS termination + +The listener speaks plain HTTP. Terminate TLS at your reverse proxy (Caddy, Nginx, Cloudflare Tunnel, AWS ALB) and proxy to the listener over the local network. Graph refuses to deliver to non-HTTPS endpoints, so there's no path for unencrypted traffic to reach you from Graph itself. + +### Response hygiene + +On success the listener returns `202 Accepted` with an empty body — internal counters stay out of the wire response. Operators can observe counts via `/health`. + +Status code table: + +| Outcome | Status | +|---------|--------| +| Notification(s) accepted or deduped | 202 | +| Validation handshake (GET with `validationToken`) | 200 (echoes the token) | +| Every item in batch failed clientState | 403 | +| Malformed JSON / missing `value` array / unknown resource | 400 | +| Source IP not in allowlist | 403 | +| Bare GET without `validationToken` | 400 | + +## Troubleshooting + +| Problem | What to check | +|---------|---------------| +| Graph subscription validation fails | Public URL is reachable, `/msgraph/webhook` path matches, GET with `validationToken` echoes the token verbatim as `text/plain` within 10 seconds. | +| Notifications POST but nothing ingests | `client_state` matches what you registered the subscription with. Re-run `openssl rand -hex 32` and create a new subscription if the value drifted. Check `accepted_resources` includes the resource path Graph is sending. | +| Every notification 403s | `clientState` mismatch (forged, or subscription registered with a different value). Re-create the subscription with `hermes teams-pipeline subscribe --client-state "$MSGRAPH_WEBHOOK_CLIENT_STATE" ...` (ships with the pipeline runtime PR). | +| Listener starts but `curl http://localhost:8646/health` hangs | Port binding collision. Check `ss -tlnp \| grep 8646` and change `port:` if needed. | +| Real Graph requests from Microsoft get 403'd | Source IP allowlist is too narrow. Remove `allowed_source_cidrs` temporarily, confirm traffic flows, then widen the list to include the current Microsoft egress ranges. | + +## Related Docs + +- [Register a Microsoft Graph Application](/docs/guides/microsoft-graph-app-registration) — Azure app registration prereq +- [Environment Variables → Microsoft Graph](/docs/reference/environment-variables#microsoft-graph-teams-meetings) — full env var list +- [Microsoft Teams bot setup](/docs/user-guide/messaging/teams) — the different platform that lets users chat with Hermes in Teams diff --git a/website/sidebars.ts b/website/sidebars.ts index 05dc891821..ba5be971c4 100644 --- a/website/sidebars.ts +++ b/website/sidebars.ts @@ -136,6 +136,7 @@ const sidebars: SidebarsConfig = { 'user-guide/messaging/qqbot', 'user-guide/messaging/yuanbao', 'user-guide/messaging/teams', + 'user-guide/messaging/msgraph-webhook', 'user-guide/messaging/open-webui', 'user-guide/messaging/webhooks', ],