diff --git a/composer.json b/composer.json index ff2fbae..5424080 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "wpunit-test": "vendor/bin/codecept run wpunit" }, "require": { - "firebase/php-jwt": "^5.0" + "firebase/php-jwt": "6.1.0" }, "require-dev": { "lucatume/wp-browser": "3.1.0", diff --git a/src/Auth.php b/src/Auth.php index f9b3cdb..0103f22 100644 --- a/src/Auth.php +++ b/src/Auth.php @@ -2,9 +2,11 @@ namespace WPGraphQL\JWT_Authentication; +use Exception; use Firebase\JWT\JWT; +use Firebase\JWT\Key; use GraphQL\Error\UserError; -use WPGraphQL\Data\DataSource; +use WPGraphQL\Model\User; class Auth { @@ -35,7 +37,7 @@ public static function get_secret_key() { * @param string $password * * @return mixed - * @throws \Exception + * @throws Exception * @since 0.0.1 */ public static function login_and_get_token( $username, $password ) { @@ -44,7 +46,7 @@ public static function login_and_get_token( $username, $password ) { * First thing, check the secret key if not exist return a error */ if ( empty( self::get_secret_key() ) ) { - throw new UserError( __( 'JWT Auth is not configured correctly. Please contact a site administrator.', 'wp-graphql-jwt-authentication' ) ); + return new UserError( __( 'JWT Auth is not configured correctly. Please contact a site administrator.', 'wp-graphql-jwt-authentication' ) ); } /** @@ -78,7 +80,7 @@ public static function login_and_get_token( $username, $password ) { $response = [ 'authToken' => self::get_signed_token( wp_get_current_user() ), 'refreshToken' => self::get_refresh_token( wp_get_current_user() ), - 'user' => DataSource::resolve_user( $user->data->ID, \WPGraphQL::get_app_context() ), + 'user' => new User( $user ), 'id' => $user->data->ID, ]; @@ -88,7 +90,7 @@ public static function login_and_get_token( $username, $password ) { * @param \WP_User $user The authenticated user * @param array $response The default response */ - $response = apply_filters( 'graphql_jwt_auth_after_authenticate', $user, $response ); + $response = apply_filters( 'graphql_jwt_auth_after_authenticate', $response, $user ); return ! empty( $response ) ? $response : []; } @@ -186,7 +188,7 @@ protected static function get_signed_token( $user, $cap_check = true ) { * Encode the token */ JWT::$leeway = 60; - $token = JWT::encode( $token, self::get_secret_key() ); + $token = JWT::encode( $token, self::get_secret_key(), 'HS256' ); /** * Filter the token before returning it, allowing for individual systems to override what's returned. @@ -392,7 +394,7 @@ protected static function authenticate_user( $username, $password ) { * @param (int|bool) $user Logged User ID * * @return mixed|false|\WP_User - * @throws \Exception + * @throws Exception */ public static function filter_determine_current_user( $user ) { @@ -531,7 +533,7 @@ protected static function set_status( $status_code ) { * * @param string $token The encoded JWT Token * - * @throws \Exception + * @throws Exception * @return mixed|boolean|string */ public static function validate_token( $token = null, $refresh = false ) { @@ -577,26 +579,24 @@ public static function validate_token( $token = null, $refresh = false ) { return new \WP_Error( 'invalid-secret-key', __( 'JWT is not configured properly', 'wp-graphql-jwt-authentication' ) ); } - - /** * Decode the Token */ JWT::$leeway = 60; - $secret = self::get_secret_key(); + codecept_debug( [ 'tokenYo' => $token ] ); try { - $token = ! empty( $token ) ? JWT::decode( $token, $secret, [ 'HS256' ] ) : null; - } catch ( \Exception $exception ) { - return new \WP_Error( 'invalid-secret-key', $exception->getMessage() ); + $token = ! empty( $token ) ? JWT::decode( $token, new Key( self::get_secret_key(), 'HS256') ) : null; + } catch ( Exception $exception ) { + $token = new \WP_Error( 'invalid-secret-key', $exception->getMessage() ); } /** * If there's no token listed, just bail now before validating an empty token. * This will treat the request as a public request */ - if ( empty( $token ) ) { + if ( empty( $token ) || is_wp_error( $token ) ) { return $token; } @@ -614,7 +614,7 @@ public static function validate_token( $token = null, $refresh = false ) { * The Token is decoded now validate the iss */ - if ( ! isset( $token->iss ) || !in_array($token->iss, $allowed_domains) ) { + if ( ! isset( $token->iss ) || ! in_array( $token->iss, $allowed_domains ) ) { // See https://github.com/wp-graphql/wp-graphql-jwt-authentication/issues/111 self::set_status(401); return new \WP_Error( 'invalid-jwt', __( 'The iss do not match with this server', 'wp-graphql-jwt-authentication' ) ); diff --git a/tests/wpunit/AuthenticationTest.php b/tests/wpunit/AuthenticationTest.php index 97810a6..8757284 100644 --- a/tests/wpunit/AuthenticationTest.php +++ b/tests/wpunit/AuthenticationTest.php @@ -12,6 +12,8 @@ class AuthenticationTest extends \Codeception\TestCase\WPTestCase { public $admin; public $login_mutation; + public $admin_username; + public $admin_password; /** * This function is run before each method @@ -19,7 +21,6 @@ class AuthenticationTest extends \Codeception\TestCase\WPTestCase { */ public function setUp(): void { - $_SERVER['HTTP_AUTHORIZATION'] = 'Bearer goo'; add_filter( 'graphql_debug_enabled', '__return_true' ); add_filter( 'graphql_jwt_auth_secret_key', function() { @@ -28,10 +29,13 @@ public function setUp(): void { parent::setUp(); + $this->admin_password = 'testPassword'; + $this->admin_username = 'testuser'; + $this->admin = $this->factory->user->create( [ 'role' => 'administrator', - 'user_login' => 'testuser', - 'user_pass' => 'testPassword', + 'user_login' => $this->admin_username, + 'user_pass' => $this->admin_password, ] ); @@ -44,9 +48,13 @@ public function setUp(): void { pages{ edges{ node{ - id title content + author { + node { + databaseId + } + } } } } @@ -77,9 +85,9 @@ public function testLoginWithBadCredentials() { 'query' => $this->login_mutation, 'variables' => [ 'input' => [ - 'username' => 'testuser', + 'username' => $this->admin_username, 'password' => 'badPassword', - 'clientMutationId' => uniqid(), + 'clientMutationId' => uniqid( '', true ), ] ] ]); @@ -125,9 +133,8 @@ public function testLoginWithPage() { 'query' => $this->login_mutation, 'variables' => [ 'input' => [ - 'username' => 'testuser', - 'password' => 'testPassword', - 'clientMutationId' => uniqid(), + 'username' => $this->admin_username, + 'password' => $this->admin_password, ] ] ]); @@ -138,14 +145,18 @@ public function testLoginWithPage() { * Establish the expectation for the output of the query */ $expected_user = [ - 'username' => 'testuser', + 'username' => $this->admin_username, 'pages' => [ 'edges' => [ [ 'node' => [ - 'id' => $global_id, 'title' => 'Test Page Title', 'content' => apply_filters( 'the_content', $args['post_content'] ), + 'author' => [ + 'node' => [ + 'databaseId' => $this->admin + ], + ], ], ], ], @@ -175,9 +186,9 @@ public function testLoginWithNoSecretKeyConfigured() { 'query' => $this->login_mutation, 'variables' => [ 'input' => [ - 'username' => 'testuser', - 'password' => 'testPassword', - 'clientMutationId' => uniqid(), + 'username' => $this->admin_username, + 'password' => $this->admin_password, + 'clientMutationId' => uniqid( '', true ), ] ] ] ); @@ -189,14 +200,16 @@ public function testLoginWithNoSecretKeyConfigured() { } + public function filter_authentication () { + return 'goo'; + } + public function testLoginWithValidUserThatWasJustDeleted() { /** * Filter the authentication to make sure it returns an error */ - add_filter( 'authenticate', function() { - return 'goo'; - }, 9999 ); + add_filter( 'authenticate', [ $this, 'filter_authentication'], 9999 ); /** * Run the GraphQL query @@ -205,9 +218,9 @@ public function testLoginWithValidUserThatWasJustDeleted() { 'query' => $this->login_mutation, 'variables' => [ 'input' => [ - 'username' => 'testuser', - 'password' => 'testPassword', - 'clientMutationId' => uniqid(), + 'username' => $this->admin_username, + 'password' => $this->admin_password, + 'clientMutationId' => uniqid( '', true ), ] ] ]); @@ -217,6 +230,8 @@ public function testLoginWithValidUserThatWasJustDeleted() { */ $this->assertArrayHasKey( 'errors', $actual ); + remove_filter( 'authenticate', [ $this, 'filter_authentication'], 9999 ); + } public function testNonAuthenticatedRequest() { @@ -267,6 +282,8 @@ public function testRequestWithNoToken() { public function testRequestWithInvalidToken() { + wp_set_current_user( $this->admin ); + add_filter( 'graphql_jwt_auth_token_before_sign', function( $token ) { $token['iss'] = null; return $token; @@ -279,11 +296,15 @@ public function testRequestWithInvalidToken() { return 'Bearer ' . $token; } ); + codecept_debug( [ 'invalidToken' => $token ]); + /** * Validate the token (should not work because we filtered the iss to make it invalid) */ $token = \WPGraphQL\JWT_Authentication\Auth::validate_token( $token ); + codecept_debug( $token ); + /** * Validate token should return nothing if it can't be validated properly */ @@ -296,6 +317,8 @@ public function testRequestWithInvalidToken() { */ public function testNoSecretKey() { +// $_SERVER['HTTP_AUTHORIZATION'] = 'Bearer goo'; + /** * Filter the secret key to return null, which should cause an exception to be thrown */ @@ -303,20 +326,31 @@ public function testNoSecretKey() { return null; } ); - /** - * Set our expected exception - */ - $this->expectException( 'Exception', 'JWT is not configured properly' ); /** * Run the function to determine the current user */ $user = \WPGraphQL\JWT_Authentication\Auth::filter_determine_current_user( 0 ); + codecept_debug( [ 'user' => $user ] ); + + $actual = graphql([ + 'query' => $this->login_mutation, + 'variables' => [ + 'input' => [ + 'username' => $this->admin_username, + 'password' => $this->admin_password, + ] + ] + ]); + + codecept_debug( $actual ); + /** * Ensure that the Exception prevented any user from being authenticated */ - $this->assertEquals( 0, $user ); + $this->assertNull( $actual['data']['login'] ); + $this->assertArrayHasKey( 'errors', $actual ); } diff --git a/vendor/composer/InstalledVersions.php b/vendor/composer/InstalledVersions.php index ae70983..7e05a8f 100644 --- a/vendor/composer/InstalledVersions.php +++ b/vendor/composer/InstalledVersions.php @@ -30,7 +30,7 @@ class InstalledVersions 'aliases' => array ( ), - 'reference' => 'e09174b283f67cd17517d966c90d8800f66e1854', + 'reference' => 'f4518357b5349b7325e8c974f5deec2cdcf1ae1a', 'name' => 'wp-graphql/wp-graphql-jwt-authentication', ), 'versions' => @@ -215,12 +215,12 @@ class InstalledVersions ), 'firebase/php-jwt' => array ( - 'pretty_version' => 'v5.5.1', - 'version' => '5.5.1.0', + 'pretty_version' => 'v6.1.0', + 'version' => '6.1.0.0', 'aliases' => array ( ), - 'reference' => '83b609028194aa042ea33b5af2d41a7427de80e6', + 'reference' => 'fbb2967a3a68b07e37678c00c0cf51165051495f', ), 'flow/jsonpath' => array ( @@ -971,7 +971,7 @@ class InstalledVersions 'aliases' => array ( ), - 'reference' => 'e09174b283f67cd17517d966c90d8800f66e1854', + 'reference' => 'f4518357b5349b7325e8c974f5deec2cdcf1ae1a', ), 'zordius/lightncandy' => array ( diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json index b80f4d7..f0b1565 100644 --- a/vendor/composer/installed.json +++ b/vendor/composer/installed.json @@ -1143,29 +1143,29 @@ }, { "name": "firebase/php-jwt", - "version": "v5.5.1", - "version_normalized": "5.5.1.0", + "version": "v6.1.0", + "version_normalized": "6.1.0.0", "source": { "type": "git", "url": "https://github.com/firebase/php-jwt.git", - "reference": "83b609028194aa042ea33b5af2d41a7427de80e6" + "reference": "fbb2967a3a68b07e37678c00c0cf51165051495f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/firebase/php-jwt/zipball/83b609028194aa042ea33b5af2d41a7427de80e6", - "reference": "83b609028194aa042ea33b5af2d41a7427de80e6", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/fbb2967a3a68b07e37678c00c0cf51165051495f", + "reference": "fbb2967a3a68b07e37678c00c0cf51165051495f", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": "^7.1||^8.0" }, "require-dev": { - "phpunit/phpunit": ">=4.8 <=9" + "phpunit/phpunit": "^7.5||9.5" }, "suggest": { "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" }, - "time": "2021-11-08T20:18:51+00:00", + "time": "2022-03-23T18:26:04+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -1197,7 +1197,7 @@ ], "support": { "issues": "https://github.com/firebase/php-jwt/issues", - "source": "https://github.com/firebase/php-jwt/tree/v5.5.1" + "source": "https://github.com/firebase/php-jwt/tree/v6.1.0" }, "install-path": "../firebase/php-jwt" }, diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php index 9a9f982..209ffd8 100644 --- a/vendor/composer/installed.php +++ b/vendor/composer/installed.php @@ -6,7 +6,7 @@ 'aliases' => array ( ), - 'reference' => 'e09174b283f67cd17517d966c90d8800f66e1854', + 'reference' => 'f4518357b5349b7325e8c974f5deec2cdcf1ae1a', 'name' => 'wp-graphql/wp-graphql-jwt-authentication', ), 'versions' => @@ -191,12 +191,12 @@ ), 'firebase/php-jwt' => array ( - 'pretty_version' => 'v5.5.1', - 'version' => '5.5.1.0', + 'pretty_version' => 'v6.1.0', + 'version' => '6.1.0.0', 'aliases' => array ( ), - 'reference' => '83b609028194aa042ea33b5af2d41a7427de80e6', + 'reference' => 'fbb2967a3a68b07e37678c00c0cf51165051495f', ), 'flow/jsonpath' => array ( @@ -947,7 +947,7 @@ 'aliases' => array ( ), - 'reference' => 'e09174b283f67cd17517d966c90d8800f66e1854', + 'reference' => 'f4518357b5349b7325e8c974f5deec2cdcf1ae1a', ), 'zordius/lightncandy' => array ( diff --git a/vendor/composer/platform_check.php b/vendor/composer/platform_check.php index 7621d4f..6d3407d 100644 --- a/vendor/composer/platform_check.php +++ b/vendor/composer/platform_check.php @@ -4,8 +4,8 @@ $issues = array(); -if (!(PHP_VERSION_ID >= 50300)) { - $issues[] = 'Your Composer dependencies require a PHP version ">= 5.3.0". You are running ' . PHP_VERSION . '.'; +if (!(PHP_VERSION_ID >= 70100)) { + $issues[] = 'Your Composer dependencies require a PHP version ">= 7.1.0". You are running ' . PHP_VERSION . '.'; } if ($issues) { diff --git a/vendor/firebase/php-jwt/README.md b/vendor/firebase/php-jwt/README.md index 1d392cd..2dd7669 100644 --- a/vendor/firebase/php-jwt/README.md +++ b/vendor/firebase/php-jwt/README.md @@ -1,4 +1,4 @@ -[![Build Status](https://travis-ci.org/firebase/php-jwt.png?branch=master)](https://travis-ci.org/firebase/php-jwt) +![Build Status](https://github.com/firebase/php-jwt/actions/workflows/tests.yml/badge.svg) [![Latest Stable Version](https://poser.pugx.org/firebase/php-jwt/v/stable)](https://packagist.org/packages/firebase/php-jwt) [![Total Downloads](https://poser.pugx.org/firebase/php-jwt/downloads)](https://packagist.org/packages/firebase/php-jwt) [![License](https://poser.pugx.org/firebase/php-jwt/license)](https://packagist.org/packages/firebase/php-jwt) @@ -198,15 +198,44 @@ use Firebase\JWT\JWT; // this endpoint: https://www.gstatic.com/iap/verify/public_key-jwk $jwks = ['keys' => []]; -// JWK::parseKeySet($jwks) returns an associative array of **kid** to private -// key. Pass this as the second parameter to JWT::decode. -// NOTE: The deprecated $supportedAlgorithm must be supplied when parsing from JWK. -JWT::decode($payload, JWK::parseKeySet($jwks), $supportedAlgorithm); +// JWK::parseKeySet($jwks) returns an associative array of **kid** to Firebase\JWT\Key +// objects. Pass this as the second parameter to JWT::decode. +JWT::decode($payload, JWK::parseKeySet($jwks)); +``` + +Miscellaneous +------------- + +#### Casting to array + +The return value of `JWT::decode` is the generic PHP object `stdClass`. If you'd like to handle with arrays +instead, you can do the following: + +```php +// return type is stdClass +$decoded = JWT::decode($payload, $keys); + +// cast to array +$decoded = json_decode(json_encode($decoded), true); ``` Changelog --------- +#### 6.1.0 / 2022-03-23 + + - Drop support for PHP 5.3, 5.4, 5.5, 5.6, and 7.0 + - Add parameter typing and return types where possible + +#### 6.0.0 / 2022-01-24 + + - **Backwards-Compatibility Breaking Changes**: See the [Release Notes](https://github.com/firebase/php-jwt/releases/tag/v6.0.0) for more information. + - New Key object to prevent key/algorithm type confusion (#365) + - Add JWK support (#273) + - Add ES256 support (#256) + - Add ES384 support (#324) + - Add Ed25519 support (#343) + #### 5.0.0 / 2017-06-26 - Support RS384 and RS512. See [#117](https://github.com/firebase/php-jwt/pull/117). Thanks [@joostfaassen](https://github.com/joostfaassen)! diff --git a/vendor/firebase/php-jwt/composer.json b/vendor/firebase/php-jwt/composer.json index 6146e2d..5ef2ea2 100644 --- a/vendor/firebase/php-jwt/composer.json +++ b/vendor/firebase/php-jwt/composer.json @@ -20,7 +20,7 @@ ], "license": "BSD-3-Clause", "require": { - "php": ">=5.3.0" + "php": "^7.1||^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": "^7.5||9.5" } } diff --git a/vendor/firebase/php-jwt/phpstan.neon.dist b/vendor/firebase/php-jwt/phpstan.neon.dist new file mode 100644 index 0000000..56aeebf --- /dev/null +++ b/vendor/firebase/php-jwt/phpstan.neon.dist @@ -0,0 +1,5 @@ +parameters: + level: 7 + paths: + - src + treatPhpDocTypesAsCertain: false diff --git a/vendor/firebase/php-jwt/src/JWK.php b/vendor/firebase/php-jwt/src/JWK.php index 981a9ba..dbc446e 100644 --- a/vendor/firebase/php-jwt/src/JWK.php +++ b/vendor/firebase/php-jwt/src/JWK.php @@ -23,9 +23,9 @@ class JWK /** * Parse a set of JWK keys * - * @param array $jwks The JSON Web Key Set as an associative array + * @param array $jwks The JSON Web Key Set as an associative array * - * @return array An associative array that represents the set of keys + * @return array An associative array of key IDs (kid) to Key objects * * @throws InvalidArgumentException Provided JWK Set is empty * @throws UnexpectedValueException Provided JWK Set was invalid @@ -33,13 +33,14 @@ class JWK * * @uses parseKey */ - public static function parseKeySet(array $jwks) + public static function parseKeySet(array $jwks): array { - $keys = array(); + $keys = []; if (!isset($jwks['keys'])) { throw new UnexpectedValueException('"keys" member must exist in the JWK Set'); } + if (empty($jwks['keys'])) { throw new InvalidArgumentException('JWK Set did not contain any keys'); } @@ -47,7 +48,7 @@ public static function parseKeySet(array $jwks) foreach ($jwks['keys'] as $k => $v) { $kid = isset($v['kid']) ? $v['kid'] : $k; if ($key = self::parseKey($v)) { - $keys[$kid] = $key; + $keys[(string) $kid] = $key; } } @@ -61,9 +62,9 @@ public static function parseKeySet(array $jwks) /** * Parse a JWK key * - * @param array $jwk An individual JWK + * @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 @@ -71,15 +72,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 an "alg" parameter'); + } + switch ($jwk['kty']) { case 'RSA': if (!empty($jwk['d'])) { @@ -96,11 +105,13 @@ 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; } + + return null; } /** @@ -113,22 +124,22 @@ public static function parseKey(array $jwk) * * @uses encodeLength */ - private static function createPemFromModulusAndExponent($n, $e) - { - $modulus = JWT::urlsafeB64Decode($n); - $publicExponent = JWT::urlsafeB64Decode($e); + private static function createPemFromModulusAndExponent( + string $n, + string $e + ): string { + $mod = JWT::urlsafeB64Decode($n); + $exp = JWT::urlsafeB64Decode($e); - $components = array( - 'modulus' => \pack('Ca*a*', 2, self::encodeLength(\strlen($modulus)), $modulus), - 'publicExponent' => \pack('Ca*a*', 2, self::encodeLength(\strlen($publicExponent)), $publicExponent) - ); + $modulus = \pack('Ca*a*', 2, self::encodeLength(\strlen($mod)), $mod); + $publicExponent = \pack('Ca*a*', 2, self::encodeLength(\strlen($exp)), $exp); $rsaPublicKey = \pack( 'Ca*a*a*', 48, - self::encodeLength(\strlen($components['modulus']) + \strlen($components['publicExponent'])), - $components['modulus'], - $components['publicExponent'] + self::encodeLength(\strlen($modulus) + \strlen($publicExponent)), + $modulus, + $publicExponent ); // sequence(oid(1.2.840.113549.1.1.1), null)) = rsaEncryption. @@ -159,7 +170,7 @@ private static function createPemFromModulusAndExponent($n, $e) * @param int $length * @return string */ - private static function encodeLength($length) + private static function encodeLength(int $length): string { if ($length <= 0x7F) { return \chr($length); diff --git a/vendor/firebase/php-jwt/src/JWT.php b/vendor/firebase/php-jwt/src/JWT.php index ec1641b..843d0ae 100644 --- a/vendor/firebase/php-jwt/src/JWT.php +++ b/vendor/firebase/php-jwt/src/JWT.php @@ -7,8 +7,11 @@ use Exception; use InvalidArgumentException; use OpenSSLAsymmetricKey; +use OpenSSLCertificate; +use TypeError; use UnexpectedValueException; use DateTime; +use stdClass; /** * JSON Web Token implementation, based on this spec: @@ -25,52 +28,57 @@ */ class JWT { - const ASN1_INTEGER = 0x02; - const ASN1_SEQUENCE = 0x10; - const ASN1_BIT_STRING = 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. + * + * @var int */ 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. + * + * @var ?int */ 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'), - ); + /** + * @var array + */ + public static $supported_algs = [ + 'ES384' => ['openssl', 'SHA384'], + 'ES256' => ['openssl', 'SHA256'], + 'HS256' => ['hash_hmac', 'SHA256'], + 'HS384' => ['hash_hmac', 'SHA384'], + 'HS512' => ['hash_hmac', 'SHA512'], + 'RS256' => ['openssl', 'SHA256'], + 'RS384' => ['openssl', 'SHA384'], + 'RS512' => ['openssl', 'SHA512'], + 'EdDSA' => ['sodium_crypto', 'EdDSA'], + ]; /** * Decodes a JWT string into a PHP object. * - * @param string $jwt The JWT - * @param Key|array|mixed $keyOrKeyArray The Key or array of Key objects. - * If the algorithm used is asymmetric, this is the public key - * Each Key object contains an algorithm and matching key. - * Supported algorithms are 'ES384','ES256', 'HS256', 'HS384', - * 'HS512', 'RS256', 'RS384', and 'RS512' - * @param array $allowed_algs [DEPRECATED] List of supported verification algorithms. Only - * should be used for backwards compatibility. + * @param string $jwt The JWT + * @param Key|array $keyOrKeyArray The Key or associative array of key IDs (kid) to Key objects. + * If the algorithm used is asymmetric, this is the public key + * Each Key object contains an algorithm and matching key. + * Supported algorithms are 'ES384','ES256', 'HS256', 'HS384', + * 'HS512', 'RS256', 'RS384', and 'RS512' * - * @return object The JWT's payload as a PHP object + * @return stdClass The JWT's payload as a PHP object * - * @throws InvalidArgumentException Provided JWT was empty + * @throws InvalidArgumentException Provided key/key-array was empty + * @throws DomainException Provided JWT is malformed * @throws UnexpectedValueException Provided JWT was invalid * @throws SignatureInvalidException Provided JWT was invalid because the signature verification failed * @throws BeforeValidException Provided JWT is trying to be used before it's eligible as defined by 'nbf' @@ -80,8 +88,11 @@ class JWT * @uses jsonDecode * @uses urlsafeB64Decode */ - public static function decode($jwt, $keyOrKeyArray, array $allowed_algs = array()) - { + public static function decode( + string $jwt, + $keyOrKeyArray + ): stdClass { + // Validate JWT $timestamp = \is_null(static::$timestamp) ? \time() : static::$timestamp; if (empty($keyOrKeyArray)) { @@ -92,15 +103,18 @@ public static function decode($jwt, $keyOrKeyArray, array $allowed_algs = array( throw new UnexpectedValueException('Wrong number of segments'); } list($headb64, $bodyb64, $cryptob64) = $tks; - if (null === ($header = static::jsonDecode(static::urlsafeB64Decode($headb64)))) { + $headerRaw = static::urlsafeB64Decode($headb64); + if (null === ($header = static::jsonDecode($headerRaw))) { throw new UnexpectedValueException('Invalid header encoding'); } - if (null === $payload = static::jsonDecode(static::urlsafeB64Decode($bodyb64))) { + $payloadRaw = static::urlsafeB64Decode($bodyb64); + if (null === ($payload = static::jsonDecode($payloadRaw))) { throw new UnexpectedValueException('Invalid claims encoding'); } - if (false === ($sig = static::urlsafeB64Decode($cryptob64))) { - throw new UnexpectedValueException('Invalid signature encoding'); + if (!$payload instanceof stdClass) { + throw new UnexpectedValueException('Payload must be a JSON object'); } + $sig = static::urlsafeB64Decode($cryptob64); if (empty($header->alg)) { throw new UnexpectedValueException('Empty algorithm'); } @@ -108,31 +122,18 @@ public static function decode($jwt, $keyOrKeyArray, array $allowed_algs = array( throw new UnexpectedValueException('Algorithm not supported'); } - list($keyMaterial, $algorithm) = self::getKeyMaterialAndAlgorithm( - $keyOrKeyArray, - empty($header->kid) ? null : $header->kid - ); + $key = self::getKey($keyOrKeyArray, property_exists($header, 'kid') ? $header->kid : null); - if (empty($algorithm)) { - // Use deprecated "allowed_algs" to determine if the algorithm is supported. - // This opens up the possibility of an attack in some implementations. - // @see https://github.com/firebase/php-jwt/issues/351 - if (!\in_array($header->alg, $allowed_algs)) { - throw new UnexpectedValueException('Algorithm not allowed'); - } - } else { - // Check the algorithm - if (!self::constantTimeEquals($algorithm, $header->alg)) { - // See issue #351 - throw new UnexpectedValueException('Incorrect key for this algorithm'); - } + // Check the algorithm + if (!self::constantTimeEquals($key->getAlgorithm(), $header->alg)) { + // See issue #351 + throw new UnexpectedValueException('Incorrect key for this algorithm'); } if ($header->alg === 'ES256' || $header->alg === 'ES384') { // OpenSSL expects an ASN.1 DER sequence for ES256/ES384 signatures $sig = self::signatureToDER($sig); } - - if (!static::verify("$headb64.$bodyb64", $sig, $keyMaterial, $header->alg)) { + if (!self::verify("$headb64.$bodyb64", $sig, $key->getKeyMaterial(), $header->alg)) { throw new SignatureInvalidException('Signature verification failed'); } @@ -164,32 +165,33 @@ public static function decode($jwt, $keyOrKeyArray, array $allowed_algs = array( /** * Converts and signs a PHP object or array into a JWT string. * - * @param object|array $payload PHP object or array - * @param string|resource $key The secret key. - * If the algorithm used is asymmetric, this is the private key - * @param string $alg The signing algorithm. - * Supported algorithms are 'ES384','ES256', 'HS256', 'HS384', - * 'HS512', 'RS256', 'RS384', and 'RS512' - * @param mixed $keyId - * @param array $head An array with header elements to attach + * @param array $payload PHP array + * @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $key The secret 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 = 'HS256', $keyId = null, $head = null) - { - $header = array('typ' => 'JWT', 'alg' => $alg); + public static function encode( + array $payload, + $key, + string $alg, + string $keyId = null, + array $head = null + ): string { + $header = ['typ' => 'JWT', 'alg' => $alg]; if ($keyId !== null) { $header['kid'] = $keyId; } if (isset($head) && \is_array($head)) { $header = \array_merge($head, $header); } - $segments = array(); - $segments[] = static::urlsafeB64Encode(static::jsonEncode($header)); - $segments[] = static::urlsafeB64Encode(static::jsonEncode($payload)); + $segments = []; + $segments[] = static::urlsafeB64Encode((string) static::jsonEncode($header)); + $segments[] = static::urlsafeB64Encode((string) static::jsonEncode($payload)); $signing_input = \implode('.', $segments); $signature = static::sign($signing_input, $key, $alg); @@ -201,28 +203,33 @@ public static function encode($payload, $key, $alg = 'HS256', $keyId = null, $he /** * 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|OpenSSLCertificate $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 = 'HS256') - { + public static function sign( + string $msg, + $key, + string $alg + ): string { if (empty(static::$supported_algs[$alg])) { throw new DomainException('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'); + } return \hash_hmac($algorithm, $msg, $key, true); case 'openssl': $signature = ''; - $success = \openssl_sign($msg, $signature, $key, $algorithm); + $success = \openssl_sign($msg, $signature, $key, $algorithm); // @phpstan-ignore-line if (!$success) { throw new DomainException("OpenSSL unable to sign data"); } @@ -236,32 +243,41 @@ public static function sign($msg, $key, $alg = 'HS256') if (!function_exists('sodium_crypto_sign_detached')) { throw new DomainException('libsodium is not available'); } + if (!is_string($key)) { + throw new InvalidArgumentException('key must be a string when using EdDSA'); + } try { // The last non-empty line is used as the key. $lines = array_filter(explode("\n", $key)); - $key = base64_decode(end($lines)); + $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('Algorithm not supported'); } /** * 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|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $keyMaterial For HS*, a string key works. for RS*, must be an instance of OpenSSLAsymmetricKey + * @param string $alg The algorithm * * @return bool * * @throws DomainException Invalid Algorithm, bad key, or OpenSSL failure */ - private static function verify($msg, $signature, $key, $alg) - { + private static function verify( + string $msg, + string $signature, + $keyMaterial, + string $alg + ): bool { if (empty(static::$supported_algs[$alg])) { throw new DomainException('Algorithm not supported'); } @@ -269,7 +285,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); // @phpstan-ignore-line if ($success === 1) { return true; } elseif ($success === 0) { @@ -283,18 +299,24 @@ private static function verify($msg, $signature, $key, $alg) if (!function_exists('sodium_crypto_sign_verify_detached')) { throw new DomainException('libsodium is not available'); } + if (!is_string($keyMaterial)) { + throw new InvalidArgumentException('key must be a string when using EdDSA'); + } try { // The last non-empty line is used as the key. - $lines = array_filter(explode("\n", $key)); - $key = base64_decode(end($lines)); + $lines = array_filter(explode("\n", $keyMaterial)); + $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); } case 'hash_hmac': default: - $hash = \hash_hmac($algorithm, $msg, $key, true); - return self::constantTimeEquals($signature, $hash); + if (!is_string($keyMaterial)) { + throw new InvalidArgumentException('key must be a string when using hmac'); + } + $hash = \hash_hmac($algorithm, $msg, $keyMaterial, true); + return self::constantTimeEquals($hash, $signature); } } @@ -303,30 +325,16 @@ private static function verify($msg, $signature, $key, $alg) * * @param string $input JSON string * - * @return object Object representation of JSON string + * @return mixed The decoded JSON string * * @throws DomainException Provided string was invalid JSON */ - public static function jsonDecode($input) + public static function jsonDecode(string $input) { - 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); + self::handleJsonError($errno); } elseif ($obj === null && $input !== 'null') { throw new DomainException('Null result with non-null input'); } @@ -334,22 +342,30 @@ public static function jsonDecode($input) } /** - * Encode a PHP object into a JSON string. + * Encode a PHP array into a JSON string. * - * @param object|array $input A PHP object or array + * @param array $input A PHP array * - * @return string JSON representation of the PHP object or array + * @return string JSON representation of the PHP array * * @throws DomainException Provided object could not be encoded to valid JSON */ - public static function jsonEncode($input) + public static function jsonEncode(array $input): string { - $json = \json_encode($input); + if (PHP_VERSION_ID >= 50400) { + $json = \json_encode($input, \JSON_UNESCAPED_SLASHES); + } else { + // PHP 5.3 only + $json = \json_encode($input); + } if ($errno = \json_last_error()) { - static::handleJsonError($errno); + self::handleJsonError($errno); } elseif ($json === 'null' && $input !== null) { throw new DomainException('Null result with non-null input'); } + if ($json === false) { + throw new DomainException('Provided object could not be encoded to valid JSON'); + } return $json; } @@ -359,8 +375,10 @@ public static function jsonEncode($input) * @param string $input A Base64 encoded string * * @return string A decoded string + * + * @throws InvalidArgumentException invalid base64 characters */ - public static function urlsafeB64Decode($input) + public static function urlsafeB64Decode(string $input): string { $remainder = \strlen($input) % 4; if ($remainder) { @@ -377,7 +395,7 @@ public static function urlsafeB64Decode($input) * * @return string The base64 encode of what you passed in */ - public static function urlsafeB64Encode($input) + public static function urlsafeB64Encode(string $input): string { return \str_replace('=', '', \strtr(\base64_encode($input), '+/', '-_')); } @@ -386,67 +404,56 @@ public static function urlsafeB64Encode($input) /** * Determine if an algorithm has been provided for each Key * - * @param Key|array|mixed $keyOrKeyArray - * @param string|null $kid + * @param Key|array $keyOrKeyArray + * @param string|null $kid * * @throws UnexpectedValueException * - * @return array containing the keyMaterial and algorithm + * @return Key */ - private static function getKeyMaterialAndAlgorithm($keyOrKeyArray, $kid = null) - { - if ( - is_string($keyOrKeyArray) - || is_resource($keyOrKeyArray) - || $keyOrKeyArray instanceof OpenSSLAsymmetricKey - ) { - return array($keyOrKeyArray, null); - } - + private static function getKey( + $keyOrKeyArray, + ?string $kid + ): Key { if ($keyOrKeyArray instanceof Key) { - return array($keyOrKeyArray->getKeyMaterial(), $keyOrKeyArray->getAlgorithm()); + return $keyOrKeyArray; } - if (is_array($keyOrKeyArray) || $keyOrKeyArray instanceof ArrayAccess) { - if (!isset($kid)) { - throw new UnexpectedValueException('"kid" empty, unable to lookup correct key'); - } - if (!isset($keyOrKeyArray[$kid])) { - throw new UnexpectedValueException('"kid" invalid, unable to lookup correct key'); - } - - $key = $keyOrKeyArray[$kid]; - - if ($key instanceof Key) { - return array($key->getKeyMaterial(), $key->getAlgorithm()); + foreach ($keyOrKeyArray as $keyId => $key) { + if (!$key instanceof Key) { + throw new TypeError( + '$keyOrKeyArray must be an instance of Firebase\JWT\Key key or an ' + . 'array of Firebase\JWT\Key keys' + ); } - - return array($key, null); + } + if (!isset($kid)) { + throw new UnexpectedValueException('"kid" empty, unable to lookup correct key'); + } + if (!isset($keyOrKeyArray[$kid])) { + throw new UnexpectedValueException('"kid" invalid, unable to lookup correct key'); } - throw new UnexpectedValueException( - '$keyOrKeyArray must be a string|resource key, an array of string|resource keys, ' - . 'an instance of Firebase\JWT\Key key or an array of Firebase\JWT\Key keys' - ); + return $keyOrKeyArray[$kid]; } /** - * @param string $left - * @param string $right + * @param string $left The string of known length to compare against + * @param string $right The user-supplied string * @return bool */ - public static function constantTimeEquals($left, $right) + public static function constantTimeEquals(string $left, string $right): bool { if (\function_exists('hash_equals')) { return \hash_equals($left, $right); } - $len = \min(static::safeStrlen($left), static::safeStrlen($right)); + $len = \min(self::safeStrlen($left), self::safeStrlen($right)); $status = 0; for ($i = 0; $i < $len; $i++) { $status |= (\ord($left[$i]) ^ \ord($right[$i])); } - $status |= (static::safeStrlen($left) ^ static::safeStrlen($right)); + $status |= (self::safeStrlen($left) ^ self::safeStrlen($right)); return ($status === 0); } @@ -456,17 +463,19 @@ public static function constantTimeEquals($left, $right) * * @param int $errno An error number from json_last_error() * + * @throws DomainException + * * @return void */ - private static function handleJsonError($errno) + private static function handleJsonError(int $errno): void { - $messages = array( + $messages = [ JSON_ERROR_DEPTH => 'Maximum stack depth exceeded', JSON_ERROR_STATE_MISMATCH => 'Invalid or malformed JSON', JSON_ERROR_CTRL_CHAR => 'Unexpected control character found', JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON', JSON_ERROR_UTF8 => 'Malformed UTF-8 characters' //PHP >= 5.3.3 - ); + ]; throw new DomainException( isset($messages[$errno]) ? $messages[$errno] @@ -481,7 +490,7 @@ private static function handleJsonError($errno) * * @return int */ - private static function safeStrlen($str) + private static function safeStrlen(string $str): int { if (\function_exists('mb_strlen')) { return \mb_strlen($str, '8bit'); @@ -495,10 +504,11 @@ 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)); + $length = max(1, (int) (\strlen($sig) / 2)); + list($r, $s) = \str_split($sig, $length > 0 ? $length : 1); // Trim leading zeros $r = \ltrim($r, "\x00"); @@ -525,9 +535,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) { @@ -548,9 +559,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); @@ -575,9 +587,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 + * + * @return array{int, string|null} 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); @@ -606,6 +619,6 @@ private static function readDER($der, $offset = 0) $data = null; } - return array($pos, $data); + return [$pos, $data]; } } diff --git a/vendor/firebase/php-jwt/src/Key.php b/vendor/firebase/php-jwt/src/Key.php index f1ede6f..b09ad19 100644 --- a/vendor/firebase/php-jwt/src/Key.php +++ b/vendor/firebase/php-jwt/src/Key.php @@ -2,39 +2,44 @@ namespace Firebase\JWT; -use InvalidArgumentException; use OpenSSLAsymmetricKey; +use OpenSSLCertificate; +use TypeError; +use InvalidArgumentException; class Key { - /** @var string $algorithm */ - private $algorithm; - - /** @var string|resource|OpenSSLAsymmetricKey $keyMaterial */ + /** @var string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate */ private $keyMaterial; + /** @var string */ + private $algorithm; /** - * @param string|resource|OpenSSLAsymmetricKey $keyMaterial + * @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $keyMaterial * @param string $algorithm */ - public function __construct($keyMaterial, $algorithm) - { + public function __construct( + $keyMaterial, + string $algorithm + ) { if ( !is_string($keyMaterial) - && !is_resource($keyMaterial) && !$keyMaterial instanceof OpenSSLAsymmetricKey + && !$keyMaterial instanceof OpenSSLCertificate + && !is_resource($keyMaterial) ) { - 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'); } + // TODO: Remove in PHP 8.0 in favor of class constructor property promotion $this->keyMaterial = $keyMaterial; $this->algorithm = $algorithm; } @@ -44,13 +49,13 @@ public function __construct($keyMaterial, $algorithm) * * @return string */ - public function getAlgorithm() + public function getAlgorithm(): string { return $this->algorithm; } /** - * @return string|resource|OpenSSLAsymmetricKey + * @return string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate */ public function getKeyMaterial() { diff --git a/wp-graphql-jwt-authentication.php b/wp-graphql-jwt-authentication.php index d7b9158..fcf1b4f 100644 --- a/wp-graphql-jwt-authentication.php +++ b/wp-graphql-jwt-authentication.php @@ -192,7 +192,7 @@ private static function init() { $jwt_secret = Auth::get_secret_key(); if ( empty( $jwt_secret ) || 'graphql-jwt-auth' === $jwt_secret ) { - throw new \Exception( __( 'You must define the GraphQL JWT Auth secret to use the WPGraphQL JWT Authentication plugin.', 'graphql-jwt-auth' ) ); + graphql_debug( __( 'You must define the GraphQL JWT Auth secret to use the WPGraphQL JWT Authentication plugin.', 'graphql-jwt-auth' ) ); } else { $token = Auth::validate_token(); if ( is_wp_error( $token ) ) {