// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.Options; using Microsoft.Identity.Abstractions; using Microsoft.Identity.Web; using Microsoft.Identity.Web.TokenCacheProviders.InMemory; using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.Teams.Core; using Microsoft.Teams.Core.Hosting; namespace PABot { internal static class InitTeamsBotAdapter { private const string DefaultScope = "https://api.botframework.com/.default"; private const string AdapterKeyName = "BotAdapter"; /// /// Configuration values for MSAL identity (bot or agent). /// private sealed record MsalIdentityConfig { public required IConfigurationSection ConfigSection { get; init; } public required string ClientId { get; init; } public required string TenantId { get; init; } public required string Scope { get; init; } public required string Instance { get; init; } } /// /// Configuration values for a bot adapter. /// private sealed record AdapterConfig { public MsalIdentityConfig? BotIdentity { get; init; } public MsalIdentityConfig? AgentIdentity { get; init; } } public static IServiceCollection AddTeamsBotApplications(this IServiceCollection services) { // Register shared services (needed once for all adapters) services.AddHttpClient(); services.AddTokenAcquisition(true); services.AddInMemoryTokenCaches(); services.AddAgentIdentities(); services.AddHttpContextAccessor(); // Register adapter using standard MsalBot/MsalAgent configuration RegisterTeamsBotApplication(services); return services; } private static void RegisterTeamsBotApplication(IServiceCollection services) { // Read configuration for this adapter AdapterConfig config = ReadAdapterConfig(services); // Set up token validation (authentication schemes and authorization policy) ConfigureTokenValidation(services, config); // Register MSAL options for token acquisition ConfigureMsalOptions(services, config); // Register the routed token acquisition service RegisterRoutedTokenService(services, config); // Register HTTP clients with auth handlers RegisterHttpClients(services, config); // Register Bot Framework clients RegisterBotClients(services, config); } private static AdapterConfig ReadAdapterConfig(IServiceCollection services) { IConfiguration configuration = services.BuildServiceProvider() .GetRequiredService(); IConfigurationSection msalBotSection = configuration.GetSection("MsalBot"); IConfigurationSection msalAgentSection = configuration.GetSection("MsalAgent"); // Read bot identity configuration if provided MsalIdentityConfig? botIdentity = null; string? botClientId = msalBotSection["ClientId"]; if (!string.IsNullOrEmpty(botClientId)) { botIdentity = new MsalIdentityConfig { ConfigSection = msalBotSection, ClientId = botClientId, TenantId = msalBotSection["TenantId"] ?? string.Empty, Scope = msalBotSection["Scope"] ?? DefaultScope, Instance = msalBotSection["Instance"] ?? "https://login.microsoftonline.com/" }; } // Read agent identity configuration if provided MsalIdentityConfig? agentIdentity = null; string? agentClientId = msalAgentSection["ClientId"]; if (!string.IsNullOrEmpty(agentClientId)) { agentIdentity = new MsalIdentityConfig { ConfigSection = msalAgentSection, ClientId = agentClientId, TenantId = msalAgentSection["TenantId"] ?? string.Empty, Scope = msalAgentSection["Scope"] ?? DefaultScope, Instance = msalAgentSection["Instance"] ?? botIdentity?.Instance ?? "https://login.microsoftonline.com/" }; } // At least one identity must be configured if (botIdentity is null && agentIdentity is null) { throw new InvalidOperationException("At least one identity (MsalBot or MsalAgent) must be configured with a ClientId"); } return new AdapterConfig { BotIdentity = botIdentity, AgentIdentity = agentIdentity }; } private static void ConfigureTokenValidation(IServiceCollection services, AdapterConfig config) { // This demonstrates an edge case scenario where two token validation schemes are registered // with different audiences (client IDs). The authorization policy will succeed if EITHER // scheme validates successfully - only one token needs to pass, not both. // Use case: When a bot is also registered as an agentic application and needs to accept // tokens from both the bot registration AND the agentic application registration. AuthenticationBuilder authBuilder = services.AddAuthentication(); // Configure authentication schemes for bot identity if present string? botScheme = null; if (config.BotIdentity is not null) { botScheme = "MsalBot"; authBuilder.AddBotAuthentication(config.BotIdentity.ClientId, config.BotIdentity.TenantId, botScheme); } // Configure authentication schemes for agent identity if present string? agentScheme = null; if (config.AgentIdentity is not null) { agentScheme = "MsalAgent"; authBuilder.AddBotAuthentication(config.AgentIdentity.ClientId, config.AgentIdentity.TenantId, agentScheme); } // Create policy scheme that routes based on token audience authBuilder.AddPolicyScheme(AdapterKeyName, AdapterKeyName, options => { options.ForwardDefaultSelector = context => SelectAuthenticationScheme(context, config, botScheme, agentScheme); }); // Create authorization policy services.AddAuthorizationBuilder() .AddPolicy(AdapterKeyName, policy => { policy.AuthenticationSchemes.Add(AdapterKeyName); policy.RequireAuthenticatedUser(); }); } private static string SelectAuthenticationScheme( HttpContext context, AdapterConfig config, string? botScheme, string? agentScheme) { // Default to first available scheme string defaultScheme = botScheme ?? agentScheme ?? throw new InvalidOperationException("No authentication scheme configured"); string? authHeader = context.Request.Headers.Authorization.ToString(); if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) { return defaultScheme; } try { string token = authHeader["Bearer ".Length..].Trim(); JsonWebToken jwt = new(token); string? audience = jwt.GetClaim("aud")?.Value; // Check bot identity if (config.BotIdentity is not null && botScheme is not null && (audience == config.BotIdentity.ClientId || audience == $"api://{config.BotIdentity.ClientId}")) { return botScheme; } // Check agent identity if (config.AgentIdentity is not null && agentScheme is not null && (audience == config.AgentIdentity.ClientId || audience == $"api://{config.AgentIdentity.ClientId}")) { return agentScheme; } } catch { // If token parsing fails, default to first available scheme } return defaultScheme; } private static void ConfigureMsalOptions(IServiceCollection services, AdapterConfig config) { // Configure MSAL options for bot identity if present - bind directly from MsalBot configuration section if (config.BotIdentity is not null) { services.Configure("MsalBot", config.BotIdentity.ConfigSection); } // Configure MSAL options for agent identity if present - bind directly from MsalAgent configuration section if (config.AgentIdentity is not null) { services.Configure("MsalAgent", config.AgentIdentity.ConfigSection); } } private static void RegisterRoutedTokenService(IServiceCollection services, AdapterConfig config) { services.AddSingleton(sp => { return new RoutedTokenAcquisitionService( config.BotIdentity is not null, config.AgentIdentity is not null, sp.GetRequiredService(), sp.GetRequiredService>()); }); } private static void RegisterHttpClients(IServiceCollection services, AdapterConfig config) { services.AddHttpClient("ConversationClient") .AddHttpMessageHandler(sp => CreatePACustomAuthHandler(sp, config)); services.AddHttpClient("UserTokenClient") .AddHttpMessageHandler(sp => CreatePACustomAuthHandler(sp, config)); services.AddHttpClient("TeamsApiClient") .AddHttpMessageHandler(sp => CreatePACustomAuthHandler(sp, config)); } private static void RegisterBotClients(IServiceCollection services, AdapterConfig config) { // Register ConversationClient services.AddSingleton(sp => { HttpClient httpClient = sp.GetRequiredService() .CreateClient("ConversationClient"); return new ConversationClient(httpClient, sp.GetRequiredService>()); }); // Register UserTokenClient services.AddSingleton(sp => { HttpClient httpClient = sp.GetRequiredService() .CreateClient("UserTokenClient"); return new UserTokenClient( httpClient, sp.GetRequiredService(), sp.GetRequiredService>()); }); // Register TeamsBotApplication services.AddSingleton(sp => { return new BotApplication( sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService>() ); }); } private static DelegatingHandler CreatePACustomAuthHandler(IServiceProvider sp, AdapterConfig config) { // Use bot scope if available, otherwise use agent scope string? botScope = config.BotIdentity?.Scope; string? agentScope = config.AgentIdentity?.Scope; return new PACustomAuthHandler( sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService>(), botScope ?? agentScope ?? DefaultScope, agentScope, sp.GetService>()); } } }