Navigace
ELOWEN · Dokumentace

Poslední aktualizace: 20. října 2026

Procházet dokumentaci · Microsoft 365 permissions

Tento článek zatím není přeložen. Je dostupný v angličtině.

Developer reference

msteams — Microsoft permissions reference

This reference separates the bot's application permissions, Teams resource-specific consent and the delegated scopes used by Microsoft 365 tools.

Setting this plugin up is not one consent screen. There are four independent permission surfaces, each configured in a different portal and approved by a different person. Getting one right tells you nothing about the others, and three of the four are optional — the plugin degrades to a narrower feature set instead of failing to start.

# Surface Configured in Consented by Optional?
1 Bot identity Azure Bot resource + Entra app nobody (no Graph permission at all) required
2 Application Graph permissions Entra → the bot app → API permissions tenant admin, once optional (graphLookup)
3 Resource-specific consent (RSC) the Teams app package manifest a team owner, per team optional (channelMessagesRsc)
4 Delegated Graph scopes a second Entra app + Bot Framework OAuth connection tenant admin, then each user at first sign-in optional (accountLinking)

Surface 5 is not a permission at all but is grouped here because it is the other thing an Entra admin has to type: the redirect URIs for web sign-in.


1. Bot identity — needs no Graph permission

The chat itself runs entirely on the Bot Connector, not on Graph. Nothing in this section requires an API permission or admin consent.

  • One Entra app registration, signInAudience: AzureADMyOrg (single tenant). Its Application (client) ID becomes the plugin's appId, the directory ID becomes tenantId, and a client secret becomes appPassword.
  • One Azure Bot resource with msaAppType: SingleTenant, bound to that app id and tenant, messaging endpoint https://<public-host>/hooks/msteams/messages, with the Teams channel enabled.
  • Outbound calls authenticate with the fixed scope https://api.botframework.com/.default (SCOPE in lib/connector.mjs), issued by the tenant token endpoint built from tenantId (TokenSource in lib/token.mjs).

The plugin refuses to connect at all until appId, appPassword and tenantId are all set — no platform, no webhook, no tools (register in index.mjs).

With only this surface configured the bot can read and reply in any chat or channel it has been added to, but it can only reach people it has already seen.

2. Application Graph permissions — on the bot app

These use the bot's own app-only token, so they apply tenant-wide and need tenant admin consent. They exist to close exactly one gap: writing to somebody the bot has never met.

Turn the feature on with graphLookup. While it is off, the adapter constructor builds no GraphClient, so none of these permissions are exercised. The channel-history reader is the only other Graph consumer. It is created only when channelMessagesRsc is on (threadReader in lib/transcript.mjs), reuses the directory's client when graphLookup is also on, and needs ChannelMessage.Read.Group from section 3, not the permissions in this table.

Permission Required for Call
User.ReadBasic.All Resolving an e-mail address to a tenant user GET /users/{address}, then a $filter fallback on mail/userPrincipalName (findUser in lib/graph.mjs)
TeamsAppInstallation.ReadWriteSelfForUser.All Installing the Teams app for that user so a 1:1 chat can be opened POST /users/{id}/teamwork/installedApps (installApp in lib/graph.mjs)
ProfilePhoto.Read.All Directory avatars in the admin people list — optional GET /users/{id}/photos/48x48/$value (userPhoto in lib/graph.mjs)

ProfilePhoto.Read.All degrades gracefully: without it the photo fetch fails, the plugin logs one warning naming the permission and then stops asking for five minutes, the people list falls back to the linked Elowen account avatar and then to initials, and everything else keeps working (userPhoto).

Path: Entra admin center → App registrations → the bot's app → API permissions → Add a permission → Microsoft Graph → Application permissions → the permission → Grant admin consent for the tenant. On a 401/403 the plugin quotes this exact path back at you with the missing permission name filled in, instead of forwarding a bare status code (consentHelp and GraphClient.call in lib/graph.mjs).

Do not over-grant. The plugin does not use AppCatalog.Read.All or GroupMember.Read.All on the bot app. The catalog entry is supplied by configuration (graphCatalogAppId), not looked up, and group membership is read through the delegated connection in surface 4. A new instance should leave both out.

