Search by

vuillaume-agency / symfony-turnstile

VuillaumeAgency

Symfony bundle for Cloudflare Turnstile - a privacy-friendly, GDPR-compliant CAPTCHA alternative. Drop-in reCAPTCHA replacement with zero user friction.

Package info

github.com/vuillaume-agency/symfony-turnstile

pkg:composer/vuillaume-agency/symfony-turnstile

Statistics

Installs: 10 729

Dependents: 0

Suggesters: 0

Stars: 4

Open Issues: 0

v1.2.1 2026-10-10 17:08 UTC

This package is auto-updated.

Last update: 2026-10-10 17:09:35 UTC


README

CI Minimum PHP Version Minimum Symfony Version License

A Symfony bundle to integrate Cloudflare Turnstile on your forms. Turnstile is a privacy-preserving alternative to CAPTCHA that doesn't require user interaction.

Features

  • Zero user friction — No puzzles, no clicking on traffic lights
  • Login page covered — One option protects form_login and custom login-form authenticators, something a form field cannot do (see Protecting the login form). As of October 2026 we found no other Symfony Turnstile bundle doing it
  • GDPR-friendly — No cookie consent required, privacy-first design
  • 22 languages included — All major European languages supported
  • Symfony 8 ready — Full support for Symfony 7.4 LTS and 8.x
  • Customizable messages — Override error messages via form options or translations
  • Easy theming — Light, dark, or auto theme support
  • Fully tested — PHPUnit tests and PHPStan static analysis

Quick Start

composer require vuillaume-agency/symfony-turnstile
# .env
TURNSTILE_KEY="your-site-key"
TURNSTILE_SECRET="your-secret-key"
// In your form
->add('captcha', TurnstileType::class)

Get your free keys at Cloudflare Dashboard.

Why Turnstile over reCAPTCHA?

Feature Turnstile reCAPTCHA
Usually invisible (no puzzles) Yes Yes (v3 only)
GDPR compliant by design Yes No (requires consent)
Uses cookies No Yes
Data used for advertising No Unclear
Script size ~30KB ~80KB
Free unlimited Yes Yes (with limits)

GDPR considerations

For European websites, reCAPTCHA poses significant compliance challenges:

  • Cookies: reCAPTCHA sets cookies that require user consent under GDPR
  • Data transfer: User data is sent to Google servers in the US
  • Consent required: Must obtain explicit consent before loading reCAPTCHA
  • Fines: French DPA (CNIL) has fined companies for improper reCAPTCHA implementation

Turnstile is designed with privacy in mind:

  • No cookies: Doesn't set any cookies, no consent banner needed
  • Minimal data: Only collects what's necessary for bot detection (IP, TLS fingerprint)
  • No advertising: Cloudflare explicitly prohibits using collected data for ads or tracking
  • GDPR-ready: Can be used without additional consent mechanisms

About this fork

Originally a fork of pixelopen/cloudflare-turnstile-bundle by Pixel Développement.

What's new:

  • Password login protection (protect_password_login): the upstream bundle, like every Symfony Turnstile bundle we know of, stops at the form field; the login page needs a hook in the security system, which 1.2 provides
  • Symfony 7.4 LTS and 8.x (both bundles support Symfony 8 now; this one targets 7.4 LTS and up)
  • 22 languages for error messages
  • Customizable error messages via form options
  • disable_submit_until_verified option to prevent premature form submission
  • PHP 8.2+ with modern features (readonly, constructor promotion)
  • Active maintenance and community contributions welcome

Requirements

Requirement Version
PHP >= 8.2
Symfony >= 7.4

Installation

Step 1: Install the package

composer require vuillaume-agency/symfony-turnstile

Step 2: Register the bundle

Add the bundle to your config/bundles.php:

return [
    // ...
    VuillaumeAgency\TurnstileBundle\VuillaumeAgencyTurnstileBundle::class => ['all' => true],
];

Step 3: Add your Cloudflare credentials

Get your keys from the Cloudflare Dashboard and add them to your .env file:

TURNSTILE_KEY="your-site-key"
TURNSTILE_SECRET="your-secret-key"

