Versatile OpenID Connect stack for ASP.NET Core and Microsoft.Owin (compatible with ASP.NET 4.6.1)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

1522 lines
50 KiB

using System.Reflection;
using System.Runtime.Versioning;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using Moq;
using Xunit;
using static OpenIddict.Client.OpenIddictClientEvents;
namespace OpenIddict.Client.Tests;
public class OpenIddictClientBuilderTests
{
[Fact]
public void Constructor_ThrowsAnExceptionForNullServices()
{
// Arrange
var services = (IServiceCollection) null!;
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => new OpenIddictClientBuilder(services));
Assert.Equal("services", exception.ParamName);
}
[Fact]
public void ValidateOnStart_CanBeInvoked()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act and assert
Assert.Same(builder, builder.ValidateOnStart());
}
[Fact]
public void AddEventHandler_ThrowsAnExceptionWhenConfigurationIsNull()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => builder.AddEventHandler<BaseContext>(configuration: null!));
Assert.Equal("configuration", exception.ParamName);
}
[Fact]
public void AddEventHandler_ThrowsAnExceptionWhenDescriptorIsNull()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => builder.AddEventHandler(descriptor: null!));
Assert.Equal("descriptor", exception.ParamName);
}
[Fact]
public void AddEventHandler_HandlerIsAttached()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.AddEventHandler<CustomContext>(options => options.UseSingletonHandler<CustomHandler>());
// Assert
Assert.Contains(services, service => service.ServiceType == typeof(CustomHandler));
}
[Fact]
public void AddEventHandler_HandlerInstanceIsRegistered()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.AddEventHandler<CustomContext>(options => options.UseSingletonHandler(new CustomHandler()));
// Assert
Assert.Contains(services, service =>
service.ServiceType == typeof(CustomHandler) &&
service.ImplementationInstance?.GetType() == typeof(CustomHandler) &&
service.Lifetime == ServiceLifetime.Singleton);
}
[Fact]
public void RemoveEventHandler_ThrowsAnExceptionWhenDescriptorIsNull()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => builder.RemoveEventHandler(descriptor: null!));
Assert.Equal("descriptor", exception.ParamName);
}
[Fact]
public void RemoveEventHandler_RemovesService()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
OpenIddictClientHandlerDescriptor descriptor = OpenIddictClientHandlerDescriptor
.CreateBuilder<CustomContext>().UseSingletonHandler<CustomHandler>().Build();
builder.AddEventHandler(descriptor);
// Act
builder.RemoveEventHandler(descriptor);
var options = GetOptions(services);
// Assert
Assert.DoesNotContain(services, service => service.ServiceType == descriptor.ServiceDescriptor.ServiceType);
Assert.DoesNotContain(options.Handlers, handler => handler.ServiceDescriptor.ServiceType == descriptor.ServiceDescriptor.ServiceType);
}
[Fact]
public void Configure_DelegateIsCorrectlyRegistered()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
var configuration = new Action<OpenIddictClientOptions>(_ => { });
// Act
builder.Configure(configuration);
// Assert
Assert.Contains(services, service => service.ServiceType == typeof(IConfigureOptions<OpenIddictClientOptions>) &&
service.ImplementationInstance is ConfigureNamedOptions<OpenIddictClientOptions> options &&
options.Action == configuration && string.IsNullOrEmpty(options.Name));
}
[Fact]
public void Configure_ThrowsAnExceptionWhenConfigurationIsNull()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => builder.Configure(configuration: null!));
Assert.Equal("configuration", exception.ParamName);
}
[Fact]
public void AddEncryptionCredentials_ThrowsExceptionWhenCredentialsAreNull()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => builder.AddEncryptionCredentials(credentials: null!));
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()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => builder.AddEncryptionKey(key: null!));
Assert.Equal("key", exception.ParamName);
}
[Fact]
public void AddEncryptionKey_ThrowsExceptionWhenAsymmetricKeyPrivateKeyIsMissing()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
var key = Mock.Of<AsymmetricSecurityKey>(key => key.PrivateKeyStatus == PrivateKeyStatus.DoesNotExist);
// Act and assert
var exception = Assert.Throws<InvalidOperationException>(() => builder.AddEncryptionKey(key));
Assert.Equal("The asymmetric encryption key doesn't contain the required private key.", exception.Message);
}
[Fact]
public void AddEncryptionKey_EncryptingKeyIsCorrectlyAdded()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
var key = Mock.Of<SecurityKey>(mock => mock.KeySize == 256 && mock.IsSupportedAlgorithm(SecurityAlgorithms.Aes256KW));
// Act
builder.AddEncryptionKey(key);
var options = GetOptions(services);
// Assert
Assert.Same(key, options.EncryptionCredentials[0].Key);
}
[Fact]
public void AddEncryptionKey_ThrowsExceptionWhenSymmetricKeyIsTooShort()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
var key = Mock.Of<SecurityKey>(mock => mock.KeySize == 128 && mock.IsSupportedAlgorithm(SecurityAlgorithms.Aes256KW));
// Act and assert
var exception = Assert.Throws<InvalidOperationException>(() => builder.AddEncryptionKey(key));
Assert.Equal(SR.FormatID0283(256, 128), exception.Message);
}
[Fact]
public void AddEncryptionKey_ThrowsExceptionWhenSymmetricKeyIsTooLong()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
var key = Mock.Of<SecurityKey>(mock => mock.KeySize == 384 && mock.IsSupportedAlgorithm(SecurityAlgorithms.Aes256KW));
// Act and assert
var exception = Assert.Throws<InvalidOperationException>(() => builder.AddEncryptionKey(key));
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 AddDevelopmentEncryptionCertificate_ThrowsAnExceptionForNullSubject()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => builder.AddDevelopmentEncryptionCertificate(subject: null!));
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(OpenIddictClientBuilderTests).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(OpenIddictClientBuilderTests).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 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 AddSigningKey_ThrowsExceptionWhenKeyIsNull()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => builder.AddSigningKey(key: null!));
Assert.Equal("key", exception.ParamName);
}
[Fact]
public void AddSigningKey_ThrowsExceptionWhenAsymmetricKeyPrivateKeyIsMissing()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
var key = Mock.Of<AsymmetricSecurityKey>(key => key.PrivateKeyStatus == PrivateKeyStatus.DoesNotExist);
// Act and assert
var exception = Assert.Throws<InvalidOperationException>(() => builder.AddSigningKey(key));
Assert.Equal("The asymmetric signing key doesn't contain the required private key.", exception.Message);
}
[Theory]
[InlineData(SecurityAlgorithms.HmacSha256)]
[InlineData(SecurityAlgorithms.RsaSha256)]
[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
var services = CreateServices();
var builder = CreateBuilder(services);
var key = Mock.Of<SecurityKey>(mock => mock.IsSupportedAlgorithm(algorithm));
// Act
builder.AddSigningKey(key);
var options = GetOptions(services);
// Assert
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 AddDevelopmentSigningCertificate_ThrowsAnExceptionForNullSubject()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => builder.AddDevelopmentSigningCertificate(subject: null!));
Assert.Equal("subject", exception.ParamName);
}
[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_DefaultSigningKeyIsCorrectlyAdded()
{
// 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);
}
[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
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.AddEphemeralSigningKey(algorithm);
var options = GetOptions(services);
var credentials = options.SigningCredentials[0];
// Assert
Assert.Equal(algorithm, credentials.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(OpenIddictClientBuilderTests).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(OpenIddictClientBuilderTests).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 AddRegistration_ThrowsAnExceptionForNullRegistration()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => builder.AddRegistration(registration: null!));
Assert.Equal("registration", exception.ParamName);
}
[Fact]
public void AddRegistration_RegistrationIsCorrectlyAdded()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
var registration = new OpenIddictClientRegistration
{
Issuer = new Uri("https://fabrikam.com/")
};
// Act
builder.AddRegistration(registration);
var options = GetOptions(services);
// Assert
Assert.Contains(registration, options.Registrations);
}
[Fact]
public void DisableTokenStorage_TokenStorageIsDisabled()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.DisableTokenStorage();
var options = GetOptions(services);
// Assert
Assert.True(options.DisableTokenStorage);
}
[Fact]
public void DisableWebServicesFederationClaimMapping_IsDisabled()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.DisableWebServicesFederationClaimMapping();
var options = GetOptions(services);
// Assert
Assert.True(options.DisableWebServicesFederationClaimMapping);
}
[Fact]
public void AllowAuthorizationCodeFlow_CodeFlowIsAdded()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.AllowAuthorizationCodeFlow();
var options = GetOptions(services);
// Assert
Assert.Contains(GrantTypes.AuthorizationCode, options.GrantTypes);
Assert.Contains(ResponseTypes.Code, options.ResponseTypes);
}
[Fact]
public void AllowClientCredentialsFlow_ClientCredentialsFlowIsAdded()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.AllowClientCredentialsFlow();
var options = GetOptions(services);
// Assert
Assert.Contains(GrantTypes.ClientCredentials, options.GrantTypes);
}
[Theory]
[InlineData(null)]
[InlineData("")]
public void AllowCustomFlow_ThrowsAnExceptionForType(string? type)
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act and assert
var exception = Assert.ThrowsAny<ArgumentException>(() => builder.AllowCustomFlow(type!));
Assert.Equal("type", exception.ParamName);
}
[Fact]
public void AllowCustomFlow_CustomFlowIsAdded()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.AllowCustomFlow("urn:ietf:params:oauth:grant-type:custom_grant");
var options = GetOptions(services);
// Assert
Assert.Contains("urn:ietf:params:oauth:grant-type:custom_grant", options.GrantTypes);
}
[Fact]
public void AllowDeviceAuthorizationFlow_DeviceFlowIsAdded()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.AllowDeviceAuthorizationFlow();
var options = GetOptions(services);
// Assert
Assert.Contains(GrantTypes.DeviceCode, options.GrantTypes);
}
[Fact]
public void AllowHybridFlow_HybridFlowIsAdded()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.AllowHybridFlow();
var options = GetOptions(services);
// Assert
Assert.Contains(GrantTypes.AuthorizationCode, options.GrantTypes);
Assert.Contains(GrantTypes.Implicit, options.GrantTypes);
Assert.Contains(ResponseTypes.Code + ' ' + ResponseTypes.IdToken, options.ResponseTypes);
Assert.Contains(ResponseTypes.Code + ' ' + ResponseTypes.IdToken + ' ' + ResponseTypes.Token, options.ResponseTypes);
Assert.Contains(ResponseTypes.Code + ' ' + ResponseTypes.Token, options.ResponseTypes);
}
[Fact]
public void AllowImplicitFlow_ImplicitFlowIsAddedWithoutUnsafeTokenResponseType()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.AllowImplicitFlow();
var options = GetOptions(services);
// Assert
Assert.Contains(GrantTypes.Implicit, options.GrantTypes);
Assert.Contains(ResponseTypes.IdToken, options.ResponseTypes);
Assert.Contains(ResponseTypes.IdToken + ' ' + ResponseTypes.Token, options.ResponseTypes);
Assert.DoesNotContain(ResponseTypes.Token, options.ResponseTypes);
}
[Fact]
public void AllowNoneFlow_NoneFlowIsAdded()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.AllowNoneFlow();
var options = GetOptions(services);
// Assert
Assert.Contains(ResponseTypes.None, options.ResponseTypes);
}
[Fact]
public void AllowPasswordFlow_PasswordFlowIsAdded()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.AllowPasswordFlow();
var options = GetOptions(services);
// Assert
Assert.Contains(GrantTypes.Password, options.GrantTypes);
}
[Fact]
public void AllowRefreshTokenFlow_RefreshTokenFlowIsAdded()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.AllowRefreshTokenFlow();
var options = GetOptions(services);
// Assert
Assert.Contains(GrantTypes.RefreshToken, options.GrantTypes);
}
[Fact]
public void AllowTokenExchangeFlow_TokenExchangeFlowIsAdded()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.AllowTokenExchangeFlow();
var options = GetOptions(services);
// Assert
Assert.Contains(GrantTypes.TokenExchange, options.GrantTypes);
}
[Fact]
public void SetPostLogoutRedirectionEndpointUris_ThrowsExceptionWhenUrisIsNull()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => builder.SetPostLogoutRedirectionEndpointUris(uris: (Uri[]) null!));
Assert.Equal("uris", exception.ParamName);
}
[Fact]
public void SetPostLogoutRedirectionEndpointUris_Strings_ThrowsExceptionWhenUrisIsNull()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => builder.SetPostLogoutRedirectionEndpointUris(uris: (string[]) null!));
Assert.Equal("uris", exception.ParamName);
}
[Theory]
[InlineData(@"C:\")]
public void SetPostLogoutRedirectionEndpointUris_ThrowsExceptionForMalformedUri(string uri)
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act and assert
var exception = Assert.Throws<ArgumentException>(() => builder.SetPostLogoutRedirectionEndpointUris(new Uri(uri)));
Assert.Equal("uris", exception.ParamName);
Assert.Contains(SR.GetResourceString(SR.ID0072), exception.Message);
}
[Theory]
[InlineData("~/path")]
public void SetPostLogoutRedirectionEndpointUris_ThrowsExceptionForInvalidRelativeUri(string uri)
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act and assert
var exception = Assert.Throws<ArgumentException>(() => builder.SetPostLogoutRedirectionEndpointUris(new Uri(uri, UriKind.RelativeOrAbsolute)));
Assert.Equal("uris", exception.ParamName);
Assert.Contains(SR.FormatID0081("~"), exception.Message);
}
[Fact]
public void SetPostLogoutRedirectionEndpointUris_ClearsUris()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.SetPostLogoutRedirectionEndpointUris(Array.Empty<Uri>());
var options = GetOptions(services);
// Assert
Assert.Empty(options.PostLogoutRedirectionEndpointUris);
}
[Fact]
public void SetPostLogoutRedirectionEndpointUris_AddsUri()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.SetPostLogoutRedirectionEndpointUris("http://localhost/postlogout");
var options = GetOptions(services);
// Assert
Assert.Contains(new Uri("http://localhost/postlogout"), options.PostLogoutRedirectionEndpointUris);
}
[Fact]
public void SetRedirectionEndpointUris_ThrowsExceptionWhenUrisIsNull()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => builder.SetRedirectionEndpointUris(uris: (Uri[]) null!));
Assert.Equal("uris", exception.ParamName);
}
[Fact]
public void SetRedirectionEndpointUris_Strings_ThrowsExceptionWhenUrisIsNull()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => builder.SetRedirectionEndpointUris(uris: (string[]) null!));
Assert.Equal("uris", exception.ParamName);
}
[Theory]
[InlineData(@"C:\")]
public void SetRedirectionEndpointUris_ThrowsExceptionForMalformedUri(string uri)
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act and assert
var exception = Assert.Throws<ArgumentException>(() => builder.SetRedirectionEndpointUris(new Uri(uri)));
Assert.Equal("uris", exception.ParamName);
Assert.Contains(SR.GetResourceString(SR.ID0072), exception.Message);
}
[Theory]
[InlineData("~/path")]
public void SetRedirectionEndpointUris_ThrowsExceptionForInvalidRelativeUri(string uri)
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act and assert
var exception = Assert.Throws<ArgumentException>(() => builder.SetRedirectionEndpointUris(new Uri(uri, UriKind.RelativeOrAbsolute)));
Assert.Equal("uris", exception.ParamName);
Assert.Contains(SR.FormatID0081("~"), exception.Message);
}
[Fact]
public void SetRedirectionEndpointUris_ClearsUris()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.SetRedirectionEndpointUris(Array.Empty<Uri>());
var options = GetOptions(services);
// Assert
Assert.Empty(options.RedirectionEndpointUris);
}
[Fact]
public void SetRedirectionEndpointUris_AddsUri()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.SetRedirectionEndpointUris("http://localhost/callback");
var options = GetOptions(services);
// Assert
Assert.Contains(new Uri("http://localhost/callback"), options.RedirectionEndpointUris);
}
[Fact]
public void SetClientAssertionLifetime_ClientAssertionLifetimeCanBeSetToNull()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.SetClientAssertionLifetime(null);
var options = GetOptions(services);
// Assert
Assert.Null(options.ClientAssertionLifetime);
}
[Fact]
public void SetClientAssertionLifetime_DefaultClientAssertionLifetimeIsReplaced()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.SetClientAssertionLifetime(TimeSpan.FromMinutes(42));
var options = GetOptions(services);
// Assert
Assert.Equal(TimeSpan.FromMinutes(42), options.ClientAssertionLifetime);
}
[Fact]
public void SetStateTokenLifetime_StateTokenLifetimeCanBeSetToNull()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.SetStateTokenLifetime(null);
var options = GetOptions(services);
// Assert
Assert.Null(options.StateTokenLifetime);
}
[Fact]
public void SetStateTokenLifetime_DefaultStateTokenLifetimeIsReplaced()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.SetStateTokenLifetime(TimeSpan.FromMinutes(42));
var options = GetOptions(services);
// Assert
Assert.Equal(TimeSpan.FromMinutes(42), options.StateTokenLifetime);
}
[Fact]
public void SetClientUri_Uri_ThrowsAnExceptionForNullUri()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => builder.SetClientUri(uri: (Uri) null!));
Assert.Equal("uri", exception.ParamName);
}
[Theory]
[InlineData(null)]
[InlineData("")]
public void SetClientUri_String_ThrowsAnExceptionForNullOrEmptyUri(string? uri)
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act and assert
var exception = Assert.ThrowsAny<ArgumentException>(() => builder.SetClientUri(uri!));
Assert.Equal("uri", exception.ParamName);
}
[Theory]
[InlineData("relative/path")]
[InlineData(@"C:\")]
public void SetClientUri_String_ThrowsAnExceptionForInvalidUri(string uri)
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act and assert
var exception = Assert.Throws<ArgumentException>(() => builder.SetClientUri(uri));
Assert.Equal("uri", exception.ParamName);
Assert.StartsWith(SR.GetResourceString(SR.ID0144), exception.Message, StringComparison.Ordinal);
}
[Fact]
public void SetClientUri_Uri_ClientUriIsSet()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
var uri = new Uri("https://www.fabrikam.com/client");
// Act
builder.SetClientUri(uri);
var options = GetOptions(services);
// Assert
Assert.Equal(uri, options.ClientUri);
}
[Fact]
public void SetClientUri_String_ClientUriIsSet()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.SetClientUri("https://www.fabrikam.com/client");
var options = GetOptions(services);
// Assert
Assert.Equal(new Uri("https://www.fabrikam.com/client"), options.ClientUri);
}
private static X509Certificate2 CreateCertificate(X509KeyUsageFlags usages, bool includePrivateKey)
{
using var algorithm = RSA.Create(keySizeInBits: 2048);
var request = new CertificateRequest(
subjectName: "CN=OpenIddict Client 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()
=> new ServiceCollection().AddOptions();
private static OpenIddictClientBuilder CreateBuilder(IServiceCollection services)
=> new(services);
private static OpenIddictClientOptions GetOptions(IServiceCollection services)
{
var provider = services.BuildServiceProvider();
var options = provider.GetRequiredService<IOptions<OpenIddictClientOptions>>();
return options.Value;
}
private class CustomContext : BaseContext
{
public CustomContext(OpenIddictClientTransaction transaction) : base(transaction) { }
}
private class CustomHandler : IOpenIddictClientHandler<CustomContext>
{
public ValueTask HandleAsync(CustomContext context) => ValueTask.CompletedTask;
}
}