CloudflareTurnstile - viames/pair GitHub Wiki
Pair\Services\CloudflareTurnstile is a small helper for Cloudflare Turnstile widgets and server-side token validation.
It keeps the Pair core free from provider SDK dependencies. The helper uses cURL directly and only depends on the standard Turnstile browser script plus Cloudflare's Siteverify endpoint.
Turnstile can protect public write flows such as contact forms, registration, newsletter signup, and unauthenticated API endpoints. It does not require the application to use Cloudflare as a CDN or reverse proxy.
CLOUDFLARE_TURNSTILE_SITE_KEY=
CLOUDFLARE_TURNSTILE_SECRET_KEY=
CLOUDFLARE_TURNSTILE_VERIFY_URL="https://challenges.cloudflare.com/turnstile/v0/siteverify"
CLOUDFLARE_TURNSTILE_RESPONSE_FIELD=cf-turnstile-response
CLOUDFLARE_TURNSTILE_TIMEOUT=10
CLOUDFLARE_TURNSTILE_CONNECT_TIMEOUT=3Keys:
-
CLOUDFLARE_TURNSTILE_SITE_KEYis the public widget key. -
CLOUDFLARE_TURNSTILE_SECRET_KEYis required before server-side validation. -
CLOUDFLARE_TURNSTILE_RESPONSE_FIELDdefaults to the field posted by implicit Turnstile widgets. -
CLOUDFLARE_TURNSTILE_VERIFY_URLis configurable for tests and controlled proxy setups.
Pair v4 integrations should be registered explicitly. An application or optional pair/cloudflare-turnstile package can expose Turnstile through a project-owned adapter key:
use Pair\Core\Application;
use Pair\Services\CloudflareTurnstile;
$app = Application::getInstance();
$app->setAdapter('anti_bot', new CloudflareTurnstile());
$turnstile = $app->adapter('anti_bot', CloudflareTurnstile::class);The framework does not reserve a core adapter key for anti-bot providers yet. This keeps the integration optional and lets projects switch to another provider without changing Pair's core contracts.
widgetHtml(array $options = []): stringscriptTag(bool $explicit = false, array $attributes = []): stringverifyToken(string $token, ?string $remoteIp = null, array $options = []): arrayassertToken(string $token, ?string $remoteIp = null, array $options = []): arrayverifyPost(array $post, ?string $remoteIp = null, array $options = []): arrayassertPost(array $post, ?string $remoteIp = null, array $options = []): arraytokenFromPost(array $post): stringsiteKeySet(): boolsecretKeySet(): bool
Use verify* when the caller wants to inspect the provider response. Use assert* when failure should raise a PairException with ErrorCodes::CLOUDFLARE_TURNSTILE_ERROR.
Render the script once on the page and add the widget inside the form:
use Pair\Services\CloudflareTurnstile;
$turnstile = new CloudflareTurnstile();
?>
<form method="post" action="/contact/send">
<input type="email" name="email" required>
<textarea name="message" required></textarea>
<?= $turnstile->widgetHtml([
'theme' => 'auto',
'action' => 'contact_form',
]) ?>
<button type="submit">Send</button>
</form>
<?= $turnstile->scriptTag() ?>widgetHtml(...) renders an implicit Turnstile widget. Options are mapped to data-* attributes, so theme becomes data-theme and action becomes data-action.
Server-side validation must still run after submit:
use Pair\Services\CloudflareTurnstile;
$turnstile = new CloudflareTurnstile();
$turnstile->assertPost($_POST, $_SERVER['REMOTE_ADDR'] ?? null, [
'idempotency_key' => $_SERVER['HTTP_X_REQUEST_ID'] ?? null,
]);For public JSON endpoints, validate the Turnstile token before doing writes:
use Pair\Http\JsonResponse;
use Pair\Services\CloudflareTurnstile;
$payload = json_decode((string)file_get_contents('php://input'), true) ?: [];
$turnstile = new CloudflareTurnstile();
$turnstile->assertToken(
(string)($payload['turnstile_token'] ?? ''),
$_SERVER['REMOTE_ADDR'] ?? null,
['idempotency_key' => $_SERVER['HTTP_X_REQUEST_ID'] ?? null]
);
// Continue with the public write only after the token is valid.
return new JsonResponse(['ok' => true]);In production controllers, catch PairException through the normal Pair error path or convert it into the endpoint's standard ApiErrorResponse.
- Keep
CLOUDFLARE_TURNSTILE_SECRET_KEYserver-side only. - Always validate tokens server-side; the browser widget alone is not authoritative.
- Treat tokens as single-use and short-lived.
- Pass the remote IP when available.
- Use an idempotency key for retryable form/API submissions.
- Store provider results only when the project has a clear audit need.
- Do not log the full response token.
See also: Integrations, Configuration file, Env, Form, API, ApiErrorResponse.