Django : ReCaptha Everywhere
Tags: django, github, security, tutorial
Published: 2021-06-15 04:42:15

In the previous post in this series, I talk about how we can use django-recaptcha to make sure our forms are protected from bot spam submission, however it can be tedious to add them to each form and also does not make it hard to add the recaptcha field if the form are generated by a django package that you had installed previously.

Using the django-honeypot middleware method and knowledge on how django-recaptcha work, I had manage to roll my own middleware that inject the Google Recaptcha field into every page that have a form in a Django weebsite.

##1) Start a new django app

We Can first start an django app called G_Recaptha, the name of the app can be anything you want but I'm going with this.

```
python manage.py startapp G_Recaptha
```

##2) G_Recaptha middleware

We now should add a middleware.py file in G_Recaptha.

in this file, insert the following

```python
import re
from django.utils.safestring import mark_safe
from django.template.loader import render_to_string
from django.conf import settings
from django.utils.encoding import force_str
from .helpers import verify_recaptcha

_POST_FORM_RE = re.compile(
    r'(<form\W[^>]*\bmethod\s*=\s*(\'|"|)POST(\'|"|)\b[^>]*>)', re.IGNORECASE
)
_HTML_TYPES = ("text/html", "application/xhtml+xml")


class BaseRecapthaMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        return self.get_response(request)


class RecaptchaRequestMiddleware(BaseRecapthaMiddleware):
    def process_view(self, request, callback, callback_args, callback_kwargs):
        if request.is_ajax():
            return None

        return verify_recaptcha(request)


class RecaptchaResponseMiddleware(BaseRecapthaMiddleware):

    def __call__(self, request):
        response = self.get_response(request)

        try:
            content_type = response["Content-Type"].split(";")[0]

        except (KeyError, AttributeError):
            content_type = None

        if content_type in _HTML_TYPES:

            def add_recaptcha_field(match):
                return mark_safe(
                    match.group()
                    + render_to_string(
                        "G_Recaptcha/RecaptchaV3.html",
                        {'recaptcha_site_key': settings.RECAPTCHA_PUBLIC_KEY},
                    )
                )

            response.content = _POST_FORM_RE.sub(
                add_recaptcha_field, force_str(response.content)
            )

        return response


class RecaptchaMiddleware(
        RecaptchaRequestMiddleware,
        RecaptchaResponseMiddleware):
    pass

```

this middleware file is almost the exact copy of django-honeypot that have been customize to insert google recaptcha field into and pages in our website that have a form field in the HTML.

##3) G_Recaptha helpers.py

In the middleware.py file on line 6 we call for verify_recaptcha from helpers.py file.

Lets now create a file called helpers.py file and insert the following lines into the file:

```python
import requests
from django.conf import settings
from django.http import HttpResponseBadRequest
from django.template.loader import render_to_string


def verify_recaptcha(request):
    if request.method == "POST":
        recaptcha_response = request.POST.get('g-recaptcha-response')
        data = {
            'secret': settings.RECAPTCHA_PRIVATE_KEY,
            'response': recaptcha_response
        }
        r = requests.post(
            'https://www.google.com/recaptcha/api/siteverify', data=data)
        result = r.json()

        if result['success'] and \
                float(result['score']) > settings.RECAPTCHA_REQUIRED_SCORE:
            return None

        respond = render_to_string(
            'G_Recaptcha/ReCaptcha_error.html',
            {}
        )
        return HttpResponseBadRequest(respond)

```

##4) G_Recaptha templates

In middleware.py file, we force insert a html string into all the pages at line 47. This template is located at 

G_Recaptcha/templates/G_Recaptcha/RecaptchaV3.html.

the html for the template are

```django
<script src='https://www.google.com/recaptcha/api.js?render={{recaptcha_site_key}}'></script>
<script>
    //global grecaptcha
    grecaptcha.ready(function() {
        grecaptcha.execute('{{recaptcha_site_key}}', {action: "form"}).then(function(token) {
        document.getElementById('g-recaptcha-response').value = token;
        });
    });
</script>
<input type="hidden" id="g-recaptcha-response" name="g-recaptcha-response">
```

In helpers.py file we also have a template that responds if the user failed the Google Recaptha test with the template name Recaptcha_error.html located at 

G_Recaptcha/templates/G_Recaptcha/Recaptcha_error.html

the template in its barebone is as follow,

```html
<H1>Google ReCaptha Error </H1>
```
you may want to change the design as required.

##5) G_Recaptcha Settings configurations.

lets now open settings.py file and make the following changes as follows

```python
INSTALLED_APPS = [
    #...
    # Custom Google ReCaptcha
    'G_Recaptcha',
    #...
]

MIDDLEWARE = [
    #...
    # Custom Google ReCaptcha
    'G_Recaptcha.middleware.RecaptchaMiddleware',
    #...
]

# Custom Google Recaptcha
RECAPTCHA_PUBLIC_KEY = config('RECAPTCHA_PUBLIC_KEY')
RECAPTCHA_PRIVATE_KEY = config('RECAPTCHA_PRIVATE_KEY')
RECAPTCHA_REQUIRED_SCORE = config('RECAPTCHA_REQUIRED_SCORE', cast=float)
```

##6)Update .env files

Since we are using python-decouple, we should now update the .env file with the following fields

```
RECAPTCHA_PUBLIC_KEY = '<site_key>'
RECAPTCHA_PRIVATE_KEY = '<public_key>'
RECAPTCHA_REQUIRED_SCORE = <value between 0.00 to 1.00>
```

##7) Testing

the easiest place to test is the admin login page

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cbmhi5qckmbww81j2svv.png)

as you can see, the google recaptcha had been displayed located at the bottom of such form

now we should test if we failed the google recaptcha by setting the `RECAPTCHA_REQUIRED_SCORE = 1.00` in the .env file

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a4xz1hw6d1rjyl8ji6yg.png)
 
as you can see I had failed the recaptcha test.

now lower the  `RECAPTCHA_REQUIRED_SCORE = 1.00` to the value you are comfortable with such as 0.85

you should now be able to login as normal.

## End

We now able to protect all the form in our project with google ReCaptcha. Hopefully this will slowdown or stop any attempt to attack our django webpage.