Browse Source

Add the ability to re-hash client secrets dynamically and make the PBKDF2 options configurable

pull/2472/head
Kévin Chalet 4 months ago
parent
commit
7404f1438c
  1. 2
      sandbox/OpenIddict.Sandbox.AspNetCore.Client/Program.cs
  2. 22
      src/OpenIddict.Abstractions/OpenIddictResources.resx
  3. 184
      src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs
  4. 9
      src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs
  5. 6
      src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs
  6. 72
      src/OpenIddict.Core/OpenIddictCoreBuilder.cs
  7. 41
      src/OpenIddict.Core/OpenIddictCoreConfiguration.cs
  8. 4
      src/OpenIddict.Core/OpenIddictCoreExtensions.cs
  9. 36
      src/OpenIddict.Core/OpenIddictCoreOptions.cs
  10. 7
      test/OpenIddict.Client.IntegrationTests/OpenIddict.Client.IntegrationTests.csproj
  11. 437
      test/OpenIddict.Core.Tests/Caches/OpenIddictApplicationCacheTests.cs
  12. 330
      test/OpenIddict.Core.Tests/Caches/OpenIddictAuthorizationCacheTests.cs
  13. 417
      test/OpenIddict.Core.Tests/Caches/OpenIddictScopeCacheTests.cs
  14. 503
      test/OpenIddict.Core.Tests/Caches/OpenIddictTokenCacheTests.cs
  15. 3561
      test/OpenIddict.Core.Tests/Managers/OpenIddictApplicationManagerTests.cs
  16. 1290
      test/OpenIddict.Core.Tests/Managers/OpenIddictAuthorizationManagerTests.cs
  17. 977
      test/OpenIddict.Core.Tests/Managers/OpenIddictScopeManagerTests.cs
  18. 1343
      test/OpenIddict.Core.Tests/Managers/OpenIddictTokenManagerTests.cs
  19. 4
      test/OpenIddict.Core.Tests/OpenIddict.Core.Tests.csproj
  20. 255
      test/OpenIddict.Core.Tests/OpenIddictCoreBuilderTests.cs
  21. 303
      test/OpenIddict.Core.Tests/OpenIddictCoreConfigurationTests.cs
  22. 7
      test/OpenIddict.Core.Tests/OpenIddictCoreExtensionsTests.cs
  23. 152
      test/OpenIddict.Core.Tests/OpenIddictCoreOptionsTests.cs
  24. 7
      test/OpenIddict.Server.IntegrationTests/OpenIddict.Server.IntegrationTests.csproj
  25. 7
      test/OpenIddict.Validation.IntegrationTests/OpenIddict.Validation.IntegrationTests.csproj

2
sandbox/OpenIddict.Sandbox.AspNetCore.Client/Program.cs

@ -97,6 +97,8 @@ builder.Services.AddOpenIddict()
RedirectUri = new Uri("callback/login/local", UriKind.Relative), RedirectUri = new Uri("callback/login/local", UriKind.Relative),
PostLogoutRedirectUri = new Uri("callback/logout/local", UriKind.Relative), PostLogoutRedirectUri = new Uri("callback/logout/local", UriKind.Relative),
// ClientSecret = "emCimpdc9SeOaZzN5jzm4_eek-STF6VenfVlKO1_qt0",
//
// On supported platforms, this application can authenticate using 3 different client // On supported platforms, this application can authenticate using 3 different client
// authentication methods that all offer a higher security level than shared client secrets: // authentication methods that all offer a higher security level than shared client secrets:
// //

22
src/OpenIddict.Abstractions/OpenIddictResources.resx

@ -849,10 +849,10 @@ To register the validation services, use 'services.AddOpenIddict().AddValidation
<value>The secret cannot be null or empty.</value> <value>The secret cannot be null or empty.</value>
</data> </data>
<data name="ID0217" xml:space="preserve"> <data name="ID0217" xml:space="preserve">
<value>The specified hash algorithm is not valid.</value> <value>The specified hash algorithm ({0}) is not valid or is not supported by the platform.</value>
</data> </data>
<data name="ID0218" xml:space="preserve"> <data name="ID0218" xml:space="preserve">
<value>The comparand cannot be null or empty.</value> <value>The client secret payload stored in the database is malformed, uses an unsupported version or doesn't meet the minimum security requirements and may have been tampered with.</value>
</data> </data>
<data name="ID0219" xml:space="preserve"> <data name="ID0219" xml:space="preserve">
<value>One or more validation error(s) occurred while trying to create a new authorization:</value> <value>One or more validation error(s) occurred while trying to create a new authorization:</value>
@ -1869,6 +1869,15 @@ To use a custom policy relying on the system store, set 'OpenIddictServerOptions
<data name="ID0517" xml:space="preserve"> <data name="ID0517" xml:space="preserve">
<value>The '{0}' grant type is already assigned to a standard grant type and cannot be used for custom flows.</value> <value>The '{0}' grant type is already assigned to a standard grant type and cannot be used for custom flows.</value>
</data> </data>
<data name="ID0518" xml:space="preserve">
<value>The number of iterations for derivation of client secrets cannot be lower than {0} or higher than {1}.</value>
</data>
<data name="ID0519" xml:space="preserve">
<value>The length of salts used for derivation of client secrets cannot be lower than {0} or higher than {1} bits.</value>
</data>
<data name="ID0520" xml:space="preserve">
<value>The output length used for derivation of client secrets cannot be lower than {0} or higher than {1} bits.</value>
</data>
<data name="ID2000" xml:space="preserve"> <data name="ID2000" xml:space="preserve">
<value>The security token is missing.</value> <value>The security token is missing.</value>
</data> </data>
@ -2549,6 +2558,9 @@ To use a custom policy relying on the system store, set 'OpenIddictServerOptions
<data name="ID4020" xml:space="preserve"> <data name="ID4020" xml:space="preserve">
<value>The X.509 client certificate shouldn't be null at this point.</value> <value>The X.509 client certificate shouldn't be null at this point.</value>
</data> </data>
<data name="ID4021" xml:space="preserve">
<value>The length of the memory span ({0}) doesn't match the expected value ({1}).</value>
</data>
<data name="ID6000" xml:space="preserve"> <data name="ID6000" xml:space="preserve">
<value>An error occurred while validating the token '{Token}'.</value> <value>An error occurred while validating the token '{Token}'.</value>
</data> </data>
@ -3332,6 +3344,12 @@ This may indicate that the hashed entry is corrupted or malformed.</value>
<data name="ID6293" xml:space="preserve"> <data name="ID6293" xml:space="preserve">
<value>Certificate validation failed because the token binding certificate provided by an anonymous client was not valid: {Errors}.</value> <value>Certificate validation failed because the token binding certificate provided by an anonymous client was not valid: {Errors}.</value>
</data> </data>
<data name="ID6294" xml:space="preserve">
<value>The client secret attached to the client application {ClientId} was automatically re-hashed to match the new settings.</value>
</data>
<data name="ID6295" xml:space="preserve">
<value>The client secret attached to the client application {ClientId} couldn't be automatically re-hashed due to a concurency exception.</value>
</data>
<data name="ID8000" xml:space="preserve"> <data name="ID8000" xml:space="preserve">
<value>https://documentation.openiddict.com/errors/{0}</value> <value>https://documentation.openiddict.com/errors/{0}</value>
</data> </data>

184
src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs

@ -4,9 +4,11 @@
* the license and the contributors participating to this project. * the license and the contributors participating to this project.
*/ */
using System.Buffers;
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
@ -894,7 +896,7 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
ArgumentNullException.ThrowIfNull(application); ArgumentNullException.ThrowIfNull(application);
ArgumentException.ThrowIfNullOrEmpty(type); ArgumentException.ThrowIfNullOrEmpty(type);
return string.Equals(await GetApplicationTypeAsync(application, cancellationToken), type, StringComparison.OrdinalIgnoreCase); return string.Equals(await GetApplicationTypeAsync(application, cancellationToken), type, StringComparison.Ordinal);
} }
/// <summary> /// <summary>
@ -910,7 +912,7 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
ArgumentNullException.ThrowIfNull(application); ArgumentNullException.ThrowIfNull(application);
ArgumentException.ThrowIfNullOrEmpty(type); ArgumentException.ThrowIfNullOrEmpty(type);
return string.Equals(await GetClientTypeAsync(application, cancellationToken), type, StringComparison.OrdinalIgnoreCase); return string.Equals(await GetClientTypeAsync(application, cancellationToken), type, StringComparison.Ordinal);
} }
/// <summary> /// <summary>
@ -926,7 +928,7 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
ArgumentNullException.ThrowIfNull(application); ArgumentNullException.ThrowIfNull(application);
ArgumentException.ThrowIfNullOrEmpty(type); ArgumentException.ThrowIfNullOrEmpty(type);
return string.Equals(await GetConsentTypeAsync(application, cancellationToken), type, StringComparison.OrdinalIgnoreCase); return string.Equals(await GetConsentTypeAsync(application, cancellationToken), type, StringComparison.Ordinal);
} }
/// <summary> /// <summary>
@ -1284,22 +1286,21 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
else else
{ {
// Ensure the application type is supported by the manager. // Ensure the application type is supported by the manager.
if (!string.Equals(type, ClientTypes.Confidential, StringComparison.OrdinalIgnoreCase) && if (type is not (ClientTypes.Confidential or ClientTypes.Public))
!string.Equals(type, ClientTypes.Public, StringComparison.OrdinalIgnoreCase))
{ {
yield return new ValidationResult(SR.GetResourceString(SR.ID2112)); yield return new ValidationResult(SR.GetResourceString(SR.ID2112));
} }
// Ensure no client secret was specified if the client is a public application. // Ensure no client secret was specified if the client is a public application.
var secret = await Store.GetClientSecretAsync(application, cancellationToken); var secret = await Store.GetClientSecretAsync(application, cancellationToken);
if (!string.IsNullOrEmpty(secret) && string.Equals(type, ClientTypes.Public, StringComparison.OrdinalIgnoreCase)) if (!string.IsNullOrEmpty(secret) && type is ClientTypes.Public)
{ {
yield return new ValidationResult(SR.GetResourceString(SR.ID2114)); yield return new ValidationResult(SR.GetResourceString(SR.ID2114));
} }
// Ensure a client secret or a JSON Web Key suitable for signing // Ensure a client secret or a JSON Web Key suitable for signing
// was specified if the client is a confidential application. // was specified if the client is a confidential application.
if (string.IsNullOrEmpty(secret) && string.Equals(type, ClientTypes.Confidential, StringComparison.OrdinalIgnoreCase)) if (string.IsNullOrEmpty(secret) && type is ClientTypes.Confidential)
{ {
var set = await Store.GetJsonWebKeySetAsync(application, cancellationToken); var set = await Store.GetJsonWebKeySetAsync(application, cancellationToken);
if (set?.Keys is null || !set.Keys.Any(static key => if (set?.Keys is null || !set.Keys.Any(static key =>
@ -1395,13 +1396,35 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
return false; return false;
} }
if (!await ValidateClientSecretAsync(secret, value, cancellationToken)) var result = await ValidateClientSecretAsync(secret, value, cancellationToken);
if (!result.IsValid)
{ {
Logger.LogInformation(6161, SR.GetResourceString(SR.ID6161), await GetClientIdAsync(application, cancellationToken)); Logger.LogInformation(6161, SR.GetResourceString(SR.ID6161), await GetClientIdAsync(application, cancellationToken));
return false; return false;
} }
// If the client secret was valid but a rehash is required, update the stored client secret with the new hash.
if (result.IsRehashRequired && !Options.CurrentValue.DisableAutomaticClientSecretRehashing)
{
try
{
await UpdateAsync(application, secret, cancellationToken);
}
catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception))
{
// If a non-fatal exception is thrown, ignore it: the client secret will be updated the next time it is validated again.
Logger.LogDebug(6295, exception, SR.GetResourceString(SR.ID6295), await GetClientIdAsync(application, cancellationToken));
return true;
}
Logger.LogInformation(6294, SR.GetResourceString(SR.ID6294), await GetClientIdAsync(application, cancellationToken));
return true;
}
return true; return true;
} }
@ -1729,30 +1752,37 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
{ {
ArgumentException.ThrowIfNullOrEmpty(secret); ArgumentException.ThrowIfNullOrEmpty(secret);
// Note: the PRF, iteration count, salt length and key length currently all match the default values
// used by CryptoHelper and ASP.NET Core Identity but this may change in the future, if necessary.
var salt = RandomNumberGenerator.GetBytes(count: 128 / 8);
var hash = HashSecret(secret, salt, HashAlgorithmName.SHA256, iterations: 10_000, length: 256 / 8);
return new(Convert.ToBase64String(hash));
// Note: the following logic deliberately uses the same format as CryptoHelper (used in OpenIddict 1.x/2.x), // Note: the following logic deliberately uses the same format as CryptoHelper (used in OpenIddict 1.x/2.x),
// which was itself based on ASP.NET Core Identity's latest hashed password format. This guarantees that // which was itself based on ASP.NET Core Identity's latest hashed password format. This guarantees that
// secrets hashed using a recent OpenIddict version can still be read by older packages (and vice versa). // secrets hashed using a recent OpenIddict version can still be read by older packages (and vice versa).
static byte[] HashSecret(string secret, byte[] salt, HashAlgorithmName algorithm, int iterations, int length) var options = Options.CurrentValue;
{
var key = Rfc2898DeriveBytes.Pbkdf2(secret, salt, iterations, algorithm, length); var salt = RandomNumberGenerator.GetBytes(options.ClientSecretKeyDerivationSaltLength / 8);
var payload = new byte[13 + salt.Length + key.Length]; var key = Rfc2898DeriveBytes.Pbkdf2(secret, salt,
options.ClientSecretKeyDerivationIterations,
options.ClientSecretKeyDerivationHashAlgorithm,
options.ClientSecretKeyDerivationOutputLength / 8);
var length = 1 + sizeof(uint) * 3 + salt.Length + key.Length;
// To avoid unnecessary allocations on the heap, use a stack-allocated buffer when the total length is less
// than 256 bytes. Otherwise, rent a buffer from the shared array pool and return it to the pool after use.
byte[]? array = null;
Span<byte> payload = (length is <= 256
? stackalloc byte[256]
: (array = ArrayPool<byte>.Shared.Rent(minimumLength: length)))[..length];
Debug.Assert(payload.Length == length, SR.FormatID4021(payload.Length, length));
try
{
// Write the format marker. // Write the format marker.
payload[0] = 0x01; payload[0] = 0x01;
// Write the hashing algorithm version. // Write the hashing algorithm version.
BinaryPrimitives.WriteUInt32BigEndian(payload.AsSpan(1, sizeof(uint)), algorithm switch BinaryPrimitives.WriteUInt32BigEndian(payload.Slice(1, sizeof(uint)), options.ClientSecretKeyDerivationHashAlgorithm switch
{ {
var name when name == HashAlgorithmName.SHA1 => 0, var name when name == HashAlgorithmName.SHA1 => 0,
var name when name == HashAlgorithmName.SHA256 => 1, var name when name == HashAlgorithmName.SHA256 => 1,
var name when name == HashAlgorithmName.SHA512 => 2, var name when name == HashAlgorithmName.SHA512 => 2,
@ -1760,18 +1790,27 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
}); });
// Write the iteration count of the algorithm. // Write the iteration count of the algorithm.
BinaryPrimitives.WriteUInt32BigEndian(payload.AsSpan(5, sizeof(uint)), (uint) iterations); BinaryPrimitives.WriteUInt32BigEndian(payload.Slice(5, sizeof(uint)), (uint) options.ClientSecretKeyDerivationIterations);
// Write the size of the salt. // Write the size of the salt.
BinaryPrimitives.WriteUInt32BigEndian(payload.AsSpan(9, sizeof(uint)), (uint) salt.Length); BinaryPrimitives.WriteUInt32BigEndian(payload.Slice(9, sizeof(uint)), (uint) salt.Length);
// Write the salt. // Write the salt.
salt.CopyTo(payload.AsSpan(13)); salt.CopyTo(payload.Slice(13, salt.Length));
// Write the subkey. // Write the subkey.
key.CopyTo(payload.AsSpan(13 + salt.Length)); key.CopyTo(payload.Slice(13 + salt.Length, key.Length));
return payload; return new(Convert.ToBase64String(payload, Base64FormattingOptions.None));
}
finally
{
// Return the rented buffer to the pool if one was used.
if (array is not null)
{
ArrayPool<byte>.Shared.Return(array, clearArray: true);
}
} }
} }
@ -1783,43 +1822,31 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
/// <param name="comparand">The value stored in the database, which is usually a hashed representation of the secret.</param> /// <param name="comparand">The value stored in the database, which is usually a hashed representation of the secret.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param> /// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns> /// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation, /// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation, whose result returns
/// whose result returns a boolean indicating whether the specified value was valid. /// a tuple indicating whether the client secret was valid and whether the client secret should be re-hashed.
/// </returns> /// </returns>
protected virtual ValueTask<bool> ValidateClientSecretAsync( protected virtual ValueTask<(bool IsValid, bool IsRehashRequired)> ValidateClientSecretAsync(
string secret, string comparand, CancellationToken cancellationToken = default) string secret, string comparand, CancellationToken cancellationToken = default)
{ {
ArgumentException.ThrowIfNullOrEmpty(secret); ArgumentException.ThrowIfNullOrEmpty(secret);
ArgumentException.ThrowIfNullOrEmpty(comparand); ArgumentException.ThrowIfNullOrEmpty(comparand);
try
{
return new(VerifyHashedSecret(comparand, secret));
}
catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception))
{
Logger.LogWarning(6163, exception, SR.GetResourceString(SR.ID6163));
return new(false);
}
// Note: the following logic deliberately uses the same format as CryptoHelper (used in OpenIddict 1.x/2.x), // Note: the following logic deliberately uses the same format as CryptoHelper (used in OpenIddict 1.x/2.x),
// which was itself based on ASP.NET Core Identity's latest hashed password format. This guarantees that // which was itself based on ASP.NET Core Identity's latest hashed password format. This guarantees that
// secrets hashed using a recent OpenIddict version can still be read by older packages (and vice versa). // secrets hashed using a recent OpenIddict version can still be read by older packages (and vice versa).
static bool VerifyHashedSecret(string hash, string secret) try
{ {
var payload = new ReadOnlySpan<byte>(Convert.FromBase64String(hash)); ReadOnlySpan<byte> payload = Convert.FromBase64String(comparand);
if (payload.Length is 0) if (payload is [])
{ {
return false; throw new ArgumentException(SR.GetResourceString(SR.ID0218), nameof(comparand));
} }
// Verify the hashing format version. // Verify the hashing format version.
if (payload[0] is not 0x01) if (payload[0] is not 0x01)
{ {
return false; throw new ArgumentException(SR.GetResourceString(SR.ID0218), nameof(comparand));
} }
// Read the hashing algorithm version. // Read the hashing algorithm version.
@ -1829,32 +1856,67 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
1 => HashAlgorithmName.SHA256, 1 => HashAlgorithmName.SHA256,
2 => HashAlgorithmName.SHA512, 2 => HashAlgorithmName.SHA512,
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0217)) _ => throw new ArgumentException(SR.GetResourceString(SR.ID0218), nameof(comparand))
}; };
// Read the iteration count of the algorithm. // Read the iteration count of the algorithm and ensure it's more than
// 10 000 iterations, which is the value used in previous OpenIddict versions.
var iterations = (int) BinaryPrimitives.ReadUInt32BigEndian(payload.Slice(5, sizeof(uint))); var iterations = (int) BinaryPrimitives.ReadUInt32BigEndian(payload.Slice(5, sizeof(uint)));
if (iterations is not (>= 10_000 and <= 10_000_000))
{
throw new ArgumentException(SR.GetResourceString(SR.ID0218), nameof(comparand));
}
// Read the size of the salt and ensure it's more than 128 bits. // Read the size of the salt and ensure it's more than 128 bits,
var saltLength = (int) BinaryPrimitives.ReadUInt32BigEndian(payload.Slice(9, sizeof(uint))); // which is the value used in previous OpenIddict versions.
if (saltLength is < 128 / 8) var length = (int) BinaryPrimitives.ReadUInt32BigEndian(payload.Slice(9, sizeof(uint)));
if (length is not (>= 128 / 8 and <= 1024 / 8))
{ {
return false; throw new ArgumentException(SR.GetResourceString(SR.ID0218), nameof(comparand));
} }
// Read the salt. // Read the salt.
var salt = payload.Slice(13, saltLength); var salt = payload.Slice(13, length);
// Ensure the derived key length is more than 128 bits. // Ensure the derived key length is more than 128 bits,
var keyLength = payload.Length - 13 - salt.Length; // which is the value used in previous OpenIddict versions.
if (keyLength is < 128 / 8) length = payload.Length - 13 - salt.Length;
if (length is not (>= 128 / 8 and <= 2048 / 8))
{ {
return false; throw new ArgumentException(SR.GetResourceString(SR.ID0218), nameof(comparand));
} }
return CryptographicOperations.FixedTimeEquals( // Read the derived key.
left : payload.Slice(13 + salt.Length, keyLength), var key = payload.Slice(13 + salt.Length, length);
right: Rfc2898DeriveBytes.Pbkdf2(secret, salt.ToArray(), iterations, algorithm, keyLength));
// Hash the specified client secret with the same salt, iteration count and algorithm as the
// stored value, and compare the results: if they don't match, the client secret is invalid.
if (!CryptographicOperations.FixedTimeEquals(key, Rfc2898DeriveBytes.Pbkdf2(
secret, salt, iterations, algorithm, key.Length)))
{
return new((IsValid: false, IsRehashRequired: false));
}
var options = Options.CurrentValue;
// Note: if the client secret is valid but one of the key derivation options is not strictly identical (even
// when the application is now configured to use a lower security level), indicate that a rehash is required.
//
// This deliberately differs from ASP.NET Core Identity's logic, which only considers that a rehash is required
// when the iteration count configured in the password options is higher than the one extracted from the payload:
// doing that allows developers to select a lower security level (e.g fewer iterations or a less expensive hash
// algorithm) if the previous settings used in production proved to be too slow for their needs.
return new((IsValid: true, IsRehashRequired: algorithm != options.ClientSecretKeyDerivationHashAlgorithm ||
iterations != options.ClientSecretKeyDerivationIterations ||
salt.Length != options.ClientSecretKeyDerivationSaltLength / 8 ||
key.Length != options.ClientSecretKeyDerivationOutputLength / 8));
}
catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception))
{
Logger.LogWarning(6163, exception, SR.GetResourceString(SR.ID6163));
return new((IsValid: false, IsRehashRequired: false));
} }
} }

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

