Merge lp:~cjwatson/canonical-identity-provider/openid-macaroon-discharge-v1 into lp:canonical-identity-provider/release

Proposed by Colin Watson
Status: Merged
Approved by: Colin Watson
Approved revision: no longer in the source branch.
Merged at revision: 1452
Proposed branch: lp:~cjwatson/canonical-identity-provider/openid-macaroon-discharge-v1
Merge into: lp:canonical-identity-provider/release
Diff against target: 672 lines (+141/-124)
10 files modified
src/api/v20/tests/test_handlers.py (+12/-5)
src/identityprovider/forms.py (+11/-6)
src/identityprovider/macaroon.py (+12/-26)
src/identityprovider/tests/test_auth.py (+14/-11)
src/identityprovider/tests/test_forms.py (+19/-20)
src/identityprovider/tests/test_macaroon.py (+12/-16)
src/identityprovider/tests/test_views_server.py (+17/-9)
src/identityprovider/tests/utils.py (+30/-8)
src/identityprovider/views/server.py (+9/-7)
src/webui/views/consumer.py (+5/-16)
To merge this branch: bzr merge lp:~cjwatson/canonical-identity-provider/openid-macaroon-discharge-v1
Reviewer Review Type Date Requested Status
Ricardo Kirkner (community) Approve
Review via email: mp+294272@code.launchpad.net

Commit message

Switch the OpenID discharge macaroon protocol over to the new caveat-only mechanism and v1 caveat IDs.

Description of the change

Switch the OpenID discharge macaroon protocol over to the new caveat-only mechanism and v1 caveat IDs.

v0 caveat IDs should in fact still be accepted for the time being, but we won't support further features built on top of this branch such as human-readable descriptions with them, and we only test the new format.

I didn't bother keeping the older OpenID macaroon protocol around, because AFAIK it has no users yet other than my WIP changes to Launchpad.

The Launchpad side of this is in https://code.launchpad.net/~cjwatson/launchpad/login-discharge-macaroon/+merge/294355.

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

