clock = is_callable($clock) ? $clock : 'time'; } public function authenticate(WP_REST_Request $request) { $state = Akkedli_Connector_State::load(); if (!is_array($state) || !in_array($state['mode'], array('v2_pending', 'v2'), true)) { return false; } if ('v2' === $state['mode'] && $this->present($request, 'X-Akkedli-Idempotency-Key')) { return false; } $callback_url = 'v2_pending' === $state['mode'] ? $state['callback_url'] : Akkedli_V2_Callback_URL::derive(); if (!is_string($callback_url)) { return $this->invalid(); } $parts = wp_parse_url($callback_url); $path = is_array($parts) && isset($parts['path']) ? $parts['path'] : ''; if ('POST' !== $request->get_method() || Akkedli_V2_Callback_Auth::ROUTE !== $request->get_route() || !isset($_SERVER['REQUEST_URI']) || !is_string($_SERVER['REQUEST_URI']) || !hash_equals($path, $_SERVER['REQUEST_URI'])) { return $this->invalid(); } $type = $this->header($request, 'Content-Type'); $credential = $this->header($request, 'X-Akkedli-Connector-Id'); $timestamp = $this->header($request, 'X-Akkedli-Timestamp'); $request_id = $this->header($request, 'X-Akkedli-Request-Id'); $signature = $this->header($request, 'X-Akkedli-Signature'); if ('application/json' !== $type || !is_string($credential) || !hash_equals($state['credential_id'], $credential) || !is_string($timestamp) || !preg_match('/^[1-9]\d{9,10}$/D', $timestamp) || !is_string($request_id) || !preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/D', $request_id) || !is_string($signature) || !preg_match('/^v1=([A-Za-z0-9_-]{43})$/D', $signature, $match) || $this->present($request, 'X-Akkedli-Idempotency-Key')) { return $this->denied(); } $now = call_user_func($this->clock); if (!is_int($now) || abs($now - (int) $timestamp) > 300 || (string) ((int) $timestamp) !== $timestamp) { return $this->denied(); } $body = $request->get_body(); $payload = is_string($body) && '' !== $body && strlen($body) <= self::BODY_LIMIT ? Akkedli_V2_Strict_JSON::decode($body, array('type', 'challenge')) : false; if (!is_array($payload) || 'host_binding' !== $payload['type'] || !is_string($payload['challenge']) || !preg_match('/^[A-Za-z0-9_-]{43}$/D', $payload['challenge']) || !hash_equals(wp_json_encode(array('type' => 'host_binding', 'challenge' => $payload['challenge']), JSON_UNESCAPED_SLASHES), $body)) { return $this->invalid(); } try { $secret = Akkedli_V2_Crypto::base64url_decode($state['secret']); $provided = Akkedli_V2_Crypto::base64url_decode($match[1]); $challenge = Akkedli_V2_Crypto::base64url_decode($payload['challenge']); } catch (Throwable $error) { unset($error); return $this->denied(); } $canonical = implode("\n", array('AKKEDLI-WC-HOST-BINDING-CHALLENGE-V1', 'POST', $path, $timestamp, $request_id, hash('sha256', $body))); $expected = hash_hmac('sha256', $canonical, $secret, true); $challenge_value = hash_hmac('sha256', implode("\n", array( 'AKKEDLI-WC-HOST-BINDING-VALUE-V1', $credential, $callback_url)), $secret, true); if (32 !== strlen($secret) || 32 !== strlen($provided) || 32 !== strlen($challenge) || !hash_equals($expected, $provided) || !hash_equals($challenge_value, $challenge)) { return $this->denied(); } return array('kind' => 'host_binding', 'credential_id' => $credential, 'callback_url' => $callback_url, 'challenge' => $payload['challenge'], 'secret' => $secret, 'state' => $state); } private function header($request, $name) { $headers = $request->get_headers(); $key = strtolower(str_replace('-', '_', $name)); if (!isset($headers[$key]) || !is_array($headers[$key]) || 1 !== count($headers[$key])) return false; $value = $headers[$key][0]; return is_string($value) && '' !== $value && !preg_match('/[\x00-\x20\x7f,]/', $value) ? $value : false; } private function present($request, $name) { $headers = $request->get_headers(); return array_key_exists(strtolower(str_replace('-', '_', $name)), $headers); } private function invalid() { return new WP_Error('akkedli_v2_invalid_request', 'Invalid Connector request.', array('status' => 400)); } private function denied() { return new WP_Error('akkedli_v2_authentication_failed', 'Connector authentication failed.', array('status' => 401)); } }