openclaw - 💡(How to fix) Fix [Bug]: msteams federated managed identity ignores FIC, leaks MI appid in outbound Bot Framework calls

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…

When channels.msteams.authType: "federated" and useManagedIdentity: true, the plugin's createManagedIdentityApp constructs a plain @azure/identity ManagedIdentityCredential and calls getToken("https://api.botframework.com/.default") on it. The token returned by IMDS carries the managed identity's own appid, not the bot app's appid — so Bot Framework rejects every outbound call with 401, and inbound DMs/mentions never get a reply. Surface symptom in the gateway log is a single bare [msteams] failed to deliver 1 of 1 message blocks warning with no underlying cause printed.

A correctly-configured federated identity credential (FIC) on the bot app registration is never consulted in this code path, because ManagedIdentityCredential does not perform a token exchange. The certificate sibling (createCertificateApp) does not have this bug because ClientCertificateCredential(tenantId, appId, ...) encodes the bot's identity in the credential itself.

Error Message

[msteams] failed to deliver 1 of 1 message blocks ← WARN [msteams] failed to deliver 1 of 1 message blocks ← WARN, no underlying cause logged The [msteams] failed to deliver 1 of 1 message blocks warning carrying no cause information also made this very hard to diagnose. A secondary ask: surface the underlying error (at minimum the HTTP status from the Bot Framework call) at WARN, not just the block-count summary. Knowing it was a 401 would have cut hours off the investigation. throw new Error(

Root Cause

A correctly-configured federated identity credential (FIC) on the bot app registration is never consulted in this code path, because ManagedIdentityCredential does not perform a token exchange. The certificate sibling (createCertificateApp) does not have this bug because ClientCertificateCredential(tenantId, appId, ...) encodes the bot's identity in the credential itself.

Fix Action

Fix / Workaround

  1. Provision an Azure VM with a system-assigned managed identity.
  2. Create a Bot Framework / Entra ID app registration for the OpenClaw Teams bot.
  3. On the bot app, add a federated identity credential that trusts the VM's MI:
    • issuer: https://login.microsoftonline.com/<tenantId>/v2.0
    • subject: the MI service principal's object ID
    • audiences: ["api://AzureADTokenExchange"]
  4. Configure OpenClaw:
    {
      "channels": {
        "msteams": {
          "enabled": true,
          "appId": "<BOT_APP_ID>",
          "tenantId": "<TENANT_ID>",
          "authType": "federated",
          "useManagedIdentity": true,
          "webhook": { "port": 3978, "path": "/api/messages" }
        }
      }
    }
  5. Install the Teams app, DM the bot from an allowlisted/paired account.
  6. Observe in the gateway log:
    [msteams] received message
    [msteams] dispatching to agent
    [msteams] failed to deliver 1 of 1 message blocks   ← WARN
    [msteams] dispatch complete
    No reply is delivered to the Teams client.
[msteams] starting provider (port 3978)
[msteams] msteams provider started on port 3978
[msteams] received message
[msteams] dispatching to agent
[msteams] failed to deliver 1 of 1 message blocks      ← WARN, no underlying cause logged
[msteams] dispatch complete

After applying the patch below (wrap MI in ClientAssertionCredential so the FIC mediates the exchange), the exact same DM produces:

Code Example

{
     "channels": {
       "msteams": {
         "enabled": true,
         "appId": "<BOT_APP_ID>",
         "tenantId": "<TENANT_ID>",
         "authType": "federated",
         "useManagedIdentity": true,
         "webhook": { "port": 3978, "path": "/api/messages" }
       }
     }
   }

---

[msteams] received message
   [msteams] dispatching to agent
   [msteams] failed to deliver 1 of 1 message blocks   ← WARN
   [msteams] dispatch complete

---

{
  "aud": "https://api.botframework.com",
  "iss": "https://sts.windows.net/<TENANT_ID>/",
  "appid": "<MI_APPID>",           // ← MI's own appid, not the bot's
  "oid":   "<MI_PRINCIPAL_ID>",
  "sub":   "<MI_PRINCIPAL_ID>",
  "tid":   "<TENANT_ID>",
  "idtyp": "app",
  "appidacr": "2"
}

---

{
  "aud":   "https://api.botframework.com",
  "appid": "<BOT_APP_ID>",          // ← bot's appid, as required
  "tid":   "<TENANT_ID>"
}

---

[msteams] starting provider (port 3978)
[msteams] msteams provider started on port 3978
[msteams] received message
[msteams] dispatching to agent
[msteams] failed to deliver 1 of 1 message blocks      ← WARN, no underlying cause logged
[msteams] dispatch complete

---

[msteams] received message
[msteams] dispatching to agent
[msteams] dispatch complete                            ← no "failed to deliver" line

---

function createManagedIdentityApp(
  creds: MSTeamsFederatedCredentials,
  sdk: MSTeamsTeamsSdk,
): MSTeamsApp {
  let credentialPromise: Promise<AzureTokenCredential> | null = null;

  const getCredential = async () => {
    if (!credentialPromise) {
      credentialPromise = loadAzureIdentity().then((az) =>
        creds.managedIdentityClientId
          ? new az.ManagedIdentityCredential(creds.managedIdentityClientId)
          : new az.ManagedIdentityCredential(),
      );
    }
    return credentialPromise;
  };
  ...
}

---

const getCredential = async () => {
  if (!credentialPromise) {
    credentialPromise = loadAzureIdentity().then((az) => {
      const miCred = creds.managedIdentityClientId
        ? new az.ManagedIdentityCredential(creds.managedIdentityClientId)
        : new az.ManagedIdentityCredential();
      return new az.ClientAssertionCredential(
        creds.tenantId,
        creds.appId,
        async () => {
          const assertion = await miCred.getToken("api://AzureADTokenExchange");
          if (!assertion?.token) {
            throw new Error(
              "Failed to acquire MI assertion for federated identity exchange.",
            );
          }
          return assertion.token;
        },
      );
    });
  }
  return credentialPromise;
};
RAW_BUFFERClick to expand / collapse

Summary

When channels.msteams.authType: "federated" and useManagedIdentity: true, the plugin's createManagedIdentityApp constructs a plain @azure/identity ManagedIdentityCredential and calls getToken("https://api.botframework.com/.default") on it. The token returned by IMDS carries the managed identity's own appid, not the bot app's appid — so Bot Framework rejects every outbound call with 401, and inbound DMs/mentions never get a reply. Surface symptom in the gateway log is a single bare [msteams] failed to deliver 1 of 1 message blocks warning with no underlying cause printed.

A correctly-configured federated identity credential (FIC) on the bot app registration is never consulted in this code path, because ManagedIdentityCredential does not perform a token exchange. The certificate sibling (createCertificateApp) does not have this bug because ClientCertificateCredential(tenantId, appId, ...) encodes the bot's identity in the credential itself.

Steps to reproduce

  1. Provision an Azure VM with a system-assigned managed identity.
  2. Create a Bot Framework / Entra ID app registration for the OpenClaw Teams bot.
  3. On the bot app, add a federated identity credential that trusts the VM's MI:
    • issuer: https://login.microsoftonline.com/<tenantId>/v2.0
    • subject: the MI service principal's object ID
    • audiences: ["api://AzureADTokenExchange"]
  4. Configure OpenClaw:
    {
      "channels": {
        "msteams": {
          "enabled": true,
          "appId": "<BOT_APP_ID>",
          "tenantId": "<TENANT_ID>",
          "authType": "federated",
          "useManagedIdentity": true,
          "webhook": { "port": 3978, "path": "/api/messages" }
        }
      }
    }
  5. Install the Teams app, DM the bot from an allowlisted/paired account.
  6. Observe in the gateway log:
    [msteams] received message
    [msteams] dispatching to agent
    [msteams] failed to deliver 1 of 1 message blocks   ← WARN
    [msteams] dispatch complete
    No reply is delivered to the Teams client.

Expected behavior

Per the docs at /channels/msteams#federated-authentication-certificate-plus-managed-identity:

A federated identity credential links the managed identity to the Entra ID app registration. At runtime, OpenClaw uses @azure/identity to acquire tokens from the Azure IMDS endpoint. The token is passed to the Teams SDK for bot authentication.

The token presented to Bot Framework should be issued for the bot app (appid == channels.msteams.appId), via the FIC exchange. Outbound replies should succeed for any sender allowed by dmPolicy / groupPolicy / allowFrom.

The certificate sibling already meets this contract — ClientCertificateCredential(tenantId, appId, ...) produces tokens with appid == channels.msteams.appId because the credential itself encodes the bot identity.

Actual behavior

The MI token reaching Bot Framework is the raw IMDS token, carrying the MI's own appid. Bot Framework rejects it because that appid is not registered as a bot.

Decoded JWT claims from the token the plugin actually sends (sensitive identifiers replaced with placeholders, all other claims verbatim):

{
  "aud": "https://api.botframework.com",
  "iss": "https://sts.windows.net/<TENANT_ID>/",
  "appid": "<MI_APPID>",           // ← MI's own appid, not the bot's
  "oid":   "<MI_PRINCIPAL_ID>",
  "sub":   "<MI_PRINCIPAL_ID>",
  "tid":   "<TENANT_ID>",
  "idtyp": "app",
  "appidacr": "2"
}

For comparison, manually performing the token exchange the plugin should be doing — i.e. asking IMDS for an assertion at api://AzureADTokenExchange and POSTing it to https://login.microsoftonline.com/<tenant>/oauth2/v2.0/token with grant_type=client_credentials, client_id=<BOT_APP_ID>, client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer — produces a token with the correct claims:

{
  "aud":   "https://api.botframework.com",
  "appid": "<BOT_APP_ID>",          // ← bot's appid, as required
  "tid":   "<TENANT_ID>"
}

So the FIC and the Entra ID side are wired correctly; the plugin just isn't using the exchange flow.

OpenClaw version

2026.5.19 (also reproducible on main — see "Additional information").

Operating system

Ubuntu 24.04 LTS on an Azure VM (Standard B-series).

Install method

npm install -g openclaw host, with @openclaw/msteams installed via openclaw plugins install into ~/.openclaw/npm/node_modules/.

Model

anthropic/claude-opus-4-7 (agentRuntime: "claude-cli").

Provider / routing chain

openclaw → anthropic (claude-cli backend) for the model side. The bug is independent of the model — it lives entirely in the msteams channel plugin's outbound auth path.

Additional provider/model setup details

Not relevant. The bug is in extensions/msteams/src/sdk.ts, createManagedIdentityApp, and only the channel plugin's outbound auth path. Reproduces with any model provider.

Logs, screenshots, and evidence

Gateway log around a failed delivery (no reply ever appears in Teams):

[msteams] starting provider (port 3978)
[msteams] msteams provider started on port 3978
[msteams] received message
[msteams] dispatching to agent
[msteams] failed to deliver 1 of 1 message blocks      ← WARN, no underlying cause logged
[msteams] dispatch complete

After applying the patch below (wrap MI in ClientAssertionCredential so the FIC mediates the exchange), the exact same DM produces:

[msteams] received message
[msteams] dispatching to agent
[msteams] dispatch complete                            ← no "failed to deliver" line

…and the reply lands in Teams.

The [msteams] failed to deliver 1 of 1 message blocks warning carrying no cause information also made this very hard to diagnose. A secondary ask: surface the underlying error (at minimum the HTTP status from the Bot Framework call) at WARN, not just the block-count summary. Knowing it was a 401 would have cut hours off the investigation.

Impact and severity

  • Affected: Every OpenClaw deployment using channels.msteams.authType: "federated" + useManagedIdentity: true on a host where the FIC pattern requires a token exchange — i.e. essentially every plain Azure VM (system-assigned or user-assigned MI). AKS workload identity may incidentally work, but only if a DefaultAzureCredential-style env was injected, which the plugin doesn't use either; haven't independently verified.
  • Severity: High. Blocks the entire passwordless MI deployment path that the docs and #40855 / #54903 explicitly promote as the production-recommended posture. The only workarounds are (a) switching to client-secret auth (defeats the purpose) or (b) patching the plugin locally.
  • Frequency: 100% deterministic. Every outbound reply on this path fails.
  • Consequence: Bot receives inbound activities, agent runs to completion (and burns model cost), reply is silently dropped. From the Teams user's perspective, the bot is paired and approved but never responds.

Additional information

Buggy code (current main): extensions/msteams/src/sdk.ts:193-206

function createManagedIdentityApp(
  creds: MSTeamsFederatedCredentials,
  sdk: MSTeamsTeamsSdk,
): MSTeamsApp {
  let credentialPromise: Promise<AzureTokenCredential> | null = null;

  const getCredential = async () => {
    if (!credentialPromise) {
      credentialPromise = loadAzureIdentity().then((az) =>
        creds.managedIdentityClientId
          ? new az.ManagedIdentityCredential(creds.managedIdentityClientId)
          : new az.ManagedIdentityCredential(),
      );
    }
    return credentialPromise;
  };
  ...
}

Proposed fix: wrap the MI in a ClientAssertionCredential so @azure/identity performs the federation exchange against the configured FIC. The credential constructor is told who to act as (the bot app), and the assertion callback supplies the MI-signed proof.

const getCredential = async () => {
  if (!credentialPromise) {
    credentialPromise = loadAzureIdentity().then((az) => {
      const miCred = creds.managedIdentityClientId
        ? new az.ManagedIdentityCredential(creds.managedIdentityClientId)
        : new az.ManagedIdentityCredential();
      return new az.ClientAssertionCredential(
        creds.tenantId,
        creds.appId,
        async () => {
          const assertion = await miCred.getToken("api://AzureADTokenExchange");
          if (!assertion?.token) {
            throw new Error(
              "Failed to acquire MI assertion for federated identity exchange.",
            );
          }
          return assertion.token;
        },
      );
    });
  }
  return credentialPromise;
};

This mirrors the shape of createCertificateApp (which already passes tenantId + appId to ClientCertificateCredential, hence its tokens have the correct appid).

ClientAssertionCredential has shipped in @azure/identity since 3.0 and is already in the plugin's transitive deps; the typing extension at the top of sdk.ts for MSTeamsAzureIdentity would need a one-line addition for the new constructor.

Verified in this deployment:

  • Manual MI → AzureADTokenExchange → bot-app token exchange (using curl against the v2.0 token endpoint with the IMDS-issued assertion) succeeds with appid == <BOT_APP_ID>.
  • The local patch above (applied to the installed plugin at ~/.openclaw/npm/node_modules/@openclaw/msteams/dist/graph-users-*.js) makes the failed to deliver log line disappear and DM replies land in Teams immediately.

Related (already-closed) feature requests that landed the buggy implementation: #40855, #54903. The implementations match the docs' described behavior, but the docs' described behavior never actually worked on plain VMs — only AKS workload identity (which has its own injected env) would coincidentally work, and only if DefaultAzureCredential were used; the plugin uses ManagedIdentityCredential directly which doesn't pick up that env either.

Happy to send a PR with the patch + a unit test against a mocked ClientAssertionCredential if a maintainer can confirm direction (drop-in ClientAssertionCredential vs. switching to DefaultAzureCredential with both certificate and MI auto-discovery).

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…

FAQ

Expected behavior

Per the docs at /channels/msteams#federated-authentication-certificate-plus-managed-identity:

A federated identity credential links the managed identity to the Entra ID app registration. At runtime, OpenClaw uses @azure/identity to acquire tokens from the Azure IMDS endpoint. The token is passed to the Teams SDK for bot authentication.

The token presented to Bot Framework should be issued for the bot app (appid == channels.msteams.appId), via the FIC exchange. Outbound replies should succeed for any sender allowed by dmPolicy / groupPolicy / allowFrom.

The certificate sibling already meets this contract — ClientCertificateCredential(tenantId, appId, ...) produces tokens with appid == channels.msteams.appId because the credential itself encodes the bot identity.

Still need to ship something?

×6

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

Back to top recommendations

TRENDING