Refs #23957 -- Required session verification per deprecation timeline.

This commit is contained in:
Tim Graham 2015-09-02 20:50:34 -04:00
parent 5d383549ee
commit 849037af36
19 changed files with 38 additions and 152 deletions

View File

@ -9,11 +9,9 @@ a list of all possible variables.
import importlib import importlib
import os import os
import time import time
import warnings
from django.conf import global_settings from django.conf import global_settings
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.utils.deprecation import RemovedInDjango110Warning
from django.utils.functional import LazyObject, empty from django.utils.functional import LazyObject, empty
ENVIRONMENT_VARIABLE = "DJANGO_SETTINGS_MODULE" ENVIRONMENT_VARIABLE = "DJANGO_SETTINGS_MODULE"
@ -118,16 +116,6 @@ class Settings(BaseSettings):
if not self.SECRET_KEY: if not self.SECRET_KEY:
raise ImproperlyConfigured("The SECRET_KEY setting must not be empty.") 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: if hasattr(time, 'tzset') and self.TIME_ZONE:
# When we can, attempt to validate the timezone. If we can't find # When we can, attempt to validate the timezone. If we can't find
# this file, no check happens and it's harmless. # this file, no check happens and it's harmless.

View File

@ -45,7 +45,6 @@ MIDDLEWARE_CLASSES = [
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', 'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
] ]

View File

