Browse Source

Use System.TimeProvider on .NET 8.0+

pull/2074/head
Jan Trejbal 2 years ago
committed by GitHub
parent
commit
2475ed3c1a
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 207
      src/OpenIddict.Client/OpenIddictClientBuilder.cs
  2. 38
      src/OpenIddict.Client/OpenIddictClientConfiguration.cs
  3. 6
      src/OpenIddict.Client/OpenIddictClientHandlers.Introspection.cs
  4. 6
      src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs
  5. 57
      src/OpenIddict.Client/OpenIddictClientHandlers.cs
  6. 7
      src/OpenIddict.Client/OpenIddictClientOptions.cs
  7. 6
      src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs
  8. 6
      src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs
  9. 33
      src/OpenIddict.Core/OpenIddictCoreConfiguration.cs
  10. 4
      src/OpenIddict.Core/OpenIddictCoreExtensions.cs
  11. 7
      src/OpenIddict.Core/OpenIddictCoreOptions.cs
  12. 26
      src/OpenIddict.Quartz/OpenIddictQuartzConfiguration.cs
  13. 9
      src/OpenIddict.Quartz/OpenIddictQuartzExtensions.cs
  14. 12
      src/OpenIddict.Quartz/OpenIddictQuartzJob.cs
  15. 7
      src/OpenIddict.Quartz/OpenIddictQuartzOptions.cs
  16. 210
      src/OpenIddict.Server/OpenIddictServerBuilder.cs
  17. 46
      src/OpenIddict.Server/OpenIddictServerConfiguration.cs
  18. 16
      src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs
  19. 68
      src/OpenIddict.Server/OpenIddictServerHandlers.cs
  20. 7
      src/OpenIddict.Server/OpenIddictServerOptions.cs
  21. 44
      src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs
  22. 6
      src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs
  23. 6
      src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs
  24. 6
      src/OpenIddict.Validation/OpenIddictValidationHandlers.cs
  25. 7
      src/OpenIddict.Validation/OpenIddictValidationOptions.cs
  26. 5
      test/OpenIddict.Quartz.Tests/OpenIddictQuartzConfigurationTests.cs
  27. 20
      test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs

207
src/OpenIddict.Client/OpenIddictClientBuilder.cs

@ -200,67 +200,79 @@ public sealed class OpenIddictClientBuilder
throw new ArgumentNullException(nameof(subject)); throw new ArgumentNullException(nameof(subject));
} }
using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser); Services.AddOptions<OpenIddictClientOptions>().Configure<IServiceProvider>((options, provider) =>
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<X509Certificate2>()
.ToList();
if (!certificates.Exists(static certificate => certificate.NotBefore < DateTime.Now && certificate.NotAfter > DateTime.Now))
{ {
#if SUPPORTS_CERTIFICATE_GENERATION #if SUPPORTS_TIME_PROVIDER
using var algorithm = OpenIddictHelpers.CreateRsaKey(size: 2048); var now = (options.TimeProvider ?? provider.GetService<TimeProvider>())?.GetUtcNow() ?? DateTimeOffset.UtcNow;
#else
var request = new CertificateRequest(subject, algorithm, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); var now = DateTimeOffset.UtcNow;
request.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.KeyEncipherment, critical: true)); #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<X509Certificate2>()
.ToList();
// Note: setting the friendly name is not supported on Unix machines (including Linux and macOS). if (!certificates.Exists(certificate => certificate.NotBefore < now.LocalDateTime && certificate.NotAfter > now.LocalDateTime))
// To ensure an exception is not thrown by the property setter, an OS runtime check is used here.
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{ {
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 var request = new CertificateRequest(subject, algorithm, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
// as "persisted", which eventually prevents X509Store.Add() from correctly storing the private key. request.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.KeyEncipherment, critical: true));
// 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);
try var certificate = request.CreateSelfSigned(now, now.AddYears(2));
{
var flags = X509KeyStorageFlags.PersistKeySet;
// Note: macOS requires marking the certificate private key as exportable. // Note: setting the friendly name is not supported on Unix machines (including Linux and macOS).
// If this flag is not set, a CryptographicException is thrown at runtime. // To ensure an exception is not thrown by the property setter, an OS runtime check is used here.
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) 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 try
{ {
Array.Clear(data, 0, data.Length); 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 #else
throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0264)); throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0264));
#endif #endif
} }
return Configure(options => options.EncryptionCredentials.AddRange( options.EncryptionCredentials.AddRange(
from certificate in certificates from certificate in certificates
let key = new X509SecurityKey(certificate) let key = new X509SecurityKey(certificate)
select new EncryptingCredentials(key, SecurityAlgorithms.RsaOAEP, SecurityAlgorithms.Aes256CbcHmacSha512))); select new EncryptingCredentials(key, SecurityAlgorithms.RsaOAEP,
SecurityAlgorithms.Aes256CbcHmacSha512));
});
return this;
} }
/// <summary> /// <summary>
@ -563,67 +575,78 @@ public sealed class OpenIddictClientBuilder
throw new ArgumentNullException(nameof(subject)); throw new ArgumentNullException(nameof(subject));
} }
using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser); Services.AddOptions<OpenIddictClientOptions>().Configure<IServiceProvider>((options, provider) =>
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<X509Certificate2>()
.ToList();
if (!certificates.Exists(static certificate => certificate.NotBefore < DateTime.Now && certificate.NotAfter > DateTime.Now))
{ {
#if SUPPORTS_CERTIFICATE_GENERATION #if SUPPORTS_TIME_PROVIDER
using var algorithm = OpenIddictHelpers.CreateRsaKey(size: 2048); var now = (options.TimeProvider ?? provider.GetService<TimeProvider>())?.GetUtcNow() ?? DateTimeOffset.UtcNow;
#else
var request = new CertificateRequest(subject, algorithm, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); var now = DateTimeOffset.UtcNow;
request.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature, critical: true)); #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<X509Certificate2>()
.ToList();
// Note: setting the friendly name is not supported on Unix machines (including Linux and macOS). if (!certificates.Exists(certificate => certificate.NotBefore < now.LocalDateTime && certificate.NotAfter > now.LocalDateTime))
// To ensure an exception is not thrown by the property setter, an OS runtime check is used here.
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{ {
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 var request = new CertificateRequest(subject, algorithm, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
// as "persisted", which eventually prevents X509Store.Add() from correctly storing the private key. request.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature, critical: true));
// 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);
try var certificate = request.CreateSelfSigned(now, now.AddYears(2));
{
var flags = X509KeyStorageFlags.PersistKeySet;
// Note: macOS requires marking the certificate private key as exportable. // Note: setting the friendly name is not supported on Unix machines (including Linux and macOS).
// If this flag is not set, a CryptographicException is thrown at runtime. // To ensure an exception is not thrown by the property setter, an OS runtime check is used here.
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) 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 try
{ {
Array.Clear(data, 0, data.Length); 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 #else
throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0264)); throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0264));
#endif #endif
} }
return Configure(options => options.SigningCredentials.AddRange( options.SigningCredentials.AddRange(
from certificate in certificates from certificate in certificates
let key = new X509SecurityKey(certificate) let key = new X509SecurityKey(certificate)
select new SigningCredentials(key, SecurityAlgorithms.RsaSha256))); select new SigningCredentials(key, SecurityAlgorithms.RsaSha256));
});
return this;
} }
/// <summary> /// <summary>

