class TestCachedClient(unittest.TestCase): server_url = TEST_SERVER_URL def setUp(self): self.client = Client(server_url=self.server_url) self.body = ('{"user": "******", "scope": ["profile"],' '"client_id": "abc"}') responses.add(responses.POST, 'https://server/v1/verify', body=self.body, content_type='application/json') add_jwks_response() def test_has_default_cache(self): self.assertIsNotNone(self.client.cache) self.assertEqual(self.client.cache.ttl, 300) def test_can_change_default_cache(self): cache = MemoryCache(0.01) self.client = Client(cache=cache) self.assertEqual(self.client.cache, cache) self.assertEqual(self.client.cache.ttl, 0.01) def test_can_deactivate_cache(self): self.client = Client(cache=None) self.assertIsNone(self.client.cache) @responses.activate def test_client_verify_code_is_cached(self): with mock.patch.object(self.client.cache, 'set') as mocked_set: with mock.patch.object(self.client.cache, 'get', return_value=None): # First call verification = self.client.verify_token(token='abc') self.assertTrue(mocked_set.called) self.assertDictEqual(verification, json.loads(self.body)) @responses.activate def test_client_verify_code_cached_value_is_used(self): with mock.patch.object(self.client.cache, 'set') as mocked_set: with mock.patch.object(self.client.cache, 'get', return_value=self.body): # Second call verification = self.client.verify_token(token='abc') self.assertFalse(mocked_set.called) self.assertDictEqual(verification, json.loads(self.body)) @responses.activate def test_client_verify_code_cached_value_is_not_used_if_no_cache(self): self.client = Client(cache=None, server_url=self.server_url) # First call verification = self.client.verify_token(token='abc') self.assertDictEqual(verification, json.loads(self.body)) # Second call verification = self.client.verify_token(token='abc') self.assertDictEqual(verification, json.loads(self.body))
class TestCachedClient(unittest.TestCase): server_url = TEST_SERVER_URL def setUp(self): self.client = Client(server_url=self.server_url) self.body = ('{"user": "******", "scope": ["profile"],' '"client_id": "abc"}') responses.add(responses.POST, 'https://server/v1/verify', body=self.body, content_type='application/json') def test_has_default_cache(self): self.assertIsNotNone(self.client.cache) self.assertEqual(self.client.cache.ttl, 300) def test_can_change_default_cache(self): cache = MemoryCache(0.01) self.client = Client(cache=cache) self.assertEqual(self.client.cache, cache) self.assertEqual(self.client.cache.ttl, 0.01) def test_can_deactivate_cache(self): self.client = Client(cache=None) self.assertIsNone(self.client.cache) @responses.activate def test_client_verify_code_is_cached(self): with mock.patch.object(self.client.cache, 'set') as mocked_set: with mock.patch.object(self.client.cache, 'get', return_value=None): # First call verification = self.client.verify_token(token='abc') self.assertTrue(mocked_set.called) self.assertDictEqual(verification, json.loads(self.body)) @responses.activate def test_client_verify_code_cached_value_is_used(self): with mock.patch.object(self.client.cache, 'set') as mocked_set: with mock.patch.object(self.client.cache, 'get', return_value=self.body): # Second call verification = self.client.verify_token(token='abc') self.assertFalse(mocked_set.called) self.assertDictEqual(verification, json.loads(self.body)) @responses.activate def test_client_verify_code_cached_value_is_not_used_if_no_cache(self): self.client = Client(cache=None, server_url=self.server_url) # First call verification = self.client.verify_token(token='abc') self.assertDictEqual(verification, json.loads(self.body)) # Second call verification = self.client.verify_token(token='abc') self.assertDictEqual(verification, json.loads(self.body))
def _get_credentials(self, request): authorization = request.headers.get('Authorization', '') try: authmeth, auth = authorization.split(' ', 1) except ValueError: return None if authmeth.lower() != 'bearer': return None # Use PyFxa defaults if not specified server_url = fxa_conf(request, 'oauth_uri') scope = aslist(fxa_conf(request, 'required_scope')) auth_cache = self._get_cache(request) auth_client = OAuthClient(server_url=server_url, cache=auth_cache) try: profile = auth_client.verify_token(token=auth, scope=scope) user_id = profile['user'] except fxa_errors.OutOfProtocolError as e: logger.error(e) raise httpexceptions.HTTPServiceUnavailable() except (fxa_errors.InProtocolError, fxa_errors.TrustError) as e: logger.info(e) return None return user_id
def _get_credentials(self, request): authorization = request.headers.get('Authorization', '') settings = request.registry.settings try: authmeth, auth = authorization.split(' ', 1) assert authmeth.lower() == 'bearer' except (AssertionError, ValueError): return None # Trace authentication type. request.auth_type = 'FxA' # Use PyFxa defaults if not specified server_url = settings['fxa-oauth.oauth_uri'] scope = settings['fxa-oauth.scope'] auth_client = OAuthClient(server_url=server_url, cache=self.cache) try: profile = auth_client.verify_token(token=auth, scope=scope) user_id = profile['user'] except fxa_errors.OutOfProtocolError: raise httpexceptions.HTTPServiceUnavailable() except (fxa_errors.InProtocolError, fxa_errors.TrustError): return None return 'fxa_%s' % user_id
class TestAuthClientVerifyCode(unittest.TestCase): server_url = TEST_SERVER_URL @responses.activate def setUp(self): self.client = Client(server_url=self.server_url) body = '{"user": "******", "scope": ["profile"], "client_id": "abc"}' responses.add(responses.POST, 'https://server/v1/verify', body=body, content_type='application/json') self.verification = self.client.verify_token(token='abc') self.response = responses.calls[0] def test_reaches_server_on_verify_url(self): self.assertEqual(self.response.request.url, 'https://server/v1/verify') def test_posts_token_to_server(self): body = json.loads(_decoded(self.response.request.body)) expected = { "token": "abc", } self.assertEqual(body, expected) def test_returns_response_given_by_server(self): expected = { "user": "******", "scope": ["profile"], "client_id": "abc" } self.assertEqual(self.verification, expected) @responses.activate def test_raises_error_if_some_attributes_are_not_returned(self): responses.add(responses.POST, 'https://server/v1/verify', body='{"missing": "attributes"}', content_type='application/json') self.assertRaises(fxa.errors.OutOfProtocolError, self.client.verify_token, token='1234') @responses.activate def test_raises_error_if_scopes_do_not_match(self): body = '{"user": "******", "scope": ["files"], "client_id": "abc"}' responses.add(responses.POST, 'https://server/v1/verify', body=body, content_type='application/json') self.assertRaises(fxa.errors.ScopeMismatchError, self.client.verify_token, token='1234', scope='readinglist')
class TestAuthClientVerifyCode(unittest.TestCase): server_url = TEST_SERVER_URL @responses.activate def setUp(self): self.client = Client(server_url=self.server_url) body = '{"user": "******", "scope": ["profile"], "client_id": "abc"}' responses.add(responses.POST, 'https://server/v1/verify', body=body, content_type='application/json') add_jwks_response() self.verification = self.client.verify_token(token='abc') self.response = responses.calls[1] def test_reaches_server_on_verify_url(self): self.assertEqual(self.response.request.url, 'https://server/v1/verify') def test_posts_token_to_server(self): body = json.loads(_decoded(self.response.request.body)) expected = { "token": "abc", } self.assertEqual(body, expected) def test_returns_response_given_by_server(self): expected = {"user": "******", "scope": ["profile"], "client_id": "abc"} self.assertEqual(self.verification, expected) @responses.activate def test_raises_error_if_some_attributes_are_not_returned(self): responses.add(responses.POST, 'https://server/v1/verify', body='{"missing": "attributes"}', content_type='application/json') add_jwks_response() self.assertRaises(fxa.errors.OutOfProtocolError, self.client.verify_token, token='1234') @responses.activate def test_raises_error_if_scopes_do_not_match(self): body = '{"user": "******", "scope": ["files"], "client_id": "abc"}' responses.add(responses.POST, 'https://server/v1/verify', body=body, content_type='application/json') add_jwks_response() self.assertRaises(fxa.errors.ScopeMismatchError, self.client.verify_token, token='1234', scope='readinglist')
class FxaOAuthClient: def __init__(self, server_url=None, jwks=None): self._client = Client(server_url=server_url, jwks=jwks) def verify_token(self, token): try: token_data = self._client.verify_token(token, DEFAULT_OAUTH_SCOPE) # Serialize the data to make it easier to parse in Rust return json.dumps(token_data) except (ClientError, TrustError): return None
def _verify_token(self, token, request): """Verify the token extracted from the Authorization header. This method stores the result in two locations to avoid hitting the auth remote server as much as possible: - on the request object, in case the Pyramid authentication methods like `effective_principals()` or `authenticated_userid()` are called several times during the request cycle; - in the cache backend, to reuse validated token from one request to another (during ``cache_ttl_seconds`` seconds.) """ # First check if this request was already verified. # `request.bound_data` is an attribute provided by Kinto to store # some data that is shared among sub-requests (e.g. default bucket # or batch requests) if REIFY_KEY not in request.bound_data: # Use PyFxa defaults if not specified server_url = fxa_conf(request, 'oauth_uri') auth_cache = self._get_cache(request) auth_client = OAuthClient(server_url=server_url, cache=auth_cache) user_id = None client_name = None for scope, client in request.registry._fxa_oauth_scope_routing.items(): try: profile = auth_client.verify_token(token=token, scope=aslist(scope)) user_id = profile['user'] scope = profile['scope'] client_name = client # Make sure the bearer token scopes don't match multiple configs. routing_scopes = request.registry._fxa_oauth_scope_routing intersecting_scopes = [x for x in routing_scopes.keys() if x and set(x.split()).issubset(set(scope))] if len(intersecting_scopes) > 1: logger.warn("Invalid FxA token: {} matches multiple config" % scope) return None, None break except fxa_errors.OutOfProtocolError as e: logger.exception("Protocol error") raise httpexceptions.HTTPServiceUnavailable() except (fxa_errors.InProtocolError, fxa_errors.TrustError) as e: logger.debug("Invalid FxA token: %s" % e) # Save for next call. request.bound_data[REIFY_KEY] = (user_id, client_name) return request.bound_data[REIFY_KEY]
def _verify_token(self, token, request): """Verify the token extracted from the Authorization header. This method stores the result in two locations to avoid hitting the auth remote server as much as possible: - on the request object, in case the Pyramid authentication methods like `effective_principals()` or `authenticated_userid()` are called several times during the request cycle; - in the cache backend, to reuse validated token from one request to another (during ``cache_ttl_seconds`` seconds.) """ # First check if this request was already verified. # `request.bound_data` is an attribute provided by Kinto to store # some data that is shared among sub-requests (e.g. default bucket # or batch requests) key = 'fxa_verified_token' if key in request.bound_data: return request.bound_data[key] # Use PyFxa defaults if not specified server_url = fxa_conf(request, 'oauth_uri') scope = aslist(fxa_conf(request, 'required_scope')) auth_cache = self._get_cache(request) auth_client = OAuthClient(server_url=server_url, cache=auth_cache) try: profile = auth_client.verify_token(token=token, scope=scope) user_id = profile['user'] except fxa_errors.OutOfProtocolError as e: logger.exception("Protocol error") raise httpexceptions.HTTPServiceUnavailable() except (fxa_errors.InProtocolError, fxa_errors.TrustError) as e: logger.debug("Invalid FxA token: %s" % e) user_id = None # Save for next call. request.bound_data[key] = user_id return user_id
def verify_oauth_token(token, scope='profile', oauth_server=OAUTH_SERVER, client_id=CLIENT_ID): oauth_client = OAuthClient(client_id, server_url=oauth_server) return oauth_client.verify_token(token, scope=scope)
class TestJwtToken(unittest.TestCase): server_url = TEST_SERVER_URL def callback(self, request): if self.verify_will_succeed: return (200, {}, self.body) return (500, {}, '{}') def _make_jwt(self, payload, key, alg="RS256", header={"typ": "at+jwt"}): header = header.copy( ) # So we don't accidentally mutate argument in-place return six.ensure_text(jwt.encode(payload, key, alg, header)) def setUp(self): self.client = Client(server_url=self.server_url) self.body = ('{"user": "******", "scope": ["profile"],' '"client_id": "abc"}') responses.add_callback(responses.POST, 'https://server/v1/verify', callback=self.callback, content_type='application/json') add_jwks_response() self.verify_will_succeed = True def get_file_contents(self, filename): return jwt.algorithms.RSAAlgorithm.from_jwk( open(os.path.join(os.path.dirname(__file__), filename)).read()) @responses.activate def test_good_jwt_token(self): private_key = self.get_file_contents("private-key.json") token = self._make_jwt( { "sub": "asdf", "scope": "qwer", "client_id": "foo" }, private_key) self.client.verify_token(token) for c in responses.calls: if c.request.url == 'https://server/v1/verify': raise Exception("testing with a good token should not have \ resulted in a call to /verify, but it did.") @responses.activate def test_wrong_key_jwt_token(self): self.verify_will_succeed = False bad_key = self.get_file_contents("bad-key.json") token = self._make_jwt({}, bad_key) with self.assertRaises(fxa.errors.TrustError): self.client.verify_token(token) for c in responses.calls: if c.request.url == 'https://server/v1/verify': raise Exception( "testing with a well-formed token with invalid signature \ should not have resulted in a call to /verify, but it did." ) @responses.activate def test_expired_jwt_token(self): private_key = self.get_file_contents("private-key.json") token = self._make_jwt({"qwer": "asdf", "exp": 0}, private_key) with self.assertRaises(fxa.errors.TrustError): self.client.verify_token(token) @responses.activate def test_garbage_jwt_token(self): self.verify_will_succeed = False with self.assertRaises(fxa.errors.ServerError): self.client.verify_token("garbage") for c in responses.calls: if c.request.url == 'https://server/v1/verify': break else: raise Exception("testing with a garbage token should have \ called /verify, but it did not.")