// 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); } }