Skip to content
Draft
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
1 change: 1 addition & 0 deletions core/core.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,5 @@
<Project Path="src/Microsoft.Teams.Bot.Apps/Microsoft.Teams.Bot.Apps.csproj" Id="0e993a63-8a1a-4cdf-8a29-cc8c59bd6c30" />
<Project Path="src/Microsoft.Teams.Bot.Compat/Microsoft.Teams.Bot.Compat.csproj" Id="42d14898-dcc0-43a4-bb61-60e289f63c44" />
<Project Path="src/Microsoft.Teams.Bot.Core/Microsoft.Teams.Bot.Core.csproj" />
<Project Path="src/Microsoft.Teams.Bot.DevTools/Microsoft.Teams.Bot.DevTools.csproj" />
</Solution>
1 change: 1 addition & 0 deletions core/samples/CoreBot/CoreBot.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.Teams.Bot.Core\Microsoft.Teams.Bot.Core.csproj" />
<ProjectReference Include="..\..\src\Microsoft.Teams.Bot.DevTools\Microsoft.Teams.Bot.DevTools.csproj" />
</ItemGroup>

</Project>
7 changes: 7 additions & 0 deletions core/samples/CoreBot/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,21 @@
using Microsoft.Teams.Bot.Core;
using Microsoft.Teams.Bot.Core.Hosting;
using Microsoft.Teams.Bot.Core.Schema;
using Microsoft.Teams.Bot.DevTools;

WebApplicationBuilder webAppBuilder = WebApplication.CreateSlimBuilder(args);
webAppBuilder.Services.AddBotApplication();
webAppBuilder.Services.AddDevTools();
WebApplication webApp = webAppBuilder.Build();

webApp.MapGet("/", () => "CoreBot is running.");
BotApplication botApp = webApp.UseBotApplication();

if (webApp.Environment.IsDevelopment())
{
webApp.UseDevTools();
}

