Automattic\WooCommerce\Admin\API
MobileAppQRLogin::get_session_status
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() 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 );
}