From 0a84572858c57c60642e0465f27872c3782b6e0b Mon Sep 17 00:00:00 2001 From: Ignacio Inglese Date: Wed, 31 Jul 2024 15:24:27 +0100 Subject: [PATCH 1/5] Refactored ValidateSignature to remove exceptions and some logs --- .../JsonWebTokenHandler.ValidateSignature.cs | 300 ++++++++++++++++++ .../Delegates.cs | 25 ++ .../Validation/SignatureValidationResult.cs | 47 +++ .../Validation/ValidationFailureType.cs | 6 + .../Validation/ValidationParameters.cs | 21 +- .../Validation/ValidationResult.cs | 2 +- 6 files changed, 395 insertions(+), 6 deletions(-) create mode 100644 src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateSignature.cs create mode 100644 src/Microsoft.IdentityModel.Tokens/Validation/SignatureValidationResult.cs diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateSignature.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateSignature.cs new file mode 100644 index 0000000000..89f3b4681a --- /dev/null +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateSignature.cs @@ -0,0 +1,300 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using Microsoft.IdentityModel.Abstractions; +using Microsoft.IdentityModel.JsonWebTokens.Results; +using Microsoft.IdentityModel.Logging; +using Microsoft.IdentityModel.Tokens; +using TokenLogMessages = Microsoft.IdentityModel.Tokens.LogMessages; + +namespace Microsoft.IdentityModel.JsonWebTokens +{ +#nullable enable + /// This partial class contains methods and logic related to the validation of tokens' signatures. + public partial class JsonWebTokenHandler : TokenHandler + { + /// + /// Validates the JWT signature. + /// + private static SignatureValidationResult ValidateSignature( + JsonWebToken jwtToken, + ValidationParameters validationParameters, + BaseConfiguration configuration, + CallContext callContext) + { + // Delegate is set by the user, we call it and return the result. + if (validationParameters.SignatureValidator is not null) + return validationParameters.SignatureValidator(jwtToken, validationParameters, configuration, callContext); + + // If the user wants to accept unsigned tokens, they must implement the delegate. + if (!jwtToken.IsSigned) + return new SignatureValidationResult( + ValidationFailureType.SignatureValidationFailed, + new ExceptionDetail( + new MessageDetail( + TokenLogMessages.IDX10504, + LogHelper.MarkAsSecurityArtifact( + jwtToken.EncodedToken, + JwtTokenUtilities.SafeLogJwtToken) + ), + typeof(SecurityTokenInvalidSignatureException), + new StackFrame())); + + SecurityKey? key = null; + if (validationParameters.IssuerSigningKeyResolver is not null) + { + key = validationParameters.IssuerSigningKeyResolver( + jwtToken.EncodedToken, + jwtToken, + jwtToken.Kid, + validationParameters, + configuration, + callContext); + } + else + { + // Resolve the key using the token's 'kid' and 'x5t' headers. + // Fall back to the validation parameters' keys if configuration keys are not set. + key = JwtTokenUtilities.ResolveTokenSigningKey(jwtToken.Kid, jwtToken.X5t, configuration.SigningKeys) + ?? JwtTokenUtilities.ResolveTokenSigningKey(jwtToken.Kid, jwtToken.X5t, validationParameters.IssuerSigningKeys); + } + + if (key is not null) + { + jwtToken.SigningKey = key; + + // If the key is found, validate the signature. + return ValidateSignatureWithKey(jwtToken, key, validationParameters, callContext); + } + + // Key could not be resolved. Depending on the configuration, try all keys or return an error. + if (validationParameters.TryAllIssuerSigningKeys) + return ValidateSignatureUsingAllKeys(jwtToken, validationParameters, configuration, callContext); + else + return new SignatureValidationResult( + ValidationFailureType.SignatureValidationFailed, + new ExceptionDetail( + new MessageDetail(TokenLogMessages.IDX10500), + typeof(SecurityTokenSignatureKeyNotFoundException), + new StackFrame())); + } + + private static SignatureValidationResult ValidateSignatureUsingAllKeys( + JsonWebToken jwtToken, + ValidationParameters + validationParameters, BaseConfiguration configuration, + CallContext callContext) + { + // control gets here if: + // 1. User specified delegate: IssuerSigningKeyResolver returned null + // 2. ResolveIssuerSigningKey returned null + // Try all the keys. This is the degenerate case, not concerned about perf. + ICollection keys = configuration?.SigningKeys ?? new List(); + if (!(keys is List keysList)) + keysList = keys.ToList(); + + if (!validationParameters.IssuerSigningKeys.IsNullOrEmpty()) + keysList.AddRange(validationParameters.IssuerSigningKeys); + + // keep track of exceptions thrown, keys that were tried + StringBuilder? exceptionStrings = null; + StringBuilder? keysAttempted = null; + var kidExists = !string.IsNullOrEmpty(jwtToken.Kid); + var kidMatched = false; + + for (int i = 0; i < keysList.Count; i++) + { + SecurityKey key = keysList[i]; + SignatureValidationResult result = ValidateSignatureWithKey( + jwtToken, + key, + validationParameters, + callContext); + + if (result.IsValid) + { + if (LogHelper.IsEnabled(EventLogLevel.Informational)) + LogHelper.LogInformation(TokenLogMessages.IDX10242, jwtToken); + + jwtToken.SigningKey = key; + return result; // return the first valid signature. + } + else + (exceptionStrings ??= new StringBuilder()).AppendLine(result.ExceptionDetail?.MessageDetail.Message ?? "Null"); + + if (key != null) + { + (keysAttempted ??= new StringBuilder()).Append(key.ToString()).Append(" , KeyId: ").AppendLine(key.KeyId); + if (kidExists && !kidMatched && key.KeyId != null) + kidMatched = jwtToken.Kid.Equals(key.KeyId, key is X509SecurityKey ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); + } + } + + // No valid signature found. Return the exception details. + return new SignatureValidationResult( + ValidationFailureType.SignatureValidationFailed, + GetSignatureValidationFailureExceptionDetails( + jwtToken, + validationParameters, + configuration, + exceptionStrings, + keysAttempted, + kidExists, + kidMatched)); + } + + private static SignatureValidationResult ValidateSignatureWithKey( + JsonWebToken jsonWebToken, + SecurityKey key, + ValidationParameters validationParameters, + CallContext callContext) + { + var cryptoProviderFactory = validationParameters.CryptoProviderFactory ?? key.CryptoProviderFactory; + if (!cryptoProviderFactory.IsSupportedAlgorithm(jsonWebToken.Alg, key)) + { + return new SignatureValidationResult( + ValidationFailureType.SignatureValidationFailed, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX14000, + LogHelper.MarkAsNonPII(jsonWebToken.Alg), + key), + typeof(SecurityTokenInvalidAlgorithmException), + new StackFrame())); + } + + AlgorithmValidationResult result = validationParameters.AlgorithmValidator( + jsonWebToken.Alg, + key, + jsonWebToken, + validationParameters, + callContext); + if (!result.IsValid) + return new SignatureValidationResult( + ValidationFailureType.SignatureValidationFailed, + result.ExceptionDetail); + + var signatureProvider = cryptoProviderFactory.CreateForVerifying(key, jsonWebToken.Alg); + try + { + if (signatureProvider == null) + return new SignatureValidationResult( + ValidationFailureType.SignatureValidationFailed, + new ExceptionDetail( + new MessageDetail(TokenLogMessages.IDX10636, + key?.ToString() ?? "Null", + LogHelper.MarkAsNonPII(jsonWebToken.Alg)), + typeof(InvalidOperationException), + new StackFrame())); + + bool valid = EncodingUtils.PerformEncodingDependentOperation( + jsonWebToken.EncodedToken, + 0, + jsonWebToken.Dot2, + Encoding.UTF8, + jsonWebToken.EncodedToken, + jsonWebToken.Dot2, + signatureProvider, + ValidateSignature); + + if (valid) + return new SignatureValidationResult(); + else + return new SignatureValidationResult( + ValidationFailureType.SignatureValidationFailed, + new ExceptionDetail( + new MessageDetail(TokenLogMessages.IDX10504), + typeof(SecurityTokenInvalidSignatureException), + new StackFrame())); + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception ex) +#pragma warning restore CA1031 // Do not catch general exception types + { + return new SignatureValidationResult( + ValidationFailureType.SignatureValidationFailed, + new ExceptionDetail( + new MessageDetail(TokenLogMessages.IDX10504, ex.ToString()), + ex.GetType(), + new StackFrame(), + ex)); + } + finally + { + cryptoProviderFactory.ReleaseSignatureProvider(signatureProvider); + } + } + + private static ExceptionDetail GetSignatureValidationFailureExceptionDetails( + JsonWebToken jwtToken, + ValidationParameters validationParameters, + BaseConfiguration? configuration, + StringBuilder? exceptionStrings, + StringBuilder? keysAttempted, + bool kidExists, + bool kidMatched) + { + // Get information on where keys used during token validation came from for debugging purposes. + var keysInTokenValidationParameters = validationParameters.IssuerSigningKeys; + var keysInConfiguration = configuration?.SigningKeys; + var numKeysInTokenValidationParameters = keysInTokenValidationParameters.Count; + var numKeysInConfiguration = keysInConfiguration?.Count ?? 0; + + if (kidExists && kidMatched) + { + JsonWebToken localJwtToken = jwtToken; // avoid closure on non-exceptional path + var isKidInTVP = keysInTokenValidationParameters.Any(x => x.KeyId.Equals(localJwtToken.Kid)); + var keyLocation = isKidInTVP ? "TokenValidationParameters" : "Configuration"; + return new ExceptionDetail( + new MessageDetail( + TokenLogMessages.IDX10511, + LogHelper.MarkAsNonPII(keysAttempted?.ToString() ?? ""), + LogHelper.MarkAsNonPII(numKeysInTokenValidationParameters), + LogHelper.MarkAsNonPII(numKeysInConfiguration), + LogHelper.MarkAsNonPII(keyLocation), + LogHelper.MarkAsNonPII(jwtToken.Kid), + exceptionStrings?.ToString() ?? "", + LogHelper.MarkAsSecurityArtifact(jwtToken.EncodedToken, JwtTokenUtilities.SafeLogJwtToken)), + typeof(SecurityTokenSignatureKeyNotFoundException), + new StackFrame()); + } + + if (keysAttempted is null) + return new ExceptionDetail( + new MessageDetail( + TokenLogMessages.IDX10500), // No keys found. + typeof(SecurityTokenSignatureKeyNotFoundException), + new StackFrame()); + + if (kidExists) + return new ExceptionDetail( + new MessageDetail( + TokenLogMessages.IDX10503, // No match for kid found among the keys provided. + LogHelper.MarkAsNonPII(jwtToken.Kid), + LogHelper.MarkAsNonPII(keysAttempted?.ToString() ?? ""), + LogHelper.MarkAsNonPII(numKeysInTokenValidationParameters), + LogHelper.MarkAsNonPII(numKeysInConfiguration), + exceptionStrings?.ToString() ?? "", + LogHelper.MarkAsSecurityArtifact(jwtToken.EncodedToken, JwtTokenUtilities.SafeLogJwtToken)), + typeof(SecurityTokenSignatureKeyNotFoundException), + new StackFrame()); + + return new ExceptionDetail( + new MessageDetail( + TokenLogMessages.IDX10517, // Kid is missing and no keys match. + LogHelper.MarkAsNonPII(keysAttempted?.ToString() ?? ""), + LogHelper.MarkAsNonPII(numKeysInTokenValidationParameters), + LogHelper.MarkAsNonPII(numKeysInConfiguration), + exceptionStrings?.ToString() ?? "", + LogHelper.MarkAsSecurityArtifact(jwtToken.EncodedToken, JwtTokenUtilities.SafeLogJwtToken)), + typeof(SecurityTokenSignatureKeyNotFoundException), + new StackFrame()); + } + } +#nullable restore +} diff --git a/src/Microsoft.IdentityModel.Tokens/Delegates.cs b/src/Microsoft.IdentityModel.Tokens/Delegates.cs index 5a1eac59c8..b9de79bff8 100644 --- a/src/Microsoft.IdentityModel.Tokens/Delegates.cs +++ b/src/Microsoft.IdentityModel.Tokens/Delegates.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.IdentityModel.JsonWebTokens.Results; namespace Microsoft.IdentityModel.Tokens { @@ -171,6 +172,19 @@ namespace Microsoft.IdentityModel.Tokens public delegate SecurityToken TransformBeforeSignatureValidation(SecurityToken token, TokenValidationParameters validationParameters); #nullable enable + /// + /// Resolves the signing key used for validating a token's signature. + /// + /// The string representation of the token being validated. + /// The being validated, which may be null. + /// The key identifier, which may be null. + /// The to be used for validating the token. + /// The to be used for validating the token. + /// The used for logging. + /// The used to validate the signature. + /// If both and are set, takes priority. + internal delegate SecurityKey? IssuerSigningKeyResolverDelegate(string token, SecurityToken? securityToken, string? kid, ValidationParameters validationParameters, BaseConfiguration? configuration, CallContext? callContext); + /// /// Resolves the decryption key for the security token. /// @@ -181,5 +195,16 @@ namespace Microsoft.IdentityModel.Tokens /// The to be used for logging. /// The used to decrypt the token. internal delegate IList ResolveTokenDecryptionKeyDelegate(string token, SecurityToken securityToken, string kid, ValidationParameters validationParameters, CallContext? callContext); + + /// + /// Validates the signature of the security token. + /// + /// The with a signature. + /// The to be used for validating the token. + /// The to be used for validating the token. + /// The to be used for logging. + /// This method is not expected to throw. + /// The validated . + internal delegate SignatureValidationResult SignatureValidatorDelegate(SecurityToken token, ValidationParameters validationParameters, BaseConfiguration? configuration, CallContext? callContext); #nullable restore } diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/SignatureValidationResult.cs b/src/Microsoft.IdentityModel.Tokens/Validation/SignatureValidationResult.cs new file mode 100644 index 0000000000..fc6bd82ae0 --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens/Validation/SignatureValidationResult.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + + +using System; +using Microsoft.IdentityModel.Tokens; + +namespace Microsoft.IdentityModel.JsonWebTokens.Results +{ +#nullable enable + internal class SignatureValidationResult: ValidationResult + { + private Exception? _exception; + + public SignatureValidationResult() : base(ValidationFailureType.ValidationSucceeded) + { + IsValid = true; + } + + public SignatureValidationResult(ValidationFailureType validationFailure, ExceptionDetail? exceptionDetail) + : base(validationFailure, exceptionDetail) + { + IsValid = false; + } + + public override Exception? Exception + { + get + { + if (_exception != null || ExceptionDetail == null) + return _exception; + + HasValidOrExceptionWasRead = true; + _exception = ExceptionDetail.GetException(); + _exception.Source = "Microsoft.IdentityModel.JsonWebTokens"; + + if (_exception is SecurityTokenException securityTokenException) + { + securityTokenException.ExceptionDetail = ExceptionDetail; + } + + return _exception; + } + } + } +#nullable restore +} diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs b/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs index 479d2ff19a..e7f4ca7f40 100644 --- a/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs +++ b/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs @@ -51,6 +51,12 @@ private class AudienceValidationFailure : ValidationFailureType { internal Audie public static readonly ValidationFailureType TokenTypeValidationFailed = new TokenTypeValidationFailure("TokenTypeValidationFailed"); private class TokenTypeValidationFailure : ValidationFailureType { internal TokenTypeValidationFailure(string name) : base(name) { } } + /// + /// Defines a type that represents that the token's signature validation failed. + /// + public static readonly ValidationFailureType SignatureValidationFailed = new SignatureValidationFailure("SignatureValidationFailed"); + private class SignatureValidationFailure : ValidationFailureType { internal SignatureValidationFailure(string name) : base(name) { } } + /// /// Defines a type that represents that signing key validation failed. /// diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/ValidationParameters.cs b/src/Microsoft.IdentityModel.Tokens/Validation/ValidationParameters.cs index c064531798..f78087588e 100644 --- a/src/Microsoft.IdentityModel.Tokens/Validation/ValidationParameters.cs +++ b/src/Microsoft.IdentityModel.Tokens/Validation/ValidationParameters.cs @@ -27,6 +27,7 @@ internal class ValidationParameters private AudienceValidatorDelegate _audienceValidator = Validators.ValidateAudience; private IssuerValidationDelegateAsync _issuerValidatorAsync = Validators.ValidateIssuerAsync; private LifetimeValidatorDelegate _lifetimeValidator = Validators.ValidateLifetime; + private SignatureValidatorDelegate _signatureValidator; private TokenReplayValidatorDelegate _tokenReplayValidator = Validators.ValidateTokenReplay; private TypeValidatorDelegate _typeValidator = Validators.ValidateTokenType; @@ -201,7 +202,7 @@ public virtual ValidationParameters Clone() /// A with Authentication, NameClaimType and RoleClaimType set. public virtual ClaimsIdentity CreateClaimsIdentity(SecurityToken securityToken, string issuer) { - string nameClaimType = null; + string nameClaimType; if (NameClaimTypeRetriever != null) { nameClaimType = NameClaimTypeRetriever(securityToken, issuer); @@ -211,7 +212,7 @@ public virtual ClaimsIdentity CreateClaimsIdentity(SecurityToken securityToken, nameClaimType = NameClaimType; } - string roleClaimType = null; + string roleClaimType; if (RoleClaimTypeRetriever != null) { roleClaimType = RoleClaimTypeRetriever(securityToken, issuer); @@ -290,7 +291,7 @@ public virtual ClaimsIdentity CreateClaimsIdentity(SecurityToken securityToken, /// If both and are set, IssuerSigningKeyResolverUsingConfiguration takes /// priority. /// - public IssuerSigningKeyResolver IssuerSigningKeyResolver { get; set; } + public IssuerSigningKeyResolverDelegate IssuerSigningKeyResolver { get; set; } /// /// Gets or sets an used for signature validation. @@ -372,7 +373,7 @@ public string NameClaimType public IDictionary PropertyBag { get; } /// - /// Gets or sets a boolean to control if configuration required to be refreshed before token validation. + /// A boolean to control whether configuration should be refreshed before validating a token. /// /// /// The default is false. @@ -430,7 +431,10 @@ public string RoleClaimType /// /// If set, this delegate will be called to validate the signature of the token, instead of default processing. /// - public SignatureValidator SignatureValidator { get; set; } + public SignatureValidatorDelegate SignatureValidator { + get { return _signatureValidator; } + set { _signatureValidator = value; } + } /// /// Gets or sets a delegate that will be called to retreive a used for decryption. @@ -466,6 +470,13 @@ public TokenReplayValidatorDelegate TokenReplayValidator set { _tokenReplayValidator = value ?? throw new ArgumentNullException(nameof(value), "TokenReplayValidator cannot be set as null."); } } + /// + /// If the IssuerSigningKeyResolver is unable to resolve the key when validating the signature of the SecurityToken, + /// all available keys will be tried. + /// + /// Default is false. + public bool TryAllIssuerSigningKeys { get; set; } + /// /// Allows overriding the delegate that will be used to validate the type of the token. /// If the token type cannot be validated, a MUST be returned by the delegate. diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/ValidationResult.cs b/src/Microsoft.IdentityModel.Tokens/Validation/ValidationResult.cs index db6f9bbbb8..dd62bb549d 100644 --- a/src/Microsoft.IdentityModel.Tokens/Validation/ValidationResult.cs +++ b/src/Microsoft.IdentityModel.Tokens/Validation/ValidationResult.cs @@ -38,7 +38,7 @@ protected ValidationResult(ValidationFailureType validationFailureType) /// /// The that occurred during validation. /// The representing the that occurred during validation. - protected ValidationResult(ValidationFailureType validationFailureType, ExceptionDetail exceptionDetail) + protected ValidationResult(ValidationFailureType validationFailureType, ExceptionDetail? exceptionDetail) { ValidationFailureType = validationFailureType; ExceptionDetail = exceptionDetail; From b68f179ebfedda44c1f1ec24ef642a9f294913d4 Mon Sep 17 00:00:00 2001 From: Ignacio Inglese Date: Wed, 31 Jul 2024 16:00:36 +0100 Subject: [PATCH 2/5] Updated documentation. Added null checks for parameters. Added base for tests --- .../JsonWebTokenHandler.ValidateSignature.cs | 22 +++++-- .../Validation/SignatureValidationResult.cs | 25 +++++++- ...nWebTokenHandler.ValidateSignatureTests.cs | 57 +++++++++++++++++ .../IdentityComparer.cs | 64 +++++++++++++++++++ 4 files changed, 163 insertions(+), 5 deletions(-) create mode 100644 test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.ValidateSignatureTests.cs diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateSignature.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateSignature.cs index 89f3b4681a..7f6061ca88 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateSignature.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateSignature.cs @@ -21,12 +21,26 @@ public partial class JsonWebTokenHandler : TokenHandler /// /// Validates the JWT signature. /// - private static SignatureValidationResult ValidateSignature( + /// The JWT token to validate. + /// The parameters used for validation. + /// The optional configuration used for validation. + /// The context in which the method is called. + /// Returned if or is null." + /// Returned by the default implementation if the token is not signed, or if the validation fails. + /// Returned if the algorithm is not supported by the key. + /// Returned if the key cannot be resolved. + internal static SignatureValidationResult ValidateSignature( JsonWebToken jwtToken, ValidationParameters validationParameters, - BaseConfiguration configuration, + BaseConfiguration? configuration, CallContext callContext) { + if (jwtToken is null) + return SignatureValidationResult.NullParameterFailure(nameof(jwtToken)); + + if (validationParameters is null) + return SignatureValidationResult.NullParameterFailure(nameof(validationParameters)); + // Delegate is set by the user, we call it and return the result. if (validationParameters.SignatureValidator is not null) return validationParameters.SignatureValidator(jwtToken, validationParameters, configuration, callContext); @@ -60,7 +74,7 @@ private static SignatureValidationResult ValidateSignature( { // Resolve the key using the token's 'kid' and 'x5t' headers. // Fall back to the validation parameters' keys if configuration keys are not set. - key = JwtTokenUtilities.ResolveTokenSigningKey(jwtToken.Kid, jwtToken.X5t, configuration.SigningKeys) + key = JwtTokenUtilities.ResolveTokenSigningKey(jwtToken.Kid, jwtToken.X5t, configuration?.SigningKeys) ?? JwtTokenUtilities.ResolveTokenSigningKey(jwtToken.Kid, jwtToken.X5t, validationParameters.IssuerSigningKeys); } @@ -87,7 +101,7 @@ private static SignatureValidationResult ValidateSignature( private static SignatureValidationResult ValidateSignatureUsingAllKeys( JsonWebToken jwtToken, ValidationParameters - validationParameters, BaseConfiguration configuration, + validationParameters, BaseConfiguration? configuration, CallContext callContext) { // control gets here if: diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/SignatureValidationResult.cs b/src/Microsoft.IdentityModel.Tokens/Validation/SignatureValidationResult.cs index fc6bd82ae0..a4fa9df96c 100644 --- a/src/Microsoft.IdentityModel.Tokens/Validation/SignatureValidationResult.cs +++ b/src/Microsoft.IdentityModel.Tokens/Validation/SignatureValidationResult.cs @@ -1,28 +1,51 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. - using System; using Microsoft.IdentityModel.Tokens; namespace Microsoft.IdentityModel.JsonWebTokens.Results { #nullable enable + /// + /// Contains the result of validating a signature. + /// The contains a collection of for each step in the token validation. + /// internal class SignatureValidationResult: ValidationResult { private Exception? _exception; + /// + /// Creates an instance of representing the successful result of validating a signature. + /// public SignatureValidationResult() : base(ValidationFailureType.ValidationSucceeded) { IsValid = true; } + /// + /// Creates an instance of representing the failed result of validating a signature. + /// + /// is the that occurred while validating the signature. + /// contains the of the error that occurred while validating the signature. public SignatureValidationResult(ValidationFailureType validationFailure, ExceptionDetail? exceptionDetail) : base(validationFailure, exceptionDetail) { IsValid = false; } + /// + /// Creates an instance of representing a failure due to a null parameter. + /// + /// The name of the null parameter. + internal static SignatureValidationResult NullParameterFailure(string parameterName) => + new SignatureValidationResult( + ValidationFailureType.SignatureValidationFailed, + ExceptionDetail.NullParameter(parameterName)); + + /// + /// Gets the that occurred while validating the signature. + /// public override Exception? Exception { get diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.ValidateSignatureTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.ValidateSignatureTests.cs new file mode 100644 index 0000000000..08506558a7 --- /dev/null +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.ValidateSignatureTests.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using Microsoft.IdentityModel.JsonWebTokens.Results; +using Microsoft.IdentityModel.Logging; +using Microsoft.IdentityModel.TestUtils; +using Microsoft.IdentityModel.Tokens; +using Xunit; +using TokenLogMessages = Microsoft.IdentityModel.Tokens.LogMessages; + +namespace Microsoft.IdentityModel.JsonWebTokens.Tests +{ + public class JsonWebTokenHandlerValidateSignatureTests + { + [Theory, MemberData(nameof(JsonWebTokenHandlerValidateSignatureTestCases), DisableDiscoveryEnumeration = true)] + public void ValidateSignature(JsonWebTokenHandlerValidateSignatureTheoryData theoryData) + { + CompareContext context = TestUtilities.WriteHeader($"{this}.ValidateSignature", theoryData); + + SignatureValidationResult validationResult = JsonWebTokenHandler.ValidateSignature( + theoryData.JWT, + theoryData.ValidationParameters, + theoryData.Configuration, + new CallContext()); + + if (validationResult.Exception != null) + theoryData.ExpectedException.ProcessException(validationResult.Exception); + else + theoryData.ExpectedException?.ProcessNoException(); + + IdentityComparer.AreSignatureValidationResultsEqual(validationResult, theoryData.SignatureValidationResult, context); + TestUtilities.AssertFailIfErrors(context); + } + + public static TheoryData JsonWebTokenHandlerValidateSignatureTestCases + { + get + { + return new TheoryData + { + new JsonWebTokenHandlerValidateSignatureTheoryData { + + } + }; + } + } + } + + public class JsonWebTokenHandlerValidateSignatureTheoryData : TheoryDataBase + { + public JsonWebToken JWT { get; set; } + public BaseConfiguration Configuration { get; set; } + internal SignatureValidationResult SignatureValidationResult { get; set; } + internal ValidationParameters ValidationParameters { get; set; } + } +} diff --git a/test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs b/test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs index 0e8a3ae226..cd695cc699 100644 --- a/test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs +++ b/test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs @@ -21,6 +21,7 @@ using System.Text.Json; using System.Text.RegularExpressions; using Microsoft.IdentityModel.JsonWebTokens; +using Microsoft.IdentityModel.JsonWebTokens.Results; using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Protocols.WsFederation; @@ -1627,6 +1628,69 @@ public static bool AreSecurityKeyEnumsEqual(object object1, object object2, Comp return AreEnumsEqual(object1 as IEnumerable, object2 as IEnumerable, context, AreSecurityKeysEqual); } + public static bool AreSignatureValidationResultsEqual(object object1, object object2, CompareContext context) + { + var localContext = new CompareContext(context); + if (!ContinueCheckingEquality(object1, object2, context)) + return context.Merge(localContext); + + return AreSignatureValidationResultsEqual( + object1 as SignatureValidationResult, + object2 as SignatureValidationResult, + "SignatureValidationResult1", + "SignatureValidationResult2", + null, + context); + } + + internal static bool AreSignatureValidationResultsEqual( + SignatureValidationResult signatureValidationResult1, + SignatureValidationResult signatureValidationResult2, + string name1, + string name2, + string stackPrefix, + CompareContext context) + { + var localContext = new CompareContext(context); + if (!ContinueCheckingEquality(signatureValidationResult1, signatureValidationResult2, localContext)) + return context.Merge(localContext); + + if (signatureValidationResult1.IsValid != signatureValidationResult2.IsValid) + localContext.Diffs.Add($"{name1}.IsValid: {signatureValidationResult1.IsValid} != {name2}.IsValid: {signatureValidationResult2.IsValid}"); + + if (signatureValidationResult1.ValidationFailureType != signatureValidationResult2.ValidationFailureType) + localContext.Diffs.Add($"{name1}.IsValid: {signatureValidationResult1.ValidationFailureType} != {name2}.IsValid: {signatureValidationResult2.ValidationFailureType}"); + + // true => both are not null. + if (ContinueCheckingEquality(signatureValidationResult1.Exception, signatureValidationResult2.Exception, localContext)) + { + AreStringsEqual( + signatureValidationResult1.Exception.Message, + signatureValidationResult2.Exception.Message, + $"({name1}).Exception.Message", + $"({name2}).Exception.Message", + localContext); + + AreStringsEqual( + signatureValidationResult1.Exception.Source, + signatureValidationResult2.Exception.Source, + $"({name1}).Exception.Source", + $"({name2}).Exception.Source", + localContext); + + if (!string.IsNullOrEmpty(stackPrefix)) + AreStringPrefixesEqual( + signatureValidationResult1.Exception.StackTrace.Trim(), + signatureValidationResult2.Exception.StackTrace.Trim(), + $"({name1}).Exception.StackTrace", + $"({name2}).Exception.StackTrace", + stackPrefix.Trim(), + localContext); + } + + return context.Merge(localContext); + } + public static bool AreSignedInfosEqual(SignedInfo signedInfo1, SignedInfo signedInfo2, CompareContext context) { var localContext = new CompareContext(context); From a96eb311b8c81fe4d8a8f3b3c40a36184f025d42 Mon Sep 17 00:00:00 2001 From: Ignacio Inglese Date: Wed, 31 Jul 2024 18:08:30 +0100 Subject: [PATCH 3/5] Added test --- .../JsonWebTokenHandler.ValidateSignatureTests.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.ValidateSignatureTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.ValidateSignatureTests.cs index 08506558a7..ac6b9143ab 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.ValidateSignatureTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.ValidateSignatureTests.cs @@ -40,7 +40,16 @@ public static TheoryData JsonWeb return new TheoryData { new JsonWebTokenHandlerValidateSignatureTheoryData { - + JWT = null, + ExpectedException = ExpectedException.ArgumentNullException("IDX10000:"), + SignatureValidationResult = new SignatureValidationResult( + ValidationFailureType.SignatureValidationFailed, + new ExceptionDetail( + new MessageDetail( + TokenLogMessages.IDX10000, + "jwtToken"), + typeof(ArgumentNullException), + new System.Diagnostics.StackFrame())) } }; } From cec927c9ff329b06fe0aed53b84a7d5a39da0748 Mon Sep 17 00:00:00 2001 From: Ignacio Inglese Date: Thu, 1 Aug 2024 18:26:58 +0100 Subject: [PATCH 4/5] Add tests --- .../Validation/ValidationParameters.cs | 2 +- ...nWebTokenHandler.ValidateSignatureTests.cs | 166 +++++++++++++++++- 2 files changed, 163 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/ValidationParameters.cs b/src/Microsoft.IdentityModel.Tokens/Validation/ValidationParameters.cs index f78087588e..2843dcb25f 100644 --- a/src/Microsoft.IdentityModel.Tokens/Validation/ValidationParameters.cs +++ b/src/Microsoft.IdentityModel.Tokens/Validation/ValidationParameters.cs @@ -296,7 +296,7 @@ public virtual ClaimsIdentity CreateClaimsIdentity(SecurityToken securityToken, /// /// Gets or sets an used for signature validation. /// - public IList IssuerSigningKeys { get; } + public IList IssuerSigningKeys { get; set; } /// /// Allows overriding the delegate that will be used to validate the issuer of the token. diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.ValidateSignatureTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.ValidateSignatureTests.cs index ac6b9143ab..9ccdc3bd38 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.ValidateSignatureTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.ValidateSignatureTests.cs @@ -2,8 +2,10 @@ // Licensed under the MIT License. using System; +using System.IdentityModel.Tokens.Jwt.Tests; using Microsoft.IdentityModel.JsonWebTokens.Results; using Microsoft.IdentityModel.Logging; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.TestUtils; using Microsoft.IdentityModel.Tokens; using Xunit; @@ -17,12 +19,29 @@ public class JsonWebTokenHandlerValidateSignatureTests public void ValidateSignature(JsonWebTokenHandlerValidateSignatureTheoryData theoryData) { CompareContext context = TestUtilities.WriteHeader($"{this}.ValidateSignature", theoryData); + JsonWebToken jsonWebToken; + if (theoryData.JWT == null && theoryData.SigningCredentials != null) + { + var tokenDescriptor = new SecurityTokenDescriptor + { + SigningCredentials = theoryData.SigningCredentials, + + }; + var tokenHandler = new JsonWebTokenHandler(); + jsonWebToken = new JsonWebToken(tokenHandler.CreateToken(tokenDescriptor)); + } + else + jsonWebToken = theoryData.JWT; + + + if (theoryData.Configuration is not null && theoryData.KeyToAddToConfiguration is not null) + theoryData.Configuration.SigningKeys.Add(theoryData.KeyToAddToConfiguration); SignatureValidationResult validationResult = JsonWebTokenHandler.ValidateSignature( - theoryData.JWT, - theoryData.ValidationParameters, - theoryData.Configuration, - new CallContext()); + jsonWebToken, + theoryData.ValidationParameters, + theoryData.Configuration, + new CallContext()); if (validationResult.Exception != null) theoryData.ExpectedException.ProcessException(validationResult.Exception); @@ -37,9 +56,11 @@ public static TheoryData JsonWeb { get { + var unsignedToken = new JsonWebToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ."); return new TheoryData { new JsonWebTokenHandlerValidateSignatureTheoryData { + TestId = "Invalid_Null_JWT", JWT = null, ExpectedException = ExpectedException.ArgumentNullException("IDX10000:"), SignatureValidationResult = new SignatureValidationResult( @@ -50,6 +71,141 @@ public static TheoryData JsonWeb "jwtToken"), typeof(ArgumentNullException), new System.Diagnostics.StackFrame())) + }, + new JsonWebTokenHandlerValidateSignatureTheoryData { + TestId = "Invalid_Null_ValidationParameters", + JWT = new JsonWebToken(EncodedJwts.LiveJwt), + ValidationParameters = null, + ExpectedException = ExpectedException.ArgumentNullException("IDX10000:"), + SignatureValidationResult = new SignatureValidationResult( + ValidationFailureType.SignatureValidationFailed, + new ExceptionDetail( + new MessageDetail( + TokenLogMessages.IDX10000, + "validationParameters"), + typeof(ArgumentNullException), + new System.Diagnostics.StackFrame())) + }, + new JsonWebTokenHandlerValidateSignatureTheoryData { + TestId = "Invalid_DelegateReturnsFailure", + JWT = new JsonWebToken(EncodedJwts.LiveJwt), + ValidationParameters = new ValidationParameters + { + SignatureValidator = (token, parameters, configuration, callContext) => SignatureValidationResult.NullParameterFailure("fakeParameter") + }, + ExpectedException = ExpectedException.ArgumentNullException("IDX10000:"), + SignatureValidationResult = new SignatureValidationResult( + ValidationFailureType.SignatureValidationFailed, + new ExceptionDetail( + new MessageDetail( + TokenLogMessages.IDX10000, + "fakeParameter"), + typeof(ArgumentNullException), + new System.Diagnostics.StackFrame())) + }, + new JsonWebTokenHandlerValidateSignatureTheoryData + { + TestId = "Invalid_NoSignature", + JWT = unsignedToken, + ValidationParameters = new ValidationParameters(), + ExpectedException = ExpectedException.SecurityTokenInvalidSignatureException("IDX10504:"), + SignatureValidationResult = new SignatureValidationResult( + ValidationFailureType.SignatureValidationFailed, + new ExceptionDetail( + new MessageDetail( + TokenLogMessages.IDX10504, + LogHelper.MarkAsSecurityArtifact(unsignedToken, JwtTokenUtilities.SafeLogJwtToken)), + typeof(SecurityTokenInvalidSignatureException), + new System.Diagnostics.StackFrame())) + }, + new JsonWebTokenHandlerValidateSignatureTheoryData + { + TestId = "Valid_DelegateReturnsSuccess", + JWT = new JsonWebToken(EncodedJwts.LiveJwt), + ValidationParameters = new ValidationParameters + { + SignatureValidator = (token, parameters, configuration, callContext) => new SignatureValidationResult() + }, + SignatureValidationResult = new SignatureValidationResult() + }, + new JsonWebTokenHandlerValidateSignatureTheoryData + { + TestId = "Valid_SignatureValidationResult_Success_KidMatches", + SigningCredentials = KeyingMaterial.JsonWebKeyRsa256SigningCredentials, + ValidationParameters = new ValidationParameters + { + IssuerSigningKeys = [KeyingMaterial.JsonWebKeyRsa256SigningCredentials.Key] + }, + SignatureValidationResult = new SignatureValidationResult() + }, + new JsonWebTokenHandlerValidateSignatureTheoryData + { + TestId = "Valid_SignatureValidationResult_Success_X5tMatches", + SigningCredentials = KeyingMaterial.X509SigningCreds_1024_RsaSha2_Sha2, + ValidationParameters = new ValidationParameters + { + IssuerSigningKeys = [KeyingMaterial.X509SigningCreds_1024_RsaSha2_Sha2.Key] + }, + SignatureValidationResult = new SignatureValidationResult() + }, + new JsonWebTokenHandlerValidateSignatureTheoryData + { + TestId = "Valid_IssuerSigningKeyResolverReturnsKeyThatMatches", + SigningCredentials = KeyingMaterial.JsonWebKeyRsa256SigningCredentials, + ValidationParameters = new ValidationParameters + { + IssuerSigningKeyResolver = (token, securityToken, kid, validationParameters, configuration, callContext) => KeyingMaterial.JsonWebKeyRsa256SigningCredentials.Key + }, + SignatureValidationResult = new SignatureValidationResult() + }, + new JsonWebTokenHandlerValidateSignatureTheoryData + { + TestId = "Valid_ConfurationReturnsKeyThatMatches", + SigningCredentials = KeyingMaterial.JsonWebKeyRsa256SigningCredentials, + Configuration = new OpenIdConnectConfiguration(), + KeyToAddToConfiguration = KeyingMaterial.JsonWebKeyRsa256SigningCredentials.Key, + ValidationParameters = new ValidationParameters(), + SignatureValidationResult = new SignatureValidationResult() + }, + new JsonWebTokenHandlerValidateSignatureTheoryData + { + TestId = "Valid_NoKeyId_TryAllKeys", + SigningCredentials = KeyingMaterial.DefaultSymmetricSigningCreds_256_Sha2_NoKeyId, + ValidationParameters = new ValidationParameters + { + IssuerSigningKeys = [KeyingMaterial.DefaultSymmetricSigningCreds_256_Sha2_NoKeyId.Key], + TryAllIssuerSigningKeys = true + }, + SignatureValidationResult = new SignatureValidationResult() + }, + new JsonWebTokenHandlerValidateSignatureTheoryData + { + TestId = "Invalid_NoKeyId_DontTryAllKeys", + SigningCredentials = KeyingMaterial.DefaultSymmetricSigningCreds_256_Sha2_NoKeyId, + ValidationParameters = new ValidationParameters + { + IssuerSigningKeys = [KeyingMaterial.DefaultSymmetricSigningCreds_256_Sha2_NoKeyId.Key], + }, + ExpectedException = ExpectedException.SecurityTokenSignatureKeyNotFoundException("IDX10500:"), + SignatureValidationResult = new SignatureValidationResult( + ValidationFailureType.SignatureValidationFailed, + new ExceptionDetail( + new MessageDetail(TokenLogMessages.IDX10500), + typeof(SecurityTokenSignatureKeyNotFoundException), + new System.Diagnostics.StackFrame())) + }, + new JsonWebTokenHandlerValidateSignatureTheoryData + { + TestId = "Invalid_NoKeys", + JWT = new JsonWebToken(EncodedJwts.LiveJwt), + ValidationParameters = new ValidationParameters(), + ExpectedException = ExpectedException.SecurityTokenSignatureKeyNotFoundException("IDX10500:"), + SignatureValidationResult = new SignatureValidationResult( + ValidationFailureType.SignatureValidationFailed, + new ExceptionDetail( + new MessageDetail(TokenLogMessages.IDX10500), + typeof(SecurityTokenSignatureKeyNotFoundException), + new System.Diagnostics.StackFrame())) } }; } @@ -60,6 +216,8 @@ public class JsonWebTokenHandlerValidateSignatureTheoryData : TheoryDataBase { public JsonWebToken JWT { get; set; } public BaseConfiguration Configuration { get; set; } + public SigningCredentials SigningCredentials { get; internal set; } + public SecurityKey KeyToAddToConfiguration { get; internal set; } internal SignatureValidationResult SignatureValidationResult { get; set; } internal ValidationParameters ValidationParameters { get; set; } } From 0fd03abb6bf068d989fca430d550e72057068b52 Mon Sep 17 00:00:00 2001 From: Ignacio Inglese Date: Fri, 2 Aug 2024 16:46:37 +0100 Subject: [PATCH 5/5] Addresed PR feedback --- .../JsonWebTokenHandler.ValidateSignature.cs | 170 ++++++++++++------ .../Validation/SignatureValidationResult.cs | 10 +- .../Validation/ValidationParameters.cs | 10 +- ...nWebTokenHandler.ValidateSignatureTests.cs | 45 ++--- 4 files changed, 150 insertions(+), 85 deletions(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateSignature.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateSignature.cs index 7f6061ca88..a6ad80e9ad 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateSignature.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateSignature.cs @@ -108,46 +108,40 @@ private static SignatureValidationResult ValidateSignatureUsingAllKeys( // 1. User specified delegate: IssuerSigningKeyResolver returned null // 2. ResolveIssuerSigningKey returned null // Try all the keys. This is the degenerate case, not concerned about perf. - ICollection keys = configuration?.SigningKeys ?? new List(); - if (!(keys is List keysList)) - keysList = keys.ToList(); + (SignatureValidationResult? configResult, bool configKidMatched, KeyMatchFailedResult? configFailedResult) = ValidateUsingKeys( + jwtToken, + validationParameters, + configuration?.SigningKeys, + callContext); - if (!validationParameters.IssuerSigningKeys.IsNullOrEmpty()) - keysList.AddRange(validationParameters.IssuerSigningKeys); + if (configResult is not null) + return configResult; - // keep track of exceptions thrown, keys that were tried - StringBuilder? exceptionStrings = null; - StringBuilder? keysAttempted = null; - var kidExists = !string.IsNullOrEmpty(jwtToken.Kid); - var kidMatched = false; + (SignatureValidationResult? vpResult, bool vpKidMatched, KeyMatchFailedResult? vpFailedResult) = ValidateUsingKeys( + jwtToken, + validationParameters, + validationParameters.IssuerSigningKeys, + callContext); - for (int i = 0; i < keysList.Count; i++) - { - SecurityKey key = keysList[i]; - SignatureValidationResult result = ValidateSignatureWithKey( - jwtToken, - key, - validationParameters, - callContext); + if (vpResult is not null) + return vpResult; - if (result.IsValid) - { - if (LogHelper.IsEnabled(EventLogLevel.Informational)) - LogHelper.LogInformation(TokenLogMessages.IDX10242, jwtToken); + if (vpFailedResult is null && configFailedResult is null) // No keys were attempted + return new SignatureValidationResult( + ValidationFailureType.SignatureValidationFailed, + new ExceptionDetail( + new MessageDetail(TokenLogMessages.IDX10500), + typeof(SecurityTokenSignatureKeyNotFoundException), + new StackFrame())); - jwtToken.SigningKey = key; - return result; // return the first valid signature. - } - else - (exceptionStrings ??= new StringBuilder()).AppendLine(result.ExceptionDetail?.MessageDetail.Message ?? "Null"); + StringBuilder exceptionStrings = new(); + StringBuilder keysAttempted = new (); - if (key != null) - { - (keysAttempted ??= new StringBuilder()).Append(key.ToString()).Append(" , KeyId: ").AppendLine(key.KeyId); - if (kidExists && !kidMatched && key.KeyId != null) - kidMatched = jwtToken.Kid.Equals(key.KeyId, key is X509SecurityKey ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); - } - } + PopulateFailedResults(configFailedResult, exceptionStrings, keysAttempted); + PopulateFailedResults(vpFailedResult, exceptionStrings, keysAttempted); + + bool kidExists = !string.IsNullOrEmpty(jwtToken.Kid); + bool kidMatched = configKidMatched || vpKidMatched; // No valid signature found. Return the exception details. return new SignatureValidationResult( @@ -162,13 +156,57 @@ private static SignatureValidationResult ValidateSignatureUsingAllKeys( kidMatched)); } + private static (SignatureValidationResult? validResult, bool KidMatched, KeyMatchFailedResult? failedResult) ValidateUsingKeys( + JsonWebToken jwtToken, + ValidationParameters validationParameters, + ICollection? keys, + CallContext callContext) + { + if (keys is null || keys.Count == 0) + return (null, false, null); + + if (keys is not IList keysList) + keysList = keys.ToList(); + + bool kidExists = !string.IsNullOrEmpty(jwtToken.Kid); + bool kidMatched = false; + IList? keysAttempted = null; + IList? results = null; + + for (int i = 0; i < keysList.Count; i++) + { + SecurityKey key = keysList[i]; + SignatureValidationResult result = ValidateSignatureWithKey(jwtToken, key, validationParameters, callContext); + if (result.IsValid) + { + jwtToken.SigningKey = key; + return (result, true, null); + } + + keysAttempted ??= []; + results ??= []; + + results.Add(result); + keysAttempted.Add(key); + + if (kidExists && !kidMatched && key.KeyId is not null) + kidMatched = jwtToken.Kid.Equals(key.KeyId, key is X509SecurityKey ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); + } + + if (results is not null && results.Count > 0 && keysAttempted is not null && keysAttempted.Count > 0) + return (null, kidMatched, new KeyMatchFailedResult(results, keysAttempted)); + + // No keys were attempted. + return (null, kidMatched, null); + } + private static SignatureValidationResult ValidateSignatureWithKey( JsonWebToken jsonWebToken, SecurityKey key, ValidationParameters validationParameters, CallContext callContext) { - var cryptoProviderFactory = validationParameters.CryptoProviderFactory ?? key.CryptoProviderFactory; + CryptoProviderFactory cryptoProviderFactory = validationParameters.CryptoProviderFactory ?? key.CryptoProviderFactory; if (!cryptoProviderFactory.IsSupportedAlgorithm(jsonWebToken.Alg, key)) { return new SignatureValidationResult( @@ -193,7 +231,7 @@ private static SignatureValidationResult ValidateSignatureWithKey( ValidationFailureType.SignatureValidationFailed, result.ExceptionDetail); - var signatureProvider = cryptoProviderFactory.CreateForVerifying(key, jsonWebToken.Alg); + SignatureProvider signatureProvider = cryptoProviderFactory.CreateForVerifying(key, jsonWebToken.Alg); try { if (signatureProvider == null) @@ -217,7 +255,7 @@ private static SignatureValidationResult ValidateSignatureWithKey( ValidateSignature); if (valid) - return new SignatureValidationResult(); + return SignatureValidationResult.Success(); else return new SignatureValidationResult( ValidationFailureType.SignatureValidationFailed, @@ -248,52 +286,45 @@ private static ExceptionDetail GetSignatureValidationFailureExceptionDetails( JsonWebToken jwtToken, ValidationParameters validationParameters, BaseConfiguration? configuration, - StringBuilder? exceptionStrings, - StringBuilder? keysAttempted, + StringBuilder exceptionStrings, + StringBuilder keysAttempted, bool kidExists, bool kidMatched) { // Get information on where keys used during token validation came from for debugging purposes. - var keysInTokenValidationParameters = validationParameters.IssuerSigningKeys; - var keysInConfiguration = configuration?.SigningKeys; - var numKeysInTokenValidationParameters = keysInTokenValidationParameters.Count; - var numKeysInConfiguration = keysInConfiguration?.Count ?? 0; + IList keysInTokenValidationParameters = validationParameters.IssuerSigningKeys; + ICollection? keysInConfiguration = configuration?.SigningKeys; + int numKeysInTokenValidationParameters = keysInTokenValidationParameters.Count; + int numKeysInConfiguration = keysInConfiguration?.Count ?? 0; if (kidExists && kidMatched) { JsonWebToken localJwtToken = jwtToken; // avoid closure on non-exceptional path - var isKidInTVP = keysInTokenValidationParameters.Any(x => x.KeyId.Equals(localJwtToken.Kid)); - var keyLocation = isKidInTVP ? "TokenValidationParameters" : "Configuration"; + bool isKidInTVP = keysInTokenValidationParameters.Any(x => x.KeyId.Equals(localJwtToken.Kid)); + string keyLocation = isKidInTVP ? "TokenValidationParameters" : "Configuration"; return new ExceptionDetail( new MessageDetail( TokenLogMessages.IDX10511, - LogHelper.MarkAsNonPII(keysAttempted?.ToString() ?? ""), + LogHelper.MarkAsNonPII(keysAttempted.ToString()), LogHelper.MarkAsNonPII(numKeysInTokenValidationParameters), LogHelper.MarkAsNonPII(numKeysInConfiguration), LogHelper.MarkAsNonPII(keyLocation), LogHelper.MarkAsNonPII(jwtToken.Kid), - exceptionStrings?.ToString() ?? "", + exceptionStrings.ToString(), LogHelper.MarkAsSecurityArtifact(jwtToken.EncodedToken, JwtTokenUtilities.SafeLogJwtToken)), typeof(SecurityTokenSignatureKeyNotFoundException), new StackFrame()); } - if (keysAttempted is null) - return new ExceptionDetail( - new MessageDetail( - TokenLogMessages.IDX10500), // No keys found. - typeof(SecurityTokenSignatureKeyNotFoundException), - new StackFrame()); - if (kidExists) return new ExceptionDetail( new MessageDetail( TokenLogMessages.IDX10503, // No match for kid found among the keys provided. LogHelper.MarkAsNonPII(jwtToken.Kid), - LogHelper.MarkAsNonPII(keysAttempted?.ToString() ?? ""), + LogHelper.MarkAsNonPII(keysAttempted.ToString()), LogHelper.MarkAsNonPII(numKeysInTokenValidationParameters), LogHelper.MarkAsNonPII(numKeysInConfiguration), - exceptionStrings?.ToString() ?? "", + exceptionStrings.ToString(), LogHelper.MarkAsSecurityArtifact(jwtToken.EncodedToken, JwtTokenUtilities.SafeLogJwtToken)), typeof(SecurityTokenSignatureKeyNotFoundException), new StackFrame()); @@ -301,14 +332,37 @@ private static ExceptionDetail GetSignatureValidationFailureExceptionDetails( return new ExceptionDetail( new MessageDetail( TokenLogMessages.IDX10517, // Kid is missing and no keys match. - LogHelper.MarkAsNonPII(keysAttempted?.ToString() ?? ""), + LogHelper.MarkAsNonPII(keysAttempted.ToString()), LogHelper.MarkAsNonPII(numKeysInTokenValidationParameters), LogHelper.MarkAsNonPII(numKeysInConfiguration), - exceptionStrings?.ToString() ?? "", + exceptionStrings.ToString(), LogHelper.MarkAsSecurityArtifact(jwtToken.EncodedToken, JwtTokenUtilities.SafeLogJwtToken)), typeof(SecurityTokenSignatureKeyNotFoundException), new StackFrame()); } + + private static void PopulateFailedResults( + KeyMatchFailedResult? failedResult, + StringBuilder exceptionStrings, + StringBuilder keysAttempted) + { + if (failedResult is KeyMatchFailedResult result) + { + for (int i = 0; i < result.KeysAttempted.Count; i++) + { + exceptionStrings.AppendLine(result.FailedResults[i].ExceptionDetail?.MessageDetail.Message ?? "Null"); + keysAttempted.AppendLine(result.KeysAttempted[i].ToString()); + } + } + } + + private struct KeyMatchFailedResult( + IList failedResults, + IList keysAttempted) + { + public IList FailedResults = failedResults; + public IList KeysAttempted = keysAttempted; + } } #nullable restore } diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/SignatureValidationResult.cs b/src/Microsoft.IdentityModel.Tokens/Validation/SignatureValidationResult.cs index a4fa9df96c..9ca45eb72c 100644 --- a/src/Microsoft.IdentityModel.Tokens/Validation/SignatureValidationResult.cs +++ b/src/Microsoft.IdentityModel.Tokens/Validation/SignatureValidationResult.cs @@ -18,9 +18,9 @@ internal class SignatureValidationResult: ValidationResult /// /// Creates an instance of representing the successful result of validating a signature. /// - public SignatureValidationResult() : base(ValidationFailureType.ValidationSucceeded) + public SignatureValidationResult(bool isValid, ValidationFailureType validationFailureType) : base(validationFailureType) { - IsValid = true; + IsValid = isValid; } /// @@ -34,6 +34,12 @@ public SignatureValidationResult(ValidationFailureType validationFailure, Except IsValid = false; } + /// + /// Creates an instance of representing a successful validation. + /// + internal static SignatureValidationResult Success() => + new SignatureValidationResult(true, ValidationFailureType.ValidationSucceeded); + /// /// Creates an instance of representing a failure due to a null parameter. /// diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/ValidationParameters.cs b/src/Microsoft.IdentityModel.Tokens/Validation/ValidationParameters.cs index 2843dcb25f..2d4a4a75bf 100644 --- a/src/Microsoft.IdentityModel.Tokens/Validation/ValidationParameters.cs +++ b/src/Microsoft.IdentityModel.Tokens/Validation/ValidationParameters.cs @@ -22,6 +22,7 @@ internal class ValidationParameters private string _roleClaimType = ClaimsIdentity.DefaultRoleClaimType; private Dictionary _instancePropertyBag; private IList _validTokenTypes = []; + private IList _issuerSigningKeys; private AlgorithmValidatorDelegate _algorithmValidator = Validators.ValidateAlgorithm; private AudienceValidatorDelegate _audienceValidator = Validators.ValidateAudience; @@ -67,7 +68,7 @@ protected ValidationParameters(ValidationParameters other) IncludeTokenOnFailedValidation = other.IncludeTokenOnFailedValidation; IgnoreTrailingSlashWhenValidatingAudience = other.IgnoreTrailingSlashWhenValidatingAudience; IssuerSigningKeyResolver = other.IssuerSigningKeyResolver; - IssuerSigningKeys = other.IssuerSigningKeys; + _issuerSigningKeys = other.IssuerSigningKeys; IssuerSigningKeyValidator = other.IssuerSigningKeyValidator; IssuerValidatorAsync = other.IssuerValidatorAsync; LifetimeValidator = other.LifetimeValidator; @@ -294,9 +295,12 @@ public virtual ClaimsIdentity CreateClaimsIdentity(SecurityToken securityToken, public IssuerSigningKeyResolverDelegate IssuerSigningKeyResolver { get; set; } /// - /// Gets or sets an used for signature validation. + /// Gets the used for signature validation. /// - public IList IssuerSigningKeys { get; set; } + public IList IssuerSigningKeys => + _issuerSigningKeys ?? + Interlocked.CompareExchange(ref _issuerSigningKeys, [], null) ?? + _issuerSigningKeys; /// /// Allows overriding the delegate that will be used to validate the issuer of the token. diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.ValidateSignatureTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.ValidateSignatureTests.cs index 9ccdc3bd38..80b59e9b3b 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.ValidateSignatureTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.ValidateSignatureTests.cs @@ -37,11 +37,17 @@ public void ValidateSignature(JsonWebTokenHandlerValidateSignatureTheoryData the if (theoryData.Configuration is not null && theoryData.KeyToAddToConfiguration is not null) theoryData.Configuration.SigningKeys.Add(theoryData.KeyToAddToConfiguration); + if (theoryData.ValidationParameters is not null && theoryData.KeyToAddToValidationParameters is not null) + theoryData.ValidationParameters.IssuerSigningKeys.Add(theoryData.KeyToAddToValidationParameters); + SignatureValidationResult validationResult = JsonWebTokenHandler.ValidateSignature( jsonWebToken, theoryData.ValidationParameters, theoryData.Configuration, - new CallContext()); + new CallContext + { + DebugId = theoryData.TestId + }); if (validationResult.Exception != null) theoryData.ExpectedException.ProcessException(validationResult.Exception); @@ -124,29 +130,25 @@ public static TheoryData JsonWeb JWT = new JsonWebToken(EncodedJwts.LiveJwt), ValidationParameters = new ValidationParameters { - SignatureValidator = (token, parameters, configuration, callContext) => new SignatureValidationResult() + SignatureValidator = (token, parameters, configuration, callContext) => SignatureValidationResult.Success() }, - SignatureValidationResult = new SignatureValidationResult() + SignatureValidationResult = SignatureValidationResult.Success() }, new JsonWebTokenHandlerValidateSignatureTheoryData { TestId = "Valid_SignatureValidationResult_Success_KidMatches", SigningCredentials = KeyingMaterial.JsonWebKeyRsa256SigningCredentials, - ValidationParameters = new ValidationParameters - { - IssuerSigningKeys = [KeyingMaterial.JsonWebKeyRsa256SigningCredentials.Key] - }, - SignatureValidationResult = new SignatureValidationResult() - }, + ValidationParameters = new ValidationParameters(), + KeyToAddToValidationParameters = KeyingMaterial.JsonWebKeyRsa256SigningCredentials.Key, + SignatureValidationResult = SignatureValidationResult.Success(), + }, new JsonWebTokenHandlerValidateSignatureTheoryData { TestId = "Valid_SignatureValidationResult_Success_X5tMatches", SigningCredentials = KeyingMaterial.X509SigningCreds_1024_RsaSha2_Sha2, - ValidationParameters = new ValidationParameters - { - IssuerSigningKeys = [KeyingMaterial.X509SigningCreds_1024_RsaSha2_Sha2.Key] - }, - SignatureValidationResult = new SignatureValidationResult() + ValidationParameters = new ValidationParameters(), + KeyToAddToValidationParameters = KeyingMaterial.X509SigningCreds_1024_RsaSha2_Sha2.Key, + SignatureValidationResult = SignatureValidationResult.Success() }, new JsonWebTokenHandlerValidateSignatureTheoryData { @@ -156,7 +158,7 @@ public static TheoryData JsonWeb { IssuerSigningKeyResolver = (token, securityToken, kid, validationParameters, configuration, callContext) => KeyingMaterial.JsonWebKeyRsa256SigningCredentials.Key }, - SignatureValidationResult = new SignatureValidationResult() + SignatureValidationResult = SignatureValidationResult.Success() }, new JsonWebTokenHandlerValidateSignatureTheoryData { @@ -165,7 +167,7 @@ public static TheoryData JsonWeb Configuration = new OpenIdConnectConfiguration(), KeyToAddToConfiguration = KeyingMaterial.JsonWebKeyRsa256SigningCredentials.Key, ValidationParameters = new ValidationParameters(), - SignatureValidationResult = new SignatureValidationResult() + SignatureValidationResult = SignatureValidationResult.Success() }, new JsonWebTokenHandlerValidateSignatureTheoryData { @@ -173,19 +175,17 @@ public static TheoryData JsonWeb SigningCredentials = KeyingMaterial.DefaultSymmetricSigningCreds_256_Sha2_NoKeyId, ValidationParameters = new ValidationParameters { - IssuerSigningKeys = [KeyingMaterial.DefaultSymmetricSigningCreds_256_Sha2_NoKeyId.Key], TryAllIssuerSigningKeys = true }, - SignatureValidationResult = new SignatureValidationResult() + KeyToAddToValidationParameters = KeyingMaterial.DefaultSymmetricSigningCreds_256_Sha2_NoKeyId.Key, + SignatureValidationResult = SignatureValidationResult.Success() }, new JsonWebTokenHandlerValidateSignatureTheoryData { TestId = "Invalid_NoKeyId_DontTryAllKeys", SigningCredentials = KeyingMaterial.DefaultSymmetricSigningCreds_256_Sha2_NoKeyId, - ValidationParameters = new ValidationParameters - { - IssuerSigningKeys = [KeyingMaterial.DefaultSymmetricSigningCreds_256_Sha2_NoKeyId.Key], - }, + ValidationParameters = new ValidationParameters(), + KeyToAddToValidationParameters = KeyingMaterial.DefaultSymmetricSigningCreds_256_Sha2_NoKeyId.Key, ExpectedException = ExpectedException.SecurityTokenSignatureKeyNotFoundException("IDX10500:"), SignatureValidationResult = new SignatureValidationResult( ValidationFailureType.SignatureValidationFailed, @@ -218,6 +218,7 @@ public class JsonWebTokenHandlerValidateSignatureTheoryData : TheoryDataBase public BaseConfiguration Configuration { get; set; } public SigningCredentials SigningCredentials { get; internal set; } public SecurityKey KeyToAddToConfiguration { get; internal set; } + public SecurityKey KeyToAddToValidationParameters { get; internal set; } internal SignatureValidationResult SignatureValidationResult { get; set; } internal ValidationParameters ValidationParameters { get; set; } }