Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #7723 - implemented a secure password reset form that uses a to…

…ken and prompts user for new password.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@8162 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit fcd837cd0f9b2c706bc49af509628778d442bb3f 1 parent 9a56fe7
Luke Plant authored July 31, 2008
3  django/conf/global_settings.py
@@ -366,6 +366,9 @@
366 366
 
367 367
 LOGIN_REDIRECT_URL = '/accounts/profile/'
368 368
 
  369
+# The number of days a password reset link is valid for
  370
+PASSWORD_RESET_TIMEOUT_DAYS = 3
  371
+
369 372
 ###########
370 373
 # TESTING #
371 374
 ###########
14  django/contrib/admin/templates/registration/password_reset_complete.html
... ...
@@ -0,0 +1,14 @@
  1
+{% extends "admin/base_site.html" %}
  2
+{% load i18n %}
  3
+
  4
+{% block breadcrumbs %}<div class="breadcrumbs"><a href="../../">{% trans 'Home' %}</a> &rsaquo; {% trans 'Password reset' %}</div>{% endblock %}
  5
+
  6
+{% block title %}{% trans 'Password reset complete' %}{% endblock %}
  7
+
  8
+{% block content %}
  9
+
  10
+<h1>{% trans 'Password reset complete' %}</h1>
  11
+
  12
+<p>{% trans "Your password has been set.  You may go ahead and log in now." %}</p>
  13
+
  14
+{% endblock %}
32  django/contrib/admin/templates/registration/password_reset_confirm.html
... ...
@@ -0,0 +1,32 @@
  1
+{% extends "admin/base_site.html" %}
  2
+{% load i18n %}
  3
+
  4
+{% block breadcrumbs %}<div class="breadcrumbs"><a href="../">{% trans 'Home' %}</a> &rsaquo; {% trans 'Password reset confirmation' %}</div>{% endblock %}
  5
+
  6
+{% block title %}{% trans 'Password reset' %}{% endblock %}
  7
+
  8
+{% block content %}
  9
+
  10
+{% if validlink %}
  11
+
  12
+<h1>{% trans 'Enter new password' %}</h1>
  13
+
  14
+<p>{% trans "Please enter your new password twice so we can verify you typed it in correctly." %}</p>
  15
+
  16
+<form action="" method="post">
  17
+{% if form.new_password1.errors %}{{ form.new_password1.errors }}{% endif %}
  18
+<p class="aligned wide"><label for="id_new_password1">{% trans 'New password:' %}</label>{{ form.new_password1 }}</p>
  19
+{% if form.new_password2.errors %}{{ form.new_password2.errors }}{% endif %}
  20
+<p class="aligned wide"><label for="id_new_password2">{% trans 'Confirm password:' %}</label>{{ form.new_password2 }}</p>
  21
+<p><input type="submit" value="{% trans 'Change my password' %}" /></p>
  22
+</form>
  23
+
  24
+{% else %}
  25
+
  26
+<h1>{% trans 'Password reset unsuccessful' %}</h1>
  27
+
  28
+<p>{% trans "The password reset link was invalid, possibly because it has already been used.  Please request a new password reset." %}
  29
+
  30
+{% endif %}
  31
+
  32
+{% endblock %}
2  django/contrib/admin/templates/registration/password_reset_done.html
@@ -9,6 +9,6 @@
9 9
 
10 10
 <h1>{% trans 'Password reset successful' %}</h1>
11 11
 
12  
-<p>{% trans "We've e-mailed a new password to the e-mail address you submitted. You should be receiving it shortly." %}</p>
  12
+<p>{% trans "We've e-mailed you instructions for setting your password to the e-mail address you submitted. You should be receiving it shortly." %}</p>
13 13
 
14 14
 {% endblock %}
14  django/contrib/admin/templates/registration/password_reset_email.html
... ...
@@ -1,15 +1,15 @@
1  
-{% load i18n %}
  1
