microsoft/teams.net

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
feature/extended-markdown-text-format

Branches

Tags

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

Clone

HTTPS

Download ZIP

core/src/Microsoft.Teams.Core/Hosting/BotAuthenticationHandler.cs

166lines · modecode

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4using System.Diagnostics;
5using System.IdentityModel.Tokens.Jwt;
6using System.Net.Http.Headers;
7using Microsoft.Extensions.Logging;
8using Microsoft.Extensions.Options;
9using Microsoft.Identity.Abstractions;
10using Microsoft.Identity.Web;
11using Microsoft.Teams.Core.Diagnostics;
12using Microsoft.Teams.Core.Schema;
13
14namespace Microsoft.Teams.Core.Hosting;
15
16/// <summary>
17/// HTTP message handler that automatically acquires and attaches authentication tokens
18/// for Bot Framework API calls. Supports both app-only and agentic (user-delegated) token acquisition.
19/// </summary>
20/// <remarks>
21/// Initializes a new instance of the <see cref="BotAuthenticationHandler"/> class.
22/// </remarks>
23/// <param name="authorizationHeaderProvider">The authorization header provider for acquiring tokens.</param>
24/// <param name="logger">The logger instance.</param>
25/// <param name="authenticationOptionsName">The name of the MSAL configuration options to use for token acquisition. Defaults to "AzureAd".</param>
26/// <param name="managedIdentityOptions">Optional managed identity options monitor. When the named entry matching <paramref name="authenticationOptionsName"/> has a non-empty <c>UserAssignedClientId</c>, tokens are acquired via the IMDS endpoint as the configured managed identity instead of via the app-credentials flow.</param>
27internal sealed class BotAuthenticationHandler(
28 IAuthorizationHeaderProvider authorizationHeaderProvider,
29 ILogger<BotAuthenticationHandler> logger,
30 string? authenticationOptionsName = null,
31 IOptionsMonitor<ManagedIdentityOptions>? managedIdentityOptions = null) : DelegatingHandler
32{
33 private const string AgenticScope = "https://botapi.skype.com/.default";
34 private const string BotAppScope = "https://api.botframework.com/.default";
35
36 private readonly IAuthorizationHeaderProvider _authorizationHeaderProvider = authorizationHeaderProvider ?? throw new ArgumentNullException(nameof(authorizationHeaderProvider));
37 private readonly ILogger<BotAuthenticationHandler> _logger = logger ?? throw new ArgumentNullException(nameof(logger));
38 private readonly IOptionsMonitor<ManagedIdentityOptions>? _managedIdentityOptions = managedIdentityOptions;
39 private static readonly Action<ILogger, string, Exception?> _logAgenticToken =
40 LoggerMessage.Define<string>(LogLevel.Debug, new(2), "Acquiring agentic token for AgenticAppId {AgenticAppId}");
41 private static readonly Action<ILogger, string, Exception?> _logAppOnlyToken =
42 LoggerMessage.Define<string>(LogLevel.Debug, new(3), "Acquiring app-only token for scope: {Scope}");
43 private static readonly Action<ILogger, string, Exception?> _logTokenClaims =
44 LoggerMessage.Define<string>(LogLevel.Trace, new(4), "Acquired token claims:{Claims}");
45 private static readonly Action<ILogger, string, Exception?> _logInvalidAgenticUserId =
46 LoggerMessage.Define<string>(LogLevel.Warning, new(5), "Invalid AgenticUserId '{AgenticUserId}'; falling back to app-only token.");
47 private static readonly Action<ILogger, Exception?> _logTokenParseFailure =
48 LoggerMessage.Define(LogLevel.Warning, new(6), "Failed to parse JWT token for trace logging.");
49
50 /// <summary>
51 /// Key used to store the agentic identity in HttpRequestMessage options.
52 /// </summary>
53 public static readonly HttpRequestOptionsKey<AgenticIdentity?> AgenticIdentityKey = new("AgenticIdentity");
54
55 /// <inheritdoc/>
56 protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
57 {
58 request.Options.TryGetValue(AgenticIdentityKey, out AgenticIdentity? agenticIdentity);
59
60 string token = await GetAuthorizationHeaderAsync(agenticIdentity, cancellationToken).ConfigureAwait(false);
61
62 string tokenValue = token.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)
63 ? token["Bearer ".Length..]
64 : token;
65
66 LogTokenClaims(tokenValue);
67
68 request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokenValue);
69
70 return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
71 }
72
73 /// <summary>
74 /// Gets an authorization header for Bot Framework API calls.
75 /// Supports both app-only and agentic (user-delegated) token acquisition.
76 /// </summary>
77 /// <param name="agenticIdentity">Optional agentic identity for user-delegated token acquisition. If not provided, acquires an app-only token.</param>
78 /// <param name="cancellationToken">Cancellation token.</param>
79 /// <returns>The authorization header value.</returns>
80 private async Task<string> GetAuthorizationHeaderAsync(AgenticIdentity? agenticIdentity, CancellationToken cancellationToken)
81 {
82 string optionsName = authenticationOptionsName ?? BotConfig.DefaultSectionName;
83 using Activity? span = Telemetry.Source.StartActivity(Telemetry.Spans.AuthOutbound, ActivityKind.Client);
84
85
86 try
87 {
88 AuthorizationHeaderProviderOptions options = new()
89 {
90 AcquireTokenOptions = new AcquireTokenOptions()
91 {
92 AuthenticationOptionsName = optionsName,
93 }
94 };
95
96 // Conditionally apply ManagedIdentity configuration if registered
97 if (_managedIdentityOptions is not null)
98 {
99 ManagedIdentityOptions miOptions = _managedIdentityOptions.Get(optionsName);
100
101 if (!string.IsNullOrEmpty(miOptions.UserAssignedClientId))
102 {
103 _logger.InferringUserAssignedManagedIdentity(miOptions.UserAssignedClientId);
104 options.AcquireTokenOptions.ManagedIdentity = miOptions;
105 span?.SetTag(Telemetry.Tags.AuthFlow, "managed_identity");
106 }
107 }
108
109 if (agenticIdentity is not null &&
110 !string.IsNullOrEmpty(agenticIdentity.AgenticAppId) &&
111 !string.IsNullOrEmpty(agenticIdentity.AgenticUserId))
112 {
113 span?.SetTag(Telemetry.Tags.AuthScope, AgenticScope);
114 _logAgenticToken(_logger, agenticIdentity.AgenticAppId, null);
115
116 if (!Guid.TryParse(agenticIdentity.AgenticUserId, out Guid agenticUserGuid))
117 {
118 _logInvalidAgenticUserId(_logger, agenticIdentity.AgenticUserId, null);
119 }
120 else
121 {
122 span?.SetTag(Telemetry.Tags.AuthFlow, "agentic");
123 options.WithAgentUserIdentity(agenticIdentity.AgenticAppId, agenticUserGuid);
124 string token = await _authorizationHeaderProvider.CreateAuthorizationHeaderAsync([AgenticScope], options, null, cancellationToken).ConfigureAwait(false);
125 return token;
126 }
127 }
128 span?.SetTag(Telemetry.Tags.AuthScope, BotAppScope);
129 _logAppOnlyToken(_logger, BotAppScope, null);
130 // Don't overwrite a more specific flow (managed_identity) already set above.
131 if (span is not null && !span.TagObjects.Any(t => t.Key == Telemetry.Tags.AuthFlow))
132 {
133 span.SetTag(Telemetry.Tags.AuthFlow, "app_only");
134 }
135 string appToken = await _authorizationHeaderProvider.CreateAuthorizationHeaderForAppAsync(BotAppScope, options, cancellationToken).ConfigureAwait(false);
136
137
138 return appToken;
139 }
140 catch (Exception ex)
141 {
142 span.RecordException(ex);
143 throw;
144 }
145 }
146
147 private void LogTokenClaims(string token)
148 {
149 if (!_logger.IsEnabled(LogLevel.Trace))
150 {
151 return;
152 }
153
154
155 try
156 {
157 JwtSecurityToken jwtToken = new(token);
158 string claims = Environment.NewLine + string.Join(Environment.NewLine, jwtToken.Claims.Select(c => $" {c.Type}: {c.Value}"));
159 _logTokenClaims(_logger, claims, null);
160 }
161 catch (ArgumentException ex)
162 {
163 _logTokenParseFailure(_logger, ex);
164 }
165 }
166}
167