Skip to content

Commit f817240

Browse files
authored
Add telemetry around signature validation (#3410) (#3415)
* Added helper class and methods to track signature validation telemetry. Added tests * Added a counter for signature validation. Updated ITelemetryClient interface and TelemetryClient to support the new telemetry. Added null telemetry client for no op * Added telemetry to JWT and SAML/SAML2 handlers. Expanded catching of exceptions to identify the stage at which the signature validation failed. * Added tests * Addressed Copilot's feedback * Reverted readonly changes to allow setting the telemetry client in tests * Replaced lock with volatile modifier for immutable array, removed the hashset in favour of the array to avoid converting in the getter. * Updated CryptoTelemetry's public API and tests * Added benchmarks for signature validation telemetry * Removed issuer caching from CryptoTelemetry signature validation telemetry. Updated tests * Updated public API to match the updated enable telemetry method * Addressed PR feedback - Rename _telemetryClient to TelemetryClient in all token handlers and update all references accordingly. - Simplify CryptoTelemetry.GetTrackedIssuerOrOther: now matches tracked issuers by substring (case-insensitive) instead of parsing host; remove ExtractHostFromIssuer. - Update comments to clarify substring matching and its limitations. - Remove TelemetryConfiguration enum from benchmarks; update benchmark attributes and tracked issuer values for consistency. - Refactor and expand CryptoTelemetry tests: remove host extraction tests, consolidate key algorithm ID tests, and add more scenarios for tracked issuer matching and allowlist filtering. - Update API documentation to reflect field renaming. - Overall, unify telemetry client usage and streamline issuer tracking logic, with tests updated to match new behavior. * Removed case insensitive comparison for telemetry issuer extraction based on PR feedback (cherry picked from commit 61449b8)
1 parent bcd41b7 commit f817240

29 files changed

Lines changed: 2207 additions & 43 deletions

benchmark/Microsoft.IdentityModel.Benchmarks/ValidateTokenAsyncTests.cs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using BenchmarkDotNet.Attributes;
1111
using BenchmarkDotNet.Configs;
1212
using Microsoft.IdentityModel.JsonWebTokens;
13+
using Microsoft.IdentityModel.Telemetry;
1314
using Microsoft.IdentityModel.Tokens;
1415
using Microsoft.IdentityModel.Tokens.Experimental;
1516

@@ -193,4 +194,75 @@ public async Task<List<Claim>> JsonWebTokenHandler_ValidateTokenAsyncWithVP_Crea
193194
return claims.ToList();
194195
}
195196
}
197+
198+
// ===== Telemetry Impact Benchmarks =====
199+
// "Tracking" in this context refers to tracking signature validation for specific issuer hosts.
200+
// When enabled, telemetry collects data for the configured hosts (e.g., "contoso.com").
201+
// Every other host is reported as "other" to avoid excessive cardinality in telemetry.
202+
// These benchmarks measure the performance impact of enabling telemetry overall, and of tracking specific hosts.
203+
[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]
204+
public class ValidateTokenAsyncTests_TelemetryImpact
205+
{
206+
private const int IterationCount = 10000;
207+
private JsonWebTokenHandler _jsonWebTokenHandler;
208+
private string _jwsClaims;
209+
private TokenValidationParameters _tokenValidationParameters;
210+
211+
[GlobalSetup]
212+
public void GlobalSetup()
213+
{
214+
var tokenDescriptorClaims = new SecurityTokenDescriptor
215+
{
216+
Claims = BenchmarkUtils.Claims,
217+
SigningCredentials = BenchmarkUtils.SigningCredentialsRsaSha256,
218+
};
219+
220+
_jsonWebTokenHandler = new JsonWebTokenHandler();
221+
_jwsClaims = _jsonWebTokenHandler.CreateToken(tokenDescriptorClaims);
222+
223+
_tokenValidationParameters = new TokenValidationParameters()
224+
{
225+
ValidAudience = BenchmarkUtils.Audience,
226+
ValidateLifetime = true,
227+
ValidIssuer = BenchmarkUtils.Issuer,
228+
IssuerSigningKey = BenchmarkUtils.SigningCredentialsRsaSha256.Key,
229+
};
230+
}
231+
232+
[IterationSetup(Target = nameof(JsonWebTokenHandler_ValidateTokenAsync_TelemetryDisabled))]
233+
public void Setup_TelemetryDisabled()
234+
{
235+
CryptoTelemetry.EnableSignatureValidationTelemetry(false, null);
236+
}
237+
238+
[BenchmarkCategory("ValidateTokenAsync_TelemetryImpact"), Benchmark(Baseline = true, OperationsPerInvoke = IterationCount)]
239+
public async Task<TokenValidationResult> JsonWebTokenHandler_ValidateTokenAsync_TelemetryDisabled()
240+
{
241+
return await _jsonWebTokenHandler.ValidateTokenAsync(_jwsClaims, _tokenValidationParameters).ConfigureAwait(false);
242+
}
243+
244+
[IterationSetup(Target = nameof(JsonWebTokenHandler_ValidateTokenAsync_TelemetryEnabledNoTracking))]
245+
public void Setup_TelemetryEnabledNoTracking()
246+
{
247+
CryptoTelemetry.EnableSignatureValidationTelemetry(true, null);
248+
}
249+
250+
[BenchmarkCategory("ValidateTokenAsync_TelemetryImpact"), Benchmark(OperationsPerInvoke = IterationCount)]
251+
public async Task<TokenValidationResult> JsonWebTokenHandler_ValidateTokenAsync_TelemetryEnabledNoTracking()
252+
{
253+
return await _jsonWebTokenHandler.ValidateTokenAsync(_jwsClaims, _tokenValidationParameters).ConfigureAwait(false);
254+
}
255+
256+
[IterationSetup(Target = nameof(JsonWebTokenHandler_ValidateTokenAsync_TelemetryEnabledWithTracking))]
257+
public void Setup_TelemetryEnabledWithTracking()
258+
{
259+
CryptoTelemetry.EnableSignatureValidationTelemetry(true, new[] { "contoso.com" });
260+
}
261+
262+
[BenchmarkCategory("ValidateTokenAsync_TelemetryImpact"), Benchmark(OperationsPerInvoke = IterationCount)]
263+
public async Task<TokenValidationResult> JsonWebTokenHandler_ValidateTokenAsync_TelemetryEnabledWithTracking()
264+
{
265+
return await _jsonWebTokenHandler.ValidateTokenAsync(_jwsClaims, _tokenValidationParameters).ConfigureAwait(false);
266+
}
267+
}
196268
}

