Add TimeProvider support to token validation#2573
Add TimeProvider support to token validation#2573alexmurari wants to merge 2 commits intoAzureAD:devfrom alexmurari:feature/support-timeprovider-token-validation
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 |
| </PropertyGroup> | ||
|
|
||
| <PropertyGroup> | ||
| <FrameworksWithoutTimeProvider>|net461|net462|net472|netstandard2.0|net6.0|</FrameworksWithoutTimeProvider> |
There was a problem hiding this comment.
TimeProvider is included by default in .NET 8 onwards. For every other target, we must eat the Microsoft.Bcl.TimeProvider nuget.
| <PackageReference Include="Microsoft.Azure.KeyVault.Cryptography" Version="$(MicrosoftAzureKeyVaultCryptographyVersion)" /> | ||
| </ItemGroup> | ||
|
|
||
| <ItemGroup Condition="'$(TargetFramework)' != 'net461'"> |
There was a problem hiding this comment.
Every test target, except .NET FWK 4.6.1 supports the Microsoft.Extensions.TimeProvider.Testing.
| </PropertyGroup> | ||
|
|
||
| <PropertyGroup> | ||
| <SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings> |
There was a problem hiding this comment.
Suppress "Microsoft.Bcl.TimeProvider doesn't support .NET Framework 4.6.1, consider upgrading [...]" warnings. (Altough it's compatible with that framework version).
| </PropertyGroup> | ||
|
|
||
| <PropertyGroup> | ||
| <SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings> |
| <SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings> | ||
| <NoWarn>$(NoWarn);SYSLIB0050</NoWarn> | ||
| <NoWarn>$(NoWarn);SYSLIB0051</NoWarn> | ||
| <NoWarn>$(NoWarn);NU1701</NoWarn> |
There was a problem hiding this comment.
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
TokenValidationParametersclass to hold theTimeProviderinstance.The default behavior's still the use of
DateTimeclass to obtain the current time.Fixes #2572