Skip to content

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

Merged
ivan-c merged 10 commits into
uwcirg:developfrom
CodeRhymesLife:lockout
Aug 21, 2018
Merged

TN-977 Lockout users if they fail password verification 5 times#2567
ivan-c merged 10 commits into
uwcirg:developfrom
CodeRhymesLife:lockout

Conversation

@CodeRhymesLife

@CodeRhymesLife CodeRhymesLife commented Aug 16, 2018

Copy link
Copy Markdown
Contributor

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

pep8speaks commented Aug 16, 2018

Copy link
Copy Markdown

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
Copy Markdown
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.

@CodeRhymesLife CodeRhymesLife left a comment

Copy link
Copy Markdown
Contributor Author

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

Comment thread portal/views/auth.py
from flask_user.signals import (
user_changed_password,
user_logged_in,
user_password_failed,

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

Comment thread portal/views/auth.py
the user will be locked out of the system.
"""
count = user.add_password_verification_failure()
auditable_event(

@CodeRhymesLife CodeRhymesLife Aug 16, 2018

Copy link
Copy Markdown
Contributor Author

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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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

Comment thread requirements.txt Outdated
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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 :)

@pbugni pbugni left a comment

Copy link
Copy Markdown
Collaborator

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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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.

Comment thread 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. \

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

Comment thread requirements.txt Outdated
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
Copy Markdown
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
Copy Markdown
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

@ivan-c ivan-c left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks great, thanks!

Comment thread tests/test_auth.py
from portal.models.intervention import INTERVENTION
from portal.models.role import ROLE
from portal.models.user import (
LOCKOUT_PERIOD,

Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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.

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