diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs index 122c7ebec4..23254e27d1 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs @@ -82,15 +82,15 @@ internal async ValueTask ValidateJWSAsync( if (validationParameters.TransformBeforeSignatureValidation != null) jsonWebToken = validationParameters.TransformBeforeSignatureValidation(jsonWebToken, validationParameters) as JsonWebToken; - if (validationParameters.SignatureValidator != null || validationParameters.SignatureValidatorUsingConfiguration != null) + if (validationParameters.SignatureValidator != null || validationParameters.SignatureValidatorUsingConfiguration != null || validationParameters.SignatureValidatorWithToken != null) { - var validatedToken = ValidateSignatureUsingDelegates(jsonWebToken, validationParameters); + var validatedToken = ValidateSignatureUsingDelegates(jsonWebToken, validationParameters, configuration); tokenValidationResult = await ValidateTokenPayloadAsync( validatedToken, validationParameters, configuration).ConfigureAwait(false); - Validators.ValidateIssuerSecurityKey(validatedToken.SigningKey, validatedToken, validationParameters); + Validators.ValidateIssuerSecurityKey(validatedToken.SigningKey, validatedToken, validationParameters, configuration); } else { @@ -325,6 +325,50 @@ internal static bool ValidateSignature(JsonWebToken jsonWebToken, SecurityKey ke return ValidateSignature(jsonWebToken, key, validationParameters, Telemetry.NullTelemetryClient.Instance); } + /// + /// Validates the signature of a using a single specified + /// . + /// + /// The whose signature is to be validated. + /// The to use for signature verification. + /// The containing + /// validation configuration, including the and algorithm + /// restrictions. + /// if the signature is valid for the specified key; + /// otherwise, . + /// + /// This method is intended for use within a + /// or + /// delegate, + /// enabling the delegate to call back into the handler's signature validation logic for + /// algorithms it does not handle directly. + /// This method validates a single key — the caller is responsible for key resolution. + /// Argument validation errors ( parameters) throw immediately. + /// Algorithm or provider failures throw + /// or . A return value of + /// indicates the cryptographic signature did not match. + /// + /// Thrown if the token's algorithm + /// fails algorithm validation. + /// Thrown if a + /// cannot be created for the key/algorithm pair. + public bool TryValidateSignature( + JsonWebToken jsonWebToken, + SecurityKey key, + TokenValidationParameters validationParameters) + { + if (jsonWebToken == null) + throw LogHelper.LogArgumentNullException(nameof(jsonWebToken)); + + if (key == null) + throw LogHelper.LogArgumentNullException(nameof(key)); + + if (validationParameters == null) + throw LogHelper.LogArgumentNullException(nameof(validationParameters)); + + return ValidateSignature(jsonWebToken, key, validationParameters, TelemetryClient); + } + private static void RecordSignatureValidationTelemetry( Telemetry.ITelemetryClient telemetryClient, string errorType, @@ -436,12 +480,21 @@ internal static bool ValidateSignature(JsonWebToken jsonWebToken, SecurityKey ke } } - private static JsonWebToken ValidateSignatureUsingDelegates(JsonWebToken jsonWebToken, TokenValidationParameters validationParameters) + private static JsonWebToken ValidateSignatureUsingDelegates(JsonWebToken jsonWebToken, TokenValidationParameters validationParameters, BaseConfiguration configuration) { - if (validationParameters.SignatureValidatorUsingConfiguration != null) + if (validationParameters.SignatureValidatorWithToken != null) + { + var validatedToken = validationParameters.SignatureValidatorWithToken(jsonWebToken, validationParameters, configuration); + if (validatedToken == null) + throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant(TokenLogMessages.IDX10505, jsonWebToken))); + + if (!(validatedToken is JsonWebToken validatedJsonWebToken)) + throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant(TokenLogMessages.IDX10506, LogHelper.MarkAsNonPII(typeof(JsonWebToken)), LogHelper.MarkAsNonPII(validatedToken.GetType()), jsonWebToken))); + + return validatedJsonWebToken; + } + else if (validationParameters.SignatureValidatorUsingConfiguration != null) { - // TODO - get configuration from validationParameters - BaseConfiguration configuration = null; var validatedToken = validationParameters.SignatureValidatorUsingConfiguration(jsonWebToken.EncodedToken, validationParameters, configuration); if (validatedToken == null) throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant(TokenLogMessages.IDX10505, jsonWebToken))); diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.JsonWebTokens/PublicAPI.Unshipped.txt index aed7d583d3..7f315ef826 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.JsonWebTokens/PublicAPI.Unshipped.txt @@ -1 +1,2 @@ -Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.DecryptTokenWithConfigurationAsync(Microsoft.IdentityModel.JsonWebTokens.JsonWebToken jwtToken, Microsoft.IdentityModel.Tokens.TokenValidationParameters validationParameters, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task \ No newline at end of file +Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.DecryptTokenWithConfigurationAsync(Microsoft.IdentityModel.JsonWebTokens.JsonWebToken jwtToken, Microsoft.IdentityModel.Tokens.TokenValidationParameters validationParameters, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task +Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.TryValidateSignature(Microsoft.IdentityModel.JsonWebTokens.JsonWebToken jsonWebToken, Microsoft.IdentityModel.Tokens.SecurityKey key, Microsoft.IdentityModel.Tokens.TokenValidationParameters validationParameters) -> bool diff --git a/src/Microsoft.IdentityModel.Tokens/Delegates.cs b/src/Microsoft.IdentityModel.Tokens/Delegates.cs index 42cc1d210b..b35c182908 100644 --- a/src/Microsoft.IdentityModel.Tokens/Delegates.cs +++ b/src/Microsoft.IdentityModel.Tokens/Delegates.cs @@ -136,6 +136,15 @@ namespace Microsoft.IdentityModel.Tokens /// The validated . public delegate SecurityToken SignatureValidatorUsingConfiguration(string token, TokenValidationParameters validationParameters, BaseConfiguration configuration); + /// + /// Validates the signature of an already-parsed token using additional configuration. + /// + /// The parsed . + /// The to be used for validating the token. + /// The configuration required for validation. + /// The validated . + public delegate SecurityToken SignatureValidatorWithToken(SecurityToken token, TokenValidationParameters validationParameters, BaseConfiguration configuration); + /// /// Reads the security token. /// diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net10.0/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net10.0/PublicAPI.Unshipped.txt index adef10032b..484b350e19 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net10.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net10.0/PublicAPI.Unshipped.txt @@ -1 +1,5 @@ ~Microsoft.IdentityModel.Tokens.CaseSensitiveClaimsIdentity.SecurityToken.set -> void +Microsoft.IdentityModel.Tokens.SignatureValidatorWithToken +Microsoft.IdentityModel.Tokens.TokenValidationParameters.SignatureValidatorWithToken.get -> Microsoft.IdentityModel.Tokens.SignatureValidatorWithToken +Microsoft.IdentityModel.Tokens.TokenValidationParameters.SignatureValidatorWithToken.set -> void +~virtual Microsoft.IdentityModel.Tokens.SignatureValidatorWithToken.Invoke(Microsoft.IdentityModel.Tokens.SecurityToken token, Microsoft.IdentityModel.Tokens.TokenValidationParameters validationParameters, Microsoft.IdentityModel.Tokens.BaseConfiguration configuration) -> Microsoft.IdentityModel.Tokens.SecurityToken diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net462/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net462/PublicAPI.Unshipped.txt index adef10032b..484b350e19 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net462/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net462/PublicAPI.Unshipped.txt @@ -1 +1,5 @@ ~Microsoft.IdentityModel.Tokens.CaseSensitiveClaimsIdentity.SecurityToken.set -> void +Microsoft.IdentityModel.Tokens.SignatureValidatorWithToken +Microsoft.IdentityModel.Tokens.TokenValidationParameters.SignatureValidatorWithToken.get -> Microsoft.IdentityModel.Tokens.SignatureValidatorWithToken +Microsoft.IdentityModel.Tokens.TokenValidationParameters.SignatureValidatorWithToken.set -> void +~virtual Microsoft.IdentityModel.Tokens.SignatureValidatorWithToken.Invoke(Microsoft.IdentityModel.Tokens.SecurityToken token, Microsoft.IdentityModel.Tokens.TokenValidationParameters validationParameters, Microsoft.IdentityModel.Tokens.BaseConfiguration configuration) -> Microsoft.IdentityModel.Tokens.SecurityToken diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net472/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net472/PublicAPI.Unshipped.txt index adef10032b..484b350e19 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net472/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net472/PublicAPI.Unshipped.txt @@ -1 +1,5 @@ ~Microsoft.IdentityModel.Tokens.CaseSensitiveClaimsIdentity.SecurityToken.set -> void +Microsoft.IdentityModel.Tokens.SignatureValidatorWithToken +Microsoft.IdentityModel.Tokens.TokenValidationParameters.SignatureValidatorWithToken.get -> Microsoft.IdentityModel.Tokens.SignatureValidatorWithToken +Microsoft.IdentityModel.Tokens.TokenValidationParameters.SignatureValidatorWithToken.set -> void +~virtual Microsoft.IdentityModel.Tokens.SignatureValidatorWithToken.Invoke(Microsoft.IdentityModel.Tokens.SecurityToken token, Microsoft.IdentityModel.Tokens.TokenValidationParameters validationParameters, Microsoft.IdentityModel.Tokens.BaseConfiguration configuration) -> Microsoft.IdentityModel.Tokens.SecurityToken diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net6.0/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net6.0/PublicAPI.Unshipped.txt index adef10032b..484b350e19 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net6.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net6.0/PublicAPI.Unshipped.txt @@ -1 +1,5 @@ ~Microsoft.IdentityModel.Tokens.CaseSensitiveClaimsIdentity.SecurityToken.set -> void +Microsoft.IdentityModel.Tokens.SignatureValidatorWithToken +Microsoft.IdentityModel.Tokens.TokenValidationParameters.SignatureValidatorWithToken.get -> Microsoft.IdentityModel.Tokens.SignatureValidatorWithToken +Microsoft.IdentityModel.Tokens.TokenValidationParameters.SignatureValidatorWithToken.set -> void +~virtual Microsoft.IdentityModel.Tokens.SignatureValidatorWithToken.Invoke(Microsoft.IdentityModel.Tokens.SecurityToken token, Microsoft.IdentityModel.Tokens.TokenValidationParameters validationParameters, Microsoft.IdentityModel.Tokens.BaseConfiguration configuration) -> Microsoft.IdentityModel.Tokens.SecurityToken diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net8.0/PublicAPI.Unshipped.txt index adef10032b..484b350e19 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net8.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net8.0/PublicAPI.Unshipped.txt @@ -1 +1,5 @@ ~Microsoft.IdentityModel.Tokens.CaseSensitiveClaimsIdentity.SecurityToken.set -> void +Microsoft.IdentityModel.Tokens.SignatureValidatorWithToken +Microsoft.IdentityModel.Tokens.TokenValidationParameters.SignatureValidatorWithToken.get -> Microsoft.IdentityModel.Tokens.SignatureValidatorWithToken +Microsoft.IdentityModel.Tokens.TokenValidationParameters.SignatureValidatorWithToken.set -> void +~virtual Microsoft.IdentityModel.Tokens.SignatureValidatorWithToken.Invoke(Microsoft.IdentityModel.Tokens.SecurityToken token, Microsoft.IdentityModel.Tokens.TokenValidationParameters validationParameters, Microsoft.IdentityModel.Tokens.BaseConfiguration configuration) -> Microsoft.IdentityModel.Tokens.SecurityToken diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net9.0/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net9.0/PublicAPI.Unshipped.txt index adef10032b..484b350e19 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net9.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net9.0/PublicAPI.Unshipped.txt @@ -1 +1,5 @@ ~Microsoft.IdentityModel.Tokens.CaseSensitiveClaimsIdentity.SecurityToken.set -> void +Microsoft.IdentityModel.Tokens.SignatureValidatorWithToken +Microsoft.IdentityModel.Tokens.TokenValidationParameters.SignatureValidatorWithToken.get -> Microsoft.IdentityModel.Tokens.SignatureValidatorWithToken +Microsoft.IdentityModel.Tokens.TokenValidationParameters.SignatureValidatorWithToken.set -> void +~virtual Microsoft.IdentityModel.Tokens.SignatureValidatorWithToken.Invoke(Microsoft.IdentityModel.Tokens.SecurityToken token, Microsoft.IdentityModel.Tokens.TokenValidationParameters validationParameters, Microsoft.IdentityModel.Tokens.BaseConfiguration configuration) -> Microsoft.IdentityModel.Tokens.SecurityToken diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt index adef10032b..484b350e19 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt @@ -1 +1,5 @@ ~Microsoft.IdentityModel.Tokens.CaseSensitiveClaimsIdentity.SecurityToken.set -> void +Microsoft.IdentityModel.Tokens.SignatureValidatorWithToken +Microsoft.IdentityModel.Tokens.TokenValidationParameters.SignatureValidatorWithToken.get -> Microsoft.IdentityModel.Tokens.SignatureValidatorWithToken +Microsoft.IdentityModel.Tokens.TokenValidationParameters.SignatureValidatorWithToken.set -> void +~virtual Microsoft.IdentityModel.Tokens.SignatureValidatorWithToken.Invoke(Microsoft.IdentityModel.Tokens.SecurityToken token, Microsoft.IdentityModel.Tokens.TokenValidationParameters validationParameters, Microsoft.IdentityModel.Tokens.BaseConfiguration configuration) -> Microsoft.IdentityModel.Tokens.SecurityToken diff --git a/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs b/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs index fa6b589762..789fc6f736 100644 --- a/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs +++ b/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs @@ -84,6 +84,7 @@ protected TokenValidationParameters(TokenValidationParameters other) SaveSigninToken = other.SaveSigninToken; SignatureValidator = other.SignatureValidator; SignatureValidatorUsingConfiguration = other.SignatureValidatorUsingConfiguration; + SignatureValidatorWithToken = other.SignatureValidatorWithToken; TokenDecryptionKey = other.TokenDecryptionKey; TokenDecryptionKeyResolver = other.TokenDecryptionKeyResolver; TokenDecryptionKeys = other.TokenDecryptionKeys; @@ -556,6 +557,18 @@ public string RoleClaimType /// public SignatureValidatorUsingConfiguration SignatureValidatorUsingConfiguration { get; set; } + /// + /// Gets or sets a delegate that will be used to validate the signature of an already-parsed token, + /// using the and the . + /// + /// + /// If set, this delegate takes priority over + /// and . Unlike those delegates, this one receives the parsed + /// directly, avoiding the need to re-parse the token string. + /// This delegate is only supported by JsonWebTokenHandler. + /// + public SignatureValidatorWithToken SignatureValidatorWithToken { get; set; } + /// /// Gets or sets the time provider. /// diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.TryValidateSignatureTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.TryValidateSignatureTests.cs new file mode 100644 index 0000000000..361517fd3a --- /dev/null +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.TryValidateSignatureTests.cs @@ -0,0 +1,351 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Threading.Tasks; +using Microsoft.IdentityModel.Protocols; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Microsoft.IdentityModel.TestUtils; +using Microsoft.IdentityModel.Tokens; +using Xunit; + +namespace Microsoft.IdentityModel.JsonWebTokens.Tests +{ + public class JsonWebTokenHandlerTryValidateSignatureTests + { + [Fact] + public void TryValidateSignature_ValidSignature_ReturnsTrue() + { + var handler = new JsonWebTokenHandler(); + var descriptor = new SecurityTokenDescriptor + { + SigningCredentials = new SigningCredentials( + KeyingMaterial.RsaSecurityKey_2048, SecurityAlgorithms.RsaSha256), + Issuer = "https://test-issuer.example.com" + }; + + var tokenString = handler.CreateToken(descriptor); + var jsonWebToken = new JsonWebToken(tokenString); + + var tvp = new TokenValidationParameters + { + ValidateIssuer = false, + ValidateAudience = false, + ValidateLifetime = false + }; + + bool result = handler.TryValidateSignature( + jsonWebToken, + KeyingMaterial.RsaSecurityKey_2048, + tvp); + + Assert.True(result); + } + + [Fact] + public void TryValidateSignature_WrongKey_ReturnsFalse() + { + var handler = new JsonWebTokenHandler(); + var descriptor = new SecurityTokenDescriptor + { + SigningCredentials = new SigningCredentials( + KeyingMaterial.RsaSecurityKey_2048, SecurityAlgorithms.RsaSha256), + Issuer = "https://test-issuer.example.com" + }; + + var tokenString = handler.CreateToken(descriptor); + var jsonWebToken = new JsonWebToken(tokenString); + + var tvp = new TokenValidationParameters + { + ValidateIssuer = false, + ValidateAudience = false, + ValidateLifetime = false + }; + + // Use a different key — signature should not match + bool result = handler.TryValidateSignature( + jsonWebToken, + KeyingMaterial.RsaSecurityKey_4096, + tvp); + + Assert.False(result); + } + + [Fact] + public void TryValidateSignature_EcdsaKey_ReturnsTrue() + { + var handler = new JsonWebTokenHandler(); + var descriptor = new SecurityTokenDescriptor + { + SigningCredentials = new SigningCredentials( + KeyingMaterial.Ecdsa256Key, SecurityAlgorithms.EcdsaSha256), + Issuer = "https://test-issuer.example.com" + }; + + var tokenString = handler.CreateToken(descriptor); + var jsonWebToken = new JsonWebToken(tokenString); + + var tvp = new TokenValidationParameters + { + ValidateIssuer = false, + ValidateAudience = false, + ValidateLifetime = false + }; + + bool result = handler.TryValidateSignature( + jsonWebToken, + KeyingMaterial.Ecdsa256Key, + tvp); + + Assert.True(result); + } + + [Fact] + public void TryValidateSignature_NullToken_ThrowsArgumentNullException() + { + var handler = new JsonWebTokenHandler(); + var tvp = new TokenValidationParameters(); + + Assert.Throws(() => + handler.TryValidateSignature(null, KeyingMaterial.RsaSecurityKey_2048, tvp)); + } + + [Fact] + public void TryValidateSignature_NullKey_ThrowsArgumentNullException() + { + var handler = new JsonWebTokenHandler(); + var tokenString = handler.CreateToken(new SecurityTokenDescriptor + { + SigningCredentials = new SigningCredentials( + KeyingMaterial.RsaSecurityKey_2048, SecurityAlgorithms.RsaSha256) + }); + var jsonWebToken = new JsonWebToken(tokenString); + var tvp = new TokenValidationParameters(); + + Assert.Throws(() => + handler.TryValidateSignature(jsonWebToken, null, tvp)); + } + + [Fact] + public void TryValidateSignature_NullValidationParameters_ThrowsArgumentNullException() + { + var handler = new JsonWebTokenHandler(); + var tokenString = handler.CreateToken(new SecurityTokenDescriptor + { + SigningCredentials = new SigningCredentials( + KeyingMaterial.RsaSecurityKey_2048, SecurityAlgorithms.RsaSha256) + }); + var jsonWebToken = new JsonWebToken(tokenString); + + Assert.Throws(() => + handler.TryValidateSignature(jsonWebToken, KeyingMaterial.RsaSecurityKey_2048, null)); + } + + [Fact] + public void TryValidateSignature_AlgorithmMismatch_ReturnsFalse() + { + var handler = new JsonWebTokenHandler(); + var descriptor = new SecurityTokenDescriptor + { + SigningCredentials = new SigningCredentials( + KeyingMaterial.RsaSecurityKey_2048, SecurityAlgorithms.RsaSha256), + Issuer = "https://test-issuer.example.com" + }; + + var tokenString = handler.CreateToken(descriptor); + var jsonWebToken = new JsonWebToken(tokenString); + + var tvp = new TokenValidationParameters + { + ValidateIssuer = false, + ValidateAudience = false, + ValidateLifetime = false + }; + + // ECDSA key can't verify RSA signature + bool result = handler.TryValidateSignature( + jsonWebToken, + KeyingMaterial.Ecdsa256Key, + tvp); + + Assert.False(result); + } + + [Fact] + public async Task TryValidateSignature_UsedWithinSignatureValidatorDelegate() + { + var handler = new JsonWebTokenHandler(); + var signingKey = KeyingMaterial.RsaSecurityKey_2048; + + var tokenString = handler.CreateToken(new SecurityTokenDescriptor + { + SigningCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.RsaSha256), + Issuer = "https://test-issuer.example.com", + Audience = "https://test-audience.example.com" + }); + + // Set up a SignatureValidatorWithToken delegate that calls back + // via TryValidateSignature, simulating a delegate that handles some algorithms + // directly and falls back to the handler for others. + var tvp = new TokenValidationParameters + { + ValidIssuer = "https://test-issuer.example.com", + ValidAudience = "https://test-audience.example.com", + IssuerSigningKey = signingKey, + ValidateLifetime = false, + SignatureValidatorWithToken = (token, validationParameters, configuration) => + { + var jwt = (JsonWebToken)token; + var key = validationParameters.IssuerSigningKey; + + if (!handler.TryValidateSignature(jwt, key, validationParameters)) + throw new SecurityTokenInvalidSignatureException("Signature validation failed."); + + jwt.SigningKey = key; + return jwt; + } + }; + + var result = await handler.ValidateTokenAsync(tokenString, tvp); + Assert.True(result.IsValid, $"Validation failed: {result.Exception?.Message}"); + } + + [Fact] + public async Task SignatureValidatorWithToken_ReceivesResolvedBaseConfiguration() + { + var handler = new JsonWebTokenHandler(); + var signingKey = KeyingMaterial.RsaSecurityKey_2048; + + var tokenString = handler.CreateToken(new SecurityTokenDescriptor + { + SigningCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.RsaSha256), + Issuer = "https://test-issuer.example.com", + Audience = "https://test-audience.example.com" + }); + + BaseConfiguration capturedConfiguration = null; + + var tvp = new TokenValidationParameters + { + ValidIssuer = "https://test-issuer.example.com", + ValidAudience = "https://test-audience.example.com", + IssuerSigningKey = signingKey, + ValidateLifetime = false, + SignatureValidatorWithToken = (token, validationParameters, configuration) => + { + capturedConfiguration = configuration; + var jwt = (JsonWebToken)token; + jwt.SigningKey = validationParameters.IssuerSigningKey; + return jwt; + } + }; + + // Arrange — set a BaseConfiguration via ConfigurationManager simulation + var expectedConfiguration = new OpenIdConnectConfiguration + { + Issuer = "https://test-issuer.example.com" + }; + expectedConfiguration.SigningKeys.Add(signingKey); + + // Use the ConfigurationManager property to provide configuration + tvp.ConfigurationManager = new StaticConfigurationManager(expectedConfiguration); + + var result = await handler.ValidateTokenAsync(tokenString, tvp); + Assert.True(result.IsValid, $"Validation failed: {result.Exception?.Message}"); + Assert.NotNull(capturedConfiguration); + Assert.Same(expectedConfiguration, capturedConfiguration); + } + + [Fact] + public async Task SignatureValidatorUsingConfiguration_ReceivesResolvedBaseConfiguration() + { + var handler = new JsonWebTokenHandler(); + var signingKey = KeyingMaterial.RsaSecurityKey_2048; + + var tokenString = handler.CreateToken(new SecurityTokenDescriptor + { + SigningCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.RsaSha256), + Issuer = "https://test-issuer.example.com", + Audience = "https://test-audience.example.com" + }); + + BaseConfiguration capturedConfiguration = null; + + var expectedConfiguration = new OpenIdConnectConfiguration + { + Issuer = "https://test-issuer.example.com" + }; + expectedConfiguration.SigningKeys.Add(signingKey); + + var tvp = new TokenValidationParameters + { + ValidIssuer = "https://test-issuer.example.com", + ValidAudience = "https://test-audience.example.com", + IssuerSigningKey = signingKey, + ValidateLifetime = false, + ConfigurationManager = new StaticConfigurationManager(expectedConfiguration), + SignatureValidatorUsingConfiguration = (token, validationParameters, configuration) => + { + capturedConfiguration = configuration; + var jwt = new JsonWebToken(token); + jwt.SigningKey = validationParameters.IssuerSigningKey; + return jwt; + } + }; + + var result = await handler.ValidateTokenAsync(tokenString, tvp); + Assert.True(result.IsValid, $"Validation failed: {result.Exception?.Message}"); + Assert.NotNull(capturedConfiguration); + Assert.Same(expectedConfiguration, capturedConfiguration); + } + + [Fact] + public async Task IssuerSigningKeyValidatorUsingConfiguration_ReceivesConfiguration_WhenSignatureDelegateUsed() + { + var handler = new JsonWebTokenHandler(); + var signingKey = KeyingMaterial.RsaSecurityKey_2048; + + var tokenString = handler.CreateToken(new SecurityTokenDescriptor + { + SigningCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.RsaSha256), + Issuer = "https://test-issuer.example.com", + Audience = "https://test-audience.example.com" + }); + + BaseConfiguration capturedKeyValidationConfig = null; + + var expectedConfiguration = new OpenIdConnectConfiguration + { + Issuer = "https://test-issuer.example.com" + }; + expectedConfiguration.SigningKeys.Add(signingKey); + + var tvp = new TokenValidationParameters + { + ValidIssuer = "https://test-issuer.example.com", + ValidAudience = "https://test-audience.example.com", + IssuerSigningKey = signingKey, + ValidateLifetime = false, + ValidateIssuerSigningKey = true, + ConfigurationManager = new StaticConfigurationManager(expectedConfiguration), + SignatureValidatorWithToken = (token, validationParameters, configuration) => + { + var jwt = (JsonWebToken)token; + jwt.SigningKey = validationParameters.IssuerSigningKey; + return jwt; + }, + IssuerSigningKeyValidatorUsingConfiguration = (securityKey, securityToken, tvp, configuration) => + { + capturedKeyValidationConfig = configuration; + return true; + } + }; + + var result = await handler.ValidateTokenAsync(tokenString, tvp); + Assert.True(result.IsValid, $"Validation failed: {result.Exception?.Message}"); + Assert.NotNull(capturedKeyValidationConfig); + Assert.Same(expectedConfiguration, capturedKeyValidationConfig); + } + } +} \ No newline at end of file