Commit 7ea8ce435e3 for woocommerce
commit 7ea8ce435e38cbf7b89cc35c5cdb0ace2d3f3385
Author: malinajirka <malinajirka@gmail.com>
Date: Fri Sep 11 10:05:45 2026 +0200
Update mobile app QR login session handling (#68540)
* Update mobile app QR login session handling
* Add tests for mobile app QR login session handling
diff --git a/plugins/woocommerce/changelog/fix-woo6-146-qr-login-session-handling b/plugins/woocommerce/changelog/fix-woo6-146-qr-login-session-handling
new file mode 100644
index 00000000000..accb03ea126
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-woo6-146-qr-login-session-handling
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Update mobile app QR login session handling.
diff --git a/plugins/woocommerce/src/Admin/API/MobileAppQRLogin.php b/plugins/woocommerce/src/Admin/API/MobileAppQRLogin.php
index 3eeee5bbf19..d037e5c6137 100644
--- a/plugins/woocommerce/src/Admin/API/MobileAppQRLogin.php
+++ b/plugins/woocommerce/src/Admin/API/MobileAppQRLogin.php
@@ -1842,6 +1842,11 @@ class MobileAppQRLogin extends \WC_REST_Data_Controller {
return $this->rest_ensure_nocache_response( array( 'state' => self::STATE_EXPIRED ) );
}
+ $canonical_session = isset( $record['challenge']['session_id'] ) ? (string) $record['challenge']['session_id'] : '';
+ if ( '' === $canonical_session || ! hash_equals( $canonical_session, $session_id ) ) {
+ return $this->rest_ensure_nocache_response( array( 'state' => self::STATE_EXPIRED ) );
+ }
+
$state = isset( $record['state'] ) ? (string) $record['state'] : self::STATE_PENDING;
$response = array( 'state' => $state );
diff --git a/plugins/woocommerce/tests/php/src/Admin/API/MobileAppQRLoginTest.php b/plugins/woocommerce/tests/php/src/Admin/API/MobileAppQRLoginTest.php
index 4a8a0e8febf..c9eec80d728 100644
--- a/plugins/woocommerce/tests/php/src/Admin/API/MobileAppQRLoginTest.php
+++ b/plugins/woocommerce/tests/php/src/Admin/API/MobileAppQRLoginTest.php
@@ -2392,6 +2392,50 @@ class MobileAppQRLoginTest extends WC_Unit_Test_Case {
$this->assertNotEmpty( $post->get_data()['exchange_grant'] );
}
+ /**
+ * @testdox Session-status is scoped to the session recorded on the token.
+ */
+ public function test_session_status_is_scoped_to_the_recorded_session(): void {
+ $plaintext = $this->generate_token_as_admin();
+
+ wp_set_current_user( 0 );
+ $scan_data = $this->dispatch_scan( $plaintext )->get_data();
+ $recorded_session = $scan_data['session_id'];
+
+ // A second session id can also resolve to the same token record, while
+ // the record itself still names the scanned session above.
+ $other_session = wp_generate_uuid4();
+ set_transient(
+ MobileAppQRLogin::SESSION_TRANSIENT_PREFIX . hash( 'sha256', $other_session ),
+ $this->token_hash( $plaintext ),
+ MobileAppQRLogin::TOKEN_TTL
+ );
+
+ wp_set_current_user( $this->admin_id );
+ $this->assertSame( 200, $this->dispatch_approve( $plaintext, $scan_data['real_number'] )->get_status() );
+ wp_set_current_user( 0 );
+
+ $other = $this->dispatch_session_status( $other_session, $plaintext );
+ $this->assertSame( 200, $other->get_status() );
+ $this->assertSame(
+ MobileAppQRLogin::STATE_EXPIRED,
+ $other->get_data()['state'],
+ 'State is only returned for the session recorded on the token.'
+ );
+ $this->assertArrayNotHasKey(
+ 'exchange_grant',
+ $other->get_data(),
+ 'The exchange grant is only returned for the session recorded on the token.'
+ );
+
+ $recorded = $this->dispatch_session_status( $recorded_session, $plaintext );
+ $this->assertSame( MobileAppQRLogin::STATE_APPROVED, $recorded->get_data()['state'] );
+ $this->assertNotEmpty(
+ $recorded->get_data()['exchange_grant'],
+ 'The recorded session receives its grant.'
+ );
+ }
+
// -----------------------------------------------------------------------
// Availability endpoint (`/qr-login-availability`).
// -----------------------------------------------------------------------