microsoft/teams.net

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
next/oauth-card-null-ref-bug

Branches

Tags

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

Clone

HTTPS

Download ZIP

core/samples/PABot/InitCompatAdapter.cs

310lines · modecode

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4using Microsoft.AspNetCore.Authentication;
5using Microsoft.Extensions.Options;
6using Microsoft.Identity.Abstractions;
7using Microsoft.Identity.Web;
8using Microsoft.Identity.Web.TokenCacheProviders.InMemory;
9using Microsoft.IdentityModel.JsonWebTokens;
10using Microsoft.Teams.Bot.Apps;
11using Microsoft.Teams.Bot.Core;
12using Microsoft.Teams.Bot.Core.Hosting;
13
14namespace PABot
15{
16 internal static class InitCompatAdapter
17 {
18 private const string DefaultScope = "https://api.botframework.com/.default";
19 private const string AdapterKeyName = "BotAdapter";
20
21 /// <summary>
22 /// Configuration values for MSAL identity (bot or agent).
23 /// </summary>
24 private sealed record MsalIdentityConfig
25 {
26 public required IConfigurationSection ConfigSection { get; init; }
27 public required string ClientId { get; init; }
28 public required string TenantId { get; init; }
29 public required string Scope { get; init; }
30 public required string Instance { get; init; }
31 }
32
33 /// <summary>
34 /// Configuration values for a bot adapter.
35 /// </summary>
36 private sealed record AdapterConfig
37 {
38 public MsalIdentityConfig? BotIdentity { get; init; }
39 public MsalIdentityConfig? AgentIdentity { get; init; }
40 }
41
42 public static IServiceCollection AddTeamsBotApplications(this IServiceCollection services)
43 {
44 // Register shared services (needed once for all adapters)
45 services.AddHttpClient();
46 services.AddTokenAcquisition(true);
47 services.AddInMemoryTokenCaches();
48 services.AddAgentIdentities();
49 services.AddHttpContextAccessor();
50
51 // Register adapter using standard MsalBot/MsalAgent configuration
52 RegisterTeamsBotApplication(services);
53
54 return services;
55 }
56
57 private static void RegisterTeamsBotApplication(IServiceCollection services)
58 {
59 // Read configuration for this adapter
60 AdapterConfig config = ReadAdapterConfig(services);
61
62 // Set up token validation (authentication schemes and authorization policy)
63 ConfigureTokenValidation(services, config);
64
65 // Register MSAL options for token acquisition
66 ConfigureMsalOptions(services, config);
67
68 // Register the routed token acquisition service
69 RegisterRoutedTokenService(services, config);
70
71 // Register HTTP clients with auth handlers
72 RegisterHttpClients(services, config);
73
74 // Register Bot Framework clients
75 RegisterBotClients(services, config);
76 }
77
78 private static AdapterConfig ReadAdapterConfig(IServiceCollection services)
79 {
80 IConfiguration configuration = services.BuildServiceProvider()
81 .GetRequiredService<IConfiguration>();
82
83 IConfigurationSection msalBotSection = configuration.GetSection("MsalBot");
84 IConfigurationSection msalAgentSection = configuration.GetSection("MsalAgent");
85
86 // Read bot identity configuration if provided
87 MsalIdentityConfig? botIdentity = null;
88 string? botClientId = msalBotSection["ClientId"];
89 if (!string.IsNullOrEmpty(botClientId))
90 {
91 botIdentity = new MsalIdentityConfig
92 {
93 ConfigSection = msalBotSection,
94 ClientId = botClientId,
95 TenantId = msalBotSection["TenantId"] ?? string.Empty,
96 Scope = msalBotSection["Scope"] ?? DefaultScope,
97 Instance = msalBotSection["Instance"] ?? "https://login.microsoftonline.com/"
98 };
99 }
100
101 // Read agent identity configuration if provided
102 MsalIdentityConfig? agentIdentity = null;
103 string? agentClientId = msalAgentSection["ClientId"];
104 if (!string.IsNullOrEmpty(agentClientId))
105 {
106 agentIdentity = new MsalIdentityConfig
107 {
108 ConfigSection = msalAgentSection,
109 ClientId = agentClientId,
110 TenantId = msalAgentSection["TenantId"] ?? string.Empty,
111 Scope = msalAgentSection["Scope"] ?? DefaultScope,
112 Instance = msalAgentSection["Instance"] ?? botIdentity?.Instance ?? "https://login.microsoftonline.com/"
113 };
114 }
115
116 // At least one identity must be configured
117 if (botIdentity is null && agentIdentity is null)
118 {
119 throw new InvalidOperationException("At least one identity (MsalBot or MsalAgent) must be configured with a ClientId");
120 }
121
122 return new AdapterConfig
123 {
124 BotIdentity = botIdentity,
125 AgentIdentity = agentIdentity
126 };
127 }
128
129 private static void ConfigureTokenValidation(IServiceCollection services, AdapterConfig config)
130 {
131 // This demonstrates an edge case scenario where two token validation schemes are registered
132 // with different audiences (client IDs). The authorization policy will succeed if EITHER
133 // scheme validates successfully - only one token needs to pass, not both.
134 // Use case: When a bot is also registered as an agentic application and needs to accept
135 // tokens from both the bot registration AND the agentic application registration.
136
137 AuthenticationBuilder authBuilder = services.AddAuthentication();
138
139 // Configure authentication schemes for bot identity if present
140 string? botScheme = null;
141 if (config.BotIdentity is not null)
142 {
143 botScheme = "MsalBot";
144 authBuilder.AddBotAuthentication(config.BotIdentity.ClientId, config.BotIdentity.TenantId, botScheme);
145 }
146
147 // Configure authentication schemes for agent identity if present
148 string? agentScheme = null;
149 if (config.AgentIdentity is not null)
150 {
151 agentScheme = "MsalAgent";
152 authBuilder.AddBotAuthentication(config.AgentIdentity.ClientId, config.AgentIdentity.TenantId, agentScheme);
153 }
154
155 // Create policy scheme that routes based on token audience
156 authBuilder.AddPolicyScheme(AdapterKeyName, AdapterKeyName, options =>
157 {
158 options.ForwardDefaultSelector = context =>
159 SelectAuthenticationScheme(context, config, botScheme, agentScheme);
160 });
161
162 // Create authorization policy
163 services.AddAuthorizationBuilder()
164 .AddPolicy(AdapterKeyName, policy =>
165 {
166 policy.AuthenticationSchemes.Add(AdapterKeyName);
167 policy.RequireAuthenticatedUser();
168 });
169 }
170
171 private static string SelectAuthenticationScheme(
172 HttpContext context,
173 AdapterConfig config,
174 string? botScheme,
175 string? agentScheme)
176 {
177 // Default to first available scheme
178 string defaultScheme = botScheme ?? agentScheme ?? throw new InvalidOperationException("No authentication scheme configured");
179
180 string? authHeader = context.Request.Headers.Authorization.ToString();
181 if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
182 {
183 return defaultScheme;
184 }
185
186 try
187 {
188 string token = authHeader["Bearer ".Length..].Trim();
189 JsonWebToken jwt = new(token);
190 string? audience = jwt.GetClaim("aud")?.Value;
191
192 // Check bot identity
193 if (config.BotIdentity is not null && botScheme is not null &&
194 (audience == config.BotIdentity.ClientId || audience == $"api://{config.BotIdentity.ClientId}"))
195 {
196 return botScheme;
197 }
198
199 // Check agent identity
200 if (config.AgentIdentity is not null && agentScheme is not null &&
201 (audience == config.AgentIdentity.ClientId || audience == $"api://{config.AgentIdentity.ClientId}"))
202 {
203 return agentScheme;
204 }
205 }
206 catch
207 {
208 // If token parsing fails, default to first available scheme
209 }
210
211 return defaultScheme;
212 }
213
214 private static void ConfigureMsalOptions(IServiceCollection services, AdapterConfig config)
215 {
216 // Configure MSAL options for bot identity if present - bind directly from MsalBot configuration section
217 if (config.BotIdentity is not null)
218 {
219 services.Configure<MicrosoftIdentityApplicationOptions>("MsalBot", config.BotIdentity.ConfigSection);
220 }
221
222 // Configure MSAL options for agent identity if present - bind directly from MsalAgent configuration section
223 if (config.AgentIdentity is not null)
224 {
225 services.Configure<MicrosoftIdentityApplicationOptions>("MsalAgent", config.AgentIdentity.ConfigSection);
226 }
227 }
228
229 private static void RegisterRoutedTokenService(IServiceCollection services, AdapterConfig config)
230 {
231 services.AddSingleton<IRoutedTokenAcquisitionService>(sp =>
232 {
233 return new RoutedTokenAcquisitionService(
234 config.BotIdentity is not null,
235 config.AgentIdentity is not null,
236 sp.GetRequiredService<IAuthorizationHeaderProvider>(),
237 sp.GetRequiredService<ILogger<RoutedTokenAcquisitionService>>());
238 });
239 }
240
241 private static void RegisterHttpClients(IServiceCollection services, AdapterConfig config)
242 {
243 services.AddHttpClient("ConversationClient")
244 .AddHttpMessageHandler(sp => CreatePACustomAuthHandler(sp, config));
245
246 services.AddHttpClient("UserTokenClient")
247 .AddHttpMessageHandler(sp => CreatePACustomAuthHandler(sp, config));
248
249 services.AddHttpClient("TeamsApiClient")
250 .AddHttpMessageHandler(sp => CreatePACustomAuthHandler(sp, config));
251 }
252
253 private static void RegisterBotClients(IServiceCollection services, AdapterConfig config)
254 {
255 // Register ConversationClient
256 services.AddSingleton<ConversationClient>(sp =>
257 {
258 HttpClient httpClient = sp.GetRequiredService<IHttpClientFactory>()
259 .CreateClient("ConversationClient");
260 return new ConversationClient(httpClient, sp.GetRequiredService<ILogger<ConversationClient>>());
261 });
262
263 // Register UserTokenClient
264 services.AddSingleton<UserTokenClient>(sp =>
265 {
266 HttpClient httpClient = sp.GetRequiredService<IHttpClientFactory>()
267 .CreateClient("UserTokenClient");
268 return new UserTokenClient(
269 httpClient,
270 sp.GetRequiredService<IConfiguration>(),
271 sp.GetRequiredService<ILogger<UserTokenClient>>());
272 });
273
274 // Register TeamsApiClient
275 services.AddSingleton<TeamsApiClient>(sp =>
276 {
277 HttpClient httpClient = sp.GetRequiredService<IHttpClientFactory>()
278 .CreateClient("TeamsApiClient");
279 return new TeamsApiClient(httpClient, sp.GetRequiredService<ILogger<TeamsApiClient>>());
280 });
281
282 // Register TeamsBotApplication
283 services.AddSingleton<TeamsBotApplication>(sp =>
284 {
285 return new TeamsBotApplication(
286 sp.GetRequiredService<ConversationClient>(),
287 sp.GetRequiredService<UserTokenClient>(),
288 sp.GetRequiredService<TeamsApiClient>(),
289 sp.GetRequiredService<IHttpContextAccessor>(),
290 sp.GetRequiredService<ILogger<TeamsBotApplication>>()
291 );
292 });
293 }
294
295 private static DelegatingHandler CreatePACustomAuthHandler(IServiceProvider sp, AdapterConfig config)
296 {
297 // Use bot scope if available, otherwise use agent scope
298 string? botScope = config.BotIdentity?.Scope;
299 string? agentScope = config.AgentIdentity?.Scope;
300
301 return new PACustomAuthHandler(
302 sp.GetRequiredService<IAuthorizationHeaderProvider>(),
303 sp.GetRequiredService<IRoutedTokenAcquisitionService>(),
304 sp.GetRequiredService<ILogger<PACustomAuthHandler>>(),
305 botScope ?? agentScope ?? DefaultScope,
306 agentScope,
307 sp.GetService<IOptions<ManagedIdentityOptions>>());
308 }
309 }
310}
311