// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace Microsoft.Teams.Core.Hosting; /// /// Configuration model for bot authentication credentials, sourced from a /// Microsoft.Identity.Web compatible configuration section (e.g. "AzureAd"). /// public sealed class BotConfig { internal const string DefaultSectionName = "AzureAd"; internal const string BotFrameworkSectionName = "BotFramework"; internal const string DefaultOpenIdMetadataUrl = "https://login.botframework.com/v1/.well-known/openid-configuration"; internal const string DefaultEntraInstance = "https://login.microsoftonline.com/"; internal const string DefaultBotTokenIssuer = "https://api.botframework.com"; /// /// Gets or sets the Azure AD tenant ID. /// public string TenantId { get; set; } = string.Empty; /// /// Gets or sets the application (client) ID from Azure AD app registration. /// public string ClientId { get; set; } = string.Empty; /// /// Gets or sets the configuration section name used to resolve this BotConfig. /// Also used as the MSAL named-options key and the JWT auth scheme name. /// public string SectionName { get; set; } = DefaultSectionName; /// /// Gets or sets the Bot Framework OpenID metadata URL used to fetch signing keys /// for validating inbound Bot Framework tokens. For sovereign clouds, set /// BotFramework:OpenIdMetadataUrl in configuration, e.g. /// "https://login.botframework.azure.us/v1/.well-known/openid-configuration" for USGov. /// Defaults to the public-cloud endpoint when not configured. /// public string OpenIdMetadataUrl { get; set; } = DefaultOpenIdMetadataUrl; /// /// Gets or sets the Entra login instance used when validating Entra-issued tokens. /// For sovereign clouds, set {SectionName}:Instance in configuration /// (the standard Microsoft.Identity.Web key), e.g. /// "https://login.microsoftonline.us/" for USGov. /// Defaults to the public-cloud instance when not configured. /// public string EntraInstance { get; set; } = DefaultEntraInstance; /// /// Gets or sets the expected Bot Framework token issuer used to validate inbound /// Bot Framework tokens. For sovereign clouds, set BotFramework:BotTokenIssuer /// in configuration, e.g. "https://api.botframework.us" for USGov. /// Defaults to the public-cloud issuer when not configured. /// public string BotTokenIssuer { get; set; } = DefaultBotTokenIssuer; internal IConfigurationSection? MsalConfigurationSection { get; set; } /// /// Gets a value indicating whether this configuration uses User-Assigned Managed Identity (UMI) for authentication. /// Returns true when no ClientCredentials are configured in the section. /// internal bool IsUserAssignedManagedIdentity => MsalConfigurationSection is not null && !MsalConfigurationSection.GetSection("ClientCredentials").GetChildren().Any(); /// /// Resolves a BotConfig from a service collection by extracting configuration and logger. /// /// The service collection containing IConfiguration and ILoggerFactory registrations. /// The configuration section name. Defaults to "AzureAd". /// A BotConfig populated from the section, or an empty BotConfig if no ClientId is configured. public static BotConfig Resolve(IServiceCollection services, string sectionName = DefaultSectionName) { ArgumentNullException.ThrowIfNull(services); IConfiguration? configuration = AddBotApplicationExtensions.ResolveFromServicesPreHost(services); if (configuration is null) { throw new InvalidOperationException( "IConfiguration must be registered in the service collection before calling BotConfig.Resolve. " + "Ensure AddConfiguration() or WebApplication.CreateBuilder() has been called."); } IConfigurationSection section = configuration.GetSection(sectionName); IConfigurationSection botFrameworkSection = configuration.GetSection(BotFrameworkSectionName); BotConfig config = new() { TenantId = section["TenantId"] ?? string.Empty, ClientId = section["ClientId"] ?? string.Empty, EntraInstance = ResolveAbsoluteUri(section, "Instance", DefaultEntraInstance), OpenIdMetadataUrl = ResolveAbsoluteUri(botFrameworkSection, "OpenIdMetadataUrl", DefaultOpenIdMetadataUrl), BotTokenIssuer = ResolveAbsoluteUri(botFrameworkSection, "BotTokenIssuer", DefaultBotTokenIssuer), MsalConfigurationSection = section, SectionName = sectionName }; AddBotApplicationExtensions.LogFromServices(services, l => { if (!string.IsNullOrEmpty(config.ClientId)) _logUsingSectionConfig(l, sectionName, null); else _logNoConfigFound(l, null); }, typeof(BotConfig)); return config; } private static string ResolveAbsoluteUri(IConfigurationSection section, string key, string defaultValue) { ArgumentNullException.ThrowIfNull(section); string? value = section[key]; if (value is null) { return defaultValue; } if (!Uri.TryCreate(value, UriKind.Absolute, out _)) { throw new InvalidOperationException( $"Configuration value '{section.Key}:{key}' is not a valid absolute URI: '{value}'."); } return value; } private static readonly Action _logUsingSectionConfig = LoggerMessage.Define(LogLevel.Debug, new(3), "Resolved bot configuration from '{SectionName}' configuration section"); private static readonly Action _logNoConfigFound = LoggerMessage.Define(LogLevel.Warning, new(4), "No bot configuration found in configuration."); }