Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions Libraries/Microsoft.Teams.Api/Auth/AnonymousToken.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

namespace Microsoft.Teams.Api.Auth;

/// <summary>
/// A fallback token used when no authentication is provided (e.g., skipAuth mode).
/// Mirrors the behavior of Python and TypeScript SDKs.
/// </summary>
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<string> Scopes => [];

public AnonymousToken(string serviceUrl)
{
// Ensure serviceUrl has trailing slash for consistency
ServiceUrl = serviceUrl.EndsWith('/') ? serviceUrl : serviceUrl + '/';
Comment on lines +32 to +33
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The constructor accepts a serviceUrl parameter that could be an empty string. When an empty string is passed, the normalization logic will convert it to "/", which is not a valid service URL. Consider adding validation to handle empty strings, either by throwing an exception or falling back to a default service URL like "https://smba.trafficmanager.net/teams".

Suggested change
// Ensure serviceUrl has trailing slash for consistency
ServiceUrl = serviceUrl.EndsWith('/') ? serviceUrl : serviceUrl + '/';
// Use a default service URL if the provided value is null, empty, or whitespace
var normalizedServiceUrl = string.IsNullOrWhiteSpace(serviceUrl)
? "https://smba.trafficmanager.net/teams"
: serviceUrl;
// Ensure serviceUrl has trailing slash for consistency
ServiceUrl = normalizedServiceUrl.EndsWith('/')
? normalizedServiceUrl
: normalizedServiceUrl + '/';

Copilot uses AI. Check for mistakes.
}

public override string ToString() => string.Empty;
}
8 changes: 4 additions & 4 deletions Libraries/Microsoft.Teams.Api/Clients/ActivityClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Resource?> CreateAsync(string conversationId, IActivity activity)
Expand Down
32 changes: 16 additions & 16 deletions Libraries/Microsoft.Teams.Api/Clients/ApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
3 changes: 3 additions & 0 deletions Libraries/Microsoft.Teams.Api/Clients/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
24 changes: 12 additions & 12 deletions Libraries/Microsoft.Teams.Api/Clients/ConversationClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ConversationResource> CreateAsync(CreateRequest request)
Expand Down
8 changes: 4 additions & 4 deletions Libraries/Microsoft.Teams.Api/Clients/MeetingClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Meeting> GetByIdAsync(string id)
Expand Down
8 changes: 4 additions & 4 deletions Libraries/Microsoft.Teams.Api/Clients/MemberClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<List<Account>> GetAsync(string conversationId)
Expand Down
8 changes: 4 additions & 4 deletions Libraries/Microsoft.Teams.Api/Clients/TeamClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Team> GetByIdAsync(string id)
Expand Down
6 changes: 3 additions & 3 deletions Libraries/Microsoft.Teams.Apps/App.cs
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ private async Task<Response> 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,
Expand All @@ -338,8 +338,8 @@ private async Task<Response> Process(ISenderPlugin sender, ActivityEvent @event,
var stream = sender.CreateStream(reference, cancellationToken);
var context = new Context<IActivity>(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,
Expand Down
2 changes: 1 addition & 1 deletion Libraries/Microsoft.Teams.Apps/Events/ActivityEvent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, object?>? Extra { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,10 @@ public async Task<IResult> 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");
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When activity.ServiceUrl is an empty string (not null), it will be passed to AnonymousToken, which will normalize it to just "/" due to the trailing slash logic. This could cause issues when making API calls. Consider using string.IsNullOrEmpty(activity.ServiceUrl) instead of just the null coalescing operator to ensure empty strings also fall back to the default service URL.

Copilot uses AI. Check for mistakes.

var data = new Dictionary<string, object?>
{
["Request.TraceId"] = httpContext.TraceIdentifier
Expand All @@ -200,7 +204,7 @@ public async Task<IResult> Do(HttpContext httpContext, CancellationToken cancell

var res = await Do(new ActivityEvent()
{
Token = token,
Token = resolvedToken,
Activity = activity,
Extra = data,
Services = httpContext.RequestServices
Expand Down Expand Up @@ -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 ", ""));
}

Expand Down
6 changes: 4 additions & 2 deletions Tests/Microsoft.Teams.Api.Tests/Clients/ApiClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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);
}
}
Loading
Loading