When 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 urls.py:
from django.contrib import admin admin.autodiscover()
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:
try:
self.user_cache = auth.authenticate(username=username,
password=password)
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'])
self.check_for_test_cookie()
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 urls.py):
url(r'^login/$',
auth_views.login,
{'template_name': 'registration/login.html',
'authentication_form': AuthenticationLockForm},
name='auth_login'),
and we also have to patch admin site to use this form (also in urls.py):
from django.contrib import admin #patch site login form admin.site.login_form=AuthenticationLockForm admin.autodiscover()