Alex Heist - Software Developer

reCAPTCHA v3 in Django

Google's reCAPTCHA has become an industry standard for filtering out spam form submissions. It's a free service and is fairly easy to work with once you get it up and running. Today, I'll step you through the instructions of how to get reCAPTCHA working with Django.

The first thing that you will need to do is register your app in Google's reCAPTCHA Admin. For testing purposes, simply put 127.0.0.1 as your site domain. Once your application is ready for production, you would want to change this to the actual domain of your website.

Alright, so with that all set up, let's create our Django app.

We'll create a virtual environment called django_recaptcha:

$ python3 -m venv django_recaptcha

Then we'll activate the virtual environment and install our project packages:

$ cd django_recaptcha && source bin/activate
$ pip install django requests

I prefer using the requests package to validate the reCAPTCHA response, but it is not required. A way to avoid the extra dependency would be to use the urllib library.

Ok, so with that installed, we'll create our Django project:

$ django-admin startproject contact_app .

Once that command finishes, you should have a file structure that looks like this:

.
├── bin
├── contact_app
│   ├── __init__.py
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── include
├── lib
├── manage.py
└── pyvenv.cfg

Once you have that all set up, we need to make some small changes to our settings.py:

  1. Add contact_app into the list of INSTALLED_APPS
  2. Add our reCAPTCHA keys
  3. RECAPTCHA_PUBLIC_KEY = ''
  4. RECAPTCHA_PRIVATE_KEY = ''

With the settings our of the way, it's time to add our contact form. In the contact_app directory, add the following files:

# models.py
from django.db import models
from django.utils import timezone

class Contact(models.Model):
    name = models.CharField(max_length=255)
    email = models.EmailField()
    message = models.TextField()
    timestamp = models.DateTimeField(default=timezone.now)
# forms.py
from django import forms
import .models

class ContactForm(forms.ModelForm):
    class Meta:
        model = models.Contact
        exclude = ('timestamp',)

Run $ ./manage.py makemigrations contact_app && ./manage.py migrate

Now, in our view, we'll add the code required to at least get started with submitting the form:

# views.py
import requests
from django.conf import settings
from django.shortcuts import render
from django.views import generic

import .forms

class Contact(generic.FormView):
    template_name = 'contact.html'
    form_class = forms.ContactForm
    success_url = ''
    extra_context = {
        'recaptcha_key': settings.RECAPTCHA_PUBLIC_KEY
    }

    def form_valid(self, form):
        form.save()
        return render(self.request, 'contact.html', {
            'form': self.get_form(),
            'message': 'Thank you'
        })

Lastly, we'll add our template for the form and update our urls.py:

<!-- contact_app/templates/contact.html -->
<html>
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  </head>
  <body>
    <form id="contactForm" method="post">
      {% csrf_token %}
      {{ form.as_p }}

      <!-- used for submitting recaptcha token to the view -->
      <input id="recaptcha" type="hidden" name="g-recaptcha-response" />

      <input type="submit" value="Submit" />
    </form>
    <p>{{ message }}</p>
  </body>
</html>
# urls.py

from django.contrib import admin
from django.urls import path
import .views

urlpatterns = [
    path('', views.Contact.as_view()),
    path('admin/', admin.site.urls),
]

Now, you should have a working contact form, the only thing left to do is add the logic for validating submissions with Google's reCAPTCHA.

To do this, we'll first need to edit contact.html again:

<!-- contact_app/templates/contact.html -->
<html>
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  </head>
  <body>
    <form id="contactForm" method="post">
      {% csrf_token %}
      {{ form.as_p }}

      <!-- used for submitting recaptcha token to the view -->
      <input id="recaptcha" type="hidden" name="g-recaptcha-response" />

      <input type="submit" value="Submit" />
    </form>
    <p>{{ message }}</p>
  </body>

  <!-- dependencies -->
  <script src="https://code.jquery.com/jquery-3.4.1.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
  <script src="https://www.google.com/recaptcha/api.js?render={{ recaptcha_key }}"></script>

  <!-- Google reCAPTCHA Integration -->
  <script>
    grecaptcha.ready(function() {
      $('#contactForm').submit(function(e){
        var form = this;
        e.preventDefault()
        grecaptcha.execute('{{ recaptcha_key }}', {action: 'contactForm'}).then(function(token) {
          $('#recaptcha').val(token)
            form.submit()
        });
      })
    });
  </script>
</html>

Here, the javascript essentially overrides the form submit event to retrieve a reCAPTCHA token from Google. More information can be found about this here.

The next thing that we'll need to do is edit views.py again to validate the token generated by Google:

import requests
from django.conf import settings
from django.shortcuts import render
from django.views import generic

import .forms

class Contact(generic.FormView):
    template_name = 'contact.html'
    form_class = forms.ContactForm
    success_url = ''
    extra_context = {
        'recaptcha_key': settings.RECAPTCHA_PUBLIC_KEY
    }

    def form_valid(self, form):
        # retrieve token
        token = self.request.POST('g-recaptcha-response')
        if token:
            data = {
                'secret': settings.RECAPTCHA_PRIVATE_KEY,
                'response': token
            }
            # verify response with Google
            response = requests.post(
                'https://www.google.com/recaptcha/api/siteverify',
                data = data
            )
            result = response.json()
            # check results
            if result['success'] == True and result['score'] >= 0.5:
                form.save()
        return render(self.request, 'contact.html', {
            'form': self.get_form(),
            'message': 'Thank you'
        })

This code sends the token generated by the Javascript in the form over to Google, where a response is returned that includes whether or not the token was valid as well as a confidence score. The confidence score is a scale from 0.0 to 1.0, low scores indicating that the form was likely filled out by a bot and high scores indicate that it was filled out by a human.

That is pretty much all you need to get started with Google reCAPTCHA in Django. If you would like more information, do check out the official documentation.

Also, feel free to let me know if you have any questions or found this article useful through my contact form.