Adding Recaptcha in Django REST Framework (Throttling)

Adding Recaptcha in Django REST Framework (Throttling)
Adding Recaptcha in Django REST Framework (Throttling)

In this article, we'll cover how to add Recaptcha in Django REST Framework using throttling.

Adding Recaptcha is always good practice to avoid spambots such as creating dummy users that will fill the database with useless data. In DRF, some public API endpoints (register/login/reset password and etc.) need to be protected from such attacks.

First, we need to get API credentials from the Recaptcha admin to use them later to verify requests. Then, we are going to add a custom Throttle class to manage requests and force them for verification if the limit is exceeded.

Implementation

Create a new Django project with a new app inside. Usually, it's good to keep user-related configurations in account app.

DRF allows limiting the number of requests for specific endpoints. In this case, we'll set a limit to allow a maximum of 3 requests per 6 hours in settings.py:

REST_FRAMEWORK = {
    'DEFAULT_THROTTLE_CLASSES': (
        'rest_framework.throttling.AnonRateThrottle',
        'rest_framework.throttling.UserRateThrottle'
    ),
    'DEFAULT_THROTTLE_RATES': {
        'anon': '500/minute',
        'user': '1000/minute',
        'loginAttempts': '3/hr',

    }
}

We have set 3 throttle rates - loginAttempts will be custom throttling that we'll implement but the anon and user are built-in throttling comes with DRF.

Once you get your Recaptcha credentials, create a new file named throttle.py inside the app.

account/throttle.py

from django.contrib.auth.models import User
from rest_framework.throttling import SimpleRateThrottle

from account.api.utils.helpers import verify_recaptcha


class UserLoginRateThrottle(SimpleRateThrottle):
    scope = 'loginAttempts'

    def get_cache_key(self, request, view):
        user = User.objects.filter(email=request.data.get('email'))
        ident = user[0].pk if user else self.get_ident(request)

        return self.cache_format % {
            'scope': self.scope,
            'ident': ident
        }

    def check_recaptcha(self, request, view):
        g_value = request.data.get('recaptcha')
        if g_value:
            is_verified = verify_recaptcha(g_value)
            return is_verified
        return False

    def allow_request(self, request, view):
        if self.rate is None:
            return True

        self.key = self.get_cache_key(request, view)
        if self.key is None:
            return True

        self.history = self.cache.get(self.key, [])
        self.now = self.timer()

        while self.history and self.history[-1] <= self.now - self.duration:
            self.history.pop()

        is_recaptcha_exists_and_verified = self.check_recaptcha(request, view)
        if len(self.history) >= self.num_requests and not is_recaptcha_exists_and_verified:
            return self.throttle_failure()

        return self.throttle_success()

Now, let's break down the class:

We're inheriting from SimpleRateThrottle class to override allow_request function.

scope = 'loginAttempts' - will be used in settings to define throttle rates.

    def get_cache_key(self, request, view):
        user = User.objects.filter(email=request.data.get('email'))
        ident = user[0].pk if user else self.get_ident(request)

        return self.cache_format % {
            'scope': self.scope,
            'ident': ident
        }

get_cache_key - limits the rate of API calls that may be made by a given user.

This method must be overridden by defining scope and ident. The user will be used as a unique cache key if the user is authenticated. Β For anonymous requests, the IP address of the request will be used which is ident.

    def check_recaptcha(self, request, view):
        g_value = request.data.get('recaptcha')
        if g_value:
            is_verified = verify_recaptcha(g_value)
            return is_verified
        return False

check_recaptcha - will return a boolean that checks if the Recaptcha payload is verified. There is a helper function which sends POST request to google Recaptcha API for verification:

account/helpers.py

def verify_recaptcha(g_token: str) -> bool:
    data = {
        'response': g_token,
        'secret': settings.RE_CAPTCHA_SECRET_KEY

    }
    resp = requests.post('https://www.google.com/recaptcha/api/siteverify', data=data)
    result_json = resp.json()
    return result_json.get('success') is True

For best practices keep your Recaptcha credentials in environment variables and get them in the settings.py by use of os.environ.get() method.

g_token will be passed from the request object as defined in check_recaptcha function. It's the payload of Recaptcha that will be sent from the front-end side and then send for verification alongside with secret key.

    def allow_request(self, request, view):
        if self.rate is None:
            return True

        self.key = self.get_cache_key(request, view)
        if self.key is None:
            return True

        self.history = self.cache.get(self.key, [])
        self.now = self.timer()

        while self.history and self.history[-1] <= self.now - self.duration:
            self.history.pop()

        is_recaptcha_exists_and_verified = self.check_recaptcha(request, view)
        if len(self.history) >= self.num_requests and not is_recaptcha_exists_and_verified:
            return self.throttle_failure()

        return self.throttle_success()

allow_request - Checks to see if the request should be throttled. We override this method by adding extra checks to see if the request includes verified Recaptcha.

self.num_requests - is given the request rate string in settings.

        if len(self.history) >= self.num_requests and not is_recaptcha_exists_and_verified:
            return self.throttle_failure()

So, we're checking if the limit is exceeded and also Recaptcha is not verified, then return throttle_failure() function to prevent further requests. However, if ReCaptcha is verified regardless of the limit, then allow requests by returning throttle_sucess().

Since we have a custom throttling ready, it's time to define it in views:

account/views.py

class RegisterUserAPIView(CreateAPIView):

    permission_classes = [AllowAny]
    serializer_class = UserCreateSerializer
    throttle_classes = (UserLoginRateThrottle,)

    def perform_create(self, serializer):
        user = serializer.save()

    def throttled(self, request, wait):
        raise Throttled(detail={
            "message": "recaptcha_required",
        })

throttle_classes = (UserLoginRateThrottle,) sets throttling for this particular view.

throttled() allows overriding the default behaviour which in this case will return a message recaptcha_required. Then the front-end side can appear Recaptcha and force a request for verification. Even if the limit is exceeded, our custom throttling class will allow requests if there is a verified Recaptcha payload.

Now, you can test the functionality by sending 3 requests in a row and then the response will return recaptcha_required message which is handled by throttling.

Support 🌏

If you feel like you unlocked new skills, please share them with your friends and subscribe to the youtube channel to not miss any valuable information.