Skip to content

Commit

Permalink
Use System.TimeProvider on .NET 8.0+
Browse files Browse the repository at this point in the history
  • Loading branch information
trejjam committed May 12, 2024
1 parent 7dfa52d commit 2475ed3
Show file tree
Hide file tree
Showing 27 changed files with 616 additions and 256 deletions.
207 changes: 115 additions & 92 deletions src/OpenIddict.Client/OpenIddictClientBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<X509Certificate2>()
.ToList();

if (!certificates.Exists(static certificate => certificate.NotBefore < DateTime.Now && certificate.NotAfter > DateTime.Now))
Services.AddOptions<OpenIddictClientOptions>().Configure<IServiceProvider>((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<TimeProvider>())?.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<X509Certificate2>()
.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;
}

/// <summary>
Expand Down Expand Up @@ -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<X509Certificate2>()
.ToList();

if (!certificates.Exists(static certificate => certificate.NotBefore < DateTime.Now && certificate.NotAfter > DateTime.Now))
Services.AddOptions<OpenIddictClientOptions>().Configure<IServiceProvider>((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<TimeProvider>())?.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<X509Certificate2>()
.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;
}

/// <summary>
Expand Down
38 changes: 32 additions & 6 deletions src/OpenIddict.Client/OpenIddictClientConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -23,13 +25,26 @@ namespace OpenIddict.Client;
public sealed class OpenIddictClientConfiguration : IPostConfigureOptions<OpenIddictClientOptions>
{
private readonly OpenIddictClientService _service;
private readonly IServiceProvider _provider;

/// <summary>
/// Creates a new instance of the <see cref="OpenIddictClientConfiguration"/> class.
/// </summary>
/// <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)
=> _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/>
public void PostConfigure(string? name, OpenIddictClientOptions options)
Expand All @@ -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>() ?? TimeProvider.System;
#endif

foreach (var registration in options.Registrations)
{
if (registration.Issuer is null)
Expand Down Expand Up @@ -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)
Expand All @@ -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,
Expand All @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 5 additions & 1 deletion src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading

0 comments on commit 2475ed3

Please sign in to comment.