Skip to content

fix(msteams): accept strict Bot Framework and Entra service tokens#56631

Merged
BradGroux merged 2 commits intoopenclaw:mainfrom
BradGroux:fix/issue-56603
Mar 28, 2026
Merged

fix(msteams): accept strict Bot Framework and Entra service tokens#56631
BradGroux merged 2 commits intoopenclaw:mainfrom
BradGroux:fix/issue-56603

Conversation

@BradGroux
Copy link
Copy Markdown
Contributor

@BradGroux BradGroux commented Mar 28, 2026

Summary

  • keep strict Bot Framework token validation with issuer and JWKS checks
  • add strict Entra fallback validation for service tokens using common discovery JWKS and tenant issuer allowlist
  • preserve serviceUrl validation override behavior
  • add focused tests for legacy path, Entra fallback path, and all-fail behavior
  • update local Teams SDK type declarations for JwtValidator usage

Root cause fixed

Webhook auth only validated tokens against the legacy Bot Framework issuer (https://api.botframework.com) and Bot Framework JWKS. Valid Entra-signed service tokens were rejected even with correct signature and audience.

Testing

  • pnpm vitest extensions/msteams/src/sdk.test.ts extensions/msteams/src/monitor-handler/message-handler.authz.test.ts
    • passed (2 files, 10 tests)

Closes #56603

Copilot AI review requested due to automatic review settings March 28, 2026 21:49
@openclaw-barnacle openclaw-barnacle bot added channel: msteams Channel integration: msteams size: S maintainer Maintainer-authored PR labels Mar 28, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves observability for inbound Microsoft Teams messages that are intentionally dropped by policy decisions, addressing previously silent drops reported in #56603.

Changes:

  • Add info-level logs for DM drops caused by dmPolicy decisions (disabled / not allowlisted).
  • Add info-level logs for group message drops caused by route allowlists and group sender allowlists/policy.
  • Add authz-focused tests asserting info-level drop logging for DM allowlist rejection and group empty-allowlist scenarios.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
extensions/msteams/src/monitor-handler/message-handler.ts Emits new info logs for policy-based inbound drop paths (DM + group) to make default logging diagnosable.
extensions/msteams/src/monitor-handler/message-handler.authz.test.ts Adds regression tests to assert the new info-level drop reasons are logged for key policy drop scenarios.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 28, 2026

Greptile Summary

This PR addresses issue #56603 by promoting all six inbound-policy drop paths in the MS Teams message handler from silent debug-only logging to visible info-level logging, making policy-based drops diagnosable under default log settings. Two new unit tests assert that the info log fires for the DM-allowlist-reject and empty-group-allowlist paths.

Key observations:

  • The logic changes are purely additive and observability-only; no routing, authorization, or data-handling behaviour is altered.
  • For four of the six drop paths the newly added log.info(...) call and the retained log.debug?.() call carry the same message string and identical fields, producing duplicate lines at debug log level. The \"dropping dm (not allowlisted)\" and \"dropping dm (dms disabled)\" cases are fine because the info call surfaces extra context (dmPolicy, reason, sender/label) not in the debug call.
  • Test coverage is partial: the \"dms disabled\", \"not in team/channel allowlist\", \"groupPolicy: disabled\", and \"not in groupAllowFrom\" info logs have no new test assertions.

Confidence Score: 5/5

Safe to merge — purely additive observability change with no logic or data-handling impact.

All findings are P2 (style/improvement suggestions). The changes are correct and well-scoped: only logging calls are added, no routing or authorization behaviour is altered. The two minor issues do not block merge.

No files require special attention.

Important Files Changed

Filename Overview
extensions/msteams/src/monitor-handler/message-handler.ts Adds log.info() calls for all six inbound policy-drop paths. Existing log.debug?.() calls are preserved, resulting in redundant dual-logging for four paths where payloads are identical.
extensions/msteams/src/monitor-handler/message-handler.authz.test.ts Adds two new test cases for the DM allowlist-reject and empty group-allowlist drop paths. Four other new info logs remain without test assertions.

Comments Outside Diff (1)

  1. extensions/msteams/src/monitor-handler/message-handler.ts, line 198-212 (link)

    P2 Redundant dual log calls with identical payloads

    For four of the six drop paths the new log.info(...) call and the retained log.debug?.() call carry the same message string and exactly the same fields. At debug log level every drop event will therefore produce two identical lines. The affected paths are:

    • "dropping group message (not in team/channel allowlist)" — lines 198–211
    • "dropping group message (groupPolicy: disabled)" — lines 228–233
    • "dropping group message (groupPolicy: allowlist, no allowlist)" — lines 237–242
    • "dropping group message (not in groupAllowFrom)" — lines 252–261

    The "dropping dm (not allowlisted)" and "dropping dm (dms disabled)" calls are fine because the new info-level calls include extra fields (dmPolicy, reason, sender, label) not present in their debug counterparts.

    For the four fully-redundant cases, consider removing the log.debug?.() lines that are now completely subsumed by the corresponding log.info() calls.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: extensions/msteams/src/monitor-handler/message-handler.ts
    Line: 198-212
    
    Comment:
    **Redundant dual log calls with identical payloads**
    
    For four of the six drop paths the new `log.info(...)` call and the retained `log.debug?.()` call carry the same message string and exactly the same fields. At debug log level every drop event will therefore produce two identical lines. The affected paths are:
    
    - `"dropping group message (not in team/channel allowlist)"` — lines 198–211
    - `"dropping group message (groupPolicy: disabled)"` — lines 228–233
    - `"dropping group message (groupPolicy: allowlist, no allowlist)"` — lines 237–242
    - `"dropping group message (not in groupAllowFrom)"` — lines 252–261
    
    The `"dropping dm (not allowlisted)"` and `"dropping dm (dms disabled)"` calls are fine because the new info-level calls include extra fields (`dmPolicy`, `reason`, `sender`, `label`) not present in their debug counterparts.
    
    For the four fully-redundant cases, consider removing the `log.debug?.()` lines that are now completely subsumed by the corresponding `log.info()` calls.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/msteams/src/monitor-handler/message-handler.authz.test.ts
Line: 200-289

Comment:
**Incomplete test coverage for new info logs**

The PR adds six new `log.info(...)` calls across six distinct drop paths, but only two of them are exercised by new tests:

| Drop path | Tested? |
|---|---|
| `"dropping dm (dms disabled)"` ||
| `"dropping dm (not allowlisted)"` | ✓ (new test) |
| `"dropping group message (not in team/channel allowlist)"` ||
| `"dropping group message (groupPolicy: disabled)"` ||
| `"dropping group message (groupPolicy: allowlist, no allowlist)"` | ✓ (new test) |
| `"dropping group message (not in groupAllowFrom)"` ||

The untested paths are the main motivation for the PR (silent drops that are hard to diagnose), so it's worth adding assertions for at least the `"dms disabled"` and `"not in groupAllowFrom"` cases to guard against message text or field regressions.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: extensions/msteams/src/monitor-handler/message-handler.ts
Line: 198-212

Comment:
**Redundant dual log calls with identical payloads**

For four of the six drop paths the new `log.info(...)` call and the retained `log.debug?.()` call carry the same message string and exactly the same fields. At debug log level every drop event will therefore produce two identical lines. The affected paths are:

- `"dropping group message (not in team/channel allowlist)"` — lines 198–211
- `"dropping group message (groupPolicy: disabled)"` — lines 228–233
- `"dropping group message (groupPolicy: allowlist, no allowlist)"` — lines 237–242
- `"dropping group message (not in groupAllowFrom)"` — lines 252–261

The `"dropping dm (not allowlisted)"` and `"dropping dm (dms disabled)"` calls are fine because the new info-level calls include extra fields (`dmPolicy`, `reason`, `sender`, `label`) not present in their debug counterparts.

For the four fully-redundant cases, consider removing the `log.debug?.()` lines that are now completely subsumed by the corresponding `log.info()` calls.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "msteams: log policy-based inbound drops ..." | Re-trigger Greptile

Comment on lines +200 to 289
it("logs an info drop reason when dmPolicy allowlist rejects a sender", async () => {
const { deps } = createDeps({
channels: {
msteams: {
dmPolicy: "allowlist",
allowFrom: ["trusted-aad"],
},
},
} as OpenClawConfig);

const handler = createMSTeamsMessageHandler(deps);
await handler({
activity: {
id: "msg-drop-dm",
type: "message",
text: "hello",
from: {
id: "attacker-id",
aadObjectId: "attacker-aad",
name: "Attacker",
},
recipient: {
id: "bot-id",
name: "Bot",
},
conversation: {
id: "a:personal-chat",
conversationType: "personal",
},
channelData: {},
attachments: [],
},
sendActivity: vi.fn(async () => undefined),
} as unknown as Parameters<typeof handler>[0]);

expect(deps.log.info).toHaveBeenCalledWith(
"dropping dm (not allowlisted)",
expect.objectContaining({
sender: "attacker-aad",
dmPolicy: "allowlist",
reason: "dmPolicy=allowlist (not allowlisted)",
}),
);
});

it("logs an info drop reason when group policy has an empty allowlist", async () => {
const { deps } = createDeps({
channels: {
msteams: {
dmPolicy: "pairing",
allowFrom: [],
groupPolicy: "allowlist",
groupAllowFrom: [],
},
},
} as OpenClawConfig);

const handler = createMSTeamsMessageHandler(deps);
await handler({
activity: {
id: "msg-drop-group",
type: "message",
text: "hello",
from: {
id: "attacker-id",
aadObjectId: "attacker-aad",
name: "Attacker",
},
recipient: {
id: "bot-id",
name: "Bot",
},
conversation: {
id: "19:[email protected]",
conversationType: "groupChat",
},
channelData: {},
attachments: [],
},
sendActivity: vi.fn(async () => undefined),
} as unknown as Parameters<typeof handler>[0]);

expect(deps.log.info).toHaveBeenCalledWith(
"dropping group message (groupPolicy: allowlist, no allowlist)",
expect.objectContaining({
conversationId: "19:[email protected]",
}),
);
});
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Incomplete test coverage for new info logs

The PR adds six new log.info(...) calls across six distinct drop paths, but only two of them are exercised by new tests:

Drop path Tested?
"dropping dm (dms disabled)"
"dropping dm (not allowlisted)" ✓ (new test)
"dropping group message (not in team/channel allowlist)"
"dropping group message (groupPolicy: disabled)"
"dropping group message (groupPolicy: allowlist, no allowlist)" ✓ (new test)
"dropping group message (not in groupAllowFrom)"

The untested paths are the main motivation for the PR (silent drops that are hard to diagnose), so it's worth adding assertions for at least the "dms disabled" and "not in groupAllowFrom" cases to guard against message text or field regressions.

Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/msteams/src/monitor-handler/message-handler.authz.test.ts
Line: 200-289

Comment:
**Incomplete test coverage for new info logs**

The PR adds six new `log.info(...)` calls across six distinct drop paths, but only two of them are exercised by new tests:

| Drop path | Tested? |
|---|---|
| `"dropping dm (dms disabled)"` ||
| `"dropping dm (not allowlisted)"` | ✓ (new test) |
| `"dropping group message (not in team/channel allowlist)"` ||
| `"dropping group message (groupPolicy: disabled)"` ||
| `"dropping group message (groupPolicy: allowlist, no allowlist)"` | ✓ (new test) |
| `"dropping group message (not in groupAllowFrom)"` ||

The untested paths are the main motivation for the PR (silent drops that are hard to diagnose), so it's worth adding assertions for at least the `"dms disabled"` and `"not in groupAllowFrom"` cases to guard against message text or field regressions.

How can I resolve this? If you propose a fix, please make it concise.

@BradGroux BradGroux changed the title msteams: log inbound policy drops at info level fix(msteams): accept strict Bot Framework and Entra service tokens Mar 28, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d162d282c0

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +454 to +455
const legacyResult = await botFrameworkValidator.validateAccessToken(token, overrides);
if (legacyResult != null) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Attempt Entra validation after legacy validator errors

validate() currently wraps both issuer checks in a single try/catch, so if botFrameworkValidator.validateAccessToken(...) throws, the function returns false immediately and never evaluates the Entra validator. This causes valid Entra-signed inbound requests to be rejected whenever the legacy path errors (for example issuer/JWKS mismatch behavior surfaced as exceptions), defeating the intended dual-issuer fallback and producing 401s for legitimate traffic.

Useful? React with 👍 / 👎.

@BradGroux BradGroux merged commit dc382b0 into openclaw:main Mar 28, 2026
89 of 133 checks passed
alexcode-cc pushed a commit to alexcode-cc/clawdbot that referenced this pull request Mar 30, 2026
…penclaw#56631)

* msteams: log policy-based inbound drops at info level

* fix(msteams): validate Bot Framework and Entra service token issuers

---------

Co-authored-by: Brad Groux <[email protected]>
alexjiang1 pushed a commit to alexjiang1/openclaw that referenced this pull request Mar 31, 2026
…penclaw#56631)

* msteams: log policy-based inbound drops at info level

* fix(msteams): validate Bot Framework and Entra service token issuers

---------

Co-authored-by: Brad Groux <[email protected]>
livingghost pushed a commit to livingghost/openclaw that referenced this pull request Mar 31, 2026
…penclaw#56631)

* msteams: log policy-based inbound drops at info level

* fix(msteams): validate Bot Framework and Entra service token issuers

---------

Co-authored-by: Brad Groux <[email protected]>
pgondhi987 pushed a commit to pgondhi987/openclaw that referenced this pull request Mar 31, 2026
…penclaw#56631)

* msteams: log policy-based inbound drops at info level

* fix(msteams): validate Bot Framework and Entra service token issuers

---------

Co-authored-by: Brad Groux <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: msteams Channel integration: msteams maintainer Maintainer-authored PR size: M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

msteams plugin: three stacked bugs prevent inbound Teams webhooks on v2026.3.24

2 participants