diff --git a/CHANGELOG.md b/CHANGELOG.md index a6ef83d6..83caf152 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Stop adding ?schema=openid to userinfo endpoint URL. #449 +- Add support for EdDSA signed JWTs ## [1.0.1] - 2024-09-13 diff --git a/src/OpenIDConnectClient.php b/src/OpenIDConnectClient.php index 7b859c56..114dfd11 100644 --- a/src/OpenIDConnectClient.php +++ b/src/OpenIDConnectClient.php @@ -173,7 +173,7 @@ class OpenIDConnectClient /** * @var mixed holds well-known openid configuration parameters, like policy for MS Azure AD B2C User Flow - * @see https://docs.microsoft.com/en-us/azure/active-directory-b2c/user-flow-overview + * @see https://docs.microsoft.com/en-us/azure/active-directory-b2c/user-flow-overview */ private $wellKnownConfigParameters = []; @@ -686,7 +686,7 @@ public function getRedirectURL(): string } else { $protocol = 'http'; } - + if (isset($_SERVER['HTTP_X_FORWARDED_PORT'])) { $port = (int)$_SERVER['HTTP_X_FORWARDED_PORT']; } elseif (isset($_SERVER['SERVER_PORT'])) { @@ -709,7 +709,7 @@ public function getRedirectURL(): string } $port = (443 === $port) || (80 === $port) ? '' : ':' . $port; - + $explodedRequestUri = isset($_SERVER['REQUEST_URI']) ? explode('?', $_SERVER['REQUEST_URI']) : []; return sprintf('%s://%s%s/%s', $protocol, $host, $port, trim(reset($explodedRequestUri), '/')); } @@ -1087,6 +1087,18 @@ private function verifyRSAJWTSignature(string $hashType, stdClass $key, $payload return $key->verify($payload, $signature); } + private function verifyEdDSAJWTsignature($key, $payload, $signature) { + if (!(property_exists($key, 'x'))) { + throw new OpenIDConnectClientException('Malformed key object'); + } + + if (!function_exists("sodium_crypto_sign_verify_detached")) { + throw new OpenIDConnectClientException('sodium_crypto_sign_verify_detached support unavailable.'); + } + + return sodium_crypto_sign_verify_detached($signature, $payload, base64url_decode($key->x)); + } + private function verifyHMACJWTSignature(string $hashType, string $key, string $payload, string $signature): bool { $expected = hash_hmac($hashType, $payload, $key, true); @@ -1145,6 +1157,24 @@ public function verifyJWTSignature(string $jwt): bool $jwk, $payload, $signature, $signatureType); break; + case 'EdDSA': + if (isset($header->jwk)) { + $jwk = $header->jwk; + $this->verifyJWKHeader($jwk); + } else { + $jwks = json_decode($this->fetchURL($this->getProviderConfigValue('jwks_uri'))); + if ($jwks === NULL) { + throw new OpenIDConnectClientException('Error decoding JSON from jwks_uri'); + } + $jwk = $this->getKeyForHeader($jwks->keys, $header); + } + + $verified = $this->verifyEdDSAJWTsignature( + $jwk, + $payload, + $signature + ); + break; case 'HS256': case 'HS512': case 'HS384': @@ -1192,12 +1222,19 @@ protected function validateIssuer(string $iss): bool protected function verifyJWTClaims($claims, string $accessToken = null): bool { if(isset($claims->at_hash, $accessToken)) { - if(isset($this->getIdTokenHeader()->alg) && $this->getIdTokenHeader()->alg !== 'none') { - $bit = substr($this->getIdTokenHeader()->alg, 2, 3); - } else { - // TODO: Error case. throw exception??? - $bit = '256'; + switch($this->getIdTokenHeader()->alg ?? '') { + case 'EdDSA': + $bit = '512'; + break; + case 'none': + case '': + // TODO: Error case. throw exception??? + $bit = '256'; + break; + default: + $bit = substr($this->getIdTokenHeader()->alg, 2, 3); } + $len = ((int)$bit)/16; $expected_at_hash = $this->urlEncode(substr(hash('sha'.$bit, $accessToken, true), 0, $len)); } diff --git a/tests/TokenVerificationTest.php b/tests/TokenVerificationTest.php index 0715911e..85d3871b 100644 --- a/tests/TokenVerificationTest.php +++ b/tests/TokenVerificationTest.php @@ -29,7 +29,9 @@ public function testTokenVerification($alg, $jwt) public function providesTokens(): array { return [ - 'PS256' => ['ps256', 'eyJhbGciOiJQUzI1NiIsImtpZCI6Imtvbm5lY3RkLXRva2Vucy1zaWduaW5nLWtleSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJrcG9wLWh0dHBzOi8va29wYW5vLmRlbW8vbWVldC8iLCJleHAiOjE1NjgzNzE0NjEsImp0aSI6IkpkR0tDbEdOTXl2VXJpcmlRRUlWUXZCVmttT2FfQkRjIiwiaWF0IjoxNTY4MzcxMjIxLCJpc3MiOiJodHRwczovL2tvcGFuby5kZW1vIiwic3ViIjoiUHpUVWp3NHBlXzctWE5rWlBILXJxVHE0MTQ1Z3lDdlRvQmk4V1E5bFBrcW5rbEc1aktvRU5LM21Qb0I1WGY1ZTM5dFRMR2RKWXBMNEJubXFnelpaX0FAa29ubmVjdCIsImtjLmlzQWNjZXNzVG9rZW4iOnRydWUsImtjLmF1dGhvcml6ZWRTY29wZXMiOlsicHJvZmlsZSIsImVtYWlsIiwia29wYW5vL2t3bSIsImtvcGFuby9nYyIsImtvcGFuby9rdnMiLCJvcGVuaWQiXSwia2MuYXV0aG9yaXplZENsYWltcyI6eyJpZF90b2tlbiI6eyJuYW1lIjpudWxsfX0sImtjLmlkZW50aXR5Ijp7ImtjLmkuZG4iOiJKb25hcyBCcmVra2UiLCJrYy5pLmlkIjoiQUFBQUFLd2hxVkJBMCs1SXN4bjdwMU13UkNVQkFBQUFCZ0FBQUJzQUFBQk5VVDA5QUFBQUFBPT0iLCJrYy5pLnVuIjoidXNlcjEiLCJrYy5pLnVzIjoiTVEifSwia2MucHJvdmlkZXIiOiJpZGVudGlmaWVyLWtjIn0.hGRuXvul2kOiALHexwYp5MBEJVwz1YV3ehyM3AOuwCoK2w5sJxdciqqY_TfXCKyO6nAEbYLK3J0CBOjfup_IG0aCZcwzjto8khYlc4ezXkGnFsbJBNQdDGkpHtWnioWx-OJ3cXvY9F8aOvjaq0gw11ZDAcqQl0g7LTbJ9-J_yx0pmy3NGai2JB30Fh1OgSDzYfxWnE0RRgZG-x68e65RXfSBaEGW85OUh4wihxO2zdTGAHJ3Iq_-QAG4yRbXZtLx3ZspG7LNmqG-YE3huy3Rd8u3xrJNhmUOfEnz3x07q7VW0cj9NedX98BAbj3iNvksQsE0oG0J_f_Tu8Ai8VbWB72sJuXZWxANDKdz0BBYLzXhsjXkNByRq9x3zqDVsX-cVHei_XudxEOVRBjhkvW2MmIjcAHNKCKsdar865-gFG9McP4PCcBlY28tC0Cvnzyi83LBfpGRXdl6MJunnUsKQ1C79iCoVI1doK1erFN959Q-TGJfJA3Tr5LNpuGawB5rpe1nDGWvmYhg3uYfNl8uTTyvNgvvejcflEb2DURuXdqABuSiP7RkDWYtzx6mq49G0tRxelBbvyjQ2id2QjmRRdQ6dHEZ2NCJ51b8OFoDJBtxN1CD62TTxa3FUqCdZAPAUR3hHn_69vYq82MR514s-Gb67A6j2PbMPFATQP2UdK8'] + 'PS256' => ['ps256', 'eyJhbGciOiJQUzI1NiIsImtpZCI6Imtvbm5lY3RkLXRva2Vucy1zaWduaW5nLWtleSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJrcG9wLWh0dHBzOi8va29wYW5vLmRlbW8vbWVldC8iLCJleHAiOjE1NjgzNzE0NjEsImp0aSI6IkpkR0tDbEdOTXl2VXJpcmlRRUlWUXZCVmttT2FfQkRjIiwiaWF0IjoxNTY4MzcxMjIxLCJpc3MiOiJodHRwczovL2tvcGFuby5kZW1vIiwic3ViIjoiUHpUVWp3NHBlXzctWE5rWlBILXJxVHE0MTQ1Z3lDdlRvQmk4V1E5bFBrcW5rbEc1aktvRU5LM21Qb0I1WGY1ZTM5dFRMR2RKWXBMNEJubXFnelpaX0FAa29ubmVjdCIsImtjLmlzQWNjZXNzVG9rZW4iOnRydWUsImtjLmF1dGhvcml6ZWRTY29wZXMiOlsicHJvZmlsZSIsImVtYWlsIiwia29wYW5vL2t3bSIsImtvcGFuby9nYyIsImtvcGFuby9rdnMiLCJvcGVuaWQiXSwia2MuYXV0aG9yaXplZENsYWltcyI6eyJpZF90b2tlbiI6eyJuYW1lIjpudWxsfX0sImtjLmlkZW50aXR5Ijp7ImtjLmkuZG4iOiJKb25hcyBCcmVra2UiLCJrYy5pLmlkIjoiQUFBQUFLd2hxVkJBMCs1SXN4bjdwMU13UkNVQkFBQUFCZ0FBQUJzQUFBQk5VVDA5QUFBQUFBPT0iLCJrYy5pLnVuIjoidXNlcjEiLCJrYy5pLnVzIjoiTVEifSwia2MucHJvdmlkZXIiOiJpZGVudGlmaWVyLWtjIn0.hGRuXvul2kOiALHexwYp5MBEJVwz1YV3ehyM3AOuwCoK2w5sJxdciqqY_TfXCKyO6nAEbYLK3J0CBOjfup_IG0aCZcwzjto8khYlc4ezXkGnFsbJBNQdDGkpHtWnioWx-OJ3cXvY9F8aOvjaq0gw11ZDAcqQl0g7LTbJ9-J_yx0pmy3NGai2JB30Fh1OgSDzYfxWnE0RRgZG-x68e65RXfSBaEGW85OUh4wihxO2zdTGAHJ3Iq_-QAG4yRbXZtLx3ZspG7LNmqG-YE3huy3Rd8u3xrJNhmUOfEnz3x07q7VW0cj9NedX98BAbj3iNvksQsE0oG0J_f_Tu8Ai8VbWB72sJuXZWxANDKdz0BBYLzXhsjXkNByRq9x3zqDVsX-cVHei_XudxEOVRBjhkvW2MmIjcAHNKCKsdar865-gFG9McP4PCcBlY28tC0Cvnzyi83LBfpGRXdl6MJunnUsKQ1C79iCoVI1doK1erFN959Q-TGJfJA3Tr5LNpuGawB5rpe1nDGWvmYhg3uYfNl8uTTyvNgvvejcflEb2DURuXdqABuSiP7RkDWYtzx6mq49G0tRxelBbvyjQ2id2QjmRRdQ6dHEZ2NCJ51b8OFoDJBtxN1CD62TTxa3FUqCdZAPAUR3hHn_69vYq82MR514s-Gb67A6j2PbMPFATQP2UdK8'], + 'RS512' => ['rs512', 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiIsImtpZCI6IjNiNGI3ZmNiYWM4MTAwZmU1Mjg5MTI3NzY0MTcwMDlhIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.rBovVXkUymQRHeTolWO07nJyw2NJgho8JbyjPAbZ2VAcQKvrjL8SNrkIkdjuI4FDfJQvu_NOlsKu0LhGNUJATxQhGvFqWOfF9nggFtl7ZTpAu3E6Xm1s-VGSy9LOQvmBiFvXDQJ7bd0xn0Ld1XO2lIVLHItoPr6Gw1m0_vdtUlMrX_dF4ZrJCBaQWXw114zgwH4WIZ8nvvDRdw3n1FLvPrYFZzRBI0Z8wXkBVEnw_kxlQWi7waOp-5NwZFYF5Tei1KUDQMSUcxAckNh01it8UdHoQf4HhgRF_GeDi9HJRVPUCO4N1wVtKRVqDMKRvQxZCn-_ohsUHA2u1-CUakbsd1EDkP8SaPFtvtW0QKB7K3KWQVSHUh7Kp6cct4scbDCGzXPwrgGyKF9V3d1g4fed6epkFlFnif0ZM9JvMSp7ult40HdC7D-9YCdJ39d5T2RGVOeQEKEk0UqxanG-dbp2RmjMYms70h75XatR7Bfbt1bsDB0dwnEwbwFLps1H_dVS'], + 'EdDSA' => ['eddsa', 'eyJraWQiOiItMTkwOTU3MjI1NyIsImFsZyI6IkVkRFNBIn0.eyJqdGkiOiIyMjkxNmYzYy05MDkzLTQ4MTMtODM5Ny1mMTBlNmI3MDRiNjgiLCJkZWxlZ2F0aW9uSWQiOiJiNGFlNDdhNy02MjVhLTQ2MzAtOTcyNy00NTc2NGE3MTJjY2UiLCJleHAiOjE2NTUyNzkxMDksIm5iZiI6MTY1NTI3ODgwOSwic2NvcGUiOiJyZWFkIG9wZW5pZCIsImlzcyI6Imh0dHBzOi8vaWRzdnIuZXhhbXBsZS5jb20iLCJzdWIiOiJ1c2VybmFtZSIsImF1ZCI6ImFwaS5leGFtcGxlLmNvbSIsImlhdCI6MTY1NTI3ODgwOSwicHVycG9zZSI6ImFjY2Vzc190b2tlbiJ9.rjeE8D_e4RYzgvpu-nOwwx7PWMiZyDZwkwO6RiHR5t8g4JqqVokUKQt-oST1s45wubacfeDSFogOrIhe3UHDAg'] ]; } } diff --git a/tests/data/jwks-eddsa.json b/tests/data/jwks-eddsa.json new file mode 100644 index 00000000..cc597408 --- /dev/null +++ b/tests/data/jwks-eddsa.json @@ -0,0 +1,11 @@ +{ + "keys": [ + { + "kty": "OKP", + "kid": "-1909572257", + "alg": "EdDSA", + "crv": "Ed25519", + "x": "XWxGtApfcqmKI7p0OKnF5JSEWMVoLsytFXLEP7xZ_l8" + } + ] +} diff --git a/tests/data/jwks-rs512.json b/tests/data/jwks-rs512.json new file mode 100644 index 00000000..df1d49e6 --- /dev/null +++ b/tests/data/jwks-rs512.json @@ -0,0 +1,13 @@ +{ + "keys": [ + { + "alg": "RS512", + "e": "AQAB", + "key_ops": ["verify"], + "kty": "RSA", + "n": "sOFO_fifz82fLvrrOo9S3lBU347DrM3DmzH47SNw1ILMkNwDMK38on-GNcl59If0DTEEr7WITPLJ5FS_Rf3XcX09ui2Ol2y7LhMLpL3AQMf7kFyWKhMtOiEkaUrUc_nozyAF4tsKRAf8anYrImaS1NHHmsLBJ1QAWYd4N6b5L09zHX6LkOrLqBY-xgI1W4hQUOKNwF2BoA6KC8eX89iDqgRKQFXLt2pV6h55BRjJ7EBEkoT7KvaACVoUNL4Y5Pg2CLwLm0bYPMugaO-fLPp9tok5mdTD2fQZRGChzIQjWl6BLT2uO639Ccwmtb3VkjHDv061UI8TIAK4_mWDqscOUdbRpa5Z-oPq7ZM7eps83YxJXKvdkZzmx6tJayrNYH7BEHP03p3TZ1pDszDOLD-kmCppja0SG9NeH4Zu6PqxaNtimOKEakmUx0s9MhnlYWUZnB0mdl4YXr_Ypd0nsOJLrpvvjlQacozDIgflTaXcpZJkiwiRWoBmHuQXJHTuqP3_", + "use": "sig", + "kid": "3b4b7fcbac8100fe528912776417009a" + } + ] +}