38
src/OpenIddict.Client/OpenIddictClientConfiguration.cs

@ -6,9 +6,11 @@
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
@ -23,13 +25,26 @@ namespace OpenIddict.Client;
public sealed class OpenIddictClientConfiguration : IPostConfigureOptions<OpenIddictClientOptions> public sealed class OpenIddictClientConfiguration : IPostConfigureOptions<OpenIddictClientOptions>
{ {
private readonly OpenIddictClientService _service; private readonly OpenIddictClientService _service;
private readonly IServiceProvider _provider;
/// <summary> /// <summary>
/// Creates a new instance of the <see cref="OpenIddictClientConfiguration"/> class. /// Creates a new instance of the <see cref="OpenIddictClientConfiguration"/> class.
/// </summary> /// </summary>
/// <param name="service">The OpenIddict client service.</param> /// <param name="service">The OpenIddict client service.</param>
[Obsolete("This constructor is no longer supported and will be removed in a future version.", error: true)]
public OpenIddictClientConfiguration(OpenIddictClientService service) public OpenIddictClientConfiguration(OpenIddictClientService service)
=> _service = service ?? throw new ArgumentNullException(nameof(service)); => throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictClientConfiguration"/> class.
/// </summary>
/// <param name="provider">The service provider.</param>
/// <param name="service">The OpenIddict client service.</param>
public OpenIddictClientConfiguration(IServiceProvider provider, OpenIddictClientService service)
{
_provider = provider ?? throw new ArgumentNullException(nameof(provider));
_service = service ?? throw new ArgumentNullException(nameof(service));
}
/// <inheritdoc/> /// <inheritdoc/>
public void PostConfigure(string? name, OpenIddictClientOptions options) public void PostConfigure(string? name, OpenIddictClientOptions options)
@ -44,6 +59,10 @@ public sealed class OpenIddictClientConfiguration : IPostConfigureOptions<OpenId
throw new InvalidOperationException(SR.GetResourceString(SR.ID0075)); throw new InvalidOperationException(SR.GetResourceString(SR.ID0075));
} }
#if SUPPORTS_TIME_PROVIDER
options.TimeProvider ??= _provider.GetService<TimeProvider>() ?? TimeProvider.System;
#endif
foreach (var registration in options.Registrations) foreach (var registration in options.Registrations)
{ {
if (registration.Issuer is null) if (registration.Issuer is null)
@ -212,9 +231,16 @@ public sealed class OpenIddictClientConfiguration : IPostConfigureOptions<OpenId
// Sort the handlers collection using the order associated with each handler. // Sort the handlers collection using the order associated with each handler.
options.Handlers.Sort((left, right) => left.Order.CompareTo(right.Order)); 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. // Sort the encryption and signing credentials.
options.EncryptionCredentials.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)); 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. // 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) foreach (var key in options.EncryptionCredentials.Select(credentials => credentials.Key)
@ -234,7 +260,7 @@ public sealed class OpenIddictClientConfiguration : IPostConfigureOptions<OpenId
from credentials in options.EncryptionCredentials from credentials in options.EncryptionCredentials
select credentials.Key; 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. // If the two keys refer to the same instances, return 0.
(SecurityKey first, SecurityKey second) when ReferenceEquals(first, second) => 0, (SecurityKey first, SecurityKey second) when ReferenceEquals(first, second) => 0,
@ -245,8 +271,8 @@ public sealed class OpenIddictClientConfiguration : IPostConfigureOptions<OpenId
(SecurityKey, SymmetricSecurityKey) => 1, (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. // 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, (X509SecurityKey first, SecurityKey) when first.Certificate.NotBefore > now => 1,
(SecurityKey, X509SecurityKey second) when second.Certificate.NotBefore > DateTime.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. // 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), (X509SecurityKey first, X509SecurityKey second) => -first.Certificate.NotAfter.CompareTo(second.Certificate.NotAfter),

6
src/OpenIddict.Client/OpenIddictClientHandlers.Introspection.cs

@ -295,7 +295,11 @@ public static partial class OpenIddictClientHandlers
if (long.TryParse((string?) context.Response[Claims.ExpiresAt], if (long.TryParse((string?) context.Response[Claims.ExpiresAt],
NumberStyles.Integer, CultureInfo.InvariantCulture, out var value) && NumberStyles.Integer, CultureInfo.InvariantCulture, out var value) &&
DateTimeOffset.FromUnixTimeSeconds(value) is DateTimeOffset date && 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( context.Reject(
error: Errors.ServerError, error: Errors.ServerError,

6
src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs

@ -653,7 +653,11 @@ public static partial class OpenIddictClientHandlers
Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
var date = context.Principal.GetExpirationDate(); 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( context.Reject(
error: Errors.InvalidToken, error: Errors.InvalidToken,

57
src/OpenIddict.Client/OpenIddictClientHandlers.cs

@ -1494,8 +1494,11 @@ public static partial class OpenIddictClientHandlers
context.FrontchannelAccessTokenExpirationDate = context.EndpointType switch context.FrontchannelAccessTokenExpirationDate = context.EndpointType switch
{ {
OpenIddictClientEndpointType.Redirection when context.ExtractFrontchannelAccessToken OpenIddictClientEndpointType.Redirection when context.ExtractFrontchannelAccessToken
=> ((long?) context.Request[Parameters.ExpiresIn]) is long value ? => (long?) context.Request[Parameters.ExpiresIn] is long value ? (
DateTimeOffset.UtcNow.AddSeconds(value) : null, #if SUPPORTS_TIME_PROVIDER
context.Options.TimeProvider?.GetUtcNow() ??
#endif
DateTimeOffset.UtcNow).AddSeconds(value) : null,
_ => null _ => null
}; };
@ -2471,7 +2474,11 @@ public static partial class OpenIddictClientHandlers
nameType: Claims.Name, nameType: Claims.Name,
roleType: Claims.Role)); 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; var lifetime = context.Options.ClientAssertionLifetime;
if (lifetime.HasValue) if (lifetime.HasValue)
@ -2849,7 +2856,13 @@ public static partial class OpenIddictClientHandlers
context.BackchannelAccessTokenExpirationDate = context.BackchannelAccessTokenExpirationDate =
context.ExtractBackchannelAccessToken && 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.BackchannelIdentityToken = context.ExtractBackchannelIdentityToken ?
context.TokenResponse.IdToken : null; context.TokenResponse.IdToken : null;
@ -4741,7 +4754,7 @@ public static partial class OpenIddictClientHandlers
// response mode as it offers a better protection for SPA applications. // response mode as it offers a better protection for SPA applications.
// Unfortunately, server-side clients like ASP.NET Core applications cannot // Unfortunately, server-side clients like ASP.NET Core applications cannot
// natively use response_mode=fragment as URI fragments are never sent to servers. // 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 // 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 // expected that specialized hosts like Blazor implement custom event handlers to
// opt for fragment by default, if it is supported by the authorization server. // opt for fragment by default, if it is supported by the authorization server.
@ -5154,7 +5167,11 @@ public static partial class OpenIddictClientHandlers
return true; 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; var lifetime = context.Principal.GetStateTokenLifetime() ?? context.Options.StateTokenLifetime;
if (lifetime.HasValue) if (lifetime.HasValue)
@ -5335,7 +5352,7 @@ public static partial class OpenIddictClientHandlers
// //
// Note: the nonce is always hashed before being sent, as recommended the specification. // 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. // 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<string> types && context.ResponseType?.Split(Separators.Space) is IList<string> types &&
(types.Contains(ResponseTypes.Code) || types.Contains(ResponseTypes.IdToken))) (types.Contains(ResponseTypes.Code) || types.Contains(ResponseTypes.IdToken)))
{ {
@ -5571,7 +5588,11 @@ public static partial class OpenIddictClientHandlers
nameType: Claims.Name, nameType: Claims.Name,
roleType: Claims.Role)); 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; var lifetime = context.Options.ClientAssertionLifetime;
if (lifetime.HasValue) if (lifetime.HasValue)
@ -5824,7 +5845,7 @@ public static partial class OpenIddictClientHandlers
// validated by default. Clients that need to deal with non-standard implementations // 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). // can use custom handlers to validate user codes that use a readable format (e.g JWT).
GrantTypes.DeviceCode => (true, true, false, false), GrantTypes.DeviceCode => (true, true, false, false),
_ => (false, false, false, false) _ => (false, false, false, false)
}; };
@ -6239,7 +6260,11 @@ public static partial class OpenIddictClientHandlers
nameType: Claims.Name, nameType: Claims.Name,
roleType: Claims.Role)); 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; var lifetime = context.Options.ClientAssertionLifetime;
if (lifetime.HasValue) if (lifetime.HasValue)
@ -6831,7 +6856,11 @@ public static partial class OpenIddictClientHandlers
nameType: Claims.Name, nameType: Claims.Name,
roleType: Claims.Role)); 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; var lifetime = context.Options.ClientAssertionLifetime;
if (lifetime.HasValue) if (lifetime.HasValue)
@ -7463,7 +7492,11 @@ public static partial class OpenIddictClientHandlers
return true; 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; var lifetime = context.Principal.GetStateTokenLifetime() ?? context.Options.StateTokenLifetime;
if (lifetime.HasValue) if (lifetime.HasValue)