That's it! The bundle automatically reads from these environment variables.

Optional: Custom configuration

If you need to customize the bundle, create config/packages/vuillaume_agency_turnstile.yaml:

vuillaume_agency_turnstile:
    key: '%env(TURNSTILE_KEY)%'      # Default: reads from TURNSTILE_KEY env var
    secret: '%env(TURNSTILE_SECRET)%' # Default: reads from TURNSTILE_SECRET env var
    enable: true                      # Default: true
    disable_submit_until_verified: false  # Default: false
Option Type Default Description
key string %env(TURNSTILE_KEY)% Your Turnstile site key (public)
secret string %env(TURNSTILE_SECRET)% Your Turnstile secret key (private)
enable boolean true Enable/disable validation
disable_submit_until_verified boolean false Disable submit buttons until Turnstile verification completes

Usage

Adding Turnstile to a form

Use TurnstileType in your form builder:

<?php

namespace App\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\FormBuilderInterface;
use VuillaumeAgency\TurnstileBundle\Type\TurnstileType;

class ContactType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('email', EmailType::class)
            ->add('message', TextareaType::class)
            ->add('captcha', TurnstileType::class, [
                'label' => false,
            ])
            ->add('submit', SubmitType::class);
    }
}

Widget customization

Customize the Turnstile widget appearance:

->add('captcha', TurnstileType::class, [
    'label' => false,
    'attr' => [
        'data-theme' => 'dark',      // 'light', 'dark', or 'auto'
        'data-size' => 'compact',    // 'normal' or 'compact'
        'data-action' => 'contact',  // Custom action name for analytics
    ],
])

See Cloudflare Turnstile documentation for all available options.

Disable submit until verified

To prevent form submission before Turnstile completes, enable automatic submit button disabling:

# config/packages/vuillaume_agency_turnstile.yaml
vuillaume_agency_turnstile:
    disable_submit_until_verified: true

When enabled:

  • Submit buttons are disabled on page load (server-side)
  • JavaScript enables them after Turnstile verification succeeds
  • Buttons are re-disabled if verification expires or fails
  • Nothing is disabled when the bundle itself is off (enable: false), since the widget is not rendered

The widget declares its data-callback handlers as global functions named after the widget id. They are defined by the inline script rendered with the widget, before Turnstile renders it, so no code of yours is needed and no other script may redefine them.

This improves UX by preventing users from clicking submit before the challenge is ready.

Important: This feature requires using SubmitType in your form builder. Raw HTML <button> elements in Twig templates will not be affected.

use Symfony\Component\Form\Extension\Core\Type\SubmitType;

$builder
    ->add('captcha', TurnstileType::class)
    ->add('submit', SubmitType::class, ['label' => 'Send']);

Custom error messages

Pass custom messages directly as form options:

->add('captcha', TurnstileType::class, [
    'label' => false,
    'missing_response_message' => 'Please verify you are human.',
    'verification_failed_message' => 'Verification failed, please try again.',
])

Or override translations in translations/validators.{locale}.yaml:

turnstile.missing_response: Please verify you are human.
turnstile.verification_failed: Verification failed. Please try again.

Protecting the login form

Symfony's form_login reads the login POST itself, outside any form type, so a Turnstile field in a form class does not apply to the login page. Since 1.2 one option covers it:

# config/packages/vuillaume_agency_turnstile.yaml
vuillaume_agency_turnstile:
    protect_password_login: true

Then render the widget in your login template, above the submit button:

{{ turnstile_widget({'data-action': 'login'}) }}

The token is verified while the passport is checked, after the CSRF token and before the account is looked up, so a refused challenge reveals nothing about the account. A refusal counts as a failed attempt for login_throttling. The check is skipped when enable is false, as for form fields.

What it covers: form_login and every authenticator extending AbstractLoginFormAuthenticator (the one make:security:form-login and the documentation use). What it leaves alone: http_basic, json_login, login_link, remember_me, OAuth and Security::login(), so API clients keep working. A login written outside the Security component (a controller checking the password itself) does not go through the passport: use the TurnstileType field on that form instead.

One token, one verification. If your project already verifies cf-turnstile-response in a listener or an authenticator, remove it when you turn the option on: a Turnstile token is accepted once.

