From 58df8aa40fe88f753ba79e091a52f236246260b3 Mon Sep 17 00:00:00 2001 From: Rob Date: Thu, 23 May 2019 22:18:49 +1000 Subject: [PATCH] Fixed #28780 -- Allowed specyfing a token parameter displayed in password reset URLs. Co-authored-by: Tim Givois --- AUTHORS | 1 + django/contrib/auth/views.py | 6 +++--- docs/releases/3.0.txt | 4 +++- docs/topics/auth/default.txt | 7 +++++++ tests/auth_tests/client.py | 6 ++++-- tests/auth_tests/test_views.py | 20 ++++++++++++++++++++ tests/auth_tests/urls.py | 4 ++++ 7 files changed, 42 insertions(+), 6 deletions(-) diff --git a/AUTHORS b/AUTHORS index 4487126aa3..b417f8d676 100644 --- a/AUTHORS +++ b/AUTHORS @@ -847,6 +847,7 @@ answer newbie questions, and generally made Django that much better: Thomas Tanner tibimicu@gmx.net Tim Allen + Tim Givois Tim Graham Tim Heap Tim Saylor diff --git a/django/contrib/auth/views.py b/django/contrib/auth/views.py index 8c5435e726..0d2326702a 100644 --- a/django/contrib/auth/views.py +++ b/django/contrib/auth/views.py @@ -234,7 +234,6 @@ class PasswordResetView(PasswordContextMixin, FormView): return super().form_valid(form) -INTERNAL_RESET_URL_TOKEN = 'set-password' INTERNAL_RESET_SESSION_TOKEN = '_password_reset_token' @@ -247,6 +246,7 @@ class PasswordResetConfirmView(PasswordContextMixin, FormView): form_class = SetPasswordForm post_reset_login = False post_reset_login_backend = None + reset_url_token = 'set-password' success_url = reverse_lazy('password_reset_complete') template_name = 'registration/password_reset_confirm.html' title = _('Enter new password') @@ -262,7 +262,7 @@ class PasswordResetConfirmView(PasswordContextMixin, FormView): if self.user is not None: token = kwargs['token'] - if token == INTERNAL_RESET_URL_TOKEN: + if token == self.reset_url_token: session_token = self.request.session.get(INTERNAL_RESET_SESSION_TOKEN) if self.token_generator.check_token(self.user, session_token): # If the token is valid, display the password reset form. @@ -275,7 +275,7 @@ class PasswordResetConfirmView(PasswordContextMixin, FormView): # avoids the possibility of leaking the token in the # HTTP Referer header. self.request.session[INTERNAL_RESET_SESSION_TOKEN] = token - redirect_url = self.request.path.replace(token, INTERNAL_RESET_URL_TOKEN) + redirect_url = self.request.path.replace(token, self.reset_url_token) return HttpResponseRedirect(redirect_url) # Display the "Password reset unsuccessful" page. diff --git a/docs/releases/3.0.txt b/docs/releases/3.0.txt index ac275b31b9..5e661d8943 100644 --- a/docs/releases/3.0.txt +++ b/docs/releases/3.0.txt @@ -59,7 +59,9 @@ Minor features :mod:`django.contrib.auth` ~~~~~~~~~~~~~~~~~~~~~~~~~~ -* ... +* The new ``reset_url_token`` attribute in + :class:`~django.contrib.auth.views.PasswordResetConfirmView` allows specifying + a token parameter displayed as a component of password reset URLs. :mod:`django.contrib.contenttypes` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/topics/auth/default.txt b/docs/topics/auth/default.txt index d7c0732794..691a7cbd24 100644 --- a/docs/topics/auth/default.txt +++ b/docs/topics/auth/default.txt @@ -1395,6 +1395,13 @@ implementation details see :ref:`using-the-views`. * ``extra_context``: A dictionary of context data that will be added to the default context data passed to the template. + * ``reset_url_token``: Token parameter displayed as a component of password + reset URLs. Defaults to ``'set-password'``. + + .. versionchanged:: 3.0 + + The ``reset_url_token`` class attribute was added. + **Template context:** * ``form``: The form (see ``form_class`` above) for setting the new user's diff --git a/tests/auth_tests/client.py b/tests/auth_tests/client.py index 8f09f115cd..42740bb0e8 100644 --- a/tests/auth_tests/client.py +++ b/tests/auth_tests/client.py @@ -1,7 +1,7 @@ import re from django.contrib.auth.views import ( - INTERNAL_RESET_SESSION_TOKEN, INTERNAL_RESET_URL_TOKEN, + INTERNAL_RESET_SESSION_TOKEN, PasswordResetConfirmView, ) from django.test import Client @@ -22,6 +22,8 @@ class PasswordResetConfirmClient(Client): >>> client = PasswordResetConfirmClient() >>> client.get('/reset/bla/my-token/') """ + reset_url_token = PasswordResetConfirmView.reset_url_token + def _get_password_reset_confirm_redirect_url(self, url): token = extract_token_from_url(url) if not token: @@ -30,7 +32,7 @@ class PasswordResetConfirmClient(Client): session = self.session session[INTERNAL_RESET_SESSION_TOKEN] = token session.save() - return url.replace(token, INTERNAL_RESET_URL_TOKEN) + return url.replace(token, self.reset_url_token) def get(self, path, *args, **kwargs): redirect_url = self._get_password_reset_confirm_redirect_url(path) diff --git a/tests/auth_tests/test_views.py b/tests/auth_tests/test_views.py index 99de78e44d..0b07b7ebbc 100644 --- a/tests/auth_tests/test_views.py +++ b/tests/auth_tests/test_views.py @@ -304,6 +304,16 @@ class PasswordResetTest(AuthViewsTestCase): response = self.client.post(path, {'new_password1': 'anewpassword', 'new_password2': 'anewpassword'}) self.assertRedirects(response, '/password_reset/', fetch_redirect_response=False) + def test_confirm_custom_reset_url_token(self): + url, path = self._test_confirm_start() + path = path.replace('/reset/', '/reset/custom/token/') + self.client.reset_url_token = 'set-passwordcustom' + response = self.client.post( + path, + {'new_password1': 'anewpassword', 'new_password2': 'anewpassword'}, + ) + self.assertRedirects(response, '/reset/done/', fetch_redirect_response=False) + def test_confirm_login_post_reset(self): url, path = self._test_confirm_start() path = path.replace('/reset/', '/reset/post_reset_login/') @@ -360,6 +370,16 @@ class PasswordResetTest(AuthViewsTestCase): self.assertRedirects(response, '/reset/%s/set-password/' % uuidb64) self.assertEqual(client.session['_password_reset_token'], token) + def test_confirm_custom_reset_url_token_link_redirects_to_set_password_page(self): + url, path = self._test_confirm_start() + path = path.replace('/reset/', '/reset/custom/token/') + client = Client() + response = client.get(path) + token = response.resolver_match.kwargs['token'] + uuidb64 = response.resolver_match.kwargs['uidb64'] + self.assertRedirects(response, '/reset/custom/token/%s/set-passwordcustom/' % 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('/') diff --git a/tests/auth_tests/urls.py b/tests/auth_tests/urls.py index 142a2b49c2..f3cfa9f982 100644 --- a/tests/auth_tests/urls.py +++ b/tests/auth_tests/urls.py @@ -111,6 +111,10 @@ urlpatterns = auth_urlpatterns + [ '^reset/custom/named/{}/$'.format(uid_token), views.PasswordResetConfirmView.as_view(success_url=reverse_lazy('password_reset')), ), + re_path( + '^reset/custom/token/{}/$'.format(uid_token), + views.PasswordResetConfirmView.as_view(reset_url_token='set-passwordcustom'), + ), re_path( '^reset/post_reset_login/{}/$'.format(uid_token), views.PasswordResetConfirmView.as_view(post_reset_login=True),