Skip to content
4 changes: 4 additions & 0 deletions Libraries/Microsoft.Teams.Api/Auth/ClientCredentials.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@

namespace Microsoft.Teams.Api.Auth;

/// <summary>
/// ClientId / ClientSecret based credentials
/// https://learn.microsoft.com/en-us/microsoft-365/agents-sdk/azure-bot-create-single-secret
/// </summary>
public class ClientCredentials : IHttpCredentials
{
public string ClientId { get; set; }
Expand Down
8 changes: 7 additions & 1 deletion Libraries/Microsoft.Teams.Api/Auth/TokenCredentials.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ namespace Microsoft.Teams.Api.Auth;

public delegate Task<ITokenResponse> TokenFactory(string? tenantId, params string[] scopes);

/// <summary>
/// Provide a <code>TokenFactory</code> that will be invoked whenever
/// the application needs a token.
/// TokenCredentials should be used with 3rd party packages like MSAL/Azure.Identity
/// to authenticate for any Federated/Managed Identity scenarios.
/// </summary>
public class TokenCredentials : IHttpCredentials
{
public string ClientId { get; set; }
Expand All @@ -26,7 +32,7 @@ public TokenCredentials(string clientId, string tenantId, TokenFactory token)
Token = token;
}

public async Task<ITokenResponse> Resolve(IHttpClient _client, string[] scopes, CancellationToken cancellationToken = default)
public async Task<ITokenResponse> Resolve(IHttpClient _, string[] scopes, CancellationToken cancellationToken = default)
{
return await Token(TenantId, scopes);
}
Expand Down
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 @@ -32,7 +32,7 @@ public ActivityClient(string serviceUrl, IHttpClientFactory factory, Cancellatio
ServiceUrl = serviceUrl;
}

public async Task<Resource?> CreateAsync(string conversationId, IActivity activity, bool isTargeted = false)
public virtual async Task<Resource?> CreateAsync(string conversationId, IActivity activity, bool isTargeted = false)
{
var url = $"{ServiceUrl}v3/conversations/{conversationId}/activities";
if (isTargeted)
Expand All @@ -50,7 +50,7 @@ public ActivityClient(string serviceUrl, IHttpClientFactory factory, Cancellatio
return body;
}

public async Task<Resource?> UpdateAsync(string conversationId, string id, IActivity activity, bool isTargeted = false)
public virtual async Task<Resource?> UpdateAsync(string conversationId, string id, IActivity activity, bool isTargeted = false)
{
var url = $"{ServiceUrl}v3/conversations/{conversationId}/activities/{id}";
if (isTargeted)
Expand All @@ -68,7 +68,7 @@ public ActivityClient(string serviceUrl, IHttpClientFactory factory, Cancellatio
return body;
}

public async Task<Resource?> ReplyAsync(string conversationId, string id, IActivity activity, bool isTargeted = false)
public virtual async Task<Resource?> ReplyAsync(string conversationId, string id, IActivity activity, bool isTargeted = false)
{
activity.ReplyToId = id;

Expand All @@ -88,7 +88,7 @@ public ActivityClient(string serviceUrl, IHttpClientFactory factory, Cancellatio
return body;
}

public async Task DeleteAsync(string conversationId, string id, bool isTargeted = false)
public virtual async Task DeleteAsync(string conversationId, string id, bool isTargeted = false)
{
var url = $"{ServiceUrl}v3/conversations/{conversationId}/activities/{id}";
if (isTargeted)
Expand Down
2 changes: 1 addition & 1 deletion Libraries/Microsoft.Teams.Api/Clients/BotClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace Microsoft.Teams.Api.Clients;
public class BotClient : Client
{
public virtual BotTokenClient Token { get; }
public BotSignInClient SignIn { get; }
public virtual BotSignInClient SignIn { get; }

public BotClient() : this(default)
{
Expand Down
4 changes: 2 additions & 2 deletions Libraries/Microsoft.Teams.Api/Clients/BotSignInClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public BotSignInClient(IHttpClientFactory factory, CancellationToken cancellatio

}

public async Task<string> GetUrlAsync(GetUrlRequest request)
public virtual async Task<string> GetUrlAsync(GetUrlRequest request)
{
var query = QueryString.Serialize(request);
var req = HttpRequest.Get(
Expand All @@ -38,7 +38,7 @@ public async Task<string> GetUrlAsync(GetUrlRequest request)
return res.Body;
}

public async Task<SignIn.UrlResponse> GetResourceAsync(GetResourceRequest request)
public virtual async Task<SignIn.UrlResponse> GetResourceAsync(GetResourceRequest request)
{
var query = QueryString.Serialize(request);
var req = HttpRequest.Get(
Expand Down
4 changes: 2 additions & 2 deletions Libraries/Microsoft.Teams.Api/Clients/BotTokenClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public class BotTokenClient : Client
public static readonly string BotScope = "https://api.botframework.com/.default";
public static readonly string GraphScope = "https://graph.microsoft.com/.default";

public BotTokenClient() : this(default)
public BotTokenClient() : base()
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

This parameterless constructor calls base(), but the parent Client class does not have a parameterless constructor. This will cause a compilation error. The previous code was calling base(default) which correctly called the Client(CancellationToken) constructor. Either revert this change to base(default) or add a parameterless constructor to the Client base class.

Suggested change
public BotTokenClient() : base()
public BotTokenClient() : base(default)

Copilot uses AI. Check for mistakes.
{

}
Comment on lines +13 to 16
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

The parameterless constructor calls base() which doesn't exist in the abstract Client class. The base Client class only has constructors that accept parameters (CancellationToken, IHttpClient, etc.). This will cause a compilation error. Either remove this constructor or ensure the base class has a parameterless constructor.

Copilot uses AI. Check for mistakes.
Expand Down Expand Up @@ -40,7 +40,7 @@ public virtual async Task<ITokenResponse> GetAsync(IHttpCredentials credentials,
return await credentials.Resolve(http ?? _http, [BotScope], _cancellationToken);
}

public async Task<ITokenResponse> GetGraphAsync(IHttpCredentials credentials, IHttpClient? http = null)
public virtual async Task<ITokenResponse> GetGraphAsync(IHttpCredentials credentials, IHttpClient? http = null)
{
return await credentials.Resolve(http ?? _http, [GraphScope], _cancellationToken);
}
Expand Down
6 changes: 3 additions & 3 deletions Libraries/Microsoft.Teams.Api/Clients/ConversationClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ namespace Microsoft.Teams.Api.Clients;
public class ConversationClient : Client
{
public readonly string ServiceUrl;
public readonly ActivityClient Activities;
public readonly MemberClient Members;
public virtual ActivityClient Activities { get; }
public virtual MemberClient Members { get; }
Comment on lines +14 to +15
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

The Activities and Members properties are changed to virtual with auto-property syntax, but they are initialized in the constructor. This means they cannot be overridden in derived classes since they're set in the base constructor. Consider making them virtual with explicit backing fields, or ensure this pattern is intentional for mocking purposes only.

Copilot uses AI. Check for mistakes.

public ConversationClient(string serviceUrl, CancellationToken cancellationToken = default) : base(cancellationToken)
{
Expand Down Expand Up @@ -42,7 +42,7 @@ public ConversationClient(string serviceUrl, IHttpClientFactory factory, Cancell
Members = new MemberClient(serviceUrl, _http, cancellationToken);
}

public async Task<ConversationResource> CreateAsync(CreateRequest request)
public virtual async Task<ConversationResource> CreateAsync(CreateRequest request)
{
var req = HttpRequest.Post($"{ServiceUrl}v3/conversations", body: request);
var res = await _http.SendAsync<ConversationResource>(req, _cancellationToken);
Expand Down
4 changes: 2 additions & 2 deletions Libraries/Microsoft.Teams.Api/Clients/MeetingClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,14 @@ public MeetingClient(string serviceUrl, IHttpClientFactory factory, Cancellation
ServiceUrl = serviceUrl;
}

public async Task<Meeting> GetByIdAsync(string id)
public virtual async Task<Meeting> GetByIdAsync(string id)
{
var request = HttpRequest.Get($"{ServiceUrl}v1/meetings/{id}");
var response = await _http.SendAsync<Meeting>(request, _cancellationToken);
return response.Body;
}

public async Task<MeetingParticipant> GetParticipantAsync(string meetingId, string id)
public virtual async Task<MeetingParticipant> GetParticipantAsync(string meetingId, string id)
{
var request = HttpRequest.Get($"{ServiceUrl}v1/meetings/{meetingId}/participants/{id}");
var response = await _http.SendAsync<MeetingParticipant>(request, _cancellationToken);
Expand Down
6 changes: 3 additions & 3 deletions Libraries/Microsoft.Teams.Api/Clients/MemberClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,21 +29,21 @@ public MemberClient(string serviceUrl, IHttpClientFactory factory, CancellationT
ServiceUrl = serviceUrl;
}

public async Task<List<Account>> GetAsync(string conversationId)
public virtual async Task<List<Account>> GetAsync(string conversationId)
{
var request = HttpRequest.Get($"{ServiceUrl}v3/conversations/{conversationId}/members");
var response = await _http.SendAsync<List<Account>>(request, _cancellationToken);
return response.Body;
}

public async Task<Account> GetByIdAsync(string conversationId, string memberId)
public virtual async Task<Account> GetByIdAsync(string conversationId, string memberId)
{
var request = HttpRequest.Get($"{ServiceUrl}v3/conversations/{conversationId}/members/{memberId}");
var response = await _http.SendAsync<Account>(request, _cancellationToken);
return response.Body;
}

public async Task DeleteAsync(string conversationId, string memberId)
public virtual async Task DeleteAsync(string conversationId, string memberId)
{
var request = HttpRequest.Delete($"{ServiceUrl}v3/conversations/{conversationId}/members/{memberId}");
await _http.SendAsync(request, _cancellationToken);
Expand Down
4 changes: 2 additions & 2 deletions Libraries/Microsoft.Teams.Api/Clients/TeamClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,14 @@ public TeamClient(string serviceUrl, IHttpClientFactory factory, CancellationTok
ServiceUrl = serviceUrl;
}

public async Task<Team> GetByIdAsync(string id)
public virtual async Task<Team> GetByIdAsync(string id)
{
var request = HttpRequest.Get($"{ServiceUrl}v3/teams/{id}");
var response = await _http.SendAsync<Team>(request, _cancellationToken);
return response.Body;
}

public async Task<List<Channel>> GetConversationsAsync(string id)
public virtual async Task<List<Channel>> GetConversationsAsync(string id)
{
var request = HttpRequest.Get($"{ServiceUrl}v3/teams/{id}/conversations");
var response = await _http.SendAsync<List<Channel>>(request, _cancellationToken);
Expand Down
7 changes: 6 additions & 1 deletion Libraries/Microsoft.Teams.Api/Clients/UserClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ namespace Microsoft.Teams.Api.Clients;

public class UserClient : Client
{
public UserTokenClient Token { get; }
public virtual UserTokenClient Token { get; }

public UserClient() : base()
{
Token = new UserTokenClient(_http, _cancellationToken);
}

Comment on lines +12 to 16
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

The parameterless constructor calls base() which doesn't exist in the abstract Client class. Additionally, the Token property is initialized using _http and _cancellationToken which won't be initialized yet when calling base(). This will cause compilation and runtime errors. Either remove this constructor or properly initialize it by calling an existing base constructor.

Suggested change
public UserClient() : base()
{
Token = new UserTokenClient(_http, _cancellationToken);
}

Copilot uses AI. Check for mistakes.
public UserClient(CancellationToken cancellationToken = default) : base(cancellationToken)
Comment on lines +12 to 17
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

This parameterless constructor calls base(), but the parent Client class does not have a parameterless constructor. This will cause a compilation error. Either add a parameterless constructor to the Client base class, or remove this constructor if it's not needed.

Suggested change
public UserClient() : base()
{
Token = new UserTokenClient(_http, _cancellationToken);
}
public UserClient(CancellationToken cancellationToken = default) : base(cancellationToken)
public UserClient(CancellationToken cancellationToken = default) : base(cancellationToken)
{
Token = new UserTokenClient(_http, cancellationToken);
}
public UserClient(IHttpClient client, CancellationToken cancellationToken = default) : base(client, cancellationToken)

Copilot uses AI. Check for mistakes.
{
Expand Down
15 changes: 10 additions & 5 deletions Libraries/Microsoft.Teams.Api/Clients/UserTokenClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ public class UserTokenClient : Client
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};

public UserTokenClient() : base()
{

}
Comment on lines +18 to +21
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

The parameterless constructor calls base() which doesn't exist in the abstract Client class. The base Client class only has constructors that accept parameters (CancellationToken, IHttpClient, etc.). This will cause a compilation error. Either remove this constructor or ensure the base class has a parameterless constructor.

Copilot uses AI. Check for mistakes.

Comment on lines +18 to +22
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

This parameterless constructor calls base(), but the parent Client class does not have a parameterless constructor. This will cause a compilation error. Either add a parameterless constructor to the Client base class, or remove this constructor if it's not needed.

Suggested change
public UserTokenClient() : base()
{
}

Copilot uses AI. Check for mistakes.
public UserTokenClient(CancellationToken cancellationToken = default) : base(cancellationToken)
{

Expand All @@ -35,38 +40,38 @@ public UserTokenClient(IHttpClientFactory factory, CancellationToken cancellatio

}

public async Task<Token.Response> GetAsync(GetTokenRequest request)
public virtual async Task<Token.Response> GetAsync(GetTokenRequest request)
{
var query = QueryString.Serialize(request);
var req = HttpRequest.Get($"https://token.botframework.com/api/usertoken/GetToken?{query}");
var res = await _http.SendAsync<Token.Response>(req, _cancellationToken);
return res.Body;
}

public async Task<IDictionary<string, Token.Response>> GetAadAsync(GetAadTokenRequest request)
public virtual async Task<IDictionary<string, Token.Response>> GetAadAsync(GetAadTokenRequest request)
{
var query = QueryString.Serialize(request);
var req = HttpRequest.Post($"https://token.botframework.com/api/usertoken/GetAadTokens?{query}", body: request);
var res = await _http.SendAsync<IDictionary<string, Token.Response>>(req, _cancellationToken);
return res.Body;
}

public async Task<IList<Token.Status>> GetStatusAsync(GetTokenStatusRequest request)
public virtual async Task<IList<Token.Status>> GetStatusAsync(GetTokenStatusRequest request)
{
var query = QueryString.Serialize(request);
var req = HttpRequest.Get($"https://token.botframework.com/api/usertoken/GetTokenStatus?{query}");
var res = await _http.SendAsync<IList<Token.Status>>(req, _cancellationToken);
return res.Body;
}

public async Task SignOutAsync(SignOutRequest request)
public virtual async Task SignOutAsync(SignOutRequest request)
{
var query = QueryString.Serialize(request);
var req = HttpRequest.Delete($"https://token.botframework.com/api/usertoken/SignOut?{query}");
await _http.SendAsync(req, _cancellationToken);
}

public async Task<Token.Response> ExchangeAsync(ExchangeTokenRequest request)
public virtual async Task<Token.Response> ExchangeAsync(ExchangeTokenRequest request)
{
var query = QueryString.Serialize(new
{
Expand Down
9 changes: 8 additions & 1 deletion Libraries/Microsoft.Teams.Apps/AppBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,16 @@ public AppBuilder AddPlugin(Func<Task<IPlugin>> @delegate)
return this;
}

public AppBuilder AddOAuth(OAuthSettings oauthSettings)
{
_options.OAuth = oauthSettings;
return this;
}

public AppBuilder AddOAuth(string defaultConnectionName)
{
_options.OAuth = new OAuthSettings(defaultConnectionName);
_options.OAuth ??= new();
_options.OAuth.DefaultConnectionName = defaultConnectionName;
return this;
}

Expand Down
37 changes: 36 additions & 1 deletion Libraries/Microsoft.Teams.Apps/AppOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,49 @@ namespace Microsoft.Teams.Apps;

public class AppOptions
{
/// <summary>
/// The applications optional storage provider that allows
/// the application to access shared dependencies.
/// </summary>
public IServiceProvider? Provider { get; set; }

/// <summary>
/// The applications optional ILogger instance.
/// </summary>
public Common.Logging.ILogger? Logger { get; set; }

/// <summary>
/// The applications optional IStorage instance.
/// </summary>
public Common.Storage.IStorage<string, object>? Storage { get; set; }

/// <summary>
/// When provided, the application will use this <code>IHttpClient</code> instance
/// to send all http requests.
/// </summary>
public Common.Http.IHttpClient? Client { get; set; }

/// <summary>
/// When provided, the application will use this <code>IHttpClientFactory</code> to
/// initialize a new client whenever needed.
/// </summary>
public Common.Http.IHttpClientFactory? ClientFactory { get; set; }

/// <summary>
/// When provided, the application will use these credentials to resolve tokens it
/// uses to make API requests.
/// </summary>
public Common.Http.IHttpCredentials? Credentials { get; set; }

/// <summary>
/// A list of plugins to import into the application.
/// </summary>
public IList<IPlugin> Plugins { get; set; } = [];
public OAuthSettings OAuth { get; set; } = new OAuthSettings();

/// <summary>
/// User <code>OAuth</code> settings for the deferred (User) auth flows.
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

Grammar issue in documentation. The phrase "User OAuth settings" should be "User OAuth settings" without the extra word "User" at the beginning, or it should read "User OAuth settings for the deferred (user) auth flows" with consistent capitalization. Currently "User" is capitalized but "(User)" in the description has capital U, creating inconsistency.

Suggested change
/// User <code>OAuth</code> settings for the deferred (User) auth flows.
/// User <code>OAuth</code> settings for the deferred (user) auth flows.

Copilot uses AI. Check for mistakes.
/// </summary>
public OAuthSettings OAuth { get; set; } = new();

public AppOptions()
{
Expand Down
6 changes: 5 additions & 1 deletion Libraries/Microsoft.Teams.Apps/AppRouting.cs
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,11 @@ await Events.Emit(
Token = res
}
);
return new Response(HttpStatusCode.OK);

return new Response(HttpStatusCode.OK, OAuth.AccountLinkingUrl is null ? null : new
{
accountLinkingUrl = OAuth.AccountLinkingUrl
});
}
catch (HttpException ex)
{
Expand Down
10 changes: 10 additions & 0 deletions Libraries/Microsoft.Teams.Apps/Contexts/Context.cs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,15 @@ public partial interface IContext<TActivity> where TActivity : IActivity
/// </summary>
public Task<object?> Next();

/// <summary>
/// Called to continue the chain of route handlers using the specified context instance.
/// Use this overload when you want to invoke the next handler with a different or wrapped
/// <see cref="IContext{TActivity}"/> than the current one; if not called, no other handlers
/// in the sequence will be executed.
/// </summary>
/// <param name="context">The context to pass to the next handler in the chain.</param>
public Task<object?> Next(IContext<TActivity> context);

/// <summary>
Comment on lines +116 to 124
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

The new Next(IContext<TActivity> context) overload appears redundant. It converts the passed context to IActivity type and calls OnNext, which is the same as what Next() does with the current context. In the test at line 70, context.Next(context) is called, which is functionally equivalent to just calling context.Next(). Consider removing this overload unless there's a specific use case where a different context instance needs to be passed.

Suggested change
/// Called to continue the chain of route handlers using the specified context instance.
/// Use this overload when you want to invoke the next handler with a different or wrapped
/// <see cref="IContext{TActivity}"/> than the current one; if not called, no other handlers
/// in the sequence will be executed.
/// </summary>
/// <param name="context">The context to pass to the next handler in the chain.</param>
public Task<object?> Next(IContext<TActivity> context);
/// <summary>

Copilot uses AI. Check for mistakes.
/// convert the context to that of another activity type
/// </summary>
Expand Down Expand Up @@ -168,6 +177,7 @@ public void Deconstruct(out string appId, out ILogger log, out ApiClient api, ou
}
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

The new Next(IContext<TActivity> context) overload appears redundant. It converts the passed context to IActivity type and calls OnNext, which is the same as what Next() does with the current context. In the test at line 70, context.Next(context) is called, which is functionally equivalent to just calling context.Next(). Consider removing this overload unless there's a specific use case where a different context instance needs to be passed.

Copilot uses AI. Check for mistakes.

public Task<object?> Next() => OnNext(ToActivityType());
public Task<object?> Next(IContext<TActivity> context) => OnNext(context.ToActivityType());
public IContext<IActivity> ToActivityType() => ToActivityType<IActivity>();
public IContext<TToActivity> ToActivityType<TToActivity>() where TToActivity : IActivity
{
Expand Down
17 changes: 15 additions & 2 deletions Libraries/Microsoft.Teams.Apps/OAuthSettings.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

public class OAuthSettings(string? connectionName = "graph")
namespace Microsoft.Teams.Apps;

/// <summary>
/// Settings for Deferred (User) auth flows
/// </summary>
public class OAuthSettings
{
public string DefaultConnectionName { get; set; } = connectionName;
/// <summary>
/// The default connection name to use
/// </summary>
public string DefaultConnectionName { get; set; } = "graph";

/// <summary>
/// Url used for client to perform tab auth and link the NAA account to the bot login account.
/// </summary>
public string? AccountLinkingUrl { get; set; }
}
Loading