From 0c667a1d9634c24f468ef7e493de0518bfb2f9c6 Mon Sep 17 00:00:00 2001 From: Kavin Singh Date: Fri, 20 Mar 2026 13:25:04 -0700 Subject: [PATCH 1/7] refactor hosting --- core/samples/CompatBot/Program.cs | 2 +- .../TeamsBotApplication.HostingExtensions.cs | 31 +-- .../Hosting/AddBotApplicationExtensions.cs | 239 ++++-------------- .../Hosting/BotAuthenticationHandler.cs | 2 +- .../Hosting/BotConfig.cs | 108 ++++++-- .../Hosting/JwtExtensions.cs | 47 +++- .../Hosting/MsalConfigurationExtensions.cs | 188 ++++++++++++++ .../AddBotApplicationExtensionsTests.cs | 12 +- 8 files changed, 388 insertions(+), 241 deletions(-) create mode 100644 core/src/Microsoft.Teams.Bot.Core/Hosting/MsalConfigurationExtensions.cs diff --git a/core/samples/CompatBot/Program.cs b/core/samples/CompatBot/Program.cs index 673bea7e..b88dff7e 100644 --- a/core/samples/CompatBot/Program.cs +++ b/core/samples/CompatBot/Program.cs @@ -34,7 +34,7 @@ compatAdapter.Use(new MyCompatMiddleware()); app.MapPost("/api/messages", async (IBotFrameworkHttpAdapter adapter, IBot bot, HttpRequest request, HttpResponse response, CancellationToken ct) => - await adapter.ProcessAsync(request, response, bot, ct)); + await adapter.ProcessAsync(request, response, bot, ct)).RequireAuthorization(); app.MapGet("/api/notify/{cid}", async (IBotFrameworkHttpAdapter adapter, string cid, CancellationToken ct) => { diff --git a/core/src/Microsoft.Teams.Bot.Apps/TeamsBotApplication.HostingExtensions.cs b/core/src/Microsoft.Teams.Bot.Apps/TeamsBotApplication.HostingExtensions.cs index 2d43b5da..8e8bc830 100644 --- a/core/src/Microsoft.Teams.Bot.Apps/TeamsBotApplication.HostingExtensions.cs +++ b/core/src/Microsoft.Teams.Bot.Apps/TeamsBotApplication.HostingExtensions.cs @@ -2,11 +2,7 @@ // Licensed under the MIT License. using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Identity.Abstractions; using Microsoft.Teams.Bot.Core.Hosting; namespace Microsoft.Teams.Bot.Apps; @@ -47,30 +43,13 @@ public static IServiceCollection AddTeamsBotApplication(this IServiceCollection /// The updated WebApplicationBuilder instance. public static IServiceCollection AddTeamsBotApplication(this IServiceCollection services, string sectionName = "AzureAd") where TApp : TeamsBotApplication { - // Register options to defer configuration reading until ServiceProvider is built - services.AddOptions() - .Configure((options, configuration) => - { - options.Scope = "https://api.botframework.com/.default"; - if (!string.IsNullOrEmpty(configuration[$"{sectionName}:Scope"])) - options.Scope = configuration[$"{sectionName}:Scope"]!; - if (!string.IsNullOrEmpty(configuration["Scope"])) - options.Scope = configuration["Scope"]!; - options.SectionName = sectionName; - }); + // Resolve BotConfig to get authentication configuration + BotConfig botConfig = BotConfig.Resolve(services, sectionName); - services.AddHttpClient(TeamsApiClient.TeamsHttpClientName) - .AddHttpMessageHandler(sp => - { - BotClientOptions options = sp.GetRequiredService>().Value; - return new BotAuthenticationHandler( - sp.GetRequiredService(), - sp.GetRequiredService>(), - options.Scope, - sp.GetService>()); - }); + // Reuse AddBotClient infrastructure for TeamsApiClient + services.AddBotClient(TeamsApiClient.TeamsHttpClientName, botConfig); - services.AddBotApplication(); + services.AddBotApplication(sectionName); return services; } diff --git a/core/src/Microsoft.Teams.Bot.Core/Hosting/AddBotApplicationExtensions.cs b/core/src/Microsoft.Teams.Bot.Core/Hosting/AddBotApplicationExtensions.cs index d7624b5b..a772b9e5 100644 --- a/core/src/Microsoft.Teams.Bot.Core/Hosting/AddBotApplicationExtensions.cs +++ b/core/src/Microsoft.Teams.Bot.Core/Hosting/AddBotApplicationExtensions.cs @@ -24,8 +24,6 @@ namespace Microsoft.Teams.Bot.Core.Hosting; /// methods are called in the application's service configuration pipeline. public static class AddBotApplicationExtensions { - internal const string MsalConfigKey = "AzureAd"; - /// /// Initializes the default route /// @@ -91,24 +89,20 @@ public static IServiceCollection AddBotApplication(this IServiceCollection servi /// public static IServiceCollection AddBotApplication(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() - ?? (ILogger)Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance; + BotConfig botConfig = BotConfig.Resolve(services, sectionName); services.AddSingleton(sp => { IConfiguration config = sp.GetRequiredService(); return new BotApplicationOptions { - AppId = config["MicrosoftAppId"] ?? config["CLIENT_ID"] ?? config[$"{sectionName}:ClientId"] ?? string.Empty + AppId = botConfig.ClientId }; }); services.AddHttpContextAccessor(); - services.AddBotAuthorization(sectionName, logger); - services.AddConversationClient(sectionName); - services.AddUserTokenClient(sectionName); + services.AddBotAuthorization(aadSectionName: botConfig.SectionName); + services.AddConversationClient(botConfig); + services.AddUserTokenClient(botConfig); services.AddSingleton(); return services; } @@ -119,8 +113,11 @@ public static IServiceCollection AddBotApplication(this IServiceCollection /// service collection /// Configuration Section name, defaults to AzureAD /// - public static IServiceCollection AddConversationClient(this IServiceCollection services, string sectionName = "AzureAd") => - services.AddBotClient(ConversationClient.ConversationHttpClientName, sectionName); + public static IServiceCollection AddConversationClient(this IServiceCollection services, string sectionName = "AzureAd") + { + BotConfig botConfig = BotConfig.Resolve(services, sectionName); + return services.AddConversationClient(botConfig); + } /// /// Adds user token client to the service collection. @@ -128,24 +125,35 @@ public static IServiceCollection AddConversationClient(this IServiceCollection s /// service collection /// Configuration Section name, defaults to AzureAD /// - public static IServiceCollection AddUserTokenClient(this IServiceCollection services, string sectionName = "AzureAd") => - services.AddBotClient(UserTokenClient.UserTokenHttpClientName, sectionName); + public static IServiceCollection AddUserTokenClient(this IServiceCollection services, string sectionName = "AzureAd") + { + BotConfig botConfig = BotConfig.Resolve(services, sectionName); + return services.AddUserTokenClient(botConfig); + } - private static IServiceCollection AddBotClient( + /// + /// Adds conversation client to the service collection using an already-resolved BotConfig. + /// + private static IServiceCollection AddConversationClient(this IServiceCollection services, BotConfig botConfig) => + services.AddBotClient(ConversationClient.ConversationHttpClientName, botConfig); + + /// + /// Adds user token client to the service collection using an already-resolved BotConfig. + /// + private static IServiceCollection AddUserTokenClient(this IServiceCollection services, BotConfig botConfig) => + services.AddBotClient(UserTokenClient.UserTokenHttpClientName, botConfig); + + internal static IServiceCollection AddBotClient( this IServiceCollection services, string httpClientName, - string sectionName) where TClient : class + BotConfig botConfig) where TClient : class { - // Register options to defer scope configuration reading + // Register options using values from BotConfig services.AddOptions() - .Configure((options, configuration) => + .Configure(options => { - options.Scope = "https://api.botframework.com/.default"; - if (!string.IsNullOrEmpty(configuration[$"{sectionName}:Scope"])) - options.Scope = configuration[$"{sectionName}:Scope"]!; - if (!string.IsNullOrEmpty(configuration["Scope"])) - options.Scope = configuration["Scope"]!; - options.SectionName = sectionName; + options.Scope = botConfig.Scope; + options.SectionName = botConfig.SectionName; }); services @@ -154,28 +162,9 @@ private static IServiceCollection AddBotClient( .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(); - if (loggerFactory == null) - { - logger = tempProvider.GetRequiredService().CreateLogger(typeof(AddBotApplicationExtensions)); - } - } + ILogger logger = GetLoggerFromServices(services); - // Configure MSAL during registration (not deferred) - if (services.ConfigureMSAL(configuration, sectionName, logger)) + if (services.ConfigureMSAL(botConfig, logger)) { services.AddHttpClient(httpClientName) .AddHttpMessageHandler(sp => @@ -190,157 +179,27 @@ private static IServiceCollection AddBotClient( } else { - _logAuthConfigNotFound(logger, null); + _logAuthConfigNotFound(logger, httpClientName, null); services.AddHttpClient(httpClientName); } return services; } - private static bool ConfigureMSAL(this IServiceCollection services, IConfiguration configuration, string sectionName, ILogger logger) - { - ArgumentNullException.ThrowIfNull(configuration); - - if (configuration["MicrosoftAppId"] is not null) - { - _logUsingBFConfig(logger, null); - BotConfig botConfig = BotConfig.FromBFConfig(configuration); - services.ConfigureMSALFromBotConfig(botConfig, logger); - } - else if (configuration["CLIENT_ID"] is not null) - { - _logUsingCoreConfig(logger, null); - BotConfig botConfig = BotConfig.FromCoreConfig(configuration); - services.ConfigureMSALFromBotConfig(botConfig, logger); - } - else - { - _logUsingSectionConfig(logger, sectionName, null); - services.ConfigureMSALFromConfig(configuration.GetSection(sectionName)); - } - return true; - } - - private static IServiceCollection ConfigureMSALFromConfig(this IServiceCollection services, IConfigurationSection msalConfigSection) - { - ArgumentNullException.ThrowIfNull(msalConfigSection); - services.Configure(MsalConfigKey, msalConfigSection); - return services; - } - - private static IServiceCollection ConfigureMSALWithSecret(this IServiceCollection services, string tenantId, string clientId, string clientSecret) - { - ArgumentException.ThrowIfNullOrWhiteSpace(tenantId); - ArgumentException.ThrowIfNullOrWhiteSpace(clientId); - ArgumentException.ThrowIfNullOrWhiteSpace(clientSecret); - - services.Configure(MsalConfigKey, options => - { - options.Instance = "https://login.microsoftonline.com/"; - options.TenantId = tenantId; - options.ClientId = clientId; - options.ClientCredentials = [ - new CredentialDescription() - { - SourceType = CredentialSource.ClientSecret, - ClientSecret = clientSecret - } - ]; - }); - return services; - } - - private static IServiceCollection ConfigureMSALWithFIC(this IServiceCollection services, string tenantId, string clientId, string? ficClientId) - { - ArgumentException.ThrowIfNullOrWhiteSpace(tenantId); - ArgumentException.ThrowIfNullOrWhiteSpace(clientId); - - CredentialDescription ficCredential = new() - { - SourceType = CredentialSource.SignedAssertionFromManagedIdentity, - }; - if (!string.IsNullOrEmpty(ficClientId) && !IsSystemAssignedManagedIdentity(ficClientId)) - { - ficCredential.ManagedIdentityClientId = ficClientId; - } - - services.Configure(MsalConfigKey, options => - { - options.Instance = "https://login.microsoftonline.com/"; - options.TenantId = tenantId; - options.ClientId = clientId; - options.ClientCredentials = [ - ficCredential - ]; - }); - return services; - } - - private static IServiceCollection ConfigureMSALWithUMI(this IServiceCollection services, string tenantId, string clientId, string? managedIdentityClientId = null) - { - ArgumentNullException.ThrowIfNullOrWhiteSpace(tenantId); - ArgumentNullException.ThrowIfNullOrWhiteSpace(clientId); - - // Register ManagedIdentityOptions for BotAuthenticationHandler to use - bool isSystemAssigned = IsSystemAssignedManagedIdentity(managedIdentityClientId); - string? umiClientId = isSystemAssigned ? null : (managedIdentityClientId ?? clientId); - - services.Configure(options => - { - options.UserAssignedClientId = umiClientId; - }); - - services.Configure(MsalConfigKey, options => - { - options.Instance = "https://login.microsoftonline.com/"; - options.TenantId = tenantId; - options.ClientId = clientId; - }); - return services; - } - - private static IServiceCollection ConfigureMSALFromBotConfig(this IServiceCollection services, BotConfig botConfig, ILogger logger) - { - ArgumentNullException.ThrowIfNull(botConfig); - if (!string.IsNullOrEmpty(botConfig.ClientSecret)) - { - _logUsingClientSecret(logger, null); - services.ConfigureMSALWithSecret(botConfig.TenantId, botConfig.ClientId, botConfig.ClientSecret); - } - else if (string.IsNullOrEmpty(botConfig.FicClientId) || botConfig.FicClientId == botConfig.ClientId) - { - _logUsingUMI(logger, null); - services.ConfigureMSALWithUMI(botConfig.TenantId, botConfig.ClientId, botConfig.FicClientId); - } - else - { - bool isSystemAssigned = IsSystemAssignedManagedIdentity(botConfig.FicClientId); - _logUsingFIC(logger, isSystemAssigned ? "System-Assigned" : "User-Assigned", null); - services.ConfigureMSALWithFIC(botConfig.TenantId, botConfig.ClientId, botConfig.FicClientId); - } - return services; - } - /// - /// Determines if the provided client ID represents a system-assigned managed identity. + /// Gets a logger instance from the service collection without building the service provider. /// - private static bool IsSystemAssignedManagedIdentity(string? clientId) - => string.Equals(clientId, BotConfig.SystemManagedIdentityIdentifier, StringComparison.OrdinalIgnoreCase); - - private static readonly Action _logUsingBFConfig = - LoggerMessage.Define(LogLevel.Debug, new(1), "Configuring MSAL from Bot Framework configuration"); - private static readonly Action _logUsingCoreConfig = - LoggerMessage.Define(LogLevel.Debug, new(2), "Configuring MSAL from Core bot configuration"); - private static readonly Action _logUsingSectionConfig = - LoggerMessage.Define(LogLevel.Debug, new(3), "Configuring MSAL from {SectionName} configuration section"); - private static readonly Action _logUsingClientSecret = - LoggerMessage.Define(LogLevel.Debug, new(4), "Configuring authentication with client secret"); - private static readonly Action _logUsingUMI = - LoggerMessage.Define(LogLevel.Debug, new(5), "Configuring authentication with User-Assigned Managed Identity"); - private static readonly Action _logUsingFIC = - LoggerMessage.Define(LogLevel.Debug, new(6), "Configuring authentication with Federated Identity Credential (Managed Identity) with {IdentityType} Managed Identity"); - private static readonly Action _logAuthConfigNotFound = - LoggerMessage.Define(LogLevel.Warning, new(7), "Authentication configuration not found. Running without Auth"); - + /// The service collection to extract the logger from. + /// The type to use for the logger category. If null, uses AddBotApplicationExtensions. + /// An ILogger instance, or NullLogger if no logger factory is registered. + private static ILogger GetLoggerFromServices(IServiceCollection services, Type? categoryType = null) + { + ServiceDescriptor? loggerFactoryDescriptor = services.FirstOrDefault(d => d.ServiceType == typeof(ILoggerFactory)); + ILoggerFactory? loggerFactory = loggerFactoryDescriptor?.ImplementationInstance as ILoggerFactory; + return loggerFactory?.CreateLogger(categoryType ?? typeof(AddBotApplicationExtensions)) + ?? Extensions.Logging.Abstractions.NullLogger.Instance; + } + private static readonly Action _logAuthConfigNotFound = + LoggerMessage.Define(LogLevel.Warning, new(7), "Authentication configuration not found. Outgoing requests from '{HttpClientName}' will not be authenticated."); } diff --git a/core/src/Microsoft.Teams.Bot.Core/Hosting/BotAuthenticationHandler.cs b/core/src/Microsoft.Teams.Bot.Core/Hosting/BotAuthenticationHandler.cs index 4dd8d07e..5005f75d 100644 --- a/core/src/Microsoft.Teams.Bot.Core/Hosting/BotAuthenticationHandler.cs +++ b/core/src/Microsoft.Teams.Bot.Core/Hosting/BotAuthenticationHandler.cs @@ -75,7 +75,7 @@ private async Task GetAuthorizationHeaderAsync(AgenticIdentity? agenticI { AcquireTokenOptions = new AcquireTokenOptions() { - AuthenticationOptionsName = AddBotApplicationExtensions.MsalConfigKey, + AuthenticationOptionsName = MsalConfigurationExtensions.MsalConfigKey, } }; diff --git a/core/src/Microsoft.Teams.Bot.Core/Hosting/BotConfig.cs b/core/src/Microsoft.Teams.Bot.Core/Hosting/BotConfig.cs index 48386ced..642de155 100644 --- a/core/src/Microsoft.Teams.Bot.Core/Hosting/BotConfig.cs +++ b/core/src/Microsoft.Teams.Bot.Core/Hosting/BotConfig.cs @@ -2,6 +2,9 @@ // Licensed under the MIT License. using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; namespace Microsoft.Teams.Bot.Core.Hosting; @@ -10,7 +13,7 @@ namespace Microsoft.Teams.Bot.Core.Hosting; /// /// /// This class consolidates bot authentication settings from various configuration sources including -/// Bot Framework SDK configuration, Core configuration, and Azure AD configuration sections. +/// Bot Framework SDK configuration, Core configuration, and MSAL configuration sections. /// It supports multiple authentication modes: client secrets, system-assigned managed identities, /// user-assigned managed identities, and federated identity credentials (FIC). /// @@ -22,6 +25,10 @@ internal sealed class BotConfig /// public const string SystemManagedIdentityIdentifier = "system"; + private const string BotScope = "https://api.botframework.com/.default"; + + private const string DefaultSectionName = "AzureAd"; + /// /// Gets or sets the Azure AD tenant ID. /// @@ -44,6 +51,19 @@ internal sealed class BotConfig /// public string? FicClientId { get; set; } + /// + /// Gets or sets the configuration section name used to resolve this BotConfig. + /// + public string SectionName { get; set; } = DefaultSectionName; + + /// + /// Gets or sets the scope for token acquisition. + /// Defaults to "https://api.botframework.com/.default" if not specified. + /// + public string Scope { get; set; } = BotScope; + + internal IConfigurationSection? MsalConfigurationSection { get; set; } + /// /// Creates a BotConfig from Bot Framework SDK configuration format. /// @@ -58,6 +78,7 @@ public static BotConfig FromBFConfig(IConfiguration configuration) TenantId = configuration["MicrosoftAppTenantId"] ?? string.Empty, ClientId = configuration["MicrosoftAppId"] ?? string.Empty, ClientSecret = configuration["MicrosoftAppPassword"], + Scope = configuration["Scope"] ?? BotScope }; } @@ -80,21 +101,22 @@ public static BotConfig FromCoreConfig(IConfiguration configuration) ClientId = configuration["CLIENT_ID"] ?? string.Empty, ClientSecret = configuration["CLIENT_SECRET"], FicClientId = configuration["MANAGED_IDENTITY_CLIENT_ID"], + Scope = configuration["Scope"] ?? BotScope, }; } /// - /// Creates a BotConfig from Azure AD configuration section format. + /// Creates a BotConfig from MSAL configuration section format. /// - /// Configuration containing an Azure AD configuration section. - /// The name of the configuration section containing Azure AD settings. Defaults to "AzureAd". - /// A new BotConfig instance with settings from the Azure AD configuration section. + /// Configuration containing an MSAL configuration section. + /// The name of the configuration section containing MSAL settings. Defaults to "AzureAd". + /// A new BotConfig instance with settings from the MSAL configuration section. /// Thrown when is null. /// /// This format is compatible with Microsoft.Identity.Web configuration sections in appsettings.json. /// The section should contain TenantId, ClientId, and optionally ClientSecret properties. /// - public static BotConfig FromAadConfig(IConfiguration configuration, string sectionName = "AzureAd") + public static BotConfig FromMsalConfig(IConfiguration configuration, string sectionName = "AzureAd") { ArgumentNullException.ThrowIfNull(configuration); IConfigurationSection section = configuration.GetSection(sectionName); @@ -103,30 +125,84 @@ public static BotConfig FromAadConfig(IConfiguration configuration, string secti TenantId = section["TenantId"] ?? string.Empty, ClientId = section["ClientId"] ?? string.Empty, ClientSecret = section["ClientSecret"], + Scope = section["Scope"] ?? configuration["Scope"] ?? BotScope, + MsalConfigurationSection = section, + SectionName = sectionName }; } + /// + /// Resolves a BotConfig from a service collection by extracting configuration and logger, + /// then trying all configuration formats in priority order. + /// + /// The service collection containing IConfiguration and ILoggerFactory registrations. + /// The MSAL configuration section name. Defaults to "AzureAd". + /// The first BotConfig with a non-empty ClientId, or a BotConfig with empty ClientId if none is found. + public static BotConfig Resolve(IServiceCollection services, string sectionName = "AzureAd") + { + ArgumentNullException.ThrowIfNull(services); + + // Extract IConfiguration from service collection + ServiceDescriptor? configDescriptor = services.FirstOrDefault(d => d.ServiceType == typeof(IConfiguration)); + IConfiguration configuration = configDescriptor?.ImplementationInstance as IConfiguration + ?? services.BuildServiceProvider().GetRequiredService(); + + // Extract ILogger from service collection + ServiceDescriptor? loggerFactoryDescriptor = services.FirstOrDefault(d => d.ServiceType == typeof(ILoggerFactory)); + ILoggerFactory? loggerFactory = loggerFactoryDescriptor?.ImplementationInstance as ILoggerFactory; + ILogger logger = loggerFactory?.CreateLogger(typeof(BotConfig)) + ?? NullLogger.Instance; + + return Resolve(configuration, sectionName, logger); + } + /// /// Resolves a BotConfig by trying all configuration formats in priority order: - /// AzureAd section, Core environment variables, then Bot Framework SDK keys. + /// Bot Framework SDK keys, Core environment variables, then MSAL section. /// /// The application configuration. - /// The AAD configuration section name. Defaults to "AzureAd". - /// The first BotConfig with a non-empty ClientId. - /// Thrown when no ClientId is found in any configuration format. - public static BotConfig Resolve(IConfiguration configuration, string sectionName = "AzureAd") + /// The MSAL configuration section name. Defaults to "AzureAd". + /// Optional logger to log which configuration source was used. + /// The first BotConfig with a non-empty ClientId, or a BotConfig with empty ClientId if none is found. + public static BotConfig Resolve(IConfiguration configuration, string sectionName = "AzureAd", ILogger? logger = null) { ArgumentNullException.ThrowIfNull(configuration); + logger ??= NullLogger.Instance; - BotConfig config = FromAadConfig(configuration, sectionName); - if (!string.IsNullOrEmpty(config.ClientId)) return config; + BotConfig config = FromMsalConfig(configuration, sectionName); + if (!string.IsNullOrEmpty(config.ClientId)) + { + _logUsingSectionConfig(logger, sectionName, null); + config.SectionName = sectionName; + return config; + } config = FromCoreConfig(configuration); - if (!string.IsNullOrEmpty(config.ClientId)) return config; + if (!string.IsNullOrEmpty(config.ClientId)) + { + _logUsingCoreConfig(logger, null); + return config; + } config = FromBFConfig(configuration); - if (!string.IsNullOrEmpty(config.ClientId)) return config; + if (!string.IsNullOrEmpty(config.ClientId)) + { + _logUsingBFConfig(logger, null); + return config; + } - throw new InvalidOperationException("ClientID not found in configuration."); + // No configuration found - log warning and return empty config + _logNoConfigFound(logger, sectionName, null); + return new BotConfig { SectionName = sectionName }; } + + private static readonly Action _logUsingBFConfig = + LoggerMessage.Define(LogLevel.Debug, new(1), "Resolved bot configuration from Bot Framework configuration keys"); + private static readonly Action _logUsingCoreConfig = + LoggerMessage.Define(LogLevel.Debug, new(2), "Resolved bot configuration from Core environment variables"); + private static readonly Action _logUsingSectionConfig = + LoggerMessage.Define(LogLevel.Debug, new(3), "Resolved bot configuration from '{SectionName}' configuration section"); + private static readonly Action _logNoConfigFound = + LoggerMessage.Define(LogLevel.Warning, new(4), "No bot configuration found in '{SectionName}' configuration section"); + } diff --git a/core/src/Microsoft.Teams.Bot.Core/Hosting/JwtExtensions.cs b/core/src/Microsoft.Teams.Bot.Core/Hosting/JwtExtensions.cs index 69fc63c8..56557eb1 100644 --- a/core/src/Microsoft.Teams.Bot.Core/Hosting/JwtExtensions.cs +++ b/core/src/Microsoft.Teams.Bot.Core/Hosting/JwtExtensions.cs @@ -77,7 +77,14 @@ public static AuthenticationBuilder AddBotAuthentication( string schemeName = "AzureAd", ILogger? logger = null) { - builder.AddTeamsJwtBearer(schemeName, clientId, tenantId, logger); + if (string.IsNullOrWhiteSpace(clientId)) + { + builder.AddBypassAuthentication(schemeName, logger); + } + else + { + builder.AddTeamsJwtBearer(schemeName, clientId, tenantId, logger); + } return builder; } @@ -261,6 +268,44 @@ private static AuthenticationBuilder AddTeamsJwtBearer(this AuthenticationBuilde return builder; } + /// + /// Adds a bypass authentication scheme that always succeeds without validating tokens. + /// This is used when no ClientId is configured and should only be used for development. + /// + private static AuthenticationBuilder AddBypassAuthentication(this AuthenticationBuilder builder, string schemeName, ILogger? logger = null) + { + (logger ?? NullLogger.Instance).LogWarning("ClientId not provided for scheme '{SchemeName}'. Configuring bypass authentication (no token validation). This is INSECURE and should only be used for development.", schemeName); + + builder.AddJwtBearer(schemeName, jwtOptions => + { +#pragma warning disable CA5404 // Do not disable token validation checks + jwtOptions.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = false, + ValidateAudience = false, + ValidateLifetime = false, + ValidateIssuerSigningKey = false, + RequireSignedTokens = false, + SignatureValidator = (token, _) => new JsonWebToken(token) + }; +#pragma warning restore CA5404 // Do not disable token validation checks + jwtOptions.Events = new JwtBearerEvents + { + OnMessageReceived = context => + { + // Always succeed authentication even without a token + GetLogger(context.HttpContext, logger).LogWarning("Using bypass authentication scheme succeeded for scheme: {Scheme}. This is INSECURE and should only be used for development.", schemeName); + context.NoResult(); + context.Principal = new System.Security.Claims.ClaimsPrincipal( + new System.Security.Claims.ClaimsIdentity("BypassAuth")); + context.Success(); + return Task.CompletedTask; + } + }; + }); + return builder; + } + private static BotConfig ResolveBotConfig(IServiceCollection services, string sectionName) { ServiceDescriptor? configDescriptor = services.FirstOrDefault(d => d.ServiceType == typeof(IConfiguration)); diff --git a/core/src/Microsoft.Teams.Bot.Core/Hosting/MsalConfigurationExtensions.cs b/core/src/Microsoft.Teams.Bot.Core/Hosting/MsalConfigurationExtensions.cs new file mode 100644 index 00000000..7a5fa88a --- /dev/null +++ b/core/src/Microsoft.Teams.Bot.Core/Hosting/MsalConfigurationExtensions.cs @@ -0,0 +1,188 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Identity.Abstractions; +using Microsoft.Identity.Web; + +namespace Microsoft.Teams.Bot.Core.Hosting; + +/// +/// Provides extension methods for configuring MSAL (Microsoft Authentication Library) with different credential types. +/// +internal static class MsalConfigurationExtensions +{ + internal const string MsalConfigKey = "AzureAd"; + /// + /// Checks if MSAL has already been configured in the service collection. + /// + /// The service collection to check. + /// True if MSAL configuration services are registered, false otherwise. + internal static bool IsMsalConfigured(this IServiceCollection services) + { + // Check if there's any IConfigureOptions registered + // This indicates that MSAL configuration has been set up + return services.Any(descriptor => + descriptor.ServiceType == typeof(IConfigureOptions) || + descriptor.ServiceType == typeof(IPostConfigureOptions)); + } + + /// + /// Configures MSAL authentication based on the provided BotConfig. + /// + /// The service collection to configure. + /// The bot configuration containing authentication settings. + /// Logger for configuration messages. + /// True if MSAL was configured, false if ClientId is not present. + internal static bool ConfigureMSAL(this IServiceCollection services, BotConfig botConfig, ILogger logger) + { + ArgumentNullException.ThrowIfNull(botConfig); + + if (string.IsNullOrWhiteSpace(botConfig.ClientId)) + { + // Don't configure MSAL if ClientId is not present + return false; + } + else if (botConfig.MsalConfigurationSection != null) + { + services.ConfigureMSALFromConfig(botConfig.MsalConfigurationSection); + } + else + { + services.ConfigureMSALFromBotConfig(botConfig, logger); + } + + return true; + } + + /// + /// Configures MSAL from an IConfigurationSection. + /// + private static IServiceCollection ConfigureMSALFromConfig(this IServiceCollection services, IConfigurationSection msalConfigSection) + { + ArgumentNullException.ThrowIfNull(msalConfigSection); + services.Configure(MsalConfigKey, msalConfigSection); + return services; + } + + /// + /// Configures MSAL with client secret authentication. + /// + private static IServiceCollection ConfigureMSALWithSecret(this IServiceCollection services, string tenantId, string clientId, string clientSecret) + { + ArgumentException.ThrowIfNullOrWhiteSpace(tenantId); + ArgumentException.ThrowIfNullOrWhiteSpace(clientId); + ArgumentException.ThrowIfNullOrWhiteSpace(clientSecret); + + services.Configure(MsalConfigKey, options => + { + options.Instance = "https://login.microsoftonline.com/"; + options.TenantId = tenantId; + options.ClientId = clientId; + options.ClientCredentials = [ + new CredentialDescription() + { + SourceType = CredentialSource.ClientSecret, + ClientSecret = clientSecret + } + ]; + }); + return services; + } + + /// + /// Configures MSAL with Federated Identity Credential (FIC) authentication using managed identity. + /// + private static IServiceCollection ConfigureMSALWithFIC(this IServiceCollection services, string tenantId, string clientId, string? ficClientId) + { + ArgumentException.ThrowIfNullOrWhiteSpace(tenantId); + ArgumentException.ThrowIfNullOrWhiteSpace(clientId); + + CredentialDescription ficCredential = new() + { + SourceType = CredentialSource.SignedAssertionFromManagedIdentity, + }; + if (!string.IsNullOrEmpty(ficClientId) && !IsSystemAssignedManagedIdentity(ficClientId)) + { + ficCredential.ManagedIdentityClientId = ficClientId; + } + + services.Configure(MsalConfigKey, options => + { + options.Instance = "https://login.microsoftonline.com/"; + options.TenantId = tenantId; + options.ClientId = clientId; + options.ClientCredentials = [ + ficCredential + ]; + }); + return services; + } + + /// + /// Configures MSAL with User-Assigned Managed Identity (UMI) authentication. + /// + private static IServiceCollection ConfigureMSALWithUMI(this IServiceCollection services, string tenantId, string clientId, string? managedIdentityClientId = null) + { + ArgumentNullException.ThrowIfNullOrWhiteSpace(tenantId); + ArgumentNullException.ThrowIfNullOrWhiteSpace(clientId); + + // Register ManagedIdentityOptions for BotAuthenticationHandler to use + bool isSystemAssigned = IsSystemAssignedManagedIdentity(managedIdentityClientId); + string? umiClientId = isSystemAssigned ? null : (managedIdentityClientId ?? clientId); + + services.Configure(options => + { + options.UserAssignedClientId = umiClientId; + }); + + services.Configure(MsalConfigKey, options => + { + options.Instance = "https://login.microsoftonline.com/"; + options.TenantId = tenantId; + options.ClientId = clientId; + }); + return services; + } + + /// + /// Configures MSAL by selecting the appropriate credential type based on BotConfig properties. + /// + private static IServiceCollection ConfigureMSALFromBotConfig(this IServiceCollection services, BotConfig botConfig, ILogger logger) + { + ArgumentNullException.ThrowIfNull(botConfig); + if (!string.IsNullOrEmpty(botConfig.ClientSecret)) + { + _logUsingClientSecret(logger, null); + services.ConfigureMSALWithSecret(botConfig.TenantId, botConfig.ClientId, botConfig.ClientSecret); + } + else if (string.IsNullOrEmpty(botConfig.FicClientId) || botConfig.FicClientId == botConfig.ClientId) + { + _logUsingUMI(logger, null); + services.ConfigureMSALWithUMI(botConfig.TenantId, botConfig.ClientId, botConfig.FicClientId); + } + else + { + bool isSystemAssigned = IsSystemAssignedManagedIdentity(botConfig.FicClientId); + _logUsingFIC(logger, isSystemAssigned ? "System-Assigned" : "User-Assigned", null); + services.ConfigureMSALWithFIC(botConfig.TenantId, botConfig.ClientId, botConfig.FicClientId); + } + return services; + } + + /// + /// Determines if the provided client ID represents a system-assigned managed identity. + /// + private static bool IsSystemAssignedManagedIdentity(string? clientId) + => string.Equals(clientId, BotConfig.SystemManagedIdentityIdentifier, StringComparison.OrdinalIgnoreCase); + + private static readonly Action _logUsingClientSecret = + LoggerMessage.Define(LogLevel.Debug, new(1), "Configuring authentication with client secret"); + private static readonly Action _logUsingUMI = + LoggerMessage.Define(LogLevel.Debug, new(2), "Configuring authentication with User-Assigned Managed Identity"); + private static readonly Action _logUsingFIC = + LoggerMessage.Define(LogLevel.Debug, new(3), "Configuring authentication with Federated Identity Credential (Managed Identity) with {IdentityType} Managed Identity"); +} diff --git a/core/test/Microsoft.Teams.Bot.Core.UnitTests/Hosting/AddBotApplicationExtensionsTests.cs b/core/test/Microsoft.Teams.Bot.Core.UnitTests/Hosting/AddBotApplicationExtensionsTests.cs index ebef6800..1b2948ad 100644 --- a/core/test/Microsoft.Teams.Bot.Core.UnitTests/Hosting/AddBotApplicationExtensionsTests.cs +++ b/core/test/Microsoft.Teams.Bot.Core.UnitTests/Hosting/AddBotApplicationExtensionsTests.cs @@ -37,7 +37,7 @@ private static void AssertMsalOptions(ServiceProvider serviceProvider, string ex { MicrosoftIdentityApplicationOptions msalOptions = serviceProvider .GetRequiredService>() - .Get(AddBotApplicationExtensions.MsalConfigKey); + .Get(MsalConfigurationExtensions.MsalConfigKey); Assert.Equal(expectedClientId, msalOptions.ClientId); Assert.Equal(expectedTenantId, msalOptions.TenantId); Assert.Equal(expectedInstance, msalOptions.Instance); @@ -61,7 +61,7 @@ public void AddConversationClient_WithBotFrameworkConfig_ConfiguresClientSecret( AssertMsalOptions(serviceProvider, "test-app-id", "test-tenant-id"); MicrosoftIdentityApplicationOptions msalOptions = serviceProvider .GetRequiredService>() - .Get(AddBotApplicationExtensions.MsalConfigKey); + .Get(MsalConfigurationExtensions.MsalConfigKey); Assert.NotNull(msalOptions.ClientCredentials); Assert.Single(msalOptions.ClientCredentials); CredentialDescription credential = msalOptions.ClientCredentials.First(); @@ -87,7 +87,7 @@ public void AddConversationClient_WithCoreConfigAndClientSecret_ConfiguresClient AssertMsalOptions(serviceProvider, "test-client-id", "test-tenant-id"); MicrosoftIdentityApplicationOptions msalOptions = serviceProvider .GetRequiredService>() - .Get(AddBotApplicationExtensions.MsalConfigKey); + .Get(MsalConfigurationExtensions.MsalConfigKey); Assert.NotNull(msalOptions.ClientCredentials); Assert.Single(msalOptions.ClientCredentials); CredentialDescription credential = msalOptions.ClientCredentials.First(); @@ -113,7 +113,7 @@ public void AddConversationClient_WithCoreConfigAndSystemAssignedMI_ConfiguresSy AssertMsalOptions(serviceProvider, "test-client-id", "test-tenant-id"); MicrosoftIdentityApplicationOptions msalOptions = serviceProvider .GetRequiredService>() - .Get(AddBotApplicationExtensions.MsalConfigKey); + .Get(MsalConfigurationExtensions.MsalConfigKey); Assert.NotNull(msalOptions.ClientCredentials); Assert.Single(msalOptions.ClientCredentials); CredentialDescription credential = msalOptions.ClientCredentials.First(); @@ -142,7 +142,7 @@ public void AddConversationClient_WithCoreConfigAndUserAssignedMI_ConfiguresUser AssertMsalOptions(serviceProvider, "test-client-id", "test-tenant-id"); MicrosoftIdentityApplicationOptions msalOptions = serviceProvider .GetRequiredService>() - .Get(AddBotApplicationExtensions.MsalConfigKey); + .Get(MsalConfigurationExtensions.MsalConfigKey); Assert.NotNull(msalOptions.ClientCredentials); Assert.Single(msalOptions.ClientCredentials); CredentialDescription credential = msalOptions.ClientCredentials.First(); @@ -170,7 +170,7 @@ public void AddConversationClient_WithCoreConfigAndNoManagedIdentity_ConfiguresU AssertMsalOptions(serviceProvider, "test-client-id", "test-tenant-id"); MicrosoftIdentityApplicationOptions msalOptions = serviceProvider .GetRequiredService>() - .Get(AddBotApplicationExtensions.MsalConfigKey); + .Get(MsalConfigurationExtensions.MsalConfigKey); Assert.Null(msalOptions.ClientCredentials); ManagedIdentityOptions managedIdentityOptions = serviceProvider.GetRequiredService>().Value; From 623a4d8805c759b477ee9318ad92b4e7d909aef4 Mon Sep 17 00:00:00 2001 From: Kavin Singh Date: Fri, 20 Mar 2026 13:50:30 -0700 Subject: [PATCH 2/7] refactor logging setup --- .../Hosting/AddBotApplicationExtensions.cs | 19 ++++++++++++++++--- .../Hosting/BotConfig.cs | 17 ++++++++--------- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/core/src/Microsoft.Teams.Bot.Core/Hosting/AddBotApplicationExtensions.cs b/core/src/Microsoft.Teams.Bot.Core/Hosting/AddBotApplicationExtensions.cs index a772b9e5..3e77500b 100644 --- a/core/src/Microsoft.Teams.Bot.Core/Hosting/AddBotApplicationExtensions.cs +++ b/core/src/Microsoft.Teams.Bot.Core/Hosting/AddBotApplicationExtensions.cs @@ -1,12 +1,14 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Linq; 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; @@ -187,16 +189,27 @@ internal static IServiceCollection AddBotClient( } /// - /// Gets a logger instance from the service collection without building the service provider. + /// Gets a logger instance from the service collection. + /// If the logger factory is not available as an instance, builds a temporary service provider to create the logger. /// /// The service collection to extract the logger from. /// The type to use for the logger category. If null, uses AddBotApplicationExtensions. /// An ILogger instance, or NullLogger if no logger factory is registered. - private static ILogger GetLoggerFromServices(IServiceCollection services, Type? categoryType = null) + internal static ILogger GetLoggerFromServices(IServiceCollection services, Type? categoryType = null) { ServiceDescriptor? loggerFactoryDescriptor = services.FirstOrDefault(d => d.ServiceType == typeof(ILoggerFactory)); ILoggerFactory? loggerFactory = loggerFactoryDescriptor?.ImplementationInstance as ILoggerFactory; - return loggerFactory?.CreateLogger(categoryType ?? typeof(AddBotApplicationExtensions)) + + // If logger factory is available as an instance, use it directly + if (loggerFactory != null) + { + return loggerFactory.CreateLogger(categoryType ?? typeof(AddBotApplicationExtensions)); + } + + // Otherwise, build a temporary service provider to create the logger + using ServiceProvider tempProvider = services.BuildServiceProvider(); + ILoggerFactory? tempFactory = tempProvider.GetService(); + return (ILogger?)tempFactory?.CreateLogger(categoryType ?? typeof(AddBotApplicationExtensions)) ?? Extensions.Logging.Abstractions.NullLogger.Instance; } diff --git a/core/src/Microsoft.Teams.Bot.Core/Hosting/BotConfig.cs b/core/src/Microsoft.Teams.Bot.Core/Hosting/BotConfig.cs index 642de155..dfda863d 100644 --- a/core/src/Microsoft.Teams.Bot.Core/Hosting/BotConfig.cs +++ b/core/src/Microsoft.Teams.Bot.Core/Hosting/BotConfig.cs @@ -147,18 +147,15 @@ public static BotConfig Resolve(IServiceCollection services, string sectionName IConfiguration configuration = configDescriptor?.ImplementationInstance as IConfiguration ?? services.BuildServiceProvider().GetRequiredService(); - // Extract ILogger from service collection - ServiceDescriptor? loggerFactoryDescriptor = services.FirstOrDefault(d => d.ServiceType == typeof(ILoggerFactory)); - ILoggerFactory? loggerFactory = loggerFactoryDescriptor?.ImplementationInstance as ILoggerFactory; - ILogger logger = loggerFactory?.CreateLogger(typeof(BotConfig)) - ?? NullLogger.Instance; + // Get logger using the helper method from AddBotApplicationExtensions + ILogger logger = AddBotApplicationExtensions.GetLoggerFromServices(services, typeof(BotConfig)); return Resolve(configuration, sectionName, logger); } /// /// Resolves a BotConfig by trying all configuration formats in priority order: - /// Bot Framework SDK keys, Core environment variables, then MSAL section. + /// MSAL section, Core environment variables, then Bot Framework SDK keys. /// /// The application configuration. /// The MSAL configuration section name. Defaults to "AzureAd". @@ -181,6 +178,7 @@ public static BotConfig Resolve(IConfiguration configuration, string sectionName if (!string.IsNullOrEmpty(config.ClientId)) { _logUsingCoreConfig(logger, null); + config.SectionName = sectionName; return config; } @@ -188,11 +186,12 @@ public static BotConfig Resolve(IConfiguration configuration, string sectionName if (!string.IsNullOrEmpty(config.ClientId)) { _logUsingBFConfig(logger, null); + config.SectionName = sectionName; return config; } // No configuration found - log warning and return empty config - _logNoConfigFound(logger, sectionName, null); + _logNoConfigFound(logger, null); return new BotConfig { SectionName = sectionName }; } @@ -202,7 +201,7 @@ public static BotConfig Resolve(IConfiguration configuration, string sectionName LoggerMessage.Define(LogLevel.Debug, new(2), "Resolved bot configuration from Core environment variables"); private static readonly Action _logUsingSectionConfig = LoggerMessage.Define(LogLevel.Debug, new(3), "Resolved bot configuration from '{SectionName}' configuration section"); - private static readonly Action _logNoConfigFound = - LoggerMessage.Define(LogLevel.Warning, new(4), "No bot configuration found in '{SectionName}' configuration section"); + private static readonly Action _logNoConfigFound = + LoggerMessage.Define(LogLevel.Warning, new(4), "No bot configuration found in configuration."); } From 904c9de9b13f8a6e8b07a3a2bcdd857e54c6d42b Mon Sep 17 00:00:00 2001 From: Kavin Singh Date: Fri, 20 Mar 2026 14:13:06 -0700 Subject: [PATCH 3/7] add template --- .../Properties/launchSettings.TEMPLATE.json | 49 +++++++++++++++++++ .../Hosting/MsalConfigurationExtensions.cs | 13 ----- 2 files changed, 49 insertions(+), 13 deletions(-) create mode 100644 core/samples/TeamsBot/Properties/launchSettings.TEMPLATE.json diff --git a/core/samples/TeamsBot/Properties/launchSettings.TEMPLATE.json b/core/samples/TeamsBot/Properties/launchSettings.TEMPLATE.json new file mode 100644 index 00000000..a8990e53 --- /dev/null +++ b/core/samples/TeamsBot/Properties/launchSettings.TEMPLATE.json @@ -0,0 +1,49 @@ +{ + "profiles": { + "TeamsBot-MsalConfig": { + "commandName": "Project", + "launchBrowser": false, + "applicationUrl": "http://localhost:3978", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "Logging__LogLevel__Microsoft.Teams": "Trace", + "Logging__LogLevel__System.Net.Http.HttpClient": "Warning", + "AzureAd__Instance": "", + "AzureAd__Scope": "", + "AzureAd__ClientId": "", + "AzureAd__TenantId": "", + "AzureAd__ClientCredentials__0__SourceType": "ClientSecret", + "AzureAd__ClientCredentials__0__ClientSecret": "" + } + }, + "TeamsBot-CoreConfig": { + "commandName": "Project", + "launchBrowser": false, + "applicationUrl": "http://localhost:3978", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "Logging__LogLevel__Microsoft.Teams": "Trace", + "Logging__LogLevel__System.Net.Http.HttpClient": "Warning", + "Scope": "", + "TENANT_ID": "", + "CLIENT_ID": "", + "CLIENT_SECRET": "", + "MANAGED_IDENTITY_CLIENT_ID": "" + } + }, + "TeamsBot-BFConfig": { + "commandName": "Project", + "launchBrowser": false, + "applicationUrl": "http://localhost:3978", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "Logging__LogLevel__Microsoft.Teams": "Trace", + "Logging__LogLevel__System.Net.Http.HttpClient": "Warning", + "Scope": "", + "MicrosoftAppPassword": "", + "MicrosoftAppId": "", + "MicrosoftAppTenantId": "" + } + } + } +} diff --git a/core/src/Microsoft.Teams.Bot.Core/Hosting/MsalConfigurationExtensions.cs b/core/src/Microsoft.Teams.Bot.Core/Hosting/MsalConfigurationExtensions.cs index 7a5fa88a..fcf51e99 100644 --- a/core/src/Microsoft.Teams.Bot.Core/Hosting/MsalConfigurationExtensions.cs +++ b/core/src/Microsoft.Teams.Bot.Core/Hosting/MsalConfigurationExtensions.cs @@ -16,19 +16,6 @@ namespace Microsoft.Teams.Bot.Core.Hosting; internal static class MsalConfigurationExtensions { internal const string MsalConfigKey = "AzureAd"; - /// - /// Checks if MSAL has already been configured in the service collection. - /// - /// The service collection to check. - /// True if MSAL configuration services are registered, false otherwise. - internal static bool IsMsalConfigured(this IServiceCollection services) - { - // Check if there's any IConfigureOptions registered - // This indicates that MSAL configuration has been set up - return services.Any(descriptor => - descriptor.ServiceType == typeof(IConfigureOptions) || - descriptor.ServiceType == typeof(IPostConfigureOptions)); - } /// /// Configures MSAL authentication based on the provided BotConfig. From 7d4b44b31fe4acca3573a9f61dd0319b0d61a804 Mon Sep 17 00:00:00 2001 From: Kavin Singh Date: Fri, 20 Mar 2026 14:21:23 -0700 Subject: [PATCH 4/7] update test --- .../Hosting/AddBotApplicationExtensionsTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/test/Microsoft.Teams.Bot.Core.UnitTests/Hosting/AddBotApplicationExtensionsTests.cs b/core/test/Microsoft.Teams.Bot.Core.UnitTests/Hosting/AddBotApplicationExtensionsTests.cs index 1b2948ad..7c30a685 100644 --- a/core/test/Microsoft.Teams.Bot.Core.UnitTests/Hosting/AddBotApplicationExtensionsTests.cs +++ b/core/test/Microsoft.Teams.Bot.Core.UnitTests/Hosting/AddBotApplicationExtensionsTests.cs @@ -306,9 +306,9 @@ public void AddBotApplication_WithCustomSection_SetsAppIdFromCustomSection() } [Fact] - public void AddBotApplication_MicrosoftAppIdTakesPrecedenceOverClientId() + public void AddBotApplication_ClientIdTakesPrecedenceOverMicrosoftAppId() { - // Arrange — both keys present; MicrosoftAppId is highest priority + // Arrange — both keys present; CLIENT_ID is highest priority Dictionary configData = new() { ["MicrosoftAppId"] = "bf-app-id", @@ -321,6 +321,6 @@ public void AddBotApplication_MicrosoftAppIdTakesPrecedenceOverClientId() ServiceProvider serviceProvider = BuildServiceProviderForBotApp(configData); // Assert - Assert.Equal("bf-app-id", GetAppId(serviceProvider)); + Assert.Equal("core-client-id", GetAppId(serviceProvider)); } } From 1c298d5a458fb8fbbbd8ef57e8094d3c7c9834d7 Mon Sep 17 00:00:00 2001 From: Kavin Singh Date: Mon, 23 Mar 2026 11:58:30 -0700 Subject: [PATCH 5/7] remove xml comments from private methods --- .../Hosting/JwtExtensions.cs | 4 ---- .../Hosting/MsalConfigurationExtensions.cs | 18 ------------------ 2 files changed, 22 deletions(-) diff --git a/core/src/Microsoft.Teams.Bot.Core/Hosting/JwtExtensions.cs b/core/src/Microsoft.Teams.Bot.Core/Hosting/JwtExtensions.cs index 56557eb1..ec58cee5 100644 --- a/core/src/Microsoft.Teams.Bot.Core/Hosting/JwtExtensions.cs +++ b/core/src/Microsoft.Teams.Bot.Core/Hosting/JwtExtensions.cs @@ -268,10 +268,6 @@ private static AuthenticationBuilder AddTeamsJwtBearer(this AuthenticationBuilde return builder; } - /// - /// Adds a bypass authentication scheme that always succeeds without validating tokens. - /// This is used when no ClientId is configured and should only be used for development. - /// private static AuthenticationBuilder AddBypassAuthentication(this AuthenticationBuilder builder, string schemeName, ILogger? logger = null) { (logger ?? NullLogger.Instance).LogWarning("ClientId not provided for scheme '{SchemeName}'. Configuring bypass authentication (no token validation). This is INSECURE and should only be used for development.", schemeName); diff --git a/core/src/Microsoft.Teams.Bot.Core/Hosting/MsalConfigurationExtensions.cs b/core/src/Microsoft.Teams.Bot.Core/Hosting/MsalConfigurationExtensions.cs index fcf51e99..83d9953c 100644 --- a/core/src/Microsoft.Teams.Bot.Core/Hosting/MsalConfigurationExtensions.cs +++ b/core/src/Microsoft.Teams.Bot.Core/Hosting/MsalConfigurationExtensions.cs @@ -45,9 +45,6 @@ internal static bool ConfigureMSAL(this IServiceCollection services, BotConfig b return true; } - /// - /// Configures MSAL from an IConfigurationSection. - /// private static IServiceCollection ConfigureMSALFromConfig(this IServiceCollection services, IConfigurationSection msalConfigSection) { ArgumentNullException.ThrowIfNull(msalConfigSection); @@ -55,9 +52,6 @@ private static IServiceCollection ConfigureMSALFromConfig(this IServiceCollectio return services; } - /// - /// Configures MSAL with client secret authentication. - /// private static IServiceCollection ConfigureMSALWithSecret(this IServiceCollection services, string tenantId, string clientId, string clientSecret) { ArgumentException.ThrowIfNullOrWhiteSpace(tenantId); @@ -80,9 +74,6 @@ private static IServiceCollection ConfigureMSALWithSecret(this IServiceCollectio return services; } - /// - /// Configures MSAL with Federated Identity Credential (FIC) authentication using managed identity. - /// private static IServiceCollection ConfigureMSALWithFIC(this IServiceCollection services, string tenantId, string clientId, string? ficClientId) { ArgumentException.ThrowIfNullOrWhiteSpace(tenantId); @@ -109,9 +100,6 @@ private static IServiceCollection ConfigureMSALWithFIC(this IServiceCollection s return services; } - /// - /// Configures MSAL with User-Assigned Managed Identity (UMI) authentication. - /// private static IServiceCollection ConfigureMSALWithUMI(this IServiceCollection services, string tenantId, string clientId, string? managedIdentityClientId = null) { ArgumentNullException.ThrowIfNullOrWhiteSpace(tenantId); @@ -135,9 +123,6 @@ private static IServiceCollection ConfigureMSALWithUMI(this IServiceCollection s return services; } - /// - /// Configures MSAL by selecting the appropriate credential type based on BotConfig properties. - /// private static IServiceCollection ConfigureMSALFromBotConfig(this IServiceCollection services, BotConfig botConfig, ILogger logger) { ArgumentNullException.ThrowIfNull(botConfig); @@ -160,9 +145,6 @@ private static IServiceCollection ConfigureMSALFromBotConfig(this IServiceCollec return services; } - /// - /// Determines if the provided client ID represents a system-assigned managed identity. - /// private static bool IsSystemAssignedManagedIdentity(string? clientId) => string.Equals(clientId, BotConfig.SystemManagedIdentityIdentifier, StringComparison.OrdinalIgnoreCase); From a6c78ca6fc76230374c28ef5bdc4dfd46bebef01 Mon Sep 17 00:00:00 2001 From: Kavin Singh Date: Mon, 23 Mar 2026 12:01:16 -0700 Subject: [PATCH 6/7] fix scope resolution logic --- core/src/Microsoft.Teams.Bot.Core/Hosting/BotConfig.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/Microsoft.Teams.Bot.Core/Hosting/BotConfig.cs b/core/src/Microsoft.Teams.Bot.Core/Hosting/BotConfig.cs index dfda863d..4bfef98a 100644 --- a/core/src/Microsoft.Teams.Bot.Core/Hosting/BotConfig.cs +++ b/core/src/Microsoft.Teams.Bot.Core/Hosting/BotConfig.cs @@ -125,7 +125,7 @@ public static BotConfig FromMsalConfig(IConfiguration configuration, string sect TenantId = section["TenantId"] ?? string.Empty, ClientId = section["ClientId"] ?? string.Empty, ClientSecret = section["ClientSecret"], - Scope = section["Scope"] ?? configuration["Scope"] ?? BotScope, + Scope = section["Scope"] ?? BotScope, MsalConfigurationSection = section, SectionName = sectionName }; From 5ac435d2d851a0a753133af0b320aedd081c7a9e Mon Sep 17 00:00:00 2001 From: Kavin Singh Date: Mon, 23 Mar 2026 12:48:34 -0700 Subject: [PATCH 7/7] remove redundant logging --- .../TeamsBotApplication.HostingExtensions.cs | 4 +- .../Hosting/AddBotApplicationExtensions.cs | 39 ++++++++++++------- .../Hosting/JwtExtensions.cs | 16 +++++++- 3 files changed, 41 insertions(+), 18 deletions(-) diff --git a/core/src/Microsoft.Teams.Bot.Apps/TeamsBotApplication.HostingExtensions.cs b/core/src/Microsoft.Teams.Bot.Apps/TeamsBotApplication.HostingExtensions.cs index 8e8bc830..c8bd4ec2 100644 --- a/core/src/Microsoft.Teams.Bot.Apps/TeamsBotApplication.HostingExtensions.cs +++ b/core/src/Microsoft.Teams.Bot.Apps/TeamsBotApplication.HostingExtensions.cs @@ -43,13 +43,11 @@ public static IServiceCollection AddTeamsBotApplication(this IServiceCollection /// The updated WebApplicationBuilder instance. public static IServiceCollection AddTeamsBotApplication(this IServiceCollection services, string sectionName = "AzureAd") where TApp : TeamsBotApplication { - // Resolve BotConfig to get authentication configuration BotConfig botConfig = BotConfig.Resolve(services, sectionName); - // Reuse AddBotClient infrastructure for TeamsApiClient services.AddBotClient(TeamsApiClient.TeamsHttpClientName, botConfig); - services.AddBotApplication(sectionName); + services.AddBotApplication(botConfig); return services; } diff --git a/core/src/Microsoft.Teams.Bot.Core/Hosting/AddBotApplicationExtensions.cs b/core/src/Microsoft.Teams.Bot.Core/Hosting/AddBotApplicationExtensions.cs index 3e77500b..02a46e02 100644 --- a/core/src/Microsoft.Teams.Bot.Core/Hosting/AddBotApplicationExtensions.cs +++ b/core/src/Microsoft.Teams.Bot.Core/Hosting/AddBotApplicationExtensions.cs @@ -74,25 +74,39 @@ public static TApp UseBotApplication( } /// - /// Adds a bot application to the service collection with the default configuration section name "AzureAd". + /// 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 = "AzureAd") => services.AddBotApplication(sectionName); /// - /// Adds a bot application to the service collection. + /// 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 = "AzureAd") 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. + internal static IServiceCollection AddBotApplication(this IServiceCollection services, BotConfig botConfig) where TApp : BotApplication + { services.AddSingleton(sp => { IConfiguration config = sp.GetRequiredService(); @@ -102,7 +116,7 @@ public static IServiceCollection AddBotApplication(this IServiceCollection }; }); services.AddHttpContextAccessor(); - services.AddBotAuthorization(aadSectionName: botConfig.SectionName); + services.AddBotAuthorization(botConfig); services.AddConversationClient(botConfig); services.AddUserTokenClient(botConfig); services.AddSingleton(); @@ -158,6 +172,7 @@ internal static IServiceCollection AddBotClient( options.SectionName = botConfig.SectionName; }); + // TODO: This shouldn't be called multiple times. It will being called once for each client we support. services .AddHttpClient() .AddTokenAcquisition(true) @@ -181,7 +196,6 @@ internal static IServiceCollection AddBotClient( } else { - _logAuthConfigNotFound(logger, httpClientName, null); services.AddHttpClient(httpClientName); } @@ -212,7 +226,4 @@ internal static ILogger GetLoggerFromServices(IServiceCollection services, Type? return (ILogger?)tempFactory?.CreateLogger(categoryType ?? typeof(AddBotApplicationExtensions)) ?? Extensions.Logging.Abstractions.NullLogger.Instance; } - - private static readonly Action _logAuthConfigNotFound = - LoggerMessage.Define(LogLevel.Warning, new(7), "Authentication configuration not found. Outgoing requests from '{HttpClientName}' will not be authenticated."); } diff --git a/core/src/Microsoft.Teams.Bot.Core/Hosting/JwtExtensions.cs b/core/src/Microsoft.Teams.Bot.Core/Hosting/JwtExtensions.cs index ec58cee5..b04327fe 100644 --- a/core/src/Microsoft.Teams.Bot.Core/Hosting/JwtExtensions.cs +++ b/core/src/Microsoft.Teams.Bot.Core/Hosting/JwtExtensions.cs @@ -100,7 +100,21 @@ public static AuthorizationBuilder AddBotAuthorization(this IServiceCollection s logger ??= NullLogger.Instance; BotConfig botConfig = ResolveBotConfig(services, aadSectionName); - return services.AddBotAuthorization(botConfig.ClientId, botConfig.TenantId, aadSectionName, logger); + return services.AddBotAuthorization(botConfig, logger); + } + + /// + /// Adds authorization policies to the service collection using configuration from appsettings. + /// + /// The service collection to add authorization to. + /// The bot configuration settings. + /// Optional logger instance for logging. If null, a NullLogger will be used. + /// An for further authorization configuration. + internal static AuthorizationBuilder AddBotAuthorization(this IServiceCollection services, BotConfig botConfig, ILogger? logger = null) + { + logger ??= NullLogger.Instance; + + return services.AddBotAuthorization(botConfig.ClientId, botConfig.TenantId, botConfig.SectionName, logger); } ///