Browse Source

Implement ML-DSA support in the client, core, server and validation stacks

pull/2489/head
Kévin Chalet 1 month ago
parent
commit
defa944d7b
  1. 15
      Directory.Packages.props
  2. 2
      gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs
  3. 6
      sandbox/OpenIddict.Sandbox.AspNet.Client/Web.config
  4. 6
      sandbox/OpenIddict.Sandbox.AspNet.Server/Web.config
  5. 8
      src/OpenIddict.Abstractions/OpenIddictResources.resx
  6. 1
      src/OpenIddict.Client/OpenIddict.Client.csproj
  7. 80
      src/OpenIddict.Client/OpenIddictClientBuilder.cs
  8. 50
      src/OpenIddict.Client/OpenIddictClientConfiguration.cs
  9. 33
      src/OpenIddict.Client/OpenIddictClientHandlers.Discovery.cs
  10. 47
      src/OpenIddict.Client/OpenIddictClientHandlers.cs
  11. 17
      src/OpenIddict.Client/OpenIddictClientRetriever.cs
  12. 9
      src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs
  13. 1
      src/OpenIddict.Server/OpenIddict.Server.csproj
  14. 80
      src/OpenIddict.Server/OpenIddictServerBuilder.cs
  15. 50
      src/OpenIddict.Server/OpenIddictServerConfiguration.cs
  16. 306
      src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs
  17. 28
      src/OpenIddict.Server/OpenIddictServerHandlers.cs
  18. 3
      src/OpenIddict.Validation/OpenIddictValidationBuilder.cs
  19. 33
      src/OpenIddict.Validation/OpenIddictValidationHandlers.Discovery.cs
  20. 17
      src/OpenIddict.Validation/OpenIddictValidationRetriever.cs
  21. 1
      test/OpenIddict.Client.Tests/OpenIddict.Client.Tests.csproj
  22. 1522
      test/OpenIddict.Client.Tests/OpenIddictClientBuilderTests.cs
  23. 471
      test/OpenIddict.Client.Tests/OpenIddictClientConfigurationTests.cs
  24. 1
      test/OpenIddict.Server.IntegrationTests/OpenIddict.Server.IntegrationTests.csproj
  25. 28
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Discovery.cs
  26. 1
      test/OpenIddict.Server.Tests/OpenIddict.Server.Tests.csproj
  27. 630
      test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs
  28. 555
      test/OpenIddict.Server.Tests/OpenIddictServerConfigurationTests.cs
  29. 1
      test/OpenIddict.Validation.Tests/OpenIddict.Validation.Tests.csproj
  30. 1126
      test/OpenIddict.Validation.Tests/OpenIddictValidationBuilderTests.cs
  31. 372
      test/OpenIddict.Validation.Tests/OpenIddictValidationConfigurationTests.cs

15
Directory.Packages.props

@ -21,6 +21,7 @@
<ItemGroup Label="Package versions for .NET Framework 4.8"
Condition=" '$(TargetFrameworkIdentifier)' == '.NETFramework' And $([MSBuild]::VersionEquals($(TargetFrameworkVersion), '4.8.0')) ">
<PackageVersion Include="EntityFramework" Version="6.5.2" />
<PackageVersion Include="BouncyCastle.Cryptography" Version="2.6.2" />
<PackageVersion Include="Microsoft.AspNetCore.DataProtection" Version="10.0.8" />
<PackageVersion Include="Microsoft.Bcl.Cryptography" Version="10.0.8" />
<PackageVersion Include="Microsoft.Bcl.HashCode" Version="6.0.0" />
@ -32,9 +33,9 @@
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.8" />
<PackageVersion Include="Microsoft.Extensions.Primitives" Version="10.0.8" />
<PackageVersion Include="Microsoft.Extensions.WebEncoders" Version="10.0.8" />
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.18.0" />
<PackageVersion Include="Microsoft.IdentityModel.Protocols" Version="8.18.0" />
<PackageVersion Include="Microsoft.IdentityModel.Tokens" Version="8.18.0" />
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.19.1" />
<PackageVersion Include="Microsoft.IdentityModel.Protocols" Version="8.19.1" />
<PackageVersion Include="Microsoft.IdentityModel.Tokens" Version="8.19.1" />
<PackageVersion Include="Microsoft.Owin.Security" Version="4.2.3" />
<PackageVersion Include="Microsoft.Windows.SDK.Contracts" Version="10.0.17763.1000" />
<PackageVersion Include="MongoDB.Bson" Version="3.8.1" />
@ -59,6 +60,7 @@
<PackageVersion Include="Microsoft.Owin.Testing" Version="4.2.3" />
<PackageVersion Include="Moq" Version="4.18.4" />
<PackageVersion Include="System.Linq.Async" Version="7.0.1" />
<PackageVersion Include="Xunit.SkippableFact" Version="1.5.61" />
<!--
Note: the following references are exclusively used in the samples:
@ -110,9 +112,9 @@
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.8" />
<PackageVersion Include="Microsoft.Extensions.Http.Polly" Version="10.0.8" />
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="10.6.0" />
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.18.0" />
<PackageVersion Include="Microsoft.IdentityModel.Protocols" Version="8.18.0" />
<PackageVersion Include="Microsoft.IdentityModel.Tokens" Version="8.18.0" />
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.19.1" />
<PackageVersion Include="Microsoft.IdentityModel.Protocols" Version="8.19.1" />
<PackageVersion Include="Microsoft.IdentityModel.Tokens" Version="8.19.1" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.8" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.8" />
<PackageVersion Include="Microsoft.Extensions.Primitives" Version="10.0.8" />
@ -132,6 +134,7 @@
<PackageVersion Include="Microsoft.AspNetCore.TestHost" Version="10.0.8" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.8" />
<PackageVersion Include="Moq" Version="4.18.4" />
<PackageVersion Include="Xunit.SkippableFact" Version="1.5.61" />
<!--
Note: the following references are exclusively used in the samples:

2
gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs

@ -50,6 +50,7 @@ using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
@ -659,6 +660,7 @@ public sealed partial class OpenIddictClientWebIntegrationBuilder
{{~ if setting.obsolete ~}}
[Obsolete(""This option is no longer supported and will be removed in a future version."")]
{{~ end ~}}
[UnsupportedOSPlatform(""linux"")]
public {{ provider.name }} Set{{ setting.property_name }}(string thumbprint)
{
ArgumentException.ThrowIfNullOrEmpty(thumbprint);

6
sandbox/OpenIddict.Sandbox.AspNet.Client/Web.config

@ -232,6 +232,12 @@
<bindingRedirect oldVersion="0.0.0.0-9.1.0.0" newVersion="9.1.0.0" />
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Microsoft.Bcl.Cryptography" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-10.0.0.8" newVersion="10.0.0.8" />
</dependentAssembly>
</assemblyBinding>
</runtime>
<system.codedom>
<compilers>

6
sandbox/OpenIddict.Sandbox.AspNet.Server/Web.config

@ -238,6 +238,12 @@
<bindingRedirect oldVersion="0.0.0.0-9.1.0.0" newVersion="9.1.0.0" />
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Microsoft.Bcl.Cryptography" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-10.0.0.8" newVersion="10.0.0.8" />
</dependentAssembly>
</assemblyBinding>
</runtime>
<entityFramework>
<providers>

8
src/OpenIddict.Abstractions/OpenIddictResources.resx

@ -353,9 +353,6 @@ Consider using 'options.AddEncryptionCredentials(EncryptingCredentials)' instead
<value>A signature algorithm cannot be automatically inferred from the signing key.
Consider using 'options.AddSigningCredentials(SigningCredentials)' instead.</value>
</data>
<data name="ID0069" xml:space="preserve">
<value>ECDSA signing keys are not supported on this platform.</value>
</data>
<data name="ID0070" xml:space="preserve">
<value>The specified certificate is not a signing certificate.</value>
</data>
@ -2721,7 +2718,7 @@ The principal used to create the token contained the following claims: {Claims}.
<value>A JSON Web Key was excluded from the key set because it didn't contain the mandatory '{Parameter}' parameter.</value>
</data>
<data name="ID6071" xml:space="preserve">
<value>An unsupported signing key of type '{Type}' was ignored and excluded from the key set. Only RSA and ECDSA asymmetric security keys can be exposed via the JSON Web Key Set endpoint.</value>
<value>An unsupported signing key of type '{Type}' was ignored and excluded from the key set. Only RSA, ECDSA and ML-DSA asymmetric security keys can be exposed via the JSON Web Key Set endpoint.</value>
</data>
<data name="ID6072" xml:space="preserve">
<value>An unsupported signing key of type '{Type}' was ignored and excluded from the key set. Only RSA asymmetric security keys can be exposed via the JSON Web Key Set endpoint.</value>
@ -3348,6 +3345,9 @@ This may indicate that the hashed entry is corrupted or malformed.</value>
<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="ID6296" xml:space="preserve">
<value>A signing key of type '{Type}' was ignored because its ML-DSA public key couldn't be extracted.</value>
</data>
<data name="ID8000" xml:space="preserve">
<value>https://documentation.openiddict.com/errors/{0}</value>
</data>

1
src/OpenIddict.Client/OpenIddict.Client.csproj

@ -24,6 +24,7 @@ To use the client feature on ASP.NET Core or OWIN/Katana, reference the OpenIddi
</ItemGroup>
<ItemGroup Condition=" '$(TargetFrameworkIdentifier)' == '.NETFramework' ">
<PackageReference Include="BouncyCastle.Cryptography" />
<PackageReference Include="Microsoft.Bcl.Cryptography" />
</ItemGroup>

80
src/OpenIddict.Client/OpenIddictClientBuilder.cs

@ -7,6 +7,7 @@
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Runtime.Versioning;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
@ -154,7 +155,7 @@ public sealed class OpenIddictClientBuilder
if (key.IsSupportedAlgorithm(SecurityAlgorithms.Aes256KW))
{
if (key.KeySize != 256)
if (key.KeySize is not 256)
{
throw new InvalidOperationException(SR.FormatID0283(256, key.KeySize));
}
@ -414,6 +415,7 @@ public sealed class OpenIddictClientBuilder
/// </summary>
/// <param name="thumbprint">The thumbprint of the certificate used to identify it in the X.509 store.</param>
/// <returns>The <see cref="OpenIddictClientBuilder"/> instance.</returns>
[UnsupportedOSPlatform("linux")]
public OpenIddictClientBuilder AddEncryptionCertificate(string thumbprint)
{
ArgumentException.ThrowIfNullOrEmpty(thumbprint);
@ -493,9 +495,20 @@ public sealed class OpenIddictClientBuilder
throw new InvalidOperationException(SR.GetResourceString(SR.ID0067));
}
if (key.IsSupportedAlgorithm(SecurityAlgorithms.RsaSha256))
// Note: ECDSA algorithms are bound to specific curves and must be treated separately.
if (key.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha256))
{
return AddSigningCredentials(new SigningCredentials(key, SecurityAlgorithms.RsaSha256));
return AddSigningCredentials(new SigningCredentials(key, SecurityAlgorithms.EcdsaSha256));
}
if (key.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha384))
{
return AddSigningCredentials(new SigningCredentials(key, SecurityAlgorithms.EcdsaSha384));
}
if (key.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha512))
{
return AddSigningCredentials(new SigningCredentials(key, SecurityAlgorithms.EcdsaSha512));
}
if (key.IsSupportedAlgorithm(SecurityAlgorithms.HmacSha256))
@ -503,20 +516,24 @@ public sealed class OpenIddictClientBuilder
return AddSigningCredentials(new SigningCredentials(key, SecurityAlgorithms.HmacSha256));
}
// Note: ECDSA algorithms are bound to specific curves and must be treated separately.
if (key.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha256))
if (key.IsSupportedAlgorithm(SecurityAlgorithms.MlDsa44))
{
return AddSigningCredentials(new SigningCredentials(key, SecurityAlgorithms.EcdsaSha256));
return AddSigningCredentials(new SigningCredentials(key, SecurityAlgorithms.MlDsa44));
}
if (key.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha384))
if (key.IsSupportedAlgorithm(SecurityAlgorithms.MlDsa65))
{
return AddSigningCredentials(new SigningCredentials(key, SecurityAlgorithms.EcdsaSha384));
return AddSigningCredentials(new SigningCredentials(key, SecurityAlgorithms.MlDsa65));
}
if (key.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha512))
if (key.IsSupportedAlgorithm(SecurityAlgorithms.MlDsa87))
{
return AddSigningCredentials(new SigningCredentials(key, SecurityAlgorithms.EcdsaSha512));
return AddSigningCredentials(new SigningCredentials(key, SecurityAlgorithms.MlDsa87));
}
if (key.IsSupportedAlgorithm(SecurityAlgorithms.RsaSha256))
{
return AddSigningCredentials(new SigningCredentials(key, SecurityAlgorithms.RsaSha256));
}
throw new InvalidOperationException(SR.GetResourceString(SR.ID0068));
@ -644,21 +661,6 @@ public sealed class OpenIddictClientBuilder
return algorithm switch
{
SecurityAlgorithms.RsaSha256 or
SecurityAlgorithms.RsaSha384 or
SecurityAlgorithms.RsaSha512 or
SecurityAlgorithms.RsaSha256Signature or
SecurityAlgorithms.RsaSha384Signature or
SecurityAlgorithms.RsaSha512Signature or
SecurityAlgorithms.RsaSsaPssSha256 or
SecurityAlgorithms.RsaSsaPssSha384 or
SecurityAlgorithms.RsaSsaPssSha512 or
SecurityAlgorithms.RsaSsaPssSha256Signature or
SecurityAlgorithms.RsaSsaPssSha384Signature or
SecurityAlgorithms.RsaSsaPssSha512Signature
=> AddSigningCredentials(new SigningCredentials(new RsaSecurityKey(
RSA.Create(keySizeInBits: 4096)), algorithm)),
SecurityAlgorithms.EcdsaSha256 or
SecurityAlgorithms.EcdsaSha256Signature
=> AddSigningCredentials(new SigningCredentials(new ECDsaSecurityKey(
@ -674,6 +676,33 @@ public sealed class OpenIddictClientBuilder
=> AddSigningCredentials(new SigningCredentials(new ECDsaSecurityKey(
ECDsa.Create(ECCurve.NamedCurves.nistP521)), algorithm)),
SecurityAlgorithms.MlDsa44
=> AddSigningCredentials(new SigningCredentials(new MlDsaSecurityKey(
MLDsa.GenerateKey(MLDsaAlgorithm.MLDsa44)), algorithm)),
SecurityAlgorithms.MlDsa65
=> AddSigningCredentials(new SigningCredentials(new MlDsaSecurityKey(
MLDsa.GenerateKey(MLDsaAlgorithm.MLDsa65)), algorithm)),
SecurityAlgorithms.MlDsa87
=> AddSigningCredentials(new SigningCredentials(new MlDsaSecurityKey(
MLDsa.GenerateKey(MLDsaAlgorithm.MLDsa87)), algorithm)),
SecurityAlgorithms.RsaSha256 or
SecurityAlgorithms.RsaSha384 or
SecurityAlgorithms.RsaSha512 or
SecurityAlgorithms.RsaSha256Signature or
SecurityAlgorithms.RsaSha384Signature or
SecurityAlgorithms.RsaSha512Signature or
SecurityAlgorithms.RsaSsaPssSha256 or
SecurityAlgorithms.RsaSsaPssSha384 or
SecurityAlgorithms.RsaSsaPssSha512 or
SecurityAlgorithms.RsaSsaPssSha256Signature or
SecurityAlgorithms.RsaSsaPssSha384Signature or
SecurityAlgorithms.RsaSsaPssSha512Signature
=> AddSigningCredentials(new SigningCredentials(new RsaSecurityKey(
RSA.Create(keySizeInBits: 4096)), algorithm)),
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0058))
};
}
@ -782,6 +811,7 @@ public sealed class OpenIddictClientBuilder
/// </summary>
/// <param name="thumbprint">The thumbprint of the certificate used to identify it in the X.509 store.</param>
/// <returns>The <see cref="OpenIddictClientBuilder"/> instance.</returns>
[UnsupportedOSPlatform("linux")]
public OpenIddictClientBuilder AddSigningCertificate(string thumbprint)
{
ArgumentException.ThrowIfNullOrEmpty(thumbprint);

50
src/OpenIddict.Client/OpenIddictClientConfiguration.cs

@ -8,6 +8,7 @@ using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
@ -160,41 +161,50 @@ public sealed class OpenIddictClientConfiguration : IPostConfigureOptions<OpenId
// inferred from the hexadecimal representation of the certificate thumbprint (SHA-1)
// when the key is bound to a X.509 certificate or from the public part of the signing key.
if (key is X509SecurityKey x509SecurityKey)
return key switch
{
return x509SecurityKey.Certificate.Thumbprint;
X509SecurityKey value => value.Certificate.Thumbprint,
RsaSecurityKey value => GetRsaSecurityKeyIdentifier(value),
ECDsaSecurityKey value => GetEcdsaSecurityKeyIdentifier(value),
MlDsaSecurityKey value => GetMLDsaSecurityKeyIdentifier(value),
_ => null
};
static string GetEcdsaSecurityKeyIdentifier(ECDsaSecurityKey key)
{
var parameters = key.ECDsa.ExportParameters(includePrivateParameters: false);
Debug.Assert(parameters.Q.X is not null, SR.GetResourceString(SR.ID4004));
// Only use the 40 first chars of the base64url-encoded X coordinate.
var identifier = Base64UrlEncoder.Encode(parameters.Q.X);
return identifier[.. Math.Min(identifier.Length, 40)].ToUpperInvariant();
}
static string GetMLDsaSecurityKeyIdentifier(MlDsaSecurityKey key)
{
// Only use the 40 first chars of the base64url-encoded SHA256 of the ML-DSA public key.
var identifier = Base64UrlEncoder.Encode(SHA256.HashData(key.MLDsa.ExportMLDsaPublicKey()));
return identifier[.. Math.Min(identifier.Length, 40)].ToUpperInvariant();
}
if (key is RsaSecurityKey rsaSecurityKey)
static string GetRsaSecurityKeyIdentifier(RsaSecurityKey key)
{
// Note: if the RSA parameters are not attached to the signing key,
// extract them by calling ExportParameters on the RSA instance.
var parameters = rsaSecurityKey.Parameters;
var parameters = key.Parameters;
if (parameters.Modulus is null)
{
parameters = rsaSecurityKey.Rsa.ExportParameters(includePrivateParameters: false);
parameters = key.Rsa.ExportParameters(includePrivateParameters: false);
Debug.Assert(parameters.Modulus is not null, SR.GetResourceString(SR.ID4003));
}
// Only use the 40 first chars of the base64url-encoded modulus.
var identifier = Base64UrlEncoder.Encode(parameters.Modulus);
return identifier[..Math.Min(identifier.Length, 40)].ToUpperInvariant();
}
if (key is ECDsaSecurityKey ecsdaSecurityKey)
{
// Extract the ECDSA parameters from the signing credentials.
var parameters = ecsdaSecurityKey.ECDsa.ExportParameters(includePrivateParameters: false);
Debug.Assert(parameters.Q.X is not null, SR.GetResourceString(SR.ID4004));
// Only use the 40 first chars of the base64url-encoded X coordinate.
var identifier = Base64UrlEncoder.Encode(parameters.Q.X);
return identifier[..Math.Min(identifier.Length, 40)].ToUpperInvariant();
return identifier[.. Math.Min(identifier.Length, 40)].ToUpperInvariant();
}
return null;
}
static string ComputeDefaultRegistrationId(OpenIddictClientRegistration registration)

