Browse Source

Revamp the client authentication method negotiation logic and support mTLS token binding in the client, server and validation stacks

pull/2429/head
Kévin Chalet 1 month ago
parent
commit
7b9fd7677f
  1. 12
      gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs
  2. 4
      sandbox/OpenIddict.Sandbox.AspNetCore.Client/Controllers/HomeController.cs
  3. 375
      sandbox/OpenIddict.Sandbox.AspNetCore.Client/Startup.cs
  4. 26
      sandbox/OpenIddict.Sandbox.AspNetCore.Server/Startup.cs
  5. 86
      sandbox/OpenIddict.Sandbox.Console.Client/InteractiveService.cs
  6. 16
      shared/OpenIddict.Extensions/OpenIddictHelpers.cs
  7. 1
      shared/OpenIddict.Extensions/OpenIddictPolyfills.cs
  8. 66
      src/OpenIddict.Abstractions/Managers/IOpenIddictApplicationManager.cs
  9. 1
      src/OpenIddict.Abstractions/OpenIddictConstants.cs
  10. 66
      src/OpenIddict.Abstractions/OpenIddictResources.resx
  11. 1
      src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlers.Authentication.cs
  12. 1
      src/OpenIddict.Client.Owin/OpenIddictClientOwinHelpers.cs
  13. 56
      src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationMarshal.cs
  14. 2
      src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpBuilder.cs
  15. 118
      src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpConfiguration.cs
  16. 83
      src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpContext.cs
  17. 3
      src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpExtensions.cs
  18. 654
      src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.cs
  19. 2
      src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpOptions.cs
  20. 34
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationConfiguration.cs
  21. 3
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationExtensions.cs
  22. 11
      src/OpenIddict.Client/OpenIddictClientConfiguration.cs
  23. 57
      src/OpenIddict.Client/OpenIddictClientEvents.cs
  24. 1120
      src/OpenIddict.Client/OpenIddictClientHandlers.cs
  25. 223
      src/OpenIddict.Client/OpenIddictClientModels.cs
  26. 5
      src/OpenIddict.Client/OpenIddictClientOptions.cs
  27. 16
      src/OpenIddict.Client/OpenIddictClientRegistration.cs
  28. 71
      src/OpenIddict.Client/OpenIddictClientService.cs
  29. 344
      src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs
  30. 4
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConfiguration.cs
  31. 2
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Authentication.cs
  32. 2
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Device.cs
  33. 2
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Exchange.cs
  34. 2
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Introspection.cs
  35. 2
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Revocation.cs
  36. 1
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Userinfo.cs
  37. 55
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs
  38. 4
      src/OpenIddict.Server.Owin/OpenIddictServerOwinConfiguration.cs
  39. 2
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Authentication.cs
  40. 2
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Device.cs
  41. 2
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Exchange.cs
  42. 2
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Introspection.cs
  43. 2
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Revocation.cs
  44. 1
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Userinfo.cs
  45. 78
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs
  46. 105
      src/OpenIddict.Server/OpenIddictServerBuilder.cs
  47. 49
      src/OpenIddict.Server/OpenIddictServerConfiguration.cs
  48. 10
      src/OpenIddict.Server/OpenIddictServerEvents.Discovery.cs
  49. 6
      src/OpenIddict.Server/OpenIddictServerEvents.Introspection.cs
  50. 5
      src/OpenIddict.Server/OpenIddictServerEvents.Protection.cs
  51. 9
      src/OpenIddict.Server/OpenIddictServerEvents.cs
  52. 1
      src/OpenIddict.Server/OpenIddictServerExtensions.cs
  53. 18
      src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs
  54. 13
      src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs
  55. 42
      src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs
  56. 88
      src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs
  57. 400
      src/OpenIddict.Server/OpenIddictServerHandlers.cs
  58. 62
      src/OpenIddict.Server/OpenIddictServerOptions.cs
  59. 4
      src/OpenIddict.Server/OpenIddictServerTransaction.cs
  60. 38
      src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandlers.cs
  61. 51
      src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandlers.cs
  62. 2
      src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpBuilder.cs
  63. 98
      src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpConfiguration.cs
  64. 67
      src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpContext.cs
  65. 3
      src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpExtensions.cs
  66. 151
      src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.cs
  67. 2
      src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpOptions.cs
  68. 5
      src/OpenIddict.Validation/OpenIddictValidationEvents.Protection.cs
  69. 13
      src/OpenIddict.Validation/OpenIddictValidationEvents.cs
  70. 1
      src/OpenIddict.Validation/OpenIddictValidationExtensions.cs
  71. 14
      src/OpenIddict.Validation/OpenIddictValidationHandlerFilters.cs
  72. 5
      src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs
  73. 88
      src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs
  74. 245
      src/OpenIddict.Validation/OpenIddictValidationHandlers.cs
  75. 9
      src/OpenIddict.Validation/OpenIddictValidationService.cs
  76. 6
      src/OpenIddict.Validation/OpenIddictValidationTransaction.cs
  77. 26
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Discovery.cs
  78. 95
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Introspection.cs
  79. 578
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs
  80. 108
      test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs

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

@ -285,6 +285,18 @@ public sealed partial class OpenIddictClientWebIntegrationBuilder
return Set(registration => registration.ClientSecret = secret);
}
/// <summary>
/// Sets the client type (typically, ""public"" or ""confidential"").
/// </summary>
/// <param name=""type"">The client type.</param>
/// <returns>The <see cref=""OpenIddictClientWebIntegrationBuilder.{{ provider.name }}""/> instance.</returns>
public {{ provider.name }} SetClientType(string type)
{
ArgumentException.ThrowIfNullOrEmpty(type);
return Set(registration => registration.ClientType = type);
}
/// <summary>
/// Sets the post-logout redirection URI, if applicable.
/// </summary>

4
sandbox/OpenIddict.Sandbox.AspNetCore.Client/Controllers/HomeController.cs

@ -39,9 +39,9 @@ public class HomeController : Controller
// authentication options shouldn't be used, a specific scheme can be specified here.
var token = await HttpContext.GetTokenAsync(Tokens.BackchannelAccessToken);
using var client = _httpClientFactory.CreateClient();
using var client = _httpClientFactory.CreateClient("ApiClient");
using var request = new HttpRequestMessage(HttpMethod.Get, "https://localhost:44395/api/message");
using var request = new HttpRequestMessage(HttpMethod.Get, "api/message");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
using var response = await client.SendAsync(request, cancellationToken);

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

