Skip to content

Your own Microsoft (Teams / Entra) apps

Around thirty minutes, most of it in the Azure portal. At the end your people can sign in to TeamHUD with their Microsoft work account — no separate TeamHUD password — and your server can tell their apps the moment a Teams message arrives (the “doorbell”), without the message ever passing through your server.

Read INTEGRATIONS.md first: it covers the hostname requirement and the desktop setting without which none of this is used. Throughout, https://teamhud.example.com stands for your PUBLIC_BASE_URL.

Part State Needs
Sign in with Microsoft (part 1) ✅ works, verified against a real Entra tenant one app registration, GOTRUE_EXTERNAL_AZURE_*, AZURE_TENANT
Live updates / doorbell — server side (part 2) ✅ built and tested against a simulated Graph; not yet run against a real tenant a second app registration with admin consent, MSTEAMS_*, a public HTTPS address
Teams chats and channels inside TeamHUD ⬜ not built — there is no Teams connector, and the desktop app does not yet listen for the doorbell —

Be honest with your users: signing in with Microsoft works; reading Teams inside TeamHUD does not yet.

TeamHUD accounts made this way have no TeamHUD password — Microsoft is their only way in. (forgot password sends them nothing, and the reset route refuses them.)

Azure portal → Microsoft Entra ID → App registrations → New registration.

  • Name: something your users recognise, for example Acme TeamHUD. They see it on Microsoft’s consent screen.
  • Supported account types: Accounts in this organizational directory only (single tenant). Your server is for your organisation; a multi-tenant app would let other organisations’ staff reach your consent screen.
  • Redirect URI: platform Web, https://teamhud.example.com/auth/v1/callback — your PUBLIC_BASE_URL followed by /auth/v1/callback, character for character. That is your server’s sign-in service, not the desktop app, and it is the address your server sends Microsoft unless you override GOTRUE_EXTERNAL_AZURE_REDIRECT_URI. Microsoft requires https except for localhost. (Trying it on a local stack first? See On a local stack below.)

Then, on the new app:

  1. Overview → copy the Application (client) ID and the Directory (tenant) ID.
  2. Certificates & secrets → New client secret → copy the Value immediately (it is hidden afterwards). Put a reminder in your calendar: secrets expire (24 months at most) and sign-in stops the day it does.
  3. Token configuration → Add optional claim → ID token → email. TeamHUD needs an email address for every account, and Microsoft does not always send one. If the portal offers to turn on the Microsoft Graph email permission with it, accept.
  4. Every person who will sign in needs an Email set on their user (Entra → Users → the user → Edit properties → Contact information). A user with no mailbox otherwise has no email claim and sign-in fails with a vague error.
Terminal window
GOTRUE_EXTERNAL_AZURE_ENABLED=true
GOTRUE_EXTERNAL_AZURE_CLIENT_ID=<application (client) id>
GOTRUE_EXTERNAL_AZURE_SECRET=<client secret value>
AZURE_TENANT=<directory (tenant) id>
SOCIAL_PROVIDERS=azure # or slack_oidc,azure

Set AZURE_TENANT. It pins sign-in to your organisation: the server sends people to your tenant’s own Microsoft sign-in page. Use the directory (tenant) id or a verified domain (contoso.onmicrosoft.com). firstboot.sh asks for it and refuses to continue without one. Unset, the server uses Microsoft’s shared common address instead. A single-tenant app like the one above is refused there (AADSTS50194); an app switched to multi-tenant would let any Microsoft account in the world through. (organizations, any work or school account, also needs a multi-tenant app.)

No /v2.0 on the end. The authority is https://login.microsoftonline.com/<tenant> and nothing more. The sign-in service appends /oauth2/v2.0/authorize itself, and Microsoft answers the doubled path with a 404 (checked again 2026-10-16). This matters only if you set the whole authority yourself in GOTRUE_EXTERNAL_AZURE_URL, which replaces AZURE_TENANT; most installs should not.

Terminal window
docker compose up -d
curl -s https://teamhud.example.com/auth/providers # should list "azure"
./verify-stack.sh # step 13 fails if a provider in SOCIAL_PROVIDERS is not offered

Microsoft sent no display name in our 2026-10-08 test, so that person’s first organization was named from their email address (tech@… became Tech’s Team).

