Protecting Django Application Against Brute Force Password Guessing

lockWhen you bring  your web application live, you can expected various types of attacks –   one could be a brute force scanning of possible logins.   As a standard mean of prevention against such types of attacks login should be temporarily disabled after some number of unsuccessful attempts.  For Django nice package called django-lockout exists.

Main advantage of this package is that it keeps history of unsuccessful login attempts in memory (using Django cache system),  so checks are very quick.   django-lockout is fairly easy to implement, however I’ve found one issue, when it is used together with django admin site.

django-lockout is  monkey-patching django.contrib.auth.authenticate function (this function is wrapped in custom decorator, that handles checking and blocking of unsuccessful login attempts).  This patching is done in  middle-ware class of django-lockout , as part of  its instantiation.   However as many things in Django,  instantiation of middle-ware classes    is done in latest possible moment –  in this case when first request comes.   And this was source of my issues with django-lockout –   if  we get reference to  django.contrib.auth.authenticate function before middle-ware instantiation,  we get unpatched version of the function.

And exactly this happens if we include admin site  into

from django.contrib import admin

Import of admin causes also import of  django.contrib.auth.forms.AuthenticationForm , which imports:

from django.contrib.auth import authenticate, get_user_model

so we end up with AuthenticationForm, which uses unpatched  authenticate function.   To fix this we  create subclass of  AuthenticationForm :

from django.contrib.auth.forms import AuthenticationForm
from django.contrib import auth
from lockout.exceptions import LockedOut

class AuthenticationLockForm(AuthenticationForm):

    def clean(self):
        username = self.cleaned_data.get('username')
        password = self.cleaned_data.get('password')
        if username and password:
                self.user_cache = auth.authenticate(username=username,
            except LockedOut:
                raise ValidationError(_('Login is temporarily disabled due to high amount of unsuccessful attempts from your address - try again later'))
            if self.user_cache is None:
                raise forms.ValidationError(
                    self.error_messages['invalid_login']% {'username': self.username_field.verbose_name})
            elif not self.user_cache.is_active:
                raise forms.ValidationError(self.error_messages['inactive'])
        return self.cleaned_data

Method clean is overridden with two important changes –   first –  we refer auth.authenticate  (via package reference) so we get now patched version of the function,  second – we handle LockedOut exception to provide appropriate message.

This modified form has to be used for our application login page (in

    {'template_name': 'registration/login.html',
    'authentication_form': AuthenticationLockForm},

and we also have to patch admin site to use this form (also in

from django.contrib import admin
#patch site login form





Leave a Reply

Your email address will not be published. Required fields are marked *