+{% load i18n %}{% autoescape off %}
2 2
 {% trans "You're receiving this e-mail because you requested a password reset" %}
3 3
 {% blocktrans %}for your user account at {{ site_name }}{% endblocktrans %}.
4 4
 
5  
-{% blocktrans %}Your new password is: {{ new_password }}{% endblocktrans %}
6  
-
7  
-{% trans "Feel free to change this password by going to this page:" %}
8  
-
9  
-http://{{ domain }}/password_change/
10  
-
  5
+{% trans "Please go to the following page and choose a new password:" %}
  6
+{% block reset_link %}
  7
+{{ protocol }}://{{ domain }}/reset/{{ uid }}-{{ token }}/
  8
+{% endblock %}
11 9
 {% trans "Your username, in case you've forgotten:" %} {{ user.username }}
12 10
 
13 11
 {% trans "Thanks for using our site!" %}
14 12
 
15 13
 {% blocktrans %}The {{ site_name }} team{% endblocktrans %}
  14
+
  15
+{% endautoescape %}
2  django/contrib/admin/templates/registration/password_reset_form.html
@@ -9,7 +9,7 @@
9 9
 
10 10
 <h1>{% trans "Password reset" %}</h1>
11 11
 
12  
-<p>{% trans "Forgotten your password? Enter your e-mail address below, and we'll reset your password and e-mail the new one to you." %}</p>
  12
+<p>{% trans "Forgotten your password? Enter your e-mail address below, and we'll e-mail instructions for setting a new one." %}</p>
13 13
 
14 14
 <form action="" method="post">
15 15
 {% if form.email.errors %}{{ form.email.errors }}{% endif %}
62  django/contrib/auth/forms.py
... ...
@@ -1,9 +1,11 @@
1 1
 from django.contrib.auth.models import User
2 2
 from django.contrib.auth import authenticate
  3
+from django.contrib.auth.tokens import default_token_generator
3 4
 from django.contrib.sites.models import Site
4 5
 from django.template import Context, loader
5 6
 from django import forms
6 7
 from django.utils.translation import ugettext_lazy as _
  8
+from django.utils.http import int_to_base36
7 9
 
8 10
 class UserCreationForm(forms.ModelForm):
9 11
     """
@@ -97,16 +99,14 @@ def clean_email(self):
97 99
         self.users_cache = User.objects.filter(email__iexact=email)
98 100
         if len(self.users_cache) == 0:
99 101
             raise forms.ValidationError(_("That e-mail address doesn't have an associated user account. Are you sure you've registered?"))
100  
-    
101  
-    def save(self, domain_override=None, email_template_name='registration/password_reset_email.html'):
  102
+
  103
+    def save(self, domain_override=None, email_template_name='registration/password_reset_email.html',
  104
+             use_https=False, token_generator=default_token_generator):
102 105
         """
103  
-        Calculates a new password randomly and sends it to the user.
  106
+        Generates a one-use only link for restting password and sends to the user
104 107
         """
105 108
         from django.core.mail import send_mail
106 109
         for user in self.users_cache:
107  
-            new_pass = User.objects.make_random_password()
108  
-            user.set_password(new_pass)
109  
-            user.save()
110 110
             if not domain_override:
111 111
                 current_site = Site.objects.get_current()
112 112
                 site_name = current_site.name
@@ -115,36 +115,29 @@ def save(self, domain_override=None, email_template_name='registration/password_
115 115
                 site_name = domain = domain_override
116 116
             t = loader.get_template(email_template_name)
117 117
             c = {
118  
-                'new_password': new_pass,
119 118
                 'email': user.email,
120 119
                 'domain': domain,
121 120
                 'site_name': site_name,
  121
+                'uid': int_to_base36(user.id),
122 122
                 'user': user,
  123
+                'token': token_generator.make_token(user),
  124
+                'protocol': use_https and 'https' or 'http',
123 125
             }
124 126
             send_mail(_("Password reset on %s") % site_name,
125 127
                 t.render(Context(c)), None, [user.email])
126 128
 
127  
-class PasswordChangeForm(forms.Form):
  129
+class SetPasswordForm(forms.Form):
128 130
     """
