From 4a28a69700bc727e17ace034276208669e528cdb Mon Sep 17 00:00:00 2001 From: Ignacio Inglese Date: Thu, 24 Oct 2024 09:58:07 +0100 Subject: [PATCH] Regression tests: Algorithm (#2934) * Added AlgorithmValidationError, refactored error creation in ValidateSignature to surface the invalid algorithm as part of the error * Added regression/comparison tests for algorithm validation scenarios. --- .../JsonWebTokenHandler.ValidateSignature.cs | 30 +++- .../InternalAPI.Unshipped.txt | 5 + .../Details/AlgorithmValidationError.cs | 42 ++++++ .../Validation/Validators.Algorithm.cs | 6 +- ...ndler.ValidateTokenAsyncTests.Algorithm.cs | 134 ++++++++++++++++++ 5 files changed, 207 insertions(+), 10 deletions(-) create mode 100644 src/Microsoft.IdentityModel.Tokens/Validation/Results/Details/AlgorithmValidationError.cs create mode 100644 test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.ValidateTokenAsyncTests.Algorithm.cs diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateSignature.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateSignature.cs index 2b66d17adf..1340c61781 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateSignature.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateSignature.cs @@ -241,13 +241,29 @@ private static ValidationResult ValidateSignatureWithKey( callContext); if (!result.IsValid) - return new ValidationError( - new MessageDetail( - TokenLogMessages.IDX10518, - result.UnwrapError().MessageDetail.Message), - ValidationFailureType.SignatureAlgorithmValidationFailed, - typeof(SecurityTokenInvalidAlgorithmException), - new StackFrame(true)); + { + if (result.UnwrapError() is AlgorithmValidationError algorithmValidationError) + { + return new AlgorithmValidationError( + new MessageDetail( + TokenLogMessages.IDX10518, + algorithmValidationError.MessageDetail.Message), + typeof(SecurityTokenInvalidAlgorithmException), + new StackFrame(true), + algorithmValidationError.InvalidAlgorithm); + } + else + { + // overridden delegate did not return an AlgorithmValidationError + return new ValidationError( + new MessageDetail( + TokenLogMessages.IDX10518, + result.UnwrapError().MessageDetail.Message), + ValidationFailureType.SignatureAlgorithmValidationFailed, + typeof(SecurityTokenInvalidAlgorithmException), + new StackFrame(true)); + } + } SignatureProvider signatureProvider = cryptoProviderFactory.CreateForVerifying(key, jsonWebToken.Alg); try diff --git a/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt index c656cb5fc9..56f33d0862 100644 --- a/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt @@ -1,5 +1,9 @@ const Microsoft.IdentityModel.Tokens.LogMessages.IDX10002 = "IDX10002: Unknown exception type returned. Type: '{0}'. Message: '{1}'." -> string const Microsoft.IdentityModel.Tokens.LogMessages.IDX10268 = "IDX10268: Unable to validate audience, validationParameters.ValidAudiences.Count == 0." -> string +Microsoft.IdentityModel.Tokens.AlgorithmValidationError +Microsoft.IdentityModel.Tokens.AlgorithmValidationError.AlgorithmValidationError(Microsoft.IdentityModel.Tokens.MessageDetail messageDetail, System.Type exceptionType, System.Diagnostics.StackFrame stackFrame, string invalidAlgorithm) -> void +Microsoft.IdentityModel.Tokens.AlgorithmValidationError.InvalidAlgorithm.get -> string +Microsoft.IdentityModel.Tokens.AlgorithmValidationError._invalidAlgorithm -> string Microsoft.IdentityModel.Tokens.AudienceValidationError.AudienceValidationError(Microsoft.IdentityModel.Tokens.MessageDetail messageDetail, Microsoft.IdentityModel.Tokens.ValidationFailureType failureType, System.Type exceptionType, System.Diagnostics.StackFrame stackFrame, System.Collections.Generic.IList tokenAudiences, System.Collections.Generic.IList validAudiences) -> void Microsoft.IdentityModel.Tokens.AudienceValidationError.TokenAudiences.get -> System.Collections.Generic.IList Microsoft.IdentityModel.Tokens.IssuerValidationSource.IssuerMatchedConfiguration = 1 -> Microsoft.IdentityModel.Tokens.IssuerValidationSource @@ -12,6 +16,7 @@ Microsoft.IdentityModel.Tokens.ValidationError.GetException(System.Type exceptio Microsoft.IdentityModel.Tokens.ValidationResult.Error.get -> Microsoft.IdentityModel.Tokens.ValidationError Microsoft.IdentityModel.Tokens.ValidationResult.IsValid.get -> bool Microsoft.IdentityModel.Tokens.ValidationResult.Result.get -> TResult +override Microsoft.IdentityModel.Tokens.AlgorithmValidationError.GetException() -> System.Exception static Microsoft.IdentityModel.Tokens.AudienceValidationError.AudiencesCountZero -> System.Diagnostics.StackFrame static Microsoft.IdentityModel.Tokens.AudienceValidationError.AudiencesNull -> System.Diagnostics.StackFrame static Microsoft.IdentityModel.Tokens.AudienceValidationError.ValidateAudienceFailed -> System.Diagnostics.StackFrame diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/Results/Details/AlgorithmValidationError.cs b/src/Microsoft.IdentityModel.Tokens/Validation/Results/Details/AlgorithmValidationError.cs new file mode 100644 index 0000000000..c867fdb193 --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens/Validation/Results/Details/AlgorithmValidationError.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; + +#nullable enable +namespace Microsoft.IdentityModel.Tokens +{ + internal class AlgorithmValidationError : ValidationError + { + protected string? _invalidAlgorithm; + + public AlgorithmValidationError( + MessageDetail messageDetail, + Type exceptionType, + StackFrame stackFrame, + string? invalidAlgorithm) : + base(messageDetail, ValidationFailureType.AlgorithmValidationFailed, exceptionType, stackFrame) + { + _invalidAlgorithm = invalidAlgorithm; + } + + internal override Exception GetException() + { + if (ExceptionType == typeof(SecurityTokenInvalidAlgorithmException)) + { + SecurityTokenInvalidAlgorithmException exception = new(MessageDetail.Message, InnerException) + { + InvalidAlgorithm = _invalidAlgorithm + }; + + return exception; + } + + return base.GetException(); + } + + internal string? InvalidAlgorithm => _invalidAlgorithm; + } +} +#nullable restore diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Algorithm.cs b/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Algorithm.cs index f21f7e237d..1651cd960b 100644 --- a/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Algorithm.cs +++ b/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Algorithm.cs @@ -53,13 +53,13 @@ internal static ValidationResult ValidateAlgorithm( if (validationParameters.ValidAlgorithms != null && validationParameters.ValidAlgorithms.Count > 0 && !validationParameters.ValidAlgorithms.Contains(algorithm, StringComparer.Ordinal)) - return new ValidationError( + return new AlgorithmValidationError( new MessageDetail( LogMessages.IDX10696, LogHelper.MarkAsNonPII(algorithm)), - ValidationFailureType.AlgorithmValidationFailed, typeof(SecurityTokenInvalidAlgorithmException), - new StackFrame(true)); + new StackFrame(true), + algorithm); return algorithm; } diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.ValidateTokenAsyncTests.Algorithm.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.ValidateTokenAsyncTests.Algorithm.cs new file mode 100644 index 0000000000..dfd0c76c86 --- /dev/null +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.ValidateTokenAsyncTests.Algorithm.cs @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#nullable enable +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.IdentityModel.TestUtils; +using Microsoft.IdentityModel.Tokens; +using Xunit; + +namespace Microsoft.IdentityModel.JsonWebTokens.Tests +{ + public partial class JsonWebTokenHandlerValidateTokenAsyncTests + { + [Theory, MemberData(nameof(ValidateTokenAsync_AlgorithmTestCases), DisableDiscoveryEnumeration = true)] + public async Task ValidateTokenAsync_Algorithm(ValidateTokenAsyncAlgorithmTheoryData theoryData) + { + var context = TestUtilities.WriteHeader($"{this}.ValidateTokenAsync_Algorithm", theoryData); + + string jwtString = CreateTokenWithSigningCredentials(theoryData.SigningCredentials); + + await ValidateAndCompareResults(jwtString, theoryData, context); + + TestUtilities.AssertFailIfErrors(context); + } + + public static TheoryData ValidateTokenAsync_AlgorithmTestCases + { + get + { + var theoryData = new TheoryData(); + + theoryData.Add(new ValidateTokenAsyncAlgorithmTheoryData("Valid_AlgorithmIsValid") + { + SigningCredentials = KeyingMaterial.DefaultX509SigningCreds_2048_RsaSha2_Sha2, + TokenValidationParameters = CreateTokenValidationParameters( + KeyingMaterial.DefaultX509SigningCreds_2048_RsaSha2_Sha2.Key, + validAlgorithms: [SecurityAlgorithms.RsaSha256Signature]), + ValidationParameters = CreateValidationParameters( + KeyingMaterial.DefaultX509SigningCreds_2048_RsaSha2_Sha2.Key, + validAlgorithms: [SecurityAlgorithms.RsaSha256Signature]), + }); + + theoryData.Add(new ValidateTokenAsyncAlgorithmTheoryData("Valid_ValidAlgorithmsIsNull") + { + SigningCredentials = KeyingMaterial.DefaultX509SigningCreds_2048_RsaSha2_Sha2, + TokenValidationParameters = CreateTokenValidationParameters( + KeyingMaterial.DefaultX509SigningCreds_2048_RsaSha2_Sha2.Key, + validAlgorithms: null), + ValidationParameters = CreateValidationParameters( + KeyingMaterial.DefaultX509SigningCreds_2048_RsaSha2_Sha2.Key, + validAlgorithms: null), + }); + + theoryData.Add(new ValidateTokenAsyncAlgorithmTheoryData("Valid_ValidAlgorithmsIsEmptyList") + { + SigningCredentials = KeyingMaterial.DefaultX509SigningCreds_2048_RsaSha2_Sha2, + TokenValidationParameters = CreateTokenValidationParameters( + KeyingMaterial.DefaultX509SigningCreds_2048_RsaSha2_Sha2.Key, validAlgorithms: []), + ValidationParameters = CreateValidationParameters( + KeyingMaterial.DefaultX509SigningCreds_2048_RsaSha2_Sha2.Key, validAlgorithms: []), + }); + + theoryData.Add(new ValidateTokenAsyncAlgorithmTheoryData("Invalid_TokenIsSignedWithAnInvalidAlgorithm") + { + // Token is signed with HmacSha256 but only sha256 is considered valid for this test's purposes + SigningCredentials = KeyingMaterial.DefaultSymmetricSigningCreds_256_Sha2, + TokenValidationParameters = CreateTokenValidationParameters( + KeyingMaterial.DefaultSymmetricSigningCreds_256_Sha2.Key, + validAlgorithms: [SecurityAlgorithms.Sha256]), + ValidationParameters = CreateValidationParameters( + KeyingMaterial.DefaultSymmetricSigningCreds_256_Sha2.Key, + validAlgorithms: [SecurityAlgorithms.Sha256]), + ExpectedIsValid = false, + ExpectedException = ExpectedException.SecurityTokenInvalidSignatureException("IDX10511:"), + ExpectedExceptionValidationParameters = ExpectedException.SecurityTokenInvalidAlgorithmException( + "IDX10518:", + propertiesExpected: new() { { "InvalidAlgorithm", SecurityAlgorithms.HmacSha256Signature } }), + }); + + return theoryData; + + static TokenValidationParameters CreateTokenValidationParameters( + SecurityKey? signingKey = null, List? validAlgorithms = null) + { + // only validate the signature and algorithm + var tokenValidationParameters = new TokenValidationParameters + { + ValidateAudience = false, + ValidateIssuer = false, + ValidateLifetime = false, + ValidateTokenReplay = false, + ValidateIssuerSigningKey = false, + RequireSignedTokens = true, + IssuerSigningKey = signingKey, + }; + + tokenValidationParameters.ValidAlgorithms = validAlgorithms; + + return tokenValidationParameters; + } + + static ValidationParameters CreateValidationParameters( + SecurityKey? signingKey = null, List? validAlgorithms = null) + { + ValidationParameters validationParameters = new ValidationParameters(); + + if (signingKey is not null) + validationParameters.IssuerSigningKeys.Add(signingKey); + + validationParameters.ValidAlgorithms = validAlgorithms; + + // Skip all validations except signature and algorithm + validationParameters.AudienceValidator = SkipValidationDelegates.SkipAudienceValidation; + validationParameters.IssuerSigningKeyValidator = SkipValidationDelegates.SkipIssuerSigningKeyValidation; + validationParameters.IssuerValidatorAsync = SkipValidationDelegates.SkipIssuerValidation; + validationParameters.LifetimeValidator = SkipValidationDelegates.SkipLifetimeValidation; + validationParameters.TokenReplayValidator = SkipValidationDelegates.SkipTokenReplayValidation; + validationParameters.TypeValidator = SkipValidationDelegates.SkipTokenTypeValidation; + + return validationParameters; + } + } + } + + public class ValidateTokenAsyncAlgorithmTheoryData : ValidateTokenAsyncBaseTheoryData + { + public ValidateTokenAsyncAlgorithmTheoryData(string testId) : base(testId) { } + + public SigningCredentials? SigningCredentials { get; set; } + } + } +} +#nullable restore