Browse Source

Implement native mTLS client authentication support in the server stack

pull/2419/head
Kévin Chalet 4 days ago
parent
commit
5c1cda0ac5
  1. 6
      Directory.Build.targets
  2. 7
      gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs
  3. 2
      sandbox/OpenIddict.Sandbox.AspNet.Client/Startup.cs
  4. 2
      sandbox/OpenIddict.Sandbox.AspNet.Server/Startup.cs
  5. 231
      sandbox/OpenIddict.Sandbox.AspNetCore.Client/Startup.cs
  6. 156
      sandbox/OpenIddict.Sandbox.AspNetCore.Server/Startup.cs
  7. 37
      sandbox/OpenIddict.Sandbox.AspNetCore.Server/Worker.cs
  8. 101
      shared/OpenIddict.Extensions/OpenIddictHelpers.cs
  9. 52
      shared/OpenIddict.Extensions/OpenIddictPolyfills.cs
  10. 62
      src/OpenIddict.Abstractions/Managers/IOpenIddictApplicationManager.cs
  11. 4
      src/OpenIddict.Abstractions/Managers/IOpenIddictAuthorizationManager.cs
  12. 4
      src/OpenIddict.Abstractions/Managers/IOpenIddictScopeManager.cs
  13. 4
      src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs
  14. 14
      src/OpenIddict.Abstractions/OpenIddictConstants.cs
  15. 82
      src/OpenIddict.Abstractions/OpenIddictResources.resx
  16. 2
      src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreConfiguration.cs
  17. 63
      src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpConfiguration.cs
  18. 22
      src/OpenIddict.Client/OpenIddictClientBuilder.cs
  19. 9
      src/OpenIddict.Client/OpenIddictClientHandlers.Discovery.cs
  20. 277
      src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs
  21. 23
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConfiguration.cs
  22. 5
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreExtensions.cs
  23. 1
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Authentication.cs
  24. 1
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Device.cs
  25. 1
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Exchange.cs
  26. 1
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Introspection.cs
  27. 1
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Revocation.cs
  28. 88
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs
  29. 19
      src/OpenIddict.Server.Owin/OpenIddictServerOwinConfiguration.cs
  30. 3
      src/OpenIddict.Server.Owin/OpenIddictServerOwinExtensions.cs
  31. 1
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Authentication.cs
  32. 1
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Device.cs
  33. 1
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Exchange.cs
  34. 1
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Introspection.cs
  35. 1
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Revocation.cs
  36. 121
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs
  37. 443
      src/OpenIddict.Server/OpenIddictServerBuilder.cs
  38. 110
      src/OpenIddict.Server/OpenIddictServerConfiguration.cs
  39. 25
      src/OpenIddict.Server/OpenIddictServerEvents.Discovery.cs
  40. 10
      src/OpenIddict.Server/OpenIddictServerEvents.cs
  41. 1
      src/OpenIddict.Server/OpenIddictServerExtensions.cs
  42. 14
      src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs
  43. 49
      src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs
  44. 2
      src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs
  45. 134
      src/OpenIddict.Server/OpenIddictServerHandlers.cs
  46. 89
      src/OpenIddict.Server/OpenIddictServerOptions.cs
  47. 6
      src/OpenIddict.Server/OpenIddictServerTransaction.cs
  48. 2
      src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreConfiguration.cs
  49. 62
      src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpConfiguration.cs
  50. 18
      src/OpenIddict.Validation/OpenIddictValidationBuilder.cs
  51. 7
      src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs
  52. 2
      src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs
  53. 124
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Authentication.cs
  54. 118
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Device.cs
  55. 65
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Discovery.cs
  56. 127
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs
  57. 118
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Introspection.cs
  58. 124
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Revocation.cs
  59. 1162
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs
  60. 756
      test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs

6
Directory.Build.targets

@ -68,6 +68,7 @@
<DefineConstants>$(DefineConstants);SUPPORTS_HTTP_CLIENT_DEFAULT_REQUEST_VERSION_POLICY</DefineConstants> <DefineConstants>$(DefineConstants);SUPPORTS_HTTP_CLIENT_DEFAULT_REQUEST_VERSION_POLICY</DefineConstants>
<DefineConstants>$(DefineConstants);SUPPORTS_HTTP_CLIENT_RESILIENCE</DefineConstants> <DefineConstants>$(DefineConstants);SUPPORTS_HTTP_CLIENT_RESILIENCE</DefineConstants>
<DefineConstants>$(DefineConstants);SUPPORTS_INT32_RANDOM_NUMBER_GENERATOR_METHODS</DefineConstants> <DefineConstants>$(DefineConstants);SUPPORTS_INT32_RANDOM_NUMBER_GENERATOR_METHODS</DefineConstants>
<DefineConstants>$(DefineConstants);SUPPORTS_KESTREL_TLS_HANDSHAKE_CALLBACK_OPTIONS</DefineConstants>
<DefineConstants>$(DefineConstants);SUPPORTS_MULTIPLE_VALUES_IN_QUERYHELPERS</DefineConstants> <DefineConstants>$(DefineConstants);SUPPORTS_MULTIPLE_VALUES_IN_QUERYHELPERS</DefineConstants>
<DefineConstants>$(DefineConstants);SUPPORTS_NAMED_PIPE_STATIC_FACTORY_WITH_ACL</DefineConstants> <DefineConstants>$(DefineConstants);SUPPORTS_NAMED_PIPE_STATIC_FACTORY_WITH_ACL</DefineConstants>
<DefineConstants>$(DefineConstants);SUPPORTS_ONE_SHOT_HASHING_METHODS</DefineConstants> <DefineConstants>$(DefineConstants);SUPPORTS_ONE_SHOT_HASHING_METHODS</DefineConstants>
@ -78,6 +79,9 @@
<DefineConstants>$(DefineConstants);SUPPORTS_TEXT_ELEMENT_ENUMERATOR</DefineConstants> <DefineConstants>$(DefineConstants);SUPPORTS_TEXT_ELEMENT_ENUMERATOR</DefineConstants>
<DefineConstants>$(DefineConstants);SUPPORTS_VALUETASK_COMPLETED_TASK</DefineConstants> <DefineConstants>$(DefineConstants);SUPPORTS_VALUETASK_COMPLETED_TASK</DefineConstants>
<DefineConstants>$(DefineConstants);SUPPORTS_WINFORMS_TASK_DIALOG</DefineConstants> <DefineConstants>$(DefineConstants);SUPPORTS_WINFORMS_TASK_DIALOG</DefineConstants>
<DefineConstants>$(DefineConstants);SUPPORTS_X509_CHAIN_POLICY_CUSTOM_TRUST_STORE</DefineConstants>
<DefineConstants>$(DefineConstants);SUPPORTS_X509_CHAIN_POLICY_DOWNLOAD_MODE</DefineConstants>
<DefineConstants>$(DefineConstants);SUPPORTS_X509_CHAIN_POLICY_TRUST_MODE</DefineConstants>
<DefineConstants>$(DefineConstants);SUPPORTS_ZLIB_COMPRESSION</DefineConstants> <DefineConstants>$(DefineConstants);SUPPORTS_ZLIB_COMPRESSION</DefineConstants>
</PropertyGroup> </PropertyGroup>
@ -114,6 +118,8 @@
<DefineConstants>$(DefineConstants);SUPPORTS_JSON_ELEMENT_DEEP_EQUALS</DefineConstants> <DefineConstants>$(DefineConstants);SUPPORTS_JSON_ELEMENT_DEEP_EQUALS</DefineConstants>
<DefineConstants>$(DefineConstants);SUPPORTS_JSON_ELEMENT_PROPERTY_COUNT</DefineConstants> <DefineConstants>$(DefineConstants);SUPPORTS_JSON_ELEMENT_PROPERTY_COUNT</DefineConstants>
<DefineConstants>$(DefineConstants);SUPPORTS_TYPE_DESCRIPTOR_TYPE_REGISTRATION</DefineConstants> <DefineConstants>$(DefineConstants);SUPPORTS_TYPE_DESCRIPTOR_TYPE_REGISTRATION</DefineConstants>
<DefineConstants>$(DefineConstants);SUPPORTS_X509_CHAIN_POLICY_CLONING</DefineConstants>
<DefineConstants>$(DefineConstants);SUPPORTS_X509_CHAIN_POLICY_VERIFICATION_TIME_MODE</DefineConstants>
</PropertyGroup> </PropertyGroup>
<PropertyGroup <PropertyGroup

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

@ -628,7 +628,7 @@ public sealed partial class OpenIddictClientWebIntegrationBuilder
store.Open(OpenFlags.ReadOnly); store.Open(OpenFlags.ReadOnly);
return store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false) return store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false)
.OfType<X509Certificate2>() .Cast<X509Certificate2>()
.SingleOrDefault(); .SingleOrDefault();
} }
} }
@ -652,7 +652,7 @@ public sealed partial class OpenIddictClientWebIntegrationBuilder
return Set{{ setting.property_name }}( return Set{{ setting.property_name }}(
store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false) store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false)
.OfType<X509Certificate2>() .Cast<X509Certificate2>()
.SingleOrDefault() ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0066))); .SingleOrDefault() ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0066)));
} }
{{~ else if setting.clr_type == 'bool' ~}} {{~ else if setting.clr_type == 'bool' ~}}
@ -1163,8 +1163,7 @@ public sealed partial class OpenIddictClientWebIntegrationConfiguration
if (settings.{{ setting.property_name }} is not null) if (settings.{{ setting.property_name }} is not null)
{ {
// If the signing key is an asymmetric security key, ensure it has a private key. // If the signing key is an asymmetric security key, ensure it has a private key.
if (settings.{{ setting.property_name }} is AsymmetricSecurityKey asymmetricSecurityKey && if (settings.{{ setting.property_name }} is AsymmetricSecurityKey { PrivateKeyStatus: PrivateKeyStatus.DoesNotExist })
asymmetricSecurityKey.PrivateKeyStatus is PrivateKeyStatus.DoesNotExist)
{ {
throw new InvalidOperationException(SR.GetResourceString(SR.ID0067)); throw new InvalidOperationException(SR.GetResourceString(SR.ID0067));
} }

2
sandbox/OpenIddict.Sandbox.AspNet.Client/Startup.cs

@ -71,7 +71,7 @@ public class Startup
ProviderDisplayName = "Local OIDC server", ProviderDisplayName = "Local OIDC server",
ClientId = "mvc", ClientId = "mvc",
ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654", ClientSecret = "emCimpdc9SeOaZzN5jzm4_eek-STF6VenfVlKO1_qt0",
Scopes = { Scopes.Email, Scopes.Profile, Scopes.OfflineAccess, "demo_api" }, Scopes = { Scopes.Email, Scopes.Profile, Scopes.OfflineAccess, "demo_api" },
RedirectUri = new Uri("callback/login/local", UriKind.Relative), RedirectUri = new Uri("callback/login/local", UriKind.Relative),

2
sandbox/OpenIddict.Sandbox.AspNet.Server/Startup.cs

@ -199,7 +199,7 @@ public class Startup
{ {
ApplicationType = ApplicationTypes.Web, ApplicationType = ApplicationTypes.Web,
ClientId = "mvc", ClientId = "mvc",
ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654", ClientSecret = "emCimpdc9SeOaZzN5jzm4_eek-STF6VenfVlKO1_qt0",
ClientType = ClientTypes.Confidential, ClientType = ClientTypes.Confidential,
ConsentType = ConsentTypes.Systematic, ConsentType = ConsentTypes.Systematic,
DisplayName = "MVC client application", DisplayName = "MVC client application",

231
sandbox/OpenIddict.Sandbox.AspNetCore.Client/Startup.cs

@ -1,4 +1,5 @@
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
@ -112,26 +113,52 @@ public class Startup
PostLogoutRedirectUri = new Uri("callback/logout/local", UriKind.Relative), PostLogoutRedirectUri = new Uri("callback/logout/local", UriKind.Relative),
#if SUPPORTS_PEM_ENCODED_KEY_IMPORT #if SUPPORTS_PEM_ENCODED_KEY_IMPORT
// On supported platforms, this application authenticates by generating JWT client // On supported platforms, this application can authenticate using 3 different client
// assertions that are signed using a signing key instead of using a client secret. // 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 // 1) tls_client_auth (PKI-based mutual TLS authentication): while it requires
// the OpenIddict client to automatically generate client assertions when needed. // 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 // 2) self_signed_tls_client_auth (self-signed certificate-based mutual TLS authentication):
// to know the private key to be able to generate and sign the client assertions. // 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 = SigningCredentials =
{ {
new SigningCredentials(GetECDsaSigningKey($""" // Note: this certificate can be used with either tls_client_auth or private_key_jwt,
-----BEGIN EC PRIVATE KEY----- // depending on the server configuration (and the client authentication methods explicitly
MHcCAQEEIMGxf/eMzKuW2F8KKWPJo3bwlrO68rK5+xCeO1atwja2oAoGCCqGSM49 // configured via OpenIddictClientRegistration.ClientAuthenticationMethods, if applicable).
AwEHoUQDQgAEI23kaVsRRAWIez/pqEZOByJFmlXda6iSQ4QqcH23Ir8aYPPX5lsV //
nBsExNsl7SOYOiIhgTaX6+PTS7yxTnmvSw== // GetPublicKeyInfrastructureCertificate(),
-----END EC PRIVATE KEY-----
"""), SecurityAlgorithms.EcdsaSha256, SecurityAlgorithms.Sha256) // 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 #else
ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654" ClientSecret = "emCimpdc9SeOaZzN5jzm4_eek-STF6VenfVlKO1_qt0"
#endif #endif
}); });
@ -165,13 +192,181 @@ public class Startup
}); });
#if SUPPORTS_PEM_ENCODED_KEY_IMPORT #if SUPPORTS_PEM_ENCODED_KEY_IMPORT
static ECDsaSecurityKey GetECDsaSigningKey(ReadOnlySpan<char> 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(); 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 #endif
}); });

156
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.Identity;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.Server.Kestrel.Https;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using OpenIddict.Sandbox.AspNetCore.Server.Models; using OpenIddict.Sandbox.AspNetCore.Server.Models;
using OpenIddict.Sandbox.AspNetCore.Server.Services; using OpenIddict.Sandbox.AspNetCore.Server.Services;
@ -158,6 +162,106 @@ public class Startup
// you don't own, you can disable access token encryption: // you don't own, you can disable access token encryption:
// //
// options.DisableAccessTokenEncryption(); // 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. // Register the OpenIddict validation components.
@ -178,7 +282,7 @@ public class Startup
// options.UseIntrospection() // options.UseIntrospection()
// .SetIssuer("https://localhost:44395/") // .SetIssuer("https://localhost:44395/")
// .SetClientId("resource_server") // .SetClientId("resource_server")
// .SetClientSecret("80B552BB-4CD8-48DA-946E-0815E0147DD2"); // .SetClientSecret("vVQ-yjr42sXP5VHj6AswkXuS7MU1i2gFjvJjY0TdGMk");
// //
// When introspection is used, the System.Net.Http integration must be enabled. // 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. // 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. // Note: in a real world application, this step should be part of a setup script.
services.AddHostedService<Worker>(); services.AddHostedService<Worker>();
#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<KestrelServerOptions>(options => options.ListenAnyIP(44395, options =>
{
options.UseHttps(new TlsHandshakeCallbackOptions
{
OnConnection = GetServerAuthenticationOptionsAsync
});
}));
static ValueTask<SslServerAuthenticationOptions> 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<X509Certificate2>()
.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) public void Configure(IApplicationBuilder app)

37
sandbox/OpenIddict.Sandbox.AspNetCore.Server/Worker.cs

