Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
8 changes: 4 additions & 4 deletions Libraries/Microsoft.Teams.Api/Clients/ApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
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
8 changes: 4 additions & 4 deletions Libraries/Microsoft.Teams.Api/Clients/ConversationClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Comment thread
rajan-chari marked this conversation as resolved.
Outdated
}

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);
Comment thread
rajan-chari marked this conversation as resolved.
Outdated
}

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);
Comment thread
rajan-chari marked this conversation as resolved.
Outdated
}

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);
Comment thread
rajan-chari marked this conversation as resolved.
Outdated
}
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 @@ -306,7 +306,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,
Comment thread
rajan-chari marked this conversation as resolved.
Outdated
ChannelId = @event.Activity.ChannelId,
Bot = @event.Activity.Recipient,
User = @event.Activity.From,
Expand All @@ -332,8 +332,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 @@ -165,6 +165,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
// This matches Python/TypeScript SDK behavior for skipAuth scenarios
IToken resolvedToken = (IToken?)token ?? new AnonymousToken(activity.ServiceUrl ?? string.Empty);
Comment thread
rajan-chari marked this conversation as resolved.
Outdated

var data = new Dictionary<string, object?>
{
["Request.TraceId"] = httpContext.TraceIdentifier
Expand All @@ -181,7 +185,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 @@ -216,9 +220,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
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -196,4 +208,49 @@ public async Task Test_Do_Core_ReturnsResponseAndLogs()
Assert.Same(response, res);
logger.Verify(l => l.Debug(It.IsAny<object[]>()), 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;
// Token should be an AnonymousToken when no auth header (matches Python/TypeScript behavior)
Assert.NotNull(activityEvent.Token);
Assert.IsType<AnonymousToken>(activityEvent.Token);
Assert.Equal(string.Empty, activityEvent.Token.AppId);
return Task.FromResult<object?>(coreResponse);
}
return Task.FromResult<object?>(null);
};

var plugin = CreatePlugin(new Mock<ILogger>(), events);
var ctx = CreateHttpContextWithoutAuth(activity);

// Act
var result = await plugin.Do(ctx);

// Assert
var jsonResult = Assert.IsType<Microsoft.AspNetCore.Http.HttpResults.JsonHttpResult<object?>>(result);
Assert.Equal(200, jsonResult.StatusCode);
}
Comment thread
rajan-chari marked this conversation as resolved.
Comment thread
rajan-chari marked this conversation as resolved.
}
Loading