Closed whwkong closed 4 years ago
Hey @whwkong! The following functional updates have been made after 5.0.18, could one of them be the culprit?
Is it possible for you to post a more complete test set? The username and IP look the same, so it's hard to discern where the different might be.
EDIT: Seems like the username is updating and a new record is still being created for the different username. Seems to me like https://github.com/jazzband/django-axes/commit/0b8bea5d647caf7fafd6e7d231cc5e9a7a1ecb03 could be causing this bug. Would it be possible for you to open a PR that reproduces this bug and provides a test which fails when running with the configuration you are using? This way we could confirm the bug with a unit test or integration test and then introduce a patch that fixes the bug. This needs further investigation before a possible rollback is made.
@hramezani could you check if https://github.com/jazzband/django-axes/commit/0b8bea5d647caf7fafd6e7d231cc5e9a7a1ecb03 or another 5.0.19 release patch is causing this?
@aleksihakli sure. will check it. I think this https://github.com/jazzband/django-axes/commit/0b8bea5d647caf7fafd6e7d231cc5e9a7a1ecb03 is causing. based on this comment here https://github.com/jazzband/django-axes/blob/9d18bfc8921e364100a1cde6eab1b53f45940b93/axes/handlers/database.py#L119 I think we can revert this commit.
Just created a PR to test this issue. I don't know if I understood it correctly or not.
@hramezani I believe the new PR captures the problem. Thank you.
@whwkong You are welcome. Here is the newly added test case description:
I tried user failed login with two times with same username
and enable AXES_LOCK_OUT_BY_COMBINATION_USER_AND_IP
.
After the second failed login we have just one record in AccessAttempt
model.
I think this test case does not capture the problem. Rather, it proves that everything is right.
Please give us more information about your fail test.
@hramezani test upstreamed.
@whwkong I'll leave this issue open for a while, but in order to change behaviour and implementation, I'd like to see a test that either produces an error (either a true negative or false positive) with the current configuration.
(Note: I added the Django authentication backend dict to my original question body)
After poking around, the problem is with the AxesDatabaseHandler._get_user_attempts()
method from commit b8ef12c
.
Running full integration test (my app uses axes
+ drf
+ allauth
), the first time AxesDatabaseHandler._get_user_attempts()
is called, the credentials
argument is None
.
In subsequent calls, the credentials
argument is correctly filled out, eg: {"login": "somebody", "password": "123"}
. However, the request.axes_user_attempts
attribute is now populated, and the cached result is returned.
Removing the caching or amending ._get_user_attempts()
to the following passes my tests.
class AxesDatabaseHandler(AxesHandler):
def _get_user_attempts(self, request, credentials: dict = None):
if not hasattr(request, "axes_user_attempts"):
request.axes_user_attempts = {
str(credentials): get_user_attempts(request, credentials)
}
try:
return request.axes_user_attempts[str(credentials)]
except KeyError:
request.axes_user_attempts[str(credentials)] = get_user_attempts(
request,
credentials,
)
return request.axes_user_attempts[str(credentials)]
For the first _get_user_attempts()
call with credentials=None
the stack trace is:
[44] /home/prometheus/.virtualenvs/v_zenos.backend/lib/python3.7/site-packages/django/utils/deprecation.py(91)__call__()
-> response = response or self.get_response(request)
[45] /home/prometheus/.virtualenvs/v_zenos.backend/lib/python3.7/site-packages/django/core/handlers/exception.py(34)inner()
-> response = get_response(request)
[46] /home/prometheus/.virtualenvs/v_zenos.backend/lib/python3.7/site-packages/axes/middleware.py(30)__call__()
-> response = self.get_response(request)
[47] /home/prometheus/.virtualenvs/v_zenos.backend/lib/python3.7/site-packages/django/core/handlers/exception.py(34)inner()
-> response = get_response(request)
[48] /home/prometheus/.virtualenvs/v_zenos.backend/lib/python3.7/site-packages/django/core/handlers/base.py(124)_get_response()
-> response = wrapped_callback(request, *callback_args, **callback_kwargs)
[49] /usr/lib/python3.7/contextlib.py(74)inner()
-> return func(*args, **kwds)
[50] /home/prometheus/.virtualenvs/v_zenos.backend/lib/python3.7/site-packages/django/views/decorators/csrf.py(54)wrapped_view()
-> return view_func(*args, **kwargs)
[51] /home/prometheus/.virtualenvs/v_zenos.backend/lib/python3.7/site-packages/django/views/generic/base.py(68)view()
-> return self.dispatch(request, *args, **kwargs)
[52] /home/prometheus/.virtualenvs/v_zenos.backend/lib/python3.7/site-packages/django/utils/decorators.py(45)_wrapper()
-> return bound_method(*args, **kwargs)
[53] /home/prometheus/.virtualenvs/v_zenos.backend/lib/python3.7/site-packages/axes/decorators.py(9)inner()
-> if AxesProxyHandler.is_allowed(request):
[54] /home/prometheus/.virtualenvs/v_zenos.backend/lib/python3.7/site-packages/axes/handlers/proxy.py(83)is_allowed()
-> return cls.get_implementation().is_allowed(request, credentials)
[55] /home/prometheus/.virtualenvs/v_zenos.backend/lib/python3.7/site-packages/axes/handlers/base.py(72)is_allowed()
-> if self.is_locked(request, credentials):
[56] /home/prometheus/.virtualenvs/v_zenos.backend/lib/python3.7/site-packages/axes/handlers/database.py(88)is_locked()
-> return super().is_locked(request, credentials)
[57] /home/prometheus/.virtualenvs/v_zenos.backend/lib/python3.7/site-packages/axes/handlers/base.py(135)is_locked()
-> return self.get_failures(request, credentials) >= get_failure_limit(
[58] /home/prometheus/.virtualenvs/v_zenos.backend/lib/python3.7/site-packages/axes/handlers/database.py(77)get_failures()
-> attempts = self._get_user_attempts(request, credentials)
[59] > /home/prometheus/.virtualenvs/v_zenos.backend/lib/python3.7/site-packages/axes/handlers/database.py(41)_get_user_attempts()
-> if not hasattr(request, "axes_user_attempts"):
So the request has yet to pass through the allauth
backend at this point.
For the second ._get_user_attempts()
call with credentials
properly filled out, the stack trace is:
[45] /home/prometheus/.virtualenvs/v_zenos.backend/lib/python3.7/site-packages/django/core/handlers/exception.py(34)inner()
-> response = get_response(request)
[46] /home/prometheus/.virtualenvs/v_zenos.backend/lib/python3.7/site-packages/axes/middleware.py(30)__call__()
-> response = self.get_response(request)
[47] /home/prometheus/.virtualenvs/v_zenos.backend/lib/python3.7/site-packages/django/core/handlers/exception.py(34)inner()
-> response = get_response(request)
[48] /home/prometheus/.virtualenvs/v_zenos.backend/lib/python3.7/site-packages/django/core/handlers/base.py(124)_get_response()
-> response = wrapped_callback(request, *callback_args, **callback_kwargs)
[49] /usr/lib/python3.7/contextlib.py(74)inner()
-> return func(*args, **kwds)
[50] /home/prometheus/.virtualenvs/v_zenos.backend/lib/python3.7/site-packages/django/views/decorators/csrf.py(54)wrapped_view()
-> return view_func(*args, **kwargs)
[51] /home/prometheus/.virtualenvs/v_zenos.backend/lib/python3.7/site-packages/django/views/generic/base.py(68)view()
-> return self.dispatch(request, *args, **kwargs)
[52] /home/prometheus/.virtualenvs/v_zenos.backend/lib/python3.7/site-packages/django/utils/decorators.py(45)_wrapper()
-> return bound_method(*args, **kwargs)
[53] /home/prometheus/.virtualenvs/v_zenos.backend/lib/python3.7/site-packages/axes/decorators.py(10)inner()
-> return func(request, *args, **kwargs)
[54] /home/prometheus/.virtualenvs/v_zenos.backend/lib/python3.7/site-packages/django/utils/decorators.py(45)_wrapper()
-> return bound_method(*args, **kwargs)
[55] /home/prometheus/.virtualenvs/v_zenos.backend/lib/python3.7/site-packages/django/views/decorators/debug.py(76)sensitive_post_parameters_wrapper()
-> return view(request, *args, **kwargs)
[56] /home/prometheus/.virtualenvs/v_zenos.backend/lib/python3.7/site-packages/rest_auth/views.py(49)dispatch()
-> return super(LoginView, self).dispatch(*args, **kwargs)
[57] /home/prometheus/.virtualenvs/v_zenos.backend/lib/python3.7/site-packages/rest_framework/views.py(480)dispatch()
-> response = handler(request, *args, **kwargs)
[58] /home/prometheus/bhtai/zenos.backend/apps/users/views.py(68)post()
-> if self.serializer.is_valid():
[59] /home/prometheus/.virtualenvs/v_zenos.backend/lib/python3.7/site-packages/rest_framework/serializers.py(236)is_valid()
-> self._validated_data = self.run_validation(self.initial_data)
[60] /home/prometheus/.virtualenvs/v_zenos.backend/lib/python3.7/site-packages/rest_framework/serializers.py(437)run_validation()
-> value = self.validate(value)
[61] /home/prometheus/bhtai/zenos.backend/apps/users/serializers.py(107)validate()
-> user = self._validate_username(username, password)
[62] /home/prometheus/bhtai/zenos.backend/apps/users/serializers.py(60)_validate_username()
-> user = self.authenticate(username=username, password=password)
[63] /home/prometheus/bhtai/zenos.backend/apps/users/serializers.py(76)authenticate()
-> return authenticate(*args, request=request, **credentials)
[64] /home/prometheus/bhtai/zenos.backend/apps/users/utils.py(43)authenticate()
-> axes_backend.authenticate(request, **credentials)
[65] /home/prometheus/.virtualenvs/v_zenos.backend/lib/python3.7/site-packages/axes/helpers.py(411)inner()
-> return func(*args, **kwargs)
[66] /home/prometheus/.virtualenvs/v_zenos.backend/lib/python3.7/site-packages/axes/backends.py(44)authenticate()
-> if AxesProxyHandler.is_allowed(request, credentials):
[67] /home/prometheus/.virtualenvs/v_zenos.backend/lib/python3.7/site-packages/axes/handlers/proxy.py(83)is_allowed()
-> return cls.get_implementation().is_allowed(request, credentials)
[68] /home/prometheus/.virtualenvs/v_zenos.backend/lib/python3.7/site-packages/axes/handlers/base.py(72)is_allowed()
-> if self.is_locked(request, credentials):
[69] /home/prometheus/.virtualenvs/v_zenos.backend/lib/python3.7/site-packages/axes/handlers/database.py(88)is_locked()
-> return super().is_locked(request, credentials)
[70] /home/prometheus/.virtualenvs/v_zenos.backend/lib/python3.7/site-packages/axes/handlers/base.py(135)is_locked()
-> return self.get_failures(request, credentials) >= get_failure_limit(
[71] > /home/prometheus/.virtualenvs/v_zenos.backend/lib/python3.7/site-packages/axes/handlers/database.py(77)get_failures()
-> attempts = self._get_user_attempts(request, credentials)
It's only after the serializers fill out the credentials
dict (at [63]
above) that it is then passed to AxesDatabaseHandler._get_user_attempts()
.
Since the unit tests that @hramezani wrote does not properly simulate the above scenario in an integrated app, I'm not sure if I'm familiar enough with axes
to write a good test case for this.
I forgot to mention:
My app does not use session authentication (edit: there is no authentication via a form and django.contrib.auth
is never called; everything is done through drf
); I use allauth
and issue jwt tokens upon authentication.
I think the problem cames from the first attempt to AxesDatabaseHandler._get_user_attempts()
, where the credential is None
.
I think the source of the problem is in integrating with Django Allauth
. based on the integration-with-django-allauth:
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.
here is my the scenario which I think cause the problem:
AxesDatabaseHandler._get_user_attempts()
the credential is None
(as you said), AccessAttempt
is empty, So request.axes_user_attempts
is empty.user_login_failed
a new record with the credential will be created. (because we have credential here)AxesDatabaseHandler._get_user_attempts()
the credential is None
(as you said), AccessAttempt
has one record but with a username, So request.axes_user_attempts
is empty because the last record which is inserted in step 1 has the credential and we don't have it hewuser_login_failed
a new record with the credential will be created because request.axes_user_attempts
is empty.this is just my assumption. you can check your integration with Django Allauth
again.
@aleksihakli can correct me if I am wrong.
I was suspecting this might be due to the caching that has been introduced. I think we can either
Which option do you think is correct?
@aleksihakli I think it is better to disable it. because it is going to be more complicated and maybe error prune in the future. If you agree with me I can create a PR and revert my commit.
I think the problem cames from the first attempt to
AxesDatabaseHandler._get_user_attempts()
, where the credential isNone
. I think the source of the problem is in integrating withDjango Allauth
. based on the integration-with-django-allauth: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.
here is my the scenario which I think cause the problem: ... this is just my assumption. you can check your integration with
Django Allauth
again.
Yes, this is a correct description of the problem.
@hramezani I agree with you. As the caching is supplementary it is probably better to opt for behaviour that is always be correct than to introduce possibilities for bugs. I think that a rollback and possibly adding a comment about caching being dangerous is the best option here.
Fix upstreamed and published on PyPI in release 5.0.20.
(Hey everyone, thanks for making axes such a wonderful project! I know a lot of hard work was put into this!)
I've been using
django-axes
for nearly a year (edit: since4.5.x
) without any problems, and recently upgraded from5.0.18
to5.0.19
and my unit tests now indicate that lockouts are no longer working.The lockouts are based on user login plus IP. We are using
axes
withdjango_rest_framework 3.8.2
anddjango_allauth 0.40.0
.Some relevant settings:
With
axes 5.0.18
, after two unsuccessful local attempts against the same login:With
5.0.18
both attempts are correctly represented by the same model instance.After upgrading to
axes 5.0.19
, when I run the same test as above:With
5.0.19
, both attempts with the same login and IP are recorded to separate model instances and there is no lockout on subsequent attempts.Has anyone else experienced anything like this?