Automattic\WooCommerce\Admin\API

MobileAppQRLogin::get_session_statuspublicWC 1.0

Mobile app polls this with the session id from /scan. Returns the current state of the underlying token, plus — when state is approved — the exchange_grant nonce required by /qr-login-exchange.

Method of the class: MobileAppQRLogin{}

No Hooks.

Returns

\WP_REST_Response|\WP_Error.

Usage

$MobileAppQRLogin = new MobileAppQRLogin();
$MobileAppQRLogin->get_session_status( $request );
$request(required)
.

MobileAppQRLogin::get_session_status() code WC 11.0.1

public function get_session_status( $request ) {
	// Defeat any intermediary cache (Cloudflare, NGINX micro-cache, OkHttp's shared
	// cache, edge proxy) that might pin this GET to its first response. Polling
	// endpoints are by definition state-bearing — every tick must see the live
	// transient. Returning a stale `scanned` response forever is exactly the
	// symptom we see if the cache pins the first hit.
	nocache_headers();

	if ( ! is_ssl() ) {
		return new \WP_Error(
			'ssl_required',
			__( 'QR login requires an HTTPS connection.', 'woocommerce' ),
			array( 'status' => 403 )
		);
	}

	$session_id     = (string) $request->get_param( 'session_id' );
	$submitted_hash = (string) $request->get_param( 'token_hash' );

	$token_hash = get_transient( self::SESSION_TRANSIENT_PREFIX . hash( 'sha256', $session_id ) );
	if ( ! is_string( $token_hash ) || '' === $token_hash ) {
		// Either the session never existed or it has expired. Either way,
		// surface as expired to the polling app.
		return $this->rest_ensure_nocache_response( array( 'state' => self::STATE_EXPIRED ) );
	}

	// Bind grant delivery to proof of token knowledge: an attacker who
	// learns the session_id alone (mobile logs, network capture, debug
	// output) cannot poll for state transitions and walk away with the
	// `exchange_grant` the moment the merchant approves. The mobile app
	// already holds the plaintext token from the QR scan — passing
	// SHA-256(token) on every poll is essentially free for it.
	// `hash_equals` for constant-time comparison; `expired` opacity so
	// we never leak whether the session_id is real or not.
	if ( ! hash_equals( $token_hash, $submitted_hash ) ) {
		return $this->rest_ensure_nocache_response( array( 'state' => self::STATE_EXPIRED ) );
	}

	if ( ! $this->check_session_status_rate_limit( $session_id ) ) {
		return new \WP_Error(
			'rate_limit_exceeded',
			__( 'Too many QR login session-status checks. Please try again later.', 'woocommerce' ),
			array( 'status' => 429 )
		);
	}

	$record = get_transient( self::TOKEN_TRANSIENT_PREFIX . $token_hash );
	if ( ! is_array( $record ) ) {
		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 );

	if ( in_array( $state, array( self::STATE_REJECTED, self::STATE_EXPIRED ), true ) ) {
		return $this->rest_ensure_nocache_response( $response );
	}

	if ( ! empty( $record['expires_at'] ) && time() >= (int) $record['expires_at'] ) {
		return $this->rest_ensure_nocache_response( array( 'state' => self::STATE_EXPIRED ) );
	}

	if ( self::STATE_APPROVED === $state && ! empty( $record['exchange_grant'] ) ) {
		$response['exchange_grant'] = (string) $record['exchange_grant'];
	}

	return $this->rest_ensure_nocache_response( $response );
}