Commit 60ccbf3884d for woocommerce
commit 60ccbf3884d05d4dd7cb6261c8919d49ef86540a
Author: Raluca Stan <ralucastn@gmail.com>
Date: Wed Sep 2 12:28:03 2026 +0200
Use the REST route resolved by WordPress for API-key authentication (#68247)
* Support API keys on resolved WooCommerce REST routes
* Test API keys on resolved WooCommerce REST routes
* Add changelog for rewritten REST route authentication
* Drop @since from the new private methods
diff --git a/plugins/woocommerce/changelog/fix-rest-api-key-rewritten-routes b/plugins/woocommerce/changelog/fix-rest-api-key-rewritten-routes
new file mode 100644
index 00000000000..5ede23338c3
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-rest-api-key-rewritten-routes
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Improve REST API key authentication for WooCommerce routes reached through custom URLs.
diff --git a/plugins/woocommerce/includes/class-wc-rest-authentication.php b/plugins/woocommerce/includes/class-wc-rest-authentication.php
index 8109658fc48..ddc8ac1d82e 100644
--- a/plugins/woocommerce/includes/class-wc-rest-authentication.php
+++ b/plugins/woocommerce/includes/class-wc-rest-authentication.php
@@ -72,9 +72,9 @@ class WC_REST_Authentication {
return false;
}
- // 'wc/' is WooCommerce; 'wc-' lets third party plugins use our authentication methods.
- $route = $this->route_from_request_uri();
- $is_wc_route = str_starts_with( $route, 'wc/' ) || str_starts_with( $route, 'wc-' );
+ $resolved_route = $this->resolved_route();
+ $is_wc_route = $this->is_wc_namespace( $this->route_from_request_uri() )
+ || ( null !== $resolved_route && $this->is_wc_namespace( $resolved_route ) );
/**
* Filters whether the current request is a request to the WooCommerce REST API.
@@ -86,6 +86,35 @@ class WC_REST_Authentication {
return apply_filters( 'woocommerce_rest_is_request_to_rest_api', $is_wc_route );
}
+ /**
+ * Whether a route is in a namespace a WooCommerce API key may authenticate.
+ *
+ * 'wc/' is WooCommerce; 'wc-' lets third party plugins use our authentication methods.
+ *
+ * @param string $route Route without the REST prefix or surrounding slashes.
+ * @return bool
+ */
+ private function is_wc_namespace( string $route ): bool {
+ return str_starts_with( $route, 'wc/' ) || str_starts_with( $route, 'wc-' );
+ }
+
+ /**
+ * The route WordPress resolved for this request, trimmed of surrounding slashes.
+ *
+ * Null when WordPress has not parsed the request yet, which is not the same as an empty route.
+ *
+ * @return string|null
+ */
+ private function resolved_route(): ?string {
+ global $wp;
+
+ if ( ! $wp instanceof WP || ! isset( $wp->query_vars['rest_route'] ) || ! is_string( $wp->query_vars['rest_route'] ) ) {
+ return null;
+ }
+
+ return trim( $wp->query_vars['rest_route'], '/' );
+ }
+
/**
* The REST route the request URI points to, normalized the way WordPress matches it.
*
@@ -264,19 +293,16 @@ class WC_REST_Authentication {
* @return bool False only when the resolved route is not ours and the request URI never named it.
*/
private function is_resolved_route_in_scope() {
- global $wp;
-
// Has WordPress picked a route yet? If not, there is nothing to compare.
- if ( ! $wp instanceof WP || ! isset( $wp->query_vars['rest_route'] ) || ! is_string( $wp->query_vars['rest_route'] ) ) {
+ $resolved_route = $this->resolved_route();
+
+ if ( null === $resolved_route ) {
return true;
}
- // Our own namespaces are always in scope for a WooCommerce key: 'wc/' is WooCommerce, 'wc-'
- // is a third party using our auth. The read/write permission check in check_user_permissions()
- // still bounds what the key can do there.
- $resolved_route = trim( $wp->query_vars['rest_route'], '/' );
-
- if ( str_starts_with( $resolved_route, 'wc/' ) || str_starts_with( $resolved_route, 'wc-' ) ) {
+ // Our own namespaces are always in scope for a WooCommerce key. The read/write permission
+ // check in check_user_permissions() still bounds what the key can do there.
+ if ( $this->is_wc_namespace( $resolved_route ) ) {
return true;
}
diff --git a/plugins/woocommerce/tests/php/includes/rest-api/class-wc-rest-authentication-tests.php b/plugins/woocommerce/tests/php/includes/rest-api/class-wc-rest-authentication-tests.php
index 85319daeaad..a5a42f32384 100644
--- a/plugins/woocommerce/tests/php/includes/rest-api/class-wc-rest-authentication-tests.php
+++ b/plugins/woocommerce/tests/php/includes/rest-api/class-wc-rest-authentication-tests.php
@@ -178,6 +178,38 @@ class WC_REST_Authentication_Tests extends WC_REST_Unit_Test_Case {
);
}
+ /**
+ * @testdox Should identify a WooCommerce REST request by the route WordPress resolved.
+ *
+ * @dataProvider provider_resolved_routes_for_rest_api_detection
+ *
+ * @param string $request_uri Request URI.
+ * @param string $resolved_route Route WordPress resolved for the request.
+ * @param bool $expected Expected result.
+ */
+ public function test_is_request_to_rest_api_checks_resolved_route( string $request_uri, string $resolved_route, bool $expected ): void {
+ global $wp;
+
+ $_SERVER['REQUEST_URI'] = $request_uri;
+ $wp->query_vars['rest_route'] = $resolved_route;
+
+ $this->assertSame( $expected, $this->is_request_to_rest_api() );
+ }
+
+ /**
+ * Data provider for resolved REST routes.
+ *
+ * @return array[]
+ */
+ public static function provider_resolved_routes_for_rest_api_detection(): array {
+ return array(
+ 'language-prefixed woocommerce route' => array( '/en/wp-json/wc/v3/products', '/wc/v3/products', true ),
+ 'route differs from resolved route' => array( '/en/wp-json/wc/v3/products', '/wp/v2/users', false ),
+ 'custom rewrite without REST prefix' => array( '/shop-api/products', '/wc/v3/products', true ),
+ 'resolved route overrides URI route' => array( '/wp-json/wp/v2/users', '/wc/v3/products', true ),
+ );
+ }
+
/**
* @testdox Should detect WooCommerce routes on a subdirectory install, matching how WordPress strips the home path.
*
@@ -465,9 +497,15 @@ class WC_REST_Authentication_Tests extends WC_REST_Unit_Test_Case {
}
/**
- * @testdox Should let the authentication fallback through for a WooCommerce route.
+ * @testdox Should let the authentication fallback through for an in-scope WooCommerce route.
+ *
+ * @testWith ["/wp-json/wc/v3/products"]
+ * ["/ja/wp-json/wc/v3/products"]
+ * ["/shop-api/products"]
+ *
+ * @param string $request_uri Request URI.
*/
- public function test_rest_authentication_errors_allows_in_scope_route_from_fallback(): void {
+ public function test_rest_authentication_errors_allows_in_scope_route_from_fallback( string $request_uri ): void {
global $wp, $wpdb;
$consumer_key = 'ck_' . wp_generate_password( 32, false );
@@ -488,7 +526,7 @@ class WC_REST_Authentication_Tests extends WC_REST_Unit_Test_Case {
$_SERVER['HTTPS'] = 'on';
$_SERVER['PHP_AUTH_USER'] = $consumer_key;
$_SERVER['PHP_AUTH_PW'] = $consumer_secret;
- $_SERVER['REQUEST_URI'] = '/wp-json/wc/v3/products';
+ $_SERVER['REQUEST_URI'] = $request_uri;
$wp->query_vars['rest_route'] = '/wc/v3/products';
wp_set_current_user( 0 );