openclaw - 💡(How to fix) Fix msteams: every inbound Bot Connector POST returns 401 after 4.24/4.25 runtime-deps move (CJS interop) [1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#73026Fetched 2026-04-28 06:28:24
View on GitHub
Comments
1
Participants
2
Timeline
2
Reactions
0
Timeline (top)
closed ×1commented ×1

After upgrading to 2026.4.25 (and likely 2026.4.24 — the release that moved bundled deps to external runtime-deps), the bundled msteams plugin returns HTTP 401 to every inbound POST /api/messages from Microsoft Bot Connector. The bot appears "offline" — Teams users get no replies. Outbound sends still work because they use a different code path.

The 401 is silent: the only log entry is "JWT validation failed" at debug level, which is below the default info and never surfaces in normal logs.

Error Message

  1. Every POST → 401 {"error":"Unauthorized"} from openclaw's middleware.

Suggestion: surface JWT validation errors at warn

The current path logs JWT validation failed and JWT validation error: ... at debug. A signature/audience/issuer rejection is a security-relevant event, and a TypeError from inside jwt.verify is an internal error that absolutely shouldn't have been silent. Suggest:

  • JWT validation failed (audience/issuer/signature mismatch) → warn with redacted token metadata (kid, iss, audience hash) — bounded, low-cardinality.
  • Internal TypeError / unexpected throws → error with the exception name + message, no token contents.

Root Cause

dist/extensions/msteams/graph-users-*.js, in loadBotFrameworkJwtDeps() (around line 950):

botFrameworkJwtDepsPromise ??= Promise.all([
    import("jsonwebtoken"),
    import("jwks-rsa"),
]).then(([jwt, { JwksClient }]) => ({ jwt, JwksClient }));

await import("jsonwebtoken") on [email protected] returns a module-namespace object whose named-export proxy is incomplete. Node's cjs-module-lexer only detects decode (and a literal key called "module.exports") as a top-level named export — verify, sign, JsonWebTokenError, NotBeforeError, TokenExpiredError are only available on m.default.

Verified directly:

$ cd ~/.openclaw/plugin-runtime-deps/openclaw-2026.4.25-*/
$ node -e "import('jsonwebtoken').then(m => {
    console.log('keys:', Object.keys(m));
    console.log('verify:', typeof m.verify, '| default.verify:', typeof m.default?.verify);
  })"
keys: [ 'decode', 'default', 'module.exports' ]
verify: undefined | default.verify: function

So when createBotFrameworkJwtValidator calls jwt.verify(token, publicKey, opts) (around line 1016), it throws TypeError: jwt.verify is not a function. The catch { return false; } swallows it; the middleware logs "JWT validation failed" at debug; the request 401s.

jwt.decode works because it happens to be one of the named exports the lexer picked up — that's why the kid/iss/audience pre-checks pass and the failure looks like signature verification rather than missing function.

Fix Action

Fix / Workaround

Verified end-to-end on a production install (2026.4.25 on macOS 15, Node 25.6.1, [email protected], [email protected]). After the patch, the same real Bot Connector JWT that 401'd pre-patch returns 200 and the activity routes through to the agent handler.

Happy to provide a redacted token sample, ngrok request capture, or test the patch on additional builds.

Code Example

botFrameworkJwtDepsPromise ??= Promise.all([
    import("jsonwebtoken"),
    import("jwks-rsa"),
]).then(([jwt, { JwksClient }]) => ({ jwt, JwksClient }));

---

$ cd ~/.openclaw/plugin-runtime-deps/openclaw-2026.4.25-*/
$ node -e "import('jsonwebtoken').then(m => {
    console.log('keys:', Object.keys(m));
    console.log('verify:', typeof m.verify, '| default.verify:', typeof m.default?.verify);
  })"
keys: [ 'decode', 'default', 'module.exports' ]
verify: undefined | default.verify: function

---

botFrameworkJwtDepsPromise ??= Promise.all([
    import("./jsonwebtoken-CeFsl65-.js"),
    import("./src-kg7BhsLW.js"),
]).then(([jwt, { JwksClient }]) => ({ jwt, JwksClient }));

