From ba259062cd65a3d52b73cb423d0a294cdcd57de5 Mon Sep 17 00:00:00 2001 From: Matteo Gaggiano Date: Fri, 15 Mar 2024 17:04:50 +0100 Subject: [PATCH 01/29] docs: update .gitignore to ignore .idea directory --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 59ac2a2..174a021 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /.vscode +/.idea /node_modules /vendor /.phpunit.result.cache From 1567f530716ef1dd6d99743312e8909ffaf006e7 Mon Sep 17 00:00:00 2001 From: Matteo Gaggiano Date: Fri, 15 Mar 2024 17:13:03 +0100 Subject: [PATCH 02/29] feat(refresh token): add possibility to generate and return a refresh_token in token generation response --- class-auth.php | 316 +++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 253 insertions(+), 63 deletions(-) diff --git a/class-auth.php b/class-auth.php index 21784cd..8b45b2b 100644 --- a/class-auth.php +++ b/class-auth.php @@ -8,15 +8,13 @@ namespace JWTAuth; use Exception; - +use Firebase\JWT\JWT; +use Firebase\JWT\Key; use WP_Error; use WP_REST_Request; use WP_REST_Response; use WP_REST_Server; -use Firebase\JWT\JWT; -use Firebase\JWT\Key; - /** * The public-facing functionality of the plugin. */ @@ -102,7 +100,7 @@ public function register_rest_routes() { public function add_cors_support() { $enable_cors = defined( 'JWT_AUTH_CORS_ENABLE' ) ? JWT_AUTH_CORS_ENABLE : false; - if ( $enable_cors && !headers_sent() ) { + if ( $enable_cors && ! headers_sent() ) { $headers = apply_filters( 'jwt_auth_cors_allow_headers', 'X-Requested-With, Content-Type, Accept, Origin, Authorization, Cookie' ); header( sprintf( 'Access-Control-Allow-Headers: %s', $headers ) ); @@ -114,7 +112,7 @@ public function add_cors_support() { * * @param string $username The username. * @param string $password The password. - * @param mixed $custom_auth The custom auth data (if any). + * @param mixed $custom_auth The custom auth data (if any). * * @return WP_User|WP_Error $user Returns WP_User object if success, or WP_Error if failed. */ @@ -139,6 +137,7 @@ public function authenticate_user( $username, $password, $custom_auth = '' ) { * Get token by sending POST request to jwt-auth/v1/token. * * @param WP_REST_Request $request The request. + * * @return WP_REST_Response The response. */ public function get_token( WP_REST_Request $request ) { @@ -162,15 +161,16 @@ public function get_token( WP_REST_Request $request ) { ); } - if ( isset( $_COOKIE['refresh_token'] ) ) { - $device = $request->get_param( 'device' ) ?: ''; - $user_id = $this->validate_refresh_token( $_COOKIE['refresh_token'], $device ); + $refresh_token = $this->retrieve_refresh_token( $request ); + + if ( ! empty( $refresh_token ) ) { + $payload = $this->validate_refresh_token( $request, false ); // If we receive a REST response, then validation failed. - if ( $user_id instanceof WP_REST_Response ) { - return $user_id; + if ( $payload instanceof WP_REST_Response ) { + return $payload; } - $user = get_user_by( 'id', $user_id ); + $user = get_user_by( 'id', $payload->data->user->id ); } else { $user = $this->authenticate_user( $username, $password, $custom_auth ); } @@ -196,17 +196,19 @@ public function get_token( WP_REST_Request $request ) { // Add the refresh token as a HttpOnly cookie to the response. if ( $username && $password ) { - $this->send_refresh_token( $user, $request ); + $refresh_token = $this->send_refresh_token( $user, $request ); } + $response['data']['refresh_token'] = $refresh_token; + return $response; } /** * Generate access token. * - * @param WP_User $user The WP_User object. - * @param bool $return_raw Whether or not to return as raw token string. + * @param \WP_User $user The WP_User object. + * @param bool $return_raw Whether or not to return as raw token string. * * @return WP_REST_Response|string Return as raw token string or as a formatted WP_REST_Response. */ @@ -267,15 +269,24 @@ public function generate_token( $user, $return_raw = true ) { * @param \WP_User $user The WP_User object. * @param \WP_REST_Request $request The request. * - * @return void + * @return string */ public function send_refresh_token( \WP_User $user, \WP_REST_Request $request ) { - $refresh_token = bin2hex( random_bytes( 32 ) ); - $created = time(); - $expires = $created + DAY_IN_SECONDS * 30; - $expires = apply_filters( 'jwt_auth_refresh_expire', $expires, $created ); + $secret_key = defined( 'JWT_AUTH_SECRET_KEY' ) ? JWT_AUTH_SECRET_KEY : false; + $refresh_token = $this->generate_refresh_token( $user, $request ); - setcookie( 'refresh_token', $user->ID . '.' . $refresh_token, $expires, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true ); + $alg = $this->get_alg(); + + $payload = JWT::decode( $refresh_token, new Key( $secret_key, $alg ) ); + + $flow = $this->get_flow(); + + if ( 'parameter' === $flow ) { + + } else { + // Send the refresh token as a HttpOnly cookie in the response. + setcookie( 'refresh_token', $refresh_token, $payload->exp, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true ); + } // Save new refresh token for the user, replacing the previous one. // The refresh token is rotated for the passed device only, not affecting @@ -284,21 +295,66 @@ public function send_refresh_token( \WP_User $user, \WP_REST_Request $request ) if ( ! is_array( $user_refresh_tokens ) ) { $user_refresh_tokens = array(); } - $device = $request->get_param( 'device' ) ?: ''; + $device = empty($payload->data->device) ? '' : $payload->data->device; $user_refresh_tokens[ $device ] = array( 'token' => $refresh_token, - 'expires' => $expires, + 'expires' => $payload->exp, ); update_user_meta( $user->ID, 'jwt_auth_refresh_tokens', $user_refresh_tokens ); // Store next expiry for cron_purge_expired_refresh_tokens event. - $expires_next = $expires; + $expires_next = $payload->exp; foreach ( $user_refresh_tokens as $device ) { if ( $device['expires'] < $expires_next ) { $expires_next = $device['expires']; } } update_user_meta( $user->ID, 'jwt_auth_refresh_tokens_expires_next', $expires_next ); + + return $refresh_token; + } + + /** + * Generate a new refresh token. + * + * @param \WP_User $user The WP_User object. + * @param \WP_REST_Request $request The request. + * + * @return string + */ + public function generate_refresh_token( \WP_User $user, \WP_REST_Request $request ) { + $secret_key = defined( 'JWT_AUTH_SECRET_KEY' ) ? JWT_AUTH_SECRET_KEY : false; + $created = time(); + $expires = $created + DAY_IN_SECONDS * 30; + $expires = apply_filters( 'jwt_auth_refresh_expire', $expires, $created ); + + $device = $request->get_param( 'device' ) ?: ''; + + $payload = array( + 'iss' => $this->get_iss(), + 'iat' => $created, + 'nbf' => $created, + 'exp' => $expires, + 'data' => array( + 'device' => $device, + 'user' => array( + 'id' => $user->ID, + ), + ), + ); + + $alg = $this->get_alg(); + + return JWT::encode( apply_filters( 'jwt_auth_refresh_token_payload', $payload, $user ), $secret_key, $alg ); + } + + /** + * Get the refresh token flow + * + * @return mixed|null + */ + public function get_flow() { + return apply_filters( 'jwt_auth_flow', 'cookie' ); } /** @@ -325,6 +381,7 @@ public function get_alg() { * Determine if given response is an error response. * * @param mixed $response The response. + * * @return boolean */ public function is_error_response( $response ) { @@ -340,21 +397,24 @@ public function is_error_response( $response ) { /** * Public token validation function based on Authorization header. * - * @param bool $return_response Either to return full WP_REST_Response or to return the payload only. + * @param bool|WP_REST_Request $return_response Either to return full WP_REST_Response or to return the payload only. * - * @return WP_REST_Response | Array Returns WP_REST_Response or token's $payload. + * @return \stdClass|WP_REST_Response Returns WP_REST_Response or token's $payload. */ - public function validate_token( $return_response = true ) { + public function validate_token( $return_response_or_request = true ) { + $return_response = $return_response_or_request instanceof WP_REST_Request ? true : $return_response_or_request; + $request = $return_response_or_request instanceof WP_REST_Request ? $return_response_or_request : null; + /** * Looking for the HTTP_AUTHORIZATION header, if not present just * return the user. */ $headerkey = apply_filters( 'jwt_auth_authorization_header', 'HTTP_AUTHORIZATION' ); - $auth = isset( $_SERVER[ $headerkey ] ) ? $_SERVER[ $headerkey ] : false; + $auth = empty($_SERVER[ $headerkey ]) ? false : $_SERVER[ $headerkey ]; // Double check for different auth header string (server dependent). if ( ! $auth ) { - $auth = isset( $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ) ? $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] : false; + $auth = empty($_SERVER['REDIRECT_HTTP_AUTHORIZATION']) ? false : $_SERVER['REDIRECT_HTTP_AUTHORIZATION']; } if ( ! $auth ) { @@ -374,7 +434,7 @@ public function validate_token( $return_response = true ) { * The HTTP_AUTHORIZATION is present, verify the format. * If the format is wrong return the user. */ - list($token) = sscanf( $auth, 'Bearer %s' ); + list( $token ) = sscanf( $auth, 'Bearer %s' ); if ( ! $token ) { return new WP_REST_Response( @@ -408,7 +468,7 @@ public function validate_token( $return_response = true ) { // Try to decode the token. try { $alg = $this->get_alg(); - $payload = JWT::decode( $token, new Key( $secret_key , $alg )); + $payload = JWT::decode( $token, new Key( $secret_key, $alg ) ); // The Token is decoded now validate the iss. if ( $payload->iss !== $this->get_iss() ) { @@ -510,89 +570,202 @@ public function validate_token( $return_response = true ) { * Validates refresh token and generates a new refresh token. * * @param WP_REST_Request $request The request. + * * @return WP_REST_Response Returns WP_REST_Response. */ - public function refresh_token( WP_REST_Request $request ) { - if ( ! isset( $_COOKIE['refresh_token'] ) ) { + public function refresh_token( \WP_REST_Request $request ) { + + $refresh_token = $this->retrieve_refresh_token( $request ); + + if ( empty( $refresh_token ) ) { return new WP_REST_Response( array( 'success' => false, 'statusCode' => 401, - 'code' => 'jwt_auth_no_auth_cookie', - 'message' => __( 'Refresh token cookie not found.', 'jwt-auth' ), + 'code' => 'jwt_auth_no_refresh_token', + 'message' => __( 'Refresh token not found.', 'jwt-auth' ), ), 401 ); } - $device = $request->get_param( 'device' ) ?: ''; - $user_id = $this->validate_refresh_token( $_COOKIE['refresh_token'], $device ); - if ( $user_id instanceof WP_REST_Response ) { - return $user_id; + $payload = $this->validate_refresh_token( $request, false ); + if ( $payload instanceof WP_REST_Response ) { + return $payload; } // Generate a new access token. - $user = get_user_by( 'id', $user_id ); - $this->send_refresh_token( $user, $request ); + $user = get_user_by( 'id', $payload->data->user->id ); + $request->set_param( 'device', $payload->data->device ); + $refresh_token = $this->send_refresh_token( $user, $request ); $response = array( 'success' => true, 'statusCode' => 200, 'code' => 'jwt_auth_valid_token', 'message' => __( 'Token is valid', 'jwt-auth' ), + 'data' => array( + 'refresh_token' => $refresh_token, + ), ); + return new WP_REST_Response( $response ); } /** * Validates refresh token. * - * @param string $refresh_token_cookie The refresh token to validate. - * @param string $device The device of the refresh token. - * @return int|WP_REST_Response Returns user ID if valid or WP_REST_Response on error. + * @param WP_REST_Request $request The request. + * @param bool $return_response Either to return full WP_REST_Response or to return the payload only. + * + * @return \stdClass|WP_REST_Response Returns user ID if valid or WP_REST_Response on error. */ - public function validate_refresh_token( $refresh_token_cookie, $device ) { - $parts = explode( '.', $refresh_token_cookie ); - if ( count( $parts ) !== 2 || empty( intval( $parts[0] ) ) || empty( $parts[1] ) ) { + public function validate_refresh_token( \WP_REST_Request $request, $return_response = true ) { + + $refresh_token = $this->retrieve_refresh_token( $request ); + + // Get the Secret Key. + $secret_key = defined( 'JWT_AUTH_SECRET_KEY' ) ? JWT_AUTH_SECRET_KEY : false; + + if ( ! $secret_key ) { return new WP_REST_Response( array( 'success' => false, 'statusCode' => 401, - 'code' => 'jwt_auth_invalid_refresh_token', - 'message' => __( 'Invalid refresh token', 'jwt-auth' ), + 'code' => 'jwt_auth_bad_config', + 'message' => __( 'JWT is not configured properly.', 'jwt-auth' ), + 'data' => array(), ), 401 ); } - // The refresh token must match the last issued refresh token for the passed - // device. - $user_id = intval( $parts[0] ); - $user_refresh_tokens = get_user_meta( $user_id, 'jwt_auth_refresh_tokens', true ); - $refresh_token = $parts[1]; + // Try to decode the token. + try { + $alg = $this->get_alg(); + $payload = JWT::decode( $refresh_token, new Key( $secret_key, $alg ) ); + + // The Token is decoded now validate the iss. + if ( $payload->iss !== $this->get_iss() ) { + // The iss do not match, return error. + return new WP_REST_Response( + array( + 'success' => false, + 'statusCode' => 401, + 'code' => 'jwt_auth_bad_iss', + 'message' => __( 'The iss do not match with this server.', 'jwt-auth' ), + 'data' => array(), + ), + 401 + ); + } + + // Check the user id existence in the token. + if ( ! isset( $payload->data->user->id ) ) { + // No user id in the token, abort!! + return new WP_REST_Response( + array( + 'success' => false, + 'statusCode' => 401, + 'code' => 'jwt_auth_bad_request', + 'message' => __( 'User ID not found in the refresh token.', 'jwt-auth' ), + 'data' => array(), + ), + 401 + ); + } + + // So far so good, check if the given user id exists in db. + $user = get_user_by( 'id', $payload->data->user->id ); - if ( empty( $user_refresh_tokens[ $device ] ) || - $user_refresh_tokens[ $device ]['token'] !== $refresh_token || - $user_refresh_tokens[ $device ]['expires'] < time() + if ( ! $user ) { + // No user id in the token, abort!! + return new WP_REST_Response( + array( + 'success' => false, + 'statusCode' => 401, + 'code' => 'jwt_auth_user_not_found', + 'message' => __( "User doesn't exist", 'jwt-auth' ), + 'data' => array(), + ), + 401 + ); + } + + // The refresh token must match the last issued refresh token for the passed + // device. + $user_id = $payload->data->user->id; + $user_refresh_tokens = get_user_meta( $user_id, 'jwt_auth_refresh_tokens', true ); + + if ( empty( $user_refresh_tokens[ $payload->data->device ] ) || + $user_refresh_tokens[ $payload->data->device ]['token'] !== $refresh_token || + $user_refresh_tokens[ $payload->data->device ]['expires'] < time() ) { + return new WP_REST_Response( + array( + 'success' => false, + 'statusCode' => 401, + 'code' => 'jwt_auth_obsolete_token', + 'message' => __( 'Token is obsolete', 'jwt-auth' ), + ), + 401 + ); + } + + // Check extra condition if exists. + $failed_msg = apply_filters( 'jwt_auth_extra_refresh_token_check', '', $user, $refresh_token, $payload ); + + if ( ! empty( $failed_msg ) ) { + // No user id in the token, abort!! + return new WP_REST_Response( + array( + 'success' => false, + 'statusCode' => 401, + 'code' => 'jwt_auth_obsolete_token', + 'message' => __( 'Token is obsolete', 'jwt-auth' ), + 'data' => array(), + ), + 401 + ); + } + + // Everything looks good, return the payload if $return_response is set to false. + if ( ! $return_response ) { + return $payload; + } + + $response = array( + 'success' => true, + 'statusCode' => 200, + 'code' => 'jwt_auth_valid_token', + 'message' => __( 'Refresh token is valid', 'jwt-auth' ), + 'data' => array(), + ); + + $response = apply_filters( 'jwt_auth_valid_refresh_token_response', $response, $user, $refresh_token, $payload ); + + // Otherwise, return success response. + return new WP_REST_Response( $response ); + } catch ( Exception $e ) { + // Something is wrong when trying to decode the token, return error response. return new WP_REST_Response( array( 'success' => false, 'statusCode' => 401, - 'code' => 'jwt_auth_obsolete_token', - 'message' => __( 'Token is obsolete', 'jwt-auth' ), + 'code' => 'jwt_auth_invalid_refresh_token', + 'message' => $e->getMessage(), + 'data' => array(), ), 401 ); } - - return $user_id; } /** * This is our Middleware to try to authenticate the user according to the token sent. * * @param int|bool $user_id User ID if one has been determined, false otherwise. + * * @return int|bool User ID if one has been determined, false otherwise. */ public function determine_current_user( $user_id ) { @@ -640,8 +813,8 @@ public function determine_current_user( $user_id ) { * Filter to hook the rest_pre_dispatch, if there is an error in the request * send it, if there is no error just continue with the current request. * - * @param mixed $result Can be anything a normal endpoint can return, or null to not hijack the request. - * @param WP_REST_Server $server Server instance. + * @param mixed $result Can be anything a normal endpoint can return, or null to not hijack the request. + * @param WP_REST_Server $server Server instance. * @param WP_REST_Request $request The request. * * @return mixed $result @@ -657,4 +830,21 @@ public function rest_pre_dispatch( $result, WP_REST_Server $server, WP_REST_Requ return $result; } + + /** + * Retrieves the refresh token based on a flow + * + * @param WP_REST_Request $request + * + * @return string|null + */ + private function retrieve_refresh_token( WP_REST_Request $request ): ?string { + $flow = $this->get_flow(); + + if ( 'parameter' === $flow ) { + return $request->get_param( 'refresh_token' ); + } + + return $_COOKIE['refresh_token'] ?? null; + } } From 636f634de5b1084898df5020762e886856d7b96d Mon Sep 17 00:00:00 2001 From: Matteo Gaggiano Date: Mon, 18 Mar 2024 10:07:59 +0100 Subject: [PATCH 03/29] feat(refresh token): add support for issued at and add different hook for different token type --- class-auth.php | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/class-auth.php b/class-auth.php index 8b45b2b..a0cf52c 100644 --- a/class-auth.php +++ b/class-auth.php @@ -217,8 +217,10 @@ public function generate_token( $user, $return_raw = true ) { $issued_at = time(); $not_before = $issued_at; $not_before = apply_filters( 'jwt_auth_not_before', $not_before, $issued_at ); + $not_before = apply_filters( 'jwt_auth_token_not_before', $not_before, $issued_at ); $expire = $issued_at + ( MINUTE_IN_SECONDS * 10 ); $expire = apply_filters( 'jwt_auth_expire', $expire, $issued_at ); + $expire = apply_filters( 'jwt_auth_token_expire', $expire, $issued_at ); $payload = array( 'iss' => $this->get_iss(), @@ -324,16 +326,18 @@ public function send_refresh_token( \WP_User $user, \WP_REST_Request $request ) */ public function generate_refresh_token( \WP_User $user, \WP_REST_Request $request ) { $secret_key = defined( 'JWT_AUTH_SECRET_KEY' ) ? JWT_AUTH_SECRET_KEY : false; - $created = time(); - $expires = $created + DAY_IN_SECONDS * 30; - $expires = apply_filters( 'jwt_auth_refresh_expire', $expires, $created ); + $issued_at = time(); + $not_before = $issued_at; + $not_before = apply_filters( 'jwt_auth_refresh_not_before', $not_before, $issued_at ); + $expires = $issued_at + DAY_IN_SECONDS * 30; + $expires = apply_filters( 'jwt_auth_refresh_expire', $expires, $issued_at ); $device = $request->get_param( 'device' ) ?: ''; $payload = array( 'iss' => $this->get_iss(), - 'iat' => $created, - 'nbf' => $created, + 'iat' => $issued_at, + 'nbf' => $not_before, 'exp' => $expires, 'data' => array( 'device' => $device, From 7ce659ca069db872a159f636bd73f55f88d13dd8 Mon Sep 17 00:00:00 2001 From: Matteo Gaggiano Date: Mon, 18 Mar 2024 10:09:05 +0100 Subject: [PATCH 04/29] feature(refresh token): add different flow types as: body, parameter, query --- class-auth.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/class-auth.php b/class-auth.php index a0cf52c..eef759d 100644 --- a/class-auth.php +++ b/class-auth.php @@ -283,7 +283,7 @@ public function send_refresh_token( \WP_User $user, \WP_REST_Request $request ) $flow = $this->get_flow(); - if ( 'parameter' === $flow ) { + if ( 'body' === $flow ) { } else { // Send the refresh token as a HttpOnly cookie in the response. @@ -297,7 +297,7 @@ public function send_refresh_token( \WP_User $user, \WP_REST_Request $request ) if ( ! is_array( $user_refresh_tokens ) ) { $user_refresh_tokens = array(); } - $device = empty($payload->data->device) ? '' : $payload->data->device; + $device = empty( $payload->data->device ) ? '' : $payload->data->device; $user_refresh_tokens[ $device ] = array( 'token' => $refresh_token, 'expires' => $payload->exp, @@ -414,11 +414,11 @@ public function validate_token( $return_response_or_request = true ) { * return the user. */ $headerkey = apply_filters( 'jwt_auth_authorization_header', 'HTTP_AUTHORIZATION' ); - $auth = empty($_SERVER[ $headerkey ]) ? false : $_SERVER[ $headerkey ]; + $auth = empty( $_SERVER[ $headerkey ] ) ? false : $_SERVER[ $headerkey ]; // Double check for different auth header string (server dependent). if ( ! $auth ) { - $auth = empty($_SERVER['REDIRECT_HTTP_AUTHORIZATION']) ? false : $_SERVER['REDIRECT_HTTP_AUTHORIZATION']; + $auth = empty( $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ) ? false : $_SERVER['REDIRECT_HTTP_AUTHORIZATION']; } if ( ! $auth ) { @@ -845,7 +845,7 @@ public function rest_pre_dispatch( $result, WP_REST_Server $server, WP_REST_Requ private function retrieve_refresh_token( WP_REST_Request $request ): ?string { $flow = $this->get_flow(); - if ( 'parameter' === $flow ) { + if ( 'body' === $flow || 'query' === $flow || 'parameter' === $flow ) { return $request->get_param( 'refresh_token' ); } From 77bfe666eaf70db2c7f94ba8354dda1c00b9bff5 Mon Sep 17 00:00:00 2001 From: Matteo Gaggiano Date: Mon, 18 Mar 2024 11:30:57 +0100 Subject: [PATCH 05/29] fix(refresh token): wrong flow check for cookie flow --- class-auth.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/class-auth.php b/class-auth.php index eef759d..b8e8dbd 100644 --- a/class-auth.php +++ b/class-auth.php @@ -283,9 +283,7 @@ public function send_refresh_token( \WP_User $user, \WP_REST_Request $request ) $flow = $this->get_flow(); - if ( 'body' === $flow ) { - - } else { + if ( 'cookie' === $flow ) { // Send the refresh token as a HttpOnly cookie in the response. setcookie( 'refresh_token', $refresh_token, $payload->exp, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true ); } From a33f3f1cd2e6d4e4f263bb4c1774d39333723d4e Mon Sep 17 00:00:00 2001 From: Matteo Gaggiano Date: Mon, 18 Mar 2024 11:48:32 +0100 Subject: [PATCH 06/29] fix(refresh token): manage cookie as supported flow feat(refresh token): add hook to retrieve the refresh token from a custom filter --- class-auth.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/class-auth.php b/class-auth.php index b8e8dbd..1e0c8a9 100644 --- a/class-auth.php +++ b/class-auth.php @@ -843,10 +843,12 @@ public function rest_pre_dispatch( $result, WP_REST_Server $server, WP_REST_Requ private function retrieve_refresh_token( WP_REST_Request $request ): ?string { $flow = $this->get_flow(); - if ( 'body' === $flow || 'query' === $flow || 'parameter' === $flow ) { - return $request->get_param( 'refresh_token' ); + if ( 'cookie' === $flow ) { + $refresh_token = isset($_COOKIE['refresh_token']) ? $_COOKIE['refresh_token'] : null; + } else { + $refresh_token = $request->get_param( 'refresh_token' ); } - return $_COOKIE['refresh_token'] ?? null; + return apply_filters( 'jwt_auth_retrieve_refresh_token', $refresh_token, $request, $flow ); } } From 3fd08c5d171a824aadc74ee8af51251b5df3c0f2 Mon Sep 17 00:00:00 2001 From: Matteo Gaggiano Date: Tue, 19 Mar 2024 11:55:41 +0100 Subject: [PATCH 07/29] fix(refresh token): manage device as mandatory field. Can be empty --- class-auth.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/class-auth.php b/class-auth.php index 1e0c8a9..3613e19 100644 --- a/class-auth.php +++ b/class-auth.php @@ -699,6 +699,11 @@ public function validate_refresh_token( \WP_REST_Request $request, $return_respo $user_id = $payload->data->user->id; $user_refresh_tokens = get_user_meta( $user_id, 'jwt_auth_refresh_tokens', true ); + if ( ! isset( $payload->data->device ) ) { + // Throw invalid token response + throw new Exception( __( 'Device not found in the refresh token.', 'jwt-auth' ) ); + } + if ( empty( $user_refresh_tokens[ $payload->data->device ] ) || $user_refresh_tokens[ $payload->data->device ]['token'] !== $refresh_token || $user_refresh_tokens[ $payload->data->device ]['expires'] < time() From 322fbe8504f043f4a0a016871c463802ddbdc9ee Mon Sep 17 00:00:00 2001 From: Matteo Gaggiano Date: Tue, 19 Mar 2024 11:56:08 +0100 Subject: [PATCH 08/29] fix(refresh token): better retrieve data from request based on flow definition removing dependency on request variable --- class-auth.php | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/class-auth.php b/class-auth.php index 3613e19..8e81383 100644 --- a/class-auth.php +++ b/class-auth.php @@ -161,10 +161,10 @@ public function get_token( WP_REST_Request $request ) { ); } - $refresh_token = $this->retrieve_refresh_token( $request ); + $refresh_token = $this->retrieve_refresh_token(); if ( ! empty( $refresh_token ) ) { - $payload = $this->validate_refresh_token( $request, false ); + $payload = $this->validate_refresh_token( false ); // If we receive a REST response, then validation failed. if ( $payload instanceof WP_REST_Response ) { @@ -577,7 +577,7 @@ public function validate_token( $return_response_or_request = true ) { */ public function refresh_token( \WP_REST_Request $request ) { - $refresh_token = $this->retrieve_refresh_token( $request ); + $refresh_token = $this->retrieve_refresh_token(); if ( empty( $refresh_token ) ) { return new WP_REST_Response( @@ -591,7 +591,7 @@ public function refresh_token( \WP_REST_Request $request ) { ); } - $payload = $this->validate_refresh_token( $request, false ); + $payload = $this->validate_refresh_token( false ); if ( $payload instanceof WP_REST_Response ) { return $payload; } @@ -617,14 +617,13 @@ public function refresh_token( \WP_REST_Request $request ) { /** * Validates refresh token. * - * @param WP_REST_Request $request The request. * @param bool $return_response Either to return full WP_REST_Response or to return the payload only. * * @return \stdClass|WP_REST_Response Returns user ID if valid or WP_REST_Response on error. */ - public function validate_refresh_token( \WP_REST_Request $request, $return_response = true ) { + public function validate_refresh_token( $return_response = true ) { - $refresh_token = $this->retrieve_refresh_token( $request ); + $refresh_token = $this->retrieve_refresh_token(); // Get the Secret Key. $secret_key = defined( 'JWT_AUTH_SECRET_KEY' ) ? JWT_AUTH_SECRET_KEY : false; @@ -841,19 +840,23 @@ public function rest_pre_dispatch( $result, WP_REST_Server $server, WP_REST_Requ /** * Retrieves the refresh token based on a flow * - * @param WP_REST_Request $request - * * @return string|null */ - private function retrieve_refresh_token( WP_REST_Request $request ): ?string { + private function retrieve_refresh_token(): ?string { $flow = $this->get_flow(); - if ( 'cookie' === $flow ) { - $refresh_token = isset($_COOKIE['refresh_token']) ? $_COOKIE['refresh_token'] : null; - } else { - $refresh_token = $request->get_param( 'refresh_token' ); + if ( 'body' === $flow ) { + $_array = $_POST; + } else if ( 'query' === $flow ) { + $_array = $_REQUEST; + } else if ( 'header' === $flow ) { + $_array = getallheaders() ? : array(); + } else { // default cookie + $_array = $_COOKIE; } - return apply_filters( 'jwt_auth_retrieve_refresh_token', $refresh_token, $request, $flow ); + $refresh_token = $_array['refresh_token'] ?? null; + + return apply_filters( 'jwt_auth_retrieve_refresh_token', $refresh_token, $flow ); } } From f9b3454e73fb6b1a54e5e8067f46660a6d0d751b Mon Sep 17 00:00:00 2001 From: Matteo Gaggiano Date: Tue, 19 Mar 2024 12:07:55 +0100 Subject: [PATCH 09/29] fix(refresh token): restore response content based on flow when refreshing refresh token --- class-auth.php | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/class-auth.php b/class-auth.php index 8e81383..6b4a72b 100644 --- a/class-auth.php +++ b/class-auth.php @@ -601,17 +601,26 @@ public function refresh_token( \WP_REST_Request $request ) { $request->set_param( 'device', $payload->data->device ); $refresh_token = $this->send_refresh_token( $user, $request ); + $flow = $this->get_flow(); + + $additional_fields = array(); + + if ( $flow !== 'cookie' ) { + $additional_fields = array( + 'data' => array( + 'refresh_token' => $refresh_token, + ), + ); + } + $response = array( 'success' => true, 'statusCode' => 200, 'code' => 'jwt_auth_valid_token', 'message' => __( 'Token is valid', 'jwt-auth' ), - 'data' => array( - 'refresh_token' => $refresh_token, - ), ); - return new WP_REST_Response( $response ); + return new WP_REST_Response( array_merge( $response, $additional_fields ) ); } /** @@ -850,7 +859,7 @@ private function retrieve_refresh_token(): ?string { } else if ( 'query' === $flow ) { $_array = $_REQUEST; } else if ( 'header' === $flow ) { - $_array = getallheaders() ? : array(); + $_array = getallheaders() ?: array(); } else { // default cookie $_array = $_COOKIE; } From 7f00143cae58da74f5d4b0b88c376411d5b613f9 Mon Sep 17 00:00:00 2001 From: Matteo Gaggiano Date: Tue, 19 Mar 2024 12:58:22 +0100 Subject: [PATCH 10/29] feat(refresh token): add token type to differentiate token in validation process --- class-auth.php | 38 ++++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/class-auth.php b/class-auth.php index 6b4a72b..fd5f466 100644 --- a/class-auth.php +++ b/class-auth.php @@ -223,6 +223,7 @@ public function generate_token( $user, $return_raw = true ) { $expire = apply_filters( 'jwt_auth_token_expire', $expire, $issued_at ); $payload = array( + 'type' => 'access_token', 'iss' => $this->get_iss(), 'iat' => $issued_at, 'nbf' => $not_before, @@ -333,6 +334,7 @@ public function generate_refresh_token( \WP_User $user, \WP_REST_Request $reques $device = $request->get_param( 'device' ) ?: ''; $payload = array( + 'type' => 'refresh', 'iss' => $this->get_iss(), 'iat' => $issued_at, 'nbf' => $not_before, @@ -405,7 +407,6 @@ public function is_error_response( $response ) { */ public function validate_token( $return_response_or_request = true ) { $return_response = $return_response_or_request instanceof WP_REST_Request ? true : $return_response_or_request; - $request = $return_response_or_request instanceof WP_REST_Request ? $return_response_or_request : null; /** * Looking for the HTTP_AUTHORIZATION header, if not present just @@ -487,6 +488,10 @@ public function validate_token( $return_response_or_request = true ) { ); } + if ( $payload->type !== 'access_token' ) { + throw new Exception( __( 'Invalid token type', 'jwt-auth' ) ); + } + // Check the user id existence in the token. if ( ! isset( $payload->data->user->id ) ) { // No user id in the token, abort!! @@ -577,9 +582,9 @@ public function validate_token( $return_response_or_request = true ) { */ public function refresh_token( \WP_REST_Request $request ) { - $refresh_token = $this->retrieve_refresh_token(); + $input_refresh_token = $this->retrieve_refresh_token(); - if ( empty( $refresh_token ) ) { + if ( empty( $input_refresh_token ) ) { return new WP_REST_Response( array( 'success' => false, @@ -670,6 +675,10 @@ public function validate_refresh_token( $return_response = true ) { ); } + if ( $payload->type !== 'refresh_token' ) { + throw new Exception( __( 'Invalid token type', 'jwt-auth' ) ); + } + // Check the user id existence in the token. if ( ! isset( $payload->data->user->id ) ) { // No user id in the token, abort!! @@ -702,20 +711,29 @@ public function validate_refresh_token( $return_response = true ) { ); } + if ( ! isset( $payload->data->device ) ) { + // Throw invalid token response + throw new Exception( __( 'Device not found in the refresh token.', 'jwt-auth' ) ); + } + // The refresh token must match the last issued refresh token for the passed // device. $user_id = $payload->data->user->id; $user_refresh_tokens = get_user_meta( $user_id, 'jwt_auth_refresh_tokens', true ); - if ( ! isset( $payload->data->device ) ) { - // Throw invalid token response - throw new Exception( __( 'Device not found in the refresh token.', 'jwt-auth' ) ); + if ( ! is_array( $user_refresh_tokens ) ) { + $user_refresh_tokens = array(); + } + + $device = empty( $payload->data->device ) ? '' : $payload->data->device; + $last_refresh_token_issued = $user_refresh_tokens[ $device ] ?? null; + + if ( empty( $last_refresh_token_issued ) || $last_refresh_token_issued['token'] !== $refresh_token ) { + // The refresh token do not match, return error. + throw new Exception( __( 'Refresh token not found for the device.', 'jwt-auth' ) ); } - if ( empty( $user_refresh_tokens[ $payload->data->device ] ) || - $user_refresh_tokens[ $payload->data->device ]['token'] !== $refresh_token || - $user_refresh_tokens[ $payload->data->device ]['expires'] < time() - ) { + if ( $last_refresh_token_issued['expires'] < time() ) { return new WP_REST_Response( array( 'success' => false, From c440257d7bedd80ae2ad749f594a68f631cd8c73 Mon Sep 17 00:00:00 2001 From: Matteo Gaggiano Date: Tue, 19 Mar 2024 13:20:35 +0100 Subject: [PATCH 11/29] feat(logger): add psr/log as base dependency to let users define a custom logger logic to be used --- class-auth.php | 14 ++- class-devices.php | 257 ++++++++++++++++++++++++++-------------------- class-setup.php | 37 +++++-- composer.json | 3 +- composer.lock | 54 +++++++++- 5 files changed, 241 insertions(+), 124 deletions(-) diff --git a/class-auth.php b/class-auth.php index fd5f466..85badce 100644 --- a/class-auth.php +++ b/class-auth.php @@ -10,6 +10,7 @@ use Exception; use Firebase\JWT\JWT; use Firebase\JWT\Key; +use Psr\Log\LoggerInterface; use WP_Error; use WP_REST_Request; use WP_REST_Response; @@ -47,16 +48,27 @@ class Auth { */ private $rest_api_slug = 'wp-json'; + /** + * The logger interface to use + * + * @var LoggerInterface + */ + private $logger; + /** * Setup action & filter hooks. + * + * @param LoggerInterface $logger The logger interface to use. */ - public function __construct() { + public function __construct( LoggerInterface $logger ) { $this->namespace = 'jwt-auth/v1'; $this->messages = array( 'jwt_auth_no_auth_header' => __( 'Authorization header not found.', 'jwt-auth' ), 'jwt_auth_bad_auth_header' => __( 'Authorization header malformed.', 'jwt-auth' ), ); + + $this->logger = $logger; } /** diff --git a/class-devices.php b/class-devices.php index 53bdee8..9b4baa8 100644 --- a/class-devices.php +++ b/class-devices.php @@ -7,16 +7,27 @@ namespace JWTAuth; +use Psr\Log\LoggerInterface; + /** * Display the devices connected with token and let remove them in user profile page. * Developed by Rodrigo M. Souza https://github.com/pesseba */ class Devices { + /** + * The logger object. + * + * @var LoggerInterface + */ + private $logger; + /** * Setup action & filter hooks. + * + * @param LoggerInterface $logger The logger object. */ - public function __construct() { + public function __construct( LoggerInterface $logger ) { add_action( 'show_user_profile', array( $this, 'custom_user_profile_fields' ), 10, 1 ); add_action( 'edit_user_profile', array( $this, 'custom_user_profile_fields' ), 10, 1 ); @@ -27,15 +38,17 @@ public function __construct() { add_action( 'profile_update', array( $this, 'profile_update' ), 10, 2 ); add_action( 'after_password_reset', array( $this, 'after_password_reset' ), 10, 2 ); add_action( 'user_register', array( $this, 'after_user_creation' ), 10, 1 ); - + add_filter( 'jwt_auth_payload', array( $this, 'jwt_auth_payload' ), 10, 2 ); add_filter( 'jwt_auth_extra_token_check', array( $this, 'check_device_and_pass' ), 10, 4 ); + + $this->logger = $logger; } /** * Filter payload to add device and pass. * - * @param array $payload The token's payload. + * @param array $payload The token's payload. * @param WP_User $user The user who owns the token. * * @return array $payload The modified token's payload. @@ -75,10 +88,10 @@ public function jwt_auth_payload( $payload, $user ) { /** * Filter token validation to check device and pass. * - * @param string $error_msg The failed message. + * @param string $error_msg The failed message. * @param WP_User $user The user who owns the token. - * @param string $token The token. - * @param array $payload The token's payload. + * @param string $token The token. + * @param array $payload The token's payload. * * @return string The error message if failed, empty string if it passes. */ @@ -198,10 +211,11 @@ private function sanitize_device_key( $key ) { /** * Fires immediately after an existing user is updated. * + * @param int $user_id User ID. + * @param WP_User $old_user_data Object containing user's data prior to update. + * * @since 2.0.0 * - * @param int $user_id User ID. - * @param WP_User $old_user_data Object containing user's data prior to update. */ public function profile_update( $user_id, $old_user_data ) { @@ -216,25 +230,27 @@ public function profile_update( $user_id, $old_user_data ) { /** * Fires after the user's password is reset. * + * @param WP_User $user The user. + * @param string $new_pass New user password. + * * @since 4.4.0 * - * @param WP_User $user The user. - * @param string $new_pass New user password. */ public function after_password_reset( $user, $new_pass ) { $this->block_all_tokens( $user->ID ); } - + /** * Fires after the user' is created * + * @param int $user_id The user ID. + * * @since 2.0.0 * - * @param int $user_id The user ID. */ public function after_user_creation( $user_id ) { - + $this->refresh_pass( $user_id ); } @@ -270,10 +286,11 @@ private function block_all_tokens( $user_id ) { * @param int $user_id The user id. */ private function refresh_pass( $user_id ) { - $pass = (string) md5( uniqid( wp_rand(), true )); - if(!empty(update_user_meta( $user_id, 'jwt_auth_pass', $pass ) )){ + $pass = (string) md5( uniqid( wp_rand(), true ) ); + if ( ! empty( update_user_meta( $user_id, 'jwt_auth_pass', $pass ) ) ) { return $pass; } + return ''; } @@ -284,13 +301,14 @@ private function refresh_pass( $user_id ) { */ public function remove_device() { - $nonce = isset( $_POST['nonce'] ) ? $_POST['nonce'] : ''; + $nonce = isset( $_POST['nonce'] ) ? $_POST['nonce'] : ''; $device = isset( $_POST['device'] ) ? sanitize_text_field( $_POST['device'] ) : ''; // phpcs:ignore $user_id = isset( $_POST['user_id'] ) && is_numeric( $_POST['user_id'] ) ? absint( $_POST['user_id'] ) : 0; // phpcs:ignore - - if(!wp_verify_nonce( $nonce, 'jwt_auth_remove_device_'.$user_id)){ + + if ( ! wp_verify_nonce( $nonce, 'jwt_auth_remove_device_' . $user_id ) ) { wp_send_json_error(); wp_die(); + return; } @@ -323,10 +341,10 @@ public function custom_user_profile_fields( $profileuser ) { } ?> -

-
- -
+

+
+ +
- + - if (response['success'] == true) { + + jQuery.post(ajaxurl, data, function (response) { -
+ if (response['success'] == true) { - 30 ) ? substr( $title, 0, 27 ) . '...' : $title; - $device_data = (array) get_user_meta( $user_id, $this->sanitize_device_key( $device ), true ); - $icon = ( $device_data['is_mobile'] ) ? 'dashicons-smartphone' : 'dashicons-laptop'; - $date = $device_data['date']; - $agent = $device_data['agent']; - $agent = preg_replace( '/(\S{15})(?=\S)/', '$1 ', $agent ); + alert(""); + } - ?> + }); + } + } + -
-
- -

-

-

+
+ + 30 ) ? substr( $title, 0, 27 ) . '...' : $title; + $device_data = (array) get_user_meta( $user_id, $this->sanitize_device_key( $device ), true ); + $icon = ( $device_data['is_mobile'] ) ? 'dashicons-smartphone' : 'dashicons-laptop'; + $date = $device_data['date']; + $agent = $device_data['agent']; + $agent = preg_replace( '/(\S{15})(?=\S)/', '$1 ', $agent ); + + ?> + +
+
+ +

+

+

+

+ + + '" class="button wp-generate-pw' . + '" type="button" value="' . __( 'Remove', 'jwt-auth' ) . + '" onclick="jwt_auth_remove_device(\'' . esc_attr( $user_id ) . '\',\'' . esc_attr( $device ) . '\',\'' . esc_attr( $i ) . '\' )" /> '; - ?> -
-
- + ?> +
+
+ -
-
+
+
auth = new Auth(); - $this->devices = new Devices(); + /** + * Allow to define a custom LoggerInterface for the plugin. + */ + $logger = apply_filters( 'jwt_auth_logger', new NullLogger() ); + + if ( ! $logger instanceof LoggerInterface ) { + $logger = new NullLogger(); + } + $this->logger = $logger; + + $this->auth = new Auth( $this->logger ); + $this->devices = new Devices( $this->logger ); add_action( 'rest_api_init', array( $this->auth, 'register_rest_routes' ) ); add_filter( 'rest_api_init', array( $this->auth, 'add_cors_support' ) ); add_filter( 'rest_pre_dispatch', array( $this->auth, 'rest_pre_dispatch' ), 10, 3 ); add_filter( 'determine_current_user', array( $this->auth, 'determine_current_user' ) ); - if ( ! wp_next_scheduled( 'jwt_auth_purge_expired_refresh_tokens' )) { + if ( ! wp_next_scheduled( 'jwt_auth_purge_expired_refresh_tokens' ) ) { wp_schedule_event( time(), 'weekly', 'jwt_auth_purge_expired_refresh_tokens' ); } add_action( 'jwt_auth_purge_expired_refresh_tokens', array( $this, 'cron_purge_expired_refresh_tokens' ) ); @@ -59,6 +78,8 @@ public function setup_text_domain() { public function cron_purge_expired_refresh_tokens() { global $wpdb; + $this->logger->notice("Cron purge expired refresh tokens."); + // Retain expired refresh tokens for one month for potential debugging. $purge_timestamp = time() - 30 * DAY_IN_SECONDS; @@ -67,7 +88,7 @@ public function cron_purge_expired_refresh_tokens() { AND meta_value <= %d ", $purge_timestamp ) ); - foreach ($user_ids as $user_id) { + foreach ( $user_ids as $user_id ) { $user_refresh_tokens = get_user_meta( $user_id, 'jwt_auth_refresh_tokens', true ); if ( is_array( $user_refresh_tokens ) ) { $expires_next = 0; @@ -83,11 +104,13 @@ public function cron_purge_expired_refresh_tokens() { update_user_meta( $user_id, 'jwt_auth_refresh_tokens', $user_refresh_tokens ); update_user_meta( $user_id, 'jwt_auth_refresh_tokens_expires_next', $expires_next ); } else { - delete_user_meta( $user_id, 'jwt_auth_refresh_tokens' ); - delete_user_meta( $user_id, 'jwt_auth_refresh_tokens_expires_next' ); + delete_user_meta( $user_id, 'jwt_auth_refresh_tokens' ); + delete_user_meta( $user_id, 'jwt_auth_refresh_tokens_expires_next' ); } } } + + $this->logger->notice("Cron purge expired refresh tokens completed."); } } diff --git a/composer.json b/composer.json index 17b6528..79442e6 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,8 @@ ], "minimum-stability": "stable", "require": { - "firebase/php-jwt": "^6.3" + "firebase/php-jwt": "^6.3", + "psr/log": "^3.0" }, "require-dev": { "php": ">=7.4", diff --git a/composer.lock b/composer.lock index 35368b9..fece5d0 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "02bd4aac8b0bcb72865747d72072927c", + "content-hash": "9ae4b449bdec71506599958bfcb3a62d", "packages": [ { "name": "firebase/php-jwt", @@ -63,6 +63,56 @@ "php" ], "time": "2022-07-15T16:48:45+00:00" + }, + { + "name": "psr/log", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/fe5ea303b0887d5caefd3d431c3e61ad47037001", + "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.0" + }, + "time": "2021-07-14T16:46:02+00:00" } ], "packages-dev": [ @@ -2268,5 +2318,5 @@ "platform-dev": { "php": ">=7.4" }, - "plugin-api-version": "1.1.0" + "plugin-api-version": "2.6.0" } From d27630a28797bd727b4e8c3772edb42d073464ff Mon Sep 17 00:00:00 2001 From: Matteo Gaggiano Date: Tue, 19 Mar 2024 14:43:01 +0100 Subject: [PATCH 12/29] feature(refresh token): add type (typ) property for token to check correct token type --- class-auth.php | 8 ++++---- class-devices.php | 2 ++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/class-auth.php b/class-auth.php index 85badce..e02be44 100644 --- a/class-auth.php +++ b/class-auth.php @@ -235,7 +235,7 @@ public function generate_token( $user, $return_raw = true ) { $expire = apply_filters( 'jwt_auth_token_expire', $expire, $issued_at ); $payload = array( - 'type' => 'access_token', + 'typ' => 'access', 'iss' => $this->get_iss(), 'iat' => $issued_at, 'nbf' => $not_before, @@ -346,7 +346,7 @@ public function generate_refresh_token( \WP_User $user, \WP_REST_Request $reques $device = $request->get_param( 'device' ) ?: ''; $payload = array( - 'type' => 'refresh', + 'typ' => 'refresh', 'iss' => $this->get_iss(), 'iat' => $issued_at, 'nbf' => $not_before, @@ -500,7 +500,7 @@ public function validate_token( $return_response_or_request = true ) { ); } - if ( $payload->type !== 'access_token' ) { + if ( ! isset( $payload->typ ) || $payload->typ !== 'access' ) { throw new Exception( __( 'Invalid token type', 'jwt-auth' ) ); } @@ -687,7 +687,7 @@ public function validate_refresh_token( $return_response = true ) { ); } - if ( $payload->type !== 'refresh_token' ) { + if ( ! isset( $payload->typ ) || $payload->typ !== 'refresh' ) { throw new Exception( __( 'Invalid token type', 'jwt-auth' ) ); } diff --git a/class-devices.php b/class-devices.php index 9b4baa8..142cc58 100644 --- a/class-devices.php +++ b/class-devices.php @@ -121,6 +121,7 @@ public function check_device_and_pass( $error_msg, $user, $token, $payload ) { * Sanitize the device name. * * @param string $device The device name. + * * @return string The sanitized device name. */ private function sanitize_device_name( $device ) { @@ -202,6 +203,7 @@ private function sanitize_device_name( $device ) { * Sanitize the device key. * * @param string $key The device key. + * * @return string The sanitized device key. */ private function sanitize_device_key( $key ) { From 6b3395ae2a3221517900bee87aa35bb2a7df862b Mon Sep 17 00:00:00 2001 From: Matteo Gaggiano Date: Tue, 19 Mar 2024 15:19:41 +0100 Subject: [PATCH 13/29] fix(refresh token): use device from validated token --- class-auth.php | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/class-auth.php b/class-auth.php index e02be44..b48e949 100644 --- a/class-auth.php +++ b/class-auth.php @@ -183,8 +183,10 @@ public function get_token( WP_REST_Request $request ) { return $payload; } $user = get_user_by( 'id', $payload->data->user->id ); + $device = $payload->data->device; } else { $user = $this->authenticate_user( $username, $password, $custom_auth ); + $device = $request->get_param( 'device' ) ? $request->get_param( 'device' ) : ''; } // If the authentication is failed return error response. @@ -208,7 +210,7 @@ public function get_token( WP_REST_Request $request ) { // Add the refresh token as a HttpOnly cookie to the response. if ( $username && $password ) { - $refresh_token = $this->send_refresh_token( $user, $request ); + $refresh_token = $this->send_refresh_token( $user, $device ); } $response['data']['refresh_token'] = $refresh_token; @@ -282,20 +284,19 @@ public function generate_token( $user, $return_raw = true ) { * Sends a new refresh token. * * @param \WP_User $user The WP_User object. - * @param \WP_REST_Request $request The request. + * @param string $device Device name. Default empty string * * @return string */ - public function send_refresh_token( \WP_User $user, \WP_REST_Request $request ) { + public function send_refresh_token( \WP_User $user, string $device = ''): string { $secret_key = defined( 'JWT_AUTH_SECRET_KEY' ) ? JWT_AUTH_SECRET_KEY : false; - $refresh_token = $this->generate_refresh_token( $user, $request ); + $refresh_token = $this->generate_refresh_token( $user, $device ); $alg = $this->get_alg(); + $flow = $this->get_flow(); $payload = JWT::decode( $refresh_token, new Key( $secret_key, $alg ) ); - $flow = $this->get_flow(); - if ( 'cookie' === $flow ) { // Send the refresh token as a HttpOnly cookie in the response. setcookie( 'refresh_token', $refresh_token, $payload->exp, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true ); @@ -308,7 +309,7 @@ public function send_refresh_token( \WP_User $user, \WP_REST_Request $request ) if ( ! is_array( $user_refresh_tokens ) ) { $user_refresh_tokens = array(); } - $device = empty( $payload->data->device ) ? '' : $payload->data->device; + $user_refresh_tokens[ $device ] = array( 'token' => $refresh_token, 'expires' => $payload->exp, @@ -331,11 +332,11 @@ public function send_refresh_token( \WP_User $user, \WP_REST_Request $request ) * Generate a new refresh token. * * @param \WP_User $user The WP_User object. - * @param \WP_REST_Request $request The request. + * @param string $device Device name. Default empty string * * @return string */ - public function generate_refresh_token( \WP_User $user, \WP_REST_Request $request ) { + public function generate_refresh_token( \WP_User $user, string $device = '' ): string { $secret_key = defined( 'JWT_AUTH_SECRET_KEY' ) ? JWT_AUTH_SECRET_KEY : false; $issued_at = time(); $not_before = $issued_at; @@ -343,8 +344,6 @@ public function generate_refresh_token( \WP_User $user, \WP_REST_Request $reques $expires = $issued_at + DAY_IN_SECONDS * 30; $expires = apply_filters( 'jwt_auth_refresh_expire', $expires, $issued_at ); - $device = $request->get_param( 'device' ) ?: ''; - $payload = array( 'typ' => 'refresh', 'iss' => $this->get_iss(), @@ -615,8 +614,7 @@ public function refresh_token( \WP_REST_Request $request ) { // Generate a new access token. $user = get_user_by( 'id', $payload->data->user->id ); - $request->set_param( 'device', $payload->data->device ); - $refresh_token = $this->send_refresh_token( $user, $request ); + $refresh_token = $this->send_refresh_token( $user, $payload->data->device ); $flow = $this->get_flow(); @@ -898,4 +896,5 @@ private function retrieve_refresh_token(): ?string { return apply_filters( 'jwt_auth_retrieve_refresh_token', $refresh_token, $flow ); } + } From 50dcd46038b7ae3537d6b687dd8cf39c3becf745 Mon Sep 17 00:00:00 2001 From: Matteo Gaggiano Date: Tue, 19 Mar 2024 15:20:15 +0100 Subject: [PATCH 14/29] fix(refresh token): don't let hook modify the token type --- class-auth.php | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/class-auth.php b/class-auth.php index b48e949..8bba692 100644 --- a/class-auth.php +++ b/class-auth.php @@ -237,7 +237,7 @@ public function generate_token( $user, $return_raw = true ) { $expire = apply_filters( 'jwt_auth_token_expire', $expire, $issued_at ); $payload = array( - 'typ' => 'access', + 'typ' => 'access', 'iss' => $this->get_iss(), 'iat' => $issued_at, 'nbf' => $not_before, @@ -251,8 +251,13 @@ public function generate_token( $user, $return_raw = true ) { $alg = $this->get_alg(); + $payload = apply_filters( 'jwt_auth_payload', $payload, $user ); + + // Make sure to not lose type property + $payload['typ'] = 'access'; + // Let the user modify the token data before the sign. - $token = JWT::encode( apply_filters( 'jwt_auth_payload', $payload, $user ), $secret_key, $alg ); + $token = JWT::encode( $payload, $secret_key, $alg ); // If return as raw token string. if ( $return_raw ) { @@ -345,7 +350,7 @@ public function generate_refresh_token( \WP_User $user, string $device = '' ): s $expires = apply_filters( 'jwt_auth_refresh_expire', $expires, $issued_at ); $payload = array( - 'typ' => 'refresh', + 'typ' => 'refresh', 'iss' => $this->get_iss(), 'iat' => $issued_at, 'nbf' => $not_before, @@ -360,7 +365,12 @@ public function generate_refresh_token( \WP_User $user, string $device = '' ): s $alg = $this->get_alg(); - return JWT::encode( apply_filters( 'jwt_auth_refresh_token_payload', $payload, $user ), $secret_key, $alg ); + $payload = apply_filters( 'jwt_auth_refresh_token_payload', $payload, $user ); + + // Make sure to not lose type property + $payload['typ'] = 'refresh'; + + return JWT::encode( $payload, $secret_key, $alg ); } /** From 69f36b54867811f6fa7bf1e78cfe6fb28438e32e Mon Sep 17 00:00:00 2001 From: Matteo Gaggiano Date: Tue, 19 Mar 2024 15:57:48 +0100 Subject: [PATCH 15/29] fix(refresh token): restore error code for invalid refresh token (not exist, expired) --- class-auth.php | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/class-auth.php b/class-auth.php index 8bba692..386d370 100644 --- a/class-auth.php +++ b/class-auth.php @@ -182,10 +182,10 @@ public function get_token( WP_REST_Request $request ) { if ( $payload instanceof WP_REST_Response ) { return $payload; } - $user = get_user_by( 'id', $payload->data->user->id ); + $user = get_user_by( 'id', $payload->data->user->id ); $device = $payload->data->device; } else { - $user = $this->authenticate_user( $username, $password, $custom_auth ); + $user = $this->authenticate_user( $username, $password, $custom_auth ); $device = $request->get_param( 'device' ) ? $request->get_param( 'device' ) : ''; } @@ -293,11 +293,11 @@ public function generate_token( $user, $return_raw = true ) { * * @return string */ - public function send_refresh_token( \WP_User $user, string $device = ''): string { + public function send_refresh_token( \WP_User $user, string $device = '' ): string { $secret_key = defined( 'JWT_AUTH_SECRET_KEY' ) ? JWT_AUTH_SECRET_KEY : false; $refresh_token = $this->generate_refresh_token( $user, $device ); - $alg = $this->get_alg(); + $alg = $this->get_alg(); $flow = $this->get_flow(); $payload = JWT::decode( $refresh_token, new Key( $secret_key, $alg ) ); @@ -323,9 +323,9 @@ public function send_refresh_token( \WP_User $user, string $device = ''): string // Store next expiry for cron_purge_expired_refresh_tokens event. $expires_next = $payload->exp; - foreach ( $user_refresh_tokens as $device ) { - if ( $device['expires'] < $expires_next ) { - $expires_next = $device['expires']; + foreach ( $user_refresh_tokens as $user_refresh_token ) { + if ( $user_refresh_token['expires'] < $expires_next ) { + $expires_next = $user_refresh_token['expires']; } } update_user_meta( $user->ID, 'jwt_auth_refresh_tokens_expires_next', $expires_next ); @@ -623,8 +623,9 @@ public function refresh_token( \WP_REST_Request $request ) { } // Generate a new access token. - $user = get_user_by( 'id', $payload->data->user->id ); - $refresh_token = $this->send_refresh_token( $user, $payload->data->device ); + $user = get_user_by( 'id', $payload->data->user->id ); + $device = $payload->data->device; + $refresh_token = $this->send_refresh_token( $user, $device ); $flow = $this->get_flow(); @@ -748,12 +749,9 @@ public function validate_refresh_token( $return_response = true ) { $device = empty( $payload->data->device ) ? '' : $payload->data->device; $last_refresh_token_issued = $user_refresh_tokens[ $device ] ?? null; - if ( empty( $last_refresh_token_issued ) || $last_refresh_token_issued['token'] !== $refresh_token ) { - // The refresh token do not match, return error. - throw new Exception( __( 'Refresh token not found for the device.', 'jwt-auth' ) ); - } - - if ( $last_refresh_token_issued['expires'] < time() ) { + if ( empty( $last_refresh_token_issued ) || + $last_refresh_token_issued['token'] !== $refresh_token || + $last_refresh_token_issued['expires'] < time() ) { return new WP_REST_Response( array( 'success' => false, From 8d2559cefda9e806cd05a0a165ff05a01aa923ca Mon Sep 17 00:00:00 2001 From: Matteo Gaggiano Date: Tue, 19 Mar 2024 16:00:59 +0100 Subject: [PATCH 16/29] feature(unit test): update unit test for new refresh token format --- phpunit.xml.dist | 1 + tests/src/AccessTokenTest.php | 286 +++++++----- tests/src/RefreshTokenTest.php | 803 ++++++++++++++++++++------------- tests/src/RestTestTrait.php | 119 +++-- 4 files changed, 733 insertions(+), 476 deletions(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 1f31dd4..b839643 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -15,6 +15,7 @@ + diff --git a/tests/src/AccessTokenTest.php b/tests/src/AccessTokenTest.php index 80c13f3..511c62b 100644 --- a/tests/src/AccessTokenTest.php +++ b/tests/src/AccessTokenTest.php @@ -1,4 +1,4 @@ -client->post('/wp-json/jwt-auth/v1/token', [ - 'form_params' => [ - 'username' => $this->username, - 'password' => $this->password, - ], - ]); - $body = json_decode($response->getBody()->getContents(), true); - $this->assertEquals('jwt_auth_valid_credential', $body['code']); - $this->assertEquals(200, $response->getStatusCode()); - $this->assertEquals(true, $body['success']); - - $this->assertArrayHasKey('data', $body); - $this->assertArrayHasKey('token', $body['data']); - $this->token = $body['data']['token']; - $this->assertNotEmpty($this->token); - - $cookie = $this->cookies->getCookieByName('refresh_token'); - $this->refreshToken = $cookie->getValue(); - $this->assertNotEmpty($this->refreshToken); - $this->assertNotEquals($this->token, $this->refreshToken); - - return $this->token; - } - - /** - * @depends testToken - */ - public function testTokenValidate(string $token): void { - $this->assertNotEmpty($token); - - $response = $this->client->post('/wp-json/jwt-auth/v1/token/validate', [ - 'headers' => [ - 'Authorization' => "Bearer $token", - ], - ]); - $body = json_decode($response->getBody()->getContents(), true); - $this->assertEquals('jwt_auth_valid_token', $body['code']); - $this->assertEquals(200, $response->getStatusCode()); - $this->assertEquals(true, $body['success']); - } - - /** - * @depends testToken - */ - public function testTokenValidateWithInvalidToken(string $token): void { - $this->assertNotEmpty($token); - - $response = $this->client->post('/wp-json/jwt-auth/v1/token/validate', [ - 'headers' => [ - 'Authorization' => "Bearer {$token}123", - ], - ]); - $body = json_decode($response->getBody()->getContents(), true); - $this->assertEquals('jwt_auth_invalid_token', $body['code']); - $this->assertEquals(401, $response->getStatusCode()); - $this->assertEquals(false, $body['success']); - } - - /** - * @depends testToken - */ - public function testTokenRefreshWithInvalidToken(string $token): void { - $this->assertNotEmpty($token); - - $response = $this->client->post('/wp-json/jwt-auth/v1/token/refresh', [ - 'headers' => [ - 'Authorization' => "Bearer {$token}", - ], - ]); - $body = json_decode($response->getBody()->getContents(), true); - $this->assertEquals('jwt_auth_no_auth_cookie', $body['code']); - $this->assertEquals(401, $response->getStatusCode()); - $this->assertEquals(false, $body['success']); - - $cookies = [ - 'refresh_token' => $token, - ]; - $domain = $this->client->getConfig('base_uri')->getHost(); - $cookies = CookieJar::fromArray($cookies, $domain); - - $response = $this->client->post('/wp-json/jwt-auth/v1/token/refresh', [ - 'cookies' => $cookies, - ]); - $body = json_decode($response->getBody()->getContents(), true); - $this->assertEquals('jwt_auth_invalid_refresh_token', $body['code']); - $this->assertEquals(401, $response->getStatusCode()); - $this->assertEquals(false, $body['success']); - } - - /** - * @depends testToken - */ - public function testTokenWithInvalidRefreshToken(string $token): void { - $this->assertNotEmpty($token); - - $cookies = [ - 'refresh_token' => $token, - ]; - $domain = $this->client->getConfig('base_uri')->getHost(); - $cookies = CookieJar::fromArray($cookies, $domain); - - $response = $this->client->post('/wp-json/jwt-auth/v1/token', [ - 'cookies' => $cookies, - ]); - $body = json_decode($response->getBody()->getContents(), true); - $this->assertEquals('jwt_auth_invalid_refresh_token', $body['code']); - $this->assertEquals(401, $response->getStatusCode()); - $this->assertEquals(false, $body['success']); - } + use RestTestTrait; + + public function testToken(): string { + $response = $this->client->post( '/wp-json/jwt-auth/v1/token', [ + 'form_params' => [ + 'username' => $this->username, + 'password' => $this->password, + ], + ] ); + $body = json_decode( $response->getBody()->getContents(), true ); + $this->assertEquals( 'jwt_auth_valid_credential', $body['code'] ); + $this->assertEquals( 200, $response->getStatusCode() ); + $this->assertEquals( true, $body['success'] ); + + $this->assertArrayHasKey( 'data', $body ); + $this->assertArrayHasKey( 'token', $body['data'] ); + $this->token = $body['data']['token']; + $this->assertNotEmpty( $this->token ); + + if ( $this->flow === 'cookie' ) { + $cookie = $this->cookies->getCookieByName( 'refresh_token' ); + $this->refreshToken = $cookie->getValue(); + } else { + $this->assertArrayHasKey( 'refresh_token', $body['data'] ); + $this->refreshToken = $body['data']['refresh_token']; + } + + $this->assertNotEmpty( $this->refreshToken ); + $this->assertNotEquals( $this->token, $this->refreshToken ); + + return $this->token; + } + + /** + * @depends testToken + */ + public function testTokenWithEditedTokenType( string $token ): void { + $this->assertNotEmpty( $token ); + + $payload = json_decode(base64_decode(explode('.', $token)[1]), false); + $payload->typ = 'refresh'; + $malicious_token = implode('.', [ + explode('.', $token)[0], + base64_encode(json_encode($payload)), + explode('.', $token)[2], + ]); + + $request_data = array(); + + if ( $this->flow === 'cookie') { + $cookies = [ + 'refresh_token' => $malicious_token, + ]; + $domain = $this->client->getConfig( 'base_uri' )->getHost(); + $cookies = CookieJar::fromArray( $cookies, $domain ); + $request_data['cookies'] = $cookies; + } else { + $request_data['form_params'] = [ + 'refresh_token' => $malicious_token, + ]; + } + + $response = $this->client->post( '/wp-json/jwt-auth/v1/token/refresh', $request_data ); + $body = json_decode( $response->getBody()->getContents(), true ); + $this->assertIsArray( $body ); + $this->assertArrayHasKey( 'data', $body ); + $this->assertEquals( 'jwt_auth_invalid_refresh_token', $body['code'] ); + $this->assertEquals( 401, $response->getStatusCode() ); + $this->assertEquals( false, $body['success'] ); + } + + /** + * @depends testToken + */ + public function testTokenValidate( string $token ): void { + $this->assertNotEmpty( $token ); + + $response = $this->client->post( '/wp-json/jwt-auth/v1/token/validate', [ + 'headers' => [ + 'Authorization' => "Bearer $token", + ], + ] ); + $body = json_decode( $response->getBody()->getContents(), true ); + $this->assertEquals( 'jwt_auth_valid_token', $body['code'] ); + $this->assertEquals( 200, $response->getStatusCode() ); + $this->assertEquals( true, $body['success'] ); + } + + /** + * @depends testToken + */ + public function testTokenValidateWithInvalidToken( string $token ): void { + $this->assertNotEmpty( $token ); + + $response = $this->client->post( '/wp-json/jwt-auth/v1/token/validate', [ + 'headers' => [ + 'Authorization' => "Bearer {$token}123", + ], + ] ); + $body = json_decode( $response->getBody()->getContents(), true ); + $this->assertEquals( 'jwt_auth_invalid_token', $body['code'] ); + $this->assertEquals( 401, $response->getStatusCode() ); + $this->assertEquals( false, $body['success'] ); + } + + /** + * @depends testToken + */ + public function testTokenRefreshWithInvalidToken( string $token ): void { + $this->assertNotEmpty( $token ); + + $response = $this->client->post( '/wp-json/jwt-auth/v1/token/refresh', [ + 'headers' => [ + 'Authorization' => "Bearer {$token}", + ], + ] ); + $body = json_decode( $response->getBody()->getContents(), true ); + if ( $this->flow === 'cookie' ) { + $this->assertEquals( 'jwt_auth_no_auth_cookie', $body['code'] ); + } else { + $this->assertEquals( 'jwt_auth_no_refresh_token', $body['code'] ); + } + $this->assertEquals( 401, $response->getStatusCode() ); + $this->assertEquals( false, $body['success'] ); + + $request_data = array(); + + if ( $this->flow === 'cookie' ) { + $cookies = [ + 'refresh_token' => $token, + ]; + $domain = $this->client->getConfig( 'base_uri' )->getHost(); + $cookies = CookieJar::fromArray( $cookies, $domain ); + $request_data['cookies'] = $cookies; + } else { + $request_data['form_params'] = [ + 'refresh_token' => $token, + ]; + } + $response = $this->client->post( '/wp-json/jwt-auth/v1/token/refresh', $request_data ); + $body = json_decode( $response->getBody()->getContents(), true ); + $this->assertEquals( 'jwt_auth_invalid_refresh_token', $body['code'] ); + $this->assertEquals( 401, $response->getStatusCode() ); + $this->assertEquals( false, $body['success'] ); + } + + /** + * @depends testToken + */ + public function testTokenWithInvalidRefreshToken( string $token ): void { + $this->assertNotEmpty( $token ); + + $request_data = array(); + + if ( $this->flow === 'cookie' ) { + $cookies = [ + 'refresh_token' => $token, + ]; + $domain = $this->client->getConfig( 'base_uri' )->getHost(); + $cookies = CookieJar::fromArray( $cookies, $domain ); + $request_data['cookies'] = $cookies; + } else { + $request_data['form_params'] = [ + 'refresh_token' => $token, + ]; + } + $response = $this->client->post( '/wp-json/jwt-auth/v1/token', $request_data ); + $body = json_decode( $response->getBody()->getContents(), true ); + $this->assertEquals( 'jwt_auth_invalid_refresh_token', $body['code'] ); + $this->assertEquals( 401, $response->getStatusCode() ); + $this->assertEquals( false, $body['success'] ); + } } diff --git a/tests/src/RefreshTokenTest.php b/tests/src/RefreshTokenTest.php index c8783e9..9842eea 100644 --- a/tests/src/RefreshTokenTest.php +++ b/tests/src/RefreshTokenTest.php @@ -1,4 +1,4 @@ -client->post('/wp-json/jwt-auth/v1/token', [ - 'form_params' => [ - 'username' => $this->username, - 'password' => $this->password, - ], - ]); - $body = json_decode($response->getBody()->getContents(), true); - $this->assertEquals('jwt_auth_valid_credential', $body['code']); - $this->assertEquals(200, $response->getStatusCode()); - $this->assertEquals(true, $body['success']); - - $this->assertArrayHasKey('data', $body); - $this->assertArrayHasKey('token', $body['data']); - $this->token = $body['data']['token']; - $this->assertNotEmpty($this->token); - - // Discard the refresh_token cookie we set above to only retain the - // refresh_token cookie from the response. - $this->cookies->clearSessionCookies(); - - $cookie = $this->cookies->getCookieByName('refresh_token'); - $this->refreshToken = $cookie->getValue(); - $this->assertNotEmpty($this->refreshToken); - $this->assertNotEquals($this->token, $this->refreshToken); - - return $this->refreshToken; - } - - /** - * @depends testToken - */ - public function testTokenValidateWithRefreshToken(string $refreshToken): void { - $this->assertNotEmpty($refreshToken); - - $response = $this->client->post('/wp-json/jwt-auth/v1/token/validate', [ - 'headers' => [ - 'Authorization' => "Bearer {$refreshToken}", - ], - ]); - $body = json_decode($response->getBody()->getContents(), true); - $this->assertEquals('jwt_auth_invalid_token', $body['code']); - $this->assertEquals(401, $response->getStatusCode()); - $this->assertEquals(false, $body['success']); - } - - /** - * @depends testToken - */ - public function testTokenWithRefreshToken(string $refreshToken): void { - $this->assertNotEmpty($refreshToken); - - $cookies = [ - 'refresh_token' => $refreshToken, - ]; - $domain = $this->client->getConfig('base_uri')->getHost(); - $cookies = CookieJar::fromArray($cookies, $domain); - - $response = $this->client->post('/wp-json/jwt-auth/v1/token', [ - 'cookies' => $cookies, - ]); - $body = json_decode($response->getBody()->getContents(), true); - $this->assertEquals('jwt_auth_valid_credential', $body['code']); - $this->assertEquals(200, $response->getStatusCode()); - $this->assertEquals(true, $body['success']); - - $this->assertArrayHasKey('data', $body); - $this->assertArrayHasKey('token', $body['data']); - $this->token = $body['data']['token']; - $this->assertNotEmpty($this->token); - $this->assertNotEquals($this->token, $refreshToken); - - // Discard the refresh_token cookie we set above to only retain the - // refresh_token cookie from the response. - $cookies->clearSessionCookies(); - - $cookie = $cookies->getCookieByName('refresh_token'); - $this->assertEmpty($cookie); - } - - /** - * @depends testToken - */ - public function testTokenWithInvalidRefreshToken(string $refreshToken): void { - $this->assertNotEmpty($refreshToken); - - $cookies = [ - 'refresh_token' => $refreshToken . '123', - ]; - $domain = $this->client->getConfig('base_uri')->getHost(); - $cookies = CookieJar::fromArray($cookies, $domain); - - $response = $this->client->post('/wp-json/jwt-auth/v1/token', [ - 'cookies' => $cookies, - ]); - $body = json_decode($response->getBody()->getContents(), true); - $this->assertEquals('jwt_auth_obsolete_token', $body['code']); - $this->assertEquals(401, $response->getStatusCode()); - $this->assertEquals(false, $body['success']); - } - - /** - * @depends testToken - */ - public function testTokenRefresh(string $refreshToken): string { - $this->assertNotEmpty($refreshToken); - - $cookies = [ - 'refresh_token' => $refreshToken, - ]; - $domain = $this->client->getConfig('base_uri')->getHost(); - $cookies = CookieJar::fromArray($cookies, $domain); - - $response = $this->client->post('/wp-json/jwt-auth/v1/token/refresh', [ - 'cookies' => $cookies, - ]); - $body = json_decode($response->getBody()->getContents(), true); - $this->assertEquals('jwt_auth_valid_token', $body['code']); - $this->assertEquals(200, $response->getStatusCode()); - $this->assertEquals(true, $body['success']); - $this->assertArrayNotHasKey('data', $body); - - // Discard the refresh_token cookie we set above to only retain the - // refresh_token cookie from the response. - $cookies->clearSessionCookies(); - - $cookie = $cookies->getCookieByName('refresh_token'); - $this->refreshToken = $cookie->getValue(); - $this->assertNotEmpty($this->refreshToken); - $this->assertNotEquals($this->refreshToken, $refreshToken); - - return $this->refreshToken; - } - - public function testTokenWithRotatedRefreshToken(): void { - // Not using @depends, because refresh token rotation relies on particular - // order. - $refreshToken1 = $this->testToken(); - $this->assertNotEmpty($refreshToken1); - - $domain = $this->client->getConfig('base_uri')->getHost(); - - // Fetch a new refresh token. - $this->cookies->clear(); - $this->setCookie('refresh_token', $refreshToken1, $domain); - $response = $this->client->post('/wp-json/jwt-auth/v1/token/refresh'); - $body = json_decode($response->getBody()->getContents(), true); - $this->assertEquals('jwt_auth_valid_token', $body['code']); - $this->assertEquals(200, $response->getStatusCode()); - $this->assertEquals(true, $body['success']); - $this->assertArrayNotHasKey('data', $body); - - // Discard the refresh_token cookie we set above to only retain the - // refresh_token cookie from the response. - $this->cookies->clearSessionCookies(); - - $cookie = $this->cookies->getCookieByName('refresh_token'); - $refreshToken2 = $cookie->getValue(); - $this->assertNotEmpty($refreshToken2); - - // Confirm the refresh token was rotated. - $this->assertNotEquals($refreshToken2, $refreshToken1); - - // Confirm the rotated refresh token is valid. - $this->cookies->clear(); - $this->setCookie('refresh_token', $refreshToken2, $domain); - $response = $this->client->post('/wp-json/jwt-auth/v1/token'); - $body = json_decode($response->getBody()->getContents(), true); - $this->assertEquals('jwt_auth_valid_credential', $body['code']); - $this->assertEquals(200, $response->getStatusCode()); - $this->assertEquals(true, $body['success']); - - $this->assertArrayHasKey('data', $body); - $this->assertArrayHasKey('token', $body['data']); - $this->token = $body['data']['token']; - $this->assertNotEmpty($this->token); - $this->assertNotEquals($this->token, $refreshToken2); - - // Discard the refresh_token cookie we set above to only retain the - // refresh_token cookie from the response. - $this->cookies->clearSessionCookies(); - - $cookie = $this->cookies->getCookieByName('refresh_token'); - $this->assertEmpty($cookie); - - // Confirm the previous refresh token is no longer valid. - $this->cookies->clear(); - $this->setCookie('refresh_token', $refreshToken1, $domain); - $response = $this->client->post('/wp-json/jwt-auth/v1/token'); - $body = json_decode($response->getBody()->getContents(), true); - $this->assertEquals('jwt_auth_obsolete_token', $body['code']); - $this->assertEquals(401, $response->getStatusCode()); - $this->assertEquals(false, $body['success']); - } - - public function testTokenRefreshRotationByDevice() { - $domain = $this->client->getConfig('base_uri')->getHost(); - - $devices = [ - 1 => [ - 'device' => 'device1', - ], - 2 => [ - 'device' => 'device2', - ], - ]; - - $this->cookies->clear(); - - // Authenticate with each device. - for ($i = 1; $i <= count($devices); $i++) { - $response = $this->client->post('/wp-json/jwt-auth/v1/token', [ - 'form_params' => [ - 'username' => $this->username, - 'password' => $this->password, - 'device' => $devices[$i]['device'], - ], - ]); - $body = json_decode($response->getBody()->getContents(), true); - $this->assertEquals('jwt_auth_valid_credential', $body['code']); - $cookie = $this->cookies->getCookieByName('refresh_token'); - $devices[$i]['refresh_token'] = $cookie->getValue(); - $this->assertNotEmpty($devices[$i]['refresh_token']); - - if (isset($devices[$i - 1]['refresh_token'])) { - $this->assertNotEquals($devices[$i - 1]['refresh_token'], $devices[$i]['refresh_token']); - } - - $this->cookies->clear(); - } - - // Refresh token with each device. - for ($i = 1; $i <= count($devices); $i++) { - $initial_refresh_token = $devices[$i]['refresh_token']; - - $this->setCookie('refresh_token', $devices[$i]['refresh_token'], $domain); - $response = $this->client->post('/wp-json/jwt-auth/v1/token/refresh', [ - 'form_params' => [ - 'device' => $devices[$i]['device'], - ], - ]); - $body = json_decode($response->getBody()->getContents(), true); - $this->assertEquals('jwt_auth_valid_token', $body['code']); - - // Discard the refresh_token cookie we set above to only retain the - // refresh_token cookie from the response. - $this->cookies->clearSessionCookies(); - $cookie = $this->cookies->getCookieByName('refresh_token'); - $devices[$i]['refresh_token'] = $cookie->getValue(); - $this->assertNotEmpty($devices[$i]['refresh_token']); - - $this->assertNotEquals($initial_refresh_token, $devices[$i]['refresh_token']); - if (isset($devices[$i - 1]['refresh_token'])) { - $this->assertNotEquals($devices[$i - 1]['refresh_token'], $devices[$i]['refresh_token']); - } - - $this->cookies->clear(); - } - - // Confirm each device can use its refresh token to authenticate. - for ($i = 1; $i <= count($devices); $i++) { - $this->setCookie('refresh_token', $devices[$i]['refresh_token'], $domain); - $response = $this->client->post('/wp-json/jwt-auth/v1/token', [ - 'form_params' => [ - 'device' => $devices[$i]['device'], - ], - ]); - $body = json_decode($response->getBody()->getContents(), true); - $this->assertEquals('jwt_auth_valid_credential', $body['code']); - $this->assertArrayHasKey('token', $body['data']); - - $this->cookies->clear(); - } - - // Confirm the previous refresh token is no longer valid. - $this->setCookie('refresh_token', $initial_refresh_token, $domain); - $response = $this->client->post('/wp-json/jwt-auth/v1/token', [ - 'form_params' => [ - 'device' => $devices[count($devices)]['device'], - ], - ]); - $this->assertEquals(401, $response->getStatusCode()); - $body = json_decode($response->getBody()->getContents(), true); - $this->assertEquals('jwt_auth_obsolete_token', $body['code']); - } - - /** - * @depends testToken - */ - public function testTokenRefreshWithInvalidRefreshToken(string $refreshToken): void { - $this->assertNotEmpty($refreshToken); - - $response = $this->client->post('/wp-json/jwt-auth/v1/token/refresh', [ - 'headers' => [ - 'Authorization' => "Bearer {$refreshToken}", - ], - ]); - $body = json_decode($response->getBody()->getContents(), true); - $this->assertEquals('jwt_auth_no_auth_cookie', $body['code']); - $this->assertEquals(401, $response->getStatusCode()); - $this->assertEquals(false, $body['success']); - - $cookies = [ - 'refresh_token' => $refreshToken, - ]; - $domain = $this->client->getConfig('base_uri')->getHost(); - $cookies = CookieJar::fromArray($cookies, $domain); - - $response = $this->client->post('/wp-json/jwt-auth/v1/token/refresh', [ - 'cookies' => $cookies, - ]); - $body = json_decode($response->getBody()->getContents(), true); - $this->assertEquals('jwt_auth_obsolete_token', $body['code']); - $this->assertEquals(401, $response->getStatusCode()); - $this->assertEquals(false, $body['success']); - } + use RestTestTrait; + + public function testToken(): string { + $response = $this->client->post( '/wp-json/jwt-auth/v1/token', [ + 'form_params' => [ + 'username' => $this->username, + 'password' => $this->password, + ], + ] ); + $body = json_decode( $response->getBody()->getContents(), true ); + $this->assertEquals( 'jwt_auth_valid_credential', $body['code'] ); + $this->assertEquals( 200, $response->getStatusCode() ); + $this->assertEquals( true, $body['success'] ); + + $this->assertArrayHasKey( 'data', $body ); + $this->assertArrayHasKey( 'token', $body['data'] ); + $this->token = $body['data']['token']; + $this->assertNotEmpty( $this->token ); + + if ( $this->flow === 'cookie' ) { + // Discard the refresh_token cookie we set above to only retain the + // refresh_token cookie from the response. + $this->cookies->clearSessionCookies(); + + $cookie = $this->cookies->getCookieByName( 'refresh_token' ); + $this->refreshToken = $cookie->getValue(); + } else { + $this->assertArrayHasKey( 'refresh_token', $body['data'] ); + $this->refreshToken = $body['data']['refresh_token']; + } + + $this->assertNotEmpty( $this->refreshToken ); + $this->assertNotEquals( $this->token, $this->refreshToken ); + + return $this->refreshToken; + } + + /** + * @depends testToken + */ + public function testTokenWithEditedTokenType( string $refreshToken ): void { + $this->assertNotEmpty( $refreshToken ); + + $this->assertCount( 3, explode( '.', $refreshToken ) ); + + $payload = json_decode( base64_decode( explode( '.', $refreshToken )[1] ), false ); + $payload->typ = 'access'; + $malicious_refreshToken = implode( '.', [ + explode( '.', $refreshToken )[0], + base64_encode( json_encode( $payload ) ), + explode( '.', $refreshToken )[2], + ] ); + + $response = $this->client->post( '/wp-json/jwt-auth/v1/token/validate', [ + 'headers' => [ + 'Authorization' => "Bearer {$malicious_refreshToken}", + ], + ] ); + $body = json_decode( $response->getBody()->getContents(), true ); + $this->assertIsArray( $body ); + $this->assertArrayHasKey( 'data', $body ); + $this->assertEquals( 'jwt_auth_invalid_token', $body['code'] ); + $this->assertEquals( 401, $response->getStatusCode() ); + $this->assertEquals( false, $body['success'] ); + } + + /** + * @depends testToken + */ + public function testTokenValidateWithRefreshToken( string $refreshToken ): void { + $this->assertNotEmpty( $refreshToken ); + + $response = $this->client->post( '/wp-json/jwt-auth/v1/token/validate', [ + 'headers' => [ + 'Authorization' => "Bearer {$refreshToken}", + ], + ] ); + $body = json_decode( $response->getBody()->getContents(), true ); + $this->assertIsArray( $body ); + $this->assertArrayHasKey( 'data', $body ); + $this->assertEquals( 'jwt_auth_invalid_token', $body['code'] ); + $this->assertEquals( 401, $response->getStatusCode() ); + $this->assertEquals( false, $body['success'] ); + } + + /** + * @depends testToken + */ + public function testTokenWithRefreshToken( string $refreshToken ): void { + $this->assertNotEmpty( $refreshToken ); + + $request_data = array(); + + if ( $this->flow === 'cookie' ) { + $cookies = [ + 'refresh_token' => $refreshToken, + ]; + $domain = $this->client->getConfig( 'base_uri' )->getHost(); + $cookies = CookieJar::fromArray( $cookies, $domain ); + + $request_data['cookies'] = $cookies; + + } else { + $request_data['form_params'] = [ + 'refresh_token' => $refreshToken, + ]; + } + + $response = $this->client->post( '/wp-json/jwt-auth/v1/token', $request_data ); + + $body = json_decode( $response->getBody()->getContents(), true ); + $this->assertEquals( 'jwt_auth_valid_credential', $body['code'] ); + $this->assertEquals( 200, $response->getStatusCode() ); + $this->assertEquals( true, $body['success'] ); + + $this->assertArrayHasKey( 'data', $body ); + $this->assertArrayHasKey( 'token', $body['data'] ); + $this->token = $body['data']['token']; + $this->assertNotEmpty( $this->token ); + $this->assertNotEquals( $this->token, $refreshToken ); + + if ( $this->flow === 'cookie' ) { + // Discard the refresh_token cookie we set above to only retain the + // refresh_token cookie from the response. + $this->cookies->clearSessionCookies(); + + $cookie = $this->cookies->getCookieByName( 'refresh_token' ); + $this->refreshToken = $cookie->getValue(); + } else { + $this->assertArrayHasKey( 'refresh_token', $body['data'] ); + $this->refreshToken = $body['data']['refresh_token']; + } + + $this->assertNotEmpty( $this->refreshToken ); + $this->assertNotEquals( $this->token, $this->refreshToken ); + } + + /** + * @depends testToken + */ + public function testTokenWithInvalidRefreshToken( string $refreshToken ): void { + $this->assertNotEmpty( $refreshToken ); + + $request_data = array(); + + if ( $this->flow === 'cookie' ) { + + $cookies = [ + 'refresh_token' => $refreshToken . '123', + ]; + $domain = $this->client->getConfig( 'base_uri' )->getHost(); + $cookies = CookieJar::fromArray( $cookies, $domain ); + + $request_data['cookies'] = $cookies; + } else { + + $request_data['form_params'] = [ + 'refresh_token' => $refreshToken . '123', + ]; + + } + + $response = $this->client->post( '/wp-json/jwt-auth/v1/token', $request_data ); + $body = json_decode( $response->getBody()->getContents(), true ); + $this->assertEquals( 'jwt_auth_invalid_refresh_token', $body['code'] ); + $this->assertEquals( 401, $response->getStatusCode() ); + $this->assertEquals( false, $body['success'] ); + } + + /** + * @depends testToken + */ + public function testTokenRefresh( string $refreshToken ): string { + $this->assertNotEmpty( $refreshToken ); + + // Wait 1 seconds as the token creation is based on timestamp in seconds. + sleep( 1 ); + + $request_data = array(); + + if ( $this->flow === 'cookie' ) { + $cookies = [ + 'refresh_token' => $refreshToken, + ]; + $domain = $this->client->getConfig( 'base_uri' )->getHost(); + $cookies = CookieJar::fromArray( $cookies, $domain ); + + $request_data['cookies'] = $cookies; + } else { + $request_data['form_params'] = [ + 'refresh_token' => $refreshToken, + ]; + } + + $response = $this->client->post( '/wp-json/jwt-auth/v1/token/refresh', $request_data ); + $body = json_decode( $response->getBody()->getContents(), true ); + $this->assertEquals( 'jwt_auth_valid_token', $body['code'] ); + $this->assertEquals( 200, $response->getStatusCode() ); + $this->assertEquals( true, $body['success'] ); + + if ( $this->flow === 'cookie' ) { + $this->assertArrayNotHasKey( 'data', $body ); + + // Discard the refresh_token cookie we set above to only retain the + // refresh_token cookie from the response. + $cookies->clearSessionCookies(); + + $cookie = $cookies->getCookieByName( 'refresh_token' ); + $this->refreshToken = $cookie->getValue(); + } else { + $this->assertArrayHasKey( 'data', $body ); + $this->assertArrayHasKey( 'refresh_token', $body['data'] ); + $this->refreshToken = $body['data']['refresh_token']; + } + + $this->assertNotEmpty( $this->refreshToken ); + $this->assertNotEquals( $this->refreshToken, $refreshToken ); + + return $this->refreshToken; + } + + public function testTokenWithRotatedRefreshToken(): void { + // Not using @depends, because refresh token rotation relies on particular + // order. + $refreshToken1 = $this->testToken(); + $this->assertNotEmpty( $refreshToken1 ); + + // Wait 1 seconds as the token creation is based on timestamp in seconds. + sleep( 1 ); + + $request_data = array(); + + if ( $this->flow === 'cookie' ) { + $domain = $this->client->getConfig( 'base_uri' )->getHost(); + + // Fetch a new refresh token. + $this->cookies->clear(); + $this->setCookie( 'refresh_token', $refreshToken1, $domain ); + } else { + $request_data['form_params'] = [ + 'refresh_token' => $refreshToken1, + ]; + } + + $response = $this->client->post( '/wp-json/jwt-auth/v1/token/refresh', $request_data ); + $body = json_decode( $response->getBody()->getContents(), true ); + $this->assertEquals( 'jwt_auth_valid_token', $body['code'] ); + $this->assertEquals( 200, $response->getStatusCode() ); + $this->assertEquals( true, $body['success'] ); + + if ( $this->flow === 'cookie' ) { + $this->assertArrayNotHasKey( 'data', $body ); + + // Discard the refresh_token cookie we set above to only retain the + // refresh_token cookie from the response. + $this->cookies->clearSessionCookies(); + + $cookie = $this->cookies->getCookieByName( 'refresh_token' ); + $refreshToken2 = $cookie->getValue(); + + } else { + $this->assertArrayHasKey( 'data', $body ); + $this->assertArrayHasKey( 'refresh_token', $body['data'] ); + $refreshToken2 = $body['data']['refresh_token']; + } + $this->assertNotEmpty( $refreshToken2 ); + + // Confirm the refresh token was rotated. + $this->assertNotEquals( $refreshToken2, $refreshToken1 ); + + if ( $this->flow === 'cookie' ) { + // Confirm the rotated refresh token is valid. + $this->cookies->clear(); + $this->setCookie( 'refresh_token', $refreshToken2, $domain ); + } else { + $request_data['form_params'] = [ + 'refresh_token' => $refreshToken2, + ]; + } + + $response = $this->client->post( '/wp-json/jwt-auth/v1/token', $request_data ); + $body = json_decode( $response->getBody()->getContents(), true ); + $this->assertEquals( 'jwt_auth_valid_credential', $body['code'] ); + $this->assertEquals( 200, $response->getStatusCode() ); + $this->assertEquals( true, $body['success'] ); + + $this->assertArrayHasKey( 'data', $body ); + $this->assertArrayHasKey( 'token', $body['data'] ); + $this->token = $body['data']['token']; + $this->assertNotEmpty( $this->token ); + $this->assertNotEquals( $this->token, $refreshToken2 ); + + if ( $this->flow === 'cookie' ) { + // Discard the refresh_token cookie we set above to only retain the + // refresh_token cookie from the response. + $this->cookies->clearSessionCookies(); + + $cookie = $this->cookies->getCookieByName( 'refresh_token' ); + $this->assertEmpty( $cookie ); + + // Confirm the previous refresh token is no longer valid. + $this->cookies->clear(); + $this->setCookie( 'refresh_token', $refreshToken1, $domain ); + } else { + $request_data['form_params'] = [ + 'refresh_token' => $refreshToken1, + ]; + } + $response = $this->client->post( '/wp-json/jwt-auth/v1/token', $request_data ); + $body = json_decode( $response->getBody()->getContents(), true ); + $this->assertEquals( 'jwt_auth_obsolete_token', $body['code'], $body['message'] ); + $this->assertEquals( 401, $response->getStatusCode() ); + $this->assertEquals( false, $body['success'] ); + } + + public function testTokenRefreshRotationByDevice() { + $domain = $this->client->getConfig( 'base_uri' )->getHost(); + + $devices = [ + 1 => [ + 'device' => 'device1', + ], + 2 => [ + 'device' => 'device2', + ], + ]; + + $this->cookies->clear(); + + // Authenticate with each device. + for ( $i = 1; $i <= count( $devices ); $i ++ ) { + $response = $this->client->post( '/wp-json/jwt-auth/v1/token', [ + 'form_params' => [ + 'username' => $this->username, + 'password' => $this->password, + 'device' => $devices[ $i ]['device'], + ], + ] ); + $body = json_decode( $response->getBody()->getContents(), true ); + $this->assertEquals( 'jwt_auth_valid_credential', $body['code'] ); + + if ( $this->flow === 'cookie' ) { + $cookie = $this->cookies->getCookieByName( 'refresh_token' ); + $devices[ $i ]['refresh_token'] = $cookie->getValue(); + } else { + $this->assertArrayHasKey( 'data', $body ); + $this->assertArrayHasKey( 'refresh_token', $body['data'] ); + $devices[ $i ]['refresh_token'] = $body['data']['refresh_token']; + } + $this->assertNotEmpty( $devices[ $i ]['refresh_token'] ); + + if ( isset( $devices[ $i - 1 ]['refresh_token'] ) ) { + $this->assertNotEquals( $devices[ $i - 1 ]['refresh_token'], $devices[ $i ]['refresh_token'] ); + } + + $this->cookies->clear(); + } + + // Wait 1 seconds as the token creation is based on timestamp in seconds. + sleep( 1 ); + + // Refresh token with each device. + for ( $i = 1; $i <= count( $devices ); $i ++ ) { + $initial_refresh_token = $devices[ $i ]['refresh_token']; + + $request_data = array(); + if ( $this->flow === 'cookie' ) { + $request_data['form_params'] = [ + 'device' => $devices[ $i ]['device'], + ]; + $this->setCookie( 'refresh_token', $devices[ $i ]['refresh_token'], $domain ); + } else { + $request_data['form_params'] = [ + 'refresh_token' => $devices[ $i ]['refresh_token'], + ]; + } + + $response = $this->client->post( '/wp-json/jwt-auth/v1/token/refresh', $request_data ); + $body = json_decode( $response->getBody()->getContents(), true ); + $this->assertEquals( 'jwt_auth_valid_token', $body['code'] ); + + if ( $this->flow === 'cookie' ) { + // Discard the refresh_token cookie we set above to only retain the + // refresh_token cookie from the response. + $this->cookies->clearSessionCookies(); + $cookie = $this->cookies->getCookieByName( 'refresh_token' ); + $devices[ $i ]['refresh_token'] = $cookie->getValue(); + } else { + $this->assertArrayHasKey( 'data', $body ); + $this->assertArrayHasKey( 'refresh_token', $body['data'] ); + $devices[ $i ]['refresh_token'] = $body['data']['refresh_token']; + } + $this->assertNotEmpty( $devices[ $i ]['refresh_token'] ); + + $this->assertNotEquals( $initial_refresh_token, $devices[ $i ]['refresh_token'] ); + if ( isset( $devices[ $i - 1 ]['refresh_token'] ) ) { + $this->assertNotEquals( $devices[ $i - 1 ]['refresh_token'], $devices[ $i ]['refresh_token'] ); + } + + $this->cookies->clear(); + } + + // Confirm each device can use its refresh token to authenticate. + for ( $i = 1; $i <= count( $devices ); $i ++ ) { + + $request_data = array(); + if ( $this->flow === 'cookie' ) { + $this->setCookie( 'refresh_token', $devices[ $i ]['refresh_token'], $domain ); + } else { + $request_data['form_params'] = [ + 'refresh_token' => $devices[ $i ]['refresh_token'], + ]; + } + $response = $this->client->post( '/wp-json/jwt-auth/v1/token', $request_data ); + $body = json_decode( $response->getBody()->getContents(), true ); + $this->assertEquals( 'jwt_auth_valid_credential', $body['code'] ); + $this->assertArrayHasKey( 'data', $body ); + $this->assertArrayHasKey( 'token', $body['data'] ); + + if ( $this->flow === 'cookie' ) { + $this->cookies->clear(); + } else { + $this->assertArrayHasKey( 'refresh_token', $body['data'] ); + } + } + + $request_data = array(); + // Confirm the previous refresh token is no longer valid. + if ( $this->flow === 'cookie' ) { + $this->setCookie( 'refresh_token', $initial_refresh_token, $domain ); + } else { + $request_data['form_params'] = [ + 'refresh_token' => $initial_refresh_token, + ]; + } + + $response = $this->client->post( '/wp-json/jwt-auth/v1/token', $request_data ); + $this->assertEquals( 401, $response->getStatusCode() ); + $body = json_decode( $response->getBody()->getContents(), true ); + $this->assertEquals( 'jwt_auth_obsolete_token', $body['code'] ); + } + + /** + * @depends testToken + */ + public function testTokenRefreshWithInvalidRefreshToken( string $refreshToken ): void { + $this->assertNotEmpty( $refreshToken ); + + $response = $this->client->post( '/wp-json/jwt-auth/v1/token/refresh', [ + 'headers' => [ + 'Authorization' => "Bearer {$refreshToken}", + ], + ] ); + $body = json_decode( $response->getBody()->getContents(), true ); + + if ( $this->flow === 'cookie' ) { + $this->assertEquals( 'jwt_auth_no_auth_cookie', $body['code'] ); + } else { + $this->assertEquals( 'jwt_auth_no_refresh_token', $body['code'] ); + } + $this->assertEquals( 401, $response->getStatusCode() ); + $this->assertEquals( false, $body['success'] ); + + $request_data = array(); + if ( $this->flow === 'cookie' ) { + $cookies = [ + 'refresh_token' => $refreshToken, + ]; + $domain = $this->client->getConfig( 'base_uri' )->getHost(); + $cookies = CookieJar::fromArray( $cookies, $domain ); + $request_data['cookies'] = $cookies; + } else { + $request_data['form_params'] = [ + 'refresh_token' => $refreshToken, + ]; + } + + $response = $this->client->post( '/wp-json/jwt-auth/v1/token/refresh', $request_data ); + $body = json_decode( $response->getBody()->getContents(), true ); + $this->assertEquals( 'jwt_auth_obsolete_token', $body['code'] ); + $this->assertEquals( 401, $response->getStatusCode() ); + $this->assertEquals( false, $body['success'] ); + } } diff --git a/tests/src/RestTestTrait.php b/tests/src/RestTestTrait.php index b7b4a20..65b331c 100644 --- a/tests/src/RestTestTrait.php +++ b/tests/src/RestTestTrait.php @@ -1,4 +1,4 @@ -cookies = new CookieJar(); - $options = [ - 'base_uri' => $_ENV['URL'], - 'http_errors' => false, - 'cookies' => $this->cookies, - // PHP's cURL library attempts to resolve domains with IPv6, causing a - // long delay on local dev machines, since typically not set up for IPv6. - // @see https://stackoverflow.com/questions/17814925/php-curl-consistently-taking-15s-to-resolve-dns - // @see https://www.php.net/manual/de/function.curl-setopt.php - // @see https://wordpress.org/support/topic/curl-error-28-operation-timed-out-after-5000-milliseconds-with-0-bytes-received/ - // @see https://docs.presscustomizr.com/article/326-how-to-fix-a-curl-error-28-connection-timed-out-in-wordpress - // How to avoid this: - // - Add to /etc/hosts: "::1 example.com" - // - Add to Apache httpd.conf: "Listen ::1" - // - Listen to any IP: "" - 'force_ip_resolve' => 'v4', - 'curl' => [ - CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4, - ], - ]; - if (in_array('--debug', $_SERVER['argv'], true)) { - $options['debug'] = true; - } - $this->client = new Client($options); - $this->username = $_ENV['USERNAME']; - $this->password = $_ENV['PASSWORD']; - } + /** + * @var \GuzzleHttp\Cookie\CookieJar + */ + protected $cookies; - protected function setCookie($name, $value, $domain): CookieJar { - $this->cookies->setCookie(new SetCookie([ - 'Domain' => $domain, - 'Name' => $name, - 'Value' => $value, - 'Discard' => true, - ])); - return $this->cookies; - } + /** + * @var string + + */ + protected $username; + + /** + * @var string + */ + protected $password; + + /** + * @var string + */ + protected $flow; + + /** + * @var string + */ + protected $token; + + /** + * @var string + */ + protected $refreshToken; + + protected function setUp(): void { + $this->cookies = new CookieJar(); + $options = [ + 'base_uri' => $_ENV['TEST_URL'], + 'http_errors' => false, + 'cookies' => $this->cookies, + // PHP's cURL library attempts to resolve domains with IPv6, causing a + // long delay on local dev machines, since typically not set up for IPv6. + // @see https://stackoverflow.com/questions/17814925/php-curl-consistently-taking-15s-to-resolve-dns + // @see https://www.php.net/manual/de/function.curl-setopt.php + // @see https://wordpress.org/support/topic/curl-error-28-operation-timed-out-after-5000-milliseconds-with-0-bytes-received/ + // @see https://docs.presscustomizr.com/article/326-how-to-fix-a-curl-error-28-connection-timed-out-in-wordpress + // How to avoid this: + // - Add to /etc/hosts: "::1 example.com" + // - Add to Apache httpd.conf: "Listen ::1" + // - Listen to any IP: "" + 'force_ip_resolve' => 'v4', + 'curl' => [ + 113 => 1, + ], + ]; + if ( in_array( '--debug', $_SERVER['argv'], true ) ) { + $options['debug'] = true; + } + $this->client = new Client( $options ); + $this->username = $_ENV['TEST_USERNAME']; + $this->password = $_ENV['TEST_PASSWORD']; + $this->flow = $_ENV['TEST_FLOW']; + } + + protected function setCookie( $name, $value, $domain ): CookieJar { + $this->cookies->setCookie( new SetCookie( [ + 'Domain' => $domain, + 'Name' => $name, + 'Value' => $value, + 'Discard' => true, + ] ) ); + + return $this->cookies; + } } From acbbbeba0aa8d5c21998a7141ec2139a9ae57132 Mon Sep 17 00:00:00 2001 From: Matteo Gaggiano Date: Tue, 19 Mar 2024 16:26:50 +0100 Subject: [PATCH 17/29] fix(unit test): restore environment names --- tests/src/RestTestTrait.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/src/RestTestTrait.php b/tests/src/RestTestTrait.php index 65b331c..b95aa4a 100644 --- a/tests/src/RestTestTrait.php +++ b/tests/src/RestTestTrait.php @@ -57,7 +57,7 @@ trait RestTestTrait { protected function setUp(): void { $this->cookies = new CookieJar(); $options = [ - 'base_uri' => $_ENV['TEST_URL'], + 'base_uri' => $_ENV['URL'], 'http_errors' => false, 'cookies' => $this->cookies, // PHP's cURL library attempts to resolve domains with IPv6, causing a @@ -79,9 +79,9 @@ protected function setUp(): void { $options['debug'] = true; } $this->client = new Client( $options ); - $this->username = $_ENV['TEST_USERNAME']; - $this->password = $_ENV['TEST_PASSWORD']; - $this->flow = $_ENV['TEST_FLOW']; + $this->username = $_ENV['USERNAME']; + $this->password = $_ENV['PASSWORD']; + $this->flow = $_ENV['FLOW']; } protected function setCookie( $name, $value, $domain ): CookieJar { From 4453afd02ccd654e9b6bc4cb154de8ef95ef331e Mon Sep 17 00:00:00 2001 From: Matteo Gaggiano Date: Tue, 19 Mar 2024 16:30:58 +0100 Subject: [PATCH 18/29] fix(unit test): restore CURL constants --- tests/src/RestTestTrait.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/src/RestTestTrait.php b/tests/src/RestTestTrait.php index b95aa4a..9f139b8 100644 --- a/tests/src/RestTestTrait.php +++ b/tests/src/RestTestTrait.php @@ -30,7 +30,6 @@ trait RestTestTrait { /** * @var string - */ protected $username; @@ -72,7 +71,7 @@ protected function setUp(): void { // - Listen to any IP: "" 'force_ip_resolve' => 'v4', 'curl' => [ - 113 => 1, + CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4, ], ]; if ( in_array( '--debug', $_SERVER['argv'], true ) ) { From 364d106180e0633eee7833f004630ad6967c7092 Mon Sep 17 00:00:00 2001 From: Matteo Gaggiano Date: Tue, 19 Mar 2024 16:36:48 +0100 Subject: [PATCH 19/29] docs: add missing dev requirement ext-curl --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index 79442e6..14f3771 100644 --- a/composer.json +++ b/composer.json @@ -20,6 +20,7 @@ }, "require-dev": { "php": ">=7.4", + "ext-curl": "*", "phpunit/phpunit": "^9.5", "guzzlehttp/guzzle": "^7.4" }, From 6bfa233454595d7930622d4982db2f0713894e7d Mon Sep 17 00:00:00 2001 From: Matteo Gaggiano Date: Wed, 20 Mar 2024 15:53:52 +0100 Subject: [PATCH 20/29] fix(refresh token): rollback WP_REST_Request to retrieve refresh token from request --- class-auth.php | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/class-auth.php b/class-auth.php index 386d370..91da090 100644 --- a/class-auth.php +++ b/class-auth.php @@ -173,10 +173,10 @@ public function get_token( WP_REST_Request $request ) { ); } - $refresh_token = $this->retrieve_refresh_token(); + $refresh_token = $this->retrieve_refresh_token( $request ); if ( ! empty( $refresh_token ) ) { - $payload = $this->validate_refresh_token( false ); + $payload = $this->validate_refresh_token( $refresh_token, false ); // If we receive a REST response, then validation failed. if ( $payload instanceof WP_REST_Response ) { @@ -603,7 +603,7 @@ public function validate_token( $return_response_or_request = true ) { */ public function refresh_token( \WP_REST_Request $request ) { - $input_refresh_token = $this->retrieve_refresh_token(); + $input_refresh_token = $this->retrieve_refresh_token( $request ); if ( empty( $input_refresh_token ) ) { return new WP_REST_Response( @@ -617,7 +617,7 @@ public function refresh_token( \WP_REST_Request $request ) { ); } - $payload = $this->validate_refresh_token( false ); + $payload = $this->validate_refresh_token( $input_refresh_token, false ); if ( $payload instanceof WP_REST_Response ) { return $payload; } @@ -652,13 +652,12 @@ public function refresh_token( \WP_REST_Request $request ) { /** * Validates refresh token. * + * @param string $refresh_token The refresh token. * @param bool $return_response Either to return full WP_REST_Response or to return the payload only. * * @return \stdClass|WP_REST_Response Returns user ID if valid or WP_REST_Response on error. */ - public function validate_refresh_token( $return_response = true ) { - - $refresh_token = $this->retrieve_refresh_token(); + public function validate_refresh_token( $refresh_token, $return_response = true ) { // Get the Secret Key. $secret_key = defined( 'JWT_AUTH_SECRET_KEY' ) ? JWT_AUTH_SECRET_KEY : false; @@ -885,17 +884,19 @@ public function rest_pre_dispatch( $result, WP_REST_Server $server, WP_REST_Requ /** * Retrieves the refresh token based on a flow * + * @param WP_REST_Request $request + * * @return string|null */ - private function retrieve_refresh_token(): ?string { + private function retrieve_refresh_token( WP_REST_Request $request ): ?string { $flow = $this->get_flow(); if ( 'body' === $flow ) { - $_array = $_POST; + $_array = $request->get_json_params(); } else if ( 'query' === $flow ) { - $_array = $_REQUEST; + $_array = $request->get_query_params(); } else if ( 'header' === $flow ) { - $_array = getallheaders() ?: array(); + $_array = $request->get_headers(); } else { // default cookie $_array = $_COOKIE; } From 2f72a02109d00a396bad74ee4322304a346bad0a Mon Sep 17 00:00:00 2001 From: Matteo Gaggiano Date: Fri, 12 Apr 2024 15:34:12 +0200 Subject: [PATCH 21/29] docs(refresh token): updated README.md with latest information, updated readme.txt as well --- README.md | 191 ++++++++++++++++++++++++++++++++++++++++++++++++--- jwt-auth.php | 2 +- readme.txt | 3 + 3 files changed, 185 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index a17d3fe..9cff2a7 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,7 @@ You can use the optional parameter `device` with the device identifier to let us "message": "Credential is valid", "data": { "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczpcL1wvcG9pbnRzLmNvdXZlZS5jby5pZCIsImlhdCI6MTU4ODQ5OTE0OSwibmJmIjoxNTg4NDk5MTQ5LCJleHAiOjE1ODkxMDM5NDksImRhdGEiOnsidXNlciI6eyJpZCI6MX19fQ.w3pf5PslhviHohmiGF-JlPZV00XWE9c2MfvBK7Su9Fw", + "refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczpcL1wvcG9pbnRzLmNvdXZlZS5jby5pZCIsImlhdCI6MTU4ODQ5OTE0OSwibmJmIjoxNTg4NDk5MTQ5LCJleHAiOjE1ODkxMDM5NDksImRhdGEiOnsidXNlciI6eyJpZCI6MX19fQ.w3pf5PslhviHohmiGF-JlPZV00XWE9c2MfvBK7Su9Fw", "id": 1, "email": "contactjavas@gmail.com", "nicename": "contactjavas", @@ -206,7 +207,6 @@ If the refresh token is valid, then you receive a new refresh token as a cookie By default, each refresh token expires after 30 days. - ### Refresh Token Rotation Whenever you are authenticating afresh or refreshing the refresh token, only the last issued refresh token remains valid. All previously issued refresh tokens can no longer be used. @@ -217,10 +217,24 @@ This means that a refresh token cannot be shared. To allow multiple devices to a curl -F device="abc-def" -F username=myuser -F password=mypass /wp-json/jwt-auth/v1/token ``` ```sh -curl -F device="abc-def" -b "refresh_token=123.abcdef..." /wp-json/jwt-auth/v1/token +# For a cookie flow +curl -F device="abc-def" -b "refresh_token=eyJ0eXAiOi..." /wp-json/jwt-auth/v1/token + +# For a body flow +curl -F device="abc-def" -d "refresh_token=eyJ0eXAiOi..." /wp-json/jwt-auth/v1/token + +# For a parameter flow +curl -F device="abc-def" "/wp-json/jwt-auth/v1/token?refresh_token=eyJ0eXAiOi..." ``` ```sh -curl -F device="abc-def" -b "refresh_token=123.abcdef..." /wp-json/jwt-auth/v1/token/refresh +# For a cookie flow +curl -F device="abc-def" -b "refresh_token=eyJ0eXAiOi..." /wp-json/jwt-auth/v1/token/refresh + +# For a body flow +curl -F device="abc-def" -d "refresh_token=eyJ0eXAiOi..." /wp-json/jwt-auth/v1/token/refresh + +# For a parameter flow +curl -F device="abc-def" "/wp-json/jwt-auth/v1/token/refresh?refresh_token=eyJ0eXAiOi..." ``` @@ -324,7 +338,29 @@ If the token is invalid an error will be returned. Here are some samples of erro } ``` -## Available Filter Hooks +**Invalid Refresh Token** + +```json +{ + "success": false, + "statusCode": 401, + "code": "jwt_auth_invalid_refresh_token", + "message": "Device not found in the refresh token.", + "data": [] +} +``` + +```json +{ + "success": false, + "statusCode": 401, + "code": "jwt_auth_invalid_refresh_token", + "message": "Invalid token type", + "data": [] +} +``` + ++## Available Filter Hooks **JWT Auth** is developer friendly and has some filters available to override the default settings. @@ -357,6 +393,36 @@ add_filter( ``` +### jwt_auth_flow + +The **jwt_auth_flow** allows you to decide which flow use for current request. + +The supported options are: +- cookie __*(default)*__ +- body +- query +- header + +To enable the desired refresh token flow add an hook to your theme's functions.php file. +```php +/** + * Change the flow for refresh token. + * + * @param string $flow The current flow. + */ +add_filter( + 'jwt_auth_flow', + function ( $headers ) { + if (wp_doing_ajax()) { + // Modify the flow here. + return 'body'; + } + return $flow; +); +``` + +This value will be used to establish from with part of the request the refresh token will be taken. + ### jwt_auth_authorization_header The **jwt_auth_authorization_header** allows you to modify the Authorization header key used to validating a token. Useful when the server already uses the 'Authorization' key for another auth method. @@ -419,6 +485,8 @@ add_filter( ### jwt_auth_not_before +#### alias for [jwt_auth_toke_not_before](#jwt_auth_token_not_before) + The `jwt_auth_not_before` allows you to change the [**nbf**](https://tools.ietf.org/html/rfc7519#section-4.1.5) value before the payload is encoded to be a token Default Value: @@ -450,8 +518,43 @@ add_filter( ); ``` +### jwt_auth_token_not_before + +The `jwt_auth_token_not_before` allows you to change the [**nbf**](https://tools.ietf.org/html/rfc7519#section-4.1.5) value before the payload is encoded to be a token + +Default Value: + +``` +// Creation time. +time() +``` + +Usage example: + +```php +/** + * Change the token's nbf value. + * + * @param int $not_before The default "nbf" value in timestamp. + * @param int $issued_at The "iat" value in timestamp. + * + * @return int The "nbf" value. + */ +add_filter( + 'jwt_auth_token_not_before', + function ( $not_before, $issued_at ) { + // Modify the "not_before" here. + return $not_before; + }, + 10, + 2 +); +``` + ### jwt_auth_expire +#### alias for [jwt_auth_token_expire](#jwt_auth_token_expire) + The `jwt_auth_expire` allows you to change the [**exp**](https://tools.ietf.org/html/rfc7519#section-4.1.4) value before the payload is encoded to be a token Default Value: @@ -482,9 +585,77 @@ add_filter( ); ``` + +### jwt_auth_token_expire + +The `jwt_auth_token_expire` allows you to change the [**exp**](https://tools.ietf.org/html/rfc7519#section-4.1.4) value before the payload is encoded to be a token + +Default Value: + +``` +time() + (MINUTE_IN_SECONDS * 10) +``` + +Usage example: + +```php +/** + * Change the token's expire value. + * + * @param int $expire The default "exp" value in timestamp. + * @param int $issued_at The "iat" value in timestamp. + * + * @return int The "nbf" value. + */ +add_filter( + 'jwt_auth_token_expire', + function ( $expire, $issued_at ) { + // Modify the "expire" here. + return $expire; + }, + 10, + 2 +); +``` + + + +### jwt_auth_refresh_not_before + +The `jwt_auth_refresh_not_before` allows you to change the [**nbf**](https://tools.ietf.org/html/rfc7519#section-4.1.5) value before the payload is encoded to be a refresh token + +Default Value: + +``` +// Creation time. +time() +``` + +Usage example: + +```php +/** + * Change the refresh token's nbf value. + * + * @param int $not_before The default "nbf" value in timestamp. + * @param int $issued_at The "iat" value in timestamp. + * + * @return int The "nbf" value. + */ +add_filter( + 'jwt_auth_refresh_not_before', + function ( $not_before, $issued_at ) { + // Modify the "not_before" here. + return $not_before; + }, + 10, + 2 +); +``` + ### jwt_auth_refresh_expire -The `jwt_auth_refresh_expire` filter hook allows you to change the expiration date of the refresh token. +The `jwt_auth_refresh_expire` allows you to change the [**exp**](https://tools.ietf.org/html/rfc7519#section-4.1.4) value before the payload is encoded to be a refresh token Default Value: @@ -714,15 +885,15 @@ add_filter( There are end-to-end tests you can run to confirm that the API works correctly: ```console -$ URL=https://example.local USERNAME=myuser PASSWORD=mypass composer run test +$ URL=https://example.local USERNAME=myuser PASSWORD=mypass FLOW=cookie composer run test > ./vendor/bin/phpunit -PHPUnit 9.5.13 by Sebastian Bergmann and contributors. +PHPUnit 9.5.25 #StandWithUkraine -............. 13 / 13 (100%) +............... 15 / 15 (100%) -Time: 00:12.377, Memory: 6.00 MB +Time: 00:48.086, Memory: 8.00 MB -OK (13 tests, 110 assertions) +OK (15 tests, 143 assertions) ``` diff --git a/jwt-auth.php b/jwt-auth.php index 1c6b429..83bcc92 100644 --- a/jwt-auth.php +++ b/jwt-auth.php @@ -19,7 +19,7 @@ // Helper constants. define( 'JWT_AUTH_PLUGIN_DIR', rtrim( plugin_dir_path( __FILE__ ), '/' ) ); define( 'JWT_AUTH_PLUGIN_URL', rtrim( plugin_dir_url( __FILE__ ), '/' ) ); -define( 'JWT_AUTH_PLUGIN_VERSION', '3.0.1' ); +define( 'JWT_AUTH_PLUGIN_VERSION', '3.0.2' ); // Require composer. require __DIR__ . '/vendor/autoload.php'; diff --git a/readme.txt b/readme.txt index 8d57c76..1d1642f 100644 --- a/readme.txt +++ b/readme.txt @@ -747,6 +747,9 @@ You can help this plugin stay alive and maintained by giving **5 Stars** Rating/ 3. Other error responses == Changelog == += 3.0.2 = +- New Feature: Added support for different flow + = 3.0.1 = - Updated firebase/php-jwt to 6.3 to address security issue in versions prior to 6.x. From 021dcbc8fdf6d031bf1425aa6756305572e9e04f Mon Sep 17 00:00:00 2001 From: Matteo Gaggiano Date: Thu, 25 Jul 2024 18:05:23 +0200 Subject: [PATCH 22/29] fix: updated tests with correct body implementation --- composer.json | 2 +- tests/src/AccessTokenTest.php | 40 +++++++---- tests/src/RefreshTokenTest.php | 118 ++++++++++++++++++++++----------- 3 files changed, 107 insertions(+), 53 deletions(-) diff --git a/composer.json b/composer.json index a890557..481c440 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,7 @@ "minimum-stability": "stable", "require": { "firebase/php-jwt": "^6.3", - "psr/log": "^3.0", + "psr/log": "^1.1.4", "collizo4sky/persist-admin-notices-dismissal": "^1.4" }, "require-dev": { diff --git a/tests/src/AccessTokenTest.php b/tests/src/AccessTokenTest.php index c7540bf..a762b92 100644 --- a/tests/src/AccessTokenTest.php +++ b/tests/src/AccessTokenTest.php @@ -59,22 +59,26 @@ public function testTokenWithEditedTokenType( string $token ): void { explode( '.', $token )[2], ] ); - $request_data = array(); + $request_options = array(); if ( $this->flow === 'cookie' ) { $cookies = [ 'refresh_token' => $malicious_token, ]; - $domain = $this->client->getConfig( 'base_uri' )->getHost(); + $domain = $this->getDomain(); $cookies = CookieJar::fromArray( $cookies, $domain ); - $request_data['cookies'] = $cookies; + $request_options['cookies'] = $cookies; + } else if ($this->flow === 'body') { + $request_options[\GuzzleHttp\RequestOptions::JSON] = [ + 'refresh_token' => $token, + ]; } else { - $request_data['form_params'] = [ - 'refresh_token' => $malicious_token, + $request_options['form_params'] = [ + 'refresh_token' => $token, ]; } - $response = $this->client->post( '/wp-json/jwt-auth/v1/token/refresh', $request_data ); + $response = $this->client->post( '/wp-json/jwt-auth/v1/token/refresh', $request_options ); $body = json_decode( $response->getBody()->getContents(), true ); $this->assertIsArray( $body ); $this->assertArrayHasKey( 'data', $body ); @@ -140,7 +144,7 @@ public function testTokenRefreshWithInvalidToken( string $token ): void { $this->assertEquals( 401, $response->getStatusCode() ); $this->assertEquals( false, $body['success'] ); - $request_data = array(); + $request_options = array(); if ( $this->flow === 'cookie' ) { $cookies = [ @@ -148,13 +152,17 @@ public function testTokenRefreshWithInvalidToken( string $token ): void { ]; $domain = $this->getDomain(); $cookies = CookieJar::fromArray( $cookies, $domain ); - $request_data['cookies'] = $cookies; + $request_options['cookies'] = $cookies; + } else if ($this->flow === 'body') { + $request_options[\GuzzleHttp\RequestOptions::JSON] = [ + 'refresh_token' => $token, + ]; } else { - $request_data['form_params'] = [ + $request_options['form_params'] = [ 'refresh_token' => $token, ]; } - $response = $this->client->post( '/wp-json/jwt-auth/v1/token/refresh', $request_data ); + $response = $this->client->post( '/wp-json/jwt-auth/v1/token/refresh', $request_options ); $body = json_decode( $response->getBody()->getContents(), true ); $this->assertEquals( 'jwt_auth_invalid_refresh_token', $body['code'] ); $this->assertEquals( 401, $response->getStatusCode() ); @@ -168,7 +176,7 @@ public function testTokenRefreshWithInvalidToken( string $token ): void { public function testTokenWithInvalidRefreshToken( string $token ): void { $this->assertNotEmpty( $token ); - $request_data = array(); + $request_options = array(); if ( $this->flow === 'cookie' ) { $cookies = [ @@ -176,13 +184,17 @@ public function testTokenWithInvalidRefreshToken( string $token ): void { ]; $domain = $this->getDomain(); $cookies = CookieJar::fromArray( $cookies, $domain ); - $request_data['cookies'] = $cookies; + $request_options['cookies'] = $cookies; + } else if ($this->flow === 'body') { + $request_options[\GuzzleHttp\RequestOptions::JSON] = [ + 'refresh_token' => $token, + ]; } else { - $request_data['form_params'] = [ + $request_options['form_params'] = [ 'refresh_token' => $token, ]; } - $response = $this->client->post( '/wp-json/jwt-auth/v1/token', $request_data ); + $response = $this->client->post( '/wp-json/jwt-auth/v1/token', $request_options ); $body = json_decode( $response->getBody()->getContents(), true ); $this->assertEquals( 'jwt_auth_invalid_refresh_token', $body['code'] ); $this->assertEquals( 401, $response->getStatusCode() ); diff --git a/tests/src/RefreshTokenTest.php b/tests/src/RefreshTokenTest.php index 413eecb..edb603b 100644 --- a/tests/src/RefreshTokenTest.php +++ b/tests/src/RefreshTokenTest.php @@ -106,7 +106,7 @@ public function testTokenValidateWithRefreshToken( string $refreshToken ): void public function testTokenWithRefreshToken( string $refreshToken ): void { $this->assertNotEmpty( $refreshToken ); - $request_data = array(); + $request_options = array(); if ( $this->flow === 'cookie' ) { $cookies = [ @@ -115,15 +115,19 @@ public function testTokenWithRefreshToken( string $refreshToken ): void { $domain = $this->getDomain(); $cookies = CookieJar::fromArray( $cookies, $domain ); - $request_data['cookies'] = $cookies; + $request_options['cookies'] = $cookies; + } elseif ($this->flow === 'body') { + $request_options[\GuzzleHttp\RequestOptions::JSON] = [ + 'refresh_token' => $refreshToken, + ]; } else { - $request_data['form_params'] = [ + $request_options['form_params'] = [ 'refresh_token' => $refreshToken, ]; } - $response = $this->client->post( '/wp-json/jwt-auth/v1/token', $request_data ); + $response = $this->client->post( '/wp-json/jwt-auth/v1/token', $request_options ); $body = json_decode( $response->getBody()->getContents(), true ); $this->assertEquals( 'jwt_auth_valid_credential', $body['code'] ); @@ -159,7 +163,7 @@ public function testTokenWithRefreshToken( string $refreshToken ): void { public function testTokenWithInvalidRefreshToken( string $refreshToken ): void { $this->assertNotEmpty( $refreshToken ); - $request_data = array(); + $request_options = array(); if ( $this->flow === 'cookie' ) { @@ -169,18 +173,20 @@ public function testTokenWithInvalidRefreshToken( string $refreshToken ): void { $domain = $this->getDomain(); $cookies = CookieJar::fromArray( $cookies, $domain ); - $request_data['cookies'] = $cookies; + $request_options['cookies'] = $cookies; + } elseif ($this->flow === 'body') { + $request_options[\GuzzleHttp\RequestOptions::JSON] = [ + 'refresh_token' => $refreshToken . '123', + ]; } else { - - $request_data['form_params'] = [ + $request_options['form_params'] = [ 'refresh_token' => $refreshToken . '123', ]; - } - $response = $this->client->post( '/wp-json/jwt-auth/v1/token', $request_data ); + $response = $this->client->post( '/wp-json/jwt-auth/v1/token', $request_options ); $body = json_decode( $response->getBody()->getContents(), true ); - $this->assertEquals( 'jwt_auth_obsolete_refresh_token', $body['code'] ); + $this->assertEquals( 'jwt_auth_invalid_refresh_token', $body['code'] ); $this->assertEquals( 401, $response->getStatusCode() ); $this->assertEquals( false, $body['success'] ); } @@ -195,7 +201,7 @@ public function testTokenRefresh( string $refreshToken ): string { // Wait 1 seconds as the token creation is based on timestamp in seconds. sleep( 1 ); - $request_data = array(); + $request_options = array(); if ( $this->flow === 'cookie' ) { $cookies = [ @@ -204,14 +210,18 @@ public function testTokenRefresh( string $refreshToken ): string { $domain = $this->getDomain(); $cookies = CookieJar::fromArray( $cookies, $domain ); - $request_data['cookies'] = $cookies; + $request_options['cookies'] = $cookies; + } elseif ($this->flow === 'body') { + $request_options[\GuzzleHttp\RequestOptions::JSON] = [ + 'refresh_token' => $refreshToken, + ]; } else { - $request_data['form_params'] = [ + $request_options['form_params'] = [ 'refresh_token' => $refreshToken, ]; } - $response = $this->client->post( '/wp-json/jwt-auth/v1/token/refresh', $request_data ); + $response = $this->client->post( '/wp-json/jwt-auth/v1/token/refresh', $request_options ); $body = json_decode( $response->getBody()->getContents(), true ); $this->assertEquals( 'jwt_auth_valid_token', $body['code'] ); $this->assertEquals( 200, $response->getStatusCode() ); @@ -250,7 +260,7 @@ public function testTokenWithRotatedRefreshToken(): void { // Wait 1 seconds as the token creation is based on timestamp in seconds. sleep( 1 ); - $request_data = array(); + $request_options = array(); if ( $this->flow === 'cookie' ) { $domain = $this->getDomain(); @@ -258,13 +268,17 @@ public function testTokenWithRotatedRefreshToken(): void { // Fetch a new refresh token. $this->cookies->clear(); $this->setCookie( 'refresh_token', $refreshToken1, $domain ); + } elseif ($this->flow === 'body') { + $request_options[\GuzzleHttp\RequestOptions::JSON] = [ + 'refresh_token' => $refreshToken1, + ]; } else { - $request_data['form_params'] = [ + $request_options['form_params'] = [ 'refresh_token' => $refreshToken1, ]; } - $response = $this->client->post( '/wp-json/jwt-auth/v1/token/refresh', $request_data ); + $response = $this->client->post( '/wp-json/jwt-auth/v1/token/refresh', $request_options ); $body = json_decode( $response->getBody()->getContents(), true ); $this->assertEquals( 'jwt_auth_valid_token', $body['code'] ); $this->assertEquals( 200, $response->getStatusCode() ); @@ -291,16 +305,22 @@ public function testTokenWithRotatedRefreshToken(): void { $this->assertNotEquals( $refreshToken2, $refreshToken1 ); if ( $this->flow === 'cookie' ) { + $domain = $this->getDomain(); + // Confirm the rotated refresh token is valid. $this->cookies->clear(); $this->setCookie( 'refresh_token', $refreshToken2, $domain ); + } elseif ($this->flow === 'body') { + $request_options[\GuzzleHttp\RequestOptions::JSON] = [ + 'refresh_token' => $refreshToken2, + ]; } else { - $request_data['form_params'] = [ + $request_options['form_params'] = [ 'refresh_token' => $refreshToken2, ]; } - $response = $this->client->post( '/wp-json/jwt-auth/v1/token', $request_data ); + $response = $this->client->post( '/wp-json/jwt-auth/v1/token', $request_options ); $body = json_decode( $response->getBody()->getContents(), true ); $this->assertEquals( 'jwt_auth_valid_credential', $body['code'] ); $this->assertEquals( 200, $response->getStatusCode() ); @@ -313,6 +333,8 @@ public function testTokenWithRotatedRefreshToken(): void { $this->assertNotEquals( $this->token, $refreshToken2 ); if ( $this->flow === 'cookie' ) { + $domain = $this->getDomain(); + // Discard the refresh_token cookie we set above to only retain the // refresh_token cookie from the response. $this->cookies->clearSessionCookies(); @@ -323,12 +345,16 @@ public function testTokenWithRotatedRefreshToken(): void { // Confirm the previous refresh token is no longer valid. $this->cookies->clear(); $this->setCookie( 'refresh_token', $refreshToken1, $domain ); + } elseif ($this->flow === 'body') { + $request_options[\GuzzleHttp\RequestOptions::JSON] = [ + 'refresh_token' => $refreshToken1, + ]; } else { - $request_data['form_params'] = [ + $request_options['form_params'] = [ 'refresh_token' => $refreshToken1, ]; } - $response = $this->client->post( '/wp-json/jwt-auth/v1/token', $request_data ); + $response = $this->client->post( '/wp-json/jwt-auth/v1/token', $request_options ); $body = json_decode( $response->getBody()->getContents(), true ); $this->assertEquals( 'jwt_auth_obsolete_refresh_token', $body['code'], $body['message'] ); $this->assertEquals( 401, $response->getStatusCode() ); @@ -388,19 +414,23 @@ public function testTokenRefreshRotationByDevice() { for ( $i = 1; $i <= count( $devices ); $i ++ ) { $initial_refresh_token = $devices[ $i ]['refresh_token']; - $request_data = array(); + $request_options = array(); if ( $this->flow === 'cookie' ) { - $request_data['form_params'] = [ + $request_options['form_params'] = [ 'device' => $devices[ $i ]['device'], ]; - $this->setCookie( 'refresh_token', $devices[ $i ]['refresh_token'], $domain ); + $this->setCookie( 'refresh_token', $initial_refresh_token, $domain ); + } elseif ($this->flow === 'body') { + $request_options[\GuzzleHttp\RequestOptions::JSON] = [ + 'refresh_token' => $initial_refresh_token, + ]; } else { - $request_data['form_params'] = [ - 'refresh_token' => $devices[ $i ]['refresh_token'], + $request_options['form_params'] = [ + 'refresh_token' => $initial_refresh_token, ]; } - $response = $this->client->post( '/wp-json/jwt-auth/v1/token/refresh', $request_data ); + $response = $this->client->post( '/wp-json/jwt-auth/v1/token/refresh', $request_options ); $body = json_decode( $response->getBody()->getContents(), true ); $this->assertEquals( 'jwt_auth_valid_token', $body['code'] ); @@ -428,15 +458,19 @@ public function testTokenRefreshRotationByDevice() { // Confirm each device can use its refresh token to authenticate. for ( $i = 1; $i <= count( $devices ); $i ++ ) { - $request_data = array(); + $request_options = array(); if ( $this->flow === 'cookie' ) { $this->setCookie( 'refresh_token', $devices[ $i ]['refresh_token'], $domain ); + } elseif ($this->flow === 'body') { + $request_options[\GuzzleHttp\RequestOptions::JSON] = [ + 'refresh_token' => $devices[ $i ]['refresh_token'], + ]; } else { - $request_data['form_params'] = [ + $request_options['form_params'] = [ 'refresh_token' => $devices[ $i ]['refresh_token'], ]; } - $response = $this->client->post( '/wp-json/jwt-auth/v1/token', $request_data ); + $response = $this->client->post( '/wp-json/jwt-auth/v1/token', $request_options ); $body = json_decode( $response->getBody()->getContents(), true ); $this->assertEquals( 'jwt_auth_valid_credential', $body['code'] ); $this->assertArrayHasKey( 'data', $body ); @@ -449,17 +483,21 @@ public function testTokenRefreshRotationByDevice() { } } - $request_data = array(); + $request_options = array(); // Confirm the previous refresh token is no longer valid. if ( $this->flow === 'cookie' ) { $this->setCookie( 'refresh_token', $initial_refresh_token, $domain ); + } elseif ($this->flow === 'body') { + $request_options[\GuzzleHttp\RequestOptions::JSON] = [ + 'refresh_token' => $initial_refresh_token, + ]; } else { - $request_data['form_params'] = [ + $request_options['form_params'] = [ 'refresh_token' => $initial_refresh_token, ]; } - $response = $this->client->post( '/wp-json/jwt-auth/v1/token', $request_data ); + $response = $this->client->post( '/wp-json/jwt-auth/v1/token', $request_options ); $this->assertEquals( 401, $response->getStatusCode() ); $body = json_decode( $response->getBody()->getContents(), true ); $this->assertEquals( 'jwt_auth_obsolete_refresh_token', $body['code'] ); @@ -487,21 +525,25 @@ public function testTokenRefreshWithInvalidRefreshToken( string $refreshToken ): $this->assertEquals( 401, $response->getStatusCode() ); $this->assertEquals( false, $body['success'] ); - $request_data = array(); + $request_options = array(); if ( $this->flow === 'cookie' ) { $cookies = [ 'refresh_token' => $refreshToken, ]; $domain = $this->getDomain(); $cookies = CookieJar::fromArray( $cookies, $domain ); - $request_data['cookies'] = $cookies; + $request_options['cookies'] = $cookies; + } elseif ($this->flow === 'body') { + $request_options[\GuzzleHttp\RequestOptions::JSON] = [ + 'refresh_token' => $refreshToken, + ]; } else { - $request_data['form_params'] = [ + $request_options['form_params'] = [ 'refresh_token' => $refreshToken, ]; } - $response = $this->client->post( '/wp-json/jwt-auth/v1/token/refresh', $request_data ); + $response = $this->client->post( '/wp-json/jwt-auth/v1/token/refresh', $request_options ); $body = json_decode( $response->getBody()->getContents(), true ); $this->assertEquals( 'jwt_auth_obsolete_refresh_token', $body['code'] ); $this->assertEquals( 401, $response->getStatusCode() ); From 4b5cc49297d488d527c803015f043eba8abeb9cb Mon Sep 17 00:00:00 2001 From: Matteo Gaggiano Date: Tue, 30 Jul 2024 10:03:30 +0200 Subject: [PATCH 23/29] feat: add method to check if a flow is enabled (possibility to implementa a multi-flow support) --- class-auth.php | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/class-auth.php b/class-auth.php index e4b1950..4f3bb18 100644 --- a/class-auth.php +++ b/class-auth.php @@ -314,11 +314,10 @@ public function send_refresh_token( \WP_User $user, string $device = '' ): strin $refresh_token = $this->generate_refresh_token( $user, $device ); $alg = $this->get_alg(); - $flow = $this->get_flow(); $payload = JWT::decode( $refresh_token, new Key( $secret_key, $alg ) ); - if ( 'cookie' === $flow ) { + if ( $this->is_flow( 'cookie' ) ) { // Send the refresh token as a HttpOnly cookie in the response. setcookie( 'refresh_token', $refresh_token, $payload->exp, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true ); } @@ -398,6 +397,17 @@ public function get_flow() { return apply_filters( 'jwt_auth_flow', 'cookie' ); } + /** + * Check if the current flow is one of the given flows. + * + * @param string ...$desired The desired flows. + * + * @return bool + */ + public function is_flow(...$desired) { + return in_array($this->get_flow(), $desired, true); + } + /** * Get the token issuer. * @@ -643,11 +653,9 @@ public function refresh_token( \WP_REST_Request $request ) { $device = $payload->data->device; $refresh_token = $this->send_refresh_token( $user, $device ); - $flow = $this->get_flow(); - $additional_fields = array(); - if ( $flow !== 'cookie' ) { + if ( ! $this->is_flow( 'cookie' ) ) { $additional_fields = array( 'data' => array( 'refresh_token' => $refresh_token, From fcf74314ff884ddb303ec6210c977982ee379e81 Mon Sep 17 00:00:00 2001 From: Matteo Gaggiano Date: Tue, 30 Jul 2024 10:03:45 +0200 Subject: [PATCH 24/29] chore: update composer.lock --- composer.lock | 366 +++++++++++++++++++++++++++++--------------------- 1 file changed, 214 insertions(+), 152 deletions(-) diff --git a/composer.lock b/composer.lock index f5d87fb..fb88f00 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "9cbcd0e3f958a25fe379a4c9f9e226e0", + "content-hash": "36baf9ffc0a30ae565209d739dc65a2c", "packages": [ { "name": "collizo4sky/persist-admin-notices-dismissal", @@ -41,34 +41,39 @@ } ], "description": "Simple library to persist dismissal of admin notices across pages in WordPress dashboard.", + "support": { + "issues": "https://github.com/w3guy/persist-admin-notices-dismissal/issues", + "source": "https://github.com/w3guy/persist-admin-notices-dismissal" + }, "time": "2024-03-10T15:11:42+00:00" }, { "name": "firebase/php-jwt", - "version": "v6.3.2", + "version": "v6.10.0", "source": { "type": "git", "url": "https://github.com/firebase/php-jwt.git", - "reference": "ea7dda77098b96e666c5ef382452f94841e439cd" + "reference": "a49db6f0a5033aef5143295342f1c95521b075ff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/firebase/php-jwt/zipball/ea7dda77098b96e666c5ef382452f94841e439cd", - "reference": "ea7dda77098b96e666c5ef382452f94841e439cd", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/a49db6f0a5033aef5143295342f1c95521b075ff", + "reference": "a49db6f0a5033aef5143295342f1c95521b075ff", "shasum": "" }, "require": { - "php": "^7.1||^8.0" + "php": "^7.4||^8.0" }, "require-dev": { "guzzlehttp/guzzle": "^6.5||^7.4", - "phpspec/prophecy-phpunit": "^1.1", - "phpunit/phpunit": "^7.5||^9.5", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", "psr/cache": "^1.0||^2.0", "psr/http-client": "^1.0", "psr/http-factory": "^1.0" }, "suggest": { + "ext-sodium": "Support EdDSA (Ed25519) signatures", "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" }, "type": "library", @@ -101,38 +106,88 @@ ], "support": { "issues": "https://github.com/firebase/php-jwt/issues", - "source": "https://github.com/firebase/php-jwt/tree/v6.3.2" + "source": "https://github.com/firebase/php-jwt/tree/v6.10.0" + }, + "time": "2023-12-01T16:26:39+00:00" + }, + { + "name": "psr/log", + "version": "1.1.4", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } }, - "time": "2022-12-19T17:10:46+00:00" + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/1.1.4" + }, + "time": "2021-05-03T11:20:27+00:00" } ], "packages-dev": [ { "name": "doctrine/instantiator", - "version": "2.0.0", + "version": "1.5.0", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0" + "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", - "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b", + "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b", "shasum": "" }, "require": { - "php": "^8.1" + "php": "^7.1 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^11", + "doctrine/coding-standard": "^9 || ^11", "ext-pdo": "*", "ext-phar": "*", - "phpbench/phpbench": "^1.2", - "phpstan/phpstan": "^1.9.4", - "phpstan/phpstan-phpunit": "^1.3", - "phpunit/phpunit": "^9.5.27", - "vimeo/psalm": "^5.4" + "phpbench/phpbench": "^0.16 || ^1", + "phpstan/phpstan": "^1.4", + "phpstan/phpstan-phpunit": "^1", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "vimeo/psalm": "^4.30 || ^5.4" }, "type": "library", "autoload": { @@ -159,7 +214,7 @@ ], "support": { "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/2.0.0" + "source": "https://github.com/doctrine/instantiator/tree/1.5.0" }, "funding": [ { @@ -175,26 +230,26 @@ "type": "tidelift" } ], - "time": "2022-12-30T00:23:10+00:00" + "time": "2022-12-30T00:15:36+00:00" }, { "name": "guzzlehttp/guzzle", - "version": "7.8.1", + "version": "7.9.2", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "41042bc7ab002487b876a0683fc8dce04ddce104" + "reference": "d281ed313b989f213357e3be1a179f02196ac99b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/41042bc7ab002487b876a0683fc8dce04ddce104", - "reference": "41042bc7ab002487b876a0683fc8dce04ddce104", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/d281ed313b989f213357e3be1a179f02196ac99b", + "reference": "d281ed313b989f213357e3be1a179f02196ac99b", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^1.5.3 || ^2.0.1", - "guzzlehttp/psr7": "^1.9.1 || ^2.5.1", + "guzzlehttp/promises": "^1.5.3 || ^2.0.3", + "guzzlehttp/psr7": "^2.7.0", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0", "symfony/deprecation-contracts": "^2.2 || ^3.0" @@ -205,9 +260,9 @@ "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", "ext-curl": "*", - "php-http/client-integration-tests": "dev-master#2c025848417c1135031fdf9c728ee53d0a7ceaee as 3.0.999", + "guzzle/client-integration-tests": "3.0.2", "php-http/message-factory": "^1.1", - "phpunit/phpunit": "^8.5.36 || ^9.6.15", + "phpunit/phpunit": "^8.5.39 || ^9.6.20", "psr/log": "^1.1 || ^2.0 || ^3.0" }, "suggest": { @@ -285,7 +340,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.8.1" + "source": "https://github.com/guzzle/guzzle/tree/7.9.2" }, "funding": [ { @@ -301,20 +356,20 @@ "type": "tidelift" } ], - "time": "2023-12-03T20:35:24+00:00" + "time": "2024-07-24T11:22:20+00:00" }, { "name": "guzzlehttp/promises", - "version": "2.0.2", + "version": "2.0.3", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "bbff78d96034045e58e13dedd6ad91b5d1253223" + "reference": "6ea8dd08867a2a42619d65c3deb2c0fcbf81c8f8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/bbff78d96034045e58e13dedd6ad91b5d1253223", - "reference": "bbff78d96034045e58e13dedd6ad91b5d1253223", + "url": "https://api.github.com/repos/guzzle/promises/zipball/6ea8dd08867a2a42619d65c3deb2c0fcbf81c8f8", + "reference": "6ea8dd08867a2a42619d65c3deb2c0fcbf81c8f8", "shasum": "" }, "require": { @@ -322,7 +377,7 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.36 || ^9.6.15" + "phpunit/phpunit": "^8.5.39 || ^9.6.20" }, "type": "library", "extra": { @@ -368,7 +423,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.0.2" + "source": "https://github.com/guzzle/promises/tree/2.0.3" }, "funding": [ { @@ -384,20 +439,20 @@ "type": "tidelift" } ], - "time": "2023-12-03T20:19:20+00:00" + "time": "2024-07-18T10:29:17+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.6.2", + "version": "2.7.0", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221" + "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/45b30f99ac27b5ca93cb4831afe16285f57b8221", - "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/a70f5c95fb43bc83f07c9c948baa0dc1829bf201", + "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201", "shasum": "" }, "require": { @@ -412,8 +467,8 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "http-interop/http-factory-tests": "^0.9", - "phpunit/phpunit": "^8.5.36 || ^9.6.15" + "http-interop/http-factory-tests": "0.9.0", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" }, "suggest": { "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" @@ -484,7 +539,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.6.2" + "source": "https://github.com/guzzle/psr7/tree/2.7.0" }, "funding": [ { @@ -500,20 +555,20 @@ "type": "tidelift" } ], - "time": "2023-12-03T20:05:35+00:00" + "time": "2024-07-18T11:15:46+00:00" }, { "name": "myclabs/deep-copy", - "version": "1.11.1", + "version": "1.12.0", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" + "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", - "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", + "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", "shasum": "" }, "require": { @@ -521,11 +576,12 @@ }, "conflict": { "doctrine/collections": "<1.6.8", - "doctrine/common": "<2.13.3 || >=3,<3.2.2" + "doctrine/common": "<2.13.3 || >=3 <3.2.2" }, "require-dev": { "doctrine/collections": "^1.6.8", "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" }, "type": "library", @@ -551,7 +607,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" + "source": "https://github.com/myclabs/DeepCopy/tree/1.12.0" }, "funding": [ { @@ -559,20 +615,20 @@ "type": "tidelift" } ], - "time": "2023-03-08T13:26:56+00:00" + "time": "2024-06-12T14:39:25+00:00" }, { "name": "nikic/php-parser", - "version": "v5.0.0", + "version": "v5.1.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "4a21235f7e56e713259a6f76bf4b5ea08502b9dc" + "reference": "683130c2ff8c2739f4822ff7ac5c873ec529abd1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/4a21235f7e56e713259a6f76bf4b5ea08502b9dc", - "reference": "4a21235f7e56e713259a6f76bf4b5ea08502b9dc", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/683130c2ff8c2739f4822ff7ac5c873ec529abd1", + "reference": "683130c2ff8c2739f4822ff7ac5c873ec529abd1", "shasum": "" }, "require": { @@ -583,7 +639,7 @@ }, "require-dev": { "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" + "phpunit/phpunit": "^9.0" }, "bin": [ "bin/php-parse" @@ -615,26 +671,27 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.0.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.1.0" }, - "time": "2024-01-07T17:17:35+00:00" + "time": "2024-07-01T20:03:41+00:00" }, { "name": "phar-io/manifest", - "version": "2.0.3", + "version": "2.0.4", "source": { "type": "git", "url": "https://github.com/phar-io/manifest.git", - "reference": "97803eca37d319dfa7826cc2437fc020857acb53" + "reference": "54750ef60c58e43759730615a392c31c80e23176" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53", - "reference": "97803eca37d319dfa7826cc2437fc020857acb53", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", "shasum": "" }, "require": { "ext-dom": "*", + "ext-libxml": "*", "ext-phar": "*", "ext-xmlwriter": "*", "phar-io/version": "^3.0.1", @@ -675,9 +732,15 @@ "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", "support": { "issues": "https://github.com/phar-io/manifest/issues", - "source": "https://github.com/phar-io/manifest/tree/2.0.3" + "source": "https://github.com/phar-io/manifest/tree/2.0.4" }, - "time": "2021-07-20T11:28:43+00:00" + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" }, { "name": "phar-io/version", @@ -732,16 +795,16 @@ }, { "name": "phpunit/php-code-coverage", - "version": "9.2.30", + "version": "9.2.31", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "ca2bd87d2f9215904682a9cb9bb37dda98e76089" + "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ca2bd87d2f9215904682a9cb9bb37dda98e76089", - "reference": "ca2bd87d2f9215904682a9cb9bb37dda98e76089", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/48c34b5d8d983006bd2adc2d0de92963b9155965", + "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965", "shasum": "" }, "require": { @@ -798,7 +861,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.30" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.31" }, "funding": [ { @@ -806,7 +869,7 @@ "type": "github" } ], - "time": "2023-12-22T06:47:57+00:00" + "time": "2024-03-02T06:37:42+00:00" }, { "name": "phpunit/php-file-iterator", @@ -1051,45 +1114,45 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.16", + "version": "9.6.20", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "3767b2c56ce02d01e3491046f33466a1ae60a37f" + "reference": "49d7820565836236411f5dc002d16dd689cde42f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3767b2c56ce02d01e3491046f33466a1ae60a37f", - "reference": "3767b2c56ce02d01e3491046f33466a1ae60a37f", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/49d7820565836236411f5dc002d16dd689cde42f", + "reference": "49d7820565836236411f5dc002d16dd689cde42f", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.3.1 || ^2", + "doctrine/instantiator": "^1.5.0 || ^2", "ext-dom": "*", "ext-json": "*", "ext-libxml": "*", "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.10.1", - "phar-io/manifest": "^2.0.3", - "phar-io/version": "^3.0.2", + "myclabs/deep-copy": "^1.12.0", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", "php": ">=7.3", - "phpunit/php-code-coverage": "^9.2.28", - "phpunit/php-file-iterator": "^3.0.5", + "phpunit/php-code-coverage": "^9.2.31", + "phpunit/php-file-iterator": "^3.0.6", "phpunit/php-invoker": "^3.1.1", - "phpunit/php-text-template": "^2.0.3", - "phpunit/php-timer": "^5.0.2", - "sebastian/cli-parser": "^1.0.1", - "sebastian/code-unit": "^1.0.6", + "phpunit/php-text-template": "^2.0.4", + "phpunit/php-timer": "^5.0.3", + "sebastian/cli-parser": "^1.0.2", + "sebastian/code-unit": "^1.0.8", "sebastian/comparator": "^4.0.8", - "sebastian/diff": "^4.0.3", - "sebastian/environment": "^5.1.3", - "sebastian/exporter": "^4.0.5", - "sebastian/global-state": "^5.0.1", - "sebastian/object-enumerator": "^4.0.3", - "sebastian/resource-operations": "^3.0.3", - "sebastian/type": "^3.2", + "sebastian/diff": "^4.0.6", + "sebastian/environment": "^5.1.5", + "sebastian/exporter": "^4.0.6", + "sebastian/global-state": "^5.0.7", + "sebastian/object-enumerator": "^4.0.4", + "sebastian/resource-operations": "^3.0.4", + "sebastian/type": "^3.2.1", "sebastian/version": "^3.0.2" }, "suggest": { @@ -1134,7 +1197,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.16" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.20" }, "funding": [ { @@ -1150,7 +1213,7 @@ "type": "tidelift" } ], - "time": "2024-01-19T07:03:14+00:00" + "time": "2024-07-10T11:45:39+00:00" }, { "name": "psr/http-client", @@ -1206,20 +1269,20 @@ }, { "name": "psr/http-factory", - "version": "1.0.2", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/php-fig/http-factory.git", - "reference": "e616d01114759c4c489f93b099585439f795fe35" + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-factory/zipball/e616d01114759c4c489f93b099585439f795fe35", - "reference": "e616d01114759c4c489f93b099585439f795fe35", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", "shasum": "" }, "require": { - "php": ">=7.0.0", + "php": ">=7.1", "psr/http-message": "^1.0 || ^2.0" }, "type": "library", @@ -1243,7 +1306,7 @@ "homepage": "https://www.php-fig.org/" } ], - "description": "Common interfaces for PSR-7 HTTP message factories", + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", "keywords": [ "factory", "http", @@ -1255,9 +1318,9 @@ "response" ], "support": { - "source": "https://github.com/php-fig/http-factory/tree/1.0.2" + "source": "https://github.com/php-fig/http-factory" }, - "time": "2023-04-10T20:10:41+00:00" + "time": "2024-04-15T12:06:14+00:00" }, { "name": "psr/http-message", @@ -1358,16 +1421,16 @@ }, { "name": "sebastian/cli-parser", - "version": "1.0.1", + "version": "1.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2" + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2", - "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b", "shasum": "" }, "require": { @@ -1402,7 +1465,7 @@ "homepage": "https://github.com/sebastianbergmann/cli-parser", "support": { "issues": "https://github.com/sebastianbergmann/cli-parser/issues", - "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.1" + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2" }, "funding": [ { @@ -1410,7 +1473,7 @@ "type": "github" } ], - "time": "2020-09-28T06:08:49+00:00" + "time": "2024-03-02T06:27:43+00:00" }, { "name": "sebastian/code-unit", @@ -1656,16 +1719,16 @@ }, { "name": "sebastian/diff", - "version": "4.0.5", + "version": "4.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131" + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/74be17022044ebaaecfdf0c5cd504fc9cd5a7131", - "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", "shasum": "" }, "require": { @@ -1710,7 +1773,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/4.0.5" + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6" }, "funding": [ { @@ -1718,7 +1781,7 @@ "type": "github" } ], - "time": "2023-05-07T05:35:17+00:00" + "time": "2024-03-02T06:30:58+00:00" }, { "name": "sebastian/environment", @@ -1785,16 +1848,16 @@ }, { "name": "sebastian/exporter", - "version": "4.0.5", + "version": "4.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d" + "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", - "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72", + "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72", "shasum": "" }, "require": { @@ -1850,7 +1913,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.5" + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6" }, "funding": [ { @@ -1858,20 +1921,20 @@ "type": "github" } ], - "time": "2022-09-14T06:03:37+00:00" + "time": "2024-03-02T06:33:00+00:00" }, { "name": "sebastian/global-state", - "version": "5.0.6", + "version": "5.0.7", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "bde739e7565280bda77be70044ac1047bc007e34" + "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bde739e7565280bda77be70044ac1047bc007e34", - "reference": "bde739e7565280bda77be70044ac1047bc007e34", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", "shasum": "" }, "require": { @@ -1914,7 +1977,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.6" + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.7" }, "funding": [ { @@ -1922,7 +1985,7 @@ "type": "github" } ], - "time": "2023-08-02T09:26:13+00:00" + "time": "2024-03-02T06:35:11+00:00" }, { "name": "sebastian/lines-of-code", @@ -2158,16 +2221,16 @@ }, { "name": "sebastian/resource-operations", - "version": "3.0.3", + "version": "3.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8" + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", - "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e", "shasum": "" }, "require": { @@ -2179,7 +2242,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-main": "3.0-dev" } }, "autoload": { @@ -2200,8 +2263,7 @@ "description": "Provides a list of PHP built-in functions that operate on resources", "homepage": "https://www.github.com/sebastianbergmann/resource-operations", "support": { - "issues": "https://github.com/sebastianbergmann/resource-operations/issues", - "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.3" + "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4" }, "funding": [ { @@ -2209,7 +2271,7 @@ "type": "github" } ], - "time": "2020-09-28T06:45:17+00:00" + "time": "2024-03-14T16:00:52+00:00" }, { "name": "sebastian/type", @@ -2322,25 +2384,25 @@ }, { "name": "symfony/deprecation-contracts", - "version": "v3.4.0", + "version": "v2.5.3", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf" + "reference": "80d075412b557d41002320b96a096ca65aa2c98d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/7c3aff79d10325257a001fcf92d991f24fc967cf", - "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/80d075412b557d41002320b96a096ca65aa2c98d", + "reference": "80d075412b557d41002320b96a096ca65aa2c98d", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=7.1" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "3.4-dev" + "dev-main": "2.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -2369,7 +2431,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.4.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.3" }, "funding": [ { @@ -2385,20 +2447,20 @@ "type": "tidelift" } ], - "time": "2023-05-23T14:45:45+00:00" + "time": "2023-01-24T14:02:46+00:00" }, { "name": "theseer/tokenizer", - "version": "1.2.2", + "version": "1.2.3", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96" + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b2ad5003ca10d4ee50a12da31de12a5774ba6b96", - "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", "shasum": "" }, "require": { @@ -2427,7 +2489,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.2" + "source": "https://github.com/theseer/tokenizer/tree/1.2.3" }, "funding": [ { @@ -2435,7 +2497,7 @@ "type": "github" } ], - "time": "2023-11-20T00:12:19+00:00" + "time": "2024-03-03T12:36:25+00:00" } ], "aliases": [], @@ -2449,5 +2511,5 @@ "ext-json": "*", "ext-curl": "*" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.2.0" } From 1e9617fb0025387fb86e3a06b7b75a1ce6e1b896 Mon Sep 17 00:00:00 2001 From: Matteo Gaggiano Date: Mon, 30 Sep 2024 17:29:46 +0200 Subject: [PATCH 25/29] fix(refresh_token): revert all style modification to code fix(refresh_token): revert logger integration (will be present in a different PR) --- class-auth.php | 37 ++--- class-devices.php | 246 +++++++++++++++------------------ class-setup.php | 35 +---- composer.json | 1 - tests/src/RefreshTokenTest.php | 2 +- tests/src/RestTestTrait.php | 98 ++++++------- 6 files changed, 180 insertions(+), 239 deletions(-) diff --git a/class-auth.php b/class-auth.php index 4f3bb18..2043f6d 100644 --- a/class-auth.php +++ b/class-auth.php @@ -8,14 +8,14 @@ namespace JWTAuth; use Exception; -use Firebase\JWT\JWT; -use Firebase\JWT\Key; -use Psr\Log\LoggerInterface; use WP_Error; use WP_REST_Request; use WP_REST_Response; use WP_REST_Server; +use Firebase\JWT\JWT; +use Firebase\JWT\Key; + /** * The public-facing functionality of the plugin. */ @@ -48,27 +48,16 @@ class Auth { */ private $rest_api_slug = 'wp-json'; - /** - * The logger interface to use - * - * @var LoggerInterface - */ - private $logger; - /** * Setup action & filter hooks. - * - * @param LoggerInterface $logger The logger interface to use. */ - public function __construct( LoggerInterface $logger ) { + public function __construct() { $this->namespace = 'jwt-auth/v1'; $this->messages = array( 'jwt_auth_no_auth_header' => __( 'Authorization header not found.', 'jwt-auth' ), 'jwt_auth_bad_auth_header' => __( 'Authorization header malformed.', 'jwt-auth' ), ); - - $this->logger = $logger; } /** @@ -140,7 +129,7 @@ public function add_cors_support() { * * @param string $username The username. * @param string $password The password. - * @param mixed $custom_auth The custom auth data (if any). + * @param mixed $custom_auth The custom auth data (if any). * * @return WP_User|WP_Error $user Returns WP_User object if success, or WP_Error if failed. */ @@ -165,7 +154,6 @@ public function authenticate_user( $username, $password, $custom_auth = '' ) { * Get token by sending POST request to jwt-auth/v1/token. * * @param WP_REST_Request $request The request. - * * @return WP_REST_Response The response. */ public function get_token( WP_REST_Request $request ) { @@ -237,8 +225,8 @@ public function get_token( WP_REST_Request $request ) { /** * Generate access token. * - * @param \WP_User $user The WP_User object. - * @param bool $return_raw Whether or not to return as raw token string. + * @param WP_User $user The WP_User object. + * @param bool $return_raw Whether or not to return as raw token string. * * @return WP_REST_Response|string Return as raw token string or as a formatted WP_REST_Response. */ @@ -432,7 +420,6 @@ public function get_alg() { * Determine if given response is an error response. * * @param mixed $response The response. - * * @return boolean */ public function is_error_response( $response ) { @@ -484,7 +471,7 @@ public function validate_token( $return_response_or_request = true ) { * The HTTP_AUTHORIZATION is present, verify the format. * If the format is wrong return the user. */ - list( $token ) = sscanf( $auth, 'Bearer %s' ); + list($token) = sscanf( $auth, 'Bearer %s' ); if ( ! $token ) { return new WP_REST_Response( @@ -518,7 +505,7 @@ public function validate_token( $return_response_or_request = true ) { // Try to decode the token. try { $alg = $this->get_alg(); - $payload = JWT::decode( $token, new Key( $secret_key, $alg ) ); + $payload = JWT::decode( $token, new Key( $secret_key , $alg )); // The Token is decoded now validate the iss. if ( $payload->iss !== $this->get_iss() ) { @@ -624,7 +611,6 @@ public function validate_token( $return_response_or_request = true ) { * Validates refresh token and generates a new refresh token. * * @param WP_REST_Request $request The request. - * * @return WP_REST_Response Returns WP_REST_Response. */ public function refresh_token( \WP_REST_Request $request ) { @@ -857,7 +843,6 @@ public function validate_refresh_token( $refresh_token, $return_response = true * This is our Middleware to try to authenticate the user according to the token sent. * * @param int|bool $user_id User ID if one has been determined, false otherwise. - * * @return int|bool User ID if one has been determined, false otherwise. */ public function determine_current_user( $user_id ) { @@ -905,8 +890,8 @@ public function determine_current_user( $user_id ) { * Filter to hook the rest_pre_dispatch, if there is an error in the request * send it, if there is no error just continue with the current request. * - * @param mixed $result Can be anything a normal endpoint can return, or null to not hijack the request. - * @param WP_REST_Server $server Server instance. + * @param mixed $result Can be anything a normal endpoint can return, or null to not hijack the request. + * @param WP_REST_Server $server Server instance. * @param WP_REST_Request $request The request. * * @return mixed $result diff --git a/class-devices.php b/class-devices.php index 031bf6c..95a8736 100644 --- a/class-devices.php +++ b/class-devices.php @@ -7,27 +7,16 @@ namespace JWTAuth; -use Psr\Log\LoggerInterface; - /** * Display the devices connected with token and let remove them in user profile page. * Developed by Rodrigo M. Souza https://github.com/pesseba */ class Devices { - /** - * The logger object. - * - * @var LoggerInterface - */ - private $logger; - /** * Setup action & filter hooks. - * - * @param LoggerInterface $logger The logger object. */ - public function __construct( LoggerInterface $logger ) { + public function __construct() { add_action( 'show_user_profile', array( $this, 'custom_user_profile_fields' ) ); add_action( 'edit_user_profile', array( $this, 'custom_user_profile_fields' ) ); @@ -41,14 +30,12 @@ public function __construct( LoggerInterface $logger ) { add_filter( 'jwt_auth_payload', array( $this, 'jwt_auth_payload' ), 10, 2 ); add_filter( 'jwt_auth_extra_token_check', array( $this, 'check_device_and_pass' ), 10, 4 ); - - $this->logger = $logger; } /** * Filter payload to add device and pass. * - * @param array $payload The token's payload. + * @param array $payload The token's payload. * @param WP_User $user The user who owns the token. * * @return array $payload The modified token's payload. @@ -88,10 +75,10 @@ public function jwt_auth_payload( $payload, $user ) { /** * Filter token validation to check device and pass. * - * @param string $error_msg The failed message. + * @param string $error_msg The failed message. * @param WP_User $user The user who owns the token. - * @param string $token The token. - * @param array $payload The token's payload. + * @param string $token The token. + * @param array $payload The token's payload. * * @return string The error message if failed, empty string if it passes. */ @@ -121,7 +108,6 @@ public function check_device_and_pass( $error_msg, $user, $token, $payload ) { * Sanitize the device name. * * @param string $device The device name. - * * @return string The sanitized device name. */ private function sanitize_device_name( $device ) { @@ -203,7 +189,6 @@ private function sanitize_device_name( $device ) { * Sanitize the device key. * * @param string $key The device key. - * * @return string The sanitized device key. */ private function sanitize_device_key( $key ) { @@ -213,11 +198,10 @@ private function sanitize_device_key( $key ) { /** * Fires immediately after an existing user is updated. * - * @param int $user_id User ID. - * @param WP_User $old_user_data Object containing user's data prior to update. - * * @since 2.0.0 * + * @param int $user_id User ID. + * @param WP_User $old_user_data Object containing user's data prior to update. */ public function profile_update( $user_id, $old_user_data ) { @@ -232,11 +216,10 @@ public function profile_update( $user_id, $old_user_data ) { /** * Fires after the user's password is reset. * - * @param WP_User $user The user. - * @param string $new_pass New user password. - * * @since 4.4.0 * + * @param WP_User $user The user. + * @param string $new_pass New user password. */ public function after_password_reset( $user, $new_pass ) { @@ -246,11 +229,11 @@ public function after_password_reset( $user, $new_pass ) { /** * Fires after the user' is created * - * @param int $user_id The user ID. - * - * @since 2.0.0 + * @ssince 2.0.0 * - */ + * @param int $user_id The user ID. + * + */ public function after_user_creation( $user_id ) { $this->refresh_pass( $user_id ); @@ -288,8 +271,8 @@ private function block_all_tokens( $user_id ) { * @param int $user_id The user id. */ private function refresh_pass( $user_id ) { - $pass = (string) md5( uniqid( wp_rand(), true ) ); - if ( ! empty( update_user_meta( $user_id, 'jwt_auth_pass', $pass ) ) ) { + $pass = (string) md5( uniqid( wp_rand(), true )); + if (!empty(update_user_meta( $user_id, 'jwt_auth_pass', $pass ) )){ return $pass; } @@ -303,14 +286,13 @@ private function refresh_pass( $user_id ) { */ public function remove_device() { - $nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] )) : ''; + $nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] )) : ''; $device = isset( $_POST['device'] ) ? sanitize_text_field( $_POST['device'] ) : ''; // phpcs:ignore $user_id = isset( $_POST['user_id'] ) && is_numeric( $_POST['user_id'] ) ? absint( $_POST['user_id'] ) : 0; // phpcs:ignore - if ( ! wp_verify_nonce( $nonce, 'jwt_auth_remove_device_' . $user_id ) ) { + if (!wp_verify_nonce( $nonce, 'jwt_auth_remove_device_'.$user_id)){ wp_send_json_error(); wp_die(); - return; } @@ -376,133 +358,133 @@ public function shortcode_jwt_auth_devices( $atts ) { ob_start(); ?> - + .device_agent { + font-size: 0.5vw; + } + - + }); + } + } + -
+
- 30 ) ? substr( $title, 0, 27 ) . '...' : $title; - $device_data = (array) get_user_meta( $user_id, $this->sanitize_device_key( $device ), true ); - $icon = ( $device_data['is_mobile'] ) ? 'dashicons-smartphone' : 'dashicons-laptop'; - $date = $device_data['date']; - $agent = $device_data['agent']; - $agent = preg_replace( '/(\S{15})(?=\S)/', '$1 ', $agent ); + $device = $devices[ $i ]; + $title = preg_replace( '/(\S{15})(?=\S)/', '$1 ', $device ); + $title = ( strlen( $title ) > 30 ) ? substr( $title, 0, 27 ) . '...' : $title; + $device_data = (array) get_user_meta( $user_id, $this->sanitize_device_key( $device ), true ); + $icon = ( $device_data['is_mobile'] ) ? 'dashicons-smartphone' : 'dashicons-laptop'; + $date = $device_data['date']; + $agent = $device_data['agent']; + $agent = preg_replace( '/(\S{15})(?=\S)/', '$1 ', $agent ); - ?> + ?> -
-
- -

-

-

-

+
+
+ +

+

+

+

- + '" class="button wp-generate-pw' . + '" type="button" value="' . __( 'Remove', 'jwt-auth' ) . + '" onclick="jwt_auth_remove_device(\'' . esc_attr( $user_id ) . '\',\'' . esc_attr( $device ) . '\',\'' . esc_attr( $i ) . '\' )" /> '; - ?> + ?> +
-
- + -
-
+
+
logger = $logger; - - $this->auth = new Auth( $this->logger ); - $this->devices = new Devices( $this->logger ); add_action( 'rest_api_init', array( $this->auth, 'register_rest_routes' ) ); add_filter( 'rest_api_init', array( $this->auth, 'add_cors_support' ) ); @@ -67,7 +46,7 @@ public function __construct() { } - if ( ! wp_next_scheduled( 'jwt_auth_purge_expired_refresh_tokens' ) ) { + if ( ! wp_next_scheduled( 'jwt_auth_purge_expired_refresh_tokens' )) { wp_schedule_event( time(), 'weekly', 'jwt_auth_purge_expired_refresh_tokens' ); } add_action( 'jwt_auth_purge_expired_refresh_tokens', array( $this, 'cron_purge_expired_refresh_tokens' ) ); @@ -86,8 +65,6 @@ public function setup_text_domain() { public function cron_purge_expired_refresh_tokens() { global $wpdb; - $this->logger->notice( "Cron purge expired refresh tokens." ); - // Retain expired refresh tokens for one month for potential debugging. $purge_timestamp = time() - 30 * DAY_IN_SECONDS; @@ -96,7 +73,7 @@ public function cron_purge_expired_refresh_tokens() { AND meta_value <= %d ", $purge_timestamp ) ); - foreach ( $user_ids as $user_id ) { + foreach ($user_ids as $user_id) { $user_refresh_tokens = get_user_meta( $user_id, 'jwt_auth_refresh_tokens', true ); if ( is_array( $user_refresh_tokens ) ) { $expires_next = 0; @@ -112,13 +89,11 @@ public function cron_purge_expired_refresh_tokens() { update_user_meta( $user_id, 'jwt_auth_refresh_tokens', $user_refresh_tokens ); update_user_meta( $user_id, 'jwt_auth_refresh_tokens_expires_next', $expires_next ); } else { - delete_user_meta( $user_id, 'jwt_auth_refresh_tokens' ); - delete_user_meta( $user_id, 'jwt_auth_refresh_tokens_expires_next' ); + delete_user_meta( $user_id, 'jwt_auth_refresh_tokens' ); + delete_user_meta( $user_id, 'jwt_auth_refresh_tokens_expires_next' ); } } } - - $this->logger->notice("Cron purge expired refresh tokens completed."); } } diff --git a/composer.json b/composer.json index 481c440..ca53d94 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,6 @@ "minimum-stability": "stable", "require": { "firebase/php-jwt": "^6.3", - "psr/log": "^1.1.4", "collizo4sky/persist-admin-notices-dismissal": "^1.4" }, "require-dev": { diff --git a/tests/src/RefreshTokenTest.php b/tests/src/RefreshTokenTest.php index edb603b..a91807e 100644 --- a/tests/src/RefreshTokenTest.php +++ b/tests/src/RefreshTokenTest.php @@ -1,4 +1,4 @@ -cookies = new CookieJar(); - $this->httpClientConfig = [ - 'base_uri' => $_ENV['URL'] ?? 'http://localhost', - 'http_errors' => false, - 'cookies' => $this->cookies, - // PHP's cURL library attempts to resolve domains with IPv6, causing a - // long delay on local dev machines, since typically not set up for IPv6. - // @see https://stackoverflow.com/questions/17814925/php-curl-consistently-taking-15s-to-resolve-dns - // @see https://www.php.net/manual/de/function.curl-setopt.php - // @see https://wordpress.org/support/topic/curl-error-28-operation-timed-out-after-5000-milliseconds-with-0-bytes-received/ - // @see https://docs.presscustomizr.com/article/326-how-to-fix-a-curl-error-28-connection-timed-out-in-wordpress - // How to avoid this: - // - Add to /etc/hosts: "::1 example.com" - // - Add to Apache httpd.conf: "Listen ::1" - // - Listen to any IP: "" - 'force_ip_resolve' => 'v4', - 'curl' => [ - CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4, - ], - ]; - if ( in_array( '--debug', $_SERVER['argv'], true ) ) { - $this->httpClientConfig['debug'] = true; - } - $this->client = new Client( $this->httpClientConfig ); - $this->username = $_ENV['USERNAME'] ?? null; - $this->password = $_ENV['PASSWORD'] ?? null; - $this->flow = $_ENV['FLOW']; - } + protected function setUp(): void { + $this->cookies = new CookieJar(); + $this->httpClientConfig = [ + 'base_uri' => $_ENV['URL'] ?? 'http://localhost', + 'http_errors' => false, + 'cookies' => $this->cookies, + // PHP's cURL library attempts to resolve domains with IPv6, causing a + // long delay on local dev machines, since typically not set up for IPv6. + // @see https://stackoverflow.com/questions/17814925/php-curl-consistently-taking-15s-to-resolve-dns + // @see https://www.php.net/manual/de/function.curl-setopt.php + // @see https://wordpress.org/support/topic/curl-error-28-operation-timed-out-after-5000-milliseconds-with-0-bytes-received/ + // @see https://docs.presscustomizr.com/article/326-how-to-fix-a-curl-error-28-connection-timed-out-in-wordpress + // How to avoid this: + // - Add to /etc/hosts: "::1 example.com" + // - Add to Apache httpd.conf: "Listen ::1" + // - Listen to any IP: "" + 'force_ip_resolve' => 'v4', + 'curl' => [ + CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4, + ], + ]; + if ( in_array( '--debug', $_SERVER['argv'], true ) ) { + $this->httpClientConfig['debug'] = true; + } + $this->client = new Client( $this->httpClientConfig ); + $this->username = $_ENV['USERNAME'] ?? null; + $this->password = $_ENV['PASSWORD'] ?? null; + $this->flow = $_ENV['FLOW']; + } - protected function setCookie( $name, $value, $domain ): CookieJar { - $this->cookies->setCookie( new SetCookie( [ - 'Domain' => $domain, - 'Name' => $name, - 'Value' => $value, - 'Discard' => true, - ] ) ); + protected function setCookie( $name, $value, $domain ): CookieJar { + $this->cookies->setCookie( new SetCookie( [ + 'Domain' => $domain, + 'Name' => $name, + 'Value' => $value, + 'Discard' => true, + ] ) ); - return $this->cookies; - } + return $this->cookies; + } - protected function getDomain(): string { - return parse_url( $this->httpClientConfig['base_uri'], PHP_URL_HOST ); - } + protected function getDomain(): string { + return parse_url( $this->httpClientConfig['base_uri'], PHP_URL_HOST ); + } } From 8deeff844275b1a416e3cd92b2e085e3ea216b12 Mon Sep 17 00:00:00 2001 From: Matteo Gaggiano Date: Wed, 2 Oct 2024 12:57:16 +0200 Subject: [PATCH 26/29] fix(refresh_token): revert class-devices.php --- class-devices.php | 181 +++++++++++++++++++++------------------------- 1 file changed, 83 insertions(+), 98 deletions(-) diff --git a/class-devices.php b/class-devices.php index 95a8736..639893f 100644 --- a/class-devices.php +++ b/class-devices.php @@ -27,7 +27,7 @@ public function __construct() { add_action( 'profile_update', array( $this, 'profile_update' ), 10, 2 ); add_action( 'after_password_reset', array( $this, 'after_password_reset' ), 10, 2 ); add_action( 'user_register', array( $this, 'after_user_creation' ), 10, 1 ); - + add_filter( 'jwt_auth_payload', array( $this, 'jwt_auth_payload' ), 10, 2 ); add_filter( 'jwt_auth_extra_token_check', array( $this, 'check_device_and_pass' ), 10, 4 ); } @@ -225,17 +225,16 @@ public function after_password_reset( $user, $new_pass ) { $this->block_all_tokens( $user->ID ); } - + /** * Fires after the user' is created * - * @ssince 2.0.0 + * @since 2.0.0 * - * @param int $user_id The user ID. - * - */ + * @param int $user_id The user ID. + */ public function after_user_creation( $user_id ) { - + $this->refresh_pass( $user_id ); } @@ -272,10 +271,9 @@ private function block_all_tokens( $user_id ) { */ private function refresh_pass( $user_id ) { $pass = (string) md5( uniqid( wp_rand(), true )); - if (!empty(update_user_meta( $user_id, 'jwt_auth_pass', $pass ) )){ + if(!empty(update_user_meta( $user_id, 'jwt_auth_pass', $pass ) )){ return $pass; } - return ''; } @@ -286,11 +284,11 @@ private function refresh_pass( $user_id ) { */ public function remove_device() { - $nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] )) : ''; + $nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] )) : ''; $device = isset( $_POST['device'] ) ? sanitize_text_field( $_POST['device'] ) : ''; // phpcs:ignore $user_id = isset( $_POST['user_id'] ) && is_numeric( $_POST['user_id'] ) ? absint( $_POST['user_id'] ) : 0; // phpcs:ignore - - if (!wp_verify_nonce( $nonce, 'jwt_auth_remove_device_'.$user_id)){ + + if(!wp_verify_nonce( $nonce, 'jwt_auth_remove_device_'.$user_id)){ wp_send_json_error(); wp_die(); return; @@ -358,97 +356,85 @@ public function shortcode_jwt_auth_devices( $atts ) { ob_start(); ?> - + - + }); + } + } + -
+
-
-
- -

-

-

-

+
+
+ +

+

+

+ '" class="button wp-generate-pw' . + '" type="button" value="' . esc_attr( __( 'Remove', 'jwt-auth' )) . + '" onclick="jwt_auth_remove_device(\'' . esc_attr( $user_id ) . '\',\'' . esc_attr( $device ) . '\',\'' . esc_attr( $i ) . '\' )" /> '; ?> -
-
+
+
-
-
+
+
Date: Wed, 2 Oct 2024 12:59:05 +0200 Subject: [PATCH 27/29] fix(refresh_token): revert property initialization in class-setup.php --- class-setup.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/class-setup.php b/class-setup.php index e793000..b783e58 100644 --- a/class-setup.php +++ b/class-setup.php @@ -33,6 +33,8 @@ public static function getInstance() { public function __construct() { add_action( 'init', array( $this, 'setup_text_domain' ) ); + $this->auth = new Auth(); + $this->devices = new Devices(); add_action( 'rest_api_init', array( $this->auth, 'register_rest_routes' ) ); add_filter( 'rest_api_init', array( $this->auth, 'add_cors_support' ) ); From f4ae050950253d48d920eae65237b964c6741d36 Mon Sep 17 00:00:00 2001 From: Matteo Gaggiano Date: Wed, 2 Oct 2024 13:00:36 +0200 Subject: [PATCH 28/29] fix(refresh_token): revert const definition for JWT_AUTH_PLUGIN_VERSION --- jwt-auth.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jwt-auth.php b/jwt-auth.php index 74cd876..5347d56 100644 --- a/jwt-auth.php +++ b/jwt-auth.php @@ -19,7 +19,7 @@ // Helper constants. define( 'JWT_AUTH_PLUGIN_DIR', rtrim( plugin_dir_path( __FILE__ ), '/' ) ); define( 'JWT_AUTH_PLUGIN_URL', rtrim( plugin_dir_url( __FILE__ ), '/' ) ); -define( 'JWT_AUTH_PLUGIN_VERSION', '3.0.2' ); +define( 'JWT_AUTH_PLUGIN_VERSION', '3.0.1' ); // Require composer. require __DIR__ . '/vendor/autoload.php'; From 25363ee46247c8ce38ddaf4069cb4817121f6810 Mon Sep 17 00:00:00 2001 From: Matteo Gaggiano Date: Wed, 2 Oct 2024 16:48:20 +0200 Subject: [PATCH 29/29] fix(refresh token): indent with spaces --- tests/src/AccessTokenTest.php | 386 ++++++------ tests/src/RefreshTokenTest.php | 1076 ++++++++++++++++---------------- tests/src/RestTestTrait.php | 25 +- 3 files changed, 743 insertions(+), 744 deletions(-) diff --git a/tests/src/AccessTokenTest.php b/tests/src/AccessTokenTest.php index a762b92..0973a23 100644 --- a/tests/src/AccessTokenTest.php +++ b/tests/src/AccessTokenTest.php @@ -1,4 +1,4 @@ -client->post( '/wp-json/jwt-auth/v1/token', [ - 'form_params' => [ - 'username' => $this->username, - 'password' => $this->password, - ], - ] ); - $body = json_decode( $response->getBody()->getContents(), true ); - $this->assertEquals( 'jwt_auth_valid_credential', $body['code'] ); - $this->assertEquals( 200, $response->getStatusCode() ); - $this->assertEquals( true, $body['success'] ); - - $this->assertArrayHasKey( 'data', $body ); - $this->assertArrayHasKey( 'token', $body['data'] ); - $this->token = $body['data']['token']; - $this->assertNotEmpty( $this->token ); - - if ( $this->flow === 'cookie' ) { - $cookie = $this->cookies->getCookieByName( 'refresh_token' ); - $this->refreshToken = $cookie->getValue(); - } else { - $this->assertArrayHasKey( 'refresh_token', $body['data'] ); - $this->refreshToken = $body['data']['refresh_token']; - } - - $this->assertNotEmpty( $this->refreshToken ); - $this->assertNotEquals( $this->token, $this->refreshToken ); - - return $this->token; - } - - /** - * @depends testToken - * @throws GuzzleException - */ - public function testTokenWithEditedTokenType( string $token ): void { - $this->assertNotEmpty( $token ); - - $payload = json_decode( base64_decode( explode( '.', $token )[1] ), false ); - $payload->typ = 'refresh'; - $malicious_token = implode( '.', [ - explode( '.', $token )[0], - base64_encode( json_encode( $payload ) ), - explode( '.', $token )[2], - ] ); - - $request_options = array(); - - if ( $this->flow === 'cookie' ) { - $cookies = [ - 'refresh_token' => $malicious_token, - ]; - $domain = $this->getDomain(); - $cookies = CookieJar::fromArray( $cookies, $domain ); - $request_options['cookies'] = $cookies; - } else if ($this->flow === 'body') { - $request_options[\GuzzleHttp\RequestOptions::JSON] = [ - 'refresh_token' => $token, - ]; - } else { - $request_options['form_params'] = [ - 'refresh_token' => $token, - ]; - } - - $response = $this->client->post( '/wp-json/jwt-auth/v1/token/refresh', $request_options ); - $body = json_decode( $response->getBody()->getContents(), true ); - $this->assertIsArray( $body ); - $this->assertArrayHasKey( 'data', $body ); - $this->assertEquals( 'jwt_auth_invalid_refresh_token', $body['code'] ); - $this->assertEquals( 401, $response->getStatusCode() ); - $this->assertEquals( false, $body['success'] ); - } - - /** - * @depends testToken - * @throws GuzzleException - */ - public function testTokenValidate( string $token ): void { - $this->assertNotEmpty( $token ); - - $response = $this->client->post( '/wp-json/jwt-auth/v1/token/validate', [ - 'headers' => [ - 'Authorization' => "Bearer $token", - ], - ] ); - $body = json_decode( $response->getBody()->getContents(), true ); - $this->assertEquals( 'jwt_auth_valid_token', $body['code'] ); - $this->assertEquals( 200, $response->getStatusCode() ); - $this->assertEquals( true, $body['success'] ); - } - - /** - * @depends testToken - * @throws GuzzleException - */ - public function testTokenValidateWithInvalidToken( string $token ): void { - $this->assertNotEmpty( $token ); - - $response = $this->client->post( '/wp-json/jwt-auth/v1/token/validate', [ - 'headers' => [ - 'Authorization' => "Bearer {$token}123", - ], - ] ); - $body = json_decode( $response->getBody()->getContents(), true ); - $this->assertEquals( 'jwt_auth_invalid_token', $body['code'] ); - $this->assertEquals( 401, $response->getStatusCode() ); - $this->assertEquals( false, $body['success'] ); - } - - /** - * @depends testToken - * @throws GuzzleException - */ - public function testTokenRefreshWithInvalidToken( string $token ): void { - $this->assertNotEmpty( $token ); - - $response = $this->client->post( '/wp-json/jwt-auth/v1/token/refresh', [ - 'headers' => [ - 'Authorization' => "Bearer {$token}", - ], - ] ); - $body = json_decode( $response->getBody()->getContents(), true ); - if ( $this->flow === 'cookie' ) { - $this->assertEquals( 'jwt_auth_no_auth_cookie', $body['code'] ); - } else { - $this->assertEquals( 'jwt_auth_no_refresh_token', $body['code'] ); - } - $this->assertEquals( 401, $response->getStatusCode() ); - $this->assertEquals( false, $body['success'] ); - - $request_options = array(); - - if ( $this->flow === 'cookie' ) { - $cookies = [ - 'refresh_token' => $token, - ]; - $domain = $this->getDomain(); - $cookies = CookieJar::fromArray( $cookies, $domain ); - $request_options['cookies'] = $cookies; - } else if ($this->flow === 'body') { - $request_options[\GuzzleHttp\RequestOptions::JSON] = [ - 'refresh_token' => $token, - ]; - } else { - $request_options['form_params'] = [ - 'refresh_token' => $token, - ]; - } - $response = $this->client->post( '/wp-json/jwt-auth/v1/token/refresh', $request_options ); - $body = json_decode( $response->getBody()->getContents(), true ); - $this->assertEquals( 'jwt_auth_invalid_refresh_token', $body['code'] ); - $this->assertEquals( 401, $response->getStatusCode() ); - $this->assertEquals( false, $body['success'] ); - } - - /** - * @depends testToken - * @throws GuzzleException - */ - public function testTokenWithInvalidRefreshToken( string $token ): void { - $this->assertNotEmpty( $token ); - - $request_options = array(); - - if ( $this->flow === 'cookie' ) { - $cookies = [ - 'refresh_token' => $token, - ]; - $domain = $this->getDomain(); - $cookies = CookieJar::fromArray( $cookies, $domain ); - $request_options['cookies'] = $cookies; - } else if ($this->flow === 'body') { - $request_options[\GuzzleHttp\RequestOptions::JSON] = [ - 'refresh_token' => $token, - ]; - } else { - $request_options['form_params'] = [ - 'refresh_token' => $token, - ]; - } - $response = $this->client->post( '/wp-json/jwt-auth/v1/token', $request_options ); - $body = json_decode( $response->getBody()->getContents(), true ); - $this->assertEquals( 'jwt_auth_invalid_refresh_token', $body['code'] ); - $this->assertEquals( 401, $response->getStatusCode() ); - $this->assertEquals( false, $body['success'] ); - } + use RestTestTrait; + + /** + * @throws GuzzleException + */ + public function testToken(): string { + $response = $this->client->post('/wp-json/jwt-auth/v1/token', [ + 'form_params' => [ + 'username' => $this->username, + 'password' => $this->password, + ], + ]); + $body = json_decode($response->getBody()->getContents(), true); + $this->assertEquals('jwt_auth_valid_credential', $body['code']); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals(true, $body['success']); + + $this->assertArrayHasKey('data', $body); + $this->assertArrayHasKey('token', $body['data']); + $this->token = $body['data']['token']; + $this->assertNotEmpty( $this->token ); + + if ($this->flow === 'cookie') { + $cookie = $this->cookies->getCookieByName('refresh_token'); + $this->refreshToken = $cookie->getValue(); + } else { + $this->assertArrayHasKey('refresh_token', $body['data']); + $this->refreshToken = $body['data']['refresh_token']; + } + + $this->assertNotEmpty($this->refreshToken); + $this->assertNotEquals($this->token, $this->refreshToken); + + return $this->token; + } + + /** + * @depends testToken + * @throws GuzzleException + */ + public function testTokenWithEditedTokenType(string $token): void { + $this->assertNotEmpty($token); + + $payload = json_decode(base64_decode(explode('.', $token)[1]), false); + $payload->typ = 'refresh'; + $malicious_token = implode('.', [ + explode('.', $token )[0], + base64_encode(json_encode($payload)), + explode('.', $token )[2], + ]); + + $request_options = array(); + + if ($this->flow === 'cookie') { + $cookies = [ + 'refresh_token' => $malicious_token, + ]; + $domain = $this->getDomain(); + $cookies = CookieJar::fromArray($cookies, $domain); + $request_options['cookies'] = $cookies; + } else if ($this->flow === 'body') { + $request_options[\GuzzleHttp\RequestOptions::JSON] = [ + 'refresh_token' => $token, + ]; + } else { + $request_options['form_params'] = [ + 'refresh_token' => $token, + ]; + } + + $response = $this->client->post('/wp-json/jwt-auth/v1/token/refresh', $request_options); + $body = json_decode($response->getBody()->getContents(), true); + $this->assertIsArray($body); + $this->assertArrayHasKey('data', $body); + $this->assertEquals('jwt_auth_invalid_refresh_token', $body['code']); + $this->assertEquals(401, $response->getStatusCode()); + $this->assertEquals(false, $body['success']); + } + + /** + * @depends testToken + * @throws GuzzleException + */ + public function testTokenValidate(string $token): void { + $this->assertNotEmpty($token); + + $response = $this->client->post('/wp-json/jwt-auth/v1/token/validate', [ + 'headers' => [ + 'Authorization' => "Bearer $token", + ], + ]); + $body = json_decode($response->getBody()->getContents(), true); + $this->assertEquals('jwt_auth_valid_token', $body['code']); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals(true, $body['success']); + } + + /** + * @depends testToken + * @throws GuzzleException + */ + public function testTokenValidateWithInvalidToken(string $token): void { + $this->assertNotEmpty($token); + + $response = $this->client->post('/wp-json/jwt-auth/v1/token/validate', [ + 'headers' => [ + 'Authorization' => "Bearer {$token}123", + ], + ]); + $body = json_decode($response->getBody()->getContents(), true); + $this->assertEquals('jwt_auth_invalid_token', $body['code']); + $this->assertEquals(401, $response->getStatusCode()); + $this->assertEquals(false, $body['success']); + } + + /** + * @depends testToken + * @throws GuzzleException + */ + public function testTokenRefreshWithInvalidToken(string $token): void { + $this->assertNotEmpty($token); + + $response = $this->client->post('/wp-json/jwt-auth/v1/token/refresh', [ + 'headers' => [ + 'Authorization' => "Bearer {$token}", + ], + ]); + $body = json_decode($response->getBody()->getContents(), true); + if ($this->flow === 'cookie') { + $this->assertEquals('jwt_auth_no_auth_cookie', $body['code']); + } else { + $this->assertEquals('jwt_auth_no_refresh_token', $body['code']); + } + $this->assertEquals(401, $response->getStatusCode()); + $this->assertEquals(false, $body['success']); + + $request_options = array(); + + if ($this->flow === 'cookie') { + $cookies = [ + 'refresh_token' => $token, + ]; + $domain = $this->getDomain(); + $cookies = CookieJar::fromArray($cookies, $domain); + $request_options['cookies'] = $cookies; + } else if ($this->flow === 'body') { + $request_options[\GuzzleHttp\RequestOptions::JSON] = [ + 'refresh_token' => $token, + ]; + } else { + $request_options['form_params'] = [ + 'refresh_token' => $token, + ]; + } + $response = $this->client->post('/wp-json/jwt-auth/v1/token/refresh', $request_options); + $body = json_decode($response->getBody()->getContents(), true); + $this->assertEquals('jwt_auth_invalid_refresh_token', $body['code']); + $this->assertEquals(401, $response->getStatusCode()); + $this->assertEquals(false, $body['success']); + } + + /** + * @depends testToken + * @throws GuzzleException + */ + public function testTokenWithInvalidRefreshToken(string $token): void { + $this->assertNotEmpty($token); + + $request_options = array(); + + if ($this->flow === 'cookie') { + $cookies = [ + 'refresh_token' => $token, + ]; + $domain = $this->getDomain(); + $cookies = CookieJar::fromArray( $cookies, $domain ); + $request_options['cookies'] = $cookies; + } else if ($this->flow === 'body') { + $request_options[\GuzzleHttp\RequestOptions::JSON] = [ + 'refresh_token' => $token, + ]; + } else { + $request_options['form_params'] = [ + 'refresh_token' => $token, + ]; + } + $response = $this->client->post('/wp-json/jwt-auth/v1/token', $request_options); + $body = json_decode($response->getBody()->getContents(), true); + $this->assertEquals('jwt_auth_invalid_refresh_token', $body['code']); + $this->assertEquals(401, $response->getStatusCode()); + $this->assertEquals(false, $body['success']); + } } diff --git a/tests/src/RefreshTokenTest.php b/tests/src/RefreshTokenTest.php index a91807e..cd3bcf2 100644 --- a/tests/src/RefreshTokenTest.php +++ b/tests/src/RefreshTokenTest.php @@ -11,543 +11,543 @@ */ final class RefreshTokenTest extends TestCase { - use RestTestTrait; - - /** - * @throws GuzzleException - */ - public function testToken(): string { - $response = $this->client->post( '/wp-json/jwt-auth/v1/token', [ - 'form_params' => [ - 'username' => $this->username, - 'password' => $this->password, - ], - ] ); - $body = json_decode( $response->getBody()->getContents(), true ); - $this->assertEquals( 'jwt_auth_valid_credential', $body['code'] ); - $this->assertEquals( 200, $response->getStatusCode() ); - $this->assertEquals( true, $body['success'] ); - - $this->assertArrayHasKey( 'data', $body ); - $this->assertArrayHasKey( 'token', $body['data'] ); - $this->token = $body['data']['token']; - $this->assertNotEmpty( $this->token ); - - if ( $this->flow === 'cookie' ) { - // Discard the refresh_token cookie we set above to only retain the - // refresh_token cookie from the response. - $this->cookies->clearSessionCookies(); - - $cookie = $this->cookies->getCookieByName( 'refresh_token' ); - $this->refreshToken = $cookie->getValue(); - } else { - $this->assertArrayHasKey( 'refresh_token', $body['data'] ); - $this->refreshToken = $body['data']['refresh_token']; - } - - $this->assertNotEmpty( $this->refreshToken ); - $this->assertNotEquals( $this->token, $this->refreshToken ); - - return $this->refreshToken; - } - - /** - * @depends testToken - */ - public function testTokenWithEditedTokenType( string $refreshToken ): void { - $this->assertNotEmpty( $refreshToken ); - - $this->assertCount( 3, explode( '.', $refreshToken ) ); - - $payload = json_decode( base64_decode( explode( '.', $refreshToken )[1] ), false ); - $payload->typ = 'access'; - $malicious_refreshToken = implode( '.', [ - explode( '.', $refreshToken )[0], - base64_encode( json_encode( $payload ) ), - explode( '.', $refreshToken )[2], - ] ); - - $response = $this->client->post( '/wp-json/jwt-auth/v1/token/validate', [ - 'headers' => [ - 'Authorization' => "Bearer {$malicious_refreshToken}", - ], - ] ); - $body = json_decode( $response->getBody()->getContents(), true ); - $this->assertIsArray( $body ); - $this->assertArrayHasKey( 'data', $body ); - $this->assertEquals( 'jwt_auth_invalid_token', $body['code'] ); - $this->assertEquals( 401, $response->getStatusCode() ); - $this->assertEquals( false, $body['success'] ); - } - - /** - * @depends testToken - */ - public function testTokenValidateWithRefreshToken( string $refreshToken ): void { - $this->assertNotEmpty( $refreshToken ); - - $response = $this->client->post( '/wp-json/jwt-auth/v1/token/validate', [ - 'headers' => [ - 'Authorization' => "Bearer {$refreshToken}", - ], - ] ); - $body = json_decode( $response->getBody()->getContents(), true ); - $this->assertIsArray( $body ); - $this->assertArrayHasKey( 'data', $body ); - $this->assertEquals( 'jwt_auth_invalid_token', $body['code'] ); - $this->assertEquals( 401, $response->getStatusCode() ); - $this->assertEquals( false, $body['success'] ); - } - - /** - * @depends testToken - * @throws GuzzleException - */ - public function testTokenWithRefreshToken( string $refreshToken ): void { - $this->assertNotEmpty( $refreshToken ); - - $request_options = array(); - - if ( $this->flow === 'cookie' ) { - $cookies = [ - 'refresh_token' => $refreshToken, - ]; - $domain = $this->getDomain(); - $cookies = CookieJar::fromArray( $cookies, $domain ); - - $request_options['cookies'] = $cookies; - - } elseif ($this->flow === 'body') { - $request_options[\GuzzleHttp\RequestOptions::JSON] = [ - 'refresh_token' => $refreshToken, - ]; - } else { - $request_options['form_params'] = [ - 'refresh_token' => $refreshToken, - ]; - } - - $response = $this->client->post( '/wp-json/jwt-auth/v1/token', $request_options ); - - $body = json_decode( $response->getBody()->getContents(), true ); - $this->assertEquals( 'jwt_auth_valid_credential', $body['code'] ); - $this->assertEquals( 200, $response->getStatusCode() ); - $this->assertEquals( true, $body['success'] ); - - $this->assertArrayHasKey( 'data', $body ); - $this->assertArrayHasKey( 'token', $body['data'] ); - $this->token = $body['data']['token']; - $this->assertNotEmpty( $this->token ); - $this->assertNotEquals( $this->token, $refreshToken ); - - if ( $this->flow === 'cookie' ) { - // Discard the refresh_token cookie we set above to only retain the - // refresh_token cookie from the response. - $this->cookies->clearSessionCookies(); - - $cookie = $this->cookies->getCookieByName( 'refresh_token' ); - $this->refreshToken = $cookie->getValue(); - } else { - $this->assertArrayHasKey( 'refresh_token', $body['data'] ); - $this->refreshToken = $body['data']['refresh_token']; - } - - $this->assertNotEmpty( $this->refreshToken ); - $this->assertNotEquals( $this->token, $this->refreshToken ); - } - - /** - * @depends testToken - * @throws GuzzleException - */ - public function testTokenWithInvalidRefreshToken( string $refreshToken ): void { - $this->assertNotEmpty( $refreshToken ); - - $request_options = array(); - - if ( $this->flow === 'cookie' ) { - - $cookies = [ - 'refresh_token' => $refreshToken . '123', - ]; - $domain = $this->getDomain(); - $cookies = CookieJar::fromArray( $cookies, $domain ); - - $request_options['cookies'] = $cookies; - } elseif ($this->flow === 'body') { - $request_options[\GuzzleHttp\RequestOptions::JSON] = [ - 'refresh_token' => $refreshToken . '123', - ]; - } else { - $request_options['form_params'] = [ - 'refresh_token' => $refreshToken . '123', - ]; - } - - $response = $this->client->post( '/wp-json/jwt-auth/v1/token', $request_options ); - $body = json_decode( $response->getBody()->getContents(), true ); - $this->assertEquals( 'jwt_auth_invalid_refresh_token', $body['code'] ); - $this->assertEquals( 401, $response->getStatusCode() ); - $this->assertEquals( false, $body['success'] ); - } - - /** - * @depends testToken - * @throws GuzzleException - */ - public function testTokenRefresh( string $refreshToken ): string { - $this->assertNotEmpty( $refreshToken ); - - // Wait 1 seconds as the token creation is based on timestamp in seconds. - sleep( 1 ); - - $request_options = array(); - - if ( $this->flow === 'cookie' ) { - $cookies = [ - 'refresh_token' => $refreshToken, - ]; - $domain = $this->getDomain(); - $cookies = CookieJar::fromArray( $cookies, $domain ); - - $request_options['cookies'] = $cookies; - } elseif ($this->flow === 'body') { - $request_options[\GuzzleHttp\RequestOptions::JSON] = [ - 'refresh_token' => $refreshToken, - ]; - } else { - $request_options['form_params'] = [ - 'refresh_token' => $refreshToken, - ]; - } - - $response = $this->client->post( '/wp-json/jwt-auth/v1/token/refresh', $request_options ); - $body = json_decode( $response->getBody()->getContents(), true ); - $this->assertEquals( 'jwt_auth_valid_token', $body['code'] ); - $this->assertEquals( 200, $response->getStatusCode() ); - $this->assertEquals( true, $body['success'] ); - - if ( $this->flow === 'cookie' ) { - $this->assertArrayNotHasKey( 'data', $body ); - - // Discard the refresh_token cookie we set above to only retain the - // refresh_token cookie from the response. - $cookies->clearSessionCookies(); - - $cookie = $cookies->getCookieByName( 'refresh_token' ); - $this->refreshToken = $cookie->getValue(); - } else { - $this->assertArrayHasKey( 'data', $body ); - $this->assertArrayHasKey( 'refresh_token', $body['data'] ); - $this->refreshToken = $body['data']['refresh_token']; - } - - $this->assertNotEmpty( $this->refreshToken ); - $this->assertNotEquals( $this->refreshToken, $refreshToken ); - - return $this->refreshToken; - } - - /** - * @throws GuzzleException - */ - public function testTokenWithRotatedRefreshToken(): void { - // Not using @depends, because refresh token rotation relies on particular - // order. - $refreshToken1 = $this->testToken(); - $this->assertNotEmpty( $refreshToken1 ); - - // Wait 1 seconds as the token creation is based on timestamp in seconds. - sleep( 1 ); - - $request_options = array(); - - if ( $this->flow === 'cookie' ) { - $domain = $this->getDomain(); - - // Fetch a new refresh token. - $this->cookies->clear(); - $this->setCookie( 'refresh_token', $refreshToken1, $domain ); - } elseif ($this->flow === 'body') { - $request_options[\GuzzleHttp\RequestOptions::JSON] = [ - 'refresh_token' => $refreshToken1, - ]; - } else { - $request_options['form_params'] = [ - 'refresh_token' => $refreshToken1, - ]; - } - - $response = $this->client->post( '/wp-json/jwt-auth/v1/token/refresh', $request_options ); - $body = json_decode( $response->getBody()->getContents(), true ); - $this->assertEquals( 'jwt_auth_valid_token', $body['code'] ); - $this->assertEquals( 200, $response->getStatusCode() ); - $this->assertEquals( true, $body['success'] ); - - if ( $this->flow === 'cookie' ) { - $this->assertArrayNotHasKey( 'data', $body ); - - // Discard the refresh_token cookie we set above to only retain the - // refresh_token cookie from the response. - $this->cookies->clearSessionCookies(); - - $cookie = $this->cookies->getCookieByName( 'refresh_token' ); - $refreshToken2 = $cookie->getValue(); - - } else { - $this->assertArrayHasKey( 'data', $body ); - $this->assertArrayHasKey( 'refresh_token', $body['data'] ); - $refreshToken2 = $body['data']['refresh_token']; - } - $this->assertNotEmpty( $refreshToken2 ); - - // Confirm the refresh token was rotated. - $this->assertNotEquals( $refreshToken2, $refreshToken1 ); - - if ( $this->flow === 'cookie' ) { - $domain = $this->getDomain(); - - // Confirm the rotated refresh token is valid. - $this->cookies->clear(); - $this->setCookie( 'refresh_token', $refreshToken2, $domain ); - } elseif ($this->flow === 'body') { - $request_options[\GuzzleHttp\RequestOptions::JSON] = [ - 'refresh_token' => $refreshToken2, - ]; - } else { - $request_options['form_params'] = [ - 'refresh_token' => $refreshToken2, - ]; - } - - $response = $this->client->post( '/wp-json/jwt-auth/v1/token', $request_options ); - $body = json_decode( $response->getBody()->getContents(), true ); - $this->assertEquals( 'jwt_auth_valid_credential', $body['code'] ); - $this->assertEquals( 200, $response->getStatusCode() ); - $this->assertEquals( true, $body['success'] ); - - $this->assertArrayHasKey( 'data', $body ); - $this->assertArrayHasKey( 'token', $body['data'] ); - $this->token = $body['data']['token']; - $this->assertNotEmpty( $this->token ); - $this->assertNotEquals( $this->token, $refreshToken2 ); - - if ( $this->flow === 'cookie' ) { - $domain = $this->getDomain(); - - // Discard the refresh_token cookie we set above to only retain the - // refresh_token cookie from the response. - $this->cookies->clearSessionCookies(); - - $cookie = $this->cookies->getCookieByName( 'refresh_token' ); - $this->assertEmpty( $cookie ); - - // Confirm the previous refresh token is no longer valid. - $this->cookies->clear(); - $this->setCookie( 'refresh_token', $refreshToken1, $domain ); - } elseif ($this->flow === 'body') { - $request_options[\GuzzleHttp\RequestOptions::JSON] = [ - 'refresh_token' => $refreshToken1, - ]; - } else { - $request_options['form_params'] = [ - 'refresh_token' => $refreshToken1, - ]; - } - $response = $this->client->post( '/wp-json/jwt-auth/v1/token', $request_options ); - $body = json_decode( $response->getBody()->getContents(), true ); - $this->assertEquals( 'jwt_auth_obsolete_refresh_token', $body['code'], $body['message'] ); - $this->assertEquals( 401, $response->getStatusCode() ); - $this->assertEquals( false, $body['success'] ); - } - - /** - * @throws GuzzleException - */ - public function testTokenRefreshRotationByDevice() { - $domain = $this->getDomain(); - - $devices = [ - 1 => [ - 'device' => 'device1', - ], - 2 => [ - 'device' => 'device2', - ], - ]; - - $this->cookies->clear(); - - // Authenticate with each device. - for ( $i = 1; $i <= count( $devices ); $i ++ ) { - $response = $this->client->post( '/wp-json/jwt-auth/v1/token', [ - 'form_params' => [ - 'username' => $this->username, - 'password' => $this->password, - 'device' => $devices[ $i ]['device'], - ], - ] ); - $body = json_decode( $response->getBody()->getContents(), true ); - $this->assertEquals( 'jwt_auth_valid_credential', $body['code'] ); - - if ( $this->flow === 'cookie' ) { - $cookie = $this->cookies->getCookieByName( 'refresh_token' ); - $devices[ $i ]['refresh_token'] = $cookie->getValue(); - } else { - $this->assertArrayHasKey( 'data', $body ); - $this->assertArrayHasKey( 'refresh_token', $body['data'] ); - $devices[ $i ]['refresh_token'] = $body['data']['refresh_token']; - } - $this->assertNotEmpty( $devices[ $i ]['refresh_token'] ); - - if ( isset( $devices[ $i - 1 ]['refresh_token'] ) ) { - $this->assertNotEquals( $devices[ $i - 1 ]['refresh_token'], $devices[ $i ]['refresh_token'] ); - } - - $this->cookies->clear(); - } - - // Wait 1 seconds as the token creation is based on timestamp in seconds. - sleep( 1 ); - - // Refresh token with each device. - for ( $i = 1; $i <= count( $devices ); $i ++ ) { - $initial_refresh_token = $devices[ $i ]['refresh_token']; - - $request_options = array(); - if ( $this->flow === 'cookie' ) { - $request_options['form_params'] = [ - 'device' => $devices[ $i ]['device'], - ]; - $this->setCookie( 'refresh_token', $initial_refresh_token, $domain ); - } elseif ($this->flow === 'body') { - $request_options[\GuzzleHttp\RequestOptions::JSON] = [ - 'refresh_token' => $initial_refresh_token, - ]; - } else { - $request_options['form_params'] = [ - 'refresh_token' => $initial_refresh_token, - ]; - } - - $response = $this->client->post( '/wp-json/jwt-auth/v1/token/refresh', $request_options ); - $body = json_decode( $response->getBody()->getContents(), true ); - $this->assertEquals( 'jwt_auth_valid_token', $body['code'] ); - - if ( $this->flow === 'cookie' ) { - // Discard the refresh_token cookie we set above to only retain the - // refresh_token cookie from the response. - $this->cookies->clearSessionCookies(); - $cookie = $this->cookies->getCookieByName( 'refresh_token' ); - $devices[ $i ]['refresh_token'] = $cookie->getValue(); - } else { - $this->assertArrayHasKey( 'data', $body ); - $this->assertArrayHasKey( 'refresh_token', $body['data'] ); - $devices[ $i ]['refresh_token'] = $body['data']['refresh_token']; - } - $this->assertNotEmpty( $devices[ $i ]['refresh_token'] ); - - $this->assertNotEquals( $initial_refresh_token, $devices[ $i ]['refresh_token'] ); - if ( isset( $devices[ $i - 1 ]['refresh_token'] ) ) { - $this->assertNotEquals( $devices[ $i - 1 ]['refresh_token'], $devices[ $i ]['refresh_token'] ); - } - - $this->cookies->clear(); - } - - // Confirm each device can use its refresh token to authenticate. - for ( $i = 1; $i <= count( $devices ); $i ++ ) { - - $request_options = array(); - if ( $this->flow === 'cookie' ) { - $this->setCookie( 'refresh_token', $devices[ $i ]['refresh_token'], $domain ); - } elseif ($this->flow === 'body') { - $request_options[\GuzzleHttp\RequestOptions::JSON] = [ - 'refresh_token' => $devices[ $i ]['refresh_token'], - ]; - } else { - $request_options['form_params'] = [ - 'refresh_token' => $devices[ $i ]['refresh_token'], - ]; - } - $response = $this->client->post( '/wp-json/jwt-auth/v1/token', $request_options ); - $body = json_decode( $response->getBody()->getContents(), true ); - $this->assertEquals( 'jwt_auth_valid_credential', $body['code'] ); - $this->assertArrayHasKey( 'data', $body ); - $this->assertArrayHasKey( 'token', $body['data'] ); - - if ( $this->flow === 'cookie' ) { - $this->cookies->clear(); - } else { - $this->assertArrayHasKey( 'refresh_token', $body['data'] ); - } - } - - $request_options = array(); - // Confirm the previous refresh token is no longer valid. - if ( $this->flow === 'cookie' ) { - $this->setCookie( 'refresh_token', $initial_refresh_token, $domain ); - } elseif ($this->flow === 'body') { - $request_options[\GuzzleHttp\RequestOptions::JSON] = [ - 'refresh_token' => $initial_refresh_token, - ]; - } else { - $request_options['form_params'] = [ - 'refresh_token' => $initial_refresh_token, - ]; - } - - $response = $this->client->post( '/wp-json/jwt-auth/v1/token', $request_options ); - $this->assertEquals( 401, $response->getStatusCode() ); - $body = json_decode( $response->getBody()->getContents(), true ); - $this->assertEquals( 'jwt_auth_obsolete_refresh_token', $body['code'] ); - } - - /** - * @depends testToken - * @throws GuzzleException - */ - public function testTokenRefreshWithInvalidRefreshToken( string $refreshToken ): void { - $this->assertNotEmpty( $refreshToken ); - - $response = $this->client->post( '/wp-json/jwt-auth/v1/token/refresh', [ - 'headers' => [ - 'Authorization' => "Bearer {$refreshToken}", - ], - ] ); - $body = json_decode( $response->getBody()->getContents(), true ); - - if ( $this->flow === 'cookie' ) { - $this->assertEquals( 'jwt_auth_no_auth_cookie', $body['code'] ); - } else { - $this->assertEquals( 'jwt_auth_no_refresh_token', $body['code'] ); - } - $this->assertEquals( 401, $response->getStatusCode() ); - $this->assertEquals( false, $body['success'] ); - - $request_options = array(); - if ( $this->flow === 'cookie' ) { - $cookies = [ - 'refresh_token' => $refreshToken, - ]; - $domain = $this->getDomain(); - $cookies = CookieJar::fromArray( $cookies, $domain ); - $request_options['cookies'] = $cookies; - } elseif ($this->flow === 'body') { - $request_options[\GuzzleHttp\RequestOptions::JSON] = [ - 'refresh_token' => $refreshToken, - ]; - } else { - $request_options['form_params'] = [ - 'refresh_token' => $refreshToken, - ]; - } - - $response = $this->client->post( '/wp-json/jwt-auth/v1/token/refresh', $request_options ); - $body = json_decode( $response->getBody()->getContents(), true ); - $this->assertEquals( 'jwt_auth_obsolete_refresh_token', $body['code'] ); - $this->assertEquals( 401, $response->getStatusCode() ); - $this->assertEquals( false, $body['success'] ); - } + use RestTestTrait; + + /** + * @throws GuzzleException + */ + public function testToken(): string { + $response = $this->client->post('/wp-json/jwt-auth/v1/token', [ + 'form_params' => [ + 'username' => $this->username, + 'password' => $this->password, + ], + ]); + $body = json_decode($response->getBody()->getContents(), true); + $this->assertEquals('jwt_auth_valid_credential', $body['code']); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals(true, $body['success']); + + $this->assertArrayHasKey('data', $body); + $this->assertArrayHasKey('token', $body['data']); + $this->token = $body['data']['token']; + $this->assertNotEmpty($this->token); + + if ($this->flow === 'cookie') { + // Discard the refresh_token cookie we set above to only retain the + // refresh_token cookie from the response. + $this->cookies->clearSessionCookies(); + + $cookie = $this->cookies->getCookieByName('refresh_token'); + $this->refreshToken = $cookie->getValue(); + } else { + $this->assertArrayHasKey('refresh_token', $body['data']); + $this->refreshToken = $body['data']['refresh_token']; + } + + $this->assertNotEmpty($this->refreshToken); + $this->assertNotEquals($this->token, $this->refreshToken); + + return $this->refreshToken; + } + + /** + * @depends testToken + */ + public function testTokenWithEditedTokenType(string $refreshToken): void { + $this->assertNotEmpty($refreshToken); + + $this->assertCount(3, explode('.', $refreshToken)); + + $payload = json_decode(base64_decode(explode('.', $refreshToken)[1]), false); + $payload->typ = 'access'; + $malicious_refreshToken = implode('.', [ + explode('.', $refreshToken)[0], + base64_encode(json_encode($payload)), + explode('.', $refreshToken)[2], + ]); + + $response = $this->client->post('/wp-json/jwt-auth/v1/token/validate', [ + 'headers' => [ + 'Authorization' => "Bearer {$malicious_refreshToken}", + ], + ]); + $body = json_decode($response->getBody()->getContents(), true); + $this->assertIsArray($body); + $this->assertArrayHasKey('data', $body); + $this->assertEquals('jwt_auth_invalid_token', $body['code']); + $this->assertEquals(401, $response->getStatusCode()); + $this->assertEquals(false, $body['success']); + } + + /** + * @depends testToken + */ + public function testTokenValidateWithRefreshToken(string $refreshToken): void { + $this->assertNotEmpty($refreshToken); + + $response = $this->client->post('/wp-json/jwt-auth/v1/token/validate', [ + 'headers' => [ + 'Authorization' => "Bearer {$refreshToken}", + ], + ]); + $body = json_decode($response->getBody()->getContents(), true); + $this->assertIsArray($body); + $this->assertArrayHasKey('data', $body); + $this->assertEquals('jwt_auth_invalid_token', $body['code']); + $this->assertEquals(401, $response->getStatusCode()); + $this->assertEquals(false, $body['success']); + } + + /** + * @depends testToken + * @throws GuzzleException + */ + public function testTokenWithRefreshToken(string $refreshToken): void { + $this->assertNotEmpty($refreshToken); + + $request_options = array(); + + if ($this->flow === 'cookie') { + $cookies = [ + 'refresh_token' => $refreshToken, + ]; + $domain = $this->getDomain(); + $cookies = CookieJar::fromArray($cookies, $domain); + + $request_options['cookies'] = $cookies; + + } elseif ($this->flow === 'body') { + $request_options[\GuzzleHttp\RequestOptions::JSON] = [ + 'refresh_token' => $refreshToken, + ]; + } else { + $request_options['form_params'] = [ + 'refresh_token' => $refreshToken, + ]; + } + + $response = $this->client->post('/wp-json/jwt-auth/v1/token', $request_options); + + $body = json_decode($response->getBody()->getContents(), true); + $this->assertEquals('jwt_auth_valid_credential', $body['code']); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals(true, $body['success']); + + $this->assertArrayHasKey('data', $body); + $this->assertArrayHasKey('token', $body['data']); + $this->token = $body['data']['token']; + $this->assertNotEmpty($this->token); + $this->assertNotEquals($this->token, $refreshToken); + + if ($this->flow === 'cookie') { + // Discard the refresh_token cookie we set above to only retain the + // refresh_token cookie from the response. + $this->cookies->clearSessionCookies(); + + $cookie = $this->cookies->getCookieByName('refresh_token'); + $this->refreshToken = $cookie->getValue(); + } else { + $this->assertArrayHasKey('refresh_token', $body['data']); + $this->refreshToken = $body['data']['refresh_token']; + } + + $this->assertNotEmpty($this->refreshToken); + $this->assertNotEquals($this->token, $this->refreshToken); + } + + /** + * @depends testToken + * @throws GuzzleException + */ + public function testTokenWithInvalidRefreshToken(string $refreshToken): void { + $this->assertNotEmpty($refreshToken); + + $request_options = array(); + + if ($this->flow === 'cookie') { + + $cookies = [ + 'refresh_token' => $refreshToken . '123', + ]; + $domain = $this->getDomain(); + $cookies = CookieJar::fromArray($cookies, $domain); + + $request_options['cookies'] = $cookies; + } elseif ($this->flow === 'body') { + $request_options[\GuzzleHttp\RequestOptions::JSON] = [ + 'refresh_token' => $refreshToken . '123', + ]; + } else { + $request_options['form_params'] = [ + 'refresh_token' => $refreshToken . '123', + ]; + } + + $response = $this->client->post('/wp-json/jwt-auth/v1/token', $request_options); + $body = json_decode($response->getBody()->getContents(), true); + $this->assertEquals('jwt_auth_invalid_refresh_token', $body['code']); + $this->assertEquals(401, $response->getStatusCode()); + $this->assertEquals(false, $body['success']); + } + + /** + * @depends testToken + * @throws GuzzleException + */ + public function testTokenRefresh(string $refreshToken): string { + $this->assertNotEmpty($refreshToken); + + // Wait 1 seconds as the token creation is based on timestamp in seconds. + sleep(1); + + $request_options = array(); + + if ($this->flow === 'cookie') { + $cookies = [ + 'refresh_token' => $refreshToken, + ]; + $domain = $this->getDomain(); + $cookies = CookieJar::fromArray($cookies, $domain); + + $request_options['cookies'] = $cookies; + } elseif ($this->flow === 'body') { + $request_options[\GuzzleHttp\RequestOptions::JSON] = [ + 'refresh_token' => $refreshToken, + ]; + } else { + $request_options['form_params'] = [ + 'refresh_token' => $refreshToken, + ]; + } + + $response = $this->client->post('/wp-json/jwt-auth/v1/token/refresh', $request_options); + $body = json_decode($response->getBody()->getContents(), true); + $this->assertEquals('jwt_auth_valid_token', $body['code']); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals(true, $body['success']); + + if ($this->flow === 'cookie') { + $this->assertArrayNotHasKey('data', $body); + + // Discard the refresh_token cookie we set above to only retain the + // refresh_token cookie from the response. + $cookies->clearSessionCookies(); + + $cookie = $cookies->getCookieByName('refresh_token'); + $this->refreshToken = $cookie->getValue(); + } else { + $this->assertArrayHasKey('data', $body); + $this->assertArrayHasKey('refresh_token', $body['data']); + $this->refreshToken = $body['data']['refresh_token']; + } + + $this->assertNotEmpty($this->refreshToken); + $this->assertNotEquals($this->refreshToken, $refreshToken); + + return $this->refreshToken; + } + + /** + * @throws GuzzleException + */ + public function testTokenWithRotatedRefreshToken(): void { + // Not using @depends, because refresh token rotation relies on particular + // order. + $refreshToken1 = $this->testToken(); + $this->assertNotEmpty($refreshToken1); + + // Wait 1 seconds as the token creation is based on timestamp in seconds. + sleep(1); + + $request_options = array(); + + if ($this->flow === 'cookie') { + $domain = $this->getDomain(); + + // Fetch a new refresh token. + $this->cookies->clear(); + $this->setCookie('refresh_token', $refreshToken1, $domain); + } elseif ($this->flow === 'body') { + $request_options[\GuzzleHttp\RequestOptions::JSON] = [ + 'refresh_token' => $refreshToken1, + ]; + } else { + $request_options['form_params'] = [ + 'refresh_token' => $refreshToken1, + ]; + } + + $response = $this->client->post('/wp-json/jwt-auth/v1/token/refresh', $request_options); + $body = json_decode($response->getBody()->getContents(), true); + $this->assertEquals('jwt_auth_valid_token', $body['code']); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals(true, $body['success']); + + if ($this->flow === 'cookie') { + $this->assertArrayNotHasKey('data', $body); + + // Discard the refresh_token cookie we set above to only retain the + // refresh_token cookie from the response. + $this->cookies->clearSessionCookies(); + + $cookie = $this->cookies->getCookieByName('refresh_token'); + $refreshToken2 = $cookie->getValue(); + + } else { + $this->assertArrayHasKey('data', $body); + $this->assertArrayHasKey('refresh_token', $body['data']); + $refreshToken2 = $body['data']['refresh_token']; + } + $this->assertNotEmpty($refreshToken2); + + // Confirm the refresh token was rotated. + $this->assertNotEquals($refreshToken2, $refreshToken1); + + if ($this->flow === 'cookie') { + $domain = $this->getDomain(); + + // Confirm the rotated refresh token is valid. + $this->cookies->clear(); + $this->setCookie('refresh_token', $refreshToken2, $domain); + } elseif ($this->flow === 'body') { + $request_options[\GuzzleHttp\RequestOptions::JSON] = [ + 'refresh_token' => $refreshToken2, + ]; + } else { + $request_options['form_params'] = [ + 'refresh_token' => $refreshToken2, + ]; + } + + $response = $this->client->post('/wp-json/jwt-auth/v1/token', $request_options); + $body = json_decode($response->getBody()->getContents(), true); + $this->assertEquals('jwt_auth_valid_credential', $body['code']); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals(true, $body['success']); + + $this->assertArrayHasKey('data', $body); + $this->assertArrayHasKey('token', $body['data']); + $this->token = $body['data']['token']; + $this->assertNotEmpty($this->token); + $this->assertNotEquals($this->token, $refreshToken2); + + if ($this->flow === 'cookie') { + $domain = $this->getDomain(); + + // Discard the refresh_token cookie we set above to only retain the + // refresh_token cookie from the response. + $this->cookies->clearSessionCookies(); + + $cookie = $this->cookies->getCookieByName('refresh_token'); + $this->assertEmpty($cookie); + + // Confirm the previous refresh token is no longer valid. + $this->cookies->clear(); + $this->setCookie('refresh_token', $refreshToken1, $domain); + } elseif ($this->flow === 'body') { + $request_options[\GuzzleHttp\RequestOptions::JSON] = [ + 'refresh_token' => $refreshToken1, + ]; + } else { + $request_options['form_params'] = [ + 'refresh_token' => $refreshToken1, + ]; + } + $response = $this->client->post('/wp-json/jwt-auth/v1/token', $request_options); + $body = json_decode($response->getBody()->getContents(), true); + $this->assertEquals('jwt_auth_obsolete_refresh_token', $body['code'], $body['message']); + $this->assertEquals(401, $response->getStatusCode()); + $this->assertEquals(false, $body['success']); + } + + /** + * @throws GuzzleException + */ + public function testTokenRefreshRotationByDevice() { + $domain = $this->getDomain(); + + $devices = [ + 1 => [ + 'device' => 'device1', + ], + 2 => [ + 'device' => 'device2', + ], + ]; + + $this->cookies->clear(); + + // Authenticate with each device. + for ($i = 1; $i <= count($devices); $i++) { + $response = $this->client->post('/wp-json/jwt-auth/v1/token', [ + 'form_params' => [ + 'username' => $this->username, + 'password' => $this->password, + 'device' => $devices[$i]['device'], + ], + ]); + $body = json_decode($response->getBody()->getContents(), true); + $this->assertEquals('jwt_auth_valid_credential', $body['code']); + + if ($this->flow === 'cookie') { + $cookie = $this->cookies->getCookieByName('refresh_token'); + $devices[$i]['refresh_token'] = $cookie->getValue(); + } else { + $this->assertArrayHasKey('data', $body); + $this->assertArrayHasKey('refresh_token', $body['data']); + $devices[$i]['refresh_token'] = $body['data']['refresh_token']; + } + $this->assertNotEmpty($devices[$i]['refresh_token']); + + if (isset($devices[$i - 1]['refresh_token'])) { + $this->assertNotEquals($devices[$i - 1]['refresh_token'], $devices[$i]['refresh_token']); + } + + $this->cookies->clear(); + } + + // Wait 1 seconds as the token creation is based on timestamp in seconds. + sleep(1); + + // Refresh token with each device. + for ($i = 1; $i <= count($devices); $i++) { + $initial_refresh_token = $devices[$i]['refresh_token']; + + $request_options = array(); + if ($this->flow === 'cookie') { + $request_options['form_params'] = [ + 'device' => $devices[$i]['device'], + ]; + $this->setCookie('refresh_token', $initial_refresh_token, $domain); + } elseif ($this->flow === 'body') { + $request_options[\GuzzleHttp\RequestOptions::JSON] = [ + 'refresh_token' => $initial_refresh_token, + ]; + } else { + $request_options['form_params'] = [ + 'refresh_token' => $initial_refresh_token, + ]; + } + + $response = $this->client->post('/wp-json/jwt-auth/v1/token/refresh', $request_options); + $body = json_decode($response->getBody()->getContents(), true); + $this->assertEquals('jwt_auth_valid_token', $body['code']); + + if ($this->flow === 'cookie') { + // Discard the refresh_token cookie we set above to only retain the + // refresh_token cookie from the response. + $this->cookies->clearSessionCookies(); + $cookie = $this->cookies->getCookieByName('refresh_token'); + $devices[$i]['refresh_token'] = $cookie->getValue(); + } else { + $this->assertArrayHasKey('data', $body); + $this->assertArrayHasKey('refresh_token', $body['data']); + $devices[$i]['refresh_token'] = $body['data']['refresh_token']; + } + $this->assertNotEmpty($devices[$i]['refresh_token']); + + $this->assertNotEquals($initial_refresh_token, $devices[$i]['refresh_token']); + if (isset($devices[$i - 1]['refresh_token'])) { + $this->assertNotEquals($devices[$i - 1]['refresh_token'], $devices[$i]['refresh_token']); + } + + $this->cookies->clear(); + } + + // Confirm each device can use its refresh token to authenticate. + for ($i = 1; $i <= count($devices); $i++) { + + $request_options = array(); + if ($this->flow === 'cookie') { + $this->setCookie('refresh_token', $devices[$i]['refresh_token'], $domain); + } elseif ($this->flow === 'body') { + $request_options[\GuzzleHttp\RequestOptions::JSON] = [ + 'refresh_token' => $devices[$i]['refresh_token'], + ]; + } else { + $request_options['form_params'] = [ + 'refresh_token' => $devices[$i]['refresh_token'], + ]; + } + $response = $this->client->post('/wp-json/jwt-auth/v1/token', $request_options); + $body = json_decode($response->getBody()->getContents(), true); + $this->assertEquals('jwt_auth_valid_credential', $body['code']); + $this->assertArrayHasKey('data', $body); + $this->assertArrayHasKey('token', $body['data']); + + if ($this->flow === 'cookie') { + $this->cookies->clear(); + } else { + $this->assertArrayHasKey('refresh_token', $body['data']); + } + } + + $request_options = array(); + // Confirm the previous refresh token is no longer valid. + if ($this->flow === 'cookie') { + $this->setCookie('refresh_token', $initial_refresh_token, $domain); + } elseif ($this->flow === 'body') { + $request_options[\GuzzleHttp\RequestOptions::JSON] = [ + 'refresh_token' => $initial_refresh_token, + ]; + } else { + $request_options['form_params'] = [ + 'refresh_token' => $initial_refresh_token, + ]; + } + + $response = $this->client->post('/wp-json/jwt-auth/v1/token', $request_options); + $this->assertEquals(401, $response->getStatusCode()); + $body = json_decode($response->getBody()->getContents(), true); + $this->assertEquals('jwt_auth_obsolete_refresh_token', $body['code']); + } + + /** + * @depends testToken + * @throws GuzzleException + */ + public function testTokenRefreshWithInvalidRefreshToken(string $refreshToken): void { + $this->assertNotEmpty($refreshToken); + + $response = $this->client->post('/wp-json/jwt-auth/v1/token/refresh', [ + 'headers' => [ + 'Authorization' => "Bearer {$refreshToken}", + ], + ]); + $body = json_decode($response->getBody()->getContents(), true); + + if ($this->flow === 'cookie') { + $this->assertEquals('jwt_auth_no_auth_cookie', $body['code']); + } else { + $this->assertEquals('jwt_auth_no_refresh_token', $body['code']); + } + $this->assertEquals(401, $response->getStatusCode()); + $this->assertEquals(false, $body['success']); + + $request_options = array(); + if ($this->flow === 'cookie') { + $cookies = [ + 'refresh_token' => $refreshToken, + ]; + $domain = $this->getDomain(); + $cookies = CookieJar::fromArray($cookies, $domain); + $request_options['cookies'] = $cookies; + } elseif ($this->flow === 'body') { + $request_options[\GuzzleHttp\RequestOptions::JSON] = [ + 'refresh_token' => $refreshToken, + ]; + } else { + $request_options['form_params'] = [ + 'refresh_token' => $refreshToken, + ]; + } + + $response = $this->client->post('/wp-json/jwt-auth/v1/token/refresh', $request_options); + $body = json_decode($response->getBody()->getContents(), true); + $this->assertEquals('jwt_auth_obsolete_refresh_token', $body['code']); + $this->assertEquals(401, $response->getStatusCode()); + $this->assertEquals(false, $body['success']); + } } diff --git a/tests/src/RestTestTrait.php b/tests/src/RestTestTrait.php index cddbd32..359f924 100644 --- a/tests/src/RestTestTrait.php +++ b/tests/src/RestTestTrait.php @@ -28,7 +28,7 @@ trait RestTestTrait { protected ?string $refreshToken; protected function setUp(): void { - $this->cookies = new CookieJar(); + $this->cookies = new CookieJar(); $this->httpClientConfig = [ 'base_uri' => $_ENV['URL'] ?? 'http://localhost', 'http_errors' => false, @@ -48,28 +48,27 @@ protected function setUp(): void { CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4, ], ]; - if ( in_array( '--debug', $_SERVER['argv'], true ) ) { + if (in_array('--debug', $_SERVER['argv'], true)) { $this->httpClientConfig['debug'] = true; } - $this->client = new Client( $this->httpClientConfig ); + $this->client = new Client($this->httpClientConfig); $this->username = $_ENV['USERNAME'] ?? null; $this->password = $_ENV['PASSWORD'] ?? null; - $this->flow = $_ENV['FLOW']; + $this->flow = $_ENV['FLOW']; } - protected function setCookie( $name, $value, $domain ): CookieJar { - $this->cookies->setCookie( new SetCookie( [ - 'Domain' => $domain, - 'Name' => $name, - 'Value' => $value, - 'Discard' => true, - ] ) ); - + protected function setCookie($name, $value, $domain): CookieJar { + $this->cookies->setCookie(new SetCookie([ + 'Domain' => $domain, + 'Name' => $name, + 'Value' => $value, + 'Discard' => true, + ])); return $this->cookies; } protected function getDomain(): string { - return parse_url( $this->httpClientConfig['base_uri'], PHP_URL_HOST ); + return parse_url($this->httpClientConfig['base_uri'], PHP_URL_HOST); } }