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

Proposed by David Owen
Status: Merged
Merged at revision: 107
Proposed branch: lp:~canonical-isd-hackers/canonical-identity-provider/bug_608920_csrf_token
Merge into: lp:canonical-identity-provider/release
Diff against target: 89 lines (+63/-2)
2 files modified
identityprovider/tests/test_middleware.py (+60/-2)
identityprovider/views/server.py (+3/-0)
To merge this branch: bzr merge lp:~canonical-isd-hackers/canonical-identity-provider/bug_608920_csrf_token
Reviewer Review Type Date Requested Status
Ricardo Kirkner (community) Approve
Review via email: mp+33412@code.launchpad.net

Description of the change

= Overview =

CSRF protection inserts CSRF tokens into all outgoing forms that
aren't marked as exempt (or whose generating views aren't marked as
exempt).

SSO has been inserting CSRF tokens into forms that submit to LP and
other places. Specifically, when the OpenID return_to URL is to long
to allow a GET response, we generate a form for the user to POST.
This form, as it submits to LP, shouldn't have any CSRF tokens in it.

= Details =

The view (+decide) that generates the OpenID POST reponse also
generates several other, user-visible responses. Some of these
redirect, and one other generates a form that submits back to +decide.

The OpenID POST *response* is marked as exempt (not the entire view).

The new test verifies that other aspects of +decide work as expected,
especially that the other form (submitting back to SSO) is still
CSRF-protected.

To post a comment you must log in.
Revision history for this message
Ricardo Kirkner (ricardokirkner) wrote :

Very clean and elegant test. Reads almost like a doctest. Keep up the good work!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'identityprovider/tests/test_middleware.py'
2--- identityprovider/tests/test_middleware.py 2010-06-24 13:36:36 +0000
3+++ identityprovider/tests/test_middleware.py 2010-08-23 16:46:41 +0000
4@@ -12,9 +12,11 @@
5 from django.http import QueryDict
6 import django.test.client
7
8+from openid.message import (IDENTIFIER_SELECT, OPENID1_URL_LIMIT, OPENID2_NS)
9+
10 from identityprovider.tests.utils import (BasicAccountTestCase, MockRequest,
11- TestCase)
12-from identityprovider.models import EmailAddress, Account
13+ OpenIDProviderTestCase, TestCase)
14+from identityprovider.models import Account, EmailAddress, OpenIDRPConfig
15 from identityprovider.middleware.exception import LogExceptionMiddleware
16 from identityprovider.middleware.useraccount import (
17 UserAccountConversionMiddleware)
18@@ -255,3 +257,59 @@
19 def test_multiple_windows_old_cookie_logged_out(self):
20 self._login_multiple_windows(start_with_old_cookie=True,
21 logout_before_stale_login=True)
22+
23+ def test_csrf_in_openid_forms(self):
24+ trust_root = 'http://localhost/'
25+ return_to = trust_root + (OPENID1_URL_LIMIT * 'a')
26+
27+ # Setup the trust_root to be trusted for OpenID operations.
28+ self.rpconfig = OpenIDRPConfig(trust_root=trust_root)
29+ self.rpconfig.save()
30+
31+ # 1. Establish a shared secret with the Provider.
32+ r = self.client.get('/+openid', {
33+ 'openid.mode': 'associate',
34+ 'openid.assoc_type': 'HMAC-SHA1'})
35+ [assoc_handle] = re.findall('assoc_handle:(.*)', r.content)
36+
37+ # Reset cookies, just in case, because after this we pretend
38+ # to be the user.
39+ self.client.cookies.clear()
40+
41+ # 2. RP directs user to Provider for checkid_setup and login.
42+ r = self.client.get('/+openid', {
43+ 'openid.mode': 'checkid_setup',
44+ 'openid.realm': trust_root,
45+ 'openid.return_to': return_to,
46+ 'openid.ns': OPENID2_NS,
47+ 'openid.identity': IDENTIFIER_SELECT,
48+ 'openid.claimed_id': 'http://openid.launchpad.dev/+id/mark_oid',
49+ 'openid.assoc_handle': assoc_handle,
50+ }, follow=True)
51+ self.assertTemplateUsed(r, 'registration/login.html')
52+
53+ [oid_token] = re.findall(r'http://[^/]+/([^/]+)/\+decide',
54+ r.redirect_chain[-1][0])
55+
56+ # 3. User logs in to SSO, but is allowed to decide whether to
57+ # continue back to RP.
58+ r = self.client.post('/%s/+login' % oid_token, {
59+ 'email': 'mark@example.com',
60+ 'password': 'test'
61+ }, follow=True)
62+ self.assertTemplateUsed(r, 'decide.html')
63+
64+ csrf_token = _extract_csrf_token(r)
65+ # We *must* have a CSRF token on the form that submits to SSO.
66+ self.assertNotEquals(csrf_token, None)
67+
68+ # 4. User decides to continue on to RP.
69+ r = self.client.post('/%s/+decide' % oid_token, {
70+ 'csrfmiddlewaretoken': csrf_token,
71+ 'yes': 'yes'
72+ }, follow=True)
73+ self.assertTemplateUsed(r, 'post-assertion.html')
74+
75+ # We *must not* have a CSRF token on the form that submits
76+ # to the RP.
77+ self.assertEquals(_extract_csrf_token(r), None)
78
79=== modified file 'identityprovider/views/server.py'
80--- identityprovider/views/server.py 2010-08-04 23:02:20 +0000
81+++ identityprovider/views/server.py 2010-08-23 16:46:41 +0000
82@@ -508,6 +508,9 @@
83 r.content = loader.render_to_string("post-assertion.html", {
84 'form': r.content})
85 r['Content-Type'] = 'text/html'
86+ # Also, as this is POSTing to another server, disabled CSRF
87+ # protection.
88+ r.csrf_exempt = True
89 return r
90
91