| @@ -84,8 +84,9 @@ | ||
| 84 | 84 | |
| 85 | 85 | // These filters allow changes to the WC REST API response. |
| 86 | 86 | add_filter( 'rest_dispatch_request', array( $this, 'rest_dispatch_request' ), 10, 4 ); |
| 87 | 87 | add_filter( 'rest_pre_dispatch', array( $this, 'rest_pre_dispatch' ), 10, 3 ); |
| 88 | + add_filter( 'rest_pre_dispatch', array( $this, 'clear_third_party_jwt_error' ), 50, 3 ); | |
| 88 | 89 | add_filter( 'rest_post_dispatch', array( $this, 'rest_post_dispatch' ), 10, 3 ); |
| 89 | 90 | } |
| 90 | 91 | |
| 91 | 92 | /** |
| @@ -329,13 +330,14 @@ | ||
| 329 | 330 | * 1. WooCommerce issue #26847: determine_current_user may not be called when |
| 330 | 331 | * WordPress has already cached the current user. We attempt auth here as a |
| 331 | 332 | * fallback. |
| 332 | 333 | * |
| 333 | - * 2. JWT plugin conflict: a third-party JWT plugin (e.g. jwt-authentication-for-wp-rest-api) | |
| 334 | - * sees our Bearer token, fails to validate it with its own secret, and returns | |
| 335 | - * a WP_Error via rest_authentication_errors at priority 10. We run at priority 50 | |
| 336 | - * and attempt our own Bearer-token validation. If it succeeds, we clear the | |
| 337 | - * stale error — our authentication wins. | |
| 334 | + * 2. JWT plugin conflict: a third-party JWT plugin sees our Bearer token, fails | |
| 335 | + * to validate it with its own secret, and returns a WP_Error via | |
| 336 | + * rest_authentication_errors at priority 10. We run at priority 50 and attempt | |
| 337 | + * our own Bearer-token validation. If it succeeds, we clear the stale error — | |
| 338 | + * our authentication wins. (jwt-authentication-for-wp-rest-api surfaces its | |
| 339 | + * error through rest_pre_dispatch instead; see clear_third_party_jwt_error().) | |
| 338 | 340 | * |
| 339 | 341 | * @param mixed $errors Authentication errors. |
| 340 | 342 | * |
| 341 | 343 | * @return mixed |
| @@ -348,19 +350,9 @@ | ||
| 348 | 350 | if ( ! empty( $errors ) ) { |
| 349 | 351 | // Only clear errors that originate from JWT authentication plugins. Errors |
| 350 | 352 | // from other mechanisms (maintenance locks, IP restrictions, etc.) should |
| 351 | 353 | // be passed through even when the WCPOS Bearer token is valid. |
| 352 | - $is_jwt_plugin_error = is_wp_error( $errors ) && 0 === strpos( $errors->get_error_code(), 'jwt_auth_' ); | |
| 353 | - | |
| 354 | - if ( $is_jwt_plugin_error && ! $this->authenticated_via_wcpos ) { | |
| 355 | - $user_id = $this->authenticate( false ); | |
| 356 | - if ( $user_id && ! is_wp_error( $user_id ) ) { | |
| 357 | - wp_set_current_user( $user_id ); | |
| 358 | - $this->authenticated_via_wcpos = true; | |
| 359 | - } | |
| 360 | - } | |
| 361 | - | |
| 362 | - if ( $this->authenticated_via_wcpos && $is_jwt_plugin_error ) { | |
| 354 | + if ( $this->is_third_party_jwt_error( $errors ) && $this->ensure_authenticated_via_wcpos() ) { | |
| 363 | 355 | return null; |
| 364 | 356 | } |
| 365 | 357 | |
| 366 | 358 | return $errors; |
| @@ -366,20 +358,80 @@ | ||
| 366 | 358 | return $errors; |
| 367 | 359 | } |
| 368 | 360 | |
| 369 | 361 | // check if determine_current_user has been called. |
| 370 | - if ( ! $this->is_auth_checked ) { | |
| 371 | - // Authentication hasn't occurred during `determine_current_user`, so check auth. | |
| 362 | + if ( ! $this->is_auth_checked && $this->ensure_authenticated_via_wcpos() ) { | |
| 363 | + // Authentication hadn't occurred during `determine_current_user`, but our token is valid. | |
| 364 | + return true; | |
| 365 | + } | |
| 366 | + | |
| 367 | + return $errors; | |
| 368 | + } | |
| 369 | + | |
| 370 | + /** | |
| 371 | + * Clear a third-party JWT plugin's stale error from the dispatch result. | |
| 372 | + * | |
| 373 | + * The plugin jwt-authentication-for-wp-rest-api (verified at 1.5.0) validates every | |
| 374 | + * Bearer token in determine_current_user (priority 10) with its own secret. Ours fails, | |
| 375 | + * so it stores a `jwt_auth_invalid_token` WP_Error and returns the user untouched; | |
| 376 | + * our priority-20 filter then authenticates the request. The plugin later returns | |
| 377 | + * that stored error from rest_pre_dispatch (priority 10, registered at | |
| 378 | + * plugins_loaded), which replaces the dispatch result with a 403. | |
| 379 | + * | |
| 380 | + * Priority 50: after the plugin's callback, and after our own priority-10 | |
| 381 | + * permission gate, whose `woocommerce_pos_rest_*` errors must pass through untouched. | |
| 382 | + * | |
| 383 | + * Unlike rest_authentication_errors(), this never switches the current user: the | |
| 384 | + * priority-10 gate and the core-order audit guard have already judged the user in | |
| 385 | + * scope, so the error is cleared only when our token resolves to that same user. | |
| 386 | + * | |
| 387 | + * @param mixed $result Dispatch result, or null to not hijack the request. | |
| 388 | + * @param WP_REST_Server $server Server instance. | |
| 389 | + * @param WP_REST_Request $request Request used to generate the response. | |
| 390 | + * | |
| 391 | + * @return mixed | |
| 392 | + */ | |
| 393 | + public function clear_third_party_jwt_error( $result, $server, $request ) { | |
| 394 | + if ( ! $this->is_third_party_jwt_error( $result ) ) { | |
| 395 | + return $result; | |
| 396 | + } | |
| 397 | + | |
| 398 | + if ( ! $this->authenticated_via_wcpos ) { | |
| 372 | 399 | $user_id = $this->authenticate( false ); |
| 400 | + if ( $user_id && ! is_wp_error( $user_id ) && get_current_user_id() === (int) $user_id ) { | |
| 401 | + $this->authenticated_via_wcpos = true; | |
| 402 | + } | |
| 403 | + } | |
| 404 | + | |
| 405 | + return $this->authenticated_via_wcpos ? null : $result; | |
| 406 | + } | |
| 407 | + | |
| 408 | + /** | |
| 409 | + * Whether a value is a WP_Error raised by a third-party JWT plugin (`jwt_auth_*`). | |
| 410 | + * | |
| 411 | + * @param mixed $maybe_error Value to inspect. | |
| 412 | + * | |
| 413 | + * @return bool | |
| 414 | + */ | |
| 415 | + private function is_third_party_jwt_error( $maybe_error ): bool { | |
| 416 | + return is_wp_error( $maybe_error ) && 0 === strpos( $maybe_error->get_error_code(), 'jwt_auth_' ); | |
| 417 | + } | |
| 418 | + | |
| 419 | + /** | |
| 420 | + * Authenticate the request with its WCPOS Bearer token if that hasn't happened yet. | |
| 421 | + * | |
| 422 | + * @return bool True when the request is authenticated via a WCPOS-issued token. | |
| 423 | + */ | |
| 424 | + private function ensure_authenticated_via_wcpos(): bool { | |
| 425 | + if ( ! $this->authenticated_via_wcpos ) { | |
| 426 | + $user_id = $this->authenticate( false ); | |
| 373 | 427 | if ( $user_id && ! is_wp_error( $user_id ) ) { |
| 374 | 428 | wp_set_current_user( $user_id ); |
| 375 | 429 | $this->authenticated_via_wcpos = true; |
| 376 | - | |
| 377 | - return true; | |
| 378 | 430 | } |
| 379 | 431 | } |
| 380 | 432 | |
| 381 | - return $errors; | |
| 433 | + return $this->authenticated_via_wcpos; | |
| 382 | 434 | } |
| 383 | 435 | |
| 384 | 436 | /** |
| 385 | 437 | * Extract the Authorization Bearer token from the request. |