33
src/OpenIddict.Client/OpenIddictClientHandlers.Discovery.cs

@ -1404,11 +1404,11 @@ public static partial class OpenIddictClientHandlers
var key = (string?) keys[index][JsonWebKeyParameterNames.Kty] switch
{
JsonWebAlgorithmsKeyTypes.RSA => new JsonWebKey
JsonWebAlgorithmsKeyTypes.Akp => new JsonWebKey
{
Kty = JsonWebAlgorithmsKeyTypes.RSA,
E = (string?) keys[index][JsonWebKeyParameterNames.E],
N = (string?) keys[index][JsonWebKeyParameterNames.N]
Kty = JsonWebAlgorithmsKeyTypes.Akp,
Alg = (string?) keys[index][JsonWebKeyParameterNames.Alg],
Pub = (string?) keys[index][JsonWebKeyParameterNames.Pub]
},
JsonWebAlgorithmsKeyTypes.EllipticCurve => new JsonWebKey
@ -1419,6 +1419,13 @@ public static partial class OpenIddictClientHandlers
Y = (string?) keys[index][JsonWebKeyParameterNames.Y]
},
JsonWebAlgorithmsKeyTypes.RSA => new JsonWebKey
{
Kty = JsonWebAlgorithmsKeyTypes.RSA,
E = (string?) keys[index][JsonWebKeyParameterNames.E],
N = (string?) keys[index][JsonWebKeyParameterNames.N]
},
_ => null
};
@ -1432,9 +1439,9 @@ public static partial class OpenIddictClientHandlers
return ValueTask.CompletedTask;
}
// If the key is a RSA key, ensure the mandatory parameters are all present.
if (string.Equals(key.Kty, JsonWebAlgorithmsKeyTypes.RSA, StringComparison.Ordinal) &&
(string.IsNullOrEmpty(key.E) || string.IsNullOrEmpty(key.N)))
// If the key is an AKP key, ensure the mandatory parameters are all present.
if (string.Equals(key.Kty, JsonWebAlgorithmsKeyTypes.Akp, StringComparison.Ordinal) &&
(string.IsNullOrEmpty(key.Alg) || string.IsNullOrEmpty(key.Pub)))
{
context.Reject(
error: Errors.ServerError,
@ -1456,6 +1463,18 @@ public static partial class OpenIddictClientHandlers
return ValueTask.CompletedTask;
}
// If the key is a RSA key, ensure the mandatory parameters are all present.
if (string.Equals(key.Kty, JsonWebAlgorithmsKeyTypes.RSA, StringComparison.Ordinal) &&
(string.IsNullOrEmpty(key.E) || string.IsNullOrEmpty(key.N)))
{
context.Reject(
error: Errors.ServerError,
description: SR.GetResourceString(SR.ID2104),
uri: SR.FormatID8000(SR.ID2104));
return ValueTask.CompletedTask;
}
key.KeyId = (string?) keys[index][JsonWebKeyParameterNames.Kid];
key.X5t = (string?) keys[index][JsonWebKeyParameterNames.X5t];
key.X5tS256 = (string?) keys[index][JsonWebKeyParameterNames.X5tS256];

47
src/OpenIddict.Client/OpenIddictClientHandlers.cs

@ -18,6 +18,11 @@ using Microsoft.Extensions.Primitives;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
using static OpenIddict.Abstractions.OpenIddictExceptions;
using System.Runtime.CompilerServices;
#if !NET
using Org.BouncyCastle.Crypto.Digests;
#endif
namespace OpenIddict.Client;
@ -2057,6 +2062,11 @@ public static partial class OpenIddictClientHandlers
SecurityAlgorithms.RsaSha512 or SecurityAlgorithms.RsaSsaPssSha512
=> SHA512.HashData(Encoding.ASCII.GetBytes(token)),
// Note: while not officially adopted yet, the OpenID Connect Working Group has proposed to use SHAKE256
// for ML-DSA-based algorithms. See https://bitbucket.org/openid/connect/issues/1125 for more information.
SecurityAlgorithms.MlDsa44 or SecurityAlgorithms.MlDsa65 or SecurityAlgorithms.MlDsa87
=> GetShake256Digest(Encoding.ASCII.GetBytes(token), length: 64),
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0293))
};
@ -2065,6 +2075,22 @@ public static partial class OpenIddictClientHandlers
return Base64UrlEncoder.Encode(hash, 0, hash.Length / 2).AsSpan();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static byte[] GetShake256Digest(byte[] data, int length)
{
#if NET
return Shake256.HashData(data, length);
#else
var digest = new ShakeDigest(256);
digest.BlockUpdate(data, 0, data.Length);
var hash = new byte[length];
digest.DoFinal(hash, 0);
return hash;
#endif
}
static bool ValidateTokenHash(string algorithm, string token, string hash) =>
CryptographicOperations.FixedTimeEquals(
left: MemoryMarshal.AsBytes(hash.AsSpan()),
@ -3736,6 +3762,11 @@ public static partial class OpenIddictClientHandlers
SecurityAlgorithms.RsaSha512 or SecurityAlgorithms.RsaSsaPssSha512
=> SHA512.HashData(Encoding.ASCII.GetBytes(token)),
// Note: while not officially adopted yet, the OpenID Connect Working Group has proposed to use SHAKE256
// for ML-DSA-based algorithms. See https://bitbucket.org/openid/connect/issues/1125 for more information.
SecurityAlgorithms.MlDsa44 or SecurityAlgorithms.MlDsa65 or SecurityAlgorithms.MlDsa87
=> GetShake256Digest(Encoding.ASCII.GetBytes(token), length: 64),
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0295))
};
@ -3744,6 +3775,22 @@ public static partial class OpenIddictClientHandlers
return Base64UrlEncoder.Encode(hash, 0, hash.Length / 2).AsSpan();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static byte[] GetShake256Digest(byte[] data, int length)
{
#if NET
return Shake256.HashData(data, length);
#else
var digest = new ShakeDigest(256);
digest.BlockUpdate(data, 0, data.Length);
var hash = new byte[length];
digest.DoFinal(hash, 0);
return hash;
#endif
}
static bool ValidateTokenHash(string algorithm, string token, string hash) =>
CryptographicOperations.FixedTimeEquals(
left: MemoryMarshal.AsBytes(hash.AsSpan()),

17
src/OpenIddict.Client/OpenIddictClientRetriever.cs

@ -7,6 +7,7 @@
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Tokens;
namespace OpenIddict.Client;
@ -64,6 +65,22 @@ public sealed class OpenIddictClientRetriever : IConfigurationRetriever<OpenIddi
configuration.SigningKeys.Add(key);
}
// Note: IdentityModel doesn't currently return AKP keys when calling GetSigningKeys(), so a
// second pass is made to ensure that all AKP keys are added to the signing keys collection.
//
// For more information, see
// https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/3534.
for (var index = 0; index < configuration.JsonWebKeySet.Keys.Count; index++)
{
if (configuration.JsonWebKeySet.Keys[index] is {
Kty: JsonWebAlgorithmsKeyTypes.Akp,
Alg: SecurityAlgorithms.MlDsa44 or SecurityAlgorithms.MlDsa65 or SecurityAlgorithms.MlDsa87 } &&
JsonWebKeyConverter.TryConvertToSecurityKey(configuration.JsonWebKeySet.Keys[index], out SecurityKey? key))
{
configuration.SigningKeys.Add(key);
}
}
return configuration;
}
}

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

