Automattic\WooCommerce\Admin\API
MobileAppQRLogin::approve_token │ public │ WC 1.0
Merchant taps a number on wc-admin. Server validates against the stored real number with hash_equals() (constant-time). Correct → approved + mints exchange_grant. Wrong → rejected (terminal, security event logged). One-strike: no retry.
Method of the class: MobileAppQRLogin{}
No Hooks.
Returns
\WP_REST_Response|\WP_Error.
Usage
$MobileAppQRLogin = new MobileAppQRLogin(); $MobileAppQRLogin->approve_token( $request );
- $request(required)
- .
MobileAppQRLogin::approve_token() MobileAppQRLogin::approve token code WC 11.0.1
public function approve_token( $request ) {
$user_id = get_current_user_id();
if ( ! $this->check_approve_rate_limit( $user_id ) ) {
return new \WP_Error(
'rate_limit_exceeded',
__( 'Too many QR login approval attempts. Please try again later.', 'woocommerce' ),
array( 'status' => 429 )
);
}
$token = (string) $request->get_param( 'token' );
$token_hash = hash( 'sha256', $token );
$key = self::TOKEN_TRANSIENT_PREFIX . $token_hash;
$record = get_transient( $key );
if ( ! is_array( $record ) ) {
return new \WP_Error(
'invalid_token',
__( 'Invalid or expired QR login token.', 'woocommerce' ),
array( 'status' => 401 )
);
}
$approval_claim_expires_at = ! empty( $record['challenge']['expires_at'] )
? (int) $record['challenge']['expires_at']
: ( isset( $record['expires_at'] ) ? (int) $record['expires_at'] : time() + self::TOKEN_TTL );
if ( ! $this->claim_token_for_approval( $token_hash, $approval_claim_expires_at ) ) {
return new \WP_Error(
'qr_login_approval_in_progress',
__( 'This QR login session is already being approved.', 'woocommerce' ),
array( 'status' => 409 )
);
}
// Re-read after acquiring the database claim in case another request
// approved, rejected, or expired the challenge while this one was waiting.
$record = get_transient( $key );
if ( ! is_array( $record ) ) {
$this->release_token_approval_claim( $token_hash );
return new \WP_Error(
'invalid_token',
__( 'Invalid or expired QR login token.', 'woocommerce' ),
array( 'status' => 401 )
);
}
// Same cross-user defense as get_status — only the user that minted
// the token can approve it.
if ( ! isset( $record['user_id'] ) || (int) $record['user_id'] !== (int) $user_id ) {
$this->release_token_approval_claim( $token_hash );
return new \WP_Error(
'invalid_token',
__( 'Invalid or expired QR login token.', 'woocommerce' ),
array( 'status' => 401 )
);
}
if ( ! empty( $record['expires_at'] ) && time() >= (int) $record['expires_at'] ) {
$record['state'] = self::STATE_EXPIRED;
$record['state_at'] = time();
set_transient( $key, $record, 60 );
$this->release_token_approval_claim( $token_hash );
return new \WP_Error(
'qr_login_expired',
__( 'The QR login challenge has expired. Please generate a new code.', 'woocommerce' ),
array( 'status' => 410 )
);
}
$current_state = isset( $record['state'] ) ? (string) $record['state'] : self::STATE_PENDING;
if ( self::STATE_SCANNED !== $current_state ) {
$this->release_token_approval_claim( $token_hash );
return new \WP_Error(
'qr_login_not_scanned',
__( 'This QR login session is not waiting for approval.', 'woocommerce' ),
array( 'status' => 409 )
);
}
// Challenge expiry — normally 90 s after scan, capped by token expiry.
if ( ! empty( $record['challenge']['expires_at'] ) && time() > (int) $record['challenge']['expires_at'] ) {
$record['state'] = self::STATE_EXPIRED;
$record['state_at'] = time();
set_transient( $key, $record, 60 );
$this->release_token_approval_claim( $token_hash );
return new \WP_Error(
'qr_login_expired',
__( 'The QR login challenge has expired. Please generate a new code.', 'woocommerce' ),
array( 'status' => 410 )
);
}
$choice = (string) $request->get_param( 'choice' );
$real = isset( $record['challenge']['real'] ) ? (string) $record['challenge']['real'] : '';
// Constant-time compare. Defends against PHP string-comparison fast
// paths that can leak prefix-matching info via timing.
if ( '' === $real || ! hash_equals( $real, $choice ) ) {
$record['state'] = self::STATE_REJECTED;
$record['state_at'] = time();
set_transient( $key, $record, 60 );
wc_get_logger()->warning(
'QR login number-match rejected — wrong choice submitted',
array(
'source' => 'qr-login-security',
'user_id' => (int) $user_id,
'ip' => $this->get_client_ip(),
'device' => isset( $record['challenge']['device'] ) ? $record['challenge']['device'] : array(),
)
);
$this->release_token_approval_claim( $token_hash );
return rest_ensure_response( array( 'state' => self::STATE_REJECTED ) );
}
$record['state'] = self::STATE_APPROVED;
$record['state_at'] = time();
$record['exchange_grant'] = bin2hex( random_bytes( self::EXCHANGE_GRANT_BYTES ) );
$ttl = max(
1,
isset( $record['expires_at'] ) ? (int) $record['expires_at'] - time() : self::CHALLENGE_TTL_SECONDS
);
set_transient( $key, $record, $ttl );
$this->release_token_approval_claim( $token_hash );
return rest_ensure_response( array( 'state' => self::STATE_APPROVED ) );
}