microsoft/teams.net

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
copilot/sub-pr-338

Branches

Tags

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

Clone

HTTPS

Download ZIP

core/src/Microsoft.Teams.Bot.Core/Hosting/JwtExtensions.cs

286lines · modecode

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4using System.IdentityModel.Tokens.Jwt;
5using System.Linq;
6using Microsoft.AspNetCore.Authentication;
7using Microsoft.AspNetCore.Authentication.JwtBearer;
8using Microsoft.AspNetCore.Authorization;
9using Microsoft.Extensions.Configuration;
10using Microsoft.Extensions.DependencyInjection;
11using Microsoft.Extensions.Logging;
12using Microsoft.IdentityModel.Protocols;
13using Microsoft.IdentityModel.Protocols.OpenIdConnect;
14using Microsoft.IdentityModel.Tokens;
15using Microsoft.IdentityModel.Validators;
16
17namespace Microsoft.Teams.Bot.Core.Hosting
18{
19 /// <summary>
20 /// Provides extension methods for configuring JWT authentication and authorization for bots and agents.
21 /// </summary>
22 public static class JwtExtensions
23 {
24 internal const string BotScheme = "BotScheme";
25 internal const string AgentScheme = "AgentScheme";
26 internal const string BotScope = "https://api.botframework.com/.default";
27 internal const string AgentScope = "https://botapi.skype.com/.default";
28 internal const string BotOIDC = "https://login.botframework.com/v1/.well-known/openid-configuration";
29 internal const string AgentOIDC = "https://login.microsoftonline.com/";
30
31 /// <summary>
32 /// Adds JWT authentication for bots and agents.
33 /// </summary>
34 /// <param name="services">The service collection to add authentication to.</param>
35 /// <param name="configuration">The application configuration containing the settings.</param>
36 /// <param name="useAgentAuth">Indicates whether to use agent authentication (true) or bot authentication (false).</param>
37 /// <param name="aadSectionName">The configuration section name for the settings. Defaults to "AzureAd".</param>
38 /// <param name="logger">The logger instance for logging.</param>
39 /// <returns>An <see cref="AuthenticationBuilder"/> for further authentication configuration.</returns>
40 public static AuthenticationBuilder AddBotAuthentication(this IServiceCollection services, IConfiguration configuration, bool useAgentAuth, ILogger logger, string aadSectionName = "AzureAd")
41 {
42
43 // TODO: Task 5039187: Refactor use of BotConfig for MSAL and JWT
44
45 AuthenticationBuilder builder = services.AddAuthentication();
46 ArgumentNullException.ThrowIfNull(configuration);
47 string audience = configuration[$"{aadSectionName}:ClientId"]
48 ?? configuration["CLIENT_ID"]
49 ?? configuration["MicrosoftAppId"]
50 ?? throw new InvalidOperationException("ClientID not found in configuration, tried the 3 option");
51
52 if (!useAgentAuth)
53 {
54 string[] validIssuers = ["https://api.botframework.com"];
55 builder.AddCustomJwtBearer(BotScheme, validIssuers, audience, logger);
56 }
57 else
58 {
59 string tenantId = configuration[$"{aadSectionName}:TenantId"]
60 ?? configuration["TENANT_ID"]
61 ?? configuration["MicrosoftAppTenantId"]
62 ?? "botframework.com"; // TODO: Task 5039198: Test JWT Validation for MultiTenant
63
64 string[] validIssuers = [$"https://sts.windows.net/{tenantId}/", $"https://login.microsoftonline.com/{tenantId}/v2", "https://api.botframework.com"];
65 builder.AddCustomJwtBearer(AgentScheme, validIssuers, audience, logger);
66 }
67 return builder;
68 }
69
70 /// <summary>
71 /// Adds authorization policies to the service collection.
72 /// </summary>
73 /// <param name="services">The service collection to add authorization to.</param>
74 /// <param name="aadSectionName">The configuration section name for the settings. Defaults to "AzureAd".</param>
75 /// <param name="logger">Optional logger instance for logging. If null, a NullLogger will be used.</param>
76 /// <returns>An <see cref="AuthorizationBuilder"/> for further authorization configuration.</returns>
77 public static AuthorizationBuilder AddAuthorization(this IServiceCollection services, ILogger? logger = null, string aadSectionName = "AzureAd")
78 {
79 // Use NullLogger if no logger provided
80 logger ??= Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance;
81
82 // We need IConfiguration to determine which authentication scheme to register (Bot vs Agent)
83 // This is a registration-time decision that cannot be deferred
84 // Try to get it from service descriptors first (fast path)
85 var configDescriptor = services.FirstOrDefault(d => d.ServiceType == typeof(IConfiguration));
86 IConfiguration? configuration = configDescriptor?.ImplementationInstance as IConfiguration;
87
88 // If not available as ImplementationInstance, build a temporary ServiceProvider
89 // NOTE: This is generally an anti-pattern, but acceptable here because:
90 // 1. We need configuration at registration time to select auth scheme
91 // 2. We properly dispose the temporary ServiceProvider immediately
92 // 3. This only happens once during application startup
93 if (configuration == null)
94 {
95 using var tempProvider = services.BuildServiceProvider();
96 configuration = tempProvider.GetRequiredService<IConfiguration>();
97 }
98
99 string? azureScope = configuration["Scope"];
100 bool useAgentAuth = string.Equals(azureScope, AgentScope, StringComparison.OrdinalIgnoreCase);
101
102 services.AddBotAuthentication(configuration, useAgentAuth, logger, aadSectionName);
103 AuthorizationBuilder authorizationBuilder = services
104 .AddAuthorizationBuilder()
105 .AddDefaultPolicy("DefaultPolicy", policy =>
106 {
107 if (!useAgentAuth)
108 {
109 policy.AuthenticationSchemes.Add(BotScheme);
110 }
111 else
112 {
113 policy.AuthenticationSchemes.Add(AgentScheme);
114 }
115 policy.RequireAuthenticatedUser();
116 });
117 return authorizationBuilder;
118 }
119
120 private static AuthenticationBuilder AddCustomJwtBearer(this AuthenticationBuilder builder, string schemeName, string[] validIssuers, string audience, ILogger? logger)
121 {
122 builder.AddJwtBearer(schemeName, jwtOptions =>
123 {
124 jwtOptions.SaveToken = true;
125 jwtOptions.IncludeErrorDetails = true;
126 jwtOptions.Audience = audience;
127 jwtOptions.TokenValidationParameters = new TokenValidationParameters
128 {
129 ValidateIssuerSigningKey = true,
130 RequireSignedTokens = true,
131 ValidateIssuer = true,
132 ValidateAudience = true,
133 ValidIssuers = validIssuers
134 };
135 jwtOptions.TokenValidationParameters.EnableAadSigningKeyIssuerValidation();
136 jwtOptions.MapInboundClaims = true;
137 jwtOptions.Events = new JwtBearerEvents
138 {
139 OnMessageReceived = async context =>
140 {
141 // Resolve logger at runtime from request services to ensure we always have proper logging
142 var loggerFactory = context.HttpContext.RequestServices.GetService<ILoggerFactory>();
143 var requestLogger = loggerFactory?.CreateLogger(typeof(JwtExtensions).FullName ?? "JwtExtensions")
144 ?? logger
145 ?? Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance;
146
147 requestLogger.LogDebug("OnMessageReceived invoked for scheme: {Scheme}", schemeName);
148 string authorizationHeader = context.Request.Headers.Authorization.ToString();
149
150 if (string.IsNullOrEmpty(authorizationHeader))
151 {
152 // Default to AadTokenValidation handling
153 context.Options.TokenValidationParameters.ConfigurationManager ??= jwtOptions.ConfigurationManager as BaseConfigurationManager;
154 await Task.CompletedTask.ConfigureAwait(false);
155 requestLogger.LogWarning("Authorization header is missing for scheme: {Scheme}", schemeName);
156 return;
157 }
158
159 string[] parts = authorizationHeader?.Split(' ')!;
160 if (parts.Length != 2 || parts[0] != "Bearer")
161 {
162 // Default to AadTokenValidation handling
163 context.Options.TokenValidationParameters.ConfigurationManager ??= jwtOptions.ConfigurationManager as BaseConfigurationManager;
164 await Task.CompletedTask.ConfigureAwait(false);
165 requestLogger.LogWarning("Invalid authorization header format for scheme: {Scheme}", schemeName);
166 return;
167 }
168
169 JwtSecurityToken token = new(parts[1]);
170 string issuer = token.Claims.FirstOrDefault(claim => claim.Type == "iss")?.Value!;
171 string tid = token.Claims.FirstOrDefault(claim => claim.Type == "tid")?.Value!;
172
173 string oidcAuthority = issuer.Equals("https://api.botframework.com", StringComparison.OrdinalIgnoreCase)
174 ? BotOIDC : $"{AgentOIDC}{tid ?? "botframework.com"}/v2.0/.well-known/openid-configuration";
175
176 requestLogger.LogDebug("Using OIDC Authority: {OidcAuthority} for issuer: {Issuer}", oidcAuthority, issuer);
177
178 jwtOptions.ConfigurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(
179 oidcAuthority,
180 new OpenIdConnectConfigurationRetriever(),
181 new HttpDocumentRetriever
182 {
183 RequireHttps = jwtOptions.RequireHttpsMetadata
184 });
185
186
187 await Task.CompletedTask.ConfigureAwait(false);
188 },
189 OnTokenValidated = context =>
190 {
191 // Resolve logger at runtime
192 var loggerFactory = context.HttpContext.RequestServices.GetService<ILoggerFactory>();
193 var requestLogger = loggerFactory?.CreateLogger(typeof(JwtExtensions).FullName ?? "JwtExtensions")
194 ?? logger
195 ?? Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance;
196
197 requestLogger.LogInformation("Token validated successfully for scheme: {Scheme}", schemeName);
198 return Task.CompletedTask;
199 },
200 OnForbidden = context =>
201 {
202 // Resolve logger at runtime
203 var loggerFactory = context.HttpContext.RequestServices.GetService<ILoggerFactory>();
204 var requestLogger = loggerFactory?.CreateLogger(typeof(JwtExtensions).FullName ?? "JwtExtensions")
205 ?? logger
206 ?? Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance;
207
208 requestLogger.LogWarning("Forbidden response for scheme: {Scheme}", schemeName);
209 return Task.CompletedTask;
210 },
211 OnAuthenticationFailed = context =>
212 {
213 // Resolve logger at runtime to ensure authentication failures are always logged
214 var loggerFactory = context.HttpContext.RequestServices.GetService<ILoggerFactory>();
215 var requestLogger = loggerFactory?.CreateLogger(typeof(JwtExtensions).FullName ?? "JwtExtensions")
216 ?? logger
217 ?? Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance;
218
219 // Extract detailed information for troubleshooting
220 string? tokenAudience = null;
221 string? tokenIssuer = null;
222 string? tokenExpiration = null;
223 string? tokenSubject = null;
224
225 try
226 {
227 // Try to parse the token to extract claims
228 string authHeader = context.Request.Headers.Authorization.ToString();
229 if (!string.IsNullOrEmpty(authHeader) && authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
230 {
231 string tokenString = authHeader.Substring("Bearer ".Length).Trim();
232 var token = new JwtSecurityToken(tokenString);
233
234 tokenAudience = token.Audiences?.FirstOrDefault();
235 tokenIssuer = token.Issuer;
236 tokenExpiration = token.ValidTo.ToString("o");
237 tokenSubject = token.Subject;
238 }
239 }
240#pragma warning disable CA1031 // Do not catch general exception types - we want to continue logging even if token parsing fails
241 catch
242 {
243 // If we can't parse the token, continue with logging the exception
244 }
245#pragma warning restore CA1031
246
247 // Get configured validation parameters
248 var validationParams = context.Options?.TokenValidationParameters;
249 string configuredAudience = validationParams?.ValidAudience ?? "null";
250 string configuredAudiences = validationParams?.ValidAudiences != null
251 ? string.Join(", ", validationParams.ValidAudiences)
252 : "null";
253 string configuredIssuers = validationParams?.ValidIssuers != null
254 ? string.Join(", ", validationParams.ValidIssuers)
255 : "null";
256
257 // Log detailed failure information
258 requestLogger.LogError(context.Exception,
259 "JWT Authentication failed for scheme: {Scheme}\n" +
260 " Failure Reason: {ExceptionMessage}\n" +
261 " Token Audience: {TokenAudience}\n" +
262 " Expected Audience: {ConfiguredAudience}\n" +
263 " Expected Audiences: {ConfiguredAudiences}\n" +
264 " Token Issuer: {TokenIssuer}\n" +
265 " Valid Issuers: {ConfiguredIssuers}\n" +
266 " Token Expiration: {TokenExpiration}\n" +
267 " Token Subject: {TokenSubject}",
268 schemeName,
269 context.Exception.Message,
270 tokenAudience ?? "Unable to parse",
271 configuredAudience,
272 configuredAudiences,
273 tokenIssuer ?? "Unable to parse",
274 configuredIssuers,
275 tokenExpiration ?? "Unable to parse",
276 tokenSubject ?? "Unable to parse");
277
278 return Task.CompletedTask;
279 }
280 };
281 jwtOptions.Validate();
282 });
283 return builder;
284 }
285 }
286}
287