summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorRomain Garrigues <romain.garrigues@makina-corpus.com>2017-01-13 14:17:54 +0000
committerTim Graham <timograham@gmail.com>2017-01-13 09:17:54 -0500
commitede59ef6f39ff8a6443c2b24df0208ef6ec41ee0 (patch)
treeee8c155dbc4520371e06fe3251e45e283fc5115d /tests
parent91023d79ec70df9289271e63a67675ee51e7dea8 (diff)
Fixed #27518 -- Prevented possibie password reset token leak via HTTP Referer header.
Thanks Florian Apolloner for contributing to this patch and Collin Anderson, Markus Holtermann, and Tim Graham for review.
Diffstat (limited to 'tests')
-rw-r--r--tests/auth_tests/client.py41
-rw-r--r--tests/auth_tests/test_templates.py13
-rw-r--r--tests/auth_tests/test_tokens.py7
-rw-r--r--tests/auth_tests/test_views.py31
4 files changed, 87 insertions, 5 deletions
diff --git a/tests/auth_tests/client.py b/tests/auth_tests/client.py
new file mode 100644
index 0000000000..004101108b
--- /dev/null
+++ b/tests/auth_tests/client.py
@@ -0,0 +1,41 @@
+import re
+
+from django.contrib.auth.views import (
+ INTERNAL_RESET_SESSION_TOKEN, INTERNAL_RESET_URL_TOKEN,
+)
+from django.test import Client
+
+
+def extract_token_from_url(url):
+ token_search = re.search(r'/reset/.*/(.+?)/', url)
+ if token_search:
+ return token_search.group(1)
+
+
+class PasswordResetConfirmClient(Client):
+ """
+ This client eases testing the password reset flow by emulating the
+ PasswordResetConfirmView's redirect and saving of the reset token in the
+ user's session. This request puts 'my-token' in the session and redirects
+ to '/reset/bla/set-password/':
+
+ >>> client = PasswordResetConfirmClient()
+ >>> client.get('/reset/bla/my-token/')
+ """
+ def _get_password_reset_confirm_redirect_url(self, url):
+ token = extract_token_from_url(url)
+ if not token:
+ return url
+ # Add the token to the session
+ session = self.session
+ session[INTERNAL_RESET_SESSION_TOKEN] = token
+ session.save()
+ return url.replace(token, INTERNAL_RESET_URL_TOKEN)
+
+ def get(self, path, *args, **kwargs):
+ redirect_url = self._get_password_reset_confirm_redirect_url(path)
+ return super(PasswordResetConfirmClient, self).get(redirect_url, *args, **kwargs)
+
+ def post(self, path, *args, **kwargs):
+ redirect_url = self._get_password_reset_confirm_redirect_url(path)
+ return super(PasswordResetConfirmClient, self).post(redirect_url, *args, **kwargs)
diff --git a/tests/auth_tests/test_templates.py b/tests/auth_tests/test_templates.py
index 9414f8b299..a1d14c9774 100644
--- a/tests/auth_tests/test_templates.py
+++ b/tests/auth_tests/test_templates.py
@@ -3,12 +3,15 @@ from django.contrib.auth.models import User
from django.contrib.auth.tokens import PasswordResetTokenGenerator
from django.contrib.auth.views import (
PasswordChangeDoneView, PasswordChangeView, PasswordResetCompleteView,
- PasswordResetConfirmView, PasswordResetDoneView, PasswordResetView,
+ PasswordResetDoneView, PasswordResetView,
)
from django.test import RequestFactory, TestCase, override_settings
+from django.urls import reverse
from django.utils.encoding import force_bytes, force_text
from django.utils.http import urlsafe_base64_encode
+from .client import PasswordResetConfirmClient
+
@override_settings(ROOT_URLCONF='auth_tests.urls')
class AuthTemplateTests(TestCase):
@@ -34,16 +37,20 @@ class AuthTemplateTests(TestCase):
def test_PasswordResetConfirmView_invalid_token(self):
# PasswordResetConfirmView invalid token
- response = PasswordResetConfirmView.as_view(success_url='dummy/')(self.request, uidb64='Bad', token='Bad')
+ client = PasswordResetConfirmClient()
+ url = reverse('password_reset_confirm', kwargs={'uidb64': 'Bad', 'token': 'Bad-Token'})
+ response = client.get(url)
self.assertContains(response, '<title>Password reset unsuccessful</title>')
self.assertContains(response, '<h1>Password reset unsuccessful</h1>')
def test_PasswordResetConfirmView_valid_token(self):
# PasswordResetConfirmView valid token
+ client = PasswordResetConfirmClient()
default_token_generator = PasswordResetTokenGenerator()
token = default_token_generator.make_token(self.user)
uidb64 = force_text(urlsafe_base64_encode(force_bytes(self.user.pk)))
- response = PasswordResetConfirmView.as_view(success_url='dummy/')(self.request, uidb64=uidb64, token=token)
+ url = reverse('password_reset_confirm', kwargs={'uidb64': uidb64, 'token': token})
+ response = client.get(url)
self.assertContains(response, '<title>Enter new password</title>')
self.assertContains(response, '<h1>Enter new password</h1>')
diff --git a/tests/auth_tests/test_tokens.py b/tests/auth_tests/test_tokens.py
index 99f9741a0a..7ff3f15f3d 100644
--- a/tests/auth_tests/test_tokens.py
+++ b/tests/auth_tests/test_tokens.py
@@ -62,3 +62,10 @@ class TokenGeneratorTest(TestCase):
# This will put a 14-digit base36 timestamp into the token, which is too large.
with self.assertRaises(ValueError):
p0._make_token_with_timestamp(user, 175455491841851871349)
+
+ def test_check_token_with_nonexistent_token_and_user(self):
+ user = User.objects.create_user('tokentestuser', 'test2@example.com', 'testpw')
+ p0 = PasswordResetTokenGenerator()
+ tk1 = p0.make_token(user)
+ self.assertIs(p0.check_token(None, tk1), False)
+ self.assertIs(p0.check_token(user, None), False)
diff --git a/tests/auth_tests/test_views.py b/tests/auth_tests/test_views.py
index 209f9f698a..2d0d2ae96f 100644
--- a/tests/auth_tests/test_views.py
+++ b/tests/auth_tests/test_views.py
@@ -16,7 +16,8 @@ from django.contrib.auth.forms import (
)
from django.contrib.auth.models import User
from django.contrib.auth.views import (
- LoginView, logout_then_login, redirect_to_login,
+ INTERNAL_RESET_SESSION_TOKEN, LoginView, logout_then_login,
+ redirect_to_login,
)
from django.contrib.sessions.middleware import SessionMiddleware
from django.contrib.sites.requests import RequestSite
@@ -24,7 +25,7 @@ from django.core import mail
from django.db import connection
from django.http import HttpRequest, QueryDict
from django.middleware.csrf import CsrfViewMiddleware, get_token
-from django.test import TestCase, override_settings
+from django.test import Client, TestCase, override_settings
from django.test.utils import patch_logger
from django.urls import NoReverseMatch, reverse, reverse_lazy
from django.utils.deprecation import RemovedInDjango21Warning
@@ -33,6 +34,7 @@ from django.utils.http import urlquote
from django.utils.six.moves.urllib.parse import ParseResult, urlparse
from django.utils.translation import LANGUAGE_SESSION_KEY
+from .client import PasswordResetConfirmClient
from .models import CustomUser, UUIDUser
from .settings import AUTH_TEMPLATES
@@ -116,6 +118,9 @@ class AuthViewNamedURLTests(AuthViewsTestCase):
class PasswordResetTest(AuthViewsTestCase):
+ def setUp(self):
+ self.client = PasswordResetConfirmClient()
+
def test_email_not_found(self):
"""If the provided email is not registered, don't raise any error but
also don't send any email."""
@@ -278,6 +283,8 @@ class PasswordResetTest(AuthViewsTestCase):
# Check the password has been changed
u = User.objects.get(email='staffmember@example.com')
self.assertTrue(u.check_password("anewpassword"))
+ # The reset token is deleted from the session.
+ self.assertNotIn(INTERNAL_RESET_SESSION_TOKEN, self.client.session)
# Check we can't use the link again
response = self.client.get(path)
@@ -338,6 +345,23 @@ class PasswordResetTest(AuthViewsTestCase):
response = self.client.get('/reset/zzzzzzzzzzzzz/1-1/')
self.assertContains(response, "Hello, .")
+ def test_confirm_link_redirects_to_set_password_page(self):
+ url, path = self._test_confirm_start()
+ # Don't use PasswordResetConfirmClient (self.client) here which
+ # automatically fetches the redirect page.
+ client = Client()
+ response = client.get(path)
+ token = response.resolver_match.kwargs['token']
+ uuidb64 = response.resolver_match.kwargs['uidb64']
+ self.assertRedirects(response, '/reset/%s/set-password/' % uuidb64)
+ self.assertEqual(client.session['_password_reset_token'], token)
+
+ def test_invalid_link_if_going_directly_to_the_final_reset_password_url(self):
+ url, path = self._test_confirm_start()
+ _, uuidb64, _ = path.strip('/').split('/')
+ response = Client().get('/reset/%s/set-password/' % uuidb64)
+ self.assertContains(response, 'The password reset link was invalid')
+
@override_settings(AUTH_USER_MODEL='auth_tests.CustomUser')
class CustomUserPasswordResetTest(AuthViewsTestCase):
@@ -352,6 +376,9 @@ class CustomUserPasswordResetTest(AuthViewsTestCase):
cls.u1.set_password('password')
cls.u1.save()
+ def setUp(self):
+ self.client = PasswordResetConfirmClient()
+
def _test_confirm_start(self):
# Start by creating the email
response = self.client.post('/password_reset/', {'email': self.user_email})