mirror of
https://github.com/jazzband/django-axes.git
synced 2026-03-23 17:40:24 +00:00
Revert some of the PR changes to tests to make sure that all of the old format function invocations work with the old and new tests. Implement small enchancements to documentation and credentials resolving for usability and flexibility with custom authentication backends. Update documentation to indicate that backwards compatibility is supported as well as the new format credentials invocations. Signed-off-by: Aleksi Häkli <aleksi.hakli@iki.fi>
220 lines
8 KiB
ReStructuredText
220 lines
8 KiB
ReStructuredText
.. _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 access attempts will be logged and visible in the "Access Attempts"
|
|
secion of the admin app.
|
|
|
|
By default, django-axes will lock out repeated attempts from the same IP
|
|
address. You can allow this IP to attempt again by deleting the relevant
|
|
``AccessAttempt`` records in the admin.
|
|
|
|
You can also use the ``axes_reset`` and ``axes_reset_user`` management commands
|
|
using Django's ``manage.py``.
|
|
|
|
* ``manage.py axes_reset`` will reset all lockouts and access records.
|
|
* ``manage.py axes_reset ip`` will clear lockout/records for ip
|
|
* ``manage.py axes_reset_user username`` will clear lockout/records for an username
|
|
|
|
In your code, you can use ``from axes.utils import reset``.
|
|
|
|
* ``reset()`` will reset all lockouts and access records.
|
|
* ``reset(ip=ip)`` will clear lockout/records for ip
|
|
* ``reset(username=username)`` will clear lockout/records for a 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.views.decorators.csrf import csrf_exempt
|
|
from django.utils.decorators import method_decorator
|
|
from django.http import JsonResponse, HttpResponse
|
|
from django.contrib.auth.signals import user_logged_in,\
|
|
user_logged_out,\
|
|
user_login_failed
|
|
import json
|
|
from myapp.forms import LoginForm
|
|
from myapp.auth import custom_authenticate, custom_login
|
|
|
|
from axes.decorators import axes_dispatch
|
|
|
|
@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):
|
|
# decode post json to dict & validate
|
|
post_data = json.loads(request.body.decode('utf-8'))
|
|
form = LoginForm(post_data)
|
|
|
|
if not form.is_valid():
|
|
# inform axes of failed login
|
|
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)
|
|
user_logged_in.send(
|
|
sender = User,
|
|
request = request,
|
|
user = user,
|
|
)
|
|
return JsonResponse({'message':'success!'}, status=200)
|
|
else:
|
|
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.
|