@ -630,7 +630,7 @@ public class OpenIddictAuthorizationManager<TAuthorization> : IOpenIddictAuthori
ArgumentNullException.ThrowIfNull(authorization); ArgumentNullException.ThrowIfNull(authorization);
ArgumentException.ThrowIfNullOrEmpty(status); ArgumentException.ThrowIfNullOrEmpty(status);
return string.Equals(await GetStatusAsync(authorization, cancellationToken), status, StringComparison.OrdinalIgnoreCase); return string.Equals(await GetStatusAsync(authorization, cancellationToken), status, StringComparison.Ordinal);
} }
/// <summary> /// <summary>
@ -646,7 +646,7 @@ public class OpenIddictAuthorizationManager<TAuthorization> : IOpenIddictAuthori
ArgumentNullException.ThrowIfNull(authorization); ArgumentNullException.ThrowIfNull(authorization);
ArgumentException.ThrowIfNullOrEmpty(type); ArgumentException.ThrowIfNullOrEmpty(type);
return string.Equals(await GetTypeAsync(authorization, cancellationToken), type, StringComparison.OrdinalIgnoreCase); return string.Equals(await GetTypeAsync(authorization, cancellationToken), type, StringComparison.Ordinal);
} }
/// <summary> /// <summary>
@ -811,7 +811,7 @@ public class OpenIddictAuthorizationManager<TAuthorization> : IOpenIddictAuthori
ArgumentNullException.ThrowIfNull(authorization); ArgumentNullException.ThrowIfNull(authorization);
var status = await Store.GetStatusAsync(authorization, cancellationToken); var status = await Store.GetStatusAsync(authorization, cancellationToken);
if (string.Equals(status, Statuses.Revoked, StringComparison.OrdinalIgnoreCase)) if (status is Statuses.Revoked)
{ {
return true; return true;
} }
@ -935,8 +935,7 @@ public class OpenIddictAuthorizationManager<TAuthorization> : IOpenIddictAuthori
yield return new ValidationResult(SR.GetResourceString(SR.ID2116)); yield return new ValidationResult(SR.GetResourceString(SR.ID2116));
} }
else if (!string.Equals(type, AuthorizationTypes.AdHoc, StringComparison.OrdinalIgnoreCase) && else if (type is not (AuthorizationTypes.AdHoc or AuthorizationTypes.Permanent))
!string.Equals(type, AuthorizationTypes.Permanent, StringComparison.OrdinalIgnoreCase))
{ {
yield return new ValidationResult(SR.GetResourceString(SR.ID2117)); yield return new ValidationResult(SR.GetResourceString(SR.ID2117));
} }

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

@ -708,7 +708,7 @@ public class OpenIddictTokenManager<TToken> : IOpenIddictTokenManager where TTok
ArgumentNullException.ThrowIfNull(token); ArgumentNullException.ThrowIfNull(token);
ArgumentException.ThrowIfNullOrEmpty(status); ArgumentException.ThrowIfNullOrEmpty(status);
return string.Equals(await GetStatusAsync(token, cancellationToken), status, StringComparison.OrdinalIgnoreCase); return string.Equals(await GetStatusAsync(token, cancellationToken), status, StringComparison.Ordinal);
} }
/// <summary> /// <summary>
@ -723,7 +723,7 @@ public class OpenIddictTokenManager<TToken> : IOpenIddictTokenManager where TTok
ArgumentNullException.ThrowIfNull(token); ArgumentNullException.ThrowIfNull(token);
ArgumentException.ThrowIfNullOrEmpty(type); ArgumentException.ThrowIfNullOrEmpty(type);
return string.Equals(await GetTypeAsync(token, cancellationToken), type, StringComparison.OrdinalIgnoreCase); return string.Equals(await GetTypeAsync(token, cancellationToken), type, StringComparison.Ordinal);
} }
/// <summary> /// <summary>
@ -745,7 +745,7 @@ public class OpenIddictTokenManager<TToken> : IOpenIddictTokenManager where TTok
for (var index = 0; index < types.Length; index++) for (var index = 0; index < types.Length; index++)
{ {
if (string.Equals(type, types[index], StringComparison.OrdinalIgnoreCase)) if (string.Equals(type, types[index], StringComparison.Ordinal))
{ {
return true; return true;
} }

72
src/OpenIddict.Core/OpenIddictCoreBuilder.cs

@ -6,6 +6,7 @@
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Security.Cryptography;
using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.DependencyInjection.Extensions;
using OpenIddict.Core; using OpenIddict.Core;
@ -372,6 +373,7 @@ public sealed class OpenIddictCoreBuilder
/// Disabling this feature MAY result in security vulnerabilities in the other cases. /// Disabling this feature MAY result in security vulnerabilities in the other cases.
/// </summary> /// </summary>
/// <returns>The <see cref="OpenIddictCoreBuilder"/> instance.</returns> /// <returns>The <see cref="OpenIddictCoreBuilder"/> instance.</returns>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public OpenIddictCoreBuilder DisableAdditionalFiltering() public OpenIddictCoreBuilder DisableAdditionalFiltering()
=> Configure(options => options.DisableAdditionalFiltering = true); => Configure(options => options.DisableAdditionalFiltering = true);
@ -381,9 +383,79 @@ public sealed class OpenIddictCoreBuilder
/// of your application and result in multiple queries being sent by the stores. /// of your application and result in multiple queries being sent by the stores.
/// </summary> /// </summary>
/// <returns>The <see cref="OpenIddictCoreBuilder"/> instance.</returns> /// <returns>The <see cref="OpenIddictCoreBuilder"/> instance.</returns>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public OpenIddictCoreBuilder DisableEntityCaching() public OpenIddictCoreBuilder DisableEntityCaching()
=> Configure(options => options.DisableEntityCaching = true); => Configure(options => options.DisableEntityCaching = true);
/// <summary>
/// Disables the automatic client secret rehashing applied by the application
/// manager when a client secret is validated and needs to be rehashed.
/// </summary>
/// <returns>The <see cref="OpenIddictCoreBuilder"/> instance.</returns>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public OpenIddictCoreBuilder DisableAutomaticClientSecretRehashing()
=> Configure(options => options.DisableAutomaticClientSecretRehashing = true);
/// <summary>
/// Configures OpenIddict to use the specified hash algorithm to protect the client secrets.
/// </summary>
/// <param name="algorithm">The hash algorithm to use.</param>
/// <returns>The <see cref="OpenIddictCoreBuilder"/> instance.</returns>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public OpenIddictCoreBuilder SetClientSecretKeyDerivationHashAlgorithm(HashAlgorithmName algorithm)
{
if (algorithm != HashAlgorithmName.SHA1 &&
algorithm != HashAlgorithmName.SHA256 &&
algorithm != HashAlgorithmName.SHA512)
{
throw new ArgumentException(SR.FormatID0217(algorithm.Name), nameof(algorithm));
}
return Configure(options => options.ClientSecretKeyDerivationHashAlgorithm = algorithm);
}
/// <summary>
/// Configures OpenIddict to use the specified number of iterations to protect the client secrets.
/// </summary>
/// <param name="iterations">The number of iterations to use.</param>
/// <returns>The <see cref="OpenIddictCoreBuilder"/> instance.</returns>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public OpenIddictCoreBuilder SetClientSecretKeyDerivationIterations(int iterations)
{
ArgumentOutOfRangeException.ThrowIfLessThan(iterations, 10_000);
ArgumentOutOfRangeException.ThrowIfGreaterThan(iterations, 10_000_000);
return Configure(options => options.ClientSecretKeyDerivationIterations = iterations);
}
/// <summary>
/// Configures OpenIddict to use the specified output length (in bits) to protect the client secrets.
/// </summary>
/// <param name="length">The output length to use.</param>
/// <returns>The <see cref="OpenIddictCoreBuilder"/> instance.</returns>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public OpenIddictCoreBuilder SetClientSecretKeyDerivationOutputLength(int length)
{
ArgumentOutOfRangeException.ThrowIfLessThan(length, 256);
ArgumentOutOfRangeException.ThrowIfGreaterThan(length, 2048);
return Configure(options => options.ClientSecretKeyDerivationOutputLength = length);
}
/// <summary>
/// Configures OpenIddict to use the specified salt length (in bits) to protect the client secrets.
/// </summary>
/// <param name="length">The salt length to use.</param>
/// <returns>The <see cref="OpenIddictCoreBuilder"/> instance.</returns>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public OpenIddictCoreBuilder SetClientSecretKeyDerivationSaltLength(int length)
{
ArgumentOutOfRangeException.ThrowIfLessThan(length, 128);
ArgumentOutOfRangeException.ThrowIfGreaterThan(length, 1024);
return Configure(options => options.ClientSecretKeyDerivationSaltLength = length);
}
/// <summary> /// <summary>
/// Configures OpenIddict to use the specified entity as the default application entity. /// Configures OpenIddict to use the specified entity as the default application entity.
/// </summary> /// </summary>

41
src/OpenIddict.Core/OpenIddictCoreConfiguration.cs

@ -4,6 +4,7 @@
* the license and the contributors participating to this project. * the license and the contributors participating to this project.
*/ */
using System.Security.Cryptography;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
@ -12,7 +13,7 @@ namespace OpenIddict.Core;
/// <summary> /// <summary>
/// Contains the methods required to ensure that the OpenIddict core configuration is valid. /// Contains the methods required to ensure that the OpenIddict core configuration is valid.
/// </summary> /// </summary>
public class OpenIddictCoreConfiguration : IPostConfigureOptions<OpenIddictCoreOptions> public class OpenIddictCoreConfiguration : IPostConfigureOptions<OpenIddictCoreOptions>, IValidateOptions<OpenIddictCoreOptions>
{ {
private readonly IServiceProvider _provider; private readonly IServiceProvider _provider;
@ -26,6 +27,44 @@ public class OpenIddictCoreConfiguration : IPostConfigureOptions<OpenIddictCoreO
/// <inheritdoc/> /// <inheritdoc/>
public void PostConfigure(string? name, OpenIddictCoreOptions options) public void PostConfigure(string? name, OpenIddictCoreOptions options)
{ {
ArgumentNullException.ThrowIfNull(options);
options.TimeProvider ??= _provider.GetService<TimeProvider>() ?? TimeProvider.System; options.TimeProvider ??= _provider.GetService<TimeProvider>() ?? TimeProvider.System;
} }
/// <inheritdoc/>
public ValidateOptionsResult Validate(string? name, OpenIddictCoreOptions options)
{
ArgumentNullException.ThrowIfNull(options);
var builder = new ValidateOptionsResultBuilder();
// Ensure the options used to feed the PBKDF-based client secret protector respect
// the minimum security requirements used in previous versions of OpenIddict.
//
// Note: the values used here MUST be kept in sync with the values in the application manager.
if (options.ClientSecretKeyDerivationHashAlgorithm != HashAlgorithmName.SHA1 &&
options.ClientSecretKeyDerivationHashAlgorithm != HashAlgorithmName.SHA256 &&
options.ClientSecretKeyDerivationHashAlgorithm != HashAlgorithmName.SHA512)
{
builder.AddError(SR.FormatID0217(options.ClientSecretKeyDerivationHashAlgorithm.Name));
}
if (options.ClientSecretKeyDerivationIterations is not (>= 10_000 and <= 10_000_000))
{
builder.AddError(SR.FormatID0518(10_000, 10_000_000));
}
if (options.ClientSecretKeyDerivationSaltLength is not (>= 128 and <= 1024))
{
builder.AddError(SR.FormatID0519(128, 1024));
}
if (options.ClientSecretKeyDerivationOutputLength is not (>= 256 and <= 2048))
{
builder.AddError(SR.FormatID0520(256, 2048));
}
return builder.Build();
}
} }

4
src/OpenIddict.Core/OpenIddictCoreExtensions.cs

@ -51,9 +51,11 @@ public static class OpenIddictCoreExtensions
builder.Services.TryAddScoped<IOpenIddictTokenManager>(static provider => builder.Services.TryAddScoped<IOpenIddictTokenManager>(static provider =>
throw new InvalidOperationException(SR.GetResourceString(SR.ID0472))); throw new InvalidOperationException(SR.GetResourceString(SR.ID0472)));
// 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(ServiceDescriptor.Singleton<
IPostConfigureOptions<OpenIddictCoreOptions>, OpenIddictCoreConfiguration>()); IPostConfigureOptions<OpenIddictCoreOptions>, OpenIddictCoreConfiguration>());
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<
IValidateOptions<OpenIddictCoreOptions>, OpenIddictCoreConfiguration>());
return new OpenIddictCoreBuilder(builder.Services); return new OpenIddictCoreBuilder(builder.Services);
} }

36
src/OpenIddict.Core/OpenIddictCoreOptions.cs

