'GET', 'callback' => array( __CLASS__, 'rest_verify' ), // Route publique par nature: c'est l'API Convrail qui l'appelle, sans session WordPress. // Sans risque: elle ne repond qu'un HMAC d'un challenge a usage unique, vivant 2 minutes, // cree par un administrateur au clic sur "Connect"; hors de cette fenetre elle repond 404. 'permission_callback' => '__return_true', 'args' => array( 'nonce' => array( 'required' => true, 'type' => 'string', 'validate_callback' => array( __CLASS__, 'is_valid_nonce' ), ), ), ) ); } public static function is_valid_nonce( $value ) { return is_string( $value ) && 1 === preg_match( '/^[a-f0-9]{64}$/', $value ); } /** * Domaine tel que le site le declare (hote de home_url, en minuscules). C'est ce domaine * qui est envoye a l'API et qui entre dans le HMAC: les deux cotes doivent coincider. */ public static function site_domain() { return strtolower( (string) wp_parse_url( home_url(), PHP_URL_HOST ) ); } public static function create_challenge() { $challenge = wp_generate_password( 64, false, false ); set_transient( self::TRANSIENT, $challenge, self::TTL ); return $challenge; } /** Appele des que register a repondu, succes ou echec: le challenge ne sert qu'une fois. */ public static function clear_challenge() { delete_transient( self::TRANSIENT ); } public static function compute_proof( $challenge, $domain, $nonce ) { return hash_hmac( 'sha256', 'convrail-verify:' . $domain . ':' . $nonce, $challenge ); } /** * Le transient n'est pas efface a la lecture: un tiers qui appellerait la route pourrait * sinon faire echouer la connexion du marchand. Lire des HMAC ne revele pas le challenge, * et l'API refuse de toute facon un challenge deja presente. */ public static function rest_verify( $request ) { $challenge = get_transient( self::TRANSIENT ); if ( ! is_string( $challenge ) || '' === $challenge ) { return new WP_Error( 'convrail_no_pending_verification', 'No pending verification.', array( 'status' => 404 ) ); } $response = new WP_REST_Response( array( 'proof' => self::compute_proof( $challenge, self::site_domain(), (string) $request->get_param( 'nonce' ) ) ), 200 ); $response->header( 'Cache-Control', 'no-store, max-age=0' ); return $response; } }