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 4963f629..ce651f1f 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)
diff --git a/Libraries/Microsoft.Teams.Api/Clients/ApiClient.cs b/Libraries/Microsoft.Teams.Api/Clients/ApiClient.cs
index 3a7d9269..2acfcb6a 100644
--- a/Libraries/Microsoft.Teams.Api/Clients/ApiClient.cs
+++ b/Libraries/Microsoft.Teams.Api/Clients/ApiClient.cs
@@ -16,42 +16,42 @@ 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);
+ 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 = serviceUrl;
+ 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 = serviceUrl;
+ 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 = serviceUrl;
+ 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/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..a66677e9 100644
--- a/Libraries/Microsoft.Teams.Api/Clients/ConversationClient.cs
+++ b/Libraries/Microsoft.Teams.Api/Clients/ConversationClient.cs
@@ -16,30 +16,30 @@ public class ConversationClient : Client
public ConversationClient(string serviceUrl, CancellationToken cancellationToken = default) : base(cancellationToken)
{
- ServiceUrl = serviceUrl;
- Activities = new ActivityClient(serviceUrl, _http, cancellationToken);
- Members = new MemberClient(serviceUrl, _http, cancellationToken);
+ 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;
- Activities = new ActivityClient(serviceUrl, _http, cancellationToken);
- Members = new MemberClient(serviceUrl, _http, cancellationToken);
+ 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;
- Activities = new ActivityClient(serviceUrl, _http, cancellationToken);
- Members = new MemberClient(serviceUrl, _http, cancellationToken);
+ 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;
- Activities = new ActivityClient(serviceUrl, _http, cancellationToken);
- Members = new MemberClient(serviceUrl, _http, cancellationToken);
+ ServiceUrl = NormalizeServiceUrl(serviceUrl);
+ Activities = new ActivityClient(ServiceUrl, _http, cancellationToken);
+ Members = new MemberClient(ServiceUrl, _http, cancellationToken);
}
public async Task CreateAsync(CreateRequest request)
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.Apps/App.cs b/Libraries/Microsoft.Teams.Apps/App.cs
index 5f7768bc..f2a1c925 100644
--- a/Libraries/Microsoft.Teams.Apps/App.cs
+++ b/Libraries/Microsoft.Teams.Apps/App.cs
@@ -312,7 +312,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,
@@ -338,8 +338,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 169ec146..199a1d22 100644
--- a/Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/AspNetCorePlugin.cs
+++ b/Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/AspNetCorePlugin.cs
@@ -184,6 +184,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 (or default)
+ // This matches Python/TypeScript SDK behavior for skipAuth scenarios
+ IToken resolvedToken = (IToken?)token ?? new AnonymousToken(activity.ServiceUrl ?? "https://smba.trafficmanager.net/teams");
+
var data = new Dictionary
{
["Request.TraceId"] = httpContext.TraceIdentifier
@@ -200,7 +204,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
@@ -235,9 +239,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.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
diff --git a/Tests/Microsoft.Teams.Plugins.AspNetCore.Tests/AspNetCorePluginTests.cs b/Tests/Microsoft.Teams.Plugins.AspNetCore.Tests/AspNetCorePluginTests.cs
index a8370c4d..91f15f24 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,49 @@ public async Task Test_Do_Core_ReturnsResponseAndLogs()
Assert.Same(response, res);
logger.Verify(l => l.Debug(It.IsAny