// 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>());
}
}
}