Skip to content

Migrate MicrosoftTeams plugin from Bot Framework SDK to M365 Agents SDK#1372

Merged
Oceania2018 merged 2 commits into
SciSharp:masterfrom
byin-lessen:feature/teams-plugin
Jul 3, 2026
Merged

Migrate MicrosoftTeams plugin from Bot Framework SDK to M365 Agents SDK#1372
Oceania2018 merged 2 commits into
SciSharp:masterfrom
byin-lessen:feature/teams-plugin

Conversation

@byin-lessen

Copy link
Copy Markdown

No description provided.

Bo Yin and others added 2 commits June 29, 2026 16:52
…DK v1.6.150

Replace Microsoft.Bot.Builder.Integration.AspNet.Core (v4.22.7) with the
Microsoft 365 Agents SDK (Microsoft.Agents.* v1.6.150).

Package changes:
- Add Microsoft.Agents.Hosting.AspNetCore, Microsoft.Agents.Builder,
  Microsoft.Agents.Authentication.Msal
- Remove Microsoft.Bot.Builder.Integration.AspNet.Core

Key changes:
- All Microsoft.Bot.* namespaces replaced with Microsoft.Agents.* equivalents
- Auth registration: ConfigurationBotFrameworkAuthentication replaced with
  AddDefaultMsalAuth + AddAgent<TeamsActivityBot, TeamsAdapter>
- CloudAdapter constructor updated to new signature
  (IChannelServiceClientFactory, IActivityTaskQueue, ...)
- ActivityHandler moved to Microsoft.Agents.Builder.Compat namespace
- Activity types and models moved to Microsoft.Agents.Core.Models
- Proactive messaging: ContinueConversationAsync now takes ClaimsIdentity
  via AgentClaims.CreateIdentity instead of raw appId string
- Inbound endpoint registered via MapAgentApplicationEndpoints() in
  IBotSharpAppPlugin.Configure (route: /teams/api/messages) instead of
  a controller action, avoiding MVC application-part discovery issues
- Scoped BotSharp services resolved via CreateScope() inside turn handlers
  since IAgent is resolved from the root DI provider by the SDK
- TeamsRequestState removed; agentId now read from MicrosoftTeamsSetting
- MicrosoftTeamsSetting.AllowUnauthenticated added for local debugging
  with Agents Playground (set true in appsettings.Development.json)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@qodo-code-review

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

Migrate MicrosoftTeams plugin from Bot Framework SDK to M365 Agents SDK

✨ Enhancement ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Replaces Microsoft.Bot.Builder.Integration.AspNet.Core (v4.22.7) with three Microsoft.Agents.*
 packages (v1.6.150): Hosting.AspNetCore, Builder, and Authentication.Msal.
• Rewires DI registration: ConfigurationBotFrameworkAuthenticationAddDefaultMsalAuth +
 AddAgent with an isolated ConfigurationConnections sub-config.
• Updates TeamsAdapter constructor to the new `CloudAdapter(IChannelServiceClientFactory,
 IActivityTaskQueue, ...)` signature.
• Replaces TeamsRequestState scoped-state pattern with MicrosoftTeamsSetting.AgentId read
 directly from config, removing the need to thread agentId through the HTTP request.
• Adds per-turn user resolution: fetches sender email via IConnectorClient, looks up the BotSharp
 User by email (with 30-minute IMemoryCache), and populates HttpContext.User with claims so
 IUserIdentity resolves correctly.
• Switches proactive messaging (ContinueConversationAsync) from raw appId string to
 ClaimsIdentity via AgentClaims.CreateIdentity.
• Restricts inbound message handling to 1:1 personal chat only; drops group/channel messages with a
 warning.
Diagram

graph TD
    ABS["Azure Bot Service"] -->|"JWT-validated POST"| CTRL["TeamsMessageController"]
    CTRL --> ADAPT["TeamsAdapter\n(CloudAdapter)"]
    ADAPT -->|"ProcessAsync"| BOT["TeamsActivityBot\n(ActivityHandler)"]
    BOT -->|"IConnectorClient"| ABS
    BOT -->|"email lookup"| REPO[("IBotSharpRepository")]
    BOT -->|"IMemoryCache"| CACHE[("User Cache")]
    BOT -->|"CreateScope"| HANDLER["TeamsMessageHandler"]
    HANDLER -->|"BotSharp engine"| ENGINE["Conversation Engine"]
    NOTIF["TeamsNotificationService"] -->|"ContinueConversationAsync\n(AgentClaims identity)"| ADAPT
    NOTIF --> HANDLER

    subgraph Legend
      direction LR
      _ctrl["Controller"] ~~~ _svc(["Service"]) ~~~ _db[("Database/Cache")]
    end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Configure adapter-level unauthenticated bypass instead of stripping Authorization header
  • ➕ Cleaner, explicit opt-in via SDK configuration
  • ➕ No risk of accidentally stripping valid auth headers in other scenarios
  • ➖ Requires understanding the SDK's AllowUnauthenticated configuration surface
  • ➖ Slightly more configuration ceremony
2. Use distributed cache (IDistributedCache) instead of IMemoryCache for user resolution
  • ➕ Works correctly in multi-instance / load-balanced deployments
  • ➕ User cache survives pod restarts
  • ➖ Adds infrastructure dependency (Redis, etc.)
  • ➖ More complex serialization for User objects
  • ➖ Overkill for single-instance deployments

Recommendation: The PR's approach is sound for a production SDK migration. One area worth considering is the Authorization header removal in the controller (Request.Headers.Remove(&quot;Authorization&quot;)), which is a workaround rather than a proper configuration of the adapter's JWT validation. The M365 Agents SDK supports configuring AllowUnauthenticated at the adapter level, which would be cleaner. Additionally, the TeamsRequestState model file still exists in the codebase despite being unused — it should be deleted to avoid confusion.

