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

Password check #988

Merged
merged 24 commits into from
Mar 4, 2018
Merged

Password check #988

merged 24 commits into from
Mar 4, 2018

Conversation

umang-malik
Copy link
Contributor

Whenever the user sets/changes his password, his password is checked in the "pwned passwords" database (https://www.troyhunt.com/ive-just-launched-pwned-passwords-version-2/). The strength of the password based on the characters is also checked.
If the password is found weak on the basis of the above, then the user is suggested to use a better password.
This PR is with reference to #986.

@umang-malik umang-malik mentioned this pull request Feb 27, 2018
Copy link
Member

@Changaco Changaco left a comment

Choose a reason for hiding this comment

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

This is a good start, but more work is required.

  • You put an error message in out['msg'], that's incorrect since it'll be displayed to the user as a success notification. Instead we should show a confirmation warning to the user with response.render('templates/confirm.spt', ...).
  • Your 4th commit sends the full hash to the pwnedpasswords API, we want to use the /range endpoint instead.
  • You only implemented checking the password when it's being modified, but we also want to check during log-in. We'll need to store the time of the last check in our DB so we don't query the API too often.

@umang-malik
Copy link
Contributor Author

  1. Ask user for confirmation using response.render('templates/confirm.spt', ...). Using out['msg'] for successful change in password.
  2. Changed to /range endpoint of pwnedpasswords instead of sending the entire hash.

@Changaco, I don't understand why we need to check the strength of the password during log-in. The password has already been assessed while setting/changing it.
Also, I am not sure why we need to store the time of last check in our DB. There is already a restriction upon changing passwords, which itself restricts the API querying.

@Changaco
Copy link
Member

We need to check during log-in because:

  1. most people never change their password
  2. we already have more than 3400 passwords in our database
  3. the pwnedpasswords database can grow, a password that isn't in it now may end up in it later

msg = _(
"Are you sure you want to change your password?"
)
raise response.render('templates/confirm.spt', state, cls='warning', msg=msg, back_to=back_to)
Copy link
Member

Choose a reason for hiding this comment

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

This confirmation warning is unnecessary, we should only warn the user when the password is weak or compromised.

"The new password is not strong enough. We recommend you to choose a combination of letters, symbols, and numbers."
"Are you sure you want to change your password?"
)
raise response.render('templates/confirm.spt', state, msg=msg, back_to=back_to)
Copy link
Member

Choose a reason for hiding this comment

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

We need at least two different warning messages here, one when the password is weak, and another when the password is compromised (i.e. when it was found in the pwned database).

p.update_password(body['new-password'])
out['msg'] = _("Your password has been changed.")
out['msg'] = _("Your password has been changed")
Copy link
Member

Choose a reason for hiding this comment

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

This change should be reverted.

@Changaco
Copy link
Member