@ -159,8 +159,9 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
throw new ArgumentException(SR.GetResourceString(SR.ID0206), nameof(application));
}
// If no client type was specified, assume it's a confidential application if a secret was
// provided or a JSON Web Key Set was attached and contains at least one RSA/ECDSA signing key.
// If no client type was specified, assume it's a confidential application if a
// secret was provided or a JSON Web Key Set was attached and contains at least
// one AKP (typically used with the ML-DSA algorithm), ECDSA or RSA signing key.
var type = await Store.GetClientTypeAsync(application, cancellationToken);
if (string.IsNullOrEmpty(type))
{
@ -173,7 +174,7 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
{
var set = await Store.GetJsonWebKeySetAsync(application, cancellationToken);
if (set is not null && set.Keys.Any(static key =>
key.Kty is JsonWebAlgorithmsKeyTypes.EllipticCurve or JsonWebAlgorithmsKeyTypes.RSA &&
key.Kty is JsonWebAlgorithmsKeyTypes.Akp or JsonWebAlgorithmsKeyTypes.EllipticCurve or JsonWebAlgorithmsKeyTypes.RSA &&
key.Use is JsonWebKeyUseNames.Sig or null))
{
await Store.SetClientTypeAsync(application, ClientTypes.Confidential, cancellationToken);
@ -1304,7 +1305,7 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
{
var set = await Store.GetJsonWebKeySetAsync(application, cancellationToken);
if (set?.Keys is null || !set.Keys.Any(static key =>
key.Kty is JsonWebAlgorithmsKeyTypes.EllipticCurve or JsonWebAlgorithmsKeyTypes.RSA &&
key.Kty is JsonWebAlgorithmsKeyTypes.Akp or JsonWebAlgorithmsKeyTypes.EllipticCurve or JsonWebAlgorithmsKeyTypes.RSA &&
key.Use is JsonWebKeyUseNames.Sig or null))
{
yield return new ValidationResult(SR.GetResourceString(SR.ID2113));

1
src/OpenIddict.Server/OpenIddict.Server.csproj

@ -22,6 +22,7 @@ To use the server feature on ASP.NET Core or OWIN/Katana, reference the OpenIddi
</ItemGroup>
<ItemGroup Condition=" '$(TargetFrameworkIdentifier)' == '.NETFramework' ">
<PackageReference Include="BouncyCastle.Cryptography" />
<PackageReference Include="Microsoft.Bcl.Cryptography" />
</ItemGroup>

80
src/OpenIddict.Server/OpenIddictServerBuilder.cs

@ -8,6 +8,7 @@ using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Reflection;
using System.Runtime.Versioning;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.DependencyInjection.Extensions;
@ -165,7 +166,7 @@ public sealed class OpenIddictServerBuilder
if (key.IsSupportedAlgorithm(SecurityAlgorithms.Aes256KW))
{
if (key.KeySize != 256)
if (key.KeySize is not 256)
{
throw new InvalidOperationException(SR.FormatID0283(256, key.KeySize));
}
@ -425,6 +426,7 @@ public sealed class OpenIddictServerBuilder
/// </summary>
/// <param name="thumbprint">The thumbprint of the certificate used to identify it in the X.509 store.</param>
/// <returns>The <see cref="OpenIddictServerBuilder"/> instance.</returns>
[UnsupportedOSPlatform("linux")]
public OpenIddictServerBuilder AddEncryptionCertificate(string thumbprint)
{
ArgumentException.ThrowIfNullOrEmpty(thumbprint);
@ -504,9 +506,20 @@ public sealed class OpenIddictServerBuilder
throw new InvalidOperationException(SR.GetResourceString(SR.ID0067));
}
if (key.IsSupportedAlgorithm(SecurityAlgorithms.RsaSha256))
// Note: ECDSA algorithms are bound to specific curves and must be treated separately.
if (key.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha256))
{
return AddSigningCredentials(new SigningCredentials(key, SecurityAlgorithms.RsaSha256));
return AddSigningCredentials(new SigningCredentials(key, SecurityAlgorithms.EcdsaSha256));
}
if (key.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha384))
{
return AddSigningCredentials(new SigningCredentials(key, SecurityAlgorithms.EcdsaSha384));
}
if (key.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha512))
{
return AddSigningCredentials(new SigningCredentials(key, SecurityAlgorithms.EcdsaSha512));
}
if (key.IsSupportedAlgorithm(SecurityAlgorithms.HmacSha256))
@ -514,20 +527,24 @@ public sealed class OpenIddictServerBuilder
return AddSigningCredentials(new SigningCredentials(key, SecurityAlgorithms.HmacSha256));
}
// Note: ECDSA algorithms are bound to specific curves and must be treated separately.
if (key.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha256))
if (key.IsSupportedAlgorithm(SecurityAlgorithms.MlDsa44))
{
return AddSigningCredentials(new SigningCredentials(key, SecurityAlgorithms.EcdsaSha256));
return AddSigningCredentials(new SigningCredentials(key, SecurityAlgorithms.MlDsa44));
}
if (key.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha384))
if (key.IsSupportedAlgorithm(SecurityAlgorithms.MlDsa65))
{
return AddSigningCredentials(new SigningCredentials(key, SecurityAlgorithms.EcdsaSha384));
return AddSigningCredentials(new SigningCredentials(key, SecurityAlgorithms.MlDsa65));
}
if (key.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha512))
if (key.IsSupportedAlgorithm(SecurityAlgorithms.MlDsa87))
{
return AddSigningCredentials(new SigningCredentials(key, SecurityAlgorithms.EcdsaSha512));
return AddSigningCredentials(new SigningCredentials(key, SecurityAlgorithms.MlDsa87));
}
if (key.IsSupportedAlgorithm(SecurityAlgorithms.RsaSha256))
{
return AddSigningCredentials(new SigningCredentials(key, SecurityAlgorithms.RsaSha256));
}
throw new InvalidOperationException(SR.GetResourceString(SR.ID0068));
@ -655,21 +672,6 @@ public sealed class OpenIddictServerBuilder
return algorithm switch
{
SecurityAlgorithms.RsaSha256 or
SecurityAlgorithms.RsaSha384 or
SecurityAlgorithms.RsaSha512 or
SecurityAlgorithms.RsaSha256Signature or
SecurityAlgorithms.RsaSha384Signature or
SecurityAlgorithms.RsaSha512Signature or
SecurityAlgorithms.RsaSsaPssSha256 or
SecurityAlgorithms.RsaSsaPssSha384 or
SecurityAlgorithms.RsaSsaPssSha512 or
SecurityAlgorithms.RsaSsaPssSha256Signature or
SecurityAlgorithms.RsaSsaPssSha384Signature or
SecurityAlgorithms.RsaSsaPssSha512Signature
=> AddSigningCredentials(new SigningCredentials(new RsaSecurityKey(
RSA.Create(keySizeInBits: 4096)), algorithm)),
SecurityAlgorithms.EcdsaSha256 or
SecurityAlgorithms.EcdsaSha256Signature
=> AddSigningCredentials(new SigningCredentials(new ECDsaSecurityKey(
@ -685,6 +687,33 @@ public sealed class OpenIddictServerBuilder
=> AddSigningCredentials(new SigningCredentials(new ECDsaSecurityKey(
ECDsa.Create(ECCurve.NamedCurves.nistP521)), algorithm)),
SecurityAlgorithms.MlDsa44
=> AddSigningCredentials(new SigningCredentials(new MlDsaSecurityKey(
MLDsa.GenerateKey(MLDsaAlgorithm.MLDsa44)), algorithm)),
SecurityAlgorithms.MlDsa65
=> AddSigningCredentials(new SigningCredentials(new MlDsaSecurityKey(
MLDsa.GenerateKey(MLDsaAlgorithm.MLDsa65)), algorithm)),
SecurityAlgorithms.MlDsa87
=> AddSigningCredentials(new SigningCredentials(new MlDsaSecurityKey(
MLDsa.GenerateKey(MLDsaAlgorithm.MLDsa87)), algorithm)),
SecurityAlgorithms.RsaSha256 or
SecurityAlgorithms.RsaSha384 or
SecurityAlgorithms.RsaSha512 or
SecurityAlgorithms.RsaSha256Signature or
SecurityAlgorithms.RsaSha384Signature or
SecurityAlgorithms.RsaSha512Signature or
SecurityAlgorithms.RsaSsaPssSha256 or
SecurityAlgorithms.RsaSsaPssSha384 or
SecurityAlgorithms.RsaSsaPssSha512 or
SecurityAlgorithms.RsaSsaPssSha256Signature or
SecurityAlgorithms.RsaSsaPssSha384Signature or
SecurityAlgorithms.RsaSsaPssSha512Signature
=> AddSigningCredentials(new SigningCredentials(new RsaSecurityKey(
RSA.Create(keySizeInBits: 4096)), algorithm)),
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0058))
};
}
@ -793,6 +822,7 @@ public sealed class OpenIddictServerBuilder
/// </summary>
/// <param name="thumbprint">The thumbprint of the certificate used to identify it in the X.509 store.</param>
/// <returns>The <see cref="OpenIddictServerBuilder"/> instance.</returns>
[UnsupportedOSPlatform("linux")]
public OpenIddictServerBuilder AddSigningCertificate(string thumbprint)
{
ArgumentException.ThrowIfNullOrEmpty(thumbprint);

50
src/OpenIddict.Server/OpenIddictServerConfiguration.cs

@ -7,6 +7,7 @@
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
@ -123,41 +124,50 @@ public sealed class OpenIddictServerConfiguration : IPostConfigureOptions<OpenId
// inferred from the hexadecimal representation of the certificate thumbprint (SHA-1)
// when the key is bound to a X.509 certificate or from the public part of the signing key.
if (key is X509SecurityKey x509SecurityKey)
return key switch
{
return x509SecurityKey.Certificate.Thumbprint;
X509SecurityKey value => value.Certificate.Thumbprint,
RsaSecurityKey value => GetRsaSecurityKeyIdentifier(value),
ECDsaSecurityKey value => GetEcdsaSecurityKeyIdentifier(value),
MlDsaSecurityKey value => GetMLDsaSecurityKeyIdentifier(value),
_ => null
};
static string GetEcdsaSecurityKeyIdentifier(ECDsaSecurityKey key)
{
var parameters = key.ECDsa.ExportParameters(includePrivateParameters: false);
Debug.Assert(parameters.Q.X is not null, SR.GetResourceString(SR.ID4004));
// Only use the 40 first chars of the base64url-encoded X coordinate.
var identifier = Base64UrlEncoder.Encode(parameters.Q.X);
return identifier[.. Math.Min(identifier.Length, 40)].ToUpperInvariant();
}
static string GetMLDsaSecurityKeyIdentifier(MlDsaSecurityKey key)
{
// Only use the 40 first chars of the base64url-encoded SHA256 of the ML-DSA public key.
var identifier = Base64UrlEncoder.Encode(SHA256.HashData(key.MLDsa.ExportMLDsaPublicKey()));
return identifier[.. Math.Min(identifier.Length, 40)].ToUpperInvariant();
}
if (key is RsaSecurityKey rsaSecurityKey)
static string GetRsaSecurityKeyIdentifier(RsaSecurityKey key)
{
// Note: if the RSA parameters are not attached to the signing key,
// extract them by calling ExportParameters on the RSA instance.
var parameters = rsaSecurityKey.Parameters;
var parameters = key.Parameters;
if (parameters.Modulus is null)
{
parameters = rsaSecurityKey.Rsa.ExportParameters(includePrivateParameters: false);
parameters = key.Rsa.ExportParameters(includePrivateParameters: false);
Debug.Assert(parameters.Modulus is not null, SR.GetResourceString(SR.ID4003));
}
// Only use the 40 first chars of the base64url-encoded modulus.
var identifier = Base64UrlEncoder.Encode(parameters.Modulus);
return identifier[..Math.Min(identifier.Length, 40)].ToUpperInvariant();
}
if (key is ECDsaSecurityKey ecsdaSecurityKey)
{
// Extract the ECDSA parameters from the signing credentials.
var parameters = ecsdaSecurityKey.ECDsa.ExportParameters(includePrivateParameters: false);
Debug.Assert(parameters.Q.X is not null, SR.GetResourceString(SR.ID4004));
// Only use the 40 first chars of the base64url-encoded X coordinate.
var identifier = Base64UrlEncoder.Encode(parameters.Q.X);
return identifier[..Math.Min(identifier.Length, 40)].ToUpperInvariant();
return identifier[.. Math.Min(identifier.Length, 40)].ToUpperInvariant();
}
return null;
}
}

306
src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs

@ -6,9 +6,9 @@
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
@ -770,6 +770,10 @@ public static partial class OpenIddictServerHandlers
SecurityAlgorithms.EcdsaSha512 or SecurityAlgorithms.EcdsaSha512Signature
=> SecurityAlgorithms.EcdsaSha512,
SecurityAlgorithms.MlDsa44 => SecurityAlgorithms.MlDsa44,
SecurityAlgorithms.MlDsa65 => SecurityAlgorithms.MlDsa65,
SecurityAlgorithms.MlDsa87 => SecurityAlgorithms.MlDsa87,
SecurityAlgorithms.RsaSha256 or SecurityAlgorithms.RsaSha256Signature
=> SecurityAlgorithms.RsaSha256,
SecurityAlgorithms.RsaSha384 or SecurityAlgorithms.RsaSha384Signature
@ -1025,76 +1029,102 @@ public static partial class OpenIddictServerHandlers
return;
}
using var stream = new MemoryStream();
using var writer = new Utf8JsonWriter(stream);
writer.WriteStartArray();
List<JsonObject> keys = [];
foreach (var key in notification.Keys)
for (var index = 0; index < notification.Keys.Count; index++)
{
// Ensure a key type has been provided.
// See https://tools.ietf.org/html/rfc7517#section-4.1
if (string.IsNullOrEmpty(key.Kty))
if (string.IsNullOrEmpty(notification.Keys[index].Kty))
{
context.Logger.LogWarning(6070, SR.GetResourceString(SR.ID6070), JsonWebKeyParameterNames.Kty);
continue;
}
writer.WriteStartObject();
if (!string.IsNullOrEmpty(key.Kid)) writer.WriteString(JsonWebKeyParameterNames.Kid, key.Kid);
if (!string.IsNullOrEmpty(key.Use)) writer.WriteString(JsonWebKeyParameterNames.Use, key.Use);
if (!string.IsNullOrEmpty(key.Kty)) writer.WriteString(JsonWebKeyParameterNames.Kty, key.Kty);
if (!string.IsNullOrEmpty(key.Alg)) writer.WriteString(JsonWebKeyParameterNames.Alg, key.Alg);
if (!string.IsNullOrEmpty(key.Crv)) writer.WriteString(JsonWebKeyParameterNames.Crv, key.Crv);
if (!string.IsNullOrEmpty(key.E)) writer.WriteString(JsonWebKeyParameterNames.E, key.E);
if (!string.IsNullOrEmpty(key.N)) writer.WriteString(JsonWebKeyParameterNames.N, key.N);
if (!string.IsNullOrEmpty(key.X)) writer.WriteString(JsonWebKeyParameterNames.X, key.X);
if (!string.IsNullOrEmpty(key.Y)) writer.WriteString(JsonWebKeyParameterNames.Y, key.Y);
if (!string.IsNullOrEmpty(key.X5t)) writer.WriteString(JsonWebKeyParameterNames.X5t, key.X5t);
if (!string.IsNullOrEmpty(key.X5u)) writer.WriteString(JsonWebKeyParameterNames.X5u, key.X5u);
if (key.KeyOps.Count is not 0)
// Important: the JSON Web Keys returned to the caller MUST NOT
// include ANY parameter containing private key material.
var key = new JsonObject();
if (!string.IsNullOrEmpty(notification.Keys[index].Kid))
{
writer.WritePropertyName(JsonWebKeyParameterNames.KeyOps);
writer.WriteStartArray();
key[JsonWebKeyParameterNames.Kid] = notification.Keys[index].Kid;
}
for (var index = 0; index < key.KeyOps.Count; index++)
{
writer.WriteStringValue(key.KeyOps[index]);
}
if (!string.IsNullOrEmpty(notification.Keys[index].Use))
{
key[JsonWebKeyParameterNames.Use] = notification.Keys[index].Use;
}
writer.WriteEndArray();
if (!string.IsNullOrEmpty(notification.Keys[index].Kty))
{
key[JsonWebKeyParameterNames.Kty] = notification.Keys[index].Kty;
}
if (key.X5c.Count is not 0)
if (!string.IsNullOrEmpty(notification.Keys[index].Alg))
{
writer.WritePropertyName(JsonWebKeyParameterNames.X5c);
writer.WriteStartArray();
key[JsonWebKeyParameterNames.Alg] = notification.Keys[index].Alg;
}
for (var index = 0; index < key.X5c.Count; index++)
{
writer.WriteStringValue(key.X5c[index]);
}
if (!string.IsNullOrEmpty(notification.Keys[index].Crv))
{
key[JsonWebKeyParameterNames.Crv] = notification.Keys[index].Crv;
}
writer.WriteEndArray();
if (!string.IsNullOrEmpty(notification.Keys[index].E))
{
key[JsonWebKeyParameterNames.E] = notification.Keys[index].E;
}
writer.WriteEndObject();
}
if (!string.IsNullOrEmpty(notification.Keys[index].N))
{
key[JsonWebKeyParameterNames.N] = notification.Keys[index].N;
}
if (!string.IsNullOrEmpty(notification.Keys[index].X))
{
key[JsonWebKeyParameterNames.X] = notification.Keys[index].X;
}
if (!string.IsNullOrEmpty(notification.Keys[index].Y))
{
key[JsonWebKeyParameterNames.Y] = notification.Keys[index].Y;
}
if (!string.IsNullOrEmpty(notification.Keys[index].Pub))
{
key[JsonWebKeyParameterNames.Pub] = notification.Keys[index].Pub;
}
writer.WriteEndArray();
writer.Flush();
stream.Seek(0L, SeekOrigin.Begin);
if (!string.IsNullOrEmpty(notification.Keys[index].X5t))
{
key[JsonWebKeyParameterNames.X5t] = notification.Keys[index].X5t;
}
if (!string.IsNullOrEmpty(notification.Keys[index].X5u))
{
key[JsonWebKeyParameterNames.X5u] = notification.Keys[index].X5u;
}
if (notification.Keys[index].KeyOps.Count is not 0)
{
key[JsonWebKeyParameterNames.KeyOps] = new JsonArray([.. notification.Keys[index].KeyOps]);
}
using var document = JsonDocument.Parse(stream);
if (notification.Keys[index].X5c.Count is not 0)
{
key[JsonWebKeyParameterNames.X5c] = new JsonArray([.. notification.Keys[index].X5c]);
}
keys.Add(key);
}
// Note: AddParameter() is used here to ensure the mandatory "keys" node
// is returned to the caller, even if the key set doesn't expose any key.
// See https://tools.ietf.org/html/rfc7517#section-5 for more information.
var response = new OpenIddictResponse();
response.AddParameter(Parameters.Keys, document.RootElement.Clone());
response.AddParameter(Parameters.Keys, new JsonArray([.. keys]));
context.Transaction.Response = response;
}
@ -1167,17 +1197,23 @@ public static partial class OpenIddictServerHandlers
foreach (var credentials in context.Options.SigningCredentials)
{
if (!credentials.Key.IsSupportedAlgorithm(SecurityAlgorithms.RsaSha256) &&
!credentials.Key.IsSupportedAlgorithm(SecurityAlgorithms.RsaSsaPssSha256) &&
!credentials.Key.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha256) &&
if (!credentials.Key.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha256) &&
!credentials.Key.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha384) &&
!credentials.Key.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha512))
!credentials.Key.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha512) &&
!credentials.Key.IsSupportedAlgorithm(SecurityAlgorithms.MlDsa44) &&
!credentials.Key.IsSupportedAlgorithm(SecurityAlgorithms.MlDsa65) &&
!credentials.Key.IsSupportedAlgorithm(SecurityAlgorithms.MlDsa87) &&
!credentials.Key.IsSupportedAlgorithm(SecurityAlgorithms.RsaSha256) &&
!credentials.Key.IsSupportedAlgorithm(SecurityAlgorithms.RsaSsaPssSha256))
{
context.Logger.LogInformation(6071, SR.GetResourceString(SR.ID6071), credentials.Key.GetType().Name);
continue;
}
// Important: the JSON Web Keys returned to the caller MUST NOT
// include ANY parameter containing private key material.
var key = new JsonWebKey
{
Use = JsonWebKeyUseNames.Sig,
@ -1206,6 +1242,10 @@ public static partial class OpenIddictServerHandlers
SecurityAlgorithms.RsaSsaPssSha512 or SecurityAlgorithms.RsaSsaPssSha512Signature
=> SecurityAlgorithms.RsaSsaPssSha512,
SecurityAlgorithms.MlDsa44 => SecurityAlgorithms.MlDsa44,
SecurityAlgorithms.MlDsa65 => SecurityAlgorithms.MlDsa65,
SecurityAlgorithms.MlDsa87 => SecurityAlgorithms.MlDsa87,
_ => null
},
@ -1213,56 +1253,11 @@ public static partial class OpenIddictServerHandlers
Kid = credentials.Kid
};
if (credentials.Key.IsSupportedAlgorithm(SecurityAlgorithms.RsaSha256) ||
credentials.Key.IsSupportedAlgorithm(SecurityAlgorithms.RsaSsaPssSha256))
{
// Note: IdentityModel 5 doesn't expose a method allowing to retrieve the underlying algorithm
// from a generic asymmetric security key. To work around this limitation, try to cast
// the security key to the built-in IdentityModel types to extract the required RSA instance.
// See https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/395.
var parameters = credentials.Key switch
{
X509SecurityKey { PublicKey: RSA algorithm } => algorithm.ExportParameters(includePrivateParameters: false),
RsaSecurityKey { Rsa: RSA algorithm } => algorithm.ExportParameters(includePrivateParameters: false),
RsaSecurityKey { Parameters: RSAParameters value } => value,
_ => (RSAParameters?) null
};
if (parameters is null)
{
context.Logger.LogWarning(6073, SR.GetResourceString(SR.ID6073), credentials.Key.GetType().Name);
continue;
}
Debug.Assert(parameters.Value.Exponent is not null &&
parameters.Value.Modulus is not null, SR.GetResourceString(SR.ID4003));
key.Kty = JsonWebAlgorithmsKeyTypes.RSA;
// Note: both E and N must be base64url-encoded.
// See https://tools.ietf.org/html/rfc7518#section-6.3.1.1.
key.E = Base64UrlEncoder.Encode(parameters.Value.Exponent);
key.N = Base64UrlEncoder.Encode(parameters.Value.Modulus);
}
else if (credentials.Key.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha256) ||
credentials.Key.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha384) ||
credentials.Key.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha512))
{
var parameters = credentials.Key switch
{
X509SecurityKey { PublicKey: ECDsa algorithm } => algorithm.ExportParameters(includePrivateParameters: false),
ECDsaSecurityKey { ECDsa: ECDsa algorithm } => algorithm.ExportParameters(includePrivateParameters: false),
_ => (ECParameters?) null
};
if (parameters is null)
if (credentials.Key.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha256) ||
credentials.Key.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha384) ||
credentials.Key.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha512))
{;
if (!TryGetECParameters(credentials.Key, out var parameters))
{
context.Logger.LogWarning(6074, SR.GetResourceString(SR.ID6074), credentials.Key.GetType().Name);
@ -1272,7 +1267,7 @@ public static partial class OpenIddictServerHandlers
// Warning: on .NET Framework 4.x, exported ECParameters generally have a null OID
// value attached. To work around this limitation, both the raw OID values and the
// friendly names are compared to determine whether the curve is of the specified type.
var curve = parameters.Value.Curve.Oid switch
var curve = parameters.Curve.Oid switch
{
{ FriendlyName: "nistP256" } or { Value: "1.2.840.10045.3.1.7" } => JsonWebKeyECTypes.P256,
{ FriendlyName: "nistP384" } or { Value: "1.3.132.0.34" } => JsonWebKeyECTypes.P384,
@ -1288,16 +1283,54 @@ public static partial class OpenIddictServerHandlers
continue;
}
Debug.Assert(parameters.Value.Q.X is not null &&
parameters.Value.Q.Y is not null, SR.GetResourceString(SR.ID4004));
Debug.Assert(parameters.Q.X is not null && parameters.Q.Y is not null, SR.GetResourceString(SR.ID4004));
key.Kty = JsonWebAlgorithmsKeyTypes.EllipticCurve;
key.Crv = curve;
// Note: both X and Y must be base64url-encoded.
// See https://tools.ietf.org/html/rfc7518#section-6.2.1.2.
key.X = Base64UrlEncoder.Encode(parameters.Value.Q.X);
key.Y = Base64UrlEncoder.Encode(parameters.Value.Q.Y);
key.X = Base64UrlEncoder.Encode(parameters.Q.X);
key.Y = Base64UrlEncoder.Encode(parameters.Q.Y);
}
// Note: while ML-DSA is supported on .NET Framework via the Microsoft.Bcl.Cryptography package, SHAKE256 - used
// to produce and validate access token and authorization code hashes when a ML-DSA key is used - is not supported.
//
// As a result, ML-DSA keys are not supported by OpenIddict on .NET Framework and are ignored here.
else if (credentials.Key.IsSupportedAlgorithm(SecurityAlgorithms.MlDsa44) ||
credentials.Key.IsSupportedAlgorithm(SecurityAlgorithms.MlDsa65) ||
credentials.Key.IsSupportedAlgorithm(SecurityAlgorithms.MlDsa87))
{
if (!TryGetMLDsaPublicKey(credentials.Key, out byte[]? blob))
{
context.Logger.LogWarning(6297, SR.GetResourceString(SR.ID6296), credentials.Key.GetType().Name);
continue;
}
key.Kty = JsonWebAlgorithmsKeyTypes.Akp;
key.Pub = Base64UrlEncoder.Encode(blob);
}
else if (credentials.Key.IsSupportedAlgorithm(SecurityAlgorithms.RsaSha256) ||
credentials.Key.IsSupportedAlgorithm(SecurityAlgorithms.RsaSsaPssSha256))
{
if (!TryGetRSAParameters(credentials.Key, out RSAParameters parameters))
{
context.Logger.LogWarning(6073, SR.GetResourceString(SR.ID6073), credentials.Key.GetType().Name);
continue;
}
Debug.Assert(parameters.Exponent is not null && parameters.Modulus is not null, SR.GetResourceString(SR.ID4003));
key.Kty = JsonWebAlgorithmsKeyTypes.RSA;
// Note: both E and N must be base64url-encoded.
// See https://tools.ietf.org/html/rfc7518#section-6.3.1.1.
key.E = Base64UrlEncoder.Encode(parameters.Exponent);
key.N = Base64UrlEncoder.Encode(parameters.Modulus);
}
// If the signing key is embedded in a X.509 certificate, set
@ -1322,6 +1355,73 @@ public static partial class OpenIddictServerHandlers
}
return ValueTask.CompletedTask;
// Note: IdentityModel 5+ doesn't expose a method allowing to retrieve the underlying algorithm
// from a generic security key. To work around this limitation, these local functions try to
// cast the security key to the built-in IdentityModel types to extract the required instance.
//
// See https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/395.
static bool TryGetECParameters(SecurityKey key, out ECParameters parameters)
{
switch (key)
{
case X509SecurityKey { PublicKey: ECDsa algorithm }:
parameters = algorithm.ExportParameters(includePrivateParameters: false);
return true;
case ECDsaSecurityKey { ECDsa: ECDsa algorithm }:
parameters = algorithm.ExportParameters(includePrivateParameters: false);
return true;
default:
parameters = default;
return false;
}
}
static bool TryGetMLDsaPublicKey(SecurityKey key, [NotNullWhen(true)] out byte[]? blob)
{
switch (key)
{
case X509SecurityKey { Certificate: X509Certificate2 certificate }
#pragma warning disable SYSLIB5006
when certificate.GetMLDsaPublicKey() is MLDsa algorithm:
#pragma warning restore SYSLIB5006
blob = algorithm.ExportMLDsaPublicKey();
return true;
case MlDsaSecurityKey { MLDsa: MLDsa algorithm }:
blob = algorithm.ExportMLDsaPublicKey();
return true;
default:
blob = default;
return false;
}
}
static bool TryGetRSAParameters(SecurityKey key, out RSAParameters parameters)
{
switch (key)
{
case X509SecurityKey { PublicKey: RSA algorithm }:
parameters = algorithm.ExportParameters(includePrivateParameters: false);
return true;
case RsaSecurityKey { Rsa: RSA algorithm }:
parameters = algorithm.ExportParameters(includePrivateParameters: false);
return true;
case RsaSecurityKey { Parameters: RSAParameters value }:
parameters = value;
return true;
default:
parameters = default;
return false;
}
}
}
}
}

