From 079c8972032551b5dc9e1f18f94b1888b58a8560 Mon Sep 17 00:00:00 2001 From: Jakub Kuszneruk Date: Tue, 21 Mar 2017 00:32:11 +0100 Subject: [PATCH] Example djangorestframework auth method - sample authentication method described in README piggyback: - typo in lockout.html --- README.md | 83 ++++++++++++++++++++++++ defender/templates/defender/lockout.html | 2 +- 2 files changed, 84 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 52bd52b..121e0e0 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,7 @@ Features - list of blocked usernames and ip's - ability to unblock people - list of recent login attempts +- Can be easly adapted to custom authentication method. Long term goals =============== @@ -340,6 +341,88 @@ long to keep the access attempt records in the database before the management command cleans them up. [Default: ``24``] +Adapting to other authentication method +-------------------- + +`defender` can be used for authentication other than `Django authentication system`. +E.g. if `django-rest-framework` authentication has to be protected from brute force attack,custom authentication method can be implemented. + +There's sample `BasicAuthenticationDefender` class based on `djangorestframework.BasicAuthentication`: + +```python +import base64 +import binascii + +from defender import utils +from defender import config +from django.utils.translation import ugettext_lazy as _ + +from rest_framework import HTTP_HEADER_ENCODING, exceptions + +from rest_framework.authentication import ( + BasicAuthentication, + get_authorization_header, + ) + + +class BasicAuthenticationDefender(BasicAuthentication): + + def get_username_from_request(self, request): + auth = get_authorization_header(request).split() + return base64.b64decode(auth[1]).decode(HTTP_HEADER_ENCODING).partition(':')[0] + + def authenticate(self, request): + auth = get_authorization_header(request).split() + + if not auth or auth[0].lower() != b'basic': + return None + + if len(auth) == 1: + msg = _('Invalid basic header. No credentials provided.') + raise exceptions.AuthenticationFailed(msg) + elif len(auth) > 2: + msg = _('Invalid basic header. Credentials string should not contain spaces.') + raise exceptions.AuthenticationFailed(msg) + + if utils.is_already_locked(request, get_username=self.get_username_from_request): + detail = "You have attempted to login {failure_limit} times, with no success." \ + "Your account is locked for {cooloff_time_seconds} seconds" \ + "".format( + failure_limit=config.FAILURE_LIMIT, + cooloff_time_seconds=config.COOLOFF_TIME + ) + raise exceptions.AuthenticationFailed(_(detail)) + + try: + auth_parts = base64.b64decode(auth[1]).decode(HTTP_HEADER_ENCODING).partition(':') + except (TypeError, UnicodeDecodeError, binascii.Error): + msg = _('Invalid basic header. Credentials not correctly base64 encoded.') + raise exceptions.AuthenticationFailed(msg) + + userid, password = auth_parts[0], auth_parts[2] + login_unsuccessful = False + login_exception = None + try: + response = self.authenticate_credentials(userid, password) + except exceptions.AuthenticationFailed as e: + login_unsuccessful = True + login_exception = e + + utils.add_login_attempt_to_db(request, + login_valid=not login_unsuccessful, + get_username=self.get_username_from_request) + + user_not_blocked = utils.check_request(request, + login_unsuccessful=login_unsuccessful, + get_username=self.get_username_from_request) + if user_not_blocked and not login_unsuccessful: + return response + + raise login_exception + +``` + +To make it works add `BasicAuthenticationDefender` to `DEFAULT_AUTHENTICATION_CLASSES` in your `settings.py`. Running Tests ============= diff --git a/defender/templates/defender/lockout.html b/defender/templates/defender/lockout.html index a365513..294cea2 100644 --- a/defender/templates/defender/lockout.html +++ b/defender/templates/defender/lockout.html @@ -1,7 +1,7 @@

Locked out

-

Your have attempted to login {{failure_limit}} times, with no success. +

You have attempted to login {{failure_limit}} times, with no success. Your account is locked for {{cooloff_time_seconds}} seconds