No description
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-05-21 20:57:15 +02:00
.gitignore First commit 2026-05-21 19:55:43 +02:00
config.php.example Added Device code auntentication 2026-05-21 20:39:10 +02:00
docker-compose.yml First commit 2026-05-21 19:55:43 +02:00
README.md Readme commit 2026-05-21 20:57:15 +02:00
TeamsMessenger.php Added Device code auntentication 2026-05-21 20:39:10 +02:00
TeamsPooling.php Added Device code auntentication 2026-05-21 20:39:10 +02:00
TeamsSender.php Added Device code auntentication 2026-05-21 20:39:10 +02:00

TeamsMessenger

TeamsMessenger is a PHP helper for working with Microsoft Teams 1:1 chat messages over Microsoft Graph. It is designed for a single delegated user and supports sending messages, polling for unread messages, reading reactions, and marking chats as read so the same messages are not returned on every poll.

The intended use case is a small internal integration, script, or daemon where one Microsoft 365 user account is authorized once and then reused silently through cached tokens. The recommended modern authentication model for that setup is delegated OAuth with Device Code flow and offline_access, not username/password or app passwords.

What it does

The code is meant to act as a lightweight Teams chat client wrapper around Microsoft Graph for PHP. In practical terms, it can:

  • Send a chat message to a user in a 1:1 Teams conversation.
  • Read a user's chats and recent messages.
  • Detect unread chats using the chat viewpoint.lastMessageReadDateTime field.
  • Mark chats as read after processing so the same unread messages do not keep appearing on every run.
  • Return message reactions as part of the normalized message data.

This project is not a full Teams client and does not aim to render every rich message type. Teams messages can contain plain text, reactions.

Authentication model

Teams chat send/read scenarios generally require delegated permissions because the app is acting on behalf of a user, and sending chat messages is documented primarily under delegated permissions rather than normal application-only auth.

For a single fixed user that should authenticate once and then run unattended, the recommended pattern is:

  1. Use Device Code flow for the initial sign-in.
  2. Request offline_access so a refresh token is issued.
  3. Store the access token, refresh token, expiry, and /me profile in the token cache.
  4. Refresh silently on later runs until the refresh token is revoked or expires.

This avoids storing the user's password and works better with modern Microsoft identity controls than ROPC.

Microsoft tenant setup

The following Microsoft Entra / Azure app registration setup is required for this code to work with Device Code flow and Microsoft Graph Teams chat APIs.

1. Create an app registration

In the Microsoft Entra admin center:

  1. Go to Microsoft Entra ID.
  2. Open App registrations.
  3. Click New registration.
  4. Give the app a name such as TeamsMessenger.
  5. Choose the tenant scope that matches the environment, usually Accounts in this organizational directory only for a single-tenant internal tool.

After creation, note these values:

  • Application (client) ID → used as clientId in the PHP code (update the config.php file).
  • Directory (tenant) ID → used as tenantId in the PHP code (update the config.php file).

2. Enable public client flows

Because Device Code flow is a public client flow, the app registration must allow public client flows. If this is not enabled, the token polling step fails with AADSTS7000218 and Microsoft asks for client_secret or client_assertion instead.

In the app registration:

  1. Open Authentication.
  2. Scroll to Advanced settings.
  3. Set Allow public client flows to Yes.

This is one of the most important setup steps for Device Code flow.

3. Add Microsoft Graph delegated permissions

In the app registration:

  1. Open API permissions.
  2. Click Add a permission.
  3. Choose Microsoft Graph.
  4. Choose Delegated permissions.
  5. Add the permissions required by the code.

Permissions for this project:

Permission Why it is needed
User.Read Read /me so the app can identify the signed-in user and cache their profile.
Chat.Read or Chat.ReadWrite Read chats, messages, and chat state for the delegated user.
ChatMessage.Send Send Teams chat messages as the delegated user.
offline_access Receive a refresh token for silent long-term reuse.
openid and profile Standard identity scopes commonly used with delegated sign-in flows.

If the application needs to mark chats as read with markChatReadForUser, the delegated chat permissions must also allow that Graph call in the user's context.

Depending on tenant policy, some delegated permissions may require admin consent before the app can be used. If users are not allowed to self-consent in the tenant, a tenant administrator must approve the requested Microsoft Graph permissions in the app registration.

In the app registration:

  1. Open API permissions.
  2. Click Grant admin consent if required by tenant policy.

5. Device Code bootstrap

On the first run, the app should start Device Code flow and show a message similar to:

To sign in, use a web browser to open the page https://login.microsoft.com/device and enter the provided code.

The user then:

  1. Opens the device login page.
  2. Enters the shown code.
  3. Signs in with the Microsoft 365 account that should own the integration.
  4. Accepts consent if prompted.

The PHP script must print this message and flush output immediately so the user can actually see the code before the polling loop waits for authorization.

6. Store token cache

After the first successful Device Code sign-in, the code should save a token cache file containing at least:

  • access_token
  • refresh_token
  • expires_at
  • tenant_id
  • cached /me profile

This lets later runs silently reuse the token cache or refresh the access token when it expires, instead of forcing the user through browser login every time.

Unread message logic

This project uses Teams chat read state from the viewpoint.lastMessageReadDateTime property on a chat to determine whether a chat has unread content. Microsoft documents that this caller-specific read-state information is available on the chat object and can be compared with lastMessagePreview.createdDateTime to decide whether the chat is unread.

A good polling flow is:

  1. List chats with lastMessagePreview, members, and viewpoint.
  2. Keep only chats where the last preview is newer than the read timestamp.
  3. Fetch messages for those chats.
  4. Filter to message items newer than the read timestamp.
  5. Mark the chat as read after processing so the same messages are not returned again.

Message content limitations

Teams messages are not always plain text. Some messages contain HTML, reactions, file attachments, or hosted content such as inline images and GIF-like content, and Microsoft exposes hosted content through dedicated resources and endpoints.

For this reason, the code should be described as a text-first Teams chat helper rather than a full-fidelity Teams renderer. Core read/send/reaction support is the main scope; advanced media handling can be added later if needed.

Security notes

Security recommendations:

  • Keep the token cache outside the web root and protect file permissions.
  • Do not commit token cache files to version control.
  • Use a dedicated Entra app registration for this integration rather than sharing one across unrelated tools.

Setup checklist

Use this checklist when deploying the tool to a tenant:

  • Create a Microsoft Entra app registration.
  • Enable Allow public client flows.
  • Copy the tenant ID and client ID into the PHP config.
  • Add delegated Microsoft Graph permissions: User.Read, Chat.Read or Chat.ReadWrite, ChatMessage.Send, and request offline_access during sign-in.
  • Grant admin consent if tenant policy requires it.
  • Run first-time Device Code authentication and complete the browser sign-in.
  • Confirm the token cache file contains access token, refresh token, expiry, and /me profile.
  • Run polling and sending scripts without interactive login on subsequent executions.