Files changed (13) +246 / -80

Enhancement (1) +164 / -27
TeamsActivityBot.csAdd per-turn user resolution, claims injection, and personal-chat guard +164/-27

Add per-turn user resolution, claims injection, and personal-chat guard

• Replaces 'TeamsRequestState' with 'IHttpContextAccessor', 'IMemoryCache', and 'MicrosoftTeamsSetting'. On every turn, resolves the sender's BotSharp 'User' by fetching their email via 'IConnectorClient' (with 30-minute cache) and populates 'HttpContext.User' with claims. Adds a guard to process only 1:1 personal chat messages and drops unknown senders with a user-facing error. Resolves scoped services via 'CreateScope()' inside turn handlers.

src/Plugins/BotSharp.Plugin.MicrosoftTeams/Services/TeamsActivityBot.cs

Refactor (9) +75 / -51
MicrosoftTeamsPlugin.csRewire DI registration to M365 Agents SDK auth and adapter infrastructure +35/-18

Rewire DI registration to M365 Agents SDK auth and adapter infrastructure

• Replaces 'ConfigurationBotFrameworkAuthentication' with 'AddDefaultMsalAuth' + an explicit 'ConfigurationConnections' singleton built from an isolated in-memory sub-config. Uses 'AddAgent<TeamsActivityBot, TeamsAdapter>()' for full SDK infrastructure registration and exposes the adapter as 'IChannelAdapter' for proactive messaging. Removes the 'TeamsRequestState' scoped registration.

src/Plugins/BotSharp.Plugin.MicrosoftTeams/MicrosoftTeamsPlugin.cs

TeamsMessageController.csUpdate controller to use IAgentHttpAdapter/IAgent and strip Authorization header +12/-16

Update controller to use IAgentHttpAdapter/IAgent and strip Authorization header

• Replaces 'IBotFrameworkHttpAdapter'/'IBot' with 'IAgentHttpAdapter'/'IAgent'. Removes 'TeamsRequestState' injection. Adds 'CancellationToken' parameter to 'PostAsync' and strips the 'Authorization' header before delegating to the adapter.

src/Plugins/BotSharp.Plugin.MicrosoftTeams/Controllers/TeamsMessageController.cs

TeamsAdapter.csUpdate CloudAdapter constructor to M365 Agents SDK signature +10/-5

Update CloudAdapter constructor to M365 Agents SDK signature

• Changes the base 'CloudAdapter' constructor call from '(BotFrameworkAuthentication, ILogger)' to '(IChannelServiceClientFactory, IActivityTaskQueue, ILogger, null, null, IConfiguration)' as required by the new SDK. Updates using directives accordingly.

src/Plugins/BotSharp.Plugin.MicrosoftTeams/Services/TeamsAdapter.cs

TeamsNotificationService.csSwitch proactive ContinueConversationAsync to ClaimsIdentity via AgentClaims +9/-5

Switch proactive ContinueConversationAsync to ClaimsIdentity via AgentClaims

• Changes adapter injection from concrete 'TeamsAdapter' to 'IChannelAdapter'. Replaces the raw 'appId' string argument in 'ContinueConversationAsync' calls with a 'ClaimsIdentity' created via 'AgentClaims.CreateIdentity'.

src/Plugins/BotSharp.Plugin.MicrosoftTeams/Services/TeamsNotificationService.cs

AdaptiveCardConverter.csUpdate namespaces from Microsoft.Bot.* to Microsoft.Agents.* +4/-4

Update namespaces from Microsoft.Bot.* to Microsoft.Agents.*

• Replaces 'Microsoft.Bot.Builder' and 'Microsoft.Bot.Schema' usings with 'Microsoft.Agents.Builder' and 'Microsoft.Agents.Core.Models'. Updates the explicit 'Attachment' type reference to the new namespace.

src/Plugins/BotSharp.Plugin.MicrosoftTeams/Services/AdaptiveCardConverter.cs

IConversationReferenceStore.csUpdate ConversationReference namespace to Microsoft.Agents.Core.Models +1/-1

Update ConversationReference namespace to Microsoft.Agents.Core.Models

• Replaces 'using Microsoft.Bot.Schema' with 'using Microsoft.Agents.Core.Models' to use the M365 Agents SDK model types.

src/Plugins/BotSharp.Plugin.MicrosoftTeams/Services/IConversationReferenceStore.cs

InMemoryConversationReferenceStore.csUpdate ConversationReference namespace to Microsoft.Agents.Core.Models +1/-1

Update ConversationReference namespace to Microsoft.Agents.Core.Models

• Replaces 'using Microsoft.Bot.Schema' with 'using Microsoft.Agents.Core.Models' to align with the SDK migration.

src/Plugins/BotSharp.Plugin.MicrosoftTeams/Services/InMemoryConversationReferenceStore.cs

TeamsMessageHandler.csUpdate Activity model namespace to Microsoft.Agents.Core.Models +1/-1

Update Activity model namespace to Microsoft.Agents.Core.Models

• Replaces 'using Microsoft.Bot.Schema' with 'using Microsoft.Agents.Core.Models'.

src/Plugins/BotSharp.Plugin.MicrosoftTeams/Services/TeamsMessageHandler.cs

Using.csAdd global usings for repository and user model abstractions +2/-0

Add global usings for repository and user model abstractions

• Adds 'BotSharp.Abstraction.Repositories' and 'BotSharp.Abstraction.Users.Models' as global usings to support the new 'IBotSharpRepository' and 'User' references in 'TeamsActivityBot'.