7
src/OpenIddict.Client/OpenIddictClientOptions.cs

@ -157,4 +157,11 @@ public sealed class OpenIddictClientOptions
/// </summary> /// </summary>
[EditorBrowsable(EditorBrowsableState.Advanced)] [EditorBrowsable(EditorBrowsableState.Advanced)]
public HashSet<string> ResponseTypes { get; } = new(StringComparer.Ordinal); public HashSet<string> ResponseTypes { get; } = new(StringComparer.Ordinal);
#if SUPPORTS_TIME_PROVIDER
/// <summary>
/// Gets or sets the time provider.
/// </summary>
public TimeProvider? TimeProvider { get; set; }
#endif
} }

6
src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs

@ -232,7 +232,11 @@ public class OpenIddictAuthorizationManager<TAuthorization> : IOpenIddictAuthori
var descriptor = new OpenIddictAuthorizationDescriptor var descriptor = new OpenIddictAuthorizationDescriptor
{ {
ApplicationId = client, ApplicationId = client,
CreationDate = DateTimeOffset.UtcNow, CreationDate =
#if SUPPORTS_TIME_PROVIDER
Options.CurrentValue.TimeProvider?.GetUtcNow() ??
#endif
DateTimeOffset.UtcNow,
Principal = principal, Principal = principal,
Status = Statuses.Valid, Status = Statuses.Valid,
Subject = subject, Subject = subject,

6
src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs

@ -1088,7 +1088,11 @@ public class OpenIddictTokenManager<TToken> : IOpenIddictTokenManager where TTok
// the first time the token is redeemed. In this case, attach the current date. // the first time the token is redeemed. In this case, attach the current date.
if (await Store.GetRedemptionDateAsync(token, cancellationToken) is null) 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); await Store.SetStatusAsync(token, Statuses.Redeemed, cancellationToken);

33
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;
/// <summary>
/// Contains the methods required to ensure that the OpenIddict core configuration is valid.
/// </summary>
public class OpenIddictCoreConfiguration : IPostConfigureOptions<OpenIddictCoreOptions>
{
private readonly IServiceProvider _provider;
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictCoreConfiguration"/> class.
/// </summary>
/// <param name="provider">The service provider.</param>
public OpenIddictCoreConfiguration(IServiceProvider provider)
=> _provider = provider ?? throw new ArgumentNullException(nameof(provider));
/// <inheritdoc/>
public void PostConfigure(string? name, OpenIddictCoreOptions options)
{
#if SUPPORTS_TIME_PROVIDER
options.TimeProvider ??= _provider.GetService<TimeProvider>() ?? TimeProvider.System;
#endif
}
}

4
src/OpenIddict.Core/OpenIddictCoreExtensions.cs

@ -88,6 +88,10 @@ public static class OpenIddictCoreExtensions
typeof(OpenIddictTokenManager<>).MakeGenericType(type)); typeof(OpenIddictTokenManager<>).MakeGenericType(type));
}); });
// Note: TryAddEnumerable() is used here to ensure the initializer is registered only once.
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<
IPostConfigureOptions<OpenIddictCoreOptions>, OpenIddictCoreConfiguration>());
return new OpenIddictCoreBuilder(builder.Services); return new OpenIddictCoreBuilder(builder.Services);
} }