28
src/OpenIddict.Server/OpenIddictServerHandlers.cs

@ -8,6 +8,7 @@ using System.Collections.Immutable;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
@ -20,6 +21,10 @@ using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
#if !NET
using Org.BouncyCastle.Crypto.Digests;
#endif
namespace OpenIddict.Server;
[EditorBrowsable(EditorBrowsableState.Never)]
@ -5302,7 +5307,7 @@ public static partial class OpenIddictServerHandlers
}
var credentials = context.Options.SigningCredentials.Find(
credentials => credentials.Key is AsymmetricSecurityKey) ??
static credentials => credentials.Key is AsymmetricSecurityKey) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0266));
if (!string.IsNullOrEmpty(context.AccessToken))
@ -5351,8 +5356,29 @@ public static partial class OpenIddictServerHandlers
{ Algorithm: SecurityAlgorithms.RsaSsaPssSha512 or SecurityAlgorithms.RsaSsaPssSha512Signature }
=> SHA512.HashData(Encoding.ASCII.GetBytes(token)),
// Note: while not officially adopted yet, the OpenID Connect Working Group has proposed to use SHAKE256
// for ML-DSA-based algorithms. See https://bitbucket.org/openid/connect/issues/1125 for more information.
{ Algorithm: SecurityAlgorithms.MlDsa44 or SecurityAlgorithms.MlDsa65 or SecurityAlgorithms.MlDsa87 }
=> GetShake256Digest(Encoding.ASCII.GetBytes(token), length: 64),
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0267))
};
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static byte[] GetShake256Digest(byte[] data, int length)
{
#if NET
return Shake256.HashData(data, length);
#else
var digest = new ShakeDigest(256);
digest.BlockUpdate(data, 0, data.Length);
var hash = new byte[length];
digest.DoFinal(hash, 0);
return hash;
#endif
}
}
}

3
src/OpenIddict.Validation/OpenIddictValidationBuilder.cs

@ -7,6 +7,7 @@
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Runtime.Versioning;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.DependencyInjection.Extensions;
@ -283,6 +284,7 @@ public sealed class OpenIddictValidationBuilder
/// </summary>
/// <param name="thumbprint">The thumbprint of the certificate used to identify it in the X.509 store.</param>
/// <returns>The <see cref="OpenIddictValidationBuilder"/> instance.</returns>
[UnsupportedOSPlatform("linux")]
public OpenIddictValidationBuilder AddEncryptionCertificate(string thumbprint)
{
ArgumentException.ThrowIfNullOrEmpty(thumbprint);
@ -508,6 +510,7 @@ public sealed class OpenIddictValidationBuilder
/// </summary>
/// <param name="thumbprint">The thumbprint of the certificate used to identify it in the X.509 store.</param>
/// <returns>The <see cref="OpenIddictValidationBuilder"/> instance.</returns>
[UnsupportedOSPlatform("linux")]
public OpenIddictValidationBuilder AddSigningCertificate(string thumbprint)
{
ArgumentException.ThrowIfNullOrEmpty(thumbprint);

33
src/OpenIddict.Validation/OpenIddictValidationHandlers.Discovery.cs

@ -517,11 +517,11 @@ public static partial class OpenIddictValidationHandlers
var key = (string?) keys[index][JsonWebKeyParameterNames.Kty] switch
{
JsonWebAlgorithmsKeyTypes.RSA => new JsonWebKey
JsonWebAlgorithmsKeyTypes.Akp => new JsonWebKey
{
Kty = JsonWebAlgorithmsKeyTypes.RSA,
E = (string?) keys[index][JsonWebKeyParameterNames.E],
N = (string?) keys[index][JsonWebKeyParameterNames.N]
Kty = JsonWebAlgorithmsKeyTypes.Akp,
Alg = (string?) keys[index][JsonWebKeyParameterNames.Alg],
Pub = (string?) keys[index][JsonWebKeyParameterNames.Pub]
},
JsonWebAlgorithmsKeyTypes.EllipticCurve => new JsonWebKey
@ -532,6 +532,13 @@ public static partial class OpenIddictValidationHandlers
Y = (string?) keys[index][JsonWebKeyParameterNames.Y]
},
JsonWebAlgorithmsKeyTypes.RSA => new JsonWebKey
{
Kty = JsonWebAlgorithmsKeyTypes.RSA,
E = (string?) keys[index][JsonWebKeyParameterNames.E],
N = (string?) keys[index][JsonWebKeyParameterNames.N]
},
_ => null
};
@ -545,9 +552,9 @@ public static partial class OpenIddictValidationHandlers
return ValueTask.CompletedTask;
}
// If the key is a RSA key, ensure the mandatory parameters are all present.
if (string.Equals(key.Kty, JsonWebAlgorithmsKeyTypes.RSA, StringComparison.Ordinal) &&
(string.IsNullOrEmpty(key.E) || string.IsNullOrEmpty(key.N)))
// If the key is an AKP key, ensure the mandatory parameters are all present.
if (string.Equals(key.Kty, JsonWebAlgorithmsKeyTypes.Akp, StringComparison.Ordinal) &&
(string.IsNullOrEmpty(key.Alg) || string.IsNullOrEmpty(key.Pub)))
{
context.Reject(
error: Errors.ServerError,
@ -569,6 +576,18 @@ public static partial class OpenIddictValidationHandlers
return ValueTask.CompletedTask;
}
// If the key is a RSA key, ensure the mandatory parameters are all present.
if (string.Equals(key.Kty, JsonWebAlgorithmsKeyTypes.RSA, StringComparison.Ordinal) &&
(string.IsNullOrEmpty(key.E) || string.IsNullOrEmpty(key.N)))
{
context.Reject(
error: Errors.ServerError,
description: SR.GetResourceString(SR.ID2104),
uri: SR.FormatID8000(SR.ID2104));
return ValueTask.CompletedTask;
}
key.KeyId = (string?) keys[index][JsonWebKeyParameterNames.Kid];
key.X5t = (string?) keys[index][JsonWebKeyParameterNames.X5t];
key.X5tS256 = (string?) keys[index][JsonWebKeyParameterNames.X5tS256];

17
src/OpenIddict.Validation/OpenIddictValidationRetriever.cs

@ -7,6 +7,7 @@
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Tokens;
namespace OpenIddict.Validation;
@ -58,6 +59,22 @@ public sealed class OpenIddictValidationRetriever : IConfigurationRetriever<Open
configuration.SigningKeys.Add(key);
}
// Note: IdentityModel doesn't currently return AKP keys when calling GetSigningKeys(), so a
// second pass is made to ensure that all AKP keys are added to the signing keys collection.
//
// For more information, see
// https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/3534.
for (var index = 0; index < configuration.JsonWebKeySet.Keys.Count; index++)
{
if (configuration.JsonWebKeySet.Keys[index] is {
Kty: JsonWebAlgorithmsKeyTypes.Akp,
Alg: SecurityAlgorithms.MlDsa44 or SecurityAlgorithms.MlDsa65 or SecurityAlgorithms.MlDsa87 } &&
JsonWebKeyConverter.TryConvertToSecurityKey(configuration.JsonWebKeySet.Keys[index], out SecurityKey? key))
{
configuration.SigningKeys.Add(key);
}
}
return configuration;
}
}

1
test/OpenIddict.Client.Tests/OpenIddict.Client.Tests.csproj

@ -11,6 +11,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Moq" />
<PackageReference Include="Xunit.SkippableFact" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFrameworkIdentifier)' == '.NETFramework' ">

1522
test/OpenIddict.Client.Tests/OpenIddictClientBuilderTests.cs

File diff suppressed because it is too large

471
test/OpenIddict.Client.Tests/OpenIddictClientConfigurationTests.cs

