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