To keep track of password checks I suggest using the add_event and get_last_event_of_type methods, with a new 'password_check' event type and an empty payload (because all we need is a timestamp, and that's the event time ts).

@umang-malik
Copy link
Contributor Author

umang-malik commented Feb 27, 2018

The mentioned changes have been made.

Where should I implement the login-check? I am currently doing it in liberapay/security/authentication.py but am unable to send the notification to the user once he logs in.

@umang-malik
Copy link
Contributor Author

@Changaco, p.add_event(website.db.get_cursor(), 'password-check', None) line is giving an error which I am unable to fix:

  File "/home/umang/Desktop/folder/liberapay.com/env/lib/python2.7/site-packages/algorithm.py", line 321, in loop
    new_state = function(**deps.as_kwargs)
  File "/home/umang/Desktop/folder/liberapay.com/liberapay/security/authentication.py", line 195, in authenticate_user_if_possible
    p = sign_in_with_form_data(body, state)
  File "/home/umang/Desktop/folder/liberapay.com/liberapay/security/authentication.py", line 88, in sign_in_with_form_data
    p.add_event(website.db.get_cursor(), 'password-check', None)
  File "/home/umang/Desktop/folder/liberapay.com/liberapay/models/participant.py", line 1126, in add_event
    return c.one("""
AttributeError: 'CursorContextManager' object has no attribute 'one'

Also, how do I notify the user about his weak password? I tried using %if state.get('password-check') in templates/log-in-form.html but it is not working.

@Changaco
Copy link
Member

@umang-malik Dropping .get_cursor() should work, i.e.: p.add_event(website.db, 'password-check', None).

To notify the user I see two possibilities:

  • create a new templates/password-warning.spt template and call response.render(...)
  • create a new emails/password-warning.spt template and call p.notify(...)

The latter should be simpler to implement, and it would allow moving the password check to a background worker, so I think it's the best option.

@umang-malik
Copy link
Contributor Author

@Changaco, All the requested things have been done. 😃
The time period for password check during login has been set to 30 days for now. Also, I have currently disabled an email notification for the same.
Please review the PR.

Copy link
Member

@Changaco Changaco left a comment

Choose a reason for hiding this comment

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

Good work @umang-malik, it just needs some polishing.

.gitignore Outdated
@@ -12,7 +12,9 @@ node_modules/
.tox/
htmlcov/
tests/py/fixtures/TestTranslations.yml
tests/py/fixtures/TestLogIn.yml
Copy link
Member

Choose a reason for hiding this comment

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

This test fixture file should be committed, not ignored.

.gitignore Outdated
.idea/
payday-1.txt*
Copy link
Member

Choose a reason for hiding this comment

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

This should be payday-*.txt, and if we're adding that then we should also add payday.pid. Alternatively we could modify the default LOG_DIR value to put all this stuff in a sub-directory, and then ignore it.

{{ _("Update your Liberapay password") }}

[---] text/html
<p>{{ _("{0}",message) }}</p>
Copy link
Member

Choose a reason for hiding this comment

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

This isn't right, _("{0}", message) is a no-op. You shouldn't send an already-translated message to a notification template. Instead you should send the variables that the template needs to generate the message. In this case it could be either password_strength and password_leak_count, or a password_status string (possible values: 'weak', 'common', 'compromised'). The latter is probably better because it allows changing the thresholds without breaking old notifications.

@@ -14,6 +17,9 @@
from liberapay.models.participant import Participant
from liberapay.utils import get_ip_net

import passwordmeter
from hashlib import sha1
import requests
Copy link
Member

Choose a reason for hiding this comment

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

Please don't break the import style like this. Imports are split into specific groups (based on PEP8), and ordered alphabetically within each group.

"You are recommended to change your password."
)
p.notify('password_warning', email=False, message=password_check_message)
p.add_event(website.db, 'password-check', None)
Copy link
Member

Choose a reason for hiding this comment

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

This should be moved to a separate function, and the call to that new function should be wrapped to report and mask errors, like this:

try:
    check_password(password)
except Exception as e:
    website.tell_sentry(e, state)

@@ -57,6 +63,41 @@ def sign_in_with_form_data(body, state):
)
if not p:
state['log-in.error'] = _("Bad username or password.")
else:
last_password_check = p.get_last_event_of_type('password-check')
if (not last_password_check) or utcnow() - last_password_check.ts > timedelta(days=30):
Copy link
Member

Choose a reason for hiding this comment

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

I don't think we need to check every 30 days, once or twice per year should be enough, e.g. days=180.

suffix = line.split(":")[0]
if passhash_short + suffix == passhash:
count = int(line.split(":")[1].strip())
strength, improvements = passwordmeter.test(body['new-password'])
Copy link
Member

Choose a reason for hiding this comment

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

This is code duplication, it should be replaced with a call to a separate check_password function.

--hash=sha256:a77328ac55dbb5735da99441870251befe135f687ab707a7a178561363b27704
globre==0.1.5 \
--hash=sha256:ee214204f237e9114b8f61eeb61c2abd1e665ca3b59e5a6a0b070971c0bb12e2

Copy link
Member

Choose a reason for hiding this comment

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

I've looked through these new dependencies, they appear to be okay (although their source code could use a good cleanup).

One issue is that passwordmeter includes its own list of ten thousand common passwords, and always loads it, even if we skip the notword factor (which we should do since it's redundant with the pwnedpasswords check). This wastes approximately 1.2MB of RAM (measured with this get_size function on my laptop).

@umang-malik
Copy link
Contributor Author

@Changaco , all the changes have been made.

  • I have added a seperate method to check password.
  • The notword factor in passwordmeter has been skipped. I can't think of an easy solution to the extra memory required.
  • tests/py/fixtures/TestLogIn.yml contains the details of the requests made to the pwnedpasswords API. The response headers change every time the tests are run, and hence adding the file to .gitignore seems appropriate.

@Changaco
Copy link
Member

Changaco commented Mar 3, 2018

I've made lots of changes.

@umang-malik You should look at them and let me know what you think.

Screenshots:


screenshot of a warning displayed when a user attempts to protect their account with a weak common password


screenshot of a warning notification shown after a user logged in with a weak common password

@Changaco
Copy link
Member

Changaco commented Mar 3, 2018

I dropped the password strength test because it wasn't good. See this blog post for a complete explanation.

I didn't replace passwordmeter with zxcvbn-python because its algorithms are language-specific and by default only English is supported. We can try to integrate zxcvbn in the future, but it's not a priority.

@umang-malik
Copy link
Contributor Author

umang-malik commented Mar 3, 2018

@Changaco, I agree with the given blog's argument. Though I think that keeping a measure of password strength would help in setting a minimum threshold for passwords, especially in languages other than English, as the pwnedpassword database is heavily dominated by the it (and other common languages).

For example, hello1234 appears ~20,000 times, as compared to 172 of bonjour1234, or that müller just appears 11 times! Some common passwords in less-common languages might pass the pwnedpasswords test.
Other than that, the pwnedpasswords test looks good enough.

@umang-malik
Copy link
Contributor Author

Thanks for improving the templates. They look a lot better than mine.

@Changaco
Copy link
Member

Changaco commented Mar 3, 2018

The truth is that for Liberapay anything that's not a long string of random characters is a bad password. It doesn't really matter whether it's hello1234 or correcthorsebatterystaple, or how secure the latter is compared to the former, because the only secure password is the one you can’t remember.

@umang-malik
Copy link
Contributor Author

Well, I guess it shall be fine. We can always pull up a new issue in case we feel the need to do so.
P.S. All this reading about passwords has made me realize how much I need to work on my own passwords. 😅

Signed-off-by: Umang Malik <umang99m@gmail.com>
1. Check if new password exists in pwned password database.
2. Check the strength of a password and ask for a stronger one if password is weak.

Signed-off-by: Umang Malik <umang99m@gmail.com>
Changed the new-password to a stronger one for the test to check if changing password works correctly.

Signed-off-by: Umang Malik <umang99m@gmail.com>
Signed-off-by: Umang Malik <umang99m@gmail.com>
…n while changing password.

1. Shifted from the to '/range' api for better security.
2. Ask user for confirmation while changing password.
3. Make change to test_sign_in.py in accordance with the above.

Signed-off-by: Umang Malik <umang99m@gmail.com>
Different warnings are shown depending if password is weak, commonly used or is compromised.
Warning is not shown if password is strong and not compromised.

Signed-off-by: Umang Malik <umang99m@gmail.com>
Signed-off-by: Umang Malik <umang99m@gmail.com>
Also fixed add_event for password-check.

Signed-off-by: Umang Malik <umang99m@gmail.com>
tests/py/fixtures/TestLogIn.yml, payday-1.txt and payday-1.txt.part are generated while running tox.

Signed-off-by: Umang Malik <umang99m@gmail.com>
@Changaco
Copy link
Member

Changaco commented Mar 4, 2018

Rebased on master and cleaned up the commit messages.

@Changaco
Copy link
Member

Changaco commented Mar 4, 2018

@umang-malik In the future you can drop the Signed-off-by lines in commit messages.

@Changaco Changaco merged commit 27d1c90 into liberapay:master Mar 4, 2018
@umang-malik umang-malik deleted the password-check branch March 4, 2018 10:39
@Changaco
Copy link
Member

Changaco commented Mar 5, 2018

Deployed and announced.

@Changaco
Copy link
Member

Changaco commented Mar 6, 2018

I've emailed Troy Hunt to let him know about this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

2 participants