@ -0,0 +1,471 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Tokens;
using Moq;
using Xunit;
namespace OpenIddict.Client.Tests;
public class OpenIddictClientConfigurationTests
{
[Fact]
public void Constructor_ThrowsAnExceptionForNullProvider()
{
// Arrange
var provider = (IServiceProvider) null!;
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => new OpenIddictClientConfiguration(provider));
Assert.Equal("provider", exception.ParamName);
}
[Fact]
public void PostConfigure_ThrowsAnExceptionForNullOptions()
{
// Arrange
var configuration = new OpenIddictClientConfiguration(new ServiceCollection().BuildServiceProvider());
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => configuration.PostConfigure(name: null, options: null!));
Assert.Equal("options", exception.ParamName);
}
[Fact]
public void PostConfigure_SetsTimeProviderToSystemWhenNotRegistered()
{
// Arrange
var configuration = new OpenIddictClientConfiguration(new ServiceCollection().BuildServiceProvider());
var options = new OpenIddictClientOptions();
// Act
configuration.PostConfigure(name: null, options);
// Assert
Assert.Same(TimeProvider.System, options.TimeProvider);
}
[Fact]
public void PostConfigure_UsesRegisteredTimeProvider()
{
// Arrange
var timeProvider = new FakeTimeProvider();
var services = new ServiceCollection();
services.AddSingleton<TimeProvider>(timeProvider);
var configuration = new OpenIddictClientConfiguration(services.BuildServiceProvider());
var options = new OpenIddictClientOptions();
// Act
configuration.PostConfigure(name: null, options);
// Assert
Assert.Same(timeProvider, options.TimeProvider);
}
[Fact]
public void PostConfigure_DoesNotOverrideExplicitlySetTimeProvider()
{
// Arrange
var explicitProvider = new FakeTimeProvider();
var registeredProvider = new FakeTimeProvider();
var services = new ServiceCollection();
services.AddSingleton<TimeProvider>(registeredProvider);
var configuration = new OpenIddictClientConfiguration(services.BuildServiceProvider());
var options = new OpenIddictClientOptions { TimeProvider = explicitProvider };
// Act
configuration.PostConfigure(name: null, options);
// Assert
Assert.Same(explicitProvider, options.TimeProvider);
}
[Fact]
public void PostConfigure_ComputesDefaultRegistrationIdentifierAndClientType()
{
// Arrange
var configuration = new OpenIddictClientConfiguration(new ServiceCollection().BuildServiceProvider());
var registration = new OpenIddictClientRegistration
{
Issuer = new Uri("https://www.contoso.com/"),
ClientSecret = "secret",
Configuration = new OpenIddictConfiguration()
};
var options = new OpenIddictClientOptions();
options.Registrations.Add(registration);
// Act
configuration.PostConfigure(name: null, options);
// Assert
Assert.False(string.IsNullOrEmpty(registration.RegistrationId));
Assert.Equal(ClientTypes.Confidential, registration.ClientType);
}
[Fact]
public void PostConfigure_AssignsPublicClientTypeWhenNoSecretOrSigningCredentialsAreConfigured()
{
// Arrange
var configuration = new OpenIddictClientConfiguration(new ServiceCollection().BuildServiceProvider());
var registration = new OpenIddictClientRegistration
{
Issuer = new Uri("https://www.contoso.com/"),
Configuration = new OpenIddictConfiguration()
};
var options = new OpenIddictClientOptions();
options.Registrations.Add(registration);
// Act
configuration.PostConfigure(name: null, options);
// Assert
Assert.Equal(ClientTypes.Public, registration.ClientType);
}
[Fact]
public void PostConfigure_AddsRedirectionUrisFromRegistrations()
{
// Arrange
var configuration = new OpenIddictClientConfiguration(new ServiceCollection().BuildServiceProvider());
var options = new OpenIddictClientOptions();
options.Registrations.Add(new OpenIddictClientRegistration
{
RedirectUri = new Uri("https://www.contoso.com/callback")
});
options.Registrations.Add(new OpenIddictClientRegistration
{
PostLogoutRedirectUri = new Uri("https://www.contoso.com/logout-callback")
});
// Act
configuration.PostConfigure(name: null, options);
// Assert
Assert.Contains(new Uri("https://www.contoso.com/callback"), options.RedirectionEndpointUris);
Assert.Contains(new Uri("https://www.contoso.com/logout-callback"), options.PostLogoutRedirectionEndpointUris);
}
[Fact]
public void Validate_ThrowsAnExceptionForNullOptions()
{
// Arrange
var configuration = new OpenIddictClientConfiguration(new ServiceCollection().BuildServiceProvider());
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => configuration.Validate(name: null, options: null!));
Assert.Equal("options", exception.ParamName);
}
[Fact]
public void Validate_ReturnsAnErrorWhenJsonWebTokenHandlerIsMissing()
{
// Arrange
var configuration = new OpenIddictClientConfiguration(new ServiceCollection().BuildServiceProvider());
var options = CreateBaseOptions();
options.JsonWebTokenHandler = null!;
// Act
var result = configuration.Validate(name: null, options);
// Assert
Assert.Contains(SR.GetResourceString(SR.ID0075), result.Failures!);
}
[Fact]
public void Validate_ReturnsAnErrorWhenRegistrationIdentifierContainsSeparator()
{
// Arrange
var configuration = new OpenIddictClientConfiguration(new ServiceCollection().BuildServiceProvider());
var options = CreateBaseOptions();
options.Registrations.Add(new OpenIddictClientRegistration
{
RegistrationId = "invalid\u001eidentifier",
Issuer = new Uri("https://www.contoso.com/"),
ConfigurationManager = new StaticConfigurationManager<OpenIddictConfiguration>(new OpenIddictConfiguration())
});
// Act
var result = configuration.Validate(name: null, options);
// Assert
Assert.Contains(SR.GetResourceString(SR.ID0455), result.Failures!);
}
[Fact]
public void Validate_ReturnsAnErrorWhenRegistrationIdentifierIsMissing()
{
// Arrange
var configuration = new OpenIddictClientConfiguration(new ServiceCollection().BuildServiceProvider());
var options = CreateBaseOptions();
options.Registrations.Add(new OpenIddictClientRegistration
{
Issuer = new Uri("https://www.contoso.com/"),
ConfigurationManager = new StaticConfigurationManager<OpenIddictConfiguration>(new OpenIddictConfiguration())
});
// Act
var result = configuration.Validate(name: null, options);
// Assert
Assert.Contains(SR.GetResourceString(SR.ID0521), result.Failures!);
}
[Fact]
public void Validate_ReturnsAnErrorWhenIssuerIsInvalid()
{
// Arrange
var configuration = new OpenIddictClientConfiguration(new ServiceCollection().BuildServiceProvider());
var options = CreateBaseOptions();
options.Registrations.Add(new OpenIddictClientRegistration
{
RegistrationId = "contoso",
Issuer = new Uri("/relative", UriKind.Relative),
ConfigurationManager = new StaticConfigurationManager<OpenIddictConfiguration>(new OpenIddictConfiguration())
});
// Act
var result = configuration.Validate(name: null, options);
// Assert
Assert.Contains(SR.GetResourceString(SR.ID0136), result.Failures!);
}
[Fact]
public void Validate_ReturnsAnErrorWhenIssuerContainsQueryOrFragment()
{
// Arrange
var configuration = new OpenIddictClientConfiguration(new ServiceCollection().BuildServiceProvider());
var options = CreateBaseOptions();
options.Registrations.Add(new OpenIddictClientRegistration
{
RegistrationId = "contoso",
Issuer = new Uri("https://www.contoso.com/?query=parameter#fragment"),
ConfigurationManager = new StaticConfigurationManager<OpenIddictConfiguration>(new OpenIddictConfiguration())
});
// Act
var result = configuration.Validate(name: null, options);
// Assert
Assert.Contains(SR.GetResourceString(SR.ID0137), result.Failures!);
}
[Fact]
public void Validate_ReturnsAnErrorWhenRegistrationConfigurationIssuerDoesNotMatchRegistrationIssuer()
{
// Arrange
var configuration = new OpenIddictClientConfiguration(new ServiceCollection().BuildServiceProvider());
var options = CreateBaseOptions();
options.Registrations.Add(new OpenIddictClientRegistration
{
RegistrationId = "contoso",
Issuer = new Uri("https://www.contoso.com/"),
Configuration = new OpenIddictConfiguration { Issuer = new Uri("https://www.fabrikam.com/") },
ConfigurationManager = new StaticConfigurationManager<OpenIddictConfiguration>(new OpenIddictConfiguration())
});
// Act
var result = configuration.Validate(name: null, options);
// Assert
Assert.Contains(SR.GetResourceString(SR.ID0395), result.Failures!);
}
[Fact]
public void Validate_ReturnsAnErrorWhenConfigurationManagerIsMissing()
{
// Arrange
var configuration = new OpenIddictClientConfiguration(new ServiceCollection().BuildServiceProvider());
var options = CreateBaseOptions();
options.Registrations.Add(new OpenIddictClientRegistration
{
RegistrationId = "contoso",
Issuer = new Uri("https://www.contoso.com/")
});
// Act
var result = configuration.Validate(name: null, options);
// Assert
Assert.Contains(SR.GetResourceString(SR.ID0522), result.Failures!);
}
[Fact]
public void Validate_ReturnsAnErrorWhenNonStaticConfigurationManagerIsUsedWithoutRequiredHandlers()
{
// Arrange
var configuration = new OpenIddictClientConfiguration(new ServiceCollection().BuildServiceProvider());
var options = CreateBaseOptions();
options.Registrations.Add(new OpenIddictClientRegistration
{
RegistrationId = "contoso",
Issuer = new Uri("https://www.contoso.com/"),
ConfigurationManager = Mock.Of<IConfigurationManager<OpenIddictConfiguration>>()
});
// Act
var result = configuration.Validate(name: null, options);
// Assert
Assert.Contains(SR.GetResourceString(SR.ID0313), result.Failures!);
}
[Fact]
public void Validate_ReturnsAnErrorWhenNoFlowIsEnabled()
{
// Arrange
var configuration = new OpenIddictClientConfiguration(new ServiceCollection().BuildServiceProvider());
var options = new OpenIddictClientOptions();
// Act
var result = configuration.Validate(name: null, options);
// Assert
Assert.Contains(SR.GetResourceString(SR.ID0076), result.Failures!);
}
[Fact]
public void Validate_ReturnsAnErrorWhenEndpointUrisAreNotUnique()
{
// Arrange
var configuration = new OpenIddictClientConfiguration(new ServiceCollection().BuildServiceProvider());
var options = CreateBaseOptions();
var uri = new Uri("https://www.contoso.com/callback");
options.RedirectionEndpointUris.Add(uri);
options.PostLogoutRedirectionEndpointUris.Add(uri);
// Act
var result = configuration.Validate(name: null, options);
// Assert
Assert.Contains(SR.GetResourceString(SR.ID0285), result.Failures!);
}
[Fact]
public void Validate_ReturnsAnErrorWhenRedirectionEndpointIsMissingForAuthorizationCodeGrant()
{
// Arrange
var configuration = new OpenIddictClientConfiguration(new ServiceCollection().BuildServiceProvider());
var options = CreateBaseOptions();
options.GrantTypes.Add(GrantTypes.AuthorizationCode);
// Act
var result = configuration.Validate(name: null, options);
// Assert
Assert.Contains(SR.GetResourceString(SR.ID0356), result.Failures!);
}
[Fact]
public void Validate_ReturnsAnErrorWhenStateTokenCredentialsAreMissing()
{
// Arrange
var configuration = new OpenIddictClientConfiguration(new ServiceCollection().BuildServiceProvider());
var options = CreateBaseOptions();
options.RedirectionEndpointUris.Add(new Uri("https://www.contoso.com/callback"));
// Act
var result = configuration.Validate(name: null, options);
// Assert
Assert.Contains(SR.GetResourceString(SR.ID0357), result.Failures!);
Assert.Contains(SR.GetResourceString(SR.ID0358), result.Failures!);
}
[Fact]
public void Validate_ReturnsAnErrorWhenResponseTypeIsInconsistentWithEnabledGrantTypes()
{
// Arrange
var configuration = new OpenIddictClientConfiguration(new ServiceCollection().BuildServiceProvider());
var options = CreateBaseOptions();
options.ResponseTypes.Add(ResponseTypes.Code);
// Act
var result = configuration.Validate(name: null, options);
// Assert
Assert.Contains(SR.FormatID0281(ResponseTypes.Code), result.Failures!);
}
[Fact]
public void Validate_SucceedsForConsistentMinimalConfiguration()
{
// Arrange
var configuration = new OpenIddictClientConfiguration(new ServiceCollection().BuildServiceProvider());
var options = CreateBaseOptions();
options.Registrations.Add(new OpenIddictClientRegistration
{
RegistrationId = "contoso",
Issuer = new Uri("https://www.contoso.com/"),
ConfigurationManager = new StaticConfigurationManager<OpenIddictConfiguration>(new OpenIddictConfiguration())
});
// Act
var result = configuration.Validate(name: null, options);
// Assert
Assert.True(result.Succeeded);
}
[Fact]
public void Validate_ReturnsAnErrorWhenRegistrationIdentifiersAreDuplicated()
{
// Arrange
var configuration = new OpenIddictClientConfiguration(new ServiceCollection().BuildServiceProvider());
var options = CreateBaseOptions();
var manager = new StaticConfigurationManager<OpenIddictConfiguration>(new OpenIddictConfiguration());
options.Registrations.Add(new OpenIddictClientRegistration
{
RegistrationId = "duplicate",
Issuer = new Uri("https://www.contoso.com/"),
ConfigurationManager = manager
});
options.Registrations.Add(new OpenIddictClientRegistration
{
RegistrationId = "DUPLICATE",
Issuer = new Uri("https://www.fabrikam.com/"),
ConfigurationManager = manager
});
// Act
var result = configuration.Validate(name: null, options);
// Assert
Assert.Contains(SR.GetResourceString(SR.ID0347), result.Failures!);
}
private static OpenIddictClientOptions CreateBaseOptions()
{
var options = new OpenIddictClientOptions
{
TimeProvider = TimeProvider.System
};
options.GrantTypes.Add(GrantTypes.ClientCredentials);
return options;
}
private sealed class FakeTimeProvider : TimeProvider;
}

1
test/OpenIddict.Server.IntegrationTests/OpenIddict.Server.IntegrationTests.csproj

@ -14,6 +14,7 @@
<PackageReference Include="MartinCostello.Logging.XUnit" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Moq" />
<PackageReference Include="Xunit.SkippableFact" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFrameworkIdentifier)' == '.NETFramework' ">

28
test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Discovery.cs

@ -900,6 +900,9 @@ public abstract partial class OpenIddictServerIntegrationTests
[InlineData(Algorithms.EcdsaSha256)]
[InlineData(Algorithms.EcdsaSha384)]
[InlineData(Algorithms.EcdsaSha512)]
[InlineData(SecurityAlgorithms.MlDsa44)]
[InlineData(SecurityAlgorithms.MlDsa65)]
[InlineData(SecurityAlgorithms.MlDsa87)]
public async Task HandleConfigurationRequest_SigningAlgorithmsAreCorrectlyReturned(string algorithm)
{
// Arrange
@ -1538,6 +1541,31 @@ public abstract partial class OpenIddictServerIntegrationTests
Assert.Equal(parameters.Q.Y, Base64UrlEncoder.DecodeBytes((string?) key?[JsonWebKeyParameterNames.Y]));
}
[SkippableFact(typeof(PlatformNotSupportedException))]
public async Task HandleJsonWebKeySetRequest_MlDsaSecurityKeysAreCorrectlyExposed()
{
// Arrange
using var algorithm = MLDsa.GenerateKey(MLDsaAlgorithm.MLDsa44);
var blob = algorithm.ExportMLDsaPublicKey();
await using var server = await CreateServerAsync(options =>
{
options.Configure(options => options.SigningCredentials.Clear());
options.AddSigningKey(new MlDsaSecurityKey(algorithm));
});
await using var client = await server.CreateClientAsync();
// Act
var response = await client.GetAsync("/.well-known/jwks");
var key = response[Parameters.Keys]?[0];
// Assert
Assert.Equal(JsonWebAlgorithmsKeyTypes.Akp, (string?) key?[JsonWebKeyParameterNames.Kty]);
Assert.Equal(SecurityAlgorithms.MlDsa44, (string?) key?[JsonWebKeyParameterNames.Alg]);
Assert.Equal(blob, Base64UrlEncoder.DecodeBytes((string?) key?[JsonWebKeyParameterNames.Pub]));
}
[Fact]
public async Task HandleJsonWebKeySetRequest_X509CertificatesAreCorrectlyExposed()
{

1
test/OpenIddict.Server.Tests/OpenIddict.Server.Tests.csproj

@ -11,6 +11,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Moq" />
<PackageReference Include="Xunit.SkippableFact" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFrameworkIdentifier)' == '.NETFramework' ">

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

