Merge lp:~facundo/canonical-identity-provider/new-era-discharge into lp:canonical-identity-provider/release

Proposed by Facundo Batista
Status: Merged
Merged at revision: 1440
Proposed branch: lp:~facundo/canonical-identity-provider/new-era-discharge
Merge into: lp:canonical-identity-provider/release
Diff against target: 952 lines (+461/-107)
9 files modified
src/api/v20/handlers.py (+41/-16)
src/api/v20/tests/test_handlers.py (+103/-18)
src/identityprovider/auth.py (+84/-21)
src/identityprovider/tests/test_auth.py (+219/-38)
src/identityprovider/tests/test_forms.py (+6/-6)
src/identityprovider/tests/test_macaroon.py (+1/-1)
src/identityprovider/tests/test_views_server.py (+4/-4)
src/identityprovider/tests/utils.py (+1/-1)
src/identityprovider/views/server.py (+2/-2)
To merge this branch: bzr merge lp:~facundo/canonical-identity-provider/new-era-discharge
Reviewer Review Type Date Requested Status
William Grant Needs Fixing
Facundo Batista (community) Approve
Ricardo Kirkner (community) Approve
Review via email: mp+293879@code.launchpad.net

Commit message

Accept the caveat_id (instead of macaroon) and return the unbound discharge (instead of binding it) for both create and refresh discharges.

Description of the change

Accept the caveat_id (instead of macaroon) and return the unbound discharge (instead of binding it) for both create and refresh discharges.

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

LGTM

review: Approve
Revision history for this message
Facundo Batista (facundo) :
review: Approve
Revision history for this message
Ubuntu One Auto Pilot (otto-pilot) wrote :
Download full text (33.8 KiB)

The attempt to merge lp:~facundo/canonical-identity-provider/new-era-discharge into lp:canonical-identity-provider failed. Below is the output from the failed tests.

