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/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.Apps/TeamsBotApplication.HostingExtensions.cs b/core/src/Microsoft.Teams.Bot.Apps/TeamsBotApplication.HostingExtensions.cs
index 5c8a7ac1..3c5a9b9b 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,11 @@ 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;
- });
+ 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>());
- });
+ services.AddBotClient(TeamsApiClient.TeamsHttpClientName, botConfig);
- services.AddBotApplication();
+ 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 d7624b5b..02a46e02 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;
@@ -24,8 +26,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
///
@@ -74,41 +74,51 @@ 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
{
- // 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.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();
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(botConfig);
+ services.AddConversationClient(botConfig);
+ services.AddUserTokenClient(botConfig);
services.AddSingleton();
return services;
}
@@ -119,8 +129,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,54 +141,47 @@ 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;
});
+ // TODO: This shouldn't be called multiple times. It will being called once for each client we support.
services
.AddHttpClient()
.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));
+ ILogger logger = GetLoggerFromServices(services);
- 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));
- }
- }
-
- // Configure MSAL during registration (not deferred)
- if (services.ConfigureMSAL(configuration, sectionName, logger))
+ if (services.ConfigureMSAL(botConfig, logger))
{
services.AddHttpClient(httpClientName)
.AddHttpMessageHandler(sp =>
@@ -190,157 +196,34 @@ private static IServiceCollection AddBotClient(
}
else
{
- _logAuthConfigNotFound(logger, 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)
+ ///
+ /// 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.
+ internal static ILogger GetLoggerFromServices(IServiceCollection services, Type? categoryType = null)
{
- ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
- ArgumentException.ThrowIfNullOrWhiteSpace(clientId);
+ ServiceDescriptor? loggerFactoryDescriptor = services.FirstOrDefault(d => d.ServiceType == typeof(ILoggerFactory));
+ ILoggerFactory? loggerFactory = loggerFactoryDescriptor?.ImplementationInstance as ILoggerFactory;
- CredentialDescription ficCredential = new()
+ // If logger factory is available as an instance, use it directly
+ if (loggerFactory != null)
{
- SourceType = CredentialSource.SignedAssertionFromManagedIdentity,
- };
- if (!string.IsNullOrEmpty(ficClientId) && !IsSystemAssignedManagedIdentity(ficClientId))
- {
- ficCredential.ManagedIdentityClientId = ficClientId;
+ return loggerFactory.CreateLogger(categoryType ?? typeof(AddBotApplicationExtensions));
}
- 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;
+ // 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;
}
-
- ///
- /// 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 _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");
-
-
}
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..4bfef98a 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,83 @@ public static BotConfig FromAadConfig(IConfiguration configuration, string secti
TenantId = section["TenantId"] ?? string.Empty,
ClientId = section["ClientId"] ?? string.Empty,
ClientSecret = section["ClientSecret"],
+ Scope = section["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();
+
+ // 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:
- /// AzureAd section, Core environment variables, then Bot Framework SDK keys.
+ /// MSAL section, Core environment variables, then Bot Framework SDK keys.
///
/// 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);
+ config.SectionName = sectionName;
+ return config;
+ }
config = FromBFConfig(configuration);
- if (!string.IsNullOrEmpty(config.ClientId)) return config;
+ if (!string.IsNullOrEmpty(config.ClientId))
+ {
+ _logUsingBFConfig(logger, null);
+ config.SectionName = sectionName;
+ return config;
+ }
- throw new InvalidOperationException("ClientID not found in configuration.");
+ // No configuration found - log warning and return empty config
+ _logNoConfigFound(logger, 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 configuration.");
+
}
diff --git a/core/src/Microsoft.Teams.Bot.Core/Hosting/JwtExtensions.cs b/core/src/Microsoft.Teams.Bot.Core/Hosting/JwtExtensions.cs
index 0e879fa9..19fd4daf 100644
--- a/core/src/Microsoft.Teams.Bot.Core/Hosting/JwtExtensions.cs
+++ b/core/src/Microsoft.Teams.Bot.Core/Hosting/JwtExtensions.cs
@@ -78,7 +78,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;
}
@@ -94,7 +101,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);
}
///
@@ -262,6 +283,40 @@ private static AuthenticationBuilder AddTeamsJwtBearer(this AuthenticationBuilde
return builder;
}
+ 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..83d9953c
--- /dev/null
+++ b/core/src/Microsoft.Teams.Bot.Core/Hosting/MsalConfigurationExtensions.cs
@@ -0,0 +1,157 @@
+// 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";
+
+ ///
+ /// 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;
+ }
+
+ 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;
+ }
+
+ 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..7c30a685 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;
@@ -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));
}
}