Refs #23957 -- Required session verification per deprecation timeline.
This commit is contained in:
parent
5d383549ee
commit
849037af36
|
@ -9,11 +9,9 @@ a list of all possible variables.
|
|||
import importlib
|
||||
import os
|
||||
import time
|
||||
import warnings
|
||||
|
||||
from django.conf import global_settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.utils.deprecation import RemovedInDjango110Warning
|
||||
from django.utils.functional import LazyObject, empty
|
||||
|
||||
ENVIRONMENT_VARIABLE = "DJANGO_SETTINGS_MODULE"
|
||||
|
@ -118,16 +116,6 @@ class Settings(BaseSettings):
|
|||
if not self.SECRET_KEY:
|
||||
raise ImproperlyConfigured("The SECRET_KEY setting must not be empty.")
|
||||
|
||||
if ('django.contrib.auth.middleware.AuthenticationMiddleware' in self.MIDDLEWARE_CLASSES and
|
||||
'django.contrib.auth.middleware.SessionAuthenticationMiddleware' not in self.MIDDLEWARE_CLASSES):
|
||||
warnings.warn(
|
||||
"Session verification will become mandatory in Django 1.10. "
|
||||
"Please add 'django.contrib.auth.middleware.SessionAuthenticationMiddleware' "
|
||||
"to your MIDDLEWARE_CLASSES setting when you are ready to opt-in after "
|
||||
"reading the upgrade considerations in the 1.8 release notes.",
|
||||
RemovedInDjango110Warning
|
||||
)
|
||||
|
||||
if hasattr(time, 'tzset') and self.TIME_ZONE:
|
||||
# When we can, attempt to validate the timezone. If we can't find
|
||||
# this file, no check happens and it's harmless.
|
||||
|
|
|
@ -45,7 +45,6 @@ MIDDLEWARE_CLASSES = [
|
|||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
|
|
@ -173,8 +173,7 @@ def get_user(request):
|
|||
backend = load_backend(backend_path)
|
||||
user = backend.get_user(user_id)
|
||||
# Verify the session
|
||||
if ('django.contrib.auth.middleware.SessionAuthenticationMiddleware'
|
||||
in settings.MIDDLEWARE_CLASSES and hasattr(user, 'get_session_auth_hash')):
|
||||
if hasattr(user, 'get_session_auth_hash'):
|
||||
session_hash = request.session.get(HASH_SESSION_KEY)
|
||||
session_hash_verified = session_hash and constant_time_compare(
|
||||
session_hash,
|
||||
|
@ -196,8 +195,7 @@ def get_permission_codename(action, opts):
|
|||
|
||||
def update_session_auth_hash(request, user):
|
||||
"""
|
||||
Updating a user's password logs out all sessions for the user if
|
||||
django.contrib.auth.middleware.SessionAuthenticationMiddleware is enabled.
|
||||
Updating a user's password logs out all sessions for the user.
|
||||
|
||||
This function takes the current request and the updated user object from
|
||||
which the new session hash will be derived and updates the session hash
|
||||
|
|
|
@ -28,8 +28,8 @@ class SessionAuthenticationMiddleware(object):
|
|||
correspond to the user's current session authentication hash. However, it
|
||||
caused the "Vary: Cookie" header on all responses.
|
||||
|
||||
Now a backwards compatibility shim that enables session verification in
|
||||
auth.get_user() if this middleware is in MIDDLEWARE_CLASSES.
|
||||
It's now a shim to allow a single settings file to more easily support
|
||||
multiple versions of Django. Will be RemovedInDjango20Warning.
|
||||
"""
|
||||
def process_request(self, request):
|
||||
pass
|
||||
|
|
|
@ -303,9 +303,7 @@ def password_change(request,
|
|||
if form.is_valid():
|
||||
form.save()
|
||||
# Updating the password logs out all other sessions for the user
|
||||
# except the current one if
|
||||
# django.contrib.auth.middleware.SessionAuthenticationMiddleware
|
||||
# is enabled.
|
||||
# except the current one.
|
||||
update_session_auth_hash(request, form.user)
|
||||
return HttpResponseRedirect(post_change_redirect)
|
||||
else:
|
||||
|
|
|
@ -382,13 +382,6 @@ Middleware for utilizing Web server provided authentication when enabled only
|
|||
on the login page. See :ref:`persistent-remote-user-middleware-howto` for usage
|
||||
details.
|
||||
|
||||
.. class:: SessionAuthenticationMiddleware
|
||||
|
||||
Allows a user's sessions to be invalidated when their password changes. See
|
||||
:ref:`session-invalidation-on-password-change` for details. This middleware must
|
||||
appear after :class:`django.contrib.auth.middleware.AuthenticationMiddleware`
|
||||
in :setting:`MIDDLEWARE_CLASSES`.
|
||||
|
||||
CSRF protection middleware
|
||||
--------------------------
|
||||
|
||||
|
|
|
@ -1943,9 +1943,7 @@ The secret key is used for:
|
|||
|
||||
* All :doc:`sessions </topics/http/sessions>` if you are using
|
||||
any other session backend than ``django.contrib.sessions.backends.cache``,
|
||||
or if you use
|
||||
:class:`~django.contrib.auth.middleware.SessionAuthenticationMiddleware`
|
||||
and are using the default
|
||||
or are using the default
|
||||
:meth:`~django.contrib.auth.models.AbstractBaseUser.get_session_auth_hash()`.
|
||||
* All :doc:`messages </ref/contrib/messages>` if you are using
|
||||
:class:`~django.contrib.messages.storage.cookie.CookieStorage` or
|
||||
|
|
|
@ -349,7 +349,9 @@ removed in Django 1.10 (please see the :ref:`deprecation timeline
|
|||
|
||||
* Session verification is enabled regardless of whether or not
|
||||
``'django.contrib.auth.middleware.SessionAuthenticationMiddleware'`` is in
|
||||
``MIDDLEWARE_CLASSES``.
|
||||
``MIDDLEWARE_CLASSES``. ``SessionAuthenticationMiddleware`` no longer has
|
||||
any purpose and can be removed from ``MIDDLEWARE_CLASSES``. It's kept as
|
||||
a stub until Django 2.0 as a courtesy for users who don't read this note.
|
||||
|
||||
* Private attribute ``django.db.models.Field.related`` is removed.
|
||||
|
||||
|
|
|
@ -106,7 +106,7 @@ Bugfixes
|
|||
(:ticket:`23950`).
|
||||
|
||||
* Prevented the
|
||||
:class:`~django.contrib.auth.middleware.SessionAuthenticationMiddleware` from
|
||||
``django.contrib.auth.middleware.SessionAuthenticationMiddleware`` from
|
||||
setting a ``"Vary: Cookie"`` header on all responses (:ticket:`23939`).
|
||||
|
||||
* Fixed a crash when adding ``blank=True`` to ``TextField()`` on MySQL
|
||||
|
|
|
@ -435,9 +435,8 @@ Minor features
|
|||
method was added and if your :setting:`AUTH_USER_MODEL` inherits from
|
||||
:class:`~django.contrib.auth.models.AbstractBaseUser`, changing a user's
|
||||
password now invalidates old sessions if the
|
||||
:class:`~django.contrib.auth.middleware.SessionAuthenticationMiddleware` is
|
||||
enabled. See :ref:`session-invalidation-on-password-change` for more details
|
||||
including upgrade considerations when enabling this new middleware.
|
||||
``django.contrib.auth.middleware.SessionAuthenticationMiddleware`` is
|
||||
enabled. See :ref:`session-invalidation-on-password-change` for more details.
|
||||
|
||||
``django.contrib.formtools``
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
@ -1455,7 +1454,7 @@ Miscellaneous
|
|||
when the input is not valid UTF-8.
|
||||
|
||||
* With the addition of the
|
||||
:class:`~django.contrib.auth.middleware.SessionAuthenticationMiddleware` to
|
||||
``django.contrib.auth.middleware.SessionAuthenticationMiddleware`` to
|
||||
the default project template (pre-1.7.2 only), a database must be created
|
||||
before accessing a page using :djadmin:`runserver`.
|
||||
|
||||
|
|
|
@ -1621,7 +1621,7 @@ attribute will change from ``True`` to ``False`` in Django 1.9.
|
|||
Using ``AuthenticationMiddleware`` without ``SessionAuthenticationMiddleware``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
:class:`django.contrib.auth.middleware.SessionAuthenticationMiddleware` was
|
||||
``django.contrib.auth.middleware.SessionAuthenticationMiddleware`` was
|
||||
added in Django 1.7. In Django 1.7.2, its functionality was moved to
|
||||
``auth.get_user()`` and, for backwards compatibility, enabled only if
|
||||
``'django.contrib.auth.middleware.SessionAuthenticationMiddleware'`` appears in
|
||||
|
|
|
@ -108,9 +108,8 @@ Django also provides :ref:`views <built-in-auth-views>` and :ref:`forms
|
|||
<built-in-auth-forms>` that may be used to allow users to change their own
|
||||
passwords.
|
||||
|
||||
Changing a user's password will log out all their sessions if the
|
||||
:class:`~django.contrib.auth.middleware.SessionAuthenticationMiddleware` is
|
||||
enabled. See :ref:`session-invalidation-on-password-change` for details.
|
||||
Changing a user's password will log out all their sessions. See
|
||||
:ref:`session-invalidation-on-password-change` for details.
|
||||
|
||||
Authenticating Users
|
||||
--------------------
|
||||
|
@ -801,29 +800,23 @@ user to the login page or issue an HTTP 403 Forbidden response.
|
|||
Session invalidation on password change
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. warning::
|
||||
.. versionchanged:: 1.10
|
||||
|
||||
This protection only applies if
|
||||
:class:`~django.contrib.auth.middleware.SessionAuthenticationMiddleware`
|
||||
is enabled in :setting:`MIDDLEWARE_CLASSES`. It's included if
|
||||
``settings.py`` was generated by :djadmin:`startproject` on Django ≥ 1.7.
|
||||
|
||||
Session verification will become mandatory in Django 1.10 regardless of
|
||||
whether or not ``SessionAuthenticationMiddleware`` is enabled. If you have
|
||||
a pre-1.7 project or one generated using a template that doesn't include
|
||||
``SessionAuthenticationMiddleware``, consider enabling it before then after
|
||||
reading the upgrade considerations below.
|
||||
Session verification is enabled and mandatory in Django 1.10 (there's no
|
||||
way to disable it) regardless of whether or not
|
||||
``SessionAuthenticationMiddleware`` is enabled. In older
|
||||
versions, this protection only applies if
|
||||
``django.contrib.auth.middleware.SessionAuthenticationMiddleware``
|
||||
is enabled in :setting:`MIDDLEWARE_CLASSES`.
|
||||
|
||||
If your :setting:`AUTH_USER_MODEL` inherits from
|
||||
:class:`~django.contrib.auth.models.AbstractBaseUser` or implements its own
|
||||
:meth:`~django.contrib.auth.models.AbstractBaseUser.get_session_auth_hash()`
|
||||
method, authenticated sessions will include the hash returned by this function.
|
||||
In the :class:`~django.contrib.auth.models.AbstractBaseUser` case, this is an
|
||||
HMAC of the password field. If the
|
||||
:class:`~django.contrib.auth.middleware.SessionAuthenticationMiddleware` is
|
||||
enabled, Django verifies that the hash sent along with each request matches
|
||||
the one that's computed server-side. This allows a user to log out all of their
|
||||
sessions by changing their password.
|
||||
HMAC of the password field. Django verifies that the hash sent along with each
|
||||
request matches the one that's computed server-side. This allows a user to log
|
||||
out all of their sessions by changing their password.
|
||||
|
||||
The default password change views included with Django,
|
||||
:func:`django.contrib.auth.views.password_change` and the
|
||||
|
@ -849,15 +842,6 @@ and wish to have similar behavior, use this function:
|
|||
else:
|
||||
...
|
||||
|
||||
If you are upgrading an existing site and wish to enable this middleware without
|
||||
requiring all your users to re-login afterward, you should first upgrade to
|
||||
Django 1.7 and run it for a while so that as sessions are naturally recreated
|
||||
as users login, they include the session hash as described above. Once you
|
||||
start running your site with
|
||||
:class:`~django.contrib.auth.middleware.SessionAuthenticationMiddleware`, any
|
||||
users who have not logged in and had their session updated with the verification
|
||||
hash will have their existing session invalidated and be required to login.
|
||||
|
||||
.. note::
|
||||
|
||||
Since
|
||||
|
|
|
@ -66,8 +66,6 @@ and these items in your :setting:`MIDDLEWARE_CLASSES` setting:
|
|||
:doc:`sessions </topics/http/sessions>` across requests.
|
||||
2. :class:`~django.contrib.auth.middleware.AuthenticationMiddleware` associates
|
||||
users with requests using sessions.
|
||||
3. :class:`~django.contrib.auth.middleware.SessionAuthenticationMiddleware`
|
||||
logs users out of their other sessions after a password change.
|
||||
|
||||
With these settings in place, running the command ``manage.py migrate`` creates
|
||||
the necessary database tables for auth related models and permissions for any
|
||||
|
|
|
@ -33,7 +33,6 @@ here's the default value created by :djadmin:`django-admin startproject
|
|||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
|
|
@ -773,7 +773,7 @@ class AdminViewBasicTest(AdminViewBasicTestCase):
|
|||
user = User.objects.get(username='super')
|
||||
user.set_unusable_password()
|
||||
user.save()
|
||||
|
||||
self.client.force_login(user)
|
||||
response = self.client.get(reverse('admin:index'))
|
||||
self.assertNotContains(response, reverse('admin:password_change'),
|
||||
msg_prefix='The "change password" link should not be displayed if a user does not have a usable password.')
|
||||
|
|
|
@ -4,42 +4,21 @@ from django.http import HttpRequest
|
|||
from django.test import TestCase
|
||||
|
||||
|
||||
class TestSessionAuthenticationMiddleware(TestCase):
|
||||
class TestAuthenticationMiddleware(TestCase):
|
||||
def setUp(self):
|
||||
self.user_password = 'test_password'
|
||||
self.user = User.objects.create_user('test_user',
|
||||
'test@example.com',
|
||||
self.user_password)
|
||||
|
||||
self.user = User.objects.create_user('test_user', 'test@example.com', 'test_password')
|
||||
self.middleware = AuthenticationMiddleware()
|
||||
self.assertTrue(self.client.login(
|
||||
username=self.user.username,
|
||||
password=self.user_password,
|
||||
))
|
||||
self.client.force_login(self.user)
|
||||
self.request = HttpRequest()
|
||||
self.request.session = self.client.session
|
||||
|
||||
def test_changed_password_doesnt_invalidate_session(self):
|
||||
"""
|
||||
Changing a user's password shouldn't invalidate the session if session
|
||||
verification isn't activated.
|
||||
"""
|
||||
session_key = self.request.session.session_key
|
||||
def test_no_password_change_doesnt_invalidate_session(self):
|
||||
self.request.session = self.client.session
|
||||
self.middleware.process_request(self.request)
|
||||
self.assertIsNotNone(self.request.user)
|
||||
self.assertFalse(self.request.user.is_anonymous())
|
||||
|
||||
# After password change, user should remain logged in.
|
||||
self.user.set_password('new_password')
|
||||
self.user.save()
|
||||
self.middleware.process_request(self.request)
|
||||
self.assertIsNotNone(self.request.user)
|
||||
self.assertFalse(self.request.user.is_anonymous())
|
||||
self.assertEqual(session_key, self.request.session.session_key)
|
||||
|
||||
def test_changed_password_invalidates_session_with_middleware(self):
|
||||
with self.modify_settings(
|
||||
MIDDLEWARE_CLASSES={'append': ['django.contrib.auth.middleware.SessionAuthenticationMiddleware']}):
|
||||
def test_changed_password_invalidates_session(self):
|
||||
# After password change, user should be anonymous
|
||||
self.user.set_password('new_password')
|
||||
self.user.save()
|
||||
|
|
|
@ -24,7 +24,7 @@ from django.core.urlresolvers import NoReverseMatch, reverse, reverse_lazy
|
|||
from django.db import connection
|
||||
from django.http import HttpRequest, QueryDict
|
||||
from django.middleware.csrf import CsrfViewMiddleware, get_token
|
||||
from django.test import TestCase, modify_settings, override_settings
|
||||
from django.test import TestCase, override_settings
|
||||
from django.test.utils import patch_logger
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.http import urlquote
|
||||
|
@ -506,9 +506,6 @@ class ChangePasswordTest(AuthViewsTestCase):
|
|||
self.assertURLEqual(response.url, '/password_reset/')
|
||||
|
||||
|
||||
@modify_settings(MIDDLEWARE_CLASSES={
|
||||
'append': 'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
|
||||
})
|
||||
class SessionAuthenticationTests(AuthViewsTestCase):
|
||||
def test_user_password_change_updates_session(self):
|
||||
"""
|
||||
|
@ -876,9 +873,6 @@ class LogoutTest(AuthViewsTestCase):
|
|||
|
||||
# Redirect in test_user_change_password will fail if session auth hash
|
||||
# isn't updated after password change (#21649)
|
||||
@modify_settings(MIDDLEWARE_CLASSES={
|
||||
'append': 'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
|
||||
})
|
||||
@override_settings(
|
||||
PASSWORD_HASHERS=['django.contrib.auth.hashers.SHA1PasswordHasher'],
|
||||
ROOT_URLCONF='auth_tests.urls_admin',
|
||||
|
|
|
@ -10,8 +10,10 @@ class CustomUserAdmin(UserAdmin):
|
|||
def log_change(self, request, object, message):
|
||||
# LogEntry.user column doesn't get altered to expect a UUID, so set an
|
||||
# integer manually to avoid causing an error.
|
||||
original_pk = request.user.pk
|
||||
request.user.pk = 1
|
||||
super(CustomUserAdmin, self).log_change(request, object, message)
|
||||
request.user.pk = original_pk
|
||||
|
||||
site.register(get_user_model(), CustomUserAdmin)
|
||||
|
||||
|
|
|
@ -12,7 +12,6 @@ from django.test import (
|
|||
override_settings, signals,
|
||||
)
|
||||
from django.utils import six
|
||||
from django.utils.encoding import force_text
|
||||
|
||||
|
||||
@modify_settings(ITEMS={
|
||||
|
@ -489,47 +488,3 @@ class TestListSettings(unittest.TestCase):
|
|||
finally:
|
||||
del sys.modules['fake_settings_module']
|
||||
delattr(settings_module, setting)
|
||||
|
||||
|
||||
class TestSessionVerification(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.settings_module = ModuleType('fake_settings_module')
|
||||
self.settings_module.SECRET_KEY = 'foo'
|
||||
|
||||
def tearDown(self):
|
||||
if 'fake_settings_module' in sys.modules:
|
||||
del sys.modules['fake_settings_module']
|
||||
|
||||
def test_session_verification_deprecation_no_verification(self):
|
||||
self.settings_module.MIDDLEWARE_CLASSES = ['django.contrib.auth.middleware.AuthenticationMiddleware']
|
||||
sys.modules['fake_settings_module'] = self.settings_module
|
||||
with warnings.catch_warnings(record=True) as warn:
|
||||
warnings.filterwarnings('always')
|
||||
Settings('fake_settings_module')
|
||||
self.assertEqual(
|
||||
force_text(warn[0].message),
|
||||
"Session verification will become mandatory in Django 1.10. "
|
||||
"Please add 'django.contrib.auth.middleware.SessionAuthenticationMiddleware' "
|
||||
"to your MIDDLEWARE_CLASSES setting when you are ready to opt-in after "
|
||||
"reading the upgrade considerations in the 1.8 release notes.",
|
||||
)
|
||||
|
||||
def test_session_verification_deprecation_both(self):
|
||||
self.settings_module.MIDDLEWARE_CLASSES = [
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
|
||||
]
|
||||
sys.modules['fake_settings_module'] = self.settings_module
|
||||
with warnings.catch_warnings(record=True) as warn:
|
||||
warnings.filterwarnings('always')
|
||||
Settings('fake_settings_module')
|
||||
self.assertEqual(len(warn), 0)
|
||||
|
||||
def test_session_verification_deprecation_neither(self):
|
||||
self.settings_module.MIDDLEWARE_CLASSES = []
|
||||
sys.modules['fake_settings_module'] = self.settings_module
|
||||
with warnings.catch_warnings(record=True) as warn:
|
||||
warnings.filterwarnings('always')
|
||||
Settings('fake_settings_module')
|
||||
self.assertEqual(len(warn), 0)
|
||||
|
|
Loading…
Reference in New Issue