129  
-    A form that lets a user change his/her password.
  131
+    A form that lets a user change set his/her password without
  132
+    entering the old password
130 133
     """
131  
-    old_password = forms.CharField(label=_("Old password"), max_length=30, widget=forms.PasswordInput)
132  
-    new_password1 = forms.CharField(label=_("New password"), max_length=30, widget=forms.PasswordInput)
133  
-    new_password2 = forms.CharField(label=_("New password confirmation"), max_length=30, widget=forms.PasswordInput)
134  
-    
  134
+    new_password1 = forms.CharField(label=_("New password"), max_length=60, widget=forms.PasswordInput)
  135
+    new_password2 = forms.CharField(label=_("New password confirmation"), max_length=60, widget=forms.PasswordInput)
  136
+
135 137
     def __init__(self, user, *args, **kwargs):
136 138
         self.user = user
137  
-        super(PasswordChangeForm, self).__init__(*args, **kwargs)
138  
-    
139  
-    def clean_old_password(self):
140  
-        """
141  
-        Validates that the old_password field is correct.
142  
-        """
143  
-        old_password = self.cleaned_data["old_password"]
144  
-        if not self.user.check_password(old_password):
145  
-            raise forms.ValidationError(_("Your old password was entered incorrectly. Please enter it again."))
146  
-        return old_password
147  
-    
  139
+        super(SetPasswordForm, self).__init__(*args, **kwargs)
  140
+
148 141
     def clean_new_password2(self):
149 142
         password1 = self.cleaned_data.get('new_password1')
150 143
         password2 = self.cleaned_data.get('new_password2')
@@ -152,13 +145,30 @@ def clean_new_password2(self):
152 145
             if password1 != password2:
153 146
                 raise forms.ValidationError(_("The two password fields didn't match."))
154 147
         return password2
155  
-    
  148
+
156 149
     def save(self, commit=True):
157 150
         self.user.set_password(self.cleaned_data['new_password1'])
158 151
         if commit:
159 152
             self.user.save()
160 153
         return self.user
161  
-
  154
+    
  155
+class PasswordChangeForm(SetPasswordForm):
  156
+    """
  157
+    A form that lets a user change his/her password by entering
  158
+    their old password.
  159
+    """
  160
+    old_password = forms.CharField(label=_("Old password"), max_length=60, widget=forms.PasswordInput)
  161
+    
  162
+    def clean_old_password(self):
  163
+        """
  164
+        Validates that the old_password field is correct.
  165
+        """
  166
+        old_password = self.cleaned_data["old_password"]
  167
+        if not self.user.check_password(old_password):
  168
+            raise forms.ValidationError(_("Your old password was entered incorrectly. Please enter it again."))
  169
+        return old_password
  170
+PasswordChangeForm.base_fields.keyOrder = ['old_password', 'new_password1', 'new_password2']
  171
+    
162 172
 class AdminPasswordChangeForm(forms.Form):
163 173
     """
164 174
     A form used to change the password of a user in the admin interface.
5  django/contrib/auth/tests/__init__.py
... ...
@@ -1,8 +1,11 @@
1  
-from django.contrib.auth.tests.basic import BASIC_TESTS, PasswordResetTest
  1
+from django.contrib.auth.tests.basic import BASIC_TESTS
  2
+from django.contrib.auth.tests.views import PasswordResetTest
2 3
 from django.contrib.auth.tests.forms import FORM_TESTS
  4
+from django.contrib.auth.tests.tokens import TOKEN_GENERATOR_TESTS
3 5
 
