Browse Source

Implement native mTLS client authentication support in the server stack

pull/2417/head
Kévin Chalet 4 days ago
parent
commit
a2789bef06
  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_RESILIENCE</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_NAMED_PIPE_STATIC_FACTORY_WITH_ACL</DefineConstants>
<DefineConstants>$(DefineConstants);SUPPORTS_ONE_SHOT_HASHING_METHODS</DefineConstants>
@ -78,6 +79,9 @@
<DefineConstants>$(DefineConstants);SUPPORTS_TEXT_ELEMENT_ENUMERATOR</DefineConstants>
<DefineConstants>$(DefineConstants);SUPPORTS_VALUETASK_COMPLETED_TASK</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>
</PropertyGroup>
@ -114,6 +118,8 @@
<DefineConstants>$(DefineConstants);SUPPORTS_JSON_ELEMENT_DEEP_EQUALS</DefineConstants>
<DefineConstants>$(DefineConstants);SUPPORTS_JSON_ELEMENT_PROPERTY_COUNT</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

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

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

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

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

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

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

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

@ -1,4 +1,5 @@
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
@ -112,26 +113,52 @@ public class Startup
PostLogoutRedirectUri = new Uri("callback/logout/local", UriKind.Relative),
#if SUPPORTS_PEM_ENCODED_KEY_IMPORT
// On supported platforms, this application authenticates by generating JWT client
// assertions that are signed using a signing key instead of using a client secret.
// On supported platforms, this application can authenticate using 3 different client
// authentication methods that all offer a higher security level than shared client secrets:
//
// As such, no client secret is set, but an ECDSA key is registered and used by
// the OpenIddict client to automatically generate client assertions when needed.
// 1) tls_client_auth (PKI-based mutual TLS authentication): while it requires
// setting up a proper Public Key Infrastructure, this method offers a very
// high level of security, as the authorization server never has access to the
// private key used by the client to authenticate itself and can dynamically check
// the revocation status of the client certificate using standard PKI mechanisms.
//
// Note: while the server only needs access to the public key, the client needs
// to know the private key to be able to generate and sign the client assertions.
// 2) self_signed_tls_client_auth (self-signed certificate-based mutual TLS authentication):
// this method is easier to deploy than PKI-based mutual TLS authentication, while
// still offering a high level of security. Unlike PKI-based mutual TLS authentication,
// the revocation status of the client certificate is never checked but certificates can
// be "revoked" by being removed from the JSON Web Key Set associated with the client.
//
// 3) private_key_jwt (JWT client assertions signed with a private key): while this
// method doesn't offer the same security guarantees as mutual TLS authentication,
// it is more secure than shared secrets and doesn't have the operational constraints
// required by the two mutual TLS methods described above (such as TLS configuration).
//
// The actual client authentication method used by the OpenIddict client is automatically
// selected based on the registered credentials and the methods supported by the server:
// when supported by the server, mutual TLS authentication methods are always preferred.
//
// In all cases, no client secret is necessary but the client needs to be able to access the
// private key of the certificate/key to be able to generate and sign the client assertions.
SigningCredentials =
{
new SigningCredentials(GetECDsaSigningKey($"""
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIMGxf/eMzKuW2F8KKWPJo3bwlrO68rK5+xCeO1atwja2oAoGCCqGSM49
AwEHoUQDQgAEI23kaVsRRAWIez/pqEZOByJFmlXda6iSQ4QqcH23Ir8aYPPX5lsV
nBsExNsl7SOYOiIhgTaX6+PTS7yxTnmvSw==
-----END EC PRIVATE KEY-----
"""), SecurityAlgorithms.EcdsaSha256, SecurityAlgorithms.Sha256)
}
// Note: this certificate can be used with either tls_client_auth or private_key_jwt,
// depending on the server configuration (and the client authentication methods explicitly
// configured via OpenIddictClientRegistration.ClientAuthenticationMethods, if applicable).
//
// GetPublicKeyInfrastructureCertificate(),
// Note: this certificate can be used with either self_signed_tls_client_auth or private_key_jwt,
// depending on the server configuration (and the client authentication methods explicitly
// configured via OpenIddictClientRegistration.ClientAuthenticationMethods, if applicable):
//
// GetSelfSignedCertificate(),
// Note: this key can only be used with private_key_jwt as raw keys cannot be used with TLS.
GetSigningKey()
},
#else
ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654"
ClientSecret = "emCimpdc9SeOaZzN5jzm4_eek-STF6VenfVlKO1_qt0"
#endif
});
@ -165,13 +192,181 @@ public class Startup
});
#if SUPPORTS_PEM_ENCODED_KEY_IMPORT
static ECDsaSecurityKey GetECDsaSigningKey(ReadOnlySpan<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();
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
});

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.Server.Kestrel.Core;
using Microsoft.AspNetCore.Server.Kestrel.Https;
using Microsoft.EntityFrameworkCore;
using OpenIddict.Sandbox.AspNetCore.Server.Models;
using OpenIddict.Sandbox.AspNetCore.Server.Services;
@ -158,6 +162,106 @@ public class Startup
// you don't own, you can disable access token encryption:
//
// options.DisableAccessTokenEncryption();
#if SUPPORTS_KESTREL_TLS_HANDSHAKE_CALLBACK_OPTIONS
// Enable both tls_client_auth and self_signed_tls_client_auth to allow clients
// to authenticate using either PKI certificates or self-signed certificates.
//
// Note: PKI and self-signed certificate authentication can be enabled independently.
options.EnablePublicKeyInfrastructureClientCertificateAuthentication(
[
// Root certificate:
X509Certificate2.CreateFromPem($"""
-----BEGIN CERTIFICATE-----
MIIE7jCCAtagAwIBAgIJAN+SZB+xc7usMA0GCSqGSIb3DQEBCwUAMBIxEDAOBgNV
BAMTB1Jvb3QgQ0EwIBcNMjYwMjAxMTQxNDQzWhgPMjEyNjAyMDIxNDE0NDNaMBIx
EDAOBgNVBAMTB1Jvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC
AQC68DOD6IvsJM0mc7n8bYeNaVe8e0ytJCJozdMPNXAe80vMPP4cVUPFvJ/tbSjX
yhREJ9xz2dYgQAhWCaTnEHY4AaE1Tj2rYqotenDQxs18qqaqoZlcaFfkRUPHRH3Q
iS8D8gbxzlYkjxNsfDJRi0cXFxr4wb4FmSP4ES2DFWWAWbN9wt7Tb2uDiHkjSefZ
Pni5F6fN6nE7wgGMYrdrCiiwJf7jEZiIZ60bsiUnJ5VUX6g4ob469CLocH/q/9Yr
Dad9/+YYp6SuHZilsPmW4X0fziuF/RvtsRLw4bw5jwj69KH3Y0jqUMQoyzz2CIJz
cDMB/MLREgcT9jTVB/M5Pl61DCzR/0d4t6RENpkNqpAIVM0Unp0nDuHPwjoeEZn3
vSvUiGpiYY355GaSl05OE3SOKoRHt4lBXvY43y8fRBMOwlNHYn4eO3ZDuzZYzhfs
68ywK4zUy47Qyn1BgNNqc/KC7kzxeLFxqTg2VJgBeXuJfucwzhOFqkOSfpeIGDK9
8MODFlA3usf5LXxQ7DJhkeBgPkW56BUlYVkenm9ORWe77GnoXL95p2HQUEXATHir
unXFPVcHET6hyegvc9AzSTZFQL4RMO3ZV5ESs+JF/YY4ycBc1+WYy2kAuP5sfsGn
mpeKf2Dv9MGFcDxU7iimeM92n7t5lbCRlX8NUaYeQ8jKgQIDAQABo0UwQzASBgNV
HRMBAf8ECDAGAQH/AgEBMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUyHIqQnw/
x6e92OuJ+y+e0LSKSqQwDQYJKoZIhvcNAQELBQADggIBACCOjX4+RyoZt9tVoKdT
uGFoEJUGBUfcYaMdqiDstuSoNrqZXHnilzj33ZbNqj6X6rS1w5qVnkj7ZY4Wu8MP
Sj85Wp9cq0jMv3NfPZfKmJd2K7favGQKvgSPptSl9VgcIrpRam9BG2db1IP053tf
ydBB3w/yI7MTb4fkPqLWtKcuPPM8t9SsxAlKhEm+gbNEsqDX9ZIfxolHEpL2zLOi
a4v7+SlJdVBfo4mj+iLUeZXFRPglAnPQ3CZngfbPsjEklpOCU1v8TnhHwV8jyCgl
oLAceLjdlXHWVfhKU+N0jdAt8V2NPxq+yJ/gPX+J3YOrYRCHMdQZ/OFEUhmkxHNp
UUPkL1VJ9c8ZW2/gszFPyvsh7GHwl43y7bN8doiQVOSj6jZ7uCkQl1oz731fl97b
FqKVyGGx6UUEi57YS7mWsY02qNvYSObOxhSNusX/Ct06XbXS1Pn+co/3FMGMcEVf
IwzboV61sMqRu4l3YD0Z1AxdhXFERMlHBYyyj8CQYIXtnCUoeT40IIR3aFmEG5u8
/lwehTnV4slDDMJMFSW54aENpT1XP4b8m46kioNhxN+7ukdcWnYoapePuiDRboA3
GwRExwFUDiGO8zpnyvV4JTIGu9MZi51O3RbSlIDDhyzFsdQW3PeztwGjZWZAXD+7
qIoAaBbG/12cvNeZH7L9Mcpo
-----END CERTIFICATE-----
"""),
// Intermediate certificate (optional):
X509Certificate2.CreateFromPem($"""
-----BEGIN CERTIFICATE-----
MIIFQzCCAyugAwIBAgIQb6sP8aiPyzYFX0lET/XJOzANBgkqhkiG9w0BAQsFADAS
MRAwDgYDVQQDEwdSb290IENBMCAXDTI2MDIwMTE0MTQ0M1oYDzIxMjYwMjAyMTQx
NDQzWjAaMRgwFgYDVQQDEw9JbnRlcm1lZGlhdGUgQ0EwggIiMA0GCSqGSIb3DQEB
AQUAA4ICDwAwggIKAoICAQCfObzu9v5dfio8kCCKpb0vLXUrilcOM6FGVx50rPtc
MjlHNG4GpghoLXjJxrUIsoeGdsCI6W+K3R+5PRlEsbCT3l/0n2/ixW3rN9rO3FOt
VGOHYrE2wI+i1aWP1/w/0bcCbH1J6PLKPv5syzhWWdkoTy2K72gye5Kx3zXkFQoC
uBFMvj3HBgmTngaDRTT1QGsRSlhuvoEiHHAvgoTfYt7bgbRhM5I5upEbXB0cucj7
Ghzws5R2/4qsr/QorwA8l6aNeb1dm0uB+FlMVlGelYMZ76+SjBs1rOxD0qt82h13
BYPBR4gLvNFafOEskFndeP3OkNaQ6kPm+uClj5OxwONnBcy7neJPqZGMtxpApLK3
reK3IZ/ieg1nY9zZ7OkqIzQDt1CeCBQWU3RkpEtVojkRDLCmg+pKSjHtLxUUGiQ3
UHrXO2Yrej8Qpx4JHdKGUfku25r4SSaj2YF61ZIvsDxlOMROfJUFbpQdyAtQPCCq
zpfkVKaCeCiTlifI6AZODngc9c8U+s7vLxjucz/Q4gNHwcgg/mASbjh8A2hYsbW6
qeg7lE9k5t6Lv820FudjfgFiq7k+zIbNsDNy3Y7CSsgBHQQSyNFngg25PPQOon9c
yd3PiFK36OzktnRkcTs98i3fwO2+3pp6qgOSk1Sdx877egszMjBPFzxrBX9CXIpM
yQIDAQABo4GKMIGHMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEG
MB0GA1UdDgQWBBThFsFtfMN62ev+YiMAAddufdjpRzBCBgNVHSMEOzA5gBTIcipC
fD/Hp73Y64n7L57QtIpKpKEWpBQwEjEQMA4GA1UEAxMHUm9vdCBDQYIJAN+SZB+x
c7usMA0GCSqGSIb3DQEBCwUAA4ICAQA6sBGs28FWhKgh6TxZ6U8Lc+iCdc4c9PeM
L5pQQosHekT0oBJK8WdvyXZS95Fz2ddJaKiiQyUKSP4XHpxE+6tBt8OOV0LJJxnx
yKTBZtcSiOFssu2j6aqx3oMotRZJrhuI/5ChExaPwFT1W7aQDIY6lN2KcQ/xndbX
Nts/2nwCvlplfiOGM7XrRMU8b4X+AVWXSksvLXiByrDh9W6WGDsBHyu+FKQVnwmW
QVnshKpwxIsW25JDOhFE8+VHn6yciUKUTqnCFt5HjZpZh00q8hhmlhrNBEdkxA8N
OF7S1uWWftJywqq23qG6pGIDQ1r1dwNzgaeNhmW6QKm2zBXUmuOW//Xt+1wtHrly
bDjXKKSa/zhR9plYPdvGe9PopXwTw/fQWRYcxML6aH+WbWY9AgCHFgY56YCJYZd9
eUIfrvPVJLn8fqwLmsWQtIY+XkAS/YQ4wTQs0zZS3+bdxeGQ6oHIMgxCDiBK8Qcc
RHf+RvYHiBllOJmaRaJHdsauMk9IlYYYpxPPwuWGti9B5HI4JO6bIqmR5Q8x3L/g
tFGMPzvWDTA2+dQcrh7WKULDH9Ngnnoodc6Hb9Iv1yCGYahcS6ARt9BzRyG1+6d9
bq/zCH8KQCjryiTn3ZEpsln/iXtp5nHiLegUc1OoXldrUKAz9V93l61GHUw1kdhD
V+KJceDj3Q==
-----END CERTIFICATE-----
""")
]);
options.EnableSelfSignedClientCertificateAuthentication();
// Note: setting a static issuer is mandatory when using mTLS aliases
// to ensure it is not dynamically computed based on the request URI,
// as this would result in two different issuers being used (one
// pointing to the mTLS domain and one pointing to the regular one).
options.SetIssuer("https://localhost:44395/");
// Configure the mTLS endpoint aliases that will be used by client applications opting
// for TLS-based client authentication to communicate with the authorization server:
// the configured URIs MUST point to a domain for which the HTTPS server is configured
// to require the use of client certificates when receiving TLS handshakes from clients.
//
// Using mTLS endpoint aliases is not mandatory but is strongly recommended to avoid
// severely degrading the experience of users of browser-based clients, as TLS client
// authentication can only be enforced globally and not per-client, which would result
// in certificate selection prompts being systematically displayed by browsers.
options.SetMtlsDeviceAuthorizationEndpointAliasUri("https://mtls.dev.localhost:44395/connect/device")
.SetMtlsIntrospectionEndpointAliasUri("https://mtls.dev.localhost:44395/connect/introspect")
.SetMtlsPushedAuthorizationEndpointAliasUri("https://mtls.dev.localhost:44395/connect/par")
.SetMtlsRevocationEndpointAliasUri("https://mtls.dev.localhost:44395/connect/revoke")
.SetMtlsTokenEndpointAliasUri("https://mtls.dev.localhost:44395/connect/token");
#endif
})
// Register the OpenIddict validation components.
@ -178,7 +282,7 @@ public class Startup
// options.UseIntrospection()
// .SetIssuer("https://localhost:44395/")
// .SetClientId("resource_server")
// .SetClientSecret("80B552BB-4CD8-48DA-946E-0815E0147DD2");
// .SetClientSecret("vVQ-yjr42sXP5VHj6AswkXuS7MU1i2gFjvJjY0TdGMk");
//
// When introspection is used, the System.Net.Http integration must be enabled.
//
@ -202,6 +306,56 @@ public class Startup
// Register the worker responsible for seeding the database with the sample clients.
// Note: in a real world application, this step should be part of a setup script.
services.AddHostedService<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)

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

