diff --git a/axes/middleware.py b/axes/middleware.py index ff93a61..4e7d7be 100644 --- a/axes/middleware.py +++ b/axes/middleware.py @@ -40,19 +40,18 @@ class AxesMiddleware: markcoroutinefunction(self) @staticmethod - def _set_retry_after_header( - response: HttpResponse, request: HttpRequest + def set_retry_after_header(request: HttpRequest, response: HttpResponse) -> None: + if settings.AXES_ENABLE_RETRY_AFTER_HEADER: + response["Retry-After"] = str(int(get_cool_off(request).total_seconds())) + + def build_lockout_response( + self, + request: HttpRequest, + response: HttpResponse, + credentials, ) -> HttpResponse: - if not settings.AXES_ENABLE_RETRY_AFTER_HEADER: - return response - - if settings.AXES_LOCKOUT_CALLABLE or settings.AXES_LOCKOUT_URL: - return response - - cool_off = get_cool_off(request) - if cool_off is not None: - response["Retry-After"] = str(int(cool_off.total_seconds())) - + response = get_lockout_response(request, response, credentials) # type: ignore + self.set_retry_after_header(request, response) return response def __call__(self, request: HttpRequest) -> HttpResponse: @@ -64,8 +63,7 @@ class AxesMiddleware: if settings.AXES_ENABLED: if getattr(request, "axes_locked_out", None): credentials = getattr(request, "axes_credentials", None) - response = get_lockout_response(request, response, credentials) # type: ignore - response = self._set_retry_after_header(response, request) + response = self.build_lockout_response(request, response, credentials) return response @@ -76,10 +74,9 @@ class AxesMiddleware: if getattr(request, "axes_locked_out", None): credentials = getattr(request, "axes_credentials", None) response = await sync_to_async( - get_lockout_response, thread_sensitive=True + self.build_lockout_response, thread_sensitive=True )( request, response, credentials - ) # type: ignore - response = self._set_retry_after_header(response, request) + ) return response diff --git a/docs/4_configuration.rst b/docs/4_configuration.rst index ff1d3a5..2be58ad 100644 --- a/docs/4_configuration.rst +++ b/docs/4_configuration.rst @@ -91,8 +91,7 @@ The following ``settings.py`` options are available for customizing Axes behavio .. note:: If ``AXES_ENABLE_RETRY_AFTER_HEADER`` is enabled and ``AXES_COOLOFF_TIME`` is configured, ``AxesMiddleware`` adds a ``Retry-After`` HTTP header (`RFC 7231 `_) - with the cool-off duration in seconds. This header is not added for redirects - (``AXES_LOCKOUT_URL``) or custom lockout responses (``AXES_LOCKOUT_CALLABLE``). + with the cool-off duration in seconds for lockout responses. The configuration option precedences for the access attempt monitoring are: diff --git a/tests/test_middleware.py b/tests/test_middleware.py index a52a6b8..d53cc95 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -74,27 +74,29 @@ class MiddlewareTestCase(AxesTestCase): @override_settings( AXES_COOLOFF_TIME=timedelta(seconds=120), + AXES_ENABLE_RETRY_AFTER_HEADER=True, AXES_LOCKOUT_URL="https://example.com", ) - def test_lockout_redirect_response_does_not_set_retry_after_header(self): + def test_lockout_redirect_response_sets_retry_after_header(self): def get_response(request): request.axes_locked_out = True return HttpResponse() response = AxesMiddleware(get_response)(self.request) - self.assertFalse(response.has_header("Retry-After")) + self.assertEqual(response["Retry-After"], "120") @override_settings( AXES_COOLOFF_TIME=timedelta(seconds=120), + AXES_ENABLE_RETRY_AFTER_HEADER=True, AXES_LOCKOUT_CALLABLE="tests.test_middleware.get_custom_lockout_response", ) - def test_lockout_callable_response_does_not_set_retry_after_header(self): + def test_lockout_callable_response_sets_retry_after_header(self): def get_response(request): request.axes_locked_out = True return HttpResponse() response = AxesMiddleware(get_response)(self.request) - self.assertFalse(response.has_header("Retry-After")) + self.assertEqual(response["Retry-After"], "120") @override_settings(AXES_USERNAME_CALLABLE="tests.test_middleware.get_username") def test_lockout_response_with_axes_callable_username(self):