4 6
 __test__ = {
5 7
     'BASIC_TESTS': BASIC_TESTS,
6 8
     'PASSWORDRESET_TESTS': PasswordResetTest,
7 9
     'FORM_TESTS': FORM_TESTS,
  10
+    'TOKEN_GENERATOR_TESTS': TOKEN_GENERATOR_TESTS
8 11
 }
21  django/contrib/auth/tests/basic.py
@@ -54,24 +54,3 @@
54 54
 >>> u.password
55 55
 u'!'
56 56
 """
57  
-
58  
-from django.test import TestCase
59  
-from django.core import mail
60  
-
61  
-class PasswordResetTest(TestCase):
62  
-    fixtures = ['authtestdata.json']
63  
-    urls = 'django.contrib.auth.urls'
64  
-    
65  
-    def test_email_not_found(self):
66  
-        "Error is raised if the provided email address isn't currently registered"
67  
-        response = self.client.get('/password_reset/')
68  
-        self.assertEquals(response.status_code, 200)
69  
-        response = self.client.post('/password_reset/', {'email': 'not_a_real_email@email.com'})
70  
-        self.assertContains(response, "That e-mail address doesn't have an associated user account")
71  
-        self.assertEquals(len(mail.outbox), 0)
72  
-    
73  
-    def test_email_found(self):
74  
-        "Email is sent if a valid email address is provided for password reset"
75  
-        response = self.client.post('/password_reset/', {'email': 'staffmember@example.com'})
76  
-        self.assertEquals(response.status_code, 302)
77  
-        self.assertEquals(len(mail.outbox), 1)
33  django/contrib/auth/tests/forms.py
@@ -2,7 +2,7 @@
2 2
 FORM_TESTS = """
3 3
 >>> from django.contrib.auth.models import User
4 4
 >>> from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
5  
->>> from django.contrib.auth.forms import PasswordChangeForm
  5
+>>> from django.contrib.auth.forms import PasswordChangeForm, SetPasswordForm
6 6
 
7 7
 The user already exists.
8 8
 
@@ -95,6 +95,32 @@
95 95
 >>> form.non_field_errors()
96 96
 []
97 97
 
  98
+SetPasswordForm:
  99
+
  100
+The two new passwords do not match.
  101
+
  102
+>>> data = {
  103
+...     'new_password1': 'abc123',
  104
+...     'new_password2': 'abc',
  105
+... }
  106
+>>> form = SetPasswordForm(user, data)
  107
+>>> form.is_valid()
  108
+False
  109
+>>> form["new_password2"].errors
  110
+[u"The two password fields didn't match."]
  111
+
  112
+The success case.
  113
+
  114
+>>> data = {
  115
+...     'new_password1': 'abc123',
  116
+...     'new_password2': 'abc123',
  117
+... }
  118
+>>> form = SetPasswordForm(user, data)
  119
+>>> form.is_valid()
  120
+True
  121
+
  122
+PasswordChangeForm:
  123
+
98 124
 The old password is incorrect.
99 125
 
