diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.Internal.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.Internal.cs index 222ae2b314..f1f4ccd521 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.Internal.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.Internal.cs @@ -341,7 +341,7 @@ await ValidateJWSAsync(actorToken, actorParameters, configuration, callContext, ValidationResult issuerSigningKeyValidationResult = validationParameters.IssuerSigningKeyValidator( - signatureValidationResult.UnwrapResult(), jsonWebToken, validationParameters, configuration, callContext); + jsonWebToken.SigningKey, jsonWebToken, validationParameters, configuration, callContext); if (!issuerSigningKeyValidationResult.IsValid) { StackFrame issuerSigningKeyValidationFailureStackFrame = StackFrames.IssuerSigningKeyValidationFailed ??= new StackFrame(true); diff --git a/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt index dc08ab0a3b..c656cb5fc9 100644 --- a/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt @@ -6,6 +6,8 @@ Microsoft.IdentityModel.Tokens.IssuerValidationSource.IssuerMatchedConfiguration Microsoft.IdentityModel.Tokens.IssuerValidationSource.IssuerMatchedValidationParameters = 2 -> Microsoft.IdentityModel.Tokens.IssuerValidationSource Microsoft.IdentityModel.Tokens.LifetimeValidationError._expires -> System.DateTime Microsoft.IdentityModel.Tokens.LifetimeValidationError._notBefore -> System.DateTime +Microsoft.IdentityModel.Tokens.TokenValidationParameters.TimeProvider.get -> System.TimeProvider +Microsoft.IdentityModel.Tokens.TokenValidationParameters.TimeProvider.set -> void Microsoft.IdentityModel.Tokens.ValidationError.GetException(System.Type exceptionType, System.Exception innerException) -> System.Exception Microsoft.IdentityModel.Tokens.ValidationResult.Error.get -> Microsoft.IdentityModel.Tokens.ValidationError Microsoft.IdentityModel.Tokens.ValidationResult.IsValid.get -> bool diff --git a/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs b/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs index ab93e8f58a..7ea251d8dc 100644 --- a/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs +++ b/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs @@ -540,6 +540,11 @@ public string RoleClaimType /// public SignatureValidatorUsingConfiguration SignatureValidatorUsingConfiguration { get; set; } + /// + /// Gets or sets the time provider. + /// + internal TimeProvider TimeProvider { get; set; } = TimeProvider.System; + /// /// Gets or sets the that is to be used for decryption. /// diff --git a/src/Microsoft.IdentityModel.Tokens/ValidatorUtilities.cs b/src/Microsoft.IdentityModel.Tokens/ValidatorUtilities.cs index 6945912645..5aae796ee7 100644 --- a/src/Microsoft.IdentityModel.Tokens/ValidatorUtilities.cs +++ b/src/Microsoft.IdentityModel.Tokens/ValidatorUtilities.cs @@ -36,7 +36,7 @@ internal static void ValidateLifetime(DateTime? notBefore, DateTime? expires, Se Expires = expires }); - DateTime utcNow = DateTime.UtcNow; + DateTime utcNow = validationParameters.TimeProvider.GetUtcNow().UtcDateTime; if (notBefore.HasValue && (notBefore.Value > DateTimeUtil.Add(utcNow, validationParameters.ClockSkew))) throw LogHelper.LogExceptionMessage(new SecurityTokenNotYetValidException(LogHelper.FormatInvariant(LogMessages.IDX10222, LogHelper.MarkAsNonPII(notBefore.Value), LogHelper.MarkAsNonPII(utcNow))) { diff --git a/src/Microsoft.IdentityModel.Tokens/Validators.cs b/src/Microsoft.IdentityModel.Tokens/Validators.cs index 6002eded77..157588bd51 100644 --- a/src/Microsoft.IdentityModel.Tokens/Validators.cs +++ b/src/Microsoft.IdentityModel.Tokens/Validators.cs @@ -404,7 +404,7 @@ internal static void ValidateIssuerSigningKeyLifeTime(SecurityKey securityKey, T X509SecurityKey x509SecurityKey = securityKey as X509SecurityKey; if (x509SecurityKey?.Certificate is X509Certificate2 cert) { - DateTime utcNow = DateTime.UtcNow; + DateTime utcNow = validationParameters.TimeProvider.GetUtcNow().UtcDateTime; var notBeforeUtc = cert.NotBefore.ToUniversalTime(); var notAfterUtc = cert.NotAfter.ToUniversalTime(); diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.ValidateTokenAsyncTests.IssuerSigningKey.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.ValidateTokenAsyncTests.IssuerSigningKey.cs new file mode 100644 index 0000000000..0c7f6ddf53 --- /dev/null +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.ValidateTokenAsyncTests.IssuerSigningKey.cs @@ -0,0 +1,140 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#nullable enable +using System; +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_IssuerSigningKeyTestCases), DisableDiscoveryEnumeration = true)] + public async Task ValidateTokenAsync_IssuerSigningKey(ValidateTokenAsyncIssuerSigningKeyTheoryData theoryData) + { + var context = TestUtilities.WriteHeader($"{this}.ValidateTokenAsync_IssuerSigningKey", theoryData); + + string jwtString = CreateTokenWithSigningCredentials(theoryData.SigningCredentials); + + await ValidateAndCompareResults(jwtString, theoryData, context); + + TestUtilities.AssertFailIfErrors(context); + } + + public static TheoryData ValidateTokenAsync_IssuerSigningKeyTestCases + { + get + { + int currentYear = DateTime.UtcNow.Year; + // Mock time provider, 100 years in the future + TimeProvider futureTimeProvider = new MockTimeProvider(new DateTimeOffset(currentYear + 100, 1, 1, 0, 0, 0, new(0))); + // Mock time provider, 100 years in the past + TimeProvider pastTimeProvider = new MockTimeProvider(new DateTimeOffset(currentYear - 100, 9, 16, 0, 0, 0, new(0))); + + return new TheoryData + { + new ValidateTokenAsyncIssuerSigningKeyTheoryData("Valid_IssuerSigningKeyIsValid") + { + SigningCredentials = KeyingMaterial.DefaultX509SigningCreds_2048_RsaSha2_Sha2, + TokenValidationParameters = CreateTokenValidationParameters(KeyingMaterial.DefaultX509SigningCreds_2048_RsaSha2_Sha2.Key), + ValidationParameters = CreateValidationParameters(KeyingMaterial.DefaultX509SigningCreds_2048_RsaSha2_Sha2.Key), + }, + new ValidateTokenAsyncIssuerSigningKeyTheoryData("Invalid_IssuerSigningKeyIsExpired") + { + // Signing key is valid between September 2011 and December 2039 + // Mock time provider is set to 100 years in the future, after the key expired + SigningCredentials = KeyingMaterial.DefaultX509SigningCreds_2048_RsaSha2_Sha2, + TokenValidationParameters = CreateTokenValidationParameters( + KeyingMaterial.DefaultX509SigningCreds_2048_RsaSha2_Sha2.Key, futureTimeProvider), + ValidationParameters = CreateValidationParameters( + KeyingMaterial.DefaultX509SigningCreds_2048_RsaSha2_Sha2.Key, futureTimeProvider), + ExpectedIsValid = false, + ExpectedException = ExpectedException.SecurityTokenInvalidSigningKeyException("IDX10249:"), + }, + new ValidateTokenAsyncIssuerSigningKeyTheoryData("Invalid_IssuerSigningKeyNotYetValid") + { + // Signing key is valid between September 2011 and December 2039 + // Mock time provider is set to 100 years in the past, before the key was valid. + SigningCredentials = KeyingMaterial.DefaultX509SigningCreds_2048_RsaSha2_Sha2, + TokenValidationParameters = CreateTokenValidationParameters( + KeyingMaterial.DefaultX509SigningCreds_2048_RsaSha2_Sha2.Key, pastTimeProvider), + ValidationParameters = CreateValidationParameters( + KeyingMaterial.DefaultX509SigningCreds_2048_RsaSha2_Sha2.Key, pastTimeProvider), + ExpectedIsValid = false, + ExpectedException = ExpectedException.SecurityTokenInvalidSigningKeyException("IDX10248:"), + }, + }; + + static TokenValidationParameters CreateTokenValidationParameters( + SecurityKey? signingKey = null, TimeProvider? timeProvider = null) + { + // only validate the signature and issuer signing key + var tokenValidationParameters = new TokenValidationParameters + { + ValidateAudience = false, + ValidateIssuer = false, + ValidateLifetime = false, + ValidateTokenReplay = false, + ValidateIssuerSigningKey = true, + RequireSignedTokens = true, + IssuerSigningKey = signingKey, + }; + + if (timeProvider is not null) + tokenValidationParameters.TimeProvider = timeProvider; + + return tokenValidationParameters; + } + + static ValidationParameters CreateValidationParameters( + SecurityKey? signingKey = null, TimeProvider? timeProvider = null) + { + ValidationParameters validationParameters = new ValidationParameters(); + + if (signingKey is not null) + validationParameters.IssuerSigningKeys.Add(signingKey); + + if (timeProvider is not null) + validationParameters.TimeProvider = timeProvider; + + // Skip all validations except signature and issuer signing key + validationParameters.AlgorithmValidator = SkipValidationDelegates.SkipAlgorithmValidation; + validationParameters.AudienceValidator = SkipValidationDelegates.SkipAudienceValidation; + validationParameters.IssuerValidatorAsync = SkipValidationDelegates.SkipIssuerValidation; + validationParameters.LifetimeValidator = SkipValidationDelegates.SkipLifetimeValidation; + validationParameters.TokenReplayValidator = SkipValidationDelegates.SkipTokenReplayValidation; + validationParameters.TypeValidator = SkipValidationDelegates.SkipTokenTypeValidation; + + return validationParameters; + } + } + } + + public class ValidateTokenAsyncIssuerSigningKeyTheoryData : ValidateTokenAsyncBaseTheoryData + { + public ValidateTokenAsyncIssuerSigningKeyTheoryData(string testId) : base(testId) { } + + public SigningCredentials? SigningCredentials { get; set; } + } + + // Tokens must be signed in order to validate the issuer signing key. + // While the ValidationParameters path allows us to test the issuer signing key without a signature, + // the TokenValidationParameters path requires a signature or it will skip the issuer signing key validation. + private static string CreateTokenWithSigningCredentials(SigningCredentials? signingCredentials) + { + JsonWebTokenHandler jsonWebTokenHandler = new JsonWebTokenHandler(); + + SecurityTokenDescriptor securityTokenDescriptor = new SecurityTokenDescriptor + { + Subject = Default.ClaimsIdentity, + SigningCredentials = signingCredentials, + }; + + return jsonWebTokenHandler.CreateToken(securityTokenDescriptor); + } + } +} +#nullable restore diff --git a/test/Microsoft.IdentityModel.TestUtils/MockTimeProvider.cs b/test/Microsoft.IdentityModel.TestUtils/MockTimeProvider.cs index 36f3dcea2a..5a3c28aefb 100644 --- a/test/Microsoft.IdentityModel.TestUtils/MockTimeProvider.cs +++ b/test/Microsoft.IdentityModel.TestUtils/MockTimeProvider.cs @@ -7,7 +7,13 @@ namespace Microsoft.IdentityModel.TestUtils { internal class MockTimeProvider : TimeProvider { - // always return 09/16/2024 00:00:00:00 - public override DateTimeOffset GetUtcNow() => new DateTimeOffset(2024, 9, 16, 0, 0, 0, new(0)); + DateTimeOffset _mockUtcNow; + public MockTimeProvider(DateTimeOffset? mockUtcNow = null) + { + // if left unspecified, it will return 09/16/2024 00:00:00:00 + _mockUtcNow = mockUtcNow ?? new DateTimeOffset(2024, 9, 16, 0, 0, 0, new(0)); + } + + public override DateTimeOffset GetUtcNow() => _mockUtcNow; } } diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/TokenValidationParametersTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/TokenValidationParametersTests.cs index 3f80091899..0728420d51 100644 --- a/test/Microsoft.IdentityModel.Tokens.Tests/TokenValidationParametersTests.cs +++ b/test/Microsoft.IdentityModel.Tokens.Tests/TokenValidationParametersTests.cs @@ -15,7 +15,12 @@ namespace Microsoft.IdentityModel.Tokens.Tests { public class TokenValidationParametersTests { - int ExpectedPropertyCount = 59; + int ExpectedPropertyCount = 60; + + // GetSets() compares the total property count which includes internal properties, against a list of public properties, minus delegates. + // This allows us to keep track of any properties we are including in the total that are not public nor delegates. + // Remove if/once we make TimeProvider public. As the GetSets() test will fail. + List internalNonDelegateProperties = new() { "TimeProvider" }; [Fact] public void Publics() @@ -236,7 +241,7 @@ public void GetSets() }; // check that we have checked all properties, subract the number of delegates. - if (context.PropertyNamesAndSetGetValue.Count != ExpectedPropertyCount - delegates.Count) + if (context.PropertyNamesAndSetGetValue.Count != ExpectedPropertyCount - delegates.Count - internalNonDelegateProperties.Count) compareContext.AddDiff($"Number of properties being set is: {context.PropertyNamesAndSetGetValue.Count}, number of properties is: {properties.Length - delegates.Count} (#Properties - #Delegates), adjust tests"); TestUtilities.GetSet(context);