Alternative of PASSWORD_RESET_TIMEOUT in customized/overridden PasswordResetConfirmView?

Hi there,

I have two kinds of PasswordResetConfirm Views. One is for those registered users who want to reset their password and this is using the DJango Auth’s default PasswordResetConfim view, and other for those anonymous users who have been invited by admin to join the system and they just fill up their password as other information has already been added by admin.

In the former PasswordResetConfirm view, I want the password reset link to be expired within 1 hour and in the latter one I want password reset link to be expired within 1 day. So even if I use PASSWORD_RESET_TIMEOUT, how will it distinguish between the two same kind of views? If it’s not possible for PASSWORD_RESET_TIMEOUT to distinguish between two similar views, how can I implement this expiration terminology as customized in both views?

My views.py is:

class InviteUser(View):
    def get(self, request: HttpRequest) -> HttpResponse:        
        invite_user_form = InviteUserForm()

        context = {
            "title": "Invite Someone for Registration",
            "invite_user_form": invite_user_form,
        }

        return render(request, "accounts/invite-user.html", context)

    def post(self, request: HttpRequest):
        invite_user_form = InviteUserForm(request.POST)
        if invite_user_form.is_valid():
            data = invite_user_form.cleaned_data

            if request.user.is_authenticated and request.user.is_superuser:
                subject = "Invitation to Join PricingMeister"
                email_template_name = "accounts/invite-user-email.txt"
                user = request.user

                email_details = {
                    "email": urlsafe_base64_encode(str(data["invitee_email"]).encode('utf-8')),
                    "domain": "127.0.0.1:8000",
                    "site_name": "PricingMeister",
                    "uid": urlsafe_base64_encode(str(user.pk).encode('utf-8')),
                    "user": user,
                    "token": default_token_generator.make_token(user),
                    "protocol": "http",
                }

                email = render_to_string(email_template_name, email_details)

                invitation = invite_user_form.save(commit=False)

                invitation.created_by = request.user

                invitation.expiration_date = (
                    datetime.datetime.now() + datetime.timedelta(days=1)
                )

                invitation.save()

                try:
                    send_mail(
                        subject,
                        email,
                        "admin@pricingmeister.com",
                        [str(data["invitee_email"])],
                        fail_silently=False,
                    )
                except BadHeaderError:
                    return HttpResponse("Invalid header found.")

                return redirect("/accounts/invite-user/done/")
            else:
                return HttpResponse("You are not authorized to view this page.")
        else:
            print("Form is not valid")


class InviteUserDone(View):
    def get(self, request: HttpRequest) -> HttpResponse:
        context = {
            "title": "Invitation Sent",
        }

        return render(request, "accounts/invite-user-done.html", context)

class InviteUserConfirm(View):
    def get(self, request: HttpRequest, uidb64: str, email: str, token: str) -> HttpResponse:
        password_set_form = PasswordSetForm()

        context = {"title": "Set Your Password", "password_set_form": password_set_form}

        return render(request, "accounts/password-set-confirm.html", context)

    def post(self, request: HttpRequest, uidb64: str, email: str, token: str) -> HttpResponse:
        password_set_form = PasswordSetForm(request.POST)

        if password_set_form.is_valid():
            login = password_set_form.cleaned_data.get("login")
            password1 = password_set_form.cleaned_data.get("password1")
            password2 = password_set_form.cleaned_data.get("password2")

            user_pk = int(urlsafe_base64_decode(uidb64).decode("ascii"))
            invitee_email = str(urlsafe_base64_decode(email).decode("ascii"))
            user = User.objects.get(pk=user_pk)
            invitation = Invitation.objects.get(created_by=user, invitee_email=invitee_email, is_canceled=False)

            if password1 == password2:
                new_user = User.objects.create_user(login=login, email=invitation.invitee_email, password=password1)

                if new_user:
                    new_user.fist_name = invitation.invitee_first_name
                    new_user.last_name = invitation.invitee_last_name
                    new_user.save()

                    invitation.accepted_at = datetime.datetime.now()
                    invitation.is_canceled = True
                    invitation.save()

                    messages.info(
                        request, f"You have set your login name and password successfully"
                    )  # just for test purpose right now

                    return render(
                        request, "accounts/success.html"
                    )  # just for test purpose right now
                else:
                    messages.info(
                        request, f"ERROR: User already exists with given login name or email"
                    )  # just for test purpose right now

                    return render(
                        request, "accounts/success.html"
                    )  # just for test purpose right now
            else:
                messages.error(
                    request, "Invalid username or password."
                )  # just for test purpose right now
                return render(
                    request, "accounts/error.html"
                )  # just for test purpose right now
        else:
            messages.error(
                request, "Invalid username or password."
            )  # just for test purpose right now
            return render(
                request, "accounts/error.html"
            )  # just for test purpose right now

