Developer Diaries: Comments and Cloudflare Turnstile

I stumbled upon Cloudflare Turnstile when I had to log into my GenerateBlocks account. On previous login attempts, GenerateBlocks would send a code via email that you would then have to enter. Super inconvenient, especially if you can’t access the purchaser’s email account. So I was pleasantly surprised when I saw GenerateBlocks change up their bot protection.

GenerateBlocks Login Page With Cloudflare Turnstile
GenerateBlocks Login Page With Cloudflare Turnstile

So I did some digging. It turns out the login form is using Cloudflare Turnstile to validate its users.

What is Turnstile?

Cloudflare Turnstile is an alternative to a CAPTCHA, and seems to be competing directly with reCAPTCHA 3. It can be embedded in forms, logins, or any place you need to validate that someone is, in fact, human.

Cloudflare’s CDN already offers its customers a Challenge page if the visitor seems spammy or malicious. This same challenge now comes in widget form without having to use Cloudflare’s CDN. This means that Turnstile is a free solution.

The widget outlays a challenge, often without interaction, for the end user. This challenge is adaptable. Meaning if one challenge is not working well, then the challenge can be updated without any code modifications on your end. So it’s a one and done implementation, in theory.

Opting in to Turnstile

It turns out that if you have a Cloudflare account, you have a new Turnstile tab. Note the beta tag.

Turnstile (Beta) Tab in Cloudflare Account Settings
Turnstile (Beta) Tab in Cloudflare Account Settings

From there, you can set up a site that will utilize the Turnstile service.

Turnstile Getting Started Screen
Turnstile Getting Started Screen

Once you click on “Add Site,” you’re taken to a screen where you can add a site name, add a domain, and select your widget type.

Turnstile Add Site Screen
Turnstile Add Site Screen

For my case, I chose the “Non-interactive” version.

Once you choose to “Create” the site, you’ll be shown instructions and receive a Site Key and a Secret Key.

Turnstile Site Key and Secret Key
Turnstile Site Key and Secret Key

Adding Turnstile to your site

Now here’s the rub. Turnstile is relatively new, so not everything has a solution yet.

For WordPress sites, however, there is a very versatile plugin called Simple Cloudflare Turnstile, which integrates natively with many solutions.

I like the idea of a plugin that integrates with many solutions, but I really only cared about one type of implementation. Comments.

Turnstile and WordPress comments

I have a premium plugin called Comment Edit Pro, which integrates with several anti-spam services such as reCAPTCHA 3 and Akismet.

Since I was so impressed with Turnstile and how it worked, I wanted to add a native integration specific to comments. Yes, the plugin above does comments, but since I already integrated reCAPTCHA 3 into Comment Edit Pro, I felt Turnstile needed to be added too.

So that takes me back to square one. I needed to develop my own solution.

Getting started?

Turnstile Getting Started Page
Turnstile Getting Started Page

On the Turnstile Getting Started page, you’re shown a script you can include that can run in the <head> element.

You’re then shown steps for the client-side (user-facing) integration. You can choose between Explicit and Implicit rendering.

Implicit rendering

An implicit render is more of a hands-off approach, hence the name implicit. You just need to add a sliver of HTML to your form.

But first you’ll need to add the Turnstile script to your page.

<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>Code language: HTML, XML (xml)

After that, find your form’s source and add in the HTML.

<form method="POST" action="/handler">
	<!-- form inputs and fields -->
	<div class="cf-turnstile" data-sitekey="1x00000000000000000000AA"></div>
	<button class="w-100 btn btn-lg btn-primary" type="submit">Sign in</button>
</form>
Code language: HTML, XML (xml)

Just add a div with class cf-turnstile and sitekey data attribute.

Turnstile will then inject a hidden input element named cf-turnstile-response, which will contain a token if a challenge is passed.

<input type="hidden" name="cf-turnstile-response" id="cf-chl-widget-mgegk_response" value="XXXX.DUMMY.TOKEN.XXXX">Code language: HTML, XML (xml)

