Merge lp:~canonical-isd-hackers/canonical-identity-provider/bug_589335_auth_tokens into lp:canonical-identity-provider/release

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
Reviewer Review Type Date Requested Status
Canonical ISD hackers Pending
Review via email: mp+27595@code.launchpad.net
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&rsquo;t received it?" %}</h2>
605 <p>{% blocktrans %}If you don&rsquo;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&rsquo;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&rsquo;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]