summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMariusz Felisiak <felisiak.mariusz@gmail.com>2020-10-22 13:21:14 +0200
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2020-10-22 13:22:00 +0200
commit767e06b5a830070939a6cd69c1ed1581446fb82b (patch)
treef268dffdba46bddb19c946ec0553baeb6d656239
parentc6b95be190e8270ff8936dbe28c5a2c02bb7b296 (diff)
[3.1.x] Fixed #32130 -- Fixed pre-Django 3.1 password reset tokens validation.
Thanks Gordon Wrigley for the report and implementation idea. Regression in 226ebb17290b604ef29e82fb5c1fbac3594ac163. Backport of 34180922380cf41cd684f846ecf00f92eb289bcf from master
-rw-r--r--django/contrib/auth/tokens.py12
-rw-r--r--docs/releases/3.1.3.txt3
-rw-r--r--tests/auth_tests/test_tokens.py23
3 files changed, 35 insertions, 3 deletions
diff --git a/django/contrib/auth/tokens.py b/django/contrib/auth/tokens.py
index 9bad0b4e42..838fd420b3 100644
--- a/django/contrib/auth/tokens.py
+++ b/django/contrib/auth/tokens.py
@@ -1,4 +1,4 @@
-from datetime import datetime
+from datetime import datetime, time
from django.conf import settings
from django.utils.crypto import constant_time_compare, salted_hmac
@@ -35,6 +35,8 @@ class PasswordResetTokenGenerator:
# Parse the token
try:
ts_b36, _ = token.split("-")
+ # RemovedInDjango40Warning.
+ legacy_token = len(ts_b36) < 4
except ValueError:
return False
@@ -54,8 +56,14 @@ class PasswordResetTokenGenerator:
):
return False
+ # RemovedInDjango40Warning: convert days to seconds and round to
+ # midnight (server time) for pre-Django 3.1 tokens.
+ now = self._now()
+ if legacy_token:
+ ts *= 24 * 60 * 60
+ ts += int((now - datetime.combine(now.date(), time.min)).total_seconds())
# Check the timestamp is within limit.
- if (self._num_seconds(self._now()) - ts) > settings.PASSWORD_RESET_TIMEOUT:
+ if (self._num_seconds(now) - ts) > settings.PASSWORD_RESET_TIMEOUT:
return False
return True
diff --git a/docs/releases/3.1.3.txt b/docs/releases/3.1.3.txt
index 9c58586f46..6f526aa5c9 100644
--- a/docs/releases/3.1.3.txt
+++ b/docs/releases/3.1.3.txt
@@ -48,3 +48,6 @@ Bugfixes
* Fixed a regression in Django 3.1.2 that caused incorrect form input layout on
small screens in the admin change form view (:ticket:`32069`).
+
+* Fixed a regression in Django 3.1 that invalidated pre-Django 3.1 password
+ reset tokens (:ticket:`32130`).
diff --git a/tests/auth_tests/test_tokens.py b/tests/auth_tests/test_tokens.py
index bba435be84..f350eddc43 100644
--- a/tests/auth_tests/test_tokens.py
+++ b/tests/auth_tests/test_tokens.py
@@ -1,4 +1,4 @@
-from datetime import datetime, timedelta
+from datetime import date, datetime, timedelta
from django.conf import settings
from django.contrib.auth.models import User
@@ -63,6 +63,27 @@ class TokenGeneratorTest(TestCase):
)
self.assertIs(p4.check_token(user, tk1), False)
+ def test_legacy_days_timeout(self):
+ # RemovedInDjango40Warning: pre-Django 3.1 tokens will be invalid.
+ class LegacyPasswordResetTokenGenerator(MockedPasswordResetTokenGenerator):
+ """Pre-Django 3.1 tokens generator."""
+ def _num_seconds(self, dt):
+ # Pre-Django 3.1 tokens use days instead of seconds.
+ return (dt.date() - date(2001, 1, 1)).days
+
+ user = User.objects.create_user('tokentestuser', 'test2@example.com', 'testpw')
+ now = datetime.now()
+ p0 = LegacyPasswordResetTokenGenerator(now)
+ tk1 = p0.make_token(user)
+ p1 = MockedPasswordResetTokenGenerator(
+ now + timedelta(seconds=settings.PASSWORD_RESET_TIMEOUT),
+ )
+ self.assertIs(p1.check_token(user, tk1), True)
+ p2 = MockedPasswordResetTokenGenerator(
+ now + timedelta(seconds=(settings.PASSWORD_RESET_TIMEOUT + 24 * 60 * 60)),
+ )
+ self.assertIs(p2.check_token(user, tk1), False)
+
def test_check_token_with_nonexistent_token_and_user(self):
user = User.objects.create_user('tokentestuser', 'test2@example.com', 'testpw')
p0 = PasswordResetTokenGenerator()