@ -1,4 +1,5 @@
using System.Reflection;
using System.Runtime.Versioning;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.DependencyInjection;
@ -12,6 +13,17 @@ namespace OpenIddict.Server.Tests;
public class OpenIddictServerBuilderTests
{
[Fact]
public void ValidateOnStart_CanBeInvoked()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act and assert
Assert.Same(builder, builder.ValidateOnStart());
}
[Fact]
public void Constructor_ThrowsAnExceptionForNullServices()
{
@ -135,6 +147,27 @@ public class OpenIddictServerBuilderTests
Assert.Equal("credentials", exception.ParamName);
}
[Fact]
public void AddEncryptionCredentials_EncryptingCredentialsAreCorrectlyAdded()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
var credentials = new EncryptingCredentials(
Mock.Of<SecurityKey>(key => key.KeySize == 256),
SecurityAlgorithms.Aes256KW,
SecurityAlgorithms.Aes256CbcHmacSha512);
// Act
builder.AddEncryptionCredentials(credentials);
var options = GetOptions(services);
// Assert
Assert.Same(credentials, options.EncryptionCredentials[0]);
}
[Fact]
public void AddEncryptionKey_ThrowsExceptionWhenKeyIsNull()
{
@ -204,6 +237,74 @@ public class OpenIddictServerBuilderTests
Assert.Equal(SR.FormatID0283(256, 384), exception.Message);
}
[Fact]
public void AddEncryptionKey_UsesRsaOaepWhenSupported()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
var key = Mock.Of<SecurityKey>(mock => mock.IsSupportedAlgorithm(SecurityAlgorithms.RsaOAEP));
// Act
builder.AddEncryptionKey(key);
var options = GetOptions(services);
// Assert
Assert.Equal(SecurityAlgorithms.RsaOAEP, options.EncryptionCredentials[0].Alg);
}
[Fact]
public void AddEncryptionKey_ThrowsExceptionWhenNoSupportedAlgorithmIsFound()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
var key = Mock.Of<SecurityKey>(mock =>
!mock.IsSupportedAlgorithm(SecurityAlgorithms.Aes256KW) &&
!mock.IsSupportedAlgorithm(SecurityAlgorithms.RsaOAEP));
// Act and assert
var exception = Assert.Throws<InvalidOperationException>(() => builder.AddEncryptionKey(key));
Assert.Equal(SR.GetResourceString(SR.ID0056), exception.Message);
}
[Fact]
public void AddEncryptionKeys_ThrowsExceptionWhenKeysAreNull()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => builder.AddEncryptionKeys(keys: null!));
Assert.Equal("keys", exception.ParamName);
}
[Fact]
public void AddEncryptionKeys_KeysAreCorrectlyAdded()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
var keys = new SecurityKey[]
{
Mock.Of<SecurityKey>(mock => mock.KeySize == 256 && mock.IsSupportedAlgorithm(SecurityAlgorithms.Aes256KW)),
Mock.Of<SecurityKey>(mock => mock.IsSupportedAlgorithm(SecurityAlgorithms.RsaOAEP))
};
// Act
builder.AddEncryptionKeys(keys);
var options = GetOptions(services);
// Assert
Assert.Equal(2, options.EncryptionCredentials.Count);
}
[Fact]
public void RemoveEventHandler_ThrowsAnExceptionWhenDescriptorIsNull()
{
@ -276,6 +377,220 @@ public class OpenIddictServerBuilderTests
Assert.Equal("subject", exception.ParamName);
}
[Fact]
public void AddEphemeralEncryptionKey_ThrowsAnExceptionForNullOrEmptyAlgorithm()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act and assert
var nullException = Assert.ThrowsAny<ArgumentException>(() => builder.AddEphemeralEncryptionKey(algorithm: null!));
var emptyException = Assert.ThrowsAny<ArgumentException>(() => builder.AddEphemeralEncryptionKey(string.Empty));
Assert.Equal("algorithm", nullException.ParamName);
Assert.Equal("algorithm", emptyException.ParamName);
}
[Fact]
public void AddEphemeralEncryptionKey_DefaultAlgorithmIsRsaOaep()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.AddEphemeralEncryptionKey();
var options = GetOptions(services);
// Assert
Assert.Equal(SecurityAlgorithms.RsaOAEP, options.EncryptionCredentials[0].Alg);
}
[Theory]
[InlineData(SecurityAlgorithms.Aes256KW)]
[InlineData(SecurityAlgorithms.RsaOAEP)]
[InlineData(SecurityAlgorithms.RsaOaepKeyWrap)]
public void AddEphemeralEncryptionKey_EncryptionCredentialsUseSpecifiedAlgorithm(string algorithm)
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.AddEphemeralEncryptionKey(algorithm);
var options = GetOptions(services);
// Assert
Assert.Equal(algorithm, options.EncryptionCredentials[0].Alg);
}
[Fact]
public void AddEphemeralEncryptionKey_ThrowsExceptionForUnsupportedAlgorithm()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act and assert
var exception = Assert.Throws<InvalidOperationException>(() => builder.AddEphemeralEncryptionKey("unsupported"));
Assert.Equal(SR.GetResourceString(SR.ID0058), exception.Message);
}
[Fact]
public void AddEncryptionCertificate_ThrowsAnExceptionForNullCertificate()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => builder.AddEncryptionCertificate(certificate: null!));
Assert.Equal("certificate", exception.ParamName);
}
[Fact]
public void AddEncryptionCertificate_ThrowsExceptionWhenPrivateKeyIsMissing()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
using var certificate = CreateEncryptionCertificate(includePrivateKey: false);
// Act and assert
var exception = Assert.Throws<InvalidOperationException>(() => builder.AddEncryptionCertificate(certificate));
Assert.Equal(SR.GetResourceString(SR.ID0061), exception.Message);
}
[Fact]
public void AddEncryptionCertificate_ThrowsExceptionWhenKeyUsageIsInvalid()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
using var certificate = CreateCertificate(X509KeyUsageFlags.DigitalSignature, includePrivateKey: true);
// Act and assert
var exception = Assert.Throws<InvalidOperationException>(() => builder.AddEncryptionCertificate(certificate));
Assert.Equal(SR.GetResourceString(SR.ID0060), exception.Message);
}
[Fact]
public void AddEncryptionCertificate_Stream_ThrowsExceptionWhenStreamIsNull()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => builder.AddEncryptionCertificate(stream: null!, password: "password"));
Assert.Equal("stream", exception.ParamName);
}
[Fact]
public void AddEncryptionCertificate_Stream_ThrowsExceptionWhenContentTypeIsInvalid()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
using var stream = new MemoryStream([0x01, 0x02, 0x03]);
// Act and assert
Assert.ThrowsAny<Exception>(() => builder.AddEncryptionCertificate(stream, "password", X509KeyStorageFlags.Exportable));
}
[Fact]
public void AddEncryptionCertificate_Stream_CertificateIsCorrectlyAdded()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
using var certificate = CreateEncryptionCertificate(includePrivateKey: true);
var payload = certificate.Export(X509ContentType.Pfx, "password");
using var stream = new MemoryStream(payload);
// Act
builder.AddEncryptionCertificate(stream, "password", X509KeyStorageFlags.Exportable);
var options = GetOptions(services);
// Assert
Assert.IsType<X509SecurityKey>(options.EncryptionCredentials[0].Key);
}
[Fact]
public void AddEncryptionCertificate_Assembly_ThrowsExceptionWhenAssemblyIsNull()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => builder.AddEncryptionCertificate(
assembly: null!, resource: "resource", password: "password", flags: X509KeyStorageFlags.Exportable));
Assert.Equal("assembly", exception.ParamName);
}
[Fact]
public void AddEncryptionCertificate_Assembly_ThrowsExceptionWhenResourceIsNullOrEmpty()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
var assembly = typeof(OpenIddictServerBuilderTests).GetTypeInfo().Assembly;
// Act and assert
var nullException = Assert.ThrowsAny<ArgumentException>(() => builder.AddEncryptionCertificate(
assembly, resource: null!, password: "password", flags: X509KeyStorageFlags.Exportable));
var emptyException = Assert.ThrowsAny<ArgumentException>(() => builder.AddEncryptionCertificate(
assembly, resource: string.Empty, password: "password", flags: X509KeyStorageFlags.Exportable));
Assert.Equal("resource", nullException.ParamName);
Assert.Equal("resource", emptyException.ParamName);
}
[Fact]
public void AddEncryptionCertificate_Assembly_ThrowsExceptionWhenResourceCannotBeFound()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
var assembly = typeof(OpenIddictServerBuilderTests).GetTypeInfo().Assembly;
// Act and assert
var exception = Assert.Throws<InvalidOperationException>(() => builder.AddEncryptionCertificate(
assembly, resource: "missing.pfx", password: "password", flags: X509KeyStorageFlags.Exportable));
Assert.Equal(SR.GetResourceString(SR.ID0064), exception.Message);
}
[SkippableFact, UnsupportedOSPlatform("linux")]
public void AddEncryptionCertificate_ThrowsExceptionWhenCertificateCannotBeFoundInStores()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act and assert
var exception = Assert.Throws<InvalidOperationException>(() => builder.AddEncryptionCertificate(Guid.NewGuid().ToString("N")));
Assert.Equal(SR.GetResourceString(SR.ID0066), exception.Message);
}
[Fact]
public void AddEncryptionCertificates_ThrowsExceptionWhenCertificatesAreNull()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => builder.AddEncryptionCertificates(certificates: null!));
Assert.Equal("certificates", exception.ParamName);
}
[Fact]
public void AddDevelopmentEncryptionCertificate_CanGenerateCertificate()
{
@ -311,6 +626,35 @@ public class OpenIddictServerBuilderTests
Assert.Equal("subject", exception.ParamName);
}
[Fact]
public void AddSigningCredentials_ThrowsExceptionWhenCredentialsAreNull()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => builder.AddSigningCredentials(credentials: null!));
Assert.Equal("credentials", exception.ParamName);
}
[Fact]
public void AddSigningCredentials_SigningCredentialsAreCorrectlyAdded()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
var credentials = new SigningCredentials(Mock.Of<SecurityKey>(), SecurityAlgorithms.HmacSha256);
// Act
builder.AddSigningCredentials(credentials);
var options = GetOptions(services);
// Assert
Assert.Same(credentials, options.SigningCredentials[0]);
}
[Fact]
public void AddDevelopmentSigningCertificate_CanGenerateCertificate()
{
@ -345,13 +689,16 @@ public class OpenIddictServerBuilderTests
Assert.Single(options.SigningCredentials);
}
[Theory]
[SkippableTheory(typeof(PlatformNotSupportedException))]
[InlineData(SecurityAlgorithms.RsaSha256)]
[InlineData(SecurityAlgorithms.RsaSha384)]
[InlineData(SecurityAlgorithms.RsaSha512)]
[InlineData(SecurityAlgorithms.EcdsaSha256)]
[InlineData(SecurityAlgorithms.EcdsaSha384)]
[InlineData(SecurityAlgorithms.EcdsaSha512)]
[InlineData(SecurityAlgorithms.MlDsa44)]
[InlineData(SecurityAlgorithms.MlDsa65)]
[InlineData(SecurityAlgorithms.MlDsa87)]
public void AddEphemeralSigningKey_SigningCredentialsUseSpecifiedAlgorithm(string algorithm)
{
// Arrange
@ -399,6 +746,9 @@ public class OpenIddictServerBuilderTests
[InlineData(SecurityAlgorithms.EcdsaSha256)]
[InlineData(SecurityAlgorithms.EcdsaSha384)]
[InlineData(SecurityAlgorithms.EcdsaSha512)]
[InlineData(SecurityAlgorithms.MlDsa44)]
[InlineData(SecurityAlgorithms.MlDsa65)]
[InlineData(SecurityAlgorithms.MlDsa87)]
public void AddSigningKey_SigningKeyIsCorrectlyAdded(string algorithm)
{
// Arrange
@ -416,6 +766,254 @@ public class OpenIddictServerBuilderTests
Assert.Same(key, options.SigningCredentials[0].Key);
}
[Fact]
public void AddSigningKey_ThrowsExceptionWhenNoSupportedAlgorithmIsFound()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
var key = Mock.Of<SecurityKey>(mock =>
!mock.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha256) &&
!mock.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha384) &&
!mock.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha512) &&
!mock.IsSupportedAlgorithm(SecurityAlgorithms.HmacSha256) &&
!mock.IsSupportedAlgorithm(SecurityAlgorithms.RsaSha256));
// Act and assert
var exception = Assert.Throws<InvalidOperationException>(() => builder.AddSigningKey(key));
Assert.Equal(SR.GetResourceString(SR.ID0068), exception.Message);
}
[Fact]
public void AddSigningKeys_ThrowsExceptionWhenKeysAreNull()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => builder.AddSigningKeys(keys: null!));
Assert.Equal("keys", exception.ParamName);
}
[Fact]
public void AddSigningKeys_KeysAreCorrectlyAdded()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
var keys = new SecurityKey[]
{
Mock.Of<SecurityKey>(mock => mock.IsSupportedAlgorithm(SecurityAlgorithms.HmacSha256)),
Mock.Of<SecurityKey>(mock => mock.IsSupportedAlgorithm(SecurityAlgorithms.RsaSha256))
};
// Act
builder.AddSigningKeys(keys);
var options = GetOptions(services);
// Assert
Assert.Equal(2, options.SigningCredentials.Count);
}
[Fact]
public void AddEphemeralSigningKey_ThrowsAnExceptionForNullOrEmptyAlgorithm()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act and assert
var nullException = Assert.ThrowsAny<ArgumentException>(() => builder.AddEphemeralSigningKey(algorithm: null!));
var emptyException = Assert.ThrowsAny<ArgumentException>(() => builder.AddEphemeralSigningKey(string.Empty));
Assert.Equal("algorithm", nullException.ParamName);
Assert.Equal("algorithm", emptyException.ParamName);
}
[Fact]
public void AddEphemeralSigningKey_DefaultSigningAlgorithmIsRsaSha256()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.AddEphemeralSigningKey();
var options = GetOptions(services);
// Assert
Assert.Single(options.SigningCredentials);
Assert.Equal(SecurityAlgorithms.RsaSha256, options.SigningCredentials[0].Algorithm);
}
[Fact]
public void AddEphemeralSigningKey_ThrowsExceptionForUnsupportedAlgorithm()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act and assert
var exception = Assert.Throws<InvalidOperationException>(() => builder.AddEphemeralSigningKey("unsupported"));
Assert.Equal(SR.GetResourceString(SR.ID0058), exception.Message);
}
[Fact]
public void AddSigningCertificate_ThrowsAnExceptionForNullCertificate()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => builder.AddSigningCertificate(certificate: null!));
Assert.Equal("certificate", exception.ParamName);
}
[Fact]
public void AddSigningCertificate_ThrowsExceptionWhenPrivateKeyIsMissing()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
using var certificate = CreateSigningCertificate(includePrivateKey: false);
// Act and assert
var exception = Assert.Throws<InvalidOperationException>(() => builder.AddSigningCertificate(certificate));
Assert.Equal(SR.GetResourceString(SR.ID0061), exception.Message);
}
[Fact]
public void AddSigningCertificate_ThrowsExceptionWhenKeyUsageIsInvalid()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
using var certificate = CreateCertificate(X509KeyUsageFlags.KeyEncipherment, includePrivateKey: true);
// Act and assert
var exception = Assert.Throws<InvalidOperationException>(() => builder.AddSigningCertificate(certificate));
Assert.Equal(SR.GetResourceString(SR.ID0070), exception.Message);
}
[Fact]
public void AddSigningCertificate_Stream_ThrowsExceptionWhenStreamIsNull()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => builder.AddSigningCertificate(stream: null!, password: "password"));
Assert.Equal("stream", exception.ParamName);
}
[Fact]
public void AddSigningCertificate_Stream_ThrowsExceptionWhenContentTypeIsInvalid()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
using var stream = new MemoryStream([0x01, 0x02, 0x03]);
// Act and assert
Assert.ThrowsAny<Exception>(() => builder.AddSigningCertificate(stream, "password", X509KeyStorageFlags.Exportable));
}
[Fact]
public void AddSigningCertificate_Stream_CertificateIsCorrectlyAdded()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
using var certificate = CreateSigningCertificate(includePrivateKey: true);
var payload = certificate.Export(X509ContentType.Pfx, "password");
using var stream = new MemoryStream(payload);
// Act
builder.AddSigningCertificate(stream, "password", X509KeyStorageFlags.Exportable);
var options = GetOptions(services);
// Assert
Assert.IsType<X509SecurityKey>(options.SigningCredentials[0].Key);
}
[Fact]
public void AddSigningCertificate_Assembly_ThrowsExceptionWhenAssemblyIsNull()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => builder.AddSigningCertificate(
assembly: null!, resource: "resource", password: "password", flags: X509KeyStorageFlags.Exportable));
Assert.Equal("assembly", exception.ParamName);
}
[Fact]
public void AddSigningCertificate_Assembly_ThrowsExceptionWhenResourceIsNullOrEmpty()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
var assembly = typeof(OpenIddictServerBuilderTests).GetTypeInfo().Assembly;
// Act and assert
var nullException = Assert.ThrowsAny<ArgumentException>(() => builder.AddSigningCertificate(
assembly, resource: null!, password: "password", flags: X509KeyStorageFlags.Exportable));
var emptyException = Assert.ThrowsAny<ArgumentException>(() => builder.AddSigningCertificate(
assembly, resource: string.Empty, password: "password", flags: X509KeyStorageFlags.Exportable));
Assert.Equal("resource", nullException.ParamName);
Assert.Equal("resource", emptyException.ParamName);
}
[Fact]
public void AddSigningCertificate_Assembly_ThrowsExceptionWhenResourceCannotBeFound()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
var assembly = typeof(OpenIddictServerBuilderTests).GetTypeInfo().Assembly;
// Act and assert
var exception = Assert.Throws<InvalidOperationException>(() => builder.AddSigningCertificate(
assembly, resource: "missing.pfx", password: "password", flags: X509KeyStorageFlags.Exportable));
Assert.Equal(SR.GetResourceString(SR.ID0064), exception.Message);
}
[SkippableFact, UnsupportedOSPlatform("linux")]
public void AddSigningCertificate_ThrowsExceptionWhenCertificateCannotBeFoundInStores()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act and assert
var exception = Assert.Throws<InvalidOperationException>(() => builder.AddSigningCertificate(Guid.NewGuid().ToString("N")));
Assert.Equal(SR.GetResourceString(SR.ID0066), exception.Message);
}
[Fact]
public void AddSigningCertificates_ThrowsExceptionWhenCertificatesAreNull()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => builder.AddSigningCertificates(certificates: null!));
Assert.Equal("certificates", exception.ParamName);
}
[Fact]
public void AddSigningCertificate_SigningKeyIsCorrectlyAdded()
{
@ -3179,6 +3777,36 @@ public class OpenIddictServerBuilderTests
Assert.True(options.UseClientCertificateBoundRefreshTokens);
}
private static X509Certificate2 CreateCertificate(X509KeyUsageFlags usages, bool includePrivateKey)
{
using var algorithm = RSA.Create(keySizeInBits: 2048);
var request = new CertificateRequest(
subjectName: "CN=OpenIddict Server Tests",
key: algorithm,
hashAlgorithm: HashAlgorithmName.SHA256,
padding: RSASignaturePadding.Pkcs1);
request.CertificateExtensions.Add(new X509KeyUsageExtension(usages, critical: true));
var certificate = request.CreateSelfSigned(
notBefore: DateTimeOffset.UtcNow.AddDays(-1),
notAfter: DateTimeOffset.UtcNow.AddDays(1));
if (includePrivateKey)
{
return certificate;
}
return X509CertificateLoader.LoadCertificate(certificate.Export(X509ContentType.Cert));
}
private static X509Certificate2 CreateEncryptionCertificate(bool includePrivateKey)
=> CreateCertificate(X509KeyUsageFlags.KeyEncipherment, includePrivateKey);
private static X509Certificate2 CreateSigningCertificate(bool includePrivateKey)
=> CreateCertificate(X509KeyUsageFlags.DigitalSignature, includePrivateKey);
private static IServiceCollection CreateServices()
{
return new ServiceCollection().AddOptions();

555
test/OpenIddict.Server.Tests/OpenIddictServerConfigurationTests.cs

@ -0,0 +1,555 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using Xunit;
namespace OpenIddict.Server.Tests;
public class OpenIddictServerConfigurationTests
{
[Fact]
public void Constructor_ThrowsAnExceptionForNullProvider()
{
// Arrange
var provider = (IServiceProvider) null!;
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => new OpenIddictServerConfiguration(provider));
Assert.Equal("provider", exception.ParamName);
}
[Fact]
public void PostConfigure_ThrowsAnExceptionForNullOptions()
{
// Arrange
var configuration = new OpenIddictServerConfiguration(new ServiceCollection().BuildServiceProvider());
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => configuration.PostConfigure(name: null, options: null!));
Assert.Equal("options", exception.ParamName);
}
[Fact]
public void PostConfigure_SetsTimeProviderToSystemWhenNotRegistered()
{
// Arrange
var configuration = new OpenIddictServerConfiguration(new ServiceCollection().BuildServiceProvider());
var options = new OpenIddictServerOptions();
// Act
configuration.PostConfigure(name: null, options);
// Assert
Assert.Same(TimeProvider.System, options.TimeProvider);
}
[Fact]
public void PostConfigure_UsesRegisteredTimeProvider()
{
// Arrange
var timeProvider = new FakeTimeProvider();
var services = new ServiceCollection();
services.AddSingleton<TimeProvider>(timeProvider);
var configuration = new OpenIddictServerConfiguration(services.BuildServiceProvider());
var options = new OpenIddictServerOptions();
// Act
configuration.PostConfigure(name: null, options);
// Assert
Assert.Same(timeProvider, options.TimeProvider);
}
[Fact]
public void PostConfigure_DoesNotOverrideExplicitlySetTimeProvider()
{
// Arrange
var explicitProvider = new FakeTimeProvider();
var registeredProvider = new FakeTimeProvider();
var services = new ServiceCollection();
services.AddSingleton<TimeProvider>(registeredProvider);
var configuration = new OpenIddictServerConfiguration(services.BuildServiceProvider());
var options = new OpenIddictServerOptions { TimeProvider = explicitProvider };
// Act
configuration.PostConfigure(name: null, options);
// Assert
Assert.Same(explicitProvider, options.TimeProvider);
}
[Fact]
public void PostConfigure_DisablesFeaturesWhenDegradedModeIsEnabled()
{
// Arrange
var configuration = new OpenIddictServerConfiguration(new ServiceCollection().BuildServiceProvider());
var options = new OpenIddictServerOptions
{
EnableDegradedMode = true,
EnableAuthorizationRequestCaching = true,
EnableEndSessionRequestCaching = true,
UseReferenceAccessTokens = true,
UseReferenceRefreshTokens = true
};
// Act
configuration.PostConfigure(name: null, options);
// Assert
Assert.True(options.DisableAuthorizationStorage);
Assert.True(options.DisableTokenStorage);
Assert.True(options.DisableRollingRefreshTokens);
Assert.False(options.EnableAuthorizationRequestCaching);
Assert.False(options.EnableEndSessionRequestCaching);
Assert.True(options.IgnoreEndpointPermissions);
Assert.True(options.IgnoreGrantTypePermissions);
Assert.True(options.IgnoreResponseTypePermissions);
Assert.True(options.IgnoreScopePermissions);
Assert.False(options.UseReferenceAccessTokens);
Assert.False(options.UseReferenceRefreshTokens);
}
[Fact]
public void PostConfigure_DisablesUserCodeFormattingWhenTokenStorageIsDisabled()
{
// Arrange
var configuration = new OpenIddictServerConfiguration(new ServiceCollection().BuildServiceProvider());
var options = new OpenIddictServerOptions
{
DisableTokenStorage = true
};
// Act
configuration.PostConfigure(name: null, options);
// Assert
Assert.True(options.DisableRollingRefreshTokens);
Assert.Equal(0, options.UserCodeLength);
Assert.Empty(options.UserCodeCharset);
Assert.Null(options.UserCodeDisplayFormat);
}
[Fact]
public void Validate_ThrowsAnExceptionForNullOptions()
{
// Arrange
var configuration = new OpenIddictServerConfiguration(new ServiceCollection().BuildServiceProvider());
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => configuration.Validate(name: null, options: null!));
Assert.Equal("options", exception.ParamName);
}
[Fact]
public void Validate_ReturnsAnErrorWhenJsonWebTokenHandlerIsMissing()
{
// Arrange
var configuration = new OpenIddictServerConfiguration(new ServiceCollection().BuildServiceProvider());
var options = CreateBaseOptions();
options.JsonWebTokenHandler = null!;
// Act
var result = configuration.Validate(name: null, options);
// Assert
Assert.Contains(SR.GetResourceString(SR.ID0075), result.Failures!);
}
[Fact]
public void Validate_ReturnsAnErrorWhenNoFlowIsEnabled()
{
// Arrange
var configuration = new OpenIddictServerConfiguration(new ServiceCollection().BuildServiceProvider());
var options = CreateBaseOptions();
// Act
var result = configuration.Validate(name: null, options);
// Assert
Assert.Contains(SR.GetResourceString(SR.ID0076), result.Failures!);
}
[Fact]
public void Validate_ReturnsAnErrorWhenEndpointUrisAreNotUnique()
{
// Arrange
var configuration = new OpenIddictServerConfiguration(new ServiceCollection().BuildServiceProvider());
var options = CreateBaseOptions();
var uri = new Uri("https://www.contoso.com/connect/shared");
options.AuthorizationEndpointUris.Add(uri);
options.TokenEndpointUris.Add(uri);
// Act
var result = configuration.Validate(name: null, options);
// Assert
Assert.Contains(SR.GetResourceString(SR.ID0285), result.Failures!);
}
[Fact]
public void Validate_ReturnsAnErrorWhenAuthorizationEndpointIsMissingForAuthorizationCodeGrant()
{
// Arrange
var configuration = new OpenIddictServerConfiguration(new ServiceCollection().BuildServiceProvider());
var options = CreateBaseOptions();
options.GrantTypes.Add(GrantTypes.AuthorizationCode);
// Act
var result = configuration.Validate(name: null, options);
// Assert
Assert.Contains(SR.GetResourceString(SR.ID0077), result.Failures!);
}
[Fact]
public void Validate_ReturnsAnErrorWhenTokenEndpointIsMissingForPasswordGrant()
{
// Arrange
var configuration = new OpenIddictServerConfiguration(new ServiceCollection().BuildServiceProvider());
var options = CreateBaseOptions();
options.GrantTypes.Add(GrantTypes.Password);
// Act
var result = configuration.Validate(name: null, options);
// Assert
Assert.Contains(SR.GetResourceString(SR.ID0079), result.Failures!);
}
[Fact]
public void Validate_ReturnsAnErrorWhenDeviceVerificationEndpointIsMissingForDeviceGrant()
{
// Arrange
var configuration = new OpenIddictServerConfiguration(new ServiceCollection().BuildServiceProvider());
var options = CreateBaseOptions();
options.GrantTypes.Add(GrantTypes.DeviceCode);
// Act
var result = configuration.Validate(name: null, options);
// Assert
Assert.Contains(SR.GetResourceString(SR.ID0080), result.Failures!);
}
[Fact]
public void Validate_ReturnsAnErrorWhenDeviceEndpointIsEnabledWithoutDeviceGrant()
{
// Arrange
var configuration = new OpenIddictServerConfiguration(new ServiceCollection().BuildServiceProvider());
var options = CreateBaseOptions();
options.DeviceAuthorizationEndpointUris.Add(new Uri("https://www.contoso.com/connect/device"));
// Act
var result = configuration.Validate(name: null, options);
// Assert
Assert.Contains(SR.GetResourceString(SR.ID0084), result.Failures!);
}
[Fact]
public void Validate_ReturnsAnErrorWhenNoClientAuthenticationMethodIsEnabledForNonInteractiveEndpoints()
{
// Arrange
var configuration = new OpenIddictServerConfiguration(new ServiceCollection().BuildServiceProvider());
var options = CreateBaseOptions();
options.TokenEndpointUris.Add(new Uri("https://www.contoso.com/connect/token"));
options.ClientAuthenticationMethods.Clear();
// Act
var result = configuration.Validate(name: null, options);
// Assert
Assert.Contains(SR.GetResourceString(SR.ID0419), result.Failures!);
}
[Fact]
public void Validate_ReturnsAnErrorWhenPrivateKeyJwtIsEnabledWithoutJwtBearerAssertionType()
{
// Arrange
var configuration = new OpenIddictServerConfiguration(new ServiceCollection().BuildServiceProvider());
var options = CreateBaseOptions();
options.ClientAuthenticationMethods.Clear();
options.ClientAuthenticationMethods.Add(ClientAuthenticationMethods.PrivateKeyJwt);
options.ClientAssertionTypes.Clear();
// Act
var result = configuration.Validate(name: null, options);
// Assert
Assert.Contains(SR.FormatID0420(ClientAssertionTypes.JwtBearer, ClientAuthenticationMethods.PrivateKeyJwt), result.Failures!);
}
[Fact]
public void Validate_ReturnsAnErrorWhenTlsClientAuthMethodIsEnabledWithoutPolicy()
{
// Arrange
var configuration = new OpenIddictServerConfiguration(new ServiceCollection().BuildServiceProvider());
var options = CreateBaseOptions();
options.ClientAuthenticationMethods.Add(ClientAuthenticationMethods.TlsClientAuth);
// Act
var result = configuration.Validate(name: null, options);
// Assert
Assert.Contains(SR.GetResourceString(SR.ID0505), result.Failures!);
}
[Fact]
public void Validate_ReturnsAnErrorWhenSelfSignedTlsClientAuthMethodIsEnabledWithoutPolicy()
{
// Arrange
var configuration = new OpenIddictServerConfiguration(new ServiceCollection().BuildServiceProvider());
var options = CreateBaseOptions();
options.ClientAuthenticationMethods.Add(ClientAuthenticationMethods.SelfSignedTlsClientAuth);
// Act
var result = configuration.Validate(name: null, options);
// Assert
Assert.Contains(SR.GetResourceString(SR.ID0506), result.Failures!);
}
[Fact]
public void Validate_ReturnsAnErrorWhenSubjectTypesAreMissing()
{
// Arrange
var configuration = new OpenIddictServerConfiguration(new ServiceCollection().BuildServiceProvider());
var options = CreateBaseOptions();
options.SubjectTypes.Clear();
// Act
var result = configuration.Validate(name: null, options);
// Assert
Assert.Contains(SR.GetResourceString(SR.ID0421), result.Failures!);
}
[Fact]
public void Validate_ReturnsAnErrorWhenReferenceTokensAreEnabledWithDisabledTokenStorage()
{
// Arrange
var configuration = new OpenIddictServerConfiguration(new ServiceCollection().BuildServiceProvider());
var options = CreateBaseOptions();
options.DisableTokenStorage = true;
options.UseReferenceAccessTokens = true;
// Act
var result = configuration.Validate(name: null, options);
// Assert
Assert.Contains(SR.GetResourceString(SR.ID0083), result.Failures!);
}
[Fact]
public void Validate_ReturnsAnErrorWhenRequestCachingIsEnabledWithDisabledTokenStorage()
{
// Arrange
var configuration = new OpenIddictServerConfiguration(new ServiceCollection().BuildServiceProvider());
var options = CreateBaseOptions();
options.DisableTokenStorage = true;
options.EnableAuthorizationRequestCaching = true;
// Act
var result = configuration.Validate(name: null, options);
// Assert
Assert.Contains(SR.GetResourceString(SR.ID0465), result.Failures!);
}
[Fact]
public void Validate_ReturnsAnErrorWhenDeviceGrantIsEnabledWithDisabledTokenStorageOutsideDegradedMode()
{
// Arrange
var configuration = new OpenIddictServerConfiguration(new ServiceCollection().BuildServiceProvider());
var options = CreateBaseOptions();
options.DisableTokenStorage = true;
options.EnableDegradedMode = false;
options.GrantTypes.Add(GrantTypes.DeviceCode);
// Act
var result = configuration.Validate(name: null, options);
// Assert
Assert.Contains(SR.GetResourceString(SR.ID0367), result.Failures!);
}
[Fact]
public void Validate_ReturnsAnErrorWhenTokenExchangeGrantIsEnabledWithoutSubjectTokenTypes()
{
// Arrange
var configuration = new OpenIddictServerConfiguration(new ServiceCollection().BuildServiceProvider());
var options = CreateBaseOptions();
options.GrantTypes.Add(GrantTypes.TokenExchange);
options.SubjectTokenTypes.Clear();
// Act
var result = configuration.Validate(name: null, options);
// Assert
Assert.Contains(SR.GetResourceString(SR.ID0486), result.Failures!);
}
[Fact]
public void Validate_ReturnsAnErrorWhenDefaultRequestedTokenTypeIsMissing()
{
// Arrange
var configuration = new OpenIddictServerConfiguration(new ServiceCollection().BuildServiceProvider());
var options = CreateBaseOptions();
options.DefaultRequestedTokenType = string.Empty;
// Act
var result = configuration.Validate(name: null, options);
// Assert
Assert.Contains(SR.GetResourceString(SR.ID0490), result.Failures!);
}
[Fact]
public void Validate_ReturnsAnErrorWhenDefaultRequestedTokenTypeIsNotAllowed()
{
// Arrange
var configuration = new OpenIddictServerConfiguration(new ServiceCollection().BuildServiceProvider());
var options = CreateBaseOptions();
options.RequestedTokenTypes.Clear();
options.DefaultRequestedTokenType = TokenTypeIdentifiers.AccessToken;
// Act
var result = configuration.Validate(name: null, options);
// Assert
Assert.Contains(SR.GetResourceString(SR.ID0492), result.Failures!);
}
[Fact]
public void Validate_ReturnsAnErrorWhenEncryptionCredentialsAreMissing()
{
// Arrange
var configuration = new OpenIddictServerConfiguration(new ServiceCollection().BuildServiceProvider());
var options = CreateBaseOptions();
// Act
var result = configuration.Validate(name: null, options);
// Assert
Assert.Contains(SR.GetResourceString(SR.ID0085), result.Failures!);
}
[Fact]
public void Validate_ReturnsAnErrorWhenNoAsymmetricSigningCredentialIsRegistered()
{
// Arrange
var configuration = new OpenIddictServerConfiguration(new ServiceCollection().BuildServiceProvider());
var options = CreateBaseOptions();
options.EncryptionCredentials.Add(new EncryptingCredentials(
new SymmetricSecurityKey(new byte[32]),
SecurityAlgorithms.Aes256KW,
SecurityAlgorithms.Aes256CbcHmacSha512));
options.SigningCredentials.Add(new SigningCredentials(
new SymmetricSecurityKey(new byte[32]),
SecurityAlgorithms.HmacSha256));
// Act
var result = configuration.Validate(name: null, options);
// Assert
Assert.Contains(SR.GetResourceString(SR.ID0086), result.Failures!);
}
[Fact]
public void Validate_ReturnsAnErrorWhenMtlsEndpointAliasIsInvalid()
{
// Arrange
var configuration = new OpenIddictServerConfiguration(new ServiceCollection().BuildServiceProvider());
var options = CreateBaseOptions();
options.MtlsTokenEndpointAliasUri = new Uri("http://www.contoso.com/connect/token", UriKind.Absolute);
// Act
var result = configuration.Validate(name: null, options);
// Assert
Assert.Contains(SR.GetResourceString(SR.ID0499), result.Failures!);
}
[Fact]
public void Validate_ReturnsAnErrorWhenMtlsEndpointAliasIsConfiguredWithoutEndpoint()
{
// Arrange
var configuration = new OpenIddictServerConfiguration(new ServiceCollection().BuildServiceProvider());
var options = CreateBaseOptions();
options.MtlsTokenEndpointAliasUri = new Uri("https://www.contoso.com/connect/token", UriKind.Absolute);
// Act
var result = configuration.Validate(name: null, options);
// Assert
Assert.Contains(SR.GetResourceString(SR.ID0510), result.Failures!);
}
[Fact]
public void Validate_ReturnsAnErrorWhenMtlsEndpointAliasIsConfiguredWithoutIssuer()
{
// Arrange
var configuration = new OpenIddictServerConfiguration(new ServiceCollection().BuildServiceProvider());
var options = CreateBaseOptions();
options.TokenEndpointUris.Add(new Uri("https://www.contoso.com/connect/token"));
options.MtlsTokenEndpointAliasUri = new Uri("https://mtls.contoso.com/connect/token", UriKind.Absolute);
// Act
var result = configuration.Validate(name: null, options);
// Assert
Assert.Contains(SR.GetResourceString(SR.ID0500), result.Failures!);
}
[Fact]
public void Validate_ReturnsAnErrorWhenDegradedModeIsEnabledAndCustomTokenEndpointHandlersAreMissing()
{
// Arrange
var configuration = new OpenIddictServerConfiguration(new ServiceCollection().BuildServiceProvider());
var options = CreateBaseOptions();
options.EnableDegradedMode = true;
options.TokenEndpointUris.Add(new Uri("https://www.contoso.com/connect/token"));
// Act
var result = configuration.Validate(name: null, options);
// Assert
Assert.Contains(SR.GetResourceString(SR.ID0094), result.Failures!);
}
[Fact]
public void Validate_ReturnsAnErrorWhenUserCodeLengthIsTooShort()
{
// Arrange
var configuration = new OpenIddictServerConfiguration(new ServiceCollection().BuildServiceProvider());
var options = CreateBaseOptions();
options.UserCodeLength = 5;
// Act
var result = configuration.Validate(name: null, options);
// Assert
Assert.Contains(SR.FormatID0439(6), result.Failures!);
}
private static OpenIddictServerOptions CreateBaseOptions()
=> new()
{
TimeProvider = TimeProvider.System
};
private sealed class FakeTimeProvider : TimeProvider;
}

