New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
use an authentication backend to check if user is locked out. #315
Conversation
axes/middleware.py
Outdated
from django.contrib.auth.backends import ModelBackend | ||
from django.core.exceptions import PermissionDenied | ||
|
||
class DjangoAxesAuthBackend(ModelBackend): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Name suggestions?
3263cb4
to
c4769c7
Compare
axes/middleware.py
Outdated
if is_already_locked(request): | ||
# locked out, don't try to authenticate, just update return_context and return | ||
error_msg = get_lockout_message() | ||
response_context = kwargs.get('response_context', {}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Its a bit weird to pass a context and expect a response value but its nice to get a "why" back.
c4769c7
to
0674f27
Compare
Looks good so far, please keep going. |
axes/middleware.py
Outdated
# locked out, don't try to authenticate, just update return_context and return | ||
error_msg = get_lockout_message() | ||
response_context = kwargs.get('response_context', {}) | ||
response_context['error'] = error_msg |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this mutate kwargs contents?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It expects a dict called 'response_context' to be passed as a kwarg and mutates that if it is present.
I agree its a bit weird to pass a context and expect a value set but its nice to get a "why" back and I couldn't figure out a better way to do this that still complies with the django auth backend structure.
axes/conf.py
Outdated
AXES_COOLOFF_MESSAGE = 'Account locked: too many login attempts. Please try again later' | ||
|
||
# message to show when locked out and have cooloff disabled | ||
AXES_PERMALOCK_MESSAGE= 'Account locked: too many login attempts. Contact an admin to unlock your account.' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing a newline.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also missing a space on the =
if settings.AXES_COOLOFF_TIME: | ||
return settings.AXES_COOLOFF_MESSAGE | ||
else: | ||
return settings.AXES_PERMALOCK_MESSAGE |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing a newline.
axes/middleware.py
Outdated
from django.core.exceptions import PermissionDenied | ||
|
||
|
||
class DjangoAxesAuthBackend(ModelBackend): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe AxesModelBackend
would be a neater name?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sure! I copy pasted a bunch of this from my subclass implementation and rushed to redact some of the naming and messages
axes/middleware.py
Outdated
from axes.attempts import is_already_locked | ||
from axes.utils import get_lockout_message | ||
from django.contrib.auth.backends import ModelBackend | ||
from django.core.exceptions import PermissionDenied |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A more traditional import order would be to import Python core packages first, then Django packages and then custom packages. Something like
from django import ...
from axes import ...
axes/middleware.py
Outdated
@@ -0,0 +1,30 @@ | |||
from axes.attempts import is_already_locked |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file is not middleware - it should be backends.py
so the import for this module would be something like axes.backends.AxesModelBackend
.
Looks good, the naming is a bit conflicting as this patchset implements an authentication backend instead of middleware (though it's easy to see it as middlewareish in functionality). The approach, however, is very nice. An authentication backend denying logins is MUCH more neater and universal than monkey patching view dispatchers which is the current functionality. If you could clean this up slightly and implement unit tests for the backend it would be nice to get this merged 👍 |
@markddavidoff can we have this soon? Looks amazing. |
Yeah, i hope to get to this this week or next week.
…On Wed, Mar 21, 2018, 9:56 AM Camilo Nova ***@***.***> wrote:
@markddavidoff <https://github.com/markddavidoff> can we have this soon?
Looks amazing.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#315 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/ABTCn2vSicLER5I7PJ9oPZvfdiMHh-cCks5tgoZAgaJpZM4SpIXo>
.
|
818e882
to
cd3c074
Compare
axes/backends.py
Outdated
@@ -21,6 +21,9 @@ def authenticate(self, request, username=None, password=None, **kwargs): | |||
:return: Nothing, but will update return_context with lockout message if user is locked out. | |||
""" | |||
|
|||
if request is None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@camilonova @aleksihakli addressed pr concerns, but i have another query as to handle when request is None.
Django Rest Framework (which is very widely used) does not set request
when passing it to authenticate, so things like @authentication_classes((BasicAuthentication,)) will error out.
options:
- Error out like this
- no-op if no request and log a warning
But i think we need to address this as just a no-op is a way that axes could not be called due to programmer error (and i think security type things should be on-by-default).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this behaviour is fine for the time being. If someone wants to use this custom backend in their project they need to make sure it can function correctly.
cd3c074
to
d8cbb42
Compare
Looks good to me! I think this is ready for merging. I'll let @camilonova check and merge this :) |
moved the exception to a subclass and updated the documentation as well |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few small cleanups could be used here :)
The RequestParameterRequired
exception could maybe subclass the Django SuspiciousOperation
exception?
This looks good for merging, though.
axes/backends.py
Outdated
@@ -7,6 +7,13 @@ | |||
|
|||
class AxesModelBackend(ModelBackend): | |||
|
|||
class RequestParameterRequired(Exception): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This exception should really live in axes.exceptions
as per good naming conventions. You just create that exceptions.py
file. There is no need to put this inside the AxesModelBackend
class, nesting classes just makes this harder to read and find.
docs/configuration.rst
Outdated
- make sure any auth libraries you use that call the authentication middleware stack pass request. Notably Django Rest | ||
Framework (DRF) ``BasicAuthentication`` does not pass request. `Here is an example workaround for DRF`_. | ||
|
||
.. _Here is an example workaround for DRF: http://www.python.org |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't seem like the right link.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Woops. Will fix.
|
||
- run ``python manage.py migrate`` to sync the database. | ||
|
||
Things to you might need to change in your code, especially if you get a ``AxesModelBackend.RequestParameterRequired``: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can also be just axes.exceptions.RequestParameterRequired
.
|
||
class AxesModelBackend(ModelBackend): | ||
|
||
class RequestParameterRequired(Exception): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be moved into axes.exceptions
file (you can just create that) as per good Django package conventions. Exceptions are easier to find when they are in standard locations.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I strongly disagree eith moving the exception and using suspicious operation, although im always open to naming changes.
Moving indicates it is a failure on the module level, which i dont think applies here. The convention, to me, suggests a "Misconfigured" or "BadParameter" parent exc in exceptions.py that can be subclassed here, but i think the subclass should live here as it is tied to the backend not the module.
Is this a suspicious operation? I think its more programmer error?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But maybe im thinking app as part of a big project wise and not standalone library wise
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, I think it's valid to have this here as well. Would be nice to have some tests for this, but I think we can implement those later.
response_context['error'] = error_msg | ||
raise PermissionDenied(error_msg) | ||
|
||
# No-op |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could check for the request authentication status in the model backend and lock out if there are duplicate requests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you please clarify what you mean by 'check for the request authentication status'?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Never mind, I think this is already handled by our signal handler - backend combination.
I guess the big question is if we want to do the access checking and locking out in the model backend? |
@aleksihakli, you mean as the default recommended way? |
@markddavidoff yeah! I think the best place for all that checking is the |
@aleksihakli I'm still confused what you mean, i believe this does all the checking the monkeypatched view does already. Am i missing other logic? |
@markddavidoff if the Django authentication signals which have handlers defined in I wondered if we should attempt to do access attempt logging in the backend level, but I suppose that isn't necessary in the case all signals work correctly. We should be able to remove the monkey patching with this change from I'll go ahead and merge this changeset, let's refine it further in a separate PR if necessary :) |
@markddavidoff I ran a bunch of tests on top of the new backend and it seems that authentication succesfully fails from the signals, but raising the |
It seems that https://github.com/django/django/blob/master/django/contrib/auth/__init__.py#L62 So we will either have to modify the tests that check response code on an attempted login, implement a custom middleware that returns the 403 code, or continue monkey patching stuff. |
This is meant to be used in conjunction with another middleware that
actually authenicates like in the example i added to the docs. We can use
modelbackend in the tests. Any suggestions to make that more clear in code
and docs.
…On Wed, Apr 11, 2018, 3:37 PM Aleksi Häkli ***@***.***> wrote:
It seems that authenticate does not indeed return a 403 but just skips
the authentication:
https://github.com/django/django/blob/master/django/contrib/auth/__init__.py#L62
So we will either have to modify the tests that check response code on an
attempted login, implement a custom middleware that returns the 403 code,
or continue monkey patching stuff.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#315 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/ABTCn4ZPviLpglYq20gfgcYxxnGl-5eyks5tnluKgaJpZM4SpIXo>
.
|
@camilonova First Draft for input.
USAGE
settings.py
login view call example