// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System.Diagnostics; using System.IdentityModel.Tokens.Jwt; using System.Net.Http.Headers; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Identity.Abstractions; using Microsoft.Identity.Web; using Microsoft.Teams.Core.Diagnostics; using Microsoft.Teams.Core.Schema; namespace Microsoft.Teams.Core.Hosting; /// /// HTTP message handler that automatically acquires and attaches authentication tokens /// for Bot Framework API calls. Supports both app-only and agentic (user-delegated) token acquisition. /// /// /// Initializes a new instance of the class. /// /// The authorization header provider for acquiring tokens. /// The logger instance. /// The name of the MSAL configuration options to use for token acquisition. Defaults to "AzureAd". /// Optional managed identity options monitor. When the named entry matching has a non-empty UserAssignedClientId, tokens are acquired via the IMDS endpoint as the configured managed identity instead of via the app-credentials flow. internal sealed class BotAuthenticationHandler( IAuthorizationHeaderProvider authorizationHeaderProvider, ILogger logger, string? authenticationOptionsName = null, IOptionsMonitor? managedIdentityOptions = null) : DelegatingHandler { private const string AgenticScope = "https://botapi.skype.com/.default"; private const string BotAppScope = "https://api.botframework.com/.default"; private readonly IAuthorizationHeaderProvider _authorizationHeaderProvider = authorizationHeaderProvider ?? throw new ArgumentNullException(nameof(authorizationHeaderProvider)); private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); private readonly IOptionsMonitor? _managedIdentityOptions = managedIdentityOptions; private static readonly Action _logAgenticToken = LoggerMessage.Define(LogLevel.Debug, new(2), "Acquiring agentic token for AgenticAppId {AgenticAppId}"); private static readonly Action _logAppOnlyToken = LoggerMessage.Define(LogLevel.Debug, new(3), "Acquiring app-only token for scope: {Scope}"); private static readonly Action _logTokenClaims = LoggerMessage.Define(LogLevel.Trace, new(4), "Acquired token claims:{Claims}"); private static readonly Action _logInvalidAgenticUserId = LoggerMessage.Define(LogLevel.Warning, new(5), "Invalid AgenticUserId '{AgenticUserId}'; falling back to app-only token."); private static readonly Action _logTokenParseFailure = LoggerMessage.Define(LogLevel.Warning, new(6), "Failed to parse JWT token for trace logging."); /// /// Key used to store the agentic identity in HttpRequestMessage options. /// public static readonly HttpRequestOptionsKey AgenticIdentityKey = new("AgenticIdentity"); /// protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { request.Options.TryGetValue(AgenticIdentityKey, out AgenticIdentity? agenticIdentity); string token = await GetAuthorizationHeaderAsync(agenticIdentity, cancellationToken).ConfigureAwait(false); string tokenValue = token.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase) ? token["Bearer ".Length..] : token; LogTokenClaims(tokenValue); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokenValue); return await base.SendAsync(request, cancellationToken).ConfigureAwait(false); } /// /// Gets an authorization header for Bot Framework API calls. /// Supports both app-only and agentic (user-delegated) token acquisition. /// /// Optional agentic identity for user-delegated token acquisition. If not provided, acquires an app-only token. /// Cancellation token. /// The authorization header value. private async Task GetAuthorizationHeaderAsync(AgenticIdentity? agenticIdentity, CancellationToken cancellationToken) { string optionsName = authenticationOptionsName ?? BotConfig.DefaultSectionName; using Activity? span = Telemetry.Source.StartActivity(Telemetry.Spans.AuthOutbound, ActivityKind.Client); try { AuthorizationHeaderProviderOptions options = new() { AcquireTokenOptions = new AcquireTokenOptions() { AuthenticationOptionsName = optionsName, } }; // Conditionally apply ManagedIdentity configuration if registered if (_managedIdentityOptions is not null) { ManagedIdentityOptions miOptions = _managedIdentityOptions.Get(optionsName); if (!string.IsNullOrEmpty(miOptions.UserAssignedClientId)) { _logger.InferringUserAssignedManagedIdentity(miOptions.UserAssignedClientId); options.AcquireTokenOptions.ManagedIdentity = miOptions; span?.SetTag(Telemetry.Tags.AuthFlow, "managed_identity"); } } if (agenticIdentity is not null && !string.IsNullOrEmpty(agenticIdentity.AgenticAppId) && !string.IsNullOrEmpty(agenticIdentity.AgenticUserId)) { span?.SetTag(Telemetry.Tags.AuthScope, AgenticScope); _logAgenticToken(_logger, agenticIdentity.AgenticAppId, null); if (!Guid.TryParse(agenticIdentity.AgenticUserId, out Guid agenticUserGuid)) { _logInvalidAgenticUserId(_logger, agenticIdentity.AgenticUserId, null); } else { span?.SetTag(Telemetry.Tags.AuthFlow, "agentic"); options.WithAgentUserIdentity(agenticIdentity.AgenticAppId, agenticUserGuid); string token = await _authorizationHeaderProvider.CreateAuthorizationHeaderAsync([AgenticScope], options, null, cancellationToken).ConfigureAwait(false); return token; } } span?.SetTag(Telemetry.Tags.AuthScope, BotAppScope); _logAppOnlyToken(_logger, BotAppScope, null); // Don't overwrite a more specific flow (managed_identity) already set above. if (span is not null && !span.TagObjects.Any(t => t.Key == Telemetry.Tags.AuthFlow)) { span.SetTag(Telemetry.Tags.AuthFlow, "app_only"); } string appToken = await _authorizationHeaderProvider.CreateAuthorizationHeaderForAppAsync(BotAppScope, options, cancellationToken).ConfigureAwait(false); return appToken; } catch (Exception ex) { span.RecordException(ex); throw; } } private void LogTokenClaims(string token) { if (!_logger.IsEnabled(LogLevel.Trace)) { return; } try { JwtSecurityToken jwtToken = new(token); string claims = Environment.NewLine + string.Join(Environment.NewLine, jwtToken.Claims.Select(c => $" {c.Type}: {c.Value}")); _logTokenClaims(_logger, claims, null); } catch (ArgumentException ex) { _logTokenParseFailure(_logger, ex); } } }