---

const { jwt, JwksClient } = await loadBotFrameworkJwtDeps();
//   ^- jwt.verify === undefined, throws

// vs:
import jwtCjs from "jsonwebtoken";  // default import, gives module.exports
jwtCjs.verify(token, pubkey, { ... })  // → succeeds

---

botFrameworkJwtDepsPromise ??= Promise.all([
    import("jsonwebtoken"),
    import("jwks-rsa"),
]).then(([jwtMod, jwksMod]) => {
    const jwt = jwtMod.default ?? jwtMod;
    const JwksClient = jwksMod.JwksClient ?? jwksMod.default?.JwksClient;
    return { jwt, JwksClient };
});
RAW_BUFFERClick to expand / collapse

msteams: every inbound Bot Connector POST returns 401 after 4.24/4.25 runtime-deps move (CJS interop)

Summary

After upgrading to 2026.4.25 (and likely 2026.4.24 — the release that moved bundled deps to external runtime-deps), the bundled msteams plugin returns HTTP 401 to every inbound POST /api/messages from Microsoft Bot Connector. The bot appears "offline" — Teams users get no replies. Outbound sends still work because they use a different code path.

The 401 is silent: the only log entry is "JWT validation failed" at debug level, which is below the default info and never surfaces in normal logs.

Repro

  1. Install [email protected] (pnpm add -g [email protected] or npm i -g [email protected]).
  2. Configure channels.msteams with valid appId, tenantId, appPassword for a Bot Channels Registration that was working on <= 2026.4.23. (No config change is needed — the same config that worked pre-upgrade fails post-upgrade.)
  3. Send a Teams DM to the bot, or watch ngrok/tunnel inspector for inbound Bot Connector POSTs.
  4. Every POST → 401 {"error":"Unauthorized"} from openclaw's middleware.

Root cause

dist/extensions/msteams/graph-users-*.js, in loadBotFrameworkJwtDeps() (around line 950):

botFrameworkJwtDepsPromise ??= Promise.all([
    import("jsonwebtoken"),
    import("jwks-rsa"),
]).then(([jwt, { JwksClient }]) => ({ jwt, JwksClient }));

await import("jsonwebtoken") on [email protected] returns a module-namespace object whose named-export proxy is incomplete. Node's cjs-module-lexer only detects decode (and a literal key called "module.exports") as a top-level named export — verify, sign, JsonWebTokenError, NotBeforeError, TokenExpiredError are only available on m.default.

Verified directly:

$ cd ~/.openclaw/plugin-runtime-deps/openclaw-2026.4.25-*/
$ node -e "import('jsonwebtoken').then(m => {
    console.log('keys:', Object.keys(m));
    console.log('verify:', typeof m.verify, '| default.verify:', typeof m.default?.verify);
  })"
keys: [ 'decode', 'default', 'module.exports' ]
verify: undefined | default.verify: function

So when createBotFrameworkJwtValidator calls jwt.verify(token, publicKey, opts) (around line 1016), it throws TypeError: jwt.verify is not a function. The catch { return false; } swallows it; the middleware logs "JWT validation failed" at debug; the request 401s.

jwt.decode works because it happens to be one of the named exports the lexer picked up — that's why the kid/iss/audience pre-checks pass and the failure looks like signature verification rather than missing function.

Why this only broke in 4.24/4.25

<=2026.4.23 bundled jsonwebtoken and jwks-rsa directly into the openclaw dist via a tooling-specific import path:

botFrameworkJwtDepsPromise ??= Promise.all([
    import("./jsonwebtoken-CeFsl65-.js"),
    import("./src-kg7BhsLW.js"),
]).then(([jwt, { JwksClient }]) => ({ jwt, JwksClient }));

That bundled artifact exposed jwt.verify correctly. The runtime-deps refactor switched to bare-specifier dynamic imports without unwrapping the CJS default, exposing the lexer's blind spot.

Confirmation: standalone validator works once default is unwrapped

Reproduced the validator logic against a real Bot Connector JWT captured from ngrok inspector:

const { jwt, JwksClient } = await loadBotFrameworkJwtDeps();
//   ^- jwt.verify === undefined, throws