src/Plugins/BotSharp.Plugin.MicrosoftTeams/Using.cs

Other (3) +7 / -2
Directory.Packages.propsReplace Bot Framework SDK package with three M365 Agents SDK packages +3/-1

Replace Bot Framework SDK package with three M365 Agents SDK packages

• Removes 'Microsoft.Bot.Builder.Integration.AspNet.Core' (v4.22.7) and adds 'Microsoft.Agents.Hosting.AspNetCore', 'Microsoft.Agents.Builder', and 'Microsoft.Agents.Authentication.Msal' all at v1.6.150.

Directory.Packages.props

BotSharp.Plugin.MicrosoftTeams.csprojUpdate project references to M365 Agents SDK packages +3/-1

Update project references to M365 Agents SDK packages

• Swaps the single 'Microsoft.Bot.Builder.Integration.AspNet.Core' reference for the three new 'Microsoft.Agents.*' package references matching the central version catalog.

src/Plugins/BotSharp.Plugin.MicrosoftTeams/BotSharp.Plugin.MicrosoftTeams.csproj

MicrosoftTeamsSetting.csAdd BotName configuration property +1/-0

Add BotName configuration property

• Adds a 'BotName' string property (default '"Test Bot"') to 'MicrosoftTeamsSetting' for use in bot identity scenarios.

src/Plugins/BotSharp.Plugin.MicrosoftTeams/Settings/MicrosoftTeamsSetting.cs

@qodo-code-review

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (4) 📘 Rule violations (3) 📜 Skill insights (0)

Grey Divider


Action required

1. Caching mutable User instance 📘 Rule violation ⛨ Security
Description
ResolveUserAsync stores and returns a mutable User domain model directly from IMemoryCache,
allowing downstream mutation to leak across turns/requests. This can cause cross-request state
corruption (and the cached object includes sensitive fields like Password/Salt).
Code

src/Plugins/BotSharp.Plugin.MicrosoftTeams/Services/TeamsActivityBot.cs[R229-248]