@ -173,8 +173,7 @@ def get_user(request):
backend = load_backend(backend_path) backend = load_backend(backend_path)
user = backend.get_user(user_id) user = backend.get_user(user_id)
# Verify the session # Verify the session
if ('django.contrib.auth.middleware.SessionAuthenticationMiddleware' if hasattr(user, 'get_session_auth_hash'):
in settings.MIDDLEWARE_CLASSES and hasattr(user, 'get_session_auth_hash')):
session_hash = request.session.get(HASH_SESSION_KEY) session_hash = request.session.get(HASH_SESSION_KEY)
session_hash_verified = session_hash and constant_time_compare( session_hash_verified = session_hash and constant_time_compare(
session_hash, session_hash,
@ -196,8 +195,7 @@ def get_permission_codename(action, opts):
def update_session_auth_hash(request, user): def update_session_auth_hash(request, user):
""" """
Updating a user's password logs out all sessions for the user if Updating a user's password logs out all sessions for the user.
django.contrib.auth.middleware.SessionAuthenticationMiddleware is enabled.
This function takes the current request and the updated user object from 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 which the new session hash will be derived and updates the session hash

View File

@ -28,8 +28,8 @@ class SessionAuthenticationMiddleware(object):
correspond to the user's current session authentication hash. However, it correspond to the user's current session authentication hash. However, it
caused the "Vary: Cookie" header on all responses. caused the "Vary: Cookie" header on all responses.
Now a backwards compatibility shim that enables session verification in It's now a shim to allow a single settings file to more easily support
auth.get_user() if this middleware is in MIDDLEWARE_CLASSES. multiple versions of Django. Will be RemovedInDjango20Warning.
""" """
def process_request(self, request): def process_request(self, request):
pass pass

View File

@ -303,9 +303,7 @@ def password_change(request,
if form.is_valid(): if form.is_valid():
form.save() form.save()
# Updating the password logs out all other sessions for the user # Updating the password logs out all other sessions for the user
# except the current one if # except the current one.
# django.contrib.auth.middleware.SessionAuthenticationMiddleware
# is enabled.
update_session_auth_hash(request, form.user) update_session_auth_hash(request, form.user)
return HttpResponseRedirect(post_change_redirect) return HttpResponseRedirect(post_change_redirect)
else: else:

View File

@ -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 on the login page. See :ref:`persistent-remote-user-middleware-howto` for usage
details. 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 CSRF protection middleware
-------------------------- --------------------------

View File

@ -1943,9 +1943,7 @@ The secret key is used for:
* All :doc:`sessions </topics/http/sessions>` if you are using * All :doc:`sessions </topics/http/sessions>` if you are using
any other session backend than ``django.contrib.sessions.backends.cache``, any other session backend than ``django.contrib.sessions.backends.cache``,
or if you use or are using the default
:class:`~django.contrib.auth.middleware.SessionAuthenticationMiddleware`
and are using the default
:meth:`~django.contrib.auth.models.AbstractBaseUser.get_session_auth_hash()`. :meth:`~django.contrib.auth.models.AbstractBaseUser.get_session_auth_hash()`.
* All :doc:`messages </ref/contrib/messages>` if you are using * All :doc:`messages </ref/contrib/messages>` if you are using
:class:`~django.contrib.messages.storage.cookie.CookieStorage` or :class:`~django.contrib.messages.storage.cookie.CookieStorage` or

View File

@ -349,7 +349,9 @@ removed in Django 1.10 (please see the :ref:`deprecation timeline
* Session verification is enabled regardless of whether or not * Session verification is enabled regardless of whether or not
``'django.contrib.auth.middleware.SessionAuthenticationMiddleware'`` is in ``'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. * Private attribute ``django.db.models.Field.related`` is removed.

View File

@ -106,7 +106,7 @@ Bugfixes
(:ticket:`23950`). (:ticket:`23950`).
* Prevented the * 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`). setting a ``"Vary: Cookie"`` header on all responses (:ticket:`23939`).
* Fixed a crash when adding ``blank=True`` to ``TextField()`` on MySQL * Fixed a crash when adding ``blank=True`` to ``TextField()`` on MySQL

View File

@ -435,9 +435,8 @@ Minor features
method was added and if your :setting:`AUTH_USER_MODEL` inherits from method was added and if your :setting:`AUTH_USER_MODEL` inherits from
:class:`~django.contrib.auth.models.AbstractBaseUser`, changing a user's :class:`~django.contrib.auth.models.AbstractBaseUser`, changing a user's
password now invalidates old sessions if the password now invalidates old sessions if the
:class:`~django.contrib.auth.middleware.SessionAuthenticationMiddleware` is ``django.contrib.auth.middleware.SessionAuthenticationMiddleware`` is
enabled. See :ref:`session-invalidation-on-password-change` for more details enabled. See :ref:`session-invalidation-on-password-change` for more details.
including upgrade considerations when enabling this new middleware.
``django.contrib.formtools`` ``django.contrib.formtools``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@ -1455,7 +1454,7 @@ Miscellaneous
when the input is not valid UTF-8. when the input is not valid UTF-8.
* With the addition of the * 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 the default project template (pre-1.7.2 only), a database must be created
before accessing a page using :djadmin:`runserver`. before accessing a page using :djadmin:`runserver`.

View File

@ -1621,7 +1621,7 @@ attribute will change from ``True`` to ``False`` in Django 1.9.
Using ``AuthenticationMiddleware`` without ``SessionAuthenticationMiddleware`` 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 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 ``auth.get_user()`` and, for backwards compatibility, enabled only if
``'django.contrib.auth.middleware.SessionAuthenticationMiddleware'`` appears in ``'django.contrib.auth.middleware.SessionAuthenticationMiddleware'`` appears in

View File

@ -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 <built-in-auth-forms>` that may be used to allow users to change their own
passwords. passwords.
Changing a user's password will log out all their sessions if the Changing a user's password will log out all their sessions. See
:class:`~django.contrib.auth.middleware.SessionAuthenticationMiddleware` is :ref:`session-invalidation-on-password-change` for details.
enabled. See :ref:`session-invalidation-on-password-change` for details.
Authenticating Users Authenticating Users
-------------------- --------------------
@ -801,29 +800,23 @@ user to the login page or issue an HTTP 403 Forbidden response.
Session invalidation on password change Session invalidation on password change
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. warning:: .. versionchanged:: 1.10
This protection only applies if Session verification is enabled and mandatory in Django 1.10 (there's no
:class:`~django.contrib.auth.middleware.SessionAuthenticationMiddleware` way to disable it) regardless of whether or not
is enabled in :setting:`MIDDLEWARE_CLASSES`. It's included if ``SessionAuthenticationMiddleware`` is enabled. In older
``settings.py`` was generated by :djadmin:`startproject` on Django ≥ 1.7. versions, this protection only applies if
``django.contrib.auth.middleware.SessionAuthenticationMiddleware``
Session verification will become mandatory in Django 1.10 regardless of is enabled in :setting:`MIDDLEWARE_CLASSES`.
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.
If your :setting:`AUTH_USER_MODEL` inherits from If your :setting:`AUTH_USER_MODEL` inherits from
:class:`~django.contrib.auth.models.AbstractBaseUser` or implements its own :class:`~django.contrib.auth.models.AbstractBaseUser` or implements its own
:meth:`~django.contrib.auth.models.AbstractBaseUser.get_session_auth_hash()` :meth:`~django.contrib.auth.models.AbstractBaseUser.get_session_auth_hash()`
method, authenticated sessions will include the hash returned by this function. method, authenticated sessions will include the hash returned by this function.
In the :class:`~django.contrib.auth.models.AbstractBaseUser` case, this is an In the :class:`~django.contrib.auth.models.AbstractBaseUser` case, this is an
HMAC of the password field. If the HMAC of the password field. Django verifies that the hash sent along with each
:class:`~django.contrib.auth.middleware.SessionAuthenticationMiddleware` is request matches the one that's computed server-side. This allows a user to log
enabled, Django verifies that the hash sent along with each request matches out all of their sessions by changing their password.
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, The default password change views included with Django,
:func:`django.contrib.auth.views.password_change` and the :func:`django.contrib.auth.views.password_change` and the
@ -849,15 +842,6 @@ and wish to have similar behavior, use this function:
else: 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:: .. note::
Since Since

View File

@ -66,8 +66,6 @@ and these items in your :setting:`MIDDLEWARE_CLASSES` setting:
:doc:`sessions </topics/http/sessions>` across requests. :doc:`sessions </topics/http/sessions>` across requests.
2. :class:`~django.contrib.auth.middleware.AuthenticationMiddleware` associates 2. :class:`~django.contrib.auth.middleware.AuthenticationMiddleware` associates
users with requests using sessions. 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 With these settings in place, running the command ``manage.py migrate`` creates
the necessary database tables for auth related models and permissions for any the necessary database tables for auth related models and permissions for any

View File

@ -33,7 +33,6 @@ here's the default value created by :djadmin:`django-admin startproject
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', 'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
] ]

View File

@ -773,7 +773,7 @@ class AdminViewBasicTest(AdminViewBasicTestCase):
user = User.objects.get(username='super') user = User.objects.get(username='super')
user.set_unusable_password() user.set_unusable_password()
user.save() user.save()
self.client.force_login(user)
response = self.client.get(reverse('admin:index')) response = self.client.get(reverse('admin:index'))
self.assertNotContains(response, reverse('admin:password_change'), 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.') msg_prefix='The "change password" link should not be displayed if a user does not have a usable password.')

View File

@ -4,47 +4,26 @@ from django.http import HttpRequest
from django.test import TestCase from django.test import TestCase
class TestSessionAuthenticationMiddleware(TestCase): class TestAuthenticationMiddleware(TestCase):
def setUp(self): def setUp(self):
self.user_password = 'test_password' self.user = User.objects.create_user('test_user', 'test@example.com', 'test_password')
self.user = User.objects.create_user('test_user',
'test@example.com',
self.user_password)
self.middleware = AuthenticationMiddleware() self.middleware = AuthenticationMiddleware()
self.assertTrue(self.client.login( self.client.force_login(self.user)
username=self.user.username,
password=self.user_password,
))
self.request = HttpRequest() self.request = HttpRequest()
self.request.session = self.client.session self.request.session = self.client.session
def test_changed_password_doesnt_invalidate_session(self): def test_no_password_change_doesnt_invalidate_session(self):
""" self.request.session = self.client.session
Changing a user's password shouldn't invalidate the session if session
verification isn't activated.
"""
session_key = self.request.session.session_key
self.middleware.process_request(self.request) self.middleware.process_request(self.request)
self.assertIsNotNone(self.request.user) self.assertIsNotNone(self.request.user)
self.assertFalse(self.request.user.is_anonymous()) self.assertFalse(self.request.user.is_anonymous())
# After password change, user should remain logged in. def test_changed_password_invalidates_session(self):
# After password change, user should be anonymous
self.user.set_password('new_password') self.user.set_password('new_password')
self.user.save() self.user.save()
self.middleware.process_request(self.request) self.middleware.process_request(self.request)
self.assertIsNotNone(self.request.user) self.assertIsNotNone(self.request.user)
self.assertFalse(self.request.user.is_anonymous()) self.assertTrue(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']}):
# After password change, user should be anonymous
self.user.set_password('new_password')
self.user.save()
self.middleware.process_request(self.request)
self.assertIsNotNone(self.request.user)
self.assertTrue(self.request.user.is_anonymous())
# session should be flushed # session should be flushed
self.assertIsNone(self.request.session.session_key) self.assertIsNone(self.request.session.session_key)

View File

@ -24,7 +24,7 @@ from django.core.urlresolvers import NoReverseMatch, reverse, reverse_lazy
from django.db import connection from django.db import connection
from django.http import HttpRequest, QueryDict from django.http import HttpRequest, QueryDict
from django.middleware.csrf import CsrfViewMiddleware, get_token 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.test.utils import patch_logger
from django.utils.encoding import force_text from django.utils.encoding import force_text
from django.utils.http import urlquote from django.utils.http import urlquote
@ -506,9 +506,6 @@ class ChangePasswordTest(AuthViewsTestCase):
self.assertURLEqual(response.url, '/password_reset/') self.assertURLEqual(response.url, '/password_reset/')
@modify_settings(MIDDLEWARE_CLASSES={
'append': 'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
})
class SessionAuthenticationTests(AuthViewsTestCase): class SessionAuthenticationTests(AuthViewsTestCase):
def test_user_password_change_updates_session(self): 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 # Redirect in test_user_change_password will fail if session auth hash
# isn't updated after password change (#21649) # isn't updated after password change (#21649)
@modify_settings(MIDDLEWARE_CLASSES={
'append': 'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
})
@override_settings( @override_settings(
PASSWORD_HASHERS=['django.contrib.auth.hashers.SHA1PasswordHasher'], PASSWORD_HASHERS=['django.contrib.auth.hashers.SHA1PasswordHasher'],
ROOT_URLCONF='auth_tests.urls_admin', ROOT_URLCONF='auth_tests.urls_admin',

View File

@ -10,8 +10,10 @@ class CustomUserAdmin(UserAdmin):
def log_change(self, request, object, message): def log_change(self, request, object, message):
# LogEntry.user column doesn't get altered to expect a UUID, so set an # LogEntry.user column doesn't get altered to expect a UUID, so set an
# integer manually to avoid causing an error. # integer manually to avoid causing an error.
original_pk = request.user.pk
request.user.pk = 1 request.user.pk = 1
super(CustomUserAdmin, self).log_change(request, object, message) super(CustomUserAdmin, self).log_change(request, object, message)
request.user.pk = original_pk
site.register(get_user_model(), CustomUserAdmin) site.register(get_user_model(), CustomUserAdmin)

View File

@ -12,7 +12,6 @@ from django.test import (
override_settings, signals, override_settings, signals,
) )
from django.utils import six from django.utils import six
from django.utils.encoding import force_text
@modify_settings(ITEMS={ @modify_settings(ITEMS={
@ -489,47 +488,3 @@ class TestListSettings(unittest.TestCase):
finally: finally:
del sys.modules['fake_settings_module'] del sys.modules['fake_settings_module']
delattr(settings_module, setting) 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)