Jul 21, 2012

Django: Resetting Passwords (with internal tools)

I have had a task recently. It was about adding a forms/mechanism for resetting a password in our Django based project. We have had our own registration system ongoing... It's a corporate sector project. So you can not go and register yourself. Admins (probably via LDAP sync) will register your email/login in system. So you have to go there and only set yourself a password. For security reasons you can not register. One word.

First I've tried to find standart decision. From reviewed by me were: django-registration and django password-reset. These are nice tools to install and give it a go. But I've needed a more complex decision. And the idea was that own bicycle is always better. So I've thought of django admin and that it has all the things you need to do this yourself in no time. (Actually it's django.contrib.auth part of django, but used out of the box in Admin UI) You can find views you need for this in there. they are:
  • password_reset
  • password_reset_done 
  • password_reset_confirm 
  • password_reset_complete 
To use this method you need:

1. Emails config:

Your typical Django project has settings.py variables to be set for enabling SMTP server. Usually you need this on production to send admins emails about 500 errors and/or system warnings. For our needs python has an internal mail server. I prefer to use it for debug purposes. So good method would be to use it only in Debug case. You can do it by adding those lines to your settings.py:
if DEBUG:
    EMAIL_HOST = 'localhost'
    EMAIL_PORT = 1025
    EMAIL_HOST_USER = ''
    EMAIL_HOST_PASSWORD = ''
    EMAIL_USE_TLS = False
    DEFAULT_FROM_EMAIL = 'testing@example.com'
Here you will have this custom email server set to alternative port. For production you should enter your Sendmail (or whatever you use there) parameters...

Now you will be able to start it running the terminal and executing:
python -m smtpd -n -c DebuggingServer localhost:1025
You can also read abut it here. You will be able to copy/paste email links that should be sent via Django email. Now that you have everything setup for experiments...

2. Config Url's:

urls.py
urlpatterns = patterns('',
    url(r'^user/password/reset/$', 
        'django.contrib.auth.views.password_reset', 
        {'post_reset_redirect' : '/user/password/reset/done/'},
        name="password_reset"),
    (r'^user/password/reset/done/$',
        'django.contrib.auth.views.password_reset_done'),
    (r'^user/password/reset/(?P<uidb36>[0-9A-Za-z]+)-(?P<token>.+)/$', 
        'django.contrib.auth.views.password_reset_confirm', 
        {'post_reset_redirect' : '/user/password/done/'}),
    (r'^user/password/done/$', 
        'django.contrib.auth.views.password_reset_complete'),
    # ...
)

3. Add Templates:

You have templates for your reset password forms already. Django admin templates/css/js are used there. But I think you would want custom one's for your needs. SO basically all you may need is overriding those templates, specified down here and place them on your templates Django path.
You need to override:
Django templates:

  • registration/password_reset_form.html
  • registration/password_reset_done.html
  • registration/password_reset_confirm.html
  • registration/password_reset_complete.html

Email template:

  • registration/password_reset_email.html 

I'll write down here one's I've used, according to Django's "Batteries included" philosophy.

registration/password_reset_form.html 
{% extends "base.html" %}

{% block title %}Reset Password{% endblock %}

{% block content %}
<p>Please specify your email address to receive instructions for resetting it.</p>

<form action="" method="post">
    <div style="display:none">
        <input type="hidden" value="{{ csrf_token }}" name="csrfmiddlewaretoken">
    </div>
     {{ form.email.errors }}
    <p><label for="id_email">E-mail address:</label> {{ form.email }} <input type="submit" value="Reset password" /></p>
</form>
{% endblock %}

registration/password_reset_done.html 
{% extends "base.html" %}

{% block title %}Password reset successful{% endblock %}

{% block content %}
<p>We've e-mailed you instructions for setting your password to the e-mail address you submitted.</p>
<p>You should be receiving it shortly.</p>
{% endblock %}

registration/password_reset_confirm.html 
{% extends "base.html" %}
{% block title %}Setting New password{% endblock %}

{% block content %}
    {% if validlink %}
        <p>Please enter your new password twice.<br />
           So we can verify you typed it in correctly.</p>
        <form action="" method="post">
            <div style="display:none">
                <input type="hidden" value="{{ csrf_token }}" name="csrfmiddlewaretoken">
            </div>
            <table>
                <tr>
                    <td>{{ form.new_password1.errors }}
                        <label for="id_new_password1">New password:</label></td>
                    <td>{{ form.new_password1 }}</td>
                </tr>
                <tr>
                    <td>{{ form.new_password2.errors }}
                        <label for="id_new_password2">Confirm password:</label></td>
                    <td>{{ form.new_password2 }}</td>
                </tr>
                <tr>
                    <td></td>
                    <td><input type="submit" value="Change my password" /></td>
                </tr>
            </table>
        </form>
    {% else %}
        <h1>Password reset unsuccessful</h1>
        <p>The password reset link was invalid, <br />
        possibly because it has already been used. <br />
        Please request a new password reset.</p>
    {% endif %}
{% endblock %}