7
src/OpenIddict.Core/OpenIddictCoreOptions.cs

@ -59,4 +59,11 @@ public sealed class OpenIddictCoreOptions
/// This property is not used when <see cref="DisableEntityCaching"/> is <see langword="true"/>. /// This property is not used when <see cref="DisableEntityCaching"/> is <see langword="true"/>.
/// </summary> /// </summary>
public int EntityCacheLimit { get; set; } = 250; public int EntityCacheLimit { get; set; } = 250;
#if SUPPORTS_TIME_PROVIDER
/// <summary>
/// Gets or sets the time provider.
/// </summary>
public TimeProvider? TimeProvider { get; set; }
#endif
} }

26
src/OpenIddict.Quartz/OpenIddictQuartzConfiguration.cs

@ -5,6 +5,7 @@
*/ */
using System.ComponentModel; using System.ComponentModel;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
namespace OpenIddict.Quartz; namespace OpenIddict.Quartz;
@ -13,8 +14,23 @@ namespace OpenIddict.Quartz;
/// Contains the methods required to ensure that the OpenIddict Quartz.NET configuration is valid. /// Contains the methods required to ensure that the OpenIddict Quartz.NET configuration is valid.
/// </summary> /// </summary>
[EditorBrowsable(EditorBrowsableState.Advanced)] [EditorBrowsable(EditorBrowsableState.Advanced)]
public sealed class OpenIddictQuartzConfiguration : IConfigureOptions<QuartzOptions> public sealed class OpenIddictQuartzConfiguration : IConfigureOptions<QuartzOptions>, IPostConfigureOptions<OpenIddictQuartzOptions>
{ {
private readonly IServiceProvider _provider;
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictQuartzConfiguration"/> class.
/// </summary>
[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));
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictQuartzConfiguration"/> class.
/// </summary>
/// <param name="provider">The service provider.</param>
public OpenIddictQuartzConfiguration(IServiceProvider provider)
=> _provider = provider ?? throw new ArgumentNullException(nameof(provider));
/// <inheritdoc/> /// <inheritdoc/>
public void Configure(QuartzOptions options) public void Configure(QuartzOptions options)
{ {
@ -42,4 +58,12 @@ public sealed class OpenIddictQuartzConfiguration : IConfigureOptions<QuartzOpti
.StartAt(DateBuilder.FutureDate(new Random().Next(1, 10), IntervalUnit.Minute)); .StartAt(DateBuilder.FutureDate(new Random().Next(1, 10), IntervalUnit.Minute));
}); });
} }
/// <inheritdoc/>
public void PostConfigure(string? name, OpenIddictQuartzOptions options)
{
#if SUPPORTS_TIME_PROVIDER
options.TimeProvider ??= _provider.GetService<TimeProvider>() ?? TimeProvider.System;
#endif
}
} }

9
src/OpenIddict.Quartz/OpenIddictQuartzExtensions.cs

@ -34,9 +34,12 @@ public static class OpenIddictQuartzExtensions
// Quartz.NET's DI integration to resolve it from the DI. // Quartz.NET's DI integration to resolve it from the DI.
builder.Services.TryAddTransient<OpenIddictQuartzJob>(); builder.Services.TryAddTransient<OpenIddictQuartzJob>();
// Note: TryAddEnumerable() is used here to ensure the initializer is registered only once. // Note: TryAddEnumerable() is used here to ensure the initializers are registered only once.
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton< builder.Services.TryAddEnumerable(
IConfigureOptions<QuartzOptions>, OpenIddictQuartzConfiguration>()); [
ServiceDescriptor.Singleton<IConfigureOptions<QuartzOptions>, OpenIddictQuartzConfiguration>(),
ServiceDescriptor.Singleton<IPostConfigureOptions<OpenIddictQuartzOptions>, OpenIddictQuartzConfiguration>()
]);
return new OpenIddictQuartzBuilder(builder.Services); return new OpenIddictQuartzBuilder(builder.Services);
} }

12
src/OpenIddict.Quartz/OpenIddictQuartzJob.cs

