clientId = $clientId; $this->scope = $scope; $this->redirect = $redirect; $this->state = self::generateState($clientId); [$this->verifier, $this->verifierHash] = self::generateVerifier(); } public function getClientId(): TwitterClientId { return $this->clientId; } public function getScope(): array { return $this->scope; } public function getRedirectUri(): string { return $this->redirect; } public function getState(): string { return $this->state; } public function getVerifier(): string { return $this->verifier; } public function getVerifierHash(): string { return $this->verifierHash; } public function getUri(): string { return self::AUTHORIZE . '?' . http_build_query([ 'response_type' => 'code', 'client_id' => $this->clientId->getClientId(), 'redirect_uri' => $this->redirect, 'scope' => implode(' ', $this->scope), 'state' => $this->state, 'code_challenge' => $this->verifierHash, 'code_challenge_method' => 'S256', ], '', null, PHP_QUERY_RFC3986); } public static function generateVerifier(): array { $verifier = XString::random(self::VERIFIER_LENGTH); return [ $verifier, Serialiser::uriBase64()->serialise(hash('sha256', $verifier, true)), ]; } private static function currentStateTime(): int { return time() - self::STATE_EPOCH; } public static function generateState(TwitterClientId $clientId): string { $rng = XString::random(self::STATE_RNG_LENGTH); $time = self::currentStateTime(); $string = $rng . ':' . (string)$time; $hash = hash_hmac('sha256', $string, $clientId->getClientSecret(), true); $time = Serialiser::base62()->serialise($time); $hash = Serialiser::uriBase64()->serialise($hash); return $rng . '.' . $time . '.' . $hash; } public static function verifyState(TwitterClientId $clientId, string $state): bool { $parts = explode('.', $state, 4); if(count($parts) !== 3) return false; $rng = $parts[0]; if(strlen($rng) !== self::STATE_RNG_LENGTH) return false; $currentTime = self::currentStateTime(); $time = Serialiser::base62()->deserialise($parts[1]); if($currentTime < $time || $currentTime >= ($time + self::STATE_TOLERANCE)) return false; $hash = Serialiser::uriBase64()->deserialise($parts[2]); if(strlen($hash) !== 32) return false; $string = $rng . ':' . (string)$time; $realHash = hash_hmac('sha256', $string, $clientId->getClientSecret(), true); return hash_equals($realHash, $hash); } }