tokens.py 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. from datetime import datetime, time
  2. from django.conf import settings
  3. from django.utils.crypto import constant_time_compare, salted_hmac
  4. from django.utils.http import base36_to_int, int_to_base36
  5. class PasswordResetTokenGenerator:
  6. """
  7. Strategy object used to generate and check tokens for the password
  8. reset mechanism.
  9. """
  10. key_salt = "django.contrib.auth.tokens.PasswordResetTokenGenerator"
  11. algorithm = None
  12. secret = None
  13. def __init__(self):
  14. self.secret = self.secret or settings.SECRET_KEY
  15. # RemovedInDjango40Warning: when the deprecation ends, replace with:
  16. # self.algorithm = self.algorithm or 'sha256'
  17. self.algorithm = self.algorithm or settings.DEFAULT_HASHING_ALGORITHM
  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_seconds(self._now()))
  24. def check_token(self, user, token):
  25. """
  26. Check that a password reset token is correct for a given user.
  27. """
  28. if not (user and token):
  29. return False
  30. # Parse the token
  31. try:
  32. ts_b36, _ = token.split("-")
  33. # RemovedInDjango40Warning.
  34. legacy_token = len(ts_b36) < 4
  35. except ValueError:
  36. return False
  37. try:
  38. ts = base36_to_int(ts_b36)
  39. except ValueError:
  40. return False
  41. # Check that the timestamp/uid has not been tampered with
  42. if not constant_time_compare(self._make_token_with_timestamp(user, ts), token):
  43. # RemovedInDjango40Warning: when the deprecation ends, replace
  44. # with:
  45. # return False
  46. if not constant_time_compare(
  47. self._make_token_with_timestamp(user, ts, legacy=True),
  48. token,
  49. ):
  50. return False
  51. # RemovedInDjango40Warning: convert days to seconds and round to
  52. # midnight (server time) for pre-Django 3.1 tokens.
  53. now = self._now()
  54. if legacy_token:
  55. ts *= 24 * 60 * 60
  56. ts += int((now - datetime.combine(now.date(), time.min)).total_seconds())
  57. # Check the timestamp is within limit.
  58. if (self._num_seconds(now) - ts) > settings.PASSWORD_RESET_TIMEOUT:
  59. return False
  60. return True
  61. def _make_token_with_timestamp(self, user, timestamp, legacy=False):
  62. # timestamp is number of seconds since 2001-1-1. Converted to base 36,
  63. # this gives us a 6 digit string until about 2069.
  64. ts_b36 = int_to_base36(timestamp)
  65. hash_string = salted_hmac(
  66. self.key_salt,
  67. self._make_hash_value(user, timestamp),
  68. secret=self.secret,
  69. # RemovedInDjango40Warning: when the deprecation ends, remove the
  70. # legacy argument and replace with:
  71. # algorithm=self.algorithm,
  72. algorithm='sha1' if legacy else self.algorithm,
  73. ).hexdigest()[::2] # Limit to shorten the URL.
  74. return "%s-%s" % (ts_b36, hash_string)
  75. def _make_hash_value(self, user, timestamp):
  76. """
  77. Hash the user's primary key, email (if available), and some user state
  78. that's sure to change after a password reset to produce a token that is
  79. invalidated when it's used:
  80. 1. The password field will change upon a password reset (even if the
  81. same password is chosen, due to password salting).
  82. 2. The last_login field will usually be updated very shortly after
  83. a password reset.
  84. Failing those things, settings.PASSWORD_RESET_TIMEOUT eventually
  85. invalidates the token.
  86. Running this data through salted_hmac() prevents password cracking
  87. attempts using the reset token, provided the secret isn't compromised.
  88. """
  89. # Truncate microseconds so that tokens are consistent even if the
  90. # database doesn't support microseconds.
  91. login_timestamp = '' if user.last_login is None else user.last_login.replace(microsecond=0, tzinfo=None)
  92. email_field = user.get_email_field_name()
  93. email = getattr(user, email_field, '') or ''
  94. return f'{user.pk}{user.password}{login_timestamp}{timestamp}{email}'
  95. def _num_seconds(self, dt):
  96. return int((dt - datetime(2001, 1, 1)).total_seconds())
  97. def _now(self):
  98. # Used for mocking in tests
  99. return datetime.now()
  100. default_token_generator = PasswordResetTokenGenerator()