@ -1,5 +1,6 @@
using System.Globalization; using System.Globalization;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using OpenIddict.Abstractions; using OpenIddict.Abstractions;
using OpenIddict.Sandbox.AspNetCore.Server.Models; using OpenIddict.Sandbox.AspNetCore.Server.Models;
@ -157,8 +158,36 @@ public class Worker : IHostedService
{ {
Keys = Keys =
{ {
// On supported platforms, this application authenticates by generating JWT client // On supported platforms, this application can authenticate by using a
// assertions that are signed using a signing key instead of using a client secret. // 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 // 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. // to know the public key to be able to validate the client assertions it receives.
@ -171,7 +200,7 @@ public class Worker : IHostedService
} }
}, },
#else #else
ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654", ClientSecret = "emCimpdc9SeOaZzN5jzm4_eek-STF6VenfVlKO1_qt0",
#endif #endif
RedirectUris = RedirectUris =
{ {
@ -306,7 +335,7 @@ public class Worker : IHostedService
var descriptor = new OpenIddictApplicationDescriptor var descriptor = new OpenIddictApplicationDescriptor
{ {
ClientId = "resource_server", ClientId = "resource_server",
ClientSecret = "80B552BB-4CD8-48DA-946E-0815E0147DD2", ClientSecret = "vVQ-yjr42sXP5VHj6AswkXuS7MU1i2gFjvJjY0TdGMk",
ClientType = ClientTypes.Confidential, ClientType = ClientTypes.Confidential,
Permissions = Permissions =
{ {

101
shared/OpenIddict.Extensions/OpenIddictHelpers.cs

@ -11,6 +11,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Nodes; using System.Text.Json.Nodes;
@ -196,7 +197,7 @@ internal static class OpenIddictHelpers
ArgumentNullException.ThrowIfNull(uri); ArgumentNullException.ThrowIfNull(uri);
var builder = new StringBuilder(uri.Query); var builder = new StringBuilder(uri.Query);
if (builder.Length > 0) if (builder.Length is > 0)
{ {
builder.Append('&'); builder.Append('&');
} }
@ -238,7 +239,7 @@ internal static class OpenIddictHelpers
// only append the parameter key to the query string. // only append the parameter key to the query string.
if (parameter.Value.Count is 0) if (parameter.Value.Count is 0)
{ {
if (builder.Length > 0) if (builder.Length is > 0)
{ {
builder.Append('&'); builder.Append('&');
} }
@ -252,7 +253,7 @@ internal static class OpenIddictHelpers
{ {
foreach (var value in parameter.Value) foreach (var value in parameter.Value)
{ {
if (builder.Length > 0) if (builder.Length is > 0)
{ {
builder.Append('&'); builder.Append('&');
} }
@ -286,7 +287,7 @@ internal static class OpenIddictHelpers
.Select(static parameter => parameter.Split(Separators.EqualsSign, StringSplitOptions.RemoveEmptyEntries)) .Select(static parameter => parameter.Split(Separators.EqualsSign, StringSplitOptions.RemoveEmptyEntries))
.Select(static parts => ( .Select(static parts => (
Key: parts[0] is string key ? Uri.UnescapeDataString(key) : null, 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)) .Where(static pair => !string.IsNullOrEmpty(pair.Key))
.GroupBy(static pair => pair.Key) .GroupBy(static pair => pair.Key)
.ToDictionary(static pair => pair.Key!, static pair => new StringValues([.. pair.Select(parts => parts.Value)])); .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 parameter => parameter.Split(Separators.EqualsSign, StringSplitOptions.RemoveEmptyEntries))
.Select(static parts => ( .Select(static parts => (
Key: parts[0] is string key ? Uri.UnescapeDataString(key) : null, 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)) .Where(static pair => !string.IsNullOrEmpty(pair.Key))
.GroupBy(static pair => pair.Key) .GroupBy(static pair => pair.Key)
.ToDictionary(static pair => pair.Key!, static pair => new StringValues([.. pair.Select(parts => parts.Value)])); .ToDictionary(static pair => pair.Key!, static pair => new StringValues([.. pair.Select(parts => parts.Value)]));
@ -998,7 +999,7 @@ internal static class OpenIddictHelpers
/// </summary> /// </summary>
/// <param name="node">The <see cref="JsonNode"/>.</param> /// <param name="node">The <see cref="JsonNode"/>.</param>
/// <returns> /// <returns>
/// <see langword="true"/> if the JSON node is null or empty <see langword="false"/> otherwise. /// <see langword="true"/> if the JSON node is null or empty, <see langword="false"/> otherwise.
/// </returns> /// </returns>
public static bool IsNullOrEmpty([NotNullWhen(false)] JsonNode? node) => node switch 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)) JsonNode value => IsNullOrEmpty(value.Deserialize(OpenIddictSerializer.Default.JsonElement))
}; };
/// <summary>
/// Determines whether the specified <paramref name="certificate"/> is a certificate authority.
/// </summary>
/// <param name="certificate">The <see cref="X509Certificate2"/>.</param>
/// <returns>
/// <see langword="true"/> if the certificate is a certificate authority, <see langword="false"/> otherwise.
/// </returns>
public static bool IsCertificateAuthority(X509Certificate2 certificate)
{
ArgumentNullException.ThrowIfNull(certificate);
return certificate.Extensions.OfType<X509BasicConstraintsExtension>()
.Any(static extension => extension.CertificateAuthority);
}
/// <summary>
/// Determines whether the specified <paramref name="certificate"/> has the specified extended key usage.
/// </summary>
/// <param name="certificate">The <see cref="X509Certificate2"/>.</param>
/// <param name="usage">The extended key usage.</param>
/// <returns>
/// <see langword="true"/> if the certificate has the specified extended key usage, <see langword="false"/> otherwise.
/// </returns>
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;
}
}
/// <summary>
/// Determines whether the specified <paramref name="certificate"/> has the specified key usage.
/// </summary>
/// <param name="certificate">The <see cref="X509Certificate2"/>.</param>
/// <param name="usage">The <see cref="X509KeyUsageFlags"/>.</param>
/// <returns>
/// <see langword="true"/> if the certificate has the specified key usage, <see langword="false"/> otherwise.
/// </returns>
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;
}
/// <summary>
/// Determines whether the specified <paramref name="certificate"/> is self-issued.
/// </summary>
/// <param name="certificate">The <see cref="X509Certificate2"/>.</param>
/// <returns>
/// <see langword="true"/> if the certificate is self-issued, <see langword="false"/> otherwise.
/// </returns>
public static bool IsSelfIssuedCertificate(X509Certificate2 certificate)
{
ArgumentNullException.ThrowIfNull(certificate);
return certificate.SubjectName.RawData.AsSpan().SequenceEqual(certificate.IssuerName.RawData);
}
/// <summary> /// <summary>
/// Determines whether the items contained in <paramref name="element"/> /// Determines whether the items contained in <paramref name="element"/>
/// are of the specified <paramref name="kind"/>. /// are of the specified <paramref name="kind"/>.

52
shared/OpenIddict.Extensions/OpenIddictPolyfills.cs

@ -8,6 +8,8 @@ using System.Diagnostics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Runtime.Versioning; using System.Runtime.Versioning;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
namespace OpenIddict.Extensions; namespace OpenIddict.Extensions;
@ -303,4 +305,54 @@ internal static class OpenIddictPolyfills
return currentRevision >= revision; return currentRevision >= revision;
} }
#endif #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
}
} }

62
src/OpenIddict.Abstractions/Managers/IOpenIddictApplicationManager.cs

@ -8,6 +8,7 @@ using System.Collections.Immutable;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
using System.Security.Cryptography.X509Certificates;
using System.Text.Json; using System.Text.Json;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
@ -15,12 +16,14 @@ namespace OpenIddict.Abstractions;
/// <summary> /// <summary>
/// Provides methods allowing to manage the applications stored in the store. /// Provides methods allowing to manage the applications stored in the store.
/// </summary>
/// <remarks>
/// Note: this interface is not meant to be implemented by custom managers, /// Note: this interface is not meant to be implemented by custom managers,
/// that should inherit from the generic OpenIddictApplicationManager class. /// that should inherit from the generic OpenIddictApplicationManager class.
/// It is primarily intended to be used by services that cannot easily depend /// It is primarily intended to be used by services that cannot easily depend
/// on the generic application manager. The actual application entity type /// on the generic application manager. The actual application entity type
/// is automatically determined at runtime based on the OpenIddict core options. /// is automatically determined at runtime based on the OpenIddict core options.
/// </summary> /// </remarks>
public interface IOpenIddictApplicationManager public interface IOpenIddictApplicationManager
{ {
/// <summary> /// <summary>
@ -171,6 +174,19 @@ public interface IOpenIddictApplicationManager
Func<IQueryable<object>, TState, IQueryable<TResult>> query, Func<IQueryable<object>, TState, IQueryable<TResult>> query,
TState state, CancellationToken cancellationToken = default); TState state, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves the client certificate chain policy enforced for this application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="policy">The base policy from which the returned instance will be derived.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client certificate chain policy enforced for this application.
/// </returns>
ValueTask<X509ChainPolicy?> GetClientCertificateChainPolicyAsync(
object application, X509ChainPolicy policy, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
/// Retrieves the client identifier associated with an application. /// Retrieves the client identifier associated with an application.
/// </summary> /// </summary>
@ -330,6 +346,19 @@ public interface IOpenIddictApplicationManager
/// </returns> /// </returns>
ValueTask<ImmutableArray<string>> GetRequirementsAsync(object application, CancellationToken cancellationToken = default); ValueTask<ImmutableArray<string>> GetRequirementsAsync(object application, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves the self-signed client certificate chain policy enforced for this application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="policy">The base policy from which the returned instance will be derived.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation, whose
/// result returns the self-signed client certificate chain policy enforced for this application.
/// </returns>
ValueTask<X509ChainPolicy?> GetSelfSignedClientCertificateChainPolicyAsync(
object application, X509ChainPolicy policy, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
/// Retrieves the settings associated with an application. /// Retrieves the settings associated with an application.
/// </summary> /// </summary>
@ -475,6 +504,21 @@ public interface IOpenIddictApplicationManager
/// </returns> /// </returns>
ValueTask UpdateAsync(object application, string secret, CancellationToken cancellationToken = default); ValueTask UpdateAsync(object application, string secret, CancellationToken cancellationToken = default);
/// <summary>
/// Validates the client certificate associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="certificate">The certificate that should be compared to the certificates associated with the application.</param>
/// <param name="policy">The chain policy used to validate the certificate.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.</returns>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// whose result returns a boolean indicating whether the client certificate was valid.
/// </returns>
ValueTask<bool> ValidateClientCertificateAsync(object application,
X509Certificate2 certificate, X509ChainPolicy policy, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
/// Validates the application to ensure it's in a consistent state. /// Validates the application to ensure it's in a consistent state.
/// </summary> /// </summary>
@ -522,4 +566,20 @@ public interface IOpenIddictApplicationManager
/// </returns> /// </returns>
ValueTask<bool> ValidateRedirectUriAsync(object application, ValueTask<bool> ValidateRedirectUriAsync(object application,
[StringSyntax(StringSyntaxAttribute.Uri)] string uri, CancellationToken cancellationToken = default); [StringSyntax(StringSyntaxAttribute.Uri)] string uri, CancellationToken cancellationToken = default);
/// <summary>
/// Validates the self-signed client certificate associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="certificate">The certificate that should be compared to the certificates associated with the application.</param>
/// <param name="policy">The chain policy used to validate the certificate.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.</returns>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation, whose
/// result returns a boolean indicating whether the self-signed client certificate was valid.
/// </returns>
ValueTask<bool> ValidateSelfSignedClientCertificateAsync(
object application, X509Certificate2 certificate,
X509ChainPolicy policy, CancellationToken cancellationToken = default);
} }

4
src/OpenIddict.Abstractions/Managers/IOpenIddictAuthorizationManager.cs

@ -13,12 +13,14 @@ namespace OpenIddict.Abstractions;
/// <summary> /// <summary>
/// Provides methods allowing to manage the authorizations stored in the store. /// Provides methods allowing to manage the authorizations stored in the store.
/// </summary>
/// <remarks>
/// Note: this interface is not meant to be implemented by custom managers, /// Note: this interface is not meant to be implemented by custom managers,
/// that should inherit from the generic OpenIddictAuthorizationManager class. /// that should inherit from the generic OpenIddictAuthorizationManager class.
/// It is primarily intended to be used by services that cannot easily depend /// It is primarily intended to be used by services that cannot easily depend
/// on the generic authorization manager. The actual authorization entity type /// on the generic authorization manager. The actual authorization entity type
/// is automatically determined at runtime based on the OpenIddict core options. /// is automatically determined at runtime based on the OpenIddict core options.
/// </summary> /// </remarks>
public interface IOpenIddictAuthorizationManager public interface IOpenIddictAuthorizationManager
{ {
/// <summary> /// <summary>

4
src/OpenIddict.Abstractions/Managers/IOpenIddictScopeManager.cs

@ -13,12 +13,14 @@ namespace OpenIddict.Abstractions;
/// <summary> /// <summary>
/// Provides methods allowing to manage the scopes stored in the store. /// Provides methods allowing to manage the scopes stored in the store.
/// </summary>
/// <remarks>
/// Note: this interface is not meant to be implemented by custom managers, /// Note: this interface is not meant to be implemented by custom managers,
/// that should inherit from the generic OpenIddictScopeManager class. /// that should inherit from the generic OpenIddictScopeManager class.
/// It is primarily intended to be used by services that cannot easily /// It is primarily intended to be used by services that cannot easily
/// depend on the generic scope manager. The actual scope entity type is /// depend on the generic scope manager. The actual scope entity type is
/// automatically determined at runtime based on the OpenIddict core options. /// automatically determined at runtime based on the OpenIddict core options.
/// </summary> /// </remarks>
public interface IOpenIddictScopeManager public interface IOpenIddictScopeManager
{ {
/// <summary> /// <summary>

4
src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs

@ -12,12 +12,14 @@ namespace OpenIddict.Abstractions;
/// <summary> /// <summary>
/// Provides methods allowing to manage the tokens stored in the store. /// Provides methods allowing to manage the tokens stored in the store.
/// </summary>
/// <remarks>
/// Note: this interface is not meant to be implemented by custom managers, /// Note: this interface is not meant to be implemented by custom managers,
/// that should inherit from the generic OpenIddictTokenManager class. /// that should inherit from the generic OpenIddictTokenManager class.
/// It is primarily intended to be used by services that cannot easily /// It is primarily intended to be used by services that cannot easily
/// depend on the generic token manager. The actual token entity type is /// depend on the generic token manager. The actual token entity type is
/// automatically determined at runtime based on the OpenIddict core options. /// automatically determined at runtime based on the OpenIddict core options.
/// </summary> /// </remarks>
public interface IOpenIddictTokenManager public interface IOpenIddictTokenManager
{ {
/// <summary> /// <summary>

14
src/OpenIddict.Abstractions/OpenIddictConstants.cs

@ -336,6 +336,20 @@ public static class OpenIddictConstants
public const string UserInfoSigningAlgValuesSupported = "userinfo_signing_alg_values_supported"; 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 static class Parameters
{ {
public const string AccessToken = "access_token"; public const string AccessToken = "access_token";

82
src/OpenIddict.Abstractions/OpenIddictResources.resx

@ -1802,6 +1802,55 @@ Alternatively, any value respecting the '[region]-[subregion]-[identifier]' patt
<data name="ID0497" xml:space="preserve"> <data name="ID0497" xml:space="preserve">
<value>An unknown error occurred while trying to start a custom tabs intent.</value> <value>An unknown error occurred while trying to start a custom tabs intent.</value>
</data> </data>
<data name="ID0498" xml:space="preserve">
<value>The X.509 TLS client certificate provided by the OWIN host is not an instance of type 'X509Certificate2'.</value>
</data>
<data name="ID0499" xml:space="preserve">
<value>mTLS endpoint aliases must be absolute HTTPS URLs.</value>
</data>
<data name="ID0500" xml:space="preserve">
<value>A static issuer must be explicitly set when configuring a mTLS endpoint alias.
To set a static issuer, use 'services.AddOpenIddict().AddServer().SetIssuer()'.</value>
</data>
<data name="ID0501" xml:space="preserve">
<value>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.</value>
</data>
<data name="ID0502" xml:space="preserve">
<value>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.</value>
</data>
<data name="ID0503" xml:space="preserve">
<value>Public Key Infrastructure-based client authentication cannot be used with self-signed certificates.</value>
</data>
<data name="ID0504" xml:space="preserve">
<value>Self-signed client authentication can only be used with self-signed certificates.</value>
</data>
<data name="ID0505" xml:space="preserve">
<value>A certificate chain policy must be configured when enabling the 'tls_client_auth' authentication method.
To configure a policy, use 'services.AddOpenIddict().AddServer().EnablePublicKeyInfrastructureClientCertificateAuthentication()'.</value>
</data>
<data name="ID0506" xml:space="preserve">
<value>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()'.</value>
</data>
<data name="ID0507" xml:space="preserve">
<value>At least one certificate authority must be added to the certificate collection.</value>
</data>
<data name="ID0508" xml:space="preserve">
<value>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.</value>
</data>
<data name="ID0509" xml:space="preserve">
<value>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.</value>
</data>
<data name="ID0510" xml:space="preserve">
<value>mTLS endpoint aliases cannot be set when the corresponding endpoints have not been enabled.</value>
</data>
<data name="ID0511" xml:space="preserve">
<value>Public Key Infrastructure certificates cannot contain private keys.</value>
</data>
<data name="ID2000" xml:space="preserve"> <data name="ID2000" xml:space="preserve">
<value>The security token is missing.</value> <value>The security token is missing.</value>
</data> </data>
@ -2387,6 +2436,15 @@ Alternatively, any value respecting the '[region]-[subregion]-[identifier]' patt
<data name="ID2195" xml:space="preserve"> <data name="ID2195" xml:space="preserve">
<value>The '{0}' parameter is only allowed for OAuth 2.0 Token Exchange requests.</value> <value>The '{0}' parameter is only allowed for OAuth 2.0 Token Exchange requests.</value>
</data> </data>
<data name="ID2196" xml:space="preserve">
<value>Certificate-based authentication is not valid for this client application.</value>
</data>
<data name="ID2197" xml:space="preserve">
<value>The specified TLS client certificate is invalid, expired or has been revoked.</value>
</data>
<data name="ID2198" xml:space="preserve">
<value>Client authentication is required for this application.</value>
</data>
<data name="ID4000" xml:space="preserve"> <data name="ID4000" xml:space="preserve">
<value>The '{0}' parameter shouldn't be null or empty at this point.</value> <value>The '{0}' parameter shouldn't be null or empty at this point.</value>
</data> </data>
@ -2447,6 +2505,9 @@ Alternatively, any value respecting the '[region]-[subregion]-[identifier]' patt
<data name="ID4019" xml:space="preserve"> <data name="ID4019" xml:space="preserve">
<value>The nonce shouldn't be null or empty at this point.</value> <value>The nonce shouldn't be null or empty at this point.</value>
</data> </data>
<data name="ID4020" xml:space="preserve">
<value>The X.509 client certificate shouldn't be null at this point.</value>
</data>
<data name="ID6000" xml:space="preserve"> <data name="ID6000" xml:space="preserve">
<value>An error occurred while validating the token '{Token}'.</value> <value>An error occurred while validating the token '{Token}'.</value>
</data> </data>
@ -3194,6 +3255,27 @@ This may indicate that the hashed entry is corrupted or malformed.</value>
<data name="ID6281" xml:space="preserve"> <data name="ID6281" xml:space="preserve">
<value>The token request was rejected because the '{Parameter}' contained a URI fragment: {RedirectUri}.</value> <value>The token request was rejected because the '{Parameter}' contained a URI fragment: {RedirectUri}.</value>
</data> </data>
<data name="ID6282" xml:space="preserve">
<value>The authentication demand was rejected because the public application '{ClientId}' was not allowed to send a client certificate.</value>
</data>
<data name="ID6283" xml:space="preserve">
<value>The authentication demand was rejected because the confidential application '{ClientId}' didn't specify a valid client certificate.</value>
</data>
<data name="ID6284" xml:space="preserve">
<value>The authentication demand was rejected because the confidential application '{ClientId}' didn't specify a valid self-signed client certificate.</value>
</data>
<data name="ID6285" xml:space="preserve">
<value>Certificate-based client authentication failed for {ClientId} because no redirection URI was associated with the application.</value>
</data>
<data name="ID6286" xml:space="preserve">
<value>Certificate-based client authentication failed for {ClientId} because the certificate was not valid: {Errors}.</value>
</data>
<data name="ID6287" xml:space="preserve">
<value>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.</value>
</data>
<data name="ID6288" xml:space="preserve">
<value>An error occurred while trying to validate a client certificate, which may indicate that the certificate is malformed or has an invalid chain.</value>
</data>
<data name="ID8000" xml:space="preserve"> <data name="ID8000" xml:space="preserve">
<value>https://documentation.openiddict.com/errors/{0}</value> <value>https://documentation.openiddict.com/errors/{0}</value>
</data> </data>

2
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 // 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 // is dynamically added if one of the default schemes properties is not set
// and less than 2 handlers were registered in the authentication options. // 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.DefaultAuthenticateScheme) ||
string.IsNullOrEmpty(options.DefaultSignInScheme) || string.IsNullOrEmpty(options.DefaultSignInScheme) ||
string.IsNullOrEmpty(options.DefaultSignOutScheme))) string.IsNullOrEmpty(options.DefaultSignOutScheme)))

63
src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpConfiguration.cs

@ -7,7 +7,6 @@
using System.ComponentModel; using System.ComponentModel;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http; using Microsoft.Extensions.Http;
@ -271,10 +270,12 @@ public sealed class OpenIddictClientSystemNetHttpConfiguration : IConfigureOptio
{ {
foreach (var credentials in registration.SigningCredentials) 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 } && if (credentials.Key is X509SecurityKey { Certificate: X509Certificate2 certificate } &&
certificate.Version is >= 3 && IsSelfIssuedCertificate(certificate) && certificate.Version is >= 3 && OpenIddictHelpers.IsSelfIssuedCertificate(certificate) &&
HasDigitalSignatureKeyUsage(certificate) && OpenIddictHelpers.HasKeyUsage(certificate, X509KeyUsageFlags.DigitalSignature) &&
HasClientAuthenticationExtendedKeyUsage(certificate)) OpenIddictHelpers.HasExtendedKeyUsage(certificate, ObjectIdentifiers.ExtendedKeyUsages.ClientAuthentication))
{ {
return certificate; return certificate;
} }
@ -287,10 +288,12 @@ public sealed class OpenIddictClientSystemNetHttpConfiguration : IConfigureOptio
{ {
foreach (var credentials in registration.SigningCredentials) 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 } && if (credentials.Key is X509SecurityKey { Certificate: X509Certificate2 certificate } &&
certificate.Version is >= 3 && !IsSelfIssuedCertificate(certificate) && certificate.Version is >= 3 && !OpenIddictHelpers.IsSelfIssuedCertificate(certificate) &&
HasDigitalSignatureKeyUsage(certificate) && OpenIddictHelpers.HasKeyUsage(certificate, X509KeyUsageFlags.DigitalSignature) &&
HasClientAuthenticationExtendedKeyUsage(certificate)) OpenIddictHelpers.HasExtendedKeyUsage(certificate, ObjectIdentifiers.ExtendedKeyUsages.ClientAuthentication))
{ {
return certificate; return certificate;
} }
@ -298,51 +301,5 @@ public sealed class OpenIddictClientSystemNetHttpConfiguration : IConfigureOptio
return null; 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);
} }
} }

22
src/OpenIddict.Client/OpenIddictClientBuilder.cs

@ -135,8 +135,7 @@ public sealed class OpenIddictClientBuilder
ArgumentNullException.ThrowIfNull(key); ArgumentNullException.ThrowIfNull(key);
// If the encryption key is an asymmetric security key, ensure it has a private key. // If the encryption key is an asymmetric security key, ensure it has a private key.
if (key is AsymmetricSecurityKey asymmetricSecurityKey && if (key is AsymmetricSecurityKey { PrivateKeyStatus: PrivateKeyStatus.DoesNotExist })
asymmetricSecurityKey.PrivateKeyStatus is PrivateKeyStatus.DoesNotExist)
{ {
throw new InvalidOperationException(SR.GetResourceString(SR.ID0055)); 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. // If no valid existing certificate was found, create a new encryption certificate.
var certificates = store.Certificates var certificates = store.Certificates
.Find(X509FindType.FindBySubjectDistinguishedName, subject.Name, validOnly: false) .Find(X509FindType.FindBySubjectDistinguishedName, subject.Name, validOnly: false)
.OfType<X509Certificate2>() .Cast<X509Certificate2>()
.ToList(); .ToList();
if (!certificates.Exists(certificate => certificate.NotBefore < now.LocalDateTime && certificate.NotAfter > now.LocalDateTime)) 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 // 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. // 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<X509KeyUsageExtension>().ToList(); var extensions = certificate.Extensions.OfType<X509KeyUsageExtension>().ToList();
if (extensions.Count is not 0 && !extensions.Exists(static extension => if (extensions.Count is not 0 && !extensions.Exists(static extension =>
@ -437,7 +436,7 @@ public sealed class OpenIddictClientBuilder
store.Open(OpenFlags.ReadOnly); store.Open(OpenFlags.ReadOnly);
return store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false) return store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false)
.OfType<X509Certificate2>() .Cast<X509Certificate2>()
.SingleOrDefault(); .SingleOrDefault();
} }
} }
@ -458,7 +457,7 @@ public sealed class OpenIddictClientBuilder
return AddEncryptionCertificate( return AddEncryptionCertificate(
store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false) store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false)
.OfType<X509Certificate2>() .Cast<X509Certificate2>()
.SingleOrDefault() ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0066))); .SingleOrDefault() ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0066)));
} }
@ -496,8 +495,7 @@ public sealed class OpenIddictClientBuilder
ArgumentNullException.ThrowIfNull(key); ArgumentNullException.ThrowIfNull(key);
// If the signing key is an asymmetric security key, ensure it has a private key. // If the signing key is an asymmetric security key, ensure it has a private key.
if (key is AsymmetricSecurityKey asymmetricSecurityKey && if (key is AsymmetricSecurityKey { PrivateKeyStatus: PrivateKeyStatus.DoesNotExist })
asymmetricSecurityKey.PrivateKeyStatus is PrivateKeyStatus.DoesNotExist)
{ {
throw new InvalidOperationException(SR.GetResourceString(SR.ID0067)); 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. // If no valid existing certificate was found, create a new signing certificate.
var certificates = store.Certificates var certificates = store.Certificates
.Find(X509FindType.FindBySubjectDistinguishedName, subject.Name, validOnly: false) .Find(X509FindType.FindBySubjectDistinguishedName, subject.Name, validOnly: false)
.OfType<X509Certificate2>() .Cast<X509Certificate2>()
.ToList(); .ToList();
if (!certificates.Exists(certificate => certificate.NotBefore < now.LocalDateTime && certificate.NotAfter > now.LocalDateTime)) 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 // 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. // 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<X509KeyUsageExtension>().ToList(); var extensions = certificate.Extensions.OfType<X509KeyUsageExtension>().ToList();
if (extensions.Count is not 0 && !extensions.Exists(static extension => if (extensions.Count is not 0 && !extensions.Exists(static extension =>
@ -844,7 +842,7 @@ public sealed class OpenIddictClientBuilder
store.Open(OpenFlags.ReadOnly); store.Open(OpenFlags.ReadOnly);
return store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false) return store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false)
.OfType<X509Certificate2>() .Cast<X509Certificate2>()
.SingleOrDefault(); .SingleOrDefault();
} }
} }
@ -865,7 +863,7 @@ public sealed class OpenIddictClientBuilder
return AddSigningCertificate( return AddSigningCertificate(
store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false) store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false)
.OfType<X509Certificate2>() .Cast<X509Certificate2>()
.SingleOrDefault() ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0066))); .SingleOrDefault() ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0066)));
} }

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

