Hacks by Ruddra

Implementation of 'Forgot/Reset Password' feature in Django

This post is depricated in favour of django authentication views

Django has its own implementation for reset/forgot password for its admin site. We are going to use that piece of code as reference to implement similar feature for a non admin-site authentication page. Although there are tons of good packages which will allow user to use their password reseting system. But if the system isn’t too complex and doesn’t need such authentication plugins, then reusing the django’s very own implementation can be a good option.

Class based view is going to be used instead of method based view(for no particular reason, so using either of them is alright.). And please read the comments of the example codes for better understanding of implementation.

This implementation is going to divided into two parts. First part is sending an email with reset url, and the Second part is clicking the reset url attached in email and entering new password for reset completion.

Before starting anything, lets look at the django’s reset/forgot password’s implementation in django/contrib/auth/forms.py (source) and django/contrib/auth/views.py (source).

Implementation of sending an email for forgot password with reset url

First need to configure smtp/email configuration so the system can send email. Gmail’s SMTP service is going to be used here.

EMAIL_USE_TLS = True
DEFAULT_FROM_EMAIL = '[email protected]'
SERVER_EMAIL = '[email protected]'
EMAIL_HOST = 'smtp.gmail.com'
EMAIL_PORT = 587
EMAIL_HOST_USER = '[email protected]'
EMAIL_HOST_PASSWORD = 'test123##'
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'

Now we are going to make a reset password form where we are going to add an text field which will take either username or email address associated with the corresponding user.

from django import forms

class PasswordResetRequestForm(forms.Form):
    email_or_username = forms.CharField(label=("Email Or Username"), max_length=254)

We are going to make a view which will check the input email/username and send an email to user’s email address(implementation reference: source).

from django.contrib.auth.tokens import default_token_generator
from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
from django.template import loader
from django.core.validators import validate_email
from django.core.exceptions import ValidationError
from django.core.mail import send_mail
from settings import DEFAULT_FROM_EMAIL
from django.views.generic import *
from utils.forms.reset_password_form import PasswordResetRequestForm
from django.contrib import messages
from django.contrib.auth.models import User
from django.db.models.query_utils import Q

class ResetPasswordRequestView(FormView):
    template_name = "account/test_template.html"    #code for template is given below the view's code
    success_url = '/account/login'
    form_class = PasswordResetRequestForm

    @staticmethod
    def validate_email_address(email):
    '''
    This method here validates the if the input is an email address or not. Its return type is boolean, True if the input is a email address or False if its not.
    '''
        try:
            validate_email(email)
            return True
        except ValidationError:
            return False

    def post(self, request, *args, **kwargs):
    '''
    A normal post request which takes input from field "email_or_username" (in ResetPasswordRequestForm).
    '''
        form = self.form_class(request.POST)
        if form.is_valid():
            data= form.cleaned_data["email_or_username"]
        if self.validate_email_address(data) is True:                 #uses the method written above
            '''
            If the input is an valid email address, then the following code will lookup for users associated with that email address. If found then an email will be sent to the address, else an error message will be printed on the screen.
            '''
            associated_users= User.objects.filter(Q(email=data)|Q(username=data))
            if associated_users.exists():
                for user in associated_users:
                        c = {
                            'email': user.email,
                            'domain': request.META['HTTP_HOST'],
                            'site_name': 'your site',
                            'uid': urlsafe_base64_encode(force_bytes(user.pk)),
                            'user': user,
                            'token': default_token_generator.make_token(user),
                            'protocol': 'http',
                            }
                        subject_template_name='registration/password_reset_subject.txt'
                        # copied from django/contrib/admin/templates/registration/password_reset_subject.txt to templates directory
                        email_template_name='registration/password_reset_email.html'
                        # copied from django/contrib/admin/templates/registration/password_reset_email.html to templates directory
                        subject = loader.render_to_string(subject_template_name, c)
                        # Email subject *must not* contain newlines
                        subject = ''.join(subject.splitlines())
                        email = loader.render_to_string(email_template_name, c)
                        send_mail(subject, email, DEFAULT_FROM_EMAIL , [user.email], fail_silently=False)
                result = self.form_valid(form)
                messages.success(request, 'An email has been sent to ' + data +". Please check its inbox to continue reseting password.")
                return result
            result = self.form_invalid(form)
            messages.error(request, 'No user is associated with this email address')
            return result
        else:
            '''
            If the input is an username, then the following code will lookup for users associated with that user. If found then an email will be sent to the user's address, else an error message will be printed on the screen.
            '''
            associated_users= User.objects.filter(username=data)
            if associated_users.exists():
                for user in associated_users:
                    c = {
                        'email': user.email,
                        'domain': 'example.com', #or your domain
                        'site_name': 'example',
                        'uid': urlsafe_base64_encode(force_bytes(user.pk)),
                        'user': user,
                        'token': default_token_generator.make_token(user),
                        'protocol': 'http',
                        }
                    subject_template_name='registration/password_reset_subject.txt'
                    email_template_name='registration/password_reset_email.html'
                    subject = loader.render_to_string(subject_template_name, c)
                    # Email subject *must not* contain newlines
                    subject = ''.join(subject.splitlines())
                    email = loader.render_to_string(email_template_name, c)
                    send_mail(subject, email, DEFAULT_FROM_EMAIL , [user.email], fail_silently=False)
                result = self.form_valid(form)
                messages.success(request, 'Email has been sent to ' + data +"'s email address. Please check its inbox to continue reseting password.")
                return result
            result = self.form_invalid(form)
            messages.error(request, 'This username does not exist in the system.')
            return result
        messages.error(request, 'Invalid Input')
        return self.form_invalid(form)

