Skip to content

[Bug] UserAuthorization refresh sends composite Copilot channel to GetToken #456

Description

@Carvalh0XYZ

Summary

UserAuthorization normalizes the initial M365 Copilot Web sign-in request to the base msteams channel, but its later refresh path sends the raw composite msteams:COPILOT channel to Bot Framework GetToken.

The initial route auth gate succeeds, then Authorization.get_token() / exchange_token() refreshes through the raw channel partition and receives HTTP 404. Teams works because its incoming channel is already msteams; the failure is specific to M365 Copilot's composite channel.

Environment

  • microsoft-agents-hosting-core==1.1.0
  • Also reproduced from current main at e24623e
  • Also present in the published 1.2.0.dev9 wheel
  • UserAuthorization with an Azure Bot OAuth connection
  • Custom engine agent invoked from M365 Copilot Web

Reproduction

  1. Configure a normal UserAuthorization handler backed by an Azure Bot OAuth connection.
  2. Register a message route with that handler in auth_handlers.
  3. Expose the agent in M365 Copilot Web so inbound activities use channelId=msteams:COPILOT.
  4. Complete silent SSO or interactive sign-in.
  5. Read or exchange the authenticated token in the route with Authorization.get_token() or Authorization.exchange_token().

Observed token-service sequence:

GetTokenOrSignInResource(channelId=msteams)          -> 200
GetToken(channelId=msteams:COPILOT)                 -> 404

The first request uses the base channel because #392 calls get_conversation_reference(force_base_channel=True) in _OAuthFlow.begin_flow(). The refresh request does not.

Root cause

The remaining path is:

Authorization.exchange_token()
  -> _UserAuthorization.get_refreshed_token()
  -> _UserAuthorization._load_flow()
       flow_state.channel_id = context.activity.channel_id
  -> _OAuthFlow.__init__()
       self._channel_id = flow_state.channel_id
  -> _OAuthFlow.get_user_token()
       UserToken.get_token(channel_id=self._channel_id)

_UserAuthorization._load_flow() records the raw activity channel in _FlowState, and _OAuthFlow reuses it for Bot Framework token operations. For Copilot Web this remains msteams:COPILOT.

Expected behavior

Bot Framework token operations should use the base msteams channel while OAuth flow storage, continuation activities, telemetry, and replies retain the original msteams:COPILOT channel.

The activity itself must not be mutated, because outbound streaming and replies need the original Copilot conversation reference.

Impact

A user can be signed in successfully at the route gate, but the handler's subsequent token read/refresh fails. The message turn then raises and M365 Copilot displays a generic failure instead of invoking the agent. Repeated attempts continue to fail because the successful base-channel token is queried through the wrong composite partition.

Proposed fix

Normalize only _OAuthFlow's operational token channel to the base segment while preserving _FlowState.channel_id unchanged:

self._channel_id = self._flow_state.channel_id.split(":", 1)[0]

This keeps flow storage and continuation semantics surface-specific while making GetToken, token exchange, and sign-out consistent with the base-channel behavior already added to begin_flow() by #392.

A regression test should assert both halves:

  • UserToken.get_token(..., channel_id="msteams")
  • flow.flow_state.channel_id == "msteams:COPILOT"

Validation

An application-level implementation of the same boundary was deployed and verified with consecutive M365 Copilot Web turns:

message_received channel=msteams:COPILOT
 token exchange succeeded
 agent invocation completed successfully
 streamed reply finalized on channel=msteams:COPILOT

The previous GetToken 404 / failed-turn signature disappeared. No tenant IDs, application IDs, tokens, internal URLs, or conversation identifiers are included here.

Related work

  • #391 described the composite-channel OAuth requirement.
  • #392 fixed the initial GetTokenOrSignInResource and encoded-state path.
  • microsoft/Agents-for-net#827 tracks the corresponding composite-channel behavior in .NET.

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugSomething isn't working

    Type

    Fields

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions