Refs #17209 -- Removed login/logout and password reset/change function-based views.

Per deprecation timeline.
This commit is contained in:
Tim Graham 2017-09-02 19:24:18 -04:00
parent deb592b3e3
commit 4f313e284e
30 changed files with 42 additions and 870 deletions

View File

@ -15,8 +15,7 @@ from django.contrib.auth.tokens import default_token_generator
from django.contrib.sites.shortcuts import get_current_site
from django.http import HttpResponseRedirect, QueryDict
from django.shortcuts import resolve_url
from django.template.response import TemplateResponse
from django.urls import reverse, reverse_lazy
from django.urls import reverse_lazy
from django.utils.decorators import method_decorator
from django.utils.deprecation import RemovedInDjango21Warning
from django.utils.http import is_safe_url, urlsafe_base64_decode
@ -107,23 +106,6 @@ class LoginView(SuccessURLAllowedHostsMixin, FormView):
return context
def login(request, template_name='registration/login.html',
redirect_field_name=REDIRECT_FIELD_NAME,
authentication_form=AuthenticationForm,
extra_context=None, redirect_authenticated_user=False):
warnings.warn(
'The login() view is superseded by the class-based LoginView().',
RemovedInDjango21Warning, stacklevel=2
)
return LoginView.as_view(
template_name=template_name,
redirect_field_name=redirect_field_name,
form_class=authentication_form,
extra_context=extra_context,
redirect_authenticated_user=redirect_authenticated_user,
)(request)
class LogoutView(SuccessURLAllowedHostsMixin, TemplateView):
"""
Log out the user and display the 'You are logged out' message.
@ -184,22 +166,6 @@ class LogoutView(SuccessURLAllowedHostsMixin, TemplateView):
return context
def logout(request, next_page=None,
template_name='registration/logged_out.html',
redirect_field_name=REDIRECT_FIELD_NAME,
extra_context=None):
warnings.warn(
'The logout() view is superseded by the class-based LogoutView().',
RemovedInDjango21Warning, stacklevel=2
)
return LogoutView.as_view(
next_page=next_page,
template_name=template_name,
redirect_field_name=redirect_field_name,
extra_context=extra_context,
)(request)
_sentinel = object()
@ -234,143 +200,6 @@ def redirect_to_login(next, login_url=None, redirect_field_name=REDIRECT_FIELD_N
return HttpResponseRedirect(urlunparse(login_url_parts))
# 4 views for password reset:
# - password_reset sends the mail
# - password_reset_done shows a success message for the above
# - password_reset_confirm checks the link the user clicked and
# prompts for a new password
# - password_reset_complete shows a success message for the above
@csrf_protect
def password_reset(request,
template_name='registration/password_reset_form.html',
email_template_name='registration/password_reset_email.html',
subject_template_name='registration/password_reset_subject.txt',
password_reset_form=PasswordResetForm,
token_generator=default_token_generator,
post_reset_redirect=None,
from_email=None,
extra_context=None,
html_email_template_name=None,
extra_email_context=None):
warnings.warn("The password_reset() view is superseded by the "
"class-based PasswordResetView().",
RemovedInDjango21Warning, stacklevel=2)
if post_reset_redirect is None:
post_reset_redirect = reverse('password_reset_done')
else:
post_reset_redirect = resolve_url(post_reset_redirect)
if request.method == "POST":
form = password_reset_form(request.POST)
if form.is_valid():
opts = {
'use_https': request.is_secure(),
'token_generator': token_generator,
'from_email': from_email,
'email_template_name': email_template_name,
'subject_template_name': subject_template_name,
'request': request,
'html_email_template_name': html_email_template_name,
'extra_email_context': extra_email_context,
}
form.save(**opts)
return HttpResponseRedirect(post_reset_redirect)
else:
form = password_reset_form()
context = {
'form': form,
'title': _('Password reset'),
}
if extra_context is not None:
context.update(extra_context)
return TemplateResponse(request, template_name, context)
def password_reset_done(request,
template_name='registration/password_reset_done.html',
extra_context=None):
warnings.warn("The password_reset_done() view is superseded by the "
"class-based PasswordResetDoneView().",
RemovedInDjango21Warning, stacklevel=2)
context = {
'title': _('Password reset sent'),
}
if extra_context is not None:
context.update(extra_context)
return TemplateResponse(request, template_name, context)
# Doesn't need csrf_protect since no-one can guess the URL
@sensitive_post_parameters()
@never_cache
def password_reset_confirm(request, uidb64=None, token=None,
template_name='registration/password_reset_confirm.html',
token_generator=default_token_generator,
set_password_form=SetPasswordForm,
post_reset_redirect=None,
extra_context=None):
"""
Check the hash in a password reset link and present a form for entering a
new password.
"""
warnings.warn("The password_reset_confirm() view is superseded by the "
"class-based PasswordResetConfirmView().",
RemovedInDjango21Warning, stacklevel=2)
assert uidb64 is not None and token is not None # checked by URLconf
if post_reset_redirect is None:
post_reset_redirect = reverse('password_reset_complete')
else:
post_reset_redirect = resolve_url(post_reset_redirect)
try:
# urlsafe_base64_decode() decodes to bytestring
uid = urlsafe_base64_decode(uidb64).decode()
user = UserModel._default_manager.get(pk=uid)
except (TypeError, ValueError, OverflowError, UserModel.DoesNotExist):
user = None
if user is not None and token_generator.check_token(user, token):
validlink = True
title = _('Enter new password')
if request.method == 'POST':
form = set_password_form(user, request.POST)
if form.is_valid():
form.save()
return HttpResponseRedirect(post_reset_redirect)
else:
form = set_password_form(user)
else:
validlink = False
form = None
title = _('Password reset unsuccessful')
context = {
'form': form,
'title': title,
'validlink': validlink,
}
if extra_context is not None:
context.update(extra_context)
return TemplateResponse(request, template_name, context)
def password_reset_complete(request,
template_name='registration/password_reset_complete.html',
extra_context=None):
warnings.warn("The password_reset_complete() view is superseded by the "
"class-based PasswordResetCompleteView().",
RemovedInDjango21Warning, stacklevel=2)
context = {
'login_url': resolve_url(settings.LOGIN_URL),
'title': _('Password reset complete'),
}
if extra_context is not None:
context.update(extra_context)
return TemplateResponse(request, template_name, context)
# Class-based password reset views
# - PasswordResetView sends the mail
# - PasswordResetDoneView shows a success message for the above
@ -511,57 +340,6 @@ class PasswordResetCompleteView(PasswordContextMixin, TemplateView):
return context
@sensitive_post_parameters()
@csrf_protect
@login_required
def password_change(request,
template_name='registration/password_change_form.html',
post_change_redirect=None,
password_change_form=PasswordChangeForm,
extra_context=None):
warnings.warn("The password_change() view is superseded by the "
"class-based PasswordChangeView().",
RemovedInDjango21Warning, stacklevel=2)
if post_change_redirect is None:
post_change_redirect = reverse('password_change_done')
else:
post_change_redirect = resolve_url(post_change_redirect)
if request.method == "POST":
form = password_change_form(user=request.user, data=request.POST)
if form.is_valid():
form.save()
# Updating the password logs out all other sessions for the user
# except the current one.
update_session_auth_hash(request, form.user)
return HttpResponseRedirect(post_change_redirect)
else:
form = password_change_form(user=request.user)
context = {
'form': form,
'title': _('Password change'),
}
if extra_context is not None:
context.update(extra_context)
return TemplateResponse(request, template_name, context)
@login_required
def password_change_done(request,
template_name='registration/password_change_done.html',
extra_context=None):
warnings.warn("The password_change_done() view is superseded by the "
"class-based PasswordChangeDoneView().",
RemovedInDjango21Warning, stacklevel=2)
context = {
'title': _('Password change successful'),
}
if extra_context is not None:
context.update(extra_context)
return TemplateResponse(request, template_name, context)
class PasswordChangeView(PasswordContextMixin, FormView):
form_class = PasswordChangeForm
success_url = reverse_lazy('password_change_done')

View File

@ -714,7 +714,7 @@ details on these changes.
remove calls to this method, and instead ensure that their auth related views
are CSRF protected, which ensures that cookies are enabled.
* The version of :func:`django.contrib.auth.views.password_reset_confirm` that
* The version of ``django.contrib.auth.views.password_reset_confirm()`` that
supports base36 encoded user IDs
(``django.contrib.auth.views.password_reset_confirm_uidb36``) will be
removed. If your site has been running Django 1.6 for more than

View File

@ -10,7 +10,7 @@ CVE-2017-7233: Open redirect and possible XSS attack via user-supplied numeric r
============================================================================================
Django relies on user input in some cases (e.g.
:func:`django.contrib.auth.views.login` and :doc:`i18n </topics/i18n/index>`)
``django.contrib.auth.views.login()`` and :doc:`i18n </topics/i18n/index>`)
to redirect the user to an "on success" URL. The security check for these
redirects (namely ``django.utils.http.is_safe_url()``) considered some numeric
URLs (e.g. ``http:999999999``) "safe" when they shouldn't be.

View File

@ -118,19 +118,19 @@ Minor features
subclassed ``django.contrib.auth.hashers.PBKDF2PasswordHasher`` to change the
default value.
* The :func:`~django.contrib.auth.views.logout` view sends "no-cache" headers
* The ``django.contrib.auth.views.logout()`` view sends "no-cache" headers
to prevent an issue where Safari caches redirects and prevents a user from
being able to log out.
* Added the optional ``backend`` argument to :func:`~django.contrib.auth.login`
* Added the optional ``backend`` argument to :func:`django.contrib.auth.login`
to allow using it without credentials.
* The new :setting:`LOGOUT_REDIRECT_URL` setting controls the redirect of the
:func:`~django.contrib.auth.views.logout` view, if the view doesn't get a
``django.contrib.auth.views.logout()`` view, if the view doesn't get a
``next_page`` argument.
* The new ``redirect_authenticated_user`` parameter for the
:func:`~django.contrib.auth.views.login` view allows redirecting
``django.contrib.auth.views.login()`` view allows redirecting
authenticated users visiting the login page.
* The new :class:`~django.contrib.auth.backends.AllowAllUsersModelBackend` and

View File

@ -636,7 +636,7 @@ message as a nonexistent account.
Password reset view now accepts ``from_email``
----------------------------------------------
The :func:`django.contrib.auth.views.password_reset` view now accepts a
The ``django.contrib.auth.views.password_reset()`` view now accepts a
``from_email`` parameter, which is passed to the ``password_reset_form``s
``save()`` method as a keyword argument. If you are using this view with a
custom password reset form, then you will need to ensure your form's ``save()``

View File

@ -36,7 +36,7 @@ which are accepted by some browsers. This allows a user to be redirected to
an unsafe URL unexpectedly.
Django relies on user input in some cases (e.g.
:func:`django.contrib.auth.views.login`, ``django.contrib.comments``, and
``django.contrib.auth.views.login()``, ``django.contrib.comments``, and
:doc:`i18n </topics/i18n/index>`) to redirect the user to an "on success" URL.
The security checks for these redirects (namely
``django.utils.http.is_safe_url()``) did not correctly validate some malformed

View File

@ -35,7 +35,7 @@ Mitigated possible XSS attack via user-supplied redirect URLs
=============================================================
Django relies on user input in some cases (e.g.
:func:`django.contrib.auth.views.login` and :doc:`i18n </topics/i18n/index>`)
``django.contrib.auth.views.login()`` and :doc:`i18n </topics/i18n/index>`)
to redirect the user to an "on success" URL. The security checks for these
redirects (namely ``django.utils.http.is_safe_url()``) didn't strip leading
whitespace on the tested URL and as such considered URLs like

View File

@ -10,7 +10,7 @@ Mitigated possible XSS attack via user-supplied redirect URLs
=============================================================
Django relies on user input in some cases (e.g.
:func:`django.contrib.auth.views.login` and :doc:`i18n </topics/i18n/index>`)
``django.contrib.auth.views.login()`` and :doc:`i18n </topics/i18n/index>`)
to redirect the user to an "on success" URL. The security checks for these
redirects (namely ``django.utils.http.is_safe_url()``) accepted URLs with
leading control characters and so considered URLs like ``\x08javascript:...``

View File

@ -14,7 +14,7 @@ Denial-of-service possibility in ``logout()`` view by filling session store
===========================================================================
Previously, a session could be created when anonymously accessing the
:func:`django.contrib.auth.views.logout` view (provided it wasn't decorated
``django.contrib.auth.views.logout()`` view (provided it wasn't decorated
with :func:`~django.contrib.auth.decorators.login_required` as done in the
admin). This could allow an attacker to easily create many new session records
by sending repeated requests, potentially filling up the session store or

View File

@ -13,7 +13,7 @@ Mitigated possible XSS attack via user-supplied redirect URLs
=============================================================
Django relies on user input in some cases (e.g.
:func:`django.contrib.auth.views.login`, ``django.contrib.comments``, and
``django.contrib.auth.views.login()``, ``django.contrib.comments``, and
:doc:`i18n </topics/i18n/index>`) to redirect the user to an "on success" URL.
The security checks for these redirects (namely
``django.utils.http.is_safe_url()``) didn't check if the scheme is ``http(s)``

View File

@ -10,7 +10,7 @@ Mitigated possible XSS attack via user-supplied redirect URLs
=============================================================
Django relies on user input in some cases (e.g.
:func:`django.contrib.auth.views.login`, ``django.contrib.comments``, and
``django.contrib.auth.views.login()``, ``django.contrib.comments``, and
:doc:`i18n </topics/i18n/index>`) to redirect the user to an "on success" URL.
The security checks for these redirects (namely
``django.utils.http.is_safe_url()``) didn't check if the scheme is ``http(s)``

View File

@ -36,7 +36,7 @@ which are accepted by some browsers. This allows a user to be redirected to
an unsafe URL unexpectedly.
Django relies on user input in some cases (e.g.
:func:`django.contrib.auth.views.login`, ``django.contrib.comments``, and
``django.contrib.auth.views.login()``, ``django.contrib.comments``, and
:doc:`i18n </topics/i18n/index>`) to redirect the user to an "on success" URL.
The security checks for these redirects (namely
``django.utils.http.is_safe_url()``) did not correctly validate some malformed

View File

@ -34,7 +34,7 @@ Mitigated possible XSS attack via user-supplied redirect URLs
=============================================================
Django relies on user input in some cases (e.g.
:func:`django.contrib.auth.views.login` and :doc:`i18n </topics/i18n/index>`)
``django.contrib.auth.views.login()`` and :doc:`i18n </topics/i18n/index>`)
to redirect the user to an "on success" URL. The security checks for these
redirects (namely ``django.utils.http.is_safe_url()``) didn't strip leading
whitespace on the tested URL and as such considered URLs like

View File

@ -27,7 +27,7 @@ Mitigated possible XSS attack via user-supplied redirect URLs
=============================================================
Django relies on user input in some cases (e.g.
:func:`django.contrib.auth.views.login` and :doc:`i18n </topics/i18n/index>`)
``django.contrib.auth.views.login()`` and :doc:`i18n </topics/i18n/index>`)
to redirect the user to an "on success" URL. The security checks for these
redirects (namely ``django.utils.http.is_safe_url()``) accepted URLs with
leading control characters and so considered URLs like ``\x08javascript:...``

View File

@ -36,7 +36,7 @@ which are accepted by some browsers. This allows a user to be redirected to
an unsafe URL unexpectedly.
Django relies on user input in some cases (e.g.
:func:`django.contrib.auth.views.login`, ``django.contrib.comments``, and
``django.contrib.auth.views.login()``, ``django.contrib.comments``, and
:doc:`i18n </topics/i18n/index>`) to redirect the user to an "on success" URL.
The security checks for these redirects (namely
``django.utils.http.is_safe_url()``) did not correctly validate some malformed

View File

@ -280,10 +280,10 @@ Minor features
:attr:`~django.http.HttpResponse.reason_phrase`.
* When giving the URL of the next page for
:func:`~django.contrib.auth.views.logout`,
:func:`~django.contrib.auth.views.password_reset`,
:func:`~django.contrib.auth.views.password_reset_confirm`,
and :func:`~django.contrib.auth.views.password_change`, you can now pass
``django.contrib.auth.views.logout()``,
``django.contrib.auth.views.password_reset()``,
``django.contrib.auth.views.password_reset_confirm()``,
and ``django.contrib.auth.views.password_change()``, you can now pass
URL names and they will be resolved.
* The new :option:`dumpdata --pks` option specifies the primary keys of objects
@ -742,17 +742,17 @@ can set the
Past versions of Django used base 36 encoding of the ``User`` primary key in
the password reset views and URLs
(:func:`django.contrib.auth.views.password_reset_confirm`). Base 36 encoding is
(``django.contrib.auth.views.password_reset_confirm()``). Base 36 encoding is
sufficient if the user primary key is an integer, however, with the
introduction of custom user models in Django 1.5, that assumption may no longer
be true.
:func:`django.contrib.auth.views.password_reset_confirm` has been modified to
``django.contrib.auth.views.password_reset_confirm()`` has been modified to
take a ``uidb64`` parameter instead of ``uidb36``. If you are reversing this
view, for example in a custom ``password_reset_email.html`` template, be sure
to update your code.
A temporary shim for :func:`django.contrib.auth.views.password_reset_confirm`
A temporary shim for ``django.contrib.auth.views.password_reset_confirm()``
that will allow password reset links generated prior to Django 1.6 to continue
to work has been added to provide backwards compatibility; this will be removed
in Django 1.7. Thus, as long as your site has been running Django 1.6 for more

View File

@ -10,7 +10,7 @@ Denial-of-service possibility in ``logout()`` view by filling session store
===========================================================================
Previously, a session could be created when anonymously accessing the
:func:`django.contrib.auth.views.logout` view (provided it wasn't decorated
``django.contrib.auth.views.logout()`` view (provided it wasn't decorated
with :func:`~django.contrib.auth.decorators.login_required` as done in the
admin). This could allow an attacker to easily create many new session records
by sending repeated requests, potentially filling up the session store or

View File

@ -34,7 +34,7 @@ Mitigated possible XSS attack via user-supplied redirect URLs
=============================================================
Django relies on user input in some cases (e.g.
:func:`django.contrib.auth.views.login` and :doc:`i18n </topics/i18n/index>`)
``django.contrib.auth.views.login()`` and :doc:`i18n </topics/i18n/index>`)
to redirect the user to an "on success" URL. The security checks for these
redirects (namely ``django.utils.http.is_safe_url()``) didn't strip leading
whitespace on the tested URL and as such considered URLs like

View File

@ -27,7 +27,7 @@ Mitigated possible XSS attack via user-supplied redirect URLs
=============================================================
Django relies on user input in some cases (e.g.
:func:`django.contrib.auth.views.login` and :doc:`i18n </topics/i18n/index>`)
``django.contrib.auth.views.login()`` and :doc:`i18n </topics/i18n/index>`)
to redirect the user to an "on success" URL. The security checks for these
redirects (namely ``django.utils.http.is_safe_url()``) accepted URLs with
leading control characters and so considered URLs like ``\x08javascript:...``

View File

@ -421,7 +421,7 @@ Minor features
<django.contrib.auth.forms.AuthenticationForm.confirm_login_allowed>` method
to more easily customize the login policy.
* :func:`django.contrib.auth.views.password_reset` takes an optional
* ``django.contrib.auth.views.password_reset()`` takes an optional
``html_email_template_name`` parameter used to send a multipart HTML email
for password resets.
@ -1846,7 +1846,7 @@ remove usage of these features.
* The ``check_for_test_cookie`` method in
:class:`~django.contrib.auth.forms.AuthenticationForm` is removed.
* The version of :func:`django.contrib.auth.views.password_reset_confirm` that
* The version of ``django.contrib.auth.views.password_reset_confirm()`` that
supports base36 encoded user IDs
(``django.contrib.auth.views.password_reset_confirm_uidb36``) is removed.

View File

@ -10,7 +10,7 @@ CVE-2016-2512: Malicious redirect and possible XSS attack via user-supplied redi
===============================================================================================================
Django relies on user input in some cases (e.g.
:func:`django.contrib.auth.views.login` and :doc:`i18n </topics/i18n/index>`)
``django.contrib.auth.views.login()`` and :doc:`i18n </topics/i18n/index>`)
to redirect the user to an "on success" URL. The security check for these
redirects (namely ``django.utils.http.is_safe_url()``) considered some URLs
with basic authentication credentials "safe" when they shouldn't be.

View File

@ -10,7 +10,7 @@ CVE-2017-7233: Open redirect and possible XSS attack via user-supplied numeric r
============================================================================================
Django relies on user input in some cases (e.g.
:func:`django.contrib.auth.views.login` and :doc:`i18n </topics/i18n/index>`)
``django.contrib.auth.views.login()`` and :doc:`i18n </topics/i18n/index>`)
to redirect the user to an "on success" URL. The security check for these
redirects (namely ``django.utils.http.is_safe_url()``) considered some numeric
URLs (e.g. ``http:999999999``) "safe" when they shouldn't be.

View File

@ -10,7 +10,7 @@ Denial-of-service possibility in ``logout()`` view by filling session store
===========================================================================
Previously, a session could be created when anonymously accessing the
:func:`django.contrib.auth.views.logout` view (provided it wasn't decorated
``django.contrib.auth.views.logout()`` view (provided it wasn't decorated
with :func:`~django.contrib.auth.decorators.login_required` as done in the
admin). This could allow an attacker to easily create many new session records
by sending repeated requests, potentially filling up the session store or

View File

@ -11,7 +11,7 @@ CVE-2017-7233: Open redirect and possible XSS attack via user-supplied numeric r
============================================================================================
Django relies on user input in some cases (e.g.
:func:`django.contrib.auth.views.login` and :doc:`i18n </topics/i18n/index>`)
``django.contrib.auth.views.login()`` and :doc:`i18n </topics/i18n/index>`)
to redirect the user to an "on success" URL. The security check for these
redirects (namely ``django.utils.http.is_safe_url()``) considered some numeric
URLs (e.g. ``http:999999999``) "safe" when they shouldn't be.

View File

@ -10,7 +10,7 @@ CVE-2016-2512: Malicious redirect and possible XSS attack via user-supplied redi
===============================================================================================================
Django relies on user input in some cases (e.g.
:func:`django.contrib.auth.views.login` and :doc:`i18n </topics/i18n/index>`)
``django.contrib.auth.views.login()`` and :doc:`i18n </topics/i18n/index>`)
to redirect the user to an "on success" URL. The security check for these
redirects (namely ``django.utils.http.is_safe_url()``) considered some URLs
with basic authentication credentials "safe" when they shouldn't be.

View File

@ -216,7 +216,7 @@ Minor features
makes it possible to use ``REMOTE_USER`` for setups where the header is only
populated on login pages instead of every request in the session.
* The :func:`~django.contrib.auth.views.password_reset` view accepts an
* The ``django.contrib.auth.views.password_reset()`` view accepts an
``extra_email_context`` parameter.
:mod:`django.contrib.contenttypes`

View File

@ -223,3 +223,9 @@ Features removed in 2.1
These features have reached the end of their deprecation cycle and are removed
in Django 2.1. See :ref:`deprecated-features-1.11` for details, including how
to remove usage of these features.
in Django 2.1. See :ref:`deprecated-features-1.11` and for details, including
how to remove usage of these features.
* ``contrib.auth.views.login()``, ``logout()``, ``password_change()``,
``password_change_done()``, ``password_reset()``, ``password_reset_done()``,
``password_reset_confirm()``, and ``password_reset_complete()`` are removed.

View File

@ -945,16 +945,6 @@ All authentication views
This is a list with all the views ``django.contrib.auth`` provides. For
implementation details see :ref:`using-the-views`.
.. function:: login(request, template_name=`registration/login.html`, redirect_field_name='next', authentication_form=AuthenticationForm, extra_context=None, redirect_authenticated_user=False)
.. deprecated:: 1.11
The ``login`` function-based view should be replaced by the class-based
:class:`LoginView`.
The optional arguments of this view are similar to the class-based
``LoginView`` attributes.
.. class:: LoginView
.. versionadded:: 1.11
@ -1093,16 +1083,6 @@ implementation details see :ref:`using-the-views`.
``get_user()`` method which returns the authenticated user object (this
method is only ever called after successful form validation).
.. function:: logout(request, next_page=None, template_name='registration/logged_out.html', redirect_field_name='next', extra_context=None)
.. deprecated:: 1.11
The ``logout`` function-based view should be replaced by the
class-based :class:`LogoutView`.
The optional arguments of this view are similar to the class-based
``LogoutView`` attributes.
.. class:: LogoutView
.. versionadded:: 1.11
@ -1165,18 +1145,6 @@ implementation details see :ref:`using-the-views`.
The unused ``extra_context`` parameter is deprecated and will be
removed in Django 2.1.
.. function:: password_change(request, template_name='registration/password_change_form.html', post_change_redirect=None, password_change_form=PasswordChangeForm, extra_context=None)
.. deprecated:: 1.11
The ``password_change`` function-based view should be replaced by the
class-based :class:`PasswordChangeView`.
The optional arguments of this view are similar to the class-based
``PasswordChangeView`` attributes, except the ``post_change_redirect`` and
``password_change_form`` arguments which map to the ``success_url`` and
``form_class`` attributes of the class-based view.
.. class:: PasswordChangeView
.. versionadded:: 1.11
@ -1206,16 +1174,6 @@ implementation details see :ref:`using-the-views`.
* ``form``: The password change form (see ``form_class`` above).
.. function:: password_change_done(request, template_name='registration/password_change_done.html', extra_context=None)
.. deprecated:: 1.11
The ``password_change_done`` function-based view should be replaced by
the class-based :class:`PasswordChangeDoneView`.
The optional arguments of this view are similar to the class-based
``PasswordChangeDoneView`` attributes.
.. class:: PasswordChangeDoneView
.. versionadded:: 1.11
@ -1233,18 +1191,6 @@ 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.
.. function:: password_reset(request, template_name='registration/password_reset_form.html', email_template_name='registration/password_reset_email.html', subject_template_name='registration/password_reset_subject.txt', password_reset_form=PasswordResetForm, token_generator=default_token_generator, post_reset_redirect=None, from_email=None, extra_context=None, html_email_template_name=None, extra_email_context=None)
.. deprecated:: 1.11
The ``password_reset`` function-based view should be replaced by the
class-based :class:`PasswordResetView`.
The optional arguments of this view are similar to the class-based
``PasswordResetView`` attributes, except the ``post_reset_redirect`` and
``password_reset_form`` arguments which map to the ``success_url`` and
``form_class`` attributes of the class-based view.
.. class:: PasswordResetView
.. versionadded:: 1.11
@ -1345,16 +1291,6 @@ implementation details see :ref:`using-the-views`.
The same template context is used for subject template. Subject must be
single line plain text string.
.. function:: password_reset_done(request, template_name='registration/password_reset_done.html', extra_context=None)
.. deprecated:: 1.11
The ``password_reset_done`` function-based view should be replaced by
the class-based :class:`PasswordResetDoneView`.
The optional arguments of this view are similar to the class-based
``PasswordResetDoneView`` attributes.
.. class:: PasswordResetDoneView
.. versionadded:: 1.11
@ -1380,18 +1316,6 @@ 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.
.. function:: password_reset_confirm(request, uidb64=None, token=None, template_name='registration/password_reset_confirm.html', token_generator=default_token_generator, set_password_form=SetPasswordForm, post_reset_redirect=None, extra_context=None)
.. deprecated:: 1.11
The ``password_reset_confirm`` function-based view should be replaced by
the class-based :class:`PasswordResetConfirmView`.
The optional arguments of this view are similar to the class-based
``PasswordResetConfirmView`` attributes, except the ``post_reset_redirect``
and ``set_password_form`` arguments which map to the ``success_url`` and
``form_class`` attributes of the class-based view.
.. class:: PasswordResetConfirmView
.. versionadded:: 1.11
@ -1442,16 +1366,6 @@ implementation details see :ref:`using-the-views`.
* ``validlink``: Boolean, True if the link (combination of ``uidb64`` and
``token``) is valid or unused yet.
.. function:: password_reset_complete(request, template_name='registration/password_reset_complete.html', extra_context=None)
.. deprecated:: 1.11
The ``password_reset_complete`` function-based view should be replaced
by the class-based :class:`PasswordResetCompleteView`.
The optional arguments of this view are similar to the class-based
``PasswordResetCompleteView`` attributes.
.. class:: PasswordResetCompleteView
.. versionadded:: 1.11

View File

@ -1,488 +0,0 @@
import datetime
import itertools
import re
from urllib.parse import ParseResult, urlparse
from django.conf import settings
from django.contrib.auth import SESSION_KEY
from django.contrib.auth.forms import (
AuthenticationForm, PasswordChangeForm, SetPasswordForm,
)
from django.contrib.auth.models import User
from django.contrib.auth.views import login, logout
from django.core import mail
from django.http import QueryDict
from django.test import RequestFactory, TestCase, override_settings
from django.test.utils import ignore_warnings, patch_logger
from django.utils.deprecation import RemovedInDjango21Warning
from .models import CustomUser, UUIDUser
from .settings import AUTH_TEMPLATES
@override_settings(
LANGUAGES=[('en', 'English')],
LANGUAGE_CODE='en',
TEMPLATES=AUTH_TEMPLATES,
ROOT_URLCONF='auth_tests.urls_deprecated',
)
class AuthViewsTestCase(TestCase):
"""
Helper base class for all the follow test cases.
"""
@classmethod
def setUpTestData(cls):
cls.u1 = User.objects.create_user(username='testclient', password='password', email='testclient@example.com')
cls.u3 = User.objects.create_user(username='staff', password='password', email='staffmember@example.com')
def login(self, username='testclient', password='password'):
response = self.client.post('/login/', {
'username': username,
'password': password,
})
self.assertIn(SESSION_KEY, self.client.session)
return response
def logout(self):
response = self.client.get('/admin/logout/')
self.assertEqual(response.status_code, 200)
self.assertNotIn(SESSION_KEY, self.client.session)
def assertFormError(self, response, error):
"""Assert that error is found in response.context['form'] errors"""
form_errors = list(itertools.chain(*response.context['form'].errors.values()))
self.assertIn(str(error), form_errors)
def assertURLEqual(self, url, expected, parse_qs=False):
"""
Given two URLs, make sure all their components (the ones given by
urlparse) are equal, only comparing components that are present in both
URLs.
If `parse_qs` is True, then the querystrings are parsed with QueryDict.
This is useful if you don't want the order of parameters to matter.
Otherwise, the query strings are compared as-is.
"""
fields = ParseResult._fields
for attr, x, y in zip(fields, urlparse(url), urlparse(expected)):
if parse_qs and attr == 'query':
x, y = QueryDict(x), QueryDict(y)
if x and y and x != y:
self.fail("%r != %r (%s doesn't match)" % (url, expected, attr))
@ignore_warnings(category=RemovedInDjango21Warning)
class PasswordResetTest(AuthViewsTestCase):
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."""
response = self.client.get('/password_reset/')
self.assertEqual(response.status_code, 200)
response = self.client.post('/password_reset/', {'email': 'not_a_real_email@email.com'})
self.assertEqual(response.status_code, 302)
self.assertEqual(len(mail.outbox), 0)
def test_email_found(self):
"Email is sent if a valid email address is provided for password reset"
response = self.client.post('/password_reset/', {'email': 'staffmember@example.com'})
self.assertEqual(response.status_code, 302)
self.assertEqual(len(mail.outbox), 1)
self.assertIn("http://", mail.outbox[0].body)
self.assertEqual(settings.DEFAULT_FROM_EMAIL, mail.outbox[0].from_email)
# optional multipart text/html email has been added. Make sure original,
# default functionality is 100% the same
self.assertFalse(mail.outbox[0].message().is_multipart())
def test_extra_email_context(self):
"""
extra_email_context should be available in the email template context.
"""
response = self.client.post(
'/password_reset_extra_email_context/',
{'email': 'staffmember@example.com'},
)
self.assertEqual(response.status_code, 302)
self.assertEqual(len(mail.outbox), 1)
self.assertIn('Email email context: "Hello!"', mail.outbox[0].body)
def test_html_mail_template(self):
"""
A multipart email with text/plain and text/html is sent
if the html_email_template parameter is passed to the view
"""
response = self.client.post('/password_reset/html_email_template/', {'email': 'staffmember@example.com'})
self.assertEqual(response.status_code, 302)
self.assertEqual(len(mail.outbox), 1)
message = mail.outbox[0].message()
self.assertEqual(len(message.get_payload()), 2)
self.assertTrue(message.is_multipart())
self.assertEqual(message.get_payload(0).get_content_type(), 'text/plain')
self.assertEqual(message.get_payload(1).get_content_type(), 'text/html')
self.assertNotIn('<html>', message.get_payload(0).get_payload())
self.assertIn('<html>', message.get_payload(1).get_payload())
def test_email_found_custom_from(self):
"Email is sent if a valid email address is provided for password reset when a custom from_email is provided."
response = self.client.post('/password_reset_from_email/', {'email': 'staffmember@example.com'})
self.assertEqual(response.status_code, 302)
self.assertEqual(len(mail.outbox), 1)
self.assertEqual("staffmember@example.com", mail.outbox[0].from_email)
# Skip any 500 handler action (like sending more mail...)
@override_settings(DEBUG_PROPAGATE_EXCEPTIONS=True)
def test_poisoned_http_host(self):
"Poisoned HTTP_HOST headers can't be used for reset emails"
# This attack is based on the way browsers handle URLs. The colon
# should be used to separate the port, but if the URL contains an @,
# the colon is interpreted as part of a username for login purposes,
# making 'evil.com' the request domain. Since HTTP_HOST is used to
# produce a meaningful reset URL, we need to be certain that the
# HTTP_HOST header isn't poisoned. This is done as a check when get_host()
# is invoked, but we check here as a practical consequence.
with patch_logger('django.security.DisallowedHost', 'error') as logger_calls:
response = self.client.post(
'/password_reset/',
{'email': 'staffmember@example.com'},
HTTP_HOST='www.example:dr.frankenstein@evil.tld'
)
self.assertEqual(response.status_code, 400)
self.assertEqual(len(mail.outbox), 0)
self.assertEqual(len(logger_calls), 1)
# Skip any 500 handler action (like sending more mail...)
@override_settings(DEBUG_PROPAGATE_EXCEPTIONS=True)
def test_poisoned_http_host_admin_site(self):
"Poisoned HTTP_HOST headers can't be used for reset emails on admin views"
with patch_logger('django.security.DisallowedHost', 'error') as logger_calls:
response = self.client.post(
'/admin_password_reset/',
{'email': 'staffmember@example.com'},
HTTP_HOST='www.example:dr.frankenstein@evil.tld'
)
self.assertEqual(response.status_code, 400)
self.assertEqual(len(mail.outbox), 0)
self.assertEqual(len(logger_calls), 1)
def _test_confirm_start(self):
# Start by creating the email
self.client.post('/password_reset/', {'email': 'staffmember@example.com'})
self.assertEqual(len(mail.outbox), 1)
return self._read_signup_email(mail.outbox[0])
def _read_signup_email(self, email):
urlmatch = re.search(r"https?://[^/]*(/.*reset/\S*)", email.body)
self.assertIsNotNone(urlmatch, "No URL found in sent email")
return urlmatch.group(), urlmatch.groups()[0]
def test_confirm_valid(self):
url, path = self._test_confirm_start()
response = self.client.get(path)
# redirect to a 'complete' page:
self.assertContains(response, "Please enter your new password")
def test_confirm_invalid(self):
url, path = self._test_confirm_start()
# Let's munge the token in the path, but keep the same length,
# in case the URLconf will reject a different length.
path = path[:-5] + ("0" * 4) + path[-1]
response = self.client.get(path)
self.assertContains(response, "The password reset link was invalid")
def test_confirm_invalid_user(self):
# We get a 200 response for a nonexistent user, not a 404
response = self.client.get('/reset/123456/1-1/')
self.assertContains(response, "The password reset link was invalid")
def test_confirm_overflow_user(self):
# We get a 200 response for a base36 user id that overflows int
response = self.client.get('/reset/zzzzzzzzzzzzz/1-1/')
self.assertContains(response, "The password reset link was invalid")
def test_confirm_invalid_post(self):
# Same as test_confirm_invalid, but trying
# to do a POST instead.
url, path = self._test_confirm_start()
path = path[:-5] + ("0" * 4) + path[-1]
self.client.post(path, {
'new_password1': 'anewpassword',
'new_password2': ' anewpassword',
})
# Check the password has not been changed
u = User.objects.get(email='staffmember@example.com')
self.assertTrue(not u.check_password("anewpassword"))
def test_confirm_complete(self):
url, path = self._test_confirm_start()
response = self.client.post(path, {'new_password1': 'anewpassword', 'new_password2': 'anewpassword'})
# Check the password has been changed
u = User.objects.get(email='staffmember@example.com')
self.assertTrue(u.check_password("anewpassword"))
# Check we can't use the link again
response = self.client.get(path)
self.assertContains(response, "The password reset link was invalid")
def test_confirm_different_passwords(self):
url, path = self._test_confirm_start()
response = self.client.post(path, {'new_password1': 'anewpassword', 'new_password2': 'x'})
self.assertFormError(response, SetPasswordForm.error_messages['password_mismatch'])
def test_reset_redirect_default(self):
response = self.client.post('/password_reset/', {'email': 'staffmember@example.com'})
self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, '/password_reset/done/')
def test_reset_custom_redirect(self):
response = self.client.post('/password_reset/custom_redirect/', {'email': 'staffmember@example.com'})
self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, '/custom/')
def test_reset_custom_redirect_named(self):
response = self.client.post('/password_reset/custom_redirect/named/', {'email': 'staffmember@example.com'})
self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, '/password_reset/')
def test_confirm_redirect_default(self):
url, path = self._test_confirm_start()
response = self.client.post(path, {'new_password1': 'anewpassword', 'new_password2': 'anewpassword'})
self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, '/reset/done/')
def test_confirm_redirect_custom(self):
url, path = self._test_confirm_start()
path = path.replace('/reset/', '/reset/custom/')
response = self.client.post(path, {'new_password1': 'anewpassword', 'new_password2': 'anewpassword'})
self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, '/custom/')
def test_confirm_redirect_custom_named(self):
url, path = self._test_confirm_start()
path = path.replace('/reset/', '/reset/custom/named/')
response = self.client.post(path, {'new_password1': 'anewpassword', 'new_password2': 'anewpassword'})
self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, '/password_reset/')
def test_confirm_display_user_from_form(self):
url, path = self._test_confirm_start()
response = self.client.get(path)
# #16919 -- The ``password_reset_confirm`` view should pass the user
# object to the ``SetPasswordForm``, even on GET requests.
# For this test, we render ``{{ form.user }}`` in the template
# ``registration/password_reset_confirm.html`` so that we can test this.
username = User.objects.get(email='staffmember@example.com').username
self.assertContains(response, "Hello, %s." % username)
# However, the view should NOT pass any user object on a form if the
# password reset link was invalid.
response = self.client.get('/reset/zzzzzzzzzzzzz/1-1/')
self.assertContains(response, "Hello, .")
@ignore_warnings(category=RemovedInDjango21Warning)
@override_settings(AUTH_USER_MODEL='auth_tests.CustomUser')
class CustomUserPasswordResetTest(AuthViewsTestCase):
user_email = 'staffmember@example.com'
@classmethod
def setUpTestData(cls):
cls.u1 = CustomUser.custom_objects.create(
email='staffmember@example.com',
date_of_birth=datetime.date(1976, 11, 8),
)
cls.u1.set_password('password')
cls.u1.save()
def _test_confirm_start(self):
# Start by creating the email
response = self.client.post('/password_reset/', {'email': self.user_email})
self.assertEqual(response.status_code, 302)
self.assertEqual(len(mail.outbox), 1)
return self._read_signup_email(mail.outbox[0])
def _read_signup_email(self, email):
urlmatch = re.search(r"https?://[^/]*(/.*reset/\S*)", email.body)
self.assertIsNotNone(urlmatch, "No URL found in sent email")
return urlmatch.group(), urlmatch.groups()[0]
def test_confirm_valid_custom_user(self):
url, path = self._test_confirm_start()
response = self.client.get(path)
# redirect to a 'complete' page:
self.assertContains(response, "Please enter your new password")
# then submit a new password
response = self.client.post(path, {
'new_password1': 'anewpassword',
'new_password2': 'anewpassword',
})
self.assertRedirects(response, '/reset/done/')
@ignore_warnings(category=RemovedInDjango21Warning)
@override_settings(AUTH_USER_MODEL='auth_tests.UUIDUser')
class UUIDUserPasswordResetTest(CustomUserPasswordResetTest):
def _test_confirm_start(self):
# instead of fixture
UUIDUser.objects.create_user(
email=self.user_email,
username='foo',
password='foo',
)
return super()._test_confirm_start()
@ignore_warnings(category=RemovedInDjango21Warning)
class ChangePasswordTest(AuthViewsTestCase):
def fail_login(self, password='password'):
response = self.client.post('/login/', {
'username': 'testclient',
'password': password,
})
self.assertFormError(response, AuthenticationForm.error_messages['invalid_login'] % {
'username': User._meta.get_field('username').verbose_name
})
def logout(self):
self.client.get('/logout/')
def test_password_change_fails_with_invalid_old_password(self):
self.login()
response = self.client.post('/password_change/', {
'old_password': 'donuts',
'new_password1': 'password1',
'new_password2': 'password1',
})
self.assertFormError(response, PasswordChangeForm.error_messages['password_incorrect'])
def test_password_change_fails_with_mismatched_passwords(self):
self.login()
response = self.client.post('/password_change/', {
'old_password': 'password',
'new_password1': 'password1',
'new_password2': 'donuts',
})
self.assertFormError(response, SetPasswordForm.error_messages['password_mismatch'])
def test_password_change_succeeds(self):
self.login()
self.client.post('/password_change/', {
'old_password': 'password',
'new_password1': 'password1',
'new_password2': 'password1',
})
self.fail_login()
self.login(password='password1')
def test_password_change_done_succeeds(self):
self.login()
response = self.client.post('/password_change/', {
'old_password': 'password',
'new_password1': 'password1',
'new_password2': 'password1',
})
self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, '/password_change/done/')
@override_settings(LOGIN_URL='/login/')
def test_password_change_done_fails(self):
response = self.client.get('/password_change/done/')
self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, '/login/?next=/password_change/done/')
def test_password_change_redirect_default(self):
self.login()
response = self.client.post('/password_change/', {
'old_password': 'password',
'new_password1': 'password1',
'new_password2': 'password1',
})
self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, '/password_change/done/')
def test_password_change_redirect_custom(self):
self.login()
response = self.client.post('/password_change/custom/', {
'old_password': 'password',
'new_password1': 'password1',
'new_password2': 'password1',
})
self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, '/custom/')
def test_password_change_redirect_custom_named(self):
self.login()
response = self.client.post('/password_change/custom/named/', {
'old_password': 'password',
'new_password1': 'password1',
'new_password2': 'password1',
})
self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, '/password_reset/')
@ignore_warnings(category=RemovedInDjango21Warning)
class SessionAuthenticationTests(AuthViewsTestCase):
def test_user_password_change_updates_session(self):
"""
#21649 - Ensure contrib.auth.views.password_change updates the user's
session auth hash after a password change so the session isn't logged out.
"""
self.login()
response = self.client.post('/password_change/', {
'old_password': 'password',
'new_password1': 'password1',
'new_password2': 'password1',
})
# if the hash isn't updated, retrieving the redirection page will fail.
self.assertRedirects(response, '/password_change/done/')
@ignore_warnings(category=RemovedInDjango21Warning)
class TestLogin(TestCase):
def setUp(self):
self.factory = RequestFactory()
self.request = self.factory.get('/')
def test_template_name(self):
response = login(self.request, 'template.html')
self.assertEqual(response.template_name, ['template.html'])
def test_form_class(self):
class NewForm(AuthenticationForm):
def confirm_login_allowed(self, user):
pass
response = login(self.request, 'template.html', None, NewForm)
self.assertEqual(response.context_data['form'].__class__.__name__, 'NewForm')
def test_extra_context(self):
extra_context = {'fake_context': 'fake_context'}
response = login(self.request, 'template.html', None, AuthenticationForm, extra_context)
self.assertEqual(response.resolve_context('fake_context'), 'fake_context')
@ignore_warnings(category=RemovedInDjango21Warning)
class TestLogout(AuthViewsTestCase):
def setUp(self):
self.login()
self.factory = RequestFactory()
self.request = self.factory.post('/')
self.request.session = self.client.session
def test_template_name(self):
response = logout(self.request, None, 'template.html')
self.assertEqual(response.template_name, ['template.html'])
def test_next_page(self):
response = logout(self.request, 'www.next_page.com')
self.assertEqual(response.url, 'www.next_page.com')
def test_extra_context(self):
extra_context = {'fake_context': 'fake_context'}
response = logout(self.request, None, 'template.html', None, extra_context)
self.assertEqual(response.resolve_context('fake_context'), 'fake_context')