SIGNUP_MODE=sso_only makes Microsoft the only way to a new account (no self-service passwords); an invitation still lets someone in. open also allows email + password. closed accepts nobody new — including by Microsoft. See SIGN-IN-AND-CHANNELS.md.

To try sign-in before your server has its real address. Use a separate test registration, or remove the localhost redirect from your real one afterwards.

  • Register the redirect http://localhost:8000/auth/v1/callback. Port 8000 is the gateway that carries the sign-in service, published on this machine only (GATEWAY_PORT in .env; use yours if you changed it). Not :3001: that is the API server, and Microsoft’s redirect would never reach sign-in. Not 127.0.0.1: in our 2026-10-08 test Microsoft would not match it to the registered localhost address.
  • Add the two overrides below to the settings from step 2, then docker compose up -d again. The first points the browser at the gateway; the second sends Microsoft the redirect you just registered. Without them both are built from PUBLIC_BASE_URL, which on a local stack is the API server.
Terminal window
AUTH_PUBLIC_URL=http://localhost:8000
GOTRUE_EXTERNAL_AZURE_REDIRECT_URI=http://localhost:8000/auth/v1/callback

Remove both when you move to the real hostname. After Microsoft, sign-in returns to <APP_URL>/auth/callback. The default local .env points APP_URL at the API server (port 3001), so a full round trip from the dashboard in a browser also needs APP_URL set to the dashboard’s own address.

