Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use TimeProvider #2071

Merged
merged 30 commits into from
May 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
304f05a
Use TimeProvider
trejjam May 10, 2024
a76a3f3
Use TimeProvider.GetUtcNow
trejjam May 10, 2024
67d9874
Set TimeProvider if not set
trejjam May 10, 2024
6d11cb0
Replace DateTime.Now with TimeProvider.GetUtcNow().DateTime
trejjam May 10, 2024
1262d0f
Fix tests
trejjam May 10, 2024
972b4fa
Replace forgotten DateTimeOffset.UtcNow
trejjam May 10, 2024
7fca60e
Fix XML doc
trejjam May 10, 2024
ea0d2b7
Return duplicated space
trejjam May 10, 2024
51c8188
Drop TimeProvider from tests
trejjam May 10, 2024
252850b
Inline GetUtcNow extension method
trejjam May 10, 2024
c95824d
Implement AddDevelopment*Certificate(subject, DateTimeOffset) as exte…
trejjam May 10, 2024
eeede96
Drop IOptionWithTimeProvider
trejjam May 10, 2024
3907ec3
Drop TimeProviderExtensions
trejjam May 10, 2024
893826e
Drop TimeProvider set in tests
trejjam May 10, 2024
3fa99dd
Fix CS
trejjam May 10, 2024
1a31bd3
Try to resolve TimeProvider from container
trejjam May 10, 2024
a0fa491
Rollback samples changes
trejjam May 10, 2024
b9e64eb
Use DateTime in AddDevelopment*Certificate
trejjam May 10, 2024
5d130ba
Fix OpenIddictQuartzConfigurationTests
trejjam May 10, 2024
83a86c2
Fix CS
trejjam May 10, 2024
0557320
Use TimeProvider in AddDevelopment*Certificate
trejjam May 10, 2024
f4ef680
Use DateTimeOffset.LocalDateTime
trejjam May 10, 2024
28321dc
Fix AddDevelopmentEncryptionCertificate_ThrowsAnExceptionOnUnsupporte…
trejjam May 10, 2024
a8e0fa7
Add spaces
trejjam May 10, 2024
6195f80
Use now instead of notBefore
trejjam May 10, 2024
b163d3b
Make IServiceProvider non-nullable
trejjam May 10, 2024
44eecb5
Fix XML doc
trejjam May 10, 2024
8c24ddc
Add missing inheritdoc
trejjam May 10, 2024
3716666
Use .LocalDateTime fot working with certificate
trejjam May 10, 2024
77f0cad
Add a parameterless constructor to OpenIddictQuartzConfiguration and …
kevinchalet May 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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