def test_fallback(self): key = bakery.generate_key() @urlmatch(path='.*/discharge/info') def discharge_info(url, request): return { 'status_code': 404, } @urlmatch(path='.*/publickey') def public_key(url, request): return { 'status_code': 200, 'content': { 'PublicKey': str(key.public_key), } } expectInfo = bakery.ThirdPartyInfo(public_key=key.public_key, version=bakery.VERSION_1) kr = httpbakery.ThirdPartyLocator(allow_insecure=True) with HTTMock(discharge_info): with HTTMock(public_key): info = kr.third_party_info('http://0.1.2.3/') self.assertEqual(info, expectInfo)
def test_fallback(self): key = bakery.generate_key() @urlmatch(path='.*/discharge/info') def discharge_info(url, request): return { 'status_code': 404, } @urlmatch(path='.*/publickey') def public_key(url, request): return { 'status_code': 200, 'content': { 'PublicKey': str(key.public_key), } } expectInfo = bakery.ThirdPartyInfo( public_key=key.public_key, version=bakery.VERSION_1 ) kr = httpbakery.ThirdPartyLocator(allow_insecure=True) with HTTMock(discharge_info): with HTTMock(public_key): info = kr.third_party_info('http://0.1.2.3/') self.assertEqual(info, expectInfo)
def __init__(self, name, auth, idm, locator): self._name = name self._store = _MacaroonStore(bakery.generate_key(), locator) self._checker = bakery.Checker(checker=test_checker(), authorizer=auth, identity_client=idm, macaroon_opstore=self._store)
def __init__(self, name, auth, idm, locator): self._name = name self._store = _MacaroonStore(bakery.generate_key(), locator) self._checker = bakery.Checker( checker=test_checker(), authorizer=auth, identity_client=idm, macaroon_opstore=self._store)
def test_discharge_all_local_discharge_version1(self): oc = common.new_bakery('ts', None) client_key = bakery.generate_key() m = oc.oven.macaroon(bakery.VERSION_1, common.ages, [ bakery.local_third_party_caveat( client_key.public_key, bakery.VERSION_1) ], [bakery.LOGIN_OP]) ms = bakery.discharge_all(m, no_discharge(self), client_key) oc.checker.auth([ms]).allow(common.test_context, [bakery.LOGIN_OP])
def _get_macaroon_oven_key(): """Return a private key to use for macaroon caveats signing. The key is read from the Config if found, otherwise a new one is created and saved. """ material = Config.objects.get_config("macaroon_private_key") if material: return bakery.PrivateKey.deserialize(material) key = bakery.generate_key() Config.objects.set_config("macaroon_private_key", key.serialize().decode("ascii")) return key
def is_authorized(*args, **kwargs): macaroon_bakery = bakery.Bakery( location="ubuntu.com/security", locator=httpbakery.ThirdPartyLocator(), identity_client=IdentityClient(), key=bakery.generate_key(), root_key_store=bakery.MemoryKeyStore( flask.current_app.config["SECRET_KEY"]), ) macaroons = httpbakery.extract_macaroons(flask.request.headers) auth_checker = macaroon_bakery.checker.auth(macaroons) launchpad = Launchpad.login_anonymously("ubuntu.com/security", "production", version="devel") try: auth_info = auth_checker.allow(checkers.AuthContext(), [bakery.LOGIN_OP]) except bakery._error.DischargeRequiredError: macaroon = macaroon_bakery.oven.macaroon( version=bakery.VERSION_2, expiry=datetime.utcnow() + timedelta(days=1), caveats=IDENTITY_CAVEATS, ops=[bakery.LOGIN_OP], ) content, headers = httpbakery.discharge_required_response( macaroon, "/", "cookie-suffix") return content, 401, headers username = auth_info.identity.username() lp_user = launchpad.people(username) authorized = False for team in AUTHORIZED_TEAMS: if lp_user in launchpad.people(team).members: authorized = True break if not authorized: return ( f"{username} is not in any of the authorized teams: " f"{str(AUTHORIZED_TEAMS)}", 401, ) # Validate authentication token return func(*args, **kwargs)
def __init__(self, locator=None): locator = httpbakery.ThirdPartyLocator() # generate a new keypair for encrypting third party caveats # it's safe to use a new keypair every time the server starts # as it's used only for encrypting the third party caveats # for sending them to be discharged. The private key doesn't need # to survive across restarts. key = bakery.generate_key() location = 'localhost:8000' root_key = 'private-key' self._bakery = bakery.Bakery( location=location, locator=locator, identity_client=IdentityClient(), key=key, root_key_store=bakery.MemoryKeyStore(root_key))
def test_third_party_discharge_macaroon_wrong_root_key_and_third_party_caveat( self): root_keys = bakery.MemoryKeyStore() ts = bakery.Bakery( key=bakery.generate_key(), checker=common.test_checker(), root_key_store=root_keys, identity_client=common.OneIdentity(), ) locator = bakery.ThirdPartyStore() bs = common.new_bakery('bs-loc', locator) # ts creates a macaroon with a third party caveat addressed to bs. ts_macaroon = ts.oven.macaroon(bakery.LATEST_VERSION, common.ages, None, [bakery.LOGIN_OP]) ts_macaroon.add_caveat( checkers.Caveat(location='bs-loc', condition='true'), ts.oven.key, locator, ) def get_discharge(cav, payload): return bakery.discharge( common.test_context, cav.caveat_id_bytes, payload, bs.oven.key, common.ThirdPartyStrcmpChecker('true'), bs.oven.locator, ) d = bakery.discharge_all(ts_macaroon, get_discharge) # The authorization should succeed at first. ts.checker.auth([d]).allow(common.test_context, [bakery.LOGIN_OP]) # Corrupt the root key and try again. # We should get a DischargeRequiredError because the verification has failed. root_keys._key = os.urandom(24) with self.assertRaises(bakery.PermissionDenied) as err: ts.checker.auth([d]).allow(common.test_context, [bakery.LOGIN_OP]) self.assertEqual( str(err.exception), 'verification failed: Decryption failed. Ciphertext failed verification' )
def new_bakery(location, locator=None): # Returns a new Bakery instance using a new # key pair, and registers the key with the given locator if provided. # # It uses test_checker to check first party caveats. key = bakery.generate_key() if locator is not None: locator.add_info( location, bakery.ThirdPartyInfo(public_key=key.public_key, version=bakery.LATEST_VERSION)) return bakery.Bakery( key=key, checker=test_checker(), location=location, identity_client=OneIdentity(), locator=locator, )
def new_bakery(location, locator=None): # Returns a new Bakery instance using a new # key pair, and registers the key with the given locator if provided. # # It uses test_checker to check first party caveats. key = bakery.generate_key() if locator is not None: locator.add_info(location, bakery.ThirdPartyInfo( public_key=key.public_key, version=bakery.LATEST_VERSION)) return bakery.Bakery( key=key, checker=test_checker(), location=location, identity_client=OneIdentity(), locator=locator, )
def new_bakery(location, locator, checker): '''Return a new bakery instance. @param location Location of the bakery {str}. @param locator Locator for third parties {ThirdPartyLocator or None} @param checker Caveat checker {FirstPartyCaveatChecker or None} @return {Bakery} ''' if checker is None: c = checkers.Checker() c.namespace().register('testns', '') c.register('is', 'testns', check_is_something) checker = c key = bakery.generate_key() return bakery.Bakery( location=location, locator=locator, key=key, checker=checker, )
def test_third_party_discharge_macaroon_wrong_root_key_and_third_party_caveat(self): root_keys = bakery.MemoryKeyStore() ts = bakery.Bakery( key=bakery.generate_key(), checker=common.test_checker(), root_key_store=root_keys, identity_client=common.OneIdentity(), ) locator = bakery.ThirdPartyStore() bs = common.new_bakery('bs-loc', locator) # ts creates a macaroon with a third party caveat addressed to bs. ts_macaroon = ts.oven.macaroon(bakery.LATEST_VERSION, common.ages, None, [bakery.LOGIN_OP]) ts_macaroon.add_caveat( checkers.Caveat(location='bs-loc', condition='true'), ts.oven.key, locator, ) def get_discharge(cav, payload): return bakery.discharge( common.test_context, cav.caveat_id_bytes, payload, bs.oven.key, common.ThirdPartyStrcmpChecker('true'), bs.oven.locator, ) d = bakery.discharge_all(ts_macaroon, get_discharge) # The authorization should succeed at first. ts.checker.auth([d]).allow(common.test_context, [bakery.LOGIN_OP]) # Corrupt the root key and try again. # We should get a DischargeRequiredError because the verification has failed. root_keys._key = os.urandom(24) with self.assertRaises(bakery.PermissionDenied) as err: ts.checker.auth([d]).allow(common.test_context, [bakery.LOGIN_OP]) self.assertEqual(str(err.exception), 'verification failed: Decryption failed. Ciphertext failed verification')
def test_auth_with_third_party_caveats(self): locator = _DischargerLocator() ids = _IdService('ids', locator, self) # We make an authorizer that requires a third party discharge # when authorizing. def authorize_with_tp_discharge(ctx, id, op): if (id is not None and id.id() == 'bob' and op == bakery.Op(entity='something', action='read')): return True, [ checkers.Caveat(condition='question', location='other third party') ] return False, None auth = bakery.AuthorizerFunc(authorize_with_tp_discharge) ts = _Service('myservice', auth, ids, locator) class _LocalDischargeChecker(bakery.ThirdPartyCaveatChecker): def check_third_party_caveat(_, ctx, info): if info.condition != 'question': raise ValueError('third party condition not recognized') self._discharges.append( _DischargeRecord(location='other third party', user=ctx.get(_DISCHARGE_USER_KEY))) return [] locator['other third party'] = _Discharger( key=bakery.generate_key(), checker=_LocalDischargeChecker(), locator=locator, ) client = _Client(locator) ctx = test_context.with_value(_DISCHARGE_USER_KEY, 'bob') client.do(ctx, ts, [bakery.Op(entity='something', action='read')]) self.assertEqual(self._discharges, [ _DischargeRecord(location='ids', user='******'), _DischargeRecord(location='other third party', user='******') ])
def test_auth_with_third_party_caveats(self): locator = _DischargerLocator() ids = _IdService('ids', locator, self) # We make an authorizer that requires a third party discharge # when authorizing. def authorize_with_tp_discharge(ctx, id, op): if (id is not None and id.id() == 'bob' and op == bakery.Op(entity='something', action='read')): return True, [checkers.Caveat(condition='question', location='other third party')] return False, None auth = bakery.AuthorizerFunc(authorize_with_tp_discharge) ts = _Service('myservice', auth, ids, locator) class _LocalDischargeChecker(bakery.ThirdPartyCaveatChecker): def check_third_party_caveat(_, ctx, info): if info.condition != 'question': raise ValueError('third party condition not recognized') self._discharges.append(_DischargeRecord( location='other third party', user=ctx.get(_DISCHARGE_USER_KEY) )) return [] locator['other third party'] = _Discharger( key=bakery.generate_key(), checker=_LocalDischargeChecker(), locator=locator, ) client = _Client(locator) ctx = test_context.with_value(_DISCHARGE_USER_KEY, 'bob') client.do(ctx, ts, [bakery.Op(entity='something', action='read')]) self.assertEqual(self._discharges, [ _DischargeRecord(location='ids', user='******'), _DischargeRecord(location='other third party', user='******') ])
async def test_macaroon_auth_with_bad_key(event_loop): auth_info, username = agent_auth_info() # Use a random key rather than the correct key. auth_info = auth_info._replace(key=bakery.generate_key()) # Create a bakery client can do agent authentication. client = httpbakery.Client( key=auth_info.key, interaction_methods=[agent.AgentInteractor(auth_info)], ) async with base.CleanModel(bakery_client=client) as m: async with await m.get_controller() as c: await c.grant_model(username, m.info.uuid, 'admin') try: async with Model( jujudata=NoAccountsJujuData(m._connector.jujudata), bakery_client=client, ): pytest.fail('Should not be able to connect with invalid key') except httpbakery.BakeryException: # We're expecting this because we're using the # wrong key. pass
def _register_service(client, service, auth_details): key = generate_key() response = client.register_service(service['$uri'], str(key.public_key)) auth_details.url = response['url'] auth_details.user = response['username'] auth_details.key = str(key)
def test_agent_login(self): discharge_key = bakery.generate_key() class _DischargerLocator(bakery.ThirdPartyLocator): def third_party_info(self, loc): if loc == 'http://0.3.2.1': return bakery.ThirdPartyInfo( public_key=discharge_key.public_key, version=bakery.LATEST_VERSION, ) d = _DischargerLocator() server_key = bakery.generate_key() server_bakery = bakery.Bakery(key=server_key, locator=d) @urlmatch(path='.*/here') def server_get(url, request): ctx = checkers.AuthContext() test_ops = [bakery.Op(entity='test-op', action='read')] auth_checker = server_bakery.checker.auth( httpbakery.extract_macaroons(request.headers)) try: auth_checker.allow(ctx, test_ops) resp = response(status_code=200, content='done') except bakery.PermissionDenied: caveats = [ checkers.Caveat(location='http://0.3.2.1', condition='is-ok') ] m = server_bakery.oven.macaroon( version=bakery.LATEST_VERSION, expiry=datetime.utcnow() + timedelta(days=1), caveats=caveats, ops=test_ops) content, headers = httpbakery.discharge_required_response( m, '/', 'test', 'message') resp = response(status_code=401, content=content, headers=headers) return request.hooks['response'][0](resp) @urlmatch(path='.*/discharge') def discharge(url, request): qs = parse_qs(request.body) if qs.get('token64') is None: return response( status_code=401, content={ 'Code': httpbakery.ERR_INTERACTION_REQUIRED, 'Message': 'interaction required', 'Info': { 'InteractionMethods': { 'agent': {'login-url': '/login'}, }, }, }, headers={'Content-Type': 'application/json'}) else: qs = parse_qs(request.body) content = {q: qs[q][0] for q in qs} m = httpbakery.discharge(checkers.AuthContext(), content, discharge_key, None, alwaysOK3rd) return { 'status_code': 200, 'content': { 'Macaroon': m.to_dict() } } auth_info = agent.load_auth_info(self.agent_filename) @urlmatch(path='.*/login') def login(url, request): qs = parse_qs(urlparse(request.url).query) self.assertEqual(request.method, 'GET') self.assertEqual( qs, {'username': ['test-user'], 'public-key': [PUBLIC_KEY]}) b = bakery.Bakery(key=discharge_key) m = b.oven.macaroon( version=bakery.LATEST_VERSION, expiry=datetime.utcnow() + timedelta(days=1), caveats=[bakery.local_third_party_caveat( PUBLIC_KEY, version=httpbakery.request_version(request.headers))], ops=[bakery.Op(entity='agent', action='login')]) return { 'status_code': 200, 'content': { 'macaroon': m.to_dict() } } with HTTMock(server_get), \ HTTMock(discharge), \ HTTMock(login): client = httpbakery.Client(interaction_methods=[ agent.AgentInteractor(auth_info), ]) resp = requests.get( 'http://0.1.2.3/here', cookies=client.cookies, auth=client.auth()) self.assertEqual(resp.content, b'done')
def test_agent_legacy(self): discharge_key = bakery.generate_key() class _DischargerLocator(bakery.ThirdPartyLocator): def third_party_info(self, loc): if loc == 'http://0.3.2.1': return bakery.ThirdPartyInfo( public_key=discharge_key.public_key, version=bakery.LATEST_VERSION, ) d = _DischargerLocator() server_key = bakery.generate_key() server_bakery = bakery.Bakery(key=server_key, locator=d) @urlmatch(path='.*/here') def server_get(url, request): ctx = checkers.AuthContext() test_ops = [bakery.Op(entity='test-op', action='read')] auth_checker = server_bakery.checker.auth( httpbakery.extract_macaroons(request.headers)) try: auth_checker.allow(ctx, test_ops) resp = response(status_code=200, content='done') except bakery.PermissionDenied: caveats = [ checkers.Caveat(location='http://0.3.2.1', condition='is-ok') ] m = server_bakery.oven.macaroon( version=bakery.LATEST_VERSION, expiry=datetime.utcnow() + timedelta(days=1), caveats=caveats, ops=test_ops) content, headers = httpbakery.discharge_required_response( m, '/', 'test', 'message') resp = response( status_code=401, content=content, headers=headers, ) return request.hooks['response'][0](resp) class InfoStorage: info = None @urlmatch(path='.*/discharge') def discharge(url, request): qs = parse_qs(request.body) if qs.get('caveat64') is not None: content = {q: qs[q][0] for q in qs} class InteractionRequiredError(Exception): def __init__(self, error): self.error = error class CheckerInError(bakery.ThirdPartyCaveatChecker): def check_third_party_caveat(self, ctx, info): InfoStorage.info = info raise InteractionRequiredError( httpbakery.Error( code=httpbakery.ERR_INTERACTION_REQUIRED, version=httpbakery.request_version( request.headers), message='interaction required', info=httpbakery.ErrorInfo( wait_url='http://0.3.2.1/wait?' 'dischargeid=1', visit_url='http://0.3.2.1/visit?' 'dischargeid=1' ), ), ) try: httpbakery.discharge( checkers.AuthContext(), content, discharge_key, None, CheckerInError()) except InteractionRequiredError as exc: return response( status_code=401, content={ 'Code': exc.error.code, 'Message': exc.error.message, 'Info': { 'WaitURL': exc.error.info.wait_url, 'VisitURL': exc.error.info.visit_url, }, }, headers={'Content-Type': 'application/json'}) key = bakery.generate_key() @urlmatch(path='.*/visit') def visit(url, request): if request.headers.get('Accept') == 'application/json': return { 'status_code': 200, 'content': { 'agent': '/agent-visit', } } raise Exception('unexpected call to visit without Accept header') @urlmatch(path='.*/agent-visit') def agent_visit(url, request): if request.method != "POST": raise Exception('unexpected method') log.info('agent_visit url {}'.format(url)) body = json.loads(request.body.decode('utf-8')) if body['username'] != 'test-user': raise Exception('unexpected username in body {!r}'.format(request.body)) public_key = bakery.PublicKey.deserialize(body['public_key']) ms = httpbakery.extract_macaroons(request.headers) if len(ms) == 0: b = bakery.Bakery(key=discharge_key) m = b.oven.macaroon( version=bakery.LATEST_VERSION, expiry=datetime.utcnow() + timedelta(days=1), caveats=[bakery.local_third_party_caveat( public_key, version=httpbakery.request_version(request.headers))], ops=[bakery.Op(entity='agent', action='login')]) content, headers = httpbakery.discharge_required_response( m, '/', 'test', 'message') resp = response(status_code=401, content=content, headers=headers) return request.hooks['response'][0](resp) return { 'status_code': 200, 'content': { 'agent_login': True } } @urlmatch(path='.*/wait$') def wait(url, request): class EmptyChecker(bakery.ThirdPartyCaveatChecker): def check_third_party_caveat(self, ctx, info): return [] if InfoStorage.info is None: self.fail('visit url has not been visited') m = bakery.discharge( checkers.AuthContext(), InfoStorage.info.id, InfoStorage.info.caveat, discharge_key, EmptyChecker(), _DischargerLocator(), ) return { 'status_code': 200, 'content': { 'Macaroon': m.to_dict() } } with HTTMock(server_get), \ HTTMock(discharge), \ HTTMock(visit), \ HTTMock(wait), \ HTTMock(agent_visit): client = httpbakery.Client(interaction_methods=[ agent.AgentInteractor( agent.AuthInfo( key=key, agents=[agent.Agent(username='******', url=u'http://0.3.2.1')], ), ), ]) resp = requests.get( 'http://0.1.2.3/here', cookies=client.cookies, auth=client.auth(), ) self.assertEqual(resp.content, b'done')
def __init__(self): self.key = bakery.generate_key()
def __init__(self, dischargers): self._key = bakery.generate_key() self._macaroons = {} self._dischargers = dischargers
def test_agent_login(self): discharge_key = bakery.generate_key() class _DischargerLocator(bakery.ThirdPartyLocator): def third_party_info(self, loc): if loc == 'http://0.3.2.1': return bakery.ThirdPartyInfo( public_key=discharge_key.public_key, version=bakery.LATEST_VERSION, ) d = _DischargerLocator() server_key = bakery.generate_key() server_bakery = bakery.Bakery(key=server_key, locator=d) @urlmatch(path='.*/here') def server_get(url, request): ctx = checkers.AuthContext() test_ops = [bakery.Op(entity='test-op', action='read')] auth_checker = server_bakery.checker.auth( httpbakery.extract_macaroons(request.headers)) try: auth_checker.allow(ctx, test_ops) resp = response(status_code=200, content='done') except bakery.PermissionDenied: caveats = [ checkers.Caveat(location='http://0.3.2.1', condition='is-ok') ] m = server_bakery.oven.macaroon(version=bakery.LATEST_VERSION, expiry=datetime.utcnow() + timedelta(days=1), caveats=caveats, ops=test_ops) content, headers = httpbakery.discharge_required_response( m, '/', 'test', 'message') resp = response(status_code=401, content=content, headers=headers) return request.hooks['response'][0](resp) @urlmatch(path='.*/discharge') def discharge(url, request): qs = parse_qs(request.body) if qs.get('token64') is None: return response(status_code=401, content={ 'Code': httpbakery.ERR_INTERACTION_REQUIRED, 'Message': 'interaction required', 'Info': { 'InteractionMethods': { 'agent': { 'login-url': '/login' }, }, }, }, headers={'Content-Type': 'application/json'}) else: qs = parse_qs(request.body) content = {q: qs[q][0] for q in qs} m = httpbakery.discharge(checkers.AuthContext(), content, discharge_key, None, alwaysOK3rd) return { 'status_code': 200, 'content': { 'Macaroon': m.to_dict() } } auth_info = agent.load_auth_info(self.agent_filename) @urlmatch(path='.*/login') def login(url, request): b = bakery.Bakery(key=discharge_key) m = b.oven.macaroon( version=bakery.LATEST_VERSION, expiry=datetime.utcnow() + timedelta(days=1), caveats=[ bakery.local_third_party_caveat( auth_info.key.public_key, version=httpbakery.request_version(request.headers)) ], ops=[bakery.Op(entity='agent', action='login')]) return {'status_code': 200, 'content': {'macaroon': m.to_dict()}} with HTTMock(server_get), \ HTTMock(discharge), \ HTTMock(login): client = httpbakery.Client(interaction_methods=[ agent.AgentInteractor(auth_info), ]) resp = requests.get('http://0.1.2.3/here', cookies=client.cookies, auth=client.auth()) self.assertEquals(resp.content, b'done')
def setUp(self): self.fp_key = bakery.generate_key() self.tp_key = bakery.generate_key()
def __init__(self, location, locator, test_class): self._location = location self._test = test_class key = bakery.generate_key() self._discharger = _Discharger(key=key, checker=self, locator=locator) locator[location] = self._discharger
def test_agent_legacy(self): discharge_key = bakery.generate_key() class _DischargerLocator(bakery.ThirdPartyLocator): def third_party_info(self, loc): if loc == 'http://0.3.2.1': return bakery.ThirdPartyInfo( public_key=discharge_key.public_key, version=bakery.LATEST_VERSION, ) d = _DischargerLocator() server_key = bakery.generate_key() server_bakery = bakery.Bakery(key=server_key, locator=d) @urlmatch(path='.*/here') def server_get(url, request): ctx = checkers.AuthContext() test_ops = [bakery.Op(entity='test-op', action='read')] auth_checker = server_bakery.checker.auth( httpbakery.extract_macaroons(request.headers)) try: auth_checker.allow(ctx, test_ops) resp = response(status_code=200, content='done') except bakery.PermissionDenied: caveats = [ checkers.Caveat(location='http://0.3.2.1', condition='is-ok') ] m = server_bakery.oven.macaroon(version=bakery.LATEST_VERSION, expiry=datetime.utcnow() + timedelta(days=1), caveats=caveats, ops=test_ops) content, headers = httpbakery.discharge_required_response( m, '/', 'test', 'message') resp = response( status_code=401, content=content, headers=headers, ) return request.hooks['response'][0](resp) class InfoStorage: info = None @urlmatch(path='.*/discharge') def discharge(url, request): qs = parse_qs(request.body) if qs.get('caveat64') is not None: content = {q: qs[q][0] for q in qs} class InteractionRequiredError(Exception): def __init__(self, error): self.error = error class CheckerInError(bakery.ThirdPartyCaveatChecker): def check_third_party_caveat(self, ctx, info): InfoStorage.info = info raise InteractionRequiredError( httpbakery.Error( code=httpbakery.ERR_INTERACTION_REQUIRED, version=httpbakery.request_version( request.headers), message='interaction required', info=httpbakery.ErrorInfo( wait_url='http://0.3.2.1/wait?' 'dischargeid=1', visit_url='http://0.3.2.1/visit?' 'dischargeid=1'), ), ) try: httpbakery.discharge(checkers.AuthContext(), content, discharge_key, None, CheckerInError()) except InteractionRequiredError as exc: return response( status_code=401, content={ 'Code': exc.error.code, 'Message': exc.error.message, 'Info': { 'WaitURL': exc.error.info.wait_url, 'VisitURL': exc.error.info.visit_url, }, }, headers={'Content-Type': 'application/json'}) key = bakery.generate_key() @urlmatch(path='.*/visit') def visit(url, request): if request.headers.get('Accept') == 'application/json': return { 'status_code': 200, 'content': { 'agent': '/agent-visit', } } raise Exception('unexpected call to visit without Accept header') @urlmatch(path='.*/agent-visit') def agent_visit(url, request): if request.method != "POST": raise Exception('unexpected method') log.info('agent_visit url {}'.format(url)) body = json.loads(request.body.decode('utf-8')) if body['username'] != 'test-user': raise Exception('unexpected username in body {!r}'.format( request.body)) public_key = bakery.PublicKey.deserialize(body['public_key']) ms = httpbakery.extract_macaroons(request.headers) if len(ms) == 0: b = bakery.Bakery(key=discharge_key) m = b.oven.macaroon( version=bakery.LATEST_VERSION, expiry=datetime.utcnow() + timedelta(days=1), caveats=[ bakery.local_third_party_caveat( public_key, version=httpbakery.request_version( request.headers)) ], ops=[bakery.Op(entity='agent', action='login')]) content, headers = httpbakery.discharge_required_response( m, '/', 'test', 'message') resp = response(status_code=401, content=content, headers=headers) return request.hooks['response'][0](resp) return {'status_code': 200, 'content': {'agent_login': True}} @urlmatch(path='.*/wait$') def wait(url, request): class EmptyChecker(bakery.ThirdPartyCaveatChecker): def check_third_party_caveat(self, ctx, info): return [] if InfoStorage.info is None: self.fail('visit url has not been visited') m = bakery.discharge( checkers.AuthContext(), InfoStorage.info.id, InfoStorage.info.caveat, discharge_key, EmptyChecker(), _DischargerLocator(), ) return {'status_code': 200, 'content': {'Macaroon': m.to_dict()}} with HTTMock(server_get), \ HTTMock(discharge), \ HTTMock(visit), \ HTTMock(wait), \ HTTMock(agent_visit): client = httpbakery.Client(interaction_methods=[ agent.AgentInteractor( agent.AuthInfo( key=key, agents=[ agent.Agent(username='******', url=u'http://0.3.2.1') ], ), ), ]) resp = requests.get( 'http://0.1.2.3/here', cookies=client.cookies, auth=client.auth(), ) self.assertEquals(resp.content, b'done')