Zero
Zero
Back

Invisible reCAPTCHA v2 with Django and Zero

Protecting your web application from bots is necessary to prevent spam and other forms of abuse. This article provides a step-by-step guide to implementing this type of protection with reCAPTCHA.

Sam Magura

Sam Magura

Sheets of paper that cast shadows

The unfortunate reality of web application development is that we cannot only think about how our application will be used — we must also think about how it will be misused. This article focuses on preventing automated abuse by bots through the use of captchas. A captcha is essentially a Turing test , meaning that it allows a web application to differentiate between legitimate use by a human and illegitimate use by a bot.

We will be using Google's reCAPTCHA, the most widely used captcha library on the web. If you have ever been asked to check the "I am not a robot" box, then you've used reCAPTCHA. Though, the "I'm not a robot checkbox" is just one flavor of reCAPTCHA — there are several options to choose from:

The different types of reCAPTCHA
The different types of reCAPTCHA

The rest of this article provides a step-by-step guide to setting up invisible reCAPTCHA v2 in a Django web application. Invisible reCAPTCHA v2 is a great option because it requires no user input. This flavor of reCAPTCHA is also easy to use, since it indicates whether or not the user is a bot via a simple yes/no response, rather than a confidence score like reCAPTCHA v3.

Calling the reCAPTCHA API from your backend requires a secret key. We'll store this key in the Zero secrets manager and fetch it at runtime using the Zero Python SDK .

🔗 The full code for this example is available in the zerosecrets/examples  GitHub repository.

Secure your secrets conveniently

Zero is a modern secrets manager built with usability at its core. Reliable and secure, it saves time and effort.

Zero dashboard

Overview of Integrating with reCAPTCHA v2

Adding invisible reCAPTCHA v2 to your application is a straightforward task, though it does require code on both the frontend and backend. If you get stuck or want to learn about the full range of use cases supported by the library, check out the official docs  provided by Google.

At a high level, here is how the process works. All steps occur client side, except for the last one which occurs server side.

  1. Your web application displays a form to the user, e.g. a form to sign up as a new user.
  2. The reCAPTCHA JavaScript runs in the background, analyzing the user's behavior to determine if they are a bot.
  3. The user submits the form, which does not submit the form to the server, but instead triggers the reCAPTCHA challenge.
  4. reCAPTCHA performs the challenge and invokes your callback function.
  5. Your callback function submits the form for real. The data posted to the server includes the reCAPTCHA response string.
  6. Your backend calls the reCAPTCHA API with your secret key and the response from the frontend. The API returns a success boolean that tells you if the user passed the challenge.

Signing Up for reCAPTCHA

You can sign up for reCAPTCHA and get your API key here . Select "Select reCAPTCHA v2 - Invisible reCAPTCHA badge" and add localhost as a domain. You'll be given your site key and secret key once you complete the signup flow. The site key can be shared publicly, while the secret key must be kept private.

We're using Zero to manage our secrets, so log in to Zero and create new project to store the reCAPTCHA keys. While we only need to fetch the secret key from Zero (since the site key can be committed directly to your git repository), it's convenient to save both keys in Zero. This makes Zero the ultimate source of truth for all of the credentials needed to run your project.

Adding the reCAPTCHA site key and secret key to Zero
Adding the reCAPTCHA site key and secret key to Zero

Setting Up the Django Project

If you already have a working Django project, feel free to skip this section. We can roughly follow the official Django tutorial  to get up and running with a working app. The main difference is that we'll use Poetry  so that the dependencies of our new app don't get intermingled with any Python packages that are installed globally. We also don't need to set up a database or data model for this exercise.

Here's a summary of the steps to follow:

  1. Install Poetry .

  2. Make a new recaptcha-django directory and cd into it.

  3. Run poetry init and enter "no" when asked if you want to define dependencies interactively.

  4. poetry add Django

  5. poetry run django-admin startproject recaptcha_django .

  6. Create a new Django app called pages via poetry run python manage.py startapp pages.

  7. Add 'pages.apps.PagesConfig' to the list of INSTALLED_APPS in settings.py.

  8. Configure the project to forward all requests to the pages app by setting urls.py to:

    1
    2
    3
    4
    5
    python
    from django.urls import path, include
    
    urlpatterns = [
        path('', include('pages.urls')),
    ]
    

Then, add a simple "Hello World" view and template to the pages app.

pages/views.py:

1
2
3
4
5
6
python
from django.shortcuts import render

def index(request):
    context = {}

    return render(request, 'pages/index.html', context)

pages/index/pages/index.html should be a valid HTML file which displays a simple user registration form:

1
2
3
4
5
6
7
8
9
html
<form id="demo-form" action="/" method="POST">
  {% csrf_token %}

  <label for="username">Username</label>
  <input id="username" name="username" />
  <br />

  <button>Sign up</button>
</form>

Run the Django development server with poetry run python manage.py runserver, navigate to http://localhost:8000, and you should see the signup form.