Once the form is submitted, you’d check for the cf-turnstile-response post variable on the server-side, validate it against a Turnstile endpoint, and either complete or reject the form.

Explicit rendering

Explicit rendering, as in, you are actively doing something, is a more hands-on approach. You’ll be manually rendering Turnstile as opposed to the widget being shown automatically.

The script tag is the same except for a callback function when the Turnstile script is done initializing.

<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?onload=onloadTurnstileCallback" async defer></script>Code language: HTML, XML (xml)

In this example, there is an onload callback named onloadTurnstileCallback.

You can name this callback function anything you like, but if you have onloadTurnstileCallback as the callback, you must create this callback in your own local JavaScript files.

/**
 * This is the main callback for Cloudflare.
 */
window.onloadTurnstileCallback = () => {
	// do turnstile explicit rendering stuff here.
};Code language: JavaScript (javascript)

The example Turnstile provides for explicitly rendering the widget is:

window.onloadTurnstileCallback = function () {
    turnstile.render('#example-container', {
        sitekey: '<YOUR_SITE_KEY>',
        callback: function(token) {
            console.log(`Challenge Success ${token}`);
        },
    });
};Code language: JavaScript (javascript)

Not shown is any server-side validation (we’ll get to that).

Since this is “explicit”, we’ll have to render our own widget, get the token, and then validate it.

You can also pass in additional parameters to turnstile.render to customize the appearance and output.

Customize Turnstile With Additional  Parameters
Customize Turnstile With Additional Parameters

Validating the response on the server

Once your token is read in via a form variable, you’ll need to capture that on the back-end (server-side validation).

Here’s a quick example of doing that in WordPress land:

Server-side Validation Code
Server-side Validation Code

To validate on the server, you need the Turnstile secret key and the client-side token.

Since Turnstile adds the cf-turnstile-response hidden variable in the form, you can just check the $_POST super global for its existence.

The endpoint to verify the token is:

https://challenges.cloudflare.com/turnstile/v0/siteverifyCode language: JavaScript (javascript)

The body of the request should contain keys: secret and response. Secret being your Turnstile secret key, and response being the token.

If the response is successful, it’ll pass a success parameter in its body response.

That’s pretty much it for server validation. For now.

Comment Edit Pro and Turnstile

For Comment Edit Pro, the first thing I needed was a UI to capture the site key and secret key.

Comment Edit Pro Turnstile Options
Comment Edit Pro Turnstile Options

I also added in some options to configure the Turnstile widget appearance.

Options for Turnstile Widget Appearance
Options for Turnstile Widget Appearance

The first dilemma with these options was how to get this working locally. If you remember, when enabling Turnstile for a site, you have to give an actual working domain. So how do I test if it’s working and not have to edit files directly on the server?

There are test keys. Buried in the Turnstile FAQ are the local keys you need to test with. I’ve listed them here as well.

SitekeyDescription
1x00000000000000000000AAAlways passes
2x00000000000000000000ABAlways blocks
3x00000000000000000000FFForces an interactive challenge
Site Key Local Values
Secret keyDescription
1x0000000000000000000000000000000AAAlways passes
2x0000000000000000000000000000000AAAlways fails
3x0000000000000000000000000000000AAYields a “token already spent” error
Secret Key Local Values

Armed with these test keys, I was ready to begin the process of explicitly setting the widget and doing the server-side validation.

Initializing the actions and filters

I had to use the wp action as it loads pretty late and I needed a reliable mechanism for determining if comments were open or not.

Action wp Callback
Action wp Callback

Inside the action_wp callback, I do some sanity checks to make sure that comments are open, and to ignore Turnstile for those who can moderate comments. I then load in my options, and check to see if Turnstile should be enabled for regular logged-in users.

Permission Checks to Skip Turnstile
Permission Checks to Skip Turnstile

Modifying the Submit button

For Turnstile, I needed to get the comment submit button’s HTML ID attribute value. This is so I can target the button and work backwards to get the form it’s contained in. I hooked into the comment_form_submit_button filter to do this.

