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 diff --git a/README.md b/README.md index 0934e5f..586fa94 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", @@ -217,10 +218,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..." ``` @@ -331,7 +346,17 @@ If the token is invalid an error will be returned. Here are some samples of erro "success": false, "statusCode": 401, "code": "jwt_auth_invalid_refresh_token", - "message": "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": [] } ``` @@ -393,6 +418,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. @@ -455,6 +510,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: @@ -486,8 +543,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: @@ -518,9 +610,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` filter hook 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: @@ -750,15 +910,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/class-auth.php b/class-auth.php index 204f3e1..2043f6d 100644 --- a/class-auth.php +++ b/class-auth.php @@ -8,7 +8,6 @@ namespace JWTAuth; use Exception; - use WP_Error; use WP_REST_Request; use WP_REST_Response; @@ -178,17 +177,20 @@ 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( $refresh_token, 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 ); + $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' ) : ''; } // If the authentication is failed return error response. @@ -212,9 +214,11 @@ 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, $device ); } + $response['data']['refresh_token'] = $refresh_token; + return $response; } @@ -231,10 +235,13 @@ 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( + 'typ' => 'access', 'iss' => $this->get_iss(), 'iat' => $issued_at, 'nbf' => $not_before, @@ -248,8 +255,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 ) { @@ -281,17 +293,22 @@ 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 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 ); + 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(); - setcookie( 'refresh_token', $user->ID . '.' . $refresh_token, $expires, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true ); + $payload = JWT::decode( $refresh_token, new Key( $secret_key, $alg ) ); + + 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 ); + } // Save new refresh token for the user, replacing the previous one. // The refresh token is rotated for the passed device only, not affecting @@ -300,21 +317,83 @@ 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' ) ?: ''; + $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; - foreach ( $user_refresh_tokens as $device ) { - if ( $device['expires'] < $expires_next ) { - $expires_next = $device['expires']; + $expires_next = $payload->exp; + 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 ); + + return $refresh_token; + } + + /** + * Generate a new refresh token. + * + * @param \WP_User $user The WP_User object. + * @param string $device Device name. Default empty string + * + * @return string + */ + 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; + $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 ); + + $payload = array( + 'typ' => 'refresh', + 'iss' => $this->get_iss(), + 'iat' => $issued_at, + 'nbf' => $not_before, + 'exp' => $expires, + 'data' => array( + 'device' => $device, + 'user' => array( + 'id' => $user->ID, + ), + ), + ); + + $alg = $this->get_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 ); + } + + /** + * Get the refresh token flow + * + * @return mixed|null + */ + 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); } /** @@ -356,21 +435,23 @@ 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; + /** * 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 ] ) ? sanitize_text_field( wp_unslash( $_SERVER[ $headerkey ] ) ) : false; + $auth = empty( $_SERVER[ $headerkey ] ) ? false : sanitize_text_field( wp_unslash( $_SERVER[ $headerkey ] ) ); // Double check for different auth header string (server dependent). if ( ! $auth ) { - $auth = isset( $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ) ) : false; + $auth = empty( $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ) ? false : sanitize_text_field( wp_unslash( $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ) ); } if ( ! $auth ) { @@ -441,6 +522,10 @@ public function validate_token( $return_response = true ) { ); } + if ( ! isset( $payload->typ ) || $payload->typ !== 'access' ) { + 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!! @@ -528,28 +613,41 @@ public function validate_token( $return_response = true ) { * @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 ) { + + $input_refresh_token = $this->retrieve_refresh_token( $request ); + + if ( empty( $input_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( $input_refresh_token, 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 ); + $device = $payload->data->device; + $refresh_token = $this->send_refresh_token( $user, $device ); + + $additional_fields = array(); + + if ( ! $this->is_flow( 'cookie' ) ) { + $additional_fields = array( + 'data' => array( + 'refresh_token' => $refresh_token, + ), + ); + } $response = array( 'success' => true, @@ -557,69 +655,188 @@ public function refresh_token( WP_REST_Request $request ) { 'code' => 'jwt_auth_valid_token', 'message' => __( 'Token is valid', 'jwt-auth' ), ); - return new WP_REST_Response( $response ); + + return new WP_REST_Response( array_merge( $response, $additional_fields ) ); } /** * 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 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( $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( $refresh_token, $return_response = true ) { + + // 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 ) ); - if ( empty( $user_refresh_tokens[ $device ] ) ) { - return new WP_REST_Response( - array( - 'success' => false, - 'statusCode' => 401, - 'code' => 'jwt_auth_invalid_refresh_token', - 'message' => __( 'Invalid refresh token', 'jwt-auth' ), - ), - 401 - ); - } elseif ( $refresh_token !== $user_refresh_tokens[ $device ]['token'] ) { - return new WP_REST_Response( - array( - 'success' => false, - 'statusCode' => 401, - 'code' => 'jwt_auth_obsolete_refresh_token', - 'message' => __( 'Refresh token is obsolete', 'jwt-auth' ), - ), - 401 + // 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 + ); + } + + if ( ! isset( $payload->typ ) || $payload->typ !== 'refresh' ) { + 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!! + 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 ( ! $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 + ); + } + + 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 ( ! 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 ) ) { + return new WP_REST_Response( + array( + 'success' => false, + 'statusCode' => 401, + 'code' => 'jwt_auth_invalid_refresh_token', + 'message' => __( 'Invalid refresh token', 'jwt-auth' ), + ), + 401 + ); + } elseif ( $refresh_token !== $last_refresh_token_issued['token'] ) { + return new WP_REST_Response( + array( + 'success' => false, + 'statusCode' => 401, + 'code' => 'jwt_auth_obsolete_refresh_token', + 'message' => __( 'Refresh token is obsolete', 'jwt-auth' ), + ), + 401 + ); + } elseif ( time() > $last_refresh_token_issued['expires'] ) { + return new WP_REST_Response( + array( + 'success' => false, + 'statusCode' => 401, + 'code' => 'jwt_auth_expired_refresh_token', + 'message' => __( 'Refresh token has expired', '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(), ); - } elseif ( time() > $user_refresh_tokens[ $device ]['expires'] ) { + + $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_expired_refresh_token', - 'message' => __( 'Refresh token has expired', 'jwt-auth' ), + 'code' => 'jwt_auth_invalid_refresh_token', + 'message' => $e->getMessage(), + 'data' => array(), ), 401 ); } - - return $user_id; } /** @@ -690,4 +907,30 @@ 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 ( 'body' === $flow ) { + $_array = $request->get_json_params(); + } else if ( 'query' === $flow ) { + $_array = $request->get_query_params(); + } else if ( 'header' === $flow ) { + $_array = $request->get_headers(); + } else { // default cookie + $_array = $_COOKIE; + } + + $refresh_token = $_array['refresh_token'] ?? null; + + return apply_filters( 'jwt_auth_retrieve_refresh_token', $refresh_token, $flow ); + } + } 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" } 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/readme.txt b/readme.txt index a11918f..a07020c 100644 --- a/readme.txt +++ b/readme.txt @@ -344,6 +344,16 @@ If the token is invalid an error will be returned. Here are some samples of erro } ` +` +{ + "success": false, + "statusCode": 401, + "code": "jwt_auth_invalid_refresh_token", + "message": "Invalid token type", + "data": [] +} +` + = Obsolete Refresh Token = ` diff --git a/tests/src/AccessTokenTest.php b/tests/src/AccessTokenTest.php index 73b3dd9..0973a23 100644 --- a/tests/src/AccessTokenTest.php +++ b/tests/src/AccessTokenTest.php @@ -14,118 +14,191 @@ final class AccessTokenTest extends TestCase { * @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); - - $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 - * @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); - $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->getDomain(); - $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 - * @throws GuzzleException - */ - public function testTokenWithInvalidRefreshToken(string $token): void { - $this->assertNotEmpty($token); - - $cookies = [ - 'refresh_token' => $token, - ]; - $domain = $this->getDomain(); - $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']); - } + $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 04809a7..cd3bcf2 100644 --- a/tests/src/RefreshTokenTest.php +++ b/tests/src/RefreshTokenTest.php @@ -33,12 +33,18 @@ public function testToken(): string { $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(); + 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']; + } - $cookie = $this->cookies->getCookieByName('refresh_token'); - $this->refreshToken = $cookie->getValue(); $this->assertNotEmpty($this->refreshToken); $this->assertNotEquals($this->token, $this->refreshToken); @@ -47,7 +53,35 @@ public function testToken(): string { /** * @depends testToken - * @throws GuzzleException + */ + 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); @@ -58,6 +92,8 @@ public function testTokenValidateWithRefreshToken(string $refreshToken): void { ], ]); $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']); @@ -70,15 +106,29 @@ public function testTokenValidateWithRefreshToken(string $refreshToken): void { public function testTokenWithRefreshToken(string $refreshToken): void { $this->assertNotEmpty($refreshToken); - $cookies = [ - 'refresh_token' => $refreshToken, - ]; - $domain = $this->getDomain(); - $cookies = CookieJar::fromArray($cookies, $domain); + $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); - $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()); @@ -90,12 +140,20 @@ public function testTokenWithRefreshToken(string $refreshToken): void { $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(); + 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']; + } - $cookie = $cookies->getCookieByName('refresh_token'); - $this->assertEmpty($cookie); + $this->assertNotEmpty($this->refreshToken); + $this->assertNotEquals($this->token, $this->refreshToken); } /** @@ -105,17 +163,30 @@ public function testTokenWithRefreshToken(string $refreshToken): void { public function testTokenWithInvalidRefreshToken(string $refreshToken): void { $this->assertNotEmpty($refreshToken); - $cookies = [ - 'refresh_token' => $refreshToken . '123', - ]; - $domain = $this->getDomain(); - $cookies = CookieJar::fromArray($cookies, $domain); + $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', [ - 'cookies' => $cookies, - ]); + $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']); } @@ -127,27 +198,50 @@ public function testTokenWithInvalidRefreshToken(string $refreshToken): void { public function testTokenRefresh(string $refreshToken): string { $this->assertNotEmpty($refreshToken); - $cookies = [ - 'refresh_token' => $refreshToken, - ]; - $domain = $this->getDomain(); - $cookies = CookieJar::fromArray($cookies, $domain); + // 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', [ - 'cookies' => $cookies, - ]); + $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']); - $this->assertArrayNotHasKey('data', $body); - // Discard the refresh_token cookie we set above to only retain the - // refresh_token cookie from the response. - $cookies->clearSessionCookies(); + 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']; + } - $cookie = $cookies->getCookieByName('refresh_token'); - $this->refreshToken = $cookie->getValue(); $this->assertNotEmpty($this->refreshToken); $this->assertNotEquals($this->refreshToken, $refreshToken); @@ -163,33 +257,70 @@ public function testTokenWithRotatedRefreshToken(): void { $refreshToken1 = $this->testToken(); $this->assertNotEmpty($refreshToken1); - $domain = $this->getDomain(); + // Wait 1 seconds as the token creation is based on timestamp in seconds. + sleep(1); - // 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'); + $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']); - $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(); + 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(); - $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); - // 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'); + 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()); @@ -201,19 +332,31 @@ public function testTokenWithRotatedRefreshToken(): void { $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(); + if ($this->flow === 'cookie') { + $domain = $this->getDomain(); - $cookie = $this->cookies->getCookieByName('refresh_token'); - $this->assertEmpty($cookie); + // Discard the refresh_token cookie we set above to only retain the + // refresh_token cookie from the response. + $this->cookies->clearSessionCookies(); - // 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'); + $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']); + $this->assertEquals('jwt_auth_obsolete_refresh_token', $body['code'], $body['message']); $this->assertEquals(401, $response->getStatusCode()); $this->assertEquals(false, $body['success']); } @@ -241,13 +384,20 @@ public function testTokenRefreshRotationByDevice() { 'form_params' => [ 'username' => $this->username, 'password' => $this->password, - 'device' => $devices[$i]['device'], + '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(); + + 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'])) { @@ -257,24 +407,44 @@ public function testTokenRefreshRotationByDevice() { $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']; - $this->setCookie('refresh_token', $devices[$i]['refresh_token'], $domain); - $response = $this->client->post('/wp-json/jwt-auth/v1/token/refresh', [ - 'form_params' => [ + $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']); - // 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(); + 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']); @@ -287,26 +457,47 @@ public function testTokenRefreshRotationByDevice() { // 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'], - ], - ]); + + $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']); - $this->cookies->clear(); + 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. - $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'], - ], - ]); + 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']); @@ -325,19 +516,34 @@ public function testTokenRefreshWithInvalidRefreshToken(string $refreshToken): v ], ]); $body = json_decode($response->getBody()->getContents(), true); - $this->assertEquals('jwt_auth_no_auth_cookie', $body['code']); + + 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']); - $cookies = [ - 'refresh_token' => $refreshToken, - ]; - $domain = $this->getDomain(); - $cookies = CookieJar::fromArray($cookies, $domain); + $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', [ - 'cookies' => $cookies, - ]); + $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()); diff --git a/tests/src/RestTestTrait.php b/tests/src/RestTestTrait.php index 4304c65..359f924 100644 --- a/tests/src/RestTestTrait.php +++ b/tests/src/RestTestTrait.php @@ -54,6 +54,7 @@ protected function setUp(): void { $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 {