This repository has been archived on 2026-10-18. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Miles WardandClaude Fable 5.1 ad3720edbf v0.1: OpenMFA — live TOTP delivery to Discord, Slack or Telegram
Platform-neutral successor to Discord Authenticator, forked from its v0.1.2
state with fresh history. Same vault, same UI, same live-message mechanism —
now behind one `Sink` that speaks three platforms.

Why each platform needs what it needs (this drove the whole settings model):
- Discord: an incoming-webhook URL. Webhooks can edit their own messages.
- Slack: a bot token (xoxb-, scope chat:write) + channel ID. Slack incoming
  webhooks can post but never edit, and editing IS the live mechanism, so
  chat.postMessage / chat.update via a bot is the only path.
- Telegram: a bot token + chat id. No webhook concept exists. The app detects
  the chat id itself from the bot's pending updates ("send /start, then
  Detect"), since users rarely know it.

- core: Provider enum; settings hold all three configs so switching back and
  forth never re-asks for tokens; per-platform shape validation that names the
  common mistakes (xoxp- user token, #name instead of channel ID); a
  destination key that clears message ids on platform/channel change but not
  on token rotation
- delivery: shared DeliveryError + pacing Governor; Discord client refactored
  onto them; new Slack client (classifies Slack's ok:false-on-HTTP-200
  errors) and Telegram client ("message is not modified" is success; chat
  detection via getUpdates); provider-agnostic message builders — code in a
  copyable span everywhere, rollover time as live as each platform allows
- http-api: setup accepts an optional provider; settings PUT/GET carry the
  full model; /api/slack/whoami, /api/telegram/whoami, /api/telegram/chats
- webui: three-way optional picker on setup and in Settings → Delivery with
  guided per-platform fields; "Check token" (Slack) and "Detect chat"
  (Telegram); inline white mark via currentColor
- brand: white mark on the dark plate; blue stays with the Discord sibling
- docs/TEAMS.md: exploration for v0.2 — Teams connectors were disabled May
  2026 and Workflows webhooks can't edit, so live tiles need a Bot Framework
  bot; options for the inbound endpoint on a homelab and a recommended scope

Verified against mock Discord, Slack and Telegram servers speaking each
platform's real protocol shape: edits target the same message rather than
re-posting, deleted tiles are recovered, dead credentials are surfaced with a
next step, renames refresh without a rollover, and switching platform reposts
on the new one and leaves the old one alone. 138 tests.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-09 00:23:45 -04:00

9.0 KiB
Raw Permalink Blame History

Microsoft Teams — exploration for v0.2

Short version: Teams is meaningfully harder than Discord, Slack and Telegram, and the reason is structural rather than incidental. Every path that lets an outsider post into Teams without a bot has been retired or cannot edit. Live tiles in Teams therefore require a Bot Framework bot, which means an Azure Bot registration, an app manifest, and — for a self-hosted homelab — an inbound HTTPS endpoint or a workaround for not having one. This document lays out the options and recommends one.


1. What no longer works

Office 365 Connectors / "Incoming Webhook"

The classic Teams incoming webhook was an Office 365 Connector. Microsoft retired the connector platform: URLs had to be migrated by January 2025, the service was disabled for Teams in May 2026, and it is now gone. Even when it worked, a connector could only post a message; there was no edit call. So it was never a live-tile candidate, and today it isn't a candidate for anything.

Power Automate "Workflows" webhooks

Microsoft's designated replacement. A Workflows-app webhook accepts a POST and runs a flow that posts to a channel (Adaptive Cards included, private channels supported since April 2026). It is fine for notifications. It cannot edit a message it posted earlier — the flow's "Post card in a chat or channel" action creates; there is a separate "Update an adaptive card" action, but it requires the flow to have posted the card and to still hold its state, and it is driven by the flow's own triggers, not by an external caller saying "edit message X". Chaining that into a 30-second edit loop from outside is not a supported shape and would be fragile even where it half-works.

Verdict: Workflows can be a v0.2 notification target (post-only, one message per roll — noisy), not a live-tile target.

Microsoft Graph

PATCH /teams/{team}/channels/{channel}/messages/{id} exists. But chatMessage update is delegated-permission only — an application (daemon) token cannot call it. A self-hosted service has no signed-in user to delegate from without a full interactive OAuth flow and refresh-token management, and Graph would also require the message to have been posted by that same user. Not a fit for an unattended service.


2. What does work: a Bot Framework bot

A bot registered in Azure Bot Service can post an activity to a channel and later call updateActivity on it — the Teams equivalent of chat.update. This is exactly the primitive OpenMFA needs, and it is the only supported one.

What it involves:

  1. Azure Bot registration (free tier). Yields a Microsoft App ID and a client secret (or certificate). No Azure spend is required for the bot resource itself.

  2. Teams app manifest declaring the bot, sideloaded into the tenant or installed to a team. The tenant admin must permit custom app upload.

  3. A messaging endpoint. Teams sends the bot an HTTPS POST when the bot is installed, added to a team, or messaged. The bot needs this at least once to learn the conversation.id and serviceUrl for the target channel — after that, OpenMFA can post and edit proactively using only stored values plus a token from the Bot Framework OAuth endpoint.

  4. Proactive messaging. With a stored conversation reference, the bot authenticates to https://login.microsoftonline.com/botframework.com/oauth2/v2.0/token with its App ID and secret, then calls:

    • POST {serviceUrl}/v3/conversations/{conversationId}/activities → create, returns { id }
    • PUT {serviceUrl}/v3/conversations/{conversationId}/activities/{activityId} → update
    • DELETE …/activities/{activityId} → delete

    That maps one-to-one onto the Sink trait already in crates/delivery: create / edit / delete. The engine would not change.

  5. Message format. An Adaptive Card with the code in a monospace TextBlock (fontType: Monospace) and a Copy action is the natural tile. Adaptive Cards don't have a live-ticking timestamp like Discord's <t:R>, but they do support {{DATE()}} / {{TIME()}} formatting functions that render in the viewer's locale — the Slack-equivalent "renders once, in local time" behaviour.

Rate limits

Bot Framework in Teams allows roughly 1 message per second per bot per conversation, with a burst allowance, and Teams throttles harder on channel messages than on 1:1 chats. Editing counts. With one edit per account per 30 seconds, a channel would comfortably carry ~15–20 accounts; the same one-second governor used for Slack and Telegram fits.

The self-hosting problem, and three answers

The messaging endpoint is the sticking point for a homelab. Teams must be able to reach the bot over public HTTPS, at least during installation. Options, in order of preference:

A. Cloudflare Tunnel (recommended). Miles already fronts Gitea with Cloudflare. A named tunnel exposing /api/teams/messages on the OpenMFA container gives Teams a stable public HTTPS URL with no port forwarding and no certificate management. The endpoint only needs to validate the Bot Framework JWT on inbound requests and store the conversation reference — a few dozen lines with jsonwebtoken plus the Microsoft OpenID metadata for key discovery. This keeps OpenMFA fully self-hosted.

B. One-time capture, then close the door. Because the conversation reference is only learned via the inbound endpoint, the tunnel could be opened once during setup, the reference stored in settings.json, and the tunnel torn down. Proactive posting and editing need only outbound HTTPS. Downside: if the bot is removed and re-added, or the team is recreated, the reference goes stale and the door has to be reopened. Workable for a single-user homelab; clunky as a product default.

C. Manual reference entry. The serviceUrl for a tenant is stable (https://smba.trafficmanager.net/<region>/) and the channel conversation id is visible in a Teams channel link (19:…@thread.tacv2). In principle an operator could paste both. In practice the id format has changed across Teams generations and proactive posting to a conversation the bot has never been installed into is rejected, so this only works after installation anyway. Not recommended as the primary path, but useful as an override field.


  1. crates/delivery/src/teams.rs — TeamsClient with token acquisition (client-credentials against botframework.com), create / update / delete activities, Adaptive Card builder, and error classification (ConversationNotFound → repost; 401/403 on token → fatal; 429 → back off using Retry-After).
  2. Provider::Teams in core::settings with TeamsConfig { app_id, app_secret, tenant_id, conversation_id, service_url }. Validation on the GUID shapes; destination_key on conversation_id.
  3. Inbound endpoint POST /api/teams/messages, unauthenticated by session but gated by Bot Framework JWT validation (issuer, audience = App ID, signature via Microsoft's JWKS). Handles conversationUpdate (bot added → store reference) and ignores everything else. This is the only public surface OpenMFA would ever expose and it must accept nothing but a valid Microsoft-signed token.
  4. Settings UI — a fourth segment in the picker with a guided flow: paste App ID + secret → OpenMFA shows the public endpoint URL to put in the Azure Bot's Messaging endpoint → generate a downloadable Teams app manifest (manifest.json + icons, zipped) → "install it to your team, then click Detect" → the stored conversation appears, exactly like Telegram's chat detection.
  5. Tunnel documentation — a cloudflared sidecar example in docker-compose.yml and an Unraid note.

Effort is roughly two to three times the Slack integration: the API surface is similar, but JWT validation, manifest generation and the inbound endpoint are new machinery. Nothing in it changes the engine, the vault, or the other three platforms.


4. What I would not do

  • Don't ship Teams via Workflows webhooks as if it were live. Posting a new message every 30 seconds fills a channel with 2,880 messages a day per account. If a post-only Teams mode is wanted for notifications, it should be a separate, clearly labelled "announce" mode — not the default and not called live.
  • Don't attempt Graph delegated auth. It would make OpenMFA hold a user's refresh token for their Microsoft account to edit chat messages, which is a larger credential than a bot secret and a worse thing to leak.

Sources

  • Microsoft 365 Developer Blog — Retirement of Office 365 connectors within Microsoft Teams
  • Microsoft Learn — Create an Incoming Webhook (Teams), with the Workflows migration notice
  • Microsoft Learn — Bot Framework REST API: Update Activity, Send to Conversation, Proactive messages in Teams
  • Microsoft Learn — Teams bot rate limiting
  • Microsoft Graph — chatMessage: update (delegated permissions only)