diff --git a/.gitignore b/.gitignore index 080f19aa..cc979c1b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ vendor phpunit.phar phpunit.phar.asc composer.phar -composer.lock +.idea +composer.lock \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 26f0ff0f..71c1227c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,11 +7,10 @@ php: - 7.0 - 7.1 - 7.2 - + - hhvm matrix: - include: - - php: 5.3 - dist: precise + allow_failures: + - php: hhvm sudo: false diff --git a/README.md b/README.md index b1a7a3a2..4d18cb7f 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ -[![Build Status](https://travis-ci.org/firebase/php-jwt.png?branch=master)](https://travis-ci.org/firebase/php-jwt) -[![Latest Stable Version](https://poser.pugx.org/firebase/php-jwt/v/stable)](https://packagist.org/packages/firebase/php-jwt) -[![Total Downloads](https://poser.pugx.org/firebase/php-jwt/downloads)](https://packagist.org/packages/firebase/php-jwt) -[![License](https://poser.pugx.org/firebase/php-jwt/license)](https://packagist.org/packages/firebase/php-jwt) +[![Build Status](https://travis-ci.org/fproject/php-jwt.png?branch=master)](https://travis-ci.org/fproject/php-jwt) +[![Latest Stable Version](https://poser.pugx.org/fproject/php-jwt/v/stable)](https://packagist.org/packages/fproject/php-jwt) +[![Total Downloads](https://poser.pugx.org/fproject/php-jwt/downloads)](https://packagist.org/packages/fproject/php-jwt) +[![License](https://poser.pugx.org/fproject/php-jwt/license)](https://packagist.org/packages/fproject/php-jwt) PHP-JWT ======= -A simple library to encode and decode JSON Web Tokens (JWT) in PHP, conforming to [RFC 7519](https://tools.ietf.org/html/rfc7519). +PHP library to encode and decode JSON Web Tokens (JWT). Support several key types including JWK. Conform to the [current spec](https://tools.ietf.org/html/rfc7519). Installation ------------ @@ -13,7 +13,7 @@ Installation Use composer to manage your dependencies and download PHP-JWT: ```bash -composer require firebase/php-jwt +composer require fproject/php-jwt ``` Example @@ -118,62 +118,18 @@ 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)! -- Fixes to exceptions classes. See [#81](https://github.com/firebase/php-jwt/pull/81) for details. Thanks to [@Maks3w](https://github.com/Maks3w)! -- Fixes to PHPDoc. See [#76](https://github.com/firebase/php-jwt/pull/76) for details. Thanks to [@akeeman](https://github.com/akeeman)! +#### 5.0.0 / 2018-03-21 + - Update to 5.0.0 from upstream + +#### 4.0.0 / 2016-08-10 + - Update to 4.0.0 from upstream + +#### 3.0.3 / 2015-11-05 + - Minimum PHP version updated from `5.3.0` to `5.4.0`. + - Add JWK support #### 3.0.0 / 2015-07-22 -- Minimum PHP version updated from `5.2.0` to `5.3.0`. -- Add `\Firebase\JWT` namespace. See -[#59](https://github.com/firebase/php-jwt/pull/59) for details. Thanks to -[@Dashron](https://github.com/Dashron)! -- Require a non-empty key to decode and verify a JWT. See -[#60](https://github.com/firebase/php-jwt/pull/60) for details. Thanks to -[@sjones608](https://github.com/sjones608)! -- Cleaner documentation blocks in the code. See -[#62](https://github.com/firebase/php-jwt/pull/62) for details. Thanks to -[@johanderuijter](https://github.com/johanderuijter)! - -#### 2.2.0 / 2015-06-22 -- Add support for adding custom, optional JWT headers to `JWT::encode()`. See -[#53](https://github.com/firebase/php-jwt/pull/53/files) for details. Thanks to -[@mcocaro](https://github.com/mcocaro)! - -#### 2.1.0 / 2015-05-20 -- Add support for adding a leeway to `JWT:decode()` that accounts for clock skew -between signing and verifying entities. Thanks to [@lcabral](https://github.com/lcabral)! -- Add support for passing an object implementing the `ArrayAccess` interface for -`$keys` argument in `JWT::decode()`. Thanks to [@aztech-dev](https://github.com/aztech-dev)! - -#### 2.0.0 / 2015-04-01 -- **Note**: It is strongly recommended that you update to > v2.0.0 to address - known security vulnerabilities in prior versions when both symmetric and - asymmetric keys are used together. -- Update signature for `JWT::decode(...)` to require an array of supported - algorithms to use when verifying token signatures. + - Original features from firebase/php-jwt repository Tests diff --git a/composer.json b/composer.json index b76ffd19..e25cb490 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { - "name": "firebase/php-jwt", - "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", - "homepage": "https://github.com/firebase/php-jwt", + "name": "fproject/php-jwt", + "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Support several key types including JWK.", + "homepage": "https://github.com/fproject/php-jwt", "authors": [ { "name": "Neuman Vong", @@ -12,11 +12,16 @@ "name": "Anant Narayanan", "email": "anant@php.net", "role": "Developer" + }, + { + "name": "Bui Sy Nguyen", + "email": "nguyenbs@gmail.com", + "role": "Developer" } ], "license": "BSD-3-Clause", "require": { - "php": ">=5.3.0" + "php": ">=5.4.0" }, "autoload": { "psr-4": { @@ -24,6 +29,6 @@ } }, "require-dev": { - "phpunit/phpunit": " 4.8.35" + "phpunit/phpunit": "4.8.36" } } diff --git a/package.xml b/package.xml new file mode 100644 index 00000000..5c043d5f --- /dev/null +++ b/package.xml @@ -0,0 +1,77 @@ + + + JWT + pear.php.net + A JWT encoder/decoder. + A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Support several key types including JWK + + 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. + + + + diff --git a/src/JWK.php b/src/JWK.php new file mode 100644 index 00000000..8eecb73f --- /dev/null +++ b/src/JWK.php @@ -0,0 +1,158 @@ + + * @license http://opensource.org/licenses/BSD-3-Clause 3-clause BSD + * @link https://github.com/fproject/php-jwt + */ +class JWK +{ + /** + * Parse a set of JWK keys + * @param $source + * @return array an associative array represents the set of keys + */ + public static function parseKeySet($source) + { + $keys = []; + if (is_string($source)) { + $source = json_decode($source, true); + } else if (is_object($source)) { + if (property_exists($source, 'keys')) + $source = (array)$source; + else + $source = [$source]; + } + + if (is_array($source)) { + if (isset($source['keys'])) + $source = $source['keys']; + + foreach ($source as $k => $v) { + if (!is_string($k)) { + if (is_array($v) && isset($v['kid'])) + $k = $v['kid']; + elseif (is_object($v) && property_exists($v, 'kid')) + $k = $v->{'kid'}; + } + try { + $v = self::parseKey($v); + $keys[$k] = $v; + } catch (UnexpectedValueException $e) { + //Do nothing + } + } + } + if (0 < count($keys)) { + return $keys; + } + throw new UnexpectedValueException('Failed to parse JWK'); + } + + /** + * Parse a JWK key + * @param $source + * @return resource|array an associative array represents the key + */ + public static function parseKey($source) + { + if (!is_array($source)) + $source = (array)$source; + if (!empty($source) && isset($source['kty']) && isset($source['n']) && isset($source['e'])) { + switch ($source['kty']) { + case 'RSA': + if (array_key_exists('d', $source)) + throw new UnexpectedValueException('Failed to parse JWK: RSA private key is not supported'); + + $pem = self::createPemFromModulusAndExponent($source['n'], $source['e']); + $pKey = openssl_pkey_get_public($pem); + if ($pKey !== false) + return $pKey; + break; + default: + //Currently only RSA is supported + break; + } + } + + throw new UnexpectedValueException('Failed to parse JWK'); + } + + /** + * + * 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 + */ + 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. + * + * @access private + * @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); + } + +} \ No newline at end of file diff --git a/src/JWT.php b/src/JWT.php index 22a67e32..8a54ebd0 100644 --- a/src/JWT.php +++ b/src/JWT.php @@ -23,8 +23,9 @@ class JWT { /** - * When checking nbf, iat or expiration times, - * we want to provide some extra leeway time to + * The server leeway time in seconds, to aware the acceptable different time between clocks + * of token issued server and relying parties. + * When checking nbf, iat or expiration times, we want to provide some extra leeway time to * account for clock skew. */ public static $leeway = 0; diff --git a/tests/JWTTest.php b/tests/JWTTest.php index 804a3769..f83adff4 100644 --- a/tests/JWTTest.php +++ b/tests/JWTTest.php @@ -23,6 +23,40 @@ public function testDecodeFromPython() ); } + public function testDecodeByJWKKeySetTokenExpired() + { + $jsKey = '{"keys":[{"kty":"RSA","e":"AQAB","use":"sig","kid":"s1","n":"kWp2zRA23Z3vTL4uoe8kTFptxBVFunIoP4t_8TDYJrOb7D1iZNDXVeEsYKp6ppmrTZDAgd-cNOTKLd4M39WJc5FN0maTAVKJc7NxklDeKc4dMe1BGvTZNG4MpWBo-taKULlYUu0ltYJuLzOjIrTHfarucrGoRWqM0sl3z2-fv9k"}]}'; + $key = JWK::parseKeySet($jsKey); + + $msg = 'eyJraWQiOiJzMSIsImFsZyI6IlJTMjU2In0.eyJzY3AiOlsib3BlbmlkIiwiZW1haWwiLCJwcm9maWxlIiwiYWFzIl0sInN1YiI6InRVQ1l0bmZJQlBXY3JTSmY0eUJmdk4xa3d3NEtHY3kzTElQazFHVnpzRTAiLCJjbG0iOlsiITV2OEgiXSwiaXNzIjoiaHR0cDpcL1wvMTMwLjIxMS4yNDMuMTE0OjgwODBcL2MyaWQiLCJleHAiOjE0NDExMjY1MzksInVpcCI6eyJncm91cHMiOlsiYWRtaW4iLCJhdWRpdCJdfSwiY2lkIjoicGstb2lkYy0wMSJ9.PvYrnf3k1Z0wgRwCgq0WXKaoIv1hHtzBFO5cGfCs6bl4suc6ilwCWmJqRxGYkU2fNTGyMOt3OUnnBEwl6v5qN6jv7zbkVAVKVvbQLxhHC2nXe3izvoCiVaMEH6hE7VTWwnPbX_qO72mCwTizHTJTZGLOsyXLYM6ctdOMf7sFPTI'; + $this->setExpectedException('Firebase\JWT\ExpiredException'); + JWT::decode($msg, $key, array('RS256')); + } + + public function testDecodeByJWKKeySet() + { + $jsKey = '{"keys":[{"kty":"RSA","e":"AQAB","use":"sig","kid":"s1","n":"kWp2zRA23Z3vTL4uoe8kTFptxBVFunIoP4t_8TDYJrOb7D1iZNDXVeEsYKp6ppmrTZDAgd-cNOTKLd4M39WJc5FN0maTAVKJc7NxklDeKc4dMe1BGvTZNG4MpWBo-taKULlYUu0ltYJuLzOjIrTHfarucrGoRWqM0sl3z2-fv9k"}]}'; + $key = JWK::parseKeySet($jsKey); + + $msg = 'eyJraWQiOiJzMSIsImFsZyI6IlJTMjU2In0.eyJzY3AiOlsib3BlbmlkIiwiZW1haWwiLCJwcm9maWxlIiwiYWFzIl0sInN1YiI6InRVQ1l0bmZJQlBXY3JTSmY0eUJmdk4xa3d3NEtHY3kzTElQazFHVnpzRTAiLCJjbG0iOlsiITV2OEgiXSwiaXNzIjoiaHR0cDpcL1wvMTMwLjIxMS4yNDMuMTE0OjgwODBcL2MyaWQiLCJleHAiOjE0NDExMjY1MzksInVpcCI6eyJncm91cHMiOlsiYWRtaW4iLCJhdWRpdCJdfSwiY2lkIjoicGstb2lkYy0wMSJ9.PvYrnf3k1Z0wgRwCgq0WXKaoIv1hHtzBFO5cGfCs6bl4suc6ilwCWmJqRxGYkU2fNTGyMOt3OUnnBEwl6v5qN6jv7zbkVAVKVvbQLxhHC2nXe3izvoCiVaMEH6hE7VTWwnPbX_qO72mCwTizHTJTZGLOsyXLYM6ctdOMf7sFPTI'; + $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() + { + $jsKey = '{"keys":[{"kty":"RSA","e":"AQAB","use":"sig","kid":"CXup","n":"hrwD-lc-IwzwidCANmy4qsiZk11yp9kHykOuP0yOnwi36VomYTQVEzZXgh2sDJpGgAutdQudgwLoV8tVSsTG9SQHgJjH9Pd_9V4Ab6PANyZNG6DSeiq1QfiFlEP6Obt0JbRB3W7X2vkxOVaNoWrYskZodxU2V0ogeVL_LkcCGAyNu2jdx3j0DjJatNVk7ystNxb9RfHhJGgpiIkO5S3QiSIVhbBKaJHcZHPF1vq9g0JMGuUCI-OTSVg6XBkTLEGw1C_R73WD_oVEBfdXbXnLukoLHBS11p3OxU7f4rfxA_f_72_UwmWGJnsqS3iahbms3FkvqoL9x_Vj3GhuJSf97Q"},{"kty":"EC","use":"sig","crv":"P-256","kid":"yGvt","x":"pvgdqM3RCshljmuCF1D2Ez1w5ei5k7-bpimWLPNeEHI","y":"JSmUhbUTqiFclVLEdw6dz038F7Whw4URobjXbAReDuM"},{"kty":"EC","use":"sig","crv":"P-384","kid":"9nHY","x":"JPKhjhE0Bj579Mgj3Cn3ERGA8fKVYoGOaV9BPKhtnEobphf8w4GSeigMesL-038W","y":"UbJa1QRX7fo9LxSlh7FOH5ABT5lEtiQeQUcX9BW0bpJFlEVGqwec80tYLdOIl59M"},{"kty":"EC","use":"sig","crv":"P-521","kid":"tVzS","x":"AZgkRHlIyNQJlPIwTWdHqouw41k9dS3GJO04BDEnJnd_Dd1owlCn9SMXA-JuXINn4slwbG4wcECbctXb2cvdGtmn","y":"AdBC6N9lpupzfzcIY3JLIuc8y8MnzV-ItmzHQcC5lYWMTbuM9NU_FlvINeVo8g6i4YZms2xFB-B0VVdaoF9kUswC"}]}'; + $key = JWK::parseKeySet($jsKey); + + $msg = 'eyJraWQiOiJDWHVwIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJmOGI2N2NjNDYwMzA3NzdlZmQ4YmNlNmMxYmZlMjljNmMwZjgxOGVjIiwic2NwIjpbIm9wZW5pZCIsIm5hbWUiLCJwcm9maWxlIiwicGljdHVyZSIsImVtYWlsIiwicnMtcGstbWFpbiIsInJzLXBrLXNvIiwicnMtcGstaXNzdWUiLCJycy1way13ZWIiXSwiY2xtIjpbIiE1djhIIl0sImlzcyI6Imh0dHBzOlwvXC9pZC5wcm9qZWN0a2l0Lm5ldFwvYXV0aGVudGljYXRlIiwiZXhwIjoxNDkyMjI4MzM2LCJpYXQiOjE0OTEzNjQzMzYsImNpZCI6ImNpZC1way13ZWIifQ.KW1K-72bMtiNwvyYBgffG6VaG6I59cELGYQR8M2q7HA8dmzliu6QREJrqyPtwW_rDJZbsD3eylvkRinK9tlsMXCOfEJbxLdAC9b4LKOsnsbuXXwsJHWkFG0a7osdW0ZpXJDoMFlO1aosxRGMkaqhf1wIkvQ5PM_EB08LJv7oz64Antn5bYaoajwgvJRl7ChatRDn9Sx5UIElKD1BK4Uw5WdrZwBlWdWZVNCSFhy4F6SdZvi3OBlXzluDwq61RC-pl2iivilJNljYWVrthHDS1xdtaVz4oteHW13-IS7NNEz6PVnzo5nyoPWMAB4JlRnxcfOFTTUqOA2mX5Csg0UpdQ'; + $this->setExpectedException('Firebase\JWT\ExpiredException'); + $payload = JWT::decode($msg, $key, array('RS256')); + $this->assertEquals("f8b67cc46030777efd8bce6c1bfe29c6c0f818ec",$payload->sub); + $this->assertEquals(1492228336,$payload->exp); + } + public function testUrlSafeCharacters() { $encoded = JWT::encode('f?', 'a'); @@ -277,7 +311,7 @@ public function testInvalidSignatureEncoding() public function testVerifyError() { $this->setExpectedException('DomainException'); - $pkey = openssl_pkey_new(); + $pkey = openssl_pkey_new(array('private_key_bits' => 1024)); $msg = JWT::encode('abc', $pkey, 'RS256'); self::$opensslVerifyReturnValue = -1; JWT::decode($msg, $pkey, array('RS256'));