CloudflareTurnstile - viames/pair GitHub Wiki

Pair framework: CloudflareTurnstile

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.

Configuration

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=3

Keys:

  • CLOUDFLARE_TURNSTILE_SITE_KEY is the public widget key.
  • CLOUDFLARE_TURNSTILE_SECRET_KEY is required before server-side validation.
  • CLOUDFLARE_TURNSTILE_RESPONSE_FIELD defaults to the field posted by implicit Turnstile widgets.
  • CLOUDFLARE_TURNSTILE_VERIFY_URL is configurable for tests and controlled proxy setups.

Extension path

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.

Main methods

  • widgetHtml(array $options = []): string
  • scriptTag(bool $explicit = false, array $attributes = []): string
  • verifyToken(string $token, ?string $remoteIp = null, array $options = []): array
  • assertToken(string $token, ?string $remoteIp = null, array $options = []): array
  • verifyPost(array $post, ?string $remoteIp = null, array $options = []): array
  • assertPost(array $post, ?string $remoteIp = null, array $options = []): array
  • tokenFromPost(array $post): string
  • siteKeySet(): bool
  • secretKeySet(): 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.

Pair HTML form integration

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,
]);

API guard example

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.

Operational notes

  • Keep CLOUDFLARE_TURNSTILE_SECRET_KEY server-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.

Cloudflare references

See also: Integrations, Configuration file, Env, Form, API, ApiErrorResponse.

⚠️ **GitHub.com Fallback** ⚠️