From e3aa43eea55ea3fa4eadc557a1f148106438f8b9 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Wed, 23 Feb 2022 18:30:27 +0000 Subject: [PATCH] Merged in [19967] from rjsparks@nostrum.com: From Kesara Rathnayake: Expire password reset links on use, password change through other mechanics, login, or a short configurable time (initially one hour). Patched in at 7.45.0.p2. - Legacy-Id: 19968 Note: SVN reference [19967] has been migrated to Git commit 682392081bddbd1b8653df9135388e6b7c48ee1c --- ietf/ietfauth/tests.py | 39 ++++++++++++++++++- ietf/ietfauth/views.py | 24 +++++++++--- ietf/settings.py | 1 + .../registration/password_reset_email.txt | 2 +- 4 files changed, 58 insertions(+), 8 deletions(-) diff --git a/ietf/ietfauth/tests.py b/ietf/ietfauth/tests.py index 988d15521..07be90ec1 100644 --- a/ietf/ietfauth/tests.py +++ b/ietf/ietfauth/tests.py @@ -373,9 +373,11 @@ class IetfAuthTests(TestCase): def test_reset_password(self): url = urlreverse(ietf.ietfauth.views.password_reset) + email = 'someone@example.com' + password = 'foobar' - user = User.objects.create(username="someone@example.com", email="someone@example.com") - user.set_password("forgotten") + user = User.objects.create(username=email, email=email) + user.set_password(password) user.save() p = Person.objects.create(name="Some One", ascii="Some One", user=user) Email.objects.create(address=user.username, person=p, origin=user.username) @@ -414,6 +416,39 @@ class IetfAuthTests(TestCase): self.assertEqual(len(q("form .has-error")), 0) self.assertTrue(self.username_in_htpasswd_file(user.username)) + # reuse reset url + r = self.client.get(confirm_url) + self.assertEqual(r.status_code, 404) + + # login after reset request + empty_outbox() + user.set_password(password) + user.save() + + r = self.client.post(url, { 'username': user.username }) + self.assertEqual(r.status_code, 200) + self.assertEqual(len(outbox), 1) + confirm_url = self.extract_confirm_url(outbox[-1]) + + r = self.client.post(urlreverse(ietf.ietfauth.views.login), {'username': email, 'password': password}) + + r = self.client.get(confirm_url) + self.assertEqual(r.status_code, 404) + + # change password after reset request + empty_outbox() + + r = self.client.post(url, { 'username': user.username }) + self.assertEqual(r.status_code, 200) + self.assertEqual(len(outbox), 1) + confirm_url = self.extract_confirm_url(outbox[-1]) + + user.set_password('newpassword') + user.save() + + r = self.client.get(confirm_url) + self.assertEqual(r.status_code, 404) + def test_review_overview(self): review_req = ReviewRequestFactory() assignment = ReviewAssignmentFactory(review_request=review_req,reviewer=EmailFactory(person__user__username='reviewer')) diff --git a/ietf/ietfauth/views.py b/ietf/ietfauth/views.py index d03bcd595..4f0c6faf3 100644 --- a/ietf/ietfauth/views.py +++ b/ietf/ietfauth/views.py @@ -36,7 +36,7 @@ import importlib -from datetime import date as Date +from datetime import date as Date, datetime as DateTime # needed if we revert to higher barrier for account creation #from datetime import datetime as DateTime, timedelta as TimeDelta, date as Date from collections import defaultdict @@ -418,7 +418,16 @@ def password_reset(request): if form.is_valid(): username = form.cleaned_data['username'] - auth = django.core.signing.dumps(username, salt="password_reset") + data = { 'username': username } + if User.objects.filter(username=username).exists(): + user = User.objects.get(username=username) + data['password'] = user.password and user.password[-4:] + if user.last_login: + data['last_login'] = user.last_login.timestamp() + else: + data['last_login'] = None + + auth = django.core.signing.dumps(data, salt="password_reset") domain = Site.objects.get_current().domain subject = 'Confirm password reset at %s' % domain @@ -429,7 +438,7 @@ def password_reset(request): 'domain': domain, 'auth': auth, 'username': username, - 'expire': settings.DAYS_TO_EXPIRE_REGISTRATION_LINK, + 'expire': settings.MINUTES_TO_EXPIRE_RESET_PASSWORD_LINK, }) success = True @@ -443,11 +452,16 @@ def password_reset(request): def confirm_password_reset(request, auth): try: - username = django.core.signing.loads(auth, salt="password_reset", max_age=settings.DAYS_TO_EXPIRE_REGISTRATION_LINK * 24 * 60 * 60) + data = django.core.signing.loads(auth, salt="password_reset", max_age=settings.MINUTES_TO_EXPIRE_RESET_PASSWORD_LINK * 60) + username = data['username'] + password = data['password'] + last_login = None + if data['last_login']: + last_login = DateTime.fromtimestamp(data['last_login']) except django.core.signing.BadSignature: raise Http404("Invalid or expired auth") - user = get_object_or_404(User, username=username) + user = get_object_or_404(User, username=username, password__endswith=password, last_login=last_login) success = False if request.method == 'POST': diff --git a/ietf/settings.py b/ietf/settings.py index 192aa3838..db6f0eb28 100644 --- a/ietf/settings.py +++ b/ietf/settings.py @@ -1007,6 +1007,7 @@ DE_GFM_BINARY = '/usr/bin/de-gfm.ruby2.5' # Account settings DAYS_TO_EXPIRE_REGISTRATION_LINK = 3 +MINUTES_TO_EXPIRE_RESET_PASSWORD_LINK = 60 HTPASSWD_COMMAND = "/usr/bin/htpasswd" HTPASSWD_FILE = "/www/htpasswd" diff --git a/ietf/templates/registration/password_reset_email.txt b/ietf/templates/registration/password_reset_email.txt index d0fd94f51..6882ac7c1 100644 --- a/ietf/templates/registration/password_reset_email.txt +++ b/ietf/templates/registration/password_reset_email.txt @@ -5,7 +5,7 @@ Hello, https://{{ domain }}{% url "ietf.ietfauth.views.confirm_password_reset" auth %} -This link will expire in {{ expire }} days. +This link will expire in {{ expire }} minutes. If you have not requested a password reset you can ignore this email, your credentials have been left untouched.