@ -146,7 +146,7 @@ public class Startup
// depending on the server configuration (and the client authentication methods explicitly
// configured via OpenIddictClientRegistration.ClientAuthenticationMethods, if applicable).
//
// GetPublicKeyInfrastructureCertificate(),
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
@ -155,7 +155,7 @@ public class Startup
// GetSelfSignedCertificate(),
// Note: this key can only be used with private_key_jwt as raw keys cannot be used with TLS.
GetSigningKey()
// GetSigningKey()
},
#else
ClientSecret = "emCimpdc9SeOaZzN5jzm4_eek-STF6VenfVlKO1_qt0"
@ -190,187 +190,24 @@ public class Startup
.SetRedirectUri("callback/login/reddit")
.SetDuration(OpenIddictClientWebIntegrationConstants.Reddit.Durations.Permanent);
});
});
// Register a named HTTP client that will be used to call the demo resource API.
//
// Note: since the authorization server is configured to issue certificate-bound
// access tokens, the client certificate MUST be attached to outgoing HTTP requests
// and the mTLS subdomain (for which TLS client authentication is enabled) MUST be used.
services.AddHttpClient("ApiClient")
#if SUPPORTS_PEM_ENCODED_KEY_IMPORT
#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($"""
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIMGxf/eMzKuW2F8KKWPJo3bwlrO68rK5+xCeO1atwja2oAoGCCqGSM49
AwEHoUQDQgAEI23kaVsRRAWIez/pqEZOByJFmlXda6iSQ4QqcH23Ir8aYPPX5lsV
nBsExNsl7SOYOiIhgTaX6+PTS7yxTnmvSw==
-----END EC PRIVATE KEY-----
""");
var key = new ECDsaSecurityKey(algorithm);
return new SigningCredentials(key, SecurityAlgorithms.EcdsaSha256, SecurityAlgorithms.Sha256);
}
#pragma warning restore CS8321
#endif
.ConfigureHttpClient(static client => client.BaseAddress = new Uri("https://mtls.dev.localhost:44395/"))
.ConfigurePrimaryHttpMessageHandler(static () => new HttpClientHandler
{
ClientCertificateOptions = ClientCertificateOption.Manual,
ClientCertificates = { GetPublicKeyInfrastructureCertificate().Certificate }
});
services.AddHttpClient();
#else
.ConfigureHttpClient(static client => client.BaseAddress = new Uri("https://localhost:44395/"));
#endif
services.AddMvc();
@ -406,4 +243,182 @@ public class Startup
app.UseMvcWithDefaultRoute();
#endif
}
#if SUPPORTS_PEM_ENCODED_KEY_IMPORT
#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($"""
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIMGxf/eMzKuW2F8KKWPJo3bwlrO68rK5+xCeO1atwja2oAoGCCqGSM49
AwEHoUQDQgAEI23kaVsRRAWIez/pqEZOByJFmlXda6iSQ4QqcH23Ir8aYPPX5lsV
nBsExNsl7SOYOiIhgTaX6+PTS7yxTnmvSw==
-----END EC PRIVATE KEY-----
""");
var key = new ECDsaSecurityKey(algorithm);
return new SigningCredentials(key, SecurityAlgorithms.EcdsaSha256, SecurityAlgorithms.Sha256);
}
#pragma warning restore CS8321
#endif
}

26
sandbox/OpenIddict.Sandbox.AspNetCore.Server/Startup.cs

@ -168,7 +168,7 @@ public class Startup
// to authenticate using either PKI certificates or self-signed certificates.
//
// Note: PKI and self-signed certificate authentication can be enabled independently.
options.EnablePublicKeyInfrastructureClientCertificateAuthentication(
options.EnablePublicKeyInfrastructureTlsClientAuthentication(
[
// Root certificate:
X509Certificate2.CreateFromPem($"""
@ -239,12 +239,11 @@ public class Startup
""")
]);
options.EnableSelfSignedClientCertificateAuthentication();
options.EnableSelfSignedTlsClientAuthentication();
// 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).
// Note: setting a static issuer is mandatory when using mTLS aliases to ensure it 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
@ -260,7 +259,20 @@ public class Startup
.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");
.SetMtlsTokenEndpointAliasUri("https://mtls.dev.localhost:44395/connect/token")
.SetMtlsUserInfoEndpointAliasUri("https://mtls.dev.localhost:44395/connect/userinfo");
// While public client applications cannot use mTLS for client authentication, they can use
// mTLS purely as a token binding mechanism: in this case, the refresh tokens issued to
// public clients sending a client certificate are automatically bound to the certificate,
// which requires sending the same certificate when using them to get new access tokens.
options.UseClientCertificateBoundRefreshTokens();
// Optionally, the server stack can be configured to issue client certificate-bound access tokens.
//
// When doing so, the standard "cnf" claim is automatically added to access tokens to inform
// resource servers that a proof of possession derived from the certificate must be provided.
options.UseClientCertificateBoundAccessTokens();
#endif
})

86
sandbox/OpenIddict.Sandbox.Console.Client/InteractiveService.cs

@ -1,4 +1,7 @@
using System.Security.Claims;
using System.Runtime.InteropServices;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.Hosting;
using OpenIddict.Abstractions;
using OpenIddict.Client;
@ -148,6 +151,19 @@ public class InteractiveService : BackgroundService
var type = await GetSelectedGrantTypeAsync(registration, configuration, stoppingToken);
if (type is GrantTypes.DeviceCode)
{
// Note: the OpenIddict server stack supports mTLS-based token binding for public clients:
// while these clients cannot authenticate using a TLS client certificate, the certificate
// can be used to bind the refresh (and access) tokens returned by the authorization server
// to the client application, which prevents such tokens from being used without providing a
// proof-of-possession matching the TLS client certificate used when the token was acquired.
//
// While this sample deliberately doesn't store the generated certificate in a persistent
// location, the certificate used for token binding should typically be stored in the user
// certificate store to be reloaded across application restarts in a real-world application.
var certificate = configuration.TlsClientCertificateBoundAccessTokens is true
? GenerateEphemeralTlsClientCertificate()
: null;
// Ask OpenIddict to send a device authorization request and write
// the complete verification endpoint URI to the console output.
var result = await _service.ChallengeUsingDeviceAsync(new()
@ -181,7 +197,8 @@ public class InteractiveService : BackgroundService
DeviceCode = result.DeviceCode,
Interval = result.Interval,
ProviderName = provider,
Timeout = result.ExpiresIn < TimeSpan.FromMinutes(5) ? result.ExpiresIn : TimeSpan.FromMinutes(5)
Timeout = result.ExpiresIn < TimeSpan.FromMinutes(5) ? result.ExpiresIn : TimeSpan.FromMinutes(5),
TokenBindingCertificate = certificate
});
AnsiConsole.MarkupLine("[green]Device authentication successful:[/]");
@ -223,7 +240,8 @@ public class InteractiveService : BackgroundService
{
CancellationToken = stoppingToken,
ProviderName = provider,
RefreshToken = response.RefreshToken
RefreshToken = response.RefreshToken,
TokenBindingCertificate = certificate
})).Principal));
}
}
@ -232,6 +250,10 @@ public class InteractiveService : BackgroundService
{
var (username, password) = (await GetUsernameAsync(stoppingToken), await GetPasswordAsync(stoppingToken));
var certificate = configuration.TlsClientCertificateBoundAccessTokens is true
? GenerateEphemeralTlsClientCertificate()
: null;
AnsiConsole.MarkupLine("[cyan]Sending the token request.[/]");
// Ask OpenIddict to authenticate the user using the resource owner password credentials grant.
@ -241,7 +263,8 @@ public class InteractiveService : BackgroundService
ProviderName = provider,
Username = username,
Password = password,
Scopes = [Scopes.OfflineAccess]
Scopes = [Scopes.OfflineAccess],
TokenBindingCertificate = certificate
});
AnsiConsole.MarkupLine("[green]Resource owner password credentials authentication successful:[/]");
@ -283,7 +306,8 @@ public class InteractiveService : BackgroundService
{
CancellationToken = stoppingToken,
ProviderName = provider,
RefreshToken = response.RefreshToken
RefreshToken = response.RefreshToken,
TokenBindingCertificate = certificate
})).Principal));
}
}
@ -309,6 +333,10 @@ public class InteractiveService : BackgroundService
await GetSubjectTokenAsync(stoppingToken),
await GetActorTokenAsync(stoppingToken));
var certificate = configuration.TlsClientCertificateBoundAccessTokens is true
? GenerateEphemeralTlsClientCertificate()
: null;
AnsiConsole.MarkupLine("[cyan]Sending the token request.[/]");
// Ask OpenIddict to send the specified subject token (and actor token, if available).
@ -320,7 +348,8 @@ public class InteractiveService : BackgroundService
ProviderName = provider,
RequestedTokenType = identifier,
SubjectToken = subject.Token,
SubjectTokenType = subject.TokenType
SubjectTokenType = subject.TokenType,
TokenBindingCertificate = certificate
});
AnsiConsole.MarkupLine("[green]Token exchange authentication successful:[/]");
@ -368,7 +397,8 @@ public class InteractiveService : BackgroundService
{
CancellationToken = stoppingToken,
ProviderName = provider,
RefreshToken = response.IssuedToken
RefreshToken = response.IssuedToken,
TokenBindingCertificate = certificate
})).Principal));
}
@ -381,7 +411,8 @@ public class InteractiveService : BackgroundService
{
CancellationToken = stoppingToken,
ProviderName = provider,
RefreshToken = response.RefreshToken
RefreshToken = response.RefreshToken,
TokenBindingCertificate = certificate
})).Principal));
}
}
@ -800,5 +831,44 @@ public class InteractiveService : BackgroundService
return Task.Run(Prompt, cancellationToken).WaitAsync(cancellationToken);
}
#if SUPPORTS_CERTIFICATE_GENERATION
static X509Certificate2 GenerateEphemeralTlsClientCertificate()
{
using var algorithm = RSA.Create(keySizeInBits: 4096);
var subject = new X500DistinguishedName("CN=Self-signed certificate");
var request = new CertificateRequest(subject, algorithm, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
request.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature, critical: true));
request.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension([new Oid("1.3.6.1.5.5.7.3.2")], critical: true));
var certificate = request.CreateSelfSigned(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddYears(2));
// 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 (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
#if SUPPORTS_CERTIFICATE_LOADER
certificate = X509CertificateLoader.LoadPkcs12(
data: certificate.Export(X509ContentType.Pfx, string.Empty),
password: string.Empty,
keyStorageFlags: X509KeyStorageFlags.DefaultKeySet);
#else
certificate = new X509Certificate2(
rawData: certificate.Export(X509ContentType.Pfx, string.Empty),
password: string.Empty,
keyStorageFlags: X509KeyStorageFlags.DefaultKeySet);
#endif
}
return certificate;
}
#endif
}
}

16
shared/OpenIddict.Extensions/OpenIddictHelpers.cs

@ -1104,6 +1104,22 @@ internal static class OpenIddictHelpers
return certificate.SubjectName.RawData.AsSpan().SequenceEqual(certificate.IssuerName.RawData);
}
/// <summary>
/// Determines whether the specified <paramref name="certificate"/> is suitable for client authentication.
/// </summary>
/// <param name="certificate">The <see cref="X509Certificate2"/>.</param>
/// <returns>
/// <see langword="true"/> if the certificate is suitable for client authentication, <see langword="false"/> otherwise.
/// </returns>
public static bool IsClientAuthenticationCertificate(X509Certificate2 certificate)
{
ArgumentNullException.ThrowIfNull(certificate);
return certificate.Version is >= 3 &&
OpenIddictHelpers.HasKeyUsage(certificate, X509KeyUsageFlags.DigitalSignature) &&
OpenIddictHelpers.HasExtendedKeyUsage(certificate, ObjectIdentifiers.ExtendedKeyUsages.ClientAuthentication);
}
/// <summary>
/// Determines whether the items contained in <paramref name="element"/>
/// are of the specified <paramref name="kind"/>.

1
shared/OpenIddict.Extensions/OpenIddictPolyfills.cs

@ -8,7 +8,6 @@ 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;

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

@ -174,19 +174,6 @@ 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>
@ -324,6 +311,19 @@ public interface IOpenIddictApplicationManager
/// </returns>
ValueTask<ImmutableDictionary<string, JsonElement>> GetPropertiesAsync(object application, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves the PKI client certificate authentication 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 PKI client certificate authentication policy enforced for this application.
/// </returns>
ValueTask<X509ChainPolicy?> GetPublicKeyInfrastructureTlsClientAuthenticationPolicyAsync(
object application, X509ChainPolicy policy, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves the redirect URIs associated with an application.
/// </summary>
@ -347,16 +347,16 @@ public interface IOpenIddictApplicationManager
ValueTask<ImmutableArray<string>> GetRequirementsAsync(object application, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves the self-signed client certificate chain policy enforced for this application.
/// Retrieves the self-signed client certificate authentication 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.
/// result returns the self-signed client certificate authentication policy enforced for this application.
/// </returns>
ValueTask<X509ChainPolicy?> GetSelfSignedClientCertificateChainPolicyAsync(
ValueTask<X509ChainPolicy?> GetSelfSignedTlsClientAuthenticationPolicyAsync(
object application, X509ChainPolicy policy, CancellationToken cancellationToken = default);
/// <summary>
@ -504,21 +504,6 @@ 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>
@ -554,6 +539,21 @@ public interface IOpenIddictApplicationManager
ValueTask<bool> ValidatePostLogoutRedirectUriAsync(object application,
[StringSyntax(StringSyntaxAttribute.Uri)] string uri, CancellationToken cancellationToken = default);
/// <summary>
/// Validates the PKI client certificate to ensure it can be used by the specified 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> ValidatePublicKeyInfrastructureTlsClientCertificateAsync(object application,
X509Certificate2 certificate, X509ChainPolicy policy, CancellationToken cancellationToken = default);
/// <summary>
/// Validates the redirect_uri to ensure it's associated with an application.
/// </summary>
@ -568,7 +568,7 @@ public interface IOpenIddictApplicationManager
[StringSyntax(StringSyntaxAttribute.Uri)] string uri, CancellationToken cancellationToken = default);
/// <summary>
/// Validates the self-signed client certificate associated with an application.
/// Validates the self-signed client certificate to ensure it can be used by the specified 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>
@ -579,7 +579,7 @@ public interface IOpenIddictApplicationManager
/// 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(
ValueTask<bool> ValidateSelfSignedTlsClientCertificateAsync(
object application, X509Certificate2 certificate,
X509ChainPolicy policy, CancellationToken cancellationToken = default);
}

1
src/OpenIddict.Abstractions/OpenIddictConstants.cs

@ -76,6 +76,7 @@ public static class OpenIddictConstants
public const string Birthdate = "birthdate";
public const string ClientId = "client_id";
public const string CodeHash = "c_hash";
public const string Confirmation = "cnf";
public const string Country = "country";
public const string Email = "email";
public const string EmailVerified = "email_verified";

66
src/OpenIddict.Abstractions/OpenIddictResources.resx

@ -546,7 +546,7 @@ Reference the 'OpenIddict.Validation.SystemNetHttp' package and call 'services.A
<value>The client identifier cannot be null or empty when using introspection.</value>
</data>
<data name="ID0132" xml:space="preserve">
<value>The client secret cannot be null or empty when using introspection. Alternatively, one or multiple signing credentials can be registered and used to produce client assertions if the authorization server supports this client authentication method.</value>
<value>The client secret cannot be null or empty when using introspection. Alternatively, one or multiple signing credentials can be registered and used as TLS client certificates or to produce client assertions if the authorization server supports it.</value>
</data>
<data name="ID0133" xml:space="preserve">
<value>Authorization entry validation cannot be enabled when using introspection.</value>
@ -1813,12 +1813,12 @@ Alternatively, any value respecting the '[region]-[subregion]-[identifier]' patt
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>
<value>End certificates are not allowed in the Public Key Infrastructure client certificate chain base policies attached to the server options.
To attach an end certificate to a specific client, override the 'OpenIddictApplicationManager.GetPublicKeyInfrastructureTlsClientAuthenticationPolicyAsync()' 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>
To attach a self-signed certificate to a specific client, override the 'OpenIddictApplicationManager.GetSelfSignedTlsClientAuthenticationPolicyAsync()' method.</value>
</data>
<data name="ID0503" xml:space="preserve">
<value>Public Key Infrastructure-based client authentication cannot be used with self-signed certificates.</value>
@ -1827,23 +1827,23 @@ To attach a self-signed certificate to a specific client, override the 'OpenIddi
<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>
<value>A Public Key Infrastructure certificate chain policy must be configured when enabling the 'tls_client_auth' authentication method.
To configure a policy, use 'services.AddOpenIddict().AddServer().EnablePublicKeyInfrastructureTlsClientAuthentication()'.</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>
To configure a policy, use 'services.AddOpenIddict().AddServer().EnableSelfSignedTlsClientAuthentication()'.</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>
While not recommended, TLS client authentication can be manually implemented on unsupported platforms by setting 'OpenIddictServerOptions.PublicKeyInfrastructureTlsClientAuthenticationPolicy'/'OpenIddictServerOptions.SelfSignedTlsClientAuthenticationPolicy' and overriding the the 'OpenIddictApplicationManager.ValidatePublicKeyInfrastructureTlsClientCertificateAsync()'/'OpenIddictApplicationManager.ValidateSelfSignedTlsClientCertificateAsync()' 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>
<value>Changing the trust mode of the X.509 chain policy used for TLS client authentication is not allowed by default for security reasons.
To use a custom policy relying on the system store, set 'OpenIddictServerOptions.PublicKeyInfrastructureTlsClientAuthenticationPolicy' or 'OpenIddictServerOptions.SelfSignedTlsClientAuthenticationPolicy' 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>
@ -1851,6 +1851,15 @@ To use a custom policy relying on the system store, set 'OpenIddictServerOptions
<data name="ID0511" xml:space="preserve">
<value>Public Key Infrastructure certificates cannot contain private keys.</value>
</data>
<data name="ID0512" xml:space="preserve">
<value>A certificate-based client authentication or token binding method was negotiated but no suitable certificate could be found.</value>
</data>
<data name="ID0513" xml:space="preserve">
<value>The type of the specified certificate doesn't match the negotiated client authentication or token binding method.</value>
</data>
<data name="ID0514" xml:space="preserve">
<value>TLS client certificates must contain a private key.</value>
</data>
<data name="ID2000" xml:space="preserve">
<value>The security token is missing.</value>
</data>
@ -2437,7 +2446,7 @@ To use a custom policy relying on the system store, set 'OpenIddictServerOptions
<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>
<value>The specified token binding method is invalid or not supported.</value>
</data>
<data name="ID2197" xml:space="preserve">
<value>The specified TLS client certificate is invalid, expired or has been revoked.</value>
@ -2445,6 +2454,24 @@ To use a custom policy relying on the system store, set 'OpenIddictServerOptions
<data name="ID2198" xml:space="preserve">
<value>Client authentication is required for this application.</value>
</data>
<data name="ID2199" xml:space="preserve">
<value>The confirmation claim resolved from the security principal is malformed or invalid.</value>
</data>
<data name="ID2200" xml:space="preserve">
<value>The thumbprint of the client certificate couldn't be resolved from the confirmation claim.</value>
</data>
<data name="ID2201" xml:space="preserve">
<value>An existing '{0}' instance is already attached to the execution context.</value>
</data>
<data name="ID2202" xml:space="preserve">
<value>The '{0}' attached to the execution context could not be resolved.</value>
</data>
<data name="ID2203" xml:space="preserve">
<value>A certificate-based proof-of-possession is required to use this token.</value>
</data>
<data name="ID2204" xml:space="preserve">
<value>The specified certificate-based proof-of-possession is not valid.</value>
</data>
<data name="ID4000" xml:space="preserve">
<value>The '{0}' parameter shouldn't be null or empty at this point.</value>
</data>
@ -3256,7 +3283,7 @@ This may indicate that the hashed entry is corrupted or malformed.</value>
<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>
<value>The token was rejected because the proof of possession was missing.</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>
@ -3265,17 +3292,26 @@ This may indicate that the hashed entry is corrupted or malformed.</value>
<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>
<value>Certificate validation 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>
<value>Certificate validation failed for {ClientId} because the provided 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>
<value>Certificate validation failed for {ClientId} because the provided 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="ID6289" xml:space="preserve">
<value>The token was rejected because the proof of possession was malformed or invalid.</value>
</data>
<data name="ID6290" xml:space="preserve">
<value>The revocation request was successfully sent to {Uri}: {Request}.</value>
</data>
<data name="ID6291" xml:space="preserve">
<value>The revocation response returned by {Uri} was successfully extracted: {Response}.</value>
</data>
<data name="ID8000" xml:space="preserve">
<value>https://documentation.openiddict.com/errors/{0}</value>
</data>

1
src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlers.Authentication.cs

@ -5,7 +5,6 @@
*/
using System.Collections.Immutable;
using Microsoft.Extensions.Options;
using Owin;
namespace OpenIddict.Client.Owin;

1
src/OpenIddict.Client.Owin/OpenIddictClientOwinHelpers.cs

@ -4,7 +4,6 @@
* the license and the contributors participating to this project.
*/
using Microsoft.Extensions.Options;
using OpenIddict.Client;
using OpenIddict.Client.Owin;

56
src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationMarshal.cs

@ -19,14 +19,19 @@ public sealed class OpenIddictClientSystemIntegrationMarshal
private readonly ConcurrentDictionary<string, Lazy<(
string RequestForgeryProtection,
SemaphoreSlim Semaphore,
TaskCompletionSource<ProcessAuthenticationContext> TaskCompletionSource)>> _operations = new();
TaskCompletionSource<ProcessAuthenticationContext> TaskCompletionSource)>> _tracker = new();
/// <summary>
/// Determines whether the authentication demand corresponding to the specified nonce is tracked.
/// </summary>
/// <param name="nonce">The nonce, used as a unique identifier.</param>
/// <returns><see langword="true"/> if the operation is tracked, <see langword="false"/> otherwise.</returns>
internal bool IsTracked(string nonce) => _operations.ContainsKey(nonce);
internal bool IsTracked(string nonce)
{
ArgumentException.ThrowIfNullOrEmpty(nonce);
return _tracker.ContainsKey(nonce);
}
/// <summary>
/// Tries to add the specified authentication demand to the list of tracked operations.
@ -34,10 +39,16 @@ public sealed class OpenIddictClientSystemIntegrationMarshal
/// <param name="nonce">The nonce, used as a unique identifier.</param>
/// <param name="protection">The request forgery protection associated with the specified authentication demand.</param>
/// <returns><see langword="true"/> if the operation could be added, <see langword="false"/> otherwise.</returns>
internal bool TryAdd(string nonce, string protection) => _operations.TryAdd(nonce, new(() => (
RequestForgeryProtection: protection,
Semaphore: new SemaphoreSlim(initialCount: 1, maxCount: 1),
TaskCompletionSource: new(TaskCreationOptions.RunContinuationsAsynchronously))));
internal bool TryAdd(string nonce, string protection)
{
ArgumentException.ThrowIfNullOrEmpty(nonce);
ArgumentException.ThrowIfNullOrEmpty(protection);
return _tracker.TryAdd(nonce, new(() => (
RequestForgeryProtection: protection,
Semaphore: new SemaphoreSlim(initialCount: 1, maxCount: 1),
TaskCompletionSource: new(TaskCreationOptions.RunContinuationsAsynchronously))));
}
/// <summary>
/// Tries to acquire a lock on the authentication demand corresponding to the specified nonce.
@ -47,8 +58,12 @@ public sealed class OpenIddictClientSystemIntegrationMarshal
/// <returns><see langword="true"/> if the lock could be taken, <see langword="false"/> otherwise.</returns>
/// <exception cref="OperationCanceledException">The operation was canceled by the user.</exception>
internal async Task<bool> TryAcquireLockAsync(string nonce, CancellationToken cancellationToken)
=> _operations.TryGetValue(nonce, out var operation) &&
await operation.Value.Semaphore.WaitAsync(TimeSpan.Zero, cancellationToken);
{
ArgumentException.ThrowIfNullOrEmpty(nonce);
return _tracker.TryGetValue(nonce, out var operation) &&
await operation.Value.Semaphore.WaitAsync(TimeSpan.Zero, cancellationToken);
}
/// <summary>
/// Tries to resolve the authentication context associated with the specified nonce.
@ -58,7 +73,9 @@ public sealed class OpenIddictClientSystemIntegrationMarshal
/// <returns><see langword="true"/> if the context could be resolved, <see langword="false"/> otherwise.</returns>
internal bool TryGetResult(string nonce, [NotNullWhen(true)] out ProcessAuthenticationContext? context)
{
if (!_operations.TryGetValue(nonce, out var operation))
ArgumentException.ThrowIfNullOrEmpty(nonce);
if (!_tracker.TryGetValue(nonce, out var operation))
{
context = null;
return false;
@ -83,7 +100,9 @@ public sealed class OpenIddictClientSystemIntegrationMarshal
/// <exception cref="OperationCanceledException">The operation was canceled by the user.</exception>
internal async Task<bool> TryWaitForCompletionAsync(string nonce, CancellationToken cancellationToken)
{
if (!_operations.TryGetValue(nonce, out var operation))
ArgumentException.ThrowIfNullOrEmpty(nonce);
if (!_tracker.TryGetValue(nonce, out var operation))
{
return false;
}
@ -100,7 +119,9 @@ public sealed class OpenIddictClientSystemIntegrationMarshal
/// <returns><see langword="true"/> if the operation could be validated, <see langword="false"/> otherwise.</returns>
internal bool TryGetRequestForgeryProtection(string nonce, [NotNullWhen(true)] out string? protection)
{
if (_operations.TryGetValue(nonce, out var operation))
ArgumentException.ThrowIfNullOrEmpty(nonce);
if (_tracker.TryGetValue(nonce, out var operation))
{
protection = operation.Value.RequestForgeryProtection;
return true;
@ -117,12 +138,21 @@ public sealed class OpenIddictClientSystemIntegrationMarshal
/// <param name="context">The authentication context that will be returned to the caller.</param>
/// <returns><see langword="true"/> if the operation could be completed, <see langword="false"/> otherwise.</returns>
internal bool TryComplete(string nonce, ProcessAuthenticationContext context)
=> _operations.TryGetValue(nonce, out var operation) && operation.Value.TaskCompletionSource.TrySetResult(context);
{
ArgumentException.ThrowIfNullOrEmpty(nonce);
return _tracker.TryGetValue(nonce, out var operation) && operation.Value.TaskCompletionSource.TrySetResult(context);
}
/// <summary>
/// Tries to remove the specified authentication operation from the list of tracked operations.
/// </summary>
/// <param name="nonce">The nonce, used as a unique identifier.</param>
/// <returns><see langword="true"/> if the operation could be removed, <see langword="false"/> otherwise.</returns>
internal bool TryRemove(string nonce) => _operations.TryRemove(nonce, out _);
internal bool TryRemove(string nonce)
{
ArgumentException.ThrowIfNullOrEmpty(nonce);
return _tracker.TryRemove(nonce, out _);
}
}

2
src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpBuilder.cs

@ -300,6 +300,7 @@ public sealed class OpenIddictClientSystemNetHttpBuilder
/// client authentication key usages to be automatically selected by OpenIddict).
/// </remarks>
/// <returns>The <see cref="OpenIddictClientSystemNetHttpBuilder"/> instance.</returns>
[Obsolete("This option is no longer supported and will be removed in a future version.")]
public OpenIddictClientSystemNetHttpBuilder SetSelfSignedTlsClientAuthenticationCertificateSelector(
Func<OpenIddictClientRegistration, X509Certificate2?> selector)
{
@ -321,6 +322,7 @@ public sealed class OpenIddictClientSystemNetHttpBuilder
/// client authentication key usages to be automatically selected by OpenIddict).
/// </remarks>
/// <returns>The <see cref="OpenIddictClientSystemNetHttpBuilder"/> instance.</returns>
[Obsolete("This option is no longer supported and will be removed in a future version.")]
public OpenIddictClientSystemNetHttpBuilder SetTlsClientAuthenticationCertificateSelector(
Func<OpenIddictClientRegistration, X509Certificate2?> selector)
{

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

@ -11,7 +11,6 @@ using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using Polly;
#if SUPPORTS_HTTP_CLIENT_RESILIENCE
@ -50,6 +49,9 @@ public sealed class OpenIddictClientSystemNetHttpConfiguration : IConfigureOptio
options.ClientAuthenticationMethods.Add(ClientAuthenticationMethods.ClientSecretBasic);
options.ClientAuthenticationMethods.Add(ClientAuthenticationMethods.SelfSignedTlsClientAuth);
options.ClientAuthenticationMethods.Add(ClientAuthenticationMethods.TlsClientAuth);
options.TokenBindingMethods.Add(TokenBindingMethods.Private.SelfSignedTlsClientCertificate);
options.TokenBindingMethods.Add(TokenBindingMethods.Private.TlsClientCertificate);
}
/// <inheritdoc/>
@ -73,26 +75,10 @@ public sealed class OpenIddictClientSystemNetHttpConfiguration : IConfigureOptio
// to dynamically amend the resulting HttpClient or HttpClientHandler instance.
//
// To work around this limitation, the OpenIddict System.Net.Http integration uses
// dynamic client names and supports appending a list of key-value pairs to the client
// name to flow per-instance properties (e.g the negotiated client authentication method).
var properties = name.Length >= assembly.Name!.Length + 1 && name[assembly.Name.Length] is ':' ?
name[(assembly.Name.Length + 1)..]
.Split(['\u001f'], StringSplitOptions.RemoveEmptyEntries)
.Select(static property => property.Split(['\u001e'], StringSplitOptions.RemoveEmptyEntries))
.Where(static values => values is [{ Length: > 0 }, { Length: > 0 }])
.ToDictionary(static values => values[0], static values => values[1]) : [];
if (!properties.TryGetValue("RegistrationId", out string? identifier) || string.IsNullOrEmpty(identifier))
{
return;
}
var service = _provider.GetRequiredService<OpenIddictClientService>();
// Note: while the client registration should be returned synchronously in most cases,
// the retrieval is always offloaded to the thread pool to help prevent deadlocks when
// the waiting is blocking and the operation is executed in a synchronization context.
var registration = Task.Run(async () => await service.GetClientRegistrationByIdAsync(identifier)).GetAwaiter().GetResult();
// an async-local context to flow per-instance properties and uses dynamic client
// names to ensure the inner HttpClientHandler is not reused if the context differs.
var context = OpenIddictClientSystemNetHttpContext.Current ??
throw new InvalidOperationException(SR.FormatID2202(nameof(OpenIddictClientSystemNetHttpContext)));
var settings = _provider.GetRequiredService<IOptionsMonitor<OpenIddictClientSystemNetHttpOptions>>().CurrentValue;
@ -108,7 +94,7 @@ public sealed class OpenIddictClientSystemNetHttpConfiguration : IConfigureOptio
// Register the user-defined HTTP client actions.
foreach (var action in settings.HttpClientActions)
{
options.HttpClientActions.Add(client => action(registration, client));
options.HttpClientActions.Add(client => action(context.Registration, client));
}
options.HttpMessageHandlerBuilderActions.Add(builder =>
@ -139,31 +125,25 @@ public sealed class OpenIddictClientSystemNetHttpConfiguration : IConfigureOptio
handler.ClientCertificateOptions = ClientCertificateOption.Manual;
if (properties.TryGetValue("AttachTlsClientCertificate", out string? value) &&
bool.TryParse(value, out bool result) && result)
if (context.LocalCertificate is X509Certificate2 certificate)
{
var certificate = options.CurrentValue.TlsClientAuthenticationCertificateSelector(registration);
if (certificate is not null)
// If a certificate was specified, immediately throw an excecption if it doesn't have
// a private key attached to ensure it won't be silently discarded when initiating the
// TLS handshake (which would result in a hard-to-debug scenario where the certificate
// would be attached to the HTTP handler but would not be sent to the remote peer).
if (!certificate.HasPrivateKey)
{
handler.ClientCertificates.Add(certificate);
throw new InvalidOperationException(SR.GetResourceString(SR.ID0514));
}
}
else if (properties.TryGetValue("AttachSelfSignedTlsClientCertificate", out value) &&
bool.TryParse(value, out result) && result)
{
var certificate = options.CurrentValue.SelfSignedTlsClientAuthenticationCertificateSelector(registration);
if (certificate is not null)
{
handler.ClientCertificates.Add(certificate);
}
handler.ClientCertificates.Add(certificate);
}
});
// Register the user-defined HTTP client handler actions.
foreach (var action in settings.HttpClientHandlerActions)
{
options.HttpMessageHandlerBuilderActions.Add(builder => action(registration,
options.HttpMessageHandlerBuilderActions.Add(builder => action(context.Registration,
builder.PrimaryHandler as HttpClientHandler ??
throw new InvalidOperationException(SR.FormatID0373(typeof(HttpClientHandler).FullName))));
}
@ -182,25 +162,6 @@ public sealed class OpenIddictClientSystemNetHttpConfiguration : IConfigureOptio
return;
}
// Note: HttpClientFactory doesn't support flowing a list of properties that can be
// accessed from the HttpClientAction or HttpMessageHandlerBuilderAction delegates
// to dynamically amend the resulting HttpClient or HttpClientHandler instance.
//
// To work around this limitation, the OpenIddict System.Net.Http integration uses dynamic
// client names and supports appending a list of key-value pairs to the client name to flow
// per-instance properties (e.g a flag indicating whether a client certificate should be used).
var properties = name.Length >= assembly.Name!.Length + 1 && name[assembly.Name.Length] is ':' ?
name[(assembly.Name.Length + 1)..]
.Split(['\u001f'], StringSplitOptions.RemoveEmptyEntries)
.Select(static property => property.Split(['\u001e'], StringSplitOptions.RemoveEmptyEntries))
.Where(static values => values is [{ Length: > 0 }, { Length: > 0 }])
.ToDictionary(static values => values[0], static values => values[1]) : [];
if (!properties.TryGetValue("RegistrationId", out string? identifier) || string.IsNullOrEmpty(identifier))
{
return;
}
options.HttpMessageHandlerBuilderActions.Insert(0, static builder =>
{
// Note: Microsoft.Extensions.Http 9.0+ no longer uses HttpClientHandler as the default instance
@ -258,48 +219,7 @@ public sealed class OpenIddictClientSystemNetHttpConfiguration : IConfigureOptio
}
/// <inheritdoc/>
[Obsolete("This method is no longer supported and will be removed in a future version.")]
public void PostConfigure(string? name, OpenIddictClientSystemNetHttpOptions options)
{
ArgumentNullException.ThrowIfNull(options);
// If no client authentication certificate selector was provided, use fallback delegates that
// automatically use the first X.509 signing certificate attached to the client registration
// that is suitable for both digital signature and client authentication.
options.SelfSignedTlsClientAuthenticationCertificateSelector ??= static registration =>
{
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 && OpenIddictHelpers.IsSelfIssuedCertificate(certificate) &&
OpenIddictHelpers.HasKeyUsage(certificate, X509KeyUsageFlags.DigitalSignature) &&
OpenIddictHelpers.HasExtendedKeyUsage(certificate, ObjectIdentifiers.ExtendedKeyUsages.ClientAuthentication))
{
return certificate;
}
}
return null;
};
options.TlsClientAuthenticationCertificateSelector ??= static registration =>
{
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 && !OpenIddictHelpers.IsSelfIssuedCertificate(certificate) &&
OpenIddictHelpers.HasKeyUsage(certificate, X509KeyUsageFlags.DigitalSignature) &&
OpenIddictHelpers.HasExtendedKeyUsage(certificate, ObjectIdentifiers.ExtendedKeyUsages.ClientAuthentication))
{
return certificate;
}
}
return null;
};
}
=> throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
}

83
src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpContext.cs

@ -0,0 +1,83 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project.
*/
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using Microsoft.IdentityModel.Tokens;
namespace OpenIddict.Client.SystemNetHttp;
/// <summary>
/// Represents the context used by the System.Net.Http integration when creating a new HTTP client.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed class OpenIddictClientSystemNetHttpContext
{
private static readonly AsyncLocal<OpenIddictClientSystemNetHttpContext?> _current = new();
/// <summary>
/// Gets or sets the X.509 client certificate that will be used to authenticate
/// this peer when communicating with the external endpoint, if applicable.
/// </summary>
public X509Certificate2? LocalCertificate { get; init; }
/// <summary>
/// Gets or sets the ambient context for the current execution flow.
/// </summary>
public static OpenIddictClientSystemNetHttpContext? Current
{
get => _current.Value;
set => _current.Value = value;
}
/// <summary>
/// Gets or sets the client registration associated with the HTTP client being created.
/// </summary>
public required OpenIddictClientRegistration Registration { get; init; }
/// <summary>
/// Computes a stable, unique identifier for the specified context using a cryptographic hash.
/// </summary>
/// <param name="context">The client context for which to compute the stable identifier.</param>
/// <returns>A string representing the stable identifier for the specified context.</returns>
public static string ComputeStableId(OpenIddictClientSystemNetHttpContext context)
{
ArgumentNullException.ThrowIfNull(context);
using var algorithm = CreateAlgorithm();
TransformBlock(algorithm, context.Registration.RegistrationId!);
if (context.LocalCertificate is X509Certificate2 certificate)
{
algorithm.TransformBlock(certificate.RawData, 0, certificate.RawData.Length, outputBuffer: null, outputOffset: 0);
}
algorithm.TransformFinalBlock([], 0, 0);
return Base64UrlEncoder.Encode(algorithm.Hash);
[UnconditionalSuppressMessage("Trimming", "IL2026",
Justification = "The default implementation is always used when no custom algorithm was registered.")]
static SHA256 CreateAlgorithm() => CryptoConfig.CreateFromName("OpenIddict SHA-256 Cryptographic Provider") switch
{
SHA256 result => result,
null => SHA256.Create(),
var result => throw new CryptographicException(SR.FormatID0351(result.GetType().FullName))
};
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static void TransformBlock(HashAlgorithm algorithm, string input)
{
var buffer = Encoding.UTF8.GetBytes(input);
algorithm.TransformBlock(buffer, 0, buffer.Length, outputBuffer: null, outputOffset: 0);
}
}
}

3
src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpExtensions.cs

@ -46,9 +46,6 @@ public static class OpenIddictClientSystemNetHttpExtensions
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<
IPostConfigureOptions<HttpClientFactoryOptions>, OpenIddictClientSystemNetHttpConfiguration>());
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<
IPostConfigureOptions<OpenIddictClientSystemNetHttpOptions>, OpenIddictClientSystemNetHttpConfiguration>());
return new OpenIddictClientSystemNetHttpBuilder(builder.Services);
}

654
src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.cs

@ -14,7 +14,6 @@ using System.Text;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using Microsoft.IdentityModel.Tokens;
using static OpenIddict.Client.SystemNetHttp.OpenIddictClientSystemNetHttpConstants;
namespace OpenIddict.Client.SystemNetHttp;
@ -24,28 +23,6 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
{
public static ImmutableArray<OpenIddictClientHandlerDescriptor> DefaultHandlers { get; } =
[
/*
* Authentication processing:
*/
AttachNonDefaultTokenEndpointClientAuthenticationMethod.Descriptor,
AttachNonDefaultUserInfoEndpointTokenBindingMethods.Descriptor,
/*
* Challenge processing:
*/
AttachNonDefaultDeviceAuthorizationEndpointClientAuthenticationMethod.Descriptor,
AttachNonDefaultPushedAuthorizationEndpointClientAuthenticationMethod.Descriptor,
/*
* Introspection processing:
*/
AttachNonDefaultIntrospectionEndpointClientAuthenticationMethod.Descriptor,
/*
* Revocation processing:
*/
AttachNonDefaultRevocationEndpointClientAuthenticationMethod.Descriptor,
.. Authorization.DefaultHandlers,
.. Device.DefaultHandlers,
.. Discovery.DefaultHandlers,
@ -59,14 +36,9 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
/// Contains the logic responsible for negotiating the best token endpoint client
/// authentication method supported by both the client and the authorization server.
/// </summary>
[Obsolete("This class is obsolete and will be removed in a future version.")]
public sealed class AttachNonDefaultTokenEndpointClientAuthenticationMethod : IOpenIddictClientHandler<ProcessAuthenticationContext>
{
private readonly IOptionsMonitor<OpenIddictClientSystemNetHttpOptions> _options;
public AttachNonDefaultTokenEndpointClientAuthenticationMethod(
IOptionsMonitor<OpenIddictClientSystemNetHttpOptions> options)
=> _options = options ?? throw new ArgumentNullException(nameof(options));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
@ -79,113 +51,16 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessAuthenticationContext context)
{
ArgumentNullException.ThrowIfNull(context);
// If an explicit client authentication method was attached, don't overwrite it.
if (!string.IsNullOrEmpty(context.TokenEndpointClientAuthenticationMethod))
{
return ValueTask.CompletedTask;
}
context.TokenEndpointClientAuthenticationMethod = (
// Note: if client authentication methods are explicitly listed in the client registration, only use
// the client authentication methods that are both listed and enabled in the global client options.
// Otherwise, always default to the client authentication methods that have been enabled globally.
Client: context.Registration.ClientAuthenticationMethods.Count switch
{
0 => context.Options.ClientAuthenticationMethods as ICollection<string>,
_ => context.Options.ClientAuthenticationMethods.Intersect(context.Registration.ClientAuthenticationMethods, StringComparer.Ordinal).ToList()
},
Server: context.Configuration.TokenEndpointAuthMethodsSupported) switch
{
// If a TLS client authentication certificate could be resolved and both the
// client and the server explicitly support tls_client_auth, always prefer it.
({ Count: > 0 } client, { Count: > 0 } server) when
client.Contains(ClientAuthenticationMethods.TlsClientAuth) &&
server.Contains(ClientAuthenticationMethods.TlsClientAuth) &&
(context.Configuration.MtlsTokenEndpoint ?? context.Configuration.TokenEndpoint) is Uri endpoint &&
string.Equals(endpoint.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase) &&
_options.CurrentValue.TlsClientAuthenticationCertificateSelector(context.Registration) is not null
=> ClientAuthenticationMethods.TlsClientAuth,
// If a self-signed TLS client authentication certificate could be resolved and both
// the client and the server explicitly support self_signed_tls_client_auth, use it.
({ Count: > 0 } client, { Count: > 0 } server) when
client.Contains(ClientAuthenticationMethods.SelfSignedTlsClientAuth) &&
server.Contains(ClientAuthenticationMethods.SelfSignedTlsClientAuth) &&
(context.Configuration.MtlsTokenEndpoint ?? context.Configuration.TokenEndpoint) is Uri endpoint &&
string.Equals(endpoint.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase) &&
_options.CurrentValue.SelfSignedTlsClientAuthenticationCertificateSelector(context.Registration) is not null
=> ClientAuthenticationMethods.SelfSignedTlsClientAuth,
// If at least one asymmetric signing key was attached to the client registration
// and both the client and the server explicitly support private_key_jwt, use it.
({ Count: > 0 } client, { Count: > 0 } server) when
client.Contains(ClientAuthenticationMethods.PrivateKeyJwt) &&
server.Contains(ClientAuthenticationMethods.PrivateKeyJwt) &&
context.Registration.SigningCredentials.Exists(static credentials => credentials.Key is AsymmetricSecurityKey)
=> ClientAuthenticationMethods.PrivateKeyJwt,
// If a client secret was attached to the client registration and both the client and
// the server explicitly support client_secret_post, prefer it to basic authentication.
({ Count: > 0 } client, { Count: > 0 } server) when !string.IsNullOrEmpty(context.Registration.ClientSecret) &&
client.Contains(ClientAuthenticationMethods.ClientSecretPost) &&
server.Contains(ClientAuthenticationMethods.ClientSecretPost)
=> ClientAuthenticationMethods.ClientSecretPost,
// The OAuth 2.0 specification recommends sending the client credentials using basic authentication.
// However, this authentication method is known to have severe compatibility/interoperability issues:
//
// - While restricted to clients that have been given a secret (i.e confidential clients) by the
// specification, basic authentication is also sometimes required by server implementations for
// public clients that don't have a client secret: in this case, an empty password is used and
// the client identifier is sent alone in the Authorization header (instead of being sent using
// the standard "client_id" parameter present in the request body).
//
// - While the OAuth 2.0 specification requires that the client credentials be formURL-encoded
// before being base64-encoded, many implementations are known to implement a non-standard
// encoding scheme, where neither the client_id nor the client_secret are formURL-encoded.
//
// To guarantee that the OpenIddict implementation can be used with most servers implementions,
// basic authentication is only used when a client secret is present and the server configuration
// doesn't list any supported client authentication method or doesn't support client_secret_post.
//
// If client_secret_post is not listed or if the server returned an empty methods list,
// client_secret_basic is always used, as it MUST be implemented by all OAuth 2.0 servers.
//
// See https://tools.ietf.org/html/rfc8414#section-2
// and https://tools.ietf.org/html/rfc6749#section-2.3.1 for more information.
({ Count: > 0 } client, { Count: > 0 } server) when !string.IsNullOrEmpty(context.Registration.ClientSecret) &&
client.Contains(ClientAuthenticationMethods.ClientSecretBasic) &&
server.Contains(ClientAuthenticationMethods.ClientSecretBasic)
=> ClientAuthenticationMethods.ClientSecretBasic,
({ Count: > 0 } client, { Count: 0 }) when !string.IsNullOrEmpty(context.Registration.ClientSecret) &&
client.Contains(ClientAuthenticationMethods.ClientSecretBasic)
=> ClientAuthenticationMethods.ClientSecretBasic,
_ => null
};
return ValueTask.CompletedTask;
}
public ValueTask HandleAsync(ProcessAuthenticationContext context) => ValueTask.CompletedTask;
}
/// <summary>
/// Contains the logic responsible for negotiating the best token binding
/// methods supported by both the client and the authorization server.
/// </summary>
[Obsolete("This class is obsolete and will be removed in a future version.")]
public sealed class AttachNonDefaultUserInfoEndpointTokenBindingMethods : IOpenIddictClientHandler<ProcessAuthenticationContext>
{
private readonly IOptionsMonitor<OpenIddictClientSystemNetHttpOptions> _options;
public AttachNonDefaultUserInfoEndpointTokenBindingMethods(
IOptionsMonitor<OpenIddictClientSystemNetHttpOptions> options)
=> _options = options ?? throw new ArgumentNullException(nameof(options));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
@ -198,56 +73,16 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessAuthenticationContext context)
{
ArgumentNullException.ThrowIfNull(context);
// Unlike DPoP, the mTLS specification doesn't use a specific token type to represent
// certificate-bound tokens. As such, most implementations (e.g Keycloak) simply return
// the "Bearer" value even if the access token is - by definition - not a bearer token
// and requires using the same X.509 certificate that was used for client authentication.
//
// Since the token type cannot be trusted in this case, OpenIddict assumes that the access
// token used in the userinfo request is certificate-bound if the server configuration
// indicates that the server supports certificate-bound access tokens and if either
// tls_client_auth or self_signed_tls_client_auth was used for the token request.
if (context.Configuration.TlsClientCertificateBoundAccessTokens is not true ||
!context.SendTokenRequest || string.IsNullOrEmpty(context.BackchannelAccessToken) ||
(context.Configuration.MtlsUserInfoEndpoint ?? context.Configuration.UserInfoEndpoint) is not Uri endpoint ||
!string.Equals(endpoint.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
{
return ValueTask.CompletedTask;
}
if (context.TokenEndpointClientAuthenticationMethod is ClientAuthenticationMethods.TlsClientAuth &&
_options.CurrentValue.TlsClientAuthenticationCertificateSelector(context.Registration) is not null)
{
context.UserInfoEndpointTokenBindingMethods.Add(TokenBindingMethods.Private.TlsClientCertificate);
}
else if (context.TokenEndpointClientAuthenticationMethod is ClientAuthenticationMethods.SelfSignedTlsClientAuth &&
_options.CurrentValue.SelfSignedTlsClientAuthenticationCertificateSelector(context.Registration) is not null)
{
context.UserInfoEndpointTokenBindingMethods.Add(TokenBindingMethods.Private.SelfSignedTlsClientCertificate);
}
return ValueTask.CompletedTask;
}
public ValueTask HandleAsync(ProcessAuthenticationContext context) => ValueTask.CompletedTask;
}
/// <summary>
/// Contains the logic responsible for negotiating the best device authorization endpoint
/// client authentication method supported by both the client and the authorization server.
/// </summary>
[Obsolete("This class is obsolete and will be removed in a future version.")]
public sealed class AttachNonDefaultDeviceAuthorizationEndpointClientAuthenticationMethod : IOpenIddictClientHandler<ProcessChallengeContext>
{
private readonly IOptionsMonitor<OpenIddictClientSystemNetHttpOptions> _options;
public AttachNonDefaultDeviceAuthorizationEndpointClientAuthenticationMethod(
IOptionsMonitor<OpenIddictClientSystemNetHttpOptions> options)
=> _options = options ?? throw new ArgumentNullException(nameof(options));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
@ -260,121 +95,16 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessChallengeContext context)
{
ArgumentNullException.ThrowIfNull(context);
// If an explicit client authentication method was attached, don't overwrite it.
if (!string.IsNullOrEmpty(context.DeviceAuthorizationEndpointClientAuthenticationMethod))
{
return ValueTask.CompletedTask;
}
context.DeviceAuthorizationEndpointClientAuthenticationMethod = (
// Note: if client authentication methods are explicitly listed in the client registration, only use
// the client authentication methods that are both listed and enabled in the global client options.
// Otherwise, always default to the client authentication methods that have been enabled globally.
Client: context.Registration.ClientAuthenticationMethods.Count switch
{
0 => context.Options.ClientAuthenticationMethods as ICollection<string>,
_ => context.Options.ClientAuthenticationMethods.Intersect(context.Registration.ClientAuthenticationMethods, StringComparer.Ordinal).ToList()
},
// Note: if the authorization server doesn't support the OpenIddict-specific
// "device_authorization_request_endpoint_auth_methods_supported" node,
// fall back to the "token_endpoint_auth_methods_supported" node,
// which is the same logic as for the pushed authorization endpoint.
Server: context.Configuration.DeviceAuthorizationEndpointAuthMethodsSupported.Count switch
{
0 => context.Configuration.TokenEndpointAuthMethodsSupported,
_ => context.Configuration.DeviceAuthorizationEndpointAuthMethodsSupported,
}) switch
{
// If a TLS client authentication certificate could be resolved and both the
// client and the server explicitly support tls_client_auth, always prefer it.
({ Count: > 0 } client, { Count: > 0 } server) when
client.Contains(ClientAuthenticationMethods.TlsClientAuth) &&
server.Contains(ClientAuthenticationMethods.TlsClientAuth) &&
(context.Configuration.MtlsDeviceAuthorizationEndpoint ?? context.Configuration.DeviceAuthorizationEndpoint) is Uri endpoint &&
string.Equals(endpoint.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase) &&
_options.CurrentValue.TlsClientAuthenticationCertificateSelector(context.Registration) is not null
=> ClientAuthenticationMethods.TlsClientAuth,
// If a self-signed TLS client authentication certificate could be resolved and both
// the client and the server explicitly support self_signed_tls_client_auth, use it.
({ Count: > 0 } client, { Count: > 0 } server) when
client.Contains(ClientAuthenticationMethods.SelfSignedTlsClientAuth) &&
server.Contains(ClientAuthenticationMethods.SelfSignedTlsClientAuth) &&
(context.Configuration.MtlsDeviceAuthorizationEndpoint ?? context.Configuration.DeviceAuthorizationEndpoint) is Uri endpoint &&
string.Equals(endpoint.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase) &&
_options.CurrentValue.SelfSignedTlsClientAuthenticationCertificateSelector(context.Registration) is not null
=> ClientAuthenticationMethods.SelfSignedTlsClientAuth,
// If at least one asymmetric signing key was attached to the client registration
// and both the client and the server explicitly support private_key_jwt, use it.
({ Count: > 0 } client, { Count: > 0 } server) when
client.Contains(ClientAuthenticationMethods.PrivateKeyJwt) &&
server.Contains(ClientAuthenticationMethods.PrivateKeyJwt) &&
context.Registration.SigningCredentials.Exists(static credentials => credentials.Key is AsymmetricSecurityKey)
=> ClientAuthenticationMethods.PrivateKeyJwt,
// If a client secret was attached to the client registration and both the client and
// the server explicitly support client_secret_post, prefer it to basic authentication.
({ Count: > 0 } client, { Count: > 0 } server) when !string.IsNullOrEmpty(context.Registration.ClientSecret) &&
client.Contains(ClientAuthenticationMethods.ClientSecretPost) &&
server.Contains(ClientAuthenticationMethods.ClientSecretPost)
=> ClientAuthenticationMethods.ClientSecretPost,
// The OAuth 2.0 specification recommends sending the client credentials using basic authentication.
// However, this authentication method is known to have severe compatibility/interoperability issues:
//
// - While restricted to clients that have been given a secret (i.e confidential clients) by the
// specification, basic authentication is also sometimes required by server implementations for
// public clients that don't have a client secret: in this case, an empty password is used and
// the client identifier is sent alone in the Authorization header (instead of being sent using
// the standard "client_id" parameter present in the request body).
//
// - While the OAuth 2.0 specification requires that the client credentials be formURL-encoded
// before being base64-encoded, many implementations are known to implement a non-standard
// encoding scheme, where neither the client_id nor the client_secret are formURL-encoded.
//
// To guarantee that the OpenIddict implementation can be used with most servers implementions,
// basic authentication is only used when a client secret is present and the server configuration
// doesn't list any supported client authentication method or doesn't support client_secret_post.
//
// If client_secret_post is not listed or if the server returned an empty methods list,
// client_secret_basic is always used, as it MUST be implemented by all OAuth 2.0 servers.
//
// See https://tools.ietf.org/html/rfc8414#section-2
// and https://tools.ietf.org/html/rfc6749#section-2.3.1 for more information.
({ Count: > 0 } client, { Count: > 0 } server) when !string.IsNullOrEmpty(context.Registration.ClientSecret) &&
client.Contains(ClientAuthenticationMethods.ClientSecretBasic) &&
server.Contains(ClientAuthenticationMethods.ClientSecretBasic)
=> ClientAuthenticationMethods.ClientSecretBasic,
({ Count: > 0 } client, { Count: 0 }) when !string.IsNullOrEmpty(context.Registration.ClientSecret) &&
client.Contains(ClientAuthenticationMethods.ClientSecretBasic)
=> ClientAuthenticationMethods.ClientSecretBasic,
_ => null
};
return ValueTask.CompletedTask;
}
public ValueTask HandleAsync(ProcessChallengeContext context) => ValueTask.CompletedTask;
}
/// <summary>
/// Contains the logic responsible for negotiating the best pushed authorization endpoint
/// client authentication method supported by both the client and the authorization server.
/// </summary>
[Obsolete("This class is obsolete and will be removed in a future version.")]
public sealed class AttachNonDefaultPushedAuthorizationEndpointClientAuthenticationMethod : IOpenIddictClientHandler<ProcessChallengeContext>
{
private readonly IOptionsMonitor<OpenIddictClientSystemNetHttpOptions> _options;
public AttachNonDefaultPushedAuthorizationEndpointClientAuthenticationMethod(
IOptionsMonitor<OpenIddictClientSystemNetHttpOptions> options)
=> _options = options ?? throw new ArgumentNullException(nameof(options));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
@ -387,122 +117,16 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessChallengeContext context)
{
ArgumentNullException.ThrowIfNull(context);
// If an explicit client authentication method was attached, don't overwrite it.
if (!string.IsNullOrEmpty(context.PushedAuthorizationEndpointClientAuthenticationMethod))
{
return ValueTask.CompletedTask;
}
context.PushedAuthorizationEndpointClientAuthenticationMethod = (
// Note: if client authentication methods are explicitly listed in the client registration, only use
// the client authentication methods that are both listed and enabled in the global client options.
// Otherwise, always default to the client authentication methods that have been enabled globally.
Client: context.Registration.ClientAuthenticationMethods.Count switch
{
0 => context.Options.ClientAuthenticationMethods as ICollection<string>,
_ => context.Options.ClientAuthenticationMethods.Intersect(context.Registration.ClientAuthenticationMethods, StringComparer.Ordinal).ToList()
},
// Note: if the authorization server doesn't support the OpenIddict-specific
// "pushed_authorization_request_endpoint_auth_methods_supported" node, fall back to
// the "token_endpoint_auth_methods_supported" node, as required by the specification.
//
// See https://datatracker.ietf.org/doc/html/rfc9126#section-2 for more information.
Server: context.Configuration.PushedAuthorizationEndpointAuthMethodsSupported.Count switch
{
0 => context.Configuration.TokenEndpointAuthMethodsSupported,
_ => context.Configuration.PushedAuthorizationEndpointAuthMethodsSupported,
}) switch
{
// If a TLS client authentication certificate could be resolved and both the
// client and the server explicitly support tls_client_auth, always prefer it.
({ Count: > 0 } client, { Count: > 0 } server) when
client.Contains(ClientAuthenticationMethods.TlsClientAuth) &&
server.Contains(ClientAuthenticationMethods.TlsClientAuth) &&
(context.Configuration.MtlsPushedAuthorizationEndpoint ?? context.Configuration.PushedAuthorizationEndpoint) is Uri endpoint &&
string.Equals(endpoint.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase) &&
_options.CurrentValue.TlsClientAuthenticationCertificateSelector(context.Registration) is not null
=> ClientAuthenticationMethods.TlsClientAuth,
// If a self-signed TLS client authentication certificate could be resolved and both
// the client and the server explicitly support self_signed_tls_client_auth, use it.
({ Count: > 0 } client, { Count: > 0 } server) when
client.Contains(ClientAuthenticationMethods.SelfSignedTlsClientAuth) &&
server.Contains(ClientAuthenticationMethods.SelfSignedTlsClientAuth) &&
(context.Configuration.MtlsPushedAuthorizationEndpoint ?? context.Configuration.PushedAuthorizationEndpoint) is Uri endpoint &&
string.Equals(endpoint.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase) &&
_options.CurrentValue.SelfSignedTlsClientAuthenticationCertificateSelector(context.Registration) is not null
=> ClientAuthenticationMethods.SelfSignedTlsClientAuth,
// If at least one asymmetric signing key was attached to the client registration
// and both the client and the server explicitly support private_key_jwt, use it.
({ Count: > 0 } client, { Count: > 0 } server) when
client.Contains(ClientAuthenticationMethods.PrivateKeyJwt) &&
server.Contains(ClientAuthenticationMethods.PrivateKeyJwt) &&
context.Registration.SigningCredentials.Exists(static credentials => credentials.Key is AsymmetricSecurityKey)
=> ClientAuthenticationMethods.PrivateKeyJwt,
// If a client secret was attached to the client registration and both the client and
// the server explicitly support client_secret_post, prefer it to basic authentication.
({ Count: > 0 } client, { Count: > 0 } server) when !string.IsNullOrEmpty(context.Registration.ClientSecret) &&
client.Contains(ClientAuthenticationMethods.ClientSecretPost) &&
server.Contains(ClientAuthenticationMethods.ClientSecretPost)
=> ClientAuthenticationMethods.ClientSecretPost,
// The OAuth 2.0 specification recommends sending the client credentials using basic authentication.
// However, this authentication method is known to have severe compatibility/interoperability issues:
//
// - While restricted to clients that have been given a secret (i.e confidential clients) by the
// specification, basic authentication is also sometimes required by server implementations for
// public clients that don't have a client secret: in this case, an empty password is used and
// the client identifier is sent alone in the Authorization header (instead of being sent using
// the standard "client_id" parameter present in the request body).
//
// - While the OAuth 2.0 specification requires that the client credentials be formURL-encoded
// before being base64-encoded, many implementations are known to implement a non-standard
// encoding scheme, where neither the client_id nor the client_secret are formURL-encoded.
//
// To guarantee that the OpenIddict implementation can be used with most servers implementions,
// basic authentication is only used when a client secret is present and the server configuration
// doesn't list any supported client authentication method or doesn't support client_secret_post.
//
// If client_secret_post is not listed or if the server returned an empty methods list,
// client_secret_basic is always used, as it MUST be implemented by all OAuth 2.0 servers.
//
// See https://tools.ietf.org/html/rfc8414#section-2
// and https://tools.ietf.org/html/rfc6749#section-2.3.1 for more information.
({ Count: > 0 } client, { Count: > 0 } server) when !string.IsNullOrEmpty(context.Registration.ClientSecret) &&
client.Contains(ClientAuthenticationMethods.ClientSecretBasic) &&
server.Contains(ClientAuthenticationMethods.ClientSecretBasic)
=> ClientAuthenticationMethods.ClientSecretBasic,
({ Count: > 0 } client, { Count: 0 }) when !string.IsNullOrEmpty(context.Registration.ClientSecret) &&
client.Contains(ClientAuthenticationMethods.ClientSecretBasic)
=> ClientAuthenticationMethods.ClientSecretBasic,
_ => null
};
return ValueTask.CompletedTask;
}
public ValueTask HandleAsync(ProcessChallengeContext context) => ValueTask.CompletedTask;
}
/// <summary>
/// Contains the logic responsible for negotiating the best introspection endpoint client
/// authentication method supported by both the client and the authorization server.
/// </summary>
[Obsolete("This class is obsolete and will be removed in a future version.")]
public sealed class AttachNonDefaultIntrospectionEndpointClientAuthenticationMethod : IOpenIddictClientHandler<ProcessIntrospectionContext>
{
private readonly IOptionsMonitor<OpenIddictClientSystemNetHttpOptions> _options;
public AttachNonDefaultIntrospectionEndpointClientAuthenticationMethod(
IOptionsMonitor<OpenIddictClientSystemNetHttpOptions> options)
=> _options = options ?? throw new ArgumentNullException(nameof(options));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
@ -515,113 +139,16 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessIntrospectionContext context)
{
ArgumentNullException.ThrowIfNull(context);
// If an explicit client authentication method was attached, don't overwrite it.
if (!string.IsNullOrEmpty(context.IntrospectionEndpointClientAuthenticationMethod))
{
return ValueTask.CompletedTask;
}
context.IntrospectionEndpointClientAuthenticationMethod = (
// Note: if client authentication methods are explicitly listed in the client registration, only use
// the client authentication methods that are both listed and enabled in the global client options.
// Otherwise, always default to the client authentication methods that have been enabled globally.
Client: context.Registration.ClientAuthenticationMethods.Count switch
{
0 => context.Options.ClientAuthenticationMethods as ICollection<string>,
_ => context.Options.ClientAuthenticationMethods.Intersect(context.Registration.ClientAuthenticationMethods, StringComparer.Ordinal).ToList()
},
Server: context.Configuration.IntrospectionEndpointAuthMethodsSupported) switch
{
// If a TLS client authentication certificate could be resolved and both the
// client and the server explicitly support tls_client_auth, always prefer it.
({ Count: > 0 } client, { Count: > 0 } server) when
client.Contains(ClientAuthenticationMethods.TlsClientAuth) &&
server.Contains(ClientAuthenticationMethods.TlsClientAuth) &&
(context.Configuration.MtlsIntrospectionEndpoint ?? context.Configuration.IntrospectionEndpoint) is Uri endpoint &&
string.Equals(endpoint.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase) &&
_options.CurrentValue.TlsClientAuthenticationCertificateSelector(context.Registration) is not null
=> ClientAuthenticationMethods.TlsClientAuth,
// If a self-signed TLS client authentication certificate could be resolved and both
// the client and the server explicitly support self_signed_tls_client_auth, use it.
({ Count: > 0 } client, { Count: > 0 } server) when
client.Contains(ClientAuthenticationMethods.SelfSignedTlsClientAuth) &&
server.Contains(ClientAuthenticationMethods.SelfSignedTlsClientAuth) &&
(context.Configuration.MtlsIntrospectionEndpoint ?? context.Configuration.IntrospectionEndpoint) is Uri endpoint &&
string.Equals(endpoint.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase) &&
_options.CurrentValue.SelfSignedTlsClientAuthenticationCertificateSelector(context.Registration) is not null
=> ClientAuthenticationMethods.SelfSignedTlsClientAuth,
// If at least one asymmetric signing key was attached to the client registration
// and both the client and the server explicitly support private_key_jwt, use it.
({ Count: > 0 } client, { Count: > 0 } server) when
client.Contains(ClientAuthenticationMethods.PrivateKeyJwt) &&
server.Contains(ClientAuthenticationMethods.PrivateKeyJwt) &&
context.Registration.SigningCredentials.Exists(static credentials => credentials.Key is AsymmetricSecurityKey)
=> ClientAuthenticationMethods.PrivateKeyJwt,
// If a client secret was attached to the client registration and both the client and
// the server explicitly support client_secret_post, prefer it to basic authentication.
({ Count: > 0 } client, { Count: > 0 } server) when !string.IsNullOrEmpty(context.Registration.ClientSecret) &&
client.Contains(ClientAuthenticationMethods.ClientSecretPost) &&
server.Contains(ClientAuthenticationMethods.ClientSecretPost)
=> ClientAuthenticationMethods.ClientSecretPost,
// The OAuth 2.0 specification recommends sending the client credentials using basic authentication.
// However, this authentication method is known to have severe compatibility/interoperability issues:
//
// - While restricted to clients that have been given a secret (i.e confidential clients) by the
// specification, basic authentication is also sometimes required by server implementations for
// public clients that don't have a client secret: in this case, an empty password is used and
// the client identifier is sent alone in the Authorization header (instead of being sent using
// the standard "client_id" parameter present in the request body).
//
// - While the OAuth 2.0 specification requires that the client credentials be formURL-encoded
// before being base64-encoded, many implementations are known to implement a non-standard
// encoding scheme, where neither the client_id nor the client_secret are formURL-encoded.
//
// To guarantee that the OpenIddict implementation can be used with most servers implementions,
// basic authentication is only used when a client secret is present and the server configuration
// doesn't list any supported client authentication method or doesn't support client_secret_post.
//
// If client_secret_post is not listed or if the server returned an empty methods list,
// client_secret_basic is always used, as it MUST be implemented by all OAuth 2.0 servers.
//
// See https://tools.ietf.org/html/rfc8414#section-2
// and https://tools.ietf.org/html/rfc6749#section-2.3.1 for more information.
({ Count: > 0 } client, { Count: > 0 } server) when !string.IsNullOrEmpty(context.Registration.ClientSecret) &&
client.Contains(ClientAuthenticationMethods.ClientSecretBasic) &&
server.Contains(ClientAuthenticationMethods.ClientSecretBasic)
=> ClientAuthenticationMethods.ClientSecretBasic,
({ Count: > 0 } client, { Count: 0 }) when !string.IsNullOrEmpty(context.Registration.ClientSecret) &&
client.Contains(ClientAuthenticationMethods.ClientSecretBasic)
=> ClientAuthenticationMethods.ClientSecretBasic,
_ => null
};
return ValueTask.CompletedTask;
}
public ValueTask HandleAsync(ProcessIntrospectionContext context) => ValueTask.CompletedTask;
}
/// <summary>
/// Contains the logic responsible for negotiating the best revocation endpoint client
/// authentication method supported by both the client and the authorization server.
/// </summary>
[Obsolete("This class is obsolete and will be removed in a future version.")]
public sealed class AttachNonDefaultRevocationEndpointClientAuthenticationMethod : IOpenIddictClientHandler<ProcessRevocationContext>
{
private readonly IOptionsMonitor<OpenIddictClientSystemNetHttpOptions> _options;
public AttachNonDefaultRevocationEndpointClientAuthenticationMethod(
IOptionsMonitor<OpenIddictClientSystemNetHttpOptions> options)
=> _options = options ?? throw new ArgumentNullException(nameof(options));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
@ -634,99 +161,7 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessRevocationContext context)
{
ArgumentNullException.ThrowIfNull(context);
// If an explicit client authentication method was attached, don't overwrite it.
if (!string.IsNullOrEmpty(context.RevocationEndpointClientAuthenticationMethod))
{
return ValueTask.CompletedTask;
}
context.RevocationEndpointClientAuthenticationMethod = (
// Note: if client authentication methods are explicitly listed in the client registration, only use
// the client authentication methods that are both listed and enabled in the global client options.
// Otherwise, always default to the client authentication methods that have been enabled globally.
Client: context.Registration.ClientAuthenticationMethods.Count switch
{
0 => context.Options.ClientAuthenticationMethods as ICollection<string>,
_ => context.Options.ClientAuthenticationMethods.Intersect(context.Registration.ClientAuthenticationMethods, StringComparer.Ordinal).ToList()
},
Server: context.Configuration.RevocationEndpointAuthMethodsSupported) switch
{
// If a TLS client authentication certificate could be resolved and both the
// client and the server explicitly support tls_client_auth, always prefer it.
({ Count: > 0 } client, { Count: > 0 } server) when
client.Contains(ClientAuthenticationMethods.TlsClientAuth) &&
server.Contains(ClientAuthenticationMethods.TlsClientAuth) &&
(context.Configuration.MtlsRevocationEndpoint ?? context.Configuration.RevocationEndpoint) is Uri endpoint &&
string.Equals(endpoint.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase) &&
_options.CurrentValue.TlsClientAuthenticationCertificateSelector(context.Registration) is not null
=> ClientAuthenticationMethods.TlsClientAuth,
// If a self-signed TLS client authentication certificate could be resolved and both
// the client and the server explicitly support self_signed_tls_client_auth, use it.
({ Count: > 0 } client, { Count: > 0 } server) when
client.Contains(ClientAuthenticationMethods.SelfSignedTlsClientAuth) &&
server.Contains(ClientAuthenticationMethods.SelfSignedTlsClientAuth) &&
(context.Configuration.MtlsRevocationEndpoint ?? context.Configuration.RevocationEndpoint) is Uri endpoint &&
string.Equals(endpoint.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase) &&
_options.CurrentValue.SelfSignedTlsClientAuthenticationCertificateSelector(context.Registration) is not null
=> ClientAuthenticationMethods.SelfSignedTlsClientAuth,
// If at least one asymmetric signing key was attached to the client registration
// and both the client and the server explicitly support private_key_jwt, use it.
({ Count: > 0 } client, { Count: > 0 } server) when
client.Contains(ClientAuthenticationMethods.PrivateKeyJwt) &&
server.Contains(ClientAuthenticationMethods.PrivateKeyJwt) &&
context.Registration.SigningCredentials.Exists(static credentials => credentials.Key is AsymmetricSecurityKey)
=> ClientAuthenticationMethods.PrivateKeyJwt,
// If a client secret was attached to the client registration and both the client and
// the server explicitly support client_secret_post, prefer it to basic authentication.
({ Count: > 0 } client, { Count: > 0 } server) when !string.IsNullOrEmpty(context.Registration.ClientSecret) &&
client.Contains(ClientAuthenticationMethods.ClientSecretPost) &&
server.Contains(ClientAuthenticationMethods.ClientSecretPost)
=> ClientAuthenticationMethods.ClientSecretPost,
// The OAuth 2.0 specification recommends sending the client credentials using basic authentication.
// However, this authentication method is known to have severe compatibility/interoperability issues:
//
// - While restricted to clients that have been given a secret (i.e confidential clients) by the
// specification, basic authentication is also sometimes required by server implementations for
// public clients that don't have a client secret: in this case, an empty password is used and
// the client identifier is sent alone in the Authorization header (instead of being sent using
// the standard "client_id" parameter present in the request body).
//
// - While the OAuth 2.0 specification requires that the client credentials be formURL-encoded
// before being base64-encoded, many implementations are known to implement a non-standard
// encoding scheme, where neither the client_id nor the client_secret are formURL-encoded.
//
// To guarantee that the OpenIddict implementation can be used with most servers implementions,
// basic authentication is only used when a client secret is present and the server configuration
// doesn't list any supported client authentication method or doesn't support client_secret_post.
//
// If client_secret_post is not listed or if the server returned an empty methods list,
// client_secret_basic is always used, as it MUST be implemented by all OAuth 2.0 servers.
//
// See https://tools.ietf.org/html/rfc8414#section-2
// and https://tools.ietf.org/html/rfc6749#section-2.3.1 for more information.
({ Count: > 0 } client, { Count: > 0 } server) when !string.IsNullOrEmpty(context.Registration.ClientSecret) &&
client.Contains(ClientAuthenticationMethods.ClientSecretBasic) &&
server.Contains(ClientAuthenticationMethods.ClientSecretBasic)
=> ClientAuthenticationMethods.ClientSecretBasic,
({ Count: > 0 } client, { Count: 0 }) when !string.IsNullOrEmpty(context.Registration.ClientSecret) &&
client.Contains(ClientAuthenticationMethods.ClientSecretBasic)
=> ClientAuthenticationMethods.ClientSecretBasic,
_ => null
};
return ValueTask.CompletedTask;
}
public ValueTask HandleAsync(ProcessRevocationContext context) => ValueTask.CompletedTask;
}
/// <summary>
@ -759,57 +194,40 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
// accessed from the HttpClientAction or HttpMessageHandlerBuilderAction delegates
// to dynamically amend the resulting HttpClient or HttpClientHandler instance.
//
// To work around this limitation, the OpenIddict System.Net.Http integration
// uses dynamic client names and supports appending a list of key-value pairs
// to the client name to flow per-instance properties.
var builder = new StringBuilder();
// To work around this limitation, the OpenIddict System.Net.Http integration uses
// an async-local context to flow per-instance properties and uses dynamic client
// names to ensure the inner HttpClientHandler is not reused if the context differs.
// Always prefix the HTTP client name with the assembly name of the System.Net.Http package.
builder.Append(typeof(OpenIddictClientSystemNetHttpOptions).Assembly.GetName().Name);
builder.Append(':');
// Attach the registration identifier.
builder.Append("RegistrationId")
.Append('\u001e')
.Append(context.Registration.RegistrationId);
// If both a client authentication method and one or multiple token binding methods were negotiated,
// make sure they are compatible (e.g that they all use a CA-issued or self-signed X.509 certificate).
if ((context.ClientAuthenticationMethod is ClientAuthenticationMethods.TlsClientAuth &&
context.TokenBindingMethods.Contains(TokenBindingMethods.Private.SelfSignedTlsClientCertificate)) ||
(context.ClientAuthenticationMethod is ClientAuthenticationMethods.SelfSignedTlsClientAuth &&
context.TokenBindingMethods.Contains(TokenBindingMethods.Private.TlsClientCertificate)))
if (OpenIddictClientSystemNetHttpContext.Current is not null)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0456));
throw new InvalidOperationException(SR.FormatID2201(nameof(OpenIddictClientSystemNetHttpContext)));
}
// Attach a flag indicating that a client certificate should be used in the TLS handshake.
if (context.ClientAuthenticationMethod is ClientAuthenticationMethods.TlsClientAuth ||
context.TokenBindingMethods.Contains(TokenBindingMethods.Private.TlsClientCertificate))
try
{
builder.Append('\u001f');
OpenIddictClientSystemNetHttpContext.Current = new()
{
Registration = context.Registration,
LocalCertificate = context.LocalCertificate
};
builder.Append("AttachTlsClientCertificate")
.Append('\u001e')
.Append(bool.TrueString);
}
// Generate a stable identifier representing the current context to ensure the inner
// HttpClientHandler instances are not reused for different operations if the properties
// attached to the context are not identical (e.g different TLS client certificates).
var identifier = OpenIddictClientSystemNetHttpContext.ComputeStableId(OpenIddictClientSystemNetHttpContext.Current);
// Attach a flag indicating that a self-signed client certificate should be used in the TLS handshake.
else if (context.ClientAuthenticationMethod is ClientAuthenticationMethods.SelfSignedTlsClientAuth ||
context.TokenBindingMethods.Contains(TokenBindingMethods.Private.SelfSignedTlsClientCertificate))
{
builder.Append('\u001f');
var client = _factory.CreateClient(
$"{typeof(OpenIddictClientSystemNetHttpOptions).Assembly.GetName().Name}:{identifier}") ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0174));
builder.Append("AttachSelfSignedTlsClientCertificate")
.Append('\u001e')
.Append(bool.TrueString);
// Create and store the HttpClient in the transaction properties.
context.Transaction.SetProperty(typeof(HttpClient).FullName!, client);
}
// Create and store the HttpClient in the transaction properties.
context.Transaction.SetProperty(typeof(HttpClient).FullName!, _factory.CreateClient(builder.ToString()) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0174)));
finally
{
OpenIddictClientSystemNetHttpContext.Current = null;
}
return ValueTask.CompletedTask;
}

2
src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpOptions.cs

@ -95,6 +95,7 @@ public sealed class OpenIddictClientSystemNetHttpOptions
/// client authentication key usages to be automatically selected by OpenIddict).
/// </remarks>
[EditorBrowsable(EditorBrowsableState.Advanced)]
[Obsolete("This option is no longer supported and will be removed in a future version.")]
public Func<OpenIddictClientRegistration, X509Certificate2?> SelfSignedTlsClientAuthenticationCertificateSelector { get; set; } = default!;
/// <summary>
@ -109,5 +110,6 @@ public sealed class OpenIddictClientSystemNetHttpOptions
/// client authentication key usages to be automatically selected by OpenIddict).
/// </remarks>
[EditorBrowsable(EditorBrowsableState.Advanced)]
[Obsolete("This option is no longer supported and will be removed in a future version.")]
public Func<OpenIddictClientRegistration, X509Certificate2?> TlsClientAuthenticationCertificateSelector { get; set; } = default!;
}

34
src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationConfiguration.cs

@ -5,10 +5,8 @@
*/
using System.ComponentModel;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.Options;
using OpenIddict.Client.SystemNetHttp;
using static OpenIddict.Client.WebIntegration.OpenIddictClientWebIntegrationConstants;
namespace OpenIddict.Client.WebIntegration;
@ -34,7 +32,7 @@ public sealed partial class OpenIddictClientWebIntegrationConfiguration : IConfi
{
ArgumentNullException.ThrowIfNull(options);
options.Registrations.ForEach(static registration =>
foreach (var registration in options.Registrations)
{
// If the client registration has a provider type attached, apply
// the configuration logic corresponding to the specified provider.
@ -42,37 +40,13 @@ public sealed partial class OpenIddictClientWebIntegrationConfiguration : IConfi
{
ConfigureProvider(registration);
}
});
}
}
/// <inheritdoc/>
[Obsolete("This method is no longer supported and will be removed in a future version.")]
public void PostConfigure(string? name, OpenIddictClientSystemNetHttpOptions options)
{
ArgumentNullException.ThrowIfNull(options);
// Override the default/user-defined selectors to support attaching TLS client
// certificates that don't meet the requirements enforced by default by OpenIddict.
options.SelfSignedTlsClientAuthenticationCertificateSelector = CreateSelector(options.SelfSignedTlsClientAuthenticationCertificateSelector);
options.TlsClientAuthenticationCertificateSelector = CreateSelector(options.TlsClientAuthenticationCertificateSelector);
static Func<OpenIddictClientRegistration, X509Certificate2?> CreateSelector(Func<OpenIddictClientRegistration, X509Certificate2?> selector)
=> registration =>
{
var certificate = registration.ProviderType switch
{
ProviderTypes.ProSantéConnect => registration.GetProSantéConnectSettings().SigningCertificate,
_ => null
};
if (certificate is not null)
{
return certificate;
}
return selector(registration);
};
}
=> throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
/// <summary>
/// Amends the registration with the provider-specific configuration logic.

3
src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationExtensions.cs

@ -7,7 +7,6 @@
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using OpenIddict.Client;
using OpenIddict.Client.SystemNetHttp;
using OpenIddict.Client.WebIntegration;
namespace Microsoft.Extensions.DependencyInjection;
@ -38,8 +37,6 @@ public static partial class OpenIddictClientWebIntegrationExtensions
// Note: TryAddEnumerable() is used here to ensure the initializers are registered only once.
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<
IConfigureOptions<OpenIddictClientOptions>, OpenIddictClientWebIntegrationConfiguration>());
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<
IPostConfigureOptions<OpenIddictClientSystemNetHttpOptions>, OpenIddictClientWebIntegrationConfiguration>());
// Note: the IPostConfigureOptions<OpenIddictClientOptions> service responsible for populating
// the client registrations MUST be registered before OpenIddictClientConfiguration to ensure

11
src/OpenIddict.Client/OpenIddictClientConfiguration.cs

@ -82,6 +82,17 @@ public sealed class OpenIddictClientConfiguration : IPostConfigureOptions<OpenId
throw new InvalidOperationException(SR.GetResourceString(SR.ID0455));
}
// If no client type was explicitly set, assume the client is confidential if a client secret
// or a signing key/certificate (typically used with private_key_jwt, tls_client_auth or
// self_signed_tls_client_auth) has been attached to the client registration.
if (string.IsNullOrEmpty(registration.ClientType))
{
registration.ClientType =
!string.IsNullOrEmpty(registration.ClientSecret) || registration.SigningCredentials.Count is > 0
? ClientTypes.Confidential
: ClientTypes.Public;
}
if (registration.ConfigurationManager is null)
{
if (registration.Configuration is not null)

57
src/OpenIddict.Client/OpenIddictClientEvents.cs

@ -7,6 +7,7 @@
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.Logging;
namespace OpenIddict.Client;
@ -165,9 +166,16 @@ public static partial class OpenIddictClientEvents
/// </summary>
public string? ClientAuthenticationMethod { get; set; }
/// <summary>
/// Gets or sets the X.509 client certificate that will be used to authenticate
/// this peer when communicating with the external endpoint, if applicable.
/// </summary>
public X509Certificate2? LocalCertificate { get; set; }
/// <summary>
/// Gets or sets the token binding method used when communicating with the external endpoint, if applicable.
/// </summary>
[Obsolete("This property is no longer used and will be removed in a future version.")]
public HashSet<string> TokenBindingMethods { get; } = new(StringComparer.Ordinal);
}
@ -397,15 +405,40 @@ public static partial class OpenIddictClientEvents
/// </summary>
public string? TokenEndpointClientAuthenticationMethod { get; set; }
/// <summary>
/// Gets or sets the X.509 client certificate used when
/// communicating with the token endpoint, if applicable.
/// </summary>
public X509Certificate2? TokenEndpointClientCertificate { get; set; }
/// <summary>
/// Gets or sets the token binding method used when
/// communicating with the token endpoint, if applicable.
/// </summary>
public string? TokenEndpointTokenBindingMethod { get; set; }
/// <summary>
/// Gets or sets the URI of the userinfo endpoint, if applicable.
/// </summary>
public Uri? UserInfoEndpoint { get; set; }
/// <summary>
/// Gets or sets the token binding method used when
/// communicating with the userinfo endpoint, if applicable.
/// </summary>
public string? UserInfoEndpointTokenBindingMethod { get; set; }
/// <summary>
/// Gets or sets the X.509 client certificate used when
/// communicating with the userinfo endpoint, if applicable.
/// </summary>
public X509Certificate2? UserInfoEndpointClientCertificate { get; set; }
/// <summary>
/// Gets or sets the token binding methods used when
/// communicating with the userinfo endpoint, if applicable.
/// </summary>
[Obsolete("This property is no longer used and will be removed in a future version.")]
public HashSet<string> UserInfoEndpointTokenBindingMethods { get; } = new(StringComparer.Ordinal);
/// <summary>
@ -1153,6 +1186,12 @@ public static partial class OpenIddictClientEvents
/// </summary>
public string? DeviceAuthorizationEndpointClientAuthenticationMethod { get; set; }
/// <summary>
/// Gets or sets the X.509 client certificate used when communicating
/// with the device authorization endpoint, if applicable.
/// </summary>
public X509Certificate2? DeviceAuthorizationEndpointClientCertificate { get; set; }
/// <summary>
/// Gets or sets the URI of the pushed authorization endpoint, if applicable.
/// </summary>
@ -1164,6 +1203,12 @@ public static partial class OpenIddictClientEvents
/// </summary>
public string? PushedAuthorizationEndpointClientAuthenticationMethod { get; set; }
/// <summary>
/// Gets or sets the X.509 client certificate used when communicating
/// with the pushed authorization endpoint, if applicable.
/// </summary>
public X509Certificate2? PushedAuthorizationEndpointClientCertificate { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether a state token
/// should be generated (and optionally included in the request).
@ -1461,6 +1506,12 @@ public static partial class OpenIddictClientEvents
/// </summary>
public string? IntrospectionEndpointClientAuthenticationMethod { get; set; }
/// <summary>
/// Gets or sets the X.509 client certificate used when
/// communicating with the introspection endpoint, if applicable.
/// </summary>
public X509Certificate2? IntrospectionEndpointClientCertificate { get; set; }
/// <summary>
/// Gets or sets the client identifier that will be used for the introspection demand.
/// </summary>
@ -1594,6 +1645,12 @@ public static partial class OpenIddictClientEvents
/// </summary>
public string? RevocationEndpointClientAuthenticationMethod { get; set; }
/// <summary>
/// Gets or sets the X.509 client certificate used when
/// communicating with the revocation endpoint, if applicable.
/// </summary>
public X509Certificate2? RevocationEndpointClientCertificate { get; set; }
/// <summary>
/// Gets or sets the client identifier that will be used for the revocation demand.
/// </summary>

1120
src/OpenIddict.Client/OpenIddictClientHandlers.cs

File diff suppressed because it is too large

223
src/OpenIddict.Client/OpenIddictClientModels.cs

@ -6,6 +6,7 @@
using System.ComponentModel;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
namespace OpenIddict.Client;
@ -176,6 +177,15 @@ public static class OpenIddictClientModels
/// </summary>
public string? IdentityTokenHint { get; init; }
/// <summary>
/// Gets or sets the issuer used to resolve the client registration.
/// </summary>
/// <remarks>
/// Note: if multiple client registrations point to the same issuer,
/// the <see cref="RegistrationId"/> property must be explicitly set.
/// </remarks>
public Uri? Issuer { get; init; }
/// <summary>
/// Gets or sets the optional login hint that will be sent to the authorization server, if applicable.
/// </summary>
@ -231,15 +241,6 @@ public static class OpenIddictClientModels
/// Gets the scopes that will be sent to the authorization server.
/// </summary>
public List<string>? Scopes { get; init; }
/// <summary>
/// Gets or sets the issuer used to resolve the client registration.
/// </summary>
/// <remarks>
/// Note: if multiple client registrations point to the same issuer,
/// the <see cref="RegistrationId"/> property must be explicitly set.
/// </remarks>
public Uri? Issuer { get; init; }
}
/// <summary>
@ -285,6 +286,15 @@ public static class OpenIddictClientModels
/// </summary>
public string? IdentityTokenHint { get; init; }
/// <summary>
/// Gets or sets the issuer used to resolve the client registration.
/// </summary>
/// <remarks>
/// Note: if multiple client registrations point to the same issuer,
/// the <see cref="RegistrationId"/> property must be explicitly set.
/// </remarks>
public Uri? Issuer { get; init; }
/// <summary>
/// Gets or sets the optional login hint that will be sent to the authorization server, if applicable.
/// </summary>
@ -318,15 +328,6 @@ public static class OpenIddictClientModels
/// Gets the scopes that will be sent to the authorization server.
/// </summary>
public List<string>? Scopes { get; init; }
/// <summary>
/// Gets or sets the issuer used to resolve the client registration.
/// </summary>
/// <remarks>
/// Note: if multiple client registrations point to the same issuer,
/// the <see cref="RegistrationId"/> property must be explicitly set.
/// </remarks>
public Uri? Issuer { get; init; }
}
/// <summary>
@ -366,6 +367,15 @@ public static class OpenIddictClientModels
/// </summary>
public CancellationToken CancellationToken { get; init; }
/// <summary>
/// Gets or sets the issuer used to resolve the client registration.
/// </summary>
/// <remarks>
/// Note: if multiple client registrations point to the same issuer,
/// the <see cref="RegistrationId"/> property must be explicitly set.
/// </remarks>
public Uri? Issuer { get; init; }
/// <summary>
/// Gets or sets the application-specific properties that will be added to the context.
/// </summary>
@ -396,13 +406,21 @@ public static class OpenIddictClientModels
public List<string>? Scopes { get; init; }
/// <summary>
/// Gets or sets the issuer used to resolve the client registration.
/// Gets or sets the X.509 client certificate used to bind the access and/or
/// refresh tokens issued by the authorization server, if applicable.
/// </summary>
/// <remarks>
/// Note: if multiple client registrations point to the same issuer,
/// the <see cref="RegistrationId"/> property must be explicitly set.
/// <para>
/// Note: when mTLs is also used for OAuth 2.0 client authentication, the
/// certificate set here replaces the client certificate chosen by OpenIddict.
/// </para>
/// <para>
/// Note: if a certificate-based client authentication or token binding method is
/// negotiated, the type of the certificate must match the negotiated methods.
/// </para>
/// </remarks>
public Uri? Issuer { get; init; }
[EditorBrowsable(EditorBrowsableState.Advanced)]
public X509Certificate2? TokenBindingCertificate { get; init; }
}
/// <summary>
@ -513,6 +531,15 @@ public static class OpenIddictClientModels
/// </summary>
public required string GrantType { get; init; }
/// <summary>
/// Gets or sets the issuer used to resolve the client registration.
/// </summary>
/// <remarks>
/// Note: if multiple client registrations point to the same issuer,
/// the <see cref="RegistrationId"/> property must be explicitly set.
/// </remarks>
public Uri? Issuer { get; init; }
/// <summary>
/// Gets or sets the application-specific properties that will be added to the context.
/// </summary>
@ -543,13 +570,21 @@ public static class OpenIddictClientModels
public List<string>? Scopes { get; init; }
/// <summary>
/// Gets or sets the issuer used to resolve the client registration.
/// Gets or sets the X.509 client certificate used to bind the access and/or
/// refresh tokens issued by the authorization server, if applicable.
/// </summary>
/// <remarks>
/// Note: if multiple client registrations point to the same issuer,
/// the <see cref="RegistrationId"/> property must be explicitly set.
/// <para>
/// Note: when mTLs is also used for OAuth 2.0 client authentication, the
/// certificate set here replaces the client certificate chosen by OpenIddict.
/// </para>
/// <para>
/// Note: if a certificate-based client authentication or token binding method is
/// negotiated, the type of the certificate must match the negotiated methods.
/// </para>
/// </remarks>
public Uri? Issuer { get; init; }
[EditorBrowsable(EditorBrowsableState.Advanced)]
public X509Certificate2? TokenBindingCertificate { get; init; }
}
/// <summary>
@ -655,6 +690,15 @@ public static class OpenIddictClientModels
/// </summary>
public required TimeSpan Interval { get; init; }
/// <summary>
/// Gets or sets the issuer used to resolve the client registration.
/// </summary>
/// <remarks>
/// Note: if multiple client registrations point to the same issuer,
/// the <see cref="RegistrationId"/> property must be explicitly set.
/// </remarks>
public Uri? Issuer { get; init; }
/// <summary>
/// Gets or sets the application-specific properties that will be added to the context.
/// </summary>
@ -685,13 +729,21 @@ public static class OpenIddictClientModels
public List<string>? Scopes { get; init; }
/// <summary>
/// Gets or sets the issuer used to resolve the client registration.
/// Gets or sets the X.509 client certificate used to bind the access and/or
/// refresh tokens issued by the authorization server, if applicable.
/// </summary>
/// <remarks>
/// Note: if multiple client registrations point to the same issuer,
/// the <see cref="RegistrationId"/> property must be explicitly set.
/// <para>
/// Note: when mTLs is also used for OAuth 2.0 client authentication, the
/// certificate set here replaces the client certificate chosen by OpenIddict.
/// </para>
/// <para>
/// Note: if a certificate-based client authentication or token binding method is
/// negotiated, the type of the certificate must match the negotiated methods.
/// </para>
/// </remarks>
public Uri? Issuer { get; init; }
[EditorBrowsable(EditorBrowsableState.Advanced)]
public X509Certificate2? TokenBindingCertificate { get; init; }
}
/// <summary>
@ -775,6 +827,15 @@ public static class OpenIddictClientModels
/// </summary>
public CancellationToken CancellationToken { get; init; }
/// <summary>
/// Gets or sets the issuer used to resolve the client registration.
/// </summary>
/// <remarks>
/// Note: if multiple client registrations point to the same issuer,
/// the <see cref="RegistrationId"/> property must be explicitly set.
/// </remarks>
public Uri? Issuer { get; init; }
/// <summary>
/// Gets or sets the application-specific properties that will be added to the context.
/// </summary>
@ -803,15 +864,6 @@ public static class OpenIddictClientModels
/// Gets the scopes that will be sent to the authorization server.
/// </summary>
public List<string>? Scopes { get; init; }
/// <summary>
/// Gets or sets the issuer used to resolve the client registration.
/// </summary>
/// <remarks>
/// Note: if multiple client registrations point to the same issuer,
/// the <see cref="RegistrationId"/> property must be explicitly set.
/// </remarks>
public Uri? Issuer { get; init; }
}
/// <summary>
@ -876,6 +928,15 @@ public static class OpenIddictClientModels
/// </summary>
public CancellationToken CancellationToken { get; init; }
/// <summary>
/// Gets or sets the issuer used to resolve the client registration.
/// </summary>
/// <remarks>
/// Note: if multiple client registrations point to the same issuer,
/// the <see cref="RegistrationId"/> property must be explicitly set.
/// </remarks>
public Uri? Issuer { get; init; }
/// <summary>
/// Gets or sets the application-specific properties that will be added to the context.
/// </summary>
@ -904,15 +965,6 @@ public static class OpenIddictClientModels
/// Gets the token type hint that will be sent to the authorization server.
/// </summary>
public string? TokenTypeHint { get; init; }
/// <summary>
/// Gets or sets the issuer used to resolve the client registration.
/// </summary>
/// <remarks>
/// Note: if multiple client registrations point to the same issuer,
/// the <see cref="RegistrationId"/> property must be explicitly set.
/// </remarks>
public Uri? Issuer { get; init; }
}
/// <summary>
@ -967,6 +1019,15 @@ public static class OpenIddictClientModels
/// </summary>
public bool DisableUserInfo { get; init; }
/// <summary>
/// Gets or sets the issuer used to resolve the client registration.
/// </summary>
/// <remarks>
/// Note: if multiple client registrations point to the same issuer,
/// the <see cref="RegistrationId"/> property must be explicitly set.
/// </remarks>
public Uri? Issuer { get; init; }
/// <summary>
/// Gets or sets the password that will be sent to the authorization server.
/// </summary>
@ -1007,13 +1068,21 @@ public static class OpenIddictClientModels
public required string Username { get; init; }
/// <summary>
/// Gets or sets the issuer used to resolve the client registration.
/// Gets or sets the X.509 client certificate used to bind the access and/or
/// refresh tokens issued by the authorization server, if applicable.
/// </summary>
/// <remarks>
/// Note: if multiple client registrations point to the same issuer,
/// the <see cref="RegistrationId"/> property must be explicitly set.
/// <para>
/// Note: when mTLs is also used for OAuth 2.0 client authentication, the
/// certificate set here replaces the client certificate chosen by OpenIddict.
/// </para>
/// <para>
/// Note: if a certificate-based client authentication or token binding method is
/// negotiated, the type of the certificate must match the negotiated methods.
/// </para>
/// </remarks>
public Uri? Issuer { get; init; }
[EditorBrowsable(EditorBrowsableState.Advanced)]
public X509Certificate2? TokenBindingCertificate { get; init; }
}
/// <summary>
@ -1102,6 +1171,15 @@ public static class OpenIddictClientModels
/// </summary>
public bool DisableUserInfo { get; init; }
/// <summary>
/// Gets or sets the issuer used to resolve the client registration.
/// </summary>
/// <remarks>
/// Note: if multiple client registrations point to the same issuer,
/// the <see cref="RegistrationId"/> property must be explicitly set.
/// </remarks>
public Uri? Issuer { get; init; }
/// <summary>
/// Gets or sets the application-specific properties that will be added to the context.
/// </summary>
@ -1137,13 +1215,21 @@ public static class OpenIddictClientModels
public required string RefreshToken { get; init; }
/// <summary>
/// Gets or sets the issuer used to resolve the client registration.
/// Gets or sets the X.509 client certificate used to bind the access and/or
/// refresh tokens issued by the authorization server, if applicable.
/// </summary>
/// <remarks>
/// Note: if multiple client registrations point to the same issuer,
/// the <see cref="RegistrationId"/> property must be explicitly set.
/// <para>
/// Note: when mTLs is also used for OAuth 2.0 client authentication, the
/// certificate set here replaces the client certificate chosen by OpenIddict.
/// </para>
/// <para>
/// Note: if a certificate-based client authentication or token binding method is
/// negotiated, the type of the certificate must match the negotiated methods.
/// </para>
/// </remarks>
public Uri? Issuer { get; init; }
[EditorBrowsable(EditorBrowsableState.Advanced)]
public X509Certificate2? TokenBindingCertificate { get; init; }
}
/// <summary>
@ -1245,6 +1331,15 @@ public static class OpenIddictClientModels
/// </remarks>
public bool DisableUserInfo { get; init; }
/// <summary>
/// Gets or sets the issuer used to resolve the client registration.
/// </summary>
/// <remarks>
/// Note: if multiple client registrations point to the same issuer,
/// the <see cref="RegistrationId"/> property must be explicitly set.
/// </remarks>
public Uri? Issuer { get; init; }
/// <summary>
/// Gets or sets the application-specific properties that will be added to the context.
/// </summary>
@ -1290,13 +1385,21 @@ public static class OpenIddictClientModels
public required string SubjectTokenType { get; init; }
/// <summary>
/// Gets or sets the issuer used to resolve the client registration.
/// Gets or sets the X.509 client certificate used to bind the access and/or
/// refresh tokens issued by the authorization server, if applicable.
/// </summary>
/// <remarks>
/// Note: if multiple client registrations point to the same issuer,
/// the <see cref="RegistrationId"/> property must be explicitly set.
/// <para>
/// Note: when mTLs is also used for OAuth 2.0 client authentication, the
/// certificate set here replaces the client certificate chosen by OpenIddict.
/// </para>
/// <para>
/// Note: if a certificate-based client authentication or token binding method is
/// negotiated, the type of the certificate must match the negotiated methods.
/// </para>
/// </remarks>
public Uri? Issuer { get; init; }
[EditorBrowsable(EditorBrowsableState.Advanced)]
public X509Certificate2? TokenBindingCertificate { get; init; }
}
/// <summary>

5
src/OpenIddict.Client/OpenIddictClientOptions.cs

@ -218,4 +218,9 @@ public sealed class OpenIddictClientOptions
/// If no service can be found, <see cref="TimeProvider.System"/> is used.
/// </remarks>
public TimeProvider TimeProvider { get; set; } = default!;
/// <summary>
/// Gets the OAuth 2.0 token binding methods enabled for this application.
/// </summary>
public HashSet<string> TokenBindingMethods { get; } = new(StringComparer.Ordinal);
}

16
src/OpenIddict.Client/OpenIddictClientRegistration.cs

@ -83,6 +83,12 @@ public sealed class OpenIddictClientRegistration
/// </remarks>
public HashSet<string> ClientAuthenticationMethods { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets or sets the type of the client. If no value is explicitly set, the client is assumed to be
/// "confidential" if a client secret or a signing key/certificate was assigned ("public" otherwise).
/// </summary>
public string? ClientType { get; set; }
/// <summary>
/// Gets the code challenge methods allowed by the client instance.
/// If no value is explicitly set, all the methods enabled in the client options can be used.
@ -123,6 +129,16 @@ public sealed class OpenIddictClientRegistration
/// </remarks>
public HashSet<string> ResponseTypes { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets the token binding methods allowed by the client instance.
/// If no value is explicitly set, all the methods enabled in the client options can be used.
/// </summary>
/// <remarks>
/// The final token binding method used in backchannel requests is chosen by OpenIddict based
/// on the client options, the server configuration and the values registered in this property.
/// </remarks>
public HashSet<string> TokenBindingMethods { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets or sets the issuer that will be attached to the <see cref="Claim"/>
/// instances created by the OpenIddict client stack for this registration.

71
src/OpenIddict.Client/OpenIddictClientService.cs

@ -8,6 +8,7 @@ using System.Collections.Immutable;
using System.ComponentModel;
using System.Diagnostics;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@ -426,6 +427,7 @@ public class OpenIddictClientService
Issuer = request.Issuer,
ProviderName = request.ProviderName,
RegistrationId = request.RegistrationId,
TokenEndpointClientCertificate = request.TokenBindingCertificate,
TokenRequest = request.AdditionalTokenRequestParameters
is Dictionary<string, OpenIddictParameter> parameters ? new(parameters) : new()
};
@ -517,6 +519,7 @@ public class OpenIddictClientService
GrantType = request.GrantType,
ProviderName = request.ProviderName,
RegistrationId = request.RegistrationId,
TokenEndpointClientCertificate = request.TokenBindingCertificate,
TokenRequest = request.AdditionalTokenRequestParameters
is Dictionary<string, OpenIddictParameter> parameters ? new(parameters) : new()
};
@ -611,6 +614,7 @@ public class OpenIddictClientService
Issuer = request.Issuer,
ProviderName = request.ProviderName,
RegistrationId = request.RegistrationId,
TokenEndpointClientCertificate = request.TokenBindingCertificate,
TokenRequest = request.AdditionalTokenRequestParameters
is Dictionary<string, OpenIddictParameter> parameters ? new(parameters) : new()
};
@ -795,6 +799,7 @@ public class OpenIddictClientService
Password = request.Password,
ProviderName = request.ProviderName,
RegistrationId = request.RegistrationId,
TokenEndpointClientCertificate = request.TokenBindingCertificate,
TokenRequest = request.AdditionalTokenRequestParameters
is Dictionary<string, OpenIddictParameter> parameters ? new(parameters) : new(),
Username = request.Username
@ -885,6 +890,7 @@ public class OpenIddictClientService
RequestedTokenType = request.RequestedTokenType,
SubjectToken = request.SubjectToken,
SubjectTokenType = request.SubjectTokenType,
TokenEndpointClientCertificate = request.TokenBindingCertificate,
TokenRequest = request.AdditionalTokenRequestParameters
is Dictionary<string, OpenIddictParameter> parameters ? new(parameters) : new()
};
@ -967,6 +973,7 @@ public class OpenIddictClientService
ProviderName = request.ProviderName,
RefreshToken = request.RefreshToken,
RegistrationId = request.RegistrationId,
TokenEndpointClientCertificate = request.TokenBindingCertificate,
TokenRequest = request.AdditionalTokenRequestParameters
is Dictionary<string, OpenIddictParameter> parameters ? new(parameters) : new()
};
@ -1478,11 +1485,13 @@ public class OpenIddictClientService
/// <param name="request">The device authorization request.</param>
/// <param name="uri">The uri of the remote device authorization endpoint.</param>
/// <param name="method">The client authentication method, if applicable.</param>
/// <param name="certificate">The client certificate, if applicable.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The token response.</returns>
internal async ValueTask<OpenIddictResponse> SendDeviceAuthorizationRequestAsync(
OpenIddictClientRegistration registration, OpenIddictConfiguration configuration,
OpenIddictRequest request, Uri uri, string? method, CancellationToken cancellationToken = default)
OpenIddictRequest request, Uri uri, string? method,
X509Certificate2? certificate, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(registration);
ArgumentNullException.ThrowIfNull(configuration);
@ -1518,10 +1527,11 @@ public class OpenIddictClientService
{
CancellationToken = cancellationToken,
ClientAuthenticationMethod = method,
RemoteUri = uri,
Configuration = configuration,
RemoteUri = uri,
Registration = registration,
Request = request
Request = request,
LocalCertificate = certificate
};
await dispatcher.DispatchAsync(context);
@ -1621,11 +1631,13 @@ public class OpenIddictClientService
/// <param name="request">The token request.</param>
/// <param name="uri">The uri of the remote token endpoint.</param>
/// <param name="method">The client authentication method, if applicable.</param>
/// <param name="certificate">The client certificate, if applicable.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The response and the principal extracted from the introspection response.</returns>
internal async ValueTask<(OpenIddictResponse, ClaimsPrincipal)> SendIntrospectionRequestAsync(
OpenIddictClientRegistration registration, OpenIddictConfiguration configuration,
OpenIddictRequest request, Uri uri, string? method, CancellationToken cancellationToken = default)
OpenIddictRequest request, Uri uri, string? method,
X509Certificate2? certificate, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(configuration);
ArgumentNullException.ThrowIfNull(request);
@ -1663,7 +1675,8 @@ public class OpenIddictClientService
Configuration = configuration,
Registration = registration,
RemoteUri = uri,
Request = request
Request = request,
LocalCertificate = certificate
};
await dispatcher.DispatchAsync(context);
@ -1698,7 +1711,7 @@ public class OpenIddictClientService
context.Error, context.ErrorDescription, context.ErrorUri);
}
context.Logger.LogInformation(6192, SR.GetResourceString(SR.ID6192), context.RemoteUri, context.Request);
context.Logger.LogInformation(6190, SR.GetResourceString(SR.ID6190), context.RemoteUri, context.Request);
return context.Request;
}
@ -1725,7 +1738,7 @@ public class OpenIddictClientService
Debug.Assert(context.Response is not null, SR.GetResourceString(SR.ID4007));
context.Logger.LogInformation(6193, SR.GetResourceString(SR.ID6193), context.RemoteUri, context.Response);
context.Logger.LogInformation(6191, SR.GetResourceString(SR.ID6191), context.RemoteUri, context.Response);
return context.Response;
}
@ -1765,11 +1778,13 @@ public class OpenIddictClientService
/// <param name="request">The pushed authorization request.</param>
/// <param name="uri">The uri of the remote pushed authorization endpoint.</param>
/// <param name="method">The client authentication method, if applicable.</param>
/// <param name="certificate">The client certificate, if applicable.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The token response.</returns>
internal async ValueTask<OpenIddictResponse> SendPushedAuthorizationRequestAsync(
OpenIddictClientRegistration registration, OpenIddictConfiguration configuration,
OpenIddictRequest request, Uri uri, string? method, CancellationToken cancellationToken = default)
OpenIddictRequest request, Uri uri, string? method,
X509Certificate2? certificate, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(registration);
ArgumentNullException.ThrowIfNull(configuration);
@ -1808,7 +1823,8 @@ public class OpenIddictClientService
RemoteUri = uri,
Configuration = configuration,
Registration = registration,
Request = request
Request = request,
LocalCertificate = certificate
};
await dispatcher.DispatchAsync(context);
@ -1908,11 +1924,13 @@ public class OpenIddictClientService
/// <param name="request">The token request.</param>
/// <param name="uri">The uri of the remote token endpoint.</param>
/// <param name="method">The client authentication method, if applicable.</param>
/// <param name="certificate">The client certificate, if applicable.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The response extracted from the revocation response.</returns>
internal async ValueTask<OpenIddictResponse> SendRevocationRequestAsync(
OpenIddictClientRegistration registration, OpenIddictConfiguration configuration,
OpenIddictRequest request, Uri uri, string? method, CancellationToken cancellationToken = default)
OpenIddictRequest request, Uri uri, string? method,
X509Certificate2? certificate, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(configuration);
ArgumentNullException.ThrowIfNull(request);
@ -1950,7 +1968,8 @@ public class OpenIddictClientService
Configuration = configuration,
Registration = registration,
RemoteUri = uri,
Request = request
Request = request,
LocalCertificate = certificate
};
await dispatcher.DispatchAsync(context);
@ -1985,7 +2004,7 @@ public class OpenIddictClientService
context.Error, context.ErrorDescription, context.ErrorUri);
}
context.Logger.LogInformation(6192, SR.GetResourceString(SR.ID6192), context.RemoteUri, context.Request);
context.Logger.LogInformation(6290, SR.GetResourceString(SR.ID6290), context.RemoteUri, context.Request);
return context.Request;
}
@ -2012,7 +2031,7 @@ public class OpenIddictClientService
Debug.Assert(context.Response is not null, SR.GetResourceString(SR.ID4007));
context.Logger.LogInformation(6193, SR.GetResourceString(SR.ID6193), context.RemoteUri, context.Response);
context.Logger.LogInformation(6291, SR.GetResourceString(SR.ID6291), context.RemoteUri, context.Response);
return context.Response;
}
@ -2050,11 +2069,13 @@ public class OpenIddictClientService
/// <param name="request">The token request.</param>
/// <param name="uri">The uri of the remote token endpoint.</param>
/// <param name="method">The client authentication method, if applicable.</param>
/// <param name="certificate">The client certificate, if applicable.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The token response.</returns>
internal async ValueTask<OpenIddictResponse> SendTokenRequestAsync(
OpenIddictClientRegistration registration, OpenIddictConfiguration configuration,
OpenIddictRequest request, Uri uri, string? method, CancellationToken cancellationToken = default)
OpenIddictRequest request, Uri uri, string? method,
X509Certificate2? certificate, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(registration);
ArgumentNullException.ThrowIfNull(configuration);
@ -2093,7 +2114,8 @@ public class OpenIddictClientService
Configuration = configuration,
Registration = registration,
RemoteUri = uri,
Request = request
Request = request,
LocalCertificate = certificate
};
await dispatcher.DispatchAsync(context);
@ -2116,7 +2138,8 @@ public class OpenIddictClientService
Configuration = configuration,
Registration = registration,
RemoteUri = uri,
Request = request
Request = request,
LocalCertificate = certificate
};
await dispatcher.DispatchAsync(context);
@ -2141,7 +2164,8 @@ public class OpenIddictClientService
Configuration = configuration,
Registration = registration,
RemoteUri = uri,
Request = request
Request = request,
LocalCertificate = certificate
};
await dispatcher.DispatchAsync(context);
@ -2169,7 +2193,8 @@ public class OpenIddictClientService
Registration = registration,
RemoteUri = uri,
Request = request,
Response = response
Response = response,
LocalCertificate = certificate
};
await dispatcher.DispatchAsync(context);
@ -2192,12 +2217,13 @@ public class OpenIddictClientService
/// <param name="configuration">The server configuration.</param>
/// <param name="request">The userinfo request.</param>
/// <param name="uri">The uri of the remote userinfo endpoint.</param>
/// <param name="methods">The token binding methods to use, if applicable.</param>
/// <param name="certificate">The client certificate, if applicable.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The response and the principal extracted from the userinfo response or the userinfo token.</returns>
internal async ValueTask<(OpenIddictResponse Response, (ClaimsPrincipal? Principal, string? Token))> SendUserInfoRequestAsync(
OpenIddictClientRegistration registration, OpenIddictConfiguration configuration,
OpenIddictRequest request, Uri uri, HashSet<string> methods, CancellationToken cancellationToken = default)
OpenIddictRequest request, Uri uri,
X509Certificate2? certificate, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(registration);
ArgumentNullException.ThrowIfNull(configuration);
@ -2234,11 +2260,10 @@ public class OpenIddictClientService
Configuration = configuration,
RemoteUri = uri,
Registration = registration,
Request = request
Request = request,
LocalCertificate = certificate
};
context.TokenBindingMethods.UnionWith(methods);
await dispatcher.DispatchAsync(context);
if (context.IsRejected)

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

@ -471,25 +471,6 @@ 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>
@ -724,6 +705,69 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
return Store.GetPropertiesAsync(application, cancellationToken);
}
/// <summary>
/// Retrieves the PKI client certificate authentication 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 PKI client certificate authentication policy enforced for this application.
/// </returns>
public virtual async ValueTask<X509ChainPolicy?> GetPublicKeyInfrastructureTlsClientAuthenticationPolicyAsync(
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 intermediate X.509
// certificates (suitable for signing other X.509 certificates) and attach them to the chain policy.
//
// Doing that is essential to support advanced scenarios where an authorization server allows clients
// to authenticate using end certificates signed by the organizations owning them rather than by the
// organization operating the authorization server (e.g clients running on provisioned IoT devices).
if (await GetJsonWebKeySetAsync(application, cancellationToken) is { Keys: [_, ..] keys })
{
X509Certificate2Collection certificates = [];
for (var index = 0; index < keys.Count; index++)
{
if (keys[index] is { Use: JsonWebKeyUseNames.Sig or null or { Length: 0 } } &&
JsonWebKeyConverter.TryConvertToSecurityKey(keys[index], out SecurityKey key) &&
key is X509SecurityKey { Certificate: X509Certificate2 certificate } &&
OpenIddictHelpers.IsCertificateAuthority(certificate) &&
OpenIddictHelpers.HasKeyUsage(certificate, X509KeyUsageFlags.KeyCertSign) &&
!OpenIddictHelpers.IsSelfIssuedCertificate(certificate))
{
certificates.Add(certificate);
}
}
// If one of the intermediate certificates doesn't include a CRL or AIA
// extension, ignore root revocation unknown status errors by default.
//
// This matches the logic used for the base chain policy in the server stack.
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;
}
policy.ExtraStore.AddRange(certificates);
}
return policy;
}
/// <summary>
/// Retrieves the redirect URIs associated with an application.
/// </summary>
@ -759,16 +803,16 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
}
/// <summary>
/// Retrieves the self-signed client certificate chain policy enforced for this application.
/// Retrieves the self-signed client certificate authentication 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.
/// result returns the self-signed client certificate authentication policy enforced for this application.
/// </returns>
public virtual async ValueTask<X509ChainPolicy?> GetSelfSignedClientCertificateChainPolicyAsync(
public virtual async ValueTask<X509ChainPolicy?> GetSelfSignedTlsClientAuthenticationPolicyAsync(
TApplication application, X509ChainPolicy policy, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(application);
@ -776,17 +820,17 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
// 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 a JSON Web Key Set was associated to the client application, extract the end X.509 certificates
// (suitable for digital 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) &&
if (keys[index] is { Use: JsonWebKeyUseNames.Sig or null or { Length: 0 } } &&
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))
OpenIddictHelpers.IsClientAuthenticationCertificate(certificate) &&
OpenIddictHelpers.IsSelfIssuedCertificate(certificate))
{
#if SUPPORTS_X509_CHAIN_POLICY_CUSTOM_TRUST_STORE
policy.CustomTrustStore.Add(certificate);
@ -1287,114 +1331,6 @@ 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>
@ -1502,6 +1438,107 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
return false;
}
/// <summary>
/// Validates the PKI client certificate to ensure it can be used by the specified 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> ValidatePublicKeyInfrastructureTlsClientCertificateAsync(
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 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 redirect_uri to ensure it's associated with an application.
/// </summary>
@ -1569,7 +1606,7 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
}
/// <summary>
/// Validates the self-signed client certificate associated with an application.
/// Validates the self-signed client certificate to ensure it can be used by the specified 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>
@ -1580,7 +1617,7 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
/// 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(
public virtual async ValueTask<bool> ValidateSelfSignedTlsClientCertificateAsync(
TApplication application, X509Certificate2 certificate,
X509ChainPolicy policy, CancellationToken cancellationToken = default)
{
@ -1598,13 +1635,6 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
// 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()
{
@ -1869,10 +1899,6 @@ 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);
@ -1921,6 +1947,10 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
ValueTask<ImmutableDictionary<string, JsonElement>> IOpenIddictApplicationManager.GetPropertiesAsync(object application, CancellationToken cancellationToken)
=> GetPropertiesAsync((TApplication) application, cancellationToken);
/// <inheritdoc/>
ValueTask<X509ChainPolicy?> IOpenIddictApplicationManager.GetPublicKeyInfrastructureTlsClientAuthenticationPolicyAsync(object application, X509ChainPolicy policy, CancellationToken cancellationToken)
=> GetPublicKeyInfrastructureTlsClientAuthenticationPolicyAsync((TApplication) application, policy, cancellationToken);
/// <inheritdoc/>
ValueTask<ImmutableArray<string>> IOpenIddictApplicationManager.GetRedirectUrisAsync(object application, CancellationToken cancellationToken)
=> GetRedirectUrisAsync((TApplication) application, cancellationToken);
@ -1930,8 +1960,8 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
=> GetRequirementsAsync((TApplication) application, cancellationToken);
/// <inheritdoc/>
ValueTask<X509ChainPolicy?> IOpenIddictApplicationManager.GetSelfSignedClientCertificateChainPolicyAsync(object application, X509ChainPolicy policy, CancellationToken cancellationToken)
=> GetSelfSignedClientCertificateChainPolicyAsync((TApplication) application, policy, cancellationToken);
ValueTask<X509ChainPolicy?> IOpenIddictApplicationManager.GetSelfSignedTlsClientAuthenticationPolicyAsync(object application, X509ChainPolicy policy, CancellationToken cancellationToken)
=> GetSelfSignedTlsClientAuthenticationPolicyAsync((TApplication) application, policy, cancellationToken);
/// <inheritdoc/>
ValueTask<ImmutableDictionary<string, string>> IOpenIddictApplicationManager.GetSettingsAsync(object application, CancellationToken cancellationToken)
@ -1993,10 +2023,6 @@ 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);
@ -2005,11 +2031,15 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
ValueTask<bool> IOpenIddictApplicationManager.ValidatePostLogoutRedirectUriAsync(object application, [StringSyntax(StringSyntaxAttribute.Uri)] string uri, CancellationToken cancellationToken)
=> ValidatePostLogoutRedirectUriAsync((TApplication) application, uri, cancellationToken);
/// <inheritdoc/>
ValueTask<bool> IOpenIddictApplicationManager.ValidatePublicKeyInfrastructureTlsClientCertificateAsync(object application, X509Certificate2 certificate, X509ChainPolicy policy, CancellationToken cancellationToken)
=> ValidatePublicKeyInfrastructureTlsClientCertificateAsync((TApplication) application, certificate, policy, cancellationToken);
/// <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);
ValueTask<bool> IOpenIddictApplicationManager.ValidateSelfSignedTlsClientCertificateAsync(object application, X509Certificate2 certificate, X509ChainPolicy policy, CancellationToken cancellationToken)
=> ValidateSelfSignedTlsClientCertificateAsync((TApplication) application, certificate, policy, cancellationToken);
}

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

@ -113,12 +113,12 @@ public sealed class OpenIddictServerAspNetCoreConfiguration : IConfigureOptions<
// 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)
if (options.PublicKeyInfrastructureTlsClientAuthenticationPolicy is not null)
{
options.ClientAuthenticationMethods.Add(ClientAuthenticationMethods.TlsClientAuth);
}
if (options.SelfSignedClientCertificateChainPolicy is not null)
if (options.SelfSignedTlsClientAuthenticationPolicy is not null)
{
options.ClientAuthenticationMethods.Add(ClientAuthenticationMethods.SelfSignedTlsClientAuth);
}

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

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

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

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

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

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

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

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

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

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

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

@ -19,6 +19,7 @@ public static partial class OpenIddictServerAspNetCoreHandlers
*/
ExtractGetOrPostRequest<ExtractUserInfoRequestContext>.Descriptor,
ExtractAccessToken<ExtractUserInfoRequestContext>.Descriptor,
ExtractClientCertificate<ExtractUserInfoRequestContext>.Descriptor,
/*
* UserInfo request handling:

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

@ -627,7 +627,7 @@ public static partial class OpenIddictServerAspNetCoreHandlers
.Build();
/// <inheritdoc/>
public async ValueTask HandleAsync(TContext context)
public ValueTask HandleAsync(TContext context)
{
ArgumentNullException.ThrowIfNull(context);
@ -649,7 +649,7 @@ public static partial class OpenIddictServerAspNetCoreHandlers
description: SR.FormatID2174(ClientAuthenticationMethods.ClientSecretPost),
uri: SR.FormatID8000(SR.ID2174));
return;
return ValueTask.CompletedTask;
}
// Reject requests that use client_secret_basic if support was explicitly disabled in the options.
@ -664,51 +664,22 @@ public static partial class OpenIddictServerAspNetCoreHandlers
description: SR.FormatID2174(ClientAuthenticationMethods.ClientSecretBasic),
uri: SR.FormatID8000(SR.ID2174));
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));
// Note: requests containing a TLS client certificate are never rejected here to support advanced
// scenarios like mTLS token binding without client authentication (in this case, the certificate
// is only used as a proof-of-possession mechanism and not as a client authentication method).
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;
}
}
return ValueTask.CompletedTask;
}
}
/// <summary>
/// Contains the logic responsible for extracting a client authentication certificate from the request context.
/// Contains the logic responsible for extracting a client 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>
public sealed class ExtractClientCertificate<TContext> : IOpenIddictServerHandler<TContext>
where TContext : BaseValidatingContext
{
/// <summary>
@ -717,7 +688,7 @@ public static partial class OpenIddictServerAspNetCoreHandlers
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireHttpRequest>()
.UseSingletonHandler<ExtractClientAuthenticationCertificate<TContext>>()
.UseSingletonHandler<ExtractClientCertificate<TContext>>()
.SetOrder(ValidateClientAuthenticationMethod<TContext>.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
@ -727,8 +698,6 @@ public static partial class OpenIddictServerAspNetCoreHandlers
{
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() ??
@ -738,7 +707,7 @@ public static partial class OpenIddictServerAspNetCoreHandlers
if (request.IsHttps && await request.HttpContext.Connection.GetClientCertificateAsync(
request.HttpContext.RequestAborted) is X509Certificate2 certificate)
{
context.Transaction.ClientCertificate = certificate;
context.Transaction.RemoteCertificate = certificate;
}
}
}
@ -757,7 +726,7 @@ public static partial class OpenIddictServerAspNetCoreHandlers
= OpenIddictServerHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireHttpRequest>()
.UseSingletonHandler<ExtractBasicAuthenticationCredentials<TContext>>()
.SetOrder(ExtractClientAuthenticationCertificate<TContext>.Descriptor.Order + 1_000)
.SetOrder(ExtractClientCertificate<TContext>.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();

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

@ -36,12 +36,12 @@ public sealed class OpenIddictServerOwinConfiguration : IConfigureOptions<OpenId
// 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)
if (options.PublicKeyInfrastructureTlsClientAuthenticationPolicy is not null)
{
options.ClientAuthenticationMethods.Add(ClientAuthenticationMethods.TlsClientAuth);
}
if (options.SelfSignedClientCertificateChainPolicy is not null)
if (options.SelfSignedTlsClientAuthenticationPolicy is not null)
{
options.ClientAuthenticationMethods.Add(ClientAuthenticationMethods.SelfSignedTlsClientAuth);
}

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

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

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

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

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

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

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

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

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

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

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

@ -19,6 +19,7 @@ public static partial class OpenIddictServerOwinHandlers
*/
ExtractGetOrPostRequest<ExtractUserInfoRequestContext>.Descriptor,
ExtractAccessToken<ExtractUserInfoRequestContext>.Descriptor,
ExtractClientCertificate<ExtractUserInfoRequestContext>.Descriptor,
/*
* UserInfo request handling:

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

@ -689,7 +689,7 @@ public static partial class OpenIddictServerOwinHandlers
.Build();
/// <inheritdoc/>
public async ValueTask HandleAsync(TContext context)
public ValueTask HandleAsync(TContext context)
{
ArgumentNullException.ThrowIfNull(context);
@ -711,7 +711,7 @@ public static partial class OpenIddictServerOwinHandlers
description: SR.FormatID2174(ClientAuthenticationMethods.ClientSecretPost),
uri: SR.FormatID8000(SR.ID2174));
return;
return ValueTask.CompletedTask;
}
// Reject requests that use client_secret_basic if support was explicitly disabled in the options.
@ -726,69 +726,22 @@ public static partial class OpenIddictServerOwinHandlers
description: SR.FormatID2174(ClientAuthenticationMethods.ClientSecretBasic),
uri: SR.FormatID8000(SR.ID2174));
return;
}
// 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;
}
return ValueTask.CompletedTask;
}
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;
}
// Note: requests containing a TLS client certificate are never rejected here to support advanced
// scenarios like mTLS token binding without client authentication (in this case, the certificate
// is only used as a proof-of-possession mechanism and not as a client authentication method).
return context.Get<X509Certificate>("ssl.ClientCertificate") is X509Certificate certificate
? certificate as X509Certificate2 ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0498))
: null;
}
return ValueTask.CompletedTask;
}
}
/// <summary>
/// Contains the logic responsible for extracting a client authentication certificate from the request context.
/// Contains the logic responsible for extracting a client 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>
public sealed class ExtractClientCertificate<TContext> : IOpenIddictServerHandler<TContext>
where TContext : BaseValidatingContext
{
/// <summary>
@ -797,7 +750,7 @@ public static partial class OpenIddictServerOwinHandlers
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireOwinRequest>()
.UseSingletonHandler<ExtractClientAuthenticationCertificate<TContext>>()
.UseSingletonHandler<ExtractClientCertificate<TContext>>()
.SetOrder(ValidateClientAuthenticationMethod<TContext>.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
@ -807,8 +760,6 @@ public static partial class OpenIddictServerOwinHandlers
{
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() ??
@ -817,7 +768,7 @@ public static partial class OpenIddictServerOwinHandlers
// 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;
context.Transaction.RemoteCertificate = certificate;
}
static async ValueTask<X509Certificate2?> GetClientCertificateAsync(IOwinContext context)
@ -829,11 +780,6 @@ public static partial class OpenIddictServerOwinHandlers
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;
@ -855,7 +801,7 @@ public static partial class OpenIddictServerOwinHandlers
= OpenIddictServerHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireOwinRequest>()
.UseSingletonHandler<ExtractBasicAuthenticationCredentials<TContext>>()
.SetOrder(ExtractClientAuthenticationCertificate<TContext>.Descriptor.Order + 1_000)
.SetOrder(ExtractClientCertificate<TContext>.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();

105
src/OpenIddict.Server/OpenIddictServerBuilder.cs

@ -4,7 +4,6 @@
* the license and the contributors participating to this project.
*/
using System;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
@ -12,7 +11,6 @@ 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;
@ -2170,6 +2168,56 @@ public sealed class OpenIddictServerBuilder
return SetMtlsTokenEndpointAliasUri(value);
}
/// <summary>
/// Sets the URI listed as the mTLS userinfo 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 SetMtlsUserInfoEndpointAliasUri(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.MtlsUserInfoEndpointAliasUri = uri);
}
/// <summary>
/// Sets the URI listed as the mTLS userinfo 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 SetMtlsUserInfoEndpointAliasUri(
[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 SetMtlsUserInfoEndpointAliasUri(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).
@ -2192,6 +2240,27 @@ public sealed class OpenIddictServerBuilder
public OpenIddictServerBuilder UseReferenceRefreshTokens()
=> Configure(options => options.UseReferenceRefreshTokens = true);
/// <summary>
/// Configures OpenIddict to bind access tokens to the client certificates client certificate
/// sent by public or confidential clients in the TLS handshake of token requests.
/// </summary>
/// <returns>The <see cref="OpenIddictServerBuilder"/> instance.</returns>
public OpenIddictServerBuilder UseClientCertificateBoundAccessTokens()
=> Configure(options => options.UseClientCertificateBoundAccessTokens = true);
/// <summary>
/// Configures OpenIddict to bind refresh tokens to the client certificates client
/// certificate sent by public clients in the TLS handshake of token requests.
/// </summary>
/// <remarks>
/// Note: refresh tokens are only bound to the client certificate when the client
/// is a public application, as refresh tokens issued to confidential applications
/// are already sender-constrained via standard client authentication.
/// </remarks>
/// <returns>The <see cref="OpenIddictServerBuilder"/> instance.</returns>
public OpenIddictServerBuilder UseClientCertificateBoundRefreshTokens()
=> Configure(options => options.UseClientCertificateBoundRefreshTokens = true);
/// <summary>
/// Enables authorization request storage, so that authorization requests
/// are automatically stored in the token store, which allows flowing
@ -2218,8 +2287,8 @@ public sealed class OpenIddictServerBuilder
/// <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 => { });
public OpenIddictServerBuilder EnablePublicKeyInfrastructureTlsClientAuthentication(X509Certificate2Collection certificates)
=> EnablePublicKeyInfrastructureTlsClientAuthentication(certificates, static policy => { });
/// <summary>
/// Configures OpenIddict to enable PKI client certificate authentication (mTLS) and trust
@ -2229,7 +2298,7 @@ public sealed class OpenIddictServerBuilder
/// <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(
public OpenIddictServerBuilder EnablePublicKeyInfrastructureTlsClientAuthentication(
X509Certificate2Collection certificates, Action<X509ChainPolicy> configuration)
{
ArgumentNullException.ThrowIfNull(certificates);
@ -2261,8 +2330,8 @@ public sealed class OpenIddictServerBuilder
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.
// Note: by default, OpenIddict requires that end certificates used for TLS client
// authentication explicitly list client authentication as an allowed extended key usage.
ApplicationPolicy = { new Oid(ObjectIdentifiers.ExtendedKeyUsages.ClientAuthentication) },
TrustMode = X509ChainTrustMode.CustomRootTrust
};
@ -2322,38 +2391,38 @@ public sealed class OpenIddictServerBuilder
throw new InvalidOperationException(SR.GetResourceString(SR.ID0509));
}
return Configure(options => options.ClientCertificateChainPolicy = policy);
return Configure(options => options.PublicKeyInfrastructureTlsClientAuthenticationPolicy = policy);
#else
throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0508));
#endif
}
/// <summary>
/// Configures OpenIddict to enable self-signed client certificate authentication (mTLS).
/// Configures OpenIddict to enable self-signed TLS client authentication (mTLS).
/// </summary>
/// <returns>The <see cref="OpenIddictServerBuilder"/> instance.</returns>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public OpenIddictServerBuilder EnableSelfSignedClientCertificateAuthentication()
=> EnableSelfSignedClientCertificateAuthentication(static policy => { });
public OpenIddictServerBuilder EnableSelfSignedTlsClientAuthentication()
=> EnableSelfSignedTlsClientAuthentication(static policy => { });
/// <summary>
/// Configures OpenIddict to enable self-signed client certificate authentication (mTLS).
/// Configures OpenIddict to enable self-signed TLS client 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)
public OpenIddictServerBuilder EnableSelfSignedTlsClientAuthentication(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.
// Note: by default, OpenIddict requires that end certificates used for TLS client
// 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.
// Note: self-signed TLS client certificates typically do not 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
};
@ -2370,7 +2439,7 @@ public sealed class OpenIddictServerBuilder
throw new InvalidOperationException(SR.GetResourceString(SR.ID0509));
}
return Configure(options => options.SelfSignedClientCertificateChainPolicy = policy);
return Configure(options => options.SelfSignedTlsClientAuthenticationPolicy = policy);
#else
throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0508));
#endif

49
src/OpenIddict.Server/OpenIddictServerConfiguration.cs

@ -167,13 +167,13 @@ public sealed class OpenIddictServerConfiguration : IPostConfigureOptions<OpenId
// 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)
options.PublicKeyInfrastructureTlsClientAuthenticationPolicy is null)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0505));
}
if (options.ClientAuthenticationMethods.Contains(ClientAuthenticationMethods.SelfSignedTlsClientAuth) &&
options.SelfSignedClientCertificateChainPolicy is null)
options.SelfSignedTlsClientAuthenticationPolicy is null)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0506));
}
@ -280,7 +280,8 @@ public sealed class OpenIddictServerConfiguration : IPostConfigureOptions<OpenId
!TryValidateMtlsEndpointAlias(options.MtlsIntrospectionEndpointAliasUri) ||
!TryValidateMtlsEndpointAlias(options.MtlsPushedAuthorizationEndpointAliasUri) ||
!TryValidateMtlsEndpointAlias(options.MtlsRevocationEndpointAliasUri) ||
!TryValidateMtlsEndpointAlias(options.MtlsTokenEndpointAliasUri))
!TryValidateMtlsEndpointAlias(options.MtlsTokenEndpointAliasUri) ||
!TryValidateMtlsEndpointAlias(options.MtlsUserInfoEndpointAliasUri))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0499));
}
@ -290,7 +291,8 @@ public sealed class OpenIddictServerConfiguration : IPostConfigureOptions<OpenId
(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))
(options.MtlsTokenEndpointAliasUri is not null && options.TokenEndpointUris.Count is 0) ||
(options.MtlsUserInfoEndpointAliasUri is not null && options.UserInfoEndpointUris.Count is 0))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0510));
}
@ -302,16 +304,17 @@ public sealed class OpenIddictServerConfiguration : IPostConfigureOptions<OpenId
options.MtlsIntrospectionEndpointAliasUri is not null ||
options.MtlsPushedAuthorizationEndpointAliasUri is not null ||
options.MtlsRevocationEndpointAliasUri is not null ||
options.MtlsTokenEndpointAliasUri is not null))
options.MtlsTokenEndpointAliasUri is not null ||
options.MtlsUserInfoEndpointAliasUri is not null))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0500));
}
// Ensure no end certificate was included in the PKI client certificate
// Ensure no end certificate was included in the PKI TLS client authentication
// chain policy and that none of the certificates contains a private key.
if (options.ClientCertificateChainPolicy is not null)
if (options.PublicKeyInfrastructureTlsClientAuthenticationPolicy is not null)
{
if (options.ClientCertificateChainPolicy.ExtraStore.Cast<X509Certificate2>()
if (options.PublicKeyInfrastructureTlsClientAuthenticationPolicy.ExtraStore.Cast<X509Certificate2>()
.Any(static certificate =>
!OpenIddictHelpers.IsCertificateAuthority(certificate) ||
!OpenIddictHelpers.HasKeyUsage(certificate, X509KeyUsageFlags.KeyCertSign)))
@ -319,14 +322,14 @@ public sealed class OpenIddictServerConfiguration : IPostConfigureOptions<OpenId
throw new InvalidOperationException(SR.GetResourceString(SR.ID0501));
}
if (options.ClientCertificateChainPolicy.ExtraStore.Cast<X509Certificate2>()
if (options.PublicKeyInfrastructureTlsClientAuthenticationPolicy.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>()
if (options.PublicKeyInfrastructureTlsClientAuthenticationPolicy.CustomTrustStore.Cast<X509Certificate2>()
.Any(static certificate =>
!OpenIddictHelpers.IsCertificateAuthority(certificate) ||
!OpenIddictHelpers.HasKeyUsage(certificate, X509KeyUsageFlags.KeyCertSign)))
@ -334,7 +337,7 @@ public sealed class OpenIddictServerConfiguration : IPostConfigureOptions<OpenId
throw new InvalidOperationException(SR.GetResourceString(SR.ID0501));
}
if (options.ClientCertificateChainPolicy.CustomTrustStore.Cast<X509Certificate2>()
if (options.PublicKeyInfrastructureTlsClientAuthenticationPolicy.CustomTrustStore.Cast<X509Certificate2>()
.Any(static certificate => certificate.HasPrivateKey))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0511));
@ -342,16 +345,16 @@ public sealed class OpenIddictServerConfiguration : IPostConfigureOptions<OpenId
#endif
}
// Ensure the self-signed client certificate chain policy doesn't contain any certificate.
if (options.SelfSignedClientCertificateChainPolicy is not null)
// Ensure the self-signed TLS client authentication chain policy doesn't contain any certificate.
if (options.SelfSignedTlsClientAuthenticationPolicy is not null)
{
if (options.SelfSignedClientCertificateChainPolicy.ExtraStore.Cast<X509Certificate2>().Any())
if (options.SelfSignedTlsClientAuthenticationPolicy.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())
if (options.SelfSignedTlsClientAuthenticationPolicy.CustomTrustStore.Cast<X509Certificate2>().Any())
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0502));
}
@ -365,7 +368,7 @@ public sealed class OpenIddictServerConfiguration : IPostConfigureOptions<OpenId
if (options.AuthorizationEndpointUris.Count is not 0 && !options.Handlers.Exists(static descriptor =>
descriptor.ContextType == typeof(ValidateAuthorizationRequestContext) &&
descriptor.Type == OpenIddictServerHandlerType.Custom &&
descriptor.Type is OpenIddictServerHandlerType.Custom &&
descriptor.FilterTypes.All(type => !typeof(RequireDegradedModeDisabled).IsAssignableFrom(type))))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0089));
@ -374,7 +377,7 @@ public sealed class OpenIddictServerConfiguration : IPostConfigureOptions<OpenId
if (options.DeviceAuthorizationEndpointUris.Count is not 0 && !options.Handlers.Exists(static descriptor =>
(descriptor.ContextType == typeof(ValidateDeviceAuthorizationRequestContext) ||
descriptor.ContextType == typeof(ProcessAuthenticationContext)) &&
descriptor.Type == OpenIddictServerHandlerType.Custom &&
descriptor.Type is OpenIddictServerHandlerType.Custom &&
descriptor.FilterTypes.All(type => !typeof(RequireDegradedModeDisabled).IsAssignableFrom(type))))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0090));
@ -383,7 +386,7 @@ public sealed class OpenIddictServerConfiguration : IPostConfigureOptions<OpenId
if (options.IntrospectionEndpointUris.Count is not 0 && !options.Handlers.Exists(static descriptor =>
(descriptor.ContextType == typeof(ValidateIntrospectionRequestContext) ||
descriptor.ContextType == typeof(ProcessAuthenticationContext)) &&
descriptor.Type == OpenIddictServerHandlerType.Custom &&
descriptor.Type is OpenIddictServerHandlerType.Custom &&
descriptor.FilterTypes.All(type => !typeof(RequireDegradedModeDisabled).IsAssignableFrom(type))))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0091));
@ -391,7 +394,7 @@ public sealed class OpenIddictServerConfiguration : IPostConfigureOptions<OpenId
if (options.EndSessionEndpointUris.Count is not 0 && !options.Handlers.Exists(static descriptor =>
descriptor.ContextType == typeof(ValidateEndSessionRequestContext) &&
descriptor.Type == OpenIddictServerHandlerType.Custom &&
descriptor.Type is OpenIddictServerHandlerType.Custom &&
descriptor.FilterTypes.All(type => !typeof(RequireDegradedModeDisabled).IsAssignableFrom(type))))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0092));
@ -400,7 +403,7 @@ public sealed class OpenIddictServerConfiguration : IPostConfigureOptions<OpenId
if (options.PushedAuthorizationEndpointUris.Count is not 0 && !options.Handlers.Exists(static descriptor =>
(descriptor.ContextType == typeof(ValidatePushedAuthorizationRequestContext) ||
descriptor.ContextType == typeof(ProcessAuthenticationContext)) &&
descriptor.Type == OpenIddictServerHandlerType.Custom &&
descriptor.Type is OpenIddictServerHandlerType.Custom &&
descriptor.FilterTypes.All(type => !typeof(RequireDegradedModeDisabled).IsAssignableFrom(type))))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0467));
@ -409,7 +412,7 @@ public sealed class OpenIddictServerConfiguration : IPostConfigureOptions<OpenId
if (options.RevocationEndpointUris.Count is not 0 && !options.Handlers.Exists(static descriptor =>
(descriptor.ContextType == typeof(ValidateRevocationRequestContext) ||
descriptor.ContextType == typeof(ProcessAuthenticationContext)) &&
descriptor.Type == OpenIddictServerHandlerType.Custom &&
descriptor.Type is OpenIddictServerHandlerType.Custom &&
descriptor.FilterTypes.All(type => !typeof(RequireDegradedModeDisabled).IsAssignableFrom(type))))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0093));
@ -418,7 +421,7 @@ public sealed class OpenIddictServerConfiguration : IPostConfigureOptions<OpenId
if (options.TokenEndpointUris.Count is not 0 && !options.Handlers.Exists(static descriptor =>
(descriptor.ContextType == typeof(ValidateTokenRequestContext) ||
descriptor.ContextType == typeof(ProcessAuthenticationContext)) &&
descriptor.Type == OpenIddictServerHandlerType.Custom &&
descriptor.Type is OpenIddictServerHandlerType.Custom &&
descriptor.FilterTypes.All(type => !typeof(RequireDegradedModeDisabled).IsAssignableFrom(type))))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0094));
@ -426,7 +429,7 @@ public sealed class OpenIddictServerConfiguration : IPostConfigureOptions<OpenId
if (options.EndUserVerificationEndpointUris.Count is not 0 && !options.Handlers.Exists(static descriptor =>
descriptor.ContextType == typeof(ValidateEndUserVerificationRequestContext) &&
descriptor.Type == OpenIddictServerHandlerType.Custom &&
descriptor.Type is OpenIddictServerHandlerType.Custom &&
descriptor.FilterTypes.All(type => !typeof(RequireDegradedModeDisabled).IsAssignableFrom(type))))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0095));

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

@ -161,6 +161,11 @@ public static partial class OpenIddictServerEvents
/// </summary>
public Uri? MtlsTokenEndpointAlias { get; set; }
/// <summary>
/// Gets or sets the mTLS userinfo endpoint alias URI.
/// </summary>
public Uri? MtlsUserInfoEndpointAlias { get; set; }
/// <summary>
/// Gets the list of claims supported by the authorization server.
/// </summary>
@ -247,6 +252,11 @@ public static partial class OpenIddictServerEvents
/// Gets or sets a boolean indicating whether pushed authorization requests are required.
/// </summary>
public bool RequirePushedAuthorizationRequests { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether access tokens are bound to client certificates.
/// </summary>
public bool TlsClientCertificateBoundAccessTokens { get; set; }
}
/// <summary>

6
src/OpenIddict.Server/OpenIddictServerEvents.Introspection.cs

@ -5,6 +5,7 @@
*/
using System.Security.Claims;
using System.Text.Json.Nodes;
namespace OpenIddict.Server;
@ -113,6 +114,11 @@ public static partial class OpenIddictServerEvents
/// </summary>
public string? ClientId { get; set; }
/// <summary>
/// Gets or sets the "cnf" claim returned to the caller, if applicable.
/// </summary>
public JsonObject? Confirmation { get; set; }
/// <summary>
/// Gets or sets the "exp" claim
/// returned to the caller, if applicable.

5
src/OpenIddict.Server/OpenIddictServerEvents.Protection.cs

@ -144,6 +144,11 @@ public static partial class OpenIddictServerEvents
/// </summary>
public bool DisablePresenterValidation { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether proof-of-possession validation is disabled.
/// </summary>
public bool DisableProofOfPossessionValidation { get; set; }
/// <summary>
/// Gets or sets the security token handler used to validate the token.
/// </summary>

9
src/OpenIddict.Server/OpenIddictServerEvents.cs

@ -765,15 +765,6 @@ 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

@ -91,6 +91,7 @@ public static class OpenIddictServerExtensions
builder.Services.TryAddSingleton<RequireTokenLifetimeValidationEnabled>();
builder.Services.TryAddSingleton<RequireTokenPayloadPersisted>();
builder.Services.TryAddSingleton<RequireTokenPresenterValidationEnabled>();
builder.Services.TryAddSingleton<RequireTokenProofOfPossessionValidationEnabled>();
builder.Services.TryAddSingleton<RequireTokenRequest>();
builder.Services.TryAddSingleton<RequireTokenStorageEnabled>();
builder.Services.TryAddSingleton<RequireUserCodeGenerated>();

18
src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs

@ -180,7 +180,7 @@ public static class OpenIddictServerHandlerFilters
}
/// <summary>
/// Represents a filter that excludes the associated handlers if no client authentication certificate is available.
/// Represents a filter that excludes the associated handlers if no client certificate is available.
/// </summary>
public sealed class RequireClientCertificate : IOpenIddictServerHandlerFilter<ProcessAuthenticationContext>
{
@ -189,7 +189,7 @@ public static class OpenIddictServerHandlerFilters
{
ArgumentNullException.ThrowIfNull(context);
return new(context.ClientCertificate is not null);
return new(context.Transaction.RemoteCertificate is not null);
}
}
@ -767,6 +767,20 @@ public static class OpenIddictServerHandlerFilters
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if token proof-of-possession validation was disabled.
/// </summary>
public sealed class RequireTokenProofOfPossessionValidationEnabled : IOpenIddictServerHandlerFilter<ValidateTokenContext>
{
/// <inheritdoc/>
public ValueTask<bool> IsActiveAsync(ValidateTokenContext context)
{
ArgumentNullException.ThrowIfNull(context);
return new(!context.DisableProofOfPossessionValidation);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if the request is not a token request.
/// </summary>

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

@ -252,7 +252,8 @@ public static partial class OpenIddictServerHandlers
[Metadata.RevocationEndpointAuthMethodsSupported] = notification.RevocationEndpointAuthenticationMethods.ToImmutableArray<string?>(),
[Metadata.DeviceAuthorizationEndpointAuthMethodsSupported] = notification.DeviceAuthorizationEndpointAuthenticationMethods.ToImmutableArray<string?>(),
[Metadata.PushedAuthorizationRequestEndpointAuthMethodsSupported] = notification.PushedAuthorizationEndpointAuthenticationMethods.ToImmutableArray<string?>(),
[Metadata.RequirePushedAuthorizationRequests] = notification.RequirePushedAuthorizationRequests
[Metadata.RequirePushedAuthorizationRequests] = notification.RequirePushedAuthorizationRequests,
[Metadata.TlsClientCertificateBoundAccessTokens] = notification.TlsClientCertificateBoundAccessTokens
};
foreach (var metadata in notification.Metadata)
@ -291,6 +292,11 @@ public static partial class OpenIddictServerHandlers
node.Add(Metadata.TokenEndpoint, context.MtlsTokenEndpointAlias.AbsoluteUri);
}
if (context.MtlsUserInfoEndpointAlias is not null)
{
node.Add(Metadata.UserInfoEndpoint, context.MtlsUserInfoEndpointAlias.AbsoluteUri);
}
return node;
}
}
@ -426,6 +432,9 @@ public static partial class OpenIddictServerHandlers
context.MtlsTokenEndpointAlias ??= OpenIddictHelpers.CreateAbsoluteUri(
context.BaseUri, context.Options.MtlsTokenEndpointAliasUri);
context.MtlsUserInfoEndpointAlias ??= OpenIddictHelpers.CreateAbsoluteUri(
context.BaseUri, context.Options.MtlsUserInfoEndpointAliasUri);
context.PushedAuthorizationEndpoint ??= OpenIddictHelpers.CreateAbsoluteUri(
context.BaseUri, context.Options.PushedAuthorizationEndpointUris.FirstOrDefault());
@ -812,6 +821,7 @@ public static partial class OpenIddictServerHandlers
ArgumentNullException.ThrowIfNull(context);
context.RequirePushedAuthorizationRequests = context.Options.RequirePushedAuthorizationRequests;
context.TlsClientCertificateBoundAccessTokens = context.Options.UseClientCertificateBoundAccessTokens;
return ValueTask.CompletedTask;
}
@ -842,7 +852,6 @@ public static partial class OpenIddictServerHandlers
context.Metadata[Metadata.ClaimsParameterSupported] = false;
context.Metadata[Metadata.RequestParameterSupported] = false;
context.Metadata[Metadata.RequestUriParameterSupported] = false;
context.Metadata[Metadata.TlsClientCertificateBoundAccessTokens] = false;
// As of 3.2.0, OpenIddict automatically returns an "iss" parameter containing its identity as
// part of authorization responses to help clients mitigate mix-up attacks. For more information,

42
src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs

@ -11,6 +11,7 @@ using System.Security.Claims;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
@ -235,6 +236,11 @@ public static partial class OpenIddictServerHandlers
[Claims.ClientId] = notification.ClientId
};
if (notification.Confirmation is not null)
{
response[Claims.Confirmation] = notification.Confirmation;
}
if (notification.IssuedAt is not null)
{
response[Claims.IssuedAt] = EpochTime.GetIntDate(notification.IssuedAt.Value.UtcDateTime);
@ -724,6 +730,17 @@ public static partial class OpenIddictServerHandlers
_ => null
};
context.Confirmation = context.GenericTokenPrincipal.GetTokenType() switch
{
// For access tokens that contain a confirmation claim, return it to the caller so
// that resource servers can verify the proof-of-possession when the token is used.
TokenTypeIdentifiers.AccessToken when context.GenericTokenPrincipal.GetClaim(
Claims.Confirmation) is { Length: > 0 } value => JsonObject.Parse(value) as JsonObject ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID2199)),
_ => null
};
context.IssuedAt = context.NotBefore = context.GenericTokenPrincipal.GetCreationDate();
context.ExpiresAt = context.GenericTokenPrincipal.GetExpirationDate();
@ -732,13 +749,20 @@ public static partial class OpenIddictServerHandlers
context.ClientId = context.GenericTokenPrincipal.GetClaim(Claims.ClientId) ??
context.GenericTokenPrincipal.FindFirst(Claims.Private.Presenter)?.Value;
// Note: only set "token_type" when the received token is an access token.
// See https://tools.ietf.org/html/rfc7662#section-2.2
// and https://tools.ietf.org/html/rfc6749#section-5.1 for more information.
if (context.GenericTokenPrincipal.HasTokenType(TokenTypeIdentifiers.AccessToken))
context.TokenType = context.GenericTokenPrincipal.GetTokenType() switch
{
context.TokenType = TokenTypes.Bearer;
}
// Note: only set "token_type" when the received token is an access token and doesn't
// require using a proof-of-possession: while specifications like DPoP define a specific
// token type for DPoP-protected access tokens, the mTLS specification doesn't define one
// for mutual TLS-bound access tokens. And in this case, not returning a "token_type" node is
// a better option than always returning "Bearer" even when the token is not a bearer token.
//
// See https://tools.ietf.org/html/rfc7662#section-2.2
// and https://tools.ietf.org/html/rfc6749#section-5.1 for more information.
TokenTypeIdentifiers.AccessToken when context.Confirmation is null => TokenTypes.Bearer,
_ => null
};
return ValueTask.CompletedTask;
}
@ -811,9 +835,9 @@ public static partial class OpenIddictServerHandlers
// Exclude standard claims, that are already handled via strongly-typed properties.
// Make sure to always update this list when adding new built-in claim properties.
var type = group.Key;
if (type is Claims.Audience or Claims.ExpiresAt or Claims.IssuedAt or
Claims.Issuer or Claims.NotBefore or Claims.Scope or
Claims.Subject or Claims.TokenType or Claims.TokenUsage)
if (type is Claims.Audience or Claims.Confirmation or Claims.ExpiresAt or Claims.IssuedAt or
Claims.Issuer or Claims.NotBefore or Claims.Scope or Claims.Subject or
Claims.TokenType or Claims.TokenUsage)
{
continue;
}

88
src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs

@ -7,7 +7,10 @@
using System.Collections.Immutable;
using System.Diagnostics;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
using System.Text.Json.Nodes;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@ -36,6 +39,7 @@ public static partial class OpenIddictServerHandlers
ValidateExpirationDate.Descriptor,
ValidatePresenters.Descriptor,
ValidateAudiences.Descriptor,
ValidateProofOfPossession.Descriptor,
ValidateTokenEntry.Descriptor,
ValidateAuthorizationEntry.Descriptor,
@ -1080,6 +1084,88 @@ public static partial class OpenIddictServerHandlers
}
}
/// <summary>
/// Contains the logic responsible for rejecting tokens for which no valid proof of possession was received.
/// </summary>
public sealed class ValidateProofOfPossession : IOpenIddictServerHandler<ValidateTokenContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateTokenContext>()
.AddFilter<RequireTokenProofOfPossessionValidationEnabled>()
.UseSingletonHandler<ValidateProofOfPossession>()
.SetOrder(ValidateAudiences.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ValidateTokenContext context)
{
ArgumentNullException.ThrowIfNull(context);
Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// Try to resolve the confirmation claim from the principal. If no such claim can be found,
// this indicates that the token is a bearer token and doesn't require a proof of possession.
var confirmation = context.Principal.GetClaim(Claims.Confirmation);
if (string.IsNullOrEmpty(confirmation))
{
return ValueTask.CompletedTask;
}
if (JsonObject.Parse(confirmation) is not JsonObject node)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID2199));
}
if (node.ContainsKey(JsonWebKeyParameterNames.X5tS256))
{
var thumbprint = (string?) node[JsonWebKeyParameterNames.X5tS256];
if (string.IsNullOrEmpty(thumbprint))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID2200));
}
// If no client certificate was provided, return an error as no
// proof-of-possession can be validated without the client certificate.
if (context.Transaction.RemoteCertificate is not X509Certificate2 certificate)
{
context.Logger.LogInformation(6282, SR.GetResourceString(SR.ID6282));
context.Reject(
error: Errors.InvalidToken,
description: SR.GetResourceString(SR.ID2203),
uri: SR.FormatID8000(SR.ID2203));
return ValueTask.CompletedTask;
}
// If the thumbprint of the certificate doesn't match the hash
// resolved from the confirmation claim, return an error.
var hash = Base64UrlEncoder.Encode(OpenIddictHelpers.ComputeSha256Hash(certificate.RawData));
if (!OpenIddictHelpers.FixedTimeEquals(
left : MemoryMarshal.AsBytes<char>(hash),
right: MemoryMarshal.AsBytes<char>(thumbprint)))
{
context.Logger.LogInformation(6289, SR.GetResourceString(SR.ID6289));
context.Reject(
error: Errors.InvalidToken,
description: SR.GetResourceString(SR.ID2204),
uri: SR.FormatID8000(SR.ID2204));
return ValueTask.CompletedTask;
}
return ValueTask.CompletedTask;
}
throw new InvalidOperationException(SR.GetResourceString(SR.ID2196));
}
}
/// <summary>
/// Contains the logic responsible for rejecting tokens whose
/// associated token entry is no longer valid (e.g was revoked).
@ -1103,7 +1189,7 @@ public static partial class OpenIddictServerHandlers
.AddFilter<RequireTokenStorageEnabled>()
.AddFilter<RequireTokenIdResolved>()
.UseScopedHandler<ValidateTokenEntry>()
.SetOrder(ValidateExpirationDate.Descriptor.Order + 1_000)
.SetOrder(ValidateProofOfPossession.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();

400
src/OpenIddict.Server/OpenIddictServerHandlers.cs

@ -8,11 +8,11 @@ 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 System.Text.Json.Nodes;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@ -1119,19 +1119,6 @@ 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))
{
@ -1158,12 +1145,16 @@ public static partial class OpenIddictServerHandlers
return;
}
// Note: requests containing a TLS client certificate are never rejected here to support advanced
// scenarios like mTLS token binding without client authentication (in this case, the certificate
// is only used as a proof-of-possession mechanism and not as a client authentication method).
return;
}
// Confidential applications MUST authenticate to protect them from impersonation attacks.
if (context.ClientAssertionPrincipal is null &&
context.ClientCertificate is null && string.IsNullOrEmpty(context.ClientSecret))
context.Transaction.RemoteCertificate is null && string.IsNullOrEmpty(context.ClientSecret))
{
context.Logger.LogInformation(6224, SR.GetResourceString(SR.ID6224), context.ClientId);
@ -1244,7 +1235,8 @@ public static partial class OpenIddictServerHandlers
}
/// <summary>
/// Contains the logic responsible for validating the TLS client certificate used for client authentication, if applicable.
/// Contains the logic responsible for validating the client certificate
/// used for client authentication or token binding, if applicable.
/// </summary>
public sealed class ValidateClientCertificate : IOpenIddictServerHandler<ProcessAuthenticationContext>
{
@ -1274,9 +1266,9 @@ public static partial class OpenIddictServerHandlers
ArgumentNullException.ThrowIfNull(context);
Debug.Assert(!string.IsNullOrEmpty(context.ClientId), SR.FormatID4000(Parameters.ClientId));
Debug.Assert(context.ClientCertificate is not null, SR.GetResourceString(SR.ID4020));
Debug.Assert(context.Transaction.RemoteCertificate is not null, SR.GetResourceString(SR.ID4020));
// Don't validate the client secret on endpoints that don't support client authentication.
// Don't validate the client certificate on endpoints that don't support client authentication/token binding.
if (context.EndpointType is OpenIddictServerEndpointType.Authorization or
OpenIddictServerEndpointType.EndSession or
OpenIddictServerEndpointType.EndUserVerification or
@ -1288,27 +1280,56 @@ public static partial class OpenIddictServerHandlers
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
// ValidateSelfSignedTlsClientCertificateAsync() and
// ValidatePublicKeyInfrastructureTlsClientCertificateAsync() APIs
// once the chain is built to validate whether the certificate is self-signed or not.
if (OpenIddictHelpers.IsSelfIssuedCertificate(context.ClientCertificate))
if (OpenIddictHelpers.IsSelfIssuedCertificate(context.Transaction.RemoteCertificate))
{
if (context.Options.SelfSignedClientCertificateChainPolicy is null)
if (context.Options.SelfSignedTlsClientAuthenticationPolicy 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))
if (await _applicationManager.GetSelfSignedTlsClientAuthenticationPolicyAsync(
application, context.Options.SelfSignedTlsClientAuthenticationPolicy) is not X509ChainPolicy 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;
}
// Note: OpenIddict allows using self-signed TLS client certificates for both client authentication
// and token binding: if the client application is not confidential, the client certificate cannot
// be used for client authentication but can be used for token binding. In the later case, the client
// certificate is not expected to be validated against the list of self-signed certificates attached
// to the application and is generally generated on-the-fly (e.g one per user or authorization flow).
//
// To allow validating such certificates, the chain policy is amended to consider the specified
// self-signed certificate as a trusted root and basically disable chain validation while still
// validating the other aspects of the certificate (e.g expiration date, key usage, etc).
if (await _applicationManager.HasClientTypeAsync(application, ClientTypes.Public))
{
// Always clone the X.509 chain policy to ensure the original instance is never mutated.
policy = policy.Clone();
#if SUPPORTS_X509_CHAIN_POLICY_CUSTOM_TRUST_STORE
policy.CustomTrustStore.Add(context.Transaction.RemoteCertificate);
#else
policy.ExtraStore.Add(context.Transaction.RemoteCertificate);
#endif
}
if (!await _applicationManager.ValidateSelfSignedTlsClientCertificateAsync(
application, context.Transaction.RemoteCertificate, policy))
{
context.Logger.LogInformation(6283, SR.GetResourceString(SR.ID6283), context.ClientId);
@ -1323,13 +1344,15 @@ public static partial class OpenIddictServerHandlers
else
{
if (context.Options.ClientCertificateChainPolicy is null)
if (context.Options.PublicKeyInfrastructureTlsClientAuthenticationPolicy 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))
if (await _applicationManager.GetPublicKeyInfrastructureTlsClientAuthenticationPolicyAsync(
application, context.Options.PublicKeyInfrastructureTlsClientAuthenticationPolicy) is not X509ChainPolicy policy ||
!await _applicationManager.ValidatePublicKeyInfrastructureTlsClientCertificateAsync(
application, context.Transaction.RemoteCertificate, policy))
{
context.Logger.LogInformation(6284, SR.GetResourceString(SR.ID6284), context.ClientId);
@ -1734,6 +1757,9 @@ public static partial class OpenIddictServerHandlers
OpenIddictServerEndpointType.Revocation,
DisablePresenterValidation = context.EndpointType is OpenIddictServerEndpointType.Introspection or
OpenIddictServerEndpointType.Revocation,
// Proof-of-possession validation is disabled for the introspection and revocation endpoints.
DisableProofOfPossessionValidation = context.EndpointType is OpenIddictServerEndpointType.Introspection or
OpenIddictServerEndpointType.Revocation,
Token = context.GenericToken,
TokenTypeHint = context.GenericTokenTypeHint,
@ -3396,33 +3422,22 @@ public static partial class OpenIddictServerHandlers
// Actors identities are also filtered (delegation scenarios).
var principal = context.Principal.Clone(claim =>
{
// Never exclude the subject and authorization identifier claims.
// Always include the following claims:
if (string.Equals(claim.Type, Claims.Subject, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.Private.AuthorizationId, StringComparison.OrdinalIgnoreCase))
{
return true;
}
// Never exclude the presenters and scope private claims.
if (string.Equals(claim.Type, Claims.Private.Presenter, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.Private.AuthorizationId, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.Private.Presenter, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.Private.Scope, StringComparison.OrdinalIgnoreCase))
{
return true;
}
// Never include the public or internal token identifiers to ensure the identifiers
// that are automatically inherited from the parent token are not reused for the new token.
// Never include the the following claims to ensure they are not inherited from the parent token:
if (string.Equals(claim.Type, Claims.JwtId, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.Private.TokenId, StringComparison.OrdinalIgnoreCase))
{
return false;
}
// Never include the creation and expiration dates that are automatically
// inherited from the parent token are not reused for the new token.
if (string.Equals(claim.Type, Claims.ExpiresAt, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.Private.TokenId, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.ExpiresAt, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.IssuedAt, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.NotBefore, StringComparison.OrdinalIgnoreCase))
string.Equals(claim.Type, Claims.NotBefore, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.Confirmation, StringComparison.OrdinalIgnoreCase))
{
return false;
}
@ -3510,7 +3525,21 @@ public static partial class OpenIddictServerHandlers
context.Logger.LogDebug(6010, SR.GetResourceString(SR.ID6010), scopes);
}
// If certificate-bound access tokens are enabled and a client certificate was used, bind the access
// token to the certificate by storing a confirmation claim containing the certificate thumbprint.
if (context.Options.UseClientCertificateBoundAccessTokens &&
context.Transaction.RemoteCertificate is X509Certificate2 certificate)
{
principal.SetClaim(Claims.Confirmation, CreateConfirmationClaim(certificate));
}
context.AccessTokenPrincipal = principal;
static JsonNode CreateConfirmationClaim(X509Certificate2 certificate) => new JsonObject
{
[JsonWebKeyParameterNames.X5tS256] = Base64UrlEncoder.Encode(
OpenIddictHelpers.ComputeSha256Hash(certificate.RawData))
};
}
}
@ -3557,19 +3586,13 @@ public static partial class OpenIddictServerHandlers
// Actors identities are also filtered (delegation scenarios).
var principal = context.Principal.Clone(claim =>
{
// Never include the public or internal token identifiers to ensure the identifiers
// that are automatically inherited from the parent token are not reused for the new token.
// Never include the the following claims to ensure they are not inherited from the parent token:
if (string.Equals(claim.Type, Claims.JwtId, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.Private.TokenId, StringComparison.OrdinalIgnoreCase))
{
return false;
}
// Never include the creation and expiration dates that are automatically
// inherited from the parent token are not reused for the new token.
if (string.Equals(claim.Type, Claims.ExpiresAt, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.Private.TokenId, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.ExpiresAt, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.IssuedAt, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.NotBefore, StringComparison.OrdinalIgnoreCase))
string.Equals(claim.Type, Claims.NotBefore, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.Confirmation, StringComparison.OrdinalIgnoreCase))
{
return false;
}
@ -3686,19 +3709,13 @@ public static partial class OpenIddictServerHandlers
// Actors identities are also filtered (delegation scenarios).
var principal = context.Principal.Clone(claim =>
{
// Never include the public or internal token identifiers to ensure the identifiers
// that are automatically inherited from the parent token are not reused for the new token.
// Never include the the following claims to ensure they are not inherited from the parent token:
if (string.Equals(claim.Type, Claims.JwtId, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.Private.TokenId, StringComparison.OrdinalIgnoreCase))
{
return false;
}
// Never include the creation and expiration dates that are automatically
// inherited from the parent token are not reused for the new token.
if (string.Equals(claim.Type, Claims.ExpiresAt, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.Private.TokenId, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.ExpiresAt, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.IssuedAt, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.NotBefore, StringComparison.OrdinalIgnoreCase))
string.Equals(claim.Type, Claims.NotBefore, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.Confirmation, StringComparison.OrdinalIgnoreCase))
{
return false;
}
@ -3811,33 +3828,22 @@ public static partial class OpenIddictServerHandlers
TokenTypeIdentifiers.AccessToken => context.Principal.Clone(claim =>
{
// Never exclude the subject and authorization identifier claims.
// Always include the following claims:
if (string.Equals(claim.Type, Claims.Subject, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.Private.AuthorizationId, StringComparison.OrdinalIgnoreCase))
{
return true;
}
// Never exclude the presenters and scope private claims.
if (string.Equals(claim.Type, Claims.Private.Presenter, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.Private.AuthorizationId, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.Private.Presenter, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.Private.Scope, StringComparison.OrdinalIgnoreCase))
{
return true;
}
// Never include the public or internal token identifiers to ensure the identifiers
// that are automatically inherited from the parent token are not reused for the new token.
// Never include the the following claims to ensure they are not inherited from the parent token:
if (string.Equals(claim.Type, Claims.JwtId, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.Private.TokenId, StringComparison.OrdinalIgnoreCase))
{
return false;
}
// Never include the creation and expiration dates that are automatically
// inherited from the parent token are not reused for the new token.
if (string.Equals(claim.Type, Claims.ExpiresAt, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.Private.TokenId, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.ExpiresAt, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.IssuedAt, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.NotBefore, StringComparison.OrdinalIgnoreCase))
string.Equals(claim.Type, Claims.NotBefore, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.Confirmation, StringComparison.OrdinalIgnoreCase))
{
return false;
}
@ -3862,19 +3868,13 @@ public static partial class OpenIddictServerHandlers
TokenTypeIdentifiers.RefreshToken => context.Principal.Clone(claim =>
{
// Never include the public or internal token identifiers to ensure the identifiers
// that are automatically inherited from the parent token are not reused for the new token.
// Never include the the following claims to ensure they are not inherited from the parent token:
if (string.Equals(claim.Type, Claims.JwtId, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.Private.TokenId, StringComparison.OrdinalIgnoreCase))
{
return false;
}
// Never include the creation and expiration dates that are automatically
// inherited from the parent token are not reused for the new token.
if (string.Equals(claim.Type, Claims.ExpiresAt, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.Private.TokenId, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.ExpiresAt, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.IssuedAt, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.NotBefore, StringComparison.OrdinalIgnoreCase))
string.Equals(claim.Type, Claims.NotBefore, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.Confirmation, StringComparison.OrdinalIgnoreCase))
{
return false;
}
@ -3885,33 +3885,22 @@ public static partial class OpenIddictServerHandlers
_ => context.Principal.Clone(claim =>
{
// Never exclude the subject and authorization identifier claims.
// Always include the following claims:
if (string.Equals(claim.Type, Claims.Subject, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.Private.AuthorizationId, StringComparison.OrdinalIgnoreCase))
{
return true;
}
// Never exclude the presenters and scope private claims.
if (string.Equals(claim.Type, Claims.Private.Presenter, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.Private.AuthorizationId, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.Private.Presenter, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.Private.Scope, StringComparison.OrdinalIgnoreCase))
{
return true;
}
// Never include the public or internal token identifiers to ensure the identifiers
// that are automatically inherited from the parent token are not reused for the new token.
// Never include the the following claims to ensure they are not inherited from the parent token:
if (string.Equals(claim.Type, Claims.JwtId, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.Private.TokenId, StringComparison.OrdinalIgnoreCase))
{
return false;
}
// Never include the creation and expiration dates that are automatically
// inherited from the parent token are not reused for the new token.
if (string.Equals(claim.Type, Claims.ExpiresAt, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.Private.TokenId, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.ExpiresAt, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.IssuedAt, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.NotBefore, StringComparison.OrdinalIgnoreCase))
string.Equals(claim.Type, Claims.NotBefore, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.Confirmation, StringComparison.OrdinalIgnoreCase))
{
return false;
}
@ -4024,7 +4013,63 @@ public static partial class OpenIddictServerHandlers
principal.SetClaim(Claims.ClientId, context.ClientId);
}
if (context.Transaction.RemoteCertificate is X509Certificate2 certificate)
{
// If certificate-bound access tokens are enabled and a client certificate was used, bind the access
// token to the certificate by storing a confirmation claim containing the certificate thumbprint.
if (context.IssuedTokenType is TokenTypeIdentifiers.AccessToken &&
context.Options.UseClientCertificateBoundAccessTokens)
{
principal.SetClaim(Claims.Confirmation, CreateConfirmationClaim(certificate));
}
// If certificate-bound refresh tokens are enabled and a client certificate was used, bind the refresh
// token to the certificate by storing a confirmation claim containing the certificate thumbprint.
if (context.IssuedTokenType is TokenTypeIdentifiers.RefreshToken &&
context.Options.UseClientCertificateBoundRefreshTokens &&
!string.IsNullOrEmpty(context.ClientId))
{
// If the degraded mode was enabled, it is impossible to determine whether
// the client is a public or confidential application. In this case, the
// confirmation claim is always added to the principal by default.
//
// Applications that need to use a different logic can implement their
// own event handler and remove the confirmation claim from the principal.
if (context.Options.EnableDegradedMode)
{
principal.SetClaim(Claims.Confirmation, CreateConfirmationClaim(certificate));
}
else
{
if (_applicationManager is null)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0016));
}
var application = await _applicationManager.FindByClientIdAsync(context.ClientId) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0017));
// Note: refresh tokens are only bound to the provided certificate when the client
// is a public application, as refresh tokens issued to confidential applications
// are already sender-constrained via standard client authentication, which is more
// flexible than certificate-based token binding, as rotating client credentials is
// easier in that case (specially when using PKI-based mTLS client authentication).
if (await _applicationManager.HasClientTypeAsync(application, ClientTypes.Public))
{
principal.SetClaim(Claims.Confirmation, CreateConfirmationClaim(certificate));
}
}
}
}
context.IssuedTokenPrincipal = principal;
static JsonNode CreateConfirmationClaim(X509Certificate2 certificate) => new JsonObject
{
[JsonWebKeyParameterNames.X5tS256] = Base64UrlEncoder.Encode(
OpenIddictHelpers.ComputeSha256Hash(certificate.RawData))
};
}
}
@ -4071,19 +4116,13 @@ public static partial class OpenIddictServerHandlers
// Actors identities are also filtered (delegation scenarios).
var principal = context.Principal.Clone(claim =>
{
// Never include the public or internal token identifiers to ensure the identifiers
// that are automatically inherited from the parent token are not reused for the new token.
// Never include the the following claims to ensure they are not inherited from the parent token:
if (string.Equals(claim.Type, Claims.JwtId, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.Private.TokenId, StringComparison.OrdinalIgnoreCase))
{
return false;
}
// Never include the creation and expiration dates that are automatically
// inherited from the parent token are not reused for the new token.
if (string.Equals(claim.Type, Claims.ExpiresAt, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.Private.TokenId, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.ExpiresAt, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.IssuedAt, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.NotBefore, StringComparison.OrdinalIgnoreCase))
string.Equals(claim.Type, Claims.NotBefore, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.Confirmation, StringComparison.OrdinalIgnoreCase))
{
return false;
}
@ -4205,19 +4244,13 @@ public static partial class OpenIddictServerHandlers
// Actors identities are also filtered (delegation scenarios).
var principal = context.Principal.Clone(claim =>
{
// Never include the public or internal token identifiers to ensure the identifiers
// that are automatically inherited from the parent token are not reused for the new token.
// Never include the the following claims to ensure they are not inherited from the parent token:
if (string.Equals(claim.Type, Claims.JwtId, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.Private.TokenId, StringComparison.OrdinalIgnoreCase))
{
return false;
}
// Never include the creation and expiration dates that are automatically
// inherited from the parent token are not reused for the new token.
if (string.Equals(claim.Type, Claims.ExpiresAt, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.Private.TokenId, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.ExpiresAt, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.IssuedAt, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.NotBefore, StringComparison.OrdinalIgnoreCase))
string.Equals(claim.Type, Claims.NotBefore, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.Confirmation, StringComparison.OrdinalIgnoreCase))
{
return false;
}
@ -4285,7 +4318,52 @@ public static partial class OpenIddictServerHandlers
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0496))
});
// If certificate-bound refresh tokens are enabled and a client certificate was used, bind the refresh
// token to the certificate by storing a confirmation claim containing the certificate thumbprint.
if (context.Options.UseClientCertificateBoundRefreshTokens &&
context.Transaction.RemoteCertificate is X509Certificate2 certificate &&
!string.IsNullOrEmpty(context.ClientId))
{
// If the degraded mode was enabled, it is impossible to determine whether
// the client is a public or confidential application. In this case, the
// confirmation claim is always added to the principal by default.
//
// Applications that need to use a different logic can implement their
// own event handler and remove the confirmation claim from the principal.
if (context.Options.EnableDegradedMode)
{
principal.SetClaim(Claims.Confirmation, CreateConfirmationClaim(certificate));
}
else
{
if (_applicationManager is null)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0016));
}
var application = await _applicationManager.FindByClientIdAsync(context.ClientId) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0017));
// Note: refresh tokens are only bound to the provided certificate when the client
// is a public application, as refresh tokens issued to confidential applications
// are already sender-constrained via standard client authentication, which is more
// flexible than certificate-based token binding, as rotating client credentials is
// easier in that case (specially when using PKI-based mTLS client authentication).
if (await _applicationManager.HasClientTypeAsync(application, ClientTypes.Public))
{
principal.SetClaim(Claims.Confirmation, CreateConfirmationClaim(certificate));
}
}
}
context.RefreshTokenPrincipal = principal;
static JsonNode CreateConfirmationClaim(X509Certificate2 certificate) => new JsonObject
{
[JsonWebKeyParameterNames.X5tS256] = Base64UrlEncoder.Encode(
OpenIddictHelpers.ComputeSha256Hash(certificate.RawData))
};
}
}
@ -4332,26 +4410,20 @@ public static partial class OpenIddictServerHandlers
// Actors identities are also filtered (delegation scenarios).
var principal = context.Principal.Clone(claim =>
{
// Never exclude the subject and authorization identifier claims.
// Always include the following claims:
if (string.Equals(claim.Type, Claims.Subject, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.Private.AuthorizationId, StringComparison.OrdinalIgnoreCase))
{
return true;
}
// Never include the public or internal token identifiers to ensure the identifiers
// that are automatically inherited from the parent token are not reused for the new token.
// Never include the the following claims to ensure they are not inherited from the parent token:
if (string.Equals(claim.Type, Claims.JwtId, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.Private.TokenId, StringComparison.OrdinalIgnoreCase))
{
return false;
}
// Never include the creation and expiration dates that are automatically
// inherited from the parent token are not reused for the new token.
if (string.Equals(claim.Type, Claims.ExpiresAt, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.Private.TokenId, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.ExpiresAt, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.IssuedAt, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.NotBefore, StringComparison.OrdinalIgnoreCase))
string.Equals(claim.Type, Claims.NotBefore, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.Confirmation, StringComparison.OrdinalIgnoreCase))
{
return false;
}
@ -4489,19 +4561,13 @@ public static partial class OpenIddictServerHandlers
// Actors identities are also filtered (delegation scenarios).
var principal = context.Principal.Clone(claim =>
{
// Never include the public or internal token identifiers to ensure the identifiers
// that are automatically inherited from the parent token are not reused for the new token.
// Never include the the following claims to ensure they are not inherited from the parent token:
if (string.Equals(claim.Type, Claims.JwtId, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.Private.TokenId, StringComparison.OrdinalIgnoreCase))
{
return false;
}
// Never include the creation and expiration dates that are automatically
// inherited from the parent token are not reused for the new token.
if (string.Equals(claim.Type, Claims.ExpiresAt, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.Private.TokenId, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.ExpiresAt, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.IssuedAt, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.NotBefore, StringComparison.OrdinalIgnoreCase))
string.Equals(claim.Type, Claims.NotBefore, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.Confirmation, StringComparison.OrdinalIgnoreCase))
{
return false;
}

62
src/OpenIddict.Server/OpenIddictServerOptions.cs

@ -178,6 +178,16 @@ public sealed class OpenIddictServerOptions
/// </remarks>
public Uri? MtlsTokenEndpointAliasUri { 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? MtlsUserInfoEndpointAliasUri { get; set; }
/// <summary>
/// Gets the token validation parameters used by the OpenIddict server services.
/// </summary>
@ -647,6 +657,24 @@ public sealed class OpenIddictServerOptions
/// </summary>
public bool UseReferenceRefreshTokens { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether access tokens should be bound to the
/// client certificate sent by public or confidential clients in the TLS handshake
/// of token requests.
/// </summary>
public bool UseClientCertificateBoundAccessTokens { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether access tokens should be bound to the
/// client certificate sent by public clients in the TLS handshake of token requests.
/// </summary>
/// <remarks>
/// Note: refresh tokens are only bound to the client certificate when the client
/// is a public application, as refresh tokens issued to confidential applications
/// are already sender-constrained via standard client authentication.
/// </remarks>
public bool UseClientCertificateBoundRefreshTokens { get; set; }
/// <summary>
/// Gets or sets the time provider.
/// </summary>
@ -658,40 +686,36 @@ public sealed class OpenIddictServerOptions
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).
/// Gets or sets the chain policy used when validating PKI
/// client certificates used for OAuth 2.0 client authentication.
/// </summary>
/// <remarks>
/// <list type="bullet">
/// <item>
/// <para>
/// Note: this instance serves as a base policy and is merged with
/// the per-client policies resolved using the application manager.
/// </item>
/// <item>
/// </para>
/// <para>
/// 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>
/// the system certificates store, doing so is strongly discouraged.
/// </para>
/// </remarks>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public X509ChainPolicy? ClientCertificateChainPolicy { get; set; }
public X509ChainPolicy? PublicKeyInfrastructureTlsClientAuthenticationPolicy { get; set; }
/// <summary>
/// Gets or sets the chain policy used when validating self-signed client
/// certificates used for client authentication (typically, via mTLS).
/// certificates used for OAuth 2.0 client authentication and/or token binding.
/// </summary>
/// <remarks>
/// <list type="bullet">
/// <item>
/// <para>
/// Note: this instance serves as a base policy and is merged with
/// the per-client policies resolved using the application manager.
/// </item>
/// <item>
/// </para>
/// <para>
/// 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>
/// the system certificates store, doing so is strongly discouraged.
/// </para>
/// </remarks>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public X509ChainPolicy? SelfSignedClientCertificateChainPolicy { get; set; }
public X509ChainPolicy? SelfSignedTlsClientAuthenticationPolicy { get; set; }
}

4
src/OpenIddict.Server/OpenIddictServerTransaction.cs

@ -28,9 +28,9 @@ public sealed class OpenIddictServerTransaction
public CancellationToken CancellationToken { get; set; }
/// <summary>
/// Gets or sets the X.509 client certificate, if available.
/// Gets or sets the X.509 client certificate used by the remote peer, if available.
/// </summary>
public X509Certificate2? ClientCertificate { get; set; }
public X509Certificate2? RemoteCertificate { get; set; }
/// <summary>
/// Gets or sets the type of the endpoint processing the current request.

38
src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandlers.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.Json;
using System.Text.Json.Nodes;
@ -38,6 +39,7 @@ public static partial class OpenIddictValidationAspNetCoreHandlers
ExtractAccessTokenFromAuthorizationHeader.Descriptor,
ExtractAccessTokenFromBodyForm.Descriptor,
ExtractAccessTokenFromQueryString.Descriptor,
ExtractClientCertificate.Descriptor,
/*
* Challenge processing:
@ -301,6 +303,42 @@ public static partial class OpenIddictValidationAspNetCoreHandlers
}
}
/// <summary>
/// Contains the logic responsible for extracting a client 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 ExtractClientCertificate : IOpenIddictValidationHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireHttpRequest>()
.UseSingletonHandler<ExtractClientCertificate>()
.SetOrder(ExtractAccessTokenFromQueryString.Descriptor.Order + 1_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public async ValueTask HandleAsync(ProcessAuthenticationContext context)
{
ArgumentNullException.ThrowIfNull(context);
// 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.RemoteCertificate = certificate;
}
}
}
/// <summary>
/// Contains the logic responsible for resolving the context-specific properties and parameters stored in the
/// ASP.NET Core authentication properties specified by the application that triggered the challenge operation.

51
src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandlers.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.Json;
using Microsoft.Extensions.Logging;
@ -35,6 +36,7 @@ public static partial class OpenIddictValidationOwinHandlers
ExtractAccessTokenFromAuthorizationHeader.Descriptor,
ExtractAccessTokenFromBodyForm.Descriptor,
ExtractAccessTokenFromQueryString.Descriptor,
ExtractClientCertificate.Descriptor,
/*
* Challenge processing:
@ -304,6 +306,55 @@ public static partial class OpenIddictValidationOwinHandlers
}
}
/// <summary>
/// Contains the logic responsible for extracting a client 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 ExtractClientCertificate : IOpenIddictValidationHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireOwinRequest>()
.UseSingletonHandler<ExtractClientCertificate>()
.SetOrder(ExtractAccessTokenFromQueryString.Descriptor.Order + 1_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public async ValueTask HandleAsync(ProcessAuthenticationContext context)
{
ArgumentNullException.ThrowIfNull(context);
// 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.RemoteCertificate = 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();
}
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 resolving the context-specific properties and parameters stored in the
/// OWIN authentication properties specified by the application that triggered the challenge operation.

2
src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpBuilder.cs

@ -208,6 +208,7 @@ public sealed class OpenIddictValidationSystemNetHttpBuilder
/// client authentication key usages to be automatically selected by OpenIddict).
/// </remarks>
/// <returns>The <see cref="OpenIddictValidationSystemNetHttpBuilder"/> instance.</returns>
[Obsolete("This option is no longer supported and will be removed in a future version.")]
public OpenIddictValidationSystemNetHttpBuilder SetSelfSignedTlsClientAuthenticationCertificateSelector(
Func<X509Certificate2?> selector)
{
@ -229,6 +230,7 @@ public sealed class OpenIddictValidationSystemNetHttpBuilder
/// client authentication key usages to be automatically selected by OpenIddict).
/// </remarks>
/// <returns>The <see cref="OpenIddictValidationSystemNetHttpBuilder"/> instance.</returns>
[Obsolete("This option is no longer supported and will be removed in a future version.")]
public OpenIddictValidationSystemNetHttpBuilder SetTlsClientAuthenticationCertificateSelector(
Func<X509Certificate2?> selector)
{

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

@ -7,12 +7,10 @@
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;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using Polly;
#if SUPPORTS_HTTP_CLIENT_RESILIENCE
@ -74,14 +72,10 @@ public sealed class OpenIddictValidationSystemNetHttpConfiguration : IConfigureO
// to dynamically amend the resulting HttpClient or HttpClientHandler instance.
//
// To work around this limitation, the OpenIddict System.Net.Http integration uses
// dynamic client names and supports appending a list of key-value pairs to the client
// name to flow per-instance properties (e.g the negotiated client authentication method).
var properties = name.Length >= assembly.Name!.Length + 1 && name[assembly.Name.Length] is ':' ?
name[(assembly.Name.Length + 1)..]
.Split(['\u001f'], StringSplitOptions.RemoveEmptyEntries)
.Select(static property => property.Split(['\u001e'], StringSplitOptions.RemoveEmptyEntries))
.Where(static values => values is [{ Length: > 0 }, { Length: > 0 }])
.ToDictionary(static values => values[0], static values => values[1]) : [];
// an async-local context to flow per-instance properties and uses dynamic client
// names to ensure the inner HttpClientHandler is not reused if the context differs.
var context = OpenIddictValidationSystemNetHttpContext.Current ??
throw new InvalidOperationException(SR.FormatID2202(nameof(OpenIddictValidationSystemNetHttpContext)));
var settings = _provider.GetRequiredService<IOptionsMonitor<OpenIddictValidationSystemNetHttpOptions>>().CurrentValue;
@ -128,24 +122,18 @@ public sealed class OpenIddictValidationSystemNetHttpConfiguration : IConfigureO
handler.ClientCertificateOptions = ClientCertificateOption.Manual;
if (properties.TryGetValue("AttachTlsClientCertificate", out string? value) &&
bool.TryParse(value, out bool result) && result)
if (context.LocalCertificate is X509Certificate2 certificate)
{
var certificate = options.CurrentValue.TlsClientAuthenticationCertificateSelector();
if (certificate is not null)
// If a certificate was specified, immediately throw an excecption if it doesn't have
// a private key attached to ensure it won't be silently discarded when initiating the
// TLS handshake (which would result in a hard-to-debug scenario where the certificate
// would be attached to the HTTP handler but would not be sent to the remote peer).
if (!certificate.HasPrivateKey)
{
handler.ClientCertificates.Add(certificate);
throw new InvalidOperationException(SR.GetResourceString(SR.ID0514));
}
}
else if (properties.TryGetValue("AttachSelfSignedTlsClientCertificate", out value) &&
bool.TryParse(value, out result) && result)
{
var certificate = options.CurrentValue.SelfSignedTlsClientAuthenticationCertificateSelector();
if (certificate is not null)
{
handler.ClientCertificates.Add(certificate);
}
handler.ClientCertificates.Add(certificate);
}
});
@ -170,20 +158,6 @@ public sealed class OpenIddictValidationSystemNetHttpConfiguration : IConfigureO
return;
}
// Note: HttpClientFactory doesn't support flowing a list of properties that can be
// accessed from the HttpClientAction or HttpMessageHandlerBuilderAction delegates
// to dynamically amend the resulting HttpClient or HttpClientHandler instance.
//
// To work around this limitation, the OpenIddict System.Net.Http integration uses dynamic
// client names and supports appending a list of key-value pairs to the client name to flow
// per-instance properties (e.g a flag indicating whether a client certificate should be used).
var properties = name.Length >= assembly.Name!.Length + 1 && name[assembly.Name.Length] is ':' ?
name[(assembly.Name.Length + 1)..]
.Split(['\u001f'], StringSplitOptions.RemoveEmptyEntries)
.Select(static property => property.Split(['\u001e'], StringSplitOptions.RemoveEmptyEntries))
.Where(static values => values is [{ Length: > 0 }, { Length: > 0 }])
.ToDictionary(static values => values[0], static values => values[1]) : [];
options.HttpMessageHandlerBuilderActions.Insert(0, static builder =>
{
// Note: Microsoft.Extensions.Http 9.0+ no longer uses HttpClientHandler as the default instance
@ -240,50 +214,8 @@ public sealed class OpenIddictValidationSystemNetHttpConfiguration : IConfigureO
});
}
/// <inheritdoc/>
[Obsolete("This method is no longer supported and will be removed in a future version.")]
public void PostConfigure(string? name, OpenIddictValidationSystemNetHttpOptions options)
{
ArgumentNullException.ThrowIfNull(options);
// If no client authentication certificate selector was provided, use fallback delegates that
// automatically use the first X.509 signing certificate attached to the client registration
// that is suitable for both digital signature and client authentication.
options.SelfSignedTlsClientAuthenticationCertificateSelector ??= () =>
{
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 && OpenIddictHelpers.IsSelfIssuedCertificate(certificate) &&
OpenIddictHelpers.HasKeyUsage(certificate, X509KeyUsageFlags.DigitalSignature) &&
OpenIddictHelpers.HasExtendedKeyUsage(certificate, ObjectIdentifiers.ExtendedKeyUsages.ClientAuthentication))
{
return certificate;
}
}
return null;
};
options.TlsClientAuthenticationCertificateSelector ??= () =>
{
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 && !OpenIddictHelpers.IsSelfIssuedCertificate(certificate) &&
OpenIddictHelpers.HasKeyUsage(certificate, X509KeyUsageFlags.DigitalSignature) &&
OpenIddictHelpers.HasExtendedKeyUsage(certificate, ObjectIdentifiers.ExtendedKeyUsages.ClientAuthentication))
{
return certificate;
}
}
return null;
};
}
=> throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
}

67
src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpContext.cs

@ -0,0 +1,67 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project.
*/
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Microsoft.IdentityModel.Tokens;
namespace OpenIddict.Validation.SystemNetHttp;
/// <summary>
/// Represents the context used by the System.Net.Http integration when creating a new HTTP client.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed class OpenIddictValidationSystemNetHttpContext
{
private static readonly AsyncLocal<OpenIddictValidationSystemNetHttpContext?> _current = new();
/// <summary>
/// Gets or sets the X.509 client certificate that will be used to authenticate
/// this peer when communicating with the external endpoint, if applicable.
/// </summary>
public X509Certificate2? LocalCertificate { get; init; }
/// <summary>
/// Gets or sets the ambient context for the current execution flow.
/// </summary>
public static OpenIddictValidationSystemNetHttpContext? Current
{
get => _current.Value;
set => _current.Value = value;
}
/// <summary>
/// Computes a stable, unique identifier for the specified context using a cryptographic hash.
/// </summary>
/// <param name="context">The client context for which to compute the stable identifier.</param>
/// <returns>A string representing the stable identifier for the specified context.</returns>
public static string ComputeStableId(OpenIddictValidationSystemNetHttpContext context)
{
ArgumentNullException.ThrowIfNull(context);
using var algorithm = CreateAlgorithm();
if (context.LocalCertificate is X509Certificate2 certificate)
{
algorithm.TransformBlock(certificate.RawData, 0, certificate.RawData.Length, outputBuffer: null, outputOffset: 0);
}
algorithm.TransformFinalBlock([], 0, 0);
return Base64UrlEncoder.Encode(algorithm.Hash);
[UnconditionalSuppressMessage("Trimming", "IL2026",
Justification = "The default implementation is always used when no custom algorithm was registered.")]
static SHA256 CreateAlgorithm() => CryptoConfig.CreateFromName("OpenIddict SHA-256 Cryptographic Provider") switch
{
SHA256 result => result,
null => SHA256.Create(),
var result => throw new CryptographicException(SR.FormatID0351(result.GetType().FullName))
};
}
}

3
src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpExtensions.cs

@ -46,9 +46,6 @@ public static class OpenIddictValidationSystemNetHttpExtensions
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<
IPostConfigureOptions<HttpClientFactoryOptions>, OpenIddictValidationSystemNetHttpConfiguration>());
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<
IPostConfigureOptions<OpenIddictValidationSystemNetHttpOptions>, OpenIddictValidationSystemNetHttpConfiguration>());
return new OpenIddictValidationSystemNetHttpBuilder(builder.Services);
}

151
src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.cs

@ -14,7 +14,6 @@ using System.Text;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using Microsoft.IdentityModel.Tokens;
using static OpenIddict.Validation.SystemNetHttp.OpenIddictValidationSystemNetHttpConstants;
namespace OpenIddict.Validation.SystemNetHttp;
@ -24,11 +23,6 @@ public static partial class OpenIddictValidationSystemNetHttpHandlers
{
public static ImmutableArray<OpenIddictValidationHandlerDescriptor> DefaultHandlers { get; } =
[
/*
* Authentication processing:
*/
AttachNonDefaultIntrospectionEndpointClientAuthenticationMethod.Descriptor,
.. Discovery.DefaultHandlers,
.. Introspection.DefaultHandlers
];
@ -37,14 +31,9 @@ public static partial class OpenIddictValidationSystemNetHttpHandlers
/// Contains the logic responsible for negotiating the best introspection endpoint client
/// authentication method supported by both the client and the authorization server.
/// </summary>
[Obsolete("This class is obsolete and will be removed in a future version.")]
public sealed class AttachNonDefaultIntrospectionEndpointClientAuthenticationMethod : IOpenIddictValidationHandler<ProcessAuthenticationContext>
{
private readonly IOptionsMonitor<OpenIddictValidationSystemNetHttpOptions> _options;
public AttachNonDefaultIntrospectionEndpointClientAuthenticationMethod(
IOptionsMonitor<OpenIddictValidationSystemNetHttpOptions> options)
=> _options = options ?? throw new ArgumentNullException(nameof(options));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
@ -56,94 +45,7 @@ public static partial class OpenIddictValidationSystemNetHttpHandlers
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessAuthenticationContext context)
{
ArgumentNullException.ThrowIfNull(context);
// If an explicit client authentication method was attached, don't overwrite it.
if (!string.IsNullOrEmpty(context.IntrospectionEndpointClientAuthenticationMethod))
{
return ValueTask.CompletedTask;
}
context.IntrospectionEndpointClientAuthenticationMethod = (
// Note: if client authentication methods are explicitly listed in the validation options, only use
// the client authentication methods that are both listed and enabled in the global client options.
// Otherwise, always default to the client authentication methods that have been enabled globally.
Client: context.Options.ClientAuthenticationMethods,
Server: context.Configuration.IntrospectionEndpointAuthMethodsSupported) switch
{
// If a TLS client authentication certificate could be resolved and both the
// client and the server explicitly support tls_client_auth, always prefer it.
({ Count: > 0 } client, { Count: > 0 } server) when
client.Contains(ClientAuthenticationMethods.TlsClientAuth) &&
server.Contains(ClientAuthenticationMethods.TlsClientAuth) &&
(context.Configuration.MtlsIntrospectionEndpoint ?? context.Configuration.IntrospectionEndpoint) is Uri endpoint &&
string.Equals(endpoint.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase) &&
_options.CurrentValue.TlsClientAuthenticationCertificateSelector() is not null
=> ClientAuthenticationMethods.TlsClientAuth,
// If a self-signed TLS client authentication certificate could be resolved and both
// the client and the server explicitly support self_signed_tls_client_auth, use it.
({ Count: > 0 } client, { Count: > 0 } server) when
client.Contains(ClientAuthenticationMethods.SelfSignedTlsClientAuth) &&
server.Contains(ClientAuthenticationMethods.SelfSignedTlsClientAuth) &&
(context.Configuration.MtlsIntrospectionEndpoint ?? context.Configuration.IntrospectionEndpoint) is Uri endpoint &&
string.Equals(endpoint.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase) &&
_options.CurrentValue.SelfSignedTlsClientAuthenticationCertificateSelector() is not null
=> ClientAuthenticationMethods.SelfSignedTlsClientAuth,
// If at least one asymmetric signing key was attached to the validation options
// and both the client and the server explicitly support private_key_jwt, use it.
({ Count: > 0 } client, { Count: > 0 } server) when
client.Contains(ClientAuthenticationMethods.PrivateKeyJwt) &&
server.Contains(ClientAuthenticationMethods.PrivateKeyJwt) &&
context.Options.SigningCredentials.Exists(static credentials => credentials.Key is AsymmetricSecurityKey)
=> ClientAuthenticationMethods.PrivateKeyJwt,
// If a client secret was attached to the validation options and both the client and
// the server explicitly support client_secret_post, prefer it to basic authentication.
({ Count: > 0 } client, { Count: > 0 } server) when !string.IsNullOrEmpty(context.Options.ClientSecret) &&
client.Contains(ClientAuthenticationMethods.ClientSecretPost) &&
server.Contains(ClientAuthenticationMethods.ClientSecretPost)
=> ClientAuthenticationMethods.ClientSecretPost,
// The OAuth 2.0 specification recommends sending the client credentials using basic authentication.
// However, this authentication method is known to have severe compatibility/interoperability issues:
//
// - While restricted to clients that have been given a secret (i.e confidential clients) by the
// specification, basic authentication is also sometimes required by server implementations for
// public clients that don't have a client secret: in this case, an empty password is used and
// the client identifier is sent alone in the Authorization header (instead of being sent using
// the standard "client_id" parameter present in the request body).
//
// - While the OAuth 2.0 specification requires that the client credentials be formURL-encoded
// before being base64-encoded, many implementations are known to implement a non-standard
// encoding scheme, where neither the client_id nor the client_secret are formURL-encoded.
//
// To guarantee that the OpenIddict implementation can be used with most servers implementions,
// basic authentication is only used when a client secret is present and the server configuration
// doesn't list any supported client authentication method or doesn't support client_secret_post.
//
// If client_secret_post is not listed or if the server returned an empty methods list,
// client_secret_basic is always used, as it MUST be implemented by all OAuth 2.0 servers.
//
// See https://tools.ietf.org/html/rfc8414#section-2
// and https://tools.ietf.org/html/rfc6749#section-2.3.1 for more information.
({ Count: > 0 } client, { Count: > 0 } server) when !string.IsNullOrEmpty(context.Options.ClientSecret) &&
client.Contains(ClientAuthenticationMethods.ClientSecretBasic) &&
server.Contains(ClientAuthenticationMethods.ClientSecretBasic)
=> ClientAuthenticationMethods.ClientSecretBasic,
({ Count: > 0 } client, { Count: 0 }) when !string.IsNullOrEmpty(context.Options.ClientSecret) &&
client.Contains(ClientAuthenticationMethods.ClientSecretBasic)
=> ClientAuthenticationMethods.ClientSecretBasic,
_ => null
};
return ValueTask.CompletedTask;
}
public ValueTask HandleAsync(ProcessAuthenticationContext context) => ValueTask.CompletedTask;
}
/// <summary>
@ -176,38 +78,39 @@ public static partial class OpenIddictValidationSystemNetHttpHandlers
// accessed from the HttpClientAction or HttpMessageHandlerBuilderAction delegates
// to dynamically amend the resulting HttpClient or HttpClientHandler instance.
//
// To work around this limitation, the OpenIddict System.Net.Http integration
// uses dynamic client names and supports appending a list of key-value pairs
// to the client name to flow per-instance properties.
var builder = new StringBuilder();
// To work around this limitation, the OpenIddict System.Net.Http integration uses
// an async-local context to flow per-instance properties and uses dynamic client
// names to ensure the inner HttpClientHandler is not reused if the context differs.
// Always prefix the HTTP client name with the assembly name of the System.Net.Http package.
builder.Append(typeof(OpenIddictValidationSystemNetHttpOptions).Assembly.GetName().Name);
// Attach a flag indicating that a client certificate should be used in the TLS handshake.
if (context.ClientAuthenticationMethod is ClientAuthenticationMethods.TlsClientAuth)
if (OpenIddictValidationSystemNetHttpContext.Current is not null)
{
builder.Append(':');
builder.Append("AttachTlsClientCertificate")
.Append('\u001e')
.Append(bool.TrueString);
throw new InvalidOperationException(SR.FormatID2201(nameof(OpenIddictValidationSystemNetHttpContext)));
}
// Attach a flag indicating that a self-signed client certificate should be used in the TLS handshake.
else if (context.ClientAuthenticationMethod is ClientAuthenticationMethods.SelfSignedTlsClientAuth)
try
{
builder.Append(':');
OpenIddictValidationSystemNetHttpContext.Current = new()
{
LocalCertificate = context.LocalCertificate
};
builder.Append("AttachSelfSignedTlsClientCertificate")
.Append('\u001e')
.Append(bool.TrueString);
// Generate a stable identifier representing the current context to ensure the inner
// HttpClientHandler instances are not reused for different operations if the properties
// attached to the context are not identical (e.g different TLS client certificates).
var identifier = OpenIddictValidationSystemNetHttpContext.ComputeStableId(OpenIddictValidationSystemNetHttpContext.Current);
var client = _factory.CreateClient(
$"{typeof(OpenIddictValidationSystemNetHttpOptions).Assembly.GetName().Name}:{identifier}") ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0174));
// Create and store the HttpClient in the transaction properties.
context.Transaction.SetProperty(typeof(HttpClient).FullName!, client);
}
// Create and store the HttpClient in the transaction properties.
context.Transaction.SetProperty(typeof(HttpClient).FullName!, _factory.CreateClient(builder.ToString()) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0174)));
finally
{
OpenIddictValidationSystemNetHttpContext.Current = null;
}
return ValueTask.CompletedTask;
}

2
src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpOptions.cs

@ -95,6 +95,7 @@ public sealed class OpenIddictValidationSystemNetHttpOptions
/// client authentication key usages to be automatically selected by OpenIddict).
/// </remarks>
[EditorBrowsable(EditorBrowsableState.Advanced)]
[Obsolete("This option is no longer supported and will be removed in a future version.")]
public Func<X509Certificate2?> SelfSignedTlsClientAuthenticationCertificateSelector { get; set; } = default!;
/// <summary>
@ -109,5 +110,6 @@ public sealed class OpenIddictValidationSystemNetHttpOptions
/// client authentication key usages to be automatically selected by OpenIddict).
/// </remarks>
[EditorBrowsable(EditorBrowsableState.Advanced)]
[Obsolete("This option is no longer supported and will be removed in a future version.")]
public Func<X509Certificate2?> TlsClientAuthenticationCertificateSelector { get; set; } = default!;
}

5
src/OpenIddict.Validation/OpenIddictValidationEvents.Protection.cs

@ -138,6 +138,11 @@ public static partial class OpenIddictValidationEvents
/// </summary>
public bool DisablePresenterValidation { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether proof-of-possession validation is disabled.
/// </summary>
public bool DisableProofOfPossessionValidation { get; set; }
/// <summary>
/// Gets or sets the security token handler used to validate the token.
/// </summary>

13
src/OpenIddict.Validation/OpenIddictValidationEvents.cs

@ -6,6 +6,7 @@
using System.ComponentModel;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.Logging;
namespace OpenIddict.Validation;
@ -154,6 +155,12 @@ public static partial class OpenIddictValidationEvents
/// when communicating with the external endpoint, if applicable.
/// </summary>
public string? ClientAuthenticationMethod { get; set; }
/// <summary>
/// Gets or sets the X.509 client certificate that will be used to authenticate
/// this peer when communicating with the external endpoint, if applicable.
/// </summary>
public X509Certificate2? LocalCertificate { get; set; }
}
/// <summary>
@ -305,6 +312,12 @@ public static partial class OpenIddictValidationEvents
/// </summary>
public string? IntrospectionEndpointClientAuthenticationMethod { get; set; }
/// <summary>
/// Gets or sets the X.509 client certificate used when
/// communicating with the introspection endpoint, if applicable.
/// </summary>
public X509Certificate2? IntrospectionEndpointClientCertificate { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether an introspection request should be sent.
/// </summary>

1
src/OpenIddict.Validation/OpenIddictValidationExtensions.cs

@ -51,6 +51,7 @@ public static class OpenIddictValidationExtensions
builder.Services.TryAddSingleton<RequireTokenIdResolved>();
builder.Services.TryAddSingleton<RequireTokenLifetimeValidationEnabled>();
builder.Services.TryAddSingleton<RequireTokenPresenterValidationEnabled>();
builder.Services.TryAddSingleton<RequireTokenProofOfPossessionValidationEnabled>();
// Note: TryAddEnumerable() is used here to ensure the initializer is registered only once.
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<

14
src/OpenIddict.Validation/OpenIddictValidationHandlerFilters.cs

@ -192,4 +192,18 @@ public static class OpenIddictValidationHandlerFilters
return new(!context.DisablePresenterValidation);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if token proof-of-possession validation was disabled.
/// </summary>
public sealed class RequireTokenProofOfPossessionValidationEnabled : IOpenIddictValidationHandlerFilter<ValidateTokenContext>
{
/// <inheritdoc/>
public ValueTask<bool> IsActiveAsync(ValidateTokenContext context)
{
ArgumentNullException.ThrowIfNull(context);
return new(!context.DisableProofOfPossessionValidation);
}
}
}

5
src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs

@ -84,7 +84,7 @@ public static partial class OpenIddictValidationHandlers
Claims.Active => ((JsonElement) value).ValueKind is JsonValueKind.True or JsonValueKind.False,
// The following claims MUST be formatted as unique strings:
Claims.JwtId or Claims.Issuer or Claims.Scope or Claims.TokenUsage
Claims.Issuer or Claims.JwtId or Claims.Scope or Claims.TokenUsage
=> ((JsonElement) value).ValueKind is JsonValueKind.String,
// The following claims MUST be formatted as strings or arrays of strings:
@ -100,6 +100,9 @@ public static partial class OpenIddictValidationHandlers
=> (JsonElement) value is { ValueKind: JsonValueKind.Number } element &&
element.TryGetDecimal(out decimal result) && result is >= 0,
// The following claims MUST be formatted as JSON objects:
Claims.Confirmation => ((JsonElement) value).ValueKind is JsonValueKind.Object,
// Claims that are not in the well-known list can be of any type.
_ => true
};

88
src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs

@ -7,7 +7,10 @@
using System.Collections.Immutable;
using System.Diagnostics;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
using System.Text.Json.Nodes;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
@ -33,6 +36,7 @@ public static partial class OpenIddictValidationHandlers
ValidateExpirationDate.Descriptor,
ValidatePresenters.Descriptor,
ValidateAudiences.Descriptor,
ValidateProofOfPossession.Descriptor,
ValidateTokenEntry.Descriptor,
ValidateAuthorizationEntry.Descriptor,
@ -782,6 +786,88 @@ public static partial class OpenIddictValidationHandlers
}
}
/// <summary>
/// Contains the logic responsible for rejecting tokens for which no valid proof of possession was received.
/// </summary>
public sealed class ValidateProofOfPossession : IOpenIddictValidationHandler<ValidateTokenContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ValidateTokenContext>()
.AddFilter<RequireTokenProofOfPossessionValidationEnabled>()
.UseSingletonHandler<ValidateProofOfPossession>()
.SetOrder(ValidateAudiences.Descriptor.Order + 1_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ValidateTokenContext context)
{
ArgumentNullException.ThrowIfNull(context);
Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// Try to resolve the confirmation claim from the principal. If no such claim can be found,
// this indicates that the token is a bearer token and doesn't require a proof of possession.
var confirmation = context.Principal.GetClaim(Claims.Confirmation);
if (string.IsNullOrEmpty(confirmation))
{
return ValueTask.CompletedTask;
}
if (JsonObject.Parse(confirmation) is not JsonObject node)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID2199));
}
if (node.ContainsKey(JsonWebKeyParameterNames.X5tS256))
{
var thumbprint = (string?) node[JsonWebKeyParameterNames.X5tS256];
if (string.IsNullOrEmpty(thumbprint))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID2200));
}
// If no client certificate was provided, return an error as no
// proof-of-possession can be validated without the client certificate.
if (context.Transaction.RemoteCertificate is not X509Certificate2 certificate)
{
context.Logger.LogInformation(6282, SR.GetResourceString(SR.ID6282));
context.Reject(
error: Errors.InvalidToken,
description: SR.GetResourceString(SR.ID2203),
uri: SR.FormatID8000(SR.ID2203));
return ValueTask.CompletedTask;
}
// If the thumbprint of the certificate doesn't match the hash
// resolved from the confirmation claim, return an error.
var hash = Base64UrlEncoder.Encode(OpenIddictHelpers.ComputeSha256Hash(certificate.RawData));
if (!OpenIddictHelpers.FixedTimeEquals(
left : MemoryMarshal.AsBytes<char>(hash),
right: MemoryMarshal.AsBytes<char>(thumbprint)))
{
context.Logger.LogInformation(6289, SR.GetResourceString(SR.ID6289));
context.Reject(
error: Errors.InvalidToken,
description: SR.GetResourceString(SR.ID2204),
uri: SR.FormatID8000(SR.ID2204));
return ValueTask.CompletedTask;
}
return ValueTask.CompletedTask;
}
throw new InvalidOperationException(SR.GetResourceString(SR.ID2196));
}
}
/// <summary>
/// Contains the logic responsible for rejecting tokens whose
/// associated token entry is no longer valid (e.g was revoked).
@ -803,7 +889,7 @@ public static partial class OpenIddictValidationHandlers
.AddFilter<RequireTokenEntryValidationEnabled>()
.AddFilter<RequireTokenIdResolved>()
.UseScopedHandler<ValidateTokenEntry>()
.SetOrder(ValidateAudiences.Descriptor.Order + 1_000)
.SetOrder(ValidateProofOfPossession.Descriptor.Order + 1_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();

245
src/OpenIddict.Validation/OpenIddictValidationHandlers.cs

@ -7,7 +7,10 @@
using System.Collections.Immutable;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
using System.Text.Json.Nodes;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using static OpenIddict.Abstractions.OpenIddictExceptions;
@ -27,6 +30,7 @@ public static partial class OpenIddictValidationHandlers
ResolveServerConfiguration.Descriptor,
EvaluateIntrospectionRequest.Descriptor,
AttachIntrospectionEndpointClientAuthenticationMethod.Descriptor,
AttachIntrospectionEndpointClientCertificate.Descriptor,
ResolveIntrospectionEndpoint.Descriptor,
AttachIntrospectionRequestParameters.Descriptor,
EvaluateGeneratedClientAssertion.Descriptor,
@ -36,6 +40,7 @@ public static partial class OpenIddictValidationHandlers
SendIntrospectionRequest.Descriptor,
ValidateIntrospectedTokenUsage.Descriptor,
ValidateIntrospectedTokenAudiences.Descriptor,
ValidateIntrospectedTokenProofOfPossession.Descriptor,
ValidateAccessToken.Descriptor,
/*
@ -241,11 +246,56 @@ public static partial class OpenIddictValidationHandlers
Client: context.Options.ClientAuthenticationMethods,
Server: context.Configuration.IntrospectionEndpointAuthMethodsSupported) switch
{
// If at least one signing key was attached to the validation options and both
// the client and the server explicitly support private_key_jwt, always prefer it.
({ Count: > 0 } client, { Count: > 0 } server) when context.Options.SigningCredentials.Count is not 0 &&
// If a TLS client authentication certificate could be resolved and both the
// client and the server explicitly support tls_client_auth, always prefer it.
({ Count: > 0 } client, { Count: > 0 } server) when
client.Contains(ClientAuthenticationMethods.TlsClientAuth) &&
server.Contains(ClientAuthenticationMethods.TlsClientAuth) &&
(context.Configuration.MtlsIntrospectionEndpoint ?? context.Configuration.IntrospectionEndpoint) is Uri endpoint &&
context.IntrospectionEndpointClientCertificate is X509Certificate2 certificate &&
OpenIddictHelpers.IsClientAuthenticationCertificate(certificate) &&
!OpenIddictHelpers.IsSelfIssuedCertificate(certificate)
=> ClientAuthenticationMethods.TlsClientAuth,
({ Count: > 0 } client, { Count: > 0 } server) when
client.Contains(ClientAuthenticationMethods.TlsClientAuth) &&
server.Contains(ClientAuthenticationMethods.TlsClientAuth) &&
(context.Configuration.MtlsIntrospectionEndpoint ?? context.Configuration.IntrospectionEndpoint) is Uri endpoint &&
context.IntrospectionEndpointClientCertificate is null &&
context.Options.SigningCredentials.Exists(static credentials =>
credentials.Key is X509SecurityKey { Certificate: X509Certificate2 certificate } &&
OpenIddictHelpers.IsClientAuthenticationCertificate(certificate) &&
!OpenIddictHelpers.IsSelfIssuedCertificate(certificate))
=> ClientAuthenticationMethods.TlsClientAuth,
// If a self-signed TLS client authentication certificate could be resolved and both
// the client and the server explicitly support self_signed_tls_client_auth, use it.
({ Count: > 0 } client, { Count: > 0 } server) when
client.Contains(ClientAuthenticationMethods.SelfSignedTlsClientAuth) &&
server.Contains(ClientAuthenticationMethods.SelfSignedTlsClientAuth) &&
(context.Configuration.MtlsIntrospectionEndpoint ?? context.Configuration.IntrospectionEndpoint) is Uri endpoint &&
context.IntrospectionEndpointClientCertificate is X509Certificate2 certificate &&
OpenIddictHelpers.IsClientAuthenticationCertificate(certificate) &&
OpenIddictHelpers.IsSelfIssuedCertificate(certificate)
=> ClientAuthenticationMethods.SelfSignedTlsClientAuth,
({ Count: > 0 } client, { Count: > 0 } server) when
client.Contains(ClientAuthenticationMethods.SelfSignedTlsClientAuth) &&
server.Contains(ClientAuthenticationMethods.SelfSignedTlsClientAuth) &&
(context.Configuration.MtlsIntrospectionEndpoint ?? context.Configuration.IntrospectionEndpoint) is Uri endpoint &&
context.IntrospectionEndpointClientCertificate is null &&
context.Options.SigningCredentials.Exists(static credentials =>
credentials.Key is X509SecurityKey { Certificate: X509Certificate2 certificate } &&
OpenIddictHelpers.IsClientAuthenticationCertificate(certificate) &&
OpenIddictHelpers.IsSelfIssuedCertificate(certificate))
=> ClientAuthenticationMethods.SelfSignedTlsClientAuth,
// If at least one asymmetric signing key was attached to the validation options
// and both the client and the server explicitly support private_key_jwt, use it.
({ Count: > 0 } client, { Count: > 0 } server) when
client.Contains(ClientAuthenticationMethods.PrivateKeyJwt) &&
server.Contains(ClientAuthenticationMethods.PrivateKeyJwt)
server.Contains(ClientAuthenticationMethods.PrivateKeyJwt) &&
context.Options.SigningCredentials.Exists(static credentials => credentials.Key is AsymmetricSecurityKey)
=> ClientAuthenticationMethods.PrivateKeyJwt,
// If a client secret was attached to the validation options and both the client and
@ -255,6 +305,84 @@ public static partial class OpenIddictValidationHandlers
server.Contains(ClientAuthenticationMethods.ClientSecretPost)
=> ClientAuthenticationMethods.ClientSecretPost,
// The OAuth 2.0 specification recommends sending the client credentials using basic authentication.
// However, this authentication method is known to have severe compatibility/interoperability issues:
//
// - While restricted to clients that have been given a secret (i.e confidential clients) by the
// specification, basic authentication is also sometimes required by server implementations for
// public clients that don't have a client secret: in this case, an empty password is used and
// the client identifier is sent alone in the Authorization header (instead of being sent using
// the standard "client_id" parameter present in the request body).
//
// - While the OAuth 2.0 specification requires that the client credentials be formURL-encoded
// before being base64-encoded, many implementations are known to implement a non-standard
// encoding scheme, where neither the client_id nor the client_secret are formURL-encoded.
//
// To guarantee that the OpenIddict implementation can be used with most servers implementions,
// basic authentication is only used when a client secret is present and the server configuration
// doesn't list any supported client authentication method or doesn't support client_secret_post.
//
// If client_secret_post is not listed or if the server returned an empty methods list,
// client_secret_basic is always used, as it MUST be implemented by all OAuth 2.0 servers.
//
// See https://tools.ietf.org/html/rfc8414#section-2
// and https://tools.ietf.org/html/rfc6749#section-2.3.1 for more information.
({ Count: > 0 } client, { Count: > 0 } server) when !string.IsNullOrEmpty(context.Options.ClientSecret) &&
client.Contains(ClientAuthenticationMethods.ClientSecretBasic) &&
server.Contains(ClientAuthenticationMethods.ClientSecretBasic)
=> ClientAuthenticationMethods.ClientSecretBasic,
({ Count: > 0 } client, { Count: 0 }) when !string.IsNullOrEmpty(context.Options.ClientSecret) &&
client.Contains(ClientAuthenticationMethods.ClientSecretBasic)
=> ClientAuthenticationMethods.ClientSecretBasic,
_ => null
};
return ValueTask.CompletedTask;
}
}
/// <summary>
/// Contains the logic responsible for attaching the client certificate used for
/// the introspection endpoint to the authentication context, if applicable.
/// </summary>
public sealed class AttachIntrospectionEndpointClientCertificate : IOpenIddictValidationHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.UseSingletonHandler<AttachIntrospectionEndpointClientCertificate>()
.SetOrder(AttachIntrospectionEndpointClientAuthenticationMethod.Descriptor.Order + 1_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessAuthenticationContext context)
{
ArgumentNullException.ThrowIfNull(context);
// If a certificate-based client authentication method was negotiated and
// no certificate was explicitly attached by the application, try to find a
// valid certificate in the client registration and attach it to the context.
context.IntrospectionEndpointClientCertificate ??= context.IntrospectionEndpointClientAuthenticationMethod switch
{
ClientAuthenticationMethods.TlsClientAuth => context.Options.SigningCredentials
.Select(static credentials => (credentials.Key as X509SecurityKey)?.Certificate)
.FirstOrDefault(static certificate => certificate is not null &&
OpenIddictHelpers.IsClientAuthenticationCertificate(certificate) &&
!OpenIddictHelpers.IsSelfIssuedCertificate(certificate))
?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0512)),
ClientAuthenticationMethods.SelfSignedTlsClientAuth => context.Options.SigningCredentials
.Select(static credentials => (credentials.Key as X509SecurityKey)?.Certificate)
.FirstOrDefault(static certificate => certificate is not null &&
OpenIddictHelpers.IsClientAuthenticationCertificate(certificate) &&
OpenIddictHelpers.IsSelfIssuedCertificate(certificate))
?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0512)),
_ => null
};
@ -274,7 +402,7 @@ public static partial class OpenIddictValidationHandlers
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireIntrospectionRequest>()
.UseSingletonHandler<ResolveIntrospectionEndpoint>()
.SetOrder(AttachIntrospectionEndpointClientAuthenticationMethod.Descriptor.Order + 1_000)
.SetOrder(AttachIntrospectionEndpointClientCertificate.Descriptor.Order + 1_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();
@ -283,12 +411,9 @@ public static partial class OpenIddictValidationHandlers
{
ArgumentNullException.ThrowIfNull(context);
// If the URI of the introspection endpoint endpoint wasn't explicitly
// set at this stage, try to extract it from the server configuration.
context.IntrospectionEndpoint ??= context.IntrospectionEndpointClientAuthenticationMethod switch
{
// When TLS client certificate authentication was negotiated,
// always favor the mTLS-specific endpoint if available.
// If a TLS client authentication certificate is going to be used, always favor the mTLS alias if available.
ClientAuthenticationMethods.SelfSignedTlsClientAuth or ClientAuthenticationMethods.TlsClientAuth
when context.Configuration.MtlsIntrospectionEndpoint is { IsAbsoluteUri: true } uri &&
!OpenIddictHelpers.IsImplicitFileUri(uri) => uri,
@ -582,12 +707,27 @@ public static partial class OpenIddictValidationHandlers
throw new InvalidOperationException(SR.FormatID0301(Metadata.IntrospectionEndpoint));
}
var certificate = context.IntrospectionEndpointClientAuthenticationMethod switch
{
ClientAuthenticationMethods.TlsClientAuth when context.IntrospectionEndpointClientCertificate is not null =>
OpenIddictHelpers.IsSelfIssuedCertificate(context.IntrospectionEndpointClientCertificate)
? throw new InvalidOperationException(SR.GetResourceString(SR.ID0513))
: context.IntrospectionEndpointClientCertificate,
ClientAuthenticationMethods.SelfSignedTlsClientAuth when context.IntrospectionEndpointClientCertificate is not null =>
OpenIddictHelpers.IsSelfIssuedCertificate(context.IntrospectionEndpointClientCertificate)
? context.IntrospectionEndpointClientCertificate
: throw new InvalidOperationException(SR.GetResourceString(SR.ID0513)),
_ => null
};
try
{
(context.IntrospectionResponse, context.AccessTokenPrincipal) =
await _service.SendIntrospectionRequestAsync(
context.Configuration, context.IntrospectionRequest, context.IntrospectionEndpoint,
context.IntrospectionEndpointClientAuthenticationMethod, context.CancellationToken);
context.IntrospectionEndpointClientAuthenticationMethod, certificate, context.CancellationToken);
}
catch (ProtocolException exception)
@ -719,6 +859,89 @@ public static partial class OpenIddictValidationHandlers
}
}
/// <summary>
/// Contains the logic responsible for validating the proof of possession
/// of the introspected token returned by the server, if applicable.
/// </summary>
public sealed class ValidateIntrospectedTokenProofOfPossession : IOpenIddictValidationHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireIntrospectionRequest>()
.UseSingletonHandler<ValidateIntrospectedTokenProofOfPossession>()
.SetOrder(ValidateIntrospectedTokenAudiences.Descriptor.Order + 1_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessAuthenticationContext context)
{
ArgumentNullException.ThrowIfNull(context);
Debug.Assert(context.AccessTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// Try to resolve the confirmation claim from the principal. If no such claim can be found,
// this indicates that the token is a bearer token and doesn't require a proof of possession.
var confirmation = context.AccessTokenPrincipal.GetClaim(Claims.Confirmation);
if (string.IsNullOrEmpty(confirmation))
{
return ValueTask.CompletedTask;
}
if (JsonObject.Parse(confirmation) is not JsonObject node)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID2199));
}
if (node.ContainsKey(JsonWebKeyParameterNames.X5tS256))
{
var thumbprint = (string?) node[JsonWebKeyParameterNames.X5tS256];
if (string.IsNullOrEmpty(thumbprint))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID2200));
}
// If no client certificate was provided, return an error as no
// proof-of-possession can be validated without the client certificate.
if (context.Transaction.RemoteCertificate is not X509Certificate2 certificate)
{
context.Logger.LogInformation(6282, SR.GetResourceString(SR.ID6282));
context.Reject(
error: Errors.InvalidToken,
description: SR.GetResourceString(SR.ID2203),
uri: SR.FormatID8000(SR.ID2203));
return ValueTask.CompletedTask;
}
// If the thumbprint of the certificate doesn't match the hash
// resolved from the confirmation claim, return an error.
var hash = Base64UrlEncoder.Encode(OpenIddictHelpers.ComputeSha256Hash(certificate.RawData));
if (!OpenIddictHelpers.FixedTimeEquals(
left : MemoryMarshal.AsBytes<char>(hash),
right: MemoryMarshal.AsBytes<char>(thumbprint)))
{
context.Logger.LogInformation(6289, SR.GetResourceString(SR.ID6289));
context.Reject(
error: Errors.InvalidToken,
description: SR.GetResourceString(SR.ID2204),
uri: SR.FormatID8000(SR.ID2204));
return ValueTask.CompletedTask;
}
return ValueTask.CompletedTask;
}
throw new InvalidOperationException(SR.GetResourceString(SR.ID2196));
}
}
/// <summary>
/// Contains the logic responsible for ensuring a token was correctly resolved from the context.
/// </summary>
@ -736,7 +959,7 @@ public static partial class OpenIddictValidationHandlers
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireAccessTokenValidated>()
.UseScopedHandler<ValidateAccessToken>()
.SetOrder(ValidateIntrospectedTokenUsage.Descriptor.Order + 1_000)
.SetOrder(ValidateIntrospectedTokenProofOfPossession.Descriptor.Order + 1_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();

9
src/OpenIddict.Validation/OpenIddictValidationService.cs

@ -6,6 +6,7 @@
using System.Diagnostics;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
@ -318,11 +319,12 @@ public class OpenIddictValidationService
/// <param name="request">The token request.</param>
/// <param name="uri">The uri of the remote token endpoint.</param>
/// <param name="method">The client authentication method, if applicable.</param>
/// <param name="certificate">The client certificate, if applicable.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The response and the principal extracted from the introspection response.</returns>
internal async ValueTask<(OpenIddictResponse, ClaimsPrincipal)> SendIntrospectionRequestAsync(
OpenIddictConfiguration configuration, OpenIddictRequest request,
Uri uri, string? method, CancellationToken cancellationToken = default)
Uri uri, string? method, X509Certificate2? certificate, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(configuration);
ArgumentNullException.ThrowIfNull(request);
@ -357,9 +359,10 @@ public class OpenIddictValidationService
{
CancellationToken = cancellationToken,
ClientAuthenticationMethod = method,
RemoteUri = uri,
Configuration = configuration,
Request = request
RemoteUri = uri,
Request = request,
LocalCertificate = certificate
};
await dispatcher.DispatchAsync(context);

6
src/OpenIddict.Validation/OpenIddictValidationTransaction.cs

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

26
test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Discovery.cs

@ -993,7 +993,8 @@ public abstract partial class OpenIddictServerIntegrationTests
.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");
.SetMtlsTokenEndpointAliasUri("https://mtls.fabrikam.com/path/token_endpoint")
.SetMtlsUserInfoEndpointAliasUri("https://mtls.fabrikam.com/path/userinfo_endpoint");
});
await using var client = await server.CreateClientAsync();
@ -1016,6 +1017,9 @@ public abstract partial class OpenIddictServerIntegrationTests
Assert.Equal("https://mtls.fabrikam.com/path/token_endpoint",
(string?) response[Metadata.MtlsEndpointAliases]?[Metadata.TokenEndpoint]);
Assert.Equal("https://mtls.fabrikam.com/path/userinfo_endpoint",
(string?) response[Metadata.MtlsEndpointAliases]?[Metadata.UserInfoEndpoint]);
}
[Theory]
@ -1038,6 +1042,26 @@ public abstract partial class OpenIddictServerIntegrationTests
Assert.Equal(value, (bool?) response[Metadata.RequirePushedAuthorizationRequests]);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task HandleConfigurationRequest_TlsClientCertificateBoundAccessTokensIsReflected(bool value)
{
// Arrange
await using var server = await CreateServerAsync(options => options.Configure(options =>
{
options.UseClientCertificateBoundAccessTokens = value;
}));
await using var client = await server.CreateClientAsync();
// Act
var response = await client.GetAsync("/.well-known/openid-configuration");
// Assert
Assert.Equal(value, (bool?) response[Metadata.TlsClientCertificateBoundAccessTokens]);
}
[Theory]
[InlineData("custom_error", null, null)]
[InlineData("custom_error", "custom_description", null)]

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

@ -8,8 +8,10 @@ using System.Collections.Immutable;
using System.Net.Http;
using System.Security.Claims;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
using Moq;
using Xunit;
using static OpenIddict.Server.OpenIddictServerEvents;
@ -766,6 +768,99 @@ public abstract partial class OpenIddictServerIntegrationTests
Assert.Equal("AdventureWorks Cycles", (string?) response[Claims.ClientId]);
}
[Fact]
public async Task HandleIntrospectionRequest_TokenTypeIsNotReturnedForProofOfPossessionToken()
{
// Arrange
await using var server = await CreateServerAsync(options =>
{
options.EnableDegradedMode();
options.AddEventHandler<ValidateTokenContext>(builder =>
{
builder.UseInlineHandler(context =>
{
Assert.Equal("2YotnFZFEjr1zCsicMWpAA", context.Token);
context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer"))
.SetTokenType(TokenTypeIdentifiers.AccessToken)
.SetClaim(Claims.Subject, "Bob le Magnifique")
.SetClaim(Claims.Confirmation, new JsonObject
{
[JsonWebKeyParameterNames.X5tS256] = "P6DKnG90blx5LFI_It2ZAwe6TVJt43YaPiCCErpLx9s"
});
return ValueTask.CompletedTask;
});
builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500);
});
options.RemoveEventHandler(ValidateExpirationDate.Descriptor);
});
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/introspect", new OpenIddictRequest
{
Token = "2YotnFZFEjr1zCsicMWpAA",
TokenTypeHint = TokenTypeHints.AccessToken
});
// Assert
Assert.True((bool) response[Claims.Active]);
Assert.Null((string?) response[Claims.TokenType]);
}
[Fact]
public async Task HandleIntrospectionRequest_ConfirmationClaimIsReturnedForProofOfPossessionToken()
{
// Arrange
await using var server = await CreateServerAsync(options =>
{
options.EnableDegradedMode();
options.AddEventHandler<ValidateTokenContext>(builder =>
{
builder.UseInlineHandler(context =>
{
Assert.Equal("2YotnFZFEjr1zCsicMWpAA", context.Token);
context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer"))
.SetTokenType(TokenTypeIdentifiers.AccessToken)
.SetClaim(Claims.Subject, "Bob le Magnifique")
.SetClaim(Claims.Confirmation, new JsonObject
{
[JsonWebKeyParameterNames.X5tS256] = "P6DKnG90blx5LFI_It2ZAwe6TVJt43YaPiCCErpLx9s",
["custom_property"] = "custom_value"
});
return ValueTask.CompletedTask;
});
builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500);
});
options.RemoveEventHandler(ValidateExpirationDate.Descriptor);
});
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/introspect", new OpenIddictRequest
{
Token = "2YotnFZFEjr1zCsicMWpAA",
TokenTypeHint = TokenTypeHints.AccessToken
});
// Assert
Assert.True((bool) response[Claims.Active]);
Assert.Equal(2, response[Claims.Confirmation]?.Count);
Assert.Equal("P6DKnG90blx5LFI_It2ZAwe6TVJt43YaPiCCErpLx9s", (string?) response[Claims.Confirmation]?[JsonWebKeyParameterNames.X5tS256]);
Assert.Equal("custom_value", (string?) response[Claims.Confirmation]?["custom_property"]);
}
[Fact]
public async Task HandleIntrospectionRequest_NonBasicRefreshTokenClaimsAreNotReturned()
{

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

@ -1021,204 +1021,6 @@ public abstract partial class OpenIddictServerIntegrationTests
Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny<CancellationToken>()), Times.Once());
}
#if SUPPORTS_X509_CHAIN_POLICY_CUSTOM_TRUST_STORE
[Theory]
[InlineData(OpenIddictServerEndpointType.DeviceAuthorization)]
[InlineData(OpenIddictServerEndpointType.Introspection)]
[InlineData(OpenIddictServerEndpointType.PushedAuthorization)]
[InlineData(OpenIddictServerEndpointType.Revocation)]
[InlineData(OpenIddictServerEndpointType.Token)]
public async Task ProcessAuthentication_ClientCertificateCannotBeUsedByPublicClients(OpenIddictServerEndpointType type)
{
// Arrange
var application = new OpenIddictApplication();
var manager = CreateApplicationManager(mock =>
{
mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application);
mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
});
await using var server = await CreateServerAsync(options =>
{
options.AddEventHandler<ProcessAuthenticationContext>(builder =>
{
builder.UseInlineHandler(context =>
{
context.ClientCertificate = X509Certificate2.CreateFromPem($"""
-----BEGIN CERTIFICATE-----
MIIC8jCCAdqgAwIBAgIIYfcknj8KXN0wDQYJKoZIhvcNAQELBQAwIjEgMB4GA1UE
AxMXU2VsZi1zaWduZWQgY2VydGlmaWNhdGUwIBcNMjYwMjAxMTc1MjI2WhgPMjEy
NjAyMDExNzUyMjZaMCIxIDAeBgNVBAMTF1NlbGYtc2lnbmVkIGNlcnRpZmljYXRl
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA45uKd5cdlLmEBGLDEB75
o9e3/kMmQjVhhMeBsy4m2t7zw5jZo7OPcahXiXZHttom9tJm7BWPWvYx7p0N9+ss
h/E5lzKyV7ZXg+mM+KeECtVhiy+82BuIPelCshrpaV3lIg93y47FYLIWXxdggjt6
6VUaxzlTeo+IpuMz8IssL7VpJnjCT5NmqPNVkv1VR1uuetVqP7546ZFw31RiGl/0
I1uUlb7SwLwhLUK1iyLmGNA3VDB0m0DvLmlIEY3ZE5zxQp/Rxq6DfjbXm2LWJyu6
NO7k7JixXOorEl+6HdJZHTWNFK5jCo2ZZAwWn+uUuzgmLILPJFLDVutXjuEZpsym
uQIDAQABoyowKDAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0lAQH/BAwwCgYIKwYBBQUH
AwIwDQYJKoZIhvcNAQELBQADggEBAGHH3f/bkfViTvPE7yXJkB0bs88mYxajluMA
hgihEN5joPT6zHxMLBND2sitIozCMeeaj0rg+OaT/zDBgOLup/BM92UaPpYcgDCy
3tHqZLOOJOR4aYnHhIQUnx+NRtKEM4q/hL/xLHeliKmV7TQXISEZlTbb0gOU7TFp
nJlP60Vo9F/WD6xcKNxBgV5aB/+2FjiTTw2pF0VUmvcZdQAN5ysfrmKNXbvv1oCp
AohiwRiPrwe3mJ8iCqzEY/qQqImEiIT8WC2Fty+UYyBwfXMObi1AO++QkaMUbUJl
0aBuRoD85FLotjHIHXkFHERjOolheYdKt5nrGCCz/PmXBfsSCTo=
-----END CERTIFICATE-----
""");
return ValueTask.CompletedTask;
});
builder.SetOrder(ValidateClientType.Descriptor.Order - 500);
});
options.Services.AddSingleton(manager);
});
await using var client = await server.CreateClientAsync();
// Act
var response = type switch
{
OpenIddictServerEndpointType.Introspection => await client.PostAsync("/connect/introspect", new OpenIddictRequest
{
ClientId = "Fabrikam",
Token = "2YotnFZFEjr1zCsicMWpAA"
}),
OpenIddictServerEndpointType.DeviceAuthorization => await client.PostAsync("/connect/device", new OpenIddictRequest
{
ClientId = "Fabrikam"
}),
OpenIddictServerEndpointType.PushedAuthorization => await client.PostAsync("/connect/par", new OpenIddictRequest
{
ClientId = "Fabrikam",
RedirectUri = "http://www.fabrikam.com/path",
ResponseType = ResponseTypes.Code
}),
OpenIddictServerEndpointType.Revocation => await client.PostAsync("/connect/revoke", new OpenIddictRequest
{
ClientId = "Fabrikam",
Token = "SlAV32hkKG",
TokenTypeHint = TokenTypeHints.RefreshToken
}),
OpenIddictServerEndpointType.Token => await client.PostAsync("/connect/token", new OpenIddictRequest
{
ClientId = "Fabrikam",
GrantType = GrantTypes.Password,
Username = "johndoe",
Password = "A3ddj3w"
}),
_ => throw new NotSupportedException()
};
// Assert
Assert.Equal(Errors.InvalidClient, response.Error);
Assert.Equal(SR.GetResourceString(SR.ID2196), response.ErrorDescription);
Assert.Equal(SR.FormatID8000(SR.ID2196), response.ErrorUri);
Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny<CancellationToken>()), Times.Once());
}
#endif
[Theory]
[InlineData(OpenIddictServerEndpointType.DeviceAuthorization)]
[InlineData(OpenIddictServerEndpointType.Introspection)]
[InlineData(OpenIddictServerEndpointType.PushedAuthorization)]
[InlineData(OpenIddictServerEndpointType.Revocation)]
[InlineData(OpenIddictServerEndpointType.Token)]
public async Task ProcessAuthentication_ClientAuthenticationIsRequiredForNonPublicClients(OpenIddictServerEndpointType type)
{
// Arrange
var application = new OpenIddictApplication();
var manager = CreateApplicationManager(mock =>
{
mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<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 = type switch
{
OpenIddictServerEndpointType.Introspection => await client.PostAsync("/connect/introspect", new OpenIddictRequest
{
ClientAssertion = null,
ClientAssertionType = null,
ClientId = "Fabrikam",
ClientSecret = null,
Token = "2YotnFZFEjr1zCsicMWpAA"
}),
OpenIddictServerEndpointType.DeviceAuthorization => await client.PostAsync("/connect/device", new OpenIddictRequest
{
ClientAssertion = null,
ClientAssertionType = null,
ClientId = "Fabrikam",
ClientSecret = null
}),
OpenIddictServerEndpointType.PushedAuthorization => await client.PostAsync("/connect/par", new OpenIddictRequest
{
ClientAssertion = null,
ClientAssertionType = null,
ClientId = "Fabrikam",
ClientSecret = null,
RedirectUri = "http://www.fabrikam.com/path",
ResponseType = ResponseTypes.Code
}),
OpenIddictServerEndpointType.Revocation => await client.PostAsync("/connect/revoke", new OpenIddictRequest
{
ClientAssertion = null,
ClientAssertionType = null,
ClientId = "Fabrikam",
ClientSecret = null,
Token = "SlAV32hkKG",
TokenTypeHint = TokenTypeHints.RefreshToken
}),
OpenIddictServerEndpointType.Token => await client.PostAsync("/connect/token", new OpenIddictRequest
{
ClientAssertion = null,
ClientAssertionType = null,
ClientId = "Fabrikam",
ClientSecret = null,
GrantType = GrantTypes.Password,
Username = "johndoe",
Password = "A3ddj3w"
}),
_ => throw new NotSupportedException()
};
// Assert
Assert.Equal(Errors.InvalidClient, response.Error);
Assert.Equal(SR.GetResourceString(SR.ID2198), response.ErrorDescription);
Assert.Equal(SR.FormatID8000(SR.ID2198), response.ErrorUri);
Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny<CancellationToken>()), Times.Once());
}
[Theory]
[InlineData(OpenIddictServerEndpointType.Introspection)]
[InlineData(OpenIddictServerEndpointType.PushedAuthorization)]
@ -1297,114 +1099,6 @@ public abstract partial class OpenIddictServerIntegrationTests
}
#if SUPPORTS_X509_CHAIN_POLICY_CUSTOM_TRUST_STORE
[Theory]
[InlineData(OpenIddictServerEndpointType.DeviceAuthorization)]
[InlineData(OpenIddictServerEndpointType.Introspection)]
[InlineData(OpenIddictServerEndpointType.PushedAuthorization)]
[InlineData(OpenIddictServerEndpointType.Revocation)]
[InlineData(OpenIddictServerEndpointType.Token)]
public async Task ProcessAuthentication_ThrowsAnExceptionWhenGlobalSelfSignedChainPolicyIsNull(OpenIddictServerEndpointType type)
{
// Arrange
var application = new OpenIddictApplication();
var manager = CreateApplicationManager(mock =>
{
mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application);
mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
});
await using var server = await CreateServerAsync(options =>
{
options.AddEventHandler<ProcessAuthenticationContext>(builder =>
{
builder.UseInlineHandler(context =>
{
context.ClientCertificate = X509Certificate2.CreateFromPem($"""
-----BEGIN CERTIFICATE-----
MIIC8jCCAdqgAwIBAgIIYfcknj8KXN0wDQYJKoZIhvcNAQELBQAwIjEgMB4GA1UE
AxMXU2VsZi1zaWduZWQgY2VydGlmaWNhdGUwIBcNMjYwMjAxMTc1MjI2WhgPMjEy
NjAyMDExNzUyMjZaMCIxIDAeBgNVBAMTF1NlbGYtc2lnbmVkIGNlcnRpZmljYXRl
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA45uKd5cdlLmEBGLDEB75
o9e3/kMmQjVhhMeBsy4m2t7zw5jZo7OPcahXiXZHttom9tJm7BWPWvYx7p0N9+ss
h/E5lzKyV7ZXg+mM+KeECtVhiy+82BuIPelCshrpaV3lIg93y47FYLIWXxdggjt6
6VUaxzlTeo+IpuMz8IssL7VpJnjCT5NmqPNVkv1VR1uuetVqP7546ZFw31RiGl/0
I1uUlb7SwLwhLUK1iyLmGNA3VDB0m0DvLmlIEY3ZE5zxQp/Rxq6DfjbXm2LWJyu6
NO7k7JixXOorEl+6HdJZHTWNFK5jCo2ZZAwWn+uUuzgmLILPJFLDVutXjuEZpsym
uQIDAQABoyowKDAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0lAQH/BAwwCgYIKwYBBQUH
AwIwDQYJKoZIhvcNAQELBQADggEBAGHH3f/bkfViTvPE7yXJkB0bs88mYxajluMA
hgihEN5joPT6zHxMLBND2sitIozCMeeaj0rg+OaT/zDBgOLup/BM92UaPpYcgDCy
3tHqZLOOJOR4aYnHhIQUnx+NRtKEM4q/hL/xLHeliKmV7TQXISEZlTbb0gOU7TFp
nJlP60Vo9F/WD6xcKNxBgV5aB/+2FjiTTw2pF0VUmvcZdQAN5ysfrmKNXbvv1oCp
AohiwRiPrwe3mJ8iCqzEY/qQqImEiIT8WC2Fty+UYyBwfXMObi1AO++QkaMUbUJl
0aBuRoD85FLotjHIHXkFHERjOolheYdKt5nrGCCz/PmXBfsSCTo=
-----END CERTIFICATE-----
""");
return ValueTask.CompletedTask;
});
builder.SetOrder(ValidateClientType.Descriptor.Order - 500);
});
options.Services.AddSingleton(manager);
});
await using var client = await server.CreateClientAsync();
// Act and assert
var exception = type switch
{
OpenIddictServerEndpointType.Introspection =>
await Assert.ThrowsAsync<InvalidOperationException>(() => client.PostAsync("/connect/introspect", new OpenIddictRequest
{
ClientId = "Fabrikam",
Token = "2YotnFZFEjr1zCsicMWpAA"
})),
OpenIddictServerEndpointType.DeviceAuthorization =>
await Assert.ThrowsAsync<InvalidOperationException>(() => client.PostAsync("/connect/device", new OpenIddictRequest
{
ClientId = "Fabrikam"
})),
OpenIddictServerEndpointType.PushedAuthorization =>
await Assert.ThrowsAsync<InvalidOperationException>(() => client.PostAsync("/connect/par", new OpenIddictRequest
{
ClientId = "Fabrikam",
RedirectUri = "http://www.fabrikam.com/path",
ResponseType = ResponseTypes.Code
})),
OpenIddictServerEndpointType.Revocation =>
await Assert.ThrowsAsync<InvalidOperationException>(() => client.PostAsync("/connect/revoke", new OpenIddictRequest
{
ClientId = "Fabrikam",
Token = "SlAV32hkKG",
TokenTypeHint = TokenTypeHints.RefreshToken
})),
OpenIddictServerEndpointType.Token =>
await Assert.ThrowsAsync<InvalidOperationException>(() => client.PostAsync("/connect/token", new OpenIddictRequest
{
ClientId = "Fabrikam",
GrantType = GrantTypes.Password,
Username = "johndoe",
Password = "A3ddj3w"
})),
_ => throw new NotSupportedException()
};
Assert.Equal(SR.GetResourceString(SR.ID0506), exception.Message);
Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny<CancellationToken>()), Times.AtLeastOnce());
}
[Theory]
[InlineData(OpenIddictServerEndpointType.DeviceAuthorization)]
[InlineData(OpenIddictServerEndpointType.Introspection)]
@ -1424,19 +1118,20 @@ public abstract partial class OpenIddictServerIntegrationTests
mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
mock.Setup(manager => manager.GetSelfSignedClientCertificateChainPolicyAsync(application, It.IsAny<X509ChainPolicy>(), It.IsAny<CancellationToken>()))
mock.Setup(manager => manager.GetSelfSignedTlsClientAuthenticationPolicyAsync(application, It.IsAny<X509ChainPolicy>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(value: null);
});
await using var server = await CreateServerAsync(options =>
{
options.Configure(options => options.SelfSignedClientCertificateChainPolicy = new X509ChainPolicy());
options.Configure(options => options.ClientAuthenticationMethods.Add(ClientAuthenticationMethods.SelfSignedTlsClientAuth));
options.Configure(options => options.SelfSignedTlsClientAuthenticationPolicy = new X509ChainPolicy());
options.AddEventHandler<ProcessAuthenticationContext>(builder =>
{
builder.UseInlineHandler(context =>
{
context.ClientCertificate = X509Certificate2.CreateFromPem($"""
context.Transaction.RemoteCertificate = X509Certificate2.CreateFromPem($"""
-----BEGIN CERTIFICATE-----
MIIC8jCCAdqgAwIBAgIIYfcknj8KXN0wDQYJKoZIhvcNAQELBQAwIjEgMB4GA1UE
AxMXU2VsZi1zaWduZWQgY2VydGlmaWNhdGUwIBcNMjYwMjAxMTc1MjI2WhgPMjEy
@ -1514,7 +1209,8 @@ public abstract partial class OpenIddictServerIntegrationTests
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.GetSelfSignedClientCertificateChainPolicyAsync(application, It.IsAny<X509ChainPolicy>(), It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(manager => manager.GetSelfSignedTlsClientAuthenticationPolicyAsync(application,
It.IsAny<X509ChainPolicy>(), It.IsAny<CancellationToken>()), Times.Once());
}
[Theory]
@ -1557,22 +1253,23 @@ public abstract partial class OpenIddictServerIntegrationTests
mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
mock.Setup(manager => manager.GetSelfSignedClientCertificateChainPolicyAsync(application, It.IsAny<X509ChainPolicy>(), It.IsAny<CancellationToken>()))
mock.Setup(manager => manager.GetSelfSignedTlsClientAuthenticationPolicyAsync(application, It.IsAny<X509ChainPolicy>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(policy);
mock.Setup(manager => manager.ValidateSelfSignedClientCertificateAsync(application, certificate, policy, It.IsAny<CancellationToken>()))
mock.Setup(manager => manager.ValidateSelfSignedTlsClientCertificateAsync(application, certificate, policy, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
});
await using var server = await CreateServerAsync(options =>
{
options.Configure(options => options.SelfSignedClientCertificateChainPolicy = new X509ChainPolicy());
options.Configure(options => options.ClientAuthenticationMethods.Add(ClientAuthenticationMethods.SelfSignedTlsClientAuth));
options.Configure(options => options.SelfSignedTlsClientAuthenticationPolicy = new X509ChainPolicy());
options.AddEventHandler<ProcessAuthenticationContext>(builder =>
{
builder.UseInlineHandler(context =>
{
context.ClientCertificate = certificate;
context.Transaction.RemoteCertificate = certificate;
return ValueTask.CompletedTask;
});
@ -1631,8 +1328,9 @@ public abstract partial class OpenIddictServerIntegrationTests
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.GetSelfSignedClientCertificateChainPolicyAsync(application, It.IsAny<X509ChainPolicy>(), It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(manager => manager.ValidateSelfSignedClientCertificateAsync(application, certificate, policy, It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(manager => manager.GetSelfSignedTlsClientAuthenticationPolicyAsync(application, It.IsAny<X509ChainPolicy>(), It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(manager => manager.ValidateSelfSignedTlsClientCertificateAsync(application, certificate,
It.Is<X509ChainPolicy>(policy => policy.CustomTrustStore.Count == 0 && policy.ExtraStore.Count == 0), It.IsAny<CancellationToken>()), Times.Once());
}
[Theory]
@ -1641,10 +1339,31 @@ public abstract partial class OpenIddictServerIntegrationTests
[InlineData(OpenIddictServerEndpointType.PushedAuthorization)]
[InlineData(OpenIddictServerEndpointType.Revocation)]
[InlineData(OpenIddictServerEndpointType.Token)]
public async Task ProcessAuthentication_ThrowsAnExceptionWhenGlobalPkiChainPolicyIsNull(OpenIddictServerEndpointType type)
public async Task ProcessAuthentication_SelfSignedClientCertificatePolicyIsAmendedToAllowDynamicCertificatesForPublicClients(OpenIddictServerEndpointType type)
{
// Arrange
var application = new OpenIddictApplication();
var policy = new X509ChainPolicy();
var certificate = X509Certificate2.CreateFromPem($"""
-----BEGIN CERTIFICATE-----
MIIC8jCCAdqgAwIBAgIIYfcknj8KXN0wDQYJKoZIhvcNAQELBQAwIjEgMB4GA1UE
AxMXU2VsZi1zaWduZWQgY2VydGlmaWNhdGUwIBcNMjYwMjAxMTc1MjI2WhgPMjEy
NjAyMDExNzUyMjZaMCIxIDAeBgNVBAMTF1NlbGYtc2lnbmVkIGNlcnRpZmljYXRl
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA45uKd5cdlLmEBGLDEB75
o9e3/kMmQjVhhMeBsy4m2t7zw5jZo7OPcahXiXZHttom9tJm7BWPWvYx7p0N9+ss
h/E5lzKyV7ZXg+mM+KeECtVhiy+82BuIPelCshrpaV3lIg93y47FYLIWXxdggjt6
6VUaxzlTeo+IpuMz8IssL7VpJnjCT5NmqPNVkv1VR1uuetVqP7546ZFw31RiGl/0
I1uUlb7SwLwhLUK1iyLmGNA3VDB0m0DvLmlIEY3ZE5zxQp/Rxq6DfjbXm2LWJyu6
NO7k7JixXOorEl+6HdJZHTWNFK5jCo2ZZAwWn+uUuzgmLILPJFLDVutXjuEZpsym
uQIDAQABoyowKDAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0lAQH/BAwwCgYIKwYBBQUH
AwIwDQYJKoZIhvcNAQELBQADggEBAGHH3f/bkfViTvPE7yXJkB0bs88mYxajluMA
hgihEN5joPT6zHxMLBND2sitIozCMeeaj0rg+OaT/zDBgOLup/BM92UaPpYcgDCy
3tHqZLOOJOR4aYnHhIQUnx+NRtKEM4q/hL/xLHeliKmV7TQXISEZlTbb0gOU7TFp
nJlP60Vo9F/WD6xcKNxBgV5aB/+2FjiTTw2pF0VUmvcZdQAN5ysfrmKNXbvv1oCp
AohiwRiPrwe3mJ8iCqzEY/qQqImEiIT8WC2Fty+UYyBwfXMObi1AO++QkaMUbUJl
0aBuRoD85FLotjHIHXkFHERjOolheYdKt5nrGCCz/PmXBfsSCTo=
-----END CERTIFICATE-----
""");
var manager = CreateApplicationManager(mock =>
{
@ -1652,43 +1371,25 @@ public abstract partial class OpenIddictServerIntegrationTests
.ReturnsAsync(application);
mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
mock.Setup(manager => manager.GetSelfSignedTlsClientAuthenticationPolicyAsync(application, It.IsAny<X509ChainPolicy>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(policy);
mock.Setup(manager => manager.ValidateSelfSignedTlsClientCertificateAsync(application, certificate, policy, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
});
await using var server = await CreateServerAsync(options =>
{
options.Configure(options => options.ClientAuthenticationMethods.Add(ClientAuthenticationMethods.SelfSignedTlsClientAuth));
options.Configure(options => options.SelfSignedTlsClientAuthenticationPolicy = new X509ChainPolicy());
options.AddEventHandler<ProcessAuthenticationContext>(builder =>
{
builder.UseInlineHandler(context =>
{
context.ClientCertificate = X509Certificate2.CreateFromPem($"""
-----BEGIN CERTIFICATE-----
MIIEejCCAmKgAwIBAgIQTmWjGbWFVbvtnm57bOVMZDANBgkqhkiG9w0BAQsFADAa
MRgwFgYDVQQDEw9JbnRlcm1lZGlhdGUgQ0EwIBcNMjYwMjAxMDMwNTIwWhgPMjEy
NjAyMDIwMzA1MjBaMBoxGDAWBgNVBAMTD0VuZCBjZXJ0aWZpY2F0ZTCCASIwDQYJ
KoZIhvcNAQEBBQADggEPADCCAQoCggEBAJZ++ilBjvvOBIkZehV2T5dIDTpggJPV
s+6/R2/6zMa0ykzEqaIbuvhhCaY0tDzebTYqUu9omlZkvx4jhxyA44lhwJsElqMx
ANxsfSNUucSiJdOWXaVCQs8hLWn9ATfflE+qJNFx8zZq4nqmfkj8DMQwsej3+Ilo
+FwdV1/2gyifMR0TOb/iZsgh+d386B4hIK94REZbyZ4Diod13VkDPY7I9LD8hBy7
8jU5SflnHdFmG4I1IQmZcSGWfrCl0PIFymHkooeXwUm5sBebZl970908DYNB95g6
Fj4wBxNdrhm8Ty4DHZSikOyt/kmbrdcc8OEVYSFlaSF7QDw6X73pEYUCAwEAAaOB
uTCBtjAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIHgDAWBgNVHSUBAf8EDDAK
BggrBgEFBQcDAjAdBgNVHQ4EFgQU/b5TO/v+uy1JhF2wYpYNrTBQwqgwSQYDVR0j
BEIwQIAUQG7qZEGkePGiBl7n9npI/BZ1MN6hFqQUMBIxEDAOBgNVBAMTB1Jvb3Qg
Q0GCEEZVlMw4Ppnm5GIPGl2I1nwwFAYDVR0RBA0wC4IJbG9jYWxob3N0MA0GCSqG
SIb3DQEBCwUAA4ICAQB/GDA+YullFclspzw/9d3jFgorKJdISB1BKZyWD5tuQ5fP
WX4MOsgnjylbK8wGfG1wW2NJfKc/lWYzoxWtQDDCq9kVgUvd4JBlqR3w6reowDeE
jn/KGsArtjMMv0xvVnP8Wux2GLL2RYRUq6EpcQycN9/uoVyp+JRnxT6vK0y4QzYH
G7C6z/JfoAp7UnS61Be9VlcW1I2H5WiHuzuMG4IrMPdTGJDftSJYfXaBMzAnXdSY
4BP80LsPbj1Jfuz+7tvrIO3gPmBJEprN1g0dKbcWPMRA867xLkQIQTiSnrVFADvr
UxO2G8KX/Yn7n5c68MfEHhFi9ndeijQfe7awG0aQjWX/XbaPpbaOAXxcozpzPIT/
UlciCDCofpr62BdOWJJ6XQLyx5lRg9XcB6TwRsvx9zRW434iGBmGdLMlqHVN7I2v
/kKNWEzOOa4hphG9OCyg3ZOcArCslUiwwfUGe8cOMKf8O63+NY6UUyU5S50EzsdN
5nAK3WkMijGiMbReB/5oCLbU/B9hgEghKcbd3X2QY21MBg+GCB1z9aPduKtleAQm
WExEzLEnb3Kwfr5+O84J3DXisQLG8CO3T9c9uz4Tp95LIdUD4v386dGO/nYnHcZ8
8Fpjqr/MfTyPgOnMusa3yKlAMypPRmtkhkxOk4olCaT6WDRQtoKk/RisREU8nw==
-----END CERTIFICATE-----
""");
context.Transaction.RemoteCertificate = certificate;
return ValueTask.CompletedTask;
});
@ -1701,54 +1402,55 @@ public abstract partial class OpenIddictServerIntegrationTests
await using var client = await server.CreateClientAsync();
// Act and assert
var exception = type switch
// Act
var response = type switch
{
OpenIddictServerEndpointType.Introspection =>
await Assert.ThrowsAsync<InvalidOperationException>(() => client.PostAsync("/connect/introspect", new OpenIddictRequest
{
ClientId = "Fabrikam",
Token = "2YotnFZFEjr1zCsicMWpAA"
})),
OpenIddictServerEndpointType.Introspection => await client.PostAsync("/connect/introspect", new OpenIddictRequest
{
ClientId = "Fabrikam",
Token = "2YotnFZFEjr1zCsicMWpAA"
}),
OpenIddictServerEndpointType.DeviceAuthorization =>
await Assert.ThrowsAsync<InvalidOperationException>(() => client.PostAsync("/connect/device", new OpenIddictRequest
{
ClientId = "Fabrikam"
})),
OpenIddictServerEndpointType.DeviceAuthorization => await client.PostAsync("/connect/device", new OpenIddictRequest
{
ClientId = "Fabrikam"
}),
OpenIddictServerEndpointType.PushedAuthorization =>
await Assert.ThrowsAsync<InvalidOperationException>(() => client.PostAsync("/connect/par", new OpenIddictRequest
{
ClientId = "Fabrikam",
RedirectUri = "http://www.fabrikam.com/path",
ResponseType = ResponseTypes.Code
})),
OpenIddictServerEndpointType.PushedAuthorization => await client.PostAsync("/connect/par", new OpenIddictRequest
{
ClientId = "Fabrikam",
RedirectUri = "http://www.fabrikam.com/path",
ResponseType = ResponseTypes.Code
}),
OpenIddictServerEndpointType.Revocation =>
await Assert.ThrowsAsync<InvalidOperationException>(() => client.PostAsync("/connect/revoke", new OpenIddictRequest
{
ClientId = "Fabrikam",
Token = "SlAV32hkKG",
TokenTypeHint = TokenTypeHints.RefreshToken
})),
OpenIddictServerEndpointType.Revocation => await client.PostAsync("/connect/revoke", new OpenIddictRequest
{
ClientId = "Fabrikam",
Token = "SlAV32hkKG",
TokenTypeHint = TokenTypeHints.RefreshToken
}),
OpenIddictServerEndpointType.Token =>
await Assert.ThrowsAsync<InvalidOperationException>(() => client.PostAsync("/connect/token", new OpenIddictRequest
{
ClientId = "Fabrikam",
GrantType = GrantTypes.Password,
Username = "johndoe",
Password = "A3ddj3w"
})),
OpenIddictServerEndpointType.Token => await client.PostAsync("/connect/token", new OpenIddictRequest
{
ClientId = "Fabrikam",
GrantType = GrantTypes.Password,
Username = "johndoe",
Password = "A3ddj3w"
}),
_ => throw new NotSupportedException()
};
Assert.Equal(SR.GetResourceString(SR.ID0505), exception.Message);
// Assert
Assert.Equal(Errors.InvalidClient, response.Error);
Assert.Equal(SR.GetResourceString(SR.ID2197), response.ErrorDescription);
Assert.Equal(SR.FormatID8000(SR.ID2197), response.ErrorUri);
Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(manager => manager.GetSelfSignedTlsClientAuthenticationPolicyAsync(application, It.IsAny<X509ChainPolicy>(), It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(manager => manager.ValidateSelfSignedTlsClientCertificateAsync(application, certificate,
It.Is<X509ChainPolicy>(policy => policy.CustomTrustStore.Contains(certificate)), It.IsAny<CancellationToken>()), Times.Once());
}
[Theory]
@ -1770,19 +1472,20 @@ public abstract partial class OpenIddictServerIntegrationTests
mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
mock.Setup(manager => manager.GetClientCertificateChainPolicyAsync(application, It.IsAny<X509ChainPolicy>(), It.IsAny<CancellationToken>()))
mock.Setup(manager => manager.GetPublicKeyInfrastructureTlsClientAuthenticationPolicyAsync(application, It.IsAny<X509ChainPolicy>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(value: null);
});
await using var server = await CreateServerAsync(options =>
{
options.Configure(options => options.ClientCertificateChainPolicy = new X509ChainPolicy());
options.Configure(options => options.ClientAuthenticationMethods.Add(ClientAuthenticationMethods.TlsClientAuth));
options.Configure(options => options.PublicKeyInfrastructureTlsClientAuthenticationPolicy = new X509ChainPolicy());
options.AddEventHandler<ProcessAuthenticationContext>(builder =>
{
builder.UseInlineHandler(context =>
{
context.ClientCertificate = X509Certificate2.CreateFromPem($"""
context.Transaction.RemoteCertificate = X509Certificate2.CreateFromPem($"""
-----BEGIN CERTIFICATE-----
MIIEejCCAmKgAwIBAgIQTmWjGbWFVbvtnm57bOVMZDANBgkqhkiG9w0BAQsFADAa
MRgwFgYDVQQDEw9JbnRlcm1lZGlhdGUgQ0EwIBcNMjYwMjAxMDMwNTIwWhgPMjEy
@ -1868,7 +1571,8 @@ public abstract partial class OpenIddictServerIntegrationTests
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.GetClientCertificateChainPolicyAsync(application, It.IsAny<X509ChainPolicy>(), It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(manager => manager.GetPublicKeyInfrastructureTlsClientAuthenticationPolicyAsync(application,
It.IsAny<X509ChainPolicy>(), It.IsAny<CancellationToken>()), Times.Once());
}
[Theory]
@ -1919,22 +1623,23 @@ public abstract partial class OpenIddictServerIntegrationTests
mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
mock.Setup(manager => manager.GetClientCertificateChainPolicyAsync(application, It.IsAny<X509ChainPolicy>(), It.IsAny<CancellationToken>()))
mock.Setup(manager => manager.GetPublicKeyInfrastructureTlsClientAuthenticationPolicyAsync(application, It.IsAny<X509ChainPolicy>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(policy);
mock.Setup(manager => manager.ValidateClientCertificateAsync(application, certificate, policy, It.IsAny<CancellationToken>()))
mock.Setup(manager => manager.ValidatePublicKeyInfrastructureTlsClientCertificateAsync(application, certificate, policy, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
});
await using var server = await CreateServerAsync(options =>
{
options.Configure(options => options.ClientCertificateChainPolicy = new X509ChainPolicy());
options.Configure(options => options.ClientAuthenticationMethods.Add(ClientAuthenticationMethods.TlsClientAuth));
options.Configure(options => options.PublicKeyInfrastructureTlsClientAuthenticationPolicy = new X509ChainPolicy());
options.AddEventHandler<ProcessAuthenticationContext>(builder =>
{
builder.UseInlineHandler(context =>
{
context.ClientCertificate = certificate;
context.Transaction.RemoteCertificate = certificate;
return ValueTask.CompletedTask;
});
@ -1993,11 +1698,102 @@ public abstract partial class OpenIddictServerIntegrationTests
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.GetClientCertificateChainPolicyAsync(application, It.IsAny<X509ChainPolicy>(), It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(manager => manager.ValidateClientCertificateAsync(application, certificate, policy, It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(manager => manager.GetPublicKeyInfrastructureTlsClientAuthenticationPolicyAsync(application, It.IsAny<X509ChainPolicy>(), It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(manager => manager.ValidatePublicKeyInfrastructureTlsClientCertificateAsync(application,
certificate, policy, It.IsAny<CancellationToken>()), Times.Once());
}
#endif
[Theory]
[InlineData(OpenIddictServerEndpointType.DeviceAuthorization)]
[InlineData(OpenIddictServerEndpointType.Introspection)]
[InlineData(OpenIddictServerEndpointType.PushedAuthorization)]
[InlineData(OpenIddictServerEndpointType.Revocation)]
[InlineData(OpenIddictServerEndpointType.Token)]
public async Task ProcessAuthentication_ClientAuthenticationIsRequiredForNonPublicClients(OpenIddictServerEndpointType type)
{
// Arrange
var application = new OpenIddictApplication();
var manager = CreateApplicationManager(mock =>
{
mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<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 = type switch
{
OpenIddictServerEndpointType.Introspection => await client.PostAsync("/connect/introspect", new OpenIddictRequest
{
ClientAssertion = null,
ClientAssertionType = null,
ClientId = "Fabrikam",
ClientSecret = null,
Token = "2YotnFZFEjr1zCsicMWpAA"
}),
OpenIddictServerEndpointType.DeviceAuthorization => await client.PostAsync("/connect/device", new OpenIddictRequest
{
ClientAssertion = null,
ClientAssertionType = null,
ClientId = "Fabrikam",
ClientSecret = null
}),
OpenIddictServerEndpointType.PushedAuthorization => await client.PostAsync("/connect/par", new OpenIddictRequest
{
ClientAssertion = null,
ClientAssertionType = null,
ClientId = "Fabrikam",
ClientSecret = null,
RedirectUri = "http://www.fabrikam.com/path",
ResponseType = ResponseTypes.Code
}),
OpenIddictServerEndpointType.Revocation => await client.PostAsync("/connect/revoke", new OpenIddictRequest
{
ClientAssertion = null,
ClientAssertionType = null,
ClientId = "Fabrikam",
ClientSecret = null,
Token = "SlAV32hkKG",
TokenTypeHint = TokenTypeHints.RefreshToken
}),
OpenIddictServerEndpointType.Token => await client.PostAsync("/connect/token", new OpenIddictRequest
{
ClientAssertion = null,
ClientAssertionType = null,
ClientId = "Fabrikam",
ClientSecret = null,
GrantType = GrantTypes.Password,
Username = "johndoe",
Password = "A3ddj3w"
}),
_ => throw new NotSupportedException()
};
// Assert
Assert.Equal(Errors.InvalidClient, response.Error);
Assert.Equal(SR.GetResourceString(SR.ID2198), response.ErrorDescription);
Assert.Equal(SR.FormatID8000(SR.ID2198), response.ErrorUri);
Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task ProcessAuthentication_RequestTokenPrincipalIsNotPopulatedWhenRequestTokenIsMissing()
{

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

@ -787,7 +787,7 @@ public class OpenIddictServerBuilderTests
}
[Fact]
public void EnablePublicKeyInfrastructureClientCertificateAuthentication_ThrowsAnExceptionForNullCertificates()
public void EnablePublicKeyInfrastructureTlsClientAuthentication_ThrowsAnExceptionForNullCertificates()
{
// Arrange
var services = CreateServices();
@ -795,14 +795,14 @@ public class OpenIddictServerBuilderTests
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() =>
builder.EnablePublicKeyInfrastructureClientCertificateAuthentication(certificates: null!));
builder.EnablePublicKeyInfrastructureTlsClientAuthentication(certificates: null!));
Assert.Equal("certificates", exception.ParamName);
}
#if SUPPORTS_X509_CHAIN_POLICY_CUSTOM_TRUST_STORE
[Fact]
public void EnablePublicKeyInfrastructureClientCertificateAuthentication_ThrowsAnExceptionWhenNoRootCertificateProvided()
public void EnablePublicKeyInfrastructureTlsClientAuthentication_ThrowsAnExceptionWhenNoRootCertificateProvided()
{
// Arrange
var services = CreateServices();
@ -847,13 +847,13 @@ public class OpenIddictServerBuilderTests
// Act and assert
var exception = Assert.Throws<ArgumentException>(() =>
builder.EnablePublicKeyInfrastructureClientCertificateAuthentication(certificates));
builder.EnablePublicKeyInfrastructureTlsClientAuthentication(certificates));
Assert.Equal("certificates", exception.ParamName);
}
[Fact]
public void EnablePublicKeyInfrastructureClientCertificateAuthentication_ThrowsAnExceptionWhenEndCertificateProvided()
public void EnablePublicKeyInfrastructureTlsClientAuthentication_ThrowsAnExceptionWhenEndCertificateProvided()
{
// Arrange
var services = CreateServices();
@ -961,13 +961,13 @@ public class OpenIddictServerBuilderTests
// Act and assert
var exception = Assert.Throws<ArgumentException>(() =>
builder.EnablePublicKeyInfrastructureClientCertificateAuthentication(certificates));
builder.EnablePublicKeyInfrastructureTlsClientAuthentication(certificates));
Assert.Equal("certificates", exception.ParamName);
}
[Fact]
public void EnablePublicKeyInfrastructureClientCertificateAuthentication_PolicyIsCorrectlyConfigured()
public void EnablePublicKeyInfrastructureTlsClientAuthentication_PolicyIsCorrectlyConfigured()
{
// Arrange
var services = CreateServices();
@ -1044,19 +1044,19 @@ public class OpenIddictServerBuilderTests
};
// Act
builder.EnablePublicKeyInfrastructureClientCertificateAuthentication(certificates);
builder.EnablePublicKeyInfrastructureTlsClientAuthentication(certificates);
var options = GetOptions(services);
// Assert
Assert.NotNull(options.ClientCertificateChainPolicy);
Assert.Equal(X509ChainTrustMode.CustomRootTrust, options.ClientCertificateChainPolicy.TrustMode);
Assert.Contains(options.ClientCertificateChainPolicy.ApplicationPolicy.Cast<Oid>(),
Assert.NotNull(options.PublicKeyInfrastructureTlsClientAuthenticationPolicy);
Assert.Equal(X509ChainTrustMode.CustomRootTrust, options.PublicKeyInfrastructureTlsClientAuthenticationPolicy.TrustMode);
Assert.Contains(options.PublicKeyInfrastructureTlsClientAuthenticationPolicy.ApplicationPolicy.Cast<Oid>(),
oid => oid.Value == ObjectIdentifiers.ExtendedKeyUsages.ClientAuthentication);
}
[Fact]
public void EnablePublicKeyInfrastructureClientCertificateAuthentication_ThrowsAnExceptionWhenTrustModeIsChanged()
public void EnablePublicKeyInfrastructureTlsClientAuthentication_ThrowsAnExceptionWhenTrustModeIsChanged()
{
// Arrange
var services = CreateServices();
@ -1134,7 +1134,7 @@ public class OpenIddictServerBuilderTests
// Act and assert
var exception = Assert.Throws<InvalidOperationException>(() =>
builder.EnablePublicKeyInfrastructureClientCertificateAuthentication(certificates,
builder.EnablePublicKeyInfrastructureTlsClientAuthentication(certificates,
policy => policy.TrustMode = X509ChainTrustMode.System));
Assert.Equal(SR.GetResourceString(SR.ID0509), exception.Message);
@ -1142,7 +1142,7 @@ public class OpenIddictServerBuilderTests
#endif
[Fact]
public void EnableSelfSignedClientCertificateAuthentication_ThrowsAnExceptionForNullConfiguration()
public void EnableSelfSignedTlsClientAuthentication_ThrowsAnExceptionForNullConfiguration()
{
// Arrange
var services = CreateServices();
@ -1150,34 +1150,34 @@ public class OpenIddictServerBuilderTests
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() =>
builder.EnableSelfSignedClientCertificateAuthentication(configuration: null!));
builder.EnableSelfSignedTlsClientAuthentication(configuration: null!));
Assert.Equal("configuration", exception.ParamName);
}
#if SUPPORTS_X509_CHAIN_POLICY_CUSTOM_TRUST_STORE
[Fact]
public void EnableSelfSignedClientCertificateAuthentication_PolicyIsCorrectlyConfigured()
public void EnableSelfSignedTlsClientAuthentication_PolicyIsCorrectlyConfigured()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.EnableSelfSignedClientCertificateAuthentication();
builder.EnableSelfSignedTlsClientAuthentication();
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>(),
Assert.NotNull(options.SelfSignedTlsClientAuthenticationPolicy);
Assert.Equal(X509ChainTrustMode.CustomRootTrust, options.SelfSignedTlsClientAuthenticationPolicy.TrustMode);
Assert.Equal(X509RevocationMode.NoCheck, options.SelfSignedTlsClientAuthenticationPolicy.RevocationMode);
Assert.Contains(options.SelfSignedTlsClientAuthenticationPolicy.ApplicationPolicy.Cast<Oid>(),
oid => oid.Value == ObjectIdentifiers.ExtendedKeyUsages.ClientAuthentication);
}
[Fact]
public void EnableSelfSignedClientCertificateAuthentication_ThrowsAnExceptionWhenTrustModeIsChanged()
public void EnableSelfSignedTlsClientAuthentication_ThrowsAnExceptionWhenTrustModeIsChanged()
{
// Arrange
var services = CreateServices();
@ -1185,7 +1185,7 @@ public class OpenIddictServerBuilderTests
// Act and assert
var exception = Assert.Throws<InvalidOperationException>(() =>
builder.EnableSelfSignedClientCertificateAuthentication(
builder.EnableSelfSignedTlsClientAuthentication(
policy => policy.TrustMode = X509ChainTrustMode.System));
Assert.Equal(SR.GetResourceString(SR.ID0509), exception.Message);
@ -2048,6 +2048,36 @@ public class OpenIddictServerBuilderTests
Assert.Equal(new Uri("http://localhost/endpoint-path"), options.MtlsTokenEndpointAliasUri);
}
[Theory]
[InlineData("~/path")]
public void SetMtlsUserInfoEndpointAliasUri_ThrowsExceptionForInvalidRelativeUri(string uri)
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act and assert
var exception = Assert.Throws<ArgumentException>(() => builder.SetMtlsUserInfoEndpointAliasUri(new Uri(uri, UriKind.RelativeOrAbsolute)));
Assert.Equal("uri", exception.ParamName);
Assert.Contains(SR.FormatID0081("~"), exception.Message);
}
[Fact]
public void SetMtlsUserInfoEndpointAliasUri_AddsUri()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.SetMtlsUserInfoEndpointAliasUri("http://localhost/endpoint-path");
var options = GetOptions(services);
// Assert
Assert.Equal(new Uri("http://localhost/endpoint-path"), options.MtlsUserInfoEndpointAliasUri);
}
[Fact]
public void SetIntrospectionEndpointUris_ThrowsExceptionWhenUrisIsNull()
{
@ -3149,6 +3179,38 @@ public class OpenIddictServerBuilderTests
Assert.True(options.UseReferenceRefreshTokens);
}
[Fact]
public void UseClientCertificateBoundAccessTokens_CertificateBoundTokensAreEnabled()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.UseClientCertificateBoundAccessTokens();
var options = GetOptions(services);
// Assert
Assert.True(options.UseClientCertificateBoundAccessTokens);
}
[Fact]
public void UseClientCertificateBoundRefreshTokens_CertificateBoundTokensAreEnabled()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.UseClientCertificateBoundRefreshTokens();
var options = GetOptions(services);
// Assert
Assert.True(options.UseClientCertificateBoundRefreshTokens);
}
private static IServiceCollection CreateServices()
{
return new ServiceCollection().AddOptions();

Loading…
Cancel
Save