microsoft/teams.net

Public

mirrored fromhttps://github.com/microsoft/teams.netAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
copilot/sub-pr-338

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

core/src/Microsoft.Teams.Bot.Core/Hosting/AddBotApplicationExtensions.cs

329lines · modecode

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4using System.Linq;
5using Microsoft.AspNetCore.Builder;
6using Microsoft.AspNetCore.Http;
7using Microsoft.AspNetCore.Routing;
8using Microsoft.Extensions.Configuration;
9using Microsoft.Extensions.DependencyInjection;
10using Microsoft.Extensions.Http;
11using Microsoft.Extensions.Logging;
12using Microsoft.Extensions.Options;
13using Microsoft.Identity.Abstractions;
14using Microsoft.Identity.Web;
15using Microsoft.Identity.Web.TokenCacheProviders.InMemory;
16
17namespace Microsoft.Teams.Bot.Core.Hosting;
18
19/// <summary>
20/// Provides extension methods for registering bot application clients and related authentication services with the
21/// dependency injection container.
22/// </summary>
23/// <remarks>This class is intended to be used during application startup to configure HTTP clients, token
24/// acquisition, and agent identity services required for bot-to-bot communication. The configuration section specified
25/// by the Azure Active Directory (AAD) configuration name is used to bind authentication options. Typically, these
26/// methods are called in the application's service configuration pipeline.</remarks>
27public static class AddBotApplicationExtensions
28{
29 internal const string MsalConfigKey = "AzureAd";
30
31 /// <summary>
32 /// Configures the application to handle bot messages at the specified route and returns the registered bot
33 /// application instance.
34 /// </summary>
35 /// <remarks>This method adds authentication and authorization middleware to the HTTP pipeline and maps
36 /// a POST endpoint for bot messages. The endpoint requires authorization. Ensure that the bot application
37 /// is registered in the service container before calling this method.</remarks>
38 /// <typeparam name="TApp">The type of the bot application to use. Must inherit from BotApplication.</typeparam>
39 /// <param name="endpoints">The endpoint route builder used to configure endpoints.</param>
40 /// <param name="routePath">The route path at which to listen for incoming bot messages. Defaults to "api/messages".</param>
41 /// <returns>The registered bot application instance of type TApp.</returns>
42 /// <exception cref="InvalidOperationException">Thrown if the bot application of type TApp is not registered in the application's service container.</exception>
43 public static TApp UseBotApplication<TApp>(
44 this IEndpointRouteBuilder endpoints,
45 string routePath = "api/messages")
46 where TApp : BotApplication
47 {
48 ArgumentNullException.ThrowIfNull(endpoints);
49
50 // Add authentication and authorization middleware to the pipeline
51 // This is safe because WebApplication implements both IEndpointRouteBuilder and IApplicationBuilder
52 if (endpoints is IApplicationBuilder app)
53 {
54 app.UseAuthentication();
55 app.UseAuthorization();
56 }
57
58 TApp botApp = endpoints.ServiceProvider.GetService<TApp>() ?? throw new InvalidOperationException("Application not registered");
59
60 endpoints.MapPost(routePath, (HttpContext httpContext, CancellationToken cancellationToken)
61 => botApp.ProcessAsync(httpContext, cancellationToken)
62 ).RequireAuthorization();
63
64 return botApp;
65 }
66
67 /// <summary>
68 /// Adds a bot application to the service collection with the default configuration section name "AzureAd".
69 /// </summary>
70 /// <param name="services"></param>
71 /// <param name="sectionName"></param>
72 /// <returns></returns>
73 public static IServiceCollection AddBotApplication(this IServiceCollection services, string sectionName = "AzureAd")
74 => services.AddBotApplication<BotApplication>(sectionName);
75
76 /// <summary>
77 /// Adds a bot application to the service collection.
78 /// </summary>
79 /// <typeparam name="TApp"></typeparam>
80 /// <param name="services"></param>
81 /// <param name="sectionName"></param>
82 /// <returns></returns>
83 public static IServiceCollection AddBotApplication<TApp>(this IServiceCollection services, string sectionName = "AzureAd") where TApp : BotApplication
84 {
85 // Extract ILoggerFactory from service collection to create logger without BuildServiceProvider
86 var loggerFactoryDescriptor = services.FirstOrDefault(d => d.ServiceType == typeof(ILoggerFactory));
87 var loggerFactory = loggerFactoryDescriptor?.ImplementationInstance as ILoggerFactory;
88 ILogger logger = loggerFactory?.CreateLogger<BotApplication>()
89 ?? (ILogger)Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance;
90
91 services.AddAuthorization(logger, sectionName);
92 services.AddConversationClient(sectionName);
93 services.AddUserTokenClient(sectionName);
94 services.AddSingleton<TApp>();
95 return services;
96 }
97
98 /// <summary>
99 /// Adds conversation client to the service collection.
100 /// </summary>
101 /// <param name="services">service collection</param>
102 /// <param name="sectionName">Configuration Section name, defaults to AzureAD</param>
103 /// <returns></returns>
104 public static IServiceCollection AddConversationClient(this IServiceCollection services, string sectionName = "AzureAd") =>
105 services.AddBotClient<ConversationClient>(ConversationClient.ConversationHttpClientName, sectionName);
106
107 /// <summary>
108 /// Adds user token client to the service collection.
109 /// </summary>
110 /// <param name="services">service collection</param>
111 /// <param name="sectionName">Configuration Section name, defaults to AzureAD</param>
112 /// <returns></returns>
113 public static IServiceCollection AddUserTokenClient(this IServiceCollection services, string sectionName = "AzureAd") =>
114 services.AddBotClient<UserTokenClient>(UserTokenClient.UserTokenHttpClientName, sectionName);
115
116 private static IServiceCollection AddBotClient<TClient>(
117 this IServiceCollection services,
118 string httpClientName,
119 string sectionName) where TClient : class
120 {
121 // Register options to defer scope configuration reading
122 services.AddOptions<BotClientOptions>()
123 .Configure<IConfiguration>((options, configuration) =>
124 {
125 options.Scope = "https://api.botframework.com/.default";
126 if (!string.IsNullOrEmpty(configuration[$"{sectionName}:Scope"]))
127 options.Scope = configuration[$"{sectionName}:Scope"]!;
128 if (!string.IsNullOrEmpty(configuration["Scope"]))
129 options.Scope = configuration["Scope"]!;
130 options.SectionName = sectionName;
131 });
132
133 services
134 .AddHttpClient()
135 .AddTokenAcquisition(true)
136 .AddInMemoryTokenCaches()
137 .AddAgentIdentities();
138
139 // Get configuration and logger to configure MSAL during registration
140 // Try to get from service descriptors first
141 var configDescriptor = services.FirstOrDefault(d => d.ServiceType == typeof(IConfiguration));
142 IConfiguration? configuration = configDescriptor?.ImplementationInstance as IConfiguration;
143
144 var loggerFactoryDescriptor = services.FirstOrDefault(d => d.ServiceType == typeof(ILoggerFactory));
145 var loggerFactory = loggerFactoryDescriptor?.ImplementationInstance as ILoggerFactory;
146 ILogger logger = loggerFactory?.CreateLogger(typeof(AddBotApplicationExtensions))
147 ?? (ILogger)Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance;
148
149 // If configuration not available as instance, build temporary provider
150 if (configuration == null)
151 {
152 using var tempProvider = services.BuildServiceProvider();
153 configuration = tempProvider.GetRequiredService<IConfiguration>();
154 if (loggerFactory == null)
155 {
156 logger = tempProvider.GetRequiredService<ILoggerFactory>().CreateLogger(typeof(AddBotApplicationExtensions));
157 }
158 }
159
160 // Configure MSAL during registration (not deferred)
161 if (services.ConfigureMSAL(configuration, sectionName, logger))
162 {
163 services.AddHttpClient<TClient>(httpClientName)
164 .AddHttpMessageHandler(sp =>
165 {
166 var botOptions = sp.GetRequiredService<IOptions<BotClientOptions>>().Value;
167 return new BotAuthenticationHandler(
168 sp.GetRequiredService<IAuthorizationHeaderProvider>(),
169 sp.GetRequiredService<ILogger<BotAuthenticationHandler>>(),
170 botOptions.Scope,
171 sp.GetService<IOptions<ManagedIdentityOptions>>());
172 });
173 }
174 else
175 {
176 _logAuthConfigNotFound(logger, null);
177 services.AddHttpClient<TClient>(httpClientName);
178 }
179
180 return services;
181 }
182
183 private static bool ConfigureMSAL(this IServiceCollection services, IConfiguration configuration, string sectionName, ILogger logger)
184 {
185 ArgumentNullException.ThrowIfNull(configuration);
186
187 if (configuration["MicrosoftAppId"] is not null)
188 {
189 _logUsingBFConfig(logger, null);
190 BotConfig botConfig = BotConfig.FromBFConfig(configuration);
191 services.ConfigureMSALFromBotConfig(botConfig, logger);
192 }
193 else if (configuration["CLIENT_ID"] is not null)
194 {
195 _logUsingCoreConfig(logger, null);
196 BotConfig botConfig = BotConfig.FromCoreConfig(configuration);
197 services.ConfigureMSALFromBotConfig(botConfig, logger);
198 }
199 else
200 {
201 _logUsingSectionConfig(logger, sectionName, null);
202 services.ConfigureMSALFromConfig(configuration.GetSection(sectionName));
203 }
204 return true;
205 }
206
207 private static IServiceCollection ConfigureMSALFromConfig(this IServiceCollection services, IConfigurationSection msalConfigSection)
208 {
209 ArgumentNullException.ThrowIfNull(msalConfigSection);
210 services.Configure<MicrosoftIdentityApplicationOptions>(MsalConfigKey, msalConfigSection);
211 return services;
212 }
213
214 private static IServiceCollection ConfigureMSALWithSecret(this IServiceCollection services, string tenantId, string clientId, string clientSecret)
215 {
216 ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
217 ArgumentException.ThrowIfNullOrWhiteSpace(clientId);
218 ArgumentException.ThrowIfNullOrWhiteSpace(clientSecret);
219
220 services.Configure<MicrosoftIdentityApplicationOptions>(MsalConfigKey, options =>
221 {
222 options.Instance = "https://login.microsoftonline.com/";
223 options.TenantId = tenantId;
224 options.ClientId = clientId;
225 options.ClientCredentials = [
226 new CredentialDescription()
227 {
228 SourceType = CredentialSource.ClientSecret,
229 ClientSecret = clientSecret
230 }
231 ];
232 });
233 return services;
234 }
235
236 private static IServiceCollection ConfigureMSALWithFIC(this IServiceCollection services, string tenantId, string clientId, string? ficClientId)
237 {
238 ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
239 ArgumentException.ThrowIfNullOrWhiteSpace(clientId);
240
241 CredentialDescription ficCredential = new()
242 {
243 SourceType = CredentialSource.SignedAssertionFromManagedIdentity,
244 };
245 if (!string.IsNullOrEmpty(ficClientId) && !IsSystemAssignedManagedIdentity(ficClientId))
246 {
247 ficCredential.ManagedIdentityClientId = ficClientId;
248 }
249
250 services.Configure<MicrosoftIdentityApplicationOptions>(MsalConfigKey, options =>
251 {
252 options.Instance = "https://login.microsoftonline.com/";
253 options.TenantId = tenantId;
254 options.ClientId = clientId;
255 options.ClientCredentials = [
256 ficCredential
257 ];
258 });
259 return services;
260 }
261
262 private static IServiceCollection ConfigureMSALWithUMI(this IServiceCollection services, string tenantId, string clientId, string? managedIdentityClientId = null)
263 {
264 ArgumentNullException.ThrowIfNullOrWhiteSpace(tenantId);
265 ArgumentNullException.ThrowIfNullOrWhiteSpace(clientId);
266
267 // Register ManagedIdentityOptions for BotAuthenticationHandler to use
268 bool isSystemAssigned = IsSystemAssignedManagedIdentity(managedIdentityClientId);
269 string? umiClientId = isSystemAssigned ? null : (managedIdentityClientId ?? clientId);
270
271 services.Configure<ManagedIdentityOptions>(options =>
272 {
273 options.UserAssignedClientId = umiClientId;
274 });
275
276 services.Configure<MicrosoftIdentityApplicationOptions>(MsalConfigKey, options =>
277 {
278 options.Instance = "https://login.microsoftonline.com/";
279 options.TenantId = tenantId;
280 options.ClientId = clientId;
281 });
282 return services;
283 }
284
285 private static IServiceCollection ConfigureMSALFromBotConfig(this IServiceCollection services, BotConfig botConfig, ILogger logger)
286 {
287 ArgumentNullException.ThrowIfNull(botConfig);
288 if (!string.IsNullOrEmpty(botConfig.ClientSecret))
289 {
290 _logUsingClientSecret(logger, null);
291 services.ConfigureMSALWithSecret(botConfig.TenantId, botConfig.ClientId, botConfig.ClientSecret);
292 }
293 else if (string.IsNullOrEmpty(botConfig.FicClientId) || botConfig.FicClientId == botConfig.ClientId)
294 {
295 _logUsingUMI(logger, null);
296 services.ConfigureMSALWithUMI(botConfig.TenantId, botConfig.ClientId, botConfig.FicClientId);
297 }
298 else
299 {
300 bool isSystemAssigned = IsSystemAssignedManagedIdentity(botConfig.FicClientId);
301 _logUsingFIC(logger, isSystemAssigned ? "System-Assigned" : "User-Assigned", null);
302 services.ConfigureMSALWithFIC(botConfig.TenantId, botConfig.ClientId, botConfig.FicClientId);
303 }
304 return services;
305 }
306
307 /// <summary>
308 /// Determines if the provided client ID represents a system-assigned managed identity.
309 /// </summary>
310 private static bool IsSystemAssignedManagedIdentity(string? clientId)
311 => string.Equals(clientId, BotConfig.SystemManagedIdentityIdentifier, StringComparison.OrdinalIgnoreCase);
312
313 private static readonly Action<ILogger, Exception?> _logUsingBFConfig =
314 LoggerMessage.Define(LogLevel.Debug, new(1), "Configuring MSAL from Bot Framework configuration");
315 private static readonly Action<ILogger, Exception?> _logUsingCoreConfig =
316 LoggerMessage.Define(LogLevel.Debug, new(2), "Configuring MSAL from Core bot configuration");
317 private static readonly Action<ILogger, string, Exception?> _logUsingSectionConfig =
318 LoggerMessage.Define<string>(LogLevel.Debug, new(3), "Configuring MSAL from {SectionName} configuration section");
319 private static readonly Action<ILogger, Exception?> _logUsingClientSecret =
320 LoggerMessage.Define(LogLevel.Debug, new(4), "Configuring authentication with client secret");
321 private static readonly Action<ILogger, Exception?> _logUsingUMI =
322 LoggerMessage.Define(LogLevel.Debug, new(5), "Configuring authentication with User-Assigned Managed Identity");
323 private static readonly Action<ILogger, string, Exception?> _logUsingFIC =
324 LoggerMessage.Define<string>(LogLevel.Debug, new(6), "Configuring authentication with Federated Identity Credential (Managed Identity) with {IdentityType} Managed Identity");
325 private static readonly Action<ILogger, Exception?> _logAuthConfigNotFound =
326 LoggerMessage.Define(LogLevel.Warning, new(7), "Authentication configuration not found. Running without Auth");
327
328
329}
330