* Pluggable backends, so people can use something other than Redis
* Email users when their account is blocked
* Add a whitelist for username and ip's that we will never block (admin's, etc)
* Add a permanent black list for IP addresses
* Scan for known proxy IPs and do not block requests coming from those
(improve the chances that a good IP is blocked)
* Add management command to prune old (configurable) login attempts.
Performance
***********
The goal of defender is to make it as fast as possible so that it doesn't slow
down the login process. In order to make sure our goals are met we need a way
to test the application to make sure we are on the right track. The best
way to do this is to compare how fast a normal Django login takes with defender
and django-axes.
The normal django login, would be our baseline, and we expect it to be the
fastest of the 3 methods, because there are no additional checks happening.
The defender login would most likely be slower then the django login, and
hopefully faster then the django-axes login. The goal is to make it as little
of a difference between the regular raw login, and defender.
The django-axes login speed, will probably be the slowest of the three since it
does more checks and does a lot of database queries.
The best way to determine the speed of a login is to do a load test against an
application with each setup, and compare the login times for each type.
Load testing
************
In order to make sure we cover all the different types of logins, in our load
test we need to have more then one test.
#. All success: We will do a load test with nothing but successful logins.
#. Mixed: some success some failure: We will load test with some successful logins and some failures to see how the failure effect the performance.
#. All Failures: We will load test with all failure logins and see the difference in performance.
We will need a sample application that we can use for the load test, with the
only difference is the configuration where we either load defender, axes, or
none of them.
We can use a hosted load testing service, or something like jmeter. Either way
we need to be consistent for all of the tests. If we use jmeter, we should have
our jmeter configuration for others to run the tests on their own.
Results of load tests
*********************
We will post the results here. We will explain each test, and show the results
along with some charts.
Why not django-axes
-------------------
django-axes is great but it puts everything in the database, and this causes
a bottle neck when you have a lot of data. It slows down the auth requests by
as much as 200-300ms. This might not be much for some sites, but for others it
is too long.
This started out as a fork of django-axes, and is using as much of their code
as possible, and removing the parts not needed, and speeding up the lookups
to improve the login.
How django-defender works
-------------------------
#. When someone tries to login, we first check to see if they are currently
blocked. We check the username they are trying to use, as well as the IP
address. If they are blocked, goto step 5. If not blocked go to step 2.
#. They are not blocked, so we check to see if the login was valid. If valid
go to step 6. If not valid go to step 3.
#. Login attempt wasn't valid. Add their username and IP address for this
attempt to the cache. If this brings them over the limit, add them to the
blocked list, and then goto step 5. If not over the limit goto step 4.
#. Login was invalid, but not over the limit. Send them back to the login screen
to try again.
#. User is blocked: Send them to the blocked page, telling them they are
blocked, and give an estimate on when they will be unblocked.
#. Login is valid. Reset any failed login attempts, and forward to their
destination.
Cache backend
-------------
Defender uses the cache to save the failed attempts.
Cache keys
**********
Counters:
* prefix:failed:ip:[ip] (count, TTL)
* prefix:failed:username:[username] (count, TTL)
Booleans (if present it is blocked):
* prefix:blocked:ip:[ip] (true, TTL)
* prefix:blocked:username:[username] (true, TTL)
Customizing django-defender
---------------------------
You have a couple options available to you to customize ``django-defender`` a bit.
These should be defined in your ``settings.py`` file.
*``DEFENDER_LOGIN_FAILURE_LIMIT``\ : Int: The number of login attempts allowed before a
record is created for the failed logins. [Default: ``3``\ ]
*``DEFENDER_LOGIN_FAILURE_LIMIT_USERNAME``\ : Int: The number of login attempts allowed
on a username before a record is created for the failed logins. [Default: ``DEFENDER_LOGIN_FAILURE_LIMIT``\ ]
*``DEFENDER_LOGIN_FAILURE_LIMIT_IP``\ : Int: The number of login attempts allowed
from an IP before a record is created for the failed logins. [Default: ``DEFENDER_LOGIN_FAILURE_LIMIT``\ ]
*``DEFENDER_BEHIND_REVERSE_PROXY``\ : Boolean: Is defender behind a reverse proxy?
[Default: ``False``\ ]
*``DEFENDER_REVERSE_PROXY_HEADER``\ : String: the name of the http header with your
reverse proxy IP address [Default: ``HTTP_X_FORWARDED_FOR``\ ]
*``DEFENDER_LOCK_OUT_BY_IP_AND_USERNAME``\ : Boolean: Locks a user out based on a combination of IP and Username. This stops a user denying access to the application for all other users accessing the app from behind the same IP address. [Default: ``False``\ ]
*``DEFENDER_DISABLE_IP_LOCKOUT``\ : Boolean: If this is True, it will not lockout the users IP address, it will only lockout the username. [Default: False]
*``DEFENDER_DISABLE_USERNAME_LOCKOUT``\ : Boolean: If this is True, it will not lockout usernames, it will only lockout IP addresess. [Default: False]
*``DEFENDER_COOLOFF_TIME``\ : Int: If set, defines a period of inactivity after which
*``DEFENDER_LOCKOUT_TEMPLATE``\ : String: [Default: ``None``\ ] If set, specifies a template to render when a user is locked out. Template receives the following context variables:
*``DEFENDER_REDIS_PASSWORD_QUOTE``\ : Boolean: if special character in redis password (like '@'), we can quote password ``urllib.parse.quote("password!@#")``, and set to True.
*``DEFENDER_REDIS_NAME``\ : String: the name of the cache from ``CACHES`` in your Django settings (e.g. ``"default"``). If set, ``DEFENDER_REDIS_URL`` will be ignored.
While using ``DEFENDER_COOLOFF_TIME`` alone is sufficent for most use cases, when using ``defender`` in some specific scenarios such as in a high security setting, developers may wish to have finer
grained control over how long invalid login attempts are "remembered" while under consideration for lockout compared to the time those lockout keys are actually locked out from the system.
``DEFENDER_ATTEMPT_COOLOFF_TIME`` and ``DEFENDER_LOCKOUT_COOLOFF_TIME`` allow for this exact fine grained configuration.
We can also take a low security and low scale example like a high school's website. Such a website might be run on some of the school's computers and administrated by the school's IT staff and computer
science teachers (if lucky enough to have any). In this scenario we can imagine that there are significant portions of the website accessible without authentication, but logging in to the website could
provide access to some relatively privileged information such as the student's name, email, grades, and class schedule. Finally since there is an email linked with the account, we will assume that there
is password reset functionality which unblocks the account when completed. In such a case, one could imagine that there is no need to remember failed logins for long periods of time since the application
would simply wish to protect against potential denial of service attacks. This could be accomplished keeping ``DEFENDER_ATTEMPT_COOLOFF_TIME`` low, say 30 seconds, and setting ``DEFENDER_LOCKOUT_COOLOFF_TIME``
to something much higher like 600 seconds. By keeping ``DEFENDER_ATTEMPT_COOLOFF_TIME`` low and locking out bad actors for significant periods of time by setting ``DEFENDER_LOCKOUT_COOLOFF_TIME`` high,
rapid brute force login attacks will still be defeated and their small server will have more space in their cache for other data. And by providing password reset functionality as described above, these hypothetical
administrators could limit their required involvement in unblocking real users while retaining the intended accessibility of their website.
While the previous example is somewhat contrived, the full power of these configurations is demonstrated with the following explanation and example.
When ``DEFENDER_STORE_ACCESS_ATTEMPTS`` is True, ``DEFENDER_LOCKOUT_COOLOFF_TIME`` can also be configured as a list of integers. When configured as a list,
the number of previous failed login attempts for the configured lockout key is divided by ``DEFENDER_LOGIN_FAILURE_LIMIT`` to produce an intentionally overestimated count
of the number of failed logins for the period defined by ``DEFENDER_ACCESS_ATTEMPT_EXPIRATION``. This ends up being an overestimate because the time between the failed login attempts
is not considered when doing this calculation. While this may seem harsh, in some specific scenarios the additional protection against slower attacks can be worth the\ potential\ inconvenience
caused to real users of the system.
One such example of this could be a public web accessible web application that houses sensitive information of it's users (let's say personal financial records).
The application and data therein should be accessible with minimal interruption, however security is integral so delays can be tolerated up to a point.
Under these circumstances we may have a desire to simply set ``DEFENDER_COOLOFF_TIME`` to a very large integer or even 0 for maximum protection. But this would mean that
if a real user\ does\ get locked out of the system, we will need an administrator to manually unblock them which of course is cumbersome and costly.
By setting ``DEFENDER_ATTEMPT_COOLOFF_TIME`` to a large enough number, let's say 600 and setting ``DEFENDER_LOCKOUT_COOLOFF_TIME`` to a list of increasing integers (ie. [60, 120, 300, 600, 0]) we can
protect our theoretical application comprably to if we had simply set ``DEFENDER_COOLOFF_TIME`` to 600 while disrupting our users significantly less.
To make it work add ``BasicAuthenticationDefender`` to ``DEFAULT_AUTHENTICATION_CLASSES`` above all other authentication methods in your ``settings.py``.
Below is a sample ``BasicAuthenticationDefender`` class based on ``rest_framework.authentication.TokenAuthentication`` which uses ``django-rest-auth`` library for user authentication.
..code-block:: python
import base64
import binascii
from django.conf import settings
from django.contrib.auth import get_user_model, authenticate
from django.contrib.auth.forms import PasswordResetForm, SetPasswordForm
from django.contrib.auth.tokens import default_token_generator
from django.utils.http import urlsafe_base64_decode as uid_decoder
To make it work add ``BasicAuthenticationDefender`` to ``REST_AUTH_SERIALIZERS`` dictionary in your ``settings.py`` under the key ``LOGIN_SERIALIZER``.
For example, in your settings.py add the below line,
``defender`` can be adapted for Django’s ``PasswordResetView`` to prevent too many submissions.
We need to create some new views that subclass Django’s built-in ``LoginView``, ``PasswordResetView`` & ``PasswordResetConfirmView``—then use these views in our ``urls.py`` as replacements for Django’s built-ins.
The views block based on email address submitted on the password reset view. This is different than the default implementation (which uses username), so we have to be careful to clean up after ourselves on sign-in & completed password reset.
..code-block:: python
from defender import utils as def_utils
from django.contrib.auth import views as auth_views
class UserSignIn(auth_views.LoginView):
def form_valid(self, form):
"""Force clear all the cached Defender statues for the authenticated user’s email address."""