Much nicer. Thanks!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/api/v20/tests/test_handlers.py'
2--- src/api/v20/tests/test_handlers.py 2016-05-05 20:58:23 +0000
3+++ src/api/v20/tests/test_handlers.py 2016-05-11 17:42:55 +0000
4@@ -2169,7 +2169,11 @@
5 self.login_failed_calls = []
6 login_failed.connect(self.track_failed_logins, dispatch_uid=self.id())
7
8- self.root_macaroon, self.macaroon_random_key, _ = self.build_macaroon()
9+ # XXX cjwatson 2016-05-11:
10+ # MacaroonRefreshHandlerTestCase.test_macaroon_refreshed needs to be
11+ # fixed before changing the version here.
12+ self.root_macaroon, self.macaroon_random_key, _ = self.build_macaroon(
13+ version=0)
14 self.data = dict(
15 email='foo@bar.com', password='foobar123',
16 macaroon=self.root_macaroon.serialize())
17@@ -2344,7 +2348,8 @@
18 check_login_failed=False)
19
20 def test_root_macaroon_not_for_sso(self):
21- macaroon, _, _ = self.build_macaroon(service_location="other service")
22+ macaroon, _, _ = self.build_macaroon(
23+ service_location="other service", version=1)
24 data = dict(email='foo@bar.com', password='foobar123',
25 macaroon=macaroon.serialize())
26 self.assert_failed_login('INVALID_CREDENTIALS', data,
27@@ -2360,7 +2365,8 @@
28
29 def test_multiple_macaroons(self):
30 # build *several* macaroons and send them all at once
31- roots, rkeys, _ = zip(*[self.build_macaroon() for _ in range(3)])
32+ roots, rkeys, _ = zip(*[
33+ self.build_macaroon(version=1) for _ in range(3)])
34 ids = range(3)
35 roots = dict(zip(ids, roots))
36 rkeys = dict(zip(ids, rkeys))
37@@ -2384,7 +2390,7 @@
38
39 def test_multiple_mixed(self):
40 bad_macaroon, _, _ = self.build_macaroon(
41- service_location="other service")
42+ service_location="other service", version=1)
43 data = dict(email='foo@bar.com', password='foobar123', macaroons=[
44 ('good', self.root_macaroon.serialize()),
45 ('bad', bad_macaroon.serialize()),
46@@ -2518,7 +2524,8 @@
47 check_login_failed=False)
48
49 def test_macaroon_bad_authinfo(self):
50- macaroon, _, _ = self.build_macaroon(service_location="other service")
51+ macaroon, _, _ = self.build_macaroon(
52+ service_location="other service", version=1)
53 data = dict(root_macaroon=macaroon.serialize(),
54 discharge_macaroon=self.discharge_macaroon.serialize())
55 self.assert_failed_login('INVALID_CREDENTIALS', data,
56
57=== modified file 'src/identityprovider/forms.py'
58--- src/identityprovider/forms.py 2016-05-03 12:21:13 +0000
59+++ src/identityprovider/forms.py 2016-05-11 17:42:55 +0000
60@@ -574,12 +574,13 @@
61 class MacaroonRequestForm(Form):
62 """A form object for user control over requesting discharge macaroons."""
63
64- def __init__(self, request, macaroon_request, rpconfig,
65+ def __init__(self, request, macaroon_request, rpconfig, trust_root,
66 approved_data=None):
67 self.request = request
68 self.macaroon_request = macaroon_request
69- self.root_macaroon = self.macaroon_request.root_macaroon
70+ self.caveat_id = self.macaroon_request.caveat_id
71 self.rpconfig = rpconfig
72+ self.trust_root = trust_root
73 self.approved_data = approved_data
74 self.data = self._get_data_for_user()
75 super(MacaroonRequestForm, self).__init__(self.data)
76@@ -590,14 +591,18 @@
77 def _get_data_for_user(self):
78 """Data to ask about in the form."""
79 data = {}
80- if self.root_macaroon:
81- data['macaroon'] = self.root_macaroon
82+ if self.caveat_id:
83+ data['macaroon'] = self.caveat_id
84 return data
85
86 def _init_fields(self, data):
87 """Initialise fields from the discharge macaroon request."""
88- for location, root_macaroon in data.iteritems():
89- label = 'Service authorization for %s' % root_macaroon.location
90+ for location, caveat_id in data.iteritems():
91+ if self.rpconfig:
92+ rp_displayname = self.rpconfig.displayname
93+ else:
94+ rp_displayname = self.trust_root
95+ label = 'Service authorization for %s' % rp_displayname
96 self.fields[location] = fields.BooleanField(
97 label=label, widget=forms.CheckboxInput(
98 check_test=self.check_test(location)))
99
100=== modified file 'src/identityprovider/macaroon.py'
101--- src/identityprovider/macaroon.py 2016-04-05 22:12:47 +0000
102+++ src/identityprovider/macaroon.py 2016-05-11 17:42:55 +0000
103@@ -19,8 +19,9 @@
104
105 It must be set to: http://ns.login.ubuntu.com/2016/openid-macaroon
106
107- openid.macaroon.root
108- The serialised root macaroon that the RP wants to discharge.
109+ openid.macaroon.caveat_id
110+ The SSO third-party caveat ID from the root macaroon that the RP wants
111+ to discharge.
112
113 As part of the positive assertion OpenID response, the following fields
114 will be provided:
115@@ -38,7 +39,6 @@
116 NamespaceAliasRegistrationError,
117 registerNamespaceAlias,
118 )
119-from pymacaroons import Macaroon
120
121 from identityprovider.const import MACAROON_NS
122
123@@ -101,26 +101,22 @@
124 class MacaroonRequest(Extension):
125 """An object to hold the state of a discharge macaroon request.
126
127- @ivar root_macaroon_raw: The serialised root macaroon to discharge.
128- @type root_macaroon_raw: str
129+ @ivar caveat_id: The SSO third-party caveat ID from the root macaroon
130+ that the RP wants to discharge.
131+ @type caveat_id: str
132
133- @group Consumer: requestField, requestMacaroon, getExtensionArgs,
134- addToOpenIDRequest
135+ @group Consumer: requestField, getExtensionArgs, addToOpenIDRequest
136 @group Server: fromOpenIDRequest, parseExtensionArgs
137 """
138
139 ns_alias = 'macaroon'
140
141- def __init__(self, root_macaroon_raw=None, macaroon_ns_uri=MACAROON_NS):
142+ def __init__(self, caveat_id=None, macaroon_ns_uri=MACAROON_NS):
143 """Initialize an empty discharge macaroon request."""
144 Extension.__init__(self)
145- self.root_macaroon_raw = root_macaroon_raw
146- self.root_macaroon = None
147+ self.caveat_id = caveat_id
148 self.ns_uri = macaroon_ns_uri
149
150- if root_macaroon_raw:
151- self.requestMacaroon(root_macaroon_raw)
152-
153 @classmethod
154 def fromOpenIDRequest(cls, request):
155 """Create a discharge macaroon request that contains the fields that
156@@ -166,17 +162,7 @@
157
158 @returns: None; updates this object
159 """
160- self.root_macaroon_raw = args.get('root')
161- if self.root_macaroon_raw:
162- self.requestMacaroon(self.root_macaroon_raw)
163-
164- def requestMacaroon(self, root_macaroon_raw):
165- """Request a discharge macaroon.
166-
167- @param root_macaroon_raw: The serialised root macaroon to discharge.
168- @type root_macaroon_raw: str
169- """
170- self.root_macaroon = Macaroon.deserialize(root_macaroon_raw)
171+ self.caveat_id = args.get('caveat_id')
172
173 def getExtensionArgs(self):
174 """Get a dictionary of unqualified macaroon request parameters
175@@ -189,8 +175,8 @@
176 """
177 args = {}
178
179- if self.root_macaroon_raw:
180- args['root'] = self.root_macaroon_raw
181+ if self.caveat_id:
182+ args['caveat_id'] = self.caveat_id
183
184 return args
185
186
187=== modified file 'src/identityprovider/tests/test_auth.py'
188--- src/identityprovider/tests/test_auth.py 2016-05-05 20:58:23 +0000
189+++ src/identityprovider/tests/test_auth.py 2016-05-11 17:42:55 +0000
190@@ -968,14 +968,16 @@
191
192 def setUp(self):
193 super(BuildMacaroonFromRootDischargeTestCase, self).setUp()
194- self.root_macaroon, self.macaroon_random_key, _ = self.build_macaroon()
195+ self.root_macaroon, self.macaroon_random_key, _ = self.build_macaroon(
196+ version=0)
197
198 def test_root_macaroon_corrupt(self):
199 self.assertRaises(ValidationError, build_discharge_macaroon_from_root,
200 "fake account", "I'm a seriously corrupted macaroon")
201
202 def test_root_macaroon_not_for_sso(self):
203- macaroon, _, _ = self.build_macaroon(service_location="other service")
204+ macaroon, _, _ = self.build_macaroon(
205+ service_location="other service", version=0)
206 self.assertRaises(AuthenticationError,
207 build_discharge_macaroon_from_root,
208 "fake account", macaroon.serialize())
209@@ -1043,18 +1045,18 @@
210 "fake account", "I'm a seriously corrupted caveatid")
211
212 def test_caveat_id_not_for_sso(self):
213- macaroon, _, _ = self.build_macaroon(service_location="other service")
214- (sso_caveat,) = [c for c in macaroon.third_party_caveats()
215- if c.location == "other service"]
216+ macaroon, _, _ = self.build_macaroon(
217+ service_location="other service", version=1)
218+ sso_caveat = self.get_sso_caveat(
219+ macaroon, service_location="other service")
220
221 self.assertRaises(ValidationError, build_discharge_macaroon,
222 "fake account", sso_caveat)
223
224 def test_proper_discharging(self):
225 # build the input and call
226- root_macaroon, _, random_key = self.build_macaroon()
227- (sso_caveat,) = [c for c in root_macaroon.third_party_caveats()
228- if c.location == settings.MACAROON_SERVICE_LOCATION]
229+ root_macaroon, _, random_key = self.build_macaroon(version=1)
230+ sso_caveat = self.get_sso_caveat(root_macaroon)
231
232 real_account = self.factory.make_account()
233 before = now()
234@@ -1177,7 +1179,8 @@
235
236 def setUp(self):
237 super(MacaroonRefreshFromRootTestCase, self).setUp()
238- self.root_macaroon, self.macaroon_random_key, _ = self.build_macaroon()
239+ self.root_macaroon, self.macaroon_random_key, _ = self.build_macaroon(
240+ version=0)
241
242 # discharge the test macaroon
243 self.account = self.factory.make_account()
244@@ -1196,7 +1199,7 @@
245
246 def test_macaroons_dont_verify_ok(self):
247 # just get *another* discharge so it's not for the same root macaroon
248- other_root, _, _ = self.build_macaroon()
249+ other_root, _, _ = self.build_macaroon(version=0)
250 other_discharge = build_discharge_macaroon_from_root(
251 self.account, other_root.serialize())
252 self.assertRaises(AuthenticationError, refresh_macaroons,
253@@ -1297,7 +1300,7 @@
254
255 def setUp(self):
256 super(MacaroonRefreshTestCase, self).setUp()
257- root_macaroon, _, self.random_key = self.build_macaroon()
258+ root_macaroon, _, self.random_key = self.build_macaroon(version=1)
259 (caveat,) = [c for c in root_macaroon.third_party_caveats()
260 if c.location == settings.MACAROON_SERVICE_LOCATION]
261
262
263=== modified file 'src/identityprovider/tests/test_forms.py'
264--- src/identityprovider/tests/test_forms.py 2016-05-05 11:39:43 +0000
265+++ src/identityprovider/tests/test_forms.py 2016-05-11 17:42:55 +0000
266@@ -45,7 +45,6 @@
267 UserAttribsRequestForm,
268 tos_error,
269 )
270-from identityprovider.macaroon import MacaroonRequest
271 from identityprovider.models import Account, AccountPassword, OpenIDRPConfig
272 from identityprovider.models.const import AccountStatus, EmailStatus
273 from identityprovider.tests import DEFAULT_USER_PASSWORD
274@@ -1131,71 +1130,71 @@
275 def setUp(self):
276 super(MacaroonRequestFormTestCase, self).setUp()
277 self.account = self.factory.make_account()
278+ self.trust_root = 'http://localhost/'
279 self.rpconfig = OpenIDRPConfig.objects.create(
280- trust_root='http://localhost/', description="Some description")
281+ trust_root=self.trust_root, description="Some description")
282
283 def test_field_for_trusted_site(self):
284 """The server always returns discharge macaroons to trusted sites,
285 regardless of the state of the checkbox in the UI.
286 """
287- root_macaroon, _, _ = self.build_macaroon()
288- macaroon_request = MacaroonRequest(root_macaroon.serialize())
289+ macaroon_request = self.build_macaroon_request()
290 form = MacaroonRequestForm(
291 request=self._get_request_with_post_args(),
292- macaroon_request=macaroon_request, rpconfig=self.rpconfig)
293+ macaroon_request=macaroon_request, rpconfig=self.rpconfig,
294+ trust_root=self.trust_root)
295 self.assertIn('macaroon', form.data_approved_for_request)
296
297 def test_checked_field_for_untrusted_site(self):
298 """The server returns discharge macaroons to untrusted sites when
299 the user checks the checkbox in the UI.
300 """
301- root_macaroon, _, _ = self.build_macaroon()
302- macaroon_request = MacaroonRequest(root_macaroon.serialize())
303+ macaroon_request = self.build_macaroon_request()
304 form = MacaroonRequestForm(
305 request=self._get_request_with_post_args(macaroon='macaroon'),
306- macaroon_request=macaroon_request, rpconfig=None)
307+ macaroon_request=macaroon_request, rpconfig=None,
308+ trust_root=self.trust_root)
309 self.assertIn('macaroon', form.data_approved_for_request)
310
311 def test_unchecked_field_for_untrusted_site(self):
312 """The server does not return discharge macaroons to untrusted sites
313 when the user does not check the checkbox in the UI.
314 """
315- root_macaroon, _, _ = self.build_macaroon()
316- macaroon_request = MacaroonRequest(root_macaroon.serialize())
317+ macaroon_request = self.build_macaroon_request()
318 form = MacaroonRequestForm(
319 request=self._get_request_with_post_args(),
320- macaroon_request=macaroon_request, rpconfig=None)
321+ macaroon_request=macaroon_request, rpconfig=None,
322+ trust_root=self.trust_root)
323 self.assertNotIn('macaroon', form.data_approved_for_request)
324
325 def test_checkbox_status_for_trusted_site(self):
326 """Checkboxes are always checked if the site is trusted."""
327- root_macaroon, _, _ = self.build_macaroon()
328- macaroon_request = MacaroonRequest(root_macaroon.serialize())
329+ macaroon_request = self.build_macaroon_request()
330 form = MacaroonRequestForm(
331 request=self._get_request_with_post_args(),
332- macaroon_request=macaroon_request, rpconfig=self.rpconfig)
333+ macaroon_request=macaroon_request, rpconfig=self.rpconfig,
334+ trust_root=self.trust_root)
335 self.assertTrue(form.check_test('macaroon')(True))
336
337 def test_checkbox_status_for_untrusted_site(self):
338 """Checkboxes are checked by default if the site is untrusted."""
339- root_macaroon, _, _ = self.build_macaroon()
340- macaroon_request = MacaroonRequest(root_macaroon.serialize())
341+ macaroon_request = self.build_macaroon_request()
342 form = MacaroonRequestForm(
343 request=self._get_request_with_post_args(),
344- macaroon_request=macaroon_request, rpconfig=None)
345+ macaroon_request=macaroon_request, rpconfig=None,
346+ trust_root=self.trust_root)
347 self.assertTrue(form.check_test('macaroon')(True))
348
349 def test_checkbox_status_for_untrusted_site_with_approved_data(self):
350 """Checkboxes respect user preferences on untrusted sites where
351 available.
352 """
353- root_macaroon, _, _ = self.build_macaroon()
354- macaroon_request = MacaroonRequest(root_macaroon.serialize())
355+ macaroon_request = self.build_macaroon_request()
356 approved_data = {
357 'requested': ['macaroon'],
358 'approved': []}
359 form = MacaroonRequestForm(
360 request=self._get_request_with_post_args(),
361 macaroon_request=macaroon_request, rpconfig=None,
362- approved_data=approved_data)
363+ trust_root=self.trust_root, approved_data=approved_data)
364 self.assertFalse(form.check_test('macaroon')(True))
365
366=== modified file 'src/identityprovider/tests/test_macaroon.py'
367--- src/identityprovider/tests/test_macaroon.py 2016-05-05 11:39:43 +0000
368+++ src/identityprovider/tests/test_macaroon.py 2016-05-11 17:42:55 +0000
369@@ -45,19 +45,17 @@
370 def setUp(self):
371 super(MacaroonRequestTestCase, self).setUp()
372
373- self.root_macaroon, self.macaroon_random_key, _ = self.build_macaroon()
374- self.req = MacaroonRequest(self.root_macaroon.serialize())
375-
376- def assertMacaroonsEqual(self, expected, observed):
377- self.assertEqual(expected.serialize(), observed.serialize())
378+ root_macaroon, _, _ = self.build_macaroon(version=1)
379+ sso_caveat = self.get_sso_caveat(root_macaroon)
380+ self.caveat_id = sso_caveat.caveat_id
381+ self.req = MacaroonRequest(self.caveat_id)
382
383 def test_init(self):
384 req = MacaroonRequest()
385- self.assertIsNone(req.root_macaroon_raw)
386- self.assertIsNone(req.root_macaroon)
387+ self.assertIsNone(req.caveat_id)
388 self.assertEqual(MACAROON_NS, req.ns_uri)
389
390- self.assertMacaroonsEqual(self.root_macaroon, self.req.root_macaroon)
391+ self.assertEqual(self.caveat_id, self.req.caveat_id)
392
393 def test_fromOpenIDRequest(self):
394 params = {'openid.mode': 'checkid_setup',
395@@ -68,8 +66,7 @@
396 openid_server = server._get_openid_server(request)
397 orequest = openid_server.decodeRequest(params)
398 req = MacaroonRequest.fromOpenIDRequest(orequest)
399- self.assertIsNone(req.root_macaroon_raw)
400- self.assertIsNone(req.root_macaroon)
401+ self.assertIsNone(req.caveat_id)
402 self.assertEqual(MACAROON_NS, req.ns_uri)
403
404 def test_fromOpenIDRequest_with_root(self):
405@@ -77,22 +74,21 @@
406 'openid.trust_root': 'http://localhost/',
407 'openid.return_to': 'http://localhost/',
408 'openid.identity': IDENTIFIER_SELECT,
409- 'openid.macaroon.root': self.root_macaroon.serialize()}
410+ 'openid.macaroon.caveat_id': self.caveat_id}
411 request = self.factory.make_request(**params)
412 openid_server = server._get_openid_server(request)
413 orequest = openid_server.decodeRequest(params)
414 req = MacaroonRequest.fromOpenIDRequest(orequest)
415- self.assertMacaroonsEqual(self.root_macaroon, req.root_macaroon)
416+ self.assertEqual(self.caveat_id, req.caveat_id)
417 self.assertEqual(MACAROON_NS, req.ns_uri)
418
419 def test_parseExtensionArgs(self):
420 req = MacaroonRequest()
421- req.parseExtensionArgs(
422- {'root': self.root_macaroon.serialize()})
423- self.assertMacaroonsEqual(self.root_macaroon, req.root_macaroon)
424+ req.parseExtensionArgs({'caveat_id': self.caveat_id})
425+ self.assertEqual(self.caveat_id, req.caveat_id)
426
427 def test_getExtensionArgs(self):
428- expected = {'root': self.root_macaroon.serialize()}
429+ expected = {'caveat_id': self.caveat_id}
430 self.assertEqual(expected, self.req.getExtensionArgs())
431
432 def test_getExtensionArgs_no_root(self):
433
434=== modified file 'src/identityprovider/tests/test_views_server.py'
435--- src/identityprovider/tests/test_views_server.py 2016-05-10 00:44:27 +0000
436+++ src/identityprovider/tests/test_views_server.py 2016-05-11 17:42:55 +0000
437@@ -227,7 +227,7 @@
438 self._test_auto_auth(sreg=['fullname'])
439
440 def test_handle_user_response_auto_auth_discharge_macaroon(self):
441- root_macaroon, macaroon_random_key, _ = self.build_macaroon()
442+ root_macaroon, macaroon_random_key, _ = self.build_macaroon(version=1)
443 # Add padding to force a POST after signing. We don't know exactly
444 # how long the serialized discharge macaroon will be yet, but it
445 # will probably be at least 1024 bytes.
446@@ -290,9 +290,10 @@
447 for k, v in expected_values.iteritems()
448 if k not in sreg or v is None]
449 if root_macaroon and macaroon_key:
450+ sso_caveat = self.get_sso_caveat(root_macaroon)
451 self.params.update({
452 'openid.ns.macaroon': MACAROON_NS,
453- 'openid.macaroon.root': root_macaroon.serialize(),
454+ 'openid.macaroon.caveat_id': sso_caveat.caveat_id,
455 })
456 unexpected_fields.remove('openid.macaroon.discharge')
457 response = self.client.post(self.url, self.params)
458@@ -314,6 +315,8 @@
459 verifier.satisfy_general(lambda caveat: True)
460 discharge_macaroon = Macaroon.deserialize(
461 forms[0].fields['openid.macaroon.discharge'])
462+ discharge_macaroon = root_macaroon.prepare_for_request(
463+ discharge_macaroon)
464 self.assertRaises(
465 MacaroonUnmetCaveatException, verifier.verify,
466 root_macaroon, macaroon_key, [])
467@@ -1066,10 +1069,11 @@
468 # make sure rpconfig is set to auto authorize
469 OpenIDRPConfig.objects.create(
470 trust_root='http://localhost/', auto_authorize=True)
471- root_macaroon, macaroon_random_key, _ = self.build_macaroon()
472+ root_macaroon, macaroon_random_key, _ = self.build_macaroon(version=1)
473+ sso_caveat = self.get_sso_caveat(root_macaroon)
474 param_overrides = {
475 'openid.ns.macaroon': MACAROON_NS,
476- 'openid.macaroon.root': root_macaroon.serialize(),
477+ 'openid.macaroon.caveat_id': sso_caveat.caveat_id,
478 }
479 self._prepare_openid_token(param_overrides=param_overrides)
480 response = self.client.post(self.url)
481@@ -1086,14 +1090,17 @@
482 self.assertRaises(
483 MacaroonUnmetCaveatException, verifier.verify,
484 root_macaroon, macaroon_random_key, [])
485+ discharge_macaroon = root_macaroon.prepare_for_request(
486+ discharge_macaroon)
487 self.assertTrue(verifier.verify(
488 root_macaroon, macaroon_random_key, [discharge_macaroon]))
489
490 def test_state_of_checkboxes_and_data_formats_macaroon(self):
491- root_macaroon, _, _ = self.build_macaroon()
492+ root_macaroon, _, _ = self.build_macaroon(version=1)
493+ sso_caveat = self.get_sso_caveat(root_macaroon)
494 param_overrides = {
495 'openid.ns.macaroon': MACAROON_NS,
496- 'openid.macaroon.root': root_macaroon.serialize(),
497+ 'openid.macaroon.caveat_id': sso_caveat.caveat_id,
498 }
499 self._prepare_openid_token(param_overrides=param_overrides)
500 response = self.client.post(self.url)
501@@ -1101,7 +1108,7 @@
502 # This field is checked regardless of whether a site is trusted.
503 self._test_optional_trusted_field(
504 dom, field='macaroon',
505- value='Service authorization for The store ;)')
506+ value='Service authorization for http://localhost/')
507
508
509 class DecideUserUnverifiedTestCase(DecideBaseTestCase):
510@@ -1865,9 +1872,10 @@
511 if with_teams:
512 params['openid.lp.query_membership'] = 'ubuntu-team'
513 if with_macaroon:
514- root_macaroon, _, _ = self.build_macaroon()
515+ root_macaroon, _, _ = self.build_macaroon(version=1)
516+ sso_caveat = self.get_sso_caveat(root_macaroon)
517 params['openid.ns.macaroon'] = MACAROON_NS
518- params['openid.macaroon.root'] = root_macaroon.serialize()
519+ params['openid.macaroon.caveat_id'] = sso_caveat.caveat_id
520 provider_url = get_provider_url(request)
521 openid_server = server._get_openid_server(provider_url)
522 return openid_server.decodeRequest(params)
523
524=== modified file 'src/identityprovider/tests/utils.py'
525--- src/identityprovider/tests/utils.py 2016-05-10 16:12:20 +0000
526+++ src/identityprovider/tests/utils.py 2016-05-11 17:42:55 +0000
527@@ -33,6 +33,7 @@
528 # import signals to ensure all the handlers are connected and tests use the
529 # same logic as prod systems
530 from identityprovider import signed, signals # noqa
531+from identityprovider.macaroon import MacaroonRequest
532 from identityprovider.tests import DEFAULT_USER_PASSWORD
533 from identityprovider.tests.factory import SSOObjectFactory
534 from identityprovider.utils import generate_random_string
535@@ -181,7 +182,7 @@
536 self.addCleanup(p.disable)
537 return test_rsa_priv_key, test_rsa_pub_key
538
539- def build_macaroon(self, service_location=None):
540+ def build_macaroon(self, service_location=None, version=1):
541 if service_location is None:
542 service_location = settings.MACAROON_SERVICE_LOCATION
543 test_rsa_priv_key, test_rsa_pub_key = self.setup_key_pair()
544@@ -194,16 +195,37 @@
545 identifier='A test macaroon',
546 )
547 random_key = binascii.hexlify(os.urandom(32))
548- info = {
549- 'roothash': macaroon_random_key,
550- '3rdparty': random_key,
551- }
552- info_encrypted = base64.b64encode(
553- test_rsa_pub_key.encrypt(json.dumps(info), 32)[0])
554+ if version == 0:
555+ info = {
556+ 'roothash': macaroon_random_key,
557+ '3rdparty': random_key,
558+ }
559+ payload = base64.b64encode(
560+ test_rsa_pub_key.encrypt(json.dumps(info), 32)[0])
561+ else:
562+ payload = json.dumps({
563+ 'version': 1,
564+ 'secret': base64.b64encode(
565+ test_rsa_pub_key.encrypt(random_key, 32)[0]),
566+ })
567 root_macaroon.add_third_party_caveat(
568- service_location, random_key, info_encrypted)
569+ service_location, random_key, payload)
570 return root_macaroon, macaroon_random_key, random_key
571
572+ def get_sso_caveat(self, macaroon, service_location=None):
573+ """Extract the SSO caveat from a macaroon."""
574+ if service_location is None:
575+ service_location = settings.MACAROON_SERVICE_LOCATION
576+ (sso_caveat,) = [c for c in macaroon.third_party_caveats()
577+ if c.location == service_location]
578+ return sso_caveat
579+
580+ def build_macaroon_request(self):
581+ """Build a macaroon and a MacaroonRequest to discharge it."""
582+ root_macaroon, _, _ = self.build_macaroon(version=1)
583+ sso_caveat = self.get_sso_caveat(root_macaroon)
584+ return MacaroonRequest(sso_caveat.caveat_id)
585+
586 def use_fixture(self, fixture):
587 """Set up 'fixture' to be used with the current test.
588
589
590=== modified file 'src/identityprovider/views/server.py'
591--- src/identityprovider/views/server.py 2016-05-05 11:39:43 +0000
592+++ src/identityprovider/views/server.py 2016-05-11 17:42:55 +0000
593@@ -42,7 +42,7 @@
594 from openid.yadis.constants import YADIS_HEADER_NAME
595
596 from identityprovider import signed
597-from identityprovider.auth import build_discharge_macaroon_from_root
598+from identityprovider.auth import build_discharge_macaroon
599 from identityprovider.const import (
600 AX_DATA_FIELDS,
601 MACAROON_NS,
602@@ -321,7 +321,7 @@
603 teams_form = TeamsRequestForm(request, teams_request, rpconfig,
604 approved_data=approved_data.get('teams'))
605 macaroon_form = MacaroonRequestForm(
606- request, macaroon_request, rpconfig,
607+ request, macaroon_request, rpconfig, orequest.trust_root,
608 approved_data=approved_data.get('macaroon'))
609 context = {
610 'account': request.user,
611@@ -637,9 +637,10 @@
612 'approved': teams_form.teams_approved_by_user}
613
614 macaroon_args = orequest.message.getArgs(MACAROON_NS)
615- if macaroon_args.get('root'):
616+ if macaroon_args.get('caveat_id'):
617 macaroon_form = MacaroonRequestForm(
618- request, MacaroonRequest.fromOpenIDRequest(orequest), rpconfig)
619+ request, MacaroonRequest.fromOpenIDRequest(orequest), rpconfig,
620+ orequest.trust_root)
621 approved_data['macaroon'] = {
622 'requested': macaroon_form.data.keys(),
623 'approved': macaroon_form.data_approved_for_request.keys()}
624@@ -767,10 +768,11 @@
625 """Add discharge macaroon if requested and approved."""
626 macaroon_request = MacaroonRequest.fromOpenIDRequest(openid_request)
627 rpconfig = utils.get_rpconfig(openid_request.trust_root)
628- form = MacaroonRequestForm(request, macaroon_request, rpconfig)
629+ form = MacaroonRequestForm(
630+ request, macaroon_request, rpconfig, openid_request.trust_root)
631 if form.data_approved_for_request:
632- discharge_macaroon = build_discharge_macaroon_from_root(
633- request.user, macaroon_request.root_macaroon_raw)
634+ discharge_macaroon = build_discharge_macaroon(
635+ request.user, macaroon_request.caveat_id)
636 macaroon_response = MacaroonResponse.extractResponse(
637 macaroon_request, discharge_macaroon.serialize())
638 openid_response.addExtension(macaroon_response)
639
640=== modified file 'src/webui/views/consumer.py'
641--- src/webui/views/consumer.py 2016-04-11 08:57:57 +0000
642+++ src/webui/views/consumer.py 2016-05-11 17:42:55 +0000
643@@ -171,24 +171,13 @@
644 auth_request.addExtension(teams_request_from_string(req_teams))
645
646 if request.POST.get('macaroon', False):
647- macaroon_random_key = binascii.hexlify(os.urandom(32))
648- root_macaroon = Macaroon(
649- location='Test consumer',
650- key=macaroon_random_key,
651- identifier='A test macaroon',
652- )
653 random_key = binascii.hexlify(os.urandom(32))
654- info = {
655- 'roothash': macaroon_random_key,
656- '3rdparty': random_key,
657- }
658 pubkey = settings.CRYPTO_SSO_PRIVKEY.publickey()
659- info_encrypted = base64.b64encode(
660- pubkey.encrypt(json.dumps(info), 32)[0])
661- root_macaroon.add_third_party_caveat(
662- settings.MACAROON_SERVICE_LOCATION, random_key, info_encrypted)
663- macaroon_request = macaroon.MacaroonRequest(
664- root_macaroon.serialize())
665+ caveat_id = json.dumps({
666+ 'version': 1,
667+ 'secret': base64.b64encode(pubkey.encrypt(random_key, 32)[0]),
668+ })
669+ macaroon_request = macaroon.MacaroonRequest(caveat_id)
670 auth_request.addExtension(macaroon_request)
671
672 # Compute the trust root and return URL values to build the