| @@ -85,8 +85,46 @@ | ||
| 85 | 85 | return self::$instance; |
| 86 | 86 | } |
| 87 | 87 | |
| 88 | 88 | /** |
| 89 | + * On `wp_logout`: end the web POS session named by this browser's cookie. | |
| 90 | + * | |
| 91 | + * Sessions of native apps and other browsers stay live. The cookie itself is left alone. | |
| 92 | + * | |
| 93 | + * @param mixed $user_id ID of the user logging out. | |
| 94 | + */ | |
| 95 | + public static function revoke_web_session_on_logout( $user_id ): void { | |
| 96 | + if ( absint( $user_id ) > 0 ) { | |
| 97 | + self::instance()->cleanup_previous_web_session( absint( $user_id ) ); | |
| 98 | + } | |
| 99 | + } | |
| 100 | + | |
| 101 | + /** | |
| 102 | + * On `password_reset`: end every POS session of the user. | |
| 103 | + * | |
| 104 | + * @param mixed $user User whose password is being reset. | |
| 105 | + */ | |
| 106 | + public static function revoke_sessions_on_password_reset( $user ): void { | |
| 107 | + if ( $user instanceof WP_User ) { | |
| 108 | + self::instance()->revoke_all_refresh_tokens( $user->ID ); | |
| 109 | + } | |
| 110 | + } | |
| 111 | + | |
| 112 | + /** | |
| 113 | + * On `profile_update`: end every POS session of the user when the password changed. | |
| 114 | + * | |
| 115 | + * @param mixed $user_id ID of the updated user. | |
| 116 | + * @param mixed $old_user_data User data before the update. | |
| 117 | + */ | |
| 118 | + public static function revoke_sessions_on_password_change( $user_id, $old_user_data = null ): void { | |
| 119 | + $user = get_userdata( absint( $user_id ) ); | |
| 120 | + | |
| 121 | + if ( $old_user_data instanceof WP_User && $user instanceof WP_User && $old_user_data->user_pass !== $user->user_pass ) { | |
| 122 | + self::instance()->revoke_all_refresh_tokens( $user->ID ); | |
| 123 | + } | |
| 124 | + } | |
| 125 | + | |
| 126 | + /** | |
| 89 | 127 | * Extract a WCPOS token from an authorization value. |
| 90 | 128 | * |
| 91 | 129 | * @param mixed $auth_value Authorization value. |
| 92 | 130 | * |
| @@ -250,20 +288,35 @@ | ||
| 250 | 288 | array( 'status' => 403 ) |
| 251 | 289 | ); |
| 252 | 290 | } |
| 253 | 291 | |
| 254 | - // The session is live: record that, so eviction can tell a device that is | |
| 255 | - // working right now from one that has not been seen in a week. | |
| 292 | + // The session registry is authoritative; the blacklist transient above is only a | |
| 293 | + // fast path that can be evicted or purged. Once the session is live, record that, | |
| 294 | + // so eviction can tell a device working right now from one unseen for a week. | |
| 256 | 295 | if ( isset( $decoded_token->refresh_jti ) ) { |
| 257 | - $this->sessions->touch( | |
| 258 | - absint( $decoded_token->data->user->id ), | |
| 259 | - (string) $decoded_token->refresh_jti | |
| 260 | - ); | |
| 296 | + $user_id = absint( $decoded_token->data->user->id ); | |
| 297 | + $refresh_jti = (string) $decoded_token->refresh_jti; | |
| 298 | + | |
| 299 | + if ( ! $this->sessions->is_live( $user_id, $refresh_jti ) ) { | |
| 300 | + return new WP_Error( | |
| 301 | + 'woocommerce_pos_auth_session_revoked', | |
| 302 | + 'Session has been revoked', | |
| 303 | + array( 'status' => 403 ) | |
| 304 | + ); | |
| 305 | + } | |
| 306 | + | |
| 307 | + $this->sessions->touch( $user_id, $refresh_jti ); | |
| 261 | 308 | } |
| 262 | 309 | } |
| 263 | 310 | |
| 264 | 311 | // Everything looks good return the decoded token. |
| 265 | 312 | return $decoded_token; |
| 313 | + } catch ( \WCPOS\Vendor\Firebase\JWT\ExpiredException $e ) { | |
| 314 | + return new WP_Error( | |
| 315 | + 'woocommerce_pos_auth_token_expired', | |
| 316 | + 'Token expired', | |
| 317 | + array( 'status' => 403 ) | |
| 318 | + ); | |
| 266 | 319 | } catch ( Exception $e ) { |
| 267 | 320 | // Something is wrong trying to decode the token, send back the error. |
| 268 | 321 | return new WP_Error( |
| 269 | 322 | 'woocommmerce_pos_auth_invalid_token', |
| @@ -585,9 +638,11 @@ | ||
| 585 | 638 | /* |
| 586 | 639 | * Before the first row read on this path. A refresh loads the whole session row — |
| 587 | 640 | * `is_live()` below, then `refresh_activity()` — so it needs |
| 588 | 641 | * the same protection a login has against a row too large to read (#1776). |
| 589 | - * Validating an ACCESS token needs no such guard: it no longer touches the row. | |
| 642 | + * Validating an ACCESS token READS the row through `is_live()` but never writes it, | |
| 643 | + * and runs no guard because `guard_row()` can write; the read primes the user meta | |
| 644 | + * cache that WordPress fills anyway to read the user's capabilities, so it adds no query. | |
| 590 | 645 | */ |
| 591 | 646 | $this->sessions->guard_row( absint( $decoded->data->user->id ) ); |
| 592 | 647 | |
| 593 | 648 | // Check if refresh token is still valid (not revoked). |