Skip to content
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

Merged
merged 10 commits into from
Aug 21, 2018
Merged

TN-977 Lockout users if they fail password verification 5 times #2567

merged 10 commits into from
Aug 21, 2018

Conversation

CodeRhymesLife
Copy link
Contributor

@CodeRhymesLife CodeRhymesLife commented Aug 16, 2018

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.

@pep8speaks
Copy link

pep8speaks commented Aug 16, 2018

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

@CodeRhymesLife
Copy link
Contributor Author

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.

Copy link
Contributor Author

@CodeRhymesLife CodeRhymesLife left a 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,
Copy link
Contributor Author

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(
Copy link
Contributor Author

@CodeRhymesLife CodeRhymesLife Aug 16, 2018

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?

Copy link
Member

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

if user.is_locked_out:
# Make sure validators are run so we
# can populate self.password.errors
super(LoginForm, self).validate()
Copy link
Contributor Author

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(
Copy link
Contributor Author

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.

Copy link
Collaborator

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. \
Copy link
Contributor Author

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
Copy link
Contributor Author

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.

Copy link
Member

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 :)

Copy link
Contributor Author

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/Flask-User.git@v0.6.21.1#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.

Copy link
Contributor Author

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 😮

Copy link
Member

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 :)

Copy link
Collaborator

@pbugni pbugni left a 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(
Copy link
Collaborator

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',
Copy link
Member

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

Copy link
Contributor Author

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.


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. \
Copy link
Member

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)

Copy link
Contributor Author

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
Copy link
Member

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 :)

@uwcirg uwcirg deleted a comment Aug 17, 2018
@uwcirg uwcirg deleted a comment Aug 17, 2018
@uwcirg uwcirg deleted a comment Aug 17, 2018
@uwcirg uwcirg deleted a comment Aug 17, 2018
@uwcirg uwcirg deleted a comment from CodeRhymesLife Aug 17, 2018
@uwcirg uwcirg deleted a comment from CodeRhymesLife Aug 17, 2018
@uwcirg uwcirg deleted a comment from CodeRhymesLife Aug 17, 2018
@CodeRhymesLife
Copy link
Contributor Author

working on UTs now. Next time I'll try and do these first.

@uwcirg uwcirg deleted a comment Aug 20, 2018
@uwcirg uwcirg deleted a comment Aug 20, 2018
@uwcirg uwcirg deleted a comment from CodeRhymesLife Aug 20, 2018
@uwcirg uwcirg deleted a comment Aug 20, 2018
Copy link
Member

@ivan-c ivan-c left a 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,
Copy link
Member

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes

Copy link
Contributor Author

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.

@ivan-c ivan-c merged commit a9c1b0f into uwcirg:develop Aug 21, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants