Merge lp:~mhall119/django-openid-auth/provides-784239 into lp:~django-openid-auth/django-openid-auth/trunk
- provides-784239
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 85 |
Proposed branch: | lp:~mhall119/django-openid-auth/provides-784239 |
Merge into: | lp:~django-openid-auth/django-openid-auth/trunk |
Prerequisite: | lp:~mhall119/django-openid-auth/django-yubikey-auth |
Diff against target: |
462 lines (+261/-28) 5 files modified
README.txt (+12/-0) django_openid_auth/auth.py (+15/-12) django_openid_auth/exceptions.py (+68/-0) django_openid_auth/tests/test_views.py (+150/-4) django_openid_auth/views.py (+16/-12) |
To merge this branch: | bzr merge lp:~mhall119/django-openid-auth/provides-784239 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Anthony Lenton | Approve | ||
Software Center Agent (community) | Approve | ||
Review via email: mp+61497@code.launchpad.net |
Commit message
Allow authentication exceptions to bubble up and get passed along to the failure view.
Description of the change
Overview
========
Allow exceptions thrown during authentication to be used to inform the user of the cause of a login failure.
Details
=======
Previously we silently swallowed exceptions during calls to authenticate. Instead we will catch them and pass them along to the failure view so the user can be told explicitly what the problem was with their login attempt.
Anthony Lenton (elachuni) wrote : | # |
- 85. By Michael Hall
-
Cleanup exception class heirarchy and imports, make overriding the login failure handler a settings option, add additional test cases to veryfiy that overriding the handler works and that the proper exceptions are being passed
- 86. By Michael Hall
-
Merge from prerequisite branch
- 87. By Michael Hall
-
Merge from trunk
- 88. By Michael Hall
-
Merge from prerequisite branch
- 89. By Michael Hall
-
Fix syntax error from merge
Software Center Agent (software-center-agent) wrote : | # |
Thanks Michael!
Anthony Lenton (elachuni) : | # |
Preview Diff
1 | === modified file 'README.txt' |
2 | --- README.txt 2011-08-23 18:55:24 +0000 |
3 | +++ README.txt 2011-08-23 18:55:24 +0000 |
4 | @@ -154,3 +154,15 @@ |
5 | If the user's OpenID provider supports the PAPE extension and provides the Physical Multifactor authentication policy, this will |
6 | cause the OpenID login to fail if the user does not provide valid physical authentication to the provider. |
7 | |
8 | +== Override Login Failure Handling == |
9 | + |
10 | +You can optionally provide your own handler for login failures by adding the following setting: |
11 | + |
12 | + OPENID_RENDER_FAILURE = failure_handler_function |
13 | + |
14 | +Where failure_handler_function is a function reference that will take the following parameters: |
15 | + |
16 | + def failure_handler_function(request, message, status=None, template_name=None, exception=None) |
17 | + |
18 | +This function must return a Django.http.HttpResponse instance. |
19 | + |
20 | |
21 | === modified file 'django_openid_auth/auth.py' |
22 | --- django_openid_auth/auth.py 2011-08-23 18:55:24 +0000 |
23 | +++ django_openid_auth/auth.py 2011-08-23 18:55:24 +0000 |
24 | @@ -37,16 +37,13 @@ |
25 | |
26 | from django_openid_auth import teams |
27 | from django_openid_auth.models import UserOpenID |
28 | - |
29 | - |
30 | -class IdentityAlreadyClaimed(Exception): |
31 | - pass |
32 | - |
33 | -class StrictUsernameViolation(Exception): |
34 | - pass |
35 | - |
36 | -class RequiredAttributeNotReturned(Exception): |
37 | - pass |
38 | +from django_openid_auth.exceptions import ( |
39 | + IdentityAlreadyClaimed, |
40 | + DuplicateUsernameViolation, |
41 | + MissingUsernameViolation, |
42 | + MissingPhysicalMultiFactor, |
43 | + RequiredAttributeNotReturned, |
44 | +) |
45 | |
46 | class OpenIDBackend: |
47 | """A django.contrib.auth backend that authenticates the user based on |
48 | @@ -92,7 +89,7 @@ |
49 | pape_response = pape.Response.fromSuccessResponse(openid_response) |
50 | if pape_response is None or \ |
51 | pape.AUTH_MULTI_FACTOR_PHYSICAL not in pape_response.auth_policies: |
52 | - return None |
53 | + raise MissingPhysicalMultiFactor() |
54 | |
55 | teams_response = teams.TeamsResponse.fromSuccessResponse( |
56 | openid_response) |
57 | @@ -150,6 +147,12 @@ |
58 | first_name=first_name, last_name=last_name) |
59 | |
60 | def _get_available_username(self, nickname, identity_url): |
61 | + # If we're being strict about usernames, throw an error if we didn't |
62 | + # get one back from the provider |
63 | + if getattr(settings, 'OPENID_STRICT_USERNAMES', False): |
64 | + if nickname is None or nickname == '': |
65 | + raise MissingUsernameViolation() |
66 | + |
67 | # If we don't have a nickname, and we're not being strict, use a default |
68 | nickname = nickname or 'openiduser' |
69 | |
70 | @@ -184,7 +187,7 @@ |
71 | |
72 | if getattr(settings, 'OPENID_STRICT_USERNAMES', False): |
73 | if User.objects.filter(username__exact=nickname).count() > 0: |
74 | - raise StrictUsernameViolation( |
75 | + raise DuplicateUsernameViolation( |
76 | "The username (%s) with which you tried to log in is " |
77 | "already in use for a different account." % nickname) |
78 | |
79 | |
80 | === added file 'django_openid_auth/exceptions.py' |
81 | --- django_openid_auth/exceptions.py 1970-01-01 00:00:00 +0000 |
82 | +++ django_openid_auth/exceptions.py 2011-08-23 18:55:24 +0000 |
83 | @@ -0,0 +1,68 @@ |
84 | +# django-openid-auth - OpenID integration for django.contrib.auth |
85 | +# |
86 | +# Copyright (C) 2008-2010 Canonical Ltd. |
87 | +# |
88 | +# Redistribution and use in source and binary forms, with or without |
89 | +# modification, are permitted provided that the following conditions |
90 | +# are met: |
91 | +# |
92 | +# * Redistributions of source code must retain the above copyright |
93 | +# notice, this list of conditions and the following disclaimer. |
94 | +# |
95 | +# * Redistributions in binary form must reproduce the above copyright |
96 | +# notice, this list of conditions and the following disclaimer in the |
97 | +# documentation and/or other materials provided with the distribution. |
98 | +# |
99 | +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
100 | +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
101 | +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS |
102 | +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE |
103 | +# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, |
104 | +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, |
105 | +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
106 | +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
107 | +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT |
108 | +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN |
109 | +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE |
110 | +# POSSIBILITY OF SUCH DAMAGE. |
111 | + |
112 | +"""Exception classes thrown by OpenID Authentication and Validation.""" |
113 | + |
114 | +class DjangoOpenIDException(Exception): |
115 | + pass |
116 | + |
117 | +class RequiredAttributeNotReturned(DjangoOpenIDException): |
118 | + pass |
119 | + |
120 | +class IdentityAlreadyClaimed(DjangoOpenIDException): |
121 | + |
122 | + def __init__(self, message=None): |
123 | + if message is None: |
124 | + self.message = "Another user already exists for your selected OpenID" |
125 | + else: |
126 | + self.message = message |
127 | + |
128 | +class DuplicateUsernameViolation(DjangoOpenIDException): |
129 | + |
130 | + def __init__(self, message=None): |
131 | + if message is None: |
132 | + self.message = "Your desired username is already being used." |
133 | + else: |
134 | + self.message = message |
135 | + |
136 | +class MissingUsernameViolation(DjangoOpenIDException): |
137 | + |
138 | + def __init__(self, message=None): |
139 | + if message is None: |
140 | + self.message = "No nickname given for your account." |
141 | + else: |
142 | + self.message = message |
143 | + |
144 | +class MissingPhysicalMultiFactor(DjangoOpenIDException): |
145 | + |
146 | + def __init__(self, message=None): |
147 | + if message is None: |
148 | + self.message = "Login requires physical multi-factor authentication." |
149 | + else: |
150 | + self.message = message |
151 | + |
152 | |
153 | === modified file 'django_openid_auth/tests/test_views.py' |
154 | --- django_openid_auth/tests/test_views.py 2011-08-23 18:55:24 +0000 |
155 | +++ django_openid_auth/tests/test_views.py 2011-08-23 18:55:24 +0000 |
156 | @@ -32,7 +32,7 @@ |
157 | |
158 | from django.conf import settings |
159 | from django.contrib.auth.models import User, Group |
160 | -from django.http import HttpRequest |
161 | +from django.http import HttpRequest, HttpResponse |
162 | from django.test import TestCase |
163 | from openid.consumer.consumer import Consumer, SuccessResponse |
164 | from openid.consumer.discover import OpenIDServiceEndpoint |
165 | @@ -53,11 +53,15 @@ |
166 | from django_openid_auth.auth import OpenIDBackend |
167 | from django_openid_auth.signals import openid_login_complete |
168 | from django_openid_auth.store import DjangoOpenIDStore |
169 | - |
170 | +from django_openid_auth.exceptions import ( |
171 | + MissingUsernameViolation, |
172 | + DuplicateUsernameViolation, |
173 | + MissingPhysicalMultiFactor, |
174 | + RequiredAttributeNotReturned, |
175 | +) |
176 | |
177 | ET = importElementTree() |
178 | |
179 | - |
180 | class StubOpenIDProvider(HTTPFetcher): |
181 | |
182 | def __init__(self, base_url): |
183 | @@ -179,7 +183,9 @@ |
184 | self.old_use_as_admin_login = getattr(settings, 'OPENID_USE_AS_ADMIN_LOGIN', False) |
185 | self.old_follow_renames = getattr(settings, 'OPENID_FOLLOW_RENAMES', False) |
186 | self.old_physical_multifactor = getattr(settings, 'OPENID_PHYSICAL_MULTIFACTOR_REQUIRED', False) |
187 | + self.old_login_render_failure = getattr(settings, 'OPENID_RENDER_FAILURE', None) |
188 | self.old_consumer_complete = Consumer.complete |
189 | + |
190 | self.old_required_fields = getattr( |
191 | settings, 'OPENID_SREG_REQUIRED_FIELDS', []) |
192 | |
193 | @@ -203,6 +209,7 @@ |
194 | settings.OPENID_USE_AS_ADMIN_LOGIN = self.old_use_as_admin_login |
195 | settings.OPENID_FOLLOW_RENAMES = self.old_follow_renames |
196 | settings.OPENID_PHYSICAL_MULTIFACTOR_REQUIRED = self.old_physical_multifactor |
197 | + settings.OPENID_RENDER_FAILURE = self.old_login_render_failure |
198 | Consumer.complete = self.old_consumer_complete |
199 | settings.OPENID_SREG_REQUIRED_FIELDS = self.old_required_fields |
200 | |
201 | @@ -485,6 +492,63 @@ |
202 | |
203 | response = self.complete(openid_response) |
204 | self.assertEquals(403, response.status_code) |
205 | + self.assertContains(response, '<h1>OpenID failed</h1>', status_code=403) |
206 | + self.assertContains(response, '<p>Login requires physical multi-factor authentication.</p>', status_code=403) |
207 | + |
208 | + def test_login_physical_multifactor_not_provided_override(self): |
209 | + settings.OPENID_PHYSICAL_MULTIFACTOR_REQUIRED = True |
210 | + preferred_auth = pape.AUTH_MULTI_FACTOR_PHYSICAL |
211 | + self.provider.type_uris.append(pape.ns_uri) |
212 | + |
213 | + # Override the login_failure handler |
214 | + def mock_login_failure_handler(request, message, status=403, |
215 | + template_name=None, |
216 | + exception=None): |
217 | + self.assertTrue(isinstance(exception, MissingPhysicalMultiFactor)) |
218 | + return HttpResponse('Test Failure Override', status=200) |
219 | + settings.OPENID_RENDER_FAILURE = mock_login_failure_handler |
220 | + |
221 | + def mock_complete(this, request_args, return_to): |
222 | + request = {'openid.mode': 'checkid_setup', |
223 | + 'openid.trust_root': 'http://localhost/', |
224 | + 'openid.return_to': 'http://localhost/', |
225 | + 'openid.identity': IDENTIFIER_SELECT, |
226 | + 'openid.ns.pape' : pape.ns_uri, |
227 | + 'openid.pape.auth_policies': request_args.get('openid.pape.auth_policies', pape.AUTH_NONE), |
228 | + } |
229 | + openid_server = self.provider.server |
230 | + orequest = openid_server.decodeRequest(request) |
231 | + response = SuccessResponse( |
232 | + self.endpoint, orequest.message, |
233 | + signed_fields=['openid.pape.auth_policies',]) |
234 | + return response |
235 | + Consumer.complete = mock_complete |
236 | + |
237 | + user = User.objects.create_user('testuser', 'test@example.com') |
238 | + useropenid = UserOpenID( |
239 | + user=user, |
240 | + claimed_id='http://example.com/identity', |
241 | + display_id='http://example.com/identity') |
242 | + useropenid.save() |
243 | + |
244 | + openid_req = {'openid_identifier': 'http://example.com/identity', |
245 | + 'next': '/getuser/'} |
246 | + openid_resp = {'nickname': 'testuser', 'fullname': 'Openid User', |
247 | + 'email': 'test@example.com'} |
248 | + |
249 | + openid_request = self._get_login_request(openid_req) |
250 | + openid_response = self._get_login_response(openid_request, openid_req, openid_resp, use_pape=pape.AUTH_NONE) |
251 | + |
252 | + response_auth = openid_request.message.getArg( |
253 | + 'http://specs.openid.net/extensions/pape/1.0', |
254 | + 'auth_policies', |
255 | + ) |
256 | + self.assertNotEqual(response_auth, preferred_auth) |
257 | + |
258 | + # Status code should be 200, since we over-rode the login_failure handler |
259 | + response = self.complete(openid_response) |
260 | + self.assertEquals(200, response.status_code) |
261 | + self.assertContains(response, 'Test Failure Override') |
262 | |
263 | def test_login_without_nickname(self): |
264 | settings.OPENID_CREATE_USERS = True |
265 | @@ -680,6 +744,7 @@ |
266 | def test_strict_username_no_nickname(self): |
267 | settings.OPENID_CREATE_USERS = True |
268 | settings.OPENID_STRICT_USERNAMES = True |
269 | + settings.OPENID_SREG_REQUIRED_FIELDS = [] |
270 | |
271 | # Posting in an identity URL begins the authentication request: |
272 | response = self.client.post('/openid/login/', |
273 | @@ -701,6 +766,44 @@ |
274 | |
275 | # Status code should be 403: Forbidden |
276 | self.assertEquals(403, response.status_code) |
277 | + self.assertContains(response, '<h1>OpenID failed</h1>', status_code=403) |
278 | + self.assertContains(response, "An attribute required for logging in was not returned " |
279 | + "(nickname)", status_code=403) |
280 | + |
281 | + def test_strict_username_no_nickname_override(self): |
282 | + settings.OPENID_CREATE_USERS = True |
283 | + settings.OPENID_STRICT_USERNAMES = True |
284 | + settings.OPENID_SREG_REQUIRED_FIELDS = [] |
285 | + |
286 | + # Override the login_failure handler |
287 | + def mock_login_failure_handler(request, message, status=403, |
288 | + template_name=None, |
289 | + exception=None): |
290 | + self.assertTrue(isinstance(exception, (RequiredAttributeNotReturned, MissingUsernameViolation))) |
291 | + return HttpResponse('Test Failure Override', status=200) |
292 | + settings.OPENID_RENDER_FAILURE = mock_login_failure_handler |
293 | + |
294 | + # Posting in an identity URL begins the authentication request: |
295 | + response = self.client.post('/openid/login/', |
296 | + {'openid_identifier': 'http://example.com/identity', |
297 | + 'next': '/getuser/'}) |
298 | + self.assertContains(response, 'OpenID transaction in progress') |
299 | + |
300 | + # Complete the request, passing back some simple registration |
301 | + # data. The user is redirected to the next URL. |
302 | + openid_request = self.provider.parseFormPost(response.content) |
303 | + sreg_request = sreg.SRegRequest.fromOpenIDRequest(openid_request) |
304 | + openid_response = openid_request.answer(True) |
305 | + sreg_response = sreg.SRegResponse.extractResponse( |
306 | + sreg_request, {'nickname': '', # No nickname |
307 | + 'fullname': 'Some User', |
308 | + 'email': 'foo@example.com'}) |
309 | + openid_response.addExtension(sreg_response) |
310 | + response = self.complete(openid_response) |
311 | + |
312 | + # Status code should be 200, since we over-rode the login_failure handler |
313 | + self.assertEquals(200, response.status_code) |
314 | + self.assertContains(response, 'Test Failure Override') |
315 | |
316 | def test_strict_username_duplicate_user(self): |
317 | settings.OPENID_CREATE_USERS = True |
318 | @@ -731,11 +834,54 @@ |
319 | response = self.complete(openid_response) |
320 | |
321 | # Status code should be 403: Forbidden |
322 | + self.assertEquals(403, response.status_code) |
323 | + self.assertContains(response, '<h1>OpenID failed</h1>', status_code=403) |
324 | self.assertContains(response, |
325 | "The username (someuser) with which you tried to log in is " |
326 | "already in use for a different account.", |
327 | status_code=403) |
328 | |
329 | + def test_strict_username_duplicate_user_override(self): |
330 | + settings.OPENID_CREATE_USERS = True |
331 | + settings.OPENID_STRICT_USERNAMES = True |
332 | + |
333 | + # Override the login_failure handler |
334 | + def mock_login_failure_handler(request, message, status=403, |
335 | + template_name=None, |
336 | + exception=None): |
337 | + self.assertTrue(isinstance(exception, DuplicateUsernameViolation)) |
338 | + return HttpResponse('Test Failure Override', status=200) |
339 | + settings.OPENID_RENDER_FAILURE = mock_login_failure_handler |
340 | + |
341 | + # Create a user with the same name as we'll pass back via sreg. |
342 | + user = User.objects.create_user('someuser', 'someone@example.com') |
343 | + useropenid = UserOpenID( |
344 | + user=user, |
345 | + claimed_id='http://example.com/different_identity', |
346 | + display_id='http://example.com/different_identity') |
347 | + useropenid.save() |
348 | + |
349 | + # Posting in an identity URL begins the authentication request: |
350 | + response = self.client.post('/openid/login/', |
351 | + {'openid_identifier': 'http://example.com/identity', |
352 | + 'next': '/getuser/'}) |
353 | + self.assertContains(response, 'OpenID transaction in progress') |
354 | + |
355 | + # Complete the request, passing back some simple registration |
356 | + # data. The user is redirected to the next URL. |
357 | + openid_request = self.provider.parseFormPost(response.content) |
358 | + sreg_request = sreg.SRegRequest.fromOpenIDRequest(openid_request) |
359 | + openid_response = openid_request.answer(True) |
360 | + sreg_response = sreg.SRegResponse.extractResponse( |
361 | + sreg_request, {'nickname': 'someuser', 'fullname': 'Some User', |
362 | + 'email': 'foo@example.com'}) |
363 | + openid_response.addExtension(sreg_response) |
364 | + response = self.complete(openid_response) |
365 | + |
366 | + # Status code should be 200, since we over-rode the login_failure handler |
367 | + self.assertEquals(200, response.status_code) |
368 | + self.assertContains(response, 'Test Failure Override') |
369 | + |
370 | def test_login_requires_sreg_required_fields(self): |
371 | # If any required attributes are not included in the response, |
372 | # we fail with a forbidden. |
373 | @@ -1054,7 +1200,7 @@ |
374 | self.assertTrue(self.signal_handler_called) |
375 | openid_login_complete.disconnect(login_callback) |
376 | |
377 | - |
378 | + |
379 | class HelperFunctionsTest(TestCase): |
380 | def test_sanitise_redirect_url(self): |
381 | settings.ALLOWED_EXTERNAL_OPENID_REDIRECT_DOMAINS = [ |
382 | |
383 | === modified file 'django_openid_auth/views.py' |
384 | --- django_openid_auth/views.py 2011-08-23 18:55:24 +0000 |
385 | +++ django_openid_auth/views.py 2011-08-23 18:55:24 +0000 |
386 | @@ -51,14 +51,14 @@ |
387 | from openid.extensions import sreg, ax, pape |
388 | |
389 | from django_openid_auth import teams |
390 | -from django_openid_auth.auth import ( |
391 | - RequiredAttributeNotReturned, |
392 | - StrictUsernameViolation, |
393 | - ) |
394 | from django_openid_auth.forms import OpenIDLoginForm |
395 | from django_openid_auth.models import UserOpenID |
396 | from django_openid_auth.signals import openid_login_complete |
397 | from django_openid_auth.store import DjangoOpenIDStore |
398 | +from django_openid_auth.exceptions import ( |
399 | + RequiredAttributeNotReturned, |
400 | + DjangoOpenIDException, |
401 | +) |
402 | |
403 | |
404 | next_url_re = re.compile('^/[-\w/]+$') |
405 | @@ -122,10 +122,11 @@ |
406 | |
407 | |
408 | def default_render_failure(request, message, status=403, |
409 | - template_name='openid/failure.html'): |
410 | + template_name='openid/failure.html', |
411 | + exception=None): |
412 | """Render an error page to the user.""" |
413 | data = render_to_string( |
414 | - template_name, dict(message=message), |
415 | + template_name, dict(message=message, exception=exception), |
416 | context_instance=RequestContext(request)) |
417 | return HttpResponse(data, status=status) |
418 | |
419 | @@ -175,7 +176,8 @@ |
420 | openid_request = consumer.begin(openid_url) |
421 | except DiscoveryFailure, exc: |
422 | return render_failure( |
423 | - request, "OpenID discovery error: %s" % (str(exc),), status=500) |
424 | + request, "OpenID discovery error: %s" % (str(exc),), status=500, |
425 | + exception=exc) |
426 | |
427 | # Request some user details. If the provider advertises support |
428 | # for attribute exchange, use that. |
429 | @@ -220,7 +222,6 @@ |
430 | pape_request = pape.Request(preferred_auth_policies=preferred_auth) |
431 | openid_request.addExtension(pape_request) |
432 | |
433 | - |
434 | # Request team info |
435 | teams_mapping_auto = getattr(settings, 'OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO', False) |
436 | teams_mapping_auto_blacklist = getattr(settings, 'OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO_BLACKLIST', []) |
437 | @@ -250,8 +251,11 @@ |
438 | |
439 | @csrf_exempt |
440 | def login_complete(request, redirect_field_name=REDIRECT_FIELD_NAME, |
441 | - render_failure=default_render_failure): |
442 | + render_failure=None): |
443 | redirect_to = request.REQUEST.get(redirect_field_name, '') |
444 | + render_failure = render_failure or \ |
445 | + getattr(settings, 'OPENID_RENDER_FAILURE', None) or \ |
446 | + default_render_failure |
447 | |
448 | openid_response = parse_openid_response(request) |
449 | if not openid_response: |
450 | @@ -261,9 +265,9 @@ |
451 | if openid_response.status == SUCCESS: |
452 | try: |
453 | user = authenticate(openid_response=openid_response) |
454 | - except (StrictUsernameViolation, RequiredAttributeNotReturned), e: |
455 | - return render_failure(request, e) |
456 | - |
457 | + except DjangoOpenIDException, e: |
458 | + return render_failure(request, e.message, exception=e) |
459 | + |
460 | if user is not None: |
461 | if user.is_active: |
462 | auth_login(request, user) |
Hi Michael,
I like the approach in general. A few comments/ suggestions, iolation make sense now? you only use it to inherit from it from two other classes that you raise separately and catch together with all other DjangoOpenIDExc eption. If you think eventually people will want to catch all StrictUsernameV iolations together then it's fine as is, otherwise removing it will flatten exception class tree and make it clearer I think.
- you import all defined exceptions on lines 145/151 of the diff, but then you don't seem to use them.
- does StrictUsernameV
- and ideally it would have a test or two I think (to ensure that the messages do indeed reach the user, for example?)
Thanks!