From 99abfe8f4d3caebcd73548f5bf9e4755bdfed318 Mon Sep 17 00:00:00 2001
From: Mariusz Felisiak <felisiak.mariusz@gmail.com>
Date: Fri, 7 Aug 2020 21:42:39 +0200
Subject: [PATCH] Fixed #31864 -- Fixed encoding session data during transition
 to Django 3.1.
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Thanks אורי for the report.
---
 django/contrib/sessions/backends/base.py |  9 +++++++++
 docs/releases/3.1.1.txt                  |  3 +++
 tests/sessions_tests/tests.py            | 19 +++++++++++++------
 3 files changed, 25 insertions(+), 6 deletions(-)

diff --git a/django/contrib/sessions/backends/base.py b/django/contrib/sessions/backends/base.py
index b5453160a5..187e14b1b7 100644
--- a/django/contrib/sessions/backends/base.py
+++ b/django/contrib/sessions/backends/base.py
@@ -108,6 +108,9 @@ class SessionBase:
 
     def encode(self, session_dict):
         "Return the given session dictionary serialized and encoded as a string."
+        # RemovedInDjango40Warning: DEFAULT_HASHING_ALGORITHM will be removed.
+        if settings.DEFAULT_HASHING_ALGORITHM == 'sha1':
+            return self._legacy_encode(session_dict)
         return signing.dumps(
             session_dict, salt=self.key_salt, serializer=self.serializer,
             compress=True,
@@ -121,6 +124,12 @@ class SessionBase:
         except Exception:
             return self._legacy_decode(session_data)
 
+    def _legacy_encode(self, session_dict):
+        # RemovedInDjango40Warning.
+        serialized = self.serializer().dumps(session_dict)
+        hash = self._hash(serialized)
+        return base64.b64encode(hash.encode() + b':' + serialized).decode('ascii')
+
     def _legacy_decode(self, session_data):
         # RemovedInDjango40Warning: pre-Django 3.1 format will be invalid.
         encoded_data = base64.b64decode(session_data.encode('ascii'))
diff --git a/docs/releases/3.1.1.txt b/docs/releases/3.1.1.txt
index 516d47cd03..c8201d4e41 100644
--- a/docs/releases/3.1.1.txt
+++ b/docs/releases/3.1.1.txt
@@ -14,3 +14,6 @@ Bugfixes
 
 * Fixed wrapping of long model names in the admin's navigation sidebar
   (:ticket:`31854`).
+
+* Fixed encoding session data while upgrading multiple instances of the same
+  project to Django 3.1 (:ticket:`31864`).
diff --git a/tests/sessions_tests/tests.py b/tests/sessions_tests/tests.py
index 248dae82aa..e7615d0f11 100644
--- a/tests/sessions_tests/tests.py
+++ b/tests/sessions_tests/tests.py
@@ -31,9 +31,11 @@ from django.core.cache.backends.base import InvalidCacheBackendError
 from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation
 from django.http import HttpResponse
 from django.test import (
-    RequestFactory, TestCase, ignore_warnings, override_settings,
+    RequestFactory, SimpleTestCase, TestCase, ignore_warnings,
+    override_settings,
 )
 from django.utils import timezone
+from django.utils.deprecation import RemovedInDjango40Warning
 
 from .models import SessionStore as CustomDatabaseSession
 
@@ -323,6 +325,13 @@ class SessionTestsMixin:
             {'a test key': 'a test value'},
         )
 
+    @ignore_warnings(category=RemovedInDjango40Warning)
+    def test_default_hashing_algorith_legacy_decode(self):
+        with self.settings(DEFAULT_HASHING_ALGORITHM='sha1'):
+            data = {'a test key': 'a test value'}
+            encoded = self.session.encode(data)
+            self.assertEqual(self.session._legacy_decode(encoded), data)
+
     def test_decode_failure_logged_to_security(self):
         bad_encode = base64.b64encode(b'flaskdj:alkdjf').decode('ascii')
         with self.assertLogs('django.security.SuspiciousSession', 'WARNING') as cm:
@@ -526,8 +535,7 @@ class CacheDBSessionWithTimeZoneTests(CacheDBSessionTests):
     pass
 
 
-# Don't need DB flushing for these tests, so can use unittest.TestCase as base class
-class FileSessionTests(SessionTestsMixin, unittest.TestCase):
+class FileSessionTests(SessionTestsMixin, SimpleTestCase):
 
     backend = FileSession
 
@@ -620,7 +628,7 @@ class FileSessionPathLibTests(FileSessionTests):
         return Path(tmp_dir)
 
 
-class CacheSessionTests(SessionTestsMixin, unittest.TestCase):
+class CacheSessionTests(SessionTestsMixin, SimpleTestCase):
 
     backend = CacheSession
 
@@ -854,8 +862,7 @@ class SessionMiddlewareTests(TestCase):
         self.assertEqual(response['Vary'], 'Cookie')
 
 
-# Don't need DB flushing for these tests, so can use unittest.TestCase as base class
-class CookieSessionTests(SessionTestsMixin, unittest.TestCase):
+class CookieSessionTests(SessionTestsMixin, SimpleTestCase):
 
     backend = CookieSession