Search code examples
pythondjangotokenchange-password

Password recovery - Django token never expires


I've set up a password recovery system in my application which work pretty well however I'm facing a problem with the token issued which apparently never expire, at least when it gets used multiple time by the user.

The link sent by email to the user remain valid even after changing the password x times with the same link.

I'm using the regular way I've found on internet with token_generator.make_token(user)

utils.py

from django.contrib.auth.tokens import PasswordResetTokenGenerator
from six import text_type

class AppTokenGenerator(PasswordResetTokenGenerator):

    def _make_hash_value(self, user, timestamp):
        return (text_type(user.is_active), text_type(user.pk), text_type(timestamp))


token_generator = AppTokenGenerator()

api_email.py

def send_email_user_account_password_recover(request, user, language):
    try:
        uidb64 = urlsafe_base64_encode(force_bytes(user.pk))
        token = token_generator.make_token(user)

        url_base = get_url_base(request)
        email_text = emailText["user_account_password_recover"][language]

        if language == "fr":
            link_text = "Réinitialiser mon mot de passe"
            activate_url = url_base + f"/fr/recover-password-authorised/{uidb64}/{token}/"
        else:
            link_text = "Reset my password"
            activate_url = url_base + f"/en/recover-password-authorised/{uidb64}/{token}/"

        context = {"title": email_text["title"],
                   "content": email_text["text"],
                   "url_base": url_base,
                   "link": activate_url,
                   "link_text": link_text,
                   "language": language}

        html_content = render_to_string("email/email-template-extends.html", context)
        text_content = strip_tags(html_content)

        email = EmailMultiAlternatives(
            subject=email_text["title"],
            body=text_content,
            to=[user.email])

        email.attach_alternative(html_content, "text/html")
        email.send(fail_silently=False)

        logger.info(f"Email user password recover for user ({user.id_code}) sent from {EMAIL_HOST_USER} to {user.email}.")
        return True

    except:
        logger.error(f"Email user password recover for user ({user.id_code}) could not be sent.")
        return False

views.py

def AccountVerification(request, language=None, uidb64=None, verification_token=None):
    if verification_token:
        if not language:
            if request.LANGUAGE_CODE == "fr":
                return HttpResponseRedirect(f'/fr/verification/email/{uidb64}/{verification_token}/')
            else:
                return HttpResponseRedirect(f'/en/verification/email/{uidb64}/{verification_token}/')

        id = force_text(urlsafe_base64_decode(uidb64))
        user = api.charge_user_from_id(id)

        try:
            if not token_generator.check_token(user, verification_token):
                logger.error(f"{get_first_part_log(request)} Link not valid anymore.")

                if language == "fr":
                    messages.error(request, f"Le lien n'est plus valide.")
                    return HttpResponseRedirect("/fr/se-connecter/")
                else:
                    messages.error(request, f"The link is not valid anymore.")
                    return HttpResponseRedirect("/en/login/")

            if user.is_active:
                logger.info(f"{get_first_part_log(request)} User already activated, redirect to login.")

                if language == "fr":
                    return HttpResponseRedirect("/fr/se-connecter/")
                else:
                    return HttpResponseRedirect("/en/login/")

            user.is_active = True
            user.is_email_validated = True
            user.save()

            logger.info(f"{get_first_part_log(request)} Charging email verification completed page.")

            if language == "fr":
                return render(request, "fr/authentication/email-verification-completed.html", {})
            else:
                return render(request, "en/authentication/email-verification-completed.html", {})

        except:
            logger.error(f"{get_first_part_log(request)} An error occurred.")

            if language == "fr":
                messages.error(request, f"Une erreur est survenue, contactez le support ([email protected])")
                return HttpResponseRedirect("/fr/se-connecter/")
            else:
                messages.error(request, f"An error occurred, please contact support ([email protected])")
                return HttpResponseRedirect("/en/login/")

    else:
        pass

My question is simple, how can I delete the token from record or make it invalid if the user already used it AND changed his password successfully ?

Thank you in advance for your help !


Solution

  • This doesn't fully answer your question as I don't think the tokens can be set as a one use only but you can reduce the number of seconds that the token is valid for in setting.py. The default is 3 days as per the below.

    PASSWORD_RESET_TIMEOUT = 259200 # Default: 259200 (3 days, in seconds)

    token_generator.check_token(user, verification_token)
    

    if the timeout has elapsed the above would return false