diff --git a/Directory.Build.targets b/Directory.Build.targets
index 6db20806..93a20cbe 100644
--- a/Directory.Build.targets
+++ b/Directory.Build.targets
@@ -68,6 +68,7 @@
$(DefineConstants);SUPPORTS_HTTP_CLIENT_DEFAULT_REQUEST_VERSION_POLICY
$(DefineConstants);SUPPORTS_HTTP_CLIENT_RESILIENCE
$(DefineConstants);SUPPORTS_INT32_RANDOM_NUMBER_GENERATOR_METHODS
+ $(DefineConstants);SUPPORTS_KESTREL_TLS_HANDSHAKE_CALLBACK_OPTIONS
$(DefineConstants);SUPPORTS_MULTIPLE_VALUES_IN_QUERYHELPERS
$(DefineConstants);SUPPORTS_NAMED_PIPE_STATIC_FACTORY_WITH_ACL
$(DefineConstants);SUPPORTS_ONE_SHOT_HASHING_METHODS
@@ -78,6 +79,9 @@
$(DefineConstants);SUPPORTS_TEXT_ELEMENT_ENUMERATOR
$(DefineConstants);SUPPORTS_VALUETASK_COMPLETED_TASK
$(DefineConstants);SUPPORTS_WINFORMS_TASK_DIALOG
+ $(DefineConstants);SUPPORTS_X509_CHAIN_POLICY_CUSTOM_TRUST_STORE
+ $(DefineConstants);SUPPORTS_X509_CHAIN_POLICY_DOWNLOAD_MODE
+ $(DefineConstants);SUPPORTS_X509_CHAIN_POLICY_TRUST_MODE
$(DefineConstants);SUPPORTS_ZLIB_COMPRESSION
@@ -114,6 +118,8 @@
$(DefineConstants);SUPPORTS_JSON_ELEMENT_DEEP_EQUALS
$(DefineConstants);SUPPORTS_JSON_ELEMENT_PROPERTY_COUNT
$(DefineConstants);SUPPORTS_TYPE_DESCRIPTOR_TYPE_REGISTRATION
+ $(DefineConstants);SUPPORTS_X509_CHAIN_POLICY_CLONING
+ $(DefineConstants);SUPPORTS_X509_CHAIN_POLICY_VERIFICATION_TIME_MODE
()
+ .Cast()
.SingleOrDefault();
}
}
@@ -652,7 +652,7 @@ public sealed partial class OpenIddictClientWebIntegrationBuilder
return Set{{ setting.property_name }}(
store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false)
- .OfType()
+ .Cast()
.SingleOrDefault() ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0066)));
}
{{~ else if setting.clr_type == 'bool' ~}}
@@ -1163,8 +1163,7 @@ public sealed partial class OpenIddictClientWebIntegrationConfiguration
if (settings.{{ setting.property_name }} is not null)
{
// If the signing key is an asymmetric security key, ensure it has a private key.
- if (settings.{{ setting.property_name }} is AsymmetricSecurityKey asymmetricSecurityKey &&
- asymmetricSecurityKey.PrivateKeyStatus is PrivateKeyStatus.DoesNotExist)
+ if (settings.{{ setting.property_name }} is AsymmetricSecurityKey { PrivateKeyStatus: PrivateKeyStatus.DoesNotExist })
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0067));
}
diff --git a/sandbox/OpenIddict.Sandbox.AspNet.Client/Startup.cs b/sandbox/OpenIddict.Sandbox.AspNet.Client/Startup.cs
index 7e7932e0..3e9e1b59 100644
--- a/sandbox/OpenIddict.Sandbox.AspNet.Client/Startup.cs
+++ b/sandbox/OpenIddict.Sandbox.AspNet.Client/Startup.cs
@@ -71,7 +71,7 @@ public class Startup
ProviderDisplayName = "Local OIDC server",
ClientId = "mvc",
- ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654",
+ ClientSecret = "emCimpdc9SeOaZzN5jzm4_eek-STF6VenfVlKO1_qt0",
Scopes = { Scopes.Email, Scopes.Profile, Scopes.OfflineAccess, "demo_api" },
RedirectUri = new Uri("callback/login/local", UriKind.Relative),
diff --git a/sandbox/OpenIddict.Sandbox.AspNet.Server/Startup.cs b/sandbox/OpenIddict.Sandbox.AspNet.Server/Startup.cs
index cd45438d..c5e20904 100644
--- a/sandbox/OpenIddict.Sandbox.AspNet.Server/Startup.cs
+++ b/sandbox/OpenIddict.Sandbox.AspNet.Server/Startup.cs
@@ -199,7 +199,7 @@ public class Startup
{
ApplicationType = ApplicationTypes.Web,
ClientId = "mvc",
- ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654",
+ ClientSecret = "emCimpdc9SeOaZzN5jzm4_eek-STF6VenfVlKO1_qt0",
ClientType = ClientTypes.Confidential,
ConsentType = ConsentTypes.Systematic,
DisplayName = "MVC client application",
diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Startup.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Startup.cs
index 53115126..0f017395 100644
--- a/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Startup.cs
+++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Startup.cs
@@ -1,4 +1,5 @@
using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
@@ -112,26 +113,52 @@ public class Startup
PostLogoutRedirectUri = new Uri("callback/logout/local", UriKind.Relative),
#if SUPPORTS_PEM_ENCODED_KEY_IMPORT
- // On supported platforms, this application authenticates by generating JWT client
- // assertions that are signed using a signing key instead of using a client secret.
+ // On supported platforms, this application can authenticate using 3 different client
+ // authentication methods that all offer a higher security level than shared client secrets:
//
- // As such, no client secret is set, but an ECDSA key is registered and used by
- // the OpenIddict client to automatically generate client assertions when needed.
+ // 1) tls_client_auth (PKI-based mutual TLS authentication): while it requires
+ // setting up a proper Public Key Infrastructure, this method offers a very
+ // high level of security, as the authorization server never has access to the
+ // private key used by the client to authenticate itself and can dynamically check
+ // the revocation status of the client certificate using standard PKI mechanisms.
//
- // Note: while the server only needs access to the public key, the client needs
- // to know the private key to be able to generate and sign the client assertions.
+ // 2) self_signed_tls_client_auth (self-signed certificate-based mutual TLS authentication):
+ // this method is easier to deploy than PKI-based mutual TLS authentication, while
+ // still offering a high level of security. Unlike PKI-based mutual TLS authentication,
+ // the revocation status of the client certificate is never checked but certificates can
+ // be "revoked" by being removed from the JSON Web Key Set associated with the client.
+ //
+ // 3) private_key_jwt (JWT client assertions signed with a private key): while this
+ // method doesn't offer the same security guarantees as mutual TLS authentication,
+ // it is more secure than shared secrets and doesn't have the operational constraints
+ // required by the two mutual TLS methods described above (such as TLS configuration).
+ //
+ // The actual client authentication method used by the OpenIddict client is automatically
+ // selected based on the registered credentials and the methods supported by the server:
+ // when supported by the server, mutual TLS authentication methods are always preferred.
+ //
+ // In all cases, no client secret is necessary but the client needs to be able to access the
+ // private key of the certificate/key to be able to generate and sign the client assertions.
+
SigningCredentials =
{
- new SigningCredentials(GetECDsaSigningKey($"""
- -----BEGIN EC PRIVATE KEY-----
- MHcCAQEEIMGxf/eMzKuW2F8KKWPJo3bwlrO68rK5+xCeO1atwja2oAoGCCqGSM49
- AwEHoUQDQgAEI23kaVsRRAWIez/pqEZOByJFmlXda6iSQ4QqcH23Ir8aYPPX5lsV
- nBsExNsl7SOYOiIhgTaX6+PTS7yxTnmvSw==
- -----END EC PRIVATE KEY-----
- """), SecurityAlgorithms.EcdsaSha256, SecurityAlgorithms.Sha256)
- }
+ // Note: this certificate can be used with either tls_client_auth or private_key_jwt,
+ // depending on the server configuration (and the client authentication methods explicitly
+ // configured via OpenIddictClientRegistration.ClientAuthenticationMethods, if applicable).
+ //
+ // GetPublicKeyInfrastructureCertificate(),
+
+ // Note: this certificate can be used with either self_signed_tls_client_auth or private_key_jwt,
+ // depending on the server configuration (and the client authentication methods explicitly
+ // configured via OpenIddictClientRegistration.ClientAuthenticationMethods, if applicable):
+ //
+ // GetSelfSignedCertificate(),
+
+ // Note: this key can only be used with private_key_jwt as raw keys cannot be used with TLS.
+ GetSigningKey()
+ },
#else
- ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654"
+ ClientSecret = "emCimpdc9SeOaZzN5jzm4_eek-STF6VenfVlKO1_qt0"
#endif
});
@@ -165,13 +192,181 @@ public class Startup
});
#if SUPPORTS_PEM_ENCODED_KEY_IMPORT
- static ECDsaSecurityKey GetECDsaSigningKey(ReadOnlySpan key)
+#pragma warning disable CS8321
+ static X509SigningCredentials GetPublicKeyInfrastructureCertificate()
+ {
+ // Note: OpenIddict only negotiates PKI-based or self-signed mutual
+ // TLS authentication if the certificate explicitly contains the
+ // "digitalSignature" key usage and the "clientAuth" extended key usage.
+ var certificate = X509Certificate2.CreateFromPem(
+ certPem: $"""
+ -----BEGIN CERTIFICATE-----
+ MIIEezCCAmOgAwIBAgIRALTZE9ezjPCWDFr38cp6AMAwDQYJKoZIhvcNAQELBQAw
+ GjEYMBYGA1UEAxMPSW50ZXJtZWRpYXRlIENBMCAXDTI2MDIwMTE0MTQ0M1oYDzIx
+ MjYwMjAyMTQxNDQzWjAaMRgwFgYDVQQDEw9FbmQgY2VydGlmaWNhdGUwggEiMA0G
+ CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDMs9spnvKeKw6VwPbpB47ikC6bL0Cn
+ S+K19Fp8dJg8b4dA1J1Y8dA2gi2nU/+ntOMYp1A6EvMZ8UpbgnSmhUN/2JQFU5Hc
+ PP0/IMjZAl2Iseh2yiK3Ril4Agbng6YW7e9P5YtMV+6i/stYujwNTXsUMr/+QSUI
+ Nze7856XSIl9gRjWEKJ17Jk/tJpun/zdpl4hXcptrsxxLU/E03bC3LcjiXzg8/Zl
+ 3/oEHqcHfv9C8RTdIBBw66zJAYzGfxwV31cJ9QQ2udlipi2l+ZR6jFWzzJI4XmiC
+ FzdwZRvhMLJsyK5miVIl0qPp3zJ2IyEb/2pLA0bc/ylZwVq6Z49k2xhZAgMBAAGj
+ gbkwgbYwDAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0lAQH/BAww
+ CgYIKwYBBQUHAwIwHQYDVR0OBBYEFHPamNF/deBBv5JpDwiiRctPw4ziMEkGA1Ud
+ IwRCMECAFOEWwW18w3rZ6/5iIwAB12592OlHoRakFDASMRAwDgYDVQQDEwdSb290
+ IENBghBvqw/xqI/LNgVfSURP9ck7MBQGA1UdEQQNMAuCCWxvY2FsaG9zdDANBgkq
+ hkiG9w0BAQsFAAOCAgEAbBq73JzDpK11hoRUxHq7LvplQEe/FNuD/slvn9Crfm2d
+ jJj0HsQQZpMgxP7SZ9FNvFqCo+/dm9PchIlwqwSjWtTxgYmcMOXw0Rzst85Ug4U1
+ I2PG6iPxJ4WLSW2gzo//jFPa7MD1AnqDYwcCQTVsQW6aJavY3mFD31SJKsvSKqsV
+ 6xTXsajLRetCSXGe5qFgfyLC9tOhtTWXsCed/ISoQ9bljhOSqT6pxkpOVu0AHHMB
+ 1CMZay/B5ecjb66mwSoRcAPweMlAYJkjU5HXHSi7kB3gRQTsb1ZymEn67Q4C5cpI
+ Lq6UFK5bWZf1A0kFbYJBmn3oHsWxMQqv0F6QE7r4Mg6pfk9swzYZ8WqcgjiGHQET
+ pVU7ZKkUsg2JREXxRnhh5+Q+vGsF/DjhzQ6NrfPm8sqs+X+LzUN2cne8ZPclfyW2
+ VKCHTPZ6o8mELiAlIPdBYUYsgUEOsfmUWbx4wfx5IB7vnenrenInLLyGOOCxR33d
+ o/gDMLFdeKHXK2ISsbDCk+zwEF8kztn1cXWK+K6H9cr8oJjDi1OJwTkqz9msar+9
+ mjZ1CPAF0X+mLgrhVnNYqd5oqeeLerXKkAvpC2TgvlWJRGyDILhjva3J+2fQAYXZ
+ +OKFHNPf3n8Co4s5TMr1eiGVtS1etH6hPxnn5Jwnes9JZWFRLcjeTmPLSRWFucg=
+ -----END CERTIFICATE-----
+ """,
+ keyPem: $"""
+ -----BEGIN RSA PRIVATE KEY-----
+ MIIEpAIBAAKCAQEAzLPbKZ7ynisOlcD26QeO4pAumy9Ap0vitfRafHSYPG+HQNSd
+ WPHQNoItp1P/p7TjGKdQOhLzGfFKW4J0poVDf9iUBVOR3Dz9PyDI2QJdiLHodsoi
+ t0YpeAIG54OmFu3vT+WLTFfuov7LWLo8DU17FDK//kElCDc3u/Oel0iJfYEY1hCi
+ deyZP7Sabp/83aZeIV3Kba7McS1PxNN2wty3I4l84PP2Zd/6BB6nB37/QvEU3SAQ
+ cOusyQGMxn8cFd9XCfUENrnZYqYtpfmUeoxVs8ySOF5oghc3cGUb4TCybMiuZolS
+ JdKj6d8ydiMhG/9qSwNG3P8pWcFaumePZNsYWQIDAQABAoIBACorfyHC4d5dpmKJ
+ XxRAf1oDM+a6REpyoqCzVxS+fEIvA6ECa+vP3QHtrXQEJO2qoQIKLcfY8YXNpHDX
+ nipT18T1nADA55KEafNgUKAMEbLAW9Bk8ePpq09Ss5NsFoIwwBUoh5rRnpKrhL6h
+ lw9yf8F4dv7s8rEPlwa8OFaYFeLpoBLsPaX3nMu45CKb25dFZzSv9ORVs28LALrS
+ oK9MbtNFkmf/4EmpYA+nblkZd2bu4BomOF7C2F4bwtikN29vl4NPMhlbZGTy1hm9
+ jzMOOvO1DwvIjHRVcfHKMDZ7cw1Pj5TmeApToSs6ygu1lce0GQcNVm+KV2qZMjNQ
+ Al6cdFUCgYEA7HZ6wvZU/WA7ei+jIwrJdyQQ3jU/LAu7GGhXiMU3z2RSR/vieY5R
+ 4IjQOgUkLBuQcy9uoQcSLpH/SNLIi6qhlMBvZuHq9QKF60t68tuW0PFSoa+SKaEn
+ DCZ70bnxo4OSRUtrzxikYHnwOvRGEli4EAOENETaQBKJUUygov/pOWMCgYEA3Z2a
+ TJlptRq75G6LHZvbBBzZdG9Mr04O6zvh5TGsJW86b6ov9BTAGz6Z37KWR1yUDfyH
+ dqNf90kJ8hs1eO6gGDQyGaH9yerrlULukANQfvpC0rEeJ7DfXSc1iLa3Q6+AOt5v
+ 9TkQY7s/47iOPoCmblZ4FeVcIMx88ms2mBRXshMCgYAN9pkdNiqio7Ifbvy1Lwfi
+ jzCnzoEierbbpB23J9450vTA53DiOLNBDRMuuer+58nJ430m6SH7ugdXJ4tMJBFS
+ lWJ+ssyLF1ENKfHisXDgeb+laJa6+pcxsnwRUGeifjx+9wswuYXLZKf48z/ICZEk
+ 8PA3nfE9Y1rUgC/kMDR3fQKBgQCyQRRdTICUJV7ATJIlTLmLw1C9sNBzqUuitlXq
+ rluS+LZ+HtvXbeFfiKjoH5N07ug/n8GuEZcdJmiTjoMiNH4dOc6ag4vJH+ZB9sZA
+ nAnhOJcLNV/V+RSQrvsGbkFWdhGkSEqxaibesTyghFAVwhEcavzIT+Yck55ktwwA
+ o0wudQKBgQDR0hyl/cf6MBgZ3gce6dOcznLKoa2icypmmfNkA6sqwXwW20/WfDGb
+ ZNdaL4U3xReSN1mzrs0yStq0UrAChwrwqJc6T7uhGR/lDjvJCeZP9zO2yCSBvtul
+ LWFkJnofc7NUYkhVSGaAMeT14xUY/XlFbkXp0jZOqKMRo7PeeeXZaQ==
+ -----END RSA PRIVATE KEY-----
+ """);
+
+ // On Windows, a certificate loaded from PEM-encoded material is ephemeral and
+ // cannot be directly used with TLS, as Schannel cannot access it in this case.
+ //
+ // To work this limitation, the certificate is exported and re-imported from a
+ // PFX blob to ensure the private key is persisted in a way that Schannel can use.
+ //
+ // In a real world application, the certificate wouldn't be embedded in the source code
+ // and would be installed in the certificate store, making this workaround unnecessary.
+ if (OperatingSystem.IsWindows())
+ {
+ certificate = X509CertificateLoader.LoadPkcs12(
+ data : certificate.Export(X509ContentType.Pfx, string.Empty),
+ password : string.Empty,
+ keyStorageFlags: X509KeyStorageFlags.DefaultKeySet);
+ }
+
+ return new X509SigningCredentials(certificate);
+ }
+
+ static X509SigningCredentials GetSelfSignedCertificate()
+ {
+ // Note: OpenIddict only negotiates PKI-based or self-signed mutual
+ // TLS authentication if the certificate explicitly contains the
+ // "digitalSignature" key usage and the "clientAuth" extended key usage.
+ var certificate = X509Certificate2.CreateFromPem(
+ certPem: $"""
+ -----BEGIN CERTIFICATE-----
+ MIIC8zCCAdugAwIBAgIJAIZ9BN3TUnZQMA0GCSqGSIb3DQEBCwUAMCIxIDAeBgNV
+ BAMTF1NlbGYtc2lnbmVkIGNlcnRpZmljYXRlMCAXDTI2MDIwMjE0MzM0OVoYDzIx
+ MjYwMjAyMTQzMzQ5WjAiMSAwHgYDVQQDExdTZWxmLXNpZ25lZCBjZXJ0aWZpY2F0
+ ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOtfKVPM7ghVFh4U/sz4
+ sTrpaNJGQ2NORqawYxAHwluhr101yIOW7rWvFlFncA64Lkq9SAbFFCVSAbo28c6B
+ 2Mi41jyC4LHQU11jhv08K/3FUuckCuzEpzTnXUhxJHWxrRDVEuvKINGPs1VgVtTT
+ ra8rjP8s1YRAzCYnByxSx+8GXNGHprylLh0agpWKb2+2FYwDqY5ME2g3xTL9FTUu
+ FYWTcyspsvN0U1Eo1vlCeOxSYGPRct0MK0AS6eXEGBv+3kCYI7a5+UhQok0WvErF
+ pjIVo7USISDgKhW9GhTsWN+WywwdG4Kx4V6SB8ZLAHFSBSR3gjWS3TGOyqAWoBXc
+ znkCAwEAAaMqMCgwDgYDVR0PAQH/BAQDAgeAMBYGA1UdJQEB/wQMMAoGCCsGAQUF
+ BwMCMA0GCSqGSIb3DQEBCwUAA4IBAQBf5i/S7shmNalVxMuP8/Mk8cOhRRZjnAXd
+ zz3eOuXu0CH8iY/DwCgss04O2NTxuz87rKiuNKOrtY0oN/G4aFjWPvbgoQ+N1XP1
+ zvbhqbyo3fQr07FyjWkrIUoHYFQ3JRfL+GPGjWizJsgdpdCRJSK6G9VX8eU3Akjv
+ YhMRLmbkrH5etOURqFtLpZlxNmLzCpqWIvzRiYyyj74iOipA2I0acgcvkakWn6rE
+ Wio7luBAZ3dXlukEfHTOg+ft4k0nOlRXPTtASOmyFQBOs6iYJeztHDz6MQnknAPe
+ +W53US8kLWktspcOQmxhVVH1g1/T4ynl9iX7tzqvUbdYwZNi92+x
+ -----END CERTIFICATE-----
+ """,
+ keyPem: $"""
+ -----BEGIN RSA PRIVATE KEY-----
+ MIIEpAIBAAKCAQEA618pU8zuCFUWHhT+zPixOulo0kZDY05GprBjEAfCW6GvXTXI
+ g5buta8WUWdwDrguSr1IBsUUJVIBujbxzoHYyLjWPILgsdBTXWOG/Twr/cVS5yQK
+ 7MSnNOddSHEkdbGtENUS68og0Y+zVWBW1NOtryuM/yzVhEDMJicHLFLH7wZc0Yem
+ vKUuHRqClYpvb7YVjAOpjkwTaDfFMv0VNS4VhZNzKymy83RTUSjW+UJ47FJgY9Fy
+ 3QwrQBLp5cQYG/7eQJgjtrn5SFCiTRa8SsWmMhWjtRIhIOAqFb0aFOxY35bLDB0b
+ grHhXpIHxksAcVIFJHeCNZLdMY7KoBagFdzOeQIDAQABAoIBAQDgBOqov9uuQo2S
+ hBkfrXPBxnXl7MomslG8RRWEJF5wKCtoY9A8rmL0uXhccj7NQ6+LoyvyhZDvFGZg
+ ffsXua5DHOmLHmYN12IA+MF6NNMJ7c1CAaQERgd+6tZ2JHm3Kyy1YJdppDAoRMVC
+ 9Tavyej9WE4ScPGntqSXi33gScnRTEGuuC0HydomT/rmguSWx8oPumeWelSTCh9c
+ vZ9Q1NOnRlW/VrNbYyyByiaWEgdrM2E/z3p+MFgrIsYxnIGQ/Ql1FbT0LxbeIYzc
+ 9MT4cbOlMrD0SZVk9lyxnCs/c1pN7pXDHutmDg6JzSj0xW5AYKzKSvXKjy7+uQay
+ YVyYh/QhAoGBAPKL1cZJMqwdQBzHMaHChth5cMh8/IkU6m3U7Ll75dztmaLFce+Y
+ Ova6te/D5Cm/l9pxx+vL5fuAafc2/FTesmKkE2DEERvy4EOQqB1Uho6XEoBBfnJT
+ 0xmNY5Jvh0TfyquS23KvzezT7+epFYNhZDQwgWPnx2z+jwa/zn8Ows/nAoGBAPht
+ crkmXBMncO7CXzFzFbDghIitW9cZnqBTzKwr2k9lVsbioTIYDbGruvABwI5sN2b4
+ gJqcvnkun7dmooRPAGX/nMl5UxeGhdSlYGVzHchZz/310MdEg/JThIV219sHR5fd
+ pBlrydWDyfDTkiGZHDiYUzuZ6hCyOjf+MUgGlyKfAoGBAKYblF1G9hgftC/BT8Fb
+ quQIT3BPANiU5XQwtarWKndilax/EmenVwJwnndFLjZVS5dEA0n+i1Px/yBanPc2
+ yO57NfY4cQs2C9bZ8/iaUcjHt9j0gbekptdCGKZKEVbe+TsFyZrCwgHmp8984gnn
+ IiwH6CVWsCJ6N9PEepRTtKGTAoGAV/wTdKW0WIhQhA9NPas/1GxAJFQZwd3uA2SK
+ ibPiVtpSWJAtfRttxi5HP/eu5gJHwO1kRt4ay7qKkJ8GEgwU3Qsh0W1p01wui/ii
+ YmvZ8Xp1osFr1xdaD/oqZkaH/qfeYFf8ZZB6ZGePnv6fs8yRZS311JcXgiBNZEVf
+ 2N2Uq4sCgYAoVe3zkP37MjIH6nykFiR396den5ZyMflR42QtO0Z2QJuQKs6yZ7ii
+ cqQy4r1Z2i6bdtUlesyGF5U7BPvcers/Mczax0u81Y2S9PdIsv8cw8sr8M6HHiS3
+ IWBJpVJNyoHKLusRTYVqti+b5EHXQ55FZ9EJggvceGbcBamZ+ynYrg==
+ -----END RSA PRIVATE KEY-----
+ """);
+
+ // On Windows, a certificate loaded from PEM-encoded material is ephemeral and
+ // cannot be directly used with TLS, as Schannel cannot access it in this case.
+ //
+ // To work this limitation, the certificate is exported and re-imported from a
+ // PFX blob to ensure the private key is persisted in a way that Schannel can use.
+ //
+ // In a real world application, the certificate wouldn't be embedded in the source code
+ // and would be installed in the certificate store, making this workaround unnecessary.
+ if (OperatingSystem.IsWindows())
+ {
+ certificate = X509CertificateLoader.LoadPkcs12(
+ data : certificate.Export(X509ContentType.Pfx, string.Empty),
+ password : string.Empty,
+ keyStorageFlags: X509KeyStorageFlags.DefaultKeySet);
+ }
+
+ return new X509SigningCredentials(certificate);
+ }
+
+ static SigningCredentials GetSigningKey()
{
var algorithm = ECDsa.Create();
- algorithm.ImportFromPem(key);
+ algorithm.ImportFromPem($"""
+ -----BEGIN EC PRIVATE KEY-----
+ MHcCAQEEIMGxf/eMzKuW2F8KKWPJo3bwlrO68rK5+xCeO1atwja2oAoGCCqGSM49
+ AwEHoUQDQgAEI23kaVsRRAWIez/pqEZOByJFmlXda6iSQ4QqcH23Ir8aYPPX5lsV
+ nBsExNsl7SOYOiIhgTaX6+PTS7yxTnmvSw==
+ -----END EC PRIVATE KEY-----
+ """);
+
+ var key = new ECDsaSecurityKey(algorithm);
- return new ECDsaSecurityKey(algorithm);
+ return new SigningCredentials(key, SecurityAlgorithms.EcdsaSha256, SecurityAlgorithms.Sha256);
}
+#pragma warning restore CS8321
#endif
});
diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Startup.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Startup.cs
index c93aff16..9478fa2b 100644
--- a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Startup.cs
+++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Startup.cs
@@ -1,4 +1,8 @@
+using System.Net.Security;
+using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.Identity;
+using Microsoft.AspNetCore.Server.Kestrel.Core;
+using Microsoft.AspNetCore.Server.Kestrel.Https;
using Microsoft.EntityFrameworkCore;
using OpenIddict.Sandbox.AspNetCore.Server.Models;
using OpenIddict.Sandbox.AspNetCore.Server.Services;
@@ -158,6 +162,106 @@ public class Startup
// you don't own, you can disable access token encryption:
//
// options.DisableAccessTokenEncryption();
+
+#if SUPPORTS_KESTREL_TLS_HANDSHAKE_CALLBACK_OPTIONS
+ // Enable both tls_client_auth and self_signed_tls_client_auth to allow clients
+ // to authenticate using either PKI certificates or self-signed certificates.
+ //
+ // Note: PKI and self-signed certificate authentication can be enabled independently.
+ options.EnablePublicKeyInfrastructureClientCertificateAuthentication(
+ [
+ // Root certificate:
+ X509Certificate2.CreateFromPem($"""
+ -----BEGIN CERTIFICATE-----
+ MIIE7jCCAtagAwIBAgIJAN+SZB+xc7usMA0GCSqGSIb3DQEBCwUAMBIxEDAOBgNV
+ BAMTB1Jvb3QgQ0EwIBcNMjYwMjAxMTQxNDQzWhgPMjEyNjAyMDIxNDE0NDNaMBIx
+ EDAOBgNVBAMTB1Jvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC
+ AQC68DOD6IvsJM0mc7n8bYeNaVe8e0ytJCJozdMPNXAe80vMPP4cVUPFvJ/tbSjX
+ yhREJ9xz2dYgQAhWCaTnEHY4AaE1Tj2rYqotenDQxs18qqaqoZlcaFfkRUPHRH3Q
+ iS8D8gbxzlYkjxNsfDJRi0cXFxr4wb4FmSP4ES2DFWWAWbN9wt7Tb2uDiHkjSefZ
+ Pni5F6fN6nE7wgGMYrdrCiiwJf7jEZiIZ60bsiUnJ5VUX6g4ob469CLocH/q/9Yr
+ Dad9/+YYp6SuHZilsPmW4X0fziuF/RvtsRLw4bw5jwj69KH3Y0jqUMQoyzz2CIJz
+ cDMB/MLREgcT9jTVB/M5Pl61DCzR/0d4t6RENpkNqpAIVM0Unp0nDuHPwjoeEZn3
+ vSvUiGpiYY355GaSl05OE3SOKoRHt4lBXvY43y8fRBMOwlNHYn4eO3ZDuzZYzhfs
+ 68ywK4zUy47Qyn1BgNNqc/KC7kzxeLFxqTg2VJgBeXuJfucwzhOFqkOSfpeIGDK9
+ 8MODFlA3usf5LXxQ7DJhkeBgPkW56BUlYVkenm9ORWe77GnoXL95p2HQUEXATHir
+ unXFPVcHET6hyegvc9AzSTZFQL4RMO3ZV5ESs+JF/YY4ycBc1+WYy2kAuP5sfsGn
+ mpeKf2Dv9MGFcDxU7iimeM92n7t5lbCRlX8NUaYeQ8jKgQIDAQABo0UwQzASBgNV
+ HRMBAf8ECDAGAQH/AgEBMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUyHIqQnw/
+ x6e92OuJ+y+e0LSKSqQwDQYJKoZIhvcNAQELBQADggIBACCOjX4+RyoZt9tVoKdT
+ uGFoEJUGBUfcYaMdqiDstuSoNrqZXHnilzj33ZbNqj6X6rS1w5qVnkj7ZY4Wu8MP
+ Sj85Wp9cq0jMv3NfPZfKmJd2K7favGQKvgSPptSl9VgcIrpRam9BG2db1IP053tf
+ ydBB3w/yI7MTb4fkPqLWtKcuPPM8t9SsxAlKhEm+gbNEsqDX9ZIfxolHEpL2zLOi
+ a4v7+SlJdVBfo4mj+iLUeZXFRPglAnPQ3CZngfbPsjEklpOCU1v8TnhHwV8jyCgl
+ oLAceLjdlXHWVfhKU+N0jdAt8V2NPxq+yJ/gPX+J3YOrYRCHMdQZ/OFEUhmkxHNp
+ UUPkL1VJ9c8ZW2/gszFPyvsh7GHwl43y7bN8doiQVOSj6jZ7uCkQl1oz731fl97b
+ FqKVyGGx6UUEi57YS7mWsY02qNvYSObOxhSNusX/Ct06XbXS1Pn+co/3FMGMcEVf
+ IwzboV61sMqRu4l3YD0Z1AxdhXFERMlHBYyyj8CQYIXtnCUoeT40IIR3aFmEG5u8
+ /lwehTnV4slDDMJMFSW54aENpT1XP4b8m46kioNhxN+7ukdcWnYoapePuiDRboA3
+ GwRExwFUDiGO8zpnyvV4JTIGu9MZi51O3RbSlIDDhyzFsdQW3PeztwGjZWZAXD+7
+ qIoAaBbG/12cvNeZH7L9Mcpo
+ -----END CERTIFICATE-----
+ """),
+
+ // Intermediate certificate (optional):
+ X509Certificate2.CreateFromPem($"""
+ -----BEGIN CERTIFICATE-----
+ MIIFQzCCAyugAwIBAgIQb6sP8aiPyzYFX0lET/XJOzANBgkqhkiG9w0BAQsFADAS
+ MRAwDgYDVQQDEwdSb290IENBMCAXDTI2MDIwMTE0MTQ0M1oYDzIxMjYwMjAyMTQx
+ NDQzWjAaMRgwFgYDVQQDEw9JbnRlcm1lZGlhdGUgQ0EwggIiMA0GCSqGSIb3DQEB
+ AQUAA4ICDwAwggIKAoICAQCfObzu9v5dfio8kCCKpb0vLXUrilcOM6FGVx50rPtc
+ MjlHNG4GpghoLXjJxrUIsoeGdsCI6W+K3R+5PRlEsbCT3l/0n2/ixW3rN9rO3FOt
+ VGOHYrE2wI+i1aWP1/w/0bcCbH1J6PLKPv5syzhWWdkoTy2K72gye5Kx3zXkFQoC
+ uBFMvj3HBgmTngaDRTT1QGsRSlhuvoEiHHAvgoTfYt7bgbRhM5I5upEbXB0cucj7
+ Ghzws5R2/4qsr/QorwA8l6aNeb1dm0uB+FlMVlGelYMZ76+SjBs1rOxD0qt82h13
+ BYPBR4gLvNFafOEskFndeP3OkNaQ6kPm+uClj5OxwONnBcy7neJPqZGMtxpApLK3
+ reK3IZ/ieg1nY9zZ7OkqIzQDt1CeCBQWU3RkpEtVojkRDLCmg+pKSjHtLxUUGiQ3
+ UHrXO2Yrej8Qpx4JHdKGUfku25r4SSaj2YF61ZIvsDxlOMROfJUFbpQdyAtQPCCq
+ zpfkVKaCeCiTlifI6AZODngc9c8U+s7vLxjucz/Q4gNHwcgg/mASbjh8A2hYsbW6
+ qeg7lE9k5t6Lv820FudjfgFiq7k+zIbNsDNy3Y7CSsgBHQQSyNFngg25PPQOon9c
+ yd3PiFK36OzktnRkcTs98i3fwO2+3pp6qgOSk1Sdx877egszMjBPFzxrBX9CXIpM
+ yQIDAQABo4GKMIGHMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEG
+ MB0GA1UdDgQWBBThFsFtfMN62ev+YiMAAddufdjpRzBCBgNVHSMEOzA5gBTIcipC
+ fD/Hp73Y64n7L57QtIpKpKEWpBQwEjEQMA4GA1UEAxMHUm9vdCBDQYIJAN+SZB+x
+ c7usMA0GCSqGSIb3DQEBCwUAA4ICAQA6sBGs28FWhKgh6TxZ6U8Lc+iCdc4c9PeM
+ L5pQQosHekT0oBJK8WdvyXZS95Fz2ddJaKiiQyUKSP4XHpxE+6tBt8OOV0LJJxnx
+ yKTBZtcSiOFssu2j6aqx3oMotRZJrhuI/5ChExaPwFT1W7aQDIY6lN2KcQ/xndbX
+ Nts/2nwCvlplfiOGM7XrRMU8b4X+AVWXSksvLXiByrDh9W6WGDsBHyu+FKQVnwmW
+ QVnshKpwxIsW25JDOhFE8+VHn6yciUKUTqnCFt5HjZpZh00q8hhmlhrNBEdkxA8N
+ OF7S1uWWftJywqq23qG6pGIDQ1r1dwNzgaeNhmW6QKm2zBXUmuOW//Xt+1wtHrly
+ bDjXKKSa/zhR9plYPdvGe9PopXwTw/fQWRYcxML6aH+WbWY9AgCHFgY56YCJYZd9
+ eUIfrvPVJLn8fqwLmsWQtIY+XkAS/YQ4wTQs0zZS3+bdxeGQ6oHIMgxCDiBK8Qcc
+ RHf+RvYHiBllOJmaRaJHdsauMk9IlYYYpxPPwuWGti9B5HI4JO6bIqmR5Q8x3L/g
+ tFGMPzvWDTA2+dQcrh7WKULDH9Ngnnoodc6Hb9Iv1yCGYahcS6ARt9BzRyG1+6d9
+ bq/zCH8KQCjryiTn3ZEpsln/iXtp5nHiLegUc1OoXldrUKAz9V93l61GHUw1kdhD
+ V+KJceDj3Q==
+ -----END CERTIFICATE-----
+ """)
+ ]);
+
+ options.EnableSelfSignedClientCertificateAuthentication();
+
+ // Note: setting a static issuer is mandatory when using mTLS aliases
+ // to ensure it is not dynamically computed based on the request URI,
+ // as this would result in two different issuers being used (one
+ // pointing to the mTLS domain and one pointing to the regular one).
+ options.SetIssuer("https://localhost:44395/");
+
+ // Configure the mTLS endpoint aliases that will be used by client applications opting
+ // for TLS-based client authentication to communicate with the authorization server:
+ // the configured URIs MUST point to a domain for which the HTTPS server is configured
+ // to require the use of client certificates when receiving TLS handshakes from clients.
+ //
+ // Using mTLS endpoint aliases is not mandatory but is strongly recommended to avoid
+ // severely degrading the experience of users of browser-based clients, as TLS client
+ // authentication can only be enforced globally and not per-client, which would result
+ // in certificate selection prompts being systematically displayed by browsers.
+ options.SetMtlsDeviceAuthorizationEndpointAliasUri("https://mtls.dev.localhost:44395/connect/device")
+ .SetMtlsIntrospectionEndpointAliasUri("https://mtls.dev.localhost:44395/connect/introspect")
+ .SetMtlsPushedAuthorizationEndpointAliasUri("https://mtls.dev.localhost:44395/connect/par")
+ .SetMtlsRevocationEndpointAliasUri("https://mtls.dev.localhost:44395/connect/revoke")
+ .SetMtlsTokenEndpointAliasUri("https://mtls.dev.localhost:44395/connect/token");
+#endif
})
// Register the OpenIddict validation components.
@@ -178,7 +282,7 @@ public class Startup
// options.UseIntrospection()
// .SetIssuer("https://localhost:44395/")
// .SetClientId("resource_server")
- // .SetClientSecret("80B552BB-4CD8-48DA-946E-0815E0147DD2");
+ // .SetClientSecret("vVQ-yjr42sXP5VHj6AswkXuS7MU1i2gFjvJjY0TdGMk");
//
// When introspection is used, the System.Net.Http integration must be enabled.
//
@@ -202,6 +306,56 @@ public class Startup
// Register the worker responsible for seeding the database with the sample clients.
// Note: in a real world application, this step should be part of a setup script.
services.AddHostedService();
+
+#if SUPPORTS_KESTREL_TLS_HANDSHAKE_CALLBACK_OPTIONS
+ // Configure Kestrel to listen on the 44395 port and configure it to enforce mTLS.
+ //
+ // Note: depending on the operating system, the mtls.dev.localhost
+ // subdomain MAY have to be manually mapped to 127.0.0.1 or ::1.
+ services.Configure(options => options.ListenAnyIP(44395, options =>
+ {
+ options.UseHttps(new TlsHandshakeCallbackOptions
+ {
+ OnConnection = GetServerAuthenticationOptionsAsync
+ });
+ }));
+
+ static ValueTask GetServerAuthenticationOptionsAsync(TlsHandshakeCallbackContext context)
+ {
+ using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
+ store.Open(OpenFlags.ReadOnly);
+
+ return ValueTask.FromResult(new SslServerAuthenticationOptions
+ {
+ // Require a client certificate for all the requests pointing to the mTLS subdomain.
+ ClientCertificateRequired = string.Equals(context.ClientHelloInfo.ServerName,
+ "mtls.dev.localhost", StringComparison.OrdinalIgnoreCase),
+
+ // Ignore all the client certificate errors for requests pointing to
+ // the mTLS-specific domain, even if they indicate that the chain is
+ // invalid: this is necessary to allow OpenIddict to validate the PKI
+ // and self-signed certificates using its own per-client chain policies.
+ RemoteCertificateValidationCallback = (sender, certificate, chain, errors) =>
+ {
+ if (string.Equals(context.ClientHelloInfo.ServerName,
+ "mtls.dev.localhost", StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+
+ return errors is SslPolicyErrors.None or SslPolicyErrors.RemoteCertificateNotAvailable;
+ },
+
+ // Use the same TLS server certificate as the default server instance.
+ ServerCertificate = store.Certificates
+ .Find(X509FindType.FindByExtension, "1.3.6.1.4.1.311.84.1.1", validOnly: false)
+ .Cast()
+ .OrderByDescending(static certificate => certificate.NotAfter)
+ .FirstOrDefault() ??
+ throw new InvalidOperationException("The ASP.NET Core HTTPS development certificate was not found.")
+ });
+ }
+#endif
}
public void Configure(IApplicationBuilder app)
diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Worker.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Worker.cs
index ed53e2f6..8653b910 100644
--- a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Worker.cs
+++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Worker.cs
@@ -1,5 +1,6 @@
using System.Globalization;
using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.Abstractions;
using OpenIddict.Sandbox.AspNetCore.Server.Models;
@@ -157,8 +158,36 @@ public class Worker : IHostedService
{
Keys =
{
- // On supported platforms, this application authenticates by generating JWT client
- // assertions that are signed using a signing key instead of using a client secret.
+ // On supported platforms, this application can authenticate by using a
+ // self-signed client authentication certificate during the TLS handshake
+ // (a method known as "mutual TLS" or mTLS).
+ //
+ // Note: while the client needs access to the private key, the server only needs
+ // to know the public part to be able to validate the certificates it receives.
+ JsonWebKeyConverter.ConvertFromX509SecurityKey(new X509SecurityKey(
+ X509Certificate2.CreateFromPem($"""
+ -----BEGIN CERTIFICATE-----
+ MIIC8zCCAdugAwIBAgIJAIZ9BN3TUnZQMA0GCSqGSIb3DQEBCwUAMCIxIDAeBgNV
+ BAMTF1NlbGYtc2lnbmVkIGNlcnRpZmljYXRlMCAXDTI2MDIwMjE0MzM0OVoYDzIx
+ MjYwMjAyMTQzMzQ5WjAiMSAwHgYDVQQDExdTZWxmLXNpZ25lZCBjZXJ0aWZpY2F0
+ ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOtfKVPM7ghVFh4U/sz4
+ sTrpaNJGQ2NORqawYxAHwluhr101yIOW7rWvFlFncA64Lkq9SAbFFCVSAbo28c6B
+ 2Mi41jyC4LHQU11jhv08K/3FUuckCuzEpzTnXUhxJHWxrRDVEuvKINGPs1VgVtTT
+ ra8rjP8s1YRAzCYnByxSx+8GXNGHprylLh0agpWKb2+2FYwDqY5ME2g3xTL9FTUu
+ FYWTcyspsvN0U1Eo1vlCeOxSYGPRct0MK0AS6eXEGBv+3kCYI7a5+UhQok0WvErF
+ pjIVo7USISDgKhW9GhTsWN+WywwdG4Kx4V6SB8ZLAHFSBSR3gjWS3TGOyqAWoBXc
+ znkCAwEAAaMqMCgwDgYDVR0PAQH/BAQDAgeAMBYGA1UdJQEB/wQMMAoGCCsGAQUF
+ BwMCMA0GCSqGSIb3DQEBCwUAA4IBAQBf5i/S7shmNalVxMuP8/Mk8cOhRRZjnAXd
+ zz3eOuXu0CH8iY/DwCgss04O2NTxuz87rKiuNKOrtY0oN/G4aFjWPvbgoQ+N1XP1
+ zvbhqbyo3fQr07FyjWkrIUoHYFQ3JRfL+GPGjWizJsgdpdCRJSK6G9VX8eU3Akjv
+ YhMRLmbkrH5etOURqFtLpZlxNmLzCpqWIvzRiYyyj74iOipA2I0acgcvkakWn6rE
+ Wio7luBAZ3dXlukEfHTOg+ft4k0nOlRXPTtASOmyFQBOs6iYJeztHDz6MQnknAPe
+ +W53US8kLWktspcOQmxhVVH1g1/T4ynl9iX7tzqvUbdYwZNi92+x
+ -----END CERTIFICATE-----
+ """))),
+
+ // On supported platforms, this application can also authenticate by
+ // generating JWT client assertions that are signed using a signing key.
//
// Note: while the client needs access to the private key, the server only needs
// to know the public key to be able to validate the client assertions it receives.
@@ -171,7 +200,7 @@ public class Worker : IHostedService
}
},
#else
- ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654",
+ ClientSecret = "emCimpdc9SeOaZzN5jzm4_eek-STF6VenfVlKO1_qt0",
#endif
RedirectUris =
{
@@ -306,7 +335,7 @@ public class Worker : IHostedService
var descriptor = new OpenIddictApplicationDescriptor
{
ClientId = "resource_server",
- ClientSecret = "80B552BB-4CD8-48DA-946E-0815E0147DD2",
+ ClientSecret = "vVQ-yjr42sXP5VHj6AswkXuS7MU1i2gFjvJjY0TdGMk",
ClientType = ClientTypes.Confidential,
Permissions =
{
diff --git a/shared/OpenIddict.Extensions/OpenIddictHelpers.cs b/shared/OpenIddict.Extensions/OpenIddictHelpers.cs
index 3a7328c6..7ca3e7ab 100644
--- a/shared/OpenIddict.Extensions/OpenIddictHelpers.cs
+++ b/shared/OpenIddict.Extensions/OpenIddictHelpers.cs
@@ -11,6 +11,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
@@ -196,7 +197,7 @@ internal static class OpenIddictHelpers
ArgumentNullException.ThrowIfNull(uri);
var builder = new StringBuilder(uri.Query);
- if (builder.Length > 0)
+ if (builder.Length is > 0)
{
builder.Append('&');
}
@@ -238,7 +239,7 @@ internal static class OpenIddictHelpers
// only append the parameter key to the query string.
if (parameter.Value.Count is 0)
{
- if (builder.Length > 0)
+ if (builder.Length is > 0)
{
builder.Append('&');
}
@@ -252,7 +253,7 @@ internal static class OpenIddictHelpers
{
foreach (var value in parameter.Value)
{
- if (builder.Length > 0)
+ if (builder.Length is > 0)
{
builder.Append('&');
}
@@ -286,7 +287,7 @@ internal static class OpenIddictHelpers
.Select(static parameter => parameter.Split(Separators.EqualsSign, StringSplitOptions.RemoveEmptyEntries))
.Select(static parts => (
Key: parts[0] is string key ? Uri.UnescapeDataString(key) : null,
- Value: parts.Length > 1 && parts[1] is string value ? Uri.UnescapeDataString(value) : null))
+ Value: parts.Length is > 1 && parts[1] is string value ? Uri.UnescapeDataString(value) : null))
.Where(static pair => !string.IsNullOrEmpty(pair.Key))
.GroupBy(static pair => pair.Key)
.ToDictionary(static pair => pair.Key!, static pair => new StringValues([.. pair.Select(parts => parts.Value)]));
@@ -307,7 +308,7 @@ internal static class OpenIddictHelpers
.Select(static parameter => parameter.Split(Separators.EqualsSign, StringSplitOptions.RemoveEmptyEntries))
.Select(static parts => (
Key: parts[0] is string key ? Uri.UnescapeDataString(key) : null,
- Value: parts.Length > 1 && parts[1] is string value ? Uri.UnescapeDataString(value) : null))
+ Value: parts.Length is > 1 && parts[1] is string value ? Uri.UnescapeDataString(value) : null))
.Where(static pair => !string.IsNullOrEmpty(pair.Key))
.GroupBy(static pair => pair.Key)
.ToDictionary(static pair => pair.Key!, static pair => new StringValues([.. pair.Select(parts => parts.Value)]));
@@ -998,7 +999,7 @@ internal static class OpenIddictHelpers
///
/// The .
///
- /// if the JSON node is null or empty otherwise.
+ /// if the JSON node is null or empty, otherwise.
///
public static bool IsNullOrEmpty([NotNullWhen(false)] JsonNode? node) => node switch
{
@@ -1015,6 +1016,94 @@ internal static class OpenIddictHelpers
JsonNode value => IsNullOrEmpty(value.Deserialize(OpenIddictSerializer.Default.JsonElement))
};
+ ///
+ /// Determines whether the specified is a certificate authority.
+ ///
+ /// The .
+ ///
+ /// if the certificate is a certificate authority, otherwise.
+ ///
+ public static bool IsCertificateAuthority(X509Certificate2 certificate)
+ {
+ ArgumentNullException.ThrowIfNull(certificate);
+
+ return certificate.Extensions.OfType()
+ .Any(static extension => extension.CertificateAuthority);
+ }
+
+ ///
+ /// Determines whether the specified has the specified extended key usage.
+ ///
+ /// The .
+ /// The extended key usage.
+ ///
+ /// if the certificate has the specified extended key usage, otherwise.
+ ///
+ public static bool HasExtendedKeyUsage(X509Certificate2 certificate, string usage)
+ {
+ for (var index = 0; index < certificate.Extensions.Count; index++)
+ {
+ if (certificate.Extensions[index] is X509EnhancedKeyUsageExtension extension &&
+ HasOid(extension.EnhancedKeyUsages, usage))
+ {
+ return true;
+ }
+ }
+
+ return false;
+
+ static bool HasOid(OidCollection collection, string value)
+ {
+ for (var index = 0; index < collection.Count; index++)
+ {
+ if (collection[index] is Oid oid && string.Equals(oid.Value, value, StringComparison.Ordinal))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+
+ ///
+ /// Determines whether the specified has the specified key usage.
+ ///
+ /// The .
+ /// The .
+ ///
+ /// if the certificate has the specified key usage, otherwise.
+ ///
+ public static bool HasKeyUsage(X509Certificate2 certificate, X509KeyUsageFlags usage)
+ {
+ ArgumentNullException.ThrowIfNull(certificate);
+
+ for (var index = 0; index < certificate.Extensions.Count; index++)
+ {
+ if (certificate.Extensions[index] is X509KeyUsageExtension extension &&
+ extension.KeyUsages.HasFlag(usage))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ ///
+ /// Determines whether the specified is self-issued.
+ ///
+ /// The .
+ ///
+ /// if the certificate is self-issued, otherwise.
+ ///
+ public static bool IsSelfIssuedCertificate(X509Certificate2 certificate)
+ {
+ ArgumentNullException.ThrowIfNull(certificate);
+
+ return certificate.SubjectName.RawData.AsSpan().SequenceEqual(certificate.IssuerName.RawData);
+ }
+
///
/// Determines whether the items contained in
/// are of the specified .
diff --git a/shared/OpenIddict.Extensions/OpenIddictPolyfills.cs b/shared/OpenIddict.Extensions/OpenIddictPolyfills.cs
index 07f12b29..78d8e04d 100644
--- a/shared/OpenIddict.Extensions/OpenIddictPolyfills.cs
+++ b/shared/OpenIddict.Extensions/OpenIddictPolyfills.cs
@@ -8,6 +8,8 @@ using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
namespace OpenIddict.Extensions;
@@ -303,4 +305,54 @@ internal static class OpenIddictPolyfills
return currentRevision >= revision;
}
#endif
+
+ extension(X509ChainPolicy policy)
+ {
+#if !SUPPORTS_X509_CHAIN_POLICY_CLONING
+ public X509ChainPolicy Clone()
+ {
+ var clone = new X509ChainPolicy
+ {
+#if SUPPORTS_X509_CHAIN_POLICY_DOWNLOAD_MODE
+ DisableCertificateDownloads = policy.DisableCertificateDownloads,
+#endif
+ RevocationMode = policy.RevocationMode,
+ RevocationFlag = policy.RevocationFlag,
+#if SUPPORTS_X509_CHAIN_POLICY_TRUST_MODE
+ TrustMode = policy.TrustMode,
+#endif
+ UrlRetrievalTimeout = policy.UrlRetrievalTimeout,
+ VerificationFlags = policy.VerificationFlags,
+ VerificationTime = policy.VerificationTime,
+#if SUPPORTS_X509_CHAIN_POLICY_VERIFICATION_TIME_MODE
+ VerificationTimeIgnored = policy.VerificationTimeIgnored
+#endif
+ };
+
+ if (policy.ApplicationPolicy.Count is > 0)
+ {
+ for (var index = 0; index < policy.ApplicationPolicy.Count; index++)
+ {
+ clone.ApplicationPolicy.Add(policy.ApplicationPolicy[index]);
+ }
+ }
+
+ if (policy.CertificatePolicy.Count is > 0)
+ {
+ for (var index = 0; index < policy.CertificatePolicy.Count; index++)
+ {
+ clone.CertificatePolicy.Add(policy.CertificatePolicy[index]);
+ }
+ }
+
+#if SUPPORTS_X509_CHAIN_POLICY_CUSTOM_TRUST_STORE
+ clone.CustomTrustStore.AddRange(policy.CustomTrustStore);
+#endif
+
+ clone.ExtraStore.AddRange(policy.ExtraStore);
+
+ return clone;
+ }
+#endif
+ }
}
diff --git a/src/OpenIddict.Abstractions/Managers/IOpenIddictApplicationManager.cs b/src/OpenIddict.Abstractions/Managers/IOpenIddictApplicationManager.cs
index 55b47d9a..2accbe4d 100644
--- a/src/OpenIddict.Abstractions/Managers/IOpenIddictApplicationManager.cs
+++ b/src/OpenIddict.Abstractions/Managers/IOpenIddictApplicationManager.cs
@@ -8,6 +8,7 @@ using System.Collections.Immutable;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
+using System.Security.Cryptography.X509Certificates;
using System.Text.Json;
using Microsoft.IdentityModel.Tokens;
@@ -15,12 +16,14 @@ namespace OpenIddict.Abstractions;
///
/// Provides methods allowing to manage the applications stored in the store.
+///
+///
/// Note: this interface is not meant to be implemented by custom managers,
/// that should inherit from the generic OpenIddictApplicationManager class.
/// It is primarily intended to be used by services that cannot easily depend
/// on the generic application manager. The actual application entity type
/// is automatically determined at runtime based on the OpenIddict core options.
-///
+///
public interface IOpenIddictApplicationManager
{
///
@@ -171,6 +174,19 @@ public interface IOpenIddictApplicationManager
Func, TState, IQueryable> query,
TState state, CancellationToken cancellationToken = default);
+ ///
+ /// Retrieves the client certificate chain policy enforced for this application.
+ ///
+ /// The application.
+ /// The base policy from which the returned instance will be derived.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the client certificate chain policy enforced for this application.
+ ///
+ ValueTask GetClientCertificateChainPolicyAsync(
+ object application, X509ChainPolicy policy, CancellationToken cancellationToken = default);
+
///
/// Retrieves the client identifier associated with an application.
///
@@ -330,6 +346,19 @@ public interface IOpenIddictApplicationManager
///
ValueTask> GetRequirementsAsync(object application, CancellationToken cancellationToken = default);
+ ///
+ /// Retrieves the self-signed client certificate chain policy enforced for this application.
+ ///
+ /// The application.
+ /// The base policy from which the returned instance will be derived.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation, whose
+ /// result returns the self-signed client certificate chain policy enforced for this application.
+ ///
+ ValueTask GetSelfSignedClientCertificateChainPolicyAsync(
+ object application, X509ChainPolicy policy, CancellationToken cancellationToken = default);
+
///
/// Retrieves the settings associated with an application.
///
@@ -475,6 +504,21 @@ public interface IOpenIddictApplicationManager
///
ValueTask UpdateAsync(object application, string secret, CancellationToken cancellationToken = default);
+ ///
+ /// Validates the client certificate associated with an application.
+ ///
+ /// The application.
+ /// The certificate that should be compared to the certificates associated with the application.
+ /// The chain policy used to validate the certificate.
+ /// The that can be used to abort the operation.
+ /// A that can be used to monitor the asynchronous operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns a boolean indicating whether the client certificate was valid.
+ ///
+ ValueTask ValidateClientCertificateAsync(object application,
+ X509Certificate2 certificate, X509ChainPolicy policy, CancellationToken cancellationToken = default);
+
///
/// Validates the application to ensure it's in a consistent state.
///
@@ -522,4 +566,20 @@ public interface IOpenIddictApplicationManager
///
ValueTask ValidateRedirectUriAsync(object application,
[StringSyntax(StringSyntaxAttribute.Uri)] string uri, CancellationToken cancellationToken = default);
+
+ ///
+ /// Validates the self-signed client certificate associated with an application.
+ ///
+ /// The application.
+ /// The certificate that should be compared to the certificates associated with the application.
+ /// The chain policy used to validate the certificate.
+ /// The that can be used to abort the operation.
+ /// A that can be used to monitor the asynchronous operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation, whose
+ /// result returns a boolean indicating whether the self-signed client certificate was valid.
+ ///
+ ValueTask ValidateSelfSignedClientCertificateAsync(
+ object application, X509Certificate2 certificate,
+ X509ChainPolicy policy, CancellationToken cancellationToken = default);
}
diff --git a/src/OpenIddict.Abstractions/Managers/IOpenIddictAuthorizationManager.cs b/src/OpenIddict.Abstractions/Managers/IOpenIddictAuthorizationManager.cs
index a50cb1ba..a5ad9310 100644
--- a/src/OpenIddict.Abstractions/Managers/IOpenIddictAuthorizationManager.cs
+++ b/src/OpenIddict.Abstractions/Managers/IOpenIddictAuthorizationManager.cs
@@ -13,12 +13,14 @@ namespace OpenIddict.Abstractions;
///
/// Provides methods allowing to manage the authorizations stored in the store.
+///
+///
/// Note: this interface is not meant to be implemented by custom managers,
/// that should inherit from the generic OpenIddictAuthorizationManager class.
/// It is primarily intended to be used by services that cannot easily depend
/// on the generic authorization manager. The actual authorization entity type
/// is automatically determined at runtime based on the OpenIddict core options.
-///
+///
public interface IOpenIddictAuthorizationManager
{
///
diff --git a/src/OpenIddict.Abstractions/Managers/IOpenIddictScopeManager.cs b/src/OpenIddict.Abstractions/Managers/IOpenIddictScopeManager.cs
index 8dbb5459..65c4fc33 100644
--- a/src/OpenIddict.Abstractions/Managers/IOpenIddictScopeManager.cs
+++ b/src/OpenIddict.Abstractions/Managers/IOpenIddictScopeManager.cs
@@ -13,12 +13,14 @@ namespace OpenIddict.Abstractions;
///
/// Provides methods allowing to manage the scopes stored in the store.
+///
+///
/// Note: this interface is not meant to be implemented by custom managers,
/// that should inherit from the generic OpenIddictScopeManager class.
/// It is primarily intended to be used by services that cannot easily
/// depend on the generic scope manager. The actual scope entity type is
/// automatically determined at runtime based on the OpenIddict core options.
-///
+///
public interface IOpenIddictScopeManager
{
///
diff --git a/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs b/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs
index 3773e702..4b035d93 100644
--- a/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs
+++ b/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs
@@ -12,12 +12,14 @@ namespace OpenIddict.Abstractions;
///
/// Provides methods allowing to manage the tokens stored in the store.
+///
+///
/// Note: this interface is not meant to be implemented by custom managers,
/// that should inherit from the generic OpenIddictTokenManager class.
/// It is primarily intended to be used by services that cannot easily
/// depend on the generic token manager. The actual token entity type is
/// automatically determined at runtime based on the OpenIddict core options.
-///
+///
public interface IOpenIddictTokenManager
{
///
diff --git a/src/OpenIddict.Abstractions/OpenIddictConstants.cs b/src/OpenIddict.Abstractions/OpenIddictConstants.cs
index d1abe08d..db113429 100644
--- a/src/OpenIddict.Abstractions/OpenIddictConstants.cs
+++ b/src/OpenIddict.Abstractions/OpenIddictConstants.cs
@@ -336,6 +336,20 @@ public static class OpenIddictConstants
public const string UserInfoSigningAlgValuesSupported = "userinfo_signing_alg_values_supported";
}
+ public static class ObjectIdentifiers
+ {
+ public static class CertificateExtensions
+ {
+ public const string AuthorityInfoAccess = "1.3.6.1.5.5.7.1.1";
+ public const string CrlDistributionPoints = "2.5.29.31";
+ }
+
+ public static class ExtendedKeyUsages
+ {
+ public const string ClientAuthentication = "1.3.6.1.5.5.7.3.2";
+ }
+ }
+
public static class Parameters
{
public const string AccessToken = "access_token";
diff --git a/src/OpenIddict.Abstractions/OpenIddictResources.resx b/src/OpenIddict.Abstractions/OpenIddictResources.resx
index bdab1a15..7089451b 100644
--- a/src/OpenIddict.Abstractions/OpenIddictResources.resx
+++ b/src/OpenIddict.Abstractions/OpenIddictResources.resx
@@ -1802,6 +1802,55 @@ Alternatively, any value respecting the '[region]-[subregion]-[identifier]' patt
An unknown error occurred while trying to start a custom tabs intent.
+
+ The X.509 TLS client certificate provided by the OWIN host is not an instance of type 'X509Certificate2'.
+
+
+ mTLS endpoint aliases must be absolute HTTPS URLs.
+
+
+ A static issuer must be explicitly set when configuring a mTLS endpoint alias.
+To set a static issuer, use 'services.AddOpenIddict().AddServer().SetIssuer()'.
+
+
+ End certificates are not allowed in the client certificate chain base policies attached to the server options.
+To attach an end certificate to a specific client, override the 'OpenIddictApplicationManager.GetClientCertificateChainPolicyAsync()' method.
+
+
+ Certificates are not allowed in the self-signed client certificate chain base policies attached to the server options.
+To attach a self-signed certificate to a specific client, override the 'OpenIddictApplicationManager.GetSelfSignedClientCertificateChainPolicyAsync()' method.
+
+
+ Public Key Infrastructure-based client authentication cannot be used with self-signed certificates.
+
+
+ Self-signed client authentication can only be used with self-signed certificates.
+
+
+ A certificate chain policy must be configured when enabling the 'tls_client_auth' authentication method.
+To configure a policy, use 'services.AddOpenIddict().AddServer().EnablePublicKeyInfrastructureClientCertificateAuthentication()'.
+
+
+ A self-signed certificate chain policy must be configured when enabling the 'self_signed_tls_client_auth' authentication method.
+To configure a policy, use 'services.AddOpenIddict().AddServer().EnableSelfSignedClientCertificateAuthentication()'.
+
+
+ At least one certificate authority must be added to the certificate collection.
+
+
+ X.509 custom trust stores are not supported on this platform.
+While not recommended, certificate-based client authentication can be manually implemented on unsupported platforms by setting 'OpenIddictServerOptions.ClientCertificateChainPolicy'/'OpenIddictServerOptions.SelfSignedClientCertificateChainPolicy' and overriding the the 'OpenIddictApplicationManager.ValidateClientCertificateAsync()'/'OpenIddictApplicationManager.ValidateSelfSignedClientCertificateAsync()' methods.
+
+
+ Changing the trust mode of the X.509 chain policy used for client authentication is not allowed by default for security reasons.
+To use a custom policy relying on the system store, set 'OpenIddictServerOptions.ClientCertificateChainPolicy' or 'OpenIddictServerOptions.SelfSignedClientCertificateChainPolicy' manually.
+
+
+ mTLS endpoint aliases cannot be set when the corresponding endpoints have not been enabled.
+
+
+ Public Key Infrastructure certificates cannot contain private keys.
+
The security token is missing.
@@ -2387,6 +2436,15 @@ Alternatively, any value respecting the '[region]-[subregion]-[identifier]' patt
The '{0}' parameter is only allowed for OAuth 2.0 Token Exchange requests.
+
+ Certificate-based authentication is not valid for this client application.
+
+
+ The specified TLS client certificate is invalid, expired or has been revoked.
+
+
+ Client authentication is required for this application.
+
The '{0}' parameter shouldn't be null or empty at this point.
@@ -2447,6 +2505,9 @@ Alternatively, any value respecting the '[region]-[subregion]-[identifier]' patt
The nonce shouldn't be null or empty at this point.
+
+ The X.509 client certificate shouldn't be null at this point.
+
An error occurred while validating the token '{Token}'.
@@ -3194,6 +3255,27 @@ This may indicate that the hashed entry is corrupted or malformed.
The token request was rejected because the '{Parameter}' contained a URI fragment: {RedirectUri}.
+
+ The authentication demand was rejected because the public application '{ClientId}' was not allowed to send a client certificate.
+
+
+ The authentication demand was rejected because the confidential application '{ClientId}' didn't specify a valid client certificate.
+
+
+ The authentication demand was rejected because the confidential application '{ClientId}' didn't specify a valid self-signed client certificate.
+
+
+ Certificate-based client authentication failed for {ClientId} because no redirection URI was associated with the application.
+
+
+ Certificate-based client authentication failed for {ClientId} because the certificate was not valid: {Errors}.
+
+
+ Certificate-based client authentication failed for {ClientId} because the certificate didn't match any of the hostnames extracted from the redirection URIs associated with the application.
+
+
+ An error occurred while trying to validate a client certificate, which may indicate that the certificate is malformed or has an invalid chain.
+
https://documentation.openiddict.com/errors/{0}
diff --git a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreConfiguration.cs b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreConfiguration.cs
index cf699322..ad850f42 100644
--- a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreConfiguration.cs
+++ b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreConfiguration.cs
@@ -125,7 +125,7 @@ public sealed class OpenIddictClientAspNetCoreConfiguration : IConfigureOptions<
// on invalid endpoints. To opt out this undesirable behavior, a fake entry
// is dynamically added if one of the default schemes properties is not set
// and less than 2 handlers were registered in the authentication options.
- if (options.SchemeMap.Count < 2 && string.IsNullOrEmpty(options.DefaultScheme) &&
+ if (options.SchemeMap.Count is < 2 && string.IsNullOrEmpty(options.DefaultScheme) &&
(string.IsNullOrEmpty(options.DefaultAuthenticateScheme) ||
string.IsNullOrEmpty(options.DefaultSignInScheme) ||
string.IsNullOrEmpty(options.DefaultSignOutScheme)))
diff --git a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpConfiguration.cs b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpConfiguration.cs
index fb84d7da..c017dba1 100644
--- a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpConfiguration.cs
+++ b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpConfiguration.cs
@@ -7,7 +7,6 @@
using System.ComponentModel;
using System.Net;
using System.Net.Http;
-using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http;
@@ -271,10 +270,12 @@ public sealed class OpenIddictClientSystemNetHttpConfiguration : IConfigureOptio
{
foreach (var credentials in registration.SigningCredentials)
{
+ // Note: to avoid building and introspecting a X.509 certificate chain and reduce the cost
+ // of this check, a certificate is always assumed to be self-signed when it is self-issued.
if (credentials.Key is X509SecurityKey { Certificate: X509Certificate2 certificate } &&
- certificate.Version is >= 3 && IsSelfIssuedCertificate(certificate) &&
- HasDigitalSignatureKeyUsage(certificate) &&
- HasClientAuthenticationExtendedKeyUsage(certificate))
+ certificate.Version is >= 3 && OpenIddictHelpers.IsSelfIssuedCertificate(certificate) &&
+ OpenIddictHelpers.HasKeyUsage(certificate, X509KeyUsageFlags.DigitalSignature) &&
+ OpenIddictHelpers.HasExtendedKeyUsage(certificate, ObjectIdentifiers.ExtendedKeyUsages.ClientAuthentication))
{
return certificate;
}
@@ -287,10 +288,12 @@ public sealed class OpenIddictClientSystemNetHttpConfiguration : IConfigureOptio
{
foreach (var credentials in registration.SigningCredentials)
{
+ // Note: to avoid building and introspecting a X.509 certificate chain and reduce the cost
+ // of this check, a certificate is always assumed to be self-signed when it is self-issued.
if (credentials.Key is X509SecurityKey { Certificate: X509Certificate2 certificate } &&
- certificate.Version is >= 3 && !IsSelfIssuedCertificate(certificate) &&
- HasDigitalSignatureKeyUsage(certificate) &&
- HasClientAuthenticationExtendedKeyUsage(certificate))
+ certificate.Version is >= 3 && !OpenIddictHelpers.IsSelfIssuedCertificate(certificate) &&
+ OpenIddictHelpers.HasKeyUsage(certificate, X509KeyUsageFlags.DigitalSignature) &&
+ OpenIddictHelpers.HasExtendedKeyUsage(certificate, ObjectIdentifiers.ExtendedKeyUsages.ClientAuthentication))
{
return certificate;
}
@@ -298,51 +301,5 @@ public sealed class OpenIddictClientSystemNetHttpConfiguration : IConfigureOptio
return null;
};
-
- static bool HasClientAuthenticationExtendedKeyUsage(X509Certificate2 certificate)
- {
- for (var index = 0; index < certificate.Extensions.Count; index++)
- {
- if (certificate.Extensions[index] is X509EnhancedKeyUsageExtension extension &&
- HasOid(extension.EnhancedKeyUsages, "1.3.6.1.5.5.7.3.2"))
- {
- return true;
- }
- }
-
- return false;
-
- static bool HasOid(OidCollection collection, string value)
- {
- for (var index = 0; index < collection.Count; index++)
- {
- if (collection[index] is Oid oid && string.Equals(oid.Value, value, StringComparison.Ordinal))
- {
- return true;
- }
- }
-
- return false;
- }
- }
-
- static bool HasDigitalSignatureKeyUsage(X509Certificate2 certificate)
- {
- for (var index = 0; index < certificate.Extensions.Count; index++)
- {
- if (certificate.Extensions[index] is X509KeyUsageExtension extension &&
- extension.KeyUsages.HasFlag(X509KeyUsageFlags.DigitalSignature))
- {
- return true;
- }
- }
-
- return false;
- }
-
- // Note: to avoid building and introspecting a X.509 certificate chain and reduce the cost
- // of this check, a certificate is always assumed to be self-signed when it is self-issued.
- static bool IsSelfIssuedCertificate(X509Certificate2 certificate)
- => certificate.SubjectName.RawData.AsSpan().SequenceEqual(certificate.IssuerName.RawData);
}
}
diff --git a/src/OpenIddict.Client/OpenIddictClientBuilder.cs b/src/OpenIddict.Client/OpenIddictClientBuilder.cs
index 8f27ebbc..0eb9d157 100644
--- a/src/OpenIddict.Client/OpenIddictClientBuilder.cs
+++ b/src/OpenIddict.Client/OpenIddictClientBuilder.cs
@@ -135,8 +135,7 @@ public sealed class OpenIddictClientBuilder
ArgumentNullException.ThrowIfNull(key);
// If the encryption key is an asymmetric security key, ensure it has a private key.
- if (key is AsymmetricSecurityKey asymmetricSecurityKey &&
- asymmetricSecurityKey.PrivateKeyStatus is PrivateKeyStatus.DoesNotExist)
+ if (key is AsymmetricSecurityKey { PrivateKeyStatus: PrivateKeyStatus.DoesNotExist })
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0055));
}
@@ -202,7 +201,7 @@ public sealed class OpenIddictClientBuilder
// If no valid existing certificate was found, create a new encryption certificate.
var certificates = store.Certificates
.Find(X509FindType.FindBySubjectDistinguishedName, subject.Name, validOnly: false)
- .OfType()
+ .Cast()
.ToList();
if (!certificates.Exists(certificate => certificate.NotBefore < now.LocalDateTime && certificate.NotAfter > now.LocalDateTime))
@@ -318,7 +317,7 @@ public sealed class OpenIddictClientBuilder
// 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)
+ if (certificate.Version is >= 3)
{
var extensions = certificate.Extensions.OfType().ToList();
if (extensions.Count is not 0 && !extensions.Exists(static extension =>
@@ -437,7 +436,7 @@ public sealed class OpenIddictClientBuilder
store.Open(OpenFlags.ReadOnly);
return store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false)
- .OfType()
+ .Cast()
.SingleOrDefault();
}
}
@@ -458,7 +457,7 @@ public sealed class OpenIddictClientBuilder
return AddEncryptionCertificate(
store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false)
- .OfType()
+ .Cast()
.SingleOrDefault() ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0066)));
}
@@ -496,8 +495,7 @@ public sealed class OpenIddictClientBuilder
ArgumentNullException.ThrowIfNull(key);
// If the signing key is an asymmetric security key, ensure it has a private key.
- if (key is AsymmetricSecurityKey asymmetricSecurityKey &&
- asymmetricSecurityKey.PrivateKeyStatus is PrivateKeyStatus.DoesNotExist)
+ if (key is AsymmetricSecurityKey { PrivateKeyStatus: PrivateKeyStatus.DoesNotExist })
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0067));
}
@@ -581,7 +579,7 @@ public sealed class OpenIddictClientBuilder
// If no valid existing certificate was found, create a new signing certificate.
var certificates = store.Certificates
.Find(X509FindType.FindBySubjectDistinguishedName, subject.Name, validOnly: false)
- .OfType()
+ .Cast()
.ToList();
if (!certificates.Exists(certificate => certificate.NotBefore < now.LocalDateTime && certificate.NotAfter > now.LocalDateTime))
@@ -725,7 +723,7 @@ public sealed class OpenIddictClientBuilder
// 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)
+ if (certificate.Version is >= 3)
{
var extensions = certificate.Extensions.OfType().ToList();
if (extensions.Count is not 0 && !extensions.Exists(static extension =>
@@ -844,7 +842,7 @@ public sealed class OpenIddictClientBuilder
store.Open(OpenFlags.ReadOnly);
return store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false)
- .OfType()
+ .Cast()
.SingleOrDefault();
}
}
@@ -865,7 +863,7 @@ public sealed class OpenIddictClientBuilder
return AddSigningCertificate(
store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false)
- .OfType()
+ .Cast()
.SingleOrDefault() ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0066)));
}
diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.Discovery.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.Discovery.cs
index c9687c7d..96de6d31 100644
--- a/src/OpenIddict.Client/OpenIddictClientHandlers.Discovery.cs
+++ b/src/OpenIddict.Client/OpenIddictClientHandlers.Discovery.cs
@@ -568,7 +568,8 @@ public static partial class OpenIddictClientHandlers
}
///
- /// Contains the logic responsible for extracting the mTLS-enabled revocation endpoint URI from the discovery document.
+ /// Contains the logic responsible for extracting the mTLS-enabled
+ /// revocation endpoint URI from the discovery document.
///
public sealed class ExtractMtlsRevocationEndpoint : IOpenIddictClientHandler
{
@@ -600,7 +601,8 @@ public static partial class OpenIddictClientHandlers
}
///
- /// Contains the logic responsible for extracting the mTLS-enabled token endpoint URI from the discovery document.
+ /// Contains the logic responsible for extracting the mTLS-enabled
+ /// token endpoint URI from the discovery document.
///
public sealed class ExtractMtlsTokenEndpoint : IOpenIddictClientHandler
{
@@ -632,7 +634,8 @@ public static partial class OpenIddictClientHandlers
}
///
- /// Contains the logic responsible for extracting the mTLS-enabled userinfo endpoint URI from the discovery document.
+ /// Contains the logic responsible for extracting the mTLS-enabled
+ /// userinfo endpoint URI from the discovery document.
///
public sealed class ExtractMtlsUserInfoEndpoint : IOpenIddictClientHandler
{
diff --git a/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs b/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs
index bb529182..70641f43 100644
--- a/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs
+++ b/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs
@@ -11,6 +11,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Logging;
@@ -18,7 +19,6 @@ using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using ValidationException = OpenIddict.Abstractions.OpenIddictExceptions.ValidationException;
-
#if !SUPPORTS_KEY_DERIVATION_WITH_SPECIFIED_HASH_ALGORITHM
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Digests;
@@ -471,6 +471,25 @@ public class OpenIddictApplicationManager : IOpenIddictApplication
return Store.GetAsync(query, state, cancellationToken);
}
+ ///
+ /// Retrieves the client certificate chain policy enforced for this application.
+ ///
+ /// The application.
+ /// The base policy from which the returned instance will be derived.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the client certificate chain policy enforced for this application.
+ ///
+ public virtual ValueTask GetClientCertificateChainPolicyAsync(
+ TApplication application, X509ChainPolicy policy, CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(application);
+
+ // Always clone the X.509 chain policy to ensure the original instance is never mutated.
+ return new(policy.Clone());
+ }
+
///
/// Retrieves the client identifier associated with an application.
///
@@ -739,6 +758,48 @@ public class OpenIddictApplicationManager : IOpenIddictApplication
return Store.GetRequirementsAsync(application, cancellationToken);
}
+ ///
+ /// Retrieves the self-signed client certificate chain policy enforced for this application.
+ ///
+ /// The application.
+ /// The base policy from which the returned instance will be derived.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation, whose
+ /// result returns the self-signed client certificate chain policy enforced for this application.
+ ///
+ public virtual async ValueTask GetSelfSignedClientCertificateChainPolicyAsync(
+ TApplication application, X509ChainPolicy policy, CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(application);
+
+ // Always clone the X.509 chain policy to ensure the original instance is never mutated.
+ policy = policy.Clone();
+
+ // If a JSON Web Key Set was associated to the client application, extract the signing keys containing
+ // a X.509 certificate suitable for signing and client authentication and attach them to the chain policy.
+ if (await GetJsonWebKeySetAsync(application, cancellationToken) is { Keys: [_, ..] keys })
+ {
+ for (var index = 0; index < keys.Count; index++)
+ {
+ if (JsonWebKeyConverter.TryConvertToSecurityKey(keys[index], out SecurityKey key) &&
+ key is X509SecurityKey { Certificate: X509Certificate2 certificate } &&
+ certificate.Version is >= 3 &&
+ OpenIddictHelpers.HasKeyUsage(certificate, X509KeyUsageFlags.DigitalSignature) &&
+ OpenIddictHelpers.HasExtendedKeyUsage(certificate, ObjectIdentifiers.ExtendedKeyUsages.ClientAuthentication))
+ {
+#if SUPPORTS_X509_CHAIN_POLICY_CUSTOM_TRUST_STORE
+ policy.CustomTrustStore.Add(certificate);
+#else
+ policy.ExtraStore.Add(certificate);
+#endif
+ }
+ }
+ }
+
+ return policy;
+ }
+
///
/// Retrieves the settings associated with an application.
///
@@ -1226,6 +1287,114 @@ public class OpenIddictApplicationManager : IOpenIddictApplication
}
}
+ ///
+ /// Validates the client certificate associated with an application.
+ ///
+ /// The application.
+ /// The certificate that should be compared to the certificates associated with the application.
+ /// The chain policy used to validate the certificate.
+ /// The that can be used to abort the operation.
+ /// A that can be used to monitor the asynchronous operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns a boolean indicating whether the client certificate was valid.
+ ///
+ public virtual async ValueTask ValidateClientCertificateAsync(
+ TApplication application, X509Certificate2 certificate,
+ X509ChainPolicy policy, CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(application);
+ ArgumentNullException.ThrowIfNull(certificate);
+ ArgumentNullException.ThrowIfNull(policy);
+
+ // Important: the certificate and policy instances MUST NOT be mutated in this method.
+
+ if (OpenIddictHelpers.IsSelfIssuedCertificate(certificate))
+ {
+ throw new ArgumentException(SR.GetResourceString(SR.ID0503), nameof(certificate));
+ }
+
+ // Note: using a policy relying on the default system trust store
+ // is strongly discouraged but deliberately not prevented here.
+
+ if (await HasClientTypeAsync(application, ClientTypes.Public, cancellationToken))
+ {
+ Logger.LogWarning(6159, SR.GetResourceString(SR.ID6159));
+
+ return false;
+ }
+
+#if SUPPORTS_X509_CHAIN_POLICY_CUSTOM_TRUST_STORE
+ var uris = await GetRedirectUrisAsync(application, cancellationToken);
+ if (uris.IsDefaultOrEmpty)
+ {
+ Logger.LogInformation(6285, SR.GetResourceString(SR.ID6285), await GetClientIdAsync(application, cancellationToken));
+
+ return false;
+ }
+
+ using var chain = new X509Chain()
+ {
+ ChainPolicy = policy
+ };
+
+ try
+ {
+ // Ensure the specified certificate is valid based on the chain policy.
+ if (!chain.Build(certificate))
+ {
+ Logger.LogInformation(6286, SR.GetResourceString(SR.ID6286),
+ await GetClientIdAsync(application, cancellationToken),
+ chain.ChainStatus.Select(static status => status.Status).ToArray());
+
+ return false;
+ }
+
+ // Note: this method MUST NOT be used with self-signed certificates. While self-issued
+ // certificates are immediately rejected by this method, determining whether a certificate
+ // is actually self-signed can only be done after building and validating the chain.
+ if (chain.ChainElements.Count is not > 1)
+ {
+ throw new ArgumentException(SR.GetResourceString(SR.ID0503), nameof(certificate));
+ }
+
+ // By default, OpenIddict requires that certificates issued by PKIs be valid for one of the domains
+ // used in redirect URIs. Implementations that need a different logic can override this method.
+ for (var index = 0; index < uris.Length; index++)
+ {
+ if (Uri.TryCreate(uris[index], UriKind.Absolute, out Uri? uri) && !OpenIddictHelpers.IsImplicitFileUri(uri) &&
+ uri.HostNameType is UriHostNameType.Dns or UriHostNameType.IPv4 or UriHostNameType.IPv6 &&
+ certificate.MatchesHostname(hostname: uri.IdnHost, allowWildcards: true, allowCommonName: true))
+ {
+ return true;
+ }
+ }
+
+ Logger.LogInformation(6287, SR.GetResourceString(SR.ID6287), await GetClientIdAsync(application, cancellationToken));
+
+ return false;
+ }
+
+ catch (CryptographicException exception) when (!OpenIddictHelpers.IsFatal(exception))
+ {
+ Logger.LogWarning(6288, exception, SR.GetResourceString(SR.ID6288));
+
+ return false;
+ }
+
+ finally
+ {
+ // Dispose the certificates instantiated internally while building the chain.
+ for (var index = 0; index < chain.ChainElements.Count; index++)
+ {
+ chain.ChainElements[index].Certificate.Dispose();
+ }
+ }
+#else
+ throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0508));
+#endif
+ }
+
///
/// Validates the client_secret associated with an application.
///
@@ -1399,6 +1568,92 @@ public class OpenIddictApplicationManager : IOpenIddictApplication
return false;
}
+ ///
+ /// Validates the self-signed client certificate associated with an application.
+ ///
+ /// The application.
+ /// The certificate that should be compared to the certificates associated with the application.
+ /// The chain policy used to validate the certificate.
+ /// The that can be used to abort the operation.
+ /// A that can be used to monitor the asynchronous operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation, whose
+ /// result returns a boolean indicating whether the self-signed client certificate was valid.
+ ///
+ public virtual async ValueTask ValidateSelfSignedClientCertificateAsync(
+ TApplication application, X509Certificate2 certificate,
+ X509ChainPolicy policy, CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(application);
+ ArgumentNullException.ThrowIfNull(certificate);
+ ArgumentNullException.ThrowIfNull(policy);
+
+ // Important: the certificate and policy instances MUST NOT be mutated in this method.
+
+ if (!OpenIddictHelpers.IsSelfIssuedCertificate(certificate))
+ {
+ throw new ArgumentException(SR.GetResourceString(SR.ID0504), nameof(certificate));
+ }
+
+ // Note: using a policy relying on the default system trust store
+ // is strongly discouraged but deliberately not prevented here.
+
+ if (await HasClientTypeAsync(application, ClientTypes.Public, cancellationToken))
+ {
+ Logger.LogWarning(6159, SR.GetResourceString(SR.ID6159));
+
+ return false;
+ }
+
+#if SUPPORTS_X509_CHAIN_POLICY_CUSTOM_TRUST_STORE
+ using var chain = new X509Chain()
+ {
+ ChainPolicy = policy
+ };
+
+ try
+ {
+ // Ensure the specified certificate is valid based on the chain policy.
+ if (!chain.Build(certificate))
+ {
+ Logger.LogInformation(6286, SR.GetResourceString(SR.ID6286),
+ await GetClientIdAsync(application, cancellationToken),
+ chain.ChainStatus.Select(static status => status.Status).ToArray());
+
+ return false;
+ }
+
+ // Note: this method MUST be used with self-signed certificates. While self-issued
+ // certificates are immediately rejected by this method, determining whether a certificate
+ // is actually self-signed can only be done after building and validating the chain.
+ if (chain.ChainElements is not [X509ChainElement])
+ {
+ throw new ArgumentException(SR.GetResourceString(SR.ID0504), nameof(certificate));
+ }
+
+ return true;
+ }
+
+ catch (CryptographicException exception) when (!OpenIddictHelpers.IsFatal(exception))
+ {
+ Logger.LogWarning(6288, exception, SR.GetResourceString(SR.ID6288));
+
+ return false;
+ }
+
+ finally
+ {
+ // Dispose the certificates instantiated internally while building the chain.
+ for (var index = 0; index < chain.ChainElements.Count; index++)
+ {
+ chain.ChainElements[index].Certificate.Dispose();
+ }
+ }
+#else
+ throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0508));
+#endif
+ }
+
///
/// Obfuscates the specified client secret so it can be safely stored in a database.
/// By default, this method returns a complex hashed representation computed using PBKDF2.
@@ -1520,7 +1775,7 @@ public class OpenIddictApplicationManager : IOpenIddictApplication
// Read the size of the salt and ensure it's more than 128 bits.
var saltLength = (int) BinaryPrimitives.ReadUInt32BigEndian(payload.Slice(9, sizeof(uint)));
- if (saltLength < 128 / 8)
+ if (saltLength is < 128 / 8)
{
return false;
}
@@ -1530,7 +1785,7 @@ public class OpenIddictApplicationManager : IOpenIddictApplication
// Ensure the derived key length is more than 128 bits.
var keyLength = payload.Length - 13 - salt.Length;
- if (keyLength < 128 / 8)
+ if (keyLength is < 128 / 8)
{
return false;
}
@@ -1614,6 +1869,10 @@ public class OpenIddictApplicationManager : IOpenIddictApplication
ValueTask IOpenIddictApplicationManager.GetAsync(Func, TState, IQueryable> query, TState state, CancellationToken cancellationToken) where TResult : default
=> GetAsync(query, state, cancellationToken);
+ ///
+ ValueTask IOpenIddictApplicationManager.GetClientCertificateChainPolicyAsync(object application, X509ChainPolicy policy, CancellationToken cancellationToken)
+ => GetClientCertificateChainPolicyAsync((TApplication) application, policy, cancellationToken);
+
///
ValueTask IOpenIddictApplicationManager.GetClientIdAsync(object application, CancellationToken cancellationToken)
=> GetClientIdAsync((TApplication) application, cancellationToken);
@@ -1670,6 +1929,10 @@ public class OpenIddictApplicationManager : IOpenIddictApplication
ValueTask> IOpenIddictApplicationManager.GetRequirementsAsync(object application, CancellationToken cancellationToken)
=> GetRequirementsAsync((TApplication) application, cancellationToken);
+ ///
+ ValueTask IOpenIddictApplicationManager.GetSelfSignedClientCertificateChainPolicyAsync(object application, X509ChainPolicy policy, CancellationToken cancellationToken)
+ => GetSelfSignedClientCertificateChainPolicyAsync((TApplication) application, policy, cancellationToken);
+
///
ValueTask> IOpenIddictApplicationManager.GetSettingsAsync(object application, CancellationToken cancellationToken)
=> GetSettingsAsync((TApplication) application, cancellationToken);
@@ -1730,6 +1993,10 @@ public class OpenIddictApplicationManager : IOpenIddictApplication
IAsyncEnumerable IOpenIddictApplicationManager.ValidateAsync(object application, CancellationToken cancellationToken)
=> ValidateAsync((TApplication) application, cancellationToken);
+ ///
+ ValueTask IOpenIddictApplicationManager.ValidateClientCertificateAsync(object application, X509Certificate2 certificate, X509ChainPolicy policy, CancellationToken cancellationToken)
+ => ValidateClientCertificateAsync((TApplication) application, certificate, policy, cancellationToken);
+
///
ValueTask IOpenIddictApplicationManager.ValidateClientSecretAsync(object application, string secret, CancellationToken cancellationToken)
=> ValidateClientSecretAsync((TApplication) application, secret, cancellationToken);
@@ -1741,4 +2008,8 @@ public class OpenIddictApplicationManager : IOpenIddictApplication
///
ValueTask IOpenIddictApplicationManager.ValidateRedirectUriAsync(object application, [StringSyntax(StringSyntaxAttribute.Uri)] string uri, CancellationToken cancellationToken)
=> ValidateRedirectUriAsync((TApplication) application, uri, cancellationToken);
+
+ ///
+ ValueTask IOpenIddictApplicationManager.ValidateSelfSignedClientCertificateAsync(object application, X509Certificate2 certificate, X509ChainPolicy policy, CancellationToken cancellationToken)
+ => ValidateSelfSignedClientCertificateAsync((TApplication) application, certificate, policy, cancellationToken);
}
diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConfiguration.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConfiguration.cs
index 58052a70..309d1749 100644
--- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConfiguration.cs
+++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConfiguration.cs
@@ -16,7 +16,8 @@ namespace OpenIddict.Server.AspNetCore;
public sealed class OpenIddictServerAspNetCoreConfiguration : IConfigureOptions,
IConfigureOptions,
IPostConfigureOptions,
- IPostConfigureOptions
+ IPostConfigureOptions,
+ IPostConfigureOptions
{
///
public void Configure(AuthenticationOptions options)
@@ -71,7 +72,7 @@ public sealed class OpenIddictServerAspNetCoreConfiguration : IConfigureOptions<
// on invalid endpoints. To opt out this undesirable behavior, a fake entry
// is dynamically added if one of the default schemes properties is not set
// and less than 2 handlers were registered in the authentication options.
- if (options.SchemeMap.Count < 2 && string.IsNullOrEmpty(options.DefaultScheme) &&
+ if (options.SchemeMap.Count is < 2 && string.IsNullOrEmpty(options.DefaultScheme) &&
(string.IsNullOrEmpty(options.DefaultAuthenticateScheme) ||
string.IsNullOrEmpty(options.DefaultChallengeScheme) ||
string.IsNullOrEmpty(options.DefaultForbidScheme) ||
@@ -104,4 +105,22 @@ public sealed class OpenIddictServerAspNetCoreConfiguration : IConfigureOptions<
throw new InvalidOperationException(SR.GetResourceString(SR.ID0110));
}
}
+
+ ///
+ public void PostConfigure(string? name, OpenIddictServerOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(options);
+
+ // Enable tls_client_auth and self_signed_tls_client_auth support if the
+ // corresponding chain policies have been configured in the server options.
+ if (options.ClientCertificateChainPolicy is not null)
+ {
+ options.ClientAuthenticationMethods.Add(ClientAuthenticationMethods.TlsClientAuth);
+ }
+
+ if (options.SelfSignedClientCertificateChainPolicy is not null)
+ {
+ options.ClientAuthenticationMethods.Add(ClientAuthenticationMethods.SelfSignedTlsClientAuth);
+ }
+ }
}
diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreExtensions.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreExtensions.cs
index ea6cebb6..3caea366 100644
--- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreExtensions.cs
+++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreExtensions.cs
@@ -45,7 +45,7 @@ public static class OpenIddictServerAspNetCoreExtensions
builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
- // Register the option initializer used by the OpenIddict ASP.NET Core server integration services.
+ // Register the option initializers used by the OpenIddict ASP.NET Core server integration services.
// Note: TryAddEnumerable() is used here to ensure the initializers are only registered once.
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<
IConfigureOptions, OpenIddictServerAspNetCoreConfiguration>());
@@ -59,6 +59,9 @@ public static class OpenIddictServerAspNetCoreExtensions
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<
IPostConfigureOptions, OpenIddictServerAspNetCoreConfiguration>());
+ builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<
+ IPostConfigureOptions, OpenIddictServerAspNetCoreConfiguration>());
+
return new OpenIddictServerAspNetCoreBuilder(builder.Services);
}
diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Authentication.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Authentication.cs
index f7d75027..11376e4b 100644
--- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Authentication.cs
+++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Authentication.cs
@@ -48,6 +48,7 @@ public static partial class OpenIddictServerAspNetCoreHandlers
*/
ExtractPostRequest.Descriptor,
ValidateClientAuthenticationMethod.Descriptor,
+ ExtractClientAuthenticationCertificate.Descriptor,
ExtractBasicAuthenticationCredentials.Descriptor,
/*
diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Device.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Device.cs
index 2f27242f..f1187caf 100644
--- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Device.cs
+++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Device.cs
@@ -21,6 +21,7 @@ public static partial class OpenIddictServerAspNetCoreHandlers
*/
ExtractPostRequest.Descriptor,
ValidateClientAuthenticationMethod.Descriptor,
+ ExtractClientAuthenticationCertificate.Descriptor,
ExtractBasicAuthenticationCredentials.Descriptor,
/*
diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Exchange.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Exchange.cs
index e76c2972..5b1c5d83 100644
--- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Exchange.cs
+++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Exchange.cs
@@ -19,6 +19,7 @@ public static partial class OpenIddictServerAspNetCoreHandlers
*/
ExtractPostRequest.Descriptor,
ValidateClientAuthenticationMethod.Descriptor,
+ ExtractClientAuthenticationCertificate.Descriptor,
ExtractBasicAuthenticationCredentials.Descriptor,
/*
diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Introspection.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Introspection.cs
index 5ac7e188..6f7331c4 100644
--- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Introspection.cs
+++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Introspection.cs
@@ -19,6 +19,7 @@ public static partial class OpenIddictServerAspNetCoreHandlers
*/
ExtractGetOrPostRequest.Descriptor,
ValidateClientAuthenticationMethod.Descriptor,
+ ExtractClientAuthenticationCertificate.Descriptor,
ExtractBasicAuthenticationCredentials.Descriptor,
/*
diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Revocation.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Revocation.cs
index 77368682..11dec150 100644
--- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Revocation.cs
+++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Revocation.cs
@@ -19,6 +19,7 @@ public static partial class OpenIddictServerAspNetCoreHandlers
*/
ExtractPostRequest.Descriptor,
ValidateClientAuthenticationMethod.Descriptor,
+ ExtractClientAuthenticationCertificate.Descriptor,
ExtractBasicAuthenticationCredentials.Descriptor,
/*
diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs
index ece0221a..0a354278 100644
--- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs
+++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs
@@ -8,6 +8,7 @@ using System.Collections.Immutable;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.InteropServices;
+using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
@@ -626,7 +627,7 @@ public static partial class OpenIddictServerAspNetCoreHandlers
.Build();
///
- public ValueTask HandleAsync(TContext context)
+ public async ValueTask HandleAsync(TContext context)
{
ArgumentNullException.ThrowIfNull(context);
@@ -648,13 +649,10 @@ public static partial class OpenIddictServerAspNetCoreHandlers
description: SR.FormatID2174(ClientAuthenticationMethods.ClientSecretPost),
uri: SR.FormatID8000(SR.ID2174));
- return ValueTask.CompletedTask;
+ return;
}
// Reject requests that use client_secret_basic if support was explicitly disabled in the options.
- //
- // Note: the client_secret_jwt authentication method is not supported by OpenIddict out-of-the-box but
- // is specified here to account for custom implementations that explicitly add client_secret_jwt support.
string? header = request.Headers[HeaderNames.Authorization];
if (!string.IsNullOrEmpty(header) && header.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase) &&
!context.Options.ClientAuthenticationMethods.Contains(ClientAuthenticationMethods.ClientSecretBasic))
@@ -666,10 +664,82 @@ public static partial class OpenIddictServerAspNetCoreHandlers
description: SR.FormatID2174(ClientAuthenticationMethods.ClientSecretBasic),
uri: SR.FormatID8000(SR.ID2174));
- return ValueTask.CompletedTask;
+ return;
}
- return ValueTask.CompletedTask;
+ // If the request was sent using HTTPS, reject requests that use mTLS-based client authentication
+ // (self_signed_tls_client_auth or tls_client_auth) if support was not enabled in the server options.
+ if (request.IsHttps && await request.HttpContext.Connection.GetClientCertificateAsync(
+ request.HttpContext.RequestAborted) is X509Certificate2 certificate)
+ {
+ // Note: to avoid building and introspecting a X.509 certificate chain and reduce the cost
+ // of this check, a certificate is always assumed to be self-signed when it is self-issued.
+ if (OpenIddictHelpers.IsSelfIssuedCertificate(certificate))
+ {
+ if (!context.Options.ClientAuthenticationMethods.Contains(ClientAuthenticationMethods.SelfSignedTlsClientAuth))
+ {
+ context.Logger.LogInformation(6227, SR.GetResourceString(SR.ID6227), ClientAuthenticationMethods.SelfSignedTlsClientAuth);
+
+ context.Reject(
+ error: Errors.InvalidClient,
+ description: SR.FormatID2174(ClientAuthenticationMethods.SelfSignedTlsClientAuth),
+ uri: SR.FormatID8000(SR.ID2174));
+
+ return;
+ }
+ }
+
+ else if (!context.Options.ClientAuthenticationMethods.Contains(ClientAuthenticationMethods.TlsClientAuth))
+ {
+ context.Logger.LogInformation(6227, SR.GetResourceString(SR.ID6227), ClientAuthenticationMethods.TlsClientAuth);
+
+ context.Reject(
+ error: Errors.InvalidClient,
+ description: SR.FormatID2174(ClientAuthenticationMethods.TlsClientAuth),
+ uri: SR.FormatID8000(SR.ID2174));
+
+ return;
+ }
+ }
+ }
+ }
+
+ ///
+ /// Contains the logic responsible for extracting a client authentication certificate from the request context.
+ /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core.
+ ///
+ public sealed class ExtractClientAuthenticationCertificate : IOpenIddictServerHandler
+ where TContext : BaseValidatingContext
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .UseSingletonHandler>()
+ .SetOrder(ValidateClientAuthenticationMethod.Descriptor.Order + 1_000)
+ .SetType(OpenIddictServerHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public async ValueTask HandleAsync(TContext context)
+ {
+ ArgumentNullException.ThrowIfNull(context);
+
+ Debug.Assert(context.Transaction.Request is not null, SR.GetResourceString(SR.ID4008));
+
+ // This handler only applies to ASP.NET Core requests. If the HTTP context cannot be resolved,
+ // this may indicate that the request was incorrectly processed by another server stack.
+ var request = context.Transaction.GetHttpRequest() ??
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0114));
+
+ // If a client certificate was used during the TLS handshake, attach it to the context.
+ if (request.IsHttps && await request.HttpContext.Connection.GetClientCertificateAsync(
+ request.HttpContext.RequestAborted) is X509Certificate2 certificate)
+ {
+ context.Transaction.ClientCertificate = certificate;
+ }
}
}
@@ -687,7 +757,7 @@ public static partial class OpenIddictServerAspNetCoreHandlers
= OpenIddictServerHandlerDescriptor.CreateBuilder()
.AddFilter()
.UseSingletonHandler>()
- .SetOrder(ValidateClientAuthenticationMethod.Descriptor.Order + 1_000)
+ .SetOrder(ExtractClientAuthenticationCertificate.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
@@ -730,7 +800,7 @@ public static partial class OpenIddictServerAspNetCoreHandlers
var data = Encoding.ASCII.GetString(Convert.FromBase64String(value));
var index = data.IndexOf(':');
- if (index < 0)
+ if (index is < 0)
{
context.Reject(
error: Errors.InvalidRequest,
diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinConfiguration.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinConfiguration.cs
index ae00aa30..cd2ddd65 100644
--- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinConfiguration.cs
+++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinConfiguration.cs
@@ -14,6 +14,7 @@ namespace OpenIddict.Server.Owin;
///
[EditorBrowsable(EditorBrowsableState.Advanced)]
public sealed class OpenIddictServerOwinConfiguration : IConfigureOptions,
+ IPostConfigureOptions,
IPostConfigureOptions
{
///
@@ -28,6 +29,24 @@ public sealed class OpenIddictServerOwinConfiguration : IConfigureOptions
+ public void PostConfigure(string? name, OpenIddictServerOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(options);
+
+ // Enable tls_client_auth and self_signed_tls_client_auth support if the
+ // corresponding chain policies have been configured in the server options.
+ if (options.ClientCertificateChainPolicy is not null)
+ {
+ options.ClientAuthenticationMethods.Add(ClientAuthenticationMethods.TlsClientAuth);
+ }
+
+ if (options.SelfSignedClientCertificateChainPolicy is not null)
+ {
+ options.ClientAuthenticationMethods.Add(ClientAuthenticationMethods.SelfSignedTlsClientAuth);
+ }
+ }
+
///
public void PostConfigure(string? name, OpenIddictServerOwinOptions options)
{
diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinExtensions.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinExtensions.cs
index 72fa200b..9088b27b 100644
--- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinExtensions.cs
+++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinExtensions.cs
@@ -55,6 +55,9 @@ public static class OpenIddictServerOwinExtensions
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<
IPostConfigureOptions, OpenIddictServerOwinConfiguration>());
+ builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<
+ IPostConfigureOptions, OpenIddictServerOwinConfiguration>());
+
return new OpenIddictServerOwinBuilder(builder.Services);
}
diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Authentication.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Authentication.cs
index 0ce2d6ac..7f34049d 100644
--- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Authentication.cs
+++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Authentication.cs
@@ -48,6 +48,7 @@ public static partial class OpenIddictServerOwinHandlers
*/
ExtractPostRequest.Descriptor,
ValidateClientAuthenticationMethod.Descriptor,
+ ExtractClientAuthenticationCertificate.Descriptor,
ExtractBasicAuthenticationCredentials.Descriptor,
/*
diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Device.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Device.cs
index a4b99066..08f434ab 100644
--- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Device.cs
+++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Device.cs
@@ -21,6 +21,7 @@ public static partial class OpenIddictServerOwinHandlers
*/
ExtractPostRequest.Descriptor,
ValidateClientAuthenticationMethod.Descriptor,
+ ExtractClientAuthenticationCertificate.Descriptor,
ExtractBasicAuthenticationCredentials.Descriptor,
/*
diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Exchange.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Exchange.cs
index 86968326..6e388389 100644
--- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Exchange.cs
+++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Exchange.cs
@@ -19,6 +19,7 @@ public static partial class OpenIddictServerOwinHandlers
*/
ExtractPostRequest.Descriptor,
ValidateClientAuthenticationMethod.Descriptor,
+ ExtractClientAuthenticationCertificate.Descriptor,
ExtractBasicAuthenticationCredentials.Descriptor,
/*
diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Introspection.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Introspection.cs
index 58c449be..af3064e4 100644
--- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Introspection.cs
+++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Introspection.cs
@@ -19,6 +19,7 @@ public static partial class OpenIddictServerOwinHandlers
*/
ExtractGetOrPostRequest.Descriptor,
ValidateClientAuthenticationMethod.Descriptor,
+ ExtractClientAuthenticationCertificate.Descriptor,
ExtractBasicAuthenticationCredentials.Descriptor,
/*
diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Revocation.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Revocation.cs
index fcb34cc4..f0ef99f5 100644
--- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Revocation.cs
+++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Revocation.cs
@@ -19,6 +19,7 @@ public static partial class OpenIddictServerOwinHandlers
*/
ExtractPostRequest.Descriptor,
ValidateClientAuthenticationMethod.Descriptor,
+ ExtractClientAuthenticationCertificate.Descriptor,
ExtractBasicAuthenticationCredentials.Descriptor,
/*
diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs
index ea560cc8..04092b9b 100644
--- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs
+++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs
@@ -8,6 +8,7 @@ using System.Collections.Immutable;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
+using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
@@ -688,7 +689,7 @@ public static partial class OpenIddictServerOwinHandlers
.Build();
///
- public ValueTask HandleAsync(TContext context)
+ public async ValueTask HandleAsync(TContext context)
{
ArgumentNullException.ThrowIfNull(context);
@@ -710,7 +711,7 @@ public static partial class OpenIddictServerOwinHandlers
description: SR.FormatID2174(ClientAuthenticationMethods.ClientSecretPost),
uri: SR.FormatID8000(SR.ID2174));
- return ValueTask.CompletedTask;
+ return;
}
// Reject requests that use client_secret_basic if support was explicitly disabled in the options.
@@ -725,10 +726,118 @@ public static partial class OpenIddictServerOwinHandlers
description: SR.FormatID2174(ClientAuthenticationMethods.ClientSecretBasic),
uri: SR.FormatID8000(SR.ID2174));
- return ValueTask.CompletedTask;
+ return;
}
- return ValueTask.CompletedTask;
+ // If the request was sent using HTTPS, reject requests that use mTLS-based client authentication
+ // (self_signed_tls_client_auth or tls_client_auth) if support was not enabled in the server options.
+ if (request.IsSecure && await GetClientCertificateAsync(request.Context) is X509Certificate2 certificate)
+ {
+ // Note: to avoid building and introspecting a X.509 certificate chain and reduce the cost
+ // of this check, a certificate is always assumed to be self-signed when it is self-issued.
+ if (OpenIddictHelpers.IsSelfIssuedCertificate(certificate))
+ {
+ if (!context.Options.ClientAuthenticationMethods.Contains(ClientAuthenticationMethods.SelfSignedTlsClientAuth))
+ {
+ context.Logger.LogInformation(6227, SR.GetResourceString(SR.ID6227), ClientAuthenticationMethods.SelfSignedTlsClientAuth);
+
+ context.Reject(
+ error: Errors.InvalidClient,
+ description: SR.FormatID2174(ClientAuthenticationMethods.SelfSignedTlsClientAuth),
+ uri: SR.FormatID8000(SR.ID2174));
+
+ return;
+ }
+ }
+
+ else if (!context.Options.ClientAuthenticationMethods.Contains(ClientAuthenticationMethods.TlsClientAuth))
+ {
+ context.Logger.LogInformation(6227, SR.GetResourceString(SR.ID6227), ClientAuthenticationMethods.TlsClientAuth);
+
+ context.Reject(
+ error: Errors.InvalidClient,
+ description: SR.FormatID2174(ClientAuthenticationMethods.TlsClientAuth),
+ uri: SR.FormatID8000(SR.ID2174));
+
+ return;
+ }
+ }
+
+ static async ValueTask GetClientCertificateAsync(IOwinContext context)
+ {
+ // If a loading function was provided by the OWIN host, always invoke it before trying
+ // to resolve the certificate to ensure it is present in the environment dictionary.
+ if (context.Get>("ssl.LoadClientCertAsync") is Func loader)
+ {
+ await loader();
+ }
+
+ if (context.Get("ssl.ClientCertificateErrors") is not null)
+ {
+ return null;
+ }
+
+ return context.Get("ssl.ClientCertificate") is X509Certificate certificate
+ ? certificate as X509Certificate2 ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0498))
+ : null;
+ }
+ }
+ }
+
+ ///
+ /// Contains the logic responsible for extracting a client authentication certificate from the request context.
+ /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN.
+ ///
+ public sealed class ExtractClientAuthenticationCertificate : IOpenIddictServerHandler
+ where TContext : BaseValidatingContext
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .UseSingletonHandler>()
+ .SetOrder(ValidateClientAuthenticationMethod.Descriptor.Order + 1_000)
+ .SetType(OpenIddictServerHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public async ValueTask HandleAsync(TContext context)
+ {
+ ArgumentNullException.ThrowIfNull(context);
+
+ Debug.Assert(context.Transaction.Request is not null, SR.GetResourceString(SR.ID4008));
+
+ // This handler only applies to OWIN requests. If The OWIN request cannot be resolved,
+ // this may indicate that the request was incorrectly processed by another server stack.
+ var request = context.Transaction.GetOwinRequest() ??
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0120));
+
+ // If a client certificate was used during the TLS handshake, attach it to the context.
+ if (request.IsSecure && await GetClientCertificateAsync(request.Context) is X509Certificate2 certificate)
+ {
+ context.Transaction.ClientCertificate = certificate;
+ }
+
+ static async ValueTask GetClientCertificateAsync(IOwinContext context)
+ {
+ // If a loading function was provided by the OWIN host, always invoke it before trying
+ // to resolve the certificate to ensure it is present in the environment dictionary.
+ if (context.Get>("ssl.LoadClientCertAsync") is Func loader)
+ {
+ await loader();
+ }
+
+ if (context.Get("ssl.ClientCertificateErrors") is not null)
+ {
+ return null;
+ }
+
+ return context.Get("ssl.ClientCertificate") is X509Certificate certificate
+ ? certificate as X509Certificate2 ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0498))
+ : null;
+ }
}
}
@@ -746,7 +855,7 @@ public static partial class OpenIddictServerOwinHandlers
= OpenIddictServerHandlerDescriptor.CreateBuilder()
.AddFilter()
.UseSingletonHandler>()
- .SetOrder(ValidateClientAuthenticationMethod.Descriptor.Order + 1_000)
+ .SetOrder(ExtractClientAuthenticationCertificate.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
@@ -789,7 +898,7 @@ public static partial class OpenIddictServerOwinHandlers
var data = Encoding.ASCII.GetString(Convert.FromBase64String(value));
var index = data.IndexOf(':');
- if (index < 0)
+ if (index is < 0)
{
context.Reject(
error: Errors.InvalidRequest,
diff --git a/src/OpenIddict.Server/OpenIddictServerBuilder.cs b/src/OpenIddict.Server/OpenIddictServerBuilder.cs
index f0ec51fd..3a6e19fb 100644
--- a/src/OpenIddict.Server/OpenIddictServerBuilder.cs
+++ b/src/OpenIddict.Server/OpenIddictServerBuilder.cs
@@ -4,6 +4,7 @@
* the license and the contributors participating to this project.
*/
+using System;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
@@ -11,6 +12,7 @@ using System.Reflection;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.Server;
@@ -144,8 +146,7 @@ public sealed class OpenIddictServerBuilder
ArgumentNullException.ThrowIfNull(key);
// If the encryption key is an asymmetric security key, ensure it has a private key.
- if (key is AsymmetricSecurityKey asymmetricSecurityKey &&
- asymmetricSecurityKey.PrivateKeyStatus is PrivateKeyStatus.DoesNotExist)
+ if (key is AsymmetricSecurityKey { PrivateKeyStatus: PrivateKeyStatus.DoesNotExist })
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0055));
}
@@ -211,7 +212,7 @@ public sealed class OpenIddictServerBuilder
// If no valid existing certificate was found, create a new encryption certificate.
var certificates = store.Certificates
.Find(X509FindType.FindBySubjectDistinguishedName, subject.Name, validOnly: false)
- .OfType()
+ .Cast()
.ToList();
if (!certificates.Exists(certificate => certificate.NotBefore < now.LocalDateTime && certificate.NotAfter > now.LocalDateTime))
@@ -327,7 +328,7 @@ public sealed class OpenIddictServerBuilder
// 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)
+ if (certificate.Version is >= 3)
{
var extensions = certificate.Extensions.OfType().ToList();
if (extensions.Count is not 0 && !extensions.Exists(static extension =>
@@ -446,7 +447,7 @@ public sealed class OpenIddictServerBuilder
store.Open(OpenFlags.ReadOnly);
return store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false)
- .OfType()
+ .Cast()
.SingleOrDefault();
}
}
@@ -467,7 +468,7 @@ public sealed class OpenIddictServerBuilder
return AddEncryptionCertificate(
store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false)
- .OfType()
+ .Cast()
.SingleOrDefault() ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0066)));
}
@@ -505,8 +506,7 @@ public sealed class OpenIddictServerBuilder
ArgumentNullException.ThrowIfNull(key);
// If the signing key is an asymmetric security key, ensure it has a private key.
- if (key is AsymmetricSecurityKey asymmetricSecurityKey &&
- asymmetricSecurityKey.PrivateKeyStatus is PrivateKeyStatus.DoesNotExist)
+ if (key is AsymmetricSecurityKey { PrivateKeyStatus: PrivateKeyStatus.DoesNotExist })
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0067));
}
@@ -590,7 +590,7 @@ public sealed class OpenIddictServerBuilder
// If no valid existing certificate was found, create a new signing certificate.
var certificates = store.Certificates
.Find(X509FindType.FindBySubjectDistinguishedName, subject.Name, validOnly: false)
- .OfType()
+ .Cast()
.ToList();
if (!certificates.Exists(certificate =>
@@ -735,7 +735,7 @@ public sealed class OpenIddictServerBuilder
// 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)
+ if (certificate.Version is >= 3)
{
var extensions = certificate.Extensions.OfType().ToList();
if (extensions.Count is not 0 && !extensions.Exists(static extension =>
@@ -854,7 +854,7 @@ public sealed class OpenIddictServerBuilder
store.Open(OpenFlags.ReadOnly);
return store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false)
- .OfType()
+ .Cast()
.SingleOrDefault();
}
}
@@ -875,7 +875,7 @@ public sealed class OpenIddictServerBuilder
return AddSigningCertificate(
store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false)
- .OfType()
+ .Cast()
.SingleOrDefault() ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0066)));
}
@@ -1893,7 +1893,7 @@ public sealed class OpenIddictServerBuilder
/// Sets the issuer URI, which is used as the value of the "issuer" claim and
/// is returned from the discovery endpoint to identify the authorization server.
///
- /// The issuer uri.
+ /// The issuer URI.
/// The instance.
public OpenIddictServerBuilder SetIssuer(Uri uri)
{
@@ -1906,7 +1906,7 @@ public sealed class OpenIddictServerBuilder
/// Sets the issuer URI, which is used as the value of the "issuer" claim and
/// is returned from the discovery endpoint to identify the authorization server.
///
- /// The issuer uri.
+ /// The issuer URI.
/// The instance.
public OpenIddictServerBuilder SetIssuer(
[StringSyntax(StringSyntaxAttribute.Uri, UriKind.Absolute)] string uri)
@@ -1921,6 +1921,256 @@ public sealed class OpenIddictServerBuilder
return SetIssuer(value);
}
+ ///
+ /// Sets the URI listed as the mTLS device authorization
+ /// endpoint alias in the server configuration metadata.
+ ///
+ ///
+ /// Note: this URI MUST be absolute and MUST point to a domain for
+ /// which TLS client authentication is enforced by the web server.
+ ///
+ /// The endpoint URI.
+ /// The instance.
+ public OpenIddictServerBuilder SetMtlsDeviceAuthorizationEndpointAliasUri(Uri uri)
+ {
+ ArgumentNullException.ThrowIfNull(uri);
+
+ if (OpenIddictHelpers.IsImplicitFileUri(uri))
+ {
+ throw new ArgumentException(SR.GetResourceString(SR.ID0072), nameof(uri));
+ }
+
+ if (uri.OriginalString.StartsWith("~", StringComparison.OrdinalIgnoreCase))
+ {
+ throw new ArgumentException(SR.FormatID0081("~"), nameof(uri));
+ }
+
+ return Configure(options => options.MtlsDeviceAuthorizationEndpointAliasUri = uri);
+ }
+
+ ///
+ /// Sets the URI listed as the mTLS device authorization
+ /// endpoint alias in the server configuration metadata.
+ ///
+ ///
+ /// Note: this URI MUST be absolute and MUST point to a domain for
+ /// which TLS client authentication is enforced by the web server.
+ ///
+ /// The endpoint URI.
+ /// The instance.
+ public OpenIddictServerBuilder SetMtlsDeviceAuthorizationEndpointAliasUri(
+ [StringSyntax(StringSyntaxAttribute.Uri, UriKind.Absolute)] string uri)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(uri);
+
+ if (!Uri.TryCreate(uri, UriKind.Absolute, out Uri? value) || OpenIddictHelpers.IsImplicitFileUri(value))
+ {
+ throw new ArgumentException(SR.GetResourceString(SR.ID0072), nameof(uri));
+ }
+
+ return SetMtlsDeviceAuthorizationEndpointAliasUri(value);
+ }
+
+ ///
+ /// Sets the URI listed as the mTLS introspection
+ /// endpoint alias in the server configuration metadata.
+ ///
+ ///
+ /// Note: this URI MUST be absolute and MUST point to a domain for
+ /// which TLS client authentication is enforced by the web server.
+ ///
+ /// The endpoint URI.
+ /// The instance.
+ public OpenIddictServerBuilder SetMtlsIntrospectionEndpointAliasUri(Uri uri)
+ {
+ ArgumentNullException.ThrowIfNull(uri);
+
+ if (OpenIddictHelpers.IsImplicitFileUri(uri))
+ {
+ throw new ArgumentException(SR.GetResourceString(SR.ID0072), nameof(uri));
+ }
+
+ if (uri.OriginalString.StartsWith("~", StringComparison.OrdinalIgnoreCase))
+ {
+ throw new ArgumentException(SR.FormatID0081("~"), nameof(uri));
+ }
+
+ return Configure(options => options.MtlsIntrospectionEndpointAliasUri = uri);
+ }
+
+ ///
+ /// Sets the URI listed as the mTLS introspection endpoint
+ /// alias in the server configuration metadata.
+ ///
+ ///
+ /// Note: this URI MUST be absolute and MUST point to a domain for
+ /// which TLS client authentication is enforced by the web server.
+ ///
+ /// The endpoint URI.
+ /// The instance.
+ public OpenIddictServerBuilder SetMtlsIntrospectionEndpointAliasUri(
+ [StringSyntax(StringSyntaxAttribute.Uri, UriKind.Absolute)] string uri)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(uri);
+
+ if (!Uri.TryCreate(uri, UriKind.Absolute, out Uri? value) || OpenIddictHelpers.IsImplicitFileUri(value))
+ {
+ throw new ArgumentException(SR.GetResourceString(SR.ID0072), nameof(uri));
+ }
+
+ return SetMtlsIntrospectionEndpointAliasUri(value);
+ }
+
+ ///
+ /// Sets the URI listed as the mTLS pushed authorization
+ /// endpoint alias in the server configuration metadata.
+ ///
+ ///
+ /// Note: this URI MUST be absolute and MUST point to a domain for
+ /// which TLS client authentication is enforced by the web server.
+ ///
+ /// The endpoint URI.
+ /// The instance.
+ public OpenIddictServerBuilder SetMtlsPushedAuthorizationEndpointAliasUri(Uri uri)
+ {
+ ArgumentNullException.ThrowIfNull(uri);
+
+ if (OpenIddictHelpers.IsImplicitFileUri(uri))
+ {
+ throw new ArgumentException(SR.GetResourceString(SR.ID0072), nameof(uri));
+ }
+
+ if (uri.OriginalString.StartsWith("~", StringComparison.OrdinalIgnoreCase))
+ {
+ throw new ArgumentException(SR.FormatID0081("~"), nameof(uri));
+ }
+
+ return Configure(options => options.MtlsPushedAuthorizationEndpointAliasUri = uri);
+ }
+
+ ///
+ /// Sets the URI listed as the mTLS pushed authorization
+ /// endpoint alias in the server configuration metadata.
+ ///
+ ///
+ /// Note: this URI MUST be absolute and MUST point to a domain for
+ /// which TLS client authentication is enforced by the web server.
+ ///
+ /// The endpoint URI.
+ /// The instance.
+ public OpenIddictServerBuilder SetMtlsPushedAuthorizationEndpointAliasUri(
+ [StringSyntax(StringSyntaxAttribute.Uri, UriKind.Absolute)] string uri)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(uri);
+
+ if (!Uri.TryCreate(uri, UriKind.Absolute, out Uri? value) || OpenIddictHelpers.IsImplicitFileUri(value))
+ {
+ throw new ArgumentException(SR.GetResourceString(SR.ID0072), nameof(uri));
+ }
+
+ return SetMtlsPushedAuthorizationEndpointAliasUri(value);
+ }
+
+ ///
+ /// Sets the URI listed as the mTLS revocation endpoint
+ /// alias in the server configuration metadata.
+ ///
+ ///
+ /// Note: this URI MUST be absolute and MUST point to a domain for
+ /// which TLS client authentication is enforced by the web server.
+ ///
+ /// The endpoint URI.
+ /// The instance.
+ public OpenIddictServerBuilder SetMtlsRevocationEndpointAliasUri(Uri uri)
+ {
+ ArgumentNullException.ThrowIfNull(uri);
+
+ if (OpenIddictHelpers.IsImplicitFileUri(uri))
+ {
+ throw new ArgumentException(SR.GetResourceString(SR.ID0072), nameof(uri));
+ }
+
+ if (uri.OriginalString.StartsWith("~", StringComparison.OrdinalIgnoreCase))
+ {
+ throw new ArgumentException(SR.FormatID0081("~"), nameof(uri));
+ }
+
+ return Configure(options => options.MtlsRevocationEndpointAliasUri = uri);
+ }
+
+ ///
+ /// Sets the URI listed as the mTLS revocation endpoint
+ /// alias in the server configuration metadata.
+ ///
+ ///
+ /// Note: this URI MUST be absolute and MUST point to a domain for
+ /// which TLS client authentication is enforced by the web server.
+ ///
+ /// The endpoint URI.
+ /// The instance.
+ public OpenIddictServerBuilder SetMtlsRevocationEndpointAliasUri(
+ [StringSyntax(StringSyntaxAttribute.Uri, UriKind.Absolute)] string uri)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(uri);
+
+ if (!Uri.TryCreate(uri, UriKind.Absolute, out Uri? value) || OpenIddictHelpers.IsImplicitFileUri(value))
+ {
+ throw new ArgumentException(SR.GetResourceString(SR.ID0072), nameof(uri));
+ }
+
+ return SetMtlsRevocationEndpointAliasUri(value);
+ }
+
+ ///
+ /// Sets the URI listed as the mTLS token endpoint
+ /// alias in the server configuration metadata.
+ ///
+ ///
+ /// Note: this URI MUST be absolute and MUST point to a domain for
+ /// which TLS client authentication is enforced by the web server.
+ ///
+ /// The endpoint URI.
+ /// The instance.
+ public OpenIddictServerBuilder SetMtlsTokenEndpointAliasUri(Uri uri)
+ {
+ ArgumentNullException.ThrowIfNull(uri);
+
+ if (OpenIddictHelpers.IsImplicitFileUri(uri))
+ {
+ throw new ArgumentException(SR.GetResourceString(SR.ID0072), nameof(uri));
+ }
+
+ if (uri.OriginalString.StartsWith("~", StringComparison.OrdinalIgnoreCase))
+ {
+ throw new ArgumentException(SR.FormatID0081("~"), nameof(uri));
+ }
+
+ return Configure(options => options.MtlsTokenEndpointAliasUri = uri);
+ }
+
+ ///
+ /// Sets the URI listed as the mTLS token endpoint
+ /// alias in the server configuration metadata.
+ ///
+ ///
+ /// Note: this URI MUST be absolute and MUST point to a domain for
+ /// which TLS client authentication is enforced by the web server.
+ ///
+ /// The endpoint URI.
+ /// The instance.
+ public OpenIddictServerBuilder SetMtlsTokenEndpointAliasUri(
+ [StringSyntax(StringSyntaxAttribute.Uri, UriKind.Absolute)] string uri)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(uri);
+
+ if (!Uri.TryCreate(uri, UriKind.Absolute, out Uri? value) || OpenIddictHelpers.IsImplicitFileUri(value))
+ {
+ throw new ArgumentException(SR.GetResourceString(SR.ID0072), nameof(uri));
+ }
+
+ return SetMtlsTokenEndpointAliasUri(value);
+ }
+
///
/// Configures OpenIddict to use reference tokens, so that the access token payloads
/// are stored in the database (only an identifier is returned to the client application).
@@ -1962,6 +2212,171 @@ public sealed class OpenIddictServerBuilder
public OpenIddictServerBuilder EnableEndSessionRequestCaching()
=> Configure(options => options.EnableEndSessionRequestCaching = true);
+ ///
+ /// Configures OpenIddict to enable PKI client certificate authentication (mTLS) and trust
+ /// the specified root and intermediate certificates when validating client certificates.
+ ///
+ /// The store containing the root and intermediate certificates to trust.
+ /// The instance.
+ [EditorBrowsable(EditorBrowsableState.Advanced)]
+ public OpenIddictServerBuilder EnablePublicKeyInfrastructureClientCertificateAuthentication(X509Certificate2Collection certificates)
+ => EnablePublicKeyInfrastructureClientCertificateAuthentication(certificates, static policy => { });
+
+ ///
+ /// Configures OpenIddict to enable PKI client certificate authentication (mTLS) and trust
+ /// the specified root and intermediate certificates when validating client certificates.
+ ///
+ /// The store containing the root and intermediate certificates to trust.
+ /// The delegate used to amend the created X.509 chain policy.
+ /// The instance.
+ [EditorBrowsable(EditorBrowsableState.Advanced)]
+ public OpenIddictServerBuilder EnablePublicKeyInfrastructureClientCertificateAuthentication(
+ X509Certificate2Collection certificates, Action configuration)
+ {
+ ArgumentNullException.ThrowIfNull(certificates);
+ ArgumentNullException.ThrowIfNull(configuration);
+
+#if SUPPORTS_X509_CHAIN_POLICY_CUSTOM_TRUST_STORE
+ // Ensure at least one root certificate authority was included in the certificate collection.
+ if (!certificates.Cast().Any(static certificate =>
+ OpenIddictHelpers.IsCertificateAuthority(certificate) &&
+ OpenIddictHelpers.HasKeyUsage(certificate, X509KeyUsageFlags.KeyCertSign) &&
+ OpenIddictHelpers.IsSelfIssuedCertificate(certificate)))
+ {
+ throw new ArgumentException(SR.GetResourceString(SR.ID0507), nameof(certificates));
+ }
+
+ // Ensure no end certificate was included in the certificate collection.
+ if (certificates.Cast().Any(static certificate =>
+ !OpenIddictHelpers.IsCertificateAuthority(certificate) ||
+ !OpenIddictHelpers.HasKeyUsage(certificate, X509KeyUsageFlags.KeyCertSign)))
+ {
+ throw new ArgumentException(SR.GetResourceString(SR.ID0501), nameof(certificates));
+ }
+
+ // Ensure none of the certificates contains a private key.
+ if (certificates.Cast().Any(static certificate => certificate.HasPrivateKey))
+ {
+ throw new ArgumentException(SR.GetResourceString(SR.ID0511), nameof(certificates));
+ }
+
+ var policy = new X509ChainPolicy
+ {
+ // Note: by default, OpenIddict requires that end certificates used for authentication
+ // explicitly list client authentication as an allowed extended key usage.
+ ApplicationPolicy = { new Oid(ObjectIdentifiers.ExtendedKeyUsages.ClientAuthentication) },
+ TrustMode = X509ChainTrustMode.CustomRootTrust
+ };
+
+ policy.CustomTrustStore.AddRange(certificates);
+
+ // If one of the root certificates doesn't include a CRL or AIA
+ // extension, ignore root revocation unknown status errors by default.
+ if (certificates.Cast()
+ .Where(static certificate =>
+ OpenIddictHelpers.IsCertificateAuthority(certificate) &&
+ OpenIddictHelpers.HasKeyUsage(certificate, X509KeyUsageFlags.KeyCertSign) &&
+ OpenIddictHelpers.IsSelfIssuedCertificate(certificate))
+ .Any(static certificate =>
+ certificate.Extensions[ObjectIdentifiers.CertificateExtensions.CrlDistributionPoints] is null &&
+ certificate.Extensions[ObjectIdentifiers.CertificateExtensions.AuthorityInfoAccess] is null))
+ {
+ policy.VerificationFlags |= X509VerificationFlags.IgnoreRootRevocationUnknown;
+ }
+
+ // If one of the intermediate certificates doesn't include a CRL or AIA
+ // extension, ignore root revocation unknown status errors by default.
+ if (certificates.Cast()
+ .Where(static certificate =>
+ OpenIddictHelpers.IsCertificateAuthority(certificate) &&
+ OpenIddictHelpers.HasKeyUsage(certificate, X509KeyUsageFlags.KeyCertSign) &&
+ !OpenIddictHelpers.IsSelfIssuedCertificate(certificate))
+ .Any(static certificate =>
+ certificate.Extensions[ObjectIdentifiers.CertificateExtensions.CrlDistributionPoints] is null &&
+ certificate.Extensions[ObjectIdentifiers.CertificateExtensions.AuthorityInfoAccess] is null))
+ {
+ policy.VerificationFlags |= X509VerificationFlags.IgnoreCertificateAuthorityRevocationUnknown;
+ }
+
+ // If the root or intermediate certificates doesn't include a CRL or AIA, assume
+ // by default that the end certificates won't have a CRL or AIA extension either.
+ if (certificates.Cast()
+ .Where(static certificate =>
+ OpenIddictHelpers.IsCertificateAuthority(certificate) &&
+ OpenIddictHelpers.HasKeyUsage(certificate, X509KeyUsageFlags.KeyCertSign))
+ .Any(static certificate =>
+ certificate.Extensions[ObjectIdentifiers.CertificateExtensions.CrlDistributionPoints] is null &&
+ certificate.Extensions[ObjectIdentifiers.CertificateExtensions.AuthorityInfoAccess] is null))
+ {
+ policy.VerificationFlags |= X509VerificationFlags.IgnoreEndRevocationUnknown;
+ }
+
+ // Run the user-provided configuration delegate and ensure the trust mode wasn't accidentally changed to
+ // prevent spoofing attacks (i.e attacks that consist in using a client certificate issued by a certificate
+ // authority trusted by the operating system but that isn't the one expected by the authorization server
+ // for client authentication). While discouraged, applications that need to use the system root store
+ // (e.g applications running on .NET Framework) can manually attach a custom policy to the server options.
+ configuration(policy);
+
+ if (policy.TrustMode is not X509ChainTrustMode.CustomRootTrust)
+ {
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0509));
+ }
+
+ return Configure(options => options.ClientCertificateChainPolicy = policy);
+#else
+ throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0508));
+#endif
+ }
+
+ ///
+ /// Configures OpenIddict to enable self-signed client certificate authentication (mTLS).
+ ///
+ /// The instance.
+ [EditorBrowsable(EditorBrowsableState.Advanced)]
+ public OpenIddictServerBuilder EnableSelfSignedClientCertificateAuthentication()
+ => EnableSelfSignedClientCertificateAuthentication(static policy => { });
+
+ ///
+ /// Configures OpenIddict to enable self-signed client certificate authentication (mTLS).
+ ///
+ /// The delegate used to amend the created X.509 chain policy.
+ /// The instance.
+ [EditorBrowsable(EditorBrowsableState.Advanced)]
+ public OpenIddictServerBuilder EnableSelfSignedClientCertificateAuthentication(Action configuration)
+ {
+ ArgumentNullException.ThrowIfNull(configuration);
+
+#if SUPPORTS_X509_CHAIN_POLICY_CUSTOM_TRUST_STORE
+ var policy = new X509ChainPolicy
+ {
+ // Note: by default, OpenIddict requires that end certificates used for authentication
+ // explicitly list client authentication as an allowed extended key usage.
+ ApplicationPolicy = { new Oid(ObjectIdentifiers.ExtendedKeyUsages.ClientAuthentication) },
+ // Note: self-signed certificates used for client authentication typically never include revocation
+ // information (CRL or AIA) and are "revoked" by simply being removed from the JSON Web Key Set.
+ RevocationMode = X509RevocationMode.NoCheck,
+ TrustMode = X509ChainTrustMode.CustomRootTrust
+ };
+
+ // Run the user-provided configuration delegate and ensure the trust mode wasn't accidentally changed to
+ // prevent spoofing attacks (i.e attacks that consist in using a client certificate issued by a certificate
+ // authority trusted by the operating system but that isn't the one expected by the authorization server
+ // for client authentication). While discouraged, applications that need to use the system root store
+ // (e.g applications running on .NET Framework) can manually attach a custom policy to the server options.
+ configuration(policy);
+
+ if (policy.TrustMode is not X509ChainTrustMode.CustomRootTrust)
+ {
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0509));
+ }
+
+ return Configure(options => options.SelfSignedClientCertificateChainPolicy = policy);
+#else
+ throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0508));
+#endif
+ }
+
///
[EditorBrowsable(EditorBrowsableState.Never)]
public override bool Equals(object? obj) => base.Equals(obj);
diff --git a/src/OpenIddict.Server/OpenIddictServerConfiguration.cs b/src/OpenIddict.Server/OpenIddictServerConfiguration.cs
index 1bcc9e4e..58b504f4 100644
--- a/src/OpenIddict.Server/OpenIddictServerConfiguration.cs
+++ b/src/OpenIddict.Server/OpenIddictServerConfiguration.cs
@@ -7,6 +7,7 @@
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
+using System.Security.Cryptography.X509Certificates;
using System.Text;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
@@ -164,6 +165,19 @@ public sealed class OpenIddictServerConfiguration : IPostConfigureOptions credentials.Key is X509SecurityKey x509SecurityKey &&
- (x509SecurityKey.Certificate.NotBefore > now || x509SecurityKey.Certificate.NotAfter < now)))
+ if (options.EncryptionCredentials.TrueForAll(credentials =>
+ credentials.Key is X509SecurityKey { Certificate: X509Certificate2 certificate } &&
+ (certificate.NotBefore > now || certificate.NotAfter < now)))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0087));
}
// If all the registered signing credentials are backed by a X.509 certificate, at least one of them must be valid.
- if (options.SigningCredentials.TrueForAll(credentials => credentials.Key is X509SecurityKey x509SecurityKey &&
- (x509SecurityKey.Certificate.NotBefore > now || x509SecurityKey.Certificate.NotAfter < now)))
+ if (options.SigningCredentials.TrueForAll(credentials =>
+ credentials.Key is X509SecurityKey { Certificate: X509Certificate2 certificate } &&
+ (certificate.NotBefore > now || certificate.NotAfter < now)))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0088));
}
+ // When set, the mTLS endpoint aliases MUST represent absolute HTTPS URLs.
+ if (!TryValidateMtlsEndpointAlias(options.MtlsDeviceAuthorizationEndpointAliasUri) ||
+ !TryValidateMtlsEndpointAlias(options.MtlsIntrospectionEndpointAliasUri) ||
+ !TryValidateMtlsEndpointAlias(options.MtlsPushedAuthorizationEndpointAliasUri) ||
+ !TryValidateMtlsEndpointAlias(options.MtlsRevocationEndpointAliasUri) ||
+ !TryValidateMtlsEndpointAlias(options.MtlsTokenEndpointAliasUri))
+ {
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0499));
+ }
+
+ // Prevent the mTLS aliases from being configured if the corresponding endpoints haven't been enabled.
+ if ((options.MtlsDeviceAuthorizationEndpointAliasUri is not null && options.DeviceAuthorizationEndpointUris.Count is 0) ||
+ (options.MtlsIntrospectionEndpointAliasUri is not null && options.IntrospectionEndpointUris.Count is 0) ||
+ (options.MtlsPushedAuthorizationEndpointAliasUri is not null && options.PushedAuthorizationEndpointUris.Count is 0) ||
+ (options.MtlsRevocationEndpointAliasUri is not null && options.RevocationEndpointUris.Count is 0) ||
+ (options.MtlsTokenEndpointAliasUri is not null && options.TokenEndpointUris.Count is 0))
+ {
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0510));
+ }
+
+ // If at least one mTLS endpoint alias was configured, require that the issuer be explicitly set
+ // to ensure it is not dynamically computed based on the current URI, as this would result in two
+ // different issuers being used (one pointing to the mTLS domain and one pointing to the regular one).
+ if (options.Issuer is null && (options.MtlsDeviceAuthorizationEndpointAliasUri is not null ||
+ options.MtlsIntrospectionEndpointAliasUri is not null ||
+ options.MtlsPushedAuthorizationEndpointAliasUri is not null ||
+ options.MtlsRevocationEndpointAliasUri is not null ||
+ options.MtlsTokenEndpointAliasUri is not null))
+ {
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0500));
+ }
+
+ // Ensure no end certificate was included in the PKI client certificate
+ // chain policy and that none of the certificates contains a private key.
+ if (options.ClientCertificateChainPolicy is not null)
+ {
+ if (options.ClientCertificateChainPolicy.ExtraStore.Cast()
+ .Any(static certificate =>
+ !OpenIddictHelpers.IsCertificateAuthority(certificate) ||
+ !OpenIddictHelpers.HasKeyUsage(certificate, X509KeyUsageFlags.KeyCertSign)))
+ {
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0501));
+ }
+
+ if (options.ClientCertificateChainPolicy.ExtraStore.Cast()
+ .Any(static certificate => certificate.HasPrivateKey))
+ {
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0511));
+ }
+
+#if SUPPORTS_X509_CHAIN_POLICY_CUSTOM_TRUST_STORE && SUPPORTS_X509_CHAIN_POLICY_TRUST_MODE
+ if (options.ClientCertificateChainPolicy.CustomTrustStore.Cast()
+ .Any(static certificate =>
+ !OpenIddictHelpers.IsCertificateAuthority(certificate) ||
+ !OpenIddictHelpers.HasKeyUsage(certificate, X509KeyUsageFlags.KeyCertSign)))
+ {
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0501));
+ }
+
+ if (options.ClientCertificateChainPolicy.CustomTrustStore.Cast()
+ .Any(static certificate => certificate.HasPrivateKey))
+ {
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0511));
+ }
+#endif
+ }
+
+ // Ensure the self-signed client certificate chain policy doesn't contain any certificate.
+ if (options.SelfSignedClientCertificateChainPolicy is not null)
+ {
+ if (options.SelfSignedClientCertificateChainPolicy.ExtraStore.Cast().Any())
+ {
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0502));
+ }
+
+#if SUPPORTS_X509_CHAIN_POLICY_CUSTOM_TRUST_STORE && SUPPORTS_X509_CHAIN_POLICY_TRUST_MODE
+ if (options.SelfSignedClientCertificateChainPolicy.CustomTrustStore.Cast().Any())
+ {
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0502));
+ }
+#endif
+ }
+
if (options.EnableDegradedMode)
{
// If the degraded mode was enabled, ensure custom validation handlers
@@ -529,5 +628,8 @@ public sealed class OpenIddictServerConfiguration : IPostConfigureOptions uri is null ||
+ (uri.IsAbsoluteUri && string.Equals(uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase));
}
}
diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.Discovery.cs b/src/OpenIddict.Server/OpenIddictServerEvents.Discovery.cs
index 50ac2b82..0222a8b0 100644
--- a/src/OpenIddict.Server/OpenIddictServerEvents.Discovery.cs
+++ b/src/OpenIddict.Server/OpenIddictServerEvents.Discovery.cs
@@ -136,6 +136,31 @@ public static partial class OpenIddictServerEvents
///
public Uri? UserInfoEndpoint { get; set; }
+ ///
+ /// Gets or sets the mTLS device authorization endpoint alias URI.
+ ///
+ public Uri? MtlsDeviceAuthorizationEndpointAlias { get; set; }
+
+ ///
+ /// Gets or sets the mTLS-specific introspection endpoint alias URI.
+ ///
+ public Uri? MtlsIntrospectionEndpointAlias { get; set; }
+
+ ///
+ /// Gets or sets the mTLS pushed authorization endpoint alias URI.
+ ///
+ public Uri? MtlsPushedAuthorizationEndpointAlias { get; set; }
+
+ ///
+ /// Gets or sets the mTLS revocation endpoint alias URI.
+ ///
+ public Uri? MtlsRevocationEndpointAlias { get; set; }
+
+ ///
+ /// Gets or sets the mTLS token endpoint alias URI.
+ ///
+ public Uri? MtlsTokenEndpointAlias { get; set; }
+
///
/// Gets the list of claims supported by the authorization server.
///
diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.cs b/src/OpenIddict.Server/OpenIddictServerEvents.cs
index 6346072b..c0cdd265 100644
--- a/src/OpenIddict.Server/OpenIddictServerEvents.cs
+++ b/src/OpenIddict.Server/OpenIddictServerEvents.cs
@@ -6,6 +6,7 @@
using System.ComponentModel;
using System.Security.Claims;
+using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.Logging;
namespace OpenIddict.Server;
@@ -764,6 +765,15 @@ public static partial class OpenIddictServerEvents
///
public ClaimsPrincipal? ClientAssertionPrincipal { get; set; }
+ ///
+ /// Gets or sets the client certificate (typically obtained via mTLS), if applicable.
+ ///
+ public X509Certificate2? ClientCertificate
+ {
+ get => Transaction.ClientCertificate;
+ set => Transaction.ClientCertificate = value;
+ }
+
///
/// Gets or sets the device code to validate, if applicable.
///
diff --git a/src/OpenIddict.Server/OpenIddictServerExtensions.cs b/src/OpenIddict.Server/OpenIddictServerExtensions.cs
index 55449f73..f6be36be 100644
--- a/src/OpenIddict.Server/OpenIddictServerExtensions.cs
+++ b/src/OpenIddict.Server/OpenIddictServerExtensions.cs
@@ -49,6 +49,7 @@ public static class OpenIddictServerExtensions
builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
+ builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
diff --git a/src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs b/src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs
index d2f091f9..18e31440 100644
--- a/src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs
+++ b/src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs
@@ -179,6 +179,20 @@ public static class OpenIddictServerHandlerFilters
}
}
+ ///
+ /// Represents a filter that excludes the associated handlers if no client authentication certificate is available.
+ ///
+ public sealed class RequireClientCertificate : IOpenIddictServerHandlerFilter
+ {
+ ///
+ public ValueTask IsActiveAsync(ProcessAuthenticationContext context)
+ {
+ ArgumentNullException.ThrowIfNull(context);
+
+ return new(context.ClientCertificate is not null);
+ }
+ }
+
///
/// Represents a filter that excludes the associated handlers when no client identifier is received.
///
diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs
index 2b572c28..c99467b4 100644
--- a/src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs
+++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs
@@ -8,6 +8,7 @@ using System.Collections.Immutable;
using System.Diagnostics;
using System.Security.Cryptography;
using System.Text.Json;
+using System.Text.Json.Nodes;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
@@ -235,6 +236,7 @@ public static partial class OpenIddictServerHandlers
[Metadata.UserInfoEndpoint] = notification.UserInfoEndpoint?.AbsoluteUri,
[Metadata.DeviceAuthorizationEndpoint] = notification.DeviceAuthorizationEndpoint?.AbsoluteUri,
[Metadata.PushedAuthorizationRequestEndpoint] = notification.PushedAuthorizationEndpoint?.AbsoluteUri,
+ [Metadata.MtlsEndpointAliases] = CreateMtlsEndpointAliases(notification),
[Metadata.JwksUri] = notification.JsonWebKeySetEndpoint?.AbsoluteUri,
[Metadata.GrantTypesSupported] = notification.GrantTypes.ToImmutableArray(),
[Metadata.ResponseTypesSupported] = notification.ResponseTypes.ToImmutableArray(),
@@ -259,6 +261,38 @@ public static partial class OpenIddictServerHandlers
}
context.Transaction.Response = response;
+
+ static JsonObject CreateMtlsEndpointAliases(HandleConfigurationRequestContext context)
+ {
+ var node = new JsonObject();
+
+ if (context.MtlsDeviceAuthorizationEndpointAlias is not null)
+ {
+ node.Add(Metadata.DeviceAuthorizationEndpoint, context.MtlsDeviceAuthorizationEndpointAlias.AbsoluteUri);
+ }
+
+ if (context.MtlsIntrospectionEndpointAlias is not null)
+ {
+ node.Add(Metadata.IntrospectionEndpoint, context.MtlsIntrospectionEndpointAlias.AbsoluteUri);
+ }
+
+ if (context.MtlsPushedAuthorizationEndpointAlias is not null)
+ {
+ node.Add(Metadata.PushedAuthorizationRequestEndpoint, context.MtlsPushedAuthorizationEndpointAlias.AbsoluteUri);
+ }
+
+ if (context.MtlsRevocationEndpointAlias is not null)
+ {
+ node.Add(Metadata.RevocationEndpoint, context.MtlsRevocationEndpointAlias.AbsoluteUri);
+ }
+
+ if (context.MtlsTokenEndpointAlias is not null)
+ {
+ node.Add(Metadata.TokenEndpoint, context.MtlsTokenEndpointAlias.AbsoluteUri);
+ }
+
+ return node;
+ }
}
}
@@ -377,6 +411,21 @@ public static partial class OpenIddictServerHandlers
context.JsonWebKeySetEndpoint ??= OpenIddictHelpers.CreateAbsoluteUri(
context.BaseUri, context.Options.JsonWebKeySetEndpointUris.FirstOrDefault());
+ context.MtlsDeviceAuthorizationEndpointAlias ??= OpenIddictHelpers.CreateAbsoluteUri(
+ context.BaseUri, context.Options.MtlsDeviceAuthorizationEndpointAliasUri);
+
+ context.MtlsIntrospectionEndpointAlias ??= OpenIddictHelpers.CreateAbsoluteUri(
+ context.BaseUri, context.Options.MtlsIntrospectionEndpointAliasUri);
+
+ context.MtlsPushedAuthorizationEndpointAlias ??= OpenIddictHelpers.CreateAbsoluteUri(
+ context.BaseUri, context.Options.MtlsPushedAuthorizationEndpointAliasUri);
+
+ context.MtlsRevocationEndpointAlias ??= OpenIddictHelpers.CreateAbsoluteUri(
+ context.BaseUri, context.Options.MtlsRevocationEndpointAliasUri);
+
+ context.MtlsTokenEndpointAlias ??= OpenIddictHelpers.CreateAbsoluteUri(
+ context.BaseUri, context.Options.MtlsTokenEndpointAliasUri);
+
context.PushedAuthorizationEndpoint ??= OpenIddictHelpers.CreateAbsoluteUri(
context.BaseUri, context.Options.PushedAuthorizationEndpointUris.FirstOrDefault());
diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs
index 494428dc..351ac1fc 100644
--- a/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs
+++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs
@@ -627,7 +627,7 @@ public static partial class OpenIddictServerHandlers
// To achieve that, all the "scope" claims are combined into a single one containg all the values.
// Visit https://datatracker.ietf.org/doc/html/rfc9068 for more information.
var scopes = context.Principal.GetClaims(Claims.Scope);
- if (scopes.Length > 1)
+ if (scopes.Length is > 1)
{
context.Principal.SetClaim(Claims.Scope, string.Join(" ", scopes));
}
diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.cs
index 58ffa3af..47c81773 100644
--- a/src/OpenIddict.Server/OpenIddictServerHandlers.cs
+++ b/src/OpenIddict.Server/OpenIddictServerHandlers.cs
@@ -8,7 +8,9 @@ using System.Collections.Immutable;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
+using System.Runtime.InteropServices;
using System.Security.Claims;
+using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.DependencyInjection;
@@ -36,13 +38,14 @@ public static partial class OpenIddictServerHandlers
EvaluateValidatedTokens.Descriptor,
ResolveValidatedTokens.Descriptor,
ValidateRequiredTokens.Descriptor,
- ValidateClientId.Descriptor,
- ValidateClientType.Descriptor,
- ValidateClientSecret.Descriptor,
ValidateClientAssertion.Descriptor,
ValidateClientAssertionWellknownClaims.Descriptor,
ValidateClientAssertionIssuer.Descriptor,
ValidateClientAssertionAudience.Descriptor,
+ ValidateClientId.Descriptor,
+ ValidateClientType.Descriptor,
+ ValidateClientSecret.Descriptor,
+ ValidateClientCertificate.Descriptor,
ValidateRequestToken.Descriptor,
ValidateRequestTokenType.Descriptor,
ValidateAccessToken.Descriptor,
@@ -1116,6 +1119,19 @@ public static partial class OpenIddictServerHandlers
return;
}
+ // Reject requests containing a TLS client certificate when the client is a public application.
+ if (context.ClientCertificate is not null)
+ {
+ context.Logger.LogInformation(6282, SR.GetResourceString(SR.ID6282), context.ClientId);
+
+ context.Reject(
+ error: Errors.InvalidClient,
+ description: SR.GetResourceString(SR.ID2196),
+ uri: SR.FormatID8000(SR.ID2196));
+
+ return;
+ }
+
// Reject requests containing a client_assertion when the client is a public application.
if (!string.IsNullOrEmpty(context.ClientAssertion))
{
@@ -1145,15 +1161,16 @@ public static partial class OpenIddictServerHandlers
return;
}
- // Confidential and hybrid applications MUST authenticate to protect them from impersonation attacks.
- if (context.ClientAssertionPrincipal is null && string.IsNullOrEmpty(context.ClientSecret))
+ // Confidential applications MUST authenticate to protect them from impersonation attacks.
+ if (context.ClientAssertionPrincipal is null &&
+ context.ClientCertificate is null && string.IsNullOrEmpty(context.ClientSecret))
{
context.Logger.LogInformation(6224, SR.GetResourceString(SR.ID6224), context.ClientId);
context.Reject(
error: Errors.InvalidClient,
- description: SR.FormatID2054(Parameters.ClientSecret),
- uri: SR.FormatID8000(SR.ID2054));
+ description: SR.GetResourceString(SR.ID2198),
+ uri: SR.FormatID8000(SR.ID2198));
return;
}
@@ -1226,6 +1243,107 @@ public static partial class OpenIddictServerHandlers
}
}
+ ///
+ /// Contains the logic responsible for validating the TLS client certificate used for client authentication, if applicable.
+ ///
+ public sealed class ValidateClientCertificate : IOpenIddictServerHandler
+ {
+ private readonly IOpenIddictApplicationManager _applicationManager;
+
+ public ValidateClientCertificate() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016));
+
+ public ValidateClientCertificate(IOpenIddictApplicationManager applicationManager)
+ => _applicationManager = applicationManager ?? throw new ArgumentNullException(nameof(applicationManager));
+
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .AddFilter()
+ .AddFilter()
+ .UseScopedHandler()
+ .SetOrder(ValidateClientSecret.Descriptor.Order + 1_000)
+ .SetType(OpenIddictServerHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public async ValueTask HandleAsync(ProcessAuthenticationContext context)
+ {
+ ArgumentNullException.ThrowIfNull(context);
+
+ Debug.Assert(!string.IsNullOrEmpty(context.ClientId), SR.FormatID4000(Parameters.ClientId));
+ Debug.Assert(context.ClientCertificate is not null, SR.GetResourceString(SR.ID4020));
+
+ // Don't validate the client secret on endpoints that don't support client authentication.
+ if (context.EndpointType is OpenIddictServerEndpointType.Authorization or
+ OpenIddictServerEndpointType.EndSession or
+ OpenIddictServerEndpointType.EndUserVerification or
+ OpenIddictServerEndpointType.UserInfo)
+ {
+ return;
+ }
+
+ var application = await _applicationManager.FindByClientIdAsync(context.ClientId) ??
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0032));
+
+ // If the application is a public client, don't validate the client certificate.
+ if (await _applicationManager.HasClientTypeAsync(application, ClientTypes.Public))
+ {
+ return;
+ }
+
+ // Note: to avoid building and introspecting a X.509 certificate chain and reduce the cost
+ // of this check, a certificate is always assumed to be self-signed when it is self-issued.
+ //
+ // A second pass is internally performed by the default implementations of the
+ // ValidateSelfSignedClientCertificateAsync() and ValidateClientCertificateAsync() APIs
+ // once the chain is built to validate whether the certificate is self-signed or not.
+ if (OpenIddictHelpers.IsSelfIssuedCertificate(context.ClientCertificate))
+ {
+ if (context.Options.SelfSignedClientCertificateChainPolicy is null)
+ {
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0506));
+ }
+
+ var policy = await _applicationManager.GetSelfSignedClientCertificateChainPolicyAsync(application, context.Options.SelfSignedClientCertificateChainPolicy);
+ if (policy is null || !await _applicationManager.ValidateSelfSignedClientCertificateAsync(application, context.ClientCertificate, policy))
+ {
+ context.Logger.LogInformation(6283, SR.GetResourceString(SR.ID6283), context.ClientId);
+
+ context.Reject(
+ error: Errors.InvalidClient,
+ description: SR.GetResourceString(SR.ID2197),
+ uri: SR.FormatID8000(SR.ID2197));
+
+ return;
+ }
+ }
+
+ else
+ {
+ if (context.Options.ClientCertificateChainPolicy is null)
+ {
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0505));
+ }
+
+ var policy = await _applicationManager.GetClientCertificateChainPolicyAsync(application, context.Options.ClientCertificateChainPolicy);
+ if (policy is null || !await _applicationManager.ValidateClientCertificateAsync(application, context.ClientCertificate, policy))
+ {
+ context.Logger.LogInformation(6284, SR.GetResourceString(SR.ID6284), context.ClientId);
+
+ context.Reject(
+ error: Errors.InvalidClient,
+ description: SR.GetResourceString(SR.ID2197),
+ uri: SR.FormatID8000(SR.ID2197));
+
+ return;
+ }
+ }
+ }
+ }
+
///
/// Contains the logic responsible for validating the request token resolved from the context.
///
@@ -1243,7 +1361,7 @@ public static partial class OpenIddictServerHandlers
= OpenIddictServerHandlerDescriptor.CreateBuilder()
.AddFilter()
.UseScopedHandler()
- .SetOrder(ValidateClientSecret.Descriptor.Order + 1_000)
+ .SetOrder(ValidateClientCertificate.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
diff --git a/src/OpenIddict.Server/OpenIddictServerOptions.cs b/src/OpenIddict.Server/OpenIddictServerOptions.cs
index 58f9aaad..768319e5 100644
--- a/src/OpenIddict.Server/OpenIddictServerOptions.cs
+++ b/src/OpenIddict.Server/OpenIddictServerOptions.cs
@@ -5,6 +5,7 @@
*/
using System.ComponentModel;
+using System.Security.Cryptography.X509Certificates;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
@@ -127,6 +128,56 @@ public sealed class OpenIddictServerOptions
SetDefaultTimesOnTokenCreation = false
};
+ ///
+ /// Gets or sets the URI listed as the mTLS device authorization
+ /// endpoint alias in the server configuration metadata.
+ ///
+ ///
+ /// Note: this URI MUST be absolute and MUST point to a domain for
+ /// which TLS client authentication is enforced by the web server.
+ ///
+ public Uri? MtlsDeviceAuthorizationEndpointAliasUri { get; set; }
+
+ ///
+ /// Gets or sets the URI listed as the mTLS introspection
+ /// endpoint alias in the server configuration metadata.
+ ///
+ ///
+ /// Note: this URI MUST be absolute and MUST point to a domain for
+ /// which TLS client authentication is enforced by the web server.
+ ///
+ public Uri? MtlsIntrospectionEndpointAliasUri { get; set; }
+
+ ///
+ /// Gets or sets the URI listed as the mTLS pushed authorization
+ /// endpoint alias in the server configuration metadata.
+ ///
+ ///
+ /// Note: this URI MUST be absolute and MUST point to a domain for
+ /// which TLS client authentication is enforced by the web server.
+ ///
+ public Uri? MtlsPushedAuthorizationEndpointAliasUri { get; set; }
+
+ ///
+ /// Gets or sets the URI listed as the mTLS revocation
+ /// endpoint alias in the server configuration metadata.
+ ///
+ ///
+ /// Note: this URI MUST be absolute and MUST point to a domain for
+ /// which TLS client authentication is enforced by the web server.
+ ///
+ public Uri? MtlsRevocationEndpointAliasUri { get; set; }
+
+ ///
+ /// Gets or sets the URI listed as the mTLS token
+ /// endpoint alias in the server configuration metadata.
+ ///
+ ///
+ /// Note: this URI MUST be absolute and MUST point to a domain for
+ /// which TLS client authentication is enforced by the web server.
+ ///
+ public Uri? MtlsTokenEndpointAliasUri { get; set; }
+
///
/// Gets the token validation parameters used by the OpenIddict server services.
///
@@ -605,4 +656,42 @@ public sealed class OpenIddictServerOptions
/// If no service can be found, is used.
///
public TimeProvider TimeProvider { get; set; } = default!;
+
+ ///
+ /// Gets or sets the chain policy used when validating client certificates
+ /// used for client authentication (typically, via mTLS).
+ ///
+ ///
+ ///
+ /// -
+ /// Note: this instance serves as a base policy and is merged with
+ /// the per-client policies resolved using the application manager.
+ ///
+ /// -
+ /// Note: while it is possible to use a policy configured to use the
+ /// the system certificates store, doing is so is strongly discouraged.
+ ///
+ ///
+ ///
+ [EditorBrowsable(EditorBrowsableState.Advanced)]
+ public X509ChainPolicy? ClientCertificateChainPolicy { get; set; }
+
+ ///
+ /// Gets or sets the chain policy used when validating self-signed client
+ /// certificates used for client authentication (typically, via mTLS).
+ ///
+ ///
+ ///
+ /// -
+ /// Note: this instance serves as a base policy and is merged with
+ /// the per-client policies resolved using the application manager.
+ ///
+ /// -
+ /// Note: while it is possible to use a policy configured to use the
+ /// the system certificates store, doing is so is strongly discouraged.
+ ///
+ ///
+ ///
+ [EditorBrowsable(EditorBrowsableState.Advanced)]
+ public X509ChainPolicy? SelfSignedClientCertificateChainPolicy { get; set; }
}
diff --git a/src/OpenIddict.Server/OpenIddictServerTransaction.cs b/src/OpenIddict.Server/OpenIddictServerTransaction.cs
index b177e187..795fbd9b 100644
--- a/src/OpenIddict.Server/OpenIddictServerTransaction.cs
+++ b/src/OpenIddict.Server/OpenIddictServerTransaction.cs
@@ -5,6 +5,7 @@
*/
using System.ComponentModel;
+using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.Logging;
namespace OpenIddict.Server;
@@ -26,6 +27,11 @@ public sealed class OpenIddictServerTransaction
///
public CancellationToken CancellationToken { get; set; }
+ ///
+ /// Gets or sets the X.509 client certificate, if available.
+ ///
+ public X509Certificate2? ClientCertificate { get; set; }
+
///
/// Gets or sets the type of the endpoint processing the current request.
///
diff --git a/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreConfiguration.cs b/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreConfiguration.cs
index 898a7073..d453e53b 100644
--- a/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreConfiguration.cs
+++ b/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreConfiguration.cs
@@ -63,7 +63,7 @@ public sealed class OpenIddictValidationAspNetCoreConfiguration : IConfigureOpti
// on invalid endpoints. To opt out this undesirable behavior, a fake entry
// is dynamically added if one of the default schemes properties is not set
// and less than 2 handlers were registered in the authentication options.
- if (options.SchemeMap.Count < 2 && string.IsNullOrEmpty(options.DefaultScheme) &&
+ if (options.SchemeMap.Count is < 2 && string.IsNullOrEmpty(options.DefaultScheme) &&
(string.IsNullOrEmpty(options.DefaultSignInScheme) ||
string.IsNullOrEmpty(options.DefaultSignOutScheme)))
{
diff --git a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpConfiguration.cs b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpConfiguration.cs
index ef8fcdfc..ba7b1120 100644
--- a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpConfiguration.cs
+++ b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpConfiguration.cs
@@ -253,10 +253,12 @@ public sealed class OpenIddictValidationSystemNetHttpConfiguration : IConfigureO
foreach (var credentials in _provider.GetRequiredService>()
.CurrentValue.SigningCredentials)
{
+ // Note: to avoid building and introspecting a X.509 certificate chain and reduce the cost
+ // of this check, a certificate is always assumed to be self-signed when it is self-issued.
if (credentials.Key is X509SecurityKey { Certificate: X509Certificate2 certificate } &&
- certificate.Version is >= 3 && IsSelfIssuedCertificate(certificate) &&
- HasDigitalSignatureKeyUsage(certificate) &&
- HasClientAuthenticationExtendedKeyUsage(certificate))
+ certificate.Version is >= 3 && OpenIddictHelpers.IsSelfIssuedCertificate(certificate) &&
+ OpenIddictHelpers.HasKeyUsage(certificate, X509KeyUsageFlags.DigitalSignature) &&
+ OpenIddictHelpers.HasExtendedKeyUsage(certificate, ObjectIdentifiers.ExtendedKeyUsages.ClientAuthentication))
{
return certificate;
}
@@ -270,10 +272,12 @@ public sealed class OpenIddictValidationSystemNetHttpConfiguration : IConfigureO
foreach (var credentials in _provider.GetRequiredService>()
.CurrentValue.SigningCredentials)
{
+ // Note: to avoid building and introspecting a X.509 certificate chain and reduce the cost
+ // of this check, a certificate is always assumed to be self-signed when it is self-issued.
if (credentials.Key is X509SecurityKey { Certificate: X509Certificate2 certificate } &&
- certificate.Version is >= 3 && !IsSelfIssuedCertificate(certificate) &&
- HasDigitalSignatureKeyUsage(certificate) &&
- HasClientAuthenticationExtendedKeyUsage(certificate))
+ certificate.Version is >= 3 && !OpenIddictHelpers.IsSelfIssuedCertificate(certificate) &&
+ OpenIddictHelpers.HasKeyUsage(certificate, X509KeyUsageFlags.DigitalSignature) &&
+ OpenIddictHelpers.HasExtendedKeyUsage(certificate, ObjectIdentifiers.ExtendedKeyUsages.ClientAuthentication))
{
return certificate;
}
@@ -281,51 +285,5 @@ public sealed class OpenIddictValidationSystemNetHttpConfiguration : IConfigureO
return null;
};
-
- static bool HasClientAuthenticationExtendedKeyUsage(X509Certificate2 certificate)
- {
- for (var index = 0; index < certificate.Extensions.Count; index++)
- {
- if (certificate.Extensions[index] is X509EnhancedKeyUsageExtension extension &&
- HasOid(extension.EnhancedKeyUsages, "1.3.6.1.5.5.7.3.2"))
- {
- return true;
- }
- }
-
- return false;
-
- static bool HasOid(OidCollection collection, string value)
- {
- for (var index = 0; index < collection.Count; index++)
- {
- if (collection[index] is Oid oid && string.Equals(oid.Value, value, StringComparison.Ordinal))
- {
- return true;
- }
- }
-
- return false;
- }
- }
-
- static bool HasDigitalSignatureKeyUsage(X509Certificate2 certificate)
- {
- for (var index = 0; index < certificate.Extensions.Count; index++)
- {
- if (certificate.Extensions[index] is X509KeyUsageExtension extension &&
- extension.KeyUsages.HasFlag(X509KeyUsageFlags.DigitalSignature))
- {
- return true;
- }
- }
-
- return false;
- }
-
- // Note: to avoid building and introspecting a X.509 certificate chain and reduce the cost
- // of this check, a certificate is always assumed to be self-signed when it is self-issued.
- static bool IsSelfIssuedCertificate(X509Certificate2 certificate)
- => certificate.SubjectName.RawData.AsSpan().SequenceEqual(certificate.IssuerName.RawData);
}
}
diff --git a/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs b/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs
index 3d1e7ea8..f99e26f7 100644
--- a/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs
+++ b/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs
@@ -134,8 +134,7 @@ public sealed class OpenIddictValidationBuilder
ArgumentNullException.ThrowIfNull(key);
// If the encryption key is an asymmetric security key, ensure it has a private key.
- if (key is AsymmetricSecurityKey asymmetricSecurityKey &&
- asymmetricSecurityKey.PrivateKeyStatus is PrivateKeyStatus.DoesNotExist)
+ if (key is AsymmetricSecurityKey { PrivateKeyStatus: PrivateKeyStatus.DoesNotExist })
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0055));
}
@@ -178,7 +177,7 @@ public sealed class OpenIddictValidationBuilder
// 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)
+ if (certificate.Version is >= 3)
{
var extensions = certificate.Extensions.OfType().ToList();
if (extensions.Count is not 0 && !extensions.Exists(static extension =>
@@ -299,7 +298,7 @@ public sealed class OpenIddictValidationBuilder
store.Open(OpenFlags.ReadOnly);
return store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false)
- .OfType()
+ .Cast()
.SingleOrDefault();
}
}
@@ -321,7 +320,7 @@ public sealed class OpenIddictValidationBuilder
return AddEncryptionCertificate(
store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false)
- .OfType()
+ .Cast()
.SingleOrDefault() ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0066)));
}
@@ -359,8 +358,7 @@ public sealed class OpenIddictValidationBuilder
ArgumentNullException.ThrowIfNull(key);
// If the signing key is an asymmetric security key, ensure it has a private key.
- if (key is AsymmetricSecurityKey asymmetricSecurityKey &&
- asymmetricSecurityKey.PrivateKeyStatus is PrivateKeyStatus.DoesNotExist)
+ if (key is AsymmetricSecurityKey { PrivateKeyStatus: PrivateKeyStatus.DoesNotExist })
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0067));
}
@@ -426,7 +424,7 @@ public sealed class OpenIddictValidationBuilder
// 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)
+ if (certificate.Version is >= 3)
{
var extensions = certificate.Extensions.OfType().ToList();
if (extensions.Count is not 0 && !extensions.Exists(static extension =>
@@ -545,7 +543,7 @@ public sealed class OpenIddictValidationBuilder
store.Open(OpenFlags.ReadOnly);
return store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false)
- .OfType()
+ .Cast()
.SingleOrDefault();
}
}
@@ -566,7 +564,7 @@ public sealed class OpenIddictValidationBuilder
return AddSigningCertificate(
store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false)
- .OfType()
+ .Cast()
.SingleOrDefault() ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0066)));
}
diff --git a/src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs b/src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs
index 8e4c73fa..4e984058 100644
--- a/src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs
+++ b/src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs
@@ -4,6 +4,7 @@
* the license and the contributors participating to this project.
*/
+using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Protocols;
@@ -97,9 +98,9 @@ public sealed class OpenIddictValidationConfiguration : IPostConfigureOptions credentials.Key is X509SecurityKey x509SecurityKey &&
- (x509SecurityKey.Certificate.NotBefore > now || x509SecurityKey.Certificate.NotAfter < now)))
+ if (options.EncryptionCredentials.Count is not 0 && options.EncryptionCredentials.TrueForAll(credentials =>
+ credentials.Key is X509SecurityKey { Certificate: X509Certificate2 certificate } &&
+ (certificate.NotBefore > now || certificate.NotAfter < now)))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0087));
}
diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs
index 77590d8c..55d241bf 100644
--- a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs
+++ b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs
@@ -398,7 +398,7 @@ public static partial class OpenIddictValidationHandlers
// To achieve that, all the "scope" claims are combined into a single one containg all the values.
// Visit https://datatracker.ietf.org/doc/html/rfc9068 for more information.
var scopes = context.Principal.GetClaims(Claims.Scope);
- if (scopes.Length > 1)
+ if (scopes.Length is > 1)
{
context.Principal.SetClaim(Claims.Scope, string.Join(" ", scopes));
}
diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Authentication.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Authentication.cs
index e27ac704..f2f780c9 100644
--- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Authentication.cs
+++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Authentication.cs
@@ -3214,130 +3214,6 @@ public abstract partial class OpenIddictServerIntegrationTests
Assert.Equal(SR.FormatID8000(SR.ID2029), response.ErrorUri);
}
- [Fact]
- public async Task ValidatePushedAuthorizationRequest_ClientSecretCannotBeUsedByPublicClients()
- {
- // Arrange
- var application = new OpenIddictApplication();
-
- var manager = CreateApplicationManager(mock =>
- {
- mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()))
- .ReturnsAsync(application);
-
- mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()))
- .ReturnsAsync(true);
- });
-
- await using var server = await CreateServerAsync(options =>
- {
- options.Services.AddSingleton(manager);
- });
-
- await using var client = await server.CreateClientAsync();
-
- // Act
- var response = await client.PostAsync("/connect/par", new OpenIddictRequest
- {
- ClientId = "Fabrikam",
- ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw",
- RedirectUri = "http://www.fabrikam.com/path",
- ResponseType = ResponseTypes.Code
- });
-
- // Assert
- Assert.Equal(Errors.InvalidClient, response.Error);
- Assert.Equal(SR.FormatID2053(Parameters.ClientSecret), response.ErrorDescription);
- Assert.Equal(SR.FormatID8000(SR.ID2053), response.ErrorUri);
-
- Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce());
- Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()), Times.Once());
- }
-
- [Fact]
- public async Task ValidatePushedAuthorizationRequest_ClientSecretIsRequiredForNonPublicClients()
- {
- // Arrange
- var application = new OpenIddictApplication();
-
- var manager = CreateApplicationManager(mock =>
- {
- mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()))
- .ReturnsAsync(application);
-
- mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()))
- .ReturnsAsync(false);
- });
-
- await using var server = await CreateServerAsync(options =>
- {
- options.Services.AddSingleton(manager);
- });
-
- await using var client = await server.CreateClientAsync();
-
- // Act
- var response = await client.PostAsync("/connect/par", new OpenIddictRequest
- {
- ClientId = "Fabrikam",
- ClientSecret = null,
- RedirectUri = "http://www.fabrikam.com/path",
- ResponseType = ResponseTypes.Code
- });
-
- // Assert
- Assert.Equal(Errors.InvalidClient, response.Error);
- Assert.Equal(SR.FormatID2054(Parameters.ClientSecret), response.ErrorDescription);
- Assert.Equal(SR.FormatID8000(SR.ID2054), response.ErrorUri);
-
- Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce());
- Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()), Times.Once());
- }
-
- [Fact]
- public async Task ValidatePushedAuthorizationRequest_RequestIsRejectedWhenClientCredentialsAreInvalid()
- {
- // Arrange
- var application = new OpenIddictApplication();
-
- var manager = CreateApplicationManager(mock =>
- {
- mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()))
- .ReturnsAsync(application);
-
- mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()))
- .ReturnsAsync(false);
-
- mock.Setup(manager => manager.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny()))
- .ReturnsAsync(false);
- });
-
- await using var server = await CreateServerAsync(options =>
- {
- options.Services.AddSingleton(manager);
- });
-
- await using var client = await server.CreateClientAsync();
-
- // Act
- var response = await client.PostAsync("/connect/par", new OpenIddictRequest
- {
- ClientId = "Fabrikam",
- ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw",
- RedirectUri = "http://www.fabrikam.com/path",
- ResponseType = ResponseTypes.Code
- });
-
- // Assert
- Assert.Equal(Errors.InvalidClient, response.Error);
- Assert.Equal(SR.GetResourceString(SR.ID2055), response.ErrorDescription);
- Assert.Equal(SR.FormatID8000(SR.ID2055), response.ErrorUri);
-
- Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce());
- Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()), Times.AtLeastOnce());
- Mock.Get(manager).Verify(manager => manager.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny()), Times.Once());
- }
-
[Fact]
public async Task ValidatePushedAuthorizationRequest_MissingRedirectUriCausesAnErrorForOpenIdRequests()
{
diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Device.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Device.cs
index 3d3b404d..2e3bfeef 100644
--- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Device.cs
+++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Device.cs
@@ -491,124 +491,6 @@ public abstract partial class OpenIddictServerIntegrationTests
Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce());
}
- [Fact]
- public async Task ValidateDeviceAuthorizationRequest_ClientSecretCannotBeUsedByPublicClients()
- {
- // Arrange
- var application = new OpenIddictApplication();
-
- var manager = CreateApplicationManager(mock =>
- {
- mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()))
- .ReturnsAsync(application);
-
- mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()))
- .ReturnsAsync(true);
- });
-
- await using var server = await CreateServerAsync(options =>
- {
- options.Services.AddSingleton(manager);
- });
-
- await using var client = await server.CreateClientAsync();
-
- // Act
- var response = await client.PostAsync("/connect/device", new OpenIddictRequest
- {
- ClientId = "Fabrikam",
- ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw"
- });
-
- // Assert
- Assert.Equal(Errors.InvalidClient, response.Error);
- Assert.Equal(SR.FormatID2053(Parameters.ClientSecret), response.ErrorDescription);
- Assert.Equal(SR.FormatID8000(SR.ID2053), response.ErrorUri);
-
- Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce());
- Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()), Times.Once());
- }
-
- [Fact]
- public async Task ValidateDeviceAuthorizationRequest_ClientSecretIsRequiredForNonPublicClients()
- {
- // Arrange
- var application = new OpenIddictApplication();
-
- var manager = CreateApplicationManager(mock =>
- {
- mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()))
- .ReturnsAsync(application);
-
- mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()))
- .ReturnsAsync(false);
- });
-
- await using var server = await CreateServerAsync(options =>
- {
- options.Services.AddSingleton(manager);
- });
-
- await using var client = await server.CreateClientAsync();
-
- // Act
- var response = await client.PostAsync("/connect/device", new OpenIddictRequest
- {
- ClientId = "Fabrikam",
- ClientSecret = null
- });
-
- // Assert
- Assert.Equal(Errors.InvalidClient, response.Error);
- Assert.Equal(SR.FormatID2054(Parameters.ClientSecret), response.ErrorDescription);
- Assert.Equal(SR.FormatID8000(SR.ID2054), response.ErrorUri);
-
- Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce());
- Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()), Times.Once());
- }
-
- [Fact]
- public async Task ValidateDeviceAuthorizationRequest_RequestIsRejectedWhenClientCredentialsAreInvalid()
- {
- // Arrange
- var application = new OpenIddictApplication();
-
- var manager = CreateApplicationManager(mock =>
- {
- mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()))
- .ReturnsAsync(application);
-
- mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()))
- .ReturnsAsync(false);
-
- mock.Setup(manager => manager.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny()))
- .ReturnsAsync(false);
- });
-
- await using var server = await CreateServerAsync(options =>
- {
- options.Services.AddSingleton(manager);
- });
-
- await using var client = await server.CreateClientAsync();
-
- // Act
- var response = await client.PostAsync("/connect/device", new OpenIddictRequest
- {
- ClientId = "Fabrikam",
- ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw"
- });
-
- // Assert
- Assert.Equal(Errors.InvalidClient, response.Error);
- Assert.Equal(SR.GetResourceString(SR.ID2055), response.ErrorDescription);
- Assert.Equal(SR.FormatID8000(SR.ID2055), response.ErrorUri);
-
- Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce());
- Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()), Times.AtLeastOnce());
- Mock.Get(manager).Verify(manager => manager.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny()), Times.Once());
- }
-
[Fact]
public async Task ValidateDeviceAuthorizationRequest_RequestIsRejectedWhenEndpointPermissionIsNotGranted()
{
diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Discovery.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Discovery.cs
index 59f2f1ca..1ceea847 100644
--- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Discovery.cs
+++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Discovery.cs
@@ -265,9 +265,10 @@ public abstract partial class OpenIddictServerIntegrationTests
{
options.SetAuthorizationEndpointUris("https://www.fabrikam.com/path/authorization_endpoint")
.SetJsonWebKeySetEndpointUris("https://www.fabrikam.com/path/cryptography_endpoint")
- .SetDeviceAuthorizationEndpointUris("https://www.fabrikam.com/path/device_endpoint")
+ .SetDeviceAuthorizationEndpointUris("https://www.fabrikam.com/path/device_authorization_endpoint")
.SetIntrospectionEndpointUris("https://www.fabrikam.com/path/introspection_endpoint")
- .SetEndSessionEndpointUris("https://www.fabrikam.com/path/logout_endpoint")
+ .SetEndSessionEndpointUris("https://www.fabrikam.com/path/end_session_endpoint")
+ .SetPushedAuthorizationEndpointUris("https://www.fabrikam.com/path/pushed_authorization_endpoint")
.SetRevocationEndpointUris("https://www.fabrikam.com/path/revocation_endpoint")
.SetTokenEndpointUris("https://www.fabrikam.com/path/token_endpoint")
.SetUserInfoEndpointUris("https://www.fabrikam.com/path/userinfo_endpoint");
@@ -288,15 +289,18 @@ public abstract partial class OpenIddictServerIntegrationTests
Assert.Equal("https://www.fabrikam.com/path/authorization_endpoint",
(string?) response[Metadata.AuthorizationEndpoint]);
- Assert.Equal("https://www.fabrikam.com/path/device_endpoint",
+ Assert.Equal("https://www.fabrikam.com/path/device_authorization_endpoint",
(string?) response[Metadata.DeviceAuthorizationEndpoint]);
Assert.Equal("https://www.fabrikam.com/path/introspection_endpoint",
(string?) response[Metadata.IntrospectionEndpoint]);
- Assert.Equal("https://www.fabrikam.com/path/logout_endpoint",
+ Assert.Equal("https://www.fabrikam.com/path/end_session_endpoint",
(string?) response[Metadata.EndSessionEndpoint]);
+ Assert.Equal("https://www.fabrikam.com/path/pushed_authorization_endpoint",
+ (string?) response[Metadata.PushedAuthorizationRequestEndpoint]);
+
Assert.Equal("https://www.fabrikam.com/path/revocation_endpoint",
(string?) response[Metadata.RevocationEndpoint]);
@@ -315,9 +319,9 @@ public abstract partial class OpenIddictServerIntegrationTests
{
options.SetAuthorizationEndpointUris("path/authorization_endpoint")
.SetJsonWebKeySetEndpointUris("path/cryptography_endpoint")
- .SetDeviceAuthorizationEndpointUris("path/device_endpoint")
+ .SetDeviceAuthorizationEndpointUris("path/device_authorization_endpoint")
.SetIntrospectionEndpointUris("path/introspection_endpoint")
- .SetEndSessionEndpointUris("path/logout_endpoint")
+ .SetEndSessionEndpointUris("path/end_session_endpoint")
.SetPushedAuthorizationEndpointUris("path/pushed_authorization_endpoint")
.SetRevocationEndpointUris("path/revocation_endpoint")
.SetTokenEndpointUris("path/token_endpoint")
@@ -332,9 +336,9 @@ public abstract partial class OpenIddictServerIntegrationTests
// Assert
Assert.Equal("http://localhost/path/authorization_endpoint", (string?) response[Metadata.AuthorizationEndpoint]);
Assert.Equal("http://localhost/path/cryptography_endpoint", (string?) response[Metadata.JwksUri]);
- Assert.Equal("http://localhost/path/device_endpoint", (string?) response[Metadata.DeviceAuthorizationEndpoint]);
+ Assert.Equal("http://localhost/path/device_authorization_endpoint", (string?) response[Metadata.DeviceAuthorizationEndpoint]);
Assert.Equal("http://localhost/path/introspection_endpoint", (string?) response[Metadata.IntrospectionEndpoint]);
- Assert.Equal("http://localhost/path/logout_endpoint", (string?) response[Metadata.EndSessionEndpoint]);
+ Assert.Equal("http://localhost/path/end_session_endpoint", (string?) response[Metadata.EndSessionEndpoint]);
Assert.Equal("http://localhost/path/pushed_authorization_endpoint", (string?) response[Metadata.PushedAuthorizationRequestEndpoint]);
Assert.Equal("http://localhost/path/revocation_endpoint", (string?) response[Metadata.RevocationEndpoint]);
Assert.Equal("http://localhost/path/token_endpoint", (string?) response[Metadata.TokenEndpoint]);
@@ -349,9 +353,9 @@ public abstract partial class OpenIddictServerIntegrationTests
{
options.SetAuthorizationEndpointUris("path/authorization_endpoint")
.SetJsonWebKeySetEndpointUris("path/cryptography_endpoint")
- .SetDeviceAuthorizationEndpointUris("path/device_endpoint")
+ .SetDeviceAuthorizationEndpointUris("path/device_authorization_endpoint")
.SetIntrospectionEndpointUris("path/introspection_endpoint")
- .SetEndSessionEndpointUris("path/logout_endpoint")
+ .SetEndSessionEndpointUris("path/end_session_endpoint")
.SetPushedAuthorizationEndpointUris("path/pushed_authorization_endpoint")
.SetRevocationEndpointUris("path/revocation_endpoint")
.SetTokenEndpointUris("path/token_endpoint")
@@ -378,9 +382,9 @@ public abstract partial class OpenIddictServerIntegrationTests
// Assert
Assert.Equal("https://contoso.com/issuer/path/authorization_endpoint", (string?) response[Metadata.AuthorizationEndpoint]);
Assert.Equal("https://contoso.com/issuer/path/cryptography_endpoint", (string?) response[Metadata.JwksUri]);
- Assert.Equal("https://contoso.com/issuer/path/device_endpoint", (string?) response[Metadata.DeviceAuthorizationEndpoint]);
+ Assert.Equal("https://contoso.com/issuer/path/device_authorization_endpoint", (string?) response[Metadata.DeviceAuthorizationEndpoint]);
Assert.Equal("https://contoso.com/issuer/path/introspection_endpoint", (string?) response[Metadata.IntrospectionEndpoint]);
- Assert.Equal("https://contoso.com/issuer/path/logout_endpoint", (string?) response[Metadata.EndSessionEndpoint]);
+ Assert.Equal("https://contoso.com/issuer/path/end_session_endpoint", (string?) response[Metadata.EndSessionEndpoint]);
Assert.Equal("https://contoso.com/issuer/path/pushed_authorization_endpoint", (string?) response[Metadata.PushedAuthorizationRequestEndpoint]);
Assert.Equal("https://contoso.com/issuer/path/revocation_endpoint", (string?) response[Metadata.RevocationEndpoint]);
Assert.Equal("https://contoso.com/issuer/path/token_endpoint", (string?) response[Metadata.TokenEndpoint]);
@@ -977,6 +981,43 @@ public abstract partial class OpenIddictServerIntegrationTests
Assert.False((bool?) response[Metadata.RequestUriParameterSupported]);
}
+ [Fact]
+ public async Task HandleConfigurationRequest_MtlsAliasesAreReturned()
+ {
+ // Arrange
+ await using var server = await CreateServerAsync(options =>
+ {
+ options.SetIssuer("https://mtls.fabrikam.com/");
+
+ options.SetMtlsDeviceAuthorizationEndpointAliasUri("https://mtls.fabrikam.com/path/device_authorization_endpoint")
+ .SetMtlsIntrospectionEndpointAliasUri("https://mtls.fabrikam.com/path/introspection_endpoint")
+ .SetMtlsPushedAuthorizationEndpointAliasUri("https://mtls.fabrikam.com/path/pushed_authorization_endpoint")
+ .SetMtlsRevocationEndpointAliasUri("https://mtls.fabrikam.com/path/revocation_endpoint")
+ .SetMtlsTokenEndpointAliasUri("https://mtls.fabrikam.com/path/token_endpoint");
+ });
+
+ await using var client = await server.CreateClientAsync();
+
+ // Act
+ var response = await client.GetAsync("/.well-known/openid-configuration");
+
+ // Assert
+ Assert.Equal("https://mtls.fabrikam.com/path/device_authorization_endpoint",
+ (string?) response[Metadata.MtlsEndpointAliases]?[Metadata.DeviceAuthorizationEndpoint]);
+
+ Assert.Equal("https://mtls.fabrikam.com/path/introspection_endpoint",
+ (string?) response[Metadata.MtlsEndpointAliases]?[Metadata.IntrospectionEndpoint]);
+
+ Assert.Equal("https://mtls.fabrikam.com/path/pushed_authorization_endpoint",
+ (string?) response[Metadata.MtlsEndpointAliases]?[Metadata.PushedAuthorizationRequestEndpoint]);
+
+ Assert.Equal("https://mtls.fabrikam.com/path/revocation_endpoint",
+ (string?) response[Metadata.MtlsEndpointAliases]?[Metadata.RevocationEndpoint]);
+
+ Assert.Equal("https://mtls.fabrikam.com/path/token_endpoint",
+ (string?) response[Metadata.MtlsEndpointAliases]?[Metadata.TokenEndpoint]);
+ }
+
[Theory]
[InlineData(true)]
[InlineData(false)]
diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs
index 59fc926d..6307bda1 100644
--- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs
+++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs
@@ -2533,133 +2533,6 @@ public abstract partial class OpenIddictServerIntegrationTests
Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()), Times.Once());
}
- [Fact]
- public async Task ValidateTokenRequest_ClientSecretCannotBeUsedByPublicClients()
- {
- // Arrange
- var application = new OpenIddictApplication();
-
- var manager = CreateApplicationManager(mock =>
- {
- mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()))
- .ReturnsAsync(application);
-
- mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()))
- .ReturnsAsync(true);
- });
-
- await using var server = await CreateServerAsync(options =>
- {
- options.Services.AddSingleton(manager);
- });
-
- await using var client = await server.CreateClientAsync();
-
- // Act
- var response = await client.PostAsync("/connect/token", new OpenIddictRequest
- {
- ClientId = "Fabrikam",
- ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw",
- GrantType = GrantTypes.Password,
- Username = "johndoe",
- Password = "A3ddj3w"
- });
-
- // Assert
- Assert.Equal(Errors.InvalidClient, response.Error);
- Assert.Equal(SR.FormatID2053(Parameters.ClientSecret), response.ErrorDescription);
- Assert.Equal(SR.FormatID8000(SR.ID2053), response.ErrorUri);
-
- Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce());
- Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()), Times.Once());
- }
-
- [Fact]
- public async Task ValidateTokenRequest_ClientSecretIsRequiredForNonPublicClients()
- {
- // Arrange
- var application = new OpenIddictApplication();
-
- var manager = CreateApplicationManager(mock =>
- {
- mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()))
- .ReturnsAsync(application);
-
- mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()))
- .ReturnsAsync(false);
- });
-
- await using var server = await CreateServerAsync(options =>
- {
- options.Services.AddSingleton(manager);
- });
-
- await using var client = await server.CreateClientAsync();
-
- // Act
- var response = await client.PostAsync("/connect/token", new OpenIddictRequest
- {
- ClientId = "Fabrikam",
- ClientSecret = null,
- GrantType = GrantTypes.Password,
- Username = "johndoe",
- Password = "A3ddj3w"
- });
-
- // Assert
- Assert.Equal(Errors.InvalidClient, response.Error);
- Assert.Equal(SR.FormatID2054(Parameters.ClientSecret), response.ErrorDescription);
- Assert.Equal(SR.FormatID8000(SR.ID2054), response.ErrorUri);
-
- Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce());
- Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()), Times.Once());
- }
-
- [Fact]
- public async Task ValidateTokenRequest_RequestIsRejectedWhenClientCredentialsAreInvalid()
- {
- // Arrange
- var application = new OpenIddictApplication();
-
- var manager = CreateApplicationManager(mock =>
- {
- mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()))
- .ReturnsAsync(application);
-
- mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()))
- .ReturnsAsync(false);
-
- mock.Setup(manager => manager.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny()))
- .ReturnsAsync(false);
- });
-
- await using var server = await CreateServerAsync(options =>
- {
- options.Services.AddSingleton(manager);
- });
-
- await using var client = await server.CreateClientAsync();
-
- // Act
- var response = await client.PostAsync("/connect/token", new OpenIddictRequest
- {
- ClientId = "Fabrikam",
- ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw",
- GrantType = GrantTypes.Password,
- Username = "johndoe",
- Password = "A3ddj3w"
- });
-
- // Assert
- Assert.Equal(Errors.InvalidClient, response.Error);
- Assert.Equal(SR.GetResourceString(SR.ID2055), response.ErrorDescription);
- Assert.Equal(SR.FormatID8000(SR.ID2055), response.ErrorUri);
-
- Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce());
- Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()), Times.AtLeastOnce());
- Mock.Get(manager).Verify(manager => manager.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny()), Times.Once());
- }
-
[Fact]
public async Task ValidateTokenRequest_RequestIsRejectedWhenEndpointPermissionIsNotGranted()
{
diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Introspection.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Introspection.cs
index ed616186..de8480bf 100644
--- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Introspection.cs
+++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Introspection.cs
@@ -364,124 +364,6 @@ public abstract partial class OpenIddictServerIntegrationTests
Permissions.Endpoints.Introspection, It.IsAny()), Times.Once());
}
- [Fact]
- public async Task ValidateIntrospectionRequest_ClientSecretCannotBeUsedByPublicClients()
- {
- // Arrange
- var application = new OpenIddictApplication();
-
- var manager = CreateApplicationManager(mock =>
- {
- mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()))
- .ReturnsAsync(application);
-
- mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()))
- .ReturnsAsync(true);
- });
-
- await using var server = await CreateServerAsync(options =>
- {
- options.Services.AddSingleton(manager);
- });
-
- await using var client = await server.CreateClientAsync();
-
- // Act
- var response = await client.PostAsync("/connect/introspect", new OpenIddictRequest
- {
- ClientId = "Fabrikam",
- ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw",
- Token = "2YotnFZFEjr1zCsicMWpAA"
- });
-
- // Assert
- Assert.Equal(Errors.InvalidClient, response.Error);
- Assert.Equal(SR.FormatID2053(Parameters.ClientSecret), response.ErrorDescription);
- Assert.Equal(SR.FormatID8000(SR.ID2053), response.ErrorUri);
-
- Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce());
- Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()), Times.Once());
- }
-
- [Fact]
- public async Task ValidateIntrospectionRequest_ClientSecretIsRequiredForNonPublicClients()
- {
- // Arrange
- var application = new OpenIddictApplication();
-
- var manager = CreateApplicationManager(mock =>
- {
- mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()))
- .ReturnsAsync(application);
-
- mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()))
- .ReturnsAsync(false);
- });
-
- await using var server = await CreateServerAsync(options =>
- {
- options.Services.AddSingleton(manager);
- });
-
- await using var client = await server.CreateClientAsync();
-
- // Act
- var response = await client.PostAsync("/connect/introspect", new OpenIddictRequest
- {
- ClientId = "Fabrikam",
- ClientSecret = null,
- Token = "2YotnFZFEjr1zCsicMWpAA"
- });
-
- // Assert
- Assert.Equal(Errors.InvalidClient, response.Error);
- Assert.Equal(SR.FormatID2054(Parameters.ClientSecret), response.ErrorDescription);
- Assert.Equal(SR.FormatID8000(SR.ID2054), response.ErrorUri);
-
- Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce());
- Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()), Times.Once());
- }
-
- [Fact]
- public async Task ValidateIntrospectionRequest_RequestIsRejectedWhenClientCredentialsAreInvalid()
- {
- // Arrange
- var application = new OpenIddictApplication();
-
- var manager = CreateApplicationManager(mock =>
- {
- mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()))
- .ReturnsAsync(application);
-
- mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()))
- .ReturnsAsync(false);
-
- mock.Setup(manager => manager.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny()))
- .ReturnsAsync(false);
- });
-
- await using var server = await CreateServerAsync(options =>
- {
- options.Services.AddSingleton(manager);
- });
-
- await using var client = await server.CreateClientAsync();
-
- // Act
- var response = await client.PostAsync("/connect/introspect", new OpenIddictRequest
- {
- ClientId = "Fabrikam",
- ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw",
- Token = "2YotnFZFEjr1zCsicMWpAA"
- });
-
- // Assert
-
- Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce());
- Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()), Times.AtLeastOnce());
- Mock.Get(manager).Verify(manager => manager.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny()), Times.Once());
- }
-
[Fact]
public async Task ValidateIntrospectionRequest_InvalidTokenCausesAnError()
{
diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Revocation.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Revocation.cs
index 02934f9a..8dce3ff0 100644
--- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Revocation.cs
+++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Revocation.cs
@@ -365,130 +365,6 @@ public abstract partial class OpenIddictServerIntegrationTests
Permissions.Endpoints.Revocation, It.IsAny()), Times.Once());
}
- [Fact]
- public async Task ValidateRevocationRequest_ClientSecretCannotBeUsedByPublicClients()
- {
- // Arrange
- var application = new OpenIddictApplication();
-
- var manager = CreateApplicationManager(mock =>
- {
- mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()))
- .ReturnsAsync(application);
-
- mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()))
- .ReturnsAsync(true);
- });
-
- await using var server = await CreateServerAsync(options =>
- {
- options.Services.AddSingleton(manager);
- });
-
- await using var client = await server.CreateClientAsync();
-
- // Act
- var response = await client.PostAsync("/connect/revoke", new OpenIddictRequest
- {
- ClientId = "Fabrikam",
- ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw",
- Token = "SlAV32hkKG",
- TokenTypeHint = TokenTypeHints.RefreshToken
- });
-
- // Assert
- Assert.Equal(Errors.InvalidClient, response.Error);
- Assert.Equal(SR.FormatID2053(Parameters.ClientSecret), response.ErrorDescription);
- Assert.Equal(SR.FormatID8000(SR.ID2053), response.ErrorUri);
-
- Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce());
- Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()), Times.Once());
- }
-
- [Fact]
- public async Task ValidateRevocationRequest_ClientSecretIsRequiredForNonPublicClients()
- {
- // Arrange
- var application = new OpenIddictApplication();
-
- var manager = CreateApplicationManager(mock =>
- {
- mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()))
- .ReturnsAsync(application);
-
- mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()))
- .ReturnsAsync(false);
- });
-
- await using var server = await CreateServerAsync(options =>
- {
- options.Services.AddSingleton(manager);
- });
-
- await using var client = await server.CreateClientAsync();
-
- // Act
- var response = await client.PostAsync("/connect/revoke", new OpenIddictRequest
- {
- ClientId = "Fabrikam",
- ClientSecret = null,
- Token = "SlAV32hkKG",
- TokenTypeHint = TokenTypeHints.RefreshToken
- });
-
- // Assert
- Assert.Equal(Errors.InvalidClient, response.Error);
- Assert.Equal(SR.FormatID2054(Parameters.ClientSecret), response.ErrorDescription);
- Assert.Equal(SR.FormatID8000(SR.ID2054), response.ErrorUri);
-
- Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce());
- Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()), Times.Once());
- }
-
- [Fact]
- public async Task ValidateRevocationRequest_RequestIsRejectedWhenClientCredentialsAreInvalid()
- {
- // Arrange
- var application = new OpenIddictApplication();
-
- var manager = CreateApplicationManager(mock =>
- {
- mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()))
- .ReturnsAsync(application);
-
- mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()))
- .ReturnsAsync(false);
-
- mock.Setup(manager => manager.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny()))
- .ReturnsAsync(false);
- });
-
- await using var server = await CreateServerAsync(builder =>
- {
- builder.Services.AddSingleton(manager);
- });
-
- await using var client = await server.CreateClientAsync();
-
- // Act
- var response = await client.PostAsync("/connect/revoke", new OpenIddictRequest
- {
- ClientId = "Fabrikam",
- ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw",
- Token = "SlAV32hkKG",
- TokenTypeHint = TokenTypeHints.RefreshToken
- });
-
- // Assert
- Assert.Equal(Errors.InvalidClient, response.Error);
- Assert.Equal(SR.GetResourceString(SR.ID2055), response.ErrorDescription);
- Assert.Equal(SR.FormatID8000(SR.ID2055), response.ErrorUri);
-
- Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce());
- Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()), Times.AtLeastOnce());
- Mock.Get(manager).Verify(manager => manager.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny()), Times.Once());
- }
-
[Theory]
[InlineData(TokenTypeIdentifiers.Private.AuthorizationCode)]
[InlineData(TokenTypeIdentifiers.Private.DeviceCode)]
diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs
index b8484798..b6ef0a3e 100644
--- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs
+++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs
@@ -9,6 +9,7 @@
using System.Collections.Immutable;
using System.Globalization;
using System.Security.Claims;
+using System.Security.Cryptography.X509Certificates;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.Extensions.DependencyInjection;
@@ -836,6 +837,1167 @@ public abstract partial class OpenIddictServerIntegrationTests
Assert.Equal("Bob le Magnifique", (string?) response[Claims.Subject]);
}
+ [Theory]
+ [InlineData(OpenIddictServerEndpointType.DeviceAuthorization)]
+ [InlineData(OpenIddictServerEndpointType.Introspection)]
+ [InlineData(OpenIddictServerEndpointType.PushedAuthorization)]
+ [InlineData(OpenIddictServerEndpointType.Revocation)]
+ [InlineData(OpenIddictServerEndpointType.Token)]
+ public async Task ProcessAuthentication_ClientSecretCannotBeUsedByPublicClients(OpenIddictServerEndpointType type)
+ {
+ // Arrange
+ var application = new OpenIddictApplication();
+
+ var manager = CreateApplicationManager(mock =>
+ {
+ mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()))
+ .ReturnsAsync(application);
+
+ mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()))
+ .ReturnsAsync(true);
+ });
+
+ await using var server = await CreateServerAsync(options =>
+ {
+ options.Services.AddSingleton(manager);
+ });
+
+ await using var client = await server.CreateClientAsync();
+
+ // Act
+ var response = type switch
+ {
+ OpenIddictServerEndpointType.Introspection => await client.PostAsync("/connect/introspect", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam",
+ ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw",
+ Token = "2YotnFZFEjr1zCsicMWpAA"
+ }),
+
+ OpenIddictServerEndpointType.DeviceAuthorization => await client.PostAsync("/connect/device", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam",
+ ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw"
+ }),
+
+ OpenIddictServerEndpointType.PushedAuthorization => await client.PostAsync("/connect/par", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam",
+ ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw",
+ RedirectUri = "http://www.fabrikam.com/path",
+ ResponseType = ResponseTypes.Code
+ }),
+
+ OpenIddictServerEndpointType.Revocation => await client.PostAsync("/connect/revoke", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam",
+ ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw",
+ Token = "SlAV32hkKG",
+ TokenTypeHint = TokenTypeHints.RefreshToken
+ }),
+
+ OpenIddictServerEndpointType.Token => await client.PostAsync("/connect/token", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam",
+ ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw",
+ GrantType = GrantTypes.Password,
+ Username = "johndoe",
+ Password = "A3ddj3w"
+ }),
+
+ _ => throw new NotSupportedException()
+ };
+
+ // Assert
+ Assert.Equal(Errors.InvalidClient, response.Error);
+ Assert.Equal(SR.FormatID2053(Parameters.ClientSecret), response.ErrorDescription);
+ Assert.Equal(SR.FormatID8000(SR.ID2053), response.ErrorUri);
+
+ Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce());
+ Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()), Times.Once());
+ }
+
+ [Theory]
+ [InlineData(OpenIddictServerEndpointType.DeviceAuthorization)]
+ [InlineData(OpenIddictServerEndpointType.Introspection)]
+ [InlineData(OpenIddictServerEndpointType.PushedAuthorization)]
+ [InlineData(OpenIddictServerEndpointType.Revocation)]
+ [InlineData(OpenIddictServerEndpointType.Token)]
+ public async Task ProcessAuthentication_ClientAssertionCannotBeUsedByPublicClients(OpenIddictServerEndpointType type)
+ {
+ // Arrange
+ var application = new OpenIddictApplication();
+
+ var manager = CreateApplicationManager(mock =>
+ {
+ mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()))
+ .ReturnsAsync(application);
+
+ mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()))
+ .ReturnsAsync(true);
+ });
+
+ await using var server = await CreateServerAsync(options =>
+ {
+ options.AddEventHandler(builder =>
+ {
+ builder.UseInlineHandler(context =>
+ {
+ Assert.Equal("2YotnFZFEjr1zCsicMWpAA", context.Token);
+
+ context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer"))
+ .SetTokenType(TokenTypeIdentifiers.Private.ClientAssertion)
+ .SetClaim(Claims.Issuer, "Fabrikam")
+ .SetClaim(Claims.Subject, "Fabrikam")
+ .SetClaim(Claims.Audience, "http://localhost/")
+ .SetClaim(Claims.ExpiresAt, 4099766400);
+
+ return ValueTask.CompletedTask;
+ });
+
+ builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500);
+ });
+
+ options.Services.AddSingleton(manager);
+ });
+
+ await using var client = await server.CreateClientAsync();
+
+ // Act
+ var response = type switch
+ {
+ OpenIddictServerEndpointType.Introspection => await client.PostAsync("/connect/introspect", new OpenIddictRequest
+ {
+ ClientAssertion = "2YotnFZFEjr1zCsicMWpAA",
+ ClientAssertionType = ClientAssertionTypes.JwtBearer,
+ ClientId = "Fabrikam",
+ Token = "2YotnFZFEjr1zCsicMWpAA"
+ }),
+
+ OpenIddictServerEndpointType.DeviceAuthorization => await client.PostAsync("/connect/device", new OpenIddictRequest
+ {
+ ClientAssertion = "2YotnFZFEjr1zCsicMWpAA",
+ ClientAssertionType = ClientAssertionTypes.JwtBearer,
+ ClientId = "Fabrikam"
+ }),
+
+ OpenIddictServerEndpointType.PushedAuthorization => await client.PostAsync("/connect/par", new OpenIddictRequest
+ {
+ ClientAssertion = "2YotnFZFEjr1zCsicMWpAA",
+ ClientAssertionType = ClientAssertionTypes.JwtBearer,
+ ClientId = "Fabrikam",
+ RedirectUri = "http://www.fabrikam.com/path",
+ ResponseType = ResponseTypes.Code
+ }),
+
+ OpenIddictServerEndpointType.Revocation => await client.PostAsync("/connect/revoke", new OpenIddictRequest
+ {
+ ClientAssertion = "2YotnFZFEjr1zCsicMWpAA",
+ ClientAssertionType = ClientAssertionTypes.JwtBearer,
+ ClientId = "Fabrikam",
+ Token = "SlAV32hkKG",
+ TokenTypeHint = TokenTypeHints.RefreshToken
+ }),
+
+ OpenIddictServerEndpointType.Token => await client.PostAsync("/connect/token", new OpenIddictRequest
+ {
+ ClientAssertion = "2YotnFZFEjr1zCsicMWpAA",
+ ClientAssertionType = ClientAssertionTypes.JwtBearer,
+ ClientId = "Fabrikam",
+ GrantType = GrantTypes.Password,
+ Username = "johndoe",
+ Password = "A3ddj3w"
+ }),
+
+ _ => throw new NotSupportedException()
+ };
+
+ // Assert
+ Assert.Equal(Errors.InvalidClient, response.Error);
+ Assert.Equal(SR.FormatID2053(Parameters.ClientAssertion), response.ErrorDescription);
+ Assert.Equal(SR.FormatID8000(SR.ID2053), response.ErrorUri);
+
+ Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce());
+ Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()), Times.Once());
+ }
+
+#if SUPPORTS_X509_CHAIN_POLICY_CUSTOM_TRUST_STORE
+ [Theory]
+ [InlineData(OpenIddictServerEndpointType.DeviceAuthorization)]
+ [InlineData(OpenIddictServerEndpointType.Introspection)]
+ [InlineData(OpenIddictServerEndpointType.PushedAuthorization)]
+ [InlineData(OpenIddictServerEndpointType.Revocation)]
+ [InlineData(OpenIddictServerEndpointType.Token)]
+ public async Task ProcessAuthentication_ClientCertificateCannotBeUsedByPublicClients(OpenIddictServerEndpointType type)
+ {
+ // Arrange
+ var application = new OpenIddictApplication();
+
+ var manager = CreateApplicationManager(mock =>
+ {
+ mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()))
+ .ReturnsAsync(application);
+
+ mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()))
+ .ReturnsAsync(true);
+ });
+
+ await using var server = await CreateServerAsync(options =>
+ {
+ options.AddEventHandler(builder =>
+ {
+ builder.UseInlineHandler(context =>
+ {
+ context.ClientCertificate = X509Certificate2.CreateFromPem($"""
+ -----BEGIN CERTIFICATE-----
+ MIIC8jCCAdqgAwIBAgIIYfcknj8KXN0wDQYJKoZIhvcNAQELBQAwIjEgMB4GA1UE
+ AxMXU2VsZi1zaWduZWQgY2VydGlmaWNhdGUwIBcNMjYwMjAxMTc1MjI2WhgPMjEy
+ NjAyMDExNzUyMjZaMCIxIDAeBgNVBAMTF1NlbGYtc2lnbmVkIGNlcnRpZmljYXRl
+ MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA45uKd5cdlLmEBGLDEB75
+ o9e3/kMmQjVhhMeBsy4m2t7zw5jZo7OPcahXiXZHttom9tJm7BWPWvYx7p0N9+ss
+ h/E5lzKyV7ZXg+mM+KeECtVhiy+82BuIPelCshrpaV3lIg93y47FYLIWXxdggjt6
+ 6VUaxzlTeo+IpuMz8IssL7VpJnjCT5NmqPNVkv1VR1uuetVqP7546ZFw31RiGl/0
+ I1uUlb7SwLwhLUK1iyLmGNA3VDB0m0DvLmlIEY3ZE5zxQp/Rxq6DfjbXm2LWJyu6
+ NO7k7JixXOorEl+6HdJZHTWNFK5jCo2ZZAwWn+uUuzgmLILPJFLDVutXjuEZpsym
+ uQIDAQABoyowKDAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0lAQH/BAwwCgYIKwYBBQUH
+ AwIwDQYJKoZIhvcNAQELBQADggEBAGHH3f/bkfViTvPE7yXJkB0bs88mYxajluMA
+ hgihEN5joPT6zHxMLBND2sitIozCMeeaj0rg+OaT/zDBgOLup/BM92UaPpYcgDCy
+ 3tHqZLOOJOR4aYnHhIQUnx+NRtKEM4q/hL/xLHeliKmV7TQXISEZlTbb0gOU7TFp
+ nJlP60Vo9F/WD6xcKNxBgV5aB/+2FjiTTw2pF0VUmvcZdQAN5ysfrmKNXbvv1oCp
+ AohiwRiPrwe3mJ8iCqzEY/qQqImEiIT8WC2Fty+UYyBwfXMObi1AO++QkaMUbUJl
+ 0aBuRoD85FLotjHIHXkFHERjOolheYdKt5nrGCCz/PmXBfsSCTo=
+ -----END CERTIFICATE-----
+ """);
+
+ return ValueTask.CompletedTask;
+ });
+
+ builder.SetOrder(ValidateClientType.Descriptor.Order - 500);
+ });
+
+ options.Services.AddSingleton(manager);
+ });
+
+ await using var client = await server.CreateClientAsync();
+
+ // Act
+ var response = type switch
+ {
+ OpenIddictServerEndpointType.Introspection => await client.PostAsync("/connect/introspect", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam",
+ Token = "2YotnFZFEjr1zCsicMWpAA"
+ }),
+
+ OpenIddictServerEndpointType.DeviceAuthorization => await client.PostAsync("/connect/device", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam"
+ }),
+
+ OpenIddictServerEndpointType.PushedAuthorization => await client.PostAsync("/connect/par", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam",
+ RedirectUri = "http://www.fabrikam.com/path",
+ ResponseType = ResponseTypes.Code
+ }),
+
+ OpenIddictServerEndpointType.Revocation => await client.PostAsync("/connect/revoke", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam",
+ Token = "SlAV32hkKG",
+ TokenTypeHint = TokenTypeHints.RefreshToken
+ }),
+
+ OpenIddictServerEndpointType.Token => await client.PostAsync("/connect/token", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam",
+ GrantType = GrantTypes.Password,
+ Username = "johndoe",
+ Password = "A3ddj3w"
+ }),
+
+ _ => throw new NotSupportedException()
+ };
+
+ // Assert
+ Assert.Equal(Errors.InvalidClient, response.Error);
+ Assert.Equal(SR.GetResourceString(SR.ID2196), response.ErrorDescription);
+ Assert.Equal(SR.FormatID8000(SR.ID2196), response.ErrorUri);
+
+ Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce());
+ Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()), Times.Once());
+ }
+#endif
+
+ [Theory]
+ [InlineData(OpenIddictServerEndpointType.DeviceAuthorization)]
+ [InlineData(OpenIddictServerEndpointType.Introspection)]
+ [InlineData(OpenIddictServerEndpointType.PushedAuthorization)]
+ [InlineData(OpenIddictServerEndpointType.Revocation)]
+ [InlineData(OpenIddictServerEndpointType.Token)]
+ public async Task ProcessAuthentication_ClientAuthenticationIsRequiredForNonPublicClients(OpenIddictServerEndpointType type)
+ {
+ // Arrange
+ var application = new OpenIddictApplication();
+
+ var manager = CreateApplicationManager(mock =>
+ {
+ mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()))
+ .ReturnsAsync(application);
+
+ mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()))
+ .ReturnsAsync(false);
+ });
+
+ await using var server = await CreateServerAsync(options =>
+ {
+ options.Services.AddSingleton(manager);
+ });
+
+ await using var client = await server.CreateClientAsync();
+
+ // Act
+ var response = type switch
+ {
+ OpenIddictServerEndpointType.Introspection => await client.PostAsync("/connect/introspect", new OpenIddictRequest
+ {
+ ClientAssertion = null,
+ ClientAssertionType = null,
+ ClientId = "Fabrikam",
+ ClientSecret = null,
+ Token = "2YotnFZFEjr1zCsicMWpAA"
+ }),
+
+ OpenIddictServerEndpointType.DeviceAuthorization => await client.PostAsync("/connect/device", new OpenIddictRequest
+ {
+ ClientAssertion = null,
+ ClientAssertionType = null,
+ ClientId = "Fabrikam",
+ ClientSecret = null
+ }),
+
+ OpenIddictServerEndpointType.PushedAuthorization => await client.PostAsync("/connect/par", new OpenIddictRequest
+ {
+ ClientAssertion = null,
+ ClientAssertionType = null,
+ ClientId = "Fabrikam",
+ ClientSecret = null,
+ RedirectUri = "http://www.fabrikam.com/path",
+ ResponseType = ResponseTypes.Code
+ }),
+
+ OpenIddictServerEndpointType.Revocation => await client.PostAsync("/connect/revoke", new OpenIddictRequest
+ {
+ ClientAssertion = null,
+ ClientAssertionType = null,
+ ClientId = "Fabrikam",
+ ClientSecret = null,
+ Token = "SlAV32hkKG",
+ TokenTypeHint = TokenTypeHints.RefreshToken
+ }),
+
+ OpenIddictServerEndpointType.Token => await client.PostAsync("/connect/token", new OpenIddictRequest
+ {
+ ClientAssertion = null,
+ ClientAssertionType = null,
+ ClientId = "Fabrikam",
+ ClientSecret = null,
+ GrantType = GrantTypes.Password,
+ Username = "johndoe",
+ Password = "A3ddj3w"
+ }),
+
+ _ => throw new NotSupportedException()
+ };
+
+ // Assert
+ Assert.Equal(Errors.InvalidClient, response.Error);
+ Assert.Equal(SR.GetResourceString(SR.ID2198), response.ErrorDescription);
+ Assert.Equal(SR.FormatID8000(SR.ID2198), response.ErrorUri);
+
+ Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce());
+ Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()), Times.Once());
+ }
+
+ [Theory]
+ [InlineData(OpenIddictServerEndpointType.Introspection)]
+ [InlineData(OpenIddictServerEndpointType.PushedAuthorization)]
+ [InlineData(OpenIddictServerEndpointType.Revocation)]
+ [InlineData(OpenIddictServerEndpointType.Token)]
+ public async Task ProcessAuthentication_RequestIsRejectedWhenClientSecretIsInvalid(OpenIddictServerEndpointType type)
+ {
+ // Arrange
+ var application = new OpenIddictApplication();
+
+ var manager = CreateApplicationManager(mock =>
+ {
+ mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()))
+ .ReturnsAsync(application);
+
+ mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()))
+ .ReturnsAsync(false);
+
+ mock.Setup(manager => manager.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny()))
+ .ReturnsAsync(false);
+ });
+
+ await using var server = await CreateServerAsync(options =>
+ {
+ options.Services.AddSingleton(manager);
+ });
+
+ await using var client = await server.CreateClientAsync();
+
+ // Act
+ var response = type switch
+ {
+ OpenIddictServerEndpointType.Introspection => await client.PostAsync("/connect/introspect", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam",
+ ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw",
+ Token = "2YotnFZFEjr1zCsicMWpAA"
+ }),
+
+ OpenIddictServerEndpointType.PushedAuthorization => await client.PostAsync("/connect/par", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam",
+ ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw",
+ RedirectUri = "http://www.fabrikam.com/path",
+ ResponseType = ResponseTypes.Code
+ }),
+
+ OpenIddictServerEndpointType.Revocation => await client.PostAsync("/connect/revoke", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam",
+ ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw",
+ Token = "SlAV32hkKG",
+ TokenTypeHint = TokenTypeHints.RefreshToken
+ }),
+
+ OpenIddictServerEndpointType.Token => await client.PostAsync("/connect/token", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam",
+ ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw",
+ GrantType = GrantTypes.Password,
+ Username = "johndoe",
+ Password = "A3ddj3w"
+ }),
+
+ _ => throw new NotSupportedException()
+ };
+
+ // Assert
+ Assert.Equal(Errors.InvalidClient, response.Error);
+ Assert.Equal(SR.GetResourceString(SR.ID2055), response.ErrorDescription);
+ Assert.Equal(SR.FormatID8000(SR.ID2055), response.ErrorUri);
+
+ Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce());
+ Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()), Times.AtLeastOnce());
+ Mock.Get(manager).Verify(manager => manager.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny()), Times.Once());
+ }
+
+#if SUPPORTS_X509_CHAIN_POLICY_CUSTOM_TRUST_STORE
+ [Theory]
+ [InlineData(OpenIddictServerEndpointType.DeviceAuthorization)]
+ [InlineData(OpenIddictServerEndpointType.Introspection)]
+ [InlineData(OpenIddictServerEndpointType.PushedAuthorization)]
+ [InlineData(OpenIddictServerEndpointType.Revocation)]
+ [InlineData(OpenIddictServerEndpointType.Token)]
+ public async Task ProcessAuthentication_ThrowsAnExceptionWhenGlobalSelfSignedChainPolicyIsNull(OpenIddictServerEndpointType type)
+ {
+ // Arrange
+ var application = new OpenIddictApplication();
+
+ var manager = CreateApplicationManager(mock =>
+ {
+ mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()))
+ .ReturnsAsync(application);
+
+ mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()))
+ .ReturnsAsync(false);
+ });
+
+ await using var server = await CreateServerAsync(options =>
+ {
+ options.AddEventHandler(builder =>
+ {
+ builder.UseInlineHandler(context =>
+ {
+ context.ClientCertificate = X509Certificate2.CreateFromPem($"""
+ -----BEGIN CERTIFICATE-----
+ MIIC8jCCAdqgAwIBAgIIYfcknj8KXN0wDQYJKoZIhvcNAQELBQAwIjEgMB4GA1UE
+ AxMXU2VsZi1zaWduZWQgY2VydGlmaWNhdGUwIBcNMjYwMjAxMTc1MjI2WhgPMjEy
+ NjAyMDExNzUyMjZaMCIxIDAeBgNVBAMTF1NlbGYtc2lnbmVkIGNlcnRpZmljYXRl
+ MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA45uKd5cdlLmEBGLDEB75
+ o9e3/kMmQjVhhMeBsy4m2t7zw5jZo7OPcahXiXZHttom9tJm7BWPWvYx7p0N9+ss
+ h/E5lzKyV7ZXg+mM+KeECtVhiy+82BuIPelCshrpaV3lIg93y47FYLIWXxdggjt6
+ 6VUaxzlTeo+IpuMz8IssL7VpJnjCT5NmqPNVkv1VR1uuetVqP7546ZFw31RiGl/0
+ I1uUlb7SwLwhLUK1iyLmGNA3VDB0m0DvLmlIEY3ZE5zxQp/Rxq6DfjbXm2LWJyu6
+ NO7k7JixXOorEl+6HdJZHTWNFK5jCo2ZZAwWn+uUuzgmLILPJFLDVutXjuEZpsym
+ uQIDAQABoyowKDAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0lAQH/BAwwCgYIKwYBBQUH
+ AwIwDQYJKoZIhvcNAQELBQADggEBAGHH3f/bkfViTvPE7yXJkB0bs88mYxajluMA
+ hgihEN5joPT6zHxMLBND2sitIozCMeeaj0rg+OaT/zDBgOLup/BM92UaPpYcgDCy
+ 3tHqZLOOJOR4aYnHhIQUnx+NRtKEM4q/hL/xLHeliKmV7TQXISEZlTbb0gOU7TFp
+ nJlP60Vo9F/WD6xcKNxBgV5aB/+2FjiTTw2pF0VUmvcZdQAN5ysfrmKNXbvv1oCp
+ AohiwRiPrwe3mJ8iCqzEY/qQqImEiIT8WC2Fty+UYyBwfXMObi1AO++QkaMUbUJl
+ 0aBuRoD85FLotjHIHXkFHERjOolheYdKt5nrGCCz/PmXBfsSCTo=
+ -----END CERTIFICATE-----
+ """);
+
+ return ValueTask.CompletedTask;
+ });
+
+ builder.SetOrder(ValidateClientType.Descriptor.Order - 500);
+ });
+
+ options.Services.AddSingleton(manager);
+ });
+
+ await using var client = await server.CreateClientAsync();
+
+ // Act and assert
+ var exception = type switch
+ {
+ OpenIddictServerEndpointType.Introspection =>
+ await Assert.ThrowsAsync(() => client.PostAsync("/connect/introspect", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam",
+ Token = "2YotnFZFEjr1zCsicMWpAA"
+ })),
+
+ OpenIddictServerEndpointType.DeviceAuthorization =>
+ await Assert.ThrowsAsync(() => client.PostAsync("/connect/device", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam"
+ })),
+
+ OpenIddictServerEndpointType.PushedAuthorization =>
+ await Assert.ThrowsAsync(() => client.PostAsync("/connect/par", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam",
+ RedirectUri = "http://www.fabrikam.com/path",
+ ResponseType = ResponseTypes.Code
+ })),
+
+ OpenIddictServerEndpointType.Revocation =>
+ await Assert.ThrowsAsync(() => client.PostAsync("/connect/revoke", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam",
+ Token = "SlAV32hkKG",
+ TokenTypeHint = TokenTypeHints.RefreshToken
+ })),
+
+ OpenIddictServerEndpointType.Token =>
+ await Assert.ThrowsAsync(() => client.PostAsync("/connect/token", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam",
+ GrantType = GrantTypes.Password,
+ Username = "johndoe",
+ Password = "A3ddj3w"
+ })),
+
+ _ => throw new NotSupportedException()
+ };
+
+ Assert.Equal(SR.GetResourceString(SR.ID0506), exception.Message);
+
+ Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce());
+ Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()), Times.AtLeastOnce());
+ }
+
+ [Theory]
+ [InlineData(OpenIddictServerEndpointType.DeviceAuthorization)]
+ [InlineData(OpenIddictServerEndpointType.Introspection)]
+ [InlineData(OpenIddictServerEndpointType.PushedAuthorization)]
+ [InlineData(OpenIddictServerEndpointType.Revocation)]
+ [InlineData(OpenIddictServerEndpointType.Token)]
+ public async Task ProcessAuthentication_SelfSignedClientCertificateIsRejectedWhenChainPolicyIsNull(OpenIddictServerEndpointType type)
+ {
+ // Arrange
+ var application = new OpenIddictApplication();
+
+ var manager = CreateApplicationManager(mock =>
+ {
+ mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()))
+ .ReturnsAsync(application);
+
+ mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()))
+ .ReturnsAsync(false);
+
+ mock.Setup(manager => manager.GetSelfSignedClientCertificateChainPolicyAsync(application, It.IsAny(), It.IsAny()))
+ .ReturnsAsync(value: null);
+ });
+
+ await using var server = await CreateServerAsync(options =>
+ {
+ options.Configure(options => options.SelfSignedClientCertificateChainPolicy = new X509ChainPolicy());
+
+ options.AddEventHandler(builder =>
+ {
+ builder.UseInlineHandler(context =>
+ {
+ context.ClientCertificate = X509Certificate2.CreateFromPem($"""
+ -----BEGIN CERTIFICATE-----
+ MIIC8jCCAdqgAwIBAgIIYfcknj8KXN0wDQYJKoZIhvcNAQELBQAwIjEgMB4GA1UE
+ AxMXU2VsZi1zaWduZWQgY2VydGlmaWNhdGUwIBcNMjYwMjAxMTc1MjI2WhgPMjEy
+ NjAyMDExNzUyMjZaMCIxIDAeBgNVBAMTF1NlbGYtc2lnbmVkIGNlcnRpZmljYXRl
+ MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA45uKd5cdlLmEBGLDEB75
+ o9e3/kMmQjVhhMeBsy4m2t7zw5jZo7OPcahXiXZHttom9tJm7BWPWvYx7p0N9+ss
+ h/E5lzKyV7ZXg+mM+KeECtVhiy+82BuIPelCshrpaV3lIg93y47FYLIWXxdggjt6
+ 6VUaxzlTeo+IpuMz8IssL7VpJnjCT5NmqPNVkv1VR1uuetVqP7546ZFw31RiGl/0
+ I1uUlb7SwLwhLUK1iyLmGNA3VDB0m0DvLmlIEY3ZE5zxQp/Rxq6DfjbXm2LWJyu6
+ NO7k7JixXOorEl+6HdJZHTWNFK5jCo2ZZAwWn+uUuzgmLILPJFLDVutXjuEZpsym
+ uQIDAQABoyowKDAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0lAQH/BAwwCgYIKwYBBQUH
+ AwIwDQYJKoZIhvcNAQELBQADggEBAGHH3f/bkfViTvPE7yXJkB0bs88mYxajluMA
+ hgihEN5joPT6zHxMLBND2sitIozCMeeaj0rg+OaT/zDBgOLup/BM92UaPpYcgDCy
+ 3tHqZLOOJOR4aYnHhIQUnx+NRtKEM4q/hL/xLHeliKmV7TQXISEZlTbb0gOU7TFp
+ nJlP60Vo9F/WD6xcKNxBgV5aB/+2FjiTTw2pF0VUmvcZdQAN5ysfrmKNXbvv1oCp
+ AohiwRiPrwe3mJ8iCqzEY/qQqImEiIT8WC2Fty+UYyBwfXMObi1AO++QkaMUbUJl
+ 0aBuRoD85FLotjHIHXkFHERjOolheYdKt5nrGCCz/PmXBfsSCTo=
+ -----END CERTIFICATE-----
+ """);
+
+ return ValueTask.CompletedTask;
+ });
+
+ builder.SetOrder(ValidateClientType.Descriptor.Order - 500);
+ });
+
+ options.Services.AddSingleton(manager);
+ });
+
+ await using var client = await server.CreateClientAsync();
+
+ // Act
+ var response = type switch
+ {
+ OpenIddictServerEndpointType.Introspection => await client.PostAsync("/connect/introspect", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam",
+ Token = "2YotnFZFEjr1zCsicMWpAA"
+ }),
+
+ OpenIddictServerEndpointType.DeviceAuthorization => await client.PostAsync("/connect/device", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam"
+ }),
+
+ OpenIddictServerEndpointType.PushedAuthorization => await client.PostAsync("/connect/par", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam",
+ RedirectUri = "http://www.fabrikam.com/path",
+ ResponseType = ResponseTypes.Code
+ }),
+
+ OpenIddictServerEndpointType.Revocation => await client.PostAsync("/connect/revoke", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam",
+ Token = "SlAV32hkKG",
+ TokenTypeHint = TokenTypeHints.RefreshToken
+ }),
+
+ OpenIddictServerEndpointType.Token => await client.PostAsync("/connect/token", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam",
+ GrantType = GrantTypes.Password,
+ Username = "johndoe",
+ Password = "A3ddj3w"
+ }),
+
+ _ => throw new NotSupportedException()
+ };
+
+ // Assert
+ Assert.Equal(Errors.InvalidClient, response.Error);
+ Assert.Equal(SR.GetResourceString(SR.ID2197), response.ErrorDescription);
+ Assert.Equal(SR.FormatID8000(SR.ID2197), response.ErrorUri);
+
+ Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce());
+ Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()), Times.AtLeastOnce());
+ Mock.Get(manager).Verify(manager => manager.GetSelfSignedClientCertificateChainPolicyAsync(application, It.IsAny(), It.IsAny()), Times.Once());
+ }
+
+ [Theory]
+ [InlineData(OpenIddictServerEndpointType.DeviceAuthorization)]
+ [InlineData(OpenIddictServerEndpointType.Introspection)]
+ [InlineData(OpenIddictServerEndpointType.PushedAuthorization)]
+ [InlineData(OpenIddictServerEndpointType.Revocation)]
+ [InlineData(OpenIddictServerEndpointType.Token)]
+ public async Task ProcessAuthentication_SelfSignedClientCertificateIsRejectedWhenInvalid(OpenIddictServerEndpointType type)
+ {
+ // Arrange
+ var application = new OpenIddictApplication();
+ var policy = new X509ChainPolicy();
+ var certificate = X509Certificate2.CreateFromPem($"""
+ -----BEGIN CERTIFICATE-----
+ MIIC8jCCAdqgAwIBAgIIYfcknj8KXN0wDQYJKoZIhvcNAQELBQAwIjEgMB4GA1UE
+ AxMXU2VsZi1zaWduZWQgY2VydGlmaWNhdGUwIBcNMjYwMjAxMTc1MjI2WhgPMjEy
+ NjAyMDExNzUyMjZaMCIxIDAeBgNVBAMTF1NlbGYtc2lnbmVkIGNlcnRpZmljYXRl
+ MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA45uKd5cdlLmEBGLDEB75
+ o9e3/kMmQjVhhMeBsy4m2t7zw5jZo7OPcahXiXZHttom9tJm7BWPWvYx7p0N9+ss
+ h/E5lzKyV7ZXg+mM+KeECtVhiy+82BuIPelCshrpaV3lIg93y47FYLIWXxdggjt6
+ 6VUaxzlTeo+IpuMz8IssL7VpJnjCT5NmqPNVkv1VR1uuetVqP7546ZFw31RiGl/0
+ I1uUlb7SwLwhLUK1iyLmGNA3VDB0m0DvLmlIEY3ZE5zxQp/Rxq6DfjbXm2LWJyu6
+ NO7k7JixXOorEl+6HdJZHTWNFK5jCo2ZZAwWn+uUuzgmLILPJFLDVutXjuEZpsym
+ uQIDAQABoyowKDAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0lAQH/BAwwCgYIKwYBBQUH
+ AwIwDQYJKoZIhvcNAQELBQADggEBAGHH3f/bkfViTvPE7yXJkB0bs88mYxajluMA
+ hgihEN5joPT6zHxMLBND2sitIozCMeeaj0rg+OaT/zDBgOLup/BM92UaPpYcgDCy
+ 3tHqZLOOJOR4aYnHhIQUnx+NRtKEM4q/hL/xLHeliKmV7TQXISEZlTbb0gOU7TFp
+ nJlP60Vo9F/WD6xcKNxBgV5aB/+2FjiTTw2pF0VUmvcZdQAN5ysfrmKNXbvv1oCp
+ AohiwRiPrwe3mJ8iCqzEY/qQqImEiIT8WC2Fty+UYyBwfXMObi1AO++QkaMUbUJl
+ 0aBuRoD85FLotjHIHXkFHERjOolheYdKt5nrGCCz/PmXBfsSCTo=
+ -----END CERTIFICATE-----
+ """);
+
+ var manager = CreateApplicationManager(mock =>
+ {
+ mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()))
+ .ReturnsAsync(application);
+
+ mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()))
+ .ReturnsAsync(false);
+
+ mock.Setup(manager => manager.GetSelfSignedClientCertificateChainPolicyAsync(application, It.IsAny(), It.IsAny()))
+ .ReturnsAsync(policy);
+
+ mock.Setup(manager => manager.ValidateSelfSignedClientCertificateAsync(application, certificate, policy, It.IsAny()))
+ .ReturnsAsync(false);
+ });
+
+ await using var server = await CreateServerAsync(options =>
+ {
+ options.Configure(options => options.SelfSignedClientCertificateChainPolicy = new X509ChainPolicy());
+
+ options.AddEventHandler(builder =>
+ {
+ builder.UseInlineHandler(context =>
+ {
+ context.ClientCertificate = certificate;
+
+ return ValueTask.CompletedTask;
+ });
+
+ builder.SetOrder(ValidateClientType.Descriptor.Order - 500);
+ });
+
+ options.Services.AddSingleton(manager);
+ });
+
+ await using var client = await server.CreateClientAsync();
+
+ // Act
+ var response = type switch
+ {
+ OpenIddictServerEndpointType.Introspection => await client.PostAsync("/connect/introspect", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam",
+ Token = "2YotnFZFEjr1zCsicMWpAA"
+ }),
+
+ OpenIddictServerEndpointType.DeviceAuthorization => await client.PostAsync("/connect/device", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam"
+ }),
+
+ OpenIddictServerEndpointType.PushedAuthorization => await client.PostAsync("/connect/par", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam",
+ RedirectUri = "http://www.fabrikam.com/path",
+ ResponseType = ResponseTypes.Code
+ }),
+
+ OpenIddictServerEndpointType.Revocation => await client.PostAsync("/connect/revoke", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam",
+ Token = "SlAV32hkKG",
+ TokenTypeHint = TokenTypeHints.RefreshToken
+ }),
+
+ OpenIddictServerEndpointType.Token => await client.PostAsync("/connect/token", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam",
+ GrantType = GrantTypes.Password,
+ Username = "johndoe",
+ Password = "A3ddj3w"
+ }),
+
+ _ => throw new NotSupportedException()
+ };
+
+ // Assert
+ Assert.Equal(Errors.InvalidClient, response.Error);
+ Assert.Equal(SR.GetResourceString(SR.ID2197), response.ErrorDescription);
+ Assert.Equal(SR.FormatID8000(SR.ID2197), response.ErrorUri);
+
+ Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce());
+ Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()), Times.AtLeastOnce());
+ Mock.Get(manager).Verify(manager => manager.GetSelfSignedClientCertificateChainPolicyAsync(application, It.IsAny(), It.IsAny()), Times.Once());
+ Mock.Get(manager).Verify(manager => manager.ValidateSelfSignedClientCertificateAsync(application, certificate, policy, It.IsAny()), Times.Once());
+ }
+
+ [Theory]
+ [InlineData(OpenIddictServerEndpointType.DeviceAuthorization)]
+ [InlineData(OpenIddictServerEndpointType.Introspection)]
+ [InlineData(OpenIddictServerEndpointType.PushedAuthorization)]
+ [InlineData(OpenIddictServerEndpointType.Revocation)]
+ [InlineData(OpenIddictServerEndpointType.Token)]
+ public async Task ProcessAuthentication_ThrowsAnExceptionWhenGlobalPkiChainPolicyIsNull(OpenIddictServerEndpointType type)
+ {
+ // Arrange
+ var application = new OpenIddictApplication();
+
+ var manager = CreateApplicationManager(mock =>
+ {
+ mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()))
+ .ReturnsAsync(application);
+
+ mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()))
+ .ReturnsAsync(false);
+ });
+
+ await using var server = await CreateServerAsync(options =>
+ {
+ options.AddEventHandler(builder =>
+ {
+ builder.UseInlineHandler(context =>
+ {
+ context.ClientCertificate = X509Certificate2.CreateFromPem($"""
+ -----BEGIN CERTIFICATE-----
+ MIIEejCCAmKgAwIBAgIQTmWjGbWFVbvtnm57bOVMZDANBgkqhkiG9w0BAQsFADAa
+ MRgwFgYDVQQDEw9JbnRlcm1lZGlhdGUgQ0EwIBcNMjYwMjAxMDMwNTIwWhgPMjEy
+ NjAyMDIwMzA1MjBaMBoxGDAWBgNVBAMTD0VuZCBjZXJ0aWZpY2F0ZTCCASIwDQYJ
+ KoZIhvcNAQEBBQADggEPADCCAQoCggEBAJZ++ilBjvvOBIkZehV2T5dIDTpggJPV
+ s+6/R2/6zMa0ykzEqaIbuvhhCaY0tDzebTYqUu9omlZkvx4jhxyA44lhwJsElqMx
+ ANxsfSNUucSiJdOWXaVCQs8hLWn9ATfflE+qJNFx8zZq4nqmfkj8DMQwsej3+Ilo
+ +FwdV1/2gyifMR0TOb/iZsgh+d386B4hIK94REZbyZ4Diod13VkDPY7I9LD8hBy7
+ 8jU5SflnHdFmG4I1IQmZcSGWfrCl0PIFymHkooeXwUm5sBebZl970908DYNB95g6
+ Fj4wBxNdrhm8Ty4DHZSikOyt/kmbrdcc8OEVYSFlaSF7QDw6X73pEYUCAwEAAaOB
+ uTCBtjAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIHgDAWBgNVHSUBAf8EDDAK
+ BggrBgEFBQcDAjAdBgNVHQ4EFgQU/b5TO/v+uy1JhF2wYpYNrTBQwqgwSQYDVR0j
+ BEIwQIAUQG7qZEGkePGiBl7n9npI/BZ1MN6hFqQUMBIxEDAOBgNVBAMTB1Jvb3Qg
+ Q0GCEEZVlMw4Ppnm5GIPGl2I1nwwFAYDVR0RBA0wC4IJbG9jYWxob3N0MA0GCSqG
+ SIb3DQEBCwUAA4ICAQB/GDA+YullFclspzw/9d3jFgorKJdISB1BKZyWD5tuQ5fP
+ WX4MOsgnjylbK8wGfG1wW2NJfKc/lWYzoxWtQDDCq9kVgUvd4JBlqR3w6reowDeE
+ jn/KGsArtjMMv0xvVnP8Wux2GLL2RYRUq6EpcQycN9/uoVyp+JRnxT6vK0y4QzYH
+ G7C6z/JfoAp7UnS61Be9VlcW1I2H5WiHuzuMG4IrMPdTGJDftSJYfXaBMzAnXdSY
+ 4BP80LsPbj1Jfuz+7tvrIO3gPmBJEprN1g0dKbcWPMRA867xLkQIQTiSnrVFADvr
+ UxO2G8KX/Yn7n5c68MfEHhFi9ndeijQfe7awG0aQjWX/XbaPpbaOAXxcozpzPIT/
+ UlciCDCofpr62BdOWJJ6XQLyx5lRg9XcB6TwRsvx9zRW434iGBmGdLMlqHVN7I2v
+ /kKNWEzOOa4hphG9OCyg3ZOcArCslUiwwfUGe8cOMKf8O63+NY6UUyU5S50EzsdN
+ 5nAK3WkMijGiMbReB/5oCLbU/B9hgEghKcbd3X2QY21MBg+GCB1z9aPduKtleAQm
+ WExEzLEnb3Kwfr5+O84J3DXisQLG8CO3T9c9uz4Tp95LIdUD4v386dGO/nYnHcZ8
+ 8Fpjqr/MfTyPgOnMusa3yKlAMypPRmtkhkxOk4olCaT6WDRQtoKk/RisREU8nw==
+ -----END CERTIFICATE-----
+ """);
+
+ return ValueTask.CompletedTask;
+ });
+
+ builder.SetOrder(ValidateClientType.Descriptor.Order - 500);
+ });
+
+ options.Services.AddSingleton(manager);
+ });
+
+ await using var client = await server.CreateClientAsync();
+
+ // Act and assert
+ var exception = type switch
+ {
+ OpenIddictServerEndpointType.Introspection =>
+ await Assert.ThrowsAsync(() => client.PostAsync("/connect/introspect", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam",
+ Token = "2YotnFZFEjr1zCsicMWpAA"
+ })),
+
+ OpenIddictServerEndpointType.DeviceAuthorization =>
+ await Assert.ThrowsAsync(() => client.PostAsync("/connect/device", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam"
+ })),
+
+ OpenIddictServerEndpointType.PushedAuthorization =>
+ await Assert.ThrowsAsync(() => client.PostAsync("/connect/par", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam",
+ RedirectUri = "http://www.fabrikam.com/path",
+ ResponseType = ResponseTypes.Code
+ })),
+
+ OpenIddictServerEndpointType.Revocation =>
+ await Assert.ThrowsAsync(() => client.PostAsync("/connect/revoke", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam",
+ Token = "SlAV32hkKG",
+ TokenTypeHint = TokenTypeHints.RefreshToken
+ })),
+
+ OpenIddictServerEndpointType.Token =>
+ await Assert.ThrowsAsync(() => client.PostAsync("/connect/token", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam",
+ GrantType = GrantTypes.Password,
+ Username = "johndoe",
+ Password = "A3ddj3w"
+ })),
+
+ _ => throw new NotSupportedException()
+ };
+
+ Assert.Equal(SR.GetResourceString(SR.ID0505), exception.Message);
+
+ Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce());
+ Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()), Times.AtLeastOnce());
+ }
+
+ [Theory]
+ [InlineData(OpenIddictServerEndpointType.DeviceAuthorization)]
+ [InlineData(OpenIddictServerEndpointType.Introspection)]
+ [InlineData(OpenIddictServerEndpointType.PushedAuthorization)]
+ [InlineData(OpenIddictServerEndpointType.Revocation)]
+ [InlineData(OpenIddictServerEndpointType.Token)]
+ public async Task ProcessAuthentication_PkiClientCertificateIsRejectedWhenChainPolicyIsNull(OpenIddictServerEndpointType type)
+ {
+ // Arrange
+ var application = new OpenIddictApplication();
+
+ var manager = CreateApplicationManager(mock =>
+ {
+ mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()))
+ .ReturnsAsync(application);
+
+ mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()))
+ .ReturnsAsync(false);
+
+ mock.Setup(manager => manager.GetClientCertificateChainPolicyAsync(application, It.IsAny(), It.IsAny()))
+ .ReturnsAsync(value: null);
+ });
+
+ await using var server = await CreateServerAsync(options =>
+ {
+ options.Configure(options => options.ClientCertificateChainPolicy = new X509ChainPolicy());
+
+ options.AddEventHandler(builder =>
+ {
+ builder.UseInlineHandler(context =>
+ {
+ context.ClientCertificate = X509Certificate2.CreateFromPem($"""
+ -----BEGIN CERTIFICATE-----
+ MIIEejCCAmKgAwIBAgIQTmWjGbWFVbvtnm57bOVMZDANBgkqhkiG9w0BAQsFADAa
+ MRgwFgYDVQQDEw9JbnRlcm1lZGlhdGUgQ0EwIBcNMjYwMjAxMDMwNTIwWhgPMjEy
+ NjAyMDIwMzA1MjBaMBoxGDAWBgNVBAMTD0VuZCBjZXJ0aWZpY2F0ZTCCASIwDQYJ
+ KoZIhvcNAQEBBQADggEPADCCAQoCggEBAJZ++ilBjvvOBIkZehV2T5dIDTpggJPV
+ s+6/R2/6zMa0ykzEqaIbuvhhCaY0tDzebTYqUu9omlZkvx4jhxyA44lhwJsElqMx
+ ANxsfSNUucSiJdOWXaVCQs8hLWn9ATfflE+qJNFx8zZq4nqmfkj8DMQwsej3+Ilo
+ +FwdV1/2gyifMR0TOb/iZsgh+d386B4hIK94REZbyZ4Diod13VkDPY7I9LD8hBy7
+ 8jU5SflnHdFmG4I1IQmZcSGWfrCl0PIFymHkooeXwUm5sBebZl970908DYNB95g6
+ Fj4wBxNdrhm8Ty4DHZSikOyt/kmbrdcc8OEVYSFlaSF7QDw6X73pEYUCAwEAAaOB
+ uTCBtjAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIHgDAWBgNVHSUBAf8EDDAK
+ BggrBgEFBQcDAjAdBgNVHQ4EFgQU/b5TO/v+uy1JhF2wYpYNrTBQwqgwSQYDVR0j
+ BEIwQIAUQG7qZEGkePGiBl7n9npI/BZ1MN6hFqQUMBIxEDAOBgNVBAMTB1Jvb3Qg
+ Q0GCEEZVlMw4Ppnm5GIPGl2I1nwwFAYDVR0RBA0wC4IJbG9jYWxob3N0MA0GCSqG
+ SIb3DQEBCwUAA4ICAQB/GDA+YullFclspzw/9d3jFgorKJdISB1BKZyWD5tuQ5fP
+ WX4MOsgnjylbK8wGfG1wW2NJfKc/lWYzoxWtQDDCq9kVgUvd4JBlqR3w6reowDeE
+ jn/KGsArtjMMv0xvVnP8Wux2GLL2RYRUq6EpcQycN9/uoVyp+JRnxT6vK0y4QzYH
+ G7C6z/JfoAp7UnS61Be9VlcW1I2H5WiHuzuMG4IrMPdTGJDftSJYfXaBMzAnXdSY
+ 4BP80LsPbj1Jfuz+7tvrIO3gPmBJEprN1g0dKbcWPMRA867xLkQIQTiSnrVFADvr
+ UxO2G8KX/Yn7n5c68MfEHhFi9ndeijQfe7awG0aQjWX/XbaPpbaOAXxcozpzPIT/
+ UlciCDCofpr62BdOWJJ6XQLyx5lRg9XcB6TwRsvx9zRW434iGBmGdLMlqHVN7I2v
+ /kKNWEzOOa4hphG9OCyg3ZOcArCslUiwwfUGe8cOMKf8O63+NY6UUyU5S50EzsdN
+ 5nAK3WkMijGiMbReB/5oCLbU/B9hgEghKcbd3X2QY21MBg+GCB1z9aPduKtleAQm
+ WExEzLEnb3Kwfr5+O84J3DXisQLG8CO3T9c9uz4Tp95LIdUD4v386dGO/nYnHcZ8
+ 8Fpjqr/MfTyPgOnMusa3yKlAMypPRmtkhkxOk4olCaT6WDRQtoKk/RisREU8nw==
+ -----END CERTIFICATE-----
+ """);
+
+ return ValueTask.CompletedTask;
+ });
+
+ builder.SetOrder(ValidateClientType.Descriptor.Order - 500);
+ });
+
+ options.Services.AddSingleton(manager);
+ });
+
+ await using var client = await server.CreateClientAsync();
+
+ // Act
+ var response = type switch
+ {
+ OpenIddictServerEndpointType.Introspection => await client.PostAsync("/connect/introspect", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam",
+ Token = "2YotnFZFEjr1zCsicMWpAA"
+ }),
+
+ OpenIddictServerEndpointType.DeviceAuthorization => await client.PostAsync("/connect/device", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam"
+ }),
+
+ OpenIddictServerEndpointType.PushedAuthorization => await client.PostAsync("/connect/par", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam",
+ RedirectUri = "http://www.fabrikam.com/path",
+ ResponseType = ResponseTypes.Code
+ }),
+
+ OpenIddictServerEndpointType.Revocation => await client.PostAsync("/connect/revoke", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam",
+ Token = "SlAV32hkKG",
+ TokenTypeHint = TokenTypeHints.RefreshToken
+ }),
+
+ OpenIddictServerEndpointType.Token => await client.PostAsync("/connect/token", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam",
+ GrantType = GrantTypes.Password,
+ Username = "johndoe",
+ Password = "A3ddj3w"
+ }),
+
+ _ => throw new NotSupportedException()
+ };
+
+ // Assert
+ Assert.Equal(Errors.InvalidClient, response.Error);
+ Assert.Equal(SR.GetResourceString(SR.ID2197), response.ErrorDescription);
+ Assert.Equal(SR.FormatID8000(SR.ID2197), response.ErrorUri);
+
+ Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce());
+ Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()), Times.AtLeastOnce());
+ Mock.Get(manager).Verify(manager => manager.GetClientCertificateChainPolicyAsync(application, It.IsAny(), It.IsAny()), Times.Once());
+ }
+
+ [Theory]
+ [InlineData(OpenIddictServerEndpointType.DeviceAuthorization)]
+ [InlineData(OpenIddictServerEndpointType.Introspection)]
+ [InlineData(OpenIddictServerEndpointType.PushedAuthorization)]
+ [InlineData(OpenIddictServerEndpointType.Revocation)]
+ [InlineData(OpenIddictServerEndpointType.Token)]
+ public async Task ProcessAuthentication_PkiClientCertificateIsRejectedWhenInvalid(OpenIddictServerEndpointType type)
+ {
+ // Arrange
+ var application = new OpenIddictApplication();
+ var policy = new X509ChainPolicy();
+ var certificate = X509Certificate2.CreateFromPem($"""
+ -----BEGIN CERTIFICATE-----
+ MIIEejCCAmKgAwIBAgIQTmWjGbWFVbvtnm57bOVMZDANBgkqhkiG9w0BAQsFADAa
+ MRgwFgYDVQQDEw9JbnRlcm1lZGlhdGUgQ0EwIBcNMjYwMjAxMDMwNTIwWhgPMjEy
+ NjAyMDIwMzA1MjBaMBoxGDAWBgNVBAMTD0VuZCBjZXJ0aWZpY2F0ZTCCASIwDQYJ
+ KoZIhvcNAQEBBQADggEPADCCAQoCggEBAJZ++ilBjvvOBIkZehV2T5dIDTpggJPV
+ s+6/R2/6zMa0ykzEqaIbuvhhCaY0tDzebTYqUu9omlZkvx4jhxyA44lhwJsElqMx
+ ANxsfSNUucSiJdOWXaVCQs8hLWn9ATfflE+qJNFx8zZq4nqmfkj8DMQwsej3+Ilo
+ +FwdV1/2gyifMR0TOb/iZsgh+d386B4hIK94REZbyZ4Diod13VkDPY7I9LD8hBy7
+ 8jU5SflnHdFmG4I1IQmZcSGWfrCl0PIFymHkooeXwUm5sBebZl970908DYNB95g6
+ Fj4wBxNdrhm8Ty4DHZSikOyt/kmbrdcc8OEVYSFlaSF7QDw6X73pEYUCAwEAAaOB
+ uTCBtjAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIHgDAWBgNVHSUBAf8EDDAK
+ BggrBgEFBQcDAjAdBgNVHQ4EFgQU/b5TO/v+uy1JhF2wYpYNrTBQwqgwSQYDVR0j
+ BEIwQIAUQG7qZEGkePGiBl7n9npI/BZ1MN6hFqQUMBIxEDAOBgNVBAMTB1Jvb3Qg
+ Q0GCEEZVlMw4Ppnm5GIPGl2I1nwwFAYDVR0RBA0wC4IJbG9jYWxob3N0MA0GCSqG
+ SIb3DQEBCwUAA4ICAQB/GDA+YullFclspzw/9d3jFgorKJdISB1BKZyWD5tuQ5fP
+ WX4MOsgnjylbK8wGfG1wW2NJfKc/lWYzoxWtQDDCq9kVgUvd4JBlqR3w6reowDeE
+ jn/KGsArtjMMv0xvVnP8Wux2GLL2RYRUq6EpcQycN9/uoVyp+JRnxT6vK0y4QzYH
+ G7C6z/JfoAp7UnS61Be9VlcW1I2H5WiHuzuMG4IrMPdTGJDftSJYfXaBMzAnXdSY
+ 4BP80LsPbj1Jfuz+7tvrIO3gPmBJEprN1g0dKbcWPMRA867xLkQIQTiSnrVFADvr
+ UxO2G8KX/Yn7n5c68MfEHhFi9ndeijQfe7awG0aQjWX/XbaPpbaOAXxcozpzPIT/
+ UlciCDCofpr62BdOWJJ6XQLyx5lRg9XcB6TwRsvx9zRW434iGBmGdLMlqHVN7I2v
+ /kKNWEzOOa4hphG9OCyg3ZOcArCslUiwwfUGe8cOMKf8O63+NY6UUyU5S50EzsdN
+ 5nAK3WkMijGiMbReB/5oCLbU/B9hgEghKcbd3X2QY21MBg+GCB1z9aPduKtleAQm
+ WExEzLEnb3Kwfr5+O84J3DXisQLG8CO3T9c9uz4Tp95LIdUD4v386dGO/nYnHcZ8
+ 8Fpjqr/MfTyPgOnMusa3yKlAMypPRmtkhkxOk4olCaT6WDRQtoKk/RisREU8nw==
+ -----END CERTIFICATE-----
+ """);
+
+ var manager = CreateApplicationManager(mock =>
+ {
+ mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()))
+ .ReturnsAsync(application);
+
+ mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()))
+ .ReturnsAsync(false);
+
+ mock.Setup(manager => manager.GetClientCertificateChainPolicyAsync(application, It.IsAny(), It.IsAny()))
+ .ReturnsAsync(policy);
+
+ mock.Setup(manager => manager.ValidateClientCertificateAsync(application, certificate, policy, It.IsAny()))
+ .ReturnsAsync(false);
+ });
+
+ await using var server = await CreateServerAsync(options =>
+ {
+ options.Configure(options => options.ClientCertificateChainPolicy = new X509ChainPolicy());
+
+ options.AddEventHandler(builder =>
+ {
+ builder.UseInlineHandler(context =>
+ {
+ context.ClientCertificate = certificate;
+
+ return ValueTask.CompletedTask;
+ });
+
+ builder.SetOrder(ValidateClientType.Descriptor.Order - 500);
+ });
+
+ options.Services.AddSingleton(manager);
+ });
+
+ await using var client = await server.CreateClientAsync();
+
+ // Act
+ var response = type switch
+ {
+ OpenIddictServerEndpointType.Introspection => await client.PostAsync("/connect/introspect", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam",
+ Token = "2YotnFZFEjr1zCsicMWpAA"
+ }),
+
+ OpenIddictServerEndpointType.DeviceAuthorization => await client.PostAsync("/connect/device", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam"
+ }),
+
+ OpenIddictServerEndpointType.PushedAuthorization => await client.PostAsync("/connect/par", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam",
+ RedirectUri = "http://www.fabrikam.com/path",
+ ResponseType = ResponseTypes.Code
+ }),
+
+ OpenIddictServerEndpointType.Revocation => await client.PostAsync("/connect/revoke", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam",
+ Token = "SlAV32hkKG",
+ TokenTypeHint = TokenTypeHints.RefreshToken
+ }),
+
+ OpenIddictServerEndpointType.Token => await client.PostAsync("/connect/token", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam",
+ GrantType = GrantTypes.Password,
+ Username = "johndoe",
+ Password = "A3ddj3w"
+ }),
+
+ _ => throw new NotSupportedException()
+ };
+
+ // Assert
+ Assert.Equal(Errors.InvalidClient, response.Error);
+ Assert.Equal(SR.GetResourceString(SR.ID2197), response.ErrorDescription);
+ Assert.Equal(SR.FormatID8000(SR.ID2197), response.ErrorUri);
+
+ Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce());
+ Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()), Times.AtLeastOnce());
+ Mock.Get(manager).Verify(manager => manager.GetClientCertificateChainPolicyAsync(application, It.IsAny(), It.IsAny()), Times.Once());
+ Mock.Get(manager).Verify(manager => manager.ValidateClientCertificateAsync(application, certificate, policy, It.IsAny()), Times.Once());
+ }
+#endif
+
[Fact]
public async Task ProcessAuthentication_RequestTokenPrincipalIsNotPopulatedWhenRequestTokenIsMissing()
{
diff --git a/test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs b/test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs
index e84a8482..6000100c 100644
--- a/test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs
+++ b/test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs
@@ -1,4 +1,6 @@
using System.Reflection;
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
@@ -309,9 +311,9 @@ public class OpenIddictServerBuilderTests
builder.AddDevelopmentEncryptionCertificate(
subject: new X500DistinguishedName("CN=" + Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture)));
- var serviceProvider = services.BuildServiceProvider();
+ var provider = services.BuildServiceProvider();
- var options = serviceProvider.GetRequiredService>();
+ var options = provider.GetRequiredService>();
// Act and assert
var exception = Assert.Throws(() => options.Value);
@@ -365,9 +367,9 @@ public class OpenIddictServerBuilderTests
builder.AddDevelopmentSigningCertificate(
subject: new X500DistinguishedName("CN=" + Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture)));
- var serviceProvider = services.BuildServiceProvider();
+ var provider = services.BuildServiceProvider();
- var options = serviceProvider.GetRequiredService>();
+ var options = provider.GetRequiredService>();
// Act and assert
var exception = Assert.Throws(() => options.Value);
@@ -784,6 +786,412 @@ public class OpenIddictServerBuilderTests
Assert.True(options.DisableTokenStorage);
}
+ [Fact]
+ public void EnablePublicKeyInfrastructureClientCertificateAuthentication_ThrowsAnExceptionForNullCertificates()
+ {
+ // Arrange
+ var services = CreateServices();
+ var builder = CreateBuilder(services);
+
+ // Act and assert
+ var exception = Assert.Throws(() =>
+ builder.EnablePublicKeyInfrastructureClientCertificateAuthentication(certificates: null!));
+
+ Assert.Equal("certificates", exception.ParamName);
+ }
+
+#if SUPPORTS_X509_CHAIN_POLICY_CUSTOM_TRUST_STORE
+ [Fact]
+ public void EnablePublicKeyInfrastructureClientCertificateAuthentication_ThrowsAnExceptionWhenNoRootCertificateProvided()
+ {
+ // Arrange
+ var services = CreateServices();
+ var builder = CreateBuilder(services);
+ var certificates = new X509Certificate2Collection
+ {
+ // Intermediate certificate:
+ X509Certificate2.CreateFromPem($"""
+ -----BEGIN CERTIFICATE-----
+ MIIFRDCCAyygAwIBAgIRALpTKvDtz6lGPaqNaK8aULowDQYJKoZIhvcNAQELBQAw
+ EjEQMA4GA1UEAxMHUm9vdCBDQTAgFw0yNjAxMzAxNzM4NTVaGA8yMTI2MDEzMTE3
+ Mzg1NVowGjEYMBYGA1UEAxMPSW50ZXJtZWRpYXRlIENBMIICIjANBgkqhkiG9w0B
+ AQEFAAOCAg8AMIICCgKCAgEAvHiS4aNz7vL5mOJNjbpybcK75RhH1sXifLwKW8Zg
+ nHm+KjdRENf3X9yp7c+xNrtpHhG4/gp8M++0G1Cz4Yvq8idZu8IpMiqk9/KT447b
+ VocaRPCFC4NIC9U6g4s3rwHLUr2wMCAWiM9yjWcbXcvIlnSuA/i/lSAfAUPjrn8X
+ LLDgqlEkInmWRvYvDmdmw7vdqfDobFTDh0YRWB/y/LuDvkPFBDg3cfY8+AyrDkha
+ y3m1Ot3NTsg0O/HOL6MXMN9HRd4vX37XBV88kZtFE+vyHdYDs2NzGjAbfz4JZ6xz
+ 4+weUjklOc9ucAEgfAnwijH9w4KFBJEHAqtOMsbrIy74MvPTFj3LeayLo5nhLeqp
+ GbqvJcEX1UM83vFt+JUaDVbXDUG2ECHMDe6W5r5eYQtZW1ErKkRYNTJu++I0vDZr
+ EeZdYDYp15dbksMXUDyzhJ0WS0N23b7s57S6YAbok97UD/d+aGMtY3kJ/wIiftYY
+ Sel/MO/QXrgNchnVtUbShgE2oFvAJUYRvlZarG9/egp3Jb3B4WNjwIyzaT6SFtnG
+ Tg+IEXYPE2s5x4YZ8GINygWKrDbV7UuANRjKvoBlGmcrW/iz2Aaa+H5696p1HLVA
+ k7gXTw7WlJxzP6JPs2ZjWu27k88oAUV8HJjzFzGUsRPIjkf8KcJuxAqwLPoqFelh
+ uNECAwEAAaOBijCBhzASBgNVHRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIB
+ BjAdBgNVHQ4EFgQUq7mtVl4BCJyLWTHEpSCqokobTCcwQgYDVR0jBDswOYAUKv1o
+ 4gZNED0vspeWdqb0WeS3N06hFqQUMBIxEDAOBgNVBAMTB1Jvb3QgQ0GCCQDNRQQ8
+ F7il/DANBgkqhkiG9w0BAQsFAAOCAgEArlp0WSTwHgv8wgI+XT/QNxUQBiVyrHql
+ SHIMCBA7rDPPsl2RURWzQDE7zqovA3r7fnrYMfVXAAdgzXhDLQwL15RdaeoZUsjH
+ xN4y5Mtn0zv1yp7PPtZUc0mZ4Q0xWo4MPve82IfhiqWXretUxvcZ4NKY3sni0s8W
+ hViZdHH77vVIWWcWK414cpRwvsDtaKkgS4h8yHiUOtlgKgTViyUd0ovphR0boLtF
+ Ddw+jmLGM9c5keIs87RCTqCcHD4nP81kHHUaE60NDMtHH5UONSA5ecsHo11tC1am
+ 9U2TRs5+zwyBnwy4oOE/EZxXslcz27XyAX7MOhZppue+xtEDyex4gjiS27Nl8Va1
+ R1I1vkI5A209OQQ4JXzJZcAtgWep/ez0hu8TOkdtn0l/6aGkj2l3iwVG8edjiwSz
+ nVSPaBFKRtrHPuk9uEqu1xtP2klMeJEs7a5bVOyBOzZksafDwVTSPdRnDJDxo/Rx
+ bGzSWWYqKNsDxyV9aVMZ1iABW2O7qh6eXbioICzWAWQyplLeihnZ1d0o0h9gk/Kt
+ dPuLATo/WSXBA3oDfdEjjEWhmEdj6mR92KrOTQUF7JkOM74ZGs/lCxXsCOva7Z0N
+ kZmmpiU3Dewr11+SUxGx8g/4Ba1FhW9pXI9jSYguzY1KY210nF2P5YLsz/HgIM2r
+ SVA+QXhvcj0=
+ -----END CERTIFICATE-----
+ """)
+ };
+
+ // Act and assert
+ var exception = Assert.Throws(() =>
+ builder.EnablePublicKeyInfrastructureClientCertificateAuthentication(certificates));
+
+ Assert.Equal("certificates", exception.ParamName);
+ }
+
+ [Fact]
+ public void EnablePublicKeyInfrastructureClientCertificateAuthentication_ThrowsAnExceptionWhenEndCertificateProvided()
+ {
+ // Arrange
+ var services = CreateServices();
+ var builder = CreateBuilder(services);
+ var certificates = new X509Certificate2Collection
+ {
+ // Root certificate:
+ X509Certificate2.CreateFromPem($"""
+ -----BEGIN CERTIFICATE-----
+ MIIE7jCCAtagAwIBAgIJAM1FBDwXuKX8MA0GCSqGSIb3DQEBCwUAMBIxEDAOBgNV
+ BAMTB1Jvb3QgQ0EwIBcNMjYwMTMwMTczODU1WhgPMjEyNjAxMzExNzM4NTVaMBIx
+ EDAOBgNVBAMTB1Jvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC
+ AQDt/Nz3V5bKbYrJoITYrk3dL9CF+rDcMM6VyJ1pj33feXMTCcRJXkydKDc25sX+
+ 2C/yrx75zxcUBRoWzsg++YdHJffcZI0+6Q7XBOndNKflCL8lBihvT/EUO9MQYRWV
+ 495ie3deXoVGebrl6XbGAfm755Ml8KikYFryU6WOxApfQxjcKxnQLNLCTkIyn3WE
+ lzce9awtECPgdQfjDzCmE32xXMQcn/0HQUDxiyGvBQbBf8ZM9h3iJz4VGGVQkE7m
+ Ix15CwjAyrPc7jGAnUx00QGOCGzfT4bQaHOAZMgEJ8/KJhmx0Fmd6KR1fNJDqDvx
+ JYW683y1P512QZgN16e2ZEOdg9fsuXV/PaS6NmHOh7s/hwZsoIf3CJ5dX/M1h1BA
+ 4buxlvRfeZdANQHJLuQFPC4DQ8SWgbxXhL8KCo0jS9rUPTaxikL7+prFK/t39YFt
+ LzowUL8d+sMvUrn3v9yXb363wBB7fja1ZG1EOl7r2YO1uGleRR1ztymfRVziQ/Np
+ wRjDeBb9rWL9srPPilvo+5VsJe2a/XtfZuxMoH6vNEl04W6/iyYE4cVizwRC8GTW
+ hSHdhk2vT2/eyGSK3Cj5U74x+orHD+3XS6xHd63qB1oo+hJl2Ln/7pBAm9qFnNed
+ u6Wn/++Oi7M7nMU/ngEkbPKUfwrR/fEKQuweJaXTgiqj3QIDAQABo0UwQzASBgNV
+ HRMBAf8ECDAGAQH/AgEBMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUKv1o4gZN
+ ED0vspeWdqb0WeS3N04wDQYJKoZIhvcNAQELBQADggIBACLp5z1zaemqoFPtQ5Sr
+ Ii2ijs03Gc52Y/Pbg1V83xg1nMa+vI4aQYc90FZnNOv7I4VAmR6I3cI5bA3tnrzB
+ /yCMOkdxiFt6W0OQPMlmlVdCbPtUqXWM3tLilRn90XYEEWZB8I1sOrk2WH7oEHmu
+ W7BC2I3igjhUDug2bl7VwdBXzRJrWFgYdhVsjRU9rx3AbZqbD/3pC9B3PcwZxTGz
+ k8wRMP/9cF49VvUFVWhp01Bol1StwgX3r6IbaamDdIJd0MvHp0ctgqL6PLRzgRRY
+ adzWX8SPlgARVCixSOkXmAGNeuNWT/Ulo0W1xaNVTvOssfx58v3lwnjoaMLi7UFA
+ tCPrLZtZYvScB0+0AjUwfIfKrHqRdP5OqJZi7PhahR39tTh9pQX63INQvKYKO583
+ iidiNDau4HU+e+ujnXR5xJdgNWtuehKRdZFlLBH0lKKXrrnPyW9YtIjCVf1zhc+L
+ VSyo/aajWKBGqyTTbM1zetGe1rah97i7/0snoh97sWHlenmSX3BQUnajLkp9ZMh0
+ vY4koR1fxIlxHnOQ7SebZ23QiWUjdmJoOXW6Bub6VhWMDY96EpMr17QiGtexbUHD
+ 1nm2Z0xGwyWeyg2QSnNEAIZ6F2EeaZ6jmD/5ASXujcutNGyPwsSGkog4/Ir5Ol15
+ +3OP4DTt75BHxzMUxB6XmbYN
+ -----END CERTIFICATE-----
+ """),
+
+ // Intermediate certificate:
+ X509Certificate2.CreateFromPem($"""
+ -----BEGIN CERTIFICATE-----
+ MIIFRDCCAyygAwIBAgIRALpTKvDtz6lGPaqNaK8aULowDQYJKoZIhvcNAQELBQAw
+ EjEQMA4GA1UEAxMHUm9vdCBDQTAgFw0yNjAxMzAxNzM4NTVaGA8yMTI2MDEzMTE3
+ Mzg1NVowGjEYMBYGA1UEAxMPSW50ZXJtZWRpYXRlIENBMIICIjANBgkqhkiG9w0B
+ AQEFAAOCAg8AMIICCgKCAgEAvHiS4aNz7vL5mOJNjbpybcK75RhH1sXifLwKW8Zg
+ nHm+KjdRENf3X9yp7c+xNrtpHhG4/gp8M++0G1Cz4Yvq8idZu8IpMiqk9/KT447b
+ VocaRPCFC4NIC9U6g4s3rwHLUr2wMCAWiM9yjWcbXcvIlnSuA/i/lSAfAUPjrn8X
+ LLDgqlEkInmWRvYvDmdmw7vdqfDobFTDh0YRWB/y/LuDvkPFBDg3cfY8+AyrDkha
+ y3m1Ot3NTsg0O/HOL6MXMN9HRd4vX37XBV88kZtFE+vyHdYDs2NzGjAbfz4JZ6xz
+ 4+weUjklOc9ucAEgfAnwijH9w4KFBJEHAqtOMsbrIy74MvPTFj3LeayLo5nhLeqp
+ GbqvJcEX1UM83vFt+JUaDVbXDUG2ECHMDe6W5r5eYQtZW1ErKkRYNTJu++I0vDZr
+ EeZdYDYp15dbksMXUDyzhJ0WS0N23b7s57S6YAbok97UD/d+aGMtY3kJ/wIiftYY
+ Sel/MO/QXrgNchnVtUbShgE2oFvAJUYRvlZarG9/egp3Jb3B4WNjwIyzaT6SFtnG
+ Tg+IEXYPE2s5x4YZ8GINygWKrDbV7UuANRjKvoBlGmcrW/iz2Aaa+H5696p1HLVA
+ k7gXTw7WlJxzP6JPs2ZjWu27k88oAUV8HJjzFzGUsRPIjkf8KcJuxAqwLPoqFelh
+ uNECAwEAAaOBijCBhzASBgNVHRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIB
+ BjAdBgNVHQ4EFgQUq7mtVl4BCJyLWTHEpSCqokobTCcwQgYDVR0jBDswOYAUKv1o
+ 4gZNED0vspeWdqb0WeS3N06hFqQUMBIxEDAOBgNVBAMTB1Jvb3QgQ0GCCQDNRQQ8
+ F7il/DANBgkqhkiG9w0BAQsFAAOCAgEArlp0WSTwHgv8wgI+XT/QNxUQBiVyrHql
+ SHIMCBA7rDPPsl2RURWzQDE7zqovA3r7fnrYMfVXAAdgzXhDLQwL15RdaeoZUsjH
+ xN4y5Mtn0zv1yp7PPtZUc0mZ4Q0xWo4MPve82IfhiqWXretUxvcZ4NKY3sni0s8W
+ hViZdHH77vVIWWcWK414cpRwvsDtaKkgS4h8yHiUOtlgKgTViyUd0ovphR0boLtF
+ Ddw+jmLGM9c5keIs87RCTqCcHD4nP81kHHUaE60NDMtHH5UONSA5ecsHo11tC1am
+ 9U2TRs5+zwyBnwy4oOE/EZxXslcz27XyAX7MOhZppue+xtEDyex4gjiS27Nl8Va1
+ R1I1vkI5A209OQQ4JXzJZcAtgWep/ez0hu8TOkdtn0l/6aGkj2l3iwVG8edjiwSz
+ nVSPaBFKRtrHPuk9uEqu1xtP2klMeJEs7a5bVOyBOzZksafDwVTSPdRnDJDxo/Rx
+ bGzSWWYqKNsDxyV9aVMZ1iABW2O7qh6eXbioICzWAWQyplLeihnZ1d0o0h9gk/Kt
+ dPuLATo/WSXBA3oDfdEjjEWhmEdj6mR92KrOTQUF7JkOM74ZGs/lCxXsCOva7Z0N
+ kZmmpiU3Dewr11+SUxGx8g/4Ba1FhW9pXI9jSYguzY1KY210nF2P5YLsz/HgIM2r
+ SVA+QXhvcj0=
+ -----END CERTIFICATE-----
+ """),
+
+ // End certificate:
+ X509Certificate2.CreateFromPem($"""
+ -----BEGIN CERTIFICATE-----
+ MIIEfDCCAmSgAwIBAgIRAMrgbbME5gBGSu3XfBUTMZAwDQYJKoZIhvcNAQELBQAw
+ GjEYMBYGA1UEAxMPSW50ZXJtZWRpYXRlIENBMCAXDTI2MDEzMDE3Mzg1NVoYDzIx
+ MjYwMTMxMTczODU1WjAaMRgwFgYDVQQDEw9FbmQgY2VydGlmaWNhdGUwggEiMA0G
+ CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDY5UyMDT2evGgOXTzuH6adZmLcjLbv
+ 9u1cdpfYc2jgdA+jXm0hvQmhwmQMosu0KDFtMX0okZ4xM0H5XEDbxmKBY7WwAdFF
+ Morz7tu/UzRLYXUJp/SRk6/zXKb4qkvtWaEcKBujlLA7jUEDcrCmTaoyU0Zz9ZzG
+ zrJc59UceXSH3gAoAkoQesNKUBsdgdk8Na3h+U8nLl4rHXWkSYi/VwKkROaDfnT2
+ mgpsJfBVyQtuAJDXsLFRhtmsPnR3KoutJUh69WnlC7mRrTVI2rhUPKFny1gGxk1W
+ rsI9vzkIIiLkc+tsAR76DNnoVkXhuqEQ9SMnl5CxV2GFWEX/TOLhteVVAgMBAAGj
+ gbowgbcwDAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0lAQH/BAww
+ CgYIKwYBBQUHAwIwHQYDVR0OBBYEFCRF8sJJqhgyr/A8RnsP/JUE06dGMEoGA1Ud
+ IwRDMEGAFKu5rVZeAQici1kxxKUgqqJKG0wnoRakFDASMRAwDgYDVQQDEwdSb290
+ IENBghEAulMq8O3PqUY9qo1orxpQujAUBgNVHREEDTALgglsb2NhbGhvc3QwDQYJ
+ KoZIhvcNAQELBQADggIBACW7bCe7KpuAVZO4jIjtMLwKJ+1QZ7551FIsvBLDCFF+
+ fX2zND0ne+8hv1qemHdzDBED6WeurOQqGtgI+adj7HrXAfAXKihBaCjq2U6U3sR5
+ fqyYY5of1gvJhvJK+TEY3Fb55PvA+j38GwI52hKSkRdunPpCjRI1d/+Jvb+voUOa
+ kzLsARgOaMhJtYQeMebKr7uSLezFoOaRfc5rqpiVTA7xXO8dHkz2p49bxfIRj1Tq
+ 2Lgx9uAE4omwzxSB/cwZiG1tNgpVVvn2Tb20SgCIBGl7Oqave+LRm1Eztl0Q8e8C
+ iz6bLMiCcesRRFxU8TE2mhmNOeNUsBIP730+ZOnP2rgX3Zs93gTFX/omPuQS8Kj3
+ ly5+v+PZkuN3xZ56mLXURlDRmWI1gqNRgNYl1jwYyQ0ll05yk3JFIyUvxdg3klRK
+ 9/+MoKw8PVGbSKntzoHiqVUHnrB1lemJqZ91Dx+h8K58eaRs3/aL0lUEli7NpNXR
+ 5Q6Tl7cCVe5ZjtR7Z56IQLmswq/TNJVXQSBjLLwLCkaJvTXWwojkG5ETU/t/ikCG
+ c2bH6ICUizzz2gdJNdcIP8wc7SsxONdpPmK3ED3KQBlTVDRUJ/w9Q4HYztXEFiyg
+ xkNzboe3SYq92mqVilBTiY51SNZuOr8zRZp9gcMKoQgmxgfl7j6JiRVpZzoxb54Y
+ -----END CERTIFICATE-----
+ """)
+ };
+
+ // Act and assert
+ var exception = Assert.Throws(() =>
+ builder.EnablePublicKeyInfrastructureClientCertificateAuthentication(certificates));
+
+ Assert.Equal("certificates", exception.ParamName);
+ }
+
+ [Fact]
+ public void EnablePublicKeyInfrastructureClientCertificateAuthentication_PolicyIsCorrectlyConfigured()
+ {
+ // Arrange
+ var services = CreateServices();
+ var builder = CreateBuilder(services);
+ var certificates = new X509Certificate2Collection
+ {
+ // Root certificate:
+ X509Certificate2.CreateFromPem($"""
+ -----BEGIN CERTIFICATE-----
+ MIIE7jCCAtagAwIBAgIJAM1FBDwXuKX8MA0GCSqGSIb3DQEBCwUAMBIxEDAOBgNV
+ BAMTB1Jvb3QgQ0EwIBcNMjYwMTMwMTczODU1WhgPMjEyNjAxMzExNzM4NTVaMBIx
+ EDAOBgNVBAMTB1Jvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC
+ AQDt/Nz3V5bKbYrJoITYrk3dL9CF+rDcMM6VyJ1pj33feXMTCcRJXkydKDc25sX+
+ 2C/yrx75zxcUBRoWzsg++YdHJffcZI0+6Q7XBOndNKflCL8lBihvT/EUO9MQYRWV
+ 495ie3deXoVGebrl6XbGAfm755Ml8KikYFryU6WOxApfQxjcKxnQLNLCTkIyn3WE
+ lzce9awtECPgdQfjDzCmE32xXMQcn/0HQUDxiyGvBQbBf8ZM9h3iJz4VGGVQkE7m
+ Ix15CwjAyrPc7jGAnUx00QGOCGzfT4bQaHOAZMgEJ8/KJhmx0Fmd6KR1fNJDqDvx
+ JYW683y1P512QZgN16e2ZEOdg9fsuXV/PaS6NmHOh7s/hwZsoIf3CJ5dX/M1h1BA
+ 4buxlvRfeZdANQHJLuQFPC4DQ8SWgbxXhL8KCo0jS9rUPTaxikL7+prFK/t39YFt
+ LzowUL8d+sMvUrn3v9yXb363wBB7fja1ZG1EOl7r2YO1uGleRR1ztymfRVziQ/Np
+ wRjDeBb9rWL9srPPilvo+5VsJe2a/XtfZuxMoH6vNEl04W6/iyYE4cVizwRC8GTW
+ hSHdhk2vT2/eyGSK3Cj5U74x+orHD+3XS6xHd63qB1oo+hJl2Ln/7pBAm9qFnNed
+ u6Wn/++Oi7M7nMU/ngEkbPKUfwrR/fEKQuweJaXTgiqj3QIDAQABo0UwQzASBgNV
+ HRMBAf8ECDAGAQH/AgEBMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUKv1o4gZN
+ ED0vspeWdqb0WeS3N04wDQYJKoZIhvcNAQELBQADggIBACLp5z1zaemqoFPtQ5Sr
+ Ii2ijs03Gc52Y/Pbg1V83xg1nMa+vI4aQYc90FZnNOv7I4VAmR6I3cI5bA3tnrzB
+ /yCMOkdxiFt6W0OQPMlmlVdCbPtUqXWM3tLilRn90XYEEWZB8I1sOrk2WH7oEHmu
+ W7BC2I3igjhUDug2bl7VwdBXzRJrWFgYdhVsjRU9rx3AbZqbD/3pC9B3PcwZxTGz
+ k8wRMP/9cF49VvUFVWhp01Bol1StwgX3r6IbaamDdIJd0MvHp0ctgqL6PLRzgRRY
+ adzWX8SPlgARVCixSOkXmAGNeuNWT/Ulo0W1xaNVTvOssfx58v3lwnjoaMLi7UFA
+ tCPrLZtZYvScB0+0AjUwfIfKrHqRdP5OqJZi7PhahR39tTh9pQX63INQvKYKO583
+ iidiNDau4HU+e+ujnXR5xJdgNWtuehKRdZFlLBH0lKKXrrnPyW9YtIjCVf1zhc+L
+ VSyo/aajWKBGqyTTbM1zetGe1rah97i7/0snoh97sWHlenmSX3BQUnajLkp9ZMh0
+ vY4koR1fxIlxHnOQ7SebZ23QiWUjdmJoOXW6Bub6VhWMDY96EpMr17QiGtexbUHD
+ 1nm2Z0xGwyWeyg2QSnNEAIZ6F2EeaZ6jmD/5ASXujcutNGyPwsSGkog4/Ir5Ol15
+ +3OP4DTt75BHxzMUxB6XmbYN
+ -----END CERTIFICATE-----
+ """),
+
+ // Intermediate certificate:
+ X509Certificate2.CreateFromPem($"""
+ -----BEGIN CERTIFICATE-----
+ MIIFRDCCAyygAwIBAgIRALpTKvDtz6lGPaqNaK8aULowDQYJKoZIhvcNAQELBQAw
+ EjEQMA4GA1UEAxMHUm9vdCBDQTAgFw0yNjAxMzAxNzM4NTVaGA8yMTI2MDEzMTE3
+ Mzg1NVowGjEYMBYGA1UEAxMPSW50ZXJtZWRpYXRlIENBMIICIjANBgkqhkiG9w0B
+ AQEFAAOCAg8AMIICCgKCAgEAvHiS4aNz7vL5mOJNjbpybcK75RhH1sXifLwKW8Zg
+ nHm+KjdRENf3X9yp7c+xNrtpHhG4/gp8M++0G1Cz4Yvq8idZu8IpMiqk9/KT447b
+ VocaRPCFC4NIC9U6g4s3rwHLUr2wMCAWiM9yjWcbXcvIlnSuA/i/lSAfAUPjrn8X
+ LLDgqlEkInmWRvYvDmdmw7vdqfDobFTDh0YRWB/y/LuDvkPFBDg3cfY8+AyrDkha
+ y3m1Ot3NTsg0O/HOL6MXMN9HRd4vX37XBV88kZtFE+vyHdYDs2NzGjAbfz4JZ6xz
+ 4+weUjklOc9ucAEgfAnwijH9w4KFBJEHAqtOMsbrIy74MvPTFj3LeayLo5nhLeqp
+ GbqvJcEX1UM83vFt+JUaDVbXDUG2ECHMDe6W5r5eYQtZW1ErKkRYNTJu++I0vDZr
+ EeZdYDYp15dbksMXUDyzhJ0WS0N23b7s57S6YAbok97UD/d+aGMtY3kJ/wIiftYY
+ Sel/MO/QXrgNchnVtUbShgE2oFvAJUYRvlZarG9/egp3Jb3B4WNjwIyzaT6SFtnG
+ Tg+IEXYPE2s5x4YZ8GINygWKrDbV7UuANRjKvoBlGmcrW/iz2Aaa+H5696p1HLVA
+ k7gXTw7WlJxzP6JPs2ZjWu27k88oAUV8HJjzFzGUsRPIjkf8KcJuxAqwLPoqFelh
+ uNECAwEAAaOBijCBhzASBgNVHRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIB
+ BjAdBgNVHQ4EFgQUq7mtVl4BCJyLWTHEpSCqokobTCcwQgYDVR0jBDswOYAUKv1o
+ 4gZNED0vspeWdqb0WeS3N06hFqQUMBIxEDAOBgNVBAMTB1Jvb3QgQ0GCCQDNRQQ8
+ F7il/DANBgkqhkiG9w0BAQsFAAOCAgEArlp0WSTwHgv8wgI+XT/QNxUQBiVyrHql
+ SHIMCBA7rDPPsl2RURWzQDE7zqovA3r7fnrYMfVXAAdgzXhDLQwL15RdaeoZUsjH
+ xN4y5Mtn0zv1yp7PPtZUc0mZ4Q0xWo4MPve82IfhiqWXretUxvcZ4NKY3sni0s8W
+ hViZdHH77vVIWWcWK414cpRwvsDtaKkgS4h8yHiUOtlgKgTViyUd0ovphR0boLtF
+ Ddw+jmLGM9c5keIs87RCTqCcHD4nP81kHHUaE60NDMtHH5UONSA5ecsHo11tC1am
+ 9U2TRs5+zwyBnwy4oOE/EZxXslcz27XyAX7MOhZppue+xtEDyex4gjiS27Nl8Va1
+ R1I1vkI5A209OQQ4JXzJZcAtgWep/ez0hu8TOkdtn0l/6aGkj2l3iwVG8edjiwSz
+ nVSPaBFKRtrHPuk9uEqu1xtP2klMeJEs7a5bVOyBOzZksafDwVTSPdRnDJDxo/Rx
+ bGzSWWYqKNsDxyV9aVMZ1iABW2O7qh6eXbioICzWAWQyplLeihnZ1d0o0h9gk/Kt
+ dPuLATo/WSXBA3oDfdEjjEWhmEdj6mR92KrOTQUF7JkOM74ZGs/lCxXsCOva7Z0N
+ kZmmpiU3Dewr11+SUxGx8g/4Ba1FhW9pXI9jSYguzY1KY210nF2P5YLsz/HgIM2r
+ SVA+QXhvcj0=
+ -----END CERTIFICATE-----
+ """)
+ };
+
+ // Act
+ builder.EnablePublicKeyInfrastructureClientCertificateAuthentication(certificates);
+
+ var options = GetOptions(services);
+
+ // Assert
+ Assert.NotNull(options.ClientCertificateChainPolicy);
+ Assert.Equal(X509ChainTrustMode.CustomRootTrust, options.ClientCertificateChainPolicy.TrustMode);
+ Assert.Contains(options.ClientCertificateChainPolicy.ApplicationPolicy.Cast(),
+ oid => oid.Value == ObjectIdentifiers.ExtendedKeyUsages.ClientAuthentication);
+ }
+
+ [Fact]
+ public void EnablePublicKeyInfrastructureClientCertificateAuthentication_ThrowsAnExceptionWhenTrustModeIsChanged()
+ {
+ // Arrange
+ var services = CreateServices();
+ var builder = CreateBuilder(services);
+ var certificates = new X509Certificate2Collection
+ {
+ // Root certificate:
+ X509Certificate2.CreateFromPem($"""
+ -----BEGIN CERTIFICATE-----
+ MIIE7jCCAtagAwIBAgIJAM1FBDwXuKX8MA0GCSqGSIb3DQEBCwUAMBIxEDAOBgNV
+ BAMTB1Jvb3QgQ0EwIBcNMjYwMTMwMTczODU1WhgPMjEyNjAxMzExNzM4NTVaMBIx
+ EDAOBgNVBAMTB1Jvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC
+ AQDt/Nz3V5bKbYrJoITYrk3dL9CF+rDcMM6VyJ1pj33feXMTCcRJXkydKDc25sX+
+ 2C/yrx75zxcUBRoWzsg++YdHJffcZI0+6Q7XBOndNKflCL8lBihvT/EUO9MQYRWV
+ 495ie3deXoVGebrl6XbGAfm755Ml8KikYFryU6WOxApfQxjcKxnQLNLCTkIyn3WE
+ lzce9awtECPgdQfjDzCmE32xXMQcn/0HQUDxiyGvBQbBf8ZM9h3iJz4VGGVQkE7m
+ Ix15CwjAyrPc7jGAnUx00QGOCGzfT4bQaHOAZMgEJ8/KJhmx0Fmd6KR1fNJDqDvx
+ JYW683y1P512QZgN16e2ZEOdg9fsuXV/PaS6NmHOh7s/hwZsoIf3CJ5dX/M1h1BA
+ 4buxlvRfeZdANQHJLuQFPC4DQ8SWgbxXhL8KCo0jS9rUPTaxikL7+prFK/t39YFt
+ LzowUL8d+sMvUrn3v9yXb363wBB7fja1ZG1EOl7r2YO1uGleRR1ztymfRVziQ/Np
+ wRjDeBb9rWL9srPPilvo+5VsJe2a/XtfZuxMoH6vNEl04W6/iyYE4cVizwRC8GTW
+ hSHdhk2vT2/eyGSK3Cj5U74x+orHD+3XS6xHd63qB1oo+hJl2Ln/7pBAm9qFnNed
+ u6Wn/++Oi7M7nMU/ngEkbPKUfwrR/fEKQuweJaXTgiqj3QIDAQABo0UwQzASBgNV
+ HRMBAf8ECDAGAQH/AgEBMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUKv1o4gZN
+ ED0vspeWdqb0WeS3N04wDQYJKoZIhvcNAQELBQADggIBACLp5z1zaemqoFPtQ5Sr
+ Ii2ijs03Gc52Y/Pbg1V83xg1nMa+vI4aQYc90FZnNOv7I4VAmR6I3cI5bA3tnrzB
+ /yCMOkdxiFt6W0OQPMlmlVdCbPtUqXWM3tLilRn90XYEEWZB8I1sOrk2WH7oEHmu
+ W7BC2I3igjhUDug2bl7VwdBXzRJrWFgYdhVsjRU9rx3AbZqbD/3pC9B3PcwZxTGz
+ k8wRMP/9cF49VvUFVWhp01Bol1StwgX3r6IbaamDdIJd0MvHp0ctgqL6PLRzgRRY
+ adzWX8SPlgARVCixSOkXmAGNeuNWT/Ulo0W1xaNVTvOssfx58v3lwnjoaMLi7UFA
+ tCPrLZtZYvScB0+0AjUwfIfKrHqRdP5OqJZi7PhahR39tTh9pQX63INQvKYKO583
+ iidiNDau4HU+e+ujnXR5xJdgNWtuehKRdZFlLBH0lKKXrrnPyW9YtIjCVf1zhc+L
+ VSyo/aajWKBGqyTTbM1zetGe1rah97i7/0snoh97sWHlenmSX3BQUnajLkp9ZMh0
+ vY4koR1fxIlxHnOQ7SebZ23QiWUjdmJoOXW6Bub6VhWMDY96EpMr17QiGtexbUHD
+ 1nm2Z0xGwyWeyg2QSnNEAIZ6F2EeaZ6jmD/5ASXujcutNGyPwsSGkog4/Ir5Ol15
+ +3OP4DTt75BHxzMUxB6XmbYN
+ -----END CERTIFICATE-----
+ """),
+
+ // Intermediate certificate:
+ X509Certificate2.CreateFromPem($"""
+ -----BEGIN CERTIFICATE-----
+ MIIFRDCCAyygAwIBAgIRALpTKvDtz6lGPaqNaK8aULowDQYJKoZIhvcNAQELBQAw
+ EjEQMA4GA1UEAxMHUm9vdCBDQTAgFw0yNjAxMzAxNzM4NTVaGA8yMTI2MDEzMTE3
+ Mzg1NVowGjEYMBYGA1UEAxMPSW50ZXJtZWRpYXRlIENBMIICIjANBgkqhkiG9w0B
+ AQEFAAOCAg8AMIICCgKCAgEAvHiS4aNz7vL5mOJNjbpybcK75RhH1sXifLwKW8Zg
+ nHm+KjdRENf3X9yp7c+xNrtpHhG4/gp8M++0G1Cz4Yvq8idZu8IpMiqk9/KT447b
+ VocaRPCFC4NIC9U6g4s3rwHLUr2wMCAWiM9yjWcbXcvIlnSuA/i/lSAfAUPjrn8X
+ LLDgqlEkInmWRvYvDmdmw7vdqfDobFTDh0YRWB/y/LuDvkPFBDg3cfY8+AyrDkha
+ y3m1Ot3NTsg0O/HOL6MXMN9HRd4vX37XBV88kZtFE+vyHdYDs2NzGjAbfz4JZ6xz
+ 4+weUjklOc9ucAEgfAnwijH9w4KFBJEHAqtOMsbrIy74MvPTFj3LeayLo5nhLeqp
+ GbqvJcEX1UM83vFt+JUaDVbXDUG2ECHMDe6W5r5eYQtZW1ErKkRYNTJu++I0vDZr
+ EeZdYDYp15dbksMXUDyzhJ0WS0N23b7s57S6YAbok97UD/d+aGMtY3kJ/wIiftYY
+ Sel/MO/QXrgNchnVtUbShgE2oFvAJUYRvlZarG9/egp3Jb3B4WNjwIyzaT6SFtnG
+ Tg+IEXYPE2s5x4YZ8GINygWKrDbV7UuANRjKvoBlGmcrW/iz2Aaa+H5696p1HLVA
+ k7gXTw7WlJxzP6JPs2ZjWu27k88oAUV8HJjzFzGUsRPIjkf8KcJuxAqwLPoqFelh
+ uNECAwEAAaOBijCBhzASBgNVHRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIB
+ BjAdBgNVHQ4EFgQUq7mtVl4BCJyLWTHEpSCqokobTCcwQgYDVR0jBDswOYAUKv1o
+ 4gZNED0vspeWdqb0WeS3N06hFqQUMBIxEDAOBgNVBAMTB1Jvb3QgQ0GCCQDNRQQ8
+ F7il/DANBgkqhkiG9w0BAQsFAAOCAgEArlp0WSTwHgv8wgI+XT/QNxUQBiVyrHql
+ SHIMCBA7rDPPsl2RURWzQDE7zqovA3r7fnrYMfVXAAdgzXhDLQwL15RdaeoZUsjH
+ xN4y5Mtn0zv1yp7PPtZUc0mZ4Q0xWo4MPve82IfhiqWXretUxvcZ4NKY3sni0s8W
+ hViZdHH77vVIWWcWK414cpRwvsDtaKkgS4h8yHiUOtlgKgTViyUd0ovphR0boLtF
+ Ddw+jmLGM9c5keIs87RCTqCcHD4nP81kHHUaE60NDMtHH5UONSA5ecsHo11tC1am
+ 9U2TRs5+zwyBnwy4oOE/EZxXslcz27XyAX7MOhZppue+xtEDyex4gjiS27Nl8Va1
+ R1I1vkI5A209OQQ4JXzJZcAtgWep/ez0hu8TOkdtn0l/6aGkj2l3iwVG8edjiwSz
+ nVSPaBFKRtrHPuk9uEqu1xtP2klMeJEs7a5bVOyBOzZksafDwVTSPdRnDJDxo/Rx
+ bGzSWWYqKNsDxyV9aVMZ1iABW2O7qh6eXbioICzWAWQyplLeihnZ1d0o0h9gk/Kt
+ dPuLATo/WSXBA3oDfdEjjEWhmEdj6mR92KrOTQUF7JkOM74ZGs/lCxXsCOva7Z0N
+ kZmmpiU3Dewr11+SUxGx8g/4Ba1FhW9pXI9jSYguzY1KY210nF2P5YLsz/HgIM2r
+ SVA+QXhvcj0=
+ -----END CERTIFICATE-----
+ """)
+ };
+
+ // Act and assert
+ var exception = Assert.Throws(() =>
+ builder.EnablePublicKeyInfrastructureClientCertificateAuthentication(certificates,
+ policy => policy.TrustMode = X509ChainTrustMode.System));
+
+ Assert.Equal(SR.GetResourceString(SR.ID0509), exception.Message);
+ }
+#endif
+
+ [Fact]
+ public void EnableSelfSignedClientCertificateAuthentication_ThrowsAnExceptionForNullConfiguration()
+ {
+ // Arrange
+ var services = CreateServices();
+ var builder = CreateBuilder(services);
+
+ // Act and assert
+ var exception = Assert.Throws(() =>
+ builder.EnableSelfSignedClientCertificateAuthentication(configuration: null!));
+
+ Assert.Equal("configuration", exception.ParamName);
+ }
+
+#if SUPPORTS_X509_CHAIN_POLICY_CUSTOM_TRUST_STORE
+ [Fact]
+ public void EnableSelfSignedClientCertificateAuthentication_PolicyIsCorrectlyConfigured()
+ {
+ // Arrange
+ var services = CreateServices();
+ var builder = CreateBuilder(services);
+
+ // Act
+ builder.EnableSelfSignedClientCertificateAuthentication();
+
+ var options = GetOptions(services);
+
+ // Assert
+ Assert.NotNull(options.SelfSignedClientCertificateChainPolicy);
+ Assert.Equal(X509ChainTrustMode.CustomRootTrust, options.SelfSignedClientCertificateChainPolicy.TrustMode);
+ Assert.Equal(X509RevocationMode.NoCheck, options.SelfSignedClientCertificateChainPolicy.RevocationMode);
+ Assert.Contains(options.SelfSignedClientCertificateChainPolicy.ApplicationPolicy.Cast(),
+ oid => oid.Value == ObjectIdentifiers.ExtendedKeyUsages.ClientAuthentication);
+ }
+
+ [Fact]
+ public void EnableSelfSignedClientCertificateAuthentication_ThrowsAnExceptionWhenTrustModeIsChanged()
+ {
+ // Arrange
+ var services = CreateServices();
+ var builder = CreateBuilder(services);
+
+ // Act and assert
+ var exception = Assert.Throws(() =>
+ builder.EnableSelfSignedClientCertificateAuthentication(
+ policy => policy.TrustMode = X509ChainTrustMode.System));
+
+ Assert.Equal(SR.GetResourceString(SR.ID0509), exception.Message);
+ }
+#endif
+
[Fact]
public void IgnoreAudiencePermissions_AudiencePermissionsAreIgnored()
{
@@ -1300,6 +1708,346 @@ public class OpenIddictServerBuilderTests
Assert.Contains(new Uri("http://localhost/endpoint-path"), options.EndSessionEndpointUris);
}
+ [Fact]
+ public void SetMtlsDeviceAuthorizationEndpointAliasUri_ThrowsExceptionWhenUriIsNull()
+ {
+ // Arrange
+ var services = CreateServices();
+ var builder = CreateBuilder(services);
+
+ // Act and assert
+ var exception = Assert.Throws(() => builder.SetMtlsDeviceAuthorizationEndpointAliasUri(uri: (null as Uri)!));
+ Assert.Equal("uri", exception.ParamName);
+ }
+
+ [Fact]
+ public void SetMtlsDeviceAuthorizationEndpointAliasUri_String_ThrowsExceptionWhenUriIsNull()
+ {
+ // Arrange
+ var services = CreateServices();
+ var builder = CreateBuilder(services);
+
+ // Act and assert
+ var exception = Assert.Throws(() => builder.SetMtlsDeviceAuthorizationEndpointAliasUri(uri: (null as string)!));
+ Assert.Equal("uri", exception.ParamName);
+ }
+
+ [Theory]
+ [InlineData(@"C:\")]
+ public void SetMtlsDeviceAuthorizationEndpointAliasUri_ThrowsExceptionForMalformedUri(string uri)
+ {
+ // Arrange
+ var services = CreateServices();
+ var builder = CreateBuilder(services);
+
+ // Act and assert
+ var exception = Assert.Throws(() => builder.SetMtlsDeviceAuthorizationEndpointAliasUri(new Uri(uri)));
+ Assert.Equal("uri", exception.ParamName);
+ Assert.Contains(SR.GetResourceString(SR.ID0072), exception.Message);
+ }
+
+ [Theory]
+ [InlineData("~/path")]
+ public void SetMtlsDeviceAuthorizationEndpointAliasUri_ThrowsExceptionForInvalidRelativeUri(string uri)
+ {
+ // Arrange
+ var services = CreateServices();
+ var builder = CreateBuilder(services);
+
+ // Act and assert
+ var exception = Assert.Throws(() => builder.SetMtlsDeviceAuthorizationEndpointAliasUri(new Uri(uri, UriKind.RelativeOrAbsolute)));
+ Assert.Equal("uri", exception.ParamName);
+ Assert.Contains(SR.FormatID0081("~"), exception.Message);
+ }
+
+ [Fact]
+ public void SetMtlsDeviceAuthorizationEndpointAliasUri_AddsUri()
+ {
+ // Arrange
+ var services = CreateServices();
+ var builder = CreateBuilder(services);
+
+ // Act
+ builder.SetMtlsDeviceAuthorizationEndpointAliasUri("http://localhost/endpoint-path");
+
+ var options = GetOptions(services);
+
+ // Assert
+ Assert.Equal(new Uri("http://localhost/endpoint-path"), options.MtlsDeviceAuthorizationEndpointAliasUri);
+ }
+
+ [Fact]
+ public void SetMtlsIntrospectionEndpointAliasUri_ThrowsExceptionWhenUriIsNull()
+ {
+ // Arrange
+ var services = CreateServices();
+ var builder = CreateBuilder(services);
+
+ // Act and assert
+ var exception = Assert.Throws(() => builder.SetMtlsIntrospectionEndpointAliasUri(uri: (null as Uri)!));
+ Assert.Equal("uri", exception.ParamName);
+ }
+
+ [Fact]
+ public void SetMtlsIntrospectionEndpointAliasUri_String_ThrowsExceptionWhenUriIsNull()
+ {
+ // Arrange
+ var services = CreateServices();
+ var builder = CreateBuilder(services);
+
+ // Act and assert
+ var exception = Assert.Throws(() => builder.SetMtlsIntrospectionEndpointAliasUri(uri: (null as string)!));
+ Assert.Equal("uri", exception.ParamName);
+ }
+
+ [Theory]
+ [InlineData(@"C:\")]
+ public void SetMtlsIntrospectionEndpointAliasUri_ThrowsExceptionForMalformedUri(string uri)
+ {
+ // Arrange
+ var services = CreateServices();
+ var builder = CreateBuilder(services);
+
+ // Act and assert
+ var exception = Assert.Throws(() => builder.SetMtlsIntrospectionEndpointAliasUri(new Uri(uri)));
+ Assert.Equal("uri", exception.ParamName);
+ Assert.Contains(SR.GetResourceString(SR.ID0072), exception.Message);
+ }
+
+ [Theory]
+ [InlineData("~/path")]
+ public void SetMtlsIntrospectionEndpointAliasUri_ThrowsExceptionForInvalidRelativeUri(string uri)
+ {
+ // Arrange
+ var services = CreateServices();
+ var builder = CreateBuilder(services);
+
+ // Act and assert
+ var exception = Assert.Throws(() => builder.SetMtlsIntrospectionEndpointAliasUri(new Uri(uri, UriKind.RelativeOrAbsolute)));
+ Assert.Equal("uri", exception.ParamName);
+ Assert.Contains(SR.FormatID0081("~"), exception.Message);
+ }
+
+ [Fact]
+ public void SetMtlsIntrospectionEndpointAliasUri_AddsUri()
+ {
+ // Arrange
+ var services = CreateServices();
+ var builder = CreateBuilder(services);
+
+ // Act
+ builder.SetMtlsIntrospectionEndpointAliasUri("http://localhost/endpoint-path");
+
+ var options = GetOptions(services);
+
+ // Assert
+ Assert.Equal(new Uri("http://localhost/endpoint-path"), options.MtlsIntrospectionEndpointAliasUri);
+ }
+
+ [Fact]
+ public void SetMtlsPushedAuthorizationEndpointAliasUri_ThrowsExceptionWhenUriIsNull()
+ {
+ // Arrange
+ var services = CreateServices();
+ var builder = CreateBuilder(services);
+
+ // Act and assert
+ var exception = Assert.Throws(() => builder.SetMtlsPushedAuthorizationEndpointAliasUri(uri: (null as Uri)!));
+ Assert.Equal("uri", exception.ParamName);
+ }
+
+ [Fact]
+ public void SetMtlsPushedAuthorizationEndpointAliasUri_String_ThrowsExceptionWhenUriIsNull()
+ {
+ // Arrange
+ var services = CreateServices();
+ var builder = CreateBuilder(services);
+
+ // Act and assert
+ var exception = Assert.Throws(() => builder.SetMtlsPushedAuthorizationEndpointAliasUri(uri: (null as string)!));
+ Assert.Equal("uri", exception.ParamName);
+ }
+
+ [Theory]
+ [InlineData(@"C:\")]
+ public void SetMtlsPushedAuthorizationEndpointAliasUri_ThrowsExceptionForMalformedUri(string uri)
+ {
+ // Arrange
+ var services = CreateServices();
+ var builder = CreateBuilder(services);
+
+ // Act and assert
+ var exception = Assert.Throws(() => builder.SetMtlsPushedAuthorizationEndpointAliasUri(new Uri(uri)));
+ Assert.Equal("uri", exception.ParamName);
+ Assert.Contains(SR.GetResourceString(SR.ID0072), exception.Message);
+ }
+
+ [Theory]
+ [InlineData("~/path")]
+ public void SetMtlsPushedAuthorizationEndpointAliasUri_ThrowsExceptionForInvalidRelativeUri(string uri)
+ {
+ // Arrange
+ var services = CreateServices();
+ var builder = CreateBuilder(services);
+
+ // Act and assert
+ var exception = Assert.Throws(() => builder.SetMtlsPushedAuthorizationEndpointAliasUri(new Uri(uri, UriKind.RelativeOrAbsolute)));
+ Assert.Equal("uri", exception.ParamName);
+ Assert.Contains(SR.FormatID0081("~"), exception.Message);
+ }
+
+ [Fact]
+ public void SetMtlsPushedAuthorizationEndpointAliasUri_AddsUri()
+ {
+ // Arrange
+ var services = CreateServices();
+ var builder = CreateBuilder(services);
+
+ // Act
+ builder.SetMtlsPushedAuthorizationEndpointAliasUri("http://localhost/endpoint-path");
+
+ var options = GetOptions(services);
+
+ // Assert
+ Assert.Equal(new Uri("http://localhost/endpoint-path"), options.MtlsPushedAuthorizationEndpointAliasUri);
+ }
+
+ [Fact]
+ public void SetMtlsRevocationEndpointAliasUri_ThrowsExceptionWhenUriIsNull()
+ {
+ // Arrange
+ var services = CreateServices();
+ var builder = CreateBuilder(services);
+
+ // Act and assert
+ var exception = Assert.Throws(() => builder.SetMtlsRevocationEndpointAliasUri(uri: (null as Uri)!));
+ Assert.Equal("uri", exception.ParamName);
+ }
+
+ [Fact]
+ public void SetMtlsRevocationEndpointAliasUri_String_ThrowsExceptionWhenUriIsNull()
+ {
+ // Arrange
+ var services = CreateServices();
+ var builder = CreateBuilder(services);
+
+ // Act and assert
+ var exception = Assert.Throws(() => builder.SetMtlsRevocationEndpointAliasUri(uri: (null as string)!));
+ Assert.Equal("uri", exception.ParamName);
+ }
+
+ [Theory]
+ [InlineData(@"C:\")]
+ public void SetMtlsRevocationEndpointAliasUri_ThrowsExceptionForMalformedUri(string uri)
+ {
+ // Arrange
+ var services = CreateServices();
+ var builder = CreateBuilder(services);
+
+ // Act and assert
+ var exception = Assert.Throws(() => builder.SetMtlsRevocationEndpointAliasUri(new Uri(uri)));
+ Assert.Equal("uri", exception.ParamName);
+ Assert.Contains(SR.GetResourceString(SR.ID0072), exception.Message);
+ }
+
+ [Theory]
+ [InlineData("~/path")]
+ public void SetMtlsRevocationEndpointAliasUri_ThrowsExceptionForInvalidRelativeUri(string uri)
+ {
+ // Arrange
+ var services = CreateServices();
+ var builder = CreateBuilder(services);
+
+ // Act and assert
+ var exception = Assert.Throws(() => builder.SetMtlsRevocationEndpointAliasUri(new Uri(uri, UriKind.RelativeOrAbsolute)));
+ Assert.Equal("uri", exception.ParamName);
+ Assert.Contains(SR.FormatID0081("~"), exception.Message);
+ }
+
+ [Fact]
+ public void SetMtlsRevocationEndpointAliasUri_AddsUri()
+ {
+ // Arrange
+ var services = CreateServices();
+ var builder = CreateBuilder(services);
+
+ // Act
+ builder.SetMtlsRevocationEndpointAliasUri("http://localhost/endpoint-path");
+
+ var options = GetOptions(services);
+
+ // Assert
+ Assert.Equal(new Uri("http://localhost/endpoint-path"), options.MtlsRevocationEndpointAliasUri);
+ }
+
+ [Fact]
+ public void SetMtlsTokenEndpointAliasUri_ThrowsExceptionWhenUriIsNull()
+ {
+ // Arrange
+ var services = CreateServices();
+ var builder = CreateBuilder(services);
+
+ // Act and assert
+ var exception = Assert.Throws(() => builder.SetMtlsTokenEndpointAliasUri(uri: (null as Uri)!));
+ Assert.Equal("uri", exception.ParamName);
+ }
+
+ [Fact]
+ public void SetMtlsTokenEndpointAliasUri_String_ThrowsExceptionWhenUriIsNull()
+ {
+ // Arrange
+ var services = CreateServices();
+ var builder = CreateBuilder(services);
+
+ // Act and assert
+ var exception = Assert.Throws(() => builder.SetMtlsTokenEndpointAliasUri(uri: (null as string)!));
+ Assert.Equal("uri", exception.ParamName);
+ }
+
+ [Theory]
+ [InlineData(@"C:\")]
+ public void SetMtlsTokenEndpointAliasUri_ThrowsExceptionForMalformedUri(string uri)
+ {
+ // Arrange
+ var services = CreateServices();
+ var builder = CreateBuilder(services);
+
+ // Act and assert
+ var exception = Assert.Throws(() => builder.SetMtlsTokenEndpointAliasUri(new Uri(uri)));
+ Assert.Equal("uri", exception.ParamName);
+ Assert.Contains(SR.GetResourceString(SR.ID0072), exception.Message);
+ }
+
+ [Theory]
+ [InlineData("~/path")]
+ public void SetMtlsTokenEndpointAliasUri_ThrowsExceptionForInvalidRelativeUri(string uri)
+ {
+ // Arrange
+ var services = CreateServices();
+ var builder = CreateBuilder(services);
+
+ // Act and assert
+ var exception = Assert.Throws(() => builder.SetMtlsTokenEndpointAliasUri(new Uri(uri, UriKind.RelativeOrAbsolute)));
+ Assert.Equal("uri", exception.ParamName);
+ Assert.Contains(SR.FormatID0081("~"), exception.Message);
+ }
+
+ [Fact]
+ public void SetMtlsTokenEndpointAliasUri_AddsUri()
+ {
+ // Arrange
+ var services = CreateServices();
+ var builder = CreateBuilder(services);
+
+ // Act
+ builder.SetMtlsTokenEndpointAliasUri("http://localhost/endpoint-path");
+
+ var options = GetOptions(services);
+
+ // Assert
+ Assert.Equal(new Uri("http://localhost/endpoint-path"), options.MtlsTokenEndpointAliasUri);
+ }
+
[Fact]
public void SetIntrospectionEndpointUris_ThrowsExceptionWhenUrisIsNull()
{