+    private async Task<User?> ResolveUserAsync(string aadId, ITurnContext turnContext, CancellationToken cancellationToken)
+    {
+        var key = CacheKey(aadId);
+
+        // IMemoryCache.TryGetValue returns false for missing keys AND for cached nulls stored
+        // as object, so we wrap with a sentinel to distinguish "not cached" from "cached null".
+        if (_cache.TryGetValue(key, out User? cached))
+        {
+            return cached;
+        }
+
+        var email = await GetSenderEmailAsync(turnContext, cancellationToken);
+        var user = string.IsNullOrEmpty(email) ? null : await FindUserByEmailAsync(email, cancellationToken);
+
+        _cache.Set(key, user, new MemoryCacheEntryOptions
+        {
+            SlidingExpiration = CacheTtl
+        });
+
+        return user;
Evidence
The checklist requires defensive copies when returning cached/shared mutable domain models.
ResolveUserAsync caches and returns User directly, and User is a mutable model with settable
properties, so cached instances can be mutated and reused across turns.

src/Plugins/BotSharp.Plugin.MicrosoftTeams/Services/TeamsActivityBot.cs[229-249]
src/Infrastructure/BotSharp.Abstraction/Users/Models/User.cs[5-33]
Best Practice: Learned patterns

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`TeamsActivityBot.ResolveUserAsync` caches and returns a mutable `User` object directly. This violates the requirement to avoid exposing shared/cached mutable domain models by reference.

## Issue Context
`User` is mutable (many settable properties) and includes sensitive fields (e.g., `Password`, `Salt`). Returning a cached instance can leak mutations across requests/turns.

## Fix Focus Areas
- src/Plugins/BotSharp.Plugin.MicrosoftTeams/Services/TeamsActivityBot.cs[229-249]
- src/Infrastructure/BotSharp.Abstraction/Users/Models/User.cs[5-37]

## Implementation notes
- Prefer caching a purpose-built immutable DTO (e.g., `CachedUserIdentity` with only `Id`, `UserName`, `Email`, `FirstName`, `LastName`, `Role`).
- Alternatively, if you must cache `User`, store and return a defensive copy (and copy nested collections like `Permissions`), ensuring sensitive fields are not retained if not needed.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Missing AgentId fails silently 📘 Rule violation ☼ Reliability
Description
When MicrosoftTeamsSetting.AgentId is empty, message handling returns without a predictable
error/response, effectively dropping requests. This silent fallback makes misconfiguration hard to
detect and violates the fail-fast boundary validation requirement.
Code

src/Plugins/BotSharp.Plugin.MicrosoftTeams/Services/TeamsActivityBot.cs[R110-114]

+        var agentId = _setting.AgentId;
        if (string.IsNullOrEmpty(agentId))
        {
-            _logger.LogWarning("Teams: no agentId on the request route, dropping message.");
            return;
        }
-
-        var userId = GetUserId(turnContext.Activity);
-
-        // Show the typing indicator while the agent is thinking.
-        await turnContext.SendActivityAsync(new Activity { Type = ActivityTypes.Typing }, cancellationToken);
-
-        var handler = _services.GetRequiredService<TeamsMessageHandler>();
-        await handler.Handle(userId, agentId, text,
Evidence
The checklist requires null/empty validation at boundaries with predictable failures. The new code
checks for empty agentId from configuration and simply returns, causing silent dropping of inbound
messages instead of a clear error/fallback.

src/Plugins/BotSharp.Plugin.MicrosoftTeams/Services/TeamsActivityBot.cs[105-117]
Best Practice: Learned patterns

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`TeamsActivityBot` silently returns when `_setting.AgentId` is empty, dropping inbound messages without a predictable error path.

## Issue Context
`AgentId` is a required boundary/config input for routing messages. Silent no-op behavior masks configuration issues.

## Fix Focus Areas
- src/Plugins/BotSharp.Plugin.MicrosoftTeams/Services/TeamsActivityBot.cs[110-114]

## Implementation notes
- Prefer validating `MicrosoftTeams:AgentId` at startup (e.g., in plugin DI registration) and throw a clear exception / log error so misconfiguration is detected early.
- If runtime validation is required, log at `Error`/`Warning` and send a clear message back to the user (or set an appropriate response) instead of returning silently.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Authorization header stripped 🐞 Bug ⛨ Security
Description
TeamsMessageController.PostAsync removes the Authorization header before calling the adapter, which
can disable/break the Bot Service JWT validation and make the inbound /teams/messages endpoint
accept unauthenticated/forged activities or reject legitimate ones. The code also documents a
MicrosoftTeams:AllowUnauthenticated switch, but no such setting exists, so this behavior is
always-on.
Code

src/Plugins/BotSharp.Plugin.MicrosoftTeams/Controllers/TeamsMessageController.cs[R35-38]

+    public async Task PostAsync([FromRoute] string agentId, CancellationToken cancellationToken)
    {
-        _requestState.AgentId = agentId;
-        await _adapter.ProcessAsync(Request, Response, _bot);
+        Request.Headers.Remove("Authorization");
+        await _adapter.ProcessAsync(Request, Response, _bot, cancellationToken);
Evidence
The inbound controller explicitly deletes the Authorization header right before handing the request
to the adapter, while also claiming the adapter performs JWT validation and referencing a config
switch that is not implemented anywhere in settings.

src/Plugins/BotSharp.Plugin.MicrosoftTeams/Controllers/TeamsMessageController.cs[28-39]
src/Plugins/BotSharp.Plugin.MicrosoftTeams/Settings/MicrosoftTeamsSetting.cs[7-35]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`TeamsMessageController.PostAsync` unconditionally removes the inbound `Authorization` header and then calls `ProcessAsync`. This prevents the adapter from seeing the Bot Service JWT, breaking or bypassing authentication.

### Issue Context
The controller comment suggests using `MicrosoftTeams:AllowUnauthenticated=true` for local dev, but `MicrosoftTeamsSetting` has no such property, and the code does not conditionally apply this behavior.

### Fix Focus Areas
- src/Plugins/BotSharp.Plugin.MicrosoftTeams/Controllers/TeamsMessageController.cs[28-39]
- src/Plugins/BotSharp.Plugin.MicrosoftTeams/Settings/MicrosoftTeamsSetting.cs[7-35]

### Proposed fix
1. Remove `Request.Headers.Remove("Authorization")`.
2. If local-dev unauth is desired, add `bool AllowUnauthenticated { get; set; }` to `MicrosoftTeamsSetting` and only bypass auth when explicitly enabled (and ideally only in Development environment).
3. Keep the adapter as the single place that validates Bot Service JWTs.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. Route agentId ignored 📘 Rule violation ≡ Correctness
Description
The inbound Teams message endpoint accepts an {agentId} route value but neither validates nor uses
it, so requests silently default to MicrosoftTeamsSetting.AgentId regardless of the URL. This
makes the route parameter misleading, breaks per-route agent routing, and violates boundary input
validation requirements by masking client/configuration errors.
Code

src/Plugins/BotSharp.Plugin.MicrosoftTeams/Controllers/TeamsMessageController.cs[R34-39]

    [HttpPost("/teams/messages/{agentId}")]
-    public async Task PostAsync([FromRoute] string agentId)
+    public async Task PostAsync([FromRoute] string agentId, CancellationToken cancellationToken)
    {
-        _requestState.AgentId = agentId;
-        await _adapter.ProcessAsync(Request, Response, _bot);
+        Request.Headers.Remove("Authorization");
+        await _adapter.ProcessAsync(Request, Response, _bot, cancellationToken);
    }
Evidence
The controller action still exposes an {agentId} route parameter, yet the described behavior is
that agent selection is resolved exclusively from configuration (MicrosoftTeamsSetting.AgentId)
rather than the route value, meaning unexpected or invalid route inputs are never detected or
handled. This contradicts the checklist requirement that boundary inputs (like route parameters) be
validated instead of silently defaulted, and the presence of a TeamsRequestState model that
documents a prior mechanism for propagating the routed agentId indicates the intended design was
to carry the route-based agent selection into the bot’s per-request scope rather than ignoring it.

src/Plugins/BotSharp.Plugin.MicrosoftTeams/Controllers/TeamsMessageController.cs[34-39]
src/Plugins/BotSharp.Plugin.MicrosoftTeams/Controllers/TeamsMessageController.cs[33-39]
src/Plugins/BotSharp.Plugin.MicrosoftTeams/Services/TeamsActivityBot.cs[105-116]
src/Plugins/BotSharp.Plugin.MicrosoftTeams/Models/TeamsRequestState.cs[3-10]
Best Practice: Learned patterns

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`TeamsMessageController.PostAsync` accepts a route parameter `{agentId}` but does not validate or use it, and inbound bot processing instead always uses `MicrosoftTeamsSetting.AgentId`, causing silent default behavior and making the route misleading.

## Issue Context
At system boundaries (routes), required inputs should be validated and handled predictably (e.g., `BadRequest`, `NotFound`, or an explicit documented fallback) rather than being ignored. The repository still contains `TeamsRequestState`, which reflects a prior/expected design of propagating the routed `agentId` into the bot’s turn scope for request-scoped routing.

## Fix Focus Areas
- src/Plugins/BotSharp.Plugin.MicrosoftTeams/Controllers/TeamsMessageController.cs[33-39]
- src/Plugins/BotSharp.Plugin.MicrosoftTeams/Services/TeamsActivityBot.cs[105-116]
- src/Plugins/BotSharp.Plugin.MicrosoftTeams/Models/TeamsRequestState.cs[3-10]

## Implementation notes
Choose one coherent approach and make the behavior explicit:

- **Option A (restore route-based routing + validation):**
 1. Re-introduce/register a scoped `TeamsRequestState` in DI.
 2. In `TeamsMessageController.PostAsync`, validate `agentId` (e.g., non-empty) and set `requestState.AgentId = agentId`.
 3. In `TeamsActivityBot`, prefer `requestState.AgentId` for agent selection (optionally fallback to `_setting.AgentId` only if empty), and consider rejecting unknown/mismatched agentIds with `BadRequest`/`NotFound` depending on desired semantics.

- **Option B (single-agent endpoint):**
 1. Remove `{agentId}` from the route and method signature (change to `/teams/messages`).
 2. Keep using `_setting.AgentId` and update any related documentation/configuration so there is no misleading, unused route input.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Empty userId possible 🐞 Bug ≡ Correctness
Description
TeamsActivityBot.GetUserId now returns only From.AadObjectId (or empty string), so if AadObjectId is
missing the bot will treat senders as unknown and/or pass an empty conversation id into the BotSharp
conversation engine. Using an empty conversation id risks conversation collisions and incorrect
message routing.
Code

src/Plugins/BotSharp.Plugin.MicrosoftTeams/Services/TeamsActivityBot.cs[R277-279]

    private static string GetUserId(IActivity activity)
        => activity.From?.AadObjectId
-           ?? activity.From?.Id
-           ?? activity.Conversation?.Id
           ?? string.Empty;
Evidence
GetUserId can return an empty string. That value is then used as the conversation id and routing key
in TeamsMessageHandler, so an empty id can merge unrelated senders into the same conversation
context.

src/Plugins/BotSharp.Plugin.MicrosoftTeams/Services/TeamsActivityBot.cs[277-279]
src/Plugins/BotSharp.Plugin.MicrosoftTeams/Services/TeamsMessageHandler.cs[35-39]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`GetUserId` returns `activity.From?.AadObjectId ?? string.Empty`. If AadObjectId is absent for any activity type/environment, downstream logic receives an empty `userId`, which is used as the BotSharp conversation id.

### Issue Context
`TeamsMessageHandler` uses `userId` as both routing key and conversation id.

### Fix Focus Areas
- src/Plugins/BotSharp.Plugin.MicrosoftTeams/Services/TeamsActivityBot.cs[277-279]
- src/Plugins/BotSharp.Plugin.MicrosoftTeams/Services/TeamsMessageHandler.cs[35-39]

### Proposed fix
1. Restore a stable fallback chain, e.g. `AadObjectId ?? From.Id ?? Conversation.Id`, and ensure the final result is non-empty.
2. If no stable identifier is available, explicitly log and return a user-friendly error instead of proceeding with an empty id.
3. Keep `TeamsNotifyRequest.UserId` semantics aligned with whatever identifier you persist in the conversation reference store.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. AppType/MSI support dropped 🐞 Bug ≡ Correctness
Description
MicrosoftTeamsPlugin hardcodes MSAL auth to ClientSecret using settings.AppPassword, while
MicrosoftTeamsSetting still documents UserAssignedMSI and an empty secret. Deployments using MSI (or
any non-client-secret auth mode) will stop working or misconfigure authentication.
Code

src/Plugins/BotSharp.Plugin.MicrosoftTeams/MicrosoftTeamsPlugin.cs[R27-39]

+        // Build sub-config with Connections section required by Microsoft.Agents.Authentication.Msal.
+        // Maps our MicrosoftTeamsSetting keys to the format ConfigurationConnections expects.
        var authConfig = new ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string?>
            {
-                ["MicrosoftAppType"] = settings.AppType,
-                ["MicrosoftAppId"] = settings.AppId,
-                ["MicrosoftAppPassword"] = settings.AppPassword,
-                ["MicrosoftAppTenantId"] = settings.TenantId
+                ["Connections:BotServiceConnection:Assembly"] = "Microsoft.Agents.Authentication.Msal",
+                ["Connections:BotServiceConnection:Type"] = "MsalAuth",
+                ["Connections:BotServiceConnection:Settings:AuthType"] = "ClientSecret",
+                ["Connections:BotServiceConnection:Settings:ClientId"] = settings.AppId,
+                ["Connections:BotServiceConnection:Settings:ClientSecret"] = settings.AppPassword,
+                ["Connections:BotServiceConnection:Settings:TenantId"] = settings.TenantId,
+                ["Connections:BotServiceConnection:Settings:Scopes:0"] = "https://api.botframework.com/.default",
            })
Evidence
The plugin explicitly configures MSAL to use client-secret auth, while the settings contract still
claims to support MSI and empty secrets, creating an incompatibility for existing deployments
relying on AppType.

src/Plugins/BotSharp.Plugin.MicrosoftTeams/MicrosoftTeamsPlugin.cs[27-39]
src/Plugins/BotSharp.Plugin.MicrosoftTeams/Settings/MicrosoftTeamsSetting.cs[9-28]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The new Agents SDK auth configuration is always created with `AuthType=ClientSecret` and always sets `ClientSecret=settings.AppPassword`, ignoring `MicrosoftTeamsSetting.AppType`.

### Issue Context
`MicrosoftTeamsSetting` still exposes `AppType` with values like `UserAssignedMSI`, and its comment explicitly says the secret can be empty in that mode.

### Fix Focus Areas
- src/Plugins/BotSharp.Plugin.MicrosoftTeams/MicrosoftTeamsPlugin.cs[27-39]
- src/Plugins/BotSharp.Plugin.MicrosoftTeams/Settings/MicrosoftTeamsSetting.cs[9-28]

### Proposed fix
1. Either implement `AppType`-based configuration mapping for the Agents SDK (ClientSecret vs Managed Identity / SingleTenant), or
2. If MSI is not supported in this plugin anymore, remove/rename `AppType` and update documentation, and fail fast at startup when `AppType != ClientSecret` to avoid silent misconfiguration.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (1)
7. HttpContext null dereference 🐞 Bug ☼ Reliability
Description
TeamsActivityBot.SetHttpContextUser force-dereferences _httpContextAccessor.HttpContext, which will
crash if a turn executes without an active HttpContext (e.g., queued/background turn execution).
This makes message processing fragile and can cause intermittent 500s.
Code

src/Plugins/BotSharp.Plugin.MicrosoftTeams/Services/TeamsActivityBot.cs[R255-268]

+    private void SetHttpContextUser(User user)
+    {
+        var claims = new[]
+        {
+            new Claim(ClaimTypes.NameIdentifier, user.Id),
+            new Claim(ClaimTypes.Name, user.UserName ?? string.Empty),
+            new Claim(ClaimTypes.Email, user.Email ?? string.Empty),
+            new Claim(ClaimTypes.GivenName, user.FirstName ?? string.Empty),
+            new Claim(ClaimTypes.Surname, user.LastName ?? string.Empty),
+            new Claim(ClaimTypes.Role, user.Role ?? string.Empty),
+        };
+        var identity = new ClaimsIdentity(claims, authenticationType: "Teams");
+        _httpContextAccessor.HttpContext!.User = new System.Security.Principal.GenericPrincipal(identity, roles: null);
+    }
Evidence
The code uses the null-forgiving operator on HttpContext, so a null context will crash. The adapter
wiring includes a background task queue type, indicating turns may be processed outside the normal
HTTP request context depending on configuration.

src/Plugins/BotSharp.Plugin.MicrosoftTeams/Services/TeamsActivityBot.cs[255-268]
src/Plugins/BotSharp.Plugin.MicrosoftTeams/Services/TeamsAdapter.cs[15-21]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`SetHttpContextUser` assigns `_httpContextAccessor.HttpContext!.User = ...` with no null check. If `HttpContext` is absent, this throws and fails the turn.

### Issue Context
The adapter is constructed with an `IActivityTaskQueue` (background queue), which commonly implies some work may execute outside the request thread/context.

### Fix Focus Areas
- src/Plugins/BotSharp.Plugin.MicrosoftTeams/Services/TeamsActivityBot.cs[255-268]
- src/Plugins/BotSharp.Plugin.MicrosoftTeams/Services/TeamsAdapter.cs[15-21]

### Proposed fix
1. Change to:
  - `var http = _httpContextAccessor.HttpContext; if (http == null) { _logger.LogDebug(...); return; }`
  - then set `http.User = new ClaimsPrincipal(identity)` (or keep GenericPrincipal if desired).
2. Keep downstream code resilient when no HttpContext is present (avoid assuming IUserIdentity is available in such execution paths).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment on lines +229 to +248
private async Task<User?> ResolveUserAsync(string aadId, ITurnContext turnContext, CancellationToken cancellationToken)
{
var key = CacheKey(aadId);

// IMemoryCache.TryGetValue returns false for missing keys AND for cached nulls stored
// as object, so we wrap with a sentinel to distinguish "not cached" from "cached null".
if (_cache.TryGetValue(key, out User? cached))
{
return cached;
}

var email = await GetSenderEmailAsync(turnContext, cancellationToken);
var user = string.IsNullOrEmpty(email) ? null : await FindUserByEmailAsync(email, cancellationToken);

_cache.Set(key, user, new MemoryCacheEntryOptions
{
SlidingExpiration = CacheTtl
});

return user;

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.

Action required

1. Caching mutable user instance 📘 Rule violation ⛨ Security

ResolveUserAsync stores and returns a mutable User domain model directly from IMemoryCache,
allowing downstream mutation to leak across turns/requests. This can cause cross-request state
corruption (and the cached object includes sensitive fields like Password/Salt).
Agent Prompt
## Issue description
`TeamsActivityBot.ResolveUserAsync` caches and returns a mutable `User` object directly. This violates the requirement to avoid exposing shared/cached mutable domain models by reference.

## Issue Context
`User` is mutable (many settable properties) and includes sensitive fields (e.g., `Password`, `Salt`). Returning a cached instance can leak mutations across requests/turns.

## Fix Focus Areas
- src/Plugins/BotSharp.Plugin.MicrosoftTeams/Services/TeamsActivityBot.cs[229-249]
- src/Infrastructure/BotSharp.Abstraction/Users/Models/User.cs[5-37]

## Implementation notes
- Prefer caching a purpose-built immutable DTO (e.g., `CachedUserIdentity` with only `Id`, `UserName`, `Email`, `FirstName`, `LastName`, `Role`).
- Alternatively, if you must cache `User`, store and return a defensive copy (and copy nested collections like `Permissions`), ensuring sensitive fields are not retained if not needed.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +110 to 114
var agentId = _setting.AgentId;
if (string.IsNullOrEmpty(agentId))
{
_logger.LogWarning("Teams: no agentId on the request route, dropping message.");
return;
}

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.

Action required

2. Missing agentid fails silently 📘 Rule violation ☼ Reliability

When MicrosoftTeamsSetting.AgentId is empty, message handling returns without a predictable
error/response, effectively dropping requests. This silent fallback makes misconfiguration hard to
detect and violates the fail-fast boundary validation requirement.
Agent Prompt
## Issue description
`TeamsActivityBot` silently returns when `_setting.AgentId` is empty, dropping inbound messages without a predictable error path.

## Issue Context
`AgentId` is a required boundary/config input for routing messages. Silent no-op behavior masks configuration issues.

## Fix Focus Areas
- src/Plugins/BotSharp.Plugin.MicrosoftTeams/Services/TeamsActivityBot.cs[110-114]

## Implementation notes
- Prefer validating `MicrosoftTeams:AgentId` at startup (e.g., in plugin DI registration) and throw a clear exception / log error so misconfiguration is detected early.
- If runtime validation is required, log at `Error`/`Warning` and send a clear message back to the user (or set an appropriate response) instead of returning silently.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines 34 to 39
[HttpPost("/teams/messages/{agentId}")]
public async Task PostAsync([FromRoute] string agentId)
public async Task PostAsync([FromRoute] string agentId, CancellationToken cancellationToken)
{
_requestState.AgentId = agentId;
await _adapter.ProcessAsync(Request, Response, _bot);
Request.Headers.Remove("Authorization");
await _adapter.ProcessAsync(Request, Response, _bot, cancellationToken);
}

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.

Remediation recommended

3. Route agentid ignored 📘 Rule violation ≡ Correctness

The inbound Teams message endpoint accepts an {agentId} route value but neither validates nor uses
it, so requests silently default to MicrosoftTeamsSetting.AgentId regardless of the URL. This
makes the route parameter misleading, breaks per-route agent routing, and violates boundary input
validation requirements by masking client/configuration errors.
Agent Prompt
## Issue description
`TeamsMessageController.PostAsync` accepts a route parameter `{agentId}` but does not validate or use it, and inbound bot processing instead always uses `MicrosoftTeamsSetting.AgentId`, causing silent default behavior and making the route misleading.

## Issue Context
At system boundaries (routes), required inputs should be validated and handled predictably (e.g., `BadRequest`, `NotFound`, or an explicit documented fallback) rather than being ignored. The repository still contains `TeamsRequestState`, which reflects a prior/expected design of propagating the routed `agentId` into the bot’s turn scope for request-scoped routing.

## Fix Focus Areas
- src/Plugins/BotSharp.Plugin.MicrosoftTeams/Controllers/TeamsMessageController.cs[33-39]
- src/Plugins/BotSharp.Plugin.MicrosoftTeams/Services/TeamsActivityBot.cs[105-116]
- src/Plugins/BotSharp.Plugin.MicrosoftTeams/Models/TeamsRequestState.cs[3-10]

## Implementation notes
Choose one coherent approach and make the behavior explicit:

- **Option A (restore route-based routing + validation):**
  1. Re-introduce/register a scoped `TeamsRequestState` in DI.
  2. In `TeamsMessageController.PostAsync`, validate `agentId` (e.g., non-empty) and set `requestState.AgentId = agentId`.
  3. In `TeamsActivityBot`, prefer `requestState.AgentId` for agent selection (optionally fallback to `_setting.AgentId` only if empty), and consider rejecting unknown/mismatched agentIds with `BadRequest`/`NotFound` depending on desired semantics.

- **Option B (single-agent endpoint):**
  1. Remove `{agentId}` from the route and method signature (change to `/teams/messages`).
  2. Keep using `_setting.AgentId` and update any related documentation/configuration so there is no misleading, unused route input.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +35 to +38
public async Task PostAsync([FromRoute] string agentId, CancellationToken cancellationToken)
{
_requestState.AgentId = agentId;
await _adapter.ProcessAsync(Request, Response, _bot);
Request.Headers.Remove("Authorization");
await _adapter.ProcessAsync(Request, Response, _bot, cancellationToken);

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.

Action required

4. Authorization header stripped 🐞 Bug ⛨ Security

TeamsMessageController.PostAsync removes the Authorization header before calling the adapter, which
can disable/break the Bot Service JWT validation and make the inbound /teams/messages endpoint
accept unauthenticated/forged activities or reject legitimate ones. The code also documents a
MicrosoftTeams:AllowUnauthenticated switch, but no such setting exists, so this behavior is
always-on.
Agent Prompt
### Issue description
`TeamsMessageController.PostAsync` unconditionally removes the inbound `Authorization` header and then calls `ProcessAsync`. This prevents the adapter from seeing the Bot Service JWT, breaking or bypassing authentication.

### Issue Context
The controller comment suggests using `MicrosoftTeams:AllowUnauthenticated=true` for local dev, but `MicrosoftTeamsSetting` has no such property, and the code does not conditionally apply this behavior.

### Fix Focus Areas
- src/Plugins/BotSharp.Plugin.MicrosoftTeams/Controllers/TeamsMessageController.cs[28-39]
- src/Plugins/BotSharp.Plugin.MicrosoftTeams/Settings/MicrosoftTeamsSetting.cs[7-35]

### Proposed fix
1. Remove `Request.Headers.Remove("Authorization")`.
2. If local-dev unauth is desired, add `bool AllowUnauthenticated { get; set; }` to `MicrosoftTeamsSetting` and only bypass auth when explicitly enabled (and ideally only in Development environment).
3. Keep the adapter as the single place that validates Bot Service JWTs.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines 277 to 279
private static string GetUserId(IActivity activity)
=> activity.From?.AadObjectId
?? activity.From?.Id
?? activity.Conversation?.Id
?? string.Empty;

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.

Remediation recommended

5. Empty userid possible 🐞 Bug ≡ Correctness

TeamsActivityBot.GetUserId now returns only From.AadObjectId (or empty string), so if AadObjectId is
missing the bot will treat senders as unknown and/or pass an empty conversation id into the BotSharp
conversation engine. Using an empty conversation id risks conversation collisions and incorrect
message routing.
Agent Prompt
### Issue description
`GetUserId` returns `activity.From?.AadObjectId ?? string.Empty`. If AadObjectId is absent for any activity type/environment, downstream logic receives an empty `userId`, which is used as the BotSharp conversation id.

### Issue Context
`TeamsMessageHandler` uses `userId` as both routing key and conversation id.

### Fix Focus Areas
- src/Plugins/BotSharp.Plugin.MicrosoftTeams/Services/TeamsActivityBot.cs[277-279]
- src/Plugins/BotSharp.Plugin.MicrosoftTeams/Services/TeamsMessageHandler.cs[35-39]

### Proposed fix
1. Restore a stable fallback chain, e.g. `AadObjectId ?? From.Id ?? Conversation.Id`, and ensure the final result is non-empty.
2. If no stable identifier is available, explicitly log and return a user-friendly error instead of proceeding with an empty id.
3. Keep `TeamsNotifyRequest.UserId` semantics aligned with whatever identifier you persist in the conversation reference store.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +27 to 39
// Build sub-config with Connections section required by Microsoft.Agents.Authentication.Msal.
// Maps our MicrosoftTeamsSetting keys to the format ConfigurationConnections expects.
var authConfig = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["MicrosoftAppType"] = settings.AppType,
["MicrosoftAppId"] = settings.AppId,
["MicrosoftAppPassword"] = settings.AppPassword,
["MicrosoftAppTenantId"] = settings.TenantId
["Connections:BotServiceConnection:Assembly"] = "Microsoft.Agents.Authentication.Msal",
["Connections:BotServiceConnection:Type"] = "MsalAuth",
["Connections:BotServiceConnection:Settings:AuthType"] = "ClientSecret",
["Connections:BotServiceConnection:Settings:ClientId"] = settings.AppId,
["Connections:BotServiceConnection:Settings:ClientSecret"] = settings.AppPassword,
["Connections:BotServiceConnection:Settings:TenantId"] = settings.TenantId,
["Connections:BotServiceConnection:Settings:Scopes:0"] = "https://api.botframework.com/.default",
})

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.

Remediation recommended

6. Apptype/msi support dropped 🐞 Bug ≡ Correctness

MicrosoftTeamsPlugin hardcodes MSAL auth to ClientSecret using settings.AppPassword, while
MicrosoftTeamsSetting still documents UserAssignedMSI and an empty secret. Deployments using MSI (or
any non-client-secret auth mode) will stop working or misconfigure authentication.
Agent Prompt
### Issue description
The new Agents SDK auth configuration is always created with `AuthType=ClientSecret` and always sets `ClientSecret=settings.AppPassword`, ignoring `MicrosoftTeamsSetting.AppType`.

### Issue Context
`MicrosoftTeamsSetting` still exposes `AppType` with values like `UserAssignedMSI`, and its comment explicitly says the secret can be empty in that mode.

### Fix Focus Areas
- src/Plugins/BotSharp.Plugin.MicrosoftTeams/MicrosoftTeamsPlugin.cs[27-39]
- src/Plugins/BotSharp.Plugin.MicrosoftTeams/Settings/MicrosoftTeamsSetting.cs[9-28]

### Proposed fix
1. Either implement `AppType`-based configuration mapping for the Agents SDK (ClientSecret vs Managed Identity / SingleTenant), or
2. If MSI is not supported in this plugin anymore, remove/rename `AppType` and update documentation, and fail fast at startup when `AppType != ClientSecret` to avoid silent misconfiguration.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +255 to +268
private void SetHttpContextUser(User user)
{
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, user.Id),
new Claim(ClaimTypes.Name, user.UserName ?? string.Empty),
new Claim(ClaimTypes.Email, user.Email ?? string.Empty),
new Claim(ClaimTypes.GivenName, user.FirstName ?? string.Empty),
new Claim(ClaimTypes.Surname, user.LastName ?? string.Empty),
new Claim(ClaimTypes.Role, user.Role ?? string.Empty),
};
var identity = new ClaimsIdentity(claims, authenticationType: "Teams");
_httpContextAccessor.HttpContext!.User = new System.Security.Principal.GenericPrincipal(identity, roles: null);
}

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.