@ -568,7 +568,8 @@ public static partial class OpenIddictClientHandlers
} }
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
public sealed class ExtractMtlsRevocationEndpoint : IOpenIddictClientHandler<HandleConfigurationResponseContext> public sealed class ExtractMtlsRevocationEndpoint : IOpenIddictClientHandler<HandleConfigurationResponseContext>
{ {
@ -600,7 +601,8 @@ public static partial class OpenIddictClientHandlers
} }
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
public sealed class ExtractMtlsTokenEndpoint : IOpenIddictClientHandler<HandleConfigurationResponseContext> public sealed class ExtractMtlsTokenEndpoint : IOpenIddictClientHandler<HandleConfigurationResponseContext>
{ {
@ -632,7 +634,8 @@ public static partial class OpenIddictClientHandlers
} }
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
public sealed class ExtractMtlsUserInfoEndpoint : IOpenIddictClientHandler<HandleConfigurationResponseContext> public sealed class ExtractMtlsUserInfoEndpoint : IOpenIddictClientHandler<HandleConfigurationResponseContext>
{ {

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

@ -11,6 +11,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -18,7 +19,6 @@ using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using ValidationException = OpenIddict.Abstractions.OpenIddictExceptions.ValidationException; using ValidationException = OpenIddict.Abstractions.OpenIddictExceptions.ValidationException;
#if !SUPPORTS_KEY_DERIVATION_WITH_SPECIFIED_HASH_ALGORITHM #if !SUPPORTS_KEY_DERIVATION_WITH_SPECIFIED_HASH_ALGORITHM
using Org.BouncyCastle.Crypto; using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Digests; using Org.BouncyCastle.Crypto.Digests;
@ -471,6 +471,25 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
return Store.GetAsync(query, state, cancellationToken); return Store.GetAsync(query, state, cancellationToken);
} }
/// <summary>
/// Retrieves the client certificate chain policy enforced for this application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="policy">The base policy from which the returned instance will be derived.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client certificate chain policy enforced for this application.
/// </returns>
public virtual ValueTask<X509ChainPolicy?> 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());
}
/// <summary> /// <summary>
/// Retrieves the client identifier associated with an application. /// Retrieves the client identifier associated with an application.
/// </summary> /// </summary>
@ -739,6 +758,48 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
return Store.GetRequirementsAsync(application, cancellationToken); return Store.GetRequirementsAsync(application, cancellationToken);
} }
/// <summary>
/// Retrieves the self-signed client certificate chain policy enforced for this application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="policy">The base policy from which the returned instance will be derived.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation, whose
/// result returns the self-signed client certificate chain policy enforced for this application.
/// </returns>
public virtual async ValueTask<X509ChainPolicy?> 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;
}
/// <summary> /// <summary>
/// Retrieves the settings associated with an application. /// Retrieves the settings associated with an application.
/// </summary> /// </summary>
@ -1226,6 +1287,114 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
} }
} }
/// <summary>
/// Validates the client certificate associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="certificate">The certificate that should be compared to the certificates associated with the application.</param>
/// <param name="policy">The chain policy used to validate the certificate.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.</returns>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
/// whose result returns a boolean indicating whether the client certificate was valid.
/// </returns>
public virtual async ValueTask<bool> 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
}
/// <summary> /// <summary>
/// Validates the client_secret associated with an application. /// Validates the client_secret associated with an application.
/// </summary> /// </summary>
@ -1399,6 +1568,92 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
return false; return false;
} }
/// <summary>
/// Validates the self-signed client certificate associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="certificate">The certificate that should be compared to the certificates associated with the application.</param>
/// <param name="policy">The chain policy used to validate the certificate.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.</returns>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation, whose
/// result returns a boolean indicating whether the self-signed client certificate was valid.
/// </returns>
public virtual async ValueTask<bool> 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
}
/// <summary> /// <summary>
/// Obfuscates the specified client secret so it can be safely stored in a database. /// 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. /// By default, this method returns a complex hashed representation computed using PBKDF2.
@ -1520,7 +1775,7 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
// Read the size of the salt and ensure it's more than 128 bits. // Read the size of the salt and ensure it's more than 128 bits.
var saltLength = (int) BinaryPrimitives.ReadUInt32BigEndian(payload.Slice(9, sizeof(uint))); var saltLength = (int) BinaryPrimitives.ReadUInt32BigEndian(payload.Slice(9, sizeof(uint)));
if (saltLength < 128 / 8) if (saltLength is < 128 / 8)
{ {
return false; return false;
} }
@ -1530,7 +1785,7 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
// Ensure the derived key length is more than 128 bits. // Ensure the derived key length is more than 128 bits.
var keyLength = payload.Length - 13 - salt.Length; var keyLength = payload.Length - 13 - salt.Length;
if (keyLength < 128 / 8) if (keyLength is < 128 / 8)
{ {
return false; return false;
} }
@ -1614,6 +1869,10 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
ValueTask<TResult?> IOpenIddictApplicationManager.GetAsync<TState, TResult>(Func<IQueryable<object>, TState, IQueryable<TResult>> query, TState state, CancellationToken cancellationToken) where TResult : default ValueTask<TResult?> IOpenIddictApplicationManager.GetAsync<TState, TResult>(Func<IQueryable<object>, TState, IQueryable<TResult>> query, TState state, CancellationToken cancellationToken) where TResult : default
=> GetAsync(query, state, cancellationToken); => GetAsync(query, state, cancellationToken);
/// <inheritdoc/>
ValueTask<X509ChainPolicy?> IOpenIddictApplicationManager.GetClientCertificateChainPolicyAsync(object application, X509ChainPolicy policy, CancellationToken cancellationToken)
=> GetClientCertificateChainPolicyAsync((TApplication) application, policy, cancellationToken);
/// <inheritdoc/> /// <inheritdoc/>
ValueTask<string?> IOpenIddictApplicationManager.GetClientIdAsync(object application, CancellationToken cancellationToken) ValueTask<string?> IOpenIddictApplicationManager.GetClientIdAsync(object application, CancellationToken cancellationToken)
=> GetClientIdAsync((TApplication) application, cancellationToken); => GetClientIdAsync((TApplication) application, cancellationToken);
@ -1670,6 +1929,10 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
ValueTask<ImmutableArray<string>> IOpenIddictApplicationManager.GetRequirementsAsync(object application, CancellationToken cancellationToken) ValueTask<ImmutableArray<string>> IOpenIddictApplicationManager.GetRequirementsAsync(object application, CancellationToken cancellationToken)
=> GetRequirementsAsync((TApplication) application, cancellationToken); => GetRequirementsAsync((TApplication) application, cancellationToken);
/// <inheritdoc/>
ValueTask<X509ChainPolicy?> IOpenIddictApplicationManager.GetSelfSignedClientCertificateChainPolicyAsync(object application, X509ChainPolicy policy, CancellationToken cancellationToken)
=> GetSelfSignedClientCertificateChainPolicyAsync((TApplication) application, policy, cancellationToken);
/// <inheritdoc/> /// <inheritdoc/>
ValueTask<ImmutableDictionary<string, string>> IOpenIddictApplicationManager.GetSettingsAsync(object application, CancellationToken cancellationToken) ValueTask<ImmutableDictionary<string, string>> IOpenIddictApplicationManager.GetSettingsAsync(object application, CancellationToken cancellationToken)
=> GetSettingsAsync((TApplication) application, cancellationToken); => GetSettingsAsync((TApplication) application, cancellationToken);
@ -1730,6 +1993,10 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
IAsyncEnumerable<ValidationResult> IOpenIddictApplicationManager.ValidateAsync(object application, CancellationToken cancellationToken) IAsyncEnumerable<ValidationResult> IOpenIddictApplicationManager.ValidateAsync(object application, CancellationToken cancellationToken)
=> ValidateAsync((TApplication) application, cancellationToken); => ValidateAsync((TApplication) application, cancellationToken);
/// <inheritdoc/>
ValueTask<bool> IOpenIddictApplicationManager.ValidateClientCertificateAsync(object application, X509Certificate2 certificate, X509ChainPolicy policy, CancellationToken cancellationToken)
=> ValidateClientCertificateAsync((TApplication) application, certificate, policy, cancellationToken);
/// <inheritdoc/> /// <inheritdoc/>
ValueTask<bool> IOpenIddictApplicationManager.ValidateClientSecretAsync(object application, string secret, CancellationToken cancellationToken) ValueTask<bool> IOpenIddictApplicationManager.ValidateClientSecretAsync(object application, string secret, CancellationToken cancellationToken)
=> ValidateClientSecretAsync((TApplication) application, secret, cancellationToken); => ValidateClientSecretAsync((TApplication) application, secret, cancellationToken);
@ -1741,4 +2008,8 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
/// <inheritdoc/> /// <inheritdoc/>
ValueTask<bool> IOpenIddictApplicationManager.ValidateRedirectUriAsync(object application, [StringSyntax(StringSyntaxAttribute.Uri)] string uri, CancellationToken cancellationToken) ValueTask<bool> IOpenIddictApplicationManager.ValidateRedirectUriAsync(object application, [StringSyntax(StringSyntaxAttribute.Uri)] string uri, CancellationToken cancellationToken)
=> ValidateRedirectUriAsync((TApplication) application, uri, cancellationToken); => ValidateRedirectUriAsync((TApplication) application, uri, cancellationToken);
/// <inheritdoc/>
ValueTask<bool> IOpenIddictApplicationManager.ValidateSelfSignedClientCertificateAsync(object application, X509Certificate2 certificate, X509ChainPolicy policy, CancellationToken cancellationToken)
=> ValidateSelfSignedClientCertificateAsync((TApplication) application, certificate, policy, cancellationToken);
} }

