| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117 |
- from datetime import datetime, time
- from django.conf import settings
- from django.utils.crypto import constant_time_compare, salted_hmac
- from django.utils.http import base36_to_int, int_to_base36
- class PasswordResetTokenGenerator:
- """
- Strategy object used to generate and check tokens for the password
- reset mechanism.
- """
- key_salt = "django.contrib.auth.tokens.PasswordResetTokenGenerator"
- algorithm = None
- secret = None
- def __init__(self):
- self.secret = self.secret or settings.SECRET_KEY
- # RemovedInDjango40Warning: when the deprecation ends, replace with:
- # self.algorithm = self.algorithm or 'sha256'
- self.algorithm = self.algorithm or settings.DEFAULT_HASHING_ALGORITHM
- def make_token(self, user):
- """
- Return a token that can be used once to do a password reset
- for the given user.
- """
- return self._make_token_with_timestamp(user, self._num_seconds(self._now()))
- def check_token(self, user, token):
- """
- Check that a password reset token is correct for a given user.
- """
- if not (user and token):
- return False
- # Parse the token
- try:
- ts_b36, _ = token.split("-")
- # RemovedInDjango40Warning.
- legacy_token = len(ts_b36) < 4
- except ValueError:
- return False
- try:
- ts = base36_to_int(ts_b36)
- except ValueError:
- return False
- # Check that the timestamp/uid has not been tampered with
- if not constant_time_compare(self._make_token_with_timestamp(user, ts), token):
- # RemovedInDjango40Warning: when the deprecation ends, replace
- # with:
- # return False
- if not constant_time_compare(
- self._make_token_with_timestamp(user, ts, legacy=True),
- token,
- ):
- 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(now) - ts) > settings.PASSWORD_RESET_TIMEOUT:
- return False
- return True
- def _make_token_with_timestamp(self, user, timestamp, legacy=False):
- # timestamp is number of seconds since 2001-1-1. Converted to base 36,
- # this gives us a 6 digit string until about 2069.
- ts_b36 = int_to_base36(timestamp)
- hash_string = salted_hmac(
- self.key_salt,
- self._make_hash_value(user, timestamp),
- secret=self.secret,
- # RemovedInDjango40Warning: when the deprecation ends, remove the
- # legacy argument and replace with:
- # algorithm=self.algorithm,
- algorithm='sha1' if legacy else self.algorithm,
- ).hexdigest()[::2] # Limit to shorten the URL.
- return "%s-%s" % (ts_b36, hash_string)
- def _make_hash_value(self, user, timestamp):
- """
- 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
- a password reset.
- Failing those things, settings.PASSWORD_RESET_TIMEOUT eventually
- invalidates the token.
- Running this data through salted_hmac() prevents password cracking
- attempts using the reset token, provided the secret isn't compromised.
- """
- # 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)
- 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())
- def _now(self):
- # Used for mocking in tests
- return datetime.now()
- default_token_generator = PasswordResetTokenGenerator()
|