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
=== modified file 'src/api/v20/tests/test_handlers.py'
--- src/api/v20/tests/test_handlers.py 2016-05-05 20:58:23 +0000
+++ src/api/v20/tests/test_handlers.py 2016-05-11 17:42:55 +0000
@@ -2169,7 +2169,11 @@
2169 self.login_failed_calls = []2169 self.login_failed_calls = []
2170 login_failed.connect(self.track_failed_logins, dispatch_uid=self.id())2170 login_failed.connect(self.track_failed_logins, dispatch_uid=self.id())
21712171
2172 self.root_macaroon, self.macaroon_random_key, _ = self.build_macaroon()2172 # XXX cjwatson 2016-05-11:
2173 # MacaroonRefreshHandlerTestCase.test_macaroon_refreshed needs to be
2174 # fixed before changing the version here.
2175 self.root_macaroon, self.macaroon_random_key, _ = self.build_macaroon(
2176 version=0)
2173 self.data = dict(2177 self.data = dict(
2174 email='foo@bar.com', password='foobar123',2178 email='foo@bar.com', password='foobar123',
2175 macaroon=self.root_macaroon.serialize())2179 macaroon=self.root_macaroon.serialize())
@@ -2344,7 +2348,8 @@
2344 check_login_failed=False)2348 check_login_failed=False)
23452349
2346 def test_root_macaroon_not_for_sso(self):2350 def test_root_macaroon_not_for_sso(self):
2347 macaroon, _, _ = self.build_macaroon(service_location="other service")2351 macaroon, _, _ = self.build_macaroon(
2352 service_location="other service", version=1)
2348 data = dict(email='foo@bar.com', password='foobar123',2353 data = dict(email='foo@bar.com', password='foobar123',
2349 macaroon=macaroon.serialize())2354 macaroon=macaroon.serialize())
2350 self.assert_failed_login('INVALID_CREDENTIALS', data,2355 self.assert_failed_login('INVALID_CREDENTIALS', data,
@@ -2360,7 +2365,8 @@
23602365
2361 def test_multiple_macaroons(self):2366 def test_multiple_macaroons(self):
2362 # build *several* macaroons and send them all at once2367 # build *several* macaroons and send them all at once
2363 roots, rkeys, _ = zip(*[self.build_macaroon() for _ in range(3)])2368 roots, rkeys, _ = zip(*[
2369 self.build_macaroon(version=1) for _ in range(3)])
2364 ids = range(3)2370 ids = range(3)
2365 roots = dict(zip(ids, roots))2371 roots = dict(zip(ids, roots))
2366 rkeys = dict(zip(ids, rkeys))2372 rkeys = dict(zip(ids, rkeys))
@@ -2384,7 +2390,7 @@
23842390
2385 def test_multiple_mixed(self):2391 def test_multiple_mixed(self):
2386 bad_macaroon, _, _ = self.build_macaroon(2392 bad_macaroon, _, _ = self.build_macaroon(
2387 service_location="other service")2393 service_location="other service", version=1)
2388 data = dict(email='foo@bar.com', password='foobar123', macaroons=[2394 data = dict(email='foo@bar.com', password='foobar123', macaroons=[
2389 ('good', self.root_macaroon.serialize()),2395 ('good', self.root_macaroon.serialize()),
2390 ('bad', bad_macaroon.serialize()),2396 ('bad', bad_macaroon.serialize()),
@@ -2518,7 +2524,8 @@
2518 check_login_failed=False)2524 check_login_failed=False)
25192525
2520 def test_macaroon_bad_authinfo(self):2526 def test_macaroon_bad_authinfo(self):
2521 macaroon, _, _ = self.build_macaroon(service_location="other service")2527 macaroon, _, _ = self.build_macaroon(
2528 service_location="other service", version=1)
2522 data = dict(root_macaroon=macaroon.serialize(),2529 data = dict(root_macaroon=macaroon.serialize(),
2523 discharge_macaroon=self.discharge_macaroon.serialize())2530 discharge_macaroon=self.discharge_macaroon.serialize())
2524 self.assert_failed_login('INVALID_CREDENTIALS', data,2531 self.assert_failed_login('INVALID_CREDENTIALS', data,
25252532
=== modified file 'src/identityprovider/forms.py'
--- src/identityprovider/forms.py 2016-05-03 12:21:13 +0000
+++ src/identityprovider/forms.py 2016-05-11 17:42:55 +0000
@@ -574,12 +574,13 @@
574class MacaroonRequestForm(Form):574class MacaroonRequestForm(Form):
575 """A form object for user control over requesting discharge macaroons."""575 """A form object for user control over requesting discharge macaroons."""
576576
577 def __init__(self, request, macaroon_request, rpconfig,577 def __init__(self, request, macaroon_request, rpconfig, trust_root,
578 approved_data=None):578 approved_data=None):
579 self.request = request579 self.request = request
580 self.macaroon_request = macaroon_request580 self.macaroon_request = macaroon_request
581 self.root_macaroon = self.macaroon_request.root_macaroon581 self.caveat_id = self.macaroon_request.caveat_id
582 self.rpconfig = rpconfig582 self.rpconfig = rpconfig
583 self.trust_root = trust_root
583 self.approved_data = approved_data584 self.approved_data = approved_data
584 self.data = self._get_data_for_user()585 self.data = self._get_data_for_user()
585 super(MacaroonRequestForm, self).__init__(self.data)586 super(MacaroonRequestForm, self).__init__(self.data)
@@ -590,14 +591,18 @@
590 def _get_data_for_user(self):591 def _get_data_for_user(self):
591 """Data to ask about in the form."""592 """Data to ask about in the form."""
592 data = {}593 data = {}
593 if self.root_macaroon:594 if self.caveat_id:
594 data['macaroon'] = self.root_macaroon595 data['macaroon'] = self.caveat_id
595 return data596 return data
596597
597 def _init_fields(self, data):598 def _init_fields(self, data):
598 """Initialise fields from the discharge macaroon request."""599 """Initialise fields from the discharge macaroon request."""
599 for location, root_macaroon in data.iteritems():600 for location, caveat_id in data.iteritems():
600 label = 'Service authorization for %s' % root_macaroon.location601 if self.rpconfig:
602 rp_displayname = self.rpconfig.displayname
603 else:
604 rp_displayname = self.trust_root
605 label = 'Service authorization for %s' % rp_displayname
601 self.fields[location] = fields.BooleanField(606 self.fields[location] = fields.BooleanField(
602 label=label, widget=forms.CheckboxInput(607 label=label, widget=forms.CheckboxInput(
603 check_test=self.check_test(location)))608 check_test=self.check_test(location)))
604609
=== modified file 'src/identityprovider/macaroon.py'
--- src/identityprovider/macaroon.py 2016-04-05 22:12:47 +0000
+++ src/identityprovider/macaroon.py 2016-05-11 17:42:55 +0000
@@ -19,8 +19,9 @@
1919
20 It must be set to: http://ns.login.ubuntu.com/2016/openid-macaroon20 It must be set to: http://ns.login.ubuntu.com/2016/openid-macaroon
2121
22 openid.macaroon.root22 openid.macaroon.caveat_id
23 The serialised root macaroon that the RP wants to discharge.23 The SSO third-party caveat ID from the root macaroon that the RP wants
24 to discharge.
2425
25As part of the positive assertion OpenID response, the following fields26As part of the positive assertion OpenID response, the following fields
26will be provided:27will be provided:
@@ -38,7 +39,6 @@
38 NamespaceAliasRegistrationError,39 NamespaceAliasRegistrationError,
39 registerNamespaceAlias,40 registerNamespaceAlias,
40)41)
41from pymacaroons import Macaroon
4242
43from identityprovider.const import MACAROON_NS43from identityprovider.const import MACAROON_NS
4444
@@ -101,26 +101,22 @@
101class MacaroonRequest(Extension):101class MacaroonRequest(Extension):
102 """An object to hold the state of a discharge macaroon request.102 """An object to hold the state of a discharge macaroon request.
103103
104 @ivar root_macaroon_raw: The serialised root macaroon to discharge.104 @ivar caveat_id: The SSO third-party caveat ID from the root macaroon
105 @type root_macaroon_raw: str105 that the RP wants to discharge.
106 @type caveat_id: str
106107
107 @group Consumer: requestField, requestMacaroon, getExtensionArgs,108 @group Consumer: requestField, getExtensionArgs, addToOpenIDRequest
108 addToOpenIDRequest
109 @group Server: fromOpenIDRequest, parseExtensionArgs109 @group Server: fromOpenIDRequest, parseExtensionArgs
110 """110 """
111111
112 ns_alias = 'macaroon'112 ns_alias = 'macaroon'
113113
114 def __init__(self, root_macaroon_raw=None, macaroon_ns_uri=MACAROON_NS):114 def __init__(self, caveat_id=None, macaroon_ns_uri=MACAROON_NS):
115 """Initialize an empty discharge macaroon request."""115 """Initialize an empty discharge macaroon request."""
116 Extension.__init__(self)116 Extension.__init__(self)
117 self.root_macaroon_raw = root_macaroon_raw117 self.caveat_id = caveat_id
118 self.root_macaroon = None
119 self.ns_uri = macaroon_ns_uri118 self.ns_uri = macaroon_ns_uri
120119
121 if root_macaroon_raw:
122 self.requestMacaroon(root_macaroon_raw)
123
124 @classmethod120 @classmethod
125 def fromOpenIDRequest(cls, request):121 def fromOpenIDRequest(cls, request):
126 """Create a discharge macaroon request that contains the fields that122 """Create a discharge macaroon request that contains the fields that
@@ -166,17 +162,7 @@
166162
167 @returns: None; updates this object163 @returns: None; updates this object
168 """164 """
169 self.root_macaroon_raw = args.get('root')165 self.caveat_id = args.get('caveat_id')
170 if self.root_macaroon_raw:
171 self.requestMacaroon(self.root_macaroon_raw)
172
173 def requestMacaroon(self, root_macaroon_raw):
174 """Request a discharge macaroon.
175
176 @param root_macaroon_raw: The serialised root macaroon to discharge.
177 @type root_macaroon_raw: str
178 """
179 self.root_macaroon = Macaroon.deserialize(root_macaroon_raw)
180166
181 def getExtensionArgs(self):167 def getExtensionArgs(self):
182 """Get a dictionary of unqualified macaroon request parameters168 """Get a dictionary of unqualified macaroon request parameters
@@ -189,8 +175,8 @@
189 """175 """
190 args = {}176 args = {}
191177
192 if self.root_macaroon_raw:178 if self.caveat_id:
193 args['root'] = self.root_macaroon_raw179 args['caveat_id'] = self.caveat_id
194180
195 return args181 return args
196182
197183
=== modified file 'src/identityprovider/tests/test_auth.py'
--- src/identityprovider/tests/test_auth.py 2016-05-05 20:58:23 +0000
+++ src/identityprovider/tests/test_auth.py 2016-05-11 17:42:55 +0000
@@ -968,14 +968,16 @@
968968
969 def setUp(self):969 def setUp(self):
970 super(BuildMacaroonFromRootDischargeTestCase, self).setUp()970 super(BuildMacaroonFromRootDischargeTestCase, self).setUp()
971 self.root_macaroon, self.macaroon_random_key, _ = self.build_macaroon()971 self.root_macaroon, self.macaroon_random_key, _ = self.build_macaroon(
972 version=0)
972973
973 def test_root_macaroon_corrupt(self):974 def test_root_macaroon_corrupt(self):
974 self.assertRaises(ValidationError, build_discharge_macaroon_from_root,975 self.assertRaises(ValidationError, build_discharge_macaroon_from_root,
975 "fake account", "I'm a seriously corrupted macaroon")976 "fake account", "I'm a seriously corrupted macaroon")
976977
977 def test_root_macaroon_not_for_sso(self):978 def test_root_macaroon_not_for_sso(self):
978 macaroon, _, _ = self.build_macaroon(service_location="other service")979 macaroon, _, _ = self.build_macaroon(
980 service_location="other service", version=0)
979 self.assertRaises(AuthenticationError,981 self.assertRaises(AuthenticationError,
980 build_discharge_macaroon_from_root,982 build_discharge_macaroon_from_root,
981 "fake account", macaroon.serialize())983 "fake account", macaroon.serialize())
@@ -1043,18 +1045,18 @@
1043 "fake account", "I'm a seriously corrupted caveatid")1045 "fake account", "I'm a seriously corrupted caveatid")
10441046
1045 def test_caveat_id_not_for_sso(self):1047 def test_caveat_id_not_for_sso(self):
1046 macaroon, _, _ = self.build_macaroon(service_location="other service")1048 macaroon, _, _ = self.build_macaroon(
1047 (sso_caveat,) = [c for c in macaroon.third_party_caveats()1049 service_location="other service", version=1)
1048 if c.location == "other service"]1050 sso_caveat = self.get_sso_caveat(
1051 macaroon, service_location="other service")
10491052
1050 self.assertRaises(ValidationError, build_discharge_macaroon,1053 self.assertRaises(ValidationError, build_discharge_macaroon,
1051 "fake account", sso_caveat)1054 "fake account", sso_caveat)
10521055
1053 def test_proper_discharging(self):1056 def test_proper_discharging(self):
1054 # build the input and call1057 # build the input and call
1055 root_macaroon, _, random_key = self.build_macaroon()1058 root_macaroon, _, random_key = self.build_macaroon(version=1)
1056 (sso_caveat,) = [c for c in root_macaroon.third_party_caveats()1059 sso_caveat = self.get_sso_caveat(root_macaroon)
1057 if c.location == settings.MACAROON_SERVICE_LOCATION]
10581060
1059 real_account = self.factory.make_account()1061 real_account = self.factory.make_account()
1060 before = now()1062 before = now()
@@ -1177,7 +1179,8 @@
11771179
1178 def setUp(self):1180 def setUp(self):
1179 super(MacaroonRefreshFromRootTestCase, self).setUp()1181 super(MacaroonRefreshFromRootTestCase, self).setUp()
1180 self.root_macaroon, self.macaroon_random_key, _ = self.build_macaroon()1182 self.root_macaroon, self.macaroon_random_key, _ = self.build_macaroon(
1183 version=0)
11811184
1182 # discharge the test macaroon1185 # discharge the test macaroon
1183 self.account = self.factory.make_account()1186 self.account = self.factory.make_account()
@@ -1196,7 +1199,7 @@
11961199
1197 def test_macaroons_dont_verify_ok(self):1200 def test_macaroons_dont_verify_ok(self):
1198 # just get *another* discharge so it's not for the same root macaroon1201 # just get *another* discharge so it's not for the same root macaroon
1199 other_root, _, _ = self.build_macaroon()1202 other_root, _, _ = self.build_macaroon(version=0)
1200 other_discharge = build_discharge_macaroon_from_root(1203 other_discharge = build_discharge_macaroon_from_root(
1201 self.account, other_root.serialize())1204 self.account, other_root.serialize())
1202 self.assertRaises(AuthenticationError, refresh_macaroons,1205 self.assertRaises(AuthenticationError, refresh_macaroons,
@@ -1297,7 +1300,7 @@
12971300
1298 def setUp(self):1301 def setUp(self):
1299 super(MacaroonRefreshTestCase, self).setUp()1302 super(MacaroonRefreshTestCase, self).setUp()
1300 root_macaroon, _, self.random_key = self.build_macaroon()1303 root_macaroon, _, self.random_key = self.build_macaroon(version=1)
1301 (caveat,) = [c for c in root_macaroon.third_party_caveats()1304 (caveat,) = [c for c in root_macaroon.third_party_caveats()
1302 if c.location == settings.MACAROON_SERVICE_LOCATION]1305 if c.location == settings.MACAROON_SERVICE_LOCATION]
13031306
13041307
=== modified file 'src/identityprovider/tests/test_forms.py'
--- src/identityprovider/tests/test_forms.py 2016-05-05 11:39:43 +0000
+++ src/identityprovider/tests/test_forms.py 2016-05-11 17:42:55 +0000
@@ -45,7 +45,6 @@
45 UserAttribsRequestForm,45 UserAttribsRequestForm,
46 tos_error,46 tos_error,
47)47)
48from identityprovider.macaroon import MacaroonRequest
49from identityprovider.models import Account, AccountPassword, OpenIDRPConfig48from identityprovider.models import Account, AccountPassword, OpenIDRPConfig
50from identityprovider.models.const import AccountStatus, EmailStatus49from identityprovider.models.const import AccountStatus, EmailStatus
51from identityprovider.tests import DEFAULT_USER_PASSWORD50from identityprovider.tests import DEFAULT_USER_PASSWORD
@@ -1131,71 +1130,71 @@
1131 def setUp(self):1130 def setUp(self):
1132 super(MacaroonRequestFormTestCase, self).setUp()1131 super(MacaroonRequestFormTestCase, self).setUp()
1133 self.account = self.factory.make_account()1132 self.account = self.factory.make_account()
1133 self.trust_root = 'http://localhost/'
1134 self.rpconfig = OpenIDRPConfig.objects.create(1134 self.rpconfig = OpenIDRPConfig.objects.create(
1135 trust_root='http://localhost/', description="Some description")1135 trust_root=self.trust_root, description="Some description")
11361136
1137 def test_field_for_trusted_site(self):1137 def test_field_for_trusted_site(self):
1138 """The server always returns discharge macaroons to trusted sites,1138 """The server always returns discharge macaroons to trusted sites,
1139 regardless of the state of the checkbox in the UI.1139 regardless of the state of the checkbox in the UI.
1140 """1140 """
1141 root_macaroon, _, _ = self.build_macaroon()1141 macaroon_request = self.build_macaroon_request()
1142 macaroon_request = MacaroonRequest(root_macaroon.serialize())
1143 form = MacaroonRequestForm(1142 form = MacaroonRequestForm(
1144 request=self._get_request_with_post_args(),1143 request=self._get_request_with_post_args(),
1145 macaroon_request=macaroon_request, rpconfig=self.rpconfig)1144 macaroon_request=macaroon_request, rpconfig=self.rpconfig,
1145 trust_root=self.trust_root)
1146 self.assertIn('macaroon', form.data_approved_for_request)1146 self.assertIn('macaroon', form.data_approved_for_request)
11471147
1148 def test_checked_field_for_untrusted_site(self):1148 def test_checked_field_for_untrusted_site(self):
1149 """The server returns discharge macaroons to untrusted sites when1149 """The server returns discharge macaroons to untrusted sites when
1150 the user checks the checkbox in the UI.1150 the user checks the checkbox in the UI.
1151 """1151 """
1152 root_macaroon, _, _ = self.build_macaroon()1152 macaroon_request = self.build_macaroon_request()
1153 macaroon_request = MacaroonRequest(root_macaroon.serialize())
1154 form = MacaroonRequestForm(1153 form = MacaroonRequestForm(
1155 request=self._get_request_with_post_args(macaroon='macaroon'),1154 request=self._get_request_with_post_args(macaroon='macaroon'),
1156 macaroon_request=macaroon_request, rpconfig=None)1155 macaroon_request=macaroon_request, rpconfig=None,
1156 trust_root=self.trust_root)
1157 self.assertIn('macaroon', form.data_approved_for_request)1157 self.assertIn('macaroon', form.data_approved_for_request)
11581158
1159 def test_unchecked_field_for_untrusted_site(self):1159 def test_unchecked_field_for_untrusted_site(self):
1160 """The server does not return discharge macaroons to untrusted sites1160 """The server does not return discharge macaroons to untrusted sites
1161 when the user does not check the checkbox in the UI.1161 when the user does not check the checkbox in the UI.
1162 """1162 """
1163 root_macaroon, _, _ = self.build_macaroon()1163 macaroon_request = self.build_macaroon_request()
1164 macaroon_request = MacaroonRequest(root_macaroon.serialize())
1165 form = MacaroonRequestForm(1164 form = MacaroonRequestForm(
1166 request=self._get_request_with_post_args(),1165 request=self._get_request_with_post_args(),
1167 macaroon_request=macaroon_request, rpconfig=None)1166 macaroon_request=macaroon_request, rpconfig=None,
1167 trust_root=self.trust_root)
1168 self.assertNotIn('macaroon', form.data_approved_for_request)1168 self.assertNotIn('macaroon', form.data_approved_for_request)
11691169
1170 def test_checkbox_status_for_trusted_site(self):1170 def test_checkbox_status_for_trusted_site(self):
1171 """Checkboxes are always checked if the site is trusted."""1171 """Checkboxes are always checked if the site is trusted."""
1172 root_macaroon, _, _ = self.build_macaroon()1172 macaroon_request = self.build_macaroon_request()
1173 macaroon_request = MacaroonRequest(root_macaroon.serialize())
1174 form = MacaroonRequestForm(1173 form = MacaroonRequestForm(
1175 request=self._get_request_with_post_args(),1174 request=self._get_request_with_post_args(),
1176 macaroon_request=macaroon_request, rpconfig=self.rpconfig)1175 macaroon_request=macaroon_request, rpconfig=self.rpconfig,
1176 trust_root=self.trust_root)
1177 self.assertTrue(form.check_test('macaroon')(True))1177 self.assertTrue(form.check_test('macaroon')(True))
11781178
1179 def test_checkbox_status_for_untrusted_site(self):1179 def test_checkbox_status_for_untrusted_site(self):
1180 """Checkboxes are checked by default if the site is untrusted."""1180 """Checkboxes are checked by default if the site is untrusted."""
1181 root_macaroon, _, _ = self.build_macaroon()1181 macaroon_request = self.build_macaroon_request()
1182 macaroon_request = MacaroonRequest(root_macaroon.serialize())
1183 form = MacaroonRequestForm(1182 form = MacaroonRequestForm(
1184 request=self._get_request_with_post_args(),1183 request=self._get_request_with_post_args(),
1185 macaroon_request=macaroon_request, rpconfig=None)1184 macaroon_request=macaroon_request, rpconfig=None,
1185 trust_root=self.trust_root)
1186 self.assertTrue(form.check_test('macaroon')(True))1186 self.assertTrue(form.check_test('macaroon')(True))
11871187
1188 def test_checkbox_status_for_untrusted_site_with_approved_data(self):1188 def test_checkbox_status_for_untrusted_site_with_approved_data(self):
1189 """Checkboxes respect user preferences on untrusted sites where1189 """Checkboxes respect user preferences on untrusted sites where
1190 available.1190 available.
1191 """1191 """
1192 root_macaroon, _, _ = self.build_macaroon()1192 macaroon_request = self.build_macaroon_request()
1193 macaroon_request = MacaroonRequest(root_macaroon.serialize())
1194 approved_data = {1193 approved_data = {
1195 'requested': ['macaroon'],1194 'requested': ['macaroon'],
1196 'approved': []}1195 'approved': []}
1197 form = MacaroonRequestForm(1196 form = MacaroonRequestForm(
1198 request=self._get_request_with_post_args(),1197 request=self._get_request_with_post_args(),
1199 macaroon_request=macaroon_request, rpconfig=None,1198 macaroon_request=macaroon_request, rpconfig=None,
1200 approved_data=approved_data)1199 trust_root=self.trust_root, approved_data=approved_data)
1201 self.assertFalse(form.check_test('macaroon')(True))1200 self.assertFalse(form.check_test('macaroon')(True))
12021201
=== modified file 'src/identityprovider/tests/test_macaroon.py'
--- src/identityprovider/tests/test_macaroon.py 2016-05-05 11:39:43 +0000
+++ src/identityprovider/tests/test_macaroon.py 2016-05-11 17:42:55 +0000
@@ -45,19 +45,17 @@
45 def setUp(self):45 def setUp(self):
46 super(MacaroonRequestTestCase, self).setUp()46 super(MacaroonRequestTestCase, self).setUp()
4747
48 self.root_macaroon, self.macaroon_random_key, _ = self.build_macaroon()48 root_macaroon, _, _ = self.build_macaroon(version=1)
49 self.req = MacaroonRequest(self.root_macaroon.serialize())49 sso_caveat = self.get_sso_caveat(root_macaroon)
5050 self.caveat_id = sso_caveat.caveat_id
51 def assertMacaroonsEqual(self, expected, observed):51 self.req = MacaroonRequest(self.caveat_id)
52 self.assertEqual(expected.serialize(), observed.serialize())
5352
54 def test_init(self):53 def test_init(self):
55 req = MacaroonRequest()54 req = MacaroonRequest()
56 self.assertIsNone(req.root_macaroon_raw)55 self.assertIsNone(req.caveat_id)
57 self.assertIsNone(req.root_macaroon)
58 self.assertEqual(MACAROON_NS, req.ns_uri)56 self.assertEqual(MACAROON_NS, req.ns_uri)
5957
60 self.assertMacaroonsEqual(self.root_macaroon, self.req.root_macaroon)58 self.assertEqual(self.caveat_id, self.req.caveat_id)
6159
62 def test_fromOpenIDRequest(self):60 def test_fromOpenIDRequest(self):
63 params = {'openid.mode': 'checkid_setup',61 params = {'openid.mode': 'checkid_setup',
@@ -68,8 +66,7 @@
68 openid_server = server._get_openid_server(request)66 openid_server = server._get_openid_server(request)
69 orequest = openid_server.decodeRequest(params)67 orequest = openid_server.decodeRequest(params)
70 req = MacaroonRequest.fromOpenIDRequest(orequest)68 req = MacaroonRequest.fromOpenIDRequest(orequest)
71 self.assertIsNone(req.root_macaroon_raw)69 self.assertIsNone(req.caveat_id)
72 self.assertIsNone(req.root_macaroon)
73 self.assertEqual(MACAROON_NS, req.ns_uri)70 self.assertEqual(MACAROON_NS, req.ns_uri)
7471
75 def test_fromOpenIDRequest_with_root(self):72 def test_fromOpenIDRequest_with_root(self):
@@ -77,22 +74,21 @@
77 'openid.trust_root': 'http://localhost/',74 'openid.trust_root': 'http://localhost/',
78 'openid.return_to': 'http://localhost/',75 'openid.return_to': 'http://localhost/',
79 'openid.identity': IDENTIFIER_SELECT,76 'openid.identity': IDENTIFIER_SELECT,
80 'openid.macaroon.root': self.root_macaroon.serialize()}77 'openid.macaroon.caveat_id': self.caveat_id}
81 request = self.factory.make_request(**params)78 request = self.factory.make_request(**params)
82 openid_server = server._get_openid_server(request)79 openid_server = server._get_openid_server(request)
83 orequest = openid_server.decodeRequest(params)80 orequest = openid_server.decodeRequest(params)
84 req = MacaroonRequest.fromOpenIDRequest(orequest)81 req = MacaroonRequest.fromOpenIDRequest(orequest)
85 self.assertMacaroonsEqual(self.root_macaroon, req.root_macaroon)82 self.assertEqual(self.caveat_id, req.caveat_id)
86 self.assertEqual(MACAROON_NS, req.ns_uri)83 self.assertEqual(MACAROON_NS, req.ns_uri)
8784
88 def test_parseExtensionArgs(self):85 def test_parseExtensionArgs(self):
89 req = MacaroonRequest()86 req = MacaroonRequest()
90 req.parseExtensionArgs(87 req.parseExtensionArgs({'caveat_id': self.caveat_id})
91 {'root': self.root_macaroon.serialize()})88 self.assertEqual(self.caveat_id, req.caveat_id)
92 self.assertMacaroonsEqual(self.root_macaroon, req.root_macaroon)
9389
94 def test_getExtensionArgs(self):90 def test_getExtensionArgs(self):
95 expected = {'root': self.root_macaroon.serialize()}91 expected = {'caveat_id': self.caveat_id}
96 self.assertEqual(expected, self.req.getExtensionArgs())92 self.assertEqual(expected, self.req.getExtensionArgs())
9793
98 def test_getExtensionArgs_no_root(self):94 def test_getExtensionArgs_no_root(self):
9995
=== modified file 'src/identityprovider/tests/test_views_server.py'
--- src/identityprovider/tests/test_views_server.py 2016-05-10 00:44:27 +0000
+++ src/identityprovider/tests/test_views_server.py 2016-05-11 17:42:55 +0000
@@ -227,7 +227,7 @@
227 self._test_auto_auth(sreg=['fullname'])227 self._test_auto_auth(sreg=['fullname'])
228228
229 def test_handle_user_response_auto_auth_discharge_macaroon(self):229 def test_handle_user_response_auto_auth_discharge_macaroon(self):
230 root_macaroon, macaroon_random_key, _ = self.build_macaroon()230 root_macaroon, macaroon_random_key, _ = self.build_macaroon(version=1)
231 # Add padding to force a POST after signing. We don't know exactly231 # Add padding to force a POST after signing. We don't know exactly
232 # how long the serialized discharge macaroon will be yet, but it232 # how long the serialized discharge macaroon will be yet, but it
233 # will probably be at least 1024 bytes.233 # will probably be at least 1024 bytes.
@@ -290,9 +290,10 @@
290 for k, v in expected_values.iteritems()290 for k, v in expected_values.iteritems()
291 if k not in sreg or v is None]291 if k not in sreg or v is None]
292 if root_macaroon and macaroon_key:292 if root_macaroon and macaroon_key:
293 sso_caveat = self.get_sso_caveat(root_macaroon)
293 self.params.update({294 self.params.update({
294 'openid.ns.macaroon': MACAROON_NS,295 'openid.ns.macaroon': MACAROON_NS,
295 'openid.macaroon.root': root_macaroon.serialize(),296 'openid.macaroon.caveat_id': sso_caveat.caveat_id,
296 })297 })
297 unexpected_fields.remove('openid.macaroon.discharge')298 unexpected_fields.remove('openid.macaroon.discharge')
298 response = self.client.post(self.url, self.params)299 response = self.client.post(self.url, self.params)
@@ -314,6 +315,8 @@
314 verifier.satisfy_general(lambda caveat: True)315 verifier.satisfy_general(lambda caveat: True)
315 discharge_macaroon = Macaroon.deserialize(316 discharge_macaroon = Macaroon.deserialize(
316 forms[0].fields['openid.macaroon.discharge'])317 forms[0].fields['openid.macaroon.discharge'])
318 discharge_macaroon = root_macaroon.prepare_for_request(
319 discharge_macaroon)
317 self.assertRaises(320 self.assertRaises(
318 MacaroonUnmetCaveatException, verifier.verify,321 MacaroonUnmetCaveatException, verifier.verify,
319 root_macaroon, macaroon_key, [])322 root_macaroon, macaroon_key, [])
@@ -1066,10 +1069,11 @@
1066 # make sure rpconfig is set to auto authorize1069 # make sure rpconfig is set to auto authorize
1067 OpenIDRPConfig.objects.create(1070 OpenIDRPConfig.objects.create(
1068 trust_root='http://localhost/', auto_authorize=True)1071 trust_root='http://localhost/', auto_authorize=True)
1069 root_macaroon, macaroon_random_key, _ = self.build_macaroon()1072 root_macaroon, macaroon_random_key, _ = self.build_macaroon(version=1)
1073 sso_caveat = self.get_sso_caveat(root_macaroon)
1070 param_overrides = {1074 param_overrides = {
1071 'openid.ns.macaroon': MACAROON_NS,1075 'openid.ns.macaroon': MACAROON_NS,
1072 'openid.macaroon.root': root_macaroon.serialize(),1076 'openid.macaroon.caveat_id': sso_caveat.caveat_id,
1073 }1077 }
1074 self._prepare_openid_token(param_overrides=param_overrides)1078 self._prepare_openid_token(param_overrides=param_overrides)
1075 response = self.client.post(self.url)1079 response = self.client.post(self.url)
@@ -1086,14 +1090,17 @@
1086 self.assertRaises(1090 self.assertRaises(
1087 MacaroonUnmetCaveatException, verifier.verify,1091 MacaroonUnmetCaveatException, verifier.verify,
1088 root_macaroon, macaroon_random_key, [])1092 root_macaroon, macaroon_random_key, [])
1093 discharge_macaroon = root_macaroon.prepare_for_request(
1094 discharge_macaroon)
1089 self.assertTrue(verifier.verify(1095 self.assertTrue(verifier.verify(
1090 root_macaroon, macaroon_random_key, [discharge_macaroon]))1096 root_macaroon, macaroon_random_key, [discharge_macaroon]))
10911097
1092 def test_state_of_checkboxes_and_data_formats_macaroon(self):1098 def test_state_of_checkboxes_and_data_formats_macaroon(self):
1093 root_macaroon, _, _ = self.build_macaroon()1099 root_macaroon, _, _ = self.build_macaroon(version=1)
1100 sso_caveat = self.get_sso_caveat(root_macaroon)
1094 param_overrides = {1101 param_overrides = {
1095 'openid.ns.macaroon': MACAROON_NS,1102 'openid.ns.macaroon': MACAROON_NS,
1096 'openid.macaroon.root': root_macaroon.serialize(),1103 'openid.macaroon.caveat_id': sso_caveat.caveat_id,
1097 }1104 }
1098 self._prepare_openid_token(param_overrides=param_overrides)1105 self._prepare_openid_token(param_overrides=param_overrides)
1099 response = self.client.post(self.url)1106 response = self.client.post(self.url)
@@ -1101,7 +1108,7 @@
1101 # This field is checked regardless of whether a site is trusted.1108 # This field is checked regardless of whether a site is trusted.
1102 self._test_optional_trusted_field(1109 self._test_optional_trusted_field(
1103 dom, field='macaroon',1110 dom, field='macaroon',
1104 value='Service authorization for The store ;)')1111 value='Service authorization for http://localhost/')
11051112
11061113
1107class DecideUserUnverifiedTestCase(DecideBaseTestCase):1114class DecideUserUnverifiedTestCase(DecideBaseTestCase):
@@ -1865,9 +1872,10 @@
1865 if with_teams:1872 if with_teams:
1866 params['openid.lp.query_membership'] = 'ubuntu-team'1873 params['openid.lp.query_membership'] = 'ubuntu-team'
1867 if with_macaroon:1874 if with_macaroon:
1868 root_macaroon, _, _ = self.build_macaroon()1875 root_macaroon, _, _ = self.build_macaroon(version=1)
1876 sso_caveat = self.get_sso_caveat(root_macaroon)
1869 params['openid.ns.macaroon'] = MACAROON_NS1877 params['openid.ns.macaroon'] = MACAROON_NS
1870 params['openid.macaroon.root'] = root_macaroon.serialize()1878 params['openid.macaroon.caveat_id'] = sso_caveat.caveat_id
1871 provider_url = get_provider_url(request)1879 provider_url = get_provider_url(request)
1872 openid_server = server._get_openid_server(provider_url)1880 openid_server = server._get_openid_server(provider_url)
1873 return openid_server.decodeRequest(params)1881 return openid_server.decodeRequest(params)
18741882
=== modified file 'src/identityprovider/tests/utils.py'
--- src/identityprovider/tests/utils.py 2016-05-10 16:12:20 +0000
+++ src/identityprovider/tests/utils.py 2016-05-11 17:42:55 +0000
@@ -33,6 +33,7 @@
33# import signals to ensure all the handlers are connected and tests use the33# import signals to ensure all the handlers are connected and tests use the
34# same logic as prod systems34# same logic as prod systems
35from identityprovider import signed, signals # noqa35from identityprovider import signed, signals # noqa
36from identityprovider.macaroon import MacaroonRequest
36from identityprovider.tests import DEFAULT_USER_PASSWORD37from identityprovider.tests import DEFAULT_USER_PASSWORD
37from identityprovider.tests.factory import SSOObjectFactory38from identityprovider.tests.factory import SSOObjectFactory
38from identityprovider.utils import generate_random_string39from identityprovider.utils import generate_random_string
@@ -181,7 +182,7 @@
181 self.addCleanup(p.disable)182 self.addCleanup(p.disable)
182 return test_rsa_priv_key, test_rsa_pub_key183 return test_rsa_priv_key, test_rsa_pub_key
183184
184 def build_macaroon(self, service_location=None):185 def build_macaroon(self, service_location=None, version=1):
185 if service_location is None:186 if service_location is None:
186 service_location = settings.MACAROON_SERVICE_LOCATION187 service_location = settings.MACAROON_SERVICE_LOCATION
187 test_rsa_priv_key, test_rsa_pub_key = self.setup_key_pair()188 test_rsa_priv_key, test_rsa_pub_key = self.setup_key_pair()
@@ -194,16 +195,37 @@
194 identifier='A test macaroon',195 identifier='A test macaroon',
195 )196 )
196 random_key = binascii.hexlify(os.urandom(32))197 random_key = binascii.hexlify(os.urandom(32))
197 info = {198 if version == 0:
198 'roothash': macaroon_random_key,199 info = {
199 '3rdparty': random_key,200 'roothash': macaroon_random_key,
200 }201 '3rdparty': random_key,
201 info_encrypted = base64.b64encode(202 }
202 test_rsa_pub_key.encrypt(json.dumps(info), 32)[0])203 payload = base64.b64encode(
204 test_rsa_pub_key.encrypt(json.dumps(info), 32)[0])
205 else:
206 payload = json.dumps({
207 'version': 1,
208 'secret': base64.b64encode(
209 test_rsa_pub_key.encrypt(random_key, 32)[0]),
210 })
203 root_macaroon.add_third_party_caveat(211 root_macaroon.add_third_party_caveat(
204 service_location, random_key, info_encrypted)212 service_location, random_key, payload)
205 return root_macaroon, macaroon_random_key, random_key213 return root_macaroon, macaroon_random_key, random_key
206214
215 def get_sso_caveat(self, macaroon, service_location=None):
216 """Extract the SSO caveat from a macaroon."""
217 if service_location is None:
218 service_location = settings.MACAROON_SERVICE_LOCATION
219 (sso_caveat,) = [c for c in macaroon.third_party_caveats()
220 if c.location == service_location]
221 return sso_caveat
222
223 def build_macaroon_request(self):
224 """Build a macaroon and a MacaroonRequest to discharge it."""
225 root_macaroon, _, _ = self.build_macaroon(version=1)
226 sso_caveat = self.get_sso_caveat(root_macaroon)
227 return MacaroonRequest(sso_caveat.caveat_id)
228
207 def use_fixture(self, fixture):229 def use_fixture(self, fixture):
208 """Set up 'fixture' to be used with the current test.230 """Set up 'fixture' to be used with the current test.
209231
210232
=== modified file 'src/identityprovider/views/server.py'
--- src/identityprovider/views/server.py 2016-05-05 11:39:43 +0000
+++ src/identityprovider/views/server.py 2016-05-11 17:42:55 +0000
@@ -42,7 +42,7 @@
42from openid.yadis.constants import YADIS_HEADER_NAME42from openid.yadis.constants import YADIS_HEADER_NAME
4343
44from identityprovider import signed44from identityprovider import signed
45from identityprovider.auth import build_discharge_macaroon_from_root45from identityprovider.auth import build_discharge_macaroon
46from identityprovider.const import (46from identityprovider.const import (
47 AX_DATA_FIELDS,47 AX_DATA_FIELDS,
48 MACAROON_NS,48 MACAROON_NS,
@@ -321,7 +321,7 @@
321 teams_form = TeamsRequestForm(request, teams_request, rpconfig,321 teams_form = TeamsRequestForm(request, teams_request, rpconfig,
322 approved_data=approved_data.get('teams'))322 approved_data=approved_data.get('teams'))
323 macaroon_form = MacaroonRequestForm(323 macaroon_form = MacaroonRequestForm(
324 request, macaroon_request, rpconfig,324 request, macaroon_request, rpconfig, orequest.trust_root,
325 approved_data=approved_data.get('macaroon'))325 approved_data=approved_data.get('macaroon'))
326 context = {326 context = {
327 'account': request.user,327 'account': request.user,
@@ -637,9 +637,10 @@
637 'approved': teams_form.teams_approved_by_user}637 'approved': teams_form.teams_approved_by_user}
638638
639 macaroon_args = orequest.message.getArgs(MACAROON_NS)639 macaroon_args = orequest.message.getArgs(MACAROON_NS)
640 if macaroon_args.get('root'):640 if macaroon_args.get('caveat_id'):
641 macaroon_form = MacaroonRequestForm(641 macaroon_form = MacaroonRequestForm(
642 request, MacaroonRequest.fromOpenIDRequest(orequest), rpconfig)642 request, MacaroonRequest.fromOpenIDRequest(orequest), rpconfig,
643 orequest.trust_root)
643 approved_data['macaroon'] = {644 approved_data['macaroon'] = {
644 'requested': macaroon_form.data.keys(),645 'requested': macaroon_form.data.keys(),
645 'approved': macaroon_form.data_approved_for_request.keys()}646 'approved': macaroon_form.data_approved_for_request.keys()}
@@ -767,10 +768,11 @@
767 """Add discharge macaroon if requested and approved."""768 """Add discharge macaroon if requested and approved."""
768 macaroon_request = MacaroonRequest.fromOpenIDRequest(openid_request)769 macaroon_request = MacaroonRequest.fromOpenIDRequest(openid_request)
769 rpconfig = utils.get_rpconfig(openid_request.trust_root)770 rpconfig = utils.get_rpconfig(openid_request.trust_root)
770 form = MacaroonRequestForm(request, macaroon_request, rpconfig)771 form = MacaroonRequestForm(
772 request, macaroon_request, rpconfig, openid_request.trust_root)
771 if form.data_approved_for_request:773 if form.data_approved_for_request:
772 discharge_macaroon = build_discharge_macaroon_from_root(774 discharge_macaroon = build_discharge_macaroon(
773 request.user, macaroon_request.root_macaroon_raw)775 request.user, macaroon_request.caveat_id)
774 macaroon_response = MacaroonResponse.extractResponse(776 macaroon_response = MacaroonResponse.extractResponse(
775 macaroon_request, discharge_macaroon.serialize())777 macaroon_request, discharge_macaroon.serialize())
776 openid_response.addExtension(macaroon_response)778 openid_response.addExtension(macaroon_response)
777779
=== modified file 'src/webui/views/consumer.py'
--- src/webui/views/consumer.py 2016-04-11 08:57:57 +0000
+++ src/webui/views/consumer.py 2016-05-11 17:42:55 +0000
@@ -171,24 +171,13 @@
171 auth_request.addExtension(teams_request_from_string(req_teams))171 auth_request.addExtension(teams_request_from_string(req_teams))
172172
173 if request.POST.get('macaroon', False):173 if request.POST.get('macaroon', False):
174 macaroon_random_key = binascii.hexlify(os.urandom(32))
175 root_macaroon = Macaroon(
176 location='Test consumer',
177 key=macaroon_random_key,
178 identifier='A test macaroon',
179 )
180 random_key = binascii.hexlify(os.urandom(32))174 random_key = binascii.hexlify(os.urandom(32))
181 info = {
182 'roothash': macaroon_random_key,
183 '3rdparty': random_key,
184 }
185 pubkey = settings.CRYPTO_SSO_PRIVKEY.publickey()175 pubkey = settings.CRYPTO_SSO_PRIVKEY.publickey()
186 info_encrypted = base64.b64encode(176 caveat_id = json.dumps({
187 pubkey.encrypt(json.dumps(info), 32)[0])177 'version': 1,
188 root_macaroon.add_third_party_caveat(178 'secret': base64.b64encode(pubkey.encrypt(random_key, 32)[0]),
189 settings.MACAROON_SERVICE_LOCATION, random_key, info_encrypted)179 })
190 macaroon_request = macaroon.MacaroonRequest(180 macaroon_request = macaroon.MacaroonRequest(caveat_id)
191 root_macaroon.serialize())
192 auth_request.addExtension(macaroon_request)181 auth_request.addExtension(macaroon_request)
193182
194 # Compute the trust root and return URL values to build the183 # Compute the trust root and return URL values to build the