src/Microsoft.IdentityModel.JsonWebTokens/Experimental/JsonWebTokenHandler.ValidateSignature.cs

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Collections.Generic;
66
using System.Text;
77
using Microsoft.IdentityModel.Logging;
8+
using Microsoft.IdentityModel.Telemetry;
89
using Microsoft.IdentityModel.Tokens;
910
using Microsoft.IdentityModel.Tokens.Experimental;
1011
using TokenLogMessages = Microsoft.IdentityModel.Tokens.LogMessages;
@@ -23,7 +24,7 @@ public partial class JsonWebTokenHandler : TokenHandler
2324
/// <param name="configuration">The optional configuration used for validation.</param>
2425
/// <param name="callContext">The context in which the method is called.</param>
2526
/// <returns>A <see cref="ValidationResult{SecurityKey, ValidationError}"/> with the <see cref="SecurityKey"/> that signed the tokenif valid or a <see cref="ValidationError"/>.</returns>
26-
internal static ValidationResult<SecurityKey, ValidationError> ValidateSignature(
27+
internal ValidationResult<SecurityKey, ValidationError> ValidateSignature(
2728
JsonWebToken jwtToken,
2829
ValidationParameters validationParameters,
2930
BaseConfiguration? configuration,
@@ -101,6 +102,12 @@ internal static ValidationResult<SecurityKey, ValidationError> ValidateSignature
101102
if (validationParameters.TryAllSigningKeys)
102103
return ValidateSignatureUsingAllKeys(jwtToken, validationParameters, configuration, callContext);
103104

105+
RecordSignatureValidationTelemetry(
106+
TelemetryClient,
107+
TelemetryConstants.SignatureValidationErrors.SigningKeyNotFound,
108+
jwtToken,
109+
key: null);
110+
104111
// kid was NOT found, no matching keys available.
105112
if (string.IsNullOrEmpty(jwtToken.Kid))
106113
{
@@ -126,7 +133,7 @@ internal static ValidationResult<SecurityKey, ValidationError> ValidateSignature
126133
ValidationError.GetCurrentStackFrame());
127134
}
128135

129-
private static ValidationResult<SecurityKey, ValidationError> ValidateSignatureUsingAllKeys(
136+
private ValidationResult<SecurityKey, ValidationError> ValidateSignatureUsingAllKeys(
130137
JsonWebToken jwtToken,
131138
ValidationParameters validationParameters,
132139
BaseConfiguration? configuration,
@@ -218,7 +225,7 @@ private static ValidationResult<SecurityKey, ValidationError> ValidateSignatureU
218225
ValidationError.GetCurrentStackFrame());
219226
}
220227

221-
private static ValidationResult<SecurityKey, ValidationError> ValidateSignatureWithKey(
228+
private ValidationResult<SecurityKey, ValidationError> ValidateSignatureWithKey(
222229
JsonWebToken jsonWebToken,
223230
ValidationParameters validationParameters,
224231
SecurityKey key,
@@ -229,6 +236,12 @@ private static ValidationResult<SecurityKey, ValidationError> ValidateSignatureW
229236
CryptoProviderFactory cryptoProviderFactory = validationParameters.CryptoProviderFactory ?? key.CryptoProviderFactory;
230237
if (!cryptoProviderFactory.IsSupportedAlgorithm(jsonWebToken.Alg, key))
231238
{
239+
RecordSignatureValidationTelemetry(
240+
TelemetryClient,
241+
TelemetryConstants.SignatureValidationErrors.AlgorithmNotSupported,
242+
jsonWebToken,
243+
key);
244+
232245
return new SignatureValidationError(
233246
new MessageDetail(
234247
TokenLogMessages.IDX10652,
@@ -242,13 +255,21 @@ private static ValidationResult<SecurityKey, ValidationError> ValidateSignatureW
242255
try
243256
{
244257
if (signatureProvider == null)
258+
{
259+
RecordSignatureValidationTelemetry(
260+
TelemetryClient,
261+
TelemetryConstants.SignatureValidationErrors.SignatureProviderCreationFailed,
262+
jsonWebToken,
263+
key);
264+
245265
return new SignatureValidationError(
246266
new MessageDetail(
247267
TokenLogMessages.IDX10636,
248268
LogHelper.MarkAsNonPII(key?.KeyId ?? "Null"),
249269
LogHelper.MarkAsNonPII(jsonWebToken.Alg)),
250270
ValidationFailureType.CryptoProviderReturnedNull,
251271
ValidationError.GetCurrentStackFrame());
272+
}
252273

253274
bool valid = EncodingUtils.PerformEncodingDependentOperation<bool, string, int, SignatureProvider>(
254275
jsonWebToken.EncodedToken,
@@ -262,21 +283,41 @@ private static ValidationResult<SecurityKey, ValidationError> ValidateSignatureW
262283

263284
if (valid)
264285
{
286+
RecordSignatureValidationTelemetry(
287+
TelemetryClient,
288+
TelemetryConstants.SignatureValidationErrors.None,
289+
jsonWebToken,
290+
key);
291+
265292
jsonWebToken.SigningKey = key;
266293
return key;
267294
}
268295
else
296+
{
297+
RecordSignatureValidationTelemetry(
298+
TelemetryClient,
299+
TelemetryConstants.SignatureValidationErrors.SignatureVerificationFailed,
300+
jsonWebToken,
301+
key);
302+
269303
return new SignatureValidationError(
270304
new MessageDetail(
271305
TokenLogMessages.IDX10520,
272306
LogHelper.MarkAsNonPII(key.ToString())),
273307
SignatureValidationFailure.ValidationFailed,
274308
ValidationError.GetCurrentStackFrame());
309+
}
275310
}
276311
#pragma warning disable CA1031 // Do not catch general exception types
277312
catch (Exception ex)
278313
#pragma warning restore CA1031 // Do not catch general exception types
279314
{
315+
RecordSignatureValidationTelemetry(
316+
TelemetryClient,
317+
TelemetryConstants.SignatureValidationErrors.SignatureVerificationFailed,
318+
jsonWebToken,
319+
key);
320+
280321
return new SignatureValidationError(
281322
new MessageDetail(
282323
TokenLogMessages.IDX10521,

src/Microsoft.IdentityModel.JsonWebTokens/InternalAPI.Shipped.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ Microsoft.IdentityModel.JsonWebTokens.JsonWebToken.ReadToken(System.ReadOnlyMemo
105105
Microsoft.IdentityModel.JsonWebTokens.JsonWebToken.Typ.set -> void
106106
Microsoft.IdentityModel.JsonWebTokens.JsonWebToken.ValidFromNullable.get -> System.DateTime?
107107
Microsoft.IdentityModel.JsonWebTokens.JsonWebToken.ValidToNullable.get -> System.DateTime?
108-
Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler._telemetryClient -> Microsoft.IdentityModel.Telemetry.ITelemetryClient
108+
Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.TelemetryClient -> Microsoft.IdentityModel.Telemetry.ITelemetryClient
109109
Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.GetContentEncryptionKeys(Microsoft.IdentityModel.JsonWebTokens.JsonWebToken jwtToken, Microsoft.IdentityModel.Tokens.TokenValidationParameters validationParameters, Microsoft.IdentityModel.Tokens.BaseConfiguration configuration) -> System.Collections.Generic.IEnumerable<Microsoft.IdentityModel.Tokens.SecurityKey>
110110
Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.GetContentEncryptionKeys(Microsoft.IdentityModel.JsonWebTokens.JsonWebToken jwtToken, Microsoft.IdentityModel.Tokens.ValidationParameters validationParameters, Microsoft.IdentityModel.Tokens.BaseConfiguration configuration, Microsoft.IdentityModel.Tokens.CallContext callContext) -> (System.Collections.Generic.IList<Microsoft.IdentityModel.Tokens.SecurityKey>, Microsoft.IdentityModel.Tokens.ValidationError)
111111
Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.StackFrames

0 commit comments

Comments
 (0)