@ -1,5 +1,6 @@
using System.Globalization;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.Abstractions;
using OpenIddict.Sandbox.AspNetCore.Server.Models;
@ -157,8 +158,36 @@ public class Worker : IHostedService
{
Keys =
{
// On supported platforms, this application authenticates by generating JWT client
// assertions that are signed using a signing key instead of using a client secret.
// On supported platforms, this application can authenticate by using a
// self-signed client authentication certificate during the TLS handshake
// (a method known as "mutual TLS" or mTLS).
//
// Note: while the client needs access to the private key, the server only needs
// to know the public part to be able to validate the certificates it receives.
JsonWebKeyConverter.ConvertFromX509SecurityKey(new X509SecurityKey(
X509Certificate2.CreateFromPem($"""
-----BEGIN CERTIFICATE-----
MIIC8zCCAdugAwIBAgIJAIZ9BN3TUnZQMA0GCSqGSIb3DQEBCwUAMCIxIDAeBgNV
BAMTF1NlbGYtc2lnbmVkIGNlcnRpZmljYXRlMCAXDTI2MDIwMjE0MzM0OVoYDzIx
MjYwMjAyMTQzMzQ5WjAiMSAwHgYDVQQDExdTZWxmLXNpZ25lZCBjZXJ0aWZpY2F0
ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOtfKVPM7ghVFh4U/sz4
sTrpaNJGQ2NORqawYxAHwluhr101yIOW7rWvFlFncA64Lkq9SAbFFCVSAbo28c6B
2Mi41jyC4LHQU11jhv08K/3FUuckCuzEpzTnXUhxJHWxrRDVEuvKINGPs1VgVtTT
ra8rjP8s1YRAzCYnByxSx+8GXNGHprylLh0agpWKb2+2FYwDqY5ME2g3xTL9FTUu
FYWTcyspsvN0U1Eo1vlCeOxSYGPRct0MK0AS6eXEGBv+3kCYI7a5+UhQok0WvErF
pjIVo7USISDgKhW9GhTsWN+WywwdG4Kx4V6SB8ZLAHFSBSR3gjWS3TGOyqAWoBXc
znkCAwEAAaMqMCgwDgYDVR0PAQH/BAQDAgeAMBYGA1UdJQEB/wQMMAoGCCsGAQUF
BwMCMA0GCSqGSIb3DQEBCwUAA4IBAQBf5i/S7shmNalVxMuP8/Mk8cOhRRZjnAXd
zz3eOuXu0CH8iY/DwCgss04O2NTxuz87rKiuNKOrtY0oN/G4aFjWPvbgoQ+N1XP1
zvbhqbyo3fQr07FyjWkrIUoHYFQ3JRfL+GPGjWizJsgdpdCRJSK6G9VX8eU3Akjv
YhMRLmbkrH5etOURqFtLpZlxNmLzCpqWIvzRiYyyj74iOipA2I0acgcvkakWn6rE
Wio7luBAZ3dXlukEfHTOg+ft4k0nOlRXPTtASOmyFQBOs6iYJeztHDz6MQnknAPe
+W53US8kLWktspcOQmxhVVH1g1/T4ynl9iX7tzqvUbdYwZNi92+x
-----END CERTIFICATE-----
"""))),
// On supported platforms, this application can also authenticate by
// generating JWT client assertions that are signed using a signing key.
//
// Note: while the client needs access to the private key, the server only needs
// to know the public key to be able to validate the client assertions it receives.
@ -171,7 +200,7 @@ public class Worker : IHostedService
}
},
#else
ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654",
ClientSecret = "emCimpdc9SeOaZzN5jzm4_eek-STF6VenfVlKO1_qt0",
#endif
RedirectUris =
{
@ -306,7 +335,7 @@ public class Worker : IHostedService
var descriptor = new OpenIddictApplicationDescriptor
{
ClientId = "resource_server",
ClientSecret = "80B552BB-4CD8-48DA-946E-0815E0147DD2",
ClientSecret = "vVQ-yjr42sXP5VHj6AswkXuS7MU1i2gFjvJjY0TdGMk",
ClientType = ClientTypes.Confidential,
Permissions =
{

101
shared/OpenIddict.Extensions/OpenIddictHelpers.cs

@ -11,6 +11,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
@ -196,7 +197,7 @@ internal static class OpenIddictHelpers
ArgumentNullException.ThrowIfNull(uri);
var builder = new StringBuilder(uri.Query);
if (builder.Length > 0)
if (builder.Length is > 0)
{
builder.Append('&');
}
@ -238,7 +239,7 @@ internal static class OpenIddictHelpers
// only append the parameter key to the query string.
if (parameter.Value.Count is 0)
{
if (builder.Length > 0)
if (builder.Length is > 0)
{
builder.Append('&');
}
@ -252,7 +253,7 @@ internal static class OpenIddictHelpers
{
foreach (var value in parameter.Value)
{
if (builder.Length > 0)
if (builder.Length is > 0)
{
builder.Append('&');
}
@ -286,7 +287,7 @@ internal static class OpenIddictHelpers
.Select(static parameter => parameter.Split(Separators.EqualsSign, StringSplitOptions.RemoveEmptyEntries))
.Select(static parts => (
Key: parts[0] is string key ? Uri.UnescapeDataString(key) : null,
Value: parts.Length > 1 && parts[1] is string value ? Uri.UnescapeDataString(value) : null))
Value: parts.Length is > 1 && parts[1] is string value ? Uri.UnescapeDataString(value) : null))
.Where(static pair => !string.IsNullOrEmpty(pair.Key))
.GroupBy(static pair => pair.Key)
.ToDictionary(static pair => pair.Key!, static pair => new StringValues([.. pair.Select(parts => parts.Value)]));
@ -307,7 +308,7 @@ internal static class OpenIddictHelpers
.Select(static parameter => parameter.Split(Separators.EqualsSign, StringSplitOptions.RemoveEmptyEntries))
.Select(static parts => (
Key: parts[0] is string key ? Uri.UnescapeDataString(key) : null,
Value: parts.Length > 1 && parts[1] is string value ? Uri.UnescapeDataString(value) : null))
Value: parts.Length is > 1 && parts[1] is string value ? Uri.UnescapeDataString(value) : null))
.Where(static pair => !string.IsNullOrEmpty(pair.Key))
.GroupBy(static pair => pair.Key)
.ToDictionary(static pair => pair.Key!, static pair => new StringValues([.. pair.Select(parts => parts.Value)]));
@ -998,7 +999,7 @@ internal static class OpenIddictHelpers
/// </summary>
/// <param name="node">The <see cref="JsonNode"/>.</param>
/// <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>
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))
};
/// <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>
/// Determines whether the items contained in <paramref name="element"/>
/// 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.InteropServices;
using System.Runtime.Versioning;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
namespace OpenIddict.Extensions;
@ -303,4 +305,54 @@ internal static class OpenIddictPolyfills
return currentRevision >= revision;
}
#endif
extension(X509ChainPolicy policy)
{
#if !SUPPORTS_X509_CHAIN_POLICY_CLONING
public X509ChainPolicy Clone()
{
var clone = new X509ChainPolicy
{
#if SUPPORTS_X509_CHAIN_POLICY_DOWNLOAD_MODE
DisableCertificateDownloads = policy.DisableCertificateDownloads,
#endif
RevocationMode = policy.RevocationMode,
RevocationFlag = policy.RevocationFlag,
#if SUPPORTS_X509_CHAIN_POLICY_TRUST_MODE
TrustMode = policy.TrustMode,
#endif
UrlRetrievalTimeout = policy.UrlRetrievalTimeout,
VerificationFlags = policy.VerificationFlags,
VerificationTime = policy.VerificationTime,
#if SUPPORTS_X509_CHAIN_POLICY_VERIFICATION_TIME_MODE
VerificationTimeIgnored = policy.VerificationTimeIgnored
#endif
};
if (policy.ApplicationPolicy.Count is > 0)
{
for (var index = 0; index < policy.ApplicationPolicy.Count; index++)
{
clone.ApplicationPolicy.Add(policy.ApplicationPolicy[index]);
}
}
if (policy.CertificatePolicy.Count is > 0)
{
for (var index = 0; index < policy.CertificatePolicy.Count; index++)
{
clone.CertificatePolicy.Add(policy.CertificatePolicy[index]);
}
}
#if SUPPORTS_X509_CHAIN_POLICY_CUSTOM_TRUST_STORE
clone.CustomTrustStore.AddRange(policy.CustomTrustStore);
#endif
clone.ExtraStore.AddRange(policy.ExtraStore);
return clone;
}
#endif
}
}

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