// vs:
import jwtCjs from "jsonwebtoken";  // default import, gives module.exports
jwtCjs.verify(token, pubkey, { ... })  // → succeeds

Same token, same JWKS endpoint, same creds. Only difference is how jwt is obtained.

Proposed fix

Unwrap default for both deps in loadBotFrameworkJwtDeps():

botFrameworkJwtDepsPromise ??= Promise.all([
    import("jsonwebtoken"),
    import("jwks-rsa"),
]).then(([jwtMod, jwksMod]) => {
    const jwt = jwtMod.default ?? jwtMod;
    const JwksClient = jwksMod.JwksClient ?? jwksMod.default?.JwksClient;
    return { jwt, JwksClient };
});

Verified end-to-end on a production install (2026.4.25 on macOS 15, Node 25.6.1, [email protected], [email protected]). After the patch, the same real Bot Connector JWT that 401'd pre-patch returns 200 and the activity routes through to the agent handler.

Suggestion: surface JWT validation errors at warn

The current path logs JWT validation failed and JWT validation error: ... at debug. A signature/audience/issuer rejection is a security-relevant event, and a TypeError from inside jwt.verify is an internal error that absolutely shouldn't have been silent. Suggest:

  • JWT validation failed (audience/issuer/signature mismatch) → warn with redacted token metadata (kid, iss, audience hash) — bounded, low-cardinality.
  • Internal TypeError / unexpected throws → error with the exception name + message, no token contents.

That alone would have surfaced this regression on day one instead of after multi-hour debugging by every affected operator.

Other plugins likely affected

Any bundled plugin that uses bare-specifier dynamic imports of CJS packages where the named-export proxy is incomplete. Worth grepping for Promise.all([import( across dist/extensions/ and verifying each m.<fn> is real.

Environment

  • openclaw 2026.4.25 (commit aa36ee6) installed via pnpm add -g
  • Node v25.6.1 (also reproduced on v24.13.1)
  • macOS 15.x (Darwin 24.6.0), arm64
  • jsonwebtoken 9.0.3, jwks-rsa 4.0.1 (both from ~/.openclaw/plugin-runtime-deps/openclaw-2026.4.25-*/node_modules/)
  • Microsoft Bot Channels Registration with MultiTenant app, no SSO

Happy to provide a redacted token sample, ngrok request capture, or test the patch on additional builds.

extent analysis

TL;DR

The most likely fix is to unwrap the default export for both jsonwebtoken and jwks-rsa dependencies in the loadBotFrameworkJwtDeps() function.

Guidance

  1. Verify the issue: Confirm that the problem occurs with the specified versions of openclaw, Node, jsonwebtoken, and jwks-rsa.
  2. Apply the proposed fix: Update the loadBotFrameworkJwtDeps() function to unwrap the default export for both dependencies, as shown in the proposed fix.
  3. Test the fix: Verify that the fix resolves the issue by sending a Teams DM to the bot or inspecting inbound Bot Connector POSTs.
  4. Surface JWT validation errors: Consider logging JWT validation failed and JWT validation error at the warn level to improve error visibility.

Example

The proposed fix can be applied by updating the loadBotFrameworkJwtDeps() function as follows:

botFrameworkJwtDepsPromise ??= Promise.all([
    import("jsonwebtoken"),
    import("jwks-rsa"),
]).then(([jwtMod, jwksMod]) => {
    const jwt = jwtMod.default ?? jwtMod;
    const JwksClient = jwksMod.JwksClient ?? jwksMod.default?.JwksClient;
    return { jwt, JwksClient };
});

Notes

This fix assumes that the issue is caused by the incomplete named-export proxy in [email protected]. If the issue persists after applying the fix, further investigation may be necessary.

Recommendation

Apply the proposed workaround by updating the loadBotFrameworkJwtDeps() function to unwrap the default export for both dependencies. This should resolve the issue and allow the bot to process inbound messages correctly.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - 💡(How to fix) Fix msteams: every inbound Bot Connector POST returns 401 after 4.24/4.25 runtime-deps move (CJS interop) [1 comments, 2 participants]