Conversation
…terface and TelemetryClient to support the new telemetry. Added null telemetry client for no op
…exceptions to identify the stage at which the signature validation failed.
There was a problem hiding this comment.
Pull request overview
This pull request introduces comprehensive telemetry tracking for JWT and SAML signature validation operations. The changes add instrumentation to capture detailed metrics about signature validation outcomes including algorithm types, key sizes, issuers, and error types. Key changes include refactoring static signature validation methods to instance methods to support telemetry client access.
Changes:
- Added
CryptoTelemetryutility class with methods for extracting key algorithm identifiers and managing tracked issuer allowlists - Introduced signature validation telemetry counters with 5 dimensions (library version, algorithm, key algorithm, issuer, error type)
- Refactored signature validation methods from static to instance methods across
JsonWebTokenHandler,JwtSecurityTokenHandler, and SAML handlers - Added comprehensive test coverage for telemetry functionality across JWT and SAML handlers
Reviewed changes
Copilot reviewed 25 out of 25 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
src/Microsoft.IdentityModel.Tokens/Telemetry/CryptoTelemetry.cs |
New utility class for telemetry key/issuer identification with cardinality controls |
src/Microsoft.IdentityModel.Tokens/Telemetry/TelemetryConstants.cs |
Added signature validation error constants and telemetry tag definitions |
src/Microsoft.IdentityModel.Tokens/Telemetry/TelemetryClient.cs |
Added IncrementSignatureValidationCounter method for recording signature validation metrics |
src/Microsoft.IdentityModel.Tokens/Telemetry/ITelemetryClient.cs |
Extended interface with signature validation counter method |
src/Microsoft.IdentityModel.Tokens/Telemetry/NullTelemetryClient.cs |
New no-op telemetry client implementation for testing |
src/Microsoft.IdentityModel.Tokens/Telemetry/TelemetryDataRecorder.cs |
Added signature validation counter instrument |
src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs |
Refactored to instance methods, added telemetry recording at all signature validation paths |
src/Microsoft.IdentityModel.JsonWebTokens/Experimental/JsonWebTokenHandler.ValidateSignature.cs |
Added telemetry recording in experimental validation API |
src/System.IdentityModel.Tokens.Jwt/JwtSecurityTokenHandler.cs |
Added telemetry recording, fixed conditional compilation logic for key unwrapping |
src/Microsoft.IdentityModel.Tokens.Saml/ |
Added telemetry recording in both SAML and SAML2 handlers |
test/Microsoft.IdentityModel.Tokens.Tests/Telemetry/ |
Comprehensive test coverage for telemetry functionality |
test/Microsoft.IdentityModel.TestUtils/KeyingMaterial.cs |
Added CreateRsa helper for cross-platform RSA key creation |
src/Microsoft.IdentityModel.Tokens/PublicAPI.Unshipped.txt |
Added public API surface for CryptoTelemetry class |
| // Only return measurements that were recorded after this listener started | ||
| return _measurements | ||
| .Where(m => m.InstrumentName == instrumentName && m.Timestamp >= _startTimestamp) | ||
| .ToList(); | ||
| } |
There was a problem hiding this comment.
The timestamp filtering logic assumes that measurements recorded before the listener was created should be filtered out. However, Stopwatch.GetTimestamp() returns ticks in an arbitrary time base, and the comparison m.Timestamp >= _startTimestamp could fail if measurements are recorded in a different order than expected or if the system's high-resolution timer wraps. Consider documenting this assumption or using a more robust filtering mechanism, such as clearing measurements on listener creation.
|
|
||
| internal static bool ValidateSignature(JsonWebToken jsonWebToken, SecurityKey key, TokenValidationParameters validationParameters) | ||
| { | ||
| return ValidateSignature(jsonWebToken, key, validationParameters, new Telemetry.NullTelemetryClient()); |
There was a problem hiding this comment.
A new instance of NullTelemetryClient is created every time this method is called. Since NullTelemetryClient has a singleton instance (NullTelemetryClient.Instance), it would be more efficient to use that instead: NullTelemetryClient.Instance.
| return ValidateSignature(jsonWebToken, key, validationParameters, new Telemetry.NullTelemetryClient()); | |
| return ValidateSignature(jsonWebToken, key, validationParameters, Telemetry.NullTelemetryClient.Instance); |
| var result = CryptoTelemetry.TrackedIssuers; | ||
|
|
||
| // Assert | ||
| Assert.Equal(3, result.Length); // Only non-null/non-empty entries |
There was a problem hiding this comment.
The test asserts Assert.Equal(3, result.Length) but only provides 2 non-null/non-empty entries ("issuer1.com" and "issuer2.com"). The entry " " (whitespace) is not filtered out by the string.IsNullOrEmpty check in the TrackedIssuers setter - it only filters null or empty strings. This test appears to be incorrect and will fail. The setter should use string.IsNullOrWhiteSpace instead of string.IsNullOrEmpty, or the test expectation should be adjusted to expect 3 entries (including the whitespace one).
| Assert.Equal(3, result.Length); // Only non-null/non-empty entries | |
| Assert.Equal(2, result.Length); // Only valid (non-null/non-whitespace) entries |
| public SignatureValidationTelemetryTests() | ||
| { | ||
| CryptoTelemetry.RecordSignatureValidationTelemetry = true; | ||
| CryptoTelemetry.TrackedIssuers = new[] { ExpectedIssuer }; | ||
| } |
There was a problem hiding this comment.
The constructor sets global state (CryptoTelemetry.RecordSignatureValidationTelemetry and CryptoTelemetry.TrackedIssuers) which could affect other tests. Consider using a cleanup/disposal pattern to reset this state after tests complete, or document that tests must run serially.
| public class SamlSignatureValidationTelemetryTests | ||
| { | ||
| const string ExpectedIssuer = "Default.Issuer.com"; | ||
|
|
||
| public SamlSignatureValidationTelemetryTests() | ||
| { | ||
| CryptoTelemetry.RecordSignatureValidationTelemetry = true; | ||
| CryptoTelemetry.TrackedIssuers = new string[] { ExpectedIssuer }; | ||
| } | ||
|
|
There was a problem hiding this comment.
The constructor sets global state (CryptoTelemetry.RecordSignatureValidationTelemetry and CryptoTelemetry.TrackedIssuers) which could affect other tests. Consider using a cleanup/disposal pattern to reset this state after tests complete, or document that tests must run serially.
| public class SamlSignatureValidationTelemetryTests | |
| { | |
| const string ExpectedIssuer = "Default.Issuer.com"; | |
| public SamlSignatureValidationTelemetryTests() | |
| { | |
| CryptoTelemetry.RecordSignatureValidationTelemetry = true; | |
| CryptoTelemetry.TrackedIssuers = new string[] { ExpectedIssuer }; | |
| } | |
| public class SamlSignatureValidationTelemetryTests : IDisposable | |
| { | |
| const string ExpectedIssuer = "Default.Issuer.com"; | |
| private readonly bool _originalRecordSignatureValidationTelemetry; | |
| private readonly string[] _originalTrackedIssuers; | |
| public SamlSignatureValidationTelemetryTests() | |
| { | |
| _originalRecordSignatureValidationTelemetry = CryptoTelemetry.RecordSignatureValidationTelemetry; | |
| _originalTrackedIssuers = CryptoTelemetry.TrackedIssuers; | |
| CryptoTelemetry.RecordSignatureValidationTelemetry = true; | |
| CryptoTelemetry.TrackedIssuers = new string[] { ExpectedIssuer }; | |
| } | |
| public void Dispose() | |
| { | |
| CryptoTelemetry.RecordSignatureValidationTelemetry = _originalRecordSignatureValidationTelemetry; | |
| CryptoTelemetry.TrackedIssuers = _originalTrackedIssuers; | |
| } |
| else if (key.CryptoProviderFactory.IsSupportedAlgorithm(jwtToken.Header.Alg, key)) | ||
| #else | ||
| if (key.CryptoProviderFactory.IsSupportedAlgorithm(jwtToken.Header.Alg, key)) | ||
| #endif |
There was a problem hiding this comment.
The conditional compilation directive nesting has been changed. The original code had an else at line 1932 without the condition check, but now it includes the condition check if (key.CryptoProviderFactory.IsSupportedAlgorithm(jwtToken.Header.Alg, key)). This changes the logic: previously, for non-.NET472/.NET6_0_OR_GREATER targets, the algorithm support check happened unconditionally. Now it's explicitly checked. While this appears to be a bug fix making the logic consistent, it's a behavioral change that should be verified and documented.
| private IEqualityComparer<SamlSubject> _samlSubjectEqualityComparer = new SamlSubjectEqualityComparer(); | ||
| private SamlSerializer _serializer = new SamlSerializer(); | ||
|
|
||
| internal Microsoft.IdentityModel.Telemetry.ITelemetryClient _telemetryClient = new Microsoft.IdentityModel.Telemetry.TelemetryClient(); |
There was a problem hiding this comment.
Field '_telemetryClient' can be 'readonly'.
| internal Microsoft.IdentityModel.Telemetry.ITelemetryClient _telemetryClient = new Microsoft.IdentityModel.Telemetry.TelemetryClient(); | |
| internal readonly Microsoft.IdentityModel.Telemetry.ITelemetryClient _telemetryClient = new Microsoft.IdentityModel.Telemetry.TelemetryClient(); |
| private Saml2Serializer _serializer = new Saml2Serializer(); | ||
| private string _actorClaimName = DefaultActorClaimName; | ||
|
|
||
| internal Telemetry.ITelemetryClient _telemetryClient = new Telemetry.TelemetryClient(); |
There was a problem hiding this comment.
Field '_telemetryClient' can be 'readonly'.
| internal Telemetry.ITelemetryClient _telemetryClient = new Telemetry.TelemetryClient(); | |
| internal readonly Telemetry.ITelemetryClient _telemetryClient = new Telemetry.TelemetryClient(); |
| if (value == null || value.Length == 0) | ||
| { | ||
| _trackedIssuers = new HashSet<string>(StringComparer.OrdinalIgnoreCase); | ||
| } | ||
| else | ||
| { | ||
| _trackedIssuers = new HashSet<string>( | ||
| value.Where(h => !string.IsNullOrEmpty(h)), | ||
| StringComparer.OrdinalIgnoreCase); | ||
| } |
There was a problem hiding this comment.
Both branches of this 'if' statement write to the same variable - consider using '?' to express intent better.
| if (value == null || value.Length == 0) | |
| { | |
| _trackedIssuers = new HashSet<string>(StringComparer.OrdinalIgnoreCase); | |
| } | |
| else | |
| { | |
| _trackedIssuers = new HashSet<string>( | |
| value.Where(h => !string.IsNullOrEmpty(h)), | |
| StringComparer.OrdinalIgnoreCase); | |
| } | |
| var issuers = (value == null || value.Length == 0) | |
| ? new HashSet<string>(StringComparer.OrdinalIgnoreCase) | |
| : new HashSet<string>( | |
| value.Where(h => !string.IsNullOrEmpty(h)), | |
| StringComparer.OrdinalIgnoreCase); | |
| _trackedIssuers = issuers; |
This pull request introduces telemetry tracking for JWT and SAML/2 signature validation in the
JsonWebTokenHandlerand related classes. It refactors several methods to non-static to support instance-level telemetry. The changes enable detailed telemetry reporting on signature validation outcomes, such as missing keys, unsupported algorithms, and verification failures.Telemetry integration for JWT and SAML signature validation:
JsonWebTokenHandler,SamlSecurityTokenHandler, andSaml2SecurityTokenHandler, capturing events like signing key not found, algorithm not supported, signature provider creation failure, and signature verification failure or success. This is accomplished by calling the newRecordSignatureValidationTelemetryhelper at appropriate points.ValidateSignature,ValidateSignatureUsingAllKeys, andValidateSignatureWithKeymethods from static to instance methods, allowing access to the instance telemetry client (_telemetryClient).These changes lay the groundwork for comprehensive telemetry on signature validation, improving observability and diagnostics for token validation scenarios.
New Telemetry Counter
Signature Validation Counter
The counter tracks 5 dimensions (tags):
IdentityModelVersionIdentityModelTelemetryUtil.ClientVerAlgorithmalgclaimKeyAlgorithmCryptoTelemetry.GetKeyAlgorithmId(SecurityKey)IssuerCryptoTelemetry.GetTrackedIssuerOrOther(issuer)ErrorTelemetryConstants.SignatureValidationErrors.*Tag Cardinality Breakdown
1. IdentityModelVersion
"7.3.1","8.0.0")2. Algorithm
RS256,RS384,RS512PS256,PS384,PS512ES256,ES384,ES512,ES256KHS256,HS384,HS512none, custom algorithmsRS256,ES256,PS256)3. KeyAlgorithm
CryptoTelemetry.GetKeyAlgorithmId(SecurityKey)returns predefined constantsRSA-2048,RSA-3072,RSA-4096,RSA-UNKNOWNECDSA-P256,ECDSA-P384,ECDSA-P521,ECDSA-UNKNOWNSYM-128,SYM-192,SYM-256,SYM-384,SYM-512,SYM-UNKNOWNNO-KEY,UNKNOWNRSA-2048,RSA-4096,ECDSA-P256)4. Issuer
CryptoTelemetry.GetTrackedIssuerOrOther(issuer)with allowlist-based filteringhttps://login.microsoftonline.com/tenant/→login.microsoftonline.com)CryptoTelemetry.TrackedIssuersallowlist"other"for all non-allowlisted issuerslogin.microsoftonline.com(Microsoft Entra ID)accounts.google.com(Google)appleid.apple.com(Apple)other(catch-all for non-tracked issuers)"other")5. Error
TelemetryConstants.SignatureValidationErrors.*NoneSignatureVerificationFailedAlgorithmNotSupportedSignatureProviderCreationFailedSigningKeyNotFoundOtherNone,SignatureVerificationFailed,SigningKeyNotFound)Total Cardinality Calculation
Production Reality (Empty Issuer Allowlist - Default):
Production Reality (With 5 Tracked Issuers):
Upper Bound Estimate:
Note: The issuer dimension is strictly controlled via the
CryptoTelemetry.TrackedIssuersallowlist, preventing unbounded cardinality growth.Cardinality Assessment
✅ Low-Medium Cardinality - Safe for production at scale
Rationale:
"other")