From 58df8aa40fe88f753ba79e091a52f236246260b3 Mon Sep 17 00:00:00 2001
From: Rob <tienrobertnguyenn@gmail.com>
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 <tim.givois.mendez@gmail.com>
---
 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 <tanner@gmx.net>
     tibimicu@gmx.net
     Tim Allen <tim@pyphilly.org>
+    Tim Givois <tim.givois.mendez@gmail.com>
     Tim Graham <timograham@gmail.com>
     Tim Heap <tim@timheap.me>
     Tim Saylor <tim.saylor@gmail.com>
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),