Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 117 additions & 55 deletions class-auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -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' ),
Expand All @@ -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,
)
);
}
}
}

Expand All @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -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,
);
}

/**
Expand Down Expand Up @@ -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 );
}
}

/**
Expand All @@ -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(
Expand Down