23
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConfiguration.cs

@ -16,7 +16,8 @@ namespace OpenIddict.Server.AspNetCore;
public sealed class OpenIddictServerAspNetCoreConfiguration : IConfigureOptions<AuthenticationOptions>, public sealed class OpenIddictServerAspNetCoreConfiguration : IConfigureOptions<AuthenticationOptions>,
IConfigureOptions<OpenIddictServerOptions>, IConfigureOptions<OpenIddictServerOptions>,
IPostConfigureOptions<AuthenticationOptions>, IPostConfigureOptions<AuthenticationOptions>,
IPostConfigureOptions<OpenIddictServerAspNetCoreOptions> IPostConfigureOptions<OpenIddictServerAspNetCoreOptions>,
IPostConfigureOptions<OpenIddictServerOptions>
{ {
/// <inheritdoc/> /// <inheritdoc/>
public void Configure(AuthenticationOptions options) 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 // 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 // is dynamically added if one of the default schemes properties is not set
// and less than 2 handlers were registered in the authentication options. // 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.DefaultAuthenticateScheme) ||
string.IsNullOrEmpty(options.DefaultChallengeScheme) || string.IsNullOrEmpty(options.DefaultChallengeScheme) ||
string.IsNullOrEmpty(options.DefaultForbidScheme) || string.IsNullOrEmpty(options.DefaultForbidScheme) ||
@ -104,4 +105,22 @@ public sealed class OpenIddictServerAspNetCoreConfiguration : IConfigureOptions<
throw new InvalidOperationException(SR.GetResourceString(SR.ID0110)); throw new InvalidOperationException(SR.GetResourceString(SR.ID0110));
} }
} }
/// <inheritdoc/>
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);
}
}
} }

5
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreExtensions.cs

@ -45,7 +45,7 @@ public static class OpenIddictServerAspNetCoreExtensions
builder.Services.TryAddSingleton<RequireUserInfoEndpointPassthroughEnabled>(); builder.Services.TryAddSingleton<RequireUserInfoEndpointPassthroughEnabled>();
builder.Services.TryAddSingleton<RequireVerificationEndpointPassthroughEnabled>(); builder.Services.TryAddSingleton<RequireVerificationEndpointPassthroughEnabled>();
// 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. // Note: TryAddEnumerable() is used here to ensure the initializers are only registered once.
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton< builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<
IConfigureOptions<AuthenticationOptions>, OpenIddictServerAspNetCoreConfiguration>()); IConfigureOptions<AuthenticationOptions>, OpenIddictServerAspNetCoreConfiguration>());
@ -59,6 +59,9 @@ public static class OpenIddictServerAspNetCoreExtensions
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton< builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<
IPostConfigureOptions<OpenIddictServerAspNetCoreOptions>, OpenIddictServerAspNetCoreConfiguration>()); IPostConfigureOptions<OpenIddictServerAspNetCoreOptions>, OpenIddictServerAspNetCoreConfiguration>());
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<
IPostConfigureOptions<OpenIddictServerOptions>, OpenIddictServerAspNetCoreConfiguration>());
return new OpenIddictServerAspNetCoreBuilder(builder.Services); return new OpenIddictServerAspNetCoreBuilder(builder.Services);
} }

1
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Authentication.cs

