Merge lp:~canonical-isd-hackers/canonical-identity-provider/enforce-user-requirestwo-factor into lp:canonical-identity-provider/release

Proposed by Michael Foord
Status: Merged
Merged at revision: 324
Proposed branch: lp:~canonical-isd-hackers/canonical-identity-provider/enforce-user-requirestwo-factor
Merge into: lp:canonical-identity-provider/release
Diff against target: 798 lines (+264/-76)
15 files modified
identityprovider/decorators.py (+37/-0)
identityprovider/models/twofactor.py (+36/-0)
identityprovider/tests/acceptance/devices/add_device.py (+1/-1)
identityprovider/tests/acceptance/devices/login.py (+1/-1)
identityprovider/tests/acceptance/devices/prefs.py (+12/-2)
identityprovider/tests/acceptance/devices/remove_device.py (+1/-1)
identityprovider/tests/acceptance/devices/rename_device.py (+1/-1)
identityprovider/tests/acceptance/views_protected.csv (+8/-0)
identityprovider/tests/acceptance/views_protected.py (+44/-0)
identityprovider/tests/unit/test_decorators.py (+74/-2)
identityprovider/tests/unit/test_views_ui.py (+12/-13)
identityprovider/views/account.py (+8/-9)
identityprovider/views/devices.py (+10/-10)
identityprovider/views/server.py (+2/-2)
identityprovider/views/ui.py (+17/-34)
To merge this branch: bzr merge lp:~canonical-isd-hackers/canonical-identity-provider/enforce-user-requirestwo-factor
Reviewer Review Type Date Requested Status
Canonical ISD hackers Pending
Review via email: mp+91824@code.launchpad.net

Commit message