As you see above, the code is fairly simple(although it looks long). An encoded user id has been generated here using urlsafebase64_encode(force___bytes(user.pk)) and a token by using defaulttokengenerator.make___token(user). This user id is going to be used later to get the user, the token will be used for checking validity of the url for that user and both the token and the user id is going to be used as unique reference for reset password url. c is a dictionary which has user id, token and other related data etc. This dictionary is going to be blent with the template registration/password_reset_email.html and send to the user’s email address.

For displaying messages(if you are using messages framework of django-1.7, details: source), add this piece of code in your template:

{# test template #}
<!-- code for displaying success or error message in template -->
{% if messages %}
<ul class="messages">
    {% for message in messages %}
    <li>{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
    {% endfor %}
    {% endif %}
</ul>

<!-- Form rendering code for template -->
<form action="" method="post">

    {% csrf_token %}
    {{ form.as_p }}

    <input type="submit" value="Submit" />
</form>

Two more things before wrapping up sending email part. One, making a url for using this view.

urlpatterns = patterns('',
                        url(r'^admin/', include(admin.site.urls)),
                        # url(r'^account/reset_password_confirm/(?P<uidb64>[0-9A-Za-z]+)-(?P<token>.+)/$', PasswordResetConfirmView.as_view(),name='reset_password_confirm'),
                        # PS: url above is going to used for next section of implementation.
                        url(r'^account/reset_password', ResetPasswordRequestView.as_view(), name="reset_password"),
                        )

Two, editing the template of registration/password_reset_email.html or else you will get errors.

{% load i18n %}{% autoescape off %}
{% blocktrans %}You're receiving this email because you requested a password reset for your user account at {{ site_name }}.{% endblocktrans %}

{% trans "Please go to the following page and choose a new password:" %}
    {% block reset_link %}
        {{ domain }}{% url 'reset_password_confirm' uidb64=uid token=token %}
        <!--This is the only change from ` django/contrib/admin/templates/registration/password_reset_subject.html`. the url name is commented out in urls.py section. The view associated with the url is going to described later in this post. -->
    {% endblock %}
{% trans "Your username, in case you've forgotten:" %} {{ user.get_username }}

{% trans "Thanks for using our site!" %}

{% blocktrans %}The {{ site_name }} team{% endblocktrans %}

{% endautoescape %}

Now run the server and you will see forms like the screen shots below: (This screenshots look cool because django adminsite’s js/css have been used here.) (**Image sequence is according the implementation flow)

Screenshots

Rendered template from PasswordResetRequestForm form

Rendered template from PasswordResetRequestForm form with error messages

Rendered template of login form with sent email confirmation message

Sent Email look

Implemetation of clicking the reset url and entering new password for reset completation.

First, lets write a form which will have two fields new password and retype password field.

class SetPasswordForm(forms.Form):
    """
    A form that lets a user change set their password without entering the old
    password
    """
    error_messages = {
        'password_mismatch': ("The two password fields didn't match."),
        }
    new_password1 = forms.CharField(label=("New password"),
                                    widget=forms.PasswordInput)
    new_password2 = forms.CharField(label=("New password confirmation"),
                                    widget=forms.PasswordInput)

    def clean_new_password2(self):
        password1 = self.cleaned_data.get('new_password1')
        password2 = self.cleaned_data.get('new_password2')
        if password1 and password2:
            if password1 != password2:
                raise forms.ValidationError(
                    self.error_messages['password_mismatch'],
                    code='password_mismatch',
                    )
        return password2

It will take two password input and verify if they match, if those inputs match(in clean method), it will return password. Now using that form, we are going to write a view(reference for implementation:(source).

class PasswordResetConfirmView(FormView):
    template_name = "account/test_template.html"
    success_url = '/admin/'
    form_class = SetPasswordForm

    def post(self, request, uidb64=None, token=None, *arg, **kwargs):
        """
        View that checks the hash in a password reset link and presents a
        form for entering a new password.
        """
        UserModel = get_user_model()
        form = self.form_class(request.POST)
        assert uidb64 is not None and token is not None  # checked by URLconf
        try:
            uid = urlsafe_base64_decode(uidb64)
            user = UserModel._default_manager.get(pk=uid)
        except (TypeError, ValueError, OverflowError, UserModel.DoesNotExist):
            user = None

        if user is not None and default_token_generator.check_token(user, token):
            if form.is_valid():
                new_password= form.cleaned_data['new_password2']
                user.set_password(new_password)
                user.save()
                messages.success(request, 'Password has been reset.')
                return self.form_valid(form)
            else:
                messages.error(request, 'Password reset has not been unsuccessful.')
                return self.form_invalid(form)
        else:
            messages.error(request,'The reset password link is no longer valid.')
            return self.form_invalid(form)

URL for this view:

urlpatterns += patterns('',
                        url(r'^admin/', include(admin.site.urls)),
                        url(r'^account/reset_password_confirm/(?P<uidb64>[0-9A-Za-z]+)-(?P<token>.+)/$', PasswordResetConfirmView.as_view(),name='reset_password_confirm'),
                    )

Well PasswordResetConfirmView takes two parameter from urls, uidb64 and token, those were sent within email generated by ResetPasswordRequestView. We got user id hence the user by decoding uid64 by using urlsafebase64decode, and function defaulttokengenerator.check__token checks the token against the user. If they are valid and the form is valid, we set new password for the user using .set__password(‘password’) function. If they are not valid, it will show an error message saying the url is no longer valid.

More screenshots:(sequencial to implementation)

Screenshots

Rendered template for SetPasswordForm form

Reset successful

Thus you implement your very own forgot or reset the password.

For full project/implementation, please check/fork this repository. This code has been tested for Python3 and django 1.71.10 .
Share Your Thoughts
M ↓   Markdown
ares19896
Tuesday, Oct 8, 2019

In registration/password_reset_email.html: {{ domain }}{% url 'reset_password_confirm' uidb64=uid token=token %} I want to show in email, this link is ‘Reset Password’. Please help me

Uma Maheswara Rao Pinninti
Tuesday, Mar 8, 2016

Hi, This template throwing the below error. Reverse for 'password_reset_confirm' with arguments '()' and keyword arguments '{u'uidb64': 'Mw', u'token': u'4a1-0432bcab41a9fde28f68'}' not found. 0 pattern(s) tried: [] Please respond as soon as possible.. Thank you.

Arnab Kumar Shil
Thursday, Jul 14, 2016

Check this answer, which should solve your problem :) http://stackoverflow.com/qu...

Ashutosh
Thursday, Oct 20, 2016

I can not see the screenshots..

Ashutosh
Thursday, Oct 20, 2016

also i can not see the template code of other templates

Ruddra
Friday, Oct 21, 2016

Thanks for noticing. I have updated the post :)

Alex
Sunday, Dec 11, 2016

Thank You! is very useful.

Aswa
Wednesday, Feb 1, 2017

Great article, very useful! However, I cannot find django/contrib/admin/templates/registration/password_reset_subject.txt

m1ckaveli
Tuesday, Feb 7, 2017

you have to write it yourself i think. Create a folder named registration in your template folder and add the file inside of the folder

sravani
Wednesday, Mar 22, 2017

what is the username and password

Ruddra
Wednesday, Mar 29, 2017

You can create user in your system and use the user’s username and password

Alejandro Hurtado
Saturday, Dec 23, 2017

and if I want to use my own template? it does not look stylish with django admin, thanks

Ruddra
Sunday, Dec 24, 2017

You can do that. It is not a big deal. Just change your template_name in Views.

Prabhash Tiwari
Tuesday, Mar 27, 2018

TemplateDoesNotExist at /account/reset_password why shows this error..

Ruddra
Tuesday, Mar 27, 2018

Hi, can you please create an issue on the github repository with the error stacktrace? In that way, I can easily figure out if something is wrong. Also don’t forget to mention which django version you are using. Thanks

Chinmoy Acharjee
Thursday, May 17, 2018

If i want to resend the email with that particular URL then how can i do it?

Ruddra
Friday, May 18, 2018

You can just use this code: # somehow you got email or username and put it in data associated_users= User.objects.filter(Q(email=data)|Q(username=data)) if associated_users.exists(): for user in associated_users: c = { 'email': user.email, 'domain': request.META['HTTP_HOST'], 'site_name': 'your site', 'uid': urlsafe_base64_encode(force_bytes(user.pk)), 'user': user, 'token': default_token_generator.make_token(user), 'protocol': 'http', } subject_template_name='registration/password_reset_subject.txt' # copied from django/contrib/admin/templates/registration/password_reset_subject.txt to templates directory email_template_name='registration/password_reset_email.html' # copied from django/contrib/admin/templates/registration/password_reset_email.html to templates directory subject = loader.render_to_string(subject_template_name, c) # Email subject *must not* contain newlines subject = ''.join(subject.splitlines()) email = loader.render_to_string(email_template_name, c) send_mail(subject, email, DEFAULT_FROM_EMAIL , [user.email], fail_silently=False)

firoj khan
Thursday, Oct 4, 2018

it giving error like The reset password link is no longer valid. in mail this link is going http://127.0.0.1:8000/user/reset_password_confirm/(%3FPb'YTNlYWQyYWUtMTAyOC00ZjY1LTgwYTMtMWQ3NThiYjJjOTQ3'%5B0-9A-Za-z%5D+)-(%3FP505-9a4d4776744e45518e0e.+)/

disqus_ZwWOr4FAus
Friday, Jan 4, 2019

disqus_0GqaZnP8Yj important