Bootstrapping...
rm -rf /mnt/tarmac/cache/canonical-identity-provider/merges/trunk/env
rm -rf branches/wheels
rm -rf branches/*
rm -rf staticfiles
rm -f lib/versioninfo.py
find -name '*.pyc' -delete
find -name '*.~*' -delete
Not deleting /mnt/tarmac/cache/canonical-identity-provider/merges/trunk/env/bin
New python executable in /mnt/tarmac/cache/canonical-identity-provider/merges/trunk/env/bin/python
Installing setuptools, pip...done.
/usr/lib/config-manager/cm.py update config-manager.txt
touch branches/last_build
[ -d branches/wheels ] && (cd branches/wheels && bzr pull) || (bzr branch lp:~ubuntuone-pqm-team/canonical-identity-provider/dependencies branches/wheels)
bzr version-info --format=python > lib/versioninfo.py
/mnt/tarmac/cache/canonical-identity-provider/merges/trunk/env/bin/python /mnt/tarmac/cache/canonical-identity-provider/merges/trunk/env/bin/pip install --find-links=branches/wheels --no-index -r requirements.txt
Ignoring indexes: https://pypi.python.org/simple/
Downloading/unpacking bson==0.3.3 (from -r requirements.txt (line 1))
Downloading/unpacking canonical-raven==0.0.3 (from -r requirements.txt (line 2))
Downloading/unpacking convoy==0.4.1 (from -r requirements.txt (line 3))
Downloading/unpacking django==1.8.13 (from -r requirements.txt (line 4))
Downloading/unpacking django-honeypot==0.4.0 (from -r requirements.txt (line 5))
Downloading/unpacking django-jsonfield==0.9.13 (from -r requirements.txt (line 6))
Downloading/unpacking django-model-utils==2.2 (from -r requirements.txt (line 7))
Downloading/unpacking django-modeldict==1.4.1 (from -r requirements.txt (line 8))
Downloading/unpacking django-preflight==0.1.5 (from -r requirements.txt (line 9))
Downloading/unpacking django-secure==1.0.1 (from -r requirements.txt (line 10))
Downloading/unpacking django-statsd-mozilla==0.3.15 (from -r requirements.txt (line 11))
Downloading/unpacking gargoyle==0.11.0 (from -r requirements.txt (line 12))
Downloading/unpacking gunicorn==19.3.0 (from -r requirements.txt (line 13))
Downloading/unpacking lazr.authentication==0.1.3 (from -r requirements.txt (line 14))
Downloading/unpacking libnacl==1.3.6 (from -r requirements.txt (line 15))
Downloading/unpacking nexus==0.3.1 (from -r requirements.txt (line 16))
Downloading/unpacking oath==1.4.0 (from -r requirements.txt (line 17))
Downloading/unpacking oauthlib==0.7.2 (from -r requirements.txt (line 18))
Requirement already satisfied (use --upgrade to upgrade): oops==0.0.13 in /usr/lib/pymodules/python2.7 (from -r requirements.txt (line 19))
Downloading/unpacking oops-amqp==0.0.8b1 (from -r requirements.txt (line 20))
Downloading/unpacking oops-datedir-repo==0.0.23 (from -r requirements.txt (line 21))
Downloading/unpacking oops-dictconfig==0.0.6 (from -r requirements.txt (line 22))
Downloading/unpacking oops-timeline==0.0.2 (from -r requirements.txt (line 23))
Downloading/unpacking oops-wsgi==0.0.11 (from -r requirements.txt (line 24))
Downloading/unpacking paste==2.0.1 (from -r requirements.txt (line 25))
Requirement already s...

Revision history for this message
Facundo Batista (facundo) wrote :

Already fixed

review: Approve
Revision history for this message
William Grant (wgrant) :
review: Needs Fixing
Revision history for this message
Facundo Batista (facundo) :
Revision history for this message
William Grant (wgrant) :
Revision history for this message
Facundo Batista (facundo) wrote :

Addressed the "no caveat id in refreshing" issue

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/api/v20/handlers.py'
2--- src/api/v20/handlers.py 2016-04-18 15:53:18 +0000
3+++ src/api/v20/handlers.py 2016-05-05 12:45:29 +0000
4@@ -58,6 +58,7 @@
5 TokenScope,
6 )
7 from identityprovider.signals import login_failed, login_succeeded
8+from identityprovider.stats import stats
9 from identityprovider.timeline_helpers import get_request_timing_function
10 from identityprovider.utils import redirection_url_for_token
11
12@@ -446,11 +447,19 @@
13 if 'macaroon' in data:
14 simple_macaroon = True
15 root_macaroons_info = [(None, data['macaroon'])]
16+ stats.increment('macaroon.discharge', key='single_macaroon')
17 elif 'macaroons' in data:
18 simple_macaroon = False
19 root_macaroons_info = data['macaroons']
20+ stats.increment('macaroon.discharge', key='multi_macaroons')
21+ elif 'caveat_id' in data:
22+ simple_macaroon = True
23+ root_macaroons_info = None
24+ caveat_id = data['caveat_id']
25+ stats.increment('macaroon.discharge', key='caveat_id')
26 else:
27- missing = {'macaroon or macaroons': [FIELD_REQUIRED]}
28+ # let's flag the new one only, not the deprecated ones
29+ missing = {'caveat_id': [FIELD_REQUIRED]}
30 return errors.INVALID_DATA(**missing)
31
32 account = None
33@@ -490,16 +499,22 @@
34 if response is not None:
35 return response
36
37- all_discharges = []
38- for mac_id, root_macaroon_raw in root_macaroons_info:
39- try:
40- discharge = auth.build_discharge_macaroon(
41- account, root_macaroon_raw)
42- except ValidationError:
43- return errors.INVALID_DATA()
44- except AuthenticationError:
45- return errors.INVALID_CREDENTIALS()
46- all_discharges.append((mac_id, discharge.serialize()))
47+ try:
48+ if root_macaroons_info is None:
49+ discharge = auth.build_discharge_macaroon(account, caveat_id)
50+ all_discharges = [(None, discharge.serialize())]
51+ else:
52+ # 2016-05-03: this is deprecated, just supporting it until
53+ # no more clients send root macaroon(s)
54+ all_discharges = []
55+ for mac_id, root_macaroon_raw in root_macaroons_info:
56+ discharge = auth.build_discharge_macaroon_from_root(
57+ account, root_macaroon_raw)
58+ all_discharges.append((mac_id, discharge.serialize()))
59+ except ValidationError:
60+ return errors.INVALID_DATA()
61+ except AuthenticationError:
62+ return errors.INVALID_CREDENTIALS()
63
64 response = rc.ALL_OK
65 if simple_macaroon:
66@@ -523,16 +538,26 @@
67
68 data = request.data
69 try:
70- root_macaroon_raw = data['root_macaroon']
71 discharge_macaroon_raw = data['discharge_macaroon']
72 except KeyError:
73- expected = {'root_macaroon', 'discharge_macaroon'}
74- missing = dict((k, [FIELD_REQUIRED]) for k in expected - set(data))
75+ missing = {'discharge_macaroon': [FIELD_REQUIRED]}
76+ return errors.INVALID_DATA(**missing)
77+
78+ if 'caveat_id' in data:
79+ method = auth.refresh_discharge
80+ params = (data['caveat_id'], discharge_macaroon_raw)
81+ stats.increment('macaroon.refresh', key='caveat_id')
82+ elif 'root_macaroon' in data:
83+ method = auth.refresh_macaroons
84+ params = (data['root_macaroon'], discharge_macaroon_raw)
85+ stats.increment('macaroon.refresh', key='macaroon')
86+ else:
87+ # let's flag the new one only, not the deprecated one
88+ missing = {'caveat_id': [FIELD_REQUIRED]}
89 return errors.INVALID_DATA(**missing)
90
91 try:
92- new_discharge = auth.refresh_macaroons(root_macaroon_raw,
93- discharge_macaroon_raw)
94+ new_discharge = method(*params)
95 except ValidationError:
96 return errors.INVALID_DATA()
97 except AccountDeactivated:
98
99=== modified file 'src/api/v20/tests/test_handlers.py'
100--- src/api/v20/tests/test_handlers.py 2016-04-18 15:53:18 +0000
101+++ src/api/v20/tests/test_handlers.py 2016-05-05 12:45:29 +0000
102@@ -2169,13 +2169,25 @@
103 self.login_failed_calls = []
104 login_failed.connect(self.track_failed_logins, dispatch_uid=self.id())
105
106- self.root_macaroon, self.macaroon_random_key = self.build_macaroon()
107+ self.root_macaroon, self.macaroon_random_key, _ = self.build_macaroon()
108 self.data = dict(
109 email='foo@bar.com', password='foobar123',
110 macaroon=self.root_macaroon.serialize())
111 self.account = self.factory.make_account(
112 email=self.data['email'], password=self.data['password'])
113
114+ self._stats_increment = self.patch('api.v20.handlers.stats.increment')
115+
116+ def check_stats_increment(self, *args, **kwargs):
117+ """Check stats increment was properly called."""
118+ calls = self._stats_increment.call_args_list
119+ self.assertEqual(len(calls), 1)
120+ call_args, call_kwargs = calls[0]
121+ self.assertEqual(call_args, args,
122+ "Bad args in stats increment call {}".format(calls))
123+ self.assertEqual(call_kwargs, kwargs,
124+ "Bad kwargs in stats increment call {}".format(calls))
125+
126
127 class MacaroonDischargeHandlerTestCase(MacaroonHandlerBaseTestCase,
128 AuthLogTestCaseMixin):
129@@ -2199,7 +2211,7 @@
130 response)
131 return json.loads(response.content)
132
133- def assert_response_correct(self, json_body, account):
134+ def assert_response_correct(self, json_body, account, bind=False):
135 """Check we received a good discharge macaroon for the root sent.
136
137 Proper verification of the received macaroon internals happens in
138@@ -2207,6 +2219,9 @@
139 """
140 discharge_macaroon_raw = json_body['discharge_macaroon']
141 discharge_macaroon = Macaroon.deserialize(discharge_macaroon_raw)
142+ if bind:
143+ discharge_macaroon = self.root_macaroon.prepare_for_request(
144+ discharge_macaroon)
145 v = Verifier()
146 v.satisfy_general(lambda c: True)
147 v.verify(
148@@ -2327,6 +2342,7 @@
149 def test_macaroon_created(self):
150 json_body = self.do_post()
151 self.assert_response_correct(json_body, self.account)
152+ self.check_stats_increment('macaroon.discharge', key='single_macaroon')
153
154 def test_root_macaroon_corrupt(self):
155 data = dict(
156@@ -2337,23 +2353,23 @@
157 check_login_failed=False)
158
159 def test_root_macaroon_not_for_sso(self):
160- macaroon, _ = self.build_macaroon(service_location="other service")
161+ macaroon, _, _ = self.build_macaroon(service_location="other service")
162 data = dict(email='foo@bar.com', password='foobar123',
163 macaroon=macaroon.serialize())
164 self.assert_failed_login('INVALID_CREDENTIALS', data,
165 expected_status_code=401,
166 check_login_failed=False)
167
168- def test_missing_macaroons(self):
169+ def test_missing_macaroon_info(self):
170 data = dict(email='foo@bar.com', password='foobar123')
171- extra = {'macaroon or macaroons': [handlers.FIELD_REQUIRED]}
172+ extra = {'caveat_id': [handlers.FIELD_REQUIRED]}
173 self.assert_failed_login('INVALID_DATA', data, extra=extra,
174 expected_status_code=400,
175 check_login_failed=False)
176
177 def test_multiple_macaroons(self):
178 # build *several* macaroons and send them all at once
179- roots, rkeys = zip(*[self.build_macaroon() for _ in range(3)])
180+ roots, rkeys, _ = zip(*[self.build_macaroon() for _ in range(3)])
181 ids = range(3)
182 roots = dict(zip(ids, roots))
183 rkeys = dict(zip(ids, rkeys))
184@@ -2372,8 +2388,11 @@
185 v.satisfy_general(verifying_function)
186 v.verify(root_macaroon, key, [discharge_macaroon])
187
188+ self.check_stats_increment('macaroon.discharge', key='multi_macaroons')
189+
190 def test_multiple_mixed(self):
191- bad_macaroon, _ = self.build_macaroon(service_location="other service")
192+ bad_macaroon, _, _ = self.build_macaroon(
193+ service_location="other service")
194 data = dict(email='foo@bar.com', password='foobar123', macaroons=[
195 ('good', self.root_macaroon.serialize()),
196 ('bad', bad_macaroon.serialize()),
197@@ -2382,6 +2401,24 @@
198 expected_status_code=401,
199 check_login_failed=False)
200
201+ def test_caveat_id_discharge_created(self):
202+ # discharge the test macaroon
203+ (caveat,) = [c for c in self.root_macaroon.third_party_caveats()
204+ if c.location == settings.MACAROON_SERVICE_LOCATION]
205+ data = dict(email='foo@bar.com', password='foobar123',
206+ caveat_id=caveat.caveat_id)
207+
208+ json_body = self.do_post(data=data)
209+ self.assert_response_correct(json_body, self.account, bind=True)
210+ self.check_stats_increment('macaroon.discharge', key='caveat_id')
211+
212+ def test_caveat_id_corrupt(self):
213+ data = dict(email='foo@bar.com', password='foobar123',
214+ caveat_id="I'm a seriously corrupted caveat")
215+ self.assert_failed_login('INVALID_DATA', data,
216+ expected_status_code=400,
217+ check_login_failed=False)
218+
219
220 class MacaroonHandlerTimelineTestCase(MacaroonHandlerBaseTestCase,
221 TimelineActionMixin):
222@@ -2428,12 +2465,16 @@
223 super(MacaroonRefreshHandlerTestCase, self).setUp()
224
225 # discharge the test macaroon
226- root_macaroon_raw = self.root_macaroon.serialize()
227- self.discharge_macaroon = build_discharge_macaroon(
228- self.account, root_macaroon_raw)
229-
230- self.data = {'root_macaroon': root_macaroon_raw,
231- 'discharge_macaroon': self.discharge_macaroon.serialize()}
232+ (caveat,) = [c for c in self.root_macaroon.third_party_caveats()
233+ if c.location == settings.MACAROON_SERVICE_LOCATION]
234+
235+ unbound_discharge = build_discharge_macaroon(
236+ self.account, caveat.caveat_id)
237+ self.discharge_macaroon = self.root_macaroon.prepare_for_request(
238+ unbound_discharge)
239+
240+ self.data = {'caveat_id': caveat.caveat_id,
241+ 'discharge_macaroon': unbound_discharge.serialize()}
242
243 def do_post(self, data=None, expected_status_code=200,
244 content_type='application/json', **kwargs):
245@@ -2463,13 +2504,23 @@
246 self.assertEqual(json_body['extra'], extra)
247 return json_body
248
249- def test_required_parameters(self):
250+ def test_required_parameters_discharge(self):
251 json_body = self.do_post(expected_status_code=400, data={})
252
253 self.assertEqual(json_body, {
254 'code': 'INVALID_DATA',
255- 'extra': {'root_macaroon': [handlers.FIELD_REQUIRED],
256- 'discharge_macaroon': [handlers.FIELD_REQUIRED]},
257+ 'extra': {'discharge_macaroon': [handlers.FIELD_REQUIRED]},
258+ 'message': 'Invalid request data'},
259+ )
260+ self.assertEqual(self.login_failed_calls, [])
261+
262+ def test_required_parameters_caveat_id(self):
263+ json_body = self.do_post(expected_status_code=400,
264+ data={'discharge_macaroon': 'whatever'})
265+
266+ self.assertEqual(json_body, {
267+ 'code': 'INVALID_DATA',
268+ 'extra': {'caveat_id': [handlers.FIELD_REQUIRED]},
269 'message': 'Invalid request data'},
270 )
271 self.assertEqual(self.login_failed_calls, [])
272@@ -2486,7 +2537,7 @@
273 check_login_failed=False)
274
275 def test_macaroon_bad_authinfo(self):
276- macaroon, _ = self.build_macaroon(service_location="other service")
277+ macaroon, _, _ = self.build_macaroon(service_location="other service")
278 data = dict(root_macaroon=macaroon.serialize(),
279 discharge_macaroon=self.discharge_macaroon.serialize())
280 self.assert_failed_login('INVALID_CREDENTIALS', data,
281@@ -2494,7 +2545,9 @@
282 check_login_failed=False)
283
284 def test_macaroon_refreshed(self):
285- json_body = self.do_post()
286+ data = {'root_macaroon': self.root_macaroon.serialize(),
287+ 'discharge_macaroon': self.discharge_macaroon.serialize()}
288+ json_body = self.do_post(data=data)
289
290 # get new discharge macaroon and verify with old root (its internals
291 # are verified by the tests of the 'refresh_macaroons' function itself)
292@@ -2504,3 +2557,35 @@
293 v.satisfy_general(lambda c: True)
294 v.verify(
295 self.root_macaroon, self.macaroon_random_key, [discharge_macaroon])
296+ self.check_stats_increment('macaroon.refresh', key='macaroon')
297+
298+ def test_caveat_id_corrupt(self):
299+ data = dict(caveat_id="I'm a seriously corrupted caveat",
300+ discharge_macaroon="Also broken")
301+ self.assert_failed_login('INVALID_DATA', data,
302+ expected_status_code=400,
303+ check_login_failed=False)
304+
305+ def test_caveat_id_bad_authinfo(self):
306+ macaroon, _, _ = self.build_macaroon(service_location="other service")
307+ (caveat,) = [c for c in macaroon.third_party_caveats()
308+ if c.location == "other service"]
309+ data = dict(caveat_id=caveat.caveat_id,
310+ discharge_macaroon=self.discharge_macaroon.serialize())
311+ self.assert_failed_login('INVALID_CREDENTIALS', data,
312+ expected_status_code=401,
313+ check_login_failed=False)
314+
315+ def test_caveat_id_refreshed(self):
316+ json_body = self.do_post()
317+
318+ # get new discharge macaroon, apply it and verify with old root (its
319+ # internals are verified by the tests of the 'refresh_macaroons'
320+ # function itself)
321+ unbound_discharge_raw = json_body['discharge_macaroon']
322+ unbound_discharge = Macaroon.deserialize(unbound_discharge_raw)
323+ discharge = self.root_macaroon.prepare_for_request(unbound_discharge)
324+ v = Verifier()
325+ v.satisfy_general(lambda c: True)
326+ v.verify(self.root_macaroon, self.macaroon_random_key, [discharge])
327+ self.check_stats_increment('macaroon.refresh', key='caveat_id')
328
329=== modified file 'src/identityprovider/auth.py'
330--- src/identityprovider/auth.py 2016-03-23 16:20:24 +0000
331+++ src/identityprovider/auth.py 2016-05-05 12:45:29 +0000
332@@ -507,16 +507,21 @@
333 # the macaroon doesn't have a location for this service
334 raise AuthenticationError("The received macaroon is not for ours")
335
336- # get the only-for-this-project random key
337+ caveat_info = _decrypt_caveat(sso_caveat.caveat_id)
338+ return sso_caveat.caveat_id, caveat_info
339+
340+
341+def _decrypt_caveat(raw_caveat_id):
342+ """Decrypt and decode the caveat id info."""
343 try:
344 caveat_info_raw = settings.CRYPTO_SSO_PRIVKEY.decrypt(
345- base64.b64decode(sso_caveat.caveat_id))
346+ base64.b64decode(raw_caveat_id))
347 caveat_info = json.loads(caveat_info_raw)
348 except:
349 # not properly encrypted information inside
350- raise AuthenticationError("Bad info in the caveat_id")
351+ raise ValidationError("Bad info in the caveat_id")
352
353- return sso_caveat.caveat_id, caveat_info
354+ return caveat_info
355
356
357 def _build_discharge(macaroon_key, caveat_id, account,
358@@ -556,8 +561,12 @@
359 return d
360
361
362-def build_discharge_macaroon(account, root_macaroon_raw):
363- """Build a discharge macaroon from a root one."""
364+def build_discharge_macaroon_from_root(account, root_macaroon_raw):
365+ """Build a discharge macaroon from a root one.
366+
367+ This function is deprecated; will be removed when clients stop hitting
368+ the handler passing the root macaroon.
369+ """
370 try:
371 root_macaroon = Macaroon.deserialize(root_macaroon_raw)
372 except:
373@@ -576,18 +585,24 @@
374 return discharge
375
376
377-def refresh_macaroons(root_macaroon_raw, discharge_macaroon_raw):
378- """Refresh a root/discharge pair with a new discharge macaroon."""
379+def build_discharge_macaroon(account, raw_caveat_id):
380+ """Build a discharge macaroon from the caveat id."""
381+ # get the decrypted deserialized info
382+ caveat_info = _decrypt_caveat(raw_caveat_id)
383+
384+ # create a discharge macaroon with same location, key and
385+ # identifier than it's original 3rd-party caveat (so they can
386+ # be matched and verified)
387+ discharge = _build_discharge(
388+ caveat_info['3rdparty'], raw_caveat_id, account)
389+
390+ # return the unbound discharge macaroon
391+ return discharge
392+
393+
394+def _account_from_macaroon(macaroon, key, discharge=None):
395+ """Get the new discharge macaroon."""
396 service_location = settings.MACAROON_SERVICE_LOCATION
397-
398- try:
399- root_macaroon = Macaroon.deserialize(root_macaroon_raw)
400- discharge_macaroon = Macaroon.deserialize(discharge_macaroon_raw)
401- except:
402- raise ValidationError("The received Macaroons are corrupt")
403-
404- # get the raw caveat id and the decrypted deserialized info
405- raw_caveat_id, caveat_info = _get_own_caveat(root_macaroon)
406 info_holder = {}
407
408 def checker(caveat):
409@@ -606,7 +621,10 @@
410 v = Verifier()
411 v.satisfy_general(checker)
412 try:
413- v.verify(root_macaroon, caveat_info['roothash'], [discharge_macaroon])
414+ if discharge is None:
415+ v.verify(macaroon, key)
416+ else:
417+ v.verify(macaroon, key, [discharge])
418 except:
419 raise AuthenticationError("Not verifying macaroons")
420
421@@ -620,14 +638,59 @@
422 if account.accountpassword.date_changed > last_auth_ts:
423 raise AuthenticationError("Password changed")
424
425+ return account, info_holder
426+
427+
428+def refresh_macaroons(root_macaroon_raw, discharge_macaroon_raw):
429+ """Refresh a root/discharge pair with a new discharge macaroon.
430+
431+ This function is deprecated; will be removed when clients stop hitting
432+ the handler passing the root macaroon.
433+ """
434+ try:
435+ root_macaroon = Macaroon.deserialize(root_macaroon_raw)
436+ discharge_macaroon = Macaroon.deserialize(discharge_macaroon_raw)
437+ except:
438+ raise ValidationError("The received Macaroons are corrupt")
439+
440+ # get the raw caveat id and the decrypted deserialized info
441+ raw_caveat_id, caveat_info = _get_own_caveat(root_macaroon)
442+
443+ account, macaroon_info = _account_from_macaroon(
444+ root_macaroon, caveat_info['roothash'], discharge_macaroon)
445+
446 # create the new discharge macaroon with same location, key and
447 # identifier than it's original 3rd-party caveat (so they can
448 # be matched and verified), and keeping the last_auth and expires from
449 # the original discharge macaroon
450- d = _build_discharge(caveat_info['3rdparty'], raw_caveat_id, account,
451- last_auth=info_holder['last_auth'],
452- expires=info_holder['expires'])
453+ d = _build_discharge(
454+ caveat_info['3rdparty'], raw_caveat_id, account,
455+ last_auth=macaroon_info['last_auth'], expires=macaroon_info['expires'])
456
457 # return the properly prepared discharge macaroon
458 discharge = root_macaroon.prepare_for_request(d)
459 return discharge
460+
461+
462+def refresh_discharge(raw_caveat_id, discharge_macaroon_raw):
463+ """Refresh a discharge with a new discharge macaroon."""
464+ try:
465+ discharge_macaroon = Macaroon.deserialize(discharge_macaroon_raw)
466+ except:
467+ raise ValidationError("The received Macaroons are corrupt")
468+
469+ # get the decrypted deserialized info
470+ caveat_info = _decrypt_caveat(raw_caveat_id)
471+ account, macaroon_info = _account_from_macaroon(
472+ discharge_macaroon, caveat_info['3rdparty'])
473+
474+ # create the new discharge macaroon with same location, key and
475+ # identifier than it's original 3rd-party caveat (so they can
476+ # be matched and verified), and keeping the last_auth and expires from
477+ # the original discharge macaroon
478+ discharge = _build_discharge(
479+ caveat_info['3rdparty'], raw_caveat_id, account,
480+ last_auth=macaroon_info['last_auth'], expires=macaroon_info['expires'])
481+
482+ # return the unbound discharge macaroon
483+ return discharge
484
485=== modified file 'src/identityprovider/tests/test_auth.py'
486--- src/identityprovider/tests/test_auth.py 2016-04-01 19:35:14 +0000
487+++ src/identityprovider/tests/test_auth.py 2016-05-05 12:45:29 +0000
488@@ -29,9 +29,11 @@
489 LaunchpadBackend,
490 SSOOAuthAuthentication,
491 SSORequestValidator,
492- _get_own_caveat,
493+ _decrypt_caveat,
494 basic_authenticate,
495 build_discharge_macaroon,
496+ build_discharge_macaroon_from_root,
497+ refresh_discharge,
498 refresh_macaroons,
499 validate_oauth_signature,
500 )
501@@ -958,26 +960,31 @@
502 timer=self.dummy_timer)
503
504
505-class BuildMacaroonDischargeTestCase(SSOBaseTestCase):
506+class BuildMacaroonFromRootDischargeTestCase(SSOBaseTestCase):
507+ """Test the deprecated build_discharge_macaroon_from_root.
508+
509+ These tests are kept separated as that function won't evolve no more.
510+ """
511
512 def setUp(self):
513- super(BuildMacaroonDischargeTestCase, self).setUp()
514- self.root_macaroon, self.macaroon_random_key = self.build_macaroon()
515+ super(BuildMacaroonFromRootDischargeTestCase, self).setUp()
516+ self.root_macaroon, self.macaroon_random_key, _ = self.build_macaroon()
517
518 def test_root_macaroon_corrupt(self):
519- self.assertRaises(ValidationError, build_discharge_macaroon,
520+ self.assertRaises(ValidationError, build_discharge_macaroon_from_root,
521 "fake account", "I'm a seriously corrupted macaroon")
522
523 def test_root_macaroon_not_for_sso(self):
524- macaroon, _ = self.build_macaroon(service_location="other service")
525- self.assertRaises(AuthenticationError, build_discharge_macaroon,
526+ macaroon, _, _ = self.build_macaroon(service_location="other service")
527+ self.assertRaises(AuthenticationError,
528+ build_discharge_macaroon_from_root,
529 "fake account", macaroon.serialize())
530
531 def test_proper_discharging(self):
532 # build the input and call
533 real_account = self.factory.make_account()
534 before = now()
535- discharge_macaroon = build_discharge_macaroon(
536+ discharge_macaroon = build_discharge_macaroon_from_root(
537 real_account, self.root_macaroon.serialize())
538 after = now()
539
540@@ -1029,6 +1036,76 @@
541 [discharge_macaroon])
542
543
544+class BuildMacaroonDischargeTestCase(SSOBaseTestCase):
545+
546+ def test_caveat_id_corrupt(self):
547+ self.assertRaises(ValidationError, build_discharge_macaroon,
548+ "fake account", "I'm a seriously corrupted caveatid")
549+
550+ def test_caveat_id_not_for_sso(self):
551+ macaroon, _, _ = self.build_macaroon(service_location="other service")
552+ (sso_caveat,) = [c for c in macaroon.third_party_caveats()
553+ if c.location == "other service"]
554+
555+ self.assertRaises(ValidationError, build_discharge_macaroon,
556+ "fake account", sso_caveat)
557+
558+ def test_proper_discharging(self):
559+ # build the input and call
560+ root_macaroon, _, random_key = self.build_macaroon()
561+ (sso_caveat,) = [c for c in root_macaroon.third_party_caveats()
562+ if c.location == settings.MACAROON_SERVICE_LOCATION]
563+
564+ real_account = self.factory.make_account()
565+ before = now()
566+ discharge_macaroon = build_discharge_macaroon(
567+ real_account, sso_caveat.caveat_id)
568+ after = now()
569+
570+ # test
571+ def checker(caveat):
572+ """Assure all caveats inside the discharged macaroon are ok."""
573+ source, key, value = caveat.split("|", 2)
574+
575+ if key == 'valid_since':
576+ valid_since = datetime.strptime(value, '%Y-%m-%dT%H:%M:%S.%f')
577+ self.assertGreater(valid_since, before)
578+ self.assertGreater(after, valid_since)
579+ return True
580+
581+ if key == 'last_auth':
582+ last_auth = datetime.strptime(value, '%Y-%m-%dT%H:%M:%S.%f')
583+ self.assertGreater(last_auth, before)
584+ self.assertGreater(after, last_auth)
585+ return True
586+
587+ if key == 'account':
588+ acc = json.loads(base64.b64decode(value).decode("utf8"))
589+ self.assertEqual(acc['openid'], real_account.openid_identifier)
590+ self.assertEqual(acc['email'],
591+ real_account.preferredemail.email)
592+ self.assertEqual(acc['displayname'], real_account.displayname)
593+ self.assertEqual(acc['is_verified'], real_account.is_verified)
594+ return True
595+
596+ if key == 'expires':
597+ expires = datetime.strptime(value, '%Y-%m-%dT%H:%M:%S.%f')
598+ before_plus_ttl = before + timedelta(
599+ seconds=settings.MACAROON_TTL)
600+ after_plus_ttl = after + timedelta(
601+ seconds=settings.MACAROON_TTL)
602+ self.assertGreater(expires, before_plus_ttl)
603+ self.assertGreater(after_plus_ttl, expires)
604+ return True
605+
606+ # we're not validating an SSO from the discharged macaroon, fail!
607+ return False
608+
609+ v = Verifier()
610+ v.satisfy_general(checker)
611+ v.verify(discharge_macaroon, random_key, [])
612+
613+
614 class MacaroonHelpersTestCase(SSOBaseTestCase):
615
616 def test_get_caveat_ok(self):
617@@ -1052,42 +1129,29 @@
618 settings.MACAROON_SERVICE_LOCATION, random_key, info_encrypted)
619
620 # check
621- raw_caveat_id, caveat_info = _get_own_caveat(root_macaroon)
622- self.assertEqual(raw_caveat_id, info_encrypted)
623+ (sso_caveat,) = [c for c in root_macaroon.third_party_caveats()
624+ if c.location == settings.MACAROON_SERVICE_LOCATION]
625+ caveat_info = _decrypt_caveat(sso_caveat.caveat_id)
626 self.assertEqual(caveat_info, info)
627
628- def test_get_caveat_not_for_sso(self):
629- macaroon, _ = self.build_macaroon(service_location="other service")
630- self.assertRaises(AuthenticationError, _get_own_caveat, macaroon)
631-
632 def test_get_caveat_badly_encrypted(self):
633- test_rsa_priv_key, test_rsa_pub_key = self.setup_key_pair()
634-
635- # create a Macaron with the proper third party caveat
636- macaroon_random_key = binascii.hexlify(os.urandom(32))
637- root_macaroon = Macaroon(
638- location='The store ;)',
639- key=macaroon_random_key,
640- identifier='A test macaroon',
641- )
642- random_key = binascii.hexlify(os.urandom(32))
643- root_macaroon.add_third_party_caveat(
644- settings.MACAROON_SERVICE_LOCATION, random_key,
645- b"not really well encrypted stuff")
646-
647- # check
648- self.assertRaises(AuthenticationError, _get_own_caveat, root_macaroon)
649-
650-
651-class MacaroonRefreshTestCase(SSOBaseTestCase):
652+ self.assertRaises(ValidationError,
653+ _decrypt_caveat, b"not really well encrypted stuff")
654+
655+
656+class MacaroonRefreshFromRootTestCase(SSOBaseTestCase):
657+ """Test the deprecated refresh_macaroons.
658+
659+ These tests are kept separated as that function won't evolve no more.
660+ """
661
662 def setUp(self):
663- super(MacaroonRefreshTestCase, self).setUp()
664- self.root_macaroon, self.macaroon_random_key = self.build_macaroon()
665+ super(MacaroonRefreshFromRootTestCase, self).setUp()
666+ self.root_macaroon, self.macaroon_random_key, _ = self.build_macaroon()
667
668 # discharge the test macaroon
669 self.account = self.factory.make_account()
670- self.discharge_macaroon = build_discharge_macaroon(
671+ self.discharge_macaroon = build_discharge_macaroon_from_root(
672 self.account, self.root_macaroon.serialize())
673
674 def test_root_macaroon_corrupt(self):
675@@ -1102,8 +1166,8 @@
676
677 def test_macaroons_dont_verify_ok(self):
678 # just get *another* discharge so it's not for the same root macaroon
679- other_root, _ = self.build_macaroon()
680- other_discharge = build_discharge_macaroon(
681+ other_root, _, _ = self.build_macaroon()
682+ other_discharge = build_discharge_macaroon_from_root(
683 self.account, other_root.serialize())
684 self.assertRaises(AuthenticationError, refresh_macaroons,
685 self.root_macaroon.serialize(),
686@@ -1197,3 +1261,120 @@
687 v.satisfy_general(checker)
688 v.verify(self.root_macaroon, self.macaroon_random_key,
689 [new_discharge])
690+
691+
692+class MacaroonRefreshTestCase(SSOBaseTestCase):
693+
694+ def setUp(self):
695+ super(MacaroonRefreshTestCase, self).setUp()
696+ root_macaroon, _, self.random_key = self.build_macaroon()
697+ (caveat,) = [c for c in root_macaroon.third_party_caveats()
698+ if c.location == settings.MACAROON_SERVICE_LOCATION]
699+ self.caveat_id = caveat.caveat_id
700+
701+ # get a discharge for the test macaroon
702+ self.account = self.factory.make_account()
703+ self.discharge_macaroon = build_discharge_macaroon(
704+ self.account, self.caveat_id)
705+
706+ def test_caveat_id_corrupt(self):
707+ self.assertRaises(
708+ ValidationError, refresh_discharge,
709+ "Corrupted caveat id", self.discharge_macaroon.serialize())
710+
711+ def test_discharge_macaroon_corrupt(self):
712+ self.assertRaises(
713+ ValidationError, refresh_discharge,
714+ self.caveat_id, "Seriously corrupted macaroon")
715+
716+ def test_macaroons_dont_verify_ok(self):
717+ # just get *another* discharge so it's not for the same root macaroon
718+ other_root, _, _ = self.build_macaroon()
719+ (other_caveat,) = [c for c in other_root.third_party_caveats()
720+ if c.location == settings.MACAROON_SERVICE_LOCATION]
721+ other_discharge = build_discharge_macaroon(
722+ self.account, other_caveat.caveat_id)
723+ self.assertRaises(AuthenticationError, refresh_discharge,
724+ self.caveat_id, other_discharge.serialize())
725+
726+ def test_deactivated_account(self):
727+ self.account.deactivate()
728+ self.assertRaises(AccountDeactivated, refresh_discharge,
729+ self.caveat_id, self.discharge_macaroon.serialize())
730+
731+ def test_password_changed(self):
732+ self.account.set_password("a new password")
733+ self.assertRaises(AuthenticationError, refresh_discharge,
734+ self.caveat_id, self.discharge_macaroon.serialize())
735+
736+ def test_password_with_no_datetime(self):
737+ # simulate an "old" account password (before we started to keep the
738+ # changed date); note that I'm bypassing the AccountPassword save(),
739+ # which will update the attribute that I want in Null! (and we confirm
740+ # that in the assert)
741+ self.account.accountpassword.date_changed = None
742+ super(AccountPassword, self.account.accountpassword).save()
743+ assert self.account.accountpassword.date_changed is None
744+
745+ # check that auths ok
746+ refresh_discharge(self.caveat_id, self.discharge_macaroon.serialize())
747+
748+ def test_proper_refreshing(self):
749+ old_discharge = self.discharge_macaroon # just rename for readability
750+ service_location = settings.MACAROON_SERVICE_LOCATION
751+
752+ def get_value(search_key):
753+ for caveat in old_discharge.first_party_caveats():
754+ source, key, value = caveat.caveat_id.split("|", 2)
755+ if source == service_location and key == search_key:
756+ return value
757+
758+ # get old values from the macaroon and also change the account to see
759+ # that reflected
760+ old_last_auth = get_value('last_auth')
761+ old_expires = get_value('expires')
762+ new_mail = self.factory.make_email_for_account(
763+ self.account, status=EmailStatus.PREFERRED)
764+ self.account.displayname = "New test display name"
765+ self.account.save()
766+
767+ # call!
768+ before = now()
769+ new_discharge = refresh_discharge(self.caveat_id,
770+ old_discharge.serialize())
771+ after = now()
772+
773+ # test
774+ def checker(caveat):
775+ """Assure all caveats inside the discharged macaroon are ok."""
776+ source, key, value = caveat.split("|", 2)
777+
778+ if key == 'valid_since':
779+ valid_since = datetime.strptime(value, '%Y-%m-%dT%H:%M:%S.%f')
780+ self.assertGreater(valid_since, before)
781+ self.assertGreater(after, valid_since)
782+ return True
783+
784+ if key == 'last_auth':
785+ self.assertEqual(value, old_last_auth)
786+ return True
787+
788+ if key == 'account':
789+ acc = json.loads(base64.b64decode(value).decode("utf8"))
790+ self.assertEqual(acc['openid'], self.account.openid_identifier)
791+ self.assertEqual(acc['email'], new_mail.email)
792+ self.assertEqual(acc['displayname'], "New test display name")
793+ self.assertEqual(acc['is_verified'], self.account.is_verified)
794+ return True
795+
796+ if key == 'expires':
797+ self.assertEqual(value, old_expires)
798+ return True
799+
800+ # we're not validating an SSO from the discharged macaroon, fail!
801+ return False
802+
803+ # verify using the NEW discharge macaroon
804+ v = Verifier()
805+ v.satisfy_general(checker)
806+ v.verify(new_discharge, self.random_key, [])
807
808=== modified file 'src/identityprovider/tests/test_forms.py'
809--- src/identityprovider/tests/test_forms.py 2016-04-11 11:57:38 +0000
810+++ src/identityprovider/tests/test_forms.py 2016-05-05 12:45:29 +0000
811@@ -1138,7 +1138,7 @@
812 """The server always returns discharge macaroons to trusted sites,
813 regardless of the state of the checkbox in the UI.
814 """
815- root_macaroon, _ = self.build_macaroon()
816+ root_macaroon, _, _ = self.build_macaroon()
817 macaroon_request = MacaroonRequest(root_macaroon.serialize())
818 form = MacaroonRequestForm(
819 request=self._get_request_with_post_args(),
820@@ -1149,7 +1149,7 @@
821 """The server returns discharge macaroons to untrusted sites when
822 the user checks the checkbox in the UI.
823 """
824- root_macaroon, _ = self.build_macaroon()
825+ root_macaroon, _, _ = self.build_macaroon()
826 macaroon_request = MacaroonRequest(root_macaroon.serialize())
827 form = MacaroonRequestForm(
828 request=self._get_request_with_post_args(macaroon='macaroon'),
829@@ -1160,7 +1160,7 @@
830 """The server does not return discharge macaroons to untrusted sites
831 when the user does not check the checkbox in the UI.
832 """
833- root_macaroon, _ = self.build_macaroon()
834+ root_macaroon, _, _ = self.build_macaroon()
835 macaroon_request = MacaroonRequest(root_macaroon.serialize())
836 form = MacaroonRequestForm(
837 request=self._get_request_with_post_args(),
838@@ -1169,7 +1169,7 @@
839
840 def test_checkbox_status_for_trusted_site(self):
841 """Checkboxes are always checked if the site is trusted."""
842- root_macaroon, _ = self.build_macaroon()
843+ root_macaroon, _, _ = self.build_macaroon()
844 macaroon_request = MacaroonRequest(root_macaroon.serialize())
845 form = MacaroonRequestForm(
846 request=self._get_request_with_post_args(),
847@@ -1178,7 +1178,7 @@
848
849 def test_checkbox_status_for_untrusted_site(self):
850 """Checkboxes are checked by default if the site is untrusted."""
851- root_macaroon, _ = self.build_macaroon()
852+ root_macaroon, _, _ = self.build_macaroon()
853 macaroon_request = MacaroonRequest(root_macaroon.serialize())
854 form = MacaroonRequestForm(
855 request=self._get_request_with_post_args(),
856@@ -1189,7 +1189,7 @@
857 """Checkboxes respect user preferences on untrusted sites where
858 available.
859 """
860- root_macaroon, _ = self.build_macaroon()
861+ root_macaroon, _, _ = self.build_macaroon()
862 macaroon_request = MacaroonRequest(root_macaroon.serialize())
863 approved_data = {
864 'requested': ['macaroon'],
865
866=== modified file 'src/identityprovider/tests/test_macaroon.py'
867--- src/identityprovider/tests/test_macaroon.py 2016-04-05 22:12:47 +0000
868+++ src/identityprovider/tests/test_macaroon.py 2016-05-05 12:45:29 +0000
869@@ -45,7 +45,7 @@
870 def setUp(self):
871 super(MacaroonRequestTestCase, self).setUp()
872
873- self.root_macaroon, self.macaroon_random_key = self.build_macaroon()
874+ self.root_macaroon, self.macaroon_random_key, _ = self.build_macaroon()
875 self.req = MacaroonRequest(self.root_macaroon.serialize())
876
877 def assertMacaroonsEqual(self, expected, observed):
878
879=== modified file 'src/identityprovider/tests/test_views_server.py'
880--- src/identityprovider/tests/test_views_server.py 2016-04-04 15:41:06 +0000
881+++ src/identityprovider/tests/test_views_server.py 2016-05-05 12:45:29 +0000
882@@ -227,7 +227,7 @@
883 self._test_auto_auth(sreg=['fullname'])
884
885 def test_handle_user_response_auto_auth_discharge_macaroon(self):
886- root_macaroon, macaroon_random_key = self.build_macaroon()
887+ root_macaroon, macaroon_random_key, _ = self.build_macaroon()
888 # Add padding to force a POST after signing. We don't know exactly
889 # how long the serialized discharge macaroon will be yet, but it
890 # will probably be at least 1024 bytes.
891@@ -1065,7 +1065,7 @@
892 # make sure rpconfig is set to auto authorize
893 OpenIDRPConfig.objects.create(
894 trust_root='http://localhost/', auto_authorize=True)
895- root_macaroon, macaroon_random_key = self.build_macaroon()
896+ root_macaroon, macaroon_random_key, _ = self.build_macaroon()
897 param_overrides = {
898 'openid.ns.macaroon': MACAROON_NS,
899 'openid.macaroon.root': root_macaroon.serialize(),
900@@ -1089,7 +1089,7 @@
901 root_macaroon, macaroon_random_key, [discharge_macaroon]))
902
903 def test_state_of_checkboxes_and_data_formats_macaroon(self):
904- root_macaroon, _ = self.build_macaroon()
905+ root_macaroon, _, _ = self.build_macaroon()
906 param_overrides = {
907 'openid.ns.macaroon': MACAROON_NS,
908 'openid.macaroon.root': root_macaroon.serialize(),
909@@ -1864,7 +1864,7 @@
910 if with_teams:
911 params['openid.lp.query_membership'] = 'ubuntu-team'
912 if with_macaroon:
913- root_macaroon, _ = self.build_macaroon()
914+ root_macaroon, _, _ = self.build_macaroon()
915 params['openid.ns.macaroon'] = MACAROON_NS
916 params['openid.macaroon.root'] = root_macaroon.serialize()
917 provider_url = get_provider_url(request)
918
919=== modified file 'src/identityprovider/tests/utils.py'
920--- src/identityprovider/tests/utils.py 2016-04-28 13:01:53 +0000
921+++ src/identityprovider/tests/utils.py 2016-05-05 12:45:29 +0000
922@@ -202,7 +202,7 @@
923 test_rsa_pub_key.encrypt(json.dumps(info), 32)[0])
924 root_macaroon.add_third_party_caveat(
925 service_location, random_key, info_encrypted)
926- return root_macaroon, macaroon_random_key
927+ return root_macaroon, macaroon_random_key, random_key
928
929
930 class SSOBaseTestCase(SSOBaseTestCaseMixin, TestCase):
931
932=== modified file 'src/identityprovider/views/server.py'
933--- src/identityprovider/views/server.py 2016-04-29 14:17:15 +0000
934+++ src/identityprovider/views/server.py 2016-05-05 12:45:29 +0000
935@@ -42,7 +42,7 @@
936 from openid.yadis.constants import YADIS_HEADER_NAME
937
938 from identityprovider import signed
939-from identityprovider.auth import build_discharge_macaroon
940+from identityprovider.auth import build_discharge_macaroon_from_root
941 from identityprovider.const import (
942 AX_DATA_FIELDS,
943 MACAROON_NS,
944@@ -769,7 +769,7 @@
945 rpconfig = utils.get_rpconfig(openid_request.trust_root)
946 form = MacaroonRequestForm(request, macaroon_request, rpconfig)
947 if form.data_approved_for_request:
948- discharge_macaroon = build_discharge_macaroon(
949+ discharge_macaroon = build_discharge_macaroon_from_root(
950 request.user, macaroon_request.root_macaroon_raw)
951 macaroon_response = MacaroonResponse.extractResponse(
952 macaroon_request, discharge_macaroon.serialize())