From 1ce98a64979f04886a0d9bc7daeda05e93d7500c Mon Sep 17 00:00:00 2001 From: Alexandre Murari Junior Date: Sun, 28 Apr 2024 19:17:17 -0300 Subject: [PATCH 1/2] Add TimeProvider support to token validation The TimeProvider class abstracts time, facilitating deterministic tests by removing reliance on ambient context for obtaining the current time. If not set, validators will fall back to using the DateTime class to obtain the current time. --- build/common.props | 1 + build/dependencies.props | 1 + .../Microsoft.IdentityModel.Tokens.csproj | 8 ++++++++ .../TokenValidationParameters.cs | 10 +++++++++- .../ValidatorUtilities.cs | 2 +- 5 files changed, 20 insertions(+), 2 deletions(-) diff --git a/build/common.props b/build/common.props index 8e47d50a59..61b8082815 100644 --- a/build/common.props +++ b/build/common.props @@ -55,6 +55,7 @@ + true $(NoWarn);SYSLIB0050 $(NoWarn);SYSLIB0051 diff --git a/build/dependencies.props b/build/dependencies.props index 23e1b91065..50a52bc42b 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -4,6 +4,7 @@ 2.1.1 3.0.5 1.0.3 + 8.0.1 4.5.0 1.0.0 2.0.3 diff --git a/src/Microsoft.IdentityModel.Tokens/Microsoft.IdentityModel.Tokens.csproj b/src/Microsoft.IdentityModel.Tokens/Microsoft.IdentityModel.Tokens.csproj index 3536e2f1b0..cff134141b 100644 --- a/src/Microsoft.IdentityModel.Tokens/Microsoft.IdentityModel.Tokens.csproj +++ b/src/Microsoft.IdentityModel.Tokens/Microsoft.IdentityModel.Tokens.csproj @@ -23,6 +23,10 @@ $(DefineConstants);HAVE_ADO_NET;HAVE_APP_DOMAIN;HAVE_ASYNC;HAVE_ASYNC_DISPOSABLE;HAVE_BIG_INTEGER;HAVE_BINARY_FORMATTER;HAVE_BINARY_SERIALIZATION;HAVE_BINARY_EXCEPTION_SERIALIZATION;HAVE_CHAR_TO_LOWER_WITH_CULTURE;HAVE_CHAR_TO_STRING_WITH_CULTURE;HAVE_COM_ATTRIBUTES;HAVE_COMPONENT_MODEL;HAVE_CONCURRENT_COLLECTIONS;HAVE_COVARIANT_GENERICS;HAVE_DATA_CONTRACTS;HAVE_DATE_TIME_OFFSET;HAVE_DB_NULL_TYPE_CODE;HAVE_DYNAMIC;HAVE_EMPTY_TYPES;HAVE_ENTITY_FRAMEWORK;HAVE_EXPRESSIONS;HAVE_FAST_REVERSE;HAVE_FSHARP_TYPES;HAVE_FULL_REFLECTION;HAVE_GUID_TRY_PARSE;HAVE_HASH_SET;HAVE_ICLONEABLE;HAVE_ICONVERTIBLE;HAVE_IGNORE_DATA_MEMBER_ATTRIBUTE;HAVE_INOTIFY_COLLECTION_CHANGED;HAVE_INOTIFY_PROPERTY_CHANGING;HAVE_ISET;HAVE_LINQ;HAVE_MEMORY_BARRIER;HAVE_METHOD_IMPL_ATTRIBUTE;HAVE_NON_SERIALIZED_ATTRIBUTE;HAVE_READ_ONLY_COLLECTIONS;HAVE_REFLECTION_EMIT;HAVE_REGEX_TIMEOUTS;HAVE_SECURITY_SAFE_CRITICAL_ATTRIBUTE;HAVE_SERIALIZATION_BINDER_BIND_TO_NAME;HAVE_STREAM_READER_WRITER_CLOSE;HAVE_STRING_JOIN_WITH_ENUMERABLE;HAVE_TIME_SPAN_PARSE_WITH_CULTURE;HAVE_TIME_SPAN_TO_STRING_WITH_CULTURE;HAVE_TIME_ZONE_INFO;HAVE_TRACE_WRITER;HAVE_TYPE_DESCRIPTOR;HAVE_UNICODE_SURROGATE_DETECTION;HAVE_VARIANT_TYPE_PARAMETERS;HAVE_VERSION_TRY_PARSE;HAVE_XLINQ;HAVE_XML_DOCUMENT;HAVE_XML_DOCUMENT_TYPE;HAVE_CONCURRENT_DICTIONARY;HAVE_INDEXOF_STRING_COMPARISON;HAVE_REPLACE_STRING_COMPARISON;HAVE_REPLACE_STRING_COMPARISON;HAVE_GETHASHCODE_STRING_COMPARISON;HAVE_NULLABLE_ATTRIBUTES;HAVE_DYNAMIC_CODE_COMPILED;HAS_ARRAY_EMPTY;HAVE_DATE_ONLY;$(AdditionalConstants) + + |net461|net462|net472|netstandard2.0|net6.0| + + full true @@ -50,6 +54,10 @@ + + + + all diff --git a/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs b/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs index f110a31e45..de13a5ec8b 100644 --- a/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs +++ b/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs @@ -253,6 +253,7 @@ protected TokenValidationParameters(TokenValidationParameters other) SaveSigninToken = other.SaveSigninToken; SignatureValidator = other.SignatureValidator; SignatureValidatorUsingConfiguration = other.SignatureValidatorUsingConfiguration; + TimeProvider = other.TimeProvider; TokenDecryptionKey = other.TokenDecryptionKey; TokenDecryptionKeyResolver = other.TokenDecryptionKeyResolver; TokenDecryptionKeys = other.TokenDecryptionKeys; @@ -381,6 +382,14 @@ public TimeSpan ClockSkew } } + /// + /// Gets or sets the time provider used for time validation. + /// + /// + /// If not set, validators will fall back to using the class to obtain the current time. + /// + public TimeProvider TimeProvider { get; set; } + /// /// Returns a new instance of with values copied from this object. /// @@ -539,7 +548,6 @@ public virtual ClaimsIdentity CreateClaimsIdentity(SecurityToken securityToken, /// public IssuerValidator IssuerValidator { get; set; } - /// /// Gets or sets a delegate that will be used to validate the issuer of the token. /// diff --git a/src/Microsoft.IdentityModel.Tokens/ValidatorUtilities.cs b/src/Microsoft.IdentityModel.Tokens/ValidatorUtilities.cs index 482f2f3d37..df8f964329 100644 --- a/src/Microsoft.IdentityModel.Tokens/ValidatorUtilities.cs +++ b/src/Microsoft.IdentityModel.Tokens/ValidatorUtilities.cs @@ -35,7 +35,7 @@ internal static void ValidateLifetime(DateTime? notBefore, DateTime? expires, Se Expires = expires }); - DateTime utcNow = DateTime.UtcNow; + DateTime utcNow = validationParameters.TimeProvider?.GetUtcNow().UtcDateTime ?? DateTime.UtcNow; 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))) { From 7b29662339c69bb8b6f711e1b635cac85d9e152e Mon Sep 17 00:00:00 2001 From: Alexandre Murari Junior Date: Sun, 28 Apr 2024 20:17:27 -0300 Subject: [PATCH 2/2] Add TimeProvider tests --- build/commonTest.props | 2 ++ build/dependenciesTest.props | 1 + .../Microsoft.IdentityModel.Tokens.Tests.csproj | 4 ++++ .../ValidatorsTests.cs | 14 ++++++++++++++ 4 files changed, 21 insertions(+) diff --git a/build/commonTest.props b/build/commonTest.props index ff56d0f461..f501f0b903 100644 --- a/build/commonTest.props +++ b/build/commonTest.props @@ -25,8 +25,10 @@ + true $(NoWarn);SYSLIB0050 $(NoWarn);SYSLIB0051 + $(NoWarn);NU1701 diff --git a/build/dependenciesTest.props b/build/dependenciesTest.props index ccb413a36d..0f3528489b 100644 --- a/build/dependenciesTest.props +++ b/build/dependenciesTest.props @@ -2,6 +2,7 @@ 2.1.30 2.0.5 + 8.4.0 16.10.0 2.0.3 13.0.3 diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/Microsoft.IdentityModel.Tokens.Tests.csproj b/test/Microsoft.IdentityModel.Tokens.Tests/Microsoft.IdentityModel.Tokens.Tests.csproj index 571c3cdde6..dba1fe24cb 100644 --- a/test/Microsoft.IdentityModel.Tokens.Tests/Microsoft.IdentityModel.Tokens.Tests.csproj +++ b/test/Microsoft.IdentityModel.Tokens.Tests/Microsoft.IdentityModel.Tokens.Tests.csproj @@ -24,6 +24,10 @@ + + + + diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/ValidatorsTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/ValidatorsTests.cs index d7af19095e..862bd76100 100644 --- a/test/Microsoft.IdentityModel.Tokens.Tests/ValidatorsTests.cs +++ b/test/Microsoft.IdentityModel.Tokens.Tests/ValidatorsTests.cs @@ -4,6 +4,9 @@ using System; using System.Collections.Generic; using System.IdentityModel.Tokens.Jwt; +#if NET462_OR_GREATER || NETCOREAPP2_1_OR_GREATER +using Microsoft.Extensions.Time.Testing; +#endif using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.TestUtils; using Xunit; @@ -366,6 +369,17 @@ public void Lifetime(DateTime? notBefore, DateTime? expires, SecurityToken secur dataset.Add(DateTime.UtcNow + TimeSpan.FromMinutes(2), DateTime.UtcNow + TimeSpan.FromHours(1), null, new TokenValidationParameters{ ClockSkew = TimeSpan.FromMinutes(5) }, ExpectedException.NoExceptionExpected); dataset.Add(DateTime.UtcNow - TimeSpan.FromMinutes(2), DateTime.UtcNow - TimeSpan.FromMinutes(1), null, new TokenValidationParameters{ ClockSkew = TimeSpan.FromMinutes(5) }, ExpectedException.NoExceptionExpected); + // With TimeProvider (default) + dataset.Add(DateTime.UtcNow - TimeSpan.FromHours(2), DateTime.UtcNow - TimeSpan.FromHours(1), null, new TokenValidationParameters { TimeProvider = TimeProvider.System }, ExpectedException.SecurityTokenExpiredException("IDX10223:")); + dataset.Add(DateTime.UtcNow - TimeSpan.FromHours(2), DateTime.UtcNow + TimeSpan.FromHours(1), null, new TokenValidationParameters { TimeProvider = TimeProvider.System }, ExpectedException.NoExceptionExpected); + +#if NET462_OR_GREATER || NETCOREAPP2_1_OR_GREATER + // With TimeProvider (fake) + DateTime fakeUtcTime = DateTime.UtcNow.AddYears(-1); + dataset.Add(fakeUtcTime - TimeSpan.FromHours(2), fakeUtcTime - TimeSpan.FromHours(1), null, new TokenValidationParameters { TimeProvider = new FakeTimeProvider(fakeUtcTime) }, ExpectedException.SecurityTokenExpiredException("IDX10223:")); + dataset.Add(fakeUtcTime - TimeSpan.FromHours(2), fakeUtcTime + TimeSpan.FromHours(1), null, new TokenValidationParameters { TimeProvider = new FakeTimeProvider(fakeUtcTime) }, ExpectedException.NoExceptionExpected); +#endif + notBefore = EpochTime.DateTime(EpochTime.GetIntDate((DateTime.UtcNow + TimeSpan.FromMinutes(6)).ToUniversalTime())); expires = EpochTime.DateTime(EpochTime.GetIntDate((DateTime.UtcNow + TimeSpan.FromHours(1)).ToUniversalTime())); dataset.Add(notBefore, expires, null, new TokenValidationParameters{ ClockSkew = TimeSpan.FromMinutes(5) }, ExpectedException.SecurityTokenNotYetValidException("IDX10222:", propertiesExpected: new Dictionary { { "NotBefore", notBefore } }));