1
test/OpenIddict.Validation.Tests/OpenIddict.Validation.Tests.csproj

@ -11,6 +11,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Moq" />
<PackageReference Include="Xunit.SkippableFact" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFrameworkIdentifier)' == '.NETFramework' ">

1126
test/OpenIddict.Validation.Tests/OpenIddictValidationBuilderTests.cs

File diff suppressed because it is too large

372
test/OpenIddict.Validation.Tests/OpenIddictValidationConfigurationTests.cs

@ -0,0 +1,372 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Tokens;
using Moq;
using Xunit;
using static OpenIddict.Validation.OpenIddictValidationEvents;
namespace OpenIddict.Validation.Tests;
public class OpenIddictValidationConfigurationTests
{
[Fact]
public void Constructor_ThrowsAnExceptionForNullProvider()
{
// Arrange
var provider = (IServiceProvider) null!;
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => new OpenIddictValidationConfiguration(provider));
Assert.Equal("provider", exception.ParamName);
}
[Fact]
public void PostConfigure_ThrowsAnExceptionForNullOptions()
{
// Arrange
var configuration = new OpenIddictValidationConfiguration(new ServiceCollection().BuildServiceProvider());
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => configuration.PostConfigure(name: null, options: null!));
Assert.Equal("options", exception.ParamName);
}
[Fact]
public void PostConfigure_SetsTimeProviderToSystemWhenNotRegistered()
{
// Arrange
var configuration = new OpenIddictValidationConfiguration(new ServiceCollection().BuildServiceProvider());
var options = new OpenIddictValidationOptions();
// Act
configuration.PostConfigure(name: null, options);
// Assert
Assert.Same(TimeProvider.System, options.TimeProvider);
}
[Fact]
public void PostConfigure_UsesRegisteredTimeProvider()
{
// Arrange
var timeProvider = new FakeTimeProvider();
var services = new ServiceCollection();
services.AddSingleton<TimeProvider>(timeProvider);
var configuration = new OpenIddictValidationConfiguration(services.BuildServiceProvider());
var options = new OpenIddictValidationOptions();
// Act
configuration.PostConfigure(name: null, options);
// Assert
Assert.Same(timeProvider, options.TimeProvider);
}
[Fact]
public void PostConfigure_DoesNotOverrideExplicitlySetTimeProvider()
{
// Arrange
var explicitProvider = new FakeTimeProvider();
var registeredProvider = new FakeTimeProvider();
var services = new ServiceCollection();
services.AddSingleton<TimeProvider>(registeredProvider);
var configuration = new OpenIddictValidationConfiguration(services.BuildServiceProvider());
var options = new OpenIddictValidationOptions { TimeProvider = explicitProvider };
// Act
configuration.PostConfigure(name: null, options);
// Assert
Assert.Same(explicitProvider, options.TimeProvider);
}
[Fact]
public void PostConfigure_CreatesStaticConfigurationManagerFromStaticConfiguration()
{
// Arrange
var configuration = new OpenIddictValidationConfiguration(new ServiceCollection().BuildServiceProvider());
var options = new OpenIddictValidationOptions
{
Issuer = new Uri("https://www.contoso.com/"),
Configuration = new OpenIddictConfiguration()
};
// Act
configuration.PostConfigure(name: null, options);
// Assert
Assert.IsType<StaticConfigurationManager<OpenIddictConfiguration>>(options.ConfigurationManager);
Assert.Equal(options.Issuer, options.Configuration.Issuer);
}
[Fact]
public void PostConfigure_AttachesEncryptionKeysToTokenValidationParameters()
{
// Arrange
var configuration = new OpenIddictValidationConfiguration(new ServiceCollection().BuildServiceProvider());
var options = new OpenIddictValidationOptions();
var key = new SymmetricSecurityKey(new byte[32]);
options.EncryptionCredentials.Add(new EncryptingCredentials(
key,
SecurityAlgorithms.Aes256KW,
SecurityAlgorithms.Aes256CbcHmacSha512));
// Act
configuration.PostConfigure(name: null, options);
// Assert
Assert.Contains(key, options.TokenValidationParameters.TokenDecryptionKeys);
}
[Fact]
public void Validate_ThrowsAnExceptionForNullOptions()
{
// Arrange
var configuration = new OpenIddictValidationConfiguration(new ServiceCollection().BuildServiceProvider());
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => configuration.Validate(name: null, options: null!));
Assert.Equal("options", exception.ParamName);
}
[Fact]
public void Validate_ReturnsAnErrorWhenJsonWebTokenHandlerIsMissing()
{
// Arrange
var configuration = new OpenIddictValidationConfiguration(new ServiceCollection().BuildServiceProvider());
var options = CreateBaseOptions();
options.JsonWebTokenHandler = null!;
// Act
var result = configuration.Validate(name: null, options);
// Assert
Assert.Contains(SR.GetResourceString(SR.ID0075), result.Failures!);
}
[Fact]
public void Validate_ReturnsAnErrorWhenNoIssuerOrConfigurationInformationIsProvided()
{
// Arrange
var configuration = new OpenIddictValidationConfiguration(new ServiceCollection().BuildServiceProvider());
var options = CreateBaseOptions();
// Act
var result = configuration.Validate(name: null, options);
// Assert
Assert.Contains(SR.GetResourceString(SR.ID0128), result.Failures!);
}
[Fact]
public void Validate_ReturnsAnErrorWhenIssuerIsInvalid()
{
// Arrange
var configuration = new OpenIddictValidationConfiguration(new ServiceCollection().BuildServiceProvider());
var options = CreateBaseOptions();
options.Issuer = new Uri("/relative", UriKind.Relative);
// Act
var result = configuration.Validate(name: null, options);
// Assert
Assert.Contains(SR.GetResourceString(SR.ID0136), result.Failures!);
}
[Fact]
public void Validate_ReturnsAnErrorWhenIssuerContainsQueryOrFragment()
{
// Arrange
var configuration = new OpenIddictValidationConfiguration(new ServiceCollection().BuildServiceProvider());
var options = CreateBaseOptions();
options.Issuer = new Uri("https://www.contoso.com/?query=parameter#fragment");
// Act
var result = configuration.Validate(name: null, options);
// Assert
Assert.Contains(SR.GetResourceString(SR.ID0137), result.Failures!);
}
[Fact]
public void Validate_ReturnsAnErrorWhenConfigurationIssuerDoesNotMatchOptionsIssuer()
{
// Arrange
var configuration = new OpenIddictValidationConfiguration(new ServiceCollection().BuildServiceProvider());
var options = CreateBaseOptions();
options.Issuer = new Uri("https://www.contoso.com/");
options.Configuration = new OpenIddictConfiguration
{
Issuer = new Uri("https://www.fabrikam.com/")
};
// Act
var result = configuration.Validate(name: null, options);
// Assert
Assert.Contains(SR.GetResourceString(SR.ID0394), result.Failures!);
}
[Fact]
public void Validate_ReturnsAnErrorWhenConfigurationManagerIsMissing()
{
// Arrange
var configuration = new OpenIddictValidationConfiguration(new ServiceCollection().BuildServiceProvider());
var options = CreateBaseOptions();
options.Issuer = new Uri("https://www.contoso.com/");
// Act
var result = configuration.Validate(name: null, options);
// Assert
Assert.Contains(SR.GetResourceString(SR.ID0523), result.Failures!);
}
[Fact]
public void Validate_ReturnsAnErrorWhenNonStaticConfigurationManagerIsUsedWithoutRequiredHandlers()
{
// Arrange
var configuration = new OpenIddictValidationConfiguration(new ServiceCollection().BuildServiceProvider());
var options = CreateBaseOptions();
options.Issuer = new Uri("https://www.contoso.com/");
options.ConfigurationManager = Mock.Of<IConfigurationManager<OpenIddictConfiguration>>();
// Act
var result = configuration.Validate(name: null, options);
// Assert
Assert.Contains(SR.GetResourceString(SR.ID0135), result.Failures!);
}
[Fact]
public void Validate_ReturnsAnErrorWhenIntrospectionHandlersAreMissing()
{
// Arrange
var configuration = new OpenIddictValidationConfiguration(new ServiceCollection().BuildServiceProvider());
var options = CreateBaseOptions();
options.ValidationType = OpenIddictValidationType.Introspection;
options.Issuer = new Uri("https://www.contoso.com/");
options.ConfigurationEndpoint = new Uri("https://www.contoso.com/.well-known/openid-configuration");
options.ClientId = "client_id";
options.ClientSecret = "client_secret";
options.ConfigurationManager = new StaticConfigurationManager<OpenIddictConfiguration>(new OpenIddictConfiguration());
// Act
var result = configuration.Validate(name: null, options);
// Assert
Assert.Contains(SR.GetResourceString(SR.ID0129), result.Failures!);
}
[Fact]
public void Validate_ReturnsAnErrorWhenIntrospectionIssuerAndConfigurationEndpointAreMissing()
{
// Arrange
var configuration = new OpenIddictValidationConfiguration(new ServiceCollection().BuildServiceProvider());
var options = CreateBaseOptions();
options.ValidationType = OpenIddictValidationType.Introspection;
options.ClientId = "client_id";
options.ClientSecret = "client_secret";
options.ConfigurationManager = new StaticConfigurationManager<OpenIddictConfiguration>(new OpenIddictConfiguration());
options.Handlers.Add(OpenIddictValidationHandlerDescriptor.CreateBuilder<ApplyIntrospectionRequestContext>()
.UseSingletonHandler<CustomIntrospectionHandler>().Build());
// Act
var result = configuration.Validate(name: null, options);
// Assert
Assert.Contains(SR.GetResourceString(SR.ID0130), result.Failures!);
}
[Fact]
public void Validate_ReturnsAnErrorWhenIntrospectionClientCredentialsAreMissing()
{
// Arrange
var configuration = new OpenIddictValidationConfiguration(new ServiceCollection().BuildServiceProvider());
var options = CreateBaseOptions();
options.ValidationType = OpenIddictValidationType.Introspection;
options.Issuer = new Uri("https://www.contoso.com/");
options.ConfigurationEndpoint = new Uri("https://www.contoso.com/.well-known/openid-configuration");
options.ConfigurationManager = new StaticConfigurationManager<OpenIddictConfiguration>(new OpenIddictConfiguration());
// Act
var result = configuration.Validate(name: null, options);
// Assert
Assert.Contains(SR.GetResourceString(SR.ID0131), result.Failures!);
Assert.Contains(SR.GetResourceString(SR.ID0132), result.Failures!);
}
[Fact]
public void Validate_ReturnsAnErrorWhenAuthorizationOrTokenEntryValidationIsEnabledInIntrospectionMode()
{
// Arrange
var configuration = new OpenIddictValidationConfiguration(new ServiceCollection().BuildServiceProvider());
var options = CreateBaseOptions();
options.ValidationType = OpenIddictValidationType.Introspection;
options.Issuer = new Uri("https://www.contoso.com/");
options.ConfigurationEndpoint = new Uri("https://www.contoso.com/.well-known/openid-configuration");
options.ClientId = "client_id";
options.ClientSecret = "client_secret";
options.EnableAuthorizationEntryValidation = true;
options.EnableTokenEntryValidation = true;
options.ConfigurationManager = new StaticConfigurationManager<OpenIddictConfiguration>(new OpenIddictConfiguration());
// Act
var result = configuration.Validate(name: null, options);
// Assert
Assert.Contains(SR.GetResourceString(SR.ID0133), result.Failures!);
Assert.Contains(SR.GetResourceString(SR.ID0134), result.Failures!);
}
[Fact]
public void Validate_SucceedsForValidIntrospectionConfigurationUsingClientAssertion()
{
// Arrange
var configuration = new OpenIddictValidationConfiguration(new ServiceCollection().BuildServiceProvider());
var options = CreateBaseOptions();
options.ValidationType = OpenIddictValidationType.Introspection;
options.Issuer = new Uri("https://www.contoso.com/");
options.ConfigurationEndpoint = new Uri("https://www.contoso.com/.well-known/openid-configuration");
options.ClientId = "client_id";
options.ConfigurationManager = new StaticConfigurationManager<OpenIddictConfiguration>(new OpenIddictConfiguration());
options.SigningCredentials.Add(new SigningCredentials(
new SymmetricSecurityKey(new byte[32]),
SecurityAlgorithms.HmacSha256));
options.Handlers.Add(OpenIddictValidationHandlerDescriptor.CreateBuilder<ApplyIntrospectionRequestContext>()
.UseSingletonHandler<CustomIntrospectionHandler>().Build());
// Act
var result = configuration.Validate(name: null, options);
// Assert
Assert.True(result.Succeeded);
}
private static OpenIddictValidationOptions CreateBaseOptions()
=> new()
{
TimeProvider = TimeProvider.System
};
private sealed class CustomIntrospectionHandler : IOpenIddictValidationHandler<ApplyIntrospectionRequestContext>
{
public ValueTask HandleAsync(ApplyIntrospectionRequestContext context) => ValueTask.CompletedTask;
}
private sealed class FakeTimeProvider : TimeProvider;
}
Loading…
Cancel
Save