microsoft/teams.net

Public

mirrored from https://github.com/microsoft/teams.netAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
c92c1919ec3b5a0b25a067ad4ad46da598a2fdca

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

323lines · 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.
69 /// </summary>
70 /// <typeparam name="TApp"></typeparam>
71 /// <param name="services"></param>
72 /// <param name="sectionName"></param>
73 /// <returns></returns>
74 public static IServiceCollection AddBotApplication<TApp>(this IServiceCollection services, string sectionName = "AzureAd") where TApp : BotApplication
75 {
76 // Extract ILoggerFactory from service collection to create logger without BuildServiceProvider
77 var loggerFactoryDescriptor = services.FirstOrDefault(d => d.ServiceType == typeof(ILoggerFactory));
78 var loggerFactory = loggerFactoryDescriptor?.ImplementationInstance as ILoggerFactory;
79 ILogger logger = loggerFactory?.CreateLogger<BotApplication>()
80 ?? (ILogger)Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance;
81
82 services.AddAuthorization(logger, sectionName);
83 services.AddConversationClient(sectionName);
84 services.AddUserTokenClient(sectionName);
85 services.AddSingleton<TApp>();
86 return services;
87 }
88
89 /// <summary>
90 /// Adds conversation client to the service collection.
91 /// </summary>
92 /// <param name="services">service collection</param>
93 /// <param name="sectionName">Configuration Section name, defaults to AzureAD</param>
94 /// <returns></returns>
95 public static IServiceCollection AddConversationClient(this IServiceCollection services, string sectionName = "AzureAd") =>
96 services.AddBotClient<ConversationClient>(ConversationClient.ConversationHttpClientName, sectionName);
97
98 /// <summary>
99 /// Adds user token 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 AddUserTokenClient(this IServiceCollection services, string sectionName = "AzureAd") =>
105 services.AddBotClient<UserTokenClient>(UserTokenClient.UserTokenHttpClientName, sectionName);
106
107 private static IServiceCollection AddBotClient<TClient>(
108 this IServiceCollection services,
109 string httpClientName,
110 string sectionName) where TClient : class
111 {
112 // Register options to defer scope configuration reading
113 services.AddOptions<BotClientOptions>()
114 .Configure<IConfiguration>((options, configuration) =>
115 {
116 options.Scope = "https://api.botframework.com/.default";
117 if (!string.IsNullOrEmpty(configuration[$"{sectionName}:Scope"]))
118 options.Scope = configuration[$"{sectionName}:Scope"]!;
119 if (!string.IsNullOrEmpty(configuration["Scope"]))
120 options.Scope = configuration["Scope"]!;
121 options.SectionName = sectionName;
122 });
123
124 services
125 .AddHttpClient()
126 .AddTokenAcquisition(true)
127 .AddInMemoryTokenCaches()
128 .AddAgentIdentities();
129
130 // Get configuration and logger to configure MSAL during registration
131 // Try to get from service descriptors first
132 var configDescriptor = services.FirstOrDefault(d => d.ServiceType == typeof(IConfiguration));
133 IConfiguration? configuration = configDescriptor?.ImplementationInstance as IConfiguration;
134
135 var loggerFactoryDescriptor = services.FirstOrDefault(d => d.ServiceType == typeof(ILoggerFactory));
136 var loggerFactory = loggerFactoryDescriptor?.ImplementationInstance as ILoggerFactory;
137 ILogger logger = loggerFactory?.CreateLogger(typeof(AddBotApplicationExtensions))
138 ?? (ILogger)Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance;
139
140 // If configuration not available as instance, build temporary provider
141 if (configuration == null)
142 {
143 using var tempProvider = services.BuildServiceProvider();
144 configuration = tempProvider.GetRequiredService<IConfiguration>();
145 if (loggerFactory == null)
146 {
147 logger = tempProvider.GetRequiredService<ILoggerFactory>().CreateLogger(typeof(AddBotApplicationExtensions));
148 }
149 }
150
151 // Configure MSAL during registration (not deferred)
152 if (services.ConfigureMSAL(configuration, sectionName, logger))
153 {
154 services.AddHttpClient<TClient>(httpClientName)
155 .AddHttpMessageHandler(sp =>
156 {
157 var botOptions = sp.GetRequiredService<IOptions<BotClientOptions>>().Value;
158 return new BotAuthenticationHandler(
159 sp.GetRequiredService<IAuthorizationHeaderProvider>(),
160 sp.GetRequiredService<ILogger<BotAuthenticationHandler>>(),
161 botOptions.Scope,
162 sp.GetService<IOptions<ManagedIdentityOptions>>());
163 });
164 }
165 else
166 {
167 _logAuthConfigNotFound(logger, null);
168 services.AddHttpClient<TClient>(httpClientName);
169 }
170
171 return services;
172 }
173
174 private static bool ConfigureMSAL(this IServiceCollection services, IConfiguration configuration, string sectionName, ILogger logger)
175 {
176 ArgumentNullException.ThrowIfNull(configuration);
177
178 if (configuration["MicrosoftAppId"] is not null)
179 {
180 _logUsingBFConfig(logger, null);
181 BotConfig botConfig = BotConfig.FromBFConfig(configuration);
182 services.ConfigureMSALFromBotConfig(botConfig, logger);
183 }
184 else if (configuration["CLIENT_ID"] is not null)
185 {
186 _logUsingCoreConfig(logger, null);
187 BotConfig botConfig = BotConfig.FromCoreConfig(configuration);
188 services.ConfigureMSALFromBotConfig(botConfig, logger);
189 }
190 else
191 {
192 _logUsingSectionConfig(logger, sectionName, null);
193 services.ConfigureMSALFromConfig(configuration.GetSection(sectionName));
194 }
195 return true;
196 }
197
198 private static IServiceCollection ConfigureMSALFromConfig(this IServiceCollection services, IConfigurationSection msalConfigSection)
199 {
200 ArgumentNullException.ThrowIfNull(msalConfigSection);
201 services.Configure<MicrosoftIdentityApplicationOptions>(MsalConfigKey, msalConfigSection);
202 return services;
203 }
204
205 private static IServiceCollection ConfigureMSALWithSecret(this IServiceCollection services, string tenantId, string clientId, string clientSecret)
206 {
207 ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
208 ArgumentException.ThrowIfNullOrWhiteSpace(clientId);
209 ArgumentException.ThrowIfNullOrWhiteSpace(clientSecret);
210
211 services.Configure<MicrosoftIdentityApplicationOptions>(MsalConfigKey, options =>
212 {
213 // TODO: Make Instance configurable
214 options.Instance = "https://login.microsoftonline.com/";
215 options.TenantId = tenantId;
216 options.ClientId = clientId;
217 options.ClientCredentials = [
218 new CredentialDescription()
219 {
220 SourceType = CredentialSource.ClientSecret,
221 ClientSecret = clientSecret
222 }
223 ];
224 });
225 return services;
226 }
227
228 private static IServiceCollection ConfigureMSALWithFIC(this IServiceCollection services, string tenantId, string clientId, string? ficClientId)
229 {
230 ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
231 ArgumentException.ThrowIfNullOrWhiteSpace(clientId);
232
233 CredentialDescription ficCredential = new()
234 {
235 SourceType = CredentialSource.SignedAssertionFromManagedIdentity,
236 };
237 if (!string.IsNullOrEmpty(ficClientId) && !IsSystemAssignedManagedIdentity(ficClientId))
238 {
239 ficCredential.ManagedIdentityClientId = ficClientId;
240 }
241
242 services.Configure<MicrosoftIdentityApplicationOptions>(MsalConfigKey, options =>
243 {
244 // TODO: Make Instance configurable
245 options.Instance = "https://login.microsoftonline.com/";
246 options.TenantId = tenantId;
247 options.ClientId = clientId;
248 options.ClientCredentials = [
249 ficCredential
250 ];
251 });
252 return services;
253 }
254
255 private static IServiceCollection ConfigureMSALWithUMI(this IServiceCollection services, string tenantId, string clientId, string? managedIdentityClientId = null)
256 {
257 ArgumentNullException.ThrowIfNullOrWhiteSpace(tenantId);
258 ArgumentNullException.ThrowIfNullOrWhiteSpace(clientId);
259
260 // Register ManagedIdentityOptions for BotAuthenticationHandler to use
261 bool isSystemAssigned = IsSystemAssignedManagedIdentity(managedIdentityClientId);
262 string? umiClientId = isSystemAssigned ? null : (managedIdentityClientId ?? clientId);
263
264 services.Configure<ManagedIdentityOptions>(options =>
265 {
266 options.UserAssignedClientId = umiClientId;
267 });
268
269 services.Configure<MicrosoftIdentityApplicationOptions>(MsalConfigKey, options =>
270 {
271 // TODO: Make Instance configurable
272 options.Instance = "https://login.microsoftonline.com/";
273 options.TenantId = tenantId;
274 options.ClientId = clientId;
275 });
276 return services;
277 }
278
279 private static IServiceCollection ConfigureMSALFromBotConfig(this IServiceCollection services, BotConfig botConfig, ILogger logger)
280 {
281 ArgumentNullException.ThrowIfNull(botConfig);
282 if (!string.IsNullOrEmpty(botConfig.ClientSecret))
283 {
284 _logUsingClientSecret(logger, null);
285 services.ConfigureMSALWithSecret(botConfig.TenantId, botConfig.ClientId, botConfig.ClientSecret);
286 }
287 else if (string.IsNullOrEmpty(botConfig.FicClientId) || botConfig.FicClientId == botConfig.ClientId)
288 {
289 _logUsingUMI(logger, null);
290 services.ConfigureMSALWithUMI(botConfig.TenantId, botConfig.ClientId, botConfig.FicClientId);
291 }
292 else
293 {
294 bool isSystemAssigned = IsSystemAssignedManagedIdentity(botConfig.FicClientId);
295 _logUsingFIC(logger, isSystemAssigned ? "System-Assigned" : "User-Assigned", null);
296 services.ConfigureMSALWithFIC(botConfig.TenantId, botConfig.ClientId, botConfig.FicClientId);
297 }
298 return services;
299 }
300
301 /// <summary>
302 /// Determines if the provided client ID represents a system-assigned managed identity.
303 /// </summary>
304 private static bool IsSystemAssignedManagedIdentity(string? clientId)
305 => string.Equals(clientId, BotConfig.SystemManagedIdentityIdentifier, StringComparison.OrdinalIgnoreCase);
306
307 private static readonly Action<ILogger, Exception?> _logUsingBFConfig =
308 LoggerMessage.Define(LogLevel.Debug, new(1), "Configuring MSAL from Bot Framework configuration");
309 private static readonly Action<ILogger, Exception?> _logUsingCoreConfig =
310 LoggerMessage.Define(LogLevel.Debug, new(2), "Configuring MSAL from Core bot configuration");
311 private static readonly Action<ILogger, string, Exception?> _logUsingSectionConfig =
312 LoggerMessage.Define<string>(LogLevel.Debug, new(3), "Configuring MSAL from {SectionName} configuration section");
313 private static readonly Action<ILogger, Exception?> _logUsingClientSecret =
314 LoggerMessage.Define(LogLevel.Debug, new(4), "Configuring authentication with client secret");
315 private static readonly Action<ILogger, Exception?> _logUsingUMI =
316 LoggerMessage.Define(LogLevel.Debug, new(5), "Configuring authentication with User-Assigned Managed Identity");
317 private static readonly Action<ILogger, string, Exception?> _logUsingFIC =
318 LoggerMessage.Define<string>(LogLevel.Debug, new(6), "Configuring authentication with Federated Identity Credential (Managed Identity) with {IdentityType} Managed Identity");
319 private static readonly Action<ILogger, Exception?> _logAuthConfigNotFound =
320 LoggerMessage.Define(LogLevel.Warning, new(7), "Authentication configuration not found. Running without Auth");
321
322
323}
324