mirror of
https://github.com/django/django.git
synced 2024-12-01 15:42:04 +01:00
Fixed #34384 -- Fixed session validation when rotation secret keys.
Bug in 0dcd549bbe
.
Thanks Eric Zarowny for the report.
This commit is contained in:
parent
9b22457987
commit
2396933ca9
@ -199,12 +199,26 @@ def get_user(request):
|
|||||||
# Verify the session
|
# Verify the session
|
||||||
if hasattr(user, "get_session_auth_hash"):
|
if 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(
|
if not session_hash:
|
||||||
session_hash, user.get_session_auth_hash()
|
session_hash_verified = False
|
||||||
)
|
else:
|
||||||
|
session_auth_hash = user.get_session_auth_hash()
|
||||||
|
session_hash_verified = constant_time_compare(
|
||||||
|
session_hash, session_auth_hash
|
||||||
|
)
|
||||||
if not session_hash_verified:
|
if not session_hash_verified:
|
||||||
request.session.flush()
|
# If the current secret does not verify the session, try
|
||||||
user = None
|
# with the fallback secrets and stop when a matching one is
|
||||||
|
# found.
|
||||||
|
if session_hash and any(
|
||||||
|
constant_time_compare(session_hash, fallback_auth_hash)
|
||||||
|
for fallback_auth_hash in user.get_session_auth_fallback_hash()
|
||||||
|
):
|
||||||
|
request.session.cycle_key()
|
||||||
|
request.session[HASH_SESSION_KEY] = session_auth_hash
|
||||||
|
else:
|
||||||
|
request.session.flush()
|
||||||
|
user = None
|
||||||
|
|
||||||
return user or AnonymousUser()
|
return user or AnonymousUser()
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ not in INSTALLED_APPS.
|
|||||||
import unicodedata
|
import unicodedata
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.contrib.auth import password_validation
|
from django.contrib.auth import password_validation
|
||||||
from django.contrib.auth.hashers import (
|
from django.contrib.auth.hashers import (
|
||||||
check_password,
|
check_password,
|
||||||
@ -135,10 +136,18 @@ class AbstractBaseUser(models.Model):
|
|||||||
"""
|
"""
|
||||||
Return an HMAC of the password field.
|
Return an HMAC of the password field.
|
||||||
"""
|
"""
|
||||||
|
return self._get_session_auth_hash()
|
||||||
|
|
||||||
|
def get_session_auth_fallback_hash(self):
|
||||||
|
for fallback_secret in settings.SECRET_KEY_FALLBACKS:
|
||||||
|
yield self._get_session_auth_hash(secret=fallback_secret)
|
||||||
|
|
||||||
|
def _get_session_auth_hash(self, secret=None):
|
||||||
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(
|
return salted_hmac(
|
||||||
key_salt,
|
key_salt,
|
||||||
self.password,
|
self.password,
|
||||||
|
secret=secret,
|
||||||
algorithm="sha256",
|
algorithm="sha256",
|
||||||
).hexdigest()
|
).hexdigest()
|
||||||
|
|
||||||
|
@ -695,10 +695,17 @@ Utility functions
|
|||||||
``get_user()`` method to retrieve the user model instance and then verifies
|
``get_user()`` method to retrieve the user model instance and then verifies
|
||||||
the session by calling the user model's
|
the session by calling the user model's
|
||||||
:meth:`~django.contrib.auth.models.AbstractBaseUser.get_session_auth_hash`
|
:meth:`~django.contrib.auth.models.AbstractBaseUser.get_session_auth_hash`
|
||||||
method.
|
method. If the verification fails and :setting:`SECRET_KEY_FALLBACKS` are
|
||||||
|
provided, it verifies the session against each fallback key using
|
||||||
|
:meth:`~django.contrib.auth.models.AbstractBaseUser.\
|
||||||
|
get_session_auth_fallback_hash`.
|
||||||
|
|
||||||
Returns an instance of :class:`~django.contrib.auth.models.AnonymousUser`
|
Returns an instance of :class:`~django.contrib.auth.models.AnonymousUser`
|
||||||
if the authentication backend stored in the session is no longer in
|
if the authentication backend stored in the session is no longer in
|
||||||
:setting:`AUTHENTICATION_BACKENDS`, if a user isn't returned by the
|
:setting:`AUTHENTICATION_BACKENDS`, if a user isn't returned by the
|
||||||
backend's ``get_user()`` method, or if the session auth hash doesn't
|
backend's ``get_user()`` method, or if the session auth hash doesn't
|
||||||
validate.
|
validate.
|
||||||
|
|
||||||
|
.. versionchanged:: 4.1.8
|
||||||
|
|
||||||
|
Fallback verification with :setting:`SECRET_KEY_FALLBACKS` was added.
|
||||||
|
@ -9,4 +9,5 @@ Django 4.1.8 fixes several bugs in 4.1.7.
|
|||||||
Bugfixes
|
Bugfixes
|
||||||
========
|
========
|
||||||
|
|
||||||
* ...
|
* Fixed a bug in Django 4.1 that caused invalidation of sessions when rotating
|
||||||
|
secret keys with ``SECRET_KEY_FALLBACKS`` (:ticket:`34384`).
|
||||||
|
@ -722,6 +722,13 @@ 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`.
|
||||||
|
|
||||||
|
.. method:: models.AbstractBaseUser.get_session_auth_fallback_hash()
|
||||||
|
|
||||||
|
.. versionadded:: 4.1.8
|
||||||
|
|
||||||
|
Yields the HMAC of the password field using
|
||||||
|
:setting:`SECRET_KEY_FALLBACKS`. Used by ``get_user()``.
|
||||||
|
|
||||||
: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.conf import settings
|
||||||
from django.contrib.auth import get_user, get_user_model
|
from django.contrib.auth import get_user, get_user_model
|
||||||
from django.contrib.auth.models import AnonymousUser, User
|
from django.contrib.auth.models import AnonymousUser, User
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
@ -138,3 +139,26 @@ class TestGetUser(TestCase):
|
|||||||
user = get_user(request)
|
user = get_user(request)
|
||||||
self.assertIsInstance(user, User)
|
self.assertIsInstance(user, User)
|
||||||
self.assertEqual(user.username, created_user.username)
|
self.assertEqual(user.username, created_user.username)
|
||||||
|
|
||||||
|
def test_get_user_fallback_secret(self):
|
||||||
|
created_user = User.objects.create_user(
|
||||||
|
"testuser", "test@example.com", "testpw"
|
||||||
|
)
|
||||||
|
self.client.login(username="testuser", password="testpw")
|
||||||
|
request = HttpRequest()
|
||||||
|
request.session = self.client.session
|
||||||
|
prev_session_key = request.session.session_key
|
||||||
|
with override_settings(
|
||||||
|
SECRET_KEY="newsecret",
|
||||||
|
SECRET_KEY_FALLBACKS=[settings.SECRET_KEY],
|
||||||
|
):
|
||||||
|
user = get_user(request)
|
||||||
|
self.assertIsInstance(user, User)
|
||||||
|
self.assertEqual(user.username, created_user.username)
|
||||||
|
self.assertNotEqual(request.session.session_key, prev_session_key)
|
||||||
|
# Remove the fallback secret.
|
||||||
|
# The session hash should be updated using the current secret.
|
||||||
|
with override_settings(SECRET_KEY="newsecret"):
|
||||||
|
user = get_user(request)
|
||||||
|
self.assertIsInstance(user, User)
|
||||||
|
self.assertEqual(user.username, created_user.username)
|
||||||
|
Loading…
Reference in New Issue
Block a user