@ -48,6 +48,7 @@ public static partial class OpenIddictServerAspNetCoreHandlers
*/ */
ExtractPostRequest<ExtractPushedAuthorizationRequestContext>.Descriptor, ExtractPostRequest<ExtractPushedAuthorizationRequestContext>.Descriptor,
ValidateClientAuthenticationMethod<ExtractPushedAuthorizationRequestContext>.Descriptor, ValidateClientAuthenticationMethod<ExtractPushedAuthorizationRequestContext>.Descriptor,
ExtractClientAuthenticationCertificate<ExtractPushedAuthorizationRequestContext>.Descriptor,
ExtractBasicAuthenticationCredentials<ExtractPushedAuthorizationRequestContext>.Descriptor, ExtractBasicAuthenticationCredentials<ExtractPushedAuthorizationRequestContext>.Descriptor,
/* /*

1
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Device.cs

@ -21,6 +21,7 @@ public static partial class OpenIddictServerAspNetCoreHandlers
*/ */
ExtractPostRequest<ExtractDeviceAuthorizationRequestContext>.Descriptor, ExtractPostRequest<ExtractDeviceAuthorizationRequestContext>.Descriptor,
ValidateClientAuthenticationMethod<ExtractDeviceAuthorizationRequestContext>.Descriptor, ValidateClientAuthenticationMethod<ExtractDeviceAuthorizationRequestContext>.Descriptor,
ExtractClientAuthenticationCertificate<ExtractDeviceAuthorizationRequestContext>.Descriptor,
ExtractBasicAuthenticationCredentials<ExtractDeviceAuthorizationRequestContext>.Descriptor, ExtractBasicAuthenticationCredentials<ExtractDeviceAuthorizationRequestContext>.Descriptor,
/* /*

1
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Exchange.cs

@ -19,6 +19,7 @@ public static partial class OpenIddictServerAspNetCoreHandlers
*/ */
ExtractPostRequest<ExtractTokenRequestContext>.Descriptor, ExtractPostRequest<ExtractTokenRequestContext>.Descriptor,
ValidateClientAuthenticationMethod<ExtractTokenRequestContext>.Descriptor, ValidateClientAuthenticationMethod<ExtractTokenRequestContext>.Descriptor,
ExtractClientAuthenticationCertificate<ExtractTokenRequestContext>.Descriptor,
ExtractBasicAuthenticationCredentials<ExtractTokenRequestContext>.Descriptor, ExtractBasicAuthenticationCredentials<ExtractTokenRequestContext>.Descriptor,
/* /*

1
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Introspection.cs

@ -19,6 +19,7 @@ public static partial class OpenIddictServerAspNetCoreHandlers
*/ */
ExtractGetOrPostRequest<ExtractIntrospectionRequestContext>.Descriptor, ExtractGetOrPostRequest<ExtractIntrospectionRequestContext>.Descriptor,
ValidateClientAuthenticationMethod<ExtractIntrospectionRequestContext>.Descriptor, ValidateClientAuthenticationMethod<ExtractIntrospectionRequestContext>.Descriptor,
ExtractClientAuthenticationCertificate<ExtractIntrospectionRequestContext>.Descriptor,
ExtractBasicAuthenticationCredentials<ExtractIntrospectionRequestContext>.Descriptor, ExtractBasicAuthenticationCredentials<ExtractIntrospectionRequestContext>.Descriptor,
/* /*

1
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Revocation.cs

@ -19,6 +19,7 @@ public static partial class OpenIddictServerAspNetCoreHandlers
*/ */
ExtractPostRequest<ExtractRevocationRequestContext>.Descriptor, ExtractPostRequest<ExtractRevocationRequestContext>.Descriptor,
ValidateClientAuthenticationMethod<ExtractRevocationRequestContext>.Descriptor, ValidateClientAuthenticationMethod<ExtractRevocationRequestContext>.Descriptor,
ExtractClientAuthenticationCertificate<ExtractRevocationRequestContext>.Descriptor,
ExtractBasicAuthenticationCredentials<ExtractRevocationRequestContext>.Descriptor, ExtractBasicAuthenticationCredentials<ExtractRevocationRequestContext>.Descriptor,
/* /*

88
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs

@ -8,6 +8,7 @@ using System.Collections.Immutable;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates;
using System.Text; using System.Text;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using System.Text.Json; using System.Text.Json;
@ -626,7 +627,7 @@ public static partial class OpenIddictServerAspNetCoreHandlers
.Build(); .Build();
/// <inheritdoc/> /// <inheritdoc/>
public ValueTask HandleAsync(TContext context) public async ValueTask HandleAsync(TContext context)
{ {
ArgumentNullException.ThrowIfNull(context); ArgumentNullException.ThrowIfNull(context);
@ -648,13 +649,10 @@ public static partial class OpenIddictServerAspNetCoreHandlers
description: SR.FormatID2174(ClientAuthenticationMethods.ClientSecretPost), description: SR.FormatID2174(ClientAuthenticationMethods.ClientSecretPost),
uri: SR.FormatID8000(SR.ID2174)); uri: SR.FormatID8000(SR.ID2174));
return ValueTask.CompletedTask; return;
} }
// Reject requests that use client_secret_basic if support was explicitly disabled in the options. // 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]; string? header = request.Headers[HeaderNames.Authorization];
if (!string.IsNullOrEmpty(header) && header.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase) && if (!string.IsNullOrEmpty(header) && header.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase) &&
!context.Options.ClientAuthenticationMethods.Contains(ClientAuthenticationMethods.ClientSecretBasic)) !context.Options.ClientAuthenticationMethods.Contains(ClientAuthenticationMethods.ClientSecretBasic))
@ -666,10 +664,82 @@ public static partial class OpenIddictServerAspNetCoreHandlers
description: SR.FormatID2174(ClientAuthenticationMethods.ClientSecretBasic), description: SR.FormatID2174(ClientAuthenticationMethods.ClientSecretBasic),
uri: SR.FormatID8000(SR.ID2174)); 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;
}
}
}
}
/// <summary>
/// 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.
/// </summary>
public sealed class ExtractClientAuthenticationCertificate<TContext> : IOpenIddictServerHandler<TContext>
where TContext : BaseValidatingContext
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireHttpRequest>()
.UseSingletonHandler<ExtractClientAuthenticationCertificate<TContext>>()
.SetOrder(ValidateClientAuthenticationMethod<TContext>.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
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<TContext>() = OpenIddictServerHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireHttpRequest>() .AddFilter<RequireHttpRequest>()
.UseSingletonHandler<ExtractBasicAuthenticationCredentials<TContext>>() .UseSingletonHandler<ExtractBasicAuthenticationCredentials<TContext>>()
.SetOrder(ValidateClientAuthenticationMethod<TContext>.Descriptor.Order + 1_000) .SetOrder(ExtractClientAuthenticationCertificate<TContext>.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn) .SetType(OpenIddictServerHandlerType.BuiltIn)
.Build(); .Build();
@ -730,7 +800,7 @@ public static partial class OpenIddictServerAspNetCoreHandlers
var data = Encoding.ASCII.GetString(Convert.FromBase64String(value)); var data = Encoding.ASCII.GetString(Convert.FromBase64String(value));
var index = data.IndexOf(':'); var index = data.IndexOf(':');
if (index < 0) if (index is < 0)
{ {
context.Reject( context.Reject(
error: Errors.InvalidRequest, error: Errors.InvalidRequest,

19
src/OpenIddict.Server.Owin/OpenIddictServerOwinConfiguration.cs

@ -14,6 +14,7 @@ namespace OpenIddict.Server.Owin;
/// </summary> /// </summary>
[EditorBrowsable(EditorBrowsableState.Advanced)] [EditorBrowsable(EditorBrowsableState.Advanced)]
public sealed class OpenIddictServerOwinConfiguration : IConfigureOptions<OpenIddictServerOptions>, public sealed class OpenIddictServerOwinConfiguration : IConfigureOptions<OpenIddictServerOptions>,
IPostConfigureOptions<OpenIddictServerOptions>,
IPostConfigureOptions<OpenIddictServerOwinOptions> IPostConfigureOptions<OpenIddictServerOwinOptions>
{ {
/// <inheritdoc/> /// <inheritdoc/>
@ -28,6 +29,24 @@ public sealed class OpenIddictServerOwinConfiguration : IConfigureOptions<OpenId
options.ClientAuthenticationMethods.Add(ClientAuthenticationMethods.ClientSecretBasic); options.ClientAuthenticationMethods.Add(ClientAuthenticationMethods.ClientSecretBasic);
} }
/// <inheritdoc/>
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);
}
}
/// <inheritdoc/> /// <inheritdoc/>
public void PostConfigure(string? name, OpenIddictServerOwinOptions options) public void PostConfigure(string? name, OpenIddictServerOwinOptions options)
{ {

3
src/OpenIddict.Server.Owin/OpenIddictServerOwinExtensions.cs

@ -55,6 +55,9 @@ public static class OpenIddictServerOwinExtensions
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton< builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<
IPostConfigureOptions<OpenIddictServerOwinOptions>, OpenIddictServerOwinConfiguration>()); IPostConfigureOptions<OpenIddictServerOwinOptions>, OpenIddictServerOwinConfiguration>());
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<
IPostConfigureOptions<OpenIddictServerOptions>, OpenIddictServerOwinConfiguration>());
return new OpenIddictServerOwinBuilder(builder.Services); return new OpenIddictServerOwinBuilder(builder.Services);
} }

1
src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Authentication.cs

@ -48,6 +48,7 @@ public static partial class OpenIddictServerOwinHandlers
*/ */
ExtractPostRequest<ExtractPushedAuthorizationRequestContext>.Descriptor, ExtractPostRequest<ExtractPushedAuthorizationRequestContext>.Descriptor,
ValidateClientAuthenticationMethod<ExtractPushedAuthorizationRequestContext>.Descriptor, ValidateClientAuthenticationMethod<ExtractPushedAuthorizationRequestContext>.Descriptor,
ExtractClientAuthenticationCertificate<ExtractPushedAuthorizationRequestContext>.Descriptor,
ExtractBasicAuthenticationCredentials<ExtractPushedAuthorizationRequestContext>.Descriptor, ExtractBasicAuthenticationCredentials<ExtractPushedAuthorizationRequestContext>.Descriptor,
/* /*

1
src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Device.cs

@ -21,6 +21,7 @@ public static partial class OpenIddictServerOwinHandlers
*/ */
ExtractPostRequest<ExtractDeviceAuthorizationRequestContext>.Descriptor, ExtractPostRequest<ExtractDeviceAuthorizationRequestContext>.Descriptor,
ValidateClientAuthenticationMethod<ExtractDeviceAuthorizationRequestContext>.Descriptor, ValidateClientAuthenticationMethod<ExtractDeviceAuthorizationRequestContext>.Descriptor,
ExtractClientAuthenticationCertificate<ExtractDeviceAuthorizationRequestContext>.Descriptor,
ExtractBasicAuthenticationCredentials<ExtractDeviceAuthorizationRequestContext>.Descriptor, ExtractBasicAuthenticationCredentials<ExtractDeviceAuthorizationRequestContext>.Descriptor,
/* /*

1
src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Exchange.cs

@ -19,6 +19,7 @@ public static partial class OpenIddictServerOwinHandlers
*/ */
ExtractPostRequest<ExtractTokenRequestContext>.Descriptor, ExtractPostRequest<ExtractTokenRequestContext>.Descriptor,
ValidateClientAuthenticationMethod<ExtractTokenRequestContext>.Descriptor, ValidateClientAuthenticationMethod<ExtractTokenRequestContext>.Descriptor,
ExtractClientAuthenticationCertificate<ExtractTokenRequestContext>.Descriptor,
ExtractBasicAuthenticationCredentials<ExtractTokenRequestContext>.Descriptor, ExtractBasicAuthenticationCredentials<ExtractTokenRequestContext>.Descriptor,
/* /*

1
src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Introspection.cs

@ -19,6 +19,7 @@ public static partial class OpenIddictServerOwinHandlers
*/ */
ExtractGetOrPostRequest<ExtractIntrospectionRequestContext>.Descriptor, ExtractGetOrPostRequest<ExtractIntrospectionRequestContext>.Descriptor,
ValidateClientAuthenticationMethod<ExtractIntrospectionRequestContext>.Descriptor, ValidateClientAuthenticationMethod<ExtractIntrospectionRequestContext>.Descriptor,
ExtractClientAuthenticationCertificate<ExtractIntrospectionRequestContext>.Descriptor,
ExtractBasicAuthenticationCredentials<ExtractIntrospectionRequestContext>.Descriptor, ExtractBasicAuthenticationCredentials<ExtractIntrospectionRequestContext>.Descriptor,
/* /*

1
src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Revocation.cs

@ -19,6 +19,7 @@ public static partial class OpenIddictServerOwinHandlers
*/ */
ExtractPostRequest<ExtractRevocationRequestContext>.Descriptor, ExtractPostRequest<ExtractRevocationRequestContext>.Descriptor,
ValidateClientAuthenticationMethod<ExtractRevocationRequestContext>.Descriptor, ValidateClientAuthenticationMethod<ExtractRevocationRequestContext>.Descriptor,
ExtractClientAuthenticationCertificate<ExtractRevocationRequestContext>.Descriptor,
ExtractBasicAuthenticationCredentials<ExtractRevocationRequestContext>.Descriptor, ExtractBasicAuthenticationCredentials<ExtractRevocationRequestContext>.Descriptor,
/* /*

121
src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs

@ -8,6 +8,7 @@ using System.Collections.Immutable;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.Security.Cryptography.X509Certificates;
using System.Text; using System.Text;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using System.Text.Json; using System.Text.Json;
@ -688,7 +689,7 @@ public static partial class OpenIddictServerOwinHandlers
.Build(); .Build();
/// <inheritdoc/> /// <inheritdoc/>
public ValueTask HandleAsync(TContext context) public async ValueTask HandleAsync(TContext context)
{ {
ArgumentNullException.ThrowIfNull(context); ArgumentNullException.ThrowIfNull(context);
@ -710,7 +711,7 @@ public static partial class OpenIddictServerOwinHandlers
description: SR.FormatID2174(ClientAuthenticationMethods.ClientSecretPost), description: SR.FormatID2174(ClientAuthenticationMethods.ClientSecretPost),
uri: SR.FormatID8000(SR.ID2174)); uri: SR.FormatID8000(SR.ID2174));
return ValueTask.CompletedTask; return;
} }
// Reject requests that use client_secret_basic if support was explicitly disabled in the options. // 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), description: SR.FormatID2174(ClientAuthenticationMethods.ClientSecretBasic),
uri: SR.FormatID8000(SR.ID2174)); 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<X509Certificate2?> 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<Func<Task>>("ssl.LoadClientCertAsync") is Func<Task> loader)
{
await loader();
}
if (context.Get<Exception>("ssl.ClientCertificateErrors") is not null)
{
return null;
}
return context.Get<X509Certificate>("ssl.ClientCertificate") is X509Certificate certificate
? certificate as X509Certificate2 ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0498))
: null;
}
}
}
/// <summary>
/// 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.
/// </summary>
public sealed class ExtractClientAuthenticationCertificate<TContext> : IOpenIddictServerHandler<TContext>
where TContext : BaseValidatingContext
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireOwinRequest>()
.UseSingletonHandler<ExtractClientAuthenticationCertificate<TContext>>()
.SetOrder(ValidateClientAuthenticationMethod<TContext>.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
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<X509Certificate2?> 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<Func<Task>>("ssl.LoadClientCertAsync") is Func<Task> loader)
{
await loader();
}
if (context.Get<Exception>("ssl.ClientCertificateErrors") is not null)
{
return null;
}
return context.Get<X509Certificate>("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<TContext>() = OpenIddictServerHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireOwinRequest>() .AddFilter<RequireOwinRequest>()
.UseSingletonHandler<ExtractBasicAuthenticationCredentials<TContext>>() .UseSingletonHandler<ExtractBasicAuthenticationCredentials<TContext>>()
.SetOrder(ValidateClientAuthenticationMethod<TContext>.Descriptor.Order + 1_000) .SetOrder(ExtractClientAuthenticationCertificate<TContext>.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn) .SetType(OpenIddictServerHandlerType.BuiltIn)
.Build(); .Build();
@ -789,7 +898,7 @@ public static partial class OpenIddictServerOwinHandlers
var data = Encoding.ASCII.GetString(Convert.FromBase64String(value)); var data = Encoding.ASCII.GetString(Convert.FromBase64String(value));
var index = data.IndexOf(':'); var index = data.IndexOf(':');
if (index < 0) if (index is < 0)
{ {
context.Reject( context.Reject(
error: Errors.InvalidRequest, error: Errors.InvalidRequest,

443
src/OpenIddict.Server/OpenIddictServerBuilder.cs

@ -4,6 +4,7 @@
* the license and the contributors participating to this project. * the license and the contributors participating to this project.
*/ */
using System;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
@ -11,6 +12,7 @@ using System.Reflection;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using OpenIddict.Server; using OpenIddict.Server;
@ -144,8 +146,7 @@ public sealed class OpenIddictServerBuilder
ArgumentNullException.ThrowIfNull(key); ArgumentNullException.ThrowIfNull(key);
// If the encryption key is an asymmetric security key, ensure it has a private key. // If the encryption key is an asymmetric security key, ensure it has a private key.
if (key is AsymmetricSecurityKey asymmetricSecurityKey && if (key is AsymmetricSecurityKey { PrivateKeyStatus: PrivateKeyStatus.DoesNotExist })
asymmetricSecurityKey.PrivateKeyStatus is PrivateKeyStatus.DoesNotExist)
{ {
throw new InvalidOperationException(SR.GetResourceString(SR.ID0055)); 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. // If no valid existing certificate was found, create a new encryption certificate.
var certificates = store.Certificates var certificates = store.Certificates
.Find(X509FindType.FindBySubjectDistinguishedName, subject.Name, validOnly: false) .Find(X509FindType.FindBySubjectDistinguishedName, subject.Name, validOnly: false)
.OfType<X509Certificate2>() .Cast<X509Certificate2>()
.ToList(); .ToList();
if (!certificates.Exists(certificate => certificate.NotBefore < now.LocalDateTime && certificate.NotAfter > now.LocalDateTime)) 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 // 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. // 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<X509KeyUsageExtension>().ToList(); var extensions = certificate.Extensions.OfType<X509KeyUsageExtension>().ToList();
if (extensions.Count is not 0 && !extensions.Exists(static extension => if (extensions.Count is not 0 && !extensions.Exists(static extension =>
@ -446,7 +447,7 @@ public sealed class OpenIddictServerBuilder
store.Open(OpenFlags.ReadOnly); store.Open(OpenFlags.ReadOnly);
return store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false) return store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false)
.OfType<X509Certificate2>() .Cast<X509Certificate2>()
.SingleOrDefault(); .SingleOrDefault();
} }
} }
@ -467,7 +468,7 @@ public sealed class OpenIddictServerBuilder
return AddEncryptionCertificate( return AddEncryptionCertificate(
store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false) store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false)
.OfType<X509Certificate2>() .Cast<X509Certificate2>()
.SingleOrDefault() ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0066))); .SingleOrDefault() ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0066)));
} }
@ -505,8 +506,7 @@ public sealed class OpenIddictServerBuilder
ArgumentNullException.ThrowIfNull(key); ArgumentNullException.ThrowIfNull(key);
// If the signing key is an asymmetric security key, ensure it has a private key. // If the signing key is an asymmetric security key, ensure it has a private key.
if (key is AsymmetricSecurityKey asymmetricSecurityKey && if (key is AsymmetricSecurityKey { PrivateKeyStatus: PrivateKeyStatus.DoesNotExist })
asymmetricSecurityKey.PrivateKeyStatus is PrivateKeyStatus.DoesNotExist)
{ {
throw new InvalidOperationException(SR.GetResourceString(SR.ID0067)); 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. // If no valid existing certificate was found, create a new signing certificate.
var certificates = store.Certificates var certificates = store.Certificates
.Find(X509FindType.FindBySubjectDistinguishedName, subject.Name, validOnly: false) .Find(X509FindType.FindBySubjectDistinguishedName, subject.Name, validOnly: false)
.OfType<X509Certificate2>() .Cast<X509Certificate2>()
.ToList(); .ToList();
if (!certificates.Exists(certificate => if (!certificates.Exists(certificate =>
@ -735,7 +735,7 @@ public sealed class OpenIddictServerBuilder
// If the certificate is a X.509v3 certificate that specifies at least // 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. // 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<X509KeyUsageExtension>().ToList(); var extensions = certificate.Extensions.OfType<X509KeyUsageExtension>().ToList();
if (extensions.Count is not 0 && !extensions.Exists(static extension => if (extensions.Count is not 0 && !extensions.Exists(static extension =>
@ -854,7 +854,7 @@ public sealed class OpenIddictServerBuilder
store.Open(OpenFlags.ReadOnly); store.Open(OpenFlags.ReadOnly);
return store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false) return store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false)
.OfType<X509Certificate2>() .Cast<X509Certificate2>()
.SingleOrDefault(); .SingleOrDefault();
} }
} }
@ -875,7 +875,7 @@ public sealed class OpenIddictServerBuilder
return AddSigningCertificate( return AddSigningCertificate(
store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false) store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false)
.OfType<X509Certificate2>() .Cast<X509Certificate2>()
.SingleOrDefault() ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0066))); .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 /// 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. /// is returned from the discovery endpoint to identify the authorization server.
/// </summary> /// </summary>
/// <param name="uri">The issuer uri.</param> /// <param name="uri">The issuer URI.</param>
/// <returns>The <see cref="OpenIddictServerBuilder"/> instance.</returns> /// <returns>The <see cref="OpenIddictServerBuilder"/> instance.</returns>
public OpenIddictServerBuilder SetIssuer(Uri uri) 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 /// 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. /// is returned from the discovery endpoint to identify the authorization server.
/// </summary> /// </summary>
/// <param name="uri">The issuer uri.</param> /// <param name="uri">The issuer URI.</param>
/// <returns>The <see cref="OpenIddictServerBuilder"/> instance.</returns> /// <returns>The <see cref="OpenIddictServerBuilder"/> instance.</returns>
public OpenIddictServerBuilder SetIssuer( public OpenIddictServerBuilder SetIssuer(
[StringSyntax(StringSyntaxAttribute.Uri, UriKind.Absolute)] string uri) [StringSyntax(StringSyntaxAttribute.Uri, UriKind.Absolute)] string uri)
@ -1921,6 +1921,256 @@ public sealed class OpenIddictServerBuilder
return SetIssuer(value); return SetIssuer(value);
} }
/// <summary>
/// Sets the URI listed as the mTLS device authorization
/// endpoint alias in the server configuration metadata.
/// </summary>
/// <remarks>
/// Note: this URI MUST be absolute and MUST point to a domain for
/// which TLS client authentication is enforced by the web server.
/// </remarks>
/// <param name="uri">The endpoint URI.</param>
/// <returns>The <see cref="OpenIddictServerBuilder"/> instance.</returns>
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);
}
/// <summary>
/// Sets the URI listed as the mTLS device authorization
/// endpoint alias in the server configuration metadata.
/// </summary>
/// <remarks>
/// Note: this URI MUST be absolute and MUST point to a domain for
/// which TLS client authentication is enforced by the web server.
/// </remarks>
/// <param name="uri">The endpoint URI.</param>
/// <returns>The <see cref="OpenIddictServerBuilder"/> instance.</returns>
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);
}
/// <summary>
/// Sets the URI listed as the mTLS introspection
/// endpoint alias in the server configuration metadata.
/// </summary>
/// <remarks>
/// Note: this URI MUST be absolute and MUST point to a domain for
/// which TLS client authentication is enforced by the web server.
/// </remarks>
/// <param name="uri">The endpoint URI.</param>
/// <returns>The <see cref="OpenIddictServerBuilder"/> instance.</returns>
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);
}
/// <summary>
/// Sets the URI listed as the mTLS introspection endpoint
/// alias in the server configuration metadata.
/// </summary>
/// <remarks>
/// Note: this URI MUST be absolute and MUST point to a domain for
/// which TLS client authentication is enforced by the web server.
/// </remarks>
/// <param name="uri">The endpoint URI.</param>
/// <returns>The <see cref="OpenIddictServerBuilder"/> instance.</returns>
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);
}
/// <summary>
/// Sets the URI listed as the mTLS pushed authorization
/// endpoint alias in the server configuration metadata.
/// </summary>
/// <remarks>
/// Note: this URI MUST be absolute and MUST point to a domain for
/// which TLS client authentication is enforced by the web server.
/// </remarks>
/// <param name="uri">The endpoint URI.</param>
/// <returns>The <see cref="OpenIddictServerBuilder"/> instance.</returns>
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);
}
/// <summary>
/// Sets the URI listed as the mTLS pushed authorization
/// endpoint alias in the server configuration metadata.
/// </summary>
/// <remarks>
/// Note: this URI MUST be absolute and MUST point to a domain for
/// which TLS client authentication is enforced by the web server.
/// </remarks>
/// <param name="uri">The endpoint URI.</param>
/// <returns>The <see cref="OpenIddictServerBuilder"/> instance.</returns>
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);
}
/// <summary>
/// Sets the URI listed as the mTLS revocation endpoint
/// alias in the server configuration metadata.
/// </summary>
/// <remarks>
/// Note: this URI MUST be absolute and MUST point to a domain for
/// which TLS client authentication is enforced by the web server.
/// </remarks>
/// <param name="uri">The endpoint URI.</param>
/// <returns>The <see cref="OpenIddictServerBuilder"/> instance.</returns>
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);
}
/// <summary>
/// Sets the URI listed as the mTLS revocation endpoint
/// alias in the server configuration metadata.
/// </summary>
/// <remarks>
/// Note: this URI MUST be absolute and MUST point to a domain for
/// which TLS client authentication is enforced by the web server.
/// </remarks>
/// <param name="uri">The endpoint URI.</param>
/// <returns>The <see cref="OpenIddictServerBuilder"/> instance.</returns>
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);
}
/// <summary>
/// Sets the URI listed as the mTLS token endpoint
/// alias in the server configuration metadata.
/// </summary>
/// <remarks>
/// Note: this URI MUST be absolute and MUST point to a domain for
/// which TLS client authentication is enforced by the web server.
/// </remarks>
/// <param name="uri">The endpoint URI.</param>
/// <returns>The <see cref="OpenIddictServerBuilder"/> instance.</returns>
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);
}
/// <summary>
/// Sets the URI listed as the mTLS token endpoint
/// alias in the server configuration metadata.
/// </summary>
/// <remarks>
/// Note: this URI MUST be absolute and MUST point to a domain for
/// which TLS client authentication is enforced by the web server.
/// </remarks>
/// <param name="uri">The endpoint URI.</param>
/// <returns>The <see cref="OpenIddictServerBuilder"/> instance.</returns>
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);
}
/// <summary> /// <summary>
/// Configures OpenIddict to use reference tokens, so that the access token payloads /// 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). /// 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() public OpenIddictServerBuilder EnableEndSessionRequestCaching()
=> Configure(options => options.EnableEndSessionRequestCaching = true); => Configure(options => options.EnableEndSessionRequestCaching = true);
/// <summary>
/// Configures OpenIddict to enable PKI client certificate authentication (mTLS) and trust
/// the specified root and intermediate certificates when validating client certificates.
/// </summary>
/// <param name="certificates">The store containing the root and intermediate certificates to trust.</param>
/// <returns>The <see cref="OpenIddictServerBuilder"/> instance.</returns>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public OpenIddictServerBuilder EnablePublicKeyInfrastructureClientCertificateAuthentication(X509Certificate2Collection certificates)
=> EnablePublicKeyInfrastructureClientCertificateAuthentication(certificates, static policy => { });
/// <summary>
/// Configures OpenIddict to enable PKI client certificate authentication (mTLS) and trust
/// the specified root and intermediate certificates when validating client certificates.
/// </summary>
/// <param name="certificates">The store containing the root and intermediate certificates to trust.</param>
/// <param name="configuration">The delegate used to amend the created X.509 chain policy.</param>
/// <returns>The <see cref="OpenIddictServerBuilder"/> instance.</returns>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public OpenIddictServerBuilder EnablePublicKeyInfrastructureClientCertificateAuthentication(
X509Certificate2Collection certificates, Action<X509ChainPolicy> 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<X509Certificate2>().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<X509Certificate2>().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<X509Certificate2>().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<X509Certificate2>()
.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<X509Certificate2>()
.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<X509Certificate2>()
.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
}
/// <summary>
/// Configures OpenIddict to enable self-signed client certificate authentication (mTLS).
/// </summary>
/// <returns>The <see cref="OpenIddictServerBuilder"/> instance.</returns>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public OpenIddictServerBuilder EnableSelfSignedClientCertificateAuthentication()
=> EnableSelfSignedClientCertificateAuthentication(static policy => { });
/// <summary>
/// Configures OpenIddict to enable self-signed client certificate authentication (mTLS).
/// </summary>
/// <param name="configuration">The delegate used to amend the created X.509 chain policy.</param>
/// <returns>The <see cref="OpenIddictServerBuilder"/> instance.</returns>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public OpenIddictServerBuilder EnableSelfSignedClientCertificateAuthentication(Action<X509ChainPolicy> 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
}
/// <inheritdoc/> /// <inheritdoc/>
[EditorBrowsable(EditorBrowsableState.Never)] [EditorBrowsable(EditorBrowsableState.Never)]
public override bool Equals(object? obj) => base.Equals(obj); public override bool Equals(object? obj) => base.Equals(obj);

