Django + Turnstile: A Quick Step-by-Step Tutorial

Learn how to integrate the Turnstile CAPTCHA alternative with Django. This tutorial covers every step of the process.

Register your domain(s) with Cloudflare Turnstile

After registering one or more domains in the Turnstile dashboard, you’ll receive both a public site key and a secret key. The former is required by the Turnstile widget, while the latter will be used for validating the widget’s response in the backend.(( Server-side validation · Cloudflare Turnstile docs ))

Pro tip: Keep the secret key in a secure storage such as your application’s environment or a tool like the AWS Secrets Manager.

Pro tip #2: Don’t forget to add localhost as one of your domains to enable local development and testing.

Add the Turnstile widget to your frontend

First, place the following Turnstile script tag within your HTML document’s <head> element:

<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>

Then, add a div with class cf-turnstile to the form (e.g. login form) you’d like to protect with Turnstile:

<form action="/login" method="POST">
   <input type="text" name="email" placeholder="Email"/>
   <input type="password" name="password" placeholder="Password"/>
   <div class="cf-turnstile" data-sitekey="<YOUR_SITE_KEY>"></div>
   <button type="submit">Log in</button>
</form>

The .cf-turnstile element indicates the location where a hidden input, named cf-turnstile-response, wil be added to the form by the Turnstile JavaScript code. It also contains a data attribute with the public site key generated in the previous step of this tutorial.

Validate the Turnstile widget response in your backend

When a user submits the above form, the hidden cf-turnstile-response input is sent to the server along with the other form fields. The official Turnstile documentation explains that:

The presence of a response alone is not enough to verify it as it does not protect from replay or forgery attacks. In some cases, Turnstile may purposely create invalid responses that are rejected by the siteverify API.

Cloudflare Turnstile docs(( Server-side validation · Cloudflare Turnstile docs ))

Therefore, it’s essential to validate the Turnstile widget response in your backend to ensure its authenticity and protect against replay or forgery attacks. The following Python function does just that:

import requests
def validate_turnstile_widget_response(token, request_ip):
    # get the secret Turnstile key from the environment
    try:
        secret_key = os.environ["CLOUDFLARE_TURNSTILE_SECRET_KEY"]
    except KeyError:
        raise ValueError("CLOUDFLARE_TURNSTILE_SECRET_KEY environment variable not set")
    # send the widget response token, secret key, and request IP to the Turnstile API
    return requests.post(
        "https://challenges.cloudflare.com/turnstile/v0/siteverify",
        data={
            "secret": secret_key,
            "response": token,
            "remoteip": request_ip,
        }
    ).json()

Depending on your specific web framework, you’ll now need to integrate the function into the appropriate part of your server-side code. The next section will demonstrate an approach for Django.

Wrapping it all up in a Django FormField

The smoothest way to integrate Turnstile with Django is through a custom FormField. This method centralizes all the code, including the core logic, in one place:

import os
import requests
from django import forms
from django.core.exceptions import ValidationError
from django.utils.html import html_safe

class TurnstileField(forms.CharField):
    class Widget(forms.TextInput):
        def render(self, **kwargs):
            return f'<div class="cf-turnstile" data-sitekey="{ os.environ["CLOUDFLARE_TURNSTILE_SITE_KEY"] }"></div>'
    
        @html_safe
        class ScriptTag:
            def __str__(self):
                return '<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>'
    
        class Media:
            js = [ScriptTag()]
    def __init__(self, *args, **kwargs):
        self.client_ip = kwargs.pop('client_ip')
        super().__init__(*args, **kwargs)
    def clean(self, value):
        # get the secret Turnstile key from the environment
        try:
            secret_key = os.environ["CLOUDFLARE_TURNSTILE_SECRET_KEY"]
        except KeyError:
            raise ValueError("CLOUDFLARE_TURNSTILE_SECRET_KEY environment variable not set")
    
        # send the widget response token value, secret key, and request IP to the Turnstile API
        response = requests.post(
            "https://challenges.cloudflare.com/turnstile/v0/siteverify",
            data={
                "secret": secret_key,
                "response": value,
                "remoteip": self.client_ip,
            }
        ).json()
        if not response["success"]:
            raise ValidationError(_("Please try again. If the problem persists, please contact us."), code=",".join(response["error-codes"]))
        return value

Note that the Turnstile validation process needs the requesting user’s IP address. You can capture the IP in the view class or function and then pass it on to the field like so:

# abcxyz/views.py
def login_view(request):
    client_ip = request.META.get('REMOTE_ADDR')
    form = LoginForm(initial={'client_ip': client_ip})
    # …
# abcxyz/forms.py
from django import forms
class LoginForm(forms.Form):
    def __init__(self, *args, **kwargs):
        client_ip = kwargs['initial']['client_ip']
        super().__init__(*args, **kwargs)
        self.fields['turnstile'] = TurnstileField(client_ip=client_ip)

FAQ

What exactly is Cloudflare Turnstile?

🛈 The response to this FAQ was in part generated by ChatGPT-4 and has been manually reviewed & edited by me for factual accuracy.

Cloudflare Turnstile is a security service designed for web applications, offering user-friendly authentication and spam protection. It’s primarily known for being a CAPTCHA alternative.(( Cloudflare Turnstile, a free CAPTCHA replacement | Cloudflare ))

And how can I integrate Turnstile into a Flask application?

Here you go:

import requests
from flask import Flask, request, jsonify
app = Flask(__name__)

def validate_turnstile_widget_response(token, request_ip):
    # get the secret Turnstile key from the environment
    try:
        secret_key = os.environ["CLOUDFLARE_TURNSTILE_SECRET_KEY"]
    except KeyError:
        raise ValueError("CLOUDFLARE_TURNSTILE_SECRET_KEY environment variable not set")
    # send the widget response token, secret key, and request IP to the Turnstile API
    return requests.post(
        "https://challenges.cloudflare.com/turnstile/v0/siteverify",
        data={
            "secret": secret_key,
            "response": token,
            "remoteip": request_ip,
        }
    ).json()

@app.route('/submit-form', methods=['POST'])
def submit_form():
    token = request.form.get('cf-turnstile-response')
    request_ip = request.remote_addr
    validation_response = validate_turnstile_widget_response(token, request_ip)
    if validation_response.get('success'):
        return jsonify({'message': 'Success'})
    else:
        return jsonify({'error': 'Invalid response'}, status=400)
if __name__ == '__main__':
    app.run

Sources

Published

Leave a comment

Your email address will not be published. Required fields are marked *