graphCatalogAppId is not appId

Installing the app for a user binds appCatalogs/teamsApps/{graphCatalogAppId} (installApp). That id is the app's entry in the organisation's Teams catalog (Teams admin center → Manage apps → the uploaded app), which is a different GUID from the bot's appId. Leave it empty only when the app is already pushed to users by a Teams app policy; the plugin then skips the install and opens the chat directly (findPersonViaGraph in lib/proactive.mjs).

RSC is the one surface no tenant admin touches. A team owner approves it when the app package is installed or updated in their team, and it applies only inside that team.

Enabling channelMessagesRsc adds this block to the generated app package (appManifest in lib/appPackage.mjs):

"authorization": {
  "permissions": {
    "resourceSpecific": [{ "type": "Application", "name": "ChannelMessage.Read.Group" }]
  }
}
Permission Type Buys you
ChannelMessage.Read.Group Application (RSC) In a team where the app is installed, the bot sees every channel message instead of only @mentions, which is what fills the conversation history backfill (historyLimit)

Teams accepts this instead of the tenant-wide ChannelMessage.Read.All, and it is the least-privileged option for the job — which is the whole reason it is used (the PERM_READ_CHANNEL_MESSAGES comment in lib/graph.mjs).

Two traps:

  • The setting alone changes nothing. The package has to be re-downloaded and re-uploaded into the team, and the team owner has to accept the new permission prompt.
  • Reading a channel message still only sees it. In a team channel, Teams delivers a message to a bot only when it is @mentioned — that is a Teams rule and respondWithoutMention does not override it.

webApplicationInfo (id = the bot appId, resource = any valid URL) is required by the manifest schema whenever RSC is declared; the resource value is otherwise unused (appManifest).

4. Delegated Graph scopes — the second app registration

This is the surface people miss, because it lives on a separate Entra app registration from the bot.

It powers the Microsoft 365 tools (MicrosoftOutlook, MicrosoftFiles, MicrosoftSharePoint, MicrosoftDirectory, MicrosoftTeams, …; the full list of eight is in the Microsoft 365 page). Every call runs as the signed-in person with that person's own rights, never as the bot — so the tools can never reach a document the user could not open themselves. Tokens are held by the Bot Framework Token Service, not by Elowen.

The wiring

  1. Create a second Entra app registration (single tenant), separate from the bot app. Give it the redirect URI https://token.botframework.com/.auth/web/redirect and a client secret.
  2. Add the delegated scopes below to it and grant admin consent for the tenant.
  3. On the Azure Bot resource, add a Connection Setting:
    • Service provider: Azure Active Directory v2
    • Client id / secret: from the second app
    • Tenant ID: your tenant
    • Token Exchange URL: leave empty
    • Scopes: the list below, space-separated
  4. Put the connection's exact name into the plugin's oauthConnectionName, and switch accountLinking on.

The scopes

Configure the following delegated scopes and grant the necessary administrator consent:

openid profile email offline_access
User.Read User.Read.All People.Read
Files.ReadWrite.All Sites.ReadWrite.All
Mail.ReadWrite Mail.Send Mail.ReadWrite.Shared Mail.Send.Shared
Calendars.ReadWrite Calendars.ReadWrite.Shared
Contacts.ReadWrite Tasks.ReadWrite
Group.ReadWrite.All Notes.ReadWrite.All
Chat.ReadWrite ChatMessage.Send
Team.ReadBasic.All Channel.ReadBasic.All ChannelMessage.Read.All ChannelMessage.Send

