diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateSignature.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateSignature.cs
new file mode 100644
index 0000000000..a6ad80e9ad
--- /dev/null
+++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateSignature.cs
@@ -0,0 +1,368 @@
+// 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.
+ ///
+ /// 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,
+ 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);
+
+ // 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.
+ (SignatureValidationResult? configResult, bool configKidMatched, KeyMatchFailedResult? configFailedResult) = ValidateUsingKeys(
+ jwtToken,
+ validationParameters,
+ configuration?.SigningKeys,
+ callContext);
+
+ if (configResult is not null)
+ return configResult;
+
+ (SignatureValidationResult? vpResult, bool vpKidMatched, KeyMatchFailedResult? vpFailedResult) = ValidateUsingKeys(
+ jwtToken,
+ validationParameters,
+ validationParameters.IssuerSigningKeys,
+ callContext);
+
+ if (vpResult is not null)
+ return vpResult;
+
+ 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()));
+
+ StringBuilder exceptionStrings = new();
+ StringBuilder keysAttempted = new ();
+
+ 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(
+ ValidationFailureType.SignatureValidationFailed,
+ GetSignatureValidationFailureExceptionDetails(
+ jwtToken,
+ validationParameters,
+ configuration,
+ exceptionStrings,
+ keysAttempted,
+ kidExists,
+ 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)
+ {
+ CryptoProviderFactory 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);
+
+ SignatureProvider 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 SignatureValidationResult.Success();
+ 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.
+ 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
+ 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(numKeysInTokenValidationParameters),
+ LogHelper.MarkAsNonPII(numKeysInConfiguration),
+ LogHelper.MarkAsNonPII(keyLocation),
+ LogHelper.MarkAsNonPII(jwtToken.Kid),
+ exceptionStrings.ToString(),
+ LogHelper.MarkAsSecurityArtifact(jwtToken.EncodedToken, JwtTokenUtilities.SafeLogJwtToken)),
+ 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());
+ }
+
+ 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/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..9ca45eb72c
--- /dev/null
+++ b/src/Microsoft.IdentityModel.Tokens/Validation/SignatureValidationResult.cs
@@ -0,0 +1,76 @@
+// 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(bool isValid, ValidationFailureType validationFailureType) : base(validationFailureType)
+ {
+ IsValid = isValid;
+ }
+
+ ///
+ /// 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 successful validation.
+ ///
+ internal static SignatureValidationResult Success() =>
+ new SignatureValidationResult(true, ValidationFailureType.ValidationSucceeded);
+
+ ///
+ /// 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
+ {
+ 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 d8c41f774d..97aa7169c1 100644
--- a/src/Microsoft.IdentityModel.Tokens/Validation/ValidationParameters.cs
+++ b/src/Microsoft.IdentityModel.Tokens/Validation/ValidationParameters.cs
@@ -21,7 +21,7 @@ internal class ValidationParameters
private string _nameClaimType = ClaimsIdentity.DefaultNameClaimType;
private string _roleClaimType = ClaimsIdentity.DefaultRoleClaimType;
private Dictionary _instancePropertyBag;
-
+ private IList _issuerSigningKeys;
private IList _validIssuers;
private IList _validTokenTypes;
private IList _validAudiences;
@@ -30,6 +30,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;
@@ -69,7 +70,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;
@@ -204,7 +205,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);
@@ -214,7 +215,7 @@ public virtual ClaimsIdentity CreateClaimsIdentity(SecurityToken securityToken,
nameClaimType = NameClaimType;
}
- string roleClaimType = null;
+ string roleClaimType;
if (RoleClaimTypeRetriever != null)
{
roleClaimType = RoleClaimTypeRetriever(securityToken, issuer);
@@ -293,12 +294,15 @@ 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.
+ /// Gets the used for signature validation.
///
- public IList IssuerSigningKeys { get; }
+ 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.
@@ -375,7 +379,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.
@@ -433,7 +437,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.
@@ -469,6 +476,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;
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..80b59e9b3b
--- /dev/null
+++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.ValidateSignatureTests.cs
@@ -0,0 +1,225 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// 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;
+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);
+ 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);
+
+ 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
+ {
+ DebugId = theoryData.TestId
+ });
+
+ 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
+ {
+ var unsignedToken = new JsonWebToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.");
+ return new TheoryData
+ {
+ new JsonWebTokenHandlerValidateSignatureTheoryData {
+ TestId = "Invalid_Null_JWT",
+ JWT = null,
+ ExpectedException = ExpectedException.ArgumentNullException("IDX10000:"),
+ SignatureValidationResult = new SignatureValidationResult(
+ ValidationFailureType.SignatureValidationFailed,
+ new ExceptionDetail(
+ new MessageDetail(
+ TokenLogMessages.IDX10000,
+ "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) => SignatureValidationResult.Success()
+ },
+ SignatureValidationResult = SignatureValidationResult.Success()
+ },
+ new JsonWebTokenHandlerValidateSignatureTheoryData
+ {
+ TestId = "Valid_SignatureValidationResult_Success_KidMatches",
+ SigningCredentials = KeyingMaterial.JsonWebKeyRsa256SigningCredentials,
+ 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(),
+ KeyToAddToValidationParameters = KeyingMaterial.X509SigningCreds_1024_RsaSha2_Sha2.Key,
+ SignatureValidationResult = SignatureValidationResult.Success()
+ },
+ new JsonWebTokenHandlerValidateSignatureTheoryData
+ {
+ TestId = "Valid_IssuerSigningKeyResolverReturnsKeyThatMatches",
+ SigningCredentials = KeyingMaterial.JsonWebKeyRsa256SigningCredentials,
+ ValidationParameters = new ValidationParameters
+ {
+ IssuerSigningKeyResolver = (token, securityToken, kid, validationParameters, configuration, callContext) => KeyingMaterial.JsonWebKeyRsa256SigningCredentials.Key
+ },
+ SignatureValidationResult = SignatureValidationResult.Success()
+ },
+ new JsonWebTokenHandlerValidateSignatureTheoryData
+ {
+ TestId = "Valid_ConfurationReturnsKeyThatMatches",
+ SigningCredentials = KeyingMaterial.JsonWebKeyRsa256SigningCredentials,
+ Configuration = new OpenIdConnectConfiguration(),
+ KeyToAddToConfiguration = KeyingMaterial.JsonWebKeyRsa256SigningCredentials.Key,
+ ValidationParameters = new ValidationParameters(),
+ SignatureValidationResult = SignatureValidationResult.Success()
+ },
+ new JsonWebTokenHandlerValidateSignatureTheoryData
+ {
+ TestId = "Valid_NoKeyId_TryAllKeys",
+ SigningCredentials = KeyingMaterial.DefaultSymmetricSigningCreds_256_Sha2_NoKeyId,
+ ValidationParameters = new ValidationParameters
+ {
+ TryAllIssuerSigningKeys = true
+ },
+ 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(),
+ KeyToAddToValidationParameters = 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()))
+ }
+ };
+ }
+ }
+ }
+
+ 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; }
+ public SecurityKey KeyToAddToValidationParameters { get; internal 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);