commit 602e933874aff1d618e60920ac862d272d3e0eaf
Author: Jonas Haag <jonas@lophus.org>
Date: Wed Sep 28 21:56:51 2011 +0200
Non-integer-pk auth password reset
diff --git a/django/contrib/admin/templates/registration/password_reset_email.html b/django/contrib/admin/templates/registration/password_reset_email.html
index de9dc79..665ea11 100644
a
|
b
|
|
3 | 3 | |
4 | 4 | {% trans "Please go to the following page and choose a new password:" %} |
5 | 5 | {% block reset_link %} |
6 | | {{ protocol }}://{{ domain }}{% url 'django.contrib.auth.views.password_reset_confirm' uidb36=uid token=token %} |
| 6 | {{ protocol }}://{{ domain }}{% url 'django.contrib.auth.views.password_reset_confirm' uidb64=uid token=token %} |
7 | 7 | {% endblock %} |
8 | 8 | {% trans "Your username, in case you've forgotten:" %} {{ user.username }} |
9 | 9 | |
diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py
index b97c5d7..3864341 100644
a
|
b
|
|
1 | 1 | from django import forms |
2 | 2 | from django.template import loader |
3 | | from django.utils.http import int_to_base36 |
| 3 | from django.utils.http import urlsafe_base64_encode |
4 | 4 | from django.utils.translation import ugettext_lazy as _ |
5 | 5 | |
6 | 6 | from django.contrib.auth.models import User |
… |
… |
class PasswordResetForm(forms.Form):
|
144 | 144 | 'email': user.email, |
145 | 145 | 'domain': domain, |
146 | 146 | 'site_name': site_name, |
147 | | 'uid': int_to_base36(user.id), |
| 147 | 'uid': urlsafe_base64_encode(str(user.id)), |
148 | 148 | 'user': user, |
149 | 149 | 'token': token_generator.make_token(user), |
150 | 150 | 'protocol': use_https and 'https' or 'http', |
diff --git a/django/contrib/auth/tests/templates/registration/password_reset_email.html b/django/contrib/auth/tests/templates/registration/password_reset_email.html
index 1b9a482..baac2fc 100644
a
|
b
|
|
1 | | {{ protocol }}://{{ domain }}/reset/{{ uid }}-{{ token }}/ |
2 | | No newline at end of file |
| 1 | {{ protocol }}://{{ domain }}/reset/{{ uid }}/{{ token }}/ |
diff --git a/django/contrib/auth/tests/views.py b/django/contrib/auth/tests/views.py
index a0be92f..c536d30 100644
a
|
b
|
class PasswordResetTest(AuthViewsTestCase):
|
129 | 129 | |
130 | 130 | def test_confirm_invalid_user(self): |
131 | 131 | # Ensure that we get a 200 response for a non-existant user, not a 404 |
132 | | response = self.client.get('/reset/123456-1-1/') |
| 132 | response = self.client.get('/reset/123456/1-1/') |
133 | 133 | self.assertEqual(response.status_code, 200) |
134 | 134 | self.assertTrue("The password reset link was invalid" in response.content) |
135 | 135 | |
136 | 136 | def test_confirm_overflow_user(self): |
137 | 137 | # Ensure that we get a 200 response for a base36 user id that overflows int |
138 | | response = self.client.get('/reset/zzzzzzzzzzzzz-1-1/') |
| 138 | response = self.client.get('/reset/zzzzzzzzzzzzz/1-1/') |
139 | 139 | self.assertEqual(response.status_code, 200) |
140 | 140 | self.assertTrue("The password reset link was invalid" in response.content) |
141 | 141 | |
diff --git a/django/contrib/auth/urls.py b/django/contrib/auth/urls.py
index c5e87ed..85b8b28 100644
a
|
b
|
urlpatterns = patterns('',
|
12 | 12 | url(r'^password_change/done/$', 'django.contrib.auth.views.password_change_done', name='password_change_done'), |
13 | 13 | url(r'^password_reset/$', 'django.contrib.auth.views.password_reset', name='password_reset'), |
14 | 14 | url(r'^password_reset/done/$', 'django.contrib.auth.views.password_reset_done', name='password_reset_done'), |
15 | | url(r'^reset/(?P<uidb36>[0-9A-Za-z]{1,13})-(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$', |
| 15 | url(r'^reset/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$', |
16 | 16 | 'django.contrib.auth.views.password_reset_confirm', |
17 | 17 | name='password_reset_confirm'), |
18 | 18 | url(r'^reset/done/$', 'django.contrib.auth.views.password_reset_complete', name='password_reset_complete'), |
diff --git a/django/contrib/auth/views.py b/django/contrib/auth/views.py
index c86ef53..86c0d47 100644
a
|
b
|
from django.conf import settings
|
4 | 4 | from django.core.urlresolvers import reverse |
5 | 5 | from django.http import HttpResponseRedirect, QueryDict |
6 | 6 | from django.template.response import TemplateResponse |
7 | | from django.utils.http import base36_to_int |
| 7 | from django.utils.http import urlsafe_base64_decode |
8 | 8 | from django.utils.translation import ugettext as _ |
9 | 9 | from django.views.decorators.debug import sensitive_post_parameters |
10 | 10 | from django.views.decorators.cache import never_cache |
… |
… |
def password_reset_done(request,
|
181 | 181 | # Doesn't need csrf_protect since no-one can guess the URL |
182 | 182 | @sensitive_post_parameters() |
183 | 183 | @never_cache |
184 | | def password_reset_confirm(request, uidb36=None, token=None, |
| 184 | def password_reset_confirm(request, uidb64=None, token=None, |
185 | 185 | template_name='registration/password_reset_confirm.html', |
186 | 186 | token_generator=default_token_generator, |
187 | 187 | set_password_form=SetPasswordForm, |
… |
… |
def password_reset_confirm(request, uidb36=None, token=None,
|
191 | 191 | View that checks the hash in a password reset link and presents a |
192 | 192 | form for entering a new password. |
193 | 193 | """ |
194 | | assert uidb36 is not None and token is not None # checked by URLconf |
| 194 | assert uidb64 is not None and token is not None # checked by URLconf |
195 | 195 | if post_reset_redirect is None: |
196 | 196 | post_reset_redirect = reverse('django.contrib.auth.views.password_reset_complete') |
197 | 197 | try: |
198 | | uid_int = base36_to_int(uidb36) |
199 | | user = User.objects.get(id=uid_int) |
200 | | except (ValueError, User.DoesNotExist): |
| 198 | uid = urlsafe_base64_decode(str(uidb64)) |
| 199 | user = User.objects.get(id=uid) |
| 200 | except (TypeError, ValueError, User.DoesNotExist): |
201 | 201 | user = None |
202 | 202 | |
203 | 203 | if user is not None and token_generator.check_token(user, token): |
diff --git a/django/utils/http.py b/django/utils/http.py
index af44ff4..b1bb7bf 100644
a
|
b
|
|
| 1 | import base64 |
1 | 2 | import calendar |
2 | 3 | import datetime |
3 | 4 | import re |
4 | 5 | import sys |
5 | 6 | import urllib |
6 | 7 | import urlparse |
| 8 | from binascii import Error as BinasciiError |
7 | 9 | from email.utils import formatdate |
8 | 10 | |
9 | 11 | from django.utils.datastructures import MultiValueDict |
… |
… |
def int_to_base36(i):
|
171 | 173 | factor -= 1 |
172 | 174 | return ''.join(base36) |
173 | 175 | |
| 176 | def urlsafe_base64_encode(s): |
| 177 | return base64.urlsafe_b64encode(s).rstrip('\n=') |
| 178 | |
| 179 | def urlsafe_base64_decode(s): |
| 180 | assert isinstance(s, str) |
| 181 | try: |
| 182 | return base64.urlsafe_b64decode(s.ljust(len(s) + len(s) % 4, '=')) |
| 183 | except (LookupError, BinasciiError), e: |
| 184 | raise ValueError(e) |
| 185 | |
174 | 186 | def parse_etags(etag_str): |
175 | 187 | """ |
176 | 188 | Parses a string with one or several etags passed in If-None-Match and |