Improve documentation

- Add information on handlers
- Document configuration options and precedences
- Restructure documentation for better readability

Signed-off-by: Aleksi Häkli <aleksi.hakli@iki.fi>
This commit is contained in:
Aleksi Häkli 2019-02-25 16:59:44 +02:00
parent 1ab8d89869
commit 677d4c48f4
No known key found for this signature in database
GPG key ID: 3E7146964D726BBE
14 changed files with 686 additions and 525 deletions

View file

@ -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', '')

View file

@ -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

View file

@ -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': {

124
docs/architecture.rst Normal file
View file

@ -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.

View file

@ -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::
<form action="" method="post">
{% csrf_token %}
{{ form.captcha.errors }}
{{ form.captcha }}
<div class="form-actions">
<input type="submit" value="Submit" />
</div>
</form>

View file

@ -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 <https://gist.github.com/markddavidoff/7e442b1ea2a2e68d390e76731c35afe7>`_.
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

144
docs/customization.rst Normal file
View file

@ -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.

View file

@ -3,11 +3,7 @@
Development
===========
You can contribute to this project forking it from github and sending pull requests.
This is a `Jazzband <https://jazzband.co>`_ project. By contributing you agree to
abide by the `Contributor Code of Conduct <https://jazzband.co/about/conduct>`_
and follow the `guidelines <https://jazzband.co/about/guidelines>`_.
You can contribute to this project forking it from GitHub and sending pull requests.
Running tests
-------------

View file

@ -14,13 +14,14 @@ Contents
.. toctree::
:maxdepth: 2
installation
configuration
migration
usage
requirements
installation
usage
configuration
customization
migration
development
captcha
architecture
Indices and tables
------------------

View file

@ -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.

156
docs/integration.rst Normal file
View file

@ -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``::
<form action="" method="post">
{% csrf_token %}
{{ form.captcha.errors }}
{{ form.captcha }}
<div class="form-actions">
<input type="submit" value="Submit" />
</div>
</form>

View file

@ -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``.

View file

@ -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 <https://github.com/jazzband/django-axes/>`_ and see the
`Travis CI configuration <https://github.com/jazzband/django-axes/blob/master/.travis.yml>`_ and
`Python package definition <https://github.com/jazzband/django-axes/blob/master/setup.py>`_
to check if your Django and Python version are supported.
The `Travis CI builds <https://travis-ci.org/jazzband/django-axes>`_
test Axes compatibility with the Django master branch for future compatibility as well.

View file

@ -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.