-
Notifications
You must be signed in to change notification settings - Fork 413
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add TimeProvider support to token validation #2573
Add TimeProvider support to token validation #2573
Conversation
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.
@microsoft-github-policy-service agree |
@@ -23,6 +23,10 @@ | |||
<DefineConstants>$(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)</DefineConstants> | |||
</PropertyGroup> | |||
|
|||
<PropertyGroup> | |||
<FrameworksWithoutTimeProvider>|net461|net462|net472|netstandard2.0|net6.0|</FrameworksWithoutTimeProvider> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TimeProvider
is included by default in .NET 8 onwards. For every other target, we must eat the Microsoft.Bcl.TimeProvider
nuget.
@@ -24,6 +24,10 @@ | |||
<PackageReference Include="Microsoft.Azure.KeyVault.Cryptography" Version="$(MicrosoftAzureKeyVaultCryptographyVersion)" /> | |||
</ItemGroup> | |||
|
|||
<ItemGroup Condition="'$(TargetFramework)' != 'net461'"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Every test target, except .NET FWK 4.6.1 supports the Microsoft.Extensions.TimeProvider.Testing
.
@@ -55,6 +55,7 @@ | |||
</PropertyGroup> | |||
|
|||
<PropertyGroup> | |||
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suppress "Microsoft.Bcl.TimeProvider doesn't support .NET Framework 4.6.1, consider upgrading [...]" warnings. (Altough it's compatible with that framework version).
@@ -25,8 +25,10 @@ | |||
</PropertyGroup> | |||
|
|||
<PropertyGroup> | |||
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same.
<NoWarn>$(NoWarn);SYSLIB0050</NoWarn> | ||
<NoWarn>$(NoWarn);SYSLIB0051</NoWarn> | ||
<NoWarn>$(NoWarn);NU1701</NoWarn> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suppresses the "This package may not be fully compatible with your project" warning. The cause is that Microsoft.Extensions.TimeProvider.Testing
package is not compatible with .NET FWK 4.6.1.
@alexmurari Thanks for the proposal. After team discussion, there is more design and work needed here. (Our PR process is described in the contributing guide, making sure a design is discussed before time is spent making changes.)
Proposal could be:
|
@pmaytak Thanks for the review and the modified proposal. I agree to those points. Client code would still need to provide a a. (Constructor/Method injection) Every class/method that needs a - OR - b. (Property-injection) We create an options class (e.g. public class TimeProviderOptions
{
private TimeProvider _timeProvider = TimeProvider.System; // Local default
public TimeProvider TimeProvider
{
get
{
return _timeProvider;
}
set
{
if (value == null)
throw new ArgumentNullException("value");
_timeProvider = value;
}
}
} We just need somewhere to set an instance of this options class, then the What do you think? |
We used a similar path with TimeProvider in OpenIddict: One of the constraints was also not to use the Bcl package. So there is |
This way, the PR becomes simple and minimalistic. I am willing to prepare PR using this approach. |
A workaround can look like this: private const string TimeProviderName = "TimeProviderName";
/// <summary>
/// Validates the lifetime of a <see cref="SecurityToken"/>.
/// </summary>
internal static bool ValidateLifetime(DateTime? notBefore, DateTime? expires, SecurityToken securityToken, TokenValidationParameters validationParameters)
{
if (!expires.HasValue && validationParameters.RequireExpirationTime)
{
throw LogHelper.LogExceptionMessage(new SecurityTokenNoExpirationException(
LogHelper.FormatInvariant("IDX10225", LogHelper.MarkAsNonPII(securityToken.GetType().ToString()))
));
}
if (notBefore.HasValue && expires.HasValue && notBefore.Value > expires.Value)
{
throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidLifetimeException(
LogHelper.FormatInvariant("IDX10224", LogHelper.MarkAsNonPII(notBefore.Value), LogHelper.MarkAsNonPII(expires.Value))
)
{
NotBefore = notBefore,
Expires = expires,
});
}
var timeProvider = (TimeProvider) validationParameters.InstancePropertyBag[TimeProviderName];
var utcNow = timeProvider.GetUtcNow().UtcDateTime;
if (notBefore.HasValue && notBefore.Value > DateTimeUtil.Add(utcNow, validationParameters.ClockSkew))
{
throw LogHelper.LogExceptionMessage(new SecurityTokenNotYetValidException(
LogHelper.FormatInvariant("IDX10222", LogHelper.MarkAsNonPII(notBefore.Value), LogHelper.MarkAsNonPII(utcNow))
)
{
NotBefore = notBefore.Value,
});
}
if (expires.HasValue && expires.Value < DateTimeUtil.Add(utcNow, validationParameters.ClockSkew.Negate()))
{
throw LogHelper.LogExceptionMessage(new SecurityTokenExpiredException(
LogHelper.FormatInvariant("IDX10223", LogHelper.MarkAsNonPII(expires.Value), LogHelper.MarkAsNonPII(utcNow))
)
{
Expires = expires.Value,
});
}
// if it reaches here, that means lifetime of the token is valid
LogHelper.LogInformation("IDX10239");
return true;
}
var timeProvider = TimeProvider.System;
var validationParameters = new TokenValidationParameters
{
LifetimeValidator = ValidateLifetime,
}
validationParameters.InstancePropertyBag.Add(TimeProviderName, timeProvider); |
@trejjam That's a great approach! The changes to OpenIddict were minimal. Fine by me a new PR with this approach. I'm just not sure where client code will configure the desired |
Hi my PR is here: #2612 |
Closing in favor of #2612 |
Add TimeProvider support to token validation
TimeProvider abstracts time and enables deterministic tests.
Description
Added a new parameter to the
TokenValidationParameters
class to hold theTimeProvider
instance.The default behavior's still the use of
DateTime
class to obtain the current time.Fixes #2572