110
src/OpenIddict.Server/OpenIddictServerConfiguration.cs

@ -7,6 +7,7 @@
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.Security.Cryptography.X509Certificates;
using System.Text; using System.Text;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
@ -164,6 +165,19 @@ public sealed class OpenIddictServerConfiguration : IPostConfigureOptions<OpenId
ClientAssertionTypes.JwtBearer, ClientAuthenticationMethods.ClientSecretJwt)); ClientAssertionTypes.JwtBearer, ClientAuthenticationMethods.ClientSecretJwt));
} }
// If the tls_client_auth or self_signed_tls_client_auth methods are enabled, ensure a chain policy has been set.
if (options.ClientAuthenticationMethods.Contains(ClientAuthenticationMethods.TlsClientAuth) &&
options.ClientCertificateChainPolicy is null)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0505));
}
if (options.ClientAuthenticationMethods.Contains(ClientAuthenticationMethods.SelfSignedTlsClientAuth) &&
options.SelfSignedClientCertificateChainPolicy is null)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0506));
}
// Ensure at least one supported subject type is listed. // Ensure at least one supported subject type is listed.
if (options.SubjectTypes.Count is 0) if (options.SubjectTypes.Count is 0)
{ {
@ -246,19 +260,104 @@ public sealed class OpenIddictServerConfiguration : IPostConfigureOptions<OpenId
var now = options.TimeProvider.GetUtcNow().LocalDateTime; var now = options.TimeProvider.GetUtcNow().LocalDateTime;
// If all the registered encryption credentials are backed by a X.509 certificate, at least one of them must be valid. // If all the registered encryption credentials are backed by a X.509 certificate, at least one of them must be valid.
if (options.EncryptionCredentials.TrueForAll(credentials => credentials.Key is X509SecurityKey x509SecurityKey && if (options.EncryptionCredentials.TrueForAll(credentials =>
(x509SecurityKey.Certificate.NotBefore > now || x509SecurityKey.Certificate.NotAfter < now))) credentials.Key is X509SecurityKey { Certificate: X509Certificate2 certificate } &&
(certificate.NotBefore > now || certificate.NotAfter < now)))
{ {
throw new InvalidOperationException(SR.GetResourceString(SR.ID0087)); 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 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 && if (options.SigningCredentials.TrueForAll(credentials =>
(x509SecurityKey.Certificate.NotBefore > now || x509SecurityKey.Certificate.NotAfter < now))) credentials.Key is X509SecurityKey { Certificate: X509Certificate2 certificate } &&
(certificate.NotBefore > now || certificate.NotAfter < now)))
{ {
throw new InvalidOperationException(SR.GetResourceString(SR.ID0088)); 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<X509Certificate2>()
.Any(static certificate =>
!OpenIddictHelpers.IsCertificateAuthority(certificate) ||
!OpenIddictHelpers.HasKeyUsage(certificate, X509KeyUsageFlags.KeyCertSign)))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0501));
}
if (options.ClientCertificateChainPolicy.ExtraStore.Cast<X509Certificate2>()
.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<X509Certificate2>()
.Any(static certificate =>
!OpenIddictHelpers.IsCertificateAuthority(certificate) ||
!OpenIddictHelpers.HasKeyUsage(certificate, X509KeyUsageFlags.KeyCertSign)))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0501));
}
if (options.ClientCertificateChainPolicy.CustomTrustStore.Cast<X509Certificate2>()
.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<X509Certificate2>().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<X509Certificate2>().Any())
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0502));
}
#endif
}
if (options.EnableDegradedMode) if (options.EnableDegradedMode)
{ {
// If the degraded mode was enabled, ensure custom validation handlers // If the degraded mode was enabled, ensure custom validation handlers
@ -529,5 +628,8 @@ public sealed class OpenIddictServerConfiguration : IPostConfigureOptions<OpenId
return null; return null;
} }
static bool TryValidateMtlsEndpointAlias(Uri? uri) => uri is null ||
(uri.IsAbsoluteUri && string.Equals(uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase));
} }
} }

25
src/OpenIddict.Server/OpenIddictServerEvents.Discovery.cs

@ -136,6 +136,31 @@ public static partial class OpenIddictServerEvents
/// </summary> /// </summary>
public Uri? UserInfoEndpoint { get; set; } public Uri? UserInfoEndpoint { get; set; }
/// <summary>
/// Gets or sets the mTLS device authorization endpoint alias URI.
/// </summary>
public Uri? MtlsDeviceAuthorizationEndpointAlias { get; set; }
/// <summary>
/// Gets or sets the mTLS-specific introspection endpoint alias URI.
/// </summary>
public Uri? MtlsIntrospectionEndpointAlias { get; set; }
/// <summary>
/// Gets or sets the mTLS pushed authorization endpoint alias URI.
/// </summary>
public Uri? MtlsPushedAuthorizationEndpointAlias { get; set; }
/// <summary>
/// Gets or sets the mTLS revocation endpoint alias URI.
/// </summary>
public Uri? MtlsRevocationEndpointAlias { get; set; }
/// <summary>
/// Gets or sets the mTLS token endpoint alias URI.
/// </summary>
public Uri? MtlsTokenEndpointAlias { get; set; }
/// <summary> /// <summary>
/// Gets the list of claims supported by the authorization server. /// Gets the list of claims supported by the authorization server.
/// </summary> /// </summary>

10
src/OpenIddict.Server/OpenIddictServerEvents.cs

@ -6,6 +6,7 @@
using System.ComponentModel; using System.ComponentModel;
using System.Security.Claims; using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace OpenIddict.Server; namespace OpenIddict.Server;
@ -764,6 +765,15 @@ public static partial class OpenIddictServerEvents
/// </summary> /// </summary>
public ClaimsPrincipal? ClientAssertionPrincipal { get; set; } public ClaimsPrincipal? ClientAssertionPrincipal { get; set; }
/// <summary>
/// Gets or sets the client certificate (typically obtained via mTLS), if applicable.
/// </summary>
public X509Certificate2? ClientCertificate
{
get => Transaction.ClientCertificate;
set => Transaction.ClientCertificate = value;
}
/// <summary> /// <summary>
/// Gets or sets the device code to validate, if applicable. /// Gets or sets the device code to validate, if applicable.
/// </summary> /// </summary>

1
src/OpenIddict.Server/OpenIddictServerExtensions.cs

@ -49,6 +49,7 @@ public static class OpenIddictServerExtensions
builder.Services.TryAddSingleton<RequireAuthorizationRequest>(); builder.Services.TryAddSingleton<RequireAuthorizationRequest>();
builder.Services.TryAddSingleton<RequireClientAssertionPrincipal>(); builder.Services.TryAddSingleton<RequireClientAssertionPrincipal>();
builder.Services.TryAddSingleton<RequireClientAssertionValidated>(); builder.Services.TryAddSingleton<RequireClientAssertionValidated>();
builder.Services.TryAddSingleton<RequireClientCertificate>();
builder.Services.TryAddSingleton<RequireClientIdParameter>(); builder.Services.TryAddSingleton<RequireClientIdParameter>();
builder.Services.TryAddSingleton<RequireClientSecretParameter>(); builder.Services.TryAddSingleton<RequireClientSecretParameter>();
builder.Services.TryAddSingleton<RequireConfigurationRequest>(); builder.Services.TryAddSingleton<RequireConfigurationRequest>();

14
src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs

@ -179,6 +179,20 @@ public static class OpenIddictServerHandlerFilters
} }
} }
/// <summary>
/// Represents a filter that excludes the associated handlers if no client authentication certificate is available.
/// </summary>
public sealed class RequireClientCertificate : IOpenIddictServerHandlerFilter<ProcessAuthenticationContext>
{
/// <inheritdoc/>
public ValueTask<bool> IsActiveAsync(ProcessAuthenticationContext context)
{
ArgumentNullException.ThrowIfNull(context);
return new(context.ClientCertificate is not null);
}
}
/// <summary> /// <summary>
/// Represents a filter that excludes the associated handlers when no client identifier is received. /// Represents a filter that excludes the associated handlers when no client identifier is received.
/// </summary> /// </summary>

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

