-
Notifications
You must be signed in to change notification settings - Fork 4
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
TN-977 Lockout users if they fail password verification 5 times #2567
Conversation
Hello @drryanjames! Thanks for updating the PR. Cheers ! There are no PEP8 issues in this Pull Request. 🍻 Comment last updated on August 20, 2018 at 09:52 Hours UTC |
I still need to add unit tests. Since I'm out tomorrow and @pbugni is out Fri, wanted to get this out for review today to try and get it in this sprint. |
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.
Just a few comments and questions
@@ -26,6 +26,7 @@ | |||
from flask_user.signals import ( | |||
user_changed_password, | |||
user_logged_in, | |||
user_password_failed, |
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.
Here's the new signal I added to flask-user
the user will be locked out of the system. | ||
""" | ||
count = user.add_password_verification_failure() | ||
auditable_event( |
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.
Should this be a normal log instead of an auditable event because the user is not authenticated?
If I should be a normal log, is it safe to use the user's id in the log?
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.
Should this be a normal log instead of an auditable event because the user is not authenticated?
Good point, that might make more sense
If I should be a normal log, is it safe to use the user's id in the log?
Yup yup, those aren't publicly visible anywhere
portal/views/extend_flask_user.py
Outdated
if user.is_locked_out: | ||
# Make sure validators are run so we | ||
# can populate self.password.errors | ||
super(LoginForm, self).validate() |
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 is a bit odd. I'm calling the base.base class method to get things setup. I don't want to call the base method because it would add some stuff I don't want. I didn't find a better and reliable way to do this.
# can populate self.password.errors | ||
super(LoginForm, self).validate() | ||
|
||
auditable_event( |
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.
Again, the user isn't authenticated so not sure if this should be an auditable event.
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.
Yes, keep this. When we get support calls about users not being able to log in - it'll be helpful to have the available records.
context='login' | ||
) | ||
|
||
error_message = _('We see you\'re having trouble - let us help. \ |
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 wish this message was longer
requirements.txt
Outdated
@@ -32,7 +32,7 @@ flask-session==0.3.1 | |||
flask-sqlalchemy==2.3.2 | |||
flask-swagger==0.2.13 | |||
flask-testing==0.7.1 | |||
flask-user==0.6.21 # pyup: <0.7 # pin until 1.0 is ready for prod | |||
git+https://github.com/uwcirg/Flask-User.git@v0.6.21#egg=flask-user # pyup: <0.7 # pin until 1.0 is ready for prod |
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 like how easy this was.
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.
Almost :)
Paul helped me repro this, but you'll need to increment the version in your fork if you want it to be installed over (ie replace) the vanilla un-forked flask-user. If you create a fresh virtual environment this will work, but won't work for a venv that already has flask-user installed since pip thinks it's already installed at the same version.
You'll want to pick a version that's higher than v0.6.21
, but that won't conflict with the author's next release. Judging from their previous releases, I would suggest v0.6.21.1
.
Also, I'm not sure how picky pip is when comparing versions, or installing from git sources using a git tag. If you have any issues, I would suggest your git tag
exactly match the version in requirements.txt
, ie don't include _with_password_failed_signal
:)
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.
Took a while to figure out, but you can force pip to use the version specified in the egg param. I ended up doing this:
git+https://github.com/uwcirg/[email protected]#egg=flask-user==0.6.21.1 # pyup: <0.7 # pin until 1.0 is ready for prod
where v0.6.21.1
is the branch and flask-user==0.6.21.1
tells pip that it's installing version 0.6.21.1
.
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.
Nice catch BTW. This would have broken the build 😮
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.
Ah nice! I've only figured out what works for my narrow use-case, will have to try that in the future :)
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.
Looks great Ryan.
# can populate self.password.errors | ||
super(LoginForm, self).validate() | ||
|
||
auditable_event( |
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.
Yes, keep this. When we get support calls about users not being able to log in - it'll be helpful to have the available records.
op.add_column( | ||
'users', | ||
sa.Column( | ||
'last_password_verification_failure', |
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.
In the future I would suggest using redis for storing data that could be considered ephemeral (flask-user similarly uses redis to store server-side session data).
Redis also has the ability to automatically expire keys which could slightly simplify the timeout logic
No need for any change- wouldn't be worth the effort to refactor
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.
Thanks for calling this out. When I started thinking about this work I didn't want to store it in a DB because it doesn't make perfect sense there, but didn't know where else to put it. Wish I had spoken with you last week! Will keep redis in mind for the future. I've used it in the past for other stuff. Would have been a great place to put it.
portal/views/extend_flask_user.py
Outdated
|
||
error_message = _('We see you\'re having trouble - let us help. \ | ||
Your account will now be locked while we give it a refresh. \ | ||
Please try again in 30 minutes. \ |
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 replace the 30
with a variable (see below)? In we ever need to change the timeout value, it will save us the effort of re-translating it.
error_message = _('...Please try again in %d minutes...', 30)
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.
Oooo, good call
requirements.txt
Outdated
@@ -32,7 +32,7 @@ flask-session==0.3.1 | |||
flask-sqlalchemy==2.3.2 | |||
flask-swagger==0.2.13 | |||
flask-testing==0.7.1 | |||
flask-user==0.6.21 # pyup: <0.7 # pin until 1.0 is ready for prod | |||
git+https://github.com/uwcirg/Flask-User.git@v0.6.21#egg=flask-user # pyup: <0.7 # pin until 1.0 is ready for prod |
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.
Almost :)
Paul helped me repro this, but you'll need to increment the version in your fork if you want it to be installed over (ie replace) the vanilla un-forked flask-user. If you create a fresh virtual environment this will work, but won't work for a venv that already has flask-user installed since pip thinks it's already installed at the same version.
You'll want to pick a version that's higher than v0.6.21
, but that won't conflict with the author's next release. Judging from their previous releases, I would suggest v0.6.21.1
.
Also, I'm not sure how picky pip is when comparing versions, or installing from git sources using a git tag. If you have any issues, I would suggest your git tag
exactly match the version in requirements.txt
, ie don't include _with_password_failed_signal
:)
working on UTs now. Next time I'll try and do these first. |
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.
Looks great, thanks!
@@ -18,6 +18,8 @@ | |||
from portal.models.intervention import INTERVENTION | |||
from portal.models.role import ROLE | |||
from portal.models.user import ( | |||
LOCKOUT_PERIOD, |
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 move these default/constant values to config.py
in a subsequent PR to keep them consistent?
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.
Yes
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.
Created #2578 to track this.
If someone or some thing tries to login to a user's account with an incorrect password more than 5 times, each time within 30 mins of the last, these changes lock the user out of the system.
story
These changes rely on a fork of flask_user that adds an event we use to track password verification failures. Here's the commit that adds the event. Here's the PR that will hopefully get accepted so we can once again rely on the flask_user release.