Introduction
Cloudflare Turnstile is a user-friendly, privacy-preserving alternative to traditional CAPTCHAs. It provides robust spam and bot protection without forcing legitimate users to solve frustrating puzzles.
This guide covers the core terminology, how to generate your API keys, and two methods for integrating Turnstile into your Drupal site: using the contributed module (recommended) and building a manual integration.
Part 1: Understanding the Terminology
Before implementing Turnstile, it helps to understand how the process works. The implementation requires two distinct phases:
- Client-Side Rendering (The Browser): The Turnstile widget must run on the user’s device. The JavaScript evaluates the browser environment and user behavior. If it determines the user is human, it generates a unique string of text called a “token.”
- Server-Side Validation (The Backend): Your Drupal server cannot just trust the browser. When the form is submitted, Drupal takes the token and makes a secure, server-to-server API call to Cloudflare. If Cloudflare replies with
success: true, Drupal processes the form.
Part 2: Get Your Cloudflare Turnstile Keys
Regardless of which method you choose, you need API keys.
- Log in to your Cloudflare Dashboard.
- In the left-hand sidebar, navigate to Turnstile.
- Click the Add widget button.
- Enter a Site Name (for your reference) and add your website’s Domain.
- Choose your Widget Mode (the default Managed mode is highly recommended).
- Click Create.
- Keep this tab open—you will need the Site Key and Secret Key for the steps below.
Part 3: Method A – Using the Drupal Module (Recommended)
This is the fastest and most maintainable way to add Turnstile to a Drupal 10 or 11 site. Turnstile acts as an extension of the standard Drupal CAPTCHA module.
1. Install the Modules
Run the following Composer commands in your project root:
Bash
composer require drupal/captcha
composer require drupal/turnstile
2. Enable the Modules
Use Drush to enable both modules:
Bash
drush en captcha turnstile -y
3. Configure the Keys
- The cloudflare turnstile module works with the drupal key module making it easier to handle credentials. Go to Configuration > System > Keys (/admin/config/system/keys)
- Create a new key and switch the key type to Authentication (Multivalue) and ensure your JSON includes “site_key” and “secret_key”, for example:
{ “site_key”: “0x000…”, “secret_key”: “0x000…” } - In your Drupal admin, go to Configuration > People > CAPTCHA module settings > Turnstile (
/admin/config/people/captcha/turnstile). - Select the key you created for Cloudflare Turnstile
- Click Save configuration.
4. Apply Turnstile to Your Forms
- Go to Configuration > People > CAPTCHA module settings (
/admin/config/people/captcha). - Change the Challenge type to Turnstile.
- Click Save configuration.
- Navigate to Captcha Points Configuration > People > CAPTCHA module settings > Captcha Points. For every form you want to protect (e.g.,
user_login_form,contact_message_feedback_form, webform_submission_turnstile_test_form_add_form) enable captcha and specifically turnstile
Part 4: Method B – Manual Integration (Custom Module)
If you prefer not to use contributed modules, you can integrate Turnstile manually by writing a small custom module.
Module Directory and Info File
Navigate to your Drupal installation’s custom module directory and create a home for your new extension:
Directory: docroot/modules/custom/custom_turnstile/
Inside this folder, create the module’s primary definition file.
File: custom_turnstile.info.yml
name: 'Custom Cloudflare Turnstile'
type: module
description: 'Integrates Cloudflare Turnstile bot protection into Drupal forms.'
package: Custom
core_version_requirement: ^10 || ^11
dependencies:
Define Configuration Schema
To store our backend authentication keys cleanly and safely within Drupal’s configuration management layer,
we must supply a schema layout.
File: config/schema/custom_turnstile.schema.yml
custom_turnstile.settings:
type: config_object
label: 'Custom Cloudflare Turnstile settings'
mapping:
site_key:
type: string
label: 'Cloudflare Turnstile Site Key'
secret_key:
type: string
label: 'Cloudflare Turnstile Secret Key'
Create the Administrative Settings UI
We need a custom settings form that allows administrators to cleanly input their Turnstile credentials provided
inside the Cloudflare Dashboard console.
File: src/Form/TurnstileSettingsForm.php
<?php
namespace Drupal\custom_turnstile\Form;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
class TurnstileSettingsForm extends ConfigFormBase {
protected function getEditableConfigNames() {
return ['custom_turnstile.settings'];
}
public function getFormId() {
return 'custom_turnstile_settings_form';
}
public function buildForm(array $form, FormStateInterface $form_state) {
$config = $this->config('custom_turnstile.settings');
$form['site_key'] = [
'#type' => 'textfield',
'#title' => $this->t('Site Key'),
'#default_value' => $config->get('site_key'),
'#required' => TRUE,
];
$form['secret_key'] = [
'#type' => 'textfield',
'#title' => $this->t('Secret Key'),
'#default_value' => $config->get('secret_key'),
'#required' => TRUE,
];
return parent::buildForm($form, $form_state);
}
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->config('custom_turnstile.settings')
->set('site_key', $form_state->getValue('site_key'))
->set('secret_key', $form_state->getValue('secret_key'))
->save();
parent::submitForm($form, $form_state);
}
}
To expose this administrative form inside Drupal’s routing architecture, map a reliable path endpoint:
File: custom_turnstile.routing.yml
custom_turnstile.settings:
path: '/admin/config/services/turnstile'
defaults:
_form: '\Drupal\custom_turnstile\Form\TurnstileSettingsForm'
_title: 'Cloudflare Turnstile Settings'
requirements:
_permission: 'administer site configuration'
Register Cloudflare JavaScript with Asset License
Modern iterations of Drupal require explicit remote metadata licenses when pointing asset pipelines directly at
a remote CDN. Neglecting this mapping produces severe system faults. Create your definitions file
Accordingly:
File: custom_turnstile.libraries.yml
turnstile.api:
remote: https://developers.cloudflare.com/turnstile
version: 1.x
license:
name: Public Domain
url: https://developers.cloudflare.com/turnstile
gpl-compatible: true
js:
https://challenges.cloudflare.com/turnstile/v0/api.js: { type: external, attributes: { async: true, defer: true } }
Architect a Reusable Render Element Plugin
By creating a dedicated @FormElement plugin object, we completely avoid inline hardcoding hacks. This
structures the widget cleanly so that any custom form on your site can instantly render and auto-validate
challenges by adding a single line element parameter: ‘#type’ => ‘turnstile’.
File: src/Element/Turnstile.php
<?php
namespace Drupal\custom_turnstile\Element;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element\FormElement;
/**
* Provides a Cloudflare Turnstile form element.
*
* @FormElement("turnstile")
*/
class Turnstile extends FormElement {
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return [
'#input' => TRUE,
'#process' => [
[$class, 'processTurnstile'],
],
'#element_validate' => [
[$class, 'validateTurnstile'],
],
'#theme_wrappers' => ['form_element'],
];
}
/**
* Processes the Turnstile element to render the widget Markup.
*/
public static function processTurnstile(&$element, FormStateInterface $form_state, &$complete_form) {
$config = \Drupal::config('custom_turnstile.settings');
$site_key = $config->get('site_key');
// Attach external cloudflare script.
$element['#attached']['library'][] = 'custom_turnstile/turnstile.api';
// Output the div wrapper that Cloudflare's API detects automatically.
$element['#markup'] = '<div class="cf-turnstile" data-sitekey="' . htmlspecialchars($site_key, ENT_QUOTES, 'UTF-8') . '"></div>';
return $element;
}
/**
* Server-side validation of the Turnstile token response.
*/
public static function validateTurnstile(&$element, FormStateInterface $form_state, &$complete_form) {
// Cloudflare automatically appends 'cf-turnstile-response' to POST data.
$user_input = $form_state->getUserInput();
$token = $user_input['cf-turnstile-response'] ?? '';
if (empty($token)) {
$form_state->setError($element, t('Please complete the security challenge.'));
return;
}
$config = \Drupal::config('custom_turnstile.settings');
$secret_key = $config->get('secret_key');
// Call Cloudflare Siteverify API.
try {
$client = \Drupal::httpClient();
$response = $client->post('https://challenges.cloudflare.com/turnstile/v0/siteverify', [
'form_params' => [
'secret' => $secret_key,
'response' => $token,
'remoteip' => \Drupal::request()->getClientIp(),
],
]);
$data = json_decode($response->getBody()->getContents(), TRUE);
if (empty($data['success'])) {
$form_state->setError($element, t('Security validation failed. Please try again.'));
}
} catch (\Exception $e) {
\Drupal::logger('custom_turnstile')->error('Turnstile verification error: @message', ['@message' => $e->getMessage()]);
$form_state->setError($element, t('Unable to validate security challenges at this time.'));
}
}
}
Target and Intercept Core Forms
To deploy our element wrapper onto core forms dynamically (such as login pages, user generation screens,
and password recovery setups), implement standard application level form hook interception.
File: custom_turnstile.module
<?php
use Drupal\Core\Form\FormStateInterface;
/**
* Implements hook_form_alter().
*/
function custom_turnstile_form_alter(&$form, FormStateInterface $form_state, $form_id) {
// Target forms (e.g., User Login, User Registration, User Password Reset).
$target_forms = [
'user_login_form',
'user_register_form',
'webform_submission_turnstile_test_form_add_form',
];
if (in_array($form_id, $target_forms)) {
// Inject our custom Turnstile render element right above the form actions (submit buttons).
$form['captcha_turnstile'] = [
'#type' => 'turnstile',
'#weight' => 90, // Position it right above the submit button
];
}
}
Module Initialization
Execute these terminal commands to initialize configurations and reset system caches:
# Enable the new module via Drush
drush en custom_turnstile -y
# Rebuild definitions to incorporate updates smoothly
drush cr
Post-Activation Steps: Navigate directly to /admin/config/services/turnstile inside your
management window. Plug your respective Cloudflare public and private API credentials in, hit save, and visit
your site’s target forms in an incognito window to verify successful security challenge operations.
Testing Your Implementation
Regardless of the method you choose, always test your forms.
- Open an incognito/private browsing window.
- Navigate to one of your protected forms.
- Verify the Turnstile widget renders correctly (a loading circle followed by a green checkmark).
- Submit the form to ensure validation passes.
- (For manual setups) Try submitting the form before the widget loads or with JavaScript disabled to ensure your validation properly blocks the submission.