Integrating Google Recaptcha to Django

Like many, I hate captchas. They are tedius and put the burden on your real users to prove they are real. I prefer whenever possible to try to implement a honeypot anti-spam layer. Recently, however I had a site with public forms that a honeypot wasn’t doing an adequate job of protecting.

I recently decided to try out Google’s checkbox recaptcha and I’m not unhappy with it.

How does it work?

At a very basic level, there is a block of html containing your public key that you add to your html form. A google hosted JS script hijacks and makes the html block into a recaptcha element. When a person checks the checkbox, the JS reaches out to the recaptcha service and retrieves a one-time token that gets submitted with the form via a hidden field.

In your backend code, you take the token and a secret key provided to you at signup and then send them to the service API URL via a post. The service API returns a JSON string representing success or failure. Based on the success or failure of this API call, validate the form appropriately.

How to integrate into Django

Settings

Add some settings to your Django project. You want these credentials to be managed at an environment level and not embedded in your code.

# RECAPTCHA STUFF
GOOGLE_RECAPTCHA_SITE_KEY = "YOUR-PUBLIC-KEY"
GOOGLE_RECAPTCHA_SECRET_KEY = "YOUR-PRIVATE-KEY"

Create a context processor. The point of this context processor is to make the public key, GOOGLE_RECAPTCHA_SITE_KEY, available to your template system.

Add a context_processors.py file to your app’s folder if you do not already have one and add the following function.

def recaptcha(request):
    """context processor to add alerts to the site"""
    from django.conf import settings

    return {
        'GOOGLE_RECAPTCHA_SITE_KEY': settings.GOOGLE_RECAPTCHA_SITE_KEY,
    }

Next, tell your site to use this Context. In your site’s settings file add 'contact.context_processors.recaptcha' to the TEMPLATE_CONTEXT_PROCESSORS.

import django.conf.global_settings as DEFAULT_SETTINGS
TEMPLATE_CONTEXT_PROCESSORS = DEFAULT_SETTINGS.TEMPLATE_CONTEXT_PROCESSORS + (
    'contact.context_processors.recaptcha',
)

Template Work

Now that your public key is available in your templates, add the recaptcha html element to your forms before the submit button and closing </form> tag.

<div class="g-recaptcha" data-sitekey="{{ GOOGLE_RECAPTCHA_SITE_KEY}}"></div>

Add Google’s script before your closing </head> tag per their instructions.

    ...
    <script src="https://www.google.com/recaptcha/api.js" async defer></script>
</head>
...

Django Forms work

Your form will need access to the request object in order to send some optional information to the service. So, in your view pass the request obect into the form as a keyword argument.

if request.method == "POST" :
    form = MyForm(request.POST, request=request)
    if form.is_valid() :
        ...

Then, in your form test the recaptcha token and user’s IP against the recaptcha serice by utilizing the request object that you passed to the form in the step above.

from django import forms
from django.conf import settings
from django.utils.translation import ugettext as _
import urllib
import urllib2
import json

class MyForm(forms.ModelForm):
    class Meta :
        model = MyModel

    def __init__(self, *args, **kwargs):
        # make the request object available to the form object
        self.request = kwargs.pop('request', None)
        super(MyForm, self).__init__(*args, **kwargs)

    def clean(self):
        super(MyForm, self).clean()

        # test the google recaptcha
        url = "https://www.google.com/recaptcha/api/siteverify"
        values = {
            'secret': settings.GOOGLE_RECAPTCHA_SECRET_KEY,
            'response': self.request.POST.get(u'g-recaptcha-response', None),
            'remoteip': self.request.META.get("REMOTE_ADDR", None),
        }
        data = urllib.urlencode(values)
        req = urllib2.Request(url, data)
        response = urllib2.urlopen(req)
        result = json.loads(response.read())

        # result["success"] will be True on a success
        if not result["success"]:
            raise forms.ValidationError(_(u'Only humans are allowed to submit this form.'))

        return self.cleaned_data

Success!

Written on April 17, 2015