Don't reset cooloff time in case of login attempt during lockout

This commit is contained in:
Antoine Dujardin 2022-03-22 11:11:34 +01:00 committed by Aleksi Häkli
parent 246d884b84
commit 1015bad451
5 changed files with 46 additions and 5 deletions

View file

@ -51,6 +51,9 @@ class AxesBackend(ModelBackend):
response_context = kwargs.get("response_context", {})
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
# 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.
# After this error is caught by authenticate it emits a signal indicating user login failed,

View file

@ -24,7 +24,6 @@ class AxesCacheHandler(AbstractAxesHandler, AxesBaseHandler):
def __init__(self):
self.cache = get_cache()
self.cache_timeout = get_cache_timeout()
def reset_attempts(
self,
@ -84,6 +83,17 @@ 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:
request.axes_credentials = credentials
user_locked_out.send(
"axes",
request=request,
username=username,
ip_address=request.axes_ip_address,
)
return
client_str = get_client_str(
username,
request.axes_ip_address,
@ -115,7 +125,7 @@ class AxesCacheHandler(AbstractAxesHandler, AxesBaseHandler):
cache_keys = get_client_cache_key(request, credentials)
for cache_key in cache_keys:
failures = self.cache.get(cache_key, default=0)
self.cache.set(cache_key, failures + 1, self.cache_timeout)
self.cache.set(cache_key, failures + 1, get_cache_timeout())
if (
settings.AXES_LOCK_OUT_AT_FAILURE

View file

@ -139,6 +139,17 @@ class AxesDatabaseHandler(AbstractAxesHandler, AxesBaseHandler):
request,
)
# If axes denied access, don't record the failed attempt as that would reset the lockout time.
if request.axes_locked_out:
request.axes_credentials = credentials
user_locked_out.send(
"axes",
request=request,
username=username,
ip_address=request.axes_ip_address,
)
return
# This replaces null byte chars that crash saving failures.
get_data = get_query_str(request.GET).replace("\0", "0x00")
post_data = get_query_str(request.POST).replace("\0", "0x00")

View file

@ -78,7 +78,8 @@ class AxesProxyHandler(AbstractAxesHandler, AxesBaseHandler):
)
return
if not hasattr(request, "axes_updated"):
request.axes_locked_out = False
if not hasattr(request, "axes_locked_out"):
request.axes_locked_out = False
request.axes_attempt_time = now()
request.axes_ip_address = get_client_ip_address(request)
request.axes_user_agent = get_client_user_agent(request)

View file

@ -3,8 +3,9 @@ Integration tests for the login handling.
TODO: Clean up the tests in this module.
"""
from datetime import timedelta
from importlib import import_module
from time import sleep
from django.contrib.auth import get_user_model, login, logout
from django.http import HttpRequest
@ -12,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
from axes.helpers import get_cache, make_cache_key_list, get_cool_off
from axes.models import AccessAttempt
from tests.base import AxesTestCase
@ -631,6 +632,21 @@ 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))
def test_login_during_lockout_doesnt_reset_cool_off_time(self):
# Lockout
self.lockout()
# Attempt during lockout
sleep_time = get_cool_off().total_seconds() / 2
sleep(sleep_time)
self.login()
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)
# Test the same logic with cache handler
@override_settings(AXES_HANDLER="axes.handlers.cache.AxesCacheHandler")