@ -74,7 +74,11 @@ public sealed class OpenIddictQuartzJob : IJob
UnscheduleFiringTrigger = true 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 try
{ {
@ -119,7 +123,11 @@ public sealed class OpenIddictQuartzJob : IJob
UnscheduleFiringTrigger = true 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 try
{ {

7
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. /// By default, this value is set to 14 days and cannot be less than 10 minutes.
/// </summary> /// </summary>
public TimeSpan MinimumTokenLifespan { get; set; } = TimeSpan.FromDays(14); public TimeSpan MinimumTokenLifespan { get; set; } = TimeSpan.FromDays(14);
#if SUPPORTS_TIME_PROVIDER
/// <summary>
/// Gets or sets the time provider.
/// </summary>
public TimeProvider? TimeProvider { get; set; }
#endif
} }

210
src/OpenIddict.Server/OpenIddictServerBuilder.cs

@ -209,67 +209,79 @@ public sealed class OpenIddictServerBuilder
throw new ArgumentNullException(nameof(subject)); throw new ArgumentNullException(nameof(subject));
} }
using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser); Services.AddOptions<OpenIddictServerOptions>().Configure<IServiceProvider>((options, provider) =>
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<X509Certificate2>()
.ToList();
if (!certificates.Exists(static certificate => certificate.NotBefore < DateTime.Now && certificate.NotAfter > DateTime.Now))
{ {
#if SUPPORTS_CERTIFICATE_GENERATION #if SUPPORTS_TIME_PROVIDER
using var algorithm = OpenIddictHelpers.CreateRsaKey(size: 2048); var now = (options.TimeProvider ?? provider.GetService<TimeProvider>())?.GetUtcNow() ?? DateTimeOffset.UtcNow;
#else
var request = new CertificateRequest(subject, algorithm, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); var now = DateTimeOffset.UtcNow;
request.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.KeyEncipherment, critical: true)); #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<X509Certificate2>()
.ToList();
// Note: setting the friendly name is not supported on Unix machines (including Linux and macOS). if (!certificates.Exists(certificate => certificate.NotBefore < now.LocalDateTime && certificate.NotAfter > now.LocalDateTime))
// To ensure an exception is not thrown by the property setter, an OS runtime check is used here.
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{ {
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 var request = new CertificateRequest(subject, algorithm, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
// as "persisted", which eventually prevents X509Store.Add() from correctly storing the private key. request.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.KeyEncipherment, critical: true));
// 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);
try var certificate = request.CreateSelfSigned(now, now.AddYears(2));
{
var flags = X509KeyStorageFlags.PersistKeySet;
// Note: macOS requires marking the certificate private key as exportable. // Note: setting the friendly name is not supported on Unix machines (including Linux and macOS).
// If this flag is not set, a CryptographicException is thrown at runtime. // To ensure an exception is not thrown by the property setter, an OS runtime check is used here.
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) 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 try
{ {
Array.Clear(data, 0, data.Length); 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 #else
throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0264)); throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0264));
#endif #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( return this;
from certificate in certificates
let key = new X509SecurityKey(certificate)
select new EncryptingCredentials(key, SecurityAlgorithms.RsaOAEP, SecurityAlgorithms.Aes256CbcHmacSha512)));
} }
/// <summary> /// <summary>
@ -572,67 +584,79 @@ public sealed class OpenIddictServerBuilder
throw new ArgumentNullException(nameof(subject)); throw new ArgumentNullException(nameof(subject));
} }
using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser); Services.AddOptions<OpenIddictServerOptions>().Configure<IServiceProvider>((options, provider) =>
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<X509Certificate2>()
.ToList();
if (!certificates.Exists(static certificate => certificate.NotBefore < DateTime.Now && certificate.NotAfter > DateTime.Now))
{ {
#if SUPPORTS_CERTIFICATE_GENERATION #if SUPPORTS_TIME_PROVIDER
using var algorithm = OpenIddictHelpers.CreateRsaKey(size: 2048); var now = (options.TimeProvider ?? provider.GetService<TimeProvider>())?.GetUtcNow() ?? DateTimeOffset.UtcNow;
#else
var request = new CertificateRequest(subject, algorithm, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); var now = DateTimeOffset.UtcNow;
request.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature, critical: true)); #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<X509Certificate2>()
.ToList();
// Note: setting the friendly name is not supported on Unix machines (including Linux and macOS). if (!certificates.Exists(certificate =>
// To ensure an exception is not thrown by the property setter, an OS runtime check is used here. certificate.NotBefore < now.LocalDateTime && certificate.NotAfter > now.LocalDateTime))
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{ {
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 var request = new CertificateRequest(subject, algorithm, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
// as "persisted", which eventually prevents X509Store.Add() from correctly storing the private key. request.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature, critical: true));
// 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);
try var certificate = request.CreateSelfSigned(now, now.AddYears(2));
{
var flags = X509KeyStorageFlags.PersistKeySet;
// Note: macOS requires marking the certificate private key as exportable. // Note: setting the friendly name is not supported on Unix machines (including Linux and macOS).
// If this flag is not set, a CryptographicException is thrown at runtime. // To ensure an exception is not thrown by the property setter, an OS runtime check is used here.
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) 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 try
{ {
Array.Clear(data, 0, data.Length); 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 #else
throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0264)); throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0264));
#endif #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( return this;
from certificate in certificates
let key = new X509SecurityKey(certificate)
select new SigningCredentials(key, SecurityAlgorithms.RsaSha256)));
} }
/// <summary> /// <summary>
@ -1627,7 +1651,7 @@ public sealed class OpenIddictServerBuilder
{ {
throw new ArgumentNullException(nameof(scopes)); throw new ArgumentNullException(nameof(scopes));
} }
if (Array.Exists(scopes, string.IsNullOrEmpty)) if (Array.Exists(scopes, string.IsNullOrEmpty))
{ {
throw new ArgumentException(SR.GetResourceString(SR.ID0074), nameof(scopes)); throw new ArgumentException(SR.GetResourceString(SR.ID0074), nameof(scopes));

46
src/OpenIddict.Server/OpenIddictServerConfiguration.cs

@ -9,6 +9,7 @@ using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using OpenIddict.Extensions; using OpenIddict.Extensions;
@ -21,6 +22,21 @@ namespace OpenIddict.Server;
[EditorBrowsable(EditorBrowsableState.Advanced)] [EditorBrowsable(EditorBrowsableState.Advanced)]
public sealed class OpenIddictServerConfiguration : IPostConfigureOptions<OpenIddictServerOptions> public sealed class OpenIddictServerConfiguration : IPostConfigureOptions<OpenIddictServerOptions>
{ {
private readonly IServiceProvider _provider;
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictServerConfiguration"/> class.
/// </summary>
[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));
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictServerConfiguration"/> class.
/// </summary>
/// <param name="provider">The service provider.</param>
public OpenIddictServerConfiguration(IServiceProvider provider)
=> _provider = provider ?? throw new ArgumentNullException(nameof(provider));
/// <inheritdoc/> /// <inheritdoc/>
public void PostConfigure(string? name, OpenIddictServerOptions options) public void PostConfigure(string? name, OpenIddictServerOptions options)
{ {
@ -29,6 +45,10 @@ public sealed class OpenIddictServerConfiguration : IPostConfigureOptions<OpenId
throw new ArgumentNullException(nameof(options)); throw new ArgumentNullException(nameof(options));
} }
#if SUPPORTS_TIME_PROVIDER
options.TimeProvider ??= _provider.GetService<TimeProvider>() ?? TimeProvider.System;
#endif
// Explicitly disable all the features that are implicitly excluded when the degraded mode is active. // Explicitly disable all the features that are implicitly excluded when the degraded mode is active.
if (options.EnableDegradedMode) if (options.EnableDegradedMode)
{ {
@ -183,16 +203,24 @@ public sealed class OpenIddictServerConfiguration : IPostConfigureOptions<OpenId
throw new InvalidOperationException(SR.GetResourceString(SR.ID0086)); 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 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 && if (options.EncryptionCredentials.TrueForAll(credentials => credentials.Key is X509SecurityKey x509SecurityKey &&
(x509SecurityKey.Certificate.NotBefore > DateTime.Now || x509SecurityKey.Certificate.NotAfter < DateTime.Now))) (x509SecurityKey.Certificate.NotBefore > now || x509SecurityKey.Certificate.NotAfter < now)))
{ {
throw new InvalidOperationException(SR.GetResourceString(SR.ID0087)); 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 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 && if (options.SigningCredentials.TrueForAll(credentials => credentials.Key is X509SecurityKey x509SecurityKey &&
(x509SecurityKey.Certificate.NotBefore > DateTime.Now || x509SecurityKey.Certificate.NotAfter < DateTime.Now))) (x509SecurityKey.Certificate.NotBefore > now || x509SecurityKey.Certificate.NotAfter < now)))
{ {
throw new InvalidOperationException(SR.GetResourceString(SR.ID0088)); throw new InvalidOperationException(SR.GetResourceString(SR.ID0088));
} }
@ -368,8 +396,8 @@ public sealed class OpenIddictServerConfiguration : IPostConfigureOptions<OpenId
options.Handlers.Sort(static (left, right) => left.Order.CompareTo(right.Order)); options.Handlers.Sort(static (left, right) => left.Order.CompareTo(right.Order));
// Sort the encryption and signing credentials. // Sort the encryption and signing credentials.
options.EncryptionCredentials.Sort(static (left, right) => Compare(left.Key, right.Key)); options.EncryptionCredentials.Sort((left, right) => Compare(left.Key, right.Key, now));
options.SigningCredentials.Sort(static (left, right) => Compare(left.Key, right.Key)); 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. // 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) foreach (var key in options.EncryptionCredentials.Select(credentials => credentials.Key)
@ -389,7 +417,7 @@ public sealed class OpenIddictServerConfiguration : IPostConfigureOptions<OpenId
from credentials in options.EncryptionCredentials from credentials in options.EncryptionCredentials
select credentials.Key; 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. // If the two keys refer to the same instances, return 0.
(SecurityKey first, SecurityKey second) when ReferenceEquals(first, second) => 0, (SecurityKey first, SecurityKey second) when ReferenceEquals(first, second) => 0,
@ -400,8 +428,8 @@ public sealed class OpenIddictServerConfiguration : IPostConfigureOptions<OpenId
(SecurityKey, SymmetricSecurityKey) => 1, (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. // 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, (X509SecurityKey first, SecurityKey) when first.Certificate.NotBefore > now => 1,
(SecurityKey, X509SecurityKey second) when second.Certificate.NotBefore > DateTime.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. // 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), (X509SecurityKey first, X509SecurityKey second) => -first.Certificate.NotAfter.CompareTo(second.Certificate.NotAfter),

16
src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs

@ -908,7 +908,11 @@ public static partial class OpenIddictServerHandlers
Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
var date = context.Principal.GetExpirationDate(); 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( context.Reject(
error: context.Principal.GetTokenType() switch error: context.Principal.GetTokenType() switch
@ -1110,7 +1114,11 @@ public static partial class OpenIddictServerHandlers
} }
var date = await _tokenManager.GetRedemptionDateAsync(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; return true;
} }
@ -1404,7 +1412,7 @@ public static partial class OpenIddictServerHandlers
// entry to the token) that can be used by the resource servers to determine // 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. // 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. // string to respect the usual representation of the standard scope claim.
// //
// See https://datatracker.ietf.org/doc/html/rfc9068 for more information. // See https://datatracker.ietf.org/doc/html/rfc9068 for more information.
@ -1421,7 +1429,7 @@ public static partial class OpenIddictServerHandlers
// For authorization/device/user codes and refresh tokens, // For authorization/device/user codes and refresh tokens,
// attach claims destinations to the JWT claims collection. // 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) TokenTypeHints.RefreshToken or TokenTypeHints.UserCode)
{ {
var destinations = principal.GetDestinations(); var destinations = principal.GetDestinations();

68
src/OpenIddict.Server/OpenIddictServerHandlers.cs

@ -104,7 +104,7 @@ public static partial class OpenIddictServerHandlers
*/ */
ValidateSignOutDemand.Descriptor, ValidateSignOutDemand.Descriptor,
AttachCustomSignOutParameters.Descriptor, AttachCustomSignOutParameters.Descriptor,
/* /*
* Error processing: * Error processing:
*/ */
@ -2747,7 +2747,11 @@ public static partial class OpenIddictServerHandlers
var descriptor = new OpenIddictAuthorizationDescriptor var descriptor = new OpenIddictAuthorizationDescriptor
{ {
CreationDate = DateTimeOffset.UtcNow, CreationDate =
#if SUPPORTS_TIME_PROVIDER
context.Options.TimeProvider?.GetUtcNow() ??
#endif
DateTimeOffset.UtcNow,
Principal = context.Principal, Principal = context.Principal,
Status = Statuses.Valid, Status = Statuses.Valid,
Subject = context.Principal.GetClaim(Claims.Subject), Subject = context.Principal.GetClaim(Claims.Subject),
@ -2887,7 +2891,11 @@ public static partial class OpenIddictServerHandlers
claim.Properties.Remove(Properties.Destinations); 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. // If a specific token lifetime was attached to the principal, prefer it over any other value.
var lifetime = context.Principal.GetAccessTokenLifetime(); var lifetime = context.Principal.GetAccessTokenLifetime();
@ -3011,7 +3019,11 @@ public static partial class OpenIddictServerHandlers
return true; 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. // If a specific token lifetime was attached to the principal, prefer it over any other value.
var lifetime = context.Principal.GetAuthorizationCodeLifetime(); var lifetime = context.Principal.GetAuthorizationCodeLifetime();
@ -3137,7 +3149,11 @@ public static partial class OpenIddictServerHandlers
return true; 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. // If a specific token lifetime was attached to the principal, prefer it over any other value.
var lifetime = context.Principal.GetDeviceCodeLifetime(); var lifetime = context.Principal.GetDeviceCodeLifetime();
@ -3250,7 +3266,11 @@ public static partial class OpenIddictServerHandlers
return true; 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 // 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. // and must exactly match the expiration date of the refresh token used in the token request.
@ -3401,7 +3421,11 @@ public static partial class OpenIddictServerHandlers
claim.Properties.Remove(Properties.Destinations); 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. // If a specific token lifetime was attached to the principal, prefer it over any other value.
var lifetime = context.Principal.GetIdentityTokenLifetime(); var lifetime = context.Principal.GetIdentityTokenLifetime();
@ -3526,7 +3550,11 @@ public static partial class OpenIddictServerHandlers
return true; 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. // If a specific token lifetime was attached to the principal, prefer it over any other value.
var lifetime = context.Principal.GetUserCodeLifetime(); var lifetime = context.Principal.GetUserCodeLifetime();
@ -4288,9 +4316,17 @@ public static partial class OpenIddictServerHandlers
{ {
// If an expiration date was set on the access token principal, return it to the client application. // If an expiration date was set on the access token principal, return it to the client application.
if (context.AccessTokenPrincipal.GetExpirationDate() 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 // If the granted access token scopes differ from the requested scopes, return the granted scopes
@ -4352,8 +4388,16 @@ public static partial class OpenIddictServerHandlers
{ {
// If an expiration date was set on the device code or user // If an expiration date was set on the device code or user
// code principal, return it to the client application. // code principal, return it to the client application.
DateTimeOffset date when date > DateTimeOffset.UtcNow DateTimeOffset date when date > (
=> (long) ((date - DateTimeOffset.UtcNow).TotalSeconds + .5), #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" // Otherwise, return an arbitrary value, as the "expires_in"
// parameter is required in device authorization responses. // parameter is required in device authorization responses.

7
src/OpenIddict.Server/OpenIddictServerOptions.cs

@ -453,4 +453,11 @@ public sealed class OpenIddictServerOptions
/// that provides additional protection against token leakage. /// that provides additional protection against token leakage.
/// </summary> /// </summary>
public bool UseReferenceRefreshTokens { get; set; } public bool UseReferenceRefreshTokens { get; set; }
#if SUPPORTS_TIME_PROVIDER
/// <summary>
/// Gets or sets the time provider.
/// </summary>
public TimeProvider? TimeProvider { get; set; }
#endif
} }

44
src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs

@ -4,6 +4,7 @@
* the license and the contributors participating to this project. * the license and the contributors participating to this project.
*/ */
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
@ -17,13 +18,26 @@ namespace OpenIddict.Validation;
public sealed class OpenIddictValidationConfiguration : IPostConfigureOptions<OpenIddictValidationOptions> public sealed class OpenIddictValidationConfiguration : IPostConfigureOptions<OpenIddictValidationOptions>
{ {
private readonly OpenIddictValidationService _service; private readonly OpenIddictValidationService _service;
private readonly IServiceProvider _provider;
/// <summary> /// <summary>
/// Creates a new instance of the <see cref="OpenIddictValidationConfiguration"/> class. /// Creates a new instance of the <see cref="OpenIddictValidationConfiguration"/> class.
/// </summary> /// </summary>
/// <param name="service">The validation service.</param> /// <param name="service">The validation service.</param>
[Obsolete("This constructor is no longer supported and will be removed in a future version.", error: true)]
public OpenIddictValidationConfiguration(OpenIddictValidationService service) public OpenIddictValidationConfiguration(OpenIddictValidationService service)
=> _service = service ?? throw new ArgumentNullException(nameof(service)); => throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictValidationConfiguration"/> class.
/// </summary>
/// <param name="provider">The service provider.</param>
/// <param name="service">The validation service.</param>
public OpenIddictValidationConfiguration(IServiceProvider provider, OpenIddictValidationService service)
{
_provider = provider ?? throw new ArgumentNullException(nameof(provider));
_service = service ?? throw new ArgumentNullException(nameof(service));
}
/// <inheritdoc/> /// <inheritdoc/>
public void PostConfigure(string? name, OpenIddictValidationOptions options) public void PostConfigure(string? name, OpenIddictValidationOptions options)
@ -33,12 +47,16 @@ public sealed class OpenIddictValidationConfiguration : IPostConfigureOptions<Op
throw new ArgumentNullException(nameof(options)); throw new ArgumentNullException(nameof(options));
} }
#if SUPPORTS_TIME_PROVIDER
options.TimeProvider ??= _provider.GetService<TimeProvider>() ?? TimeProvider.System;
#endif
if (options.JsonWebTokenHandler is null) if (options.JsonWebTokenHandler is null)
{ {
throw new InvalidOperationException(SR.GetResourceString(SR.ID0075)); 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) options.Issuer is null && options.ConfigurationEndpoint is null)
{ {
throw new InvalidOperationException(SR.GetResourceString(SR.ID0128)); throw new InvalidOperationException(SR.GetResourceString(SR.ID0128));
@ -90,10 +108,18 @@ public sealed class OpenIddictValidationConfiguration : IPostConfigureOptions<Op
} }
} }
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 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 && if (options.EncryptionCredentials.Count is not 0 &&
options.EncryptionCredentials.TrueForAll(static credentials => credentials.Key is X509SecurityKey x509SecurityKey && options.EncryptionCredentials.TrueForAll(credentials => credentials.Key is X509SecurityKey x509SecurityKey &&
(x509SecurityKey.Certificate.NotBefore > DateTime.Now || x509SecurityKey.Certificate.NotAfter < DateTime.Now))) (x509SecurityKey.Certificate.NotBefore > now || x509SecurityKey.Certificate.NotAfter < now)))
{ {
throw new InvalidOperationException(SR.GetResourceString(SR.ID0087)); throw new InvalidOperationException(SR.GetResourceString(SR.ID0087));
} }
@ -139,15 +165,15 @@ public sealed class OpenIddictValidationConfiguration : IPostConfigureOptions<Op
options.Handlers.Sort((left, right) => left.Order.CompareTo(right.Order)); options.Handlers.Sort((left, right) => left.Order.CompareTo(right.Order));
// Sort the encryption and signing credentials. // Sort the encryption and signing credentials.
options.EncryptionCredentials.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)); options.SigningCredentials.Sort((left, right) => Compare(left.Key, right.Key, now));
// Attach the encryption credentials to the token validation parameters. // Attach the encryption credentials to the token validation parameters.
options.TokenValidationParameters.TokenDecryptionKeys = options.TokenValidationParameters.TokenDecryptionKeys =
from credentials in options.EncryptionCredentials from credentials in options.EncryptionCredentials
select credentials.Key; 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. // If the two keys refer to the same instances, return 0.
(SecurityKey first, SecurityKey second) when ReferenceEquals(first, second) => 0, (SecurityKey first, SecurityKey second) when ReferenceEquals(first, second) => 0,
@ -158,8 +184,8 @@ public sealed class OpenIddictValidationConfiguration : IPostConfigureOptions<Op
(SecurityKey, SymmetricSecurityKey) => 1, (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. // 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, (X509SecurityKey first, SecurityKey) when first.Certificate.NotBefore > now => 1,
(SecurityKey, X509SecurityKey second) when second.Certificate.NotBefore > DateTime.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. // 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), (X509SecurityKey first, X509SecurityKey second) => -first.Certificate.NotAfter.CompareTo(second.Certificate.NotAfter),

6
src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs

@ -295,7 +295,11 @@ public static partial class OpenIddictValidationHandlers
if (long.TryParse((string?) context.Response[Claims.ExpiresAt], if (long.TryParse((string?) context.Response[Claims.ExpiresAt],
NumberStyles.Integer, CultureInfo.InvariantCulture, out var value) && NumberStyles.Integer, CultureInfo.InvariantCulture, out var value) &&
DateTimeOffset.FromUnixTimeSeconds(value) is DateTimeOffset date && 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( context.Reject(
error: Errors.InvalidToken, error: Errors.InvalidToken,

6
src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs

@ -646,7 +646,11 @@ public static partial class OpenIddictValidationHandlers
Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
var date = context.Principal.GetExpirationDate(); 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)); context.Logger.LogInformation(SR.GetResourceString(SR.ID6156));

6
src/OpenIddict.Validation/OpenIddictValidationHandlers.cs

@ -361,7 +361,11 @@ public static partial class OpenIddictValidationHandlers
nameType: Claims.Name, nameType: Claims.Name,
roleType: Claims.Role)); 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; var lifetime = context.Options.ClientAssertionLifetime;
if (lifetime.HasValue) if (lifetime.HasValue)

7
src/OpenIddict.Validation/OpenIddictValidationOptions.cs

@ -181,4 +181,11 @@ public sealed class OpenIddictValidationOptions
ValidateAudience = false, ValidateAudience = false,
ValidateLifetime = false ValidateLifetime = false
}; };
#if SUPPORTS_TIME_PROVIDER
/// <summary>
/// Gets or sets the time provider.
/// </summary>
public TimeProvider? TimeProvider { get; set; }
#endif
} }

5
test/OpenIddict.Quartz.Tests/OpenIddictQuartzConfigurationTests.cs

@ -1,3 +1,4 @@
using Microsoft.Extensions.DependencyInjection;
using Quartz; using Quartz;
using Xunit; using Xunit;
@ -10,7 +11,7 @@ public class OpenIddictQuartzConfigurationTests
{ {
// Arrange // Arrange
var options = new QuartzOptions(); var options = new QuartzOptions();
var configuration = new OpenIddictQuartzConfiguration(); var configuration = new OpenIddictQuartzConfiguration(new ServiceCollection().BuildServiceProvider());
// Act // Act
configuration.Configure(options); configuration.Configure(options);
@ -28,7 +29,7 @@ public class OpenIddictQuartzConfigurationTests
{ {
// Arrange // Arrange
var options = new QuartzOptions(); var options = new QuartzOptions();
var configuration = new OpenIddictQuartzConfiguration(); var configuration = new OpenIddictQuartzConfiguration(new ServiceCollection().BuildServiceProvider());
// Act // Act
configuration.Configure(options); configuration.Configure(options);

20
test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs

@ -303,9 +303,15 @@ public class OpenIddictServerBuilderTests
var services = CreateServices(); var services = CreateServices();
var builder = CreateBuilder(services); var builder = CreateBuilder(services);
builder.AddDevelopmentEncryptionCertificate(
subject: new X500DistinguishedName("CN=" + Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture)));
var serviceProvider = services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<OpenIddictServerOptions>>();
// Act and assert // Act and assert
var exception = Assert.Throws<PlatformNotSupportedException>(() => builder.AddDevelopmentEncryptionCertificate( var exception = Assert.Throws<PlatformNotSupportedException>(() => options.Value);
subject: new X500DistinguishedName("CN=" + Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture))));
Assert.Equal("X.509 certificate generation is not supported on this platform.", exception.Message); Assert.Equal("X.509 certificate generation is not supported on this platform.", exception.Message);
} }
@ -353,9 +359,15 @@ public class OpenIddictServerBuilderTests
var services = CreateServices(); var services = CreateServices();
var builder = CreateBuilder(services); var builder = CreateBuilder(services);
builder.AddDevelopmentSigningCertificate(
subject: new X500DistinguishedName("CN=" + Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture)));
var serviceProvider = services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<OpenIddictServerOptions>>();
// Act and assert // Act and assert
var exception = Assert.Throws<PlatformNotSupportedException>(() => builder.AddDevelopmentSigningCertificate( var exception = Assert.Throws<PlatformNotSupportedException>(() => options.Value);
subject: new X500DistinguishedName("CN=" + Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture))));
Assert.Equal("X.509 certificate generation is not supported on this platform.", exception.Message); Assert.Equal("X.509 certificate generation is not supported on this platform.", exception.Message);
} }

Loading…
Cancel
Save