diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 50d8a5f0..873eae24 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", "8.1"] + php: [ "8.0", "8.1"] name: PHP ${{matrix.php }} Unit Test steps: - uses: actions/checkout@v2 @@ -24,45 +24,9 @@ jobs: timeout_minutes: 10 max_attempts: 3 command: composer install - - if: ${{ matrix.php == '5.6' }} - run: composer require --dev --with-dependencies paragonie/sodium_compat - name: Run Script run: vendor/bin/phpunit - # use dockerfiles for old versions of php (setup-php times out for those). - test_php55: - name: "PHP 5.5 Unit Test" - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v2 - - name: Run Unit Tests - uses: docker://php:5.5-cli - with: - entrypoint: ./.github/actions/entrypoint.sh - - test_php54: - name: "PHP 5.4 Unit Test" - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v2 - - name: Run Unit Tests - uses: docker://php:5.4-cli - with: - entrypoint: ./.github/actions/entrypoint.sh - - test_php53: - name: "PHP 5.3 Unit Test" - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v2 - - name: Run Unit Tests - uses: docker://tomsowerby/php-5.3:cli - with: - entrypoint: ./.github/actions/entrypoint.sh - style: runs-on: ubuntu-latest name: PHP Style Check @@ -71,7 +35,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: "7.0" + php-version: "8.0" - name: Run Script run: | composer require friendsofphp/php-cs-fixer diff --git a/composer.json b/composer.json index 6146e2dc..4e190ea3 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,7 @@ ], "license": "BSD-3-Clause", "require": { - "php": ">=5.3.0" + "php": "^8.0" }, "suggest": { "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" @@ -31,6 +31,6 @@ } }, "require-dev": { - "phpunit/phpunit": ">=4.8 <=9" + "phpunit/phpunit": "^9.5" } } diff --git a/src/JWK.php b/src/JWK.php index c5506548..5663c948 100644 --- a/src/JWK.php +++ b/src/JWK.php @@ -33,13 +33,14 @@ class JWK * * @uses parseKey */ - public static function parseKeySet(array $jwks) + public static function parseKeySet(array $jwks): array { - $keys = array(); + $keys = []; if (!isset($jwks['keys'])) { throw new UnexpectedValueException('"keys" member must exist in the JWK Set'); } + if (empty($jwks['keys'])) { throw new InvalidArgumentException('JWK Set did not contain any keys'); } @@ -71,14 +72,16 @@ public static function parseKeySet(array $jwks) * * @uses createPemFromModulusAndExponent */ - public static function parseKey(array $jwk) + public static function parseKey(array $jwk): ?Key { if (empty($jwk)) { throw new InvalidArgumentException('JWK must not be empty'); } + if (!isset($jwk['kty'])) { throw new UnexpectedValueException('JWK must contain a "kty" parameter'); } + if (!isset($jwk['alg'])) { // The "alg" parameter is optional in a KTY, but is required for parsing in // this library. Add it manually to your JWK array if it doesn't already exist. @@ -107,6 +110,8 @@ public static function parseKey(array $jwk) // Currently only RSA is supported break; } + + return null; } /** @@ -124,10 +129,10 @@ private static function createPemFromModulusAndExponent($n, $e) $modulus = JWT::urlsafeB64Decode($n); $publicExponent = JWT::urlsafeB64Decode($e); - $components = array( + $components = [ 'modulus' => \pack('Ca*a*', 2, self::encodeLength(\strlen($modulus)), $modulus), 'publicExponent' => \pack('Ca*a*', 2, self::encodeLength(\strlen($publicExponent)), $publicExponent) - ); + ]; $rsaPublicKey = \pack( 'Ca*a*a*', diff --git a/src/JWT.php b/src/JWT.php index 6130c59c..f5852dcd 100644 --- a/src/JWT.php +++ b/src/JWT.php @@ -9,6 +9,7 @@ use OpenSSLAsymmetricKey; use UnexpectedValueException; use DateTime; +use stdClass; /** * JSON Web Token implementation, based on this spec: @@ -25,51 +26,40 @@ */ class JWT { - // const ASN1_INTEGER = 0x02; - // const ASN1_SEQUENCE = 0x10; - // const ASN1_BIT_STRING = 0x03; - private static $asn1Integer = 0x02; - private static $asn1Sequence = 0x10; - private static $asn1BitString = 0x03; + private const ASN1_INTEGER = 0x02; + private const ASN1_SEQUENCE = 0x10; + private const ASN1_BIT_STRING = 0x03; /** * When checking nbf, iat or expiration times, * we want to provide some extra leeway time to * account for clock skew. */ - public static $leeway = 0; - - /** - * Allow the current timestamp to be specified. - * Useful for fixing a value within unit testing. - * - * Will default to PHP time() value if null. - */ - public static $timestamp = null; - - public static $supported_algs = array( - 'ES384' => array('openssl', 'SHA384'), - 'ES256' => array('openssl', 'SHA256'), - 'HS256' => array('hash_hmac', 'SHA256'), - 'HS384' => array('hash_hmac', 'SHA384'), - 'HS512' => array('hash_hmac', 'SHA512'), - 'RS256' => array('openssl', 'SHA256'), - 'RS384' => array('openssl', 'SHA384'), - 'RS512' => array('openssl', 'SHA512'), - 'EdDSA' => array('sodium_crypto', 'EdDSA'), - ); + public static int $leeway = 0; + + public static array $supported_algs = [ + 'ES384' => ['openssl', 'SHA384'], + 'ES256' => ['openssl', 'SHA256'], + 'HS256' => ['hash_hmac', 'SHA256'], + 'HS384' => ['hash_hmac', 'SHA384'], + 'HS512' => ['hash_hmac', 'SHA512'], + 'RS256' => ['openssl', 'SHA256'], + 'RS384' => ['openssl', 'SHA384'], + 'RS512' => ['openssl', 'SHA512'], + 'EdDSA' => ['sodium_crypto', 'EdDSA'], + ]; /** * Decodes a JWT string into a PHP object. * - * @param string $jwt The JWT - * @param Key|array $keyOrKeyArray The Key or associative array of key IDs (kid) to Key objects. - * If the algorithm used is asymmetric, this is the public key - * Each Key object contains an algorithm and matching key. - * Supported algorithms are 'ES384','ES256', 'HS256', 'HS384', - * 'HS512', 'RS256', 'RS384', and 'RS512' + * @param string $jwt The JWT + * @param Key|array $keyOrKeyArray The Key or associative array of key IDs (kid) to Key objects. + * If the algorithm used is asymmetric, this is the public key + * Each Key object contains an algorithm and matching key. + * Supported algorithms are 'ES384','ES256', 'HS256', 'HS384', + * 'HS512', 'RS256', 'RS384', and 'RS512' * - * @return object The JWT's payload as a PHP object + * @return stdClass The JWT's payload as a PHP object * * @throws InvalidArgumentException Provided key/key-array was empty * @throws DomainException Provided JWT is malformed @@ -82,10 +72,10 @@ class JWT * @uses jsonDecode * @uses urlsafeB64Decode */ - public static function decode($jwt, $keyOrKeyArray) + public static function decode(string $jwt, Key|array|ArrayAccess $keyOrKeyArray): stdClass { // Validate JWT - $timestamp = \is_null(static::$timestamp) ? \time() : static::$timestamp; + $timestamp = \time(); if (empty($keyOrKeyArray)) { throw new InvalidArgumentException('Key may not be empty'); @@ -101,6 +91,9 @@ public static function decode($jwt, $keyOrKeyArray) if (null === $payload = static::jsonDecode(static::urlsafeB64Decode($bodyb64))) { throw new UnexpectedValueException('Invalid claims encoding'); } + if (!$payload instanceof stdClass) { + throw new UnexpectedValueException('Payload must be a JSON object'); + } if (false === ($sig = static::urlsafeB64Decode($cryptob64))) { throw new UnexpectedValueException('Invalid signature encoding'); } @@ -154,30 +147,32 @@ public static function decode($jwt, $keyOrKeyArray) /** * Converts and signs a PHP object or array into a JWT string. * - * @param object|array $payload PHP object or array - * @param string|resource $key The secret key. - * If the algorithm used is asymmetric, this is the private key - * @param string $alg The signing algorithm. - * Supported algorithms are 'ES384','ES256', 'HS256', 'HS384', - * 'HS512', 'RS256', 'RS384', and 'RS512' - * @param mixed $keyId - * @param array $head An array with header elements to attach + * @param array $payload PHP array + * @param string|OpenSSLAsymmetricKey $key The secret key. + * If the algorithm used is asymmetric, this is the private key + * @param string $keyId + * @param array $head An array with header elements to attach * * @return string A signed JWT * * @uses jsonEncode * @uses urlsafeB64Encode */ - public static function encode($payload, $key, $alg, $keyId = null, $head = null) - { - $header = array('typ' => 'JWT', 'alg' => $alg); + public static function encode( + array $payload, + string|OpenSSLAsymmetricKey $key, + string $alg, + string $keyId = null, + array $head = null + ): string { + $header = ['typ' => 'JWT', 'alg' => $alg]; if ($keyId !== null) { $header['kid'] = $keyId; } if (isset($head) && \is_array($head)) { $header = \array_merge($head, $header); } - $segments = array(); + $segments = []; $segments[] = static::urlsafeB64Encode(static::jsonEncode($header)); $segments[] = static::urlsafeB64Encode(static::jsonEncode($payload)); $signing_input = \implode('.', $segments); @@ -191,17 +186,16 @@ public static function encode($payload, $key, $alg, $keyId = null, $head = null) /** * Sign a string with a given key and algorithm. * - * @param string $msg The message to sign - * @param string|resource $key The secret key - * @param string $alg The signing algorithm. - * Supported algorithms are 'ES384','ES256', 'HS256', 'HS384', - * 'HS512', 'RS256', 'RS384', and 'RS512' + * @param string $msg The message to sign + * @param string|OpenSSLAsymmetricKey $key The secret key. + * @param string $alg Supported algorithms are 'ES384','ES256', 'HS256', 'HS384', + * 'HS512', 'RS256', 'RS384', and 'RS512' * * @return string An encrypted message * * @throws DomainException Unsupported algorithm or bad key was specified */ - public static function sign($msg, $key, $alg) + public static function sign(string $msg, string|OpenSSLAsymmetricKey $key, string $alg): string { if (empty(static::$supported_algs[$alg])) { throw new DomainException('Algorithm not supported'); @@ -241,17 +235,21 @@ public static function sign($msg, $key, $alg) * Verify a signature with the message, key and method. Not all methods * are symmetric, so we must have a separate verify and sign method. * - * @param string $msg The original message (header and body) - * @param string $signature The original signature - * @param string|resource $key For HS*, a string key works. for RS*, must be a resource of an openssl public key - * @param string $alg The algorithm + * @param string $msg The original message (header and body) + * @param string $signature The original signature + * @param string|OpenSSLAsymmetricKey $key For HS*, a string key works. for RS*, must be an instance of OpenSSLAsymmetricKey + * @param string $alg The algorithm * * @return bool * * @throws DomainException Invalid Algorithm, bad key, or OpenSSL failure */ - private static function verify($msg, $signature, $key, $alg) - { + private static function verify( + string $msg, + string $signature, + string|OpenSSLAsymmetricKey $keyMaterial, + string $alg + ): bool { if (empty(static::$supported_algs[$alg])) { throw new DomainException('Algorithm not supported'); } @@ -259,7 +257,7 @@ private static function verify($msg, $signature, $key, $alg) list($function, $algorithm) = static::$supported_algs[$alg]; switch ($function) { case 'openssl': - $success = \openssl_verify($msg, $signature, $key, $algorithm); + $success = \openssl_verify($msg, $signature, $keyMaterial, $algorithm); if ($success === 1) { return true; } elseif ($success === 0) { @@ -275,7 +273,7 @@ private static function verify($msg, $signature, $key, $alg) } try { // The last non-empty line is used as the key. - $lines = array_filter(explode("\n", $key)); + $lines = array_filter(explode("\n", $keyMaterial)); $key = base64_decode(end($lines)); return sodium_crypto_sign_verify_detached($signature, $msg, $key); } catch (Exception $e) { @@ -283,7 +281,7 @@ private static function verify($msg, $signature, $key, $alg) } case 'hash_hmac': default: - $hash = \hash_hmac($algorithm, $msg, $key, true); + $hash = \hash_hmac($algorithm, $msg, $keyMaterial, true); return self::constantTimeEquals($hash, $signature); } } @@ -293,27 +291,13 @@ private static function verify($msg, $signature, $key, $alg) * * @param string $input JSON string * - * @return object Object representation of JSON string + * @return mixed The decoded JSON string * * @throws DomainException Provided string was invalid JSON */ - public static function jsonDecode($input) + public static function jsonDecode(string $input): mixed { - if (\version_compare(PHP_VERSION, '5.4.0', '>=') && !(\defined('JSON_C_VERSION') && PHP_INT_SIZE > 4)) { - /** In PHP >=5.4.0, json_decode() accepts an options parameter, that allows you - * to specify that large ints (like Steam Transaction IDs) should be treated as - * strings, rather than the PHP default behaviour of converting them to floats. - */ - $obj = \json_decode($input, false, 512, JSON_BIGINT_AS_STRING); - } else { - /** Not all servers will support that, however, so for older versions we must - * manually detect large ints in the JSON string and quote them (thus converting - *them to strings) before decoding, hence the preg_replace() call. - */ - $max_int_length = \strlen((string) PHP_INT_MAX) - 1; - $json_without_bigints = \preg_replace('/:\s*(-?\d{'.$max_int_length.',})/', ': "$1"', $input); - $obj = \json_decode($json_without_bigints); - } + $obj = \json_decode($input, false, 512, JSON_BIGINT_AS_STRING); if ($errno = \json_last_error()) { static::handleJsonError($errno); @@ -324,15 +308,15 @@ public static function jsonDecode($input) } /** - * Encode a PHP object into a JSON string. + * Encode a PHP array into a JSON string. * - * @param object|array $input A PHP object or array + * @param array $input A PHP array * - * @return string JSON representation of the PHP object or array + * @return string JSON representation of the PHP array * * @throws DomainException Provided object could not be encoded to valid JSON */ - public static function jsonEncode($input) + public static function jsonEncode(array $input): string { if (PHP_VERSION_ID >= 50400) { $json = \json_encode($input, \JSON_UNESCAPED_SLASHES); @@ -355,7 +339,7 @@ public static function jsonEncode($input) * * @return string A decoded string */ - public static function urlsafeB64Decode($input) + public static function urlsafeB64Decode(string $input): string { $remainder = \strlen($input) % 4; if ($remainder) { @@ -372,7 +356,7 @@ public static function urlsafeB64Decode($input) * * @return string The base64 encode of what you passed in */ - public static function urlsafeB64Encode($input) + public static function urlsafeB64Encode(string $input): string { return \str_replace('=', '', \strtr(\base64_encode($input), '+/', '-_')); } @@ -386,9 +370,9 @@ public static function urlsafeB64Encode($input) * * @throws UnexpectedValueException * - * @return array containing the keyMaterial and algorithm + * @return Key */ - private static function getKey($keyOrKeyArray, $kid = null) + private static function getKey(Key|array|ArrayAccess $keyOrKeyArray, ?string $kid): Key { if ($keyOrKeyArray instanceof Key) { return $keyOrKeyArray; @@ -397,7 +381,7 @@ private static function getKey($keyOrKeyArray, $kid = null) if (is_array($keyOrKeyArray) || $keyOrKeyArray instanceof ArrayAccess) { foreach ($keyOrKeyArray as $keyId => $key) { if (!$key instanceof Key) { - throw new UnexpectedValueException( + throw new TypeError( '$keyOrKeyArray must be an instance of Firebase\JWT\Key key or an ' . 'array of Firebase\JWT\Key keys' ); @@ -424,7 +408,7 @@ private static function getKey($keyOrKeyArray, $kid = null) * @param string $right The user-supplied string * @return bool */ - public static function constantTimeEquals($left, $right) + public static function constantTimeEquals(string $left, string $right): bool { if (\function_exists('hash_equals')) { return \hash_equals($left, $right); @@ -445,17 +429,19 @@ public static function constantTimeEquals($left, $right) * * @param int $errno An error number from json_last_error() * + * @throws DomainException + * * @return void */ - private static function handleJsonError($errno) + private static function handleJsonError(int $errno): void { - $messages = array( + $messages = [ JSON_ERROR_DEPTH => 'Maximum stack depth exceeded', JSON_ERROR_STATE_MISMATCH => 'Invalid or malformed JSON', JSON_ERROR_CTRL_CHAR => 'Unexpected control character found', JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON', JSON_ERROR_UTF8 => 'Malformed UTF-8 characters' //PHP >= 5.3.3 - ); + ]; throw new DomainException( isset($messages[$errno]) ? $messages[$errno] @@ -470,7 +456,7 @@ private static function handleJsonError($errno) * * @return int */ - private static function safeStrlen($str) + private static function safeStrlen(string $str): int { if (\function_exists('mb_strlen')) { return \mb_strlen($str, '8bit'); @@ -484,7 +470,7 @@ private static function safeStrlen($str) * @param string $sig The ECDSA signature to convert * @return string The encoded DER object */ - private static function signatureToDER($sig) + private static function signatureToDER(string $sig): string { // Separate the signature into r-value and s-value list($r, $s) = \str_split($sig, (int) (\strlen($sig) / 2)); @@ -503,9 +489,9 @@ private static function signatureToDER($sig) } return self::encodeDER( - self::$asn1Sequence, - self::encodeDER(self::$asn1Integer, $r) . - self::encodeDER(self::$asn1Integer, $s) + self::ASN1_SEQUENCE, + self::encodeDER(self::ASN1_INTEGER, $r) . + self::encodeDER(self::ASN1_INTEGER, $s) ); } @@ -514,12 +500,13 @@ private static function signatureToDER($sig) * * @param int $type DER tag * @param string $value the value to encode + * * @return string the encoded object */ - private static function encodeDER($type, $value) + private static function encodeDER(int $type, string $value): string { $tag_header = 0; - if ($type === self::$asn1Sequence) { + if ($type === self::ASN1_SEQUENCE) { $tag_header |= 0x20; } @@ -537,9 +524,10 @@ private static function encodeDER($type, $value) * * @param string $der binary signature in DER format * @param int $keySize the number of bits in the key + * * @return string the signature */ - private static function signatureFromDER($der, $keySize) + private static function signatureFromDER(string $der, int $keySize): string { // OpenSSL returns the ECDSA signatures as a binary ASN.1 DER SEQUENCE list($offset, $_) = self::readDER($der); @@ -564,9 +552,10 @@ private static function signatureFromDER($der, $keySize) * @param string $der the binary data in DER format * @param int $offset the offset of the data stream containing the object * to decode + * * @return array [$offset, $data] the new offset and the decoded object */ - private static function readDER($der, $offset = 0) + private static function readDER(string $der, int $offset = 0): array { $pos = $offset; $size = \strlen($der); @@ -584,7 +573,7 @@ private static function readDER($der, $offset = 0) } // Value - if ($type == self::$asn1BitString) { + if ($type == self::ASN1_BIT_STRING) { $pos++; // Skip the first contents octet (padding indicator) $data = \substr($der, $pos, $len - 1); $pos += $len - 1; @@ -595,6 +584,6 @@ private static function readDER($der, $offset = 0) $data = null; } - return array($pos, $data); + return [$pos, $data]; } } diff --git a/src/Key.php b/src/Key.php index f1ede6f2..286a3143 100644 --- a/src/Key.php +++ b/src/Key.php @@ -2,41 +2,34 @@ namespace Firebase\JWT; -use InvalidArgumentException; use OpenSSLAsymmetricKey; +use TypeError; +use InvalidArgumentException; class Key { - /** @var string $algorithm */ - private $algorithm; - - /** @var string|resource|OpenSSLAsymmetricKey $keyMaterial */ - private $keyMaterial; - /** - * @param string|resource|OpenSSLAsymmetricKey $keyMaterial + * @param string|OpenSSLAsymmetricKey $keyMaterial * @param string $algorithm */ - public function __construct($keyMaterial, $algorithm) - { + public function __construct( + private string|OpenSSLAsymmetricKey $keyMaterial, + private string $algorithm + ) { if ( !is_string($keyMaterial) - && !is_resource($keyMaterial) && !$keyMaterial instanceof OpenSSLAsymmetricKey ) { - throw new InvalidArgumentException('Type error: $keyMaterial must be a string, resource, or OpenSSLAsymmetricKey'); + throw new TypeError('Key material must be a string or OpenSSLAsymmetricKey'); } if (empty($keyMaterial)) { - throw new InvalidArgumentException('Type error: $keyMaterial must not be empty'); + throw new InvalidArgumentException('Key material must not be empty'); } - if (!is_string($algorithm)|| empty($keyMaterial)) { - throw new InvalidArgumentException('Type error: $algorithm must be a string'); + if (empty($algorithm)) { + throw new InvalidArgumentException('Algorithm must not be empty'); } - - $this->keyMaterial = $keyMaterial; - $this->algorithm = $algorithm; } /** @@ -44,15 +37,15 @@ public function __construct($keyMaterial, $algorithm) * * @return string */ - public function getAlgorithm() + public function getAlgorithm(): string { return $this->algorithm; } /** - * @return string|resource|OpenSSLAsymmetricKey + * @return string|OpenSSLAsymmetricKey */ - public function getKeyMaterial() + public function getKeyMaterial(): mixed { return $this->keyMaterial; } diff --git a/tests/JWKTest.php b/tests/JWKTest.php index c580f40f..d80f6c4b 100644 --- a/tests/JWKTest.php +++ b/tests/JWKTest.php @@ -3,6 +3,8 @@ namespace Firebase\JWT; use PHPUnit\Framework\TestCase; +use InvalidArgumentException; +use UnexpectedValueException; class JWKTest extends TestCase { @@ -13,29 +15,29 @@ class JWKTest extends TestCase public function testMissingKty() { $this->setExpectedException( - 'UnexpectedValueException', + UnexpectedValueException::class, 'JWK must contain a "kty" parameter' ); - $badJwk = array('kid' => 'foo'); - $keys = JWK::parseKeySet(array('keys' => array($badJwk))); + $badJwk = ['kid' => 'foo']; + $keys = JWK::parseKeySet(['keys' => [$badJwk]]); } public function testInvalidAlgorithm() { $this->setExpectedException( - 'UnexpectedValueException', + UnexpectedValueException::class, 'No supported algorithms found in JWK Set' ); - $badJwk = array('kty' => 'BADALG', 'alg' => 'RSA256'); - $keys = JWK::parseKeySet(array('keys' => array($badJwk))); + $badJwk = ['kty' => 'BADTYPE', 'alg' => 'RSA256']; + $keys = JWK::parseKeySet(['keys' => [$badJwk]]); } public function testParsePrivateKey() { $this->setExpectedException( - 'UnexpectedValueException', + UnexpectedValueException::class, 'RSA private keys are not supported' ); @@ -51,7 +53,7 @@ public function testParsePrivateKey() public function testParsePrivateKeyWithoutAlg() { $this->setExpectedException( - 'UnexpectedValueException', + UnexpectedValueException::class, 'JWK must contain an "alg" parameter' ); @@ -92,16 +94,16 @@ public function testParseJwkKeySet() public function testParseJwkKey_empty() { - $this->setExpectedException('InvalidArgumentException', 'JWK must not be empty'); + $this->setExpectedException(InvalidArgumentException::class, 'JWK must not be empty'); - JWK::parseKeySet(array('keys' => array(array()))); + JWK::parseKeySet(['keys' => [[]]]); } public function testParseJwkKeySet_empty() { - $this->setExpectedException('InvalidArgumentException', 'JWK Set did not contain any keys'); + $this->setExpectedException(InvalidArgumentException::class, 'JWK Set did not contain any keys'); - JWK::parseKeySet(array('keys' => array())); + JWK::parseKeySet(['keys' => []]); } /** @@ -110,12 +112,12 @@ public function testParseJwkKeySet_empty() public function testDecodeByJwkKeySetTokenExpired() { $privKey1 = file_get_contents(__DIR__ . '/data/rsa1-private.pem'); - $payload = array('exp' => strtotime('-1 hour')); + $payload = ['exp' => strtotime('-1 hour')]; $msg = JWT::encode($payload, $privKey1, 'RS256', 'jwk1'); - $this->setExpectedException('Firebase\JWT\ExpiredException'); + $this->setExpectedException(ExpiredException::class); - JWT::decode($msg, self::$keys, array('RS256')); + JWT::decode($msg, self::$keys); } /** @@ -124,10 +126,10 @@ public function testDecodeByJwkKeySetTokenExpired() public function testDecodeByJwkKeySet() { $privKey1 = file_get_contents(__DIR__ . '/data/rsa1-private.pem'); - $payload = array('sub' => 'foo', 'exp' => strtotime('+10 seconds')); + $payload = ['sub' => 'foo', 'exp' => strtotime('+10 seconds')]; $msg = JWT::encode($payload, $privKey1, 'RS256', 'jwk1'); - $result = JWT::decode($msg, self::$keys, array('RS256')); + $result = JWT::decode($msg, self::$keys); $this->assertEquals("foo", $result->sub); } @@ -138,10 +140,10 @@ public function testDecodeByJwkKeySet() public function testDecodeByMultiJwkKeySet() { $privKey2 = file_get_contents(__DIR__ . '/data/rsa2-private.pem'); - $payload = array('sub' => 'bar', 'exp' => strtotime('+10 seconds')); + $payload = ['sub' => 'bar', 'exp' => strtotime('+10 seconds')]; $msg = JWT::encode($payload, $privKey2, 'RS256', 'jwk2'); - $result = JWT::decode($msg, self::$keys, array('RS256')); + $result = JWT::decode($msg, self::$keys); $this->assertEquals("bar", $result->sub); } diff --git a/tests/JWTTest.php b/tests/JWTTest.php index 36e2095e..aa5ce140 100644 --- a/tests/JWTTest.php +++ b/tests/JWTTest.php @@ -4,6 +4,10 @@ use ArrayObject; use PHPUnit\Framework\TestCase; +use DomainException; +use InvalidArgumentException; +use UnexpectedValueException; +use stdClass; class JWTTest extends TestCase { @@ -19,68 +23,61 @@ public function setExpectedException($exceptionName, $message = '', $code = null } } - public function testDecodeFromPython() - { - $msg = 'eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.Iio6aHR0cDovL2FwcGxpY2F0aW9uL2NsaWNreT9ibGFoPTEuMjMmZi5vbz00NTYgQUMwMDAgMTIzIg.E_U8X2YpMT5K1cEiT_3-IvBYfrdIFIeVYeOqre_Z5Cg'; - $this->assertEquals( - JWT::decode($msg, new Key('my_key', 'HS256')), - '*:http://application/clicky?blah=1.23&f.oo=456 AC000 123' - ); - } - public function testUrlSafeCharacters() { - $encoded = JWT::encode('f?', 'a', 'HS256'); - $this->assertEquals('f?', JWT::decode($encoded, new Key('a', 'HS256'))); + $encoded = JWT::encode(['message' => 'f?'], 'a', 'HS256'); + $expected = new stdClass(); + $expected->message = 'f?'; + $this->assertEquals($expected, JWT::decode($encoded, new Key('a', 'HS256'))); } public function testMalformedUtf8StringsFail() { - $this->setExpectedException('DomainException'); - JWT::encode(pack('c', 128), 'a', 'HS256'); + $this->setExpectedException(DomainException::class); + JWT::encode(['message' => pack('c', 128)], 'a', 'HS256'); } public function testMalformedJsonThrowsException() { - $this->setExpectedException('DomainException'); + $this->setExpectedException(DomainException::class); JWT::jsonDecode('this is not valid JSON string'); } public function testExpiredToken() { - $this->setExpectedException('Firebase\JWT\ExpiredException'); - $payload = array( + $this->setExpectedException(ExpiredException::class); + $payload = [ "message" => "abc", - "exp" => time() - 20); // time in the past + "exp" => time() - 20]; // time in the past $encoded = JWT::encode($payload, 'my_key', 'HS256'); JWT::decode($encoded, new Key('my_key', 'HS256')); } public function testBeforeValidTokenWithNbf() { - $this->setExpectedException('Firebase\JWT\BeforeValidException'); - $payload = array( + $this->setExpectedException(BeforeValidException::class); + $payload = [ "message" => "abc", - "nbf" => time() + 20); // time in the future + "nbf" => time() + 20]; // time in the future $encoded = JWT::encode($payload, 'my_key', 'HS256'); JWT::decode($encoded, new Key('my_key', 'HS256')); } public function testBeforeValidTokenWithIat() { - $this->setExpectedException('Firebase\JWT\BeforeValidException'); - $payload = array( + $this->setExpectedException(BeforeValidException::class); + $payload = [ "message" => "abc", - "iat" => time() + 20); // time in the future + "iat" => time() + 20]; // time in the future $encoded = JWT::encode($payload, 'my_key', 'HS256'); JWT::decode($encoded, new Key('my_key', 'HS256')); } public function testValidToken() { - $payload = array( + $payload = [ "message" => "abc", - "exp" => time() + JWT::$leeway + 20); // time in the future + "exp" => time() + JWT::$leeway + 20]; // time in the future $encoded = JWT::encode($payload, 'my_key', 'HS256'); $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); $this->assertEquals($decoded->message, 'abc'); @@ -89,9 +86,9 @@ public function testValidToken() public function testValidTokenWithLeeway() { JWT::$leeway = 60; - $payload = array( + $payload = [ "message" => "abc", - "exp" => time() - 20); // time in the past + "exp" => time() - 20]; // time in the past $encoded = JWT::encode($payload, 'my_key', 'HS256'); $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); $this->assertEquals($decoded->message, 'abc'); @@ -101,10 +98,10 @@ public function testValidTokenWithLeeway() public function testExpiredTokenWithLeeway() { JWT::$leeway = 60; - $payload = array( + $payload = [ "message" => "abc", - "exp" => time() - 70); // time far in the past - $this->setExpectedException('Firebase\JWT\ExpiredException'); + "exp" => time() - 70]; // time far in the past + $this->setExpectedException(ExpiredException::class); $encoded = JWT::encode($payload, 'my_key', 'HS256'); $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); $this->assertEquals($decoded->message, 'abc'); @@ -113,11 +110,11 @@ public function testExpiredTokenWithLeeway() public function testValidTokenWithNbf() { - $payload = array( + $payload = [ "message" => "abc", "iat" => time(), "exp" => time() + 20, // time in the future - "nbf" => time() - 20); + "nbf" => time() - 20]; $encoded = JWT::encode($payload, 'my_key', 'HS256'); $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); $this->assertEquals($decoded->message, 'abc'); @@ -126,9 +123,9 @@ public function testValidTokenWithNbf() public function testValidTokenWithNbfLeeway() { JWT::$leeway = 60; - $payload = array( + $payload = [ "message" => "abc", - "nbf" => time() + 20); // not before in near (leeway) future + "nbf" => time() + 20]; // not before in near (leeway) future $encoded = JWT::encode($payload, 'my_key', 'HS256'); $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); $this->assertEquals($decoded->message, 'abc'); @@ -138,11 +135,11 @@ public function testValidTokenWithNbfLeeway() public function testInvalidTokenWithNbfLeeway() { JWT::$leeway = 60; - $payload = array( + $payload = [ "message" => "abc", - "nbf" => time() + 65); // not before too far in future + "nbf" => time() + 65]; // not before too far in future $encoded = JWT::encode($payload, 'my_key', 'HS256'); - $this->setExpectedException('Firebase\JWT\BeforeValidException'); + $this->setExpectedException(BeforeValidException::class); JWT::decode($encoded, new Key('my_key', 'HS256')); JWT::$leeway = 0; } @@ -150,9 +147,9 @@ public function testInvalidTokenWithNbfLeeway() public function testValidTokenWithIatLeeway() { JWT::$leeway = 60; - $payload = array( + $payload = [ "message" => "abc", - "iat" => time() + 20); // issued in near (leeway) future + "iat" => time() + 20]; // issued in near (leeway) future $encoded = JWT::encode($payload, 'my_key', 'HS256'); $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); $this->assertEquals($decoded->message, 'abc'); @@ -162,123 +159,133 @@ public function testValidTokenWithIatLeeway() public function testInvalidTokenWithIatLeeway() { JWT::$leeway = 60; - $payload = array( + $payload = [ "message" => "abc", - "iat" => time() + 65); // issued too far in future + "iat" => time() + 65]; // issued too far in future $encoded = JWT::encode($payload, 'my_key', 'HS256'); - $this->setExpectedException('Firebase\JWT\BeforeValidException'); + $this->setExpectedException(BeforeValidException::class); JWT::decode($encoded, new Key('my_key', 'HS256')); JWT::$leeway = 0; } public function testInvalidToken() { - $payload = array( + $payload = [ "message" => "abc", - "exp" => time() + 20); // time in the future + "exp" => time() + 20]; // time in the future $encoded = JWT::encode($payload, 'my_key', 'HS256'); - $this->setExpectedException('Firebase\JWT\SignatureInvalidException'); + $this->setExpectedException(SignatureInvalidException::class); JWT::decode($encoded, new Key('my_key2', 'HS256')); } public function testNullKeyFails() { - $payload = array( + $payload = [ "message" => "abc", - "exp" => time() + JWT::$leeway + 20); // time in the future + "exp" => time() + JWT::$leeway + 20]; // time in the future $encoded = JWT::encode($payload, 'my_key', 'HS256'); - $this->setExpectedException('InvalidArgumentException'); + $this->setExpectedException('TypeError'); JWT::decode($encoded, new Key(null, 'HS256')); } public function testEmptyKeyFails() { - $payload = array( + $payload = [ "message" => "abc", - "exp" => time() + JWT::$leeway + 20); // time in the future + "exp" => time() + JWT::$leeway + 20]; // time in the future $encoded = JWT::encode($payload, 'my_key', 'HS256'); - $this->setExpectedException('InvalidArgumentException'); + $this->setExpectedException(InvalidArgumentException::class); JWT::decode($encoded, new Key('', 'HS256')); } public function testKIDChooser() { - $keys = array( + $keys = [ '1' => new Key('my_key', 'HS256'), '2' => new Key('my_key2', 'HS256') - ); - $msg = JWT::encode('abc', $keys['1']->getKeyMaterial(), 'HS256', '1'); + ]; + $msg = JWT::encode(['message' => 'abc'], $keys['1']->getKeyMaterial(), 'HS256', '1'); $decoded = JWT::decode($msg, $keys); - $this->assertEquals($decoded, 'abc'); + $expected = new stdClass(); + $expected->message = 'abc'; + $this->assertEquals($decoded, $expected); } public function testArrayAccessKIDChooser() { - $keys = new ArrayObject(array( + $keys = new ArrayObject([ '1' => new Key('my_key', 'HS256'), '2' => new Key('my_key2', 'HS256'), - )); - $msg = JWT::encode('abc', $keys['1']->getKeyMaterial(), 'HS256', '1'); + ]); + $msg = JWT::encode(['message' => 'abc'], $keys['1']->getKeyMaterial(), 'HS256', '1'); $decoded = JWT::decode($msg, $keys); - $this->assertEquals($decoded, 'abc'); + $expected = new stdClass(); + $expected->message = 'abc'; + $this->assertEquals($decoded, $expected); } public function testNoneAlgorithm() { - $msg = JWT::encode('abc', 'my_key', 'HS256'); - $this->setExpectedException('UnexpectedValueException'); + $msg = JWT::encode(['message' => 'abc'], 'my_key', 'HS256'); + $this->setExpectedException(UnexpectedValueException::class); JWT::decode($msg, new Key('my_key', 'none')); } public function testIncorrectAlgorithm() { - $msg = JWT::encode('abc', 'my_key', 'HS256'); - $this->setExpectedException('UnexpectedValueException'); + $msg = JWT::encode(['message' => 'abc'], 'my_key', 'HS256'); + $this->setExpectedException(UnexpectedValueException::class); JWT::decode($msg, new Key('my_key', 'RS256')); } public function testEmptyAlgorithm() { - $msg = JWT::encode('abc', 'my_key', 'HS256'); - $this->setExpectedException('UnexpectedValueException'); + $msg = JWT::encode(['message' => 'abc'], 'my_key', 'HS256'); + $this->setExpectedException(InvalidArgumentException::class); JWT::decode($msg, new Key('my_key', '')); } public function testAdditionalHeaders() { - $msg = JWT::encode('abc', 'my_key', 'HS256', null, array('cty' => 'test-eit;v=1')); - $this->assertEquals(JWT::decode($msg, new Key('my_key', 'HS256')), 'abc'); + $msg = JWT::encode(['message' => 'abc'], 'my_key', 'HS256', null, ['cty' => 'test-eit;v=1']); + $expected = new stdClass(); + $expected->message = 'abc'; + $this->assertEquals(JWT::decode($msg, new Key('my_key', 'HS256')), $expected); } public function testInvalidSegmentCount() { - $this->setExpectedException('UnexpectedValueException'); + $this->setExpectedException(UnexpectedValueException::class); JWT::decode('brokenheader.brokenbody', new Key('my_key', 'HS256')); } public function testInvalidSignatureEncoding() { $msg = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwibmFtZSI6ImZvbyJ9.Q4Kee9E8o0Xfo4ADXvYA8t7dN_X_bU9K5w6tXuiSjlUxx"; - $this->setExpectedException('UnexpectedValueException'); + $this->setExpectedException(UnexpectedValueException::class); JWT::decode($msg, new Key('secret', 'HS256')); } public function testHSEncodeDecode() { - $msg = JWT::encode('abc', 'my_key', 'HS256'); - $this->assertEquals(JWT::decode($msg, new Key('my_key', 'HS256')), 'abc'); + $msg = JWT::encode(['message' => 'abc'], 'my_key', 'HS256'); + $expected = new stdClass(); + $expected->message = 'abc'; + $this->assertEquals(JWT::decode($msg, new Key('my_key', 'HS256')), $expected); } public function testRSEncodeDecode() { - $privKey = openssl_pkey_new(array('digest_alg' => 'sha256', + $privKey = openssl_pkey_new(['digest_alg' => 'sha256', 'private_key_bits' => 1024, - 'private_key_type' => OPENSSL_KEYTYPE_RSA)); - $msg = JWT::encode('abc', $privKey, 'RS256'); + 'private_key_type' => OPENSSL_KEYTYPE_RSA]); + $msg = JWT::encode(['message' => 'abc'], $privKey, 'RS256'); $pubKey = openssl_pkey_get_details($privKey); $pubKey = $pubKey['key']; $decoded = JWT::decode($msg, new Key($pubKey, 'RS256')); - $this->assertEquals($decoded, 'abc'); + $expected = new stdClass(); + $expected->message = 'abc'; + $this->assertEquals($decoded, $expected); } public function testEdDsaEncodeDecode() @@ -286,7 +293,7 @@ public function testEdDsaEncodeDecode() $keyPair = sodium_crypto_sign_keypair(); $privKey = base64_encode(sodium_crypto_sign_secretkey($keyPair)); - $payload = array('foo' => 'bar'); + $payload = ['foo' => 'bar']; $msg = JWT::encode($payload, $privKey, 'EdDSA'); $pubKey = base64_encode(sodium_crypto_sign_publickey($keyPair)); @@ -299,13 +306,13 @@ public function testInvalidEdDsaEncodeDecode() $keyPair = sodium_crypto_sign_keypair(); $privKey = base64_encode(sodium_crypto_sign_secretkey($keyPair)); - $payload = array('foo' => 'bar'); + $payload = ['foo' => 'bar']; $msg = JWT::encode($payload, $privKey, 'EdDSA'); // Generate a different key. $keyPair = sodium_crypto_sign_keypair(); $pubKey = base64_encode(sodium_crypto_sign_publickey($keyPair)); - $this->setExpectedException('Firebase\JWT\SignatureInvalidException'); + $this->setExpectedException(SignatureInvalidException::class); JWT::decode($msg, new Key($pubKey, 'EdDSA')); } @@ -316,11 +323,13 @@ public function testRSEncodeDecodeWithPassphrase() 'passphrase' ); - $jwt = JWT::encode('abc', $privateKey, 'RS256'); + $jwt = JWT::encode(['message' => 'abc'], $privateKey, 'RS256'); $keyDetails = openssl_pkey_get_details($privateKey); $pubKey = $keyDetails['key']; $decoded = JWT::decode($jwt, new Key($pubKey, 'RS256')); - $this->assertEquals($decoded, 'abc'); + $expected = new stdClass(); + $expected->message = 'abc'; + $this->assertEquals($decoded, $expected); } /** @@ -330,7 +339,7 @@ public function testRSEncodeDecodeWithPassphrase() public function testEncodeDecode($privateKeyFile, $publicKeyFile, $alg) { $privateKey = file_get_contents($privateKeyFile); - $payload = array('foo' => 'bar'); + $payload = ['foo' => 'bar']; $encoded = JWT::encode($payload, $privateKey, $alg); // Verify decoding succeeds @@ -342,12 +351,12 @@ public function testEncodeDecode($privateKeyFile, $publicKeyFile, $alg) public function provideEncodeDecode() { - return array( - array(__DIR__ . '/data/ecdsa-private.pem', __DIR__ . '/data/ecdsa-public.pem', 'ES256'), - array(__DIR__ . '/data/ecdsa384-private.pem', __DIR__ . '/data/ecdsa384-public.pem', 'ES384'), - array(__DIR__ . '/data/rsa1-private.pem', __DIR__ . '/data/rsa1-public.pub', 'RS512'), - array(__DIR__ . '/data/ed25519-1.sec', __DIR__ . '/data/ed25519-1.pub', 'EdDSA'), - ); + return [ + [__DIR__ . '/data/ecdsa-private.pem', __DIR__ . '/data/ecdsa-public.pem', 'ES256'], + [__DIR__ . '/data/ecdsa384-private.pem', __DIR__ . '/data/ecdsa384-public.pem', 'ES384'], + [__DIR__ . '/data/rsa1-private.pem', __DIR__ . '/data/rsa1-public.pub', 'RS512'], + [__DIR__ . '/data/ed25519-1.sec', __DIR__ . '/data/ed25519-1.pub', 'EdDSA'], + ]; } public function testEncodeDecodeWithResource() @@ -356,7 +365,7 @@ public function testEncodeDecodeWithResource() $resource = openssl_pkey_get_public($pem); $privateKey = file_get_contents(__DIR__ . '/data/rsa1-private.pem'); - $payload = array('foo' => 'bar'); + $payload = ['foo' => 'bar']; $encoded = JWT::encode($payload, $privateKey, 'RS512'); // Verify decoding succeeds