mirror of
https://github.com/jazzband/django-axes.git
synced 2026-03-16 22:30:23 +00:00
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:
parent
1ab8d89869
commit
677d4c48f4
14 changed files with 686 additions and 525 deletions
|
|
@ -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', '')
|
||||
|
|
|
|||
13
axes/conf.py
13
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
|
||||
|
|
|
|||
|
|
@ -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
124
docs/architecture.rst
Normal 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.
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
@ -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
144
docs/customization.rst
Normal 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.
|
||||
|
|
@ -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
|
||||
-------------
|
||||
|
|
|
|||
|
|
@ -14,13 +14,14 @@ Contents
|
|||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
installation
|
||||
configuration
|
||||
migration
|
||||
usage
|
||||
requirements
|
||||
installation
|
||||
usage
|
||||
configuration
|
||||
customization
|
||||
migration
|
||||
development
|
||||
captcha
|
||||
architecture
|
||||
|
||||
Indices and tables
|
||||
------------------
|
||||
|
|
|
|||
|
|
@ -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
156
docs/integration.rst
Normal 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>
|
||||
|
|
@ -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``.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
229
docs/usage.rst
229
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.
|
||||
Loading…
Reference in a new issue