From 642e9a419a90d507bf8370631699cac3d306626b Mon Sep 17 00:00:00 2001 From: Jan Trejbal Date: Mon, 27 May 2024 11:37:26 +0200 Subject: [PATCH 1/4] Define SUPPORTS_TIME_PROVIDER  Conflicts:  build/common.props --- build/common.props | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/build/common.props b/build/common.props index 5428c3247d..b8c768afe9 100644 --- a/build/common.props +++ b/build/common.props @@ -32,6 +32,9 @@ true + + $(DefineConstants);SUPPORTS_TIME_PROVIDER + false @@ -72,5 +75,5 @@ true - + From 1a8012c3ce8b11550e608dedb37a1e24f5959438 Mon Sep 17 00:00:00 2001 From: Jan Trejbal Date: Mon, 27 May 2024 11:38:25 +0200 Subject: [PATCH 2/4] Add TokenValidationParameters.TimeProvider --- .../TokenValidationParameters.cs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs b/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs index ab93e8f58a..13059a7b76 100644 --- a/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs +++ b/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs @@ -82,6 +82,9 @@ protected TokenValidationParameters(TokenValidationParameters other) SaveSigninToken = other.SaveSigninToken; SignatureValidator = other.SignatureValidator; SignatureValidatorUsingConfiguration = other.SignatureValidatorUsingConfiguration; +#if SUPPORTS_TIME_PROVIDER + TimeProvider = other.TimeProvider; +#endif TokenDecryptionKey = other.TokenDecryptionKey; TokenDecryptionKeyResolver = other.TokenDecryptionKeyResolver; TokenDecryptionKeys = other.TokenDecryptionKeys; @@ -352,7 +355,7 @@ public virtual ClaimsIdentity CreateClaimsIdentity(SecurityToken securityToken, /// This means that no default 'issuer' validation will occur. /// Even if is false, this delegate will still be called. /// If both and are set, IssuerValidatorUsingConfiguration takes - /// priority. + /// priority. /// public IssuerValidator IssuerValidator { get; set; } @@ -540,6 +543,16 @@ public string RoleClaimType /// public SignatureValidatorUsingConfiguration SignatureValidatorUsingConfiguration { get; set; } +#if SUPPORTS_TIME_PROVIDER + /// + /// 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; } +#endif + /// /// Gets or sets the that is to be used for decryption. /// @@ -649,7 +662,7 @@ public string RoleClaimType /// Gets or sets a boolean that controls if validation of the that signed the securityToken is called. /// /// It is possible for tokens to contain the public key needed to check the signature. For example, X509Data can be hydrated into an X509Certificate, - /// which can be used to validate the signature. In these cases it is important to validate the SigningKey that was used to validate the signature. + /// which can be used to validate the signature. In these cases it is important to validate the SigningKey that was used to validate the signature. /// This boolean only applies to default signing key validation. If is set, it will be called regardless of whether this /// property is true or false. /// The default is false. @@ -679,7 +692,7 @@ public string RoleClaimType /// /// Gets or sets a boolean to control if the token replay will be validated during token validation. - /// + /// /// /// This boolean only applies to default token replay validation. If is set, it will be called regardless of whether this /// property is true or false. From 0fa4bcf33a3c3e092387e679b9510f82602c5b24 Mon Sep 17 00:00:00 2001 From: Jan Trejbal Date: Mon, 27 May 2024 11:41:54 +0200 Subject: [PATCH 3/4] Use TimeProvider in ValidateLifetime --- src/Microsoft.IdentityModel.Tokens/ValidatorUtilities.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Microsoft.IdentityModel.Tokens/ValidatorUtilities.cs b/src/Microsoft.IdentityModel.Tokens/ValidatorUtilities.cs index 6945912645..9221780d0f 100644 --- a/src/Microsoft.IdentityModel.Tokens/ValidatorUtilities.cs +++ b/src/Microsoft.IdentityModel.Tokens/ValidatorUtilities.cs @@ -36,7 +36,11 @@ internal static void ValidateLifetime(DateTime? notBefore, DateTime? expires, Se Expires = expires }); +#if SUPPORTS_TIME_PROVIDER + DateTime utcNow = validationParameters.TimeProvider?.GetUtcNow().UtcDateTime ?? DateTime.UtcNow; +#else DateTime utcNow = DateTime.UtcNow; +#endif 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 385311e1cff9ee99f85bbe2e4fe1f52169c082d0 Mon Sep 17 00:00:00 2001 From: Jan Trejbal Date: Mon, 27 May 2024 12:15:09 +0200 Subject: [PATCH 4/4] Use TimeProvider --- .../JsonWebTokenHandler.CreateToken.cs | 6 ++- .../OpenIdConnectProtocolValidator.cs | 27 +++++++++- .../SignedHttpRequestCreationParameters.cs | 26 ++++++--- .../SignedHttpRequestHandler.cs | 53 +++++++++++-------- .../Configuration/ConfigurationManager.cs | 20 +++++-- .../Saml/SamlSecurityTokenHandler.cs | 35 ++++++++---- .../Saml2/Saml2SecurityTokenHandler.cs | 34 +++++++----- .../BaseConfigurationManager.cs | 46 ++++++++++++++-- .../SecurityTokenDescriptor.cs | 16 +++++- .../TokenValidationParameters.cs | 8 +-- .../ValidatorUtilities.cs | 7 +-- .../Validators.cs | 7 ++- .../JwtSecurityTokenHandler.cs | 53 ++++++++++++++++--- 13 files changed, 259 insertions(+), 79 deletions(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs index 7c7d63907e..a253ffd74e 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs @@ -835,7 +835,11 @@ internal static void WriteJwsPayload( // By default we set these three properties only if they haven't been detected before. if (setDefaultTimesOnTokenCreation && !(expSet && iatSet && nbfSet)) { - DateTime now = DateTime.UtcNow; + DateTime now = +#if SUPPORTS_TIME_PROVIDER + tokenDescriptor.TimeProvider?.GetUtcNow().UtcDateTime ?? +#endif + DateTime.UtcNow; if (!expSet) { diff --git a/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/OpenIdConnectProtocolValidator.cs b/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/OpenIdConnectProtocolValidator.cs index 7408ce1bfb..e178738d9c 100644 --- a/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/OpenIdConnectProtocolValidator.cs +++ b/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/OpenIdConnectProtocolValidator.cs @@ -88,12 +88,30 @@ public virtual string GenerateNonce() string nonce = Convert.ToBase64String(Encoding.UTF8.GetBytes(Guid.NewGuid().ToString() + Guid.NewGuid().ToString())); if (RequireTimeStampInNonce) { - return DateTime.UtcNow.Ticks.ToString(CultureInfo.InvariantCulture) + "." + nonce; + DateTime utcNow = +#if SUPPORTS_TIME_PROVIDER + TimeProvider?.GetUtcNow().UtcDateTime ?? +#endif + DateTime.UtcNow; + + return utcNow.Ticks.ToString(CultureInfo.InvariantCulture) + "." + nonce; } return nonce; } +#if SUPPORTS_TIME_PROVIDER +#nullable enable + /// + /// Gets or sets the time provider. + /// + /// + /// If not set, fall back to using the class to obtain the current time. + /// + public TimeProvider? TimeProvider { get; set; } +#nullable restore +#endif + /// /// Gets the algorithm mapping between OpenIdConnect and .Net for Hash algorithms. /// a that contains mappings from the JWT namespace to .NET. @@ -658,7 +676,12 @@ protected virtual void ValidateNonce(OpenIdConnectProtocolValidationContext vali throw LogHelper.LogExceptionMessage(new OpenIdConnectProtocolInvalidNonceException(LogHelper.FormatInvariant(LogMessages.IDX21327, LogHelper.MarkAsNonPII(timestamp), LogHelper.MarkAsNonPII(DateTime.MinValue.Ticks.ToString(CultureInfo.InvariantCulture)), LogHelper.MarkAsNonPII(DateTime.MaxValue.Ticks.ToString(CultureInfo.InvariantCulture))), ex)); } - DateTime utcNow = DateTime.UtcNow; + DateTime utcNow = +#if SUPPORTS_TIME_PROVIDER + TimeProvider?.GetUtcNow().UtcDateTime ?? +#endif + DateTime.UtcNow; + if (nonceTime + NonceLifetime < utcNow) throw LogHelper.LogExceptionMessage(new OpenIdConnectProtocolInvalidNonceException(LogHelper.FormatInvariant(LogMessages.IDX21324, nonceFoundInJwt, LogHelper.MarkAsNonPII(nonceTime.ToString(CultureInfo.InvariantCulture)), LogHelper.MarkAsNonPII(utcNow.ToString(CultureInfo.InvariantCulture)), LogHelper.MarkAsNonPII(NonceLifetime.ToString("c", CultureInfo.InvariantCulture))))); } diff --git a/src/Microsoft.IdentityModel.Protocols.SignedHttpRequest/SignedHttpRequestCreationParameters.cs b/src/Microsoft.IdentityModel.Protocols.SignedHttpRequest/SignedHttpRequestCreationParameters.cs index 781d56e2c5..56fb919ba0 100644 --- a/src/Microsoft.IdentityModel.Protocols.SignedHttpRequest/SignedHttpRequestCreationParameters.cs +++ b/src/Microsoft.IdentityModel.Protocols.SignedHttpRequest/SignedHttpRequestCreationParameters.cs @@ -14,7 +14,7 @@ public class SignedHttpRequestCreationParameters /// Gets or sets a value indicating whether the claim should be created and added or not. /// /// - /// will be used as a "cnf" claim value, if set. + /// will be used as a "cnf" claim value, if set. /// Otherwise, a "cnf" claim value will be derived from . /// public bool CreateCnf { get; set; } = true; @@ -27,19 +27,19 @@ public class SignedHttpRequestCreationParameters /// /// Gets or sets a value indicating whether the claim should be created and added or not. /// - /// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-signed-http-request-03#section-3 + /// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-signed-http-request-03#section-3 public bool CreateTs { get; set; } = true; /// /// Gets or sets a value indicating whether the claim should be created and added or not. /// - /// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-signed-http-request-03#section-3 + /// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-signed-http-request-03#section-3 public bool CreateM { get; set; } = true; /// /// Gets or sets a value indicating whether the claim should be created and added or not. /// - /// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-signed-http-request-03#section-3 + /// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-signed-http-request-03#section-3 public bool CreateU { get; set; } = true; /// @@ -51,19 +51,19 @@ public class SignedHttpRequestCreationParameters /// /// Gets or sets a value indicating whether the claim should be created and added or not. /// - /// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-signed-http-request-03#section-3 + /// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-signed-http-request-03#section-3 public bool CreateQ { get; set; } /// /// Gets or sets a value indicating whether the claim should be created and added or not. /// - /// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-signed-http-request-03#section-3 + /// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-signed-http-request-03#section-3 public bool CreateH { get; set; } /// /// Gets or sets a value indicating whether the claim should be created and added or not. /// - /// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-signed-http-request-03#section-3 + /// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-signed-http-request-03#section-3 public bool CreateB { get; set; } /// @@ -76,5 +76,17 @@ public class SignedHttpRequestCreationParameters /// /// Allows for adjusting the local time so it matches a server time. public TimeSpan TimeAdjustment { get; set; } = DefaultTimeAdjustment; + +#if SUPPORTS_TIME_PROVIDER +#nullable enable + /// + /// Gets or sets the time provider. + /// + /// + /// If not set, fall back to using the class to obtain the current time. + /// + public TimeProvider? TimeProvider { get; set; } +#nullable restore +#endif } } diff --git a/src/Microsoft.IdentityModel.Protocols.SignedHttpRequest/SignedHttpRequestHandler.cs b/src/Microsoft.IdentityModel.Protocols.SignedHttpRequest/SignedHttpRequestHandler.cs index 1411d14732..0b366aa2dc 100644 --- a/src/Microsoft.IdentityModel.Protocols.SignedHttpRequest/SignedHttpRequestHandler.cs +++ b/src/Microsoft.IdentityModel.Protocols.SignedHttpRequest/SignedHttpRequestHandler.cs @@ -21,7 +21,7 @@ namespace Microsoft.IdentityModel.Protocols.SignedHttpRequest { /// - /// A handler designed for creating and validating signed http requests. + /// A handler designed for creating and validating signed http requests. /// /// The handler implementation is based on 'A Method for Signing HTTP Requests for OAuth' specification. public class SignedHttpRequestHandler @@ -143,7 +143,7 @@ public string CreateSignedHttpRequest(SignedHttpRequestDescriptor signedHttpRequ /// Creates a JSON representation of a HttpRequest payload. /// /// A structure that wraps parameters needed for SignedHttpRequest creation. - /// An opaque context used to store work and logs when working with authentication artifacts. + /// An opaque context used to store work and logs when working with authentication artifacts. /// A JSON representation of an HttpRequest payload. /// /// Users can utilize to create additional claim(s) and add them to the signed http request. @@ -227,13 +227,19 @@ internal virtual void AddAtClaim(ref Utf8JsonWriter writer, SignedHttpRequestDes /// A structure that wraps parameters needed for SignedHttpRequest creation. /// /// This method will be executed only if is set to true. - /// + /// internal virtual void AddTsClaim(ref Utf8JsonWriter writer, SignedHttpRequestDescriptor signedHttpRequestDescriptor) { + DateTime utcNow = +#if SUPPORTS_TIME_PROVIDER + signedHttpRequestDescriptor.SignedHttpRequestCreationParameters.TimeProvider?.GetUtcNow().UtcDateTime ?? +#endif + DateTime.UtcNow; + writer.WriteNumber( SignedHttpRequestClaimTypes.Ts, EpochTime.GetIntDate( - DateTime.UtcNow.Add(signedHttpRequestDescriptor.SignedHttpRequestCreationParameters.TimeAdjustment))); + utcNow.Add(signedHttpRequestDescriptor.SignedHttpRequestCreationParameters.TimeAdjustment))); } /// @@ -243,7 +249,7 @@ internal virtual void AddTsClaim(ref Utf8JsonWriter writer, SignedHttpRequestDes /// A structure that wraps parameters needed for SignedHttpRequest creation. /// /// This method will be executed only if is set to true. - /// + /// internal virtual void AddMClaim(ref Utf8JsonWriter writer, SignedHttpRequestDescriptor signedHttpRequestDescriptor) { if (string.IsNullOrEmpty(signedHttpRequestDescriptor.HttpRequestData.Method)) @@ -263,7 +269,7 @@ internal virtual void AddMClaim(ref Utf8JsonWriter writer, SignedHttpRequestDesc /// A structure that wraps parameters needed for SignedHttpRequest creation. /// /// This method will be executed only if is set to true. - /// + /// internal virtual void AddUClaim(ref Utf8JsonWriter writer, SignedHttpRequestDescriptor signedHttpRequestDescriptor) { if (signedHttpRequestDescriptor.HttpRequestData.Uri == null) @@ -289,7 +295,7 @@ internal virtual void AddUClaim(ref Utf8JsonWriter writer, SignedHttpRequestDesc /// A structure that wraps parameters needed for SignedHttpRequest creation. /// /// This method will be executed only if is set to true. - /// + /// internal virtual void AddPClaim(ref Utf8JsonWriter writer, SignedHttpRequestDescriptor signedHttpRequestDescriptor) { if (signedHttpRequestDescriptor.HttpRequestData.Uri == null) @@ -351,7 +357,7 @@ internal virtual void AddQClaim(ref Utf8JsonWriter writer, SignedHttpRequestDesc /// A structure that wraps parameters needed for SignedHttpRequest creation. /// /// This method will be executed only if is set to true. - /// + /// internal void AddHClaim(ref Utf8JsonWriter writer, SignedHttpRequestDescriptor signedHttpRequestDescriptor) { IDictionary sanitizedHeaders = SanitizeHeaders(signedHttpRequestDescriptor.HttpRequestData.Headers); @@ -389,7 +395,7 @@ internal void AddHClaim(ref Utf8JsonWriter writer, SignedHttpRequestDescriptor s /// A structure that wraps parameters needed for SignedHttpRequest creation. /// /// This method will be executed only if is set to true. - /// + /// internal virtual void AddBClaim(ref Utf8JsonWriter writer, SignedHttpRequestDescriptor signedHttpRequestDescriptor) { try @@ -442,7 +448,7 @@ internal virtual void AddCnfClaim(ref Utf8JsonWriter writer, SignedHttpRequestDe JsonWebKey jsonWebKey; if (signedHttpRequestDescriptor.SigningCredentials.Key is JsonWebKey jwk) jsonWebKey = jwk; - // create a JsonWebKey from an X509SecurityKey, represented as an RsaSecurityKey. + // create a JsonWebKey from an X509SecurityKey, represented as an RsaSecurityKey. else if (signedHttpRequestDescriptor.SigningCredentials.Key is X509SecurityKey x509SecurityKey) jsonWebKey = JsonWebKeyConverter.ConvertFromX509SecurityKey(x509SecurityKey, true); else if (signedHttpRequestDescriptor.SigningCredentials.Key is AsymmetricSecurityKey asymmetricSecurityKey) @@ -480,7 +486,7 @@ internal virtual void AddCnfClaim(ref Utf8JsonWriter writer, SignedHttpRequestDe /// /// A structure that wraps parameters needed for SignedHttpRequest validation. /// Propagates notification that operations should be canceled. - /// A . + /// A . /// will be true if the signed http request was successfully validated, false otherwise. /// public async Task ValidateSignedHttpRequestAsync(SignedHttpRequestValidationContext signedHttpRequestValidationContext, CancellationToken cancellationToken) @@ -725,7 +731,12 @@ internal virtual void ValidateTsClaim(JsonWebToken signedHttpRequest, SignedHttp if (!signedHttpRequest.TryGetPayloadValue(SignedHttpRequestClaimTypes.Ts, out long tsClaimValue)) throw LogHelper.LogExceptionMessage(new SignedHttpRequestInvalidTsClaimException(LogHelper.FormatInvariant(LogMessages.IDX23003, LogHelper.MarkAsNonPII(SignedHttpRequestClaimTypes.Ts)))); - DateTime utcNow = DateTime.UtcNow; + DateTime utcNow = +#if SUPPORTS_TIME_PROVIDER + signedHttpRequestValidationContext.AccessTokenValidationParameters.TimeProvider?.GetUtcNow().UtcDateTime ?? +#endif + DateTime.UtcNow; + DateTime signedHttpRequestCreationTime = EpochTime.DateTime(tsClaimValue); DateTime signedHttpRequestExpirationTime = signedHttpRequestCreationTime.Add(signedHttpRequestValidationContext.SignedHttpRequestValidationParameters.SignedHttpRequestLifetime); @@ -762,7 +773,7 @@ internal virtual void ValidateMClaim(JsonWebToken signedHttpRequest, SignedHttpR } /// - /// Validates the signed http request "u" claim. + /// Validates the signed http request "u" claim. /// /// A SignedHttpRequest. /// A structure that wraps parameters needed for SignedHttpRequest validation. @@ -796,7 +807,7 @@ internal virtual void ValidateUClaim(JsonWebToken signedHttpRequest, SignedHttpR } /// - /// Validates the signed http request "p" claim. + /// Validates the signed http request "p" claim. /// /// A SignedHttpRequest. /// A structure that wraps parameters needed for SignedHttpRequest validation. @@ -825,7 +836,7 @@ internal virtual void ValidatePClaim(JsonWebToken signedHttpRequest, SignedHttpR } /// - /// Validates the signed http request "q" claim. + /// Validates the signed http request "q" claim. /// /// A SignedHttpRequest. /// A structure that wraps parameters needed for SignedHttpRequest validation. @@ -900,7 +911,7 @@ internal virtual void ValidateQClaim(JsonWebToken signedHttpRequest, SignedHttpR } /// - /// Validates the signed http request "h" claim. + /// Validates the signed http request "h" claim. /// /// A SignedHttpRequest. /// A structure that wraps parameters needed for SignedHttpRequest validation. @@ -969,7 +980,7 @@ internal virtual void ValidateHClaim(JsonWebToken signedHttpRequest, SignedHttpR } /// - /// Validates the signed http request "b" claim. + /// Validates the signed http request "b" claim. /// /// A SignedHttpRequest. /// A structure that wraps parameters needed for SignedHttpRequest validation. @@ -1082,7 +1093,7 @@ internal virtual async Task ResolvePopKeyFromCnfClaimAsync(Cnf cnf, } /// - /// Resolves a PoP from the asymmetric representation of a PoP key. + /// Resolves a PoP from the asymmetric representation of a PoP key. /// /// The JsonWebKey to resolve. /// A structure that wraps parameters needed for SignedHttpRequest validation. @@ -1103,7 +1114,7 @@ internal virtual SecurityKey ResolvePopKeyFromJwk(JsonWebKey jsonWebKey, SignedH } /// - /// Resolves a PoP from the encrypted symmetric representation of a PoP key. + /// Resolves a PoP from the encrypted symmetric representation of a PoP key. /// /// An encrypted symmetric representation of a PoP key (JSON). /// A structure that wraps parameters needed for SignedHttpRequest validation. @@ -1119,7 +1130,7 @@ internal virtual async Task ResolvePopKeyFromJweAsync(string jwe, S } /// - /// Resolves a PoP from the URL reference to a PoP key. + /// Resolves a PoP from the URL reference to a PoP key. /// /// A URL reference to a PoP JWK set. /// A confirmation ("cnf") claim as a JObject. @@ -1203,7 +1214,7 @@ internal virtual async Task> GetPopKeysFromJkuAsync(string jk } /// - /// Resolves a PoP using a key identifier of a PoP key. + /// Resolves a PoP using a key identifier of a PoP key. /// /// A claim value. /// A signed http request as a JWT. diff --git a/src/Microsoft.IdentityModel.Protocols/Configuration/ConfigurationManager.cs b/src/Microsoft.IdentityModel.Protocols/Configuration/ConfigurationManager.cs index 3ab4c16aef..37e11c5431 100644 --- a/src/Microsoft.IdentityModel.Protocols/Configuration/ConfigurationManager.cs +++ b/src/Microsoft.IdentityModel.Protocols/Configuration/ConfigurationManager.cs @@ -190,8 +190,14 @@ public virtual async Task GetConfigurationAsync(CancellationToken cancel) result.ErrorMessage))); } + DateTimeOffset utcNow = +#if SUPPORTS_TIME_PROVIDER + TimeProvider?.GetUtcNow() ?? +#endif + DateTimeOffset.UtcNow; + // Add a random amount between 0 and 5% of AutomaticRefreshInterval jitter to avoid spike traffic to IdentityProvider. - _syncAfter = DateTimeUtil.Add(DateTime.UtcNow, AutomaticRefreshInterval + + _syncAfter = DateTimeUtil.Add(utcNow.UtcDateTime, AutomaticRefreshInterval + TimeSpan.FromSeconds(new Random().Next((int)AutomaticRefreshInterval.TotalSeconds / 20))); _currentConfiguration = configuration; @@ -200,6 +206,12 @@ public virtual async Task GetConfigurationAsync(CancellationToken cancel) { fetchMetadataFailure = ex; + DateTime utcNow = +#if SUPPORTS_TIME_PROVIDER + TimeProvider?.GetUtcNow().UtcDateTime ?? +#endif + DateTime.UtcNow; + // In this case configuration was never obtained. if (_currentConfiguration == null) { @@ -208,12 +220,12 @@ public virtual async Task GetConfigurationAsync(CancellationToken cancel) // Adopt exponential backoff for bootstrap refresh interval with a decorrelated jitter if it is not longer than the refresh interval. TimeSpan _bootstrapRefreshIntervalWithJitter = TimeSpan.FromSeconds(new Random().Next((int)_bootstrapRefreshInterval.TotalSeconds)); _bootstrapRefreshInterval += _bootstrapRefreshInterval; - _syncAfter = DateTimeUtil.Add(DateTime.UtcNow, _bootstrapRefreshIntervalWithJitter); + _syncAfter = DateTimeUtil.Add(utcNow, _bootstrapRefreshIntervalWithJitter); } else { _syncAfter = DateTimeUtil.Add( - DateTime.UtcNow, + utcNow, AutomaticRefreshInterval < RefreshInterval ? AutomaticRefreshInterval : RefreshInterval); } @@ -229,7 +241,7 @@ public virtual async Task GetConfigurationAsync(CancellationToken cancel) else { _syncAfter = DateTimeUtil.Add( - DateTime.UtcNow, + utcNow, AutomaticRefreshInterval < RefreshInterval ? AutomaticRefreshInterval : RefreshInterval); LogHelper.LogExceptionMessage( diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.cs b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.cs index d82ff3f75e..6828da5487 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.cs +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.cs @@ -70,7 +70,7 @@ public IEqualityComparer SamlSubjectEqualityComparer /// /// Gets or set the that will be used to read and write a . /// - /// 'value' is null. + /// 'value' is null. public SamlSerializer Serializer { get { return _serializer; } @@ -186,7 +186,7 @@ protected virtual ICollection ConsolidateAttributes(ICollection - /// Override this method to provide a SamlAdvice to place in the Samltoken. + /// Override this method to provide a SamlAdvice to place in the Samltoken. /// /// Contains information about the token. /// SamlAdvice, default is null. @@ -286,7 +286,7 @@ protected virtual SamlAttributeStatement CreateAttributeStatement(SamlSubject su } /// - /// Creates a SamlAuthenticationStatement for each AuthenticationInformation found in AuthenticationInformation. + /// Creates a SamlAuthenticationStatement for each AuthenticationInformation found in AuthenticationInformation. /// Override this method to provide a custom implementation. /// /// The SamlSubject of the Statement. @@ -360,12 +360,20 @@ protected virtual SamlConditions CreateConditions(SecurityTokenDescriptor tokenD if (tokenDescriptor.NotBefore.HasValue) conditions.NotBefore = tokenDescriptor.NotBefore.Value; else if (SetDefaultTimesOnTokenCreation) - conditions.NotBefore = DateTime.UtcNow; + conditions.NotBefore = +#if SUPPORTS_TIME_PROVIDER + tokenDescriptor.TimeProvider?.GetUtcNow().UtcDateTime ?? +#endif + DateTime.UtcNow; if (tokenDescriptor.Expires.HasValue) conditions.NotOnOrAfter = tokenDescriptor.Expires.Value; else if (SetDefaultTimesOnTokenCreation) - conditions.NotOnOrAfter = DateTime.UtcNow + TimeSpan.FromMinutes(TokenLifetimeInMinutes); + conditions.NotOnOrAfter = +#if SUPPORTS_TIME_PROVIDER + tokenDescriptor.TimeProvider?.GetUtcNow().UtcDateTime ?? +#endif + DateTime.UtcNow + TimeSpan.FromMinutes(TokenLifetimeInMinutes); if (tokenDescriptor.Audiences.Count > 0) { @@ -516,7 +524,12 @@ public virtual SecurityToken CreateToken(SecurityTokenDescriptor tokenDescriptor var conditions = CreateConditions(tokenDescriptor); var advice = CreateAdvice(tokenDescriptor); - var issuedAt = tokenDescriptor.IssuedAt.HasValue ? tokenDescriptor.IssuedAt.Value : DateTime.UtcNow; + var issuedAt = tokenDescriptor.IssuedAt.HasValue ? tokenDescriptor.IssuedAt.Value : +#if SUPPORTS_TIME_PROVIDER + tokenDescriptor.TimeProvider?.GetUtcNow().UtcDateTime ?? +#endif + DateTime.UtcNow; + return new SamlSecurityToken(new SamlAssertion("_" + Guid.NewGuid().ToString(), tokenDescriptor.Issuer, issuedAt, conditions, advice, statements) { SigningCredentials = tokenDescriptor.SigningCredentials @@ -525,11 +538,11 @@ public virtual SecurityToken CreateToken(SecurityTokenDescriptor tokenDescriptor } /// - /// Builds an XML formated string from a collection of saml attributes that represent an Actor. + /// Builds an XML formated string from a collection of saml attributes that represent an Actor. /// /// . /// A well formed XML string. - /// The string is of the form "<Actor><SamlAttribute name, ns><SamlAttributeValue>...</SamlAttributeValue>, ...</SamlAttribute>...</Actor>" + /// The string is of the form "<Actor><SamlAttribute name, ns><SamlAttributeValue>...</SamlAttributeValue>, ...</SamlAttribute>...</Actor>" protected virtual string CreateXmlStringFromAttributes(ICollection attributes) { if (attributes == null) @@ -795,7 +808,7 @@ public virtual SamlSecurityToken ReadSamlToken(XmlReader reader) /// /// Deserializes from XML a token of the type handled by this instance. /// - /// An XML reader positioned at the token's start + /// An XML reader positioned at the token's start /// element. /// The to be used for validating the token. /// An instance of . @@ -830,7 +843,7 @@ protected virtual SecurityKey ResolveIssuerSigningKey(string token, SamlSecurity } /// - /// This method gets called when a special type of SamlAttribute is detected. The SamlAttribute passed in wraps a SamlAttribute + /// This method gets called when a special type of SamlAttribute is detected. The SamlAttribute passed in wraps a SamlAttribute /// that contains a collection of AttributeValues, each of which are mapped to a claim. All of the claims will be returned /// in an ClaimsIdentity with the specified issuer. /// @@ -1008,7 +1021,7 @@ protected virtual void ValidateLifetime(DateTime? notBefore, DateTime? expires, /// If returns null"/>. /// If returns null OR an object other than a . /// If a signature is not found and is true. - /// If the 'token' has a key identifier and none of the (s) provided result in a validated signature. + /// If the 'token' has a key identifier and none of the (s) provided result in a validated signature. /// This can indicate that a key refresh is required. /// If after trying all the (s), none result in a validated signture AND the 'token' does not have a key identifier. /// A that has had the signature validated if token was signed. diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.cs b/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.cs index 90184df46f..574993c049 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.cs +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.cs @@ -374,7 +374,7 @@ protected virtual void ValidateConfirmationData(Saml2SecurityToken samlToken, To /// If return null. /// If returns null OR an object other than a . /// If a signature is not found and is true. - /// If the has a key identifier and none of the (s) provided result in a validated signature. + /// If the has a key identifier and none of the (s) provided result in a validated signature. /// This can indicate that a key refresh is required. /// If after trying all the (s), none result in a validated signature AND the 'token' does not have a key identifier. /// A that has had the signature validated if token was signed. @@ -608,9 +608,9 @@ internal static bool IsSaml2Assertion(XmlReader reader) /// /// /// - /// Generally, conditions should be included in assertions to limit the - /// impact of misuse of the assertion. Specifying the NotBefore and - /// NotOnOrAfter conditions can limit the period of vulnerability in + /// Generally, conditions should be included in assertions to limit the + /// impact of misuse of the assertion. Specifying the NotBefore and + /// NotOnOrAfter conditions can limit the period of vulnerability in /// the case of a compromised assertion. The AudienceRestrictionCondition /// can be used to explicitly state the intended relying party or parties /// of the assertion, which coupled with appropriate audience restriction @@ -619,8 +619,8 @@ internal static bool IsSaml2Assertion(XmlReader reader) /// /// /// The default implementation creates NotBefore and NotOnOrAfter conditions - /// based on the tokenDescriptor.Lifetime. It will also generate an - /// AudienceRestrictionCondition limiting consumption of the assertion to + /// based on the tokenDescriptor.Lifetime. It will also generate an + /// AudienceRestrictionCondition limiting consumption of the assertion to /// tokenDescriptor.Scope.Address. /// /// @@ -636,12 +636,20 @@ protected virtual Saml2Conditions CreateConditions(SecurityTokenDescriptor token if (tokenDescriptor.NotBefore.HasValue) conditions.NotBefore = tokenDescriptor.NotBefore.Value; else if (SetDefaultTimesOnTokenCreation) - conditions.NotBefore = DateTime.UtcNow; + conditions.NotBefore = +#if SUPPORTS_TIME_PROVIDER + tokenDescriptor.TimeProvider?.GetUtcNow().UtcDateTime ?? +#endif + DateTime.UtcNow; if (tokenDescriptor.Expires.HasValue) conditions.NotOnOrAfter = tokenDescriptor.Expires.Value; else if (SetDefaultTimesOnTokenCreation) - conditions.NotOnOrAfter = DateTime.UtcNow + TimeSpan.FromMinutes(TokenLifetimeInMinutes); + conditions.NotOnOrAfter = +#if SUPPORTS_TIME_PROVIDER + tokenDescriptor.TimeProvider?.GetUtcNow().UtcDateTime ?? +#endif + DateTime.UtcNow + TimeSpan.FromMinutes(TokenLifetimeInMinutes); if (tokenDescriptor.Audiences.Count > 0) { @@ -801,7 +809,7 @@ protected virtual ICollection ConsolidateAttributes(ICollection< /// A to be transformed. /// if is null. /// A well-formed XML string. - /// Normally this is called when creating a from a . When is not null, + /// Normally this is called when creating a from a . When is not null, /// this method is called to create an string representation to add as an attribute. /// The string is formed: "<Actor><Attribute name, namespace><AttributeValue>...</AttributeValue>, ...</Attribute>...</Actor> protected string CreateActorString(ClaimsIdentity actor) @@ -820,7 +828,7 @@ protected string CreateActorString(ClaimsIdentity actor) } /// - /// Builds an XML formatted string from a collection of SAML attributes that represent the Actor. + /// Builds an XML formatted string from a collection of SAML attributes that represent the Actor. /// /// An enumeration of Saml2Attributes. /// A well-formed XML string. @@ -1059,14 +1067,14 @@ protected virtual void ValidateOneTimeUseCondition(Saml2SecurityToken securityTo } /// - /// This method gets called when a special type of Saml2Attribute is detected. The Saml2Attribute passed in - /// wraps a Saml2Attribute that contains a collection of AttributeValues, each of which will get mapped to a + /// This method gets called when a special type of Saml2Attribute is detected. The Saml2Attribute passed in + /// wraps a Saml2Attribute that contains a collection of AttributeValues, each of which will get mapped to a /// claim. All of the claims will be returned in an ClaimsIdentity with the specified issuer. /// /// The to use. /// The that is the subject of this token. /// The issuer of the claim. - /// Will be thrown if the Saml2Attribute does not contain any + /// Will be thrown if the Saml2Attribute does not contain any /// valid Saml2AttributeValues. /// protected virtual void SetClaimsIdentityActorFromAttribute(Saml2Attribute attribute, ClaimsIdentity identity, string issuer) diff --git a/src/Microsoft.IdentityModel.Tokens/BaseConfigurationManager.cs b/src/Microsoft.IdentityModel.Tokens/BaseConfigurationManager.cs index 2124c416c4..ce74359eae 100644 --- a/src/Microsoft.IdentityModel.Tokens/BaseConfigurationManager.cs +++ b/src/Microsoft.IdentityModel.Tokens/BaseConfigurationManager.cs @@ -38,6 +38,18 @@ public TimeSpan AutomaticRefreshInterval } } +#if SUPPORTS_TIME_PROVIDER +#nullable enable + /// + /// Gets or sets the time provider. + /// + /// + /// If not set, fall back to using the class to obtain the current time. + /// + public TimeProvider? TimeProvider { get; set; } +#nullable restore +#endif + /// /// Default time interval (12 hours) after which a new configuration is obtained automatically. /// @@ -101,7 +113,13 @@ public virtual Task GetBaseConfigurationAsync(CancellationTok /// A collection of all valid last known good configurations. internal BaseConfiguration[] GetValidLkgConfigurations() { - return _lastKnownGoodConfigurationCache.ToArray().Where(x => x.Value.Value > DateTime.UtcNow).Select(x => x.Key).ToArray(); + DateTime utcNow = +#if SUPPORTS_TIME_PROVIDER + TimeProvider?.GetUtcNow().UtcDateTime ?? +#endif + DateTime.UtcNow; + + return _lastKnownGoodConfigurationCache.ToArray().Where(x => x.Value.Value > utcNow).Select(x => x.Key).ToArray(); } /// @@ -116,10 +134,16 @@ public BaseConfiguration LastKnownGoodConfiguration set { _lastKnownGoodConfiguration = value ?? throw LogHelper.LogArgumentNullException(nameof(value)); - _lastKnownGoodConfigFirstUse = DateTime.UtcNow; + DateTime utcNow = +#if SUPPORTS_TIME_PROVIDER + TimeProvider?.GetUtcNow().UtcDateTime ?? +#endif + DateTime.UtcNow; + + _lastKnownGoodConfigFirstUse = utcNow; // LRU cache will remove the expired configuration - _lastKnownGoodConfigurationCache.SetValue(_lastKnownGoodConfiguration, DateTime.UtcNow + LastKnownGoodLifetime, DateTime.UtcNow + LastKnownGoodLifetime); + _lastKnownGoodConfigurationCache.SetValue(_lastKnownGoodConfiguration, utcNow + LastKnownGoodLifetime, DateTime.UtcNow + LastKnownGoodLifetime); } } @@ -178,7 +202,21 @@ public TimeSpan RefreshInterval /// // The _lastKnownGoodConfiguration private variable is accessed rather than the property (LastKnownGoodConfiguration) as we do not want this access // to trigger a change in _lastKnownGoodConfigFirstUse. - public bool IsLastKnownGoodValid => _lastKnownGoodConfiguration != null && (_lastKnownGoodConfigFirstUse == null || DateTime.UtcNow < _lastKnownGoodConfigFirstUse + LastKnownGoodLifetime); + public bool IsLastKnownGoodValid + { + get + { + DateTime utcNow = +#if SUPPORTS_TIME_PROVIDER + TimeProvider?.GetUtcNow().UtcDateTime ?? +#endif + DateTime.UtcNow; + + return _lastKnownGoodConfiguration != null && (_lastKnownGoodConfigFirstUse == null || + utcNow < _lastKnownGoodConfigFirstUse + + LastKnownGoodLifetime); + } + } /// /// Indicate that the configuration may be stale (as indicated by failing to process incoming tokens). diff --git a/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs b/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs index b9b5843143..d5ae915dd1 100644 --- a/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs +++ b/src/Microsoft.IdentityModel.Tokens/SecurityTokenDescriptor.cs @@ -77,7 +77,7 @@ public class SecurityTokenDescriptor /// Gets or sets the which contains any custom header claims that need to be added to the JWT token header. /// The 'alg', 'kid', 'x5t', 'enc', and 'zip' claims are added by default based on the , /// , and/or provided and SHOULD NOT be included in this dictionary as this - /// will result in an exception being thrown. + /// will result in an exception being thrown. /// These claims are only added to the outer header (in case of a JWE). /// public IDictionary AdditionalHeaderClaims { get; set; } @@ -86,7 +86,7 @@ public class SecurityTokenDescriptor /// Gets or sets the which contains any custom header claims that need to be added to the inner JWT token header. /// The 'alg', 'kid', 'x5t', 'enc', and 'zip' claims are added by default based on the , /// , and/or provided and SHOULD NOT be included in this dictionary as this - /// will result in an exception being thrown. + /// will result in an exception being thrown. /// /// For JsonWebTokenHandler, these claims are merged with while adding to the inner JWT header. /// @@ -105,5 +105,17 @@ public class SecurityTokenDescriptor /// values will be overridden. /// public ClaimsIdentity Subject { get; set; } + +#if SUPPORTS_TIME_PROVIDER +#nullable enable + /// + /// Gets or sets the time provider. + /// + /// + /// If not set, fall back to using the class to obtain the current time. + /// + public TimeProvider? TimeProvider { get; set; } +#nullable restore +#endif } } diff --git a/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs b/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs index 13059a7b76..443e514a25 100644 --- a/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs +++ b/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs @@ -544,13 +544,15 @@ public string RoleClaimType public SignatureValidatorUsingConfiguration SignatureValidatorUsingConfiguration { get; set; } #if SUPPORTS_TIME_PROVIDER +#nullable enable /// - /// Gets or sets the time provider used for time validation. + /// Gets or sets the time provider. /// /// - /// If not set, validators will fall back to using the class to obtain the current time. + /// If not set, fall back to using the class to obtain the current time. /// - public TimeProvider TimeProvider { get; set; } + public TimeProvider? TimeProvider { get; set; } +#nullable restore #endif /// diff --git a/src/Microsoft.IdentityModel.Tokens/ValidatorUtilities.cs b/src/Microsoft.IdentityModel.Tokens/ValidatorUtilities.cs index 9221780d0f..408bb6fca4 100644 --- a/src/Microsoft.IdentityModel.Tokens/ValidatorUtilities.cs +++ b/src/Microsoft.IdentityModel.Tokens/ValidatorUtilities.cs @@ -36,11 +36,12 @@ internal static void ValidateLifetime(DateTime? notBefore, DateTime? expires, Se Expires = expires }); + DateTime utcNow = #if SUPPORTS_TIME_PROVIDER - DateTime utcNow = validationParameters.TimeProvider?.GetUtcNow().UtcDateTime ?? DateTime.UtcNow; -#else - DateTime utcNow = DateTime.UtcNow; + validationParameters.TimeProvider?.GetUtcNow().UtcDateTime ?? #endif + 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))) { diff --git a/src/Microsoft.IdentityModel.Tokens/Validators.cs b/src/Microsoft.IdentityModel.Tokens/Validators.cs index 6002eded77..ec5222e31a 100644 --- a/src/Microsoft.IdentityModel.Tokens/Validators.cs +++ b/src/Microsoft.IdentityModel.Tokens/Validators.cs @@ -404,7 +404,12 @@ internal static void ValidateIssuerSigningKeyLifeTime(SecurityKey securityKey, T X509SecurityKey x509SecurityKey = securityKey as X509SecurityKey; if (x509SecurityKey?.Certificate is X509Certificate2 cert) { - DateTime utcNow = DateTime.UtcNow; + DateTime utcNow = +#if SUPPORTS_TIME_PROVIDER + validationParameters.TimeProvider?.GetUtcNow().UtcDateTime ?? +#endif + DateTime.UtcNow; + var notBeforeUtc = cert.NotBefore.ToUniversalTime(); var notAfterUtc = cert.NotAfter.ToUniversalTime(); diff --git a/src/System.IdentityModel.Tokens.Jwt/JwtSecurityTokenHandler.cs b/src/System.IdentityModel.Tokens.Jwt/JwtSecurityTokenHandler.cs index bad33a8ed9..84b50ce053 100644 --- a/src/System.IdentityModel.Tokens.Jwt/JwtSecurityTokenHandler.cs +++ b/src/System.IdentityModel.Tokens.Jwt/JwtSecurityTokenHandler.cs @@ -356,7 +356,11 @@ public virtual string CreateEncodedJwt( expires, issuedAt, signingCredentials, - null, null, null, null, null).RawData; + null, null, null, +#if SUPPORTS_TIME_PROVIDER + timeProvider: null, +#endif + null, null).RawData; } /// @@ -395,7 +399,11 @@ public virtual string CreateEncodedJwt( expires, issuedAt, signingCredentials, - encryptingCredentials, null, null, null, null).RawData; + encryptingCredentials, null, null, +#if SUPPORTS_TIME_PROVIDER + timeProvider: null, +#endif + null, null).RawData; } /// @@ -437,7 +445,11 @@ public virtual string CreateEncodedJwt( issuedAt, signingCredentials, encryptingCredentials, - claimCollection, null, null, null).RawData; + claimCollection, null, +#if SUPPORTS_TIME_PROVIDER + timeProvider: null, +#endif + null, null).RawData; } /// @@ -462,6 +474,9 @@ public virtual JwtSecurityToken CreateJwtSecurityToken(SecurityTokenDescriptor t tokenDescriptor.EncryptingCredentials, tokenDescriptor.Claims, tokenDescriptor.TokenType, +#if SUPPORTS_TIME_PROVIDER + tokenDescriptor.TimeProvider, +#endif tokenDescriptor.AdditionalHeaderClaims, tokenDescriptor.AdditionalInnerHeaderClaims); } @@ -505,7 +520,11 @@ public virtual JwtSecurityToken CreateJwtSecurityToken( expires, issuedAt, signingCredentials, - encryptingCredentials, null, null, null, null); + encryptingCredentials, null, null, +#if SUPPORTS_TIME_PROVIDER + timeProvider: null, +#endif + null, null); } /// @@ -550,7 +569,11 @@ public virtual JwtSecurityToken CreateJwtSecurityToken( issuedAt, signingCredentials, encryptingCredentials, - claimCollection, null, null, null); + claimCollection, null, +#if SUPPORTS_TIME_PROVIDER + timeProvider: null, +#endif + null, null); } /// @@ -588,7 +611,11 @@ public virtual JwtSecurityToken CreateJwtSecurityToken( notBefore, expires, issuedAt, - signingCredentials, null, null, null, null, null); + signingCredentials, null, null, null, +#if SUPPORTS_TIME_PROVIDER + timeProvider: null, +#endif + null, null); } /// @@ -613,6 +640,9 @@ public override SecurityToken CreateToken(SecurityTokenDescriptor tokenDescripto tokenDescriptor.EncryptingCredentials, tokenDescriptor.Claims, tokenDescriptor.TokenType, +#if SUPPORTS_TIME_PROVIDER + tokenDescriptor.TimeProvider, +#endif tokenDescriptor.AdditionalHeaderClaims, tokenDescriptor.AdditionalInnerHeaderClaims); } @@ -628,6 +658,11 @@ private JwtSecurityToken CreateJwtSecurityTokenPrivate( EncryptingCredentials encryptingCredentials, IDictionary claimCollection, string tokenType, +#if SUPPORTS_TIME_PROVIDER +#nullable enable + TimeProvider? timeProvider, +#nullable restore +#endif IDictionary additionalHeaderClaims, IDictionary additionalInnerHeaderClaims) { @@ -653,7 +688,11 @@ private JwtSecurityToken CreateJwtSecurityTokenPrivate( { if (SetDefaultTimesOnTokenCreation && (!expires.HasValue || !issuedAt.HasValue || !notBefore.HasValue)) { - DateTime now = DateTime.UtcNow; + DateTime now = +#if SUPPORTS_TIME_PROVIDER + timeProvider?.GetUtcNow().UtcDateTime ?? +#endif + DateTime.UtcNow; if (!expires.HasValue) expires = now + TimeSpan.FromMinutes(TokenLifetimeInMinutes);