From 45cb0f4874bbef8deb395e9f37ad24dfed080c54 Mon Sep 17 00:00:00 2001 From: nicumicle <20170987+nicumicleI@users.noreply.github.com> Date: Wed, 21 Sep 2022 07:41:07 +0200 Subject: [PATCH 01/17] Add error codes to all exceptions --- src/BeforeValidException.php | 2 + src/CachedKeySet.php | 20 ++++- src/ExceptionCodes.php | 60 +++++++++++++ src/ExpiredException.php | 1 + src/JWK.php | 64 +++++++++++--- src/JWT.php | 141 +++++++++++++++++++++++------- src/Key.php | 15 +++- src/SignatureInvalidException.php | 1 + 8 files changed, 254 insertions(+), 50 deletions(-) create mode 100644 src/ExceptionCodes.php diff --git a/src/BeforeValidException.php b/src/BeforeValidException.php index c147852b..70a22065 100644 --- a/src/BeforeValidException.php +++ b/src/BeforeValidException.php @@ -4,4 +4,6 @@ class BeforeValidException extends \UnexpectedValueException { + public const NBF_PRIOR_TO_DATE = 1; + const IAT_PRIOR_TO_DATE = 2; } diff --git a/src/CachedKeySet.php b/src/CachedKeySet.php index e2215b30..d0c4ea22 100644 --- a/src/CachedKeySet.php +++ b/src/CachedKeySet.php @@ -99,7 +99,10 @@ public function __construct( public function offsetGet($keyId): Key { if (!$this->keyIdExists($keyId)) { - throw new OutOfBoundsException('Key ID not found'); + throw new OutOfBoundsException( + 'Key ID not found', + ExceptionCodes::KEY_ID_NOT_FOUND + ); } return $this->keySet[$keyId]; } @@ -119,7 +122,10 @@ public function offsetExists($keyId): bool */ public function offsetSet($offset, $value): void { - throw new LogicException('Method not implemented'); + throw new LogicException( + 'Method not implemented', + ExceptionCodes::OFFSET_SET_METHOD_NOT_IMPLEMENTED + ); } /** @@ -127,7 +133,10 @@ public function offsetSet($offset, $value): void */ public function offsetUnset($offset): void { - throw new LogicException('Method not implemented'); + throw new LogicException( + 'Method not implemented', + ExceptionCodes::OFFSET_UNSET_METHOD_NOT_IMPLEMENTED + ); } private function keyIdExists(string $keyId): bool @@ -198,7 +207,10 @@ private function getCacheItem(): CacheItemInterface private function setCacheKeys(): void { if (empty($this->jwksUri)) { - throw new RuntimeException('JWKS URI is empty'); + throw new RuntimeException( + 'JWKS URI is empty', + ExceptionCodes::JWKS_URI_IS_EMPTY + ); } // ensure we do not have illegal characters diff --git a/src/ExceptionCodes.php b/src/ExceptionCodes.php new file mode 100644 index 00000000..40891fa0 --- /dev/null +++ b/src/ExceptionCodes.php @@ -0,0 +1,60 @@ + $v) { @@ -65,7 +71,11 @@ public static function parseKeySet(array $jwks, string $defaultAlg = null): arra } if (0 === \count($keys)) { - throw new UnexpectedValueException('No supported algorithms found in JWK Set'); + throw new UnexpectedValueException( + 'No supported algorithms found in JWK Set', + ExceptionCodes::JWT_ALGORITHM_NOT_SUPPORTED + + ); } return $keys; @@ -89,11 +99,17 @@ public static function parseKeySet(array $jwks, string $defaultAlg = null): arra public static function parseKey(array $jwk, string $defaultAlg = null): ?Key { if (empty($jwk)) { - throw new InvalidArgumentException('JWK must not be empty'); + throw new InvalidArgumentException( + 'JWK must not be empty', + ExceptionCodes::JWK_IS_EMPTY + ); } if (!isset($jwk['kty'])) { - throw new UnexpectedValueException('JWK must contain a "kty" parameter'); + throw new UnexpectedValueException( + 'JWK must contain a "kty" parameter', + ExceptionCodes::JWT_MISSING_KTY_PARAMETER + ); } if (!isset($jwk['alg'])) { @@ -102,7 +118,10 @@ public static function parseKey(array $jwk, string $defaultAlg = null): ?Key // for parsing in this library. Use the $defaultAlg parameter when parsing the // key set in order to prevent this error. // @see https://datatracker.ietf.org/doc/html/rfc7517#section-4.4 - throw new UnexpectedValueException('JWK must contain an "alg" parameter'); + throw new UnexpectedValueException( + 'JWK must contain an "alg" parameter', + ExceptionCodes::JWT_MISSING_ALG_PARAMETER + ); } $jwk['alg'] = $defaultAlg; } @@ -110,36 +129,55 @@ public static function parseKey(array $jwk, string $defaultAlg = null): ?Key switch ($jwk['kty']) { case 'RSA': if (!empty($jwk['d'])) { - throw new UnexpectedValueException('RSA private keys are not supported'); + throw new UnexpectedValueException( + 'RSA private keys are not supported', + ExceptionCodes::JWT_RSA_KEYS_NOT_SUPPORTED + ); } if (!isset($jwk['n']) || !isset($jwk['e'])) { - throw new UnexpectedValueException('RSA keys must contain values for both "n" and "e"'); + throw new UnexpectedValueException( + 'RSA keys must contain values for both "n" and "e"', + ExceptionCodes::JWT_RSA_KEYS_MISSING_N_AND_E + ); } $pem = self::createPemFromModulusAndExponent($jwk['n'], $jwk['e']); $publicKey = \openssl_pkey_get_public($pem); if (false === $publicKey) { throw new DomainException( - 'OpenSSL error: ' . \openssl_error_string() + 'OpenSSL error: ' . \openssl_error_string(), + ExceptionCodes::JWT_OPEN_SSL_ERROR ); } return new Key($publicKey, $jwk['alg']); case 'EC': if (isset($jwk['d'])) { // The key is actually a private key - throw new UnexpectedValueException('Key data must be for a public key'); + throw new UnexpectedValueException( + 'Key data must be for a public key', + ExceptionCodes::JWK_EC_D_IS_NOT_SET + ); } if (empty($jwk['crv'])) { - throw new UnexpectedValueException('crv not set'); + throw new UnexpectedValueException( + 'crv not set', + ExceptionCodes::JWT_EC_CRV_IS_EMPTY + ); } if (!isset(self::EC_CURVES[$jwk['crv']])) { - throw new DomainException('Unrecognised or unsupported EC curve'); + throw new DomainException( + 'Unrecognised or unsupported EC curve', + ExceptionCodes::JWK_UNSUPPORTED_EC_CURVE + ); } if (empty($jwk['x']) || empty($jwk['y'])) { - throw new UnexpectedValueException('x and y not set'); + throw new UnexpectedValueException( + 'x and y not set', + ExceptionCodes::JWT_X_AND_Y_ARE_EMPTY + ); } $publicKey = self::createPemFromCrvAndXYCoordinates($jwk['crv'], $jwk['x'], $jwk['y']); diff --git a/src/JWT.php b/src/JWT.php index 9964073d..8373998d 100644 --- a/src/JWT.php +++ b/src/JWT.php @@ -95,34 +95,55 @@ public static function decode( $timestamp = \is_null(static::$timestamp) ? \time() : static::$timestamp; if (empty($keyOrKeyArray)) { - throw new InvalidArgumentException('Key may not be empty'); + throw new InvalidArgumentException( + 'Key may not be empty', + ExceptionCodes::KEY_NOT_EMPTY + ); } $tks = \explode('.', $jwt); if (\count($tks) !== 3) { - throw new UnexpectedValueException('Wrong number of segments'); + throw new UnexpectedValueException( + 'Wrong number of segments', + ExceptionCodes::WRONG_NUMBER_OF_SEGMENTS + ); } list($headb64, $bodyb64, $cryptob64) = $tks; $headerRaw = static::urlsafeB64Decode($headb64); if (null === ($header = static::jsonDecode($headerRaw))) { - throw new UnexpectedValueException('Invalid header encoding'); + throw new UnexpectedValueException( + 'Invalid header encoding', + ExceptionCodes::INVALID_HEADER_ENCODING + ); } $payloadRaw = static::urlsafeB64Decode($bodyb64); if (null === ($payload = static::jsonDecode($payloadRaw))) { - throw new UnexpectedValueException('Invalid claims encoding'); + throw new UnexpectedValueException( + 'Invalid claims encoding', + ExceptionCodes::INVALID_CLAIMS_ENCODING + ); } if (\is_array($payload)) { // prevent PHP Fatal Error in edge-cases when payload is empty array $payload = (object) $payload; } if (!$payload instanceof stdClass) { - throw new UnexpectedValueException('Payload must be a JSON object'); + throw new UnexpectedValueException( + 'Payload must be a JSON object', + ExceptionCodes::PAYLOAD_NOT_JSON + ); } $sig = static::urlsafeB64Decode($cryptob64); if (empty($header->alg)) { - throw new UnexpectedValueException('Empty algorithm'); + throw new UnexpectedValueException( + 'Empty algorithm', + ExceptionCodes::EMPTY_ALGORITHM + ); } if (empty(static::$supported_algs[$header->alg])) { - throw new UnexpectedValueException('Algorithm not supported'); + throw new UnexpectedValueException( + 'Algorithm not supported', + ExceptionCodes::DECODE_ALGORITHM_NOT_SUPPORTED + ); } $key = self::getKey($keyOrKeyArray, property_exists($header, 'kid') ? $header->kid : null); @@ -130,21 +151,28 @@ public static function decode( // Check the algorithm if (!self::constantTimeEquals($key->getAlgorithm(), $header->alg)) { // See issue #351 - throw new UnexpectedValueException('Incorrect key for this algorithm'); + throw new UnexpectedValueException( + 'Incorrect key for this algorithm', + ExceptionCodes::INCORRECT_KEY_FOR_ALGORITHM + ); } if ($header->alg === 'ES256' || $header->alg === 'ES384') { // OpenSSL expects an ASN.1 DER sequence for ES256/ES384 signatures $sig = self::signatureToDER($sig); } if (!self::verify("${headb64}.${bodyb64}", $sig, $key->getKeyMaterial(), $header->alg)) { - throw new SignatureInvalidException('Signature verification failed'); + throw new SignatureInvalidException( + 'Signature verification failed', + SignatureInvalidException::SIGNATURE_VERIFICATION_FAILED + ); } // Check the nbf if it is defined. This is the time that the // token can actually be used. If it's not yet that time, abort. if (isset($payload->nbf) && $payload->nbf > ($timestamp + static::$leeway)) { throw new BeforeValidException( - 'Cannot handle token prior to ' . \date(DateTime::ISO8601, $payload->nbf) + 'Cannot handle token prior to ' . \date(DateTime::ISO8601, $payload->nbf), + BeforeValidException::NBF_PRIOR_TO_DATE ); } @@ -153,13 +181,14 @@ public static function decode( // correctly used the nbf claim). if (isset($payload->iat) && $payload->iat > ($timestamp + static::$leeway)) { throw new BeforeValidException( - 'Cannot handle token prior to ' . \date(DateTime::ISO8601, $payload->iat) + 'Cannot handle token prior to ' . \date(DateTime::ISO8601, $payload->iat), + BeforeValidException::IAT_PRIOR_TO_DATE ); } // Check if this token has expired. if (isset($payload->exp) && ($timestamp - static::$leeway) >= $payload->exp) { - throw new ExpiredException('Expired token'); + throw new ExpiredException('Expired token', ExpiredException::TOKEN_EXPIRED); } return $payload; @@ -223,20 +252,29 @@ public static function sign( string $alg ): string { if (empty(static::$supported_algs[$alg])) { - throw new DomainException('Algorithm not supported'); + throw new DomainException( + 'Algorithm not supported', + ExceptionCodes::SIGN_ALGORITHM_NOT_SUPPORTED + ); } list($function, $algorithm) = static::$supported_algs[$alg]; switch ($function) { case 'hash_hmac': if (!\is_string($key)) { - throw new InvalidArgumentException('key must be a string when using hmac'); + throw new InvalidArgumentException( + 'key must be a string when using hmac', + ExceptionCodes::KEY_IS_NOT_STRING + ); } return \hash_hmac($algorithm, $msg, $key, true); case 'openssl': $signature = ''; $success = \openssl_sign($msg, $signature, $key, $algorithm); // @phpstan-ignore-line if (!$success) { - throw new DomainException('OpenSSL unable to sign data'); + throw new DomainException( + 'OpenSSL unable to sign data', + ExceptionCodes::OPENSSL_CAN_NOT_SIGN_DATA + ); } if ($alg === 'ES256') { $signature = self::signatureFromDER($signature, 256); @@ -249,7 +287,10 @@ public static function sign( throw new DomainException('libsodium is not available'); } if (!\is_string($key)) { - throw new InvalidArgumentException('key must be a string when using EdDSA'); + throw new InvalidArgumentException( + 'key must be a string when using EdDSA', + ExceptionCodes::SODIUM_KEY_IS_NOT_STRING + ); } try { // The last non-empty line is used as the key. @@ -257,11 +298,18 @@ public static function sign( $key = base64_decode((string) end($lines)); return sodium_crypto_sign_detached($msg, $key); } catch (Exception $e) { - throw new DomainException($e->getMessage(), 0, $e); + throw new DomainException( + $e->getMessage(), + ExceptionCodes::SODIUM_EXCEPTION, + $e + ); } } - throw new DomainException('Algorithm not supported'); + throw new DomainException( + 'Algorithm not supported', + ExceptionCodes::SIGN_GENERAL_EXCEPTION + ); } /** @@ -284,7 +332,10 @@ private static function verify( string $alg ): bool { if (empty(static::$supported_algs[$alg])) { - throw new DomainException('Algorithm not supported'); + throw new DomainException( + 'Algorithm not supported', + ExceptionCodes::VERIFY_ALGORITHM_NOT_SUPPORTED + ); } list($function, $algorithm) = static::$supported_algs[$alg]; @@ -299,14 +350,21 @@ private static function verify( } // returns 1 on success, 0 on failure, -1 on error. throw new DomainException( - 'OpenSSL error: ' . \openssl_error_string() + 'OpenSSL error: ' . \openssl_error_string(), + ExceptionCodes::VERIFY_OPEN_SSL_ERROR ); case 'sodium_crypto': if (!\function_exists('sodium_crypto_sign_verify_detached')) { - throw new DomainException('libsodium is not available'); + throw new DomainException( + 'libsodium is not available', + ExceptionCodes::VERIFY_SODIUM_NOT_AVAILABLE + ); } if (!\is_string($keyMaterial)) { - throw new InvalidArgumentException('key must be a string when using EdDSA'); + throw new InvalidArgumentException( + 'key must be a string when using EdDSA', + ExceptionCodes::VERIFY_KEY_MATERIAL_IS_NOT_STRING + ); } try { // The last non-empty line is used as the key. @@ -314,12 +372,19 @@ private static function verify( $key = base64_decode((string) end($lines)); return sodium_crypto_sign_verify_detached($signature, $msg, $key); } catch (Exception $e) { - throw new DomainException($e->getMessage(), 0, $e); + throw new DomainException( + $e->getMessage(), + ExceptionCodes::VERIFY_SODIUM_EXCEPTION, + $e + ); } case 'hash_hmac': default: if (!\is_string($keyMaterial)) { - throw new InvalidArgumentException('key must be a string when using hmac'); + throw new InvalidArgumentException( + 'key must be a string when using hmac', + ExceptionCodes::VERIFY_KEY_IS_NOT_STRING + ); } $hash = \hash_hmac($algorithm, $msg, $keyMaterial, true); return self::constantTimeEquals($hash, $signature); @@ -342,7 +407,10 @@ public static function jsonDecode(string $input) if ($errno = \json_last_error()) { self::handleJsonError($errno); } elseif ($obj === null && $input !== 'null') { - throw new DomainException('Null result with non-null input'); + throw new DomainException( + 'Null result with non-null input', + ExceptionCodes::DECODED_JSON_IS_NULL + ); } return $obj; } @@ -367,10 +435,16 @@ public static function jsonEncode(array $input): string if ($errno = \json_last_error()) { self::handleJsonError($errno); } elseif ($json === 'null' && $input !== null) { - throw new DomainException('Null result with non-null input'); + throw new DomainException( + 'Null result with non-null input', + ExceptionCodes::ENCODED_JSON_IS_NULL + ); } if ($json === false) { - throw new DomainException('Provided object could not be encoded to valid JSON'); + throw new DomainException( + 'Provided object could not be encoded to valid JSON', + ExceptionCodes::INVALID_JSON + ); } return $json; } @@ -431,10 +505,16 @@ private static function getKey( } if (empty($kid)) { - throw new UnexpectedValueException('"kid" empty, unable to lookup correct key'); + throw new UnexpectedValueException( + '"kid" empty, unable to lookup correct key', + ExceptionCodes::KID_IS_EMPTY + ); } if (!isset($keyOrKeyArray[$kid])) { - throw new UnexpectedValueException('"kid" invalid, unable to lookup correct key'); + throw new UnexpectedValueException( + '"kid" invalid, unable to lookup correct key', + ExceptionCodes::KID_IS_INVALID + ); } return $keyOrKeyArray[$kid]; @@ -482,7 +562,8 @@ private static function handleJsonError(int $errno): void throw new DomainException( isset($messages[$errno]) ? $messages[$errno] - : 'Unknown JSON error: ' . $errno + : 'Unknown JSON error: ' . $errno, + ExceptionCodes::JSON_ERROR ); } diff --git a/src/Key.php b/src/Key.php index 00cf7f2e..56811446 100644 --- a/src/Key.php +++ b/src/Key.php @@ -28,15 +28,24 @@ public function __construct( && !$keyMaterial instanceof OpenSSLCertificate && !\is_resource($keyMaterial) ) { - throw new TypeError('Key material must be a string, resource, or OpenSSLAsymmetricKey'); + throw new TypeError( + 'Key material must be a string, resource, or OpenSSLAsymmetricKey', + ExceptionCodes::KEY_MATERIAL_IS_INVALID + ); } if (empty($keyMaterial)) { - throw new InvalidArgumentException('Key material must not be empty'); + throw new InvalidArgumentException( + 'Key material must not be empty', + ExceptionCodes::KEY_MATERIAL_IS_EMPTY + ); } if (empty($algorithm)) { - throw new InvalidArgumentException('Algorithm must not be empty'); + throw new InvalidArgumentException( + 'Algorithm must not be empty', + ExceptionCodes::KEY_ALGORITHM_IS_EMPTY + ); } // TODO: Remove in PHP 8.0 in favor of class constructor property promotion diff --git a/src/SignatureInvalidException.php b/src/SignatureInvalidException.php index d35dee9f..26d0f512 100644 --- a/src/SignatureInvalidException.php +++ b/src/SignatureInvalidException.php @@ -4,4 +4,5 @@ class SignatureInvalidException extends \UnexpectedValueException { + public const SIGNATURE_VERIFICATION_FAILED = 1; } From da07b361550e1e11789955ea40a4f01abb1f9a13 Mon Sep 17 00:00:00 2001 From: nicumicle <20170987+nicumicleI@users.noreply.github.com> Date: Wed, 21 Sep 2022 07:46:39 +0200 Subject: [PATCH 02/17] Refactor Exception codes --- src/ExceptionCodes.php | 83 ++++++++++++++++++++---------------------- 1 file changed, 40 insertions(+), 43 deletions(-) diff --git a/src/ExceptionCodes.php b/src/ExceptionCodes.php index 40891fa0..53ae6c5d 100644 --- a/src/ExceptionCodes.php +++ b/src/ExceptionCodes.php @@ -1,8 +1,5 @@ Date: Wed, 21 Sep 2022 07:59:05 +0200 Subject: [PATCH 03/17] Add newline at end of file --- src/ExceptionCodes.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ExceptionCodes.php b/src/ExceptionCodes.php index 53ae6c5d..a10ed827 100644 --- a/src/ExceptionCodes.php +++ b/src/ExceptionCodes.php @@ -54,4 +54,4 @@ class ExceptionCodes public const KEY_MATERIAL_IS_INVALID = 43; public const KEY_MATERIAL_IS_EMPTY = 44; public const KEY_ALGORITHM_IS_EMPTY = 45; -} \ No newline at end of file +} From a2d3bb73de63d4745d62574e560920c64eb07c5e Mon Sep 17 00:00:00 2001 From: nicumicle <20170987+nicumicleI@users.noreply.github.com> Date: Wed, 21 Sep 2022 08:01:34 +0200 Subject: [PATCH 04/17] Add public to const in BeforeValidException --- src/BeforeValidException.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/BeforeValidException.php b/src/BeforeValidException.php index 70a22065..e99e6d1a 100644 --- a/src/BeforeValidException.php +++ b/src/BeforeValidException.php @@ -5,5 +5,5 @@ class BeforeValidException extends \UnexpectedValueException { public const NBF_PRIOR_TO_DATE = 1; - const IAT_PRIOR_TO_DATE = 2; + public const IAT_PRIOR_TO_DATE = 2; } From bf7d6db53af222fc77e17c66bb84c5aa245d29cd Mon Sep 17 00:00:00 2001 From: nicumicle <20170987+nicumicleI@users.noreply.github.com> Date: Wed, 21 Sep 2022 10:34:45 +0200 Subject: [PATCH 05/17] Fix exception codes --- src/ExceptionCodes.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ExceptionCodes.php b/src/ExceptionCodes.php index a10ed827..de3555e7 100644 --- a/src/ExceptionCodes.php +++ b/src/ExceptionCodes.php @@ -49,9 +49,9 @@ class ExceptionCodes public const JWK_EC_D_IS_NOT_SET = 40; public const JWT_EC_CRV_IS_EMPTY = 41; public const JWK_UNSUPPORTED_EC_CURVE = 42; - public const JWT_X_AND_Y_ARE_EMPTY = 42; + public const JWT_X_AND_Y_ARE_EMPTY = 43; - public const KEY_MATERIAL_IS_INVALID = 43; - public const KEY_MATERIAL_IS_EMPTY = 44; - public const KEY_ALGORITHM_IS_EMPTY = 45; + public const KEY_MATERIAL_IS_INVALID = 44; + public const KEY_MATERIAL_IS_EMPTY = 45; + public const KEY_ALGORITHM_IS_EMPTY = 46; } From abf93d1bab467d7446808bcdb4e1a0ecec1bb228 Mon Sep 17 00:00:00 2001 From: nicumicle <20170987+nicumicleI@users.noreply.github.com> Date: Thu, 6 Oct 2022 05:25:03 +0200 Subject: [PATCH 06/17] Move all exception codes to the ExceptionCodes class --- src/BeforeValidException.php | 2 - src/ExceptionCodes.php | 80 ++++++++++++++++--------------- src/ExpiredException.php | 1 - src/JWT.php | 8 ++-- src/SignatureInvalidException.php | 1 - 5 files changed, 46 insertions(+), 46 deletions(-) diff --git a/src/BeforeValidException.php b/src/BeforeValidException.php index e99e6d1a..c147852b 100644 --- a/src/BeforeValidException.php +++ b/src/BeforeValidException.php @@ -4,6 +4,4 @@ class BeforeValidException extends \UnexpectedValueException { - public const NBF_PRIOR_TO_DATE = 1; - public const IAT_PRIOR_TO_DATE = 2; } diff --git a/src/ExceptionCodes.php b/src/ExceptionCodes.php index de3555e7..001720f5 100644 --- a/src/ExceptionCodes.php +++ b/src/ExceptionCodes.php @@ -12,46 +12,50 @@ class ExceptionCodes public const EMPTY_ALGORITHM = 6; public const DECODE_ALGORITHM_NOT_SUPPORTED = 7; public const INCORRECT_KEY_FOR_ALGORITHM = 8; - public const SIGN_ALGORITHM_NOT_SUPPORTED = 9; - public const KEY_IS_NOT_STRING = 10; - public const OPENSSL_CAN_NOT_SIGN_DATA = 11; - public const SODIUM_KEY_IS_NOT_STRING = 12; - public const SODIUM_EXCEPTION = 13; - public const SIGN_GENERAL_EXCEPTION = 14; - public const VERIFY_ALGORITHM_NOT_SUPPORTED = 15; - public const VERIFY_OPEN_SSL_ERROR = 16; - public const VERIFY_SODIUM_NOT_AVAILABLE = 17; - public const VERIFY_KEY_MATERIAL_IS_NOT_STRING = 18; - public const VERIFY_SODIUM_EXCEPTION = 19; - public const VERIFY_KEY_IS_NOT_STRING = 20; - public const DECODED_JSON_IS_NULL = 21; - public const ENCODED_JSON_IS_NULL = 22; - public const INVALID_JSON = 23; - public const KID_IS_EMPTY = 24; - public const KID_IS_INVALID = 25; - public const JSON_ERROR = 26; + public const SIGNATURE_VERIFICATION_FAILED = 9; + public const NBF_PRIOR_TO_DATE = 10; + public const IAT_PRIOR_TO_DATE = 11; + public const TOKEN_EXPIRED = 12; + public const SIGN_ALGORITHM_NOT_SUPPORTED = 13; + public const KEY_IS_NOT_STRING = 14; + public const OPENSSL_CAN_NOT_SIGN_DATA = 15; + public const SODIUM_KEY_IS_NOT_STRING = 16; + public const SODIUM_EXCEPTION = 17; + public const SIGN_GENERAL_EXCEPTION = 18; + public const VERIFY_ALGORITHM_NOT_SUPPORTED = 19; + public const VERIFY_OPEN_SSL_ERROR = 20; + public const VERIFY_SODIUM_NOT_AVAILABLE = 21; + public const VERIFY_KEY_MATERIAL_IS_NOT_STRING = 22; + public const VERIFY_SODIUM_EXCEPTION = 23; + public const VERIFY_KEY_IS_NOT_STRING = 24; + public const DECODED_JSON_IS_NULL = 25; + public const ENCODED_JSON_IS_NULL = 26; + public const INVALID_JSON = 27; + public const KID_IS_EMPTY = 28; + public const KID_IS_INVALID = 29; + public const JSON_ERROR = 30; - public const KEY_ID_NOT_FOUND = 27; - public const OFFSET_SET_METHOD_NOT_IMPLEMENTED = 28; - public const OFFSET_UNSET_METHOD_NOT_IMPLEMENTED = 29; + public const KEY_ID_NOT_FOUND = 31; + public const OFFSET_SET_METHOD_NOT_IMPLEMENTED = 32; + public const OFFSET_UNSET_METHOD_NOT_IMPLEMENTED = 33; - public const JWKS_URI_IS_EMPTY = 30; + public const JWKS_URI_IS_EMPTY = 34; - public const JWK_MISSING_KEYS = 31; - public const JWT_KEYS_IS_EMPTY = 32; - public const JWT_ALGORITHM_NOT_SUPPORTED = 33; - public const JWK_IS_EMPTY = 34; - public const JWT_MISSING_KTY_PARAMETER = 35; - public const JWT_MISSING_ALG_PARAMETER = 36; - public const JWT_RSA_KEYS_NOT_SUPPORTED = 37; - public const JWT_RSA_KEYS_MISSING_N_AND_E = 38; - public const JWT_OPEN_SSL_ERROR = 39; - public const JWK_EC_D_IS_NOT_SET = 40; - public const JWT_EC_CRV_IS_EMPTY = 41; - public const JWK_UNSUPPORTED_EC_CURVE = 42; - public const JWT_X_AND_Y_ARE_EMPTY = 43; + public const JWK_MISSING_KEYS = 35; + public const JWT_KEYS_IS_EMPTY = 36; + public const JWT_ALGORITHM_NOT_SUPPORTED = 37; + public const JWK_IS_EMPTY = 38; + public const JWT_MISSING_KTY_PARAMETER = 39; + public const JWT_MISSING_ALG_PARAMETER = 40; + public const JWT_RSA_KEYS_NOT_SUPPORTED = 41; + public const JWT_RSA_KEYS_MISSING_N_AND_E = 42; + public const JWT_OPEN_SSL_ERROR = 43; + public const JWK_EC_D_IS_NOT_SET = 44; + public const JWT_EC_CRV_IS_EMPTY = 45; + public const JWK_UNSUPPORTED_EC_CURVE = 46; + public const JWT_X_AND_Y_ARE_EMPTY = 47; - public const KEY_MATERIAL_IS_INVALID = 44; - public const KEY_MATERIAL_IS_EMPTY = 45; - public const KEY_ALGORITHM_IS_EMPTY = 46; + public const KEY_MATERIAL_IS_INVALID = 48; + public const KEY_MATERIAL_IS_EMPTY = 49; + public const KEY_ALGORITHM_IS_EMPTY = 50; } diff --git a/src/ExpiredException.php b/src/ExpiredException.php index 5b5300f1..81ba52d4 100644 --- a/src/ExpiredException.php +++ b/src/ExpiredException.php @@ -4,5 +4,4 @@ class ExpiredException extends \UnexpectedValueException { - public const TOKEN_EXPIRED = 1; } diff --git a/src/JWT.php b/src/JWT.php index 8373998d..9467d73d 100644 --- a/src/JWT.php +++ b/src/JWT.php @@ -163,7 +163,7 @@ public static function decode( if (!self::verify("${headb64}.${bodyb64}", $sig, $key->getKeyMaterial(), $header->alg)) { throw new SignatureInvalidException( 'Signature verification failed', - SignatureInvalidException::SIGNATURE_VERIFICATION_FAILED + ExceptionCodes::SIGNATURE_VERIFICATION_FAILED ); } @@ -172,7 +172,7 @@ public static function decode( if (isset($payload->nbf) && $payload->nbf > ($timestamp + static::$leeway)) { throw new BeforeValidException( 'Cannot handle token prior to ' . \date(DateTime::ISO8601, $payload->nbf), - BeforeValidException::NBF_PRIOR_TO_DATE + ExceptionCodes::NBF_PRIOR_TO_DATE ); } @@ -182,13 +182,13 @@ public static function decode( if (isset($payload->iat) && $payload->iat > ($timestamp + static::$leeway)) { throw new BeforeValidException( 'Cannot handle token prior to ' . \date(DateTime::ISO8601, $payload->iat), - BeforeValidException::IAT_PRIOR_TO_DATE + ExceptionCodes::IAT_PRIOR_TO_DATE ); } // Check if this token has expired. if (isset($payload->exp) && ($timestamp - static::$leeway) >= $payload->exp) { - throw new ExpiredException('Expired token', ExpiredException::TOKEN_EXPIRED); + throw new ExpiredException('Expired token', ExceptionCodes::TOKEN_EXPIRED); } return $payload; diff --git a/src/SignatureInvalidException.php b/src/SignatureInvalidException.php index 26d0f512..d35dee9f 100644 --- a/src/SignatureInvalidException.php +++ b/src/SignatureInvalidException.php @@ -4,5 +4,4 @@ class SignatureInvalidException extends \UnexpectedValueException { - public const SIGNATURE_VERIFICATION_FAILED = 1; } From 1b9d33a597b5f2b23b31a91f8390d7ec12e8982c Mon Sep 17 00:00:00 2001 From: nicumicle <20170987+nicumicleI@users.noreply.github.com> Date: Wed, 21 Sep 2022 07:41:07 +0200 Subject: [PATCH 07/17] Add error codes to all exceptions --- src/BeforeValidException.php | 2 + src/CachedKeySet.php | 20 ++++- src/ExceptionCodes.php | 60 +++++++++++++ src/ExpiredException.php | 1 + src/JWK.php | 64 +++++++++++--- src/JWT.php | 141 +++++++++++++++++++++++------- src/Key.php | 15 +++- src/SignatureInvalidException.php | 1 + 8 files changed, 254 insertions(+), 50 deletions(-) create mode 100644 src/ExceptionCodes.php diff --git a/src/BeforeValidException.php b/src/BeforeValidException.php index c147852b..70a22065 100644 --- a/src/BeforeValidException.php +++ b/src/BeforeValidException.php @@ -4,4 +4,6 @@ class BeforeValidException extends \UnexpectedValueException { + public const NBF_PRIOR_TO_DATE = 1; + const IAT_PRIOR_TO_DATE = 2; } diff --git a/src/CachedKeySet.php b/src/CachedKeySet.php index 87f470d7..e3703101 100644 --- a/src/CachedKeySet.php +++ b/src/CachedKeySet.php @@ -99,7 +99,10 @@ public function __construct( public function offsetGet($keyId): Key { if (!$this->keyIdExists($keyId)) { - throw new OutOfBoundsException('Key ID not found'); + throw new OutOfBoundsException( + 'Key ID not found', + ExceptionCodes::KEY_ID_NOT_FOUND + ); } return $this->keySet[$keyId]; } @@ -119,7 +122,10 @@ public function offsetExists($keyId): bool */ public function offsetSet($offset, $value): void { - throw new LogicException('Method not implemented'); + throw new LogicException( + 'Method not implemented', + ExceptionCodes::OFFSET_SET_METHOD_NOT_IMPLEMENTED + ); } /** @@ -127,7 +133,10 @@ public function offsetSet($offset, $value): void */ public function offsetUnset($offset): void { - throw new LogicException('Method not implemented'); + throw new LogicException( + 'Method not implemented', + ExceptionCodes::OFFSET_UNSET_METHOD_NOT_IMPLEMENTED + ); } private function keyIdExists(string $keyId): bool @@ -198,7 +207,10 @@ private function getCacheItem(): CacheItemInterface private function setCacheKeys(): void { if (empty($this->jwksUri)) { - throw new RuntimeException('JWKS URI is empty'); + throw new RuntimeException( + 'JWKS URI is empty', + ExceptionCodes::JWKS_URI_IS_EMPTY + ); } // ensure we do not have illegal characters diff --git a/src/ExceptionCodes.php b/src/ExceptionCodes.php new file mode 100644 index 00000000..40891fa0 --- /dev/null +++ b/src/ExceptionCodes.php @@ -0,0 +1,60 @@ + $v) { @@ -65,7 +71,11 @@ public static function parseKeySet(array $jwks, string $defaultAlg = null): arra } if (0 === \count($keys)) { - throw new UnexpectedValueException('No supported algorithms found in JWK Set'); + throw new UnexpectedValueException( + 'No supported algorithms found in JWK Set', + ExceptionCodes::JWT_ALGORITHM_NOT_SUPPORTED + + ); } return $keys; @@ -89,11 +99,17 @@ public static function parseKeySet(array $jwks, string $defaultAlg = null): arra public static function parseKey(array $jwk, string $defaultAlg = null): ?Key { if (empty($jwk)) { - throw new InvalidArgumentException('JWK must not be empty'); + throw new InvalidArgumentException( + 'JWK must not be empty', + ExceptionCodes::JWK_IS_EMPTY + ); } if (!isset($jwk['kty'])) { - throw new UnexpectedValueException('JWK must contain a "kty" parameter'); + throw new UnexpectedValueException( + 'JWK must contain a "kty" parameter', + ExceptionCodes::JWT_MISSING_KTY_PARAMETER + ); } if (!isset($jwk['alg'])) { @@ -102,7 +118,10 @@ public static function parseKey(array $jwk, string $defaultAlg = null): ?Key // for parsing in this library. Use the $defaultAlg parameter when parsing the // key set in order to prevent this error. // @see https://datatracker.ietf.org/doc/html/rfc7517#section-4.4 - throw new UnexpectedValueException('JWK must contain an "alg" parameter'); + throw new UnexpectedValueException( + 'JWK must contain an "alg" parameter', + ExceptionCodes::JWT_MISSING_ALG_PARAMETER + ); } $jwk['alg'] = $defaultAlg; } @@ -110,36 +129,55 @@ public static function parseKey(array $jwk, string $defaultAlg = null): ?Key switch ($jwk['kty']) { case 'RSA': if (!empty($jwk['d'])) { - throw new UnexpectedValueException('RSA private keys are not supported'); + throw new UnexpectedValueException( + 'RSA private keys are not supported', + ExceptionCodes::JWT_RSA_KEYS_NOT_SUPPORTED + ); } if (!isset($jwk['n']) || !isset($jwk['e'])) { - throw new UnexpectedValueException('RSA keys must contain values for both "n" and "e"'); + throw new UnexpectedValueException( + 'RSA keys must contain values for both "n" and "e"', + ExceptionCodes::JWT_RSA_KEYS_MISSING_N_AND_E + ); } $pem = self::createPemFromModulusAndExponent($jwk['n'], $jwk['e']); $publicKey = \openssl_pkey_get_public($pem); if (false === $publicKey) { throw new DomainException( - 'OpenSSL error: ' . \openssl_error_string() + 'OpenSSL error: ' . \openssl_error_string(), + ExceptionCodes::JWT_OPEN_SSL_ERROR ); } return new Key($publicKey, $jwk['alg']); case 'EC': if (isset($jwk['d'])) { // The key is actually a private key - throw new UnexpectedValueException('Key data must be for a public key'); + throw new UnexpectedValueException( + 'Key data must be for a public key', + ExceptionCodes::JWK_EC_D_IS_NOT_SET + ); } if (empty($jwk['crv'])) { - throw new UnexpectedValueException('crv not set'); + throw new UnexpectedValueException( + 'crv not set', + ExceptionCodes::JWT_EC_CRV_IS_EMPTY + ); } if (!isset(self::EC_CURVES[$jwk['crv']])) { - throw new DomainException('Unrecognised or unsupported EC curve'); + throw new DomainException( + 'Unrecognised or unsupported EC curve', + ExceptionCodes::JWK_UNSUPPORTED_EC_CURVE + ); } if (empty($jwk['x']) || empty($jwk['y'])) { - throw new UnexpectedValueException('x and y not set'); + throw new UnexpectedValueException( + 'x and y not set', + ExceptionCodes::JWT_X_AND_Y_ARE_EMPTY + ); } $publicKey = self::createPemFromCrvAndXYCoordinates($jwk['crv'], $jwk['x'], $jwk['y']); diff --git a/src/JWT.php b/src/JWT.php index 977d7fbd..77db4af2 100644 --- a/src/JWT.php +++ b/src/JWT.php @@ -95,34 +95,55 @@ public static function decode( $timestamp = \is_null(static::$timestamp) ? \time() : static::$timestamp; if (empty($keyOrKeyArray)) { - throw new InvalidArgumentException('Key may not be empty'); + throw new InvalidArgumentException( + 'Key may not be empty', + ExceptionCodes::KEY_NOT_EMPTY + ); } $tks = \explode('.', $jwt); if (\count($tks) !== 3) { - throw new UnexpectedValueException('Wrong number of segments'); + throw new UnexpectedValueException( + 'Wrong number of segments', + ExceptionCodes::WRONG_NUMBER_OF_SEGMENTS + ); } list($headb64, $bodyb64, $cryptob64) = $tks; $headerRaw = static::urlsafeB64Decode($headb64); if (null === ($header = static::jsonDecode($headerRaw))) { - throw new UnexpectedValueException('Invalid header encoding'); + throw new UnexpectedValueException( + 'Invalid header encoding', + ExceptionCodes::INVALID_HEADER_ENCODING + ); } $payloadRaw = static::urlsafeB64Decode($bodyb64); if (null === ($payload = static::jsonDecode($payloadRaw))) { - throw new UnexpectedValueException('Invalid claims encoding'); + throw new UnexpectedValueException( + 'Invalid claims encoding', + ExceptionCodes::INVALID_CLAIMS_ENCODING + ); } if (\is_array($payload)) { // prevent PHP Fatal Error in edge-cases when payload is empty array $payload = (object) $payload; } if (!$payload instanceof stdClass) { - throw new UnexpectedValueException('Payload must be a JSON object'); + throw new UnexpectedValueException( + 'Payload must be a JSON object', + ExceptionCodes::PAYLOAD_NOT_JSON + ); } $sig = static::urlsafeB64Decode($cryptob64); if (empty($header->alg)) { - throw new UnexpectedValueException('Empty algorithm'); + throw new UnexpectedValueException( + 'Empty algorithm', + ExceptionCodes::EMPTY_ALGORITHM + ); } if (empty(static::$supported_algs[$header->alg])) { - throw new UnexpectedValueException('Algorithm not supported'); + throw new UnexpectedValueException( + 'Algorithm not supported', + ExceptionCodes::DECODE_ALGORITHM_NOT_SUPPORTED + ); } $key = self::getKey($keyOrKeyArray, property_exists($header, 'kid') ? $header->kid : null); @@ -130,21 +151,28 @@ public static function decode( // Check the algorithm if (!self::constantTimeEquals($key->getAlgorithm(), $header->alg)) { // See issue #351 - throw new UnexpectedValueException('Incorrect key for this algorithm'); + throw new UnexpectedValueException( + 'Incorrect key for this algorithm', + ExceptionCodes::INCORRECT_KEY_FOR_ALGORITHM + ); } if ($header->alg === 'ES256' || $header->alg === 'ES384') { // OpenSSL expects an ASN.1 DER sequence for ES256/ES384 signatures $sig = self::signatureToDER($sig); } if (!self::verify("{$headb64}.{$bodyb64}", $sig, $key->getKeyMaterial(), $header->alg)) { - throw new SignatureInvalidException('Signature verification failed'); + throw new SignatureInvalidException( + 'Signature verification failed', + SignatureInvalidException::SIGNATURE_VERIFICATION_FAILED + ); } // Check the nbf if it is defined. This is the time that the // token can actually be used. If it's not yet that time, abort. if (isset($payload->nbf) && $payload->nbf > ($timestamp + static::$leeway)) { throw new BeforeValidException( - 'Cannot handle token prior to ' . \date(DateTime::ISO8601, $payload->nbf) + 'Cannot handle token prior to ' . \date(DateTime::ISO8601, $payload->nbf), + BeforeValidException::NBF_PRIOR_TO_DATE ); } @@ -153,13 +181,14 @@ public static function decode( // correctly used the nbf claim). if (isset($payload->iat) && $payload->iat > ($timestamp + static::$leeway)) { throw new BeforeValidException( - 'Cannot handle token prior to ' . \date(DateTime::ISO8601, $payload->iat) + 'Cannot handle token prior to ' . \date(DateTime::ISO8601, $payload->iat), + BeforeValidException::IAT_PRIOR_TO_DATE ); } // Check if this token has expired. if (isset($payload->exp) && ($timestamp - static::$leeway) >= $payload->exp) { - throw new ExpiredException('Expired token'); + throw new ExpiredException('Expired token', ExpiredException::TOKEN_EXPIRED); } return $payload; @@ -223,20 +252,29 @@ public static function sign( string $alg ): string { if (empty(static::$supported_algs[$alg])) { - throw new DomainException('Algorithm not supported'); + throw new DomainException( + 'Algorithm not supported', + ExceptionCodes::SIGN_ALGORITHM_NOT_SUPPORTED + ); } list($function, $algorithm) = static::$supported_algs[$alg]; switch ($function) { case 'hash_hmac': if (!\is_string($key)) { - throw new InvalidArgumentException('key must be a string when using hmac'); + throw new InvalidArgumentException( + 'key must be a string when using hmac', + ExceptionCodes::KEY_IS_NOT_STRING + ); } return \hash_hmac($algorithm, $msg, $key, true); case 'openssl': $signature = ''; $success = \openssl_sign($msg, $signature, $key, $algorithm); // @phpstan-ignore-line if (!$success) { - throw new DomainException('OpenSSL unable to sign data'); + throw new DomainException( + 'OpenSSL unable to sign data', + ExceptionCodes::OPENSSL_CAN_NOT_SIGN_DATA + ); } if ($alg === 'ES256') { $signature = self::signatureFromDER($signature, 256); @@ -249,7 +287,10 @@ public static function sign( throw new DomainException('libsodium is not available'); } if (!\is_string($key)) { - throw new InvalidArgumentException('key must be a string when using EdDSA'); + throw new InvalidArgumentException( + 'key must be a string when using EdDSA', + ExceptionCodes::SODIUM_KEY_IS_NOT_STRING + ); } try { // The last non-empty line is used as the key. @@ -257,11 +298,18 @@ public static function sign( $key = base64_decode((string) end($lines)); return sodium_crypto_sign_detached($msg, $key); } catch (Exception $e) { - throw new DomainException($e->getMessage(), 0, $e); + throw new DomainException( + $e->getMessage(), + ExceptionCodes::SODIUM_EXCEPTION, + $e + ); } } - throw new DomainException('Algorithm not supported'); + throw new DomainException( + 'Algorithm not supported', + ExceptionCodes::SIGN_GENERAL_EXCEPTION + ); } /** @@ -284,7 +332,10 @@ private static function verify( string $alg ): bool { if (empty(static::$supported_algs[$alg])) { - throw new DomainException('Algorithm not supported'); + throw new DomainException( + 'Algorithm not supported', + ExceptionCodes::VERIFY_ALGORITHM_NOT_SUPPORTED + ); } list($function, $algorithm) = static::$supported_algs[$alg]; @@ -299,14 +350,21 @@ private static function verify( } // returns 1 on success, 0 on failure, -1 on error. throw new DomainException( - 'OpenSSL error: ' . \openssl_error_string() + 'OpenSSL error: ' . \openssl_error_string(), + ExceptionCodes::VERIFY_OPEN_SSL_ERROR ); case 'sodium_crypto': if (!\function_exists('sodium_crypto_sign_verify_detached')) { - throw new DomainException('libsodium is not available'); + throw new DomainException( + 'libsodium is not available', + ExceptionCodes::VERIFY_SODIUM_NOT_AVAILABLE + ); } if (!\is_string($keyMaterial)) { - throw new InvalidArgumentException('key must be a string when using EdDSA'); + throw new InvalidArgumentException( + 'key must be a string when using EdDSA', + ExceptionCodes::VERIFY_KEY_MATERIAL_IS_NOT_STRING + ); } try { // The last non-empty line is used as the key. @@ -314,12 +372,19 @@ private static function verify( $key = base64_decode((string) end($lines)); return sodium_crypto_sign_verify_detached($signature, $msg, $key); } catch (Exception $e) { - throw new DomainException($e->getMessage(), 0, $e); + throw new DomainException( + $e->getMessage(), + ExceptionCodes::VERIFY_SODIUM_EXCEPTION, + $e + ); } case 'hash_hmac': default: if (!\is_string($keyMaterial)) { - throw new InvalidArgumentException('key must be a string when using hmac'); + throw new InvalidArgumentException( + 'key must be a string when using hmac', + ExceptionCodes::VERIFY_KEY_IS_NOT_STRING + ); } $hash = \hash_hmac($algorithm, $msg, $keyMaterial, true); return self::constantTimeEquals($hash, $signature); @@ -342,7 +407,10 @@ public static function jsonDecode(string $input) if ($errno = \json_last_error()) { self::handleJsonError($errno); } elseif ($obj === null && $input !== 'null') { - throw new DomainException('Null result with non-null input'); + throw new DomainException( + 'Null result with non-null input', + ExceptionCodes::DECODED_JSON_IS_NULL + ); } return $obj; } @@ -367,10 +435,16 @@ public static function jsonEncode(array $input): string if ($errno = \json_last_error()) { self::handleJsonError($errno); } elseif ($json === 'null' && $input !== null) { - throw new DomainException('Null result with non-null input'); + throw new DomainException( + 'Null result with non-null input', + ExceptionCodes::ENCODED_JSON_IS_NULL + ); } if ($json === false) { - throw new DomainException('Provided object could not be encoded to valid JSON'); + throw new DomainException( + 'Provided object could not be encoded to valid JSON', + ExceptionCodes::INVALID_JSON + ); } return $json; } @@ -431,10 +505,16 @@ private static function getKey( } if (empty($kid)) { - throw new UnexpectedValueException('"kid" empty, unable to lookup correct key'); + throw new UnexpectedValueException( + '"kid" empty, unable to lookup correct key', + ExceptionCodes::KID_IS_EMPTY + ); } if (!isset($keyOrKeyArray[$kid])) { - throw new UnexpectedValueException('"kid" invalid, unable to lookup correct key'); + throw new UnexpectedValueException( + '"kid" invalid, unable to lookup correct key', + ExceptionCodes::KID_IS_INVALID + ); } return $keyOrKeyArray[$kid]; @@ -482,7 +562,8 @@ private static function handleJsonError(int $errno): void throw new DomainException( isset($messages[$errno]) ? $messages[$errno] - : 'Unknown JSON error: ' . $errno + : 'Unknown JSON error: ' . $errno, + ExceptionCodes::JSON_ERROR ); } diff --git a/src/Key.php b/src/Key.php index 00cf7f2e..56811446 100644 --- a/src/Key.php +++ b/src/Key.php @@ -28,15 +28,24 @@ public function __construct( && !$keyMaterial instanceof OpenSSLCertificate && !\is_resource($keyMaterial) ) { - throw new TypeError('Key material must be a string, resource, or OpenSSLAsymmetricKey'); + throw new TypeError( + 'Key material must be a string, resource, or OpenSSLAsymmetricKey', + ExceptionCodes::KEY_MATERIAL_IS_INVALID + ); } if (empty($keyMaterial)) { - throw new InvalidArgumentException('Key material must not be empty'); + throw new InvalidArgumentException( + 'Key material must not be empty', + ExceptionCodes::KEY_MATERIAL_IS_EMPTY + ); } if (empty($algorithm)) { - throw new InvalidArgumentException('Algorithm must not be empty'); + throw new InvalidArgumentException( + 'Algorithm must not be empty', + ExceptionCodes::KEY_ALGORITHM_IS_EMPTY + ); } // TODO: Remove in PHP 8.0 in favor of class constructor property promotion diff --git a/src/SignatureInvalidException.php b/src/SignatureInvalidException.php index d35dee9f..26d0f512 100644 --- a/src/SignatureInvalidException.php +++ b/src/SignatureInvalidException.php @@ -4,4 +4,5 @@ class SignatureInvalidException extends \UnexpectedValueException { + public const SIGNATURE_VERIFICATION_FAILED = 1; } From 6451f8557234b451beedba37075008febf89e153 Mon Sep 17 00:00:00 2001 From: nicumicle <20170987+nicumicleI@users.noreply.github.com> Date: Wed, 21 Sep 2022 07:46:39 +0200 Subject: [PATCH 08/17] Refactor Exception codes --- src/ExceptionCodes.php | 83 ++++++++++++++++++++---------------------- 1 file changed, 40 insertions(+), 43 deletions(-) diff --git a/src/ExceptionCodes.php b/src/ExceptionCodes.php index 40891fa0..53ae6c5d 100644 --- a/src/ExceptionCodes.php +++ b/src/ExceptionCodes.php @@ -1,8 +1,5 @@ Date: Wed, 21 Sep 2022 07:59:05 +0200 Subject: [PATCH 09/17] Add newline at end of file --- src/ExceptionCodes.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ExceptionCodes.php b/src/ExceptionCodes.php index 53ae6c5d..a10ed827 100644 --- a/src/ExceptionCodes.php +++ b/src/ExceptionCodes.php @@ -54,4 +54,4 @@ class ExceptionCodes public const KEY_MATERIAL_IS_INVALID = 43; public const KEY_MATERIAL_IS_EMPTY = 44; public const KEY_ALGORITHM_IS_EMPTY = 45; -} \ No newline at end of file +} From 2f467b37fbb82f5531bc902cbaf481425e84439b Mon Sep 17 00:00:00 2001 From: nicumicle <20170987+nicumicleI@users.noreply.github.com> Date: Wed, 21 Sep 2022 08:01:34 +0200 Subject: [PATCH 10/17] Add public to const in BeforeValidException --- src/BeforeValidException.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/BeforeValidException.php b/src/BeforeValidException.php index 70a22065..e99e6d1a 100644 --- a/src/BeforeValidException.php +++ b/src/BeforeValidException.php @@ -5,5 +5,5 @@ class BeforeValidException extends \UnexpectedValueException { public const NBF_PRIOR_TO_DATE = 1; - const IAT_PRIOR_TO_DATE = 2; + public const IAT_PRIOR_TO_DATE = 2; } From 38b5fe6d5de90052287dcc963e1c561ea0c592bf Mon Sep 17 00:00:00 2001 From: nicumicle <20170987+nicumicleI@users.noreply.github.com> Date: Wed, 21 Sep 2022 10:34:45 +0200 Subject: [PATCH 11/17] Fix exception codes --- src/ExceptionCodes.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ExceptionCodes.php b/src/ExceptionCodes.php index a10ed827..de3555e7 100644 --- a/src/ExceptionCodes.php +++ b/src/ExceptionCodes.php @@ -49,9 +49,9 @@ class ExceptionCodes public const JWK_EC_D_IS_NOT_SET = 40; public const JWT_EC_CRV_IS_EMPTY = 41; public const JWK_UNSUPPORTED_EC_CURVE = 42; - public const JWT_X_AND_Y_ARE_EMPTY = 42; + public const JWT_X_AND_Y_ARE_EMPTY = 43; - public const KEY_MATERIAL_IS_INVALID = 43; - public const KEY_MATERIAL_IS_EMPTY = 44; - public const KEY_ALGORITHM_IS_EMPTY = 45; + public const KEY_MATERIAL_IS_INVALID = 44; + public const KEY_MATERIAL_IS_EMPTY = 45; + public const KEY_ALGORITHM_IS_EMPTY = 46; } From 11271dc3d34b08d53d94c2f5851baff94f5c5113 Mon Sep 17 00:00:00 2001 From: nicumicle <20170987+nicumicleI@users.noreply.github.com> Date: Thu, 6 Oct 2022 05:25:03 +0200 Subject: [PATCH 12/17] Move all exception codes to the ExceptionCodes class --- src/BeforeValidException.php | 2 - src/ExceptionCodes.php | 80 ++++++++++++++++--------------- src/ExpiredException.php | 1 - src/JWT.php | 8 ++-- src/SignatureInvalidException.php | 1 - 5 files changed, 46 insertions(+), 46 deletions(-) diff --git a/src/BeforeValidException.php b/src/BeforeValidException.php index e99e6d1a..c147852b 100644 --- a/src/BeforeValidException.php +++ b/src/BeforeValidException.php @@ -4,6 +4,4 @@ class BeforeValidException extends \UnexpectedValueException { - public const NBF_PRIOR_TO_DATE = 1; - public const IAT_PRIOR_TO_DATE = 2; } diff --git a/src/ExceptionCodes.php b/src/ExceptionCodes.php index de3555e7..001720f5 100644 --- a/src/ExceptionCodes.php +++ b/src/ExceptionCodes.php @@ -12,46 +12,50 @@ class ExceptionCodes public const EMPTY_ALGORITHM = 6; public const DECODE_ALGORITHM_NOT_SUPPORTED = 7; public const INCORRECT_KEY_FOR_ALGORITHM = 8; - public const SIGN_ALGORITHM_NOT_SUPPORTED = 9; - public const KEY_IS_NOT_STRING = 10; - public const OPENSSL_CAN_NOT_SIGN_DATA = 11; - public const SODIUM_KEY_IS_NOT_STRING = 12; - public const SODIUM_EXCEPTION = 13; - public const SIGN_GENERAL_EXCEPTION = 14; - public const VERIFY_ALGORITHM_NOT_SUPPORTED = 15; - public const VERIFY_OPEN_SSL_ERROR = 16; - public const VERIFY_SODIUM_NOT_AVAILABLE = 17; - public const VERIFY_KEY_MATERIAL_IS_NOT_STRING = 18; - public const VERIFY_SODIUM_EXCEPTION = 19; - public const VERIFY_KEY_IS_NOT_STRING = 20; - public const DECODED_JSON_IS_NULL = 21; - public const ENCODED_JSON_IS_NULL = 22; - public const INVALID_JSON = 23; - public const KID_IS_EMPTY = 24; - public const KID_IS_INVALID = 25; - public const JSON_ERROR = 26; + public const SIGNATURE_VERIFICATION_FAILED = 9; + public const NBF_PRIOR_TO_DATE = 10; + public const IAT_PRIOR_TO_DATE = 11; + public const TOKEN_EXPIRED = 12; + public const SIGN_ALGORITHM_NOT_SUPPORTED = 13; + public const KEY_IS_NOT_STRING = 14; + public const OPENSSL_CAN_NOT_SIGN_DATA = 15; + public const SODIUM_KEY_IS_NOT_STRING = 16; + public const SODIUM_EXCEPTION = 17; + public const SIGN_GENERAL_EXCEPTION = 18; + public const VERIFY_ALGORITHM_NOT_SUPPORTED = 19; + public const VERIFY_OPEN_SSL_ERROR = 20; + public const VERIFY_SODIUM_NOT_AVAILABLE = 21; + public const VERIFY_KEY_MATERIAL_IS_NOT_STRING = 22; + public const VERIFY_SODIUM_EXCEPTION = 23; + public const VERIFY_KEY_IS_NOT_STRING = 24; + public const DECODED_JSON_IS_NULL = 25; + public const ENCODED_JSON_IS_NULL = 26; + public const INVALID_JSON = 27; + public const KID_IS_EMPTY = 28; + public const KID_IS_INVALID = 29; + public const JSON_ERROR = 30; - public const KEY_ID_NOT_FOUND = 27; - public const OFFSET_SET_METHOD_NOT_IMPLEMENTED = 28; - public const OFFSET_UNSET_METHOD_NOT_IMPLEMENTED = 29; + public const KEY_ID_NOT_FOUND = 31; + public const OFFSET_SET_METHOD_NOT_IMPLEMENTED = 32; + public const OFFSET_UNSET_METHOD_NOT_IMPLEMENTED = 33; - public const JWKS_URI_IS_EMPTY = 30; + public const JWKS_URI_IS_EMPTY = 34; - public const JWK_MISSING_KEYS = 31; - public const JWT_KEYS_IS_EMPTY = 32; - public const JWT_ALGORITHM_NOT_SUPPORTED = 33; - public const JWK_IS_EMPTY = 34; - public const JWT_MISSING_KTY_PARAMETER = 35; - public const JWT_MISSING_ALG_PARAMETER = 36; - public const JWT_RSA_KEYS_NOT_SUPPORTED = 37; - public const JWT_RSA_KEYS_MISSING_N_AND_E = 38; - public const JWT_OPEN_SSL_ERROR = 39; - public const JWK_EC_D_IS_NOT_SET = 40; - public const JWT_EC_CRV_IS_EMPTY = 41; - public const JWK_UNSUPPORTED_EC_CURVE = 42; - public const JWT_X_AND_Y_ARE_EMPTY = 43; + public const JWK_MISSING_KEYS = 35; + public const JWT_KEYS_IS_EMPTY = 36; + public const JWT_ALGORITHM_NOT_SUPPORTED = 37; + public const JWK_IS_EMPTY = 38; + public const JWT_MISSING_KTY_PARAMETER = 39; + public const JWT_MISSING_ALG_PARAMETER = 40; + public const JWT_RSA_KEYS_NOT_SUPPORTED = 41; + public const JWT_RSA_KEYS_MISSING_N_AND_E = 42; + public const JWT_OPEN_SSL_ERROR = 43; + public const JWK_EC_D_IS_NOT_SET = 44; + public const JWT_EC_CRV_IS_EMPTY = 45; + public const JWK_UNSUPPORTED_EC_CURVE = 46; + public const JWT_X_AND_Y_ARE_EMPTY = 47; - public const KEY_MATERIAL_IS_INVALID = 44; - public const KEY_MATERIAL_IS_EMPTY = 45; - public const KEY_ALGORITHM_IS_EMPTY = 46; + public const KEY_MATERIAL_IS_INVALID = 48; + public const KEY_MATERIAL_IS_EMPTY = 49; + public const KEY_ALGORITHM_IS_EMPTY = 50; } diff --git a/src/ExpiredException.php b/src/ExpiredException.php index 5b5300f1..81ba52d4 100644 --- a/src/ExpiredException.php +++ b/src/ExpiredException.php @@ -4,5 +4,4 @@ class ExpiredException extends \UnexpectedValueException { - public const TOKEN_EXPIRED = 1; } diff --git a/src/JWT.php b/src/JWT.php index 77db4af2..bb6fd2e4 100644 --- a/src/JWT.php +++ b/src/JWT.php @@ -163,7 +163,7 @@ public static function decode( if (!self::verify("{$headb64}.{$bodyb64}", $sig, $key->getKeyMaterial(), $header->alg)) { throw new SignatureInvalidException( 'Signature verification failed', - SignatureInvalidException::SIGNATURE_VERIFICATION_FAILED + ExceptionCodes::SIGNATURE_VERIFICATION_FAILED ); } @@ -172,7 +172,7 @@ public static function decode( if (isset($payload->nbf) && $payload->nbf > ($timestamp + static::$leeway)) { throw new BeforeValidException( 'Cannot handle token prior to ' . \date(DateTime::ISO8601, $payload->nbf), - BeforeValidException::NBF_PRIOR_TO_DATE + ExceptionCodes::NBF_PRIOR_TO_DATE ); } @@ -182,13 +182,13 @@ public static function decode( if (isset($payload->iat) && $payload->iat > ($timestamp + static::$leeway)) { throw new BeforeValidException( 'Cannot handle token prior to ' . \date(DateTime::ISO8601, $payload->iat), - BeforeValidException::IAT_PRIOR_TO_DATE + ExceptionCodes::IAT_PRIOR_TO_DATE ); } // Check if this token has expired. if (isset($payload->exp) && ($timestamp - static::$leeway) >= $payload->exp) { - throw new ExpiredException('Expired token', ExpiredException::TOKEN_EXPIRED); + throw new ExpiredException('Expired token', ExceptionCodes::TOKEN_EXPIRED); } return $payload; diff --git a/src/SignatureInvalidException.php b/src/SignatureInvalidException.php index 26d0f512..d35dee9f 100644 --- a/src/SignatureInvalidException.php +++ b/src/SignatureInvalidException.php @@ -4,5 +4,4 @@ class SignatureInvalidException extends \UnexpectedValueException { - public const SIGNATURE_VERIFICATION_FAILED = 1; } From ff1f0540621749899b257d78fa0d9811f013bafa Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Mon, 22 May 2023 12:48:13 -0700 Subject: [PATCH 13/17] Update src/JWT.php --- src/JWT.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/JWT.php b/src/JWT.php index cff8feb3..511962ec 100644 --- a/src/JWT.php +++ b/src/JWT.php @@ -448,7 +448,7 @@ public static function jsonEncode(array $input): string } if ($errno = \json_last_error()) { self::handleJsonError($errno); - } elseif ($json === 'null' && $input !== null) { + } elseif ($json === 'null') { throw new DomainException( 'Null result with non-null input', ExceptionCodes::ENCODED_JSON_IS_NULL From 2e50332d7d5d00db42d212958cd5b3c34992f65a Mon Sep 17 00:00:00 2001 From: nicumicle <20170987+nicumicleI@users.noreply.github.com> Date: Thu, 24 Oct 2024 07:35:38 +0200 Subject: [PATCH 14/17] add exception codes --- src/CachedKeySet.php | 6 +-- src/ExceptionCodes.php | 88 +++++++++++++++++++++++------------------- src/JWK.php | 8 ++-- src/JWT.php | 21 +++++++--- 4 files changed, 71 insertions(+), 52 deletions(-) diff --git a/src/CachedKeySet.php b/src/CachedKeySet.php index 52a2762f..67b3db95 100644 --- a/src/CachedKeySet.php +++ b/src/CachedKeySet.php @@ -149,11 +149,11 @@ private function formatJwksForCache(string $jwks): array $jwks = json_decode($jwks, true); if (!isset($jwks['keys'])) { - throw new UnexpectedValueException('"keys" member must exist in the JWK Set'); + throw new UnexpectedValueException('"keys" member must exist in the JWK Set', ExceptionCodes::CACHED_KEY_MISSING); } if (empty($jwks['keys'])) { - throw new InvalidArgumentException('JWK Set did not contain any keys'); + throw new InvalidArgumentException('JWK Set did not contain any keys', ExceptionCodes::CACHED_KEY_EMPTY); } $keys = []; @@ -194,7 +194,7 @@ private function keyIdExists(string $keyId): bool $jwksResponse->getReasonPhrase(), $this->jwksUri, ), - $jwksResponse->getStatusCode() + ExceptionCodes::CACHED_KEY_GET_JWK ); } $this->keySet = $this->formatJwksForCache((string) $jwksResponse->getBody()); diff --git a/src/ExceptionCodes.php b/src/ExceptionCodes.php index 001720f5..57722f11 100644 --- a/src/ExceptionCodes.php +++ b/src/ExceptionCodes.php @@ -18,44 +18,52 @@ class ExceptionCodes public const TOKEN_EXPIRED = 12; public const SIGN_ALGORITHM_NOT_SUPPORTED = 13; public const KEY_IS_NOT_STRING = 14; - public const OPENSSL_CAN_NOT_SIGN_DATA = 15; - public const SODIUM_KEY_IS_NOT_STRING = 16; - public const SODIUM_EXCEPTION = 17; - public const SIGN_GENERAL_EXCEPTION = 18; - public const VERIFY_ALGORITHM_NOT_SUPPORTED = 19; - public const VERIFY_OPEN_SSL_ERROR = 20; - public const VERIFY_SODIUM_NOT_AVAILABLE = 21; - public const VERIFY_KEY_MATERIAL_IS_NOT_STRING = 22; - public const VERIFY_SODIUM_EXCEPTION = 23; - public const VERIFY_KEY_IS_NOT_STRING = 24; - public const DECODED_JSON_IS_NULL = 25; - public const ENCODED_JSON_IS_NULL = 26; - public const INVALID_JSON = 27; - public const KID_IS_EMPTY = 28; - public const KID_IS_INVALID = 29; - public const JSON_ERROR = 30; - - public const KEY_ID_NOT_FOUND = 31; - public const OFFSET_SET_METHOD_NOT_IMPLEMENTED = 32; - public const OFFSET_UNSET_METHOD_NOT_IMPLEMENTED = 33; - - public const JWKS_URI_IS_EMPTY = 34; - - public const JWK_MISSING_KEYS = 35; - public const JWT_KEYS_IS_EMPTY = 36; - public const JWT_ALGORITHM_NOT_SUPPORTED = 37; - public const JWK_IS_EMPTY = 38; - public const JWT_MISSING_KTY_PARAMETER = 39; - public const JWT_MISSING_ALG_PARAMETER = 40; - public const JWT_RSA_KEYS_NOT_SUPPORTED = 41; - public const JWT_RSA_KEYS_MISSING_N_AND_E = 42; - public const JWT_OPEN_SSL_ERROR = 43; - public const JWK_EC_D_IS_NOT_SET = 44; - public const JWT_EC_CRV_IS_EMPTY = 45; - public const JWK_UNSUPPORTED_EC_CURVE = 46; - public const JWT_X_AND_Y_ARE_EMPTY = 47; - - public const KEY_MATERIAL_IS_INVALID = 48; - public const KEY_MATERIAL_IS_EMPTY = 49; - public const KEY_ALGORITHM_IS_EMPTY = 50; + public const OPENSSL_SIGNATURE = 15; + public const OPENSSL_CAN_NOT_SIGN_DATA = 16; + public const SODIUM_KEY_IS_NOT_STRING = 17; + public const SODIUM_FUNC_DOES_NOT_EXIST = 18; + public const SODIUM_KEY_LENGTH_ZERO = 19; + public const SODIUM_VERIFY_KEY_LENGTH_ZERO = 20; + public const SODIUM_VERIFY_SIGNATURE_EMPTY = 21; + public const SODIUM_EXCEPTION = 22; + public const SIGN_GENERAL_EXCEPTION = 23; + public const VERIFY_ALGORITHM_NOT_SUPPORTED = 24; + public const VERIFY_OPEN_SSL_ERROR = 25; + public const VERIFY_SODIUM_NOT_AVAILABLE = 26; + public const VERIFY_KEY_MATERIAL_IS_NOT_STRING = 27; + public const VERIFY_SODIUM_EXCEPTION = 28; + public const VERIFY_KEY_IS_NOT_STRING = 29; + public const DECODED_JSON_IS_NULL = 30; + public const ENCODED_JSON_IS_NULL = 31; + public const INVALID_JSON = 32; + public const KID_IS_EMPTY = 33; + public const KID_IS_INVALID = 34; + public const JSON_ERROR = 35; + public const KEY_ID_NOT_FOUND = 36; + public const OFFSET_SET_METHOD_NOT_IMPLEMENTED = 37; + public const OFFSET_UNSET_METHOD_NOT_IMPLEMENTED = 38; + public const JWKS_URI_IS_EMPTY = 39; + public const JWK_MISSING_KEYS = 40; + public const JWT_KEYS_IS_EMPTY = 41; + public const JWT_ALGORITHM_NOT_SUPPORTED = 42; + public const JWK_IS_EMPTY = 43; + public const JWT_MISSING_KTY_PARAMETER = 44; + public const JWT_MISSING_ALG_PARAMETER = 45; + public const JWT_RSA_KEYS_NOT_SUPPORTED = 46; + public const JWT_RSA_KEYS_MISSING_N_AND_E = 47; + public const JWT_OPEN_SSL_ERROR = 48; + public const JWK_EC_D_IS_NOT_SET = 49; + public const JWT_EC_CRV_IS_EMPTY = 50; + public const JWK_UNSUPPORTED_EC_CURVE = 51; + public const JWT_X_AND_Y_ARE_EMPTY = 52; + public const KEY_MATERIAL_IS_INVALID = 53; + public const KEY_MATERIAL_IS_EMPTY = 54; + public const KEY_ALGORITHM_IS_EMPTY = 55; + public const CACHED_KEY_MISSING = 56; + public const CACHED_KEY_EMPTY = 57; + public const CACHED_KEY_GET_JWK = 58; + public const JWK_OKP_MISSING = 59; + public const JWT_CRV_MISSING = 60; + public const JWT_CRV_UNSUPPORTED = 61; + public const JWT_X_MISSING = 62; } diff --git a/src/JWK.php b/src/JWK.php index db67543e..942b256c 100644 --- a/src/JWK.php +++ b/src/JWK.php @@ -192,19 +192,19 @@ public static function parseKey(array $jwk, ?string $defaultAlg = null): ?Key case 'OKP': if (isset($jwk['d'])) { // The key is actually a private key - throw new UnexpectedValueException('Key data must be for a public key'); + throw new UnexpectedValueException('Key data must be for a public key', ExceptionCodes::JWK_OKP_MISSING); } if (!isset($jwk['crv'])) { - throw new UnexpectedValueException('crv not set'); + throw new UnexpectedValueException('crv not set', ExceptionCodes::JWT_CRV_MISSING); } if (empty(self::OKP_SUBTYPES[$jwk['crv']])) { - throw new DomainException('Unrecognised or unsupported OKP key subtype'); + throw new DomainException('Unrecognised or unsupported OKP key subtype', ExceptionCodes::JWT_CRV_UNSUPPORTED); } if (empty($jwk['x'])) { - throw new UnexpectedValueException('x not set'); + throw new UnexpectedValueException('x not set', ExceptionCodes::JWT_X_MISSING); } // This library works internally with EdDSA keys (Ed25519) encoded in standard base64. diff --git a/src/JWT.php b/src/JWT.php index 68ead3f6..d77151fa 100644 --- a/src/JWT.php +++ b/src/JWT.php @@ -287,7 +287,7 @@ public static function sign( case 'openssl': $signature = ''; if (!\is_resource($key) && !openssl_pkey_get_private($key)) { - throw new DomainException('OpenSSL unable to validate key'); + throw new DomainException('OpenSSL unable to validate key', ExceptionCodes::OPENSSL_SIGNATURE); } $success = \openssl_sign($msg, $signature, $key, $algorithm); // @phpstan-ignore-line if (!$success) { @@ -304,7 +304,9 @@ public static function sign( return $signature; case 'sodium_crypto': if (!\function_exists('sodium_crypto_sign_detached')) { - throw new DomainException('libsodium is not available'); + throw new DomainException('libsodium is not available', + ExceptionCodes::SODIUM_FUNC_DOES_NOT_EXIST + ); } if (!\is_string($key)) { throw new InvalidArgumentException( @@ -317,7 +319,10 @@ public static function sign( $lines = array_filter(explode("\n", $key)); $key = base64_decode((string) end($lines)); if (\strlen($key) === 0) { - throw new DomainException('Key cannot be empty string'); + throw new DomainException( + 'Key cannot be empty string', + ExceptionCodes::SODIUM_KEY_LENGTH_ZERO + ); } return sodium_crypto_sign_detached($msg, $key); } catch (Exception $e) { @@ -394,10 +399,16 @@ private static function verify( $lines = array_filter(explode("\n", $keyMaterial)); $key = base64_decode((string) end($lines)); if (\strlen($key) === 0) { - throw new DomainException('Key cannot be empty string'); + throw new DomainException( + 'Key cannot be empty string', + ExceptionCodes::SODIUM_VERIFY_KEY_LENGTH_ZERO + ); } if (\strlen($signature) === 0) { - throw new DomainException('Signature cannot be empty string'); + throw new DomainException( + 'Signature cannot be empty string', + ExceptionCodes::SODIUM_VERIFY_SIGNATURE_EMPTY + ); } return sodium_crypto_sign_verify_detached($signature, $msg, $key); } catch (Exception $e) { From b7fc2ef5371f52f37c8d80b090f2bdf3b22a9c67 Mon Sep 17 00:00:00 2001 From: nicumicle <20170987+nicumicleI@users.noreply.github.com> Date: Sat, 18 Jan 2025 09:04:31 +0100 Subject: [PATCH 15/17] Change exception codes class to an interface --- src/BeforeValidException.php | 2 +- src/CachedKeySet.php | 14 ++-- src/ExpiredException.php | 2 +- src/JWK.php | 34 ++++----- src/JWT.php | 70 +++++++++---------- ...ionCodes.php => JwtExceptionInterface.php} | 2 +- src/Key.php | 6 +- src/SignatureInvalidException.php | 2 +- 8 files changed, 66 insertions(+), 66 deletions(-) rename src/{ExceptionCodes.php => JwtExceptionInterface.php} (98%) diff --git a/src/BeforeValidException.php b/src/BeforeValidException.php index 595164bf..c8289c6b 100644 --- a/src/BeforeValidException.php +++ b/src/BeforeValidException.php @@ -2,7 +2,7 @@ namespace Firebase\JWT; -class BeforeValidException extends \UnexpectedValueException implements JWTExceptionWithPayloadInterface +class BeforeValidException extends \UnexpectedValueException implements JWTExceptionWithPayloadInterface, JwtExceptionInterface { private object $payload; diff --git a/src/CachedKeySet.php b/src/CachedKeySet.php index 67b3db95..d2ed7c94 100644 --- a/src/CachedKeySet.php +++ b/src/CachedKeySet.php @@ -103,7 +103,7 @@ public function offsetGet($keyId): Key if (!$this->keyIdExists($keyId)) { throw new OutOfBoundsException( 'Key ID not found', - ExceptionCodes::KEY_ID_NOT_FOUND + JwtExceptionInterface::KEY_ID_NOT_FOUND ); } return JWK::parseKey($this->keySet[$keyId], $this->defaultAlg); @@ -126,7 +126,7 @@ public function offsetSet($offset, $value): void { throw new LogicException( 'Method not implemented', - ExceptionCodes::OFFSET_SET_METHOD_NOT_IMPLEMENTED + JwtExceptionInterface::OFFSET_SET_METHOD_NOT_IMPLEMENTED ); } @@ -137,7 +137,7 @@ public function offsetUnset($offset): void { throw new LogicException( 'Method not implemented', - ExceptionCodes::OFFSET_UNSET_METHOD_NOT_IMPLEMENTED + JwtExceptionInterface::OFFSET_UNSET_METHOD_NOT_IMPLEMENTED ); } @@ -149,11 +149,11 @@ private function formatJwksForCache(string $jwks): array $jwks = json_decode($jwks, true); if (!isset($jwks['keys'])) { - throw new UnexpectedValueException('"keys" member must exist in the JWK Set', ExceptionCodes::CACHED_KEY_MISSING); + throw new UnexpectedValueException('"keys" member must exist in the JWK Set', JwtExceptionInterface::CACHED_KEY_MISSING); } if (empty($jwks['keys'])) { - throw new InvalidArgumentException('JWK Set did not contain any keys', ExceptionCodes::CACHED_KEY_EMPTY); + throw new InvalidArgumentException('JWK Set did not contain any keys', JwtExceptionInterface::CACHED_KEY_EMPTY); } $keys = []; @@ -194,7 +194,7 @@ private function keyIdExists(string $keyId): bool $jwksResponse->getReasonPhrase(), $this->jwksUri, ), - ExceptionCodes::CACHED_KEY_GET_JWK + JwtExceptionInterface::CACHED_KEY_GET_JWK ); } $this->keySet = $this->formatJwksForCache((string) $jwksResponse->getBody()); @@ -254,7 +254,7 @@ private function setCacheKeys(): void if (empty($this->jwksUri)) { throw new RuntimeException( 'JWKS URI is empty', - ExceptionCodes::JWKS_URI_IS_EMPTY + JwtExceptionInterface::JWKS_URI_IS_EMPTY ); } diff --git a/src/ExpiredException.php b/src/ExpiredException.php index 12fef094..99a059e9 100644 --- a/src/ExpiredException.php +++ b/src/ExpiredException.php @@ -2,7 +2,7 @@ namespace Firebase\JWT; -class ExpiredException extends \UnexpectedValueException implements JWTExceptionWithPayloadInterface +class ExpiredException extends \UnexpectedValueException implements JWTExceptionWithPayloadInterface, JwtExceptionInterface { private object $payload; diff --git a/src/JWK.php b/src/JWK.php index 942b256c..a7f071b5 100644 --- a/src/JWK.php +++ b/src/JWK.php @@ -59,14 +59,14 @@ public static function parseKeySet(array $jwks, ?string $defaultAlg = null): arr if (!isset($jwks['keys'])) { throw new UnexpectedValueException( '"keys" member must exist in the JWK Set', - ExceptionCodes::JWK_MISSING_KEYS + JwtExceptionInterface::JWK_MISSING_KEYS ); } if (empty($jwks['keys'])) { throw new InvalidArgumentException( 'JWK Set did not contain any keys', - ExceptionCodes::JWT_KEYS_IS_EMPTY + JwtExceptionInterface::JWT_KEYS_IS_EMPTY ); } @@ -80,7 +80,7 @@ public static function parseKeySet(array $jwks, ?string $defaultAlg = null): arr if (0 === \count($keys)) { throw new UnexpectedValueException( 'No supported algorithms found in JWK Set', - ExceptionCodes::JWT_ALGORITHM_NOT_SUPPORTED + JwtExceptionInterface::JWT_ALGORITHM_NOT_SUPPORTED ); } @@ -108,14 +108,14 @@ public static function parseKey(array $jwk, ?string $defaultAlg = null): ?Key if (empty($jwk)) { throw new InvalidArgumentException( 'JWK must not be empty', - ExceptionCodes::JWK_IS_EMPTY + JwtExceptionInterface::JWK_IS_EMPTY ); } if (!isset($jwk['kty'])) { throw new UnexpectedValueException( 'JWK must contain a "kty" parameter', - ExceptionCodes::JWT_MISSING_KTY_PARAMETER + JwtExceptionInterface::JWT_MISSING_KTY_PARAMETER ); } @@ -127,7 +127,7 @@ public static function parseKey(array $jwk, ?string $defaultAlg = null): ?Key // @see https://datatracker.ietf.org/doc/html/rfc7517#section-4.4 throw new UnexpectedValueException( 'JWK must contain an "alg" parameter', - ExceptionCodes::JWT_MISSING_ALG_PARAMETER + JwtExceptionInterface::JWT_MISSING_ALG_PARAMETER ); } $jwk['alg'] = $defaultAlg; @@ -138,13 +138,13 @@ public static function parseKey(array $jwk, ?string $defaultAlg = null): ?Key if (!empty($jwk['d'])) { throw new UnexpectedValueException( 'RSA private keys are not supported', - ExceptionCodes::JWT_RSA_KEYS_NOT_SUPPORTED + JwtExceptionInterface::JWT_RSA_KEYS_NOT_SUPPORTED ); } if (!isset($jwk['n']) || !isset($jwk['e'])) { throw new UnexpectedValueException( 'RSA keys must contain values for both "n" and "e"', - ExceptionCodes::JWT_RSA_KEYS_MISSING_N_AND_E + JwtExceptionInterface::JWT_RSA_KEYS_MISSING_N_AND_E ); } @@ -153,7 +153,7 @@ public static function parseKey(array $jwk, ?string $defaultAlg = null): ?Key if (false === $publicKey) { throw new DomainException( 'OpenSSL error: ' . \openssl_error_string(), - ExceptionCodes::JWT_OPEN_SSL_ERROR + JwtExceptionInterface::JWT_OPEN_SSL_ERROR ); } return new Key($publicKey, $jwk['alg']); @@ -162,28 +162,28 @@ public static function parseKey(array $jwk, ?string $defaultAlg = null): ?Key // The key is actually a private key throw new UnexpectedValueException( 'Key data must be for a public key', - ExceptionCodes::JWK_EC_D_IS_NOT_SET + JwtExceptionInterface::JWK_EC_D_IS_NOT_SET ); } if (empty($jwk['crv'])) { throw new UnexpectedValueException( 'crv not set', - ExceptionCodes::JWT_EC_CRV_IS_EMPTY + JwtExceptionInterface::JWT_EC_CRV_IS_EMPTY ); } if (!isset(self::EC_CURVES[$jwk['crv']])) { throw new DomainException( 'Unrecognised or unsupported EC curve', - ExceptionCodes::JWK_UNSUPPORTED_EC_CURVE + JwtExceptionInterface::JWK_UNSUPPORTED_EC_CURVE ); } if (empty($jwk['x']) || empty($jwk['y'])) { throw new UnexpectedValueException( 'x and y not set', - ExceptionCodes::JWT_X_AND_Y_ARE_EMPTY + JwtExceptionInterface::JWT_X_AND_Y_ARE_EMPTY ); } @@ -192,19 +192,19 @@ public static function parseKey(array $jwk, ?string $defaultAlg = null): ?Key case 'OKP': if (isset($jwk['d'])) { // The key is actually a private key - throw new UnexpectedValueException('Key data must be for a public key', ExceptionCodes::JWK_OKP_MISSING); + throw new UnexpectedValueException('Key data must be for a public key', JwtExceptionInterface::JWK_OKP_MISSING); } if (!isset($jwk['crv'])) { - throw new UnexpectedValueException('crv not set', ExceptionCodes::JWT_CRV_MISSING); + throw new UnexpectedValueException('crv not set', JwtExceptionInterface::JWT_CRV_MISSING); } if (empty(self::OKP_SUBTYPES[$jwk['crv']])) { - throw new DomainException('Unrecognised or unsupported OKP key subtype', ExceptionCodes::JWT_CRV_UNSUPPORTED); + throw new DomainException('Unrecognised or unsupported OKP key subtype', JwtExceptionInterface::JWT_CRV_UNSUPPORTED); } if (empty($jwk['x'])) { - throw new UnexpectedValueException('x not set', ExceptionCodes::JWT_X_MISSING); + throw new UnexpectedValueException('x not set', JwtExceptionInterface::JWT_X_MISSING); } // This library works internally with EdDSA keys (Ed25519) encoded in standard base64. diff --git a/src/JWT.php b/src/JWT.php index d77151fa..396ca8c0 100644 --- a/src/JWT.php +++ b/src/JWT.php @@ -104,14 +104,14 @@ public static function decode( if (empty($keyOrKeyArray)) { throw new InvalidArgumentException( 'Key may not be empty', - ExceptionCodes::KEY_NOT_EMPTY + JwtExceptionInterface::KEY_NOT_EMPTY ); } $tks = \explode('.', $jwt); if (\count($tks) !== 3) { throw new UnexpectedValueException( 'Wrong number of segments', - ExceptionCodes::WRONG_NUMBER_OF_SEGMENTS + JwtExceptionInterface::WRONG_NUMBER_OF_SEGMENTS ); } list($headb64, $bodyb64, $cryptob64) = $tks; @@ -119,7 +119,7 @@ public static function decode( if (null === ($header = static::jsonDecode($headerRaw))) { throw new UnexpectedValueException( 'Invalid header encoding', - ExceptionCodes::INVALID_HEADER_ENCODING + JwtExceptionInterface::INVALID_HEADER_ENCODING ); } if ($headers !== null) { @@ -129,7 +129,7 @@ public static function decode( if (null === ($payload = static::jsonDecode($payloadRaw))) { throw new UnexpectedValueException( 'Invalid claims encoding', - ExceptionCodes::INVALID_CLAIMS_ENCODING + JwtExceptionInterface::INVALID_CLAIMS_ENCODING ); } if (\is_array($payload)) { @@ -139,20 +139,20 @@ public static function decode( if (!$payload instanceof stdClass) { throw new UnexpectedValueException( 'Payload must be a JSON object', - ExceptionCodes::PAYLOAD_NOT_JSON + JwtExceptionInterface::PAYLOAD_NOT_JSON ); } $sig = static::urlsafeB64Decode($cryptob64); if (empty($header->alg)) { throw new UnexpectedValueException( 'Empty algorithm', - ExceptionCodes::EMPTY_ALGORITHM + JwtExceptionInterface::EMPTY_ALGORITHM ); } if (empty(static::$supported_algs[$header->alg])) { throw new UnexpectedValueException( 'Algorithm not supported', - ExceptionCodes::DECODE_ALGORITHM_NOT_SUPPORTED + JwtExceptionInterface::DECODE_ALGORITHM_NOT_SUPPORTED ); } @@ -163,7 +163,7 @@ public static function decode( // See issue #351 throw new UnexpectedValueException( 'Incorrect key for this algorithm', - ExceptionCodes::INCORRECT_KEY_FOR_ALGORITHM + JwtExceptionInterface::INCORRECT_KEY_FOR_ALGORITHM ); } if (\in_array($header->alg, ['ES256', 'ES256K', 'ES384'], true)) { @@ -173,7 +173,7 @@ public static function decode( if (!self::verify("{$headb64}.{$bodyb64}", $sig, $key->getKeyMaterial(), $header->alg)) { throw new SignatureInvalidException( 'Signature verification failed', - ExceptionCodes::SIGNATURE_VERIFICATION_FAILED + JwtExceptionInterface::SIGNATURE_VERIFICATION_FAILED ); } @@ -182,7 +182,7 @@ public static function decode( if (isset($payload->nbf) && floor($payload->nbf) > ($timestamp + static::$leeway)) { $ex = new BeforeValidException( 'Cannot handle token with nbf prior to ' . \date(DateTime::ISO8601, (int) $payload->nbf), - ExceptionCodes::NBF_PRIOR_TO_DATE + JwtExceptionInterface::NBF_PRIOR_TO_DATE ); $ex->setPayload($payload); throw $ex; @@ -194,7 +194,7 @@ public static function decode( if (!isset($payload->nbf) && isset($payload->iat) && floor($payload->iat) > ($timestamp + static::$leeway)) { $ex = new BeforeValidException( 'Cannot handle token with iat prior to ' . \date(DateTime::ISO8601, (int) $payload->iat), - ExceptionCodes::IAT_PRIOR_TO_DATE + JwtExceptionInterface::IAT_PRIOR_TO_DATE ); $ex->setPayload($payload); throw $ex; @@ -202,7 +202,7 @@ public static function decode( // Check if this token has expired. if (isset($payload->exp) && ($timestamp - static::$leeway) >= $payload->exp) { - $ex = new ExpiredException('Expired token', ExceptionCodes::TOKEN_EXPIRED); + $ex = new ExpiredException('Expired token', JwtExceptionInterface::TOKEN_EXPIRED); $ex->setPayload($payload); throw $ex; } @@ -271,7 +271,7 @@ public static function sign( if (empty(static::$supported_algs[$alg])) { throw new DomainException( 'Algorithm not supported', - ExceptionCodes::SIGN_ALGORITHM_NOT_SUPPORTED + JwtExceptionInterface::SIGN_ALGORITHM_NOT_SUPPORTED ); } list($function, $algorithm) = static::$supported_algs[$alg]; @@ -280,20 +280,20 @@ public static function sign( if (!\is_string($key)) { throw new InvalidArgumentException( 'key must be a string when using hmac', - ExceptionCodes::KEY_IS_NOT_STRING + JwtExceptionInterface::KEY_IS_NOT_STRING ); } return \hash_hmac($algorithm, $msg, $key, true); case 'openssl': $signature = ''; if (!\is_resource($key) && !openssl_pkey_get_private($key)) { - throw new DomainException('OpenSSL unable to validate key', ExceptionCodes::OPENSSL_SIGNATURE); + throw new DomainException('OpenSSL unable to validate key', JwtExceptionInterface::OPENSSL_SIGNATURE); } $success = \openssl_sign($msg, $signature, $key, $algorithm); // @phpstan-ignore-line if (!$success) { throw new DomainException( 'OpenSSL unable to sign data', - ExceptionCodes::OPENSSL_CAN_NOT_SIGN_DATA + JwtExceptionInterface::OPENSSL_CAN_NOT_SIGN_DATA ); } if ($alg === 'ES256' || $alg === 'ES256K') { @@ -305,13 +305,13 @@ public static function sign( case 'sodium_crypto': if (!\function_exists('sodium_crypto_sign_detached')) { throw new DomainException('libsodium is not available', - ExceptionCodes::SODIUM_FUNC_DOES_NOT_EXIST + JwtExceptionInterface::SODIUM_FUNC_DOES_NOT_EXIST ); } if (!\is_string($key)) { throw new InvalidArgumentException( 'key must be a string when using EdDSA', - ExceptionCodes::SODIUM_KEY_IS_NOT_STRING + JwtExceptionInterface::SODIUM_KEY_IS_NOT_STRING ); } try { @@ -321,14 +321,14 @@ public static function sign( if (\strlen($key) === 0) { throw new DomainException( 'Key cannot be empty string', - ExceptionCodes::SODIUM_KEY_LENGTH_ZERO + JwtExceptionInterface::SODIUM_KEY_LENGTH_ZERO ); } return sodium_crypto_sign_detached($msg, $key); } catch (Exception $e) { throw new DomainException( $e->getMessage(), - ExceptionCodes::SODIUM_EXCEPTION, + JwtExceptionInterface::SODIUM_EXCEPTION, $e ); } @@ -336,7 +336,7 @@ public static function sign( throw new DomainException( 'Algorithm not supported', - ExceptionCodes::SIGN_GENERAL_EXCEPTION + JwtExceptionInterface::SIGN_GENERAL_EXCEPTION ); } @@ -362,7 +362,7 @@ private static function verify( if (empty(static::$supported_algs[$alg])) { throw new DomainException( 'Algorithm not supported', - ExceptionCodes::VERIFY_ALGORITHM_NOT_SUPPORTED + JwtExceptionInterface::VERIFY_ALGORITHM_NOT_SUPPORTED ); } @@ -379,19 +379,19 @@ private static function verify( // returns 1 on success, 0 on failure, -1 on error. throw new DomainException( 'OpenSSL error: ' . \openssl_error_string(), - ExceptionCodes::VERIFY_OPEN_SSL_ERROR + JwtExceptionInterface::VERIFY_OPEN_SSL_ERROR ); case 'sodium_crypto': if (!\function_exists('sodium_crypto_sign_verify_detached')) { throw new DomainException( 'libsodium is not available', - ExceptionCodes::VERIFY_SODIUM_NOT_AVAILABLE + JwtExceptionInterface::VERIFY_SODIUM_NOT_AVAILABLE ); } if (!\is_string($keyMaterial)) { throw new InvalidArgumentException( 'key must be a string when using EdDSA', - ExceptionCodes::VERIFY_KEY_MATERIAL_IS_NOT_STRING + JwtExceptionInterface::VERIFY_KEY_MATERIAL_IS_NOT_STRING ); } try { @@ -401,20 +401,20 @@ private static function verify( if (\strlen($key) === 0) { throw new DomainException( 'Key cannot be empty string', - ExceptionCodes::SODIUM_VERIFY_KEY_LENGTH_ZERO + JwtExceptionInterface::SODIUM_VERIFY_KEY_LENGTH_ZERO ); } if (\strlen($signature) === 0) { throw new DomainException( 'Signature cannot be empty string', - ExceptionCodes::SODIUM_VERIFY_SIGNATURE_EMPTY + JwtExceptionInterface::SODIUM_VERIFY_SIGNATURE_EMPTY ); } return sodium_crypto_sign_verify_detached($signature, $msg, $key); } catch (Exception $e) { throw new DomainException( $e->getMessage(), - ExceptionCodes::VERIFY_SODIUM_EXCEPTION, + JwtExceptionInterface::VERIFY_SODIUM_EXCEPTION, $e ); } @@ -423,7 +423,7 @@ private static function verify( if (!\is_string($keyMaterial)) { throw new InvalidArgumentException( 'key must be a string when using hmac', - ExceptionCodes::VERIFY_KEY_IS_NOT_STRING + JwtExceptionInterface::VERIFY_KEY_IS_NOT_STRING ); } $hash = \hash_hmac($algorithm, $msg, $keyMaterial, true); @@ -449,7 +449,7 @@ public static function jsonDecode(string $input) } elseif ($obj === null && $input !== 'null') { throw new DomainException( 'Null result with non-null input', - ExceptionCodes::DECODED_JSON_IS_NULL + JwtExceptionInterface::DECODED_JSON_IS_NULL ); } return $obj; @@ -477,13 +477,13 @@ public static function jsonEncode(array $input): string } elseif ($json === 'null') { throw new DomainException( 'Null result with non-null input', - ExceptionCodes::ENCODED_JSON_IS_NULL + JwtExceptionInterface::ENCODED_JSON_IS_NULL ); } if ($json === false) { throw new DomainException( 'Provided object could not be encoded to valid JSON', - ExceptionCodes::INVALID_JSON + JwtExceptionInterface::INVALID_JSON ); } return $json; @@ -557,7 +557,7 @@ private static function getKey( if (empty($kid) && $kid !== '0') { throw new UnexpectedValueException( '"kid" empty, unable to lookup correct key', - ExceptionCodes::KID_IS_EMPTY + JwtExceptionInterface::KID_IS_EMPTY ); } @@ -569,7 +569,7 @@ private static function getKey( if (!isset($keyOrKeyArray[$kid])) { throw new UnexpectedValueException( '"kid" invalid, unable to lookup correct key', - ExceptionCodes::KID_IS_INVALID + JwtExceptionInterface::KID_IS_INVALID ); } @@ -619,7 +619,7 @@ private static function handleJsonError(int $errno): void isset($messages[$errno]) ? $messages[$errno] : 'Unknown JSON error: ' . $errno, - ExceptionCodes::JSON_ERROR + JwtExceptionInterface::JSON_ERROR ); } diff --git a/src/ExceptionCodes.php b/src/JwtExceptionInterface.php similarity index 98% rename from src/ExceptionCodes.php rename to src/JwtExceptionInterface.php index 57722f11..07c2d3b6 100644 --- a/src/ExceptionCodes.php +++ b/src/JwtExceptionInterface.php @@ -2,7 +2,7 @@ namespace Firebase\JWT; -class ExceptionCodes +interface JwtExceptionInterface { public const KEY_NOT_EMPTY = 1; public const WRONG_NUMBER_OF_SEGMENTS = 2; diff --git a/src/Key.php b/src/Key.php index 56811446..30cc85e8 100644 --- a/src/Key.php +++ b/src/Key.php @@ -30,21 +30,21 @@ public function __construct( ) { throw new TypeError( 'Key material must be a string, resource, or OpenSSLAsymmetricKey', - ExceptionCodes::KEY_MATERIAL_IS_INVALID + JwtExceptionInterface::KEY_MATERIAL_IS_INVALID ); } if (empty($keyMaterial)) { throw new InvalidArgumentException( 'Key material must not be empty', - ExceptionCodes::KEY_MATERIAL_IS_EMPTY + JwtExceptionInterface::KEY_MATERIAL_IS_EMPTY ); } if (empty($algorithm)) { throw new InvalidArgumentException( 'Algorithm must not be empty', - ExceptionCodes::KEY_ALGORITHM_IS_EMPTY + JwtExceptionInterface::KEY_ALGORITHM_IS_EMPTY ); } diff --git a/src/SignatureInvalidException.php b/src/SignatureInvalidException.php index d35dee9f..0e2815ac 100644 --- a/src/SignatureInvalidException.php +++ b/src/SignatureInvalidException.php @@ -2,6 +2,6 @@ namespace Firebase\JWT; -class SignatureInvalidException extends \UnexpectedValueException +class SignatureInvalidException extends \UnexpectedValueException implements JwtExceptionInterface { } From 8d8b372a00acca75f8e1a6ac14b874ee2f82712d Mon Sep 17 00:00:00 2001 From: nicumicle <20170987+nicumicleI@users.noreply.github.com> Date: Sat, 18 Jan 2025 13:29:15 +0100 Subject: [PATCH 16/17] Fix tests --- tests/CachedKeySetTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/CachedKeySetTest.php b/tests/CachedKeySetTest.php index 39bbc919..94656575 100644 --- a/tests/CachedKeySetTest.php +++ b/tests/CachedKeySetTest.php @@ -92,7 +92,7 @@ public function testInvalidHttpResponseThrowsException() { $this->expectException(\UnexpectedValueException::class); $this->expectExceptionMessage('HTTP Error: 404 URL not found'); - $this->expectExceptionCode(404); + $this->expectExceptionCode(JwtExceptionInterface::CACHED_KEY_GET_JWK); $response = $this->prophesize('Psr\Http\Message\ResponseInterface'); $response->getStatusCode() From 7528abed19de391041b79d318411b699a4f62fb2 Mon Sep 17 00:00:00 2001 From: nicumicle <20170987+nicumicleI@users.noreply.github.com> Date: Mon, 20 Jan 2025 06:38:47 +0100 Subject: [PATCH 17/17] fix tests --- tests/CachedKeySetTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/CachedKeySetTest.php b/tests/CachedKeySetTest.php index 94656575..942f98f6 100644 --- a/tests/CachedKeySetTest.php +++ b/tests/CachedKeySetTest.php @@ -92,7 +92,7 @@ public function testInvalidHttpResponseThrowsException() { $this->expectException(\UnexpectedValueException::class); $this->expectExceptionMessage('HTTP Error: 404 URL not found'); - $this->expectExceptionCode(JwtExceptionInterface::CACHED_KEY_GET_JWK); + $this->expectExceptionCode(58); $response = $this->prophesize('Psr\Http\Message\ResponseInterface'); $response->getStatusCode()