diff --git a/class-auth.php b/class-auth.php index 3ec988d..b06e1ca 100644 --- a/class-auth.php +++ b/class-auth.php @@ -167,8 +167,7 @@ public function get_token( WP_REST_Request $request ) { } if ( ( isset( $username ) && ! isset( $password ) ) - || ( ! isset( $username ) && isset( $password ) ) - || ! isset( $_COOKIE['refresh_token'] ) ) { + || ( ! isset( $username ) && isset( $password ) ) ) { $user = new WP_Error( 'jwt_auth_missing_credentials', __( 'Username and password are required', 'jwt-auth' ), @@ -180,25 +179,29 @@ public function get_token( WP_REST_Request $request ) { if ( isset( $username ) && isset( $password ) ) { $user = $this->authenticate_user( $username, $password, $custom_auth ); - } - elseif ( isset( $_COOKIE['refresh_token'] ) ) { - $device = $request->get_param( 'device' ) ?: ''; - $user_id = $this->validate_refresh_token( $_COOKIE['refresh_token'], $device ); - - // If we receive a REST response, then validation failed. - if ( $user_id instanceof WP_REST_Response ) { - return $user_id; + } else { + // Check for refresh_token in request body first, then fallback to cookie. + $refresh_token = $request->get_param( 'refresh_token' ); + if ( ! $refresh_token && isset( $_COOKIE['refresh_token'] ) ) { + $refresh_token = $_COOKIE['refresh_token']; } - $user = get_user_by( 'id', $user_id ); - - if ( ! $user ) { - $user = new WP_Error( - 'jwt_auth_invalid_refresh_token', - __( 'Invalid refresh token', 'jwt-auth' ), - array( - 'status' => 401, - ) - ); + if ( $refresh_token ) { + $device = $request->get_param( 'device' ) ?: ''; + $user_id = $this->validate_refresh_token( $refresh_token, $device ); + // If we receive a REST response, then validation failed. + if ( $user_id instanceof WP_REST_Response ) { + return $user_id; + } + $user = get_user_by( 'id', $user_id ); + if ( ! $user ) { + $user = new WP_Error( + 'jwt_auth_invalid_refresh_token', + __( 'Invalid refresh token', 'jwt-auth' ), + array( + 'status' => 401, + ) + ); + } } } @@ -221,9 +224,20 @@ public function get_token( WP_REST_Request $request ) { // Valid credentials, the user exists, let's generate the token. $response = $this->generate_token( $user, false ); - // Add the refresh token as a HttpOnly cookie to the response. + // Determine transport for refresh token: 'cookie' (default) or 'body'. if ( $username && $password ) { - $this->send_refresh_token( $user, $request ); + $device = $request->get_param( 'device' ) ?: ''; + $refresh_token_data = $this->generate_refresh_token( $user, $device ); + $token_transport = $request->get_param( 'token_transport' ); + if ( $token_transport === 'body' ) { + // Invalidate any existing cookie using invalidate_cookie. + $this->invalidate_cookie(); + if ( is_array( $response ) ) { + $response['data']['refresh_token'] = $refresh_token_data['token']; + } + } else { // Default: set cookie + $this->send_refresh_token( $user, $refresh_token_data ); + } } return $response; @@ -296,22 +310,44 @@ public function generate_token( $user, $return_raw = true ) { * * @return void */ - public function send_refresh_token( \WP_User $user, \WP_REST_Request $request ) { + + /** + * Sets the refresh token as a cookie. + * + * @param \WP_User $user The WP_User object. + * @param array $token_data The refresh token data array. + * @return void + */ + public function send_refresh_token( \WP_User $user, array $token_data ) { + setcookie( 'refresh_token', $token_data['token'], $token_data['expires'], COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true ); + } + + /** + * Invalidates the refresh token cookie. + * + * @return void + */ + public function invalidate_cookie() { + setcookie( 'refresh_token', '', time() - 3600, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true ); + } + + /** + * Generates and stores a new refresh token for a user/device, returns token data. + * + * @param \WP_User $user The WP_User object. + * @param string $device The device identifier. + * @return array Returns array with 'token', 'expires', and 'device'. + */ + public function generate_refresh_token( \WP_User $user, string $device ) { $refresh_token = bin2hex( random_bytes( 32 ) ); $created = time(); $expires = $created + DAY_IN_SECONDS * 30; $expires = apply_filters( 'jwt_auth_refresh_expire', $expires, $created ); - setcookie( 'refresh_token', $user->ID . '.' . $refresh_token, $expires, 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 - // other devices. $user_refresh_tokens = get_user_meta( $user->ID, 'jwt_auth_refresh_tokens', true ); 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, @@ -320,12 +356,18 @@ public function send_refresh_token( \WP_User $user, \WP_REST_Request $request ) // 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']; + foreach ( $user_refresh_tokens as $device_data ) { + if ( $device_data['expires'] < $expires_next ) { + $expires_next = $device_data['expires']; } } update_user_meta( $user->ID, 'jwt_auth_refresh_tokens_expires_next', $expires_next ); + + return array( + 'token' => $user->ID . '.' . $refresh_token, + 'expires' => $expires, + 'device' => $device, + ); } /** @@ -540,35 +582,55 @@ public function validate_token( $return_response = true ) { * @return WP_REST_Response Returns WP_REST_Response. */ public function refresh_token( WP_REST_Request $request ) { - if ( ! isset( $_COOKIE['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' ), - ), - 401 - ); + // Check for refresh token in request body first, then fallback to cookie. + $refresh_token = $request->get_param( 'refresh_token' ); + if ( ! $refresh_token ) { + if ( isset( $_COOKIE['refresh_token'] ) ) { + $refresh_token = $_COOKIE['refresh_token']; + } else { + return new WP_REST_Response( + array( + 'success' => false, + 'statusCode' => 401, + 'code' => 'jwt_auth_no_auth_cookie', + 'message' => __( 'Refresh token not provided.', 'jwt-auth' ), + ), + 401 + ); + } } $device = $request->get_param( 'device' ) ?: ''; - $user_id = $this->validate_refresh_token( $_COOKIE['refresh_token'], $device ); + $user_id = $this->validate_refresh_token( $refresh_token, $device ); if ( $user_id instanceof WP_REST_Response ) { return $user_id; } - // Generate a new access token. + // Generate a new access token and refresh token. $user = get_user_by( 'id', $user_id ); - $this->send_refresh_token( $user, $request ); - - $response = array( - 'success' => true, - 'statusCode' => 200, - 'code' => 'jwt_auth_valid_token', - 'message' => __( 'Token is valid', 'jwt-auth' ), - ); - return new WP_REST_Response( $response ); + $refresh_token_data = $this->generate_refresh_token( $user, $device ); + $token_transport = $request->get_param( 'token_transport' ); + if ( $token_transport === 'body' ) { + // Invalidate any existing cookie using invalidate_cookie. + $this->invalidate_cookie(); + $response = array( + 'success' => true, + 'statusCode' => 200, + 'code' => 'jwt_auth_valid_token', + 'message' => __( 'Token is valid', 'jwt-auth' ), + 'refresh_token' => $refresh_token_data['token'], + ); + return new WP_REST_Response( $response ); + } else { // Default: set cookie + $this->send_refresh_token( $user, $refresh_token_data ); + $response = array( + 'success' => true, + 'statusCode' => 200, + 'code' => 'jwt_auth_valid_token', + 'message' => __( 'Token is valid', 'jwt-auth' ), + ); + return new WP_REST_Response( $response ); + } } /** @@ -578,8 +640,8 @@ public function refresh_token( WP_REST_Request $request ) { * @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. */ - public function validate_refresh_token( $refresh_token_cookie, $device ) { - $parts = explode( '.', $refresh_token_cookie ); + public function validate_refresh_token( $refresh_token_value, $device ) { + $parts = explode( '.', $refresh_token_value ); if ( count( $parts ) !== 2 || empty( intval( $parts[0] ) ) || empty( $parts[1] ) ) { return new WP_REST_Response( array(