diff --git a/src/OpenIddict.Client/OpenIddictClientBuilder.cs b/src/OpenIddict.Client/OpenIddictClientBuilder.cs index 08e7546fb..f9baae2b2 100644 --- a/src/OpenIddict.Client/OpenIddictClientBuilder.cs +++ b/src/OpenIddict.Client/OpenIddictClientBuilder.cs @@ -200,67 +200,79 @@ public OpenIddictClientBuilder AddDevelopmentEncryptionCertificate(X500Distingui throw new ArgumentNullException(nameof(subject)); } - using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser); - store.Open(OpenFlags.ReadWrite); - - // Try to retrieve the existing development certificates from the specified store. - // If no valid existing certificate was found, create a new encryption certificate. - var certificates = store.Certificates.Find(X509FindType.FindBySubjectDistinguishedName, subject.Name, validOnly: false) - .OfType() - .ToList(); - - if (!certificates.Exists(static certificate => certificate.NotBefore < DateTime.Now && certificate.NotAfter > DateTime.Now)) + Services.AddOptions().Configure((options, provider) => { -#if SUPPORTS_CERTIFICATE_GENERATION - using var algorithm = OpenIddictHelpers.CreateRsaKey(size: 2048); - - var request = new CertificateRequest(subject, algorithm, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); - request.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.KeyEncipherment, critical: true)); +#if SUPPORTS_TIME_PROVIDER + var now = (options.TimeProvider ?? provider.GetService())?.GetUtcNow() ?? DateTimeOffset.UtcNow; +#else + var now = DateTimeOffset.UtcNow; +#endif + using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser); + store.Open(OpenFlags.ReadWrite); - var certificate = request.CreateSelfSigned(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddYears(2)); + // Try to retrieve the existing development certificates from the specified store. + // If no valid existing certificate was found, create a new encryption certificate. + var certificates = store.Certificates + .Find(X509FindType.FindBySubjectDistinguishedName, subject.Name, validOnly: false) + .OfType() + .ToList(); - // Note: setting the friendly name is not supported on Unix machines (including Linux and macOS). - // To ensure an exception is not thrown by the property setter, an OS runtime check is used here. - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + if (!certificates.Exists(certificate => certificate.NotBefore < now.LocalDateTime && certificate.NotAfter > now.LocalDateTime)) { - certificate.FriendlyName = "OpenIddict Client Development Encryption Certificate"; - } +#if SUPPORTS_CERTIFICATE_GENERATION + using var algorithm = OpenIddictHelpers.CreateRsaKey(size: 2048); - // Note: CertificateRequest.CreateSelfSigned() doesn't mark the key set associated with the certificate - // as "persisted", which eventually prevents X509Store.Add() from correctly storing the private key. - // To work around this issue, the certificate payload is manually exported and imported back - // into a new X509Certificate2 instance specifying the X509KeyStorageFlags.PersistKeySet flag. - var data = certificate.Export(X509ContentType.Pfx, string.Empty); + var request = new CertificateRequest(subject, algorithm, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + request.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.KeyEncipherment, critical: true)); - try - { - var flags = X509KeyStorageFlags.PersistKeySet; + var certificate = request.CreateSelfSigned(now, now.AddYears(2)); - // Note: macOS requires marking the certificate private key as exportable. - // If this flag is not set, a CryptographicException is thrown at runtime. - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + // Note: setting the friendly name is not supported on Unix machines (including Linux and macOS). + // To ensure an exception is not thrown by the property setter, an OS runtime check is used here. + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - flags |= X509KeyStorageFlags.Exportable; + certificate.FriendlyName = "OpenIddict Client Development Encryption Certificate"; } - certificates.Insert(0, certificate = new X509Certificate2(data, string.Empty, flags)); - } + // Note: CertificateRequest.CreateSelfSigned() doesn't mark the key set associated with the certificate + // as "persisted", which eventually prevents X509Store.Add() from correctly storing the private key. + // To work around this issue, the certificate payload is manually exported and imported back + // into a new X509Certificate2 instance specifying the X509KeyStorageFlags.PersistKeySet flag. + var data = certificate.Export(X509ContentType.Pfx, string.Empty); - finally - { - Array.Clear(data, 0, data.Length); - } + try + { + var flags = X509KeyStorageFlags.PersistKeySet; + + // Note: macOS requires marking the certificate private key as exportable. + // If this flag is not set, a CryptographicException is thrown at runtime. + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + flags |= X509KeyStorageFlags.Exportable; + } + + certificates.Insert(0, certificate = new X509Certificate2(data, string.Empty, flags)); + } + + finally + { + Array.Clear(data, 0, data.Length); + } - store.Add(certificate); + store.Add(certificate); #else - throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0264)); + throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0264)); #endif - } + } - return Configure(options => options.EncryptionCredentials.AddRange( - from certificate in certificates - let key = new X509SecurityKey(certificate) - select new EncryptingCredentials(key, SecurityAlgorithms.RsaOAEP, SecurityAlgorithms.Aes256CbcHmacSha512))); + options.EncryptionCredentials.AddRange( + from certificate in certificates + let key = new X509SecurityKey(certificate) + select new EncryptingCredentials(key, SecurityAlgorithms.RsaOAEP, + SecurityAlgorithms.Aes256CbcHmacSha512)); + }); + + return this; } /// @@ -563,67 +575,78 @@ public OpenIddictClientBuilder AddDevelopmentSigningCertificate(X500Distinguishe throw new ArgumentNullException(nameof(subject)); } - using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser); - store.Open(OpenFlags.ReadWrite); - - // Try to retrieve the existing development certificates from the specified store. - // If no valid existing certificate was found, create a new signing certificate. - var certificates = store.Certificates.Find(X509FindType.FindBySubjectDistinguishedName, subject.Name, validOnly: false) - .OfType() - .ToList(); - - if (!certificates.Exists(static certificate => certificate.NotBefore < DateTime.Now && certificate.NotAfter > DateTime.Now)) + Services.AddOptions().Configure((options, provider) => { -#if SUPPORTS_CERTIFICATE_GENERATION - using var algorithm = OpenIddictHelpers.CreateRsaKey(size: 2048); - - var request = new CertificateRequest(subject, algorithm, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); - request.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature, critical: true)); +#if SUPPORTS_TIME_PROVIDER + var now = (options.TimeProvider ?? provider.GetService())?.GetUtcNow() ?? DateTimeOffset.UtcNow; +#else + var now = DateTimeOffset.UtcNow; +#endif + using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser); + store.Open(OpenFlags.ReadWrite); - var certificate = request.CreateSelfSigned(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddYears(2)); + // Try to retrieve the existing development certificates from the specified store. + // If no valid existing certificate was found, create a new signing certificate. + var certificates = store.Certificates + .Find(X509FindType.FindBySubjectDistinguishedName, subject.Name, validOnly: false) + .OfType() + .ToList(); - // Note: setting the friendly name is not supported on Unix machines (including Linux and macOS). - // To ensure an exception is not thrown by the property setter, an OS runtime check is used here. - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + if (!certificates.Exists(certificate => certificate.NotBefore < now.LocalDateTime && certificate.NotAfter > now.LocalDateTime)) { - certificate.FriendlyName = "OpenIddict Client Development Signing Certificate"; - } +#if SUPPORTS_CERTIFICATE_GENERATION + using var algorithm = OpenIddictHelpers.CreateRsaKey(size: 2048); - // Note: CertificateRequest.CreateSelfSigned() doesn't mark the key set associated with the certificate - // as "persisted", which eventually prevents X509Store.Add() from correctly storing the private key. - // To work around this issue, the certificate payload is manually exported and imported back - // into a new X509Certificate2 instance specifying the X509KeyStorageFlags.PersistKeySet flag. - var data = certificate.Export(X509ContentType.Pfx, string.Empty); + var request = new CertificateRequest(subject, algorithm, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + request.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature, critical: true)); - try - { - var flags = X509KeyStorageFlags.PersistKeySet; + var certificate = request.CreateSelfSigned(now, now.AddYears(2)); - // Note: macOS requires marking the certificate private key as exportable. - // If this flag is not set, a CryptographicException is thrown at runtime. - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + // Note: setting the friendly name is not supported on Unix machines (including Linux and macOS). + // To ensure an exception is not thrown by the property setter, an OS runtime check is used here. + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - flags |= X509KeyStorageFlags.Exportable; + certificate.FriendlyName = "OpenIddict Client Development Signing Certificate"; } - certificates.Insert(0, certificate = new X509Certificate2(data, string.Empty, flags)); - } + // Note: CertificateRequest.CreateSelfSigned() doesn't mark the key set associated with the certificate + // as "persisted", which eventually prevents X509Store.Add() from correctly storing the private key. + // To work around this issue, the certificate payload is manually exported and imported back + // into a new X509Certificate2 instance specifying the X509KeyStorageFlags.PersistKeySet flag. + var data = certificate.Export(X509ContentType.Pfx, string.Empty); - finally - { - Array.Clear(data, 0, data.Length); - } + try + { + var flags = X509KeyStorageFlags.PersistKeySet; + + // Note: macOS requires marking the certificate private key as exportable. + // If this flag is not set, a CryptographicException is thrown at runtime. + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + flags |= X509KeyStorageFlags.Exportable; + } + + certificates.Insert(0, certificate = new X509Certificate2(data, string.Empty, flags)); + } + + finally + { + Array.Clear(data, 0, data.Length); + } - store.Add(certificate); + store.Add(certificate); #else - throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0264)); + throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0264)); #endif - } + } - return Configure(options => options.SigningCredentials.AddRange( - from certificate in certificates - let key = new X509SecurityKey(certificate) - select new SigningCredentials(key, SecurityAlgorithms.RsaSha256))); + options.SigningCredentials.AddRange( + from certificate in certificates + let key = new X509SecurityKey(certificate) + select new SigningCredentials(key, SecurityAlgorithms.RsaSha256)); + }); + + return this; } /// diff --git a/src/OpenIddict.Client/OpenIddictClientConfiguration.cs b/src/OpenIddict.Client/OpenIddictClientConfiguration.cs index c0095f19b..24f446112 100644 --- a/src/OpenIddict.Client/OpenIddictClientConfiguration.cs +++ b/src/OpenIddict.Client/OpenIddictClientConfiguration.cs @@ -6,9 +6,11 @@ using System.ComponentModel; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Security.Cryptography; using System.Text; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Tokens; @@ -23,13 +25,26 @@ namespace OpenIddict.Client; public sealed class OpenIddictClientConfiguration : IPostConfigureOptions { private readonly OpenIddictClientService _service; + private readonly IServiceProvider _provider; /// /// Creates a new instance of the class. /// /// The OpenIddict client service. + [Obsolete("This constructor is no longer supported and will be removed in a future version.", error: true)] public OpenIddictClientConfiguration(OpenIddictClientService service) - => _service = service ?? throw new ArgumentNullException(nameof(service)); + => throw new NotSupportedException(SR.GetResourceString(SR.ID0403)); + + /// + /// Creates a new instance of the class. + /// + /// The service provider. + /// The OpenIddict client service. + public OpenIddictClientConfiguration(IServiceProvider provider, OpenIddictClientService service) + { + _provider = provider ?? throw new ArgumentNullException(nameof(provider)); + _service = service ?? throw new ArgumentNullException(nameof(service)); + } /// public void PostConfigure(string? name, OpenIddictClientOptions options) @@ -44,6 +59,10 @@ public void PostConfigure(string? name, OpenIddictClientOptions options) throw new InvalidOperationException(SR.GetResourceString(SR.ID0075)); } +#if SUPPORTS_TIME_PROVIDER + options.TimeProvider ??= _provider.GetService() ?? TimeProvider.System; +#endif + foreach (var registration in options.Registrations) { if (registration.Issuer is null) @@ -212,9 +231,16 @@ public void PostConfigure(string? name, OpenIddictClientOptions options) // Sort the handlers collection using the order associated with each handler. options.Handlers.Sort((left, right) => left.Order.CompareTo(right.Order)); + var now = ( +#if SUPPORTS_TIME_PROVIDER + options.TimeProvider?.GetUtcNow() ?? +#endif + DateTimeOffset.UtcNow + ).LocalDateTime; + // Sort the encryption and signing credentials. - options.EncryptionCredentials.Sort((left, right) => Compare(left.Key, right.Key)); - options.SigningCredentials.Sort((left, right) => Compare(left.Key, right.Key)); + options.EncryptionCredentials.Sort((left, right) => Compare(left.Key, right.Key, now)); + options.SigningCredentials.Sort((left, right) => Compare(left.Key, right.Key, now)); // Generate a key identifier for the encryption/signing keys that don't already have one. foreach (var key in options.EncryptionCredentials.Select(credentials => credentials.Key) @@ -234,7 +260,7 @@ public void PostConfigure(string? name, OpenIddictClientOptions options) from credentials in options.EncryptionCredentials select credentials.Key; - static int Compare(SecurityKey left, SecurityKey right) => (left, right) switch + static int Compare(SecurityKey left, SecurityKey right, DateTime now) => (left, right) switch { // If the two keys refer to the same instances, return 0. (SecurityKey first, SecurityKey second) when ReferenceEquals(first, second) => 0, @@ -245,8 +271,8 @@ public void PostConfigure(string? name, OpenIddictClientOptions options) (SecurityKey, SymmetricSecurityKey) => 1, // If one of the keys is backed by a X.509 certificate, don't prefer it if it's not valid yet. - (X509SecurityKey first, SecurityKey) when first.Certificate.NotBefore > DateTime.Now => 1, - (SecurityKey, X509SecurityKey second) when second.Certificate.NotBefore > DateTime.Now => -1, + (X509SecurityKey first, SecurityKey) when first.Certificate.NotBefore > now => 1, + (SecurityKey, X509SecurityKey second) when second.Certificate.NotBefore > now => -1, // If the two keys are backed by a X.509 certificate, prefer the one with the furthest expiration date. (X509SecurityKey first, X509SecurityKey second) => -first.Certificate.NotAfter.CompareTo(second.Certificate.NotAfter), diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.Introspection.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.Introspection.cs index 764828443..122297527 100644 --- a/src/OpenIddict.Client/OpenIddictClientHandlers.Introspection.cs +++ b/src/OpenIddict.Client/OpenIddictClientHandlers.Introspection.cs @@ -295,7 +295,11 @@ public ValueTask HandleAsync(HandleIntrospectionResponseContext context) if (long.TryParse((string?) context.Response[Claims.ExpiresAt], NumberStyles.Integer, CultureInfo.InvariantCulture, out var value) && DateTimeOffset.FromUnixTimeSeconds(value) is DateTimeOffset date && - date.Add(context.Registration.TokenValidationParameters.ClockSkew) < DateTimeOffset.UtcNow) + date.Add(context.Registration.TokenValidationParameters.ClockSkew) < ( +#if SUPPORTS_TIME_PROVIDER + context.Options.TimeProvider?.GetUtcNow() ?? +#endif + DateTimeOffset.UtcNow)) { context.Reject( error: Errors.ServerError, diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs index fc4cf664c..98618bacb 100644 --- a/src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs +++ b/src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs @@ -653,7 +653,11 @@ public ValueTask HandleAsync(ValidateTokenContext context) Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); var date = context.Principal.GetExpirationDate(); - if (date.HasValue && date.Value.Add(context.TokenValidationParameters.ClockSkew) < DateTimeOffset.UtcNow) + if (date.HasValue && date.Value.Add(context.TokenValidationParameters.ClockSkew) < ( +#if SUPPORTS_TIME_PROVIDER + context.Options.TimeProvider?.GetUtcNow() ?? +#endif + DateTimeOffset.UtcNow)) { context.Reject( error: Errors.InvalidToken, diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.cs index 998f43dc9..235ce0523 100644 --- a/src/OpenIddict.Client/OpenIddictClientHandlers.cs +++ b/src/OpenIddict.Client/OpenIddictClientHandlers.cs @@ -1494,8 +1494,11 @@ public ValueTask HandleAsync(ProcessAuthenticationContext context) context.FrontchannelAccessTokenExpirationDate = context.EndpointType switch { OpenIddictClientEndpointType.Redirection when context.ExtractFrontchannelAccessToken - => ((long?) context.Request[Parameters.ExpiresIn]) is long value ? - DateTimeOffset.UtcNow.AddSeconds(value) : null, + => (long?) context.Request[Parameters.ExpiresIn] is long value ? ( +#if SUPPORTS_TIME_PROVIDER + context.Options.TimeProvider?.GetUtcNow() ?? +#endif + DateTimeOffset.UtcNow).AddSeconds(value) : null, _ => null }; @@ -2471,7 +2474,11 @@ public ValueTask HandleAsync(ProcessAuthenticationContext context) nameType: Claims.Name, roleType: Claims.Role)); - principal.SetCreationDate(DateTimeOffset.UtcNow); + principal.SetCreationDate( +#if SUPPORTS_TIME_PROVIDER + context.Options.TimeProvider?.GetUtcNow() ?? +#endif + DateTimeOffset.UtcNow); var lifetime = context.Options.ClientAssertionLifetime; if (lifetime.HasValue) @@ -2849,7 +2856,13 @@ public ValueTask HandleAsync(ProcessAuthenticationContext context) context.BackchannelAccessTokenExpirationDate = context.ExtractBackchannelAccessToken && - context.TokenResponse.ExpiresIn is long value ? DateTimeOffset.UtcNow.AddSeconds(value) : null; + context.TokenResponse.ExpiresIn is long value + ? ( +#if SUPPORTS_TIME_PROVIDER + context.Options.TimeProvider?.GetUtcNow() ?? +#endif + DateTimeOffset.UtcNow).AddSeconds(value) + : null; context.BackchannelIdentityToken = context.ExtractBackchannelIdentityToken ? context.TokenResponse.IdToken : null; @@ -4741,7 +4754,7 @@ public ValueTask HandleAsync(ProcessChallengeContext context) // response mode as it offers a better protection for SPA applications. // Unfortunately, server-side clients like ASP.NET Core applications cannot // natively use response_mode=fragment as URI fragments are never sent to servers. - // + // // As such, this handler will not choose response_mode=fragment by default and it is // expected that specialized hosts like Blazor implement custom event handlers to // opt for fragment by default, if it is supported by the authorization server. @@ -5154,7 +5167,11 @@ public ValueTask HandleAsync(ProcessChallengeContext context) return true; }); - principal.SetCreationDate(DateTimeOffset.UtcNow); + principal.SetCreationDate( +#if SUPPORTS_TIME_PROVIDER + context.Options.TimeProvider?.GetUtcNow() ?? +#endif + DateTimeOffset.UtcNow); var lifetime = context.Principal.GetStateTokenLifetime() ?? context.Options.StateTokenLifetime; if (lifetime.HasValue) @@ -5335,7 +5352,7 @@ public ValueTask HandleAsync(ProcessChallengeContext context) // // Note: the nonce is always hashed before being sent, as recommended the specification. // See https://openid.net/specs/openid-connect-core-1_0.html#NonceNotes for more information. - if (context.Scopes.Contains(Scopes.OpenId) && !string.IsNullOrEmpty(context.Nonce) && + if (context.Scopes.Contains(Scopes.OpenId) && !string.IsNullOrEmpty(context.Nonce) && context.ResponseType?.Split(Separators.Space) is IList types && (types.Contains(ResponseTypes.Code) || types.Contains(ResponseTypes.IdToken))) { @@ -5571,7 +5588,11 @@ public ValueTask HandleAsync(ProcessChallengeContext context) nameType: Claims.Name, roleType: Claims.Role)); - principal.SetCreationDate(DateTimeOffset.UtcNow); + principal.SetCreationDate( +#if SUPPORTS_TIME_PROVIDER + context.Options.TimeProvider?.GetUtcNow() ?? +#endif + DateTimeOffset.UtcNow); var lifetime = context.Options.ClientAssertionLifetime; if (lifetime.HasValue) @@ -5824,7 +5845,7 @@ public ValueTask HandleAsync(ProcessChallengeContext context) // validated by default. Clients that need to deal with non-standard implementations // can use custom handlers to validate user codes that use a readable format (e.g JWT). GrantTypes.DeviceCode => (true, true, false, false), - + _ => (false, false, false, false) }; @@ -6239,7 +6260,11 @@ public ValueTask HandleAsync(ProcessIntrospectionContext context) nameType: Claims.Name, roleType: Claims.Role)); - principal.SetCreationDate(DateTimeOffset.UtcNow); + principal.SetCreationDate( +#if SUPPORTS_TIME_PROVIDER + context.Options.TimeProvider?.GetUtcNow() ?? +#endif + DateTimeOffset.UtcNow); var lifetime = context.Options.ClientAssertionLifetime; if (lifetime.HasValue) @@ -6831,7 +6856,11 @@ public ValueTask HandleAsync(ProcessRevocationContext context) nameType: Claims.Name, roleType: Claims.Role)); - principal.SetCreationDate(DateTimeOffset.UtcNow); + principal.SetCreationDate( +#if SUPPORTS_TIME_PROVIDER + context.Options.TimeProvider?.GetUtcNow() ?? +#endif + DateTimeOffset.UtcNow); var lifetime = context.Options.ClientAssertionLifetime; if (lifetime.HasValue) @@ -7463,7 +7492,11 @@ public ValueTask HandleAsync(ProcessSignOutContext context) return true; }); - principal.SetCreationDate(DateTimeOffset.UtcNow); + principal.SetCreationDate( +#if SUPPORTS_TIME_PROVIDER + context.Options.TimeProvider?.GetUtcNow() ?? +#endif + DateTimeOffset.UtcNow); var lifetime = context.Principal.GetStateTokenLifetime() ?? context.Options.StateTokenLifetime; if (lifetime.HasValue) diff --git a/src/OpenIddict.Client/OpenIddictClientOptions.cs b/src/OpenIddict.Client/OpenIddictClientOptions.cs index 0e718f6e2..4b86e7c94 100644 --- a/src/OpenIddict.Client/OpenIddictClientOptions.cs +++ b/src/OpenIddict.Client/OpenIddictClientOptions.cs @@ -157,4 +157,11 @@ public sealed class OpenIddictClientOptions /// [EditorBrowsable(EditorBrowsableState.Advanced)] public HashSet ResponseTypes { get; } = new(StringComparer.Ordinal); + +#if SUPPORTS_TIME_PROVIDER + /// + /// Gets or sets the time provider. + /// + public TimeProvider? TimeProvider { get; set; } +#endif } diff --git a/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs b/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs index b3f5cfb34..0ef781714 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs @@ -232,7 +232,11 @@ await foreach (var result in ValidateAsync(authorization, cancellationToken)) var descriptor = new OpenIddictAuthorizationDescriptor { ApplicationId = client, - CreationDate = DateTimeOffset.UtcNow, + CreationDate = +#if SUPPORTS_TIME_PROVIDER + Options.CurrentValue.TimeProvider?.GetUtcNow() ?? +#endif + DateTimeOffset.UtcNow, Principal = principal, Status = Statuses.Valid, Subject = subject, diff --git a/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs b/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs index 26a8cb6f9..aba9da05b 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs @@ -1088,7 +1088,11 @@ public virtual async ValueTask TryRedeemAsync(TToken token, CancellationTo // the first time the token is redeemed. In this case, attach the current date. if (await Store.GetRedemptionDateAsync(token, cancellationToken) is null) { - await Store.SetRedemptionDateAsync(token, DateTimeOffset.UtcNow, cancellationToken); + await Store.SetRedemptionDateAsync(token, +#if SUPPORTS_TIME_PROVIDER + Options.CurrentValue.TimeProvider?.GetUtcNow() ?? +#endif + DateTimeOffset.UtcNow, cancellationToken); } await Store.SetStatusAsync(token, Statuses.Redeemed, cancellationToken); diff --git a/src/OpenIddict.Core/OpenIddictCoreConfiguration.cs b/src/OpenIddict.Core/OpenIddictCoreConfiguration.cs new file mode 100644 index 000000000..2c655e495 --- /dev/null +++ b/src/OpenIddict.Core/OpenIddictCoreConfiguration.cs @@ -0,0 +1,33 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace OpenIddict.Core; + +/// +/// Contains the methods required to ensure that the OpenIddict core configuration is valid. +/// +public class OpenIddictCoreConfiguration : IPostConfigureOptions +{ + private readonly IServiceProvider _provider; + + /// + /// Creates a new instance of the class. + /// + /// The service provider. + public OpenIddictCoreConfiguration(IServiceProvider provider) + => _provider = provider ?? throw new ArgumentNullException(nameof(provider)); + + /// + public void PostConfigure(string? name, OpenIddictCoreOptions options) + { +#if SUPPORTS_TIME_PROVIDER + options.TimeProvider ??= _provider.GetService() ?? TimeProvider.System; +#endif + } +} diff --git a/src/OpenIddict.Core/OpenIddictCoreExtensions.cs b/src/OpenIddict.Core/OpenIddictCoreExtensions.cs index 484e6d234..7c85a5ea1 100644 --- a/src/OpenIddict.Core/OpenIddictCoreExtensions.cs +++ b/src/OpenIddict.Core/OpenIddictCoreExtensions.cs @@ -88,6 +88,10 @@ public static OpenIddictCoreBuilder AddCore(this OpenIddictBuilder builder) typeof(OpenIddictTokenManager<>).MakeGenericType(type)); }); + // Note: TryAddEnumerable() is used here to ensure the initializer is registered only once. + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton< + IPostConfigureOptions, OpenIddictCoreConfiguration>()); + return new OpenIddictCoreBuilder(builder.Services); } diff --git a/src/OpenIddict.Core/OpenIddictCoreOptions.cs b/src/OpenIddict.Core/OpenIddictCoreOptions.cs index 0ca197349..0adda6d88 100644 --- a/src/OpenIddict.Core/OpenIddictCoreOptions.cs +++ b/src/OpenIddict.Core/OpenIddictCoreOptions.cs @@ -59,4 +59,11 @@ public sealed class OpenIddictCoreOptions /// This property is not used when is . /// public int EntityCacheLimit { get; set; } = 250; + +#if SUPPORTS_TIME_PROVIDER + /// + /// Gets or sets the time provider. + /// + public TimeProvider? TimeProvider { get; set; } +#endif } diff --git a/src/OpenIddict.Quartz/OpenIddictQuartzConfiguration.cs b/src/OpenIddict.Quartz/OpenIddictQuartzConfiguration.cs index e6f9bc0cd..ccd9c113e 100644 --- a/src/OpenIddict.Quartz/OpenIddictQuartzConfiguration.cs +++ b/src/OpenIddict.Quartz/OpenIddictQuartzConfiguration.cs @@ -5,6 +5,7 @@ */ using System.ComponentModel; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; namespace OpenIddict.Quartz; @@ -13,8 +14,23 @@ namespace OpenIddict.Quartz; /// Contains the methods required to ensure that the OpenIddict Quartz.NET configuration is valid. /// [EditorBrowsable(EditorBrowsableState.Advanced)] -public sealed class OpenIddictQuartzConfiguration : IConfigureOptions +public sealed class OpenIddictQuartzConfiguration : IConfigureOptions, IPostConfigureOptions { + private readonly IServiceProvider _provider; + + /// + /// Creates a new instance of the class. + /// + [Obsolete("This constructor is no longer supported and will be removed in a future version.", error: true)] + public OpenIddictQuartzConfiguration() => throw new NotSupportedException(SR.GetResourceString(SR.ID0403)); + + /// + /// Creates a new instance of the class. + /// + /// The service provider. + public OpenIddictQuartzConfiguration(IServiceProvider provider) + => _provider = provider ?? throw new ArgumentNullException(nameof(provider)); + /// public void Configure(QuartzOptions options) { @@ -42,4 +58,12 @@ public void Configure(QuartzOptions options) .StartAt(DateBuilder.FutureDate(new Random().Next(1, 10), IntervalUnit.Minute)); }); } + + /// + public void PostConfigure(string? name, OpenIddictQuartzOptions options) + { +#if SUPPORTS_TIME_PROVIDER + options.TimeProvider ??= _provider.GetService() ?? TimeProvider.System; +#endif + } } diff --git a/src/OpenIddict.Quartz/OpenIddictQuartzExtensions.cs b/src/OpenIddict.Quartz/OpenIddictQuartzExtensions.cs index bd80cfd3b..c42e96ab2 100644 --- a/src/OpenIddict.Quartz/OpenIddictQuartzExtensions.cs +++ b/src/OpenIddict.Quartz/OpenIddictQuartzExtensions.cs @@ -34,9 +34,12 @@ public static OpenIddictQuartzBuilder UseQuartz(this OpenIddictCoreBuilder build // Quartz.NET's DI integration to resolve it from the DI. builder.Services.TryAddTransient(); - // Note: TryAddEnumerable() is used here to ensure the initializer is registered only once. - builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton< - IConfigureOptions, OpenIddictQuartzConfiguration>()); + // Note: TryAddEnumerable() is used here to ensure the initializers are registered only once. + builder.Services.TryAddEnumerable( + [ + ServiceDescriptor.Singleton, OpenIddictQuartzConfiguration>(), + ServiceDescriptor.Singleton, OpenIddictQuartzConfiguration>() + ]); return new OpenIddictQuartzBuilder(builder.Services); } diff --git a/src/OpenIddict.Quartz/OpenIddictQuartzJob.cs b/src/OpenIddict.Quartz/OpenIddictQuartzJob.cs index 9e7a15c72..9048e48aa 100644 --- a/src/OpenIddict.Quartz/OpenIddictQuartzJob.cs +++ b/src/OpenIddict.Quartz/OpenIddictQuartzJob.cs @@ -74,7 +74,11 @@ public async Task Execute(IJobExecutionContext context) UnscheduleFiringTrigger = true }; - var threshold = DateTimeOffset.UtcNow - _options.CurrentValue.MinimumTokenLifespan; + var threshold = ( +#if SUPPORTS_TIME_PROVIDER + _options.CurrentValue.TimeProvider?.GetUtcNow() ?? +#endif + DateTimeOffset.UtcNow) - _options.CurrentValue.MinimumTokenLifespan; try { @@ -119,7 +123,11 @@ public async Task Execute(IJobExecutionContext context) UnscheduleFiringTrigger = true }; - var threshold = DateTimeOffset.UtcNow - _options.CurrentValue.MinimumAuthorizationLifespan; + var threshold = ( +#if SUPPORTS_TIME_PROVIDER + _options.CurrentValue.TimeProvider?.GetUtcNow() ?? +#endif + DateTimeOffset.UtcNow) - _options.CurrentValue.MinimumAuthorizationLifespan; try { diff --git a/src/OpenIddict.Quartz/OpenIddictQuartzOptions.cs b/src/OpenIddict.Quartz/OpenIddictQuartzOptions.cs index e8f0021b0..8cc176594 100644 --- a/src/OpenIddict.Quartz/OpenIddictQuartzOptions.cs +++ b/src/OpenIddict.Quartz/OpenIddictQuartzOptions.cs @@ -38,4 +38,11 @@ public sealed class OpenIddictQuartzOptions /// By default, this value is set to 14 days and cannot be less than 10 minutes. /// public TimeSpan MinimumTokenLifespan { get; set; } = TimeSpan.FromDays(14); + +#if SUPPORTS_TIME_PROVIDER + /// + /// Gets or sets the time provider. + /// + public TimeProvider? TimeProvider { get; set; } +#endif } diff --git a/src/OpenIddict.Server/OpenIddictServerBuilder.cs b/src/OpenIddict.Server/OpenIddictServerBuilder.cs index 624c0e96f..6e15a62a0 100644 --- a/src/OpenIddict.Server/OpenIddictServerBuilder.cs +++ b/src/OpenIddict.Server/OpenIddictServerBuilder.cs @@ -209,67 +209,79 @@ public OpenIddictServerBuilder AddDevelopmentEncryptionCertificate(X500Distingui throw new ArgumentNullException(nameof(subject)); } - using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser); - store.Open(OpenFlags.ReadWrite); - - // Try to retrieve the existing development certificates from the specified store. - // If no valid existing certificate was found, create a new encryption certificate. - var certificates = store.Certificates.Find(X509FindType.FindBySubjectDistinguishedName, subject.Name, validOnly: false) - .OfType() - .ToList(); - - if (!certificates.Exists(static certificate => certificate.NotBefore < DateTime.Now && certificate.NotAfter > DateTime.Now)) + Services.AddOptions().Configure((options, provider) => { -#if SUPPORTS_CERTIFICATE_GENERATION - using var algorithm = OpenIddictHelpers.CreateRsaKey(size: 2048); - - var request = new CertificateRequest(subject, algorithm, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); - request.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.KeyEncipherment, critical: true)); +#if SUPPORTS_TIME_PROVIDER + var now = (options.TimeProvider ?? provider.GetService())?.GetUtcNow() ?? DateTimeOffset.UtcNow; +#else + var now = DateTimeOffset.UtcNow; +#endif + using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser); + store.Open(OpenFlags.ReadWrite); - var certificate = request.CreateSelfSigned(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddYears(2)); + // Try to retrieve the existing development certificates from the specified store. + // If no valid existing certificate was found, create a new encryption certificate. + var certificates = store.Certificates + .Find(X509FindType.FindBySubjectDistinguishedName, subject.Name, validOnly: false) + .OfType() + .ToList(); - // Note: setting the friendly name is not supported on Unix machines (including Linux and macOS). - // To ensure an exception is not thrown by the property setter, an OS runtime check is used here. - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + if (!certificates.Exists(certificate => certificate.NotBefore < now.LocalDateTime && certificate.NotAfter > now.LocalDateTime)) { - certificate.FriendlyName = "OpenIddict Server Development Encryption Certificate"; - } +#if SUPPORTS_CERTIFICATE_GENERATION + using var algorithm = OpenIddictHelpers.CreateRsaKey(size: 2048); - // Note: CertificateRequest.CreateSelfSigned() doesn't mark the key set associated with the certificate - // as "persisted", which eventually prevents X509Store.Add() from correctly storing the private key. - // To work around this issue, the certificate payload is manually exported and imported back - // into a new X509Certificate2 instance specifying the X509KeyStorageFlags.PersistKeySet flag. - var data = certificate.Export(X509ContentType.Pfx, string.Empty); + var request = new CertificateRequest(subject, algorithm, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + request.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.KeyEncipherment, critical: true)); - try - { - var flags = X509KeyStorageFlags.PersistKeySet; + var certificate = request.CreateSelfSigned(now, now.AddYears(2)); - // Note: macOS requires marking the certificate private key as exportable. - // If this flag is not set, a CryptographicException is thrown at runtime. - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + // Note: setting the friendly name is not supported on Unix machines (including Linux and macOS). + // To ensure an exception is not thrown by the property setter, an OS runtime check is used here. + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - flags |= X509KeyStorageFlags.Exportable; + certificate.FriendlyName = "OpenIddict Server Development Encryption Certificate"; } - certificates.Insert(0, certificate = new X509Certificate2(data, string.Empty, flags)); - } + // Note: CertificateRequest.CreateSelfSigned() doesn't mark the key set associated with the certificate + // as "persisted", which eventually prevents X509Store.Add() from correctly storing the private key. + // To work around this issue, the certificate payload is manually exported and imported back + // into a new X509Certificate2 instance specifying the X509KeyStorageFlags.PersistKeySet flag. + var data = certificate.Export(X509ContentType.Pfx, string.Empty); - finally - { - Array.Clear(data, 0, data.Length); - } + try + { + var flags = X509KeyStorageFlags.PersistKeySet; + + // Note: macOS requires marking the certificate private key as exportable. + // If this flag is not set, a CryptographicException is thrown at runtime. + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + flags |= X509KeyStorageFlags.Exportable; + } + + certificates.Insert(0, certificate = new X509Certificate2(data, string.Empty, flags)); + } + + finally + { + Array.Clear(data, 0, data.Length); + } - store.Add(certificate); + store.Add(certificate); #else - throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0264)); + throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0264)); #endif - } + } + + options.EncryptionCredentials.AddRange( + from certificate in certificates + let key = new X509SecurityKey(certificate) + select new EncryptingCredentials(key, SecurityAlgorithms.RsaOAEP, + SecurityAlgorithms.Aes256CbcHmacSha512)); + }); - return Configure(options => options.EncryptionCredentials.AddRange( - from certificate in certificates - let key = new X509SecurityKey(certificate) - select new EncryptingCredentials(key, SecurityAlgorithms.RsaOAEP, SecurityAlgorithms.Aes256CbcHmacSha512))); + return this; } /// @@ -572,67 +584,79 @@ public OpenIddictServerBuilder AddDevelopmentSigningCertificate(X500Distinguishe throw new ArgumentNullException(nameof(subject)); } - using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser); - store.Open(OpenFlags.ReadWrite); - - // Try to retrieve the existing development certificates from the specified store. - // If no valid existing certificate was found, create a new signing certificate. - var certificates = store.Certificates.Find(X509FindType.FindBySubjectDistinguishedName, subject.Name, validOnly: false) - .OfType() - .ToList(); - - if (!certificates.Exists(static certificate => certificate.NotBefore < DateTime.Now && certificate.NotAfter > DateTime.Now)) + Services.AddOptions().Configure((options, provider) => { -#if SUPPORTS_CERTIFICATE_GENERATION - using var algorithm = OpenIddictHelpers.CreateRsaKey(size: 2048); - - var request = new CertificateRequest(subject, algorithm, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); - request.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature, critical: true)); +#if SUPPORTS_TIME_PROVIDER + var now = (options.TimeProvider ?? provider.GetService())?.GetUtcNow() ?? DateTimeOffset.UtcNow; +#else + var now = DateTimeOffset.UtcNow; +#endif + using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser); + store.Open(OpenFlags.ReadWrite); - var certificate = request.CreateSelfSigned(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddYears(2)); + // Try to retrieve the existing development certificates from the specified store. + // If no valid existing certificate was found, create a new signing certificate. + var certificates = store.Certificates + .Find(X509FindType.FindBySubjectDistinguishedName, subject.Name, validOnly: false) + .OfType() + .ToList(); - // Note: setting the friendly name is not supported on Unix machines (including Linux and macOS). - // To ensure an exception is not thrown by the property setter, an OS runtime check is used here. - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + if (!certificates.Exists(certificate => + certificate.NotBefore < now.LocalDateTime && certificate.NotAfter > now.LocalDateTime)) { - certificate.FriendlyName = "OpenIddict Server Development Signing Certificate"; - } +#if SUPPORTS_CERTIFICATE_GENERATION + using var algorithm = OpenIddictHelpers.CreateRsaKey(size: 2048); - // Note: CertificateRequest.CreateSelfSigned() doesn't mark the key set associated with the certificate - // as "persisted", which eventually prevents X509Store.Add() from correctly storing the private key. - // To work around this issue, the certificate payload is manually exported and imported back - // into a new X509Certificate2 instance specifying the X509KeyStorageFlags.PersistKeySet flag. - var data = certificate.Export(X509ContentType.Pfx, string.Empty); + var request = new CertificateRequest(subject, algorithm, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + request.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature, critical: true)); - try - { - var flags = X509KeyStorageFlags.PersistKeySet; + var certificate = request.CreateSelfSigned(now, now.AddYears(2)); - // Note: macOS requires marking the certificate private key as exportable. - // If this flag is not set, a CryptographicException is thrown at runtime. - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + // Note: setting the friendly name is not supported on Unix machines (including Linux and macOS). + // To ensure an exception is not thrown by the property setter, an OS runtime check is used here. + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - flags |= X509KeyStorageFlags.Exportable; + certificate.FriendlyName = "OpenIddict Server Development Signing Certificate"; } - certificates.Insert(0, certificate = new X509Certificate2(data, string.Empty, flags)); - } + // Note: CertificateRequest.CreateSelfSigned() doesn't mark the key set associated with the certificate + // as "persisted", which eventually prevents X509Store.Add() from correctly storing the private key. + // To work around this issue, the certificate payload is manually exported and imported back + // into a new X509Certificate2 instance specifying the X509KeyStorageFlags.PersistKeySet flag. + var data = certificate.Export(X509ContentType.Pfx, string.Empty); - finally - { - Array.Clear(data, 0, data.Length); - } + try + { + var flags = X509KeyStorageFlags.PersistKeySet; - store.Add(certificate); + // Note: macOS requires marking the certificate private key as exportable. + // If this flag is not set, a CryptographicException is thrown at runtime. + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + flags |= X509KeyStorageFlags.Exportable; + } + + certificates.Insert(0, certificate = new X509Certificate2(data, string.Empty, flags)); + } + + finally + { + Array.Clear(data, 0, data.Length); + } + + store.Add(certificate); #else - throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0264)); + throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0264)); #endif - } + } + + options.SigningCredentials.AddRange( + from certificate in certificates + let key = new X509SecurityKey(certificate) + select new SigningCredentials(key, SecurityAlgorithms.RsaSha256)); + }); - return Configure(options => options.SigningCredentials.AddRange( - from certificate in certificates - let key = new X509SecurityKey(certificate) - select new SigningCredentials(key, SecurityAlgorithms.RsaSha256))); + return this; } /// @@ -1627,7 +1651,7 @@ public OpenIddictServerBuilder RegisterScopes(params string[] scopes) { throw new ArgumentNullException(nameof(scopes)); } - + if (Array.Exists(scopes, string.IsNullOrEmpty)) { throw new ArgumentException(SR.GetResourceString(SR.ID0074), nameof(scopes)); diff --git a/src/OpenIddict.Server/OpenIddictServerConfiguration.cs b/src/OpenIddict.Server/OpenIddictServerConfiguration.cs index cdeac1840..970b56c10 100644 --- a/src/OpenIddict.Server/OpenIddictServerConfiguration.cs +++ b/src/OpenIddict.Server/OpenIddictServerConfiguration.cs @@ -9,6 +9,7 @@ using System.Globalization; using System.Runtime.InteropServices; using System.Text; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using OpenIddict.Extensions; @@ -21,6 +22,21 @@ namespace OpenIddict.Server; [EditorBrowsable(EditorBrowsableState.Advanced)] public sealed class OpenIddictServerConfiguration : IPostConfigureOptions { + private readonly IServiceProvider _provider; + + /// + /// Creates a new instance of the class. + /// + [Obsolete("This constructor is no longer supported and will be removed in a future version.", error: true)] + public OpenIddictServerConfiguration() => throw new NotSupportedException(SR.GetResourceString(SR.ID0403)); + + /// + /// Creates a new instance of the class. + /// + /// The service provider. + public OpenIddictServerConfiguration(IServiceProvider provider) + => _provider = provider ?? throw new ArgumentNullException(nameof(provider)); + /// public void PostConfigure(string? name, OpenIddictServerOptions options) { @@ -29,6 +45,10 @@ public void PostConfigure(string? name, OpenIddictServerOptions options) throw new ArgumentNullException(nameof(options)); } +#if SUPPORTS_TIME_PROVIDER + options.TimeProvider ??= _provider.GetService() ?? TimeProvider.System; +#endif + // Explicitly disable all the features that are implicitly excluded when the degraded mode is active. if (options.EnableDegradedMode) { @@ -183,16 +203,24 @@ public void PostConfigure(string? name, OpenIddictServerOptions options) throw new InvalidOperationException(SR.GetResourceString(SR.ID0086)); } + var now = ( +#if SUPPORTS_TIME_PROVIDER + options.TimeProvider?.GetUtcNow() ?? +#endif + DateTimeOffset.UtcNow + ) + .LocalDateTime; + // If all the registered encryption credentials are backed by a X.509 certificate, at least one of them must be valid. - if (options.EncryptionCredentials.TrueForAll(static credentials => credentials.Key is X509SecurityKey x509SecurityKey && - (x509SecurityKey.Certificate.NotBefore > DateTime.Now || x509SecurityKey.Certificate.NotAfter < DateTime.Now))) + if (options.EncryptionCredentials.TrueForAll(credentials => credentials.Key is X509SecurityKey x509SecurityKey && + (x509SecurityKey.Certificate.NotBefore > now || x509SecurityKey.Certificate.NotAfter < now))) { throw new InvalidOperationException(SR.GetResourceString(SR.ID0087)); } // If all the registered signing credentials are backed by a X.509 certificate, at least one of them must be valid. - if (options.SigningCredentials.TrueForAll(static credentials => credentials.Key is X509SecurityKey x509SecurityKey && - (x509SecurityKey.Certificate.NotBefore > DateTime.Now || x509SecurityKey.Certificate.NotAfter < DateTime.Now))) + if (options.SigningCredentials.TrueForAll(credentials => credentials.Key is X509SecurityKey x509SecurityKey && + (x509SecurityKey.Certificate.NotBefore > now || x509SecurityKey.Certificate.NotAfter < now))) { throw new InvalidOperationException(SR.GetResourceString(SR.ID0088)); } @@ -368,8 +396,8 @@ public void PostConfigure(string? name, OpenIddictServerOptions options) options.Handlers.Sort(static (left, right) => left.Order.CompareTo(right.Order)); // Sort the encryption and signing credentials. - options.EncryptionCredentials.Sort(static (left, right) => Compare(left.Key, right.Key)); - options.SigningCredentials.Sort(static (left, right) => Compare(left.Key, right.Key)); + options.EncryptionCredentials.Sort((left, right) => Compare(left.Key, right.Key, now)); + options.SigningCredentials.Sort((left, right) => Compare(left.Key, right.Key, now)); // Generate a key identifier for the encryption/signing keys that don't already have one. foreach (var key in options.EncryptionCredentials.Select(credentials => credentials.Key) @@ -389,7 +417,7 @@ public void PostConfigure(string? name, OpenIddictServerOptions options) from credentials in options.EncryptionCredentials select credentials.Key; - static int Compare(SecurityKey left, SecurityKey right) => (left, right) switch + static int Compare(SecurityKey left, SecurityKey right, DateTime now) => (left, right) switch { // If the two keys refer to the same instances, return 0. (SecurityKey first, SecurityKey second) when ReferenceEquals(first, second) => 0, @@ -400,8 +428,8 @@ public void PostConfigure(string? name, OpenIddictServerOptions options) (SecurityKey, SymmetricSecurityKey) => 1, // If one of the keys is backed by a X.509 certificate, don't prefer it if it's not valid yet. - (X509SecurityKey first, SecurityKey) when first.Certificate.NotBefore > DateTime.Now => 1, - (SecurityKey, X509SecurityKey second) when second.Certificate.NotBefore > DateTime.Now => -1, + (X509SecurityKey first, SecurityKey) when first.Certificate.NotBefore > now => 1, + (SecurityKey, X509SecurityKey second) when second.Certificate.NotBefore > now => -1, // If the two keys are backed by a X.509 certificate, prefer the one with the furthest expiration date. (X509SecurityKey first, X509SecurityKey second) => -first.Certificate.NotAfter.CompareTo(second.Certificate.NotAfter), diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs index d7f5f668f..07bd994ce 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs @@ -908,7 +908,11 @@ public ValueTask HandleAsync(ValidateTokenContext context) Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); var date = context.Principal.GetExpirationDate(); - if (date.HasValue && date.Value.Add(context.TokenValidationParameters.ClockSkew) < DateTimeOffset.UtcNow) + if (date.HasValue && date.Value.Add(context.TokenValidationParameters.ClockSkew) < ( +#if SUPPORTS_TIME_PROVIDER + context.Options.TimeProvider?.GetUtcNow() ?? +#endif + DateTimeOffset.UtcNow)) { context.Reject( error: context.Principal.GetTokenType() switch @@ -1110,7 +1114,11 @@ async ValueTask IsReusableAsync(object token) } var date = await _tokenManager.GetRedemptionDateAsync(token); - if (date is null || DateTimeOffset.UtcNow < date + context.Options.RefreshTokenReuseLeeway) + if (date is null || ( +#if SUPPORTS_TIME_PROVIDER + context.Options.TimeProvider?.GetUtcNow() ?? +#endif + DateTimeOffset.UtcNow) < date + context.Options.RefreshTokenReuseLeeway) { return true; } @@ -1404,7 +1412,7 @@ public ValueTask HandleAsync(GenerateTokenContext context) // entry to the token) that can be used by the resource servers to determine // whether an access token has already been used or blacklist them if necessary. // - // Note: scopes are deliberately formatted as a single space-separated + // Note: scopes are deliberately formatted as a single space-separated // string to respect the usual representation of the standard scope claim. // // See https://datatracker.ietf.org/doc/html/rfc9068 for more information. @@ -1421,7 +1429,7 @@ public ValueTask HandleAsync(GenerateTokenContext context) // For authorization/device/user codes and refresh tokens, // attach claims destinations to the JWT claims collection. - if (context.TokenType is TokenTypeHints.AuthorizationCode or TokenTypeHints.DeviceCode or + if (context.TokenType is TokenTypeHints.AuthorizationCode or TokenTypeHints.DeviceCode or TokenTypeHints.RefreshToken or TokenTypeHints.UserCode) { var destinations = principal.GetDestinations(); diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.cs index 9df9fd883..8e22962bf 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.cs @@ -104,7 +104,7 @@ public static partial class OpenIddictServerHandlers */ ValidateSignOutDemand.Descriptor, AttachCustomSignOutParameters.Descriptor, - + /* * Error processing: */ @@ -2747,7 +2747,11 @@ public async ValueTask HandleAsync(ProcessSignInContext context) var descriptor = new OpenIddictAuthorizationDescriptor { - CreationDate = DateTimeOffset.UtcNow, + CreationDate = +#if SUPPORTS_TIME_PROVIDER + context.Options.TimeProvider?.GetUtcNow() ?? +#endif + DateTimeOffset.UtcNow, Principal = context.Principal, Status = Statuses.Valid, Subject = context.Principal.GetClaim(Claims.Subject), @@ -2887,7 +2891,11 @@ public async ValueTask HandleAsync(ProcessSignInContext context) claim.Properties.Remove(Properties.Destinations); } - principal.SetCreationDate(DateTimeOffset.UtcNow); + principal.SetCreationDate( +#if SUPPORTS_TIME_PROVIDER + context.Options.TimeProvider?.GetUtcNow() ?? +#endif + DateTimeOffset.UtcNow); // If a specific token lifetime was attached to the principal, prefer it over any other value. var lifetime = context.Principal.GetAccessTokenLifetime(); @@ -3011,7 +3019,11 @@ public async ValueTask HandleAsync(ProcessSignInContext context) return true; }); - principal.SetCreationDate(DateTimeOffset.UtcNow); + principal.SetCreationDate( +#if SUPPORTS_TIME_PROVIDER + context.Options.TimeProvider?.GetUtcNow() ?? +#endif + DateTimeOffset.UtcNow); // If a specific token lifetime was attached to the principal, prefer it over any other value. var lifetime = context.Principal.GetAuthorizationCodeLifetime(); @@ -3137,7 +3149,11 @@ public async ValueTask HandleAsync(ProcessSignInContext context) return true; }); - principal.SetCreationDate(DateTimeOffset.UtcNow); + principal.SetCreationDate( +#if SUPPORTS_TIME_PROVIDER + context.Options.TimeProvider?.GetUtcNow() ?? +#endif + DateTimeOffset.UtcNow); // If a specific token lifetime was attached to the principal, prefer it over any other value. var lifetime = context.Principal.GetDeviceCodeLifetime(); @@ -3250,7 +3266,11 @@ public async ValueTask HandleAsync(ProcessSignInContext context) return true; }); - principal.SetCreationDate(DateTimeOffset.UtcNow); + principal.SetCreationDate( +#if SUPPORTS_TIME_PROVIDER + context.Options.TimeProvider?.GetUtcNow() ?? +#endif + DateTimeOffset.UtcNow); // When sliding expiration is disabled, the expiration date of generated refresh tokens is fixed // and must exactly match the expiration date of the refresh token used in the token request. @@ -3401,7 +3421,11 @@ public async ValueTask HandleAsync(ProcessSignInContext context) claim.Properties.Remove(Properties.Destinations); } - principal.SetCreationDate(DateTimeOffset.UtcNow); + principal.SetCreationDate( +#if SUPPORTS_TIME_PROVIDER + context.Options.TimeProvider?.GetUtcNow() ?? +#endif + DateTimeOffset.UtcNow); // If a specific token lifetime was attached to the principal, prefer it over any other value. var lifetime = context.Principal.GetIdentityTokenLifetime(); @@ -3526,7 +3550,11 @@ public async ValueTask HandleAsync(ProcessSignInContext context) return true; }); - principal.SetCreationDate(DateTimeOffset.UtcNow); + principal.SetCreationDate( +#if SUPPORTS_TIME_PROVIDER + context.Options.TimeProvider?.GetUtcNow() ?? +#endif + DateTimeOffset.UtcNow); // If a specific token lifetime was attached to the principal, prefer it over any other value. var lifetime = context.Principal.GetUserCodeLifetime(); @@ -4288,9 +4316,17 @@ public ValueTask HandleAsync(ProcessSignInContext context) { // If an expiration date was set on the access token principal, return it to the client application. if (context.AccessTokenPrincipal.GetExpirationDate() - is DateTimeOffset date && date > DateTimeOffset.UtcNow) + is DateTimeOffset date && date > ( +#if SUPPORTS_TIME_PROVIDER + context.Options.TimeProvider?.GetUtcNow() ?? +#endif + DateTimeOffset.UtcNow)) { - context.Response.ExpiresIn = (long) ((date - DateTimeOffset.UtcNow).TotalSeconds + .5); + context.Response.ExpiresIn = (long) ((date - ( +#if SUPPORTS_TIME_PROVIDER + context.Options.TimeProvider?.GetUtcNow() ?? +#endif + DateTimeOffset.UtcNow)).TotalSeconds + .5); } // If the granted access token scopes differ from the requested scopes, return the granted scopes @@ -4352,8 +4388,16 @@ public ValueTask HandleAsync(ProcessSignInContext context) { // If an expiration date was set on the device code or user // code principal, return it to the client application. - DateTimeOffset date when date > DateTimeOffset.UtcNow - => (long) ((date - DateTimeOffset.UtcNow).TotalSeconds + .5), + DateTimeOffset date when date > ( +#if SUPPORTS_TIME_PROVIDER + context.Options.TimeProvider?.GetUtcNow() ?? +#endif + DateTimeOffset.UtcNow) + => (long) ((date - ( +#if SUPPORTS_TIME_PROVIDER + context.Options.TimeProvider?.GetUtcNow() ?? +#endif + DateTimeOffset.UtcNow)).TotalSeconds + .5), // Otherwise, return an arbitrary value, as the "expires_in" // parameter is required in device authorization responses. diff --git a/src/OpenIddict.Server/OpenIddictServerOptions.cs b/src/OpenIddict.Server/OpenIddictServerOptions.cs index ae2a22614..cf9b7d7d2 100644 --- a/src/OpenIddict.Server/OpenIddictServerOptions.cs +++ b/src/OpenIddict.Server/OpenIddictServerOptions.cs @@ -453,4 +453,11 @@ public sealed class OpenIddictServerOptions /// that provides additional protection against token leakage. /// public bool UseReferenceRefreshTokens { get; set; } + +#if SUPPORTS_TIME_PROVIDER + /// + /// Gets or sets the time provider. + /// + public TimeProvider? TimeProvider { get; set; } +#endif } diff --git a/src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs b/src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs index f6fa86c82..ff4ffba49 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs @@ -4,6 +4,7 @@ * the license and the contributors participating to this project. */ +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Tokens; @@ -17,13 +18,26 @@ namespace OpenIddict.Validation; public sealed class OpenIddictValidationConfiguration : IPostConfigureOptions { private readonly OpenIddictValidationService _service; + private readonly IServiceProvider _provider; /// /// Creates a new instance of the class. /// /// The validation service. + [Obsolete("This constructor is no longer supported and will be removed in a future version.", error: true)] public OpenIddictValidationConfiguration(OpenIddictValidationService service) - => _service = service ?? throw new ArgumentNullException(nameof(service)); + => throw new NotSupportedException(SR.GetResourceString(SR.ID0403)); + + /// + /// Creates a new instance of the class. + /// + /// The service provider. + /// The validation service. + public OpenIddictValidationConfiguration(IServiceProvider provider, OpenIddictValidationService service) + { + _provider = provider ?? throw new ArgumentNullException(nameof(provider)); + _service = service ?? throw new ArgumentNullException(nameof(service)); + } /// public void PostConfigure(string? name, OpenIddictValidationOptions options) @@ -33,12 +47,16 @@ public void PostConfigure(string? name, OpenIddictValidationOptions options) throw new ArgumentNullException(nameof(options)); } +#if SUPPORTS_TIME_PROVIDER + options.TimeProvider ??= _provider.GetService() ?? TimeProvider.System; +#endif + if (options.JsonWebTokenHandler is null) { throw new InvalidOperationException(SR.GetResourceString(SR.ID0075)); } - if (options.Configuration is null && options.ConfigurationManager is null && + if (options.Configuration is null && options.ConfigurationManager is null && options.Issuer is null && options.ConfigurationEndpoint is null) { throw new InvalidOperationException(SR.GetResourceString(SR.ID0128)); @@ -90,10 +108,18 @@ public void PostConfigure(string? name, OpenIddictValidationOptions options) } } + var now = ( +#if SUPPORTS_TIME_PROVIDER + options.TimeProvider?.GetUtcNow() ?? +#endif + DateTimeOffset.UtcNow + ) + .LocalDateTime; + // If all the registered encryption credentials are backed by a X.509 certificate, at least one of them must be valid. if (options.EncryptionCredentials.Count is not 0 && - options.EncryptionCredentials.TrueForAll(static credentials => credentials.Key is X509SecurityKey x509SecurityKey && - (x509SecurityKey.Certificate.NotBefore > DateTime.Now || x509SecurityKey.Certificate.NotAfter < DateTime.Now))) + options.EncryptionCredentials.TrueForAll(credentials => credentials.Key is X509SecurityKey x509SecurityKey && + (x509SecurityKey.Certificate.NotBefore > now || x509SecurityKey.Certificate.NotAfter < now))) { throw new InvalidOperationException(SR.GetResourceString(SR.ID0087)); } @@ -139,15 +165,15 @@ public void PostConfigure(string? name, OpenIddictValidationOptions options) options.Handlers.Sort((left, right) => left.Order.CompareTo(right.Order)); // Sort the encryption and signing credentials. - options.EncryptionCredentials.Sort((left, right) => Compare(left.Key, right.Key)); - options.SigningCredentials.Sort((left, right) => Compare(left.Key, right.Key)); + options.EncryptionCredentials.Sort((left, right) => Compare(left.Key, right.Key, now)); + options.SigningCredentials.Sort((left, right) => Compare(left.Key, right.Key, now)); // Attach the encryption credentials to the token validation parameters. options.TokenValidationParameters.TokenDecryptionKeys = from credentials in options.EncryptionCredentials select credentials.Key; - static int Compare(SecurityKey left, SecurityKey right) => (left, right) switch + static int Compare(SecurityKey left, SecurityKey right, DateTime now) => (left, right) switch { // If the two keys refer to the same instances, return 0. (SecurityKey first, SecurityKey second) when ReferenceEquals(first, second) => 0, @@ -158,8 +184,8 @@ public void PostConfigure(string? name, OpenIddictValidationOptions options) (SecurityKey, SymmetricSecurityKey) => 1, // If one of the keys is backed by a X.509 certificate, don't prefer it if it's not valid yet. - (X509SecurityKey first, SecurityKey) when first.Certificate.NotBefore > DateTime.Now => 1, - (SecurityKey, X509SecurityKey second) when second.Certificate.NotBefore > DateTime.Now => -1, + (X509SecurityKey first, SecurityKey) when first.Certificate.NotBefore > now => 1, + (SecurityKey, X509SecurityKey second) when second.Certificate.NotBefore > now => -1, // If the two keys are backed by a X.509 certificate, prefer the one with the furthest expiration date. (X509SecurityKey first, X509SecurityKey second) => -first.Certificate.NotAfter.CompareTo(second.Certificate.NotAfter), diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs index 6c2b4c538..e95c12bc5 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs @@ -295,7 +295,11 @@ public ValueTask HandleAsync(HandleIntrospectionResponseContext context) if (long.TryParse((string?) context.Response[Claims.ExpiresAt], NumberStyles.Integer, CultureInfo.InvariantCulture, out var value) && DateTimeOffset.FromUnixTimeSeconds(value) is DateTimeOffset date && - date.Add(context.Options.TokenValidationParameters.ClockSkew) < DateTimeOffset.UtcNow) + date.Add(context.Options.TokenValidationParameters.ClockSkew) < ( +#if SUPPORTS_TIME_PROVIDER + context.Options.TimeProvider?.GetUtcNow() ?? +#endif + DateTimeOffset.UtcNow)) { context.Reject( error: Errors.InvalidToken, diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs index bd09a644d..5b6651169 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs @@ -646,7 +646,11 @@ public ValueTask HandleAsync(ValidateTokenContext context) Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); var date = context.Principal.GetExpirationDate(); - if (date.HasValue && date.Value.Add(context.TokenValidationParameters.ClockSkew) < DateTimeOffset.UtcNow) + if (date.HasValue && date.Value.Add(context.TokenValidationParameters.ClockSkew) < ( +#if SUPPORTS_TIME_PROVIDER + context.Options.TimeProvider?.GetUtcNow() ?? +#endif + DateTimeOffset.UtcNow)) { context.Logger.LogInformation(SR.GetResourceString(SR.ID6156)); diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs index 27123cde9..958922809 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs @@ -361,7 +361,11 @@ public ValueTask HandleAsync(ProcessAuthenticationContext context) nameType: Claims.Name, roleType: Claims.Role)); - principal.SetCreationDate(DateTimeOffset.UtcNow); + principal.SetCreationDate( +#if SUPPORTS_TIME_PROVIDER + context.Options.TimeProvider?.GetUtcNow() ?? +#endif + DateTimeOffset.UtcNow); var lifetime = context.Options.ClientAssertionLifetime; if (lifetime.HasValue) diff --git a/src/OpenIddict.Validation/OpenIddictValidationOptions.cs b/src/OpenIddict.Validation/OpenIddictValidationOptions.cs index 5590c9483..cb52f5b11 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationOptions.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationOptions.cs @@ -181,4 +181,11 @@ public sealed class OpenIddictValidationOptions ValidateAudience = false, ValidateLifetime = false }; + +#if SUPPORTS_TIME_PROVIDER + /// + /// Gets or sets the time provider. + /// + public TimeProvider? TimeProvider { get; set; } +#endif } diff --git a/test/OpenIddict.Quartz.Tests/OpenIddictQuartzConfigurationTests.cs b/test/OpenIddict.Quartz.Tests/OpenIddictQuartzConfigurationTests.cs index ce37626f3..2b0d5a507 100644 --- a/test/OpenIddict.Quartz.Tests/OpenIddictQuartzConfigurationTests.cs +++ b/test/OpenIddict.Quartz.Tests/OpenIddictQuartzConfigurationTests.cs @@ -1,3 +1,4 @@ +using Microsoft.Extensions.DependencyInjection; using Quartz; using Xunit; @@ -10,7 +11,7 @@ public void UseQuartz_RegistersJobDetails() { // Arrange var options = new QuartzOptions(); - var configuration = new OpenIddictQuartzConfiguration(); + var configuration = new OpenIddictQuartzConfiguration(new ServiceCollection().BuildServiceProvider()); // Act configuration.Configure(options); @@ -28,7 +29,7 @@ public void UseQuartz_RegistersTriggerDetails() { // Arrange var options = new QuartzOptions(); - var configuration = new OpenIddictQuartzConfiguration(); + var configuration = new OpenIddictQuartzConfiguration(new ServiceCollection().BuildServiceProvider()); // Act configuration.Configure(options); diff --git a/test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs b/test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs index d4d5a6d73..0166da525 100644 --- a/test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs +++ b/test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs @@ -303,9 +303,15 @@ public void AddDevelopmentEncryptionCertificate_ThrowsAnExceptionOnUnsupportedPl var services = CreateServices(); var builder = CreateBuilder(services); + builder.AddDevelopmentEncryptionCertificate( + subject: new X500DistinguishedName("CN=" + Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture))); + + var serviceProvider = services.BuildServiceProvider(); + + var options = serviceProvider.GetRequiredService>(); + // Act and assert - var exception = Assert.Throws(() => builder.AddDevelopmentEncryptionCertificate( - subject: new X500DistinguishedName("CN=" + Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture)))); + var exception = Assert.Throws(() => options.Value); Assert.Equal("X.509 certificate generation is not supported on this platform.", exception.Message); } @@ -353,9 +359,15 @@ public void AddDevelopmentSigningCertificate_ThrowsAnExceptionOnUnsupportedPlatf var services = CreateServices(); var builder = CreateBuilder(services); + builder.AddDevelopmentSigningCertificate( + subject: new X500DistinguishedName("CN=" + Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture))); + + var serviceProvider = services.BuildServiceProvider(); + + var options = serviceProvider.GetRequiredService>(); + // Act and assert - var exception = Assert.Throws(() => builder.AddDevelopmentSigningCertificate( - subject: new X500DistinguishedName("CN=" + Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture)))); + var exception = Assert.Throws(() => options.Value); Assert.Equal("X.509 certificate generation is not supported on this platform.", exception.Message); }