100 126
 >>> data = {
@@ -132,4 +158,9 @@
132 158
 >>> form.is_valid()
133 159
 True
134 160
 
  161
+Regression test - check the order of fields:
  162
+
  163
+>>> PasswordChangeForm(user, {}).fields.keys()
  164
+['old_password', 'new_password1', 'new_password2']
  165
+
135 166
 """
29  django/contrib/auth/tests/tokens.py
... ...
@@ -0,0 +1,29 @@
  1
+TOKEN_GENERATOR_TESTS = """
  2
+>>> from django.contrib.auth.models import User, AnonymousUser
  3
+>>> from django.contrib.auth.tokens import PasswordResetTokenGenerator
  4
+>>> from django.conf import settings
  5
+>>> u = User.objects.create_user('tokentestuser', 'test2@example.com', 'testpw')
  6
+>>> p0 = PasswordResetTokenGenerator()
  7
+>>> tk1 = p0.make_token(u)
  8
+>>> p0.check_token(u, tk1)
  9
+True
  10
+
  11
+Tests to ensure we can use the token after n days, but no greater.
  12
+Use a mocked version of PasswordResetTokenGenerator so we can change
  13
+the value of 'today'
  14
+
  15
+>>> class Mocked(PasswordResetTokenGenerator):
  16
+...     def __init__(self, today):
  17
+...         self._today_val = today
  18
+...     def _today(self):
  19
+...         return self._today_val
  20
+
  21
+>>> from datetime import date, timedelta
  22
+>>> p1 = Mocked(date.today() + timedelta(settings.PASSWORD_RESET_TIMEOUT_DAYS))
  23
+>>> p1.check_token(u, tk1)
  24
+True
  25
+>>> p2 = Mocked(date.today() + timedelta(settings.PASSWORD_RESET_TIMEOUT_DAYS + 1))
  26
+>>> p2.check_token(u, tk1)
  27
+False
  28
+
  29
+"""
88  django/contrib/auth/tests/views.py
... ...
@@ -0,0 +1,88 @@
  1
+
  2
+import re
  3
+from django.contrib.auth.models import User
  4
+from django.test import TestCase
  5
+from django.core import mail
  6
+
  7
+class PasswordResetTest(TestCase):
  8
+    fixtures = ['authtestdata.json']
  9
+    urls = 'django.contrib.auth.urls'
  10
+    
  11
+    def test_email_not_found(self):
  12
+        "Error is raised if the provided email address isn't currently registered"
  13
+        response = self.client.get('/password_reset/')
  14
+        self.assertEquals(response.status_code, 200)
  15
+        response = self.client.post('/password_reset/', {'email': 'not_a_real_email@email.com'})
  16
+        self.assertContains(response, "That e-mail address doesn't have an associated user account")
  17
+        self.assertEquals(len(mail.outbox), 0)
  18
+    
  19
+    def test_email_found(self):
  20
+        "Email is sent if a valid email address is provided for password reset"
  21
+        response = self.client.post('/password_reset/', {'email': 'staffmember@example.com'})
  22
+        self.assertEquals(response.status_code, 302)
  23
+        self.assertEquals(len(mail.outbox), 1)
  24
+        self.assert_("http://" in mail.outbox[0].body)
  25
+
  26
+    def _test_confirm_start(self):
  27
+        # Start by creating the email
  28
+        response = self.client.post('/password_reset/', {'email': 'staffmember@example.com'})
  29
+        self.assertEquals(response.status_code, 302)
  30
+        self.assertEquals(len(mail.outbox), 1)
  31
+        return self._read_signup_email(mail.outbox[0])
  32
+
  33
+    def _read_signup_email(self, email):
  34
+        urlmatch = re.search(r"https?://[^/]*(/.*reset/\S*)", email.body)
  35
+        self.assert_(urlmatch is not None, "No URL found in sent email")
  36
+        return urlmatch.group(), urlmatch.groups()[0]
  37
+
  38
+    def test_confirm_valid(self):
  39
+        url, path = self._test_confirm_start()
  40
+        response = self.client.get(path)
  41
+        # redirect to a 'complete' page:
  42
+        self.assertEquals(response.status_code, 200) 
  43
+        self.assert_("Please enter your new password" in response.content)
  44
+
  45
+    def test_confirm_invalid(self):
  46
+        url, path = self._test_confirm_start()
  47
+        # Lets munge the token in the path, but keep the same length,
  48
+        # in case the URL conf will reject a different length
  49
+        path = path[:-5] + ("0"*4) + path[-1]
  50
+
  51
+        response = self.client.get(path)
  52
+        self.assertEquals(response.status_code, 200) 
  53
+        self.assert_("The password reset link was invalid" in response.content)
  54
+
  55
+    def test_confirm_invalid_post(self):
  56
+        # Same as test_confirm_invalid, but trying
  57
+        # to do a POST instead.
  58
+        url, path = self._test_confirm_start()
  59
+        path = path[:-5] + ("0"*4) + path[-1]
  60
+
  61
+        response = self.client.post(path, {'new_password1': 'anewpassword',
  62
+                                           'new_password2':' anewpassword'})
  63
+        # Check the password has not been changed 
  64
+        u = User.objects.get(email='staffmember@example.com')
  65
+        self.assert_(not u.check_password("anewpassword"))
  66
+
  67
+    def test_confirm_complete(self):
  68
+        url, path = self._test_confirm_start()
  69
+        response = self.client.post(path, {'new_password1': 'anewpassword',
  70
+                                           'new_password2': 'anewpassword'})
  71
+        # It redirects us to a 'complete' page:
  72
+        self.assertEquals(response.status_code, 302) 
  73
+        # Check the password has been changed 
  74
+        u = User.objects.get(email='staffmember@example.com')
  75
+        self.assert_(u.check_password("anewpassword"))
  76
+
  77
+        # Check we can't use the link again
  78
+        response = self.client.get(path)
  79
+        self.assertEquals(response.status_code, 200) 
  80
+        self.assert_("The password reset link was invalid" in response.content)
  81
+
  82
+    def test_confirm_different_passwords(self):
  83
+        url, path = self._test_confirm_start()
  84
+        response = self.client.post(path, {'new_password1': 'anewpassword',
  85
+                                           'new_password2':' x'})
  86
+        self.assertEquals(response.status_code, 200)
  87
+        self.assert_("The two password fields didn't match" in response.content)
  88
+
66  django/contrib/auth/tokens.py
... ...
@@ -0,0 +1,66 @@
  1
+from datetime import date
  2
+from django.conf import settings
  3
+from django.utils.http import int_to_base36, base36_to_int
  4
+
  5
+class PasswordResetTokenGenerator(object):
  6
+    """
  7
+    Stratgy object used to generate and check tokens for the password
  8
+    reset mechanism.
  9
+    """
  10
+    def make_token(self, user):
  11
+        """
  12
+        Returns a token that can be used once to do a password reset
  13
+        for the given user.
  14
+        """
  15
+        return self._make_token_with_timestamp(user, self._num_days(self._today()))
  16
+
  17
+    def check_token(self, user, token):
  18
+        """
  19
+        Check that a password reset token is correct for a given user.
  20
+        """
  21
+        # Parse the tokem
  22
+        try:
  23
+            ts_b36, hash = token.split("-")
  24
+        except ValueError:
  25
+            return False
  26
+
  27
+        try:
  28
+            ts = base36_to_int(ts_b36)
  29
+        except ValueError:
  30
+            return False
  31
+
  32
+        # Check that the timestamp/uid has not been tampered with
  33
+        if self._make_token_with_timestamp(user, ts) != token:
  34
+            return False
  35
+
  36
+        # Check the timestamp is within limit
  37
+        if (self._num_days(self._today()) - ts) > settings.PASSWORD_RESET_TIMEOUT_DAYS:
  38
+            return False
  39
+
  40
+        return True
  41
+
  42
+    def _make_token_with_timestamp(self, user, timestamp):
  43
+        # timestamp is number of days since 2001-1-1.  Converted to
  44
+        # base 36, this gives us a 3 digit string until about 2121
  45
+        ts_b36 = int_to_base36(timestamp)
  46
+
  47
+        # By hashing on the internal state of the user and using state
  48
+        # that is sure to change (the password salt will change as soon as
  49
+        # the password is set, at least for current Django auth, and
  50
+        # last_login will also change), we produce a hash that will be
  51
+        # invalid as soon as it is used.
  52
+        # We limit the hash to 20 chars to keep URL short
  53
+        import sha
  54
+        hash = sha.new(settings.SECRET_KEY + unicode(user.id) + 
  55
+                       user.password + unicode(user.last_login) + 
  56
+                       unicode(timestamp)).hexdigest()[::2]
  57
+        return "%s-%s" % (ts_b36, hash)
  58
+
  59
+    def _num_days(self, dt):
  60
+        return (dt - date(2001,1,1)).days 
  61
+
  62
+    def _today(self):
  63
+        # Used for mocking in tests
  64
+        return date.today()        
  65
+
  66
+default_token_generator = PasswordResetTokenGenerator()
5  django/contrib/auth/urls.py
@@ -8,6 +8,9 @@
8 8
     ('^logout/$', 'django.contrib.auth.views.logout'),
9 9
     ('^password_change/$', 'django.contrib.auth.views.password_change'),
10 10
     ('^password_change/done/$', 'django.contrib.auth.views.password_change_done'),
11  
-    ('^password_reset/$', 'django.contrib.auth.views.password_reset')
  11
+    ('^password_reset/$', 'django.contrib.auth.views.password_reset'),
  12
+    ('^password_reset/done/$', 'django.contrib.auth.views.password_reset_done'),
  13
+    ('^reset/(?P<uidb36>[0-9A-Za-z]+)-(?P<token>.+)/$', 'django.contrib.auth.views.password_reset_confirm'),
  14
+    ('^reset/done/$', 'django.contrib.auth.views.password_reset_complete'),
12 15
 )
13 16
 
62  django/contrib/auth/views.py
... ...
@@ -1,13 +1,14 @@
1 1
 from django.contrib.auth import REDIRECT_FIELD_NAME
2 2
 from django.contrib.auth.decorators import login_required
3 3
 from django.contrib.auth.forms import AuthenticationForm
4  
-from django.contrib.auth.forms import PasswordResetForm, PasswordChangeForm, AdminPasswordChangeForm
  4
+from django.contrib.auth.forms import PasswordResetForm, SetPasswordForm, PasswordChangeForm, AdminPasswordChangeForm
  5
+from django.contrib.auth.tokens import default_token_generator
5 6
 from django.core.exceptions import PermissionDenied
6 7
 from django.shortcuts import render_to_response, get_object_or_404
7 8
 from django.contrib.sites.models import Site, RequestSite
8  
-from django.http import HttpResponseRedirect
  9
+from django.http import HttpResponseRedirect, Http404
9 10
 from django.template import RequestContext
10  
-from django.utils.http import urlquote
  11
+from django.utils.http import urlquote, base36_to_int
11 12
 from django.utils.html import escape
12 13
 from django.utils.translation import ugettext as _
13 14
 from django.contrib.auth.models import User
@@ -65,19 +66,29 @@ def redirect_to_login(next, login_url=None, redirect_field_name=REDIRECT_FIELD_N
65 66
         login_url = settings.LOGIN_URL
66 67
     return HttpResponseRedirect('%s?%s=%s' % (login_url, urlquote(redirect_field_name), urlquote(next)))
67 68
 
  69
+# 4 views for password reset:
  70
+# - password_reset sends the mail
  71
+# - password_reset_done shows a success message for the above
  72
+# - password_reset_confirm checks the link the user clicked and 
  73
+#   prompts for a new password
  74
+# - password_reset_complete shows a success message for the above
  75
+
68 76
 def password_reset(request, is_admin_site=False, template_name='registration/password_reset_form.html',
69 77
         email_template_name='registration/password_reset_email.html',
70  
-        password_reset_form=PasswordResetForm):
  78
+        password_reset_form=PasswordResetForm, token_generator=default_token_generator):
71 79
     if request.method == "POST":
72 80
         form = password_reset_form(request.POST)
73 81
         if form.is_valid():
  82
+            opts = {}
  83
+            opts['use_https'] = request.is_secure()
  84
+            opts['token_generator'] = token_generator
74 85
             if is_admin_site:
75  
-                form.save(domain_override=request.META['HTTP_HOST'])
  86
+                opts['domain_override'] = request.META['HTTP_HOST']
76 87
             else:
77  
-                if Site._meta.installed:
78  
-                    form.save(email_template_name=email_template_name)
79  
-                else:
80  
-                    form.save(domain_override=RequestSite(request).domain, email_template_name=email_template_name)
  88
+                opts['email_template_name'] = email_template_name
  89
+                if not Site._meta.installed:
  90
+                    opts['domain_override'] = RequestSite(request).domain
  91
+            form.save(**opts)
81 92
             return HttpResponseRedirect('%sdone/' % request.path)
82 93
     else:
83 94
         form = password_reset_form()
@@ -88,6 +99,39 @@ def password_reset(request, is_admin_site=False, template_name='registration/pas
88 99
 def password_reset_done(request, template_name='registration/password_reset_done.html'):
89 100
     return render_to_response(template_name, context_instance=RequestContext(request))
90 101
 
  102
+def password_reset_confirm(request, uidb36=None, token=None, template_name='registration/password_reset_confirm.html',
  103
+                           token_generator=default_token_generator, set_password_form=SetPasswordForm):
  104
+    """
  105
+    View that checks the hash in a password reset link and presents a
  106
+    form for entering a new password.
  107
+    """
  108
+    assert uidb36 is not None and token is not None # checked by URLconf
  109
+    try:
  110
+        uid_int = base36_to_int(uidb36)
  111
+    except ValueError:
  112
+        raise Http404
  113
+
  114
+    user = get_object_or_404(User, id=uid_int)
  115
+    context_instance = RequestContext(request)
  116
+
  117
+    if token_generator.check_token(user, token):
  118
+        context_instance['validlink'] = True
  119
+        if request.method == 'POST':
  120
+            form = set_password_form(user, request.POST)
  121
+            if form.is_valid():
  122
+                form.save()
  123
+                return HttpResponseRedirect("../done/")
  124
+        else:
  125
+            form = set_password_form(None)
  126
+    else:
  127
+        context_instance['validlink'] = False
  128
+        form = None
  129
+    context_instance['form'] = form    
  130
+    return render_to_response(template_name, context_instance=context_instance)
  131
+
  132
+def password_reset_complete(request, template_name='registration/password_reset_complete.html'):
  133
+    return render_to_response(template_name, context_instance=RequestContext(request))
  134
+
91 135
 def password_change(request, template_name='registration/password_change_form.html'):
92 136
     if request.method == "POST":
93 137
         form = PasswordChangeForm(request.user, request.POST)
29  django/utils/http.py
@@ -65,3 +65,32 @@ def http_date(epoch_seconds=None):
65 65
     """
66 66
     rfcdate = formatdate(epoch_seconds)
67 67
     return '%s GMT' % rfcdate[:25]
  68
+
  69
+# Base 36 functions: useful for generating compact URLs
  70
+
  71
+def base36_to_int(s):
  72
+    """
  73
+    Convertd a base 36 string to an integer
  74
+    """
  75
+    return int(s, 36)
  76
+
  77
+def int_to_base36(i):
  78
+    """
  79
+    Converts an integer to a base36 string
  80
+    """
  81
+    digits = "0123456789abcdefghijklmnopqrstuvwxyz"
  82
+    factor = 0
  83
+    # Find starting factor
  84
+    while True:
  85
+        factor += 1
  86
+        if i < 36 ** factor:
  87
+            factor -= 1
  88
+            break
  89
+    base36 = []
  90
+    # Construct base36 representation
  91
+    while factor >= 0:
  92
+        j = 36 ** factor
  93
+        base36.append(digits[i / j])
  94
+        i = i % j
  95
+        factor -= 1
  96
+    return ''.join(base36)

0 notes on commit fcd837c

Please sign in to comment.
Something went wrong with that request. Please try again.