@ -8,6 +8,7 @@ using System.Collections.Immutable;
using System.Diagnostics; using System.Diagnostics;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
@ -235,6 +236,7 @@ public static partial class OpenIddictServerHandlers
[Metadata.UserInfoEndpoint] = notification.UserInfoEndpoint?.AbsoluteUri, [Metadata.UserInfoEndpoint] = notification.UserInfoEndpoint?.AbsoluteUri,
[Metadata.DeviceAuthorizationEndpoint] = notification.DeviceAuthorizationEndpoint?.AbsoluteUri, [Metadata.DeviceAuthorizationEndpoint] = notification.DeviceAuthorizationEndpoint?.AbsoluteUri,
[Metadata.PushedAuthorizationRequestEndpoint] = notification.PushedAuthorizationEndpoint?.AbsoluteUri, [Metadata.PushedAuthorizationRequestEndpoint] = notification.PushedAuthorizationEndpoint?.AbsoluteUri,
[Metadata.MtlsEndpointAliases] = CreateMtlsEndpointAliases(notification),
[Metadata.JwksUri] = notification.JsonWebKeySetEndpoint?.AbsoluteUri, [Metadata.JwksUri] = notification.JsonWebKeySetEndpoint?.AbsoluteUri,
[Metadata.GrantTypesSupported] = notification.GrantTypes.ToImmutableArray<string?>(), [Metadata.GrantTypesSupported] = notification.GrantTypes.ToImmutableArray<string?>(),
[Metadata.ResponseTypesSupported] = notification.ResponseTypes.ToImmutableArray<string?>(), [Metadata.ResponseTypesSupported] = notification.ResponseTypes.ToImmutableArray<string?>(),
@ -259,6 +261,38 @@ public static partial class OpenIddictServerHandlers
} }
context.Transaction.Response = response; 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.JsonWebKeySetEndpoint ??= OpenIddictHelpers.CreateAbsoluteUri(
context.BaseUri, context.Options.JsonWebKeySetEndpointUris.FirstOrDefault()); 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.PushedAuthorizationEndpoint ??= OpenIddictHelpers.CreateAbsoluteUri(
context.BaseUri, context.Options.PushedAuthorizationEndpointUris.FirstOrDefault()); context.BaseUri, context.Options.PushedAuthorizationEndpointUris.FirstOrDefault());

2
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. // 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. // Visit https://datatracker.ietf.org/doc/html/rfc9068 for more information.
var scopes = context.Principal.GetClaims(Claims.Scope); var scopes = context.Principal.GetClaims(Claims.Scope);
if (scopes.Length > 1) if (scopes.Length is > 1)
{ {
context.Principal.SetClaim(Claims.Scope, string.Join(" ", scopes)); context.Principal.SetClaim(Claims.Scope, string.Join(" ", scopes));
} }

134
src/OpenIddict.Server/OpenIddictServerHandlers.cs

@ -8,7 +8,9 @@ using System.Collections.Immutable;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.Runtime.InteropServices;
using System.Security.Claims; using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -36,13 +38,14 @@ public static partial class OpenIddictServerHandlers
EvaluateValidatedTokens.Descriptor, EvaluateValidatedTokens.Descriptor,
ResolveValidatedTokens.Descriptor, ResolveValidatedTokens.Descriptor,
ValidateRequiredTokens.Descriptor, ValidateRequiredTokens.Descriptor,
ValidateClientId.Descriptor,
ValidateClientType.Descriptor,
ValidateClientSecret.Descriptor,
ValidateClientAssertion.Descriptor, ValidateClientAssertion.Descriptor,
ValidateClientAssertionWellknownClaims.Descriptor, ValidateClientAssertionWellknownClaims.Descriptor,
ValidateClientAssertionIssuer.Descriptor, ValidateClientAssertionIssuer.Descriptor,
ValidateClientAssertionAudience.Descriptor, ValidateClientAssertionAudience.Descriptor,
ValidateClientId.Descriptor,
ValidateClientType.Descriptor,
ValidateClientSecret.Descriptor,
ValidateClientCertificate.Descriptor,
ValidateRequestToken.Descriptor, ValidateRequestToken.Descriptor,
ValidateRequestTokenType.Descriptor, ValidateRequestTokenType.Descriptor,
ValidateAccessToken.Descriptor, ValidateAccessToken.Descriptor,
@ -1116,6 +1119,19 @@ public static partial class OpenIddictServerHandlers
return; 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. // Reject requests containing a client_assertion when the client is a public application.
if (!string.IsNullOrEmpty(context.ClientAssertion)) if (!string.IsNullOrEmpty(context.ClientAssertion))
{ {
@ -1145,15 +1161,16 @@ public static partial class OpenIddictServerHandlers
return; return;
} }
// Confidential and hybrid applications MUST authenticate to protect them from impersonation attacks. // Confidential applications MUST authenticate to protect them from impersonation attacks.
if (context.ClientAssertionPrincipal is null && string.IsNullOrEmpty(context.ClientSecret)) if (context.ClientAssertionPrincipal is null &&
context.ClientCertificate is null && string.IsNullOrEmpty(context.ClientSecret))
{ {
context.Logger.LogInformation(6224, SR.GetResourceString(SR.ID6224), context.ClientId); context.Logger.LogInformation(6224, SR.GetResourceString(SR.ID6224), context.ClientId);
context.Reject( context.Reject(
error: Errors.InvalidClient, error: Errors.InvalidClient,
description: SR.FormatID2054(Parameters.ClientSecret), description: SR.GetResourceString(SR.ID2198),
uri: SR.FormatID8000(SR.ID2054)); uri: SR.FormatID8000(SR.ID2198));
return; return;
} }
@ -1226,6 +1243,107 @@ public static partial class OpenIddictServerHandlers
} }
} }
/// <summary>
/// Contains the logic responsible for validating the TLS client certificate used for client authentication, if applicable.
/// </summary>
public sealed class ValidateClientCertificate : IOpenIddictServerHandler<ProcessAuthenticationContext>
{
private readonly IOpenIddictApplicationManager _applicationManager;
public ValidateClientCertificate() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016));
public ValidateClientCertificate(IOpenIddictApplicationManager applicationManager)
=> _applicationManager = applicationManager ?? throw new ArgumentNullException(nameof(applicationManager));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireClientIdParameter>()
.AddFilter<RequireClientCertificate>()
.AddFilter<RequireDegradedModeDisabled>()
.UseScopedHandler<ValidateClientCertificate>()
.SetOrder(ValidateClientSecret.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
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;
}
}
}
}
/// <summary> /// <summary>
/// Contains the logic responsible for validating the request token resolved from the context. /// Contains the logic responsible for validating the request token resolved from the context.
/// </summary> /// </summary>
@ -1243,7 +1361,7 @@ public static partial class OpenIddictServerHandlers
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>() = OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireRequestTokenValidated>() .AddFilter<RequireRequestTokenValidated>()
.UseScopedHandler<ValidateRequestToken>() .UseScopedHandler<ValidateRequestToken>()
.SetOrder(ValidateClientSecret.Descriptor.Order + 1_000) .SetOrder(ValidateClientCertificate.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn) .SetType(OpenIddictServerHandlerType.BuiltIn)
.Build(); .Build();

89
src/OpenIddict.Server/OpenIddictServerOptions.cs

@ -5,6 +5,7 @@
*/ */
using System.ComponentModel; using System.ComponentModel;
using System.Security.Cryptography.X509Certificates;
using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
@ -127,6 +128,56 @@ public sealed class OpenIddictServerOptions
SetDefaultTimesOnTokenCreation = false SetDefaultTimesOnTokenCreation = false
}; };
/// <summary>
/// Gets or sets the URI listed as the mTLS device authorization
/// endpoint alias in the server configuration metadata.
/// </summary>
/// <remarks>
/// Note: this URI MUST be absolute and MUST point to a domain for
/// which TLS client authentication is enforced by the web server.
/// </remarks>
public Uri? MtlsDeviceAuthorizationEndpointAliasUri { get; set; }
/// <summary>
/// Gets or sets the URI listed as the mTLS introspection
/// endpoint alias in the server configuration metadata.
/// </summary>
/// <remarks>
/// Note: this URI MUST be absolute and MUST point to a domain for
/// which TLS client authentication is enforced by the web server.
/// </remarks>
public Uri? MtlsIntrospectionEndpointAliasUri { get; set; }
/// <summary>
/// Gets or sets the URI listed as the mTLS pushed authorization
/// endpoint alias in the server configuration metadata.
/// </summary>
/// <remarks>
/// Note: this URI MUST be absolute and MUST point to a domain for
/// which TLS client authentication is enforced by the web server.
/// </remarks>
public Uri? MtlsPushedAuthorizationEndpointAliasUri { get; set; }
/// <summary>
/// Gets or sets the URI listed as the mTLS revocation
/// endpoint alias in the server configuration metadata.
/// </summary>
/// <remarks>
/// Note: this URI MUST be absolute and MUST point to a domain for
/// which TLS client authentication is enforced by the web server.
/// </remarks>
public Uri? MtlsRevocationEndpointAliasUri { get; set; }
/// <summary>
/// Gets or sets the URI listed as the mTLS token
/// endpoint alias in the server configuration metadata.
/// </summary>
/// <remarks>
/// Note: this URI MUST be absolute and MUST point to a domain for
/// which TLS client authentication is enforced by the web server.
/// </remarks>
public Uri? MtlsTokenEndpointAliasUri { get; set; }
/// <summary> /// <summary>
/// Gets the token validation parameters used by the OpenIddict server services. /// Gets the token validation parameters used by the OpenIddict server services.
/// </summary> /// </summary>
@ -605,4 +656,42 @@ public sealed class OpenIddictServerOptions
/// If no service can be found, <see cref="TimeProvider.System"/> is used. /// If no service can be found, <see cref="TimeProvider.System"/> is used.
/// </remarks> /// </remarks>
public TimeProvider TimeProvider { get; set; } = default!; public TimeProvider TimeProvider { get; set; } = default!;
/// <summary>
/// Gets or sets the chain policy used when validating client certificates
/// used for client authentication (typically, via mTLS).
/// </summary>
/// <remarks>
/// <list type="bullet">
/// <item>
/// Note: this instance serves as a base policy and is merged with
/// the per-client policies resolved using the application manager.
/// </item>
/// <item>
/// Note: while it is possible to use a policy configured to use the
/// the system certificates store, doing is so is strongly discouraged.
/// </item>
/// </list>
/// </remarks>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public X509ChainPolicy? ClientCertificateChainPolicy { get; set; }
/// <summary>
/// Gets or sets the chain policy used when validating self-signed client
/// certificates used for client authentication (typically, via mTLS).
/// </summary>
/// <remarks>
/// <list type="bullet">
/// <item>
/// Note: this instance serves as a base policy and is merged with
/// the per-client policies resolved using the application manager.
/// </item>
/// <item>
/// Note: while it is possible to use a policy configured to use the
/// the system certificates store, doing is so is strongly discouraged.
/// </item>
/// </list>
/// </remarks>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public X509ChainPolicy? SelfSignedClientCertificateChainPolicy { get; set; }
} }

6
src/OpenIddict.Server/OpenIddictServerTransaction.cs

@ -5,6 +5,7 @@
*/ */
using System.ComponentModel; using System.ComponentModel;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace OpenIddict.Server; namespace OpenIddict.Server;
@ -26,6 +27,11 @@ public sealed class OpenIddictServerTransaction
/// </remarks> /// </remarks>
public CancellationToken CancellationToken { get; set; } public CancellationToken CancellationToken { get; set; }
/// <summary>
/// Gets or sets the X.509 client certificate, if available.
/// </summary>
public X509Certificate2? ClientCertificate { get; set; }
/// <summary> /// <summary>
/// Gets or sets the type of the endpoint processing the current request. /// Gets or sets the type of the endpoint processing the current request.
/// </summary> /// </summary>

2
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 // 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 // is dynamically added if one of the default schemes properties is not set
// and less than 2 handlers were registered in the authentication options. // 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.DefaultSignInScheme) ||
string.IsNullOrEmpty(options.DefaultSignOutScheme))) string.IsNullOrEmpty(options.DefaultSignOutScheme)))
{ {

62
src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpConfiguration.cs

@ -253,10 +253,12 @@ public sealed class OpenIddictValidationSystemNetHttpConfiguration : IConfigureO
foreach (var credentials in _provider.GetRequiredService<IOptionsMonitor<OpenIddictValidationOptions>>() foreach (var credentials in _provider.GetRequiredService<IOptionsMonitor<OpenIddictValidationOptions>>()
.CurrentValue.SigningCredentials) .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 } && if (credentials.Key is X509SecurityKey { Certificate: X509Certificate2 certificate } &&
certificate.Version is >= 3 && IsSelfIssuedCertificate(certificate) && certificate.Version is >= 3 && OpenIddictHelpers.IsSelfIssuedCertificate(certificate) &&
HasDigitalSignatureKeyUsage(certificate) && OpenIddictHelpers.HasKeyUsage(certificate, X509KeyUsageFlags.DigitalSignature) &&
HasClientAuthenticationExtendedKeyUsage(certificate)) OpenIddictHelpers.HasExtendedKeyUsage(certificate, ObjectIdentifiers.ExtendedKeyUsages.ClientAuthentication))
{ {
return certificate; return certificate;
} }
@ -270,10 +272,12 @@ public sealed class OpenIddictValidationSystemNetHttpConfiguration : IConfigureO
foreach (var credentials in _provider.GetRequiredService<IOptionsMonitor<OpenIddictValidationOptions>>() foreach (var credentials in _provider.GetRequiredService<IOptionsMonitor<OpenIddictValidationOptions>>()
.CurrentValue.SigningCredentials) .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 } && if (credentials.Key is X509SecurityKey { Certificate: X509Certificate2 certificate } &&
certificate.Version is >= 3 && !IsSelfIssuedCertificate(certificate) && certificate.Version is >= 3 && !OpenIddictHelpers.IsSelfIssuedCertificate(certificate) &&
HasDigitalSignatureKeyUsage(certificate) && OpenIddictHelpers.HasKeyUsage(certificate, X509KeyUsageFlags.DigitalSignature) &&
HasClientAuthenticationExtendedKeyUsage(certificate)) OpenIddictHelpers.HasExtendedKeyUsage(certificate, ObjectIdentifiers.ExtendedKeyUsages.ClientAuthentication))
{ {
return certificate; return certificate;
} }
@ -281,51 +285,5 @@ public sealed class OpenIddictValidationSystemNetHttpConfiguration : IConfigureO
return null; 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);
} }
} }

18
src/OpenIddict.Validation/OpenIddictValidationBuilder.cs

@ -134,8 +134,7 @@ public sealed class OpenIddictValidationBuilder
ArgumentNullException.ThrowIfNull(key); ArgumentNullException.ThrowIfNull(key);
// If the encryption key is an asymmetric security key, ensure it has a private key. // If the encryption key is an asymmetric security key, ensure it has a private key.
if (key is AsymmetricSecurityKey asymmetricSecurityKey && if (key is AsymmetricSecurityKey { PrivateKeyStatus: PrivateKeyStatus.DoesNotExist })
asymmetricSecurityKey.PrivateKeyStatus is PrivateKeyStatus.DoesNotExist)
{ {
throw new InvalidOperationException(SR.GetResourceString(SR.ID0055)); 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 // 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. // 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<X509KeyUsageExtension>().ToList(); var extensions = certificate.Extensions.OfType<X509KeyUsageExtension>().ToList();
if (extensions.Count is not 0 && !extensions.Exists(static extension => if (extensions.Count is not 0 && !extensions.Exists(static extension =>
@ -299,7 +298,7 @@ public sealed class OpenIddictValidationBuilder
store.Open(OpenFlags.ReadOnly); store.Open(OpenFlags.ReadOnly);
return store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false) return store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false)
.OfType<X509Certificate2>() .Cast<X509Certificate2>()
.SingleOrDefault(); .SingleOrDefault();
} }
} }
@ -321,7 +320,7 @@ public sealed class OpenIddictValidationBuilder
return AddEncryptionCertificate( return AddEncryptionCertificate(
store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false) store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false)
.OfType<X509Certificate2>() .Cast<X509Certificate2>()
.SingleOrDefault() ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0066))); .SingleOrDefault() ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0066)));
} }
@ -359,8 +358,7 @@ public sealed class OpenIddictValidationBuilder
ArgumentNullException.ThrowIfNull(key); ArgumentNullException.ThrowIfNull(key);
// If the signing key is an asymmetric security key, ensure it has a private key. // If the signing key is an asymmetric security key, ensure it has a private key.
if (key is AsymmetricSecurityKey asymmetricSecurityKey && if (key is AsymmetricSecurityKey { PrivateKeyStatus: PrivateKeyStatus.DoesNotExist })
asymmetricSecurityKey.PrivateKeyStatus is PrivateKeyStatus.DoesNotExist)
{ {
throw new InvalidOperationException(SR.GetResourceString(SR.ID0067)); 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 // 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. // 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<X509KeyUsageExtension>().ToList(); var extensions = certificate.Extensions.OfType<X509KeyUsageExtension>().ToList();
if (extensions.Count is not 0 && !extensions.Exists(static extension => if (extensions.Count is not 0 && !extensions.Exists(static extension =>
@ -545,7 +543,7 @@ public sealed class OpenIddictValidationBuilder
store.Open(OpenFlags.ReadOnly); store.Open(OpenFlags.ReadOnly);
return store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false) return store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false)
.OfType<X509Certificate2>() .Cast<X509Certificate2>()
.SingleOrDefault(); .SingleOrDefault();
} }
} }
@ -566,7 +564,7 @@ public sealed class OpenIddictValidationBuilder
return AddSigningCertificate( return AddSigningCertificate(
store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false) store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false)
.OfType<X509Certificate2>() .Cast<X509Certificate2>()
.SingleOrDefault() ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0066))); .SingleOrDefault() ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0066)));
} }