Mapped to what stops working without each group (the plugin's own map, lib/m365/shared.mjs, P):

Tool family Delegated scope
identity of the signed-in person User.Read
MicrosoftDirectory User.Read.All, GroupMember.Read.All
people suggestions People.Read
MicrosoftSharePoint Sites.ReadWrite.All
MicrosoftFiles (OneDrive) Files.ReadWrite.All
MicrosoftOutlook mail Mail.ReadWrite, Mail.Send
calendar Calendars.ReadWrite
contacts Contacts.ReadWrite
to-do Tasks.ReadWrite
Planner Tasks.ReadWrite, Group.ReadWrite.All
OneNote Notes.ReadWrite.All
MicrosoftTeams Chat.ReadWrite, ChannelMessage.Read.All, ChannelMessage.Send

The .Shared variants (Mail.ReadWrite.Shared, Mail.Send.Shared, Calendars.ReadWrite.Shared) are what let a person act on a delegated or shared mailbox they have rights to — drop them if nobody uses one. openid profile email offline_access are the sign-in basics; offline_access is what makes the connection survive past the first hour, so leaving it out produces a connection that works once and then stops.

MicrosoftDirectory requires GroupMember.Read.All for group-membership reads. If a directory call answers 403, check that scope. The plugin surfaces the required permission name in the error text on a 403 (lib/m365/shared.mjs, permissionHint).

Chat.Read.All is deliberately absent. Reading arbitrary chat history through Graph needs that protected permission and a tenant admin approval process; the plugin records the traffic it receives from the Bot Framework instead (lib/transcript.mjs), and reads channel threads only through the resource-specific permission in section 3.

Scope changes need the connection rewritten, not just consented

The Azure connection stores its scope list in the top-level scopes property and in a parameters entry written by the portal. Read the top-level scopes when auditing the effective requested scope list.

5. Redirect URIs — for web sign-in

Two unrelated redirect URIs, on two different apps. Neither is a permission, both are typed in Entra.

App Redirect URI Used by
the bot app <ssoRedirectBase>/api/auth/sso/microsoft/callback the Microsoft button on the Elowen login page (ssoEnabled)
the delegated app https://token.botframework.com/.auth/web/redirect the Bot Framework OAuth connection in surface 4

The web-sign-in URI is built from the single configured public URL (redirectUri in src/auth/msSso.ts), so ssoRedirectBase must be the address people actually sign in at.

Setting up a new tenant — checklist

  1. Entra → App registrations → new registration, single tenant. → appId, tenantId, client secret → appPassword.
  2. Azure Bot resource, SingleTenant, bound to that app; messaging endpoint https://<host>/hooks/msteams/messages; enable the Teams channel.
  3. Fill appId, appPassword, tenantId in the plugin and confirm it connects. Stop here if you only need chat.
  4. Web sign-in: add <public-url>/api/auth/sso/microsoft/callback to the bot app's redirect URIs, set ssoRedirectBase, switch on ssoEnabled.
  5. Proactive messaging: grant User.ReadBasic.All + TeamsAppInstallation.ReadWriteSelfForUser.All (Application) with admin consent, upload the app to the org Teams catalog, put its catalog id in graphCatalogAppId, switch on graphLookup.
  6. Channel history: switch on channelMessagesRsc, re-download the app package, re-install it in each team, have a team owner accept the prompt.
  7. Microsoft 365 tools: second app registration + Bot Framework OAuth connection with the 25 delegated scopes and admin consent; set oauthConnectionName and accountLinking. Choose m365AccessMode — read_only blocks every external mutation, read_write still previews each mutation before the agent commits it.

Traps, collected

  • Two app registrations, not one. The bot app and the delegated-access app are separate. Reusing the bot app for the OAuth connection means its redirect URI and consent set have to carry both jobs.
  • ChannelMessage.Read.All and ChannelMessage.Read.Group are different things. The first is a delegated scope on the delegated app (surface 4). The second is an application RSC permission in the Teams package (surface 3). Having one does not give you the other.
  • graphCatalogAppId ≠ appId.
  • RSC needs a package re-upload, not just the config switch.
  • offline_access is not optional in practice — without it the delegated connection dies after the first token expires.
  • Saved configuration may still be pending. Credentials and policy changes become effective after the host-requested daemon restart. Until then the current adapter keeps its previous configuration.
  • The plugin's config hints carry the click-path. graphLookup, oauthConnectionName, ssoRedirectBase, graphCatalogAppId and channelMessagesRsc each spell out what the admin has to do, so the setting screen is usable without this page.