From 788a7a466ca3353dd5ac45433c1441843bf2462b Mon Sep 17 00:00:00 2001 From: Ignacio Inglese Date: Thu, 24 Oct 2024 10:50:21 +0100 Subject: [PATCH] Regression tests: Token Replay (#2931) * Removed calls to MarkAsUnsafeSecurityArtifact as they was printing the tokens in clear text and we cannot use JwtTokenUtilities.SafeLogJwt due to the fact that this handles other SecurityTokens as well. * Removed duplicated CreateToken method * Added regression/comparison tests for JWT on token replay scenarios * Updated unit tests to reflect updated exception * Resolve post merge issue --- .../Validation/Validators.TokenReplay.cs | 9 +- ...eTokenAsyncTests.Lifetime.Extensibility.cs | 18 +- ...ler.ValidateTokenAsyncTests.TokenReplay.cs | 163 ++++++++++++++++++ .../Validation/ReplayValidationResultTests.cs | 2 +- 4 files changed, 169 insertions(+), 23 deletions(-) create mode 100644 test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.ValidateTokenAsyncTests.TokenReplay.cs diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/Validators.TokenReplay.cs b/src/Microsoft.IdentityModel.Tokens/Validation/Validators.TokenReplay.cs index 28d3a387c2..064a6cc491 100644 --- a/src/Microsoft.IdentityModel.Tokens/Validation/Validators.TokenReplay.cs +++ b/src/Microsoft.IdentityModel.Tokens/Validation/Validators.TokenReplay.cs @@ -3,7 +3,6 @@ using System; using System.Diagnostics; -using Microsoft.IdentityModel.Logging; namespace Microsoft.IdentityModel.Tokens { @@ -60,16 +59,16 @@ public static partial class Validators return new ValidationError( new MessageDetail( LogMessages.IDX10227, - LogHelper.MarkAsUnsafeSecurityArtifact(securityToken, t => t.ToString())), + securityToken), ValidationFailureType.TokenReplayValidationFailed, - typeof(SecurityTokenReplayDetectedException), + typeof(SecurityTokenNoExpirationException), new StackFrame(true)); if (validationParameters.TokenReplayCache.TryFind(securityToken)) return new ValidationError( new MessageDetail( LogMessages.IDX10228, - LogHelper.MarkAsUnsafeSecurityArtifact(securityToken, t => t.ToString())), + securityToken), ValidationFailureType.TokenReplayValidationFailed, typeof(SecurityTokenReplayDetectedException), new StackFrame(true)); @@ -78,7 +77,7 @@ public static partial class Validators return new ValidationError( new MessageDetail( LogMessages.IDX10229, - LogHelper.MarkAsUnsafeSecurityArtifact(securityToken, t => t.ToString())), + securityToken), ValidationFailureType.TokenReplayValidationFailed, typeof(SecurityTokenReplayAddFailedException), new StackFrame(true)); diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.ValidateTokenAsyncTests.Lifetime.Extensibility.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.ValidateTokenAsyncTests.Lifetime.Extensibility.cs index dffbcebe58..b7c09dab2f 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.ValidateTokenAsyncTests.Lifetime.Extensibility.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.ValidateTokenAsyncTests.Lifetime.Extensibility.cs @@ -19,7 +19,7 @@ public async Task ValidateTokenAsync_Lifetime_Extensibility(ValidateTokenAsyncLi { var context = TestUtilities.WriteHeader($"{this}.{nameof(ValidateTokenAsync_Lifetime_Extensibility)}", theoryData); - string jwtString = CreateToken(theoryData.IssuedAt, theoryData.NotBefore, theoryData.Expires); + string jwtString = CreateTokenWithLifetime(theoryData.IssuedAt, theoryData.NotBefore, theoryData.Expires); var handler = new JsonWebTokenHandler(); ValidationResult validationResult; @@ -300,22 +300,6 @@ internal override Exception GetException() return base.GetException(); } } - - private static string CreateToken(DateTime? issuedAt, DateTime? notBefore, DateTime? expires) - { - JsonWebTokenHandler jsonWebTokenHandler = new JsonWebTokenHandler(); - jsonWebTokenHandler.SetDefaultTimesOnTokenCreation = false; // Allow for null values to be passed in to validate. - - SecurityTokenDescriptor securityTokenDescriptor = new SecurityTokenDescriptor - { - Subject = Default.ClaimsIdentity, - IssuedAt = issuedAt, - NotBefore = notBefore, - Expires = expires, - }; - - return jsonWebTokenHandler.CreateToken(securityTokenDescriptor); - } } } #nullable restore diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.ValidateTokenAsyncTests.TokenReplay.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.ValidateTokenAsyncTests.TokenReplay.cs new file mode 100644 index 0000000000..70332b4f55 --- /dev/null +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.ValidateTokenAsyncTests.TokenReplay.cs @@ -0,0 +1,163 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#nullable enable +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_TokenReplayTestCases), DisableDiscoveryEnumeration = true)] + public async Task ValidateTokenAsync_TokenReplay(ValidateTokenAsyncTokenReplayTheoryData theoryData) + { + var context = TestUtilities.WriteHeader($"{this}.ValidateTokenAsync_TokenReplay", theoryData); + + string jwtString = CreateTokenForTokenReplayValidation(theoryData.TokenHasExpiration); + + await ValidateAndCompareResults(jwtString, theoryData, context); + + TestUtilities.AssertFailIfErrors(context); + } + + public static TheoryData ValidateTokenAsync_TokenReplayTestCases + { + get + { + var successfulTokenReplayCache = new TokenReplayCache + { + OnAddReturnValue = true, + OnFindReturnValue = false, + }; + + var failToAddTokenReplayCache = new TokenReplayCache + { + OnAddReturnValue = false, + OnFindReturnValue = false, + }; + + var tokenAlreadySavedTokenReplayCache = new TokenReplayCache + { + OnAddReturnValue = true, + OnFindReturnValue = true, + }; + + var theoryData = new TheoryData(); + + theoryData.Add(new ValidateTokenAsyncTokenReplayTheoryData("Valid_TokenHasNotBeenReplayed") + { + TokenValidationParameters = CreateTokenValidationParameters(successfulTokenReplayCache), + ValidationParameters = CreateValidationParameters(successfulTokenReplayCache), + }); + + theoryData.Add(new ValidateTokenAsyncTokenReplayTheoryData("Valid_TokenHasNoExpiration_TokenReplayCacheIsNull") + { + TokenHasExpiration = false, + TokenValidationParameters = CreateTokenValidationParameters(null), + ValidationParameters = CreateValidationParameters(null), + }); + + theoryData.Add(new ValidateTokenAsyncTokenReplayTheoryData("Invalid_TokenHasNoExpiration_TokenReplayCacheIsNotNull") + { + TokenHasExpiration = false, + TokenValidationParameters = CreateTokenValidationParameters(successfulTokenReplayCache), + ValidationParameters = CreateValidationParameters(successfulTokenReplayCache), + ExpectedIsValid = false, + ExpectedException = ExpectedException.SecurityTokenNoExpirationException("IDX10227:"), + }); + + theoryData.Add(new ValidateTokenAsyncTokenReplayTheoryData("Invalid_TokenCouldNotBeAdded") + { + TokenValidationParameters = CreateTokenValidationParameters(failToAddTokenReplayCache), + ValidationParameters = CreateValidationParameters(failToAddTokenReplayCache), + ExpectedIsValid = false, + ExpectedException = ExpectedException.SecurityTokenReplayAddFailedException("IDX10229:"), + }); + + theoryData.Add(new ValidateTokenAsyncTokenReplayTheoryData("Invalid_TokenHasBeenReplayed") + { + TokenValidationParameters = CreateTokenValidationParameters(tokenAlreadySavedTokenReplayCache), + ValidationParameters = CreateValidationParameters(tokenAlreadySavedTokenReplayCache), + ExpectedIsValid = false, + ExpectedException = ExpectedException.SecurityTokenReplayDetectedException("IDX10228:"), + }); + + return theoryData; + + static TokenValidationParameters CreateTokenValidationParameters(ITokenReplayCache? tokenReplayCache) + { + // only validate that the token has not been replayed + var tokenValidationParameters = new TokenValidationParameters + { + ValidateAudience = false, + ValidateIssuer = false, + ValidateLifetime = false, + ValidateTokenReplay = true, + ValidateIssuerSigningKey = false, + RequireSignedTokens = false, + TokenReplayCache = tokenReplayCache + }; + + return tokenValidationParameters; + } + + static ValidationParameters CreateValidationParameters(ITokenReplayCache? tokenReplayCache) + { + ValidationParameters validationParameters = new ValidationParameters(); + validationParameters.TokenReplayCache = tokenReplayCache; + + // Skip all validations except token replay + validationParameters.AlgorithmValidator = SkipValidationDelegates.SkipAlgorithmValidation; + validationParameters.AudienceValidator = SkipValidationDelegates.SkipAudienceValidation; + validationParameters.IssuerSigningKeyValidator = SkipValidationDelegates.SkipIssuerSigningKeyValidation; + validationParameters.IssuerValidatorAsync = SkipValidationDelegates.SkipIssuerValidation; + validationParameters.LifetimeValidator = SkipValidationDelegates.SkipLifetimeValidation; + validationParameters.SignatureValidator = SkipValidationDelegates.SkipSignatureValidation; + validationParameters.TokenTypeValidator = SkipValidationDelegates.SkipTokenTypeValidation; + + return validationParameters; + } + } + } + + public class ValidateTokenAsyncTokenReplayTheoryData : ValidateTokenAsyncBaseTheoryData + { + public ValidateTokenAsyncTokenReplayTheoryData(string testId) : base(testId) { } + + public bool TokenHasExpiration { get; set; } = true; + } + + private static string CreateTokenForTokenReplayValidation(bool hasExpiration = true) + { + JsonWebTokenHandler jsonWebTokenHandler = new JsonWebTokenHandler(); + // If the token has expiration, we use the default times. + jsonWebTokenHandler.SetDefaultTimesOnTokenCreation = hasExpiration; + + SecurityTokenDescriptor securityTokenDescriptor; + + if (!hasExpiration) + { + securityTokenDescriptor = new SecurityTokenDescriptor + { + Subject = Default.ClaimsIdentity, + Expires = null, + NotBefore = null, + IssuedAt = null, + }; + } + else + { + securityTokenDescriptor = new SecurityTokenDescriptor + { + Subject = Default.ClaimsIdentity, + }; + } + + return jsonWebTokenHandler.CreateToken(securityTokenDescriptor); + } + } +} +#nullable restore diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/Validation/ReplayValidationResultTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/Validation/ReplayValidationResultTests.cs index b0dccb7da8..5759e055eb 100644 --- a/test/Microsoft.IdentityModel.Tokens.Tests/Validation/ReplayValidationResultTests.cs +++ b/test/Microsoft.IdentityModel.Tokens.Tests/Validation/ReplayValidationResultTests.cs @@ -126,7 +126,7 @@ public static TheoryData TokenReplayValidationTestCases new TokenReplayTheoryData { TestId = "Invalid_ReplayCacheIsPresent_ExpirationTimeIsNull", - ExpectedException = ExpectedException.SecurityTokenReplayDetected("IDX10227:"), + ExpectedException = ExpectedException.SecurityTokenNoExpirationException("IDX10227:"), ExpirationTime = null, SecurityToken = "token", ValidationParameters = new ValidationParameters