botApp.OnActivity = async (activity, cancellationToken) =>
{
string replyText = $"CoreBot running on SDK `{BotApplication.Version}`.";
Expand Down
8 changes: 8 additions & 0 deletions core/samples/TeamsBot/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,22 @@
using Microsoft.Teams.Bot.Apps;
using Microsoft.Teams.Bot.Apps.Handlers;
using Microsoft.Teams.Bot.Apps.Schema;
using Microsoft.Teams.Bot.DevTools;
using TeamsBot;

WebApplicationBuilder webAppBuilder = WebApplication.CreateSlimBuilder(args);
webAppBuilder.Services.AddTeamsBotApplication();
webAppBuilder.Services.AddDevTools();
WebApplication webApp = webAppBuilder.Build();

TeamsBotApplication teamsApp = webApp.UseTeamsBotApplication();


if (webApp.Environment.IsDevelopment())
{
webApp.UseDevTools<TeamsBotApplication>();
}

// ==================== MESSAGE HANDLERS ====================

// Help handler: matches "help" (case-insensitive)
Expand Down
1 change: 1 addition & 0 deletions core/samples/TeamsBot/TeamsBot.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.Teams.Bot.Apps\Microsoft.Teams.Bot.Apps.csproj" />
<ProjectReference Include="..\..\src\Microsoft.Teams.Bot.DevTools\Microsoft.Teams.Bot.DevTools.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -41,23 +41,17 @@ public static BotApplication UseBotApplication(
/// Configures the application to handle bot messages at the specified route and returns the registered bot
/// application instance.
/// </summary>
/// <remarks>This method adds authentication and authorization middleware to the HTTP pipeline and maps
/// a POST endpoint for bot messages. The endpoint requires authorization. Ensure that the bot application
/// is registered in the service container before calling this method.</remarks>
/// <typeparam name="TApp">The type of the bot application to use. Must inherit from BotApplication.</typeparam>
/// <param name="endpoints">The endpoint route builder used to configure endpoints.</param>
/// <param name="routePath">The route path at which to listen for incoming bot messages. Defaults to "api/messages".</param>
/// <returns>The registered bot application instance of type TApp.</returns>
/// <exception cref="InvalidOperationException">Thrown if the bot application of type TApp is not registered in the application's service container.</exception>
public static TApp UseBotApplication<TApp>(
this IEndpointRouteBuilder endpoints,
string routePath = "api/messages")
where TApp : BotApplication
{
ArgumentNullException.ThrowIfNull(endpoints);

// Add authentication and authorization middleware to the pipeline
// This is safe because WebApplication implements both IEndpointRouteBuilder and IApplicationBuilder
if (endpoints is IApplicationBuilder app)
{
app.UseAuthentication();
Expand All @@ -76,26 +70,15 @@ public static TApp UseBotApplication<TApp>(
/// <summary>
/// Adds a bot application to the service collection with the default configuration section name "AzureAd".
/// </summary>
/// <param name="services"></param>
/// <param name="sectionName"></param>
/// <returns></returns>
public static IServiceCollection AddBotApplication(this IServiceCollection services, string sectionName = "AzureAd")
=> services.AddBotApplication<BotApplication>(sectionName);

/// <summary>
/// Adds a bot application to the service collection.
/// </summary>
/// <typeparam name="TApp"></typeparam>
/// <param name="services"></param>
/// <param name="sectionName"></param>
/// <returns></returns>
public static IServiceCollection AddBotApplication<TApp>(this IServiceCollection services, string sectionName = "AzureAd") where TApp : BotApplication
{
// Extract ILoggerFactory from service collection to create logger without BuildServiceProvider
ServiceDescriptor? loggerFactoryDescriptor = services.FirstOrDefault(d => d.ServiceType == typeof(ILoggerFactory));
ILoggerFactory? loggerFactory = loggerFactoryDescriptor?.ImplementationInstance as ILoggerFactory;
ILogger logger = loggerFactory?.CreateLogger<BotApplication>()
?? (ILogger)Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance;
var (configuration, logger) = ResolveConfigAndLogger(services);

services.AddSingleton<BotApplicationOptions>(sp =>
{
Expand All @@ -107,36 +90,78 @@ public static IServiceCollection AddBotApplication<TApp>(this IServiceCollection
});
services.AddHttpContextAccessor();
services.AddBotAuthorization(sectionName, logger);
services.AddConversationClient(sectionName);
services.AddUserTokenClient(sectionName);

// Configure shared infrastructure (options, token acquisition, etc.) once
RegisterSharedInfrastructure(services, sectionName);

// Check MSAL configuration once, register both clients with the result
bool msalConfigured = services.ConfigureMSAL(configuration, sectionName, logger);
if (!msalConfigured)
{
_logAuthConfigNotFound(logger, null);
}

AddBotClient<ConversationClient>(services, ConversationClient.ConversationHttpClientName, msalConfigured);
AddBotClient<UserTokenClient>(services, UserTokenClient.UserTokenHttpClientName, msalConfigured);

services.AddSingleton<TApp>();
return services;
}

/// <summary>
/// Adds conversation client to the service collection.
/// </summary>
/// <param name="services">service collection</param>
/// <param name="sectionName">Configuration Section name, defaults to AzureAD</param>
/// <returns></returns>
public static IServiceCollection AddConversationClient(this IServiceCollection services, string sectionName = "AzureAd") =>
services.AddBotClient<ConversationClient>(ConversationClient.ConversationHttpClientName, sectionName);
public static IServiceCollection AddConversationClient(this IServiceCollection services, string sectionName = "AzureAd")
{
var (configuration, logger) = ResolveConfigAndLogger(services);
RegisterSharedInfrastructure(services, sectionName);
bool msalConfigured = services.ConfigureMSAL(configuration, sectionName, logger);
if (!msalConfigured)
{
_logAuthConfigNotFound(logger, null);
}

return AddBotClient<ConversationClient>(services, ConversationClient.ConversationHttpClientName, msalConfigured);
}

/// <summary>
/// Adds user token client to the service collection.
/// </summary>
/// <param name="services">service collection</param>
/// <param name="sectionName">Configuration Section name, defaults to AzureAD</param>
/// <returns></returns>
public static IServiceCollection AddUserTokenClient(this IServiceCollection services, string sectionName = "AzureAd") =>
services.AddBotClient<UserTokenClient>(UserTokenClient.UserTokenHttpClientName, sectionName);
public static IServiceCollection AddUserTokenClient(this IServiceCollection services, string sectionName = "AzureAd")
{
var (configuration, logger) = ResolveConfigAndLogger(services);
RegisterSharedInfrastructure(services, sectionName);
bool msalConfigured = services.ConfigureMSAL(configuration, sectionName, logger);
if (!msalConfigured)
{
_logAuthConfigNotFound(logger, null);
}

private static IServiceCollection AddBotClient<TClient>(
this IServiceCollection services,
string httpClientName,
string sectionName) where TClient : class
return AddBotClient<UserTokenClient>(services, UserTokenClient.UserTokenHttpClientName, msalConfigured);
}

private static (IConfiguration configuration, ILogger logger) ResolveConfigAndLogger(IServiceCollection services)
{
ServiceDescriptor? configDescriptor = services.FirstOrDefault(d => d.ServiceType == typeof(IConfiguration));
ServiceDescriptor? loggerFactoryDescriptor = services.FirstOrDefault(d => d.ServiceType == typeof(ILoggerFactory));
ILoggerFactory? loggerFactory = loggerFactoryDescriptor?.ImplementationInstance as ILoggerFactory;

if (configDescriptor?.ImplementationInstance is IConfiguration config)
{
ILogger logger = loggerFactory?.CreateLogger(typeof(AddBotApplicationExtensions))
?? Extensions.Logging.Abstractions.NullLogger.Instance;
return (config, logger);
}

using ServiceProvider tempProvider = services.BuildServiceProvider();
IConfiguration resolvedConfig = tempProvider.GetRequiredService<IConfiguration>();
ILogger resolvedLogger = (loggerFactory ?? tempProvider.GetRequiredService<ILoggerFactory>())
.CreateLogger(typeof(AddBotApplicationExtensions));
return (resolvedConfig, resolvedLogger);
}

private static void RegisterSharedInfrastructure(IServiceCollection services, string sectionName)
{
// Register options to defer scope configuration reading
services.AddOptions<BotClientOptions>()
.Configure<IConfiguration>((options, configuration) =>
{
Expand All @@ -153,29 +178,14 @@ private static IServiceCollection AddBotClient<TClient>(
.AddTokenAcquisition(true)
.AddInMemoryTokenCaches()
.AddAgentIdentities();
}

// Get configuration and logger to configure MSAL during registration
// Try to get from service descriptors first
ServiceDescriptor? configDescriptor = services.FirstOrDefault(d => d.ServiceType == typeof(IConfiguration));

ServiceDescriptor? loggerFactoryDescriptor = services.FirstOrDefault(d => d.ServiceType == typeof(ILoggerFactory));
ILoggerFactory? loggerFactory = loggerFactoryDescriptor?.ImplementationInstance as ILoggerFactory;
ILogger logger = loggerFactory?.CreateLogger(typeof(AddBotApplicationExtensions))
?? Extensions.Logging.Abstractions.NullLogger.Instance;

// If configuration not available as instance, build temporary provider
if (configDescriptor?.ImplementationInstance is not IConfiguration configuration)
{
using ServiceProvider tempProvider = services.BuildServiceProvider();
configuration = tempProvider.GetRequiredService<IConfiguration>();
if (loggerFactory == null)
{
logger = tempProvider.GetRequiredService<ILoggerFactory>().CreateLogger(typeof(AddBotApplicationExtensions));
}
}

// Configure MSAL during registration (not deferred)
if (services.ConfigureMSAL(configuration, sectionName, logger))
private static IServiceCollection AddBotClient<TClient>(
IServiceCollection services,
string httpClientName,
bool msalConfigured) where TClient : class
{
if (msalConfigured)
{
services.AddHttpClient<TClient>(httpClientName)
.AddHttpMessageHandler(sp =>
Expand All @@ -190,7 +200,6 @@ private static IServiceCollection AddBotClient<TClient>(
}
else
{
_logAuthConfigNotFound(logger, null);
services.AddHttpClient<TClient>(httpClientName);
}

Expand All @@ -206,19 +215,26 @@ private static bool ConfigureMSAL(this IServiceCollection services, IConfigurati
_logUsingBFConfig(logger, null);
BotConfig botConfig = BotConfig.FromBFConfig(configuration);
services.ConfigureMSALFromBotConfig(botConfig, logger);
return true;
}
else if (configuration["CLIENT_ID"] is not null)

if (configuration["CLIENT_ID"] is not null)
{
_logUsingCoreConfig(logger, null);
BotConfig botConfig = BotConfig.FromCoreConfig(configuration);
services.ConfigureMSALFromBotConfig(botConfig, logger);
return true;
}
else

_logUsingSectionConfig(logger, sectionName, null);
var section = configuration.GetSection(sectionName);
if (section["ClientId"] is not null && !string.IsNullOrEmpty(section["ClientId"]))
{
_logUsingSectionConfig(logger, sectionName, null);
services.ConfigureMSALFromConfig(configuration.GetSection(sectionName));
services.ConfigureMSALFromConfig(section);
return true;
}
return true;

return false;
}

private static IServiceCollection ConfigureMSALFromConfig(this IServiceCollection services, IConfigurationSection msalConfigSection)
Expand Down Expand Up @@ -281,7 +297,6 @@ private static IServiceCollection ConfigureMSALWithUMI(this IServiceCollection s
ArgumentNullException.ThrowIfNullOrWhiteSpace(tenantId);
ArgumentNullException.ThrowIfNullOrWhiteSpace(clientId);

// Register ManagedIdentityOptions for BotAuthenticationHandler to use
bool isSystemAssigned = IsSystemAssignedManagedIdentity(managedIdentityClientId);
string? umiClientId = isSystemAssigned ? null : (managedIdentityClientId ?? clientId);

Expand Down Expand Up @@ -321,9 +336,6 @@ private static IServiceCollection ConfigureMSALFromBotConfig(this IServiceCollec
return services;
}

/// <summary>
/// Determines if the provided client ID represents a system-assigned managed identity.
/// </summary>
private static bool IsSystemAssignedManagedIdentity(string? clientId)
=> string.Equals(clientId, BotConfig.SystemManagedIdentityIdentifier, StringComparison.OrdinalIgnoreCase);

Expand All @@ -341,6 +353,4 @@ private static bool IsSystemAssignedManagedIdentity(string? clientId)
LoggerMessage.Define<string>(LogLevel.Debug, new(6), "Configuring authentication with Federated Identity Credential (Managed Identity) with {IdentityType} Managed Identity");
private static readonly Action<ILogger, Exception?> _logAuthConfigNotFound =
LoggerMessage.Define(LogLevel.Warning, new(7), "Authentication configuration not found. Running without Auth");


}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ internal sealed class BotAuthenticationHandler(
/// <inheritdoc/>
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{


request.Options.TryGetValue(AgenticIdentityKey, out AgenticIdentity? agenticIdentity);

string token = await GetAuthorizationHeaderAsync(agenticIdentity, cancellationToken).ConfigureAwait(false);
Expand Down
3 changes: 2 additions & 1 deletion core/src/Microsoft.Teams.Bot.Core/Hosting/BotConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ public static BotConfig Resolve(IConfiguration configuration, string sectionName
config = FromBFConfig(configuration);
if (!string.IsNullOrEmpty(config.ClientId)) return config;

throw new InvalidOperationException("ClientID not found in configuration.");
// throw new InvalidOperationException("ClientID not found in configuration.");
return new BotConfig();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.Extensions.Logging;
using Microsoft.Teams.Bot.Core;
using Microsoft.Teams.Bot.Core.Schema;

namespace Microsoft.Teams.Bot.DevTools;

using CustomHeaders = Dictionary<string, string>;

/// <summary>
/// Decorator around <see cref="ConversationClient"/> that emits "sent" events to DevTools UI clients
/// whenever an activity is sent.
/// </summary>
public class DevToolsConversationClient : ConversationClient
{
private readonly DevToolsService _service;

/// <summary>
/// Creates a new DevToolsConversationClient.
/// </summary>
/// <param name="httpClient">The HTTP client for sending activities.</param>
/// <param name="logger">The logger.</param>
/// <param name="service">The shared DevTools service for emitting events.</param>
public DevToolsConversationClient(HttpClient httpClient, ILogger<ConversationClient> logger, DevToolsService service)
: base(httpClient, logger)
{
_service = service;
}

/// <inheritdoc/>
public override async Task<SendActivityResponse> SendActivityAsync(CoreActivity activity, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(activity);
var response = await base.SendActivityAsync(activity, customHeaders, cancellationToken).ConfigureAwait(false);

// Ensure activity has an ID so the DevTools UI can distinguish messages
activity.Id ??= response.Id ?? Guid.NewGuid().ToString();

// Emit sent event after successful send
await _service.EmitSent(activity, cancellationToken).ConfigureAwait(false);

return response;
}
}
Loading
Loading