Adds new sso_login_required decorator to wrap up django's login_required and add twofactor checks

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 'identityprovider/decorators.py'
2--- identityprovider/decorators.py 2012-01-24 12:23:06 +0000
3+++ identityprovider/decorators.py 2012-02-09 22:30:07 +0000
4@@ -2,9 +2,17 @@
5 # GNU Affero General Public License version 3 (see the file LICENSE).
6
7 import logging
8+from functools import wraps
9
10 from django.template import RequestContext
11 from django.http import Http404, HttpResponseRedirect, HttpResponseForbidden
12+from django.contrib.auth import REDIRECT_FIELD_NAME
13+from django.utils.decorators import available_attrs
14+from django.contrib.auth.decorators import (
15+ login_required as django_login_required,
16+)
17+from django.contrib.auth.views import redirect_to_login
18+from django.core.urlresolvers import reverse
19 from django.core.cache import cache
20 from django.conf import settings
21 from django.template.loader import render_to_string
22@@ -16,6 +24,8 @@
23
24 from identityprovider import auth
25 from identityprovider.cookies import set_test_cookie, test_cookie_worked
26+from identityprovider.models import twofactor
27+
28
29 def guest_required(func):
30 @functools.wraps(func)
31@@ -38,6 +48,33 @@
32 return _dont_cache_decorator
33
34
35+def sso_login_required(function=None, redirect_field_name=REDIRECT_FIELD_NAME, login_url=None):
36+ """
37+ Decorator the wraps up django's login_required, and also checks for 2f
38+ """
39+ def decorator(view_func):
40+ @wraps(view_func, assigned=available_attrs(view_func))
41+ def _wrapped_view(request, *args, **kwargs):
42+ if (twofactor.user_requires_twofactor_auth(request, request.user) and
43+ not twofactor.is_authenticated(request)):
44+ # some views send token as None, which breaks reverse
45+ if 'token' in kwargs and kwargs['token'] == None:
46+ del kwargs['token']
47+ return redirect_to_login(
48+ request.get_full_path(),
49+ reverse('twofactor', kwargs=kwargs),
50+ redirect_field_name)
51+ return view_func(request, *args, **kwargs)
52+ return django_login_required(
53+ _wrapped_view,
54+ redirect_field_name,
55+ login_url)
56+
57+ if function:
58+ return decorator(function)
59+ return decorator
60+
61+
62 def check_readonly(func):
63 """ A readonly aware decorator.
64
65
66=== modified file 'identityprovider/models/twofactor.py'
67--- identityprovider/models/twofactor.py 2012-01-30 14:42:31 +0000
68+++ identityprovider/models/twofactor.py 2012-02-09 22:30:07 +0000
69@@ -1,10 +1,15 @@
70 # Copyright 2010-2012 Canonical Ltd. This software is licensed under the
71 # GNU Affero General Public License version 3 (see the file LICENSE).
72+
73+import logging
74+
75 from oath.hotp import accept_hotp
76 from django.db import models
77 from django.conf import settings
78 from django.utils.translation import ugettext as _
79
80+from gargoyle import gargoyle
81+
82 from identityprovider.models import Account
83 from identityprovider.utils import AuthenticationError
84
85@@ -20,6 +25,37 @@
86 else:
87 return None
88
89+TWOFACTOR_KEY = 'logged_in_via_two_factor'
90+
91+def is_twofactor_enabled(request):
92+ return gargoyle.is_active('TWOFACTOR', request)
93+
94+def login(request):
95+ request.session[TWOFACTOR_KEY] = True
96+
97+def logout(request):
98+ if TWOFACTOR_KEY in request.session:
99+ del request.session[TWOFACTOR_KEY]
100+
101+def is_authenticated(request):
102+ return request.session.get(TWOFACTOR_KEY, False)
103+
104+def site_requires_twofactor_auth(request, token):
105+ if not is_twofactor_enabled(request):
106+ return False
107+ # TODO check if site requires 2f (pape?)
108+ return False
109+
110+def user_requires_twofactor_auth(request, account):
111+ if not is_twofactor_enabled(request):
112+ return False
113+ if account.twofactor_required:
114+ if account.has_twofactor_devices():
115+ return True
116+ logging.warning('Account %s requires two-factor but has no devices' % account.openid_identifier)
117+ return False
118+
119+
120 class AuthenticationDevice(models.Model):
121 account = models.ForeignKey(Account, related_name='devices')
122 key = models.TextField()
123
124=== added file 'identityprovider/tests/acceptance/devices/__init__.py'
125=== modified file 'identityprovider/tests/acceptance/devices/add_device.py'
126--- identityprovider/tests/acceptance/devices/add_device.py 2012-01-27 16:28:56 +0000
127+++ identityprovider/tests/acceptance/devices/add_device.py 2012-02-09 22:30:07 +0000
128@@ -4,7 +4,7 @@
129 from sst.actions import *
130
131 from identityprovider.tests.acceptance import _helpers as helpers
132-from _helpers import click_add_device_button, click_add_new_device_link
133+from identityprovider.tests.acceptance.devices._helpers import click_add_device_button, click_add_new_device_link
134
135
136 config.set_base_url_from_env()
137
138=== modified file 'identityprovider/tests/acceptance/devices/login.py'
139--- identityprovider/tests/acceptance/devices/login.py 2012-02-03 13:57:54 +0000
140+++ identityprovider/tests/acceptance/devices/login.py 2012-02-09 22:30:07 +0000
141@@ -4,7 +4,7 @@
142 from canonical.isd.tests.sst import config
143 from sst.actions import *
144
145-from _helpers import add_device
146+from identityprovider.tests.acceptance.devices._helpers import add_device
147 from identityprovider.tests.acceptance import _helpers as helpers
148
149 def enter_otp(otp):
150
151=== modified file 'identityprovider/tests/acceptance/devices/prefs.py'
152--- identityprovider/tests/acceptance/devices/prefs.py 2012-01-27 16:28:56 +0000
153+++ identityprovider/tests/acceptance/devices/prefs.py 2012-02-09 22:30:07 +0000
154@@ -1,8 +1,9 @@
155 # Test user two factor preferences
156+from oath import hotp
157 from canonical.isd.tests.sst import config
158 from sst.actions import *
159
160-from _helpers import add_device
161+from identityprovider.tests.acceptance.devices._helpers import add_device
162 from identityprovider.tests.acceptance import _helpers as helpers
163
164
165@@ -26,7 +27,7 @@
166 disabled="disabled")
167
168 # Add an authentication device
169-add_device()
170+aes_key = add_device()
171
172 # Test preferences form is now enabled
173 assert_attribute(get_element(tag="input", name="two-factor", value="always"),
174@@ -45,7 +46,16 @@
175 set_radio_value(get_element(tag="input", name="two-factor", value="always"))
176 click_button("authentication-prefs")
177
178+# so we now should now have the twofactor auth form
179+assert_url('/two_factor_auth?next=/device-list')
180+
181+# 2 factor login
182+otp = hotp.hotp(aes_key, 1)
183+write_textfield('id_oath_token', otp)
184+click_button(get_element(type='submit'))
185+
186 # Check that "always" is now shown in the user preferences
187+assert_url('/device-list')
188 assert_attribute(get_element(tag="input", name="two-factor", value="always"),
189 'checked', 'true')
190 assert_attribute(get_element(tag="input", name="two-factor", value="as-needed"),
191
192=== modified file 'identityprovider/tests/acceptance/devices/remove_device.py'
193--- identityprovider/tests/acceptance/devices/remove_device.py 2012-01-27 16:29:24 +0000
194+++ identityprovider/tests/acceptance/devices/remove_device.py 2012-02-09 22:30:07 +0000
195@@ -3,7 +3,7 @@
196 from sst.actions import *
197
198 from identityprovider.tests.acceptance import _helpers as helpers
199-from _helpers import add_device
200+from identityprovider.tests.acceptance.devices._helpers import add_device
201
202
203 def click_remove_button():
204
205=== modified file 'identityprovider/tests/acceptance/devices/rename_device.py'
206--- identityprovider/tests/acceptance/devices/rename_device.py 2012-01-27 16:28:56 +0000
207+++ identityprovider/tests/acceptance/devices/rename_device.py 2012-02-09 22:30:07 +0000
208@@ -2,7 +2,7 @@
209 from sst.actions import *
210
211 from identityprovider.tests.acceptance import _helpers as helpers
212-from _helpers import (
213+from identityprovider.tests.acceptance.devices._helpers import (
214 add_device,
215 assert_device,
216 assert_no_device,
217
218=== added file 'identityprovider/tests/acceptance/views_protected.csv'
219--- identityprovider/tests/acceptance/views_protected.csv 1970-01-01 00:00:00 +0000
220+++ identityprovider/tests/acceptance/views_protected.csv 2012-02-09 22:30:07 +0000
221@@ -0,0 +1,8 @@
222+url
223+'/+edit'
224+'/+emails'
225+'/+index'
226+'/+applications'
227+'/+new-email'
228+'/+verify-email'
229+'/+remove-email'
230
231=== added file 'identityprovider/tests/acceptance/views_protected.py'
232--- identityprovider/tests/acceptance/views_protected.py 1970-01-01 00:00:00 +0000
233+++ identityprovider/tests/acceptance/views_protected.py 2012-02-09 22:30:07 +0000
234@@ -0,0 +1,44 @@
235+import sys
236+from urllib import quote
237+
238+from canonical.isd.tests.sst import config
239+from sst.actions import *
240+
241+import _helpers as helpers
242+from devices import _helpers as dev_helpers
243+
244+config.set_base_url_from_env()
245+
246+try:
247+ import _views_protected_shared as shared
248+except ImportError:
249+ class module(object):
250+ pass
251+ m = module()
252+
253+ m.twofactor_password = 'Admin009'
254+ m.twofactor_email = helpers.register_account(password=m.twofactor_password)
255+ m.aes_key = dev_helpers.add_device()
256+ # Change the preference to always require 2 factor authentication and save
257+ set_radio_value(get_element(tag="input", name="two-factor", value="always"))
258+ click_button("authentication-prefs")
259+ assert_url('/two_factor_auth?next=/device-list')
260+ helpers.logout()
261+
262+ sys.modules['_views_protected_shared'] = m
263+ import _views_protected_shared as shared
264+
265+# are we redirected?
266+go_to(url)
267+wait_for(assert_title, 'Log in')
268+
269+helpers.login(shared.twofactor_email, shared.twofactor_password)
270+
271+go_to(url)
272+# should be redirected
273+assert_url('/two_factor_auth?next=%s' % quote(url))
274+
275+helpers.logout()
276+
277+
278+
279
280=== modified file 'identityprovider/tests/unit/test_decorators.py'
281--- identityprovider/tests/unit/test_decorators.py 2012-01-10 21:17:33 +0000
282+++ identityprovider/tests/unit/test_decorators.py 2012-02-09 22:30:07 +0000
283@@ -1,9 +1,9 @@
284 from datetime import datetime
285 from django.test import TestCase
286
287-from mock import Mock, patch
288+from mock import Mock, patch, MagicMock
289
290-from identityprovider.decorators import ratelimit
291+from identityprovider.decorators import ratelimit, sso_login_required
292
293
294 class RatelimitTestCase(TestCase):
295@@ -32,3 +32,75 @@
296 limiter.current_key(request)
297
298 self.assertEqual(mock_datetime.utcnow.called, True)
299+
300+
301+class SSOLoginRequiredTestCase(TestCase):
302+
303+ @staticmethod
304+ def fake_view(request, *args, **kwargs):
305+ return "SUCCESS"
306+
307+ def mock_request(self, authed, path='/'):
308+ mock_request = MagicMock()
309+ mock_request.method = 'GET'
310+ mock_request.user.is_authenticated.return_value = authed
311+ mock_request.POST = {}
312+ mock_request.META = {}
313+ mock_request.build_absolute_uri.return_value = path
314+ mock_request.get_full_path.return_value = path
315+ return mock_request
316+
317+ def test_django_login_check_still_works(self):
318+ view = sso_login_required(self.fake_view, login_url='/login')
319+ response = view(self.mock_request(False, '/target'))
320+ self.assertEqual(response.status_code, 302)
321+ self.assertEqual(response['Location'], '/login?next=/target')
322+
323+ @patch('identityprovider.decorators.twofactor.user_requires_twofactor_auth')
324+ def test_allowed_2f_not_required(self, mock_user):
325+ mock_user.return_value = False
326+ view = sso_login_required(self.fake_view, login_url='/login')
327+ response = view(self.mock_request(True))
328+ self.assertEqual(response, 'SUCCESS')
329+
330+ @patch('identityprovider.decorators.twofactor.is_authenticated')
331+ @patch('identityprovider.decorators.twofactor.user_requires_twofactor_auth')
332+ def test_allowed_when_2f_required_and_2f_authed(self, mock_user, mock_auth):
333+ mock_user.return_value = True
334+ mock_auth.return_value = True
335+ view = sso_login_required(self.fake_view, login_url='/login')
336+ response = view(self.mock_request(True))
337+ self.assertEqual(response, 'SUCCESS')
338+
339+ @patch('identityprovider.decorators.twofactor.is_authenticated')
340+ @patch('identityprovider.decorators.twofactor.user_requires_twofactor_auth')
341+ def test_redirected_when_2f_required_and_not_2f_authed(self, mock_user, mock_auth):
342+ mock_user.return_value = True
343+ mock_auth.return_value = False
344+ view = sso_login_required(self.fake_view, login_url='/login')
345+ response = view(self.mock_request(True, '/target'))
346+ self.assertEqual(response.status_code, 302)
347+ self.assertEqual(response['Location'], '/two_factor_auth?next=/target')
348+
349+ def test_decoration_no_params(self):
350+ @sso_login_required
351+ def view(request, *args, **kwargs):
352+ return "SUCCESS"
353+ response = view(self.mock_request(True, '/target'))
354+ # if this works, the decorator handles this invocation properly
355+ self.assertEqual(response, 'SUCCESS')
356+
357+ def test_decoration_with_paramss(self):
358+ @sso_login_required(login_url='/alt_login')
359+ def view(request, *args, **kwargs):
360+ return "SUCCESS"
361+ response = view(self.mock_request(True, '/target'))
362+ # if this works, the decorator handles this invocation properly
363+ self.assertEqual(response, 'SUCCESS')
364+
365+
366+
367+
368+
369+
370+
371
372=== modified file 'identityprovider/tests/unit/test_views_ui.py'
373--- identityprovider/tests/unit/test_views_ui.py 2012-02-09 11:47:25 +0000
374+++ identityprovider/tests/unit/test_views_ui.py 2012-02-09 22:30:07 +0000
375@@ -30,7 +30,7 @@
376 from identityprovider.models import EmailAddress, Person, OpenIDRPConfig
377 from identityprovider.models.const import EmailStatus
378 from identityprovider.views import ui, server
379-from identityprovider.models import Account, AuthToken
380+from identityprovider.models import Account, AuthToken, twofactor
381 from identityprovider.models.authtoken import (
382 InvalidEmailException,
383 create_token,
384@@ -1180,14 +1180,14 @@
385 data['next'] = next
386 return self.client.post('/two_factor_auth', data)
387
388- @patch('identityprovider.views.ui.site_requires_twofactor_auth')
389+ @patch('identityprovider.views.ui.twofactor.site_requires_twofactor_auth')
390 def test_when_site_does_not_require_twofactor_no_oath_field(self, mock_site):
391 mock_site.return_value = False
392 response = self.client.get('/+login')
393 tree = PyQuery(fromstring(response.content))
394 self.assertEqual(len(tree.find('input[name="oath_token"]')), 0)
395
396- @patch('identityprovider.views.ui.site_requires_twofactor_auth')
397+ @patch('identityprovider.views.ui.twofactor.site_requires_twofactor_auth')
398 def test_when_site_requires_twofactor_oath_field_is_present(self, mock_site):
399 mock_site.return_value = True
400 response = self.client.get('/+login')
401@@ -1197,7 +1197,7 @@
402 oath = inputs[0]
403 self.assertEqual(oath.attrib['autocomplete'], 'off')
404
405- @patch('identityprovider.views.ui.site_requires_twofactor_auth')
406+ @patch('identityprovider.views.ui.twofactor.site_requires_twofactor_auth')
407 def test_when_site_requires_twofactor_oath_field_is_present_lp(self, mock_site):
408 mock_site.return_value = True
409 response = self.client.get('/+login')
410@@ -1207,27 +1207,26 @@
411 oath = inputs[0]
412 self.assertEqual(oath.attrib['autocomplete'], 'off')
413
414-
415- @patch('identityprovider.views.ui.user_requires_twofactor_auth')
416+ @patch('identityprovider.views.ui.twofactor.user_requires_twofactor_auth')
417 def test_when_user_requires_twofactor_redirected(self, mock_user):
418 mock_user.return_value = True
419 r = self.do_login()
420 self.assertRedirects(r, '/two_factor_auth')
421
422- @patch('identityprovider.views.ui.user_requires_twofactor_auth')
423+ @patch('identityprovider.views.ui.twofactor.user_requires_twofactor_auth')
424 def test_when_user_requires_twofactor_redirected_with_token(self, mock_user):
425 mock_user.return_value = True
426 token = 'a' * 16
427 r = self.do_login(token=token)
428 self.assertRedirects(r, '/%s/two_factor_auth' % token)
429
430- @patch('identityprovider.views.ui.user_requires_twofactor_auth')
431+ @patch('identityprovider.views.ui.twofactor.user_requires_twofactor_auth')
432 def test_when_user_requires_twofactor_redirected_with_next(self, mock_user):
433 mock_user.return_value = True
434 r = self.do_login(next='/+edit')
435 self.assertRedirects(r, '/two_factor_auth?next=/%2Bedit')
436
437- @patch('identityprovider.views.ui.site_requires_twofactor_auth')
438+ @patch('identityprovider.views.ui.twofactor.site_requires_twofactor_auth')
439 @patch('identityprovider.views.ui.authenticate_device')
440 def test_full_twofactor_login_redirected_to_next(self, mock_auth, mock_site):
441 mock_auth.return_value = True
442@@ -1236,7 +1235,7 @@
443 mock_auth.assert_called_once_with(self.account, '123456')
444 self.assertRedirects(r, '/+edit')
445
446- @patch('identityprovider.views.ui.site_requires_twofactor_auth')
447+ @patch('identityprovider.views.ui.twofactor.site_requires_twofactor_auth')
448 def test_full_twofactor_login_illformed_token_displays_error(self, mock_site):
449 mock_site.return_value = True
450 response = self.do_login(next='/+edit', oath_token='XXX')
451@@ -1246,7 +1245,7 @@
452 self.assertEqual(len(error), 1)
453 self.assertEqual(error[0].text.strip(), "Please enter a 6-digit or 8-digit one-time password.")
454
455- @patch('identityprovider.views.ui.site_requires_twofactor_auth')
456+ @patch('identityprovider.views.ui.twofactor.site_requires_twofactor_auth')
457 @patch('identityprovider.views.ui.authenticate_device')
458 def test_full_twofactor_login_invalid_code_displays_error(self, mock_auth, mock_site):
459 mock_auth.side_effect = AuthenticationError("ERRORMSG")
460@@ -1314,12 +1313,12 @@
461 def test_user_requires_twofactor_auth(self):
462 def f(require2f, has_devices, returns, logs):
463 with patch('logging.warning') as mock_warning:
464- with patch('identityprovider.views.ui.is_twofactor_enabled') as mock_flag:
465+ with patch('identityprovider.views.ui.twofactor.is_twofactor_enabled') as mock_flag:
466 mock_flag.return_value = True
467 account = Mock()
468 account.twofactor_required = require2f
469 account.has_twofactor_devices.return_value = has_devices
470- r = ui.user_requires_twofactor_auth(None, account)
471+ r = twofactor.user_requires_twofactor_auth(None, account)
472 self.assertEqual(returns, r)
473 self.assertEqual(logs, mock_warning.called)
474 f(require2f=False, has_devices=False, returns=False, logs=False)
475
476=== modified file 'identityprovider/views/account.py'
477--- identityprovider/views/account.py 2012-01-13 14:49:47 +0000
478+++ identityprovider/views/account.py 2012-02-09 22:30:07 +0000
479@@ -4,7 +4,6 @@
480 from openid.yadis.constants import YADIS_HEADER_NAME, YADIS_CONTENT_TYPE
481
482 from django.conf import settings
483-from django.contrib.auth.decorators import login_required
484 from django.http import HttpResponseRedirect
485 from django.shortcuts import render_to_response, get_object_or_404
486 from django.template import RequestContext
487@@ -12,7 +11,7 @@
488 from django.views.decorators.vary import vary_on_headers
489
490 from identityprovider.cookies import set_test_cookie, test_cookie_worked
491-from identityprovider.decorators import check_readonly
492+from identityprovider.decorators import check_readonly, sso_login_required
493 from identityprovider.forms import EditAccountForm, LoginForm, NewEmailForm
494 from identityprovider.models import (send_validation_email_request,
495 EmailAddress)
496@@ -58,7 +57,7 @@
497 return set_test_cookie(render_to_response('cookies.html'))
498
499
500-@login_required
501+@sso_login_required
502 def account_index(request, token=None):
503 request.token = token
504 account = request.user
505@@ -86,7 +85,7 @@
506 return render_to_response('account/edit.html', context)
507
508
509-@login_required
510+@sso_login_required
511 def account_emails(request):
512 account = request.user
513 form = NewEmailForm()
514@@ -103,7 +102,7 @@
515 return render_to_response('account/emails.html', context)
516
517
518-@login_required
519+@sso_login_required
520 def account_deactivate(request, token=None):
521 request.token = token
522 import django.contrib.auth as auth
523@@ -152,7 +151,7 @@
524 )
525
526
527-@login_required
528+@sso_login_required
529 @check_readonly
530 def new_email(request, token=None, emailid=None):
531 request.token = token
532@@ -171,7 +170,7 @@
533 return render_to_response('account/new_email.html', context)
534
535
536-@login_required
537+@sso_login_required
538 def verify_email(request, token=None):
539 request.token = token
540 emailid = request.GET.get('id', 0)
541@@ -180,7 +179,7 @@
542 return _send_verification_email(request, email.email, token)
543
544
545-@login_required
546+@sso_login_required
547 def delete_email(request, token=None):
548 request.token = token
549 emailid = request.GET.get('id', 0)
550@@ -201,7 +200,7 @@
551 return render_to_response('account/delete_email.html', context)
552
553
554-@login_required
555+@sso_login_required
556 @check_readonly
557 def applications(request):
558 if request.method == 'POST':
559
560=== modified file 'identityprovider/views/devices.py'
561--- identityprovider/views/devices.py 2012-02-01 22:34:09 +0000
562+++ identityprovider/views/devices.py 2012-02-09 22:30:07 +0000
563@@ -4,7 +4,6 @@
564 import re
565 from base64 import b16encode
566
567-from django.contrib.auth.decorators import login_required
568 from django.core.urlresolvers import reverse
569 from django.conf import settings
570 from django.shortcuts import get_object_or_404, render_to_response
571@@ -18,6 +17,7 @@
572 from identityprovider.models import AuthenticationDevice
573 from identityprovider.models.twofactor import get_otp_type
574 from identityprovider.forms import HOTPDeviceForm, DeviceRenameForm
575+from identityprovider.decorators import sso_login_required
576
577 from .device_urldata import *
578 from .utils import HttpResponseRedirect, HttpResponseSeeOther, allow_only
579@@ -52,7 +52,7 @@
580 return rand_bytes(n)
581
582
583-@login_required
584+@sso_login_required
585 @require_twofactor
586 @allow_only('GET', 'POST')
587 def device_list(request):
588@@ -88,7 +88,7 @@
589 return render_to_response('device/list.html', context)
590
591
592-@login_required
593+@sso_login_required
594 @require_twofactor
595 @allow_only('GET', 'POST')
596 def device_addition(request):
597@@ -173,7 +173,7 @@
598
599
600
601-@login_required
602+@sso_login_required
603 @require_twofactor
604 @allow_only('GET', 'POST')
605 def device_verification(request, device_id):
606@@ -187,7 +187,7 @@
607 pass
608
609
610-@login_required
611+@sso_login_required
612 @require_twofactor
613 @allow_only('GET', 'POST')
614 def device_removal(request, device_id):
615@@ -235,10 +235,10 @@
616 device_list_path=reverse(DEVICE_LIST),
617 form=form))
618
619-device_rename = login_required(DeviceRenameView.as_view())
620-
621-
622-@login_required
623+device_rename = sso_login_required(DeviceRenameView.as_view())
624+
625+
626+@sso_login_required
627 @require_twofactor
628 @allow_only('GET', 'POST')
629 def pad_removal(request):
630@@ -248,7 +248,7 @@
631 return HttpResponseSeeOther('/device-list')
632
633
634-@login_required
635+@sso_login_required
636 @require_twofactor
637 @allow_only('GET', 'POST')
638 def device_lost(request):
639
640=== modified file 'identityprovider/views/server.py'
641--- identityprovider/views/server.py 2012-01-24 12:23:06 +0000
642+++ identityprovider/views/server.py 2012-02-09 22:30:07 +0000
643@@ -33,7 +33,6 @@
644 import django.contrib.auth as auth
645
646 from django.conf import settings
647-from django.contrib.auth.decorators import login_required
648 from django.http import (
649 Http404,
650 HttpResponse,
651@@ -55,6 +54,7 @@
652 import identityprovider.signed as signed
653
654 from identityprovider.const import LAUNCHPAD_TEAMS_NS
655+from identityprovider.decorators import sso_login_required
656 from identityprovider.forms import (
657 PreAuthorizeForm,
658 SRegRequestForm,
659@@ -532,7 +532,7 @@
660 openid_response.addExtension(sreg_response)
661
662
663-@login_required
664+@sso_login_required
665 def _process_decide(request, orequest, decision):
666 oresponse = orequest.answer(decision,
667 identity=request.user.openid_identity_url)
668
669=== modified file 'identityprovider/views/ui.py'
670--- identityprovider/views/ui.py 2012-02-06 16:54:13 +0000
671+++ identityprovider/views/ui.py 2012-02-09 22:30:07 +0000
672@@ -24,12 +24,16 @@
673 from django.utils.decorators import method_decorator
674 from django import forms
675
676-from gargoyle import gargoyle
677 from gargoyle.decorators import switch_is_active
678
679 from identityprovider.branding import current_brand
680-from identityprovider.decorators import (check_readonly, dont_cache,
681- guest_required, limitlogin, requires_cookies)
682+from identityprovider.decorators import (
683+ check_readonly,
684+ dont_cache,
685+ guest_required,
686+ limitlogin,
687+ requires_cookies,
688+ )
689 from identityprovider.forms import (
690 ConfirmNewAccountForm,
691 ForgotPasswordForm,
692@@ -48,6 +52,7 @@
693 from identityprovider.models.const import (AccountStatus, EmailStatus,
694 LoginTokenType)
695 from identityprovider.models.openidmodels import OpenIDRPConfig
696+from identityprovider.models import twofactor
697 import identityprovider.signed as signed
698 from identityprovider.utils import (
699 AuthenticationError,
700@@ -62,7 +67,6 @@
701 from identityprovider.views.utils import get_rpconfig, set_session_token_info
702
703
704-
705 logger = logging.getLogger('sso')
706
707
708@@ -91,30 +95,6 @@
709 errors.append(str(exc))
710
711
712-def is_twofactor_enabled(request):
713- return gargoyle.is_active('TWOFACTOR', request)
714-
715-
716-# these may need to live elsewhere, but putting here for now
717-def site_requires_twofactor_auth(request, token):
718- if not is_twofactor_enabled(request):
719- return False
720- # TODO check if site requires 2f (pape?)
721- return False
722-
723-
724-def user_requires_twofactor_auth(request, account):
725- if not is_twofactor_enabled(request):
726- return False
727- if account.twofactor_required:
728- if account.has_twofactor_devices():
729- return True
730- logging.warning('Account %s requires two-factor but has no devices' % account.openid_identifier)
731- return False
732-
733-
734-
735-
736 def authenticate_user(email, password):
737 """Attempts to authenticate a user. Returns account on success, or throws
738 AuthenticationErrors on failure"""
739@@ -191,12 +171,12 @@
740 template_name = 'registration/login.html'
741
742 def get_login_type(self, request, token):
743- twofactor = site_requires_twofactor_auth(request, token)
744- return twofactor, TwoFactorLoginForm if twofactor else LoginForm
745+ required = twofactor.site_requires_twofactor_auth(request, token)
746+ return required, TwoFactorLoginForm if required else LoginForm
747
748 def get(self, request, token=None, rpconfig=None):
749 rpconfig = self.setup(request, token, rpconfig)
750- twofactor, form_cls = self.get_login_type(request, token)
751+ required2f, form_cls = self.get_login_type(request, token)
752 # track login attempts
753 stats.increment('flows.login', key='requested', rpconfig=rpconfig)
754 return self.render(request, token, rpconfig, form_cls())
755@@ -223,7 +203,8 @@
756 # handle case where the user's account requires two factor but we
757 # didn't know that until we authenticated them
758 oath_token = form.cleaned_data.get('oath_token', None)
759- if not oath_token and user_requires_twofactor_auth(request, account):
760+ if (not oath_token and
761+ twofactor.user_requires_twofactor_auth(request, account)):
762 kwargs = {'token':token} if token else {}
763 url = reverse('twofactor', kwargs=kwargs)
764 if next and _is_safe_redirect_url(next):
765@@ -234,7 +215,7 @@
766 if site_twofactor or oath_token:
767 try:
768 authenticate_device(account, oath_token)
769- request.session['logged_in_via_two_factor'] = True
770+ twofactor.login(request)
771 except AuthenticationError as e:
772 return self.display_errors(request, token, rpconfig, form, e)
773
774@@ -279,7 +260,7 @@
775
776 try:
777 authenticate_device(account, form.cleaned_data['oath_token'])
778- request.session['logged_in_via_two_step'] = True
779+ twofactor.login(request)
780 stats.increment('flows.login', key='success', rpconfig=rpconfig)
781 except AuthenticationError as e:
782 return self.display_errors(request, token, rpconfig, form, e)
783@@ -297,6 +278,7 @@
784
785 @method_decorator(dont_cache)
786 @method_decorator(limitlogin())
787+ # for this page, we need to be logged in but NOT twofactored
788 @method_decorator(login_required)
789 @method_decorator(switch_is_active('TWOFACTOR'))
790 def dispatch(self, *args, **kwargs):
791@@ -380,6 +362,7 @@
792 # We don't want to lose session[token] when we log the user out
793 raw_orequest = request.session.get(token, None)
794 auth.logout(request)
795+ twofactor.logout(request)
796 self.set_orequest(request.session, token, raw_orequest)
797
798 template_file = ('%s/registration/logout.html' %