Add option to keep current behavior for cooloff reset

This commit is contained in:
Antoine Dujardin 2022-03-22 13:58:23 +01:00 committed by Aleksi Häkli
parent 1015bad451
commit 9c2ceb7eb7
6 changed files with 49 additions and 8 deletions

View file

@ -1,3 +1,4 @@
from django.conf import settings
from django.contrib.auth.backends import ModelBackend
from axes.exceptions import (
@ -52,7 +53,8 @@ class AxesBackend(ModelBackend):
response_context["error"] = error_msg
# This flag can be used later to check if it was Axes that denied the login attempt.
request.axes_locked_out = True
if not settings.AXES_RESET_COOL_OFF_ON_FAILURE_DURING_LOCKOUT:
request.axes_locked_out = True
# Raise an error that stops the authentication flows at django.contrib.auth.authenticate.
# This error stops bubbling up at the authenticate call which catches backend PermissionDenied errors.

View file

@ -136,3 +136,8 @@ settings.AXES_CLIENT_STR_CALLABLE = getattr(settings, "AXES_CLIENT_STR_CALLABLE"
# set the HTTP response code given by too many requests
settings.AXES_HTTP_RESPONSE_CODE = getattr(settings, "AXES_HTTP_RESPONSE_CODE", 403)
# If True, a failed login attempt during lockout will reset the cool off period
settings.AXES_RESET_COOL_OFF_ON_FAILURE_DURING_LOCKOUT = getattr(
settings, "AXES_RESET_COOL_OFF_ON_FAILURE_DURING_LOCKOUT", True
)

View file

@ -84,7 +84,10 @@ class AxesCacheHandler(AbstractAxesHandler, AxesBaseHandler):
return
# If axes denied access, don't record the failed attempt as that would reset the lockout time.
if request.axes_locked_out:
if (
not settings.AXES_RESET_COOL_OFF_ON_FAILURE_DURING_LOCKOUT
and request.axes_locked_out
):
request.axes_credentials = credentials
user_locked_out.send(
"axes",

View file

@ -140,7 +140,10 @@ class AxesDatabaseHandler(AbstractAxesHandler, AxesBaseHandler):
)
# If axes denied access, don't record the failed attempt as that would reset the lockout time.
if request.axes_locked_out:
if (
not settings.AXES_RESET_COOL_OFF_ON_FAILURE_DURING_LOCKOUT
and request.axes_locked_out
):
request.axes_credentials = credentials
user_locked_out.send(
"axes",

View file

@ -125,6 +125,9 @@ The following ``settings.py`` options are available for customizing Axes behavio
reached.
For example: ``AXES_HTTP_RESPONSE_CODE = 429``
Default: ``403``
* ``AXES_RESET_COOL_OFF_ON_FAILURE_DURING_LOCKOUT``: If ``True``, a failed login attempt during lockout will
reset the cool off period.
Default: ``True``
The configuration option precedences for the access attempt monitoring are:

View file

@ -13,7 +13,7 @@ from django.test import override_settings, TestCase
from django.urls import reverse
from axes.conf import settings
from axes.helpers import get_cache, make_cache_key_list, get_cool_off
from axes.helpers import get_cache, make_cache_key_list, get_cool_off, get_failure_limit
from axes.models import AccessAttempt
from tests.base import AxesTestCase
@ -632,20 +632,45 @@ class DatabaseLoginTestCase(AxesTestCase):
response = self.client.get(reverse("admin:login"), REMOTE_ADDR=self.IP_1)
self.assertContains(response, self.LOGIN_FORM_KEY, status_code=200, html=True)
@override_settings(AXES_COOLOFF_TIME=timedelta(seconds=1))
@override_settings(
AXES_COOLOFF_TIME=timedelta(seconds=1),
AXES_RESET_COOL_OFF_ON_FAILURE_DURING_LOCKOUT=False,
AXES_FAILURE_LIMIT=2,
)
def test_login_during_lockout_doesnt_reset_cool_off_time(self):
# Lockout
self.lockout()
for _ in range(get_failure_limit(None, None)):
self.login(self.USER_1)
# Attempt during lockout
sleep_time = get_cool_off().total_seconds() / 2
sleep(sleep_time)
self.login()
self.login(self.USER_1)
sleep(sleep_time)
# New attempt after initial lockout period: should work
response = self.login(is_valid_username=True, is_valid_password=True)
self.assertNotContains(response, self.LOCKED_MESSAGE, status_code=302)
self.assertNotContains(response, self.LOCKED_MESSAGE, status_code=self.ALLOWED)
@override_settings(
AXES_COOLOFF_TIME=timedelta(seconds=1),
AXES_RESET_COOL_OFF_ON_FAILURE_DURING_LOCKOUT=True,
AXES_FAILURE_LIMIT=2,
)
def test_login_during_lockout_does_reset_cool_off_time(self):
# Lockout
for _ in range(get_failure_limit(None, None)):
self.login(self.USER_1)
# Attempt during lockout
sleep_time = get_cool_off().total_seconds() / 2
sleep(sleep_time)
self.login(self.USER_1)
sleep(sleep_time)
# New attempt after initial lockout period: should not work
response = self.login(is_valid_username=True, is_valid_password=True)
self.assertContains(response, self.LOCKED_MESSAGE, status_code=self.BLOCKED)
# Test the same logic with cache handler