123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132 |
- from datetime import datetime
-
- 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
- _secret_fallbacks = None
-
- def __init__(self):
- self.algorithm = self.algorithm or "sha256"
-
- def _get_secret(self):
- return self._secret or settings.SECRET_KEY
-
- def _set_secret(self, secret):
- self._secret = secret
-
- secret = property(_get_secret, _set_secret)
-
- def _get_fallbacks(self):
- if self._secret_fallbacks is None:
- return settings.SECRET_KEY_FALLBACKS
- return self._secret_fallbacks
-
- def _set_fallbacks(self, fallbacks):
- self._secret_fallbacks = fallbacks
-
- secret_fallbacks = property(_get_fallbacks, _set_fallbacks)
-
- 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()),
- self.secret,
- )
-
- 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("-")
- 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
- for secret in [self.secret, *self.secret_fallbacks]:
- if constant_time_compare(
- self._make_token_with_timestamp(user, ts, secret),
- token,
- ):
- break
- else:
- return False
-
- # Check the timestamp is within limit.
- if (self._num_seconds(self._now()) - ts) > settings.PASSWORD_RESET_TIMEOUT:
- return False
-
- return True
-
- def _make_token_with_timestamp(self, user, timestamp, secret):
- # 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=secret,
- algorithm=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()
|