$this->challengeUrl, ]; } /** * Each call produces a new unique challenge * * @return array the challenge, which is passed to the client as is */ public function createChallenge(): array { return $this->altcha->createChallenge(new CreateChallengeOptions( algorithm: $this->algorithm, cost: $this->cost, counter: random_int($this->counterMin, $this->counterMax), // @phpstan-ignore argument.type (counterMin <= counterMax is asserted in the constructor) expiresAt: Carbon::now()->getTimestamp() + $this->ttl, data: [ 'counterMax' => $this->counterMax, ], ))->toArray(); } public function verify(string $token, ?string $remoteIp): bool { try { $payload = AltchaPayload::fromBase64($token); } catch (InvalidArgumentException) { return false; // malformed } $result = $this->altcha->verifySolution(new VerifySolutionOptions($payload, $this->algorithm)); if (!$result->verified) { return false; // expired, invalid signature or invalid solution } // The library treats a missing or zero expiresAt as "never expires". // The replay protection relies on the TTL, so such challenges are never accepted. $expiresAt = $payload->challenge->parameters->expiresAt; if (!$expiresAt) { return false; } // The signature can be trusted only after the successful verification. // Not the solution: on the fast verification path the counter isn't checked. /** @var string $signature verified signature always exists */ $signature = $payload->challenge->signature; return $this->usedChallenges->markUsed($signature, $expiresAt); } }