You see Meaning
azure missing from /auth/providers azure is not in SOCIAL_PROVIDERS, or the sign-in service has no or wrong credentials for it. In the second case ./verify-stack.sh step 13 fails and names it
Microsoft shows a 404 page right after the click the authority ends in /v2.0 — see step 2
Microsoft says the app is not multi-tenant (AADSTS50194) AZURE_TENANT is unset — see step 2
Microsoft says the redirect URI does not match (AADSTS50011) the registered redirect is not exactly <PUBLIC_BASE_URL>/auth/v1/callback (or your override)
Microsoft says the account does not exist in the tenant (AADSTS50020) the person is not in the tenant you pinned. This is the pin working as intended
Back on the start page, not signed in, no error the sign-in service dropped the return address: <APP_URL>/auth/callback must match GOTRUE_URI_ALLOW_LIST (default <PUBLIC_BASE_URL>/** plus the desktop app’s http://localhost:1420/**), which matters when APP_URL is a different host

Microsoft calls your server when a message arrives in any Teams chat or channel in your tenant; your server works out which of your people are in that conversation and rings their TeamHUD app; their app fetches the message itself with its own sign-in. Your server never receives message text — the subscriptions are created without resource data.

This needs a second app registration, because it acts as the server, not as a person.

Same place, New registration, name it Acme TeamHUD server, single tenant, no redirect URI.

  1. API permissions → Add a permission → Microsoft Graph → Application permissions, add exactly these three: ChannelMessage.Read.All, Chat.Read.All, ChannelMember.Read.All. They are the narrowest Microsoft offers for this: it accepts nothing smaller for being told about every chat and channel, and the third is only for finding who is in a channel. (ChatMember.Read.All is not needed; Chat.Read.All already covers who is in a chat.)

  2. Grant admin consent for . This needs a tenant administrator, and it is the step most often missed: without it, the server’s status page shows Insufficient privileges.

    What you are consenting to, plainly: these permissions let this server app read every Teams chat and channel message in your organisation, and who is in each chat and channel, without anyone signed in. Microsoft names them Read all channel messages, Read all chat messages and Read the members of all channels. TeamHUD uses far less: it asks Microsoft to say only that a message arrived and where, never for the message itself, and it looks up the email addresses of the people in that conversation to know whose app to ring. That restraint is TeamHUD’s code, not a limit Microsoft enforces, so treat the app’s client secret like an administrator password. The doorbell uses no one’s Microsoft sign-in, only this app’s own secret. Delete the secret or this app registration and the doorbell stops; sign-in (part 1, a separate app) and everything else keep working.

  3. Certificates & secrets → a client secret, as above.

No billing setup, no access request (Microsoft’s documentation, read 2026-10-16). Teams message change notifications are not billed: “Starting August 25, 2025, the Teams APIs listed in this article are no longer metered, and no billing configuration is required to use these APIs. … the model query parameter is no longer required and is ignored when supplied.” So you do not link an Azure subscription to this app, and TeamHUD sends no payment model. Microsoft removed its Teams protected APIs page (the access-request form) in 2023, and the current subscription and Teams notification pages name no access request. Neither is confirmed in a real tenant yet: if the status page below shows 402 Payment Required or a Microsoft error about API access, that is why.

Terminal window
ENT_SECRET=<openssl rand -hex 32> # if you do not already have one; it also derives the webhook secret
MSTEAMS_TENANT_ID=<directory (tenant) id>
MSTEAMS_CLIENT_ID=<the SERVER app's application (client) id>
MSTEAMS_CLIENT_SECRET=<the SERVER app's secret value>

All four, or the doorbell stays off. Leaving them empty is fine: everything else works, and apps simply poll.

Terminal window
docker compose up -d

Microsoft must be able to call, over HTTPS with a valid certificate (not a raw IP, not a self-signed certificate):

  • https://teamhud.example.com/webhooks/msteams/notifications
  • https://teamhud.example.com/webhooks/msteams/lifecycle

Both answer Microsoft’s validation handshake even before you finish configuring, so you can test reachability first. As a platform admin, open GET https://teamhud.example.com/msteams/doorbell/status:

You see Meaning
{"enabled": false, "missing": [...]} the named settings are not set (names only, never values)
enabled: true with two subscriptions and lastError: null working: Microsoft holds two subscriptions for you and your server renews them every 30 minutes
lastError: "...Insufficient privileges..." admin consent was not granted, or a permission is missing
lastError: "...(401): AADSTS7000215..." wrong or expired client secret
lastError: "...(402)..." or Payment Required Microsoft is billing these APIs again (not expected: unmetered since 2025-08-25) — check Microsoft’s current Teams API licensing page
lastError: "...Subscription validation request failed..." Microsoft could not reach the notification URL — check DNS, TLS and any firewall (Microsoft’s IP ranges are listed under Additional endpoints for Microsoft 365)
  • A person can only listen on their own doorbell, and only if Microsoft itself put them in your tenant at sign-in. An account made with a password and a look-alike email hears nothing.
  • A notification is accepted only if it carries the secret we set when creating the subscription, for your tenant.
  • Anything resembling message text in a notification is never read.
  • If Microsoft reports lost notifications, every connected app is told to re-check.

Subscriptions live up to three days and are renewed automatically. The client secrets (both apps) expire on the date you chose — rotate them before then, update .env, and docker compose up -d. Changing ENT_SECRET changes the webhook secret: the server recreates its subscriptions on the next 30-minute tick, and the old ones lapse.

IMAP mail is in the alpha (owner, corrected 2026-10-27), as a Pro preview with app-password providers; ImapDataSourcePlugin is wired into the desktop app (plugin-entries.ts). XOAUTH2 for Microsoft still waits on the Entra client ID.

This part exists today, and needs no server setup at all. TeamHUD’s mail connector speaks IMAP/SMTP straight from each user’s desktop. Credentials are stored only on that machine, encrypted with the OS keychain, and never sent to your server.

Mailbox How users connect Notes
Any IMAP provider with password or app-password login (Fastmail, iCloud, personal Outlook.com with 2-step verification, most hosts) host + username + app password Available, early: so far tested only against simulated mail servers. Replying from TeamHUD is still being finished
Gmail over IMAP app password (personal accounts with 2-Step Verification), or the user’s own Google OAuth client Workspace accounts usually cannot create app passwords. Prefer GOOGLE.md
Microsoft 365 / Exchange Online Microsoft sign-in (OAuth) Not available yet. Microsoft has turned off password login for IMAP on business tenants, and TeamHUD has not yet registered the app it needs. The code is written but has never run against a real Microsoft mailbox

Mail over IMAP updates through the desktop’s own IMAP connection, so it needs no Gmail push or similar server-side setup.

The design and remaining work live in the TeamHUD repository under plans/1.1.0/MS_TEAMS_PLAN.md (Teams, Sign in with Microsoft) and plans/1.1.0/IMAP_MAIL_CONNECTOR_PLAN.md (Microsoft 365 mail).