Refs #27468 -- Made user sessions use SHA-256 algorithm.
This commit is contained in:
parent
5869afe32b
commit
54646a423b
|
@ -187,8 +187,13 @@ def get_user(request):
|
||||||
user.get_session_auth_hash()
|
user.get_session_auth_hash()
|
||||||
)
|
)
|
||||||
if not session_hash_verified:
|
if not session_hash_verified:
|
||||||
request.session.flush()
|
if not (
|
||||||
user = None
|
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()
|
return user or AnonymousUser()
|
||||||
|
|
||||||
|
|
|
@ -120,12 +120,17 @@ class AbstractBaseUser(models.Model):
|
||||||
"""
|
"""
|
||||||
return is_password_usable(self.password)
|
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):
|
def get_session_auth_hash(self):
|
||||||
"""
|
"""
|
||||||
Return an HMAC of the password field.
|
Return an HMAC of the password field.
|
||||||
"""
|
"""
|
||||||
key_salt = "django.contrib.auth.models.AbstractBaseUser.get_session_auth_hash"
|
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
|
@classmethod
|
||||||
def get_email_field_name(cls):
|
def get_email_field_name(cls):
|
||||||
|
|
|
@ -57,6 +57,9 @@ details on these changes.
|
||||||
* Support for the pre-Django 3.1 ``django.core.signing.Signer`` signatures
|
* Support for the pre-Django 3.1 ``django.core.signing.Signer`` signatures
|
||||||
(encoded with the SHA-1 algorithm) will be removed.
|
(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
|
* The ``get_request`` argument for
|
||||||
``django.utils.deprecation.MiddlewareMixin.__init__()`` will be required and
|
``django.utils.deprecation.MiddlewareMixin.__init__()`` will be required and
|
||||||
won't accept ``None``.
|
won't accept ``None``.
|
||||||
|
|
|
@ -98,6 +98,10 @@ Minor features
|
||||||
* The password reset mechanism now uses the SHA-256 hashing algorithm. Support
|
* 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.
|
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`
|
:mod:`django.contrib.contenttypes`
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
@ -713,6 +713,10 @@ The following attributes and methods are available on any subclass of
|
||||||
Returns an HMAC of the password field. Used for
|
Returns an HMAC of the password field. Used for
|
||||||
:ref:`session-invalidation-on-password-change`.
|
: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` subclasses :class:`~models.AbstractBaseUser`:
|
||||||
|
|
||||||
.. class:: models.AbstractUser
|
.. class:: models.AbstractUser
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
from django.contrib.auth import HASH_SESSION_KEY
|
||||||
from django.contrib.auth.middleware import AuthenticationMiddleware
|
from django.contrib.auth.middleware import AuthenticationMiddleware
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
|
@ -18,6 +19,16 @@ class TestAuthenticationMiddleware(TestCase):
|
||||||
self.assertIsNotNone(self.request.user)
|
self.assertIsNotNone(self.request.user)
|
||||||
self.assertFalse(self.request.user.is_anonymous)
|
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):
|
def test_changed_password_invalidates_session(self):
|
||||||
# After password change, user should be anonymous
|
# After password change, user should be anonymous
|
||||||
self.user.set_password('new_password')
|
self.user.set_password('new_password')
|
||||||
|
|
|
@ -10,7 +10,7 @@ from django.apps import apps
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.admin.models import LogEntry
|
from django.contrib.admin.models import LogEntry
|
||||||
from django.contrib.auth import (
|
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 (
|
from django.contrib.auth.forms import (
|
||||||
AuthenticationForm, PasswordChangeForm, SetPasswordForm,
|
AuthenticationForm, PasswordChangeForm, SetPasswordForm,
|
||||||
|
@ -711,6 +711,27 @@ class LoginTest(AuthViewsTestCase):
|
||||||
self.login(password='foobar')
|
self.login(password='foobar')
|
||||||
self.assertNotEqual(original_session_key, self.client.session.session_key)
|
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):
|
def test_login_session_without_hash_session_key(self):
|
||||||
"""
|
"""
|
||||||
Session without django.contrib.auth.HASH_SESSION_KEY should login
|
Session without django.contrib.auth.HASH_SESSION_KEY should login
|
||||||
|
|
Loading…
Reference in New Issue