Merge lp:~canonical-isd-hackers/canonical-identity-provider/bug_589335_auth_tokens into lp:canonical-identity-provider/release
- bug_589335_auth_tokens
- Merge into trunk
Proposed by
Łukasz Czyżykowski
Status: | Merged |
---|---|
Merged at revision: | 73 |
Proposed branch: | lp:~canonical-isd-hackers/canonical-identity-provider/bug_589335_auth_tokens |
Merge into: | lp:canonical-identity-provider/release |
Diff against target: |
1395 lines (+506/-196) 22 files modified
doctests/stories/openid/per-version/sso-workflow-complete.txt (+2/-2) doctests/stories/openid/per-version/sso-workflow-register.txt (+1/-1) doctests/stories/openid/per-version/sso-workflow-reset-password.txt (+1/-1) doctests/stories/sso-server/standalone-login.txt (+172/-46) identityprovider/forms.py (+20/-1) identityprovider/models/authtoken.py (+40/-4) identityprovider/templates/enter_token.html (+37/-0) identityprovider/templates/launchpad/email/forgottenpassword.txt (+4/-0) identityprovider/templates/launchpad/email/newuser.txt (+4/-0) identityprovider/templates/launchpad/email/validate-email.txt (+4/-0) identityprovider/templates/registration/confirm_new_account.html (+1/-1) identityprovider/templates/registration/email_sent.html (+16/-6) identityprovider/templates/registration/reset_password.html (+1/-1) identityprovider/templates/ubuntu/email/forgottenpassword.txt (+4/-0) identityprovider/templates/ubuntu/email/newuser.txt (+4/-0) identityprovider/templates/ubuntu/email/validate-email.txt (+4/-0) identityprovider/tests/test_views_ui.py (+79/-93) identityprovider/tests/utils.py (+2/-0) identityprovider/urls.py (+9/-4) identityprovider/views/account.py (+11/-5) identityprovider/views/ui.py (+79/-31) identityprovider/views/utils.py (+11/-0) |
To merge this branch: | bzr merge lp:~canonical-isd-hackers/canonical-identity-provider/bug_589335_auth_tokens |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Canonical ISD hackers | Pending | ||
Review via email: mp+27595@code.launchpad.net |
Commit message
Description of the change
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'doctests/stories/openid/per-version/sso-workflow-complete.txt' |
2 | --- doctests/stories/openid/per-version/sso-workflow-complete.txt 2010-04-21 15:29:24 +0000 |
3 | +++ doctests/stories/openid/per-version/sso-workflow-complete.txt 2010-06-22 23:23:25 +0000 |
4 | @@ -54,7 +54,7 @@ |
5 | >>> token_url = 'http://openid.launchpad.dev/token/%s' % token |
6 | >>> browser.open(token_url) |
7 | >>> print browser.url |
8 | - http://openid.launchpad.dev/token/.../+newaccount |
9 | + http://openid.launchpad.dev/token/.../+newaccount/new-user%40example.com |
10 | |
11 | >>> browser.getControl(name='displayname').value = 'New User' |
12 | >>> browser.getControl(name='password').value = 'testP4ss' |
13 | @@ -134,7 +134,7 @@ |
14 | >>> token_url = 'http://openid.launchpad.dev/token/%s' % token |
15 | >>> browser.open(token_url) |
16 | >>> print browser.url |
17 | - http://.../+resetpassword |
18 | + http://.../+resetpassword/new-user%40example.com |
19 | |
20 | >>> browser.getControl(name='password').value = 'test2...' |
21 | >>> browser.getControl(name='passwordconfirm').value = 'test2...' |
22 | |
23 | === modified file 'doctests/stories/openid/per-version/sso-workflow-register.txt' |
24 | --- doctests/stories/openid/per-version/sso-workflow-register.txt 2010-04-21 15:29:24 +0000 |
25 | +++ doctests/stories/openid/per-version/sso-workflow-register.txt 2010-06-22 23:23:25 +0000 |
26 | @@ -79,7 +79,7 @@ |
27 | >>> link = 'http://openid.launchpad.dev/token/%s' % token |
28 | >>> browser.open(link) |
29 | >>> print browser.url |
30 | - http://openid.launchpad.dev/token/.../+newaccount |
31 | + http://openid.launchpad.dev/token/.../+newaccount/new-user%40example.com |
32 | |
33 | The user can enter their full name and password, to complete the |
34 | registration: |
35 | |
36 | === modified file 'doctests/stories/openid/per-version/sso-workflow-reset-password.txt' |
37 | --- doctests/stories/openid/per-version/sso-workflow-reset-password.txt 2010-04-21 15:29:24 +0000 |
38 | +++ doctests/stories/openid/per-version/sso-workflow-reset-password.txt 2010-06-22 23:23:25 +0000 |
39 | @@ -78,7 +78,7 @@ |
40 | >>> link = 'http://openid.launchpad.dev/token/%s' % token |
41 | >>> browser.open(link) |
42 | >>> print browser.url |
43 | - http://openid.launchpad.dev/token/.../+resetpassword |
44 | + http://openid.launchpad.dev/token/.../+resetpassword/test%40canonical.com |
45 | |
46 | The user can now enter a new password: |
47 | |
48 | |
49 | === modified file 'doctests/stories/sso-server/standalone-login.txt' |
50 | --- doctests/stories/sso-server/standalone-login.txt 2010-05-21 21:10:59 +0000 |
51 | +++ doctests/stories/sso-server/standalone-login.txt 2010-06-22 23:23:25 +0000 |
52 | @@ -13,9 +13,13 @@ |
53 | Log in to Ubuntu Single Sign On |
54 | ... |
55 | |
56 | - >>> browser.getControl(name='email').value = 'mark@example.com' |
57 | - >>> browser.getControl(name='password').value = 'test' |
58 | - >>> browser.getControl(name='continue').click() |
59 | + >>> def login(browser, email=None, password=None): |
60 | + ... browser.open('http://openid.launchpad.dev') |
61 | + ... browser.getControl(name='email').value = email |
62 | + ... browser.getControl(name='password').value = password |
63 | + ... browser.getControl(name='continue').click() |
64 | + |
65 | + >>> login(browser, email='mark@example.com', password='test') |
66 | |
67 | Once successfully logged in, they will see the details of their account. |
68 | |
69 | @@ -30,37 +34,51 @@ |
70 | |
71 | >>> from identityprovider.models.account import AccountManager |
72 | >>> from identityprovider.models.emailaddress import EmailAddress |
73 | - >>> try: |
74 | - ... EmailAddress.objects.filter(email='new-user@example.com').delete() |
75 | - ... except: |
76 | - ... pass |
77 | - >>> try: |
78 | - ... AccountManager().get_by_email('new-user@example.com').delete() |
79 | - ... except: |
80 | - ... pass |
81 | + |
82 | + >>> def register(browser): |
83 | + ... try: |
84 | + ... EmailAddress.objects.filter( |
85 | + ... email='new-user@example.com').delete() |
86 | + ... except: |
87 | + ... pass |
88 | + ... try: |
89 | + ... AccountManager().get_by_email('new-user@example.com').delete() |
90 | + ... except: |
91 | + ... pass |
92 | + ... browser.open('http://openid.launchpad.dev') |
93 | + ... browser.getLink('New account').click() |
94 | + ... browser.getControl(name='email').value = 'new-user@example.com' |
95 | + ... browser.getControl(name='continue').click() |
96 | + |
97 | >>> browser = setupBrowser() |
98 | - >>> browser.open('http://openid.launchpad.dev') |
99 | - >>> browser.getLink('New account').click() |
100 | - >>> browser.getControl(name='email').value = 'new-user@example.com' |
101 | - >>> browser.getControl(name='continue').click() |
102 | + >>> register(browser) |
103 | + |
104 | >>> browser.contents.find("Registration mail sent") > 0 |
105 | True |
106 | |
107 | >>> from identityprovider.models.const import LoginTokenType |
108 | >>> from identityprovider.models import AuthToken |
109 | - >>> token = AuthToken.objects.filter( |
110 | - ... email='new-user@example.com', |
111 | - ... token_type=LoginTokenType.NEWPERSONLESSACCOUNT, |
112 | - ... date_consumed=None).order_by('-date_created')[0].token |
113 | + >>> def find_my_token(type, email='new-user@example.com'): |
114 | + ... return AuthToken.objects.filter( |
115 | + ... email=email, |
116 | + ... token_type=type, |
117 | + ... date_consumed=None).order_by('-date_created')[0].token |
118 | + |
119 | + >>> token = find_my_token(LoginTokenType.NEWPERSONLESSACCOUNT) |
120 | >>> link = 'http://openid.launchpad.dev/token/%s' % token |
121 | >>> browser.open(link) |
122 | >>> print browser.url |
123 | - http://openid.launchpad.dev/token/.../+newaccount |
124 | - |
125 | - >>> browser.getControl(name='displayname').value = 'New User' |
126 | - >>> browser.getControl(name='password').value = 'testP4ss' |
127 | - >>> browser.getControl(name='passwordconfirm').value = 'testP4ss' |
128 | - >>> browser.getControl(name='continue').click() |
129 | + http://openid.launchpad.dev/token/.../+newaccount/new-user%40example.com |
130 | + |
131 | +Finish the registration process. |
132 | + |
133 | + >>> def finish_registration(browser): |
134 | + ... browser.getControl(name='displayname').value = 'New User' |
135 | + ... browser.getControl(name='password').value = 'testP4ss' |
136 | + ... browser.getControl(name='passwordconfirm').value = 'testP4ss' |
137 | + ... browser.getControl(name='continue').click() |
138 | + |
139 | + >>> finish_registration(browser) |
140 | |
141 | >>> print browser.url |
142 | http://openid.launchpad.dev/ |
143 | @@ -70,42 +88,150 @@ |
144 | >>> print AccountManager().get_by_email(email='new-user@example.com').person |
145 | None |
146 | |
147 | + |
148 | +=== Confirm out of session === |
149 | + |
150 | +Begin registration as before. |
151 | + |
152 | + >>> browser = setupBrowser() |
153 | + >>> register(browser) |
154 | + |
155 | + >>> browser.contents.find("Registration mail sent") > 0 |
156 | + True |
157 | + |
158 | +But now, try to confirm the new account from out of the original |
159 | +session. |
160 | + |
161 | + >>> browser = setupBrowser() |
162 | + >>> token = find_my_token(LoginTokenType.NEWPERSONLESSACCOUNT) |
163 | + >>> link = 'http://openid.launchpad.dev/token/%s' % token |
164 | + >>> browser.open(link) |
165 | + |
166 | +It should prompt for the e-mail that the token was sent to, as a |
167 | +security precaution. |
168 | + |
169 | + >>> print browser.url |
170 | + http://.../+enter_token?token=... |
171 | + |
172 | +Fill in the e-mail address, then continue as before. |
173 | + |
174 | + >>> browser.getControl(name='email').value = 'new-user@example.com' |
175 | + >>> browser.getControl(name='continue').click() |
176 | + >>> print browser.url |
177 | + http://openid.launchpad.dev/token/.../+newaccount/new-user%40example.com |
178 | + |
179 | + >>> finish_registration(browser) |
180 | + |
181 | + >>> print browser.url |
182 | + http://openid.launchpad.dev/ |
183 | + |
184 | + |
185 | == Resetting the password == |
186 | |
187 | + >>> def request_reset(browser): |
188 | + ... browser.open('http://openid.launchpad.dev') |
189 | + ... browser.getLink('Forgot your password?').click() |
190 | + ... browser.getControl(name='email').value = 'new-user@example.com' |
191 | + ... browser.getControl(name='continue').click() |
192 | + |
193 | >>> browser = setupBrowser() |
194 | - >>> browser.open('http://openid.launchpad.dev') |
195 | - >>> browser.getLink('Forgot your password?').click() |
196 | - >>> browser.getControl(name='email').value = 'new-user@example.com' |
197 | - >>> browser.getControl(name='continue').click() |
198 | + >>> request_reset(browser) |
199 | >>> browser.contents.find("Forgotten your password?") > 0 |
200 | True |
201 | |
202 | >>> from identityprovider.models.const import LoginTokenType |
203 | >>> from identityprovider.models import AuthToken |
204 | - >>> token = AuthToken.objects.filter( |
205 | - ... email='new-user@example.com', |
206 | - ... token_type=LoginTokenType.PASSWORDRECOVERY, |
207 | - ... date_consumed=None).order_by('-date_created')[0].token |
208 | + >>> token = find_my_token(LoginTokenType.PASSWORDRECOVERY) |
209 | >>> link = 'http://openid.launchpad.dev/token/%s' % token |
210 | >>> browser.open(link) |
211 | >>> print browser.url |
212 | - http://openid.launchpad.dev/token/.../+resetpassword |
213 | - |
214 | - >>> browser.getControl(name='password').value = 'new Password' |
215 | - >>> browser.getControl(name='passwordconfirm').value = 'new Password' |
216 | - >>> browser.getControl(name='continue').click() |
217 | - |
218 | + http://openid.launchpad.dev/token/.../+resetpassword/new-user%40example.com |
219 | + |
220 | + >>> def set_new_password(browser): |
221 | + ... browser.getControl(name='password').value = 'new Password' |
222 | + ... browser.getControl(name='passwordconfirm').value = 'new Password' |
223 | + ... browser.getControl(name='continue').click() |
224 | + |
225 | + >>> set_new_password(browser) |
226 | >>> print browser.url |
227 | http://openid.launchpad.dev/ |
228 | |
229 | - |
230 | -=== Logging in with the new password === |
231 | - |
232 | - >>> browser = setupBrowser() |
233 | - >>> browser.open('http://openid.launchpad.dev') |
234 | +Log in with the new password. |
235 | + |
236 | + >>> browser = setupBrowser() |
237 | + >>> login(browser, email='new-user@example.com', password='new Password') |
238 | + >>> print extract_text(find_tag_by_id(browser.contents, 'content')) |
239 | + New User |
240 | + ... |
241 | + new-user@example.com |
242 | + ... |
243 | + |
244 | +=== Confirm out of session === |
245 | + |
246 | +Start as before. |
247 | + |
248 | + >>> browser = setupBrowser() |
249 | + >>> request_reset(browser) |
250 | + >>> browser.contents.find("Forgotten your password?") > 0 |
251 | + True |
252 | + |
253 | +But now, try to finish the reset from out of the original session. |
254 | + |
255 | + >>> browser = setupBrowser() |
256 | + >>> token = find_my_token(LoginTokenType.PASSWORDRECOVERY) |
257 | + >>> link = 'http://openid.launchpad.dev/token/%s' % token |
258 | + >>> browser.open(link) |
259 | + |
260 | +It should prompt for the e-mail that the token was sent to. |
261 | + |
262 | + >>> print browser.url |
263 | + http://.../+enter_token?token=... |
264 | + |
265 | +Fill in the e-mail address, then continue as before. |
266 | + |
267 | >>> browser.getControl(name='email').value = 'new-user@example.com' |
268 | - >>> browser.getControl(name='password').value = 'new Password' |
269 | - >>> browser.getControl(name='continue').click() |
270 | + >>> browser.getControl(name='continue').click() |
271 | + >>> print browser.url |
272 | + http://openid.launchpad.dev/token/.../+resetpassword/new-user%40example.com |
273 | + |
274 | + >>> set_new_password(browser) |
275 | + >>> print browser.url |
276 | + http://openid.launchpad.dev/ |
277 | + |
278 | +Log in with the new password. |
279 | + |
280 | + >>> browser = setupBrowser() |
281 | + >>> login(browser, email='new-user@example.com', password='new Password') |
282 | + >>> print extract_text(find_tag_by_id(browser.contents, 'content')) |
283 | + New User |
284 | + ... |
285 | + new-user@example.com |
286 | + ... |
287 | + |
288 | + |
289 | +== Adding an e-mail address == |
290 | + |
291 | + >>> browser = setupBrowser() |
292 | + >>> login(browser, email='new-user@example.com', password='new Password') |
293 | + |
294 | + >>> def add_email(browser, email): |
295 | + ... browser.open('http://openid.launchpad.dev/+edit') |
296 | + ... browser.getLink('Add another email').click() |
297 | + ... browser.getControl(name='newemail').value = email |
298 | + ... browser.getControl(name='continue').click() |
299 | + |
300 | + >>> add_email(browser, 'alpha@example.org') |
301 | + |
302 | + >>> token = find_my_token(LoginTokenType.VALIDATEEMAIL, |
303 | + ... email='alpha@example.org') |
304 | + >>> browser.open('http://openid.launchpad.dev/token/%s' % token) |
305 | + >>> print browser.url |
306 | + http://openid.launchpad.dev/token/.../+newemail/alpha%40example.org |
307 | + |
308 | + >>> browser.getControl(name='continue').click() |
309 | + |
310 | + >>> browser = setupBrowser() |
311 | + >>> login(browser, email='alpha@example.org', password='new Password') |
312 | >>> print extract_text(find_tag_by_id(browser.contents, 'content')) |
313 | New User |
314 | ... |
315 | |
316 | === modified file 'identityprovider/forms.py' |
317 | --- identityprovider/forms.py 2010-05-20 20:08:19 +0000 |
318 | +++ identityprovider/forms.py 2010-06-22 23:23:25 +0000 |
319 | @@ -9,7 +9,7 @@ |
320 | from django.utils.safestring import mark_safe |
321 | from django.utils.translation import ugettext as _ |
322 | |
323 | -from identityprovider.models import Account, EmailAddress |
324 | +from identityprovider.models import Account, EmailAddress, verify_token_string |
325 | from identityprovider.models.const import EmailStatus |
326 | from identityprovider.utils import ( |
327 | get_person_and_account_by_email, CannotResetPasswordException, |
328 | @@ -136,6 +136,25 @@ |
329 | widget=widgets.TextInput(attrs={'class': 'textType', 'size': '20'})) |
330 | |
331 | |
332 | +class TokenForm(GenericEmailForm): |
333 | + """If the token and e-mail are entered correctly, and if the |
334 | + specified token exists and can be used, the token object is placed |
335 | + in cleaned_data['atoken'].""" |
336 | + |
337 | + token = fields.CharField() |
338 | + |
339 | + def clean(self): |
340 | + data = self.cleaned_data |
341 | + token_string = data.get('token') |
342 | + email = data.get('email') |
343 | + if token_string is not None and email is not None: |
344 | + atoken = verify_token_string(token_string, email) |
345 | + if atoken is None: |
346 | + raise forms.ValidationError(_("Token not found.")) |
347 | + data['atoken'] = atoken |
348 | + return data |
349 | + |
350 | + |
351 | class PreferredEmailField(forms.ModelChoiceField): |
352 | def label_from_instance(self, obj): |
353 | return obj.email |
354 | |
355 | === modified file 'identityprovider/models/authtoken.py' |
356 | --- identityprovider/models/authtoken.py 2010-05-20 14:07:52 +0000 |
357 | +++ identityprovider/models/authtoken.py 2010-06-22 23:23:25 +0000 |
358 | @@ -10,6 +10,7 @@ |
359 | from django.core.mail import send_mail |
360 | from django.core.urlresolvers import reverse |
361 | from django.db import models |
362 | +from django.http import urlencode |
363 | from django.template.loader import render_to_string |
364 | |
365 | from identityprovider.branding import current_brand |
366 | @@ -18,12 +19,25 @@ |
367 | from identityprovider.utils import format_address |
368 | |
369 | __all__ = ( |
370 | + 'AUTHTOKEN_LENGTH', |
371 | + 'AUTHTOKEN_PATTERN', |
372 | 'AuthToken', |
373 | 'AuthTokenFactory', |
374 | - 'send_validation_email_request' |
375 | + 'get_type_of_token', |
376 | + 'send_validation_email_request', |
377 | + 'verify_token_string' |
378 | ) |
379 | |
380 | |
381 | +AUTHTOKEN_LENGTH = 6 |
382 | + |
383 | +# For now, we have some older tokens (20 chars) still active, while we |
384 | +# introduce the shorter tokens. After a while, we could probably |
385 | +# bound this pattern's repetition more tightly. |
386 | +#AUTHTOKEN_PATTERN = '[A-Za-z0-9]{%s}' % AUTHTOKEN_LENGTH |
387 | +AUTHTOKEN_PATTERN = '[A-Za-z0-9]+' # % AUTHTOKEN_LENGTH |
388 | + |
389 | + |
390 | class AuthToken(models.Model): |
391 | date_created = models.DateTimeField(default=datetime.datetime.utcnow, |
392 | blank=True, editable=False) |
393 | @@ -50,7 +64,8 @@ |
394 | def get_absolute_url(self): |
395 | path = reverse('identityprovider.views.ui.claim_token', |
396 | kwargs={'authtoken': self.token}) |
397 | - return urljoin(settings.SSO_ROOT_URL, path) |
398 | + return urljoin(settings.SSO_ROOT_URL, path) + '?' + \ |
399 | + urlencode({'email': self.email}) |
400 | |
401 | def consume(self): |
402 | self.date_consumed = datetime.datetime.now() |
403 | @@ -76,6 +91,7 @@ |
404 | 'requester': self.requester.displayname, |
405 | 'requester_email': self.requester_email, |
406 | 'toaddress': self.email, |
407 | + 'token': self.token, |
408 | 'token_url': self.get_absolute_url(), |
409 | 'support_form_url': settings.SUPPORT_FORM_URL, |
410 | } |
411 | @@ -98,6 +114,7 @@ |
412 | |
413 | def sendPasswordResetEmail(self): |
414 | replacements = { |
415 | + 'token': self.token, |
416 | 'token_url': self.get_absolute_url(), |
417 | } |
418 | from_name = unicode(current_brand.name) |
419 | @@ -108,7 +125,8 @@ |
420 | |
421 | def sendNewUserEmail(self): |
422 | replacements = { |
423 | - 'token_url': self.get_absolute_url(), |
424 | + 'token': self.token, |
425 | + 'token_url': self.get_absolute_url() |
426 | } |
427 | from_name = unicode(current_brand.name) |
428 | message = render_to_string('%s/email/newuser.txt' % |
429 | @@ -142,7 +160,8 @@ |
430 | requester_email=requester_email, |
431 | email=email, token_type=token_type, |
432 | redirection_url=redirection_url) |
433 | - token.token = create_unique_token_for_table(20, AuthToken, 'token') |
434 | + token.token = create_unique_token_for_table( |
435 | + AUTHTOKEN_LENGTH, AuthToken, 'token') |
436 | token.save() |
437 | return token |
438 | |
439 | @@ -235,6 +254,14 @@ |
440 | return True |
441 | |
442 | |
443 | +def get_type_of_token(token_string): |
444 | + try: |
445 | + token = AuthToken.objects.get(token=token_string) |
446 | + return token.token_type |
447 | + except AuthToken.DoesNotExist: |
448 | + return None |
449 | + |
450 | + |
451 | def send_validation_email_request(account, email, redirection_url): |
452 | if account.preferredemail is None: |
453 | preferredemail_email = None |
454 | @@ -248,3 +275,12 @@ |
455 | token_type=LoginTokenType.VALIDATEEMAIL, |
456 | redirection_url=redirection_url) |
457 | token.sendEmailValidationRequest() |
458 | + |
459 | + return token |
460 | + |
461 | +def verify_token_string(token_string, email): |
462 | + try: |
463 | + return AuthToken.objects.get(token=token_string, email=email, |
464 | + date_consumed=None) |
465 | + except AuthToken.DoesNotExist: |
466 | + return None |
467 | |
468 | === added file 'identityprovider/templates/enter_token.html' |
469 | --- identityprovider/templates/enter_token.html 1970-01-01 00:00:00 +0000 |
470 | +++ identityprovider/templates/enter_token.html 2010-06-22 23:23:25 +0000 |
471 | @@ -0,0 +1,37 @@ |
472 | +<!-- Copyright 2010 Canonical Ltd. This software is licensed under the |
473 | +GNU Affero General Public License version 3 (see the file LICENSE). --> |
474 | + |
475 | +{% extends "base.html" %} |
476 | +{% load i18n %} |
477 | + |
478 | +{% block "title" %} |
479 | + {% blocktrans %}Enter token{% endblocktrans %} |
480 | +{% endblock %} |
481 | + |
482 | +{% block "content" %} |
483 | + |
484 | +<h2 class="main">Enter token</h2> |
485 | + |
486 | +<p>Enter the token that you received in an e-mail, and the e-mail |
487 | + address at which you received it.</p> |
488 | + |
489 | +<form method="post"> |
490 | + |
491 | + {% if form.non_field_errors %} |
492 | + <p><span class="error">{{ form.non_field_errors.0 }}</span></p> |
493 | + {% endif %} |
494 | + |
495 | + <p><label>Token<br> |
496 | + {{ form.token }}</label> |
497 | + {% if form.token.errors %}<span class="error">{{ form.token.errors|join:"" }}</span>{% endif %}</p> |
498 | + |
499 | + <p><label>E-mail<br> |
500 | + {{ form.email }}</label> |
501 | + {% if form.email.errors %}<span class="error">{{ form.email.errors|join:"" }}</span>{% endif %}</p> |
502 | + |
503 | + <p><button class="btn" type="submit" name="continue"> |
504 | + <span><span>Continue</span></span></button></p> |
505 | + |
506 | +</form> |
507 | + |
508 | +{% endblock %} |
509 | |
510 | === modified file 'identityprovider/templates/launchpad/email/forgottenpassword.txt' |
511 | --- identityprovider/templates/launchpad/email/forgottenpassword.txt 2010-05-13 14:41:45 +0000 |
512 | +++ identityprovider/templates/launchpad/email/forgottenpassword.txt 2010-06-22 23:23:25 +0000 |
513 | @@ -7,6 +7,10 @@ |
514 | |
515 | {{ token_url }} |
516 | |
517 | +Or, you may enter the following token manually: |
518 | + |
519 | + {{ token }} |
520 | + |
521 | If you don't know what this is about, then someone else has entered your email address at the Launchpad Login Service. Sorry about that. You don't need to do anything further, just delete this message. |
522 | |
523 | Regards, |
524 | |
525 | === modified file 'identityprovider/templates/launchpad/email/newuser.txt' |
526 | --- identityprovider/templates/launchpad/email/newuser.txt 2010-05-13 14:41:45 +0000 |
527 | +++ identityprovider/templates/launchpad/email/newuser.txt 2010-06-22 23:23:25 +0000 |
528 | @@ -7,6 +7,10 @@ |
529 | |
530 | {{ token_url }} |
531 | |
532 | +Or, you may enter the following token manually: |
533 | + |
534 | + {{ token }} |
535 | + |
536 | If you don't know what this is about, then someone has probably entered your email address by mistake at the Launchpad Login Service web site. Sorry about that. You don't need to do anything further, just delete this message. |
537 | |
538 | Regards, |
539 | |
540 | === modified file 'identityprovider/templates/launchpad/email/validate-email.txt' |
541 | --- identityprovider/templates/launchpad/email/validate-email.txt 2010-05-13 14:41:45 +0000 |
542 | +++ identityprovider/templates/launchpad/email/validate-email.txt 2010-06-22 23:23:25 +0000 |
543 | @@ -7,6 +7,10 @@ |
544 | |
545 | {{ token_url }} |
546 | |
547 | +Or, you may enter the following token manually: |
548 | + |
549 | + {{ token }} |
550 | + |
551 | If you did not make this request, please ignore this message or report it on |
552 | |
553 | {{ support_form_url }} |
554 | |
555 | === modified file 'identityprovider/templates/registration/confirm_new_account.html' |
556 | --- identityprovider/templates/registration/confirm_new_account.html 2010-06-18 12:43:10 +0000 |
557 | +++ identityprovider/templates/registration/confirm_new_account.html 2010-06-22 23:23:25 +0000 |
558 | @@ -55,7 +55,7 @@ |
559 | <div id="auth"> |
560 | <h1 class="main">{{ brand.complete_reg }}</h1> |
561 | |
562 | - <form id="login-form" class="longfields" action="+newaccount" method="post"> |
563 | + <form id="login-form" class="longfields" method="post"> |
564 | <p class="input-row{% if form.displayname.errors %} haserrors{% endif %}"> |
565 | <label for="id_displayname">{% trans "Full name" %}</label> |
566 | {{ form.displayname }} |
567 | |
568 | === modified file 'identityprovider/templates/registration/email_sent.html' |
569 | --- identityprovider/templates/registration/email_sent.html 2010-05-31 12:51:21 +0000 |
570 | +++ identityprovider/templates/registration/email_sent.html 2010-06-22 23:23:25 +0000 |
571 | @@ -8,21 +8,28 @@ |
572 | |
573 | {% block "content" %} |
574 | {% if user.is_authenticated %}<div class="email-sent-auth">{% endif %} |
575 | + |
576 | <div id="col1"> |
577 | <h2 class="main">{{ email_heading }}</h2> |
578 | <div class="larger"> |
579 | <p>{{ email_reason|safe }}</p> |
580 | <p class="last"> |
581 | - {% trans "To continue, follow the link in that message." %} |
582 | + {% trans "To continue, follow the link in that message, or enter the token that it contains below:" %} |
583 | </p> |
584 | </div> |
585 | |
586 | - {% if user.is_authenticated %} |
587 | - <p> |
588 | - <a href="./+edit" class="btn"><span><span>{% trans "Continue" %}</span></span></a> |
589 | - </p> |
590 | - {% endif %} |
591 | + <form action="/+enter_token" method="post"> |
592 | + <p><label>Token<br> |
593 | + <input name="token" type="text" class="textType"></label></p> |
594 | + <p><button type="submit" class="btn"> |
595 | + <span><span>Continue</span></span> |
596 | + </button> |
597 | + {% if user.is_authenticated %} or, <a href="/+edit">return to |
598 | + profile</a>{% endif %} |
599 | + </p> |
600 | + </form> |
601 | </div> |
602 | + |
603 | <div id="col2"> |
604 | <h2 class="main">{% trans "Haven’t received it?" %}</h2> |
605 | <p>{% blocktrans %}If you don’t receive the message within a few minutes, it might be because:{% endblocktrans %}</p> |
606 | @@ -35,5 +42,8 @@ |
607 | <p>{% blocktrans %}If neither of those work, our service might be having a problem.{% endblocktrans %} |
608 | </p> |
609 | </div> |
610 | + |
611 | +<div style="clear: both"></div> |
612 | + |
613 | {% if user.is_authenticated %}</div>{% endif %} |
614 | {% endblock %} |
615 | |
616 | === modified file 'identityprovider/templates/registration/reset_password.html' |
617 | --- identityprovider/templates/registration/reset_password.html 2010-06-18 12:43:10 +0000 |
618 | +++ identityprovider/templates/registration/reset_password.html 2010-06-22 23:23:25 +0000 |
619 | @@ -56,7 +56,7 @@ |
620 | <div id="auth"> |
621 | <h2 class="main">{{ brand.reset_password }}</h2> |
622 | |
623 | - <form id="login-form" class="longfields" action="+resetpassword" method="post"> |
624 | + <form id="login-form" class="longfields" method="post"> |
625 | <p class="input-row{% if form.password.errors or form.non_field_errors %} haserrors{% endif %}"> |
626 | <label for="id_password">{% trans "Choose password" %}</label> |
627 | <div id="password_wrapper">{{ form.password }}<div id='password_strength'></div></div> |
628 | |
629 | === modified file 'identityprovider/templates/ubuntu/email/forgottenpassword.txt' |
630 | --- identityprovider/templates/ubuntu/email/forgottenpassword.txt 2010-05-13 14:41:45 +0000 |
631 | +++ identityprovider/templates/ubuntu/email/forgottenpassword.txt 2010-06-22 23:23:25 +0000 |
632 | @@ -7,6 +7,10 @@ |
633 | |
634 | {{ token_url }} |
635 | |
636 | +Or, you may enter the following token manually: |
637 | + |
638 | + {{ token }} |
639 | + |
640 | If you don't know what this is about, then someone else has entered your email address at the Ubuntu Single Sign On service. Sorry about that. You don't need to do anything further, just delete this message. |
641 | |
642 | Regards, |
643 | |
644 | === modified file 'identityprovider/templates/ubuntu/email/newuser.txt' |
645 | --- identityprovider/templates/ubuntu/email/newuser.txt 2010-05-13 15:12:04 +0000 |
646 | +++ identityprovider/templates/ubuntu/email/newuser.txt 2010-06-22 23:23:25 +0000 |
647 | @@ -7,6 +7,10 @@ |
648 | |
649 | {{ token_url }} |
650 | |
651 | +Or, you may enter the following token manually: |
652 | + |
653 | + {{ token }} |
654 | + |
655 | If you don't know what this is about, then someone has probably entered your email address by mistake at the Ubuntu Single Sign On service web site. Sorry about that. You don't need to do anything further, just delete this message. |
656 | |
657 | Regards, |
658 | |
659 | === modified file 'identityprovider/templates/ubuntu/email/validate-email.txt' |
660 | --- identityprovider/templates/ubuntu/email/validate-email.txt 2010-05-13 15:12:04 +0000 |
661 | +++ identityprovider/templates/ubuntu/email/validate-email.txt 2010-06-22 23:23:25 +0000 |
662 | @@ -7,6 +7,10 @@ |
663 | |
664 | {{ token_url }} |
665 | |
666 | +Or, you may enter the following token manually: |
667 | + |
668 | + {{ token }} |
669 | + |
670 | If you did not make this request, please ignore this message or report it on |
671 | |
672 | {{ support_form_url }} |
673 | |
674 | === modified file 'identityprovider/tests/test_views_ui.py' |
675 | --- identityprovider/tests/test_views_ui.py 2010-06-15 16:11:25 +0000 |
676 | +++ identityprovider/tests/test_views_ui.py 2010-06-22 23:23:25 +0000 |
677 | @@ -11,10 +11,12 @@ |
678 | from django.core import mail, urlresolvers |
679 | from django.http import QueryDict |
680 | from django.test.client import Client |
681 | +from django.utils.http import urlquote |
682 | + |
683 | from openid.message import IDENTIFIER_SELECT |
684 | |
685 | from identityprovider import decorators, signed |
686 | -from identityprovider.models import authtoken as at |
687 | +from identityprovider.models import AUTHTOKEN_LENGTH, authtoken as at |
688 | from identityprovider.models import EmailAddress, Person, OpenIDRPConfig |
689 | from identityprovider.models.const import EmailStatus |
690 | from identityprovider.views import ui, server |
691 | @@ -51,13 +53,24 @@ |
692 | |
693 | def setUp(self): |
694 | logging.disable(logging.WARNING) |
695 | - |
696 | - # disable csrf |
697 | self.disable_csrf() |
698 | + self._ENABLE_TOKEN_DEBUG = getattr(settings, 'ENABLE_TOKEN_DEBUG') |
699 | + settings.ENABLE_TOKEN_DEBUG = True |
700 | |
701 | def tearDown(self): |
702 | + settings.ENABLE_TOKEN_DEBUG = self._ENABLE_TOKEN_DEBUG |
703 | self.reset_csrf() |
704 | |
705 | + def _session_token(self): |
706 | + token_string = self.client.session['token_string'] |
707 | + return AuthToken.objects.get(token=token_string) |
708 | + |
709 | + def _token_url(self, view, token_string=None, token_email=None): |
710 | + token_string = token_string or self.client.session['token_string'] |
711 | + token_email = token_email or self.client.session['token_email'] |
712 | + quoted_email = urlquote(token_email, safe='') |
713 | + return '/token/%s/%s/%s' % (token_string, view, quoted_email) |
714 | + |
715 | def authenticate(self): |
716 | self.client.login(username='mark@example.com', password='test') |
717 | |
718 | @@ -146,7 +159,8 @@ |
719 | def create_token(self, token_type, email=None, redirection_url=None): |
720 | token = at.AuthToken.objects.create( |
721 | token_type=token_type, |
722 | - token=at.create_unique_token_for_table(20, at.AuthToken, 'token'), |
723 | + token=at.create_unique_token_for_table( |
724 | + AUTHTOKEN_LENGTH, at.AuthToken, 'token'), |
725 | ) |
726 | if email: |
727 | token.email = email |
728 | @@ -155,12 +169,6 @@ |
729 | token.save() |
730 | return token |
731 | |
732 | - def test_claim_token_for_password_recovery(self): |
733 | - token = self.create_token(at.LoginTokenType.PASSWORDRECOVERY) |
734 | - r = self.client.get('/token/%s/' % token.token) |
735 | - self.assertRedirects(r, '/token/%s/+resetpassword' % token.token, |
736 | - target_status_code=302) |
737 | - |
738 | def test_claim_token_for_password_recovery_no_preferredemail(self): |
739 | """ According to bug #524582 this should be handled gracefully by |
740 | verifying the email address and resetting the password. """ |
741 | @@ -173,12 +181,9 @@ |
742 | r = self.client.post('/+forgot_password', |
743 | {'email': 'test@canonical.com'}) |
744 | |
745 | - # claim token |
746 | - token = self.client.session['token_forgotpassword'] |
747 | - |
748 | # reset password |
749 | data = {'password': 'Password1', 'passwordconfirm': 'Password1'} |
750 | - r = self.client.post("/token/%s/+resetpassword" % token, data) |
751 | + r = self.client.post(self._token_url('+resetpassword'), data) |
752 | self.assertRedirects(r, '/') |
753 | |
754 | for email_obj in account.emailaddress_set.all(): |
755 | @@ -186,25 +191,49 @@ |
756 | email_obj.save() |
757 | account.preferredemail = _preferred_email |
758 | |
759 | - def test_claim_token_for_validate_email(self): |
760 | - token = self.create_token(at.LoginTokenType.VALIDATEEMAIL) |
761 | - r = self.client.get('/token/%s/' % token.token) |
762 | - self.assertRedirects(r, '/token/%s/+newemail' % token.token, |
763 | - target_status_code=302) |
764 | + def test_enter_unexisting_token(self): |
765 | + r = self.client.post('/+enter_token', { |
766 | + 'email': 'fake@example.com', |
767 | + 'token': '0'}) |
768 | + self.assertContains(r, 'Token not found') |
769 | + |
770 | + def test_claim_unexisting_token(self): |
771 | + r = self.client.get('/token/%s/' % 0, {'email': 'fake%40example.com'}) |
772 | + self.assertEquals(r.status_code, 404) |
773 | |
774 | def test_claim_token_for_unexisting_token_type(self): |
775 | - token = self.create_token(9999) |
776 | - r = self.client.get('/token/%s/' % token.token) |
777 | + token = self.create_token(9999, 'fake@example.com') |
778 | + r = self.client.get('/token/%s/' % token.token, |
779 | + {'email': 'fake%40example.com'}) |
780 | self.assertEquals(r.status_code, 404) |
781 | |
782 | + def test_claim_consumed_token(self): |
783 | + token = self.client.post('/+forgot_password', |
784 | + {'email': 'test@canonical.com'}) |
785 | + url = self._token_url('+resetpassword') |
786 | + |
787 | + pwd = 'Password1' |
788 | + r = self.client.post(url, {'password': pwd, 'passwordconfirm': pwd}) |
789 | + self.assertRedirects(r, '/') |
790 | + |
791 | + self.client.cookies.clear() |
792 | + |
793 | + pwd = 'Password2' |
794 | + r = self.client.post(url, {'password': pwd, 'passwordconfirm': pwd}) |
795 | + self.assertRedirects(r, '/+bad-token') |
796 | + |
797 | + def test_token_form_validation(self): |
798 | + r = self.client.post('/+enter_token') |
799 | + self.assertContains(r, '<span class="error"') |
800 | + |
801 | def test_config_email(self): |
802 | self.authenticate() |
803 | |
804 | token = self.create_token(at.LoginTokenType.VALIDATEEMAIL, |
805 | - email="mark@example.com", |
806 | + email='mark@example.com', |
807 | redirection_url="/") |
808 | - r = self.client.post('/token/%s/+newemail' % token.token, |
809 | - {'post': 'yes'}) |
810 | + url = '/token/%s/+newemail/%s' % (token.token, 'mark%40example.com') |
811 | + r = self.client.post(url, {'post': 'yes'}) |
812 | self.assertRedirects(r, token.redirection_url) |
813 | |
814 | def test_token_from_email_when_it_is_turned_on(self): |
815 | @@ -230,22 +259,16 @@ |
816 | r = self.client.get('/+bad-token') |
817 | self.assertEquals(r.status_code, 200) |
818 | |
819 | - def test_confirm_account_with_bad_token(self): |
820 | - token = self.create_token(at.LoginTokenType.NEWPERSONLESSACCOUNT) |
821 | - self.authenticate() |
822 | - r = self.client.get('/+logout') |
823 | - |
824 | - r = self.client.get('/token/%s/+newaccount' % token.token) |
825 | - self.assertRedirects(r, '/+bad-token') |
826 | - |
827 | def test_logout_to_confirm(self): |
828 | r = self.client.get('/+logout-to-confirm') |
829 | self.assertEquals(r.status_code, 200) |
830 | |
831 | def test_confirm_account_while_logged_in(self): |
832 | - token = self.create_token(at.LoginTokenType.NEWPERSONLESSACCOUNT) |
833 | + token = self.create_token(at.LoginTokenType.NEWPERSONLESSACCOUNT, |
834 | + email='me@example.com') |
835 | self.authenticate() |
836 | - r = self.client.get('/token/%s/' % token.token) |
837 | + r = self.client.get('/token/%s/' % token.token, |
838 | + {'email': 'me@example.com'}) |
839 | location = None |
840 | for item in r.items(): |
841 | if item[0] == 'Location': |
842 | @@ -263,9 +286,7 @@ |
843 | self.assertRedirects(r, '/+email-sent') |
844 | |
845 | # claim token |
846 | - token2 = self.client.session['token_newaccount'] |
847 | - |
848 | - r = self.client.post("/token/%s/+newaccount" % token2, |
849 | + r = self.client.post(self._token_url('+newaccount'), |
850 | {'displayname': 'Person', 'password': 'P4ssw0rd', |
851 | 'passwordconfirm': 'P4ssw0rd'}) |
852 | self.assertRedirects(r, "/%s/+decide" % token1) |
853 | @@ -278,9 +299,7 @@ |
854 | self.assertRedirects(r, '/+email-sent') |
855 | |
856 | # claim token |
857 | - token2 = self.client.session['token_newaccount'] |
858 | - |
859 | - r = self.client.post("/token/%s/+newaccount" % token2, |
860 | + r = self.client.post(self._token_url('+newaccount'), |
861 | {'displayname': 'Person', 'password': 'P4ssw0rd', |
862 | 'passwordconfirm': 'P4ssw0rd'}) |
863 | self.assertRedirects(r, "/%s/+decide" % token1) |
864 | @@ -305,9 +324,7 @@ |
865 | rpconfig = OpenIDRPConfig.objects.create(trust_root='http://localhost/') |
866 | |
867 | # claim token |
868 | - token2 = self.client.session['token_newaccount'] |
869 | - |
870 | - r = self.client.post("/token/%s/+newaccount" % token2, |
871 | + r = self.client.post(self._token_url('+newaccount'), |
872 | {'displayname': 'Person', 'password': 'P4ssw0rd', |
873 | 'passwordconfirm': 'P4ssw0rd'}) |
874 | self.assertRedirects(r, "/%s/+decide" % token1) |
875 | @@ -320,12 +337,12 @@ |
876 | def test_confirm_account_invalid_form(self): |
877 | # setup session |
878 | token1 = create_token(16) |
879 | + email = 'person@example.com' |
880 | r = self.client.post("/%s/+new_account" % token1, |
881 | - {'email': 'person@example.com'}) |
882 | + {'email': email}) |
883 | |
884 | # test view |
885 | - token2 = self.client.session['token_newaccount'] |
886 | - r = self.client.post("/token/%s/+newaccount" % token2) |
887 | + r = self.client.post(self._token_url('+newaccount')) |
888 | self.assertTemplateUsed(r, 'registration/confirm_new_account.html') |
889 | self.assertFormError(r, 'form', 'displayname', 'Required field.') |
890 | |
891 | @@ -397,15 +414,12 @@ |
892 | r = self.client.post("/%s/+forgot_password" % token1, |
893 | {'email': 'test@canonical.com'}) |
894 | |
895 | - token2 = self.client.session['token_forgotpassword'] |
896 | - auth_token = AuthToken.objects.get(token=token2) |
897 | + auth_token = self._session_token() |
898 | self.assertEqual(auth_token.redirection_url, "/%s/+decide" % token1) |
899 | |
900 | def test_reset_password_when_account_active(self): |
901 | r = self.client.post('/+forgot_password', |
902 | {'email': 'test@canonical.com'}) |
903 | - # claim token |
904 | - token = self.client.session['token_forgotpassword'] |
905 | |
906 | account = Account.objects.get_by_email('test@canonical.com') |
907 | # make sure the account is active |
908 | @@ -414,14 +428,12 @@ |
909 | |
910 | # confirm account |
911 | data = {'password': 'Password1', 'passwordconfirm': 'Password1'} |
912 | - r = self.client.post('/token/%s/+resetpassword' % token, data) |
913 | + r = self.client.post(self._token_url('+resetpassword'), data) |
914 | self.assertRedirects(r, '/') |
915 | |
916 | def test_reset_password_when_account_active_no_password(self): |
917 | r = self.client.post('/+forgot_password', |
918 | {'email': 'test@canonical.com'}) |
919 | - # claim token |
920 | - token = self.client.session['token_forgotpassword'] |
921 | |
922 | account = Account.objects.get_by_email('test@canonical.com') |
923 | account.accountpassword.delete() |
924 | @@ -431,7 +443,7 @@ |
925 | |
926 | # confirm account |
927 | data = {'password': 'Password1', 'passwordconfirm': 'Password1'} |
928 | - r = self.client.post('/token/%s/+resetpassword' % token, data) |
929 | + r = self.client.post(self._token_url('+resetpassword'), data) |
930 | self.assertRedirects(r, '/') |
931 | |
932 | def test_reset_password_when_account_deactivated(self): |
933 | @@ -439,8 +451,6 @@ |
934 | functionality. Bug #556878 """ |
935 | r = self.client.post('/+forgot_password', |
936 | {'email': 'test@canonical.com'}) |
937 | - # claim token |
938 | - token = self.client.session['token_forgotpassword'] |
939 | |
940 | account = Account.objects.get_by_email('test@canonical.com') |
941 | # make sure the account is deactivated |
942 | @@ -449,7 +459,7 @@ |
943 | |
944 | # confirm account |
945 | data = {'password': 'Password1', 'passwordconfirm': 'Password1'} |
946 | - r = self.client.post('/token/%s/+resetpassword' % token, data) |
947 | + r = self.client.post(self._token_url('+resetpassword'), data) |
948 | self.assertRedirects(r, '/') |
949 | |
950 | def test_reset_password_when_account_deactivated_no_preferred_email(self): |
951 | @@ -469,12 +479,9 @@ |
952 | self.assertEqual(len(mail.outbox), 1) |
953 | mail.outbox = [] |
954 | |
955 | - # claim token |
956 | - token = self.client.session['token_forgotpassword'] |
957 | - |
958 | # reset password |
959 | data = {'password': 'Password1', 'passwordconfirm': 'Password1'} |
960 | - r = self.client.post('/token/%s/+resetpassword' % token, data) |
961 | + r = self.client.post(self._token_url('+resetpassword'), data) |
962 | self.assertRedirects(r, '/') |
963 | |
964 | self.assertEqual(account.preferredemail.email, 'test@canonical.com') |
965 | @@ -487,8 +494,6 @@ |
966 | def test_reset_password_when_account_noaccount(self): |
967 | r = self.client.post('/+forgot_password', |
968 | {'email': 'test@canonical.com'}) |
969 | - # claim token |
970 | - token = self.client.session['token_forgotpassword'] |
971 | |
972 | account = Account.objects.get_by_email('test@canonical.com') |
973 | # make sure the account is deactivated |
974 | @@ -497,14 +502,12 @@ |
975 | |
976 | # confirm account |
977 | data = {'password': 'Password1', 'passwordconfirm': 'Password1'} |
978 | - r = self.client.post('/token/%s/+resetpassword' % token, data) |
979 | + r = self.client.post(self._token_url('+resetpassword'), data) |
980 | self.assertRedirects(r, '/') |
981 | |
982 | def test_reset_password_when_account_suspended(self): |
983 | r = self.client.post('/+forgot_password', |
984 | {'email': 'test@canonical.com'}) |
985 | - # claim token |
986 | - token = self.client.session['token_forgotpassword'] |
987 | |
988 | account = Account.objects.get_by_email('test@canonical.com') |
989 | account.status = AccountStatus.SUSPENDED |
990 | @@ -513,18 +516,16 @@ |
991 | data = {'password': 'Password1', 'passwordconfirm': 'Password1'} |
992 | |
993 | # confirm account |
994 | - r = self.client.post('/token/%s/+resetpassword' % token, data) |
995 | + r = self.client.post(self._token_url('+resetpassword'), data) |
996 | self.assertRedirects(r, '/+bad-token') |
997 | |
998 | def test_reset_password_invalid_form(self): |
999 | # get valid session |
1000 | r = self.client.post('/+forgot_password', |
1001 | {'email': 'test@canonical.com'}) |
1002 | - # claim token |
1003 | - token = self.client.session['token_forgotpassword'] |
1004 | |
1005 | # test view |
1006 | - r = self.client.post("/token/%s/+resetpassword" % token) |
1007 | + r = self.client.post(self._token_url('+resetpassword')) |
1008 | self.assertTemplateUsed(r, 'registration/reset_password.html') |
1009 | self.assertFormError(r, 'form', 'password', 'Required field.') |
1010 | |
1011 | @@ -532,11 +533,9 @@ |
1012 | # get valid session |
1013 | r = self.client.post('/+forgot_password', |
1014 | {'email': 'test@canonical.com'}) |
1015 | - # claim token |
1016 | - token = self.client.session['token_forgotpassword'] |
1017 | |
1018 | # test view |
1019 | - r = self.client.get("/token/%s/+resetpassword" % token) |
1020 | + r = self.client.get(self._token_url('+resetpassword')) |
1021 | self.assertTemplateUsed(r, 'registration/reset_password.html') |
1022 | for context in r.context: |
1023 | self.assertEqual(context['form'].errors, {}) |
1024 | @@ -607,8 +606,7 @@ |
1025 | |
1026 | def test_new_account_when_account_not_exists_no_token(self): |
1027 | r = self.client.post('/+new_account', {'email': 'person@example.com'}) |
1028 | - token = self.client.session['token_newaccount'] |
1029 | - token_obj = AuthToken.objects.get(token=token) |
1030 | + token_obj = self._session_token() |
1031 | self.assertEqual(token_obj.redirection_url, '/') |
1032 | |
1033 | def test_edit_account_template(self): |
1034 | @@ -674,21 +672,6 @@ |
1035 | email = EmailAddress.objects.get(email=email.email) |
1036 | self.assertEqual(email.status, EmailStatus.NEW) |
1037 | |
1038 | - def test_confirm_email_post(self): |
1039 | - self.authenticate() |
1040 | - |
1041 | - account, email, token = self._setup_account_with_new_email() |
1042 | - # use verification token |
1043 | - r = self.client.post("/token/%s/+newemail" % token.token, |
1044 | - {'post': 'yes'}) |
1045 | - |
1046 | - # verify token has been consumed, and email has been verified |
1047 | - token = AuthToken.objects.get(token=token.token, |
1048 | - token_type=LoginTokenType.VALIDATEEMAIL) |
1049 | - self.assertNotEqual(token.date_consumed, None) |
1050 | - email = EmailAddress.objects.get(email=email.email) |
1051 | - self.assertEqual(email.status, EmailStatus.VALIDATED) |
1052 | - |
1053 | def test_confirm_email_as_another_user_fails(self): |
1054 | self.authenticate() |
1055 | account = Account.objects.get_by_email('test@canonical.com') |
1056 | @@ -707,14 +690,17 @@ |
1057 | |
1058 | r, token = self.get_email_token_validation_response(account) |
1059 | |
1060 | - self.assertRedirects(r, '/+login?next=/token/%s/%%2Bnewemail' % |
1061 | - token.token) |
1062 | + url = ('/+login?next=/token/%s/%%2Bnewemail/%s' % |
1063 | + (token.token, 'newemail2%40example.com')) |
1064 | + self.assertRedirects(r, url) |
1065 | |
1066 | def get_email_token_validation_response(self, account): |
1067 | email = account.emailaddress_set.create( |
1068 | email='newemail2@example.com', status=EmailStatus.NEW) |
1069 | token = self.create_token(at.LoginTokenType.VALIDATEEMAIL, email.email) |
1070 | - r = self.client.get("/token/%s/+newemail" % token.token) |
1071 | + url = ('/token/%s/+newemail/%s' % |
1072 | + (token.token, 'newemail2%40example.com')) |
1073 | + r = self.client.get(url) |
1074 | return (r, token) |
1075 | |
1076 | def test_suspended(self): |
1077 | |
1078 | === modified file 'identityprovider/tests/utils.py' |
1079 | --- identityprovider/tests/utils.py 2010-06-21 08:09:20 +0000 |
1080 | +++ identityprovider/tests/utils.py 2010-06-22 23:23:25 +0000 |
1081 | @@ -211,6 +211,8 @@ |
1082 | "A class that simulates writing to /dev/null." |
1083 | def write(self, s): |
1084 | pass |
1085 | + def flush(self): |
1086 | + pass |
1087 | |
1088 | |
1089 | # This function was adapted from the django project. |
1090 | |
1091 | === modified file 'identityprovider/urls.py' |
1092 | --- identityprovider/urls.py 2010-06-22 10:33:42 +0000 |
1093 | +++ identityprovider/urls.py 2010-06-22 23:23:25 +0000 |
1094 | @@ -5,11 +5,14 @@ |
1095 | from django.conf import settings |
1096 | |
1097 | from identityprovider.branding import current_brand |
1098 | +from identityprovider.models import AUTHTOKEN_PATTERN |
1099 | |
1100 | |
1101 | repls = { |
1102 | 'token': '(?P<token>[A-Za-z0-9]{16})/', |
1103 | 'optional_token': '((?P<token>[A-Za-z0-9]{16})/)?', |
1104 | + 'authtoken': '(?P<authtoken>%s)' % AUTHTOKEN_PATTERN, |
1105 | + 'email_address': '(?P<email_address>.+)' |
1106 | } |
1107 | |
1108 | urlpatterns = patterns('identityprovider.views.server', |
1109 | @@ -28,12 +31,14 @@ |
1110 | (r'^%(optional_token)s\+logout$' % repls, 'logout'), |
1111 | (r'^%(optional_token)s\+new_account$' % repls, 'new_account'), |
1112 | (r'^%(optional_token)s\+forgot_password$' % repls, 'forgot_password'), |
1113 | - (r'^token/(?P<authtoken>[A-Za-z0-9]{20})/$', 'claim_token'), |
1114 | - (r'^token/(?P<authtoken>[A-Za-z0-9]{20})/\+resetpassword$', |
1115 | + (r'^\+enter_token$', 'enter_token'), |
1116 | + (r'^token/%(authtoken)s/$' % repls, 'claim_token'), |
1117 | + (r'^token/%(authtoken)s/\+resetpassword/%(email_address)s$' % repls, |
1118 | 'reset_password'), |
1119 | - (r'^token/(?P<authtoken>[A-Za-z0-9]{20})/\+newaccount$', |
1120 | + (r'^token/%(authtoken)s/\+newaccount/%(email_address)s$' % repls, |
1121 | 'confirm_account'), |
1122 | - (r'^token/(?P<authtoken>[A-Za-z0-9]{20})/\+newemail$', 'confirm_email'), |
1123 | + (r'^token/%(authtoken)s/\+newemail/%(email_address)s$' % repls, |
1124 | + 'confirm_email'), |
1125 | (r'^\+bad-token', 'bad_token'), |
1126 | (r'^\+logout-to-confirm', 'logout_to_confirm'), |
1127 | (r'^%(optional_token)s\+email-sent$' % repls, 'email_sent'), |
1128 | |
1129 | === modified file 'identityprovider/views/account.py' |
1130 | --- identityprovider/views/account.py 2010-06-21 16:00:10 +0000 |
1131 | +++ identityprovider/views/account.py 2010-06-22 23:23:25 +0000 |
1132 | @@ -11,14 +11,15 @@ |
1133 | from django.template import RequestContext |
1134 | from django.utils.translation import ugettext as _ |
1135 | |
1136 | +from identityprovider.decorators import check_readonly |
1137 | from identityprovider.forms import EditAccountForm, LoginForm, NewEmailForm |
1138 | from identityprovider.models import (send_validation_email_request, |
1139 | EmailAddress) |
1140 | from identityprovider.models.oauthtoken import Consumer, Token |
1141 | from identityprovider.models.const import EmailStatus |
1142 | -from identityprovider.decorators import check_readonly |
1143 | -from identityprovider.views.utils import redirection_url_for_token |
1144 | from identityprovider.readonly import readonly_manager |
1145 | +from identityprovider.views.utils import (redirection_url_for_token, |
1146 | + set_session_token_info) |
1147 | |
1148 | |
1149 | def index(request, token=None): |
1150 | @@ -92,17 +93,22 @@ |
1151 | |
1152 | |
1153 | def _send_verification_email(account, session, email, tokenid=None): |
1154 | + # TODO: This makes use of `tokenid` if it's passed in, but the |
1155 | + # call to `send_validation_email_request` always generates a new |
1156 | + # token. Is this right? |
1157 | + |
1158 | # If there are any unverified emails that match they should be deleted. |
1159 | EmailAddress.objects.filter(email__iexact=email, |
1160 | status=EmailStatus.NEW).delete() |
1161 | |
1162 | # Ensure that this account has such email address; return value is not used |
1163 | - account.emailaddress_set.get_or_create(email=email, lp_person=account.person, |
1164 | - status=EmailStatus.NEW) |
1165 | + account.emailaddress_set.get_or_create( |
1166 | + email=email, lp_person=account.person, status=EmailStatus.NEW) |
1167 | |
1168 | redirection_url = redirection_url_for_token(tokenid) |
1169 | |
1170 | - send_validation_email_request(account, email, redirection_url) |
1171 | + token = send_validation_email_request(account, email, redirection_url) |
1172 | + set_session_token_info(session, token) |
1173 | |
1174 | session['email_feedback'] = settings.FEEDBACK_TO_ADDRESS |
1175 | session['email_heading'] = _("Validate your e-mail address") |
1176 | |
1177 | === modified file 'identityprovider/views/ui.py' |
1178 | --- identityprovider/views/ui.py 2010-06-15 14:12:57 +0000 |
1179 | +++ identityprovider/views/ui.py 2010-06-22 23:23:25 +0000 |
1180 | @@ -12,19 +12,22 @@ |
1181 | from django.core import urlresolvers |
1182 | from django.core.mail import send_mail |
1183 | from django.core.urlresolvers import Resolver404, resolve, reverse |
1184 | -from django.http import Http404, HttpResponseRedirect, HttpResponseNotFound |
1185 | +from django.http import (Http404, HttpResponseNotAllowed, HttpResponseNotFound, |
1186 | + HttpResponseRedirect, urlencode) |
1187 | from django.shortcuts import (get_object_or_404, get_list_or_404, |
1188 | render_to_response) |
1189 | from django.template import RequestContext |
1190 | +from django.template.loader import render_to_string |
1191 | +from django.utils.http import urlquote |
1192 | from django.utils.translation import ugettext as _ |
1193 | -from django.template.loader import render_to_string |
1194 | |
1195 | from identityprovider.branding import current_brand |
1196 | -from identityprovider.decorators import dont_cache, guest_required, limitlogin, requires_cookies |
1197 | +from identityprovider.decorators import (dont_cache, guest_required, limitlogin, |
1198 | + requires_cookies) |
1199 | from identityprovider.forms import (LoginForm, ForgotPasswordForm, |
1200 | - ResetPasswordForm, NewAccountForm, ConfirmNewAccountForm) |
1201 | + ResetPasswordForm, NewAccountForm, ConfirmNewAccountForm, TokenForm) |
1202 | from identityprovider.models import (Account, AccountPassword, AuthToken, |
1203 | - AuthTokenFactory, EmailAddress) |
1204 | + AuthTokenFactory, EmailAddress, get_type_of_token, verify_token_string) |
1205 | from identityprovider.models.const import (AccountStatus, EmailStatus, |
1206 | LoginTokenType) |
1207 | import identityprovider.signed as signed |
1208 | @@ -34,7 +37,7 @@ |
1209 | from identityprovider.decorators import check_readonly |
1210 | from identityprovider.middleware.csrf import csrf_exempt |
1211 | from identityprovider.models.captcha import Captcha |
1212 | -from identityprovider.views.utils import get_rpconfig |
1213 | +from identityprovider.views.utils import get_rpconfig, set_session_token_info |
1214 | |
1215 | |
1216 | logger = logging.getLogger('sso') |
1217 | @@ -112,14 +115,47 @@ |
1218 | return response |
1219 | |
1220 | |
1221 | +def enter_token(request): |
1222 | + token_string = request.REQUEST.get('token') |
1223 | + email = request.REQUEST.get('email') or request.session.get('token_email') |
1224 | + params = {'token': token_string, 'email': email} |
1225 | + if request.method == 'GET': |
1226 | + form = TokenForm(initial=params) |
1227 | + elif request.method == 'POST': |
1228 | + form = TokenForm(params) |
1229 | + if form.is_valid(): |
1230 | + token = form.cleaned_data['atoken'] |
1231 | + return _token_redirect(token.token_type, token_string, email) |
1232 | + else: |
1233 | + return HttpResponseNotAllowed(['GET', 'POST']) |
1234 | + return render_to_response('enter_token.html', |
1235 | + RequestContext(request, {'form': form})) |
1236 | + |
1237 | +def _redirect_to_enter_token(token=None, email=None): |
1238 | + params = {} |
1239 | + if token is not None: |
1240 | + params['token'] = token |
1241 | + if email is not None: |
1242 | + params['email'] = email |
1243 | + return HttpResponseRedirect('/+enter_token?%s' % urlencode(params)) |
1244 | + |
1245 | def claim_token(request, authtoken): |
1246 | - atrequest = get_object_or_404(AuthToken, token=authtoken) |
1247 | - if atrequest.token_type == LoginTokenType.PASSWORDRECOVERY: |
1248 | - return HttpResponseRedirect('/token/%s/+resetpassword' % authtoken) |
1249 | - elif atrequest.token_type == LoginTokenType.NEWPERSONLESSACCOUNT: |
1250 | - return HttpResponseRedirect('/token/%s/+newaccount' % authtoken) |
1251 | - elif atrequest.token_type == LoginTokenType.VALIDATEEMAIL: |
1252 | - return HttpResponseRedirect('/token/%s/+newemail' % authtoken) |
1253 | + email = request.GET.get('email') or request.session.get('token_email') |
1254 | + if email is None: |
1255 | + return _redirect_to_enter_token(token=authtoken) |
1256 | + token_type = get_type_of_token(authtoken) |
1257 | + return _token_redirect(token_type, authtoken, email) |
1258 | + |
1259 | +def _token_redirect(token_type, token_string, email): |
1260 | + """Returns a redirect to the location that can complete handling |
1261 | + of this token.""" |
1262 | + args = (token_string, urlquote(email, safe='')) |
1263 | + if token_type == LoginTokenType.PASSWORDRECOVERY: |
1264 | + return HttpResponseRedirect('/token/%s/+resetpassword/%s' % args) |
1265 | + elif token_type == LoginTokenType.NEWPERSONLESSACCOUNT: |
1266 | + return HttpResponseRedirect('/token/%s/+newaccount/%s' % args) |
1267 | + elif token_type == LoginTokenType.VALIDATEEMAIL: |
1268 | + return HttpResponseRedirect('/token/%s/+newemail/%s' % args) |
1269 | else: |
1270 | raise Http404() |
1271 | |
1272 | @@ -187,7 +223,7 @@ |
1273 | token_type=LoginTokenType.NEWPERSONLESSACCOUNT, |
1274 | redirection_url=redirection_url) |
1275 | token.sendNewUserEmail() |
1276 | - request.session['token_newaccount'] = token.token |
1277 | + set_session_token_info(request.session, token) |
1278 | request.session['email_feedback'] = settings.FEEDBACK_TO_ADDRESS |
1279 | request.session['email_heading'] = _("Registration mail sent") |
1280 | request.session['email_reason'] = _("We’ve just emailed " |
1281 | @@ -222,16 +258,20 @@ |
1282 | |
1283 | |
1284 | @check_readonly |
1285 | -def confirm_account(request, authtoken): |
1286 | +def confirm_account(request, authtoken, email_address): |
1287 | if request.user.is_authenticated(): |
1288 | return HttpResponseRedirect('/+logout-to-confirm') |
1289 | - atrequest = get_object_or_404(AuthToken, token=authtoken, |
1290 | - token_type=LoginTokenType.NEWPERSONLESSACCOUNT) |
1291 | - session_token = request.session.get('token_newaccount') |
1292 | - if session_token is None or session_token != atrequest.token: |
1293 | + |
1294 | + atrequest = verify_token_string(authtoken, email_address) |
1295 | + if (atrequest is None or |
1296 | + atrequest.token_type != LoginTokenType.NEWPERSONLESSACCOUNT): |
1297 | return HttpResponseRedirect('/+bad-token') |
1298 | + |
1299 | form = ConfirmNewAccountForm() |
1300 | - if request.method == 'POST': |
1301 | + |
1302 | + if request.method == 'GET': |
1303 | + pass |
1304 | + elif request.method == 'POST': |
1305 | form = ConfirmNewAccountForm(request.POST) |
1306 | if form.is_valid(): |
1307 | username = atrequest.email |
1308 | @@ -265,13 +305,21 @@ |
1309 | RequestContext(request, {'form': form})) |
1310 | |
1311 | |
1312 | +# TODO: Display a nice message at the login screen, like "Please login |
1313 | +# to {use this token,finish this action}." |
1314 | @login_required |
1315 | -def confirm_email(request, authtoken): |
1316 | - atrequest = get_object_or_404(AuthToken, token=authtoken, |
1317 | - token_type=LoginTokenType.VALIDATEEMAIL) |
1318 | +def confirm_email(request, authtoken, email_address): |
1319 | + atrequest = verify_token_string(authtoken, email_address) |
1320 | + if (atrequest is None or |
1321 | + atrequest.token_type != LoginTokenType.VALIDATEEMAIL): |
1322 | + return HttpResponseRedirect('/+bad-token') |
1323 | + |
1324 | email = get_object_or_404(EmailAddress, email__iexact=atrequest.email) |
1325 | - |
1326 | if request.user.id != email.account.id: |
1327 | + # The user is authenticated to a different account. |
1328 | + # Potentially, the token was leaked or intercepted. Let's |
1329 | + # delete it just in case. The real user can generate another |
1330 | + # token easily enough. |
1331 | atrequest.delete() |
1332 | raise Http404 |
1333 | |
1334 | @@ -380,7 +428,7 @@ |
1335 | account, email, email, LoginTokenType.PASSWORDRECOVERY, |
1336 | redirection_url=redirection_url) |
1337 | token.sendPasswordResetEmail() |
1338 | - request.session['token_forgotpassword'] = token.token |
1339 | + set_session_token_info(request.session, token) |
1340 | request.session['email_feedback'] = settings.FEEDBACK_TO_ADDRESS |
1341 | request.session['email_heading'] = _("Forgotten your password?") |
1342 | request.session['email_reason'] = _("We’ve just emailed " |
1343 | @@ -403,17 +451,17 @@ |
1344 | |
1345 | |
1346 | @guest_required |
1347 | -def reset_password(request, authtoken): |
1348 | - atrequest = get_object_or_404(AuthToken, token=authtoken, |
1349 | - token_type=LoginTokenType.PASSWORDRECOVERY) |
1350 | - account = atrequest.requester |
1351 | - session_token = request.session.get('token_forgotpassword') |
1352 | - if (session_token is None or session_token != atrequest.token or |
1353 | +def reset_password(request, authtoken, email_address): |
1354 | + atrequest = verify_token_string(authtoken, email_address) |
1355 | + account = atrequest and atrequest.requester |
1356 | + if (atrequest is None or |
1357 | + atrequest.token_type != LoginTokenType.PASSWORDRECOVERY or |
1358 | not account.can_reset_password): |
1359 | # we hide the fact the the account is inactive, to avoid |
1360 | # exposing valid accounts. however, deactivated accounts can be |
1361 | # reactivated. |
1362 | return HttpResponseRedirect('/+bad-token') |
1363 | + |
1364 | if request.method == 'POST': |
1365 | form = ResetPasswordForm(request.POST) |
1366 | if form.is_valid(): |
1367 | |
1368 | === modified file 'identityprovider/views/utils.py' |
1369 | --- identityprovider/views/utils.py 2010-05-27 15:05:18 +0000 |
1370 | +++ identityprovider/views/utils.py 2010-06-22 23:23:25 +0000 |
1371 | @@ -1,6 +1,8 @@ |
1372 | # Copyright 2010 Canonical Ltd. This software is licensed under the |
1373 | # GNU Affero General Public License version 3 (see the file LICENSE). |
1374 | |
1375 | +from django.conf import settings |
1376 | + |
1377 | from identityprovider.models import OpenIDRPConfig |
1378 | |
1379 | |
1380 | @@ -10,6 +12,15 @@ |
1381 | else: |
1382 | return "/" |
1383 | |
1384 | +def set_session_token_info(session, token): |
1385 | + """Places information about the token into the session in a |
1386 | + uniform place. Specifically, places the token's e-mail address at |
1387 | + session['token_email']. Also, if settings.ENABLE_TOKEN_DEBUG is |
1388 | + true, places the token string at session['token_string'].""" |
1389 | + session['token_email'] = token.email |
1390 | + if getattr(settings, 'ENABLE_TOKEN_DEBUG', False): |
1391 | + session['token_string'] = token.token |
1392 | + |
1393 | |
1394 | def get_rpconfig(trust_root): |
1395 | alternatives = [trust_root] |