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
This commit is contained in:
Robert Sparks 2022-02-23 18:30:27 +00:00
parent 95cb3f041e
commit e3aa43eea5
4 changed files with 58 additions and 8 deletions

View file

@ -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'))

View file

@ -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':

View file

@ -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"

View file

@ -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.