Source code for smarter.lib.django.token_generators

# pylint: disable=missing-docstring
"""Django token generators for single-use authentications."""

from urllib.parse import urlparse

from django.contrib.auth.tokens import PasswordResetTokenGenerator
from django.contrib.sites.shortcuts import get_current_site
from django.urls import reverse
from django.utils.encoding import force_bytes
from django.utils.http import (
    base36_to_int,
    urlsafe_base64_decode,
    urlsafe_base64_encode,
)
from django.utils.timezone import now as timezone_now

from smarter.apps.account.models import User
from smarter.common.exceptions import SmarterException

DEFAULT_LINK_EXPIRATION = 86400
HFS_EPOCH_UNIX_TIMESTAMP = 2082844800


[docs] class SmarterTokenError(SmarterException): """Base class for all token-related exceptions."""
[docs] class SmarterTokenParseError(SmarterTokenError): pass
[docs] class SmarterTokenConversionError(SmarterTokenError): pass
[docs] class SmarterTokenExpiredError(SmarterTokenError): pass
[docs] class SmarterTokenIntegrityError(SmarterTokenError): pass
[docs] class ExpiringTokenGenerator(PasswordResetTokenGenerator): """ An object of this class can generate a token that expires after a certain amount of time. """
[docs] def __init__(self, expiration: int = DEFAULT_LINK_EXPIRATION): self.expiration = expiration super().__init__()
[docs] def user_to_uidb64(self, user: User) -> str: return urlsafe_base64_encode(force_bytes(user.pk))
[docs] def uidb64_to_user(self, uidb64: str) -> User: uid = urlsafe_base64_decode(uidb64) return User.objects.get(pk=uid)
[docs] @staticmethod def get_timestamp() -> int: return int(timezone_now().timestamp())
[docs] def adjusted_timestamp(self, timestamp: int) -> int: return timestamp + HFS_EPOCH_UNIX_TIMESTAMP
[docs] def validate(self, user, token) -> bool: """ Check that a password reset token is correct for a given user. """ # Ensure token contains exactly one dash and two parts parts = token.split("-") if len(parts) != 2: raise SmarterTokenParseError("Token is not properly formed. It should contain one dash and two parts.") if not self.check_token(user, token): raise SmarterTokenIntegrityError("Token is invalid.") timestamp_b36 = parts[0] try: timestamp = base36_to_int(timestamp_b36) except ValueError as exc: raise SmarterTokenConversionError("Token is invalid.") from exc adjusted_timestamp = self.adjusted_timestamp(timestamp) current_time = self.get_timestamp() if (current_time - adjusted_timestamp) > self.expiration: raise SmarterTokenExpiredError("Token has expired.") return True