registration/password_reset_complete.html 
{% extends "base.html" %}

{% block title %}Password reset complete{% endblock %}

{% block content %}
<p>Your password has been set.  You may go ahead and log in now.</p>
<p><a href="{{ login_url }}">Log in</a></p>
{% endblock %}

registration/password_reset_email.html 
{% autoescape off %}
You're receiving this e-mail because you requested a password reset for your user account at {{ site_name }}.

Please go to the following page and choose a new password:
{% block reset_link %}
{{ protocol }}://{{ domain }}{% url django.contrib.auth.views.password_reset_confirm uidb36=uid, token=token %}
{% endblock %}

Your username, in case you've forgotten: {{ user.username }}

Thanks for using our site!

The {{ site_name }} team.

{% endautoescape %}

SO you're getting password reset system out of the box using only Django included tools. In no time.
Comment me...!





43 comments:

  1. Amazing tutorial!!!

    But I receive 2 emails, at the same time. I had a problem like this when I was using signals and sendmail. Do you know what's happening??

    Thanks!!!

    ReplyDelete
    Replies
    1. did you ever fix this? I had the same issue.

      Delete
  2. There's a typo in the registration/password_reset_complete.html:

    It says: ">Log in>"
    Must say: ">Log in<"

    I had to add the 'template_name' in all the URLs, otherwise the custom admin templeates were used. You might clarify how to do to assure that our templates are used before the admin ones...


    Thanks for the tutorial.

    ReplyDelete
    Replies
    1. With testing. E.g. you could add some hook in template if it is the same as admin and stick your tests to that.

      Thanks for the error correction.

      Delete
  3. Well i would like to add something:

    In settings.py, if you are using your gmail id then , settings.py should be changed as:

    EMAIL_HOST = 'smtp.gmail.com'
    EMAIL_HOST_USER = ''
    EMAIL_HOST_PASSWORD = ''
    EMAIL_PORT = 587
    EMAIL_USE_TLS = True

    ReplyDelete
    Replies
    1. Hi Shashank ! I reported your code inside settings.py file but when I type
      python -m smtpd -n -c DebuggingServer localhost:1025 I got the following errors
      SMTP proxy version 0.2
      502 Error: command "GET" not implemented
      502 Error: command "HOST:" not implemented
      502 Error: command "CONNECTION:" not implemented
      502 Error: command "ACCEPT:" not implemented
      502 Error: command "UPGRADE-INSECURE-REQUESTS:" not implemented
      502 Error: command "USER-AGENT:" not implemented
      502 Error: command "ACCEPT-ENCODING:" not implemented
      502 Error: command "ACCEPT-LANGUAGE:" not implemented
      500 Error: bad syntax

      Thanks.

      Delete
  4. what if i want the email sent contain html content?
    thanks

    ReplyDelete
    Replies
    1. there are 2 email templates in this article... Feel free to change them as you like..

      Delete
  5. Hi,

    That's perfect and everything is working, but I would like to send a html email. By default django is sending only a text email. Is there an easy and internal way to attach a beautiful html version of that email.
    Thanks

    ReplyDelete
    Replies
    1. there are 2 email templates in this article... Feel free to change them as you like..

      Delete
  6. It really helped down here. Cheers from Brazil!

    ReplyDelete
  7. Well, I only wish it was higher in Google search list :P
    Well worked out.
    Cheers

    ReplyDelete
  8. I already got it and it's works for me, but i don't want to use smtp port like google

    EMAIL_HOST = 'smtp.gmail.com'
    EMAIL_HOST_USER = ''
    EMAIL_HOST_PASSWORD = ''
    EMAIL_PORT = 587
    EMAIL_USE_TLS = True

    it's possible without using this, we send mail

    ReplyDelete
    Replies
    1. Why not?. Should work out of the box. Depending on your deployment server configuration. And how you send emails from your server.

      Delete
  9. The syntax for:
    {{ protocol }}://{{ domain }}{% url django.contrib.auth.views.password_reset_confirm uidb36=uid, token=token %}
    has changed in Django 1.5 to:
    {{ protocol }}://{{ domain }}{% url 'django.contrib.auth.views.password_reset_confirm' uidb36=uid token=token %}

    ReplyDelete
  10. tem como apenas receber a resposta do django da página password_reset_complete.html ? para colocar a resposta dele via ajax no password_reset_confirm.html ?

    ReplyDelete
  11. Not sure what I'm doing wrong - copied and pasted everything you provide above, but after resetting password, I get:

    NoReverseMatch at /user/password/reset/
    Reverse for 'password_reset_confirm' with arguments '()' and keyword arguments '{u'uidb64': 'Mg', u'token': u'3nh-f59326e4f61d0e42f9d3'}' not found. 1 pattern(s) tried: ['user/password/reset/(?P[0-9A-Za-z]+)/(?P.+)/$']

    I did name the URL, as in:

    url(r'^user/password/reset/(?P[0-9A-Za-z]+)/(?P.+)/$', 'django.contrib.auth.views.password_reset_confirm',
    {'template_name': 'password_reset_confirm.html',
    'post_reset_redirect' : 'user/password/done/'},
    name='password_reset_confirm'),

    Also, not sure how to tell it to use my password_reset_email.html and not django.contrib's

    Thanks for writing this up tho!

    ReplyDelete
    Replies
    1. registration/password_reset_email.html
      This is a name of a email template. It should be located in the root of your templates folder.
      Maybe thing is in your template loaders. You should load YOUR templates first to override admin templates.

      Not sure though. Make sure you have inserted all the urls into urls.py accordingly.

      Delete
    2. Hey Dustin! Im facing the same issue. Ive done whatever has been told. Were you able to fix your issue?

      Delete
    3. I used Dustin's modified urlconf entry for the password_reset_confirm (using url() function and naming it), and then used my new named url in the password_reset_email.html file in place of the 'django.contrib.auth.views.password_reset_confirm' in the url tag. That fixed it for me.

      I keep the registration and password reset templates in my project/mainApp/templates/registration folder and it all seems just dandy!

      Delete

    4. Hi, I had the same issue and solved it by adding the following line to my urls.py

      url(r'^reset/done/$', 'django.contrib.auth.views.password_reset_complete', name='password_reset_complete'),

      Delete
    5. Hi, for anyone facing the same issues you need to change uidb36 to uidb64 in your url.

      Delete
    6. Yeah, uidb64 needed to be changed. I also had to remove the comma and quote the password_reset_confirm portion like so:

      {{ protocol }}://{{ domain }}{% url 'django.contrib.auth.views.password_reset_confirm' uidb64=uid token=token %}

      Delete
  12. @Dustin I had the same problem but turned out, as lurii suggests, that I just didn't have it in the right templates directory,.

    Otherwise everything worked exactly as he has it above with two exceptions (for me on Django 1.6 anyway):

    1) the url template tag syntax as noted above by "F L"
    2) uidb36 in the urls.py and email template both should be uidb64 per https://docs.djangoproject.com/en/1.6/releases/1.6/#django-contrib-auth-password-reset-uses-base-64-encoding-of-user-pk

    ReplyDelete
  13. I did this for and app some months ago and it worked perfectly! I was afraid I couldn't find it again now that I need it, again

    Thank You :D

    ReplyDelete
  14. How can i check whether a email is not registered yet ???

    AJAX $.get() is not working in template password_reset_form

    ReplyDelete
  15. I can't seem to get any email! I'm using smtp gmail. Any idea why it's not sending the email?

    ReplyDelete
    Replies
    1. Where do you use Gmail? are other emails pass? How do you use gmail?
      In general I do not have context to answer your question.

      Delete
  16. This works completely fine. Except one thing.
    It should throw some error, in-case a user is not registered and trying to reset password.
    How to do that ?

    ReplyDelete
  17. how do i resolve this errror??

    TypeError at /user/password/reset/Mjk-426-d531c711333f26f94afa/
    password_reset_confirm() got an unexpected keyword argument 'uidb36'

    ReplyDelete
  18. What a fantastic post - incredibly helpful!!! I made minor alterations for example I had to use uidb64 in all cases because uidb36 is no longer in use: https://docs.djangoproject.com/en/1.8/releases/1.6/

    Also in settings.py I had to include DEFAULT_FROM_EMAIL = 'Your Name '
    So that I no longer got the error:
    SMTPRecipientsRefused: {u'recipient@recipientsdomain.com': (504, '5.5.2 : Sender address rejected: need fully-qualified address')}

    ReplyDelete
    Replies
    1. This comment has been removed by the author.

      Delete
    2. DEFAULT_FROM_EMAIL = ' your name then your email-address-within-angle-brackets'
      The entire thing in quotes as with all other settings.py values.

      This blogger program isn't allowing me to type angle brackets.

      Delete
  19. Just thought I should add, that in order to override admin template, you need to specify the directory of the app that contains your customized registration/password_reset_form.html etc, in the DIR:[] of template in your settings. https://docs.djangoproject.com/en/1.9/ref/templates/upgrading/

    ReplyDelete
  20. Thank you for your time, you helped me a lot!

    ReplyDelete