@ -8,6 +8,7 @@ using System.Collections.Immutable;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Security.Cryptography.X509Certificates;
using System.Text.Json;
using Microsoft.IdentityModel.Tokens;
@ -15,12 +16,14 @@ namespace OpenIddict.Abstractions;
/// <summary>
/// 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,
/// that should inherit from the generic OpenIddictApplicationManager class.
/// It is primarily intended to be used by services that cannot easily depend
/// on the generic application manager. The actual application entity type
/// is automatically determined at runtime based on the OpenIddict core options.
/// </summary>
/// </remarks>
public interface IOpenIddictApplicationManager
{
/// <summary>
@ -171,6 +174,19 @@ public interface IOpenIddictApplicationManager
Func<IQueryable<object>, TState, IQueryable<TResult>> query,
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>
/// Retrieves the client identifier associated with an application.
/// </summary>
@ -330,6 +346,19 @@ public interface IOpenIddictApplicationManager
/// </returns>
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>
/// Retrieves the settings associated with an application.
/// </summary>
@ -475,6 +504,21 @@ public interface IOpenIddictApplicationManager
/// </returns>
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>
/// Validates the application to ensure it's in a consistent state.
/// </summary>
@ -522,4 +566,20 @@ public interface IOpenIddictApplicationManager
/// </returns>
ValueTask<bool> ValidateRedirectUriAsync(object application,
[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>
/// 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,
/// that should inherit from the generic OpenIddictAuthorizationManager class.
/// It is primarily intended to be used by services that cannot easily depend
/// on the generic authorization manager. The actual authorization entity type
/// is automatically determined at runtime based on the OpenIddict core options.
/// </summary>
/// </remarks>
public interface IOpenIddictAuthorizationManager
{
/// <summary>

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

@ -13,12 +13,14 @@ namespace OpenIddict.Abstractions;
/// <summary>
/// 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,
/// that should inherit from the generic OpenIddictScopeManager class.
/// It is primarily intended to be used by services that cannot easily
/// depend on the generic scope manager. The actual scope entity type is
/// automatically determined at runtime based on the OpenIddict core options.
/// </summary>
/// </remarks>
public interface IOpenIddictScopeManager
{
/// <summary>

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

@ -12,12 +12,14 @@ namespace OpenIddict.Abstractions;
/// <summary>
/// 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,
/// that should inherit from the generic OpenIddictTokenManager class.
/// It is primarily intended to be used by services that cannot easily
/// depend on the generic token manager. The actual token entity type is
/// automatically determined at runtime based on the OpenIddict core options.
/// </summary>
/// </remarks>
public interface IOpenIddictTokenManager
{
/// <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 static class ObjectIdentifiers
{
public static class CertificateExtensions
{
public const string AuthorityInfoAccess = "1.3.6.1.5.5.7.1.1";
public const string CrlDistributionPoints = "2.5.29.31";
}
public static class ExtendedKeyUsages
{
public const string ClientAuthentication = "1.3.6.1.5.5.7.3.2";
}
}
public static class Parameters
{
public const string AccessToken = "access_token";

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">
<value>An unknown error occurred while trying to start a custom tabs intent.</value>
</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">
<value>The security token is missing.</value>
</data>
@ -2387,6 +2436,15 @@ Alternatively, any value respecting the '[region]-[subregion]-[identifier]' patt
<data name="ID2195" xml:space="preserve">
<value>The '{0}' parameter is only allowed for OAuth 2.0 Token Exchange requests.</value>
</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">
<value>The '{0}' parameter shouldn't be null or empty at this point.</value>
</data>
@ -2447,6 +2505,9 @@ Alternatively, any value respecting the '[region]-[subregion]-[identifier]' patt
<data name="ID4019" xml:space="preserve">
<value>The nonce shouldn't be null or empty at this point.</value>
</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">
<value>An error occurred while validating the token '{Token}'.</value>
</data>
@ -3194,6 +3255,27 @@ This may indicate that the hashed entry is corrupted or malformed.</value>
<data name="ID6281" xml:space="preserve">
<value>The token request was rejected because the '{Parameter}' contained a URI fragment: {RedirectUri}.</value>
</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">
<value>https://documentation.openiddict.com/errors/{0}</value>
</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
// is dynamically added if one of the default schemes properties is not set
// and less than 2 handlers were registered in the authentication options.
if (options.SchemeMap.Count < 2 && string.IsNullOrEmpty(options.DefaultScheme) &&
if (options.SchemeMap.Count is < 2 && string.IsNullOrEmpty(options.DefaultScheme) &&
(string.IsNullOrEmpty(options.DefaultAuthenticateScheme) ||
string.IsNullOrEmpty(options.DefaultSignInScheme) ||
string.IsNullOrEmpty(options.DefaultSignOutScheme)))

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

@ -7,7 +7,6 @@
using System.ComponentModel;
using System.Net;
using System.Net.Http;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http;
@ -271,10 +270,12 @@ public sealed class OpenIddictClientSystemNetHttpConfiguration : IConfigureOptio
{
foreach (var credentials in registration.SigningCredentials)
{
// Note: to avoid building and introspecting a X.509 certificate chain and reduce the cost
// of this check, a certificate is always assumed to be self-signed when it is self-issued.
if (credentials.Key is X509SecurityKey { Certificate: X509Certificate2 certificate } &&
certificate.Version is >= 3 && IsSelfIssuedCertificate(certificate) &&
HasDigitalSignatureKeyUsage(certificate) &&
HasClientAuthenticationExtendedKeyUsage(certificate))
certificate.Version is >= 3 && OpenIddictHelpers.IsSelfIssuedCertificate(certificate) &&
OpenIddictHelpers.HasKeyUsage(certificate, X509KeyUsageFlags.DigitalSignature) &&
OpenIddictHelpers.HasExtendedKeyUsage(certificate, ObjectIdentifiers.ExtendedKeyUsages.ClientAuthentication))
{
return certificate;
}
@ -287,10 +288,12 @@ public sealed class OpenIddictClientSystemNetHttpConfiguration : IConfigureOptio
{
foreach (var credentials in registration.SigningCredentials)
{
// Note: to avoid building and introspecting a X.509 certificate chain and reduce the cost
// of this check, a certificate is always assumed to be self-signed when it is self-issued.
if (credentials.Key is X509SecurityKey { Certificate: X509Certificate2 certificate } &&
certificate.Version is >= 3 && !IsSelfIssuedCertificate(certificate) &&
HasDigitalSignatureKeyUsage(certificate) &&
HasClientAuthenticationExtendedKeyUsage(certificate))
certificate.Version is >= 3 && !OpenIddictHelpers.IsSelfIssuedCertificate(certificate) &&
OpenIddictHelpers.HasKeyUsage(certificate, X509KeyUsageFlags.DigitalSignature) &&
OpenIddictHelpers.HasExtendedKeyUsage(certificate, ObjectIdentifiers.ExtendedKeyUsages.ClientAuthentication))
{
return certificate;
}
@ -298,51 +301,5 @@ public sealed class OpenIddictClientSystemNetHttpConfiguration : IConfigureOptio
return null;
};
static bool HasClientAuthenticationExtendedKeyUsage(X509Certificate2 certificate)
{
for (var index = 0; index < certificate.Extensions.Count; index++)
{
if (certificate.Extensions[index] is X509EnhancedKeyUsageExtension extension &&
HasOid(extension.EnhancedKeyUsages, "1.3.6.1.5.5.7.3.2"))
{
return true;
}
}
return false;
static bool HasOid(OidCollection collection, string value)
{
for (var index = 0; index < collection.Count; index++)
{
if (collection[index] is Oid oid && string.Equals(oid.Value, value, StringComparison.Ordinal))
{
return true;
}
}
return false;
}
}
static bool HasDigitalSignatureKeyUsage(X509Certificate2 certificate)
{
for (var index = 0; index < certificate.Extensions.Count; index++)
{
if (certificate.Extensions[index] is X509KeyUsageExtension extension &&
extension.KeyUsages.HasFlag(X509KeyUsageFlags.DigitalSignature))
{
return true;
}
}
return false;
}
// Note: to avoid building and introspecting a X.509 certificate chain and reduce the cost
// of this check, a certificate is always assumed to be self-signed when it is self-issued.
static bool IsSelfIssuedCertificate(X509Certificate2 certificate)
=> certificate.SubjectName.RawData.AsSpan().SequenceEqual(certificate.IssuerName.RawData);
}
}

22
src/OpenIddict.Client/OpenIddictClientBuilder.cs

@ -135,8 +135,7 @@ public sealed class OpenIddictClientBuilder
ArgumentNullException.ThrowIfNull(key);
// If the encryption key is an asymmetric security key, ensure it has a private key.
if (key is AsymmetricSecurityKey asymmetricSecurityKey &&
asymmetricSecurityKey.PrivateKeyStatus is PrivateKeyStatus.DoesNotExist)
if (key is AsymmetricSecurityKey { PrivateKeyStatus: PrivateKeyStatus.DoesNotExist })
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0055));
}
@ -202,7 +201,7 @@ public sealed class OpenIddictClientBuilder
// If no valid existing certificate was found, create a new encryption certificate.
var certificates = store.Certificates
.Find(X509FindType.FindBySubjectDistinguishedName, subject.Name, validOnly: false)
.OfType<X509Certificate2>()
.Cast<X509Certificate2>()
.ToList();
if (!certificates.Exists(certificate => certificate.NotBefore < now.LocalDateTime && certificate.NotAfter > now.LocalDateTime))
@ -318,7 +317,7 @@ public sealed class OpenIddictClientBuilder
// If the certificate is a X.509v3 certificate that specifies at least one
// key usage, ensure that the certificate key can be used for key encryption.
if (certificate.Version >= 3)
if (certificate.Version is >= 3)
{
var extensions = certificate.Extensions.OfType<X509KeyUsageExtension>().ToList();
if (extensions.Count is not 0 && !extensions.Exists(static extension =>
@ -437,7 +436,7 @@ public sealed class OpenIddictClientBuilder
store.Open(OpenFlags.ReadOnly);
return store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false)
.OfType<X509Certificate2>()
.Cast<X509Certificate2>()
.SingleOrDefault();
}
}
@ -458,7 +457,7 @@ public sealed class OpenIddictClientBuilder
return AddEncryptionCertificate(
store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false)
.OfType<X509Certificate2>()
.Cast<X509Certificate2>()
.SingleOrDefault() ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0066)));
}
@ -496,8 +495,7 @@ public sealed class OpenIddictClientBuilder
ArgumentNullException.ThrowIfNull(key);
// If the signing key is an asymmetric security key, ensure it has a private key.
if (key is AsymmetricSecurityKey asymmetricSecurityKey &&
asymmetricSecurityKey.PrivateKeyStatus is PrivateKeyStatus.DoesNotExist)
if (key is AsymmetricSecurityKey { PrivateKeyStatus: PrivateKeyStatus.DoesNotExist })
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0067));
}
@ -581,7 +579,7 @@ public sealed class OpenIddictClientBuilder
// If no valid existing certificate was found, create a new signing certificate.
var certificates = store.Certificates
.Find(X509FindType.FindBySubjectDistinguishedName, subject.Name, validOnly: false)
.OfType<X509Certificate2>()
.Cast<X509Certificate2>()
.ToList();
if (!certificates.Exists(certificate => certificate.NotBefore < now.LocalDateTime && certificate.NotAfter > now.LocalDateTime))
@ -725,7 +723,7 @@ public sealed class OpenIddictClientBuilder
// If the certificate is a X.509v3 certificate that specifies at least
// one key usage, ensure that the certificate key can be used for signing.
if (certificate.Version >= 3)
if (certificate.Version is >= 3)
{
var extensions = certificate.Extensions.OfType<X509KeyUsageExtension>().ToList();
if (extensions.Count is not 0 && !extensions.Exists(static extension =>
@ -844,7 +842,7 @@ public sealed class OpenIddictClientBuilder
store.Open(OpenFlags.ReadOnly);
return store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false)
.OfType<X509Certificate2>()
.Cast<X509Certificate2>()
.SingleOrDefault();
}
}
@ -865,7 +863,7 @@ public sealed class OpenIddictClientBuilder
return AddSigningCertificate(
store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false)
.OfType<X509Certificate2>()
.Cast<X509Certificate2>()
.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>
/// 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>
public sealed class ExtractMtlsRevocationEndpoint : IOpenIddictClientHandler<HandleConfigurationResponseContext>
{
@ -600,7 +601,8 @@ public static partial class OpenIddictClientHandlers
}
/// <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>
public sealed class ExtractMtlsTokenEndpoint : IOpenIddictClientHandler<HandleConfigurationResponseContext>
{
@ -632,7 +634,8 @@ public static partial class OpenIddictClientHandlers
}
/// <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>
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.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Logging;
@ -18,7 +19,6 @@ using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using ValidationException = OpenIddict.Abstractions.OpenIddictExceptions.ValidationException;
#if !SUPPORTS_KEY_DERIVATION_WITH_SPECIFIED_HASH_ALGORITHM
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Digests;
@ -471,6 +471,25 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
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>
/// Retrieves the client identifier associated with an application.
/// </summary>
@ -739,6 +758,48 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
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>
/// Retrieves the settings associated with an application.
/// </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>
/// Validates the client_secret associated with an application.
/// </summary>
@ -1399,6 +1568,92 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
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>
/// Obfuscates the specified client secret so it can be safely stored in a database.
/// By default, this method returns a complex hashed representation computed using PBKDF2.
@ -1520,7 +1775,7 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
// Read the size of the salt and ensure it's more than 128 bits.
var saltLength = (int) BinaryPrimitives.ReadUInt32BigEndian(payload.Slice(9, sizeof(uint)));
if (saltLength < 128 / 8)
if (saltLength is < 128 / 8)
{
return false;
}
@ -1530,7 +1785,7 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
// Ensure the derived key length is more than 128 bits.
var keyLength = payload.Length - 13 - salt.Length;
if (keyLength < 128 / 8)
if (keyLength is < 128 / 8)
{
return false;
}
@ -1614,6 +1869,10 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
ValueTask<TResult?> IOpenIddictApplicationManager.GetAsync<TState, TResult>(Func<IQueryable<object>, TState, IQueryable<TResult>> query, TState state, CancellationToken cancellationToken) where TResult : default
=> GetAsync(query, state, cancellationToken);
/// <inheritdoc/>
ValueTask<X509ChainPolicy?> IOpenIddictApplicationManager.GetClientCertificateChainPolicyAsync(object application, X509ChainPolicy policy, CancellationToken cancellationToken)
=> GetClientCertificateChainPolicyAsync((TApplication) application, policy, cancellationToken);
/// <inheritdoc/>
ValueTask<string?> IOpenIddictApplicationManager.GetClientIdAsync(object application, CancellationToken cancellationToken)
=> GetClientIdAsync((TApplication) application, cancellationToken);
@ -1670,6 +1929,10 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
ValueTask<ImmutableArray<string>> IOpenIddictApplicationManager.GetRequirementsAsync(object application, CancellationToken cancellationToken)
=> GetRequirementsAsync((TApplication) application, cancellationToken);
/// <inheritdoc/>
ValueTask<X509ChainPolicy?> IOpenIddictApplicationManager.GetSelfSignedClientCertificateChainPolicyAsync(object application, X509ChainPolicy policy, CancellationToken cancellationToken)
=> GetSelfSignedClientCertificateChainPolicyAsync((TApplication) application, policy, cancellationToken);
/// <inheritdoc/>
ValueTask<ImmutableDictionary<string, string>> IOpenIddictApplicationManager.GetSettingsAsync(object application, CancellationToken cancellationToken)
=> GetSettingsAsync((TApplication) application, cancellationToken);
@ -1730,6 +1993,10 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
IAsyncEnumerable<ValidationResult> IOpenIddictApplicationManager.ValidateAsync(object application, CancellationToken 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/>
ValueTask<bool> IOpenIddictApplicationManager.ValidateClientSecretAsync(object application, string secret, CancellationToken cancellationToken)
=> ValidateClientSecretAsync((TApplication) application, secret, cancellationToken);
@ -1741,4 +2008,8 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
/// <inheritdoc/>
ValueTask<bool> IOpenIddictApplicationManager.ValidateRedirectUriAsync(object application, [StringSyntax(StringSyntaxAttribute.Uri)] string uri, CancellationToken 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>,
IConfigureOptions<OpenIddictServerOptions>,
IPostConfigureOptions<AuthenticationOptions>,
IPostConfigureOptions<OpenIddictServerAspNetCoreOptions>
IPostConfigureOptions<OpenIddictServerAspNetCoreOptions>,
IPostConfigureOptions<OpenIddictServerOptions>
{
/// <inheritdoc/>
public void Configure(AuthenticationOptions options)
@ -71,7 +72,7 @@ public sealed class OpenIddictServerAspNetCoreConfiguration : IConfigureOptions<
// on invalid endpoints. To opt out this undesirable behavior, a fake entry
// is dynamically added if one of the default schemes properties is not set
// and less than 2 handlers were registered in the authentication options.
if (options.SchemeMap.Count < 2 && string.IsNullOrEmpty(options.DefaultScheme) &&
if (options.SchemeMap.Count is < 2 && string.IsNullOrEmpty(options.DefaultScheme) &&
(string.IsNullOrEmpty(options.DefaultAuthenticateScheme) ||
string.IsNullOrEmpty(options.DefaultChallengeScheme) ||
string.IsNullOrEmpty(options.DefaultForbidScheme) ||
@ -104,4 +105,22 @@ public sealed class OpenIddictServerAspNetCoreConfiguration : IConfigureOptions<
throw new InvalidOperationException(SR.GetResourceString(SR.ID0110));
}
}
/// <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<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.
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<
IConfigureOptions<AuthenticationOptions>, OpenIddictServerAspNetCoreConfiguration>());
@ -59,6 +59,9 @@ public static class OpenIddictServerAspNetCoreExtensions
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<
IPostConfigureOptions<OpenIddictServerAspNetCoreOptions>, OpenIddictServerAspNetCoreConfiguration>());
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<
IPostConfigureOptions<OpenIddictServerOptions>, OpenIddictServerAspNetCoreConfiguration>());
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,
ValidateClientAuthenticationMethod<ExtractPushedAuthorizationRequestContext>.Descriptor,
ExtractClientAuthenticationCertificate<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,
ValidateClientAuthenticationMethod<ExtractDeviceAuthorizationRequestContext>.Descriptor,
ExtractClientAuthenticationCertificate<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,
ValidateClientAuthenticationMethod<ExtractTokenRequestContext>.Descriptor,
ExtractClientAuthenticationCertificate<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,
ValidateClientAuthenticationMethod<ExtractIntrospectionRequestContext>.Descriptor,
ExtractClientAuthenticationCertificate<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,
ValidateClientAuthenticationMethod<ExtractRevocationRequestContext>.Descriptor,
ExtractClientAuthenticationCertificate<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.Diagnostics;
using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
@ -626,7 +627,7 @@ public static partial class OpenIddictServerAspNetCoreHandlers
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(TContext context)
public async ValueTask HandleAsync(TContext context)
{
ArgumentNullException.ThrowIfNull(context);
@ -648,13 +649,10 @@ public static partial class OpenIddictServerAspNetCoreHandlers
description: SR.FormatID2174(ClientAuthenticationMethods.ClientSecretPost),
uri: SR.FormatID8000(SR.ID2174));
return ValueTask.CompletedTask;
return;
}
// Reject requests that use client_secret_basic if support was explicitly disabled in the options.
//
// Note: the client_secret_jwt authentication method is not supported by OpenIddict out-of-the-box but
// is specified here to account for custom implementations that explicitly add client_secret_jwt support.
string? header = request.Headers[HeaderNames.Authorization];
if (!string.IsNullOrEmpty(header) && header.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase) &&
!context.Options.ClientAuthenticationMethods.Contains(ClientAuthenticationMethods.ClientSecretBasic))
@ -666,10 +664,82 @@ public static partial class OpenIddictServerAspNetCoreHandlers
description: SR.FormatID2174(ClientAuthenticationMethods.ClientSecretBasic),
uri: SR.FormatID8000(SR.ID2174));
return ValueTask.CompletedTask;
return;
}
return ValueTask.CompletedTask;
// If the request was sent using HTTPS, reject requests that use mTLS-based client authentication
// (self_signed_tls_client_auth or tls_client_auth) if support was not enabled in the server options.
if (request.IsHttps && await request.HttpContext.Connection.GetClientCertificateAsync(
request.HttpContext.RequestAborted) is X509Certificate2 certificate)
{
// Note: to avoid building and introspecting a X.509 certificate chain and reduce the cost
// of this check, a certificate is always assumed to be self-signed when it is self-issued.
if (OpenIddictHelpers.IsSelfIssuedCertificate(certificate))
{
if (!context.Options.ClientAuthenticationMethods.Contains(ClientAuthenticationMethods.SelfSignedTlsClientAuth))
{
context.Logger.LogInformation(6227, SR.GetResourceString(SR.ID6227), ClientAuthenticationMethods.SelfSignedTlsClientAuth);
context.Reject(
error: Errors.InvalidClient,
description: SR.FormatID2174(ClientAuthenticationMethods.SelfSignedTlsClientAuth),
uri: SR.FormatID8000(SR.ID2174));
return;
}
}
else if (!context.Options.ClientAuthenticationMethods.Contains(ClientAuthenticationMethods.TlsClientAuth))
{
context.Logger.LogInformation(6227, SR.GetResourceString(SR.ID6227), ClientAuthenticationMethods.TlsClientAuth);
context.Reject(
error: Errors.InvalidClient,
description: SR.FormatID2174(ClientAuthenticationMethods.TlsClientAuth),
uri: SR.FormatID8000(SR.ID2174));
return;
}
}
}
}
/// <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>()
.AddFilter<RequireHttpRequest>()
.UseSingletonHandler<ExtractBasicAuthenticationCredentials<TContext>>()
.SetOrder(ValidateClientAuthenticationMethod<TContext>.Descriptor.Order + 1_000)
.SetOrder(ExtractClientAuthenticationCertificate<TContext>.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
@ -730,7 +800,7 @@ public static partial class OpenIddictServerAspNetCoreHandlers
var data = Encoding.ASCII.GetString(Convert.FromBase64String(value));
var index = data.IndexOf(':');
if (index < 0)
if (index is < 0)
{
context.Reject(
error: Errors.InvalidRequest,

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

@ -14,6 +14,7 @@ namespace OpenIddict.Server.Owin;
/// </summary>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public sealed class OpenIddictServerOwinConfiguration : IConfigureOptions<OpenIddictServerOptions>,
IPostConfigureOptions<OpenIddictServerOptions>,
IPostConfigureOptions<OpenIddictServerOwinOptions>
{
/// <inheritdoc/>
@ -28,6 +29,24 @@ public sealed class OpenIddictServerOwinConfiguration : IConfigureOptions<OpenId
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/>
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<
IPostConfigureOptions<OpenIddictServerOwinOptions>, OpenIddictServerOwinConfiguration>());
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<
IPostConfigureOptions<OpenIddictServerOptions>, OpenIddictServerOwinConfiguration>());
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,
ValidateClientAuthenticationMethod<ExtractPushedAuthorizationRequestContext>.Descriptor,
ExtractClientAuthenticationCertificate<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,
ValidateClientAuthenticationMethod<ExtractDeviceAuthorizationRequestContext>.Descriptor,
ExtractClientAuthenticationCertificate<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,
ValidateClientAuthenticationMethod<ExtractTokenRequestContext>.Descriptor,
ExtractClientAuthenticationCertificate<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,
ValidateClientAuthenticationMethod<ExtractIntrospectionRequestContext>.Descriptor,
ExtractClientAuthenticationCertificate<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,
ValidateClientAuthenticationMethod<ExtractRevocationRequestContext>.Descriptor,
ExtractClientAuthenticationCertificate<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.Diagnostics;
using System.Globalization;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
@ -688,7 +689,7 @@ public static partial class OpenIddictServerOwinHandlers
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(TContext context)
public async ValueTask HandleAsync(TContext context)
{
ArgumentNullException.ThrowIfNull(context);
@ -710,7 +711,7 @@ public static partial class OpenIddictServerOwinHandlers
description: SR.FormatID2174(ClientAuthenticationMethods.ClientSecretPost),
uri: SR.FormatID8000(SR.ID2174));
return ValueTask.CompletedTask;
return;
}
// Reject requests that use client_secret_basic if support was explicitly disabled in the options.
@ -725,10 +726,118 @@ public static partial class OpenIddictServerOwinHandlers
description: SR.FormatID2174(ClientAuthenticationMethods.ClientSecretBasic),
uri: SR.FormatID8000(SR.ID2174));
return ValueTask.CompletedTask;
return;
}
return ValueTask.CompletedTask;
// If the request was sent using HTTPS, reject requests that use mTLS-based client authentication
// (self_signed_tls_client_auth or tls_client_auth) if support was not enabled in the server options.
if (request.IsSecure && await GetClientCertificateAsync(request.Context) is X509Certificate2 certificate)
{
// Note: to avoid building and introspecting a X.509 certificate chain and reduce the cost
// of this check, a certificate is always assumed to be self-signed when it is self-issued.
if (OpenIddictHelpers.IsSelfIssuedCertificate(certificate))
{
if (!context.Options.ClientAuthenticationMethods.Contains(ClientAuthenticationMethods.SelfSignedTlsClientAuth))
{
context.Logger.LogInformation(6227, SR.GetResourceString(SR.ID6227), ClientAuthenticationMethods.SelfSignedTlsClientAuth);
context.Reject(
error: Errors.InvalidClient,
description: SR.FormatID2174(ClientAuthenticationMethods.SelfSignedTlsClientAuth),
uri: SR.FormatID8000(SR.ID2174));
return;
}
}
else if (!context.Options.ClientAuthenticationMethods.Contains(ClientAuthenticationMethods.TlsClientAuth))
{
context.Logger.LogInformation(6227, SR.GetResourceString(SR.ID6227), ClientAuthenticationMethods.TlsClientAuth);
context.Reject(
error: Errors.InvalidClient,
description: SR.FormatID2174(ClientAuthenticationMethods.TlsClientAuth),
uri: SR.FormatID8000(SR.ID2174));
return;
}
}
static async ValueTask<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>()
.AddFilter<RequireOwinRequest>()
.UseSingletonHandler<ExtractBasicAuthenticationCredentials<TContext>>()
.SetOrder(ValidateClientAuthenticationMethod<TContext>.Descriptor.Order + 1_000)
.SetOrder(ExtractClientAuthenticationCertificate<TContext>.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
@ -789,7 +898,7 @@ public static partial class OpenIddictServerOwinHandlers
var data = Encoding.ASCII.GetString(Convert.FromBase64String(value));
var index = data.IndexOf(':');
if (index < 0)
if (index is < 0)
{
context.Reject(
error: Errors.InvalidRequest,

443
src/OpenIddict.Server/OpenIddictServerBuilder.cs

@ -4,6 +4,7 @@
* the license and the contributors participating to this project.
*/
using System;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
@ -11,6 +12,7 @@ using System.Reflection;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.Server;
@ -144,8 +146,7 @@ public sealed class OpenIddictServerBuilder
ArgumentNullException.ThrowIfNull(key);
// If the encryption key is an asymmetric security key, ensure it has a private key.
if (key is AsymmetricSecurityKey asymmetricSecurityKey &&
asymmetricSecurityKey.PrivateKeyStatus is PrivateKeyStatus.DoesNotExist)
if (key is AsymmetricSecurityKey { PrivateKeyStatus: PrivateKeyStatus.DoesNotExist })
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0055));
}
@ -211,7 +212,7 @@ public sealed class OpenIddictServerBuilder
// If no valid existing certificate was found, create a new encryption certificate.
var certificates = store.Certificates
.Find(X509FindType.FindBySubjectDistinguishedName, subject.Name, validOnly: false)
.OfType<X509Certificate2>()
.Cast<X509Certificate2>()
.ToList();
if (!certificates.Exists(certificate => certificate.NotBefore < now.LocalDateTime && certificate.NotAfter > now.LocalDateTime))
@ -327,7 +328,7 @@ public sealed class OpenIddictServerBuilder
// If the certificate is a X.509v3 certificate that specifies at least one
// key usage, ensure that the certificate key can be used for key encryption.
if (certificate.Version >= 3)
if (certificate.Version is >= 3)
{
var extensions = certificate.Extensions.OfType<X509KeyUsageExtension>().ToList();
if (extensions.Count is not 0 && !extensions.Exists(static extension =>
@ -446,7 +447,7 @@ public sealed class OpenIddictServerBuilder
store.Open(OpenFlags.ReadOnly);
return store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false)
.OfType<X509Certificate2>()
.Cast<X509Certificate2>()
.SingleOrDefault();
}
}
@ -467,7 +468,7 @@ public sealed class OpenIddictServerBuilder
return AddEncryptionCertificate(
store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false)
.OfType<X509Certificate2>()
.Cast<X509Certificate2>()
.SingleOrDefault() ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0066)));
}
@ -505,8 +506,7 @@ public sealed class OpenIddictServerBuilder
ArgumentNullException.ThrowIfNull(key);
// If the signing key is an asymmetric security key, ensure it has a private key.
if (key is AsymmetricSecurityKey asymmetricSecurityKey &&
asymmetricSecurityKey.PrivateKeyStatus is PrivateKeyStatus.DoesNotExist)
if (key is AsymmetricSecurityKey { PrivateKeyStatus: PrivateKeyStatus.DoesNotExist })
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0067));
}
@ -590,7 +590,7 @@ public sealed class OpenIddictServerBuilder
// If no valid existing certificate was found, create a new signing certificate.
var certificates = store.Certificates
.Find(X509FindType.FindBySubjectDistinguishedName, subject.Name, validOnly: false)
.OfType<X509Certificate2>()
.Cast<X509Certificate2>()
.ToList();
if (!certificates.Exists(certificate =>
@ -735,7 +735,7 @@ public sealed class OpenIddictServerBuilder
// If the certificate is a X.509v3 certificate that specifies at least
// one key usage, ensure that the certificate key can be used for signing.
if (certificate.Version >= 3)
if (certificate.Version is >= 3)
{
var extensions = certificate.Extensions.OfType<X509KeyUsageExtension>().ToList();
if (extensions.Count is not 0 && !extensions.Exists(static extension =>
@ -854,7 +854,7 @@ public sealed class OpenIddictServerBuilder
store.Open(OpenFlags.ReadOnly);
return store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false)
.OfType<X509Certificate2>()
.Cast<X509Certificate2>()
.SingleOrDefault();
}
}
@ -875,7 +875,7 @@ public sealed class OpenIddictServerBuilder
return AddSigningCertificate(
store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false)
.OfType<X509Certificate2>()
.Cast<X509Certificate2>()
.SingleOrDefault() ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0066)));
}
@ -1893,7 +1893,7 @@ public sealed class OpenIddictServerBuilder
/// Sets the issuer URI, which is used as the value of the "issuer" claim and
/// is returned from the discovery endpoint to identify the authorization server.
/// </summary>
/// <param name="uri">The issuer uri.</param>
/// <param name="uri">The issuer URI.</param>
/// <returns>The <see cref="OpenIddictServerBuilder"/> instance.</returns>
public OpenIddictServerBuilder SetIssuer(Uri uri)
{
@ -1906,7 +1906,7 @@ public sealed class OpenIddictServerBuilder
/// Sets the issuer URI, which is used as the value of the "issuer" claim and
/// is returned from the discovery endpoint to identify the authorization server.
/// </summary>
/// <param name="uri">The issuer uri.</param>
/// <param name="uri">The issuer URI.</param>
/// <returns>The <see cref="OpenIddictServerBuilder"/> instance.</returns>
public OpenIddictServerBuilder SetIssuer(
[StringSyntax(StringSyntaxAttribute.Uri, UriKind.Absolute)] string uri)
@ -1921,6 +1921,256 @@ public sealed class OpenIddictServerBuilder
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>
/// Configures OpenIddict to use reference tokens, so that the access token payloads
/// are stored in the database (only an identifier is returned to the client application).
@ -1962,6 +2212,171 @@ public sealed class OpenIddictServerBuilder
public OpenIddictServerBuilder EnableEndSessionRequestCaching()
=> Configure(options => options.EnableEndSessionRequestCaching = true);
/// <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/>
[EditorBrowsable(EditorBrowsableState.Never)]
public override bool Equals(object? obj) => base.Equals(obj);

110
src/OpenIddict.Server/OpenIddictServerConfiguration.cs

@ -7,6 +7,7 @@
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
@ -164,6 +165,19 @@ public sealed class OpenIddictServerConfiguration : IPostConfigureOptions<OpenId
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.
if (options.SubjectTypes.Count is 0)
{
@ -246,19 +260,104 @@ public sealed class OpenIddictServerConfiguration : IPostConfigureOptions<OpenId
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 (options.EncryptionCredentials.TrueForAll(credentials => credentials.Key is X509SecurityKey x509SecurityKey &&
(x509SecurityKey.Certificate.NotBefore > now || x509SecurityKey.Certificate.NotAfter < now)))
if (options.EncryptionCredentials.TrueForAll(credentials =>
credentials.Key is X509SecurityKey { Certificate: X509Certificate2 certificate } &&
(certificate.NotBefore > now || certificate.NotAfter < now)))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0087));
}
// If all the registered signing credentials are backed by a X.509 certificate, at least one of them must be valid.
if (options.SigningCredentials.TrueForAll(credentials => credentials.Key is X509SecurityKey x509SecurityKey &&
(x509SecurityKey.Certificate.NotBefore > now || x509SecurityKey.Certificate.NotAfter < now)))
if (options.SigningCredentials.TrueForAll(credentials =>
credentials.Key is X509SecurityKey { Certificate: X509Certificate2 certificate } &&
(certificate.NotBefore > now || certificate.NotAfter < now)))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0088));
}
// When set, the mTLS endpoint aliases MUST represent absolute HTTPS URLs.
if (!TryValidateMtlsEndpointAlias(options.MtlsDeviceAuthorizationEndpointAliasUri) ||
!TryValidateMtlsEndpointAlias(options.MtlsIntrospectionEndpointAliasUri) ||
!TryValidateMtlsEndpointAlias(options.MtlsPushedAuthorizationEndpointAliasUri) ||
!TryValidateMtlsEndpointAlias(options.MtlsRevocationEndpointAliasUri) ||
!TryValidateMtlsEndpointAlias(options.MtlsTokenEndpointAliasUri))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0499));
}
// Prevent the mTLS aliases from being configured if the corresponding endpoints haven't been enabled.
if ((options.MtlsDeviceAuthorizationEndpointAliasUri is not null && options.DeviceAuthorizationEndpointUris.Count is 0) ||
(options.MtlsIntrospectionEndpointAliasUri is not null && options.IntrospectionEndpointUris.Count is 0) ||
(options.MtlsPushedAuthorizationEndpointAliasUri is not null && options.PushedAuthorizationEndpointUris.Count is 0) ||
(options.MtlsRevocationEndpointAliasUri is not null && options.RevocationEndpointUris.Count is 0) ||
(options.MtlsTokenEndpointAliasUri is not null && options.TokenEndpointUris.Count is 0))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0510));
}
// If at least one mTLS endpoint alias was configured, require that the issuer be explicitly set
// to ensure it is not dynamically computed based on the current URI, as this would result in two
// different issuers being used (one pointing to the mTLS domain and one pointing to the regular one).
if (options.Issuer is null && (options.MtlsDeviceAuthorizationEndpointAliasUri is not null ||
options.MtlsIntrospectionEndpointAliasUri is not null ||
options.MtlsPushedAuthorizationEndpointAliasUri is not null ||
options.MtlsRevocationEndpointAliasUri is not null ||
options.MtlsTokenEndpointAliasUri is not null))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0500));
}
// Ensure no end certificate was included in the PKI client certificate
// chain policy and that none of the certificates contains a private key.
if (options.ClientCertificateChainPolicy is not null)
{
if (options.ClientCertificateChainPolicy.ExtraStore.Cast<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 the degraded mode was enabled, ensure custom validation handlers
@ -529,5 +628,8 @@ public sealed class OpenIddictServerConfiguration : IPostConfigureOptions<OpenId
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>
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>
/// Gets the list of claims supported by the authorization server.
/// </summary>

10
src/OpenIddict.Server/OpenIddictServerEvents.cs

@ -6,6 +6,7 @@
using System.ComponentModel;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.Logging;
namespace OpenIddict.Server;
@ -764,6 +765,15 @@ public static partial class OpenIddictServerEvents
/// </summary>
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>
/// Gets or sets the device code to validate, if applicable.
/// </summary>

1
src/OpenIddict.Server/OpenIddictServerExtensions.cs

@ -49,6 +49,7 @@ public static class OpenIddictServerExtensions
builder.Services.TryAddSingleton<RequireAuthorizationRequest>();
builder.Services.TryAddSingleton<RequireClientAssertionPrincipal>();
builder.Services.TryAddSingleton<RequireClientAssertionValidated>();
builder.Services.TryAddSingleton<RequireClientCertificate>();
builder.Services.TryAddSingleton<RequireClientIdParameter>();
builder.Services.TryAddSingleton<RequireClientSecretParameter>();
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>
/// Represents a filter that excludes the associated handlers when no client identifier is received.
/// </summary>

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

@ -8,6 +8,7 @@ using System.Collections.Immutable;
using System.Diagnostics;
using System.Security.Cryptography;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
@ -235,6 +236,7 @@ public static partial class OpenIddictServerHandlers
[Metadata.UserInfoEndpoint] = notification.UserInfoEndpoint?.AbsoluteUri,
[Metadata.DeviceAuthorizationEndpoint] = notification.DeviceAuthorizationEndpoint?.AbsoluteUri,
[Metadata.PushedAuthorizationRequestEndpoint] = notification.PushedAuthorizationEndpoint?.AbsoluteUri,
[Metadata.MtlsEndpointAliases] = CreateMtlsEndpointAliases(notification),
[Metadata.JwksUri] = notification.JsonWebKeySetEndpoint?.AbsoluteUri,
[Metadata.GrantTypesSupported] = notification.GrantTypes.ToImmutableArray<string?>(),
[Metadata.ResponseTypesSupported] = notification.ResponseTypes.ToImmutableArray<string?>(),
@ -259,6 +261,38 @@ public static partial class OpenIddictServerHandlers
}
context.Transaction.Response = response;
static JsonObject CreateMtlsEndpointAliases(HandleConfigurationRequestContext context)
{
var node = new JsonObject();
if (context.MtlsDeviceAuthorizationEndpointAlias is not null)
{
node.Add(Metadata.DeviceAuthorizationEndpoint, context.MtlsDeviceAuthorizationEndpointAlias.AbsoluteUri);
}
if (context.MtlsIntrospectionEndpointAlias is not null)
{
node.Add(Metadata.IntrospectionEndpoint, context.MtlsIntrospectionEndpointAlias.AbsoluteUri);
}
if (context.MtlsPushedAuthorizationEndpointAlias is not null)
{
node.Add(Metadata.PushedAuthorizationRequestEndpoint, context.MtlsPushedAuthorizationEndpointAlias.AbsoluteUri);
}
if (context.MtlsRevocationEndpointAlias is not null)
{
node.Add(Metadata.RevocationEndpoint, context.MtlsRevocationEndpointAlias.AbsoluteUri);
}
if (context.MtlsTokenEndpointAlias is not null)
{
node.Add(Metadata.TokenEndpoint, context.MtlsTokenEndpointAlias.AbsoluteUri);
}
return node;
}
}
}
@ -377,6 +411,21 @@ public static partial class OpenIddictServerHandlers
context.JsonWebKeySetEndpoint ??= OpenIddictHelpers.CreateAbsoluteUri(
context.BaseUri, context.Options.JsonWebKeySetEndpointUris.FirstOrDefault());
context.MtlsDeviceAuthorizationEndpointAlias ??= OpenIddictHelpers.CreateAbsoluteUri(
context.BaseUri, context.Options.MtlsDeviceAuthorizationEndpointAliasUri);
context.MtlsIntrospectionEndpointAlias ??= OpenIddictHelpers.CreateAbsoluteUri(
context.BaseUri, context.Options.MtlsIntrospectionEndpointAliasUri);
context.MtlsPushedAuthorizationEndpointAlias ??= OpenIddictHelpers.CreateAbsoluteUri(
context.BaseUri, context.Options.MtlsPushedAuthorizationEndpointAliasUri);
context.MtlsRevocationEndpointAlias ??= OpenIddictHelpers.CreateAbsoluteUri(
context.BaseUri, context.Options.MtlsRevocationEndpointAliasUri);
context.MtlsTokenEndpointAlias ??= OpenIddictHelpers.CreateAbsoluteUri(
context.BaseUri, context.Options.MtlsTokenEndpointAliasUri);
context.PushedAuthorizationEndpoint ??= OpenIddictHelpers.CreateAbsoluteUri(
context.BaseUri, context.Options.PushedAuthorizationEndpointUris.FirstOrDefault());

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

