diff --git a/src/OpenIddict.Server/OpenIddictServerBuilder.cs b/src/OpenIddict.Server/OpenIddictServerBuilder.cs index 5cd51dae..2973c5a8 100644 --- a/src/OpenIddict.Server/OpenIddictServerBuilder.cs +++ b/src/OpenIddict.Server/OpenIddictServerBuilder.cs @@ -141,7 +141,7 @@ namespace Microsoft.Extensions.DependencyInjection => Configure(options => options.AcceptAnonymousClients = true); /// - /// Registers the used to encrypt the tokens issued by OpenIddict. + /// Registers encryption credentials. /// /// The encrypting credentials. /// The . @@ -156,7 +156,7 @@ namespace Microsoft.Extensions.DependencyInjection } /// - /// Registers a used to encrypt the access tokens issued by OpenIddict. + /// Registers an encryption key. /// /// The security key. /// The . @@ -193,16 +193,14 @@ namespace Microsoft.Extensions.DependencyInjection } /// - /// Registers (and generates if necessary) a user-specific development - /// certificate used to encrypt the tokens issued by OpenIddict. + /// Registers (and generates if necessary) a user-specific development encryption certificate. /// /// The . public OpenIddictServerBuilder AddDevelopmentEncryptionCertificate() => AddDevelopmentEncryptionCertificate(new X500DistinguishedName("CN=OpenIddict Server Encryption Certificate")); /// - /// Registers (and generates if necessary) a user-specific development - /// certificate used to encrypt the tokens issued by OpenIddict. + /// Registers (and generates if necessary) a user-specific development encryption certificate. /// /// The subject name associated with the certificate. /// The . @@ -284,8 +282,8 @@ namespace Microsoft.Extensions.DependencyInjection } /// - /// Registers a new ephemeral key used to encrypt the tokens issued by OpenIddict: the key - /// is discarded when the application shuts down and tokens encrypted using this key are + /// Registers a new ephemeral encryption key. Ephemeral encryption keys are automatically + /// discarded when the application shuts down and payloads encrypted using this key are /// automatically invalidated. This method should only be used during development. /// On production, using a X.509 certificate stored in the machine store is recommended. /// @@ -294,8 +292,8 @@ namespace Microsoft.Extensions.DependencyInjection => AddEphemeralEncryptionKey(SecurityAlgorithms.RsaOAEP); /// - /// Registers a new ephemeral key used to encrypt the tokens issued by OpenIddict: the key - /// is discarded when the application shuts down and tokens encrypted using this key are + /// Registers a new ephemeral encryption key. Ephemeral encryption keys are automatically + /// discarded when the application shuts down and payloads encrypted using this key are /// automatically invalidated. This method should only be used during development. /// On production, using a X.509 certificate stored in the machine store is recommended. /// @@ -370,9 +368,9 @@ namespace Microsoft.Extensions.DependencyInjection } /// - /// Registers a that is used to encrypt the tokens issued by OpenIddict. + /// Registers an encryption certificate. /// - /// The certificate used to encrypt the security tokens issued by the server. + /// The encryption certificate. /// The . public OpenIddictServerBuilder AddEncryptionCertificate([NotNull] X509Certificate2 certificate) { @@ -381,14 +379,15 @@ namespace Microsoft.Extensions.DependencyInjection throw new ArgumentNullException(nameof(certificate)); } - if (certificate.NotBefore > DateTime.Now) + // If the certificate is a X.509v3 certificate that specifies at least one + // key usage, ensure that the certificate key can be used for key encryption. + if (certificate.Version >= 3) { - throw new InvalidOperationException("The specified certificate is not yet valid."); - } - - if (certificate.NotAfter < DateTime.Now) - { - throw new InvalidOperationException("The specified certificate is no longer valid."); + var extensions = certificate.Extensions.OfType().ToList(); + if (extensions.Count != 0 && !extensions.Any(extension => extension.KeyUsages.HasFlag(X509KeyUsageFlags.KeyEncipherment))) + { + throw new InvalidOperationException("The specified certificate is not a key encryption certificate."); + } } if (!certificate.HasPrivateKey) @@ -400,8 +399,7 @@ namespace Microsoft.Extensions.DependencyInjection } /// - /// Registers a retrieved from an - /// embedded resource and used to encrypt the tokens issued by OpenIddict. + /// Registers an encryption certificate retrieved from an embedded resource. /// /// The assembly containing the certificate. /// The name of the embedded resource. @@ -419,8 +417,7 @@ namespace Microsoft.Extensions.DependencyInjection #endif /// - /// Registers a retrieved from an - /// embedded resource and used to encrypt the tokens issued by OpenIddict. + /// Registers an encryption certificate retrieved from an embedded resource. /// /// The assembly containing the certificate. /// The name of the embedded resource. @@ -456,8 +453,7 @@ namespace Microsoft.Extensions.DependencyInjection } /// - /// Registers a extracted from a - /// stream and used to encrypt the tokens issued by OpenIddict. + /// Registers an encryption certificate extracted from a stream. /// /// The stream containing the certificate. /// The password used to open the certificate. @@ -473,8 +469,7 @@ namespace Microsoft.Extensions.DependencyInjection #endif /// - /// Registers a extracted from a - /// stream and used to encrypt the tokens issued by OpenIddict. + /// Registers an encryption certificate extracted from a stream. /// /// The stream containing the certificate. /// The password used to open the certificate. @@ -505,8 +500,7 @@ namespace Microsoft.Extensions.DependencyInjection } /// - /// Registers a retrieved from the X.509 - /// machine store and used to encrypt the tokens issued by OpenIddict. + /// Registers an encryption certificate retrieved from the X.509 user or machine store. /// /// The thumbprint of the certificate used to identify it in the X.509 store. /// The . @@ -537,8 +531,7 @@ namespace Microsoft.Extensions.DependencyInjection } /// - /// Registers a retrieved from the given - /// X.509 store and used to encrypt the tokens issued by OpenIddict. + /// Registers an encryption certificate retrieved from the specified X.509 store. /// /// The thumbprint of the certificate used to identify it in the X.509 store. /// The name of the X.509 store. @@ -568,8 +561,7 @@ namespace Microsoft.Extensions.DependencyInjection } /// - /// Registers the used to sign the tokens issued by OpenIddict. - /// Note: using asymmetric keys is recommended on production. + /// Registers signing credentials. /// /// The signing credentials. /// The . @@ -584,8 +576,7 @@ namespace Microsoft.Extensions.DependencyInjection } /// - /// Registers a used to sign the tokens issued by OpenIddict. - /// Note: using asymmetric keys is recommended on production. + /// Registers a signing key. /// /// The security key. /// The . @@ -645,16 +636,14 @@ namespace Microsoft.Extensions.DependencyInjection } /// - /// Registers (and generates if necessary) a user-specific development - /// certificate used to sign the tokens issued by OpenIddict. + /// Registers (and generates if necessary) a user-specific development signing certificate. /// /// The . public OpenIddictServerBuilder AddDevelopmentSigningCertificate() => AddDevelopmentSigningCertificate(new X500DistinguishedName("CN=OpenIddict Server Signing Certificate")); /// - /// Registers (and generates if necessary) a user-specific development - /// certificate used to sign the tokens issued by OpenIddict. + /// Registers (and generates if necessary) a user-specific development signing certificate. /// /// The subject name associated with the certificate. /// The . @@ -736,8 +725,8 @@ namespace Microsoft.Extensions.DependencyInjection } /// - /// Registers a new ephemeral key used to sign the tokens issued by OpenIddict: the key - /// is discarded when the application shuts down and tokens signed using this key are + /// Registers a new ephemeral signing key. Ephemeral signing keys are automatically + /// discarded when the application shuts down and payloads signed using this key are /// automatically invalidated. This method should only be used during development. /// On production, using a X.509 certificate stored in the machine store is recommended. /// @@ -746,8 +735,8 @@ namespace Microsoft.Extensions.DependencyInjection => AddEphemeralSigningKey(SecurityAlgorithms.RsaSha256); /// - /// Registers a new ephemeral key used to sign the tokens issued by OpenIddict: the key - /// is discarded when the application shuts down and tokens signed using this key are + /// Registers a new ephemeral signing key. Ephemeral signing keys are automatically + /// discarded when the application shuts down and payloads signed using this key are /// automatically invalidated. This method should only be used during development. /// On production, using a X.509 certificate stored in the machine store is recommended. /// @@ -841,9 +830,9 @@ namespace Microsoft.Extensions.DependencyInjection } /// - /// Registers a that is used to sign the tokens issued by OpenIddict. + /// Registers a signing certificate. /// - /// The certificate used to sign the security tokens issued by the server. + /// The signing certificate. /// The . public OpenIddictServerBuilder AddSigningCertificate([NotNull] X509Certificate2 certificate) { @@ -852,14 +841,15 @@ namespace Microsoft.Extensions.DependencyInjection throw new ArgumentNullException(nameof(certificate)); } - if (certificate.NotBefore > DateTime.Now) + // If the certificate is a X.509v3 certificate that specifies at least + // one key usage, ensure that the certificate key can be used for signing. + if (certificate.Version >= 3) { - throw new InvalidOperationException("The specified certificate is not yet valid."); - } - - if (certificate.NotAfter < DateTime.Now) - { - throw new InvalidOperationException("The specified certificate is no longer valid."); + var extensions = certificate.Extensions.OfType().ToList(); + if (extensions.Count != 0 && !extensions.Any(extension => extension.KeyUsages.HasFlag(X509KeyUsageFlags.DigitalSignature))) + { + throw new InvalidOperationException("The specified certificate is not a signing certificate."); + } } if (!certificate.HasPrivateKey) @@ -871,8 +861,7 @@ namespace Microsoft.Extensions.DependencyInjection } /// - /// Registers a retrieved from an - /// embedded resource and used to sign the tokens issued by OpenIddict. + /// Registers a signing certificate retrieved from an embedded resource. /// /// The assembly containing the certificate. /// The name of the embedded resource. @@ -890,8 +879,7 @@ namespace Microsoft.Extensions.DependencyInjection #endif /// - /// Registers a retrieved from an - /// embedded resource and used to sign the tokens issued by OpenIddict. + /// Registers a signing certificate retrieved from an embedded resource. /// /// The assembly containing the certificate. /// The name of the embedded resource. @@ -927,8 +915,7 @@ namespace Microsoft.Extensions.DependencyInjection } /// - /// Registers a extracted from a - /// stream and used to sign the tokens issued by OpenIddict. + /// Registers a signing certificate extracted from a stream. /// /// The stream containing the certificate. /// The password used to open the certificate. @@ -944,8 +931,7 @@ namespace Microsoft.Extensions.DependencyInjection #endif /// - /// Registers a extracted from a - /// stream and used to sign the tokens issued by OpenIddict. + /// Registers a signing certificate extracted from a stream. /// /// The stream containing the certificate. /// The password used to open the certificate. @@ -976,8 +962,7 @@ namespace Microsoft.Extensions.DependencyInjection } /// - /// Registers a retrieved from the X.509 - /// machine store and used to sign the tokens issued by OpenIddict. + /// Registers a signing certificate retrieved from the X.509 user or machine store. /// /// The thumbprint of the certificate used to identify it in the X.509 store. /// The . @@ -1008,8 +993,7 @@ namespace Microsoft.Extensions.DependencyInjection } /// - /// Registers a retrieved from the given - /// X.509 store and used to sign the tokens issued by OpenIddict. + /// Registers a signing certificate retrieved from the specified X.509 store. /// /// The thumbprint of the certificate used to identify it in the X.509 store. /// The name of the X.509 store. @@ -1144,11 +1128,7 @@ namespace Microsoft.Extensions.DependencyInjection return Configure(options => { options.AuthorizationEndpointUris.Clear(); - - foreach (var address in addresses) - { - options.AuthorizationEndpointUris.Add(address); - } + options.AuthorizationEndpointUris.AddRange(addresses); }); } @@ -1191,11 +1171,7 @@ namespace Microsoft.Extensions.DependencyInjection return Configure(options => { options.ConfigurationEndpointUris.Clear(); - - foreach (var address in addresses) - { - options.ConfigurationEndpointUris.Add(address); - } + options.ConfigurationEndpointUris.AddRange(addresses); }); } @@ -1238,11 +1214,7 @@ namespace Microsoft.Extensions.DependencyInjection return Configure(options => { options.CryptographyEndpointUris.Clear(); - - foreach (var address in addresses) - { - options.CryptographyEndpointUris.Add(address); - } + options.CryptographyEndpointUris.AddRange(addresses); }); } @@ -1285,11 +1257,7 @@ namespace Microsoft.Extensions.DependencyInjection return Configure(options => { options.DeviceEndpointUris.Clear(); - - foreach (var address in addresses) - { - options.DeviceEndpointUris.Add(address); - } + options.DeviceEndpointUris.AddRange(addresses); }); } @@ -1332,11 +1300,7 @@ namespace Microsoft.Extensions.DependencyInjection return Configure(options => { options.IntrospectionEndpointUris.Clear(); - - foreach (var address in addresses) - { - options.IntrospectionEndpointUris.Add(address); - } + options.IntrospectionEndpointUris.AddRange(addresses); }); } @@ -1379,11 +1343,7 @@ namespace Microsoft.Extensions.DependencyInjection return Configure(options => { options.LogoutEndpointUris.Clear(); - - foreach (var address in addresses) - { - options.LogoutEndpointUris.Add(address); - } + options.LogoutEndpointUris.AddRange(addresses); }); } @@ -1426,11 +1386,7 @@ namespace Microsoft.Extensions.DependencyInjection return Configure(options => { options.RevocationEndpointUris.Clear(); - - foreach (var address in addresses) - { - options.RevocationEndpointUris.Add(address); - } + options.RevocationEndpointUris.AddRange(addresses); }); } @@ -1473,11 +1429,7 @@ namespace Microsoft.Extensions.DependencyInjection return Configure(options => { options.TokenEndpointUris.Clear(); - - foreach (var address in addresses) - { - options.TokenEndpointUris.Add(address); - } + options.TokenEndpointUris.AddRange(addresses); }); } @@ -1520,11 +1472,7 @@ namespace Microsoft.Extensions.DependencyInjection return Configure(options => { options.UserinfoEndpointUris.Clear(); - - foreach (var address in addresses) - { - options.UserinfoEndpointUris.Add(address); - } + options.UserinfoEndpointUris.AddRange(addresses); }); } @@ -1567,11 +1515,7 @@ namespace Microsoft.Extensions.DependencyInjection return Configure(options => { options.VerificationEndpointUris.Clear(); - - foreach (var address in addresses) - { - options.VerificationEndpointUris.Add(address); - } + options.VerificationEndpointUris.AddRange(addresses); }); } diff --git a/src/OpenIddict.Server/OpenIddictServerConfiguration.cs b/src/OpenIddict.Server/OpenIddictServerConfiguration.cs index a5cb2e17..66c24cb3 100644 --- a/src/OpenIddict.Server/OpenIddictServerConfiguration.cs +++ b/src/OpenIddict.Server/OpenIddictServerConfiguration.cs @@ -129,6 +129,26 @@ namespace OpenIddict.Server .ToString()); } + // If all the registered encryption credentials are backed by a X.509 certificate, at least one of them must be valid. + if (options.EncryptionCredentials.All(credentials => credentials.Key is X509SecurityKey x509SecurityKey && + (x509SecurityKey.Certificate.NotBefore > DateTime.Now || x509SecurityKey.Certificate.NotAfter < DateTime.Now))) + { + throw new InvalidOperationException(new StringBuilder() + .AppendLine("When using X.509 encryption credentials, at least one of the registered certificates must be valid.") + .Append("To use key rollover, register both the new certificate and the old one in the credentials collection.") + .ToString()); + } + + // If all the registered signing credentials are backed by a X.509 certificate, at least one of them must be valid. + if (options.SigningCredentials.All(credentials => credentials.Key is X509SecurityKey x509SecurityKey && + (x509SecurityKey.Certificate.NotBefore > DateTime.Now || x509SecurityKey.Certificate.NotAfter < DateTime.Now))) + { + throw new InvalidOperationException(new StringBuilder() + .AppendLine("When using X.509 signing credentials, at least one of the registered certificates must be valid.") + .Append("To use key rollover, register both the new certificate and the old one in the credentials collection.") + .ToString()); + } + if (options.EnableDegradedMode) { // If the degraded mode was enabled, ensure custom validation handlers @@ -252,6 +272,10 @@ namespace OpenIddict.Server // Sort the handlers collection using the order associated with each handler. options.Handlers.Sort((left, right) => left.Order.CompareTo(right.Order)); + // Sort the encryption and signing credentials. + options.EncryptionCredentials.Sort((left, right) => Compare(left.Key, right.Key)); + options.SigningCredentials.Sort((left, right) => Compare(left.Key, right.Key)); + // Automatically add the offline_access scope if the refresh token grant has been enabled. if (options.GrantTypes.Contains(GrantTypes.RefreshToken)) { @@ -312,6 +336,31 @@ namespace OpenIddict.Server from credentials in options.EncryptionCredentials select credentials.Key; + static int Compare(SecurityKey left, SecurityKey right) => (left, right) switch + { + // If the two keys refer to the same instances, return 0. + (SecurityKey first, SecurityKey second) when ReferenceEquals(first, second) => 0, + + // If one of the keys is a symmetric key, prefer it to the other one. + (SymmetricSecurityKey _, SymmetricSecurityKey _) => 0, + (SymmetricSecurityKey _, SecurityKey _) => -1, + (SecurityKey _, SymmetricSecurityKey _) => 1, + + // If one of the keys is backed by a X.509 certificate, don't prefer it if it's not valid yet. + (X509SecurityKey first, SecurityKey _) when first.Certificate.NotBefore > DateTime.Now => 1, + (SecurityKey _, X509SecurityKey second) when second.Certificate.NotBefore > DateTime.Now => 1, + + // If the two keys are backed by a X.509 certificate, prefer the one with the furthest expiration date. + (X509SecurityKey first, X509SecurityKey second) => -first.Certificate.NotAfter.CompareTo(second.Certificate.NotAfter), + + // If one of the keys is backed by a X.509 certificate, prefer the X.509 security key. + (X509SecurityKey _, SecurityKey _) => -1, + (SecurityKey _, X509SecurityKey _) => 1, + + // If the two keys are not backed by a X.509 certificate, none should be preferred to the other. + (SecurityKey _, SecurityKey _) => 0 + }; + static string GetKeyIdentifier(SecurityKey key) { // When no key identifier can be retrieved from the security keys, a value is automatically diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.cs index f21c4ee9..9c402e24 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.cs @@ -2973,8 +2973,7 @@ namespace OpenIddict.Server Expires = context.AccessTokenPrincipal.GetExpirationDate()?.UtcDateTime, IssuedAt = context.AccessTokenPrincipal.GetCreationDate()?.UtcDateTime, Issuer = context.Issuer?.AbsoluteUri, - SigningCredentials = context.Options.SigningCredentials.FirstOrDefault(credentials => - credentials.Key is SymmetricSecurityKey) ?? context.Options.SigningCredentials.First(), + SigningCredentials = context.Options.SigningCredentials.First(), Subject = (ClaimsIdentity) principal.Identity }; @@ -2983,9 +2982,7 @@ namespace OpenIddict.Server if (!context.Options.DisableAccessTokenEncryption) { token = context.Options.JsonWebTokenHandler.EncryptToken(token, - encryptingCredentials: context.Options.EncryptionCredentials.FirstOrDefault( - credentials => credentials.Key is SymmetricSecurityKey) ?? - context.Options.EncryptionCredentials.First(), + encryptingCredentials: context.Options.EncryptionCredentials.First(), additionalHeaderClaims: descriptor.AdditionalHeaderClaims); } @@ -3251,8 +3248,7 @@ namespace OpenIddict.Server Expires = context.AuthorizationCodePrincipal.GetExpirationDate()?.UtcDateTime, IssuedAt = context.AuthorizationCodePrincipal.GetCreationDate()?.UtcDateTime, Issuer = context.Issuer?.AbsoluteUri, - SigningCredentials = context.Options.SigningCredentials.FirstOrDefault(credentials => - credentials.Key is SymmetricSecurityKey) ?? context.Options.SigningCredentials.First(), + SigningCredentials = context.Options.SigningCredentials.First(), Subject = (ClaimsIdentity) principal.Identity }; @@ -3270,9 +3266,7 @@ namespace OpenIddict.Server var token = context.Options.JsonWebTokenHandler.CreateToken(descriptor); token = context.Options.JsonWebTokenHandler.EncryptToken(token, - encryptingCredentials: context.Options.EncryptionCredentials.FirstOrDefault( - credentials => credentials.Key is SymmetricSecurityKey) ?? - context.Options.EncryptionCredentials.First(), + encryptingCredentials: context.Options.EncryptionCredentials.First(), additionalHeaderClaims: descriptor.AdditionalHeaderClaims); context.Response.Code = token; @@ -3541,8 +3535,7 @@ namespace OpenIddict.Server Expires = context.DeviceCodePrincipal.GetExpirationDate()?.UtcDateTime, IssuedAt = context.DeviceCodePrincipal.GetCreationDate()?.UtcDateTime, Issuer = context.Issuer?.AbsoluteUri, - SigningCredentials = context.Options.SigningCredentials.FirstOrDefault(credentials => - credentials.Key is SymmetricSecurityKey) ?? context.Options.SigningCredentials.First(), + SigningCredentials = context.Options.SigningCredentials.First(), Subject = (ClaimsIdentity) principal.Identity }; @@ -3560,9 +3553,7 @@ namespace OpenIddict.Server var token = context.Options.JsonWebTokenHandler.CreateToken(descriptor); token = context.Options.JsonWebTokenHandler.EncryptToken(token, - encryptingCredentials: context.Options.EncryptionCredentials.FirstOrDefault( - credentials => credentials.Key is SymmetricSecurityKey) ?? - context.Options.EncryptionCredentials.First(), + encryptingCredentials: context.Options.EncryptionCredentials.First(), additionalHeaderClaims: descriptor.AdditionalHeaderClaims); context.Response.DeviceCode = token; @@ -3929,8 +3920,7 @@ namespace OpenIddict.Server Expires = context.RefreshTokenPrincipal.GetExpirationDate()?.UtcDateTime, IssuedAt = context.RefreshTokenPrincipal.GetCreationDate()?.UtcDateTime, Issuer = context.Issuer?.AbsoluteUri, - SigningCredentials = context.Options.SigningCredentials.FirstOrDefault(credentials => - credentials.Key is SymmetricSecurityKey) ?? context.Options.SigningCredentials.First(), + SigningCredentials = context.Options.SigningCredentials.First(), Subject = (ClaimsIdentity) principal.Identity }; @@ -3948,9 +3938,7 @@ namespace OpenIddict.Server var token = context.Options.JsonWebTokenHandler.CreateToken(descriptor); token = context.Options.JsonWebTokenHandler.EncryptToken(token, - encryptingCredentials: context.Options.EncryptionCredentials.FirstOrDefault( - credentials => credentials.Key is SymmetricSecurityKey) ?? - context.Options.EncryptionCredentials.First(), + encryptingCredentials: context.Options.EncryptionCredentials.First(), additionalHeaderClaims: descriptor.AdditionalHeaderClaims); context.Response.RefreshToken = token; @@ -4262,8 +4250,7 @@ namespace OpenIddict.Server Expires = context.UserCodePrincipal.GetExpirationDate()?.UtcDateTime, IssuedAt = context.UserCodePrincipal.GetCreationDate()?.UtcDateTime, Issuer = context.Issuer?.AbsoluteUri, - SigningCredentials = context.Options.SigningCredentials.FirstOrDefault(credentials => - credentials.Key is SymmetricSecurityKey) ?? context.Options.SigningCredentials.First(), + SigningCredentials = context.Options.SigningCredentials.First(), Subject = (ClaimsIdentity) principal.Identity }; @@ -4271,9 +4258,7 @@ namespace OpenIddict.Server var token = context.Options.JsonWebTokenHandler.CreateToken(descriptor); token = context.Options.JsonWebTokenHandler.EncryptToken(token, - encryptingCredentials: context.Options.EncryptionCredentials.FirstOrDefault( - credentials => credentials.Key is SymmetricSecurityKey) ?? - context.Options.EncryptionCredentials.First(), + encryptingCredentials: context.Options.EncryptionCredentials.First(), additionalHeaderClaims: descriptor.AdditionalHeaderClaims); context.Response.UserCode = token; @@ -4699,6 +4684,8 @@ namespace OpenIddict.Server Expires = context.IdentityTokenPrincipal.GetExpirationDate()?.UtcDateTime, IssuedAt = context.IdentityTokenPrincipal.GetCreationDate()?.UtcDateTime, Issuer = context.Issuer?.AbsoluteUri, + // Note: unlike other tokens, identity tokens can only be signed using an asymmetric key + // as they are meant to be validated by clients using the public keys exposed by the server. SigningCredentials = context.Options.SigningCredentials.First(credentials => credentials.Key is AsymmetricSecurityKey), Subject = (ClaimsIdentity) principal.Identity diff --git a/src/OpenIddict.Server/OpenIddictServerOptions.cs b/src/OpenIddict.Server/OpenIddictServerOptions.cs index 79609333..ca501f28 100644 --- a/src/OpenIddict.Server/OpenIddictServerOptions.cs +++ b/src/OpenIddict.Server/OpenIddictServerOptions.cs @@ -27,18 +27,35 @@ namespace OpenIddict.Server public Uri Issuer { get; set; } /// - /// Gets the list of credentials used to encrypt the tokens issued by the - /// OpenIddict server services. Note: the encryption credentials are not - /// used to protect/unprotect tokens issued by ASP.NET Core Data Protection. - /// + /// Gets the list of encryption credentials used by the OpenIddict server services. + /// Multiple credentials can be added to support key rollover, but if X.509 keys + /// are used, at least one of them must have a valid creation/expiration date. + /// Note: the encryption credentials are not used to protect/unprotect tokens issued + /// by ASP.NET Core Data Protection, that uses its own key ring, configured separately. + /// + /// + /// Note: OpenIddict automatically sorts the credentials based on the following algorithm: + /// • Symmetric keys are always preferred when they can be used for the operation (e.g token encryption). + /// • X.509 keys are always preferred to non-X.509 asymmetric keys. + /// • X.509 keys with the furthest expiration date are preferred. + /// • X.509 keys whose backing certificate is not yet valid are never preferred. + /// public List EncryptionCredentials { get; } = new List(); /// - /// Gets the list of credentials used to sign the tokens issued by the OpenIddict server services. - /// Both asymmetric and symmetric keys are supported, but only asymmetric keys can be used to sign identity tokens. - /// Note that only asymmetric RSA and ECDSA keys can be exposed by the JWKS metadata endpoint and that the - /// signing credentials are not used to protect/unprotect tokens issued by ASP.NET Core Data Protection. + /// Gets the list of signing credentials used by the OpenIddict server services. + /// Multiple credentials can be added to support key rollover, but if X.509 keys + /// are used, at least one of them must have a valid creation/expiration date. + /// Note: the signing credentials are not used to protect/unprotect tokens issued + /// by ASP.NET Core Data Protection, that uses its own key ring, configured separately. /// + /// + /// Note: OpenIddict automatically sorts the credentials based on the following algorithm: + /// • Symmetric keys are always preferred when they can be used for the operation (e.g token signing). + /// • X.509 keys are always preferred to non-X.509 asymmetric keys. + /// • X.509 keys with the furthest expiration date are preferred. + /// • X.509 keys whose backing certificate is not yet valid are never preferred. + /// public List SigningCredentials { get; } = new List(); /// diff --git a/src/OpenIddict.Validation.ServerIntegration/OpenIddictValidationServerIntegrationConfiguration.cs b/src/OpenIddict.Validation.ServerIntegration/OpenIddictValidationServerIntegrationConfiguration.cs index 1bc085bc..3a2f9bbd 100644 --- a/src/OpenIddict.Validation.ServerIntegration/OpenIddictValidationServerIntegrationConfiguration.cs +++ b/src/OpenIddict.Validation.ServerIntegration/OpenIddictValidationServerIntegrationConfiguration.cs @@ -54,10 +54,7 @@ namespace OpenIddict.Validation.ServerIntegration } // Import the encryption keys from the server configuration. - foreach (var credentials in _options.CurrentValue.EncryptionCredentials) - { - options.EncryptionCredentials.Add(credentials); - } + options.EncryptionCredentials.AddRange(_options.CurrentValue.EncryptionCredentials); // Note: token entry validation must be enabled to be able to validate reference access tokens. options.EnableTokenEntryValidation = _options.CurrentValue.UseReferenceAccessTokens; diff --git a/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs b/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs index 1576b7b8..73bad25c 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs @@ -131,7 +131,7 @@ namespace Microsoft.Extensions.DependencyInjection } /// - /// Registers the used to decrypt the tokens issued by OpenIddict. + /// Registers encryption credentials. /// /// The encrypting credentials. /// The . @@ -146,7 +146,7 @@ namespace Microsoft.Extensions.DependencyInjection } /// - /// Registers a used to decrypt the access tokens issued by OpenIddict. + /// Registers an encryption key. /// /// The security key. /// The . @@ -183,9 +183,9 @@ namespace Microsoft.Extensions.DependencyInjection } /// - /// Registers a that is used to decrypt the tokens issued by OpenIddict. + /// Registers an encryption certificate. /// - /// The certificate used to decrypt the security tokens issued by the validation. + /// The encryption certificate. /// The . public OpenIddictValidationBuilder AddEncryptionCertificate([NotNull] X509Certificate2 certificate) { @@ -194,14 +194,15 @@ namespace Microsoft.Extensions.DependencyInjection throw new ArgumentNullException(nameof(certificate)); } - if (certificate.NotBefore > DateTime.Now) + // If the certificate is a X.509v3 certificate that specifies at least one + // key usage, ensure that the certificate key can be used for key encryption. + if (certificate.Version >= 3) { - throw new InvalidOperationException("The specified certificate is not yet valid."); - } - - if (certificate.NotAfter < DateTime.Now) - { - throw new InvalidOperationException("The specified certificate is no longer valid."); + var extensions = certificate.Extensions.OfType().ToList(); + if (extensions.Count != 0 && !extensions.Any(extension => extension.KeyUsages.HasFlag(X509KeyUsageFlags.KeyEncipherment))) + { + throw new InvalidOperationException("The specified certificate is not a key encryption certificate."); + } } if (!certificate.HasPrivateKey) @@ -213,8 +214,7 @@ namespace Microsoft.Extensions.DependencyInjection } /// - /// Registers a retrieved from an - /// embedded resource and used to decrypt the tokens issued by OpenIddict. + /// Registers an encryption certificate retrieved from an embedded resource. /// /// The assembly containing the certificate. /// The name of the embedded resource. @@ -232,8 +232,7 @@ namespace Microsoft.Extensions.DependencyInjection #endif /// - /// Registers a retrieved from an - /// embedded resource and used to decrypt the tokens issued by OpenIddict. + /// Registers an encryption certificate retrieved from an embedded resource. /// /// The assembly containing the certificate. /// The name of the embedded resource. @@ -269,8 +268,7 @@ namespace Microsoft.Extensions.DependencyInjection } /// - /// Registers a extracted from a - /// stream and used to decrypt the tokens issued by OpenIddict. + /// Registers an encryption certificate extracted from a stream. /// /// The stream containing the certificate. /// The password used to open the certificate. @@ -286,8 +284,7 @@ namespace Microsoft.Extensions.DependencyInjection #endif /// - /// Registers a extracted from a - /// stream and used to decrypt the tokens issued by OpenIddict. + /// Registers an encryption certificate extracted from a stream. /// /// The stream containing the certificate. /// The password used to open the certificate. @@ -318,8 +315,7 @@ namespace Microsoft.Extensions.DependencyInjection } /// - /// Registers a retrieved from the X.509 - /// machine store and used to decrypt the tokens issued by OpenIddict. + /// Registers an encryption certificate retrieved from the X.509 user or machine store. /// /// The thumbprint of the certificate used to identify it in the X.509 store. /// The . @@ -350,8 +346,7 @@ namespace Microsoft.Extensions.DependencyInjection } /// - /// Registers a retrieved from the given - /// X.509 store and used to decrypt the tokens issued by OpenIddict. + /// Registers an encryption certificate retrieved from the specified X.509 store. /// /// The thumbprint of the certificate used to identify it in the X.509 store. /// The name of the X.509 store. diff --git a/src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs b/src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs index f3daf110..5708d0e3 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs @@ -11,6 +11,7 @@ using JetBrains.Annotations; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Microsoft.IdentityModel.Tokens; using static OpenIddict.Validation.OpenIddictValidationEvents; namespace OpenIddict.Validation @@ -96,6 +97,17 @@ namespace OpenIddict.Validation } } + // If all the registered encryption credentials are backed by a X.509 certificate, at least one of them must be valid. + if (options.EncryptionCredentials.Count != 0 && + options.EncryptionCredentials.All(credentials => credentials.Key is X509SecurityKey x509SecurityKey && + (x509SecurityKey.Certificate.NotBefore > DateTime.Now || x509SecurityKey.Certificate.NotAfter < DateTime.Now))) + { + throw new InvalidOperationException(new StringBuilder() + .AppendLine("When using X.509 encryption credentials, at least one of the registered certificates must be valid.") + .Append("To use key rollover, register both the new certificate and the old one in the credentials collection.") + .ToString()); + } + if (options.Configuration == null && options.ConfigurationManager == null) { if (!options.Handlers.Any(descriptor => descriptor.ContextType == typeof(ApplyConfigurationRequestContext)) || diff --git a/src/OpenIddict.Validation/OpenIddictValidationOptions.cs b/src/OpenIddict.Validation/OpenIddictValidationOptions.cs index a1efb09a..ea330b26 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationOptions.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationOptions.cs @@ -21,9 +21,17 @@ namespace OpenIddict.Validation public class OpenIddictValidationOptions { /// - /// Gets the list of credentials used to encrypt the tokens issued by the - /// OpenIddict validation services. Note: only symmetric credentials are supported. + /// Gets the list of encryption credentials used by the OpenIddict validation services. + /// Note: the encryption credentials are not used to protect/unprotect tokens issued + /// by ASP.NET Core Data Protection, that uses its own key ring, configured separately. /// + /// + /// Note: OpenIddict automatically sorts the credentials based on the following algorithm: + /// • Symmetric keys are always preferred when they can be used for the operation (e.g token decryption). + /// • X.509 keys are always preferred to non-X.509 asymmetric keys. + /// • X.509 keys with the furthest expiration date are preferred. + /// • X.509 keys whose backing certificate is not yet valid are never preferred. + /// public List EncryptionCredentials { get; } = new List(); /// diff --git a/test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs b/test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs index cb88d18c..b0964b96 100644 --- a/test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs +++ b/test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs @@ -33,11 +33,10 @@ namespace OpenIddict.Server.Tests // Arrange var services = CreateServices(); var builder = CreateBuilder(services); - Action> configuration = null; // Act and assert - var exception = Assert.Throws(() => builder.AddEventHandler(configuration)); - Assert.Equal(nameof(configuration), exception.ParamName); + var exception = Assert.Throws(() => builder.AddEventHandler(configuration: null)); + Assert.Equal("configuration", exception.ParamName); } [Fact] @@ -46,11 +45,10 @@ namespace OpenIddict.Server.Tests // Arrange var services = CreateServices(); var builder = CreateBuilder(services); - OpenIddictServerHandlerDescriptor descriptor = null; // Act and assert - var exception = Assert.Throws(() => builder.AddEventHandler(descriptor)); - Assert.Equal(nameof(descriptor), exception.ParamName); + var exception = Assert.Throws(() => builder.AddEventHandler(descriptor: null)); + Assert.Equal("descriptor", exception.ParamName); } [Fact] @@ -134,11 +132,10 @@ namespace OpenIddict.Server.Tests // Arrange var services = CreateServices(); var builder = CreateBuilder(services); - EncryptingCredentials credentials = null; // Act and assert - var exception = Assert.Throws(() => builder.AddEncryptionCredentials(credentials)); - Assert.Equal(nameof(credentials), exception.ParamName); + var exception = Assert.Throws(() => builder.AddEncryptionCredentials(credentials: null)); + Assert.Equal("credentials", exception.ParamName); } [Fact] @@ -147,11 +144,10 @@ namespace OpenIddict.Server.Tests // Arrange var services = CreateServices(); var builder = CreateBuilder(services); - SecurityKey key = null; // Act and assert - var exception = Assert.Throws(() => builder.AddEncryptionKey(key)); - Assert.Equal(nameof(key), exception.ParamName); + var exception = Assert.Throws(() => builder.AddEncryptionKey(key: null)); + Assert.Equal("key", exception.ParamName); } [Fact] @@ -160,11 +156,10 @@ namespace OpenIddict.Server.Tests // Arrange var services = CreateServices(); var builder = CreateBuilder(services); - var key = new Mock(); - key.SetupGet(x => x.PrivateKeyStatus).Returns(PrivateKeyStatus.DoesNotExist); + var key = Mock.Of(key => key.PrivateKeyStatus == PrivateKeyStatus.DoesNotExist); // Act and assert - var exception = Assert.Throws(() => builder.AddEncryptionKey(key.Object)); + var exception = Assert.Throws(() => builder.AddEncryptionKey(key)); Assert.Equal("The asymmetric encryption key doesn't contain the required private key.", exception.Message); } @@ -192,11 +187,10 @@ namespace OpenIddict.Server.Tests // Arrange var services = CreateServices(); var builder = CreateBuilder(services); - OpenIddictServerHandlerDescriptor descriptor = null; // Act and assert - var exception = Assert.Throws(() => builder.RemoveEventHandler(descriptor)); - Assert.Equal(nameof(descriptor), exception.ParamName); + var exception = Assert.Throws(() => builder.RemoveEventHandler(descriptor: null)); + Assert.Equal("descriptor", exception.ParamName); } [Fact] @@ -240,11 +234,10 @@ namespace OpenIddict.Server.Tests // Arrange var services = CreateServices(); var builder = CreateBuilder(services); - Action configuration = null; // Act and assert - var exception = Assert.Throws(() => builder.Configure(configuration)); - Assert.Equal(nameof(configuration), exception.ParamName); + var exception = Assert.Throws(() => builder.Configure(configuration: null)); + Assert.Equal("configuration", exception.ParamName); } [Fact] @@ -269,11 +262,10 @@ namespace OpenIddict.Server.Tests // Arrange var services = CreateServices(); var builder = CreateBuilder(services); - X500DistinguishedName subject = null; // Act and assert - var exception = Assert.Throws(() => builder.AddDevelopmentEncryptionCertificate(subject)); - Assert.Equal(nameof(subject), exception.ParamName); + var exception = Assert.Throws(() => builder.AddDevelopmentEncryptionCertificate(subject: null)); + Assert.Equal("subject", exception.ParamName); } #if SUPPORTS_CERTIFICATE_GENERATION @@ -354,6 +346,31 @@ namespace OpenIddict.Server.Tests Assert.Equal(algorithm, credentials.Algorithm); } + [Fact] + public void AddSigningKey_ThrowsExceptionWhenKeyIsNull() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act and assert + var exception = Assert.Throws(() => 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(key => key.PrivateKeyStatus == PrivateKeyStatus.DoesNotExist); + + // Act and assert + var exception = Assert.Throws(() => 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)] @@ -531,11 +548,10 @@ namespace OpenIddict.Server.Tests // Arrange var services = CreateServices(); var builder = CreateBuilder(services); - Uri[] addresses = null; // Act and assert - var exception = Assert.Throws(() => builder.SetConfigurationEndpointUris(addresses)); - Assert.Equal(nameof(addresses), exception.ParamName); + var exception = Assert.Throws(() => builder.SetConfigurationEndpointUris(addresses: null as Uri[])); + Assert.Equal("addresses", exception.ParamName); } [Fact] @@ -544,27 +560,23 @@ namespace OpenIddict.Server.Tests // Arrange var services = CreateServices(); var builder = CreateBuilder(services); - string[] addresses = null; // Act and assert - var exception = Assert.Throws(() => builder.SetConfigurationEndpointUris(addresses)); - Assert.Equal(nameof(addresses), exception.ParamName); + var exception = Assert.Throws(() => builder.SetConfigurationEndpointUris(addresses: null as string[])); + Assert.Equal("addresses", exception.ParamName); } - public const string InvalidUriString = @"C:\"; - [Theory] - [InlineData(InvalidUriString)] + [InlineData(@"C:\")] public void SetConfigurationEndpointUris_ThrowsExceptionForUri(string uri) { // Arrange var services = CreateServices(); var builder = CreateBuilder(services); - Uri[] addresses = { new Uri(uri), }; // Act and assert - var exception = Assert.Throws(() => builder.SetConfigurationEndpointUris(addresses)); - Assert.Equal(nameof(addresses), exception.ParamName); + var exception = Assert.Throws(() => builder.SetConfigurationEndpointUris(new Uri(uri))); + Assert.Equal("addresses", exception.ParamName); Assert.Contains("One of the specified addresses is not valid.", exception.Message); } @@ -606,11 +618,10 @@ namespace OpenIddict.Server.Tests // Arrange var services = CreateServices(); var builder = CreateBuilder(services); - Uri[] addresses = null; // Act and assert - var exception = Assert.Throws(() => builder.SetDeviceEndpointUris(addresses)); - Assert.Equal(nameof(addresses), exception.ParamName); + var exception = Assert.Throws(() => builder.SetDeviceEndpointUris(addresses: null as Uri[])); + Assert.Equal("addresses", exception.ParamName); } [Fact] @@ -619,25 +630,23 @@ namespace OpenIddict.Server.Tests // Arrange var services = CreateServices(); var builder = CreateBuilder(services); - string[] addresses = null; // Act and assert - var exception = Assert.Throws(() => builder.SetDeviceEndpointUris(addresses)); - Assert.Equal(nameof(addresses), exception.ParamName); + var exception = Assert.Throws(() => builder.SetDeviceEndpointUris(addresses: null as string[])); + Assert.Equal("addresses", exception.ParamName); } [Theory] - [InlineData(InvalidUriString)] + [InlineData(@"C:\")] public void SetDeviceEndpointUris_ThrowsExceptionForUri(string uri) { // Arrange var services = CreateServices(); var builder = CreateBuilder(services); - Uri[] addresses = { new Uri(uri), }; // Act and assert - var exception = Assert.Throws(() => builder.SetDeviceEndpointUris(addresses)); - Assert.Equal(nameof(addresses), exception.ParamName); + var exception = Assert.Throws(() => builder.SetDeviceEndpointUris(new Uri(uri))); + Assert.Equal("addresses", exception.ParamName); Assert.Contains("One of the specified addresses is not valid.", exception.Message); } @@ -695,11 +704,10 @@ namespace OpenIddict.Server.Tests // Arrange var services = CreateServices(); var builder = CreateBuilder(services); - Uri[] addresses = null; // Act and assert - var exception = Assert.Throws(() => builder.SetCryptographyEndpointUris(addresses)); - Assert.Equal(nameof(addresses), exception.ParamName); + var exception = Assert.Throws(() => builder.SetCryptographyEndpointUris(addresses: null as Uri[])); + Assert.Equal("addresses", exception.ParamName); } [Fact] @@ -708,25 +716,23 @@ namespace OpenIddict.Server.Tests // Arrange var services = CreateServices(); var builder = CreateBuilder(services); - string[] addresses = null; // Act and assert - var exception = Assert.Throws(() => builder.SetCryptographyEndpointUris(addresses)); - Assert.Equal(nameof(addresses), exception.ParamName); + var exception = Assert.Throws(() => builder.SetCryptographyEndpointUris(addresses: null as string[])); + Assert.Equal("addresses", exception.ParamName); } [Theory] - [InlineData(InvalidUriString)] + [InlineData(@"C:\")] public void SetCryptographyEndpointUris_ThrowsExceptionForUri(string uri) { // Arrange var services = CreateServices(); var builder = CreateBuilder(services); - Uri[] addresses = { new Uri(uri), }; // Act and assert - var exception = Assert.Throws(() => builder.SetCryptographyEndpointUris(addresses)); - Assert.Equal(nameof(addresses), exception.ParamName); + var exception = Assert.Throws(() => builder.SetCryptographyEndpointUris(new Uri(uri))); + Assert.Equal("addresses", exception.ParamName); Assert.Contains("One of the specified addresses is not valid.", exception.Message); } @@ -832,11 +838,10 @@ namespace OpenIddict.Server.Tests // Arrange var services = CreateServices(); var builder = CreateBuilder(services); - Uri[] addresses = null; // Act and assert - var exception = Assert.Throws(() => builder.SetAuthorizationEndpointUris(addresses)); - Assert.Equal(nameof(addresses), exception.ParamName); + var exception = Assert.Throws(() => builder.SetAuthorizationEndpointUris(addresses: null as Uri[])); + Assert.Equal("addresses", exception.ParamName); } [Fact] @@ -845,25 +850,23 @@ namespace OpenIddict.Server.Tests // Arrange var services = CreateServices(); var builder = CreateBuilder(services); - string[] addresses = null; // Act and assert - var exception = Assert.Throws(() => builder.SetAuthorizationEndpointUris(addresses)); - Assert.Equal(nameof(addresses), exception.ParamName); + var exception = Assert.Throws(() => builder.SetAuthorizationEndpointUris(addresses: null as string[])); + Assert.Equal("addresses", exception.ParamName); } [Theory] - [InlineData(InvalidUriString)] + [InlineData(@"C:\")] public void SetAuthorizationEndpointUris_ThrowsExceptionForUri(string uri) { // Arrange var services = CreateServices(); var builder = CreateBuilder(services); - Uri[] addresses = { new Uri(uri), }; // Act and assert - var exception = Assert.Throws(() => builder.SetAuthorizationEndpointUris(addresses)); - Assert.Equal(nameof(addresses), exception.ParamName); + var exception = Assert.Throws(() => builder.SetAuthorizationEndpointUris(new Uri(uri))); + Assert.Equal("addresses", exception.ParamName); Assert.Contains("One of the specified addresses is not valid.", exception.Message); } @@ -905,11 +908,10 @@ namespace OpenIddict.Server.Tests // Arrange var services = CreateServices(); var builder = CreateBuilder(services); - Uri[] addresses = null; // Act and assert - var exception = Assert.Throws(() => builder.SetIntrospectionEndpointUris(addresses)); - Assert.Equal(nameof(addresses), exception.ParamName); + var exception = Assert.Throws(() => builder.SetIntrospectionEndpointUris(addresses: null as Uri[])); + Assert.Equal("addresses", exception.ParamName); } [Fact] @@ -918,25 +920,23 @@ namespace OpenIddict.Server.Tests // Arrange var services = CreateServices(); var builder = CreateBuilder(services); - string[] addresses = null; // Act and assert - var exception = Assert.Throws(() => builder.SetIntrospectionEndpointUris(addresses)); - Assert.Equal(nameof(addresses), exception.ParamName); + var exception = Assert.Throws(() => builder.SetIntrospectionEndpointUris(addresses: null as string[])); + Assert.Equal("addresses", exception.ParamName); } [Theory] - [InlineData(InvalidUriString)] + [InlineData(@"C:\")] public void SetIntrospectionEndpointUris_ThrowsExceptionForUri(string uri) { // Arrange var services = CreateServices(); var builder = CreateBuilder(services); - Uri[] addresses = { new Uri(uri), }; // Act and assert - var exception = Assert.Throws(() => builder.SetIntrospectionEndpointUris(addresses)); - Assert.Equal(nameof(addresses), exception.ParamName); + var exception = Assert.Throws(() => builder.SetIntrospectionEndpointUris(new Uri(uri))); + Assert.Equal("addresses", exception.ParamName); Assert.Contains("One of the specified addresses is not valid.", exception.Message); } @@ -978,11 +978,10 @@ namespace OpenIddict.Server.Tests // Arrange var services = CreateServices(); var builder = CreateBuilder(services); - Uri[] addresses = null; // Act and assert - var exception = Assert.Throws(() => builder.SetLogoutEndpointUris(addresses)); - Assert.Equal(nameof(addresses), exception.ParamName); + var exception = Assert.Throws(() => builder.SetLogoutEndpointUris(addresses: null as Uri[])); + Assert.Equal("addresses", exception.ParamName); } [Fact] @@ -991,25 +990,23 @@ namespace OpenIddict.Server.Tests // Arrange var services = CreateServices(); var builder = CreateBuilder(services); - string[] addresses = null; // Act and assert - var exception = Assert.Throws(() => builder.SetLogoutEndpointUris(addresses)); - Assert.Equal(nameof(addresses), exception.ParamName); + var exception = Assert.Throws(() => builder.SetLogoutEndpointUris(addresses: null as string[])); + Assert.Equal("addresses", exception.ParamName); } [Theory] - [InlineData(InvalidUriString)] + [InlineData(@"C:\")] public void SetLogoutEndpointUris_ThrowsExceptionForUri(string uri) { // Arrange var services = CreateServices(); var builder = CreateBuilder(services); - Uri[] addresses = { new Uri(uri), }; // Act and assert - var exception = Assert.Throws(() => builder.SetLogoutEndpointUris(addresses)); - Assert.Equal(nameof(addresses), exception.ParamName); + var exception = Assert.Throws(() => builder.SetLogoutEndpointUris(new Uri(uri))); + Assert.Equal("addresses", exception.ParamName); Assert.Contains("One of the specified addresses is not valid.", exception.Message); } @@ -1051,11 +1048,10 @@ namespace OpenIddict.Server.Tests // Arrange var services = CreateServices(); var builder = CreateBuilder(services); - Uri[] addresses = null; // Act and assert - var exception = Assert.Throws(() => builder.SetRevocationEndpointUris(addresses)); - Assert.Equal(nameof(addresses), exception.ParamName); + var exception = Assert.Throws(() => builder.SetRevocationEndpointUris(addresses: null as Uri[])); + Assert.Equal("addresses", exception.ParamName); } [Fact] @@ -1064,25 +1060,23 @@ namespace OpenIddict.Server.Tests // Arrange var services = CreateServices(); var builder = CreateBuilder(services); - string[] addresses = null; // Act and assert - var exception = Assert.Throws(() => builder.SetRevocationEndpointUris(addresses)); - Assert.Equal(nameof(addresses), exception.ParamName); + var exception = Assert.Throws(() => builder.SetRevocationEndpointUris(addresses: null as string[])); + Assert.Equal("addresses", exception.ParamName); } [Theory] - [InlineData(InvalidUriString)] + [InlineData(@"C:\")] public void SetRevocationEndpointUris_ThrowsExceptionForUri(string uri) { // Arrange var services = CreateServices(); var builder = CreateBuilder(services); - Uri[] addresses = { new Uri(uri), }; // Act and assert - var exception = Assert.Throws(() => builder.SetRevocationEndpointUris(addresses)); - Assert.Equal(nameof(addresses), exception.ParamName); + var exception = Assert.Throws(() => builder.SetRevocationEndpointUris(new Uri(uri))); + Assert.Equal("addresses", exception.ParamName); Assert.Contains("One of the specified addresses is not valid.", exception.Message); } @@ -1124,11 +1118,10 @@ namespace OpenIddict.Server.Tests // Arrange var services = CreateServices(); var builder = CreateBuilder(services); - Uri[] addresses = null; // Act and assert - var exception = Assert.Throws(() => builder.SetTokenEndpointUris(addresses)); - Assert.Equal(nameof(addresses), exception.ParamName); + var exception = Assert.Throws(() => builder.SetTokenEndpointUris(addresses: null as Uri[])); + Assert.Equal("addresses", exception.ParamName); } [Fact] @@ -1137,25 +1130,23 @@ namespace OpenIddict.Server.Tests // Arrange var services = CreateServices(); var builder = CreateBuilder(services); - string[] addresses = null; // Act and assert - var exception = Assert.Throws(() => builder.SetTokenEndpointUris(addresses)); - Assert.Equal(nameof(addresses), exception.ParamName); + var exception = Assert.Throws(() => builder.SetTokenEndpointUris(addresses: null as string[])); + Assert.Equal("addresses", exception.ParamName); } [Theory] - [InlineData(InvalidUriString)] + [InlineData(@"C:\")] public void SetTokenEndpointUris_ThrowsExceptionForUri(string uri) { // Arrange var services = CreateServices(); var builder = CreateBuilder(services); - Uri[] addresses = { new Uri(uri), }; // Act and assert - var exception = Assert.Throws(() => builder.SetTokenEndpointUris(addresses)); - Assert.Equal(nameof(addresses), exception.ParamName); + var exception = Assert.Throws(() => builder.SetTokenEndpointUris(new Uri(uri))); + Assert.Equal("addresses", exception.ParamName); Assert.Contains("One of the specified addresses is not valid.", exception.Message); } @@ -1197,11 +1188,10 @@ namespace OpenIddict.Server.Tests // Arrange var services = CreateServices(); var builder = CreateBuilder(services); - Uri[] addresses = null; // Act and assert - var exception = Assert.Throws(() => builder.SetUserinfoEndpointUris(addresses)); - Assert.Equal(nameof(addresses), exception.ParamName); + var exception = Assert.Throws(() => builder.SetUserinfoEndpointUris(addresses: null as Uri[])); + Assert.Equal("addresses", exception.ParamName); } [Fact] @@ -1210,25 +1200,23 @@ namespace OpenIddict.Server.Tests // Arrange var services = CreateServices(); var builder = CreateBuilder(services); - string[] addresses = null; // Act and assert - var exception = Assert.Throws(() => builder.SetUserinfoEndpointUris(addresses)); - Assert.Equal(nameof(addresses), exception.ParamName); + var exception = Assert.Throws(() => builder.SetUserinfoEndpointUris(addresses: null as string[])); + Assert.Equal("addresses", exception.ParamName); } [Theory] - [InlineData(InvalidUriString)] + [InlineData(@"C:\")] public void SetUserinfoEndpointUris_ThrowsExceptionForUri(string uri) { // Arrange var services = CreateServices(); var builder = CreateBuilder(services); - Uri[] addresses = {new Uri(uri), }; // Act and assert - var exception = Assert.Throws(() => builder.SetUserinfoEndpointUris(addresses)); - Assert.Equal(nameof(addresses), exception.ParamName); + var exception = Assert.Throws(() => builder.SetUserinfoEndpointUris(new Uri(uri))); + Assert.Equal("addresses", exception.ParamName); Assert.Contains("One of the specified addresses is not valid.", exception.Message); } @@ -1264,6 +1252,76 @@ namespace OpenIddict.Server.Tests Assert.Contains(new Uri("http://localhost/endpoint-path"), options.UserinfoEndpointUris); } + [Fact] + public void SetVerificationEndpointUris_ThrowsExceptionWhenAddressesIsNull() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act and assert + var exception = Assert.Throws(() => builder.SetVerificationEndpointUris(addresses: null as Uri[])); + Assert.Equal("addresses", exception.ParamName); + } + + [Fact] + public void SetVerificationEndpointUris_Strings_ThrowsExceptionWhenAddressesIsNull() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act and assert + var exception = Assert.Throws(() => builder.SetVerificationEndpointUris(addresses: null as string[])); + Assert.Equal("addresses", exception.ParamName); + } + + [Theory] + [InlineData(@"C:\")] + public void SetVerificationEndpointUris_ThrowsExceptionForUri(string uri) + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act and assert + var exception = Assert.Throws(() => builder.SetVerificationEndpointUris(new Uri(uri))); + Assert.Equal("addresses", exception.ParamName); + Assert.Contains("One of the specified addresses is not valid.", exception.Message); + } + + [Fact] + public void SetVerificationEndpointUris_ClearsUris() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act + builder.SetVerificationEndpointUris(Array.Empty()); + + var options = GetOptions(services); + + // Assert + Assert.Empty(options.VerificationEndpointUris); + } + + [Fact] + public void SetVerificationEndpointUris_AddsUri() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act + builder.SetVerificationEndpointUris("http://localhost/endpoint-path"); + + var options = GetOptions(services); + + // Assert + Assert.Contains(new Uri("http://localhost/endpoint-path"), options.VerificationEndpointUris); + } + [Fact] public void AcceptAnonymousClients_ClientIdentificationIsOptional() { @@ -1524,11 +1582,10 @@ namespace OpenIddict.Server.Tests // Arrange var services = CreateServices(); var builder = CreateBuilder(services); - string[] claims = null; // Act and assert - var exception = Assert.Throws(() => builder.RegisterClaims(claims)); - Assert.Equal(nameof(claims), exception.ParamName); + var exception = Assert.Throws(() => builder.RegisterClaims(claims: null)); + Assert.Equal("claims", exception.ParamName); } [Theory] @@ -1570,11 +1627,10 @@ namespace OpenIddict.Server.Tests // Arrange var services = CreateServices(); var builder = CreateBuilder(services); - string[] scopes = null; // Act and assert - var exception = Assert.Throws(() => builder.RegisterScopes(scopes)); - Assert.Equal(nameof(scopes), exception.ParamName); + var exception = Assert.Throws(() => builder.RegisterScopes(scopes: null)); + Assert.Equal("scopes", exception.ParamName); } [Theory] @@ -1641,102 +1697,6 @@ namespace OpenIddict.Server.Tests Assert.True(options.UseRollingRefreshTokens); } - [Fact] - public void SetVerificationEndpointUris_ThrowsExceptionWhenNullAddresses() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - Uri[] addresses = null; - - // Act and assert - var exception = Assert.Throws(() => builder.SetVerificationEndpointUris(addresses)); - Assert.Equal(nameof(addresses), exception.ParamName); - } - - [Fact] - public void SetVerificationEndpointUris_Strings_ThrowsExceptionWhenNullAddresses() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - string[] addresses = null; - - // Act and assert - var exception = Assert.Throws(() => builder.SetVerificationEndpointUris(addresses)); - Assert.Equal(nameof(addresses), exception.ParamName); - } - - [Fact] - public void SetVerificationEndpointUris_Strings_AddedUriIsRelativeOrAbsoluteUriKind() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - string[] addresses = {"http://localhost/verify"}; - - // Act - builder.SetVerificationEndpointUris(addresses); - - var options = GetOptions(services); - - // Assert - Assert.True(options.VerificationEndpointUris[0].IsAbsoluteUri); - } - - [Theory] - [InlineData(InvalidUriString)] - public void SetVerificationEndpointUris_ThrowsExceptionForUri(string uri) - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - Uri[] addresses = { new Uri(uri)}; - - // Act and assert - var exception = Assert.Throws(() => builder.SetVerificationEndpointUris(addresses)); - Assert.Equal(nameof(addresses), exception.ParamName); - Assert.Contains("One of the specified addresses is not valid.", exception.Message); - } - - [Fact] - public void SetVerificationEndpointUris_ClearsExistingUris() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - Uri[] addresses = Array.Empty(); - - // Act - builder.SetVerificationEndpointUris(addresses); - - var options = GetOptions(services); - - // Assert - Assert.Empty(options.VerificationEndpointUris); - } - - [Theory] - [InlineData("http://localhost/verify")] - [InlineData("http://localhost/verify-1")] - [InlineData("http://localhost/verification")] - [InlineData("http://localhost/verification-1")] - public void SetVerificationEndpointUris_AddsUri(string uri) - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - Uri[] addresses = { new Uri(uri), }; - - // Act - builder.SetVerificationEndpointUris(addresses); - - var options = GetOptions(services); - - // Assert - Assert.Contains(addresses[0], options.VerificationEndpointUris); - } - private static IServiceCollection CreateServices() { return new ServiceCollection().AddOptions();