Remediation recommended

7. Httpcontext null dereference 🐞 Bug ☼ Reliability

TeamsActivityBot.SetHttpContextUser force-dereferences _httpContextAccessor.HttpContext, which will
crash if a turn executes without an active HttpContext (e.g., queued/background turn execution).
This makes message processing fragile and can cause intermittent 500s.
Agent Prompt
### Issue description
`SetHttpContextUser` assigns `_httpContextAccessor.HttpContext!.User = ...` with no null check. If `HttpContext` is absent, this throws and fails the turn.

### Issue Context
The adapter is constructed with an `IActivityTaskQueue` (background queue), which commonly implies some work may execute outside the request thread/context.

### Fix Focus Areas
- src/Plugins/BotSharp.Plugin.MicrosoftTeams/Services/TeamsActivityBot.cs[255-268]
- src/Plugins/BotSharp.Plugin.MicrosoftTeams/Services/TeamsAdapter.cs[15-21]

### Proposed fix
1. Change to:
   - `var http = _httpContextAccessor.HttpContext; if (http == null) { _logger.LogDebug(...); return; }`
   - then set `http.User = new ClaimsPrincipal(identity)` (or keep GenericPrincipal if desired).
2. Keep downstream code resilient when no HttpContext is present (avoid assuming IUserIdentity is available in such execution paths).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@Oceania2018 Oceania2018 merged commit bc58b41 into SciSharp:master Jul 3, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants