microsoft/teams.net

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
kavin/agents-sdk-interop

Branches

Tags

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

Clone

HTTPS

Download ZIP

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

320lines · modecode

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4using Microsoft.AspNetCore.Builder;
5using Microsoft.AspNetCore.Http;
6using Microsoft.AspNetCore.Routing;
7using Microsoft.Extensions.Configuration;
8using Microsoft.Extensions.DependencyInjection;
9using Microsoft.Extensions.Logging;
10using Microsoft.Extensions.Logging.Abstractions;
11using Microsoft.Extensions.Options;
12using Microsoft.Identity.Abstractions;
13using Microsoft.Identity.Web;
14using Microsoft.Identity.Web.TokenCacheProviders.InMemory;
15
16namespace Microsoft.Teams.Core.Hosting;
17
18/// <summary>
19/// Provides extension methods for registering bot application clients and related authentication services with the
20/// dependency injection container.
21/// </summary>
22/// <remarks>This class is intended to be used during application startup to configure HTTP clients, token
23/// acquisition, and agent identity services required for bot-to-bot communication. The configuration section specified
24/// by the Azure Active Directory (AAD) configuration name is used to bind authentication options. Typically, these
25/// methods are called in the application's service configuration pipeline.</remarks>
26public static class AddBotApplicationExtensions
27{
28 /// <summary>
29 /// Configures the default <see cref="BotApplication"/> to handle bot messages at the specified route.
30 /// </summary>
31 /// <param name="endpoints">The endpoint route builder used to configure endpoints.</param>
32 /// <param name="routePath">The route path at which to listen for incoming bot messages. Defaults to "api/messages".</param>
33 /// <returns>The registered <see cref="BotApplication"/> instance.</returns>
34 public static BotApplication UseBotApplication(
35 this IEndpointRouteBuilder endpoints,
36 string routePath = "api/messages")
37 => UseBotApplication<BotApplication>(endpoints, routePath);
38
39 /// <summary>
40 /// Configures the application to handle bot messages at the specified route and returns the registered bot
41 /// application instance.
42 /// </summary>
43 /// <remarks>This method adds authentication and authorization middleware to the HTTP pipeline and maps
44 /// a POST endpoint for bot messages. The endpoint requires authorization. Ensure that the bot application
45 /// is registered in the service container before calling this method.</remarks>
46 /// <typeparam name="TApp">The type of the bot application to use. Must inherit from BotApplication.</typeparam>
47 /// <param name="endpoints">The endpoint route builder used to configure endpoints.</param>
48 /// <param name="routePath">The route path at which to listen for incoming bot messages. Defaults to "api/messages".</param>
49 /// <returns>The registered bot application instance of type TApp.</returns>
50 /// <exception cref="InvalidOperationException">Thrown if the bot application of type TApp is not registered in the application's service container.</exception>
51 public static TApp UseBotApplication<TApp>(
52 this IEndpointRouteBuilder endpoints,
53 string routePath = "api/messages")
54 where TApp : BotApplication
55 {
56 ArgumentNullException.ThrowIfNull(endpoints);
57
58 // Add authentication and authorization middleware to the pipeline
59 // This is safe because WebApplication implements both IEndpointRouteBuilder and IApplicationBuilder
60 if (endpoints is IApplicationBuilder app)
61 {
62 app.UseAuthentication();
63 app.UseAuthorization();
64 }
65
66 TApp botApp = endpoints.ServiceProvider.GetService<TApp>() ?? throw new InvalidOperationException("Application not registered");
67
68 endpoints.MapPost(routePath, (HttpContext httpContext, CancellationToken cancellationToken)
69 => botApp.ProcessAsync(httpContext, cancellationToken)
70 ).RequireAuthorization();
71
72 return botApp;
73 }
74
75 /// <summary>
76 /// Registers the default bot application and its dependencies in the service collection.
77 /// </summary>
78 /// <param name="services">The service collection to add services to.</param>
79 /// <param name="sectionName">The configuration section name containing Azure AD settings. Defaults to "AzureAd".</param>
80 /// <returns>The service collection for method chaining.</returns>
81 public static IServiceCollection AddBotApplication(this IServiceCollection services, string sectionName = BotConfig.DefaultSectionName)
82 => services.AddBotApplication<BotApplication>(sectionName);
83
84 /// <summary>
85 /// Registers a custom bot application and its dependencies in the service collection.
86 /// </summary>
87 /// <typeparam name="TApp">The custom bot application type that inherits from BotApplication.</typeparam>
88 /// <param name="services">The service collection to add services to.</param>
89 /// <param name="sectionName">The configuration section name containing Azure AD settings. Defaults to "AzureAd".</param>
90 /// <returns>The service collection for method chaining.</returns>
91 public static IServiceCollection AddBotApplication<TApp>(this IServiceCollection services, string sectionName = BotConfig.DefaultSectionName) where TApp : BotApplication
92 {
93 BotConfig botConfig = BotConfig.Resolve(services, sectionName);
94
95 services.AddBotApplication<TApp>(botConfig);
96
97 return services;
98 }
99
100 /// <summary>
101 /// Registers a custom bot application and its dependencies in the service collection.
102 /// </summary>
103 /// <typeparam name="TApp">The custom bot application type that inherits from BotApplication.</typeparam>
104 /// <param name="services">The service collection to add services to.</param>
105 /// <param name="botConfig">The configuration containing Azure AD settings.</param>
106 /// <returns>The service collection for method chaining.</returns>
107 public static IServiceCollection AddBotApplication<TApp>(this IServiceCollection services, BotConfig botConfig) where TApp : BotApplication
108 {
109 ArgumentNullException.ThrowIfNull(botConfig);
110 services.AddSingleton<BotApplicationOptions>(_ => new BotApplicationOptions { AppId = botConfig.ClientId });
111 services.AddHttpContextAccessor();
112 services.AddBotAuthorization(botConfig);
113 services.EnsureMsalServices(botConfig);
114 services.AddBotClient<ConversationClient>(ConversationClient.ConversationHttpClientName, botConfig);
115 services.AddBotClient<UserTokenClient>(UserTokenClient.UserTokenHttpClientName, botConfig);
116 services.AddSingleton<TApp>();
117 return services;
118 }
119
120 /// <summary>
121 /// Registers the <see cref="ConversationClient"/> and its dependencies in the service collection.
122 /// </summary>
123 /// <param name="services">The service collection to add services to.</param>
124 /// <param name="sectionName">The configuration section name containing Azure AD settings. Defaults to "AzureAd".</param>
125 /// <returns>The service collection for method chaining.</returns>
126 public static IServiceCollection AddConversationClient(this IServiceCollection services, string sectionName = BotConfig.DefaultSectionName)
127 {
128 BotConfig botConfig = BotConfig.Resolve(services, sectionName);
129 return services.EnsureMsalServices(botConfig)
130 .AddBotClient<ConversationClient>(ConversationClient.ConversationHttpClientName, botConfig);
131 }
132
133 /// <summary>
134 /// Registers the <see cref="UserTokenClient"/> and its dependencies in the service collection.
135 /// </summary>
136 /// <param name="services">The service collection to add services to.</param>
137 /// <param name="sectionName">The configuration section name containing Azure AD settings. Defaults to "AzureAd".</param>
138 /// <returns>The service collection for method chaining.</returns>
139 public static IServiceCollection AddUserTokenClient(this IServiceCollection services, string sectionName = BotConfig.DefaultSectionName)
140 {
141 BotConfig botConfig = BotConfig.Resolve(services, sectionName);
142 return services.EnsureMsalServices(botConfig)
143 .AddBotClient<UserTokenClient>(UserTokenClient.UserTokenHttpClientName, botConfig);
144 }
145
146 /// <summary>
147 /// Registers the shared MSAL token-acquisition pipeline and binds the named MSAL options.
148 /// </summary>
149 /// <remarks>
150 /// Safe to call multiple times: the Microsoft.Identity.Web service registrations are TryAdd-based,
151 /// and the named options binding (<see cref="MicrosoftIdentityApplicationOptions"/> and
152 /// <see cref="ManagedIdentityOptions"/>) appends an additional configure delegate per call. Those
153 /// delegates are idempotent against the same <see cref="BotConfig"/>, so re-running them produces
154 /// the same options state.
155 /// </remarks>
156 public static IServiceCollection EnsureMsalServices(this IServiceCollection services, BotConfig botConfig)
157 {
158 services.AddHttpClient()
159 .AddTokenAcquisition(true)
160 .AddInMemoryTokenCaches()
161 .AddAgentIdentities();
162
163 ArgumentNullException.ThrowIfNull(botConfig);
164 ArgumentNullException.ThrowIfNull(botConfig.MsalConfigurationSection);
165
166 if (!string.IsNullOrWhiteSpace(botConfig.ClientId))
167 {
168 services.Configure<MicrosoftIdentityApplicationOptions>(botConfig.SectionName, options =>
169 {
170 botConfig.MsalConfigurationSection.Bind(options);
171
172 // Default Instance when only TenantId is configured.
173 if (string.IsNullOrEmpty(options.Instance) && !string.IsNullOrEmpty(options.TenantId))
174 {
175 options.Instance = "https://login.microsoftonline.com/";
176 }
177
178 // MicrosoftEntraApplicationOptions.Authority is a computed property that
179 // returns Instance/TenantId/v2.0 when _authority is null. MergedOptions
180 // then sees Authority alongside Instance+TenantId and emits a warning
181 // (event 500). Setting Authority to empty prevents the computed value
182 // from propagating while Instance+TenantId remain available for MSAL.
183 if (!string.IsNullOrEmpty(options.Instance) && !string.IsNullOrEmpty(options.TenantId))
184 {
185 options.Authority = string.Empty;
186 }
187 });
188
189 // No ClientCredentials in the configured section implies pure User-Assigned Managed Identity:
190 // the bot's ClientId is the UMI's clientId (as in ABS bots with the UserAssignedMSI app type).
191 // Register ManagedIdentityOptions so BotAuthenticationHandler routes token acquisition through
192 // the IMDS endpoint instead of the standard app-credentials flow.
193 if (botConfig.IsUserAssignedManagedIdentity)
194 {
195 LogFromServices(services, l => l.InferringUserAssignedManagedIdentity(botConfig.ClientId));
196 services.Configure<ManagedIdentityOptions>(botConfig.SectionName, options =>
197 {
198 options.UserAssignedClientId = botConfig.ClientId;
199 });
200 }
201 }
202 return services;
203 }
204
205 /// <summary>
206 /// Registers a typed <see cref="HttpClient"/> for <typeparamref name="TClient"/> wired to bot authentication
207 /// using an already-resolved <see cref="BotConfig"/>.
208 /// </summary>
209 /// <remarks>
210 /// <see cref="EnsureMsalServices(IServiceCollection, BotConfig)"/> must be called on the same service
211 /// collection before the resulting client is used, so that <c>IAuthorizationHeaderProvider</c> and the
212 /// named MSAL options are registered.
213 /// </remarks>
214 /// <typeparam name="TClient">The client class to register the named <see cref="HttpClient"/> for.</typeparam>
215 /// <param name="services">The service collection to add services to.</param>
216 /// <param name="httpClientName">The named <see cref="HttpClient"/> registration to associate with <typeparamref name="TClient"/>.</param>
217 /// <param name="botConfig">The resolved bot configuration containing tenant and client settings.</param>
218 /// <returns>The service collection for method chaining.</returns>
219 public static IServiceCollection AddBotClient<TClient>(
220 this IServiceCollection services,
221 string httpClientName,
222 BotConfig botConfig) where TClient : class
223 {
224 ArgumentNullException.ThrowIfNull(botConfig);
225 if (!string.IsNullOrWhiteSpace(botConfig.ClientId))
226 {
227 services.AddHttpClient<TClient>(httpClientName)
228 .AddHttpMessageHandler(sp => new BotAuthenticationHandler(
229 sp.GetRequiredService<IAuthorizationHeaderProvider>(),
230 sp.GetRequiredService<ILogger<BotAuthenticationHandler>>(),
231 botConfig.SectionName,
232 sp.GetService<IOptionsMonitor<ManagedIdentityOptions>>()));
233 }
234 else
235 {
236 services.AddHttpClient<TClient>(httpClientName);
237 }
238 return services;
239 }
240
241 /// <summary>
242 /// Registers a named <see cref="HttpClient"/> wired to bot authentication
243 /// using an already-resolved <see cref="BotConfig"/>, without binding it to a typed client.
244 /// Use this when the client type will be registered separately via a factory.
245 /// </summary>
246 /// <param name="services">The service collection to add services to.</param>
247 /// <param name="httpClientName">The logical name for this <see cref="HttpClient"/> registration.</param>
248 /// <param name="botConfig">The resolved bot configuration containing tenant and client settings.</param>
249 /// <returns>The service collection for method chaining.</returns>
250 public static IServiceCollection AddBotHttpClient(
251 this IServiceCollection services,
252 string httpClientName,
253 BotConfig botConfig)
254 {
255 ArgumentNullException.ThrowIfNull(botConfig);
256 if (!string.IsNullOrWhiteSpace(botConfig.ClientId))
257 {
258 services.AddHttpClient(httpClientName)
259 .AddHttpMessageHandler(sp => new BotAuthenticationHandler(
260 sp.GetRequiredService<IAuthorizationHeaderProvider>(),
261 sp.GetRequiredService<ILogger<BotAuthenticationHandler>>(),
262 botConfig.SectionName,
263 sp.GetService<IOptionsMonitor<ManagedIdentityOptions>>()));
264 }
265 else
266 {
267 services.AddHttpClient(httpClientName);
268 }
269 return services;
270 }
271
272 /// <summary>
273 /// Resolves a service from the service collection before the host is built,
274 /// preferring a direct instance and falling back to building a temporary
275 /// <see cref="ServiceProvider"/> when the service is registered via factory or type.
276 /// </summary>
277 /// <remarks>
278 /// The temporary <see cref="ServiceProvider"/> is disposed before the method returns.
279 /// Only use this for services whose resolved instances remain valid after their
280 /// owning provider is disposed (e.g. <see cref="IConfiguration"/>). Do NOT use for
281 /// disposable services like <see cref="ILoggerFactory"/> — see
282 /// <see cref="LogFromServices"/> for that case.
283 /// </remarks>
284 internal static T? ResolveFromServicesPreHost<T>(IServiceCollection services) where T : class
285 {
286 ServiceDescriptor? descriptor = services.LastOrDefault(d => d.ServiceType == typeof(T));
287 if (descriptor is null)
288 {
289 return null;
290 }
291
292 if (descriptor.ImplementationInstance is T instance)
293 {
294 return instance;
295 }
296
297 using ServiceProvider tempProvider = services.BuildServiceProvider();
298 return tempProvider.GetService<T>();
299 }
300
301 internal static void LogFromServices(IServiceCollection services, Action<ILogger> action, Type? categoryType = null)
302 {
303 ServiceDescriptor? descriptor = services.LastOrDefault(d => d.ServiceType == typeof(ILoggerFactory));
304 if (descriptor is null)
305 {
306 action(NullLogger.Instance);
307 return;
308 }
309
310 if (descriptor.ImplementationInstance is ILoggerFactory directFactory)
311 {
312 action(directFactory.CreateLogger(categoryType ?? typeof(AddBotApplicationExtensions)));
313 return;
314 }
315
316 using ServiceProvider tempProvider = services.BuildServiceProvider();
317 ILoggerFactory? factory = tempProvider.GetService<ILoggerFactory>();
318 action(factory?.CreateLogger(categoryType ?? typeof(AddBotApplicationExtensions)) ?? NullLogger.Instance);
319 }
320}
321