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 );