From 7b7437899cc947037a7444c95a709796abc2fa49 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 10 Nov 2021 16:15:38 -0800 Subject: [PATCH 1/6] WIP: add typing --- src/JWT.php | 142 ++++++++++++++++++++-------------------------- src/Key.php | 27 ++++----- tests/JWTTest.php | 10 ++-- 3 files changed, 79 insertions(+), 100 deletions(-) diff --git a/src/JWT.php b/src/JWT.php index e6038648..bcb8891d 100644 --- a/src/JWT.php +++ b/src/JWT.php @@ -25,39 +25,28 @@ */ 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. @@ -82,10 +71,10 @@ class JWT * @uses jsonDecode * @uses urlsafeB64Decode */ - public static function decode($jwt, $keyOrKeyArray) + public static function decode(string $jwt, Key|array|ArrayAccess $keyOrKeyArray): object|array|string { // Validate JWT - $timestamp = \is_null(static::$timestamp) ? \time() : static::$timestamp; + $timestamp = \time(); if (empty($keyOrKeyArray)) { throw new InvalidArgumentException('Key may not be empty'); @@ -154,22 +143,24 @@ 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 object|array|string $payload PHP object or array + * @param string|resource|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) - { + public static function encode( + object|array|string $payload, + mixed $key, + string $alg, + string $keyId = null, + array $head = null + ): string { $header = array('typ' => 'JWT', 'alg' => $alg); if ($keyId !== null) { $header['kid'] = $keyId; @@ -191,17 +182,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|resource|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, mixed $key, string $alg): string { if (empty(static::$supported_algs[$alg])) { throw new DomainException('Algorithm not supported'); @@ -250,8 +240,12 @@ public static function sign($msg, $key, $alg) * * @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, + mixed $keyMaterial, + string $alg + ): bool { if (empty(static::$supported_algs[$alg])) { throw new DomainException('Algorithm not supported'); } @@ -259,7 +253,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 +269,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 +277,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($signature, $hash); } } @@ -297,23 +291,9 @@ private static function verify($msg, $signature, $key, $alg) * * @throws DomainException Provided string was invalid JSON */ - public static function jsonDecode($input) + public static function jsonDecode(string $input): object|array|string { - if (\version_compare(PHP_VERSION, '5.4.0', '>=') && !(\defined('JSON_C_VERSION') && PHP_INT_SIZE > 4)) { - /** In PHP >=5.4.0, json_decode() accepts an options parameter, that allows you - * to specify that large ints (like Steam Transaction IDs) should be treated as - * strings, rather than the PHP default behaviour of converting them to floats. - */ - $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); @@ -332,7 +312,7 @@ public static function jsonDecode($input) * * @throws DomainException Provided object could not be encoded to valid JSON */ - public static function jsonEncode($input) + public static function jsonEncode(object|array|string $input): string { if (PHP_VERSION_ID >= 50400) { $json = \json_encode($input, \JSON_UNESCAPED_SLASHES); @@ -355,7 +335,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 +352,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), '+/', '-_')); } @@ -381,14 +361,14 @@ public static function urlsafeB64Encode($input) /** * Determine if an algorithm has been provided for each Key * - * @param Key|array|mixed $keyOrKeyArray + * @param Key|array $keyOrKeyArray * @param string|null $kid * * @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 +377,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 +404,7 @@ private static function getKey($keyOrKeyArray, $kid = null) * @param string $right * @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,9 +425,11 @@ 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( JSON_ERROR_DEPTH => 'Maximum stack depth exceeded', @@ -503,9 +485,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) ); } @@ -519,7 +501,7 @@ private static function signatureToDER($sig) private static function encodeDER($type, $value) { $tag_header = 0; - if ($type === self::$asn1Sequence) { + if ($type === self::ASN1_SEQUENCE) { $tag_header |= 0x20; } @@ -584,7 +566,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; diff --git a/src/Key.php b/src/Key.php index f1ede6f2..2c38a2c7 100644 --- a/src/Key.php +++ b/src/Key.php @@ -2,37 +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 $algorithm */ - public function __construct($keyMaterial, $algorithm) - { + public function __construct( + private mixed $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, resource, 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; @@ -44,7 +41,7 @@ public function __construct($keyMaterial, $algorithm) * * @return string */ - public function getAlgorithm() + public function getAlgorithm(): string { return $this->algorithm; } @@ -52,7 +49,7 @@ public function getAlgorithm() /** * @return string|resource|OpenSSLAsymmetricKey */ - public function getKeyMaterial() + public function getKeyMaterial(): mixed { return $this->keyMaterial; } diff --git a/tests/JWTTest.php b/tests/JWTTest.php index 36e2095e..989cae57 100644 --- a/tests/JWTTest.php +++ b/tests/JWTTest.php @@ -37,7 +37,7 @@ public function testUrlSafeCharacters() public function testMalformedUtf8StringsFail() { $this->setExpectedException('DomainException'); - JWT::encode(pack('c', 128), 'a', 'HS256'); + JWT::encode([pack('c', 128)], 'a', 'HS256'); } public function testMalformedJsonThrowsException() @@ -187,7 +187,7 @@ public function testNullKeyFails() "message" => "abc", "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')); } @@ -240,7 +240,7 @@ public function testIncorrectAlgorithm() public function testEmptyAlgorithm() { $msg = JWT::encode('abc', 'my_key', 'HS256'); - $this->setExpectedException('UnexpectedValueException'); + $this->setExpectedException('InvalidArgumentException'); JWT::decode($msg, new Key('my_key', '')); } @@ -316,11 +316,11 @@ public function testRSEncodeDecodeWithPassphrase() 'passphrase' ); - $jwt = JWT::encode('abc', $privateKey, 'RS256'); + $jwt = JWT::encode(['abc'], $privateKey, 'RS256'); $keyDetails = openssl_pkey_get_details($privateKey); $pubKey = $keyDetails['key']; $decoded = JWT::decode($jwt, new Key($pubKey, 'RS256')); - $this->assertEquals($decoded, 'abc'); + $this->assertEquals($decoded, ['abc']); } /** From 27eb9e287668dc8785a4a83db3155fff71a120f6 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Thu, 30 Dec 2021 11:26:17 -0800 Subject: [PATCH 2/6] force PHP 8 --- .github/workflows/tests.yml | 40 ++----------------------------------- composer.json | 4 ++-- 2 files changed, 4 insertions(+), 40 deletions(-) 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" } } From dece732f686fe0fac412c01363e06170340e3ae9 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Thu, 30 Dec 2021 11:54:45 -0800 Subject: [PATCH 3/6] remove support for resource key types --- src/JWK.php | 33 +++++++++++++------------- src/JWT.php | 68 ++++++++++++++++++++++++++++------------------------- src/Key.php | 9 ++++--- 3 files changed, 56 insertions(+), 54 deletions(-) diff --git a/src/JWK.php b/src/JWK.php index c53251d3..9f936b06 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 Key[] An associative array that represents the set of keys * * @throws InvalidArgumentException Provided JWK Set is empty * @throws UnexpectedValueException Provided JWK Set was invalid @@ -33,30 +33,21 @@ 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'); } 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] = self::parseKey($v); } if (0 === \count($keys)) { @@ -71,7 +62,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 @@ -79,15 +70,23 @@ 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. + // @see https://datatracker.ietf.org/doc/html/rfc7517#section-4.4 + throw new UnexpectedValueException('JWK must contain a "alg" parameter'); + } + switch ($jwk['kty']) { case 'RSA': if (!empty($jwk['d'])) { @@ -104,7 +103,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 bcb8891d..491be27b 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: @@ -51,14 +52,14 @@ 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. - * 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|Key[] $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' * - * @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 @@ -71,7 +72,7 @@ class JWT * @uses jsonDecode * @uses urlsafeB64Decode */ - public static function decode(string $jwt, Key|array|ArrayAccess $keyOrKeyArray): object|array|string + public static function decode(string $jwt, Key|array|ArrayAccess $keyOrKeyArray): stdClass { // Validate JWT $timestamp = \time(); @@ -143,8 +144,8 @@ public static function decode(string $jwt, Key|array|ArrayAccess $keyOrKeyArray) /** * Converts and signs a PHP object or array into a JWT string. * - * @param object|array|string $payload PHP object or array - * @param string|resource|OpenSSLAsymmetricKey $key The secret key. + * @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 @@ -155,8 +156,8 @@ public static function decode(string $jwt, Key|array|ArrayAccess $keyOrKeyArray) * @uses urlsafeB64Encode */ public static function encode( - object|array|string $payload, - mixed $key, + array $payload, + string|OpenSSLAsymmetricKey $key, string $alg, string $keyId = null, array $head = null @@ -182,16 +183,16 @@ public static function encode( /** * Sign a string with a given key and algorithm. * - * @param string $msg The message to sign - * @param string|resource|OpenSSLAsymmetricKey $key The secret key. - * @param string $alg Supported algorithms are 'ES384','ES256', 'HS256', 'HS384', + * @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(string $msg, mixed $key, string $alg): string + public static function sign(string $msg, string|OpenSSLAsymmetricKey $key, string $alg): string { if (empty(static::$supported_algs[$alg])) { throw new DomainException('Algorithm not supported'); @@ -231,10 +232,10 @@ public static function sign(string $msg, mixed $key, string $alg): string * 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 * @@ -243,7 +244,7 @@ public static function sign(string $msg, mixed $key, string $alg): string private static function verify( string $msg, string $signature, - mixed $keyMaterial, + string|OpenSSLAsymmetricKey keyMaterial, string $alg ): bool { if (empty(static::$supported_algs[$alg])) { @@ -291,7 +292,7 @@ private static function verify( * * @throws DomainException Provided string was invalid JSON */ - public static function jsonDecode(string $input): object|array|string + public static function jsonDecode(string $input): stdClass { $obj = \json_decode($input, false, 512, JSON_BIGINT_AS_STRING); @@ -304,15 +305,15 @@ public static function jsonDecode(string $input): object|array|string } /** - * 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(object|array|string $input): string + public static function jsonEncode(array $input): string { if (PHP_VERSION_ID >= 50400) { $json = \json_encode($input, \JSON_UNESCAPED_SLASHES); @@ -361,7 +362,7 @@ public static function urlsafeB64Encode(string $input): string /** * Determine if an algorithm has been provided for each Key * - * @param Key|array $keyOrKeyArray + * @param Key|Key[] $keyOrKeyArray * @param string|null $kid * * @throws UnexpectedValueException @@ -452,7 +453,7 @@ private static function handleJsonError(int $errno): void * * @return int */ - private static function safeStrlen($str) + private static function safeStrlen(string $str): int { if (\function_exists('mb_strlen')) { return \mb_strlen($str, '8bit'); @@ -466,7 +467,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)); @@ -496,9 +497,10 @@ 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::ASN1_SEQUENCE) { @@ -519,9 +521,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); @@ -546,9 +549,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); diff --git a/src/Key.php b/src/Key.php index 2c38a2c7..eea32c2a 100644 --- a/src/Key.php +++ b/src/Key.php @@ -9,19 +9,18 @@ class Key { /** - * @param string|resource|OpenSSLAsymmetricKey $keyMaterial + * @param string|OpenSSLAsymmetricKey $keyMaterial * @param string $algorithm */ public function __construct( - private mixed $keyMaterial, + private string|OpenSSLAsymmetricKey $keyMaterial, private string $algorithm ) { if ( !is_string($keyMaterial) - && !is_resource($keyMaterial) && !$keyMaterial instanceof OpenSSLAsymmetricKey ) { - throw new TypeError('Key material must be a string, resource, or OpenSSLAsymmetricKey'); + throw new TypeError('Key material must be a string or OpenSSLAsymmetricKey'); } if (empty($keyMaterial)) { @@ -47,7 +46,7 @@ public function getAlgorithm(): string } /** - * @return string|resource|OpenSSLAsymmetricKey + * @return string|OpenSSLAsymmetricKey */ public function getKeyMaterial(): mixed { From 7d041883cbe08d32c756a30a1ac2e69cade06ae3 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Thu, 30 Dec 2021 16:11:44 -0800 Subject: [PATCH 4/6] change array to [], fix tests and types --- src/JWK.php | 18 +++--- src/JWT.php | 12 ++-- tests/JWKTest.php | 42 +++++++------ tests/JWTTest.php | 151 +++++++++++++++++++++++----------------------- 4 files changed, 116 insertions(+), 107 deletions(-) diff --git a/src/JWK.php b/src/JWK.php index 9f936b06..067fe4b4 100644 --- a/src/JWK.php +++ b/src/JWK.php @@ -47,7 +47,9 @@ public static function parseKeySet(array $jwks): array foreach ($jwks['keys'] as $k => $v) { $kid = isset($v['kid']) ? $v['kid'] : $k; - $keys[$kid] = self::parseKey($v); + if ($key = self::parseKey($v)) { + $keys[$kid] = $key; + } } if (0 === \count($keys)) { @@ -70,7 +72,7 @@ public static function parseKeySet(array $jwks): array * * @uses createPemFromModulusAndExponent */ - public static function parseKey(array $jwk): Key + public static function parseKey(array $jwk): ?Key { if (empty($jwk)) { throw new InvalidArgumentException('JWK must not be empty'); @@ -81,10 +83,10 @@ public static function parseKey(array $jwk): Key } if (!isset($jwk['alg'])) { - // The "alg" parameter is optional in a KTY, but is required for parsing in - // this library. Add it manually to your JWK array if it doesn't already exist. + // 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 a "alg" parameter'); + throw new UnexpectedValueException('JWK must contain an "alg" parameter'); } switch ($jwk['kty']) { @@ -108,6 +110,8 @@ public static function parseKey(array $jwk): Key // Currently only RSA is supported break; } + + return null; } /** @@ -125,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 491be27b..351b54de 100644 --- a/src/JWT.php +++ b/src/JWT.php @@ -162,14 +162,14 @@ public static function encode( string $keyId = null, array $head = null ): string { - $header = array('typ' => 'JWT', 'alg' => $alg); + $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); @@ -244,7 +244,7 @@ public static function sign(string $msg, string|OpenSSLAsymmetricKey $key, strin private static function verify( string $msg, string $signature, - string|OpenSSLAsymmetricKey keyMaterial, + string|OpenSSLAsymmetricKey $keyMaterial, string $alg ): bool { if (empty(static::$supported_algs[$alg])) { @@ -432,13 +432,13 @@ public static function constantTimeEquals(string $left, string $right): bool */ 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] @@ -581,6 +581,6 @@ private static function readDER(string $der, int $offset = 0): array $data = null; } - return array($pos, $data); + return [$pos, $data]; } } diff --git a/tests/JWKTest.php b/tests/JWKTest.php index b908ea64..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'); - $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,8 +53,8 @@ public function testParsePrivateKey() public function testParsePrivateKeyWithoutAlg() { $this->setExpectedException( - 'InvalidArgumentException', - 'JWK key is missing "alg"' + UnexpectedValueException::class, + 'JWK must contain an "alg" parameter' ); $jwkSet = json_decode( @@ -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 989cae57..473c3cf2 100644 --- a/tests/JWTTest.php +++ b/tests/JWTTest.php @@ -4,6 +4,9 @@ use ArrayObject; use PHPUnit\Framework\TestCase; +use DomainException; +use InvalidArgumentException; +use UnexpectedValueException; class JWTTest extends TestCase { @@ -30,57 +33,57 @@ public function testDecodeFromPython() public function testUrlSafeCharacters() { - $encoded = JWT::encode('f?', 'a', 'HS256'); - $this->assertEquals('f?', JWT::decode($encoded, new Key('a', 'HS256'))); + $encoded = JWT::encode(['f?'], 'a', 'HS256'); + $this->assertEquals(['f?'], JWT::decode($encoded, new Key('a', 'HS256'))); } public function testMalformedUtf8StringsFail() { - $this->setExpectedException('DomainException'); + $this->setExpectedException(DomainException::class); JWT::encode([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 +92,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 +104,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 +116,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 +129,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 +141,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 +153,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,30 +165,30 @@ 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('TypeError'); JWT::decode($encoded, new Key(null, 'HS256')); @@ -193,92 +196,92 @@ public function testNullKeyFails() 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(['abc'], $keys['1']->getKeyMaterial(), 'HS256', '1'); $decoded = JWT::decode($msg, $keys); - $this->assertEquals($decoded, 'abc'); + $this->assertEquals($decoded, ['abc']); } 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(['abc'], $keys['1']->getKeyMaterial(), 'HS256', '1'); $decoded = JWT::decode($msg, $keys); $this->assertEquals($decoded, 'abc'); } public function testNoneAlgorithm() { - $msg = JWT::encode('abc', 'my_key', 'HS256'); - $this->setExpectedException('UnexpectedValueException'); + $msg = JWT::encode(['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(['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('InvalidArgumentException'); + $msg = JWT::encode(['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(['abc'], 'my_key', 'HS256', null, ['cty' => 'test-eit;v=1']); + $this->assertEquals(JWT::decode($msg, new Key('my_key', 'HS256')), ['abc']); } 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(['abc'], 'my_key', 'HS256'); + $this->assertEquals(JWT::decode($msg, new Key('my_key', 'HS256')), ['abc']); } 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(['abc'], $privKey, 'RS256'); $pubKey = openssl_pkey_get_details($privKey); $pubKey = $pubKey['key']; $decoded = JWT::decode($msg, new Key($pubKey, 'RS256')); - $this->assertEquals($decoded, 'abc'); + $this->assertEquals($decoded, ['abc']); } public function testEdDsaEncodeDecode() @@ -286,7 +289,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 +302,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')); } @@ -330,7 +333,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 +345,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 +359,7 @@ public function testEncodeDecodeWithResource() $resource = openssl_pkey_get_public($pem); $privateKey = file_get_contents(__DIR__ . '/data/rsa1-private.pem'); - $payload = array('foo' => 'bar'); + $payload = ['foo' => 'bar']; $encoded = JWT::encode($payload, $privateKey, 'RS512'); // Verify decoding succeeds From fdf3999b74395cf92714a633fdaac80f727f8adc Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Thu, 30 Dec 2021 16:52:30 -0800 Subject: [PATCH 5/6] fix tests --- src/JWT.php | 7 ++++-- tests/JWTTest.php | 60 ++++++++++++++++++++++++++--------------------- 2 files changed, 38 insertions(+), 29 deletions(-) diff --git a/src/JWT.php b/src/JWT.php index 351b54de..f74c2001 100644 --- a/src/JWT.php +++ b/src/JWT.php @@ -91,6 +91,9 @@ public static function decode(string $jwt, Key|array|ArrayAccess $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'); } @@ -288,11 +291,11 @@ private static function verify( * * @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(string $input): stdClass + public static function jsonDecode(string $input): mixed { $obj = \json_decode($input, false, 512, JSON_BIGINT_AS_STRING); diff --git a/tests/JWTTest.php b/tests/JWTTest.php index 473c3cf2..aa5ce140 100644 --- a/tests/JWTTest.php +++ b/tests/JWTTest.php @@ -7,6 +7,7 @@ use DomainException; use InvalidArgumentException; use UnexpectedValueException; +use stdClass; class JWTTest extends TestCase { @@ -22,25 +23,18 @@ 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::class); - JWT::encode([pack('c', 128)], 'a', 'HS256'); + JWT::encode(['message' => pack('c', 128)], 'a', 'HS256'); } public function testMalformedJsonThrowsException() @@ -210,9 +204,11 @@ public function testKIDChooser() '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() @@ -221,36 +217,40 @@ public function testArrayAccessKIDChooser() '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'); + $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'); + $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'); + $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, ['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() @@ -268,8 +268,10 @@ public function testInvalidSignatureEncoding() 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() @@ -277,11 +279,13 @@ public function testRSEncodeDecode() $privKey = openssl_pkey_new(['digest_alg' => 'sha256', 'private_key_bits' => 1024, 'private_key_type' => OPENSSL_KEYTYPE_RSA]); - $msg = JWT::encode(['abc'], $privKey, 'RS256'); + $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() @@ -319,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); } /** From 9159bfac4082aa652f503dfdd11756c1733775e0 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 16 Feb 2022 08:11:07 -0600 Subject: [PATCH 6/6] remove redundant code --- src/Key.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Key.php b/src/Key.php index eea32c2a..286a3143 100644 --- a/src/Key.php +++ b/src/Key.php @@ -30,9 +30,6 @@ public function __construct( if (empty($algorithm)) { throw new InvalidArgumentException('Algorithm must not be empty'); } - - $this->keyMaterial = $keyMaterial; - $this->algorithm = $algorithm; } /**