Add filter: comment_form_submit_button
Add filter: comment_form_submit_button

Within the callback, I check to see if Turnstile is active and replace the comment submit button’s ID with sce-turnstile-submit.

Changing Submit Button ID Attribute
Changing Submit Button ID Attribute

Loading the frontend scripts

The next actions and filters involve loading the Cloudflare frontend script.

Initialize the Script Actions
Initialize the Script Actions

Let’s start with callback enqueue_turnstile_scripts.

Load Plugin and Turnstile Scripts and Local JS Variables
Load Plugin and Turnstile Scripts and Local JS Variables

I first enqueue my local JavaScript file, which will initialize Turnstile. I then load in the Turnstile script, making sure my local script runs first.

Lastly, I use wp_localize_script to output the widget options that were saved in the admin. This is so I can set them on the frontend.

Up next is deferring the Cloudflare script.

Defer the Cloudflare Script for Performance
Defer the Cloudflare Script for Performance

A simple search/replace gets the job done here.

Adding the widget container to the comment section

Add Widget Container to Comment Form Filter
Add Widget Container to Comment Form Filter

Using the comment_form_defaults filter, I can modify the output of the comment section and inject my empty widget container.

Adding an Empty DIV in the Comment Form
Adding an Empty DIV in the Comment Form

The frontend script

In my local JavaScript code, I initialize a global function that can be called via a callback.

Initialize Turnstile Callback
Initialize Turnstile Callback

I attempt to get the Submit button (we set the ID earlier), and I then trace backwards to get the form, so that I can then get the textarea field.

JavaScript Dom Traversing to get the Textarea
JavaScript Dom Traversing to get the Textarea

I need the textarea field because the tokens have a shelf-life of 300 seconds. I want to only initialize Turnstile if someone is actively writing a comment.

Turnstile Widget Initialization and Callback
Turnstile Widget Initialization and Callback

When a user starts typing into the textarea field, the widget will begin to be rendered. I did this because of the token timeout.

Comments are typically at the end of the post, so starting the widget on page load didn’t make sense in this case. The timeout of 300 seconds is so that I can refresh the widget and generate a new token.

The timeout is shown in this highlighted code section.

Refreshing a Token When it Expires
Refreshing a Token When it Expires

Calling turnstile.reset will generate a new token and will call the callback again with this token.

The Frontend output

If all is well, a user just has to start typing in the textarea field for Turnstile to be initialized.

Cloudflare Turnstile on the Frontend
Cloudflare Turnstile on the Frontend

Armed with a token courtesy of the injected hidden input from Turnstile, I can do server validation when the comment is submitted.

Server-side validation

Back to PHP land, I hook into the action pre_comment_on_post to make sure that we reject a comment early.

Filter: pre_comment_on_post
Filter: pre_comment_on_post

And here’s my server-validation method in all its glory.

Turnstile Server Side Validation Code
Turnstile Server Side Validation Code

Conclusion

In this developer diary, I went over what Cloudflare Turnstile is, why or not you should use it, and how to implement it.

I also covered how I added the feature to comments. In fact, it’s enabled below if you want to give it a whirl. As mentioned, the widget doesn’t initialize until you start typing a comment.

Thank you so much for reading.

Ronald Huereca
Ronald Huereca

Ronald Huereca

Ronald has been part of the WordPress community since 2006, starting off writing and eventually diving into WordPress plugin development and writing tutorials and opinionated pieces.

No stranger to controversy and opinionated takes on tough topics, Ronald writes honestly when he covers a topic.

2 thoughts on “Developer Diaries: Comments and Cloudflare Turnstile”

  1. Nice article, Ron.
    Does this service function as a replacement for Google’s recaptcha? I like the idea of one less Google service hoovering up data about me.

Comments are closed.

Ronald Huereca
MediaRon - Ronald Huereca

Ronald created MediaRon in 2011 and has more than fifteen years of releasing free and paid WordPress plugins.

Quick Links

Say Hi