The message shown on refusal is password_login_message (default: "The security check failed. Please try again."), translated through the security domain (error.messageKey|trans(error.messageData, 'security') in the template that make:security:form-login generates).

The option acts through symfony/security-http, which symfony/security-bundle installs. In a project without it there is no password login to protect, and the option has no effect. It is off by default in 1.x and will be on by default in 2.0.

Translations

The bundle provides error messages in 22 European languages:

Language Code Language Code
English en Hungarian hu
French fr Swedish sv
Spanish es Danish da
German de Finnish fi
Italian it Norwegian nb
Portuguese pt Greek el
Dutch nl Czech cs
Polish pl Slovak sk
Romanian ro Slovenian sl
Bulgarian bg Lithuanian lt
Croatian hr Latvian lv
Ukrainian uk Estonian et
Turkish tr

Migrating from pixelopen/cloudflare-turnstile-bundle

This bundle is a modernized fork. Here's how to migrate:

Why migrate: Symfony 7.4/8 support, 22 languages, customizable messages, and since 1.2 the login page, which no TurnstileType field can cover because form_login handles the POST itself. If you hit pixelopen/cloudflare-turnstile-bundle#2 on your login form, protect_password_login is the fix.

Step 1: Replace the package

composer remove pixelopen/cloudflare-turnstile-bundle
composer require vuillaume-agency/symfony-turnstile

Step 2: Update bundles.php (this is probably automatically done by the composer remove and require)

// config/bundles.php
// Remove:
// PixelOpen\CloudflareTurnstileBundle\PixelOpenCloudflareTurnstileBundle::class => ['all' => true],

// Add:
VuillaumeAgency\TurnstileBundle\VuillaumeAgencyTurnstileBundle::class => ['all' => true],

Step 3: Delete old config

rm config/packages/pixel_open_cloudflare_turnstile.yaml

Step 4: Update imports in your code

Find and replace in your project:

Old New
PixelOpen\CloudflareTurnstileBundle\Type\TurnstileType VuillaumeAgency\TurnstileBundle\Type\TurnstileType
PixelOpen\CloudflareTurnstileBundle\Validator\CloudflareTurnstile VuillaumeAgency\TurnstileBundle\Validator\CloudflareTurnstile
# Quick find/replace with sed (Linux/macOS)
find src -name "*.php" -exec sed -i '' 's/PixelOpen\\CloudflareTurnstileBundle/VuillaumeAgency\\TurnstileBundle/g' {} +

Step 5: Clear cache

php bin/console cache:clear

Migrating from reCAPTCHA

Switching from Google reCAPTCHA is straightforward:

  1. Remove your reCAPTCHA bundle and configuration
  2. Install this bundle (see Installation)
  3. Replace RecaptchaType with TurnstileType in your forms
  4. Remove reCAPTCHA from your cookie consent banner
  5. Update your privacy policy (simpler now!)

No changes needed in your controllers — validation works the same way.

Testing

During development, use Cloudflare's test credentials instead of real keys. This is the recommended approach as it keeps validation active while providing predictable behavior.

Test site keys

Site key Behavior
1x00000000000000000000AA Always passes (recommended)
2x00000000000000000000AB Always blocks
3x00000000000000000000FF Forces an interactive challenge

Test secret keys

Secret key Behavior
1x0000000000000000000000000000000AA Always passes (recommended)
2x0000000000000000000000000000000AA Always fails
3x0000000000000000000000000000000AA Returns "token already spent"

Example dev configuration

# .env.local (for development)
TURNSTILE_KEY="1x00000000000000000000AA"
TURNSTILE_SECRET="1x0000000000000000000000000000000AA"

Alternative: Disabling validation

You can also disable validation entirely, but using test keys is preferred as it keeps your code paths consistent between environments:

# config/packages/dev/vuillaume_agency_turnstile.yaml
vuillaume_agency_turnstile:
    enable: false

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

Please ensure tests pass (vendor/bin/phpunit) and code follows standards (vendor/bin/php-cs-fixer fix).

License

The MIT License (MIT). See LICENSE for more information.