reCAPTCHA Field for Django Forms

August 27, 2008

Author: jart — Originally Posted 560 Days Ago Article Tags « django python tutorial web »

Integrating ReCAPTCHA seamlessly into Django's form library is a bit tricky but it is possible. There are many other solutions available on the web for using reCAPTCHA with Django but they all involve a lot of manual labor inside your views. This approach will allow you to just add a captcha field to your existing forms. (With one small exception)

reCAPTCHA is an excellent choice for preventing spam. Although other python libraries exist for generating CAPTCHAs, some, such as the gimpy-style CAPTCHAs are not nearly as reCAPTCHA. In addition to having a secure CAPTCHA, you'll also be helping to digitize books; it's a win-win scenario!

Furthermore, it is easy to run into other security pitfalls when generating your own CAPTCHAs. For instance, many websites will store the answer to a CAPTCHA in a session on the server side. Once the form has been submitted, the web application will forget to clear this value. A spammer can then re-use this session multiple times with the same CAPTCHA answer, because the application will not change the answer on the server side until the CAPTCHA image is re-downloaded.

Before we get started, please verify that you are using Django 1.0+ (or the subversion tree as of August 2008.) There are significant differences between the forms library in Django 1.0 and 0.96 and this code will not work on older releases.

You'll also need the python ReCAPTCHA library:

sudo easy_install recaptcha-client

Now that your system is setup, sign up for a ReCAPTCHA account and generate a public and private key for you domain. Put this in your settings.py file:

Excerpt: settings.py
RECAPTCHA_PUBLIC = 'blahblah'
RECAPTCHA_PRIVATE = 'blargh'

Now add the following code to your applications:

File: forms.py
from recaptcha.client import captcha
from django import forms
from django.conf import settings
from django.utils.safestring import mark_safe

class ReCaptcha(forms.Widget):
    input_type = None # Subclasses must define this.

    def render(self, name, value, attrs=None):
        final_attrs = self.build_attrs(attrs, type=self.input_type, name=name)
        html = u"<script>var RecaptchaOptions = {theme : '%s'};</script>" % (
            final_attrs.get('theme', 'white'))
        html += captcha.displayhtml(settings.RECAPTCHA_PUBLIC)
        return mark_safe(html)

    def value_from_datadict(self, data, files, name):
        return {
            'recaptcha_challenge_field': data.get('recaptcha_challenge_field', None),
            'recaptcha_response_field': data.get('recaptcha_response_field', None),
        }

# hack: Inherit from FileField so a hack in Django passes us the
# initial value for our field, which should be set to the IP
class ReCaptchaField(forms.FileField):
    widget = ReCaptcha
    default_error_messages = {
        'invalid-site-public-key': u"Invalid public key",
        'invalid-site-private-key': u"Invalid private key",
        'invalid-request-cookie': u"Invalid cookie",
        'incorrect-captcha-sol': u"Invalid entry, please try again.",
        'verify-params-incorrect': u"The parameters to verify were incorrect, make sure you are passing all the required parameters.",
        'invalid-referrer': u"Invalid referrer domain",
        'recaptcha-not-reachable': u"Could not contact reCAPTCHA server",
    }

    def clean(self, data, initial):
        if initial is None or initial == '':
            raise Exception("ReCaptchaField requires the client's IP be set to the initial value")
        ip = initial
        resp = captcha.submit(data.get("recaptcha_challenge_field", None),
                              data.get("recaptcha_response_field", None),
                              settings.RECAPTCHA_PRIVATE, ip)
        if not resp.is_valid:
            raise forms.ValidationError(self.default_error_messages.get(
                    resp.error_code, "Unknown error: %s" % (resp.error_code)))

class CaptchaForm(forms.Form):
    captcha = ReCaptchaField()
File: views.py
from django.http import HttpResponse
from django.shortcuts import render_to_response
from project.app.forms import CaptchaForm

def captcha_form(request):
    if request.method == "POST":
        # slight hack, we need to give recaptcha the client's IP address
        form = CaptchaForm(request.POST, initial={'captcha': request.META['REMOTE_ADDR']})
        if form.is_valid():
            return HttpResponse('SUCCESS')
    else:
        form = CaptchaForm()
    return render_to_response(request, "captchaform.html", {'form': form})
File: captchaform.html
<form method="post" action="/captchaform/">
  {{ form.as_p }}
  <p><input type="submit"></p>
</form>

Now if all has gone according to plan you should have a CAPTCHA form similar to the one at the bottom of the page. Django will automatically handle all the dirty work when you call form.is_valid().

Developing this solution was especially difficult for two reasons. Firstly, the reCAPTCHA widget requires two form fields. This is possible to do with the pre-1.0 release of Django by overriding value_from_datadict() in your Widget. Be very scared though, because this feature is undocumented. Secondly, reCAPTCHA needs to use the client's IP address to verify the request. The forms library does not let us anywhere near the request object so we're passing the IP from our view through the initial dictionary.

Ready for the scary part? Normally django doesn't give us access to the initial value for a field when cleaning. The only exception is a hack that passes initial to FileFields. So naturally the solution was to falsely masquerade our Field as a FileField. Hey, at least there's no harm done.

Enjoy your new, easy to use CAPTCHA.

Comments

  1. vikash [website] on December 2, 2008 [Permalink]

    HI

  2. JeffreyATW [website] on April 12, 2009 [Permalink]

    You've got an error in views.py: you should check if request.method == "POST". You have it as '!='.

  3. artur gajowy [website] on July 4, 2009 [Permalink]

    you know it's tempting to test it here? ;)

  4. Mike [website] on January 26, 2010 [Permalink]

    helpful to me today! Thanks!

Post Comment