A common way to counteract the infernal swarm of bots, crawlers, and other inhuman entities on the internet is to implement a captcha system to protect features on your website. Google’s reCAPTCHA, ALTCHA, hCaptcha, and other systems all function similarly. Regardless of the choices you make, all follow a similar implementation pattern for securing forms:
- Using JavaScript, add a challenge field to the form
- On submit, this data is included in the form as a ’token’ field
- On the backend, POST the token and other data to verify it
- If the token is valid, allow the user to continue
Note that some of the systems above return a score instead of a pass/fail value.
Register with Cloudflare¶
First, navigate to the Turnstile page in your Cloudflare account and create a new widget. After entering your hostname1 and a few other options, you be provided with a site key and secret key that both look like long, random strings of numbers and letters. The following links will also be provided, which have good documentation on the two halves of this process:
Crucially, know that you can use localhost
as a valid domain.2
Create a separate widget for your development environment to implement
and learn the technology, and don’t share keys with your production
environment.
Add Turnstile Code to Frontend¶
Because I typically use HTMX3 with hx-boost
, which does not
evaluate page scripts each time the user navigates to a different
page, I need to make additional calls to activate turnstile widgets
after a user decides to login or signup.
<script>
document.addEventListener('htmx:after-swap', function(e) {
if(e.target === document.body){
const captchaElement = document.querySelector("div.cf-turnstile");
if (typeof turnstile !== "undefined" && captchaElement){
turnstile.render(captchaElement);
}}});
</script>
<!-- Key: Load the Cloudflare Turnstile script -->
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js"></script>
A similar approach will need to be taken for a PWA.4
Add Turnstile Element to Form¶
The following element (selected above) can be included within your
<form>
tag, and assuming you are using a templating system like
selmer,5 this is where to insert the site key from Cloudflare.
I like it right before the submission button.
<div
class="cf-turnstile"
data-sitekey="{{ captcha_site_key }}"
data-size="flexible"
theme="light"
></div>
This is all that is required to present a challenge to your users and
ensure the token field (cf-turnstile-response
) is included in form
submissions!
Handle Captcha Token on Backend¶
Now that our frontend is including the token field in our login and
signup forms, let’s handle that tocken on our site’s backend.
Fundamentally, we are simply sending the token to Cloudflare to
confirm that the user successfully completed the challenge - and
didn’t maliciously insert an additional field called cf-turnstile-response
with
random data. A simple POST
request with clj-http.client
will do
the trick.
Here is a full additional namespace you can drop into your project to validate Cloudflare tokens:
(ns <your-site>.cloudflare
(:require
[clj-http.client :as client]
[site.config :refer [env]] ; <- We pull the secret key from the site configuration
[clojure.data.json :as json]
[clojure.string :as s]
[clojure.tools.logging :as log]))
(def cf-url "https://challenges.cloudflare.com/turnstile/v0/siteverify")
;; Read the response and check for the "success" key from Cloudflare
(defn handle-response [res]
(let [body (json/read-str (:body res))]
(boolean (get body "success" false))))
;; -> Struct expects 'true' or 'false' as a validation result.
;; POST the token and your secret to Cloudflare for validation
(defn validate-token [token]
(handle-response
(client/post cf-url
{:headers {"Content-Type" "application/json"}
:accept "application/json"
:body (json/write-str
{:response (or token "")
:secret (:cf-turnstile-secret-key env)
:idempotency_key (java.util.UUID/randomUUID)})})))
;; Providing an idempotency_key ensures tokens can't be re-used
If you are using the struct
6 library to validate your forms7,
the validate-token
function written above can be called as an
additional validation function for the cf-turnstile-response
field.
Demonstrated below is a method for merging an additional field
validation with your current rules, checking the Cloudflare token
without disturbing your existing validation structures.
(defn- with-cf-field [validation-schema]
(vec (concat validation-schema
[[:cf-turnstile-response
st/required
st/string
{:message "Capcha check failed."
:validate #(cloudflare/validate-token %)}]])))
;; For example, if you wanted to validate a form with album info:
(def album-schema
[[:band st/required st/string]
[:album st/required st/string]
[:year st/required st/number]])
;; You could call:
(with-cf-field album-schema)
;; Which returns the equivalent of:
[[:band st/required st/string]
[:album st/required st/string]
[:year st/required st/number]
[:cf-turnstile-response
st/required
st/string
{:message "Capcha check failed."
:validate #(cloudflare/validate-token %)}]]
Apart wrapping your calls to (st/validate)
with the with-cf-field
function created above, no additional changes to your login or
signup backend logic are required! Enjoy your security-enhanced
forms, now slightly more protected against bots!
The hostname of the production website. ↩︎
“Allow localhost or 127.0.0.1 as acceptable domains for Turnstile” community.cloudflare.com ↩︎
PWA “Progressive Web Application” - referring to a single-page web application made with React or another JavaScript framework. ↩︎
“Selmer, A fast, Django inspired template system in pure Clojure.” github.com/yogthos/Selmer ↩︎
“Struct, A structural validation library for Clojure(Script).” github.com/funcool/struct ↩︎
“Luminus: Input Validation” luminusweb.com/docs ↩︎