microsoft/teams.net

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v2.0.7

Branches

Tags

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

Clone

HTTPS

Download ZIP

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

263lines · modecode

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4using Microsoft.AspNetCore.Builder;
5using Microsoft.Extensions.Configuration;
6using Microsoft.AspNetCore.Http;
7using Microsoft.AspNetCore.Routing;
8using Microsoft.Extensions.DependencyInjection;
9using Microsoft.Extensions.Logging;
10using Microsoft.Extensions.Options;
11using Microsoft.Identity.Abstractions;
12using Microsoft.Identity.Web;
13using Microsoft.Identity.Web.TokenCacheProviders.InMemory;
14
15namespace Microsoft.Teams.Core.Hosting;
16
17/// <summary>
18/// Provides extension methods for registering bot application clients and related authentication services with the
19/// dependency injection container.
20/// </summary>
21/// <remarks>This class is intended to be used during application startup to configure HTTP clients, token
22/// acquisition, and agent identity services required for bot-to-bot communication. The configuration section specified
23/// by the Azure Active Directory (AAD) configuration name is used to bind authentication options. Typically, these
24/// methods are called in the application's service configuration pipeline.</remarks>
25public static class AddBotApplicationExtensions
26{
27 /// <summary>
28 /// Configures the default <see cref="BotApplication"/> to handle bot messages at the specified route.
29 /// </summary>
30 /// <param name="endpoints">The endpoint route builder used to configure endpoints.</param>
31 /// <param name="routePath">The route path at which to listen for incoming bot messages. Defaults to "api/messages".</param>
32 /// <returns>The registered <see cref="BotApplication"/> instance.</returns>
33 public static BotApplication UseBotApplication(
34 this IEndpointRouteBuilder endpoints,
35 string routePath = "api/messages")
36 => UseBotApplication<BotApplication>(endpoints, routePath);
37
38 /// <summary>
39 /// Configures the application to handle bot messages at the specified route and returns the registered bot
40 /// application instance.
41 /// </summary>
42 /// <remarks>This method adds authentication and authorization middleware to the HTTP pipeline and maps
43 /// a POST endpoint for bot messages. The endpoint requires authorization. Ensure that the bot application
44 /// is registered in the service container before calling this method.</remarks>
45 /// <typeparam name="TApp">The type of the bot application to use. Must inherit from BotApplication.</typeparam>
46 /// <param name="endpoints">The endpoint route builder used to configure endpoints.</param>
47 /// <param name="routePath">The route path at which to listen for incoming bot messages. Defaults to "api/messages".</param>
48 /// <returns>The registered bot application instance of type TApp.</returns>
49 /// <exception cref="InvalidOperationException">Thrown if the bot application of type TApp is not registered in the application's service container.</exception>
50 public static TApp UseBotApplication<TApp>(
51 this IEndpointRouteBuilder endpoints,
52 string routePath = "api/messages")
53 where TApp : BotApplication
54 {
55 ArgumentNullException.ThrowIfNull(endpoints);
56
57 // Add authentication and authorization middleware to the pipeline
58 // This is safe because WebApplication implements both IEndpointRouteBuilder and IApplicationBuilder
59 if (endpoints is IApplicationBuilder app)
60 {
61 app.UseAuthentication();
62 app.UseAuthorization();
63 }
64
65 TApp botApp = endpoints.ServiceProvider.GetService<TApp>() ?? throw new InvalidOperationException("Application not registered");
66
67 endpoints.MapPost(routePath, (HttpContext httpContext, CancellationToken cancellationToken)
68 => botApp.ProcessAsync(httpContext, cancellationToken)
69 ).RequireAuthorization();
70
71 return botApp;
72 }
73
74 /// <summary>
75 /// Registers the default bot application and its dependencies in the service collection.
76 /// </summary>
77 /// <param name="services">The service collection to add services to.</param>
78 /// <param name="sectionName">The configuration section name containing Azure AD settings. Defaults to "AzureAd".</param>
79 /// <returns>The service collection for method chaining.</returns>
80 public static IServiceCollection AddBotApplication(this IServiceCollection services, string sectionName = BotConfig.DefaultSectionName)
81 => services.AddBotApplication<BotApplication>(sectionName);
82
83 /// <summary>
84 /// Registers a custom bot application and its dependencies in the service collection.
85 /// </summary>
86 /// <typeparam name="TApp">The custom bot application type that inherits from BotApplication.</typeparam>
87 /// <param name="services">The service collection to add services to.</param>
88 /// <param name="sectionName">The configuration section name containing Azure AD settings. Defaults to "AzureAd".</param>
89 /// <returns>The service collection for method chaining.</returns>
90 public static IServiceCollection AddBotApplication<TApp>(this IServiceCollection services, string sectionName = BotConfig.DefaultSectionName) where TApp : BotApplication
91 {
92 BotConfig botConfig = BotConfig.Resolve(services, sectionName);
93
94 services.AddBotApplication<TApp>(botConfig);
95
96 return services;
97 }
98
99 /// <summary>
100 /// Registers a custom bot application and its dependencies in the service collection.
101 /// </summary>
102 /// <typeparam name="TApp">The custom bot application type that inherits from BotApplication.</typeparam>
103 /// <param name="services">The service collection to add services to.</param>
104 /// <param name="botConfig">The configuration containing Azure AD settings.</param>
105 /// <returns>The service collection for method chaining.</returns>
106 public static IServiceCollection AddBotApplication<TApp>(this IServiceCollection services, BotConfig botConfig) where TApp : BotApplication
107 {
108 ArgumentNullException.ThrowIfNull(botConfig);
109 services.AddSingleton<BotApplicationOptions>(_ => new BotApplicationOptions { AppId = botConfig.ClientId });
110 services.AddHttpContextAccessor();
111 services.AddBotAuthorization(botConfig);
112 services.EnsureMsalServices(botConfig);
113 services.AddBotClient<ConversationClient>(ConversationClient.ConversationHttpClientName, botConfig);
114 services.AddBotClient<UserTokenClient>(UserTokenClient.UserTokenHttpClientName, botConfig);
115 services.AddSingleton<TApp>();
116 return services;
117 }
118
119 /// <summary>
120 /// Registers the <see cref="ConversationClient"/> and its dependencies in the service collection.
121 /// </summary>
122 /// <param name="services">The service collection to add services to.</param>
123 /// <param name="sectionName">The configuration section name containing Azure AD settings. Defaults to "AzureAd".</param>
124 /// <returns>The service collection for method chaining.</returns>
125 public static IServiceCollection AddConversationClient(this IServiceCollection services, string sectionName = BotConfig.DefaultSectionName)
126 {
127 BotConfig botConfig = BotConfig.Resolve(services, sectionName);
128 return services.EnsureMsalServices(botConfig)
129 .AddBotClient<ConversationClient>(ConversationClient.ConversationHttpClientName, botConfig);
130 }
131
132 /// <summary>
133 /// Registers the <see cref="UserTokenClient"/> and its dependencies in the service collection.
134 /// </summary>
135 /// <param name="services">The service collection to add services to.</param>
136 /// <param name="sectionName">The configuration section name containing Azure AD settings. Defaults to "AzureAd".</param>
137 /// <returns>The service collection for method chaining.</returns>
138 public static IServiceCollection AddUserTokenClient(this IServiceCollection services, string sectionName = BotConfig.DefaultSectionName)
139 {
140 BotConfig botConfig = BotConfig.Resolve(services, sectionName);
141 return services.EnsureMsalServices(botConfig)
142 .AddBotClient<UserTokenClient>(UserTokenClient.UserTokenHttpClientName, botConfig);
143 }
144
145 /// <summary>
146 /// Registers the shared MSAL token-acquisition pipeline and binds the named MSAL options.
147 /// </summary>
148 /// <remarks>
149 /// Safe to call multiple times: the Microsoft.Identity.Web service registrations are TryAdd-based,
150 /// and the named options binding (<see cref="MicrosoftIdentityApplicationOptions"/> and
151 /// <see cref="ManagedIdentityOptions"/>) appends an additional configure delegate per call. Those
152 /// delegates are idempotent against the same <see cref="BotConfig"/>, so re-running them produces
153 /// the same options state.
154 /// </remarks>
155 public static IServiceCollection EnsureMsalServices(this IServiceCollection services, BotConfig botConfig)
156 {
157 services.AddHttpClient()
158 .AddTokenAcquisition(true)
159 .AddInMemoryTokenCaches()
160 .AddAgentIdentities();
161
162 ArgumentNullException.ThrowIfNull(botConfig);
163 ArgumentNullException.ThrowIfNull(botConfig.MsalConfigurationSection);
164
165 if (!string.IsNullOrWhiteSpace(botConfig.ClientId))
166 {
167 services.Configure<MicrosoftIdentityApplicationOptions>(botConfig.SectionName, options =>
168 {
169 botConfig.MsalConfigurationSection.Bind(options);
170
171 // Default Instance when only TenantId is configured.
172 if (string.IsNullOrEmpty(options.Instance) && !string.IsNullOrEmpty(options.TenantId))
173 {
174 options.Instance = "https://login.microsoftonline.com/";
175 }
176
177 // MicrosoftEntraApplicationOptions.Authority is a computed property that
178 // returns Instance/TenantId/v2.0 when _authority is null. MergedOptions
179 // then sees Authority alongside Instance+TenantId and emits a warning
180 // (event 500). Setting Authority to empty prevents the computed value
181 // from propagating while Instance+TenantId remain available for MSAL.
182 if (!string.IsNullOrEmpty(options.Instance) && !string.IsNullOrEmpty(options.TenantId))
183 {
184 options.Authority = string.Empty;
185 }
186 });
187
188 // No ClientCredentials in the configured section implies pure User-Assigned Managed Identity:
189 // the bot's ClientId is the UMI's clientId (as in ABS bots with the UserAssignedMSI app type).
190 // Register ManagedIdentityOptions so BotAuthenticationHandler routes token acquisition through
191 // the IMDS endpoint instead of the standard app-credentials flow.
192 if (botConfig.IsUserAssignedManagedIdentity)
193 {
194 ILogger logger = GetLoggerFromServices(services);
195 logger.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 /// Gets a logger instance from the service collection.
243 /// If the logger factory is not available as an instance, builds a temporary service provider to create the logger.
244 /// </summary>
245 /// <param name="services">The service collection to extract the logger from.</param>
246 /// <param name="categoryType">The type to use for the logger category. If null, uses AddBotApplicationExtensions.</param>
247 /// <returns>An ILogger instance, or NullLogger if no logger factory is registered.</returns>
248 internal static ILogger GetLoggerFromServices(IServiceCollection services, Type? categoryType = null)
249 {
250 ServiceDescriptor? loggerFactoryDescriptor = services.FirstOrDefault(d => d.ServiceType == typeof(ILoggerFactory));
251 ILoggerFactory? loggerFactory = loggerFactoryDescriptor?.ImplementationInstance as ILoggerFactory;
252
253 // If logger factory is available as an instance, use it directly
254 if (loggerFactory != null)
255 {
256 return loggerFactory.CreateLogger(categoryType ?? typeof(AddBotApplicationExtensions));
257 }
258
259 // Logger factory not available as a direct instance; return NullLogger
260 // to avoid building a throwaway ServiceProvider during DI configuration.
261 return Extensions.Logging.Abstractions.NullLogger.Instance;
262 }
263}
264