clock = is_callable($clock) ? $clock : 'time'; } public function authenticate(WP_REST_Request $request) { $state = Akkedli_V2_Session_State::load(); if (!is_array($state)) return false; if (!empty($state['revoke_requested'])) return $this->denied(); $now = call_user_func($this->clock); if (!is_int($now) || strtotime($state['expires_at']) <= $now) return $this->denied(); $path = wp_parse_url($state['callback_url'], PHP_URL_PATH); if ('POST' !== $request->get_method() || Akkedli_V2_Callback_Auth::ROUTE !== $request->get_route() || !is_string($path) || !isset($_SERVER['REQUEST_URI']) || !is_string($_SERVER['REQUEST_URI']) || !hash_equals($path, $_SERVER['REQUEST_URI'])) { return $this->invalid(); } $type = $this->header($request, 'Content-Type'); $session = $this->header($request, 'X-Akkedli-Session'); $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($session) || !hash_equals($state['session'], $session) || !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)) { return $this->denied(); } if (abs($now - (int) $timestamp) > 300 || (string) ((int) $timestamp) !== $timestamp) return $this->denied(); $body = $request->get_body(); $payload = is_string($body) && strlen($body) <= self::LIMIT ? Akkedli_V2_Strict_JSON::decode( $body, array('type', 'session', 'challenge')) : false; if (!is_array($payload) || 'connection_session' !== $payload['type'] || !is_string($payload['session']) || !hash_equals($state['session'], $payload['session']) || !is_string($payload['challenge']) || !preg_match('/^[A-Za-z0-9_-]{43}$/D', $payload['challenge']) || !hash_equals(wp_json_encode(array('type' => 'connection_session', 'session' => $session, 'challenge' => $payload['challenge']), JSON_UNESCAPED_SLASHES), $body)) return $this->invalid(); try { $secret = Akkedli_V2_Crypto::base64url_decode($state['bootstrap_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-SESSION-REQUEST-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-SESSION-PROOF-V1', $session, $state['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' => 'connection_session', 'session' => $session, 'callback_url' => $state['callback_url'], 'challenge' => $payload['challenge'], 'secret' => $secret); } 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 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)); } }