View File

@ -1,38 +0,0 @@
from django.conf.urls import url
from django.contrib import admin
from django.contrib.auth import views
from django.contrib.auth.decorators import login_required
# special urls for deprecated function-based views
urlpatterns = [
url(r'^login/$', views.login, name='login'),
url(r'^logout/$', views.logout, name='logout'),
url(r'^password_change/$', views.password_change, name='password_change'),
url(r'^password_change/done/$', views.password_change_done, name='password_change_done'),
url(r'^password_reset/$', views.password_reset, name='password_reset'),
url(r'^password_reset/done/$', views.password_reset_done, name='password_reset_done'),
url(r'^reset/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
views.password_reset_confirm, name='password_reset_confirm'),
url(r'^reset/done/$', views.password_reset_complete, name='password_reset_complete'),
url(r'^password_reset_from_email/$', views.password_reset, {'from_email': 'staffmember@example.com'}),
url(r'^password_reset_extra_email_context/$', views.password_reset,
{'extra_email_context': {'greeting': 'Hello!'}}),
url(r'^password_reset/custom_redirect/$', views.password_reset, {'post_reset_redirect': '/custom/'}),
url(r'^password_reset/custom_redirect/named/$', views.password_reset, {'post_reset_redirect': 'password_reset'}),
url(r'^password_reset/html_email_template/$', views.password_reset,
{'html_email_template_name': 'registration/html_password_reset_email.html'}),
url(r'^reset/custom/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
views.password_reset_confirm,
{'post_reset_redirect': '/custom/'}),
url(r'^reset/custom/named/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
views.password_reset_confirm,
{'post_reset_redirect': 'password_reset'}),
url(r'^password_change/custom/$', views.password_change, {'post_change_redirect': '/custom/'}),
url(r'^password_change/custom/named/$', views.password_change, {'post_change_redirect': 'password_reset'}),
url(r'^login_required/$', login_required(views.password_reset)),
url(r'^login_required_login_url/$', login_required(views.password_reset, login_url='/somewhere/')),
# This line is only required to render the password reset with is_admin=True
url(r'^admin/', admin.site.urls),
]