diff --git a/gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs b/gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs index 49d4a40a..bd733d18 100644 --- a/gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs +++ b/gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs @@ -285,6 +285,18 @@ public sealed partial class OpenIddictClientWebIntegrationBuilder return Set(registration => registration.ClientSecret = secret); } + /// + /// Sets the client type (typically, ""public"" or ""confidential""). + /// + /// The client type. + /// The instance. + public {{ provider.name }} SetClientType(string type) + { + ArgumentException.ThrowIfNullOrEmpty(type); + + return Set(registration => registration.ClientType = type); + } + /// /// Sets the post-logout redirection URI, if applicable. /// diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Controllers/HomeController.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Controllers/HomeController.cs index ef5ecd53..0f5db338 100644 --- a/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Controllers/HomeController.cs +++ b/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); diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Startup.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Startup.cs index 0f017395..73394e40 100644 --- a/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Startup.cs +++ b/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 } diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Startup.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Startup.cs index 9478fa2b..361c92cb 100644 --- a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Startup.cs +++ b/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 }) diff --git a/sandbox/OpenIddict.Sandbox.Console.Client/InteractiveService.cs b/sandbox/OpenIddict.Sandbox.Console.Client/InteractiveService.cs index 856be8ab..e3d83376 100644 --- a/sandbox/OpenIddict.Sandbox.Console.Client/InteractiveService.cs +++ b/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 } } diff --git a/shared/OpenIddict.Extensions/OpenIddictHelpers.cs b/shared/OpenIddict.Extensions/OpenIddictHelpers.cs index e314c520..65bc5579 100644 --- a/shared/OpenIddict.Extensions/OpenIddictHelpers.cs +++ b/shared/OpenIddict.Extensions/OpenIddictHelpers.cs @@ -1104,6 +1104,22 @@ internal static class OpenIddictHelpers return certificate.SubjectName.RawData.AsSpan().SequenceEqual(certificate.IssuerName.RawData); } + /// + /// Determines whether the specified is suitable for client authentication. + /// + /// The . + /// + /// if the certificate is suitable for client authentication, otherwise. + /// + 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); + } + /// /// Determines whether the items contained in /// are of the specified . diff --git a/shared/OpenIddict.Extensions/OpenIddictPolyfills.cs b/shared/OpenIddict.Extensions/OpenIddictPolyfills.cs index 11332e11..cce7a9c2 100644 --- a/shared/OpenIddict.Extensions/OpenIddictPolyfills.cs +++ b/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; diff --git a/src/OpenIddict.Abstractions/Managers/IOpenIddictApplicationManager.cs b/src/OpenIddict.Abstractions/Managers/IOpenIddictApplicationManager.cs index 2accbe4d..49a55504 100644 --- a/src/OpenIddict.Abstractions/Managers/IOpenIddictApplicationManager.cs +++ b/src/OpenIddict.Abstractions/Managers/IOpenIddictApplicationManager.cs @@ -174,19 +174,6 @@ public interface IOpenIddictApplicationManager Func, TState, IQueryable> query, TState state, CancellationToken cancellationToken = default); - /// - /// Retrieves the client certificate chain policy enforced for this application. - /// - /// The application. - /// The base policy from which the returned instance will be derived. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the client certificate chain policy enforced for this application. - /// - ValueTask GetClientCertificateChainPolicyAsync( - object application, X509ChainPolicy policy, CancellationToken cancellationToken = default); - /// /// Retrieves the client identifier associated with an application. /// @@ -324,6 +311,19 @@ public interface IOpenIddictApplicationManager /// ValueTask> GetPropertiesAsync(object application, CancellationToken cancellationToken = default); + /// + /// Retrieves the PKI client certificate authentication policy enforced for this application. + /// + /// The application. + /// The base policy from which the returned instance will be derived. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, whose + /// result returns the PKI client certificate authentication policy enforced for this application. + /// + ValueTask GetPublicKeyInfrastructureTlsClientAuthenticationPolicyAsync( + object application, X509ChainPolicy policy, CancellationToken cancellationToken = default); + /// /// Retrieves the redirect URIs associated with an application. /// @@ -347,16 +347,16 @@ public interface IOpenIddictApplicationManager ValueTask> GetRequirementsAsync(object application, CancellationToken cancellationToken = default); /// - /// Retrieves the self-signed client certificate chain policy enforced for this application. + /// Retrieves the self-signed client certificate authentication policy enforced for this application. /// /// The application. /// The base policy from which the returned instance will be derived. /// The that can be used to abort the operation. /// /// A that can be used to monitor the asynchronous operation, whose - /// result returns the self-signed client certificate chain policy enforced for this application. + /// result returns the self-signed client certificate authentication policy enforced for this application. /// - ValueTask GetSelfSignedClientCertificateChainPolicyAsync( + ValueTask GetSelfSignedTlsClientAuthenticationPolicyAsync( object application, X509ChainPolicy policy, CancellationToken cancellationToken = default); /// @@ -504,21 +504,6 @@ public interface IOpenIddictApplicationManager /// ValueTask UpdateAsync(object application, string secret, CancellationToken cancellationToken = default); - /// - /// Validates the client certificate associated with an application. - /// - /// The application. - /// The certificate that should be compared to the certificates associated with the application. - /// The chain policy used to validate the certificate. - /// The that can be used to abort the operation. - /// A that can be used to monitor the asynchronous operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns a boolean indicating whether the client certificate was valid. - /// - ValueTask ValidateClientCertificateAsync(object application, - X509Certificate2 certificate, X509ChainPolicy policy, CancellationToken cancellationToken = default); - /// /// Validates the application to ensure it's in a consistent state. /// @@ -554,6 +539,21 @@ public interface IOpenIddictApplicationManager ValueTask ValidatePostLogoutRedirectUriAsync(object application, [StringSyntax(StringSyntaxAttribute.Uri)] string uri, CancellationToken cancellationToken = default); + /// + /// Validates the PKI client certificate to ensure it can be used by the specified application. + /// + /// The application. + /// The certificate that should be compared to the certificates associated with the application. + /// The chain policy used to validate the certificate. + /// The that can be used to abort the operation. + /// A that can be used to monitor the asynchronous operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns a boolean indicating whether the client certificate was valid. + /// + ValueTask ValidatePublicKeyInfrastructureTlsClientCertificateAsync(object application, + X509Certificate2 certificate, X509ChainPolicy policy, CancellationToken cancellationToken = default); + /// /// Validates the redirect_uri to ensure it's associated with an application. /// @@ -568,7 +568,7 @@ public interface IOpenIddictApplicationManager [StringSyntax(StringSyntaxAttribute.Uri)] string uri, CancellationToken cancellationToken = default); /// - /// 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. /// /// The application. /// The certificate that should be compared to the certificates associated with the application. @@ -579,7 +579,7 @@ public interface IOpenIddictApplicationManager /// A that can be used to monitor the asynchronous operation, whose /// result returns a boolean indicating whether the self-signed client certificate was valid. /// - ValueTask ValidateSelfSignedClientCertificateAsync( + ValueTask ValidateSelfSignedTlsClientCertificateAsync( object application, X509Certificate2 certificate, X509ChainPolicy policy, CancellationToken cancellationToken = default); } diff --git a/src/OpenIddict.Abstractions/OpenIddictConstants.cs b/src/OpenIddict.Abstractions/OpenIddictConstants.cs index 9f6401c7..a0c9c076 100644 --- a/src/OpenIddict.Abstractions/OpenIddictConstants.cs +++ b/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"; diff --git a/src/OpenIddict.Abstractions/OpenIddictResources.resx b/src/OpenIddict.Abstractions/OpenIddictResources.resx index 7089451b..5541fcaf 100644 --- a/src/OpenIddict.Abstractions/OpenIddictResources.resx +++ b/src/OpenIddict.Abstractions/OpenIddictResources.resx @@ -546,7 +546,7 @@ Reference the 'OpenIddict.Validation.SystemNetHttp' package and call 'services.A The client identifier cannot be null or empty when using introspection. - 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. + 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. Authorization entry validation cannot be enabled when using introspection. @@ -1813,12 +1813,12 @@ Alternatively, any value respecting the '[region]-[subregion]-[identifier]' patt To set a static issuer, use 'services.AddOpenIddict().AddServer().SetIssuer()'. - End certificates are not allowed in the client certificate chain base policies attached to the server options. -To attach an end certificate to a specific client, override the 'OpenIddictApplicationManager.GetClientCertificateChainPolicyAsync()' method. + 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. 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. +To attach a self-signed certificate to a specific client, override the 'OpenIddictApplicationManager.GetSelfSignedTlsClientAuthenticationPolicyAsync()' method. Public Key Infrastructure-based client authentication cannot be used with self-signed certificates. @@ -1827,23 +1827,23 @@ To attach a self-signed certificate to a specific client, override the 'OpenIddi Self-signed client authentication can only be used with self-signed certificates. - A certificate chain policy must be configured when enabling the 'tls_client_auth' authentication method. -To configure a policy, use 'services.AddOpenIddict().AddServer().EnablePublicKeyInfrastructureClientCertificateAuthentication()'. + A 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()'. 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()'. +To configure a policy, use 'services.AddOpenIddict().AddServer().EnableSelfSignedTlsClientAuthentication()'. At least one certificate authority must be added to the certificate collection. X.509 custom trust stores are not supported on this platform. -While not recommended, certificate-based client authentication can be manually implemented on unsupported platforms by setting 'OpenIddictServerOptions.ClientCertificateChainPolicy'/'OpenIddictServerOptions.SelfSignedClientCertificateChainPolicy' and overriding the the 'OpenIddictApplicationManager.ValidateClientCertificateAsync()'/'OpenIddictApplicationManager.ValidateSelfSignedClientCertificateAsync()' methods. +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. - 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. + 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. mTLS endpoint aliases cannot be set when the corresponding endpoints have not been enabled. @@ -1851,6 +1851,15 @@ To use a custom policy relying on the system store, set 'OpenIddictServerOptions Public Key Infrastructure certificates cannot contain private keys. + + A certificate-based client authentication or token binding method was negotiated but no suitable certificate could be found. + + + The type of the specified certificate doesn't match the negotiated client authentication or token binding method. + + + TLS client certificates must contain a private key. + The security token is missing. @@ -2437,7 +2446,7 @@ To use a custom policy relying on the system store, set 'OpenIddictServerOptions The '{0}' parameter is only allowed for OAuth 2.0 Token Exchange requests. - Certificate-based authentication is not valid for this client application. + The specified token binding method is invalid or not supported. The specified TLS client certificate is invalid, expired or has been revoked. @@ -2445,6 +2454,24 @@ To use a custom policy relying on the system store, set 'OpenIddictServerOptions Client authentication is required for this application. + + The confirmation claim resolved from the security principal is malformed or invalid. + + + The thumbprint of the client certificate couldn't be resolved from the confirmation claim. + + + An existing '{0}' instance is already attached to the execution context. + + + The '{0}' attached to the execution context could not be resolved. + + + A certificate-based proof-of-possession is required to use this token. + + + The specified certificate-based proof-of-possession is not valid. + The '{0}' parameter shouldn't be null or empty at this point. @@ -3256,7 +3283,7 @@ This may indicate that the hashed entry is corrupted or malformed. The token request was rejected because the '{Parameter}' contained a URI fragment: {RedirectUri}. - The authentication demand was rejected because the public application '{ClientId}' was not allowed to send a client certificate. + The token was rejected because the proof of possession was missing. The authentication demand was rejected because the confidential application '{ClientId}' didn't specify a valid client certificate. @@ -3265,17 +3292,26 @@ This may indicate that the hashed entry is corrupted or malformed. The authentication demand was rejected because the confidential application '{ClientId}' didn't specify a valid self-signed client certificate. - Certificate-based client authentication failed for {ClientId} because no redirection URI was associated with the application. + Certificate validation failed for {ClientId} because no redirection URI was associated with the application. - Certificate-based client authentication failed for {ClientId} because the certificate was not valid: {Errors}. + Certificate validation failed for {ClientId} because the provided certificate was not valid: {Errors}. - Certificate-based client authentication failed for {ClientId} because the certificate didn't match any of the hostnames extracted from the redirection URIs associated with the application. + 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. An error occurred while trying to validate a client certificate, which may indicate that the certificate is malformed or has an invalid chain. + + The token was rejected because the proof of possession was malformed or invalid. + + + The revocation request was successfully sent to {Uri}: {Request}. + + + The revocation response returned by {Uri} was successfully extracted: {Response}. + https://documentation.openiddict.com/errors/{0} diff --git a/src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlers.Authentication.cs b/src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlers.Authentication.cs index c3c1d529..ce59dc99 100644 --- a/src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlers.Authentication.cs +++ b/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; diff --git a/src/OpenIddict.Client.Owin/OpenIddictClientOwinHelpers.cs b/src/OpenIddict.Client.Owin/OpenIddictClientOwinHelpers.cs index 4bb75548..48cf46af 100644 --- a/src/OpenIddict.Client.Owin/OpenIddictClientOwinHelpers.cs +++ b/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; diff --git a/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationMarshal.cs b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationMarshal.cs index f265237c..6584c090 100644 --- a/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationMarshal.cs +++ b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationMarshal.cs @@ -19,14 +19,19 @@ public sealed class OpenIddictClientSystemIntegrationMarshal private readonly ConcurrentDictionary TaskCompletionSource)>> _operations = new(); + TaskCompletionSource TaskCompletionSource)>> _tracker = new(); /// /// Determines whether the authentication demand corresponding to the specified nonce is tracked. /// /// The nonce, used as a unique identifier. /// if the operation is tracked, otherwise. - internal bool IsTracked(string nonce) => _operations.ContainsKey(nonce); + internal bool IsTracked(string nonce) + { + ArgumentException.ThrowIfNullOrEmpty(nonce); + + return _tracker.ContainsKey(nonce); + } /// /// Tries to add the specified authentication demand to the list of tracked operations. @@ -34,10 +39,16 @@ public sealed class OpenIddictClientSystemIntegrationMarshal /// The nonce, used as a unique identifier. /// The request forgery protection associated with the specified authentication demand. /// if the operation could be added, otherwise. - 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)))); + } /// /// Tries to acquire a lock on the authentication demand corresponding to the specified nonce. @@ -47,8 +58,12 @@ public sealed class OpenIddictClientSystemIntegrationMarshal /// if the lock could be taken, otherwise. /// The operation was canceled by the user. internal async Task 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); + } /// /// Tries to resolve the authentication context associated with the specified nonce. @@ -58,7 +73,9 @@ public sealed class OpenIddictClientSystemIntegrationMarshal /// if the context could be resolved, otherwise. 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 /// The operation was canceled by the user. internal async Task 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 /// if the operation could be validated, otherwise. 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 /// The authentication context that will be returned to the caller. /// if the operation could be completed, otherwise. 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); + } /// /// Tries to remove the specified authentication operation from the list of tracked operations. /// /// The nonce, used as a unique identifier. /// if the operation could be removed, otherwise. - internal bool TryRemove(string nonce) => _operations.TryRemove(nonce, out _); + internal bool TryRemove(string nonce) + { + ArgumentException.ThrowIfNullOrEmpty(nonce); + + return _tracker.TryRemove(nonce, out _); + } } diff --git a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpBuilder.cs b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpBuilder.cs index 0def36f1..6d62ea3a 100644 --- a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpBuilder.cs +++ b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpBuilder.cs @@ -300,6 +300,7 @@ public sealed class OpenIddictClientSystemNetHttpBuilder /// client authentication key usages to be automatically selected by OpenIddict). /// /// The instance. + [Obsolete("This option is no longer supported and will be removed in a future version.")] public OpenIddictClientSystemNetHttpBuilder SetSelfSignedTlsClientAuthenticationCertificateSelector( Func selector) { @@ -321,6 +322,7 @@ public sealed class OpenIddictClientSystemNetHttpBuilder /// client authentication key usages to be automatically selected by OpenIddict). /// /// The instance. + [Obsolete("This option is no longer supported and will be removed in a future version.")] public OpenIddictClientSystemNetHttpBuilder SetTlsClientAuthenticationCertificateSelector( Func selector) { diff --git a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpConfiguration.cs b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpConfiguration.cs index c017dba1..ef9783bc 100644 --- a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpConfiguration.cs +++ b/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); } /// @@ -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(); - - // 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>().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 } /// + [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)); } diff --git a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpContext.cs b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpContext.cs new file mode 100644 index 00000000..104bbbba --- /dev/null +++ b/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; + +/// +/// Represents the context used by the System.Net.Http integration when creating a new HTTP client. +/// +[EditorBrowsable(EditorBrowsableState.Never)] +public sealed class OpenIddictClientSystemNetHttpContext +{ + private static readonly AsyncLocal _current = new(); + + /// + /// Gets or sets the X.509 client certificate that will be used to authenticate + /// this peer when communicating with the external endpoint, if applicable. + /// + public X509Certificate2? LocalCertificate { get; init; } + + /// + /// Gets or sets the ambient context for the current execution flow. + /// + public static OpenIddictClientSystemNetHttpContext? Current + { + get => _current.Value; + set => _current.Value = value; + } + + /// + /// Gets or sets the client registration associated with the HTTP client being created. + /// + public required OpenIddictClientRegistration Registration { get; init; } + + /// + /// Computes a stable, unique identifier for the specified context using a cryptographic hash. + /// + /// The client context for which to compute the stable identifier. + /// A string representing the stable identifier for the specified context. + 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); + } + } +} diff --git a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpExtensions.cs b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpExtensions.cs index 9cbf374e..c4c5f58f 100644 --- a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpExtensions.cs +++ b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpExtensions.cs @@ -46,9 +46,6 @@ public static class OpenIddictClientSystemNetHttpExtensions builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton< IPostConfigureOptions, OpenIddictClientSystemNetHttpConfiguration>()); - builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton< - IPostConfigureOptions, OpenIddictClientSystemNetHttpConfiguration>()); - return new OpenIddictClientSystemNetHttpBuilder(builder.Services); } diff --git a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.cs b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.cs index a72696e8..31e811fe 100644 --- a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.cs +++ b/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 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. /// + [Obsolete("This class is obsolete and will be removed in a future version.")] public sealed class AttachNonDefaultTokenEndpointClientAuthenticationMethod : IOpenIddictClientHandler { - private readonly IOptionsMonitor _options; - - public AttachNonDefaultTokenEndpointClientAuthenticationMethod( - IOptionsMonitor options) - => _options = options ?? throw new ArgumentNullException(nameof(options)); - /// /// Gets the default descriptor definition assigned to this handler. /// @@ -79,113 +51,16 @@ public static partial class OpenIddictClientSystemNetHttpHandlers .Build(); /// - 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, - _ => 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; } /// /// Contains the logic responsible for negotiating the best token binding /// methods supported by both the client and the authorization server. /// + [Obsolete("This class is obsolete and will be removed in a future version.")] public sealed class AttachNonDefaultUserInfoEndpointTokenBindingMethods : IOpenIddictClientHandler { - private readonly IOptionsMonitor _options; - - public AttachNonDefaultUserInfoEndpointTokenBindingMethods( - IOptionsMonitor options) - => _options = options ?? throw new ArgumentNullException(nameof(options)); - /// /// Gets the default descriptor definition assigned to this handler. /// @@ -198,56 +73,16 @@ public static partial class OpenIddictClientSystemNetHttpHandlers .Build(); /// - 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; } /// /// Contains the logic responsible for negotiating the best device authorization endpoint /// client authentication method supported by both the client and the authorization server. /// + [Obsolete("This class is obsolete and will be removed in a future version.")] public sealed class AttachNonDefaultDeviceAuthorizationEndpointClientAuthenticationMethod : IOpenIddictClientHandler { - private readonly IOptionsMonitor _options; - - public AttachNonDefaultDeviceAuthorizationEndpointClientAuthenticationMethod( - IOptionsMonitor options) - => _options = options ?? throw new ArgumentNullException(nameof(options)); - /// /// Gets the default descriptor definition assigned to this handler. /// @@ -260,121 +95,16 @@ public static partial class OpenIddictClientSystemNetHttpHandlers .Build(); /// - 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, - _ => 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; } /// /// Contains the logic responsible for negotiating the best pushed authorization endpoint /// client authentication method supported by both the client and the authorization server. /// + [Obsolete("This class is obsolete and will be removed in a future version.")] public sealed class AttachNonDefaultPushedAuthorizationEndpointClientAuthenticationMethod : IOpenIddictClientHandler { - private readonly IOptionsMonitor _options; - - public AttachNonDefaultPushedAuthorizationEndpointClientAuthenticationMethod( - IOptionsMonitor options) - => _options = options ?? throw new ArgumentNullException(nameof(options)); - /// /// Gets the default descriptor definition assigned to this handler. /// @@ -387,122 +117,16 @@ public static partial class OpenIddictClientSystemNetHttpHandlers .Build(); /// - 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, - _ => 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; } /// /// Contains the logic responsible for negotiating the best introspection endpoint client /// authentication method supported by both the client and the authorization server. /// + [Obsolete("This class is obsolete and will be removed in a future version.")] public sealed class AttachNonDefaultIntrospectionEndpointClientAuthenticationMethod : IOpenIddictClientHandler { - private readonly IOptionsMonitor _options; - - public AttachNonDefaultIntrospectionEndpointClientAuthenticationMethod( - IOptionsMonitor options) - => _options = options ?? throw new ArgumentNullException(nameof(options)); - /// /// Gets the default descriptor definition assigned to this handler. /// @@ -515,113 +139,16 @@ public static partial class OpenIddictClientSystemNetHttpHandlers .Build(); /// - 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, - _ => 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; } /// /// Contains the logic responsible for negotiating the best revocation endpoint client /// authentication method supported by both the client and the authorization server. /// + [Obsolete("This class is obsolete and will be removed in a future version.")] public sealed class AttachNonDefaultRevocationEndpointClientAuthenticationMethod : IOpenIddictClientHandler { - private readonly IOptionsMonitor _options; - - public AttachNonDefaultRevocationEndpointClientAuthenticationMethod( - IOptionsMonitor options) - => _options = options ?? throw new ArgumentNullException(nameof(options)); - /// /// Gets the default descriptor definition assigned to this handler. /// @@ -634,99 +161,7 @@ public static partial class OpenIddictClientSystemNetHttpHandlers .Build(); /// - 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, - _ => 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; } /// @@ -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; } diff --git a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpOptions.cs b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpOptions.cs index dce47330..4e86ef4f 100644 --- a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpOptions.cs +++ b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpOptions.cs @@ -95,6 +95,7 @@ public sealed class OpenIddictClientSystemNetHttpOptions /// client authentication key usages to be automatically selected by OpenIddict). /// [EditorBrowsable(EditorBrowsableState.Advanced)] + [Obsolete("This option is no longer supported and will be removed in a future version.")] public Func SelfSignedTlsClientAuthenticationCertificateSelector { get; set; } = default!; /// @@ -109,5 +110,6 @@ public sealed class OpenIddictClientSystemNetHttpOptions /// client authentication key usages to be automatically selected by OpenIddict). /// [EditorBrowsable(EditorBrowsableState.Advanced)] + [Obsolete("This option is no longer supported and will be removed in a future version.")] public Func TlsClientAuthenticationCertificateSelector { get; set; } = default!; } diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationConfiguration.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationConfiguration.cs index 3e0e462b..377c70ed 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationConfiguration.cs +++ b/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); } - }); + } } /// + [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 CreateSelector(Func 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)); /// /// Amends the registration with the provider-specific configuration logic. diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationExtensions.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationExtensions.cs index a79f6f2e..250baf0a 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationExtensions.cs +++ b/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, OpenIddictClientWebIntegrationConfiguration>()); - builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton< - IPostConfigureOptions, OpenIddictClientWebIntegrationConfiguration>()); // Note: the IPostConfigureOptions service responsible for populating // the client registrations MUST be registered before OpenIddictClientConfiguration to ensure diff --git a/src/OpenIddict.Client/OpenIddictClientConfiguration.cs b/src/OpenIddict.Client/OpenIddictClientConfiguration.cs index cb615005..5681974e 100644 --- a/src/OpenIddict.Client/OpenIddictClientConfiguration.cs +++ b/src/OpenIddict.Client/OpenIddictClientConfiguration.cs @@ -82,6 +82,17 @@ public sealed class OpenIddictClientConfiguration : IPostConfigureOptions 0 + ? ClientTypes.Confidential + : ClientTypes.Public; + } + if (registration.ConfigurationManager is null) { if (registration.Configuration is not null) diff --git a/src/OpenIddict.Client/OpenIddictClientEvents.cs b/src/OpenIddict.Client/OpenIddictClientEvents.cs index f211c4e2..1e3a0180 100644 --- a/src/OpenIddict.Client/OpenIddictClientEvents.cs +++ b/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 /// public string? ClientAuthenticationMethod { get; set; } + /// + /// Gets or sets the X.509 client certificate that will be used to authenticate + /// this peer when communicating with the external endpoint, if applicable. + /// + public X509Certificate2? LocalCertificate { get; set; } + /// /// Gets or sets the token binding method used when communicating with the external endpoint, if applicable. /// + [Obsolete("This property is no longer used and will be removed in a future version.")] public HashSet TokenBindingMethods { get; } = new(StringComparer.Ordinal); } @@ -397,15 +405,40 @@ public static partial class OpenIddictClientEvents /// public string? TokenEndpointClientAuthenticationMethod { get; set; } + /// + /// Gets or sets the X.509 client certificate used when + /// communicating with the token endpoint, if applicable. + /// + public X509Certificate2? TokenEndpointClientCertificate { get; set; } + + /// + /// Gets or sets the token binding method used when + /// communicating with the token endpoint, if applicable. + /// + public string? TokenEndpointTokenBindingMethod { get; set; } + /// /// Gets or sets the URI of the userinfo endpoint, if applicable. /// public Uri? UserInfoEndpoint { get; set; } + /// + /// Gets or sets the token binding method used when + /// communicating with the userinfo endpoint, if applicable. + /// + public string? UserInfoEndpointTokenBindingMethod { get; set; } + + /// + /// Gets or sets the X.509 client certificate used when + /// communicating with the userinfo endpoint, if applicable. + /// + public X509Certificate2? UserInfoEndpointClientCertificate { get; set; } + /// /// Gets or sets the token binding methods used when /// communicating with the userinfo endpoint, if applicable. /// + [Obsolete("This property is no longer used and will be removed in a future version.")] public HashSet UserInfoEndpointTokenBindingMethods { get; } = new(StringComparer.Ordinal); /// @@ -1153,6 +1186,12 @@ public static partial class OpenIddictClientEvents /// public string? DeviceAuthorizationEndpointClientAuthenticationMethod { get; set; } + /// + /// Gets or sets the X.509 client certificate used when communicating + /// with the device authorization endpoint, if applicable. + /// + public X509Certificate2? DeviceAuthorizationEndpointClientCertificate { get; set; } + /// /// Gets or sets the URI of the pushed authorization endpoint, if applicable. /// @@ -1164,6 +1203,12 @@ public static partial class OpenIddictClientEvents /// public string? PushedAuthorizationEndpointClientAuthenticationMethod { get; set; } + /// + /// Gets or sets the X.509 client certificate used when communicating + /// with the pushed authorization endpoint, if applicable. + /// + public X509Certificate2? PushedAuthorizationEndpointClientCertificate { get; set; } + /// /// 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 /// public string? IntrospectionEndpointClientAuthenticationMethod { get; set; } + /// + /// Gets or sets the X.509 client certificate used when + /// communicating with the introspection endpoint, if applicable. + /// + public X509Certificate2? IntrospectionEndpointClientCertificate { get; set; } + /// /// Gets or sets the client identifier that will be used for the introspection demand. /// @@ -1594,6 +1645,12 @@ public static partial class OpenIddictClientEvents /// public string? RevocationEndpointClientAuthenticationMethod { get; set; } + /// + /// Gets or sets the X.509 client certificate used when + /// communicating with the revocation endpoint, if applicable. + /// + public X509Certificate2? RevocationEndpointClientCertificate { get; set; } + /// /// Gets or sets the client identifier that will be used for the revocation demand. /// diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.cs index 7919982b..be10f7ab 100644 --- a/src/OpenIddict.Client/OpenIddictClientHandlers.cs +++ b/src/OpenIddict.Client/OpenIddictClientHandlers.cs @@ -9,6 +9,7 @@ using System.ComponentModel; using System.Diagnostics; using System.Runtime.InteropServices; using System.Security.Claims; +using System.Security.Cryptography.X509Certificates; using System.Text; using System.Text.Json; using Microsoft.Extensions.Logging; @@ -65,6 +66,8 @@ public static partial class OpenIddictClientHandlers EvaluateTokenRequest.Descriptor, AttachTokenEndpointClientAuthenticationMethod.Descriptor, + AttachTokenEndpointTokenBindingMethod.Descriptor, + AttachTokenEndpointClientCertificate.Descriptor, ResolveTokenEndpoint.Descriptor, AttachTokenRequestParameters.Descriptor, EvaluateGeneratedClientAssertion.Descriptor, @@ -89,7 +92,8 @@ public static partial class OpenIddictClientHandlers ValidateRefreshToken.Descriptor, EvaluateUserInfoRequest.Descriptor, - AttachUserInfoEndpointTokenBindingMethods.Descriptor, + AttachUserInfoEndpointTokenBindingMethod.Descriptor, + AttachUserInfoEndpointClientCertificate.Descriptor, ResolveUserInfoEndpoint.Descriptor, AttachUserInfoRequestParameters.Descriptor, SendUserInfoRequest.Descriptor, @@ -124,11 +128,13 @@ public static partial class OpenIddictClientHandlers EvaluateDeviceAuthorizationRequest.Descriptor, AttachDeviceAuthorizationEndpointClientAuthenticationMethod.Descriptor, + AttachDeviceAuthorizationEndpointClientCertificate.Descriptor, ResolveDeviceAuthorizationEndpoint.Descriptor, AttachDeviceAuthorizationRequestParameters.Descriptor, EvaluatePushedAuthorizationRequest.Descriptor, AttachPushedAuthorizationEndpointClientAuthenticationMethod.Descriptor, + AttachPushedAuthorizationEndpointClientCertificate.Descriptor, ResolvePushedAuthorizationEndpoint.Descriptor, AttachPushedAuthorizationRequestParameters.Descriptor, @@ -162,6 +168,7 @@ public static partial class OpenIddictClientHandlers AttachClientIdToIntrospectionContext.Descriptor, EvaluateIntrospectionRequest.Descriptor, AttachIntrospectionEndpointClientAuthenticationMethod.Descriptor, + AttachIntrospectionEndpointClientCertificate.Descriptor, ResolveIntrospectionEndpoint.Descriptor, AttachIntrospectionRequestParameters.Descriptor, EvaluateGeneratedIntrospectionClientAssertion.Descriptor, @@ -179,6 +186,7 @@ public static partial class OpenIddictClientHandlers AttachClientIdToRevocationContext.Descriptor, EvaluateRevocationRequest.Descriptor, AttachRevocationEndpointClientAuthenticationMethod.Descriptor, + AttachRevocationEndpointClientCertificate.Descriptor, ResolveRevocationEndpoint.Descriptor, AttachRevocationRequestParameters.Descriptor, EvaluateGeneratedRevocationClientAssertion.Descriptor, @@ -2276,6 +2284,14 @@ public static partial class OpenIddictClientHandlers return ValueTask.CompletedTask; } + // If the client is a public application, do not negotiate a client authentication method. + if (context.Registration.ClientType is ClientTypes.Public) + { + context.TokenEndpointClientAuthenticationMethod = ClientAuthenticationMethods.None; + + 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. @@ -2288,11 +2304,60 @@ public static partial class OpenIddictClientHandlers Server: context.Configuration.TokenEndpointAuthMethodsSupported) switch { - // If at least one signing key was attached to the client registration and both - // the client and the server explicitly support private_key_jwt, always prefer it. - ({ Count: > 0 } client, { Count: > 0 } server) when context.Registration.SigningCredentials.Count is not 0 && + // If a Public Key Infrastructure TLS client authentication certificate can 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) && + context.TokenEndpointClientCertificate 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.MtlsTokenEndpoint ?? context.Configuration.TokenEndpoint) is Uri endpoint && + string.Equals(endpoint.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase) && + context.TokenEndpointClientCertificate is null && + context.Registration.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 can 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) && + context.TokenEndpointClientCertificate 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.MtlsTokenEndpoint ?? context.Configuration.TokenEndpoint) is Uri endpoint && + string.Equals(endpoint.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase) && + context.TokenEndpointClientCertificate is null && + context.Registration.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 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) + 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 @@ -2302,6 +2367,188 @@ public static partial class OpenIddictClientHandlers 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; + } + } + + /// + /// Contains the logic responsible for negotiating the best token endpoint + /// binding method supported by both the client and the authorization server. + /// + public sealed class AttachTokenEndpointTokenBindingMethod : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(AttachTokenEndpointClientAuthenticationMethod.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessAuthenticationContext context) + { + ArgumentNullException.ThrowIfNull(context); + + // If an explicit token binding method was attached, don't overwrite it. + if (!string.IsNullOrEmpty(context.TokenEndpointTokenBindingMethod)) + { + return ValueTask.CompletedTask; + } + + // Note: if token binding methods are explicitly listed in the client registration, only use + // the token binding methods that are both listed and enabled in the global client options. + // Otherwise, always default to the token binding methods that have been enabled globally. + context.TokenEndpointTokenBindingMethod = context.Registration.TokenBindingMethods.Count switch + { + 0 => context.Options.TokenBindingMethods as ICollection, + _ => context.Options.TokenBindingMethods.Intersect(context.Registration.TokenBindingMethods, StringComparer.Ordinal).ToList() + } + switch + { + // If a Public Key Infrastructure TLS client authentication certificate can be resolved and both + // the client and the server explicitly support certificate-bound access tokens, always prefer it. + { Count: > 0 } client when + client.Contains(TokenBindingMethods.Private.TlsClientCertificate) && + context.Configuration.TlsClientCertificateBoundAccessTokens is true && + (context.Configuration.MtlsTokenEndpoint ?? context.Configuration.TokenEndpoint) is Uri endpoint && + string.Equals(endpoint.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase) && + context.TokenEndpointClientCertificate is X509Certificate2 certificate && + OpenIddictHelpers.IsClientAuthenticationCertificate(certificate) && + !OpenIddictHelpers.IsSelfIssuedCertificate(certificate) + => TokenBindingMethods.Private.TlsClientCertificate, + + { Count: > 0 } client when + client.Contains(TokenBindingMethods.Private.TlsClientCertificate) && + context.Configuration.TlsClientCertificateBoundAccessTokens is true && + (context.Configuration.MtlsTokenEndpoint ?? context.Configuration.TokenEndpoint) is Uri endpoint && + string.Equals(endpoint.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase) && + context.TokenEndpointClientCertificate is null && + context.Registration.SigningCredentials.Exists(static credentials => + credentials.Key is X509SecurityKey { Certificate: X509Certificate2 certificate } && + OpenIddictHelpers.IsClientAuthenticationCertificate(certificate) && + !OpenIddictHelpers.IsSelfIssuedCertificate(certificate)) + => TokenBindingMethods.Private.TlsClientCertificate, + + // If a self-signed TLS client authentication certificate can be resolved and both the client and + // the server explicitly support certificate-bound access tokens or the client is a public client, + // assume the server supports certificate-bound refresh tokens for public clients and use it. + // + // Note: the same logic deliberately doesn't apply to Public Key Infrastructure TLS client + // certificates, as public clients exclusively use self-signed certificates for token binding. + { Count: > 0 } client when + client.Contains(TokenBindingMethods.Private.TlsClientCertificate) && + (context.Configuration.TlsClientCertificateBoundAccessTokens is true || + context.Registration.ClientType is ClientTypes.Public) && + (context.Configuration.MtlsTokenEndpoint ?? context.Configuration.TokenEndpoint) is Uri endpoint && + string.Equals(endpoint.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase) && + context.TokenEndpointClientCertificate is X509Certificate2 certificate && + OpenIddictHelpers.IsClientAuthenticationCertificate(certificate) && + OpenIddictHelpers.IsSelfIssuedCertificate(certificate) + => TokenBindingMethods.Private.SelfSignedTlsClientCertificate, + + { Count: > 0 } client when + client.Contains(TokenBindingMethods.Private.TlsClientCertificate) && + (context.Configuration.TlsClientCertificateBoundAccessTokens is true || + context.Registration.ClientType is ClientTypes.Public) && + (context.Configuration.MtlsTokenEndpoint ?? context.Configuration.TokenEndpoint) is Uri endpoint && + string.Equals(endpoint.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase) && + context.TokenEndpointClientCertificate is null && + context.Registration.SigningCredentials.Exists(static credentials => + credentials.Key is X509SecurityKey { Certificate: X509Certificate2 certificate } && + OpenIddictHelpers.IsClientAuthenticationCertificate(certificate) && + OpenIddictHelpers.IsSelfIssuedCertificate(certificate)) + => TokenBindingMethods.Private.SelfSignedTlsClientCertificate, + + _ => null + }; + + return ValueTask.CompletedTask; + } + } + + /// + /// Contains the logic responsible for attaching the client certificate used + /// for the token endpoint to the authentication context, if applicable. + /// + public sealed class AttachTokenEndpointClientCertificate : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(AttachTokenEndpointTokenBindingMethod.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessAuthenticationContext context) + { + ArgumentNullException.ThrowIfNull(context); + + // If a certificate-based client authentication method or token binding 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.TokenEndpointClientCertificate ??= ( + context.TokenEndpointClientAuthenticationMethod, + context.TokenEndpointTokenBindingMethod) switch + { + (ClientAuthenticationMethods.TlsClientAuth, _) or (_, TokenBindingMethods.Private.TlsClientCertificate) + => context.Registration.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, _) or (_, TokenBindingMethods.Private.SelfSignedTlsClientCertificate) + => context.Registration.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 }; @@ -2321,7 +2568,7 @@ public static partial class OpenIddictClientHandlers = OpenIddictClientHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler() - .SetOrder(AttachTokenEndpointClientAuthenticationMethod.Descriptor.Order + 1_000) + .SetOrder(AttachTokenEndpointClientCertificate.Descriptor.Order + 1_000) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); @@ -2330,13 +2577,16 @@ public static partial class OpenIddictClientHandlers { ArgumentNullException.ThrowIfNull(context); - // If the URI of the token endpoint wasn't explicitly set at - // this stage, try to extract it from the server configuration. - context.TokenEndpoint ??= context.TokenEndpointClientAuthenticationMethod switch + context.TokenEndpoint ??= ( + context.TokenEndpointClientAuthenticationMethod, + context.TokenEndpointTokenBindingMethod) switch { - // When TLS client certificate authentication was negotiated, - // always favor the mTLS-specific endpoint if available. - ClientAuthenticationMethods.SelfSignedTlsClientAuth or ClientAuthenticationMethods.TlsClientAuth + // If a TLS client authentication certificate is going to be used, always favor the mTLS alias if available. + (ClientAuthenticationMethods.TlsClientAuth or ClientAuthenticationMethods.SelfSignedTlsClientAuth, _) + when context.Configuration.MtlsTokenEndpoint is { IsAbsoluteUri: true } uri && + !OpenIddictHelpers.IsImplicitFileUri(uri) => uri, + + (_, TokenBindingMethods.Private.TlsClientCertificate or TokenBindingMethods.Private.SelfSignedTlsClientCertificate) when context.Configuration.MtlsTokenEndpoint is { IsAbsoluteUri: true } uri && !OpenIddictHelpers.IsImplicitFileUri(uri) => uri, @@ -2550,7 +2800,7 @@ public static partial class OpenIddictClientHandlers // including token endpoint URIs or issuer identifiers used by other authorization servers, which could // result in impersonation attacks if the same set of credentials were used to generate the assertions // for all the client registrations (which is not a recommended pattern in OpenIddict). To mitigate that, - // OpenIddict no longer allows uses the token endpoint URI and always uses the issuer identity instead. + // OpenIddict no longer allows using the token endpoint URI and always uses the issuer identity instead. // Unlike the token endpoint URI, the issuer returned by the authorization server in its configuration // document is always validated and must exactly match the value expected by the client application. // @@ -2724,12 +2974,39 @@ public static partial class OpenIddictClientHandlers throw new InvalidOperationException(SR.FormatID0301(Metadata.TokenEndpoint)); } + var certificate = ( + context.TokenEndpointClientAuthenticationMethod, + context.TokenEndpointTokenBindingMethod) switch + { + (ClientAuthenticationMethods.TlsClientAuth, _) when context.TokenEndpointClientCertificate is not null => + OpenIddictHelpers.IsSelfIssuedCertificate(context.TokenEndpointClientCertificate) + ? throw new InvalidOperationException(SR.GetResourceString(SR.ID0513)) + : context.TokenEndpointClientCertificate, + + (ClientAuthenticationMethods.SelfSignedTlsClientAuth, _) when context.TokenEndpointClientCertificate is not null => + OpenIddictHelpers.IsSelfIssuedCertificate(context.TokenEndpointClientCertificate) + ? context.TokenEndpointClientCertificate + : throw new InvalidOperationException(SR.GetResourceString(SR.ID0513)), + + (_, TokenBindingMethods.Private.TlsClientCertificate) when context.TokenEndpointClientCertificate is not null => + OpenIddictHelpers.IsSelfIssuedCertificate(context.TokenEndpointClientCertificate) + ? throw new InvalidOperationException(SR.GetResourceString(SR.ID0513)) + : context.TokenEndpointClientCertificate, + + (_, TokenBindingMethods.Private.SelfSignedTlsClientCertificate) when context.TokenEndpointClientCertificate is not null => + OpenIddictHelpers.IsSelfIssuedCertificate(context.TokenEndpointClientCertificate) + ? context.TokenEndpointClientCertificate + : throw new InvalidOperationException(SR.GetResourceString(SR.ID0513)), + + _ => null + }; + try { context.TokenResponse = await _service.SendTokenRequestAsync( - context.Registration, context.Configuration, - context.TokenRequest, context.TokenEndpoint, - context.TokenEndpointClientAuthenticationMethod, context.CancellationToken); + context.Registration, context.Configuration, context.TokenRequest, + context.TokenEndpoint, context.TokenEndpointClientAuthenticationMethod, + certificate, context.CancellationToken); } catch (ProtocolException exception) @@ -3776,7 +4053,7 @@ public static partial class OpenIddictClientHandlers /// Contains the logic responsible for negotiating the best userinfo endpoint client /// authentication method supported by both the client and the authorization server. /// - public sealed class AttachUserInfoEndpointTokenBindingMethods : IOpenIddictClientHandler + public sealed class AttachUserInfoEndpointTokenBindingMethod : IOpenIddictClientHandler { /// /// Gets the default descriptor definition assigned to this handler. @@ -3784,7 +4061,7 @@ public static partial class OpenIddictClientHandlers public static OpenIddictClientHandlerDescriptor Descriptor { get; } = OpenIddictClientHandlerDescriptor.CreateBuilder() .AddFilter() - .UseSingletonHandler() + .UseSingletonHandler() .SetOrder(EvaluateUserInfoRequest.Descriptor.Order + 1_000) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); @@ -3794,15 +4071,98 @@ public static partial class OpenIddictClientHandlers { ArgumentNullException.ThrowIfNull(context); - // By default, assume that sending the access token is enough and doesn't require using - // an additional token binding method. To support advanced scenarios like mTLS-protected - // HTTPS userinfo endpoints, a specialized event handler is used by the System.Net.Http - // integration package to send a client certificate (self-signed or not) if necessary. + // If an explicit token binding method was attached, don't overwrite it. + if (!string.IsNullOrEmpty(context.UserInfoEndpointTokenBindingMethod)) + { + return ValueTask.CompletedTask; + } + + context.UserInfoEndpointTokenBindingMethod = context.UserInfoEndpointClientCertificate switch + { + // If a client certificate was explicitly attached, infer the token binding method from the certificate type. + X509Certificate2 certificate => OpenIddictHelpers.IsSelfIssuedCertificate(certificate) + ? TokenBindingMethods.Private.SelfSignedTlsClientCertificate + : TokenBindingMethods.Private.TlsClientCertificate, + + // Otherwise, assume the token binding method used for the + // token endpoint is also used for the userinfo endpoint. + _ => context.TokenEndpointTokenBindingMethod + }; + + return ValueTask.CompletedTask; + } + } + + /// + /// Contains the logic responsible for attaching the client certificate used + /// for the userinfo endpoint to the authentication context, if applicable. + /// + public sealed class AttachUserInfoEndpointClientCertificate : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(AttachUserInfoEndpointTokenBindingMethod.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessAuthenticationContext context) + { + ArgumentNullException.ThrowIfNull(context); + + // If a certificate-based token binding method was negotiated and no + // certificate was explicitly attached by the application, use the + // same X.509 certificate as the one used during the token request. + context.UserInfoEndpointClientCertificate ??= context.UserInfoEndpointTokenBindingMethod switch + { + TokenBindingMethods.Private.TlsClientCertificate => context.TokenEndpointClientCertificate switch + { + X509Certificate2 certificate when !OpenIddictHelpers.IsSelfIssuedCertificate(certificate) => certificate, + + _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0512)) + }, + + TokenBindingMethods.Private.SelfSignedTlsClientCertificate => context.TokenEndpointClientCertificate switch + { + X509Certificate2 certificate when OpenIddictHelpers.IsSelfIssuedCertificate(certificate) => certificate, + + _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0512)) + }, + + _ => null + }; return ValueTask.CompletedTask; } } + /// + /// Contains the logic responsible for negotiating the best userinfo endpoint client + /// authentication method supported by both the client and the authorization server. + /// + [Obsolete("This class is obsolete and will be removed in a future version.")] + public sealed class AttachUserInfoEndpointTokenBindingMethods : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(EvaluateUserInfoRequest.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessAuthenticationContext context) => ValueTask.CompletedTask; + } + /// /// Contains the logic responsible for resolving the URI of the userinfo endpoint. /// @@ -3815,7 +4175,7 @@ public static partial class OpenIddictClientHandlers = OpenIddictClientHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler() - .SetOrder(AttachUserInfoEndpointTokenBindingMethods.Descriptor.Order + 1_000) + .SetOrder(AttachUserInfoEndpointClientCertificate.Descriptor.Order + 1_000) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); @@ -3824,16 +4184,11 @@ public static partial class OpenIddictClientHandlers { ArgumentNullException.ThrowIfNull(context); - // If the URI of the userinfo endpoint wasn't explicitly set at - // this stage, try to extract it from the server configuration. - context.UserInfoEndpoint ??= context.UserInfoEndpointTokenBindingMethods switch + context.UserInfoEndpoint ??= context.UserInfoEndpointTokenBindingMethod switch { - // When TLS client certificate authentication was negotiated, - // always favor the mTLS-specific endpoint if available. - { } methods when ( - methods.Contains(ClientAuthenticationMethods.SelfSignedTlsClientAuth) || - methods.Contains(ClientAuthenticationMethods.TlsClientAuth)) && - context.Configuration.MtlsUserInfoEndpoint is { IsAbsoluteUri: true } uri && + // If a TLS client authentication certificate is going to be used, always favor the mTLS alias if available. + TokenBindingMethods.Private.TlsClientCertificate or TokenBindingMethods.Private.SelfSignedTlsClientCertificate + when context.Configuration.MtlsUserInfoEndpoint is { IsAbsoluteUri: true } uri && !OpenIddictHelpers.IsImplicitFileUri(uri) => uri, // Otherwise, use the non-mTLS-specific endpoint. @@ -3930,13 +4285,28 @@ public static partial class OpenIddictClientHandlers // - application/json responses containing a JSON object listing the user claims as-is. // - application/jwt responses containing a signed/encrypted JSON Web Token containing the user claims. + var certificate = context.UserInfoEndpointTokenBindingMethod switch + { + TokenBindingMethods.Private.TlsClientCertificate when context.UserInfoEndpointClientCertificate is not null => + OpenIddictHelpers.IsSelfIssuedCertificate(context.UserInfoEndpointClientCertificate) + ? throw new InvalidOperationException(SR.GetResourceString(SR.ID0513)) + : context.UserInfoEndpointClientCertificate, + + TokenBindingMethods.Private.SelfSignedTlsClientCertificate when context.UserInfoEndpointClientCertificate is not null => + OpenIddictHelpers.IsSelfIssuedCertificate(context.UserInfoEndpointClientCertificate) + ? context.UserInfoEndpointClientCertificate + : throw new InvalidOperationException(SR.GetResourceString(SR.ID0513)), + + _ => null + }; + try { (context.UserInfoResponse, (context.UserInfoTokenPrincipal, context.UserInfoToken)) = await _service.SendUserInfoRequestAsync( context.Registration, context.Configuration, context.UserInfoRequest, context.UserInfoEndpoint, - context.UserInfoEndpointTokenBindingMethods, context.CancellationToken); + certificate, context.CancellationToken); } catch (ProtocolException exception) @@ -5599,6 +5969,14 @@ public static partial class OpenIddictClientHandlers return ValueTask.CompletedTask; } + // If the client is a public application, do not negotiate a client authentication method. + if (context.Registration.ClientType is ClientTypes.Public) + { + context.DeviceAuthorizationEndpointClientAuthenticationMethod = ClientAuthenticationMethods.None; + + 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. @@ -5619,11 +5997,60 @@ public static partial class OpenIddictClientHandlers _ => context.Configuration.DeviceAuthorizationEndpointAuthMethodsSupported, }) switch { - // If at least one signing key was attached to the client registration and both - // the client and the server explicitly support private_key_jwt, always prefer it. - ({ Count: > 0 } client, { Count: > 0 } server) when context.Registration.SigningCredentials.Count is not 0 && + // If a Public Key Infrastructure TLS client authentication certificate can 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) && + context.DeviceAuthorizationEndpointClientCertificate 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.MtlsDeviceAuthorizationEndpoint ?? context.Configuration.DeviceAuthorizationEndpoint) is Uri endpoint && + string.Equals(endpoint.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase) && + context.DeviceAuthorizationEndpointClientCertificate is null && + context.Registration.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 can 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) && + context.DeviceAuthorizationEndpointClientCertificate 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.MtlsDeviceAuthorizationEndpoint ?? context.Configuration.DeviceAuthorizationEndpoint) is Uri endpoint && + string.Equals(endpoint.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase) && + context.DeviceAuthorizationEndpointClientCertificate is null && + context.Registration.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 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) + 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 @@ -5633,6 +6060,85 @@ public static partial class OpenIddictClientHandlers 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; + } + } + + /// + /// Contains the logic responsible for attaching the client certificate used for + /// the device authorization endpoint to the authentication context, if applicable. + /// + public sealed class AttachDeviceAuthorizationEndpointClientCertificate : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(AttachDeviceAuthorizationEndpointClientAuthenticationMethod.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessChallengeContext 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.DeviceAuthorizationEndpointClientCertificate ??= context.DeviceAuthorizationEndpointClientAuthenticationMethod switch + { + ClientAuthenticationMethods.TlsClientAuth => context.Registration.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.Registration.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 }; @@ -5652,7 +6158,7 @@ public static partial class OpenIddictClientHandlers = OpenIddictClientHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler() - .SetOrder(AttachDeviceAuthorizationEndpointClientAuthenticationMethod.Descriptor.Order + 1_000) + .SetOrder(AttachDeviceAuthorizationEndpointClientCertificate.Descriptor.Order + 1_000) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); @@ -5661,13 +6167,10 @@ public static partial class OpenIddictClientHandlers { ArgumentNullException.ThrowIfNull(context); - // If the URI of the device authorization endpoint endpoint wasn't explicitly - // set at this stage, try to extract it from the server configuration. context.DeviceAuthorizationEndpoint ??= context.DeviceAuthorizationEndpointClientAuthenticationMethod switch { - // When TLS client certificate authentication was negotiated, - // always favor the mTLS-specific endpoint if available. - ClientAuthenticationMethods.SelfSignedTlsClientAuth or ClientAuthenticationMethods.TlsClientAuth + // If a TLS client authentication certificate is going to be used, always favor the mTLS alias if available. + ClientAuthenticationMethods.TlsClientAuth or ClientAuthenticationMethods.SelfSignedTlsClientAuth when context.Configuration.MtlsDeviceAuthorizationEndpoint is { IsAbsoluteUri: true } uri && !OpenIddictHelpers.IsImplicitFileUri(uri) => uri, @@ -5787,6 +6290,14 @@ public static partial class OpenIddictClientHandlers return ValueTask.CompletedTask; } + // If the client is a public application, do not negotiate a client authentication method. + if (context.Registration.ClientType is ClientTypes.Public) + { + context.PushedAuthorizationEndpointClientAuthenticationMethod = ClientAuthenticationMethods.None; + + 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. @@ -5808,11 +6319,60 @@ public static partial class OpenIddictClientHandlers _ => context.Configuration.PushedAuthorizationEndpointAuthMethodsSupported, }) switch { - // If at least one signing key was attached to the client registration and both - // the client and the server explicitly support private_key_jwt, always prefer it. - ({ Count: > 0 } client, { Count: > 0 } server) when context.Registration.SigningCredentials.Count is not 0 && + // If a Public Key Infrastructure TLS client authentication certificate can 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) && + context.PushedAuthorizationEndpointClientCertificate 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.MtlsPushedAuthorizationEndpoint ?? context.Configuration.PushedAuthorizationEndpoint) is Uri endpoint && + string.Equals(endpoint.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase) && + context.PushedAuthorizationEndpointClientCertificate is null && + context.Registration.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 can 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) && + context.PushedAuthorizationEndpointClientCertificate 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.MtlsPushedAuthorizationEndpoint ?? context.Configuration.PushedAuthorizationEndpoint) is Uri endpoint && + string.Equals(endpoint.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase) && + context.PushedAuthorizationEndpointClientCertificate is null && + context.Registration.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 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) + 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 @@ -5822,6 +6382,85 @@ public static partial class OpenIddictClientHandlers 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; + } + } + + /// + /// Contains the logic responsible for attaching the client certificate used for + /// the pushed authorization endpoint to the authentication context, if applicable. + /// + public sealed class AttachPushedAuthorizationEndpointClientCertificate : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(AttachPushedAuthorizationEndpointClientAuthenticationMethod.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessChallengeContext 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.PushedAuthorizationEndpointClientCertificate ??= context.PushedAuthorizationEndpointClientAuthenticationMethod switch + { + ClientAuthenticationMethods.TlsClientAuth => context.Registration.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.Registration.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 }; @@ -5841,7 +6480,7 @@ public static partial class OpenIddictClientHandlers = OpenIddictClientHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler() - .SetOrder(AttachPushedAuthorizationEndpointClientAuthenticationMethod.Descriptor.Order + 1_000) + .SetOrder(AttachPushedAuthorizationEndpointClientCertificate.Descriptor.Order + 1_000) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); @@ -5850,13 +6489,10 @@ public static partial class OpenIddictClientHandlers { ArgumentNullException.ThrowIfNull(context); - // If the URI of the pushed authorization endpoint endpoint wasn't - // explicitly set at this stage, try to extract it from the server configuration. context.PushedAuthorizationEndpoint ??= context.PushedAuthorizationEndpointClientAuthenticationMethod switch { - // When TLS client certificate authentication was negotiated, - // always favor the mTLS-specific endpoint if available. - ClientAuthenticationMethods.SelfSignedTlsClientAuth or ClientAuthenticationMethods.TlsClientAuth + // If a TLS client authentication certificate is going to be used, always favor the mTLS alias if available. + ClientAuthenticationMethods.TlsClientAuth or ClientAuthenticationMethods.SelfSignedTlsClientAuth when context.Configuration.MtlsPushedAuthorizationEndpoint is { IsAbsoluteUri: true } uri && !OpenIddictHelpers.IsImplicitFileUri(uri) => uri, @@ -6144,12 +6780,28 @@ public static partial class OpenIddictClientHandlers throw new InvalidOperationException(SR.FormatID0301(Metadata.DeviceAuthorizationEndpoint)); } + var certificate = context.DeviceAuthorizationEndpointClientAuthenticationMethod switch + { + ClientAuthenticationMethods.TlsClientAuth when context.DeviceAuthorizationEndpointClientCertificate is not null => + OpenIddictHelpers.IsSelfIssuedCertificate(context.DeviceAuthorizationEndpointClientCertificate) + ? throw new InvalidOperationException(SR.GetResourceString(SR.ID0513)) + : context.DeviceAuthorizationEndpointClientCertificate, + + ClientAuthenticationMethods.SelfSignedTlsClientAuth when context.DeviceAuthorizationEndpointClientCertificate is not null => + OpenIddictHelpers.IsSelfIssuedCertificate(context.DeviceAuthorizationEndpointClientCertificate) + ? context.DeviceAuthorizationEndpointClientCertificate + : throw new InvalidOperationException(SR.GetResourceString(SR.ID0513)), + + _ => null + }; + try { context.DeviceAuthorizationResponse = await _service.SendDeviceAuthorizationRequestAsync( context.Registration, context.Configuration, context.DeviceAuthorizationRequest, context.DeviceAuthorizationEndpoint, - context.DeviceAuthorizationEndpointClientAuthenticationMethod, context.CancellationToken); + context.DeviceAuthorizationEndpointClientAuthenticationMethod, + certificate, context.CancellationToken); } catch (ProtocolException exception) @@ -6394,12 +7046,28 @@ public static partial class OpenIddictClientHandlers throw new InvalidOperationException(SR.FormatID0301(Metadata.PushedAuthorizationRequestEndpoint)); } + var certificate = context.PushedAuthorizationEndpointClientAuthenticationMethod switch + { + ClientAuthenticationMethods.TlsClientAuth when context.PushedAuthorizationEndpointClientCertificate is not null => + OpenIddictHelpers.IsSelfIssuedCertificate(context.PushedAuthorizationEndpointClientCertificate) + ? throw new InvalidOperationException(SR.GetResourceString(SR.ID0513)) + : context.PushedAuthorizationEndpointClientCertificate, + + ClientAuthenticationMethods.SelfSignedTlsClientAuth when context.PushedAuthorizationEndpointClientCertificate is not null => + OpenIddictHelpers.IsSelfIssuedCertificate(context.PushedAuthorizationEndpointClientCertificate) + ? context.PushedAuthorizationEndpointClientCertificate + : throw new InvalidOperationException(SR.GetResourceString(SR.ID0513)), + + _ => null + }; + try { context.PushedAuthorizationResponse = await _service.SendPushedAuthorizationRequestAsync( context.Registration, context.Configuration, context.PushedAuthorizationRequest, context.PushedAuthorizationEndpoint, - context.PushedAuthorizationEndpointClientAuthenticationMethod, context.CancellationToken); + context.PushedAuthorizationEndpointClientAuthenticationMethod, + certificate, context.CancellationToken); } catch (ProtocolException exception) @@ -6797,6 +7465,14 @@ public static partial class OpenIddictClientHandlers return ValueTask.CompletedTask; } + // If the client is a public application, do not negotiate a client authentication method. + if (context.Registration.ClientType is ClientTypes.Public) + { + context.IntrospectionEndpointClientAuthenticationMethod = ClientAuthenticationMethods.None; + + 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. @@ -6809,11 +7485,60 @@ public static partial class OpenIddictClientHandlers Server: context.Configuration.IntrospectionEndpointAuthMethodsSupported) switch { - // If at least one signing key was attached to the client registration and both - // the client and the server explicitly support private_key_jwt, always prefer it. - ({ Count: > 0 } client, { Count: > 0 } server) when context.Registration.SigningCredentials.Count is not 0 && + // If a Public Key Infrastructure TLS client authentication certificate can 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) && + 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 && + string.Equals(endpoint.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase) && + context.IntrospectionEndpointClientCertificate is null && + context.Registration.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 can 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) && + 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 && + string.Equals(endpoint.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase) && + context.IntrospectionEndpointClientCertificate is null && + context.Registration.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 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) + 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 @@ -6823,6 +7548,85 @@ public static partial class OpenIddictClientHandlers 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; + } + } + + /// + /// Contains the logic responsible for attaching the client certificate used + /// for the introspection endpoint to the authentication context, if applicable. + /// + public sealed class AttachIntrospectionEndpointClientCertificate : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(AttachIntrospectionEndpointClientAuthenticationMethod.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessIntrospectionContext 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.Registration.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.Registration.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 }; @@ -6842,7 +7646,7 @@ public static partial class OpenIddictClientHandlers = OpenIddictClientHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler() - .SetOrder(AttachIntrospectionEndpointClientAuthenticationMethod.Descriptor.Order + 1_000) + .SetOrder(AttachIntrospectionEndpointClientCertificate.Descriptor.Order + 1_000) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); @@ -6851,13 +7655,10 @@ public static partial class OpenIddictClientHandlers { 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. - ClientAuthenticationMethods.SelfSignedTlsClientAuth or ClientAuthenticationMethods.TlsClientAuth + // If a TLS client authentication certificate is going to be used, always favor the mTLS alias if available. + ClientAuthenticationMethods.TlsClientAuth or ClientAuthenticationMethods.SelfSignedTlsClientAuth when context.Configuration.MtlsIntrospectionEndpoint is { IsAbsoluteUri: true } uri && !OpenIddictHelpers.IsImplicitFileUri(uri) => uri, @@ -7143,12 +7944,28 @@ public static partial class OpenIddictClientHandlers 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.Principal) = await _service.SendIntrospectionRequestAsync( context.Registration, context.Configuration, context.IntrospectionRequest, context.IntrospectionEndpoint, - context.IntrospectionEndpointClientAuthenticationMethod, context.CancellationToken); + context.IntrospectionEndpointClientAuthenticationMethod, + certificate, context.CancellationToken); } catch (ProtocolException exception) @@ -7429,6 +8246,14 @@ public static partial class OpenIddictClientHandlers return ValueTask.CompletedTask; } + // If the client is a public application, do not negotiate a client authentication method. + if (context.Registration.ClientType is ClientTypes.Public) + { + context.RevocationEndpointClientAuthenticationMethod = ClientAuthenticationMethods.None; + + 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. @@ -7441,11 +8266,60 @@ public static partial class OpenIddictClientHandlers Server: context.Configuration.RevocationEndpointAuthMethodsSupported) switch { - // If at least one signing key was attached to the client registration and both - // the client and the server explicitly support private_key_jwt, always prefer it. - ({ Count: > 0 } client, { Count: > 0 } server) when context.Registration.SigningCredentials.Count is not 0 && + // If a Public Key Infrastructure TLS client authentication certificate can 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) && + context.RevocationEndpointClientCertificate 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.MtlsRevocationEndpoint ?? context.Configuration.RevocationEndpoint) is Uri endpoint && + string.Equals(endpoint.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase) && + context.RevocationEndpointClientCertificate is null && + context.Registration.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 can 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) && + context.RevocationEndpointClientCertificate 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.MtlsRevocationEndpoint ?? context.Configuration.RevocationEndpoint) is Uri endpoint && + string.Equals(endpoint.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase) && + context.RevocationEndpointClientCertificate is null && + context.Registration.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 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) + 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 @@ -7455,6 +8329,85 @@ public static partial class OpenIddictClientHandlers 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; + } + } + + /// + /// Contains the logic responsible for attaching the client certificate used + /// for the revocation endpoint to the authentication context, if applicable. + /// + public sealed class AttachRevocationEndpointClientCertificate : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(AttachRevocationEndpointClientAuthenticationMethod.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessRevocationContext 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.RevocationEndpointClientCertificate ??= context.RevocationEndpointClientAuthenticationMethod switch + { + ClientAuthenticationMethods.TlsClientAuth => context.Registration.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.Registration.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 }; @@ -7474,7 +8427,7 @@ public static partial class OpenIddictClientHandlers = OpenIddictClientHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler() - .SetOrder(AttachRevocationEndpointClientAuthenticationMethod.Descriptor.Order + 1_000) + .SetOrder(AttachRevocationEndpointClientCertificate.Descriptor.Order + 1_000) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); @@ -7483,13 +8436,10 @@ public static partial class OpenIddictClientHandlers { ArgumentNullException.ThrowIfNull(context); - // If the URI of the revocation endpoint endpoint wasn't explicitly - // set at this stage, try to extract it from the server configuration. context.RevocationEndpoint ??= context.RevocationEndpointClientAuthenticationMethod switch { - // When TLS client certificate authentication was negotiated, - // always favor the mTLS-specific endpoint if available. - ClientAuthenticationMethods.SelfSignedTlsClientAuth or ClientAuthenticationMethods.TlsClientAuth + // If a TLS client authentication certificate is going to be used, always favor the mTLS alias if available. + ClientAuthenticationMethods.TlsClientAuth or ClientAuthenticationMethods.SelfSignedTlsClientAuth when context.Configuration.MtlsRevocationEndpoint is { IsAbsoluteUri: true } uri && !OpenIddictHelpers.IsImplicitFileUri(uri) => uri, @@ -7774,12 +8724,28 @@ public static partial class OpenIddictClientHandlers throw new InvalidOperationException(SR.FormatID0301(Metadata.RevocationEndpoint)); } + var certificate = context.RevocationEndpointClientAuthenticationMethod switch + { + ClientAuthenticationMethods.TlsClientAuth when context.RevocationEndpointClientCertificate is not null => + OpenIddictHelpers.IsSelfIssuedCertificate(context.RevocationEndpointClientCertificate) + ? throw new InvalidOperationException(SR.GetResourceString(SR.ID0513)) + : context.RevocationEndpointClientCertificate, + + ClientAuthenticationMethods.SelfSignedTlsClientAuth when context.RevocationEndpointClientCertificate is not null => + OpenIddictHelpers.IsSelfIssuedCertificate(context.RevocationEndpointClientCertificate) + ? context.RevocationEndpointClientCertificate + : throw new InvalidOperationException(SR.GetResourceString(SR.ID0513)), + + _ => null + }; + try { context.RevocationResponse = await _service.SendRevocationRequestAsync( context.Registration, context.Configuration, context.RevocationRequest, context.RevocationEndpoint, - context.RevocationEndpointClientAuthenticationMethod, context.CancellationToken); + context.RevocationEndpointClientAuthenticationMethod, + certificate, context.CancellationToken); } catch (ProtocolException exception) diff --git a/src/OpenIddict.Client/OpenIddictClientModels.cs b/src/OpenIddict.Client/OpenIddictClientModels.cs index 3132e9e8..524c93b8 100644 --- a/src/OpenIddict.Client/OpenIddictClientModels.cs +++ b/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 /// public string? IdentityTokenHint { get; init; } + /// + /// Gets or sets the issuer used to resolve the client registration. + /// + /// + /// Note: if multiple client registrations point to the same issuer, + /// the property must be explicitly set. + /// + public Uri? Issuer { get; init; } + /// /// Gets or sets the optional login hint that will be sent to the authorization server, if applicable. /// @@ -231,15 +241,6 @@ public static class OpenIddictClientModels /// Gets the scopes that will be sent to the authorization server. /// public List? Scopes { get; init; } - - /// - /// Gets or sets the issuer used to resolve the client registration. - /// - /// - /// Note: if multiple client registrations point to the same issuer, - /// the property must be explicitly set. - /// - public Uri? Issuer { get; init; } } /// @@ -285,6 +286,15 @@ public static class OpenIddictClientModels /// public string? IdentityTokenHint { get; init; } + /// + /// Gets or sets the issuer used to resolve the client registration. + /// + /// + /// Note: if multiple client registrations point to the same issuer, + /// the property must be explicitly set. + /// + public Uri? Issuer { get; init; } + /// /// Gets or sets the optional login hint that will be sent to the authorization server, if applicable. /// @@ -318,15 +328,6 @@ public static class OpenIddictClientModels /// Gets the scopes that will be sent to the authorization server. /// public List? Scopes { get; init; } - - /// - /// Gets or sets the issuer used to resolve the client registration. - /// - /// - /// Note: if multiple client registrations point to the same issuer, - /// the property must be explicitly set. - /// - public Uri? Issuer { get; init; } } /// @@ -366,6 +367,15 @@ public static class OpenIddictClientModels /// public CancellationToken CancellationToken { get; init; } + /// + /// Gets or sets the issuer used to resolve the client registration. + /// + /// + /// Note: if multiple client registrations point to the same issuer, + /// the property must be explicitly set. + /// + public Uri? Issuer { get; init; } + /// /// Gets or sets the application-specific properties that will be added to the context. /// @@ -396,13 +406,21 @@ public static class OpenIddictClientModels public List? Scopes { get; init; } /// - /// 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. /// /// - /// Note: if multiple client registrations point to the same issuer, - /// the property must be explicitly set. + /// + /// Note: when mTLs is also used for OAuth 2.0 client authentication, the + /// certificate set here replaces the client certificate chosen by OpenIddict. + /// + /// + /// Note: if a certificate-based client authentication or token binding method is + /// negotiated, the type of the certificate must match the negotiated methods. + /// /// - public Uri? Issuer { get; init; } + [EditorBrowsable(EditorBrowsableState.Advanced)] + public X509Certificate2? TokenBindingCertificate { get; init; } } /// @@ -513,6 +531,15 @@ public static class OpenIddictClientModels /// public required string GrantType { get; init; } + /// + /// Gets or sets the issuer used to resolve the client registration. + /// + /// + /// Note: if multiple client registrations point to the same issuer, + /// the property must be explicitly set. + /// + public Uri? Issuer { get; init; } + /// /// Gets or sets the application-specific properties that will be added to the context. /// @@ -543,13 +570,21 @@ public static class OpenIddictClientModels public List? Scopes { get; init; } /// - /// 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. /// /// - /// Note: if multiple client registrations point to the same issuer, - /// the property must be explicitly set. + /// + /// Note: when mTLs is also used for OAuth 2.0 client authentication, the + /// certificate set here replaces the client certificate chosen by OpenIddict. + /// + /// + /// Note: if a certificate-based client authentication or token binding method is + /// negotiated, the type of the certificate must match the negotiated methods. + /// /// - public Uri? Issuer { get; init; } + [EditorBrowsable(EditorBrowsableState.Advanced)] + public X509Certificate2? TokenBindingCertificate { get; init; } } /// @@ -655,6 +690,15 @@ public static class OpenIddictClientModels /// public required TimeSpan Interval { get; init; } + /// + /// Gets or sets the issuer used to resolve the client registration. + /// + /// + /// Note: if multiple client registrations point to the same issuer, + /// the property must be explicitly set. + /// + public Uri? Issuer { get; init; } + /// /// Gets or sets the application-specific properties that will be added to the context. /// @@ -685,13 +729,21 @@ public static class OpenIddictClientModels public List? Scopes { get; init; } /// - /// 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. /// /// - /// Note: if multiple client registrations point to the same issuer, - /// the property must be explicitly set. + /// + /// Note: when mTLs is also used for OAuth 2.0 client authentication, the + /// certificate set here replaces the client certificate chosen by OpenIddict. + /// + /// + /// Note: if a certificate-based client authentication or token binding method is + /// negotiated, the type of the certificate must match the negotiated methods. + /// /// - public Uri? Issuer { get; init; } + [EditorBrowsable(EditorBrowsableState.Advanced)] + public X509Certificate2? TokenBindingCertificate { get; init; } } /// @@ -775,6 +827,15 @@ public static class OpenIddictClientModels /// public CancellationToken CancellationToken { get; init; } + /// + /// Gets or sets the issuer used to resolve the client registration. + /// + /// + /// Note: if multiple client registrations point to the same issuer, + /// the property must be explicitly set. + /// + public Uri? Issuer { get; init; } + /// /// Gets or sets the application-specific properties that will be added to the context. /// @@ -803,15 +864,6 @@ public static class OpenIddictClientModels /// Gets the scopes that will be sent to the authorization server. /// public List? Scopes { get; init; } - - /// - /// Gets or sets the issuer used to resolve the client registration. - /// - /// - /// Note: if multiple client registrations point to the same issuer, - /// the property must be explicitly set. - /// - public Uri? Issuer { get; init; } } /// @@ -876,6 +928,15 @@ public static class OpenIddictClientModels /// public CancellationToken CancellationToken { get; init; } + /// + /// Gets or sets the issuer used to resolve the client registration. + /// + /// + /// Note: if multiple client registrations point to the same issuer, + /// the property must be explicitly set. + /// + public Uri? Issuer { get; init; } + /// /// Gets or sets the application-specific properties that will be added to the context. /// @@ -904,15 +965,6 @@ public static class OpenIddictClientModels /// Gets the token type hint that will be sent to the authorization server. /// public string? TokenTypeHint { get; init; } - - /// - /// Gets or sets the issuer used to resolve the client registration. - /// - /// - /// Note: if multiple client registrations point to the same issuer, - /// the property must be explicitly set. - /// - public Uri? Issuer { get; init; } } /// @@ -967,6 +1019,15 @@ public static class OpenIddictClientModels /// public bool DisableUserInfo { get; init; } + /// + /// Gets or sets the issuer used to resolve the client registration. + /// + /// + /// Note: if multiple client registrations point to the same issuer, + /// the property must be explicitly set. + /// + public Uri? Issuer { get; init; } + /// /// Gets or sets the password that will be sent to the authorization server. /// @@ -1007,13 +1068,21 @@ public static class OpenIddictClientModels public required string Username { get; init; } /// - /// 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. /// /// - /// Note: if multiple client registrations point to the same issuer, - /// the property must be explicitly set. + /// + /// Note: when mTLs is also used for OAuth 2.0 client authentication, the + /// certificate set here replaces the client certificate chosen by OpenIddict. + /// + /// + /// Note: if a certificate-based client authentication or token binding method is + /// negotiated, the type of the certificate must match the negotiated methods. + /// /// - public Uri? Issuer { get; init; } + [EditorBrowsable(EditorBrowsableState.Advanced)] + public X509Certificate2? TokenBindingCertificate { get; init; } } /// @@ -1102,6 +1171,15 @@ public static class OpenIddictClientModels /// public bool DisableUserInfo { get; init; } + /// + /// Gets or sets the issuer used to resolve the client registration. + /// + /// + /// Note: if multiple client registrations point to the same issuer, + /// the property must be explicitly set. + /// + public Uri? Issuer { get; init; } + /// /// Gets or sets the application-specific properties that will be added to the context. /// @@ -1137,13 +1215,21 @@ public static class OpenIddictClientModels public required string RefreshToken { get; init; } /// - /// 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. /// /// - /// Note: if multiple client registrations point to the same issuer, - /// the property must be explicitly set. + /// + /// Note: when mTLs is also used for OAuth 2.0 client authentication, the + /// certificate set here replaces the client certificate chosen by OpenIddict. + /// + /// + /// Note: if a certificate-based client authentication or token binding method is + /// negotiated, the type of the certificate must match the negotiated methods. + /// /// - public Uri? Issuer { get; init; } + [EditorBrowsable(EditorBrowsableState.Advanced)] + public X509Certificate2? TokenBindingCertificate { get; init; } } /// @@ -1245,6 +1331,15 @@ public static class OpenIddictClientModels /// public bool DisableUserInfo { get; init; } + /// + /// Gets or sets the issuer used to resolve the client registration. + /// + /// + /// Note: if multiple client registrations point to the same issuer, + /// the property must be explicitly set. + /// + public Uri? Issuer { get; init; } + /// /// Gets or sets the application-specific properties that will be added to the context. /// @@ -1290,13 +1385,21 @@ public static class OpenIddictClientModels public required string SubjectTokenType { get; init; } /// - /// 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. /// /// - /// Note: if multiple client registrations point to the same issuer, - /// the property must be explicitly set. + /// + /// Note: when mTLs is also used for OAuth 2.0 client authentication, the + /// certificate set here replaces the client certificate chosen by OpenIddict. + /// + /// + /// Note: if a certificate-based client authentication or token binding method is + /// negotiated, the type of the certificate must match the negotiated methods. + /// /// - public Uri? Issuer { get; init; } + [EditorBrowsable(EditorBrowsableState.Advanced)] + public X509Certificate2? TokenBindingCertificate { get; init; } } /// diff --git a/src/OpenIddict.Client/OpenIddictClientOptions.cs b/src/OpenIddict.Client/OpenIddictClientOptions.cs index 7edf7e96..62152541 100644 --- a/src/OpenIddict.Client/OpenIddictClientOptions.cs +++ b/src/OpenIddict.Client/OpenIddictClientOptions.cs @@ -218,4 +218,9 @@ public sealed class OpenIddictClientOptions /// If no service can be found, is used. /// public TimeProvider TimeProvider { get; set; } = default!; + + /// + /// Gets the OAuth 2.0 token binding methods enabled for this application. + /// + public HashSet TokenBindingMethods { get; } = new(StringComparer.Ordinal); } diff --git a/src/OpenIddict.Client/OpenIddictClientRegistration.cs b/src/OpenIddict.Client/OpenIddictClientRegistration.cs index 933f9121..bd42bc14 100644 --- a/src/OpenIddict.Client/OpenIddictClientRegistration.cs +++ b/src/OpenIddict.Client/OpenIddictClientRegistration.cs @@ -83,6 +83,12 @@ public sealed class OpenIddictClientRegistration /// public HashSet ClientAuthenticationMethods { get; } = new(StringComparer.Ordinal); + /// + /// 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). + /// + public string? ClientType { get; set; } + /// /// 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 /// public HashSet ResponseTypes { get; } = new(StringComparer.Ordinal); + /// + /// 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. + /// + /// + /// 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. + /// + public HashSet TokenBindingMethods { get; } = new(StringComparer.Ordinal); + /// /// Gets or sets the issuer that will be attached to the /// instances created by the OpenIddict client stack for this registration. diff --git a/src/OpenIddict.Client/OpenIddictClientService.cs b/src/OpenIddict.Client/OpenIddictClientService.cs index 25a4e864..a0949190 100644 --- a/src/OpenIddict.Client/OpenIddictClientService.cs +++ b/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 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 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 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 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 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 parameters ? new(parameters) : new() }; @@ -1478,11 +1485,13 @@ public class OpenIddictClientService /// The device authorization request. /// The uri of the remote device authorization endpoint. /// The client authentication method, if applicable. + /// The client certificate, if applicable. /// The that can be used to abort the operation. /// The token response. internal async ValueTask 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 /// The token request. /// The uri of the remote token endpoint. /// The client authentication method, if applicable. + /// The client certificate, if applicable. /// The that can be used to abort the operation. /// The response and the principal extracted from the introspection response. 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 /// The pushed authorization request. /// The uri of the remote pushed authorization endpoint. /// The client authentication method, if applicable. + /// The client certificate, if applicable. /// The that can be used to abort the operation. /// The token response. internal async ValueTask 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 /// The token request. /// The uri of the remote token endpoint. /// The client authentication method, if applicable. + /// The client certificate, if applicable. /// The that can be used to abort the operation. /// The response extracted from the revocation response. internal async ValueTask 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 /// The token request. /// The uri of the remote token endpoint. /// The client authentication method, if applicable. + /// The client certificate, if applicable. /// The that can be used to abort the operation. /// The token response. internal async ValueTask 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 /// The server configuration. /// The userinfo request. /// The uri of the remote userinfo endpoint. - /// The token binding methods to use, if applicable. + /// The client certificate, if applicable. /// The that can be used to abort the operation. /// The response and the principal extracted from the userinfo response or the userinfo token. internal async ValueTask<(OpenIddictResponse Response, (ClaimsPrincipal? Principal, string? Token))> SendUserInfoRequestAsync( OpenIddictClientRegistration registration, OpenIddictConfiguration configuration, - OpenIddictRequest request, Uri uri, HashSet 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) diff --git a/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs b/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs index 70641f43..bb6178e2 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs @@ -471,25 +471,6 @@ public class OpenIddictApplicationManager : IOpenIddictApplication return Store.GetAsync(query, state, cancellationToken); } - /// - /// Retrieves the client certificate chain policy enforced for this application. - /// - /// The application. - /// The base policy from which the returned instance will be derived. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the client certificate chain policy enforced for this application. - /// - public virtual ValueTask GetClientCertificateChainPolicyAsync( - TApplication application, X509ChainPolicy policy, CancellationToken cancellationToken = default) - { - ArgumentNullException.ThrowIfNull(application); - - // Always clone the X.509 chain policy to ensure the original instance is never mutated. - return new(policy.Clone()); - } - /// /// Retrieves the client identifier associated with an application. /// @@ -724,6 +705,69 @@ public class OpenIddictApplicationManager : IOpenIddictApplication return Store.GetPropertiesAsync(application, cancellationToken); } + /// + /// Retrieves the PKI client certificate authentication policy enforced for this application. + /// + /// The application. + /// The base policy from which the returned instance will be derived. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, whose + /// result returns the PKI client certificate authentication policy enforced for this application. + /// + public virtual async ValueTask 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() + .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; + } + /// /// Retrieves the redirect URIs associated with an application. /// @@ -759,16 +803,16 @@ public class OpenIddictApplicationManager : IOpenIddictApplication } /// - /// Retrieves the self-signed client certificate chain policy enforced for this application. + /// Retrieves the self-signed client certificate authentication policy enforced for this application. /// /// The application. /// The base policy from which the returned instance will be derived. /// The that can be used to abort the operation. /// /// A that can be used to monitor the asynchronous operation, whose - /// result returns the self-signed client certificate chain policy enforced for this application. + /// result returns the self-signed client certificate authentication policy enforced for this application. /// - public virtual async ValueTask GetSelfSignedClientCertificateChainPolicyAsync( + public virtual async ValueTask GetSelfSignedTlsClientAuthenticationPolicyAsync( TApplication application, X509ChainPolicy policy, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(application); @@ -776,17 +820,17 @@ public class OpenIddictApplicationManager : 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 : IOpenIddictApplication } } - /// - /// Validates the client certificate associated with an application. - /// - /// The application. - /// The certificate that should be compared to the certificates associated with the application. - /// The chain policy used to validate the certificate. - /// The that can be used to abort the operation. - /// A that can be used to monitor the asynchronous operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns a boolean indicating whether the client certificate was valid. - /// - public virtual async ValueTask ValidateClientCertificateAsync( - TApplication application, X509Certificate2 certificate, - X509ChainPolicy policy, CancellationToken cancellationToken = default) - { - ArgumentNullException.ThrowIfNull(application); - ArgumentNullException.ThrowIfNull(certificate); - ArgumentNullException.ThrowIfNull(policy); - - // Important: the certificate and policy instances MUST NOT be mutated in this method. - - if (OpenIddictHelpers.IsSelfIssuedCertificate(certificate)) - { - throw new ArgumentException(SR.GetResourceString(SR.ID0503), nameof(certificate)); - } - - // Note: using a policy relying on the default system trust store - // is strongly discouraged but deliberately not prevented here. - - if (await HasClientTypeAsync(application, ClientTypes.Public, cancellationToken)) - { - Logger.LogWarning(6159, SR.GetResourceString(SR.ID6159)); - - return false; - } - -#if SUPPORTS_X509_CHAIN_POLICY_CUSTOM_TRUST_STORE - var uris = await GetRedirectUrisAsync(application, cancellationToken); - if (uris.IsDefaultOrEmpty) - { - Logger.LogInformation(6285, SR.GetResourceString(SR.ID6285), await GetClientIdAsync(application, cancellationToken)); - - return false; - } - - using var chain = new X509Chain() - { - ChainPolicy = policy - }; - - try - { - // Ensure the specified certificate is valid based on the chain policy. - if (!chain.Build(certificate)) - { - Logger.LogInformation(6286, SR.GetResourceString(SR.ID6286), - await GetClientIdAsync(application, cancellationToken), - chain.ChainStatus.Select(static status => status.Status).ToArray()); - - return false; - } - - // Note: this method MUST NOT be used with self-signed certificates. While self-issued - // certificates are immediately rejected by this method, determining whether a certificate - // is actually self-signed can only be done after building and validating the chain. - if (chain.ChainElements.Count is not > 1) - { - throw new ArgumentException(SR.GetResourceString(SR.ID0503), nameof(certificate)); - } - - // By default, OpenIddict requires that certificates issued by PKIs be valid for one of the domains - // used in redirect URIs. Implementations that need a different logic can override this method. - for (var index = 0; index < uris.Length; index++) - { - if (Uri.TryCreate(uris[index], UriKind.Absolute, out Uri? uri) && !OpenIddictHelpers.IsImplicitFileUri(uri) && - uri.HostNameType is UriHostNameType.Dns or UriHostNameType.IPv4 or UriHostNameType.IPv6 && - certificate.MatchesHostname(hostname: uri.IdnHost, allowWildcards: true, allowCommonName: true)) - { - return true; - } - } - - Logger.LogInformation(6287, SR.GetResourceString(SR.ID6287), await GetClientIdAsync(application, cancellationToken)); - - return false; - } - - catch (CryptographicException exception) when (!OpenIddictHelpers.IsFatal(exception)) - { - Logger.LogWarning(6288, exception, SR.GetResourceString(SR.ID6288)); - - return false; - } - - finally - { - // Dispose the certificates instantiated internally while building the chain. - for (var index = 0; index < chain.ChainElements.Count; index++) - { - chain.ChainElements[index].Certificate.Dispose(); - } - } -#else - throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0508)); -#endif - } - /// /// Validates the client_secret associated with an application. /// @@ -1502,6 +1438,107 @@ public class OpenIddictApplicationManager : IOpenIddictApplication return false; } + /// + /// Validates the PKI client certificate to ensure it can be used by the specified application. + /// + /// The application. + /// The certificate that should be compared to the certificates associated with the application. + /// The chain policy used to validate the certificate. + /// The that can be used to abort the operation. + /// A that can be used to monitor the asynchronous operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns a boolean indicating whether the client certificate was valid. + /// + public virtual async ValueTask 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 + } + /// /// Validates the redirect_uri to ensure it's associated with an application. /// @@ -1569,7 +1606,7 @@ public class OpenIddictApplicationManager : IOpenIddictApplication } /// - /// 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. /// /// The application. /// The certificate that should be compared to the certificates associated with the application. @@ -1580,7 +1617,7 @@ public class OpenIddictApplicationManager : IOpenIddictApplication /// A that can be used to monitor the asynchronous operation, whose /// result returns a boolean indicating whether the self-signed client certificate was valid. /// - public virtual async ValueTask ValidateSelfSignedClientCertificateAsync( + public virtual async ValueTask ValidateSelfSignedTlsClientCertificateAsync( TApplication application, X509Certificate2 certificate, X509ChainPolicy policy, CancellationToken cancellationToken = default) { @@ -1598,13 +1635,6 @@ public class OpenIddictApplicationManager : 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 : IOpenIddictApplication ValueTask IOpenIddictApplicationManager.GetAsync(Func, TState, IQueryable> query, TState state, CancellationToken cancellationToken) where TResult : default => GetAsync(query, state, cancellationToken); - /// - ValueTask IOpenIddictApplicationManager.GetClientCertificateChainPolicyAsync(object application, X509ChainPolicy policy, CancellationToken cancellationToken) - => GetClientCertificateChainPolicyAsync((TApplication) application, policy, cancellationToken); - /// ValueTask IOpenIddictApplicationManager.GetClientIdAsync(object application, CancellationToken cancellationToken) => GetClientIdAsync((TApplication) application, cancellationToken); @@ -1921,6 +1947,10 @@ public class OpenIddictApplicationManager : IOpenIddictApplication ValueTask> IOpenIddictApplicationManager.GetPropertiesAsync(object application, CancellationToken cancellationToken) => GetPropertiesAsync((TApplication) application, cancellationToken); + /// + ValueTask IOpenIddictApplicationManager.GetPublicKeyInfrastructureTlsClientAuthenticationPolicyAsync(object application, X509ChainPolicy policy, CancellationToken cancellationToken) + => GetPublicKeyInfrastructureTlsClientAuthenticationPolicyAsync((TApplication) application, policy, cancellationToken); + /// ValueTask> IOpenIddictApplicationManager.GetRedirectUrisAsync(object application, CancellationToken cancellationToken) => GetRedirectUrisAsync((TApplication) application, cancellationToken); @@ -1930,8 +1960,8 @@ public class OpenIddictApplicationManager : IOpenIddictApplication => GetRequirementsAsync((TApplication) application, cancellationToken); /// - ValueTask IOpenIddictApplicationManager.GetSelfSignedClientCertificateChainPolicyAsync(object application, X509ChainPolicy policy, CancellationToken cancellationToken) - => GetSelfSignedClientCertificateChainPolicyAsync((TApplication) application, policy, cancellationToken); + ValueTask IOpenIddictApplicationManager.GetSelfSignedTlsClientAuthenticationPolicyAsync(object application, X509ChainPolicy policy, CancellationToken cancellationToken) + => GetSelfSignedTlsClientAuthenticationPolicyAsync((TApplication) application, policy, cancellationToken); /// ValueTask> IOpenIddictApplicationManager.GetSettingsAsync(object application, CancellationToken cancellationToken) @@ -1993,10 +2023,6 @@ public class OpenIddictApplicationManager : IOpenIddictApplication IAsyncEnumerable IOpenIddictApplicationManager.ValidateAsync(object application, CancellationToken cancellationToken) => ValidateAsync((TApplication) application, cancellationToken); - /// - ValueTask IOpenIddictApplicationManager.ValidateClientCertificateAsync(object application, X509Certificate2 certificate, X509ChainPolicy policy, CancellationToken cancellationToken) - => ValidateClientCertificateAsync((TApplication) application, certificate, policy, cancellationToken); - /// ValueTask IOpenIddictApplicationManager.ValidateClientSecretAsync(object application, string secret, CancellationToken cancellationToken) => ValidateClientSecretAsync((TApplication) application, secret, cancellationToken); @@ -2005,11 +2031,15 @@ public class OpenIddictApplicationManager : IOpenIddictApplication ValueTask IOpenIddictApplicationManager.ValidatePostLogoutRedirectUriAsync(object application, [StringSyntax(StringSyntaxAttribute.Uri)] string uri, CancellationToken cancellationToken) => ValidatePostLogoutRedirectUriAsync((TApplication) application, uri, cancellationToken); + /// + ValueTask IOpenIddictApplicationManager.ValidatePublicKeyInfrastructureTlsClientCertificateAsync(object application, X509Certificate2 certificate, X509ChainPolicy policy, CancellationToken cancellationToken) + => ValidatePublicKeyInfrastructureTlsClientCertificateAsync((TApplication) application, certificate, policy, cancellationToken); + /// ValueTask IOpenIddictApplicationManager.ValidateRedirectUriAsync(object application, [StringSyntax(StringSyntaxAttribute.Uri)] string uri, CancellationToken cancellationToken) => ValidateRedirectUriAsync((TApplication) application, uri, cancellationToken); /// - ValueTask IOpenIddictApplicationManager.ValidateSelfSignedClientCertificateAsync(object application, X509Certificate2 certificate, X509ChainPolicy policy, CancellationToken cancellationToken) - => ValidateSelfSignedClientCertificateAsync((TApplication) application, certificate, policy, cancellationToken); + ValueTask IOpenIddictApplicationManager.ValidateSelfSignedTlsClientCertificateAsync(object application, X509Certificate2 certificate, X509ChainPolicy policy, CancellationToken cancellationToken) + => ValidateSelfSignedTlsClientCertificateAsync((TApplication) application, certificate, policy, cancellationToken); } diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConfiguration.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConfiguration.cs index 309d1749..545b24e8 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConfiguration.cs +++ b/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); } diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Authentication.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Authentication.cs index 11376e4b..c20568a2 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Authentication.cs +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Authentication.cs @@ -48,7 +48,7 @@ public static partial class OpenIddictServerAspNetCoreHandlers */ ExtractPostRequest.Descriptor, ValidateClientAuthenticationMethod.Descriptor, - ExtractClientAuthenticationCertificate.Descriptor, + ExtractClientCertificate.Descriptor, ExtractBasicAuthenticationCredentials.Descriptor, /* diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Device.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Device.cs index f1187caf..62242c53 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Device.cs +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Device.cs @@ -21,7 +21,7 @@ public static partial class OpenIddictServerAspNetCoreHandlers */ ExtractPostRequest.Descriptor, ValidateClientAuthenticationMethod.Descriptor, - ExtractClientAuthenticationCertificate.Descriptor, + ExtractClientCertificate.Descriptor, ExtractBasicAuthenticationCredentials.Descriptor, /* diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Exchange.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Exchange.cs index 5b1c5d83..fbc3d97a 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Exchange.cs +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Exchange.cs @@ -19,7 +19,7 @@ public static partial class OpenIddictServerAspNetCoreHandlers */ ExtractPostRequest.Descriptor, ValidateClientAuthenticationMethod.Descriptor, - ExtractClientAuthenticationCertificate.Descriptor, + ExtractClientCertificate.Descriptor, ExtractBasicAuthenticationCredentials.Descriptor, /* diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Introspection.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Introspection.cs index 6f7331c4..6e638883 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Introspection.cs +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Introspection.cs @@ -19,7 +19,7 @@ public static partial class OpenIddictServerAspNetCoreHandlers */ ExtractGetOrPostRequest.Descriptor, ValidateClientAuthenticationMethod.Descriptor, - ExtractClientAuthenticationCertificate.Descriptor, + ExtractClientCertificate.Descriptor, ExtractBasicAuthenticationCredentials.Descriptor, /* diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Revocation.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Revocation.cs index 11dec150..cbc2fb7c 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Revocation.cs +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Revocation.cs @@ -19,7 +19,7 @@ public static partial class OpenIddictServerAspNetCoreHandlers */ ExtractPostRequest.Descriptor, ValidateClientAuthenticationMethod.Descriptor, - ExtractClientAuthenticationCertificate.Descriptor, + ExtractClientCertificate.Descriptor, ExtractBasicAuthenticationCredentials.Descriptor, /* diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Userinfo.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Userinfo.cs index 7db31228..a077d104 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Userinfo.cs +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Userinfo.cs @@ -19,6 +19,7 @@ public static partial class OpenIddictServerAspNetCoreHandlers */ ExtractGetOrPostRequest.Descriptor, ExtractAccessToken.Descriptor, + ExtractClientCertificate.Descriptor, /* * UserInfo request handling: diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs index 0a354278..ed198e83 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs @@ -627,7 +627,7 @@ public static partial class OpenIddictServerAspNetCoreHandlers .Build(); /// - 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; } } /// - /// 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. /// - public sealed class ExtractClientAuthenticationCertificate : IOpenIddictServerHandler + public sealed class ExtractClientCertificate : IOpenIddictServerHandler where TContext : BaseValidatingContext { /// @@ -717,7 +688,7 @@ public static partial class OpenIddictServerAspNetCoreHandlers public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() .AddFilter() - .UseSingletonHandler>() + .UseSingletonHandler>() .SetOrder(ValidateClientAuthenticationMethod.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() .AddFilter() .UseSingletonHandler>() - .SetOrder(ExtractClientAuthenticationCertificate.Descriptor.Order + 1_000) + .SetOrder(ExtractClientCertificate.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinConfiguration.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinConfiguration.cs index cd2ddd65..a199f0b6 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinConfiguration.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinConfiguration.cs @@ -36,12 +36,12 @@ public sealed class OpenIddictServerOwinConfiguration : IConfigureOptions.Descriptor, ValidateClientAuthenticationMethod.Descriptor, - ExtractClientAuthenticationCertificate.Descriptor, + ExtractClientCertificate.Descriptor, ExtractBasicAuthenticationCredentials.Descriptor, /* diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Device.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Device.cs index 08f434ab..ea8612c5 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Device.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Device.cs @@ -21,7 +21,7 @@ public static partial class OpenIddictServerOwinHandlers */ ExtractPostRequest.Descriptor, ValidateClientAuthenticationMethod.Descriptor, - ExtractClientAuthenticationCertificate.Descriptor, + ExtractClientCertificate.Descriptor, ExtractBasicAuthenticationCredentials.Descriptor, /* diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Exchange.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Exchange.cs index 6e388389..5e6d6054 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Exchange.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Exchange.cs @@ -19,7 +19,7 @@ public static partial class OpenIddictServerOwinHandlers */ ExtractPostRequest.Descriptor, ValidateClientAuthenticationMethod.Descriptor, - ExtractClientAuthenticationCertificate.Descriptor, + ExtractClientCertificate.Descriptor, ExtractBasicAuthenticationCredentials.Descriptor, /* diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Introspection.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Introspection.cs index af3064e4..4633e62b 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Introspection.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Introspection.cs @@ -19,7 +19,7 @@ public static partial class OpenIddictServerOwinHandlers */ ExtractGetOrPostRequest.Descriptor, ValidateClientAuthenticationMethod.Descriptor, - ExtractClientAuthenticationCertificate.Descriptor, + ExtractClientCertificate.Descriptor, ExtractBasicAuthenticationCredentials.Descriptor, /* diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Revocation.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Revocation.cs index f0ef99f5..6cff2b3b 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Revocation.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Revocation.cs @@ -19,7 +19,7 @@ public static partial class OpenIddictServerOwinHandlers */ ExtractPostRequest.Descriptor, ValidateClientAuthenticationMethod.Descriptor, - ExtractClientAuthenticationCertificate.Descriptor, + ExtractClientCertificate.Descriptor, ExtractBasicAuthenticationCredentials.Descriptor, /* diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Userinfo.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Userinfo.cs index 8e1123a5..c47cf54d 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Userinfo.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Userinfo.cs @@ -19,6 +19,7 @@ public static partial class OpenIddictServerOwinHandlers */ ExtractGetOrPostRequest.Descriptor, ExtractAccessToken.Descriptor, + ExtractClientCertificate.Descriptor, /* * UserInfo request handling: diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs index f311a339..e92d99ee 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs @@ -689,7 +689,7 @@ public static partial class OpenIddictServerOwinHandlers .Build(); /// - 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 GetClientCertificateAsync(IOwinContext context) - { - // If a loading function was provided by the OWIN host, always invoke it before trying - // to resolve the certificate to ensure it is present in the environment dictionary. - if (context.Get>("ssl.LoadClientCertAsync") is Func loader) - { - await loader(); - } - - if (context.Get("ssl.ClientCertificateErrors") is not null) - { - return null; - } + // 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("ssl.ClientCertificate") is X509Certificate certificate - ? certificate as X509Certificate2 ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0498)) - : null; - } + return ValueTask.CompletedTask; } } /// - /// 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. /// - public sealed class ExtractClientAuthenticationCertificate : IOpenIddictServerHandler + public sealed class ExtractClientCertificate : IOpenIddictServerHandler where TContext : BaseValidatingContext { /// @@ -797,7 +750,7 @@ public static partial class OpenIddictServerOwinHandlers public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() .AddFilter() - .UseSingletonHandler>() + .UseSingletonHandler>() .SetOrder(ValidateClientAuthenticationMethod.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 GetClientCertificateAsync(IOwinContext context) @@ -829,11 +780,6 @@ public static partial class OpenIddictServerOwinHandlers await loader(); } - if (context.Get("ssl.ClientCertificateErrors") is not null) - { - return null; - } - return context.Get("ssl.ClientCertificate") is X509Certificate certificate ? certificate as X509Certificate2 ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0498)) : null; @@ -855,7 +801,7 @@ public static partial class OpenIddictServerOwinHandlers = OpenIddictServerHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler>() - .SetOrder(ExtractClientAuthenticationCertificate.Descriptor.Order + 1_000) + .SetOrder(ExtractClientCertificate.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); diff --git a/src/OpenIddict.Server/OpenIddictServerBuilder.cs b/src/OpenIddict.Server/OpenIddictServerBuilder.cs index 8cb4fb4a..48005fe5 100644 --- a/src/OpenIddict.Server/OpenIddictServerBuilder.cs +++ b/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); } + /// + /// Sets the URI listed as the mTLS userinfo endpoint + /// alias in the server configuration metadata. + /// + /// + /// Note: this URI MUST be absolute and MUST point to a domain for + /// which TLS client authentication is enforced by the web server. + /// + /// The endpoint URI. + /// The instance. + public OpenIddictServerBuilder 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); + } + + /// + /// Sets the URI listed as the mTLS userinfo endpoint + /// alias in the server configuration metadata. + /// + /// + /// Note: this URI MUST be absolute and MUST point to a domain for + /// which TLS client authentication is enforced by the web server. + /// + /// The endpoint URI. + /// The instance. + public OpenIddictServerBuilder 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); + } + /// /// 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); + /// + /// 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. + /// + /// The instance. + public OpenIddictServerBuilder UseClientCertificateBoundAccessTokens() + => Configure(options => options.UseClientCertificateBoundAccessTokens = true); + + /// + /// Configures OpenIddict to bind refresh tokens to the client certificates client + /// certificate sent by public clients in the TLS handshake of token requests. + /// + /// + /// 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. + /// + /// The instance. + public OpenIddictServerBuilder UseClientCertificateBoundRefreshTokens() + => Configure(options => options.UseClientCertificateBoundRefreshTokens = true); + /// /// 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 /// The store containing the root and intermediate certificates to trust. /// The instance. [EditorBrowsable(EditorBrowsableState.Advanced)] - public OpenIddictServerBuilder EnablePublicKeyInfrastructureClientCertificateAuthentication(X509Certificate2Collection certificates) - => EnablePublicKeyInfrastructureClientCertificateAuthentication(certificates, static policy => { }); + public OpenIddictServerBuilder EnablePublicKeyInfrastructureTlsClientAuthentication(X509Certificate2Collection certificates) + => EnablePublicKeyInfrastructureTlsClientAuthentication(certificates, static policy => { }); /// /// Configures OpenIddict to enable PKI client certificate authentication (mTLS) and trust @@ -2229,7 +2298,7 @@ public sealed class OpenIddictServerBuilder /// The delegate used to amend the created X.509 chain policy. /// The instance. [EditorBrowsable(EditorBrowsableState.Advanced)] - public OpenIddictServerBuilder EnablePublicKeyInfrastructureClientCertificateAuthentication( + public OpenIddictServerBuilder EnablePublicKeyInfrastructureTlsClientAuthentication( X509Certificate2Collection certificates, Action 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 }; @@ -2270,7 +2339,7 @@ public sealed class OpenIddictServerBuilder policy.CustomTrustStore.AddRange(certificates); // If one of the root certificates doesn't include a CRL or AIA - // extension, ignore root revocation unknown status errors by default. + // extension, ignore root revocation unknown status errors by default. if (certificates.Cast() .Where(static certificate => OpenIddictHelpers.IsCertificateAuthority(certificate) && @@ -2284,7 +2353,7 @@ public sealed class OpenIddictServerBuilder } // If one of the intermediate certificates doesn't include a CRL or AIA - // extension, ignore root revocation unknown status errors by default. + // extension, ignore root revocation unknown status errors by default. if (certificates.Cast() .Where(static certificate => OpenIddictHelpers.IsCertificateAuthority(certificate) && @@ -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 } /// - /// Configures OpenIddict to enable self-signed client certificate authentication (mTLS). + /// Configures OpenIddict to enable self-signed TLS client authentication (mTLS). /// /// The instance. [EditorBrowsable(EditorBrowsableState.Advanced)] - public OpenIddictServerBuilder EnableSelfSignedClientCertificateAuthentication() - => EnableSelfSignedClientCertificateAuthentication(static policy => { }); + public OpenIddictServerBuilder EnableSelfSignedTlsClientAuthentication() + => EnableSelfSignedTlsClientAuthentication(static policy => { }); /// - /// Configures OpenIddict to enable self-signed client certificate authentication (mTLS). + /// Configures OpenIddict to enable self-signed TLS client authentication (mTLS). /// /// The delegate used to amend the created X.509 chain policy. /// The instance. [EditorBrowsable(EditorBrowsableState.Advanced)] - public OpenIddictServerBuilder EnableSelfSignedClientCertificateAuthentication(Action configuration) + public OpenIddictServerBuilder EnableSelfSignedTlsClientAuthentication(Action configuration) { ArgumentNullException.ThrowIfNull(configuration); #if SUPPORTS_X509_CHAIN_POLICY_CUSTOM_TRUST_STORE var policy = new X509ChainPolicy { - // Note: by default, OpenIddict requires that end certificates used for authentication - // explicitly list client authentication as an allowed extended key usage. + // 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 diff --git a/src/OpenIddict.Server/OpenIddictServerConfiguration.cs b/src/OpenIddict.Server/OpenIddictServerConfiguration.cs index 58b504f4..438f96d5 100644 --- a/src/OpenIddict.Server/OpenIddictServerConfiguration.cs +++ b/src/OpenIddict.Server/OpenIddictServerConfiguration.cs @@ -167,13 +167,13 @@ public sealed class OpenIddictServerConfiguration : IPostConfigureOptions() + if (options.PublicKeyInfrastructureTlsClientAuthenticationPolicy.ExtraStore.Cast() .Any(static certificate => !OpenIddictHelpers.IsCertificateAuthority(certificate) || !OpenIddictHelpers.HasKeyUsage(certificate, X509KeyUsageFlags.KeyCertSign))) @@ -319,14 +322,14 @@ public sealed class OpenIddictServerConfiguration : IPostConfigureOptions() + if (options.PublicKeyInfrastructureTlsClientAuthenticationPolicy.ExtraStore.Cast() .Any(static certificate => certificate.HasPrivateKey)) { throw new InvalidOperationException(SR.GetResourceString(SR.ID0511)); } #if SUPPORTS_X509_CHAIN_POLICY_CUSTOM_TRUST_STORE && SUPPORTS_X509_CHAIN_POLICY_TRUST_MODE - if (options.ClientCertificateChainPolicy.CustomTrustStore.Cast() + if (options.PublicKeyInfrastructureTlsClientAuthenticationPolicy.CustomTrustStore.Cast() .Any(static certificate => !OpenIddictHelpers.IsCertificateAuthority(certificate) || !OpenIddictHelpers.HasKeyUsage(certificate, X509KeyUsageFlags.KeyCertSign))) @@ -334,7 +337,7 @@ public sealed class OpenIddictServerConfiguration : IPostConfigureOptions() + if (options.PublicKeyInfrastructureTlsClientAuthenticationPolicy.CustomTrustStore.Cast() .Any(static certificate => certificate.HasPrivateKey)) { throw new InvalidOperationException(SR.GetResourceString(SR.ID0511)); @@ -342,16 +345,16 @@ public sealed class OpenIddictServerConfiguration : IPostConfigureOptions().Any()) + if (options.SelfSignedTlsClientAuthenticationPolicy.ExtraStore.Cast().Any()) { throw new InvalidOperationException(SR.GetResourceString(SR.ID0502)); } #if SUPPORTS_X509_CHAIN_POLICY_CUSTOM_TRUST_STORE && SUPPORTS_X509_CHAIN_POLICY_TRUST_MODE - if (options.SelfSignedClientCertificateChainPolicy.CustomTrustStore.Cast().Any()) + if (options.SelfSignedTlsClientAuthenticationPolicy.CustomTrustStore.Cast().Any()) { throw new InvalidOperationException(SR.GetResourceString(SR.ID0502)); } @@ -365,7 +368,7 @@ public sealed class OpenIddictServerConfiguration : IPostConfigureOptions 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 (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 (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 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 (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 (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 (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 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)); diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.Discovery.cs b/src/OpenIddict.Server/OpenIddictServerEvents.Discovery.cs index 0222a8b0..74f9306f 100644 --- a/src/OpenIddict.Server/OpenIddictServerEvents.Discovery.cs +++ b/src/OpenIddict.Server/OpenIddictServerEvents.Discovery.cs @@ -161,6 +161,11 @@ public static partial class OpenIddictServerEvents /// public Uri? MtlsTokenEndpointAlias { get; set; } + /// + /// Gets or sets the mTLS userinfo endpoint alias URI. + /// + public Uri? MtlsUserInfoEndpointAlias { get; set; } + /// /// Gets the list of claims supported by the authorization server. /// @@ -247,6 +252,11 @@ public static partial class OpenIddictServerEvents /// Gets or sets a boolean indicating whether pushed authorization requests are required. /// public bool RequirePushedAuthorizationRequests { get; set; } + + /// + /// Gets or sets a boolean indicating whether access tokens are bound to client certificates. + /// + public bool TlsClientCertificateBoundAccessTokens { get; set; } } /// diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.Introspection.cs b/src/OpenIddict.Server/OpenIddictServerEvents.Introspection.cs index cf8705ae..8a54b800 100644 --- a/src/OpenIddict.Server/OpenIddictServerEvents.Introspection.cs +++ b/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 /// public string? ClientId { get; set; } + /// + /// Gets or sets the "cnf" claim returned to the caller, if applicable. + /// + public JsonObject? Confirmation { get; set; } + /// /// Gets or sets the "exp" claim /// returned to the caller, if applicable. diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.Protection.cs b/src/OpenIddict.Server/OpenIddictServerEvents.Protection.cs index d3b13801..0ef164cb 100644 --- a/src/OpenIddict.Server/OpenIddictServerEvents.Protection.cs +++ b/src/OpenIddict.Server/OpenIddictServerEvents.Protection.cs @@ -144,6 +144,11 @@ public static partial class OpenIddictServerEvents /// public bool DisablePresenterValidation { get; set; } + /// + /// Gets or sets a boolean indicating whether proof-of-possession validation is disabled. + /// + public bool DisableProofOfPossessionValidation { get; set; } + /// /// Gets or sets the security token handler used to validate the token. /// diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.cs b/src/OpenIddict.Server/OpenIddictServerEvents.cs index c0cdd265..350fe8b3 100644 --- a/src/OpenIddict.Server/OpenIddictServerEvents.cs +++ b/src/OpenIddict.Server/OpenIddictServerEvents.cs @@ -765,15 +765,6 @@ public static partial class OpenIddictServerEvents /// public ClaimsPrincipal? ClientAssertionPrincipal { get; set; } - /// - /// Gets or sets the client certificate (typically obtained via mTLS), if applicable. - /// - public X509Certificate2? ClientCertificate - { - get => Transaction.ClientCertificate; - set => Transaction.ClientCertificate = value; - } - /// /// Gets or sets the device code to validate, if applicable. /// diff --git a/src/OpenIddict.Server/OpenIddictServerExtensions.cs b/src/OpenIddict.Server/OpenIddictServerExtensions.cs index f6be36be..e85d82cb 100644 --- a/src/OpenIddict.Server/OpenIddictServerExtensions.cs +++ b/src/OpenIddict.Server/OpenIddictServerExtensions.cs @@ -91,6 +91,7 @@ public static class OpenIddictServerExtensions builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); diff --git a/src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs b/src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs index 18e31440..3a9b1500 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs @@ -180,7 +180,7 @@ public static class OpenIddictServerHandlerFilters } /// - /// 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. /// public sealed class RequireClientCertificate : IOpenIddictServerHandlerFilter { @@ -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 } } + /// + /// Represents a filter that excludes the associated handlers if token proof-of-possession validation was disabled. + /// + public sealed class RequireTokenProofOfPossessionValidationEnabled : IOpenIddictServerHandlerFilter + { + /// + public ValueTask IsActiveAsync(ValidateTokenContext context) + { + ArgumentNullException.ThrowIfNull(context); + + return new(!context.DisableProofOfPossessionValidation); + } + } + /// /// Represents a filter that excludes the associated handlers if the request is not a token request. /// diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs index c99467b4..38a2710d 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs @@ -252,7 +252,8 @@ public static partial class OpenIddictServerHandlers [Metadata.RevocationEndpointAuthMethodsSupported] = notification.RevocationEndpointAuthenticationMethods.ToImmutableArray(), [Metadata.DeviceAuthorizationEndpointAuthMethodsSupported] = notification.DeviceAuthorizationEndpointAuthenticationMethods.ToImmutableArray(), [Metadata.PushedAuthorizationRequestEndpointAuthMethodsSupported] = notification.PushedAuthorizationEndpointAuthenticationMethods.ToImmutableArray(), - [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, diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs index 361d4903..6180511f 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs +++ b/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; } diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs index 351ac1fc..7b704c49 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs +++ b/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 } } + /// + /// Contains the logic responsible for rejecting tokens for which no valid proof of possession was received. + /// + public sealed class ValidateProofOfPossession : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ValidateAudiences.Descriptor.Order + 1_000) + .SetType(OpenIddictServerHandlerType.BuiltIn) + .Build(); + + /// + 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(hash), + right: MemoryMarshal.AsBytes(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)); + } + } + /// /// 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() .AddFilter() .UseScopedHandler() - .SetOrder(ValidateExpirationDate.Descriptor.Order + 1_000) + .SetOrder(ValidateProofOfPossession.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.cs index 47c81773..d67b9bb1 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.cs +++ b/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 } /// - /// 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. /// public sealed class ValidateClientCertificate : IOpenIddictServerHandler { @@ -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; } diff --git a/src/OpenIddict.Server/OpenIddictServerOptions.cs b/src/OpenIddict.Server/OpenIddictServerOptions.cs index 768319e5..a8ab0037 100644 --- a/src/OpenIddict.Server/OpenIddictServerOptions.cs +++ b/src/OpenIddict.Server/OpenIddictServerOptions.cs @@ -178,6 +178,16 @@ public sealed class OpenIddictServerOptions /// public Uri? MtlsTokenEndpointAliasUri { get; set; } + /// + /// Gets or sets the URI listed as the mTLS token + /// endpoint alias in the server configuration metadata. + /// + /// + /// Note: this URI MUST be absolute and MUST point to a domain for + /// which TLS client authentication is enforced by the web server. + /// + public Uri? MtlsUserInfoEndpointAliasUri { get; set; } + /// /// Gets the token validation parameters used by the OpenIddict server services. /// @@ -647,6 +657,24 @@ public sealed class OpenIddictServerOptions /// public bool UseReferenceRefreshTokens { get; set; } + /// + /// 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. + /// + public bool UseClientCertificateBoundAccessTokens { get; set; } + + /// + /// 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. + /// + /// + /// 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. + /// + public bool UseClientCertificateBoundRefreshTokens { get; set; } + /// /// Gets or sets the time provider. /// @@ -658,40 +686,36 @@ public sealed class OpenIddictServerOptions public TimeProvider TimeProvider { get; set; } = default!; /// - /// 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. /// /// - /// - /// + /// /// Note: this instance serves as a base policy and is merged with /// the per-client policies resolved using the application manager. - /// - /// + /// + /// /// Note: while it is possible to use a policy configured to use the - /// the system certificates store, doing is so is strongly discouraged. - /// - /// + /// the system certificates store, doing so is strongly discouraged. + /// /// [EditorBrowsable(EditorBrowsableState.Advanced)] - public X509ChainPolicy? ClientCertificateChainPolicy { get; set; } + public X509ChainPolicy? PublicKeyInfrastructureTlsClientAuthenticationPolicy { get; set; } /// /// 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. /// /// - /// - /// + /// /// Note: this instance serves as a base policy and is merged with /// the per-client policies resolved using the application manager. - /// - /// + /// + /// /// Note: while it is possible to use a policy configured to use the - /// the system certificates store, doing is so is strongly discouraged. - /// - /// + /// the system certificates store, doing so is strongly discouraged. + /// /// [EditorBrowsable(EditorBrowsableState.Advanced)] - public X509ChainPolicy? SelfSignedClientCertificateChainPolicy { get; set; } + public X509ChainPolicy? SelfSignedTlsClientAuthenticationPolicy { get; set; } } diff --git a/src/OpenIddict.Server/OpenIddictServerTransaction.cs b/src/OpenIddict.Server/OpenIddictServerTransaction.cs index 795fbd9b..3720f112 100644 --- a/src/OpenIddict.Server/OpenIddictServerTransaction.cs +++ b/src/OpenIddict.Server/OpenIddictServerTransaction.cs @@ -28,9 +28,9 @@ public sealed class OpenIddictServerTransaction public CancellationToken CancellationToken { get; set; } /// - /// 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. /// - public X509Certificate2? ClientCertificate { get; set; } + public X509Certificate2? RemoteCertificate { get; set; } /// /// Gets or sets the type of the endpoint processing the current request. diff --git a/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandlers.cs b/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandlers.cs index 7689e37e..8546c2a1 100644 --- a/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandlers.cs +++ b/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 } } + /// + /// 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. + /// + public sealed class ExtractClientCertificate : IOpenIddictValidationHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictValidationHandlerDescriptor Descriptor { get; } + = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ExtractAccessTokenFromQueryString.Descriptor.Order + 1_000) + .SetType(OpenIddictValidationHandlerType.BuiltIn) + .Build(); + + /// + 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; + } + } + } + /// /// 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. diff --git a/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandlers.cs b/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandlers.cs index ff446be6..cdac9516 100644 --- a/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandlers.cs +++ b/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 } } + /// + /// 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. + /// + public sealed class ExtractClientCertificate : IOpenIddictValidationHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictValidationHandlerDescriptor Descriptor { get; } + = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ExtractAccessTokenFromQueryString.Descriptor.Order + 1_000) + .SetType(OpenIddictValidationHandlerType.BuiltIn) + .Build(); + + /// + 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 GetClientCertificateAsync(IOwinContext context) + { + // If a loading function was provided by the OWIN host, always invoke it before trying + // to resolve the certificate to ensure it is present in the environment dictionary. + if (context.Get>("ssl.LoadClientCertAsync") is Func loader) + { + await loader(); + } + + return context.Get("ssl.ClientCertificate") is X509Certificate certificate + ? certificate as X509Certificate2 ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0498)) + : null; + } + } + } + /// /// 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. diff --git a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpBuilder.cs b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpBuilder.cs index 54d2c423..6ac5a970 100644 --- a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpBuilder.cs +++ b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpBuilder.cs @@ -208,6 +208,7 @@ public sealed class OpenIddictValidationSystemNetHttpBuilder /// client authentication key usages to be automatically selected by OpenIddict). /// /// The instance. + [Obsolete("This option is no longer supported and will be removed in a future version.")] public OpenIddictValidationSystemNetHttpBuilder SetSelfSignedTlsClientAuthenticationCertificateSelector( Func selector) { @@ -229,6 +230,7 @@ public sealed class OpenIddictValidationSystemNetHttpBuilder /// client authentication key usages to be automatically selected by OpenIddict). /// /// The instance. + [Obsolete("This option is no longer supported and will be removed in a future version.")] public OpenIddictValidationSystemNetHttpBuilder SetTlsClientAuthenticationCertificateSelector( Func selector) { diff --git a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpConfiguration.cs b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpConfiguration.cs index ba7b1120..6bd88a21 100644 --- a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpConfiguration.cs +++ b/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>().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 }); } + /// + [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>() - .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>() - .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)); } diff --git a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpContext.cs b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpContext.cs new file mode 100644 index 00000000..a221f90e --- /dev/null +++ b/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; + +/// +/// Represents the context used by the System.Net.Http integration when creating a new HTTP client. +/// +[EditorBrowsable(EditorBrowsableState.Never)] +public sealed class OpenIddictValidationSystemNetHttpContext +{ + private static readonly AsyncLocal _current = new(); + + /// + /// Gets or sets the X.509 client certificate that will be used to authenticate + /// this peer when communicating with the external endpoint, if applicable. + /// + public X509Certificate2? LocalCertificate { get; init; } + + /// + /// Gets or sets the ambient context for the current execution flow. + /// + public static OpenIddictValidationSystemNetHttpContext? Current + { + get => _current.Value; + set => _current.Value = value; + } + + /// + /// Computes a stable, unique identifier for the specified context using a cryptographic hash. + /// + /// The client context for which to compute the stable identifier. + /// A string representing the stable identifier for the specified context. + 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)) + }; + } +} diff --git a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpExtensions.cs b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpExtensions.cs index 9ce061c2..7b203fa0 100644 --- a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpExtensions.cs +++ b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpExtensions.cs @@ -46,9 +46,6 @@ public static class OpenIddictValidationSystemNetHttpExtensions builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton< IPostConfigureOptions, OpenIddictValidationSystemNetHttpConfiguration>()); - builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton< - IPostConfigureOptions, OpenIddictValidationSystemNetHttpConfiguration>()); - return new OpenIddictValidationSystemNetHttpBuilder(builder.Services); } diff --git a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.cs b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.cs index 8a99c535..00b2bf0d 100644 --- a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.cs +++ b/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 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. /// + [Obsolete("This class is obsolete and will be removed in a future version.")] public sealed class AttachNonDefaultIntrospectionEndpointClientAuthenticationMethod : IOpenIddictValidationHandler { - private readonly IOptionsMonitor _options; - - public AttachNonDefaultIntrospectionEndpointClientAuthenticationMethod( - IOptionsMonitor options) - => _options = options ?? throw new ArgumentNullException(nameof(options)); - /// /// Gets the default descriptor definition assigned to this handler. /// @@ -56,94 +45,7 @@ public static partial class OpenIddictValidationSystemNetHttpHandlers .Build(); /// - 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; } /// @@ -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; } diff --git a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpOptions.cs b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpOptions.cs index 08bddfb8..33ea9b6b 100644 --- a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpOptions.cs +++ b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpOptions.cs @@ -95,6 +95,7 @@ public sealed class OpenIddictValidationSystemNetHttpOptions /// client authentication key usages to be automatically selected by OpenIddict). /// [EditorBrowsable(EditorBrowsableState.Advanced)] + [Obsolete("This option is no longer supported and will be removed in a future version.")] public Func SelfSignedTlsClientAuthenticationCertificateSelector { get; set; } = default!; /// @@ -109,5 +110,6 @@ public sealed class OpenIddictValidationSystemNetHttpOptions /// client authentication key usages to be automatically selected by OpenIddict). /// [EditorBrowsable(EditorBrowsableState.Advanced)] + [Obsolete("This option is no longer supported and will be removed in a future version.")] public Func TlsClientAuthenticationCertificateSelector { get; set; } = default!; } diff --git a/src/OpenIddict.Validation/OpenIddictValidationEvents.Protection.cs b/src/OpenIddict.Validation/OpenIddictValidationEvents.Protection.cs index 9fa9b72f..b04b2986 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationEvents.Protection.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationEvents.Protection.cs @@ -138,6 +138,11 @@ public static partial class OpenIddictValidationEvents /// public bool DisablePresenterValidation { get; set; } + /// + /// Gets or sets a boolean indicating whether proof-of-possession validation is disabled. + /// + public bool DisableProofOfPossessionValidation { get; set; } + /// /// Gets or sets the security token handler used to validate the token. /// diff --git a/src/OpenIddict.Validation/OpenIddictValidationEvents.cs b/src/OpenIddict.Validation/OpenIddictValidationEvents.cs index 51237323..eea07a41 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationEvents.cs +++ b/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. /// public string? ClientAuthenticationMethod { get; set; } + + /// + /// Gets or sets the X.509 client certificate that will be used to authenticate + /// this peer when communicating with the external endpoint, if applicable. + /// + public X509Certificate2? LocalCertificate { get; set; } } /// @@ -305,6 +312,12 @@ public static partial class OpenIddictValidationEvents /// public string? IntrospectionEndpointClientAuthenticationMethod { get; set; } + /// + /// Gets or sets the X.509 client certificate used when + /// communicating with the introspection endpoint, if applicable. + /// + public X509Certificate2? IntrospectionEndpointClientCertificate { get; set; } + /// /// Gets or sets a boolean indicating whether an introspection request should be sent. /// diff --git a/src/OpenIddict.Validation/OpenIddictValidationExtensions.cs b/src/OpenIddict.Validation/OpenIddictValidationExtensions.cs index 0b1528d0..2b022249 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationExtensions.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationExtensions.cs @@ -51,6 +51,7 @@ public static class OpenIddictValidationExtensions builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); // Note: TryAddEnumerable() is used here to ensure the initializer is registered only once. builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton< diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlerFilters.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlerFilters.cs index ff47dcf0..4257ab06 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationHandlerFilters.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationHandlerFilters.cs @@ -192,4 +192,18 @@ public static class OpenIddictValidationHandlerFilters return new(!context.DisablePresenterValidation); } } + + /// + /// Represents a filter that excludes the associated handlers if token proof-of-possession validation was disabled. + /// + public sealed class RequireTokenProofOfPossessionValidationEnabled : IOpenIddictValidationHandlerFilter + { + /// + public ValueTask IsActiveAsync(ValidateTokenContext context) + { + ArgumentNullException.ThrowIfNull(context); + + return new(!context.DisableProofOfPossessionValidation); + } + } } diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs index c7170f65..34313898 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs +++ b/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 }; diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs index 55d241bf..9edf40d6 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs +++ b/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 } } + /// + /// Contains the logic responsible for rejecting tokens for which no valid proof of possession was received. + /// + public sealed class ValidateProofOfPossession : IOpenIddictValidationHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictValidationHandlerDescriptor Descriptor { get; } + = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ValidateAudiences.Descriptor.Order + 1_000) + .SetType(OpenIddictValidationHandlerType.BuiltIn) + .Build(); + + /// + 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(hash), + right: MemoryMarshal.AsBytes(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)); + } + } + /// /// 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() .AddFilter() .UseScopedHandler() - .SetOrder(ValidateAudiences.Descriptor.Order + 1_000) + .SetOrder(ValidateProofOfPossession.Descriptor.Order + 1_000) .SetType(OpenIddictValidationHandlerType.BuiltIn) .Build(); diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs index 346f26e6..9aefdd4d 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs +++ b/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; + } + } + + /// + /// Contains the logic responsible for attaching the client certificate used for + /// the introspection endpoint to the authentication context, if applicable. + /// + public sealed class AttachIntrospectionEndpointClientCertificate : IOpenIddictValidationHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictValidationHandlerDescriptor Descriptor { get; } + = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(AttachIntrospectionEndpointClientAuthenticationMethod.Descriptor.Order + 1_000) + .SetType(OpenIddictValidationHandlerType.BuiltIn) + .Build(); + + /// + 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() .AddFilter() .UseSingletonHandler() - .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 } } + /// + /// Contains the logic responsible for validating the proof of possession + /// of the introspected token returned by the server, if applicable. + /// + public sealed class ValidateIntrospectedTokenProofOfPossession : IOpenIddictValidationHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictValidationHandlerDescriptor Descriptor { get; } + = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ValidateIntrospectedTokenAudiences.Descriptor.Order + 1_000) + .SetType(OpenIddictValidationHandlerType.BuiltIn) + .Build(); + + /// + 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(hash), + right: MemoryMarshal.AsBytes(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)); + } + } + /// /// Contains the logic responsible for ensuring a token was correctly resolved from the context. /// @@ -736,7 +959,7 @@ public static partial class OpenIddictValidationHandlers = OpenIddictValidationHandlerDescriptor.CreateBuilder() .AddFilter() .UseScopedHandler() - .SetOrder(ValidateIntrospectedTokenUsage.Descriptor.Order + 1_000) + .SetOrder(ValidateIntrospectedTokenProofOfPossession.Descriptor.Order + 1_000) .SetType(OpenIddictValidationHandlerType.BuiltIn) .Build(); diff --git a/src/OpenIddict.Validation/OpenIddictValidationService.cs b/src/OpenIddict.Validation/OpenIddictValidationService.cs index ed65d0ff..27077254 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationService.cs +++ b/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 /// The token request. /// The uri of the remote token endpoint. /// The client authentication method, if applicable. + /// The client certificate, if applicable. /// The that can be used to abort the operation. /// The response and the principal extracted from the introspection response. 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); diff --git a/src/OpenIddict.Validation/OpenIddictValidationTransaction.cs b/src/OpenIddict.Validation/OpenIddictValidationTransaction.cs index 2bdc6a7d..8d04dc98 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationTransaction.cs +++ b/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 /// public CancellationToken CancellationToken { get; set; } + /// + /// Gets or sets the X.509 client certificate used by the remote peer, if available. + /// + public X509Certificate2? RemoteCertificate { get; set; } + /// /// Gets or sets the type of the endpoint processing the current request. /// diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Discovery.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Discovery.cs index 1ceea847..a99af36e 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Discovery.cs +++ b/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)] diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Introspection.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Introspection.cs index de8480bf..2d67d10d 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Introspection.cs +++ b/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(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(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() { diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs index b6ef0a3e..9b3a4535 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs +++ b/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()), Times.Once()); } -#if SUPPORTS_X509_CHAIN_POLICY_CUSTOM_TRUST_STORE - [Theory] - [InlineData(OpenIddictServerEndpointType.DeviceAuthorization)] - [InlineData(OpenIddictServerEndpointType.Introspection)] - [InlineData(OpenIddictServerEndpointType.PushedAuthorization)] - [InlineData(OpenIddictServerEndpointType.Revocation)] - [InlineData(OpenIddictServerEndpointType.Token)] - public async Task ProcessAuthentication_ClientCertificateCannotBeUsedByPublicClients(OpenIddictServerEndpointType type) - { - // Arrange - var application = new OpenIddictApplication(); - - var manager = CreateApplicationManager(mock => - { - mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny())) - .ReturnsAsync(true); - }); - - await using var server = await CreateServerAsync(options => - { - options.AddEventHandler(builder => - { - builder.UseInlineHandler(context => - { - context.ClientCertificate = X509Certificate2.CreateFromPem($""" - -----BEGIN CERTIFICATE----- - MIIC8jCCAdqgAwIBAgIIYfcknj8KXN0wDQYJKoZIhvcNAQELBQAwIjEgMB4GA1UE - AxMXU2VsZi1zaWduZWQgY2VydGlmaWNhdGUwIBcNMjYwMjAxMTc1MjI2WhgPMjEy - NjAyMDExNzUyMjZaMCIxIDAeBgNVBAMTF1NlbGYtc2lnbmVkIGNlcnRpZmljYXRl - MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA45uKd5cdlLmEBGLDEB75 - o9e3/kMmQjVhhMeBsy4m2t7zw5jZo7OPcahXiXZHttom9tJm7BWPWvYx7p0N9+ss - h/E5lzKyV7ZXg+mM+KeECtVhiy+82BuIPelCshrpaV3lIg93y47FYLIWXxdggjt6 - 6VUaxzlTeo+IpuMz8IssL7VpJnjCT5NmqPNVkv1VR1uuetVqP7546ZFw31RiGl/0 - I1uUlb7SwLwhLUK1iyLmGNA3VDB0m0DvLmlIEY3ZE5zxQp/Rxq6DfjbXm2LWJyu6 - NO7k7JixXOorEl+6HdJZHTWNFK5jCo2ZZAwWn+uUuzgmLILPJFLDVutXjuEZpsym - uQIDAQABoyowKDAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0lAQH/BAwwCgYIKwYBBQUH - AwIwDQYJKoZIhvcNAQELBQADggEBAGHH3f/bkfViTvPE7yXJkB0bs88mYxajluMA - hgihEN5joPT6zHxMLBND2sitIozCMeeaj0rg+OaT/zDBgOLup/BM92UaPpYcgDCy - 3tHqZLOOJOR4aYnHhIQUnx+NRtKEM4q/hL/xLHeliKmV7TQXISEZlTbb0gOU7TFp - nJlP60Vo9F/WD6xcKNxBgV5aB/+2FjiTTw2pF0VUmvcZdQAN5ysfrmKNXbvv1oCp - AohiwRiPrwe3mJ8iCqzEY/qQqImEiIT8WC2Fty+UYyBwfXMObi1AO++QkaMUbUJl - 0aBuRoD85FLotjHIHXkFHERjOolheYdKt5nrGCCz/PmXBfsSCTo= - -----END CERTIFICATE----- - """); - - return ValueTask.CompletedTask; - }); - - builder.SetOrder(ValidateClientType.Descriptor.Order - 500); - }); - - options.Services.AddSingleton(manager); - }); - - await using var client = await server.CreateClientAsync(); - - // Act - var response = type switch - { - OpenIddictServerEndpointType.Introspection => await client.PostAsync("/connect/introspect", new OpenIddictRequest - { - ClientId = "Fabrikam", - Token = "2YotnFZFEjr1zCsicMWpAA" - }), - - OpenIddictServerEndpointType.DeviceAuthorization => await client.PostAsync("/connect/device", new OpenIddictRequest - { - ClientId = "Fabrikam" - }), - - OpenIddictServerEndpointType.PushedAuthorization => await client.PostAsync("/connect/par", new OpenIddictRequest - { - ClientId = "Fabrikam", - RedirectUri = "http://www.fabrikam.com/path", - ResponseType = ResponseTypes.Code - }), - - OpenIddictServerEndpointType.Revocation => await client.PostAsync("/connect/revoke", new OpenIddictRequest - { - ClientId = "Fabrikam", - Token = "SlAV32hkKG", - TokenTypeHint = TokenTypeHints.RefreshToken - }), - - OpenIddictServerEndpointType.Token => await client.PostAsync("/connect/token", new OpenIddictRequest - { - ClientId = "Fabrikam", - GrantType = GrantTypes.Password, - Username = "johndoe", - Password = "A3ddj3w" - }), - - _ => throw new NotSupportedException() - }; - - // Assert - Assert.Equal(Errors.InvalidClient, response.Error); - Assert.Equal(SR.GetResourceString(SR.ID2196), response.ErrorDescription); - Assert.Equal(SR.FormatID8000(SR.ID2196), response.ErrorUri); - - Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce()); - Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()), Times.Once()); - } -#endif - - [Theory] - [InlineData(OpenIddictServerEndpointType.DeviceAuthorization)] - [InlineData(OpenIddictServerEndpointType.Introspection)] - [InlineData(OpenIddictServerEndpointType.PushedAuthorization)] - [InlineData(OpenIddictServerEndpointType.Revocation)] - [InlineData(OpenIddictServerEndpointType.Token)] - public async Task ProcessAuthentication_ClientAuthenticationIsRequiredForNonPublicClients(OpenIddictServerEndpointType type) - { - // Arrange - var application = new OpenIddictApplication(); - - var manager = CreateApplicationManager(mock => - { - mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny())) - .ReturnsAsync(false); - }); - - await using var server = await CreateServerAsync(options => - { - options.Services.AddSingleton(manager); - }); - - await using var client = await server.CreateClientAsync(); - - // Act - var response = type switch - { - OpenIddictServerEndpointType.Introspection => await client.PostAsync("/connect/introspect", new OpenIddictRequest - { - ClientAssertion = null, - ClientAssertionType = null, - ClientId = "Fabrikam", - ClientSecret = null, - Token = "2YotnFZFEjr1zCsicMWpAA" - }), - - OpenIddictServerEndpointType.DeviceAuthorization => await client.PostAsync("/connect/device", new OpenIddictRequest - { - ClientAssertion = null, - ClientAssertionType = null, - ClientId = "Fabrikam", - ClientSecret = null - }), - - OpenIddictServerEndpointType.PushedAuthorization => await client.PostAsync("/connect/par", new OpenIddictRequest - { - ClientAssertion = null, - ClientAssertionType = null, - ClientId = "Fabrikam", - ClientSecret = null, - RedirectUri = "http://www.fabrikam.com/path", - ResponseType = ResponseTypes.Code - }), - - OpenIddictServerEndpointType.Revocation => await client.PostAsync("/connect/revoke", new OpenIddictRequest - { - ClientAssertion = null, - ClientAssertionType = null, - ClientId = "Fabrikam", - ClientSecret = null, - Token = "SlAV32hkKG", - TokenTypeHint = TokenTypeHints.RefreshToken - }), - - OpenIddictServerEndpointType.Token => await client.PostAsync("/connect/token", new OpenIddictRequest - { - ClientAssertion = null, - ClientAssertionType = null, - ClientId = "Fabrikam", - ClientSecret = null, - GrantType = GrantTypes.Password, - Username = "johndoe", - Password = "A3ddj3w" - }), - - _ => throw new NotSupportedException() - }; - - // Assert - Assert.Equal(Errors.InvalidClient, response.Error); - Assert.Equal(SR.GetResourceString(SR.ID2198), response.ErrorDescription); - Assert.Equal(SR.FormatID8000(SR.ID2198), response.ErrorUri); - - Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce()); - Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()), Times.Once()); - } - [Theory] [InlineData(OpenIddictServerEndpointType.Introspection)] [InlineData(OpenIddictServerEndpointType.PushedAuthorization)] @@ -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())) - .ReturnsAsync(application); - - mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny())) - .ReturnsAsync(false); - }); - - await using var server = await CreateServerAsync(options => - { - options.AddEventHandler(builder => - { - builder.UseInlineHandler(context => - { - context.ClientCertificate = X509Certificate2.CreateFromPem($""" - -----BEGIN CERTIFICATE----- - MIIC8jCCAdqgAwIBAgIIYfcknj8KXN0wDQYJKoZIhvcNAQELBQAwIjEgMB4GA1UE - AxMXU2VsZi1zaWduZWQgY2VydGlmaWNhdGUwIBcNMjYwMjAxMTc1MjI2WhgPMjEy - NjAyMDExNzUyMjZaMCIxIDAeBgNVBAMTF1NlbGYtc2lnbmVkIGNlcnRpZmljYXRl - MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA45uKd5cdlLmEBGLDEB75 - o9e3/kMmQjVhhMeBsy4m2t7zw5jZo7OPcahXiXZHttom9tJm7BWPWvYx7p0N9+ss - h/E5lzKyV7ZXg+mM+KeECtVhiy+82BuIPelCshrpaV3lIg93y47FYLIWXxdggjt6 - 6VUaxzlTeo+IpuMz8IssL7VpJnjCT5NmqPNVkv1VR1uuetVqP7546ZFw31RiGl/0 - I1uUlb7SwLwhLUK1iyLmGNA3VDB0m0DvLmlIEY3ZE5zxQp/Rxq6DfjbXm2LWJyu6 - NO7k7JixXOorEl+6HdJZHTWNFK5jCo2ZZAwWn+uUuzgmLILPJFLDVutXjuEZpsym - uQIDAQABoyowKDAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0lAQH/BAwwCgYIKwYBBQUH - AwIwDQYJKoZIhvcNAQELBQADggEBAGHH3f/bkfViTvPE7yXJkB0bs88mYxajluMA - hgihEN5joPT6zHxMLBND2sitIozCMeeaj0rg+OaT/zDBgOLup/BM92UaPpYcgDCy - 3tHqZLOOJOR4aYnHhIQUnx+NRtKEM4q/hL/xLHeliKmV7TQXISEZlTbb0gOU7TFp - nJlP60Vo9F/WD6xcKNxBgV5aB/+2FjiTTw2pF0VUmvcZdQAN5ysfrmKNXbvv1oCp - AohiwRiPrwe3mJ8iCqzEY/qQqImEiIT8WC2Fty+UYyBwfXMObi1AO++QkaMUbUJl - 0aBuRoD85FLotjHIHXkFHERjOolheYdKt5nrGCCz/PmXBfsSCTo= - -----END CERTIFICATE----- - """); - - return ValueTask.CompletedTask; - }); - - builder.SetOrder(ValidateClientType.Descriptor.Order - 500); - }); - - options.Services.AddSingleton(manager); - }); - - await using var client = await server.CreateClientAsync(); - - // Act and assert - var exception = type switch - { - OpenIddictServerEndpointType.Introspection => - await Assert.ThrowsAsync(() => client.PostAsync("/connect/introspect", new OpenIddictRequest - { - ClientId = "Fabrikam", - Token = "2YotnFZFEjr1zCsicMWpAA" - })), - - OpenIddictServerEndpointType.DeviceAuthorization => - await Assert.ThrowsAsync(() => client.PostAsync("/connect/device", new OpenIddictRequest - { - ClientId = "Fabrikam" - })), - - OpenIddictServerEndpointType.PushedAuthorization => - await Assert.ThrowsAsync(() => client.PostAsync("/connect/par", new OpenIddictRequest - { - ClientId = "Fabrikam", - RedirectUri = "http://www.fabrikam.com/path", - ResponseType = ResponseTypes.Code - })), - - OpenIddictServerEndpointType.Revocation => - await Assert.ThrowsAsync(() => client.PostAsync("/connect/revoke", new OpenIddictRequest - { - ClientId = "Fabrikam", - Token = "SlAV32hkKG", - TokenTypeHint = TokenTypeHints.RefreshToken - })), - - OpenIddictServerEndpointType.Token => - await Assert.ThrowsAsync(() => client.PostAsync("/connect/token", new OpenIddictRequest - { - ClientId = "Fabrikam", - GrantType = GrantTypes.Password, - Username = "johndoe", - Password = "A3ddj3w" - })), - - _ => throw new NotSupportedException() - }; - - Assert.Equal(SR.GetResourceString(SR.ID0506), exception.Message); - - Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce()); - Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()), Times.AtLeastOnce()); - } - [Theory] [InlineData(OpenIddictServerEndpointType.DeviceAuthorization)] [InlineData(OpenIddictServerEndpointType.Introspection)] @@ -1424,19 +1118,20 @@ public abstract partial class OpenIddictServerIntegrationTests mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny())) .ReturnsAsync(false); - mock.Setup(manager => manager.GetSelfSignedClientCertificateChainPolicyAsync(application, It.IsAny(), It.IsAny())) + mock.Setup(manager => manager.GetSelfSignedTlsClientAuthenticationPolicyAsync(application, It.IsAny(), It.IsAny())) .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(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()), Times.AtLeastOnce()); Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()), Times.AtLeastOnce()); - Mock.Get(manager).Verify(manager => manager.GetSelfSignedClientCertificateChainPolicyAsync(application, It.IsAny(), It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(manager => manager.GetSelfSignedTlsClientAuthenticationPolicyAsync(application, + It.IsAny(), It.IsAny()), Times.Once()); } [Theory] @@ -1557,22 +1253,23 @@ public abstract partial class OpenIddictServerIntegrationTests mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny())) .ReturnsAsync(false); - mock.Setup(manager => manager.GetSelfSignedClientCertificateChainPolicyAsync(application, It.IsAny(), It.IsAny())) + mock.Setup(manager => manager.GetSelfSignedTlsClientAuthenticationPolicyAsync(application, It.IsAny(), It.IsAny())) .ReturnsAsync(policy); - mock.Setup(manager => manager.ValidateSelfSignedClientCertificateAsync(application, certificate, policy, It.IsAny())) + mock.Setup(manager => manager.ValidateSelfSignedTlsClientCertificateAsync(application, certificate, policy, It.IsAny())) .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(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()), Times.AtLeastOnce()); Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()), Times.AtLeastOnce()); - Mock.Get(manager).Verify(manager => manager.GetSelfSignedClientCertificateChainPolicyAsync(application, It.IsAny(), It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(manager => manager.ValidateSelfSignedClientCertificateAsync(application, certificate, policy, It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(manager => manager.GetSelfSignedTlsClientAuthenticationPolicyAsync(application, It.IsAny(), It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(manager => manager.ValidateSelfSignedTlsClientCertificateAsync(application, certificate, + It.Is(policy => policy.CustomTrustStore.Count == 0 && policy.ExtraStore.Count == 0), It.IsAny()), 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())) + .ReturnsAsync(true); + + mock.Setup(manager => manager.GetSelfSignedTlsClientAuthenticationPolicyAsync(application, It.IsAny(), It.IsAny())) + .ReturnsAsync(policy); + + mock.Setup(manager => manager.ValidateSelfSignedTlsClientCertificateAsync(application, certificate, policy, It.IsAny())) .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(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(() => 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(() => client.PostAsync("/connect/device", new OpenIddictRequest - { - ClientId = "Fabrikam" - })), + OpenIddictServerEndpointType.DeviceAuthorization => await client.PostAsync("/connect/device", new OpenIddictRequest + { + ClientId = "Fabrikam" + }), - OpenIddictServerEndpointType.PushedAuthorization => - await Assert.ThrowsAsync(() => client.PostAsync("/connect/par", new OpenIddictRequest - { - ClientId = "Fabrikam", - RedirectUri = "http://www.fabrikam.com/path", - ResponseType = ResponseTypes.Code - })), + OpenIddictServerEndpointType.PushedAuthorization => await client.PostAsync("/connect/par", new OpenIddictRequest + { + ClientId = "Fabrikam", + RedirectUri = "http://www.fabrikam.com/path", + ResponseType = ResponseTypes.Code + }), - OpenIddictServerEndpointType.Revocation => - await Assert.ThrowsAsync(() => client.PostAsync("/connect/revoke", new OpenIddictRequest - { - ClientId = "Fabrikam", - Token = "SlAV32hkKG", - TokenTypeHint = TokenTypeHints.RefreshToken - })), + OpenIddictServerEndpointType.Revocation => await client.PostAsync("/connect/revoke", new OpenIddictRequest + { + ClientId = "Fabrikam", + Token = "SlAV32hkKG", + TokenTypeHint = TokenTypeHints.RefreshToken + }), - OpenIddictServerEndpointType.Token => - await Assert.ThrowsAsync(() => client.PostAsync("/connect/token", new OpenIddictRequest - { - ClientId = "Fabrikam", - GrantType = GrantTypes.Password, - Username = "johndoe", - Password = "A3ddj3w" - })), + 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()), Times.AtLeastOnce()); Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()), Times.AtLeastOnce()); + Mock.Get(manager).Verify(manager => manager.GetSelfSignedTlsClientAuthenticationPolicyAsync(application, It.IsAny(), It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(manager => manager.ValidateSelfSignedTlsClientCertificateAsync(application, certificate, + It.Is(policy => policy.CustomTrustStore.Contains(certificate)), It.IsAny()), Times.Once()); } [Theory] @@ -1770,19 +1472,20 @@ public abstract partial class OpenIddictServerIntegrationTests mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny())) .ReturnsAsync(false); - mock.Setup(manager => manager.GetClientCertificateChainPolicyAsync(application, It.IsAny(), It.IsAny())) + mock.Setup(manager => manager.GetPublicKeyInfrastructureTlsClientAuthenticationPolicyAsync(application, It.IsAny(), It.IsAny())) .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(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()), Times.AtLeastOnce()); Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()), Times.AtLeastOnce()); - Mock.Get(manager).Verify(manager => manager.GetClientCertificateChainPolicyAsync(application, It.IsAny(), It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(manager => manager.GetPublicKeyInfrastructureTlsClientAuthenticationPolicyAsync(application, + It.IsAny(), It.IsAny()), Times.Once()); } [Theory] @@ -1919,22 +1623,23 @@ public abstract partial class OpenIddictServerIntegrationTests mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny())) .ReturnsAsync(false); - mock.Setup(manager => manager.GetClientCertificateChainPolicyAsync(application, It.IsAny(), It.IsAny())) + mock.Setup(manager => manager.GetPublicKeyInfrastructureTlsClientAuthenticationPolicyAsync(application, It.IsAny(), It.IsAny())) .ReturnsAsync(policy); - mock.Setup(manager => manager.ValidateClientCertificateAsync(application, certificate, policy, It.IsAny())) + mock.Setup(manager => manager.ValidatePublicKeyInfrastructureTlsClientCertificateAsync(application, certificate, policy, It.IsAny())) .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(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()), Times.AtLeastOnce()); Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()), Times.AtLeastOnce()); - Mock.Get(manager).Verify(manager => manager.GetClientCertificateChainPolicyAsync(application, It.IsAny(), It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(manager => manager.ValidateClientCertificateAsync(application, certificate, policy, It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(manager => manager.GetPublicKeyInfrastructureTlsClientAuthenticationPolicyAsync(application, It.IsAny(), It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(manager => manager.ValidatePublicKeyInfrastructureTlsClientCertificateAsync(application, + certificate, policy, It.IsAny()), Times.Once()); } #endif + [Theory] + [InlineData(OpenIddictServerEndpointType.DeviceAuthorization)] + [InlineData(OpenIddictServerEndpointType.Introspection)] + [InlineData(OpenIddictServerEndpointType.PushedAuthorization)] + [InlineData(OpenIddictServerEndpointType.Revocation)] + [InlineData(OpenIddictServerEndpointType.Token)] + public async Task ProcessAuthentication_ClientAuthenticationIsRequiredForNonPublicClients(OpenIddictServerEndpointType type) + { + // Arrange + var application = new OpenIddictApplication(); + + var manager = CreateApplicationManager(mock => + { + mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny())) + .ReturnsAsync(application); + + mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny())) + .ReturnsAsync(false); + }); + + await using var server = await CreateServerAsync(options => + { + options.Services.AddSingleton(manager); + }); + + await using var client = await server.CreateClientAsync(); + + // Act + var response = type switch + { + OpenIddictServerEndpointType.Introspection => await client.PostAsync("/connect/introspect", new OpenIddictRequest + { + ClientAssertion = null, + ClientAssertionType = null, + ClientId = "Fabrikam", + ClientSecret = null, + Token = "2YotnFZFEjr1zCsicMWpAA" + }), + + OpenIddictServerEndpointType.DeviceAuthorization => await client.PostAsync("/connect/device", new OpenIddictRequest + { + ClientAssertion = null, + ClientAssertionType = null, + ClientId = "Fabrikam", + ClientSecret = null + }), + + OpenIddictServerEndpointType.PushedAuthorization => await client.PostAsync("/connect/par", new OpenIddictRequest + { + ClientAssertion = null, + ClientAssertionType = null, + ClientId = "Fabrikam", + ClientSecret = null, + RedirectUri = "http://www.fabrikam.com/path", + ResponseType = ResponseTypes.Code + }), + + OpenIddictServerEndpointType.Revocation => await client.PostAsync("/connect/revoke", new OpenIddictRequest + { + ClientAssertion = null, + ClientAssertionType = null, + ClientId = "Fabrikam", + ClientSecret = null, + Token = "SlAV32hkKG", + TokenTypeHint = TokenTypeHints.RefreshToken + }), + + OpenIddictServerEndpointType.Token => await client.PostAsync("/connect/token", new OpenIddictRequest + { + ClientAssertion = null, + ClientAssertionType = null, + ClientId = "Fabrikam", + ClientSecret = null, + GrantType = GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w" + }), + + _ => throw new NotSupportedException() + }; + + // Assert + Assert.Equal(Errors.InvalidClient, response.Error); + Assert.Equal(SR.GetResourceString(SR.ID2198), response.ErrorDescription); + Assert.Equal(SR.FormatID8000(SR.ID2198), response.ErrorUri); + + Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce()); + Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()), Times.Once()); + } + [Fact] public async Task ProcessAuthentication_RequestTokenPrincipalIsNotPopulatedWhenRequestTokenIsMissing() { diff --git a/test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs b/test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs index 6000100c..36491060 100644 --- a/test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs +++ b/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(() => - 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(() => - 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(() => - 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(), + Assert.NotNull(options.PublicKeyInfrastructureTlsClientAuthenticationPolicy); + Assert.Equal(X509ChainTrustMode.CustomRootTrust, options.PublicKeyInfrastructureTlsClientAuthenticationPolicy.TrustMode); + Assert.Contains(options.PublicKeyInfrastructureTlsClientAuthenticationPolicy.ApplicationPolicy.Cast(), 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(() => - 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(() => - 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(), + 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.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(() => - 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(() => 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();