134
src/OpenIddict.Server/OpenIddictServerHandlers.cs

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

89
src/OpenIddict.Server/OpenIddictServerOptions.cs

@ -5,6 +5,7 @@
*/
using System.ComponentModel;
using System.Security.Cryptography.X509Certificates;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
@ -127,6 +128,56 @@ public sealed class OpenIddictServerOptions
SetDefaultTimesOnTokenCreation = false
};
/// <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>
/// Gets the token validation parameters used by the OpenIddict server services.
/// </summary>
@ -605,4 +656,42 @@ public sealed class OpenIddictServerOptions
/// If no service can be found, <see cref="TimeProvider.System"/> is used.
/// </remarks>
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.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.Logging;
namespace OpenIddict.Server;
@ -26,6 +27,11 @@ public sealed class OpenIddictServerTransaction
/// </remarks>
public CancellationToken CancellationToken { get; set; }
/// <summary>
/// Gets or sets the X.509 client certificate, if available.
/// </summary>
public X509Certificate2? ClientCertificate { get; set; }
/// <summary>
/// Gets or sets the type of the endpoint processing the current request.
/// </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
// is dynamically added if one of the default schemes properties is not set
// and less than 2 handlers were registered in the authentication options.
if (options.SchemeMap.Count < 2 && string.IsNullOrEmpty(options.DefaultScheme) &&
if (options.SchemeMap.Count is < 2 && string.IsNullOrEmpty(options.DefaultScheme) &&
(string.IsNullOrEmpty(options.DefaultSignInScheme) ||
string.IsNullOrEmpty(options.DefaultSignOutScheme)))
{

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

@ -253,10 +253,12 @@ public sealed class OpenIddictValidationSystemNetHttpConfiguration : IConfigureO
foreach (var credentials in _provider.GetRequiredService<IOptionsMonitor<OpenIddictValidationOptions>>()
.CurrentValue.SigningCredentials)
{
// Note: to avoid building and introspecting a X.509 certificate chain and reduce the cost
// of this check, a certificate is always assumed to be self-signed when it is self-issued.
if (credentials.Key is X509SecurityKey { Certificate: X509Certificate2 certificate } &&
certificate.Version is >= 3 && IsSelfIssuedCertificate(certificate) &&
HasDigitalSignatureKeyUsage(certificate) &&
HasClientAuthenticationExtendedKeyUsage(certificate))
certificate.Version is >= 3 && OpenIddictHelpers.IsSelfIssuedCertificate(certificate) &&
OpenIddictHelpers.HasKeyUsage(certificate, X509KeyUsageFlags.DigitalSignature) &&
OpenIddictHelpers.HasExtendedKeyUsage(certificate, ObjectIdentifiers.ExtendedKeyUsages.ClientAuthentication))
{
return certificate;
}
@ -270,10 +272,12 @@ public sealed class OpenIddictValidationSystemNetHttpConfiguration : IConfigureO
foreach (var credentials in _provider.GetRequiredService<IOptionsMonitor<OpenIddictValidationOptions>>()
.CurrentValue.SigningCredentials)
{
// Note: to avoid building and introspecting a X.509 certificate chain and reduce the cost
// of this check, a certificate is always assumed to be self-signed when it is self-issued.
if (credentials.Key is X509SecurityKey { Certificate: X509Certificate2 certificate } &&
certificate.Version is >= 3 && !IsSelfIssuedCertificate(certificate) &&
HasDigitalSignatureKeyUsage(certificate) &&
HasClientAuthenticationExtendedKeyUsage(certificate))
certificate.Version is >= 3 && !OpenIddictHelpers.IsSelfIssuedCertificate(certificate) &&
OpenIddictHelpers.HasKeyUsage(certificate, X509KeyUsageFlags.DigitalSignature) &&
OpenIddictHelpers.HasExtendedKeyUsage(certificate, ObjectIdentifiers.ExtendedKeyUsages.ClientAuthentication))
{
return certificate;
}
@ -281,51 +285,5 @@ public sealed class OpenIddictValidationSystemNetHttpConfiguration : IConfigureO
return null;
};
static bool HasClientAuthenticationExtendedKeyUsage(X509Certificate2 certificate)
{
for (var index = 0; index < certificate.Extensions.Count; index++)
{
if (certificate.Extensions[index] is X509EnhancedKeyUsageExtension extension &&
HasOid(extension.EnhancedKeyUsages, "1.3.6.1.5.5.7.3.2"))
{
return true;
}
}
return false;
static bool HasOid(OidCollection collection, string value)
{
for (var index = 0; index < collection.Count; index++)
{
if (collection[index] is Oid oid && string.Equals(oid.Value, value, StringComparison.Ordinal))
{
return true;
}
}
return false;
}
}
static bool HasDigitalSignatureKeyUsage(X509Certificate2 certificate)
{
for (var index = 0; index < certificate.Extensions.Count; index++)
{
if (certificate.Extensions[index] is X509KeyUsageExtension extension &&
extension.KeyUsages.HasFlag(X509KeyUsageFlags.DigitalSignature))
{
return true;
}
}
return false;
}
// Note: to avoid building and introspecting a X.509 certificate chain and reduce the cost
// of this check, a certificate is always assumed to be self-signed when it is self-issued.
static bool IsSelfIssuedCertificate(X509Certificate2 certificate)
=> certificate.SubjectName.RawData.AsSpan().SequenceEqual(certificate.IssuerName.RawData);
}
}

