// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Microsoft.Identity.Abstractions;
using Microsoft.Identity.Web;
using Microsoft.Identity.Web.TokenCacheProviders.Distributed;
namespace Microsoft.Teams.Core.Hosting;
///
/// Provides extension methods for registering bot application clients and related authentication services with the
/// dependency injection container.
///
/// This class is intended to be used during application startup to configure HTTP clients, token
/// acquisition, and agent identity services required for bot-to-bot communication. The configuration section specified
/// by the Azure Active Directory (AAD) configuration name is used to bind authentication options. Typically, these
/// methods are called in the application's service configuration pipeline.
public static class AddBotApplicationExtensions
{
///
/// Configures the default to handle bot messages at the specified route.
///
/// The endpoint route builder used to configure endpoints.
/// The route path at which to listen for incoming bot messages. Defaults to "api/messages".
/// The registered instance.
public static BotApplication UseBotApplication(
this IEndpointRouteBuilder endpoints,
string routePath = "api/messages")
=> UseBotApplication(endpoints, routePath);
///
/// Configures the application to handle bot messages at the specified route and returns the registered bot
/// application instance.
///
/// 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.
/// The type of the bot application to use. Must inherit from BotApplication.
/// The endpoint route builder used to configure endpoints.
/// The route path at which to listen for incoming bot messages. Defaults to "api/messages".
/// The registered bot application instance of type TApp.
/// Thrown if the bot application of type TApp is not registered in the application's service container.
public static TApp UseBotApplication(
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();
app.UseAuthorization();
}
TApp botApp = endpoints.ServiceProvider.GetService() ?? throw new InvalidOperationException("Application not registered");
endpoints.MapPost(routePath, (HttpContext httpContext, CancellationToken cancellationToken)
=> botApp.ProcessAsync(httpContext, cancellationToken)
).RequireAuthorization();
return botApp;
}
///
/// Registers the default bot application and its dependencies in the service collection.
///
/// The service collection to add services to.
/// The configuration section name containing Azure AD settings. Defaults to "AzureAd".
/// The service collection for method chaining.
public static IServiceCollection AddBotApplication(this IServiceCollection services, string sectionName = BotConfig.DefaultSectionName)
=> services.AddBotApplication(sectionName);
///
/// Registers a custom bot application and its dependencies in the service collection.
///
/// The custom bot application type that inherits from BotApplication.
/// The service collection to add services to.
/// The configuration section name containing Azure AD settings. Defaults to "AzureAd".
/// The service collection for method chaining.
public static IServiceCollection AddBotApplication(this IServiceCollection services, string sectionName = BotConfig.DefaultSectionName) where TApp : BotApplication
{
BotConfig botConfig = BotConfig.Resolve(services, sectionName);
services.AddBotApplication(botConfig);
return services;
}
///
/// Registers a custom bot application and its dependencies in the service collection.
///
/// The custom bot application type that inherits from BotApplication.
/// The service collection to add services to.
/// The configuration containing Azure AD settings.
/// The service collection for method chaining.
public static IServiceCollection AddBotApplication(this IServiceCollection services, BotConfig botConfig) where TApp : BotApplication
{
ArgumentNullException.ThrowIfNull(botConfig);
services.AddSingleton(_ => new BotApplicationOptions { AppId = botConfig.ClientId });
services.AddHttpContextAccessor();
services.AddBotAuthorization(botConfig);
services.EnsureMsalServices(botConfig);
services.AddBotClient(ConversationClient.ConversationHttpClientName, botConfig);
services.AddBotClient(UserTokenClient.UserTokenHttpClientName, botConfig);
services.AddSingleton();
return services;
}
///
/// Registers the and its dependencies in the service collection.
///
/// The service collection to add services to.
/// The configuration section name containing Azure AD settings. Defaults to "AzureAd".
/// The service collection for method chaining.
public static IServiceCollection AddConversationClient(this IServiceCollection services, string sectionName = BotConfig.DefaultSectionName)
{
BotConfig botConfig = BotConfig.Resolve(services, sectionName);
return services.EnsureMsalServices(botConfig)
.AddBotClient(ConversationClient.ConversationHttpClientName, botConfig);
}
///
/// Registers the and its dependencies in the service collection.
///
/// The service collection to add services to.
/// The configuration section name containing Azure AD settings. Defaults to "AzureAd".
/// The service collection for method chaining.
public static IServiceCollection AddUserTokenClient(this IServiceCollection services, string sectionName = BotConfig.DefaultSectionName)
{
BotConfig botConfig = BotConfig.Resolve(services, sectionName);
return services.EnsureMsalServices(botConfig)
.AddBotClient(UserTokenClient.UserTokenHttpClientName, botConfig);
}
///
/// Registers the shared MSAL token-acquisition pipeline and binds the named MSAL options.
///
///
/// Safe to call multiple times: the Microsoft.Identity.Web service registrations are TryAdd-based,
/// and the named options binding ( and
/// ) appends an additional configure delegate per call. Those
/// delegates are idempotent against the same , so re-running them produces
/// the same options state.
///
public static IServiceCollection EnsureMsalServices(this IServiceCollection services, BotConfig botConfig)
{
services.AddHttpClient()
.AddTokenAcquisition(true)
.AddDistributedTokenCaches()
.AddAgentIdentities();
ArgumentNullException.ThrowIfNull(botConfig);
ArgumentNullException.ThrowIfNull(botConfig.MsalConfigurationSection);
if (!string.IsNullOrWhiteSpace(botConfig.ClientId))
{
services.Configure(botConfig.SectionName, options =>
{
botConfig.MsalConfigurationSection.Bind(options);
// Default Instance when only TenantId is configured.
if (string.IsNullOrEmpty(options.Instance) && !string.IsNullOrEmpty(options.TenantId))
{
options.Instance = "https://login.microsoftonline.com/";
}
// MicrosoftEntraApplicationOptions.Authority is a computed property that
// returns Instance/TenantId/v2.0 when _authority is null. MergedOptions
// then sees Authority alongside Instance+TenantId and emits a warning
// (event 500). Setting Authority to empty prevents the computed value
// from propagating while Instance+TenantId remain available for MSAL.
if (!string.IsNullOrEmpty(options.Instance) && !string.IsNullOrEmpty(options.TenantId))
{
options.Authority = string.Empty;
}
});
// No ClientCredentials in the configured section implies pure User-Assigned Managed Identity:
// the bot's ClientId is the UMI's clientId (as in ABS bots with the UserAssignedMSI app type).
// Register ManagedIdentityOptions so BotAuthenticationHandler routes token acquisition through
// the IMDS endpoint instead of the standard app-credentials flow.
if (botConfig.IsUserAssignedManagedIdentity)
{
LogFromServices(services, l => l.InferringUserAssignedManagedIdentity(botConfig.ClientId));
services.Configure(botConfig.SectionName, options =>
{
options.UserAssignedClientId = botConfig.ClientId;
});
}
}
return services;
}
///
/// Registers a typed for wired to bot authentication
/// using an already-resolved .
///
///
/// must be called on the same service
/// collection before the resulting client is used, so that IAuthorizationHeaderProvider and the
/// named MSAL options are registered.
///
/// The client class to register the named for.
/// The service collection to add services to.
/// The named registration to associate with .
/// The resolved bot configuration containing tenant and client settings.
/// The service collection for method chaining.
public static IServiceCollection AddBotClient(
this IServiceCollection services,
string httpClientName,
BotConfig botConfig) where TClient : class
{
ArgumentNullException.ThrowIfNull(botConfig);
if (!string.IsNullOrWhiteSpace(botConfig.ClientId))
{
services.AddHttpClient(httpClientName)
.AddHttpMessageHandler(sp => new BotAuthenticationHandler(
sp.GetRequiredService(),
sp.GetRequiredService>(),
botConfig.SectionName,
sp.GetService>()));
}
else
{
services.AddHttpClient(httpClientName);
}
return services;
}
///
/// Registers a named wired to bot authentication
/// using an already-resolved , without binding it to a typed client.
/// Use this when the client type will be registered separately via a factory.
///
/// The service collection to add services to.
/// The logical name for this registration.
/// The resolved bot configuration containing tenant and client settings.
/// The service collection for method chaining.
public static IServiceCollection AddBotHttpClient(
this IServiceCollection services,
string httpClientName,
BotConfig botConfig)
{
ArgumentNullException.ThrowIfNull(botConfig);
if (!string.IsNullOrWhiteSpace(botConfig.ClientId))
{
services.AddHttpClient(httpClientName)
.AddHttpMessageHandler(sp => new BotAuthenticationHandler(
sp.GetRequiredService(),
sp.GetRequiredService>(),
botConfig.SectionName,
sp.GetService>()));
}
else
{
services.AddHttpClient(httpClientName);
}
return services;
}
///
/// Resolves a service from the service collection before the host is built,
/// preferring a direct instance and falling back to building a temporary
/// when the service is registered via factory or type.
///
///
/// The temporary is disposed before the method returns.
/// Only use this for services whose resolved instances remain valid after their
/// owning provider is disposed (e.g. ). Do NOT use for
/// disposable services like — see
/// for that case.
///
internal static T? ResolveFromServicesPreHost(IServiceCollection services) where T : class
{
ServiceDescriptor? descriptor = services.LastOrDefault(d => d.ServiceType == typeof(T));
if (descriptor is null)
{
return null;
}
if (descriptor.ImplementationInstance is T instance)
{
return instance;
}
using ServiceProvider tempProvider = services.BuildServiceProvider();
return tempProvider.GetService();
}
internal static void LogFromServices(IServiceCollection services, Action action, Type? categoryType = null)
{
ServiceDescriptor? descriptor = services.LastOrDefault(d => d.ServiceType == typeof(ILoggerFactory));
if (descriptor is null)
{
action(NullLogger.Instance);
return;
}
if (descriptor.ImplementationInstance is ILoggerFactory directFactory)
{
action(directFactory.CreateLogger(categoryType ?? typeof(AddBotApplicationExtensions)));
return;
}
using ServiceProvider tempProvider = services.BuildServiceProvider();
ILoggerFactory? factory = tempProvider.GetService();
action(factory?.CreateLogger(categoryType ?? typeof(AddBotApplicationExtensions)) ?? NullLogger.Instance);
}
}