Fixed #28780 -- Allowed specyfing a token parameter displayed in password reset URLs.
Co-authored-by: Tim Givois <tim.givois.mendez@gmail.com>
This commit is contained in:
parent
8000767769
commit
58df8aa40f
1
AUTHORS
1
AUTHORS
|
@ -847,6 +847,7 @@ answer newbie questions, and generally made Django that much better:
|
||||||
Thomas Tanner <tanner@gmx.net>
|
Thomas Tanner <tanner@gmx.net>
|
||||||
tibimicu@gmx.net
|
tibimicu@gmx.net
|
||||||
Tim Allen <tim@pyphilly.org>
|
Tim Allen <tim@pyphilly.org>
|
||||||
|
Tim Givois <tim.givois.mendez@gmail.com>
|
||||||
Tim Graham <timograham@gmail.com>
|
Tim Graham <timograham@gmail.com>
|
||||||
Tim Heap <tim@timheap.me>
|
Tim Heap <tim@timheap.me>
|
||||||
Tim Saylor <tim.saylor@gmail.com>
|
Tim Saylor <tim.saylor@gmail.com>
|
||||||
|
|
|
@ -234,7 +234,6 @@ class PasswordResetView(PasswordContextMixin, FormView):
|
||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
|
||||||
INTERNAL_RESET_URL_TOKEN = 'set-password'
|
|
||||||
INTERNAL_RESET_SESSION_TOKEN = '_password_reset_token'
|
INTERNAL_RESET_SESSION_TOKEN = '_password_reset_token'
|
||||||
|
|
||||||
|
|
||||||
|
@ -247,6 +246,7 @@ class PasswordResetConfirmView(PasswordContextMixin, FormView):
|
||||||
form_class = SetPasswordForm
|
form_class = SetPasswordForm
|
||||||
post_reset_login = False
|
post_reset_login = False
|
||||||
post_reset_login_backend = None
|
post_reset_login_backend = None
|
||||||
|
reset_url_token = 'set-password'
|
||||||
success_url = reverse_lazy('password_reset_complete')
|
success_url = reverse_lazy('password_reset_complete')
|
||||||
template_name = 'registration/password_reset_confirm.html'
|
template_name = 'registration/password_reset_confirm.html'
|
||||||
title = _('Enter new password')
|
title = _('Enter new password')
|
||||||
|
@ -262,7 +262,7 @@ class PasswordResetConfirmView(PasswordContextMixin, FormView):
|
||||||
|
|
||||||
if self.user is not None:
|
if self.user is not None:
|
||||||
token = kwargs['token']
|
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)
|
session_token = self.request.session.get(INTERNAL_RESET_SESSION_TOKEN)
|
||||||
if self.token_generator.check_token(self.user, session_token):
|
if self.token_generator.check_token(self.user, session_token):
|
||||||
# If the token is valid, display the password reset form.
|
# 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
|
# avoids the possibility of leaking the token in the
|
||||||
# HTTP Referer header.
|
# HTTP Referer header.
|
||||||
self.request.session[INTERNAL_RESET_SESSION_TOKEN] = token
|
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)
|
return HttpResponseRedirect(redirect_url)
|
||||||
|
|
||||||
# Display the "Password reset unsuccessful" page.
|
# Display the "Password reset unsuccessful" page.
|
||||||
|
|
|
@ -59,7 +59,9 @@ Minor features
|
||||||
:mod:`django.contrib.auth`
|
: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`
|
:mod:`django.contrib.contenttypes`
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
|
@ -1395,6 +1395,13 @@ implementation details see :ref:`using-the-views`.
|
||||||
* ``extra_context``: A dictionary of context data that will be added to the
|
* ``extra_context``: A dictionary of context data that will be added to the
|
||||||
default context data passed to the template.
|
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:**
|
**Template context:**
|
||||||
|
|
||||||
* ``form``: The form (see ``form_class`` above) for setting the new user's
|
* ``form``: The form (see ``form_class`` above) for setting the new user's
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from django.contrib.auth.views import (
|
from django.contrib.auth.views import (
|
||||||
INTERNAL_RESET_SESSION_TOKEN, INTERNAL_RESET_URL_TOKEN,
|
INTERNAL_RESET_SESSION_TOKEN, PasswordResetConfirmView,
|
||||||
)
|
)
|
||||||
from django.test import Client
|
from django.test import Client
|
||||||
|
|
||||||
|
@ -22,6 +22,8 @@ class PasswordResetConfirmClient(Client):
|
||||||
>>> client = PasswordResetConfirmClient()
|
>>> client = PasswordResetConfirmClient()
|
||||||
>>> client.get('/reset/bla/my-token/')
|
>>> client.get('/reset/bla/my-token/')
|
||||||
"""
|
"""
|
||||||
|
reset_url_token = PasswordResetConfirmView.reset_url_token
|
||||||
|
|
||||||
def _get_password_reset_confirm_redirect_url(self, url):
|
def _get_password_reset_confirm_redirect_url(self, url):
|
||||||
token = extract_token_from_url(url)
|
token = extract_token_from_url(url)
|
||||||
if not token:
|
if not token:
|
||||||
|
@ -30,7 +32,7 @@ class PasswordResetConfirmClient(Client):
|
||||||
session = self.session
|
session = self.session
|
||||||
session[INTERNAL_RESET_SESSION_TOKEN] = token
|
session[INTERNAL_RESET_SESSION_TOKEN] = token
|
||||||
session.save()
|
session.save()
|
||||||
return url.replace(token, INTERNAL_RESET_URL_TOKEN)
|
return url.replace(token, self.reset_url_token)
|
||||||
|
|
||||||
def get(self, path, *args, **kwargs):
|
def get(self, path, *args, **kwargs):
|
||||||
redirect_url = self._get_password_reset_confirm_redirect_url(path)
|
redirect_url = self._get_password_reset_confirm_redirect_url(path)
|
||||||
|
|
|
@ -304,6 +304,16 @@ class PasswordResetTest(AuthViewsTestCase):
|
||||||
response = self.client.post(path, {'new_password1': 'anewpassword', 'new_password2': 'anewpassword'})
|
response = self.client.post(path, {'new_password1': 'anewpassword', 'new_password2': 'anewpassword'})
|
||||||
self.assertRedirects(response, '/password_reset/', fetch_redirect_response=False)
|
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):
|
def test_confirm_login_post_reset(self):
|
||||||
url, path = self._test_confirm_start()
|
url, path = self._test_confirm_start()
|
||||||
path = path.replace('/reset/', '/reset/post_reset_login/')
|
path = path.replace('/reset/', '/reset/post_reset_login/')
|
||||||
|
@ -360,6 +370,16 @@ class PasswordResetTest(AuthViewsTestCase):
|
||||||
self.assertRedirects(response, '/reset/%s/set-password/' % uuidb64)
|
self.assertRedirects(response, '/reset/%s/set-password/' % uuidb64)
|
||||||
self.assertEqual(client.session['_password_reset_token'], token)
|
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):
|
def test_invalid_link_if_going_directly_to_the_final_reset_password_url(self):
|
||||||
url, path = self._test_confirm_start()
|
url, path = self._test_confirm_start()
|
||||||
_, uuidb64, _ = path.strip('/').split('/')
|
_, uuidb64, _ = path.strip('/').split('/')
|
||||||
|
|
|
@ -111,6 +111,10 @@ urlpatterns = auth_urlpatterns + [
|
||||||
'^reset/custom/named/{}/$'.format(uid_token),
|
'^reset/custom/named/{}/$'.format(uid_token),
|
||||||
views.PasswordResetConfirmView.as_view(success_url=reverse_lazy('password_reset')),
|
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(
|
re_path(
|
||||||
'^reset/post_reset_login/{}/$'.format(uid_token),
|
'^reset/post_reset_login/{}/$'.format(uid_token),
|
||||||
views.PasswordResetConfirmView.as_view(post_reset_login=True),
|
views.PasswordResetConfirmView.as_view(post_reset_login=True),
|
||||||
|
|
Loading…
Reference in New Issue