@ -4,6 +4,9 @@
* the license and the contributors participating to this project. * the license and the contributors participating to this project.
*/ */
using System.ComponentModel;
using System.Security.Cryptography;
namespace OpenIddict.Core; namespace OpenIddict.Core;
/// <summary> /// <summary>
@ -11,6 +14,30 @@ namespace OpenIddict.Core;
/// </summary> /// </summary>
public sealed class OpenIddictCoreOptions public sealed class OpenIddictCoreOptions
{ {
/// <summary>
/// Gets or sets the hash algorithm used to protect the client secrets (by default, SHA512).
/// </summary>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public HashAlgorithmName ClientSecretKeyDerivationHashAlgorithm { get; set; } = HashAlgorithmName.SHA512;
/// <summary>
/// Gets or sets the number of iterations used to protect the client secrets (by default, 100 000).
/// </summary>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public int ClientSecretKeyDerivationIterations { get; set; } = 100_000;
/// <summary>
/// Gets or sets the length (in bits) of the PBKDF2 key used to protect the client secrets (by default, 512 bits).
/// </summary>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public int ClientSecretKeyDerivationOutputLength { get; set; } = 512;
/// <summary>
/// Gets or sets the length (in bits) of the salt used to protect the client secrets (by default, 256 bits).
/// </summary>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public int ClientSecretKeyDerivationSaltLength { get; set; } = 256;
/// <summary> /// <summary>
/// Gets or sets a boolean indicating whether additional filtering should be disabled, /// Gets or sets a boolean indicating whether additional filtering should be disabled,
/// so that the OpenIddict managers don't execute a second check to ensure the results /// so that the OpenIddict managers don't execute a second check to ensure the results
@ -19,13 +46,21 @@ public sealed class OpenIddictCoreOptions
/// are guaranteed to execute case-sensitive filtering at the database level. /// are guaranteed to execute case-sensitive filtering at the database level.
/// Disabling this feature MAY result in security vulnerabilities in the other cases. /// Disabling this feature MAY result in security vulnerabilities in the other cases.
/// </summary> /// </summary>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public bool DisableAdditionalFiltering { get; set; } public bool DisableAdditionalFiltering { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether automatic client secret rehashing should be disabled.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public bool DisableAutomaticClientSecretRehashing { get; set; }
/// <summary> /// <summary>
/// Gets or sets a boolean indicating whether entity caching should be disabled. /// Gets or sets a boolean indicating whether entity caching should be disabled.
/// Disabling entity caching may have a noticeable impact on the performance /// Disabling entity caching may have a noticeable impact on the performance
/// of your application and result in multiple queries being sent by the stores. /// of your application and result in multiple queries being sent by the stores.
/// </summary> /// </summary>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public bool DisableEntityCaching { get; set; } public bool DisableEntityCaching { get; set; }
/// <summary> /// <summary>
@ -34,6 +69,7 @@ public sealed class OpenIddictCoreOptions
/// abnormally and doesn't cause a memory starvation or out-of-memory exceptions. /// abnormally and doesn't cause a memory starvation or out-of-memory exceptions.
/// 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>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public int EntityCacheLimit { get; set; } = 250; public int EntityCacheLimit { get; set; } = 250;
/// <summary> /// <summary>

7
test/OpenIddict.Client.IntegrationTests/OpenIddict.Client.IntegrationTests.csproj

@ -16,17 +16,12 @@
<PackageReference Include="Moq" /> <PackageReference Include="Moq" />
</ItemGroup> </ItemGroup>
<ItemGroup
Condition=" ('$(TargetFrameworkIdentifier)' == '.NETFramework') Or
('$(TargetFrameworkIdentifier)' == '.NETCoreApp' And $([MSBuild]::VersionLessThan($(TargetFrameworkVersion), '10.0'))) ">
<PackageReference Include="System.Linq.Async" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFrameworkIdentifier)' == '.NETFramework' "> <ItemGroup Condition=" '$(TargetFrameworkIdentifier)' == '.NETFramework' ">
<Reference Include="System.Net.Http" /> <Reference Include="System.Net.Http" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition=" '$(TargetFrameworkIdentifier)' == '.NETFramework' "> <ItemGroup Condition=" '$(TargetFrameworkIdentifier)' == '.NETFramework' ">
<PackageReference Include="System.Linq.Async" />
<PackageReference Include="System.Net.Http.Json" /> <PackageReference Include="System.Net.Http.Json" />
</ItemGroup> </ItemGroup>

437
test/OpenIddict.Core.Tests/Caches/OpenIddictApplicationCacheTests.cs

@ -0,0 +1,437 @@
/*
* 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.Options;
using Moq;
using Xunit;
namespace OpenIddict.Core.Tests;
public class OpenIddictApplicationCacheTests
{
[Fact]
public void Constructor_ThrowsAnExceptionForNullOptions()
{
// Arrange
var options = (IOptionsMonitor<OpenIddictCoreOptions>) null!;
var store = Mock.Of<IOpenIddictApplicationStore<object>>();
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => new OpenIddictApplicationCache<object>(options, store));
Assert.Equal("options", exception.ParamName);
}
[Fact]
public void Constructor_ThrowsAnExceptionForNullStore()
{
// Arrange
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = (IOpenIddictApplicationStore<object>) null!;
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => new OpenIddictApplicationCache<object>(options, store));
Assert.Equal("store", exception.ParamName);
}
[Fact]
public async Task AddAsync_ThrowsAnExceptionForNullApplication()
{
// Arrange
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = Mock.Of<IOpenIddictApplicationStore<OpenIddictApplication>>();
var cache = new OpenIddictApplicationCache<OpenIddictApplication>(options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => cache.AddAsync(application: null!, CancellationToken.None).AsTask());
Assert.Equal("application", exception.ParamName);
}
[Fact]
public void Dispose_CanBeCalledMultipleTimes()
{
// Arrange
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = Mock.Of<IOpenIddictApplicationStore<OpenIddictApplication>>();
var cache = new OpenIddictApplicationCache<OpenIddictApplication>(options, store);
// Act and assert
cache.Dispose();
cache.Dispose();
}
[Fact]
public async Task FindByClientIdAsync_ThrowsAnExceptionForNullIdentifier()
{
// Arrange
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = Mock.Of<IOpenIddictApplicationStore<OpenIddictApplication>>();
var cache = new OpenIddictApplicationCache<OpenIddictApplication>(options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => cache.FindByClientIdAsync(identifier: null!, CancellationToken.None).AsTask());
Assert.Equal("identifier", exception.ParamName);
}
[Fact]
public async Task FindByClientIdAsync_ThrowsAnExceptionForEmptyIdentifier()
{
// Arrange
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = Mock.Of<IOpenIddictApplicationStore<OpenIddictApplication>>();
var cache = new OpenIddictApplicationCache<OpenIddictApplication>(options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentException>(
() => cache.FindByClientIdAsync(identifier: string.Empty, CancellationToken.None).AsTask());
Assert.Equal("identifier", exception.ParamName);
}
[Fact]
public async Task FindByClientIdAsync_ReturnsCachedApplicationOnCacheHit()
{
// Arrange
var application = new OpenIddictApplication();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = new Mock<IOpenIddictApplicationStore<OpenIddictApplication>>();
store.Setup(store => store.GetIdAsync(application, It.IsAny<CancellationToken>()))
.ReturnsAsync("application-id");
store.Setup(store => store.GetClientIdAsync(application, It.IsAny<CancellationToken>()))
.ReturnsAsync("client-id");
store.Setup(store => store.GetPostLogoutRedirectUrisAsync(application, It.IsAny<CancellationToken>()))
.ReturnsAsync([]);
store.Setup(store => store.GetRedirectUrisAsync(application, It.IsAny<CancellationToken>()))
.ReturnsAsync([]);
var cache = new OpenIddictApplicationCache<OpenIddictApplication>(options, store.Object);
await cache.AddAsync(application, CancellationToken.None);
// Act
var result = await cache.FindByClientIdAsync("client-id", CancellationToken.None);
// Assert
Assert.Same(application, result);
store.Verify(store => store.FindByClientIdAsync("client-id", It.IsAny<CancellationToken>()), Times.Never());
}
[Fact]
public async Task FindByClientIdAsync_QueriesStoreOnCacheMiss()
{
// Arrange
var application = new OpenIddictApplication();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = new Mock<IOpenIddictApplicationStore<OpenIddictApplication>>();
store.Setup(store => store.FindByClientIdAsync("client-id", It.IsAny<CancellationToken>()))
.ReturnsAsync(application);
store.Setup(store => store.GetIdAsync(application, It.IsAny<CancellationToken>()))
.ReturnsAsync("application-id");
store.Setup(store => store.GetClientIdAsync(application, It.IsAny<CancellationToken>()))
.ReturnsAsync("client-id");
store.Setup(store => store.GetPostLogoutRedirectUrisAsync(application, It.IsAny<CancellationToken>()))
.ReturnsAsync([]);
store.Setup(store => store.GetRedirectUrisAsync(application, It.IsAny<CancellationToken>()))
.ReturnsAsync([]);
var cache = new OpenIddictApplicationCache<OpenIddictApplication>(options, store.Object);
// Act
var result = await cache.FindByClientIdAsync("client-id", CancellationToken.None);
// Assert
Assert.Same(application, result);
store.Verify(store => store.FindByClientIdAsync("client-id", It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task FindByIdAsync_ThrowsAnExceptionForNullIdentifier()
{
// Arrange
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = Mock.Of<IOpenIddictApplicationStore<OpenIddictApplication>>();
var cache = new OpenIddictApplicationCache<OpenIddictApplication>(options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => cache.FindByIdAsync(identifier: null!, CancellationToken.None).AsTask());
Assert.Equal("identifier", exception.ParamName);
}
[Fact]
public async Task FindByIdAsync_ThrowsAnExceptionForEmptyIdentifier()
{
// Arrange
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = Mock.Of<IOpenIddictApplicationStore<OpenIddictApplication>>();
var cache = new OpenIddictApplicationCache<OpenIddictApplication>(options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentException>(
() => cache.FindByIdAsync(identifier: string.Empty, CancellationToken.None).AsTask());
Assert.Equal("identifier", exception.ParamName);
}
[Fact]
public async Task FindByIdAsync_ReturnsCachedApplicationOnCacheHit()
{
// Arrange
var application = new OpenIddictApplication();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = new Mock<IOpenIddictApplicationStore<OpenIddictApplication>>();
store.Setup(store => store.GetIdAsync(application, It.IsAny<CancellationToken>()))
.ReturnsAsync("application-id");
store.Setup(store => store.GetClientIdAsync(application, It.IsAny<CancellationToken>()))
.ReturnsAsync("client-id");
store.Setup(store => store.GetPostLogoutRedirectUrisAsync(application, It.IsAny<CancellationToken>()))
.ReturnsAsync([]);
store.Setup(store => store.GetRedirectUrisAsync(application, It.IsAny<CancellationToken>()))
.ReturnsAsync([]);
var cache = new OpenIddictApplicationCache<OpenIddictApplication>(options, store.Object);
await cache.AddAsync(application, CancellationToken.None);
// Act
var result = await cache.FindByIdAsync("application-id", CancellationToken.None);
// Assert
Assert.Same(application, result);
store.Verify(store => store.FindByIdAsync("application-id", It.IsAny<CancellationToken>()), Times.Never());
}
[Fact]
public async Task FindByIdAsync_QueriesStoreOnCacheMiss()
{
// Arrange
var application = new OpenIddictApplication();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = new Mock<IOpenIddictApplicationStore<OpenIddictApplication>>();
store.Setup(store => store.FindByIdAsync("application-id", It.IsAny<CancellationToken>()))
.ReturnsAsync(application);
store.Setup(store => store.GetIdAsync(application, It.IsAny<CancellationToken>()))
.ReturnsAsync("application-id");
store.Setup(store => store.GetClientIdAsync(application, It.IsAny<CancellationToken>()))
.ReturnsAsync("client-id");
store.Setup(store => store.GetPostLogoutRedirectUrisAsync(application, It.IsAny<CancellationToken>()))
.ReturnsAsync([]);
store.Setup(store => store.GetRedirectUrisAsync(application, It.IsAny<CancellationToken>()))
.ReturnsAsync([]);
var cache = new OpenIddictApplicationCache<OpenIddictApplication>(options, store.Object);
// Act
var result = await cache.FindByIdAsync("application-id", CancellationToken.None);
// Assert
Assert.Same(application, result);
store.Verify(store => store.FindByIdAsync("application-id", It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task FindByIdAsync_ReturnsNullWhenApplicationNotFound()
{
// Arrange
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = new Mock<IOpenIddictApplicationStore<OpenIddictApplication>>();
store.Setup(store => store.FindByIdAsync("application-id", It.IsAny<CancellationToken>()))
.ReturnsAsync((OpenIddictApplication?) null);
var cache = new OpenIddictApplicationCache<OpenIddictApplication>(options, store.Object);
// Act
var result = await cache.FindByIdAsync("application-id", CancellationToken.None);
// Assert
Assert.Null(result);
}
[Fact]
public async Task FindByPostLogoutRedirectUriAsync_ReturnsCachedApplicationsOnCacheHit()
{
// Arrange
var applications = new[]
{
new OpenIddictApplication(),
new OpenIddictApplication()
};
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = new Mock<IOpenIddictApplicationStore<OpenIddictApplication>>();
store.Setup(store => store.GetIdAsync(applications[0], It.IsAny<CancellationToken>()))
.ReturnsAsync("application-id-1");
store.Setup(store => store.GetClientIdAsync(applications[0], It.IsAny<CancellationToken>()))
.ReturnsAsync("client-id-1");
store.Setup(store => store.GetPostLogoutRedirectUrisAsync(applications[0], It.IsAny<CancellationToken>()))
.ReturnsAsync([]);
store.Setup(store => store.GetRedirectUrisAsync(applications[0], It.IsAny<CancellationToken>()))
.ReturnsAsync([]);
store.Setup(store => store.GetIdAsync(applications[1], It.IsAny<CancellationToken>()))
.ReturnsAsync("application-id-2");
store.Setup(store => store.GetClientIdAsync(applications[1], It.IsAny<CancellationToken>()))
.ReturnsAsync("client-id-2");
store.Setup(store => store.GetPostLogoutRedirectUrisAsync(applications[1], It.IsAny<CancellationToken>()))
.ReturnsAsync([]);
store.Setup(store => store.GetRedirectUrisAsync(applications[1], It.IsAny<CancellationToken>()))
.ReturnsAsync([]);
store.Setup(store => store.FindByPostLogoutRedirectUriAsync("https://localhost/signout-callback", It.IsAny<CancellationToken>()))
.Returns(applications.ToAsyncEnumerable());
var cache = new OpenIddictApplicationCache<OpenIddictApplication>(options, store.Object);
await cache.FindByPostLogoutRedirectUriAsync("https://localhost/signout-callback", CancellationToken.None).ToListAsync();
// Act
var results = await cache.FindByPostLogoutRedirectUriAsync("https://localhost/signout-callback", CancellationToken.None).ToListAsync();
// Assert
Assert.Equal(2, results.Count);
Assert.Contains(applications[0], results);
Assert.Contains(applications[1], results);
store.Verify(store => store.FindByPostLogoutRedirectUriAsync("https://localhost/signout-callback", It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task FindByRedirectUriAsync_QueriesStoreOnCacheMiss()
{
// Arrange
var applications = new[]
{
new OpenIddictApplication(),
new OpenIddictApplication()
};
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = new Mock<IOpenIddictApplicationStore<OpenIddictApplication>>();
store.Setup(store => store.GetIdAsync(applications[0], It.IsAny<CancellationToken>()))
.ReturnsAsync("application-id-1");
store.Setup(store => store.GetClientIdAsync(applications[0], It.IsAny<CancellationToken>()))
.ReturnsAsync("client-id-1");
store.Setup(store => store.GetPostLogoutRedirectUrisAsync(applications[0], It.IsAny<CancellationToken>()))
.ReturnsAsync([]);
store.Setup(store => store.GetRedirectUrisAsync(applications[0], It.IsAny<CancellationToken>()))
.ReturnsAsync([]);
store.Setup(store => store.GetIdAsync(applications[1], It.IsAny<CancellationToken>()))
.ReturnsAsync("application-id-2");
store.Setup(store => store.GetClientIdAsync(applications[1], It.IsAny<CancellationToken>()))
.ReturnsAsync("client-id-2");
store.Setup(store => store.GetPostLogoutRedirectUrisAsync(applications[1], It.IsAny<CancellationToken>()))
.ReturnsAsync([]);
store.Setup(store => store.GetRedirectUrisAsync(applications[1], It.IsAny<CancellationToken>()))
.ReturnsAsync([]);
store.Setup(store => store.FindByRedirectUriAsync("https://localhost/callback", It.IsAny<CancellationToken>()))
.Returns(applications.ToAsyncEnumerable());
var cache = new OpenIddictApplicationCache<OpenIddictApplication>(options, store.Object);
// Act
var results = await cache.FindByRedirectUriAsync("https://localhost/callback", CancellationToken.None).ToListAsync();
// Assert
Assert.Equal(2, results.Count);
Assert.Contains(applications[0], results);
Assert.Contains(applications[1], results);
store.Verify(store => store.FindByRedirectUriAsync("https://localhost/callback", It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task RemoveAsync_ThrowsAnExceptionForNullApplication()
{
// Arrange
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = Mock.Of<IOpenIddictApplicationStore<OpenIddictApplication>>();
var cache = new OpenIddictApplicationCache<OpenIddictApplication>(options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => cache.RemoveAsync(application: null!, CancellationToken.None).AsTask());
Assert.Equal("application", exception.ParamName);
}
[Fact]
public async Task RemoveAsync_InvalidatesCachedEntries()
{
// Arrange
var application = new OpenIddictApplication();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = new Mock<IOpenIddictApplicationStore<OpenIddictApplication>>();
store.Setup(store => store.GetIdAsync(application, It.IsAny<CancellationToken>()))
.ReturnsAsync("application-id");
store.Setup(store => store.GetClientIdAsync(application, It.IsAny<CancellationToken>()))
.ReturnsAsync("client-id");
store.Setup(store => store.GetPostLogoutRedirectUrisAsync(application, It.IsAny<CancellationToken>()))
.ReturnsAsync([]);
store.Setup(store => store.GetRedirectUrisAsync(application, It.IsAny<CancellationToken>()))
.ReturnsAsync([]);
var cache = new OpenIddictApplicationCache<OpenIddictApplication>(options, store.Object);
await cache.AddAsync(application, CancellationToken.None);
// Act
await cache.RemoveAsync(application, CancellationToken.None);
var result = await cache.FindByIdAsync("application-id", CancellationToken.None);
// Assert
Assert.Null(result);
}
[Fact]
public async Task RemoveAsync_ThrowsForApplicationWithoutId()
{
// Arrange
var application = new OpenIddictApplication();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = new Mock<IOpenIddictApplicationStore<OpenIddictApplication>>();
store.Setup(store => store.GetIdAsync(application, It.IsAny<CancellationToken>()))
.ReturnsAsync((string?) null);
var cache = new OpenIddictApplicationCache<OpenIddictApplication>(options, store.Object);
// Act and assert
await Assert.ThrowsAsync<InvalidOperationException>(
() => cache.RemoveAsync(application, CancellationToken.None).AsTask());
}
public sealed class OpenIddictApplication { }
}

330
test/OpenIddict.Core.Tests/Caches/OpenIddictAuthorizationCacheTests.cs

@ -0,0 +1,330 @@
/*
* 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.Options;
using Moq;
using Xunit;
namespace OpenIddict.Core.Tests;
public class OpenIddictAuthorizationCacheTests
{
[Fact]
public void Constructor_ThrowsAnExceptionForNullOptions()
{
// Arrange
var options = (IOptionsMonitor<OpenIddictCoreOptions>) null!;
var store = Mock.Of<IOpenIddictAuthorizationStore<object>>();
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => new OpenIddictAuthorizationCache<object>(options, store));
Assert.Equal("options", exception.ParamName);
}
[Fact]
public void Constructor_ThrowsAnExceptionForNullStore()
{
// Arrange
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = (IOpenIddictAuthorizationStore<object>) null!;
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => new OpenIddictAuthorizationCache<object>(options, store));
Assert.Equal("store", exception.ParamName);
}
[Fact]
public async Task AddAsync_ThrowsAnExceptionForNullAuthorization()
{
// Arrange
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = Mock.Of<IOpenIddictAuthorizationStore<OpenIddictAuthorization>>();
var cache = new OpenIddictAuthorizationCache<OpenIddictAuthorization>(options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => cache.AddAsync(authorization: null!, CancellationToken.None).AsTask());
Assert.Equal("authorization", exception.ParamName);
}
[Fact]
public void Dispose_CanBeCalledMultipleTimes()
{
// Arrange
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = Mock.Of<IOpenIddictAuthorizationStore<OpenIddictAuthorization>>();
var cache = new OpenIddictAuthorizationCache<OpenIddictAuthorization>(options, store);
// Act and assert
cache.Dispose();
cache.Dispose();
}
[Fact]
public async Task FindByApplicationIdAsync_ReturnsCachedAuthorizationsOnCacheHit()
{
// Arrange
var authorizations = new[]
{
new OpenIddictAuthorization(),
new OpenIddictAuthorization()
};
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = new Mock<IOpenIddictAuthorizationStore<OpenIddictAuthorization>>();
store.Setup(store => store.GetIdAsync(authorizations[0], It.IsAny<CancellationToken>()))
.ReturnsAsync("authorization-id-1");
store.Setup(store => store.GetApplicationIdAsync(authorizations[0], It.IsAny<CancellationToken>()))
.ReturnsAsync("application-id");
store.Setup(store => store.GetSubjectAsync(authorizations[0], It.IsAny<CancellationToken>()))
.ReturnsAsync("subject-1");
store.Setup(store => store.GetIdAsync(authorizations[1], It.IsAny<CancellationToken>()))
.ReturnsAsync("authorization-id-2");
store.Setup(store => store.GetApplicationIdAsync(authorizations[1], It.IsAny<CancellationToken>()))
.ReturnsAsync("application-id");
store.Setup(store => store.GetSubjectAsync(authorizations[1], It.IsAny<CancellationToken>()))
.ReturnsAsync("subject-2");
store.Setup(store => store.FindByApplicationIdAsync("application-id", It.IsAny<CancellationToken>()))
.Returns(authorizations.ToAsyncEnumerable());
var cache = new OpenIddictAuthorizationCache<OpenIddictAuthorization>(options, store.Object);
await cache.FindByApplicationIdAsync("application-id", CancellationToken.None).ToListAsync();
// Act
var results = await cache.FindByApplicationIdAsync("application-id", CancellationToken.None).ToListAsync();
// Assert
Assert.Equal(2, results.Count);
Assert.Contains(authorizations[0], results);
Assert.Contains(authorizations[1], results);
store.Verify(store => store.FindByApplicationIdAsync("application-id", It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task FindByIdAsync_ThrowsAnExceptionForNullIdentifier()
{
// Arrange
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = Mock.Of<IOpenIddictAuthorizationStore<OpenIddictAuthorization>>();
var cache = new OpenIddictAuthorizationCache<OpenIddictAuthorization>(options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => cache.FindByIdAsync(identifier: null!, CancellationToken.None).AsTask());
Assert.Equal("identifier", exception.ParamName);
}
[Fact]
public async Task FindByIdAsync_ThrowsAnExceptionForEmptyIdentifier()
{
// Arrange
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = Mock.Of<IOpenIddictAuthorizationStore<OpenIddictAuthorization>>();
var cache = new OpenIddictAuthorizationCache<OpenIddictAuthorization>(options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentException>(
() => cache.FindByIdAsync(identifier: string.Empty, CancellationToken.None).AsTask());
Assert.Equal("identifier", exception.ParamName);
}
[Fact]
public async Task FindByIdAsync_ReturnsCachedAuthorizationOnCacheHit()
{
// Arrange
var authorization = new OpenIddictAuthorization();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = new Mock<IOpenIddictAuthorizationStore<OpenIddictAuthorization>>();
store.Setup(store => store.GetIdAsync(authorization, It.IsAny<CancellationToken>()))
.ReturnsAsync("authorization-id");
store.Setup(store => store.GetApplicationIdAsync(authorization, It.IsAny<CancellationToken>()))
.ReturnsAsync("application-id");
store.Setup(store => store.GetSubjectAsync(authorization, It.IsAny<CancellationToken>()))
.ReturnsAsync("subject");
var cache = new OpenIddictAuthorizationCache<OpenIddictAuthorization>(options, store.Object);
await cache.AddAsync(authorization, CancellationToken.None);
// Act
var result = await cache.FindByIdAsync("authorization-id", CancellationToken.None);
// Assert
Assert.Same(authorization, result);
store.Verify(store => store.FindByIdAsync("authorization-id", It.IsAny<CancellationToken>()), Times.Never());
}
[Fact]
public async Task FindByIdAsync_QueriesStoreOnCacheMiss()
{
// Arrange
var authorization = new OpenIddictAuthorization();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = new Mock<IOpenIddictAuthorizationStore<OpenIddictAuthorization>>();
store.Setup(store => store.FindByIdAsync("authorization-id", It.IsAny<CancellationToken>()))
.ReturnsAsync(authorization);
store.Setup(store => store.GetIdAsync(authorization, It.IsAny<CancellationToken>()))
.ReturnsAsync("authorization-id");
store.Setup(store => store.GetApplicationIdAsync(authorization, It.IsAny<CancellationToken>()))
.ReturnsAsync("application-id");
store.Setup(store => store.GetSubjectAsync(authorization, It.IsAny<CancellationToken>()))
.ReturnsAsync("subject");
var cache = new OpenIddictAuthorizationCache<OpenIddictAuthorization>(options, store.Object);
// Act
var result = await cache.FindByIdAsync("authorization-id", CancellationToken.None);
// Assert
Assert.Same(authorization, result);
store.Verify(store => store.FindByIdAsync("authorization-id", It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task FindByIdAsync_ReturnsNullWhenAuthorizationNotFound()
{
// Arrange
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = new Mock<IOpenIddictAuthorizationStore<OpenIddictAuthorization>>();
store.Setup(store => store.FindByIdAsync("authorization-id", It.IsAny<CancellationToken>()))
.ReturnsAsync((OpenIddictAuthorization?) null);
var cache = new OpenIddictAuthorizationCache<OpenIddictAuthorization>(options, store.Object);
// Act
var result = await cache.FindByIdAsync("authorization-id", CancellationToken.None);
// Assert
Assert.Null(result);
}
[Fact]
public async Task FindBySubjectAsync_QueriesStoreOnCacheMiss()
{
// Arrange
var authorizations = new[]
{
new OpenIddictAuthorization(),
new OpenIddictAuthorization()
};
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = new Mock<IOpenIddictAuthorizationStore<OpenIddictAuthorization>>();
store.Setup(store => store.GetIdAsync(authorizations[0], It.IsAny<CancellationToken>()))
.ReturnsAsync("authorization-id-1");
store.Setup(store => store.GetApplicationIdAsync(authorizations[0], It.IsAny<CancellationToken>()))
.ReturnsAsync("application-id-1");
store.Setup(store => store.GetSubjectAsync(authorizations[0], It.IsAny<CancellationToken>()))
.ReturnsAsync("subject");
store.Setup(store => store.GetIdAsync(authorizations[1], It.IsAny<CancellationToken>()))
.ReturnsAsync("authorization-id-2");
store.Setup(store => store.GetApplicationIdAsync(authorizations[1], It.IsAny<CancellationToken>()))
.ReturnsAsync("application-id-2");
store.Setup(store => store.GetSubjectAsync(authorizations[1], It.IsAny<CancellationToken>()))
.ReturnsAsync("subject");
store.Setup(store => store.FindBySubjectAsync("subject", It.IsAny<CancellationToken>()))
.Returns(authorizations.ToAsyncEnumerable());
var cache = new OpenIddictAuthorizationCache<OpenIddictAuthorization>(options, store.Object);
// Act
var results = await cache.FindBySubjectAsync("subject", CancellationToken.None).ToListAsync();
// Assert
Assert.Equal(2, results.Count);
Assert.Contains(authorizations[0], results);
Assert.Contains(authorizations[1], results);
store.Verify(store => store.FindBySubjectAsync("subject", It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task RemoveAsync_ThrowsAnExceptionForNullAuthorization()
{
// Arrange
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = Mock.Of<IOpenIddictAuthorizationStore<OpenIddictAuthorization>>();
var cache = new OpenIddictAuthorizationCache<OpenIddictAuthorization>(options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => cache.RemoveAsync(authorization: null!, CancellationToken.None).AsTask());
Assert.Equal("authorization", exception.ParamName);
}
[Fact]
public async Task RemoveAsync_InvalidatesCachedEntries()
{
// Arrange
var authorization = new OpenIddictAuthorization();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = new Mock<IOpenIddictAuthorizationStore<OpenIddictAuthorization>>();
store.Setup(store => store.GetIdAsync(authorization, It.IsAny<CancellationToken>()))
.ReturnsAsync("authorization-id");
store.Setup(store => store.GetApplicationIdAsync(authorization, It.IsAny<CancellationToken>()))
.ReturnsAsync("application-id");
store.Setup(store => store.GetSubjectAsync(authorization, It.IsAny<CancellationToken>()))
.ReturnsAsync("subject");
var cache = new OpenIddictAuthorizationCache<OpenIddictAuthorization>(options, store.Object);
await cache.AddAsync(authorization, CancellationToken.None);
// Act
await cache.RemoveAsync(authorization, CancellationToken.None);
var result = await cache.FindByIdAsync("authorization-id", CancellationToken.None);
// Assert
Assert.Null(result);
}
[Fact]
public async Task RemoveAsync_ThrowsForAuthorizationWithoutId()
{
// Arrange
var authorization = new OpenIddictAuthorization();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = new Mock<IOpenIddictAuthorizationStore<OpenIddictAuthorization>>();
store.Setup(store => store.GetIdAsync(authorization, It.IsAny<CancellationToken>()))
.ReturnsAsync((string?) null);
var cache = new OpenIddictAuthorizationCache<OpenIddictAuthorization>(options, store.Object);
// Act and assert
await Assert.ThrowsAsync<InvalidOperationException>(
() => cache.RemoveAsync(authorization, CancellationToken.None).AsTask());
}
public sealed class OpenIddictAuthorization { }
}

417
test/OpenIddict.Core.Tests/Caches/OpenIddictScopeCacheTests.cs

@ -0,0 +1,417 @@
/*
* 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 System.Collections.Immutable;
using Microsoft.Extensions.Options;
using Moq;
using Xunit;
namespace OpenIddict.Core.Tests;
public class OpenIddictScopeCacheTests
{
[Fact]
public void Constructor_ThrowsAnExceptionForNullOptions()
{
// Arrange
var options = (IOptionsMonitor<OpenIddictCoreOptions>) null!;
var store = Mock.Of<IOpenIddictScopeStore<object>>();
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => new OpenIddictScopeCache<object>(options, store));
Assert.Equal("options", exception.ParamName);
}
[Fact]
public void Constructor_ThrowsAnExceptionForNullStore()
{
// Arrange
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = (IOpenIddictScopeStore<object>) null!;
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => new OpenIddictScopeCache<object>(options, store));
Assert.Equal("store", exception.ParamName);
}
[Fact]
public async Task AddAsync_ThrowsAnExceptionForNullScope()
{
// Arrange
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = Mock.Of<IOpenIddictScopeStore<OpenIddictScope>>();
var cache = new OpenIddictScopeCache<OpenIddictScope>(options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => cache.AddAsync(scope: null!, CancellationToken.None).AsTask());
Assert.Equal("scope", exception.ParamName);
}
[Fact]
public void Dispose_CanBeCalledMultipleTimes()
{
// Arrange
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = Mock.Of<IOpenIddictScopeStore<OpenIddictScope>>();
var cache = new OpenIddictScopeCache<OpenIddictScope>(options, store);
// Act and assert
cache.Dispose();
cache.Dispose();
}
[Fact]
public async Task FindByIdAsync_ThrowsAnExceptionForNullIdentifier()
{
// Arrange
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = Mock.Of<IOpenIddictScopeStore<OpenIddictScope>>();
var cache = new OpenIddictScopeCache<OpenIddictScope>(options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => cache.FindByIdAsync(identifier: null!, CancellationToken.None).AsTask());
Assert.Equal("identifier", exception.ParamName);
}
[Fact]
public async Task FindByIdAsync_ThrowsAnExceptionForEmptyIdentifier()
{
// Arrange
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = Mock.Of<IOpenIddictScopeStore<OpenIddictScope>>();
var cache = new OpenIddictScopeCache<OpenIddictScope>(options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentException>(
() => cache.FindByIdAsync(identifier: string.Empty, CancellationToken.None).AsTask());
Assert.Equal("identifier", exception.ParamName);
}
[Fact]
public async Task FindByIdAsync_ReturnsCachedScopeOnCacheHit()
{
// Arrange
var scope = new OpenIddictScope();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = new Mock<IOpenIddictScopeStore<OpenIddictScope>>();
store.Setup(store => store.GetIdAsync(scope, It.IsAny<CancellationToken>()))
.ReturnsAsync("scope-id");
store.Setup(store => store.GetNameAsync(scope, It.IsAny<CancellationToken>()))
.ReturnsAsync("scope-name");
store.Setup(store => store.GetResourcesAsync(scope, It.IsAny<CancellationToken>()))
.ReturnsAsync([]);
var cache = new OpenIddictScopeCache<OpenIddictScope>(options, store.Object);
await cache.AddAsync(scope, CancellationToken.None);
// Act
var result = await cache.FindByIdAsync("scope-id", CancellationToken.None);
// Assert
Assert.Same(scope, result);
store.Verify(store => store.FindByIdAsync("scope-id", It.IsAny<CancellationToken>()), Times.Never());
}
[Fact]
public async Task FindByIdAsync_QueriesStoreOnCacheMiss()
{
// Arrange
var scope = new OpenIddictScope();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = new Mock<IOpenIddictScopeStore<OpenIddictScope>>();
store.Setup(store => store.FindByIdAsync("scope-id", It.IsAny<CancellationToken>()))
.ReturnsAsync(scope);
store.Setup(store => store.GetIdAsync(scope, It.IsAny<CancellationToken>()))
.ReturnsAsync("scope-id");
store.Setup(store => store.GetNameAsync(scope, It.IsAny<CancellationToken>()))
.ReturnsAsync("scope-name");
store.Setup(store => store.GetResourcesAsync(scope, It.IsAny<CancellationToken>()))
.ReturnsAsync([]);
var cache = new OpenIddictScopeCache<OpenIddictScope>(options, store.Object);
// Act
var result = await cache.FindByIdAsync("scope-id", CancellationToken.None);
// Assert
Assert.Same(scope, result);
store.Verify(store => store.FindByIdAsync("scope-id", It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task FindByIdAsync_ReturnsNullWhenScopeNotFound()
{
// Arrange
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = new Mock<IOpenIddictScopeStore<OpenIddictScope>>();
store.Setup(store => store.FindByIdAsync("scope-id", It.IsAny<CancellationToken>()))
.ReturnsAsync((OpenIddictScope?) null);
var cache = new OpenIddictScopeCache<OpenIddictScope>(options, store.Object);
// Act
var result = await cache.FindByIdAsync("scope-id", CancellationToken.None);
// Assert
Assert.Null(result);
}
[Fact]
public async Task FindByNameAsync_ThrowsAnExceptionForNullName()
{
// Arrange
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = Mock.Of<IOpenIddictScopeStore<OpenIddictScope>>();
var cache = new OpenIddictScopeCache<OpenIddictScope>(options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => cache.FindByNameAsync(name: null!, CancellationToken.None).AsTask());
Assert.Equal("name", exception.ParamName);
}
[Fact]
public async Task FindByNameAsync_ThrowsAnExceptionForEmptyName()
{
// Arrange
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = Mock.Of<IOpenIddictScopeStore<OpenIddictScope>>();
var cache = new OpenIddictScopeCache<OpenIddictScope>(options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentException>(
() => cache.FindByNameAsync(name: string.Empty, CancellationToken.None).AsTask());
Assert.Equal("name", exception.ParamName);
}
[Fact]
public async Task FindByNameAsync_ReturnsCachedScopeOnCacheHit()
{
// Arrange
var scope = new OpenIddictScope();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = new Mock<IOpenIddictScopeStore<OpenIddictScope>>();
store.Setup(store => store.GetIdAsync(scope, It.IsAny<CancellationToken>()))
.ReturnsAsync("scope-id");
store.Setup(store => store.GetNameAsync(scope, It.IsAny<CancellationToken>()))
.ReturnsAsync("scope-name");
store.Setup(store => store.GetResourcesAsync(scope, It.IsAny<CancellationToken>()))
.ReturnsAsync([]);
var cache = new OpenIddictScopeCache<OpenIddictScope>(options, store.Object);
await cache.AddAsync(scope, CancellationToken.None);
// Act
var result = await cache.FindByNameAsync("scope-name", CancellationToken.None);
// Assert
Assert.Same(scope, result);
store.Verify(store => store.FindByNameAsync("scope-name", It.IsAny<CancellationToken>()), Times.Never());
}
[Fact]
public async Task FindByNameAsync_QueriesStoreOnCacheMiss()
{
// Arrange
var scope = new OpenIddictScope();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = new Mock<IOpenIddictScopeStore<OpenIddictScope>>();
store.Setup(store => store.FindByNameAsync("scope-name", It.IsAny<CancellationToken>()))
.ReturnsAsync(scope);
store.Setup(store => store.GetIdAsync(scope, It.IsAny<CancellationToken>()))
.ReturnsAsync("scope-id");
store.Setup(store => store.GetNameAsync(scope, It.IsAny<CancellationToken>()))
.ReturnsAsync("scope-name");
store.Setup(store => store.GetResourcesAsync(scope, It.IsAny<CancellationToken>()))
.ReturnsAsync([]);
var cache = new OpenIddictScopeCache<OpenIddictScope>(options, store.Object);
// Act
var result = await cache.FindByNameAsync("scope-name", CancellationToken.None);
// Assert
Assert.Same(scope, result);
store.Verify(store => store.FindByNameAsync("scope-name", It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task FindByNamesAsync_QueriesStoreOnCacheMiss()
{
// Arrange
var scopes = new[]
{
new OpenIddictScope(),
new OpenIddictScope()
};
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = new Mock<IOpenIddictScopeStore<OpenIddictScope>>();
store.Setup(store => store.GetIdAsync(scopes[0], It.IsAny<CancellationToken>()))
.ReturnsAsync("scope-id-1");
store.Setup(store => store.GetNameAsync(scopes[0], It.IsAny<CancellationToken>()))
.ReturnsAsync("scope-name-1");
store.Setup(store => store.GetResourcesAsync(scopes[0], It.IsAny<CancellationToken>()))
.ReturnsAsync([]);
store.Setup(store => store.GetIdAsync(scopes[1], It.IsAny<CancellationToken>()))
.ReturnsAsync("scope-id-2");
store.Setup(store => store.GetNameAsync(scopes[1], It.IsAny<CancellationToken>()))
.ReturnsAsync("scope-name-2");
store.Setup(store => store.GetResourcesAsync(scopes[1], It.IsAny<CancellationToken>()))
.ReturnsAsync([]);
store.Setup(store => store.FindByNamesAsync(It.IsAny<ImmutableArray<string>>(), It.IsAny<CancellationToken>()))
.Returns(scopes.ToAsyncEnumerable());
var cache = new OpenIddictScopeCache<OpenIddictScope>(options, store.Object);
// Act
var results = await cache.FindByNamesAsync(["scope-name-1", "scope-name-2"], CancellationToken.None).ToListAsync();
// Assert
Assert.Equal(2, results.Count);
Assert.Contains(scopes[0], results);
Assert.Contains(scopes[1], results);
}
[Fact]
public async Task FindByResourceAsync_QueriesStoreOnCacheMiss()
{
// Arrange
var scopes = new[]
{
new OpenIddictScope(),
new OpenIddictScope()
};
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = new Mock<IOpenIddictScopeStore<OpenIddictScope>>();
store.Setup(store => store.GetIdAsync(scopes[0], It.IsAny<CancellationToken>()))
.ReturnsAsync("scope-id-1");
store.Setup(store => store.GetNameAsync(scopes[0], It.IsAny<CancellationToken>()))
.ReturnsAsync("scope-name-1");
store.Setup(store => store.GetResourcesAsync(scopes[0], It.IsAny<CancellationToken>()))
.ReturnsAsync([]);
store.Setup(store => store.GetIdAsync(scopes[1], It.IsAny<CancellationToken>()))
.ReturnsAsync("scope-id-2");
store.Setup(store => store.GetNameAsync(scopes[1], It.IsAny<CancellationToken>()))
.ReturnsAsync("scope-name-2");
store.Setup(store => store.GetResourcesAsync(scopes[1], It.IsAny<CancellationToken>()))
.ReturnsAsync([]);
store.Setup(store => store.FindByResourceAsync("resource", It.IsAny<CancellationToken>()))
.Returns(scopes.ToAsyncEnumerable());
var cache = new OpenIddictScopeCache<OpenIddictScope>(options, store.Object);
// Act
var results = await cache.FindByResourceAsync("resource", CancellationToken.None).ToListAsync();
// Assert
Assert.Equal(2, results.Count);
Assert.Contains(scopes[0], results);
Assert.Contains(scopes[1], results);
store.Verify(store => store.FindByResourceAsync("resource", It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task RemoveAsync_ThrowsAnExceptionForNullScope()
{
// Arrange
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = Mock.Of<IOpenIddictScopeStore<OpenIddictScope>>();
var cache = new OpenIddictScopeCache<OpenIddictScope>(options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => cache.RemoveAsync(scope: null!, CancellationToken.None).AsTask());
Assert.Equal("scope", exception.ParamName);
}
[Fact]
public async Task RemoveAsync_InvalidatesCachedEntries()
{
// Arrange
var scope = new OpenIddictScope();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = new Mock<IOpenIddictScopeStore<OpenIddictScope>>();
store.Setup(store => store.GetIdAsync(scope, It.IsAny<CancellationToken>()))
.ReturnsAsync("scope-id");
store.Setup(store => store.GetNameAsync(scope, It.IsAny<CancellationToken>()))
.ReturnsAsync("scope-name");
store.Setup(store => store.GetResourcesAsync(scope, It.IsAny<CancellationToken>()))
.ReturnsAsync([]);
var cache = new OpenIddictScopeCache<OpenIddictScope>(options, store.Object);
await cache.AddAsync(scope, CancellationToken.None);
// Act
await cache.RemoveAsync(scope, CancellationToken.None);
var result = await cache.FindByIdAsync("scope-id", CancellationToken.None);
// Assert
Assert.Null(result);
}
[Fact]
public async Task RemoveAsync_ThrowsForScopeWithoutId()
{
// Arrange
var scope = new OpenIddictScope();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = new Mock<IOpenIddictScopeStore<OpenIddictScope>>();
store.Setup(store => store.GetIdAsync(scope, It.IsAny<CancellationToken>()))
.ReturnsAsync((string?) null);
var cache = new OpenIddictScopeCache<OpenIddictScope>(options, store.Object);
// Act and assert
await Assert.ThrowsAsync<InvalidOperationException>(
() => cache.RemoveAsync(scope, CancellationToken.None).AsTask());
}
public sealed class OpenIddictScope { }
}

503
test/OpenIddict.Core.Tests/Caches/OpenIddictTokenCacheTests.cs

@ -0,0 +1,503 @@
/*
* 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.Options;
using Moq;
using Xunit;
namespace OpenIddict.Core.Tests;
public class OpenIddictTokenCacheTests
{
[Fact]
public void Constructor_ThrowsAnExceptionForNullOptions()
{
// Arrange
var options = (IOptionsMonitor<OpenIddictCoreOptions>) null!;
var store = Mock.Of<IOpenIddictTokenStore<object>>();
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => new OpenIddictTokenCache<object>(options, store));
Assert.Equal("options", exception.ParamName);
}
[Fact]
public void Constructor_ThrowsAnExceptionForNullStore()
{
// Arrange
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = (IOpenIddictTokenStore<object>) null!;
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => new OpenIddictTokenCache<object>(options, store));
Assert.Equal("store", exception.ParamName);
}
[Fact]
public async Task AddAsync_ThrowsAnExceptionForNullToken()
{
// Arrange
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = Mock.Of<IOpenIddictTokenStore<OpenIddictToken>>();
var cache = new OpenIddictTokenCache<OpenIddictToken>(options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => cache.AddAsync(token: null!, CancellationToken.None).AsTask());
Assert.Equal("token", exception.ParamName);
}
[Fact]
public void Dispose_CanBeCalledMultipleTimes()
{
// Arrange
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = Mock.Of<IOpenIddictTokenStore<OpenIddictToken>>();
var cache = new OpenIddictTokenCache<OpenIddictToken>(options, store);
// Act and assert
cache.Dispose();
cache.Dispose();
}
[Fact]
public async Task FindByApplicationIdAsync_QueriesStoreOnCacheMiss()
{
// Arrange
var tokens = new[]
{
new OpenIddictToken(),
new OpenIddictToken()
};
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = new Mock<IOpenIddictTokenStore<OpenIddictToken>>();
store.Setup(store => store.GetIdAsync(tokens[0], It.IsAny<CancellationToken>()))
.ReturnsAsync("token-id-1");
store.Setup(store => store.GetReferenceIdAsync(tokens[0], It.IsAny<CancellationToken>()))
.ReturnsAsync((string?) null);
store.Setup(store => store.GetApplicationIdAsync(tokens[0], It.IsAny<CancellationToken>()))
.ReturnsAsync("application-id");
store.Setup(store => store.GetAuthorizationIdAsync(tokens[0], It.IsAny<CancellationToken>()))
.ReturnsAsync((string?) null);
store.Setup(store => store.GetSubjectAsync(tokens[0], It.IsAny<CancellationToken>()))
.ReturnsAsync((string?) null);
store.Setup(store => store.GetIdAsync(tokens[1], It.IsAny<CancellationToken>()))
.ReturnsAsync("token-id-2");
store.Setup(store => store.GetReferenceIdAsync(tokens[1], It.IsAny<CancellationToken>()))
.ReturnsAsync((string?) null);
store.Setup(store => store.GetApplicationIdAsync(tokens[1], It.IsAny<CancellationToken>()))
.ReturnsAsync("application-id");
store.Setup(store => store.GetAuthorizationIdAsync(tokens[1], It.IsAny<CancellationToken>()))
.ReturnsAsync((string?) null);
store.Setup(store => store.GetSubjectAsync(tokens[1], It.IsAny<CancellationToken>()))
.ReturnsAsync((string?) null);
store.Setup(store => store.FindByApplicationIdAsync("application-id", It.IsAny<CancellationToken>()))
.Returns(tokens.ToAsyncEnumerable());
var cache = new OpenIddictTokenCache<OpenIddictToken>(options, store.Object);
// Act
var results = await cache.FindByApplicationIdAsync("application-id", CancellationToken.None).ToListAsync();
// Assert
Assert.Equal(2, results.Count);
Assert.Contains(tokens[0], results);
Assert.Contains(tokens[1], results);
store.Verify(store => store.FindByApplicationIdAsync("application-id", It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task FindByAuthorizationIdAsync_QueriesStoreOnCacheMiss()
{
// Arrange
var tokens = new[]
{
new OpenIddictToken(),
new OpenIddictToken()
};
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = new Mock<IOpenIddictTokenStore<OpenIddictToken>>();
store.Setup(store => store.GetIdAsync(tokens[0], It.IsAny<CancellationToken>()))
.ReturnsAsync("token-id-1");
store.Setup(store => store.GetReferenceIdAsync(tokens[0], It.IsAny<CancellationToken>()))
.ReturnsAsync((string?) null);
store.Setup(store => store.GetApplicationIdAsync(tokens[0], It.IsAny<CancellationToken>()))
.ReturnsAsync((string?) null);
store.Setup(store => store.GetAuthorizationIdAsync(tokens[0], It.IsAny<CancellationToken>()))
.ReturnsAsync("authorization-id");
store.Setup(store => store.GetSubjectAsync(tokens[0], It.IsAny<CancellationToken>()))
.ReturnsAsync((string?) null);
store.Setup(store => store.GetIdAsync(tokens[1], It.IsAny<CancellationToken>()))
.ReturnsAsync("token-id-2");
store.Setup(store => store.GetReferenceIdAsync(tokens[1], It.IsAny<CancellationToken>()))
.ReturnsAsync((string?) null);
store.Setup(store => store.GetApplicationIdAsync(tokens[1], It.IsAny<CancellationToken>()))
.ReturnsAsync((string?) null);
store.Setup(store => store.GetAuthorizationIdAsync(tokens[1], It.IsAny<CancellationToken>()))
.ReturnsAsync("authorization-id");
store.Setup(store => store.GetSubjectAsync(tokens[1], It.IsAny<CancellationToken>()))
.ReturnsAsync((string?) null);
store.Setup(store => store.FindByAuthorizationIdAsync("authorization-id", It.IsAny<CancellationToken>()))
.Returns(tokens.ToAsyncEnumerable());
var cache = new OpenIddictTokenCache<OpenIddictToken>(options, store.Object);
// Act
var results = await cache.FindByAuthorizationIdAsync("authorization-id", CancellationToken.None).ToListAsync();
// Assert
Assert.Equal(2, results.Count);
Assert.Contains(tokens[0], results);
Assert.Contains(tokens[1], results);
store.Verify(store => store.FindByAuthorizationIdAsync("authorization-id", It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task FindByIdAsync_ThrowsAnExceptionForNullIdentifier()
{
// Arrange
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = Mock.Of<IOpenIddictTokenStore<OpenIddictToken>>();
var cache = new OpenIddictTokenCache<OpenIddictToken>(options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => cache.FindByIdAsync(identifier: null!, CancellationToken.None).AsTask());
Assert.Equal("identifier", exception.ParamName);
}
[Fact]
public async Task FindByIdAsync_ThrowsAnExceptionForEmptyIdentifier()
{
// Arrange
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = Mock.Of<IOpenIddictTokenStore<OpenIddictToken>>();
var cache = new OpenIddictTokenCache<OpenIddictToken>(options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentException>(
() => cache.FindByIdAsync(identifier: string.Empty, CancellationToken.None).AsTask());
Assert.Equal("identifier", exception.ParamName);
}
[Fact]
public async Task FindByIdAsync_ReturnsCachedTokenOnCacheHit()
{
// Arrange
var token = new OpenIddictToken();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = new Mock<IOpenIddictTokenStore<OpenIddictToken>>();
store.Setup(store => store.GetIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync("token-id");
store.Setup(store => store.GetReferenceIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync((string?) null);
store.Setup(store => store.GetApplicationIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync((string?) null);
store.Setup(store => store.GetAuthorizationIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync((string?) null);
store.Setup(store => store.GetSubjectAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync((string?) null);
var cache = new OpenIddictTokenCache<OpenIddictToken>(options, store.Object);
await cache.AddAsync(token, CancellationToken.None);
// Act
var result = await cache.FindByIdAsync("token-id", CancellationToken.None);
// Assert
Assert.Same(token, result);
store.Verify(store => store.FindByIdAsync("token-id", It.IsAny<CancellationToken>()), Times.Never());
}
[Fact]
public async Task FindByIdAsync_QueriesStoreOnCacheMiss()
{
// Arrange
var token = new OpenIddictToken();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = new Mock<IOpenIddictTokenStore<OpenIddictToken>>();
store.Setup(store => store.FindByIdAsync("token-id", It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
store.Setup(store => store.GetIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync("token-id");
store.Setup(store => store.GetReferenceIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync((string?) null);
store.Setup(store => store.GetApplicationIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync((string?) null);
store.Setup(store => store.GetAuthorizationIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync((string?) null);
store.Setup(store => store.GetSubjectAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync((string?) null);
var cache = new OpenIddictTokenCache<OpenIddictToken>(options, store.Object);
// Act
var result = await cache.FindByIdAsync("token-id", CancellationToken.None);
// Assert
Assert.Same(token, result);
store.Verify(store => store.FindByIdAsync("token-id", It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task FindByIdAsync_ReturnsNullWhenTokenNotFound()
{
// Arrange
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = new Mock<IOpenIddictTokenStore<OpenIddictToken>>();
store.Setup(store => store.FindByIdAsync("token-id", It.IsAny<CancellationToken>()))
.ReturnsAsync((OpenIddictToken?) null);
var cache = new OpenIddictTokenCache<OpenIddictToken>(options, store.Object);
// Act
var result = await cache.FindByIdAsync("token-id", CancellationToken.None);
// Assert
Assert.Null(result);
}
[Fact]
public async Task FindByReferenceIdAsync_ThrowsAnExceptionForNullIdentifier()
{
// Arrange
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = Mock.Of<IOpenIddictTokenStore<OpenIddictToken>>();
var cache = new OpenIddictTokenCache<OpenIddictToken>(options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => cache.FindByReferenceIdAsync(identifier: null!, CancellationToken.None).AsTask());
Assert.Equal("identifier", exception.ParamName);
}
[Fact]
public async Task FindByReferenceIdAsync_ThrowsAnExceptionForEmptyIdentifier()
{
// Arrange
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = Mock.Of<IOpenIddictTokenStore<OpenIddictToken>>();
var cache = new OpenIddictTokenCache<OpenIddictToken>(options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentException>(
() => cache.FindByReferenceIdAsync(identifier: string.Empty, CancellationToken.None).AsTask());
Assert.Equal("identifier", exception.ParamName);
}
[Fact]
public async Task FindByReferenceIdAsync_ReturnsCachedTokenOnCacheHit()
{
// Arrange
var token = new OpenIddictToken();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = new Mock<IOpenIddictTokenStore<OpenIddictToken>>();
store.Setup(store => store.GetIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync("token-id");
store.Setup(store => store.GetReferenceIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync("reference-id");
store.Setup(store => store.GetApplicationIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync((string?) null);
store.Setup(store => store.GetAuthorizationIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync((string?) null);
store.Setup(store => store.GetSubjectAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync((string?) null);
var cache = new OpenIddictTokenCache<OpenIddictToken>(options, store.Object);
await cache.AddAsync(token, CancellationToken.None);
// Act
var result = await cache.FindByReferenceIdAsync("reference-id", CancellationToken.None);
// Assert
Assert.Same(token, result);
store.Verify(store => store.FindByReferenceIdAsync("reference-id", It.IsAny<CancellationToken>()), Times.Never());
}
[Fact]
public async Task FindByReferenceIdAsync_QueriesStoreOnCacheMiss()
{
// Arrange
var token = new OpenIddictToken();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = new Mock<IOpenIddictTokenStore<OpenIddictToken>>();
store.Setup(store => store.FindByReferenceIdAsync("reference-id", It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
store.Setup(store => store.GetIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync("token-id");
store.Setup(store => store.GetReferenceIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync("reference-id");
store.Setup(store => store.GetApplicationIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync((string?) null);
store.Setup(store => store.GetAuthorizationIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync((string?) null);
store.Setup(store => store.GetSubjectAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync((string?) null);
var cache = new OpenIddictTokenCache<OpenIddictToken>(options, store.Object);
// Act
var result = await cache.FindByReferenceIdAsync("reference-id", CancellationToken.None);
// Assert
Assert.Same(token, result);
store.Verify(store => store.FindByReferenceIdAsync("reference-id", It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task FindBySubjectAsync_QueriesStoreOnCacheMiss()
{
// Arrange
var tokens = new[]
{
new OpenIddictToken(),
new OpenIddictToken()
};
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = new Mock<IOpenIddictTokenStore<OpenIddictToken>>();
store.Setup(store => store.GetIdAsync(tokens[0], It.IsAny<CancellationToken>()))
.ReturnsAsync("token-id-1");
store.Setup(store => store.GetReferenceIdAsync(tokens[0], It.IsAny<CancellationToken>()))
.ReturnsAsync((string?) null);
store.Setup(store => store.GetApplicationIdAsync(tokens[0], It.IsAny<CancellationToken>()))
.ReturnsAsync((string?) null);
store.Setup(store => store.GetAuthorizationIdAsync(tokens[0], It.IsAny<CancellationToken>()))
.ReturnsAsync((string?) null);
store.Setup(store => store.GetSubjectAsync(tokens[0], It.IsAny<CancellationToken>()))
.ReturnsAsync("subject");
store.Setup(store => store.GetIdAsync(tokens[1], It.IsAny<CancellationToken>()))
.ReturnsAsync("token-id-2");
store.Setup(store => store.GetReferenceIdAsync(tokens[1], It.IsAny<CancellationToken>()))
.ReturnsAsync((string?) null);
store.Setup(store => store.GetApplicationIdAsync(tokens[1], It.IsAny<CancellationToken>()))
.ReturnsAsync((string?) null);
store.Setup(store => store.GetAuthorizationIdAsync(tokens[1], It.IsAny<CancellationToken>()))
.ReturnsAsync((string?) null);
store.Setup(store => store.GetSubjectAsync(tokens[1], It.IsAny<CancellationToken>()))
.ReturnsAsync("subject");
store.Setup(store => store.FindBySubjectAsync("subject", It.IsAny<CancellationToken>()))
.Returns(tokens.ToAsyncEnumerable());
var cache = new OpenIddictTokenCache<OpenIddictToken>(options, store.Object);
// Act
var results = await cache.FindBySubjectAsync("subject", CancellationToken.None).ToListAsync();
// Assert
Assert.Equal(2, results.Count);
Assert.Contains(tokens[0], results);
Assert.Contains(tokens[1], results);
store.Verify(store => store.FindBySubjectAsync("subject", It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task RemoveAsync_ThrowsAnExceptionForNullToken()
{
// Arrange
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = Mock.Of<IOpenIddictTokenStore<OpenIddictToken>>();
var cache = new OpenIddictTokenCache<OpenIddictToken>(options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => cache.RemoveAsync(token: null!, CancellationToken.None).AsTask());
Assert.Equal("token", exception.ParamName);
}
[Fact]
public async Task RemoveAsync_InvalidatesCachedEntries()
{
// Arrange
var token = new OpenIddictToken();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = new Mock<IOpenIddictTokenStore<OpenIddictToken>>();
store.Setup(store => store.GetIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync("token-id");
store.Setup(store => store.GetReferenceIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync((string?) null);
store.Setup(store => store.GetApplicationIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync((string?) null);
store.Setup(store => store.GetAuthorizationIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync((string?) null);
store.Setup(store => store.GetSubjectAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync((string?) null);
var cache = new OpenIddictTokenCache<OpenIddictToken>(options, store.Object);
await cache.AddAsync(token, CancellationToken.None);
// Act
await cache.RemoveAsync(token, CancellationToken.None);
var result = await cache.FindByIdAsync("token-id", CancellationToken.None);
// Assert
Assert.Null(result);
}
[Fact]
public async Task RemoveAsync_ThrowsForTokenWithoutId()
{
// Arrange
var token = new OpenIddictToken();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = new Mock<IOpenIddictTokenStore<OpenIddictToken>>();
store.Setup(store => store.GetIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync((string?) null);
var cache = new OpenIddictTokenCache<OpenIddictToken>(options, store.Object);
// Act and assert
await Assert.ThrowsAsync<InvalidOperationException>(
() => cache.RemoveAsync(token, CancellationToken.None).AsTask());
}
public sealed class OpenIddictToken { }
}

3561
test/OpenIddict.Core.Tests/Managers/OpenIddictApplicationManagerTests.cs

File diff suppressed because it is too large

1290
test/OpenIddict.Core.Tests/Managers/OpenIddictAuthorizationManagerTests.cs

File diff suppressed because it is too large

977
test/OpenIddict.Core.Tests/Managers/OpenIddictScopeManagerTests.cs

@ -0,0 +1,977 @@
/*
* 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 System.Collections.Immutable;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Moq;
using Xunit;
namespace OpenIddict.Core.Tests;
public class OpenIddictScopeManagerTests
{
[Fact]
public void Constructor_ThrowsAnExceptionForNullCache()
{
// Arrange
var cache = (IOpenIddictScopeCache<CustomScope>) null!;
var logger = Mock.Of<ILogger<OpenIddictScopeManager<CustomScope>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>();
var store = Mock.Of<IOpenIddictScopeStore<CustomScope>>();
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(
() => new OpenIddictScopeManager<CustomScope>(cache, logger, options, store));
Assert.Equal("cache", exception.ParamName);
}
[Fact]
public void Constructor_ThrowsAnExceptionForNullLogger()
{
// Arrange
var cache = Mock.Of<IOpenIddictScopeCache<CustomScope>>();
var logger = (ILogger<OpenIddictScopeManager<CustomScope>>) null!;
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>();
var store = Mock.Of<IOpenIddictScopeStore<CustomScope>>();
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(
() => new OpenIddictScopeManager<CustomScope>(cache, logger, options, store));
Assert.Equal("logger", exception.ParamName);
}
[Fact]
public void Constructor_ThrowsAnExceptionForNullOptions()
{
// Arrange
var cache = Mock.Of<IOpenIddictScopeCache<CustomScope>>();
var logger = Mock.Of<ILogger<OpenIddictScopeManager<CustomScope>>>();
var options = (IOptionsMonitor<OpenIddictCoreOptions>) null!;
var store = Mock.Of<IOpenIddictScopeStore<CustomScope>>();
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(
() => new OpenIddictScopeManager<CustomScope>(cache, logger, options, store));
Assert.Equal("options", exception.ParamName);
}
[Fact]
public void Constructor_ThrowsAnExceptionForNullStore()
{
// Arrange
var cache = Mock.Of<IOpenIddictScopeCache<CustomScope>>();
var logger = Mock.Of<ILogger<OpenIddictScopeManager<CustomScope>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>();
var store = (IOpenIddictScopeStore<CustomScope>) null!;
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(
() => new OpenIddictScopeManager<CustomScope>(cache, logger, options, store));
Assert.Equal("store", exception.ParamName);
}
[Fact]
public async Task CountAsync_CallsStoreMethod()
{
// Arrange
var cache = Mock.Of<IOpenIddictScopeCache<CustomScope>>();
var logger = Mock.Of<ILogger<OpenIddictScopeManager<CustomScope>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = new Mock<IOpenIddictScopeStore<CustomScope>>();
store.Setup(store => store.CountAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(42);
var manager = new OpenIddictScopeManager<CustomScope>(cache, logger, options, store.Object);
// Act
var count = await manager.CountAsync();
// Assert
Assert.Equal(42, count);
store.Verify(store => store.CountAsync(It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task CountAsync_WithQuery_ThrowsAnExceptionForNullQuery()
{
// Arrange
var cache = Mock.Of<IOpenIddictScopeCache<CustomScope>>();
var logger = Mock.Of<ILogger<OpenIddictScopeManager<CustomScope>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>();
var store = Mock.Of<IOpenIddictScopeStore<CustomScope>>();
var manager = new OpenIddictScopeManager<CustomScope>(cache, logger, options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => manager.CountAsync<CustomScope>(query: null!).AsTask());
Assert.Equal("query", exception.ParamName);
}
[Fact]
public async Task CreateAsync_ThrowsAnExceptionForNullScope()
{
// Arrange
var cache = Mock.Of<IOpenIddictScopeCache<CustomScope>>();
var logger = Mock.Of<ILogger<OpenIddictScopeManager<CustomScope>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = Mock.Of<IOpenIddictScopeStore<CustomScope>>();
var manager = new OpenIddictScopeManager<CustomScope>(cache, logger, options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => manager.CreateAsync(scope: null!).AsTask());
Assert.Equal("scope", exception.ParamName);
}
[Fact]
public async Task DeleteAsync_ThrowsAnExceptionForNullScope()
{
// Arrange
var cache = Mock.Of<IOpenIddictScopeCache<CustomScope>>();
var logger = Mock.Of<ILogger<OpenIddictScopeManager<CustomScope>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = Mock.Of<IOpenIddictScopeStore<CustomScope>>();
var manager = new OpenIddictScopeManager<CustomScope>(cache, logger, options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => manager.DeleteAsync(scope: null!).AsTask());
Assert.Equal("scope", exception.ParamName);
}
[Fact]
public async Task DeleteAsync_RemovesScopeFromCache_WhenCachingIsEnabled()
{
// Arrange
var cache = new Mock<IOpenIddictScopeCache<CustomScope>>();
var logger = Mock.Of<ILogger<OpenIddictScopeManager<CustomScope>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions { DisableEntityCaching = false });
var store = new Mock<IOpenIddictScopeStore<CustomScope>>();
var scope = new CustomScope();
var manager = new OpenIddictScopeManager<CustomScope>(cache.Object, logger, options, store.Object);
// Act
await manager.DeleteAsync(scope);
// Assert
cache.Verify(cache => cache.RemoveAsync(scope, It.IsAny<CancellationToken>()), Times.Once());
store.Verify(store => store.DeleteAsync(scope, It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task DeleteAsync_DoesNotRemoveFromCache_WhenCachingIsDisabled()
{
// Arrange
var cache = new Mock<IOpenIddictScopeCache<CustomScope>>();
var logger = Mock.Of<ILogger<OpenIddictScopeManager<CustomScope>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions { DisableEntityCaching = true });
var store = new Mock<IOpenIddictScopeStore<CustomScope>>();
var scope = new CustomScope();
var manager = new OpenIddictScopeManager<CustomScope>(cache.Object, logger, options, store.Object);
// Act
await manager.DeleteAsync(scope);
// Assert
cache.Verify(cache => cache.RemoveAsync(It.IsAny<CustomScope>(), It.IsAny<CancellationToken>()), Times.Never());
store.Verify(store => store.DeleteAsync(scope, It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task FindByIdAsync_ThrowsAnExceptionForNullIdentifier()
{
// Arrange
var cache = Mock.Of<IOpenIddictScopeCache<CustomScope>>();
var logger = Mock.Of<ILogger<OpenIddictScopeManager<CustomScope>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>();
var store = Mock.Of<IOpenIddictScopeStore<CustomScope>>();
var manager = new OpenIddictScopeManager<CustomScope>(cache, logger, options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => manager.FindByIdAsync(identifier: null!).AsTask());
Assert.Equal("identifier", exception.ParamName);
}
[Fact]
public async Task FindByIdAsync_ThrowsAnExceptionForEmptyIdentifier()
{
// Arrange
var cache = Mock.Of<IOpenIddictScopeCache<CustomScope>>();
var logger = Mock.Of<ILogger<OpenIddictScopeManager<CustomScope>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>();
var store = Mock.Of<IOpenIddictScopeStore<CustomScope>>();
var manager = new OpenIddictScopeManager<CustomScope>(cache, logger, options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentException>(
() => manager.FindByIdAsync(identifier: string.Empty).AsTask());
Assert.Equal("identifier", exception.ParamName);
}
[Fact]
public async Task FindByIdAsync_UsesCache_WhenCachingIsEnabled()
{
// Arrange
var scope = new CustomScope();
var cache = new Mock<IOpenIddictScopeCache<CustomScope>>();
var logger = Mock.Of<ILogger<OpenIddictScopeManager<CustomScope>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions { DisableEntityCaching = false });
var store = new Mock<IOpenIddictScopeStore<CustomScope>>();
cache.Setup(cache => cache.FindByIdAsync("id", It.IsAny<CancellationToken>()))
.ReturnsAsync(scope);
store.Setup(store => store.GetIdAsync(scope, It.IsAny<CancellationToken>()))
.ReturnsAsync("id");
var manager = new OpenIddictScopeManager<CustomScope>(cache.Object, logger, options, store.Object);
// Act
var result = await manager.FindByIdAsync("id");
// Assert
Assert.Same(scope, result);
cache.Verify(cache => cache.FindByIdAsync("id", It.IsAny<CancellationToken>()), Times.Once());
store.Verify(store => store.FindByIdAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never());
}
[Fact]
public async Task FindByIdAsync_UsesStore_WhenCachingIsDisabled()
{
// Arrange
var scope = new CustomScope();
var cache = new Mock<IOpenIddictScopeCache<CustomScope>>();
var logger = Mock.Of<ILogger<OpenIddictScopeManager<CustomScope>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions { DisableEntityCaching = true });
var store = new Mock<IOpenIddictScopeStore<CustomScope>>();
store.Setup(store => store.FindByIdAsync("id", It.IsAny<CancellationToken>()))
.ReturnsAsync(scope);
store.Setup(store => store.GetIdAsync(scope, It.IsAny<CancellationToken>()))
.ReturnsAsync("id");
var manager = new OpenIddictScopeManager<CustomScope>(cache.Object, logger, options, store.Object);
// Act
var result = await manager.FindByIdAsync("id");
// Assert
Assert.Same(scope, result);
cache.Verify(cache => cache.FindByIdAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never());
store.Verify(store => store.FindByIdAsync("id", It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task FindByNameAsync_ThrowsAnExceptionForNullName()
{
// Arrange
var cache = Mock.Of<IOpenIddictScopeCache<CustomScope>>();
var logger = Mock.Of<ILogger<OpenIddictScopeManager<CustomScope>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>();
var store = Mock.Of<IOpenIddictScopeStore<CustomScope>>();
var manager = new OpenIddictScopeManager<CustomScope>(cache, logger, options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => manager.FindByNameAsync(name: null!).AsTask());
Assert.Equal("name", exception.ParamName);
}
[Fact]
public async Task FindByNameAsync_ThrowsAnExceptionForEmptyName()
{
// Arrange
var cache = Mock.Of<IOpenIddictScopeCache<CustomScope>>();
var logger = Mock.Of<ILogger<OpenIddictScopeManager<CustomScope>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>();
var store = Mock.Of<IOpenIddictScopeStore<CustomScope>>();
var manager = new OpenIddictScopeManager<CustomScope>(cache, logger, options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentException>(
() => manager.FindByNameAsync(name: string.Empty).AsTask());
Assert.Equal("name", exception.ParamName);
}
[Fact]
public async Task FindByNamesAsync_ReturnsEmptyWhenNoScopesMatch()
{
// Arrange
var cache = new Mock<IOpenIddictScopeCache<CustomScope>>();
var logger = Mock.Of<ILogger<OpenIddictScopeManager<CustomScope>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = Mock.Of<IOpenIddictScopeStore<CustomScope>>();
cache.Setup(cache => cache.FindByNamesAsync(It.IsAny<ImmutableArray<string>>(), It.IsAny<CancellationToken>()))
.Returns((ImmutableArray<string> names, CancellationToken cancellationToken) =>
{
return Enumerable.Empty<CustomScope>().ToAsyncEnumerable();
});
var manager = new OpenIddictScopeManager<CustomScope>(cache.Object, logger, options, store);
// Act
var results = new List<CustomScope>();
await foreach (var scope in manager.FindByNamesAsync(["scope1", "scope2"]))
{
results.Add(scope);
}
// Assert
Assert.Empty(results);
}
[Fact]
public void FindByResourceAsync_ThrowsAnExceptionForNullResource()
{
// Arrange
var cache = Mock.Of<IOpenIddictScopeCache<CustomScope>>();
var logger = Mock.Of<ILogger<OpenIddictScopeManager<CustomScope>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>();
var store = Mock.Of<IOpenIddictScopeStore<CustomScope>>();
var manager = new OpenIddictScopeManager<CustomScope>(cache, logger, options, store);
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(
() => manager.FindByResourceAsync(resource: null!));
Assert.Equal("resource", exception.ParamName);
}
[Fact]
public void FindByResourceAsync_ThrowsAnExceptionForEmptyResource()
{
// Arrange
var cache = Mock.Of<IOpenIddictScopeCache<CustomScope>>();
var logger = Mock.Of<ILogger<OpenIddictScopeManager<CustomScope>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>();
var store = Mock.Of<IOpenIddictScopeStore<CustomScope>>();
var manager = new OpenIddictScopeManager<CustomScope>(cache, logger, options, store);
// Act and assert
var exception = Assert.Throws<ArgumentException>(
() => manager.FindByResourceAsync(resource: string.Empty));
Assert.Equal("resource", exception.ParamName);
}
[Fact]
public async Task GetAsync_WithQuery_ThrowsAnExceptionForNullQuery()
{
// Arrange
var cache = Mock.Of<IOpenIddictScopeCache<CustomScope>>();
var logger = Mock.Of<ILogger<OpenIddictScopeManager<CustomScope>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>();
var store = Mock.Of<IOpenIddictScopeStore<CustomScope>>();
var manager = new OpenIddictScopeManager<CustomScope>(cache, logger, options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => manager.GetAsync<CustomScope>(query: null!).AsTask());
Assert.Equal("query", exception.ParamName);
}
[Fact]
public async Task GetAsync_WithQueryAndState_ThrowsAnExceptionForNullQuery()
{
// Arrange
var cache = Mock.Of<IOpenIddictScopeCache<CustomScope>>();
var logger = Mock.Of<ILogger<OpenIddictScopeManager<CustomScope>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>();
var store = Mock.Of<IOpenIddictScopeStore<CustomScope>>();
var manager = new OpenIddictScopeManager<CustomScope>(cache, logger, options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => manager.GetAsync<object, CustomScope>(query: null!, state: null!).AsTask());
Assert.Equal("query", exception.ParamName);
}
[Fact]
public async Task GetDescriptionAsync_ThrowsAnExceptionForNullScope()
{
// Arrange
var cache = Mock.Of<IOpenIddictScopeCache<CustomScope>>();
var logger = Mock.Of<ILogger<OpenIddictScopeManager<CustomScope>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>();
var store = Mock.Of<IOpenIddictScopeStore<CustomScope>>();
var manager = new OpenIddictScopeManager<CustomScope>(cache, logger, options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => manager.GetDescriptionAsync(scope: null!).AsTask());
Assert.Equal("scope", exception.ParamName);
}
[Fact]
public async Task GetDescriptionsAsync_ThrowsAnExceptionForNullScope()
{
// Arrange
var cache = Mock.Of<IOpenIddictScopeCache<CustomScope>>();
var logger = Mock.Of<ILogger<OpenIddictScopeManager<CustomScope>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>();
var store = Mock.Of<IOpenIddictScopeStore<CustomScope>>();
var manager = new OpenIddictScopeManager<CustomScope>(cache, logger, options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => manager.GetDescriptionsAsync(scope: null!).AsTask());
Assert.Equal("scope", exception.ParamName);
}
[Fact]
public async Task GetDisplayNameAsync_ThrowsAnExceptionForNullScope()
{
// Arrange
var cache = Mock.Of<IOpenIddictScopeCache<CustomScope>>();
var logger = Mock.Of<ILogger<OpenIddictScopeManager<CustomScope>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>();
var store = Mock.Of<IOpenIddictScopeStore<CustomScope>>();
var manager = new OpenIddictScopeManager<CustomScope>(cache, logger, options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => manager.GetDisplayNameAsync(scope: null!).AsTask());
Assert.Equal("scope", exception.ParamName);
}
[Fact]
public async Task GetDisplayNamesAsync_ThrowsAnExceptionForNullScope()
{
// Arrange
var cache = Mock.Of<IOpenIddictScopeCache<CustomScope>>();
var logger = Mock.Of<ILogger<OpenIddictScopeManager<CustomScope>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>();
var store = Mock.Of<IOpenIddictScopeStore<CustomScope>>();
var manager = new OpenIddictScopeManager<CustomScope>(cache, logger, options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => manager.GetDisplayNamesAsync(scope: null!).AsTask());
Assert.Equal("scope", exception.ParamName);
}
[Fact]
public async Task GetIdAsync_ThrowsAnExceptionForNullScope()
{
// Arrange
var cache = Mock.Of<IOpenIddictScopeCache<CustomScope>>();
var logger = Mock.Of<ILogger<OpenIddictScopeManager<CustomScope>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>();
var store = Mock.Of<IOpenIddictScopeStore<CustomScope>>();
var manager = new OpenIddictScopeManager<CustomScope>(cache, logger, options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => manager.GetIdAsync(scope: null!).AsTask());
Assert.Equal("scope", exception.ParamName);
}
[Fact]
public async Task GetIdAsync_ReturnsIdentifierFromStore()
{
// Arrange
var scope = new CustomScope();
var cache = Mock.Of<IOpenIddictScopeCache<CustomScope>>();
var logger = Mock.Of<ILogger<OpenIddictScopeManager<CustomScope>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>();
var store = new Mock<IOpenIddictScopeStore<CustomScope>>();
store.Setup(store => store.GetIdAsync(scope, It.IsAny<CancellationToken>()))
.ReturnsAsync("unique-scope-id");
var manager = new OpenIddictScopeManager<CustomScope>(cache, logger, options, store.Object);
// Act
var id = await manager.GetIdAsync(scope);
// Assert
Assert.Equal("unique-scope-id", id);
store.Verify(store => store.GetIdAsync(scope, It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task GetLocalizedDescriptionAsync_ThrowsAnExceptionForNullScope()
{
// Arrange
var cache = Mock.Of<IOpenIddictScopeCache<CustomScope>>();
var logger = Mock.Of<ILogger<OpenIddictScopeManager<CustomScope>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>();
var store = Mock.Of<IOpenIddictScopeStore<CustomScope>>();
var manager = new OpenIddictScopeManager<CustomScope>(cache, logger, options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => manager.GetLocalizedDescriptionAsync(scope: null!, CultureInfo.InvariantCulture).AsTask());
Assert.Equal("scope", exception.ParamName);
}
[Fact]
public async Task GetLocalizedDescriptionAsync_ReturnsDescriptionForMatchingCulture()
{
// Arrange
var scope = new CustomScope();
var cache = Mock.Of<IOpenIddictScopeCache<CustomScope>>();
var logger = Mock.Of<ILogger<OpenIddictScopeManager<CustomScope>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>();
var store = new Mock<IOpenIddictScopeStore<CustomScope>>();
store.Setup(store => store.GetDescriptionsAsync(scope, It.IsAny<CancellationToken>()))
.ReturnsAsync(ImmutableDictionary.Create<CultureInfo, string>()
.Add(CultureInfo.GetCultureInfo("en-US"), "English description")
.Add(CultureInfo.GetCultureInfo("fr-FR"), "Description fran�aise"));
var manager = new OpenIddictScopeManager<CustomScope>(cache, logger, options, store.Object);
// Act
var description = await manager.GetLocalizedDescriptionAsync(scope, CultureInfo.GetCultureInfo("fr-FR"));
// Assert
Assert.Equal("Description fran�aise", description);
}
[Fact]
public async Task GetLocalizedDescriptionAsync_FallsBackToNonLocalizedDescriptionWhenNoCultureMatches()
{
// Arrange
var scope = new CustomScope();
var cache = Mock.Of<IOpenIddictScopeCache<CustomScope>>();
var logger = Mock.Of<ILogger<OpenIddictScopeManager<CustomScope>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>();
var store = new Mock<IOpenIddictScopeStore<CustomScope>>();
store.Setup(store => store.GetDescriptionsAsync(scope, It.IsAny<CancellationToken>()))
.ReturnsAsync(ImmutableDictionary.Create<CultureInfo, string>()
.Add(CultureInfo.GetCultureInfo("en-US"), "English description"));
store.Setup(store => store.GetDescriptionAsync(scope, It.IsAny<CancellationToken>()))
.ReturnsAsync("Default description");
var manager = new OpenIddictScopeManager<CustomScope>(cache, logger, options, store.Object);
// Act
var description = await manager.GetLocalizedDescriptionAsync(scope, CultureInfo.GetCultureInfo("ja-JP"));
// Assert
Assert.Equal("Default description", description);
}
[Fact]
public async Task GetLocalizedDisplayNameAsync_ThrowsAnExceptionForNullScope()
{
// Arrange
var cache = Mock.Of<IOpenIddictScopeCache<CustomScope>>();
var logger = Mock.Of<ILogger<OpenIddictScopeManager<CustomScope>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>();
var store = Mock.Of<IOpenIddictScopeStore<CustomScope>>();
var manager = new OpenIddictScopeManager<CustomScope>(cache, logger, options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => manager.GetLocalizedDisplayNameAsync(scope: null!, CultureInfo.InvariantCulture).AsTask());
Assert.Equal("scope", exception.ParamName);
}
[Fact]
public async Task GetLocalizedDisplayNameAsync_ReturnsDisplayNameForMatchingCulture()
{
// Arrange
var scope = new CustomScope();
var cache = Mock.Of<IOpenIddictScopeCache<CustomScope>>();
var logger = Mock.Of<ILogger<OpenIddictScopeManager<CustomScope>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>();
var store = new Mock<IOpenIddictScopeStore<CustomScope>>();
store.Setup(store => store.GetDisplayNamesAsync(scope, It.IsAny<CancellationToken>()))
.ReturnsAsync(ImmutableDictionary.Create<CultureInfo, string>()
.Add(CultureInfo.GetCultureInfo("en-US"), "English name")
.Add(CultureInfo.GetCultureInfo("fr-FR"), "Nom fran�ais"));
var manager = new OpenIddictScopeManager<CustomScope>(cache, logger, options, store.Object);
// Act
var name = await manager.GetLocalizedDisplayNameAsync(scope, CultureInfo.GetCultureInfo("fr-FR"));
// Assert
Assert.Equal("Nom fran�ais", name);
}
[Fact]
public async Task GetNameAsync_ThrowsAnExceptionForNullScope()
{
// Arrange
var cache = Mock.Of<IOpenIddictScopeCache<CustomScope>>();
var logger = Mock.Of<ILogger<OpenIddictScopeManager<CustomScope>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>();
var store = Mock.Of<IOpenIddictScopeStore<CustomScope>>();
var manager = new OpenIddictScopeManager<CustomScope>(cache, logger, options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => manager.GetNameAsync(scope: null!).AsTask());
Assert.Equal("scope", exception.ParamName);
}
[Fact]
public async Task GetPropertiesAsync_ThrowsAnExceptionForNullScope()
{
// Arrange
var cache = Mock.Of<IOpenIddictScopeCache<CustomScope>>();
var logger = Mock.Of<ILogger<OpenIddictScopeManager<CustomScope>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>();
var store = Mock.Of<IOpenIddictScopeStore<CustomScope>>();
var manager = new OpenIddictScopeManager<CustomScope>(cache, logger, options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => manager.GetPropertiesAsync(scope: null!).AsTask());
Assert.Equal("scope", exception.ParamName);
}
[Fact]
public async Task GetResourcesAsync_ThrowsAnExceptionForNullScope()
{
// Arrange
var cache = Mock.Of<IOpenIddictScopeCache<CustomScope>>();
var logger = Mock.Of<ILogger<OpenIddictScopeManager<CustomScope>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>();
var store = Mock.Of<IOpenIddictScopeStore<CustomScope>>();
var manager = new OpenIddictScopeManager<CustomScope>(cache, logger, options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => manager.GetResourcesAsync(scope: null!).AsTask());
Assert.Equal("scope", exception.ParamName);
}
[Fact]
public async Task ListAsync_ReturnsAllScopes()
{
// Arrange
var scopes = new[] { new CustomScope(), new CustomScope() };
var cache = Mock.Of<IOpenIddictScopeCache<CustomScope>>();
var logger = Mock.Of<ILogger<OpenIddictScopeManager<CustomScope>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = new Mock<IOpenIddictScopeStore<CustomScope>>();
store.Setup(store => store.ListAsync(It.IsAny<int?>(), It.IsAny<int?>(), It.IsAny<CancellationToken>()))
.Returns(scopes.ToAsyncEnumerable());
var manager = new OpenIddictScopeManager<CustomScope>(cache, logger, options, store.Object);
// Act
var results = new List<CustomScope>();
await foreach (var scp in manager.ListAsync())
{
results.Add(scp);
}
// Assert
Assert.Equal(2, results.Count);
store.Verify(store => store.ListAsync(It.IsAny<int?>(), It.IsAny<int?>(), It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task ListResourcesAsync_ReturnsDeduplicatedResourcesFromMatchingScopes()
{
// Arrange
var scopes = new[] { new CustomScope(), new CustomScope() };
var cache = Mock.Of<IOpenIddictScopeCache<CustomScope>>();
var logger = Mock.Of<ILogger<OpenIddictScopeManager<CustomScope>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions { DisableEntityCaching = true });
var store = new Mock<IOpenIddictScopeStore<CustomScope>>();
store.Setup(store => store.FindByNamesAsync(It.IsAny<ImmutableArray<string>>(), It.IsAny<CancellationToken>()))
.Returns(scopes.ToAsyncEnumerable());
store.Setup(store => store.GetNameAsync(scopes[0], It.IsAny<CancellationToken>()))
.ReturnsAsync("openid");
store.Setup(store => store.GetNameAsync(scopes[1], It.IsAny<CancellationToken>()))
.ReturnsAsync("profile");
store.Setup(store => store.GetResourcesAsync(scopes[0], It.IsAny<CancellationToken>()))
.ReturnsAsync(["resource1", "resource2"]);
store.Setup(store => store.GetResourcesAsync(scopes[1], It.IsAny<CancellationToken>()))
.ReturnsAsync(["resource2", "resource3"]);
var manager = new OpenIddictScopeManager<CustomScope>(cache, logger, options, store.Object);
// Act
var resources = await manager.ListResourcesAsync(["openid", "profile"]).ToListAsync();
// Assert
Assert.Equal(3, resources.Count);
Assert.Contains("resource1", resources);
Assert.Contains("resource2", resources);
Assert.Contains("resource3", resources);
}
[Fact]
public async Task PopulateAsync_ThrowsAnExceptionForNullScope()
{
// Arrange
var cache = Mock.Of<IOpenIddictScopeCache<CustomScope>>();
var logger = Mock.Of<ILogger<OpenIddictScopeManager<CustomScope>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>();
var store = Mock.Of<IOpenIddictScopeStore<CustomScope>>();
var manager = new OpenIddictScopeManager<CustomScope>(cache, logger, options, store);
var descriptor = new OpenIddictScopeDescriptor();
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => manager.PopulateAsync(scope: null!, descriptor).AsTask());
Assert.Equal("scope", exception.ParamName);
}
[Fact]
public async Task PopulateAsync_ThrowsAnExceptionForNullDescriptor()
{
// Arrange
var scope = new CustomScope();
var cache = Mock.Of<IOpenIddictScopeCache<CustomScope>>();
var logger = Mock.Of<ILogger<OpenIddictScopeManager<CustomScope>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>();
var store = Mock.Of<IOpenIddictScopeStore<CustomScope>>();
var manager = new OpenIddictScopeManager<CustomScope>(cache, logger, options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => manager.PopulateAsync(scope, descriptor: null!).AsTask());
Assert.Equal("descriptor", exception.ParamName);
}
[Fact]
public async Task UpdateAsync_ThrowsAnExceptionForNullScope()
{
// Arrange
var cache = Mock.Of<IOpenIddictScopeCache<CustomScope>>();
var logger = Mock.Of<ILogger<OpenIddictScopeManager<CustomScope>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = Mock.Of<IOpenIddictScopeStore<CustomScope>>();
var manager = new OpenIddictScopeManager<CustomScope>(cache, logger, options, store);
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => manager.UpdateAsync(scope: null!).AsTask());
Assert.Equal("scope", exception.ParamName);
}
[Fact]
public async Task UpdateAsync_WithDescriptor_ThrowsAnExceptionForNullScope()
{
// Arrange
var cache = Mock.Of<IOpenIddictScopeCache<CustomScope>>();
var logger = Mock.Of<ILogger<OpenIddictScopeManager<CustomScope>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = Mock.Of<IOpenIddictScopeStore<CustomScope>>();
var manager = new OpenIddictScopeManager<CustomScope>(cache, logger, options, store);
var descriptor = new OpenIddictScopeDescriptor();
// Act and assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => manager.UpdateAsync(scope: null!, descriptor).AsTask());
Assert.Equal("scope", exception.ParamName);
}
[Fact]
public void ValidateAsync_ThrowsAnExceptionForNullScope()
{
// Arrange
var cache = Mock.Of<IOpenIddictScopeCache<CustomScope>>();
var logger = Mock.Of<ILogger<OpenIddictScopeManager<CustomScope>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>();
var store = Mock.Of<IOpenIddictScopeStore<CustomScope>>();
var manager = new OpenIddictScopeManager<CustomScope>(cache, logger, options, store);
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(
() => manager.ValidateAsync(scope: null!));
Assert.Equal("scope", exception.ParamName);
}
[Fact]
public async Task ValidateAsync_ReturnsErrorWhenNameIsEmpty()
{
// Arrange
var scope = new CustomScope();
var cache = Mock.Of<IOpenIddictScopeCache<CustomScope>>();
var logger = Mock.Of<ILogger<OpenIddictScopeManager<CustomScope>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = new Mock<IOpenIddictScopeStore<CustomScope>>();
store.Setup(store => store.GetNameAsync(scope, It.IsAny<CancellationToken>()))
.ReturnsAsync(string.Empty);
var manager = new OpenIddictScopeManager<CustomScope>(cache, logger, options, store.Object);
// Act
var results = await manager.ValidateAsync(scope).ToListAsync();
// Assert
Assert.Contains(results, result => result.ErrorMessage == SR.GetResourceString(SR.ID2044));
}
[Fact]
public async Task ValidateAsync_ReturnsErrorWhenNameContainsSpace()
{
// Arrange
var scope = new CustomScope();
var cache = Mock.Of<IOpenIddictScopeCache<CustomScope>>();
var logger = Mock.Of<ILogger<OpenIddictScopeManager<CustomScope>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = new Mock<IOpenIddictScopeStore<CustomScope>>();
store.Setup(store => store.GetNameAsync(scope, It.IsAny<CancellationToken>()))
.ReturnsAsync("scope name with space");
var manager = new OpenIddictScopeManager<CustomScope>(cache, logger, options, store.Object);
// Act
var results = await manager.ValidateAsync(scope).ToListAsync();
// Assert
Assert.Contains(results, result => result.ErrorMessage == SR.GetResourceString(SR.ID2045));
}
[Fact]
public async Task ValidateAsync_ReturnsErrorWhenNameIsAlreadyUsed()
{
// Arrange
var scope = new CustomScope();
var other = new CustomScope();
var cache = Mock.Of<IOpenIddictScopeCache<CustomScope>>();
var logger = Mock.Of<ILogger<OpenIddictScopeManager<CustomScope>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = new Mock<IOpenIddictScopeStore<CustomScope>>();
store.Setup(store => store.GetNameAsync(scope, It.IsAny<CancellationToken>()))
.ReturnsAsync("openid");
store.Setup(store => store.FindByNameAsync("openid", It.IsAny<CancellationToken>()))
.ReturnsAsync(other);
store.Setup(store => store.GetIdAsync(scope, It.IsAny<CancellationToken>()))
.ReturnsAsync("scope-id");
store.Setup(store => store.GetIdAsync(other, It.IsAny<CancellationToken>()))
.ReturnsAsync("other-scope-id");
var manager = new OpenIddictScopeManager<CustomScope>(cache, logger, options, store.Object);
// Act
var results = await manager.ValidateAsync(scope).ToListAsync();
// Assert
Assert.Contains(results, result => result.ErrorMessage == SR.GetResourceString(SR.ID2060));
}
[Fact]
public async Task ValidateAsync_ReturnsNoErrorsForValidScope()
{
// Arrange
var scope = new CustomScope();
var cache = Mock.Of<IOpenIddictScopeCache<CustomScope>>();
var logger = Mock.Of<ILogger<OpenIddictScopeManager<CustomScope>>>();
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(
mock => mock.CurrentValue == new OpenIddictCoreOptions());
var store = new Mock<IOpenIddictScopeStore<CustomScope>>();
store.Setup(store => store.GetNameAsync(scope, It.IsAny<CancellationToken>()))
.ReturnsAsync("openid");
store.Setup(store => store.FindByNameAsync("openid", It.IsAny<CancellationToken>()))
.ReturnsAsync((CustomScope?) null);
var manager = new OpenIddictScopeManager<CustomScope>(cache, logger, options, store.Object);
// Act
var results = await manager.ValidateAsync(scope).ToListAsync();
// Assert
Assert.DoesNotContain(results, static result => result != ValidationResult.Success);
}
public class CustomScope { }
}

1343
test/OpenIddict.Core.Tests/Managers/OpenIddictTokenManagerTests.cs

File diff suppressed because it is too large

4
test/OpenIddict.Core.Tests/OpenIddict.Core.Tests.csproj

@ -13,6 +13,10 @@
<PackageReference Include="Moq" /> <PackageReference Include="Moq" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition=" '$(TargetFrameworkIdentifier)' == '.NETFramework' ">
<PackageReference Include="System.Linq.Async" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<Using Include="OpenIddict.Abstractions" /> <Using Include="OpenIddict.Abstractions" />
<Using Include="OpenIddict.Abstractions.OpenIddictConstants" Static="true" /> <Using Include="OpenIddict.Abstractions.OpenIddictConstants" Static="true" />

255
test/OpenIddict.Core.Tests/OpenIddictCoreBuilderTests.cs

@ -1,4 +1,4 @@
/* /*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) * 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 * See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project. * the license and the contributors participating to this project.
@ -26,28 +26,28 @@ public class OpenIddictCoreBuilderTests
} }
[Fact] [Fact]
public void ReplaceApplicationManager_ThrowsAnExceptionForInvalidManager() public void ReplaceApplicationManager_ThrowsAnExceptionForClosedSourceManager()
{ {
// Arrange // Arrange
var services = CreateServices(); var services = CreateServices();
var builder = CreateBuilder(services); var builder = CreateBuilder(services);
// Act and assert // Act and assert
var exception = Assert.Throws<ArgumentException>(() => builder.ReplaceApplicationManager(typeof(object))); var exception = Assert.Throws<ArgumentException>(() => builder.ReplaceApplicationManager(typeof(ClosedGenericApplicationManager)));
Assert.Equal("type", exception.ParamName); Assert.Equal("type", exception.ParamName);
Assert.StartsWith(SR.GetResourceString(SR.ID0232), exception.Message); Assert.StartsWith(SR.GetResourceString(SR.ID0232), exception.Message);
} }
[Fact] [Fact]
public void ReplaceApplicationManager_ThrowsAnExceptionForClosedSourceManager() public void ReplaceApplicationManager_ThrowsAnExceptionForInvalidManager()
{ {
// Arrange // Arrange
var services = CreateServices(); var services = CreateServices();
var builder = CreateBuilder(services); var builder = CreateBuilder(services);
// Act and assert // Act and assert
var exception = Assert.Throws<ArgumentException>(() => builder.ReplaceApplicationManager(typeof(ClosedGenericApplicationManager))); var exception = Assert.Throws<ArgumentException>(() => builder.ReplaceApplicationManager(typeof(object)));
Assert.Equal("type", exception.ParamName); Assert.Equal("type", exception.ParamName);
Assert.StartsWith(SR.GetResourceString(SR.ID0232), exception.Message); Assert.StartsWith(SR.GetResourceString(SR.ID0232), exception.Message);
@ -71,28 +71,28 @@ public class OpenIddictCoreBuilderTests
} }
[Fact] [Fact]
public void ReplaceAuthorizationManager_ThrowsAnExceptionForInvalidManager() public void ReplaceAuthorizationManager_ThrowsAnExceptionForClosedSourceManager()
{ {
// Arrange // Arrange
var services = CreateServices(); var services = CreateServices();
var builder = CreateBuilder(services); var builder = CreateBuilder(services);
// Act and assert // Act and assert
var exception = Assert.Throws<ArgumentException>(() => builder.ReplaceAuthorizationManager(typeof(object))); var exception = Assert.Throws<ArgumentException>(() => builder.ReplaceAuthorizationManager(typeof(ClosedGenericAuthorizationManager)));
Assert.Equal("type", exception.ParamName); Assert.Equal("type", exception.ParamName);
Assert.StartsWith(SR.GetResourceString(SR.ID0232), exception.Message); Assert.StartsWith(SR.GetResourceString(SR.ID0232), exception.Message);
} }
[Fact] [Fact]
public void ReplaceAuthorizationManager_ThrowsAnExceptionForClosedSourceManager() public void ReplaceAuthorizationManager_ThrowsAnExceptionForInvalidManager()
{ {
// Arrange // Arrange
var services = CreateServices(); var services = CreateServices();
var builder = CreateBuilder(services); var builder = CreateBuilder(services);
// Act and assert // Act and assert
var exception = Assert.Throws<ArgumentException>(() => builder.ReplaceAuthorizationManager(typeof(ClosedGenericAuthorizationManager))); var exception = Assert.Throws<ArgumentException>(() => builder.ReplaceAuthorizationManager(typeof(object)));
Assert.Equal("type", exception.ParamName); Assert.Equal("type", exception.ParamName);
Assert.StartsWith(SR.GetResourceString(SR.ID0232), exception.Message); Assert.StartsWith(SR.GetResourceString(SR.ID0232), exception.Message);
@ -116,28 +116,28 @@ public class OpenIddictCoreBuilderTests
} }
[Fact] [Fact]
public void ReplaceScopeManager_ThrowsAnExceptionForInvalidManager() public void ReplaceScopeManager_ThrowsAnExceptionForClosedSourceManager()
{ {
// Arrange // Arrange
var services = CreateServices(); var services = CreateServices();
var builder = CreateBuilder(services); var builder = CreateBuilder(services);
// Act and assert // Act and assert
var exception = Assert.Throws<ArgumentException>(() => builder.ReplaceScopeManager(typeof(object))); var exception = Assert.Throws<ArgumentException>(() => builder.ReplaceScopeManager(typeof(ClosedGenericScopeManager)));
Assert.Equal("type", exception.ParamName); Assert.Equal("type", exception.ParamName);
Assert.StartsWith(SR.GetResourceString(SR.ID0232), exception.Message); Assert.StartsWith(SR.GetResourceString(SR.ID0232), exception.Message);
} }
[Fact] [Fact]
public void ReplaceScopeManager_ThrowsAnExceptionForClosedSourceManager() public void ReplaceScopeManager_ThrowsAnExceptionForInvalidManager()
{ {
// Arrange // Arrange
var services = CreateServices(); var services = CreateServices();
var builder = CreateBuilder(services); var builder = CreateBuilder(services);
// Act and assert // Act and assert
var exception = Assert.Throws<ArgumentException>(() => builder.ReplaceScopeManager(typeof(ClosedGenericScopeManager))); var exception = Assert.Throws<ArgumentException>(() => builder.ReplaceScopeManager(typeof(object)));
Assert.Equal("type", exception.ParamName); Assert.Equal("type", exception.ParamName);
Assert.StartsWith(SR.GetResourceString(SR.ID0232), exception.Message); Assert.StartsWith(SR.GetResourceString(SR.ID0232), exception.Message);
@ -161,28 +161,28 @@ public class OpenIddictCoreBuilderTests
} }
[Fact] [Fact]
public void ReplaceTokenManager_ThrowsAnExceptionForInvalidManager() public void ReplaceTokenManager_ThrowsAnExceptionForClosedSourceManager()
{ {
// Arrange // Arrange
var services = CreateServices(); var services = CreateServices();
var builder = CreateBuilder(services); var builder = CreateBuilder(services);
// Act and assert // Act and assert
var exception = Assert.Throws<ArgumentException>(() => builder.ReplaceTokenManager(typeof(object))); var exception = Assert.Throws<ArgumentException>(() => builder.ReplaceTokenManager(typeof(ClosedGenericTokenManager)));
Assert.Equal("type", exception.ParamName); Assert.Equal("type", exception.ParamName);
Assert.StartsWith(SR.GetResourceString(SR.ID0232), exception.Message); Assert.StartsWith(SR.GetResourceString(SR.ID0232), exception.Message);
} }
[Fact] [Fact]
public void ReplaceTokenManager_ThrowsAnExceptionForClosedSourceManager() public void ReplaceTokenManager_ThrowsAnExceptionForInvalidManager()
{ {
// Arrange // Arrange
var services = CreateServices(); var services = CreateServices();
var builder = CreateBuilder(services); var builder = CreateBuilder(services);
// Act and assert // Act and assert
var exception = Assert.Throws<ArgumentException>(() => builder.ReplaceTokenManager(typeof(ClosedGenericTokenManager))); var exception = Assert.Throws<ArgumentException>(() => builder.ReplaceTokenManager(typeof(object)));
Assert.Equal("type", exception.ParamName); Assert.Equal("type", exception.ParamName);
Assert.StartsWith(SR.GetResourceString(SR.ID0232), exception.Message); Assert.StartsWith(SR.GetResourceString(SR.ID0232), exception.Message);
@ -240,71 +240,169 @@ public class OpenIddictCoreBuilderTests
} }
[Fact] [Fact]
public void SetDefaultApplicationEntity_ReplacesUntypedManager() public void DisableAutomaticClientSecretRehashing_RehashingIsCorrectlyDisabled()
{ {
// Arrange // Arrange
var services = CreateServices(); var services = CreateServices();
var builder = CreateBuilder(services); var builder = CreateBuilder(services);
// Act // Act
builder.SetDefaultApplicationEntity<CustomApplication>(); builder.DisableAutomaticClientSecretRehashing();
// Assert // Assert
Assert.Contains(services, service => var provider = services.BuildServiceProvider();
service.Lifetime == ServiceLifetime.Scoped && var options = provider.GetRequiredService<IOptionsMonitor<OpenIddictCoreOptions>>().CurrentValue;
service.ServiceType == typeof(IOpenIddictApplicationManager) &&
service.ImplementationFactory is not null); Assert.True(options.DisableAutomaticClientSecretRehashing);
} }
[Fact] [Theory]
public void SetDefaultAuthorizationEntity_ReplacesUntypedManager() [InlineData("MD5")]
[InlineData("SHA384")]
[InlineData("Invalid")]
public void SetClientSecretKeyDerivationHashAlgorithm_ThrowsAnExceptionForInvalidAlgorithm(string algorithmName)
{ {
// Arrange // Arrange
var services = CreateServices(); var services = CreateServices();
var builder = CreateBuilder(services); var builder = CreateBuilder(services);
var algorithm = new System.Security.Cryptography.HashAlgorithmName(algorithmName);
// Act and assert
var exception = Assert.Throws<ArgumentException>(() => builder.SetClientSecretKeyDerivationHashAlgorithm(algorithm));
Assert.Equal("algorithm", exception.ParamName);
}
[Theory]
[InlineData("SHA1")]
[InlineData("SHA256")]
[InlineData("SHA512")]
public void SetClientSecretKeyDerivationHashAlgorithm_AlgorithmIsCorrectlySet(string algorithmName)
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
var algorithm = new System.Security.Cryptography.HashAlgorithmName(algorithmName);
// Act // Act
builder.SetDefaultAuthorizationEntity<CustomAuthorization>(); builder.SetClientSecretKeyDerivationHashAlgorithm(algorithm);
// Assert // Assert
Assert.Contains(services, service => var provider = services.BuildServiceProvider();
service.Lifetime == ServiceLifetime.Scoped && var options = provider.GetRequiredService<IOptionsMonitor<OpenIddictCoreOptions>>().CurrentValue;
service.ServiceType == typeof(IOpenIddictAuthorizationManager) &&
service.ImplementationFactory is not null); Assert.Equal(algorithm, options.ClientSecretKeyDerivationHashAlgorithm);
} }
[Fact] [Theory]
public void SetDefaultScopeEntity_ReplacesUntypedManager() [InlineData(-1)]
[InlineData(0)]
[InlineData(9_999)]
[InlineData(10_000_001)]
public void SetClientSecretKeyDerivationIterations_ThrowsAnExceptionForInvalidIterations(int iterations)
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act and assert
var exception = Assert.Throws<ArgumentOutOfRangeException>(() => builder.SetClientSecretKeyDerivationIterations(iterations));
Assert.Equal("iterations", exception.ParamName);
}
[Theory]
[InlineData(10_000)]
[InlineData(50_000)]
[InlineData(10_000_000)]
public void SetClientSecretKeyDerivationIterations_IterationsAreCorrectlySet(int iterations)
{ {
// Arrange // Arrange
var services = CreateServices(); var services = CreateServices();
var builder = CreateBuilder(services); var builder = CreateBuilder(services);
// Act // Act
builder.SetDefaultScopeEntity<CustomScope>(); builder.SetClientSecretKeyDerivationIterations(iterations);
// Assert // Assert
Assert.Contains(services, service => var provider = services.BuildServiceProvider();
service.Lifetime == ServiceLifetime.Scoped && var options = provider.GetRequiredService<IOptionsMonitor<OpenIddictCoreOptions>>().CurrentValue;
service.ServiceType == typeof(IOpenIddictScopeManager) &&
service.ImplementationFactory is not null); Assert.Equal(iterations, options.ClientSecretKeyDerivationIterations);
} }
[Fact] [Theory]
public void SetDefaultTokenEntity_ReplacesUntypedManager() [InlineData(-1)]
[InlineData(0)]
[InlineData(255)]
[InlineData(2049)]
public void SetClientSecretKeyDerivationOutputLength_ThrowsAnExceptionForInvalidLength(int length)
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act and assert
var exception = Assert.Throws<ArgumentOutOfRangeException>(() => builder.SetClientSecretKeyDerivationOutputLength(length));
Assert.Equal("length", exception.ParamName);
}
[Theory]
[InlineData(256)]
[InlineData(512)]
[InlineData(2048)]
public void SetClientSecretKeyDerivationOutputLength_LengthIsCorrectlySet(int length)
{ {
// Arrange // Arrange
var services = CreateServices(); var services = CreateServices();
var builder = CreateBuilder(services); var builder = CreateBuilder(services);
// Act // Act
builder.SetDefaultTokenEntity<CustomToken>(); builder.SetClientSecretKeyDerivationOutputLength(length);
// Assert // Assert
Assert.Contains(services, service => var provider = services.BuildServiceProvider();
service.Lifetime == ServiceLifetime.Scoped && var options = provider.GetRequiredService<IOptionsMonitor<OpenIddictCoreOptions>>().CurrentValue;
service.ServiceType == typeof(IOpenIddictTokenManager) &&
service.ImplementationFactory is not null); Assert.Equal(length, options.ClientSecretKeyDerivationOutputLength);
}
[Theory]
[InlineData(-1)]
[InlineData(0)]
[InlineData(127)]
[InlineData(1025)]
public void SetClientSecretKeyDerivationSaltLength_ThrowsAnExceptionForInvalidLength(int length)
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act and assert
var exception = Assert.Throws<ArgumentOutOfRangeException>(() => builder.SetClientSecretKeyDerivationSaltLength(length));
Assert.Equal("length", exception.ParamName);
}
[Theory]
[InlineData(128)]
[InlineData(256)]
[InlineData(1024)]
public void SetClientSecretKeyDerivationSaltLength_LengthIsCorrectlySet(int length)
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.SetClientSecretKeyDerivationSaltLength(length);
// Assert
var provider = services.BuildServiceProvider();
var options = provider.GetRequiredService<IOptionsMonitor<OpenIddictCoreOptions>>().CurrentValue;
Assert.Equal(length, options.ClientSecretKeyDerivationSaltLength);
} }
[Theory] [Theory]
@ -340,7 +438,74 @@ public class OpenIddictCoreBuilderTests
Assert.Equal(42, options.EntityCacheLimit); Assert.Equal(42, options.EntityCacheLimit);
} }
private static OpenIddictCoreBuilder CreateBuilder(IServiceCollection services) [Fact]
public void SetDefaultApplicationEntity_ReplacesUntypedManager()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.SetDefaultApplicationEntity<CustomApplication>();
// Assert
Assert.Contains(services, service =>
service.Lifetime == ServiceLifetime.Scoped &&
service.ServiceType == typeof(IOpenIddictApplicationManager) &&
service.ImplementationFactory is not null);
}
[Fact]
public void SetDefaultAuthorizationEntity_ReplacesUntypedManager()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.SetDefaultAuthorizationEntity<CustomAuthorization>();
// Assert
Assert.Contains(services, service =>
service.Lifetime == ServiceLifetime.Scoped &&
service.ServiceType == typeof(IOpenIddictAuthorizationManager) &&
service.ImplementationFactory is not null);
}
[Fact]
public void SetDefaultScopeEntity_ReplacesUntypedManager()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.SetDefaultScopeEntity<CustomScope>();
// Assert
Assert.Contains(services, service =>
service.Lifetime == ServiceLifetime.Scoped &&
service.ServiceType == typeof(IOpenIddictScopeManager) &&
service.ImplementationFactory is not null);
}
[Fact]
public void SetDefaultTokenEntity_ReplacesUntypedManager()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.SetDefaultTokenEntity<CustomToken>();
// Assert
Assert.Contains(services, service =>
service.Lifetime == ServiceLifetime.Scoped &&
service.ServiceType == typeof(IOpenIddictTokenManager) &&
service.ImplementationFactory is not null);
}
private static OpenIddictCoreBuilder CreateBuilder(IServiceCollection services)
=> services.AddOpenIddict().AddCore(); => services.AddOpenIddict().AddCore();
private static ServiceCollection CreateServices() private static ServiceCollection CreateServices()

303
test/OpenIddict.Core.Tests/OpenIddictCoreConfigurationTests.cs

@ -0,0 +1,303 @@
/*
* 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 System.Security.Cryptography;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
namespace OpenIddict.Core.Tests;
public class OpenIddictCoreConfigurationTests
{
[Fact]
public void Constructor_ThrowsAnExceptionForNullProvider()
{
// Arrange
var provider = (IServiceProvider) null!;
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => new OpenIddictCoreConfiguration(provider));
Assert.Equal("provider", exception.ParamName);
}
[Fact]
public void PostConfigure_ThrowsAnExceptionForNullOptions()
{
// Arrange
var services = new ServiceCollection();
var provider = services.BuildServiceProvider();
var configuration = new OpenIddictCoreConfiguration(provider);
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => configuration.PostConfigure(null, null!));
Assert.Equal("options", exception.ParamName);
}
[Fact]
public void PostConfigure_SetsTimeProviderToSystemWhenNotRegistered()
{
// Arrange
var services = new ServiceCollection();
var provider = services.BuildServiceProvider();
var configuration = new OpenIddictCoreConfiguration(provider);
var options = new OpenIddictCoreOptions();
// Act
configuration.PostConfigure(null, options);
// Assert
Assert.Same(TimeProvider.System, options.TimeProvider);
}
[Fact]
public void PostConfigure_UsesRegisteredTimeProvider()
{
// Arrange
var customTimeProvider = new CustomTimeProvider();
var services = new ServiceCollection();
services.AddSingleton<TimeProvider>(customTimeProvider);
var provider = services.BuildServiceProvider();
var configuration = new OpenIddictCoreConfiguration(provider);
var options = new OpenIddictCoreOptions();
// Act
configuration.PostConfigure(null, options);
// Assert
Assert.Same(customTimeProvider, options.TimeProvider);
}
[Fact]
public void PostConfigure_DoesNotOverrideExplicitlySetTimeProvider()
{
// Arrange
var explicitTimeProvider = new CustomTimeProvider();
var registeredTimeProvider = new CustomTimeProvider();
var services = new ServiceCollection();
services.AddSingleton<TimeProvider>(registeredTimeProvider);
var provider = services.BuildServiceProvider();
var configuration = new OpenIddictCoreConfiguration(provider);
var options = new OpenIddictCoreOptions
{
TimeProvider = explicitTimeProvider
};
// Act
configuration.PostConfigure(null, options);
// Assert
Assert.Same(explicitTimeProvider, options.TimeProvider);
}
[Fact]
public void Validate_ThrowsAnExceptionForNullOptions()
{
// Arrange
var services = new ServiceCollection();
var provider = services.BuildServiceProvider();
var configuration = new OpenIddictCoreConfiguration(provider);
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => configuration.Validate(null, null!));
Assert.Equal("options", exception.ParamName);
}
[Fact]
public void Validate_SucceedsForDefaultOptions()
{
// Arrange
var services = new ServiceCollection();
var provider = services.BuildServiceProvider();
var configuration = new OpenIddictCoreConfiguration(provider);
var options = new OpenIddictCoreOptions();
// Act
var result = configuration.Validate(null, options);
// Assert
Assert.True(result.Succeeded);
}
[Theory]
[InlineData(nameof(HashAlgorithmName.SHA1))]
[InlineData(nameof(HashAlgorithmName.SHA256))]
[InlineData(nameof(HashAlgorithmName.SHA512))]
public void Validate_SucceedsForValidHashAlgorithm(string algorithmName)
{
// Arrange
var services = new ServiceCollection();
var provider = services.BuildServiceProvider();
var configuration = new OpenIddictCoreConfiguration(provider);
var options = new OpenIddictCoreOptions
{
ClientSecretKeyDerivationHashAlgorithm = new HashAlgorithmName(algorithmName)
};
// Act
var result = configuration.Validate(null, options);
// Assert
Assert.True(result.Succeeded);
}
[Fact]
public void Validate_FailsForInvalidHashAlgorithm()
{
// Arrange
var services = new ServiceCollection();
var provider = services.BuildServiceProvider();
var configuration = new OpenIddictCoreConfiguration(provider);
var options = new OpenIddictCoreOptions
{
ClientSecretKeyDerivationHashAlgorithm = new HashAlgorithmName("MD5")
};
// Act
var result = configuration.Validate(null, options);
// Assert
Assert.True(result.Failed);
Assert.Contains(SR.FormatID0217("MD5"), result.Failures);
}
[Theory]
[InlineData(9_999)]
[InlineData(10_000_001)]
public void Validate_FailsForInvalidIterationCount(int iterations)
{
// Arrange
var services = new ServiceCollection();
var provider = services.BuildServiceProvider();
var configuration = new OpenIddictCoreConfiguration(provider);
var options = new OpenIddictCoreOptions
{
ClientSecretKeyDerivationIterations = iterations
};
// Act
var result = configuration.Validate(null, options);
// Assert
Assert.True(result.Failed);
Assert.Contains(SR.FormatID0518(10_000, 10_000_000), result.Failures);
}
[Theory]
[InlineData(10_000)]
[InlineData(100_000)]
[InlineData(10_000_000)]
public void Validate_SucceedsForValidIterationCount(int iterations)
{
// Arrange
var services = new ServiceCollection();
var provider = services.BuildServiceProvider();
var configuration = new OpenIddictCoreConfiguration(provider);
var options = new OpenIddictCoreOptions
{
ClientSecretKeyDerivationIterations = iterations
};
// Act
var result = configuration.Validate(null, options);
// Assert
Assert.True(result.Succeeded);
}
[Theory]
[InlineData(127)]
[InlineData(1025)]
public void Validate_FailsForInvalidSaltLength(int saltLength)
{
// Arrange
var services = new ServiceCollection();
var provider = services.BuildServiceProvider();
var configuration = new OpenIddictCoreConfiguration(provider);
var options = new OpenIddictCoreOptions
{
ClientSecretKeyDerivationSaltLength = saltLength
};
// Act
var result = configuration.Validate(null, options);
// Assert
Assert.True(result.Failed);
Assert.Contains(SR.FormatID0519(128, 1024), result.Failures);
}
[Theory]
[InlineData(128)]
[InlineData(256)]
[InlineData(1024)]
public void Validate_SucceedsForValidSaltLength(int saltLength)
{
// Arrange
var services = new ServiceCollection();
var provider = services.BuildServiceProvider();
var configuration = new OpenIddictCoreConfiguration(provider);
var options = new OpenIddictCoreOptions
{
ClientSecretKeyDerivationSaltLength = saltLength
};
// Act
var result = configuration.Validate(null, options);
// Assert
Assert.True(result.Succeeded);
}
[Theory]
[InlineData(255)]
[InlineData(2049)]
public void Validate_FailsForInvalidOutputLength(int outputLength)
{
// Arrange
var services = new ServiceCollection();
var provider = services.BuildServiceProvider();
var configuration = new OpenIddictCoreConfiguration(provider);
var options = new OpenIddictCoreOptions
{
ClientSecretKeyDerivationOutputLength = outputLength
};
// Act
var result = configuration.Validate(null, options);
// Assert
Assert.True(result.Failed);
Assert.Contains(SR.FormatID0520(256, 2048), result.Failures);
}
[Theory]
[InlineData(256)]
[InlineData(512)]
[InlineData(2048)]
public void Validate_SucceedsForValidOutputLength(int outputLength)
{
// Arrange
var services = new ServiceCollection();
var provider = services.BuildServiceProvider();
var configuration = new OpenIddictCoreConfiguration(provider);
var options = new OpenIddictCoreOptions
{
ClientSecretKeyDerivationOutputLength = outputLength
};
// Act
var result = configuration.Validate(null, options);
// Assert
Assert.True(result.Succeeded);
}
private class CustomTimeProvider : TimeProvider
{
}
}

7
test/OpenIddict.Core.Tests/OpenIddictCoreExtensionsTests.cs

@ -1,4 +1,4 @@
/* /*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) * 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 * See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project. * the license and the contributors participating to this project.
@ -173,9 +173,4 @@ public class OpenIddictCoreExtensionsTests
Assert.Equal(SR.GetResourceString(SR.ID0472), exception.Message); Assert.Equal(SR.GetResourceString(SR.ID0472), exception.Message);
} }
public class OpenIddictApplication { }
public class OpenIddictAuthorization { }
public class OpenIddictScope { }
public class OpenIddictToken { }
} }

152
test/OpenIddict.Core.Tests/OpenIddictCoreOptionsTests.cs

@ -0,0 +1,152 @@
/*
* 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 System.Security.Cryptography;
using Xunit;
namespace OpenIddict.Core.Tests;
public class OpenIddictCoreOptionsTests
{
[Fact]
public void Constructor_SetsDefaultValues()
{
// Act
var options = new OpenIddictCoreOptions();
// Assert
Assert.Equal(HashAlgorithmName.SHA512, options.ClientSecretKeyDerivationHashAlgorithm);
Assert.Equal(100_000, options.ClientSecretKeyDerivationIterations);
Assert.Equal(512, options.ClientSecretKeyDerivationOutputLength);
Assert.Equal(256, options.ClientSecretKeyDerivationSaltLength);
Assert.False(options.DisableAdditionalFiltering);
Assert.False(options.DisableAutomaticClientSecretRehashing);
Assert.False(options.DisableEntityCaching);
Assert.Equal(250, options.EntityCacheLimit);
}
[Fact]
public void ClientSecretKeyDerivationHashAlgorithm_CanBeSet()
{
// Arrange
var options = new OpenIddictCoreOptions();
// Act
options.ClientSecretKeyDerivationHashAlgorithm = HashAlgorithmName.SHA256;
// Assert
Assert.Equal(HashAlgorithmName.SHA256, options.ClientSecretKeyDerivationHashAlgorithm);
}
[Fact]
public void ClientSecretKeyDerivationIterations_CanBeSet()
{
// Arrange
var options = new OpenIddictCoreOptions();
// Act
options.ClientSecretKeyDerivationIterations = 50_000;
// Assert
Assert.Equal(50_000, options.ClientSecretKeyDerivationIterations);
}
[Fact]
public void ClientSecretKeyDerivationOutputLength_CanBeSet()
{
// Arrange
var options = new OpenIddictCoreOptions();
// Act
options.ClientSecretKeyDerivationOutputLength = 1024;
// Assert
Assert.Equal(1024, options.ClientSecretKeyDerivationOutputLength);
}
[Fact]
public void ClientSecretKeyDerivationSaltLength_CanBeSet()
{
// Arrange
var options = new OpenIddictCoreOptions();
// Act
options.ClientSecretKeyDerivationSaltLength = 512;
// Assert
Assert.Equal(512, options.ClientSecretKeyDerivationSaltLength);
}
[Fact]
public void DisableAdditionalFiltering_CanBeSet()
{
// Arrange
var options = new OpenIddictCoreOptions();
// Act
options.DisableAdditionalFiltering = true;
// Assert
Assert.True(options.DisableAdditionalFiltering);
}
[Fact]
public void DisableAutomaticClientSecretRehashing_CanBeSet()
{
// Arrange
var options = new OpenIddictCoreOptions();
// Act
options.DisableAutomaticClientSecretRehashing = true;
// Assert
Assert.True(options.DisableAutomaticClientSecretRehashing);
}
[Fact]
public void DisableEntityCaching_CanBeSet()
{
// Arrange
var options = new OpenIddictCoreOptions();
// Act
options.DisableEntityCaching = true;
// Assert
Assert.True(options.DisableEntityCaching);
}
[Fact]
public void EntityCacheLimit_CanBeSet()
{
// Arrange
var options = new OpenIddictCoreOptions();
// Act
options.EntityCacheLimit = 500;
// Assert
Assert.Equal(500, options.EntityCacheLimit);
}
[Fact]
public void TimeProvider_CanBeSet()
{
// Arrange
var options = new OpenIddictCoreOptions();
var customTimeProvider = new CustomTimeProvider();
// Act
options.TimeProvider = customTimeProvider;
// Assert
Assert.Same(customTimeProvider, options.TimeProvider);
}
private class CustomTimeProvider : TimeProvider
{
}
}

7
test/OpenIddict.Server.IntegrationTests/OpenIddict.Server.IntegrationTests.csproj

@ -21,17 +21,12 @@
<PackageReference Include="Moq" /> <PackageReference Include="Moq" />
</ItemGroup> </ItemGroup>
<ItemGroup
Condition=" ('$(TargetFrameworkIdentifier)' == '.NETFramework') Or
('$(TargetFrameworkIdentifier)' == '.NETCoreApp' And $([MSBuild]::VersionLessThan($(TargetFrameworkVersion), '10.0'))) ">
<PackageReference Include="System.Linq.Async" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFrameworkIdentifier)' == '.NETFramework' "> <ItemGroup Condition=" '$(TargetFrameworkIdentifier)' == '.NETFramework' ">
<Reference Include="System.Net.Http" /> <Reference Include="System.Net.Http" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition=" '$(TargetFrameworkIdentifier)' == '.NETFramework' "> <ItemGroup Condition=" '$(TargetFrameworkIdentifier)' == '.NETFramework' ">
<PackageReference Include="System.Linq.Async" />
<PackageReference Include="System.Net.Http.Json" /> <PackageReference Include="System.Net.Http.Json" />
</ItemGroup> </ItemGroup>

7
test/OpenIddict.Validation.IntegrationTests/OpenIddict.Validation.IntegrationTests.csproj

@ -20,17 +20,12 @@
<PackageReference Include="Moq" /> <PackageReference Include="Moq" />
</ItemGroup> </ItemGroup>
<ItemGroup
Condition=" ('$(TargetFrameworkIdentifier)' == '.NETFramework') Or
('$(TargetFrameworkIdentifier)' == '.NETCoreApp' And $([MSBuild]::VersionLessThan($(TargetFrameworkVersion), '10.0'))) ">
<PackageReference Include="System.Linq.Async" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFrameworkIdentifier)' == '.NETFramework' "> <ItemGroup Condition=" '$(TargetFrameworkIdentifier)' == '.NETFramework' ">
<Reference Include="System.Net.Http" /> <Reference Include="System.Net.Http" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition=" '$(TargetFrameworkIdentifier)' == '.NETFramework' "> <ItemGroup Condition=" '$(TargetFrameworkIdentifier)' == '.NETFramework' ">
<PackageReference Include="System.Linq.Async" />
<PackageReference Include="System.Net.Http.Json" /> <PackageReference Include="System.Net.Http.Json" />
</ItemGroup> </ItemGroup>

Loading…
Cancel
Save