diff options
| author | Mariusz Felisiak <felisiak.mariusz@gmail.com> | 2020-10-22 13:21:14 +0200 |
|---|---|---|
| committer | Mariusz Felisiak <felisiak.mariusz@gmail.com> | 2020-10-22 13:22:00 +0200 |
| commit | 767e06b5a830070939a6cd69c1ed1581446fb82b (patch) | |
| tree | f268dffdba46bddb19c946ec0553baeb6d656239 | |
| parent | c6b95be190e8270ff8936dbe28c5a2c02bb7b296 (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.py | 12 | ||||
| -rw-r--r-- | docs/releases/3.1.3.txt | 3 | ||||
| -rw-r--r-- | tests/auth_tests/test_tokens.py | 23 |
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() |