class PasswordReset(View):
    def get(self, request: HttpRequest) -> HttpResponse:
        password_reset_form = PasswordResetForm()

        context = {
            "title": "Reset Your Password",
            "password_reset_form": password_reset_form
        }

        return render(request, "accounts/password-reset.html", context)

    def post(self, request):
        password_reset_form = PasswordResetForm(request.POST)
        
        if password_reset_form.is_valid():
            email_address = password_reset_form.cleaned_data['email']
            user = User.objects.get(email=email_address)
            
            if user:
                subject = "Password Reset Requested"
                email_template_name = "accounts/password-reset-email.txt"
                
                email_details = {
                    "email": email_address,
                    'domain': '127.0.0.1:8000',
                    'site_name': 'PricingMeister',
                    "uid": urlsafe_base64_encode(str(user.pk).encode('utf-8')),
                    "user": user,
                    'token': default_token_generator.make_token(user),
                    'protocol': 'http',
                }
                
                email = render_to_string(email_template_name, email_details)
                
                try:
                    send_mail(subject, email, "admin@pricingmeister.com" , [user.email], fail_silently=False)
                except BadHeaderError:
                    return HttpResponse('Invalid header found.')
                
                return redirect ("/accounts/password-reset/done/")
            else:
                return HttpResponse("You are not authorized to view this page.")
        else:
            print("Form is not valid")

Relevant url patterns in my urls.py are:

from django.contrib.auth import views as auth_views

urlpatterns = [
    path("invite-user/", InviteUser.as_view(), name="invite_user"),  # Invite User Page
    path("invite-user/done/", InviteUserDone.as_view(), name="invite_user_done"),  # Invite User Done Page
    path(
        "invite-user/<uidb64>/<email>/<token>/",
        InviteUserConfirm.as_view(),
        name="invite_user_confirm",
    ),  # Invite User Confirm Page
    path("password-reset/", PasswordReset.as_view(), name="password_reset"),  # Password Reset Page
    path(
        "password-reset/done/",
        auth_views.PasswordResetDoneView.as_view(
            template_name="accounts/password-reset-done.html"
        ),
        name="password_reset_done",
    ),  # Password Reset Done Page
    path(
        "password-reset/<uidb64>/<token>/",
        auth_views.PasswordResetConfirmView.as_view(
            template_name="accounts/password-reset-confirm.html"
        ),
        name="password_reset_confirm",
    ),  # Password Reset Confirm Page

Can anyone tell me how can I implement the functionality I described above?

It’s not clear to me - are you talking about one view used for both purposes or is it two views, one used for each purpose?

If it’s two different views, then you can create a custom check_token method that uses a different value than the method used in PasswordResetTokenGenerator, and verify the token instead of using the default method.

Or, you could approach this from the other end - change the make_token method to create the “short-life” token using a timestamp that is 23 hours old. (This would also work if you’re using the same view for the resets.)

See the source code for PasswordResetTokenGenerator in django.contrib.auth.tokens

1 Like

There are two views and both of them have different purposes. One is the default PasswordResetConfirm view and another is kind of it’s copy and that is:

I read your reply but I am not sure how will I have to work with the check_token or make_token to expire the password reset link. I mean what will be the logic? Secondly, I am using the model Invitation for InviteUserConfirm, and model Invitation has the attribute expiration_date:

class Invitation(models.Model):
    created_by = models.ForeignKey(User, on_delete=models.CASCADE)
    created_at = models.DateTimeField(auto_now_add=True)
    expiration_date = models.DateTimeField(null=True, blank=True)
    invitee_first_name = models.CharField(max_length=254, null=True, blank=True)
    invitee_last_name = models.CharField(max_length=254, null=True, blank=True)
    invitee_email = models.EmailField(max_length=254, blank=True)
    secret = models.CharField(max_length=254, blank=True)
    message = models.TextField(blank=True)
    accepted_at = models.DateTimeField(null=True, blank=True)
    is_canceled = models.BooleanField(default=False)

I want the password set/reset link sent to anonymous user to invite him/her to join system, gets expired at the time stored in expiration_date attribute.

As I said there are two views, for the default PasswordResetConfirm view, I can use PASSWORD_RESET_TIMEOUT in my settings.py to expire the password reset link in 1 hour, but what workflow should I follow to get same functionality as PASSWORD_RESET_TIMEOUT, in my InviteUserConfirm view to expire the password set/reset link in 1 day (24 hours)?

Read the code for PasswordResetTokenGenerator - see how it’s used. Look to see how the current time is encoded within the token, and how that is compared to the PASSWORD_RESET_TIMEOUT to validate the age of the token. Then use that as a model for your own implementation.

If you look at my views.py file, I am using from django.contrib.auth.tokens import default_token_generator token generator to generate token in the email_details vairable of post() method in InviteUser view:

Can this token generator help me to generator token for 1 hour or 24 hours and then use that timeline to make condition in template that if token exists then show the reset password form otherwise show error that password reset link is expired?

@KenWhitesell I tried to find the source code of PasswordResetTokenGenerator but I am unable to find anywhere neither in official documentation nor in any oher websites, just found some pieces of codes containing _make_token_with_timestamp() and _make_hash_value()1, besides this, I did not fine any other thing. Can you provide me the link to the whole source code of PasswordResetTokenGenerator`?

You have it in your installed version of Django. It’s located in django.contrib.auth.tokens.