7
src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs

@ -4,6 +4,7 @@
* the license and the contributors participating to this project. * the license and the contributors participating to this project.
*/ */
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Protocols;
@ -97,9 +98,9 @@ public sealed class OpenIddictValidationConfiguration : IPostConfigureOptions<Op
var now = options.TimeProvider.GetUtcNow().LocalDateTime; var now = options.TimeProvider.GetUtcNow().LocalDateTime;
// If all the registered encryption credentials are backed by a X.509 certificate, at least one of them must be valid. // If all the registered encryption credentials are backed by a X.509 certificate, at least one of them must be valid.
if (options.EncryptionCredentials.Count is not 0 && if (options.EncryptionCredentials.Count is not 0 && options.EncryptionCredentials.TrueForAll(credentials =>
options.EncryptionCredentials.TrueForAll(credentials => credentials.Key is X509SecurityKey x509SecurityKey && credentials.Key is X509SecurityKey { Certificate: X509Certificate2 certificate } &&
(x509SecurityKey.Certificate.NotBefore > now || x509SecurityKey.Certificate.NotAfter < now))) (certificate.NotBefore > now || certificate.NotAfter < now)))
{ {
throw new InvalidOperationException(SR.GetResourceString(SR.ID0087)); throw new InvalidOperationException(SR.GetResourceString(SR.ID0087));
} }

2
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. // 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. // Visit https://datatracker.ietf.org/doc/html/rfc9068 for more information.
var scopes = context.Principal.GetClaims(Claims.Scope); var scopes = context.Principal.GetClaims(Claims.Scope);
if (scopes.Length > 1) if (scopes.Length is > 1)
{ {
context.Principal.SetClaim(Claims.Scope, string.Join(" ", scopes)); context.Principal.SetClaim(Claims.Scope, string.Join(" ", scopes));
} }

124
test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Authentication.cs

@ -3214,130 +3214,6 @@ public abstract partial class OpenIddictServerIntegrationTests
Assert.Equal(SR.FormatID8000(SR.ID2029), response.ErrorUri); 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<CancellationToken>()))
.ReturnsAsync(application);
mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny<CancellationToken>()))
.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<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny<CancellationToken>()), 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<CancellationToken>()))
.ReturnsAsync(application);
mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny<CancellationToken>()))
.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<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny<CancellationToken>()), 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<CancellationToken>()))
.ReturnsAsync(application);
mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
mock.Setup(manager => manager.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny<CancellationToken>()))
.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<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(manager => manager.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny<CancellationToken>()), Times.Once());
}
[Fact] [Fact]
public async Task ValidatePushedAuthorizationRequest_MissingRedirectUriCausesAnErrorForOpenIdRequests() public async Task ValidatePushedAuthorizationRequest_MissingRedirectUriCausesAnErrorForOpenIdRequests()
{ {

118
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<CancellationToken>()), Times.AtLeastOnce()); Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()), 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<CancellationToken>()))
.ReturnsAsync(application);
mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny<CancellationToken>()))
.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<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny<CancellationToken>()), 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<CancellationToken>()))
.ReturnsAsync(application);
mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny<CancellationToken>()))
.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<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny<CancellationToken>()), 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<CancellationToken>()))
.ReturnsAsync(application);
mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
mock.Setup(manager => manager.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny<CancellationToken>()))
.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<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(manager => manager.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny<CancellationToken>()), Times.Once());
}
[Fact] [Fact]
public async Task ValidateDeviceAuthorizationRequest_RequestIsRejectedWhenEndpointPermissionIsNotGranted() public async Task ValidateDeviceAuthorizationRequest_RequestIsRejectedWhenEndpointPermissionIsNotGranted()
{ {

65
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") options.SetAuthorizationEndpointUris("https://www.fabrikam.com/path/authorization_endpoint")
.SetJsonWebKeySetEndpointUris("https://www.fabrikam.com/path/cryptography_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") .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") .SetRevocationEndpointUris("https://www.fabrikam.com/path/revocation_endpoint")
.SetTokenEndpointUris("https://www.fabrikam.com/path/token_endpoint") .SetTokenEndpointUris("https://www.fabrikam.com/path/token_endpoint")
.SetUserInfoEndpointUris("https://www.fabrikam.com/path/userinfo_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", Assert.Equal("https://www.fabrikam.com/path/authorization_endpoint",
(string?) response[Metadata.AuthorizationEndpoint]); (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]); (string?) response[Metadata.DeviceAuthorizationEndpoint]);
Assert.Equal("https://www.fabrikam.com/path/introspection_endpoint", Assert.Equal("https://www.fabrikam.com/path/introspection_endpoint",
(string?) response[Metadata.IntrospectionEndpoint]); (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]); (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", Assert.Equal("https://www.fabrikam.com/path/revocation_endpoint",
(string?) response[Metadata.RevocationEndpoint]); (string?) response[Metadata.RevocationEndpoint]);
@ -315,9 +319,9 @@ public abstract partial class OpenIddictServerIntegrationTests
{ {
options.SetAuthorizationEndpointUris("path/authorization_endpoint") options.SetAuthorizationEndpointUris("path/authorization_endpoint")
.SetJsonWebKeySetEndpointUris("path/cryptography_endpoint") .SetJsonWebKeySetEndpointUris("path/cryptography_endpoint")
.SetDeviceAuthorizationEndpointUris("path/device_endpoint") .SetDeviceAuthorizationEndpointUris("path/device_authorization_endpoint")
.SetIntrospectionEndpointUris("path/introspection_endpoint") .SetIntrospectionEndpointUris("path/introspection_endpoint")
.SetEndSessionEndpointUris("path/logout_endpoint") .SetEndSessionEndpointUris("path/end_session_endpoint")
.SetPushedAuthorizationEndpointUris("path/pushed_authorization_endpoint") .SetPushedAuthorizationEndpointUris("path/pushed_authorization_endpoint")
.SetRevocationEndpointUris("path/revocation_endpoint") .SetRevocationEndpointUris("path/revocation_endpoint")
.SetTokenEndpointUris("path/token_endpoint") .SetTokenEndpointUris("path/token_endpoint")
@ -332,9 +336,9 @@ public abstract partial class OpenIddictServerIntegrationTests
// Assert // Assert
Assert.Equal("http://localhost/path/authorization_endpoint", (string?) response[Metadata.AuthorizationEndpoint]); 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/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/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/pushed_authorization_endpoint", (string?) response[Metadata.PushedAuthorizationRequestEndpoint]);
Assert.Equal("http://localhost/path/revocation_endpoint", (string?) response[Metadata.RevocationEndpoint]); Assert.Equal("http://localhost/path/revocation_endpoint", (string?) response[Metadata.RevocationEndpoint]);
Assert.Equal("http://localhost/path/token_endpoint", (string?) response[Metadata.TokenEndpoint]); 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") options.SetAuthorizationEndpointUris("path/authorization_endpoint")
.SetJsonWebKeySetEndpointUris("path/cryptography_endpoint") .SetJsonWebKeySetEndpointUris("path/cryptography_endpoint")
.SetDeviceAuthorizationEndpointUris("path/device_endpoint") .SetDeviceAuthorizationEndpointUris("path/device_authorization_endpoint")
.SetIntrospectionEndpointUris("path/introspection_endpoint") .SetIntrospectionEndpointUris("path/introspection_endpoint")
.SetEndSessionEndpointUris("path/logout_endpoint") .SetEndSessionEndpointUris("path/end_session_endpoint")
.SetPushedAuthorizationEndpointUris("path/pushed_authorization_endpoint") .SetPushedAuthorizationEndpointUris("path/pushed_authorization_endpoint")
.SetRevocationEndpointUris("path/revocation_endpoint") .SetRevocationEndpointUris("path/revocation_endpoint")
.SetTokenEndpointUris("path/token_endpoint") .SetTokenEndpointUris("path/token_endpoint")
@ -378,9 +382,9 @@ public abstract partial class OpenIddictServerIntegrationTests
// Assert // Assert
Assert.Equal("https://contoso.com/issuer/path/authorization_endpoint", (string?) response[Metadata.AuthorizationEndpoint]); 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/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/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/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/revocation_endpoint", (string?) response[Metadata.RevocationEndpoint]);
Assert.Equal("https://contoso.com/issuer/path/token_endpoint", (string?) response[Metadata.TokenEndpoint]); 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]); 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] [Theory]
[InlineData(true)] [InlineData(true)]
[InlineData(false)] [InlineData(false)]

127
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<CancellationToken>()), Times.Once()); Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny<CancellationToken>()), 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<CancellationToken>()))
.ReturnsAsync(application);
mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny<CancellationToken>()))
.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<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny<CancellationToken>()), 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<CancellationToken>()))
.ReturnsAsync(application);
mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny<CancellationToken>()))
.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<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny<CancellationToken>()), 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<CancellationToken>()))
.ReturnsAsync(application);
mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
mock.Setup(manager => manager.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny<CancellationToken>()))
.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<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(manager => manager.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny<CancellationToken>()), Times.Once());
}
[Fact] [Fact]
public async Task ValidateTokenRequest_RequestIsRejectedWhenEndpointPermissionIsNotGranted() public async Task ValidateTokenRequest_RequestIsRejectedWhenEndpointPermissionIsNotGranted()
{ {

118
test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Introspection.cs

@ -364,124 +364,6 @@ public abstract partial class OpenIddictServerIntegrationTests
Permissions.Endpoints.Introspection, It.IsAny<CancellationToken>()), Times.Once()); Permissions.Endpoints.Introspection, It.IsAny<CancellationToken>()), 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<CancellationToken>()))
.ReturnsAsync(application);
mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny<CancellationToken>()))
.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<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny<CancellationToken>()), 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<CancellationToken>()))
.ReturnsAsync(application);
mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny<CancellationToken>()))
.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<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny<CancellationToken>()), 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<CancellationToken>()))
.ReturnsAsync(application);
mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
mock.Setup(manager => manager.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny<CancellationToken>()))
.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<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(manager => manager.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny<CancellationToken>()), Times.Once());
}
[Fact] [Fact]
public async Task ValidateIntrospectionRequest_InvalidTokenCausesAnError() public async Task ValidateIntrospectionRequest_InvalidTokenCausesAnError()
{ {

124
test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Revocation.cs

@ -365,130 +365,6 @@ public abstract partial class OpenIddictServerIntegrationTests
Permissions.Endpoints.Revocation, It.IsAny<CancellationToken>()), Times.Once()); Permissions.Endpoints.Revocation, It.IsAny<CancellationToken>()), 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<CancellationToken>()))
.ReturnsAsync(application);
mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny<CancellationToken>()))
.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<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny<CancellationToken>()), 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<CancellationToken>()))
.ReturnsAsync(application);
mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny<CancellationToken>()))
.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<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny<CancellationToken>()), 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<CancellationToken>()))
.ReturnsAsync(application);
mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
mock.Setup(manager => manager.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny<CancellationToken>()))
.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<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(manager => manager.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny<CancellationToken>()), Times.Once());
}
[Theory] [Theory]
[InlineData(TokenTypeIdentifiers.Private.AuthorizationCode)] [InlineData(TokenTypeIdentifiers.Private.AuthorizationCode)]
[InlineData(TokenTypeIdentifiers.Private.DeviceCode)] [InlineData(TokenTypeIdentifiers.Private.DeviceCode)]

1162
test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs

File diff suppressed because it is too large

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

@ -1,4 +1,6 @@
using System.Reflection; using System.Reflection;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
@ -309,9 +311,9 @@ public class OpenIddictServerBuilderTests
builder.AddDevelopmentEncryptionCertificate( builder.AddDevelopmentEncryptionCertificate(
subject: new X500DistinguishedName("CN=" + Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture))); subject: new X500DistinguishedName("CN=" + Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture)));
var serviceProvider = services.BuildServiceProvider(); var provider = services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<OpenIddictServerOptions>>(); var options = provider.GetRequiredService<IOptions<OpenIddictServerOptions>>();
// Act and assert // Act and assert
var exception = Assert.Throws<PlatformNotSupportedException>(() => options.Value); var exception = Assert.Throws<PlatformNotSupportedException>(() => options.Value);
@ -365,9 +367,9 @@ public class OpenIddictServerBuilderTests
builder.AddDevelopmentSigningCertificate( builder.AddDevelopmentSigningCertificate(
subject: new X500DistinguishedName("CN=" + Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture))); subject: new X500DistinguishedName("CN=" + Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture)));
var serviceProvider = services.BuildServiceProvider(); var provider = services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<OpenIddictServerOptions>>(); var options = provider.GetRequiredService<IOptions<OpenIddictServerOptions>>();
// Act and assert // Act and assert
var exception = Assert.Throws<PlatformNotSupportedException>(() => options.Value); var exception = Assert.Throws<PlatformNotSupportedException>(() => options.Value);
@ -784,6 +786,412 @@ public class OpenIddictServerBuilderTests
Assert.True(options.DisableTokenStorage); Assert.True(options.DisableTokenStorage);
} }
[Fact]
public void EnablePublicKeyInfrastructureClientCertificateAuthentication_ThrowsAnExceptionForNullCertificates()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() =>
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<ArgumentException>(() =>
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<ArgumentException>(() =>
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 => 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<InvalidOperationException>(() =>
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<ArgumentNullException>(() =>
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 => 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<InvalidOperationException>(() =>
builder.EnableSelfSignedClientCertificateAuthentication(
policy => policy.TrustMode = X509ChainTrustMode.System));
Assert.Equal(SR.GetResourceString(SR.ID0509), exception.Message);
}
#endif
[Fact] [Fact]
public void IgnoreAudiencePermissions_AudiencePermissionsAreIgnored() public void IgnoreAudiencePermissions_AudiencePermissionsAreIgnored()
{ {
@ -1300,6 +1708,346 @@ public class OpenIddictServerBuilderTests
Assert.Contains(new Uri("http://localhost/endpoint-path"), options.EndSessionEndpointUris); 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<ArgumentNullException>(() => 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<ArgumentNullException>(() => 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<ArgumentException>(() => 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<ArgumentException>(() => 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<ArgumentNullException>(() => 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<ArgumentNullException>(() => 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<ArgumentException>(() => 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<ArgumentException>(() => 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<ArgumentNullException>(() => 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<ArgumentNullException>(() => 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<ArgumentException>(() => 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<ArgumentException>(() => 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<ArgumentNullException>(() => 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<ArgumentNullException>(() => 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<ArgumentException>(() => 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<ArgumentException>(() => 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<ArgumentNullException>(() => 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<ArgumentNullException>(() => 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<ArgumentException>(() => 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<ArgumentException>(() => 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] [Fact]
public void SetIntrospectionEndpointUris_ThrowsExceptionWhenUrisIsNull() public void SetIntrospectionEndpointUris_ThrowsExceptionWhenUrisIsNull()
{ {

Loading…
Cancel
Save