|
| 1 | +# Directly coming from Django source code |
| 2 | + |
| 3 | +from django.utils.crypto import constant_time_compare, salted_hmac |
| 4 | +from django.utils.http import base36_to_int, int_to_base36, urlsafe_base64_encode, urlsafe_base64_decode |
| 5 | + |
| 6 | +from datetime import date, datetime |
| 7 | + |
| 8 | +class PasswordResetTokenGenerator: |
| 9 | + """ |
| 10 | + Strategy object used to generate and check tokens for the password |
| 11 | + reset mechanism. |
| 12 | + """ |
| 13 | + key_salt = "django.contrib.auth.tokens.PasswordResetTokenGenerator" |
| 14 | + |
| 15 | + def __init__(self, SECRET_KEY): |
| 16 | + self.secret = SECRET_KEY |
| 17 | + |
| 18 | + def make_token(self, user): |
| 19 | + """ |
| 20 | + Return a token that can be used once to do a password reset |
| 21 | + for the given user. |
| 22 | + """ |
| 23 | + return self._make_token_with_timestamp(user, self._num_days(self._today())) |
| 24 | + |
| 25 | + def _make_token_with_timestamp(self, user, timestamp): |
| 26 | + # timestamp is number of days since 2001-1-1. Converted to |
| 27 | + # base 36, this gives us a 3 digit string until about 2121 |
| 28 | + ts_b36 = int_to_base36(timestamp) |
| 29 | + hash_string = salted_hmac( |
| 30 | + self.key_salt, |
| 31 | + self._make_hash_value(user, timestamp), |
| 32 | + secret=self.secret, |
| 33 | + ).hexdigest()[::2] # Limit to 20 characters to shorten the URL. |
| 34 | + return "%s-%s" % (ts_b36, hash_string) |
| 35 | + |
| 36 | + def _make_hash_value(self, user, timestamp): |
| 37 | + """ |
| 38 | + Hash the user's primary key and some user state that's sure to change |
| 39 | + after a password reset to produce a token that invalidated when it's |
| 40 | + used: |
| 41 | + 1. The password field will change upon a password reset (even if the |
| 42 | + same password is chosen, due to password salting). |
| 43 | + 2. The last_login field will usually be updated very shortly after |
| 44 | + a password reset. |
| 45 | + Failing those things, settings.PASSWORD_RESET_TIMEOUT_DAYS eventually |
| 46 | + invalidates the token. |
| 47 | + Running this data through salted_hmac() prevents password cracking |
| 48 | + attempts using the reset token, provided the secret isn't compromised. |
| 49 | + """ |
| 50 | + # Truncate microseconds so that tokens are consistent even if the |
| 51 | + # database doesn't support microseconds. |
| 52 | + login_timestamp = '' if user.last_login is None else user.last_login.replace(microsecond=0, tzinfo=None) |
| 53 | + return str(user.pk) + user.password + str(login_timestamp) + str(timestamp) |
| 54 | + |
| 55 | + def _num_days(self, dt): |
| 56 | + return (dt - date(2001, 1, 1)).days |
| 57 | + |
| 58 | + def _today(self): |
| 59 | + # Used for mocking in tests |
| 60 | + return date.today() |
0 commit comments