summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMariusz Felisiak <felisiak.mariusz@gmail.com>2020-04-29 16:45:00 +0200
committerGitHub <noreply@github.com>2020-04-29 16:45:00 +0200
commit54646a423b4501aeb80bbdd9238f20500c84cd5f (patch)
treee4c55114664eb0a38fcd51ab14c116885ada86b2
parent5869afe32b9c252cacd327f18c58e38c36d1f530 (diff)
Refs #27468 -- Made user sessions use SHA-256 algorithm.
-rw-r--r--django/contrib/auth/__init__.py9
-rw-r--r--django/contrib/auth/base_user.py7
-rw-r--r--docs/internals/deprecation.txt3
-rw-r--r--docs/releases/3.1.txt4
-rw-r--r--docs/topics/auth/customizing.txt4
-rw-r--r--tests/auth_tests/test_middleware.py11
-rw-r--r--tests/auth_tests/test_views.py23
7 files changed, 57 insertions, 4 deletions
diff --git a/django/contrib/auth/__init__.py b/django/contrib/auth/__init__.py
index 09db690b5c..de2aa785e6 100644
--- a/django/contrib/auth/__init__.py
+++ b/django/contrib/auth/__init__.py
@@ -187,8 +187,13 @@ def get_user(request):
user.get_session_auth_hash()
)
if not session_hash_verified:
- request.session.flush()
- user = None
+ if not (
+ session_hash and
+ hasattr(user, '_legacy_get_session_auth_hash') and
+ constant_time_compare(session_hash, user._legacy_get_session_auth_hash())
+ ):
+ request.session.flush()
+ user = None
return user or AnonymousUser()
diff --git a/django/contrib/auth/base_user.py b/django/contrib/auth/base_user.py
index f39c12a350..bb51cfbcc9 100644
--- a/django/contrib/auth/base_user.py
+++ b/django/contrib/auth/base_user.py
@@ -120,12 +120,17 @@ class AbstractBaseUser(models.Model):
"""
return is_password_usable(self.password)
+ def _legacy_get_session_auth_hash(self):
+ # RemovedInDjango40Warning: pre-Django 3.1 hashes will be invalid.
+ key_salt = 'django.contrib.auth.models.AbstractBaseUser.get_session_auth_hash'
+ return salted_hmac(key_salt, self.password, algorithm='sha1').hexdigest()
+
def get_session_auth_hash(self):
"""
Return an HMAC of the password field.
"""
key_salt = "django.contrib.auth.models.AbstractBaseUser.get_session_auth_hash"
- return salted_hmac(key_salt, self.password).hexdigest()
+ return salted_hmac(key_salt, self.password, algorithm='sha256').hexdigest()
@classmethod
def get_email_field_name(cls):
diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt
index 8d3cc62d90..95b0c5a3a1 100644
--- a/docs/internals/deprecation.txt
+++ b/docs/internals/deprecation.txt
@@ -57,6 +57,9 @@ details on these changes.
* Support for the pre-Django 3.1 ``django.core.signing.Signer`` signatures
(encoded with the SHA-1 algorithm) will be removed.
+* Support for the pre-Django 3.1 user sessions (that use the SHA-1 algorithm)
+ will be removed.
+
* The ``get_request`` argument for
``django.utils.deprecation.MiddlewareMixin.__init__()`` will be required and
won't accept ``None``.
diff --git a/docs/releases/3.1.txt b/docs/releases/3.1.txt
index 16cb9c4e6e..1de4f24684 100644
--- a/docs/releases/3.1.txt
+++ b/docs/releases/3.1.txt
@@ -98,6 +98,10 @@ Minor features
* The password reset mechanism now uses the SHA-256 hashing algorithm. Support
for tokens that use the old hashing algorithm remains until Django 4.0.
+* :meth:`.AbstractBaseUser.get_session_auth_hash` now uses the SHA-256 hashing
+ algorithm. Support for user sessions that use the old hashing algorithm
+ remains until Django 4.0.
+
:mod:`django.contrib.contenttypes`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/docs/topics/auth/customizing.txt b/docs/topics/auth/customizing.txt
index c8a9a39158..6b816c42fd 100644
--- a/docs/topics/auth/customizing.txt
+++ b/docs/topics/auth/customizing.txt
@@ -713,6 +713,10 @@ The following attributes and methods are available on any subclass of
Returns an HMAC of the password field. Used for
:ref:`session-invalidation-on-password-change`.
+ .. versionchanged:: 3.1
+
+ The hashing algorithm was changed to the SHA-256.
+
:class:`~models.AbstractUser` subclasses :class:`~models.AbstractBaseUser`:
.. class:: models.AbstractUser
diff --git a/tests/auth_tests/test_middleware.py b/tests/auth_tests/test_middleware.py
index 3c31475d27..5538225acb 100644
--- a/tests/auth_tests/test_middleware.py
+++ b/tests/auth_tests/test_middleware.py
@@ -1,3 +1,4 @@
+from django.contrib.auth import HASH_SESSION_KEY
from django.contrib.auth.middleware import AuthenticationMiddleware
from django.contrib.auth.models import User
from django.http import HttpRequest, HttpResponse
@@ -18,6 +19,16 @@ class TestAuthenticationMiddleware(TestCase):
self.assertIsNotNone(self.request.user)
self.assertFalse(self.request.user.is_anonymous)
+ def test_no_password_change_does_not_invalidate_legacy_session(self):
+ # RemovedInDjango40Warning: pre-Django 3.1 hashes will be invalid.
+ session = self.client.session
+ session[HASH_SESSION_KEY] = self.user._legacy_get_session_auth_hash()
+ session.save()
+ self.request.session = session
+ self.middleware(self.request)
+ self.assertIsNotNone(self.request.user)
+ self.assertFalse(self.request.user.is_anonymous)
+
def test_changed_password_invalidates_session(self):
# After password change, user should be anonymous
self.user.set_password('new_password')
diff --git a/tests/auth_tests/test_views.py b/tests/auth_tests/test_views.py
index f33cbc8382..48278e23f9 100644
--- a/tests/auth_tests/test_views.py
+++ b/tests/auth_tests/test_views.py
@@ -10,7 +10,7 @@ from django.apps import apps
from django.conf import settings
from django.contrib.admin.models import LogEntry
from django.contrib.auth import (
- BACKEND_SESSION_KEY, REDIRECT_FIELD_NAME, SESSION_KEY,
+ BACKEND_SESSION_KEY, HASH_SESSION_KEY, REDIRECT_FIELD_NAME, SESSION_KEY,
)
from django.contrib.auth.forms import (
AuthenticationForm, PasswordChangeForm, SetPasswordForm,
@@ -711,6 +711,27 @@ class LoginTest(AuthViewsTestCase):
self.login(password='foobar')
self.assertNotEqual(original_session_key, self.client.session.session_key)
+ def test_legacy_session_key_flushed_on_login(self):
+ # RemovedInDjango40Warning.
+ user = User.objects.get(username='testclient')
+ engine = import_module(settings.SESSION_ENGINE)
+ session = engine.SessionStore()
+ session[SESSION_KEY] = user.id
+ session[HASH_SESSION_KEY] = user._legacy_get_session_auth_hash()
+ session.save()
+ original_session_key = session.session_key
+ self.client.cookies[settings.SESSION_COOKIE_NAME] = original_session_key
+ # Legacy session key is flushed on login.
+ self.login()
+ self.assertNotEqual(original_session_key, self.client.session.session_key)
+ # Legacy session key is flushed after a password change.
+ user.set_password('password_2')
+ user.save()
+ original_session_key = session.session_key
+ self.client.cookies[settings.SESSION_COOKIE_NAME] = original_session_key
+ self.login(password='password_2')
+ self.assertNotEqual(original_session_key, self.client.session.session_key)
+
def test_login_session_without_hash_session_key(self):
"""
Session without django.contrib.auth.HASH_SESSION_KEY should login