This guide explains how teams authenticates to Microsoft Graph, what customers need to approve, and which auth model is suitable for commercial use.
Use delegated Microsoft Graph auth for normal CLI usage.
Delegated auth means:
- A real Microsoft 365 user signs in.
- Microsoft Graph calls run on behalf of that signed-in user.
- Teams messages sent by the CLI appear as that user.
- The user's own Teams permissions still apply.
- Tenant admins can grant consent to the OSO app once for the organization.
This matches Microsoft Graph's delegated access model and avoids asking every customer to create an Entra app registration just to use the CLI.
Client credentials produce app-only tokens. App-only Graph access is valid for some admin and read scenarios, but it is not the right model for normal live Teams chat/channel sending.
Microsoft documents ordinary chat message sending as a delegated work/school operation with ChatMessage.Send; the application permission shown for send is Teamwork.Migrate.All, which is for migration/import scenarios, not normal live conversations. Channel replies are similar: delegated ChannelMessage.Send is the normal model and application permissions are migration-only.
The CLI now rejects app-only tokens before mutating normal Teams messages so users get a clear local error instead of a confusing Graph failure.
Use client credentials only for operations backed by Microsoft Graph application permissions that are appropriate for the target API.
Delegated login defaults to OSO's multi-tenant public client app:
Client ID: fba1b5d0-fdd0-4fe2-9729-9ccdc38f9595
Authority: organizations
Redirect URI: http://localhost:8400/callback
The default lets most users start with:
teams auth loginor, for SSH/headless use:
teams auth login --device-codeThe OSO app is baked into the CLI as the default delegated public client. It still requires publisher verification before broad commercial release. Publisher verification should be completed before asking external enterprise tenants to consent.
Generate an admin consent URL:
teams auth consent-url --tenant-id <tenant-id-or-domain> --output jsonFor the default app, this prints a URL like:
https://login.microsoftonline.com/<tenant>/v2.0/adminconsent?client_id=fba1b5d0-fdd0-4fe2-9729-9ccdc38f9595&scope=...&redirect_uri=...
The scope parameter is explicit so admin consent matches the CLI's default
delegated Graph scopes instead of every static permission configured on the app
registration.
Use a concrete tenant ID or verified tenant domain for customer onboarding. organizations is useful for sign-in discovery, but a customer admin consent link should normally target the customer's tenant explicitly.
The OSO app registration currently asks for these delegated Microsoft Graph permissions:
User.Read
offline_access
Team.ReadBasic.All
Channel.ReadBasic.All
ChannelMessage.Send
Chat.ReadWrite
ChatMessage.Send
ChatMessage.Read
User.ReadBasic.All
Presence.Read.All
These permissions cover the current chat read/write, channel-send, team/channel discovery, user lookup, and presence smoke tests. The default does not include ChannelMessage.Read.All because Microsoft marks that delegated Graph scope as admin-consent required. Add it explicitly when a workflow needs channel message reads:
teams auth login --device-code --scopes "User.Read ChannelMessage.Read.All offline_access"Downloading message file attachments (teams message attachments download on
reference attachments stored in SharePoint/OneDrive) additionally requires
the Files.Read.All delegated scope. Inline images and code snippets need no
scopes beyond message reads. Add Files.Read.All to the profile's scopes
and run teams auth refresh to pick it up without a new browser login.
Sending has the same split (see the scope table in docs/attachments-spec.md):
message send --image sends the picture inside the message itself and needs
no Files scope at all, while message send --attach must first upload the
file to storage — your own OneDrive for chats (Files.ReadWrite), the team's
SharePoint library for channels (Files.ReadWrite.All, typically admin
consent). Note that Graph masks drives the token cannot see as 404 rather
than 403, so a "not found" from --attach usually means the missing scope,
not a missing file.
Future features may need additional consent.
Browser PKCE login:
teams auth loginDevice-code login:
teams auth login --device-codeCustom scopes:
teams auth login --device-code --scopes "User.Read ChatMessage.Send offline_access"Scopes can also be pinned per profile so every login for that profile requests
them without repeating --scopes:
[profiles.customer]
auth_app = "byo"
client_id = "11111111-1111-1111-1111-111111111111"
tenant_id = "22222222-2222-2222-2222-222222222222"
scopes = "User.Read Chat.ReadWrite ChatMessage.Send People.Read offline_access"The profile scopes value replaces the default delegated scope string (it is
not additive); offline_access is appended when missing so refresh tokens
keep working. Resolution order is --scopes or TEAMS_CLI_SCOPES, then the
profile scopes field, then the default scope set. teams auth consent-url
and teams auth doctor reflect the profile's resolved scopes, so admin
consent links match what login will request. The default scope set remains
free of admin-consent-required permissions; requesting broader scopes is an
explicit opt-in for BYO app registrations. Do not use
https://graph.microsoft.com/.default for delegated login — it would pull in
every permission configured on the app registration instead of the explicit
scope list.
Refresh tokens are bound to the user and client, not to the scopes originally requested, so an existing session can be upgraded to newly consented permissions silently — the same mechanism MSAL uses for incremental consent:
teams auth consent-url --scopes "User.Read People.Read offline_access" # admin grants once
teams auth refresh # silent upgrade, no browserauth refresh redeems the stored refresh token for the resolved scope string
(--scopes/TEAMS_CLI_SCOPES, then profile scopes). Without an explicit
override it reuses the stored token's scope, so a plain refresh never narrows
an existing session. If a requested scope has not been consented, the
identity platform rejects the whole request (AADSTS65001) rather than issuing
a narrower token; the CLI then prints the exact consent-url command to
grant it, or fall back to teams auth login for interactive consent.
Customer-owned delegated app:
teams auth login --device-code \
--client-id <customer-client-id> \
--tenant-id <customer-tenant-id>Client credentials for supported app-only Graph operations:
export TEAMS_CLI_CLIENT_ID=<client-id>
export TEAMS_CLI_CLIENT_SECRET=<client-secret>
export TEAMS_CLI_TENANT_ID=<tenant-id>
teams auth login --client-credentialsPre-obtained token:
export TEAMS_CLI_ACCESS_TOKEN=<access-token>
teams user me --output jsonUse BYO mode when a customer requires their own Entra app registration.
Config example:
[default]
profile = "customer"
output = "json"
[profiles.customer]
auth_app = "byo"
client_id = "11111111-1111-1111-1111-111111111111"
tenant_id = "22222222-2222-2222-2222-222222222222"
auth_flow = "device-code"Then:
teams --profile customer auth login --device-code
teams --profile customer auth doctor --output jsonBYO delegated app requirements:
- Supported account type: usually single-tenant for locked-down enterprises.
- Public client flows enabled.
- Redirect URI for browser flow:
http://localhost:8400/callback. - Delegated Graph permissions required for the features the customer wants.
- Admin consent granted where required.
For enterprises that want agents to post without a human user session, the correct direction is a Teams app/bot mode:
- Customer installs a Teams app/bot into the target chat, team, or channel.
- OSO stores the bot installation/conversation reference securely.
- Agents send proactive messages through the bot identity.
- Audit logs clearly show an application/bot posted the message.
This is separate from the current Graph delegated CLI mode. Do not market client credentials as the solution for normal unattended Teams chat posting.
Tokens are stored in the operating system keyring:
- macOS: Keychain
- Windows: Credential Manager
- Linux: Secret Service or compatible keyring backend
The config file stores profile settings, not access tokens.
The CLI automatically redeems the stored refresh token when an access token is expired or near expiry, then updates the keyring with the refreshed token. If no refresh token is stored, or the identity platform rejects the refresh request, commands return AUTH_TOKEN_EXPIRED and the user must run teams auth login again.
Check configured auth app, consent URL, and token claims:
teams auth doctor --output jsonCheck whether the profile is authenticated:
teams auth status --output jsonList profiles:
teams auth list --output jsonLog out:
teams auth logout
teams auth logout --all| Variable | Purpose |
|---|---|
TEAMS_CLI_ACCESS_TOKEN |
Use this bearer token instead of the keyring token. |
TEAMS_CLI_CLIENT_ID |
Entra app client ID. |
TEAMS_CLI_CLIENT_SECRET |
Client secret for client credentials flow. |
TEAMS_CLI_TENANT_ID |
Tenant ID or tenant domain. |
TEAMS_CLI_SCOPES |
Delegated OAuth scopes for login. Same precedence as --scopes; ignored by client credentials login. |
TEAMS_CLI_DISABLE_KEYRING |
Test-only escape hatch used by CLI tests to avoid real OS keyring access. |