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:

  1. Using JavaScript, add a challenge field to the form
  2. On submit, this data is included in the form as a ’token’ field
  3. On the backend, POST the token and other data to verify it
  4. 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:

  1. Client side integration code
  2. Server side integration code

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 struct6 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!


  1. The hostname of the production website. ↩︎

  2. “Allow localhost or 127.0.0.1 as acceptable domains for Turnstile” community.cloudflare.com  ↩︎

  3. “HTMX: High power tools for HTML” htmx.org  ↩︎

  4. PWA “Progressive Web Application” - referring to a single-page web application made with React or another JavaScript framework. ↩︎

  5. “Selmer, A fast, Django inspired template system in pure Clojure.” github.com/yogthos/Selmer  ↩︎

  6. “Struct, A structural validation library for Clojure(Script).” github.com/funcool/struct  ↩︎

  7. “Luminus: Input Validation” luminusweb.com/docs  ↩︎