From 3aa3d978973f080746b2f2f75b10d3f38cbb0557 Mon Sep 17 00:00:00 2001 From: Petr Kotek Date: Thu, 1 Sep 2016 01:16:29 +0200 Subject: [PATCH 01/62] Travis: run tests also under PHP 5.6 and PHP 7.0 (#108) --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index b1135a16..96a0f19c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,8 @@ php: - 5.3 - 5.4 - 5.5 + - 5.6 + - 7.0 - hhvm sudo: false From 491ab3a80566cd1427549994ee7a07d9154311c4 Mon Sep 17 00:00:00 2001 From: Tim Stamp Date: Mon, 12 Dec 2016 12:14:10 +0000 Subject: [PATCH 02/62] 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. --- src/JWT.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/JWT.php b/src/JWT.php index 6d30e941..8170dba2 100644 --- a/src/JWT.php +++ b/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 64d7eb06134058728fae1c8027c8ebe4e61bdfc6 Mon Sep 17 00:00:00 2001 From: Tim Stamp Date: Tue, 24 Jan 2017 10:56:53 +0000 Subject: [PATCH 03/62] cosmetic updates --- src/JWT.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/JWT.php b/src/JWT.php index 8170dba2..d45052cf 100644 --- a/src/JWT.php +++ b/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 7f72b48d1fc07525b277ee83533b5e2305e3de14 Mon Sep 17 00:00:00 2001 From: Joost Faassen Date: Mon, 19 Jun 2017 18:29:59 +0200 Subject: [PATCH 04/62] Support RS384 and RS512 (#117) --- src/JWT.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/JWT.php b/src/JWT.php index 6d30e941..ea3cc7d1 100644 --- a/src/JWT.php +++ b/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 407a78d1de53feb44050e315562e988336d8d284 Mon Sep 17 00:00:00 2001 From: Arjan Keeman Date: Mon, 19 Jun 2017 18:34:20 +0200 Subject: [PATCH 05/62] draft-ietf-oauth-json-web-token-06 --> rfc7519 (#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. --- src/JWT.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/JWT.php b/src/JWT.php index ea3cc7d1..607710d1 100644 --- a/src/JWT.php +++ b/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 d6b3174112411dd2c3b1642e00987ecefdb4f1bb Mon Sep 17 00:00:00 2001 From: Henry N Date: Mon, 19 Jun 2017 18:40:24 +0200 Subject: [PATCH 06/62] example for RS256 openssl (#125) --- README.md | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/README.md b/README.md index d4589b1c..5c3261a2 100644 --- a/README.md +++ b/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 0f8f85aa4396de6a18791a561e2a626fb782399a Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 21 Jun 2017 11:26:30 -0700 Subject: [PATCH 07/62] Fixes travis, adds PHP 7.1, and removes HHVM (#160) --- .gitignore | 1 + .travis.yml | 9 +++------ composer.json | 5 ++++- composer.lock | 19 ------------------- 4 files changed, 8 insertions(+), 26 deletions(-) delete mode 100644 composer.lock diff --git a/.gitignore b/.gitignore index 7c29c87b..080f19aa 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ vendor phpunit.phar phpunit.phar.asc composer.phar +composer.lock diff --git a/.travis.yml b/.travis.yml index 96a0f19c..89131cc1 100644 --- a/.travis.yml +++ b/.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/composer.json b/composer.json index 1a5e93b5..4244f133 100644 --- a/composer.json +++ b/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/composer.lock b/composer.lock deleted file mode 100644 index 5518ae41..00000000 --- a/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 aa6419a5e92869c0c463361848788ec4f1e8728e Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 21 Jun 2017 11:38:45 -0700 Subject: [PATCH 08/62] Updates JWT::verify to handle openssl errors (#159) --- src/JWT.php | 14 +++++++++----- tests/JWTTest.php | 29 +++++++++++++++++++++++++++-- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/src/JWT.php b/src/JWT.php index 2e5758ea..7729143e 100644 --- a/src/JWT.php +++ b/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/tests/JWTTest.php b/tests/JWTTest.php index e99ea03a..99ae9c38 100644 --- a/tests/JWTTest.php +++ b/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 d2c50fdaedaa64f91229c30fa4190b0113847b45 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 21 Jun 2017 13:50:08 -0700 Subject: [PATCH 09/62] removes minimum-stabity from composer.json (#161) --- composer.json | 1 - 1 file changed, 1 deletion(-) diff --git a/composer.json b/composer.json index 4244f133..b76ffd19 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,6 @@ "Firebase\\JWT\\": "src" } }, - "minimum-stability": "dev", "require-dev": { "phpunit/phpunit": " 4.8.35" } From b2a53166f9e2d8958be837e1b368c0897fc52a77 Mon Sep 17 00:00:00 2001 From: Giorgio Balduzzi Date: Wed, 21 Jun 2017 22:57:21 +0200 Subject: [PATCH 10/62] Add the proper Exception message for all JSON error types by PHP (#110) --- src/JWT.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/JWT.php b/src/JWT.php index 7729143e..814afc0a 100644 --- a/src/JWT.php +++ b/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 d67523fd6a2da172a196fe41a73ba5d4b563619f Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 21 Jun 2017 14:51:26 -0700 Subject: [PATCH 11/62] Detect invalid Base64 encoding in signature (#162) --- src/JWT.php | 5 +++-- tests/JWTTest.php | 7 +++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/JWT.php b/src/JWT.php index 814afc0a..cb1ca7d1 100644 --- a/src/JWT.php +++ b/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/tests/JWTTest.php b/tests/JWTTest.php index 99ae9c38..804a3769 100644 --- a/tests/JWTTest.php +++ b/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 8becb3b775a2288bda7259f6d13fce89999a4a14 Mon Sep 17 00:00:00 2001 From: Chinedu Francis Nwafili Date: Thu, 22 Jun 2017 09:51:13 +0700 Subject: [PATCH 12/62] Add new line warning to README (#115) --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 5c3261a2..294234b8 100644 --- a/README.md +++ b/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 4db1e95de5e6fd56af1a6f3ea4cdc6ccd65393d0 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 13/62] added `array` type hinting to `decode` method (#101) --- src/JWT.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/JWT.php b/src/JWT.php index cb1ca7d1..22a67e32 100644 --- a/src/JWT.php +++ b/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 f97b3e35c8f0b2c9bb4c27a0eb9bf735c25713ef Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Mon, 26 Jun 2017 10:28:13 -0700 Subject: [PATCH 14/62] Remove outdated package.xml (#165) --- package.xml | 77 ----------------------------------------------------- 1 file changed, 77 deletions(-) delete mode 100644 package.xml diff --git a/package.xml b/package.xml deleted file mode 100644 index a95b056f..00000000 --- a/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 9984a4d3a32ae7673d6971ea00bae9d0a1abba0e Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Tue, 27 Jun 2017 15:17:23 -0700 Subject: [PATCH 15/62] updates README changelog for v5.0.0 (#166) --- README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/README.md b/README.md index 294234b8..b1a7a3a2 100644 --- a/README.md +++ b/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 bac0422822b92fe7a0ed1fc7b1b633d9efa37bae Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Fri, 26 Jan 2018 11:13:29 -0800 Subject: [PATCH 16/62] Adds PHP 7.2 to Travis (#186) --- .travis.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 89131cc1..26f0ff0f 100644 --- a/.travis.yml +++ b/.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 2e5281c27e1a688173a7192a0e8f0a2c4e41397f Mon Sep 17 00:00:00 2001 From: Nick Williams Date: Mon, 4 Nov 2019 22:39:45 -0600 Subject: [PATCH 17/62] Adds ES256 to supported algorithms (#239) --- src/JWT.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/JWT.php b/src/JWT.php index 22a67e32..388d671f 100644 --- a/src/JWT.php +++ b/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 5f68890c13b0bb3415861963143831eb6fd42e6e Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Mon, 4 Nov 2019 23:08:04 -0800 Subject: [PATCH 18/62] Fixes tests, adds PHP 7.3 (#257) --- .travis.yml | 9 ++++++--- composer.json | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 26f0ff0f..59d474b7 100644 --- a/.travis.yml +++ b/.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/composer.json b/composer.json index b76ffd19..9f1a42cb 100644 --- a/composer.json +++ b/composer.json @@ -24,6 +24,6 @@ } }, "require-dev": { - "phpunit/phpunit": " 4.8.35" + "phpunit/phpunit": "^4.8|^5" } } From 264e5c720603bc87bf578b3a5b8e1a06d1c0787b Mon Sep 17 00:00:00 2001 From: Martin Krisell Date: Tue, 12 Nov 2019 20:16:19 +0100 Subject: [PATCH 19/62] Whitespace style fix (#255) --- src/JWT.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/JWT.php b/src/JWT.php index 388d671f..a9e4daf6 100644 --- a/src/JWT.php +++ b/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 8228431e09bc10d1ffaa2f6f02e51c9539179f92 Mon Sep 17 00:00:00 2001 From: David Mann Date: Tue, 12 Nov 2019 14:16:54 -0500 Subject: [PATCH 20/62] Comment typo fix (#253) --- src/JWT.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/JWT.php b/src/JWT.php index a9e4daf6..2fb98625 100644 --- a/src/JWT.php +++ b/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 bc324de7381586cfbbfbe1214b2de10108fbc210 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 21/62] Clearer variable name in README (#229) --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b1a7a3a2..9c8b5455 100644 --- a/README.md +++ b/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 20f37291b90ddcad3e9a56b6bd1798bd80336055 Mon Sep 17 00:00:00 2001 From: Martin Krisell Date: Tue, 17 Dec 2019 21:19:28 +0100 Subject: [PATCH 22/62] Use json_last_error without unnecessary existence check (#263) --- src/JWT.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/JWT.php b/src/JWT.php index 2fb98625..c3c3f667 100644 --- a/src/JWT.php +++ b/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 23192ac99d9b743c197d9bfc6e31e11878039c26 Mon Sep 17 00:00:00 2001 From: sergiy-petrov Date: Wed, 12 Feb 2020 00:35:53 +0200 Subject: [PATCH 23/62] Add php 7.4 to travis (#266) --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 59d474b7..fc651ad3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,7 @@ php: - 7.1 - 7.2 - 7.3 + - 7.4 matrix: include: From ccc74fb918551911caff5e19f3d3c1308a1f7536 Mon Sep 17 00:00:00 2001 From: Sergei Kolesnikov Date: Wed, 19 Feb 2020 21:30:12 +0300 Subject: [PATCH 24/62] docs: add resource type as 2nd param in decode method (#205) --- src/JWT.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/JWT.php b/src/JWT.php index c3c3f667..18756f70 100644 --- a/src/JWT.php +++ b/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 ecb25af3f5053819431b5b116008d11dce13d60c Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 19 Feb 2020 11:32:56 -0700 Subject: [PATCH 25/62] tests: update PHPUnit test classes (#193) --- tests/JWTTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/JWTTest.php b/tests/JWTTest.php index 804a3769..c164a8f0 100644 --- a/tests/JWTTest.php +++ b/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 78ec50cd5c7d0bbcaed6ece07ace040d8843b9cf Mon Sep 17 00:00:00 2001 From: Ilia Urvachev Date: Wed, 19 Feb 2020 21:34:19 +0300 Subject: [PATCH 26/62] docs: fix missed param name (#207) --- src/JWT.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/JWT.php b/src/JWT.php index 18756f70..11f6f108 100644 --- a/src/JWT.php +++ b/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 4566062c68f76f43d44f1643f4970fe89757d4c6 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Mon, 24 Feb 2020 16:15:03 -0700 Subject: [PATCH 27/62] feat: add support for ES256 algorithm (#256) --- src/JWT.php | 138 +++++++++++++++++++++++++++++++++++++++- tests/JWTTest.php | 16 +++++ tests/ecdsa-private.pem | 18 ++++++ tests/ecdsa-public.pem | 9 +++ 4 files changed, 179 insertions(+), 2 deletions(-) create mode 100644 tests/ecdsa-private.pem create mode 100644 tests/ecdsa-public.pem diff --git a/src/JWT.php b/src/JWT.php index 11f6f108..af206618 100644 --- a/src/JWT.php +++ b/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/tests/JWTTest.php b/tests/JWTTest.php index c164a8f0..0e1c20d1 100644 --- a/tests/JWTTest.php +++ b/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/tests/ecdsa-private.pem b/tests/ecdsa-private.pem new file mode 100644 index 00000000..5c77adaf --- /dev/null +++ b/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/tests/ecdsa-public.pem b/tests/ecdsa-public.pem new file mode 100644 index 00000000..31fa053d --- /dev/null +++ b/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 9eb9f98c2fc55625fb57c4ce00e7c8c6d4bc0c58 Mon Sep 17 00:00:00 2001 From: NiMeDia Date: Sat, 29 Feb 2020 01:59:58 +0100 Subject: [PATCH 28/62] fix: added keywords to composer.json (#243) (#271) --- composer.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/composer.json b/composer.json index 9f1a42cb..e72ecd07 100644 --- a/composer.json +++ b/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 5cd7ae01ae8bfad5b1c25ac8b187265ae1ccf7e6 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Thu, 5 Mar 2020 13:29:27 -0700 Subject: [PATCH 29/62] remove ignore_bit_strings (#276) --- src/JWT.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/JWT.php b/src/JWT.php index af206618..f8fe7e65 100644 --- a/src/JWT.php +++ b/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 51034b43ea55b95d289f0722ba1d84e8bb09ba8e Mon Sep 17 00:00:00 2001 From: Phil Davis Date: Mon, 16 Mar 2020 21:08:03 +0545 Subject: [PATCH 30/62] test: allow for later versions of phpunit (#277) --- composer.json | 2 +- tests/JWTTest.php | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index e72ecd07..25d1cfa9 100644 --- a/composer.json +++ b/composer.json @@ -28,6 +28,6 @@ } }, "require-dev": { - "phpunit/phpunit": "^4.8|^5" + "phpunit/phpunit": ">=4.8 <=9" } } diff --git a/tests/JWTTest.php b/tests/JWTTest.php index 0e1c20d1..19520165 100644 --- a/tests/JWTTest.php +++ b/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 b0def5fca80851717920a3816b5c670e6182bc2f Mon Sep 17 00:00:00 2001 From: Eric Tendian Date: Sat, 21 Mar 2020 14:34:46 -0500 Subject: [PATCH 31/62] feat: adds JWK support (#273) --- src/JWK.php | 171 ++++++++++++++++++++++++++++++++++++++++++++++ tests/JWKTest.php | 159 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 330 insertions(+) create mode 100644 src/JWK.php create mode 100644 tests/JWKTest.php diff --git a/src/JWK.php b/src/JWK.php new file mode 100644 index 00000000..f2777df8 --- /dev/null +++ b/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/tests/JWKTest.php b/tests/JWKTest.php new file mode 100644 index 00000000..4f8fdf65 --- /dev/null +++ b/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 27ee05f8a52227d42d09f357d953fd04f6b6deeb Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Sat, 21 Mar 2020 12:59:57 -0700 Subject: [PATCH 32/62] test: use keys for testing JWK (#283) --- tests/JWKTest.php | 197 +++++++++++++++-------------------------- tests/rsa-jwkset.json | 17 ++++ tests/rsa1-private.pem | 27 ++++++ tests/rsa2-private.pem | 27 ++++++ 4 files changed, 141 insertions(+), 127 deletions(-) create mode 100644 tests/rsa-jwkset.json create mode 100644 tests/rsa1-private.pem create mode 100644 tests/rsa2-private.pem diff --git a/tests/JWKTest.php b/tests/JWKTest.php index 4f8fdf65..d02534f1 100644 --- a/tests/JWKTest.php +++ b/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/tests/rsa-jwkset.json b/tests/rsa-jwkset.json new file mode 100644 index 00000000..0059f8cc --- /dev/null +++ b/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/tests/rsa1-private.pem b/tests/rsa1-private.pem new file mode 100644 index 00000000..b194b5b4 --- /dev/null +++ b/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/tests/rsa2-private.pem b/tests/rsa2-private.pem new file mode 100644 index 00000000..74380869 --- /dev/null +++ b/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 3811d6946955d5cb1f046fe0932a68ef6c413952 Mon Sep 17 00:00:00 2001 From: Phil Davis Date: Tue, 24 Mar 2020 02:02:54 +0545 Subject: [PATCH 33/62] chore: code cleanup (#278) --- src/JWT.php | 2 +- tests/JWTTest.php | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/JWT.php b/src/JWT.php index f8fe7e65..6f178b4e 100644 --- a/src/JWT.php +++ b/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/tests/JWTTest.php b/tests/JWTTest.php index 19520165..867d87e5 100644 --- a/tests/JWTTest.php +++ b/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 feb0e820b8436873675fd3aca04f3728eb2185cb Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 25 Mar 2020 11:49:23 -0700 Subject: [PATCH 34/62] chore: fixes cs and adds cs checking to travis (#284) --- .gitattributes | 1 - .travis.yml | 15 ++++- run-tests.sh | 37 ----------- src/BeforeValidException.php | 1 - src/ExpiredException.php | 1 - src/JWK.php | 34 +++++----- src/JWT.php | 106 +++++++++++++++--------------- src/SignatureInvalidException.php | 1 - tests/JWKTest.php | 2 +- tests/JWTTest.php | 25 +------ 10 files changed, 87 insertions(+), 136 deletions(-) delete mode 100755 run-tests.sh diff --git a/.gitattributes b/.gitattributes index c682f861..a791521e 100644 --- a/.gitattributes +++ b/.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/.travis.yml b/.travis.yml index fc651ad3..90e516cd 100644 --- a/.travis.yml +++ b/.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/run-tests.sh b/run-tests.sh deleted file mode 100755 index c4bb9348..00000000 --- a/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/src/BeforeValidException.php b/src/BeforeValidException.php index a6ee2f7c..fdf82bd9 100644 --- a/src/BeforeValidException.php +++ b/src/BeforeValidException.php @@ -3,5 +3,4 @@ class BeforeValidException extends \UnexpectedValueException { - } diff --git a/src/ExpiredException.php b/src/ExpiredException.php index 3597370a..7f7d0568 100644 --- a/src/ExpiredException.php +++ b/src/ExpiredException.php @@ -3,5 +3,4 @@ class ExpiredException extends \UnexpectedValueException { - } diff --git a/src/JWK.php b/src/JWK.php index f2777df8..1d273917 100644 --- a/src/JWK.php +++ b/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/src/JWT.php b/src/JWT.php index 6f178b4e..4860028b 100644 --- a/src/JWT.php +++ b/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/src/SignatureInvalidException.php b/src/SignatureInvalidException.php index 27332b21..87cb34df 100644 --- a/src/SignatureInvalidException.php +++ b/src/SignatureInvalidException.php @@ -3,5 +3,4 @@ class SignatureInvalidException extends \UnexpectedValueException { - } diff --git a/tests/JWKTest.php b/tests/JWKTest.php index d02534f1..3d317d55 100644 --- a/tests/JWKTest.php +++ b/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/tests/JWTTest.php b/tests/JWTTest.php index 867d87e5..fc9c3756 100644 --- a/tests/JWTTest.php +++ b/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 1fae8c46348ed6da8a56deaff282034cb57673e0 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Mon, 26 Oct 2020 10:34:38 -0700 Subject: [PATCH 35/62] docs: add JWK usage to README (#307) --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index 9c8b5455..ba139079 100644 --- a/README.md +++ b/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 f42c9110abe98dd6cfe9053c49bc86acc70b2d23 Mon Sep 17 00:00:00 2001 From: Grant Anderson Date: Thu, 11 Feb 2021 17:02:00 -0700 Subject: [PATCH 36/62] fix: add missing use statement in JWK (#303) --- src/JWK.php | 1 + tests/JWKTest.php | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/src/JWK.php b/src/JWK.php index 1d273917..7632f4a4 100644 --- a/src/JWK.php +++ b/src/JWK.php @@ -3,6 +3,7 @@ namespace Firebase\JWT; use DomainException; +use InvalidArgumentException; use UnexpectedValueException; /** diff --git a/tests/JWKTest.php b/tests/JWKTest.php index 3d317d55..b8b67540 100644 --- a/tests/JWKTest.php +++ b/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 7b4f4d2641d5b370f73ed0e6bcf340beddcc0ca3 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 37/62] chore: add phpdoc @throws in JWT::decode (#320) --- src/JWT.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/JWT.php b/src/JWT.php index 4860028b..76a0551c 100644 --- a/src/JWT.php +++ b/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 474047dbd8442a730ab03810f2835839b107cd29 Mon Sep 17 00:00:00 2001 From: Ashutosh K Tripathi Date: Sat, 6 Mar 2021 03:07:26 +0530 Subject: [PATCH 38/62] chore: remove leading backslashes in imports (#301) Co-authored-by: Brent Shaffer --- src/JWT.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/JWT.php b/src/JWT.php index 76a0551c..b167abd7 100644 --- a/src/JWT.php +++ b/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 8ddb39535ef82b835e39fe8f5ad3c5bd452a0148 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Mon, 17 May 2021 11:37:21 -0500 Subject: [PATCH 39/62] chore: remove travis, add github actions (#331) --- .github/actions/entrypoint.sh | 18 +++++++++ .github/workflows/tests.yml | 66 +++++++++++++++++++++++++++++++ .travis.yml | 36 ----------------- phpunit.xml.dist | 1 - src/BeforeValidException.php | 1 + src/ExpiredException.php | 1 + src/SignatureInvalidException.php | 1 + tests/JWKTest.php | 1 + tests/JWTTest.php | 1 + 9 files changed, 89 insertions(+), 37 deletions(-) create mode 100755 .github/actions/entrypoint.sh create mode 100644 .github/workflows/tests.yml delete mode 100644 .travis.yml diff --git a/.github/actions/entrypoint.sh b/.github/actions/entrypoint.sh new file mode 100755 index 00000000..ce8379cb --- /dev/null +++ b/.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/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..09539931 --- /dev/null +++ b/.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/.travis.yml b/.travis.yml deleted file mode 100644 index 90e516cd..00000000 --- a/.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/phpunit.xml.dist b/phpunit.xml.dist index 9f85f5ba..092a662c 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -8,7 +8,6 @@ convertWarningsToExceptions="true" processIsolation="false" stopOnFailure="false" - syntaxCheck="false" bootstrap="tests/bootstrap.php" > diff --git a/src/BeforeValidException.php b/src/BeforeValidException.php index fdf82bd9..c147852b 100644 --- a/src/BeforeValidException.php +++ b/src/BeforeValidException.php @@ -1,4 +1,5 @@ Date: Mon, 17 May 2021 11:49:46 -0500 Subject: [PATCH 40/62] fix: allow for null d values in RSA JWK (#330) --- src/JWK.php | 2 +- tests/JWKTest.php | 30 ++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/JWK.php b/src/JWK.php index 7632f4a4..29dbbac1 100644 --- a/src/JWK.php +++ b/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/tests/JWKTest.php b/tests/JWKTest.php index 93572400..0709836d 100644 --- a/tests/JWKTest.php +++ b/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 fd08d5a171cad8ba519ab4aef9a4e2497ee0e109 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Mon, 17 May 2021 17:50:39 -0500 Subject: [PATCH 41/62] chore(docs): clean up README (#332) --- README.md | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index ba139079..9f3d0846 100644 --- a/README.md +++ b/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 75693c0d2aa03489db05f42b2791dd81fe3f1164 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Mon, 17 May 2021 15:57:50 -0700 Subject: [PATCH 42/62] chore(tests): add php 5.3 test --- .github/workflows/tests.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 09539931..0ea47358 100644 --- a/.github/workflows/tests.yml +++ b/.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 d87688f9e65ca279ca66181fd3476782efb791d2 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Mon, 17 May 2021 18:35:58 -0500 Subject: [PATCH 43/62] chore(tests): fix entrypoint for php 5.3 (#333) --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0ea47358..343df75d 100644 --- a/.github/workflows/tests.yml +++ b/.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 8d6bfd4e805a5fc323f18852aa253f4625ab212e Mon Sep 17 00:00:00 2001 From: Scott Dutton Date: Wed, 19 May 2021 19:08:37 +0100 Subject: [PATCH 44/62] chore: fix licence (#329) --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index cb0c49b3..11c01466 100644 --- a/LICENSE +++ b/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 9af3b99c1c92f20efd3f0a665f6e54aa5897ab89 Mon Sep 17 00:00:00 2001 From: Stoian Ivanov Date: Wed, 19 May 2021 21:19:30 +0300 Subject: [PATCH 45/62] feat: add ES384 support (#324) --- src/JWT.php | 17 ++++++++++++----- tests/JWTTest.php | 16 ++++++++++++++++ tests/ecdsa384-private.pem | 6 ++++++ tests/ecdsa384-public.pem | 5 +++++ 4 files changed, 39 insertions(+), 5 deletions(-) create mode 100644 tests/ecdsa384-private.pem create mode 100644 tests/ecdsa384-public.pem diff --git a/src/JWT.php b/src/JWT.php index b167abd7..c68d4e15 100644 --- a/src/JWT.php +++ b/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/tests/JWTTest.php b/tests/JWTTest.php index bc9d7a8c..2516ec0d 100644 --- a/tests/JWTTest.php +++ b/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/tests/ecdsa384-private.pem b/tests/ecdsa384-private.pem new file mode 100644 index 00000000..ee593e6f --- /dev/null +++ b/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/tests/ecdsa384-public.pem b/tests/ecdsa384-public.pem new file mode 100644 index 00000000..475f1348 --- /dev/null +++ b/tests/ecdsa384-public.pem @@ -0,0 +1,5 @@ +-----BEGIN PUBLIC KEY----- +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEFhXXcyKmWkTkdVbWYYU3dtJqpJ0JmLGf +tEdNzUEFEKSU5MlnLr/FjcneszvXAqEBM4veJF/dO/zhFk44bh/ELXbp0/nn9QaV +iVtQpuTvpu29eefx6PfUMqX0K++IS4NQ +-----END PUBLIC KEY----- From ae3188c2077d3be2d1a45d8f7fcd83bdc7394b54 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Thu, 20 May 2021 11:48:20 -0500 Subject: [PATCH 46/62] chore: test cleanup (#334) --- tests/JWTTest.php | 63 +++++++++++++++++++++-------------------------- 1 file changed, 28 insertions(+), 35 deletions(-) diff --git a/tests/JWTTest.php b/tests/JWTTest.php index 2516ec0d..46ea7ef0 100644 --- a/tests/JWTTest.php +++ b/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 3c2d70f2e64e2922345e89f2ceae47d2463faae1 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Thu, 20 May 2021 12:37:02 -0500 Subject: [PATCH 47/62] chore: add tests and docs for rsa with passphrase (#335) --- README.md | 34 ++++++++++++++++++++++++++++ src/JWT.php | 16 ++++++------- tests/JWTTest.php | 14 ++++++++++++ tests/rsa-with-passphrase.pem | 42 +++++++++++++++++++++++++++++++++++ 4 files changed, 98 insertions(+), 8 deletions(-) create mode 100644 tests/rsa-with-passphrase.pem diff --git a/README.md b/README.md index 9f3d0846..66d7d014 100644 --- a/README.md +++ b/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/src/JWT.php b/src/JWT.php index c68d4e15..4b85699f 100644 --- a/src/JWT.php +++ b/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/tests/JWTTest.php b/tests/JWTTest.php index 46ea7ef0..7c3d9649 100644 --- a/tests/JWTTest.php +++ b/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/tests/rsa-with-passphrase.pem b/tests/rsa-with-passphrase.pem new file mode 100644 index 00000000..ad55326f --- /dev/null +++ b/tests/rsa-with-passphrase.pem @@ -0,0 +1,42 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-128-CBC,D8AA4EC8D8B5883F09ACB308FB026C94 + +ixqo1+NTlkiUHUa1bucqHNQ4nca4cnaosK8Lauftc0WuyqNVE+NL/zxdiUKN+Qi1 +bhEkvMKgbqTMzPFUws3wNoPEI/eaoGYHTl4nAX79JWjJ8/DWY+VVp5IFSzNEM1MP +NMWaivfBGhd8W9kBmpOJpQjwePFk7hdLkEvSngGRhDmEV046cWr7I+koYKEG/oW9 +53NnDNKPKLPkzM/Me4GQ6nXarqUPoIn/c3qFLgkhkzLJ/Lu21wnYx46RasXJv3oq +xT3nRIat/Q6jtlzLLwvo+lpvJW3G+rKqjEi76Av7Cm1TkHQFW9CGsnQ4ZDn427KL +FGojP6QG5RjLI6IiAHgt0lnzOwtjbF1RQBHIWedC4Rufb5u92SRKJ2PvidB/suJ7 +SR/PPA2XpK22QBMccO9yjNh4ZZIV6I2cqv3BlKR2RFU0552sEQr6usxPfFhExIRR +1eiaLtIupo3uEC5e2fBKtI7D3T7WztUagTw0vSgoxhTdc8XIoT0prV91SvyEEZMw +r5LSRW4BvyCekG9FFyIS2fOWabgxmm16siNErTbS2RS3GGimX0v5O+KIN9ho1uAY +5U865amaOZshop1YYixtDJL27JhpkODhwXrB1lNQOCdi64CV2r8VlVPNg6TWZlli +vJ6agKvWmTppy07ovbBRB+llmW6eGtjwEmAvMaWNgkFNkgDF/wBnDi91tx8/8UL7 +XQy0VZz128FtpJC0G0Z/5HmxqoEJAwk1+EzO5tgnfc+2wIONGCV2ISph0efVtPui +xOP6geaeSrxBxL/BUcIX5DMfN6hsvz+Pb8bE9WT2+fz/ySCJhkfraC/vHbs3wn3R +CICCvYtR803ku53GCgsEZ8vmIxMb1D0mJnfWvSQtDBqF8XwhL6m5ShbeaMLkbmZ9 +0WLWj0zAcOkbX4TXLGVaRPRs9HjSEr7+jEVHO6OeKj60rG9M3NVmfig7J8ta/zvy +1Hk4MiucTsp0I+G/hx8dqoV4x1kTyn0WZMfD8PxnbPdPvbhG2tQn7xkZykgtvK5y +s1fMbvqVGDfn5PmLeSwYkyohYZGbiwV5UldhwdG/ZnagI1KPuJ10OYBOSLCcGufY +aUHmIFSvfYqbN5YfKsMCZmmrX73pDcXOWGWto8nTFS9f4RlQI0Vh25xJqqinD6Vu +ErP7+XxDZCLqKew/xfq1fcKoiCOA/9IK5meyjRV4Z5QxkgTeBmyNVt/MW+6QIJJJ +WoBWqpootxtb28YN2RuD0byEIyP8pmoyN3MOPYGNSia8PAQgIL6z71Ju2SejXADy +ybirbrS0Y/oZABqhLK5qDdCYe4O5zp/lbwWn2Gfp3G3xKUxfBWi4f/VQwUjUbYCz +XHFVLpDY1mMPaedo7Tp5ZGN4OHwIlpspcwI0U9TYac0AxZuSBPjE8YqJ2qJBhaiZ +dEE7CxwkSLLxXVEPp7+VO6CORZfYXXaRcpTAZfrDURSI5RkT8n6LElnrzFBilb0q +ejlKaLD4MLlvlc/NWl/w+TfuN/iGlQm02Ul8yysG1b0w8R+seMNHhHS4+848ZRBd +HoWUuYiYXZTJxmP5dc0f/Sul672YSFp7rGzt9+7hFV6WrkAFNxETkQ8cbA/GiGvz +Kvv1GI/Ms8YymAJWiv7skFTmGcHMbjxga2EOBtSfYF5mwV3KEMPRpYsn1nw6U99E +NuWFqT+p4VqVSgmeG11zwM7v+Vt3RZDUggZWDsNKGA9V9ciAlHY2U7CH6xihBCfh +suHNuzVC1nAwi/ZrhfJXMKk+hJ8o+5dXSTYp4eCEGh4U2l3pmmAejZenJqlGs0Ke +MYHQRCk5zaB5myRYuvwtUSbZ/BaVVFSQz758Vw4HxKFLnvudtAXktu3sTcOgYKQS +PaiolwZFr4lp3h74BlIYcYrREmBJv6Hy1lOLAd5X3iExiy+DdRJWkuNd+19Cblq3 +ePHf2Mgp+AElxmyA6EHyt86v3E2mL7xNAUUVrNb3UJTi6io5KASMVmNbrGGJksC7 +y3OuHaq1RM7UvR/eI38nI2YOckoKDgkhHPtaXkIpO9jX3RRYlA2uzsf44DU7etyc +c2ICApYVdKruR/pmFN45pcIPy6x3zU34fkRTMf1F3yShJzr8Ntd/C63Km8XaganW +2AVWuuvOJXjMqu4+OXzrIqObFFp6naqv1E+O8/14i8k4VW3dmWnMM7eq9FvqQdiM +y0tBbGILfAVYtjh59r+CKeqRoq7o/xlsVin1Vxn74K6uYUphjXWUhMXXStGZ8sBc +QDOPTanB+LPBeCAgQFQe1SHrGiIognXT0g2WFqW8DrxwTqr6olPoMF6LU01vqT0+ +HVZtczjk0LvDLZm8bsCDGBPDdbDI/tfvXncP5PgEtFSTUiRy+zryy82AF4rJhudH +-----END RSA PRIVATE KEY----- From c63c1a713348bc2b3c5fd2bab9d078bd702bbaa9 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Tue, 22 Jun 2021 13:05:02 -0500 Subject: [PATCH 48/62] feat: make parseKey public (#337) --- src/JWK.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/JWK.php b/src/JWK.php index 29dbbac1..981a9ba7 100644 --- a/src/JWK.php +++ b/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 21b0c7bfb71a8b9b3ffae5acc2a0d6a38eb5ae75 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 49/62] chore: export-ignore github dir (#338) --- .gitattributes | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitattributes b/.gitattributes index a791521e..6d63e560 100644 --- a/.gitattributes +++ b/.gitattributes @@ -5,3 +5,4 @@ /.gitignore export-ignore /.travis.yml export-ignore /phpunit.xml.dist export-ignore +/.github export-ignore From 44d0a5a17312b55e26b78ad87b034be71495b137 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 23 Jun 2021 11:39:56 -0500 Subject: [PATCH 50/62] chore(tests): add rsa encode/decode (#344) --- tests/JWTTest.php | 1 + tests/rsa1-public.pub | 9 +++++++++ 2 files changed, 10 insertions(+) create mode 100644 tests/rsa1-public.pub diff --git a/tests/JWTTest.php b/tests/JWTTest.php index 7c3d9649..09dac142 100644 --- a/tests/JWTTest.php +++ b/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/tests/rsa1-public.pub b/tests/rsa1-public.pub new file mode 100644 index 00000000..83a080f4 --- /dev/null +++ b/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 d2113d9b2e0e349796e72d2a63cf9319100382d2 Mon Sep 17 00:00:00 2001 From: pwolanin Date: Wed, 23 Jun 2021 15:00:23 -0400 Subject: [PATCH 51/62] feat: add Ed25519 support to JWT (#343) --- .github/actions/entrypoint.sh | 4 +++- .github/workflows/tests.yml | 2 ++ README.md | 37 +++++++++++++++++++++++++++++ composer.json | 3 +++ src/JWT.php | 44 +++++++++++++++++++++++++++-------- tests/JWTTest.php | 29 +++++++++++++++++++++++ tests/ed25519-1.pub | 1 + tests/ed25519-1.sec | 1 + 8 files changed, 110 insertions(+), 11 deletions(-) create mode 100644 tests/ed25519-1.pub create mode 100644 tests/ed25519-1.sec diff --git a/.github/actions/entrypoint.sh b/.github/actions/entrypoint.sh index ce8379cb..8b6b9e1b 100755 --- a/.github/actions/entrypoint.sh +++ b/.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/.github/workflows/tests.yml b/.github/workflows/tests.yml index 343df75d..92b4e9e0 100644 --- a/.github/workflows/tests.yml +++ b/.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/README.md b/README.md index 66d7d014..a8556aa5 100644 --- a/README.md +++ b/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/composer.json b/composer.json index 25d1cfa9..6146e2dc 100644 --- a/composer.json +++ b/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/src/JWT.php b/src/JWT.php index 4b85699f..99d6dcd2 100644 --- a/src/JWT.php +++ b/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/tests/JWTTest.php b/tests/JWTTest.php index 09dac142..3dee0450 100644 --- a/tests/JWTTest.php +++ b/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/tests/ed25519-1.pub b/tests/ed25519-1.pub new file mode 100644 index 00000000..e4ae63ac --- /dev/null +++ b/tests/ed25519-1.pub @@ -0,0 +1 @@ +uOSJMhbKSG4V5xUHS7B9YHmVg/1yVd+G+Io6oBFhSfY= diff --git a/tests/ed25519-1.sec b/tests/ed25519-1.sec new file mode 100644 index 00000000..354ffa7a --- /dev/null +++ b/tests/ed25519-1.sec @@ -0,0 +1 @@ +i4eTKkWNIISKumdk3v90cPDrY/g8WRTJWy7DmGDsdzC45IkyFspIbhXnFQdLsH1geZWD/XJV34b4ijqgEWFJ9g== From 17513df3296103261e9b7de9582e6969d14149f3 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 3 Nov 2021 15:57:11 -0600 Subject: [PATCH 52/62] chore: fix tests (#366) --- .github/actions/entrypoint.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/actions/entrypoint.sh b/.github/actions/entrypoint.sh index 8b6b9e1b..40402bc8 100755 --- a/.github/actions/entrypoint.sh +++ b/.github/actions/entrypoint.sh @@ -5,6 +5,7 @@ apt-get install -y --no-install-recommends \ git \ zip \ curl \ + ca-certificates \ unzip \ wget From 804585f8963a49cea3cea9f480e0051aa5593609 Mon Sep 17 00:00:00 2001 From: Sergiy Petrov Date: Thu, 4 Nov 2021 00:00:38 +0200 Subject: [PATCH 53/62] chore: add PHP 8.1 (#362) --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 92b4e9e0..6f0f5e91 100644 --- a/.github/workflows/tests.yml +++ b/.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 bc0df6440dfe4099266a44e99a2839a1856b8ec0 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Thu, 4 Nov 2021 10:15:22 -0600 Subject: [PATCH 54/62] feat: add Key object to prevent key/algorithm type confusion (#365) --- README.md | 17 +++++-- src/JWT.php | 122 ++++++++++++++++++++++++++++++++++------------ src/Key.php | 59 ++++++++++++++++++++++ tests/JWTTest.php | 28 +++++++++++ 4 files changed, 190 insertions(+), 36 deletions(-) create mode 100644 src/Key.php diff --git a/README.md b/README.md index a8556aa5..ee98c47f 100644 --- a/README.md +++ b/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/src/JWT.php b/src/JWT.php index 99d6dcd2..f46e8372 100644 --- a/src/JWT.php +++ b/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/src/Key.php b/src/Key.php new file mode 100644 index 00000000..f1ede6f2 --- /dev/null +++ b/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/tests/JWTTest.php b/tests/JWTTest.php index 3dee0450..63386d88 100644 --- a/tests/JWTTest.php +++ b/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 cf814442ce0e9eebe5317d61b63ccda4b85de67a Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Thu, 4 Nov 2021 10:21:41 -0600 Subject: [PATCH 55/62] chore: explicit third parameter to decode function in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ee98c47f..1d392cd1 100644 --- a/README.md +++ b/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 83b609028194aa042ea33b5af2d41a7427de80e6 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Mon, 8 Nov 2021 14:18:51 -0600 Subject: [PATCH 56/62] fix: phpdoc and exception (#371) --- src/JWT.php | 17 ++++++++++++----- tests/JWTTest.php | 15 +++++++++++++++ 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/JWT.php b/src/JWT.php index f46e8372..ec1641bc 100644 --- a/src/JWT.php +++ b/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/tests/JWTTest.php b/tests/JWTTest.php index 63386d88..1c81c1ed 100644 --- a/tests/JWTTest.php +++ b/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 12ec2fe16082a510b98e301cab07bbbcc5daea33 Mon Sep 17 00:00:00 2001 From: Andrej Rypo Date: Wed, 1 Dec 2021 23:04:08 +0100 Subject: [PATCH 57/62] chore(docs): add throws DomainException for JWT::decode (#379) --- src/JWT.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/JWT.php b/src/JWT.php index ec1641bc..b2e78041 100644 --- a/src/JWT.php +++ b/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 262f84c2dca214267051e8d311ee49bb96ec65c7 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 1 Dec 2021 17:04:45 -0500 Subject: [PATCH 58/62] chore: switch main to master (#383) --- .github/workflows/tests.yml | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6f0f5e91..50d8a5f0 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -2,7 +2,7 @@ name: Test Suite on: push: branches: - - master + - main pull_request: jobs: diff --git a/README.md b/README.md index 1d392cd1..af7ed087 100644 --- a/README.md +++ b/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 fbe639489d3348ff6e6d91f80b15198b39dc9217 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 1 Dec 2021 14:06:59 -0800 Subject: [PATCH 59/62] chore(docs): fix typo in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index af7ed087..acd1720c 100644 --- a/README.md +++ b/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 edda0f9ee45b8367699804f792a9be6d5175e816 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Thu, 30 Dec 2021 11:00:10 -0800 Subject: [PATCH 60/62] feat!: require Key object, use JSON_UNESCAPED_SLASHES, remove constants (#376) --- .gitignore | 1 + README.md | 3 +- src/JWK.php | 10 +- src/JWT.php | 94 ++++++------- tests/JWKTest.php | 38 +++-- tests/JWTTest.php | 168 +++++++++-------------- tests/autoload.php.dist | 17 --- tests/bootstrap.php | 13 +- tests/{ => data}/ecdsa-private.pem | 0 tests/{ => data}/ecdsa-public.pem | 0 tests/{ => data}/ecdsa384-private.pem | 0 tests/{ => data}/ecdsa384-public.pem | 0 tests/{ => data}/ed25519-1.pub | 0 tests/{ => data}/ed25519-1.sec | 0 tests/{ => data}/rsa-jwkset.json | 7 +- tests/{ => data}/rsa-with-passphrase.pem | 0 tests/{ => data}/rsa1-private.pem | 0 tests/{ => data}/rsa1-public.pub | 0 tests/{ => data}/rsa2-private.pem | 0 19 files changed, 161 insertions(+), 190 deletions(-) delete mode 100644 tests/autoload.php.dist rename tests/{ => data}/ecdsa-private.pem (100%) rename tests/{ => data}/ecdsa-public.pem (100%) rename tests/{ => data}/ecdsa384-private.pem (100%) rename tests/{ => data}/ecdsa384-public.pem (100%) rename tests/{ => data}/ed25519-1.pub (100%) rename tests/{ => data}/ed25519-1.sec (100%) rename tests/{ => data}/rsa-jwkset.json (86%) rename tests/{ => data}/rsa-with-passphrase.pem (100%) rename tests/{ => data}/rsa1-private.pem (100%) rename tests/{ => data}/rsa1-public.pub (100%) rename tests/{ => data}/rsa2-private.pem (100%) diff --git a/.gitignore b/.gitignore index 080f19aa..b22842cb 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ phpunit.phar phpunit.phar.asc composer.phar composer.lock +.phpunit.result.cache diff --git a/README.md b/README.md index acd1720c..0a0023b2 100644 --- a/README.md +++ b/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/src/JWK.php b/src/JWK.php index 981a9ba7..c53251d3 100644 --- a/src/JWK.php +++ b/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/src/JWT.php b/src/JWT.php index b2e78041..e6038648 100644 --- a/src/JWT.php +++ b/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/tests/JWKTest.php b/tests/JWKTest.php index 0709836d..b908ea64 100644 --- a/tests/JWKTest.php +++ b/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/tests/JWTTest.php b/tests/JWTTest.php index 1c81c1ed..36e2095e 100644 --- a/tests/JWTTest.php +++ b/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/tests/autoload.php.dist b/tests/autoload.php.dist deleted file mode 100644 index 2e4310a0..00000000 --- a/tests/autoload.php.dist +++ /dev/null @@ -1,17 +0,0 @@ - Date: Mon, 24 Jan 2022 06:32:49 -0800 Subject: [PATCH 61/62] chore: update changelog for v6.0.0 (#391) --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 0a0023b2..26e0436b 100644 --- a/README.md +++ b/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 0541cba75ab108ef901985e68055a92646c73534 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Mon, 24 Jan 2022 07:18:34 -0800 Subject: [PATCH 62/62] feat!: update return type for JWK methods (#392) --- README.md | 5 +++-- src/JWK.php | 22 ++++++++++------------ src/JWT.php | 6 +++--- tests/JWKTest.php | 6 +++--- 4 files changed, 19 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 26e0436b..7839af60 100644 --- a/README.md +++ b/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/src/JWK.php b/src/JWK.php index c53251d3..c5506548 100644 --- a/src/JWK.php +++ b/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/src/JWT.php b/src/JWT.php index e6038648..725a0832 100644 --- a/src/JWT.php +++ b/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/tests/JWKTest.php b/tests/JWKTest.php index b908ea64..c580f40f 100644 --- a/tests/JWKTest.php +++ b/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(