18
src/OpenIddict.Validation/OpenIddictValidationBuilder.cs

@ -134,8 +134,7 @@ public sealed class OpenIddictValidationBuilder
ArgumentNullException.ThrowIfNull(key);
// If the encryption key is an asymmetric security key, ensure it has a private key.
if (key is AsymmetricSecurityKey asymmetricSecurityKey &&
asymmetricSecurityKey.PrivateKeyStatus is PrivateKeyStatus.DoesNotExist)
if (key is AsymmetricSecurityKey { PrivateKeyStatus: PrivateKeyStatus.DoesNotExist })
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0055));
}
@ -178,7 +177,7 @@ public sealed class OpenIddictValidationBuilder
// If the certificate is a X.509v3 certificate that specifies at least one
// key usage, ensure that the certificate key can be used for key encryption.
if (certificate.Version >= 3)
if (certificate.Version is >= 3)
{
var extensions = certificate.Extensions.OfType<X509KeyUsageExtension>().ToList();
if (extensions.Count is not 0 && !extensions.Exists(static extension =>
@ -299,7 +298,7 @@ public sealed class OpenIddictValidationBuilder
store.Open(OpenFlags.ReadOnly);
return store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false)
.OfType<X509Certificate2>()
.Cast<X509Certificate2>()
.SingleOrDefault();
}
}
@ -321,7 +320,7 @@ public sealed class OpenIddictValidationBuilder
return AddEncryptionCertificate(
store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false)
.OfType<X509Certificate2>()
.Cast<X509Certificate2>()
.SingleOrDefault() ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0066)));
}
@ -359,8 +358,7 @@ public sealed class OpenIddictValidationBuilder
ArgumentNullException.ThrowIfNull(key);
// If the signing key is an asymmetric security key, ensure it has a private key.
if (key is AsymmetricSecurityKey asymmetricSecurityKey &&
asymmetricSecurityKey.PrivateKeyStatus is PrivateKeyStatus.DoesNotExist)
if (key is AsymmetricSecurityKey { PrivateKeyStatus: PrivateKeyStatus.DoesNotExist })
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0067));
}
@ -426,7 +424,7 @@ public sealed class OpenIddictValidationBuilder
// If the certificate is a X.509v3 certificate that specifies at least
// one key usage, ensure that the certificate key can be used for signing.
if (certificate.Version >= 3)
if (certificate.Version is >= 3)
{
var extensions = certificate.Extensions.OfType<X509KeyUsageExtension>().ToList();
if (extensions.Count is not 0 && !extensions.Exists(static extension =>
@ -545,7 +543,7 @@ public sealed class OpenIddictValidationBuilder
store.Open(OpenFlags.ReadOnly);
return store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false)
.OfType<X509Certificate2>()
.Cast<X509Certificate2>()
.SingleOrDefault();
}
}
@ -566,7 +564,7 @@ public sealed class OpenIddictValidationBuilder
return AddSigningCertificate(
store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false)
.OfType<X509Certificate2>()
.Cast<X509Certificate2>()
.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.
*/
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Protocols;
@ -97,9 +98,9 @@ public sealed class OpenIddictValidationConfiguration : IPostConfigureOptions<Op
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 (options.EncryptionCredentials.Count is not 0 &&
options.EncryptionCredentials.TrueForAll(credentials => credentials.Key is X509SecurityKey x509SecurityKey &&
(x509SecurityKey.Certificate.NotBefore > now || x509SecurityKey.Certificate.NotAfter < now)))
if (options.EncryptionCredentials.Count is not 0 && options.EncryptionCredentials.TrueForAll(credentials =>
credentials.Key is X509SecurityKey { Certificate: X509Certificate2 certificate } &&
(certificate.NotBefore > now || certificate.NotAfter < now)))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0087));
}

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

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);
}
[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]
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());
}
[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]
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")
.SetJsonWebKeySetEndpointUris("https://www.fabrikam.com/path/cryptography_endpoint")
.SetDeviceAuthorizationEndpointUris("https://www.fabrikam.com/path/device_endpoint")
.SetDeviceAuthorizationEndpointUris("https://www.fabrikam.com/path/device_authorization_endpoint")
.SetIntrospectionEndpointUris("https://www.fabrikam.com/path/introspection_endpoint")
.SetEndSessionEndpointUris("https://www.fabrikam.com/path/logout_endpoint")
.SetEndSessionEndpointUris("https://www.fabrikam.com/path/end_session_endpoint")
.SetPushedAuthorizationEndpointUris("https://www.fabrikam.com/path/pushed_authorization_endpoint")
.SetRevocationEndpointUris("https://www.fabrikam.com/path/revocation_endpoint")
.SetTokenEndpointUris("https://www.fabrikam.com/path/token_endpoint")
.SetUserInfoEndpointUris("https://www.fabrikam.com/path/userinfo_endpoint");
@ -288,15 +289,18 @@ public abstract partial class OpenIddictServerIntegrationTests
Assert.Equal("https://www.fabrikam.com/path/authorization_endpoint",
(string?) response[Metadata.AuthorizationEndpoint]);
Assert.Equal("https://www.fabrikam.com/path/device_endpoint",
Assert.Equal("https://www.fabrikam.com/path/device_authorization_endpoint",
(string?) response[Metadata.DeviceAuthorizationEndpoint]);
Assert.Equal("https://www.fabrikam.com/path/introspection_endpoint",
(string?) response[Metadata.IntrospectionEndpoint]);
Assert.Equal("https://www.fabrikam.com/path/logout_endpoint",
Assert.Equal("https://www.fabrikam.com/path/end_session_endpoint",
(string?) response[Metadata.EndSessionEndpoint]);
Assert.Equal("https://www.fabrikam.com/path/pushed_authorization_endpoint",
(string?) response[Metadata.PushedAuthorizationRequestEndpoint]);
Assert.Equal("https://www.fabrikam.com/path/revocation_endpoint",
(string?) response[Metadata.RevocationEndpoint]);
@ -315,9 +319,9 @@ public abstract partial class OpenIddictServerIntegrationTests
{
options.SetAuthorizationEndpointUris("path/authorization_endpoint")
.SetJsonWebKeySetEndpointUris("path/cryptography_endpoint")
.SetDeviceAuthorizationEndpointUris("path/device_endpoint")
.SetDeviceAuthorizationEndpointUris("path/device_authorization_endpoint")
.SetIntrospectionEndpointUris("path/introspection_endpoint")
.SetEndSessionEndpointUris("path/logout_endpoint")
.SetEndSessionEndpointUris("path/end_session_endpoint")
.SetPushedAuthorizationEndpointUris("path/pushed_authorization_endpoint")
.SetRevocationEndpointUris("path/revocation_endpoint")
.SetTokenEndpointUris("path/token_endpoint")
@ -332,9 +336,9 @@ public abstract partial class OpenIddictServerIntegrationTests
// Assert
Assert.Equal("http://localhost/path/authorization_endpoint", (string?) response[Metadata.AuthorizationEndpoint]);
Assert.Equal("http://localhost/path/cryptography_endpoint", (string?) response[Metadata.JwksUri]);
Assert.Equal("http://localhost/path/device_endpoint", (string?) response[Metadata.DeviceAuthorizationEndpoint]);
Assert.Equal("http://localhost/path/device_authorization_endpoint", (string?) response[Metadata.DeviceAuthorizationEndpoint]);
Assert.Equal("http://localhost/path/introspection_endpoint", (string?) response[Metadata.IntrospectionEndpoint]);
Assert.Equal("http://localhost/path/logout_endpoint", (string?) response[Metadata.EndSessionEndpoint]);
Assert.Equal("http://localhost/path/end_session_endpoint", (string?) response[Metadata.EndSessionEndpoint]);
Assert.Equal("http://localhost/path/pushed_authorization_endpoint", (string?) response[Metadata.PushedAuthorizationRequestEndpoint]);
Assert.Equal("http://localhost/path/revocation_endpoint", (string?) response[Metadata.RevocationEndpoint]);
Assert.Equal("http://localhost/path/token_endpoint", (string?) response[Metadata.TokenEndpoint]);
@ -349,9 +353,9 @@ public abstract partial class OpenIddictServerIntegrationTests
{
options.SetAuthorizationEndpointUris("path/authorization_endpoint")
.SetJsonWebKeySetEndpointUris("path/cryptography_endpoint")
.SetDeviceAuthorizationEndpointUris("path/device_endpoint")
.SetDeviceAuthorizationEndpointUris("path/device_authorization_endpoint")
.SetIntrospectionEndpointUris("path/introspection_endpoint")
.SetEndSessionEndpointUris("path/logout_endpoint")
.SetEndSessionEndpointUris("path/end_session_endpoint")
.SetPushedAuthorizationEndpointUris("path/pushed_authorization_endpoint")
.SetRevocationEndpointUris("path/revocation_endpoint")
.SetTokenEndpointUris("path/token_endpoint")
@ -378,9 +382,9 @@ public abstract partial class OpenIddictServerIntegrationTests
// Assert
Assert.Equal("https://contoso.com/issuer/path/authorization_endpoint", (string?) response[Metadata.AuthorizationEndpoint]);
Assert.Equal("https://contoso.com/issuer/path/cryptography_endpoint", (string?) response[Metadata.JwksUri]);
Assert.Equal("https://contoso.com/issuer/path/device_endpoint", (string?) response[Metadata.DeviceAuthorizationEndpoint]);
Assert.Equal("https://contoso.com/issuer/path/device_authorization_endpoint", (string?) response[Metadata.DeviceAuthorizationEndpoint]);
Assert.Equal("https://contoso.com/issuer/path/introspection_endpoint", (string?) response[Metadata.IntrospectionEndpoint]);
Assert.Equal("https://contoso.com/issuer/path/logout_endpoint", (string?) response[Metadata.EndSessionEndpoint]);
Assert.Equal("https://contoso.com/issuer/path/end_session_endpoint", (string?) response[Metadata.EndSessionEndpoint]);
Assert.Equal("https://contoso.com/issuer/path/pushed_authorization_endpoint", (string?) response[Metadata.PushedAuthorizationRequestEndpoint]);
Assert.Equal("https://contoso.com/issuer/path/revocation_endpoint", (string?) response[Metadata.RevocationEndpoint]);
Assert.Equal("https://contoso.com/issuer/path/token_endpoint", (string?) response[Metadata.TokenEndpoint]);
@ -977,6 +981,43 @@ public abstract partial class OpenIddictServerIntegrationTests
Assert.False((bool?) response[Metadata.RequestUriParameterSupported]);
}
[Fact]
public async Task HandleConfigurationRequest_MtlsAliasesAreReturned()
{
// Arrange
await using var server = await CreateServerAsync(options =>
{
options.SetIssuer("https://mtls.fabrikam.com/");
options.SetMtlsDeviceAuthorizationEndpointAliasUri("https://mtls.fabrikam.com/path/device_authorization_endpoint")
.SetMtlsIntrospectionEndpointAliasUri("https://mtls.fabrikam.com/path/introspection_endpoint")
.SetMtlsPushedAuthorizationEndpointAliasUri("https://mtls.fabrikam.com/path/pushed_authorization_endpoint")
.SetMtlsRevocationEndpointAliasUri("https://mtls.fabrikam.com/path/revocation_endpoint")
.SetMtlsTokenEndpointAliasUri("https://mtls.fabrikam.com/path/token_endpoint");
});
await using var client = await server.CreateClientAsync();
// Act
var response = await client.GetAsync("/.well-known/openid-configuration");
// Assert
Assert.Equal("https://mtls.fabrikam.com/path/device_authorization_endpoint",
(string?) response[Metadata.MtlsEndpointAliases]?[Metadata.DeviceAuthorizationEndpoint]);
Assert.Equal("https://mtls.fabrikam.com/path/introspection_endpoint",
(string?) response[Metadata.MtlsEndpointAliases]?[Metadata.IntrospectionEndpoint]);
Assert.Equal("https://mtls.fabrikam.com/path/pushed_authorization_endpoint",
(string?) response[Metadata.MtlsEndpointAliases]?[Metadata.PushedAuthorizationRequestEndpoint]);
Assert.Equal("https://mtls.fabrikam.com/path/revocation_endpoint",
(string?) response[Metadata.MtlsEndpointAliases]?[Metadata.RevocationEndpoint]);
Assert.Equal("https://mtls.fabrikam.com/path/token_endpoint",
(string?) response[Metadata.MtlsEndpointAliases]?[Metadata.TokenEndpoint]);
}
[Theory]
[InlineData(true)]
[InlineData(false)]

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

Loading…
Cancel
Save