From 1c276aebb282d68656be6b91e0264f31b2bd8853 Mon Sep 17 00:00:00 2001 From: Rajan Date: Thu, 29 Jan 2026 17:31:48 -0500 Subject: [PATCH 1/3] Fix UnauthorizedAccessException with skipAuth (#274) When using `builder.AddTeams(skipAuth: true)`, the app was throwing UnauthorizedAccessException from ExtractToken() even though the authorization policy was correctly bypassed. - Make ActivityEvent.Token nullable instead of required - Update ExtractToken() to return null when no Authorization header - Add null-conditional access to Token properties in App.cs - Add tests for skipAuth scenario Co-Authored-By: Claude Opus 4.5 --- Libraries/Microsoft.Teams.Apps/App.cs | 6 +-- .../Events/ActivityEvent.cs | 2 +- .../AspNetCorePlugin.cs | 8 ++- .../AspNetCorePluginTests.cs | 54 +++++++++++++++++++ 4 files changed, 64 insertions(+), 6 deletions(-) diff --git a/Libraries/Microsoft.Teams.Apps/App.cs b/Libraries/Microsoft.Teams.Apps/App.cs index b79c664b..08fffd67 100644 --- a/Libraries/Microsoft.Teams.Apps/App.cs +++ b/Libraries/Microsoft.Teams.Apps/App.cs @@ -354,7 +354,7 @@ private async Task Process(ISenderPlugin sender, ActivityEvent @event, var reference = new ConversationReference() { - ServiceUrl = @event.Activity.ServiceUrl ?? @event.Token.ServiceUrl, + ServiceUrl = @event.Activity.ServiceUrl ?? @event.Token?.ServiceUrl, ChannelId = @event.Activity.ChannelId, Bot = @event.Activity.Recipient, User = @event.Activity.From, @@ -380,8 +380,8 @@ private async Task Process(ISenderPlugin sender, ActivityEvent @event, var stream = sender.CreateStream(reference, cancellationToken); var context = new Context(sender, stream) { - AppId = @event.Token.AppId ?? Id ?? string.Empty, - TenantId = @event.Token.TenantId ?? string.Empty, + AppId = @event.Token?.AppId ?? Id ?? string.Empty, + TenantId = @event.Token?.TenantId ?? string.Empty, Log = Logger.Child(path), Storage = Storage, Api = api, diff --git a/Libraries/Microsoft.Teams.Apps/Events/ActivityEvent.cs b/Libraries/Microsoft.Teams.Apps/Events/ActivityEvent.cs index 3612dbc8..7f8365bb 100644 --- a/Libraries/Microsoft.Teams.Apps/Events/ActivityEvent.cs +++ b/Libraries/Microsoft.Teams.Apps/Events/ActivityEvent.cs @@ -9,7 +9,7 @@ namespace Microsoft.Teams.Apps.Events; public class ActivityEvent : Event { - public required IToken Token { get; set; } + public IToken? Token { get; set; } public required IActivity Activity { get; set; } public IServiceProvider? Services { get; set; } public IDictionary? Extra { get; set; } diff --git a/Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/AspNetCorePlugin.cs b/Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/AspNetCorePlugin.cs index b5113bc0..233e05c7 100644 --- a/Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/AspNetCorePlugin.cs +++ b/Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/AspNetCorePlugin.cs @@ -226,9 +226,13 @@ await Events( } } - public JsonWebToken ExtractToken(HttpRequest httpRequest) + public JsonWebToken? ExtractToken(HttpRequest httpRequest) { - var authHeader = httpRequest.Headers.Authorization.FirstOrDefault() ?? throw new UnauthorizedAccessException(); + var authHeader = httpRequest.Headers.Authorization.FirstOrDefault(); + if (string.IsNullOrEmpty(authHeader)) + { + return null; + } return new JsonWebToken(authHeader.Replace("Bearer ", "")); } diff --git a/Tests/Microsoft.Teams.Plugins.AspNetCore.Tests/AspNetCorePluginTests.cs b/Tests/Microsoft.Teams.Plugins.AspNetCore.Tests/AspNetCorePluginTests.cs index a8370c4d..71879e48 100644 --- a/Tests/Microsoft.Teams.Plugins.AspNetCore.Tests/AspNetCorePluginTests.cs +++ b/Tests/Microsoft.Teams.Plugins.AspNetCore.Tests/AspNetCorePluginTests.cs @@ -47,6 +47,18 @@ private static DefaultHttpContext CreateHttpContext(IActivity activity, string b return ctx; } + private static DefaultHttpContext CreateHttpContextWithoutAuth(IActivity activity) + { + var ctx = new DefaultHttpContext(); + ctx.TraceIdentifier = Guid.NewGuid().ToString(); + // No Authorization header + var json = JsonSerializer.Serialize(activity, new JsonSerializerOptions { DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull }); + var bytes = Encoding.UTF8.GetBytes(json); + ctx.Request.Body = new MemoryStream(bytes); + ctx.Request.ContentLength = bytes.Length; + return ctx; + } + private static MessageActivity CreateMessageActivity() { return new MessageActivity("hi") @@ -196,4 +208,46 @@ public async Task Test_Do_Core_ReturnsResponseAndLogs() Assert.Same(response, res); logger.Verify(l => l.Debug(It.IsAny()), Times.AtLeastOnce); } + + [Fact] + public void Test_ExtractToken_ReturnsNull_WhenNoAuthHeader() + { + // Arrange + var plugin = CreatePlugin(); + var ctx = CreateHttpContextWithoutAuth(CreateMessageActivity()); + + // Act + var token = plugin.ExtractToken(ctx.Request); + + // Assert + Assert.Null(token); + } + + [Fact] + public async Task Test_Do_Http_WorksWithoutAuthHeader() + { + // Arrange - simulates skipAuth scenario where no Authorization header is present + var activity = CreateMessageActivity(); + var coreResponse = new Response(HttpStatusCode.OK, new { ok = true }); + EventFunction events = (plugin, name, payload, ct) => + { + if (name == "activity") + { + var activityEvent = (ActivityEvent)payload; + Assert.Null(activityEvent.Token); // Token should be null when no auth header + return Task.FromResult(coreResponse); + } + return Task.FromResult(null); + }; + + var plugin = CreatePlugin(new Mock(), events); + var ctx = CreateHttpContextWithoutAuth(activity); + + // Act + var result = await plugin.Do(ctx); + + // Assert + var jsonResult = Assert.IsType>(result); + Assert.Equal(200, jsonResult.StatusCode); + } } \ No newline at end of file From 953fa507bc6252988e69a2ac2ad7cace9e60ab3b Mon Sep 17 00:00:00 2001 From: Rajan Date: Fri, 30 Jan 2026 15:21:33 -0500 Subject: [PATCH 2/3] Add AnonymousToken and fix serviceUrl normalization - Create AnonymousToken class for skipAuth scenarios (matches Python/TypeScript behavior) - Add NormalizeServiceUrl helper to ensure trailing slash on serviceUrl - Fix 404 errors when AgentsPlayground sends serviceUrl without trailing slash - Update tests to verify AnonymousToken is used when no auth header Manually tested with AgentsPlayground - bot responses work correctly. Co-Authored-By: Claude Opus 4.5 --- .../Auth/AnonymousToken.cs | 37 +++++++++++++++++++ .../Clients/ActivityClient.cs | 8 ++-- .../Microsoft.Teams.Api/Clients/ApiClient.cs | 8 ++-- .../Microsoft.Teams.Api/Clients/Client.cs | 3 ++ .../Clients/ConversationClient.cs | 8 ++-- .../Clients/MeetingClient.cs | 8 ++-- .../Clients/MemberClient.cs | 8 ++-- .../Microsoft.Teams.Api/Clients/TeamClient.cs | 8 ++-- .../AspNetCorePlugin.cs | 6 ++- .../AspNetCorePluginTests.cs | 5 ++- 10 files changed, 73 insertions(+), 26 deletions(-) create mode 100644 Libraries/Microsoft.Teams.Api/Auth/AnonymousToken.cs diff --git a/Libraries/Microsoft.Teams.Api/Auth/AnonymousToken.cs b/Libraries/Microsoft.Teams.Api/Auth/AnonymousToken.cs new file mode 100644 index 00000000..3c4fbd4a --- /dev/null +++ b/Libraries/Microsoft.Teams.Api/Auth/AnonymousToken.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Microsoft.Teams.Api.Auth; + +/// +/// A fallback token used when no authentication is provided (e.g., skipAuth mode). +/// Mirrors the behavior of Python and TypeScript SDKs. +/// +public class AnonymousToken : IToken +{ + public string? AppId => string.Empty; + + public string? AppDisplayName => string.Empty; + + public string? TenantId => string.Empty; + + public string ServiceUrl { get; } + + public CallerType From => CallerType.Azure; + + public string FromId => string.Empty; + + public DateTime? Expiration => null; + + public bool IsExpired => false; + + public IEnumerable Scopes => []; + + public AnonymousToken(string serviceUrl) + { + // Ensure serviceUrl has trailing slash for consistency + ServiceUrl = serviceUrl.EndsWith('/') ? serviceUrl : serviceUrl + '/'; + } + + public override string ToString() => string.Empty; +} diff --git a/Libraries/Microsoft.Teams.Api/Clients/ActivityClient.cs b/Libraries/Microsoft.Teams.Api/Clients/ActivityClient.cs index b8cd2a58..21434a8b 100644 --- a/Libraries/Microsoft.Teams.Api/Clients/ActivityClient.cs +++ b/Libraries/Microsoft.Teams.Api/Clients/ActivityClient.cs @@ -14,22 +14,22 @@ public class ActivityClient : Client public ActivityClient(string serviceUrl, CancellationToken cancellationToken = default) : base(cancellationToken) { - ServiceUrl = serviceUrl; + ServiceUrl = NormalizeServiceUrl(serviceUrl); } public ActivityClient(string serviceUrl, IHttpClient client, CancellationToken cancellationToken = default) : base(client, cancellationToken) { - ServiceUrl = serviceUrl; + ServiceUrl = NormalizeServiceUrl(serviceUrl); } public ActivityClient(string serviceUrl, IHttpClientOptions options, CancellationToken cancellationToken = default) : base(options, cancellationToken) { - ServiceUrl = serviceUrl; + ServiceUrl = NormalizeServiceUrl(serviceUrl); } public ActivityClient(string serviceUrl, IHttpClientFactory factory, CancellationToken cancellationToken = default) : base(factory, cancellationToken) { - ServiceUrl = serviceUrl; + ServiceUrl = NormalizeServiceUrl(serviceUrl); } public async Task CreateAsync(string conversationId, IActivity activity, bool isTargeted = false) diff --git a/Libraries/Microsoft.Teams.Api/Clients/ApiClient.cs b/Libraries/Microsoft.Teams.Api/Clients/ApiClient.cs index f4aeae83..2876150a 100644 --- a/Libraries/Microsoft.Teams.Api/Clients/ApiClient.cs +++ b/Libraries/Microsoft.Teams.Api/Clients/ApiClient.cs @@ -16,7 +16,7 @@ public class ApiClient : Client public ApiClient(string serviceUrl, CancellationToken cancellationToken = default) : base(cancellationToken) { - ServiceUrl = serviceUrl; + ServiceUrl = NormalizeServiceUrl(serviceUrl); Bots = new BotClient(_http, cancellationToken); Conversations = new ConversationClient(serviceUrl, _http, cancellationToken); Users = new UserClient(_http, cancellationToken); @@ -26,7 +26,7 @@ public ApiClient(string serviceUrl, CancellationToken cancellationToken = defaul public ApiClient(string serviceUrl, IHttpClient client, CancellationToken cancellationToken = default) : base(client, cancellationToken) { - ServiceUrl = serviceUrl; + ServiceUrl = NormalizeServiceUrl(serviceUrl); Bots = new BotClient(_http, cancellationToken); Conversations = new ConversationClient(serviceUrl, _http, cancellationToken); Users = new UserClient(_http, cancellationToken); @@ -36,7 +36,7 @@ public ApiClient(string serviceUrl, IHttpClient client, CancellationToken cancel public ApiClient(string serviceUrl, IHttpClientOptions options, CancellationToken cancellationToken = default) : base(options, cancellationToken) { - ServiceUrl = serviceUrl; + ServiceUrl = NormalizeServiceUrl(serviceUrl); Bots = new BotClient(_http, cancellationToken); Conversations = new ConversationClient(serviceUrl, _http, cancellationToken); Users = new UserClient(_http, cancellationToken); @@ -46,7 +46,7 @@ public ApiClient(string serviceUrl, IHttpClientOptions options, CancellationToke public ApiClient(string serviceUrl, IHttpClientFactory factory, CancellationToken cancellationToken = default) : base(factory, cancellationToken) { - ServiceUrl = serviceUrl; + ServiceUrl = NormalizeServiceUrl(serviceUrl); Bots = new BotClient(_http, cancellationToken); Conversations = new ConversationClient(serviceUrl, _http, cancellationToken); Users = new UserClient(_http, cancellationToken); diff --git a/Libraries/Microsoft.Teams.Api/Clients/Client.cs b/Libraries/Microsoft.Teams.Api/Clients/Client.cs index 4c145577..659da9ea 100644 --- a/Libraries/Microsoft.Teams.Api/Clients/Client.cs +++ b/Libraries/Microsoft.Teams.Api/Clients/Client.cs @@ -10,6 +10,9 @@ public abstract class Client protected IHttpClient _http; protected CancellationToken _cancellationToken; + protected static string NormalizeServiceUrl(string serviceUrl) => + serviceUrl.EndsWith('/') ? serviceUrl : serviceUrl + '/'; + public Client(CancellationToken cancellationToken = default) { _http = new Common.Http.HttpClient(); diff --git a/Libraries/Microsoft.Teams.Api/Clients/ConversationClient.cs b/Libraries/Microsoft.Teams.Api/Clients/ConversationClient.cs index 8523faae..08fb2cfe 100644 --- a/Libraries/Microsoft.Teams.Api/Clients/ConversationClient.cs +++ b/Libraries/Microsoft.Teams.Api/Clients/ConversationClient.cs @@ -16,28 +16,28 @@ public class ConversationClient : Client public ConversationClient(string serviceUrl, CancellationToken cancellationToken = default) : base(cancellationToken) { - ServiceUrl = serviceUrl; + ServiceUrl = NormalizeServiceUrl(serviceUrl); Activities = new ActivityClient(serviceUrl, _http, cancellationToken); Members = new MemberClient(serviceUrl, _http, cancellationToken); } public ConversationClient(string serviceUrl, IHttpClient client, CancellationToken cancellationToken = default) : base(client, cancellationToken) { - ServiceUrl = serviceUrl; + ServiceUrl = NormalizeServiceUrl(serviceUrl); Activities = new ActivityClient(serviceUrl, _http, cancellationToken); Members = new MemberClient(serviceUrl, _http, cancellationToken); } public ConversationClient(string serviceUrl, IHttpClientOptions options, CancellationToken cancellationToken = default) : base(options, cancellationToken) { - ServiceUrl = serviceUrl; + ServiceUrl = NormalizeServiceUrl(serviceUrl); Activities = new ActivityClient(serviceUrl, _http, cancellationToken); Members = new MemberClient(serviceUrl, _http, cancellationToken); } public ConversationClient(string serviceUrl, IHttpClientFactory factory, CancellationToken cancellationToken = default) : base(factory, cancellationToken) { - ServiceUrl = serviceUrl; + ServiceUrl = NormalizeServiceUrl(serviceUrl); Activities = new ActivityClient(serviceUrl, _http, cancellationToken); Members = new MemberClient(serviceUrl, _http, cancellationToken); } diff --git a/Libraries/Microsoft.Teams.Api/Clients/MeetingClient.cs b/Libraries/Microsoft.Teams.Api/Clients/MeetingClient.cs index cd14260a..23eef179 100644 --- a/Libraries/Microsoft.Teams.Api/Clients/MeetingClient.cs +++ b/Libraries/Microsoft.Teams.Api/Clients/MeetingClient.cs @@ -14,22 +14,22 @@ public class MeetingClient : Client public MeetingClient(string serviceUrl, CancellationToken cancellationToken = default) : base(cancellationToken) { - ServiceUrl = serviceUrl; + ServiceUrl = NormalizeServiceUrl(serviceUrl); } public MeetingClient(string serviceUrl, IHttpClient client, CancellationToken cancellationToken = default) : base(client, cancellationToken) { - ServiceUrl = serviceUrl; + ServiceUrl = NormalizeServiceUrl(serviceUrl); } public MeetingClient(string serviceUrl, IHttpClientOptions options, CancellationToken cancellationToken = default) : base(options, cancellationToken) { - ServiceUrl = serviceUrl; + ServiceUrl = NormalizeServiceUrl(serviceUrl); } public MeetingClient(string serviceUrl, IHttpClientFactory factory, CancellationToken cancellationToken = default) : base(factory, cancellationToken) { - ServiceUrl = serviceUrl; + ServiceUrl = NormalizeServiceUrl(serviceUrl); } public async Task GetByIdAsync(string id) diff --git a/Libraries/Microsoft.Teams.Api/Clients/MemberClient.cs b/Libraries/Microsoft.Teams.Api/Clients/MemberClient.cs index d69c96a2..974cb34a 100644 --- a/Libraries/Microsoft.Teams.Api/Clients/MemberClient.cs +++ b/Libraries/Microsoft.Teams.Api/Clients/MemberClient.cs @@ -11,22 +11,22 @@ public class MemberClient : Client public MemberClient(string serviceUrl, CancellationToken cancellationToken = default) : base(cancellationToken) { - ServiceUrl = serviceUrl; + ServiceUrl = NormalizeServiceUrl(serviceUrl); } public MemberClient(string serviceUrl, IHttpClient client, CancellationToken cancellationToken = default) : base(client, cancellationToken) { - ServiceUrl = serviceUrl; + ServiceUrl = NormalizeServiceUrl(serviceUrl); } public MemberClient(string serviceUrl, IHttpClientOptions options, CancellationToken cancellationToken = default) : base(options, cancellationToken) { - ServiceUrl = serviceUrl; + ServiceUrl = NormalizeServiceUrl(serviceUrl); } public MemberClient(string serviceUrl, IHttpClientFactory factory, CancellationToken cancellationToken = default) : base(factory, cancellationToken) { - ServiceUrl = serviceUrl; + ServiceUrl = NormalizeServiceUrl(serviceUrl); } public async Task> GetAsync(string conversationId) diff --git a/Libraries/Microsoft.Teams.Api/Clients/TeamClient.cs b/Libraries/Microsoft.Teams.Api/Clients/TeamClient.cs index 4ee05622..55c9c704 100644 --- a/Libraries/Microsoft.Teams.Api/Clients/TeamClient.cs +++ b/Libraries/Microsoft.Teams.Api/Clients/TeamClient.cs @@ -11,22 +11,22 @@ public class TeamClient : Client public TeamClient(string serviceUrl, CancellationToken cancellationToken = default) : base(cancellationToken) { - ServiceUrl = serviceUrl; + ServiceUrl = NormalizeServiceUrl(serviceUrl); } public TeamClient(string serviceUrl, IHttpClient client, CancellationToken cancellationToken = default) : base(client, cancellationToken) { - ServiceUrl = serviceUrl; + ServiceUrl = NormalizeServiceUrl(serviceUrl); } public TeamClient(string serviceUrl, IHttpClientOptions options, CancellationToken cancellationToken = default) : base(options, cancellationToken) { - ServiceUrl = serviceUrl; + ServiceUrl = NormalizeServiceUrl(serviceUrl); } public TeamClient(string serviceUrl, IHttpClientFactory factory, CancellationToken cancellationToken = default) : base(factory, cancellationToken) { - ServiceUrl = serviceUrl; + ServiceUrl = NormalizeServiceUrl(serviceUrl); } public async Task GetByIdAsync(string id) diff --git a/Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/AspNetCorePlugin.cs b/Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/AspNetCorePlugin.cs index 233e05c7..64c41593 100644 --- a/Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/AspNetCorePlugin.cs +++ b/Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/AspNetCorePlugin.cs @@ -175,6 +175,10 @@ public async Task Do(HttpContext httpContext, CancellationToken cancell return Results.BadRequest("Missing activity"); } + // If no token was extracted, create an anonymous token with serviceUrl from the activity + // This matches Python/TypeScript SDK behavior for skipAuth scenarios + IToken resolvedToken = (IToken?)token ?? new AnonymousToken(activity.ServiceUrl ?? string.Empty); + var data = new Dictionary { ["Request.TraceId"] = httpContext.TraceIdentifier @@ -191,7 +195,7 @@ public async Task Do(HttpContext httpContext, CancellationToken cancell var res = await Do(new ActivityEvent() { - Token = token, + Token = resolvedToken, Activity = activity, Extra = data, Services = httpContext.RequestServices diff --git a/Tests/Microsoft.Teams.Plugins.AspNetCore.Tests/AspNetCorePluginTests.cs b/Tests/Microsoft.Teams.Plugins.AspNetCore.Tests/AspNetCorePluginTests.cs index 71879e48..91f15f24 100644 --- a/Tests/Microsoft.Teams.Plugins.AspNetCore.Tests/AspNetCorePluginTests.cs +++ b/Tests/Microsoft.Teams.Plugins.AspNetCore.Tests/AspNetCorePluginTests.cs @@ -234,7 +234,10 @@ public async Task Test_Do_Http_WorksWithoutAuthHeader() if (name == "activity") { var activityEvent = (ActivityEvent)payload; - Assert.Null(activityEvent.Token); // Token should be null when no auth header + // Token should be an AnonymousToken when no auth header (matches Python/TypeScript behavior) + Assert.NotNull(activityEvent.Token); + Assert.IsType(activityEvent.Token); + Assert.Equal(string.Empty, activityEvent.Token.AppId); return Task.FromResult(coreResponse); } return Task.FromResult(null); From 0eb2ae0e0bbe956d2159876271214b7444043843 Mon Sep 17 00:00:00 2001 From: Rajan Date: Fri, 30 Jan 2026 15:41:17 -0500 Subject: [PATCH 3/3] Address Copilot review feedback: nested clients use normalized ServiceUrl - ApiClient: Pass normalized ServiceUrl property (not serviceUrl param) to nested clients - ConversationClient: Pass normalized ServiceUrl property to ActivityClient and MemberClient - App.cs: Add fallback to empty string when both Activity.ServiceUrl and Token.ServiceUrl are null - AspNetCorePlugin: Use default serviceUrl when activity.ServiceUrl is null for AnonymousToken - Update ApiClientTests to expect trailing slash in normalized ServiceUrl Co-Authored-By: Claude Opus 4.5 --- .../Microsoft.Teams.Api/Clients/ApiClient.cs | 24 +++++++++---------- .../Clients/ConversationClient.cs | 16 ++++++------- Libraries/Microsoft.Teams.Apps/App.cs | 2 +- .../AspNetCorePlugin.cs | 4 ++-- .../Clients/ApiClientTests.cs | 6 +++-- 5 files changed, 27 insertions(+), 25 deletions(-) diff --git a/Libraries/Microsoft.Teams.Api/Clients/ApiClient.cs b/Libraries/Microsoft.Teams.Api/Clients/ApiClient.cs index 2876150a..a11a05ee 100644 --- a/Libraries/Microsoft.Teams.Api/Clients/ApiClient.cs +++ b/Libraries/Microsoft.Teams.Api/Clients/ApiClient.cs @@ -18,40 +18,40 @@ public ApiClient(string serviceUrl, CancellationToken cancellationToken = defaul { ServiceUrl = NormalizeServiceUrl(serviceUrl); Bots = new BotClient(_http, cancellationToken); - Conversations = new ConversationClient(serviceUrl, _http, cancellationToken); + Conversations = new ConversationClient(ServiceUrl, _http, cancellationToken); Users = new UserClient(_http, cancellationToken); - Teams = new TeamClient(serviceUrl, _http, cancellationToken); - Meetings = new MeetingClient(serviceUrl, _http, cancellationToken); + Teams = new TeamClient(ServiceUrl, _http, cancellationToken); + Meetings = new MeetingClient(ServiceUrl, _http, cancellationToken); } public ApiClient(string serviceUrl, IHttpClient client, CancellationToken cancellationToken = default) : base(client, cancellationToken) { ServiceUrl = NormalizeServiceUrl(serviceUrl); Bots = new BotClient(_http, cancellationToken); - Conversations = new ConversationClient(serviceUrl, _http, cancellationToken); + Conversations = new ConversationClient(ServiceUrl, _http, cancellationToken); Users = new UserClient(_http, cancellationToken); - Teams = new TeamClient(serviceUrl, _http, cancellationToken); - Meetings = new MeetingClient(serviceUrl, _http, cancellationToken); + Teams = new TeamClient(ServiceUrl, _http, cancellationToken); + Meetings = new MeetingClient(ServiceUrl, _http, cancellationToken); } public ApiClient(string serviceUrl, IHttpClientOptions options, CancellationToken cancellationToken = default) : base(options, cancellationToken) { ServiceUrl = NormalizeServiceUrl(serviceUrl); Bots = new BotClient(_http, cancellationToken); - Conversations = new ConversationClient(serviceUrl, _http, cancellationToken); + Conversations = new ConversationClient(ServiceUrl, _http, cancellationToken); Users = new UserClient(_http, cancellationToken); - Teams = new TeamClient(serviceUrl, _http, cancellationToken); - Meetings = new MeetingClient(serviceUrl, _http, cancellationToken); + Teams = new TeamClient(ServiceUrl, _http, cancellationToken); + Meetings = new MeetingClient(ServiceUrl, _http, cancellationToken); } public ApiClient(string serviceUrl, IHttpClientFactory factory, CancellationToken cancellationToken = default) : base(factory, cancellationToken) { ServiceUrl = NormalizeServiceUrl(serviceUrl); Bots = new BotClient(_http, cancellationToken); - Conversations = new ConversationClient(serviceUrl, _http, cancellationToken); + Conversations = new ConversationClient(ServiceUrl, _http, cancellationToken); Users = new UserClient(_http, cancellationToken); - Teams = new TeamClient(serviceUrl, _http, cancellationToken); - Meetings = new MeetingClient(serviceUrl, _http, cancellationToken); + Teams = new TeamClient(ServiceUrl, _http, cancellationToken); + Meetings = new MeetingClient(ServiceUrl, _http, cancellationToken); } public ApiClient(ApiClient client) : base() diff --git a/Libraries/Microsoft.Teams.Api/Clients/ConversationClient.cs b/Libraries/Microsoft.Teams.Api/Clients/ConversationClient.cs index 08fb2cfe..a66677e9 100644 --- a/Libraries/Microsoft.Teams.Api/Clients/ConversationClient.cs +++ b/Libraries/Microsoft.Teams.Api/Clients/ConversationClient.cs @@ -17,29 +17,29 @@ public class ConversationClient : Client public ConversationClient(string serviceUrl, CancellationToken cancellationToken = default) : base(cancellationToken) { ServiceUrl = NormalizeServiceUrl(serviceUrl); - Activities = new ActivityClient(serviceUrl, _http, cancellationToken); - Members = new MemberClient(serviceUrl, _http, cancellationToken); + Activities = new ActivityClient(ServiceUrl, _http, cancellationToken); + Members = new MemberClient(ServiceUrl, _http, cancellationToken); } public ConversationClient(string serviceUrl, IHttpClient client, CancellationToken cancellationToken = default) : base(client, cancellationToken) { ServiceUrl = NormalizeServiceUrl(serviceUrl); - Activities = new ActivityClient(serviceUrl, _http, cancellationToken); - Members = new MemberClient(serviceUrl, _http, cancellationToken); + Activities = new ActivityClient(ServiceUrl, _http, cancellationToken); + Members = new MemberClient(ServiceUrl, _http, cancellationToken); } public ConversationClient(string serviceUrl, IHttpClientOptions options, CancellationToken cancellationToken = default) : base(options, cancellationToken) { ServiceUrl = NormalizeServiceUrl(serviceUrl); - Activities = new ActivityClient(serviceUrl, _http, cancellationToken); - Members = new MemberClient(serviceUrl, _http, cancellationToken); + Activities = new ActivityClient(ServiceUrl, _http, cancellationToken); + Members = new MemberClient(ServiceUrl, _http, cancellationToken); } public ConversationClient(string serviceUrl, IHttpClientFactory factory, CancellationToken cancellationToken = default) : base(factory, cancellationToken) { ServiceUrl = NormalizeServiceUrl(serviceUrl); - Activities = new ActivityClient(serviceUrl, _http, cancellationToken); - Members = new MemberClient(serviceUrl, _http, cancellationToken); + Activities = new ActivityClient(ServiceUrl, _http, cancellationToken); + Members = new MemberClient(ServiceUrl, _http, cancellationToken); } public async Task CreateAsync(CreateRequest request) diff --git a/Libraries/Microsoft.Teams.Apps/App.cs b/Libraries/Microsoft.Teams.Apps/App.cs index 1ab356a0..4aa1446f 100644 --- a/Libraries/Microsoft.Teams.Apps/App.cs +++ b/Libraries/Microsoft.Teams.Apps/App.cs @@ -306,7 +306,7 @@ private async Task Process(ISenderPlugin sender, ActivityEvent @event, var reference = new ConversationReference() { - ServiceUrl = @event.Activity.ServiceUrl ?? @event.Token?.ServiceUrl, + ServiceUrl = @event.Activity.ServiceUrl ?? @event.Token?.ServiceUrl ?? string.Empty, ChannelId = @event.Activity.ChannelId, Bot = @event.Activity.Recipient, User = @event.Activity.From, diff --git a/Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/AspNetCorePlugin.cs b/Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/AspNetCorePlugin.cs index dbd65a3d..16e96d8d 100644 --- a/Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/AspNetCorePlugin.cs +++ b/Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/AspNetCorePlugin.cs @@ -165,9 +165,9 @@ public async Task Do(HttpContext httpContext, CancellationToken cancell return Results.BadRequest("Missing activity"); } - // If no token was extracted, create an anonymous token with serviceUrl from the activity + // If no token was extracted, create an anonymous token with serviceUrl from the activity (or default) // This matches Python/TypeScript SDK behavior for skipAuth scenarios - IToken resolvedToken = (IToken?)token ?? new AnonymousToken(activity.ServiceUrl ?? string.Empty); + IToken resolvedToken = (IToken?)token ?? new AnonymousToken(activity.ServiceUrl ?? "https://smba.trafficmanager.net/teams"); var data = new Dictionary { diff --git a/Tests/Microsoft.Teams.Api.Tests/Clients/ApiClientTests.cs b/Tests/Microsoft.Teams.Api.Tests/Clients/ApiClientTests.cs index fc5e02b1..44f8d455 100644 --- a/Tests/Microsoft.Teams.Api.Tests/Clients/ApiClientTests.cs +++ b/Tests/Microsoft.Teams.Api.Tests/Clients/ApiClientTests.cs @@ -11,7 +11,8 @@ public void ApiClient_Default() var serviceUrl = "https://api.botframework.com"; var apiClient = new ApiClient(serviceUrl); - Assert.Equal(serviceUrl, apiClient.ServiceUrl); + // ServiceUrl is normalized to have trailing slash + Assert.Equal(serviceUrl + "/", apiClient.ServiceUrl); } [Fact] @@ -20,6 +21,7 @@ public void ApiClient_Users_Default() var serviceUrl = "https://api.botframework.com"; var apiClient = new ApiClient(serviceUrl); - Assert.Equal(serviceUrl, apiClient.ServiceUrl); + // ServiceUrl is normalized to have trailing slash + Assert.Equal(serviceUrl + "/", apiClient.ServiceUrl); } } \ No newline at end of file