From 2e963190e36b0a0835d3a9461524c30a56ffe2e8 Mon Sep 17 00:00:00 2001 From: Neuman Vong Date: Fri, 25 Mar 2011 16:08:00 -0700 Subject: [PATCH 001/282] Start tracking project --- Jwt/JWT.php | 164 ++++++++++++++++++++++++++++++++++++++++ Jwt/Makefile | 6 ++ Jwt/package.php | 75 ++++++++++++++++++ Jwt/package.xml | 75 ++++++++++++++++++ Jwt/tests/Bootstrap.php | 9 +++ Jwt/tests/JWTTest.php | 31 ++++++++ Jwt/tests/phpunit.xml | 7 ++ 7 files changed, 367 insertions(+) create mode 100644 Jwt/JWT.php create mode 100644 Jwt/Makefile create mode 100644 Jwt/package.php create mode 100644 Jwt/package.xml create mode 100644 Jwt/tests/Bootstrap.php create mode 100644 Jwt/tests/JWTTest.php create mode 100644 Jwt/tests/phpunit.xml diff --git a/Jwt/JWT.php b/Jwt/JWT.php new file mode 100644 index 00000000000..d1704e1a1e4 --- /dev/null +++ b/Jwt/JWT.php @@ -0,0 +1,164 @@ + + */ +class JWT +{ + /** + * @param string $jwt The JWT + * @param string|null $key The secret key + * @param bool $verify Don't skip verification process + * + * @return object The JWT's payload as a PHP object + */ + public static function decode($jwt, $key = null, $verify = true) + { + $tks = explode('.', $jwt); + if (count($tks) != 3) { + throw new UnexpectedValueException('Wrong number of segments'); + } + list($headb64, $payloadb64, $cryptob64) = $tks; + if (null === ($header = JWT::jsonDecode(JWT::urlsafeB64Decode($headb64))) + ) { + throw new UnexpectedValueException('Invalid segment encoding'); + } + if (null === $payload = JWT::jsonDecode(JWT::urlsafeB64Decode($payloadb64)) + ) { + throw new UnexpectedValueException('Invalid segment encoding'); + } + $sig = JWT::urlsafeB64Decode($cryptob64); + if ($verify) { + if (empty($header->alg)) { + throw new DomainException('Empty algorithm'); + } + if ($sig != JWT::sign("$headb64.$payloadb64", $key, $header->alg)) { + throw new UnexpectedValueException('Signature verification failed'); + } + } + return $payload; + } + + /** + * @param object|array $payload PHP object or array + * @param string $key The secret key + * @param string $algo The signing algorithm + * + * @return string A JWT + */ + public static function encode($payload, $key, $algo = 'HS256') + { + $header = array('typ' => 'jwt', 'alg' => $algo); + + $segments = array(); + $segments[] = JWT::urlsafeB64Encode(JWT::jsonEncode($header)); + $segments[] = JWT::urlsafeB64Encode(JWT::jsonEncode($payload)); + $signing_input = implode('.', $segments); + + $signature = JWT::sign($signing_input, $key, $algo); + $segments[] = JWT::urlsafeB64Encode($signature); + + return implode('.', $segments); + } + + /** + * @param string $msg The message to sign + * @param string $key The secret key + * @param string $method The signing algorithm + * + * @return string An encrypted message + */ + public static function sign($msg, $key, $method = 'HS256') + { + $methods = array( + 'HS256' => 'sha256', + 'HS384' => 'sha384', + 'HS512' => 'sha512', + ); + if (empty($methods[$method])) { + throw new DomainException('Algorithm not supported'); + } + return hash_hmac($methods[$method], $msg, $key, true); + } + + /** + * @param string $input JSON string + * + * @return object Object representation of JSON string + */ + public static function jsonDecode($input) + { + $obj = json_decode($input); + if (function_exists('json_last_error') && $errno = json_last_error()) { + JWT::handleJsonError($errno); + } + else if ($obj === null && $input !== 'null') { + throw new DomainException('Null result with non-null input'); + } + return $obj; + } + + /** + * @param object|array $input A PHP object or array + * + * @return string JSON representation of the PHP object or array + */ + public static function jsonEncode($input) + { + $json = json_encode($input); + if (function_exists('json_last_error') && $errno = json_last_error()) { + JWT::handleJsonError($errno); + } + else if ($json === 'null' && $input !== null) { + throw new DomainException('Null result with non-null input'); + } + return $json; + } + + /** + * @param string $input A base64 encoded string + * + * @return string A decoded string + */ + public static function urlsafeB64Decode($input) + { + $padlen = 4 - strlen($input) % 4; + $input .= str_repeat('=', $padlen); + return base64_decode(strtr($input, '-_', '+/')); + } + + /** + * @param string $input Anything really + * + * @return string The base64 encode of what you passed in + */ + public static function urlsafeB64Encode($input) + { + return str_replace('=', '', strtr(base64_encode($input), '+/', '-_')); + } + + /** + * @param int $errno An error number from json_last_error() + * + * @return void + */ + private static function handleJsonError($errno) + { + $messages = array( + JSON_ERROR_DEPTH => 'Maximum stack depth exceeded', + JSON_ERROR_CTRL_CHAR => 'Unexpected control character found', + JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON' + ); + throw new DomainException(isset($messages[$errno]) + ? $messages[$errno] + : 'Unknown JSON error: ' . $errno + ); + } + +} + diff --git a/Jwt/Makefile b/Jwt/Makefile new file mode 100644 index 00000000000..be761d5903d --- /dev/null +++ b/Jwt/Makefile @@ -0,0 +1,6 @@ +all: test +test: + @echo running tests + @phpunit --configuration tests/phpunit.xml + +.PHONY: all test diff --git a/Jwt/package.php b/Jwt/package.php new file mode 100644 index 00000000000..83d50bd0cb3 --- /dev/null +++ b/Jwt/package.php @@ -0,0 +1,75 @@ +setOptions( + array( + 'filelistgenerator' => 'file', + 'simpleoutput' => true, + 'baseinstalldir' => '/', + 'packagedirectory' => './', + 'dir_roles' => array( + 'tests' => 'test' + ), + 'ignore' => array( + 'package.php', + '*.tgz' + ) + ) +); + +$package->setPackage('JWT'); +$package->setSummary('A JWT encoder/decoder.'); +$package->setDescription($description); +$package->setChannel('pear.php.net'); +$package->setPackageType('php'); +$package->setLicense( + 'MIT License', + 'http://creativecommons.org/licenses/MIT/' +); + +$package->setNotes($release_notes); +$package->setReleaseVersion($release_version); +$package->setReleaseStability($release_state); +$package->setAPIVersion($api_version); +$package->setAPIStability($api_state); + +$package->addMaintainer( + 'lead', + 'lcfrs', + 'Neuman Vong', + 'neuman+pear@twilio.com' +); + +$package->addExtensionDep('required', 'json'); +$package->addExtensionDep('required', 'hash_hmac'); + +$package->setPhpDep('5.1'); + +$package->setPearInstallerDep('1.7.0'); +$package->generateContents(); +$package->addRelease(); + +if ( isset($_GET['make']) + || (isset($_SERVER['argv']) && @$_SERVER['argv'][1] == 'make') +) { + $package->writePackageFile(); +} else { + $package->debugPackageFile(); +} + +?> diff --git a/Jwt/package.xml b/Jwt/package.xml new file mode 100644 index 00000000000..44af9e30bfe --- /dev/null +++ b/Jwt/package.xml @@ -0,0 +1,75 @@ + + + JWT + pear.php.net + A JWT encoder/decoder. + A JWT encoder/decoder. + + Neuman Vong + lcfrs + neuman+pear@twilio.com + yes + + 2011-03-25 + + + 0.0.0 + 0.0.0 + + + alpha + alpha + + MIT License + +No release notes. + + + + + + + + + + + + + + + + 5.1 + + + 1.7.0 + + + json + + + hash_hmac + + + + + + + + 0.0.0 + 0.0.0 + + + alpha + alpha + + 2011-03-25 + MIT License + +No release notes. + + + + diff --git a/Jwt/tests/Bootstrap.php b/Jwt/tests/Bootstrap.php new file mode 100644 index 00000000000..235b6d32c42 --- /dev/null +++ b/Jwt/tests/Bootstrap.php @@ -0,0 +1,9 @@ +assertEquals(JWT::decode($msg, 'my_key'), 'abc'); + } + + function testDecodeFromPython() { + $msg = 'eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.Iio6aHR0cDovL2FwcGxpY2F0aW9uL2NsaWNreT9ibGFoPTEuMjMmZi5vbz00NTYgQUMwMDAgMTIzIg.E_U8X2YpMT5K1cEiT_3-IvBYfrdIFIeVYeOqre_Z5Cg'; + $this->assertEquals( + JWT::decode($msg, 'my_key'), + '*:http://application/clicky?blah=1.23&f.oo=456 AC000 123' + ); + } + + function testUrlSafeCharacters() { + $encoded = JWT::encode('f?', 'a'); + $this->assertEquals('f?', JWT::decode($encoded, 'a')); + } + + function testMalformedUtf8StringsFail() { + $this->setExpectedException('DomainException'); + JWT::encode(pack('c', 128), 'a'); + } + + function testMalformedJsonThrowsException() { + $this->setExpectedException('DomainException'); + JWT::jsonDecode('this is not valid JSON string'); + } +} diff --git a/Jwt/tests/phpunit.xml b/Jwt/tests/phpunit.xml new file mode 100644 index 00000000000..ebfe3cf5139 --- /dev/null +++ b/Jwt/tests/phpunit.xml @@ -0,0 +1,7 @@ + + + + ./ + + + From 26536fa614ee171d04fed28a87ba95b52cbb4167 Mon Sep 17 00:00:00 2001 From: Neuman Vong Date: Fri, 25 Mar 2011 16:47:36 -0700 Subject: [PATCH 002/282] Fix misspelled extension dep --- Jwt/package.php | 2 +- Jwt/package.xml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Jwt/package.php b/Jwt/package.php index 83d50bd0cb3..b2e914a3725 100644 --- a/Jwt/package.php +++ b/Jwt/package.php @@ -56,7 +56,7 @@ ); $package->addExtensionDep('required', 'json'); -$package->addExtensionDep('required', 'hash_hmac'); +$package->addExtensionDep('required', 'hash'); $package->setPhpDep('5.1'); diff --git a/Jwt/package.xml b/Jwt/package.xml index 44af9e30bfe..03eec958ba9 100644 --- a/Jwt/package.xml +++ b/Jwt/package.xml @@ -14,7 +14,7 @@ yes 2011-03-25 - + 0.0.0 0.0.0 @@ -50,7 +50,7 @@ No release notes. json - hash_hmac + hash From ce2829c2403a6db404b084d2e18094794829ace9 Mon Sep 17 00:00:00 2001 From: Neuman Vong Date: Wed, 27 Apr 2011 17:16:15 -0700 Subject: [PATCH 003/282] Stop padding inputs already divisible by 4 --- Jwt/JWT.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Jwt/JWT.php b/Jwt/JWT.php index d1704e1a1e4..2bfb14711a1 100644 --- a/Jwt/JWT.php +++ b/Jwt/JWT.php @@ -127,8 +127,11 @@ public static function jsonEncode($input) */ public static function urlsafeB64Decode($input) { - $padlen = 4 - strlen($input) % 4; - $input .= str_repeat('=', $padlen); + $remainder = strlen($input) % 4; + if ($remainder) { + $padlen = 4 - $remainder; + $input .= str_repeat('=', $padlen); + } return base64_decode(strtr($input, '-_', '+/')); } From 6b66bda5450b7760fd8d44ea8a4d505900abd7aa Mon Sep 17 00:00:00 2001 From: Jeff Lindsay Date: Wed, 29 Jun 2011 14:16:06 -0700 Subject: [PATCH 004/282] This is more correct according to the spec. --- Jwt/JWT.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jwt/JWT.php b/Jwt/JWT.php index 2bfb14711a1..0cf98fdc9a3 100644 --- a/Jwt/JWT.php +++ b/Jwt/JWT.php @@ -53,7 +53,7 @@ public static function decode($jwt, $key = null, $verify = true) */ public static function encode($payload, $key, $algo = 'HS256') { - $header = array('typ' => 'jwt', 'alg' => $algo); + $header = array('typ' => 'JWT', 'alg' => $algo); $segments = array(); $segments[] = JWT::urlsafeB64Encode(JWT::jsonEncode($header)); From 822ff0b3fd1ee70bd1c0d7e25f06307b3bf8f4be Mon Sep 17 00:00:00 2001 From: Neuman Vong Date: Mon, 31 Dec 2012 14:11:04 -0800 Subject: [PATCH 005/282] BSD3 --- Jwt/LICENSE | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 Jwt/LICENSE diff --git a/Jwt/LICENSE b/Jwt/LICENSE new file mode 100644 index 00000000000..cb0c49b3311 --- /dev/null +++ b/Jwt/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2011, Neuman Vong + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Neuman Vong nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. From d9ca9985e8d4455ac2e99524587d0d7b667c87bf Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Mon, 31 Dec 2012 15:09:27 -0800 Subject: [PATCH 006/282] Minor formatting changes --- Jwt/JWT.php | 89 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 55 insertions(+), 34 deletions(-) diff --git a/Jwt/JWT.php b/Jwt/JWT.php index 0cf98fdc9a3..b6434d6730b 100644 --- a/Jwt/JWT.php +++ b/Jwt/JWT.php @@ -1,21 +1,25 @@ + * @author Anant Narayanan */ class JWT { /** - * @param string $jwt The JWT - * @param string|null $key The secret key - * @param bool $verify Don't skip verification process + * Decodes a JWT string into a PHP object. + * + * @access public + * @param string $jwt The JWT + * @param string|null $key The secret key + * @param bool $verify Don't skip verification process * - * @return object The JWT's payload as a PHP object + * @return object The JWT's payload as a PHP object + * @uses jsonDecode + * @uses urlsafeB64Decode */ public static function decode($jwt, $key = null, $verify = true) { @@ -24,12 +28,10 @@ public static function decode($jwt, $key = null, $verify = true) throw new UnexpectedValueException('Wrong number of segments'); } list($headb64, $payloadb64, $cryptob64) = $tks; - if (null === ($header = JWT::jsonDecode(JWT::urlsafeB64Decode($headb64))) - ) { + if (null === ($header = JWT::jsonDecode(JWT::urlsafeB64Decode($headb64)))) { throw new UnexpectedValueException('Invalid segment encoding'); } - if (null === $payload = JWT::jsonDecode(JWT::urlsafeB64Decode($payloadb64)) - ) { + if (null === $payload = JWT::jsonDecode(JWT::urlsafeB64Decode($payloadb64))) { throw new UnexpectedValueException('Invalid segment encoding'); } $sig = JWT::urlsafeB64Decode($cryptob64); @@ -45,11 +47,16 @@ public static function decode($jwt, $key = null, $verify = true) } /** - * @param object|array $payload PHP object or array - * @param string $key The secret key - * @param string $algo The signing algorithm + * Converts and signs a PHP object or array into a JWT string. + * + * @access public + * @param object|array $payload PHP object or array + * @param string $key The secret key + * @param string $algo The signing algorithm * - * @return string A JWT + * @return string A signed JWT + * @uses jsonEncode + * @uses urlsafeB64Encode */ public static function encode($payload, $key, $algo = 'HS256') { @@ -67,11 +74,14 @@ public static function encode($payload, $key, $algo = 'HS256') } /** - * @param string $msg The message to sign - * @param string $key The secret key - * @param string $method The signing algorithm + * Sign a string with a given key and algorithm. * - * @return string An encrypted message + * @access public + * @param string $msg The message to sign + * @param string $key The secret key + * @param string $method The signing algorithm + * + * @return string An encrypted message */ public static function sign($msg, $key, $method = 'HS256') { @@ -87,43 +97,50 @@ public static function sign($msg, $key, $method = 'HS256') } /** - * @param string $input JSON string + * Decode a JSON string into a PHP object. + * + * @access public + * @param string $input JSON string * - * @return object Object representation of JSON string + * @return object Object representation of JSON string */ public static function jsonDecode($input) { $obj = json_decode($input); if (function_exists('json_last_error') && $errno = json_last_error()) { JWT::handleJsonError($errno); - } - else if ($obj === null && $input !== 'null') { + } else if ($obj === null && $input !== 'null') { throw new DomainException('Null result with non-null input'); } return $obj; } /** - * @param object|array $input A PHP object or array + * Encode a PHP object into a JSON string. + * + * @access public + * @param object|array $input A PHP object or array * - * @return string JSON representation of the PHP object or array + * @return string JSON representation of the PHP object or array */ public static function jsonEncode($input) { $json = json_encode($input); if (function_exists('json_last_error') && $errno = json_last_error()) { JWT::handleJsonError($errno); - } - else if ($json === 'null' && $input !== null) { + } else if ($json === 'null' && $input !== null) { throw new DomainException('Null result with non-null input'); } return $json; } /** - * @param string $input A base64 encoded string + * Decode a string with URL-safe Base64. * - * @return string A decoded string + * @access public + * @param string $input A Base64 encoded string + * + * @return string A decoded string */ public static function urlsafeB64Decode($input) { @@ -136,9 +153,12 @@ public static function urlsafeB64Decode($input) } /** - * @param string $input Anything really + * Encode a string with URL-safe Base64. + * + * @access public + * @param string $input The string you want encoded * - * @return string The base64 encode of what you passed in + * @return string The base64 encode of what you passed in */ public static function urlsafeB64Encode($input) { @@ -146,9 +166,10 @@ public static function urlsafeB64Encode($input) } /** - * @param int $errno An error number from json_last_error() + * @access private + * @param int $errno An error number from json_last_error() * - * @return void + * @return void */ private static function handleJsonError($errno) { From 619cd09d70bdd656c1ffed4125d5e36f56754a6d Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Mon, 31 Dec 2012 15:50:34 -0800 Subject: [PATCH 007/282] Update tests to latest phpunit --- Jwt/package.php | 75 ----------------------------------------- Jwt/tests/Bootstrap.php | 9 ----- Jwt/tests/JWTTest.php | 6 +++- Jwt/tests/phpunit.xml | 7 ---- 4 files changed, 5 insertions(+), 92 deletions(-) delete mode 100644 Jwt/package.php delete mode 100644 Jwt/tests/Bootstrap.php delete mode 100644 Jwt/tests/phpunit.xml diff --git a/Jwt/package.php b/Jwt/package.php deleted file mode 100644 index b2e914a3725..00000000000 --- a/Jwt/package.php +++ /dev/null @@ -1,75 +0,0 @@ -setOptions( - array( - 'filelistgenerator' => 'file', - 'simpleoutput' => true, - 'baseinstalldir' => '/', - 'packagedirectory' => './', - 'dir_roles' => array( - 'tests' => 'test' - ), - 'ignore' => array( - 'package.php', - '*.tgz' - ) - ) -); - -$package->setPackage('JWT'); -$package->setSummary('A JWT encoder/decoder.'); -$package->setDescription($description); -$package->setChannel('pear.php.net'); -$package->setPackageType('php'); -$package->setLicense( - 'MIT License', - 'http://creativecommons.org/licenses/MIT/' -); - -$package->setNotes($release_notes); -$package->setReleaseVersion($release_version); -$package->setReleaseStability($release_state); -$package->setAPIVersion($api_version); -$package->setAPIStability($api_state); - -$package->addMaintainer( - 'lead', - 'lcfrs', - 'Neuman Vong', - 'neuman+pear@twilio.com' -); - -$package->addExtensionDep('required', 'json'); -$package->addExtensionDep('required', 'hash'); - -$package->setPhpDep('5.1'); - -$package->setPearInstallerDep('1.7.0'); -$package->generateContents(); -$package->addRelease(); - -if ( isset($_GET['make']) - || (isset($_SERVER['argv']) && @$_SERVER['argv'][1] == 'make') -) { - $package->writePackageFile(); -} else { - $package->debugPackageFile(); -} - -?> diff --git a/Jwt/tests/Bootstrap.php b/Jwt/tests/Bootstrap.php deleted file mode 100644 index 235b6d32c42..00000000000 --- a/Jwt/tests/Bootstrap.php +++ /dev/null @@ -1,9 +0,0 @@ -assertEquals(JWT::decode($msg, 'my_key'), 'abc'); @@ -29,3 +31,5 @@ function testMalformedJsonThrowsException() { JWT::jsonDecode('this is not valid JSON string'); } } + +?> diff --git a/Jwt/tests/phpunit.xml b/Jwt/tests/phpunit.xml deleted file mode 100644 index ebfe3cf5139..00000000000 --- a/Jwt/tests/phpunit.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - ./ - - - From d1885ed2d4b30b4f771cd07c7d220f8a506214a9 Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Mon, 31 Dec 2012 16:07:33 -0800 Subject: [PATCH 008/282] Update package file for PEAR submission --- Jwt/JWT.php | 2 +- Jwt/Makefile | 6 ------ Jwt/package.xml | 44 +++++++++++++++++++++++--------------------- 3 files changed, 24 insertions(+), 28 deletions(-) delete mode 100644 Jwt/Makefile diff --git a/Jwt/JWT.php b/Jwt/JWT.php index b6434d6730b..fb32df14009 100644 --- a/Jwt/JWT.php +++ b/Jwt/JWT.php @@ -5,7 +5,7 @@ * http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-06 * * @author Neuman Vong - * @author Anant Narayanan + * @author Anant Narayanan */ class JWT { diff --git a/Jwt/Makefile b/Jwt/Makefile deleted file mode 100644 index be761d5903d..00000000000 --- a/Jwt/Makefile +++ /dev/null @@ -1,6 +0,0 @@ -all: test -test: - @echo running tests - @phpunit --configuration tests/phpunit.xml - -.PHONY: all test diff --git a/Jwt/package.xml b/Jwt/package.xml index 03eec958ba9..02b5c188d2d 100644 --- a/Jwt/package.xml +++ b/Jwt/package.xml @@ -6,37 +6,39 @@ JWT pear.php.net A JWT encoder/decoder. - A JWT encoder/decoder. + A JWT encoder/decoder library for PHP. Neuman Vong lcfrs neuman+pear@twilio.com yes - 2011-03-25 - + + Anant Narayanan + anant + anant@php.net + yes + + 2012-12-31 - 0.0.0 - 0.0.0 + 0.1.0 + 0.1.0 - alpha - alpha + beta + beta - MIT License + BSD 3-Clause License -No release notes. +Initial release with basic support for JWT encoding, decoding and signature verification. - - - + - - + @@ -58,17 +60,17 @@ No release notes. - 0.0.0 - 0.0.0 + 0.1.0 + 0.1.0 - alpha - alpha + beta + beta - 2011-03-25 - MIT License + 2012-12-31 + BSD 3-Clause License -No release notes. +Initial release with basic support for JWT encoding, decoding and signature verification. From a1d2bbce7f8a758c479f205834c3505fc5714efe Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Mon, 31 Dec 2012 16:28:13 -0800 Subject: [PATCH 009/282] Add README and package file --- Jwt/JWT-0.1.0.tgz | Bin 0 -> 2981 bytes Jwt/README.md | 30 ++++++++++++++++++++++++++++++ Jwt/package.xml | 2 +- 3 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 Jwt/JWT-0.1.0.tgz create mode 100644 Jwt/README.md diff --git a/Jwt/JWT-0.1.0.tgz b/Jwt/JWT-0.1.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..f06c82cc2ff90648f8aead83ce9ac9ecf4c2a97b GIT binary patch literal 2981 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 b+_(F7-~M6kzW@LL|NjF3 "http://example.org", + "aud" => "http://example.com", + "iat" => 1356999524, + "nbf" => 1357000000 + ); + + $jwt = JWT::encode($token, $key); + $decoded = JWT::decode($jwt, $key); + + print_r($decoded); + ?> + +License +------- +[3-Clause BSD](http://opensource.org/licenses/BSD-3-Clause). diff --git a/Jwt/package.xml b/Jwt/package.xml index 02b5c188d2d..9ec07e90e43 100644 --- a/Jwt/package.xml +++ b/Jwt/package.xml @@ -15,7 +15,7 @@ Anant Narayanan - anant + anant anant@php.net yes From 173f10eca21e3406ffc02a9db982d44ccfc5ffd8 Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Mon, 31 Dec 2012 16:39:20 -0800 Subject: [PATCH 010/282] Syntax highligh example --- Jwt/README.md | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/Jwt/README.md b/Jwt/README.md index 90d6bf939eb..4b60193afc5 100644 --- a/Jwt/README.md +++ b/Jwt/README.md @@ -1,29 +1,29 @@ PHP-JWT ======= - A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the [current spec](http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-06) Example ------- - - "http://example.org", - "aud" => "http://example.com", - "iat" => 1356999524, - "nbf" => 1357000000 - ); - - $jwt = JWT::encode($token, $key); - $decoded = JWT::decode($jwt, $key); - - print_r($decoded); - ?> +```php + "http://example.org", + "aud" => "http://example.com", + "iat" => 1356999524, + "nbf" => 1357000000 + ); + + $jwt = JWT::encode($token, $key); + $decoded = JWT::decode($jwt, $key); + + print_r($decoded); +?> +``` License ------- From fd02c027b8383bc1e3f1929f952b0eba109c76e5 Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Wed, 3 Apr 2013 15:56:16 -0700 Subject: [PATCH 011/282] Some re-organization as per PEPr suggestions --- Jwt/{ => Authentication}/JWT.php | 0 Jwt/JWT-0.1.0.tgz | Bin 2981 -> 0 bytes Jwt/tests/JWTTest.php | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) rename Jwt/{ => Authentication}/JWT.php (100%) delete mode 100644 Jwt/JWT-0.1.0.tgz diff --git a/Jwt/JWT.php b/Jwt/Authentication/JWT.php similarity index 100% rename from Jwt/JWT.php rename to Jwt/Authentication/JWT.php diff --git a/Jwt/JWT-0.1.0.tgz b/Jwt/JWT-0.1.0.tgz deleted file mode 100644 index f06c82cc2ff90648f8aead83ce9ac9ecf4c2a97b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2981 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 b+_(F7-~M6kzW@LL|NjF3 Date: Wed, 3 Apr 2013 16:17:10 -0700 Subject: [PATCH 012/282] Fix warnings and errors reported by PHP_CodeSniffer --- Jwt/Authentication/JWT.php | 106 ++++++++++++++++++++++--------------- 1 file changed, 63 insertions(+), 43 deletions(-) diff --git a/Jwt/Authentication/JWT.php b/Jwt/Authentication/JWT.php index fb32df14009..7a7b4a0e99b 100644 --- a/Jwt/Authentication/JWT.php +++ b/Jwt/Authentication/JWT.php @@ -4,22 +4,41 @@ * JSON Web Token implementation, based on this spec: * http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-06 * - * @author Neuman Vong - * @author Anant Narayanan + * PHP version 5 + * + * @category Authentication + * @package Authentication_JWT + * @author Neuman Vong + * @author Anant Narayanan + * @license http://opensource.org/licenses/BSD-3-Clause 3-clause BSD + * @link https://github.com/firebase/php-jwt + */ +/** + * JSON Web Token implementation, based on this spec: + * http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-06 + * + * @category Authentication + * @package Authentication_JWT + * @author Neuman Vong + * @author Anant Narayanan + * @license http://opensource.org/licenses/BSD-3-Clause 3-clause BSD + * @link https://github.com/firebase/php-jwt */ class JWT { /** * Decodes a JWT string into a PHP object. * - * @access public - * @param string $jwt The JWT - * @param string|null $key The secret key - * @param bool $verify Don't skip verification process + * @param string $jwt The JWT + * @param string|null $key The secret key + * @param bool $verify Don't skip verification process * - * @return object The JWT's payload as a PHP object - * @uses jsonDecode - * @uses urlsafeB64Decode + * @return object The JWT's payload as a PHP object + * @throws UnexpectedValueException Provided JWT was invalid + * @throws DomainException Algorithm was not provided + * + * @uses jsonDecode + * @uses urlsafeB64Decode */ public static function decode($jwt, $key = null, $verify = true) { @@ -27,11 +46,11 @@ public static function decode($jwt, $key = null, $verify = true) if (count($tks) != 3) { throw new UnexpectedValueException('Wrong number of segments'); } - list($headb64, $payloadb64, $cryptob64) = $tks; + list($headb64, $bodyb64, $cryptob64) = $tks; if (null === ($header = JWT::jsonDecode(JWT::urlsafeB64Decode($headb64)))) { throw new UnexpectedValueException('Invalid segment encoding'); } - if (null === $payload = JWT::jsonDecode(JWT::urlsafeB64Decode($payloadb64))) { + if (null === $payload = JWT::jsonDecode(JWT::urlsafeB64Decode($bodyb64))) { throw new UnexpectedValueException('Invalid segment encoding'); } $sig = JWT::urlsafeB64Decode($cryptob64); @@ -39,7 +58,7 @@ public static function decode($jwt, $key = null, $verify = true) if (empty($header->alg)) { throw new DomainException('Empty algorithm'); } - if ($sig != JWT::sign("$headb64.$payloadb64", $key, $header->alg)) { + if ($sig != JWT::sign("$headb64.$bodyb64", $key, $header->alg)) { throw new UnexpectedValueException('Signature verification failed'); } } @@ -49,14 +68,14 @@ public static function decode($jwt, $key = null, $verify = true) /** * Converts and signs a PHP object or array into a JWT string. * - * @access public - * @param object|array $payload PHP object or array - * @param string $key The secret key - * @param string $algo The signing algorithm + * @param object|array $payload PHP object or array + * @param string $key The secret key + * @param string $algo The signing algorithm. Supported + * algorithms are 'HS256', 'HS384' and 'HS512' * - * @return string A signed JWT - * @uses jsonEncode - * @uses urlsafeB64Encode + * @return string A signed JWT + * @uses jsonEncode + * @uses urlsafeB64Encode */ public static function encode($payload, $key, $algo = 'HS256') { @@ -76,12 +95,13 @@ public static function encode($payload, $key, $algo = 'HS256') /** * Sign a string with a given key and algorithm. * - * @access public - * @param string $msg The message to sign - * @param string $key The secret key - * @param string $method The signing algorithm + * @param string $msg The message to sign + * @param string $key The secret key + * @param string $method The signing algorithm. Supported + * algorithms are 'HS256', 'HS384' and 'HS512' * - * @return string An encrypted message + * @return string An encrypted message + * @throws DomainException Unsupported algorithm was specified */ public static function sign($msg, $key, $method = 'HS256') { @@ -99,16 +119,16 @@ public static function sign($msg, $key, $method = 'HS256') /** * Decode a JSON string into a PHP object. * - * @access public - * @param string $input JSON string + * @param string $input JSON string * - * @return object Object representation of JSON string + * @return object Object representation of JSON string + * @throws DomainException Provided string was invalid JSON */ public static function jsonDecode($input) { $obj = json_decode($input); if (function_exists('json_last_error') && $errno = json_last_error()) { - JWT::handleJsonError($errno); + JWT::_handleJsonError($errno); } else if ($obj === null && $input !== 'null') { throw new DomainException('Null result with non-null input'); } @@ -118,16 +138,16 @@ public static function jsonDecode($input) /** * Encode a PHP object into a JSON string. * - * @access public - * @param object|array $input A PHP object or array + * @param object|array $input A PHP object or array * - * @return string JSON representation of the PHP object or array + * @return string JSON representation of the PHP object or array + * @throws DomainException Provided object could not be encoded to valid JSON */ public static function jsonEncode($input) { $json = json_encode($input); if (function_exists('json_last_error') && $errno = json_last_error()) { - JWT::handleJsonError($errno); + JWT::_handleJsonError($errno); } else if ($json === 'null' && $input !== null) { throw new DomainException('Null result with non-null input'); } @@ -137,10 +157,9 @@ public static function jsonEncode($input) /** * Decode a string with URL-safe Base64. * - * @access public - * @param string $input A Base64 encoded string + * @param string $input A Base64 encoded string * - * @return string A decoded string + * @return string A decoded string */ public static function urlsafeB64Decode($input) { @@ -155,10 +174,9 @@ public static function urlsafeB64Decode($input) /** * Encode a string with URL-safe Base64. * - * @access public - * @param string $input The string you want encoded + * @param string $input The string you want encoded * - * @return string The base64 encode of what you passed in + * @return string The base64 encode of what you passed in */ public static function urlsafeB64Encode($input) { @@ -166,19 +184,21 @@ public static function urlsafeB64Encode($input) } /** - * @access private - * @param int $errno An error number from json_last_error() + * Helper method to create a JSON error. + * + * @param int $errno An error number from json_last_error() * - * @return void + * @return void */ - private static function handleJsonError($errno) + private static function _handleJsonError($errno) { $messages = array( JSON_ERROR_DEPTH => 'Maximum stack depth exceeded', JSON_ERROR_CTRL_CHAR => 'Unexpected control character found', JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON' ); - throw new DomainException(isset($messages[$errno]) + throw new DomainException( + isset($messages[$errno]) ? $messages[$errno] : 'Unknown JSON error: ' . $errno ); From 54804b7aca01b5a27a8327778ac92d240e4db949 Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Wed, 3 Apr 2013 16:26:05 -0700 Subject: [PATCH 013/282] Add info on tests --- Jwt/README.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Jwt/README.md b/Jwt/README.md index 4b60193afc5..2f9e4392854 100644 --- a/Jwt/README.md +++ b/Jwt/README.md @@ -8,7 +8,7 @@ Example ------- ```php ``` +Tests +----- +Run the tests using phpunit: + +```bash + $ pear install PHPUnit + $ phpunit tests/ + PHPUnit 3.7.10 by Sebastian Bergmann. + ..... + Time: 0 seconds, Memory: 2.50Mb + OK (5 tests, 5 assertions) +``` + License ------- [3-Clause BSD](http://opensource.org/licenses/BSD-3-Clause). From 21701e0373ccdc573689aaacaca20d912bc61722 Mon Sep 17 00:00:00 2001 From: Dan McFadden Date: Fri, 28 Jun 2013 15:08:17 +0000 Subject: [PATCH 014/282] Fix package file JWT.php was moved into ./Authentication dir w/o updating package file --- Jwt/package.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jwt/package.xml b/Jwt/package.xml index 9ec07e90e43..6c44498e5b7 100644 --- a/Jwt/package.xml +++ b/Jwt/package.xml @@ -37,7 +37,7 @@ Initial release with basic support for JWT encoding, decoding and signature veri - + From 22a49b7389fc7f3bb3645611940b4dc33917c511 Mon Sep 17 00:00:00 2001 From: Phansys Date: Fri, 19 Jul 2013 13:05:10 -0300 Subject: [PATCH 015/282] Added composer.json to allow install and dependency management through Composer. --- Jwt/composer.json | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 Jwt/composer.json diff --git a/Jwt/composer.json b/Jwt/composer.json new file mode 100644 index 00000000000..292edbf6dc2 --- /dev/null +++ b/Jwt/composer.json @@ -0,0 +1,26 @@ +{ + "name": "firebase/php-jwt", + "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", + "homepage": "https://github.com/firebase/php-jwt", + "authors": [ + { + "name": "Neuman Vong", + "email": "neuman+pear@twilio.com", + "role": "Developer" + }, + { + "name": "Anant Narayanan", + "email": "anant@php.net", + "role": "Developer" + } + ], + "license": "BSD-3-Clause", + "require": { + "php": ">=5.2.0" + }, + "autoload": { + "classmap": ["Authentication/"] + }, + "target-dir": "Firebase/PHP-JWT", + "minimum-stability": "dev" +} From 34ced2ee6320313c6a7c2efbd300bc97d802bbb3 Mon Sep 17 00:00:00 2001 From: Phansys Date: Fri, 30 Aug 2013 16:55:02 -0300 Subject: [PATCH 016/282] Added config for phpunit and Travis CI. --- Jwt/.travis.yml | 12 ++++++++++++ Jwt/phpunit.xml.dist | 19 +++++++++++++++++++ Jwt/tests/bootstrap.php | 7 +++++++ 3 files changed, 38 insertions(+) create mode 100644 Jwt/.travis.yml create mode 100644 Jwt/phpunit.xml.dist create mode 100644 Jwt/tests/bootstrap.php diff --git a/Jwt/.travis.yml b/Jwt/.travis.yml new file mode 100644 index 00000000000..1ecd967ef7a --- /dev/null +++ b/Jwt/.travis.yml @@ -0,0 +1,12 @@ +language: php + +php: + - 5.3 + - 5.4 + - 5.5 + +before_script: + - wget -nc http://getcomposer.org/composer.phar + - php composer.phar install + +script: phpunit --configuration phpunit.xml.dist diff --git a/Jwt/phpunit.xml.dist b/Jwt/phpunit.xml.dist new file mode 100644 index 00000000000..9f85f5baa01 --- /dev/null +++ b/Jwt/phpunit.xml.dist @@ -0,0 +1,19 @@ + + + + + + ./tests + + + diff --git a/Jwt/tests/bootstrap.php b/Jwt/tests/bootstrap.php new file mode 100644 index 00000000000..326c2166757 --- /dev/null +++ b/Jwt/tests/bootstrap.php @@ -0,0 +1,7 @@ + Date: Fri, 30 Aug 2013 17:11:23 -0300 Subject: [PATCH 017/282] Update README.md Added Travis CI build status. --- Jwt/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jwt/README.md b/Jwt/README.md index 2f9e4392854..61989aed9e2 100644 --- a/Jwt/README.md +++ b/Jwt/README.md @@ -1,4 +1,4 @@ - +[![Build Status](https://travis-ci.org/phansys/php-jwt.png?branch=master)](https://travis-ci.org/phansys/php-jwt) PHP-JWT ======= A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should From 6b679c58e15509381e96914a1430e4af124b1f46 Mon Sep 17 00:00:00 2001 From: Phansys Date: Fri, 30 Aug 2013 17:16:09 -0300 Subject: [PATCH 018/282] Added autoload.php.dist for test suite. --- Jwt/tests/JWTTest.php | 2 -- Jwt/tests/autoload.php.dist | 17 +++++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 Jwt/tests/autoload.php.dist diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index db50ee61781..a6490517d1b 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -1,7 +1,5 @@ Date: Mon, 2 Sep 2013 11:07:39 -0300 Subject: [PATCH 019/282] Updated README.md. --- Jwt/README.md | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/Jwt/README.md b/Jwt/README.md index 61989aed9e2..d0be097fb74 100644 --- a/Jwt/README.md +++ b/Jwt/README.md @@ -4,24 +4,32 @@ PHP-JWT A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the [current spec](http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-06) +Installation +------------ + +Use composer to manage your dependencies and download PHP-JWT: + +```bash +php composer require firebase/php-jwt +``` + Example ------- ```php "http://example.org", "aud" => "http://example.com", "iat" => 1356999524, "nbf" => 1357000000 - ); +); - $jwt = JWT::encode($token, $key); - $decoded = JWT::decode($jwt, $key); +$jwt = JWT::encode($token, $key); +$decoded = JWT::decode($jwt, $key); - print_r($decoded); +print_r($decoded); ?> ``` @@ -30,12 +38,12 @@ Tests Run the tests using phpunit: ```bash - $ pear install PHPUnit - $ phpunit tests/ - PHPUnit 3.7.10 by Sebastian Bergmann. - ..... - Time: 0 seconds, Memory: 2.50Mb - OK (5 tests, 5 assertions) +$ pear install PHPUnit +$ phpunit --configuration phpunit.xml.dist +PHPUnit 3.7.10 by Sebastian Bergmann. +..... +Time: 0 seconds, Memory: 2.50Mb +OK (5 tests, 5 assertions) ``` License From 4408a5eb7fec932dfca6ee351b304535ac194438 Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Tue, 3 Sep 2013 13:55:18 -0700 Subject: [PATCH 020/282] Fix build status link in README --- Jwt/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Jwt/README.md b/Jwt/README.md index d0be097fb74..095e456e3dc 100644 --- a/Jwt/README.md +++ b/Jwt/README.md @@ -1,4 +1,5 @@ -[![Build Status](https://travis-ci.org/phansys/php-jwt.png?branch=master)](https://travis-ci.org/phansys/php-jwt) +[![Build Status](https://travis-ci.org/firebase/php-jwt.png?branch=master)](https://travis-ci.org/firebase/php-jwt) + PHP-JWT ======= A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should From f1d8c47905dadc9c4bdb86cf5ca3fdab089001bc Mon Sep 17 00:00:00 2001 From: Redian Date: Thu, 13 Mar 2014 16:45:43 +0000 Subject: [PATCH 021/282] Checking the token expiry time Checking if our token has expired. --- Jwt/Authentication/JWT.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Jwt/Authentication/JWT.php b/Jwt/Authentication/JWT.php index 7a7b4a0e99b..afceed61a7a 100644 --- a/Jwt/Authentication/JWT.php +++ b/Jwt/Authentication/JWT.php @@ -61,6 +61,11 @@ public static function decode($jwt, $key = null, $verify = true) if ($sig != JWT::sign("$headb64.$bodyb64", $key, $header->alg)) { throw new UnexpectedValueException('Signature verification failed'); } + // Check token expiry time if defined. + if (isset($payload->exp) && time() >= $payload->exp){ + throw new UnexpectedValueException('Expired Token'); + } + } return $payload; } From 23797fbb59f93b315b977a46449ce0afb2ecefd8 Mon Sep 17 00:00:00 2001 From: Redian Ibra Date: Fri, 11 Apr 2014 15:14:48 +0100 Subject: [PATCH 022/282] Writing some unittests to make sure the token expiry logic works as it should. --- Jwt/tests/JWTTest.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index a6490517d1b..ae65f025f7c 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -28,6 +28,25 @@ function testMalformedJsonThrowsException() { $this->setExpectedException('DomainException'); JWT::jsonDecode('this is not valid JSON string'); } + + function testExpiredToken(){ + $this->setExpectedException('UnexpectedValueException'); + $payload = array( + "message"=> "abc", + "exp"=> time()-20); // time in the past + $encoded = JWT::encode($payload, 'my_key'); + JWT::decode($encoded); + } + + function testValidToken(){ + $payload = array( + "message"=> "abc", + "exp"=> time()+20); // time in the future + $encoded = JWT::encode($payload, 'my_key'); + $decoded = JWT::decode($encoded, 'my_key'); + $this->assertEquals($decoded->message, 'abc'); + } + } ?> From 5d98318ad90223923eba4873f67814ce83d67f03 Mon Sep 17 00:00:00 2001 From: 4026 Date: Tue, 17 Jun 2014 18:06:09 +0100 Subject: [PATCH 023/282] Adding preprocessing of the JSON string when decoding to prevent large integers being converted to floats. --- Jwt/Authentication/JWT.php | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Jwt/Authentication/JWT.php b/Jwt/Authentication/JWT.php index 7a7b4a0e99b..13827804ac5 100644 --- a/Jwt/Authentication/JWT.php +++ b/Jwt/Authentication/JWT.php @@ -126,7 +126,20 @@ public static function sign($msg, $key, $method = 'HS256') */ public static function jsonDecode($input) { - $obj = json_decode($input); + if (version_compare(PHP_VERSION, '5.4.0', '>=')) { + /* In PHP >=5.4.0, json_decode() accepts an options parameter, that allows you to specify that large ints (like Steam + * Transaction IDs) should be treated as strings, rather than the PHP default behaviour of converting them to floats. + */ + $obj = json_decode($input, false, 512, JSON_BIGINT_AS_STRING); + } else { + /* Not all servers will support that, however, so for older versions we must manually detect large ints in the JSON + * string and quote them (thus converting them to strings) before decoding, hence the preg_replace() call. + */ + $max_int_length = strlen((string) PHP_INT_MAX) - 1; + $json_without_bigints = preg_replace('/:\s*(\d{'.$max_int_length.',})/', ': "$1"', $input); + $obj = json_decode($json_without_bigints, true); + } + if (function_exists('json_last_error') && $errno = json_last_error()) { JWT::_handleJsonError($errno); } else if ($obj === null && $input !== 'null') { From b034ff39829717e4aa5d45a62d99adba9b4b0a8c Mon Sep 17 00:00:00 2001 From: 4026 Date: Wed, 18 Jun 2014 09:53:55 +0100 Subject: [PATCH 024/282] Correcting the bigint fix to produce objects rather than associative arrays in php versions < 5.4.0. --- Jwt/Authentication/JWT.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jwt/Authentication/JWT.php b/Jwt/Authentication/JWT.php index 13827804ac5..d0b71b97e21 100644 --- a/Jwt/Authentication/JWT.php +++ b/Jwt/Authentication/JWT.php @@ -137,7 +137,7 @@ public static function jsonDecode($input) */ $max_int_length = strlen((string) PHP_INT_MAX) - 1; $json_without_bigints = preg_replace('/:\s*(\d{'.$max_int_length.',})/', ': "$1"', $input); - $obj = json_decode($json_without_bigints, true); + $obj = json_decode($json_without_bigints); } if (function_exists('json_last_error') && $errno = json_last_error()) { From ab0064ee0894b263641545abbea6fc87feca9948 Mon Sep 17 00:00:00 2001 From: Chris Raynor Date: Wed, 18 Jun 2014 10:29:52 -0700 Subject: [PATCH 025/282] whitespace cleanup --- Jwt/Authentication/JWT.php | 1 - 1 file changed, 1 deletion(-) diff --git a/Jwt/Authentication/JWT.php b/Jwt/Authentication/JWT.php index afceed61a7a..aabc65b0766 100644 --- a/Jwt/Authentication/JWT.php +++ b/Jwt/Authentication/JWT.php @@ -65,7 +65,6 @@ public static function decode($jwt, $key = null, $verify = true) if (isset($payload->exp) && time() >= $payload->exp){ throw new UnexpectedValueException('Expired Token'); } - } return $payload; } From e0e711b6f47486a999bc69b81bb1489baa26fa32 Mon Sep 17 00:00:00 2001 From: John LeSueur Date: Thu, 19 Jun 2014 08:15:02 -0600 Subject: [PATCH 026/282] Update JWT.php Add a method to retrieve the header. OpenIdConnect tends to add a "kid" to the header that is used to determine which key to use for verification. Getting header information allows you to figure out which key to pass to "decode". Other possible ways to handle this, allow $key passed to decode to be an array of keys. For now, let's just let people get to the header. --- Jwt/Authentication/JWT.php | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/Jwt/Authentication/JWT.php b/Jwt/Authentication/JWT.php index cd72a7444d0..771f223df2f 100644 --- a/Jwt/Authentication/JWT.php +++ b/Jwt/Authentication/JWT.php @@ -26,6 +26,28 @@ */ class JWT { + + /** + * Returns just the header portion of the jwt. This allows + * you to determine which key should be used to verify + * the jwt, using the "kid" field + * + * @param string $jwt + * + * @return object The JWT's header object, with fields "typ","alg", and optionally "kid" + */ + public static function decodeHeader($jwt) { + $tks = explode('.', $jwt); + if (count($tks) != 3) { + throw new UnexpectedValueException('Wrong number of segments'); + } + list($headb64, $bodyb64, $cryptob64) = $tks; + if (null === ($header = JWT::jsonDecode(JWT::urlsafeB64Decode($headb64)))) { + throw new UnexpectedValueException('Invalid segment encoding'); + } + return $header; + } + /** * Decodes a JWT string into a PHP object. * @@ -117,6 +139,7 @@ public static function sign($msg, $key, $method = 'HS256') if (empty($methods[$method])) { throw new DomainException('Algorithm not supported'); } + return hash_hmac($methods[$method], $msg, $key, true); } From 1020b9234659a4ff8397e05e892c8946f380660d Mon Sep 17 00:00:00 2001 From: John LeSueur Date: Thu, 19 Jun 2014 11:03:53 -0600 Subject: [PATCH 027/282] Update JWT.php Try to add the RS256 algorithm. --- Jwt/Authentication/JWT.php | 50 ++++++++++++++++++++++++++++++-------- 1 file changed, 40 insertions(+), 10 deletions(-) diff --git a/Jwt/Authentication/JWT.php b/Jwt/Authentication/JWT.php index 771f223df2f..efb2ef0ce55 100644 --- a/Jwt/Authentication/JWT.php +++ b/Jwt/Authentication/JWT.php @@ -26,7 +26,12 @@ */ class JWT { - + static $methods = array( + 'HS256' => array('hash_hmac', 'SHA256'), + 'HS512' => array('hash_hmac', 'SHA512'), + 'HS384' => array('hash_hmac', 'SHA384'), + 'RS256' => array('openssl', 'SHA256'), + ); /** * Returns just the header portion of the jwt. This allows * you to determine which key should be used to verify @@ -80,7 +85,7 @@ public static function decode($jwt, $key = null, $verify = true) if (empty($header->alg)) { throw new DomainException('Empty algorithm'); } - if ($sig != JWT::sign("$headb64.$bodyb64", $key, $header->alg)) { + if (!JWT::verify("$headb64.$bodyb64", $sig, $key, $header->alg)) { throw new UnexpectedValueException('Signature verification failed'); } // Check token expiry time if defined. @@ -131,16 +136,41 @@ public static function encode($payload, $key, $algo = 'HS256') */ public static function sign($msg, $key, $method = 'HS256') { - $methods = array( - 'HS256' => 'sha256', - 'HS384' => 'sha384', - 'HS512' => 'sha512', - ); - if (empty($methods[$method])) { + if (empty(self::$methods[$method])) { + throw new DomainException('Algorithm not supported'); + } + list($function, $algo) = self::$methods[$method]; + switch($function) { + case 'hash_hmac': + return hash_hmac($algo, $msg, $key, true); + case 'openssl': + $signature = ''; + $success = openssl_sign($msg, $signature, $key, $algo); + if(!$success) { + throw new DomainException("OpenSSL unable to sign data"); + } else { + return $signature; + } + } + } + + public static function verify($msg, $signature $key, $method = 'HS256') { + if (empty(self::$methods[$method])) { throw new DomainException('Algorithm not supported'); } - - return hash_hmac($methods[$method], $msg, $key, true); + list($function, $algo) = self::$methods[$method]; + switch($function) { + case 'openssl': + $success = openssl_verify($msg, $signature, $key, $algo); + if(!$success) { + throw new DomainException("OpenSSL unable to sign data"); + } else { + return $signature; + } + case 'hash_hmac': + default: + return $signature === hash_hmac($algo, $msg, $key, true); + } } /** From c61a540124b3096c1a0501a4e1fb888042cb4fc9 Mon Sep 17 00:00:00 2001 From: John LeSueur Date: Thu, 19 Jun 2014 14:54:22 -0600 Subject: [PATCH 028/282] Update JWT.php --- Jwt/Authentication/JWT.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jwt/Authentication/JWT.php b/Jwt/Authentication/JWT.php index efb2ef0ce55..875a177a3da 100644 --- a/Jwt/Authentication/JWT.php +++ b/Jwt/Authentication/JWT.php @@ -154,7 +154,7 @@ public static function sign($msg, $key, $method = 'HS256') } } - public static function verify($msg, $signature $key, $method = 'HS256') { + public static function verify($msg, $signature, $key, $method = 'HS256') { if (empty(self::$methods[$method])) { throw new DomainException('Algorithm not supported'); } From 6ea7a08d2048b91c49c00b1c5ecfe6bd5d00acf4 Mon Sep 17 00:00:00 2001 From: Chris Raynor Date: Fri, 20 Jun 2014 12:26:34 -0700 Subject: [PATCH 029/282] This fixes firebase/php-jwt#8 by checking if JSON_BIGINT_AS_STRING is defined and falling back to the pre 5.4 code path otherwise --- Jwt/Authentication/JWT.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jwt/Authentication/JWT.php b/Jwt/Authentication/JWT.php index cd72a7444d0..0d226f4af84 100644 --- a/Jwt/Authentication/JWT.php +++ b/Jwt/Authentication/JWT.php @@ -130,7 +130,7 @@ public static function sign($msg, $key, $method = 'HS256') */ public static function jsonDecode($input) { - if (version_compare(PHP_VERSION, '5.4.0', '>=')) { + if (version_compare(PHP_VERSION, '5.4.0', '>=') and defined('JSON_BIGINT_AS_STRING')) { /* In PHP >=5.4.0, json_decode() accepts an options parameter, that allows you to specify that large ints (like Steam * Transaction IDs) should be treated as strings, rather than the PHP default behaviour of converting them to floats. */ From 393377cee6f2005e4013bb54e0d682d5a7755e0a Mon Sep 17 00:00:00 2001 From: Chris Raynor Date: Fri, 20 Jun 2014 12:29:48 -0700 Subject: [PATCH 030/282] style fix --- Jwt/Authentication/JWT.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jwt/Authentication/JWT.php b/Jwt/Authentication/JWT.php index 0d226f4af84..8eefaadc272 100644 --- a/Jwt/Authentication/JWT.php +++ b/Jwt/Authentication/JWT.php @@ -130,7 +130,7 @@ public static function sign($msg, $key, $method = 'HS256') */ public static function jsonDecode($input) { - if (version_compare(PHP_VERSION, '5.4.0', '>=') and defined('JSON_BIGINT_AS_STRING')) { + if (version_compare(PHP_VERSION, '5.4.0', '>=') && defined('JSON_BIGINT_AS_STRING')) { /* In PHP >=5.4.0, json_decode() accepts an options parameter, that allows you to specify that large ints (like Steam * Transaction IDs) should be treated as strings, rather than the PHP default behaviour of converting them to floats. */ From 41314d489d16140a596b083829819b6236d4601b Mon Sep 17 00:00:00 2001 From: John LeSueur Date: Mon, 23 Jun 2014 15:06:27 +0000 Subject: [PATCH 031/282] 1. Allow encode to take a "kid" parameter 2. Allow decode to take an array of keys, and use the "kid" to look up the key. 3. Add RS256 as an algorithm 4. Add tests for the above --- Jwt/Authentication/JWT.php | 47 ++++++++++++++++++-------------------- Jwt/tests/JWTTest.php | 19 +++++++++++++++ 2 files changed, 41 insertions(+), 25 deletions(-) diff --git a/Jwt/Authentication/JWT.php b/Jwt/Authentication/JWT.php index 875a177a3da..aa4995df56e 100644 --- a/Jwt/Authentication/JWT.php +++ b/Jwt/Authentication/JWT.php @@ -32,32 +32,12 @@ class JWT 'HS384' => array('hash_hmac', 'SHA384'), 'RS256' => array('openssl', 'SHA256'), ); - /** - * Returns just the header portion of the jwt. This allows - * you to determine which key should be used to verify - * the jwt, using the "kid" field - * - * @param string $jwt - * - * @return object The JWT's header object, with fields "typ","alg", and optionally "kid" - */ - public static function decodeHeader($jwt) { - $tks = explode('.', $jwt); - if (count($tks) != 3) { - throw new UnexpectedValueException('Wrong number of segments'); - } - list($headb64, $bodyb64, $cryptob64) = $tks; - if (null === ($header = JWT::jsonDecode(JWT::urlsafeB64Decode($headb64)))) { - throw new UnexpectedValueException('Invalid segment encoding'); - } - return $header; - } /** * Decodes a JWT string into a PHP object. * * @param string $jwt The JWT - * @param string|null $key The secret key + * @param string|Array|null $key The secret key, or map of keys * @param bool $verify Don't skip verification process * * @return object The JWT's payload as a PHP object @@ -85,6 +65,11 @@ public static function decode($jwt, $key = null, $verify = true) if (empty($header->alg)) { throw new DomainException('Empty algorithm'); } + if (is_array($key) && !isset($header->kid)) { + throw new DomainException('"kid" empty, unable to lookup correct key'); + } elseif(is_array($key) && isset($header->kid)) { + $key = $key[$header->kid]; + } if (!JWT::verify("$headb64.$bodyb64", $sig, $key, $header->alg)) { throw new UnexpectedValueException('Signature verification failed'); } @@ -108,10 +93,12 @@ public static function decode($jwt, $key = null, $verify = true) * @uses jsonEncode * @uses urlsafeB64Encode */ - public static function encode($payload, $key, $algo = 'HS256') + public static function encode($payload, $key, $algo = 'HS256', $keyId = null) { $header = array('typ' => 'JWT', 'alg' => $algo); - + if($keyId !== null) { + $header['kid'] = $keyId; + } $segments = array(); $segments[] = JWT::urlsafeB64Encode(JWT::jsonEncode($header)); $segments[] = JWT::urlsafeB64Encode(JWT::jsonEncode($payload)); @@ -127,7 +114,7 @@ public static function encode($payload, $key, $algo = 'HS256') * Sign a string with a given key and algorithm. * * @param string $msg The message to sign - * @param string $key The secret key + * @param string|resource $key The secret key * @param string $method The signing algorithm. Supported * algorithms are 'HS256', 'HS384' and 'HS512' * @@ -154,6 +141,16 @@ public static function sign($msg, $key, $method = 'HS256') } } + /** + * Verify a signature with the mesage, key and method. Not all methods + * are symmetric, so we must have a separate verify and sign method. + * @param string $msg the original message + * @param string $signature + * @param string|resource $key for HS*, a string key works. for RS*, must be a resource of an openssl public key + * @param string $method + * @return bool + * @throws DomainException Invalid Algorithm or OpenSSL failure + */ public static function verify($msg, $signature, $key, $method = 'HS256') { if (empty(self::$methods[$method])) { throw new DomainException('Algorithm not supported'); @@ -163,7 +160,7 @@ public static function verify($msg, $signature, $key, $method = 'HS256') { case 'openssl': $success = openssl_verify($msg, $signature, $key, $algo); if(!$success) { - throw new DomainException("OpenSSL unable to sign data"); + throw new DomainException("OpenSSL unable to verify data: " . openssl_error_string()); } else { return $signature; } diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index ae65f025f7c..5aa5dc76304 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -46,6 +46,25 @@ function testValidToken(){ $decoded = JWT::decode($encoded, 'my_key'); $this->assertEquals($decoded->message, 'abc'); } + + function testRSEncodeDecode() { + $privKey = openssl_pkey_new(array('digest_alg' => 'sha256', + 'private_key_bits' => 1024, + 'private_key_type' => OPENSSL_KEYTYPE_RSA)); + $msg = JWT::encode('abc',$privKey, 'RS256'); + $pubKey = openssl_pkey_get_details($privKey); + $pubKey = $pubKey['key']; + $decoded = JWT::decode($msg, $pubKey, true); + $this->assertEquals($decoded, 'abc'); + } + + function testKIDChooser() { + $keys = array('1' => 'my_key', '2' => 'my_key2'); + $msg = JWT::encode('abc', $keys['1'], 'HS256', '1'); + $decoded = JWT::decode($msg, $keys, true); + $this->assertEquals($decoded, 'abc'); + } + } From 7a143edba38240d1ebe6f93253c14e96ec8ceccd Mon Sep 17 00:00:00 2001 From: John LeSueur Date: Mon, 23 Jun 2014 18:38:44 +0000 Subject: [PATCH 032/282] Updates for some whitespace and other minor issues. --- Jwt/Authentication/JWT.php | 12 +++++++----- Jwt/tests/JWTTest.php | 6 +++--- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/Jwt/Authentication/JWT.php b/Jwt/Authentication/JWT.php index aa4995df56e..d82e2cc75b5 100644 --- a/Jwt/Authentication/JWT.php +++ b/Jwt/Authentication/JWT.php @@ -65,10 +65,12 @@ public static function decode($jwt, $key = null, $verify = true) if (empty($header->alg)) { throw new DomainException('Empty algorithm'); } - if (is_array($key) && !isset($header->kid)) { - throw new DomainException('"kid" empty, unable to lookup correct key'); - } elseif(is_array($key) && isset($header->kid)) { - $key = $key[$header->kid]; + if (is_array($key)) { + if(isset($header->kid)) { + $key = $key[$header->kid]; + } else { + throw new DomainException('"kid" empty, unable to lookup correct key'); + } } if (!JWT::verify("$headb64.$bodyb64", $sig, $key, $header->alg)) { throw new UnexpectedValueException('Signature verification failed'); @@ -116,7 +118,7 @@ public static function encode($payload, $key, $algo = 'HS256', $keyId = null) * @param string $msg The message to sign * @param string|resource $key The secret key * @param string $method The signing algorithm. Supported - * algorithms are 'HS256', 'HS384' and 'HS512' + * algorithms are 'HS256', 'HS384', 'HS512' and 'RS256' * * @return string An encrypted message * @throws DomainException Unsupported algorithm was specified diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index 5aa5dc76304..bdd851461d9 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -47,8 +47,8 @@ function testValidToken(){ $this->assertEquals($decoded->message, 'abc'); } - function testRSEncodeDecode() { - $privKey = openssl_pkey_new(array('digest_alg' => 'sha256', + function testRSEncodeDecode(){ + $privKey = openssl_pkey_new(array('digest_alg' => 'sha256', 'private_key_bits' => 1024, 'private_key_type' => OPENSSL_KEYTYPE_RSA)); $msg = JWT::encode('abc',$privKey, 'RS256'); @@ -58,7 +58,7 @@ function testRSEncodeDecode() { $this->assertEquals($decoded, 'abc'); } - function testKIDChooser() { + function testKIDChooser(){ $keys = array('1' => 'my_key', '2' => 'my_key2'); $msg = JWT::encode('abc', $keys['1'], 'HS256', '1'); $decoded = JWT::decode($msg, $keys, true); From f9d3beeeaf4912d64f8982bf48cb5846f5204fac Mon Sep 17 00:00:00 2001 From: Chris Raynor Date: Wed, 25 Jun 2014 12:30:42 -0700 Subject: [PATCH 033/282] whitespace cleanup --- Jwt/Authentication/JWT.php | 37 +++++++++++++------------------------ Jwt/tests/JWTTest.php | 19 +++++++++---------- 2 files changed, 22 insertions(+), 34 deletions(-) diff --git a/Jwt/Authentication/JWT.php b/Jwt/Authentication/JWT.php index 11b1bde8fac..41458698a9f 100644 --- a/Jwt/Authentication/JWT.php +++ b/Jwt/Authentication/JWT.php @@ -13,17 +13,6 @@ * @license http://opensource.org/licenses/BSD-3-Clause 3-clause BSD * @link https://github.com/firebase/php-jwt */ -/** - * JSON Web Token implementation, based on this spec: - * http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-06 - * - * @category Authentication - * @package Authentication_JWT - * @author Neuman Vong - * @author Anant Narayanan - * @license http://opensource.org/licenses/BSD-3-Clause 3-clause BSD - * @link https://github.com/firebase/php-jwt - */ class JWT { static $methods = array( @@ -32,13 +21,13 @@ class JWT 'HS384' => array('hash_hmac', 'SHA384'), 'RS256' => array('openssl', 'SHA256'), ); - + /** * Decodes a JWT string into a PHP object. * - * @param string $jwt The JWT - * @param string|Array|null $key The secret key, or map of keys - * @param bool $verify Don't skip verification process + * @param string $jwt The JWT + * @param string|Array|null $key The secret key, or map of keys + * @param bool $verify Don't skip verification process * * @return object The JWT's payload as a PHP object * @throws UnexpectedValueException Provided JWT was invalid @@ -71,7 +60,7 @@ public static function decode($jwt, $key = null, $verify = true) } else { throw new DomainException('"kid" empty, unable to lookup correct key'); } - } + } if (!JWT::verify("$headb64.$bodyb64", $sig, $key, $header->alg)) { throw new UnexpectedValueException('Signature verification failed'); } @@ -98,9 +87,9 @@ public static function decode($jwt, $key = null, $verify = true) public static function encode($payload, $key, $algo = 'HS256', $keyId = null) { $header = array('typ' => 'JWT', 'alg' => $algo); - if($keyId !== null) { - $header['kid'] = $keyId; - } + if($keyId !== null) { + $header['kid'] = $keyId; + } $segments = array(); $segments[] = JWT::urlsafeB64Encode(JWT::jsonEncode($header)); $segments[] = JWT::urlsafeB64Encode(JWT::jsonEncode($payload)); @@ -115,10 +104,10 @@ public static function encode($payload, $key, $algo = 'HS256', $keyId = null) /** * Sign a string with a given key and algorithm. * - * @param string $msg The message to sign - * @param string|resource $key The secret key - * @param string $method The signing algorithm. Supported - * algorithms are 'HS256', 'HS384', 'HS512' and 'RS256' + * @param string $msg The message to sign + * @param string|resource $key The secret key + * @param string $method The signing algorithm. Supported algorithms + * are 'HS256', 'HS384', 'HS512' and 'RS256' * * @return string An encrypted message * @throws DomainException Unsupported algorithm was specified @@ -142,7 +131,7 @@ public static function sign($msg, $key, $method = 'HS256') } } } - + /** * Verify a signature with the mesage, key and method. Not all methods * are symmetric, so we must have a separate verify and sign method. diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index bdd851461d9..ee131d4edb2 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -29,43 +29,42 @@ function testMalformedJsonThrowsException() { JWT::jsonDecode('this is not valid JSON string'); } - function testExpiredToken(){ + function testExpiredToken() { $this->setExpectedException('UnexpectedValueException'); $payload = array( - "message"=> "abc", - "exp"=> time()-20); // time in the past + "message" => "abc", + "exp" => time() - 20); // time in the past $encoded = JWT::encode($payload, 'my_key'); JWT::decode($encoded); } - function testValidToken(){ + function testValidToken() { $payload = array( - "message"=> "abc", - "exp"=> time()+20); // time in the future + "message" => "abc", + "exp" => time() + 20); // time in the future $encoded = JWT::encode($payload, 'my_key'); $decoded = JWT::decode($encoded, 'my_key'); $this->assertEquals($decoded->message, 'abc'); } - function testRSEncodeDecode(){ + function testRSEncodeDecode() { $privKey = openssl_pkey_new(array('digest_alg' => 'sha256', 'private_key_bits' => 1024, 'private_key_type' => OPENSSL_KEYTYPE_RSA)); - $msg = JWT::encode('abc',$privKey, 'RS256'); + $msg = JWT::encode('abc', $privKey, 'RS256'); $pubKey = openssl_pkey_get_details($privKey); $pubKey = $pubKey['key']; $decoded = JWT::decode($msg, $pubKey, true); $this->assertEquals($decoded, 'abc'); } - function testKIDChooser(){ + function testKIDChooser() { $keys = array('1' => 'my_key', '2' => 'my_key2'); $msg = JWT::encode('abc', $keys['1'], 'HS256', '1'); $decoded = JWT::decode($msg, $keys, true); $this->assertEquals($decoded, 'abc'); } - } ?> From 71ec7ec2c40c3d694d57ada9a94d65332a45cb9d Mon Sep 17 00:00:00 2001 From: Chris Raynor Date: Wed, 25 Jun 2014 13:50:56 -0700 Subject: [PATCH 034/282] This fixes firebase/php-jwt#9 - Improve check for offending json-c implementation with missing implementation of JSON_BIGINT_AS_STRING on 64 bit systems --- Jwt/Authentication/JWT.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jwt/Authentication/JWT.php b/Jwt/Authentication/JWT.php index 8eefaadc272..65be7a1d774 100644 --- a/Jwt/Authentication/JWT.php +++ b/Jwt/Authentication/JWT.php @@ -130,7 +130,7 @@ public static function sign($msg, $key, $method = 'HS256') */ public static function jsonDecode($input) { - if (version_compare(PHP_VERSION, '5.4.0', '>=') && defined('JSON_BIGINT_AS_STRING')) { + if (version_compare(PHP_VERSION, '5.4.0', '>=') && !(defined('JSON_C_VERSION') && PHP_INT_SIZE > 4)) { /* In PHP >=5.4.0, json_decode() accepts an options parameter, that allows you to specify that large ints (like Steam * Transaction IDs) should be treated as strings, rather than the PHP default behaviour of converting them to floats. */ @@ -140,7 +140,7 @@ public static function jsonDecode($input) * string and quote them (thus converting them to strings) before decoding, hence the preg_replace() call. */ $max_int_length = strlen((string) PHP_INT_MAX) - 1; - $json_without_bigints = preg_replace('/:\s*(\d{'.$max_int_length.',})/', ': "$1"', $input); + $json_without_bigints = preg_replace('/:\s*(-?\d{'.$max_int_length.',})/', ': "$1"', $input); $obj = json_decode($json_without_bigints); } From bb79c01702b4aec954154a3bf3aa6868121a176b Mon Sep 17 00:00:00 2001 From: Chris Raynor Date: Mon, 25 Aug 2014 08:28:46 -0700 Subject: [PATCH 035/282] adding documentation around casting the result to an associative array --- Jwt/README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Jwt/README.md b/Jwt/README.md index 095e456e3dc..353a177bab1 100644 --- a/Jwt/README.md +++ b/Jwt/README.md @@ -31,6 +31,14 @@ $jwt = JWT::encode($token, $key); $decoded = JWT::decode($jwt, $key); print_r($decoded); + +/* + NOTE: This will now be an object instead of an associative array. To get + an associative array, you will need to cast it as such: +*/ + +$decoded_array = (array) $decoded; + ?> ``` From bcfe0585da742a7da3b8297a246420b2ce56288d Mon Sep 17 00:00:00 2001 From: Chris Raynor Date: Thu, 28 Aug 2014 10:14:17 -0700 Subject: [PATCH 036/282] adding version for a stable release --- Jwt/composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/Jwt/composer.json b/Jwt/composer.json index 292edbf6dc2..940deab3969 100644 --- a/Jwt/composer.json +++ b/Jwt/composer.json @@ -2,6 +2,7 @@ "name": "firebase/php-jwt", "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", "homepage": "https://github.com/firebase/php-jwt", + "version": "1.0.0", "authors": [ { "name": "Neuman Vong", From 8c781932d5e4fce9d2e6024616ce97c941ef6af4 Mon Sep 17 00:00:00 2001 From: Chris Raynor Date: Thu, 28 Aug 2014 10:20:29 -0700 Subject: [PATCH 037/282] removing version --- Jwt/composer.json | 1 - 1 file changed, 1 deletion(-) diff --git a/Jwt/composer.json b/Jwt/composer.json index 940deab3969..292edbf6dc2 100644 --- a/Jwt/composer.json +++ b/Jwt/composer.json @@ -2,7 +2,6 @@ "name": "firebase/php-jwt", "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", "homepage": "https://github.com/firebase/php-jwt", - "version": "1.0.0", "authors": [ { "name": "Neuman Vong", From 465abb78d6f9793e0423f4f81aa9bb68f3db8890 Mon Sep 17 00:00:00 2001 From: Chris Raynor Date: Tue, 9 Sep 2014 18:40:58 -0700 Subject: [PATCH 038/282] Switching to a constant-time string comparison in the verify method --- Jwt/Authentication/JWT.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Jwt/Authentication/JWT.php b/Jwt/Authentication/JWT.php index 38a4f7e134a..90c1ac7a29e 100644 --- a/Jwt/Authentication/JWT.php +++ b/Jwt/Authentication/JWT.php @@ -157,7 +157,16 @@ public static function verify($msg, $signature, $key, $method = 'HS256') { } case 'hash_hmac': default: - return $signature === hash_hmac($algo, $msg, $key, true); + $hash = hash_hmac($algo, $msg, $key, true); + $len = min(strlen($signature), strlen($hash)); + + $status = 0; + for ($i = 0; $i < $len; $i++) { + $status |= (ord($signature[$i]) ^ ord($hash[$i])); + } + $status |= (strlen($signature) ^ strlen($hash)); + + return ($status === 0); } } From a87c62a75e386c1a9e9ac45632b4e41a3484f699 Mon Sep 17 00:00:00 2001 From: Chris Raynor Date: Tue, 9 Sep 2014 18:45:30 -0700 Subject: [PATCH 039/282] adding test for invalid signature --- Jwt/tests/JWTTest.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index ee131d4edb2..21498623a34 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -47,6 +47,15 @@ function testValidToken() { $this->assertEquals($decoded->message, 'abc'); } + function testInvalidToken() { + $payload = array( + "message" => "abc", + "exp" => time() + 20); // time in the future + $encoded = JWT::encode($payload, 'my_key'); + $this->setExpectedException('UnexpectedValueException'); + $decoded = JWT::decode($encoded, 'my_key2'); + } + function testRSEncodeDecode() { $privKey = openssl_pkey_new(array('digest_alg' => 'sha256', 'private_key_bits' => 1024, From 4ab1f0126929742d9ededc933fa5a1ee6834b75b Mon Sep 17 00:00:00 2001 From: Chris Raynor Date: Thu, 23 Oct 2014 09:21:36 -0700 Subject: [PATCH 040/282] Fixing install command typo --- Jwt/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jwt/README.md b/Jwt/README.md index 353a177bab1..48884624692 100644 --- a/Jwt/README.md +++ b/Jwt/README.md @@ -11,7 +11,7 @@ Installation Use composer to manage your dependencies and download PHP-JWT: ```bash -php composer require firebase/php-jwt +composer require firebase/php-jwt ``` Example From 6bda42f67bda0be809c7baa8e2d9dcccbdef54ee Mon Sep 17 00:00:00 2001 From: Brendan Abbott Date: Thu, 13 Nov 2014 11:56:25 +1000 Subject: [PATCH 041/282] Add a .gitignore file for the vendor/ directory --- Jwt/.gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 Jwt/.gitignore diff --git a/Jwt/.gitignore b/Jwt/.gitignore new file mode 100644 index 00000000000..22d0d82f809 --- /dev/null +++ b/Jwt/.gitignore @@ -0,0 +1 @@ +vendor From ea4a61784c6be152682de5d61304ce36e74dcc6e Mon Sep 17 00:00:00 2001 From: Brendan Abbott Date: Thu, 13 Nov 2014 11:59:03 +1000 Subject: [PATCH 042/282] Minor CS changes --- Jwt/.travis.yml | 2 +- Jwt/Authentication/JWT.php | 39 +++++----- Jwt/tests/JWTTest.php | 142 +++++++++++++++++++----------------- Jwt/tests/autoload.php.dist | 2 +- 4 files changed, 97 insertions(+), 88 deletions(-) diff --git a/Jwt/.travis.yml b/Jwt/.travis.yml index 1ecd967ef7a..d5d716e4517 100644 --- a/Jwt/.travis.yml +++ b/Jwt/.travis.yml @@ -8,5 +8,5 @@ php: before_script: - wget -nc http://getcomposer.org/composer.phar - php composer.phar install - + script: phpunit --configuration phpunit.xml.dist diff --git a/Jwt/Authentication/JWT.php b/Jwt/Authentication/JWT.php index 90c1ac7a29e..08cc91f6fe9 100644 --- a/Jwt/Authentication/JWT.php +++ b/Jwt/Authentication/JWT.php @@ -15,7 +15,7 @@ */ class JWT { - static $methods = array( + public static $methods = array( 'HS256' => array('hash_hmac', 'SHA256'), 'HS512' => array('hash_hmac', 'SHA512'), 'HS384' => array('hash_hmac', 'SHA384'), @@ -32,7 +32,7 @@ class JWT * @return object The JWT's payload as a PHP object * @throws UnexpectedValueException Provided JWT was invalid * @throws DomainException Algorithm was not provided - * + * * @uses jsonDecode * @uses urlsafeB64Decode */ @@ -55,7 +55,7 @@ public static function decode($jwt, $key = null, $verify = true) throw new DomainException('Empty algorithm'); } if (is_array($key)) { - if(isset($header->kid)) { + if (isset($header->kid)) { $key = $key[$header->kid]; } else { throw new DomainException('"kid" empty, unable to lookup correct key'); @@ -65,7 +65,7 @@ public static function decode($jwt, $key = null, $verify = true) throw new UnexpectedValueException('Signature verification failed'); } // Check token expiry time if defined. - if (isset($payload->exp) && time() >= $payload->exp){ + if (isset($payload->exp) && time() >= $payload->exp) { throw new UnexpectedValueException('Expired Token'); } } @@ -87,7 +87,7 @@ public static function decode($jwt, $key = null, $verify = true) public static function encode($payload, $key, $algo = 'HS256', $keyId = null) { $header = array('typ' => 'JWT', 'alg' => $algo); - if($keyId !== null) { + if ($keyId !== null) { $header['kid'] = $keyId; } $segments = array(); @@ -124,7 +124,7 @@ public static function sign($msg, $key, $method = 'HS256') case 'openssl': $signature = ''; $success = openssl_sign($msg, $signature, $key, $algo); - if(!$success) { + if (!$success) { throw new DomainException("OpenSSL unable to sign data"); } else { return $signature; @@ -142,7 +142,8 @@ public static function sign($msg, $key, $method = 'HS256') * @return bool * @throws DomainException Invalid Algorithm or OpenSSL failure */ - public static function verify($msg, $signature, $key, $method = 'HS256') { + public static function verify($msg, $signature, $key, $method = 'HS256') + { if (empty(self::$methods[$method])) { throw new DomainException('Algorithm not supported'); } @@ -150,7 +151,7 @@ public static function verify($msg, $signature, $key, $method = 'HS256') { switch($function) { case 'openssl': $success = openssl_verify($msg, $signature, $key, $algo); - if(!$success) { + if (!$success) { throw new DomainException("OpenSSL unable to verify data: " . openssl_error_string()); } else { return $signature; @@ -181,13 +182,15 @@ public static function verify($msg, $signature, $key, $method = 'HS256') { public static function jsonDecode($input) { if (version_compare(PHP_VERSION, '5.4.0', '>=') && !(defined('JSON_C_VERSION') && PHP_INT_SIZE > 4)) { - /* In PHP >=5.4.0, json_decode() accepts an options parameter, that allows you to specify that large ints (like Steam - * Transaction IDs) should be treated as strings, rather than the PHP default behaviour of converting them to floats. + /** In PHP >=5.4.0, json_decode() accepts an options parameter, that allows you + * to specify that large ints (like Steam Transaction IDs) should be treated as + * strings, rather than the PHP default behaviour of converting them to floats. */ $obj = json_decode($input, false, 512, JSON_BIGINT_AS_STRING); } else { - /* Not all servers will support that, however, so for older versions we must manually detect large ints in the JSON - * string and quote them (thus converting them to strings) before decoding, hence the preg_replace() call. + /** Not all servers will support that, however, so for older versions we must + * manually detect large ints in the JSON string and quote them (thus converting + *them to strings) before decoding, hence the preg_replace() call. */ $max_int_length = strlen((string) PHP_INT_MAX) - 1; $json_without_bigints = preg_replace('/:\s*(-?\d{'.$max_int_length.',})/', ': "$1"', $input); @@ -195,8 +198,8 @@ public static function jsonDecode($input) } if (function_exists('json_last_error') && $errno = json_last_error()) { - JWT::_handleJsonError($errno); - } else if ($obj === null && $input !== 'null') { + JWT::handleJsonError($errno); + } elseif ($obj === null && $input !== 'null') { throw new DomainException('Null result with non-null input'); } return $obj; @@ -214,8 +217,8 @@ public static function jsonEncode($input) { $json = json_encode($input); if (function_exists('json_last_error') && $errno = json_last_error()) { - JWT::_handleJsonError($errno); - } else if ($json === 'null' && $input !== null) { + JWT::handleJsonError($errno); + } elseif ($json === 'null' && $input !== null) { throw new DomainException('Null result with non-null input'); } return $json; @@ -257,7 +260,7 @@ public static function urlsafeB64Encode($input) * * @return void */ - private static function _handleJsonError($errno) + private static function handleJsonError($errno) { $messages = array( JSON_ERROR_DEPTH => 'Maximum stack depth exceeded', @@ -270,6 +273,4 @@ private static function _handleJsonError($errno) : 'Unknown JSON error: ' . $errno ); } - } - diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index 21498623a34..979386660f0 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -1,79 +1,87 @@ assertEquals(JWT::decode($msg, 'my_key'), 'abc'); - } +class JWTTest extends PHPUnit_Framework_TestCase +{ + public function testEncodeDecode() + { + $msg = JWT::encode('abc', 'my_key'); + $this->assertEquals(JWT::decode($msg, 'my_key'), 'abc'); + } - function testDecodeFromPython() { - $msg = 'eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.Iio6aHR0cDovL2FwcGxpY2F0aW9uL2NsaWNreT9ibGFoPTEuMjMmZi5vbz00NTYgQUMwMDAgMTIzIg.E_U8X2YpMT5K1cEiT_3-IvBYfrdIFIeVYeOqre_Z5Cg'; - $this->assertEquals( - JWT::decode($msg, 'my_key'), - '*:http://application/clicky?blah=1.23&f.oo=456 AC000 123' - ); - } + public function testDecodeFromPython() + { + $msg = 'eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.Iio6aHR0cDovL2FwcGxpY2F0aW9uL2NsaWNreT9ibGFoPTEuMjMmZi5vbz00NTYgQUMwMDAgMTIzIg.E_U8X2YpMT5K1cEiT_3-IvBYfrdIFIeVYeOqre_Z5Cg'; + $this->assertEquals( + JWT::decode($msg, 'my_key'), + '*:http://application/clicky?blah=1.23&f.oo=456 AC000 123' + ); + } - function testUrlSafeCharacters() { - $encoded = JWT::encode('f?', 'a'); - $this->assertEquals('f?', JWT::decode($encoded, 'a')); - } + public function testUrlSafeCharacters() + { + $encoded = JWT::encode('f?', 'a'); + $this->assertEquals('f?', JWT::decode($encoded, 'a')); + } - function testMalformedUtf8StringsFail() { - $this->setExpectedException('DomainException'); - JWT::encode(pack('c', 128), 'a'); - } + public function testMalformedUtf8StringsFail() + { + $this->setExpectedException('DomainException'); + JWT::encode(pack('c', 128), 'a'); + } - function testMalformedJsonThrowsException() { - $this->setExpectedException('DomainException'); - JWT::jsonDecode('this is not valid JSON string'); - } + public function testMalformedJsonThrowsException() + { + $this->setExpectedException('DomainException'); + JWT::jsonDecode('this is not valid JSON string'); + } - function testExpiredToken() { - $this->setExpectedException('UnexpectedValueException'); - $payload = array( - "message" => "abc", - "exp" => time() - 20); // time in the past - $encoded = JWT::encode($payload, 'my_key'); - JWT::decode($encoded); - } + public function testExpiredToken() + { + $this->setExpectedException('UnexpectedValueException'); + $payload = array( + "message" => "abc", + "exp" => time() - 20); // time in the past + $encoded = JWT::encode($payload, 'my_key'); + JWT::decode($encoded); + } - function testValidToken() { - $payload = array( - "message" => "abc", - "exp" => time() + 20); // time in the future - $encoded = JWT::encode($payload, 'my_key'); - $decoded = JWT::decode($encoded, 'my_key'); - $this->assertEquals($decoded->message, 'abc'); - } + public function testValidToken() + { + $payload = array( + "message" => "abc", + "exp" => time() + 20); // time in the future + $encoded = JWT::encode($payload, 'my_key'); + $decoded = JWT::decode($encoded, 'my_key'); + $this->assertEquals($decoded->message, 'abc'); + } - function testInvalidToken() { - $payload = array( - "message" => "abc", - "exp" => time() + 20); // time in the future - $encoded = JWT::encode($payload, 'my_key'); - $this->setExpectedException('UnexpectedValueException'); - $decoded = JWT::decode($encoded, 'my_key2'); - } + public function testInvalidToken() + { + $payload = array( + "message" => "abc", + "exp" => time() + 20); // time in the future + $encoded = JWT::encode($payload, 'my_key'); + $this->setExpectedException('UnexpectedValueException'); + $decoded = JWT::decode($encoded, 'my_key2'); + } - function testRSEncodeDecode() { - $privKey = openssl_pkey_new(array('digest_alg' => 'sha256', - 'private_key_bits' => 1024, - 'private_key_type' => OPENSSL_KEYTYPE_RSA)); - $msg = JWT::encode('abc', $privKey, 'RS256'); - $pubKey = openssl_pkey_get_details($privKey); - $pubKey = $pubKey['key']; - $decoded = JWT::decode($msg, $pubKey, true); - $this->assertEquals($decoded, 'abc'); - } - - function testKIDChooser() { - $keys = array('1' => 'my_key', '2' => 'my_key2'); - $msg = JWT::encode('abc', $keys['1'], 'HS256', '1'); - $decoded = JWT::decode($msg, $keys, true); - $this->assertEquals($decoded, 'abc'); - } + public function testRSEncodeDecode() + { + $privKey = openssl_pkey_new(array('digest_alg' => 'sha256', + 'private_key_bits' => 1024, + 'private_key_type' => OPENSSL_KEYTYPE_RSA)); + $msg = JWT::encode('abc', $privKey, 'RS256'); + $pubKey = openssl_pkey_get_details($privKey); + $pubKey = $pubKey['key']; + $decoded = JWT::decode($msg, $pubKey, true); + $this->assertEquals($decoded, 'abc'); + } + public function testKIDChooser() + { + $keys = array('1' => 'my_key', '2' => 'my_key2'); + $msg = JWT::encode('abc', $keys['1'], 'HS256', '1'); + $decoded = JWT::decode($msg, $keys, true); + $this->assertEquals($decoded, 'abc'); + } } - -?> diff --git a/Jwt/tests/autoload.php.dist b/Jwt/tests/autoload.php.dist index 45336241bd4..2e4310a0bb0 100644 --- a/Jwt/tests/autoload.php.dist +++ b/Jwt/tests/autoload.php.dist @@ -14,4 +14,4 @@ php composer.phar install Visit http://getcomposer.org/ for more information. '); -} \ No newline at end of file +} From 36f125441de205cf19cd3660c19654f4a542328b Mon Sep 17 00:00:00 2001 From: Brendan Abbott Date: Thu, 13 Nov 2014 12:04:38 +1000 Subject: [PATCH 043/282] Add checking of nbf claim --- Jwt/Authentication/JWT.php | 12 +++++++++++- Jwt/tests/JWTTest.php | 21 +++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/Jwt/Authentication/JWT.php b/Jwt/Authentication/JWT.php index 08cc91f6fe9..1a5916eaa6b 100644 --- a/Jwt/Authentication/JWT.php +++ b/Jwt/Authentication/JWT.php @@ -61,12 +61,22 @@ public static function decode($jwt, $key = null, $verify = true) throw new DomainException('"kid" empty, unable to lookup correct key'); } } + + // Check the signature if (!JWT::verify("$headb64.$bodyb64", $sig, $key, $header->alg)) { throw new UnexpectedValueException('Signature verification failed'); } + // Check token expiry time if defined. if (isset($payload->exp) && time() >= $payload->exp) { - throw new UnexpectedValueException('Expired Token'); + throw new UnexpectedValueException('Expired token'); + } + + // Check if the nbf if it is defined. + if (isset($payload->nbf) && $payload->nbf > time()) { + throw new UnexpectedValueException( + 'Cannot handle token prior to ' . date(DateTime::ISO8601, $payload->nbf) + ); } } return $payload; diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index 979386660f0..0a739f84921 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -45,6 +45,16 @@ public function testExpiredToken() JWT::decode($encoded); } + public function testTooEarlyToken() + { + $this->setExpectedException('UnexpectedValueException'); + $payload = array( + "message" => "abc", + "nbf" => time() + 20); // time in the past + $encoded = JWT::encode($payload, 'my_key'); + JWT::decode($encoded); + } + public function testValidToken() { $payload = array( @@ -55,6 +65,17 @@ public function testValidToken() $this->assertEquals($decoded->message, 'abc'); } + public function testValidTokenWithNbf() + { + $payload = array( + "message" => "abc", + "exp" => time() + 20, // time in the future + "nbf" => time() - 20); + $encoded = JWT::encode($payload, 'my_key'); + $decoded = JWT::decode($encoded, 'my_key'); + $this->assertEquals($decoded->message, 'abc'); + } + public function testInvalidToken() { $payload = array( From 003ee20e6487e0220d0f3eb55fa4db88fc0d5e7f Mon Sep 17 00:00:00 2001 From: Brendan Abbott Date: Thu, 13 Nov 2014 12:46:11 +1000 Subject: [PATCH 044/282] Add specific exceptions for checking claims. Fix bad tests for exp and nbf. RE: firebase/php-jwt#20 --- Jwt/Authentication/JWT.php | 7 ++++--- Jwt/Exceptions/ExpiredException.php | 6 ++++++ Jwt/Exceptions/SignatureInvalidException.php | 6 ++++++ Jwt/Exceptions/TooEarlyException.php | 6 ++++++ Jwt/composer.json | 2 +- Jwt/tests/JWTTest.php | 10 +++++----- 6 files changed, 28 insertions(+), 9 deletions(-) create mode 100644 Jwt/Exceptions/ExpiredException.php create mode 100644 Jwt/Exceptions/SignatureInvalidException.php create mode 100644 Jwt/Exceptions/TooEarlyException.php diff --git a/Jwt/Authentication/JWT.php b/Jwt/Authentication/JWT.php index 1a5916eaa6b..78fe7d2b6f8 100644 --- a/Jwt/Authentication/JWT.php +++ b/Jwt/Authentication/JWT.php @@ -64,21 +64,22 @@ public static function decode($jwt, $key = null, $verify = true) // Check the signature if (!JWT::verify("$headb64.$bodyb64", $sig, $key, $header->alg)) { - throw new UnexpectedValueException('Signature verification failed'); + throw new SignatureInvalidException('Signature verification failed'); } // Check token expiry time if defined. if (isset($payload->exp) && time() >= $payload->exp) { - throw new UnexpectedValueException('Expired token'); + throw new ExpiredException('Expired token'); } // Check if the nbf if it is defined. if (isset($payload->nbf) && $payload->nbf > time()) { - throw new UnexpectedValueException( + throw new TooEarlyException( 'Cannot handle token prior to ' . date(DateTime::ISO8601, $payload->nbf) ); } } + return $payload; } diff --git a/Jwt/Exceptions/ExpiredException.php b/Jwt/Exceptions/ExpiredException.php new file mode 100644 index 00000000000..74b0d5722f7 --- /dev/null +++ b/Jwt/Exceptions/ExpiredException.php @@ -0,0 +1,6 @@ +=5.2.0" }, "autoload": { - "classmap": ["Authentication/"] + "classmap": ["Authentication/", "Exceptions/"] }, "target-dir": "Firebase/PHP-JWT", "minimum-stability": "dev" diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index 0a739f84921..d9abe4e5389 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -37,22 +37,22 @@ public function testMalformedJsonThrowsException() public function testExpiredToken() { - $this->setExpectedException('UnexpectedValueException'); + $this->setExpectedException('ExpiredException'); $payload = array( "message" => "abc", "exp" => time() - 20); // time in the past $encoded = JWT::encode($payload, 'my_key'); - JWT::decode($encoded); + JWT::decode($encoded, 'my_key'); } public function testTooEarlyToken() { - $this->setExpectedException('UnexpectedValueException'); + $this->setExpectedException('TooEarlyException'); $payload = array( "message" => "abc", "nbf" => time() + 20); // time in the past $encoded = JWT::encode($payload, 'my_key'); - JWT::decode($encoded); + JWT::decode($encoded, 'my_key'); } public function testValidToken() @@ -82,7 +82,7 @@ public function testInvalidToken() "message" => "abc", "exp" => time() + 20); // time in the future $encoded = JWT::encode($payload, 'my_key'); - $this->setExpectedException('UnexpectedValueException'); + $this->setExpectedException('SignatureInvalidException'); $decoded = JWT::decode($encoded, 'my_key2'); } From f83e6c07a79c72f6496cae9ee4e56c9f7dedad34 Mon Sep 17 00:00:00 2001 From: drtriumph Date: Fri, 14 Nov 2014 14:05:46 -0800 Subject: [PATCH 045/282] Some minor cleanup from @brendo's PR with a quick rename --- Jwt/Authentication/JWT.php | 2 +- Jwt/Exceptions/BeforeValidException.php | 6 ++++++ Jwt/Exceptions/ExpiredException.php | 2 +- Jwt/Exceptions/SignatureInvalidException.php | 2 +- Jwt/Exceptions/TooEarlyException.php | 6 ------ Jwt/tests/JWTTest.php | 6 +++--- 6 files changed, 12 insertions(+), 12 deletions(-) create mode 100644 Jwt/Exceptions/BeforeValidException.php delete mode 100644 Jwt/Exceptions/TooEarlyException.php diff --git a/Jwt/Authentication/JWT.php b/Jwt/Authentication/JWT.php index 78fe7d2b6f8..76c33bbf3ad 100644 --- a/Jwt/Authentication/JWT.php +++ b/Jwt/Authentication/JWT.php @@ -74,7 +74,7 @@ public static function decode($jwt, $key = null, $verify = true) // Check if the nbf if it is defined. if (isset($payload->nbf) && $payload->nbf > time()) { - throw new TooEarlyException( + throw new BeforeValidException( 'Cannot handle token prior to ' . date(DateTime::ISO8601, $payload->nbf) ); } diff --git a/Jwt/Exceptions/BeforeValidException.php b/Jwt/Exceptions/BeforeValidException.php new file mode 100644 index 00000000000..5a84975e868 --- /dev/null +++ b/Jwt/Exceptions/BeforeValidException.php @@ -0,0 +1,6 @@ +setExpectedException('TooEarlyException'); + $this->setExpectedException('BeforeValidException'); $payload = array( "message" => "abc", - "nbf" => time() + 20); // time in the past + "nbf" => time() + 20); // time in the future $encoded = JWT::encode($payload, 'my_key'); JWT::decode($encoded, 'my_key'); } From 386d56edbf0ed8214c6d2f6524e89187b65cda47 Mon Sep 17 00:00:00 2001 From: drtriumph Date: Fri, 14 Nov 2014 14:29:25 -0800 Subject: [PATCH 046/282] Porting @brylie's change with a small modification --- Jwt/Authentication/JWT.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jwt/Authentication/JWT.php b/Jwt/Authentication/JWT.php index 76c33bbf3ad..7e64b4bfc11 100644 --- a/Jwt/Authentication/JWT.php +++ b/Jwt/Authentication/JWT.php @@ -44,10 +44,10 @@ public static function decode($jwt, $key = null, $verify = true) } list($headb64, $bodyb64, $cryptob64) = $tks; if (null === ($header = JWT::jsonDecode(JWT::urlsafeB64Decode($headb64)))) { - throw new UnexpectedValueException('Invalid segment encoding'); + throw new UnexpectedValueException('Invalid header encoding'); } if (null === $payload = JWT::jsonDecode(JWT::urlsafeB64Decode($bodyb64))) { - throw new UnexpectedValueException('Invalid segment encoding'); + throw new UnexpectedValueException('Invalid claims encoding'); } $sig = JWT::urlsafeB64Decode($cryptob64); if ($verify) { From 0e5198c820fd8a2cd79c9f545052bbb2041da2bc Mon Sep 17 00:00:00 2001 From: Brendan Abbott Date: Mon, 17 Nov 2014 11:27:21 +1000 Subject: [PATCH 047/282] Add check for iat claim with some minor documentation updates --- Jwt/Authentication/JWT.php | 27 ++++++++++++++++++++------- Jwt/tests/JWTTest.php | 13 ++++++++++++- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/Jwt/Authentication/JWT.php b/Jwt/Authentication/JWT.php index 7e64b4bfc11..7e8c14ecf46 100644 --- a/Jwt/Authentication/JWT.php +++ b/Jwt/Authentication/JWT.php @@ -30,8 +30,11 @@ class JWT * @param bool $verify Don't skip verification process * * @return object The JWT's payload as a PHP object - * @throws UnexpectedValueException Provided JWT was invalid * @throws DomainException Algorithm was not provided + * @throws UnexpectedValueException Provided JWT was invalid + * @throws BeforeValidException Provided JWT is trying to be used before it's eligible as defined by 'nbf' + * @throws BeforeValidException Provided JWT is trying to be used before it's been created as defined by 'iat' + * @throws ExpiredException Provided JWT has since expired, as defined by the 'exp' claim * * @uses jsonDecode * @uses urlsafeB64Decode @@ -67,17 +70,27 @@ public static function decode($jwt, $key = null, $verify = true) throw new SignatureInvalidException('Signature verification failed'); } - // Check token expiry time if defined. - if (isset($payload->exp) && time() >= $payload->exp) { - throw new ExpiredException('Expired token'); - } - - // Check if the nbf if it is defined. + // Check if the nbf if it is defined. This is the time that the + // token can actually be used. If it's not yet that time, abort. if (isset($payload->nbf) && $payload->nbf > time()) { throw new BeforeValidException( 'Cannot handle token prior to ' . date(DateTime::ISO8601, $payload->nbf) ); } + + // Check that this token has been created before 'now'. This prevents + // using tokens that have been created for later use (and haven't + // correctly used the nbf claim). + if (isset($payload->iat) && $payload->iat > time()) { + throw new BeforeValidException( + 'Cannot handle token prior to ' . date(DateTime::ISO8601, $payload->iat) + ); + } + + // Check if this token has expired. + if (isset($payload->exp) && time() >= $payload->exp) { + throw new ExpiredException('Expired token'); + } } return $payload; diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index 5a76ed4bea1..b7dbca0af9d 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -45,7 +45,7 @@ public function testExpiredToken() JWT::decode($encoded, 'my_key'); } - public function testBeforeValidToken() + public function testBeforeValidTokenWithNbf() { $this->setExpectedException('BeforeValidException'); $payload = array( @@ -55,6 +55,16 @@ public function testBeforeValidToken() JWT::decode($encoded, 'my_key'); } + public function testBeforeValidTokenWithIat() + { + $this->setExpectedException('BeforeValidException'); + $payload = array( + "message" => "abc", + "iat" => time() + 20); // time in the future + $encoded = JWT::encode($payload, 'my_key'); + JWT::decode($encoded, 'my_key'); + } + public function testValidToken() { $payload = array( @@ -69,6 +79,7 @@ public function testValidTokenWithNbf() { $payload = array( "message" => "abc", + "iat" => time(), "exp" => time() + 20, // time in the future "nbf" => time() - 20); $encoded = JWT::encode($payload, 'my_key'); From b3b51dfef342525dafd3730186a4f666edfc50ef Mon Sep 17 00:00:00 2001 From: Brendan Abbott Date: Mon, 17 Nov 2014 11:29:36 +1000 Subject: [PATCH 048/282] Minor documentation update for SignatureInvalidException --- Jwt/Authentication/JWT.php | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Jwt/Authentication/JWT.php b/Jwt/Authentication/JWT.php index 7e8c14ecf46..1955b600436 100644 --- a/Jwt/Authentication/JWT.php +++ b/Jwt/Authentication/JWT.php @@ -30,11 +30,13 @@ class JWT * @param bool $verify Don't skip verification process * * @return object The JWT's payload as a PHP object - * @throws DomainException Algorithm was not provided - * @throws UnexpectedValueException Provided JWT was invalid - * @throws BeforeValidException Provided JWT is trying to be used before it's eligible as defined by 'nbf' - * @throws BeforeValidException Provided JWT is trying to be used before it's been created as defined by 'iat' - * @throws ExpiredException Provided JWT has since expired, as defined by the 'exp' claim + * + * @throws DomainException Algorithm was not provided + * @throws UnexpectedValueException Provided JWT was invalid + * @throws SignatureInvalidException Provided JWT was invalid because the signature verification failed + * @throws BeforeValidException Provided JWT is trying to be used before it's eligible as defined by 'nbf' + * @throws BeforeValidException Provided JWT is trying to be used before it's been created as defined by 'iat' + * @throws ExpiredException Provided JWT has since expired, as defined by the 'exp' claim * * @uses jsonDecode * @uses urlsafeB64Decode From 155d0b9baed40b0174db00ace3e7772ff59fe741 Mon Sep 17 00:00:00 2001 From: Mauro Maggi Date: Thu, 12 Feb 2015 16:02:45 +0100 Subject: [PATCH 049/282] Update .travis.yml --- Jwt/.travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/Jwt/.travis.yml b/Jwt/.travis.yml index d5d716e4517..0bd12c5ab00 100644 --- a/Jwt/.travis.yml +++ b/Jwt/.travis.yml @@ -4,6 +4,7 @@ php: - 5.3 - 5.4 - 5.5 + - hhvm before_script: - wget -nc http://getcomposer.org/composer.phar From 42e868e389f79b3e6e10c869724042b0f3f0c81b Mon Sep 17 00:00:00 2001 From: Scott Arciszewski Date: Wed, 1 Apr 2015 02:33:48 -0400 Subject: [PATCH 050/282] Allow users to lock their app into an algorithm. --- Jwt/Authentication/JWT.php | 25 +++++++++++++++++++++++++ Jwt/tests/JWTTest.php | 2 ++ 2 files changed, 27 insertions(+) diff --git a/Jwt/Authentication/JWT.php b/Jwt/Authentication/JWT.php index 1955b600436..57541d3a6e1 100644 --- a/Jwt/Authentication/JWT.php +++ b/Jwt/Authentication/JWT.php @@ -15,6 +15,8 @@ */ class JWT { + public static $only_method = 'HS256'; + public static $methods = array( 'HS256' => array('hash_hmac', 'SHA256'), 'HS512' => array('hash_hmac', 'SHA512'), @@ -173,6 +175,11 @@ public static function verify($msg, $signature, $key, $method = 'HS256') if (empty(self::$methods[$method])) { throw new DomainException('Algorithm not supported'); } + if (self::$only_method === null) { + throw new DomainException('Algorithm not specified'); + } elseif ($method !== self::$only_method) { + throw new DomainException('Incorrect algorithm error'); + } list($function, $algo) = self::$methods[$method]; switch($function) { case 'openssl': @@ -299,4 +306,22 @@ private static function handleJsonError($errno) : 'Unknown JSON error: ' . $errno ); } + + /** + * Set the only allowed method for this server. + * + * @ref https://auth0.com/blog/2015/03/31/critical-vulnerabilities-in-json-web-token-libraries/ + * + * @param string $method array index in self::$methods + * + * @return boolean + */ + public static function setOnlyAllowedMethod($method) + { + if (!empty(self::$methods[$method])) { + self::$only_method = $method; + return true; + } + return false; + } } diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index b7dbca0af9d..7b9c102d81b 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -102,6 +102,7 @@ public function testRSEncodeDecode() $privKey = openssl_pkey_new(array('digest_alg' => 'sha256', 'private_key_bits' => 1024, 'private_key_type' => OPENSSL_KEYTYPE_RSA)); + JWT::setOnlyAllowedMethod('RS256'); $msg = JWT::encode('abc', $privKey, 'RS256'); $pubKey = openssl_pkey_get_details($privKey); $pubKey = $pubKey['key']; @@ -112,6 +113,7 @@ public function testRSEncodeDecode() public function testKIDChooser() { $keys = array('1' => 'my_key', '2' => 'my_key2'); + JWT::setOnlyAllowedMethod('HS256'); $msg = JWT::encode('abc', $keys['1'], 'HS256', '1'); $decoded = JWT::decode($msg, $keys, true); $this->assertEquals($decoded, 'abc'); From e41fbce2fd4432334b5c38475c120bdb8dfe0f91 Mon Sep 17 00:00:00 2001 From: Scott Arciszewski Date: Wed, 1 Apr 2015 02:47:28 -0400 Subject: [PATCH 051/282] Add note to readme --- Jwt/README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Jwt/README.md b/Jwt/README.md index 48884624692..03ec673eef7 100644 --- a/Jwt/README.md +++ b/Jwt/README.md @@ -27,6 +27,13 @@ $token = array( "nbf" => 1357000000 ); +/* + IMPORTANT + HS256 is actually the default, but you should set it explicitly + if you intend to use another strategy (e.g. RS256): +*/ +JWT::setOnlyMethodAllowed('HS256'); + $jwt = JWT::encode($token, $key); $decoded = JWT::decode($jwt, $key); From 0c7488343225083889fc79e1e206b4d7740b85c7 Mon Sep 17 00:00:00 2001 From: Scott Arciszewski Date: Wed, 1 Apr 2015 02:48:46 -0400 Subject: [PATCH 052/282] Add GnuPG verified phpunit script --- Jwt/run-tests.sh | 54 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 Jwt/run-tests.sh diff --git a/Jwt/run-tests.sh b/Jwt/run-tests.sh new file mode 100644 index 00000000000..5754213f54d --- /dev/null +++ b/Jwt/run-tests.sh @@ -0,0 +1,54 @@ + +#!/usr/bin/env bash +clean=1 # Delete phpunit.phar after the tests are complete? +gpg --fingerprint D8406D0D82947747293778314AA394086372C20A +if [ $? -ne 0 ]; then + echo -e "\033[33mDownloading PGP Public Key...\033[0m" + gpg --recv-keys D8406D0D82947747293778314AA394086372C20A + # Sebastian Bergmann + gpg --fingerprint D8406D0D82947747293778314AA394086372C20A + if [ $? -ne 0 ]; then + echo -e "\033[31mCould not download PGP public key for verification\033[0m" + exit + fi +fi + +if [ "$clean" -eq 1 ]; then + # Let's clean them up, if they exist + if [ -f phpunit.phar ]; then + rm -f phpunit.phar + fi + if [ -f phpunit.phar.asc ]; then + rm -f phpunit.phar.asc + fi +fi + +# Let's grab the latest release and its signature +if [ ! -f phpunit.phar ]; then + wget https://phar.phpunit.de/phpunit.phar +fi +if [ ! -f phpunit.phar.asc ]; then + wget https://phar.phpunit.de/phpunit.phar.asc +fi + +# Verify before running +gpg --verify phpunit.phar.asc phpunit.phar +if [ $? -eq 0 ]; then + echo + echo -e "\033[33mBegin Unit Testing\033[0m" + # Run the testing suite + phpunit --configuration phpunit.xml.dist + # Cleanup + if [ "$clean" -eq 1 ]; then + echo -e "\033[32mCleaning Up!\033[0m" + rm -f phpunit.phar + rm -f phpunit.phar.asc + fi +else + echo + chmod -x phpunit.phar + mv phpunit.phar /tmp/bad-phpunit.phar + mv phpunit.phar.asc /tmp/bad-phpunit.phar.asc + echo -e "\033[31mSignature did not match! PHPUnit has been moved to /tmp/bad-phpunit.phar\033[0m" + exit 1 +fi From 136f42ad729d0cdf94751333285ecf7b2fac41ec Mon Sep 17 00:00:00 2001 From: Scott Arciszewski Date: Wed, 1 Apr 2015 03:18:23 -0400 Subject: [PATCH 053/282] Harden against mbstring.func_overload edge-cases --- Jwt/Authentication/JWT.php | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/Jwt/Authentication/JWT.php b/Jwt/Authentication/JWT.php index 57541d3a6e1..1a78a324c7b 100644 --- a/Jwt/Authentication/JWT.php +++ b/Jwt/Authentication/JWT.php @@ -192,13 +192,13 @@ public static function verify($msg, $signature, $key, $method = 'HS256') case 'hash_hmac': default: $hash = hash_hmac($algo, $msg, $key, true); - $len = min(strlen($signature), strlen($hash)); + $len = min(self::safeStrlen($signature), self::safeStrlen($hash)); $status = 0; for ($i = 0; $i < $len; $i++) { $status |= (ord($signature[$i]) ^ ord($hash[$i])); } - $status |= (strlen($signature) ^ strlen($hash)); + $status |= (self::safeStrlen($signature) ^ self::safeStrlen($hash)); return ($status === 0); } @@ -307,6 +307,20 @@ private static function handleJsonError($errno) ); } + /** + * Get the number of bytes in cryptographic strings. + * + * @param string + * @return int + */ + private static function safeStrlen($str) + { + if (function_exists('mb_strlen')) { + return mb_strlen($str, '8bit'); + } + return strlen($str); + } + /** * Set the only allowed method for this server. * From fa9ffe79c44964e30a8d175330e1b547f718eb8a Mon Sep 17 00:00:00 2001 From: Scott Arciszewski Date: Wed, 1 Apr 2015 03:19:41 -0400 Subject: [PATCH 054/282] Use native functions when they exist. --- Jwt/Authentication/JWT.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Jwt/Authentication/JWT.php b/Jwt/Authentication/JWT.php index 1a78a324c7b..54d0effa11c 100644 --- a/Jwt/Authentication/JWT.php +++ b/Jwt/Authentication/JWT.php @@ -192,6 +192,9 @@ public static function verify($msg, $signature, $key, $method = 'HS256') case 'hash_hmac': default: $hash = hash_hmac($algo, $msg, $key, true); + if (function_exists('hash_equals')) { + return hash_equals($signature, $hash); + } $len = min(self::safeStrlen($signature), self::safeStrlen($hash)); $status = 0; From 5c08222f0fe5f3f134f81e78e3dfb4c19430e6f5 Mon Sep 17 00:00:00 2001 From: Rob DiMarco Date: Wed, 1 Apr 2015 10:12:42 -0700 Subject: [PATCH 055/282] Update to ./run-tests.sh --- Jwt/.gitignore | 2 ++ Jwt/run-tests.sh | 13 +------------ 2 files changed, 3 insertions(+), 12 deletions(-) mode change 100644 => 100755 Jwt/run-tests.sh diff --git a/Jwt/.gitignore b/Jwt/.gitignore index 22d0d82f809..96e2f2e1a51 100644 --- a/Jwt/.gitignore +++ b/Jwt/.gitignore @@ -1 +1,3 @@ vendor +phpunit.phar +phpunit.phar.asc diff --git a/Jwt/run-tests.sh b/Jwt/run-tests.sh old mode 100644 new mode 100755 index 5754213f54d..f7de6c1c54a --- a/Jwt/run-tests.sh +++ b/Jwt/run-tests.sh @@ -1,6 +1,5 @@ #!/usr/bin/env bash -clean=1 # Delete phpunit.phar after the tests are complete? gpg --fingerprint D8406D0D82947747293778314AA394086372C20A if [ $? -ne 0 ]; then echo -e "\033[33mDownloading PGP Public Key...\033[0m" @@ -13,16 +12,6 @@ if [ $? -ne 0 ]; then fi fi -if [ "$clean" -eq 1 ]; then - # Let's clean them up, if they exist - if [ -f phpunit.phar ]; then - rm -f phpunit.phar - fi - if [ -f phpunit.phar.asc ]; then - rm -f phpunit.phar.asc - fi -fi - # Let's grab the latest release and its signature if [ ! -f phpunit.phar ]; then wget https://phar.phpunit.de/phpunit.phar @@ -37,7 +26,7 @@ if [ $? -eq 0 ]; then echo echo -e "\033[33mBegin Unit Testing\033[0m" # Run the testing suite - phpunit --configuration phpunit.xml.dist + php phpunit --configuration phpunit.xml.dist # Cleanup if [ "$clean" -eq 1 ]; then echo -e "\033[32mCleaning Up!\033[0m" From 265817f9cc42049d30b8423de456b64aa6188bb2 Mon Sep 17 00:00:00 2001 From: Rob DiMarco Date: Wed, 1 Apr 2015 10:15:23 -0700 Subject: [PATCH 056/282] Add'l updates to the ./run-tests.sh script --- Jwt/run-tests.sh | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/Jwt/run-tests.sh b/Jwt/run-tests.sh index f7de6c1c54a..9d6643ebc6c 100755 --- a/Jwt/run-tests.sh +++ b/Jwt/run-tests.sh @@ -26,13 +26,7 @@ if [ $? -eq 0 ]; then echo echo -e "\033[33mBegin Unit Testing\033[0m" # Run the testing suite - php phpunit --configuration phpunit.xml.dist - # Cleanup - if [ "$clean" -eq 1 ]; then - echo -e "\033[32mCleaning Up!\033[0m" - rm -f phpunit.phar - rm -f phpunit.phar.asc - fi + php phpunit.phar --configuration phpunit.xml.dist else echo chmod -x phpunit.phar From c37a2efe285fd694e334ce59d165bf535ffba6f1 Mon Sep 17 00:00:00 2001 From: Rob DiMarco Date: Wed, 1 Apr 2015 10:47:36 -0700 Subject: [PATCH 057/282] Updates to JWT decode() / verify() path --- Jwt/Authentication/JWT.php | 75 ++++++++++++++------------------------ Jwt/run-tests.sh | 1 + Jwt/tests/JWTTest.php | 4 +- 3 files changed, 30 insertions(+), 50 deletions(-) diff --git a/Jwt/Authentication/JWT.php b/Jwt/Authentication/JWT.php index 54d0effa11c..c0c965bddc8 100644 --- a/Jwt/Authentication/JWT.php +++ b/Jwt/Authentication/JWT.php @@ -15,9 +15,7 @@ */ class JWT { - public static $only_method = 'HS256'; - - public static $methods = array( + public static $supported_algs = array( 'HS256' => array('hash_hmac', 'SHA256'), 'HS512' => array('hash_hmac', 'SHA512'), 'HS384' => array('hash_hmac', 'SHA384'), @@ -27,9 +25,9 @@ class JWT /** * Decodes a JWT string into a PHP object. * - * @param string $jwt The JWT - * @param string|Array|null $key The secret key, or map of keys - * @param bool $verify Don't skip verification process + * @param string $jwt The JWT + * @param string|Array|null $key The secret key, or map of keys + * @param bool $algs List of supported verification algorithms * * @return object The JWT's payload as a PHP object * @@ -43,7 +41,7 @@ class JWT * @uses jsonDecode * @uses urlsafeB64Decode */ - public static function decode($jwt, $key = null, $verify = true) + public static function decode($jwt, $key = null, $algs = array()) { $tks = explode('.', $jwt); if (count($tks) != 3) { @@ -57,10 +55,13 @@ public static function decode($jwt, $key = null, $verify = true) throw new UnexpectedValueException('Invalid claims encoding'); } $sig = JWT::urlsafeB64Decode($cryptob64); - if ($verify) { + if (!empty($key)) { if (empty($header->alg)) { throw new DomainException('Empty algorithm'); } + if (empty(self::$supported_algs[$header->alg])) { + throw new DomainException('Algorithm not supported'); + } if (is_array($key)) { if (isset($header->kid)) { $key = $key[$header->kid]; @@ -105,16 +106,16 @@ public static function decode($jwt, $key = null, $verify = true) * * @param object|array $payload PHP object or array * @param string $key The secret key - * @param string $algo The signing algorithm. Supported + * @param string $alg The signing algorithm. Supported * algorithms are 'HS256', 'HS384' and 'HS512' * * @return string A signed JWT * @uses jsonEncode * @uses urlsafeB64Encode */ - public static function encode($payload, $key, $algo = 'HS256', $keyId = null) + public static function encode($payload, $key, $alg = 'HS256', $keyId = null) { - $header = array('typ' => 'JWT', 'alg' => $algo); + $header = array('typ' => 'JWT', 'alg' => $alg); if ($keyId !== null) { $header['kid'] = $keyId; } @@ -123,7 +124,7 @@ public static function encode($payload, $key, $algo = 'HS256', $keyId = null) $segments[] = JWT::urlsafeB64Encode(JWT::jsonEncode($payload)); $signing_input = implode('.', $segments); - $signature = JWT::sign($signing_input, $key, $algo); + $signature = JWT::sign($signing_input, $key, $alg); $segments[] = JWT::urlsafeB64Encode($signature); return implode('.', $segments); @@ -134,24 +135,24 @@ public static function encode($payload, $key, $algo = 'HS256', $keyId = null) * * @param string $msg The message to sign * @param string|resource $key The secret key - * @param string $method The signing algorithm. Supported algorithms + * @param string $alg The signing algorithm. Supported algorithms * are 'HS256', 'HS384', 'HS512' and 'RS256' * * @return string An encrypted message * @throws DomainException Unsupported algorithm was specified */ - public static function sign($msg, $key, $method = 'HS256') + public static function sign($msg, $key, $alg = 'HS256') { - if (empty(self::$methods[$method])) { + if (empty(self::$supported_algs[$alg])) { throw new DomainException('Algorithm not supported'); } - list($function, $algo) = self::$methods[$method]; + list($function, $algorithm) = self::$supported_algs[$alg]; switch($function) { case 'hash_hmac': - return hash_hmac($algo, $msg, $key, true); + return hash_hmac($algorithm, $msg, $key, true); case 'openssl': $signature = ''; - $success = openssl_sign($msg, $signature, $key, $algo); + $success = openssl_sign($msg, $signature, $key, $algorithm); if (!$success) { throw new DomainException("OpenSSL unable to sign data"); } else { @@ -166,24 +167,20 @@ public static function sign($msg, $key, $method = 'HS256') * @param string $msg the original message * @param string $signature * @param string|resource $key for HS*, a string key works. for RS*, must be a resource of an openssl public key - * @param string $method + * @param string $algorithms * @return bool * @throws DomainException Invalid Algorithm or OpenSSL failure */ - public static function verify($msg, $signature, $key, $method = 'HS256') + private static function verify($msg, $signature, $key, $alg) { - if (empty(self::$methods[$method])) { + if (empty(self::$supported_algs[$alg])) { throw new DomainException('Algorithm not supported'); } - if (self::$only_method === null) { - throw new DomainException('Algorithm not specified'); - } elseif ($method !== self::$only_method) { - throw new DomainException('Incorrect algorithm error'); - } - list($function, $algo) = self::$methods[$method]; + + list($function, $algorithm) = self::$supported_algs[$alg]; switch($function) { case 'openssl': - $success = openssl_verify($msg, $signature, $key, $algo); + $success = openssl_verify($msg, $signature, $key, $algorithm); if (!$success) { throw new DomainException("OpenSSL unable to verify data: " . openssl_error_string()); } else { @@ -191,7 +188,7 @@ public static function verify($msg, $signature, $key, $method = 'HS256') } case 'hash_hmac': default: - $hash = hash_hmac($algo, $msg, $key, true); + $hash = hash_hmac($algorithm, $msg, $key, true); if (function_exists('hash_equals')) { return hash_equals($signature, $hash); } @@ -309,7 +306,7 @@ private static function handleJsonError($errno) : 'Unknown JSON error: ' . $errno ); } - + /** * Get the number of bytes in cryptographic strings. * @@ -323,22 +320,4 @@ private static function safeStrlen($str) } return strlen($str); } - - /** - * Set the only allowed method for this server. - * - * @ref https://auth0.com/blog/2015/03/31/critical-vulnerabilities-in-json-web-token-libraries/ - * - * @param string $method array index in self::$methods - * - * @return boolean - */ - public static function setOnlyAllowedMethod($method) - { - if (!empty(self::$methods[$method])) { - self::$only_method = $method; - return true; - } - return false; - } } diff --git a/Jwt/run-tests.sh b/Jwt/run-tests.sh index 9d6643ebc6c..d37c30fd754 100755 --- a/Jwt/run-tests.sh +++ b/Jwt/run-tests.sh @@ -26,6 +26,7 @@ if [ $? -eq 0 ]; then echo echo -e "\033[33mBegin Unit Testing\033[0m" # Run the testing suite + php --version php phpunit.phar --configuration phpunit.xml.dist else echo diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index 7b9c102d81b..c7ed6c1f51e 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -102,7 +102,7 @@ public function testRSEncodeDecode() $privKey = openssl_pkey_new(array('digest_alg' => 'sha256', 'private_key_bits' => 1024, 'private_key_type' => OPENSSL_KEYTYPE_RSA)); - JWT::setOnlyAllowedMethod('RS256'); + //JWT::setOnlyAllowedMethod('RS256'); $msg = JWT::encode('abc', $privKey, 'RS256'); $pubKey = openssl_pkey_get_details($privKey); $pubKey = $pubKey['key']; @@ -113,7 +113,7 @@ public function testRSEncodeDecode() public function testKIDChooser() { $keys = array('1' => 'my_key', '2' => 'my_key2'); - JWT::setOnlyAllowedMethod('HS256'); + //JWT::setOnlyAllowedMethod('HS256'); $msg = JWT::encode('abc', $keys['1'], 'HS256', '1'); $decoded = JWT::decode($msg, $keys, true); $this->assertEquals($decoded, 'abc'); From bba6a69efcc8affe2fa895d6bb2b541d71b2c423 Mon Sep 17 00:00:00 2001 From: Rob DiMarco Date: Wed, 1 Apr 2015 10:58:25 -0700 Subject: [PATCH 058/282] Update decode() to require allowed algorithms arg when verifying --- Jwt/Authentication/JWT.php | 13 ++++++++----- Jwt/tests/JWTTest.php | 24 +++++++++++------------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/Jwt/Authentication/JWT.php b/Jwt/Authentication/JWT.php index c0c965bddc8..022029bb599 100644 --- a/Jwt/Authentication/JWT.php +++ b/Jwt/Authentication/JWT.php @@ -25,9 +25,9 @@ class JWT /** * Decodes a JWT string into a PHP object. * - * @param string $jwt The JWT - * @param string|Array|null $key The secret key, or map of keys - * @param bool $algs List of supported verification algorithms + * @param string $jwt The JWT + * @param string|Array|null $key The secret key, or map of keys + * @param Array $allowed_algs List of supported verification algorithms * * @return object The JWT's payload as a PHP object * @@ -41,7 +41,7 @@ class JWT * @uses jsonDecode * @uses urlsafeB64Decode */ - public static function decode($jwt, $key = null, $algs = array()) + public static function decode($jwt, $key = null, $allowed_algs = array()) { $tks = explode('.', $jwt); if (count($tks) != 3) { @@ -55,13 +55,16 @@ public static function decode($jwt, $key = null, $algs = array()) throw new UnexpectedValueException('Invalid claims encoding'); } $sig = JWT::urlsafeB64Decode($cryptob64); - if (!empty($key)) { + if (isset($key)) { if (empty($header->alg)) { throw new DomainException('Empty algorithm'); } if (empty(self::$supported_algs[$header->alg])) { throw new DomainException('Algorithm not supported'); } + if (!is_array($allowed_algs) || !in_array($header->alg, $allowed_algs)) { + throw new DomainException('Algorithm not allowed'); + } if (is_array($key)) { if (isset($header->kid)) { $key = $key[$header->kid]; diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index c7ed6c1f51e..1ace7f58360 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -5,14 +5,14 @@ class JWTTest extends PHPUnit_Framework_TestCase public function testEncodeDecode() { $msg = JWT::encode('abc', 'my_key'); - $this->assertEquals(JWT::decode($msg, 'my_key'), 'abc'); + $this->assertEquals(JWT::decode($msg, 'my_key', array('HS256')), 'abc'); } public function testDecodeFromPython() { $msg = 'eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.Iio6aHR0cDovL2FwcGxpY2F0aW9uL2NsaWNreT9ibGFoPTEuMjMmZi5vbz00NTYgQUMwMDAgMTIzIg.E_U8X2YpMT5K1cEiT_3-IvBYfrdIFIeVYeOqre_Z5Cg'; $this->assertEquals( - JWT::decode($msg, 'my_key'), + JWT::decode($msg, 'my_key', array('HS256')), '*:http://application/clicky?blah=1.23&f.oo=456 AC000 123' ); } @@ -20,7 +20,7 @@ public function testDecodeFromPython() public function testUrlSafeCharacters() { $encoded = JWT::encode('f?', 'a'); - $this->assertEquals('f?', JWT::decode($encoded, 'a')); + $this->assertEquals('f?', JWT::decode($encoded, 'a', array('HS256'))); } public function testMalformedUtf8StringsFail() @@ -42,7 +42,7 @@ public function testExpiredToken() "message" => "abc", "exp" => time() - 20); // time in the past $encoded = JWT::encode($payload, 'my_key'); - JWT::decode($encoded, 'my_key'); + JWT::decode($encoded, 'my_key', array('HS256')); } public function testBeforeValidTokenWithNbf() @@ -52,7 +52,7 @@ public function testBeforeValidTokenWithNbf() "message" => "abc", "nbf" => time() + 20); // time in the future $encoded = JWT::encode($payload, 'my_key'); - JWT::decode($encoded, 'my_key'); + JWT::decode($encoded, 'my_key', array('HS256')); } public function testBeforeValidTokenWithIat() @@ -62,7 +62,7 @@ public function testBeforeValidTokenWithIat() "message" => "abc", "iat" => time() + 20); // time in the future $encoded = JWT::encode($payload, 'my_key'); - JWT::decode($encoded, 'my_key'); + JWT::decode($encoded, 'my_key', array('HS256')); } public function testValidToken() @@ -71,7 +71,7 @@ public function testValidToken() "message" => "abc", "exp" => time() + 20); // time in the future $encoded = JWT::encode($payload, 'my_key'); - $decoded = JWT::decode($encoded, 'my_key'); + $decoded = JWT::decode($encoded, 'my_key', array('HS256')); $this->assertEquals($decoded->message, 'abc'); } @@ -83,7 +83,7 @@ public function testValidTokenWithNbf() "exp" => time() + 20, // time in the future "nbf" => time() - 20); $encoded = JWT::encode($payload, 'my_key'); - $decoded = JWT::decode($encoded, 'my_key'); + $decoded = JWT::decode($encoded, 'my_key', array('HS256')); $this->assertEquals($decoded->message, 'abc'); } @@ -94,7 +94,7 @@ public function testInvalidToken() "exp" => time() + 20); // time in the future $encoded = JWT::encode($payload, 'my_key'); $this->setExpectedException('SignatureInvalidException'); - $decoded = JWT::decode($encoded, 'my_key2'); + $decoded = JWT::decode($encoded, 'my_key2', array('HS256')); } public function testRSEncodeDecode() @@ -102,20 +102,18 @@ public function testRSEncodeDecode() $privKey = openssl_pkey_new(array('digest_alg' => 'sha256', 'private_key_bits' => 1024, 'private_key_type' => OPENSSL_KEYTYPE_RSA)); - //JWT::setOnlyAllowedMethod('RS256'); $msg = JWT::encode('abc', $privKey, 'RS256'); $pubKey = openssl_pkey_get_details($privKey); $pubKey = $pubKey['key']; - $decoded = JWT::decode($msg, $pubKey, true); + $decoded = JWT::decode($msg, $pubKey, array('RS256')); $this->assertEquals($decoded, 'abc'); } public function testKIDChooser() { $keys = array('1' => 'my_key', '2' => 'my_key2'); - //JWT::setOnlyAllowedMethod('HS256'); $msg = JWT::encode('abc', $keys['1'], 'HS256', '1'); - $decoded = JWT::decode($msg, $keys, true); + $decoded = JWT::decode($msg, $keys, array('HS256')); $this->assertEquals($decoded, 'abc'); } } From 556b8b4581b0c79ba9b86ce7971eb094e1a15909 Mon Sep 17 00:00:00 2001 From: Rob DiMarco Date: Wed, 1 Apr 2015 11:02:21 -0700 Subject: [PATCH 059/282] Comment tweaks --- Jwt/Authentication/JWT.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jwt/Authentication/JWT.php b/Jwt/Authentication/JWT.php index 022029bb599..905920a7aac 100644 --- a/Jwt/Authentication/JWT.php +++ b/Jwt/Authentication/JWT.php @@ -170,7 +170,7 @@ public static function sign($msg, $key, $alg = 'HS256') * @param string $msg the original message * @param string $signature * @param string|resource $key for HS*, a string key works. for RS*, must be a resource of an openssl public key - * @param string $algorithms + * @param string $alg * @return bool * @throws DomainException Invalid Algorithm or OpenSSL failure */ From e13d2afd772eec054d1d5d4374fb891942de7ebc Mon Sep 17 00:00:00 2001 From: drtriumph Date: Wed, 1 Apr 2015 11:07:46 -0700 Subject: [PATCH 060/282] A few additional tests for none algorithm and an incorrect algorithm --- Jwt/tests/JWTTest.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index 1ace7f58360..cc2eb8caed4 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -116,4 +116,18 @@ public function testKIDChooser() $decoded = JWT::decode($msg, $keys, array('HS256')); $this->assertEquals($decoded, 'abc'); } + + public function testNoneToken() + { + $msg = JWT::encode('abc', 'my_key'); + $this->setExpectedException('DomainException'); + JWT::decode($msg, 'my_key', array('none')); + } + + public function testIncorrectAlgorithm() + { + $msg = JWT::encode('abc', 'my_key'); + $this->setExpectedException('DomainException'); + JWT::decode($msg, 'my_key', array('RS256')); + } } From aa13abb049a75fd06381343c5832eba1ca6b8e8d Mon Sep 17 00:00:00 2001 From: Rob DiMarco Date: Wed, 1 Apr 2015 11:19:55 -0700 Subject: [PATCH 061/282] Update README to reflect newly-required algorithms arguments --- Jwt/README.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/Jwt/README.md b/Jwt/README.md index 03ec673eef7..1a9e093b274 100644 --- a/Jwt/README.md +++ b/Jwt/README.md @@ -27,15 +27,14 @@ $token = array( "nbf" => 1357000000 ); -/* - IMPORTANT - HS256 is actually the default, but you should set it explicitly - if you intend to use another strategy (e.g. RS256): -*/ -JWT::setOnlyMethodAllowed('HS256'); - +/** + * IMPORTANT: + * You must specify supported algorithms for your application. See + * https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40 + * for a list of spec-compliant algorithms. + */ $jwt = JWT::encode($token, $key); -$decoded = JWT::decode($jwt, $key); +$decoded = JWT::decode($jwt, $key, array('HS256')); print_r($decoded); From 246462b6b32fe6110987e7ed6498f80b9edfb932 Mon Sep 17 00:00:00 2001 From: Rob DiMarco Date: Wed, 1 Apr 2015 11:39:10 -0700 Subject: [PATCH 062/282] Add a few add'l tests --- Jwt/tests/JWTTest.php | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index cc2eb8caed4..c48427deb33 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -75,6 +75,16 @@ public function testValidToken() $this->assertEquals($decoded->message, 'abc'); } + public function testValidTokenWithList() + { + $payload = array( + "message" => "abc", + "exp" => time() + 20); // time in the future + $encoded = JWT::encode($payload, 'my_key'); + $decoded = JWT::decode($encoded, 'my_key', array('HS256', 'HS512')); + $this->assertEquals($decoded->message, 'abc'); + } + public function testValidTokenWithNbf() { $payload = array( @@ -117,7 +127,7 @@ public function testKIDChooser() $this->assertEquals($decoded, 'abc'); } - public function testNoneToken() + public function testNoneAlgorithm() { $msg = JWT::encode('abc', 'my_key'); $this->setExpectedException('DomainException'); @@ -130,4 +140,11 @@ public function testIncorrectAlgorithm() $this->setExpectedException('DomainException'); JWT::decode($msg, 'my_key', array('RS256')); } + + public function testMissingAlgorithm() + { + $msg = JWT::encode('abc', 'my_key'); + $this->setExpectedException('DomainException'); + JWT::decode($msg, 'my_key'); + } } From 6ad3146ce3ea6c516ccd8a70d52007d1db042bbe Mon Sep 17 00:00:00 2001 From: Rob DiMarco Date: Wed, 1 Apr 2015 11:46:38 -0700 Subject: [PATCH 063/282] Bump version to 2.0.0 --- Jwt/composer.json | 1 + Jwt/package.xml | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Jwt/composer.json b/Jwt/composer.json index 15b246c9174..4cc0e6ec3c5 100644 --- a/Jwt/composer.json +++ b/Jwt/composer.json @@ -14,6 +14,7 @@ "role": "Developer" } ], + "version": "2.0.0", "license": "BSD-3-Clause", "require": { "php": ">=5.2.0" diff --git a/Jwt/package.xml b/Jwt/package.xml index 6c44498e5b7..e06616de0de 100644 --- a/Jwt/package.xml +++ b/Jwt/package.xml @@ -19,10 +19,10 @@ anant@php.net yes - 2012-12-31 + 2015-04-01 - 0.1.0 - 0.1.0 + 2.0.0 + 2.0.0 beta @@ -67,7 +67,7 @@ Initial release with basic support for JWT encoding, decoding and signature veri beta beta - 2012-12-31 + 2015-04-01 BSD 3-Clause License Initial release with basic support for JWT encoding, decoding and signature verification. From 5475935488b6be9f2d57468a3692f3885f0d2ee8 Mon Sep 17 00:00:00 2001 From: Chris Raynor Date: Wed, 1 Apr 2015 11:48:00 -0700 Subject: [PATCH 064/282] Update composer.json --- Jwt/composer.json | 1 - 1 file changed, 1 deletion(-) diff --git a/Jwt/composer.json b/Jwt/composer.json index 4cc0e6ec3c5..15b246c9174 100644 --- a/Jwt/composer.json +++ b/Jwt/composer.json @@ -14,7 +14,6 @@ "role": "Developer" } ], - "version": "2.0.0", "license": "BSD-3-Clause", "require": { "php": ">=5.2.0" From bf885bf03a9fce1a38e753c5202c37cc342d74ba Mon Sep 17 00:00:00 2001 From: Rob DiMarco Date: Wed, 1 Apr 2015 11:56:17 -0700 Subject: [PATCH 065/282] Add Changelog --- Jwt/README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Jwt/README.md b/Jwt/README.md index 1a9e093b274..3c416dc3c05 100644 --- a/Jwt/README.md +++ b/Jwt/README.md @@ -48,6 +48,17 @@ $decoded_array = (array) $decoded; ?> ``` +Changelog +--------- + +#### 2.0.0 / 2015-04-01 +- **Note**: It is strongly recommended that you update to > v2.0.0 to address + known security vulnerabilities in prior versions when both symmetric and + asymmetric keys are used together. +- Update signature for `JWT::decode(...)` to require an array of supported + algorithms to use when verifying token signatures. + + Tests ----- Run the tests using phpunit: From 72565e602665e278b9792aee50eac0f960270caf Mon Sep 17 00:00:00 2001 From: Christophe Coevoet Date: Thu, 2 Apr 2015 17:32:18 +0200 Subject: [PATCH 066/282] Remove the useless target-dir setting from the composer config target-dir was meant to allow using the root of the package as a PSR-4 root and make composer prepend the target dir when installing to make it compatible with PSR-0, before PSR-4 was a reality. This setting is deprecated in Composer in favor of using PSR-4, and it is not needed anyway when using the classmap autoloading. --- Jwt/composer.json | 1 - 1 file changed, 1 deletion(-) diff --git a/Jwt/composer.json b/Jwt/composer.json index 15b246c9174..95560afd9e5 100644 --- a/Jwt/composer.json +++ b/Jwt/composer.json @@ -21,6 +21,5 @@ "autoload": { "classmap": ["Authentication/", "Exceptions/"] }, - "target-dir": "Firebase/PHP-JWT", "minimum-stability": "dev" } From ee4aac9af346f55c119c35ddbc44a3f378690c83 Mon Sep 17 00:00:00 2001 From: Luis Miguel Cabral Date: Thu, 9 Apr 2015 13:02:54 +0200 Subject: [PATCH 067/282] Provide a leeway time in the verification of timestamps to account for clock skew --- Jwt/Authentication/JWT.php | 12 +++++++++--- Jwt/tests/JWTTest.php | 12 +++++++++--- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/Jwt/Authentication/JWT.php b/Jwt/Authentication/JWT.php index 905920a7aac..5f319e39d90 100644 --- a/Jwt/Authentication/JWT.php +++ b/Jwt/Authentication/JWT.php @@ -15,6 +15,12 @@ */ class JWT { + + /** + * When cheking nbf, iat or expiration times, we want to provide some extra leeway time to account for clock skew. + */ + const LEEWAYTIME = 60; + public static $supported_algs = array( 'HS256' => array('hash_hmac', 'SHA256'), 'HS512' => array('hash_hmac', 'SHA512'), @@ -80,7 +86,7 @@ public static function decode($jwt, $key = null, $allowed_algs = array()) // Check if the nbf if it is defined. This is the time that the // token can actually be used. If it's not yet that time, abort. - if (isset($payload->nbf) && $payload->nbf > time()) { + if (isset($payload->nbf) && $payload->nbf > (time() + self::LEEWAYTIME)) { throw new BeforeValidException( 'Cannot handle token prior to ' . date(DateTime::ISO8601, $payload->nbf) ); @@ -89,14 +95,14 @@ public static function decode($jwt, $key = null, $allowed_algs = array()) // Check that this token has been created before 'now'. This prevents // using tokens that have been created for later use (and haven't // correctly used the nbf claim). - if (isset($payload->iat) && $payload->iat > time()) { + if (isset($payload->iat) && $payload->iat > (time() + self::LEEWAYTIME)) { throw new BeforeValidException( 'Cannot handle token prior to ' . date(DateTime::ISO8601, $payload->iat) ); } // Check if this token has expired. - if (isset($payload->exp) && time() >= $payload->exp) { + if (isset($payload->exp) && (time() - self::LEEWAYTIME) >= $payload->exp) { throw new ExpiredException('Expired token'); } } diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index c48427deb33..b685ce49e61 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -38,9 +38,11 @@ public function testMalformedJsonThrowsException() public function testExpiredToken() { $this->setExpectedException('ExpiredException'); + $timeInPast = time() - JWT::LEEWAYTIME - 20; $payload = array( "message" => "abc", - "exp" => time() - 20); // time in the past + "exp" => $timeInPast // time in the past + ); $encoded = JWT::encode($payload, 'my_key'); JWT::decode($encoded, 'my_key', array('HS256')); } @@ -48,9 +50,11 @@ public function testExpiredToken() public function testBeforeValidTokenWithNbf() { $this->setExpectedException('BeforeValidException'); + $timeInFuture = time() + JWT::LEEWAYTIME + 20; $payload = array( "message" => "abc", - "nbf" => time() + 20); // time in the future + "nbf" => $timeInFuture // time in the future + ); $encoded = JWT::encode($payload, 'my_key'); JWT::decode($encoded, 'my_key', array('HS256')); } @@ -58,9 +62,11 @@ public function testBeforeValidTokenWithNbf() public function testBeforeValidTokenWithIat() { $this->setExpectedException('BeforeValidException'); + $timeInFuture = time() + JWT::LEEWAYTIME + 20; $payload = array( "message" => "abc", - "iat" => time() + 20); // time in the future + "iat" => $timeInFuture // time in the future + ); $encoded = JWT::encode($payload, 'my_key'); JWT::decode($encoded, 'my_key', array('HS256')); } From 04a57242d2b2ed7977eff5b722e595de8741c493 Mon Sep 17 00:00:00 2001 From: Luis Miguel Cabral Date: Mon, 11 May 2015 12:20:39 +0200 Subject: [PATCH 068/282] Changed the leeway to be a static variable --- Jwt/Authentication/JWT.php | 12 +++++++----- Jwt/README.md | 10 ++++++++++ Jwt/tests/JWTTest.php | 31 +++++++++++++++++++++++++++---- 3 files changed, 44 insertions(+), 9 deletions(-) diff --git a/Jwt/Authentication/JWT.php b/Jwt/Authentication/JWT.php index 5f319e39d90..e65dc503c5a 100644 --- a/Jwt/Authentication/JWT.php +++ b/Jwt/Authentication/JWT.php @@ -17,9 +17,11 @@ class JWT { /** - * When cheking nbf, iat or expiration times, we want to provide some extra leeway time to account for clock skew. + * When checking nbf, iat or expiration times, + * we want to provide some extra leeway time to + * account for clock skew. */ - const LEEWAYTIME = 60; + public static $leeway = 0; public static $supported_algs = array( 'HS256' => array('hash_hmac', 'SHA256'), @@ -86,7 +88,7 @@ public static function decode($jwt, $key = null, $allowed_algs = array()) // Check if the nbf if it is defined. This is the time that the // token can actually be used. If it's not yet that time, abort. - if (isset($payload->nbf) && $payload->nbf > (time() + self::LEEWAYTIME)) { + if (isset($payload->nbf) && $payload->nbf > (time() + self::$leeway)) { throw new BeforeValidException( 'Cannot handle token prior to ' . date(DateTime::ISO8601, $payload->nbf) ); @@ -95,14 +97,14 @@ public static function decode($jwt, $key = null, $allowed_algs = array()) // Check that this token has been created before 'now'. This prevents // using tokens that have been created for later use (and haven't // correctly used the nbf claim). - if (isset($payload->iat) && $payload->iat > (time() + self::LEEWAYTIME)) { + if (isset($payload->iat) && $payload->iat > (time() + self::$leeway)) { throw new BeforeValidException( 'Cannot handle token prior to ' . date(DateTime::ISO8601, $payload->iat) ); } // Check if this token has expired. - if (isset($payload->exp) && (time() - self::LEEWAYTIME) >= $payload->exp) { + if (isset($payload->exp) && (time() - self::$leeway) >= $payload->exp) { throw new ExpiredException('Expired token'); } } diff --git a/Jwt/README.md b/Jwt/README.md index 3c416dc3c05..bda2d96bf32 100644 --- a/Jwt/README.md +++ b/Jwt/README.md @@ -45,6 +45,16 @@ print_r($decoded); $decoded_array = (array) $decoded; +/** + * You can add a leeway to account for when there is a clock skew times between + * the signing and verifying servers. It is recomended this leeway should not + * be bigger than a few minutes. + * Source: http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html#nbfDef + */ + +JWT::$leeway = 60; +$decoded = JWT::decode($jwt, $key, array('HS256')); + ?> ``` diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index b685ce49e61..ae59455724b 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -38,7 +38,7 @@ public function testMalformedJsonThrowsException() public function testExpiredToken() { $this->setExpectedException('ExpiredException'); - $timeInPast = time() - JWT::LEEWAYTIME - 20; + $timeInPast = time() - 20; $payload = array( "message" => "abc", "exp" => $timeInPast // time in the past @@ -50,7 +50,7 @@ public function testExpiredToken() public function testBeforeValidTokenWithNbf() { $this->setExpectedException('BeforeValidException'); - $timeInFuture = time() + JWT::LEEWAYTIME + 20; + $timeInFuture = time() + 20; $payload = array( "message" => "abc", "nbf" => $timeInFuture // time in the future @@ -62,7 +62,7 @@ public function testBeforeValidTokenWithNbf() public function testBeforeValidTokenWithIat() { $this->setExpectedException('BeforeValidException'); - $timeInFuture = time() + JWT::LEEWAYTIME + 20; + $timeInFuture = time() + 20; $payload = array( "message" => "abc", "iat" => $timeInFuture // time in the future @@ -75,7 +75,30 @@ public function testValidToken() { $payload = array( "message" => "abc", - "exp" => time() + 20); // time in the future + "exp" => time() + JWT::$leeway + 20); // time in the future + $encoded = JWT::encode($payload, 'my_key'); + $decoded = JWT::decode($encoded, 'my_key', array('HS256')); + $this->assertEquals($decoded->message, 'abc'); + } + + public function testValidTokenWithLeeway() + { + JWT::$leeway = 60; + $payload = array( + "message" => "abc", + "exp" => time() - 20); // time in the past + $encoded = JWT::encode($payload, 'my_key'); + $decoded = JWT::decode($encoded, 'my_key', array('HS256')); + $this->assertEquals($decoded->message, 'abc'); + } + + public function testExpiredTokenWithLeeway() + { + JWT::$leeway = 60; + $payload = array( + "message" => "abc", + "exp" => time() - 70); // time far in the past + $this->setExpectedException('ExpiredException'); $encoded = JWT::encode($payload, 'my_key'); $decoded = JWT::decode($encoded, 'my_key', array('HS256')); $this->assertEquals($decoded->message, 'abc'); From 25d9afc16727a943b839240ce02127e8db34e455 Mon Sep 17 00:00:00 2001 From: Luis Miguel Cabral Date: Mon, 11 May 2015 12:32:11 +0200 Subject: [PATCH 069/282] Reverted some tests from first commit --- Jwt/tests/JWTTest.php | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index ae59455724b..42fb5f90ce1 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -38,11 +38,9 @@ public function testMalformedJsonThrowsException() public function testExpiredToken() { $this->setExpectedException('ExpiredException'); - $timeInPast = time() - 20; $payload = array( "message" => "abc", - "exp" => $timeInPast // time in the past - ); + "exp" => time() - 20); // time in the past $encoded = JWT::encode($payload, 'my_key'); JWT::decode($encoded, 'my_key', array('HS256')); } @@ -50,11 +48,9 @@ public function testExpiredToken() public function testBeforeValidTokenWithNbf() { $this->setExpectedException('BeforeValidException'); - $timeInFuture = time() + 20; $payload = array( "message" => "abc", - "nbf" => $timeInFuture // time in the future - ); + "nbf" => time() + 20); // time in the future $encoded = JWT::encode($payload, 'my_key'); JWT::decode($encoded, 'my_key', array('HS256')); } @@ -62,11 +58,9 @@ public function testBeforeValidTokenWithNbf() public function testBeforeValidTokenWithIat() { $this->setExpectedException('BeforeValidException'); - $timeInFuture = time() + 20; $payload = array( "message" => "abc", - "iat" => $timeInFuture // time in the future - ); + "iat" => time() + 20); // time in the future $encoded = JWT::encode($payload, 'my_key'); JWT::decode($encoded, 'my_key', array('HS256')); } From 800c39785e5ba2daba333669b3f7d62bbcf880a6 Mon Sep 17 00:00:00 2001 From: Luis Miguel Cabral Date: Fri, 15 May 2015 09:14:45 +0200 Subject: [PATCH 070/282] Added tests with leeway value in nbf and iat --- Jwt/tests/JWTTest.php | 44 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index 42fb5f90ce1..da9975c063c 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -120,6 +120,50 @@ public function testValidTokenWithNbf() $this->assertEquals($decoded->message, 'abc'); } + public function testValidTokenWithNbfLeeway() + { + JWT::$leeway = 60; + $payload = array( + "message" => "abc", + "nbf" => time() + 20); // not before in near (leeway) future + $encoded = JWT::encode($payload, 'my_key'); + $decoded = JWT::decode($encoded, 'my_key', array('HS256')); + $this->assertEquals($decoded->message, 'abc'); + } + + public function testInvalidTokenWithNbfLeeway() + { + JWT::$leeway = 60; + $payload = array( + "message" => "abc", + "nbf" => time() + 65); // not before too far in future + $encoded = JWT::encode($payload, 'my_key'); + $this->setExpectedException('BeforeValidException'); + $decoded = JWT::decode($encoded, 'my_key', array('HS256')); + } + + public function testValidTokenWithIatLeeway() + { + JWT::$leeway = 60; + $payload = array( + "message" => "abc", + "iat" => time() + 20); // issued in near (leeway) future + $encoded = JWT::encode($payload, 'my_key'); + $decoded = JWT::decode($encoded, 'my_key', array('HS256')); + $this->assertEquals($decoded->message, 'abc'); + } + + public function testInvalidTokenWithIatLeeway() + { + JWT::$leeway = 60; + $payload = array( + "message" => "abc", + "iat" => time() + 65); // issued too far in future + $encoded = JWT::encode($payload, 'my_key'); + $this->setExpectedException('BeforeValidException'); + $decoded = JWT::decode($encoded, 'my_key', array('HS256')); + } + public function testInvalidToken() { $payload = array( From e6436c2088a713c63f20bb4cd5f7b33e273c722f Mon Sep 17 00:00:00 2001 From: Rob DiMarco Date: Mon, 18 May 2015 14:38:18 -0700 Subject: [PATCH 071/282] Reset clock skew / leeway after relevant tests & update README --- Jwt/README.md | 8 ++++---- Jwt/tests/JWTTest.php | 6 ++++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Jwt/README.md b/Jwt/README.md index bda2d96bf32..d94b40ab2d4 100644 --- a/Jwt/README.md +++ b/Jwt/README.md @@ -47,12 +47,12 @@ $decoded_array = (array) $decoded; /** * You can add a leeway to account for when there is a clock skew times between - * the signing and verifying servers. It is recomended this leeway should not - * be bigger than a few minutes. + * the signing and verifying servers. It is recommended that this leeway should + * not be bigger than a few minutes. + * * Source: http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html#nbfDef */ - -JWT::$leeway = 60; +JWT::$leeway = 60; // $leeway in seconds $decoded = JWT::decode($jwt, $key, array('HS256')); ?> diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index da9975c063c..e8f510f2b53 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -84,6 +84,7 @@ public function testValidTokenWithLeeway() $encoded = JWT::encode($payload, 'my_key'); $decoded = JWT::decode($encoded, 'my_key', array('HS256')); $this->assertEquals($decoded->message, 'abc'); + JWT::$leeway = 0; } public function testExpiredTokenWithLeeway() @@ -96,6 +97,7 @@ public function testExpiredTokenWithLeeway() $encoded = JWT::encode($payload, 'my_key'); $decoded = JWT::decode($encoded, 'my_key', array('HS256')); $this->assertEquals($decoded->message, 'abc'); + JWT::$leeway = 0; } public function testValidTokenWithList() @@ -129,6 +131,7 @@ public function testValidTokenWithNbfLeeway() $encoded = JWT::encode($payload, 'my_key'); $decoded = JWT::decode($encoded, 'my_key', array('HS256')); $this->assertEquals($decoded->message, 'abc'); + JWT::$leeway = 0; } public function testInvalidTokenWithNbfLeeway() @@ -140,6 +143,7 @@ public function testInvalidTokenWithNbfLeeway() $encoded = JWT::encode($payload, 'my_key'); $this->setExpectedException('BeforeValidException'); $decoded = JWT::decode($encoded, 'my_key', array('HS256')); + JWT::$leeway = 0; } public function testValidTokenWithIatLeeway() @@ -151,6 +155,7 @@ public function testValidTokenWithIatLeeway() $encoded = JWT::encode($payload, 'my_key'); $decoded = JWT::decode($encoded, 'my_key', array('HS256')); $this->assertEquals($decoded->message, 'abc'); + JWT::$leeway = 0; } public function testInvalidTokenWithIatLeeway() @@ -162,6 +167,7 @@ public function testInvalidTokenWithIatLeeway() $encoded = JWT::encode($payload, 'my_key'); $this->setExpectedException('BeforeValidException'); $decoded = JWT::decode($encoded, 'my_key', array('HS256')); + JWT::$leeway = 0; } public function testInvalidToken() From 7d4405bc50c0ac4171f6ea6913c0f6a46ef978b6 Mon Sep 17 00:00:00 2001 From: Thibaud Fabre Date: Tue, 19 May 2015 12:39:00 +0200 Subject: [PATCH 072/282] Allow using \ArrayAccess as $key in decode --- Jwt/Authentication/JWT.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jwt/Authentication/JWT.php b/Jwt/Authentication/JWT.php index e65dc503c5a..7d6665bd687 100644 --- a/Jwt/Authentication/JWT.php +++ b/Jwt/Authentication/JWT.php @@ -73,7 +73,7 @@ public static function decode($jwt, $key = null, $allowed_algs = array()) if (!is_array($allowed_algs) || !in_array($header->alg, $allowed_algs)) { throw new DomainException('Algorithm not allowed'); } - if (is_array($key)) { + if (is_array($key) || $key instanceof \ArrayAccess) { if (isset($header->kid)) { $key = $key[$header->kid]; } else { From 5e2670f5bdd2a54080dad64f84e9e181e5628129 Mon Sep 17 00:00:00 2001 From: Aztech Date: Tue, 19 May 2015 12:46:52 +0200 Subject: [PATCH 073/282] Add tests for \ArrayAccess key chooser --- Jwt/tests/JWTTest.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index da9975c063c..b1fc1eb5f2b 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -194,6 +194,14 @@ public function testKIDChooser() $this->assertEquals($decoded, 'abc'); } + public function testArrayAccessKIDChooser() + { + $keys = new ArrayObject(array('1' => 'my_key', '2' => 'my_key2')); + $msg = JWT::encode('abc', $keys['1'], 'HS256', '1'); + $decoded = JWT::decode($msg, $keys, array('HS256')); + $this->assertEquals($decoded, 'abc'); + } + public function testNoneAlgorithm() { $msg = JWT::encode('abc', 'my_key'); From 9ffb6dd7bcbfc1a1b845264d8d60afbef4354885 Mon Sep 17 00:00:00 2001 From: Rob DiMarco Date: Wed, 20 May 2015 12:10:49 -0700 Subject: [PATCH 074/282] Update README for v2.1.0 release --- Jwt/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Jwt/README.md b/Jwt/README.md index d94b40ab2d4..00bad2ef200 100644 --- a/Jwt/README.md +++ b/Jwt/README.md @@ -61,6 +61,12 @@ $decoded = JWT::decode($jwt, $key, array('HS256')); Changelog --------- +#### 2.1.0 / 2015-05-20 +- Add support for adding a leeway to `JWT:decode()` that accounts for clock skew +between signing and verifying entities. Thanks to [@lcabral](https://github.com/lcabral)! +- Add support for passing an object implementing the `ArrayAccess` interface for +`$keys` argument in `JWT::decode()`. Thanks to [@aztech-dev](https://github.com/aztech-dev)! + #### 2.0.0 / 2015-04-01 - **Note**: It is strongly recommended that you update to > v2.0.0 to address known security vulnerabilities in prior versions when both symmetric and From 23859f7f9f44499e53fe30434de547c50e94d3c3 Mon Sep 17 00:00:00 2001 From: Rob DiMarco Date: Wed, 20 May 2015 12:16:04 -0700 Subject: [PATCH 075/282] Update package.xml --- Jwt/package.xml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Jwt/package.xml b/Jwt/package.xml index e06616de0de..b40da26d642 100644 --- a/Jwt/package.xml +++ b/Jwt/package.xml @@ -14,15 +14,15 @@ yes - Anant Narayanan - anant - anant@php.net + Firebase Operations + firebase + operations@firebase.com yes - 2015-04-01 + 2015-05-20 - 2.0.0 - 2.0.0 + 2.1.0 + 2.1.0 beta From 52eea26f7276a152bbfc6d4bb2de9658806e6517 Mon Sep 17 00:00:00 2001 From: Rob DiMarco Date: Wed, 20 May 2015 12:22:10 -0700 Subject: [PATCH 076/282] Add badges for latest release, total downloads, and license --- Jwt/README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Jwt/README.md b/Jwt/README.md index 00bad2ef200..b97dcb49a61 100644 --- a/Jwt/README.md +++ b/Jwt/README.md @@ -1,4 +1,7 @@ [![Build Status](https://travis-ci.org/firebase/php-jwt.png?branch=master)](https://travis-ci.org/firebase/php-jwt) +[![Latest Stable Version](https://poser.pugx.org/firebase/php-jwt/v/stable)](https://packagist.org/packages/firebase/php-jwt) +[![Total Downloads](https://poser.pugx.org/firebase/php-jwt/downloads)](https://packagist.org/packages/firebase/php-jwt) +[![License](https://poser.pugx.org/firebase/php-jwt/license)](https://packagist.org/packages/firebase/php-jwt) PHP-JWT ======= From 673b065f8b7dc17db29b47ad3eb22e4f1e4a534f Mon Sep 17 00:00:00 2001 From: Martin Cocaro Date: Wed, 17 Jun 2015 14:21:04 -0700 Subject: [PATCH 077/282] Adding header options to add --- Jwt/Authentication/JWT.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Jwt/Authentication/JWT.php b/Jwt/Authentication/JWT.php index 7d6665bd687..9c375395767 100644 --- a/Jwt/Authentication/JWT.php +++ b/Jwt/Authentication/JWT.php @@ -119,17 +119,21 @@ public static function decode($jwt, $key = null, $allowed_algs = array()) * @param string $key The secret key * @param string $alg The signing algorithm. Supported * algorithms are 'HS256', 'HS384' and 'HS512' + * @param array $head An array with header elements to attach * * @return string A signed JWT * @uses jsonEncode * @uses urlsafeB64Encode */ - public static function encode($payload, $key, $alg = 'HS256', $keyId = null) + public static function encode($payload, $key, $alg = 'HS256', $keyId = null, $head = null) { $header = array('typ' => 'JWT', 'alg' => $alg); if ($keyId !== null) { $header['kid'] = $keyId; } + if ( isset($head) && is_array($head) ) { + array_push($header, $head); + } $segments = array(); $segments[] = JWT::urlsafeB64Encode(JWT::jsonEncode($header)); $segments[] = JWT::urlsafeB64Encode(JWT::jsonEncode($payload)); From e3c3b9634d93bb35535d46101f56d159742a647b Mon Sep 17 00:00:00 2001 From: Martin Cocaro Date: Wed, 17 Jun 2015 14:59:20 -0700 Subject: [PATCH 078/282] Adding array_values only --- Jwt/Authentication/JWT.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jwt/Authentication/JWT.php b/Jwt/Authentication/JWT.php index 9c375395767..6efa30d2595 100644 --- a/Jwt/Authentication/JWT.php +++ b/Jwt/Authentication/JWT.php @@ -132,7 +132,7 @@ public static function encode($payload, $key, $alg = 'HS256', $keyId = null, $he $header['kid'] = $keyId; } if ( isset($head) && is_array($head) ) { - array_push($header, $head); + array_push($header, array_values($head)); } $segments = array(); $segments[] = JWT::urlsafeB64Encode(JWT::jsonEncode($header)); From b8cc28f08d4ba6f0824dfaa18614bbd22e573c52 Mon Sep 17 00:00:00 2001 From: Martin Cocaro Date: Wed, 17 Jun 2015 14:59:47 -0700 Subject: [PATCH 079/282] Merging both arrays together --- Jwt/Authentication/JWT.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jwt/Authentication/JWT.php b/Jwt/Authentication/JWT.php index 6efa30d2595..11d1e8245e3 100644 --- a/Jwt/Authentication/JWT.php +++ b/Jwt/Authentication/JWT.php @@ -132,7 +132,7 @@ public static function encode($payload, $key, $alg = 'HS256', $keyId = null, $he $header['kid'] = $keyId; } if ( isset($head) && is_array($head) ) { - array_push($header, array_values($head)); + array_merge($header, $head); } $segments = array(); $segments[] = JWT::urlsafeB64Encode(JWT::jsonEncode($header)); From d724caa33fcf1d0f57f4ddc9e5b2ab41f913aa48 Mon Sep 17 00:00:00 2001 From: Martin Cocaro Date: Wed, 17 Jun 2015 16:06:48 -0700 Subject: [PATCH 080/282] Assigning to header var merged array --- Jwt/Authentication/JWT.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jwt/Authentication/JWT.php b/Jwt/Authentication/JWT.php index 11d1e8245e3..8791000e30b 100644 --- a/Jwt/Authentication/JWT.php +++ b/Jwt/Authentication/JWT.php @@ -132,7 +132,7 @@ public static function encode($payload, $key, $alg = 'HS256', $keyId = null, $he $header['kid'] = $keyId; } if ( isset($head) && is_array($head) ) { - array_merge($header, $head); + $header = array_merge($header, $head); } $segments = array(); $segments[] = JWT::urlsafeB64Encode(JWT::jsonEncode($header)); From f5ba51673b4d6b263d98505235e7c89e6383fe70 Mon Sep 17 00:00:00 2001 From: Martin Cocaro Date: Wed, 17 Jun 2015 16:16:06 -0700 Subject: [PATCH 081/282] Adding test --- Jwt/tests/JWTTest.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index 0605e4ca07a..2aeb2017d17 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -228,4 +228,10 @@ public function testMissingAlgorithm() $this->setExpectedException('DomainException'); JWT::decode($msg, 'my_key'); } + + public function testAdditionalHeaders() + { + $msg = JWT::encode('abc', 'my_key', 'HS256', null, array('cty' => 'test-eit;v=1')); + $this->assertEquals(JWT::decode($msg, 'my_key', array('HS256')), 'abc'); + } } From 442b593e4155c1723651a6409cac5bca979b81b6 Mon Sep 17 00:00:00 2001 From: Martin Cocaro Date: Thu, 18 Jun 2015 11:29:01 -0700 Subject: [PATCH 082/282] Reversing array merge to avoid overwrites of default headers --- Jwt/Authentication/JWT.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jwt/Authentication/JWT.php b/Jwt/Authentication/JWT.php index 8791000e30b..161424bbdd8 100644 --- a/Jwt/Authentication/JWT.php +++ b/Jwt/Authentication/JWT.php @@ -132,7 +132,7 @@ public static function encode($payload, $key, $alg = 'HS256', $keyId = null, $he $header['kid'] = $keyId; } if ( isset($head) && is_array($head) ) { - $header = array_merge($header, $head); + $header = array_merge($head, $header); } $segments = array(); $segments[] = JWT::urlsafeB64Encode(JWT::jsonEncode($header)); From 3a3908b049597846eb9a799a701358e314c946f7 Mon Sep 17 00:00:00 2001 From: Rob DiMarco Date: Mon, 22 Jun 2015 16:26:39 -0700 Subject: [PATCH 083/282] Update README and bump to v2.2.0 --- Jwt/README.md | 5 +++++ Jwt/composer.lock | 19 +++++++++++++++++++ Jwt/composer.phar | Bin 0 -> 1103856 bytes Jwt/package.xml | 6 +++--- 4 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 Jwt/composer.lock create mode 100644 Jwt/composer.phar diff --git a/Jwt/README.md b/Jwt/README.md index b97dcb49a61..3fa2f3046d5 100644 --- a/Jwt/README.md +++ b/Jwt/README.md @@ -64,6 +64,11 @@ $decoded = JWT::decode($jwt, $key, array('HS256')); Changelog --------- +#### 2.2.0 / 2015-06-22 +- Add support for adding custom, optional JWT headers to `JWT::encode()`. See +[#53](https://github.com/firebase/php-jwt/pull/53/files) for details. Thanks to +[@mcocaro](https://github.com/mcocaro)! + #### 2.1.0 / 2015-05-20 - Add support for adding a leeway to `JWT:decode()` that accounts for clock skew between signing and verifying entities. Thanks to [@lcabral](https://github.com/lcabral)! diff --git a/Jwt/composer.lock b/Jwt/composer.lock new file mode 100644 index 00000000000..5518ae41cd5 --- /dev/null +++ b/Jwt/composer.lock @@ -0,0 +1,19 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "This file is @generated automatically" + ], + "hash": "60a5df5d283a7ae9000173248eba8909", + "packages": [], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=5.2.0" + }, + "platform-dev": [] +} diff --git a/Jwt/composer.phar b/Jwt/composer.phar new file mode 100644 index 0000000000000000000000000000000000000000..d1ec833d41b06250a1b086aa3a783c10f0da82b4 GIT binary patch literal 1103856 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

CB1#%K43|D^wP%UF&mTS1vlGe%{HZexn1BtoZE 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 BhpGSo literal 0 HcmV?d00001 diff --git a/Jwt/package.xml b/Jwt/package.xml index b40da26d642..efb2192b046 100644 --- a/Jwt/package.xml +++ b/Jwt/package.xml @@ -19,10 +19,10 @@ operations@firebase.com yes - 2015-05-20 + 2015-06-22 - 2.1.0 - 2.1.0 + 2.2.0 + 2.2.0 beta From 9f9742d6411b6d8616badd870233bd8b9af93eee Mon Sep 17 00:00:00 2001 From: Aaron Hedges Date: Wed, 1 Jul 2015 13:18:06 -0400 Subject: [PATCH 084/282] Add firespace\jwt namespace --- Jwt/Exceptions/BeforeValidException.php | 6 ------ Jwt/Exceptions/ExpiredException.php | 6 ------ Jwt/Exceptions/SignatureInvalidException.php | 6 ------ Jwt/composer.json | 6 ++++-- Jwt/src/BeforeValidException.php | 7 +++++++ Jwt/src/ExpiredException.php | 7 +++++++ Jwt/{Authentication => src}/JWT.php | 4 ++++ Jwt/src/SignatureInvalidException.php | 7 +++++++ Jwt/tests/JWTTest.php | 15 ++++++++------- 9 files changed, 37 insertions(+), 27 deletions(-) delete mode 100644 Jwt/Exceptions/BeforeValidException.php delete mode 100644 Jwt/Exceptions/ExpiredException.php delete mode 100644 Jwt/Exceptions/SignatureInvalidException.php create mode 100644 Jwt/src/BeforeValidException.php create mode 100644 Jwt/src/ExpiredException.php rename Jwt/{Authentication => src}/JWT.php (99%) create mode 100644 Jwt/src/SignatureInvalidException.php diff --git a/Jwt/Exceptions/BeforeValidException.php b/Jwt/Exceptions/BeforeValidException.php deleted file mode 100644 index 5a84975e868..00000000000 --- a/Jwt/Exceptions/BeforeValidException.php +++ /dev/null @@ -1,6 +0,0 @@ -=5.2.0" + "php": ">=5.3.0" }, "autoload": { - "classmap": ["Authentication/", "Exceptions/"] + "psr-4": { + "Firebase\\JWT\\": "src" + } }, "minimum-stability": "dev" } diff --git a/Jwt/src/BeforeValidException.php b/Jwt/src/BeforeValidException.php new file mode 100644 index 00000000000..a6ee2f7c690 --- /dev/null +++ b/Jwt/src/BeforeValidException.php @@ -0,0 +1,7 @@ +setExpectedException('ExpiredException'); + $this->setExpectedException('Firebase\JWT\ExpiredException'); $payload = array( "message" => "abc", "exp" => time() - 20); // time in the past @@ -47,7 +48,7 @@ public function testExpiredToken() public function testBeforeValidTokenWithNbf() { - $this->setExpectedException('BeforeValidException'); + $this->setExpectedException('Firebase\JWT\BeforeValidException'); $payload = array( "message" => "abc", "nbf" => time() + 20); // time in the future @@ -57,7 +58,7 @@ public function testBeforeValidTokenWithNbf() public function testBeforeValidTokenWithIat() { - $this->setExpectedException('BeforeValidException'); + $this->setExpectedException('Firebase\JWT\BeforeValidException'); $payload = array( "message" => "abc", "iat" => time() + 20); // time in the future @@ -93,7 +94,7 @@ public function testExpiredTokenWithLeeway() $payload = array( "message" => "abc", "exp" => time() - 70); // time far in the past - $this->setExpectedException('ExpiredException'); + $this->setExpectedException('Firebase\JWT\ExpiredException'); $encoded = JWT::encode($payload, 'my_key'); $decoded = JWT::decode($encoded, 'my_key', array('HS256')); $this->assertEquals($decoded->message, 'abc'); @@ -141,7 +142,7 @@ public function testInvalidTokenWithNbfLeeway() "message" => "abc", "nbf" => time() + 65); // not before too far in future $encoded = JWT::encode($payload, 'my_key'); - $this->setExpectedException('BeforeValidException'); + $this->setExpectedException('Firebase\JWT\BeforeValidException'); $decoded = JWT::decode($encoded, 'my_key', array('HS256')); JWT::$leeway = 0; } @@ -165,7 +166,7 @@ public function testInvalidTokenWithIatLeeway() "message" => "abc", "iat" => time() + 65); // issued too far in future $encoded = JWT::encode($payload, 'my_key'); - $this->setExpectedException('BeforeValidException'); + $this->setExpectedException('Firebase\JWT\BeforeValidException'); $decoded = JWT::decode($encoded, 'my_key', array('HS256')); JWT::$leeway = 0; } @@ -176,7 +177,7 @@ public function testInvalidToken() "message" => "abc", "exp" => time() + 20); // time in the future $encoded = JWT::encode($payload, 'my_key'); - $this->setExpectedException('SignatureInvalidException'); + $this->setExpectedException('Firebase\JWT\SignatureInvalidException'); $decoded = JWT::decode($encoded, 'my_key2', array('HS256')); } From b3ec84d801f8fcdb1639e6afea6eb8b0d21b606b Mon Sep 17 00:00:00 2001 From: Aaron Hedges Date: Wed, 1 Jul 2015 13:22:41 -0400 Subject: [PATCH 085/282] update example to use new namespace --- Jwt/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/Jwt/README.md b/Jwt/README.md index 3fa2f3046d5..b891c2c16a7 100644 --- a/Jwt/README.md +++ b/Jwt/README.md @@ -21,6 +21,7 @@ Example ------- ```php Date: Wed, 1 Jul 2015 14:18:43 -0400 Subject: [PATCH 086/282] fix unexpectedvalueexceptions and add unit tests --- Jwt/src/JWT.php | 1 + Jwt/tests/JWTTest.php | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index 855b7429c05..d1a108f32fd 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -2,6 +2,7 @@ namespace Firebase\JWT; use \DomainException; +use \UnexpectedValueException; use \DateTime; /** diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index 6c996b27d8a..f06f34e404f 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -235,4 +235,10 @@ public function testAdditionalHeaders() $msg = JWT::encode('abc', 'my_key', 'HS256', null, array('cty' => 'test-eit;v=1')); $this->assertEquals(JWT::decode($msg, 'my_key', array('HS256')), 'abc'); } + + public function testInvalidSegmentCount() + { + $this->setExpectedException('UnexpectedValueException'); + JWT::decode('brokenheader.brokenbody', 'my_key', array('HS256')); + } } From e5c674869cc3ea60d39d89188896a36323ecd21d Mon Sep 17 00:00:00 2001 From: Rob DiMarco Date: Sun, 5 Jul 2015 06:14:22 -0700 Subject: [PATCH 087/282] Add composer.phar to gitignore --- Jwt/.gitignore | 1 + Jwt/composer.phar | Bin 1103856 -> 0 bytes 2 files changed, 1 insertion(+) delete mode 100644 Jwt/composer.phar diff --git a/Jwt/.gitignore b/Jwt/.gitignore index 96e2f2e1a51..7c29c87b85d 100644 --- a/Jwt/.gitignore +++ b/Jwt/.gitignore @@ -1,3 +1,4 @@ vendor phpunit.phar phpunit.phar.asc +composer.phar diff --git a/Jwt/composer.phar b/Jwt/composer.phar deleted file mode 100644 index d1ec833d41b06250a1b086aa3a783c10f0da82b4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1103856 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

CB1#%K43|D^wP%UF&mTS1vlGe%{HZexn1BtoZE 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 BhpGSo From 81391cdbec6330fa49465560e86ace78a4f8ca94 Mon Sep 17 00:00:00 2001 From: Steve Jones Date: Wed, 8 Jul 2015 12:50:06 -0500 Subject: [PATCH 088/282] require a non-empty key to decode a JWT --- Jwt/Authentication/JWT.php | 82 +++++++++++++++++++------------------- Jwt/tests/JWTTest.php | 20 ++++++++++ 2 files changed, 62 insertions(+), 40 deletions(-) diff --git a/Jwt/Authentication/JWT.php b/Jwt/Authentication/JWT.php index 161424bbdd8..becd100b19a 100644 --- a/Jwt/Authentication/JWT.php +++ b/Jwt/Authentication/JWT.php @@ -49,8 +49,11 @@ class JWT * @uses jsonDecode * @uses urlsafeB64Decode */ - public static function decode($jwt, $key = null, $allowed_algs = array()) + public static function decode($jwt, $key, $allowed_algs = array()) { + if (empty($key)) { + throw new InvalidArgumentException('Key may not be empty'); + } $tks = explode('.', $jwt); if (count($tks) != 3) { throw new UnexpectedValueException('Wrong number of segments'); @@ -63,50 +66,49 @@ public static function decode($jwt, $key = null, $allowed_algs = array()) throw new UnexpectedValueException('Invalid claims encoding'); } $sig = JWT::urlsafeB64Decode($cryptob64); - if (isset($key)) { - if (empty($header->alg)) { - throw new DomainException('Empty algorithm'); - } - if (empty(self::$supported_algs[$header->alg])) { - throw new DomainException('Algorithm not supported'); - } - if (!is_array($allowed_algs) || !in_array($header->alg, $allowed_algs)) { - throw new DomainException('Algorithm not allowed'); - } - if (is_array($key) || $key instanceof \ArrayAccess) { - if (isset($header->kid)) { - $key = $key[$header->kid]; - } else { - throw new DomainException('"kid" empty, unable to lookup correct key'); - } + + if (empty($header->alg)) { + throw new DomainException('Empty algorithm'); + } + if (empty(self::$supported_algs[$header->alg])) { + throw new DomainException('Algorithm not supported'); + } + if (!is_array($allowed_algs) || !in_array($header->alg, $allowed_algs)) { + throw new DomainException('Algorithm not allowed'); + } + if (is_array($key) || $key instanceof \ArrayAccess) { + if (isset($header->kid)) { + $key = $key[$header->kid]; + } else { + throw new DomainException('"kid" empty, unable to lookup correct key'); } + } - // Check the signature - if (!JWT::verify("$headb64.$bodyb64", $sig, $key, $header->alg)) { - throw new SignatureInvalidException('Signature verification failed'); - } + // Check the signature + if (!JWT::verify("$headb64.$bodyb64", $sig, $key, $header->alg)) { + throw new SignatureInvalidException('Signature verification failed'); + } - // Check if the nbf if it is defined. This is the time that the - // token can actually be used. If it's not yet that time, abort. - if (isset($payload->nbf) && $payload->nbf > (time() + self::$leeway)) { - throw new BeforeValidException( - 'Cannot handle token prior to ' . date(DateTime::ISO8601, $payload->nbf) - ); - } + // Check if the nbf if it is defined. This is the time that the + // token can actually be used. If it's not yet that time, abort. + if (isset($payload->nbf) && $payload->nbf > (time() + self::$leeway)) { + throw new BeforeValidException( + 'Cannot handle token prior to ' . date(DateTime::ISO8601, $payload->nbf) + ); + } - // Check that this token has been created before 'now'. This prevents - // using tokens that have been created for later use (and haven't - // correctly used the nbf claim). - if (isset($payload->iat) && $payload->iat > (time() + self::$leeway)) { - throw new BeforeValidException( - 'Cannot handle token prior to ' . date(DateTime::ISO8601, $payload->iat) - ); - } + // Check that this token has been created before 'now'. This prevents + // using tokens that have been created for later use (and haven't + // correctly used the nbf claim). + if (isset($payload->iat) && $payload->iat > (time() + self::$leeway)) { + throw new BeforeValidException( + 'Cannot handle token prior to ' . date(DateTime::ISO8601, $payload->iat) + ); + } - // Check if this token has expired. - if (isset($payload->exp) && (time() - self::$leeway) >= $payload->exp) { - throw new ExpiredException('Expired token'); - } + // Check if this token has expired. + if (isset($payload->exp) && (time() - self::$leeway) >= $payload->exp) { + throw new ExpiredException('Expired token'); } return $payload; diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index 2aeb2017d17..ab1ae369115 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -180,6 +180,26 @@ public function testInvalidToken() $decoded = JWT::decode($encoded, 'my_key2', array('HS256')); } + public function testNullKeyFails() + { + $payload = array( + "message" => "abc", + "exp" => time() + JWT::$leeway + 20); // time in the future + $encoded = JWT::encode($payload, 'my_key'); + $this->setExpectedException('InvalidArgumentException'); + $decoded = JWT::decode($encoded, null, array('HS256')); + } + + public function testEmptyKeyFails() + { + $payload = array( + "message" => "abc", + "exp" => time() + JWT::$leeway + 20); // time in the future + $encoded = JWT::encode($payload, 'my_key'); + $this->setExpectedException('InvalidArgumentException'); + $decoded = JWT::decode($encoded, '', array('HS256')); + } + public function testRSEncodeDecode() { $privKey = openssl_pkey_new(array('digest_alg' => 'sha256', From ce094ce880e4f7b0ad6c9418f8e14441188cefa6 Mon Sep 17 00:00:00 2001 From: Johan de Ruijter Date: Wed, 15 Jul 2015 19:45:30 +0200 Subject: [PATCH 089/282] Aligned the DocBlocks Clarified and corrected the documentation where needed. --- Jwt/src/JWT.php | 55 +++++++++++++++++++++++++++++-------------------- 1 file changed, 33 insertions(+), 22 deletions(-) diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index d1a108f32fd..ff7c07d4a80 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -38,11 +38,13 @@ class JWT /** * Decodes a JWT string into a PHP object. * - * @param string $jwt The JWT - * @param string|Array|null $key The secret key, or map of keys - * @param Array $allowed_algs List of supported verification algorithms + * @param string $jwt The JWT + * @param string|array|null $key The key, or map of keys. + * If the algorithm used is asymmetric, this is the public key + * @param array $allowed_algs List of supported verification algorithms + * Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256' * - * @return object The JWT's payload as a PHP object + * @return object The JWT's payload as a PHP object * * @throws DomainException Algorithm was not provided * @throws UnexpectedValueException Provided JWT was invalid @@ -120,13 +122,15 @@ public static function decode($jwt, $key = null, $allowed_algs = array()) /** * Converts and signs a PHP object or array into a JWT string. * - * @param object|array $payload PHP object or array - * @param string $key The secret key - * @param string $alg The signing algorithm. Supported - * algorithms are 'HS256', 'HS384' and 'HS512' - * @param array $head An array with header elements to attach + * @param object|array $payload PHP object or array + * @param string $key The secret key. + * If the algorithm used is asymmetric, this is the private key + * @param string $alg The signing algorithm. + * Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256' + * @param array $head An array with header elements to attach + * + * @return string A signed JWT * - * @return string A signed JWT * @uses jsonEncode * @uses urlsafeB64Encode */ @@ -153,12 +157,13 @@ public static function encode($payload, $key, $alg = 'HS256', $keyId = null, $he /** * Sign a string with a given key and algorithm. * - * @param string $msg The message to sign - * @param string|resource $key The secret key - * @param string $alg The signing algorithm. Supported algorithms - * are 'HS256', 'HS384', 'HS512' and 'RS256' + * @param string $msg The message to sign + * @param string|resource $key The secret key + * @param string $alg The signing algorithm. + * Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256' + * + * @return string An encrypted message * - * @return string An encrypted message * @throws DomainException Unsupported algorithm was specified */ public static function sign($msg, $key, $alg = 'HS256') @@ -182,13 +187,16 @@ public static function sign($msg, $key, $alg = 'HS256') } /** - * Verify a signature with the mesage, key and method. Not all methods + * Verify a signature with the message, key and method. Not all methods * are symmetric, so we must have a separate verify and sign method. - * @param string $msg the original message - * @param string $signature - * @param string|resource $key for HS*, a string key works. for RS*, must be a resource of an openssl public key - * @param string $alg + * + * @param string $msg The original message (header and body) + * @param string $signature The original signature + * @param string|resource $key For HS*, a string key works. for RS*, must be a resource of an openssl public key + * @param string $alg The algorithm + * * @return bool + * * @throws DomainException Invalid Algorithm or OpenSSL failure */ private static function verify($msg, $signature, $key, $alg) @@ -229,7 +237,8 @@ private static function verify($msg, $signature, $key, $alg) * * @param string $input JSON string * - * @return object Object representation of JSON string + * @return object Object representation of JSON string + * * @throws DomainException Provided string was invalid JSON */ public static function jsonDecode($input) @@ -263,7 +272,8 @@ public static function jsonDecode($input) * * @param object|array $input A PHP object or array * - * @return string JSON representation of the PHP object or array + * @return string JSON representation of the PHP object or array + * * @throws DomainException Provided object could not be encoded to valid JSON */ public static function jsonEncode($input) @@ -331,6 +341,7 @@ private static function handleJsonError($errno) * Get the number of bytes in cryptographic strings. * * @param string + * * @return int */ private static function safeStrlen($str) From 1a779fa4f068d165303587a09a12bf21aaa4d221 Mon Sep 17 00:00:00 2001 From: Steve Jones Date: Tue, 21 Jul 2015 17:19:09 -0500 Subject: [PATCH 090/282] add use InvalidArgumentException to account for JWT namespace --- Jwt/src/JWT.php | 1 + 1 file changed, 1 insertion(+) diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index e8426b58241..b3532df7e0b 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -2,6 +2,7 @@ namespace Firebase\JWT; use \DomainException; +use \InvalidArgumentException; use \UnexpectedValueException; use \DateTime; From 743b249cb55eaf81be6fc8dc23a309e1e5f18036 Mon Sep 17 00:00:00 2001 From: Rob DiMarco Date: Wed, 22 Jul 2015 11:31:08 -0700 Subject: [PATCH 091/282] Update package.xml and README.md for 3.0.0 release --- Jwt/README.md | 12 ++++++++++++ Jwt/package.xml | 6 +++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/Jwt/README.md b/Jwt/README.md index b891c2c16a7..7f1b026b16d 100644 --- a/Jwt/README.md +++ b/Jwt/README.md @@ -65,6 +65,18 @@ $decoded = JWT::decode($jwt, $key, array('HS256')); Changelog --------- +#### 3.0.0 / 2015-07-22 +- Minimum PHP version updated from `5.2.0` to `5.3.0`. +- Add `\Firebase\JWT` namespace. See +[#59](https://github.com/firebase/php-jwt/pull/59) for details. Thanks to +[@Dashron](https://github.com/Dashron)! +- Require a non-empty key to decode and verify a JWT. See +[#60](https://github.com/firebase/php-jwt/pull/60) for details. Thanks to +[@sjones608](https://github.com/sjones608)! +- Cleaner documentation blocks in the code. See +[#62](https://github.com/firebase/php-jwt/pull/62) for details. Thanks to +[@johanderuijter](https://github.com/johanderuijter)! + #### 2.2.0 / 2015-06-22 - Add support for adding custom, optional JWT headers to `JWT::encode()`. See [#53](https://github.com/firebase/php-jwt/pull/53/files) for details. Thanks to diff --git a/Jwt/package.xml b/Jwt/package.xml index efb2192b046..a95b056f714 100644 --- a/Jwt/package.xml +++ b/Jwt/package.xml @@ -19,10 +19,10 @@ operations@firebase.com yes - 2015-06-22 + 2015-07-22 - 2.2.0 - 2.2.0 + 3.0.0 + 3.0.0 beta From c0ba03f5100935cce0cf1b0d54b18d2a59118e3b Mon Sep 17 00:00:00 2001 From: Jacob Wenger Date: Tue, 8 Sep 2015 11:38:36 -0500 Subject: [PATCH 092/282] Update Travis to use their new container infrastructure See [here](http://docs.travis-ci.com/user/migrating-from-legacy/) for details. --- Jwt/.travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Jwt/.travis.yml b/Jwt/.travis.yml index 0bd12c5ab00..b1135a16bb4 100644 --- a/Jwt/.travis.yml +++ b/Jwt/.travis.yml @@ -6,6 +6,8 @@ php: - 5.5 - hhvm +sudo: false + before_script: - wget -nc http://getcomposer.org/composer.phar - php composer.phar install From 5a2f30e6cbe7d987134c89248eec869e55222266 Mon Sep 17 00:00:00 2001 From: Rob DiMarco Date: Wed, 18 Nov 2015 22:05:17 -0800 Subject: [PATCH 093/282] Addresses issue firebase/php-jwt#72 - update link to current spec --- Jwt/README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Jwt/README.md b/Jwt/README.md index 7f1b026b16d..5d53658dd9a 100644 --- a/Jwt/README.md +++ b/Jwt/README.md @@ -5,8 +5,7 @@ PHP-JWT ======= -A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should -conform to the [current spec](http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-06) +A simple library to encode and decode JSON Web Tokens (JWT) in PHP, conforming to [RFC 7519](https://tools.ietf.org/html/rfc7519). Installation ------------ From 13357e5bba2f31e3088679b07f86d5013c58c454 Mon Sep 17 00:00:00 2001 From: Arjan Keeman Date: Wed, 30 Dec 2015 22:08:26 +0100 Subject: [PATCH 094/282] Remove null as possible key value in docblock The docblock states that null is a possible value for $key, however, it clearly results in an exception. --- Jwt/src/JWT.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index b3532df7e0b..955351b519b 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -39,11 +39,11 @@ class JWT /** * Decodes a JWT string into a PHP object. * - * @param string $jwt The JWT - * @param string|array|null $key The key, or map of keys. - * If the algorithm used is asymmetric, this is the public key - * @param array $allowed_algs List of supported verification algorithms - * Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256' + * @param string $jwt The JWT + * @param string|array $key The key, or map of keys. + * If the algorithm used is asymmetric, this is the public key + * @param array $allowed_algs List of supported verification algorithms + * Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256' * * @return object The JWT's payload as a PHP object * From dc498a1da249d73c3df21a7c755d3ac8a18d525f Mon Sep 17 00:00:00 2001 From: Lars Moelleken Date: Mon, 1 Feb 2016 13:26:42 +0100 Subject: [PATCH 095/282] [+]: ".gitattributes" <- prevent installing special files via composer [*]: add missing phpdoc --- Jwt/.gitattributes | 8 ++++++++ Jwt/src/JWT.php | 1 + 2 files changed, 9 insertions(+) create mode 100644 Jwt/.gitattributes diff --git a/Jwt/.gitattributes b/Jwt/.gitattributes new file mode 100644 index 00000000000..c682f8617b5 --- /dev/null +++ b/Jwt/.gitattributes @@ -0,0 +1,8 @@ +* text=auto + +/tests export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/.travis.yml export-ignore +/phpunit.xml.dist export-ignore +/run-tests.sh export-ignore diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index b3532df7e0b..78512a194b8 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -130,6 +130,7 @@ public static function decode($jwt, $key, $allowed_algs = array()) * If the algorithm used is asymmetric, this is the private key * @param string $alg The signing algorithm. * Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256' + * @param mixed $keyId * @param array $head An array with header elements to attach * * @return string A signed JWT From 2873bd16abd10626b19684e367267fce11b347a6 Mon Sep 17 00:00:00 2001 From: Joseph McDermott Date: Fri, 29 Apr 2016 15:18:23 +0100 Subject: [PATCH 096/282] Allow timestamp to be specified rather than relying on PHP time() function --- Jwt/src/JWT.php | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index b3532df7e0b..6a12063145e 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -29,6 +29,13 @@ class JWT */ public static $leeway = 0; + /** + * Allow the current timestamp to be specified. + * Useful for fixing a value within unit testing. + * Will default to PHP time() value if null. + */ + public static $timestamp = null; + public static $supported_algs = array( 'HS256' => array('hash_hmac', 'SHA256'), 'HS512' => array('hash_hmac', 'SHA512'), @@ -59,6 +66,8 @@ class JWT */ public static function decode($jwt, $key, $allowed_algs = array()) { + $timestamp = is_null(self::$timestamp) ? time() : self::$timestamp; + if (empty($key)) { throw new InvalidArgumentException('Key may not be empty'); } @@ -99,7 +108,7 @@ public static function decode($jwt, $key, $allowed_algs = array()) // Check if the nbf if it is defined. This is the time that the // token can actually be used. If it's not yet that time, abort. - if (isset($payload->nbf) && $payload->nbf > (time() + self::$leeway)) { + if (isset($payload->nbf) && $payload->nbf > ($timestamp + self::$leeway)) { throw new BeforeValidException( 'Cannot handle token prior to ' . date(DateTime::ISO8601, $payload->nbf) ); @@ -108,14 +117,14 @@ public static function decode($jwt, $key, $allowed_algs = array()) // Check that this token has been created before 'now'. This prevents // using tokens that have been created for later use (and haven't // correctly used the nbf claim). - if (isset($payload->iat) && $payload->iat > (time() + self::$leeway)) { + if (isset($payload->iat) && $payload->iat > ($timestamp + self::$leeway)) { throw new BeforeValidException( 'Cannot handle token prior to ' . date(DateTime::ISO8601, $payload->iat) ); } // Check if this token has expired. - if (isset($payload->exp) && (time() - self::$leeway) >= $payload->exp) { + if (isset($payload->exp) && ($timestamp - self::$leeway) >= $payload->exp) { throw new ExpiredException('Expired token'); } From 3b2409155a804c822ba13e4e0dba6b08970c158e Mon Sep 17 00:00:00 2001 From: Joseph McDermott Date: Tue, 3 May 2016 09:43:19 +0100 Subject: [PATCH 097/282] Commit with Google email address for CLA --- Jwt/src/JWT.php | 1 + 1 file changed, 1 insertion(+) diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index 6a12063145e..5b4304046ad 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -32,6 +32,7 @@ class JWT /** * Allow the current timestamp to be specified. * Useful for fixing a value within unit testing. + * * Will default to PHP time() value if null. */ public static $timestamp = null; From 379364fe118b1de44587fc31549c6244aeb3d788 Mon Sep 17 00:00:00 2001 From: Kunal Mehta Date: Thu, 9 Jun 2016 16:34:13 -0700 Subject: [PATCH 098/282] Remove newline at beginning of run-tests.sh The extra newline trips a warning in lintian when packaging for Debian, so just remove it. --- Jwt/run-tests.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/Jwt/run-tests.sh b/Jwt/run-tests.sh index d37c30fd754..c4bb9348415 100755 --- a/Jwt/run-tests.sh +++ b/Jwt/run-tests.sh @@ -1,4 +1,3 @@ - #!/usr/bin/env bash gpg --fingerprint D8406D0D82947747293778314AA394086372C20A if [ $? -ne 0 ]; then From 11014c88c6ac6a41f7c5fdddd5d4fd5253134b6d Mon Sep 17 00:00:00 2001 From: Maks3w Date: Thu, 16 Jun 2016 22:01:28 +0200 Subject: [PATCH 099/282] Fix exceptions classes (firebase/php-jwt#81) * Use InvalidArgumentException when $allowed_algs is not array > Exception thrown if an argument is not of the expected type. http://php.net/manual/en/class.invalidargumentexception.php * Use RuntimeExceptions for exceptions related with unencoded data. RuntimeExceptions is the correct exception error source is the decoded data. Note LogicExceptions as defined in PHP documentation implies a modification in the code by the developer. > Exception that represents error in the program logic. This kind of exception should lead directly to a fix in your code. http://php.net/manual/en/class.logicexception.php But the token is a data provided by an external source which is out side of the control of the developer so there is no way of prevent malformed tokens. --- Jwt/src/JWT.php | 14 ++++++++------ Jwt/tests/JWTTest.php | 6 +++--- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index 7228e96a466..0e613688a56 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -55,7 +55,6 @@ class JWT * * @return object The JWT's payload as a PHP object * - * @throws DomainException Algorithm was not provided * @throws UnexpectedValueException Provided JWT was invalid * @throws SignatureInvalidException Provided JWT was invalid because the signature verification failed * @throws BeforeValidException Provided JWT is trying to be used before it's eligible as defined by 'nbf' @@ -72,6 +71,9 @@ public static function decode($jwt, $key, $allowed_algs = array()) if (empty($key)) { throw new InvalidArgumentException('Key may not be empty'); } + if (!is_array($allowed_algs)) { + throw new InvalidArgumentException('Algorithm not allowed'); + } $tks = explode('.', $jwt); if (count($tks) != 3) { throw new UnexpectedValueException('Wrong number of segments'); @@ -86,19 +88,19 @@ public static function decode($jwt, $key, $allowed_algs = array()) $sig = JWT::urlsafeB64Decode($cryptob64); if (empty($header->alg)) { - throw new DomainException('Empty algorithm'); + throw new UnexpectedValueException('Empty algorithm'); } if (empty(self::$supported_algs[$header->alg])) { - throw new DomainException('Algorithm not supported'); + throw new UnexpectedValueException('Algorithm not supported'); } - if (!is_array($allowed_algs) || !in_array($header->alg, $allowed_algs)) { - throw new DomainException('Algorithm not allowed'); + if (!in_array($header->alg, $allowed_algs)) { + throw new UnexpectedValueException('Algorithm not allowed'); } if (is_array($key) || $key instanceof \ArrayAccess) { if (isset($header->kid)) { $key = $key[$header->kid]; } else { - throw new DomainException('"kid" empty, unable to lookup correct key'); + throw new UnexpectedValueException('"kid" empty, unable to lookup correct key'); } } diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index 89de8d28a4f..e99ea03a628 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -232,21 +232,21 @@ public function testArrayAccessKIDChooser() public function testNoneAlgorithm() { $msg = JWT::encode('abc', 'my_key'); - $this->setExpectedException('DomainException'); + $this->setExpectedException('UnexpectedValueException'); JWT::decode($msg, 'my_key', array('none')); } public function testIncorrectAlgorithm() { $msg = JWT::encode('abc', 'my_key'); - $this->setExpectedException('DomainException'); + $this->setExpectedException('UnexpectedValueException'); JWT::decode($msg, 'my_key', array('RS256')); } public function testMissingAlgorithm() { $msg = JWT::encode('abc', 'my_key'); - $this->setExpectedException('DomainException'); + $this->setExpectedException('UnexpectedValueException'); JWT::decode($msg, 'my_key'); } From f332440b3d57b1cb555f509655ae3bb3bdd54b8f Mon Sep 17 00:00:00 2001 From: Tom Chapman Date: Tue, 28 Jun 2016 17:57:35 +0100 Subject: [PATCH 100/282] Add in support for late static binding, so that developers can extend and override variables / methods if they so require (firebase/php-jwt#88) --- Jwt/src/JWT.php | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index 0e613688a56..6d30e941c98 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -66,7 +66,7 @@ class JWT */ public static function decode($jwt, $key, $allowed_algs = array()) { - $timestamp = is_null(self::$timestamp) ? time() : self::$timestamp; + $timestamp = is_null(static::$timestamp) ? time() : static::$timestamp; if (empty($key)) { throw new InvalidArgumentException('Key may not be empty'); @@ -79,18 +79,18 @@ public static function decode($jwt, $key, $allowed_algs = array()) throw new UnexpectedValueException('Wrong number of segments'); } list($headb64, $bodyb64, $cryptob64) = $tks; - if (null === ($header = JWT::jsonDecode(JWT::urlsafeB64Decode($headb64)))) { + if (null === ($header = static::jsonDecode(static::urlsafeB64Decode($headb64)))) { throw new UnexpectedValueException('Invalid header encoding'); } - if (null === $payload = JWT::jsonDecode(JWT::urlsafeB64Decode($bodyb64))) { + if (null === $payload = static::jsonDecode(static::urlsafeB64Decode($bodyb64))) { throw new UnexpectedValueException('Invalid claims encoding'); } - $sig = JWT::urlsafeB64Decode($cryptob64); + $sig = static::urlsafeB64Decode($cryptob64); if (empty($header->alg)) { throw new UnexpectedValueException('Empty algorithm'); } - if (empty(self::$supported_algs[$header->alg])) { + if (empty(static::$supported_algs[$header->alg])) { throw new UnexpectedValueException('Algorithm not supported'); } if (!in_array($header->alg, $allowed_algs)) { @@ -105,13 +105,13 @@ public static function decode($jwt, $key, $allowed_algs = array()) } // Check the signature - if (!JWT::verify("$headb64.$bodyb64", $sig, $key, $header->alg)) { + if (!static::verify("$headb64.$bodyb64", $sig, $key, $header->alg)) { throw new SignatureInvalidException('Signature verification failed'); } // Check if the nbf if it is defined. This is the time that the // token can actually be used. If it's not yet that time, abort. - if (isset($payload->nbf) && $payload->nbf > ($timestamp + self::$leeway)) { + if (isset($payload->nbf) && $payload->nbf > ($timestamp + static::$leeway)) { throw new BeforeValidException( 'Cannot handle token prior to ' . date(DateTime::ISO8601, $payload->nbf) ); @@ -120,14 +120,14 @@ public static function decode($jwt, $key, $allowed_algs = array()) // Check that this token has been created before 'now'. This prevents // using tokens that have been created for later use (and haven't // correctly used the nbf claim). - if (isset($payload->iat) && $payload->iat > ($timestamp + self::$leeway)) { + if (isset($payload->iat) && $payload->iat > ($timestamp + static::$leeway)) { throw new BeforeValidException( 'Cannot handle token prior to ' . date(DateTime::ISO8601, $payload->iat) ); } // Check if this token has expired. - if (isset($payload->exp) && ($timestamp - self::$leeway) >= $payload->exp) { + if (isset($payload->exp) && ($timestamp - static::$leeway) >= $payload->exp) { throw new ExpiredException('Expired token'); } @@ -160,12 +160,12 @@ public static function encode($payload, $key, $alg = 'HS256', $keyId = null, $he $header = array_merge($head, $header); } $segments = array(); - $segments[] = JWT::urlsafeB64Encode(JWT::jsonEncode($header)); - $segments[] = JWT::urlsafeB64Encode(JWT::jsonEncode($payload)); + $segments[] = static::urlsafeB64Encode(static::jsonEncode($header)); + $segments[] = static::urlsafeB64Encode(static::jsonEncode($payload)); $signing_input = implode('.', $segments); - $signature = JWT::sign($signing_input, $key, $alg); - $segments[] = JWT::urlsafeB64Encode($signature); + $signature = static::sign($signing_input, $key, $alg); + $segments[] = static::urlsafeB64Encode($signature); return implode('.', $segments); } @@ -184,10 +184,10 @@ public static function encode($payload, $key, $alg = 'HS256', $keyId = null, $he */ public static function sign($msg, $key, $alg = 'HS256') { - if (empty(self::$supported_algs[$alg])) { + if (empty(static::$supported_algs[$alg])) { throw new DomainException('Algorithm not supported'); } - list($function, $algorithm) = self::$supported_algs[$alg]; + list($function, $algorithm) = static::$supported_algs[$alg]; switch($function) { case 'hash_hmac': return hash_hmac($algorithm, $msg, $key, true); @@ -217,11 +217,11 @@ public static function sign($msg, $key, $alg = 'HS256') */ private static function verify($msg, $signature, $key, $alg) { - if (empty(self::$supported_algs[$alg])) { + if (empty(static::$supported_algs[$alg])) { throw new DomainException('Algorithm not supported'); } - list($function, $algorithm) = self::$supported_algs[$alg]; + list($function, $algorithm) = static::$supported_algs[$alg]; switch($function) { case 'openssl': $success = openssl_verify($msg, $signature, $key, $algorithm); @@ -236,13 +236,13 @@ private static function verify($msg, $signature, $key, $alg) if (function_exists('hash_equals')) { return hash_equals($signature, $hash); } - $len = min(self::safeStrlen($signature), self::safeStrlen($hash)); + $len = min(static::safeStrlen($signature), static::safeStrlen($hash)); $status = 0; for ($i = 0; $i < $len; $i++) { $status |= (ord($signature[$i]) ^ ord($hash[$i])); } - $status |= (self::safeStrlen($signature) ^ self::safeStrlen($hash)); + $status |= (static::safeStrlen($signature) ^ static::safeStrlen($hash)); return ($status === 0); } @@ -276,7 +276,7 @@ public static function jsonDecode($input) } if (function_exists('json_last_error') && $errno = json_last_error()) { - JWT::handleJsonError($errno); + static::handleJsonError($errno); } elseif ($obj === null && $input !== 'null') { throw new DomainException('Null result with non-null input'); } @@ -296,7 +296,7 @@ public static function jsonEncode($input) { $json = json_encode($input); if (function_exists('json_last_error') && $errno = json_last_error()) { - JWT::handleJsonError($errno); + static::handleJsonError($errno); } elseif ($json === 'null' && $input !== null) { throw new DomainException('Null result with non-null input'); } From 71cf43b568d36150f3fadd33e78ffa996b6dabb6 Mon Sep 17 00:00:00 2001 From: Rob DiMarco Date: Sun, 17 Jul 2016 21:51:16 -0700 Subject: [PATCH 101/282] Update README for v4.0.0 --- Jwt/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Jwt/README.md b/Jwt/README.md index 5d53658dd9a..d4589b1c25a 100644 --- a/Jwt/README.md +++ b/Jwt/README.md @@ -64,6 +64,12 @@ $decoded = JWT::decode($jwt, $key, array('HS256')); Changelog --------- +#### 4.0.0 / 2016-07-17 +- Add support for late static binding. See [#88](https://github.com/firebase/php-jwt/pull/88) for details. Thanks to [@chappy84](https://github.com/chappy84)! +- Use static `$timestamp` instead of `time()` to improve unit testing. See [#93](https://github.com/firebase/php-jwt/pull/93) for details. Thanks to [@josephmcdermott](https://github.com/josephmcdermott)! +- Fixes to exceptions classes. See [#81](https://github.com/firebase/php-jwt/pull/81) for details. Thanks to [@Maks3w](https://github.com/Maks3w)! +- Fixes to PHPDoc. See [#76](https://github.com/firebase/php-jwt/pull/76) for details. Thanks to [@akeeman](https://github.com/akeeman)! + #### 3.0.0 / 2015-07-22 - Minimum PHP version updated from `5.2.0` to `5.3.0`. - Add `\Firebase\JWT` namespace. See From 7e979212c8910e0433682ca3685273f0a64d4832 Mon Sep 17 00:00:00 2001 From: Petr Kotek Date: Thu, 1 Sep 2016 01:16:29 +0200 Subject: [PATCH 102/282] Travis: run tests also under PHP 5.6 and PHP 7.0 (firebase/php-jwt#108) --- Jwt/.travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Jwt/.travis.yml b/Jwt/.travis.yml index b1135a16bb4..96a0f19c51f 100644 --- a/Jwt/.travis.yml +++ b/Jwt/.travis.yml @@ -4,6 +4,8 @@ php: - 5.3 - 5.4 - 5.5 + - 5.6 + - 7.0 - hhvm sudo: false From 3e423624388628094e0c19d650b93eb9a9354937 Mon Sep 17 00:00:00 2001 From: Tim Stamp Date: Mon, 12 Dec 2016 12:14:10 +0000 Subject: [PATCH 103/282] bugfix: 'kid' not in given key list if 'kid' value is not found in the given key map, should throw an exception. Instead, it was outputting a php warning for using an undefined index, resulting in a null key. --- Jwt/src/JWT.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index 6d30e941c98..8170dba2d9c 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -98,6 +98,9 @@ public static function decode($jwt, $key, $allowed_algs = array()) } if (is_array($key) || $key instanceof \ArrayAccess) { if (isset($header->kid)) { + if(!isset($key[$header->kid])) { + throw new UnexpectedValueException('"kid" not found in key map, unable to lookup correct key'); + } $key = $key[$header->kid]; } else { throw new UnexpectedValueException('"kid" empty, unable to lookup correct key'); From ed7f1aa7f1f7a2e178374ce476d283a97bf2fdbb Mon Sep 17 00:00:00 2001 From: Tim Stamp Date: Tue, 24 Jan 2017 10:56:53 +0000 Subject: [PATCH 104/282] cosmetic updates --- Jwt/src/JWT.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index 8170dba2d9c..d45052cfc21 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -98,8 +98,8 @@ public static function decode($jwt, $key, $allowed_algs = array()) } if (is_array($key) || $key instanceof \ArrayAccess) { if (isset($header->kid)) { - if(!isset($key[$header->kid])) { - throw new UnexpectedValueException('"kid" not found in key map, unable to lookup correct key'); + if (!isset($key[$header->kid])) { + throw new UnexpectedValueException('"kid" invalid, unable to lookup correct key'); } $key = $key[$header->kid]; } else { From 5d3eed06b4f46265ac241aa0b2a213281951e636 Mon Sep 17 00:00:00 2001 From: Joost Faassen Date: Mon, 19 Jun 2017 18:29:59 +0200 Subject: [PATCH 105/282] Support RS384 and RS512 (firebase/php-jwt#117) --- Jwt/src/JWT.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index 6d30e941c98..ea3cc7d171d 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -42,6 +42,8 @@ class JWT 'HS512' => array('hash_hmac', 'SHA512'), 'HS384' => array('hash_hmac', 'SHA384'), 'RS256' => array('openssl', 'SHA256'), + 'RS384' => array('openssl', 'SHA384'), + 'RS512' => array('openssl', 'SHA512'), ); /** From 9250bcb663024ca41798344cce79243be10ba83a Mon Sep 17 00:00:00 2001 From: Arjan Keeman Date: Mon, 19 Jun 2017 18:34:20 +0200 Subject: [PATCH 106/282] draft-ietf-oauth-json-web-token-06 --> rfc7519 (firebase/php-jwt#107) The README says this package is conform rfc7519 (May 2015) rather than just the 6th draft (valid until June 2013). The implementation seems indeed to be compatible with the rfc. --- Jwt/src/JWT.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index ea3cc7d171d..607710d1fb4 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -8,7 +8,7 @@ /** * JSON Web Token implementation, based on this spec: - * http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-06 + * https://tools.ietf.org/html/rfc7519 * * PHP version 5 * From 663ad5e5a1321a1fba7a3073a9ed14ddb6ba60a7 Mon Sep 17 00:00:00 2001 From: Henry N Date: Mon, 19 Jun 2017 18:40:24 +0200 Subject: [PATCH 107/282] example for RS256 openssl (firebase/php-jwt#125) --- Jwt/README.md | 54 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/Jwt/README.md b/Jwt/README.md index d4589b1c25a..5c3261a2a64 100644 --- a/Jwt/README.md +++ b/Jwt/README.md @@ -58,6 +58,60 @@ $decoded_array = (array) $decoded; JWT::$leeway = 60; // $leeway in seconds $decoded = JWT::decode($jwt, $key, array('HS256')); +?> +``` +Example with RS256 (openssl) +---------------------------- +```php + "example.org", + "aud" => "example.com", + "iat" => 1356999524, + "nbf" => 1357000000 +); + +$jwt = JWT::encode($token, $privateKey, 'RS256'); +echo "Encode:\n" . print_r($jwt, true) . "\n"; + +$decoded = JWT::decode($jwt, $publicKey, array('RS256')); + +/* + NOTE: This will now be an object instead of an associative array. To get + an associative array, you will need to cast it as such: +*/ + +$decoded_array = (array) $decoded; +echo "Decode:\n" . print_r($decoded_array, true) . "\n"; ?> ``` From c5b954bb7b297f4115a3946f9b58aa6e4f90bf07 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 21 Jun 2017 11:26:30 -0700 Subject: [PATCH 108/282] Fixes travis, adds PHP 7.1, and removes HHVM (firebase/php-jwt#160) --- Jwt/.gitignore | 1 + Jwt/.travis.yml | 9 +++------ Jwt/composer.json | 5 ++++- Jwt/composer.lock | 19 ------------------- 4 files changed, 8 insertions(+), 26 deletions(-) delete mode 100644 Jwt/composer.lock diff --git a/Jwt/.gitignore b/Jwt/.gitignore index 7c29c87b85d..080f19aa729 100644 --- a/Jwt/.gitignore +++ b/Jwt/.gitignore @@ -2,3 +2,4 @@ vendor phpunit.phar phpunit.phar.asc composer.phar +composer.lock diff --git a/Jwt/.travis.yml b/Jwt/.travis.yml index 96a0f19c51f..89131cc1348 100644 --- a/Jwt/.travis.yml +++ b/Jwt/.travis.yml @@ -6,12 +6,9 @@ php: - 5.5 - 5.6 - 7.0 - - hhvm + - 7.1 sudo: false -before_script: - - wget -nc http://getcomposer.org/composer.phar - - php composer.phar install - -script: phpunit --configuration phpunit.xml.dist +before_script: composer install +script: phpunit diff --git a/Jwt/composer.json b/Jwt/composer.json index 1a5e93b53e2..4244f133c15 100644 --- a/Jwt/composer.json +++ b/Jwt/composer.json @@ -23,5 +23,8 @@ "Firebase\\JWT\\": "src" } }, - "minimum-stability": "dev" + "minimum-stability": "dev", + "require-dev": { + "phpunit/phpunit": " 4.8.35" + } } diff --git a/Jwt/composer.lock b/Jwt/composer.lock deleted file mode 100644 index 5518ae41cd5..00000000000 --- a/Jwt/composer.lock +++ /dev/null @@ -1,19 +0,0 @@ -{ - "_readme": [ - "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", - "This file is @generated automatically" - ], - "hash": "60a5df5d283a7ae9000173248eba8909", - "packages": [], - "packages-dev": [], - "aliases": [], - "minimum-stability": "dev", - "stability-flags": [], - "prefer-stable": false, - "prefer-lowest": false, - "platform": { - "php": ">=5.2.0" - }, - "platform-dev": [] -} From c86f3911d7ad7f628decebce3eb7055fb9c5fc45 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 21 Jun 2017 11:38:45 -0700 Subject: [PATCH 109/282] Updates JWT::verify to handle openssl errors (firebase/php-jwt#159) --- Jwt/src/JWT.php | 14 +++++++++----- Jwt/tests/JWTTest.php | 29 +++++++++++++++++++++++++++-- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index 2e5758ea94e..7729143e054 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -88,7 +88,7 @@ public static function decode($jwt, $key, $allowed_algs = array()) throw new UnexpectedValueException('Invalid claims encoding'); } $sig = static::urlsafeB64Decode($cryptob64); - + if (empty($header->alg)) { throw new UnexpectedValueException('Empty algorithm'); } @@ -230,11 +230,15 @@ private static function verify($msg, $signature, $key, $alg) switch($function) { case 'openssl': $success = openssl_verify($msg, $signature, $key, $algorithm); - if (!$success) { - throw new DomainException("OpenSSL unable to verify data: " . openssl_error_string()); - } else { - return $signature; + if ($success === 1) { + return true; + } elseif ($success === 0) { + return false; } + // returns 1 on success, 0 on failure, -1 on error. + throw new DomainException( + 'OpenSSL error: ' . openssl_error_string() + ); case 'hash_hmac': default: $hash = hash_hmac($algorithm, $msg, $key, true); diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index e99ea03a628..99ae9c3863b 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -1,8 +1,13 @@ 'test-eit;v=1')); - $this->assertEquals(JWT::decode($msg, 'my_key', array('HS256')), 'abc'); + $this->assertEquals(JWT::decode($msg, 'my_key', array('HS256')), 'abc'); } public function testInvalidSegmentCount() @@ -261,4 +266,24 @@ public function testInvalidSegmentCount() $this->setExpectedException('UnexpectedValueException'); JWT::decode('brokenheader.brokenbody', 'my_key', array('HS256')); } + + public function testVerifyError() + { + $this->setExpectedException('DomainException'); + $pkey = openssl_pkey_new(); + $msg = JWT::encode('abc', $pkey, 'RS256'); + self::$opensslVerifyReturnValue = -1; + JWT::decode($msg, $pkey, array('RS256')); + } +} + +/* + * Allows the testing of openssl_verify with an error return value + */ +function openssl_verify($msg, $signature, $key, $algorithm) +{ + if (null !== JWTTest::$opensslVerifyReturnValue) { + return JWTTest::$opensslVerifyReturnValue; + } + return \openssl_verify($msg, $signature, $key, $algorithm); } From f2eaa2f2c398e1706a2ee5b2f910438d718a3a0d Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 21 Jun 2017 13:50:08 -0700 Subject: [PATCH 110/282] removes minimum-stabity from composer.json (firebase/php-jwt#161) --- Jwt/composer.json | 1 - 1 file changed, 1 deletion(-) diff --git a/Jwt/composer.json b/Jwt/composer.json index 4244f133c15..b76ffd19106 100644 --- a/Jwt/composer.json +++ b/Jwt/composer.json @@ -23,7 +23,6 @@ "Firebase\\JWT\\": "src" } }, - "minimum-stability": "dev", "require-dev": { "phpunit/phpunit": " 4.8.35" } From 1d79c5a1418212a80bf687b96ea8467859868aa0 Mon Sep 17 00:00:00 2001 From: Giorgio Balduzzi Date: Wed, 21 Jun 2017 22:57:21 +0200 Subject: [PATCH 111/282] Add the proper Exception message for all JSON error types by PHP (firebase/php-jwt#110) --- Jwt/src/JWT.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index 7729143e054..814afc0aafa 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -352,8 +352,10 @@ private static function handleJsonError($errno) { $messages = array( JSON_ERROR_DEPTH => 'Maximum stack depth exceeded', + JSON_ERROR_STATE_MISMATCH => 'Invalid or malformed JSON', JSON_ERROR_CTRL_CHAR => 'Unexpected control character found', - JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON' + JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON', + JSON_ERROR_UTF8 => 'Malformed UTF-8 characters' //PHP >= 5.3.3 ); throw new DomainException( isset($messages[$errno]) From a8252d3f734b75bd11a4a1f6bb5069b648fc4651 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 21 Jun 2017 14:51:26 -0700 Subject: [PATCH 112/282] Detect invalid Base64 encoding in signature (firebase/php-jwt#162) --- Jwt/src/JWT.php | 5 +++-- Jwt/tests/JWTTest.php | 7 +++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index 814afc0aafa..cb1ca7d106f 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -87,8 +87,9 @@ public static function decode($jwt, $key, $allowed_algs = array()) if (null === $payload = static::jsonDecode(static::urlsafeB64Decode($bodyb64))) { throw new UnexpectedValueException('Invalid claims encoding'); } - $sig = static::urlsafeB64Decode($cryptob64); - + if (false === ($sig = static::urlsafeB64Decode($cryptob64))) { + throw new UnexpectedValueException('Invalid signature encoding'); + } if (empty($header->alg)) { throw new UnexpectedValueException('Empty algorithm'); } diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index 99ae9c3863b..804a3769229 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -267,6 +267,13 @@ public function testInvalidSegmentCount() JWT::decode('brokenheader.brokenbody', 'my_key', array('HS256')); } + public function testInvalidSignatureEncoding() + { + $msg = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwibmFtZSI6ImZvbyJ9.Q4Kee9E8o0Xfo4ADXvYA8t7dN_X_bU9K5w6tXuiSjlUxx"; + $this->setExpectedException('UnexpectedValueException'); + JWT::decode($msg, 'secret', array('HS256')); + } + public function testVerifyError() { $this->setExpectedException('DomainException'); From ddf9d641f023287d0f8c331a10243ce0ac4e4455 Mon Sep 17 00:00:00 2001 From: Chinedu Francis Nwafili Date: Thu, 22 Jun 2017 09:51:13 +0700 Subject: [PATCH 113/282] Add new line warning to README (firebase/php-jwt#115) --- Jwt/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Jwt/README.md b/Jwt/README.md index 5c3261a2a64..294234b861b 100644 --- a/Jwt/README.md +++ b/Jwt/README.md @@ -168,6 +168,12 @@ Time: 0 seconds, Memory: 2.50Mb OK (5 tests, 5 assertions) ``` +New Lines in private keys +----- + +If your private key contains `\n` characters, be sure to wrap it in double quotes `""` +and not single quotes `''` in order to properly interpret the escaped characters. + License ------- [3-Clause BSD](http://opensource.org/licenses/BSD-3-Clause). From c0a6aff9aa3295743bb9b4b8ad4feeb8e73018a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Mon, 26 Jun 2017 18:56:41 +0200 Subject: [PATCH 114/282] added `array` type hinting to `decode` method (firebase/php-jwt#101) --- Jwt/src/JWT.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index cb1ca7d106f..22a67e32b61 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -66,16 +66,13 @@ class JWT * @uses jsonDecode * @uses urlsafeB64Decode */ - public static function decode($jwt, $key, $allowed_algs = array()) + public static function decode($jwt, $key, array $allowed_algs = array()) { $timestamp = is_null(static::$timestamp) ? time() : static::$timestamp; if (empty($key)) { throw new InvalidArgumentException('Key may not be empty'); } - if (!is_array($allowed_algs)) { - throw new InvalidArgumentException('Algorithm not allowed'); - } $tks = explode('.', $jwt); if (count($tks) != 3) { throw new UnexpectedValueException('Wrong number of segments'); From 4ae2ae8547db600d1db832fb5ebf1b325217a618 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Mon, 26 Jun 2017 10:28:13 -0700 Subject: [PATCH 115/282] Remove outdated package.xml (firebase/php-jwt#165) --- Jwt/package.xml | 77 ------------------------------------------------- 1 file changed, 77 deletions(-) delete mode 100644 Jwt/package.xml diff --git a/Jwt/package.xml b/Jwt/package.xml deleted file mode 100644 index a95b056f714..00000000000 --- a/Jwt/package.xml +++ /dev/null @@ -1,77 +0,0 @@ - - - JWT - pear.php.net -

A JWT encoder/decoder. - A JWT encoder/decoder library for PHP. - - Neuman Vong - lcfrs - neuman+pear@twilio.com - yes - - - Firebase Operations - firebase - operations@firebase.com - yes - - 2015-07-22 - - 3.0.0 - 3.0.0 - - - beta - beta - - BSD 3-Clause License - -Initial release with basic support for JWT encoding, decoding and signature verification. - - - - - - - - - - - - - 5.1 - - - 1.7.0 - - - json - - - hash - - - - - - - - 0.1.0 - 0.1.0 - - - beta - beta - - 2015-04-01 - BSD 3-Clause License - -Initial release with basic support for JWT encoding, decoding and signature verification. - - - - From da67b59eed5af2d634c61e413c3d7ad320b3bd74 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Tue, 27 Jun 2017 15:17:23 -0700 Subject: [PATCH 116/282] updates README changelog for v5.0.0 (firebase/php-jwt#166) --- Jwt/README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/Jwt/README.md b/Jwt/README.md index 294234b861b..b1a7a3a2063 100644 --- a/Jwt/README.md +++ b/Jwt/README.md @@ -118,6 +118,27 @@ echo "Decode:\n" . print_r($decoded_array, true) . "\n"; Changelog --------- +#### 5.0.0 / 2017-06-26 +- Support RS384 and RS512. + See [#117](https://github.com/firebase/php-jwt/pull/117). Thanks [@joostfaassen](https://github.com/joostfaassen)! +- Add an example for RS256 openssl. + See [#125](https://github.com/firebase/php-jwt/pull/125). Thanks [@akeeman](https://github.com/akeeman)! +- Detect invalid Base64 encoding in signature. + See [#162](https://github.com/firebase/php-jwt/pull/162). Thanks [@psignoret](https://github.com/psignoret)! +- Update `JWT::verify` to handle OpenSSL errors. + See [#159](https://github.com/firebase/php-jwt/pull/159). Thanks [@bshaffer](https://github.com/bshaffer)! +- Add `array` type hinting to `decode` method + See [#101](https://github.com/firebase/php-jwt/pull/101). Thanks [@hywak](https://github.com/hywak)! +- Add all JSON error types. + See [#110](https://github.com/firebase/php-jwt/pull/110). Thanks [@gbalduzzi](https://github.com/gbalduzzi)! +- Bugfix 'kid' not in given key list. + See [#129](https://github.com/firebase/php-jwt/pull/129). Thanks [@stampycode](https://github.com/stampycode)! +- Miscellaneous cleanup, documentation and test fixes. + See [#107](https://github.com/firebase/php-jwt/pull/107), [#115](https://github.com/firebase/php-jwt/pull/115), + [#160](https://github.com/firebase/php-jwt/pull/160), [#161](https://github.com/firebase/php-jwt/pull/161), and + [#165](https://github.com/firebase/php-jwt/pull/165). Thanks [@akeeman](https://github.com/akeeman), + [@chinedufn](https://github.com/chinedufn), and [@bshaffer](https://github.com/bshaffer)! + #### 4.0.0 / 2016-07-17 - Add support for late static binding. See [#88](https://github.com/firebase/php-jwt/pull/88) for details. Thanks to [@chappy84](https://github.com/chappy84)! - Use static `$timestamp` instead of `time()` to improve unit testing. See [#93](https://github.com/firebase/php-jwt/pull/93) for details. Thanks to [@josephmcdermott](https://github.com/josephmcdermott)! From 528460b8fe92ac98ebf71c42d82ae149a5e226ba Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Fri, 26 Jan 2018 11:13:29 -0800 Subject: [PATCH 117/282] Adds PHP 7.2 to Travis (firebase/php-jwt#186) --- Jwt/.travis.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Jwt/.travis.yml b/Jwt/.travis.yml index 89131cc1348..26f0ff0f5e4 100644 --- a/Jwt/.travis.yml +++ b/Jwt/.travis.yml @@ -1,12 +1,17 @@ language: php php: - - 5.3 - 5.4 - 5.5 - 5.6 - 7.0 - 7.1 + - 7.2 + +matrix: + include: + - php: 5.3 + dist: precise sudo: false From c9169e6ca72de1dd1338796865a29bedac60ebe5 Mon Sep 17 00:00:00 2001 From: Nick Williams Date: Mon, 4 Nov 2019 22:39:45 -0600 Subject: [PATCH 118/282] Adds ES256 to supported algorithms (firebase/php-jwt#239) --- Jwt/src/JWT.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index 22a67e32b61..388d671f8c4 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -38,9 +38,10 @@ class JWT public static $timestamp = null; public static $supported_algs = array( + 'ES256' => array('openssl', 'SHA256'), 'HS256' => array('hash_hmac', 'SHA256'), - 'HS512' => array('hash_hmac', 'SHA512'), 'HS384' => array('hash_hmac', 'SHA384'), + 'HS512' => array('hash_hmac', 'SHA512'), 'RS256' => array('openssl', 'SHA256'), 'RS384' => array('openssl', 'SHA384'), 'RS512' => array('openssl', 'SHA512'), @@ -53,7 +54,7 @@ class JWT * @param string|array $key The key, or map of keys. * If the algorithm used is asymmetric, this is the public key * @param array $allowed_algs List of supported verification algorithms - * Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256' + * Supported algorithms are 'ES256', 'HS256', 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512' * * @return object The JWT's payload as a PHP object * @@ -144,7 +145,7 @@ public static function decode($jwt, $key, array $allowed_algs = array()) * @param string $key The secret key. * If the algorithm used is asymmetric, this is the private key * @param string $alg The signing algorithm. - * Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256' + * Supported algorithms are 'ES256', 'HS256', 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512' * @param mixed $keyId * @param array $head An array with header elements to attach * @@ -179,7 +180,7 @@ public static function encode($payload, $key, $alg = 'HS256', $keyId = null, $he * @param string $msg The message to sign * @param string|resource $key The secret key * @param string $alg The signing algorithm. - * Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256' + * Supported algorithms are 'ES256', 'HS256', 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512' * * @return string An encrypted message * From 67b63741de0635302dbe2e8079806105b1d3ce40 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Mon, 4 Nov 2019 23:08:04 -0800 Subject: [PATCH 119/282] Fixes tests, adds PHP 7.3 (firebase/php-jwt#257) --- Jwt/.travis.yml | 9 ++++++--- Jwt/composer.json | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Jwt/.travis.yml b/Jwt/.travis.yml index 26f0ff0f5e4..59d474b780a 100644 --- a/Jwt/.travis.yml +++ b/Jwt/.travis.yml @@ -1,19 +1,22 @@ language: php php: - - 5.4 - - 5.5 - 5.6 - 7.0 - 7.1 - 7.2 + - 7.3 matrix: include: - php: 5.3 dist: precise + - php: 5.4 + dist: trusty + - php: 5.5 + dist: trusty sudo: false before_script: composer install -script: phpunit +script: vendor/bin/phpunit diff --git a/Jwt/composer.json b/Jwt/composer.json index b76ffd19106..9f1a42cbebe 100644 --- a/Jwt/composer.json +++ b/Jwt/composer.json @@ -24,6 +24,6 @@ } }, "require-dev": { - "phpunit/phpunit": " 4.8.35" + "phpunit/phpunit": "^4.8|^5" } } From aa71cfbcb2eeb60563c9d3a31621b0a48d87a584 Mon Sep 17 00:00:00 2001 From: Martin Krisell Date: Tue, 12 Nov 2019 20:16:19 +0100 Subject: [PATCH 120/282] Whitespace style fix (firebase/php-jwt#255) --- Jwt/src/JWT.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index 388d671f8c4..a9e4daf6a11 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -160,7 +160,7 @@ public static function encode($payload, $key, $alg = 'HS256', $keyId = null, $he if ($keyId !== null) { $header['kid'] = $keyId; } - if ( isset($head) && is_array($head) ) { + if (isset($head) && is_array($head)) { $header = array_merge($head, $header); } $segments = array(); From 3ee9655041653ab45946304a18885f635e2d5d69 Mon Sep 17 00:00:00 2001 From: David Mann Date: Tue, 12 Nov 2019 14:16:54 -0500 Subject: [PATCH 121/282] Comment typo fix (firebase/php-jwt#253) --- Jwt/src/JWT.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index a9e4daf6a11..2fb986252fa 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -113,7 +113,7 @@ public static function decode($jwt, $key, array $allowed_algs = array()) throw new SignatureInvalidException('Signature verification failed'); } - // Check if the nbf if it is defined. This is the time that the + // Check the nbf if it is defined. This is the time that the // token can actually be used. If it's not yet that time, abort. if (isset($payload->nbf) && $payload->nbf > ($timestamp + static::$leeway)) { throw new BeforeValidException( From 220e3c8cce42d92bf5c97e6390da4e2b47b73309 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Glatzl?= Date: Tue, 12 Nov 2019 20:18:35 +0100 Subject: [PATCH 122/282] Clearer variable name in README (firebase/php-jwt#229) --- Jwt/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Jwt/README.md b/Jwt/README.md index b1a7a3a2063..9c8b5455b69 100644 --- a/Jwt/README.md +++ b/Jwt/README.md @@ -23,7 +23,7 @@ Example use \Firebase\JWT\JWT; $key = "example_key"; -$token = array( +$payload = array( "iss" => "http://example.org", "aud" => "http://example.com", "iat" => 1356999524, @@ -36,7 +36,7 @@ $token = array( * https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40 * for a list of spec-compliant algorithms. */ -$jwt = JWT::encode($token, $key); +$jwt = JWT::encode($payload, $key); $decoded = JWT::decode($jwt, $key, array('HS256')); print_r($decoded); @@ -93,14 +93,14 @@ ehde/zUxo6UvS7UrBQIDAQAB -----END PUBLIC KEY----- EOD; -$token = array( +$payload = array( "iss" => "example.org", "aud" => "example.com", "iat" => 1356999524, "nbf" => 1357000000 ); -$jwt = JWT::encode($token, $privateKey, 'RS256'); +$jwt = JWT::encode($payload, $privateKey, 'RS256'); echo "Encode:\n" . print_r($jwt, true) . "\n"; $decoded = JWT::decode($jwt, $publicKey, array('RS256')); From 2e56ca42fb3616fcf428e13fd454ff078284bb5e Mon Sep 17 00:00:00 2001 From: Martin Krisell Date: Tue, 17 Dec 2019 21:19:28 +0100 Subject: [PATCH 123/282] Use json_last_error without unnecessary existence check (firebase/php-jwt#263) --- Jwt/src/JWT.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index 2fb986252fa..c3c3f667c3c 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -283,7 +283,7 @@ public static function jsonDecode($input) $obj = json_decode($json_without_bigints); } - if (function_exists('json_last_error') && $errno = json_last_error()) { + if ($errno = json_last_error()) { static::handleJsonError($errno); } elseif ($obj === null && $input !== 'null') { throw new DomainException('Null result with non-null input'); @@ -303,7 +303,7 @@ public static function jsonDecode($input) public static function jsonEncode($input) { $json = json_encode($input); - if (function_exists('json_last_error') && $errno = json_last_error()) { + if ($errno = json_last_error()) { static::handleJsonError($errno); } elseif ($json === 'null' && $input !== null) { throw new DomainException('Null result with non-null input'); From 1e51010a5cb33dfe370d05ce93968f0b133f8476 Mon Sep 17 00:00:00 2001 From: sergiy-petrov Date: Wed, 12 Feb 2020 00:35:53 +0200 Subject: [PATCH 124/282] Add php 7.4 to travis (firebase/php-jwt#266) --- Jwt/.travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/Jwt/.travis.yml b/Jwt/.travis.yml index 59d474b780a..fc651ad3475 100644 --- a/Jwt/.travis.yml +++ b/Jwt/.travis.yml @@ -6,6 +6,7 @@ php: - 7.1 - 7.2 - 7.3 + - 7.4 matrix: include: From 723977a1b0110919f463eeddc8ebca803f0b19c0 Mon Sep 17 00:00:00 2001 From: Sergei Kolesnikov Date: Wed, 19 Feb 2020 21:30:12 +0300 Subject: [PATCH 125/282] docs: add resource type as 2nd param in decode method (firebase/php-jwt#205) --- Jwt/src/JWT.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index c3c3f667c3c..18756f7035a 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -50,11 +50,11 @@ class JWT /** * Decodes a JWT string into a PHP object. * - * @param string $jwt The JWT - * @param string|array $key The key, or map of keys. - * If the algorithm used is asymmetric, this is the public key - * @param array $allowed_algs List of supported verification algorithms - * Supported algorithms are 'ES256', 'HS256', 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512' + * @param string $jwt The JWT + * @param string|array|resource $key The key, or map of keys. + * If the algorithm used is asymmetric, this is the public key + * @param array $allowed_algs List of supported verification algorithms + * Supported algorithms are 'ES256', 'HS256', 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512' * * @return object The JWT's payload as a PHP object * From 74054f7da94f4aa4e88e00866de8108005dbac34 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 19 Feb 2020 11:32:56 -0700 Subject: [PATCH 126/282] tests: update PHPUnit test classes (firebase/php-jwt#193) --- Jwt/tests/JWTTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index 804a3769229..c164a8f02eb 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -2,9 +2,9 @@ namespace Firebase\JWT; use ArrayObject; -use PHPUnit_Framework_TestCase; +use PHPUnit\Framework\TestCase; -class JWTTest extends PHPUnit_Framework_TestCase +class JWTTest extends TestCase { public static $opensslVerifyReturnValue; From 0dbbbddc55f3eaee3f061ae2237e6162155caddf Mon Sep 17 00:00:00 2001 From: Ilia Urvachev Date: Wed, 19 Feb 2020 21:34:19 +0300 Subject: [PATCH 127/282] docs: fix missed param name (firebase/php-jwt#207) --- Jwt/src/JWT.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index 18756f7035a..11f6f108718 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -366,7 +366,7 @@ private static function handleJsonError($errno) /** * Get the number of bytes in cryptographic strings. * - * @param string + * @param string $str * * @return int */ From 660f4731306846a433e9081a037ed384c42ea038 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Mon, 24 Feb 2020 16:15:03 -0700 Subject: [PATCH 128/282] feat: add support for ES256 algorithm (firebase/php-jwt#256) --- Jwt/src/JWT.php | 138 +++++++++++++++++++++++++++++++++++- Jwt/tests/JWTTest.php | 16 +++++ Jwt/tests/ecdsa-private.pem | 18 +++++ Jwt/tests/ecdsa-public.pem | 9 +++ 4 files changed, 179 insertions(+), 2 deletions(-) create mode 100644 Jwt/tests/ecdsa-private.pem create mode 100644 Jwt/tests/ecdsa-public.pem diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index 11f6f108718..af20661894c 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -1,6 +1,7 @@ alg, $allowed_algs)) { throw new UnexpectedValueException('Algorithm not allowed'); } + if ($header->alg === 'ES256') { + // OpenSSL expects an ASN.1 DER sequence for ES256 signatures + $sig = self::signatureToDER($sig); + } + if (is_array($key) || $key instanceof \ArrayAccess) { if (isset($header->kid)) { if (!isset($key[$header->kid])) { @@ -192,7 +201,7 @@ public static function sign($msg, $key, $alg = 'HS256') throw new DomainException('Algorithm not supported'); } list($function, $algorithm) = static::$supported_algs[$alg]; - switch($function) { + switch ($function) { case 'hash_hmac': return hash_hmac($algorithm, $msg, $key, true); case 'openssl': @@ -201,6 +210,9 @@ public static function sign($msg, $key, $alg = 'HS256') if (!$success) { throw new DomainException("OpenSSL unable to sign data"); } else { + if ($alg === 'ES256') { + $signature = self::signatureFromDER($signature, 256); + } return $signature; } } @@ -226,7 +238,7 @@ private static function verify($msg, $signature, $key, $alg) } list($function, $algorithm) = static::$supported_algs[$alg]; - switch($function) { + switch ($function) { case 'openssl': $success = openssl_verify($msg, $signature, $key, $algorithm); if ($success === 1) { @@ -377,4 +389,126 @@ private static function safeStrlen($str) } return strlen($str); } + + /** + * Convert an ECDSA signature to an ASN.1 DER sequence + * + * @param string $sig The ECDSA signature to convert + * @return string The encoded DER object + */ + private static function signatureToDER($sig) + { + // Separate the signature into r-value and s-value + list($r, $s) = str_split($sig, (int) (strlen($sig) / 2)); + + // Trim leading zeros + $r = ltrim($r, "\x00"); + $s = ltrim($s, "\x00"); + + // Convert r-value and s-value from unsigned big-endian integers to + // signed two's complement + if (ord($r[0]) > 0x7f) { + $r = "\x00" . $r; + } + if (ord($s[0]) > 0x7f) { + $s = "\x00" . $s; + } + + return self::encodeDER( + self::ASN1_SEQUENCE, + self::encodeDER(self::ASN1_INTEGER, $r) . + self::encodeDER(self::ASN1_INTEGER, $s) + ); + } + + /** + * Encodes a value into a DER object. + * + * @param int $type DER tag + * @param string $value the value to encode + * @return string the encoded object + */ + private static function encodeDER($type, $value) + { + $tag_header = 0; + if ($type === self::ASN1_SEQUENCE) { + $tag_header |= 0x20; + } + + // Type + $der = chr($tag_header | $type); + + // Length + $der .= chr(strlen($value)); + + return $der . $value; + } + + /** + * Encodes signature from a DER object. + * + * @param string $der binary signature in DER format + * @param int $keySize the nubmer of bits in the key + * @return string the signature + */ + private static function signatureFromDER($der, $keySize) + { + // OpenSSL returns the ECDSA signatures as a binary ASN.1 DER SEQUENCE + list($offset, $_) = self::readDER($der); + list($offset, $r) = self::readDER($der, $offset); + list($offset, $s) = self::readDER($der, $offset); + + // Convert r-value and s-value from signed two's compliment to unsigned + // big-endian integers + $r = ltrim($r, "\x00"); + $s = ltrim($s, "\x00"); + + // Pad out r and s so that they are $keySize bits long + $r = str_pad($r, $keySize / 8, "\x00", STR_PAD_LEFT); + $s = str_pad($s, $keySize / 8, "\x00", STR_PAD_LEFT); + + return $r . $s; + } + + /** + * Reads binary DER-encoded data and decodes into a single object + * + * @param string $der the binary data in DER format + * @param int $offset the offset of the data stream containing the object + * to decode + * @return array [$offset, $data] the new offset and the decoded object + */ + private static function readDER($der, $offset = 0) + { + $pos = $offset; + $size = strlen($der); + $constructed = (ord($der[$pos]) >> 5) & 0x01; + $type = ord($der[$pos++]) & 0x1f; + + // Length + $len = ord($der[$pos++]); + if ($len & 0x80) { + $n = $len & 0x1f; + $len = 0; + while ($n-- && $pos < $size) { + $len = ($len << 8) | ord($der[$pos++]); + } + } + + // Value + if ($type == self::ASN1_BIT_STRING) { + $pos++; // Skip the first contents octet (padding indicator) + $data = substr($der, $pos, $len - 1); + if (!$ignore_bit_strings) { + $pos += $len - 1; + } + } elseif (!$constructed) { + $data = substr($der, $pos, $len); + $pos += $len; + } else { + $data = null; + } + + return array($pos, $data); + } } diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index c164a8f02eb..0e1c20d1007 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -282,6 +282,22 @@ public function testVerifyError() self::$opensslVerifyReturnValue = -1; JWT::decode($msg, $pkey, array('RS256')); } + + /** + * @runInSeparateProcess + */ + public function testEncodeAndDecodeEcdsaToken() + { + $privateKey = file_get_contents(__DIR__ . '/ecdsa-private.pem'); + $payload = array('foo' => 'bar'); + $encoded = JWT::encode($payload, $privateKey, 'ES256'); + + // Verify decoding succeeds + $publicKey = file_get_contents(__DIR__ . '/ecdsa-public.pem'); + $decoded = JWT::decode($encoded, $publicKey, array('ES256')); + + $this->assertEquals('bar', $decoded->foo); + } } /* diff --git a/Jwt/tests/ecdsa-private.pem b/Jwt/tests/ecdsa-private.pem new file mode 100644 index 00000000000..5c77adaf0d1 --- /dev/null +++ b/Jwt/tests/ecdsa-private.pem @@ -0,0 +1,18 @@ +-----BEGIN EC PARAMETERS----- +MIH3AgEBMCwGByqGSM49AQECIQD/////AAAAAQAAAAAAAAAAAAAAAP////////// +/////zBbBCD/////AAAAAQAAAAAAAAAAAAAAAP///////////////AQgWsY12Ko6 +k+ez671VdpiGvGUdBrDMU7D2O848PifSYEsDFQDEnTYIhucEk2pmeOETnSa3gZ9+ +kARBBGsX0fLhLEJH+Lzm5WOkQPJ3A32BLeszoPShOUXYmMKWT+NC4v4af5uO5+tK +fA+eFivOM1drMV7Oy7ZAaDe/UfUCIQD/////AAAAAP//////////vOb6racXnoTz +ucrC/GMlUQIBAQ== +-----END EC PARAMETERS----- +-----BEGIN EC PRIVATE KEY----- +MIIBaAIBAQQgyP9e7yS1tjpXa0l6o+80dbSxuMcqx3lUg0n2OT9AmiuggfowgfcC +AQEwLAYHKoZIzj0BAQIhAP////8AAAABAAAAAAAAAAAAAAAA//////////////// +MFsEIP////8AAAABAAAAAAAAAAAAAAAA///////////////8BCBaxjXYqjqT57Pr +vVV2mIa8ZR0GsMxTsPY7zjw+J9JgSwMVAMSdNgiG5wSTamZ44ROdJreBn36QBEEE +axfR8uEsQkf4vOblY6RA8ncDfYEt6zOg9KE5RdiYwpZP40Li/hp/m47n60p8D54W +K84zV2sxXs7LtkBoN79R9QIhAP////8AAAAA//////////+85vqtpxeehPO5ysL8 +YyVRAgEBoUQDQgAE2klp6aX6y5kAir3EWQt0QAeapTW+db/9fD65KAoDzVajtThx +PVLEf1CufcfTxMQAQPM3wkZhu0NjlWFetcMdcQ== +-----END EC PRIVATE KEY----- diff --git a/Jwt/tests/ecdsa-public.pem b/Jwt/tests/ecdsa-public.pem new file mode 100644 index 00000000000..31fa053d220 --- /dev/null +++ b/Jwt/tests/ecdsa-public.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBSzCCAQMGByqGSM49AgEwgfcCAQEwLAYHKoZIzj0BAQIhAP////8AAAABAAAA +AAAAAAAAAAAA////////////////MFsEIP////8AAAABAAAAAAAAAAAAAAAA//// +///////////8BCBaxjXYqjqT57PrvVV2mIa8ZR0GsMxTsPY7zjw+J9JgSwMVAMSd +NgiG5wSTamZ44ROdJreBn36QBEEEaxfR8uEsQkf4vOblY6RA8ncDfYEt6zOg9KE5 +RdiYwpZP40Li/hp/m47n60p8D54WK84zV2sxXs7LtkBoN79R9QIhAP////8AAAAA +//////////+85vqtpxeehPO5ysL8YyVRAgEBA0IABNpJaeml+suZAIq9xFkLdEAH +mqU1vnW//Xw+uSgKA81Wo7U4cT1SxH9Qrn3H08TEAEDzN8JGYbtDY5VhXrXDHXE= +-----END PUBLIC KEY----- From 9314c7a29120749378a190fb8820cb4fea9c34c8 Mon Sep 17 00:00:00 2001 From: NiMeDia Date: Sat, 29 Feb 2020 01:59:58 +0100 Subject: [PATCH 129/282] fix: added keywords to composer.json (firebase/php-jwt#243) (firebase/php-jwt#271) --- Jwt/composer.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Jwt/composer.json b/Jwt/composer.json index 9f1a42cbebe..e72ecd0713e 100644 --- a/Jwt/composer.json +++ b/Jwt/composer.json @@ -2,6 +2,10 @@ "name": "firebase/php-jwt", "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", "homepage": "https://github.com/firebase/php-jwt", + "keywords": [ + "php", + "jwt" + ], "authors": [ { "name": "Neuman Vong", From 7fd64f49c32980253b7fe6f199e0e1b81e5ff848 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Thu, 5 Mar 2020 13:29:27 -0700 Subject: [PATCH 130/282] remove ignore_bit_strings (firebase/php-jwt#276) --- Jwt/src/JWT.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index af20661894c..f8fe7e65080 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -499,9 +499,7 @@ private static function readDER($der, $offset = 0) if ($type == self::ASN1_BIT_STRING) { $pos++; // Skip the first contents octet (padding indicator) $data = substr($der, $pos, $len - 1); - if (!$ignore_bit_strings) { - $pos += $len - 1; - } + $pos += $len - 1; } elseif (!$constructed) { $data = substr($der, $pos, $len); $pos += $len; From 75790fa1ca6849223a952a1fd9cee56d31703d87 Mon Sep 17 00:00:00 2001 From: Phil Davis Date: Mon, 16 Mar 2020 21:08:03 +0545 Subject: [PATCH 131/282] test: allow for later versions of phpunit (firebase/php-jwt#277) --- Jwt/composer.json | 2 +- Jwt/tests/JWTTest.php | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Jwt/composer.json b/Jwt/composer.json index e72ecd0713e..25d1cfa96ad 100644 --- a/Jwt/composer.json +++ b/Jwt/composer.json @@ -28,6 +28,6 @@ } }, "require-dev": { - "phpunit/phpunit": "^4.8|^5" + "phpunit/phpunit": ">=4.8 <=9" } } diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index 0e1c20d1007..19520165f3e 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -8,6 +8,17 @@ class JWTTest extends TestCase { public static $opensslVerifyReturnValue; + /* + * For compatibility with PHPUnit 4.8 and PHP < 5.6 + */ + public function setExpectedException($exceptionName, $message = '', $code = NULL) { + if (method_exists($this, 'expectException')) { + $this->expectException($exceptionName); + } else { + parent::setExpectedException($exceptionName, $message, $code); + } + } + public function testEncodeDecode() { $msg = JWT::encode('abc', 'my_key'); From df1f3a9617fe91ff879a2538c83596572e23f532 Mon Sep 17 00:00:00 2001 From: Eric Tendian Date: Sat, 21 Mar 2020 14:34:46 -0500 Subject: [PATCH 132/282] feat: adds JWK support (firebase/php-jwt#273) --- Jwt/src/JWK.php | 171 ++++++++++++++++++++++++++++++++++++++++++ Jwt/tests/JWKTest.php | 159 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 330 insertions(+) create mode 100644 Jwt/src/JWK.php create mode 100644 Jwt/tests/JWKTest.php diff --git a/Jwt/src/JWK.php b/Jwt/src/JWK.php new file mode 100644 index 00000000000..f2777df8f26 --- /dev/null +++ b/Jwt/src/JWK.php @@ -0,0 +1,171 @@ + + * @license http://opensource.org/licenses/BSD-3-Clause 3-clause BSD + * @link https://github.com/firebase/php-jwt + */ +class JWK +{ + /** + * Parse a set of JWK keys + * + * @param array $jwks The JSON Web Key Set as an associative array + * + * @return array An associative array that represents the set of keys + * + * @throws InvalidArgumentException Provided JWK Set is empty + * @throws UnexpectedValueException Provided JWK Set was invalid + * @throws DomainException OpenSSL failure + * + * @uses parseKey + */ + public static function parseKeySet(array $jwks) + { + $keys = array(); + + if (!isset($jwks['keys'])) { + throw new UnexpectedValueException('"keys" member must exist in the JWK Set'); + } + if (empty($jwks['keys'])) { + throw new InvalidArgumentException('JWK Set did not contain any keys'); + } + + foreach ($jwks['keys'] as $k => $v) { + $kid = isset($v['kid']) ? $v['kid'] : $k; + if ($key = self::parseKey($v)) { + $keys[$kid] = $key; + } + } + + if (0 === count($keys)) { + throw new UnexpectedValueException('No supported algorithms found in JWK Set'); + } + + return $keys; + } + + /** + * Parse a JWK key + * + * @param array $jwk An individual JWK + * + * @return resource|array An associative array that represents the key + * + * @throws InvalidArgumentException Provided JWK is empty + * @throws UnexpectedValueException Provided JWK was invalid + * @throws DomainException OpenSSL failure + * + * @uses createPemFromModulusAndExponent + */ + private static function parseKey(array $jwk) + { + if (empty($jwk)) { + throw new InvalidArgumentException('JWK must not be empty'); + } + if (!isset($jwk['kty'])) { + throw new UnexpectedValueException('JWK must contain a "kty" parameter'); + } + + switch ($jwk['kty']) { + case 'RSA': + if (array_key_exists('d', $jwk)) { + throw new UnexpectedValueException('RSA private keys are not supported'); + } + if (!isset($jwk['n']) || !isset($jwk['e'])) { + throw new UnexpectedValueException('RSA keys must contain values for both "n" and "e"'); + } + + $pem = self::createPemFromModulusAndExponent($jwk['n'], $jwk['e']); + $publicKey = openssl_pkey_get_public($pem); + if (false === $publicKey) { + throw new DomainException( + 'OpenSSL error: ' . openssl_error_string() + ); + } + return $publicKey; + default: + // Currently only RSA is supported + break; + } + } + + /** + * Create a public key represented in PEM format from RSA modulus and exponent information + * + * @param string $n The RSA modulus encoded in Base64 + * @param string $e The RSA exponent encoded in Base64 + * + * @return string The RSA public key represented in PEM format + * + * @uses encodeLength + */ + private static function createPemFromModulusAndExponent($n, $e) + { + $modulus = JWT::urlsafeB64Decode($n); + $publicExponent = JWT::urlsafeB64Decode($e); + + $components = array( + 'modulus' => pack('Ca*a*', 2, self::encodeLength(strlen($modulus)), $modulus), + 'publicExponent' => pack('Ca*a*', 2, self::encodeLength(strlen($publicExponent)), $publicExponent) + ); + + $rsaPublicKey = pack( + 'Ca*a*a*', + 48, + self::encodeLength(strlen($components['modulus']) + strlen($components['publicExponent'])), + $components['modulus'], + $components['publicExponent'] + ); + + // sequence(oid(1.2.840.113549.1.1.1), null)) = rsaEncryption. + $rsaOID = pack('H*', '300d06092a864886f70d0101010500'); // hex version of MA0GCSqGSIb3DQEBAQUA + $rsaPublicKey = chr(0) . $rsaPublicKey; + $rsaPublicKey = chr(3) . self::encodeLength(strlen($rsaPublicKey)) . $rsaPublicKey; + + $rsaPublicKey = pack( + 'Ca*a*', + 48, + self::encodeLength(strlen($rsaOID . $rsaPublicKey)), + $rsaOID . $rsaPublicKey + ); + + $rsaPublicKey = "-----BEGIN PUBLIC KEY-----\r\n" . + chunk_split(base64_encode($rsaPublicKey), 64) . + '-----END PUBLIC KEY-----'; + + return $rsaPublicKey; + } + + /** + * DER-encode the length + * + * DER supports lengths up to (2**8)**127, however, we'll only support lengths up to (2**8)**4. See + * {@link http://itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#p=13 X.690 paragraph 8.1.3} for more information. + * + * @param int $length + * @return string + */ + private static function encodeLength($length) + { + if ($length <= 0x7F) { + return chr($length); + } + + $temp = ltrim(pack('N', $length), chr(0)); + + return pack('Ca*', 0x80 | strlen($temp), $temp); + } +} diff --git a/Jwt/tests/JWKTest.php b/Jwt/tests/JWKTest.php new file mode 100644 index 00000000000..4f8fdf65eee --- /dev/null +++ b/Jwt/tests/JWKTest.php @@ -0,0 +1,159 @@ +expectException($exceptionName); + } else { + parent::setExpectedException($exceptionName, $message, $code); + } + } + + public function testDecodeByJWKKeySetTokenExpired() + { + $jsKey = array( + 'kty' => 'RSA', + 'e' => 'AQAB', + 'use' => 'sig', + 'kid' => 's1', + 'n' => 'kWp2zRA23Z3vTL4uoe8kTFptxBVFunIoP4t_8TDYJrOb7D1iZNDXVeEsYKp6ppmrTZDAgd-cNOTKLd4M39WJc5FN0maTAVKJc7NxklDeKc4dMe1BGvTZNG4MpWBo-taKULlYUu0ltYJuLzOjIrTHfarucrGoRWqM0sl3z2-fv9k', + ); + + $key = JWK::parseKeySet(array('keys' => array($jsKey))); + + $header = array( + 'kid' => 's1', + 'alg' => 'RS256', + ); + $payload = array ( + 'scp' => array ('openid', 'email', 'profile', 'aas'), + 'sub' => 'tUCYtnfIBPWcrSJf4yBfvN1kww4KGcy3LIPk1GVzsE0', + 'clm' => array ('!5v8H'), + 'iss' => 'http://130.211.243.114:8080/c2id', + 'exp' => 1441126539, + 'uip' => array('groups' => array('admin', 'audit')), + 'cid' => 'pk-oidc-01', + ); + $signature = 'PvYrnf3k1Z0wgRwCgq0WXKaoIv1hHtzBFO5cGfCs6bl4suc6ilwCWmJqRxGYkU2fNTGyMOt3OUnnBEwl6v5qN6jv7zbkVAVKVvbQLxhHC2nXe3izvoCiVaMEH6hE7VTWwnPbX_qO72mCwTizHTJTZGLOsyXLYM6ctdOMf7sFPTI'; + $msg = sprintf('%s.%s.%s', + JWT::urlsafeB64Encode(json_encode($header)), + JWT::urlsafeB64Encode(json_encode($payload)), + $signature + ); + + $this->setExpectedException('Firebase\JWT\ExpiredException'); + + JWT::decode($msg, $key, array('RS256')); + } + + public function testDecodeByJWKKeySet() + { + $jsKey = array( + 'kty' => 'RSA', + 'e' => 'AQAB', + 'use' => 'sig', + 'kid' => 's1', + 'n' => 'kWp2zRA23Z3vTL4uoe8kTFptxBVFunIoP4t_8TDYJrOb7D1iZNDXVeEsYKp6ppmrTZDAgd-cNOTKLd4M39WJc5FN0maTAVKJc7NxklDeKc4dMe1BGvTZNG4MpWBo-taKULlYUu0ltYJuLzOjIrTHfarucrGoRWqM0sl3z2-fv9k', + ); + + $key = JWK::parseKeySet(array('keys' => array($jsKey))); + + $header = array( + 'kid' => 's1', + 'alg' => 'RS256', + ); + $payload = array ( + 'scp' => array ('openid', 'email', 'profile', 'aas'), + 'sub' => 'tUCYtnfIBPWcrSJf4yBfvN1kww4KGcy3LIPk1GVzsE0', + 'clm' => array ('!5v8H'), + 'iss' => 'http://130.211.243.114:8080/c2id', + 'exp' => 1441126539, + 'uip' => array('groups' => array('admin', 'audit')), + 'cid' => 'pk-oidc-01', + ); + $signature = 'PvYrnf3k1Z0wgRwCgq0WXKaoIv1hHtzBFO5cGfCs6bl4suc6ilwCWmJqRxGYkU2fNTGyMOt3OUnnBEwl6v5qN6jv7zbkVAVKVvbQLxhHC2nXe3izvoCiVaMEH6hE7VTWwnPbX_qO72mCwTizHTJTZGLOsyXLYM6ctdOMf7sFPTI'; + $msg = sprintf('%s.%s.%s', + JWT::urlsafeB64Encode(json_encode($header)), + JWT::urlsafeB64Encode(json_encode($payload)), + $signature + ); + + $this->setExpectedException('Firebase\JWT\ExpiredException'); + + $payload = JWT::decode($msg, $key, array('RS256')); + + $this->assertEquals("tUCYtnfIBPWcrSJf4yBfvN1kww4KGcy3LIPk1GVzsE0", $payload->sub); + $this->assertEquals(1441126539, $payload->exp); + } + + public function testDecodeByMultiJWKKeySet() + { + $jsKey1 = array( + 'kty' => 'RSA', + 'e' => 'AQAB', + 'use' => 'sig', + 'kid' => 'CXup', + 'n' => 'hrwD-lc-IwzwidCANmy4qsiZk11yp9kHykOuP0yOnwi36VomYTQVEzZXgh2sDJpGgAutdQudgwLoV8tVSsTG9SQHgJjH9Pd_9V4Ab6PANyZNG6DSeiq1QfiFlEP6Obt0JbRB3W7X2vkxOVaNoWrYskZodxU2V0ogeVL_LkcCGAyNu2jdx3j0DjJatNVk7ystNxb9RfHhJGgpiIkO5S3QiSIVhbBKaJHcZHPF1vq9g0JMGuUCI-OTSVg6XBkTLEGw1C_R73WD_oVEBfdXbXnLukoLHBS11p3OxU7f4rfxA_f_72_UwmWGJnsqS3iahbms3FkvqoL9x_Vj3GhuJSf97Q', + ); + $jsKey2 = array( + 'kty' => 'EC', + 'use' => 'sig', + 'crv' => 'P-256', + 'kid' => 'yGvt', + 'x' => 'pvgdqM3RCshljmuCF1D2Ez1w5ei5k7-bpimWLPNeEHI', + 'y' => 'JSmUhbUTqiFclVLEdw6dz038F7Whw4URobjXbAReDuM', + ); + $jsKey3 = array( + 'kty' => 'EC', + 'use' => 'sig', + 'crv' => 'P-384', + 'kid' => '9nHY', + 'x' => 'JPKhjhE0Bj579Mgj3Cn3ERGA8fKVYoGOaV9BPKhtnEobphf8w4GSeigMesL-038W', + 'y' => 'UbJa1QRX7fo9LxSlh7FOH5ABT5lEtiQeQUcX9BW0bpJFlEVGqwec80tYLdOIl59M', + ); + $jsKey4 = array( + 'kty' => 'EC', + 'use' => 'sig', + 'crv' => 'P-521', + 'kid' => 'tVzS', + 'x' => 'AZgkRHlIyNQJlPIwTWdHqouw41k9dS3GJO04BDEnJnd_Dd1owlCn9SMXA-JuXINn4slwbG4wcECbctXb2cvdGtmn', + 'y' => 'AdBC6N9lpupzfzcIY3JLIuc8y8MnzV-ItmzHQcC5lYWMTbuM9NU_FlvINeVo8g6i4YZms2xFB-B0VVdaoF9kUswC', + ); + + $key = JWK::parseKeySet(array('keys' => array($jsKey1, $jsKey2, $jsKey3, $jsKey4))); + + $header = array( + 'kid' => 'CXup', + 'alg' => 'RS256', + ); + $payload = array( + 'sub' => 'f8b67cc46030777efd8bce6c1bfe29c6c0f818ec', + 'scp' => array('openid', 'name', 'profile', 'picture', 'email', 'rs-pk-main', 'rs-pk-so', 'rs-pk-issue', 'rs-pk-web'), + 'clm' => array('!5v8H'), + 'iss' => 'https://id.projectkit.net/authenticate', + 'exp' => 1492228336, + 'iat' => 1491364336, + 'cid' => 'cid-pk-web', + ); + $signature = 'KW1K-72bMtiNwvyYBgffG6VaG6I59cELGYQR8M2q7HA8dmzliu6QREJrqyPtwW_rDJZbsD3eylvkRinK9tlsMXCOfEJbxLdAC9b4LKOsnsbuXXwsJHWkFG0a7osdW0ZpXJDoMFlO1aosxRGMkaqhf1wIkvQ5PM_EB08LJv7oz64Antn5bYaoajwgvJRl7ChatRDn9Sx5UIElKD1BK4Uw5WdrZwBlWdWZVNCSFhy4F6SdZvi3OBlXzluDwq61RC-pl2iivilJNljYWVrthHDS1xdtaVz4oteHW13-IS7NNEz6PVnzo5nyoPWMAB4JlRnxcfOFTTUqOA2mX5Csg0UpdQ'; + $msg = sprintf('%s.%s.%s', + JWT::urlsafeB64Encode(json_encode($header)), + JWT::urlsafeB64Encode(json_encode($payload)), + $signature + ); + + $this->setExpectedException('Firebase\JWT\ExpiredException'); + + $payload = JWT::decode($msg, $key, array('RS256')); + + $this->assertEquals("f8b67cc46030777efd8bce6c1bfe29c6c0f818ec", $payload->sub); + $this->assertEquals(1492228336, $payload->exp); + } +} From 524201b9fe47ce536397361214bb721464fd6bec Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Sat, 21 Mar 2020 12:59:57 -0700 Subject: [PATCH 133/282] test: use keys for testing JWK (firebase/php-jwt#283) --- Jwt/tests/JWKTest.php | 197 +++++++++++++------------------------ Jwt/tests/rsa-jwkset.json | 17 ++++ Jwt/tests/rsa1-private.pem | 27 +++++ Jwt/tests/rsa2-private.pem | 27 +++++ 4 files changed, 141 insertions(+), 127 deletions(-) create mode 100644 Jwt/tests/rsa-jwkset.json create mode 100644 Jwt/tests/rsa1-private.pem create mode 100644 Jwt/tests/rsa2-private.pem diff --git a/Jwt/tests/JWKTest.php b/Jwt/tests/JWKTest.php index 4f8fdf65eee..d02534f1e7b 100644 --- a/Jwt/tests/JWKTest.php +++ b/Jwt/tests/JWKTest.php @@ -5,155 +5,98 @@ class JWKTest extends TestCase { - /* - * For compatibility with PHPUnit 4.8 and PHP < 5.6 - */ - public function setExpectedException($exceptionName, $message = '', $code = NULL) { - if (method_exists($this, 'expectException')) { - $this->expectException($exceptionName); - } else { - parent::setExpectedException($exceptionName, $message, $code); - } - } + private static $keys; + private static $privKey1; + private static $privKey2; - public function testDecodeByJWKKeySetTokenExpired() + public function testMissingKty() { - $jsKey = array( - 'kty' => 'RSA', - 'e' => 'AQAB', - 'use' => 'sig', - 'kid' => 's1', - 'n' => 'kWp2zRA23Z3vTL4uoe8kTFptxBVFunIoP4t_8TDYJrOb7D1iZNDXVeEsYKp6ppmrTZDAgd-cNOTKLd4M39WJc5FN0maTAVKJc7NxklDeKc4dMe1BGvTZNG4MpWBo-taKULlYUu0ltYJuLzOjIrTHfarucrGoRWqM0sl3z2-fv9k', + $this->setExpectedException( + 'UnexpectedValueException', + 'JWK must contain a "kty" parameter' ); - $key = JWK::parseKeySet(array('keys' => array($jsKey))); + $badJwk = array('kid' => 'foo'); + $keys = JWK::parseKeySet(array('keys' => array($badJwk))); + } - $header = array( - 'kid' => 's1', - 'alg' => 'RS256', - ); - $payload = array ( - 'scp' => array ('openid', 'email', 'profile', 'aas'), - 'sub' => 'tUCYtnfIBPWcrSJf4yBfvN1kww4KGcy3LIPk1GVzsE0', - 'clm' => array ('!5v8H'), - 'iss' => 'http://130.211.243.114:8080/c2id', - 'exp' => 1441126539, - 'uip' => array('groups' => array('admin', 'audit')), - 'cid' => 'pk-oidc-01', - ); - $signature = 'PvYrnf3k1Z0wgRwCgq0WXKaoIv1hHtzBFO5cGfCs6bl4suc6ilwCWmJqRxGYkU2fNTGyMOt3OUnnBEwl6v5qN6jv7zbkVAVKVvbQLxhHC2nXe3izvoCiVaMEH6hE7VTWwnPbX_qO72mCwTizHTJTZGLOsyXLYM6ctdOMf7sFPTI'; - $msg = sprintf('%s.%s.%s', - JWT::urlsafeB64Encode(json_encode($header)), - JWT::urlsafeB64Encode(json_encode($payload)), - $signature + public function testInvalidAlgorithm() + { + $this->setExpectedException( + 'UnexpectedValueException', + 'No supported algorithms found in JWK Set' ); - $this->setExpectedException('Firebase\JWT\ExpiredException'); - - JWT::decode($msg, $key, array('RS256')); + $badJwk = array('kty' => 'BADALG'); + $keys = JWK::parseKeySet(array('keys' => array($badJwk))); } - public function testDecodeByJWKKeySet() + public function testParseJwkKeySet() { - $jsKey = array( - 'kty' => 'RSA', - 'e' => 'AQAB', - 'use' => 'sig', - 'kid' => 's1', - 'n' => 'kWp2zRA23Z3vTL4uoe8kTFptxBVFunIoP4t_8TDYJrOb7D1iZNDXVeEsYKp6ppmrTZDAgd-cNOTKLd4M39WJc5FN0maTAVKJc7NxklDeKc4dMe1BGvTZNG4MpWBo-taKULlYUu0ltYJuLzOjIrTHfarucrGoRWqM0sl3z2-fv9k', + $jwkSet = json_decode( + file_get_contents(__DIR__ . '/rsa-jwkset.json'), + true ); + $keys = JWK::parseKeySet($jwkSet); + $this->assertTrue(is_array($keys)); + $this->assertArrayHasKey('jwk1', $keys); + self::$keys = $keys; + } - $key = JWK::parseKeySet(array('keys' => array($jsKey))); - - $header = array( - 'kid' => 's1', - 'alg' => 'RS256', - ); - $payload = array ( - 'scp' => array ('openid', 'email', 'profile', 'aas'), - 'sub' => 'tUCYtnfIBPWcrSJf4yBfvN1kww4KGcy3LIPk1GVzsE0', - 'clm' => array ('!5v8H'), - 'iss' => 'http://130.211.243.114:8080/c2id', - 'exp' => 1441126539, - 'uip' => array('groups' => array('admin', 'audit')), - 'cid' => 'pk-oidc-01', - ); - $signature = 'PvYrnf3k1Z0wgRwCgq0WXKaoIv1hHtzBFO5cGfCs6bl4suc6ilwCWmJqRxGYkU2fNTGyMOt3OUnnBEwl6v5qN6jv7zbkVAVKVvbQLxhHC2nXe3izvoCiVaMEH6hE7VTWwnPbX_qO72mCwTizHTJTZGLOsyXLYM6ctdOMf7sFPTI'; - $msg = sprintf('%s.%s.%s', - JWT::urlsafeB64Encode(json_encode($header)), - JWT::urlsafeB64Encode(json_encode($payload)), - $signature - ); + /** + * @depends testParseJwkKeySet + */ + public function testDecodeByJwkKeySetTokenExpired() + { + $privKey1 = file_get_contents(__DIR__ . '/rsa1-private.pem'); + $payload = array('exp' => strtotime('-1 hour')); + $msg = JWT::encode($payload, $privKey1, 'RS256', 'jwk1'); $this->setExpectedException('Firebase\JWT\ExpiredException'); - $payload = JWT::decode($msg, $key, array('RS256')); - - $this->assertEquals("tUCYtnfIBPWcrSJf4yBfvN1kww4KGcy3LIPk1GVzsE0", $payload->sub); - $this->assertEquals(1441126539, $payload->exp); + JWT::decode($msg, self::$keys, array('RS256')); } - public function testDecodeByMultiJWKKeySet() + /** + * @depends testParseJwkKeySet + */ + public function testDecodeByJwkKeySet() { - $jsKey1 = array( - 'kty' => 'RSA', - 'e' => 'AQAB', - 'use' => 'sig', - 'kid' => 'CXup', - 'n' => 'hrwD-lc-IwzwidCANmy4qsiZk11yp9kHykOuP0yOnwi36VomYTQVEzZXgh2sDJpGgAutdQudgwLoV8tVSsTG9SQHgJjH9Pd_9V4Ab6PANyZNG6DSeiq1QfiFlEP6Obt0JbRB3W7X2vkxOVaNoWrYskZodxU2V0ogeVL_LkcCGAyNu2jdx3j0DjJatNVk7ystNxb9RfHhJGgpiIkO5S3QiSIVhbBKaJHcZHPF1vq9g0JMGuUCI-OTSVg6XBkTLEGw1C_R73WD_oVEBfdXbXnLukoLHBS11p3OxU7f4rfxA_f_72_UwmWGJnsqS3iahbms3FkvqoL9x_Vj3GhuJSf97Q', - ); - $jsKey2 = array( - 'kty' => 'EC', - 'use' => 'sig', - 'crv' => 'P-256', - 'kid' => 'yGvt', - 'x' => 'pvgdqM3RCshljmuCF1D2Ez1w5ei5k7-bpimWLPNeEHI', - 'y' => 'JSmUhbUTqiFclVLEdw6dz038F7Whw4URobjXbAReDuM', - ); - $jsKey3 = array( - 'kty' => 'EC', - 'use' => 'sig', - 'crv' => 'P-384', - 'kid' => '9nHY', - 'x' => 'JPKhjhE0Bj579Mgj3Cn3ERGA8fKVYoGOaV9BPKhtnEobphf8w4GSeigMesL-038W', - 'y' => 'UbJa1QRX7fo9LxSlh7FOH5ABT5lEtiQeQUcX9BW0bpJFlEVGqwec80tYLdOIl59M', - ); - $jsKey4 = array( - 'kty' => 'EC', - 'use' => 'sig', - 'crv' => 'P-521', - 'kid' => 'tVzS', - 'x' => 'AZgkRHlIyNQJlPIwTWdHqouw41k9dS3GJO04BDEnJnd_Dd1owlCn9SMXA-JuXINn4slwbG4wcECbctXb2cvdGtmn', - 'y' => 'AdBC6N9lpupzfzcIY3JLIuc8y8MnzV-ItmzHQcC5lYWMTbuM9NU_FlvINeVo8g6i4YZms2xFB-B0VVdaoF9kUswC', - ); + $privKey1 = file_get_contents(__DIR__ . '/rsa1-private.pem'); + $payload = array('sub' => 'foo', 'exp' => strtotime('+10 seconds')); + $msg = JWT::encode($payload, $privKey1, 'RS256', 'jwk1'); - $key = JWK::parseKeySet(array('keys' => array($jsKey1, $jsKey2, $jsKey3, $jsKey4))); + $result = JWT::decode($msg, self::$keys, array('RS256')); - $header = array( - 'kid' => 'CXup', - 'alg' => 'RS256', - ); - $payload = array( - 'sub' => 'f8b67cc46030777efd8bce6c1bfe29c6c0f818ec', - 'scp' => array('openid', 'name', 'profile', 'picture', 'email', 'rs-pk-main', 'rs-pk-so', 'rs-pk-issue', 'rs-pk-web'), - 'clm' => array('!5v8H'), - 'iss' => 'https://id.projectkit.net/authenticate', - 'exp' => 1492228336, - 'iat' => 1491364336, - 'cid' => 'cid-pk-web', - ); - $signature = 'KW1K-72bMtiNwvyYBgffG6VaG6I59cELGYQR8M2q7HA8dmzliu6QREJrqyPtwW_rDJZbsD3eylvkRinK9tlsMXCOfEJbxLdAC9b4LKOsnsbuXXwsJHWkFG0a7osdW0ZpXJDoMFlO1aosxRGMkaqhf1wIkvQ5PM_EB08LJv7oz64Antn5bYaoajwgvJRl7ChatRDn9Sx5UIElKD1BK4Uw5WdrZwBlWdWZVNCSFhy4F6SdZvi3OBlXzluDwq61RC-pl2iivilJNljYWVrthHDS1xdtaVz4oteHW13-IS7NNEz6PVnzo5nyoPWMAB4JlRnxcfOFTTUqOA2mX5Csg0UpdQ'; - $msg = sprintf('%s.%s.%s', - JWT::urlsafeB64Encode(json_encode($header)), - JWT::urlsafeB64Encode(json_encode($payload)), - $signature - ); + $this->assertEquals("foo", $result->sub); + } - $this->setExpectedException('Firebase\JWT\ExpiredException'); + /** + * @depends testParseJwkKeySet + */ + public function testDecodeByMultiJwkKeySet() + { + $privKey2 = file_get_contents(__DIR__ . '/rsa2-private.pem'); + $payload = array('sub' => 'bar', 'exp' => strtotime('+10 seconds')); + $msg = JWT::encode($payload, $privKey2, 'RS256', 'jwk2'); - $payload = JWT::decode($msg, $key, array('RS256')); + $result = JWT::decode($msg, self::$keys, array('RS256')); - $this->assertEquals("f8b67cc46030777efd8bce6c1bfe29c6c0f818ec", $payload->sub); - $this->assertEquals(1492228336, $payload->exp); + $this->assertEquals("bar", $result->sub); + } + + /* + * For compatibility with PHPUnit 4.8 and PHP < 5.6 + */ + public function setExpectedException($exceptionName, $message = '', $code = NULL) + { + if (method_exists($this, 'expectException')) { + $this->expectException($exceptionName); + if ($message) { + $this->expectExceptionMessage($message); + } + } else { + parent::setExpectedException($exceptionName, $message, $code); + } } } diff --git a/Jwt/tests/rsa-jwkset.json b/Jwt/tests/rsa-jwkset.json new file mode 100644 index 00000000000..0059f8ccbdb --- /dev/null +++ b/Jwt/tests/rsa-jwkset.json @@ -0,0 +1,17 @@ +{ + "keys": [ + { + "kty": "RSA", + "e": "AQAB", + "kid": "jwk1", + "n": "0Ttga33B1yX4w77NbpKyNYDNSVCo8j-RlZaZ9tI-KfkV1d-tfsvI9ZPAheP11FoN52ceBaY5ltelHW-IKwCfyT0orLdsxLgowaXki9woF1Azvcg2JVxQLv9aVjjAvy3CZFIG_EeN7J3nsyCXGnu1yMEbnvkWxA88__Q6HQ2K9wqfApkQ0LNlsK0YHz_sfjHNvRKxnbAJk7D5fUhZunPZXOPHXFgA5SvLvMaNIXduMKJh4OMfuoLdJowXJAR9j31Mqz_is4FMhm_9Mq7vZZ-uF09htRvIR8tRY28oJuW1gKWyg7cQQpnjHgFyG3XLXWAeXclWqyh_LfjyHQjrYhyeFw" + + }, + { + "kty": "RSA", + "e": "AQAB", + "kid": "jwk2", + "n": "pXi2o6AnNhwL30MaK_nuDHi2fxZHVen7Xwk0bjLGlHYpq3mSvXm2HBA-zR41vQCbHkYGsDpsyDhIXLBDTbSa7ue7D1ZqYdv5YLIS33zdX9GtUHfFHc6zYgXAU9ziWeyTzVn7icAbjxqcgT2xKNuGK7Zf2ZJ053rr-dxjAE-SjX4SG0WWUhwPjxlr1etF7mEurhHweuSdZYl36g39o9BtTBVfS87io2MwdIRsnL3w8ulgXRVRWjv-vvcuhMS_y6zGbzOC55Yr23sb4h2PSll32bgyglEIsGgHqjOdyjuUzl0t6jh86DHzbu9h-u1iihX8EI8t7CBbizbPPyHQygp-rQ" + } + ] +} \ No newline at end of file diff --git a/Jwt/tests/rsa1-private.pem b/Jwt/tests/rsa1-private.pem new file mode 100644 index 00000000000..b194b5b4d80 --- /dev/null +++ b/Jwt/tests/rsa1-private.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA0Ttga33B1yX4w77NbpKyNYDNSVCo8j+RlZaZ9tI+KfkV1d+t +fsvI9ZPAheP11FoN52ceBaY5ltelHW+IKwCfyT0orLdsxLgowaXki9woF1Azvcg2 +JVxQLv9aVjjAvy3CZFIG/EeN7J3nsyCXGnu1yMEbnvkWxA88//Q6HQ2K9wqfApkQ +0LNlsK0YHz/sfjHNvRKxnbAJk7D5fUhZunPZXOPHXFgA5SvLvMaNIXduMKJh4OMf +uoLdJowXJAR9j31Mqz/is4FMhm/9Mq7vZZ+uF09htRvIR8tRY28oJuW1gKWyg7cQ +QpnjHgFyG3XLXWAeXclWqyh/LfjyHQjrYhyeFwIDAQABAoIBAHMqdJsWAGEVNIVB ++792HYNXnydQr32PwemNmLeD59WglgU/9jZJoxaROjI4VLKK0wZg+uRvJ1nA3tCB ++Hh7Anh5Im9XExaAq2ZTkqXtC2AxtBktH6iW1EfaI/Y7jNRuMoaXo+Ku3A62p7cw +JBvepiOXL0Xko0RNguz7mBUvxCLPhYhzn7qCbM8uXLcjsXq/YhWQwQmtMqv0sd3W +Hy+8Jb2c18sqDeZIBne4dWD6qPClPEOsrq9gPTkl0DjbT27oVc2u1p4HMNm5BJIh +u3rMSxnZHUd7Axj1FgyLIOHl63UhaiaA1aPe/fLiVIGOA1jBZrpbnjgqDy9Uxyn6 +eydbiwECgYEA9mtRydz22idyUOlBCDXk+vdGBvFAucNYaNNUAXUJ2wfPmdGgFCA7 +g5eQG8JC6J/FU+2AfIuz6LGr7SxMBYcsWGjFAzGqs/sJib+zzN1dPUSRn4uJNFit +51yQzPgBqHS6S/XBi6YAODeZDl9jiPl3FxxucqLY5NstqZFXbE0SjIECgYEA2V3r +7xnRAK1krY1+zkPof4kcBmjqOXjnl/oRxlXP65lEXmyNJwm/ulOIko9mElWRs8CG +AxSWKaab9Gk6lc8MHjVRbuW52RGLGKq1mp6ENr4d3IBOfrNsTvD3gtNEN1JFLeF1 +jIbSsrbi2txr7VZ06Irac0C/ytro0QDOUoXkvpcCgYA8O0EzmToRWsD7e/g0XJAK +s/Q+8CtE/LWYccc/z+7HxeH9lBqPsM07Pgmwb0xRdfQSrqPQTYl9ICiJAWHXnBG/ +zmQRgstZ0MulCuGU+qq2thLuL3oq/F4NhjeykhA9r8J1nK1hSAMXuqdDtxcqPOfa +E03/4UQotFY181uuEiytgQKBgHQT+gjHqptH/XnJFCymiySAXdz2bg6fCF5aht95 +t/1C7gXWxlJQnHiuX0KVHZcw5wwtBePjPIWlmaceAtE5rmj7ZC9qsqK/AZ78mtql +SEnLoTq9si1rN624dRUCKW25m4Py4MlYvm/9xovGJkSqZOhCLoJZ05JK8QWb/pKH +Oi6lAoGBAOUN6ICpMQvzMGPgIbgS0H/gvRTnpAEs59vdgrkhlCII4tzfgvBQlVae +hRcdM6GTMq5pekBPKu45eanIzwVc88P6coT4qiWYKk2jYoLBa0UV3xEAuqBMymrj +X4nLcSbZtO0tcDGMfMpWF2JGYOEJQNetPozL/ICGVFyIO8yzXm8U +-----END RSA PRIVATE KEY----- diff --git a/Jwt/tests/rsa2-private.pem b/Jwt/tests/rsa2-private.pem new file mode 100644 index 00000000000..74380869873 --- /dev/null +++ b/Jwt/tests/rsa2-private.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEApXi2o6AnNhwL30MaK/nuDHi2fxZHVen7Xwk0bjLGlHYpq3mS +vXm2HBA+zR41vQCbHkYGsDpsyDhIXLBDTbSa7ue7D1ZqYdv5YLIS33zdX9GtUHfF +Hc6zYgXAU9ziWeyTzVn7icAbjxqcgT2xKNuGK7Zf2ZJ053rr+dxjAE+SjX4SG0WW +UhwPjxlr1etF7mEurhHweuSdZYl36g39o9BtTBVfS87io2MwdIRsnL3w8ulgXRVR +Wjv+vvcuhMS/y6zGbzOC55Yr23sb4h2PSll32bgyglEIsGgHqjOdyjuUzl0t6jh8 +6DHzbu9h+u1iihX8EI8t7CBbizbPPyHQygp+rQIDAQABAoIBACF25kj1LLjutx/x +7CsUoqX3C8Fr+gVQCrxPmkDnF+4Sb570OU8EfGX0ix7kiy2sH7LhqpydVD6x00Cb +jSD785F5YAVcDqu31xlNKi/0irjEKO7rKfw7P2AFlb3gIA7bn5CaMBrNtUUdtqUU +mu2OZ/YTLhNMYUQnQe4IOiVn8lWW5D4Kje/RlLRRdGn8voXaD5BnOwZNXAxjdXqM +RxyXRG74tLKyfe3W8xTL8uhlKCNHjsdtUg9IZdnKT7I3DJPobpqgC3fUuC/IbfGf +MPK1aiu067/3DdgonC2ZWqFeKLJqtUa7z0pSQaZeDa1iiUuRivfqKYEBovFre6ni +1qHkp8ECgYEA089VnKc74NRGVbIs0VtQGprNhkl47eBq6jhTlG3hfaFF4VuDiZiu +wT8enlbhlbDb/gM0CDr9tkfDs7R4exNnhSVvn2PT8b1mhonOAeE466y/4YBA0d9x +gj0wF2vjH/bsVNBe6MBrIx12R2tBKTZ7tbCzgJRszSZqkrK7sljTlaUCgYEAx/54 +G3Yd3ULqGIG/JA7w/QEYitgjwAUSJ+eLU+iqlIjo/njAJwJ/kixqaI3Jzcl+kYmp +yNIXNNaJUz8c0M/QsuqvQjLnHkF0FOZUrdyVseU2mSbI6DhAGsPJEtAOep/61vyz +uJSu0z34gQ6bNrKdqfkA7XIQRNJ1r0qQXrVLRmkCgYB2/UYaIDTaREZTBCp7XnHs +0ERfiUz/TZCijgweGXCQ1BXe2TtXBEhAVcZMq4BFSLr9wyzq5sD7Muu1O9BnS+pe ++T3w6/L4Hi/HqwjpM253r2+ILjW78Wvh/5/RuJE6tsvjhb+bv+UwL+/vhUhw76Ol +2WOt+zP4N/ms+e3J7m7G5QKBgQCmasN65nC3WyT8u4pX8O7rOOw5LN2ivRV8ixnO ++r5m1v46MjSCwXtyIO9yjPmt+csOQ+U6LEgPOa4PzWanAyaAmvS3OzBCZui3M2qn +OfR+kWM7UaDAS35cRyqcMvC5bUIHf0P1hhNryBdvHL5fZ4X2mDMDYnTTL+WptXwo +sucucQKBgAGHzi5+ZRwffhpZiYVR/lA6zvqyekAncJZwGe2UVDL0axTumX1NPdin +2mOnVuvKVvJkisyKTIQzFk6ClQEyiArO4+t7zhUbg5Crh8q6nObRo2R2NcP8o0Iq +BRIwPgaG/WlEvZ6zqlHQ0qH7WoL4HnRG5uyLOuzRIkjasYmZdfR8 +-----END RSA PRIVATE KEY----- From f56529b083a09227ac106d92f246aa222283ded7 Mon Sep 17 00:00:00 2001 From: Phil Davis Date: Tue, 24 Mar 2020 02:02:54 +0545 Subject: [PATCH 134/282] chore: code cleanup (firebase/php-jwt#278) --- Jwt/src/JWT.php | 2 +- Jwt/tests/JWTTest.php | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index f8fe7e65080..6f178b4e2a8 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -448,7 +448,7 @@ private static function encodeDER($type, $value) * Encodes signature from a DER object. * * @param string $der binary signature in DER format - * @param int $keySize the nubmer of bits in the key + * @param int $keySize the number of bits in the key * @return string the signature */ private static function signatureFromDER($der, $keySize) diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index 19520165f3e..867d87e573c 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -159,7 +159,7 @@ public function testInvalidTokenWithNbfLeeway() "nbf" => time() + 65); // not before too far in future $encoded = JWT::encode($payload, 'my_key'); $this->setExpectedException('Firebase\JWT\BeforeValidException'); - $decoded = JWT::decode($encoded, 'my_key', array('HS256')); + JWT::decode($encoded, 'my_key', array('HS256')); JWT::$leeway = 0; } @@ -183,7 +183,7 @@ public function testInvalidTokenWithIatLeeway() "iat" => time() + 65); // issued too far in future $encoded = JWT::encode($payload, 'my_key'); $this->setExpectedException('Firebase\JWT\BeforeValidException'); - $decoded = JWT::decode($encoded, 'my_key', array('HS256')); + JWT::decode($encoded, 'my_key', array('HS256')); JWT::$leeway = 0; } @@ -194,7 +194,7 @@ public function testInvalidToken() "exp" => time() + 20); // time in the future $encoded = JWT::encode($payload, 'my_key'); $this->setExpectedException('Firebase\JWT\SignatureInvalidException'); - $decoded = JWT::decode($encoded, 'my_key2', array('HS256')); + JWT::decode($encoded, 'my_key2', array('HS256')); } public function testNullKeyFails() @@ -204,7 +204,7 @@ public function testNullKeyFails() "exp" => time() + JWT::$leeway + 20); // time in the future $encoded = JWT::encode($payload, 'my_key'); $this->setExpectedException('InvalidArgumentException'); - $decoded = JWT::decode($encoded, null, array('HS256')); + JWT::decode($encoded, null, array('HS256')); } public function testEmptyKeyFails() @@ -214,7 +214,7 @@ public function testEmptyKeyFails() "exp" => time() + JWT::$leeway + 20); // time in the future $encoded = JWT::encode($payload, 'my_key'); $this->setExpectedException('InvalidArgumentException'); - $decoded = JWT::decode($encoded, '', array('HS256')); + JWT::decode($encoded, '', array('HS256')); } public function testRSEncodeDecode() From 4179ce0e6da13e6a942ad648175760245efc5244 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 25 Mar 2020 11:49:23 -0700 Subject: [PATCH 135/282] chore: fixes cs and adds cs checking to travis (firebase/php-jwt#284) --- Jwt/.gitattributes | 1 - Jwt/.travis.yml | 15 +++- Jwt/run-tests.sh | 37 --------- Jwt/src/BeforeValidException.php | 1 - Jwt/src/ExpiredException.php | 1 - Jwt/src/JWK.php | 34 ++++----- Jwt/src/JWT.php | 106 +++++++++++++------------- Jwt/src/SignatureInvalidException.php | 1 - Jwt/tests/JWKTest.php | 2 +- Jwt/tests/JWTTest.php | 25 +----- 10 files changed, 87 insertions(+), 136 deletions(-) delete mode 100755 Jwt/run-tests.sh diff --git a/Jwt/.gitattributes b/Jwt/.gitattributes index c682f8617b5..a791521e36e 100644 --- a/Jwt/.gitattributes +++ b/Jwt/.gitattributes @@ -5,4 +5,3 @@ /.gitignore export-ignore /.travis.yml export-ignore /phpunit.xml.dist export-ignore -/run-tests.sh export-ignore diff --git a/Jwt/.travis.yml b/Jwt/.travis.yml index fc651ad3475..90e516cd060 100644 --- a/Jwt/.travis.yml +++ b/Jwt/.travis.yml @@ -1,5 +1,8 @@ language: php +branches: + - only: [master] + php: - 5.6 - 7.0 @@ -16,8 +19,18 @@ matrix: dist: trusty - php: 5.5 dist: trusty + - name: "Check Style" + php: "7.4" + env: RUN_CS_FIXER=true sudo: false before_script: composer install -script: vendor/bin/phpunit +script: + - if [ "${RUN_CS_FIXER}" = "true" ]; then + composer require friendsofphp/php-cs-fixer && + vendor/bin/php-cs-fixer fix --diff --dry-run . && + vendor/bin/php-cs-fixer fix --rules=native_function_invocation --allow-risky=yes --diff src; + else + vendor/bin/phpunit; + fi diff --git a/Jwt/run-tests.sh b/Jwt/run-tests.sh deleted file mode 100755 index c4bb9348415..00000000000 --- a/Jwt/run-tests.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env bash -gpg --fingerprint D8406D0D82947747293778314AA394086372C20A -if [ $? -ne 0 ]; then - echo -e "\033[33mDownloading PGP Public Key...\033[0m" - gpg --recv-keys D8406D0D82947747293778314AA394086372C20A - # Sebastian Bergmann - gpg --fingerprint D8406D0D82947747293778314AA394086372C20A - if [ $? -ne 0 ]; then - echo -e "\033[31mCould not download PGP public key for verification\033[0m" - exit - fi -fi - -# Let's grab the latest release and its signature -if [ ! -f phpunit.phar ]; then - wget https://phar.phpunit.de/phpunit.phar -fi -if [ ! -f phpunit.phar.asc ]; then - wget https://phar.phpunit.de/phpunit.phar.asc -fi - -# Verify before running -gpg --verify phpunit.phar.asc phpunit.phar -if [ $? -eq 0 ]; then - echo - echo -e "\033[33mBegin Unit Testing\033[0m" - # Run the testing suite - php --version - php phpunit.phar --configuration phpunit.xml.dist -else - echo - chmod -x phpunit.phar - mv phpunit.phar /tmp/bad-phpunit.phar - mv phpunit.phar.asc /tmp/bad-phpunit.phar.asc - echo -e "\033[31mSignature did not match! PHPUnit has been moved to /tmp/bad-phpunit.phar\033[0m" - exit 1 -fi diff --git a/Jwt/src/BeforeValidException.php b/Jwt/src/BeforeValidException.php index a6ee2f7c690..fdf82bd9429 100644 --- a/Jwt/src/BeforeValidException.php +++ b/Jwt/src/BeforeValidException.php @@ -3,5 +3,4 @@ class BeforeValidException extends \UnexpectedValueException { - } diff --git a/Jwt/src/ExpiredException.php b/Jwt/src/ExpiredException.php index 3597370a6de..7f7d0568248 100644 --- a/Jwt/src/ExpiredException.php +++ b/Jwt/src/ExpiredException.php @@ -3,5 +3,4 @@ class ExpiredException extends \UnexpectedValueException { - } diff --git a/Jwt/src/JWK.php b/Jwt/src/JWK.php index f2777df8f26..1d273917403 100644 --- a/Jwt/src/JWK.php +++ b/Jwt/src/JWK.php @@ -50,7 +50,7 @@ public static function parseKeySet(array $jwks) } } - if (0 === count($keys)) { + if (0 === \count($keys)) { throw new UnexpectedValueException('No supported algorithms found in JWK Set'); } @@ -81,7 +81,7 @@ private static function parseKey(array $jwk) switch ($jwk['kty']) { case 'RSA': - if (array_key_exists('d', $jwk)) { + if (\array_key_exists('d', $jwk)) { throw new UnexpectedValueException('RSA private keys are not supported'); } if (!isset($jwk['n']) || !isset($jwk['e'])) { @@ -89,10 +89,10 @@ private static function parseKey(array $jwk) } $pem = self::createPemFromModulusAndExponent($jwk['n'], $jwk['e']); - $publicKey = openssl_pkey_get_public($pem); + $publicKey = \openssl_pkey_get_public($pem); if (false === $publicKey) { throw new DomainException( - 'OpenSSL error: ' . openssl_error_string() + 'OpenSSL error: ' . \openssl_error_string() ); } return $publicKey; @@ -118,32 +118,32 @@ private static function createPemFromModulusAndExponent($n, $e) $publicExponent = JWT::urlsafeB64Decode($e); $components = array( - 'modulus' => pack('Ca*a*', 2, self::encodeLength(strlen($modulus)), $modulus), - 'publicExponent' => pack('Ca*a*', 2, self::encodeLength(strlen($publicExponent)), $publicExponent) + 'modulus' => \pack('Ca*a*', 2, self::encodeLength(\strlen($modulus)), $modulus), + 'publicExponent' => \pack('Ca*a*', 2, self::encodeLength(\strlen($publicExponent)), $publicExponent) ); - $rsaPublicKey = pack( + $rsaPublicKey = \pack( 'Ca*a*a*', 48, - self::encodeLength(strlen($components['modulus']) + strlen($components['publicExponent'])), + self::encodeLength(\strlen($components['modulus']) + \strlen($components['publicExponent'])), $components['modulus'], $components['publicExponent'] ); // sequence(oid(1.2.840.113549.1.1.1), null)) = rsaEncryption. - $rsaOID = pack('H*', '300d06092a864886f70d0101010500'); // hex version of MA0GCSqGSIb3DQEBAQUA - $rsaPublicKey = chr(0) . $rsaPublicKey; - $rsaPublicKey = chr(3) . self::encodeLength(strlen($rsaPublicKey)) . $rsaPublicKey; + $rsaOID = \pack('H*', '300d06092a864886f70d0101010500'); // hex version of MA0GCSqGSIb3DQEBAQUA + $rsaPublicKey = \chr(0) . $rsaPublicKey; + $rsaPublicKey = \chr(3) . self::encodeLength(\strlen($rsaPublicKey)) . $rsaPublicKey; - $rsaPublicKey = pack( + $rsaPublicKey = \pack( 'Ca*a*', 48, - self::encodeLength(strlen($rsaOID . $rsaPublicKey)), + self::encodeLength(\strlen($rsaOID . $rsaPublicKey)), $rsaOID . $rsaPublicKey ); $rsaPublicKey = "-----BEGIN PUBLIC KEY-----\r\n" . - chunk_split(base64_encode($rsaPublicKey), 64) . + \chunk_split(\base64_encode($rsaPublicKey), 64) . '-----END PUBLIC KEY-----'; return $rsaPublicKey; @@ -161,11 +161,11 @@ private static function createPemFromModulusAndExponent($n, $e) private static function encodeLength($length) { if ($length <= 0x7F) { - return chr($length); + return \chr($length); } - $temp = ltrim(pack('N', $length), chr(0)); + $temp = \ltrim(\pack('N', $length), \chr(0)); - return pack('Ca*', 0x80 | strlen($temp), $temp); + return \pack('Ca*', 0x80 | \strlen($temp), $temp); } } diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index 6f178b4e2a8..4860028bc62 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -73,13 +73,13 @@ class JWT */ public static function decode($jwt, $key, array $allowed_algs = array()) { - $timestamp = is_null(static::$timestamp) ? time() : static::$timestamp; + $timestamp = \is_null(static::$timestamp) ? \time() : static::$timestamp; if (empty($key)) { throw new InvalidArgumentException('Key may not be empty'); } - $tks = explode('.', $jwt); - if (count($tks) != 3) { + $tks = \explode('.', $jwt); + if (\count($tks) != 3) { throw new UnexpectedValueException('Wrong number of segments'); } list($headb64, $bodyb64, $cryptob64) = $tks; @@ -98,7 +98,7 @@ public static function decode($jwt, $key, array $allowed_algs = array()) if (empty(static::$supported_algs[$header->alg])) { throw new UnexpectedValueException('Algorithm not supported'); } - if (!in_array($header->alg, $allowed_algs)) { + if (!\in_array($header->alg, $allowed_algs)) { throw new UnexpectedValueException('Algorithm not allowed'); } if ($header->alg === 'ES256') { @@ -106,7 +106,7 @@ public static function decode($jwt, $key, array $allowed_algs = array()) $sig = self::signatureToDER($sig); } - if (is_array($key) || $key instanceof \ArrayAccess) { + if (\is_array($key) || $key instanceof \ArrayAccess) { if (isset($header->kid)) { if (!isset($key[$header->kid])) { throw new UnexpectedValueException('"kid" invalid, unable to lookup correct key'); @@ -126,7 +126,7 @@ public static function decode($jwt, $key, array $allowed_algs = array()) // token can actually be used. If it's not yet that time, abort. if (isset($payload->nbf) && $payload->nbf > ($timestamp + static::$leeway)) { throw new BeforeValidException( - 'Cannot handle token prior to ' . date(DateTime::ISO8601, $payload->nbf) + 'Cannot handle token prior to ' . \date(DateTime::ISO8601, $payload->nbf) ); } @@ -135,7 +135,7 @@ public static function decode($jwt, $key, array $allowed_algs = array()) // correctly used the nbf claim). if (isset($payload->iat) && $payload->iat > ($timestamp + static::$leeway)) { throw new BeforeValidException( - 'Cannot handle token prior to ' . date(DateTime::ISO8601, $payload->iat) + 'Cannot handle token prior to ' . \date(DateTime::ISO8601, $payload->iat) ); } @@ -169,18 +169,18 @@ public static function encode($payload, $key, $alg = 'HS256', $keyId = null, $he if ($keyId !== null) { $header['kid'] = $keyId; } - if (isset($head) && is_array($head)) { - $header = array_merge($head, $header); + if (isset($head) && \is_array($head)) { + $header = \array_merge($head, $header); } $segments = array(); $segments[] = static::urlsafeB64Encode(static::jsonEncode($header)); $segments[] = static::urlsafeB64Encode(static::jsonEncode($payload)); - $signing_input = implode('.', $segments); + $signing_input = \implode('.', $segments); $signature = static::sign($signing_input, $key, $alg); $segments[] = static::urlsafeB64Encode($signature); - return implode('.', $segments); + return \implode('.', $segments); } /** @@ -203,10 +203,10 @@ public static function sign($msg, $key, $alg = 'HS256') list($function, $algorithm) = static::$supported_algs[$alg]; switch ($function) { case 'hash_hmac': - return hash_hmac($algorithm, $msg, $key, true); + return \hash_hmac($algorithm, $msg, $key, true); case 'openssl': $signature = ''; - $success = openssl_sign($msg, $signature, $key, $algorithm); + $success = \openssl_sign($msg, $signature, $key, $algorithm); if (!$success) { throw new DomainException("OpenSSL unable to sign data"); } else { @@ -240,7 +240,7 @@ private static function verify($msg, $signature, $key, $alg) list($function, $algorithm) = static::$supported_algs[$alg]; switch ($function) { case 'openssl': - $success = openssl_verify($msg, $signature, $key, $algorithm); + $success = \openssl_verify($msg, $signature, $key, $algorithm); if ($success === 1) { return true; } elseif ($success === 0) { @@ -248,19 +248,19 @@ private static function verify($msg, $signature, $key, $alg) } // returns 1 on success, 0 on failure, -1 on error. throw new DomainException( - 'OpenSSL error: ' . openssl_error_string() + 'OpenSSL error: ' . \openssl_error_string() ); case 'hash_hmac': default: - $hash = hash_hmac($algorithm, $msg, $key, true); - if (function_exists('hash_equals')) { - return hash_equals($signature, $hash); + $hash = \hash_hmac($algorithm, $msg, $key, true); + if (\function_exists('hash_equals')) { + return \hash_equals($signature, $hash); } - $len = min(static::safeStrlen($signature), static::safeStrlen($hash)); + $len = \min(static::safeStrlen($signature), static::safeStrlen($hash)); $status = 0; for ($i = 0; $i < $len; $i++) { - $status |= (ord($signature[$i]) ^ ord($hash[$i])); + $status |= (\ord($signature[$i]) ^ \ord($hash[$i])); } $status |= (static::safeStrlen($signature) ^ static::safeStrlen($hash)); @@ -279,23 +279,23 @@ private static function verify($msg, $signature, $key, $alg) */ public static function jsonDecode($input) { - if (version_compare(PHP_VERSION, '5.4.0', '>=') && !(defined('JSON_C_VERSION') && PHP_INT_SIZE > 4)) { + if (\version_compare(PHP_VERSION, '5.4.0', '>=') && !(\defined('JSON_C_VERSION') && PHP_INT_SIZE > 4)) { /** In PHP >=5.4.0, json_decode() accepts an options parameter, that allows you * to specify that large ints (like Steam Transaction IDs) should be treated as * strings, rather than the PHP default behaviour of converting them to floats. */ - $obj = json_decode($input, false, 512, JSON_BIGINT_AS_STRING); + $obj = \json_decode($input, false, 512, JSON_BIGINT_AS_STRING); } else { /** Not all servers will support that, however, so for older versions we must * manually detect large ints in the JSON string and quote them (thus converting *them to strings) before decoding, hence the preg_replace() call. */ - $max_int_length = strlen((string) PHP_INT_MAX) - 1; - $json_without_bigints = preg_replace('/:\s*(-?\d{'.$max_int_length.',})/', ': "$1"', $input); - $obj = json_decode($json_without_bigints); + $max_int_length = \strlen((string) PHP_INT_MAX) - 1; + $json_without_bigints = \preg_replace('/:\s*(-?\d{'.$max_int_length.',})/', ': "$1"', $input); + $obj = \json_decode($json_without_bigints); } - if ($errno = json_last_error()) { + if ($errno = \json_last_error()) { static::handleJsonError($errno); } elseif ($obj === null && $input !== 'null') { throw new DomainException('Null result with non-null input'); @@ -314,8 +314,8 @@ public static function jsonDecode($input) */ public static function jsonEncode($input) { - $json = json_encode($input); - if ($errno = json_last_error()) { + $json = \json_encode($input); + if ($errno = \json_last_error()) { static::handleJsonError($errno); } elseif ($json === 'null' && $input !== null) { throw new DomainException('Null result with non-null input'); @@ -332,12 +332,12 @@ public static function jsonEncode($input) */ public static function urlsafeB64Decode($input) { - $remainder = strlen($input) % 4; + $remainder = \strlen($input) % 4; if ($remainder) { $padlen = 4 - $remainder; - $input .= str_repeat('=', $padlen); + $input .= \str_repeat('=', $padlen); } - return base64_decode(strtr($input, '-_', '+/')); + return \base64_decode(\strtr($input, '-_', '+/')); } /** @@ -349,7 +349,7 @@ public static function urlsafeB64Decode($input) */ public static function urlsafeB64Encode($input) { - return str_replace('=', '', strtr(base64_encode($input), '+/', '-_')); + return \str_replace('=', '', \strtr(\base64_encode($input), '+/', '-_')); } /** @@ -384,10 +384,10 @@ private static function handleJsonError($errno) */ private static function safeStrlen($str) { - if (function_exists('mb_strlen')) { - return mb_strlen($str, '8bit'); + if (\function_exists('mb_strlen')) { + return \mb_strlen($str, '8bit'); } - return strlen($str); + return \strlen($str); } /** @@ -399,18 +399,18 @@ private static function safeStrlen($str) private static function signatureToDER($sig) { // Separate the signature into r-value and s-value - list($r, $s) = str_split($sig, (int) (strlen($sig) / 2)); + list($r, $s) = \str_split($sig, (int) (\strlen($sig) / 2)); // Trim leading zeros - $r = ltrim($r, "\x00"); - $s = ltrim($s, "\x00"); + $r = \ltrim($r, "\x00"); + $s = \ltrim($s, "\x00"); // Convert r-value and s-value from unsigned big-endian integers to // signed two's complement - if (ord($r[0]) > 0x7f) { + if (\ord($r[0]) > 0x7f) { $r = "\x00" . $r; } - if (ord($s[0]) > 0x7f) { + if (\ord($s[0]) > 0x7f) { $s = "\x00" . $s; } @@ -436,10 +436,10 @@ private static function encodeDER($type, $value) } // Type - $der = chr($tag_header | $type); + $der = \chr($tag_header | $type); // Length - $der .= chr(strlen($value)); + $der .= \chr(\strlen($value)); return $der . $value; } @@ -460,12 +460,12 @@ private static function signatureFromDER($der, $keySize) // Convert r-value and s-value from signed two's compliment to unsigned // big-endian integers - $r = ltrim($r, "\x00"); - $s = ltrim($s, "\x00"); + $r = \ltrim($r, "\x00"); + $s = \ltrim($s, "\x00"); // Pad out r and s so that they are $keySize bits long - $r = str_pad($r, $keySize / 8, "\x00", STR_PAD_LEFT); - $s = str_pad($s, $keySize / 8, "\x00", STR_PAD_LEFT); + $r = \str_pad($r, $keySize / 8, "\x00", STR_PAD_LEFT); + $s = \str_pad($s, $keySize / 8, "\x00", STR_PAD_LEFT); return $r . $s; } @@ -481,27 +481,27 @@ private static function signatureFromDER($der, $keySize) private static function readDER($der, $offset = 0) { $pos = $offset; - $size = strlen($der); - $constructed = (ord($der[$pos]) >> 5) & 0x01; - $type = ord($der[$pos++]) & 0x1f; + $size = \strlen($der); + $constructed = (\ord($der[$pos]) >> 5) & 0x01; + $type = \ord($der[$pos++]) & 0x1f; // Length - $len = ord($der[$pos++]); + $len = \ord($der[$pos++]); if ($len & 0x80) { $n = $len & 0x1f; $len = 0; while ($n-- && $pos < $size) { - $len = ($len << 8) | ord($der[$pos++]); + $len = ($len << 8) | \ord($der[$pos++]); } } // Value if ($type == self::ASN1_BIT_STRING) { $pos++; // Skip the first contents octet (padding indicator) - $data = substr($der, $pos, $len - 1); + $data = \substr($der, $pos, $len - 1); $pos += $len - 1; } elseif (!$constructed) { - $data = substr($der, $pos, $len); + $data = \substr($der, $pos, $len); $pos += $len; } else { $data = null; diff --git a/Jwt/src/SignatureInvalidException.php b/Jwt/src/SignatureInvalidException.php index 27332b21bea..87cb34df79b 100644 --- a/Jwt/src/SignatureInvalidException.php +++ b/Jwt/src/SignatureInvalidException.php @@ -3,5 +3,4 @@ class SignatureInvalidException extends \UnexpectedValueException { - } diff --git a/Jwt/tests/JWKTest.php b/Jwt/tests/JWKTest.php index d02534f1e7b..3d317d5557b 100644 --- a/Jwt/tests/JWKTest.php +++ b/Jwt/tests/JWKTest.php @@ -88,7 +88,7 @@ public function testDecodeByMultiJwkKeySet() /* * For compatibility with PHPUnit 4.8 and PHP < 5.6 */ - public function setExpectedException($exceptionName, $message = '', $code = NULL) + public function setExpectedException($exceptionName, $message = '', $code = null) { if (method_exists($this, 'expectException')) { $this->expectException($exceptionName); diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index 867d87e573c..fc9c37568a7 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -6,12 +6,11 @@ class JWTTest extends TestCase { - public static $opensslVerifyReturnValue; - /* * For compatibility with PHPUnit 4.8 and PHP < 5.6 */ - public function setExpectedException($exceptionName, $message = '', $code = NULL) { + public function setExpectedException($exceptionName, $message = '', $code = null) + { if (method_exists($this, 'expectException')) { $this->expectException($exceptionName); } else { @@ -285,15 +284,6 @@ public function testInvalidSignatureEncoding() JWT::decode($msg, 'secret', array('HS256')); } - public function testVerifyError() - { - $this->setExpectedException('DomainException'); - $pkey = openssl_pkey_new(); - $msg = JWT::encode('abc', $pkey, 'RS256'); - self::$opensslVerifyReturnValue = -1; - JWT::decode($msg, $pkey, array('RS256')); - } - /** * @runInSeparateProcess */ @@ -310,14 +300,3 @@ public function testEncodeAndDecodeEcdsaToken() $this->assertEquals('bar', $decoded->foo); } } - -/* - * Allows the testing of openssl_verify with an error return value - */ -function openssl_verify($msg, $signature, $key, $algorithm) -{ - if (null !== JWTTest::$opensslVerifyReturnValue) { - return JWTTest::$opensslVerifyReturnValue; - } - return \openssl_verify($msg, $signature, $key, $algorithm); -} From b4fd2d2fb3f08b9541f36b628992fc182982f8b7 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Mon, 26 Oct 2020 10:34:38 -0700 Subject: [PATCH 136/282] docs: add JWK usage to README (firebase/php-jwt#307) --- Jwt/README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Jwt/README.md b/Jwt/README.md index 9c8b5455b69..ba139079f1f 100644 --- a/Jwt/README.md +++ b/Jwt/README.md @@ -115,6 +115,19 @@ echo "Decode:\n" . print_r($decoded_array, true) . "\n"; ?> ``` +Using JWKs +---------- + +```php +// Set of keys. The "keys" key is required. For example, the JSON response to +// this endpoint: https://www.gstatic.com/iap/verify/public_key-jwk +$jwks = ['keys' => []]; + +// JWK::parseKeySet($jwks) returns an associative array of **kid** to private +// key. Pass this as the second parameter to JWT::decode. +JWT::decode($payload, JWK::parseKeySet($jwks), $supportedAlgorithm); +``` + Changelog --------- From e7c0043148679edaa3e46487ba9fb5ebce2e45b1 Mon Sep 17 00:00:00 2001 From: Grant Anderson Date: Thu, 11 Feb 2021 17:02:00 -0700 Subject: [PATCH 137/282] fix: add missing use statement in JWK (firebase/php-jwt#303) --- Jwt/src/JWK.php | 1 + Jwt/tests/JWKTest.php | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/Jwt/src/JWK.php b/Jwt/src/JWK.php index 1d273917403..7632f4a4080 100644 --- a/Jwt/src/JWK.php +++ b/Jwt/src/JWK.php @@ -3,6 +3,7 @@ namespace Firebase\JWT; use DomainException; +use InvalidArgumentException; use UnexpectedValueException; /** diff --git a/Jwt/tests/JWKTest.php b/Jwt/tests/JWKTest.php index 3d317d5557b..b8b675408fe 100644 --- a/Jwt/tests/JWKTest.php +++ b/Jwt/tests/JWKTest.php @@ -43,6 +43,20 @@ public function testParseJwkKeySet() self::$keys = $keys; } + public function testParseJwkKey_empty() + { + $this->setExpectedException('InvalidArgumentException', 'JWK must not be empty'); + + JWK::parseKeySet(array('keys' => array(array()))); + } + + public function testParseJwkKeySet_empty() + { + $this->setExpectedException('InvalidArgumentException', 'JWK Set did not contain any keys'); + + JWK::parseKeySet(array('keys' => array())); + } + /** * @depends testParseJwkKeySet */ From 59aa36b811eb4adbaf3bfca6e4f419762a728b1b Mon Sep 17 00:00:00 2001 From: Benoit Borrel <234378+bborrel@users.noreply.github.com> Date: Fri, 5 Mar 2021 14:39:07 -0500 Subject: [PATCH 138/282] chore: add phpdoc @throws in JWT::decode (firebase/php-jwt#320) --- Jwt/src/JWT.php | 1 + 1 file changed, 1 insertion(+) diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index 4860028bc62..76a0551c490 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -62,6 +62,7 @@ class JWT * * @return object The JWT's payload as a PHP object * + * @throws InvalidArgumentException Provided JWT was empty * @throws UnexpectedValueException Provided JWT was invalid * @throws SignatureInvalidException Provided JWT was invalid because the signature verification failed * @throws BeforeValidException Provided JWT is trying to be used before it's eligible as defined by 'nbf' From d071f3ad60b4c34e3035d918513c06b13d6b7326 Mon Sep 17 00:00:00 2001 From: Ashutosh K Tripathi Date: Sat, 6 Mar 2021 03:07:26 +0530 Subject: [PATCH 139/282] chore: remove leading backslashes in imports (firebase/php-jwt#301) Co-authored-by: Brent Shaffer --- Jwt/src/JWT.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index 76a0551c490..b167abd7cb2 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -2,10 +2,10 @@ namespace Firebase\JWT; -use \DomainException; -use \InvalidArgumentException; -use \UnexpectedValueException; -use \DateTime; +use DomainException; +use InvalidArgumentException; +use UnexpectedValueException; +use DateTime; /** * JSON Web Token implementation, based on this spec: From 2e5721aa9b9bf244ddbf286d418ffb55a77ce183 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Mon, 17 May 2021 11:37:21 -0500 Subject: [PATCH 140/282] chore: remove travis, add github actions (firebase/php-jwt#331) --- Jwt/.github/actions/entrypoint.sh | 18 ++++++++ Jwt/.github/workflows/tests.yml | 66 +++++++++++++++++++++++++++ Jwt/.travis.yml | 36 --------------- Jwt/phpunit.xml.dist | 1 - Jwt/src/BeforeValidException.php | 1 + Jwt/src/ExpiredException.php | 1 + Jwt/src/SignatureInvalidException.php | 1 + Jwt/tests/JWKTest.php | 1 + Jwt/tests/JWTTest.php | 1 + 9 files changed, 89 insertions(+), 37 deletions(-) create mode 100755 Jwt/.github/actions/entrypoint.sh create mode 100644 Jwt/.github/workflows/tests.yml delete mode 100644 Jwt/.travis.yml diff --git a/Jwt/.github/actions/entrypoint.sh b/Jwt/.github/actions/entrypoint.sh new file mode 100755 index 00000000000..ce8379cba69 --- /dev/null +++ b/Jwt/.github/actions/entrypoint.sh @@ -0,0 +1,18 @@ +#!/bin/sh -l + +apt-get update && \ +apt-get install -y --no-install-recommends \ + git \ + zip \ + curl \ + unzip \ + wget + +curl --silent --show-error https://getcomposer.org/installer | php +php composer.phar self-update + +echo "---Installing dependencies ---" +php composer.phar update + +echo "---Running unit tests ---" +vendor/bin/phpunit diff --git a/Jwt/.github/workflows/tests.yml b/Jwt/.github/workflows/tests.yml new file mode 100644 index 00000000000..09539931aff --- /dev/null +++ b/Jwt/.github/workflows/tests.yml @@ -0,0 +1,66 @@ +name: Test Suite +on: + push: + branches: + - master + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + php: [ "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0" ] + name: PHP ${{matrix.php }} Unit Test + steps: + - uses: actions/checkout@v2 + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + - name: Install Dependencies + uses: nick-invision/retry@v1 + with: + timeout_minutes: 10 + max_attempts: 3 + command: composer install + - name: Run Script + run: vendor/bin/phpunit + + # use dockerfiles for old versions of php (setup-php times out for those). + test_php55: + name: "PHP 5.5 Unit Test" + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Run Unit Tests + uses: docker://php:5.5-cli + with: + entrypoint: ./.github/actions/entrypoint.sh + + test_php54: + name: "PHP 5.4 Unit Test" + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Run Unit Tests + uses: docker://php:5.4-cli + with: + entrypoint: ./.github/actions/entrypoint.sh + + style: + runs-on: ubuntu-latest + name: PHP Style Check + steps: + - uses: actions/checkout@v2 + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: "7.0" + - name: Run Script + run: | + composer require friendsofphp/php-cs-fixer + vendor/bin/php-cs-fixer fix --diff --dry-run . + vendor/bin/php-cs-fixer fix --rules=native_function_invocation --allow-risky=yes --diff src diff --git a/Jwt/.travis.yml b/Jwt/.travis.yml deleted file mode 100644 index 90e516cd060..00000000000 --- a/Jwt/.travis.yml +++ /dev/null @@ -1,36 +0,0 @@ -language: php - -branches: - - only: [master] - -php: - - 5.6 - - 7.0 - - 7.1 - - 7.2 - - 7.3 - - 7.4 - -matrix: - include: - - php: 5.3 - dist: precise - - php: 5.4 - dist: trusty - - php: 5.5 - dist: trusty - - name: "Check Style" - php: "7.4" - env: RUN_CS_FIXER=true - -sudo: false - -before_script: composer install -script: - - if [ "${RUN_CS_FIXER}" = "true" ]; then - composer require friendsofphp/php-cs-fixer && - vendor/bin/php-cs-fixer fix --diff --dry-run . && - vendor/bin/php-cs-fixer fix --rules=native_function_invocation --allow-risky=yes --diff src; - else - vendor/bin/phpunit; - fi diff --git a/Jwt/phpunit.xml.dist b/Jwt/phpunit.xml.dist index 9f85f5baa01..092a662c86c 100644 --- a/Jwt/phpunit.xml.dist +++ b/Jwt/phpunit.xml.dist @@ -8,7 +8,6 @@ convertWarningsToExceptions="true" processIsolation="false" stopOnFailure="false" - syntaxCheck="false" bootstrap="tests/bootstrap.php" > diff --git a/Jwt/src/BeforeValidException.php b/Jwt/src/BeforeValidException.php index fdf82bd9429..c147852b980 100644 --- a/Jwt/src/BeforeValidException.php +++ b/Jwt/src/BeforeValidException.php @@ -1,4 +1,5 @@ Date: Mon, 17 May 2021 11:49:46 -0500 Subject: [PATCH 141/282] fix: allow for null d values in RSA JWK (firebase/php-jwt#330) --- Jwt/src/JWK.php | 2 +- Jwt/tests/JWKTest.php | 30 ++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/Jwt/src/JWK.php b/Jwt/src/JWK.php index 7632f4a4080..29dbbac1ae3 100644 --- a/Jwt/src/JWK.php +++ b/Jwt/src/JWK.php @@ -82,7 +82,7 @@ private static function parseKey(array $jwk) switch ($jwk['kty']) { case 'RSA': - if (\array_key_exists('d', $jwk)) { + if (!empty($jwk['d'])) { throw new UnexpectedValueException('RSA private keys are not supported'); } if (!isset($jwk['n']) || !isset($jwk['e'])) { diff --git a/Jwt/tests/JWKTest.php b/Jwt/tests/JWKTest.php index 935724004ec..0709836d403 100644 --- a/Jwt/tests/JWKTest.php +++ b/Jwt/tests/JWKTest.php @@ -32,6 +32,36 @@ public function testInvalidAlgorithm() $keys = JWK::parseKeySet(array('keys' => array($badJwk))); } + public function testParsePrivateKey() + { + $this->setExpectedException( + 'UnexpectedValueException', + 'RSA private keys are not supported' + ); + + $jwkSet = json_decode( + file_get_contents(__DIR__ . '/rsa-jwkset.json'), + true + ); + $jwkSet['keys'][0]['d'] = 'privatekeyvalue'; + + JWK::parseKeySet($jwkSet); + } + + public function testParseKeyWithEmptyDValue() + { + $jwkSet = json_decode( + file_get_contents(__DIR__ . '/rsa-jwkset.json'), + true + ); + + // empty or null values are ok + $jwkSet['keys'][0]['d'] = null; + + $keys = JWK::parseKeySet($jwkSet); + $this->assertTrue(is_array($keys)); + } + public function testParseJwkKeySet() { $jwkSet = json_decode( From b5b90229b0b3aeb1a39b505c36f87ad10486d7cf Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Mon, 17 May 2021 17:50:39 -0500 Subject: [PATCH 142/282] chore(docs): clean up README (firebase/php-jwt#332) --- Jwt/README.md | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/Jwt/README.md b/Jwt/README.md index ba139079f1f..9f3d084614c 100644 --- a/Jwt/README.md +++ b/Jwt/README.md @@ -19,8 +19,7 @@ composer require firebase/php-jwt Example ------- ```php - ``` Example with RS256 (openssl) ---------------------------- ```php - ``` Using JWKs ---------- ```php +use Firebase\JWT\JWK; +use Firebase\JWT\JWT; + // Set of keys. The "keys" key is required. For example, the JSON response to // this endpoint: https://www.gstatic.com/iap/verify/public_key-jwk $jwks = ['keys' => []]; From 31b45dbb0aed670d67dc28bcfe171412aeb8214a Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Mon, 17 May 2021 15:57:50 -0700 Subject: [PATCH 143/282] chore(tests): add php 5.3 test --- Jwt/.github/workflows/tests.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Jwt/.github/workflows/tests.yml b/Jwt/.github/workflows/tests.yml index 09539931aff..0ea473584c9 100644 --- a/Jwt/.github/workflows/tests.yml +++ b/Jwt/.github/workflows/tests.yml @@ -50,6 +50,17 @@ jobs: with: entrypoint: ./.github/actions/entrypoint.sh + test_php53: + name: "PHP 5.3 Unit Test" + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Run Unit Tests + uses: docker://php:5.3-cli + with: + entrypoint: ./.github/actions/entrypoint.sh + style: runs-on: ubuntu-latest name: PHP Style Check From cf577af1add11722d9628d2cf62ec7b1626460bf Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Mon, 17 May 2021 18:35:58 -0500 Subject: [PATCH 144/282] chore(tests): fix entrypoint for php 5.3 (firebase/php-jwt#333) --- Jwt/.github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jwt/.github/workflows/tests.yml b/Jwt/.github/workflows/tests.yml index 0ea473584c9..343df75de8a 100644 --- a/Jwt/.github/workflows/tests.yml +++ b/Jwt/.github/workflows/tests.yml @@ -57,7 +57,7 @@ jobs: - name: Checkout uses: actions/checkout@v2 - name: Run Unit Tests - uses: docker://php:5.3-cli + uses: docker://tomsowerby/php-5.3:cli with: entrypoint: ./.github/actions/entrypoint.sh From 9e5264c23de2d305510f5849d433b4869b4751f7 Mon Sep 17 00:00:00 2001 From: Scott Dutton Date: Wed, 19 May 2021 19:08:37 +0100 Subject: [PATCH 145/282] chore: fix licence (firebase/php-jwt#329) --- Jwt/LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jwt/LICENSE b/Jwt/LICENSE index cb0c49b3311..11c0146651c 100644 --- a/Jwt/LICENSE +++ b/Jwt/LICENSE @@ -13,7 +13,7 @@ modification, are permitted provided that the following conditions are met: disclaimer in the documentation and/or other materials provided with the distribution. - * Neither the name of Neuman Vong nor the names of other + * Neither the name of the copyright holder nor the names of other contributors may be used to endorse or promote products derived from this software without specific prior written permission. From 838b481267371a76807560df04b1ce0c20f9c24b Mon Sep 17 00:00:00 2001 From: Stoian Ivanov Date: Wed, 19 May 2021 21:19:30 +0300 Subject: [PATCH 146/282] feat: add ES384 support (firebase/php-jwt#324) --- Jwt/src/JWT.php | 17 ++++++++++++----- Jwt/tests/JWTTest.php | 16 ++++++++++++++++ Jwt/tests/ecdsa384-private.pem | 6 ++++++ Jwt/tests/ecdsa384-public.pem | 5 +++++ 4 files changed, 39 insertions(+), 5 deletions(-) create mode 100644 Jwt/tests/ecdsa384-private.pem create mode 100644 Jwt/tests/ecdsa384-public.pem diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index b167abd7cb2..c68d4e159f7 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -42,6 +42,7 @@ class JWT public static $timestamp = null; public static $supported_algs = array( + 'ES384' => array('openssl', 'SHA384'), 'ES256' => array('openssl', 'SHA256'), 'HS256' => array('hash_hmac', 'SHA256'), 'HS384' => array('hash_hmac', 'SHA384'), @@ -58,7 +59,8 @@ class JWT * @param string|array|resource $key The key, or map of keys. * If the algorithm used is asymmetric, this is the public key * @param array $allowed_algs List of supported verification algorithms - * Supported algorithms are 'ES256', 'HS256', 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512' + * Supported algorithms are 'ES384','ES256', 'HS256', 'HS384', + * 'HS512', 'RS256', 'RS384', and 'RS512' * * @return object The JWT's payload as a PHP object * @@ -102,8 +104,8 @@ public static function decode($jwt, $key, array $allowed_algs = array()) if (!\in_array($header->alg, $allowed_algs)) { throw new UnexpectedValueException('Algorithm not allowed'); } - if ($header->alg === 'ES256') { - // OpenSSL expects an ASN.1 DER sequence for ES256 signatures + if ($header->alg === 'ES256' || $header->alg === 'ES384') { + // OpenSSL expects an ASN.1 DER sequence for ES256/ES384 signatures $sig = self::signatureToDER($sig); } @@ -155,7 +157,8 @@ public static function decode($jwt, $key, array $allowed_algs = array()) * @param string $key The secret key. * If the algorithm used is asymmetric, this is the private key * @param string $alg The signing algorithm. - * Supported algorithms are 'ES256', 'HS256', 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512' + * Supported algorithms are 'ES384','ES256', 'HS256', 'HS384', + * 'HS512', 'RS256', 'RS384', and 'RS512' * @param mixed $keyId * @param array $head An array with header elements to attach * @@ -190,7 +193,8 @@ public static function encode($payload, $key, $alg = 'HS256', $keyId = null, $he * @param string $msg The message to sign * @param string|resource $key The secret key * @param string $alg The signing algorithm. - * Supported algorithms are 'ES256', 'HS256', 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512' + * Supported algorithms are 'ES384','ES256', 'HS256', 'HS384', + * 'HS512', 'RS256', 'RS384', and 'RS512' * * @return string An encrypted message * @@ -214,6 +218,9 @@ public static function sign($msg, $key, $alg = 'HS256') if ($alg === 'ES256') { $signature = self::signatureFromDER($signature, 256); } + if ($alg === 'ES384') { + $signature = self::signatureFromDER($signature, 384); + } return $signature; } } diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index bc9d7a8c22d..2516ec0d461 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -300,4 +300,20 @@ public function testEncodeAndDecodeEcdsaToken() $this->assertEquals('bar', $decoded->foo); } + + /** + * @runInSeparateProcess + */ + public function testEncodeAndDecodeEcdsa384Token() + { + $privateKey = file_get_contents(__DIR__ . '/ecdsa384-private.pem'); + $payload = array('foo' => 'bar'); + $encoded = JWT::encode($payload, $privateKey, 'ES384'); + + // Verify decoding succeeds + $publicKey = file_get_contents(__DIR__ . '/ecdsa384-public.pem'); + $decoded = JWT::decode($encoded, $publicKey, array('ES384')); + + $this->assertEquals('bar', $decoded->foo); + } } diff --git a/Jwt/tests/ecdsa384-private.pem b/Jwt/tests/ecdsa384-private.pem new file mode 100644 index 00000000000..ee593e6f980 --- /dev/null +++ b/Jwt/tests/ecdsa384-private.pem @@ -0,0 +1,6 @@ +-----BEGIN EC PRIVATE KEY----- +MIGkAgEBBDBQJuwafREZ1494Fm2MTVXuZbWXVAOwIAxGhyLdc3CChzi0FVXZq8e6 +65oR0Qq9Jv2gBwYFK4EEACKhZANiAAQWFddzIqZaROR1VtZhhTd20mqknQmYsZ+0 +R03NQQUQpJTkyWcuv8WNyd6zO9cCoQEzi94kX907/OEWTjhuH8QtdunT+ef1BpWJ +W1Cm5O+m7b155/Ho99QypfQr74hLg1A= +-----END EC PRIVATE KEY----- diff --git a/Jwt/tests/ecdsa384-public.pem b/Jwt/tests/ecdsa384-public.pem new file mode 100644 index 00000000000..475f13483ac --- /dev/null +++ b/Jwt/tests/ecdsa384-public.pem @@ -0,0 +1,5 @@ +-----BEGIN PUBLIC KEY----- +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEFhXXcyKmWkTkdVbWYYU3dtJqpJ0JmLGf +tEdNzUEFEKSU5MlnLr/FjcneszvXAqEBM4veJF/dO/zhFk44bh/ELXbp0/nn9QaV +iVtQpuTvpu29eefx6PfUMqX0K++IS4NQ +-----END PUBLIC KEY----- From be83666069b88aed24b4f255c1db54f380da3399 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Thu, 20 May 2021 11:48:20 -0500 Subject: [PATCH 147/282] chore: test cleanup (firebase/php-jwt#334) --- Jwt/tests/JWTTest.php | 63 +++++++++++++++++++------------------------ 1 file changed, 28 insertions(+), 35 deletions(-) diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index 2516ec0d461..46ea7ef0f3a 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -19,12 +19,6 @@ public function setExpectedException($exceptionName, $message = '', $code = null } } - public function testEncodeDecode() - { - $msg = JWT::encode('abc', 'my_key'); - $this->assertEquals(JWT::decode($msg, 'my_key', array('HS256')), 'abc'); - } - public function testDecodeFromPython() { $msg = 'eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.Iio6aHR0cDovL2FwcGxpY2F0aW9uL2NsaWNreT9ibGFoPTEuMjMmZi5vbz00NTYgQUMwMDAgMTIzIg.E_U8X2YpMT5K1cEiT_3-IvBYfrdIFIeVYeOqre_Z5Cg'; @@ -217,18 +211,6 @@ public function testEmptyKeyFails() JWT::decode($encoded, '', array('HS256')); } - public function testRSEncodeDecode() - { - $privKey = openssl_pkey_new(array('digest_alg' => 'sha256', - 'private_key_bits' => 1024, - 'private_key_type' => OPENSSL_KEYTYPE_RSA)); - $msg = JWT::encode('abc', $privKey, 'RS256'); - $pubKey = openssl_pkey_get_details($privKey); - $pubKey = $pubKey['key']; - $decoded = JWT::decode($msg, $pubKey, array('RS256')); - $this->assertEquals($decoded, 'abc'); - } - public function testKIDChooser() { $keys = array('1' => 'my_key', '2' => 'my_key2'); @@ -285,35 +267,46 @@ public function testInvalidSignatureEncoding() JWT::decode($msg, 'secret', array('HS256')); } - /** - * @runInSeparateProcess - */ - public function testEncodeAndDecodeEcdsaToken() + public function testHSEncodeDecode() { - $privateKey = file_get_contents(__DIR__ . '/ecdsa-private.pem'); - $payload = array('foo' => 'bar'); - $encoded = JWT::encode($payload, $privateKey, 'ES256'); - - // Verify decoding succeeds - $publicKey = file_get_contents(__DIR__ . '/ecdsa-public.pem'); - $decoded = JWT::decode($encoded, $publicKey, array('ES256')); + $msg = JWT::encode('abc', 'my_key'); + $this->assertEquals(JWT::decode($msg, 'my_key', array('HS256')), 'abc'); + } - $this->assertEquals('bar', $decoded->foo); + public function testRSEncodeDecode() + { + $privKey = openssl_pkey_new(array('digest_alg' => 'sha256', + 'private_key_bits' => 1024, + 'private_key_type' => OPENSSL_KEYTYPE_RSA)); + $msg = JWT::encode('abc', $privKey, 'RS256'); + $pubKey = openssl_pkey_get_details($privKey); + $pubKey = $pubKey['key']; + $decoded = JWT::decode($msg, $pubKey, array('RS256')); + $this->assertEquals($decoded, 'abc'); } /** * @runInSeparateProcess + * @dataProvider provideEncodeDecode */ - public function testEncodeAndDecodeEcdsa384Token() + public function testEncodeDecode($privateKeyFile, $publicKeyFile, $alg) { - $privateKey = file_get_contents(__DIR__ . '/ecdsa384-private.pem'); + $privateKey = file_get_contents($privateKeyFile); $payload = array('foo' => 'bar'); - $encoded = JWT::encode($payload, $privateKey, 'ES384'); + $encoded = JWT::encode($payload, $privateKey, $alg); // Verify decoding succeeds - $publicKey = file_get_contents(__DIR__ . '/ecdsa384-public.pem'); - $decoded = JWT::decode($encoded, $publicKey, array('ES384')); + $publicKey = file_get_contents($publicKeyFile); + $decoded = JWT::decode($encoded, $publicKey, array($alg)); $this->assertEquals('bar', $decoded->foo); } + + public function provideEncodeDecode() + { + return array( + array(__DIR__ . '/ecdsa-private.pem', __DIR__ . '/ecdsa-public.pem', 'ES256'), + array(__DIR__ . '/ecdsa384-private.pem', __DIR__ . '/ecdsa384-public.pem', 'ES384'), + ); + } } From fa16366fc1a99aed0cd3428c53bd2054264307f5 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Thu, 20 May 2021 12:37:02 -0500 Subject: [PATCH 148/282] chore: add tests and docs for rsa with passphrase (firebase/php-jwt#335) --- Jwt/README.md | 34 +++++++++++++++++++++++++ Jwt/src/JWT.php | 16 ++++++------ Jwt/tests/JWTTest.php | 14 +++++++++++ Jwt/tests/rsa-with-passphrase.pem | 42 +++++++++++++++++++++++++++++++ 4 files changed, 98 insertions(+), 8 deletions(-) create mode 100644 Jwt/tests/rsa-with-passphrase.pem diff --git a/Jwt/README.md b/Jwt/README.md index 9f3d084614c..66d7d014261 100644 --- a/Jwt/README.md +++ b/Jwt/README.md @@ -110,6 +110,40 @@ $decoded_array = (array) $decoded; echo "Decode:\n" . print_r($decoded_array, true) . "\n"; ``` +Example with a passphrase +------------------------- + +```php +// Your passphrase +$passphrase = '[YOUR_PASSPHRASE]'; + +// Your private key file with passphrase +// Can be generated with "ssh-keygen -t rsa -m pem" +$privateKeyFile = '/path/to/key-with-passphrase.pem'; + +// Create a private key of type "resource" +$privateKey = openssl_pkey_get_private( + file_get_contents($privateKeyFile), + $passphrase +); + +$payload = array( + "iss" => "example.org", + "aud" => "example.com", + "iat" => 1356999524, + "nbf" => 1357000000 +); + +$jwt = JWT::encode($payload, $privateKey, 'RS256'); +echo "Encode:\n" . print_r($jwt, true) . "\n"; + +// Get public key from the private key, or pull from from a file. +$publicKey = openssl_pkey_get_details($privateKey)['key']; + +$decoded = JWT::decode($jwt, $publicKey, array('RS256')); +echo "Decode:\n" . print_r((array) $decoded, true) . "\n"; +``` + Using JWKs ---------- diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index c68d4e159f7..4b85699f08c 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -153,14 +153,14 @@ public static function decode($jwt, $key, array $allowed_algs = array()) /** * Converts and signs a PHP object or array into a JWT string. * - * @param object|array $payload PHP object or array - * @param string $key The secret key. - * If the algorithm used is asymmetric, this is the private key - * @param string $alg The signing algorithm. - * Supported algorithms are 'ES384','ES256', 'HS256', 'HS384', - * 'HS512', 'RS256', 'RS384', and 'RS512' - * @param mixed $keyId - * @param array $head An array with header elements to attach + * @param object|array $payload PHP object or array + * @param string|resource $key The secret key. + * If the algorithm used is asymmetric, this is the private key + * @param string $alg The signing algorithm. + * Supported algorithms are 'ES384','ES256', 'HS256', 'HS384', + * 'HS512', 'RS256', 'RS384', and 'RS512' + * @param mixed $keyId + * @param array $head An array with header elements to attach * * @return string A signed JWT * diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index 46ea7ef0f3a..7c3d9649df3 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -285,6 +285,20 @@ public function testRSEncodeDecode() $this->assertEquals($decoded, 'abc'); } + public function testRSEncodeDecodeWithPassphrase() + { + $privateKey = openssl_pkey_get_private( + file_get_contents(__DIR__ . '/rsa-with-passphrase.pem'), + 'passphrase' + ); + + $jwt = JWT::encode('abc', $privateKey, 'RS256'); + $keyDetails = openssl_pkey_get_details($privateKey); + $pubKey = $keyDetails['key']; + $decoded = JWT::decode($jwt, $pubKey, array('RS256')); + $this->assertEquals($decoded, 'abc'); + } + /** * @runInSeparateProcess * @dataProvider provideEncodeDecode diff --git a/Jwt/tests/rsa-with-passphrase.pem b/Jwt/tests/rsa-with-passphrase.pem new file mode 100644 index 00000000000..ad55326f4f5 --- /dev/null +++ b/Jwt/tests/rsa-with-passphrase.pem @@ -0,0 +1,42 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-128-CBC,D8AA4EC8D8B5883F09ACB308FB026C94 + +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 +-----END RSA PRIVATE KEY----- From 01914ffacb9c41bd227473f617001751687cbf8d Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Tue, 22 Jun 2021 13:05:02 -0500 Subject: [PATCH 149/282] feat: make parseKey public (firebase/php-jwt#337) --- Jwt/src/JWK.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jwt/src/JWK.php b/Jwt/src/JWK.php index 29dbbac1ae3..981a9ba7f50 100644 --- a/Jwt/src/JWK.php +++ b/Jwt/src/JWK.php @@ -71,7 +71,7 @@ public static function parseKeySet(array $jwks) * * @uses createPemFromModulusAndExponent */ - private static function parseKey(array $jwk) + public static function parseKey(array $jwk) { if (empty($jwk)) { throw new InvalidArgumentException('JWK must not be empty'); From ffb1fb8d5f18456f7e4928818d0d81f0a075a51f Mon Sep 17 00:00:00 2001 From: Romain Menke <11521496+romainmenke@users.noreply.github.com> Date: Wed, 23 Jun 2021 18:25:50 +0200 Subject: [PATCH 150/282] chore: export-ignore github dir (firebase/php-jwt#338) --- Jwt/.gitattributes | 1 + 1 file changed, 1 insertion(+) diff --git a/Jwt/.gitattributes b/Jwt/.gitattributes index a791521e36e..6d63e560e94 100644 --- a/Jwt/.gitattributes +++ b/Jwt/.gitattributes @@ -5,3 +5,4 @@ /.gitignore export-ignore /.travis.yml export-ignore /phpunit.xml.dist export-ignore +/.github export-ignore From 7835b2ca06da91fff2708b1e5d28e4a7f0d8c6a3 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 23 Jun 2021 11:39:56 -0500 Subject: [PATCH 151/282] chore(tests): add rsa encode/decode (firebase/php-jwt#344) --- Jwt/tests/JWTTest.php | 1 + Jwt/tests/rsa1-public.pub | 9 +++++++++ 2 files changed, 10 insertions(+) create mode 100644 Jwt/tests/rsa1-public.pub diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index 7c3d9649df3..09dac1421cb 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -321,6 +321,7 @@ public function provideEncodeDecode() return array( array(__DIR__ . '/ecdsa-private.pem', __DIR__ . '/ecdsa-public.pem', 'ES256'), array(__DIR__ . '/ecdsa384-private.pem', __DIR__ . '/ecdsa384-public.pem', 'ES384'), + array(__DIR__ . '/rsa1-private.pem', __DIR__ . '/rsa1-public.pub', 'RS512'), ); } } diff --git a/Jwt/tests/rsa1-public.pub b/Jwt/tests/rsa1-public.pub new file mode 100644 index 00000000000..83a080f4d3f --- /dev/null +++ b/Jwt/tests/rsa1-public.pub @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0Ttga33B1yX4w77NbpKy +NYDNSVCo8j+RlZaZ9tI+KfkV1d+tfsvI9ZPAheP11FoN52ceBaY5ltelHW+IKwCf +yT0orLdsxLgowaXki9woF1Azvcg2JVxQLv9aVjjAvy3CZFIG/EeN7J3nsyCXGnu1 +yMEbnvkWxA88//Q6HQ2K9wqfApkQ0LNlsK0YHz/sfjHNvRKxnbAJk7D5fUhZunPZ +XOPHXFgA5SvLvMaNIXduMKJh4OMfuoLdJowXJAR9j31Mqz/is4FMhm/9Mq7vZZ+u +F09htRvIR8tRY28oJuW1gKWyg7cQQpnjHgFyG3XLXWAeXclWqyh/LfjyHQjrYhye +FwIDAQAB +-----END PUBLIC KEY----- From 82d5dd172ec4f22010e2f67ffd95cb81383e6d09 Mon Sep 17 00:00:00 2001 From: pwolanin Date: Wed, 23 Jun 2021 15:00:23 -0400 Subject: [PATCH 152/282] feat: add Ed25519 support to JWT (firebase/php-jwt#343) --- Jwt/.github/actions/entrypoint.sh | 4 ++- Jwt/.github/workflows/tests.yml | 2 ++ Jwt/README.md | 37 ++++++++++++++++++++++++++ Jwt/composer.json | 3 +++ Jwt/src/JWT.php | 44 ++++++++++++++++++++++++------- Jwt/tests/JWTTest.php | 29 ++++++++++++++++++++ Jwt/tests/ed25519-1.pub | 1 + Jwt/tests/ed25519-1.sec | 1 + 8 files changed, 110 insertions(+), 11 deletions(-) create mode 100644 Jwt/tests/ed25519-1.pub create mode 100644 Jwt/tests/ed25519-1.sec diff --git a/Jwt/.github/actions/entrypoint.sh b/Jwt/.github/actions/entrypoint.sh index ce8379cba69..8b6b9e1b298 100755 --- a/Jwt/.github/actions/entrypoint.sh +++ b/Jwt/.github/actions/entrypoint.sh @@ -12,7 +12,9 @@ curl --silent --show-error https://getcomposer.org/installer | php php composer.phar self-update echo "---Installing dependencies ---" -php composer.phar update + +# Add compatiblity for libsodium with older versions of PHP +php composer.phar require --dev --with-dependencies paragonie/sodium_compat echo "---Running unit tests ---" vendor/bin/phpunit diff --git a/Jwt/.github/workflows/tests.yml b/Jwt/.github/workflows/tests.yml index 343df75de8a..92b4e9e0050 100644 --- a/Jwt/.github/workflows/tests.yml +++ b/Jwt/.github/workflows/tests.yml @@ -24,6 +24,8 @@ jobs: timeout_minutes: 10 max_attempts: 3 command: composer install + - if: ${{ matrix.php == '5.6' }} + run: composer require --dev --with-dependencies paragonie/sodium_compat - name: Run Script run: vendor/bin/phpunit diff --git a/Jwt/README.md b/Jwt/README.md index 66d7d014261..a8556aa5be4 100644 --- a/Jwt/README.md +++ b/Jwt/README.md @@ -16,6 +16,13 @@ Use composer to manage your dependencies and download PHP-JWT: composer require firebase/php-jwt ``` +Optionally, install the `paragonie/sodium_compat` package from composer if your +php is < 7.2 or does not have libsodium installed: + +```bash +composer require paragonie/sodium_compat +``` + Example ------- ```php @@ -144,6 +151,36 @@ $decoded = JWT::decode($jwt, $publicKey, array('RS256')); echo "Decode:\n" . print_r((array) $decoded, true) . "\n"; ``` +Example with EdDSA (libsodium and Ed25519 signature) +---------------------------- +```php +use Firebase\JWT\JWT; + +// Public and private keys are expected to be Base64 encoded. The last +// non-empty line is used so that keys can be generated with +// sodium_crypto_sign_keypair(). The secret keys generated by other tools may +// need to be adjusted to match the input expected by libsodium. + +$keyPair = sodium_crypto_sign_keypair(); + +$privateKey = base64_encode(sodium_crypto_sign_secretkey($keyPair)); + +$publicKey = base64_encode(sodium_crypto_sign_publickey($keyPair)); + +$payload = array( + "iss" => "example.org", + "aud" => "example.com", + "iat" => 1356999524, + "nbf" => 1357000000 +); + +$jwt = JWT::encode($payload, $privateKey, 'EdDSA'); +echo "Encode:\n" . print_r($jwt, true) . "\n"; + +$decoded = JWT::decode($jwt, $publicKey, array('EdDSA')); +echo "Decode:\n" . print_r((array) $decoded, true) . "\n"; +```` + Using JWKs ---------- diff --git a/Jwt/composer.json b/Jwt/composer.json index 25d1cfa96ad..6146e2dcc12 100644 --- a/Jwt/composer.json +++ b/Jwt/composer.json @@ -22,6 +22,9 @@ "require": { "php": ">=5.3.0" }, + "suggest": { + "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" + }, "autoload": { "psr-4": { "Firebase\\JWT\\": "src" diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index 4b85699f08c..99d6dcd2423 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -3,6 +3,7 @@ namespace Firebase\JWT; use DomainException; +use Exception; use InvalidArgumentException; use UnexpectedValueException; use DateTime; @@ -50,6 +51,7 @@ class JWT 'RS256' => array('openssl', 'SHA256'), 'RS384' => array('openssl', 'SHA384'), 'RS512' => array('openssl', 'SHA512'), + 'EdDSA' => array('sodium_crypto', 'EdDSA'), ); /** @@ -198,7 +200,7 @@ public static function encode($payload, $key, $alg = 'HS256', $keyId = null, $he * * @return string An encrypted message * - * @throws DomainException Unsupported algorithm was specified + * @throws DomainException Unsupported algorithm or bad key was specified */ public static function sign($msg, $key, $alg = 'HS256') { @@ -214,14 +216,24 @@ public static function sign($msg, $key, $alg = 'HS256') $success = \openssl_sign($msg, $signature, $key, $algorithm); if (!$success) { throw new DomainException("OpenSSL unable to sign data"); - } else { - if ($alg === 'ES256') { - $signature = self::signatureFromDER($signature, 256); - } - if ($alg === 'ES384') { - $signature = self::signatureFromDER($signature, 384); - } - return $signature; + } + if ($alg === 'ES256') { + $signature = self::signatureFromDER($signature, 256); + } elseif ($alg === 'ES384') { + $signature = self::signatureFromDER($signature, 384); + } + return $signature; + case 'sodium_crypto': + if (!function_exists('sodium_crypto_sign_detached')) { + throw new DomainException('libsodium is not available'); + } + try { + // The last non-empty line is used as the key. + $lines = array_filter(explode("\n", $key)); + $key = base64_decode(end($lines)); + return sodium_crypto_sign_detached($msg, $key); + } catch (Exception $e) { + throw new DomainException($e->getMessage(), 0, $e); } } } @@ -237,7 +249,7 @@ public static function sign($msg, $key, $alg = 'HS256') * * @return bool * - * @throws DomainException Invalid Algorithm or OpenSSL failure + * @throws DomainException Invalid Algorithm, bad key, or OpenSSL failure */ private static function verify($msg, $signature, $key, $alg) { @@ -258,6 +270,18 @@ private static function verify($msg, $signature, $key, $alg) throw new DomainException( 'OpenSSL error: ' . \openssl_error_string() ); + case 'sodium_crypto': + if (!function_exists('sodium_crypto_sign_verify_detached')) { + throw new DomainException('libsodium is not available'); + } + try { + // The last non-empty line is used as the key. + $lines = array_filter(explode("\n", $key)); + $key = base64_decode(end($lines)); + return sodium_crypto_sign_verify_detached($signature, $msg, $key); + } catch (Exception $e) { + throw new DomainException($e->getMessage(), 0, $e); + } case 'hash_hmac': default: $hash = \hash_hmac($algorithm, $msg, $key, true); diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index 09dac1421cb..3dee0450fd8 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -285,6 +285,34 @@ public function testRSEncodeDecode() $this->assertEquals($decoded, 'abc'); } + public function testEdDsaEncodeDecode() + { + $keyPair = sodium_crypto_sign_keypair(); + $privKey = base64_encode(sodium_crypto_sign_secretkey($keyPair)); + + $payload = array('foo' => 'bar'); + $msg = JWT::encode($payload, $privKey, 'EdDSA'); + + $pubKey = base64_encode(sodium_crypto_sign_publickey($keyPair)); + $decoded = JWT::decode($msg, $pubKey, array('EdDSA')); + $this->assertEquals('bar', $decoded->foo); + } + + public function testInvalidEdDsaEncodeDecode() + { + $keyPair = sodium_crypto_sign_keypair(); + $privKey = base64_encode(sodium_crypto_sign_secretkey($keyPair)); + + $payload = array('foo' => 'bar'); + $msg = JWT::encode($payload, $privKey, 'EdDSA'); + + // Generate a different key. + $keyPair = sodium_crypto_sign_keypair(); + $pubKey = base64_encode(sodium_crypto_sign_publickey($keyPair)); + $this->setExpectedException('Firebase\JWT\SignatureInvalidException'); + JWT::decode($msg, $pubKey, array('EdDSA')); + } + public function testRSEncodeDecodeWithPassphrase() { $privateKey = openssl_pkey_get_private( @@ -322,6 +350,7 @@ public function provideEncodeDecode() array(__DIR__ . '/ecdsa-private.pem', __DIR__ . '/ecdsa-public.pem', 'ES256'), array(__DIR__ . '/ecdsa384-private.pem', __DIR__ . '/ecdsa384-public.pem', 'ES384'), array(__DIR__ . '/rsa1-private.pem', __DIR__ . '/rsa1-public.pub', 'RS512'), + array(__DIR__ . '/ed25519-1.sec', __DIR__ . '/ed25519-1.pub', 'EdDSA'), ); } } diff --git a/Jwt/tests/ed25519-1.pub b/Jwt/tests/ed25519-1.pub new file mode 100644 index 00000000000..e4ae63ac10b --- /dev/null +++ b/Jwt/tests/ed25519-1.pub @@ -0,0 +1 @@ +uOSJMhbKSG4V5xUHS7B9YHmVg/1yVd+G+Io6oBFhSfY= diff --git a/Jwt/tests/ed25519-1.sec b/Jwt/tests/ed25519-1.sec new file mode 100644 index 00000000000..354ffa7ae2e --- /dev/null +++ b/Jwt/tests/ed25519-1.sec @@ -0,0 +1 @@ +i4eTKkWNIISKumdk3v90cPDrY/g8WRTJWy7DmGDsdzC45IkyFspIbhXnFQdLsH1geZWD/XJV34b4ijqgEWFJ9g== From 8179c3f981cff78eb599ec244ad8ec2c9a8df44d Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 3 Nov 2021 15:57:11 -0600 Subject: [PATCH 153/282] chore: fix tests (firebase/php-jwt#366) --- Jwt/.github/actions/entrypoint.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/Jwt/.github/actions/entrypoint.sh b/Jwt/.github/actions/entrypoint.sh index 8b6b9e1b298..40402bc8595 100755 --- a/Jwt/.github/actions/entrypoint.sh +++ b/Jwt/.github/actions/entrypoint.sh @@ -5,6 +5,7 @@ apt-get install -y --no-install-recommends \ git \ zip \ curl \ + ca-certificates \ unzip \ wget From 1da5f116d8e5dd1e8bbdd4c363900b973f43c03d Mon Sep 17 00:00:00 2001 From: Sergiy Petrov Date: Thu, 4 Nov 2021 00:00:38 +0200 Subject: [PATCH 154/282] chore: add PHP 8.1 (firebase/php-jwt#362) --- Jwt/.github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jwt/.github/workflows/tests.yml b/Jwt/.github/workflows/tests.yml index 92b4e9e0050..6f0f5e9108e 100644 --- a/Jwt/.github/workflows/tests.yml +++ b/Jwt/.github/workflows/tests.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php: [ "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0" ] + php: [ "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"] name: PHP ${{matrix.php }} Unit Test steps: - uses: actions/checkout@v2 From 28248048afa90b35b251b0a20244c28b5f637d38 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Thu, 4 Nov 2021 10:15:22 -0600 Subject: [PATCH 155/282] feat: add Key object to prevent key/algorithm type confusion (firebase/php-jwt#365) --- Jwt/README.md | 17 ++++-- Jwt/src/JWT.php | 122 +++++++++++++++++++++++++++++++----------- Jwt/src/Key.php | 59 ++++++++++++++++++++ Jwt/tests/JWTTest.php | 28 ++++++++++ 4 files changed, 190 insertions(+), 36 deletions(-) create mode 100644 Jwt/src/Key.php diff --git a/Jwt/README.md b/Jwt/README.md index a8556aa5be4..ee98c47f43e 100644 --- a/Jwt/README.md +++ b/Jwt/README.md @@ -27,6 +27,7 @@ Example ------- ```php use Firebase\JWT\JWT; +use Firebase\JWT\Key; $key = "example_key"; $payload = array( @@ -43,7 +44,7 @@ $payload = array( * for a list of spec-compliant algorithms. */ $jwt = JWT::encode($payload, $key); -$decoded = JWT::decode($jwt, $key, array('HS256')); +$decoded = JWT::decode($jwt, new Key($key, 'HS256')); print_r($decoded); @@ -62,12 +63,13 @@ $decoded_array = (array) $decoded; * Source: http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html#nbfDef */ JWT::$leeway = 60; // $leeway in seconds -$decoded = JWT::decode($jwt, $key, array('HS256')); +$decoded = JWT::decode($jwt, new Key($key, 'HS256')); ``` Example with RS256 (openssl) ---------------------------- ```php use Firebase\JWT\JWT; +use Firebase\JWT\Key; $privateKey = << []]; // JWK::parseKeySet($jwks) returns an associative array of **kid** to private // key. Pass this as the second parameter to JWT::decode. +// NOTE: The deprecated $supportedAlgorithm must be supplied when parsing from JWK. JWT::decode($payload, JWK::parseKeySet($jwks), $supportedAlgorithm); ``` diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index 99d6dcd2423..f46e8372636 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -2,6 +2,7 @@ namespace Firebase\JWT; +use ArrayAccess; use DomainException; use Exception; use InvalidArgumentException; @@ -58,11 +59,13 @@ class JWT * Decodes a JWT string into a PHP object. * * @param string $jwt The JWT - * @param string|array|resource $key The key, or map of keys. + * @param Key|array $keyOrKeyArray The Key or array of Key objects. * If the algorithm used is asymmetric, this is the public key - * @param array $allowed_algs List of supported verification algorithms + * Each Key object contains an algorithm and matching key. * Supported algorithms are 'ES384','ES256', 'HS256', 'HS384', * 'HS512', 'RS256', 'RS384', and 'RS512' + * @param array $allowed_algs [DEPRECATED] List of supported verification algorithms. Only + * should be used for backwards compatibility. * * @return object The JWT's payload as a PHP object * @@ -76,11 +79,11 @@ class JWT * @uses jsonDecode * @uses urlsafeB64Decode */ - public static function decode($jwt, $key, array $allowed_algs = array()) + public static function decode($jwt, $keyOrKeyArray, array $allowed_algs = array()) { $timestamp = \is_null(static::$timestamp) ? \time() : static::$timestamp; - if (empty($key)) { + if (empty($keyOrKeyArray)) { throw new InvalidArgumentException('Key may not be empty'); } $tks = \explode('.', $jwt); @@ -103,27 +106,32 @@ public static function decode($jwt, $key, array $allowed_algs = array()) if (empty(static::$supported_algs[$header->alg])) { throw new UnexpectedValueException('Algorithm not supported'); } - if (!\in_array($header->alg, $allowed_algs)) { - throw new UnexpectedValueException('Algorithm not allowed'); + + list($keyMaterial, $algorithm) = self::getKeyMaterialAndAlgorithm( + $keyOrKeyArray, + empty($header->kid) ? null : $header->kid + ); + + if (empty($algorithm)) { + // Use deprecated "allowed_algs" to determine if the algorithm is supported. + // This opens up the possibility of an attack in some implementations. + // @see https://github.com/firebase/php-jwt/issues/351 + if (!\in_array($header->alg, $allowed_algs)) { + throw new UnexpectedValueException('Algorithm not allowed'); + } + } else { + // Check the algorithm + if (!self::constantTimeEquals($algorithm, $header->alg)) { + // See issue #351 + throw new UnexpectedValueException('Incorrect key for this algorithm'); + } } if ($header->alg === 'ES256' || $header->alg === 'ES384') { // OpenSSL expects an ASN.1 DER sequence for ES256/ES384 signatures $sig = self::signatureToDER($sig); } - if (\is_array($key) || $key instanceof \ArrayAccess) { - if (isset($header->kid)) { - if (!isset($key[$header->kid])) { - throw new UnexpectedValueException('"kid" invalid, unable to lookup correct key'); - } - $key = $key[$header->kid]; - } else { - throw new UnexpectedValueException('"kid" empty, unable to lookup correct key'); - } - } - - // Check the signature - if (!static::verify("$headb64.$bodyb64", $sig, $key, $header->alg)) { + if (!static::verify("$headb64.$bodyb64", $sig, $keyMaterial, $header->alg)) { throw new SignatureInvalidException('Signature verification failed'); } @@ -285,18 +293,7 @@ private static function verify($msg, $signature, $key, $alg) case 'hash_hmac': default: $hash = \hash_hmac($algorithm, $msg, $key, true); - if (\function_exists('hash_equals')) { - return \hash_equals($signature, $hash); - } - $len = \min(static::safeStrlen($signature), static::safeStrlen($hash)); - - $status = 0; - for ($i = 0; $i < $len; $i++) { - $status |= (\ord($signature[$i]) ^ \ord($hash[$i])); - } - $status |= (static::safeStrlen($signature) ^ static::safeStrlen($hash)); - - return ($status === 0); + return self::constantTimeEquals($signature, $hash); } } @@ -384,6 +381,69 @@ public static function urlsafeB64Encode($input) return \str_replace('=', '', \strtr(\base64_encode($input), '+/', '-_')); } + + /** + * Determine if an algorithm has been provided for each Key + * + * @param string|array $keyOrKeyArray + * @param string|null $kid + * + * @return an array containing the keyMaterial and algorithm + */ + private static function getKeyMaterialAndAlgorithm($keyOrKeyArray, $kid = null) + { + if (is_string($keyOrKeyArray)) { + return array($keyOrKeyArray, null); + } + + if ($keyOrKeyArray instanceof Key) { + return array($keyOrKeyArray->getKeyMaterial(), $keyOrKeyArray->getAlgorithm()); + } + + if (is_array($keyOrKeyArray) || $keyOrKeyArray instanceof ArrayAccess) { + if (!isset($kid)) { + throw new UnexpectedValueException('"kid" empty, unable to lookup correct key'); + } + if (!isset($keyOrKeyArray[$kid])) { + throw new UnexpectedValueException('"kid" invalid, unable to lookup correct key'); + } + + $key = $keyOrKeyArray[$kid]; + + if ($key instanceof Key) { + return array($key->getKeyMaterial(), $key->getAlgorithm()); + } + + return array($key, null); + } + + throw new UnexpectedValueException( + '$keyOrKeyArray must be a string key, an array of string keys, ' + . 'an instance of Firebase\JWT\Key key or an array of Firebase\JWT\Key keys' + ); + } + + /** + * @param string $left + * @param string $right + * @return bool + */ + public static function constantTimeEquals($left, $right) + { + if (\function_exists('hash_equals')) { + return \hash_equals($left, $right); + } + $len = \min(static::safeStrlen($left), static::safeStrlen($right)); + + $status = 0; + for ($i = 0; $i < $len; $i++) { + $status |= (\ord($left[$i]) ^ \ord($right[$i])); + } + $status |= (static::safeStrlen($left) ^ static::safeStrlen($right)); + + return ($status === 0); + } + /** * Helper method to create a JSON error. * diff --git a/Jwt/src/Key.php b/Jwt/src/Key.php new file mode 100644 index 00000000000..f1ede6f2717 --- /dev/null +++ b/Jwt/src/Key.php @@ -0,0 +1,59 @@ +keyMaterial = $keyMaterial; + $this->algorithm = $algorithm; + } + + /** + * Return the algorithm valid for this key + * + * @return string + */ + public function getAlgorithm() + { + return $this->algorithm; + } + + /** + * @return string|resource|OpenSSLAsymmetricKey + */ + public function getKeyMaterial() + { + return $this->keyMaterial; + } +} diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index 3dee0450fd8..63386d88ccc 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -344,6 +344,34 @@ public function testEncodeDecode($privateKeyFile, $publicKeyFile, $alg) $this->assertEquals('bar', $decoded->foo); } + /** + * @runInSeparateProcess + * @dataProvider provideEncodeDecode + */ + public function testEncodeDecodeWithKeyObject($privateKeyFile, $publicKeyFile, $alg) + { + $privateKey = file_get_contents($privateKeyFile); + $payload = array('foo' => 'bar'); + $encoded = JWT::encode($payload, $privateKey, $alg); + + // Verify decoding succeeds + $publicKey = file_get_contents($publicKeyFile); + $decoded = JWT::decode($encoded, new Key($publicKey, $alg)); + + $this->assertEquals('bar', $decoded->foo); + } + + public function testArrayAccessKIDChooserWithKeyObject() + { + $keys = new ArrayObject(array( + '1' => new Key('my_key', 'HS256'), + '2' => new Key('my_key2', 'HS256'), + )); + $msg = JWT::encode('abc', $keys['1']->getKeyMaterial(), 'HS256', '1'); + $decoded = JWT::decode($msg, $keys); + $this->assertEquals($decoded, 'abc'); + } + public function provideEncodeDecode() { return array( From 47f2b98bd360dfaf0de373afc88403858d172aae Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Thu, 4 Nov 2021 10:21:41 -0600 Subject: [PATCH 156/282] chore: explicit third parameter to decode function in README --- Jwt/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jwt/README.md b/Jwt/README.md index ee98c47f43e..1d392cd1200 100644 --- a/Jwt/README.md +++ b/Jwt/README.md @@ -43,7 +43,7 @@ $payload = array( * https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40 * for a list of spec-compliant algorithms. */ -$jwt = JWT::encode($payload, $key); +$jwt = JWT::encode($payload, $key, 'HS256'); $decoded = JWT::decode($jwt, new Key($key, 'HS256')); print_r($decoded); From 153be4102a6f2ef57bf89ff1be7ed6f936811f5b Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Mon, 8 Nov 2021 14:18:51 -0600 Subject: [PATCH 157/282] fix: phpdoc and exception (firebase/php-jwt#371) --- Jwt/src/JWT.php | 17 ++++++++++++----- Jwt/tests/JWTTest.php | 15 +++++++++++++++ 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index f46e8372636..ec1641bc404 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -6,6 +6,7 @@ use DomainException; use Exception; use InvalidArgumentException; +use OpenSSLAsymmetricKey; use UnexpectedValueException; use DateTime; @@ -59,7 +60,7 @@ class JWT * Decodes a JWT string into a PHP object. * * @param string $jwt The JWT - * @param Key|array $keyOrKeyArray The Key or array of Key objects. + * @param Key|array|mixed $keyOrKeyArray The Key or array of Key objects. * If the algorithm used is asymmetric, this is the public key * Each Key object contains an algorithm and matching key. * Supported algorithms are 'ES384','ES256', 'HS256', 'HS384', @@ -385,14 +386,20 @@ public static function urlsafeB64Encode($input) /** * Determine if an algorithm has been provided for each Key * - * @param string|array $keyOrKeyArray + * @param Key|array|mixed $keyOrKeyArray * @param string|null $kid * - * @return an array containing the keyMaterial and algorithm + * @throws UnexpectedValueException + * + * @return array containing the keyMaterial and algorithm */ private static function getKeyMaterialAndAlgorithm($keyOrKeyArray, $kid = null) { - if (is_string($keyOrKeyArray)) { + if ( + is_string($keyOrKeyArray) + || is_resource($keyOrKeyArray) + || $keyOrKeyArray instanceof OpenSSLAsymmetricKey + ) { return array($keyOrKeyArray, null); } @@ -418,7 +425,7 @@ private static function getKeyMaterialAndAlgorithm($keyOrKeyArray, $kid = null) } throw new UnexpectedValueException( - '$keyOrKeyArray must be a string key, an array of string keys, ' + '$keyOrKeyArray must be a string|resource key, an array of string|resource keys, ' . 'an instance of Firebase\JWT\Key key or an array of Firebase\JWT\Key keys' ); } diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index 63386d88ccc..1c81c1eddf1 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -381,4 +381,19 @@ public function provideEncodeDecode() array(__DIR__ . '/ed25519-1.sec', __DIR__ . '/ed25519-1.pub', 'EdDSA'), ); } + + public function testEncodeDecodeWithResource() + { + $pem = file_get_contents(__DIR__ . '/rsa1-public.pub'); + $resource = openssl_pkey_get_public($pem); + $privateKey = file_get_contents(__DIR__ . '/rsa1-private.pem'); + + $payload = array('foo' => 'bar'); + $encoded = JWT::encode($payload, $privateKey, 'RS512'); + + // Verify decoding succeeds + $decoded = JWT::decode($encoded, $resource, array('RS512')); + + $this->assertEquals('bar', $decoded->foo); + } } From c85a9bd801cac450455aa435ecb86aade06b1a6b Mon Sep 17 00:00:00 2001 From: Andrej Rypo Date: Wed, 1 Dec 2021 23:04:08 +0100 Subject: [PATCH 158/282] chore(docs): add throws DomainException for JWT::decode (firebase/php-jwt#379) --- Jwt/src/JWT.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index ec1641bc404..b2e78041ef2 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -70,7 +70,8 @@ class JWT * * @return object The JWT's payload as a PHP object * - * @throws InvalidArgumentException Provided JWT was empty + * @throws InvalidArgumentException Provided key/key-array was empty + * @throws DomainException Provided JWT is malformed * @throws UnexpectedValueException Provided JWT was invalid * @throws SignatureInvalidException Provided JWT was invalid because the signature verification failed * @throws BeforeValidException Provided JWT is trying to be used before it's eligible as defined by 'nbf' From 63e1cfc9102ae2014fbc89d8725c5e92b0bfcd52 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 1 Dec 2021 17:04:45 -0500 Subject: [PATCH 159/282] chore: switch main to master (firebase/php-jwt#383) --- Jwt/.github/workflows/tests.yml | 2 +- Jwt/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Jwt/.github/workflows/tests.yml b/Jwt/.github/workflows/tests.yml index 6f0f5e9108e..50d8a5f06d6 100644 --- a/Jwt/.github/workflows/tests.yml +++ b/Jwt/.github/workflows/tests.yml @@ -2,7 +2,7 @@ name: Test Suite on: push: branches: - - master + - main pull_request: jobs: diff --git a/Jwt/README.md b/Jwt/README.md index 1d392cd1200..af7ed087295 100644 --- a/Jwt/README.md +++ b/Jwt/README.md @@ -1,4 +1,4 @@ -[![Build Status](https://travis-ci.org/firebase/php-jwt.png?branch=master)](https://travis-ci.org/firebase/php-jwt) +[![Build Status](https://github.com/firebase/php-jwt/actions/workflows/tests.yml/badge.svg) [![Latest Stable Version](https://poser.pugx.org/firebase/php-jwt/v/stable)](https://packagist.org/packages/firebase/php-jwt) [![Total Downloads](https://poser.pugx.org/firebase/php-jwt/downloads)](https://packagist.org/packages/firebase/php-jwt) [![License](https://poser.pugx.org/firebase/php-jwt/license)](https://packagist.org/packages/firebase/php-jwt) From 7e50a6b52a5dec6c8f8be79da631ebcd39f902b1 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 1 Dec 2021 14:06:59 -0800 Subject: [PATCH 160/282] chore(docs): fix typo in README --- Jwt/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jwt/README.md b/Jwt/README.md index af7ed087295..acd1720c355 100644 --- a/Jwt/README.md +++ b/Jwt/README.md @@ -1,4 +1,4 @@ -[![Build Status](https://github.com/firebase/php-jwt/actions/workflows/tests.yml/badge.svg) +![Build Status](https://github.com/firebase/php-jwt/actions/workflows/tests.yml/badge.svg) [![Latest Stable Version](https://poser.pugx.org/firebase/php-jwt/v/stable)](https://packagist.org/packages/firebase/php-jwt) [![Total Downloads](https://poser.pugx.org/firebase/php-jwt/downloads)](https://packagist.org/packages/firebase/php-jwt) [![License](https://poser.pugx.org/firebase/php-jwt/license)](https://packagist.org/packages/firebase/php-jwt) From 648f4836ff900700959c297ff918559ae1fd2893 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Thu, 30 Dec 2021 11:00:10 -0800 Subject: [PATCH 161/282] feat!: require Key object, use JSON_UNESCAPED_SLASHES, remove constants (firebase/php-jwt#376) --- Jwt/.gitignore | 1 + Jwt/README.md | 3 +- Jwt/src/JWK.php | 10 +- Jwt/src/JWT.php | 94 +++++------ Jwt/tests/JWKTest.php | 38 +++-- Jwt/tests/JWTTest.php | 168 ++++++++----------- Jwt/tests/autoload.php.dist | 17 -- Jwt/tests/bootstrap.php | 13 +- Jwt/tests/{ => data}/ecdsa-private.pem | 0 Jwt/tests/{ => data}/ecdsa-public.pem | 0 Jwt/tests/{ => data}/ecdsa384-private.pem | 0 Jwt/tests/{ => data}/ecdsa384-public.pem | 0 Jwt/tests/{ => data}/ed25519-1.pub | 0 Jwt/tests/{ => data}/ed25519-1.sec | 0 Jwt/tests/{ => data}/rsa-jwkset.json | 7 +- Jwt/tests/{ => data}/rsa-with-passphrase.pem | 0 Jwt/tests/{ => data}/rsa1-private.pem | 0 Jwt/tests/{ => data}/rsa1-public.pub | 0 Jwt/tests/{ => data}/rsa2-private.pem | 0 19 files changed, 161 insertions(+), 190 deletions(-) delete mode 100644 Jwt/tests/autoload.php.dist rename Jwt/tests/{ => data}/ecdsa-private.pem (100%) rename Jwt/tests/{ => data}/ecdsa-public.pem (100%) rename Jwt/tests/{ => data}/ecdsa384-private.pem (100%) rename Jwt/tests/{ => data}/ecdsa384-public.pem (100%) rename Jwt/tests/{ => data}/ed25519-1.pub (100%) rename Jwt/tests/{ => data}/ed25519-1.sec (100%) rename Jwt/tests/{ => data}/rsa-jwkset.json (86%) rename Jwt/tests/{ => data}/rsa-with-passphrase.pem (100%) rename Jwt/tests/{ => data}/rsa1-private.pem (100%) rename Jwt/tests/{ => data}/rsa1-public.pub (100%) rename Jwt/tests/{ => data}/rsa2-private.pem (100%) diff --git a/Jwt/.gitignore b/Jwt/.gitignore index 080f19aa729..b22842cb5b1 100644 --- a/Jwt/.gitignore +++ b/Jwt/.gitignore @@ -3,3 +3,4 @@ phpunit.phar phpunit.phar.asc composer.phar composer.lock +.phpunit.result.cache diff --git a/Jwt/README.md b/Jwt/README.md index acd1720c355..0a0023b2304 100644 --- a/Jwt/README.md +++ b/Jwt/README.md @@ -200,8 +200,7 @@ $jwks = ['keys' => []]; // JWK::parseKeySet($jwks) returns an associative array of **kid** to private // key. Pass this as the second parameter to JWT::decode. -// NOTE: The deprecated $supportedAlgorithm must be supplied when parsing from JWK. -JWT::decode($payload, JWK::parseKeySet($jwks), $supportedAlgorithm); +JWT::decode($payload, JWK::parseKeySet($jwks)); ``` Changelog diff --git a/Jwt/src/JWK.php b/Jwt/src/JWK.php index 981a9ba7f50..c53251d39d0 100644 --- a/Jwt/src/JWK.php +++ b/Jwt/src/JWK.php @@ -47,7 +47,15 @@ public static function parseKeySet(array $jwks) foreach ($jwks['keys'] as $k => $v) { $kid = isset($v['kid']) ? $v['kid'] : $k; if ($key = self::parseKey($v)) { - $keys[$kid] = $key; + if (isset($v['alg'])) { + $keys[$kid] = new Key($key, $v['alg']); + } else { + // The "alg" parameter is optional in a KTY, but is required + // for parsing in this library. Add it manually to your JWK + // array if it doesn't already exist. + // @see https://datatracker.ietf.org/doc/html/rfc7517#section-4.4 + throw new InvalidArgumentException('JWK key is missing "alg"'); + } } } diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index b2e78041ef2..e6038648e01 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -25,9 +25,12 @@ */ class JWT { - const ASN1_INTEGER = 0x02; - const ASN1_SEQUENCE = 0x10; - const ASN1_BIT_STRING = 0x03; + // const ASN1_INTEGER = 0x02; + // const ASN1_SEQUENCE = 0x10; + // const ASN1_BIT_STRING = 0x03; + private static $asn1Integer = 0x02; + private static $asn1Sequence = 0x10; + private static $asn1BitString = 0x03; /** * When checking nbf, iat or expiration times, @@ -60,13 +63,11 @@ class JWT * Decodes a JWT string into a PHP object. * * @param string $jwt The JWT - * @param Key|array|mixed $keyOrKeyArray The Key or array of Key objects. + * @param Key|array $keyOrKeyArray The Key or array of Key objects. * If the algorithm used is asymmetric, this is the public key * Each Key object contains an algorithm and matching key. * Supported algorithms are 'ES384','ES256', 'HS256', 'HS384', * 'HS512', 'RS256', 'RS384', and 'RS512' - * @param array $allowed_algs [DEPRECATED] List of supported verification algorithms. Only - * should be used for backwards compatibility. * * @return object The JWT's payload as a PHP object * @@ -81,8 +82,9 @@ class JWT * @uses jsonDecode * @uses urlsafeB64Decode */ - public static function decode($jwt, $keyOrKeyArray, array $allowed_algs = array()) + public static function decode($jwt, $keyOrKeyArray) { + // Validate JWT $timestamp = \is_null(static::$timestamp) ? \time() : static::$timestamp; if (empty($keyOrKeyArray)) { @@ -109,31 +111,18 @@ public static function decode($jwt, $keyOrKeyArray, array $allowed_algs = array( throw new UnexpectedValueException('Algorithm not supported'); } - list($keyMaterial, $algorithm) = self::getKeyMaterialAndAlgorithm( - $keyOrKeyArray, - empty($header->kid) ? null : $header->kid - ); + $key = self::getKey($keyOrKeyArray, empty($header->kid) ? null : $header->kid); - if (empty($algorithm)) { - // Use deprecated "allowed_algs" to determine if the algorithm is supported. - // This opens up the possibility of an attack in some implementations. - // @see https://github.com/firebase/php-jwt/issues/351 - if (!\in_array($header->alg, $allowed_algs)) { - throw new UnexpectedValueException('Algorithm not allowed'); - } - } else { - // Check the algorithm - if (!self::constantTimeEquals($algorithm, $header->alg)) { - // See issue #351 - throw new UnexpectedValueException('Incorrect key for this algorithm'); - } + // Check the algorithm + if (!self::constantTimeEquals($key->getAlgorithm(), $header->alg)) { + // See issue #351 + throw new UnexpectedValueException('Incorrect key for this algorithm'); } if ($header->alg === 'ES256' || $header->alg === 'ES384') { // OpenSSL expects an ASN.1 DER sequence for ES256/ES384 signatures $sig = self::signatureToDER($sig); } - - if (!static::verify("$headb64.$bodyb64", $sig, $keyMaterial, $header->alg)) { + if (!static::verify("$headb64.$bodyb64", $sig, $key->getKeyMaterial(), $header->alg)) { throw new SignatureInvalidException('Signature verification failed'); } @@ -179,7 +168,7 @@ public static function decode($jwt, $keyOrKeyArray, array $allowed_algs = array( * @uses jsonEncode * @uses urlsafeB64Encode */ - public static function encode($payload, $key, $alg = 'HS256', $keyId = null, $head = null) + public static function encode($payload, $key, $alg, $keyId = null, $head = null) { $header = array('typ' => 'JWT', 'alg' => $alg); if ($keyId !== null) { @@ -212,7 +201,7 @@ public static function encode($payload, $key, $alg = 'HS256', $keyId = null, $he * * @throws DomainException Unsupported algorithm or bad key was specified */ - public static function sign($msg, $key, $alg = 'HS256') + public static function sign($msg, $key, $alg) { if (empty(static::$supported_algs[$alg])) { throw new DomainException('Algorithm not supported'); @@ -345,7 +334,12 @@ public static function jsonDecode($input) */ public static function jsonEncode($input) { - $json = \json_encode($input); + if (PHP_VERSION_ID >= 50400) { + $json = \json_encode($input, \JSON_UNESCAPED_SLASHES); + } else { + // PHP 5.3 only + $json = \json_encode($input); + } if ($errno = \json_last_error()) { static::handleJsonError($errno); } elseif ($json === 'null' && $input !== null) { @@ -394,21 +388,21 @@ public static function urlsafeB64Encode($input) * * @return array containing the keyMaterial and algorithm */ - private static function getKeyMaterialAndAlgorithm($keyOrKeyArray, $kid = null) + private static function getKey($keyOrKeyArray, $kid = null) { - if ( - is_string($keyOrKeyArray) - || is_resource($keyOrKeyArray) - || $keyOrKeyArray instanceof OpenSSLAsymmetricKey - ) { - return array($keyOrKeyArray, null); - } - if ($keyOrKeyArray instanceof Key) { - return array($keyOrKeyArray->getKeyMaterial(), $keyOrKeyArray->getAlgorithm()); + return $keyOrKeyArray; } if (is_array($keyOrKeyArray) || $keyOrKeyArray instanceof ArrayAccess) { + foreach ($keyOrKeyArray as $keyId => $key) { + if (!$key instanceof Key) { + throw new UnexpectedValueException( + '$keyOrKeyArray must be an instance of Firebase\JWT\Key key or an ' + . 'array of Firebase\JWT\Key keys' + ); + } + } if (!isset($kid)) { throw new UnexpectedValueException('"kid" empty, unable to lookup correct key'); } @@ -416,18 +410,12 @@ private static function getKeyMaterialAndAlgorithm($keyOrKeyArray, $kid = null) throw new UnexpectedValueException('"kid" invalid, unable to lookup correct key'); } - $key = $keyOrKeyArray[$kid]; - - if ($key instanceof Key) { - return array($key->getKeyMaterial(), $key->getAlgorithm()); - } - - return array($key, null); + return $keyOrKeyArray[$kid]; } throw new UnexpectedValueException( - '$keyOrKeyArray must be a string|resource key, an array of string|resource keys, ' - . 'an instance of Firebase\JWT\Key key or an array of Firebase\JWT\Key keys' + '$keyOrKeyArray must be an instance of Firebase\JWT\Key key or an ' + . 'array of Firebase\JWT\Key keys' ); } @@ -515,9 +503,9 @@ private static function signatureToDER($sig) } return self::encodeDER( - self::ASN1_SEQUENCE, - self::encodeDER(self::ASN1_INTEGER, $r) . - self::encodeDER(self::ASN1_INTEGER, $s) + self::$asn1Sequence, + self::encodeDER(self::$asn1Integer, $r) . + self::encodeDER(self::$asn1Integer, $s) ); } @@ -531,7 +519,7 @@ private static function signatureToDER($sig) private static function encodeDER($type, $value) { $tag_header = 0; - if ($type === self::ASN1_SEQUENCE) { + if ($type === self::$asn1Sequence) { $tag_header |= 0x20; } @@ -596,7 +584,7 @@ private static function readDER($der, $offset = 0) } // Value - if ($type == self::ASN1_BIT_STRING) { + if ($type == self::$asn1BitString) { $pos++; // Skip the first contents octet (padding indicator) $data = \substr($der, $pos, $len - 1); $pos += $len - 1; diff --git a/Jwt/tests/JWKTest.php b/Jwt/tests/JWKTest.php index 0709836d403..b908ea6496f 100644 --- a/Jwt/tests/JWKTest.php +++ b/Jwt/tests/JWKTest.php @@ -38,26 +38,42 @@ public function testParsePrivateKey() 'UnexpectedValueException', 'RSA private keys are not supported' ); - + $jwkSet = json_decode( - file_get_contents(__DIR__ . '/rsa-jwkset.json'), + file_get_contents(__DIR__ . '/data/rsa-jwkset.json'), true ); $jwkSet['keys'][0]['d'] = 'privatekeyvalue'; - + + JWK::parseKeySet($jwkSet); + } + + public function testParsePrivateKeyWithoutAlg() + { + $this->setExpectedException( + 'InvalidArgumentException', + 'JWK key is missing "alg"' + ); + + $jwkSet = json_decode( + file_get_contents(__DIR__ . '/data/rsa-jwkset.json'), + true + ); + unset($jwkSet['keys'][0]['alg']); + JWK::parseKeySet($jwkSet); } - + public function testParseKeyWithEmptyDValue() { $jwkSet = json_decode( - file_get_contents(__DIR__ . '/rsa-jwkset.json'), + file_get_contents(__DIR__ . '/data/rsa-jwkset.json'), true ); - + // empty or null values are ok $jwkSet['keys'][0]['d'] = null; - + $keys = JWK::parseKeySet($jwkSet); $this->assertTrue(is_array($keys)); } @@ -65,7 +81,7 @@ public function testParseKeyWithEmptyDValue() public function testParseJwkKeySet() { $jwkSet = json_decode( - file_get_contents(__DIR__ . '/rsa-jwkset.json'), + file_get_contents(__DIR__ . '/data/rsa-jwkset.json'), true ); $keys = JWK::parseKeySet($jwkSet); @@ -93,7 +109,7 @@ public function testParseJwkKeySet_empty() */ public function testDecodeByJwkKeySetTokenExpired() { - $privKey1 = file_get_contents(__DIR__ . '/rsa1-private.pem'); + $privKey1 = file_get_contents(__DIR__ . '/data/rsa1-private.pem'); $payload = array('exp' => strtotime('-1 hour')); $msg = JWT::encode($payload, $privKey1, 'RS256', 'jwk1'); @@ -107,7 +123,7 @@ public function testDecodeByJwkKeySetTokenExpired() */ public function testDecodeByJwkKeySet() { - $privKey1 = file_get_contents(__DIR__ . '/rsa1-private.pem'); + $privKey1 = file_get_contents(__DIR__ . '/data/rsa1-private.pem'); $payload = array('sub' => 'foo', 'exp' => strtotime('+10 seconds')); $msg = JWT::encode($payload, $privKey1, 'RS256', 'jwk1'); @@ -121,7 +137,7 @@ public function testDecodeByJwkKeySet() */ public function testDecodeByMultiJwkKeySet() { - $privKey2 = file_get_contents(__DIR__ . '/rsa2-private.pem'); + $privKey2 = file_get_contents(__DIR__ . '/data/rsa2-private.pem'); $payload = array('sub' => 'bar', 'exp' => strtotime('+10 seconds')); $msg = JWT::encode($payload, $privKey2, 'RS256', 'jwk2'); diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index 1c81c1eddf1..36e2095e91d 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -23,21 +23,21 @@ public function testDecodeFromPython() { $msg = 'eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.Iio6aHR0cDovL2FwcGxpY2F0aW9uL2NsaWNreT9ibGFoPTEuMjMmZi5vbz00NTYgQUMwMDAgMTIzIg.E_U8X2YpMT5K1cEiT_3-IvBYfrdIFIeVYeOqre_Z5Cg'; $this->assertEquals( - JWT::decode($msg, 'my_key', array('HS256')), + JWT::decode($msg, new Key('my_key', 'HS256')), '*:http://application/clicky?blah=1.23&f.oo=456 AC000 123' ); } public function testUrlSafeCharacters() { - $encoded = JWT::encode('f?', 'a'); - $this->assertEquals('f?', JWT::decode($encoded, 'a', array('HS256'))); + $encoded = JWT::encode('f?', 'a', 'HS256'); + $this->assertEquals('f?', JWT::decode($encoded, new Key('a', 'HS256'))); } public function testMalformedUtf8StringsFail() { $this->setExpectedException('DomainException'); - JWT::encode(pack('c', 128), 'a'); + JWT::encode(pack('c', 128), 'a', 'HS256'); } public function testMalformedJsonThrowsException() @@ -52,8 +52,8 @@ public function testExpiredToken() $payload = array( "message" => "abc", "exp" => time() - 20); // time in the past - $encoded = JWT::encode($payload, 'my_key'); - JWT::decode($encoded, 'my_key', array('HS256')); + $encoded = JWT::encode($payload, 'my_key', 'HS256'); + JWT::decode($encoded, new Key('my_key', 'HS256')); } public function testBeforeValidTokenWithNbf() @@ -62,8 +62,8 @@ public function testBeforeValidTokenWithNbf() $payload = array( "message" => "abc", "nbf" => time() + 20); // time in the future - $encoded = JWT::encode($payload, 'my_key'); - JWT::decode($encoded, 'my_key', array('HS256')); + $encoded = JWT::encode($payload, 'my_key', 'HS256'); + JWT::decode($encoded, new Key('my_key', 'HS256')); } public function testBeforeValidTokenWithIat() @@ -72,8 +72,8 @@ public function testBeforeValidTokenWithIat() $payload = array( "message" => "abc", "iat" => time() + 20); // time in the future - $encoded = JWT::encode($payload, 'my_key'); - JWT::decode($encoded, 'my_key', array('HS256')); + $encoded = JWT::encode($payload, 'my_key', 'HS256'); + JWT::decode($encoded, new Key('my_key', 'HS256')); } public function testValidToken() @@ -81,8 +81,8 @@ public function testValidToken() $payload = array( "message" => "abc", "exp" => time() + JWT::$leeway + 20); // time in the future - $encoded = JWT::encode($payload, 'my_key'); - $decoded = JWT::decode($encoded, 'my_key', array('HS256')); + $encoded = JWT::encode($payload, 'my_key', 'HS256'); + $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); $this->assertEquals($decoded->message, 'abc'); } @@ -92,8 +92,8 @@ public function testValidTokenWithLeeway() $payload = array( "message" => "abc", "exp" => time() - 20); // time in the past - $encoded = JWT::encode($payload, 'my_key'); - $decoded = JWT::decode($encoded, 'my_key', array('HS256')); + $encoded = JWT::encode($payload, 'my_key', 'HS256'); + $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); $this->assertEquals($decoded->message, 'abc'); JWT::$leeway = 0; } @@ -105,22 +105,12 @@ public function testExpiredTokenWithLeeway() "message" => "abc", "exp" => time() - 70); // time far in the past $this->setExpectedException('Firebase\JWT\ExpiredException'); - $encoded = JWT::encode($payload, 'my_key'); - $decoded = JWT::decode($encoded, 'my_key', array('HS256')); + $encoded = JWT::encode($payload, 'my_key', 'HS256'); + $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); $this->assertEquals($decoded->message, 'abc'); JWT::$leeway = 0; } - public function testValidTokenWithList() - { - $payload = array( - "message" => "abc", - "exp" => time() + 20); // time in the future - $encoded = JWT::encode($payload, 'my_key'); - $decoded = JWT::decode($encoded, 'my_key', array('HS256', 'HS512')); - $this->assertEquals($decoded->message, 'abc'); - } - public function testValidTokenWithNbf() { $payload = array( @@ -128,8 +118,8 @@ public function testValidTokenWithNbf() "iat" => time(), "exp" => time() + 20, // time in the future "nbf" => time() - 20); - $encoded = JWT::encode($payload, 'my_key'); - $decoded = JWT::decode($encoded, 'my_key', array('HS256')); + $encoded = JWT::encode($payload, 'my_key', 'HS256'); + $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); $this->assertEquals($decoded->message, 'abc'); } @@ -139,8 +129,8 @@ public function testValidTokenWithNbfLeeway() $payload = array( "message" => "abc", "nbf" => time() + 20); // not before in near (leeway) future - $encoded = JWT::encode($payload, 'my_key'); - $decoded = JWT::decode($encoded, 'my_key', array('HS256')); + $encoded = JWT::encode($payload, 'my_key', 'HS256'); + $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); $this->assertEquals($decoded->message, 'abc'); JWT::$leeway = 0; } @@ -151,9 +141,9 @@ public function testInvalidTokenWithNbfLeeway() $payload = array( "message" => "abc", "nbf" => time() + 65); // not before too far in future - $encoded = JWT::encode($payload, 'my_key'); + $encoded = JWT::encode($payload, 'my_key', 'HS256'); $this->setExpectedException('Firebase\JWT\BeforeValidException'); - JWT::decode($encoded, 'my_key', array('HS256')); + JWT::decode($encoded, new Key('my_key', 'HS256')); JWT::$leeway = 0; } @@ -163,8 +153,8 @@ public function testValidTokenWithIatLeeway() $payload = array( "message" => "abc", "iat" => time() + 20); // issued in near (leeway) future - $encoded = JWT::encode($payload, 'my_key'); - $decoded = JWT::decode($encoded, 'my_key', array('HS256')); + $encoded = JWT::encode($payload, 'my_key', 'HS256'); + $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); $this->assertEquals($decoded->message, 'abc'); JWT::$leeway = 0; } @@ -175,9 +165,9 @@ public function testInvalidTokenWithIatLeeway() $payload = array( "message" => "abc", "iat" => time() + 65); // issued too far in future - $encoded = JWT::encode($payload, 'my_key'); + $encoded = JWT::encode($payload, 'my_key', 'HS256'); $this->setExpectedException('Firebase\JWT\BeforeValidException'); - JWT::decode($encoded, 'my_key', array('HS256')); + JWT::decode($encoded, new Key('my_key', 'HS256')); JWT::$leeway = 0; } @@ -186,9 +176,9 @@ public function testInvalidToken() $payload = array( "message" => "abc", "exp" => time() + 20); // time in the future - $encoded = JWT::encode($payload, 'my_key'); + $encoded = JWT::encode($payload, 'my_key', 'HS256'); $this->setExpectedException('Firebase\JWT\SignatureInvalidException'); - JWT::decode($encoded, 'my_key2', array('HS256')); + JWT::decode($encoded, new Key('my_key2', 'HS256')); } public function testNullKeyFails() @@ -196,9 +186,9 @@ public function testNullKeyFails() $payload = array( "message" => "abc", "exp" => time() + JWT::$leeway + 20); // time in the future - $encoded = JWT::encode($payload, 'my_key'); + $encoded = JWT::encode($payload, 'my_key', 'HS256'); $this->setExpectedException('InvalidArgumentException'); - JWT::decode($encoded, null, array('HS256')); + JWT::decode($encoded, new Key(null, 'HS256')); } public function testEmptyKeyFails() @@ -206,71 +196,77 @@ public function testEmptyKeyFails() $payload = array( "message" => "abc", "exp" => time() + JWT::$leeway + 20); // time in the future - $encoded = JWT::encode($payload, 'my_key'); + $encoded = JWT::encode($payload, 'my_key', 'HS256'); $this->setExpectedException('InvalidArgumentException'); - JWT::decode($encoded, '', array('HS256')); + JWT::decode($encoded, new Key('', 'HS256')); } public function testKIDChooser() { - $keys = array('1' => 'my_key', '2' => 'my_key2'); - $msg = JWT::encode('abc', $keys['1'], 'HS256', '1'); - $decoded = JWT::decode($msg, $keys, array('HS256')); + $keys = array( + '1' => new Key('my_key', 'HS256'), + '2' => new Key('my_key2', 'HS256') + ); + $msg = JWT::encode('abc', $keys['1']->getKeyMaterial(), 'HS256', '1'); + $decoded = JWT::decode($msg, $keys); $this->assertEquals($decoded, 'abc'); } public function testArrayAccessKIDChooser() { - $keys = new ArrayObject(array('1' => 'my_key', '2' => 'my_key2')); - $msg = JWT::encode('abc', $keys['1'], 'HS256', '1'); - $decoded = JWT::decode($msg, $keys, array('HS256')); + $keys = new ArrayObject(array( + '1' => new Key('my_key', 'HS256'), + '2' => new Key('my_key2', 'HS256'), + )); + $msg = JWT::encode('abc', $keys['1']->getKeyMaterial(), 'HS256', '1'); + $decoded = JWT::decode($msg, $keys); $this->assertEquals($decoded, 'abc'); } public function testNoneAlgorithm() { - $msg = JWT::encode('abc', 'my_key'); + $msg = JWT::encode('abc', 'my_key', 'HS256'); $this->setExpectedException('UnexpectedValueException'); - JWT::decode($msg, 'my_key', array('none')); + JWT::decode($msg, new Key('my_key', 'none')); } public function testIncorrectAlgorithm() { - $msg = JWT::encode('abc', 'my_key'); + $msg = JWT::encode('abc', 'my_key', 'HS256'); $this->setExpectedException('UnexpectedValueException'); - JWT::decode($msg, 'my_key', array('RS256')); + JWT::decode($msg, new Key('my_key', 'RS256')); } - public function testMissingAlgorithm() + public function testEmptyAlgorithm() { - $msg = JWT::encode('abc', 'my_key'); + $msg = JWT::encode('abc', 'my_key', 'HS256'); $this->setExpectedException('UnexpectedValueException'); - JWT::decode($msg, 'my_key'); + JWT::decode($msg, new Key('my_key', '')); } public function testAdditionalHeaders() { $msg = JWT::encode('abc', 'my_key', 'HS256', null, array('cty' => 'test-eit;v=1')); - $this->assertEquals(JWT::decode($msg, 'my_key', array('HS256')), 'abc'); + $this->assertEquals(JWT::decode($msg, new Key('my_key', 'HS256')), 'abc'); } public function testInvalidSegmentCount() { $this->setExpectedException('UnexpectedValueException'); - JWT::decode('brokenheader.brokenbody', 'my_key', array('HS256')); + JWT::decode('brokenheader.brokenbody', new Key('my_key', 'HS256')); } public function testInvalidSignatureEncoding() { $msg = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwibmFtZSI6ImZvbyJ9.Q4Kee9E8o0Xfo4ADXvYA8t7dN_X_bU9K5w6tXuiSjlUxx"; $this->setExpectedException('UnexpectedValueException'); - JWT::decode($msg, 'secret', array('HS256')); + JWT::decode($msg, new Key('secret', 'HS256')); } public function testHSEncodeDecode() { - $msg = JWT::encode('abc', 'my_key'); - $this->assertEquals(JWT::decode($msg, 'my_key', array('HS256')), 'abc'); + $msg = JWT::encode('abc', 'my_key', 'HS256'); + $this->assertEquals(JWT::decode($msg, new Key('my_key', 'HS256')), 'abc'); } public function testRSEncodeDecode() @@ -281,7 +277,7 @@ public function testRSEncodeDecode() $msg = JWT::encode('abc', $privKey, 'RS256'); $pubKey = openssl_pkey_get_details($privKey); $pubKey = $pubKey['key']; - $decoded = JWT::decode($msg, $pubKey, array('RS256')); + $decoded = JWT::decode($msg, new Key($pubKey, 'RS256')); $this->assertEquals($decoded, 'abc'); } @@ -294,7 +290,7 @@ public function testEdDsaEncodeDecode() $msg = JWT::encode($payload, $privKey, 'EdDSA'); $pubKey = base64_encode(sodium_crypto_sign_publickey($keyPair)); - $decoded = JWT::decode($msg, $pubKey, array('EdDSA')); + $decoded = JWT::decode($msg, new Key($pubKey, 'EdDSA')); $this->assertEquals('bar', $decoded->foo); } @@ -310,20 +306,20 @@ public function testInvalidEdDsaEncodeDecode() $keyPair = sodium_crypto_sign_keypair(); $pubKey = base64_encode(sodium_crypto_sign_publickey($keyPair)); $this->setExpectedException('Firebase\JWT\SignatureInvalidException'); - JWT::decode($msg, $pubKey, array('EdDSA')); + JWT::decode($msg, new Key($pubKey, 'EdDSA')); } public function testRSEncodeDecodeWithPassphrase() { $privateKey = openssl_pkey_get_private( - file_get_contents(__DIR__ . '/rsa-with-passphrase.pem'), + file_get_contents(__DIR__ . '/data/rsa-with-passphrase.pem'), 'passphrase' ); $jwt = JWT::encode('abc', $privateKey, 'RS256'); $keyDetails = openssl_pkey_get_details($privateKey); $pubKey = $keyDetails['key']; - $decoded = JWT::decode($jwt, $pubKey, array('RS256')); + $decoded = JWT::decode($jwt, new Key($pubKey, 'RS256')); $this->assertEquals($decoded, 'abc'); } @@ -337,23 +333,6 @@ public function testEncodeDecode($privateKeyFile, $publicKeyFile, $alg) $payload = array('foo' => 'bar'); $encoded = JWT::encode($payload, $privateKey, $alg); - // Verify decoding succeeds - $publicKey = file_get_contents($publicKeyFile); - $decoded = JWT::decode($encoded, $publicKey, array($alg)); - - $this->assertEquals('bar', $decoded->foo); - } - - /** - * @runInSeparateProcess - * @dataProvider provideEncodeDecode - */ - public function testEncodeDecodeWithKeyObject($privateKeyFile, $publicKeyFile, $alg) - { - $privateKey = file_get_contents($privateKeyFile); - $payload = array('foo' => 'bar'); - $encoded = JWT::encode($payload, $privateKey, $alg); - // Verify decoding succeeds $publicKey = file_get_contents($publicKeyFile); $decoded = JWT::decode($encoded, new Key($publicKey, $alg)); @@ -361,38 +340,27 @@ public function testEncodeDecodeWithKeyObject($privateKeyFile, $publicKeyFile, $ $this->assertEquals('bar', $decoded->foo); } - public function testArrayAccessKIDChooserWithKeyObject() - { - $keys = new ArrayObject(array( - '1' => new Key('my_key', 'HS256'), - '2' => new Key('my_key2', 'HS256'), - )); - $msg = JWT::encode('abc', $keys['1']->getKeyMaterial(), 'HS256', '1'); - $decoded = JWT::decode($msg, $keys); - $this->assertEquals($decoded, 'abc'); - } - public function provideEncodeDecode() { return array( - array(__DIR__ . '/ecdsa-private.pem', __DIR__ . '/ecdsa-public.pem', 'ES256'), - array(__DIR__ . '/ecdsa384-private.pem', __DIR__ . '/ecdsa384-public.pem', 'ES384'), - array(__DIR__ . '/rsa1-private.pem', __DIR__ . '/rsa1-public.pub', 'RS512'), - array(__DIR__ . '/ed25519-1.sec', __DIR__ . '/ed25519-1.pub', 'EdDSA'), + array(__DIR__ . '/data/ecdsa-private.pem', __DIR__ . '/data/ecdsa-public.pem', 'ES256'), + array(__DIR__ . '/data/ecdsa384-private.pem', __DIR__ . '/data/ecdsa384-public.pem', 'ES384'), + array(__DIR__ . '/data/rsa1-private.pem', __DIR__ . '/data/rsa1-public.pub', 'RS512'), + array(__DIR__ . '/data/ed25519-1.sec', __DIR__ . '/data/ed25519-1.pub', 'EdDSA'), ); } public function testEncodeDecodeWithResource() { - $pem = file_get_contents(__DIR__ . '/rsa1-public.pub'); + $pem = file_get_contents(__DIR__ . '/data/rsa1-public.pub'); $resource = openssl_pkey_get_public($pem); - $privateKey = file_get_contents(__DIR__ . '/rsa1-private.pem'); + $privateKey = file_get_contents(__DIR__ . '/data/rsa1-private.pem'); $payload = array('foo' => 'bar'); $encoded = JWT::encode($payload, $privateKey, 'RS512'); // Verify decoding succeeds - $decoded = JWT::decode($encoded, $resource, array('RS512')); + $decoded = JWT::decode($encoded, new Key($resource, 'RS512')); $this->assertEquals('bar', $decoded->foo); } diff --git a/Jwt/tests/autoload.php.dist b/Jwt/tests/autoload.php.dist deleted file mode 100644 index 2e4310a0bb0..00000000000 --- a/Jwt/tests/autoload.php.dist +++ /dev/null @@ -1,17 +0,0 @@ - Date: Mon, 24 Jan 2022 06:32:49 -0800 Subject: [PATCH 162/282] chore: update changelog for v6.0.0 (firebase/php-jwt#391) --- Jwt/README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Jwt/README.md b/Jwt/README.md index 0a0023b2304..26e0436b72e 100644 --- a/Jwt/README.md +++ b/Jwt/README.md @@ -206,6 +206,14 @@ JWT::decode($payload, JWK::parseKeySet($jwks)); Changelog --------- +#### 6.0.0 / 2022-01-24 + + - New Key object to prevent key/algorithm type confusion (#365) + - Add JWK support (#273) + - Add ES256 support (#256) + - Add ES384 support (#324) + - Add Ed25519 support (#343) + #### 5.0.0 / 2017-06-26 - Support RS384 and RS512. See [#117](https://github.com/firebase/php-jwt/pull/117). Thanks [@joostfaassen](https://github.com/joostfaassen)! From 65b67397a0eeec7cee20bb6c42b61d43bc8ab10a Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Mon, 24 Jan 2022 07:18:34 -0800 Subject: [PATCH 163/282] feat!: update return type for JWK methods (firebase/php-jwt#392) --- Jwt/README.md | 5 +++-- Jwt/src/JWK.php | 22 ++++++++++------------ Jwt/src/JWT.php | 6 +++--- Jwt/tests/JWKTest.php | 6 +++--- 4 files changed, 19 insertions(+), 20 deletions(-) diff --git a/Jwt/README.md b/Jwt/README.md index 26e0436b72e..7839af60127 100644 --- a/Jwt/README.md +++ b/Jwt/README.md @@ -198,8 +198,8 @@ use Firebase\JWT\JWT; // this endpoint: https://www.gstatic.com/iap/verify/public_key-jwk $jwks = ['keys' => []]; -// JWK::parseKeySet($jwks) returns an associative array of **kid** to private -// key. Pass this as the second parameter to JWT::decode. +// JWK::parseKeySet($jwks) returns an associative array of **kid** to Firebase\JWT\Key +// objects. Pass this as the second parameter to JWT::decode. JWT::decode($payload, JWK::parseKeySet($jwks)); ``` @@ -208,6 +208,7 @@ Changelog #### 6.0.0 / 2022-01-24 + - **Backwards-Compatibility Breaking Changes**: See the [Release Notes](https://github.com/firebase/php-jwt/releases/tag/v5.5.1) for more information. - New Key object to prevent key/algorithm type confusion (#365) - Add JWK support (#273) - Add ES256 support (#256) diff --git a/Jwt/src/JWK.php b/Jwt/src/JWK.php index c53251d39d0..c5506548dd0 100644 --- a/Jwt/src/JWK.php +++ b/Jwt/src/JWK.php @@ -25,7 +25,7 @@ class JWK * * @param array $jwks The JSON Web Key Set as an associative array * - * @return array An associative array that represents the set of keys + * @return array An associative array of key IDs (kid) to Key objects * * @throws InvalidArgumentException Provided JWK Set is empty * @throws UnexpectedValueException Provided JWK Set was invalid @@ -47,15 +47,7 @@ public static function parseKeySet(array $jwks) foreach ($jwks['keys'] as $k => $v) { $kid = isset($v['kid']) ? $v['kid'] : $k; if ($key = self::parseKey($v)) { - if (isset($v['alg'])) { - $keys[$kid] = new Key($key, $v['alg']); - } else { - // The "alg" parameter is optional in a KTY, but is required - // for parsing in this library. Add it manually to your JWK - // array if it doesn't already exist. - // @see https://datatracker.ietf.org/doc/html/rfc7517#section-4.4 - throw new InvalidArgumentException('JWK key is missing "alg"'); - } + $keys[$kid] = $key; } } @@ -71,7 +63,7 @@ public static function parseKeySet(array $jwks) * * @param array $jwk An individual JWK * - * @return resource|array An associative array that represents the key + * @return Key The key object for the JWK * * @throws InvalidArgumentException Provided JWK is empty * @throws UnexpectedValueException Provided JWK was invalid @@ -87,6 +79,12 @@ public static function parseKey(array $jwk) if (!isset($jwk['kty'])) { throw new UnexpectedValueException('JWK must contain a "kty" parameter'); } + if (!isset($jwk['alg'])) { + // The "alg" parameter is optional in a KTY, but is required for parsing in + // this library. Add it manually to your JWK array if it doesn't already exist. + // @see https://datatracker.ietf.org/doc/html/rfc7517#section-4.4 + throw new UnexpectedValueException('JWK must contain an "alg" parameter'); + } switch ($jwk['kty']) { case 'RSA': @@ -104,7 +102,7 @@ public static function parseKey(array $jwk) 'OpenSSL error: ' . \openssl_error_string() ); } - return $publicKey; + return new Key($publicKey, $jwk['alg']); default: // Currently only RSA is supported break; diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index e6038648e01..725a0832e5e 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -63,7 +63,7 @@ class JWT * Decodes a JWT string into a PHP object. * * @param string $jwt The JWT - * @param Key|array $keyOrKeyArray The Key or array of Key objects. + * @param Key|array $keyOrKeyArray The Key or associative array of key IDs (kid) to Key objects. * If the algorithm used is asymmetric, this is the public key * Each Key object contains an algorithm and matching key. * Supported algorithms are 'ES384','ES256', 'HS256', 'HS384', @@ -381,8 +381,8 @@ public static function urlsafeB64Encode($input) /** * Determine if an algorithm has been provided for each Key * - * @param Key|array|mixed $keyOrKeyArray - * @param string|null $kid + * @param Key|array $keyOrKeyArray + * @param string|null $kid * * @throws UnexpectedValueException * diff --git a/Jwt/tests/JWKTest.php b/Jwt/tests/JWKTest.php index b908ea6496f..c580f40f2bb 100644 --- a/Jwt/tests/JWKTest.php +++ b/Jwt/tests/JWKTest.php @@ -28,7 +28,7 @@ public function testInvalidAlgorithm() 'No supported algorithms found in JWK Set' ); - $badJwk = array('kty' => 'BADALG'); + $badJwk = array('kty' => 'BADALG', 'alg' => 'RSA256'); $keys = JWK::parseKeySet(array('keys' => array($badJwk))); } @@ -51,8 +51,8 @@ public function testParsePrivateKey() public function testParsePrivateKeyWithoutAlg() { $this->setExpectedException( - 'InvalidArgumentException', - 'JWK key is missing "alg"' + 'UnexpectedValueException', + 'JWK must contain an "alg" parameter' ); $jwkSet = json_decode( From 7fdfb5cfdb830d89707aca79e093d107a82b7665 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Mon, 7 Feb 2022 06:54:21 -0800 Subject: [PATCH 164/282] fix: correct order for hash_equals (firebase/php-jwt#393) --- Jwt/src/JWT.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index 725a0832e5e..1226102c8b7 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -284,7 +284,7 @@ private static function verify($msg, $signature, $key, $alg) case 'hash_hmac': default: $hash = \hash_hmac($algorithm, $msg, $key, true); - return self::constantTimeEquals($signature, $hash); + return self::constantTimeEquals($hash, $signature); } } @@ -420,8 +420,8 @@ private static function getKey($keyOrKeyArray, $kid = null) } /** - * @param string $left - * @param string $right + * @param string $left The string of known length to compare against + * @param string $right The user-supplied string * @return bool */ public static function constantTimeEquals($left, $right) From 63c3d3f4dc8839696234a615d91df4063518aba0 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Mon, 7 Feb 2022 07:04:10 -0800 Subject: [PATCH 165/282] fix: use property_exists for checking key id (firebase/php-jwt#395) --- Jwt/src/JWT.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index 1226102c8b7..6130c59c614 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -111,7 +111,7 @@ public static function decode($jwt, $keyOrKeyArray) throw new UnexpectedValueException('Algorithm not supported'); } - $key = self::getKey($keyOrKeyArray, empty($header->kid) ? null : $header->kid); + $key = self::getKey($keyOrKeyArray, property_exists($header, 'kid') ? $header->kid : null); // Check the algorithm if (!self::constantTimeEquals($key->getAlgorithm(), $header->alg)) { From 301e9ff3c75eb3909da7cbe913280d1fa7995f46 Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 7 Feb 2022 17:45:09 +0200 Subject: [PATCH 166/282] chore(docs): fix release URL in README (firebase/php-jwt#394) --- Jwt/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jwt/README.md b/Jwt/README.md index 7839af60127..2300ae309fa 100644 --- a/Jwt/README.md +++ b/Jwt/README.md @@ -208,7 +208,7 @@ Changelog #### 6.0.0 / 2022-01-24 - - **Backwards-Compatibility Breaking Changes**: See the [Release Notes](https://github.com/firebase/php-jwt/releases/tag/v5.5.1) for more information. + - **Backwards-Compatibility Breaking Changes**: See the [Release Notes](https://github.com/firebase/php-jwt/releases/tag/v6.0.0) for more information. - New Key object to prevent key/algorithm type confusion (#365) - Add JWK support (#273) - Add ES256 support (#256) From 6d3627b2615587dd28f02c28b8ed57c82efc81f2 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 16 Feb 2022 06:14:33 -0800 Subject: [PATCH 167/282] feat: add typing, require PHP 8.0 (firebase/php-jwt#385) --- Jwt/.github/workflows/tests.yml | 40 +------ Jwt/composer.json | 4 +- Jwt/src/JWK.php | 15 ++- Jwt/src/JWT.php | 197 +++++++++++++++----------------- Jwt/src/Key.php | 35 +++--- Jwt/tests/JWKTest.php | 40 ++++--- Jwt/tests/JWTTest.php | 185 ++++++++++++++++-------------- 7 files changed, 239 insertions(+), 277 deletions(-) diff --git a/Jwt/.github/workflows/tests.yml b/Jwt/.github/workflows/tests.yml index 50d8a5f06d6..873eae245f1 100644 --- a/Jwt/.github/workflows/tests.yml +++ b/Jwt/.github/workflows/tests.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php: [ "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"] + php: [ "8.0", "8.1"] name: PHP ${{matrix.php }} Unit Test steps: - uses: actions/checkout@v2 @@ -24,45 +24,9 @@ jobs: timeout_minutes: 10 max_attempts: 3 command: composer install - - if: ${{ matrix.php == '5.6' }} - run: composer require --dev --with-dependencies paragonie/sodium_compat - name: Run Script run: vendor/bin/phpunit - # use dockerfiles for old versions of php (setup-php times out for those). - test_php55: - name: "PHP 5.5 Unit Test" - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v2 - - name: Run Unit Tests - uses: docker://php:5.5-cli - with: - entrypoint: ./.github/actions/entrypoint.sh - - test_php54: - name: "PHP 5.4 Unit Test" - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v2 - - name: Run Unit Tests - uses: docker://php:5.4-cli - with: - entrypoint: ./.github/actions/entrypoint.sh - - test_php53: - name: "PHP 5.3 Unit Test" - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v2 - - name: Run Unit Tests - uses: docker://tomsowerby/php-5.3:cli - with: - entrypoint: ./.github/actions/entrypoint.sh - style: runs-on: ubuntu-latest name: PHP Style Check @@ -71,7 +35,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: "7.0" + php-version: "8.0" - name: Run Script run: | composer require friendsofphp/php-cs-fixer diff --git a/Jwt/composer.json b/Jwt/composer.json index 6146e2dcc12..4e190ea30d7 100644 --- a/Jwt/composer.json +++ b/Jwt/composer.json @@ -20,7 +20,7 @@ ], "license": "BSD-3-Clause", "require": { - "php": ">=5.3.0" + "php": "^8.0" }, "suggest": { "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" @@ -31,6 +31,6 @@ } }, "require-dev": { - "phpunit/phpunit": ">=4.8 <=9" + "phpunit/phpunit": "^9.5" } } diff --git a/Jwt/src/JWK.php b/Jwt/src/JWK.php index c5506548dd0..5663c948d8c 100644 --- a/Jwt/src/JWK.php +++ b/Jwt/src/JWK.php @@ -33,13 +33,14 @@ class JWK * * @uses parseKey */ - public static function parseKeySet(array $jwks) + public static function parseKeySet(array $jwks): array { - $keys = array(); + $keys = []; if (!isset($jwks['keys'])) { throw new UnexpectedValueException('"keys" member must exist in the JWK Set'); } + if (empty($jwks['keys'])) { throw new InvalidArgumentException('JWK Set did not contain any keys'); } @@ -71,14 +72,16 @@ public static function parseKeySet(array $jwks) * * @uses createPemFromModulusAndExponent */ - public static function parseKey(array $jwk) + public static function parseKey(array $jwk): ?Key { if (empty($jwk)) { throw new InvalidArgumentException('JWK must not be empty'); } + if (!isset($jwk['kty'])) { throw new UnexpectedValueException('JWK must contain a "kty" parameter'); } + if (!isset($jwk['alg'])) { // The "alg" parameter is optional in a KTY, but is required for parsing in // this library. Add it manually to your JWK array if it doesn't already exist. @@ -107,6 +110,8 @@ public static function parseKey(array $jwk) // Currently only RSA is supported break; } + + return null; } /** @@ -124,10 +129,10 @@ private static function createPemFromModulusAndExponent($n, $e) $modulus = JWT::urlsafeB64Decode($n); $publicExponent = JWT::urlsafeB64Decode($e); - $components = array( + $components = [ 'modulus' => \pack('Ca*a*', 2, self::encodeLength(\strlen($modulus)), $modulus), 'publicExponent' => \pack('Ca*a*', 2, self::encodeLength(\strlen($publicExponent)), $publicExponent) - ); + ]; $rsaPublicKey = \pack( 'Ca*a*a*', diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index 6130c59c614..f5852dcda21 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -9,6 +9,7 @@ use OpenSSLAsymmetricKey; use UnexpectedValueException; use DateTime; +use stdClass; /** * JSON Web Token implementation, based on this spec: @@ -25,51 +26,40 @@ */ class JWT { - // const ASN1_INTEGER = 0x02; - // const ASN1_SEQUENCE = 0x10; - // const ASN1_BIT_STRING = 0x03; - private static $asn1Integer = 0x02; - private static $asn1Sequence = 0x10; - private static $asn1BitString = 0x03; + private const ASN1_INTEGER = 0x02; + private const ASN1_SEQUENCE = 0x10; + private const ASN1_BIT_STRING = 0x03; /** * When checking nbf, iat or expiration times, * we want to provide some extra leeway time to * account for clock skew. */ - public static $leeway = 0; - - /** - * Allow the current timestamp to be specified. - * Useful for fixing a value within unit testing. - * - * Will default to PHP time() value if null. - */ - public static $timestamp = null; - - public static $supported_algs = array( - 'ES384' => array('openssl', 'SHA384'), - 'ES256' => array('openssl', 'SHA256'), - 'HS256' => array('hash_hmac', 'SHA256'), - 'HS384' => array('hash_hmac', 'SHA384'), - 'HS512' => array('hash_hmac', 'SHA512'), - 'RS256' => array('openssl', 'SHA256'), - 'RS384' => array('openssl', 'SHA384'), - 'RS512' => array('openssl', 'SHA512'), - 'EdDSA' => array('sodium_crypto', 'EdDSA'), - ); + public static int $leeway = 0; + + public static array $supported_algs = [ + 'ES384' => ['openssl', 'SHA384'], + 'ES256' => ['openssl', 'SHA256'], + 'HS256' => ['hash_hmac', 'SHA256'], + 'HS384' => ['hash_hmac', 'SHA384'], + 'HS512' => ['hash_hmac', 'SHA512'], + 'RS256' => ['openssl', 'SHA256'], + 'RS384' => ['openssl', 'SHA384'], + 'RS512' => ['openssl', 'SHA512'], + 'EdDSA' => ['sodium_crypto', 'EdDSA'], + ]; /** * Decodes a JWT string into a PHP object. * - * @param string $jwt The JWT - * @param Key|array $keyOrKeyArray The Key or associative array of key IDs (kid) to Key objects. - * If the algorithm used is asymmetric, this is the public key - * Each Key object contains an algorithm and matching key. - * Supported algorithms are 'ES384','ES256', 'HS256', 'HS384', - * 'HS512', 'RS256', 'RS384', and 'RS512' + * @param string $jwt The JWT + * @param Key|array $keyOrKeyArray The Key or associative array of key IDs (kid) to Key objects. + * If the algorithm used is asymmetric, this is the public key + * Each Key object contains an algorithm and matching key. + * Supported algorithms are 'ES384','ES256', 'HS256', 'HS384', + * 'HS512', 'RS256', 'RS384', and 'RS512' * - * @return object The JWT's payload as a PHP object + * @return stdClass The JWT's payload as a PHP object * * @throws InvalidArgumentException Provided key/key-array was empty * @throws DomainException Provided JWT is malformed @@ -82,10 +72,10 @@ class JWT * @uses jsonDecode * @uses urlsafeB64Decode */ - public static function decode($jwt, $keyOrKeyArray) + public static function decode(string $jwt, Key|array|ArrayAccess $keyOrKeyArray): stdClass { // Validate JWT - $timestamp = \is_null(static::$timestamp) ? \time() : static::$timestamp; + $timestamp = \time(); if (empty($keyOrKeyArray)) { throw new InvalidArgumentException('Key may not be empty'); @@ -101,6 +91,9 @@ public static function decode($jwt, $keyOrKeyArray) if (null === $payload = static::jsonDecode(static::urlsafeB64Decode($bodyb64))) { throw new UnexpectedValueException('Invalid claims encoding'); } + if (!$payload instanceof stdClass) { + throw new UnexpectedValueException('Payload must be a JSON object'); + } if (false === ($sig = static::urlsafeB64Decode($cryptob64))) { throw new UnexpectedValueException('Invalid signature encoding'); } @@ -154,30 +147,32 @@ public static function decode($jwt, $keyOrKeyArray) /** * Converts and signs a PHP object or array into a JWT string. * - * @param object|array $payload PHP object or array - * @param string|resource $key The secret key. - * If the algorithm used is asymmetric, this is the private key - * @param string $alg The signing algorithm. - * Supported algorithms are 'ES384','ES256', 'HS256', 'HS384', - * 'HS512', 'RS256', 'RS384', and 'RS512' - * @param mixed $keyId - * @param array $head An array with header elements to attach + * @param array $payload PHP array + * @param string|OpenSSLAsymmetricKey $key The secret key. + * If the algorithm used is asymmetric, this is the private key + * @param string $keyId + * @param array $head An array with header elements to attach * * @return string A signed JWT * * @uses jsonEncode * @uses urlsafeB64Encode */ - public static function encode($payload, $key, $alg, $keyId = null, $head = null) - { - $header = array('typ' => 'JWT', 'alg' => $alg); + public static function encode( + array $payload, + string|OpenSSLAsymmetricKey $key, + string $alg, + string $keyId = null, + array $head = null + ): string { + $header = ['typ' => 'JWT', 'alg' => $alg]; if ($keyId !== null) { $header['kid'] = $keyId; } if (isset($head) && \is_array($head)) { $header = \array_merge($head, $header); } - $segments = array(); + $segments = []; $segments[] = static::urlsafeB64Encode(static::jsonEncode($header)); $segments[] = static::urlsafeB64Encode(static::jsonEncode($payload)); $signing_input = \implode('.', $segments); @@ -191,17 +186,16 @@ public static function encode($payload, $key, $alg, $keyId = null, $head = null) /** * Sign a string with a given key and algorithm. * - * @param string $msg The message to sign - * @param string|resource $key The secret key - * @param string $alg The signing algorithm. - * Supported algorithms are 'ES384','ES256', 'HS256', 'HS384', - * 'HS512', 'RS256', 'RS384', and 'RS512' + * @param string $msg The message to sign + * @param string|OpenSSLAsymmetricKey $key The secret key. + * @param string $alg Supported algorithms are 'ES384','ES256', 'HS256', 'HS384', + * 'HS512', 'RS256', 'RS384', and 'RS512' * * @return string An encrypted message * * @throws DomainException Unsupported algorithm or bad key was specified */ - public static function sign($msg, $key, $alg) + public static function sign(string $msg, string|OpenSSLAsymmetricKey $key, string $alg): string { if (empty(static::$supported_algs[$alg])) { throw new DomainException('Algorithm not supported'); @@ -241,17 +235,21 @@ public static function sign($msg, $key, $alg) * Verify a signature with the message, key and method. Not all methods * are symmetric, so we must have a separate verify and sign method. * - * @param string $msg The original message (header and body) - * @param string $signature The original signature - * @param string|resource $key For HS*, a string key works. for RS*, must be a resource of an openssl public key - * @param string $alg The algorithm + * @param string $msg The original message (header and body) + * @param string $signature The original signature + * @param string|OpenSSLAsymmetricKey $key For HS*, a string key works. for RS*, must be an instance of OpenSSLAsymmetricKey + * @param string $alg The algorithm * * @return bool * * @throws DomainException Invalid Algorithm, bad key, or OpenSSL failure */ - private static function verify($msg, $signature, $key, $alg) - { + private static function verify( + string $msg, + string $signature, + string|OpenSSLAsymmetricKey $keyMaterial, + string $alg + ): bool { if (empty(static::$supported_algs[$alg])) { throw new DomainException('Algorithm not supported'); } @@ -259,7 +257,7 @@ private static function verify($msg, $signature, $key, $alg) list($function, $algorithm) = static::$supported_algs[$alg]; switch ($function) { case 'openssl': - $success = \openssl_verify($msg, $signature, $key, $algorithm); + $success = \openssl_verify($msg, $signature, $keyMaterial, $algorithm); if ($success === 1) { return true; } elseif ($success === 0) { @@ -275,7 +273,7 @@ private static function verify($msg, $signature, $key, $alg) } try { // The last non-empty line is used as the key. - $lines = array_filter(explode("\n", $key)); + $lines = array_filter(explode("\n", $keyMaterial)); $key = base64_decode(end($lines)); return sodium_crypto_sign_verify_detached($signature, $msg, $key); } catch (Exception $e) { @@ -283,7 +281,7 @@ private static function verify($msg, $signature, $key, $alg) } case 'hash_hmac': default: - $hash = \hash_hmac($algorithm, $msg, $key, true); + $hash = \hash_hmac($algorithm, $msg, $keyMaterial, true); return self::constantTimeEquals($hash, $signature); } } @@ -293,27 +291,13 @@ private static function verify($msg, $signature, $key, $alg) * * @param string $input JSON string * - * @return object Object representation of JSON string + * @return mixed The decoded JSON string * * @throws DomainException Provided string was invalid JSON */ - public static function jsonDecode($input) + public static function jsonDecode(string $input): mixed { - if (\version_compare(PHP_VERSION, '5.4.0', '>=') && !(\defined('JSON_C_VERSION') && PHP_INT_SIZE > 4)) { - /** In PHP >=5.4.0, json_decode() accepts an options parameter, that allows you - * to specify that large ints (like Steam Transaction IDs) should be treated as - * strings, rather than the PHP default behaviour of converting them to floats. - */ - $obj = \json_decode($input, false, 512, JSON_BIGINT_AS_STRING); - } else { - /** Not all servers will support that, however, so for older versions we must - * manually detect large ints in the JSON string and quote them (thus converting - *them to strings) before decoding, hence the preg_replace() call. - */ - $max_int_length = \strlen((string) PHP_INT_MAX) - 1; - $json_without_bigints = \preg_replace('/:\s*(-?\d{'.$max_int_length.',})/', ': "$1"', $input); - $obj = \json_decode($json_without_bigints); - } + $obj = \json_decode($input, false, 512, JSON_BIGINT_AS_STRING); if ($errno = \json_last_error()) { static::handleJsonError($errno); @@ -324,15 +308,15 @@ public static function jsonDecode($input) } /** - * Encode a PHP object into a JSON string. + * Encode a PHP array into a JSON string. * - * @param object|array $input A PHP object or array + * @param array $input A PHP array * - * @return string JSON representation of the PHP object or array + * @return string JSON representation of the PHP array * * @throws DomainException Provided object could not be encoded to valid JSON */ - public static function jsonEncode($input) + public static function jsonEncode(array $input): string { if (PHP_VERSION_ID >= 50400) { $json = \json_encode($input, \JSON_UNESCAPED_SLASHES); @@ -355,7 +339,7 @@ public static function jsonEncode($input) * * @return string A decoded string */ - public static function urlsafeB64Decode($input) + public static function urlsafeB64Decode(string $input): string { $remainder = \strlen($input) % 4; if ($remainder) { @@ -372,7 +356,7 @@ public static function urlsafeB64Decode($input) * * @return string The base64 encode of what you passed in */ - public static function urlsafeB64Encode($input) + public static function urlsafeB64Encode(string $input): string { return \str_replace('=', '', \strtr(\base64_encode($input), '+/', '-_')); } @@ -386,9 +370,9 @@ public static function urlsafeB64Encode($input) * * @throws UnexpectedValueException * - * @return array containing the keyMaterial and algorithm + * @return Key */ - private static function getKey($keyOrKeyArray, $kid = null) + private static function getKey(Key|array|ArrayAccess $keyOrKeyArray, ?string $kid): Key { if ($keyOrKeyArray instanceof Key) { return $keyOrKeyArray; @@ -397,7 +381,7 @@ private static function getKey($keyOrKeyArray, $kid = null) if (is_array($keyOrKeyArray) || $keyOrKeyArray instanceof ArrayAccess) { foreach ($keyOrKeyArray as $keyId => $key) { if (!$key instanceof Key) { - throw new UnexpectedValueException( + throw new TypeError( '$keyOrKeyArray must be an instance of Firebase\JWT\Key key or an ' . 'array of Firebase\JWT\Key keys' ); @@ -424,7 +408,7 @@ private static function getKey($keyOrKeyArray, $kid = null) * @param string $right The user-supplied string * @return bool */ - public static function constantTimeEquals($left, $right) + public static function constantTimeEquals(string $left, string $right): bool { if (\function_exists('hash_equals')) { return \hash_equals($left, $right); @@ -445,17 +429,19 @@ public static function constantTimeEquals($left, $right) * * @param int $errno An error number from json_last_error() * + * @throws DomainException + * * @return void */ - private static function handleJsonError($errno) + private static function handleJsonError(int $errno): void { - $messages = array( + $messages = [ JSON_ERROR_DEPTH => 'Maximum stack depth exceeded', JSON_ERROR_STATE_MISMATCH => 'Invalid or malformed JSON', JSON_ERROR_CTRL_CHAR => 'Unexpected control character found', JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON', JSON_ERROR_UTF8 => 'Malformed UTF-8 characters' //PHP >= 5.3.3 - ); + ]; throw new DomainException( isset($messages[$errno]) ? $messages[$errno] @@ -470,7 +456,7 @@ private static function handleJsonError($errno) * * @return int */ - private static function safeStrlen($str) + private static function safeStrlen(string $str): int { if (\function_exists('mb_strlen')) { return \mb_strlen($str, '8bit'); @@ -484,7 +470,7 @@ private static function safeStrlen($str) * @param string $sig The ECDSA signature to convert * @return string The encoded DER object */ - private static function signatureToDER($sig) + private static function signatureToDER(string $sig): string { // Separate the signature into r-value and s-value list($r, $s) = \str_split($sig, (int) (\strlen($sig) / 2)); @@ -503,9 +489,9 @@ private static function signatureToDER($sig) } return self::encodeDER( - self::$asn1Sequence, - self::encodeDER(self::$asn1Integer, $r) . - self::encodeDER(self::$asn1Integer, $s) + self::ASN1_SEQUENCE, + self::encodeDER(self::ASN1_INTEGER, $r) . + self::encodeDER(self::ASN1_INTEGER, $s) ); } @@ -514,12 +500,13 @@ private static function signatureToDER($sig) * * @param int $type DER tag * @param string $value the value to encode + * * @return string the encoded object */ - private static function encodeDER($type, $value) + private static function encodeDER(int $type, string $value): string { $tag_header = 0; - if ($type === self::$asn1Sequence) { + if ($type === self::ASN1_SEQUENCE) { $tag_header |= 0x20; } @@ -537,9 +524,10 @@ private static function encodeDER($type, $value) * * @param string $der binary signature in DER format * @param int $keySize the number of bits in the key + * * @return string the signature */ - private static function signatureFromDER($der, $keySize) + private static function signatureFromDER(string $der, int $keySize): string { // OpenSSL returns the ECDSA signatures as a binary ASN.1 DER SEQUENCE list($offset, $_) = self::readDER($der); @@ -564,9 +552,10 @@ private static function signatureFromDER($der, $keySize) * @param string $der the binary data in DER format * @param int $offset the offset of the data stream containing the object * to decode + * * @return array [$offset, $data] the new offset and the decoded object */ - private static function readDER($der, $offset = 0) + private static function readDER(string $der, int $offset = 0): array { $pos = $offset; $size = \strlen($der); @@ -584,7 +573,7 @@ private static function readDER($der, $offset = 0) } // Value - if ($type == self::$asn1BitString) { + if ($type == self::ASN1_BIT_STRING) { $pos++; // Skip the first contents octet (padding indicator) $data = \substr($der, $pos, $len - 1); $pos += $len - 1; @@ -595,6 +584,6 @@ private static function readDER($der, $offset = 0) $data = null; } - return array($pos, $data); + return [$pos, $data]; } } diff --git a/Jwt/src/Key.php b/Jwt/src/Key.php index f1ede6f2717..286a3143e87 100644 --- a/Jwt/src/Key.php +++ b/Jwt/src/Key.php @@ -2,41 +2,34 @@ namespace Firebase\JWT; -use InvalidArgumentException; use OpenSSLAsymmetricKey; +use TypeError; +use InvalidArgumentException; class Key { - /** @var string $algorithm */ - private $algorithm; - - /** @var string|resource|OpenSSLAsymmetricKey $keyMaterial */ - private $keyMaterial; - /** - * @param string|resource|OpenSSLAsymmetricKey $keyMaterial + * @param string|OpenSSLAsymmetricKey $keyMaterial * @param string $algorithm */ - public function __construct($keyMaterial, $algorithm) - { + public function __construct( + private string|OpenSSLAsymmetricKey $keyMaterial, + private string $algorithm + ) { if ( !is_string($keyMaterial) - && !is_resource($keyMaterial) && !$keyMaterial instanceof OpenSSLAsymmetricKey ) { - throw new InvalidArgumentException('Type error: $keyMaterial must be a string, resource, or OpenSSLAsymmetricKey'); + throw new TypeError('Key material must be a string or OpenSSLAsymmetricKey'); } if (empty($keyMaterial)) { - throw new InvalidArgumentException('Type error: $keyMaterial must not be empty'); + throw new InvalidArgumentException('Key material must not be empty'); } - if (!is_string($algorithm)|| empty($keyMaterial)) { - throw new InvalidArgumentException('Type error: $algorithm must be a string'); + if (empty($algorithm)) { + throw new InvalidArgumentException('Algorithm must not be empty'); } - - $this->keyMaterial = $keyMaterial; - $this->algorithm = $algorithm; } /** @@ -44,15 +37,15 @@ public function __construct($keyMaterial, $algorithm) * * @return string */ - public function getAlgorithm() + public function getAlgorithm(): string { return $this->algorithm; } /** - * @return string|resource|OpenSSLAsymmetricKey + * @return string|OpenSSLAsymmetricKey */ - public function getKeyMaterial() + public function getKeyMaterial(): mixed { return $this->keyMaterial; } diff --git a/Jwt/tests/JWKTest.php b/Jwt/tests/JWKTest.php index c580f40f2bb..d80f6c4b909 100644 --- a/Jwt/tests/JWKTest.php +++ b/Jwt/tests/JWKTest.php @@ -3,6 +3,8 @@ namespace Firebase\JWT; use PHPUnit\Framework\TestCase; +use InvalidArgumentException; +use UnexpectedValueException; class JWKTest extends TestCase { @@ -13,29 +15,29 @@ class JWKTest extends TestCase public function testMissingKty() { $this->setExpectedException( - 'UnexpectedValueException', + UnexpectedValueException::class, 'JWK must contain a "kty" parameter' ); - $badJwk = array('kid' => 'foo'); - $keys = JWK::parseKeySet(array('keys' => array($badJwk))); + $badJwk = ['kid' => 'foo']; + $keys = JWK::parseKeySet(['keys' => [$badJwk]]); } public function testInvalidAlgorithm() { $this->setExpectedException( - 'UnexpectedValueException', + UnexpectedValueException::class, 'No supported algorithms found in JWK Set' ); - $badJwk = array('kty' => 'BADALG', 'alg' => 'RSA256'); - $keys = JWK::parseKeySet(array('keys' => array($badJwk))); + $badJwk = ['kty' => 'BADTYPE', 'alg' => 'RSA256']; + $keys = JWK::parseKeySet(['keys' => [$badJwk]]); } public function testParsePrivateKey() { $this->setExpectedException( - 'UnexpectedValueException', + UnexpectedValueException::class, 'RSA private keys are not supported' ); @@ -51,7 +53,7 @@ public function testParsePrivateKey() public function testParsePrivateKeyWithoutAlg() { $this->setExpectedException( - 'UnexpectedValueException', + UnexpectedValueException::class, 'JWK must contain an "alg" parameter' ); @@ -92,16 +94,16 @@ public function testParseJwkKeySet() public function testParseJwkKey_empty() { - $this->setExpectedException('InvalidArgumentException', 'JWK must not be empty'); + $this->setExpectedException(InvalidArgumentException::class, 'JWK must not be empty'); - JWK::parseKeySet(array('keys' => array(array()))); + JWK::parseKeySet(['keys' => [[]]]); } public function testParseJwkKeySet_empty() { - $this->setExpectedException('InvalidArgumentException', 'JWK Set did not contain any keys'); + $this->setExpectedException(InvalidArgumentException::class, 'JWK Set did not contain any keys'); - JWK::parseKeySet(array('keys' => array())); + JWK::parseKeySet(['keys' => []]); } /** @@ -110,12 +112,12 @@ public function testParseJwkKeySet_empty() public function testDecodeByJwkKeySetTokenExpired() { $privKey1 = file_get_contents(__DIR__ . '/data/rsa1-private.pem'); - $payload = array('exp' => strtotime('-1 hour')); + $payload = ['exp' => strtotime('-1 hour')]; $msg = JWT::encode($payload, $privKey1, 'RS256', 'jwk1'); - $this->setExpectedException('Firebase\JWT\ExpiredException'); + $this->setExpectedException(ExpiredException::class); - JWT::decode($msg, self::$keys, array('RS256')); + JWT::decode($msg, self::$keys); } /** @@ -124,10 +126,10 @@ public function testDecodeByJwkKeySetTokenExpired() public function testDecodeByJwkKeySet() { $privKey1 = file_get_contents(__DIR__ . '/data/rsa1-private.pem'); - $payload = array('sub' => 'foo', 'exp' => strtotime('+10 seconds')); + $payload = ['sub' => 'foo', 'exp' => strtotime('+10 seconds')]; $msg = JWT::encode($payload, $privKey1, 'RS256', 'jwk1'); - $result = JWT::decode($msg, self::$keys, array('RS256')); + $result = JWT::decode($msg, self::$keys); $this->assertEquals("foo", $result->sub); } @@ -138,10 +140,10 @@ public function testDecodeByJwkKeySet() public function testDecodeByMultiJwkKeySet() { $privKey2 = file_get_contents(__DIR__ . '/data/rsa2-private.pem'); - $payload = array('sub' => 'bar', 'exp' => strtotime('+10 seconds')); + $payload = ['sub' => 'bar', 'exp' => strtotime('+10 seconds')]; $msg = JWT::encode($payload, $privKey2, 'RS256', 'jwk2'); - $result = JWT::decode($msg, self::$keys, array('RS256')); + $result = JWT::decode($msg, self::$keys); $this->assertEquals("bar", $result->sub); } diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index 36e2095e91d..aa5ce140cd0 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -4,6 +4,10 @@ use ArrayObject; use PHPUnit\Framework\TestCase; +use DomainException; +use InvalidArgumentException; +use UnexpectedValueException; +use stdClass; class JWTTest extends TestCase { @@ -19,68 +23,61 @@ public function setExpectedException($exceptionName, $message = '', $code = null } } - public function testDecodeFromPython() - { - $msg = 'eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.Iio6aHR0cDovL2FwcGxpY2F0aW9uL2NsaWNreT9ibGFoPTEuMjMmZi5vbz00NTYgQUMwMDAgMTIzIg.E_U8X2YpMT5K1cEiT_3-IvBYfrdIFIeVYeOqre_Z5Cg'; - $this->assertEquals( - JWT::decode($msg, new Key('my_key', 'HS256')), - '*:http://application/clicky?blah=1.23&f.oo=456 AC000 123' - ); - } - public function testUrlSafeCharacters() { - $encoded = JWT::encode('f?', 'a', 'HS256'); - $this->assertEquals('f?', JWT::decode($encoded, new Key('a', 'HS256'))); + $encoded = JWT::encode(['message' => 'f?'], 'a', 'HS256'); + $expected = new stdClass(); + $expected->message = 'f?'; + $this->assertEquals($expected, JWT::decode($encoded, new Key('a', 'HS256'))); } public function testMalformedUtf8StringsFail() { - $this->setExpectedException('DomainException'); - JWT::encode(pack('c', 128), 'a', 'HS256'); + $this->setExpectedException(DomainException::class); + JWT::encode(['message' => pack('c', 128)], 'a', 'HS256'); } public function testMalformedJsonThrowsException() { - $this->setExpectedException('DomainException'); + $this->setExpectedException(DomainException::class); JWT::jsonDecode('this is not valid JSON string'); } public function testExpiredToken() { - $this->setExpectedException('Firebase\JWT\ExpiredException'); - $payload = array( + $this->setExpectedException(ExpiredException::class); + $payload = [ "message" => "abc", - "exp" => time() - 20); // time in the past + "exp" => time() - 20]; // time in the past $encoded = JWT::encode($payload, 'my_key', 'HS256'); JWT::decode($encoded, new Key('my_key', 'HS256')); } public function testBeforeValidTokenWithNbf() { - $this->setExpectedException('Firebase\JWT\BeforeValidException'); - $payload = array( + $this->setExpectedException(BeforeValidException::class); + $payload = [ "message" => "abc", - "nbf" => time() + 20); // time in the future + "nbf" => time() + 20]; // time in the future $encoded = JWT::encode($payload, 'my_key', 'HS256'); JWT::decode($encoded, new Key('my_key', 'HS256')); } public function testBeforeValidTokenWithIat() { - $this->setExpectedException('Firebase\JWT\BeforeValidException'); - $payload = array( + $this->setExpectedException(BeforeValidException::class); + $payload = [ "message" => "abc", - "iat" => time() + 20); // time in the future + "iat" => time() + 20]; // time in the future $encoded = JWT::encode($payload, 'my_key', 'HS256'); JWT::decode($encoded, new Key('my_key', 'HS256')); } public function testValidToken() { - $payload = array( + $payload = [ "message" => "abc", - "exp" => time() + JWT::$leeway + 20); // time in the future + "exp" => time() + JWT::$leeway + 20]; // time in the future $encoded = JWT::encode($payload, 'my_key', 'HS256'); $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); $this->assertEquals($decoded->message, 'abc'); @@ -89,9 +86,9 @@ public function testValidToken() public function testValidTokenWithLeeway() { JWT::$leeway = 60; - $payload = array( + $payload = [ "message" => "abc", - "exp" => time() - 20); // time in the past + "exp" => time() - 20]; // time in the past $encoded = JWT::encode($payload, 'my_key', 'HS256'); $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); $this->assertEquals($decoded->message, 'abc'); @@ -101,10 +98,10 @@ public function testValidTokenWithLeeway() public function testExpiredTokenWithLeeway() { JWT::$leeway = 60; - $payload = array( + $payload = [ "message" => "abc", - "exp" => time() - 70); // time far in the past - $this->setExpectedException('Firebase\JWT\ExpiredException'); + "exp" => time() - 70]; // time far in the past + $this->setExpectedException(ExpiredException::class); $encoded = JWT::encode($payload, 'my_key', 'HS256'); $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); $this->assertEquals($decoded->message, 'abc'); @@ -113,11 +110,11 @@ public function testExpiredTokenWithLeeway() public function testValidTokenWithNbf() { - $payload = array( + $payload = [ "message" => "abc", "iat" => time(), "exp" => time() + 20, // time in the future - "nbf" => time() - 20); + "nbf" => time() - 20]; $encoded = JWT::encode($payload, 'my_key', 'HS256'); $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); $this->assertEquals($decoded->message, 'abc'); @@ -126,9 +123,9 @@ public function testValidTokenWithNbf() public function testValidTokenWithNbfLeeway() { JWT::$leeway = 60; - $payload = array( + $payload = [ "message" => "abc", - "nbf" => time() + 20); // not before in near (leeway) future + "nbf" => time() + 20]; // not before in near (leeway) future $encoded = JWT::encode($payload, 'my_key', 'HS256'); $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); $this->assertEquals($decoded->message, 'abc'); @@ -138,11 +135,11 @@ public function testValidTokenWithNbfLeeway() public function testInvalidTokenWithNbfLeeway() { JWT::$leeway = 60; - $payload = array( + $payload = [ "message" => "abc", - "nbf" => time() + 65); // not before too far in future + "nbf" => time() + 65]; // not before too far in future $encoded = JWT::encode($payload, 'my_key', 'HS256'); - $this->setExpectedException('Firebase\JWT\BeforeValidException'); + $this->setExpectedException(BeforeValidException::class); JWT::decode($encoded, new Key('my_key', 'HS256')); JWT::$leeway = 0; } @@ -150,9 +147,9 @@ public function testInvalidTokenWithNbfLeeway() public function testValidTokenWithIatLeeway() { JWT::$leeway = 60; - $payload = array( + $payload = [ "message" => "abc", - "iat" => time() + 20); // issued in near (leeway) future + "iat" => time() + 20]; // issued in near (leeway) future $encoded = JWT::encode($payload, 'my_key', 'HS256'); $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); $this->assertEquals($decoded->message, 'abc'); @@ -162,123 +159,133 @@ public function testValidTokenWithIatLeeway() public function testInvalidTokenWithIatLeeway() { JWT::$leeway = 60; - $payload = array( + $payload = [ "message" => "abc", - "iat" => time() + 65); // issued too far in future + "iat" => time() + 65]; // issued too far in future $encoded = JWT::encode($payload, 'my_key', 'HS256'); - $this->setExpectedException('Firebase\JWT\BeforeValidException'); + $this->setExpectedException(BeforeValidException::class); JWT::decode($encoded, new Key('my_key', 'HS256')); JWT::$leeway = 0; } public function testInvalidToken() { - $payload = array( + $payload = [ "message" => "abc", - "exp" => time() + 20); // time in the future + "exp" => time() + 20]; // time in the future $encoded = JWT::encode($payload, 'my_key', 'HS256'); - $this->setExpectedException('Firebase\JWT\SignatureInvalidException'); + $this->setExpectedException(SignatureInvalidException::class); JWT::decode($encoded, new Key('my_key2', 'HS256')); } public function testNullKeyFails() { - $payload = array( + $payload = [ "message" => "abc", - "exp" => time() + JWT::$leeway + 20); // time in the future + "exp" => time() + JWT::$leeway + 20]; // time in the future $encoded = JWT::encode($payload, 'my_key', 'HS256'); - $this->setExpectedException('InvalidArgumentException'); + $this->setExpectedException('TypeError'); JWT::decode($encoded, new Key(null, 'HS256')); } public function testEmptyKeyFails() { - $payload = array( + $payload = [ "message" => "abc", - "exp" => time() + JWT::$leeway + 20); // time in the future + "exp" => time() + JWT::$leeway + 20]; // time in the future $encoded = JWT::encode($payload, 'my_key', 'HS256'); - $this->setExpectedException('InvalidArgumentException'); + $this->setExpectedException(InvalidArgumentException::class); JWT::decode($encoded, new Key('', 'HS256')); } public function testKIDChooser() { - $keys = array( + $keys = [ '1' => new Key('my_key', 'HS256'), '2' => new Key('my_key2', 'HS256') - ); - $msg = JWT::encode('abc', $keys['1']->getKeyMaterial(), 'HS256', '1'); + ]; + $msg = JWT::encode(['message' => 'abc'], $keys['1']->getKeyMaterial(), 'HS256', '1'); $decoded = JWT::decode($msg, $keys); - $this->assertEquals($decoded, 'abc'); + $expected = new stdClass(); + $expected->message = 'abc'; + $this->assertEquals($decoded, $expected); } public function testArrayAccessKIDChooser() { - $keys = new ArrayObject(array( + $keys = new ArrayObject([ '1' => new Key('my_key', 'HS256'), '2' => new Key('my_key2', 'HS256'), - )); - $msg = JWT::encode('abc', $keys['1']->getKeyMaterial(), 'HS256', '1'); + ]); + $msg = JWT::encode(['message' => 'abc'], $keys['1']->getKeyMaterial(), 'HS256', '1'); $decoded = JWT::decode($msg, $keys); - $this->assertEquals($decoded, 'abc'); + $expected = new stdClass(); + $expected->message = 'abc'; + $this->assertEquals($decoded, $expected); } public function testNoneAlgorithm() { - $msg = JWT::encode('abc', 'my_key', 'HS256'); - $this->setExpectedException('UnexpectedValueException'); + $msg = JWT::encode(['message' => 'abc'], 'my_key', 'HS256'); + $this->setExpectedException(UnexpectedValueException::class); JWT::decode($msg, new Key('my_key', 'none')); } public function testIncorrectAlgorithm() { - $msg = JWT::encode('abc', 'my_key', 'HS256'); - $this->setExpectedException('UnexpectedValueException'); + $msg = JWT::encode(['message' => 'abc'], 'my_key', 'HS256'); + $this->setExpectedException(UnexpectedValueException::class); JWT::decode($msg, new Key('my_key', 'RS256')); } public function testEmptyAlgorithm() { - $msg = JWT::encode('abc', 'my_key', 'HS256'); - $this->setExpectedException('UnexpectedValueException'); + $msg = JWT::encode(['message' => 'abc'], 'my_key', 'HS256'); + $this->setExpectedException(InvalidArgumentException::class); JWT::decode($msg, new Key('my_key', '')); } public function testAdditionalHeaders() { - $msg = JWT::encode('abc', 'my_key', 'HS256', null, array('cty' => 'test-eit;v=1')); - $this->assertEquals(JWT::decode($msg, new Key('my_key', 'HS256')), 'abc'); + $msg = JWT::encode(['message' => 'abc'], 'my_key', 'HS256', null, ['cty' => 'test-eit;v=1']); + $expected = new stdClass(); + $expected->message = 'abc'; + $this->assertEquals(JWT::decode($msg, new Key('my_key', 'HS256')), $expected); } public function testInvalidSegmentCount() { - $this->setExpectedException('UnexpectedValueException'); + $this->setExpectedException(UnexpectedValueException::class); JWT::decode('brokenheader.brokenbody', new Key('my_key', 'HS256')); } public function testInvalidSignatureEncoding() { $msg = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwibmFtZSI6ImZvbyJ9.Q4Kee9E8o0Xfo4ADXvYA8t7dN_X_bU9K5w6tXuiSjlUxx"; - $this->setExpectedException('UnexpectedValueException'); + $this->setExpectedException(UnexpectedValueException::class); JWT::decode($msg, new Key('secret', 'HS256')); } public function testHSEncodeDecode() { - $msg = JWT::encode('abc', 'my_key', 'HS256'); - $this->assertEquals(JWT::decode($msg, new Key('my_key', 'HS256')), 'abc'); + $msg = JWT::encode(['message' => 'abc'], 'my_key', 'HS256'); + $expected = new stdClass(); + $expected->message = 'abc'; + $this->assertEquals(JWT::decode($msg, new Key('my_key', 'HS256')), $expected); } public function testRSEncodeDecode() { - $privKey = openssl_pkey_new(array('digest_alg' => 'sha256', + $privKey = openssl_pkey_new(['digest_alg' => 'sha256', 'private_key_bits' => 1024, - 'private_key_type' => OPENSSL_KEYTYPE_RSA)); - $msg = JWT::encode('abc', $privKey, 'RS256'); + 'private_key_type' => OPENSSL_KEYTYPE_RSA]); + $msg = JWT::encode(['message' => 'abc'], $privKey, 'RS256'); $pubKey = openssl_pkey_get_details($privKey); $pubKey = $pubKey['key']; $decoded = JWT::decode($msg, new Key($pubKey, 'RS256')); - $this->assertEquals($decoded, 'abc'); + $expected = new stdClass(); + $expected->message = 'abc'; + $this->assertEquals($decoded, $expected); } public function testEdDsaEncodeDecode() @@ -286,7 +293,7 @@ public function testEdDsaEncodeDecode() $keyPair = sodium_crypto_sign_keypair(); $privKey = base64_encode(sodium_crypto_sign_secretkey($keyPair)); - $payload = array('foo' => 'bar'); + $payload = ['foo' => 'bar']; $msg = JWT::encode($payload, $privKey, 'EdDSA'); $pubKey = base64_encode(sodium_crypto_sign_publickey($keyPair)); @@ -299,13 +306,13 @@ public function testInvalidEdDsaEncodeDecode() $keyPair = sodium_crypto_sign_keypair(); $privKey = base64_encode(sodium_crypto_sign_secretkey($keyPair)); - $payload = array('foo' => 'bar'); + $payload = ['foo' => 'bar']; $msg = JWT::encode($payload, $privKey, 'EdDSA'); // Generate a different key. $keyPair = sodium_crypto_sign_keypair(); $pubKey = base64_encode(sodium_crypto_sign_publickey($keyPair)); - $this->setExpectedException('Firebase\JWT\SignatureInvalidException'); + $this->setExpectedException(SignatureInvalidException::class); JWT::decode($msg, new Key($pubKey, 'EdDSA')); } @@ -316,11 +323,13 @@ public function testRSEncodeDecodeWithPassphrase() 'passphrase' ); - $jwt = JWT::encode('abc', $privateKey, 'RS256'); + $jwt = JWT::encode(['message' => 'abc'], $privateKey, 'RS256'); $keyDetails = openssl_pkey_get_details($privateKey); $pubKey = $keyDetails['key']; $decoded = JWT::decode($jwt, new Key($pubKey, 'RS256')); - $this->assertEquals($decoded, 'abc'); + $expected = new stdClass(); + $expected->message = 'abc'; + $this->assertEquals($decoded, $expected); } /** @@ -330,7 +339,7 @@ public function testRSEncodeDecodeWithPassphrase() public function testEncodeDecode($privateKeyFile, $publicKeyFile, $alg) { $privateKey = file_get_contents($privateKeyFile); - $payload = array('foo' => 'bar'); + $payload = ['foo' => 'bar']; $encoded = JWT::encode($payload, $privateKey, $alg); // Verify decoding succeeds @@ -342,12 +351,12 @@ public function testEncodeDecode($privateKeyFile, $publicKeyFile, $alg) public function provideEncodeDecode() { - return array( - array(__DIR__ . '/data/ecdsa-private.pem', __DIR__ . '/data/ecdsa-public.pem', 'ES256'), - array(__DIR__ . '/data/ecdsa384-private.pem', __DIR__ . '/data/ecdsa384-public.pem', 'ES384'), - array(__DIR__ . '/data/rsa1-private.pem', __DIR__ . '/data/rsa1-public.pub', 'RS512'), - array(__DIR__ . '/data/ed25519-1.sec', __DIR__ . '/data/ed25519-1.pub', 'EdDSA'), - ); + return [ + [__DIR__ . '/data/ecdsa-private.pem', __DIR__ . '/data/ecdsa-public.pem', 'ES256'], + [__DIR__ . '/data/ecdsa384-private.pem', __DIR__ . '/data/ecdsa384-public.pem', 'ES384'], + [__DIR__ . '/data/rsa1-private.pem', __DIR__ . '/data/rsa1-public.pub', 'RS512'], + [__DIR__ . '/data/ed25519-1.sec', __DIR__ . '/data/ed25519-1.pub', 'EdDSA'], + ]; } public function testEncodeDecodeWithResource() @@ -356,7 +365,7 @@ public function testEncodeDecodeWithResource() $resource = openssl_pkey_get_public($pem); $privateKey = file_get_contents(__DIR__ . '/data/rsa1-private.pem'); - $payload = array('foo' => 'bar'); + $payload = ['foo' => 'bar']; $encoded = JWT::encode($payload, $privateKey, 'RS512'); // Verify decoding succeeds From 17640df4ce5d74f07f12f2700e817b2ec957405b Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 16 Feb 2022 06:33:52 -0800 Subject: [PATCH 168/282] chore: remove phpunit shim (firebase/php-jwt#401) --- Jwt/tests/JWKTest.php | 47 ++++++++++++------------------------------- Jwt/tests/JWTTest.php | 47 +++++++++++++++++-------------------------- 2 files changed, 31 insertions(+), 63 deletions(-) diff --git a/Jwt/tests/JWKTest.php b/Jwt/tests/JWKTest.php index d80f6c4b909..17dd4a623fd 100644 --- a/Jwt/tests/JWKTest.php +++ b/Jwt/tests/JWKTest.php @@ -14,10 +14,8 @@ class JWKTest extends TestCase public function testMissingKty() { - $this->setExpectedException( - UnexpectedValueException::class, - 'JWK must contain a "kty" parameter' - ); + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage('JWK must contain a "kty" parameter'); $badJwk = ['kid' => 'foo']; $keys = JWK::parseKeySet(['keys' => [$badJwk]]); @@ -25,10 +23,8 @@ public function testMissingKty() public function testInvalidAlgorithm() { - $this->setExpectedException( - UnexpectedValueException::class, - 'No supported algorithms found in JWK Set' - ); + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage('No supported algorithms found in JWK Set'); $badJwk = ['kty' => 'BADTYPE', 'alg' => 'RSA256']; $keys = JWK::parseKeySet(['keys' => [$badJwk]]); @@ -36,10 +32,8 @@ public function testInvalidAlgorithm() public function testParsePrivateKey() { - $this->setExpectedException( - UnexpectedValueException::class, - 'RSA private keys are not supported' - ); + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage('RSA private keys are not supported'); $jwkSet = json_decode( file_get_contents(__DIR__ . '/data/rsa-jwkset.json'), @@ -52,10 +46,8 @@ public function testParsePrivateKey() public function testParsePrivateKeyWithoutAlg() { - $this->setExpectedException( - UnexpectedValueException::class, - 'JWK must contain an "alg" parameter' - ); + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage('JWK must contain an "alg" parameter'); $jwkSet = json_decode( file_get_contents(__DIR__ . '/data/rsa-jwkset.json'), @@ -94,14 +86,16 @@ public function testParseJwkKeySet() public function testParseJwkKey_empty() { - $this->setExpectedException(InvalidArgumentException::class, 'JWK must not be empty'); + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('JWK must not be empty'); JWK::parseKeySet(['keys' => [[]]]); } public function testParseJwkKeySet_empty() { - $this->setExpectedException(InvalidArgumentException::class, 'JWK Set did not contain any keys'); + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('JWK Set did not contain any keys'); JWK::parseKeySet(['keys' => []]); } @@ -115,7 +109,7 @@ public function testDecodeByJwkKeySetTokenExpired() $payload = ['exp' => strtotime('-1 hour')]; $msg = JWT::encode($payload, $privKey1, 'RS256', 'jwk1'); - $this->setExpectedException(ExpiredException::class); + $this->expectException(ExpiredException::class); JWT::decode($msg, self::$keys); } @@ -147,19 +141,4 @@ public function testDecodeByMultiJwkKeySet() $this->assertEquals("bar", $result->sub); } - - /* - * For compatibility with PHPUnit 4.8 and PHP < 5.6 - */ - public function setExpectedException($exceptionName, $message = '', $code = null) - { - if (method_exists($this, 'expectException')) { - $this->expectException($exceptionName); - if ($message) { - $this->expectExceptionMessage($message); - } - } else { - parent::setExpectedException($exceptionName, $message, $code); - } - } } diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index aa5ce140cd0..8b23ad6b4bc 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -6,23 +6,12 @@ use PHPUnit\Framework\TestCase; use DomainException; use InvalidArgumentException; +use TypeError; use UnexpectedValueException; use stdClass; class JWTTest extends TestCase { - /* - * For compatibility with PHPUnit 4.8 and PHP < 5.6 - */ - public function setExpectedException($exceptionName, $message = '', $code = null) - { - if (method_exists($this, 'expectException')) { - $this->expectException($exceptionName); - } else { - parent::setExpectedException($exceptionName, $message, $code); - } - } - public function testUrlSafeCharacters() { $encoded = JWT::encode(['message' => 'f?'], 'a', 'HS256'); @@ -33,19 +22,19 @@ public function testUrlSafeCharacters() public function testMalformedUtf8StringsFail() { - $this->setExpectedException(DomainException::class); + $this->expectException(DomainException::class); JWT::encode(['message' => pack('c', 128)], 'a', 'HS256'); } public function testMalformedJsonThrowsException() { - $this->setExpectedException(DomainException::class); + $this->expectException(DomainException::class); JWT::jsonDecode('this is not valid JSON string'); } public function testExpiredToken() { - $this->setExpectedException(ExpiredException::class); + $this->expectException(ExpiredException::class); $payload = [ "message" => "abc", "exp" => time() - 20]; // time in the past @@ -55,7 +44,7 @@ public function testExpiredToken() public function testBeforeValidTokenWithNbf() { - $this->setExpectedException(BeforeValidException::class); + $this->expectException(BeforeValidException::class); $payload = [ "message" => "abc", "nbf" => time() + 20]; // time in the future @@ -65,7 +54,7 @@ public function testBeforeValidTokenWithNbf() public function testBeforeValidTokenWithIat() { - $this->setExpectedException(BeforeValidException::class); + $this->expectException(BeforeValidException::class); $payload = [ "message" => "abc", "iat" => time() + 20]; // time in the future @@ -101,7 +90,7 @@ public function testExpiredTokenWithLeeway() $payload = [ "message" => "abc", "exp" => time() - 70]; // time far in the past - $this->setExpectedException(ExpiredException::class); + $this->expectException(ExpiredException::class); $encoded = JWT::encode($payload, 'my_key', 'HS256'); $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); $this->assertEquals($decoded->message, 'abc'); @@ -139,7 +128,7 @@ public function testInvalidTokenWithNbfLeeway() "message" => "abc", "nbf" => time() + 65]; // not before too far in future $encoded = JWT::encode($payload, 'my_key', 'HS256'); - $this->setExpectedException(BeforeValidException::class); + $this->expectException(BeforeValidException::class); JWT::decode($encoded, new Key('my_key', 'HS256')); JWT::$leeway = 0; } @@ -163,7 +152,7 @@ public function testInvalidTokenWithIatLeeway() "message" => "abc", "iat" => time() + 65]; // issued too far in future $encoded = JWT::encode($payload, 'my_key', 'HS256'); - $this->setExpectedException(BeforeValidException::class); + $this->expectException(BeforeValidException::class); JWT::decode($encoded, new Key('my_key', 'HS256')); JWT::$leeway = 0; } @@ -174,7 +163,7 @@ public function testInvalidToken() "message" => "abc", "exp" => time() + 20]; // time in the future $encoded = JWT::encode($payload, 'my_key', 'HS256'); - $this->setExpectedException(SignatureInvalidException::class); + $this->expectException(SignatureInvalidException::class); JWT::decode($encoded, new Key('my_key2', 'HS256')); } @@ -184,7 +173,7 @@ public function testNullKeyFails() "message" => "abc", "exp" => time() + JWT::$leeway + 20]; // time in the future $encoded = JWT::encode($payload, 'my_key', 'HS256'); - $this->setExpectedException('TypeError'); + $this->expectException(TypeError::class); JWT::decode($encoded, new Key(null, 'HS256')); } @@ -194,7 +183,7 @@ public function testEmptyKeyFails() "message" => "abc", "exp" => time() + JWT::$leeway + 20]; // time in the future $encoded = JWT::encode($payload, 'my_key', 'HS256'); - $this->setExpectedException(InvalidArgumentException::class); + $this->expectException(InvalidArgumentException::class); JWT::decode($encoded, new Key('', 'HS256')); } @@ -227,21 +216,21 @@ public function testArrayAccessKIDChooser() public function testNoneAlgorithm() { $msg = JWT::encode(['message' => 'abc'], 'my_key', 'HS256'); - $this->setExpectedException(UnexpectedValueException::class); + $this->expectException(UnexpectedValueException::class); JWT::decode($msg, new Key('my_key', 'none')); } public function testIncorrectAlgorithm() { $msg = JWT::encode(['message' => 'abc'], 'my_key', 'HS256'); - $this->setExpectedException(UnexpectedValueException::class); + $this->expectException(UnexpectedValueException::class); JWT::decode($msg, new Key('my_key', 'RS256')); } public function testEmptyAlgorithm() { $msg = JWT::encode(['message' => 'abc'], 'my_key', 'HS256'); - $this->setExpectedException(InvalidArgumentException::class); + $this->expectException(InvalidArgumentException::class); JWT::decode($msg, new Key('my_key', '')); } @@ -255,14 +244,14 @@ public function testAdditionalHeaders() public function testInvalidSegmentCount() { - $this->setExpectedException(UnexpectedValueException::class); + $this->expectException(UnexpectedValueException::class); JWT::decode('brokenheader.brokenbody', new Key('my_key', 'HS256')); } public function testInvalidSignatureEncoding() { $msg = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwibmFtZSI6ImZvbyJ9.Q4Kee9E8o0Xfo4ADXvYA8t7dN_X_bU9K5w6tXuiSjlUxx"; - $this->setExpectedException(UnexpectedValueException::class); + $this->expectException(UnexpectedValueException::class); JWT::decode($msg, new Key('secret', 'HS256')); } @@ -312,7 +301,7 @@ public function testInvalidEdDsaEncodeDecode() // Generate a different key. $keyPair = sodium_crypto_sign_keypair(); $pubKey = base64_encode(sodium_crypto_sign_publickey($keyPair)); - $this->setExpectedException(SignatureInvalidException::class); + $this->expectException(SignatureInvalidException::class); JWT::decode($msg, new Key($pubKey, 'EdDSA')); } From 820340fccaadf807f5c5aff976c64990c25e2a63 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 16 Feb 2022 10:44:52 -0800 Subject: [PATCH 169/282] chore: add array casting to README (firebase/php-jwt#402) --- Jwt/README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Jwt/README.md b/Jwt/README.md index 2300ae309fa..d489c69f397 100644 --- a/Jwt/README.md +++ b/Jwt/README.md @@ -203,6 +203,22 @@ $jwks = ['keys' => []]; JWT::decode($payload, JWK::parseKeySet($jwks)); ``` +Miscellaneous +------------- + +#### Casting to array + +The return value of `JWT::decode` is the generic PHP object `stdClass`. If you'd like to handle with arrays +instead, you can do the following: + +```php +// return type is stdClass +$decoded = JWT::decode($payload, $keys); + +// cast to array +$decoded = json_decode(json_encode($decoded), true); +``` + Changelog --------- From 13c274233ff6949979bbb7fe3366a2efcf5895a7 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Thu, 17 Feb 2022 06:42:40 -0800 Subject: [PATCH 170/282] chore: add phpstan for static analysis (firebase/php-jwt#406) --- Jwt/.github/workflows/tests.yml | 14 ++++++++++++++ Jwt/src/JWT.php | 3 +++ 2 files changed, 17 insertions(+) diff --git a/Jwt/.github/workflows/tests.yml b/Jwt/.github/workflows/tests.yml index 873eae245f1..80063a00d80 100644 --- a/Jwt/.github/workflows/tests.yml +++ b/Jwt/.github/workflows/tests.yml @@ -41,3 +41,17 @@ jobs: composer require friendsofphp/php-cs-fixer vendor/bin/php-cs-fixer fix --diff --dry-run . vendor/bin/php-cs-fixer fix --rules=native_function_invocation --allow-risky=yes --diff src + + staticanalysis: + runs-on: ubuntu-latest + name: PHPStan Static Analysis + steps: + - uses: actions/checkout@v2 + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.0' + - name: Run Script + run: | + composer global require phpstan/phpstan + ~/.composer/vendor/bin/phpstan analyse src diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index f5852dcda21..b725aae470c 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -7,6 +7,7 @@ use Exception; use InvalidArgumentException; use OpenSSLAsymmetricKey; +use TypeError; use UnexpectedValueException; use DateTime; use stdClass; @@ -229,6 +230,8 @@ public static function sign(string $msg, string|OpenSSLAsymmetricKey $key, strin throw new DomainException($e->getMessage(), 0, $e); } } + + throw new DomainException('Algorithm not supported'); } /** From 4a8af8af62cce2bdc6a0b58c8454c31fd3e2bf26 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 23 Feb 2022 07:10:42 -0800 Subject: [PATCH 171/282] chore: update library to phpstan level 7 (firebase/php-jwt#407) --- Jwt/.github/workflows/tests.yml | 2 +- Jwt/phpstan.neon.dist | 5 ++ Jwt/src/JWK.php | 20 +++-- Jwt/src/JWT.php | 126 ++++++++++++++++++-------------- Jwt/src/Key.php | 16 ++-- 5 files changed, 96 insertions(+), 73 deletions(-) create mode 100644 Jwt/phpstan.neon.dist diff --git a/Jwt/.github/workflows/tests.yml b/Jwt/.github/workflows/tests.yml index 80063a00d80..816306459fa 100644 --- a/Jwt/.github/workflows/tests.yml +++ b/Jwt/.github/workflows/tests.yml @@ -54,4 +54,4 @@ jobs: - name: Run Script run: | composer global require phpstan/phpstan - ~/.composer/vendor/bin/phpstan analyse src + ~/.composer/vendor/bin/phpstan analyse diff --git a/Jwt/phpstan.neon.dist b/Jwt/phpstan.neon.dist new file mode 100644 index 00000000000..56aeebfb59a --- /dev/null +++ b/Jwt/phpstan.neon.dist @@ -0,0 +1,5 @@ +parameters: + level: 7 + paths: + - src + treatPhpDocTypesAsCertain: false diff --git a/Jwt/src/JWK.php b/Jwt/src/JWK.php index 5663c948d8c..d5ad93a89a7 100644 --- a/Jwt/src/JWK.php +++ b/Jwt/src/JWK.php @@ -23,7 +23,7 @@ class JWK /** * Parse a set of JWK keys * - * @param array $jwks The JSON Web Key Set as an associative array + * @param array $jwks The JSON Web Key Set as an associative array * * @return array An associative array of key IDs (kid) to Key objects * @@ -48,7 +48,7 @@ public static function parseKeySet(array $jwks): array foreach ($jwks['keys'] as $k => $v) { $kid = isset($v['kid']) ? $v['kid'] : $k; if ($key = self::parseKey($v)) { - $keys[$kid] = $key; + $keys[(string) $kid] = $key; } } @@ -62,7 +62,7 @@ public static function parseKeySet(array $jwks): array /** * Parse a JWK key * - * @param array $jwk An individual JWK + * @param array $jwk An individual JWK * * @return Key The key object for the JWK * @@ -124,10 +124,16 @@ public static function parseKey(array $jwk): ?Key * * @uses encodeLength */ - private static function createPemFromModulusAndExponent($n, $e) - { - $modulus = JWT::urlsafeB64Decode($n); - $publicExponent = JWT::urlsafeB64Decode($e); + private static function createPemFromModulusAndExponent( + string $n, + string $e + ): string { + if (false === ($modulus = JWT::urlsafeB64Decode($n))) { + throw new UnexpectedValueException('Invalid JWK encoding'); + } + if (false === ($publicExponent = JWT::urlsafeB64Decode($e))) { + throw new UnexpectedValueException('Invalid header encoding'); + } $components = [ 'modulus' => \pack('Ca*a*', 2, self::encodeLength(\strlen($modulus)), $modulus), diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index b725aae470c..cf58fd2a236 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -7,6 +7,7 @@ use Exception; use InvalidArgumentException; use OpenSSLAsymmetricKey; +use OpenSSLCertificate; use TypeError; use UnexpectedValueException; use DateTime; @@ -38,6 +39,9 @@ class JWT */ public static int $leeway = 0; + /** + * @var array + */ public static array $supported_algs = [ 'ES384' => ['openssl', 'SHA384'], 'ES256' => ['openssl', 'SHA256'], @@ -86,10 +90,16 @@ public static function decode(string $jwt, Key|array|ArrayAccess $keyOrKeyArray) throw new UnexpectedValueException('Wrong number of segments'); } list($headb64, $bodyb64, $cryptob64) = $tks; - if (null === ($header = static::jsonDecode(static::urlsafeB64Decode($headb64)))) { + if (false === ($headerRaw = static::urlsafeB64Decode($headb64))) { + throw new UnexpectedValueException('Invalid header encoding'); + } + if (null === ($header = static::jsonDecode($headerRaw))) { throw new UnexpectedValueException('Invalid header encoding'); } - if (null === $payload = static::jsonDecode(static::urlsafeB64Decode($bodyb64))) { + if (false === ($payloadRaw = static::urlsafeB64Decode($bodyb64))) { + throw new UnexpectedValueException('Invalid claims encoding'); + } + if (null === ($payload = static::jsonDecode($payloadRaw))) { throw new UnexpectedValueException('Invalid claims encoding'); } if (!$payload instanceof stdClass) { @@ -116,7 +126,7 @@ public static function decode(string $jwt, Key|array|ArrayAccess $keyOrKeyArray) // OpenSSL expects an ASN.1 DER sequence for ES256/ES384 signatures $sig = self::signatureToDER($sig); } - if (!static::verify("$headb64.$bodyb64", $sig, $key->getKeyMaterial(), $header->alg)) { + if (!self::verify("$headb64.$bodyb64", $sig, $key->getKeyMaterial(), $header->alg)) { throw new SignatureInvalidException('Signature verification failed'); } @@ -148,11 +158,10 @@ public static function decode(string $jwt, Key|array|ArrayAccess $keyOrKeyArray) /** * Converts and signs a PHP object or array into a JWT string. * - * @param array $payload PHP array - * @param string|OpenSSLAsymmetricKey $key The secret key. - * If the algorithm used is asymmetric, this is the private key - * @param string $keyId - * @param array $head An array with header elements to attach + * @param array $payload PHP array + * @param string|OpenSSLAsymmetricKey|OpenSSLCertificate|array $key The secret key. + * @param string $keyId + * @param array $head An array with header elements to attach * * @return string A signed JWT * @@ -161,7 +170,7 @@ public static function decode(string $jwt, Key|array|ArrayAccess $keyOrKeyArray) */ public static function encode( array $payload, - string|OpenSSLAsymmetricKey $key, + string|OpenSSLAsymmetricKey|OpenSSLCertificate|array $key, string $alg, string $keyId = null, array $head = null @@ -174,8 +183,8 @@ public static function encode( $header = \array_merge($head, $header); } $segments = []; - $segments[] = static::urlsafeB64Encode(static::jsonEncode($header)); - $segments[] = static::urlsafeB64Encode(static::jsonEncode($payload)); + $segments[] = static::urlsafeB64Encode((string) static::jsonEncode($header)); + $segments[] = static::urlsafeB64Encode((string) static::jsonEncode($payload)); $signing_input = \implode('.', $segments); $signature = static::sign($signing_input, $key, $alg); @@ -187,23 +196,29 @@ public static function encode( /** * Sign a string with a given key and algorithm. * - * @param string $msg The message to sign - * @param string|OpenSSLAsymmetricKey $key The secret key. - * @param string $alg Supported algorithms are 'ES384','ES256', 'HS256', 'HS384', - * 'HS512', 'RS256', 'RS384', and 'RS512' + * @param string $msg The message to sign + * @param string|OpenSSLAsymmetricKey|OpenSSLCertificate|array $key The secret key. + * @param string $alg Supported algorithms are 'ES384','ES256', 'HS256', 'HS384', + * 'HS512', 'RS256', 'RS384', and 'RS512' * * @return string An encrypted message * * @throws DomainException Unsupported algorithm or bad key was specified */ - public static function sign(string $msg, string|OpenSSLAsymmetricKey $key, string $alg): string - { + public static function sign( + string $msg, + string|OpenSSLAsymmetricKey|OpenSSLCertificate|array $key, + string $alg + ): string { if (empty(static::$supported_algs[$alg])) { throw new DomainException('Algorithm not supported'); } list($function, $algorithm) = static::$supported_algs[$alg]; switch ($function) { case 'hash_hmac': + if (!is_string($key)) { + throw new InvalidArgumentException('key must be a string when using hmac'); + } return \hash_hmac($algorithm, $msg, $key, true); case 'openssl': $signature = ''; @@ -221,10 +236,13 @@ public static function sign(string $msg, string|OpenSSLAsymmetricKey $key, strin if (!function_exists('sodium_crypto_sign_detached')) { throw new DomainException('libsodium is not available'); } + if (!is_string($key)) { + throw new InvalidArgumentException('key must be a string when using EdDSA'); + } try { // The last non-empty line is used as the key. $lines = array_filter(explode("\n", $key)); - $key = base64_decode(end($lines)); + $key = base64_decode((string) end($lines)); return sodium_crypto_sign_detached($msg, $key); } catch (Exception $e) { throw new DomainException($e->getMessage(), 0, $e); @@ -238,10 +256,10 @@ public static function sign(string $msg, string|OpenSSLAsymmetricKey $key, strin * Verify a signature with the message, key and method. Not all methods * are symmetric, so we must have a separate verify and sign method. * - * @param string $msg The original message (header and body) - * @param string $signature The original signature - * @param string|OpenSSLAsymmetricKey $key For HS*, a string key works. for RS*, must be an instance of OpenSSLAsymmetricKey - * @param string $alg The algorithm + * @param string $msg The original message (header and body) + * @param string $signature The original signature + * @param string|OpenSSLAsymmetricKey|OpenSSLCertificate|array $keyMaterial For HS*, a string key works. for RS*, must be an instance of OpenSSLAsymmetricKey + * @param string $alg The algorithm * * @return bool * @@ -250,7 +268,7 @@ public static function sign(string $msg, string|OpenSSLAsymmetricKey $key, strin private static function verify( string $msg, string $signature, - string|OpenSSLAsymmetricKey $keyMaterial, + string|OpenSSLAsymmetricKey|OpenSSLCertificate|array $keyMaterial, string $alg ): bool { if (empty(static::$supported_algs[$alg])) { @@ -274,16 +292,22 @@ private static function verify( if (!function_exists('sodium_crypto_sign_verify_detached')) { throw new DomainException('libsodium is not available'); } + if (!is_string($keyMaterial)) { + throw new InvalidArgumentException('key must be a string when using EdDSA'); + } try { // The last non-empty line is used as the key. $lines = array_filter(explode("\n", $keyMaterial)); - $key = base64_decode(end($lines)); + $key = base64_decode((string) end($lines)); return sodium_crypto_sign_verify_detached($signature, $msg, $key); } catch (Exception $e) { throw new DomainException($e->getMessage(), 0, $e); } case 'hash_hmac': default: + if (!is_string($keyMaterial)) { + throw new InvalidArgumentException('key must be a string when using hmac'); + } $hash = \hash_hmac($algorithm, $msg, $keyMaterial, true); return self::constantTimeEquals($hash, $signature); } @@ -303,7 +327,7 @@ public static function jsonDecode(string $input): mixed $obj = \json_decode($input, false, 512, JSON_BIGINT_AS_STRING); if ($errno = \json_last_error()) { - static::handleJsonError($errno); + self::handleJsonError($errno); } elseif ($obj === null && $input !== 'null') { throw new DomainException('Null result with non-null input'); } @@ -313,13 +337,13 @@ public static function jsonDecode(string $input): mixed /** * Encode a PHP array into a JSON string. * - * @param array $input A PHP array + * @param array $input A PHP array * - * @return string JSON representation of the PHP array + * @return string|false JSON representation of the PHP array * * @throws DomainException Provided object could not be encoded to valid JSON */ - public static function jsonEncode(array $input): string + public static function jsonEncode(array $input): string|false { if (PHP_VERSION_ID >= 50400) { $json = \json_encode($input, \JSON_UNESCAPED_SLASHES); @@ -328,7 +352,7 @@ public static function jsonEncode(array $input): string $json = \json_encode($input); } if ($errno = \json_last_error()) { - static::handleJsonError($errno); + self::handleJsonError($errno); } elseif ($json === 'null' && $input !== null) { throw new DomainException('Null result with non-null input'); } @@ -342,7 +366,7 @@ public static function jsonEncode(array $input): string * * @return string A decoded string */ - public static function urlsafeB64Decode(string $input): string + public static function urlsafeB64Decode(string $input): string|false { $remainder = \strlen($input) % 4; if ($remainder) { @@ -381,29 +405,22 @@ private static function getKey(Key|array|ArrayAccess $keyOrKeyArray, ?string $ki return $keyOrKeyArray; } - if (is_array($keyOrKeyArray) || $keyOrKeyArray instanceof ArrayAccess) { - foreach ($keyOrKeyArray as $keyId => $key) { - if (!$key instanceof Key) { - throw new TypeError( - '$keyOrKeyArray must be an instance of Firebase\JWT\Key key or an ' - . 'array of Firebase\JWT\Key keys' - ); - } - } - if (!isset($kid)) { - throw new UnexpectedValueException('"kid" empty, unable to lookup correct key'); - } - if (!isset($keyOrKeyArray[$kid])) { - throw new UnexpectedValueException('"kid" invalid, unable to lookup correct key'); + foreach ($keyOrKeyArray as $keyId => $key) { + if (!$key instanceof Key) { + throw new TypeError( + '$keyOrKeyArray must be an instance of Firebase\JWT\Key key or an ' + . 'array of Firebase\JWT\Key keys' + ); } - - return $keyOrKeyArray[$kid]; + } + if (!isset($kid)) { + throw new UnexpectedValueException('"kid" empty, unable to lookup correct key'); + } + if (!isset($keyOrKeyArray[$kid])) { + throw new UnexpectedValueException('"kid" invalid, unable to lookup correct key'); } - throw new UnexpectedValueException( - '$keyOrKeyArray must be an instance of Firebase\JWT\Key key or an ' - . 'array of Firebase\JWT\Key keys' - ); + return $keyOrKeyArray[$kid]; } /** @@ -416,13 +433,13 @@ public static function constantTimeEquals(string $left, string $right): bool if (\function_exists('hash_equals')) { return \hash_equals($left, $right); } - $len = \min(static::safeStrlen($left), static::safeStrlen($right)); + $len = \min(self::safeStrlen($left), self::safeStrlen($right)); $status = 0; for ($i = 0; $i < $len; $i++) { $status |= (\ord($left[$i]) ^ \ord($right[$i])); } - $status |= (static::safeStrlen($left) ^ static::safeStrlen($right)); + $status |= (self::safeStrlen($left) ^ self::safeStrlen($right)); return ($status === 0); } @@ -476,7 +493,8 @@ private static function safeStrlen(string $str): int private static function signatureToDER(string $sig): string { // Separate the signature into r-value and s-value - list($r, $s) = \str_split($sig, (int) (\strlen($sig) / 2)); + $length = max(1, (int) (\strlen($sig) / 2)); + list($r, $s) = \str_split($sig, $length > 0 ? $length : 1); // Trim leading zeros $r = \ltrim($r, "\x00"); @@ -556,7 +574,7 @@ private static function signatureFromDER(string $der, int $keySize): string * @param int $offset the offset of the data stream containing the object * to decode * - * @return array [$offset, $data] the new offset and the decoded object + * @return array{int, string|null} the new offset and the decoded object */ private static function readDER(string $der, int $offset = 0): array { diff --git a/Jwt/src/Key.php b/Jwt/src/Key.php index 286a3143e87..2f648dec596 100644 --- a/Jwt/src/Key.php +++ b/Jwt/src/Key.php @@ -3,26 +3,20 @@ namespace Firebase\JWT; use OpenSSLAsymmetricKey; +use OpenSSLCertificate; use TypeError; use InvalidArgumentException; class Key { /** - * @param string|OpenSSLAsymmetricKey $keyMaterial + * @param string|OpenSSLAsymmetricKey|OpenSSLCertificate|array $keyMaterial * @param string $algorithm */ public function __construct( - private string|OpenSSLAsymmetricKey $keyMaterial, + private string|OpenSSLAsymmetricKey|OpenSSLCertificate|array $keyMaterial, private string $algorithm ) { - if ( - !is_string($keyMaterial) - && !$keyMaterial instanceof OpenSSLAsymmetricKey - ) { - throw new TypeError('Key material must be a string or OpenSSLAsymmetricKey'); - } - if (empty($keyMaterial)) { throw new InvalidArgumentException('Key material must not be empty'); } @@ -43,9 +37,9 @@ public function getAlgorithm(): string } /** - * @return string|OpenSSLAsymmetricKey + * @return string|OpenSSLAsymmetricKey|OpenSSLCertificate|array */ - public function getKeyMaterial(): mixed + public function getKeyMaterial(): string|OpenSSLAsymmetricKey|OpenSSLCertificate|array { return $this->keyMaterial; } From 5d864d9a6ff0ef3c4f82680507976ef14602eefc Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Sun, 20 Mar 2022 15:59:35 -0700 Subject: [PATCH 172/282] feat: add back compatibility for >= PHP 7.1 (firebase/php-jwt#405) --- Jwt/.github/workflows/tests.yml | 2 +- Jwt/composer.json | 4 +-- Jwt/src/JWK.php | 22 +++++------- Jwt/src/JWT.php | 59 ++++++++++++++++++--------------- Jwt/src/Key.php | 28 +++++++++++++--- 5 files changed, 66 insertions(+), 49 deletions(-) diff --git a/Jwt/.github/workflows/tests.yml b/Jwt/.github/workflows/tests.yml index 816306459fa..68d4f10bb45 100644 --- a/Jwt/.github/workflows/tests.yml +++ b/Jwt/.github/workflows/tests.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php: [ "8.0", "8.1"] + php: [ "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"] name: PHP ${{matrix.php }} Unit Test steps: - uses: actions/checkout@v2 diff --git a/Jwt/composer.json b/Jwt/composer.json index 4e190ea30d7..5ef2ea2d115 100644 --- a/Jwt/composer.json +++ b/Jwt/composer.json @@ -20,7 +20,7 @@ ], "license": "BSD-3-Clause", "require": { - "php": "^8.0" + "php": "^7.1||^8.0" }, "suggest": { "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" @@ -31,6 +31,6 @@ } }, "require-dev": { - "phpunit/phpunit": "^9.5" + "phpunit/phpunit": "^7.5||9.5" } } diff --git a/Jwt/src/JWK.php b/Jwt/src/JWK.php index d5ad93a89a7..dbc446e601d 100644 --- a/Jwt/src/JWK.php +++ b/Jwt/src/JWK.php @@ -128,24 +128,18 @@ private static function createPemFromModulusAndExponent( string $n, string $e ): string { - if (false === ($modulus = JWT::urlsafeB64Decode($n))) { - throw new UnexpectedValueException('Invalid JWK encoding'); - } - if (false === ($publicExponent = JWT::urlsafeB64Decode($e))) { - throw new UnexpectedValueException('Invalid header encoding'); - } + $mod = JWT::urlsafeB64Decode($n); + $exp = JWT::urlsafeB64Decode($e); - $components = [ - 'modulus' => \pack('Ca*a*', 2, self::encodeLength(\strlen($modulus)), $modulus), - 'publicExponent' => \pack('Ca*a*', 2, self::encodeLength(\strlen($publicExponent)), $publicExponent) - ]; + $modulus = \pack('Ca*a*', 2, self::encodeLength(\strlen($mod)), $mod); + $publicExponent = \pack('Ca*a*', 2, self::encodeLength(\strlen($exp)), $exp); $rsaPublicKey = \pack( 'Ca*a*a*', 48, - self::encodeLength(\strlen($components['modulus']) + \strlen($components['publicExponent'])), - $components['modulus'], - $components['publicExponent'] + self::encodeLength(\strlen($modulus) + \strlen($publicExponent)), + $modulus, + $publicExponent ); // sequence(oid(1.2.840.113549.1.1.1), null)) = rsaEncryption. @@ -176,7 +170,7 @@ private static function createPemFromModulusAndExponent( * @param int $length * @return string */ - private static function encodeLength($length) + private static function encodeLength(int $length): string { if ($length <= 0x7F) { return \chr($length); diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index cf58fd2a236..f6a4772c744 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -36,13 +36,15 @@ class JWT * When checking nbf, iat or expiration times, * we want to provide some extra leeway time to * account for clock skew. + * + * @var int */ - public static int $leeway = 0; + public static $leeway = 0; /** * @var array */ - public static array $supported_algs = [ + public static $supported_algs = [ 'ES384' => ['openssl', 'SHA384'], 'ES256' => ['openssl', 'SHA256'], 'HS256' => ['hash_hmac', 'SHA256'], @@ -77,8 +79,10 @@ class JWT * @uses jsonDecode * @uses urlsafeB64Decode */ - public static function decode(string $jwt, Key|array|ArrayAccess $keyOrKeyArray): stdClass - { + public static function decode( + string $jwt, + $keyOrKeyArray + ): stdClass { // Validate JWT $timestamp = \time(); @@ -90,24 +94,18 @@ public static function decode(string $jwt, Key|array|ArrayAccess $keyOrKeyArray) throw new UnexpectedValueException('Wrong number of segments'); } list($headb64, $bodyb64, $cryptob64) = $tks; - if (false === ($headerRaw = static::urlsafeB64Decode($headb64))) { - throw new UnexpectedValueException('Invalid header encoding'); - } + $headerRaw = static::urlsafeB64Decode($headb64); if (null === ($header = static::jsonDecode($headerRaw))) { throw new UnexpectedValueException('Invalid header encoding'); } - if (false === ($payloadRaw = static::urlsafeB64Decode($bodyb64))) { - throw new UnexpectedValueException('Invalid claims encoding'); - } + $payloadRaw = static::urlsafeB64Decode($bodyb64); if (null === ($payload = static::jsonDecode($payloadRaw))) { throw new UnexpectedValueException('Invalid claims encoding'); } if (!$payload instanceof stdClass) { throw new UnexpectedValueException('Payload must be a JSON object'); } - if (false === ($sig = static::urlsafeB64Decode($cryptob64))) { - throw new UnexpectedValueException('Invalid signature encoding'); - } + $sig = static::urlsafeB64Decode($cryptob64); if (empty($header->alg)) { throw new UnexpectedValueException('Empty algorithm'); } @@ -159,7 +157,7 @@ public static function decode(string $jwt, Key|array|ArrayAccess $keyOrKeyArray) * Converts and signs a PHP object or array into a JWT string. * * @param array $payload PHP array - * @param string|OpenSSLAsymmetricKey|OpenSSLCertificate|array $key The secret key. + * @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $key The secret key. * @param string $keyId * @param array $head An array with header elements to attach * @@ -170,7 +168,7 @@ public static function decode(string $jwt, Key|array|ArrayAccess $keyOrKeyArray) */ public static function encode( array $payload, - string|OpenSSLAsymmetricKey|OpenSSLCertificate|array $key, + $key, string $alg, string $keyId = null, array $head = null @@ -197,7 +195,7 @@ public static function encode( * Sign a string with a given key and algorithm. * * @param string $msg The message to sign - * @param string|OpenSSLAsymmetricKey|OpenSSLCertificate|array $key The secret key. + * @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $key The secret key. * @param string $alg Supported algorithms are 'ES384','ES256', 'HS256', 'HS384', * 'HS512', 'RS256', 'RS384', and 'RS512' * @@ -207,7 +205,7 @@ public static function encode( */ public static function sign( string $msg, - string|OpenSSLAsymmetricKey|OpenSSLCertificate|array $key, + $key, string $alg ): string { if (empty(static::$supported_algs[$alg])) { @@ -222,7 +220,7 @@ public static function sign( return \hash_hmac($algorithm, $msg, $key, true); case 'openssl': $signature = ''; - $success = \openssl_sign($msg, $signature, $key, $algorithm); + $success = \openssl_sign($msg, $signature, $key, $algorithm); // @phpstan-ignore-line if (!$success) { throw new DomainException("OpenSSL unable to sign data"); } @@ -258,7 +256,7 @@ public static function sign( * * @param string $msg The original message (header and body) * @param string $signature The original signature - * @param string|OpenSSLAsymmetricKey|OpenSSLCertificate|array $keyMaterial For HS*, a string key works. for RS*, must be an instance of OpenSSLAsymmetricKey + * @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $keyMaterial For HS*, a string key works. for RS*, must be an instance of OpenSSLAsymmetricKey * @param string $alg The algorithm * * @return bool @@ -268,7 +266,7 @@ public static function sign( private static function verify( string $msg, string $signature, - string|OpenSSLAsymmetricKey|OpenSSLCertificate|array $keyMaterial, + $keyMaterial, string $alg ): bool { if (empty(static::$supported_algs[$alg])) { @@ -278,7 +276,7 @@ private static function verify( list($function, $algorithm) = static::$supported_algs[$alg]; switch ($function) { case 'openssl': - $success = \openssl_verify($msg, $signature, $keyMaterial, $algorithm); + $success = \openssl_verify($msg, $signature, $keyMaterial, $algorithm); // @phpstan-ignore-line if ($success === 1) { return true; } elseif ($success === 0) { @@ -322,7 +320,7 @@ private static function verify( * * @throws DomainException Provided string was invalid JSON */ - public static function jsonDecode(string $input): mixed + public static function jsonDecode(string $input) { $obj = \json_decode($input, false, 512, JSON_BIGINT_AS_STRING); @@ -339,11 +337,11 @@ public static function jsonDecode(string $input): mixed * * @param array $input A PHP array * - * @return string|false JSON representation of the PHP array + * @return string JSON representation of the PHP array * * @throws DomainException Provided object could not be encoded to valid JSON */ - public static function jsonEncode(array $input): string|false + public static function jsonEncode(array $input): string { if (PHP_VERSION_ID >= 50400) { $json = \json_encode($input, \JSON_UNESCAPED_SLASHES); @@ -356,6 +354,9 @@ public static function jsonEncode(array $input): string|false } elseif ($json === 'null' && $input !== null) { throw new DomainException('Null result with non-null input'); } + if ($json === false) { + throw new DomainException('Provided object could not be encoded to valid JSON'); + } return $json; } @@ -365,8 +366,10 @@ public static function jsonEncode(array $input): string|false * @param string $input A Base64 encoded string * * @return string A decoded string + * + * @throws InvalidArgumentException invalid base64 characters */ - public static function urlsafeB64Decode(string $input): string|false + public static function urlsafeB64Decode(string $input): string { $remainder = \strlen($input) % 4; if ($remainder) { @@ -399,8 +402,10 @@ public static function urlsafeB64Encode(string $input): string * * @return Key */ - private static function getKey(Key|array|ArrayAccess $keyOrKeyArray, ?string $kid): Key - { + private static function getKey( + $keyOrKeyArray, + ?string $kid + ): Key { if ($keyOrKeyArray instanceof Key) { return $keyOrKeyArray; } diff --git a/Jwt/src/Key.php b/Jwt/src/Key.php index 2f648dec596..b09ad190cd6 100644 --- a/Jwt/src/Key.php +++ b/Jwt/src/Key.php @@ -9,14 +9,28 @@ class Key { + /** @var string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate */ + private $keyMaterial; + /** @var string */ + private $algorithm; + /** - * @param string|OpenSSLAsymmetricKey|OpenSSLCertificate|array $keyMaterial + * @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $keyMaterial * @param string $algorithm */ public function __construct( - private string|OpenSSLAsymmetricKey|OpenSSLCertificate|array $keyMaterial, - private string $algorithm + $keyMaterial, + string $algorithm ) { + if ( + !is_string($keyMaterial) + && !$keyMaterial instanceof OpenSSLAsymmetricKey + && !$keyMaterial instanceof OpenSSLCertificate + && !is_resource($keyMaterial) + ) { + throw new TypeError('Key material must be a string, resource, or OpenSSLAsymmetricKey'); + } + if (empty($keyMaterial)) { throw new InvalidArgumentException('Key material must not be empty'); } @@ -24,6 +38,10 @@ public function __construct( if (empty($algorithm)) { throw new InvalidArgumentException('Algorithm must not be empty'); } + + // TODO: Remove in PHP 8.0 in favor of class constructor property promotion + $this->keyMaterial = $keyMaterial; + $this->algorithm = $algorithm; } /** @@ -37,9 +55,9 @@ public function getAlgorithm(): string } /** - * @return string|OpenSSLAsymmetricKey|OpenSSLCertificate|array + * @return string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate */ - public function getKeyMaterial(): string|OpenSSLAsymmetricKey|OpenSSLCertificate|array + public function getKeyMaterial() { return $this->keyMaterial; } From b0b9a1810a2a3bdfe423738df8cfd19301d7b911 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 23 Mar 2022 11:12:10 -0700 Subject: [PATCH 173/282] chore: add back timestamp var for compatibility (firebase/php-jwt#412) --- Jwt/src/JWT.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index f6a4772c744..843d0ae034e 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -41,6 +41,15 @@ class JWT */ public static $leeway = 0; + /** + * Allow the current timestamp to be specified. + * Useful for fixing a value within unit testing. + * Will default to PHP time() value if null. + * + * @var ?int + */ + public static $timestamp = null; + /** * @var array */ @@ -84,7 +93,7 @@ public static function decode( $keyOrKeyArray ): stdClass { // Validate JWT - $timestamp = \time(); + $timestamp = \is_null(static::$timestamp) ? \time() : static::$timestamp; if (empty($keyOrKeyArray)) { throw new InvalidArgumentException('Key may not be empty'); From 197d9089328e5cf2c7a2989b24e37c5f59ba5cc8 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 23 Mar 2022 11:26:04 -0700 Subject: [PATCH 174/282] chore: update CHANGELOG for v6.1.0 --- Jwt/README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Jwt/README.md b/Jwt/README.md index d489c69f397..2dd7669b166 100644 --- a/Jwt/README.md +++ b/Jwt/README.md @@ -222,6 +222,11 @@ $decoded = json_decode(json_encode($decoded), true); Changelog --------- +#### 6.1.0 / 2022-03-23 + + - Drop support for PHP 5.3, 5.4, 5.5, 5.6, and 7.0 + - Add parameter typing and return types where possible + #### 6.0.0 / 2022-01-24 - **Backwards-Compatibility Breaking Changes**: See the [Release Notes](https://github.com/firebase/php-jwt/releases/tag/v6.0.0) for more information. From 8fe9b549955ff60429735ed730d69c18c1b9976a Mon Sep 17 00:00:00 2001 From: Andres Berdugo Date: Wed, 13 Apr 2022 16:32:21 -0500 Subject: [PATCH 175/282] chore: add missing docblock param for $alg (firebase/php-jwt#419) --- Jwt/src/JWT.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index 843d0ae034e..bf064e34c7f 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -167,6 +167,8 @@ public static function decode( * * @param array $payload PHP array * @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $key The secret key. + * @param string $alg Supported algorithms are 'ES384','ES256', 'HS256', 'HS384', + * 'HS512', 'RS256', 'RS384', and 'RS512' * @param string $keyId * @param array $head An array with header elements to attach * From 4e69c0b36f90c17d15d4c873057261fc5c26e6ce Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 13 Apr 2022 18:27:14 -0500 Subject: [PATCH 176/282] fix: add flag to force object (firebase/php-jwt#416) --- Jwt/src/JWT.php | 2 +- Jwt/tests/JWTTest.php | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index bf064e34c7f..67514b295d2 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -355,7 +355,7 @@ public static function jsonDecode(string $input) public static function jsonEncode(array $input): string { if (PHP_VERSION_ID >= 50400) { - $json = \json_encode($input, \JSON_UNESCAPED_SLASHES); + $json = \json_encode($input, \JSON_UNESCAPED_SLASHES|\JSON_FORCE_OBJECT); } else { // PHP 5.3 only $json = \json_encode($input); diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index 8b23ad6b4bc..e1984b3473c 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -321,6 +321,15 @@ public function testRSEncodeDecodeWithPassphrase() $this->assertEquals($decoded, $expected); } + public function testDecodesEmptyArrayAsObject() + { + $key = 'yma6Hq4XQegCVND8ef23OYgxSrC3IKqk'; + $payload = []; + $jwt = JWT::encode($payload, $key, 'HS256'); + $decoded = JWT::decode($jwt, new Key($key, 'HS256')); + $this->assertEquals((object) $payload, $decoded); + } + /** * @runInSeparateProcess * @dataProvider provideEncodeDecode From 6058795bd7c1cdb3757963feaf8b0e39bc8fa85f Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Thu, 21 Apr 2022 08:37:18 -0600 Subject: [PATCH 177/282] fix: revert add flag to force object (firebase/php-jwt#420) --- Jwt/src/JWT.php | 6 +++++- Jwt/tests/JWTTest.php | 9 +++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index 67514b295d2..57d92ba859f 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -111,6 +111,10 @@ public static function decode( if (null === ($payload = static::jsonDecode($payloadRaw))) { throw new UnexpectedValueException('Invalid claims encoding'); } + if (is_array($payload)) { + // prevent PHP Fatal Error in edge-cases when payload is empty array + $payload = (object) $payload; + } if (!$payload instanceof stdClass) { throw new UnexpectedValueException('Payload must be a JSON object'); } @@ -355,7 +359,7 @@ public static function jsonDecode(string $input) public static function jsonEncode(array $input): string { if (PHP_VERSION_ID >= 50400) { - $json = \json_encode($input, \JSON_UNESCAPED_SLASHES|\JSON_FORCE_OBJECT); + $json = \json_encode($input, \JSON_UNESCAPED_SLASHES); } else { // PHP 5.3 only $json = \json_encode($input); diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index e1984b3473c..191e3d2cdf3 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -330,6 +330,15 @@ public function testDecodesEmptyArrayAsObject() $this->assertEquals((object) $payload, $decoded); } + public function testDecodesArraysInJWTAsArray() + { + $key = 'yma6Hq4XQegCVND8ef23OYgxSrC3IKqk'; + $payload = ['foo' => [1,2,3]]; + $jwt = JWT::encode($payload, $key, 'HS256'); + $decoded = JWT::decode($jwt, new Key($key, 'HS256')); + $this->assertEquals($payload['foo'], $decoded->foo); + } + /** * @runInSeparateProcess * @dataProvider provideEncodeDecode From 2b1b86226395a82000568c99638fb3251dda6bd8 Mon Sep 17 00:00:00 2001 From: Martin Krisell Date: Sat, 23 Apr 2022 19:36:00 +0200 Subject: [PATCH 178/282] chore: styles for short array and single quotes (firebase/php-jwt#423) --- Jwt/README.md | 50 +++++++++++++++++++++---------------------- Jwt/src/JWT.php | 2 +- Jwt/tests/JWKTest.php | 4 ++-- Jwt/tests/JWTTest.php | 44 ++++++++++++++++++------------------- 4 files changed, 50 insertions(+), 50 deletions(-) diff --git a/Jwt/README.md b/Jwt/README.md index 2dd7669b166..65e8fc18d85 100644 --- a/Jwt/README.md +++ b/Jwt/README.md @@ -29,13 +29,13 @@ Example use Firebase\JWT\JWT; use Firebase\JWT\Key; -$key = "example_key"; -$payload = array( - "iss" => "http://example.org", - "aud" => "http://example.com", - "iat" => 1356999524, - "nbf" => 1357000000 -); +$key = 'example_key'; +$payload = [ + 'iss' => 'http://example.org', + 'aud' => 'http://example.com', + 'iat' => 1356999524, + 'nbf' => 1357000000 +]; /** * IMPORTANT: @@ -98,12 +98,12 @@ ehde/zUxo6UvS7UrBQIDAQAB -----END PUBLIC KEY----- EOD; -$payload = array( - "iss" => "example.org", - "aud" => "example.com", - "iat" => 1356999524, - "nbf" => 1357000000 -); +$payload = [ + 'iss' => 'example.org', + 'aud' => 'example.com', + 'iat' => 1356999524, + 'nbf' => 1357000000 +]; $jwt = JWT::encode($payload, $privateKey, 'RS256'); echo "Encode:\n" . print_r($jwt, true) . "\n"; @@ -139,12 +139,12 @@ $privateKey = openssl_pkey_get_private( $passphrase ); -$payload = array( - "iss" => "example.org", - "aud" => "example.com", - "iat" => 1356999524, - "nbf" => 1357000000 -); +$payload = [ + 'iss' => 'example.org', + 'aud' => 'example.com', + 'iat' => 1356999524, + 'nbf' => 1357000000 +]; $jwt = JWT::encode($payload, $privateKey, 'RS256'); echo "Encode:\n" . print_r($jwt, true) . "\n"; @@ -173,12 +173,12 @@ $privateKey = base64_encode(sodium_crypto_sign_secretkey($keyPair)); $publicKey = base64_encode(sodium_crypto_sign_publickey($keyPair)); -$payload = array( - "iss" => "example.org", - "aud" => "example.com", - "iat" => 1356999524, - "nbf" => 1357000000 -); +$payload = [ + 'iss' => 'example.org', + 'aud' => 'example.com', + 'iat' => 1356999524, + 'nbf' => 1357000000 +]; $jwt = JWT::encode($payload, $privateKey, 'EdDSA'); echo "Encode:\n" . print_r($jwt, true) . "\n"; diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index 57d92ba859f..7db16c4bc60 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -237,7 +237,7 @@ public static function sign( $signature = ''; $success = \openssl_sign($msg, $signature, $key, $algorithm); // @phpstan-ignore-line if (!$success) { - throw new DomainException("OpenSSL unable to sign data"); + throw new DomainException('OpenSSL unable to sign data'); } if ($alg === 'ES256') { $signature = self::signatureFromDER($signature, 256); diff --git a/Jwt/tests/JWKTest.php b/Jwt/tests/JWKTest.php index 17dd4a623fd..29d2356a105 100644 --- a/Jwt/tests/JWKTest.php +++ b/Jwt/tests/JWKTest.php @@ -125,7 +125,7 @@ public function testDecodeByJwkKeySet() $result = JWT::decode($msg, self::$keys); - $this->assertEquals("foo", $result->sub); + $this->assertEquals('foo', $result->sub); } /** @@ -139,6 +139,6 @@ public function testDecodeByMultiJwkKeySet() $result = JWT::decode($msg, self::$keys); - $this->assertEquals("bar", $result->sub); + $this->assertEquals('bar', $result->sub); } } diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index 191e3d2cdf3..98562ca1338 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -36,8 +36,8 @@ public function testExpiredToken() { $this->expectException(ExpiredException::class); $payload = [ - "message" => "abc", - "exp" => time() - 20]; // time in the past + 'message' => 'abc', + 'exp' => time() - 20]; // time in the past $encoded = JWT::encode($payload, 'my_key', 'HS256'); JWT::decode($encoded, new Key('my_key', 'HS256')); } @@ -46,7 +46,7 @@ public function testBeforeValidTokenWithNbf() { $this->expectException(BeforeValidException::class); $payload = [ - "message" => "abc", + 'message' => 'abc', "nbf" => time() + 20]; // time in the future $encoded = JWT::encode($payload, 'my_key', 'HS256'); JWT::decode($encoded, new Key('my_key', 'HS256')); @@ -56,7 +56,7 @@ public function testBeforeValidTokenWithIat() { $this->expectException(BeforeValidException::class); $payload = [ - "message" => "abc", + 'message' => 'abc', "iat" => time() + 20]; // time in the future $encoded = JWT::encode($payload, 'my_key', 'HS256'); JWT::decode($encoded, new Key('my_key', 'HS256')); @@ -65,8 +65,8 @@ public function testBeforeValidTokenWithIat() public function testValidToken() { $payload = [ - "message" => "abc", - "exp" => time() + JWT::$leeway + 20]; // time in the future + 'message' => 'abc', + 'exp' => time() + JWT::$leeway + 20]; // time in the future $encoded = JWT::encode($payload, 'my_key', 'HS256'); $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); $this->assertEquals($decoded->message, 'abc'); @@ -76,8 +76,8 @@ public function testValidTokenWithLeeway() { JWT::$leeway = 60; $payload = [ - "message" => "abc", - "exp" => time() - 20]; // time in the past + 'message' => 'abc', + 'exp' => time() - 20]; // time in the past $encoded = JWT::encode($payload, 'my_key', 'HS256'); $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); $this->assertEquals($decoded->message, 'abc'); @@ -88,8 +88,8 @@ public function testExpiredTokenWithLeeway() { JWT::$leeway = 60; $payload = [ - "message" => "abc", - "exp" => time() - 70]; // time far in the past + 'message' => 'abc', + 'exp' => time() - 70]; // time far in the past $this->expectException(ExpiredException::class); $encoded = JWT::encode($payload, 'my_key', 'HS256'); $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); @@ -100,9 +100,9 @@ public function testExpiredTokenWithLeeway() public function testValidTokenWithNbf() { $payload = [ - "message" => "abc", + 'message' => 'abc', "iat" => time(), - "exp" => time() + 20, // time in the future + 'exp' => time() + 20, // time in the future "nbf" => time() - 20]; $encoded = JWT::encode($payload, 'my_key', 'HS256'); $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); @@ -113,7 +113,7 @@ public function testValidTokenWithNbfLeeway() { JWT::$leeway = 60; $payload = [ - "message" => "abc", + 'message' => 'abc', "nbf" => time() + 20]; // not before in near (leeway) future $encoded = JWT::encode($payload, 'my_key', 'HS256'); $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); @@ -125,7 +125,7 @@ public function testInvalidTokenWithNbfLeeway() { JWT::$leeway = 60; $payload = [ - "message" => "abc", + 'message' => 'abc', "nbf" => time() + 65]; // not before too far in future $encoded = JWT::encode($payload, 'my_key', 'HS256'); $this->expectException(BeforeValidException::class); @@ -137,7 +137,7 @@ public function testValidTokenWithIatLeeway() { JWT::$leeway = 60; $payload = [ - "message" => "abc", + 'message' => 'abc', "iat" => time() + 20]; // issued in near (leeway) future $encoded = JWT::encode($payload, 'my_key', 'HS256'); $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); @@ -149,7 +149,7 @@ public function testInvalidTokenWithIatLeeway() { JWT::$leeway = 60; $payload = [ - "message" => "abc", + 'message' => 'abc', "iat" => time() + 65]; // issued too far in future $encoded = JWT::encode($payload, 'my_key', 'HS256'); $this->expectException(BeforeValidException::class); @@ -160,8 +160,8 @@ public function testInvalidTokenWithIatLeeway() public function testInvalidToken() { $payload = [ - "message" => "abc", - "exp" => time() + 20]; // time in the future + 'message' => 'abc', + 'exp' => time() + 20]; // time in the future $encoded = JWT::encode($payload, 'my_key', 'HS256'); $this->expectException(SignatureInvalidException::class); JWT::decode($encoded, new Key('my_key2', 'HS256')); @@ -170,8 +170,8 @@ public function testInvalidToken() public function testNullKeyFails() { $payload = [ - "message" => "abc", - "exp" => time() + JWT::$leeway + 20]; // time in the future + 'message' => 'abc', + 'exp' => time() + JWT::$leeway + 20]; // time in the future $encoded = JWT::encode($payload, 'my_key', 'HS256'); $this->expectException(TypeError::class); JWT::decode($encoded, new Key(null, 'HS256')); @@ -180,8 +180,8 @@ public function testNullKeyFails() public function testEmptyKeyFails() { $payload = [ - "message" => "abc", - "exp" => time() + JWT::$leeway + 20]; // time in the future + 'message' => 'abc', + 'exp' => time() + JWT::$leeway + 20]; // time in the future $encoded = JWT::encode($payload, 'my_key', 'HS256'); $this->expectException(InvalidArgumentException::class); JWT::decode($encoded, new Key('', 'HS256')); From 57d6035eec9322ca05185fbb5b5e083048ac4107 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Tue, 26 Apr 2022 10:52:40 -0600 Subject: [PATCH 179/282] chore: add cs config, fix git-attributes, removed unused entrypoint.sh (firebase/php-jwt#424) --- Jwt/.gitattributes | 7 +++-- Jwt/.github/actions/entrypoint.sh | 21 ------------- Jwt/.github/workflows/tests.yml | 5 ++- Jwt/.gitignore | 1 + Jwt/.php-cs-fixer.dist.php | 24 ++++++++++++++ Jwt/src/JWT.php | 19 ++++++----- Jwt/src/Key.php | 6 ++-- Jwt/tests/JWKTest.php | 6 ++-- Jwt/tests/JWTTest.php | 52 ++++++++++++++++++++----------- 9 files changed, 79 insertions(+), 62 deletions(-) delete mode 100755 Jwt/.github/actions/entrypoint.sh create mode 100644 Jwt/.php-cs-fixer.dist.php diff --git a/Jwt/.gitattributes b/Jwt/.gitattributes index 6d63e560e94..d5d535d9a5f 100644 --- a/Jwt/.gitattributes +++ b/Jwt/.gitattributes @@ -1,8 +1,9 @@ * text=auto -/tests export-ignore /.gitattributes export-ignore /.gitignore export-ignore -/.travis.yml export-ignore -/phpunit.xml.dist export-ignore /.github export-ignore +/.php-cs-fixer.dist.php export-ignore +/phpstan.neon.dist export-ignore +/phpunit.xml.dist export-ignore +/tests export-ignore diff --git a/Jwt/.github/actions/entrypoint.sh b/Jwt/.github/actions/entrypoint.sh deleted file mode 100755 index 40402bc8595..00000000000 --- a/Jwt/.github/actions/entrypoint.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/sh -l - -apt-get update && \ -apt-get install -y --no-install-recommends \ - git \ - zip \ - curl \ - ca-certificates \ - unzip \ - wget - -curl --silent --show-error https://getcomposer.org/installer | php -php composer.phar self-update - -echo "---Installing dependencies ---" - -# Add compatiblity for libsodium with older versions of PHP -php composer.phar require --dev --with-dependencies paragonie/sodium_compat - -echo "---Running unit tests ---" -vendor/bin/phpunit diff --git a/Jwt/.github/workflows/tests.yml b/Jwt/.github/workflows/tests.yml index 68d4f10bb45..fd10d13b6de 100644 --- a/Jwt/.github/workflows/tests.yml +++ b/Jwt/.github/workflows/tests.yml @@ -38,9 +38,8 @@ jobs: php-version: "8.0" - name: Run Script run: | - composer require friendsofphp/php-cs-fixer - vendor/bin/php-cs-fixer fix --diff --dry-run . - vendor/bin/php-cs-fixer fix --rules=native_function_invocation --allow-risky=yes --diff src + composer global require friendsofphp/php-cs-fixer + ~/.composer/vendor/bin/php-cs-fixer fix --diff --dry-run --allow-risky=yes . staticanalysis: runs-on: ubuntu-latest diff --git a/Jwt/.gitignore b/Jwt/.gitignore index b22842cb5b1..f720fb76c83 100644 --- a/Jwt/.gitignore +++ b/Jwt/.gitignore @@ -4,3 +4,4 @@ phpunit.phar.asc composer.phar composer.lock .phpunit.result.cache +.php-cs-fixer.cache diff --git a/Jwt/.php-cs-fixer.dist.php b/Jwt/.php-cs-fixer.dist.php new file mode 100644 index 00000000000..fb636632fb1 --- /dev/null +++ b/Jwt/.php-cs-fixer.dist.php @@ -0,0 +1,24 @@ +setRules([ + '@PSR2' => true, + 'concat_space' => ['spacing' => 'one'], + 'no_unused_imports' => true, + 'ordered_imports' => true, + 'new_with_braces' => true, + 'method_argument_space' => false, + 'whitespace_after_comma_in_array' => true, + 'return_type_declaration' => [ + 'space_before' => 'none' + ], + 'single_quote' => true, + 'native_function_invocation' => [ + 'strict' => false + ], + ]) + ->setFinder( + PhpCsFixer\Finder::create() + ->in(__DIR__) + ) +; diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index 7db16c4bc60..98a503e4783 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -2,16 +2,15 @@ namespace Firebase\JWT; -use ArrayAccess; +use DateTime; use DomainException; use Exception; use InvalidArgumentException; use OpenSSLAsymmetricKey; use OpenSSLCertificate; +use stdClass; use TypeError; use UnexpectedValueException; -use DateTime; -use stdClass; /** * JSON Web Token implementation, based on this spec: @@ -111,7 +110,7 @@ public static function decode( if (null === ($payload = static::jsonDecode($payloadRaw))) { throw new UnexpectedValueException('Invalid claims encoding'); } - if (is_array($payload)) { + if (\is_array($payload)) { // prevent PHP Fatal Error in edge-cases when payload is empty array $payload = (object) $payload; } @@ -229,7 +228,7 @@ public static function sign( list($function, $algorithm) = static::$supported_algs[$alg]; switch ($function) { case 'hash_hmac': - if (!is_string($key)) { + if (!\is_string($key)) { throw new InvalidArgumentException('key must be a string when using hmac'); } return \hash_hmac($algorithm, $msg, $key, true); @@ -246,10 +245,10 @@ public static function sign( } return $signature; case 'sodium_crypto': - if (!function_exists('sodium_crypto_sign_detached')) { + if (!\function_exists('sodium_crypto_sign_detached')) { throw new DomainException('libsodium is not available'); } - if (!is_string($key)) { + if (!\is_string($key)) { throw new InvalidArgumentException('key must be a string when using EdDSA'); } try { @@ -302,10 +301,10 @@ private static function verify( 'OpenSSL error: ' . \openssl_error_string() ); case 'sodium_crypto': - if (!function_exists('sodium_crypto_sign_verify_detached')) { + if (!\function_exists('sodium_crypto_sign_verify_detached')) { throw new DomainException('libsodium is not available'); } - if (!is_string($keyMaterial)) { + if (!\is_string($keyMaterial)) { throw new InvalidArgumentException('key must be a string when using EdDSA'); } try { @@ -318,7 +317,7 @@ private static function verify( } case 'hash_hmac': default: - if (!is_string($keyMaterial)) { + if (!\is_string($keyMaterial)) { throw new InvalidArgumentException('key must be a string when using hmac'); } $hash = \hash_hmac($algorithm, $msg, $keyMaterial, true); diff --git a/Jwt/src/Key.php b/Jwt/src/Key.php index b09ad190cd6..00cf7f2edf3 100644 --- a/Jwt/src/Key.php +++ b/Jwt/src/Key.php @@ -2,10 +2,10 @@ namespace Firebase\JWT; +use InvalidArgumentException; use OpenSSLAsymmetricKey; use OpenSSLCertificate; use TypeError; -use InvalidArgumentException; class Key { @@ -23,10 +23,10 @@ public function __construct( string $algorithm ) { if ( - !is_string($keyMaterial) + !\is_string($keyMaterial) && !$keyMaterial instanceof OpenSSLAsymmetricKey && !$keyMaterial instanceof OpenSSLCertificate - && !is_resource($keyMaterial) + && !\is_resource($keyMaterial) ) { throw new TypeError('Key material must be a string, resource, or OpenSSLAsymmetricKey'); } diff --git a/Jwt/tests/JWKTest.php b/Jwt/tests/JWKTest.php index 29d2356a105..4167a2ba58d 100644 --- a/Jwt/tests/JWKTest.php +++ b/Jwt/tests/JWKTest.php @@ -2,8 +2,8 @@ namespace Firebase\JWT; -use PHPUnit\Framework\TestCase; use InvalidArgumentException; +use PHPUnit\Framework\TestCase; use UnexpectedValueException; class JWKTest extends TestCase @@ -69,7 +69,7 @@ public function testParseKeyWithEmptyDValue() $jwkSet['keys'][0]['d'] = null; $keys = JWK::parseKeySet($jwkSet); - $this->assertTrue(is_array($keys)); + $this->assertTrue(\is_array($keys)); } public function testParseJwkKeySet() @@ -79,7 +79,7 @@ public function testParseJwkKeySet() true ); $keys = JWK::parseKeySet($jwkSet); - $this->assertTrue(is_array($keys)); + $this->assertTrue(\is_array($keys)); $this->assertArrayHasKey('jwk1', $keys); self::$keys = $keys; } diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index 98562ca1338..58c334ed320 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -3,12 +3,12 @@ namespace Firebase\JWT; use ArrayObject; -use PHPUnit\Framework\TestCase; use DomainException; use InvalidArgumentException; +use PHPUnit\Framework\TestCase; +use stdClass; use TypeError; use UnexpectedValueException; -use stdClass; class JWTTest extends TestCase { @@ -37,7 +37,8 @@ public function testExpiredToken() $this->expectException(ExpiredException::class); $payload = [ 'message' => 'abc', - 'exp' => time() - 20]; // time in the past + 'exp' => time() - 20, // time in the past + ]; $encoded = JWT::encode($payload, 'my_key', 'HS256'); JWT::decode($encoded, new Key('my_key', 'HS256')); } @@ -47,7 +48,8 @@ public function testBeforeValidTokenWithNbf() $this->expectException(BeforeValidException::class); $payload = [ 'message' => 'abc', - "nbf" => time() + 20]; // time in the future + 'nbf' => time() + 20, // time in the future + ]; $encoded = JWT::encode($payload, 'my_key', 'HS256'); JWT::decode($encoded, new Key('my_key', 'HS256')); } @@ -57,7 +59,8 @@ public function testBeforeValidTokenWithIat() $this->expectException(BeforeValidException::class); $payload = [ 'message' => 'abc', - "iat" => time() + 20]; // time in the future + 'iat' => time() + 20, // time in the future + ]; $encoded = JWT::encode($payload, 'my_key', 'HS256'); JWT::decode($encoded, new Key('my_key', 'HS256')); } @@ -66,7 +69,8 @@ public function testValidToken() { $payload = [ 'message' => 'abc', - 'exp' => time() + JWT::$leeway + 20]; // time in the future + 'exp' => time() + JWT::$leeway + 20, // time in the future + ]; $encoded = JWT::encode($payload, 'my_key', 'HS256'); $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); $this->assertEquals($decoded->message, 'abc'); @@ -77,7 +81,8 @@ public function testValidTokenWithLeeway() JWT::$leeway = 60; $payload = [ 'message' => 'abc', - 'exp' => time() - 20]; // time in the past + 'exp' => time() - 20, // time in the past + ]; $encoded = JWT::encode($payload, 'my_key', 'HS256'); $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); $this->assertEquals($decoded->message, 'abc'); @@ -89,7 +94,8 @@ public function testExpiredTokenWithLeeway() JWT::$leeway = 60; $payload = [ 'message' => 'abc', - 'exp' => time() - 70]; // time far in the past + 'exp' => time() - 70, // time far in the past + ]; $this->expectException(ExpiredException::class); $encoded = JWT::encode($payload, 'my_key', 'HS256'); $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); @@ -101,9 +107,10 @@ public function testValidTokenWithNbf() { $payload = [ 'message' => 'abc', - "iat" => time(), + 'iat' => time(), 'exp' => time() + 20, // time in the future - "nbf" => time() - 20]; + 'nbf' => time() - 20 + ]; $encoded = JWT::encode($payload, 'my_key', 'HS256'); $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); $this->assertEquals($decoded->message, 'abc'); @@ -114,7 +121,8 @@ public function testValidTokenWithNbfLeeway() JWT::$leeway = 60; $payload = [ 'message' => 'abc', - "nbf" => time() + 20]; // not before in near (leeway) future + 'nbf' => time() + 20, // not before in near (leeway) future + ]; $encoded = JWT::encode($payload, 'my_key', 'HS256'); $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); $this->assertEquals($decoded->message, 'abc'); @@ -126,7 +134,8 @@ public function testInvalidTokenWithNbfLeeway() JWT::$leeway = 60; $payload = [ 'message' => 'abc', - "nbf" => time() + 65]; // not before too far in future + 'nbf' => time() + 65, // not before too far in future + ]; $encoded = JWT::encode($payload, 'my_key', 'HS256'); $this->expectException(BeforeValidException::class); JWT::decode($encoded, new Key('my_key', 'HS256')); @@ -138,7 +147,8 @@ public function testValidTokenWithIatLeeway() JWT::$leeway = 60; $payload = [ 'message' => 'abc', - "iat" => time() + 20]; // issued in near (leeway) future + 'iat' => time() + 20, // issued in near (leeway) future + ]; $encoded = JWT::encode($payload, 'my_key', 'HS256'); $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); $this->assertEquals($decoded->message, 'abc'); @@ -150,7 +160,8 @@ public function testInvalidTokenWithIatLeeway() JWT::$leeway = 60; $payload = [ 'message' => 'abc', - "iat" => time() + 65]; // issued too far in future + 'iat' => time() + 65, // issued too far in future + ]; $encoded = JWT::encode($payload, 'my_key', 'HS256'); $this->expectException(BeforeValidException::class); JWT::decode($encoded, new Key('my_key', 'HS256')); @@ -161,7 +172,8 @@ public function testInvalidToken() { $payload = [ 'message' => 'abc', - 'exp' => time() + 20]; // time in the future + 'exp' => time() + 20, // time in the future + ]; $encoded = JWT::encode($payload, 'my_key', 'HS256'); $this->expectException(SignatureInvalidException::class); JWT::decode($encoded, new Key('my_key2', 'HS256')); @@ -171,7 +183,8 @@ public function testNullKeyFails() { $payload = [ 'message' => 'abc', - 'exp' => time() + JWT::$leeway + 20]; // time in the future + 'exp' => time() + JWT::$leeway + 20, // time in the future + ]; $encoded = JWT::encode($payload, 'my_key', 'HS256'); $this->expectException(TypeError::class); JWT::decode($encoded, new Key(null, 'HS256')); @@ -181,7 +194,8 @@ public function testEmptyKeyFails() { $payload = [ 'message' => 'abc', - 'exp' => time() + JWT::$leeway + 20]; // time in the future + 'exp' => time() + JWT::$leeway + 20, // time in the future + ]; $encoded = JWT::encode($payload, 'my_key', 'HS256'); $this->expectException(InvalidArgumentException::class); JWT::decode($encoded, new Key('', 'HS256')); @@ -250,7 +264,7 @@ public function testInvalidSegmentCount() public function testInvalidSignatureEncoding() { - $msg = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwibmFtZSI6ImZvbyJ9.Q4Kee9E8o0Xfo4ADXvYA8t7dN_X_bU9K5w6tXuiSjlUxx"; + $msg = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwibmFtZSI6ImZvbyJ9.Q4Kee9E8o0Xfo4ADXvYA8t7dN_X_bU9K5w6tXuiSjlUxx'; $this->expectException(UnexpectedValueException::class); JWT::decode($msg, new Key('secret', 'HS256')); } @@ -333,7 +347,7 @@ public function testDecodesEmptyArrayAsObject() public function testDecodesArraysInJWTAsArray() { $key = 'yma6Hq4XQegCVND8ef23OYgxSrC3IKqk'; - $payload = ['foo' => [1,2,3]]; + $payload = ['foo' => [1, 2, 3]]; $jwt = JWT::encode($payload, $key, 'HS256'); $decoded = JWT::decode($jwt, new Key($key, 'HS256')); $this->assertEquals($payload['foo'], $decoded->foo); From 31701d42860aba29aa9b1fd8ca257add3fc3099a Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 27 Apr 2022 16:26:15 -0600 Subject: [PATCH 180/282] feat: add cached keyset (firebase/php-jwt#397) --- Jwt/.github/workflows/tests.yml | 1 + Jwt/README.md | 39 +++ Jwt/composer.json | 7 +- Jwt/phpunit.xml.dist | 2 +- Jwt/src/CachedKeySet.php | 225 +++++++++++++++ Jwt/src/JWT.php | 19 +- Jwt/tests/CachedKeySetTest.php | 481 ++++++++++++++++++++++++++++++++ Jwt/tests/bootstrap.php | 14 - 8 files changed, 761 insertions(+), 27 deletions(-) create mode 100644 Jwt/src/CachedKeySet.php create mode 100644 Jwt/tests/CachedKeySetTest.php delete mode 100644 Jwt/tests/bootstrap.php diff --git a/Jwt/.github/workflows/tests.yml b/Jwt/.github/workflows/tests.yml index fd10d13b6de..83dc88dadc6 100644 --- a/Jwt/.github/workflows/tests.yml +++ b/Jwt/.github/workflows/tests.yml @@ -52,5 +52,6 @@ jobs: php-version: '8.0' - name: Run Script run: | + composer install composer global require phpstan/phpstan ~/.composer/vendor/bin/phpstan analyse diff --git a/Jwt/README.md b/Jwt/README.md index 65e8fc18d85..42e8b6dbf4a 100644 --- a/Jwt/README.md +++ b/Jwt/README.md @@ -203,6 +203,45 @@ $jwks = ['keys' => []]; JWT::decode($payload, JWK::parseKeySet($jwks)); ``` +Using Cached Key Sets +--------------------- + +The `CachedKeySet` class can be used to fetch and cache JWKS (JSON Web Key Sets) from a public URI. +This has the following advantages: + +1. The results are cached for performance. +2. If an unrecognized key is requested, the cache is refreshed, to accomodate for key rotation. +3. If rate limiting is enabled, the JWKS URI will not make more than 10 requests a second. + +```php +use Firebase\JWT\CachedKeySet; +use Firebase\JWT\JWT; + +// The URI for the JWKS you wish to cache the results from +$jwksUri = 'https://www.gstatic.com/iap/verify/public_key-jwk'; + +// Create an HTTP client (can be any PSR-7 compatible HTTP client) +$httpClient = new GuzzleHttp\Client(); + +// Create an HTTP request factory (can be any PSR-17 compatible HTTP request factory) +$httpFactory = new GuzzleHttp\Psr\HttpFactory(); + +// Create a cache item pool (can be any PSR-6 compatible cache item pool) +$cacheItemPool = Phpfastcache\CacheManager::getInstance('files'); + +$keySet = new CachedKeySet( + $jwksUri, + $httpClient, + $httpFactory, + $cacheItemPool, + null, // $expiresAfter int seconds to set the JWKS to expire + true // $rateLimit true to enable rate limit of 10 RPS on lookup of invalid keys +); + +$jwt = 'eyJhbGci...'; // Some JWT signed by a key from the $jwkUri above +$decoded = JWT::decode($jwt, $keySet); +``` + Miscellaneous ------------- diff --git a/Jwt/composer.json b/Jwt/composer.json index 5ef2ea2d115..2a3cb2df697 100644 --- a/Jwt/composer.json +++ b/Jwt/composer.json @@ -31,6 +31,11 @@ } }, "require-dev": { - "phpunit/phpunit": "^7.5||9.5" + "guzzlehttp/guzzle": "^6.5||^7.4", + "phpspec/prophecy-phpunit": "^1.1", + "phpunit/phpunit": "^7.5||^9.5", + "psr/cache": "^1.0||^2.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0" } } diff --git a/Jwt/phpunit.xml.dist b/Jwt/phpunit.xml.dist index 092a662c86c..31195a9168a 100644 --- a/Jwt/phpunit.xml.dist +++ b/Jwt/phpunit.xml.dist @@ -8,7 +8,7 @@ convertWarningsToExceptions="true" processIsolation="false" stopOnFailure="false" - bootstrap="tests/bootstrap.php" + bootstrap="vendor/autoload.php" > diff --git a/Jwt/src/CachedKeySet.php b/Jwt/src/CachedKeySet.php new file mode 100644 index 00000000000..077dceb034f --- /dev/null +++ b/Jwt/src/CachedKeySet.php @@ -0,0 +1,225 @@ + + */ +class CachedKeySet implements ArrayAccess +{ + /** + * @var string + */ + private $jwksUri; + /** + * @var ClientInterface + */ + private $httpClient; + /** + * @var RequestFactoryInterface + */ + private $httpFactory; + /** + * @var CacheItemPoolInterface + */ + private $cache; + /** + * @var ?int + */ + private $expiresAfter; + /** + * @var ?CacheItemInterface + */ + private $cacheItem; + /** + * @var array + */ + private $keySet; + /** + * @var string + */ + private $cacheKey; + /** + * @var string + */ + private $cacheKeyPrefix = 'jwks'; + /** + * @var int + */ + private $maxKeyLength = 64; + /** + * @var bool + */ + private $rateLimit; + /** + * @var string + */ + private $rateLimitCacheKey; + /** + * @var int + */ + private $maxCallsPerMinute = 10; + + public function __construct( + string $jwksUri, + ClientInterface $httpClient, + RequestFactoryInterface $httpFactory, + CacheItemPoolInterface $cache, + int $expiresAfter = null, + bool $rateLimit = false + ) { + $this->jwksUri = $jwksUri; + $this->httpClient = $httpClient; + $this->httpFactory = $httpFactory; + $this->cache = $cache; + $this->expiresAfter = $expiresAfter; + $this->rateLimit = $rateLimit; + $this->setCacheKeys(); + } + + /** + * @param string $keyId + * @return Key + */ + public function offsetGet($keyId): Key + { + if (!$this->keyIdExists($keyId)) { + throw new OutOfBoundsException('Key ID not found'); + } + return $this->keySet[$keyId]; + } + + /** + * @param string $keyId + * @return bool + */ + public function offsetExists($keyId): bool + { + return $this->keyIdExists($keyId); + } + + /** + * @param string $offset + * @param Key $value + */ + public function offsetSet($offset, $value): void + { + throw new LogicException('Method not implemented'); + } + + /** + * @param string $offset + */ + public function offsetUnset($offset): void + { + throw new LogicException('Method not implemented'); + } + + private function keyIdExists(string $keyId): bool + { + $keySetToCache = null; + if (null === $this->keySet) { + $item = $this->getCacheItem(); + // Try to load keys from cache + if ($item->isHit()) { + // item found! Return it + $this->keySet = $item->get(); + } + } + + if (!isset($this->keySet[$keyId])) { + if ($this->rateLimitExceeded()) { + return false; + } + $request = $this->httpFactory->createRequest('get', $this->jwksUri); + $jwksResponse = $this->httpClient->sendRequest($request); + $jwks = json_decode((string) $jwksResponse->getBody(), true); + $this->keySet = $keySetToCache = JWK::parseKeySet($jwks); + + if (!isset($this->keySet[$keyId])) { + return false; + } + } + + if ($keySetToCache) { + $item = $this->getCacheItem(); + $item->set($keySetToCache); + if ($this->expiresAfter) { + $item->expiresAfter($this->expiresAfter); + } + $this->cache->save($item); + } + + return true; + } + + private function rateLimitExceeded(): bool + { + if (!$this->rateLimit) { + return false; + } + + $cacheItem = $this->cache->getItem($this->rateLimitCacheKey); + if (!$cacheItem->isHit()) { + $cacheItem->expiresAfter(1); // # of calls are cached each minute + } + + $callsPerMinute = (int) $cacheItem->get(); + if (++$callsPerMinute > $this->maxCallsPerMinute) { + return true; + } + $cacheItem->set($callsPerMinute); + $this->cache->save($cacheItem); + return false; + } + + private function getCacheItem(): CacheItemInterface + { + if (\is_null($this->cacheItem)) { + $this->cacheItem = $this->cache->getItem($this->cacheKey); + } + + return $this->cacheItem; + } + + private function setCacheKeys(): void + { + if (empty($this->jwksUri)) { + throw new RuntimeException('JWKS URI is empty'); + } + + // ensure we do not have illegal characters + $key = preg_replace('|[^a-zA-Z0-9_\.!]|', '', $this->jwksUri); + + // add prefix + $key = $this->cacheKeyPrefix . $key; + + // Hash keys if they exceed $maxKeyLength of 64 + if (\strlen($key) > $this->maxKeyLength) { + $key = substr(hash('sha256', $key), 0, $this->maxKeyLength); + } + + $this->cacheKey = $key; + + if ($this->rateLimit) { + // add prefix + $rateLimitKey = $this->cacheKeyPrefix . 'ratelimit' . $key; + + // Hash keys if they exceed $maxKeyLength of 64 + if (\strlen($rateLimitKey) > $this->maxKeyLength) { + $rateLimitKey = substr(hash('sha256', $rateLimitKey), 0, $this->maxKeyLength); + } + + $this->rateLimitCacheKey = $rateLimitKey; + } + } +} diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index 98a503e4783..9011292fd05 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -2,6 +2,7 @@ namespace Firebase\JWT; +use ArrayAccess; use DateTime; use DomainException; use Exception; @@ -9,7 +10,6 @@ use OpenSSLAsymmetricKey; use OpenSSLCertificate; use stdClass; -use TypeError; use UnexpectedValueException; /** @@ -68,7 +68,7 @@ class JWT * Decodes a JWT string into a PHP object. * * @param string $jwt The JWT - * @param Key|array $keyOrKeyArray The Key or associative array of key IDs (kid) to Key objects. + * @param Key|array $keyOrKeyArray The Key or associative array of key IDs (kid) to Key objects. * If the algorithm used is asymmetric, this is the public key * Each Key object contains an algorithm and matching key. * Supported algorithms are 'ES384','ES256', 'HS256', 'HS384', @@ -409,7 +409,7 @@ public static function urlsafeB64Encode(string $input): string /** * Determine if an algorithm has been provided for each Key * - * @param Key|array $keyOrKeyArray + * @param Key|ArrayAccess|array $keyOrKeyArray * @param string|null $kid * * @throws UnexpectedValueException @@ -424,15 +424,12 @@ private static function getKey( return $keyOrKeyArray; } - foreach ($keyOrKeyArray as $keyId => $key) { - if (!$key instanceof Key) { - throw new TypeError( - '$keyOrKeyArray must be an instance of Firebase\JWT\Key key or an ' - . 'array of Firebase\JWT\Key keys' - ); - } + if ($keyOrKeyArray instanceof CachedKeySet) { + // Skip "isset" check, as this will automatically refresh if not set + return $keyOrKeyArray[$kid]; } - if (!isset($kid)) { + + if (empty($kid)) { throw new UnexpectedValueException('"kid" empty, unable to lookup correct key'); } if (!isset($keyOrKeyArray[$kid])) { diff --git a/Jwt/tests/CachedKeySetTest.php b/Jwt/tests/CachedKeySetTest.php new file mode 100644 index 00000000000..22e1de5f5b6 --- /dev/null +++ b/Jwt/tests/CachedKeySetTest.php @@ -0,0 +1,481 @@ +expectException(RuntimeException::class); + $this->expectExceptionMessage('JWKS URI is empty'); + + $cachedKeySet = new CachedKeySet( + '', + $this->prophesize(ClientInterface::class)->reveal(), + $this->prophesize(RequestFactoryInterface::class)->reveal(), + $this->prophesize(CacheItemPoolInterface::class)->reveal() + ); + + $cachedKeySet['foo']; + } + + public function testOffsetSetThrowsException() + { + $this->expectException(LogicException::class); + $this->expectExceptionMessage('Method not implemented'); + + $cachedKeySet = new CachedKeySet( + $this->testJwksUri, + $this->prophesize(ClientInterface::class)->reveal(), + $this->prophesize(RequestFactoryInterface::class)->reveal(), + $this->prophesize(CacheItemPoolInterface::class)->reveal() + ); + + $cachedKeySet['foo'] = 'bar'; + } + + public function testOffsetUnsetThrowsException() + { + $this->expectException(LogicException::class); + $this->expectExceptionMessage('Method not implemented'); + + $cachedKeySet = new CachedKeySet( + $this->testJwksUri, + $this->prophesize(ClientInterface::class)->reveal(), + $this->prophesize(RequestFactoryInterface::class)->reveal(), + $this->prophesize(CacheItemPoolInterface::class)->reveal() + ); + + unset($cachedKeySet['foo']); + } + + public function testOutOfBoundsThrowsException() + { + $this->expectException(OutOfBoundsException::class); + $this->expectExceptionMessage('Key ID not found'); + + $cachedKeySet = new CachedKeySet( + $this->testJwksUri, + $this->getMockHttpClient($this->testJwks1), + $this->getMockHttpFactory(), + $this->getMockEmptyCache() + ); + + // keyID doesn't exist + $cachedKeySet['bar']; + } + + public function testWithExistingKeyId() + { + $cachedKeySet = new CachedKeySet( + $this->testJwksUri, + $this->getMockHttpClient($this->testJwks1), + $this->getMockHttpFactory(), + $this->getMockEmptyCache() + ); + $this->assertInstanceOf(Key::class, $cachedKeySet['foo']); + $this->assertEquals('foo', $cachedKeySet['foo']->getAlgorithm()); + } + + public function testKeyIdIsCached() + { + $cacheItem = $this->prophesize(CacheItemInterface::class); + $cacheItem->isHit() + ->willReturn(true); + $cacheItem->get() + ->willReturn(JWK::parseKeySet(json_decode($this->testJwks1, true))); + + $cache = $this->prophesize(CacheItemPoolInterface::class); + $cache->getItem($this->testJwksUriKey) + ->willReturn($cacheItem->reveal()); + $cache->save(Argument::any()) + ->willReturn(true); + + $cachedKeySet = new CachedKeySet( + $this->testJwksUri, + $this->prophesize(ClientInterface::class)->reveal(), + $this->prophesize(RequestFactoryInterface::class)->reveal(), + $cache->reveal() + ); + $this->assertInstanceOf(Key::class, $cachedKeySet['foo']); + $this->assertEquals('foo', $cachedKeySet['foo']->getAlgorithm()); + } + + public function testCachedKeyIdRefresh() + { + $cacheItem = $this->prophesize(CacheItemInterface::class); + $cacheItem->isHit() + ->shouldBeCalledOnce() + ->willReturn(true); + $cacheItem->get() + ->shouldBeCalledOnce() + ->willReturn(JWK::parseKeySet(json_decode($this->testJwks1, true))); + $cacheItem->set(Argument::any()) + ->shouldBeCalledOnce() + ->will(function () { + return $this; + }); + + $cache = $this->prophesize(CacheItemPoolInterface::class); + $cache->getItem($this->testJwksUriKey) + ->shouldBeCalledOnce() + ->willReturn($cacheItem->reveal()); + $cache->save(Argument::any()) + ->shouldBeCalledOnce() + ->willReturn(true); + + $cachedKeySet = new CachedKeySet( + $this->testJwksUri, + $this->getMockHttpClient($this->testJwks2), // updated JWK + $this->getMockHttpFactory(), + $cache->reveal() + ); + $this->assertInstanceOf(Key::class, $cachedKeySet['foo']); + $this->assertEquals('foo', $cachedKeySet['foo']->getAlgorithm()); + + $this->assertInstanceOf(Key::class, $cachedKeySet['bar']); + $this->assertEquals('bar', $cachedKeySet['bar']->getAlgorithm()); + } + + public function testCacheItemWithExpiresAfter() + { + $expiresAfter = 10; + $cacheItem = $this->prophesize(CacheItemInterface::class); + $cacheItem->isHit() + ->shouldBeCalledOnce() + ->willReturn(false); + $cacheItem->set(Argument::any()) + ->shouldBeCalledOnce() + ->will(function () { + return $this; + }); + $cacheItem->expiresAfter($expiresAfter) + ->shouldBeCalledOnce() + ->will(function () { + return $this; + }); + + $cache = $this->prophesize(CacheItemPoolInterface::class); + $cache->getItem($this->testJwksUriKey) + ->shouldBeCalledOnce() + ->willReturn($cacheItem->reveal()); + $cache->save(Argument::any()) + ->shouldBeCalledOnce(); + + $cachedKeySet = new CachedKeySet( + $this->testJwksUri, + $this->getMockHttpClient($this->testJwks1), + $this->getMockHttpFactory(), + $cache->reveal(), + $expiresAfter + ); + $this->assertInstanceOf(Key::class, $cachedKeySet['foo']); + $this->assertEquals('foo', $cachedKeySet['foo']->getAlgorithm()); + } + + public function testJwtVerify() + { + $privKey1 = file_get_contents(__DIR__ . '/data/rsa1-private.pem'); + $payload = array('sub' => 'foo', 'exp' => strtotime('+10 seconds')); + $msg = JWT::encode($payload, $privKey1, 'RS256', 'jwk1'); + + $cacheItem = $this->prophesize(CacheItemInterface::class); + $cacheItem->isHit() + ->willReturn(true); + $cacheItem->get() + ->willReturn(JWK::parseKeySet( + json_decode(file_get_contents(__DIR__ . '/data/rsa-jwkset.json'), true) + )); + + $cache = $this->prophesize(CacheItemPoolInterface::class); + $cache->getItem($this->testJwksUriKey) + ->willReturn($cacheItem->reveal()); + + $cachedKeySet = new CachedKeySet( + $this->testJwksUri, + $this->prophesize(ClientInterface::class)->reveal(), + $this->prophesize(RequestFactoryInterface::class)->reveal(), + $cache->reveal() + ); + + $result = JWT::decode($msg, $cachedKeySet); + + $this->assertEquals('foo', $result->sub); + } + + public function testRateLimit() + { + // We request the key 11 times, HTTP should only be called 10 times + $shouldBeCalledTimes = 10; + + // Instantiate the cached key set + $cachedKeySet = new CachedKeySet( + $this->testJwksUri, + $this->getMockHttpClient($this->testJwks1, $shouldBeCalledTimes), + $factory = $this->getMockHttpFactory($shouldBeCalledTimes), + new TestMemoryCacheItemPool(), + 10, // expires after seconds + true // enable rate limiting + ); + + $invalidKid = 'invalidkey'; + for ($i = 0; $i < 10; $i++) { + $this->assertFalse(isset($cachedKeySet[$invalidKid])); + } + // The 11th time does not call HTTP + $this->assertFalse(isset($cachedKeySet[$invalidKid])); + } + + /** + * @dataProvider provideFullIntegration + */ + public function testFullIntegration(string $jwkUri): void + { + if (!class_exists(\GuzzleHttp\Psr7\HttpFactory::class)) { + self::markTestSkipped('Guzzle 7 only'); + } + // Create cache and http objects + $cache = new TestMemoryCacheItemPool(); + $http = new \GuzzleHttp\Client(); + $factory = new \GuzzleHttp\Psr7\HttpFactory(); + + // Determine "kid" dynamically, because these constantly change + $response = $http->get($jwkUri); + $json = (string) $response->getBody(); + $keys = json_decode($json, true); + $kid = $keys['keys'][0]['kid'] ?? null; + $this->assertNotNull($kid); + + // Instantiate the cached key set + $cachedKeySet = new CachedKeySet( + $jwkUri, + $http, + $factory, + $cache + ); + + $this->assertArrayHasKey($kid, $cachedKeySet); + $key = $cachedKeySet[$kid]; + $this->assertInstanceOf(Key::class, $key); + $this->assertEquals($keys['keys'][0]['alg'], $key->getAlgorithm()); + } + + public function provideFullIntegration() + { + return [ + [$this->googleRsaUri], + // [$this->googleEcUri, 'LYyP2g'] + ]; + } + + private function getMockHttpClient($testJwks, int $timesCalled = 1) + { + $body = $this->prophesize('Psr\Http\Message\StreamInterface'); + $body->__toString() + ->shouldBeCalledTimes($timesCalled) + ->willReturn($testJwks); + + $response = $this->prophesize('Psr\Http\Message\ResponseInterface'); + $response->getBody() + ->shouldBeCalledTimes($timesCalled) + ->willReturn($body->reveal()); + + $http = $this->prophesize(ClientInterface::class); + $http->sendRequest(Argument::any()) + ->shouldBeCalledTimes($timesCalled) + ->willReturn($response->reveal()); + + return $http->reveal(); + } + + private function getMockHttpFactory(int $timesCalled = 1) + { + $request = $this->prophesize('Psr\Http\Message\RequestInterface'); + $factory = $this->prophesize(RequestFactoryInterface::class); + $factory->createRequest('get', $this->testJwksUri) + ->shouldBeCalledTimes($timesCalled) + ->willReturn($request->reveal()); + + return $factory->reveal(); + } + + private function getMockEmptyCache() + { + $cacheItem = $this->prophesize(CacheItemInterface::class); + $cacheItem->isHit() + ->shouldBeCalledOnce() + ->willReturn(false); + $cacheItem->set(Argument::any()) + ->will(function () { + return $this; + }); + + $cache = $this->prophesize(CacheItemPoolInterface::class); + $cache->getItem($this->testJwksUriKey) + ->shouldBeCalledOnce() + ->willReturn($cacheItem->reveal()); + $cache->save(Argument::any()) + ->willReturn(true); + + return $cache->reveal(); + } +} + +/** + * A cache item pool + */ +final class TestMemoryCacheItemPool implements CacheItemPoolInterface +{ + private $items; + private $deferredItems; + + public function getItem($key): CacheItemInterface + { + return current($this->getItems([$key])); + } + + public function getItems(array $keys = []): iterable + { + $items = []; + + foreach ($keys as $key) { + $items[$key] = $this->hasItem($key) ? clone $this->items[$key] : new TestMemoryCacheItem($key); + } + + return $items; + } + + public function hasItem($key): bool + { + return isset($this->items[$key]) && $this->items[$key]->isHit(); + } + + public function clear(): bool + { + $this->items = []; + $this->deferredItems = []; + + return true; + } + + public function deleteItem($key): bool + { + return $this->deleteItems([$key]); + } + + public function deleteItems(array $keys): bool + { + foreach ($keys as $key) { + unset($this->items[$key]); + } + + return true; + } + + public function save(CacheItemInterface $item): bool + { + $this->items[$item->getKey()] = $item; + + return true; + } + + public function saveDeferred(CacheItemInterface $item): bool + { + $this->deferredItems[$item->getKey()] = $item; + + return true; + } + + public function commit(): bool + { + foreach ($this->deferredItems as $item) { + $this->save($item); + } + + $this->deferredItems = []; + + return true; + } +} + +/** + * A cache item. + */ +final class TestMemoryCacheItem implements CacheItemInterface +{ + private $key; + private $value; + private $expiration; + private $isHit = false; + + public function __construct(string $key) + { + $this->key = $key; + } + + public function getKey(): string + { + return $this->key; + } + + public function get() + { + return $this->isHit() ? $this->value : null; + } + + public function isHit(): bool + { + if (!$this->isHit) { + return false; + } + + if ($this->expiration === null) { + return true; + } + + return $this->currentTime()->getTimestamp() < $this->expiration->getTimestamp(); + } + + public function set($value) + { + $this->isHit = true; + $this->value = $value; + + return $this; + } + + public function expiresAt($expiration) + { + $this->expiration = $expiration; + return $this; + } + + public function expiresAfter($time) + { + $this->expiration = $this->currentTime()->add(new \DateInterval("PT{$time}S")); + return $this; + } + + protected function currentTime() + { + return new \DateTime('now', new \DateTimeZone('UTC')); + } +} diff --git a/Jwt/tests/bootstrap.php b/Jwt/tests/bootstrap.php deleted file mode 100644 index 385b6706547..00000000000 --- a/Jwt/tests/bootstrap.php +++ /dev/null @@ -1,14 +0,0 @@ - Date: Fri, 13 May 2022 13:54:50 -0700 Subject: [PATCH 181/282] feat: add defaultAlg param (firebase/php-jwt#426) --- Jwt/src/CachedKeySet.php | 10 ++++++++-- Jwt/src/JWK.php | 22 +++++++++++++++------- Jwt/tests/CachedKeySetTest.php | 16 ++++++++++++++++ Jwt/tests/JWKTest.php | 12 ++++++++++++ 4 files changed, 51 insertions(+), 9 deletions(-) diff --git a/Jwt/src/CachedKeySet.php b/Jwt/src/CachedKeySet.php index 077dceb034f..f1580c92691 100644 --- a/Jwt/src/CachedKeySet.php +++ b/Jwt/src/CachedKeySet.php @@ -68,6 +68,10 @@ class CachedKeySet implements ArrayAccess * @var int */ private $maxCallsPerMinute = 10; + /** + * @var string|null + */ + private $defaultAlg; public function __construct( string $jwksUri, @@ -75,7 +79,8 @@ public function __construct( RequestFactoryInterface $httpFactory, CacheItemPoolInterface $cache, int $expiresAfter = null, - bool $rateLimit = false + bool $rateLimit = false, + string $defaultAlg = null ) { $this->jwksUri = $jwksUri; $this->httpClient = $httpClient; @@ -83,6 +88,7 @@ public function __construct( $this->cache = $cache; $this->expiresAfter = $expiresAfter; $this->rateLimit = $rateLimit; + $this->defaultAlg = $defaultAlg; $this->setCacheKeys(); } @@ -143,7 +149,7 @@ private function keyIdExists(string $keyId): bool $request = $this->httpFactory->createRequest('get', $this->jwksUri); $jwksResponse = $this->httpClient->sendRequest($request); $jwks = json_decode((string) $jwksResponse->getBody(), true); - $this->keySet = $keySetToCache = JWK::parseKeySet($jwks); + $this->keySet = $keySetToCache = JWK::parseKeySet($jwks, $this->defaultAlg); if (!isset($this->keySet[$keyId])) { return false; diff --git a/Jwt/src/JWK.php b/Jwt/src/JWK.php index dbc446e601d..7f2257013f3 100644 --- a/Jwt/src/JWK.php +++ b/Jwt/src/JWK.php @@ -24,6 +24,8 @@ class JWK * Parse a set of JWK keys * * @param array $jwks The JSON Web Key Set as an associative array + * @param string $defaultAlg The algorithm for the Key object if "alg" is not set in the + * JSON Web Key Set * * @return array An associative array of key IDs (kid) to Key objects * @@ -33,7 +35,7 @@ class JWK * * @uses parseKey */ - public static function parseKeySet(array $jwks): array + public static function parseKeySet(array $jwks, string $defaultAlg = null): array { $keys = []; @@ -47,7 +49,7 @@ public static function parseKeySet(array $jwks): array foreach ($jwks['keys'] as $k => $v) { $kid = isset($v['kid']) ? $v['kid'] : $k; - if ($key = self::parseKey($v)) { + if ($key = self::parseKey($v, $defaultAlg)) { $keys[(string) $kid] = $key; } } @@ -63,6 +65,8 @@ public static function parseKeySet(array $jwks): array * Parse a JWK key * * @param array $jwk An individual JWK + * @param string $defaultAlg The algorithm for the Key object if "alg" is not set in the + * JSON Web Key Set * * @return Key The key object for the JWK * @@ -72,7 +76,7 @@ public static function parseKeySet(array $jwks): array * * @uses createPemFromModulusAndExponent */ - public static function parseKey(array $jwk): ?Key + public static function parseKey(array $jwk, string $defaultAlg = null): ?Key { if (empty($jwk)) { throw new InvalidArgumentException('JWK must not be empty'); @@ -83,10 +87,14 @@ public static function parseKey(array $jwk): ?Key } if (!isset($jwk['alg'])) { - // The "alg" parameter is optional in a KTY, but is required for parsing in - // this library. Add it manually to your JWK array if it doesn't already exist. - // @see https://datatracker.ietf.org/doc/html/rfc7517#section-4.4 - throw new UnexpectedValueException('JWK must contain an "alg" parameter'); + if (\is_null($defaultAlg)) { + // The "alg" parameter is optional in a KTY, but an algorithm is required + // for parsing in this library. Use the $defaultAlg parameter when parsing the + // key set in order to prevent this error. + // @see https://datatracker.ietf.org/doc/html/rfc7517#section-4.4 + throw new UnexpectedValueException('JWK must contain an "alg" parameter'); + } + $jwk['alg'] = $defaultAlg; } switch ($jwk['kty']) { diff --git a/Jwt/tests/CachedKeySetTest.php b/Jwt/tests/CachedKeySetTest.php index 22e1de5f5b6..a13c80fd61d 100644 --- a/Jwt/tests/CachedKeySetTest.php +++ b/Jwt/tests/CachedKeySetTest.php @@ -18,6 +18,7 @@ class CachedKeySetTest extends TestCase private $testJwksUriKey = 'jwkshttpsjwk.uri'; private $testJwks1 = '{"keys": [{"kid":"foo","kty":"RSA","alg":"foo","n":"","e":""}]}'; private $testJwks2 = '{"keys": [{"kid":"bar","kty":"RSA","alg":"bar","n":"","e":""}]}'; + private $testJwks3 = '{"keys": [{"kid":"baz","kty":"RSA","n":"","e":""}]}'; private $googleRsaUri = 'https://www.googleapis.com/oauth2/v3/certs'; // private $googleEcUri = 'https://www.gstatic.com/iap/verify/public_key-jwk'; @@ -95,6 +96,21 @@ public function testWithExistingKeyId() $this->assertEquals('foo', $cachedKeySet['foo']->getAlgorithm()); } + public function testWithDefaultAlg() + { + $cachedKeySet = new CachedKeySet( + $this->testJwksUri, + $this->getMockHttpClient($this->testJwks3), + $this->getMockHttpFactory(), + $this->getMockEmptyCache(), + null, + false, + 'baz256' + ); + $this->assertInstanceOf(Key::class, $cachedKeySet['baz']); + $this->assertEquals('baz256', $cachedKeySet['baz']->getAlgorithm()); + } + public function testKeyIdIsCached() { $cacheItem = $this->prophesize(CacheItemInterface::class); diff --git a/Jwt/tests/JWKTest.php b/Jwt/tests/JWKTest.php index 4167a2ba58d..0bd4f63672b 100644 --- a/Jwt/tests/JWKTest.php +++ b/Jwt/tests/JWKTest.php @@ -58,6 +58,18 @@ public function testParsePrivateKeyWithoutAlg() JWK::parseKeySet($jwkSet); } + public function testParsePrivateKeyWithoutAlgWithDefaultAlgParameter() + { + $jwkSet = json_decode( + file_get_contents(__DIR__ . '/data/rsa-jwkset.json'), + true + ); + unset($jwkSet['keys'][0]['alg']); + + $jwks = JWK::parseKeySet($jwkSet, 'foo'); + $this->assertEquals('foo', $jwks['jwk1']->getAlgorithm()); + } + public function testParseKeyWithEmptyDValue() { $jwkSet = json_decode( From 91aa304914c45f41b1ffe50cffb4fe50a749f98b Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Thu, 14 Jul 2022 16:17:28 -0600 Subject: [PATCH 182/282] feat: Add ES256 support to JWK (firebase/php-jwt#399) --- Jwt/src/JWK.php | 133 ++++++++++++++++++++++++++++ Jwt/tests/JWKTest.php | 24 +++-- Jwt/tests/data/ec-jwkset.json | 22 +++++ Jwt/tests/data/ecdsa256-private.pem | 4 + 4 files changed, 178 insertions(+), 5 deletions(-) create mode 100644 Jwt/tests/data/ec-jwkset.json create mode 100644 Jwt/tests/data/ecdsa256-private.pem diff --git a/Jwt/src/JWK.php b/Jwt/src/JWK.php index 7f2257013f3..c90de4e1827 100644 --- a/Jwt/src/JWK.php +++ b/Jwt/src/JWK.php @@ -20,6 +20,16 @@ */ class JWK { + private const OID = '1.2.840.10045.2.1'; + private const ASN1_OBJECT_IDENTIFIER = 0x06; + private const ASN1_SEQUENCE = 0x10; // also defined in JWT + private const ASN1_BIT_STRING = 0x03; + private const EC_CURVES = [ + 'P-256' => '1.2.840.10045.3.1.7', // Len: 64 + // 'P-384' => '1.3.132.0.34', // Len: 96 (not yet supported) + // 'P-521' => '1.3.132.0.35', // Len: 132 (not supported) + ]; + /** * Parse a set of JWK keys * @@ -114,6 +124,26 @@ public static function parseKey(array $jwk, string $defaultAlg = null): ?Key ); } return new Key($publicKey, $jwk['alg']); + case 'EC': + if (isset($jwk['d'])) { + // The key is actually a private key + throw new UnexpectedValueException('Key data must be for a public key'); + } + + if (empty($jwk['crv'])) { + throw new UnexpectedValueException('crv not set'); + } + + if (!isset(self::EC_CURVES[$jwk['crv']])) { + throw new DomainException('Unrecognised or unsupported EC curve'); + } + + if (empty($jwk['x']) || empty($jwk['y'])) { + throw new UnexpectedValueException('x and y not set'); + } + + $publicKey = self::createPemFromCrvAndXYCoordinates($jwk['crv'], $jwk['x'], $jwk['y']); + return new Key($publicKey, $jwk['alg']); default: // Currently only RSA is supported break; @@ -122,6 +152,45 @@ public static function parseKey(array $jwk, string $defaultAlg = null): ?Key return null; } + /** + * Converts the EC JWK values to pem format. + * + * @param string $crv The EC curve (only P-256 is supported) + * @param string $x The EC x-coordinate + * @param string $y The EC y-coordinate + * + * @return string + */ + private static function createPemFromCrvAndXYCoordinates(string $crv, string $x, string $y): string + { + $pem = + self::encodeDER( + self::ASN1_SEQUENCE, + self::encodeDER( + self::ASN1_SEQUENCE, + self::encodeDER( + self::ASN1_OBJECT_IDENTIFIER, + self::encodeOID(self::OID) + ) + . self::encodeDER( + self::ASN1_OBJECT_IDENTIFIER, + self::encodeOID(self::EC_CURVES[$crv]) + ) + ) . + self::encodeDER( + self::ASN1_BIT_STRING, + chr(0x00) . chr(0x04) + . JWT::urlsafeB64Decode($x) + . JWT::urlsafeB64Decode($y) + ) + ); + + return sprintf( + "-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----\n", + wordwrap(base64_encode($pem), 64, "\n", true) + ); + } + /** * Create a public key represented in PEM format from RSA modulus and exponent information * @@ -188,4 +257,68 @@ private static function encodeLength(int $length): string return \pack('Ca*', 0x80 | \strlen($temp), $temp); } + + /** + * Encodes a value into a DER object. + * Also defined in Firebase\JWT\JWT + * + * @param int $type DER tag + * @param string $value the value to encode + * @return string the encoded object + */ + private static function encodeDER(int $type, string $value): string + { + $tag_header = 0; + if ($type === self::ASN1_SEQUENCE) { + $tag_header |= 0x20; + } + + // Type + $der = \chr($tag_header | $type); + + // Length + $der .= \chr(\strlen($value)); + + return $der . $value; + } + + /** + * Encodes a string into a DER-encoded OID. + * + * @param string $oid the OID string + * @return string the binary DER-encoded OID + */ + private static function encodeOID(string $oid): string + { + $octets = explode('.', $oid); + + // Get the first octet + $first = (int) array_shift($octets); + $second = (int) array_shift($octets); + $oid = chr($first * 40 + $second); + + // Iterate over subsequent octets + foreach ($octets as $octet) { + if ($octet == 0) { + $oid .= chr(0x00); + continue; + } + $bin = ''; + + while ($octet) { + $bin .= chr(0x80 | ($octet & 0x7f)); + $octet >>= 7; + } + $bin[0] = $bin[0] & chr(0x7f); + + // Convert to big endian if necessary + if (pack('V', 65534) == pack('L', 65534)) { + $oid .= strrev($bin); + } else { + $oid .= $bin; + } + } + + return $oid; + } } diff --git a/Jwt/tests/JWKTest.php b/Jwt/tests/JWKTest.php index 0bd4f63672b..b8c24f9887b 100644 --- a/Jwt/tests/JWKTest.php +++ b/Jwt/tests/JWKTest.php @@ -127,19 +127,33 @@ public function testDecodeByJwkKeySetTokenExpired() } /** - * @depends testParseJwkKeySet + * @dataProvider provideDecodeByJwkKeySet */ - public function testDecodeByJwkKeySet() + public function testDecodeByJwkKeySet($pemFile, $jwkFile, $alg) { - $privKey1 = file_get_contents(__DIR__ . '/data/rsa1-private.pem'); + $privKey1 = file_get_contents(__DIR__ . '/data/' . $pemFile); $payload = ['sub' => 'foo', 'exp' => strtotime('+10 seconds')]; - $msg = JWT::encode($payload, $privKey1, 'RS256', 'jwk1'); + $msg = JWT::encode($payload, $privKey1, $alg, 'jwk1'); - $result = JWT::decode($msg, self::$keys); + $jwkSet = json_decode( + file_get_contents(__DIR__ . '/data/' . $jwkFile), + true + ); + + $keys = JWK::parseKeySet($jwkSet); + $result = JWT::decode($msg, $keys); $this->assertEquals('foo', $result->sub); } + public function provideDecodeByJwkKeySet() + { + return [ + ['rsa1-private.pem', 'rsa-jwkset.json', 'RS256'], + ['ecdsa256-private.pem', 'ec-jwkset.json', 'ES256'], + ]; + } + /** * @depends testParseJwkKeySet */ diff --git a/Jwt/tests/data/ec-jwkset.json b/Jwt/tests/data/ec-jwkset.json new file mode 100644 index 00000000000..46ed8cf9586 --- /dev/null +++ b/Jwt/tests/data/ec-jwkset.json @@ -0,0 +1,22 @@ +{ + "keys": [ + { + "kty": "EC", + "use": "sig", + "crv": "P-256", + "kid": "jwk1", + "x": "ALXnvdCvbBx35J2bozBkIFHPT747KiYioLK4JquMhZU", + "y": "fAt_rGPqS95Ytwdluh4TNWTmj9xkcAbKGBRpP5kuGBk", + "alg": "ES256" + }, + { + "kty": "EC", + "use": "sig", + "crv": "P-256", + "kid": "jwk2", + "x": "mQa0q5FvxPRujxzFazQT1Mo2YJJzuKiXU3svOJ41jhw", + "y": "jAz7UwIl2oOFk06kj42ZFMOXmGMFUGjKASvyYtibCH0", + "alg": "ES256" + } + ] +} \ No newline at end of file diff --git a/Jwt/tests/data/ecdsa256-private.pem b/Jwt/tests/data/ecdsa256-private.pem new file mode 100644 index 00000000000..02b8f1b836b --- /dev/null +++ b/Jwt/tests/data/ecdsa256-private.pem @@ -0,0 +1,4 @@ +-----BEGIN PRIVATE KEY----- +MEECAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQcEJzAlAgEBBCD0KvVxLJEzRBQmcEXf +D2okKCNoUwZY8fc1/1Z4aJuJdg== +-----END PRIVATE KEY----- \ No newline at end of file From 49fbdcc422dd66b2d20442c9705f5d872899eb1a Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Thu, 14 Jul 2022 16:49:39 -0600 Subject: [PATCH 183/282] chore: fix styles and phpstan (firebase/php-jwt#440) --- Jwt/src/JWK.php | 10 +++++----- Jwt/src/JWT.php | 30 +++++++++++++++--------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/Jwt/src/JWK.php b/Jwt/src/JWK.php index c90de4e1827..a5dbc765742 100644 --- a/Jwt/src/JWK.php +++ b/Jwt/src/JWK.php @@ -179,7 +179,7 @@ private static function createPemFromCrvAndXYCoordinates(string $crv, string $x, ) . self::encodeDER( self::ASN1_BIT_STRING, - chr(0x00) . chr(0x04) + \chr(0x00) . \chr(0x04) . JWT::urlsafeB64Decode($x) . JWT::urlsafeB64Decode($y) ) @@ -295,21 +295,21 @@ private static function encodeOID(string $oid): string // Get the first octet $first = (int) array_shift($octets); $second = (int) array_shift($octets); - $oid = chr($first * 40 + $second); + $oid = \chr($first * 40 + $second); // Iterate over subsequent octets foreach ($octets as $octet) { if ($octet == 0) { - $oid .= chr(0x00); + $oid .= \chr(0x00); continue; } $bin = ''; while ($octet) { - $bin .= chr(0x80 | ($octet & 0x7f)); + $bin .= \chr(0x80 | ($octet & 0x7f)); $octet >>= 7; } - $bin[0] = $bin[0] & chr(0x7f); + $bin[0] = $bin[0] & \chr(0x7f); // Convert to big endian if necessary if (pack('V', 65534) == pack('L', 65534)) { diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index 9011292fd05..084c4a45661 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -301,20 +301,20 @@ private static function verify( 'OpenSSL error: ' . \openssl_error_string() ); case 'sodium_crypto': - if (!\function_exists('sodium_crypto_sign_verify_detached')) { - throw new DomainException('libsodium is not available'); - } - if (!\is_string($keyMaterial)) { - throw new InvalidArgumentException('key must be a string when using EdDSA'); - } - try { - // The last non-empty line is used as the key. - $lines = array_filter(explode("\n", $keyMaterial)); - $key = base64_decode((string) end($lines)); - return sodium_crypto_sign_verify_detached($signature, $msg, $key); - } catch (Exception $e) { - throw new DomainException($e->getMessage(), 0, $e); - } + if (!\function_exists('sodium_crypto_sign_verify_detached')) { + throw new DomainException('libsodium is not available'); + } + if (!\is_string($keyMaterial)) { + throw new InvalidArgumentException('key must be a string when using EdDSA'); + } + try { + // The last non-empty line is used as the key. + $lines = array_filter(explode("\n", $keyMaterial)); + $key = base64_decode((string) end($lines)); + return sodium_crypto_sign_verify_detached($signature, $msg, $key); + } catch (Exception $e) { + throw new DomainException($e->getMessage(), 0, $e); + } case 'hash_hmac': default: if (!\is_string($keyMaterial)) { @@ -510,7 +510,7 @@ private static function signatureToDER(string $sig): string { // Separate the signature into r-value and s-value $length = max(1, (int) (\strlen($sig) / 2)); - list($r, $s) = \str_split($sig, $length > 0 ? $length : 1); + list($r, $s) = \str_split($sig, $length); // Trim leading zeros $r = \ltrim($r, "\x00"); From a2cae3841c2dd0c0b0b684a99c234673a902f646 Mon Sep 17 00:00:00 2001 From: Brando Meniconi Date: Fri, 15 Jul 2022 18:09:18 +0200 Subject: [PATCH 184/282] fix: cache jwks as string in CachedKeySet (firebase/php-jwt#435) --- Jwt/src/CachedKeySet.php | 12 +++++------- Jwt/tests/CachedKeySetTest.php | 9 ++++----- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/Jwt/src/CachedKeySet.php b/Jwt/src/CachedKeySet.php index f1580c92691..e2215b3091c 100644 --- a/Jwt/src/CachedKeySet.php +++ b/Jwt/src/CachedKeySet.php @@ -132,13 +132,13 @@ public function offsetUnset($offset): void private function keyIdExists(string $keyId): bool { - $keySetToCache = null; if (null === $this->keySet) { $item = $this->getCacheItem(); // Try to load keys from cache if ($item->isHit()) { // item found! Return it - $this->keySet = $item->get(); + $jwks = $item->get(); + $this->keySet = JWK::parseKeySet(json_decode($jwks, true), $this->defaultAlg); } } @@ -148,17 +148,15 @@ private function keyIdExists(string $keyId): bool } $request = $this->httpFactory->createRequest('get', $this->jwksUri); $jwksResponse = $this->httpClient->sendRequest($request); - $jwks = json_decode((string) $jwksResponse->getBody(), true); - $this->keySet = $keySetToCache = JWK::parseKeySet($jwks, $this->defaultAlg); + $jwks = (string) $jwksResponse->getBody(); + $this->keySet = JWK::parseKeySet(json_decode($jwks, true), $this->defaultAlg); if (!isset($this->keySet[$keyId])) { return false; } - } - if ($keySetToCache) { $item = $this->getCacheItem(); - $item->set($keySetToCache); + $item->set($jwks); if ($this->expiresAfter) { $item->expiresAfter($this->expiresAfter); } diff --git a/Jwt/tests/CachedKeySetTest.php b/Jwt/tests/CachedKeySetTest.php index a13c80fd61d..9e884e6bad2 100644 --- a/Jwt/tests/CachedKeySetTest.php +++ b/Jwt/tests/CachedKeySetTest.php @@ -117,7 +117,7 @@ public function testKeyIdIsCached() $cacheItem->isHit() ->willReturn(true); $cacheItem->get() - ->willReturn(JWK::parseKeySet(json_decode($this->testJwks1, true))); + ->willReturn($this->testJwks1); $cache = $this->prophesize(CacheItemPoolInterface::class); $cache->getItem($this->testJwksUriKey) @@ -143,7 +143,7 @@ public function testCachedKeyIdRefresh() ->willReturn(true); $cacheItem->get() ->shouldBeCalledOnce() - ->willReturn(JWK::parseKeySet(json_decode($this->testJwks1, true))); + ->willReturn($this->testJwks1); $cacheItem->set(Argument::any()) ->shouldBeCalledOnce() ->will(function () { @@ -217,9 +217,8 @@ public function testJwtVerify() $cacheItem->isHit() ->willReturn(true); $cacheItem->get() - ->willReturn(JWK::parseKeySet( - json_decode(file_get_contents(__DIR__ . '/data/rsa-jwkset.json'), true) - )); + ->willReturn(file_get_contents(__DIR__ . '/data/rsa-jwkset.json') + ); $cache = $this->prophesize(CacheItemPoolInterface::class); $cache->getItem($this->testJwksUriKey) From 16118f40855164335d16446e7d2a6786c886ea14 Mon Sep 17 00:00:00 2001 From: mehdihasanpour <47119405+mehdihasanpour@users.noreply.github.com> Date: Fri, 15 Jul 2022 20:58:30 +0430 Subject: [PATCH 185/282] chore: misc cleanup (firebase/php-jwt#439) --- Jwt/src/JWK.php | 4 +--- Jwt/src/JWT.php | 9 +++++---- Jwt/tests/CachedKeySetTest.php | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/Jwt/src/JWK.php b/Jwt/src/JWK.php index a5dbc765742..15631ecc246 100644 --- a/Jwt/src/JWK.php +++ b/Jwt/src/JWK.php @@ -231,11 +231,9 @@ private static function createPemFromModulusAndExponent( $rsaOID . $rsaPublicKey ); - $rsaPublicKey = "-----BEGIN PUBLIC KEY-----\r\n" . + return "-----BEGIN PUBLIC KEY-----\r\n" . \chunk_split(\base64_encode($rsaPublicKey), 64) . '-----END PUBLIC KEY-----'; - - return $rsaPublicKey; } /** diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index 084c4a45661..9964073d5ef 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -98,7 +98,7 @@ public static function decode( throw new InvalidArgumentException('Key may not be empty'); } $tks = \explode('.', $jwt); - if (\count($tks) != 3) { + if (\count($tks) !== 3) { throw new UnexpectedValueException('Wrong number of segments'); } list($headb64, $bodyb64, $cryptob64) = $tks; @@ -136,7 +136,7 @@ public static function decode( // OpenSSL expects an ASN.1 DER sequence for ES256/ES384 signatures $sig = self::signatureToDER($sig); } - if (!self::verify("$headb64.$bodyb64", $sig, $key->getKeyMaterial(), $header->alg)) { + if (!self::verify("${headb64}.${bodyb64}", $sig, $key->getKeyMaterial(), $header->alg)) { throw new SignatureInvalidException('Signature verification failed'); } @@ -293,7 +293,8 @@ private static function verify( $success = \openssl_verify($msg, $signature, $keyMaterial, $algorithm); // @phpstan-ignore-line if ($success === 1) { return true; - } elseif ($success === 0) { + } + if ($success === 0) { return false; } // returns 1 on success, 0 on failure, -1 on error. @@ -610,7 +611,7 @@ private static function readDER(string $der, int $offset = 0): array } // Value - if ($type == self::ASN1_BIT_STRING) { + if ($type === self::ASN1_BIT_STRING) { $pos++; // Skip the first contents octet (padding indicator) $data = \substr($der, $pos, $len - 1); $pos += $len - 1; diff --git a/Jwt/tests/CachedKeySetTest.php b/Jwt/tests/CachedKeySetTest.php index 9e884e6bad2..1dc2fdc05ff 100644 --- a/Jwt/tests/CachedKeySetTest.php +++ b/Jwt/tests/CachedKeySetTest.php @@ -210,7 +210,7 @@ public function testCacheItemWithExpiresAfter() public function testJwtVerify() { $privKey1 = file_get_contents(__DIR__ . '/data/rsa1-private.pem'); - $payload = array('sub' => 'foo', 'exp' => strtotime('+10 seconds')); + $payload = ['sub' => 'foo', 'exp' => strtotime('+10 seconds')]; $msg = JWT::encode($payload, $privKey1, 'RS256', 'jwk1'); $cacheItem = $this->prophesize(CacheItemInterface::class); From bd58d4bb23acf13d9a41ee76ddb44d30a5dd9b83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?ShaoBo=20Wan=28=E7=84=A1=E5=B0=98=29?= <756684177@qq.com> Date: Sat, 16 Jul 2022 00:45:03 +0800 Subject: [PATCH 186/282] chore: update changelog for v6.2.0 (firebase/php-jwt#434) --- Jwt/README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Jwt/README.md b/Jwt/README.md index 42e8b6dbf4a..aa90baee0c7 100644 --- a/Jwt/README.md +++ b/Jwt/README.md @@ -261,6 +261,11 @@ $decoded = json_decode(json_encode($decoded), true); Changelog --------- +#### 6.2.0 / 2022-05-14 + + - Added `CachedKeySet` ([#397](https://github.com/firebase/php-jwt/pull/397)) + - Added `$defaultAlg` parameter to `JWT::parseKey` and `JWT::parseKeySet` ([#426](https://github.com/firebase/php-jwt/pull/426)). + #### 6.1.0 / 2022-03-23 - Drop support for PHP 5.3, 5.4, 5.5, 5.6, and 7.0 From 21c3fc1dacf9a5d9db33fb9b34a3d566d14b84b2 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Fri, 15 Jul 2022 10:48:45 -0600 Subject: [PATCH 187/282] chore: prepare v6.3.0 (firebase/php-jwt#441) --- Jwt/README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Jwt/README.md b/Jwt/README.md index aa90baee0c7..fed1e9541b0 100644 --- a/Jwt/README.md +++ b/Jwt/README.md @@ -261,6 +261,11 @@ $decoded = json_decode(json_encode($decoded), true); Changelog --------- +#### 6.3.0 / 2022-07-15 + + - Added ES256 support to JWK parsing ([#399](https://github.com/firebase/php-jwt/pull/399)) + - Fixed potential caching error in `CachedKeySet` by caching jwks as strings ([#435](https://github.com/firebase/php-jwt/pull/435)) + #### 6.2.0 / 2022-05-14 - Added `CachedKeySet` ([#397](https://github.com/firebase/php-jwt/pull/397)) From 42df9fd45dd90bee616b610b3a1f4f2a092fbbe3 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 5 Oct 2022 11:57:44 -0600 Subject: [PATCH 188/282] fix: casing of GET for PSR compat (firebase/php-jwt#451) --- Jwt/src/CachedKeySet.php | 2 +- Jwt/tests/CachedKeySetTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Jwt/src/CachedKeySet.php b/Jwt/src/CachedKeySet.php index e2215b3091c..87f470d7f0b 100644 --- a/Jwt/src/CachedKeySet.php +++ b/Jwt/src/CachedKeySet.php @@ -146,7 +146,7 @@ private function keyIdExists(string $keyId): bool if ($this->rateLimitExceeded()) { return false; } - $request = $this->httpFactory->createRequest('get', $this->jwksUri); + $request = $this->httpFactory->createRequest('GET', $this->jwksUri); $jwksResponse = $this->httpClient->sendRequest($request); $jwks = (string) $jwksResponse->getBody(); $this->keySet = JWK::parseKeySet(json_decode($jwks, true), $this->defaultAlg); diff --git a/Jwt/tests/CachedKeySetTest.php b/Jwt/tests/CachedKeySetTest.php index 1dc2fdc05ff..73b1213d1a0 100644 --- a/Jwt/tests/CachedKeySetTest.php +++ b/Jwt/tests/CachedKeySetTest.php @@ -325,7 +325,7 @@ private function getMockHttpFactory(int $timesCalled = 1) { $request = $this->prophesize('Psr\Http\Message\RequestInterface'); $factory = $this->prophesize(RequestFactoryInterface::class); - $factory->createRequest('get', $this->testJwksUri) + $factory->createRequest('GET', $this->testJwksUri) ->shouldBeCalledTimes($timesCalled) ->willReturn($request->reveal()); From b0e0d56af5e44398cab1d5fe261e5461c5d2a8ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Vin=C3=ADcius=20Santos=20da=20Costa=20Barros?= Date: Wed, 5 Oct 2022 16:38:41 -0300 Subject: [PATCH 189/282] fix: string interpolation format for php 8.2 (firebase/php-jwt#446) --- Jwt/src/JWT.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index 9964073d5ef..5a5640ab208 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -136,7 +136,7 @@ public static function decode( // OpenSSL expects an ASN.1 DER sequence for ES256/ES384 signatures $sig = self::signatureToDER($sig); } - if (!self::verify("${headb64}.${bodyb64}", $sig, $key->getKeyMaterial(), $header->alg)) { + if (!self::verify("{$headb64}.{$bodyb64}", $sig, $key->getKeyMaterial(), $header->alg)) { throw new SignatureInvalidException('Signature verification failed'); } From 77ec41927644538a0d914a671545c25028499144 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 5 Oct 2022 13:50:38 -0600 Subject: [PATCH 190/282] chore(docs): add examples for exception handling (firebase/php-jwt#463) --- Jwt/README.md | 52 ++++++++++++++++++++++++++++++++++++++++++++++++- Jwt/src/JWT.php | 2 +- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/Jwt/README.md b/Jwt/README.md index fed1e9541b0..79a29ec0f23 100644 --- a/Jwt/README.md +++ b/Jwt/README.md @@ -245,6 +245,56 @@ $decoded = JWT::decode($jwt, $keySet); Miscellaneous ------------- +#### Exception Handling + +When a call to `JWT::decode` is invalid, it will throw one of the following exceptions: + +```php +use Firebase\JWT\JWT; +use Firebase\JWT\SignatureInvalidException; +use Firebase\JWT\BeforeValidException; +use Firebase\JWT\ExpiredException; +use DomainException; +use InvalidArgumentException; +use UnexpectedValueException; + +try { + $decoded = JWT::decode($payload, $keys); +} catch (InvalidArgumentException $e) { + // provided key/key-array is empty or malformed. +} catch (DomainException $e) { + // provided algorithm is unsupported OR + // provided key is invalid OR + // unknown error thrown in openSSL or libsodium OR + // libsodium is required but not available. +} catch (SignatureInvalidException $e) { + // provided JWT signature verification failed. +} catch (BeforeValidException $e) { + // provided JWT is trying to be used before "nbf" claim OR + // provided JWT is trying to be used before "iat" claim. +} catch (ExpiredException $e) { + // provided JWT is trying to be used after "exp" claim. +} catch (UnexpectedValueException $e) { + // provided JWT is malformed OR + // provided JWT is missing an algorithm / using an unsupported algorithm OR + // provided JWT algorithm does not match provided key OR + // provided key ID in key/key-array is empty or invalid. +} +``` + +All exceptions in the `Firebase\JWT` namespace extend `UnexpectedValueException`, and can be simplified +like this: + +```php +try { + $decoded = JWT::decode($payload, $keys); +} catch (LogicException $e) { + // errors having to do with environmental setup or malformed JWT Keys +} catch (UnexpectedValueException $e) { + // errors having to do with JWT signature and claims +} +``` + #### Casting to array The return value of `JWT::decode` is the generic PHP object `stdClass`. If you'd like to handle with arrays @@ -269,7 +319,7 @@ Changelog #### 6.2.0 / 2022-05-14 - Added `CachedKeySet` ([#397](https://github.com/firebase/php-jwt/pull/397)) - - Added `$defaultAlg` parameter to `JWT::parseKey` and `JWT::parseKeySet` ([#426](https://github.com/firebase/php-jwt/pull/426)). + - Added `$defaultAlg` parameter to `JWT::parseKey` and `JWT::parseKeySet` ([#426](https://github.com/firebase/php-jwt/pull/426)). #### 6.1.0 / 2022-03-23 diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index 5a5640ab208..f5b466d5bca 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -76,7 +76,7 @@ class JWT * * @return stdClass The JWT's payload as a PHP object * - * @throws InvalidArgumentException Provided key/key-array was empty + * @throws InvalidArgumentException Provided key/key-array was empty or malformed * @throws DomainException Provided JWT is malformed * @throws UnexpectedValueException Provided JWT was invalid * @throws SignatureInvalidException Provided JWT was invalid because the signature verification failed From 3864599c3b4f8c30551d110d5d7c33ed0f6a6d97 Mon Sep 17 00:00:00 2001 From: Thibault RICHARD Date: Wed, 5 Oct 2022 21:51:08 +0200 Subject: [PATCH 191/282] chore(docs): encode does not accept objects (firebase/php-jwt#457) --- Jwt/src/JWT.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index f5b466d5bca..977d7fbd723 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -166,7 +166,7 @@ public static function decode( } /** - * Converts and signs a PHP object or array into a JWT string. + * Converts and signs a PHP array into a JWT string. * * @param array $payload PHP array * @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $key The secret key. From ded6e9553ef8811266a33fbae7cc0b8cba86f966 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Tue, 1 Nov 2022 14:55:48 -0600 Subject: [PATCH 192/282] chore: add release-please (firebase/php-jwt#467) --- Jwt/.github/release-please.yml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 Jwt/.github/release-please.yml diff --git a/Jwt/.github/release-please.yml b/Jwt/.github/release-please.yml new file mode 100644 index 00000000000..0a6e0cc2719 --- /dev/null +++ b/Jwt/.github/release-please.yml @@ -0,0 +1,3 @@ +releaseType: simple +handleGHRelease: true +primaryBranch: main From f5459a4c1356c0fbd8b7b207a98805a8f848e170 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Tue, 1 Nov 2022 15:06:07 -0600 Subject: [PATCH 193/282] chore: move changelog into separate file (firebase/php-jwt#469) --- Jwt/CHANGELOG.md | 84 +++++++++++++++++++++++++++++++++++++++++++++++ Jwt/README.md | 85 ------------------------------------------------ 2 files changed, 84 insertions(+), 85 deletions(-) create mode 100644 Jwt/CHANGELOG.md diff --git a/Jwt/CHANGELOG.md b/Jwt/CHANGELOG.md new file mode 100644 index 00000000000..261150b9f85 --- /dev/null +++ b/Jwt/CHANGELOG.md @@ -0,0 +1,84 @@ +# Changelog + +## 6.3.0 / 2022-07-15 + + - Added ES256 support to JWK parsing ([#399](https://github.com/firebase/php-jwt/pull/399)) + - Fixed potential caching error in `CachedKeySet` by caching jwks as strings ([#435](https://github.com/firebase/php-jwt/pull/435)) + +## 6.2.0 / 2022-05-14 + + - Added `CachedKeySet` ([#397](https://github.com/firebase/php-jwt/pull/397)) + - Added `$defaultAlg` parameter to `JWT::parseKey` and `JWT::parseKeySet` ([#426](https://github.com/firebase/php-jwt/pull/426)). + +## 6.1.0 / 2022-03-23 + + - Drop support for PHP 5.3, 5.4, 5.5, 5.6, and 7.0 + - Add parameter typing and return types where possible + +## 6.0.0 / 2022-01-24 + + - **Backwards-Compatibility Breaking Changes**: See the [Release Notes](https://github.com/firebase/php-jwt/releases/tag/v6.0.0) for more information. + - New Key object to prevent key/algorithm type confusion (#365) + - Add JWK support (#273) + - Add ES256 support (#256) + - Add ES384 support (#324) + - Add Ed25519 support (#343) + +## 5.0.0 / 2017-06-26 +- Support RS384 and RS512. + See [#117](https://github.com/firebase/php-jwt/pull/117). Thanks [@joostfaassen](https://github.com/joostfaassen)! +- Add an example for RS256 openssl. + See [#125](https://github.com/firebase/php-jwt/pull/125). Thanks [@akeeman](https://github.com/akeeman)! +- Detect invalid Base64 encoding in signature. + See [#162](https://github.com/firebase/php-jwt/pull/162). Thanks [@psignoret](https://github.com/psignoret)! +- Update `JWT::verify` to handle OpenSSL errors. + See [#159](https://github.com/firebase/php-jwt/pull/159). Thanks [@bshaffer](https://github.com/bshaffer)! +- Add `array` type hinting to `decode` method + See [#101](https://github.com/firebase/php-jwt/pull/101). Thanks [@hywak](https://github.com/hywak)! +- Add all JSON error types. + See [#110](https://github.com/firebase/php-jwt/pull/110). Thanks [@gbalduzzi](https://github.com/gbalduzzi)! +- Bugfix 'kid' not in given key list. + See [#129](https://github.com/firebase/php-jwt/pull/129). Thanks [@stampycode](https://github.com/stampycode)! +- Miscellaneous cleanup, documentation and test fixes. + See [#107](https://github.com/firebase/php-jwt/pull/107), [#115](https://github.com/firebase/php-jwt/pull/115), + [#160](https://github.com/firebase/php-jwt/pull/160), [#161](https://github.com/firebase/php-jwt/pull/161), and + [#165](https://github.com/firebase/php-jwt/pull/165). Thanks [@akeeman](https://github.com/akeeman), + [@chinedufn](https://github.com/chinedufn), and [@bshaffer](https://github.com/bshaffer)! + +## 4.0.0 / 2016-07-17 +- Add support for late static binding. See [#88](https://github.com/firebase/php-jwt/pull/88) for details. Thanks to [@chappy84](https://github.com/chappy84)! +- Use static `$timestamp` instead of `time()` to improve unit testing. See [#93](https://github.com/firebase/php-jwt/pull/93) for details. Thanks to [@josephmcdermott](https://github.com/josephmcdermott)! +- Fixes to exceptions classes. See [#81](https://github.com/firebase/php-jwt/pull/81) for details. Thanks to [@Maks3w](https://github.com/Maks3w)! +- Fixes to PHPDoc. See [#76](https://github.com/firebase/php-jwt/pull/76) for details. Thanks to [@akeeman](https://github.com/akeeman)! + +## 3.0.0 / 2015-07-22 +- Minimum PHP version updated from `5.2.0` to `5.3.0`. +- Add `\Firebase\JWT` namespace. See +[#59](https://github.com/firebase/php-jwt/pull/59) for details. Thanks to +[@Dashron](https://github.com/Dashron)! +- Require a non-empty key to decode and verify a JWT. See +[#60](https://github.com/firebase/php-jwt/pull/60) for details. Thanks to +[@sjones608](https://github.com/sjones608)! +- Cleaner documentation blocks in the code. See +[#62](https://github.com/firebase/php-jwt/pull/62) for details. Thanks to +[@johanderuijter](https://github.com/johanderuijter)! + +## 2.2.0 / 2015-06-22 +- Add support for adding custom, optional JWT headers to `JWT::encode()`. See +[#53](https://github.com/firebase/php-jwt/pull/53/files) for details. Thanks to +[@mcocaro](https://github.com/mcocaro)! + +## 2.1.0 / 2015-05-20 +- Add support for adding a leeway to `JWT:decode()` that accounts for clock skew +between signing and verifying entities. Thanks to [@lcabral](https://github.com/lcabral)! +- Add support for passing an object implementing the `ArrayAccess` interface for +`$keys` argument in `JWT::decode()`. Thanks to [@aztech-dev](https://github.com/aztech-dev)! + +## 2.0.0 / 2015-04-01 +- **Note**: It is strongly recommended that you update to > v2.0.0 to address + known security vulnerabilities in prior versions when both symmetric and + asymmetric keys are used together. +- Update signature for `JWT::decode(...)` to require an array of supported + algorithms to use when verifying token signatures. + + diff --git a/Jwt/README.md b/Jwt/README.md index 79a29ec0f23..ae2b389563a 100644 --- a/Jwt/README.md +++ b/Jwt/README.md @@ -308,91 +308,6 @@ $decoded = JWT::decode($payload, $keys); $decoded = json_decode(json_encode($decoded), true); ``` -Changelog ---------- - -#### 6.3.0 / 2022-07-15 - - - Added ES256 support to JWK parsing ([#399](https://github.com/firebase/php-jwt/pull/399)) - - Fixed potential caching error in `CachedKeySet` by caching jwks as strings ([#435](https://github.com/firebase/php-jwt/pull/435)) - -#### 6.2.0 / 2022-05-14 - - - Added `CachedKeySet` ([#397](https://github.com/firebase/php-jwt/pull/397)) - - Added `$defaultAlg` parameter to `JWT::parseKey` and `JWT::parseKeySet` ([#426](https://github.com/firebase/php-jwt/pull/426)). - -#### 6.1.0 / 2022-03-23 - - - Drop support for PHP 5.3, 5.4, 5.5, 5.6, and 7.0 - - Add parameter typing and return types where possible - -#### 6.0.0 / 2022-01-24 - - - **Backwards-Compatibility Breaking Changes**: See the [Release Notes](https://github.com/firebase/php-jwt/releases/tag/v6.0.0) for more information. - - New Key object to prevent key/algorithm type confusion (#365) - - Add JWK support (#273) - - Add ES256 support (#256) - - Add ES384 support (#324) - - Add Ed25519 support (#343) - -#### 5.0.0 / 2017-06-26 -- Support RS384 and RS512. - See [#117](https://github.com/firebase/php-jwt/pull/117). Thanks [@joostfaassen](https://github.com/joostfaassen)! -- Add an example for RS256 openssl. - See [#125](https://github.com/firebase/php-jwt/pull/125). Thanks [@akeeman](https://github.com/akeeman)! -- Detect invalid Base64 encoding in signature. - See [#162](https://github.com/firebase/php-jwt/pull/162). Thanks [@psignoret](https://github.com/psignoret)! -- Update `JWT::verify` to handle OpenSSL errors. - See [#159](https://github.com/firebase/php-jwt/pull/159). Thanks [@bshaffer](https://github.com/bshaffer)! -- Add `array` type hinting to `decode` method - See [#101](https://github.com/firebase/php-jwt/pull/101). Thanks [@hywak](https://github.com/hywak)! -- Add all JSON error types. - See [#110](https://github.com/firebase/php-jwt/pull/110). Thanks [@gbalduzzi](https://github.com/gbalduzzi)! -- Bugfix 'kid' not in given key list. - See [#129](https://github.com/firebase/php-jwt/pull/129). Thanks [@stampycode](https://github.com/stampycode)! -- Miscellaneous cleanup, documentation and test fixes. - See [#107](https://github.com/firebase/php-jwt/pull/107), [#115](https://github.com/firebase/php-jwt/pull/115), - [#160](https://github.com/firebase/php-jwt/pull/160), [#161](https://github.com/firebase/php-jwt/pull/161), and - [#165](https://github.com/firebase/php-jwt/pull/165). Thanks [@akeeman](https://github.com/akeeman), - [@chinedufn](https://github.com/chinedufn), and [@bshaffer](https://github.com/bshaffer)! - -#### 4.0.0 / 2016-07-17 -- Add support for late static binding. See [#88](https://github.com/firebase/php-jwt/pull/88) for details. Thanks to [@chappy84](https://github.com/chappy84)! -- Use static `$timestamp` instead of `time()` to improve unit testing. See [#93](https://github.com/firebase/php-jwt/pull/93) for details. Thanks to [@josephmcdermott](https://github.com/josephmcdermott)! -- Fixes to exceptions classes. See [#81](https://github.com/firebase/php-jwt/pull/81) for details. Thanks to [@Maks3w](https://github.com/Maks3w)! -- Fixes to PHPDoc. See [#76](https://github.com/firebase/php-jwt/pull/76) for details. Thanks to [@akeeman](https://github.com/akeeman)! - -#### 3.0.0 / 2015-07-22 -- Minimum PHP version updated from `5.2.0` to `5.3.0`. -- Add `\Firebase\JWT` namespace. See -[#59](https://github.com/firebase/php-jwt/pull/59) for details. Thanks to -[@Dashron](https://github.com/Dashron)! -- Require a non-empty key to decode and verify a JWT. See -[#60](https://github.com/firebase/php-jwt/pull/60) for details. Thanks to -[@sjones608](https://github.com/sjones608)! -- Cleaner documentation blocks in the code. See -[#62](https://github.com/firebase/php-jwt/pull/62) for details. Thanks to -[@johanderuijter](https://github.com/johanderuijter)! - -#### 2.2.0 / 2015-06-22 -- Add support for adding custom, optional JWT headers to `JWT::encode()`. See -[#53](https://github.com/firebase/php-jwt/pull/53/files) for details. Thanks to -[@mcocaro](https://github.com/mcocaro)! - -#### 2.1.0 / 2015-05-20 -- Add support for adding a leeway to `JWT:decode()` that accounts for clock skew -between signing and verifying entities. Thanks to [@lcabral](https://github.com/lcabral)! -- Add support for passing an object implementing the `ArrayAccess` interface for -`$keys` argument in `JWT::decode()`. Thanks to [@aztech-dev](https://github.com/aztech-dev)! - -#### 2.0.0 / 2015-04-01 -- **Note**: It is strongly recommended that you update to > v2.0.0 to address - known security vulnerabilities in prior versions when both symmetric and - asymmetric keys are used together. -- Update signature for `JWT::decode(...)` to require an array of supported - algorithms to use when verifying token signatures. - - Tests ----- Run the tests using phpunit: From 4836b28a925e16d13407988411bb1b6001da3646 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 1 Nov 2022 14:20:08 -0700 Subject: [PATCH 194/282] chore(main): release 6.3.1 (firebase/php-jwt#468) --- Jwt/CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Jwt/CHANGELOG.md b/Jwt/CHANGELOG.md index 261150b9f85..21d26679fac 100644 --- a/Jwt/CHANGELOG.md +++ b/Jwt/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [6.3.1](https://github.com/firebase/php-jwt/compare/v6.3.0...v6.3.1) (2022-11-01) + + +### Bug Fixes + +* casing of GET for PSR compat ([#451](https://github.com/firebase/php-jwt/issues/451)) ([60b52b7](https://github.com/firebase/php-jwt/commit/60b52b71978790eafcf3b95cfbd83db0439e8d22)) +* string interpolation format for php 8.2 ([#446](https://github.com/firebase/php-jwt/issues/446)) ([2e07d8a](https://github.com/firebase/php-jwt/commit/2e07d8a1524d12b69b110ad649f17461d068b8f2)) + ## 6.3.0 / 2022-07-15 - Added ES256 support to JWK parsing ([#399](https://github.com/firebase/php-jwt/pull/399)) From 2f9ec884ecb360a252784939c771fe16ab79f412 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Tue, 1 Nov 2022 14:24:10 -0700 Subject: [PATCH 195/282] fix: check kid before using as array index --- Jwt/src/JWT.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index 977d7fbd723..30b4cbb74cd 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -425,14 +425,15 @@ private static function getKey( return $keyOrKeyArray; } + if (empty($kid)) { + throw new UnexpectedValueException('"kid" empty, unable to lookup correct key'); + } + if ($keyOrKeyArray instanceof CachedKeySet) { // Skip "isset" check, as this will automatically refresh if not set return $keyOrKeyArray[$kid]; } - if (empty($kid)) { - throw new UnexpectedValueException('"kid" empty, unable to lookup correct key'); - } if (!isset($keyOrKeyArray[$kid])) { throw new UnexpectedValueException('"kid" invalid, unable to lookup correct key'); } From cadbd650e8ddacc89e4636ef37aa66a45f6c6d40 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Mon, 19 Dec 2022 09:10:46 -0800 Subject: [PATCH 196/282] chore(main): release 6.3.2 (firebase/php-jwt#470) --- Jwt/CHANGELOG.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Jwt/CHANGELOG.md b/Jwt/CHANGELOG.md index 21d26679fac..d43c9d8c92c 100644 --- a/Jwt/CHANGELOG.md +++ b/Jwt/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [6.3.2](https://github.com/firebase/php-jwt/compare/v6.3.1...v6.3.2) (2022-11-01) + + +### Bug Fixes + +* check kid before using as array index ([bad1b04](https://github.com/firebase/php-jwt/commit/bad1b040d0c736bbf86814c6b5ae614f517cf7bd)) + ## [6.3.1](https://github.com/firebase/php-jwt/compare/v6.3.0...v6.3.1) (2022-11-01) @@ -88,5 +95,3 @@ between signing and verifying entities. Thanks to [@lcabral](https://github.com/ asymmetric keys are used together. - Update signature for `JWT::decode(...)` to require an array of supported algorithms to use when verifying token signatures. - - From 17d15f0bf591886b51aae68116850067e60a6a07 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Mon, 19 Dec 2022 14:07:05 -0800 Subject: [PATCH 197/282] chore: add PHP 8.2 to test suite (firebase/php-jwt#476) --- Jwt/.github/workflows/tests.yml | 2 +- Jwt/src/JWT.php | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Jwt/.github/workflows/tests.yml b/Jwt/.github/workflows/tests.yml index 83dc88dadc6..6956aa9504a 100644 --- a/Jwt/.github/workflows/tests.yml +++ b/Jwt/.github/workflows/tests.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php: [ "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"] + php: [ "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2" ] name: PHP ${{matrix.php }} Unit Test steps: - uses: actions/checkout@v2 diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index 30b4cbb74cd..623e7055f04 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -255,6 +255,9 @@ public static function sign( // The last non-empty line is used as the key. $lines = array_filter(explode("\n", $key)); $key = base64_decode((string) end($lines)); + if (\strlen($key) === 0) { + throw new DomainException('Key cannot be empty string'); + } return sodium_crypto_sign_detached($msg, $key); } catch (Exception $e) { throw new DomainException($e->getMessage(), 0, $e); @@ -312,6 +315,12 @@ private static function verify( // The last non-empty line is used as the key. $lines = array_filter(explode("\n", $keyMaterial)); $key = base64_decode((string) end($lines)); + if (\strlen($key) === 0) { + throw new DomainException('Key cannot be empty string'); + } + if (\strlen($signature) === 0) { + throw new DomainException('Signature cannot be empty string'); + } return sodium_crypto_sign_verify_detached($signature, $msg, $key); } catch (Exception $e) { throw new DomainException($e->getMessage(), 0, $e); From 75f5febfaab51ef57bc16fce4c8987696448b445 Mon Sep 17 00:00:00 2001 From: "Chun-Sheng, Li" Date: Tue, 20 Dec 2022 06:08:43 +0800 Subject: [PATCH 198/282] chore: suggest ext-sodium (firebase/php-jwt#474) Co-authored-by: Brent Shaffer --- Jwt/composer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Jwt/composer.json b/Jwt/composer.json index 2a3cb2df697..c9aa3dbbc26 100644 --- a/Jwt/composer.json +++ b/Jwt/composer.json @@ -23,7 +23,8 @@ "php": "^7.1||^8.0" }, "suggest": { - "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" + "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present", + "ext-sodium": "Support EdDSA (Ed25519) signatures" }, "autoload": { "psr-4": { From 462e3ec0cad0038781df9850a29d9c293c98afdb Mon Sep 17 00:00:00 2001 From: Mitch Flindell <990013+scrummitch@users.noreply.github.com> Date: Tue, 20 Dec 2022 09:15:50 +1100 Subject: [PATCH 199/282] feat: add support for W3C ES256K (firebase/php-jwt#462) --- Jwt/src/JWK.php | 1 + Jwt/src/JWT.php | 15 ++++++++------- Jwt/tests/JWTTest.php | 1 + Jwt/tests/data/ec-jwkset.json | 9 +++++++++ Jwt/tests/data/secp256k1-private.pem | 5 +++++ Jwt/tests/data/secp256k1-public.pem | 4 ++++ 6 files changed, 28 insertions(+), 7 deletions(-) create mode 100644 Jwt/tests/data/secp256k1-private.pem create mode 100644 Jwt/tests/data/secp256k1-public.pem diff --git a/Jwt/src/JWK.php b/Jwt/src/JWK.php index 15631ecc246..c7eff8ae483 100644 --- a/Jwt/src/JWK.php +++ b/Jwt/src/JWK.php @@ -26,6 +26,7 @@ class JWK private const ASN1_BIT_STRING = 0x03; private const EC_CURVES = [ 'P-256' => '1.2.840.10045.3.1.7', // Len: 64 + 'secp256k1' => '1.3.132.0.10', // Len: 64 // 'P-384' => '1.3.132.0.34', // Len: 96 (not yet supported) // 'P-521' => '1.3.132.0.35', // Len: 132 (not supported) ]; diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index 623e7055f04..269e8caf0d3 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -55,6 +55,7 @@ class JWT public static $supported_algs = [ 'ES384' => ['openssl', 'SHA384'], 'ES256' => ['openssl', 'SHA256'], + 'ES256K' => ['openssl', 'SHA256'], 'HS256' => ['hash_hmac', 'SHA256'], 'HS384' => ['hash_hmac', 'SHA384'], 'HS512' => ['hash_hmac', 'SHA512'], @@ -132,8 +133,8 @@ public static function decode( // See issue #351 throw new UnexpectedValueException('Incorrect key for this algorithm'); } - if ($header->alg === 'ES256' || $header->alg === 'ES384') { - // OpenSSL expects an ASN.1 DER sequence for ES256/ES384 signatures + if (\in_array($header->alg, ['ES256', 'ES256K', 'ES384'], true)) { + // OpenSSL expects an ASN.1 DER sequence for ES256/ES256K/ES384 signatures $sig = self::signatureToDER($sig); } if (!self::verify("{$headb64}.{$bodyb64}", $sig, $key->getKeyMaterial(), $header->alg)) { @@ -170,8 +171,8 @@ public static function decode( * * @param array $payload PHP array * @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $key The secret key. - * @param string $alg Supported algorithms are 'ES384','ES256', 'HS256', 'HS384', - * 'HS512', 'RS256', 'RS384', and 'RS512' + * @param string $alg Supported algorithms are 'ES384','ES256', 'ES256K', 'HS256', + * 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512' * @param string $keyId * @param array $head An array with header elements to attach * @@ -210,8 +211,8 @@ public static function encode( * * @param string $msg The message to sign * @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $key The secret key. - * @param string $alg Supported algorithms are 'ES384','ES256', 'HS256', 'HS384', - * 'HS512', 'RS256', 'RS384', and 'RS512' + * @param string $alg Supported algorithms are 'ES384','ES256', 'ES256K', 'HS256', + * 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512' * * @return string An encrypted message * @@ -238,7 +239,7 @@ public static function sign( if (!$success) { throw new DomainException('OpenSSL unable to sign data'); } - if ($alg === 'ES256') { + if ($alg === 'ES256' || $alg === 'ES256K') { $signature = self::signatureFromDER($signature, 256); } elseif ($alg === 'ES384') { $signature = self::signatureFromDER($signature, 384); diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index 58c334ed320..e3b951442ed 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -377,6 +377,7 @@ public function provideEncodeDecode() [__DIR__ . '/data/ecdsa384-private.pem', __DIR__ . '/data/ecdsa384-public.pem', 'ES384'], [__DIR__ . '/data/rsa1-private.pem', __DIR__ . '/data/rsa1-public.pub', 'RS512'], [__DIR__ . '/data/ed25519-1.sec', __DIR__ . '/data/ed25519-1.pub', 'EdDSA'], + [__DIR__ . '/data/secp256k1-private.pem', __DIR__ . '/data/secp256k1-public.pem', 'ES256K'], ]; } diff --git a/Jwt/tests/data/ec-jwkset.json b/Jwt/tests/data/ec-jwkset.json index 46ed8cf9586..213f68acdfd 100644 --- a/Jwt/tests/data/ec-jwkset.json +++ b/Jwt/tests/data/ec-jwkset.json @@ -17,6 +17,15 @@ "x": "mQa0q5FvxPRujxzFazQT1Mo2YJJzuKiXU3svOJ41jhw", "y": "jAz7UwIl2oOFk06kj42ZFMOXmGMFUGjKASvyYtibCH0", "alg": "ES256" + }, + { + "kty": "EC", + "use": "sig", + "crv": "secp256k1", + "kid": "jwk3", + "x": "EFpwNuP322bU3WP1DtJgx67L0CUV1MxNixqPVMH2L9Q", + "y": "_fSTbijIJjpsqL16cIEvxxf3MaYMY8MbqEq066yV9ls", + "alg": "ES256K" } ] } \ No newline at end of file diff --git a/Jwt/tests/data/secp256k1-private.pem b/Jwt/tests/data/secp256k1-private.pem new file mode 100644 index 00000000000..3e69d585b01 --- /dev/null +++ b/Jwt/tests/data/secp256k1-private.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGEAgEAMBAGByqGSM49AgEGBSuBBAAKBG0wawIBAQQgC8ouvv1ZOmOjh5Nbwx6i +3b35wWN+OEkW2hzm3BKAQJ2hRANCAAT9nYGLVP6Unm/LXOoyWhsKpalffMSr3EHV +iUE8gVmj2/atnPkblx38Yj6bC3z1urERAB+JqgpWOAKaWcEYCUuO +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/Jwt/tests/data/secp256k1-public.pem b/Jwt/tests/data/secp256k1-public.pem new file mode 100644 index 00000000000..5949135ff48 --- /dev/null +++ b/Jwt/tests/data/secp256k1-public.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAE/Z2Bi1T+lJ5vy1zqMlobCqWpX3zEq9xB +1YlBPIFZo9v2rZz5G5cd/GI+mwt89bqxEQAfiaoKVjgCmlnBGAlLjg== +-----END PUBLIC KEY----- \ No newline at end of file From 780bb8e37641e5e4959dc0995bd7058750000d44 Mon Sep 17 00:00:00 2001 From: "Chun-Sheng, Li" Date: Wed, 11 Jan 2023 07:39:17 +0800 Subject: [PATCH 200/282] chore(tests): use assertSame for strict equals (firebase/php-jwt#478) --- Jwt/tests/CachedKeySetTest.php | 16 ++++++++-------- Jwt/tests/JWKTest.php | 6 +++--- Jwt/tests/JWTTest.php | 20 ++++++++++---------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/Jwt/tests/CachedKeySetTest.php b/Jwt/tests/CachedKeySetTest.php index 73b1213d1a0..91d4a27c6ca 100644 --- a/Jwt/tests/CachedKeySetTest.php +++ b/Jwt/tests/CachedKeySetTest.php @@ -93,7 +93,7 @@ public function testWithExistingKeyId() $this->getMockEmptyCache() ); $this->assertInstanceOf(Key::class, $cachedKeySet['foo']); - $this->assertEquals('foo', $cachedKeySet['foo']->getAlgorithm()); + $this->assertSame('foo', $cachedKeySet['foo']->getAlgorithm()); } public function testWithDefaultAlg() @@ -108,7 +108,7 @@ public function testWithDefaultAlg() 'baz256' ); $this->assertInstanceOf(Key::class, $cachedKeySet['baz']); - $this->assertEquals('baz256', $cachedKeySet['baz']->getAlgorithm()); + $this->assertSame('baz256', $cachedKeySet['baz']->getAlgorithm()); } public function testKeyIdIsCached() @@ -132,7 +132,7 @@ public function testKeyIdIsCached() $cache->reveal() ); $this->assertInstanceOf(Key::class, $cachedKeySet['foo']); - $this->assertEquals('foo', $cachedKeySet['foo']->getAlgorithm()); + $this->assertSame('foo', $cachedKeySet['foo']->getAlgorithm()); } public function testCachedKeyIdRefresh() @@ -165,10 +165,10 @@ public function testCachedKeyIdRefresh() $cache->reveal() ); $this->assertInstanceOf(Key::class, $cachedKeySet['foo']); - $this->assertEquals('foo', $cachedKeySet['foo']->getAlgorithm()); + $this->assertSame('foo', $cachedKeySet['foo']->getAlgorithm()); $this->assertInstanceOf(Key::class, $cachedKeySet['bar']); - $this->assertEquals('bar', $cachedKeySet['bar']->getAlgorithm()); + $this->assertSame('bar', $cachedKeySet['bar']->getAlgorithm()); } public function testCacheItemWithExpiresAfter() @@ -204,7 +204,7 @@ public function testCacheItemWithExpiresAfter() $expiresAfter ); $this->assertInstanceOf(Key::class, $cachedKeySet['foo']); - $this->assertEquals('foo', $cachedKeySet['foo']->getAlgorithm()); + $this->assertSame('foo', $cachedKeySet['foo']->getAlgorithm()); } public function testJwtVerify() @@ -233,7 +233,7 @@ public function testJwtVerify() $result = JWT::decode($msg, $cachedKeySet); - $this->assertEquals('foo', $result->sub); + $this->assertSame('foo', $result->sub); } public function testRateLimit() @@ -290,7 +290,7 @@ public function testFullIntegration(string $jwkUri): void $this->assertArrayHasKey($kid, $cachedKeySet); $key = $cachedKeySet[$kid]; $this->assertInstanceOf(Key::class, $key); - $this->assertEquals($keys['keys'][0]['alg'], $key->getAlgorithm()); + $this->assertSame($keys['keys'][0]['alg'], $key->getAlgorithm()); } public function provideFullIntegration() diff --git a/Jwt/tests/JWKTest.php b/Jwt/tests/JWKTest.php index b8c24f9887b..93afea70db2 100644 --- a/Jwt/tests/JWKTest.php +++ b/Jwt/tests/JWKTest.php @@ -67,7 +67,7 @@ public function testParsePrivateKeyWithoutAlgWithDefaultAlgParameter() unset($jwkSet['keys'][0]['alg']); $jwks = JWK::parseKeySet($jwkSet, 'foo'); - $this->assertEquals('foo', $jwks['jwk1']->getAlgorithm()); + $this->assertSame('foo', $jwks['jwk1']->getAlgorithm()); } public function testParseKeyWithEmptyDValue() @@ -143,7 +143,7 @@ public function testDecodeByJwkKeySet($pemFile, $jwkFile, $alg) $keys = JWK::parseKeySet($jwkSet); $result = JWT::decode($msg, $keys); - $this->assertEquals('foo', $result->sub); + $this->assertSame('foo', $result->sub); } public function provideDecodeByJwkKeySet() @@ -165,6 +165,6 @@ public function testDecodeByMultiJwkKeySet() $result = JWT::decode($msg, self::$keys); - $this->assertEquals('bar', $result->sub); + $this->assertSame('bar', $result->sub); } } diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index e3b951442ed..3ce912ed02b 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -73,7 +73,7 @@ public function testValidToken() ]; $encoded = JWT::encode($payload, 'my_key', 'HS256'); $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); - $this->assertEquals($decoded->message, 'abc'); + $this->assertSame($decoded->message, 'abc'); } public function testValidTokenWithLeeway() @@ -85,7 +85,7 @@ public function testValidTokenWithLeeway() ]; $encoded = JWT::encode($payload, 'my_key', 'HS256'); $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); - $this->assertEquals($decoded->message, 'abc'); + $this->assertSame($decoded->message, 'abc'); JWT::$leeway = 0; } @@ -99,7 +99,7 @@ public function testExpiredTokenWithLeeway() $this->expectException(ExpiredException::class); $encoded = JWT::encode($payload, 'my_key', 'HS256'); $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); - $this->assertEquals($decoded->message, 'abc'); + $this->assertSame($decoded->message, 'abc'); JWT::$leeway = 0; } @@ -113,7 +113,7 @@ public function testValidTokenWithNbf() ]; $encoded = JWT::encode($payload, 'my_key', 'HS256'); $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); - $this->assertEquals($decoded->message, 'abc'); + $this->assertSame($decoded->message, 'abc'); } public function testValidTokenWithNbfLeeway() @@ -125,7 +125,7 @@ public function testValidTokenWithNbfLeeway() ]; $encoded = JWT::encode($payload, 'my_key', 'HS256'); $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); - $this->assertEquals($decoded->message, 'abc'); + $this->assertSame($decoded->message, 'abc'); JWT::$leeway = 0; } @@ -151,7 +151,7 @@ public function testValidTokenWithIatLeeway() ]; $encoded = JWT::encode($payload, 'my_key', 'HS256'); $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); - $this->assertEquals($decoded->message, 'abc'); + $this->assertSame($decoded->message, 'abc'); JWT::$leeway = 0; } @@ -301,7 +301,7 @@ public function testEdDsaEncodeDecode() $pubKey = base64_encode(sodium_crypto_sign_publickey($keyPair)); $decoded = JWT::decode($msg, new Key($pubKey, 'EdDSA')); - $this->assertEquals('bar', $decoded->foo); + $this->assertSame('bar', $decoded->foo); } public function testInvalidEdDsaEncodeDecode() @@ -350,7 +350,7 @@ public function testDecodesArraysInJWTAsArray() $payload = ['foo' => [1, 2, 3]]; $jwt = JWT::encode($payload, $key, 'HS256'); $decoded = JWT::decode($jwt, new Key($key, 'HS256')); - $this->assertEquals($payload['foo'], $decoded->foo); + $this->assertSame($payload['foo'], $decoded->foo); } /** @@ -367,7 +367,7 @@ public function testEncodeDecode($privateKeyFile, $publicKeyFile, $alg) $publicKey = file_get_contents($publicKeyFile); $decoded = JWT::decode($encoded, new Key($publicKey, $alg)); - $this->assertEquals('bar', $decoded->foo); + $this->assertSame('bar', $decoded->foo); } public function provideEncodeDecode() @@ -393,6 +393,6 @@ public function testEncodeDecodeWithResource() // Verify decoding succeeds $decoded = JWT::decode($encoded, new Key($resource, 'RS512')); - $this->assertEquals('bar', $decoded->foo); + $this->assertSame('bar', $decoded->foo); } } From d9445ed6f8235fd8f9d22d180ea098ec63945c3b Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 8 Feb 2023 09:14:01 -0500 Subject: [PATCH 201/282] feat: improve caching by only decoding jwks when necessary (firebase/php-jwt#486) --- Jwt/src/CachedKeySet.php | 45 ++++++++++++++++---- Jwt/tests/CachedKeySetTest.php | 77 +++++++++++++++++++++++++++++++--- 2 files changed, 109 insertions(+), 13 deletions(-) diff --git a/Jwt/src/CachedKeySet.php b/Jwt/src/CachedKeySet.php index 87f470d7f0b..baf801f13f0 100644 --- a/Jwt/src/CachedKeySet.php +++ b/Jwt/src/CachedKeySet.php @@ -3,6 +3,7 @@ namespace Firebase\JWT; use ArrayAccess; +use InvalidArgumentException; use LogicException; use OutOfBoundsException; use Psr\Cache\CacheItemInterface; @@ -10,6 +11,7 @@ use Psr\Http\Client\ClientInterface; use Psr\Http\Message\RequestFactoryInterface; use RuntimeException; +use UnexpectedValueException; /** * @implements ArrayAccess @@ -41,7 +43,7 @@ class CachedKeySet implements ArrayAccess */ private $cacheItem; /** - * @var array + * @var array> */ private $keySet; /** @@ -101,7 +103,7 @@ public function offsetGet($keyId): Key if (!$this->keyIdExists($keyId)) { throw new OutOfBoundsException('Key ID not found'); } - return $this->keySet[$keyId]; + return JWK::parseKey($this->keySet[$keyId], $this->defaultAlg); } /** @@ -130,15 +132,43 @@ public function offsetUnset($offset): void throw new LogicException('Method not implemented'); } + /** + * @return array + */ + private function formatJwksForCache(string $jwks): array + { + $jwks = json_decode($jwks, true); + + if (!isset($jwks['keys'])) { + throw new UnexpectedValueException('"keys" member must exist in the JWK Set'); + } + + if (empty($jwks['keys'])) { + throw new InvalidArgumentException('JWK Set did not contain any keys'); + } + + $keys = []; + foreach ($jwks['keys'] as $k => $v) { + $kid = isset($v['kid']) ? $v['kid'] : $k; + $keys[(string) $kid] = $v; + } + + return $keys; + } + private function keyIdExists(string $keyId): bool { if (null === $this->keySet) { $item = $this->getCacheItem(); // Try to load keys from cache if ($item->isHit()) { - // item found! Return it - $jwks = $item->get(); - $this->keySet = JWK::parseKeySet(json_decode($jwks, true), $this->defaultAlg); + // item found! retrieve it + $this->keySet = $item->get(); + // If the cached item is a string, the JWKS response was cached (previous behavior). + // Parse this into expected format array instead. + if (\is_string($this->keySet)) { + $this->keySet = $this->formatJwksForCache($this->keySet); + } } } @@ -148,15 +178,14 @@ private function keyIdExists(string $keyId): bool } $request = $this->httpFactory->createRequest('GET', $this->jwksUri); $jwksResponse = $this->httpClient->sendRequest($request); - $jwks = (string) $jwksResponse->getBody(); - $this->keySet = JWK::parseKeySet(json_decode($jwks, true), $this->defaultAlg); + $this->keySet = $this->formatJwksForCache((string) $jwksResponse->getBody()); if (!isset($this->keySet[$keyId])) { return false; } $item = $this->getCacheItem(); - $item->set($jwks); + $item->set($this->keySet); if ($this->expiresAfter) { $item->expiresAfter($this->expiresAfter); } diff --git a/Jwt/tests/CachedKeySetTest.php b/Jwt/tests/CachedKeySetTest.php index 91d4a27c6ca..04f7994caa5 100644 --- a/Jwt/tests/CachedKeySetTest.php +++ b/Jwt/tests/CachedKeySetTest.php @@ -17,11 +17,12 @@ class CachedKeySetTest extends TestCase private $testJwksUri = 'https://jwk.uri'; private $testJwksUriKey = 'jwkshttpsjwk.uri'; private $testJwks1 = '{"keys": [{"kid":"foo","kty":"RSA","alg":"foo","n":"","e":""}]}'; + private $testCachedJwks1 = ['foo' => ['kid' => 'foo', 'kty' => 'RSA', 'alg' => 'foo', 'n' => '', 'e' => '']]; private $testJwks2 = '{"keys": [{"kid":"bar","kty":"RSA","alg":"bar","n":"","e":""}]}'; private $testJwks3 = '{"keys": [{"kid":"baz","kty":"RSA","n":"","e":""}]}'; private $googleRsaUri = 'https://www.googleapis.com/oauth2/v3/certs'; - // private $googleEcUri = 'https://www.gstatic.com/iap/verify/public_key-jwk'; + private $googleEcUri = 'https://www.gstatic.com/iap/verify/public_key-jwk'; public function testEmptyUriThrowsException() { @@ -117,7 +118,7 @@ public function testKeyIdIsCached() $cacheItem->isHit() ->willReturn(true); $cacheItem->get() - ->willReturn($this->testJwks1); + ->willReturn($this->testCachedJwks1); $cache = $this->prophesize(CacheItemPoolInterface::class); $cache->getItem($this->testJwksUriKey) @@ -136,6 +137,66 @@ public function testKeyIdIsCached() } public function testCachedKeyIdRefresh() + { + $cacheItem = $this->prophesize(CacheItemInterface::class); + $cacheItem->isHit() + ->shouldBeCalledOnce() + ->willReturn(true); + $cacheItem->get() + ->shouldBeCalledOnce() + ->willReturn($this->testCachedJwks1); + $cacheItem->set(Argument::any()) + ->shouldBeCalledOnce() + ->will(function () { + return $this; + }); + + $cache = $this->prophesize(CacheItemPoolInterface::class); + $cache->getItem($this->testJwksUriKey) + ->shouldBeCalledOnce() + ->willReturn($cacheItem->reveal()); + $cache->save(Argument::any()) + ->shouldBeCalledOnce() + ->willReturn(true); + + $cachedKeySet = new CachedKeySet( + $this->testJwksUri, + $this->getMockHttpClient($this->testJwks2), // updated JWK + $this->getMockHttpFactory(), + $cache->reveal() + ); + $this->assertInstanceOf(Key::class, $cachedKeySet['foo']); + $this->assertSame('foo', $cachedKeySet['foo']->getAlgorithm()); + + $this->assertInstanceOf(Key::class, $cachedKeySet['bar']); + $this->assertSame('bar', $cachedKeySet['bar']->getAlgorithm()); + } + + public function testKeyIdIsCachedFromPreviousFormat() + { + $cacheItem = $this->prophesize(CacheItemInterface::class); + $cacheItem->isHit() + ->willReturn(true); + $cacheItem->get() + ->willReturn($this->testJwks1); + + $cache = $this->prophesize(CacheItemPoolInterface::class); + $cache->getItem($this->testJwksUriKey) + ->willReturn($cacheItem->reveal()); + $cache->save(Argument::any()) + ->willReturn(true); + + $cachedKeySet = new CachedKeySet( + $this->testJwksUri, + $this->prophesize(ClientInterface::class)->reveal(), + $this->prophesize(RequestFactoryInterface::class)->reveal(), + $cache->reveal() + ); + $this->assertInstanceOf(Key::class, $cachedKeySet['foo']); + $this->assertSame('foo', $cachedKeySet['foo']->getAlgorithm()); + } + + public function testCachedKeyIdRefreshFromPreviousFormat() { $cacheItem = $this->prophesize(CacheItemInterface::class); $cacheItem->isHit() @@ -213,12 +274,18 @@ public function testJwtVerify() $payload = ['sub' => 'foo', 'exp' => strtotime('+10 seconds')]; $msg = JWT::encode($payload, $privKey1, 'RS256', 'jwk1'); + // format the cached value to match the expected format + $cachedJwks = []; + $rsaKeySet = file_get_contents(__DIR__ . '/data/rsa-jwkset.json'); + foreach (json_decode($rsaKeySet, true)['keys'] as $k => $v) { + $cachedJwks[$v['kid']] = $v; + } + $cacheItem = $this->prophesize(CacheItemInterface::class); $cacheItem->isHit() ->willReturn(true); $cacheItem->get() - ->willReturn(file_get_contents(__DIR__ . '/data/rsa-jwkset.json') - ); + ->willReturn($cachedJwks); $cache = $this->prophesize(CacheItemPoolInterface::class); $cache->getItem($this->testJwksUriKey) @@ -297,7 +364,7 @@ public function provideFullIntegration() { return [ [$this->googleRsaUri], - // [$this->googleEcUri, 'LYyP2g'] + [$this->googleEcUri, 'LYyP2g'] ]; } From f032b1db64038f135f6e2b33d9c1246e751cbb0b Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Thu, 9 Feb 2023 13:01:23 -0800 Subject: [PATCH 202/282] chore(main): release 6.4.0 (firebase/php-jwt#477) --- Jwt/CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Jwt/CHANGELOG.md b/Jwt/CHANGELOG.md index d43c9d8c92c..9242bd30d53 100644 --- a/Jwt/CHANGELOG.md +++ b/Jwt/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [6.4.0](https://github.com/firebase/php-jwt/compare/v6.3.2...v6.4.0) (2023-02-08) + + +### Features + +* add support for W3C ES256K ([#462](https://github.com/firebase/php-jwt/issues/462)) ([213924f](https://github.com/firebase/php-jwt/commit/213924f51936291fbbca99158b11bd4ae56c2c95)) +* improve caching by only decoding jwks when necessary ([#486](https://github.com/firebase/php-jwt/issues/486)) ([78d3ed1](https://github.com/firebase/php-jwt/commit/78d3ed1073553f7d0bbffa6c2010009a0d483d5c)) + ## [6.3.2](https://github.com/firebase/php-jwt/compare/v6.3.1...v6.3.2) (2022-11-01) From a4f8a43fc4162d101bedd51f3947d06000ca3989 Mon Sep 17 00:00:00 2001 From: Akshay Khale Date: Mon, 13 Feb 2023 18:21:14 +0530 Subject: [PATCH 203/282] chore: fix RS256 example in README --- Jwt/README.md | 49 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/Jwt/README.md b/Jwt/README.md index ae2b389563a..e38752c02b2 100644 --- a/Jwt/README.md +++ b/Jwt/README.md @@ -73,28 +73,43 @@ use Firebase\JWT\Key; $privateKey = << Date: Wed, 5 Apr 2023 23:00:14 +0200 Subject: [PATCH 204/282] chore: add ArrayAccess to PHPDoc for JWT::decode (firebase/php-jwt#443) --- Jwt/src/JWT.php | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index 269e8caf0d3..b7ca20d1a5b 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -69,11 +69,15 @@ class JWT * Decodes a JWT string into a PHP object. * * @param string $jwt The JWT - * @param Key|array $keyOrKeyArray The Key or associative array of key IDs (kid) to Key objects. - * If the algorithm used is asymmetric, this is the public key - * Each Key object contains an algorithm and matching key. - * Supported algorithms are 'ES384','ES256', 'HS256', 'HS384', - * 'HS512', 'RS256', 'RS384', and 'RS512' + * @param Key|ArrayAccess|array $keyOrKeyArray The Key or associative array of key IDs + * (kid) to Key objects. + * If the algorithm used is asymmetric, this is + * the public key. + * Each Key object contains an algorithm and + * matching key. + * Supported algorithms are 'ES384','ES256', + * 'HS256', 'HS384', 'HS512', 'RS256', 'RS384' + * and 'RS512'. * * @return stdClass The JWT's payload as a PHP object * From d85e8f7814f2a71fcf12bdf78d8e81613f7ca785 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 26 Apr 2023 09:55:25 -0700 Subject: [PATCH 205/282] chore(docs): add multiple keys example to README (firebase/php-jwt#503) --- Jwt/README.md | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/Jwt/README.md b/Jwt/README.md index e38752c02b2..86d5d102bb6 100644 --- a/Jwt/README.md +++ b/Jwt/README.md @@ -202,6 +202,44 @@ $decoded = JWT::decode($jwt, new Key($publicKey, 'EdDSA')); echo "Decode:\n" . print_r((array) $decoded, true) . "\n"; ```` +Example with multiple keys +-------------------------- +```php +use Firebase\JWT\JWT; +use Firebase\JWT\Key; + +// Example RSA keys from previous example +// $privateKey1 = '...'; +// $publicKey1 = '...'; + +// Example EdDSA keys from previous example +// $privateKey2 = '...'; +// $publicKey2 = '...'; + +$payload = [ + 'iss' => 'example.org', + 'aud' => 'example.com', + 'iat' => 1356999524, + 'nbf' => 1357000000 +]; + +$jwt1 = JWT::encode($payload, $privateKey1, 'RS256', 'kid1'); +$jwt2 = JWT::encode($payload, $privateKey2, 'EdDSA', 'kid2'); +echo "Encode 1:\n" . print_r($jwt1, true) . "\n"; +echo "Encode 2:\n" . print_r($jwt2, true) . "\n"; + +$keys = [ + 'kid1' => new Key($publicKey1, 'RS256'), + 'kid2' => new Key($publicKey2, 'EdDSA'), +]; + +$decoded1 = JWT::decode($jwt1, $keys); +$decoded2 = JWT::decode($jwt2, $keys); + +echo "Decode 1:\n" . print_r((array) $decoded1, true) . "\n"; +echo "Decode 2:\n" . print_r((array) $decoded2, true) . "\n"; +``` + Using JWKs ---------- From ab23121e2564eda614f5d9e91efc697a16d94237 Mon Sep 17 00:00:00 2001 From: Alexey Kopytko Date: Thu, 4 May 2023 06:26:21 +0900 Subject: [PATCH 206/282] chore: skip null checks when input is never null (firebase/php-jwt#502) --- Jwt/src/JWT.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index b7ca20d1a5b..61524e7b797 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -380,7 +380,7 @@ public static function jsonEncode(array $input): string } if ($errno = \json_last_error()) { self::handleJsonError($errno); - } elseif ($json === 'null' && $input !== null) { + } elseif ($json === 'null') { throw new DomainException('Null result with non-null input'); } if ($json === false) { From f410efff74fffed341dc7fe6221303e69dcdc896 Mon Sep 17 00:00:00 2001 From: Saransh Dhingra Date: Thu, 4 May 2023 06:22:43 +0530 Subject: [PATCH 207/282] chore(docs): example of unsafe header decode in README (firebase/php-jwt#501) --- Jwt/README.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/Jwt/README.md b/Jwt/README.md index 86d5d102bb6..f0382667361 100644 --- a/Jwt/README.md +++ b/Jwt/README.md @@ -65,6 +65,40 @@ $decoded_array = (array) $decoded; JWT::$leeway = 60; // $leeway in seconds $decoded = JWT::decode($jwt, new Key($key, 'HS256')); ``` +Example encode/decode headers +------- +Decoding the JWT headers without verifying the JWT first is NOT recommended, and is not supported by +this library. This is because without verifying the JWT, the header values could have been tampered with. +Any value pulled from an unverified header should be treated as if it could be any string sent in from an +attacker. If this is something you still want to do in your application for whatever reason, it's possible to +decode the header values manually simply by calling `json_decode` and `base64_decode` on the JWT +header part: +```php +use Firebase\JWT\JWT; + +$key = 'example_key'; +$payload = [ + 'iss' => 'http://example.org', + 'aud' => 'http://example.com', + 'iat' => 1356999524, + 'nbf' => 1357000000 +]; + +$headers = [ + 'x-forwarded-for' => 'www.google.com' +]; + +// Encode headers in the JWT string +$jwt = JWT::encode($payload, $key, 'HS256', null, $headers); + +// Decode headers from the JWT string WITHOUT validation +// **IMPORTANT**: This operation is vulnerable to attacks, as the JWT has not yet been verified. +// These headers could be any value sent by an attacker. +list($headersB64, $payloadB64, $sig) = explode('.', $jwt); +$decoded = json_decode(base64_decode($headersB64), true); + +print_r($decoded); +``` Example with RS256 (openssl) ---------------------------- ```php From 4d649d2b2daf55e24ef19b533b46ad99de7f0f9f Mon Sep 17 00:00:00 2001 From: Ajumal Date: Tue, 9 May 2023 20:36:06 +0530 Subject: [PATCH 208/282] fix: Allow KID index 0 --- Jwt/src/JWT.php | 2 +- Jwt/tests/JWTTest.php | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index 61524e7b797..421c42c2970 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -439,7 +439,7 @@ private static function getKey( return $keyOrKeyArray; } - if (empty($kid)) { + if (!isset($kid)) { throw new UnexpectedValueException('"kid" empty, unable to lookup correct key'); } diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index 3ce912ed02b..a5721d984f7 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -204,10 +204,11 @@ public function testEmptyKeyFails() public function testKIDChooser() { $keys = [ - '1' => new Key('my_key', 'HS256'), + '0' => new Key('my_key0', 'HS256'), + '1' => new Key('my_key1', 'HS256'), '2' => new Key('my_key2', 'HS256') ]; - $msg = JWT::encode(['message' => 'abc'], $keys['1']->getKeyMaterial(), 'HS256', '1'); + $msg = JWT::encode(['message' => 'abc'], $keys['0']->getKeyMaterial(), 'HS256', '0'); $decoded = JWT::decode($msg, $keys); $expected = new stdClass(); $expected->message = 'abc'; @@ -217,10 +218,11 @@ public function testKIDChooser() public function testArrayAccessKIDChooser() { $keys = new ArrayObject([ - '1' => new Key('my_key', 'HS256'), + '0' => new Key('my_key0', 'HS256'), + '1' => new Key('my_key1', 'HS256'), '2' => new Key('my_key2', 'HS256'), ]); - $msg = JWT::encode(['message' => 'abc'], $keys['1']->getKeyMaterial(), 'HS256', '1'); + $msg = JWT::encode(['message' => 'abc'], $keys['0']->getKeyMaterial(), 'HS256', '0'); $decoded = JWT::decode($msg, $keys); $expected = new stdClass(); $expected->message = 'abc'; From 9d9be021abe3a79524247689487f55500ddaf998 Mon Sep 17 00:00:00 2001 From: Ajumal Date: Wed, 10 May 2023 18:08:43 +0530 Subject: [PATCH 209/282] fix: Allow KID index 0 --- Jwt/src/JWT.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index 421c42c2970..c83ff09905a 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -439,7 +439,7 @@ private static function getKey( return $keyOrKeyArray; } - if (!isset($kid)) { + if (empty($kid) && $kid !== '0') { throw new UnexpectedValueException('"kid" empty, unable to lookup correct key'); } From a9ea8067141d9f4e190fe277879a05ece8028dac Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Fri, 12 May 2023 09:11:51 -0600 Subject: [PATCH 210/282] chore: drop support for PHP 7.3 (firebase/php-jwt#495) --- Jwt/.github/workflows/tests.yml | 2 +- Jwt/composer.json | 6 +++--- Jwt/tests/CachedKeySetTest.php | 3 +++ 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Jwt/.github/workflows/tests.yml b/Jwt/.github/workflows/tests.yml index 6956aa9504a..b29c8018682 100644 --- a/Jwt/.github/workflows/tests.yml +++ b/Jwt/.github/workflows/tests.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php: [ "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2" ] + php: [ "7.4", "8.0", "8.1", "8.2" ] name: PHP ${{matrix.php }} Unit Test steps: - uses: actions/checkout@v2 diff --git a/Jwt/composer.json b/Jwt/composer.json index c9aa3dbbc26..e23dfe378d8 100644 --- a/Jwt/composer.json +++ b/Jwt/composer.json @@ -20,7 +20,7 @@ ], "license": "BSD-3-Clause", "require": { - "php": "^7.1||^8.0" + "php": "^7.4||^8.0" }, "suggest": { "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present", @@ -33,8 +33,8 @@ }, "require-dev": { "guzzlehttp/guzzle": "^6.5||^7.4", - "phpspec/prophecy-phpunit": "^1.1", - "phpunit/phpunit": "^7.5||^9.5", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", "psr/cache": "^1.0||^2.0", "psr/http-client": "^1.0", "psr/http-factory": "^1.0" diff --git a/Jwt/tests/CachedKeySetTest.php b/Jwt/tests/CachedKeySetTest.php index 04f7994caa5..9142fda604c 100644 --- a/Jwt/tests/CachedKeySetTest.php +++ b/Jwt/tests/CachedKeySetTest.php @@ -6,6 +6,7 @@ use OutOfBoundsException; use PHPUnit\Framework\TestCase; use Prophecy\Argument; +use Prophecy\PhpUnit\ProphecyTrait; use Psr\Cache\CacheItemInterface; use Psr\Cache\CacheItemPoolInterface; use Psr\Http\Client\ClientInterface; @@ -14,6 +15,8 @@ class CachedKeySetTest extends TestCase { + use ProphecyTrait; + private $testJwksUri = 'https://jwk.uri'; private $testJwksUriKey = 'jwkshttpsjwk.uri'; private $testJwks1 = '{"keys": [{"kid":"foo","kty":"RSA","alg":"foo","n":"","e":""}]}'; From f93f9f1d1b6f8ca22d9176d55e2f2e3f9af64250 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Fri, 12 May 2023 08:44:11 -0700 Subject: [PATCH 211/282] chore: release 6.5.0 Release-As: 6.5.0 From 8e55034a4d3e22b9cd02c312bbe8ff0fcca43af0 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Fri, 12 May 2023 08:47:07 -0700 Subject: [PATCH 212/282] chore(main): release 6.5.0 (firebase/php-jwt#506) --- Jwt/CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Jwt/CHANGELOG.md b/Jwt/CHANGELOG.md index 9242bd30d53..35e97fe86b1 100644 --- a/Jwt/CHANGELOG.md +++ b/Jwt/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## [6.5.0](https://github.com/firebase/php-jwt/compare/v6.4.0...v6.5.0) (2023-05-12) + + +### Bug Fixes + +* allow KID of '0' ([#505](https://github.com/firebase/php-jwt/issues/505)) ([9dc46a9](https://github.com/firebase/php-jwt/commit/9dc46a9c3e5801294249cfd2554c5363c9f9326a)) + + +### Miscellaneous Chores + +* drop support for PHP 7.3 ([#495](https://github.com/firebase/php-jwt/issues/495)) + ## [6.4.0](https://github.com/firebase/php-jwt/compare/v6.3.2...v6.4.0) (2023-02-08) From b630dab6df10fb720f3276c43d401d37d8726003 Mon Sep 17 00:00:00 2001 From: Sachin Bahukhandi Date: Fri, 12 May 2023 23:46:34 +0530 Subject: [PATCH 213/282] chore(docs): add missing imports for example in README (firebase/php-jwt#507) --- Jwt/README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Jwt/README.md b/Jwt/README.md index f0382667361..5b07aa7d2d9 100644 --- a/Jwt/README.md +++ b/Jwt/README.md @@ -70,8 +70,8 @@ Example encode/decode headers Decoding the JWT headers without verifying the JWT first is NOT recommended, and is not supported by this library. This is because without verifying the JWT, the header values could have been tampered with. Any value pulled from an unverified header should be treated as if it could be any string sent in from an -attacker. If this is something you still want to do in your application for whatever reason, it's possible to -decode the header values manually simply by calling `json_decode` and `base64_decode` on the JWT +attacker. If this is something you still want to do in your application for whatever reason, it's possible to +decode the header values manually simply by calling `json_decode` and `base64_decode` on the JWT header part: ```php use Firebase\JWT\JWT; @@ -373,6 +373,8 @@ All exceptions in the `Firebase\JWT` namespace extend `UnexpectedValueException` like this: ```php +use Firebase\JWT\JWT; +use UnexpectedValueException; try { $decoded = JWT::decode($payload, $keys); } catch (LogicException $e) { From a58c197205f9d8161233b7469348fef6944201e9 Mon Sep 17 00:00:00 2001 From: croensch Date: Tue, 23 May 2023 15:57:20 +0200 Subject: [PATCH 214/282] fix: only check iat if nbf is not used (firebase/php-jwt#493) --- Jwt/src/JWT.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index c83ff09905a..7e190a3e870 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -156,7 +156,7 @@ public static function decode( // Check that this token has been created before 'now'. This prevents // using tokens that have been created for later use (and haven't // correctly used the nbf claim). - if (isset($payload->iat) && $payload->iat > ($timestamp + static::$leeway)) { + if (!isset($payload->nbf) && isset($payload->iat) && $payload->iat > ($timestamp + static::$leeway)) { throw new BeforeValidException( 'Cannot handle token prior to ' . \date(DateTime::ISO8601, $payload->iat) ); From e2cc8b1736db2c8b506a5da1ad641b845a9c05fe Mon Sep 17 00:00:00 2001 From: Pinchon Karim Date: Tue, 13 Jun 2023 18:35:01 +0200 Subject: [PATCH 215/282] feat: allow get headers when decoding token (firebase/php-jwt#442) Co-authored-by: Vishwaraj Anand Co-authored-by: Brent Shaffer --- Jwt/README.md | 6 ++++++ Jwt/src/JWT.php | 7 ++++++- Jwt/tests/JWTTest.php | 15 +++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/Jwt/README.md b/Jwt/README.md index 5b07aa7d2d9..f2cc5d03ec2 100644 --- a/Jwt/README.md +++ b/Jwt/README.md @@ -45,8 +45,14 @@ $payload = [ */ $jwt = JWT::encode($payload, $key, 'HS256'); $decoded = JWT::decode($jwt, new Key($key, 'HS256')); +print_r($decoded); + +// Pass a stdClass in as the third parameter to get the decoded header values +$decoded = JWT::decode($jwt, new Key($key, 'HS256'), $headers = new stdClass()); +print_r($headers); print_r($decoded); +print_r($headers); /* NOTE: This will now be an object instead of an associative array. To get diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index 7e190a3e870..7ffb98521c1 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -78,6 +78,7 @@ class JWT * Supported algorithms are 'ES384','ES256', * 'HS256', 'HS384', 'HS512', 'RS256', 'RS384' * and 'RS512'. + * @param stdClass $headers Optional. Populates stdClass with headers. * * @return stdClass The JWT's payload as a PHP object * @@ -94,7 +95,8 @@ class JWT */ public static function decode( string $jwt, - $keyOrKeyArray + $keyOrKeyArray, + stdClass &$headers = null ): stdClass { // Validate JWT $timestamp = \is_null(static::$timestamp) ? \time() : static::$timestamp; @@ -111,6 +113,9 @@ public static function decode( if (null === ($header = static::jsonDecode($headerRaw))) { throw new UnexpectedValueException('Invalid header encoding'); } + if ($headers !== null) { + $headers = $header; + } $payloadRaw = static::urlsafeB64Decode($bodyb64); if (null === ($payload = static::jsonDecode($payloadRaw))) { throw new UnexpectedValueException('Invalid claims encoding'); diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index a5721d984f7..7d49bf04b6c 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -397,4 +397,19 @@ public function testEncodeDecodeWithResource() $this->assertSame('bar', $decoded->foo); } + + public function testGetHeaders() + { + $payload = [ + 'message' => 'abc', + 'exp' => time() + JWT::$leeway + 20, // time in the future + ]; + $headers = new stdClass(); + + $encoded = JWT::encode($payload, 'my_key', 'HS256'); + JWT::decode($encoded, new Key('my_key', 'HS256'), $headers); + + $this->assertEquals($headers->typ, 'JWT'); + $this->assertEquals($headers->alg, 'HS256'); + } } From a12cd7e654ad84fd94a8ec89ab154311510c00f8 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Tue, 13 Jun 2023 11:08:24 -0600 Subject: [PATCH 216/282] chore: update README --- Jwt/README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/Jwt/README.md b/Jwt/README.md index f2cc5d03ec2..701de23a828 100644 --- a/Jwt/README.md +++ b/Jwt/README.md @@ -51,9 +51,6 @@ print_r($decoded); $decoded = JWT::decode($jwt, new Key($key, 'HS256'), $headers = new stdClass()); print_r($headers); -print_r($decoded); -print_r($headers); - /* NOTE: This will now be an object instead of an associative array. To get an associative array, you will need to cast it as such: From f4ff84fb9748a6c5e4c0edd0fe9ab5653e4770bb Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 13 Jun 2023 11:11:06 -0600 Subject: [PATCH 217/282] chore(main): release 6.6.0 (firebase/php-jwt#511) --- Jwt/CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Jwt/CHANGELOG.md b/Jwt/CHANGELOG.md index 35e97fe86b1..c74fd13194e 100644 --- a/Jwt/CHANGELOG.md +++ b/Jwt/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## [6.6.0](https://github.com/firebase/php-jwt/compare/v6.5.0...v6.6.0) (2023-06-13) + + +### Features + +* allow get headers when decoding token ([#442](https://github.com/firebase/php-jwt/issues/442)) ([fb85f47](https://github.com/firebase/php-jwt/commit/fb85f47cfaeffdd94faf8defdf07164abcdad6c3)) + + +### Bug Fixes + +* only check iat if nbf is not used ([#493](https://github.com/firebase/php-jwt/issues/493)) ([398ccd2](https://github.com/firebase/php-jwt/commit/398ccd25ea12fa84b9e4f1085d5ff448c21ec797)) + ## [6.5.0](https://github.com/firebase/php-jwt/compare/v6.4.0...v6.5.0) (2023-05-12) From 27d42328282473e7df5ce6488a7334354fc3d6d8 Mon Sep 17 00:00:00 2001 From: Eduardo Dobay Date: Wed, 14 Jun 2023 12:10:17 -0300 Subject: [PATCH 218/282] feat: add ed25519 support to JWK (public keys) (firebase/php-jwt#452) --- Jwt/src/JWK.php | 28 +++++++++++++++++++++++++++- Jwt/src/JWT.php | 21 ++++++++++++++++++--- Jwt/tests/JWKTest.php | 1 + Jwt/tests/data/ed25519-jwkset.json | 11 +++++++++++ 4 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 Jwt/tests/data/ed25519-jwkset.json diff --git a/Jwt/src/JWK.php b/Jwt/src/JWK.php index c7eff8ae483..873ab41a0f9 100644 --- a/Jwt/src/JWK.php +++ b/Jwt/src/JWK.php @@ -31,6 +31,12 @@ class JWK // 'P-521' => '1.3.132.0.35', // Len: 132 (not supported) ]; + // For keys with "kty" equal to "OKP" (Octet Key Pair), the "crv" parameter must contain the key subtype. + // This library supports the following subtypes: + private const OKP_SUBTYPES = [ + 'Ed25519' => true, // RFC 8037 + ]; + /** * Parse a set of JWK keys * @@ -145,8 +151,28 @@ public static function parseKey(array $jwk, string $defaultAlg = null): ?Key $publicKey = self::createPemFromCrvAndXYCoordinates($jwk['crv'], $jwk['x'], $jwk['y']); return new Key($publicKey, $jwk['alg']); + case 'OKP': + if (isset($jwk['d'])) { + // The key is actually a private key + throw new UnexpectedValueException('Key data must be for a public key'); + } + + if (!isset($jwk['crv'])) { + throw new UnexpectedValueException('crv not set'); + } + + if (empty(self::OKP_SUBTYPES[$jwk['crv']])) { + throw new DomainException('Unrecognised or unsupported OKP key subtype'); + } + + if (empty($jwk['x'])) { + throw new UnexpectedValueException('x not set'); + } + + // This library works internally with EdDSA keys (Ed25519) encoded in standard base64. + $publicKey = JWT::convertBase64urlToBase64($jwk['x']); + return new Key($publicKey, $jwk['alg']); default: - // Currently only RSA is supported break; } diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index 7ffb98521c1..56cb9314e3f 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -220,7 +220,7 @@ public static function encode( * * @param string $msg The message to sign * @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $key The secret key. - * @param string $alg Supported algorithms are 'ES384','ES256', 'ES256K', 'HS256', + * @param string $alg Supported algorithms are 'EdDSA', 'ES384', 'ES256', 'ES256K', 'HS256', * 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512' * * @return string An encrypted message @@ -283,7 +283,7 @@ public static function sign( * * @param string $msg The original message (header and body) * @param string $signature The original signature - * @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $keyMaterial For HS*, a string key works. for RS*, must be an instance of OpenSSLAsymmetricKey + * @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $keyMaterial For Ed*, ES*, HS*, a string key works. for RS*, must be an instance of OpenSSLAsymmetricKey * @param string $alg The algorithm * * @return bool @@ -404,13 +404,28 @@ public static function jsonEncode(array $input): string * @throws InvalidArgumentException invalid base64 characters */ public static function urlsafeB64Decode(string $input): string + { + return \base64_decode(self::convertBase64UrlToBase64($input)); + } + + /** + * Convert a string in the base64url (URL-safe Base64) encoding to standard base64. + * + * @param string $input A Base64 encoded string with URL-safe characters (-_ and no padding) + * + * @return string A Base64 encoded string with standard characters (+/) and padding (=), when + * needed. + * + * @see https://www.rfc-editor.org/rfc/rfc4648 + */ + public static function convertBase64UrlToBase64(string $input): string { $remainder = \strlen($input) % 4; if ($remainder) { $padlen = 4 - $remainder; $input .= \str_repeat('=', $padlen); } - return \base64_decode(\strtr($input, '-_', '+/')); + return \strtr($input, '-_', '+/'); } /** diff --git a/Jwt/tests/JWKTest.php b/Jwt/tests/JWKTest.php index 93afea70db2..4e1b0c678a7 100644 --- a/Jwt/tests/JWKTest.php +++ b/Jwt/tests/JWKTest.php @@ -151,6 +151,7 @@ public function provideDecodeByJwkKeySet() return [ ['rsa1-private.pem', 'rsa-jwkset.json', 'RS256'], ['ecdsa256-private.pem', 'ec-jwkset.json', 'ES256'], + ['ed25519-1.sec', 'ed25519-jwkset.json', 'EdDSA'], ]; } diff --git a/Jwt/tests/data/ed25519-jwkset.json b/Jwt/tests/data/ed25519-jwkset.json new file mode 100644 index 00000000000..186b8e29b73 --- /dev/null +++ b/Jwt/tests/data/ed25519-jwkset.json @@ -0,0 +1,11 @@ +{ + "keys": [ + { + "kid": "jwk1", + "alg": "EdDSA", + "kty": "OKP", + "crv": "Ed25519", + "x": "uOSJMhbKSG4V5xUHS7B9YHmVg_1yVd-G-Io6oBFhSfY" + } + ] +} From 318b4e689079e3f1ba4107183c354e5de201f470 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Wed, 14 Jun 2023 08:29:26 -0700 Subject: [PATCH 219/282] chore(main): release 6.7.0 (firebase/php-jwt#518) --- Jwt/CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Jwt/CHANGELOG.md b/Jwt/CHANGELOG.md index c74fd13194e..ee78825be71 100644 --- a/Jwt/CHANGELOG.md +++ b/Jwt/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [6.7.0](https://github.com/firebase/php-jwt/compare/v6.6.0...v6.7.0) (2023-06-14) + + +### Features + +* add ed25519 support to JWK (public keys) ([#452](https://github.com/firebase/php-jwt/issues/452)) ([e53979a](https://github.com/firebase/php-jwt/commit/e53979abae927de916a75b9d239cfda8ce32be2a)) + ## [6.6.0](https://github.com/firebase/php-jwt/compare/v6.5.0...v6.6.0) (2023-06-13) From 25081c3dddb62a7b94ba6526d07ee4cbf3c8c93b Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 14 Jun 2023 13:26:33 -0600 Subject: [PATCH 220/282] fix: handle invalid http responses (firebase/php-jwt#508) --- Jwt/src/CachedKeySet.php | 10 ++++++++++ Jwt/tests/CachedKeySetTest.php | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/Jwt/src/CachedKeySet.php b/Jwt/src/CachedKeySet.php index baf801f13f0..ee529f9f50a 100644 --- a/Jwt/src/CachedKeySet.php +++ b/Jwt/src/CachedKeySet.php @@ -178,6 +178,16 @@ private function keyIdExists(string $keyId): bool } $request = $this->httpFactory->createRequest('GET', $this->jwksUri); $jwksResponse = $this->httpClient->sendRequest($request); + if ($jwksResponse->getStatusCode() !== 200) { + throw new UnexpectedValueException( + sprintf('HTTP Error: %d %s for URI "%s"', + $jwksResponse->getStatusCode(), + $jwksResponse->getReasonPhrase(), + $this->jwksUri, + ), + $jwksResponse->getStatusCode() + ); + } $this->keySet = $this->formatJwksForCache((string) $jwksResponse->getBody()); if (!isset($this->keySet[$keyId])) { diff --git a/Jwt/tests/CachedKeySetTest.php b/Jwt/tests/CachedKeySetTest.php index 9142fda604c..1e73af6df7d 100644 --- a/Jwt/tests/CachedKeySetTest.php +++ b/Jwt/tests/CachedKeySetTest.php @@ -88,6 +88,37 @@ public function testOutOfBoundsThrowsException() $cachedKeySet['bar']; } + public function testInvalidHttpResponseThrowsException() + { + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessage('HTTP Error: 404 URL not found'); + $this->expectExceptionCode(404); + + $body = $this->prophesize('Psr\Http\Message\StreamInterface'); + + $response = $this->prophesize('Psr\Http\Message\ResponseInterface'); + $response->getStatusCode() + ->shouldBeCalled() + ->willReturn(404); + $response->getReasonPhrase() + ->shouldBeCalledTimes(1) + ->willReturn('URL not found'); + + $http = $this->prophesize(ClientInterface::class); + $http->sendRequest(Argument::any()) + ->shouldBeCalledTimes(1) + ->willReturn($response->reveal()); + + $cachedKeySet = new CachedKeySet( + $this->testJwksUri, + $http->reveal(), + $this->getMockHttpFactory(), + $this->getMockEmptyCache() + ); + + isset($cachedKeySet[0]); + } + public function testWithExistingKeyId() { $cachedKeySet = new CachedKeySet( @@ -382,6 +413,9 @@ private function getMockHttpClient($testJwks, int $timesCalled = 1) $response->getBody() ->shouldBeCalledTimes($timesCalled) ->willReturn($body->reveal()); + $response->getStatusCode() + ->shouldBeCalledTimes($timesCalled) + ->willReturn(200); $http = $this->prophesize(ClientInterface::class); $http->sendRequest(Argument::any()) From c636c61ec3e99ff74b1bb132b7e7e0dedb4c5d33 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 14 Jun 2023 12:28:03 -0700 Subject: [PATCH 221/282] chore(tests): remove unused variable --- Jwt/tests/CachedKeySetTest.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/Jwt/tests/CachedKeySetTest.php b/Jwt/tests/CachedKeySetTest.php index 1e73af6df7d..2e4e1f62fa9 100644 --- a/Jwt/tests/CachedKeySetTest.php +++ b/Jwt/tests/CachedKeySetTest.php @@ -94,8 +94,6 @@ public function testInvalidHttpResponseThrowsException() $this->expectExceptionMessage('HTTP Error: 404 URL not found'); $this->expectExceptionCode(404); - $body = $this->prophesize('Psr\Http\Message\StreamInterface'); - $response = $this->prophesize('Psr\Http\Message\ResponseInterface'); $response->getStatusCode() ->shouldBeCalled() From a8568fc5cb2ea2858bcf9b01b8de80989dbbac48 Mon Sep 17 00:00:00 2001 From: lleyton Date: Wed, 14 Jun 2023 12:28:31 -0700 Subject: [PATCH 222/282] feat: add support for P-384 curve (firebase/php-jwt#515) --- Jwt/src/JWK.php | 4 ++-- Jwt/tests/JWKTest.php | 11 ++++++----- Jwt/tests/data/ec-jwkset.json | 9 +++++++++ 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/Jwt/src/JWK.php b/Jwt/src/JWK.php index 873ab41a0f9..63fb2484b32 100644 --- a/Jwt/src/JWK.php +++ b/Jwt/src/JWK.php @@ -27,7 +27,7 @@ class JWK private const EC_CURVES = [ 'P-256' => '1.2.840.10045.3.1.7', // Len: 64 'secp256k1' => '1.3.132.0.10', // Len: 64 - // 'P-384' => '1.3.132.0.34', // Len: 96 (not yet supported) + 'P-384' => '1.3.132.0.34', // Len: 96 // 'P-521' => '1.3.132.0.35', // Len: 132 (not supported) ]; @@ -182,7 +182,7 @@ public static function parseKey(array $jwk, string $defaultAlg = null): ?Key /** * Converts the EC JWK values to pem format. * - * @param string $crv The EC curve (only P-256 is supported) + * @param string $crv The EC curve (only P-256 & P-384 is supported) * @param string $x The EC x-coordinate * @param string $y The EC y-coordinate * diff --git a/Jwt/tests/JWKTest.php b/Jwt/tests/JWKTest.php index 4e1b0c678a7..01082a40818 100644 --- a/Jwt/tests/JWKTest.php +++ b/Jwt/tests/JWKTest.php @@ -129,11 +129,11 @@ public function testDecodeByJwkKeySetTokenExpired() /** * @dataProvider provideDecodeByJwkKeySet */ - public function testDecodeByJwkKeySet($pemFile, $jwkFile, $alg) + public function testDecodeByJwkKeySet($pemFile, $jwkFile, $alg, $keyId) { $privKey1 = file_get_contents(__DIR__ . '/data/' . $pemFile); $payload = ['sub' => 'foo', 'exp' => strtotime('+10 seconds')]; - $msg = JWT::encode($payload, $privKey1, $alg, 'jwk1'); + $msg = JWT::encode($payload, $privKey1, $alg, $keyId); $jwkSet = json_decode( file_get_contents(__DIR__ . '/data/' . $jwkFile), @@ -149,9 +149,10 @@ public function testDecodeByJwkKeySet($pemFile, $jwkFile, $alg) public function provideDecodeByJwkKeySet() { return [ - ['rsa1-private.pem', 'rsa-jwkset.json', 'RS256'], - ['ecdsa256-private.pem', 'ec-jwkset.json', 'ES256'], - ['ed25519-1.sec', 'ed25519-jwkset.json', 'EdDSA'], + ['rsa1-private.pem', 'rsa-jwkset.json', 'RS256', 'jwk1'], + ['ecdsa256-private.pem', 'ec-jwkset.json', 'ES256', 'jwk1'], + ['ecdsa384-private.pem', 'ec-jwkset.json', 'ES384', 'jwk4'], + ['ed25519-1.sec', 'ed25519-jwkset.json', 'EdDSA', 'jwk1'], ]; } diff --git a/Jwt/tests/data/ec-jwkset.json b/Jwt/tests/data/ec-jwkset.json index 213f68acdfd..50c5b24e38d 100644 --- a/Jwt/tests/data/ec-jwkset.json +++ b/Jwt/tests/data/ec-jwkset.json @@ -26,6 +26,15 @@ "x": "EFpwNuP322bU3WP1DtJgx67L0CUV1MxNixqPVMH2L9Q", "y": "_fSTbijIJjpsqL16cIEvxxf3MaYMY8MbqEq066yV9ls", "alg": "ES256K" + }, + { + "kty": "EC", + "use": "sig", + "crv": "P-384", + "kid": "jwk4", + "x": "FhXXcyKmWkTkdVbWYYU3dtJqpJ0JmLGftEdNzUEFEKSU5MlnLr_FjcneszvXAqEB", + "y": "M4veJF_dO_zhFk44bh_ELXbp0_nn9QaViVtQpuTvpu29eefx6PfUMqX0K--IS4NQ", + "alg": "ES384" } ] } \ No newline at end of file From d72589af47a780e645d786d24b32e19b489a1f02 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 20 Jun 2023 09:45:35 -0700 Subject: [PATCH 223/282] chore(main): release 6.8.0 (firebase/php-jwt#519) --- Jwt/CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Jwt/CHANGELOG.md b/Jwt/CHANGELOG.md index ee78825be71..9638f2dd7b3 100644 --- a/Jwt/CHANGELOG.md +++ b/Jwt/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## [6.8.0](https://github.com/firebase/php-jwt/compare/v6.7.0...v6.8.0) (2023-06-14) + + +### Features + +* add support for P-384 curve ([#515](https://github.com/firebase/php-jwt/issues/515)) ([5de4323](https://github.com/firebase/php-jwt/commit/5de4323f4baf4d70bca8663bd87682a69c656c3d)) + + +### Bug Fixes + +* handle invalid http responses ([#508](https://github.com/firebase/php-jwt/issues/508)) ([91c39c7](https://github.com/firebase/php-jwt/commit/91c39c72b22fc3e1191e574089552c1f2041c718)) + ## [6.7.0](https://github.com/firebase/php-jwt/compare/v6.6.0...v6.7.0) (2023-06-14) From 3bfd4758ba89a0e0f82621e321fd74e4ad20db28 Mon Sep 17 00:00:00 2001 From: croensch Date: Wed, 28 Jun 2023 20:25:09 +0200 Subject: [PATCH 224/282] fix: accept float claims but round down to ignore them (firebase/php-jwt#492) --- Jwt/src/JWT.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index 56cb9314e3f..db075ad0064 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -152,18 +152,18 @@ public static function decode( // Check the nbf if it is defined. This is the time that the // token can actually be used. If it's not yet that time, abort. - if (isset($payload->nbf) && $payload->nbf > ($timestamp + static::$leeway)) { + if (isset($payload->nbf) && floor($payload->nbf) > ($timestamp + static::$leeway)) { throw new BeforeValidException( - 'Cannot handle token prior to ' . \date(DateTime::ISO8601, $payload->nbf) + 'Cannot handle token prior to ' . \date(DateTime::ISO8601, (int) $payload->nbf) ); } // Check that this token has been created before 'now'. This prevents // using tokens that have been created for later use (and haven't // correctly used the nbf claim). - if (!isset($payload->nbf) && isset($payload->iat) && $payload->iat > ($timestamp + static::$leeway)) { + if (!isset($payload->nbf) && isset($payload->iat) && floor($payload->iat) > ($timestamp + static::$leeway)) { throw new BeforeValidException( - 'Cannot handle token prior to ' . \date(DateTime::ISO8601, $payload->iat) + 'Cannot handle token prior to ' . \date(DateTime::ISO8601, (int) $payload->iat) ); } From db799262cc565f7920ef60a70e5840a4f010f6ad Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Fri, 30 Jun 2023 13:08:13 -0700 Subject: [PATCH 225/282] chore: add tests for latest fixes (firebase/php-jwt#512) --- Jwt/tests/JWTTest.php | 80 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 74 insertions(+), 6 deletions(-) diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index 7d49bf04b6c..5265e47138a 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -76,6 +76,9 @@ public function testValidToken() $this->assertSame($decoded->message, 'abc'); } + /** + * @runInSeparateProcess + */ public function testValidTokenWithLeeway() { JWT::$leeway = 60; @@ -86,9 +89,11 @@ public function testValidTokenWithLeeway() $encoded = JWT::encode($payload, 'my_key', 'HS256'); $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); $this->assertSame($decoded->message, 'abc'); - JWT::$leeway = 0; } + /** + * @runInSeparateProcess + */ public function testExpiredTokenWithLeeway() { JWT::$leeway = 60; @@ -100,7 +105,6 @@ public function testExpiredTokenWithLeeway() $encoded = JWT::encode($payload, 'my_key', 'HS256'); $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); $this->assertSame($decoded->message, 'abc'); - JWT::$leeway = 0; } public function testValidTokenWithNbf() @@ -116,6 +120,9 @@ public function testValidTokenWithNbf() $this->assertSame($decoded->message, 'abc'); } + /** + * @runInSeparateProcess + */ public function testValidTokenWithNbfLeeway() { JWT::$leeway = 60; @@ -126,9 +133,11 @@ public function testValidTokenWithNbfLeeway() $encoded = JWT::encode($payload, 'my_key', 'HS256'); $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); $this->assertSame($decoded->message, 'abc'); - JWT::$leeway = 0; } + /** + * @runInSeparateProcess + */ public function testInvalidTokenWithNbfLeeway() { JWT::$leeway = 60; @@ -139,9 +148,45 @@ public function testInvalidTokenWithNbfLeeway() $encoded = JWT::encode($payload, 'my_key', 'HS256'); $this->expectException(BeforeValidException::class); JWT::decode($encoded, new Key('my_key', 'HS256')); - JWT::$leeway = 0; } + public function testValidTokenWithNbfIgnoresIat() + { + $payload = [ + 'message' => 'abc', + 'nbf' => time() - 20, // time in the future + 'iat' => time() + 20, // time in the past + ]; + $encoded = JWT::encode($payload, 'my_key', 'HS256'); + $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); + $this->assertEquals('abc', $decoded->message); + } + + public function testValidTokenWithNbfMicrotime() + { + $payload = [ + 'message' => 'abc', + 'nbf' => microtime(true), // use microtime + ]; + $encoded = JWT::encode($payload, 'my_key', 'HS256'); + $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); + $this->assertEquals('abc', $decoded->message); + } + + public function testInvalidTokenWithNbfMicrotime() + { + $this->expectException(BeforeValidException::class); + $payload = [ + 'message' => 'abc', + 'nbf' => microtime(true) + 20, // use microtime in the future + ]; + $encoded = JWT::encode($payload, 'my_key', 'HS256'); + JWT::decode($encoded, new Key('my_key', 'HS256')); + } + + /** + * @runInSeparateProcess + */ public function testValidTokenWithIatLeeway() { JWT::$leeway = 60; @@ -152,9 +197,11 @@ public function testValidTokenWithIatLeeway() $encoded = JWT::encode($payload, 'my_key', 'HS256'); $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); $this->assertSame($decoded->message, 'abc'); - JWT::$leeway = 0; } + /** + * @runInSeparateProcess + */ public function testInvalidTokenWithIatLeeway() { JWT::$leeway = 60; @@ -165,7 +212,28 @@ public function testInvalidTokenWithIatLeeway() $encoded = JWT::encode($payload, 'my_key', 'HS256'); $this->expectException(BeforeValidException::class); JWT::decode($encoded, new Key('my_key', 'HS256')); - JWT::$leeway = 0; + } + + public function testValidTokenWithIatMicrotime() + { + $payload = [ + 'message' => 'abc', + 'iat' => microtime(true), // use microtime + ]; + $encoded = JWT::encode($payload, 'my_key', 'HS256'); + $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); + $this->assertEquals('abc', $decoded->message); + } + + public function testInvalidTokenWithIatMicrotime() + { + $this->expectException(BeforeValidException::class); + $payload = [ + 'message' => 'abc', + 'iat' => microtime(true) + 20, // use microtime in the future + ]; + $encoded = JWT::encode($payload, 'my_key', 'HS256'); + JWT::decode($encoded, new Key('my_key', 'HS256')); } public function testInvalidToken() From d01db1c45b5c5230fb562dbb95b0c89ca52449c0 Mon Sep 17 00:00:00 2001 From: Vishwaraj Anand Date: Fri, 14 Jul 2023 23:19:54 +0530 Subject: [PATCH 226/282] chore: better BeforeValidException message for decode (firebase/php-jwt#526) --- Jwt/src/JWT.php | 4 ++-- Jwt/tests/JWTTest.php | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index db075ad0064..189274525dd 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -154,7 +154,7 @@ public static function decode( // token can actually be used. If it's not yet that time, abort. if (isset($payload->nbf) && floor($payload->nbf) > ($timestamp + static::$leeway)) { throw new BeforeValidException( - 'Cannot handle token prior to ' . \date(DateTime::ISO8601, (int) $payload->nbf) + 'Cannot handle token with nbf prior to ' . \date(DateTime::ISO8601, (int) $payload->nbf) ); } @@ -163,7 +163,7 @@ public static function decode( // correctly used the nbf claim). if (!isset($payload->nbf) && isset($payload->iat) && floor($payload->iat) > ($timestamp + static::$leeway)) { throw new BeforeValidException( - 'Cannot handle token prior to ' . \date(DateTime::ISO8601, (int) $payload->iat) + 'Cannot handle token with iat prior to ' . \date(DateTime::ISO8601, (int) $payload->iat) ); } diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index 5265e47138a..44b3f04900e 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -147,6 +147,7 @@ public function testInvalidTokenWithNbfLeeway() ]; $encoded = JWT::encode($payload, 'my_key', 'HS256'); $this->expectException(BeforeValidException::class); + $this->expectExceptionMessage('Cannot handle token with nbf prior to'); JWT::decode($encoded, new Key('my_key', 'HS256')); } @@ -176,6 +177,7 @@ public function testValidTokenWithNbfMicrotime() public function testInvalidTokenWithNbfMicrotime() { $this->expectException(BeforeValidException::class); + $this->expectExceptionMessage('Cannot handle token with nbf prior to'); $payload = [ 'message' => 'abc', 'nbf' => microtime(true) + 20, // use microtime in the future @@ -211,6 +213,7 @@ public function testInvalidTokenWithIatLeeway() ]; $encoded = JWT::encode($payload, 'my_key', 'HS256'); $this->expectException(BeforeValidException::class); + $this->expectExceptionMessage('Cannot handle token with iat prior to'); JWT::decode($encoded, new Key('my_key', 'HS256')); } @@ -228,6 +231,7 @@ public function testValidTokenWithIatMicrotime() public function testInvalidTokenWithIatMicrotime() { $this->expectException(BeforeValidException::class); + $this->expectExceptionMessage('Cannot handle token with iat prior to'); $payload = [ 'message' => 'abc', 'iat' => microtime(true) + 20, // use microtime in the future From 586a79480f0c3fcb1581cc2ccc4c946c638d5cd2 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Fri, 14 Jul 2023 18:33:00 +0000 Subject: [PATCH 227/282] chore(main): release 6.8.1 (firebase/php-jwt#524) --- Jwt/CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Jwt/CHANGELOG.md b/Jwt/CHANGELOG.md index 9638f2dd7b3..353766ee02d 100644 --- a/Jwt/CHANGELOG.md +++ b/Jwt/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [6.8.1](https://github.com/firebase/php-jwt/compare/v6.8.0...v6.8.1) (2023-07-14) + + +### Bug Fixes + +* accept float claims but round down to ignore them ([#492](https://github.com/firebase/php-jwt/issues/492)) ([3936842](https://github.com/firebase/php-jwt/commit/39368423beeaacb3002afa7dcb75baebf204fe7e)) +* different BeforeValidException messages for nbf and iat ([#526](https://github.com/firebase/php-jwt/issues/526)) ([0a53cf2](https://github.com/firebase/php-jwt/commit/0a53cf2986e45c2bcbf1a269f313ebf56a154ee4)) + ## [6.8.0](https://github.com/firebase/php-jwt/compare/v6.7.0...v6.8.0) (2023-06-14) From 2202a85695c5996add4ead37bb43647cb602b28a Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 4 Oct 2023 16:59:04 -0700 Subject: [PATCH 228/282] feat: add payload to jwt exception (firebase/php-jwt#521) --- Jwt/src/BeforeValidException.php | 13 +++++++- Jwt/src/ExpiredException.php | 13 +++++++- Jwt/src/JWT.php | 12 +++++-- Jwt/src/JWTExceptionWithPayloadInterface.php | 20 ++++++++++++ Jwt/tests/JWTTest.php | 34 ++++++++++++++++++++ 5 files changed, 87 insertions(+), 5 deletions(-) create mode 100644 Jwt/src/JWTExceptionWithPayloadInterface.php diff --git a/Jwt/src/BeforeValidException.php b/Jwt/src/BeforeValidException.php index c147852b980..595164bf35d 100644 --- a/Jwt/src/BeforeValidException.php +++ b/Jwt/src/BeforeValidException.php @@ -2,6 +2,17 @@ namespace Firebase\JWT; -class BeforeValidException extends \UnexpectedValueException +class BeforeValidException extends \UnexpectedValueException implements JWTExceptionWithPayloadInterface { + private object $payload; + + public function setPayload(object $payload): void + { + $this->payload = $payload; + } + + public function getPayload(): object + { + return $this->payload; + } } diff --git a/Jwt/src/ExpiredException.php b/Jwt/src/ExpiredException.php index 81ba52d43f6..12fef094486 100644 --- a/Jwt/src/ExpiredException.php +++ b/Jwt/src/ExpiredException.php @@ -2,6 +2,17 @@ namespace Firebase\JWT; -class ExpiredException extends \UnexpectedValueException +class ExpiredException extends \UnexpectedValueException implements JWTExceptionWithPayloadInterface { + private object $payload; + + public function setPayload(object $payload): void + { + $this->payload = $payload; + } + + public function getPayload(): object + { + return $this->payload; + } } diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index 189274525dd..6efcbb56d75 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -153,23 +153,29 @@ public static function decode( // Check the nbf if it is defined. This is the time that the // token can actually be used. If it's not yet that time, abort. if (isset($payload->nbf) && floor($payload->nbf) > ($timestamp + static::$leeway)) { - throw new BeforeValidException( + $ex = new BeforeValidException( 'Cannot handle token with nbf prior to ' . \date(DateTime::ISO8601, (int) $payload->nbf) ); + $ex->setPayload($payload); + throw $ex; } // Check that this token has been created before 'now'. This prevents // using tokens that have been created for later use (and haven't // correctly used the nbf claim). if (!isset($payload->nbf) && isset($payload->iat) && floor($payload->iat) > ($timestamp + static::$leeway)) { - throw new BeforeValidException( + $ex = new BeforeValidException( 'Cannot handle token with iat prior to ' . \date(DateTime::ISO8601, (int) $payload->iat) ); + $ex->setPayload($payload); + throw $ex; } // Check if this token has expired. if (isset($payload->exp) && ($timestamp - static::$leeway) >= $payload->exp) { - throw new ExpiredException('Expired token'); + $ex = new ExpiredException('Expired token'); + $ex->setPayload($payload); + throw $ex; } return $payload; diff --git a/Jwt/src/JWTExceptionWithPayloadInterface.php b/Jwt/src/JWTExceptionWithPayloadInterface.php new file mode 100644 index 00000000000..7933ed68be5 --- /dev/null +++ b/Jwt/src/JWTExceptionWithPayloadInterface.php @@ -0,0 +1,20 @@ +assertSame($decoded->message, 'abc'); } + public function testExpiredExceptionPayload() + { + $this->expectException(ExpiredException::class); + $payload = [ + 'message' => 'abc', + 'exp' => time() - 100, // time in the past + ]; + $encoded = JWT::encode($payload, 'my_key', 'HS256'); + try { + JWT::decode($encoded, new Key('my_key', 'HS256')); + } catch (ExpiredException $e) { + $exceptionPayload = (array) $e->getPayload(); + $this->assertEquals($exceptionPayload, $payload); + throw $e; + } + } + + public function testBeforeValidExceptionPayload() + { + $this->expectException(BeforeValidException::class); + $payload = [ + 'message' => 'abc', + 'iat' => time() + 100, // time in the future + ]; + $encoded = JWT::encode($payload, 'my_key', 'HS256'); + try { + JWT::decode($encoded, new Key('my_key', 'HS256')); + } catch (BeforeValidException $e) { + $exceptionPayload = (array) $e->getPayload(); + $this->assertEquals($exceptionPayload, $payload); + throw $e; + } + } + public function testValidTokenWithNbf() { $payload = [ From 947ce9950e95b26de2ac54a39e92b03e94b4fd3d Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Wed, 4 Oct 2023 17:24:42 -0700 Subject: [PATCH 229/282] chore(main): release 6.9.0 (firebase/php-jwt#537) --- Jwt/CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Jwt/CHANGELOG.md b/Jwt/CHANGELOG.md index 353766ee02d..4279dfd2331 100644 --- a/Jwt/CHANGELOG.md +++ b/Jwt/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [6.9.0](https://github.com/firebase/php-jwt/compare/v6.8.1...v6.9.0) (2023-10-04) + + +### Features + +* add payload to jwt exception ([#521](https://github.com/firebase/php-jwt/issues/521)) ([175edf9](https://github.com/firebase/php-jwt/commit/175edf958bb61922ec135b2333acf5622f2238a2)) + ## [6.8.1](https://github.com/firebase/php-jwt/compare/v6.8.0...v6.8.1) (2023-07-14) From b983af4e839caa1a31e01ad151c8cdd9277df61b Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Tue, 28 Nov 2023 17:44:26 -0600 Subject: [PATCH 230/282] feat: allow typ header override (firebase/php-jwt#546) --- Jwt/src/JWT.php | 9 +++++---- Jwt/tests/JWTTest.php | 22 ++++++++++++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index 6efcbb56d75..263492068cb 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -203,13 +203,14 @@ public static function encode( string $keyId = null, array $head = null ): string { - $header = ['typ' => 'JWT', 'alg' => $alg]; + $header = ['typ' => 'JWT']; + if (isset($head) && \is_array($head)) { + $header = \array_merge($header, $head); + } + $header['alg'] = $alg; if ($keyId !== null) { $header['kid'] = $keyId; } - if (isset($head) && \is_array($head)) { - $header = \array_merge($head, $header); - } $segments = []; $segments[] = static::urlsafeB64Encode((string) static::jsonEncode($header)); $segments[] = static::urlsafeB64Encode((string) static::jsonEncode($payload)); diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index 1324041002a..b59c3c208a8 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -518,4 +518,26 @@ public function testGetHeaders() $this->assertEquals($headers->typ, 'JWT'); $this->assertEquals($headers->alg, 'HS256'); } + + public function testAdditionalHeaderOverrides() + { + $msg = JWT::encode( + ['message' => 'abc'], + 'my_key', + 'HS256', + 'my_key_id', + [ + 'cty' => 'test-eit;v=1', + 'typ' => 'JOSE', // override type header + 'kid' => 'not_my_key_id', // should not override $key param + 'alg' => 'BAD', // should not override $alg param + ] + ); + $headers = new stdClass(); + JWT::decode($msg, new Key('my_key', 'HS256'), $headers); + $this->assertEquals('test-eit;v=1', $headers->cty, 'additional field works'); + $this->assertEquals('JOSE', $headers->typ, 'typ override works'); + $this->assertEquals('my_key_id', $headers->kid, 'key param not overridden'); + $this->assertEquals('HS256', $headers->alg, 'alg param not overridden'); + } } From 1b29a2d4676bad89ac33208b559d81a792763c94 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Fri, 1 Dec 2023 08:26:39 -0800 Subject: [PATCH 231/282] chore(main): release 6.10.0 (firebase/php-jwt#547) --- Jwt/CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Jwt/CHANGELOG.md b/Jwt/CHANGELOG.md index 4279dfd2331..644fa0beafa 100644 --- a/Jwt/CHANGELOG.md +++ b/Jwt/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [6.10.0](https://github.com/firebase/php-jwt/compare/v6.9.0...v6.10.0) (2023-11-28) + + +### Features + +* allow typ header override ([#546](https://github.com/firebase/php-jwt/issues/546)) ([79cb30b](https://github.com/firebase/php-jwt/commit/79cb30b729a22931b2fbd6b53f20629a83031ba9)) + ## [6.9.0](https://github.com/firebase/php-jwt/compare/v6.8.1...v6.9.0) (2023-10-04) From be4e73f18caf0bfa998850c2323cd82a9a98ce3d Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Tue, 19 Dec 2023 10:00:36 -0600 Subject: [PATCH 232/282] fix: fix ratelimit cache expiration (firebase/php-jwt#550) --- Jwt/src/CachedKeySet.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jwt/src/CachedKeySet.php b/Jwt/src/CachedKeySet.php index ee529f9f50a..01e271328c8 100644 --- a/Jwt/src/CachedKeySet.php +++ b/Jwt/src/CachedKeySet.php @@ -213,7 +213,7 @@ private function rateLimitExceeded(): bool $cacheItem = $this->cache->getItem($this->rateLimitCacheKey); if (!$cacheItem->isHit()) { - $cacheItem->expiresAfter(1); // # of calls are cached each minute + $cacheItem->expiresAfter(60); // # of calls are cached each minute } $callsPerMinute = (int) $cacheItem->get(); From 8812222ba07825e0414c6cdb4b37e9fe49874639 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Thu, 21 Dec 2023 14:02:08 -0600 Subject: [PATCH 233/282] chore: add php 8.3 to ci (firebase/php-jwt#548) --- Jwt/.github/workflows/tests.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Jwt/.github/workflows/tests.yml b/Jwt/.github/workflows/tests.yml index b29c8018682..7e576e0429b 100644 --- a/Jwt/.github/workflows/tests.yml +++ b/Jwt/.github/workflows/tests.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php: [ "7.4", "8.0", "8.1", "8.2" ] + php: [ "7.4", "8.0", "8.1", "8.2", "8.3" ] name: PHP ${{matrix.php }} Unit Test steps: - uses: actions/checkout@v2 @@ -35,7 +35,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: "8.0" + php-version: "8.2" - name: Run Script run: | composer global require friendsofphp/php-cs-fixer @@ -49,7 +49,7 @@ jobs: - name: Install PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.0' + php-version: '8.2' - name: Run Script run: | composer install From 34a419651d62a2671fba6ffb6f182a57374e955c Mon Sep 17 00:00:00 2001 From: Vishwaraj Anand Date: Sat, 16 Mar 2024 02:03:57 +0530 Subject: [PATCH 234/282] chore: remove jwt incorrect key warning (firebase/php-jwt#560) --- Jwt/src/JWT.php | 3 +++ Jwt/tests/JWTTest.php | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index 263492068cb..e9d75639882 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -251,6 +251,9 @@ public static function sign( return \hash_hmac($algorithm, $msg, $key, true); case 'openssl': $signature = ''; + if (!\is_resource($key) && !openssl_pkey_get_private($key)) { + throw new DomainException('OpenSSL unable to validate key'); + } $success = \openssl_sign($msg, $signature, $key, $algorithm); // @phpstan-ignore-line if (!$success) { throw new DomainException('OpenSSL unable to sign data'); diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index b59c3c208a8..d09d43e393d 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -26,6 +26,12 @@ public function testMalformedUtf8StringsFail() JWT::encode(['message' => pack('c', 128)], 'a', 'HS256'); } + public function testInvalidKeyOpensslSignFail() + { + $this->expectException(DomainException::class); + JWT::sign('message', 'invalid key', 'openssl'); + } + public function testMalformedJsonThrowsException() { $this->expectException(DomainException::class); From 72d64bc93e0b17b9719f1750234319b1decb4c2b Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Fri, 17 May 2024 11:39:00 -0600 Subject: [PATCH 235/282] chore: drop support for PHP 7.4 (firebase/php-jwt#558) --- Jwt/.github/workflows/tests.yml | 2 +- Jwt/README.md | 2 +- Jwt/composer.json | 6 +++--- Jwt/tests/CachedKeySetTest.php | 8 ++++---- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Jwt/.github/workflows/tests.yml b/Jwt/.github/workflows/tests.yml index 7e576e0429b..13fc947f05e 100644 --- a/Jwt/.github/workflows/tests.yml +++ b/Jwt/.github/workflows/tests.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php: [ "7.4", "8.0", "8.1", "8.2", "8.3" ] + php: [ "8.0", "8.1", "8.2", "8.3" ] name: PHP ${{matrix.php }} Unit Test steps: - uses: actions/checkout@v2 diff --git a/Jwt/README.md b/Jwt/README.md index 701de23a828..4fd14074508 100644 --- a/Jwt/README.md +++ b/Jwt/README.md @@ -17,7 +17,7 @@ composer require firebase/php-jwt ``` Optionally, install the `paragonie/sodium_compat` package from composer if your -php is < 7.2 or does not have libsodium installed: +php env does not have libsodium installed: ```bash composer require paragonie/sodium_compat diff --git a/Jwt/composer.json b/Jwt/composer.json index e23dfe378d8..816cfd0bd9a 100644 --- a/Jwt/composer.json +++ b/Jwt/composer.json @@ -20,7 +20,7 @@ ], "license": "BSD-3-Clause", "require": { - "php": "^7.4||^8.0" + "php": "^8.0" }, "suggest": { "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present", @@ -32,10 +32,10 @@ } }, "require-dev": { - "guzzlehttp/guzzle": "^6.5||^7.4", + "guzzlehttp/guzzle": "^7.4", "phpspec/prophecy-phpunit": "^2.0", "phpunit/phpunit": "^9.5", - "psr/cache": "^1.0||^2.0", + "psr/cache": "^2.0||^3.0", "psr/http-client": "^1.0", "psr/http-factory": "^1.0" } diff --git a/Jwt/tests/CachedKeySetTest.php b/Jwt/tests/CachedKeySetTest.php index 2e4e1f62fa9..e5d3aa86a6a 100644 --- a/Jwt/tests/CachedKeySetTest.php +++ b/Jwt/tests/CachedKeySetTest.php @@ -553,7 +553,7 @@ public function getKey(): string return $this->key; } - public function get() + public function get(): mixed { return $this->isHit() ? $this->value : null; } @@ -571,7 +571,7 @@ public function isHit(): bool return $this->currentTime()->getTimestamp() < $this->expiration->getTimestamp(); } - public function set($value) + public function set(mixed $value): static { $this->isHit = true; $this->value = $value; @@ -579,13 +579,13 @@ public function set($value) return $this; } - public function expiresAt($expiration) + public function expiresAt($expiration): static { $this->expiration = $expiration; return $this; } - public function expiresAfter($time) + public function expiresAfter($time): static { $this->expiration = $this->currentTime()->add(new \DateInterval("PT{$time}S")); return $this; From 076bf5466af94701f6a4d65496e98b7a2c99859e Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Sat, 18 May 2024 12:03:01 -0600 Subject: [PATCH 236/282] fix: ensure ratelimit expiry is set every time (firebase/php-jwt#556) --- Jwt/src/CachedKeySet.php | 14 ++++++--- Jwt/tests/CachedKeySetTest.php | 55 ++++++++++++++++++++++++++++++++-- 2 files changed, 63 insertions(+), 6 deletions(-) diff --git a/Jwt/src/CachedKeySet.php b/Jwt/src/CachedKeySet.php index 01e271328c8..65bab74f250 100644 --- a/Jwt/src/CachedKeySet.php +++ b/Jwt/src/CachedKeySet.php @@ -212,15 +212,21 @@ private function rateLimitExceeded(): bool } $cacheItem = $this->cache->getItem($this->rateLimitCacheKey); - if (!$cacheItem->isHit()) { - $cacheItem->expiresAfter(60); // # of calls are cached each minute + + $cacheItemData = []; + if ($cacheItem->isHit() && \is_array($data = $cacheItem->get())) { + $cacheItemData = $data; } - $callsPerMinute = (int) $cacheItem->get(); + $callsPerMinute = $cacheItemData['callsPerMinute'] ?? 0; + $expiry = $cacheItemData['expiry'] ?? new \DateTime('+60 seconds', new \DateTimeZone('UTC')); + if (++$callsPerMinute > $this->maxCallsPerMinute) { return true; } - $cacheItem->set($callsPerMinute); + + $cacheItem->set(['expiry' => $expiry, 'callsPerMinute' => $callsPerMinute]); + $cacheItem->expiresAt($expiry); $this->cache->save($cacheItem); return false; } diff --git a/Jwt/tests/CachedKeySetTest.php b/Jwt/tests/CachedKeySetTest.php index e5d3aa86a6a..39bbc919d63 100644 --- a/Jwt/tests/CachedKeySetTest.php +++ b/Jwt/tests/CachedKeySetTest.php @@ -344,7 +344,7 @@ public function testRateLimit() $cachedKeySet = new CachedKeySet( $this->testJwksUri, $this->getMockHttpClient($this->testJwks1, $shouldBeCalledTimes), - $factory = $this->getMockHttpFactory($shouldBeCalledTimes), + $this->getMockHttpFactory($shouldBeCalledTimes), new TestMemoryCacheItemPool(), 10, // expires after seconds true // enable rate limiting @@ -358,6 +358,54 @@ public function testRateLimit() $this->assertFalse(isset($cachedKeySet[$invalidKid])); } + public function testRateLimitWithExpiresAfter() + { + // We request the key 17 times, HTTP should only be called 15 times + $shouldBeCalledTimes = 10; + $cachedTimes = 2; + $afterExpirationTimes = 5; + + $totalHttpTimes = $shouldBeCalledTimes + $afterExpirationTimes; + + $cachePool = new TestMemoryCacheItemPool(); + + // Instantiate the cached key set + $cachedKeySet = new CachedKeySet( + $this->testJwksUri, + $this->getMockHttpClient($this->testJwks1, $totalHttpTimes), + $this->getMockHttpFactory($totalHttpTimes), + $cachePool, + 10, // expires after seconds + true // enable rate limiting + ); + + // Set the rate limit cache to expire after 1 second + $cacheItem = $cachePool->getItem('jwksratelimitjwkshttpsjwk.uri'); + $cacheItem->set([ + 'expiry' => new \DateTime('+1 second', new \DateTimeZone('UTC')), + 'callsPerMinute' => 0, + ]); + $cacheItem->expiresAfter(1); + $cachePool->save($cacheItem); + + $invalidKid = 'invalidkey'; + for ($i = 0; $i < $shouldBeCalledTimes; $i++) { + $this->assertFalse(isset($cachedKeySet[$invalidKid])); + } + + // The next calls do not call HTTP + for ($i = 0; $i < $cachedTimes; $i++) { + $this->assertFalse(isset($cachedKeySet[$invalidKid])); + } + + sleep(1); // wait for cache to expire + + // These calls DO call HTTP because the cache has expired + for ($i = 0; $i < $afterExpirationTimes; $i++) { + $this->assertFalse(isset($cachedKeySet[$invalidKid])); + } + } + /** * @dataProvider provideFullIntegration */ @@ -466,7 +514,10 @@ final class TestMemoryCacheItemPool implements CacheItemPoolInterface public function getItem($key): CacheItemInterface { - return current($this->getItems([$key])); + $item = current($this->getItems([$key])); + $item->expiresAt(null); // mimic symfony cache behavior + + return $item; } public function getItems(array $keys = []): iterable From a06563868b587dc67bf12ad4a5d9568d85d6d2a1 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Sat, 18 May 2024 11:05:11 -0700 Subject: [PATCH 237/282] chore(main): release 6.10.1 (firebase/php-jwt#551) --- Jwt/CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Jwt/CHANGELOG.md b/Jwt/CHANGELOG.md index 644fa0beafa..2662b050234 100644 --- a/Jwt/CHANGELOG.md +++ b/Jwt/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [6.10.1](https://github.com/firebase/php-jwt/compare/v6.10.0...v6.10.1) (2024-05-18) + + +### Bug Fixes + +* ensure ratelimit expiry is set every time ([#556](https://github.com/firebase/php-jwt/issues/556)) ([09cb208](https://github.com/firebase/php-jwt/commit/09cb2081c2c3bc0f61e2f2a5fbea5741f7498648)) +* ratelimit cache expiration ([#550](https://github.com/firebase/php-jwt/issues/550)) ([dda7250](https://github.com/firebase/php-jwt/commit/dda725033585ece30ff8cae8937320d7e9f18bae)) + ## [6.10.0](https://github.com/firebase/php-jwt/compare/v6.9.0...v6.10.0) (2023-11-28) From eec1c7d498101694db7c0c497e803b14e6d49aeb Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 7 Aug 2024 10:23:58 -0700 Subject: [PATCH 238/282] chore: add test for parseKey (firebase/php-jwt#565) --- Jwt/tests/JWKTest.php | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/Jwt/tests/JWKTest.php b/Jwt/tests/JWKTest.php index 01082a40818..496f6bad506 100644 --- a/Jwt/tests/JWKTest.php +++ b/Jwt/tests/JWKTest.php @@ -169,4 +169,39 @@ public function testDecodeByMultiJwkKeySet() $this->assertSame('bar', $result->sub); } + + public function testParseKey() + { + // Use a known module and exponent, and ensure it parses as expected + $jwk = [ + 'alg' => 'RS256', + 'kty' => 'RSA', + 'n' => 'hsYvCPtkUV7SIxwkOkJsJfhwV_CMdXU5i0UmY2QEs-Pa7v0-0y-s4EjEDtsQ8Yow6hc670JhkGBcMzhU4DtrqNGROXebyOse5FX0m0UvWo1qXqNTf28uBKB990mY42Icr8sGjtOw8ajyT9kufbmXi3eZKagKpG0TDGK90oBEfoGzCxoFT87F95liNth_GoyU5S8-G3OqIqLlQCwxkI5s-g2qvg_aooALfh1rhvx2wt4EJVMSrdnxtPQSPAtZBiw5SwCnVglc6OnalVNvAB2JArbqC9GAzzz9pApAk28SYg5a4hPiPyqwRv-4X1CXEK8bO5VesIeRX0oDf7UoM-pVAw', + 'use' => 'sig', + 'e' => 'AQAB', + 'kid' => '838c06c62046c2d948affe137dd5310129f4d5d1' + ]; + + $key = JWK::parseKey($jwk); + $this->assertNotNull($key); + + $openSslKey = $key->getKeyMaterial(); + $pubKey = openssl_pkey_get_public($openSslKey); + $keyData = openssl_pkey_get_details($pubKey); + + $expectedPublicKey = <<assertEquals($expectedPublicKey, $keyData['key']); + } } From 566a007a4922b83322b9643b210c14e125c1b590 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20B=C3=BCrk?= Date: Wed, 7 Aug 2024 20:22:50 +0200 Subject: [PATCH 239/282] chore: Prepare towards PHP8.4 compatibility (firebase/php-jwt#572) --- Jwt/.php-cs-fixer.dist.php | 4 ++++ Jwt/src/CachedKeySet.php | 6 +++--- Jwt/src/JWK.php | 6 +++--- Jwt/src/JWT.php | 6 +++--- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/Jwt/.php-cs-fixer.dist.php b/Jwt/.php-cs-fixer.dist.php index fb636632fb1..93ff7a4cdd0 100644 --- a/Jwt/.php-cs-fixer.dist.php +++ b/Jwt/.php-cs-fixer.dist.php @@ -16,6 +16,10 @@ 'native_function_invocation' => [ 'strict' => false ], + 'nullable_type_declaration' => [ + 'syntax' => 'question_mark', + ], + 'nullable_type_declaration_for_default_null_value' => true, ]) ->setFinder( PhpCsFixer\Finder::create() diff --git a/Jwt/src/CachedKeySet.php b/Jwt/src/CachedKeySet.php index 65bab74f250..8e8e8d68cae 100644 --- a/Jwt/src/CachedKeySet.php +++ b/Jwt/src/CachedKeySet.php @@ -80,9 +80,9 @@ public function __construct( ClientInterface $httpClient, RequestFactoryInterface $httpFactory, CacheItemPoolInterface $cache, - int $expiresAfter = null, + ?int $expiresAfter = null, bool $rateLimit = false, - string $defaultAlg = null + ?string $defaultAlg = null ) { $this->jwksUri = $jwksUri; $this->httpClient = $httpClient; @@ -180,7 +180,7 @@ private function keyIdExists(string $keyId): bool $jwksResponse = $this->httpClient->sendRequest($request); if ($jwksResponse->getStatusCode() !== 200) { throw new UnexpectedValueException( - sprintf('HTTP Error: %d %s for URI "%s"', + \sprintf('HTTP Error: %d %s for URI "%s"', $jwksResponse->getStatusCode(), $jwksResponse->getReasonPhrase(), $this->jwksUri, diff --git a/Jwt/src/JWK.php b/Jwt/src/JWK.php index 63fb2484b32..6efc2fe3836 100644 --- a/Jwt/src/JWK.php +++ b/Jwt/src/JWK.php @@ -52,7 +52,7 @@ class JWK * * @uses parseKey */ - public static function parseKeySet(array $jwks, string $defaultAlg = null): array + public static function parseKeySet(array $jwks, ?string $defaultAlg = null): array { $keys = []; @@ -93,7 +93,7 @@ public static function parseKeySet(array $jwks, string $defaultAlg = null): arra * * @uses createPemFromModulusAndExponent */ - public static function parseKey(array $jwk, string $defaultAlg = null): ?Key + public static function parseKey(array $jwk, ?string $defaultAlg = null): ?Key { if (empty($jwk)) { throw new InvalidArgumentException('JWK must not be empty'); @@ -212,7 +212,7 @@ private static function createPemFromCrvAndXYCoordinates(string $crv, string $x, ) ); - return sprintf( + return \sprintf( "-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----\n", wordwrap(base64_encode($pem), 64, "\n", true) ); diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index e9d75639882..9100bf0f4b0 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -96,7 +96,7 @@ class JWT public static function decode( string $jwt, $keyOrKeyArray, - stdClass &$headers = null + ?stdClass &$headers = null ): stdClass { // Validate JWT $timestamp = \is_null(static::$timestamp) ? \time() : static::$timestamp; @@ -200,8 +200,8 @@ public static function encode( array $payload, $key, string $alg, - string $keyId = null, - array $head = null + ?string $keyId = null, + ?array $head = null ): string { $header = ['typ' => 'JWT']; if (isset($head) && \is_array($head)) { From 178857bad52a799c65240ac816dee402cc2f0203 Mon Sep 17 00:00:00 2001 From: Ruud Kamphuis Date: Sun, 24 Nov 2024 12:03:38 +0100 Subject: [PATCH 240/282] fix: support php 8.4 (firebase/php-jwt#583) --- Jwt/.github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jwt/.github/workflows/tests.yml b/Jwt/.github/workflows/tests.yml index 13fc947f05e..de513a56b92 100644 --- a/Jwt/.github/workflows/tests.yml +++ b/Jwt/.github/workflows/tests.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php: [ "8.0", "8.1", "8.2", "8.3" ] + php: [ "8.0", "8.1", "8.2", "8.3", "8.4" ] name: PHP ${{matrix.php }} Unit Test steps: - uses: actions/checkout@v2 From 5f47ab356e3d1d90bdcb2476aff0e91b06e50506 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Sun, 24 Nov 2024 06:19:08 -0500 Subject: [PATCH 241/282] chore: fix phpstan (firebase/php-jwt#584) --- Jwt/.github/workflows/tests.yml | 6 +++--- Jwt/src/JWT.php | 9 ++------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/Jwt/.github/workflows/tests.yml b/Jwt/.github/workflows/tests.yml index de513a56b92..6e0cd3c1443 100644 --- a/Jwt/.github/workflows/tests.yml +++ b/Jwt/.github/workflows/tests.yml @@ -35,7 +35,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: "8.2" + php-version: "8.3" - name: Run Script run: | composer global require friendsofphp/php-cs-fixer @@ -49,9 +49,9 @@ jobs: - name: Install PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.2' + php-version: '8.3' - name: Run Script run: | composer install - composer global require phpstan/phpstan + composer global require phpstan/phpstan:~1.10.0 ~/.composer/vendor/bin/phpstan analyse diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index 9100bf0f4b0..dd9292a4314 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -204,7 +204,7 @@ public static function encode( ?array $head = null ): string { $header = ['typ' => 'JWT']; - if (isset($head) && \is_array($head)) { + if (isset($head)) { $header = \array_merge($header, $head); } $header['alg'] = $alg; @@ -387,12 +387,7 @@ public static function jsonDecode(string $input) */ public static function jsonEncode(array $input): string { - if (PHP_VERSION_ID >= 50400) { - $json = \json_encode($input, \JSON_UNESCAPED_SLASHES); - } else { - // PHP 5.3 only - $json = \json_encode($input); - } + $json = \json_encode($input, \JSON_UNESCAPED_SLASHES); if ($errno = \json_last_error()) { self::handleJsonError($errno); } elseif ($json === 'null') { From 0594ba76af4f23297fa95a872adb566495ac96b7 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Sun, 24 Nov 2024 11:22:49 +0000 Subject: [PATCH 242/282] chore(main): release 6.10.2 (firebase/php-jwt#585) --- Jwt/CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Jwt/CHANGELOG.md b/Jwt/CHANGELOG.md index 2662b050234..5feeb5a67c1 100644 --- a/Jwt/CHANGELOG.md +++ b/Jwt/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [6.10.2](https://github.com/firebase/php-jwt/compare/v6.10.1...v6.10.2) (2024-11-24) + + +### Bug Fixes + +* Mitigate PHP8.4 deprecation warnings ([#570](https://github.com/firebase/php-jwt/issues/570)) ([76808fa](https://github.com/firebase/php-jwt/commit/76808fa227f3811aa5cdb3bf81233714b799a5b5)) +* support php 8.4 ([#583](https://github.com/firebase/php-jwt/issues/583)) ([e3d68b0](https://github.com/firebase/php-jwt/commit/e3d68b044421339443c74199edd020e03fb1887e)) + ## [6.10.1](https://github.com/firebase/php-jwt/compare/v6.10.0...v6.10.1) (2024-05-18) From 7cbdaa4dabaf08f8faf48f7812ce2de124a8e70b Mon Sep 17 00:00:00 2001 From: Margar Melkonyan <74971196+margar-melkonyan@users.noreply.github.com> Date: Mon, 25 Nov 2024 00:58:09 +0300 Subject: [PATCH 243/282] fix: refactor constructor Key to use PHP 8.0 syntax (firebase/php-jwt#577) Co-authored-by: Brent Shaffer --- Jwt/src/Key.php | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/Jwt/src/Key.php b/Jwt/src/Key.php index 00cf7f2edf3..b34eae25858 100644 --- a/Jwt/src/Key.php +++ b/Jwt/src/Key.php @@ -9,18 +9,13 @@ class Key { - /** @var string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate */ - private $keyMaterial; - /** @var string */ - private $algorithm; - /** * @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $keyMaterial * @param string $algorithm */ public function __construct( - $keyMaterial, - string $algorithm + private $keyMaterial, + private string $algorithm ) { if ( !\is_string($keyMaterial) @@ -38,10 +33,6 @@ public function __construct( if (empty($algorithm)) { throw new InvalidArgumentException('Algorithm must not be empty'); } - - // TODO: Remove in PHP 8.0 in favor of class constructor property promotion - $this->keyMaterial = $keyMaterial; - $this->algorithm = $algorithm; } /** From a287f1a9f4be7acc2647a3668c6b36f522041348 Mon Sep 17 00:00:00 2001 From: Anthon Pang Date: Thu, 23 Jan 2025 00:00:01 -0500 Subject: [PATCH 244/282] docs: fix example to avoid fatal error (firebase/php-jwt#590) --- Jwt/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Jwt/README.md b/Jwt/README.md index 4fd14074508..04252693ca9 100644 --- a/Jwt/README.md +++ b/Jwt/README.md @@ -48,7 +48,8 @@ $decoded = JWT::decode($jwt, new Key($key, 'HS256')); print_r($decoded); // Pass a stdClass in as the third parameter to get the decoded header values -$decoded = JWT::decode($jwt, new Key($key, 'HS256'), $headers = new stdClass()); +$headers = new stdClass(); +$decoded = JWT::decode($jwt, new Key($key, 'HS256'), $headers); print_r($headers); /* From 90217b2b28bcc43b2cc957a9d53f8b91bb31729f Mon Sep 17 00:00:00 2001 From: apiwat-chantawibul Date: Thu, 23 Jan 2025 12:04:56 +0700 Subject: [PATCH 245/282] feat: support octet typed JWK (firebase/php-jwt#587) --- Jwt/src/JWK.php | 6 ++++++ Jwt/tests/JWKTest.php | 25 +++++++++++++++++++++++++ Jwt/tests/data/octet-jwkset.json | 22 ++++++++++++++++++++++ 3 files changed, 53 insertions(+) create mode 100644 Jwt/tests/data/octet-jwkset.json diff --git a/Jwt/src/JWK.php b/Jwt/src/JWK.php index 6efc2fe3836..405dcc49b70 100644 --- a/Jwt/src/JWK.php +++ b/Jwt/src/JWK.php @@ -172,6 +172,12 @@ public static function parseKey(array $jwk, ?string $defaultAlg = null): ?Key // This library works internally with EdDSA keys (Ed25519) encoded in standard base64. $publicKey = JWT::convertBase64urlToBase64($jwk['x']); return new Key($publicKey, $jwk['alg']); + case 'oct': + if (!isset($jwk['k'])) { + throw new UnexpectedValueException('k not set'); + } + + return new Key(JWT::urlsafeB64Decode($jwk['k']), $jwk['alg']); default: break; } diff --git a/Jwt/tests/JWKTest.php b/Jwt/tests/JWKTest.php index 496f6bad506..db385c87fb6 100644 --- a/Jwt/tests/JWKTest.php +++ b/Jwt/tests/JWKTest.php @@ -170,6 +170,31 @@ public function testDecodeByMultiJwkKeySet() $this->assertSame('bar', $result->sub); } + public function testDecodeByOctetJwkKeySet() + { + $jwkSet = json_decode( + file_get_contents(__DIR__ . '/data/octet-jwkset.json'), + true + ); + $keys = JWK::parseKeySet($jwkSet); + $payload = ['sub' => 'foo', 'exp' => strtotime('+10 seconds')]; + foreach ($keys as $keyId => $key) { + $msg = JWT::encode($payload, $key->getKeyMaterial(), $key->getAlgorithm(), $keyId); + $result = JWT::decode($msg, $keys); + + $this->assertSame('foo', $result->sub); + } + } + + public function testOctetJwkMissingK() + { + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage('k not set'); + + $badJwk = ['kty' => 'oct', 'alg' => 'HS256']; + $keys = JWK::parseKeySet(['keys' => [$badJwk]]); + } + public function testParseKey() { // Use a known module and exponent, and ensure it parses as expected diff --git a/Jwt/tests/data/octet-jwkset.json b/Jwt/tests/data/octet-jwkset.json new file mode 100644 index 00000000000..5555b9dd9ae --- /dev/null +++ b/Jwt/tests/data/octet-jwkset.json @@ -0,0 +1,22 @@ +{ + "keys": [ + { + "kty": "oct", + "alg": "HS256", + "kid": "jwk1", + "k": "xUNfVvQ-WdmXB9qp6qK0SrG-yKW4AJqmcSP66Gm2TrE" + }, + { + "kty": "oct", + "alg": "HS384", + "kid": "jwk2", + "k": "z7990HoD72QDX9JKqeQc3l7EtXutco72j2YulZMjeakFVDbFGXGDFG4awOF7eu9l" + }, + { + "kty": "oct", + "alg": "HS512", + "kid": "jwk3", + "k": "EmYGSDG5W1UjkPIL7LelG-QMVtsXn7bz5lUxBrkqq3kdFEzkLWVGrXKpZxRe7YcApCe0d4s9lXRQtn5Nzaf49w" + } + ] +} From b42d044af9e174fc0c73c6b305e611389ebbb645 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Wed, 22 Jan 2025 21:11:06 -0800 Subject: [PATCH 246/282] chore(main): release 6.11.0 (firebase/php-jwt#586) --- Jwt/CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Jwt/CHANGELOG.md b/Jwt/CHANGELOG.md index 5feeb5a67c1..01fcc077414 100644 --- a/Jwt/CHANGELOG.md +++ b/Jwt/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## [6.11.0](https://github.com/firebase/php-jwt/compare/v6.10.2...v6.11.0) (2025-01-23) + + +### Features + +* support octet typed JWK ([#587](https://github.com/firebase/php-jwt/issues/587)) ([7cb8a26](https://github.com/firebase/php-jwt/commit/7cb8a265fa81edf2fa6ef8098f5bc5ae573c33ad)) + + +### Bug Fixes + +* refactor constructor Key to use PHP 8.0 syntax ([#577](https://github.com/firebase/php-jwt/issues/577)) ([29fa2ce](https://github.com/firebase/php-jwt/commit/29fa2ce9e0582cd397711eec1e80c05ce20fabca)) + ## [6.10.2](https://github.com/firebase/php-jwt/compare/v6.10.1...v6.10.2) (2024-11-24) From e9b88cf4d6a363c6d6e7c82f6df5e64bb2ea371b Mon Sep 17 00:00:00 2001 From: Syahrul Safarila Date: Thu, 10 Apr 2025 02:12:25 +0700 Subject: [PATCH 247/282] docs: fix examples in README.md (firebase/php-jwt#569) --- Jwt/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Jwt/README.md b/Jwt/README.md index 04252693ca9..e45ccb80846 100644 --- a/Jwt/README.md +++ b/Jwt/README.md @@ -291,7 +291,7 @@ $jwks = ['keys' => []]; // JWK::parseKeySet($jwks) returns an associative array of **kid** to Firebase\JWT\Key // objects. Pass this as the second parameter to JWT::decode. -JWT::decode($payload, JWK::parseKeySet($jwks)); +JWT::decode($jwt, JWK::parseKeySet($jwks)); ``` Using Cached Key Sets @@ -350,7 +350,7 @@ use InvalidArgumentException; use UnexpectedValueException; try { - $decoded = JWT::decode($payload, $keys); + $decoded = JWT::decode($jwt, $keys); } catch (InvalidArgumentException $e) { // provided key/key-array is empty or malformed. } catch (DomainException $e) { @@ -380,7 +380,7 @@ like this: use Firebase\JWT\JWT; use UnexpectedValueException; try { - $decoded = JWT::decode($payload, $keys); + $decoded = JWT::decode($jwt, $keys); } catch (LogicException $e) { // errors having to do with environmental setup or malformed JWT Keys } catch (UnexpectedValueException $e) { @@ -395,7 +395,7 @@ instead, you can do the following: ```php // return type is stdClass -$decoded = JWT::decode($payload, $keys); +$decoded = JWT::decode($jwt, $keys); // cast to array $decoded = json_decode(json_encode($decoded), true); From ea0960b2fbe70ed8afb2ccd50166cde2d0d09a1a Mon Sep 17 00:00:00 2001 From: Dan Wallis Date: Wed, 9 Apr 2025 21:29:08 +0100 Subject: [PATCH 248/282] fix: update error text for consistency (firebase/php-jwt#528) --- Jwt/src/JWT.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index dd9292a4314..833a415e691 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -154,7 +154,7 @@ public static function decode( // token can actually be used. If it's not yet that time, abort. if (isset($payload->nbf) && floor($payload->nbf) > ($timestamp + static::$leeway)) { $ex = new BeforeValidException( - 'Cannot handle token with nbf prior to ' . \date(DateTime::ISO8601, (int) $payload->nbf) + 'Cannot handle token with nbf prior to ' . \date(DateTime::ISO8601, (int) floor($payload->nbf)) ); $ex->setPayload($payload); throw $ex; @@ -165,7 +165,7 @@ public static function decode( // correctly used the nbf claim). if (!isset($payload->nbf) && isset($payload->iat) && floor($payload->iat) > ($timestamp + static::$leeway)) { $ex = new BeforeValidException( - 'Cannot handle token with iat prior to ' . \date(DateTime::ISO8601, (int) $payload->iat) + 'Cannot handle token with iat prior to ' . \date(DateTime::ISO8601, (int) floor($payload->iat)) ); $ex->setPayload($payload); throw $ex; From 6e2986f974613a3b5735062718f10179e367cd36 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Wed, 9 Apr 2025 13:32:01 -0700 Subject: [PATCH 249/282] chore(main): release 6.11.1 (firebase/php-jwt#597) --- Jwt/CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Jwt/CHANGELOG.md b/Jwt/CHANGELOG.md index 01fcc077414..7b5f6ce40c2 100644 --- a/Jwt/CHANGELOG.md +++ b/Jwt/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [6.11.1](https://github.com/firebase/php-jwt/compare/v6.11.0...v6.11.1) (2025-04-09) + + +### Bug Fixes + +* update error text for consistency ([#528](https://github.com/firebase/php-jwt/issues/528)) ([c11113a](https://github.com/firebase/php-jwt/commit/c11113afa13265e016a669e75494b9203b8a7775)) + ## [6.11.0](https://github.com/firebase/php-jwt/compare/v6.10.2...v6.11.0) (2025-01-23) From 777984919ca4273b2b828925307f72de2f6ddeac Mon Sep 17 00:00:00 2001 From: Enno Rehling Date: Wed, 9 Apr 2025 22:42:27 +0200 Subject: [PATCH 250/282] fix: use DateTime::ATOM instead of ISO8601 in exception message --- Jwt/src/JWT.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index 833a415e691..37a9e0e650b 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -154,7 +154,7 @@ public static function decode( // token can actually be used. If it's not yet that time, abort. if (isset($payload->nbf) && floor($payload->nbf) > ($timestamp + static::$leeway)) { $ex = new BeforeValidException( - 'Cannot handle token with nbf prior to ' . \date(DateTime::ISO8601, (int) floor($payload->nbf)) + 'Cannot handle token with nbf prior to ' . \date(DateTime::ATOM, (int) floor($payload->nbf)) ); $ex->setPayload($payload); throw $ex; @@ -165,7 +165,7 @@ public static function decode( // correctly used the nbf claim). if (!isset($payload->nbf) && isset($payload->iat) && floor($payload->iat) > ($timestamp + static::$leeway)) { $ex = new BeforeValidException( - 'Cannot handle token with iat prior to ' . \date(DateTime::ISO8601, (int) floor($payload->iat)) + 'Cannot handle token with iat prior to ' . \date(DateTime::ATOM, (int) floor($payload->iat)) ); $ex->setPayload($payload); throw $ex; From 39da250c101297607aecab0f577184fb27be1d7f Mon Sep 17 00:00:00 2001 From: christiandavilakoobin <46561103+christiandavilakoobin@users.noreply.github.com> Date: Wed, 16 Apr 2025 21:45:58 +0200 Subject: [PATCH 251/282] fix: validate iat and nbf on payload (firebase/php-jwt#568) --- Jwt/src/JWT.php | 10 ++++++++++ Jwt/tests/JWTTest.php | 27 +++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index 37a9e0e650b..5386b601961 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -127,6 +127,16 @@ public static function decode( if (!$payload instanceof stdClass) { throw new UnexpectedValueException('Payload must be a JSON object'); } + if (isset($payload->iat) && !\is_numeric($payload->iat)) { + throw new UnexpectedValueException('Payload iat must be a number'); + } + if (isset($payload->nbf) && !\is_numeric($payload->nbf)) { + throw new UnexpectedValueException('Payload nbf must be a number'); + } + if (isset($payload->exp) && !\is_numeric($payload->exp)) { + throw new UnexpectedValueException('Payload exp must be a number'); + } + $sig = static::urlsafeB64Decode($cryptob64); if (empty($header->alg)) { throw new UnexpectedValueException('Empty algorithm'); diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index d09d43e393d..805b867a2d0 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -546,4 +546,31 @@ public function testAdditionalHeaderOverrides() $this->assertEquals('my_key_id', $headers->kid, 'key param not overridden'); $this->assertEquals('HS256', $headers->alg, 'alg param not overridden'); } + + public function testDecodeExpectsIntegerIat() + { + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage('Payload iat must be a number'); + + $payload = JWT::encode(['iat' => 'not-an-int'], 'secret', 'HS256'); + JWT::decode($payload, new Key('secret', 'HS256')); + } + + public function testDecodeExpectsIntegerNbf() + { + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage('Payload nbf must be a number'); + + $payload = JWT::encode(['nbf' => 'not-an-int'], 'secret', 'HS256'); + JWT::decode($payload, new Key('secret', 'HS256')); + } + + public function testDecodeExpectsIntegerExp() + { + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage('Payload exp must be a number'); + + $payload = JWT::encode(['exp' => 'not-an-int'], 'secret', 'HS256'); + JWT::decode($payload, new Key('secret', 'HS256')); + } } From 9605c8d25cfbe96928268ecdc74cc029c06a4055 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Thu, 7 Aug 2025 13:19:49 -0700 Subject: [PATCH 252/282] chore: move release please from app to github action (firebase/php-jwt#606) --- Jwt/.github/release-please.yml | 3 --- Jwt/.github/workflows/release-please.yml | 19 +++++++++++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) delete mode 100644 Jwt/.github/release-please.yml create mode 100644 Jwt/.github/workflows/release-please.yml diff --git a/Jwt/.github/release-please.yml b/Jwt/.github/release-please.yml deleted file mode 100644 index 0a6e0cc2719..00000000000 --- a/Jwt/.github/release-please.yml +++ /dev/null @@ -1,3 +0,0 @@ -releaseType: simple -handleGHRelease: true -primaryBranch: main diff --git a/Jwt/.github/workflows/release-please.yml b/Jwt/.github/workflows/release-please.yml new file mode 100644 index 00000000000..f2d3ca9843a --- /dev/null +++ b/Jwt/.github/workflows/release-please.yml @@ -0,0 +1,19 @@ +name: release-please +on: + push: + branches: + - main +permissions: + # Needed for Release Please to create and update files + contents: write + # Needed for Release Please to create Release PRs + pull-requests: write +jobs: + release-please: + runs-on: ubuntu-latest + steps: + - uses: googleapis/release-please-action@v4 + id: release + with: + token: ${{ secrets.GITHUB_TOKEN }} + release-type: simple From d4b4af4c7eae33d3a404ada58b2d2d88a4103c2f Mon Sep 17 00:00:00 2001 From: Yohei Ema <24579635+meihei3@users.noreply.github.com> Date: Fri, 8 Aug 2025 05:20:40 +0900 Subject: [PATCH 253/282] feat: add SensitiveParameter attribute to security-critical parameters (firebase/php-jwt#603) --- Jwt/src/JWK.php | 4 ++-- Jwt/src/JWT.php | 10 +++++----- Jwt/src/Key.php | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Jwt/src/JWK.php b/Jwt/src/JWK.php index 405dcc49b70..d5175b21708 100644 --- a/Jwt/src/JWK.php +++ b/Jwt/src/JWK.php @@ -52,7 +52,7 @@ class JWK * * @uses parseKey */ - public static function parseKeySet(array $jwks, ?string $defaultAlg = null): array + public static function parseKeySet(#[\SensitiveParameter] array $jwks, ?string $defaultAlg = null): array { $keys = []; @@ -93,7 +93,7 @@ public static function parseKeySet(array $jwks, ?string $defaultAlg = null): arr * * @uses createPemFromModulusAndExponent */ - public static function parseKey(array $jwk, ?string $defaultAlg = null): ?Key + public static function parseKey(#[\SensitiveParameter] array $jwk, ?string $defaultAlg = null): ?Key { if (empty($jwk)) { throw new InvalidArgumentException('JWK must not be empty'); diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index 5386b601961..a2e4438a42c 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -95,7 +95,7 @@ class JWT */ public static function decode( string $jwt, - $keyOrKeyArray, + #[\SensitiveParameter] $keyOrKeyArray, ?stdClass &$headers = null ): stdClass { // Validate JWT @@ -208,7 +208,7 @@ public static function decode( */ public static function encode( array $payload, - $key, + #[\SensitiveParameter] $key, string $alg, ?string $keyId = null, ?array $head = null @@ -246,7 +246,7 @@ public static function encode( */ public static function sign( string $msg, - $key, + #[\SensitiveParameter] $key, string $alg ): string { if (empty(static::$supported_algs[$alg])) { @@ -313,7 +313,7 @@ public static function sign( private static function verify( string $msg, string $signature, - $keyMaterial, + #[\SensitiveParameter] $keyMaterial, string $alg ): bool { if (empty(static::$supported_algs[$alg])) { @@ -467,7 +467,7 @@ public static function urlsafeB64Encode(string $input): string * @return Key */ private static function getKey( - $keyOrKeyArray, + #[\SensitiveParameter] $keyOrKeyArray, ?string $kid ): Key { if ($keyOrKeyArray instanceof Key) { diff --git a/Jwt/src/Key.php b/Jwt/src/Key.php index b34eae25858..4db94851ab6 100644 --- a/Jwt/src/Key.php +++ b/Jwt/src/Key.php @@ -14,7 +14,7 @@ class Key * @param string $algorithm */ public function __construct( - private $keyMaterial, + #[\SensitiveParameter] private $keyMaterial, private string $algorithm ) { if ( From 01b598a14e3534fbede240e11f9f1fe88dbcb83c Mon Sep 17 00:00:00 2001 From: Luke Kuzmish <42181698+cosmastech@users.noreply.github.com> Date: Tue, 19 Aug 2025 11:14:18 -0400 Subject: [PATCH 254/282] feat: store timestamp in `ExpiredException` (firebase/php-jwt#604) --- Jwt/src/ExpiredException.php | 12 ++++++++++++ Jwt/src/JWT.php | 1 + Jwt/tests/JWTTest.php | 23 +++++++++++++++++++++++ 3 files changed, 36 insertions(+) diff --git a/Jwt/src/ExpiredException.php b/Jwt/src/ExpiredException.php index 12fef094486..25f445132d6 100644 --- a/Jwt/src/ExpiredException.php +++ b/Jwt/src/ExpiredException.php @@ -6,6 +6,8 @@ class ExpiredException extends \UnexpectedValueException implements JWTException { private object $payload; + private ?int $timestamp = null; + public function setPayload(object $payload): void { $this->payload = $payload; @@ -15,4 +17,14 @@ public function getPayload(): object { return $this->payload; } + + public function setTimestamp(int $timestamp): void + { + $this->timestamp = $timestamp; + } + + public function getTimestamp(): ?int + { + return $this->timestamp; + } } diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index a2e4438a42c..7e08f491b1d 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -185,6 +185,7 @@ public static function decode( if (isset($payload->exp) && ($timestamp - static::$leeway) >= $payload->exp) { $ex = new ExpiredException('Expired token'); $ex->setPayload($payload); + $ex->setTimestamp($timestamp); throw $ex; } diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index 805b867a2d0..de744311a96 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -130,6 +130,29 @@ public function testExpiredExceptionPayload() } } + /** + * @runInSeparateProcess + */ + public function testExpiredExceptionTimestamp() + { + $this->expectException(ExpiredException::class); + + JWT::$timestamp = 98765; + $payload = [ + 'message' => 'abc', + 'exp' => 1234, + ]; + $encoded = JWT::encode($payload, 'my_key', 'HS256'); + + try { + JWT::decode($encoded, new Key('my_key', 'HS256')); + } catch (ExpiredException $e) { + $exTimestamp = $e->getTimestamp(); + $this->assertSame(98765, $exTimestamp); + throw $e; + } + } + public function testBeforeValidExceptionPayload() { $this->expectException(BeforeValidException::class); From b90a694b154dde825bd5aaa88de528dbabe0a48e Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Fri, 5 Sep 2025 14:25:31 -0700 Subject: [PATCH 255/282] chore: update release-please secret (firebase/php-jwt#608) --- Jwt/.github/workflows/release-please.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jwt/.github/workflows/release-please.yml b/Jwt/.github/workflows/release-please.yml index f2d3ca9843a..409df545717 100644 --- a/Jwt/.github/workflows/release-please.yml +++ b/Jwt/.github/workflows/release-please.yml @@ -15,5 +15,5 @@ jobs: - uses: googleapis/release-please-action@v4 id: release with: - token: ${{ secrets.GITHUB_TOKEN }} + token: ${{ secrets.YOSHI_CODE_BOT_TOKEN }} release-type: simple From 8441566cf6657322924bd8aec0341fdd4b2294da Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Mon, 15 Dec 2025 11:45:09 -0700 Subject: [PATCH 256/282] feat: add key size validation (firebase/php-jwt#613) --- Jwt/src/JWT.php | 53 ++++++++- Jwt/tests/JWTTest.php | 247 ++++++++++++++++++++++++++++-------------- 2 files changed, 218 insertions(+), 82 deletions(-) diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index 7e08f491b1d..dc564efc776 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -31,6 +31,8 @@ class JWT private const ASN1_SEQUENCE = 0x10; private const ASN1_BIT_STRING = 0x03; + private const RSA_KEY_MIN_LENGTH=2048; + /** * When checking nbf, iat or expiration times, * we want to provide some extra leeway time to @@ -259,11 +261,19 @@ public static function sign( if (!\is_string($key)) { throw new InvalidArgumentException('key must be a string when using hmac'); } + self::validateHmacKeyLength($key, $algorithm); return \hash_hmac($algorithm, $msg, $key, true); case 'openssl': $signature = ''; - if (!\is_resource($key) && !openssl_pkey_get_private($key)) { - throw new DomainException('OpenSSL unable to validate key'); + if (!\is_resource($key)) { + /** @var OpenSSLAsymmetricKey|OpenSSLCertificate|string $key */ + $key = $key; + if (!$key = openssl_pkey_get_private($key)) { + throw new DomainException('OpenSSL unable to validate key'); + } + if (str_starts_with($alg, 'RS')) { + self::validateRsaKeyLength($key); + } } $success = \openssl_sign($msg, $signature, $key, $algorithm); // @phpstan-ignore-line if (!$success) { @@ -324,6 +334,13 @@ private static function verify( list($function, $algorithm) = static::$supported_algs[$alg]; switch ($function) { case 'openssl': + if (!\is_resource($keyMaterial) && str_starts_with($algorithm, 'RS')) { + /** @var OpenSSLAsymmetricKey|OpenSSLCertificate|string $keyMaterial */ + $keyMaterial = $keyMaterial; + if ($key = openssl_pkey_get_private($keyMaterial)) { + self::validateRsaKeyLength($key); + } + } $success = \openssl_verify($msg, $signature, $keyMaterial, $algorithm); // @phpstan-ignore-line if ($success === 1) { return true; @@ -361,6 +378,7 @@ private static function verify( if (!\is_string($keyMaterial)) { throw new InvalidArgumentException('key must be a string when using hmac'); } + self::validateHmacKeyLength($keyMaterial, $algorithm); $hash = \hash_hmac($algorithm, $msg, $keyMaterial, true); return self::constantTimeEquals($hash, $signature); } @@ -675,4 +693,35 @@ private static function readDER(string $der, int $offset = 0): array return [$pos, $data]; } + + /** + * Validate HMAC key length + * + * @param string $key HMAC key material + * @param string $algorithm The algorithm + * @throws DomainException Provided key is too short + */ + private static function validateHmacKeyLength(string $key, string $algorithm): void + { + $keyLength = \strlen($key) * 8; + $minKeyLength = (int) \str_replace('SHA', '', $algorithm); + if ($keyLength < $minKeyLength) { + throw new DomainException('Provided key is too short'); + } + } + + /** + * Validate RSA key length + * + * @param OpenSSLAsymmetricKey $key RSA key material + * @throws DomainException Provided key is too short + */ + private static function validateRsaKeyLength(OpenSSLAsymmetricKey $key): void + { + if ($keyDetails = \openssl_pkey_get_details($key)) { + if ($keyDetails['bits'] < self::RSA_KEY_MIN_LENGTH) { + throw new DomainException('Provided key is too short'); + } + } + } } diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index de744311a96..d64d37be24e 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -2,7 +2,6 @@ namespace Firebase\JWT; -use ArrayObject; use DomainException; use InvalidArgumentException; use PHPUnit\Framework\TestCase; @@ -12,18 +11,26 @@ class JWTTest extends TestCase { + private Key $hmacKey; + + public function setUp(): void + { + $this->hmacKey = $this->generateHmac256(); + } + public function testUrlSafeCharacters() { - $encoded = JWT::encode(['message' => 'f?'], 'a', 'HS256'); + $encoded = JWT::encode(['message' => 'f?'], $this->hmacKey->getKeyMaterial(), 'HS256'); $expected = new stdClass(); $expected->message = 'f?'; - $this->assertEquals($expected, JWT::decode($encoded, new Key('a', 'HS256'))); + $this->assertEquals($expected, JWT::decode($encoded, $this->hmacKey)); } public function testMalformedUtf8StringsFail() { + $this->expectException(DomainException::class); - JWT::encode(['message' => pack('c', 128)], 'a', 'HS256'); + JWT::encode(['message' => pack('c', 128)], $this->hmacKey->getKeyMaterial(), 'HS256'); } public function testInvalidKeyOpensslSignFail() @@ -45,8 +52,9 @@ public function testExpiredToken() 'message' => 'abc', 'exp' => time() - 20, // time in the past ]; - $encoded = JWT::encode($payload, 'my_key', 'HS256'); - JWT::decode($encoded, new Key('my_key', 'HS256')); + + $encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256'); + JWT::decode($encoded, $this->hmacKey); } public function testBeforeValidTokenWithNbf() @@ -56,8 +64,8 @@ public function testBeforeValidTokenWithNbf() 'message' => 'abc', 'nbf' => time() + 20, // time in the future ]; - $encoded = JWT::encode($payload, 'my_key', 'HS256'); - JWT::decode($encoded, new Key('my_key', 'HS256')); + $encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256'); + JWT::decode($encoded, $this->hmacKey); } public function testBeforeValidTokenWithIat() @@ -67,8 +75,8 @@ public function testBeforeValidTokenWithIat() 'message' => 'abc', 'iat' => time() + 20, // time in the future ]; - $encoded = JWT::encode($payload, 'my_key', 'HS256'); - JWT::decode($encoded, new Key('my_key', 'HS256')); + $encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256'); + JWT::decode($encoded, $this->hmacKey); } public function testValidToken() @@ -77,8 +85,8 @@ public function testValidToken() 'message' => 'abc', 'exp' => time() + JWT::$leeway + 20, // time in the future ]; - $encoded = JWT::encode($payload, 'my_key', 'HS256'); - $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); + $encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256'); + $decoded = JWT::decode($encoded, $this->hmacKey); $this->assertSame($decoded->message, 'abc'); } @@ -92,8 +100,8 @@ public function testValidTokenWithLeeway() 'message' => 'abc', 'exp' => time() - 20, // time in the past ]; - $encoded = JWT::encode($payload, 'my_key', 'HS256'); - $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); + $encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256'); + $decoded = JWT::decode($encoded, $this->hmacKey); $this->assertSame($decoded->message, 'abc'); } @@ -102,14 +110,14 @@ public function testValidTokenWithLeeway() */ public function testExpiredTokenWithLeeway() { + $this->expectException(ExpiredException::class); JWT::$leeway = 60; $payload = [ 'message' => 'abc', 'exp' => time() - 70, // time far in the past ]; - $this->expectException(ExpiredException::class); - $encoded = JWT::encode($payload, 'my_key', 'HS256'); - $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); + $encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256'); + $decoded = JWT::decode($encoded, $this->hmacKey); $this->assertSame($decoded->message, 'abc'); } @@ -120,9 +128,9 @@ public function testExpiredExceptionPayload() 'message' => 'abc', 'exp' => time() - 100, // time in the past ]; - $encoded = JWT::encode($payload, 'my_key', 'HS256'); + $encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256'); try { - JWT::decode($encoded, new Key('my_key', 'HS256')); + JWT::decode($encoded, $this->hmacKey); } catch (ExpiredException $e) { $exceptionPayload = (array) $e->getPayload(); $this->assertEquals($exceptionPayload, $payload); @@ -142,10 +150,10 @@ public function testExpiredExceptionTimestamp() 'message' => 'abc', 'exp' => 1234, ]; - $encoded = JWT::encode($payload, 'my_key', 'HS256'); + $encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256'); try { - JWT::decode($encoded, new Key('my_key', 'HS256')); + JWT::decode($encoded, $this->hmacKey); } catch (ExpiredException $e) { $exTimestamp = $e->getTimestamp(); $this->assertSame(98765, $exTimestamp); @@ -160,9 +168,9 @@ public function testBeforeValidExceptionPayload() 'message' => 'abc', 'iat' => time() + 100, // time in the future ]; - $encoded = JWT::encode($payload, 'my_key', 'HS256'); + $encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256'); try { - JWT::decode($encoded, new Key('my_key', 'HS256')); + JWT::decode($encoded, $this->hmacKey); } catch (BeforeValidException $e) { $exceptionPayload = (array) $e->getPayload(); $this->assertEquals($exceptionPayload, $payload); @@ -178,8 +186,8 @@ public function testValidTokenWithNbf() 'exp' => time() + 20, // time in the future 'nbf' => time() - 20 ]; - $encoded = JWT::encode($payload, 'my_key', 'HS256'); - $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); + $encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256'); + $decoded = JWT::decode($encoded, $this->hmacKey); $this->assertSame($decoded->message, 'abc'); } @@ -193,8 +201,8 @@ public function testValidTokenWithNbfLeeway() 'message' => 'abc', 'nbf' => time() + 20, // not before in near (leeway) future ]; - $encoded = JWT::encode($payload, 'my_key', 'HS256'); - $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); + $encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256'); + $decoded = JWT::decode($encoded, $this->hmacKey); $this->assertSame($decoded->message, 'abc'); } @@ -208,10 +216,10 @@ public function testInvalidTokenWithNbfLeeway() 'message' => 'abc', 'nbf' => time() + 65, // not before too far in future ]; - $encoded = JWT::encode($payload, 'my_key', 'HS256'); + $encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256'); $this->expectException(BeforeValidException::class); $this->expectExceptionMessage('Cannot handle token with nbf prior to'); - JWT::decode($encoded, new Key('my_key', 'HS256')); + JWT::decode($encoded, $this->hmacKey); } public function testValidTokenWithNbfIgnoresIat() @@ -221,8 +229,8 @@ public function testValidTokenWithNbfIgnoresIat() 'nbf' => time() - 20, // time in the future 'iat' => time() + 20, // time in the past ]; - $encoded = JWT::encode($payload, 'my_key', 'HS256'); - $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); + $encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256'); + $decoded = JWT::decode($encoded, $this->hmacKey); $this->assertEquals('abc', $decoded->message); } @@ -232,8 +240,8 @@ public function testValidTokenWithNbfMicrotime() 'message' => 'abc', 'nbf' => microtime(true), // use microtime ]; - $encoded = JWT::encode($payload, 'my_key', 'HS256'); - $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); + $encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256'); + $decoded = JWT::decode($encoded, $this->hmacKey); $this->assertEquals('abc', $decoded->message); } @@ -245,8 +253,8 @@ public function testInvalidTokenWithNbfMicrotime() 'message' => 'abc', 'nbf' => microtime(true) + 20, // use microtime in the future ]; - $encoded = JWT::encode($payload, 'my_key', 'HS256'); - JWT::decode($encoded, new Key('my_key', 'HS256')); + $encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256'); + JWT::decode($encoded, $this->hmacKey); } /** @@ -259,8 +267,8 @@ public function testValidTokenWithIatLeeway() 'message' => 'abc', 'iat' => time() + 20, // issued in near (leeway) future ]; - $encoded = JWT::encode($payload, 'my_key', 'HS256'); - $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); + $encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256'); + $decoded = JWT::decode($encoded, $this->hmacKey); $this->assertSame($decoded->message, 'abc'); } @@ -274,10 +282,10 @@ public function testInvalidTokenWithIatLeeway() 'message' => 'abc', 'iat' => time() + 65, // issued too far in future ]; - $encoded = JWT::encode($payload, 'my_key', 'HS256'); + $encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256'); $this->expectException(BeforeValidException::class); $this->expectExceptionMessage('Cannot handle token with iat prior to'); - JWT::decode($encoded, new Key('my_key', 'HS256')); + JWT::decode($encoded, $this->hmacKey); } public function testValidTokenWithIatMicrotime() @@ -286,8 +294,8 @@ public function testValidTokenWithIatMicrotime() 'message' => 'abc', 'iat' => microtime(true), // use microtime ]; - $encoded = JWT::encode($payload, 'my_key', 'HS256'); - $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); + $encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256'); + $decoded = JWT::decode($encoded, $this->hmacKey); $this->assertEquals('abc', $decoded->message); } @@ -299,19 +307,21 @@ public function testInvalidTokenWithIatMicrotime() 'message' => 'abc', 'iat' => microtime(true) + 20, // use microtime in the future ]; - $encoded = JWT::encode($payload, 'my_key', 'HS256'); - JWT::decode($encoded, new Key('my_key', 'HS256')); + $encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256'); + JWT::decode($encoded, $this->hmacKey); } public function testInvalidToken() { + $encodeKey = $this->generateHmac256(); + $decodeKey = $this->generateHmac256(); $payload = [ 'message' => 'abc', 'exp' => time() + 20, // time in the future ]; - $encoded = JWT::encode($payload, 'my_key', 'HS256'); + $encoded = JWT::encode($payload, $encodeKey->getKeyMaterial(), $encodeKey->getAlgorithm()); $this->expectException(SignatureInvalidException::class); - JWT::decode($encoded, new Key('my_key2', 'HS256')); + JWT::decode($encoded, $decodeKey); } public function testNullKeyFails() @@ -320,7 +330,7 @@ public function testNullKeyFails() 'message' => 'abc', 'exp' => time() + JWT::$leeway + 20, // time in the future ]; - $encoded = JWT::encode($payload, 'my_key', 'HS256'); + $encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256'); $this->expectException(TypeError::class); JWT::decode($encoded, new Key(null, 'HS256')); } @@ -331,7 +341,7 @@ public function testEmptyKeyFails() 'message' => 'abc', 'exp' => time() + JWT::$leeway + 20, // time in the future ]; - $encoded = JWT::encode($payload, 'my_key', 'HS256'); + $encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256'); $this->expectException(InvalidArgumentException::class); JWT::decode($encoded, new Key('', 'HS256')); } @@ -339,9 +349,9 @@ public function testEmptyKeyFails() public function testKIDChooser() { $keys = [ - '0' => new Key('my_key0', 'HS256'), - '1' => new Key('my_key1', 'HS256'), - '2' => new Key('my_key2', 'HS256') + '0' => $this->generateHmac256(), + '1' => $this->generateHmac256(), + '2' => $this->generateHmac256() ]; $msg = JWT::encode(['message' => 'abc'], $keys['0']->getKeyMaterial(), 'HS256', '0'); $decoded = JWT::decode($msg, $keys); @@ -352,11 +362,11 @@ public function testKIDChooser() public function testArrayAccessKIDChooser() { - $keys = new ArrayObject([ - '0' => new Key('my_key0', 'HS256'), - '1' => new Key('my_key1', 'HS256'), - '2' => new Key('my_key2', 'HS256'), - ]); + $keys = [ + '0' => $this->generateHmac256(), + '1' => $this->generateHmac256(), + '2' => $this->generateHmac256() + ]; $msg = JWT::encode(['message' => 'abc'], $keys['0']->getKeyMaterial(), 'HS256', '0'); $decoded = JWT::decode($msg, $keys); $expected = new stdClass(); @@ -366,59 +376,62 @@ public function testArrayAccessKIDChooser() public function testNoneAlgorithm() { - $msg = JWT::encode(['message' => 'abc'], 'my_key', 'HS256'); + $msg = JWT::encode(['message' => 'abc'], $this->hmacKey->getKeyMaterial(), 'HS256'); $this->expectException(UnexpectedValueException::class); - JWT::decode($msg, new Key('my_key', 'none')); + JWT::decode($msg, new Key($this->hmacKey->getKeyMaterial(), 'none')); } public function testIncorrectAlgorithm() { - $msg = JWT::encode(['message' => 'abc'], 'my_key', 'HS256'); + $msg = JWT::encode(['message' => 'abc'], $this->hmacKey->getKeyMaterial(), 'HS256'); $this->expectException(UnexpectedValueException::class); - JWT::decode($msg, new Key('my_key', 'RS256')); + // TODO: Generate proper RS256 key + JWT::decode($msg, new Key($this->hmacKey->getKeyMaterial(), 'RS256')); } public function testEmptyAlgorithm() { - $msg = JWT::encode(['message' => 'abc'], 'my_key', 'HS256'); + $msg = JWT::encode(['message' => 'abc'], $this->hmacKey->getKeyMaterial(), 'HS256'); $this->expectException(InvalidArgumentException::class); - JWT::decode($msg, new Key('my_key', '')); + JWT::decode($msg, new Key($this->hmacKey->getKeyMaterial(), '')); } public function testAdditionalHeaders() { - $msg = JWT::encode(['message' => 'abc'], 'my_key', 'HS256', null, ['cty' => 'test-eit;v=1']); + $msg = JWT::encode(['message' => 'abc'], $this->hmacKey->getKeyMaterial(), 'HS256', null, ['cty' => 'test-eit;v=1']); $expected = new stdClass(); $expected->message = 'abc'; - $this->assertEquals(JWT::decode($msg, new Key('my_key', 'HS256')), $expected); + $this->assertEquals(JWT::decode($msg, $this->hmacKey), $expected); } public function testInvalidSegmentCount() { $this->expectException(UnexpectedValueException::class); - JWT::decode('brokenheader.brokenbody', new Key('my_key', 'HS256')); + JWT::decode('brokenheader.brokenbody', $this->hmacKey); } public function testInvalidSignatureEncoding() { $msg = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwibmFtZSI6ImZvbyJ9.Q4Kee9E8o0Xfo4ADXvYA8t7dN_X_bU9K5w6tXuiSjlUxx'; $this->expectException(UnexpectedValueException::class); - JWT::decode($msg, new Key('secret', 'HS256')); + JWT::decode($msg, $this->hmacKey); } public function testHSEncodeDecode() { - $msg = JWT::encode(['message' => 'abc'], 'my_key', 'HS256'); + $msg = JWT::encode(['message' => 'abc'], $this->hmacKey->getKeyMaterial(), 'HS256'); $expected = new stdClass(); $expected->message = 'abc'; - $this->assertEquals(JWT::decode($msg, new Key('my_key', 'HS256')), $expected); + $this->assertEquals(JWT::decode($msg, $this->hmacKey), $expected); } public function testRSEncodeDecode() { - $privKey = openssl_pkey_new(['digest_alg' => 'sha256', - 'private_key_bits' => 1024, - 'private_key_type' => OPENSSL_KEYTYPE_RSA]); + $privKey = openssl_pkey_new([ + 'digest_alg' => 'sha256', + 'private_key_bits' => 2048, + 'private_key_type' => OPENSSL_KEYTYPE_RSA + ]); $msg = JWT::encode(['message' => 'abc'], $privKey, 'RS256'); $pubKey = openssl_pkey_get_details($privKey); $pubKey = $pubKey['key']; @@ -541,8 +554,8 @@ public function testGetHeaders() ]; $headers = new stdClass(); - $encoded = JWT::encode($payload, 'my_key', 'HS256'); - JWT::decode($encoded, new Key('my_key', 'HS256'), $headers); + $encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256'); + JWT::decode($encoded, $this->hmacKey, $headers); $this->assertEquals($headers->typ, 'JWT'); $this->assertEquals($headers->alg, 'HS256'); @@ -552,7 +565,7 @@ public function testAdditionalHeaderOverrides() { $msg = JWT::encode( ['message' => 'abc'], - 'my_key', + $this->hmacKey->getKeyMaterial(), 'HS256', 'my_key_id', [ @@ -563,7 +576,7 @@ public function testAdditionalHeaderOverrides() ] ); $headers = new stdClass(); - JWT::decode($msg, new Key('my_key', 'HS256'), $headers); + JWT::decode($msg, $this->hmacKey, $headers); $this->assertEquals('test-eit;v=1', $headers->cty, 'additional field works'); $this->assertEquals('JOSE', $headers->typ, 'typ override works'); $this->assertEquals('my_key_id', $headers->kid, 'key param not overridden'); @@ -575,8 +588,8 @@ public function testDecodeExpectsIntegerIat() $this->expectException(UnexpectedValueException::class); $this->expectExceptionMessage('Payload iat must be a number'); - $payload = JWT::encode(['iat' => 'not-an-int'], 'secret', 'HS256'); - JWT::decode($payload, new Key('secret', 'HS256')); + $payload = JWT::encode(['iat' => 'not-an-int'], $this->hmacKey->getKeyMaterial(), 'HS256'); + JWT::decode($payload, $this->hmacKey); } public function testDecodeExpectsIntegerNbf() @@ -584,8 +597,8 @@ public function testDecodeExpectsIntegerNbf() $this->expectException(UnexpectedValueException::class); $this->expectExceptionMessage('Payload nbf must be a number'); - $payload = JWT::encode(['nbf' => 'not-an-int'], 'secret', 'HS256'); - JWT::decode($payload, new Key('secret', 'HS256')); + $payload = JWT::encode(['nbf' => 'not-an-int'], $this->hmacKey->getKeyMaterial(), 'HS256'); + JWT::decode($payload, $this->hmacKey); } public function testDecodeExpectsIntegerExp() @@ -593,7 +606,81 @@ public function testDecodeExpectsIntegerExp() $this->expectException(UnexpectedValueException::class); $this->expectExceptionMessage('Payload exp must be a number'); - $payload = JWT::encode(['exp' => 'not-an-int'], 'secret', 'HS256'); - JWT::decode($payload, new Key('secret', 'HS256')); + $payload = JWT::encode(['exp' => 'not-an-int'], $this->hmacKey->getKeyMaterial(), 'HS256'); + JWT::decode($payload, $this->hmacKey); + } + + public function testRsaKeyLengthValidationThrowsException(): void + { + $this->expectException(DomainException::class); + $this->expectExceptionMessage('Provided key is too short'); + + // Generate an RSA key that is smaller than the 2048-bit minimum + $shortRsaKey = openssl_pkey_new([ + 'private_key_bits' => 1024, + 'private_key_type' => OPENSSL_KEYTYPE_RSA, + ]); + + self::assertNotFalse($shortRsaKey, 'Failed to generate a short RSA key for testing.'); + $payload = ['message' => 'abc']; + JWT::encode($payload, $shortRsaKey, 'RS256'); + } + + /** @dataProvider provideHmac */ + public function testHmacKeyLengthValidationThrowsExceptionEncode(string $alg, int $minLength): void + { + $this->expectException(DomainException::class); + $this->expectExceptionMessage('Provided key is too short'); + + $tooShortKeyBytes = str_repeat('b', $minLength - 1); + $payload = ['message' => 'abc']; + + JWT::encode($payload, $tooShortKeyBytes, $alg); + } + + /** @dataProvider provideHmac */ + public function testHmacKeyLengthValidationThrowsExceptionDecode(string $alg, int $minLength): void + { + $this->expectException(DomainException::class); + $this->expectExceptionMessage('Provided key is too short'); + + $tooShortKeyBytes = str_repeat('b', $minLength - 1); + $payload = ['message' => 'abc']; + + $validKeyBytes = str_repeat('b', $minLength); + $encoded = JWT::encode($payload, $validKeyBytes, $alg); + + JWT::decode($encoded, new Key($tooShortKeyBytes, $alg)); + } + + /** @dataProvider provideHmac */ + public function testHmacKeyLengthValidationPassesWithCorrectLength(string $alg, int $minLength): void + { + $payload = ['message' => 'test hmac length']; + + // Test with a key that is exactly the required length + $minKeyBytes = str_repeat('b', $minLength); + $encoded48 = JWT::encode($payload, $minKeyBytes, $alg); + $decoded48 = JWT::decode($encoded48, new Key($minKeyBytes, $alg)); + $this->assertEquals($payload['message'], $decoded48->message); + + // Test with a key that is longer than the required length + $largeKeyBytes = str_repeat('c', $minLength * 2); // Longer than min bytes + $encoded64 = JWT::encode($payload, $largeKeyBytes, $alg); + $decoded64 = JWT::decode($encoded64, new Key($largeKeyBytes, $alg)); + $this->assertEquals($payload['message'], $decoded64->message); + } + + public function provideHmac() + { + return [ + ['HS384', 48], + ['HS256', 32], + ]; + } + + private function generateHmac256(): Key + { + return new Key(random_bytes(32), 'HS256'); } } From 31caaf51aa416e6b97b07243a9a18cb69db91c59 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Mon, 15 Dec 2025 12:26:43 -0700 Subject: [PATCH 257/282] chore(main): release 7.0.0 (firebase/php-jwt#614) --- Jwt/CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Jwt/CHANGELOG.md b/Jwt/CHANGELOG.md index 7b5f6ce40c2..d08ecff892b 100644 --- a/Jwt/CHANGELOG.md +++ b/Jwt/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## [7.0.0](https://github.com/firebase/php-jwt/compare/v6.11.1...v7.0.0) (2025-12-15) + + +### ⚠️ ⚠️ ⚠️ Security Fixes ⚠️ ⚠️ ⚠️ + * add key size validation ([#613](https://github.com/firebase/php-jwt/issues/613)) ([6b80341](https://github.com/firebase/php-jwt/commit/6b80341bf57838ea2d011487917337901cd71576)) + **NOTE**: This fix will cause keys with a size below the minimally allowed size to break. + +### Features + +* add SensitiveParameter attribute to security-critical parameters ([#603](https://github.com/firebase/php-jwt/issues/603)) ([4dbfac0](https://github.com/firebase/php-jwt/commit/4dbfac0260eeb0e9e643063c99998e3219cc539b)) +* store timestamp in `ExpiredException` ([#604](https://github.com/firebase/php-jwt/issues/604)) ([f174826](https://github.com/firebase/php-jwt/commit/f1748260d218a856b6a0c23715ac7fae1d7ca95b)) + + +### Bug Fixes + +* validate iat and nbf on payload ([#568](https://github.com/firebase/php-jwt/issues/568)) ([953b2c8](https://github.com/firebase/php-jwt/commit/953b2c88bb445b7e3bb82a5141928f13d7343afd)) + ## [6.11.1](https://github.com/firebase/php-jwt/compare/v6.11.0...v6.11.1) (2025-04-09) From 2ec2405aff3a12755517edb77c95c47d2bc318a2 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Mon, 15 Dec 2025 16:11:17 -0700 Subject: [PATCH 258/282] Add key size validation (firebase/php-jwt#612) Co-authored-by: Marius Klocke --- Jwt/README.md | 2 +- Jwt/src/JWT.php | 45 ++++++++++++++++++++----------------------- Jwt/src/Key.php | 7 +++---- Jwt/tests/JWTTest.php | 7 +++---- 4 files changed, 28 insertions(+), 33 deletions(-) diff --git a/Jwt/README.md b/Jwt/README.md index e45ccb80846..65b6c8609dd 100644 --- a/Jwt/README.md +++ b/Jwt/README.md @@ -186,7 +186,7 @@ $passphrase = '[YOUR_PASSPHRASE]'; // Can be generated with "ssh-keygen -t rsa -m pem" $privateKeyFile = '/path/to/key-with-passphrase.pem'; -// Create a private key of type "resource" +/** @var OpenSSLAsymmetricKey $privateKey */ $privateKey = openssl_pkey_get_private( file_get_contents($privateKeyFile), $passphrase diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index dc564efc776..3ead151b687 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -198,7 +198,7 @@ public static function decode( * Converts and signs a PHP array into a JWT string. * * @param array $payload PHP array - * @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $key The secret key. + * @param string|OpenSSLAsymmetricKey|OpenSSLCertificate $key The secret key. * @param string $alg Supported algorithms are 'ES384','ES256', 'ES256K', 'HS256', * 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512' * @param string $keyId @@ -239,7 +239,7 @@ public static function encode( * Sign a string with a given key and algorithm. * * @param string $msg The message to sign - * @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $key The secret key. + * @param string|OpenSSLAsymmetricKey|OpenSSLCertificate $key The secret key. * @param string $alg Supported algorithms are 'EdDSA', 'ES384', 'ES256', 'ES256K', 'HS256', * 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512' * @@ -265,17 +265,13 @@ public static function sign( return \hash_hmac($algorithm, $msg, $key, true); case 'openssl': $signature = ''; - if (!\is_resource($key)) { - /** @var OpenSSLAsymmetricKey|OpenSSLCertificate|string $key */ - $key = $key; - if (!$key = openssl_pkey_get_private($key)) { - throw new DomainException('OpenSSL unable to validate key'); - } - if (str_starts_with($alg, 'RS')) { - self::validateRsaKeyLength($key); - } + if (!$key = openssl_pkey_get_private($key)) { + throw new DomainException('OpenSSL unable to validate key'); } - $success = \openssl_sign($msg, $signature, $key, $algorithm); // @phpstan-ignore-line + if (str_starts_with($alg, 'RS')) { + self::validateRsaKeyLength($key); + } + $success = \openssl_sign($msg, $signature, $key, $algorithm); if (!$success) { throw new DomainException('OpenSSL unable to sign data'); } @@ -314,7 +310,7 @@ public static function sign( * * @param string $msg The original message (header and body) * @param string $signature The original signature - * @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $keyMaterial For Ed*, ES*, HS*, a string key works. for RS*, must be an instance of OpenSSLAsymmetricKey + * @param string|OpenSSLAsymmetricKey|OpenSSLCertificate $keyMaterial For Ed*, ES*, HS*, a string key works. for RS*, must be an instance of OpenSSLAsymmetricKey * @param string $alg The algorithm * * @return bool @@ -334,14 +330,13 @@ private static function verify( list($function, $algorithm) = static::$supported_algs[$alg]; switch ($function) { case 'openssl': - if (!\is_resource($keyMaterial) && str_starts_with($algorithm, 'RS')) { - /** @var OpenSSLAsymmetricKey|OpenSSLCertificate|string $keyMaterial */ - $keyMaterial = $keyMaterial; - if ($key = openssl_pkey_get_private($keyMaterial)) { - self::validateRsaKeyLength($key); + if (str_starts_with($algorithm, 'RS')) { + if (!$key = openssl_pkey_get_private($keyMaterial)) { + throw new DomainException('OpenSSL unable to validate key'); } + self::validateRsaKeyLength($key); } - $success = \openssl_verify($msg, $signature, $keyMaterial, $algorithm); // @phpstan-ignore-line + $success = \openssl_verify($msg, $signature, $keyMaterial, $algorithm); if ($success === 1) { return true; } @@ -699,6 +694,7 @@ private static function readDER(string $der, int $offset = 0): array * * @param string $key HMAC key material * @param string $algorithm The algorithm + * * @throws DomainException Provided key is too short */ private static function validateHmacKeyLength(string $key, string $algorithm): void @@ -716,12 +712,13 @@ private static function validateHmacKeyLength(string $key, string $algorithm): v * @param OpenSSLAsymmetricKey $key RSA key material * @throws DomainException Provided key is too short */ - private static function validateRsaKeyLength(OpenSSLAsymmetricKey $key): void + private static function validateRsaKeyLength(#[\SensitiveParameter] OpenSSLAsymmetricKey $key): void { - if ($keyDetails = \openssl_pkey_get_details($key)) { - if ($keyDetails['bits'] < self::RSA_KEY_MIN_LENGTH) { - throw new DomainException('Provided key is too short'); - } + if (!$keyDetails = openssl_pkey_get_details($key)) { + throw new DomainException('Unable to validate key'); + } + if ($keyDetails['bits'] < self::RSA_KEY_MIN_LENGTH) { + throw new DomainException('Provided key is too short'); } } } diff --git a/Jwt/src/Key.php b/Jwt/src/Key.php index 4db94851ab6..694d3b13b99 100644 --- a/Jwt/src/Key.php +++ b/Jwt/src/Key.php @@ -10,7 +10,7 @@ class Key { /** - * @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $keyMaterial + * @param string|OpenSSLAsymmetricKey|OpenSSLCertificate $keyMaterial * @param string $algorithm */ public function __construct( @@ -21,9 +21,8 @@ public function __construct( !\is_string($keyMaterial) && !$keyMaterial instanceof OpenSSLAsymmetricKey && !$keyMaterial instanceof OpenSSLCertificate - && !\is_resource($keyMaterial) ) { - throw new TypeError('Key material must be a string, resource, or OpenSSLAsymmetricKey'); + throw new TypeError('Key material must be a string, OpenSSLCertificate, or OpenSSLAsymmetricKey'); } if (empty($keyMaterial)) { @@ -46,7 +45,7 @@ public function getAlgorithm(): string } /** - * @return string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate + * @return string|OpenSSLAsymmetricKey|OpenSSLCertificate */ public function getKeyMaterial() { diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index d64d37be24e..d920a851d18 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -28,7 +28,6 @@ public function testUrlSafeCharacters() public function testMalformedUtf8StringsFail() { - $this->expectException(DomainException::class); JWT::encode(['message' => pack('c', 128)], $this->hmacKey->getKeyMaterial(), 'HS256'); } @@ -531,17 +530,17 @@ public function provideEncodeDecode() ]; } - public function testEncodeDecodeWithResource() + public function testEncodeDecodeWithOpenSSLAsymmetricKey() { $pem = file_get_contents(__DIR__ . '/data/rsa1-public.pub'); - $resource = openssl_pkey_get_public($pem); + $keyMaterial = openssl_pkey_get_public($pem); $privateKey = file_get_contents(__DIR__ . '/data/rsa1-private.pem'); $payload = ['foo' => 'bar']; $encoded = JWT::encode($payload, $privateKey, 'RS512'); // Verify decoding succeeds - $decoded = JWT::decode($encoded, new Key($resource, 'RS512')); + $decoded = JWT::decode($encoded, new Key($keyMaterial, 'RS512')); $this->assertSame('bar', $decoded->foo); } From 48271b894e7e9b1814464fc58ce50547b9a31476 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Tue, 16 Dec 2025 12:59:01 -0800 Subject: [PATCH 259/282] fix: add key length validation for ec keys (firebase/php-jwt#615) --- Jwt/src/JWT.php | 32 ++++++++++++++-- Jwt/tests/JWTTest.php | 59 +++++++++++++++++++++++++++++ Jwt/tests/data/ecdsa192-private.pem | 5 +++ Jwt/tests/data/ecdsa192-public.pem | 4 ++ 4 files changed, 96 insertions(+), 4 deletions(-) create mode 100644 Jwt/tests/data/ecdsa192-private.pem create mode 100644 Jwt/tests/data/ecdsa192-public.pem diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index 3ead151b687..c18e4cc062f 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -270,6 +270,8 @@ public static function sign( } if (str_starts_with($alg, 'RS')) { self::validateRsaKeyLength($key); + } elseif (str_starts_with($alg, 'ES')) { + self::validateEcKeyLength($key, $alg); } $success = \openssl_sign($msg, $signature, $key, $algorithm); if (!$success) { @@ -330,11 +332,13 @@ private static function verify( list($function, $algorithm) = static::$supported_algs[$alg]; switch ($function) { case 'openssl': - if (str_starts_with($algorithm, 'RS')) { - if (!$key = openssl_pkey_get_private($keyMaterial)) { - throw new DomainException('OpenSSL unable to validate key'); - } + if (!$key = openssl_pkey_get_public($keyMaterial)) { + throw new DomainException('OpenSSL unable to validate key'); + } + if (str_starts_with($alg, 'RS')) { self::validateRsaKeyLength($key); + } elseif (str_starts_with($alg, 'ES')) { + self::validateEcKeyLength($key, $alg); } $success = \openssl_verify($msg, $signature, $keyMaterial, $algorithm); if ($success === 1) { @@ -721,4 +725,24 @@ private static function validateRsaKeyLength(#[\SensitiveParameter] OpenSSLAsymm throw new DomainException('Provided key is too short'); } } + + /** + * Validate RSA key length + * + * @param OpenSSLAsymmetricKey $key RSA key material + * @param string $algorithm The algorithm + * @throws DomainException Provided key is too short + */ + private static function validateEcKeyLength( + #[\SensitiveParameter] OpenSSLAsymmetricKey $key, + string $algorithm + ): void { + if (!$keyDetails = openssl_pkey_get_details($key)) { + throw new DomainException('Unable to validate key'); + } + $minKeyLength = (int) \str_replace('ES', '', $algorithm); + if ($keyDetails['bits'] < $minKeyLength) { + throw new DomainException('Provided key is too short'); + } + } } diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index d920a851d18..a1dd08a4374 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -678,6 +678,65 @@ public function provideHmac() ]; } + /** @dataProvider provideEcKeyInvalidLength */ + public function testEcKeyLengthValidationThrowsExceptionEncode(string $keyFile, string $alg): void + { + $this->expectException(DomainException::class); + $this->expectExceptionMessage('Provided key is too short'); + + $tooShortEcKey = file_get_contents(__DIR__ . '/data/' . $keyFile); + $payload = ['message' => 'abc']; + + JWT::encode($payload, $tooShortEcKey, $alg); + } + + public function testEcKeyLengthValidationThrowsExceptionDecode(): void + { + $this->expectException(DomainException::class); + $this->expectExceptionMessage('Provided key is too short'); + + $payload = ['message' => 'abc']; + + $validEcKeyBytes = file_get_contents(__DIR__ . '/data/ecdsa384-private.pem'); + $encoded = JWT::encode($payload, $validEcKeyBytes, 'ES256'); + + $tooShortEcKey = file_get_contents(__DIR__ . '/data/ecdsa192-public.pem'); + JWT::decode($encoded, new Key($tooShortEcKey, 'ES256')); + } + + /** @dataProvider provideEcKey */ + public function testEcKeyLengthValidationPassesWithCorrectLength( + string $privateKeyFile, + string $publicKeyFile, + string $alg + ): void { + $payload = ['message' => 'test hmac length']; + + // Test with a key that is the required length + $privateKeyBytes = file_get_contents(__DIR__ . '/data/' . $privateKeyFile); + $encoded48 = JWT::encode($payload, $privateKeyBytes, $alg); + + $publicKeyBytes = file_get_contents(__DIR__ . '/data/' . $publicKeyFile); + $decoded48 = JWT::decode($encoded48, new Key($publicKeyBytes, $alg)); + $this->assertEquals($payload['message'], $decoded48->message); + } + + public function provideEcKeyInvalidLength() + { + return [ + ['ecdsa192-private.pem', 'ES256'], + ['ecdsa-private.pem', 'ES384'], + ]; + } + + public function provideEcKey() + { + return [ + ['ecdsa-private.pem', 'ecdsa-public.pem', 'ES256'], + ['ecdsa384-private.pem', 'ecdsa384-public.pem', 'ES384'], + ]; + } + private function generateHmac256(): Key { return new Key(random_bytes(32), 'HS256'); diff --git a/Jwt/tests/data/ecdsa192-private.pem b/Jwt/tests/data/ecdsa192-private.pem new file mode 100644 index 00000000000..4d9bc0b1e22 --- /dev/null +++ b/Jwt/tests/data/ecdsa192-private.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MF8CAQEEGPRkK7lK/9FuZ3BE8ZX+dlHavL22Q9CN2KAKBggqhkjOPQMBAaE0AzIA +BL4pM50YcLq/I9Y8T+C+fwoOtwRW8zdV6yQmG9fD8zWaAs28+UxHeK8VD7THatbp +wg== +-----END EC PRIVATE KEY----- \ No newline at end of file diff --git a/Jwt/tests/data/ecdsa192-public.pem b/Jwt/tests/data/ecdsa192-public.pem new file mode 100644 index 00000000000..d4aeee1dcaf --- /dev/null +++ b/Jwt/tests/data/ecdsa192-public.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MEkwEwYHKoZIzj0CAQYIKoZIzj0DAQEDMgAEvikznRhwur8j1jxP4L5/Cg63BFbz +N1XrJCYb18PzNZoCzbz5TEd4rxUPtMdq1unC +-----END PUBLIC KEY----- From 05de0c7039fc6e864a70f46ac84f0ebadbf88424 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Tue, 16 Dec 2025 14:17:28 -0800 Subject: [PATCH 260/282] chore(main): release 7.0.2 (firebase/php-jwt#616) --- Jwt/CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Jwt/CHANGELOG.md b/Jwt/CHANGELOG.md index d08ecff892b..263760760d9 100644 --- a/Jwt/CHANGELOG.md +++ b/Jwt/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [7.0.2](https://github.com/firebase/php-jwt/compare/v7.0.1...v7.0.2) (2025-12-16) + + +### Bug Fixes + +* add key length validation for ec keys ([#615](https://github.com/firebase/php-jwt/issues/615)) ([7044f9a](https://github.com/firebase/php-jwt/commit/7044f9ae7e7d175d28cca71714feb236f1c0e252)) + ## [7.0.0](https://github.com/firebase/php-jwt/compare/v6.11.1...v7.0.0) (2025-12-15) From 0c58d6ead6b3b2f30c35158fcfb3f9ed84607acf Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 24 Dec 2025 07:57:36 -0800 Subject: [PATCH 261/282] docs: allow array of strings in head param (firebase/php-jwt#599) --- Jwt/src/JWT.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index c18e4cc062f..90f62ca9deb 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -202,7 +202,7 @@ public static function decode( * @param string $alg Supported algorithms are 'ES384','ES256', 'ES256K', 'HS256', * 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512' * @param string $keyId - * @param array $head An array with header elements to attach + * @param array $head An array with header elements to attach * * @return string A signed JWT * From e0a44be35d9c99fb88e965ec96c1b07d02bde881 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Thu, 15 Jan 2026 15:11:33 -0800 Subject: [PATCH 262/282] chore: add PHP 8.5 to testing matrix (firebase/php-jwt#617) --- Jwt/.github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jwt/.github/workflows/tests.yml b/Jwt/.github/workflows/tests.yml index 6e0cd3c1443..2d6d57bc89a 100644 --- a/Jwt/.github/workflows/tests.yml +++ b/Jwt/.github/workflows/tests.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php: [ "8.0", "8.1", "8.2", "8.3", "8.4" ] + php: [ "8.0", "8.1", "8.2", "8.3", "8.4", "8.5" ] name: PHP ${{matrix.php }} Unit Test steps: - uses: actions/checkout@v2 From 8b722fce880b50c3d5b8f3ef762be77ce16c4f2c Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Tue, 17 Feb 2026 09:03:55 -0800 Subject: [PATCH 263/282] chore: add environment for Release Please job (firebase/php-jwt#619) Release-As: 7.0.3 --- Jwt/.github/workflows/release-please.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/Jwt/.github/workflows/release-please.yml b/Jwt/.github/workflows/release-please.yml index 409df545717..f4ff4272389 100644 --- a/Jwt/.github/workflows/release-please.yml +++ b/Jwt/.github/workflows/release-please.yml @@ -10,6 +10,7 @@ permissions: pull-requests: write jobs: release-please: + environment: Release runs-on: ubuntu-latest steps: - uses: googleapis/release-please-action@v4 From 7335cc886c68d863cf99156d3c777bcfb8ad7ada Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 25 Feb 2026 22:16:40 +0000 Subject: [PATCH 264/282] chore(main): release 7.0.3 (firebase/php-jwt#622) --- Jwt/CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Jwt/CHANGELOG.md b/Jwt/CHANGELOG.md index 263760760d9..32a5433ac79 100644 --- a/Jwt/CHANGELOG.md +++ b/Jwt/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [7.0.3](https://github.com/firebase/php-jwt/compare/v7.0.2...v7.0.3) (2026-02-18) + + +### Miscellaneous Chores + +* add environment for Release Please job ([#619](https://github.com/firebase/php-jwt/issues/619)) ([300fd02](https://github.com/firebase/php-jwt/commit/300fd02c883f096c9067df652dbd23f62cb5e2a7)) + ## [7.0.2](https://github.com/firebase/php-jwt/compare/v7.0.1...v7.0.2) (2025-12-16) From 69e0af02c99a1ddd10e3e134b0c20e05ac0f187c Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Fri, 27 Mar 2026 13:47:23 -0700 Subject: [PATCH 265/282] fix: readme examples, add tests for all examples (firebase/php-jwt#626) --- Jwt/README.md | 70 +++++++------- Jwt/composer.json | 3 +- Jwt/tests/ReadmeTest.php | 201 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 238 insertions(+), 36 deletions(-) create mode 100644 Jwt/tests/ReadmeTest.php diff --git a/Jwt/README.md b/Jwt/README.md index 65b6c8609dd..2ca71622471 100644 --- a/Jwt/README.md +++ b/Jwt/README.md @@ -23,16 +23,16 @@ php env does not have libsodium installed: composer require paragonie/sodium_compat ``` -Example -------- +## Example + ```php use Firebase\JWT\JWT; use Firebase\JWT\Key; -$key = 'example_key'; +$key = 'example_key_of_sufficient_length'; $payload = [ - 'iss' => 'http://example.org', - 'aud' => 'http://example.com', + 'iss' => 'example.org', + 'aud' => 'example.com', 'iat' => 1356999524, 'nbf' => 1357000000 ]; @@ -69,8 +69,9 @@ $decoded_array = (array) $decoded; JWT::$leeway = 60; // $leeway in seconds $decoded = JWT::decode($jwt, new Key($key, 'HS256')); ``` -Example encode/decode headers -------- + +## Example encode/decode headers + Decoding the JWT headers without verifying the JWT first is NOT recommended, and is not supported by this library. This is because without verifying the JWT, the header values could have been tampered with. Any value pulled from an unverified header should be treated as if it could be any string sent in from an @@ -80,10 +81,10 @@ header part: ```php use Firebase\JWT\JWT; -$key = 'example_key'; +$key = 'example_key_of_sufficient_length'; $payload = [ - 'iss' => 'http://example.org', - 'aud' => 'http://example.com', + 'iss' => 'example.org', + 'aud' => 'example.com', 'iat' => 1356999524, 'nbf' => 1357000000 ]; @@ -103,8 +104,9 @@ $decoded = json_decode(base64_decode($headersB64), true); print_r($decoded); ``` -Example with RS256 (openssl) ----------------------------- + +## Example with RS256 (openssl) + ```php use Firebase\JWT\JWT; use Firebase\JWT\Key; @@ -172,8 +174,7 @@ $decoded_array = (array) $decoded; echo "Decode:\n" . print_r($decoded_array, true) . "\n"; ``` -Example with a passphrase -------------------------- +## Example with a passphrase ```php use Firebase\JWT\JWT; @@ -209,8 +210,8 @@ $decoded = JWT::decode($jwt, new Key($publicKey, 'RS256')); echo "Decode:\n" . print_r((array) $decoded, true) . "\n"; ``` -Example with EdDSA (libsodium and Ed25519 signature) ----------------------------- +## Example with EdDSA (libsodium and Ed25519 signature) + ```php use Firebase\JWT\JWT; use Firebase\JWT\Key; @@ -238,21 +239,21 @@ echo "Encode:\n" . print_r($jwt, true) . "\n"; $decoded = JWT::decode($jwt, new Key($publicKey, 'EdDSA')); echo "Decode:\n" . print_r((array) $decoded, true) . "\n"; -```` +``` + +## Example with multiple keys -Example with multiple keys --------------------------- ```php use Firebase\JWT\JWT; use Firebase\JWT\Key; // Example RSA keys from previous example -// $privateKey1 = '...'; -// $publicKey1 = '...'; +// $privateRsKey = '...'; +// $publicRsKey = '...'; // Example EdDSA keys from previous example -// $privateKey2 = '...'; -// $publicKey2 = '...'; +// $privateEcKey = '...'; +// $publicEcKey = '...'; $payload = [ 'iss' => 'example.org', @@ -261,14 +262,14 @@ $payload = [ 'nbf' => 1357000000 ]; -$jwt1 = JWT::encode($payload, $privateKey1, 'RS256', 'kid1'); -$jwt2 = JWT::encode($payload, $privateKey2, 'EdDSA', 'kid2'); +$jwt1 = JWT::encode($payload, $privateRsKey, 'RS256', 'kid1'); +$jwt2 = JWT::encode($payload, $privateEcKey, 'EdDSA', 'kid2'); echo "Encode 1:\n" . print_r($jwt1, true) . "\n"; echo "Encode 2:\n" . print_r($jwt2, true) . "\n"; $keys = [ - 'kid1' => new Key($publicKey1, 'RS256'), - 'kid2' => new Key($publicKey2, 'EdDSA'), + 'kid1' => new Key($publicRsKey, 'RS256'), + 'kid2' => new Key($publicEcKey, 'EdDSA'), ]; $decoded1 = JWT::decode($jwt1, $keys); @@ -278,8 +279,7 @@ echo "Decode 1:\n" . print_r((array) $decoded1, true) . "\n"; echo "Decode 2:\n" . print_r((array) $decoded2, true) . "\n"; ``` -Using JWKs ----------- +## Using JWKs ```php use Firebase\JWT\JWK; @@ -291,11 +291,11 @@ $jwks = ['keys' => []]; // JWK::parseKeySet($jwks) returns an associative array of **kid** to Firebase\JWT\Key // objects. Pass this as the second parameter to JWT::decode. -JWT::decode($jwt, JWK::parseKeySet($jwks)); +$decoded = JWT::decode($jwt, JWK::parseKeySet($jwks)); +print_r($decoded); ``` -Using Cached Key Sets ---------------------- +## Using Cached Key Sets The `CachedKeySet` class can be used to fetch and cache JWKS (JSON Web Key Sets) from a public URI. This has the following advantages: @@ -315,7 +315,7 @@ $jwksUri = 'https://www.gstatic.com/iap/verify/public_key-jwk'; $httpClient = new GuzzleHttp\Client(); // Create an HTTP request factory (can be any PSR-17 compatible HTTP request factory) -$httpFactory = new GuzzleHttp\Psr\HttpFactory(); +$httpFactory = new GuzzleHttp\Psr7\HttpFactory(); // Create a cache item pool (can be any PSR-6 compatible cache item pool) $cacheItemPool = Phpfastcache\CacheManager::getInstance('files'); @@ -406,8 +406,8 @@ Tests Run the tests using phpunit: ```bash -$ pear install PHPUnit -$ phpunit --configuration phpunit.xml.dist +$ composer update +$ vendor/bin/phpunit -c phpunit.xml.dist PHPUnit 3.7.10 by Sebastian Bergmann. ..... Time: 0 seconds, Memory: 2.50Mb diff --git a/Jwt/composer.json b/Jwt/composer.json index 816cfd0bd9a..4b988631dc0 100644 --- a/Jwt/composer.json +++ b/Jwt/composer.json @@ -37,6 +37,7 @@ "phpunit/phpunit": "^9.5", "psr/cache": "^2.0||^3.0", "psr/http-client": "^1.0", - "psr/http-factory": "^1.0" + "psr/http-factory": "^1.0", + "phpfastcache/phpfastcache": "^9.2" } } diff --git a/Jwt/tests/ReadmeTest.php b/Jwt/tests/ReadmeTest.php new file mode 100644 index 00000000000..aaa47858dcb --- /dev/null +++ b/Jwt/tests/ReadmeTest.php @@ -0,0 +1,201 @@ + 'example.org', + 'aud' => 'example.com', + 'iat' => 1356999524, + 'nbf' => 1357000000, + ]; + + public function testExample() + { + $codeblock = $this->extractCodeBlock('Example'); + $output = $codeblock->invoke(); + + $header = ['typ' => 'JWT', 'alg' => 'HS256']; + + $this->assertEquals( + print_r((object) $this->payload, true) . print_r((object) $header, true), + $output + ); + } + + public function testExampleEncodeDecodeHeaders() + { + $codeblock = $this->extractCodeBlock('Example encode/decode headers'); + $output = $codeblock->invoke(); + + $header = [ + 'typ' => 'JWT', + 'x-forwarded-for' => 'www.google.com', + 'alg' => 'HS256', + ]; + + $this->assertEquals( + print_r($header, true), + $output + ); + } + + public function testExampleWithRS256() + { + $codeblock = $this->extractCodeBlock('Example with RS256 (openssl)'); + $output = $codeblock->invoke(); + + $this->assertStringContainsString( + "Decode:\n" . print_r($this->payload, true), + $output + ); + } + + public function testExampleWithPassphrase() + { + $codeblock = $this->extractCodeBlock('Example with a passphrase'); + + $codeblock->replace('[YOUR_PASSPHRASE]', 'passphrase'); + $codeblock->replace( + '/path/to/key-with-passphrase.pem', + __DIR__ . '/data/rsa-with-passphrase.pem' + ); + + $output = $codeblock->invoke(); + + $this->assertStringContainsString( + "Decode:\n" . print_r($this->payload, true), + $output + ); + } + + public function testExampleWithEdDSA() + { + $codeblock = $this->extractCodeBlock('Example with EdDSA (libsodium and Ed25519 signature)'); + + $output = $codeblock->invoke(); + + $this->assertStringContainsString( + "Decode:\n" . print_r($this->payload, true), + $output + ); + } + + public function testExampleWithMultipleKeys() + { + $codeblock = $this->extractCodeBlock('Example with multiple keys'); + + $keys = [ + '$privateRsKey' => 'rsa1-private.pem', + '$publicRsKey' => 'rsa1-public.pub', + '$privateEcKey' => 'ed25519-1.sec', + '$publicEcKey' => 'ed25519-1.pub', + ]; + foreach ($keys as $varName => $keyFile) { + $codeblock->replace( + \sprintf('// %s = \'...\'', $varName), + \sprintf('%s = file_get_contents(\'%s/data/%s\')', $varName, __DIR__, $keyFile) + ); + } + + $output = $codeblock->invoke(); + + $this->assertStringContainsString( + "Decode 1:\n" . print_r($this->payload, true), + $output + ); + + $this->assertStringContainsString( + "Decode 2:\n" . print_r($this->payload, true), + $output + ); + } + + public function testUsingJWKs() + { + $codeblock = $this->extractCodeBlock('Using JWKs'); + + $privateKey = file_get_contents(__DIR__ . '/data/rsa1-private.pem'); + $jwt = JWT::encode($this->payload, $privateKey, 'RS256', 'jwk1'); + + $keysJson = file_get_contents(__DIR__ . '/data/rsa-jwkset.json'); + $jwkSet = json_decode($keysJson, true); + + $codeblock->replace('$jwt', \sprintf("'%s'", $jwt)); + $codeblock->replace( + '[\'keys\' => []]', + var_export($jwkSet, true) + ); + + $output = $codeblock->invoke(); + + $this->assertEquals( + print_r((object) $this->payload, true), + $output + ); + } + + public function testUsingCachedKeySets() + { + // We must accept a failure because we are not signing the keys + // This is the farthest we can go without retreiving an actual JWT + // or hosting our own JWKs url. + $this->expectException(SignatureInvalidException::class); + $this->expectExceptionMessage('Signature verification failed'); + + $codeblock = $this->extractCodeBlock('Using Cached Key Sets'); + + $privateKey = file_get_contents(__DIR__ . '/data/ecdsa256-private.pem'); + $jwt = JWT::encode($this->payload, $privateKey, 'ES256', '_xiGEQ'); + + $codeblock->replace('eyJhbGci...', $jwt); + $codeblock->invoke(); + } + + private function extractCodeBlock(string $header) + { + // Normalize line endings to \n to make regex handling consistent across platforms + $markdown = str_replace(["\r\n", "\r"], "\n", file_get_contents(__DIR__ . '/../README.md')); + + // find by header + $pattern = '/^#+\s*' . preg_quote($header, '/') . '\s*\n([\s\S]*?)(?=^#+.*$|\Z)/m'; + if (!preg_match($pattern, $markdown, $matches)) { + throw new \Exception('Header "' . $header . '" not found in README.md'); + } + $markdown = trim($matches[1]); + + // extract fenced codeblock + if (!preg_match_all(self::CODEBLOCK_REGEX, $markdown, $matches, PREG_SET_ORDER)) { + throw new \Exception('No code block found in README.md under header "' . $header . '"'); + } + $codeblock = $matches[0][4]; + + return new class($codeblock) { + public function __construct(public string $codeblock) + { + } + + public function invoke() + { + try { + ob_start(); + eval($this->codeblock); + return ob_get_clean(); + } catch (\Exception $e) { + ob_end_clean(); + throw $e; + } + } + + public function replace($old, $new) + { + $this->codeblock = str_replace($old, $new, $this->codeblock); + } + }; + } +} From ba4b2013ab033efa77b61f244266df271b1225ac Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Fri, 27 Mar 2026 13:47:52 -0700 Subject: [PATCH 266/282] fix: use urlsafeB64Decode everywhere (firebase/php-jwt#627) --- Jwt/src/CachedKeySet.php | 3 ++- Jwt/src/JWT.php | 51 +++++++++++++++++++--------------------- Jwt/tests/JWTTest.php | 34 +++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 28 deletions(-) diff --git a/Jwt/src/CachedKeySet.php b/Jwt/src/CachedKeySet.php index 8e8e8d68cae..37c3f94d2f5 100644 --- a/Jwt/src/CachedKeySet.php +++ b/Jwt/src/CachedKeySet.php @@ -180,7 +180,8 @@ private function keyIdExists(string $keyId): bool $jwksResponse = $this->httpClient->sendRequest($request); if ($jwksResponse->getStatusCode() !== 200) { throw new UnexpectedValueException( - \sprintf('HTTP Error: %d %s for URI "%s"', + \sprintf( + 'HTTP Error: %d %s for URI "%s"', $jwksResponse->getStatusCode(), $jwksResponse->getReasonPhrase(), $this->jwksUri, diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index 90f62ca9deb..0d2e47c9500 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -31,7 +31,7 @@ class JWT private const ASN1_SEQUENCE = 0x10; private const ASN1_BIT_STRING = 0x03; - private const RSA_KEY_MIN_LENGTH=2048; + private const RSA_KEY_MIN_LENGTH = 2048; /** * When checking nbf, iat or expiration times, @@ -284,20 +284,8 @@ public static function sign( } return $signature; case 'sodium_crypto': - if (!\function_exists('sodium_crypto_sign_detached')) { - throw new DomainException('libsodium is not available'); - } - if (!\is_string($key)) { - throw new InvalidArgumentException('key must be a string when using EdDSA'); - } try { - // The last non-empty line is used as the key. - $lines = array_filter(explode("\n", $key)); - $key = base64_decode((string) end($lines)); - if (\strlen($key) === 0) { - throw new DomainException('Key cannot be empty string'); - } - return sodium_crypto_sign_detached($msg, $key); + return sodium_crypto_sign_detached($msg, self::validateEdDSAKey($key)); } catch (Exception $e) { throw new DomainException($e->getMessage(), 0, $e); } @@ -352,19 +340,8 @@ private static function verify( 'OpenSSL error: ' . \openssl_error_string() ); case 'sodium_crypto': - if (!\function_exists('sodium_crypto_sign_verify_detached')) { - throw new DomainException('libsodium is not available'); - } - if (!\is_string($keyMaterial)) { - throw new InvalidArgumentException('key must be a string when using EdDSA'); - } try { - // The last non-empty line is used as the key. - $lines = array_filter(explode("\n", $keyMaterial)); - $key = base64_decode((string) end($lines)); - if (\strlen($key) === 0) { - throw new DomainException('Key cannot be empty string'); - } + $key = self::validateEdDSAKey($keyMaterial); if (\strlen($signature) === 0) { throw new DomainException('Signature cannot be empty string'); } @@ -473,7 +450,6 @@ public static function urlsafeB64Encode(string $input): string return \str_replace('=', '', \strtr(\base64_encode($input), '+/', '-_')); } - /** * Determine if an algorithm has been provided for each Key * @@ -745,4 +721,25 @@ private static function validateEcKeyLength( throw new DomainException('Provided key is too short'); } } + + /** + * @param string|OpenSSLAsymmetricKey|OpenSSLCertificate $keyMaterial + * @return non-empty-string + */ + private static function validateEdDSAKey(#[\SensitiveParameter] $keyMaterial): string + { + if (!\function_exists('sodium_crypto_sign_verify_detached')) { + throw new DomainException('libsodium is not available'); + } + if (!\is_string($keyMaterial)) { + throw new InvalidArgumentException('key must be a string when using EdDSA'); + } + // The last non-empty line is used as the key. + $lines = array_filter(explode("\n", $keyMaterial)); + $key = self::urlsafeB64Decode((string) end($lines)); + if (\strlen($key) === 0) { + throw new DomainException('Key cannot be empty string'); + } + return $key; + } } diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index a1dd08a4374..6832655fa40 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -678,6 +678,40 @@ public function provideHmac() ]; } + public function testEdDsaHandlesBase64UrlKeys() + { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('libsodium is not available'); + } + + // Generate a deterministic Ed25519 keypair using a specific seed. The byte "\xfb" + // translates to '+' and '/' in standard base64, which become '-' and '_' in Base64URL. + // This guarantees our keys will contain the URL-safe characters that get incorrectly + // stripped by base64_decode(). + $seed = str_repeat("\xfb", 32); + $keyPair = sodium_crypto_sign_seed_keypair($seed); + + $secretKey = sodium_crypto_sign_secretkey($keyPair); + $publicKey = sodium_crypto_sign_publickey($keyPair); + + // Convert the raw keys to Base64URL encoded strings + $secretKeyB64u = JWT::urlsafeB64Encode($secretKey); + $publicKeyB64u = JWT::urlsafeB64Encode($publicKey); + + // Ensure our test keys actually contain the characters that get + // incorrectly stripped by a standard base64_decode(). + $this->assertTrue(strpos($secretKeyB64u, '-') !== false || strpos($secretKeyB64u, '_') !== false); + $this->assertTrue(strpos($publicKeyB64u, '-') !== false || strpos($publicKeyB64u, '_') !== false); + + // Test Encoding + $token = JWT::encode(['issue' => 596], $secretKeyB64u, 'EdDSA'); + $this->assertIsString($token); + + // Test Decoding + $decoded = JWT::decode($token, new Key($publicKeyB64u, 'EdDSA')); + $this->assertSame(596, $decoded->issue); + } + /** @dataProvider provideEcKeyInvalidLength */ public function testEcKeyLengthValidationThrowsExceptionEncode(string $keyFile, string $alg): void { From 3c5d2732cef6d5bb3b42f4c9e96e75b519a6f347 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Fri, 27 Mar 2026 14:17:19 -0700 Subject: [PATCH 267/282] chore(main): release 7.0.4 (firebase/php-jwt#629) --- Jwt/CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Jwt/CHANGELOG.md b/Jwt/CHANGELOG.md index 32a5433ac79..af27c61c7a1 100644 --- a/Jwt/CHANGELOG.md +++ b/Jwt/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [7.0.4](https://github.com/firebase/php-jwt/compare/v7.0.3...v7.0.4) (2026-03-27) + + +### Bug Fixes + +* readme examples, add tests for all examples ([#626](https://github.com/firebase/php-jwt/issues/626)) ([510a00c](https://github.com/firebase/php-jwt/commit/510a00c0e6353bc7d68412fab67e57a13954cb46)) +* use urlsafeB64Decode everywhere ([#627](https://github.com/firebase/php-jwt/issues/627)) ([b889495](https://github.com/firebase/php-jwt/commit/b889495c83ddc3f3885ca3f0b65b41b1cb37a3b1)) + ## [7.0.3](https://github.com/firebase/php-jwt/compare/v7.0.2...v7.0.3) (2026-02-18) From 750b6cc7971cd4a67c08a08e0641446a031d5efd Mon Sep 17 00:00:00 2001 From: NReib <120319811+NReib@users.noreply.github.com> Date: Tue, 31 Mar 2026 21:28:59 +0200 Subject: [PATCH 268/282] fix: RSA from JWK sometimes returns empty Instance (firebase/php-jwt#628) --- Jwt/src/JWK.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Jwt/src/JWK.php b/Jwt/src/JWK.php index d5175b21708..e083c224bb7 100644 --- a/Jwt/src/JWK.php +++ b/Jwt/src/JWK.php @@ -240,6 +240,14 @@ private static function createPemFromModulusAndExponent( ): string { $mod = JWT::urlsafeB64Decode($n); $exp = JWT::urlsafeB64Decode($e); + // Correct encoding for ASN1, as ints are represented as unsigned in jwk + // but signed in ASN1. Prepending null byte makes it unsigned. + if (\strlen($mod) > 0 && \ord($mod[0]) >= 128) { + $mod = \chr(0) . $mod; + } + if (\strlen($exp) > 0 && \ord($exp[0]) >= 128) { + $exp = \chr(0) . $exp; + } $modulus = \pack('Ca*a*', 2, self::encodeLength(\strlen($mod)), $mod); $publicExponent = \pack('Ca*a*', 2, self::encodeLength(\strlen($exp)), $exp); From a304572b5bdfcb0897f637020fffbe831fe07729 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 1 Apr 2026 13:38:03 -0700 Subject: [PATCH 269/282] chore(main): release 7.0.5 (firebase/php-jwt#630) --- Jwt/CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Jwt/CHANGELOG.md b/Jwt/CHANGELOG.md index af27c61c7a1..498fb0007a0 100644 --- a/Jwt/CHANGELOG.md +++ b/Jwt/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [7.0.5](https://github.com/firebase/php-jwt/compare/v7.0.4...v7.0.5) (2026-03-31) + + +### Bug Fixes + +* RSA from JWK sometimes returns empty Instance ([#628](https://github.com/firebase/php-jwt/issues/628)) ([b4c78aa](https://github.com/firebase/php-jwt/commit/b4c78aa731664122198ad36c0033aa29e807397a)) + ## [7.0.4](https://github.com/firebase/php-jwt/compare/v7.0.3...v7.0.4) (2026-03-27) From 965be24e6353b426275975fcc75516ab2bcfa794 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 1 Apr 2026 13:38:24 -0700 Subject: [PATCH 270/282] chore: add tests for JWK ASN1 fix (firebase/php-jwt#631) --- Jwt/tests/JWKTest.php | 36 +++++++++++++++++++++++++----- Jwt/tests/data/rsa-jwk2-public.pub | 9 ++++++++ Jwt/tests/data/rsa-jwkset-2.json | 11 +++++++++ 3 files changed, 50 insertions(+), 6 deletions(-) create mode 100644 Jwt/tests/data/rsa-jwk2-public.pub create mode 100644 Jwt/tests/data/rsa-jwkset-2.json diff --git a/Jwt/tests/JWKTest.php b/Jwt/tests/JWKTest.php index db385c87fb6..7569d5fa91a 100644 --- a/Jwt/tests/JWKTest.php +++ b/Jwt/tests/JWKTest.php @@ -9,8 +9,15 @@ class JWKTest extends TestCase { private static $keys; - private static $privKey1; - private static $privKey2; + + public static function setUpBeforeClass(): void + { + $jwkSet = json_decode( + file_get_contents(__DIR__ . '/data/rsa-jwkset.json'), + true + ); + self::$keys = JWK::parseKeySet($jwkSet); + } public function testMissingKty() { @@ -84,16 +91,33 @@ public function testParseKeyWithEmptyDValue() $this->assertTrue(\is_array($keys)); } - public function testParseJwkKeySet() + /** @dataProvider provideParseJwkKeySet */ + public function testParseJwkKeySet($jwkFile, $keyId, $pubkeyFile) { $jwkSet = json_decode( - file_get_contents(__DIR__ . '/data/rsa-jwkset.json'), + file_get_contents(__DIR__ . '/data/' . $jwkFile), true ); $keys = JWK::parseKeySet($jwkSet); $this->assertTrue(\is_array($keys)); - $this->assertArrayHasKey('jwk1', $keys); - self::$keys = $keys; + $this->assertArrayHasKey($keyId, $keys); + + // verify public key + $keyMaterial = $keys[$keyId]->getKeyMaterial(); + $publicKey = openssl_pkey_get_details($keyMaterial)['key']; + + $this->assertEquals( + file_get_contents(__DIR__ . '/data/' . $pubkeyFile), + $publicKey + ); + } + + public function provideParseJwkKeySet() + { + return [ + ['rsa-jwkset.json', 'jwk1', 'rsa1-public.pub'], + ['rsa-jwkset-2.json', 'jwk2', 'rsa-jwk2-public.pub'], + ]; } public function testParseJwkKey_empty() diff --git a/Jwt/tests/data/rsa-jwk2-public.pub b/Jwt/tests/data/rsa-jwk2-public.pub new file mode 100644 index 00000000000..cdc87c98ef2 --- /dev/null +++ b/Jwt/tests/data/rsa-jwk2-public.pub @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyKqGRQyJtqxRm/Mo2YTC +CAkPSDb7uNgC7tXjgVzNv2/XB8r4vMibBpZFPbwyVUk0wGhPk8qLjrIj/K8IMu/I +Ytkq87pc1/1FAOub7e3xUrMx66GCq8QG94xROSfDWuMy7twILwjbkzNEU6bNibM0 +IQbCvdybFPhq4YHvlwOjfuMl2mNUma8wT1/l2MZenV1dmeLTg/kYGe9PGmn9JiY4 +t01Nj1FJQj9rH863KAa3LadQ4l8aBOpaIZwjANo3GCJJd4uSB67G+p0wuuDDYbiU +GtN55degXjDKrv3v5bLgpPMX6ynvt2bi0olb/QZfovTnUaLfsZpCXTk/CvUXr2Q2 +KwIDAQAB +-----END PUBLIC KEY----- diff --git a/Jwt/tests/data/rsa-jwkset-2.json b/Jwt/tests/data/rsa-jwkset-2.json new file mode 100644 index 00000000000..433a778cdac --- /dev/null +++ b/Jwt/tests/data/rsa-jwkset-2.json @@ -0,0 +1,11 @@ +{ + "keys": [ + { + "kty": "RSA", + "e": "AQAB", + "kid": "jwk2", + "alg": "RS256", + "n": "yKqGRQyJtqxRm_Mo2YTCCAkPSDb7uNgC7tXjgVzNv2_XB8r4vMibBpZFPbwyVUk0wGhPk8qLjrIj_K8IMu_IYtkq87pc1_1FAOub7e3xUrMx66GCq8QG94xROSfDWuMy7twILwjbkzNEU6bNibM0IQbCvdybFPhq4YHvlwOjfuMl2mNUma8wT1_l2MZenV1dmeLTg_kYGe9PGmn9JiY4t01Nj1FJQj9rH863KAa3LadQ4l8aBOpaIZwjANo3GCJJd4uSB67G-p0wuuDDYbiUGtN55degXjDKrv3v5bLgpPMX6ynvt2bi0olb_QZfovTnUaLfsZpCXTk_CvUXr2Q2Kw" + } + ] +} \ No newline at end of file From dc588cf6232c62aa5cb366f6b825582ec1a8b04d Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Mon, 11 May 2026 12:08:23 -0700 Subject: [PATCH 271/282] chore: fix ReadmeTest (firebase/php-jwt#636) --- Jwt/tests/ReadmeTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Jwt/tests/ReadmeTest.php b/Jwt/tests/ReadmeTest.php index aaa47858dcb..29579f4df75 100644 --- a/Jwt/tests/ReadmeTest.php +++ b/Jwt/tests/ReadmeTest.php @@ -150,8 +150,9 @@ public function testUsingCachedKeySets() $codeblock = $this->extractCodeBlock('Using Cached Key Sets'); + $jwk = json_decode(file_get_contents('https://www.gstatic.com/iap/verify/public_key-jwk'), true); $privateKey = file_get_contents(__DIR__ . '/data/ecdsa256-private.pem'); - $jwt = JWT::encode($this->payload, $privateKey, 'ES256', '_xiGEQ'); + $jwt = JWT::encode($this->payload, $privateKey, 'ES256', $jwk['keys'][0]['kid']); $codeblock->replace('eyJhbGci...', $jwt); $codeblock->invoke(); From 037090859da8f0507db83a6c19ea6ff5ed6f102c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillaume=20Delr=C3=A9?= Date: Mon, 11 May 2026 21:22:02 +0200 Subject: [PATCH 272/282] fix: validate numeric type of iat, nbf and exp claims in encode (firebase/php-jwt#634) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: validate numeric type of iat, nbf and exp claims in encode The decode() path already rejected non-numeric iat/nbf/exp values, but encode() accepted any type and silently produced an invalid token. Add the same is_numeric guards in encode() and cover them with unit tests. Signed-off-by: Guillaume Delré * test: add regression test for numeric string timestamps in encode Covers the case where iat/nbf/exp are passed as numeric strings (e.g. (string) time()), which must remain accepted to avoid a breaking change. Signed-off-by: Guillaume Delré --------- Signed-off-by: Guillaume Delré Co-authored-by: Brent Shaffer --- Jwt/src/JWT.php | 9 +++++++++ Jwt/tests/JWTTest.php | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index 0d2e47c9500..706aef58057 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -224,6 +224,15 @@ public static function encode( if ($keyId !== null) { $header['kid'] = $keyId; } + if (isset($payload['nbf']) && !\is_numeric($payload['nbf'])) { + throw new UnexpectedValueException('Payload nbf must be a number'); + } + if (isset($payload['iat']) && !\is_numeric($payload['iat'])) { + throw new UnexpectedValueException('Payload iat must be a number'); + } + if (isset($payload['exp']) && !\is_numeric($payload['exp'])) { + throw new UnexpectedValueException('Payload exp must be a number'); + } $segments = []; $segments[] = static::urlsafeB64Encode((string) static::jsonEncode($header)); $segments[] = static::urlsafeB64Encode((string) static::jsonEncode($payload)); diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index 6832655fa40..60b7ae4897b 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -609,6 +609,45 @@ public function testDecodeExpectsIntegerExp() JWT::decode($payload, $this->hmacKey); } + public function testEncodeThrowsWhenIatIsNotNumeric(): void + { + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage('Payload iat must be a number'); + + JWT::encode(['iat' => 'not-an-int'], $this->hmacKey->getKeyMaterial(), 'HS256'); + } + + public function testEncodeThrowsWhenNbfIsNotNumeric(): void + { + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage('Payload nbf must be a number'); + + JWT::encode(['nbf' => 'not-an-int'], $this->hmacKey->getKeyMaterial(), 'HS256'); + } + + public function testEncodeThrowsWhenExpIsNotNumeric(): void + { + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage('Payload exp must be a number'); + + JWT::encode(['exp' => 'not-an-int'], $this->hmacKey->getKeyMaterial(), 'HS256'); + } + + public function testEncodeAcceptsNumericStringTimestamps(): void + { + $payload = [ + 'message' => 'abc', + 'iat' => (string) time(), + 'exp' => (string) (time() + 20), + 'nbf' => (string) (time() - 20), + ]; + + $encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256'); + $decoded = JWT::decode($encoded, $this->hmacKey); + + $this->assertSame('abc', $decoded->message); + } + public function testRsaKeyLengthValidationThrowsException(): void { $this->expectException(DomainException::class); From 9f06bc9c8acb80f165f2ba468621f1517ac8923b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Mon, 11 May 2026 22:45:33 +0300 Subject: [PATCH 273/282] Fix: Update homepage URL in composer.json (firebase/php-jwt#635) Avoid redirection: - From: https://github.com/firebase/php-jwt - To: https://github.com/googleapis/php-jwt Co-authored-by: Brent Shaffer --- Jwt/composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jwt/composer.json b/Jwt/composer.json index 4b988631dc0..72b85a7da89 100644 --- a/Jwt/composer.json +++ b/Jwt/composer.json @@ -1,7 +1,7 @@ { "name": "firebase/php-jwt", "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", - "homepage": "https://github.com/firebase/php-jwt", + "homepage": "https://github.com/googleapis/php-jwt", "keywords": [ "php", "jwt" From f64bcb7ed77d00c4a71b13de6ba1f353d7058a8a Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Tue, 9 Jun 2026 14:31:44 -0700 Subject: [PATCH 274/282] fix: skip parsing valid unsupported curves (firebase/php-jwt#638) --- Jwt/src/JWK.php | 14 +++++++- Jwt/tests/JWKTest.php | 33 ++++++++++++++++++ Jwt/tests/data/unsupported-alg-keyset.json | 39 ++++++++++++++++++++++ 3 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 Jwt/tests/data/unsupported-alg-keyset.json diff --git a/Jwt/src/JWK.php b/Jwt/src/JWK.php index e083c224bb7..7a1f44883e0 100644 --- a/Jwt/src/JWK.php +++ b/Jwt/src/JWK.php @@ -31,6 +31,15 @@ class JWK // 'P-521' => '1.3.132.0.35', // Len: 132 (not supported) ]; + // Known standard curves from the IANA JOSE registry which are not supported + private const KNOWN_UNSUPPORTED_EC_CURVES = [ + 'P-521', // RFC 7518 + 'Ed25519', // RFC 8037 + 'Ed448', // RFC 8037 + 'X25519', // RFC 8037 + 'X448' // RFC 8037 + ]; + // For keys with "kty" equal to "OKP" (Octet Key Pair), the "crv" parameter must contain the key subtype. // This library supports the following subtypes: private const OKP_SUBTYPES = [ @@ -142,7 +151,10 @@ public static function parseKey(#[\SensitiveParameter] array $jwk, ?string $defa } if (!isset(self::EC_CURVES[$jwk['crv']])) { - throw new DomainException('Unrecognised or unsupported EC curve'); + if (!\in_array($jwk['crv'], self::KNOWN_UNSUPPORTED_EC_CURVES)) { + throw new DomainException('Unrecognised EC curve'); + } + return null; } if (empty($jwk['x']) || empty($jwk['y'])) { diff --git a/Jwt/tests/JWKTest.php b/Jwt/tests/JWKTest.php index 7569d5fa91a..3d3caf9a62c 100644 --- a/Jwt/tests/JWKTest.php +++ b/Jwt/tests/JWKTest.php @@ -2,6 +2,7 @@ namespace Firebase\JWT; +use DomainException; use InvalidArgumentException; use PHPUnit\Framework\TestCase; use UnexpectedValueException; @@ -136,6 +137,38 @@ public function testParseJwkKeySet_empty() JWK::parseKeySet(['keys' => []]); } + public function testParseJwkKeySetWithValidButUnsupportedCurveDoesNotThrowException() + { + $jwkSet = json_decode( + file_get_contents(__DIR__ . '/data/unsupported-alg-keyset.json'), + true + ); + + $this->assertCount(3, $jwkSet['keys']); + + $keys = JWK::parseKeySet($jwkSet); + + $this->assertCount(2, $keys); + $this->assertArrayHasKey('jwk1', $keys); + $this->assertArrayHasKey('jwk2', $keys); + $this->assertArrayNotHasKey('unsupported-ec-curve', $keys); + } + + public function testParseJwkKeySetWithInvalidCurveThrowsException() + { + $this->expectException(DomainException::class); + $this->expectExceptionMessage('Unrecognised EC curve'); + + $jwkSet = json_decode( + file_get_contents(__DIR__ . '/data/unsupported-alg-keyset.json'), + true + ); + + $jwkSet['keys'][2]['crv'] = 'invalid-curve'; + + $keys = JWK::parseKeySet($jwkSet); + } + /** * @depends testParseJwkKeySet */ diff --git a/Jwt/tests/data/unsupported-alg-keyset.json b/Jwt/tests/data/unsupported-alg-keyset.json new file mode 100644 index 00000000000..574dcc9d4fc --- /dev/null +++ b/Jwt/tests/data/unsupported-alg-keyset.json @@ -0,0 +1,39 @@ +{ + "keys": [ + { + "kid": "jwk1", + "kty": "RSA", + "alg": "RSA-OAEP", + "use": "enc", + "n": "vZi3klbtMtzknokuHuU4WyAs7HFnCG-QUSa2Y-PfbwqONlW7ZruTd9lKv6xVzQKqAldhrF2Ljm1dujMkdMSHYHW7L7WD718k0QX-duH_Z3kediGVgVVIikV5y17wYliHcM9x2lUV1EzVhfI3BQrhu-yHkmzvhuHAosr-kDELD483ReMYbG4f79DHoTl-NAg1ZgKhmUc2aPhut9NrJAIE7Q6mf2EnOeHP9e7KzQPOJW7-_ACnRb2E3iXBqpeX4-uNN5h2zCM1jzgN1m8v3xlqJnz4cydIAJ6Np1fCLief31UK6Tiz84xOgjEhCowjK_i1zrm7ciq-Sgs1heMIfKQ5JQ", + "e": "AQAB", + "x5c": [ + "MIICnzCCAYcCBgGEr+Dy7jANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDDAhlbnRyb3BpYTAeFw0yMjExMjUxNzM5MjVaFw0zMjExMjUxNzQxMDVaMBMxETAPBgNVBAMMCGVudHJvcGlhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvZi3klbtMtzknokuHuU4WyAs7HFnCG+QUSa2Y+PfbwqONlW7ZruTd9lKv6xVzQKqAldhrF2Ljm1dujMkdMSHYHW7L7WD718k0QX+duH/Z3kediGVgVVIikV5y17wYliHcM9x2lUV1EzVhfI3BQrhu+yHkmzvhuHAosr+kDELD483ReMYbG4f79DHoTl+NAg1ZgKhmUc2aPhut9NrJAIE7Q6mf2EnOeHP9e7KzQPOJW7+/ACnRb2E3iXBqpeX4+uNN5h2zCM1jzgN1m8v3xlqJnz4cydIAJ6Np1fCLief31UK6Tiz84xOgjEhCowjK/i1zrm7ciq+Sgs1heMIfKQ5JQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQAKbEduhsT1yxsCymFBHYDNdSXEtZnCSf/NGan9+wyOZtaDyYYtuAipiX43mahkXP/3RlPyDn6UfMXTsdzeD8tml1sZ1bVqD39VsbPZOr6HSvh1JlX40Bn8mLEU/FoQ4OfgrQzdgSLehFLbZ5P0RWwUcZjV+OpdxkOcC7pSb6y1W+3UfsfVhejTUHncan5ZInMI7GWdMRGVuN4JMuAbgnp2v6XqL1di3gTBJl83vFE8GRdwCoyJJiOzXUqcIjWfUsC6PLeHKtPqPlxtHHyvgJdAuGYVLameBoeDJISu3nxkng/RfSU0iwX1hf/1eHOhva0HG5FjMHQNYGJ+V7wYpxOr" + ], + "x5t": "1Nd1lnfuVZjpEjnFppIrEcpJhW4", + "x5t#S256": "8NRLcvxX_jloFTT5EZMkchtgkJylb_v0ZwK1HJSQXW4" + }, + { + "kid": "jwk2", + "kty": "RSA", + "alg": "RS256", + "use": "sig", + "n": "tI59R8YidctuCz_3-51aP86L4hGbsF8JeuEuQNmArQ1Xj22jwEUSr-YDPb5u6XViyWzLJI6_z5j3vK7O5XVOQGa09LkcHDdPWeuWq3Twy8ZTTqf8miMgIVLNhNYUmMwTFvvg9tVl-sF4XjcxEUdknvdjgDbkHQgfVEycGcinh-u66ZSXFrYcwD1sGSDPA8RzYDpb9wkedDGQU-s7UZFWRPVbFpg9HiK90f7YNJDbR_xZxwMQftsrTlsArfXptdgx295OrF11BQ_CguftLxUC-6C8W-XxXZb49mzDpznR6ITBo3YfkvWnqL4KplNzG6p9l_3ZVqbCuMUiZYv5dV5iYw", + "e": "AQAB", + "x5c": [ + "MIICnzCCAYcCBgGEr+DwPzANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDDAhlbnRyb3BpYTAeFw0yMjExMjUxNzM5MjVaFw0zMjExMjUxNzQxMDVaMBMxETAPBgNVBAMMCGVudHJvcGlhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtI59R8YidctuCz/3+51aP86L4hGbsF8JeuEuQNmArQ1Xj22jwEUSr+YDPb5u6XViyWzLJI6/z5j3vK7O5XVOQGa09LkcHDdPWeuWq3Twy8ZTTqf8miMgIVLNhNYUmMwTFvvg9tVl+sF4XjcxEUdknvdjgDbkHQgfVEycGcinh+u66ZSXFrYcwD1sGSDPA8RzYDpb9wkedDGQU+s7UZFWRPVbFpg9HiK90f7YNJDbR/xZxwMQftsrTlsArfXptdgx295OrF11BQ/CguftLxUC+6C8W+XxXZb49mzDpznR6ITBo3YfkvWnqL4KplNzG6p9l/3ZVqbCuMUiZYv5dV5iYwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQArE6mZeWH4haqOGRjtoPHcjO9J4giO9VR6Cxay4bcgrnJYVuIAn5y6umaLjUaP8IthQMMhDcXh3dkoXkaUvlZFtelB29JXZjwPW66rmrPW/5iVqst6qUghyMI80pQyw4UPZKE09atchMiWwtmJ63BbzAYXizzt4FnWJqX6vcSr5cXRrtcYn4WWxaYVAUqSN5koust3vN7YOKhTk0XBpay+dirfa3ENwLbq4KjhImmHwcZa3RpRgkHInbG2U8u6nZDTsOmyIDgVnG8/LZmBHINQ9vhpiLTiERzk0zRk1WtwojHoe9A5MfEgqC3MW5KMVOYlfE+d0QnvCJ3KKaOj8xwE" + ], + "x5t": "GQ1-KNU1SgsG8tFXiqNjTt-x1IQ", + "x5t#S256": "6L5_LevQpy2vP_VUiqOBM3hGIXVIx1EmzsTQPPh0jWc" + }, + { + "kid": "unsupported-ec-curve", + "kty": "EC", + "alg": "ES512", + "use": "sig", + "crv": "P-521", + "x": "ANCPjd7cPxRHok4a444BTEa0NjIGBfeXlQK6OtdTUjJojmeWznop8koQh4P9sN4NuU2nm49PglN2Jq7GF3eANSDr", + "y": "Aag3LtKNyL4NJualANsYqnITNQE2H00ZXzogOq_3QbcPwspG8St_dHTFoKEiOSl-Y9776_kJHBPtdlh1H7SY5AbZ" + } + ] +} From 8105e70db9529023f4aa69f5430a9a5431c69db9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Mendoza?= Date: Tue, 9 Jun 2026 17:49:36 -0400 Subject: [PATCH 275/282] feat: Add support for PS256 (firebase/php-jwt#637) --- Jwt/README.md | 32 +++++++++++++++++ Jwt/composer.json | 6 ++-- Jwt/src/JWT.php | 83 +++++++++++++++++++++++++++++++++++++++++-- Jwt/tests/JWTTest.php | 54 ++++++++++++++++++++++++++++ 4 files changed, 171 insertions(+), 4 deletions(-) diff --git a/Jwt/README.md b/Jwt/README.md index 2ca71622471..59a99d9fdda 100644 --- a/Jwt/README.md +++ b/Jwt/README.md @@ -279,6 +279,38 @@ echo "Decode 1:\n" . print_r((array) $decoded1, true) . "\n"; echo "Decode 2:\n" . print_r((array) $decoded2, true) . "\n"; ``` +## Example with PS256 + +### Note +PHP's OpenSSL extension does not support RSASSA-PSS signatures (PS256) by default, so we provide support via a soft dependency on the [phpseclib/phpseclib](https://github.com/phpseclib/phpseclib) library. It is necessary to install this library in your project if you plan to use PS256. +```bash +composer install phpseclib/phpseclib:^3.0 +``` + +```php +use Firebase\JWT\JWT; +use Firebase\JWT\Key; + +$privateRsKey = '-----BEGIN RSA PRIVATE KEY----- ...'; +$publicKey = '-----BEGIN PUBLIC KEY----- ...'; + +$payload = [ + 'iss' => 'example.org', + 'aud' => 'example.com', + 'iat' => 1356999524, + 'nbf' => 1357000000 +]; + +/** +* PS256 support requires phpseclib/phpseclib +*/ +$jwt = JWT::encode($payload, $privateRsKey, 'PS256', 'keyid'); +echo "Encode:\n" . print_r($jwt, true) . "\n"; + +$decoded = JWT::decode($jwt, new Key($publicKey, 'PS256')); +echo "Decode:\n" . print_r((array) $decoded, true) . "\n"; +``` + ## Using JWKs ```php diff --git a/Jwt/composer.json b/Jwt/composer.json index 72b85a7da89..50d0ff91c36 100644 --- a/Jwt/composer.json +++ b/Jwt/composer.json @@ -24,7 +24,8 @@ }, "suggest": { "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present", - "ext-sodium": "Support EdDSA (Ed25519) signatures" + "ext-sodium": "Support EdDSA (Ed25519) signatures", + "phpseclib/phpseclib": "Support PS256 (RSASSA-PSS) signatures" }, "autoload": { "psr-4": { @@ -38,6 +39,7 @@ "psr/cache": "^2.0||^3.0", "psr/http-client": "^1.0", "psr/http-factory": "^1.0", - "phpfastcache/phpfastcache": "^9.2" + "phpfastcache/phpfastcache": "^9.2", + "phpseclib/phpseclib": "~3.0" } } diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index 706aef58057..1c71ab3d082 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -64,7 +64,8 @@ class JWT 'RS256' => ['openssl', 'SHA256'], 'RS384' => ['openssl', 'SHA384'], 'RS512' => ['openssl', 'SHA512'], - 'EdDSA' => ['sodium_crypto', 'EdDSA'], + 'PS256' => ['openssl', 'SHA256'], + 'EdDSA' => ['sodium_crypto', 'EdDSA'] ]; /** @@ -250,7 +251,7 @@ public static function encode( * @param string $msg The message to sign * @param string|OpenSSLAsymmetricKey|OpenSSLCertificate $key The secret key. * @param string $alg Supported algorithms are 'EdDSA', 'ES384', 'ES256', 'ES256K', 'HS256', - * 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512' + * 'HS384', 'HS512', 'RS256', 'RS384', 'PS256' and 'RS512' * * @return string An encrypted message * @@ -273,6 +274,9 @@ public static function sign( self::validateHmacKeyLength($key, $algorithm); return \hash_hmac($algorithm, $msg, $key, true); case 'openssl': + if ($alg === 'PS256') { + return self::signPS256($key, $msg); + } $signature = ''; if (!$key = openssl_pkey_get_private($key)) { throw new DomainException('OpenSSL unable to validate key'); @@ -329,6 +333,9 @@ private static function verify( list($function, $algorithm) = static::$supported_algs[$alg]; switch ($function) { case 'openssl': + if ($alg === 'PS256') { + return self::verifyPS256($keyMaterial, $msg, $signature); + } if (!$key = openssl_pkey_get_public($keyMaterial)) { throw new DomainException('OpenSSL unable to validate key'); } @@ -751,4 +758,76 @@ private static function validateEdDSAKey(#[\SensitiveParameter] $keyMaterial): s } return $key; } + + /** + * Signs a message with a PS256 algorithm + * + * @param string|OpenSSLAsymmetricKey|OpenSSLCertificate $key + * @param string $message + * @throws DomainException Provided key is invalid + */ + private static function signPS256( + #[\SensitiveParameter] string|OpenSSLAsymmetricKey|OpenSSLCertificate $key, + string $message + ): string { + if (!class_exists('\phpseclib3\Crypt\RSA')) { + throw new DomainException('phpseclib/phpseclib is required for PS256 support'); + } + + if ($key instanceof OpenSSLCertificate) { + throw new DomainException('Cannot sign with an X.509 certificate. A private key is required.'); + } + + if ($key instanceof OpenSSLAsymmetricKey) { + if (!openssl_pkey_export($key, $pem)) { + throw new DomainException('OpenSSL unable to export the AsymmetricKey'); + } + $key = $pem; + } + + /** @var \phpseclib3\Crypt\RSA\PrivateKey $rsa */ + $rsa = \phpseclib3\Crypt\PublicKeyLoader::load($key); + + return $rsa->withPadding(\phpseclib3\Crypt\RSA::SIGNATURE_PSS) + ->withHash('sha256') + ->sign($message); + } + + /** + * Validates a PS256 algorithm signature. + * + * @param string|OpenSSLAsymmetricKey|OpenSSLCertificate $key + * @param string $message + * @param string $signature + * @throws DomainException Provided key is invalid + */ + private static function verifyPS256( + #[\SensitiveParameter] string|OpenSSLAsymmetricKey|OpenSSLCertificate $key, + string $message, + string $signature + ): bool { + if (!class_exists('\phpseclib3\Crypt\RSA')) { + throw new DomainException('phpseclib/phpseclib is required for PS256 support'); + } + + if ($key instanceof OpenSSLAsymmetricKey) { + $details = openssl_pkey_get_details($key); + if (!$details || !isset($details['key'])) { + throw new DomainException('OpenSSL unable to extract public key'); + } + $key = $details['key']; + } elseif ($key instanceof OpenSSLCertificate) { + if (!openssl_x509_export($key, $pem)) { + throw new DomainException('OpenSSL unable to export certificate'); + } + $key = $pem; + } + + /** @var \phpseclib3\Crypt\RSA\PublicKey $rsa */ + $rsa = \phpseclib3\Crypt\PublicKeyLoader::load($key); + + return $rsa->withPadding(\phpseclib3\Crypt\RSA::SIGNATURE_PSS) + ->withHash('sha256') + ->verify($message, $signature); + } } diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index 60b7ae4897b..40f45149c83 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -440,6 +440,60 @@ public function testRSEncodeDecode() $this->assertEquals($decoded, $expected); } + public function testPSEncodeDecode() + { + $privKey = openssl_pkey_new([ + 'digest_alg' => 'sha256', + 'private_key_bits' => 2048, + 'private_key_type' => OPENSSL_KEYTYPE_RSA + ]); + openssl_pkey_export($privKey, $privKeyPem); + $pubKeyDetails = openssl_pkey_get_details($privKey); + $pubKeyPem = $pubKeyDetails['key']; + + $payload = ['message' => 'abc']; + $msg = JWT::encode($payload, $privKeyPem, 'PS256'); + $decoded = JWT::decode($msg, new Key($pubKeyPem, 'PS256')); + + $this->assertEquals($decoded, (object) $payload); + } + + public function testPSEncodeDecodeWithOpenSSLKey() + { + $privKey = openssl_pkey_new([ + 'digest_alg' => 'sha256', + 'private_key_bits' => 2048, + 'private_key_type' => OPENSSL_KEYTYPE_RSA + ]); + $pubKey = openssl_pkey_get_public(openssl_pkey_get_details($privKey)['key']); + + $payload = ['message' => 'abc']; + $msg = JWT::encode($payload, $privKey, 'PS256'); + $decoded = JWT::decode($msg, new Key($pubKey, 'PS256')); + + $this->assertEquals($decoded, (object) $payload); + } + + public function testPSVerifyWithCertificate() + { + $privKey = openssl_pkey_new([ + 'digest_alg' => 'sha256', + 'private_key_bits' => 2048, + 'private_key_type' => OPENSSL_KEYTYPE_RSA + ]); + + $csr = openssl_csr_new(['commonName' => 'example.com'], $privKey); + $cert = openssl_csr_sign($csr, null, $privKey, 1); + + $payload = ['message' => 'abc']; + $msg = JWT::encode($payload, $privKey, 'PS256'); + + // Use certificate for verification + $decoded = JWT::decode($msg, new Key($cert, 'PS256')); + + $this->assertEquals($decoded, (object) $payload); + } + public function testEdDsaEncodeDecode() { $keyPair = sodium_crypto_sign_keypair(); From c44c33d23989361badfb0b27c1d98329fa30ea3c Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Thu, 11 Jun 2026 10:49:29 -0700 Subject: [PATCH 276/282] chore: add back release-please (firebase/php-jwt#640) --- Jwt/.github/release-please.yml | 3 +++ Jwt/.github/workflows/release-please.yml | 20 -------------------- 2 files changed, 3 insertions(+), 20 deletions(-) create mode 100644 Jwt/.github/release-please.yml delete mode 100644 Jwt/.github/workflows/release-please.yml diff --git a/Jwt/.github/release-please.yml b/Jwt/.github/release-please.yml new file mode 100644 index 00000000000..461b00a3ccf --- /dev/null +++ b/Jwt/.github/release-please.yml @@ -0,0 +1,3 @@ +releaseType: simple +handleGHRelease: true +primaryBranch: main \ No newline at end of file diff --git a/Jwt/.github/workflows/release-please.yml b/Jwt/.github/workflows/release-please.yml deleted file mode 100644 index f4ff4272389..00000000000 --- a/Jwt/.github/workflows/release-please.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: release-please -on: - push: - branches: - - main -permissions: - # Needed for Release Please to create and update files - contents: write - # Needed for Release Please to create Release PRs - pull-requests: write -jobs: - release-please: - environment: Release - runs-on: ubuntu-latest - steps: - - uses: googleapis/release-please-action@v4 - id: release - with: - token: ${{ secrets.YOSHI_CODE_BOT_TOKEN }} - release-type: simple From 01f557767cca41ef1d03eb5d487835979a6063cd Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Thu, 11 Jun 2026 10:54:14 -0700 Subject: [PATCH 277/282] chore(main): release 7.1.0 (firebase/php-jwt#641) --- Jwt/CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Jwt/CHANGELOG.md b/Jwt/CHANGELOG.md index 498fb0007a0..17d7a630bfc 100644 --- a/Jwt/CHANGELOG.md +++ b/Jwt/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## [7.1.0](https://github.com/googleapis/php-jwt/compare/v7.0.5...v7.1.0) (2026-06-11) + + +### Features + +* Add support for PS256 ([#637](https://github.com/googleapis/php-jwt/issues/637)) ([aa0b447](https://github.com/googleapis/php-jwt/commit/aa0b4472c137255545b177a1840891304eac55f1)) + + +### Bug Fixes + +* Skip parsing valid unsupported curves ([#638](https://github.com/googleapis/php-jwt/issues/638)) ([8a6c2a7](https://github.com/googleapis/php-jwt/commit/8a6c2a75d7f45e781c93d618939f8a045fe69f53)) +* Update homepage URL in composer.json ([#635](https://github.com/googleapis/php-jwt/issues/635)) ([f7ae571](https://github.com/googleapis/php-jwt/commit/f7ae571efe255aed89c8f8d0f2cb7f71c5835664)) +* Validate numeric type of iat, nbf and exp claims in encode ([#634](https://github.com/googleapis/php-jwt/issues/634)) ([958e422](https://github.com/googleapis/php-jwt/commit/958e422961e874843a7fcc339e55a0101a66201d)) + ## [7.0.5](https://github.com/firebase/php-jwt/compare/v7.0.4...v7.0.5) (2026-03-31) From a7df24dddfaaf44307692bc533d6c089d56ce8a0 Mon Sep 17 00:00:00 2001 From: scybulski Date: Thu, 23 Jul 2026 22:45:05 +0200 Subject: [PATCH 278/282] chore: allow guzzlehttp/guzzle 8.0 (firebase/php-jwt#643) --- Jwt/composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jwt/composer.json b/Jwt/composer.json index 50d0ff91c36..84360a96f82 100644 --- a/Jwt/composer.json +++ b/Jwt/composer.json @@ -33,7 +33,7 @@ } }, "require-dev": { - "guzzlehttp/guzzle": "^7.4", + "guzzlehttp/guzzle": "^7.4||^8.0", "phpspec/prophecy-phpunit": "^2.0", "phpunit/phpunit": "^9.5", "psr/cache": "^2.0||^3.0", From 4e0167d7ddae3c0950561916608557cb7d504d66 Mon Sep 17 00:00:00 2001 From: Jeroen Date: Wed, 2 Sep 2026 20:12:25 +0200 Subject: [PATCH 279/282] fix: limit the number of segments a JWT can be exploded into (firebase/php-jwt#639) --- Jwt/src/JWT.php | 2 +- Jwt/tests/JWTTest.php | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index 1c71ab3d082..f03972f78ca 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -107,7 +107,7 @@ public static function decode( if (empty($keyOrKeyArray)) { throw new InvalidArgumentException('Key may not be empty'); } - $tks = \explode('.', $jwt); + $tks = \explode('.', $jwt, 4); if (\count($tks) !== 3) { throw new UnexpectedValueException('Wrong number of segments'); } diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index 40f45149c83..d7fe5309c84 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -323,6 +323,21 @@ public function testInvalidToken() JWT::decode($encoded, $decodeKey); } + public function testInvalidTokenSegments() + { + $dummyToken = 'dGhlIHZhbHVlIGRvZXNuJ3QgbWF0dGVy.T25seSB0aGUgbnVtYmVyIG9mIHNlZ21lbnRz.VGhpcyBzaG91bGQgYmUgYSBzaWduYXR1cmU.YnV0IHRoZXJlIGlzIG1vcmU'; + $this->expectException(UnexpectedValueException::class); + JWT::decode($dummyToken, $this->hmacKey); + } + + public function testInvalidTokenManySegments() + { + $dummyToken = 'dGhlIHZhbHVlIGRvZXNuJ3QgbWF0dGVy.T25seSB0aGUgbnVtYmVyIG9mIHNlZ21lbnRz.VGhpcyBzaG91bGQgYmUgYSBzaWduYXR1cmU'; + $dummyToken .= str_repeat('.KzE', 999999); + $this->expectException(UnexpectedValueException::class); + JWT::decode($dummyToken, $this->hmacKey); + } + public function testNullKeyFails() { $payload = [ From e1adc3b30fd2a4b3a6572f5a9170cba504b8cb85 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Wed, 2 Sep 2026 18:12:47 +0000 Subject: [PATCH 280/282] chore(main): release 7.1.1 --- Jwt/CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Jwt/CHANGELOG.md b/Jwt/CHANGELOG.md index 17d7a630bfc..8597266f5d7 100644 --- a/Jwt/CHANGELOG.md +++ b/Jwt/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [7.1.1](https://github.com/googleapis/php-jwt/compare/v7.1.0...v7.1.1) (2026-09-02) + + +### Bug Fixes + +* Limit the number of segments a JWT can be exploded into ([#639](https://github.com/googleapis/php-jwt/issues/639)) ([93d248c](https://github.com/googleapis/php-jwt/commit/93d248c650aea615d74a5ffa20c91c7a818e9b8d)) + ## [7.1.0](https://github.com/googleapis/php-jwt/compare/v7.0.5...v7.1.0) (2026-06-11) From 5e266f89cc758a026a7fff18a0a5d51ffa3fd72d Mon Sep 17 00:00:00 2001 From: hectorhammett Date: Mon, 14 Sep 2026 21:55:39 +0000 Subject: [PATCH 281/282] chore: prepare the JWT library for migration to the monorepo --- Jwt/.github/pull_request_template.md | 24 +++ Jwt/.github/release-please.yml | 3 - Jwt/.github/workflows/tests.yml | 57 ------ Jwt/composer.json | 10 +- Jwt/src/JWK.php | 3 +- Jwt/src/JWT.php | 6 +- Jwt/src/Key.php | 3 +- Jwt/tests/CachedKeySetTest.php | 286 +++++++++++++-------------- Jwt/tests/JWKTest.php | 37 ++-- Jwt/tests/JWTTest.php | 20 +- 10 files changed, 219 insertions(+), 230 deletions(-) create mode 100644 Jwt/.github/pull_request_template.md delete mode 100644 Jwt/.github/release-please.yml delete mode 100644 Jwt/.github/workflows/tests.yml diff --git a/Jwt/.github/pull_request_template.md b/Jwt/.github/pull_request_template.md new file mode 100644 index 00000000000..a3a7331405f --- /dev/null +++ b/Jwt/.github/pull_request_template.md @@ -0,0 +1,24 @@ +**PLEASE READ THIS ENTIRE MESSAGE** + +Hello, and thank you for your contribution! Please note that this repository is +a read-only split of `googleapis/google-cloud-php`. As such, we are +unable to accept pull requests to this repository. + +We welcome your pull request and would be happy to consider it for inclusion in +our library if you follow these steps: + +* Clone the parent client library repository: + +```sh +$ git clone git@github.com:googleapis/google-cloud-php.git +``` + +* Move your changes into the correct location in that library. Library code +belongs in `Grafeas/src`, and tests in `Grafeas/tests`. + +* Push the changes in a new branch to a fork, and open a new pull request +[here](https://github.com/googleapis/google-cloud-php). + +Thanks again, and we look forward to seeing your proposed change! + +The Google Cloud PHP team diff --git a/Jwt/.github/release-please.yml b/Jwt/.github/release-please.yml deleted file mode 100644 index 461b00a3ccf..00000000000 --- a/Jwt/.github/release-please.yml +++ /dev/null @@ -1,3 +0,0 @@ -releaseType: simple -handleGHRelease: true -primaryBranch: main \ No newline at end of file diff --git a/Jwt/.github/workflows/tests.yml b/Jwt/.github/workflows/tests.yml deleted file mode 100644 index 2d6d57bc89a..00000000000 --- a/Jwt/.github/workflows/tests.yml +++ /dev/null @@ -1,57 +0,0 @@ -name: Test Suite -on: - push: - branches: - - main - pull_request: - -jobs: - test: - runs-on: ubuntu-latest - strategy: - matrix: - php: [ "8.0", "8.1", "8.2", "8.3", "8.4", "8.5" ] - name: PHP ${{matrix.php }} Unit Test - steps: - - uses: actions/checkout@v2 - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - - name: Install Dependencies - uses: nick-invision/retry@v1 - with: - timeout_minutes: 10 - max_attempts: 3 - command: composer install - - name: Run Script - run: vendor/bin/phpunit - - style: - runs-on: ubuntu-latest - name: PHP Style Check - steps: - - uses: actions/checkout@v2 - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: "8.3" - - name: Run Script - run: | - composer global require friendsofphp/php-cs-fixer - ~/.composer/vendor/bin/php-cs-fixer fix --diff --dry-run --allow-risky=yes . - - staticanalysis: - runs-on: ubuntu-latest - name: PHPStan Static Analysis - steps: - - uses: actions/checkout@v2 - - name: Install PHP - uses: shivammathur/setup-php@v2 - with: - php-version: '8.3' - - name: Run Script - run: | - composer install - composer global require phpstan/phpstan:~1.10.0 - ~/.composer/vendor/bin/phpstan analyse diff --git a/Jwt/composer.json b/Jwt/composer.json index 84360a96f82..9333b685a91 100644 --- a/Jwt/composer.json +++ b/Jwt/composer.json @@ -34,12 +34,20 @@ }, "require-dev": { "guzzlehttp/guzzle": "^7.4||^8.0", - "phpspec/prophecy-phpunit": "^2.0", + "phpspec/prophecy-phpunit": "^2.2", "phpunit/phpunit": "^9.5", "psr/cache": "^2.0||^3.0", "psr/http-client": "^1.0", "psr/http-factory": "^1.0", "phpfastcache/phpfastcache": "^9.2", "phpseclib/phpseclib": "~3.0" + }, + "extra": { + "component": { + "id": "jwt", + "target": "googleapis/php-jwt.git", + "path": "Jwt", + "entry": "README.md" + } } } diff --git a/Jwt/src/JWK.php b/Jwt/src/JWK.php index 7a1f44883e0..1870c6fe0f5 100644 --- a/Jwt/src/JWK.php +++ b/Jwt/src/JWK.php @@ -293,7 +293,8 @@ private static function createPemFromModulusAndExponent( * DER-encode the length * * DER supports lengths up to (2**8)**127, however, we'll only support lengths up to (2**8)**4. See - * {@link http://itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#p=13 X.690 paragraph 8.1.3} for more information. + * {@link http://itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#p=13 X.690 paragraph 8.1.3} + * for more information. * * @param int $length * @return string diff --git a/Jwt/src/JWT.php b/Jwt/src/JWT.php index f03972f78ca..f9a21e2079b 100644 --- a/Jwt/src/JWT.php +++ b/Jwt/src/JWT.php @@ -90,7 +90,8 @@ class JWT * @throws UnexpectedValueException Provided JWT was invalid * @throws SignatureInvalidException Provided JWT was invalid because the signature verification failed * @throws BeforeValidException Provided JWT is trying to be used before it's eligible as defined by 'nbf' - * @throws BeforeValidException Provided JWT is trying to be used before it's been created as defined by 'iat' + * @throws BeforeValidException Provided JWT is trying to be used before it's + * been created as defined by 'iat' * @throws ExpiredException Provided JWT has since expired, as defined by the 'exp' claim * * @uses jsonDecode @@ -313,7 +314,8 @@ public static function sign( * * @param string $msg The original message (header and body) * @param string $signature The original signature - * @param string|OpenSSLAsymmetricKey|OpenSSLCertificate $keyMaterial For Ed*, ES*, HS*, a string key works. for RS*, must be an instance of OpenSSLAsymmetricKey + * @param string|OpenSSLAsymmetricKey|OpenSSLCertificate $keyMaterial For Ed*, ES*, HS*, a string key works. + * for RS*, must be an instance of OpenSSLAsymmetricKey * @param string $alg The algorithm * * @return bool diff --git a/Jwt/src/Key.php b/Jwt/src/Key.php index 694d3b13b99..39d7f432e8c 100644 --- a/Jwt/src/Key.php +++ b/Jwt/src/Key.php @@ -17,8 +17,7 @@ public function __construct( #[\SensitiveParameter] private $keyMaterial, private string $algorithm ) { - if ( - !\is_string($keyMaterial) + if (!\is_string($keyMaterial) && !$keyMaterial instanceof OpenSSLAsymmetricKey && !$keyMaterial instanceof OpenSSLCertificate ) { diff --git a/Jwt/tests/CachedKeySetTest.php b/Jwt/tests/CachedKeySetTest.php index 39bbc919d63..3488cf7cf19 100644 --- a/Jwt/tests/CachedKeySetTest.php +++ b/Jwt/tests/CachedKeySetTest.php @@ -345,7 +345,7 @@ public function testRateLimit() $this->testJwksUri, $this->getMockHttpClient($this->testJwks1, $shouldBeCalledTimes), $this->getMockHttpFactory($shouldBeCalledTimes), - new TestMemoryCacheItemPool(), + $this->getTestMemoryCacheItemPool(), 10, // expires after seconds true // enable rate limiting ); @@ -367,7 +367,7 @@ public function testRateLimitWithExpiresAfter() $totalHttpTimes = $shouldBeCalledTimes + $afterExpirationTimes; - $cachePool = new TestMemoryCacheItemPool(); + $cachePool = $this->getTestMemoryCacheItemPool(); // Instantiate the cached key set $cachedKeySet = new CachedKeySet( @@ -415,7 +415,7 @@ public function testFullIntegration(string $jwkUri): void self::markTestSkipped('Guzzle 7 only'); } // Create cache and http objects - $cache = new TestMemoryCacheItemPool(); + $cache = $this->getTestMemoryCacheItemPool(); $http = new \GuzzleHttp\Client(); $factory = new \GuzzleHttp\Psr7\HttpFactory(); @@ -502,148 +502,148 @@ private function getMockEmptyCache() return $cache->reveal(); } -} - -/** - * A cache item pool - */ -final class TestMemoryCacheItemPool implements CacheItemPoolInterface -{ - private $items; - private $deferredItems; - - public function getItem($key): CacheItemInterface - { - $item = current($this->getItems([$key])); - $item->expiresAt(null); // mimic symfony cache behavior - - return $item; - } - - public function getItems(array $keys = []): iterable - { - $items = []; - - foreach ($keys as $key) { - $items[$key] = $this->hasItem($key) ? clone $this->items[$key] : new TestMemoryCacheItem($key); - } - - return $items; - } - - public function hasItem($key): bool - { - return isset($this->items[$key]) && $this->items[$key]->isHit(); - } - - public function clear(): bool - { - $this->items = []; - $this->deferredItems = []; - - return true; - } - - public function deleteItem($key): bool - { - return $this->deleteItems([$key]); - } - - public function deleteItems(array $keys): bool - { - foreach ($keys as $key) { - unset($this->items[$key]); - } - - return true; - } - - public function save(CacheItemInterface $item): bool - { - $this->items[$item->getKey()] = $item; - - return true; - } - - public function saveDeferred(CacheItemInterface $item): bool - { - $this->deferredItems[$item->getKey()] = $item; - - return true; - } - - public function commit(): bool - { - foreach ($this->deferredItems as $item) { - $this->save($item); - } - - $this->deferredItems = []; - - return true; - } -} - -/** - * A cache item. - */ -final class TestMemoryCacheItem implements CacheItemInterface -{ - private $key; - private $value; - private $expiration; - private $isHit = false; - public function __construct(string $key) + private function getTestMemoryCacheItemPool() { - $this->key = $key; - } - - public function getKey(): string - { - return $this->key; - } + return new class() implements CacheItemPoolInterface { + private $items; + private $deferredItems; + + public function getItem($key): CacheItemInterface + { + $item = current($this->getItems([$key])); + $item->expiresAt(null); // mimic symfony cache behavior + + return $item; + } + + public function getItems(array $keys = []): iterable + { + $items = []; + + foreach ($keys as $key) { + $items[$key] = $this->hasItem($key) ? + clone $this->items[$key] : + $this->getTestMemoryCacheItem($key); + } - public function get(): mixed - { - return $this->isHit() ? $this->value : null; - } - - public function isHit(): bool - { - if (!$this->isHit) { - return false; - } - - if ($this->expiration === null) { - return true; - } - - return $this->currentTime()->getTimestamp() < $this->expiration->getTimestamp(); - } - - public function set(mixed $value): static - { - $this->isHit = true; - $this->value = $value; - - return $this; - } - - public function expiresAt($expiration): static - { - $this->expiration = $expiration; - return $this; - } - - public function expiresAfter($time): static - { - $this->expiration = $this->currentTime()->add(new \DateInterval("PT{$time}S")); - return $this; - } - - protected function currentTime() - { - return new \DateTime('now', new \DateTimeZone('UTC')); + return $items; + } + + public function hasItem($key): bool + { + return isset($this->items[$key]) && $this->items[$key]->isHit(); + } + + public function clear(): bool + { + $this->items = []; + $this->deferredItems = []; + + return true; + } + + public function deleteItem($key): bool + { + return $this->deleteItems([$key]); + } + + public function deleteItems(array $keys): bool + { + foreach ($keys as $key) { + unset($this->items[$key]); + } + + return true; + } + + public function save(CacheItemInterface $item): bool + { + $this->items[$item->getKey()] = $item; + + return true; + } + + public function saveDeferred(CacheItemInterface $item): bool + { + $this->deferredItems[$item->getKey()] = $item; + + return true; + } + + public function commit(): bool + { + foreach ($this->deferredItems as $item) { + $this->save($item); + } + + $this->deferredItems = []; + + return true; + } + + private function getTestMemoryCacheItem(string $key) + { + return new class($key) implements CacheItemInterface{ + private $key; + private $value; + private $expiration; + private $isHit = false; + + public function __construct(string $key) + { + $this->key = $key; + } + + public function getKey(): string + { + return $this->key; + } + + public function get(): mixed + { + return $this->isHit() ? $this->value : null; + } + + public function isHit(): bool + { + if (!$this->isHit) { + return false; + } + + if ($this->expiration === null) { + return true; + } + + return $this->currentTime()->getTimestamp() < $this->expiration->getTimestamp(); + } + + public function set(mixed $value): static + { + $this->isHit = true; + $this->value = $value; + + return $this; + } + + public function expiresAt($expiration): static + { + $this->expiration = $expiration; + return $this; + } + + public function expiresAfter($time): static + { + $this->expiration = $this->currentTime()->add(new \DateInterval("PT{$time}S")); + return $this; + } + + protected function currentTime() + { + return new \DateTime('now', new \DateTimeZone('UTC')); + } + }; + } + }; } } diff --git a/Jwt/tests/JWKTest.php b/Jwt/tests/JWKTest.php index 3d3caf9a62c..c223745ba6e 100644 --- a/Jwt/tests/JWKTest.php +++ b/Jwt/tests/JWKTest.php @@ -108,7 +108,7 @@ public function testParseJwkKeySet($jwkFile, $keyId, $pubkeyFile) $publicKey = openssl_pkey_get_details($keyMaterial)['key']; $this->assertEquals( - file_get_contents(__DIR__ . '/data/' . $pubkeyFile), + str_replace("\r\n", "\n", file_get_contents(__DIR__ . '/data/' . $pubkeyFile)), $publicKey ); } @@ -121,7 +121,7 @@ public function provideParseJwkKeySet() ]; } - public function testParseJwkKey_empty() + public function testParseJwkKeyEmpty() { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('JWK must not be empty'); @@ -129,7 +129,7 @@ public function testParseJwkKey_empty() JWK::parseKeySet(['keys' => [[]]]); } - public function testParseJwkKeySet_empty() + public function testParseJwkKeySetEmpty() { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('JWK Set did not contain any keys'); @@ -258,7 +258,10 @@ public function testParseKey() $jwk = [ 'alg' => 'RS256', 'kty' => 'RSA', - 'n' => 'hsYvCPtkUV7SIxwkOkJsJfhwV_CMdXU5i0UmY2QEs-Pa7v0-0y-s4EjEDtsQ8Yow6hc670JhkGBcMzhU4DtrqNGROXebyOse5FX0m0UvWo1qXqNTf28uBKB990mY42Icr8sGjtOw8ajyT9kufbmXi3eZKagKpG0TDGK90oBEfoGzCxoFT87F95liNth_GoyU5S8-G3OqIqLlQCwxkI5s-g2qvg_aooALfh1rhvx2wt4EJVMSrdnxtPQSPAtZBiw5SwCnVglc6OnalVNvAB2JArbqC9GAzzz9pApAk28SYg5a4hPiPyqwRv-4X1CXEK8bO5VesIeRX0oDf7UoM-pVAw', + 'n' => 'hsYvCPtkUV7SIxwkOkJsJfhwV_CMdXU5i0UmY2QEs-Pa7v0-0y-s4EjEDtsQ8Yow6hc670JhkGBcMzhU4DtrqNGROXebyO' . + 'se5FX0m0UvWo1qXqNTf28uBKB990mY42Icr8sGjtOw8ajyT9kufbmXi3eZKagKpG0TDGK90oBEfoGzCxoFT87F95liNth_Goy' . + 'U5S8-G3OqIqLlQCwxkI5s-g2qvg_aooALfh1rhvx2wt4EJVMSrdnxtPQSPAtZBiw5SwCnVglc6OnalVNvAB2JArbqC9GAzzz9' . + 'pApAk28SYg5a4hPiPyqwRv-4X1CXEK8bO5VesIeRX0oDf7UoM-pVAw', 'use' => 'sig', 'e' => 'AQAB', 'kid' => '838c06c62046c2d948affe137dd5310129f4d5d1' @@ -272,18 +275,18 @@ public function testParseKey() $keyData = openssl_pkey_get_details($pubKey); $expectedPublicKey = <<assertEquals($expectedPublicKey, $keyData['key']); + -----BEGIN PUBLIC KEY----- + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAhsYvCPtkUV7SIxwkOkJs + JfhwV/CMdXU5i0UmY2QEs+Pa7v0+0y+s4EjEDtsQ8Yow6hc670JhkGBcMzhU4Dtr + qNGROXebyOse5FX0m0UvWo1qXqNTf28uBKB990mY42Icr8sGjtOw8ajyT9kufbmX + i3eZKagKpG0TDGK90oBEfoGzCxoFT87F95liNth/GoyU5S8+G3OqIqLlQCwxkI5s + +g2qvg/aooALfh1rhvx2wt4EJVMSrdnxtPQSPAtZBiw5SwCnVglc6OnalVNvAB2J + ArbqC9GAzzz9pApAk28SYg5a4hPiPyqwRv+4X1CXEK8bO5VesIeRX0oDf7UoM+pV + AwIDAQAB + -----END PUBLIC KEY----- + + EOF; + + $this->assertEquals(str_replace("\r\n", "\n", $expectedPublicKey), $keyData['key']); } } diff --git a/Jwt/tests/JWTTest.php b/Jwt/tests/JWTTest.php index d7fe5309c84..58c33bf7786 100644 --- a/Jwt/tests/JWTTest.php +++ b/Jwt/tests/JWTTest.php @@ -325,14 +325,19 @@ public function testInvalidToken() public function testInvalidTokenSegments() { - $dummyToken = 'dGhlIHZhbHVlIGRvZXNuJ3QgbWF0dGVy.T25seSB0aGUgbnVtYmVyIG9mIHNlZ21lbnRz.VGhpcyBzaG91bGQgYmUgYSBzaWduYXR1cmU.YnV0IHRoZXJlIGlzIG1vcmU'; + $dummyToken = 'dGhlIHZhbHVlIGRvZXNuJ3QgbWF0dGVy' . + '.T25seSB0aGUgbnVtYmVyIG9mIHNlZ21lbnRz.' . + 'VGhpcyBzaG91bGQgYmUgYSBzaWduYXR1cmU.' . + 'YnV0IHRoZXJlIGlzIG1vcmU'; $this->expectException(UnexpectedValueException::class); JWT::decode($dummyToken, $this->hmacKey); } public function testInvalidTokenManySegments() { - $dummyToken = 'dGhlIHZhbHVlIGRvZXNuJ3QgbWF0dGVy.T25seSB0aGUgbnVtYmVyIG9mIHNlZ21lbnRz.VGhpcyBzaG91bGQgYmUgYSBzaWduYXR1cmU'; + $dummyToken = 'dGhlIHZhbHVlIGRvZXNuJ3QgbWF0dGVy.' . + 'T25seSB0aGUgbnVtYmVyIG9mIHNlZ21lbnRz.' . + 'VGhpcyBzaG91bGQgYmUgYSBzaWduYXR1cmU'; $dummyToken .= str_repeat('.KzE', 999999); $this->expectException(UnexpectedValueException::class); JWT::decode($dummyToken, $this->hmacKey); @@ -412,7 +417,13 @@ public function testEmptyAlgorithm() public function testAdditionalHeaders() { - $msg = JWT::encode(['message' => 'abc'], $this->hmacKey->getKeyMaterial(), 'HS256', null, ['cty' => 'test-eit;v=1']); + $msg = JWT::encode( + ['message' => 'abc'], + $this->hmacKey->getKeyMaterial(), + 'HS256', + null, + ['cty' => 'test-eit;v=1'] + ); $expected = new stdClass(); $expected->message = 'abc'; $this->assertEquals(JWT::decode($msg, $this->hmacKey), $expected); @@ -426,7 +437,8 @@ public function testInvalidSegmentCount() public function testInvalidSignatureEncoding() { - $msg = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwibmFtZSI6ImZvbyJ9.Q4Kee9E8o0Xfo4ADXvYA8t7dN_X_bU9K5w6tXuiSjlUxx'; + $msg = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwibmFtZSI6ImZvbyJ9.' . + 'Q4Kee9E8o0Xfo4ADXvYA8t7dN_X_bU9K5w6tXuiSjlUxx'; $this->expectException(UnexpectedValueException::class); JWT::decode($msg, $this->hmacKey); } From 5348bad806fa59ef570c89746cd584e38bb429d7 Mon Sep 17 00:00:00 2001 From: hectorhammett Date: Mon, 14 Sep 2026 21:33:16 +0000 Subject: [PATCH 282/282] chore: prepare for the JWT library migration --- .github/run-package-tests.sh | 32 ++++++++++++++------------- .github/workflows/release-checks.yaml | 3 ++- .repo-metadata-full.json | 8 +++++++ composer.json | 8 +++++-- dev/src/Component.php | 3 ++- phpstan.neon.dist | 2 ++ phpunit.xml.dist | 1 + 7 files changed, 38 insertions(+), 19 deletions(-) diff --git a/.github/run-package-tests.sh b/.github/run-package-tests.sh index e868e242d9e..93c9388da58 100644 --- a/.github/run-package-tests.sh +++ b/.github/run-package-tests.sh @@ -70,21 +70,22 @@ run_package_test() { # Update composer to use local packages local PACKAGE_DEPENDENCIES=( - "Auth,auth" - "Gax,gax" - "CommonProtos,common-protos,4.100" - "BigQuery,cloud-bigquery" - "Core,cloud-core" - "Logging,cloud-logging" - "PubSub,cloud-pubsub" - "Storage,cloud-storage,2.100" - "ShoppingCommonProtos,shopping-common-protos" - "GeoCommonProtos,geo-common-protos,0.1" - "Monitoring,cloud-monitoring" + "Auth,google/auth" + "Gax,google/gax" + "CommonProtos,google/common-protos,4.100" + "BigQuery,google/cloud-bigquery" + "Core,google/cloud-core" + "Jwt,firebase/php-jwt,6.10.0" + "Logging,google/cloud-logging" + "PubSub,google/cloud-pubsub" + "Storage,google/cloud-storage,2.100" + "ShoppingCommonProtos,google/shopping-common-protos" + "GeoCommonProtos,google/geo-common-protos,0.1" + "Monitoring,google/cloud-monitoring" ) for i in "${PACKAGE_DEPENDENCIES[@]}"; do IFS="," read -r PKG_DIR PKG_NAME PKG_VERSION <<< "$i" - if grep -q "\"google/${PKG_NAME}\":" "${DIR}/composer.json"; then + if grep -q "\"${PKG_NAME}\":" "${DIR}/composer.json"; then # determine local package version local VERSION if [ "${STRICT}" = "true" ]; then @@ -94,11 +95,12 @@ run_package_test() { else VERSION=${PKG_VERSION} fi - echo "Use local package ${PKG_DIR} as google/${PKG_NAME}:${VERSION} in ${DIR}" + echo "Use local package ${PKG_DIR} as ${PKG_NAME}:${VERSION} in ${DIR}" # "canonical: false" ensures composer will try to install from packagist when the "--prefer-lowest" flag is set. local JSON_CONFIG - JSON_CONFIG=$(printf '{"type":"path","url":"../%s","options":{"versions":{"google/%s":"%s"}},"canonical":false}' "${PKG_DIR}" "${PKG_NAME}" "${VERSION}") - composer config "repositories.${PKG_NAME}" -d "${DIR}" "${JSON_CONFIG}" + JSON_CONFIG=$(printf '{"type":"path","url":"../%s","options":{"versions":{"%s":"%s"}},"canonical":false}' "${PKG_DIR}" "${PKG_NAME}" "${VERSION}") + local REPO_NAME="${PKG_NAME#*/}" + composer config "repositories.${REPO_NAME}" -d "${DIR}" "${JSON_CONFIG}" fi done diff --git a/.github/workflows/release-checks.yaml b/.github/workflows/release-checks.yaml index 1ef914f1ccb..0694104a387 100644 --- a/.github/workflows/release-checks.yaml +++ b/.github/workflows/release-checks.yaml @@ -146,4 +146,5 @@ jobs: -t $GH_TOKEN \ -p $PG_TOKEN \ --skip Gax:repo \ - --skip Auth:repo # Skip repo check for Gax and Auth because issues are enabled + --skip Auth:repo \ + --skip Jwt:repo # Skip repo check for Gax, Auth, and Jwt because issues are enabled on those repos diff --git a/.repo-metadata-full.json b/.repo-metadata-full.json index 6ef628748c1..c3e93ad4b7e 100644 --- a/.repo-metadata-full.json +++ b/.repo-metadata-full.json @@ -968,6 +968,14 @@ "library_type": "GAPIC_AUTO", "api_shortname": "ids" }, + "Jwt": { + "language": "php", + "distribution_name": "firebase/php-jwt", + "release_level": "stable", + "client_documentation": "https://github.com/firebase/php-jwt", + "library_type": "CORE", + "api_shortname": "" + }, "Kms": { "language": "php", "distribution_name": "google/cloud-kms", diff --git a/composer.json b/composer.json index 776dade0e15..4ce9f4df48f 100644 --- a/composer.json +++ b/composer.json @@ -67,7 +67,6 @@ "psr/cache": "^2.0||^3.0", "psr/log": "^2.0||^3.0", "grpc/grpc": "^1.13", - "firebase/php-jwt": "^6.0||^7.0", "google/common-protos": "^4.4", "google/protobuf": "^4.31||^5.34", "google/grpc-gcp": "^0.4", @@ -89,9 +88,12 @@ "dms/phpunit-arraysubset-asserts": "^0.5.0", "symfony/process": "^6.4", "kelvinmo/simplejwt": "^1.1.0", - "webmozart/assert": "^1.11.0" + "webmozart/assert": "^1.11.0", + "phpfastcache/phpfastcache": "^9.2", + "psr/http-factory": "^1.0" }, "replace": { + "firebase/php-jwt": "7.1.1", "google/access-context-manager": "1.2.2", "google/analytics-admin": "0.34.2", "google/analytics-data": "0.26.1", @@ -334,6 +336,7 @@ }, "autoload": { "psr-4": { + "Firebase\\JWT\\": "Jwt/src", "GPBMetadata\\Google\\Ads\\Admanager\\": "AdsAdManager/metadata", "GPBMetadata\\Google\\Ads\\Datamanager\\": "AdsDataManager/metadata", "GPBMetadata\\Google\\Analytics\\Admin\\": "AnalyticsAdmin/metadata", @@ -835,6 +838,7 @@ }, "autoload-dev": { "psr-4": { + "Firebase\\JWT\\": "Jwt/tests", "Google\\ApiCore\\Tests\\": "Gax/tests", "Google\\ApiCore\\Testing\\": "Gax/tests/Unit/testdata/generated", "Google\\Analytics\\Admin\\Tests\\": "AnalyticsAdmin/tests", diff --git a/dev/src/Component.php b/dev/src/Component.php index 1ab18b82d63..d438c797ce2 100644 --- a/dev/src/Component.php +++ b/dev/src/Component.php @@ -259,7 +259,8 @@ private function validateComponentFiles(): void $this->componentDependencies = []; - if ($this->name !== 'Auth') { + // Skip if Auth to avoid recursion, skip if Jwt because Jwt does not rely on Auth + if ($this->name !== 'Auth' && $this->name !== 'Jwt') { $this->componentDependencies[] = new Component('Auth'); } diff --git a/phpstan.neon.dist b/phpstan.neon.dist index f4957603895..f5d27a985d6 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -14,6 +14,8 @@ parameters: - Gax # ignore Auth because we implement a stricter phpstan.neon.dist there - Auth + # ignore Jwt because we implement a stricter phpstan.neon.dist there + - Jwt ignoreErrors: # Protobuf constant classes sometimes contain multiple values for one array key - identifier: array.duplicateKey diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 4f3709d5379..4586c5a3778 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -24,6 +24,7 @@ */tests/Unit Auth/tests + Jwt/tests dev/tests/Unit Core