💡 You must access the site via localhost rather than 127.0.0.1 since we provided localhost as our domain when signing up for reCAPTCHA.

Frontend Integration

The easiest way to set up reCAPTCHA v2 on the frontend is to have the library automatically bind the challenge to a <button> element. You can do this by pasting in the required <script> tag and adding a few data attributes to the form's submit button:

1
2
3
4
html
<head>
  <!-- ... -->
  <script src="https://www.google.com/recaptcha/api.js" async defer></script>
</head>
1
html
<button class="g-recaptcha" data-sitekey="YOUR-RECAPTCHA-SITE-KEY-HERE" data-callback="onSubmit">Sign up</button>

After reCAPTCHA performs the challenge, it will invoke the function specified in data-callback. This function must be defined in the global scope. All the callback function needs to do is submit the form to the backend, like so:

1
2
3
javascript
function onSubmit() {
  document.getElementById('demo-form').submit()
}

The reCAPTCHA response string will automatically be included in the form's POST data.

💡 "Invisible" reCAPTCHA v2 is not truly invisible, since a reCAPTCHA badge will be shown in the bottom right of the page. You are allowed to hide the badge, but you must display a message that links to Google's Privacy Policy and Terms of Service. This is described here  in the reCAPTCHA FAQ.

Backend Integration

Now we need to modify the backend logic to pass the reCAPTCHA response from the form to the reCAPTCHA web API. The API will then return a JSON response which indicates whether the user is a bot.

This is straightforward to do: you can access the form's POST data via the request.POST dictionary, and then make the API call using the requests package. The finished code looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
python
def index(request):
    context = {}

    if request.method == 'POST':
        username = request.POST['username']
        recaptcha_response = request.POST['g-recaptcha-response']

        # TODO Implement fetch_recaptcha_secret_key, which gets the key from Zero
        secret_key = fetch_recaptcha_secret_key()

        r = requests.post(
            'https://www.google.com/recaptcha/api/siteverify',
            data={
                'secret': secret_key,
                'response': recaptcha_response
            }
        )
        google_response = r.json()

        if google_response['success']:
            context['message'] = (
                'reCAPTCHA challenge passed. ' +
                'User "{}" created successfully.'.format(username)
            )
        else:
            context['message'] = 'reCAPTCHA challenge failed.'

            if 'error-codes' in google_response:
                error_codes = ', '.join(google_response['error-codes'])
                context['message'] += ' Error codes: {}'.format(error_codes)

    return render(request, 'pages/index.html', context)

context['message'] can be then be displayed by the index.html template.

The final step is to fetch the reCAPTCHA v2 secret key from Zero. This is a breeze thanks to the Zero Python SDK , which you can install via

1
shell
poetry add zero-sdk

Then all we have to do is read the ZERO_TOKEN environment variable and pass it to the SDK. Once the secret key has been retrieved, we'll store it in a module-level variable so that it does not need to be refetched every time the new user form is submitted.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
python
import os
from zero_sdk import zero

secret_key = None

def fetch_recaptcha_secret_key():
    global secret_key

    if secret_key is not None:
        return secret_key

    ZERO_TOKEN = os.getenv('ZERO_TOKEN')

    if ZERO_TOKEN is None:
        raise Exception('ZERO_TOKEN environment variable not set.')

    secrets = zero(token=ZERO_TOKEN, pick=['recaptcha']).fetch()

    if 'recaptcha' not in secrets:
        raise Exception('recaptcha secret not found.')

    if 'secret_key' not in secrets['recaptcha']:
        raise Exception('secret_key field not found.')

    secret_key = secrets['recaptcha']['secret_key']

    if len(secret_key) == 0:
        raise Exception('secret_key field is empty.')

    return secret_key

The finished app can be run with

1
shell
ZERO_TOKEN='YOUR-ZERO-TOKEN' poetry run python manage.py runserver

Conclusion

If you followed along with the post, you now have a user signup form that is protected from bots! This will prevent attackers from using automation to create bogus user accounts on your platform. While we used user signup in this example, it's a good idea to add reCAPTCHA integration to any publicly-accessible form that might be abused.

This article showcased how Zero can serve as the single source of truth for your project's secrets. The fact that the reCAPTCHA API key is fetched from Zero at runtime makes it easier to integrate your Django app with additional APIs like Stripe, Twilio, .etc in the future. It's also simpler to rotate your API keys on a regular basis when using Zero since the application's configuration does not need to be updated.

Happy coding!


Other articles

Architectural structure

Secrets Usage History: What it is and why it matters

If a secret is obtained by a malicious actor, the consequences can be severe. Monitoring the usage history of a secret in Zero allows you to detect unauthorized access and act before the secret is used in an exploit.

A cube made of smaller cubes

Build an Email Summary Bot using Claude AI, Gmail, and Slack (Part 2)

Use the Claude AI assistant to summarize emails in just a few lines of code.

Secure your secrets

Zero is a modern secrets manager built with usability at its core. Reliable and secure, it saves time and effort.