diff --git a/axes/checks.py b/axes/checks.py
index 56f6283..ab31205 100644
--- a/axes/checks.py
+++ b/axes/checks.py
@@ -27,7 +27,9 @@ def axes_cache_backend_check(app_configs, **kwargs): # pylint: disable=unused-a
axes_cache_backend = axes_cache_config.get('BACKEND', '')
axes_cache_incompatible_backends = [
+ 'django.core.cache.backends.dummy.DummyCache',
'django.core.cache.backends.locmem.LocMemCache',
+ 'django.core.cache.backends.filebased.FileBasedCache',
]
axes_handler = getattr(settings, 'AXES_HANDLER', '')
diff --git a/axes/conf.py b/axes/conf.py
index 75f25fc..b405204 100644
--- a/axes/conf.py
+++ b/axes/conf.py
@@ -21,6 +21,13 @@ class AxesAppConf(AppConf):
# see if the user has set axes to lock out logins after failure limit
LOCK_OUT_AT_FAILURE = True
+ # lock out with the combination of username and IP address
+ LOCK_OUT_BY_COMBINATION_USER_AND_IP = False
+
+ # lock out with username and never the IP or user agent
+ ONLY_USER_FAILURES = False
+
+ # lock out with the user agent, has no effect when ONLY_USER_FAILURES is set
USE_USER_AGENT = False
# use a specific username field to retrieve from login POST data
@@ -32,15 +39,9 @@ class AxesAppConf(AppConf):
# use a provided callable to transform the POSTed username into the one used in credentials
USERNAME_CALLABLE = None
- # only check user name and not location or user_agent
- ONLY_USER_FAILURES = False
-
# reset the number of failed attempts after one successful attempt
RESET_ON_SUCCESS = False
- # lock out user from particular IP based on combination USER+IP
- LOCK_OUT_BY_COMBINATION_USER_AND_IP = False
-
DISABLE_ACCESS_LOG = False
DISABLE_SUCCESS_ACCESS_LOG = False
diff --git a/axes/tests/settings.py b/axes/tests/settings.py
index e150568..46d46ad 100644
--- a/axes/tests/settings.py
+++ b/axes/tests/settings.py
@@ -10,6 +10,9 @@ DATABASES = {
CACHES = {
'default': {
+ # This cache backend is OK to use in development and testing
+ # but has the potential to break production setups with more than on server
+ # due to each computer having their own filesystems and cache files
'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',
'LOCATION': os.path.abspath(os.path.join(tempfile.gettempdir(), 'axes')),
'OPTIONS': {
diff --git a/docs/architecture.rst b/docs/architecture.rst
new file mode 100644
index 0000000..69e4e62
--- /dev/null
+++ b/docs/architecture.rst
@@ -0,0 +1,124 @@
+.. _architecture:
+
+Architecture
+============
+
+Axes is based on the existing Django authentication backend
+architecture and framework for recognizing users and aims to be
+compatible with the stock design and implementation of Django
+while offering extensibility and configurability for using the
+Axes authentication monitoring and logging for users of the package
+as well as 3rd party package vendors such as Django REST Framework,
+Django Allauth, Python Social Auth and so forth.
+
+The development of custom 3rd party package support are active goals,
+but you should check the up-to-date documentation and implementation
+of Axes for current compatibility before using Axes with custom solutions
+and make sure that authentication monitoring is working correctly.
+
+This document describes the Django authentication flow
+and how Axes augments it to achieve authentication and login
+monitoring and lock users out on too many access attempts.
+
+Django authentication flow
+--------------------------
+
+When a user tries to log in in Django, the login is usually performed
+by running a number of authentication backends that check user login
+information by calling the ``django.contrib.auth.authenticate`` function.
+
+If an authentication backend does not approve of a user login,
+it can raise a ``django.core.exceptions.PermissionDenied`` exception.
+
+If a login fails, Django then fires a
+``from django.contrib.auth.signals.user_login_failed`` signal.
+
+If this signal raises an exception, it is propagated through the
+Django middleware stack where it can be caught, or alternatively
+where it can bubble up to the default Django exception handlers.
+
+A normal login flow for Django runs as follows::
+
+ 1. Login view is called by, for example,
+ a user sending form data with browser.
+
+ 2. django.contrib.auth.authenticate is called by
+ the view code to check the authentication request
+ for credentials and return a user object matching them.
+
+ 3. AUTHENTICATION_BACKENDS are iterated over
+ and their authenticate methods called one-by-one.
+
+ 4. An authentication backend either returns
+ a user object which results in that user
+ being logged in or returns None.
+ If a PermissionDenied error is raised
+ by any of the authentication backends
+ the whole request authentication flow
+ is aborted and signal handlers triggered.
+
+
+Django authentication flow with Axes
+------------------------------------
+
+Axes monitors logins with the ``user_login_failed`` signal handler
+and after login attempts exceed the given maximum, starts blocking them.
+
+Django emits the ``user_login_failed`` signal when an authentication backend
+either raises the PermissionDenied signal or alternatively no authentication backend
+manages to recognize a given authentication request and return a user for it.
+
+The blocking is done by ``AxesBackend`` which checks every request
+coming through the Django authentication flow and verifies they
+are not blocked, and allows the requests to go through if the check passes.
+
+If any of the checks fails, an exception is raised which interrupts
+the login process and triggers the Django login failed signal handlers.
+
+Another exception is raised by a Axes signal handler, which is
+then caught by ``AxesMiddleware`` and converted into a readable
+error because the user is currently locked out of the system.
+
+Axes implements the lockout flow as follows::
+
+ 1. Login view is called.
+
+ 2. django.contrib.auth.authenticate is called.
+
+ 3. AUTHENTICATION_BACKENDS are iterated over
+ where axes.backends.AxesBackend is the first.
+
+ 4. AxesBackend checks authentication request
+ for lockout rules and either aborts the
+ authentication flow or lets the authentication
+ process proceed to the next configured
+ authentication backend in the list.
+
+ [Axes handler runs at this this stage if appropriate]
+
+ 5. If the user authentication request fails due to
+ any reason, e.g. a lockout or wrong credentials,
+ Axes receives authentication failure information
+ via the axes.signals.handle_user_login_failed signal.
+
+ 6. The selected Axes handler is run to check
+ the user login failure statistics and rules.
+
+ [Axes default handler implements these steps]
+
+ 7. Axes logs the failure and increments the failure
+ counters which keep track of failure statistics.
+
+ 8. AxesSignalPermissionDenied exception is raised
+ if appropriate and it bubbles up the middleware stack.
+
+ 9. AxesMiddleware processes the exception
+ and returns a readable lockout message to the user.
+
+This plugin assumes that the login views either call
+the ``django.contrib.auth.authenticate`` method to log in users
+or otherwise take care of notifying Axes of authentication
+attempts or login failures the same way Django does.
+
+The login flows can be customized and the Axes
+authentication backend or middleware can be easily swapped.
diff --git a/docs/captcha.rst b/docs/captcha.rst
deleted file mode 100644
index 4accb44..0000000
--- a/docs/captcha.rst
+++ /dev/null
@@ -1,47 +0,0 @@
-.. _captcha:
-
-Using a captcha
-===============
-
-Using https://github.com/mbi/django-simple-captcha you do the following:
-
-1. Change axes lockout url in ``settings.py``::
-
- AXES_LOCKOUT_URL = '/locked'
-
-2. Add the url in ``urls.py``::
-
- url(r'^locked/$', locked_out, name='locked_out'),
-
-3. Create a captcha form::
-
- class AxesCaptchaForm(forms.Form):
- captcha = CaptchaField()
-
-4. Create a captcha view for the above url that resets on captcha success and redirects::
-
- def locked_out(request):
- if request.POST:
- form = AxesCaptchaForm(request.POST)
- if form.is_valid():
- ip = get_ip_address_from_request(request)
- reset(ip=ip)
- return HttpResponseRedirect(reverse_lazy('signin'))
- else:
- form = AxesCaptchaForm()
-
- return render_to_response('locked_out.html', dict(form=form), context_instance=RequestContext(request))
-
-5. Add a captcha template::
-
-
-
diff --git a/docs/configuration.rst b/docs/configuration.rst
index f896664..7f3a16d 100644
--- a/docs/configuration.rst
+++ b/docs/configuration.rst
@@ -3,242 +3,45 @@
Configuration
=============
-Add ``axes`` to your ``INSTALLED_APPS``::
+Minimal Axes configuration is done with just ``settings.py`` updates.
- INSTALLED_APPS = [
- 'django.contrib.admin',
- 'django.contrib.auth',
- 'django.contrib.contenttypes',
- 'django.contrib.sessions',
- 'django.contrib.messages',
- 'django.contrib.staticfiles',
-
- # ... other applications per your preference.
-
- 'axes',
- ]
-
-Add ``axes.backends.AxesBackend`` to the top of ``AUTHENTICATION_BACKENDS``::
-
- AUTHENTICATION_BACKENDS = [
- # AxesBackend should be the first backend in the list.
- # It stops the authentication flow when a user is locked out.
- 'axes.backends.AxesBackend',
-
- # ... other authentication backends per your preference.
-
- # Django ModelBackend is the default authentication backend.
- 'django.contrib.auth.backends.ModelBackend',
- ]
-
-Add ``axes.middleware.AxesMiddleware`` to your list of ``MIDDLEWARE``::
-
- MIDDLEWARE = [
- # The following is the list of default middleware in new Django projects.
- 'django.middleware.security.SecurityMiddleware',
- 'django.contrib.sessions.middleware.SessionMiddleware',
- 'django.middleware.common.CommonMiddleware',
- 'django.middleware.csrf.CsrfViewMiddleware',
- 'django.contrib.auth.middleware.AuthenticationMiddleware',
- 'django.contrib.messages.middleware.MessageMiddleware',
- 'django.middleware.clickjacking.XFrameOptionsMiddleware',
-
- # ... other middleware per your preference.
-
- # AxesMiddleware should be the last middleware in the list.
- # It pretty formats authentication errors into readable responses.
- 'axes.middleware.AxesMiddleware',
- ]
-
-Run ``python manage.py migrate`` to sync the database.
-
-How does Axes function?
------------------------
-
-When a user tries to log in in Django, the login is usually performed
-by running a number of authentication backends that check user login
-information by calling the ``django.contrib.auth.authenticate`` function.
-
-If an authentication backend does not approve of a user login,
-it can raise a ``django.core.exceptions.PermissionDenied`` exception.
-
-If a login fails, Django then fires a
-``from django.contrib.auth.signals.user_login_failed`` signal.
-
-If this signal raises an exception, it is propagated through the
-Django middleware stack where it can be caught, or alternatively
-where it can bubble up to the default Django exception handlers.
-
-A normal login flow for Django runs as follows::
-
- 1. Django or plugin login view is called by
- for example user sending form data with browser.
-
- 2. django.contrib.auth.authenticate is called by
- the view code to check the authentication request
- for user and return a user object matching it.
-
- 3. AUTHENTICATION_BACKENDS are iterated over
- and their authenticate methods called one-by-one.
-
- 4. An authentication backend either returns
- a user object which results in that user
- being logged in or returns None.
- If a PermissionDenied error is raised
- by any of the authentication backends
- the whole request authentication flow
- is aborted and signal handlers triggered.
-
-Axes monitors logins with the ``user_login_failed`` signal handler
-and after login attempts exceed the given maximum, starts blocking them.
-
-The blocking is done by ``AxesBackend`` which checks every request
-coming through the Django authentication flow and verifies they
-are not blocked, and allows the requests to go through if the check passes.
-
-If any of the checks fails, an exception is raised which interrupts
-the login process and triggers the Django login failed signal handlers.
-
-Another exception is raised by a Axes signal handler, which is
-then caught by ``AxesMiddleware`` and converted into a readable
-error because the user is currently locked out of the system.
-
-Axes implements the lockout flow as follows::
-
- 1. Django or plugin login view is called.
-
- 2. django.contrib.auth.authenticate is called.
-
- 3. AUTHENTICATION_BACKENDS are iterated over
- where axes.backends.AxesBackend is the first.
-
- 4. AxesBackend checks authentication request
- for lockouts rules and either aborts the
- authentication flow or lets the authentication
- process proceed to the next
- configured authentication backend.
-
- [The lockout happens at this stage if appropriate]
-
- 5. User is locked out and signal handlers
- are notified of the failed login attempt.
-
- 6. axes.signals.log_user_login_failed runs
- and raises a AxesSignalPermissionDenied
- exception that bubbles up the middleware stack.
-
- 7. AxesMiddleware processes the exception
- and returns a readable error to the user.
-
-This plugin assumes that the login views either call
-the django.contrib.auth.authenticate method to log in users
-or otherwise take care of notifying Axes of authentication
-attempts or login failures the same way Django does.
-
-The login flows can be customized and the Axes
-authentication backend or middleware can be easily swapped.
-
-Running checks
---------------
-
-Use the ``python manage.py check`` command to verify the correct configuration in both
-development and production environments. It is probably best to use this step as part
-of your regular CI workflows to verify that your project is not misconfigured.
-
-Axes uses the checks to verify your cache configuration to see that your caches
-should be functional with the configuration of Axes. Many people have different configurations
-for their development and production environments.
+More advanced configuration and integrations might require updates
+on source code level depending on your project implementation.
-Known configuration problems
+Configuring project settings
----------------------------
-Axes has a few configuration issues with external packages and specific cache backends
-due to their internal implementations.
+The following ``settings.py`` options are available for customizing Axes behaviour.
-Cache problems
-~~~~~~~~~~~~~~
-
-If you are running Axes on a deployment with in-memory Django cache,
-the ``axes_reset`` functionality might not work predictably.
-
-Axes caches access attempts application-wide, and the in-memory cache
-only caches access attempts per Django process, so for example
-resets made in one web server process or the command line with ``axes_reset``
-might not remove lock-outs that are in the sepate process' in-memory cache
-such as the web server process serving your login or admin page.
-
-To circumvent this problem please use somethings else than
-``django.core.cache.backends.locmem.LocMemCache`` as your
-cache backend in Django cache ``BACKEND`` setting.
-
-If it is not an option to change the default cache you can add a cache
-specifically for use with Axes. This is a two step process. First you need to
-add an extra cache to ``CACHES`` with a name of your choice::
-
- CACHES = {
- 'default': {
- 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
- },
- 'axes_cache': {
- 'BACKEND': 'django.core.cache.backends.dummy.DummyCache',
- }
- }
-
-The next step is to tell Axes to use this cache through adding ``AXES_CACHE``
-to your ``settings.py`` file::
-
- AXES_CACHE = 'axes_cache'
-
-There are no known problems in other cache backends such as
-``DummyCache``, ``FileBasedCache``, or ``MemcachedCache`` backends.
-
-Authentication backend problems
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-If you get ``AxesBackendRequestParameterRequired`` exceptions,
-make sure any auth libraries and middleware you use pass the request object to authenticate.
-Notably in older versions of Django Rest Framework (DRF) (before 3.7.0), ``BasicAuthentication`` does not pass request.
-`Here is an example workaround for DRF `_.
-
-Reverse proxy configuration
----------------------------
-
-Django Axes makes use of ``django-ipware`` package to detect the IP address of the client
-and uses some conservative configuration parameters by default for security.
-
-If you are using reverse proxies, you will need to configure one or more of the
-following settings to suit your set up to correctly resolve client IP addresses:
-
-* ``AXES_PROXY_COUNT``: The number of reverse proxies in front of Django as an integer. Default: ``None``
-* ``AXES_META_PRECEDENCE_ORDER``: The names of ``request.META`` attributes as a tuple of strings
- to check to get the client IP address. Check the Django documentation for header naming conventions.
- Default: ``IPWARE_META_PRECEDENCE_ORDER`` setting if set, else ``('REMOTE_ADDR', )``
-
-Customizing Axes
-----------------
-
-You have a couple options available to you to customize ``django-axes`` a bit.
-These should be defined in your ``settings.py`` file.
-
-* ``AXES_CACHE``: The name of the cache for Axes to use.
- Default: ``'default'``
* ``AXES_FAILURE_LIMIT``: The number of login attempts allowed before a
record is created for the failed logins. Default: ``3``
* ``AXES_LOCK_OUT_AT_FAILURE``: After the number of allowed login attempts
are exceeded, should we lock out this IP (and optional user agent)?
Default: ``True``
-* ``AXES_USE_USER_AGENT``: If ``True``, lock out / log based on an IP address
- AND a user agent. This means requests from different user agents but from
- the same IP are treated differently. Default: ``False``
* ``AXES_COOLOFF_TIME``: If set, defines a period of inactivity after which
- old failed login attempts will be forgotten. Can be set to a python
+ old failed login attempts will be cleared. Can be set to a Python
timedelta object or an integer. If an integer, will be interpreted as a
number of hours. Default: ``None``
-* ``AXES_HANDLER``: If set, overrides the default signal handler backend.
- Default: ``'axes.handlers.database.AxesDatabaseHandler'``
+* ``AXES_LOCK_OUT_BY_COMBINATION_USER_AND_IP``: If ``True``, prevent login
+ from IP under a particular username if the attempt limit has been exceeded,
+ otherwise lock out based on IP.
+ Default: ``False``
+* ``AXES_ONLY_USER_FAILURES`` : If ``True``, only lock based on username,
+ and never lock based on IP if attempts exceed the limit.
+ Otherwise utilize the existing IP and user locking logic.
+ Default: ``False``
+* ``AXES_USE_USER_AGENT``: If ``True``, lock out and log based on the IP address
+ and the user agent. This means requests from different user agents but from
+ the same IP are treated differently. This settings has no effect if the
+ ``AXES_ONLY_USER_FAILURES`` setting is active. Default: ``False``
* ``AXES_LOGGER``: If set, specifies a logging mechanism for Axes to use.
Default: ``'axes.watch_login'``
+* ``AXES_HANDLER``: The path to to handler class to use.
+ If set, overrides the default signal handler backend.
+ Default: ``'axes.handlers.database.DatabaseHandler'``
+* ``AXES_CACHE``: The name of the cache for Axes to use.
+ Default: ``'default'``
* ``AXES_LOCKOUT_TEMPLATE``: If set, specifies a template to render when a
user is locked out. Template receives cooloff_time and failure_limit as
context variables. Default: ``None``
@@ -259,13 +62,6 @@ These should be defined in your ``settings.py`` file.
dictionaries based on ``AXES_USERNAME_FORM_FIELD``. Default: ``None``
* ``AXES_PASSWORD_FORM_FIELD``: the name of the form or credentials field that contains your
users password. Default: ``password``
-* ``AXES_LOCK_OUT_BY_COMBINATION_USER_AND_IP``: If ``True`` prevents the login
- from IP under a particular user if the attempt limit has been exceeded,
- otherwise lock out based on IP.
- Default: ``False``
-* ``AXES_ONLY_USER_FAILURES`` : If ``True`` only locks based on user id and never locks by IP
- if attempts limit exceed, otherwise utilize the existing IP and user locking logic
- Default: ``False``
* ``AXES_NEVER_LOCKOUT_GET``: If ``True``, Axes will never lock out HTTP GET requests.
Default: ``False``
* ``AXES_NEVER_LOCKOUT_WHITELIST``: If ``True``, users can always login from whitelisted IP addresses.
@@ -276,3 +72,116 @@ These should be defined in your ``settings.py`` file.
* ``AXES_DISABLE_ACCESS_LOG``: If ``True``, disable all access logging, so the admin interface will be empty. Default: ``False``
* ``AXES_DISABLE_SUCCESS_ACCESS_LOG``: If ``True``, successful logins will not be logged, so the access log shown in the admin interface will only list unsuccessful login attempts. Default: ``False``
* ``AXES_RESET_ON_SUCCESS``: If ``True``, a successful login will reset the number of failed logins. Default: ``False``
+
+The configuration option precedences for the access attempt monitoring are:
+
+1. Default: only use IP address.
+2. ``AXES_ONLY_USER_FAILURES``: only user username (``AXES_USE_USER_AGENT`` has no effect).
+3. ``AXES_LOCK_OUT_BY_COMBINATION_USER_AND_IP``: use username and IP address.
+
+The ``AXES_USE_USER_AGENT`` setting can be used with username and IP address or just IP address monitoring,
+but does nothing when the ``AXES_ONLY_USER_FAILURES`` setting is set.
+
+
+Configuring reverse proxies
+---------------------------
+
+Axes makes use of ``django-ipware`` package to detect the IP address of the client
+and uses some conservative configuration parameters by default for security.
+
+If you are using reverse proxies, you will need to configure one or more of the
+following settings to suit your set up to correctly resolve client IP addresses:
+
+* ``AXES_PROXY_COUNT``: The number of reverse proxies in front of Django as an integer. Default: ``None``
+* ``AXES_META_PRECEDENCE_ORDER``: The names of ``request.META`` attributes as a tuple of strings
+ to check to get the client IP address. Check the Django documentation for header naming conventions.
+ Default: ``IPWARE_META_PRECEDENCE_ORDER`` setting if set, else ``('REMOTE_ADDR', )``
+
+
+Configuring handlers
+--------------------
+
+Axes uses and provides handlers for processing signals and events
+from Django authentication and login attempts.
+
+The following handlers are offered by default and can be configured
+with the ``AXES_HANDLER`` setting in project configuration:
+
+- ``axes.handlers.database.AxesDatabaseHandler``
+ logs attempts to database and creates AccessAttempt and AccessLog records
+ that persist until removed from the database manually or automatically
+ after their cool offs expire (checked on each login event).
+- ``axes.handlers.cache.AxesCacheHandler``
+ only uses the cache for monitoring attempts and does not persist data
+ other than in the cache backend; this data can be purged automatically
+ depending on your cache configuration, so the cache handler is by design
+ less secure than the database backend but offers higher throughput
+ and can perform better with less bottlenecks.
+ The cache backend should ideally be used with a central cache system
+ such as a Memcached cache and should not rely on individual server
+ state such as the local memory or file based cache does.
+- ``axes.handlers.dummy.AxesDummyHandler``
+ does nothing with attempts and can be used to disable Axes handlers
+ if the user does not wish Axes to execute any logic on login signals.
+ Note that this effectively disables any Axes security features,
+ and is meant to be used on e.g. local development setups
+ and testing deployments where login monitoring is not wanted.
+
+
+Configuring caches
+------------------
+
+If you are running Axes with the cache based handler on a deployment with a
+local Django cache, the Axes lockout and reset functionality might not work
+predictably if the cache in use is not the same for all the Django processes.
+
+Axes needs to cache access attempts application-wide, and e.g. the
+in-memory cache only caches access attempts per Django process, so for example
+resets made in the command line might not remove lock-outs that are in a sepate
+processes in-memory cache such as the web server serving your login or admin page.
+
+To circumvent this problem, please use somethings else than
+``django.core.cache.backends.locmem.LocMemCache`` as your
+cache backend in Django cache ``BACKEND`` setting.
+
+If changing the ``'default'`` cache is not an option, you can add a cache
+specifically for use with Axes. This is a two step process. First you need to
+add an extra cache to ``CACHES`` with a name of your choice::
+
+ CACHES = {
+ 'axes': {
+ 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
+ 'LOCATION': '127.0.0.1:11211',
+ }
+ }
+
+The next step is to tell Axes to use this cache through adding ``AXES_CACHE``
+to your ``settings.py`` file::
+
+ AXES_CACHE = 'axes'
+
+There are no known problems in e.g. ``MemcachedCache`` or Redis based caches.
+
+
+Configuring authentication backends
+-----------------------------------
+
+Axes requires authentication backends to pass request objects
+with the authentication requests for performing monitoring.
+
+If you get ``AxesBackendRequestParameterRequired`` exceptions,
+make sure any libraries and middleware you use pass the request object.
+
+Please check the integration documentation for further information.
+
+
+Configuring 3rd party apps
+--------------------------
+
+Refer to the integration documentation for Axes configuration
+with third party applications and plugins such as
+
+- Django REST Framework
+- Django Allauth
+- Django Simple Captcha
+
diff --git a/docs/customization.rst b/docs/customization.rst
new file mode 100644
index 0000000..3b4dac5
--- /dev/null
+++ b/docs/customization.rst
@@ -0,0 +1,144 @@
+.. customization:
+
+Customization
+=============
+
+Axes can be customized and extended by using the correct signals.
+
+Axes listens to the following signals from ``django.contrib.auth.signals`` to log access attempts:
+
+* ``user_logged_in``
+* ``user_logged_out``
+* ``user_login_failed``
+
+You can also use Axes with your own auth module, but you'll need
+to ensure that it sends the correct signals in order for Axes to
+log the access attempts.
+
+Customizing authentication views
+--------------------------------
+
+Here is a more detailed example of sending the necessary signals using
+and a custom auth backend at an endpoint that expects JSON
+requests. The custom authentication can be swapped out with ``authenticate``
+and ``login`` from ``django.contrib.auth``, but beware that those methods take
+care of sending the nessary signals for you, and there is no need to duplicate
+them as per the example.
+
+``example/forms.py``::
+
+ from django import forms
+
+ class LoginForm(forms.Form):
+ username = forms.CharField(max_length=128, required=True)
+ password = forms.CharField(max_length=128, required=True)
+
+``example/views.py``::
+
+ from django.contrib.auth import signals
+ from django.http import JsonResponse, HttpResponse
+ from django.utils.decorators import method_decorator
+ from django.views import View
+ from django.views.decorators.csrf import csrf_exempt
+
+ from axes.decorators import axes_dispatch
+
+ from example.forms import LoginForm
+ from example.authentication import authenticate, login
+
+
+ @method_decorator(axes_dispatch, name='dispatch')
+ @method_decorator(csrf_exempt, name='dispatch')
+ class Login(View):
+ """
+ Custom login view that takes JSON credentials
+ """
+
+ http_method_names = ['post']
+
+ def post(self, request):
+ form = LoginForm(request.POST)
+
+ if not form.is_valid():
+ # inform django-axes of failed login
+ signals.user_login_failed.send(
+ sender=User,
+ request=request,
+ credentials={
+ 'username': form.cleaned_data.get('username'),
+ },
+ )
+ return HttpResponse(status=400)
+
+ user = authenticate(
+ request=request,
+ username=form.cleaned_data.get('username'),
+ password=form.cleaned_data.get('password'),
+ )
+
+ if user is not None:
+ login(request, user)
+
+ signals.user_logged_in.send(
+ sender=User,
+ request=request,
+ user=user,
+ )
+
+ return JsonResponse({
+ 'message':'success'
+ }, status=200)
+
+ # inform django-axes of failed login
+ signals.user_login_failed.send(
+ sender=User,
+ request=request,
+ credentials={
+ 'username': form.cleaned_data.get('username'),
+ },
+ )
+
+ return HttpResponse(status=403)
+
+``urls.py``::
+
+ from django.urls import path
+ from example.views import Login
+
+ urlpatterns = [
+ path('login/', Login.as_view(), name='login'),
+ ]
+
+
+
+Customizing username lookups
+----------------------------
+
+In special cases, you may have the need to modify the username that is
+submitted before attempting to authenticate. For example, adding namespacing or
+removing client-set prefixes. In these cases, ``axes`` needs to know how to make
+these changes so that it can correctly identify the user without any form
+cleaning or validation. This is where the ``AXES_USERNAME_CALLABLE`` setting
+comes in. You can define how to make these modifications in a callable that
+takes a request object and a credentials dictionary,
+and provide that callable to ``axes`` via this setting.
+
+For example, a function like this could take a post body with something like
+``username='prefixed-username'`` and ``namespace=my_namespace`` and turn it
+into ``my_namespace-username``:
+
+``example/utils.py``::
+
+ def get_username(request, credentials):
+ username = credentials.get('username')
+ namespace = credentials.get('namespace')
+ return namespace + '-' + username
+
+``settings.py``::
+
+ AXES_USERNAME_CALLABLE = 'example.utils.get_username'
+
+NOTE: You still have to make these modifications yourself before calling
+authenticate. If you want to re-use the same function for consistency, that's
+fine, but Axes does not inject these changes into the authentication flow
+for you.
\ No newline at end of file
diff --git a/docs/development.rst b/docs/development.rst
index 8693ea1..d574fac 100644
--- a/docs/development.rst
+++ b/docs/development.rst
@@ -3,11 +3,7 @@
Development
===========
-You can contribute to this project forking it from github and sending pull requests.
-
-This is a `Jazzband `_ project. By contributing you agree to
-abide by the `Contributor Code of Conduct `_
-and follow the `guidelines `_.
+You can contribute to this project forking it from GitHub and sending pull requests.
Running tests
-------------
diff --git a/docs/index.rst b/docs/index.rst
index 4682cb1..0efaa7c 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -14,13 +14,14 @@ Contents
.. toctree::
:maxdepth: 2
- installation
- configuration
- migration
- usage
requirements
+ installation
+ usage
+ configuration
+ customization
+ migration
development
- captcha
+ architecture
Indices and tables
------------------
diff --git a/docs/installation.rst b/docs/installation.rst
index 64b1e71..01f685c 100644
--- a/docs/installation.rst
+++ b/docs/installation.rst
@@ -3,7 +3,76 @@
Installation
============
-You can install the latest stable package running this command::
+Axes is easy to install from the PyPI package::
$ pip install django-axes
+
+Configuring settings
+--------------------
+
+After installing the package, the project ``settings.py`` needs to be configured.
+
+1. add ``axes`` to your ``INSTALLED_APPS``::
+
+ INSTALLED_APPS = [
+ 'django.contrib.admin',
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.messages',
+ 'django.contrib.staticfiles',
+
+ # ... other applications per your preference.
+
+ 'axes',
+ ]
+
+2. add ``axes.backends.AxesBackend`` to the top of ``AUTHENTICATION_BACKENDS``::
+
+ AUTHENTICATION_BACKENDS = [
+ # AxesBackend should be the first backend in the list.
+ # It stops the authentication flow when a user is locked out.
+ 'axes.backends.AxesBackend',
+
+ # ... other authentication backends per your preference.
+
+ # Django ModelBackend is the default authentication backend.
+ 'django.contrib.auth.backends.ModelBackend',
+ ]
+
+3. add ``axes.middleware.AxesMiddleware`` to your list of ``MIDDLEWARE``::
+
+ MIDDLEWARE = [
+ # The following is the list of default middleware in new Django projects.
+ 'django.middleware.security.SecurityMiddleware',
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.middleware.common.CommonMiddleware',
+ 'django.middleware.csrf.CsrfViewMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
+ 'django.middleware.clickjacking.XFrameOptionsMiddleware',
+
+ # ... other middleware per your preference.
+
+ # AxesMiddleware should be the last middleware in the list.
+ # It pretty formats authentication errors into readable responses.
+ 'axes.middleware.AxesMiddleware',
+ ]
+
+4. Run ``python manage.py migrate`` to sync the database.
+
+Axes is now functional with the default settings and is saving user attempts
+into your database and locking users out if they exceed the maximum attempts.
+
+
+Running Django system checks
+----------------------------
+
+Use the ``python manage.py check`` command to verify the correct configuration in both
+development and production environments. It is probably best to use this step as part
+of your regular CI workflows to verify that your project is not misconfigured.
+
+Axes uses the checks to verify your cache configuration to see that your caches
+should be functional with the configuration of Axes. Many people have different configurations
+for their development and production environments.
\ No newline at end of file
diff --git a/docs/integration.rst b/docs/integration.rst
new file mode 100644
index 0000000..3f27ea4
--- /dev/null
+++ b/docs/integration.rst
@@ -0,0 +1,156 @@
+.. _usage:
+
+Integration
+===========
+
+Axes is intended to be pluggable and usable with 3rd party authentication solutions.
+
+This document describes the integration with some commonly used 3rd party packages
+such as Django Allauth and Django REST Framework.
+
+
+Integrating with Django Allauth
+-------------------------------
+
+Axes relies on having login information stored under ``AXES_USERNAME_FORM_FIELD`` key
+both in ``request.POST`` and in ``credentials`` dict passed to
+``user_login_failed`` signal.
+
+This is not the case with Allauth. Allauth always uses the ``login`` key in post POST data
+but it becomes ``username`` key in ``credentials`` dict in signal handler.
+
+To overcome this you need to use custom login form that duplicates the value
+of ``username`` key under a ``login`` key in that dict and set ``AXES_USERNAME_FORM_FIELD = 'login'``.
+
+You also need to decorate ``dispatch()`` and ``form_invalid()`` methods of the Allauth login view.
+
+``settings.py``::
+
+ AXES_USERNAME_FORM_FIELD = 'login'
+
+``example/forms.py``::
+
+ from allauth.account.forms import LoginForm
+
+ class AxesLoginForm(LoginForm):
+ """
+ Extended login form class that supplied the
+ user credentials for Axes compatibility.
+ """
+
+ def user_credentials(self):
+ credentials = super(AllauthCompatLoginForm, self).user_credentials()
+ credentials['login'] = credentials.get('email') or credentials.get('username')
+ return credentials
+
+``example/urls.py``::
+
+ from django.utils.decorators import method_decorator
+
+ from allauth.account.views import LoginView
+
+ from axes.decorators import axes_dispatch
+ from axes.decorators import axes_form_invalid
+
+ from example.forms import AxesLoginForm
+
+ LoginView.dispatch = method_decorator(axes_dispatch)(LoginView.dispatch)
+ LoginView.form_invalid = method_decorator(axes_form_invalid)(LoginView.form_invalid)
+
+ urlpatterns = [
+ # Override allauth default login view with a patched view
+ url(r'^accounts/login/$', LoginView.as_view(form_class=AllauthCompatLoginForm), name='account_login'),
+ url(r'^accounts/', include('allauth.urls')),
+ ]
+
+
+Integrating with Django REST Framework
+--------------------------------------
+
+Modern versions of Django REST Framework after 3.7.0 work normally with Axes.
+
+Django REST Framework versions prior to
+[3.7.0](https://github.com/encode/django-rest-framework/commit/161dc2df2ccecc5307cdbc05ef6159afd614189e)
+require the request object to be passed for authentication.
+
+``example/authentication.py``::
+
+ from rest_framework.authentication import BasicAuthentication
+
+ class AxesBasicAuthentication(BasicAuthentication):
+ """
+ Extended basic authentication backend class that supplies the
+ request object into the authentication call for Axes compatibility.
+
+ NOTE: This patch is only needed for DRF versions < 3.7.0.
+ """
+
+ def authenticate(self, request):
+ # NOTE: Request is added as an instance attribute in here
+ self._current_request = request
+ return super(AxesBasicAuthentication, self).authenticate(request)
+
+ def authenticate_credentials(self, userid, password, request=None):
+ credentials = {
+ get_user_model().USERNAME_FIELD: userid,
+ 'password': password
+ }
+
+ # NOTE: Request is added as an argument to the authenticate call here
+ user = authenticate(request=request or self._current_request, **credentials)
+
+ if user is None:
+ raise exceptions.AuthenticationFailed(_('Invalid username/password.'))
+
+ if not user.is_active:
+ raise exceptions.AuthenticationFailed(_('User inactive or deleted.'))
+
+ return (user, None)
+
+
+Integrating with Django Simple Captcha
+--------------------------------------
+
+Axes supports Captcha with the Django Simple Captcha package in the following manner.
+
+``settings.py``::
+
+ AXES_LOCKOUT_URL = '/locked'
+
+``example/urls.py``::
+
+ url(r'^locked/$', locked_out, name='locked_out'),
+
+``example/forms.py``::
+
+ class AxesCaptchaForm(forms.Form):
+ captcha = CaptchaField()
+
+``example/views.py``::
+
+ from example.forms import AxesCaptchaForm
+
+ def locked_out(request):
+ if request.POST:
+ form = AxesCaptchaForm(request.POST)
+ if form.is_valid():
+ ip = get_ip_address_from_request(request)
+ reset(ip=ip)
+ return HttpResponseRedirect(reverse_lazy('signin'))
+ else:
+ form = AxesCaptchaForm()
+
+ return render_to_response('captcha.html', dict(form=form), context_instance=RequestContext(request))
+
+``example/templates/example/captcha.html``::
+
+
diff --git a/docs/migration.rst b/docs/migration.rst
index 1f292f0..716a7f2 100644
--- a/docs/migration.rst
+++ b/docs/migration.rst
@@ -3,35 +3,36 @@
Migration
=========
-This page contains migration instructions between different django-axes
+This page contains migration instructions between different Axes
versions so that users might more confidently upgrade their installations.
-From django-axes version 4 to version 5
----------------------------------------
+Migrating from Axes version 4 to 5
+----------------------------------
-Application version 5 has a few differences compared to django-axes 4.
+Axes version 5 has a few differences compared to Axes 4.
You might need to search your own codebase and check if you need to change
API endpoints or names for compatibility reasons.
- Login and logout view monkey-patching was removed.
- Login monitoring is now implemented with signals
+ Login monitoring is now implemented with signal handlers
and locking users out is implemented with a combination
- of a custom authentication backend, middlware, and signals.
+ of a custom authentication backend, middleware, and signals.
+- ``axes.utils.reset`` was moved to ``axes.attempts.reset``.
- ``AxesModelBackend`` was renamed to ``AxesBackend``
for better naming and preventing the risk of users accidentally
upgrading without noticing that the APIs have changed.
- Documentation was improved. Exceptions were renamed.
- ``axes.backends.AxesModelBackend.RequestParameterRequired``
- exception was renamed, retyped to ``ValueError`` from ``Exception``, and
- moved to ``axes.exception.AxesBackendRequestParameterRequired``.
+ exception was renamed and retyped from ``Exception`` to ``ValueError``.
+ Exception was moved to ``axes.exception.AxesBackendRequestParameterRequired``.
- ``AxesBackend`` now raises a
``axes.exceptions.AxesBackendPermissionDenied``
- exception when user is locked out which triggers signal handler
+ exception when user is locked out, which triggers signal handler
to run on failed logins, checking user lockout statuses.
- Axes lockout signal handler now raises exception
``axes.exceptions.AxesSignalPermissionDenied`` on lockouts.
-- ``AxesMiddleware`` was added to return lockout responses.
- The middleware handles ``axes.exception.AxesSignalPermissionDenied``.
+- ``AxesMiddleware`` was added to process lockout events.
+ The middleware handles the ``axes.exception.AxesSignalPermissionDenied``
+ exception and converts it to a lockout response.
- ``AXES_USERNAME_CALLABLE`` is now always called with two arguments,
- ``request`` and ``credentials`` instead of ``request``.
+ ``request`` and ``credentials`` instead of just ``request``.
diff --git a/docs/requirements.rst b/docs/requirements.rst
index 41b37c4..8dab0ab 100644
--- a/docs/requirements.rst
+++ b/docs/requirements.rst
@@ -3,8 +3,13 @@
Requirements
============
-``django-axes`` requires a supported Django version. The application is
-intended to work around the Django admin and the regular
-``django.contrib.auth`` login-powered pages.
-Look here https://github.com/jazzband/django-axes/blob/master/.travis.yml
-to check if your django / python version are supported.
+Axes requires a supported Django version and runs on Python and PyPy versions 3.5 and above.
+
+Refer to the project source code repository in
+`GitHub `_ and see the
+`Travis CI configuration `_ and
+`Python package definition `_
+to check if your Django and Python version are supported.
+
+The `Travis CI builds `_
+test Axes compatibility with the Django master branch for future compatibility as well.
diff --git a/docs/usage.rst b/docs/usage.rst
index 11b24ac..27b63f9 100644
--- a/docs/usage.rst
+++ b/docs/usage.rst
@@ -1,230 +1,27 @@
-.. _usage:
-
Usage
=====
-``django-axes`` listens to signals from ``django.contrib.auth.signals`` to
-log access attempts:
-
-* ``user_logged_in``
-* ``user_logged_out``
-* ``user_login_failed``
-
-You can also use ``django-axes`` with your own auth module, but you'll need
-to ensure that it sends the correct signals in order for ``django-axes`` to
-log the access attempts.
-
-Quickstart
-----------
-
-Once ``axes`` is in your ``INSTALLED_APPS`` in your project settings file, you can
-login and logout of your application via the ``django.contrib.auth`` views.
-The attempts will be logged and visible in the "Access Attempts" section in admin.
+Once Axes is is installed and configured, you can login and logout
+of your application via the ``django.contrib.auth`` views.
+The attempts will be logged and visible in the Access Attempts section in admin.
By default, Axes will lock out repeated access attempts from the same IP address.
-You can allow this IP to attempt again by deleting relevant ``AccessAttempt`` records.
+You can allow this IP to attempt again by deleting relevant AccessAttempt records.
Records can be deleted, for example, by using the Django admin application.
-You can also use the ``axes_reset``, ``axes_reset_ip``, and ``axes_reset_user``
+You can also use the ``axes_reset``, ``axes_reset_ip``, and ``axes_reset_username``
management commands with the Django ``manage.py`` command helpers:
-* ``manage.py axes_reset`` will reset all lockouts and access records.
-* ``manage.py axes_reset_ip ip [ip ...]``
+- ``python manage.py axes_reset``
+ will reset all lockouts and access records.
+- ``python manage.py axes_reset_ip ip [ip ...]``
will clear lockouts and records for the given IP addresses.
-* ``manage.py axes_reset_user username [username ...]``
+- ``python manage.py axes_reset_username username [username ...]``
will clear lockouts and records for the given usernames.
-In your code, you can use the ``axes.utils.reset`` function.
+In your code, you can use the ``axes.attempts.reset`` function.
-* ``reset()`` will reset all lockouts and access records.
-* ``reset(ip=ip)`` will clear lockouts and records for the given IP address.
-* ``reset(username=username)`` will clear lockouts and records for the given username.
-
-Example usage
--------------
-
-Here is a more detailed example of sending the necessary signals using
-`django-axes` and a custom auth backend at an endpoint that expects JSON
-requests. The custom authentication can be swapped out with ``authenticate``
-and ``login`` from ``django.contrib.auth``, but beware that those methods take
-care of sending the nessary signals for you, and there is no need to duplicate
-them as per the example.
-
-*forms.py:* ::
-
- from django import forms
-
- class LoginForm(forms.Form):
- username = forms.CharField(max_length=128, required=True)
- password = forms.CharField(max_length=128, required=True)
-
-*views.py:* ::
-
- from django.http import JsonResponse, HttpResponse
- from django.utils.decorators import method_decorator
- from django.contrib.auth import signals
- from django.views import View
- from django.views.decorators.csrf import csrf_exempt
-
- from axes.decorators import axes_dispatch
-
- from myapp.forms import LoginForm
- from myapp.auth import custom_authenticate, custom_login
-
-
- @method_decorator(axes_dispatch, name='dispatch')
- @method_decorator(csrf_exempt, name='dispatch')
- class Login(View):
- """
- Custom login view that takes JSON credentials
- """
-
- http_method_names = ['post']
-
- def post(self, request):
- form = LoginForm(request.POST)
-
- if not form.is_valid():
- # inform django-axes of failed login
- signals.user_login_failed.send(
- sender=User,
- request=request,
- credentials={
- 'username': form.cleaned_data.get('username'),
- },
- )
- return HttpResponse(status=400)
-
- user = custom_authenticate(
- request=request,
- username=form.cleaned_data.get('username'),
- password=form.cleaned_data.get('password'),
- )
-
- if user is not None:
- custom_login(request, user)
-
- signals.user_logged_in.send(
- sender=User,
- request=request,
- user=user,
- )
-
- return JsonResponse({
- 'message':'success'
- }, status=200)
-
- # inform django-axes of failed login
- signals.user_login_failed.send(
- sender=User,
- request=request,
- credentials={
- 'username': form.cleaned_data.get('username'),
- },
- )
-
- return HttpResponse(status=403)
-
-*urls.py:* ::
-
- from django.urls import path
- from myapp.views import Login
-
- urlpatterns = [
- path('login/', Login.as_view(), name='login'),
- ]
-
-Integration with django-allauth
--------------------------------
-
-``axes`` relies on having login information stored under ``AXES_USERNAME_FORM_FIELD`` key
-both in ``request.POST`` and in ``credentials`` dict passed to
-``user_login_failed`` signal. This is not the case with ``allauth``.
-``allauth`` always uses ``login`` key in post POST data but it becomes ``username``
-key in ``credentials`` dict in signal handler.
-
-To overcome this you need to use custom login form that duplicates the value
-of ``username`` key under a ``login`` key in that dict
-(and set ``AXES_USERNAME_FORM_FIELD = 'login'``).
-
-You also need to decorate ``dispatch()`` and ``form_invalid()`` methods
-of the ``allauth`` login view. By default ``axes`` is patching only the
-``LoginView`` from ``django.contrib.auth`` app and with ``allauth`` you have to
-do the patching of views yourself.
-
-*settings.py:* ::
-
- AXES_USERNAME_FORM_FIELD = 'login'
-
-*forms.py:* ::
-
- from allauth.account.forms import LoginForm
-
- class AllauthCompatLoginForm(LoginForm):
- def user_credentials(self):
- credentials = super(AllauthCompatLoginForm, self).user_credentials()
- credentials['login'] = credentials.get('email') or credentials.get('username')
- return credentials
-
-*urls.py:* ::
-
- from allauth.account.views import LoginView
- from axes.decorators import axes_dispatch
- from axes.decorators import axes_form_invalid
- from django.utils.decorators import method_decorator
-
- from my_app.forms import AllauthCompatLoginForm
-
- LoginView.dispatch = method_decorator(axes_dispatch)(LoginView.dispatch)
- LoginView.form_invalid = method_decorator(axes_form_invalid)(LoginView.form_invalid)
-
- urlpatterns = [
- # ...
- url(r'^accounts/login/$', # Override allauth's default view with a patched view
- LoginView.as_view(form_class=AllauthCompatLoginForm),
- name="account_login"),
- url(r'^accounts/', include('allauth.urls')),
- # ...
- ]
-
-Altering username before login
-------------------------------
-
-In special cases, you may have the need to modify the username that is
-submitted before attempting to authenticate. For example, adding namespacing or
-removing client-set prefixes. In these cases, ``axes`` needs to know how to make
-these changes so that it can correctly identify the user without any form
-cleaning or validation. This is where the ``AXES_USERNAME_CALLABLE`` setting
-comes in. You can define how to make these modifications in a callable that
-takes a request object and a credentials dictionary,
-and provide that callable to ``axes`` via this setting.
-
-For example, a function like this could take a post body with something like
-``username='prefixed-username'`` and ``namespace=my_namespace`` and turn it
-into ``my_namespace-username``:
-
-*settings.py:* ::
-
- def sample_username_modifier(request):
- provided_username = request.POST.get('username')
- some_namespace = request.POST.get('namespace')
- return '-'.join([some_namespace, provided_username[9:]])
-
- AXES_USERNAME_CALLABLE = sample_username_modifier
-
- # New format that can also be used
- # the credentials argument is provided if the
- # function signature has two arguments instead of one
-
- def sample_username_modifier_credentials(request, credentials):
- provided_username = credentials.get('username')
- some_namespace = credentials.get('namespace')
- return '-'.join([some_namespace, provided_username[9:]])
-
- AXES_USERNAME_CALLABLE = sample_username_modifier_new
-
-NOTE: You still have to make these modifications yourself before calling
-authenticate. If you want to re-use the same function for consistency, that's
-fine, but ``axes`` doesn't inject these changes into the authentication flow
-for you.
+- ``reset()`` will reset all lockouts and access records.
+- ``reset(ip=ip)`` will clear lockouts and records for the given IP address.
+- ``reset(username=username)`` will clear lockouts and records for the given username.
\ No newline at end of file