mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	Fixed #26615 -- Made password reset token invalidate when changing email.
Co-Authored-By: Silas Barta <sbarta@gmail.com>
This commit is contained in:
		
				
					committed by
					
						 Mariusz Felisiak
						Mariusz Felisiak
					
				
			
			
				
	
			
			
			
						parent
						
							7f9e4524d6
						
					
				
				
					commit
					0362b0e986
				
			| @@ -78,9 +78,9 @@ class PasswordResetTokenGenerator: | ||||
|  | ||||
|     def _make_hash_value(self, user, timestamp): | ||||
|         """ | ||||
|         Hash the user's primary key and some user state that's sure to change | ||||
|         after a password reset to produce a token that invalidated when it's | ||||
|         used: | ||||
|         Hash the user's primary key, email (if available), and some user state | ||||
|         that's sure to change after a password reset to produce a token that is | ||||
|         invalidated when it's used: | ||||
|         1. The password field will change upon a password reset (even if the | ||||
|            same password is chosen, due to password salting). | ||||
|         2. The last_login field will usually be updated very shortly after | ||||
| @@ -94,7 +94,9 @@ class PasswordResetTokenGenerator: | ||||
|         # Truncate microseconds so that tokens are consistent even if the | ||||
|         # database doesn't support microseconds. | ||||
|         login_timestamp = '' if user.last_login is None else user.last_login.replace(microsecond=0, tzinfo=None) | ||||
|         return str(user.pk) + user.password + str(login_timestamp) + str(timestamp) | ||||
|         email_field = user.get_email_field_name() | ||||
|         email = getattr(user, email_field, '') or '' | ||||
|         return f'{user.pk}{user.password}{login_timestamp}{timestamp}{email}' | ||||
|  | ||||
|     def _num_seconds(self, dt): | ||||
|         return int((dt - datetime(2001, 1, 1)).total_seconds()) | ||||
|   | ||||
| @@ -552,6 +552,9 @@ Miscellaneous | ||||
|   ``False`` if the file cannot be locked, instead of raising | ||||
|   :exc:`BlockingIOError`. | ||||
|  | ||||
| * The password reset mechanism now invalidates tokens when the user email is | ||||
|   changed. | ||||
|  | ||||
| .. _deprecated-features-3.2: | ||||
|  | ||||
| Features deprecated in 3.2 | ||||
|   | ||||
| @@ -8,6 +8,7 @@ from .minimal import MinimalUser | ||||
| from .no_password import NoPasswordUser | ||||
| from .proxy import Proxy, UserProxy | ||||
| from .uuid_pk import UUIDUser | ||||
| from .with_custom_email_field import CustomEmailField | ||||
| from .with_foreign_key import CustomUserWithFK, Email | ||||
| from .with_integer_username import IntegerUsernameUser | ||||
| from .with_last_login_attr import UserWithDisabledLastLoginField | ||||
| @@ -16,10 +17,10 @@ from .with_many_to_many import ( | ||||
| ) | ||||
|  | ||||
| __all__ = ( | ||||
|     'CustomPermissionsUser', 'CustomUser', 'CustomUserNonUniqueUsername', | ||||
|     'CustomUserWithFK', 'CustomUserWithM2M', 'CustomUserWithM2MThrough', | ||||
|     'CustomUserWithoutIsActiveField', 'Email', 'ExtensionUser', | ||||
|     'IntegerUsernameUser', 'IsActiveTestUser1', 'MinimalUser', | ||||
|     'CustomEmailField', 'CustomPermissionsUser', 'CustomUser', | ||||
|     'CustomUserNonUniqueUsername', 'CustomUserWithFK', 'CustomUserWithM2M', | ||||
|     'CustomUserWithM2MThrough', 'CustomUserWithoutIsActiveField', 'Email', | ||||
|     'ExtensionUser', 'IntegerUsernameUser', 'IsActiveTestUser1', 'MinimalUser', | ||||
|     'NoPasswordUser', 'Organization', 'Proxy', 'UUIDUser', 'UserProxy', | ||||
|     'UserWithDisabledLastLoginField', | ||||
| ) | ||||
|   | ||||
| @@ -15,7 +15,7 @@ class CustomEmailFieldUserManager(BaseUserManager): | ||||
| class CustomEmailField(AbstractBaseUser): | ||||
|     username = models.CharField(max_length=255) | ||||
|     password = models.CharField(max_length=255) | ||||
|     email_address = models.EmailField() | ||||
|     email_address = models.EmailField(null=True) | ||||
|     is_active = models.BooleanField(default=True) | ||||
|  | ||||
|     EMAIL_FIELD = 'email_address' | ||||
|   | ||||
| @@ -17,8 +17,7 @@ from django.test import ( | ||||
|     SimpleTestCase, TestCase, TransactionTestCase, override_settings, | ||||
| ) | ||||
|  | ||||
| from .models import IntegerUsernameUser | ||||
| from .models.with_custom_email_field import CustomEmailField | ||||
| from .models import CustomEmailField, IntegerUsernameUser | ||||
|  | ||||
|  | ||||
| class NaturalKeysTestCase(TestCase): | ||||
|   | ||||
| @@ -7,6 +7,8 @@ from django.test import TestCase | ||||
| from django.test.utils import ignore_warnings | ||||
| from django.utils.deprecation import RemovedInDjango40Warning | ||||
|  | ||||
| from .models import CustomEmailField | ||||
|  | ||||
|  | ||||
| class MockedPasswordResetTokenGenerator(PasswordResetTokenGenerator): | ||||
|     def __init__(self, now): | ||||
| @@ -37,6 +39,27 @@ class TokenGeneratorTest(TestCase): | ||||
|         tk2 = p0.make_token(user_reload) | ||||
|         self.assertEqual(tk1, tk2) | ||||
|  | ||||
|     def test_token_with_different_email(self): | ||||
|         """Updating the user email address invalidates the token.""" | ||||
|         tests = [ | ||||
|             (CustomEmailField, None), | ||||
|             (CustomEmailField, 'test4@example.com'), | ||||
|             (User, 'test4@example.com'), | ||||
|         ] | ||||
|         for model, email in tests: | ||||
|             with self.subTest(model=model.__qualname__, email=email): | ||||
|                 user = model.objects.create_user( | ||||
|                     'changeemailuser', | ||||
|                     email=email, | ||||
|                     password='testpw', | ||||
|                 ) | ||||
|                 p0 = PasswordResetTokenGenerator() | ||||
|                 tk1 = p0.make_token(user) | ||||
|                 self.assertIs(p0.check_token(user, tk1), True) | ||||
|                 setattr(user, user.get_email_field_name(), 'test4new@example.com') | ||||
|                 user.save() | ||||
|                 self.assertIs(p0.check_token(user, tk1), False) | ||||
|  | ||||
|     def test_timeout(self): | ||||
|         """The token is valid after n seconds, but no greater.""" | ||||
|         # Uses a mocked version of PasswordResetTokenGenerator so we can change | ||||
|   | ||||
		Reference in New Issue
	
	Block a user