From 8744cb674165542df8f711fbabf129c5005e997a Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Thu, 3 Oct 2013 17:24:19 +1000 Subject: [PATCH] Verify token binding in auth_token middleware The server side has had token bind checking implemented for a while. Now that it is possible to generate bound tokens auth_token middleware should be capable of verifying them. Change-Id: I4f9c5855ab3102333b0738864c506e2501bf9c7e --- keystoneclient/middleware/auth_token.py | 98 +++++++++- keystoneclient/tests/client_fixtures.py | 104 +++++++++++ .../tests/test_auth_token_middleware.py | 172 ++++++++++++++++++ 3 files changed, 370 insertions(+), 4 deletions(-) diff --git a/keystoneclient/middleware/auth_token.py b/keystoneclient/middleware/auth_token.py index 36c8fba66..ade945b81 100644 --- a/keystoneclient/middleware/auth_token.py +++ b/keystoneclient/middleware/auth_token.py @@ -298,7 +298,17 @@ opts = [ help='(optional) indicate whether to set the X-Service-Catalog' ' header. If False, middleware will not ask for service' ' catalog on token validation and will not set the' - ' X-Service-Catalog header.') + ' X-Service-Catalog header.'), + cfg.StrOpt('enforce_token_bind', + default='permissive', + help='Used to control the use and type of token binding. Can' + ' be set to: "disabled" to not check token binding.' + ' "permissive" (default) to validate binding information if the' + ' bind type is of a form known to the server and ignore it if' + ' not. "strict" like "permissive" but if the bind type is' + ' unknown the token will be rejected. "required" any form of' + ' token binding is needed to be allowed. Finally the name of a' + ' binding method that must be present in tokens.'), ] CONF.register_opts(opts, group='keystone_authtoken') @@ -306,6 +316,14 @@ LIST_OF_VERSIONS_TO_ATTEMPT = ['v2.0', 'v3.0'] CACHE_KEY_TEMPLATE = 'tokens/%s' +class BIND_MODE: + DISABLED = 'disabled' + PERMISSIVE = 'permissive' + STRICT = 'strict' + REQUIRED = 'required' + KERBEROS = 'kerberos' + + def will_expire_soon(expiry): """Determines if expiration is about to occur. @@ -574,7 +592,7 @@ class AuthProtocol(object): try: self._remove_auth_headers(env) user_token = self._get_user_token_from_header(env) - token_info = self._validate_user_token(user_token) + token_info = self._validate_user_token(user_token, env) env['keystone.token_info'] = token_info user_headers = self._build_user_headers(token_info) self._add_headers(env, user_headers) @@ -797,7 +815,7 @@ class AuthProtocol(object): "Unable to parse expiration time from token: %s", data) raise ServiceError('invalid json response') - def _validate_user_token(self, user_token, retry=True): + def _validate_user_token(self, user_token, env, retry=True): """Authenticate user using PKI :param user_token: user's token id @@ -820,6 +838,7 @@ class AuthProtocol(object): else: data = self.verify_uuid_token(user_token, retry) expires = confirm_token_not_expired(data) + self._confirm_token_bind(data, env) self._cache_put(token_id, data, expires) return data except NetworkError: @@ -1058,6 +1077,77 @@ class AuthProtocol(object): data_to_store, timeout=self.token_cache_time) + def _invalid_user_token(self, msg=False): + # NOTE(jamielennox): use False as the default so that None is valid + if msg is False: + msg = 'Token authorization failed' + + raise InvalidUserToken(msg) + + def _confirm_token_bind(self, data, env): + bind_mode = self._conf_get('enforce_token_bind') + + if bind_mode == BIND_MODE.DISABLED: + return + + try: + if _token_is_v2(data): + bind = data['access']['token']['bind'] + elif _token_is_v3(data): + bind = data['token']['bind'] + else: + self._invalid_user_token() + except KeyError: + bind = {} + + # permissive and strict modes don't require there to be a bind + permissive = bind_mode in (BIND_MODE.PERMISSIVE, BIND_MODE.STRICT) + + if not bind: + if permissive: + # no bind provided and none required + return + else: + self.LOG.info("No bind information present in token.") + self._invalid_user_token() + + # get the named mode if bind_mode is not one of the predefined + if permissive or bind_mode == BIND_MODE.REQUIRED: + name = None + else: + name = bind_mode + + if name and name not in bind: + self.LOG.info("Named bind mode %s not in bind information", name) + self._invalid_user_token() + + for bind_type, identifier in six.iteritems(bind): + if bind_type == BIND_MODE.KERBEROS: + if not env.get('AUTH_TYPE', '').lower() == 'negotiate': + self.LOG.info("Kerberos credentials required and " + "not present.") + self._invalid_user_token() + + if not env.get('REMOTE_USER') == identifier: + self.LOG.info("Kerberos credentials do not match " + "those in bind.") + self._invalid_user_token() + + self.LOG.debug("Kerberos bind authentication successful.") + + elif bind_mode == BIND_MODE.PERMISSIVE: + self.LOG.debug("Ignoring Unknown bind for permissive mode: " + "%(bind_type)s: %(identifier)s.", + {'bind_type': bind_type, + 'identifier': identifier}) + + else: + self.LOG.info("Couldn't verify unknown bind: %(bind_type)s: " + "%(identifier)s.", + {'bind_type': bind_type, + 'identifier': identifier}) + self._invalid_user_token() + def _cache_put(self, token_id, data, expires): """Put token data into the cache. @@ -1127,7 +1217,7 @@ class AuthProtocol(object): response.status_code) if retry: self.LOG.info('Retrying validation') - return self._validate_user_token(user_token, False) + return self._validate_user_token(user_token, env, False) else: self.LOG.warn("Invalid user token: %s. Keystone response: %s.", user_token, data) diff --git a/keystoneclient/tests/client_fixtures.py b/keystoneclient/tests/client_fixtures.py index d5bb80637..2ed050689 100644 --- a/keystoneclient/tests/client_fixtures.py +++ b/keystoneclient/tests/client_fixtures.py @@ -77,6 +77,8 @@ class Examples(fixtures.Fixture): with open(self.SIGNING_CERT_FILE) as f: self.SIGNING_CERT = f.read() + self.KERBEROS_BIND = 'USER@REALM' + self.SIGNING_KEY_FILE = os.path.join(KEYDIR, 'signing_key.pem') with open(self.SIGNING_KEY_FILE) as f: self.SIGNING_KEY = f.read() @@ -88,10 +90,14 @@ class Examples(fixtures.Fixture): self.UUID_TOKEN_DEFAULT = "ec6c0710ec2f471498484c1b53ab4f9d" self.UUID_TOKEN_NO_SERVICE_CATALOG = '8286720fbe4941e69fa8241723bb02df' self.UUID_TOKEN_UNSCOPED = '731f903721c14827be7b2dc912af7776' + self.UUID_TOKEN_BIND = '3fc54048ad64405c98225ce0897af7c5' + self.UUID_TOKEN_UNKNOWN_BIND = '8885fdf4d42e4fb9879e6379fa1eaf48' self.VALID_DIABLO_TOKEN = 'b0cf19b55dbb4f20a6ee18e6c6cf1726' self.v3_UUID_TOKEN_DEFAULT = '5603457654b346fdbb93437bfe76f2f1' self.v3_UUID_TOKEN_UNSCOPED = 'd34835fdaec447e695a0a024d84f8d79' self.v3_UUID_TOKEN_DOMAIN_SCOPED = 'e8a7b63aaa4449f38f0c5c05c3581792' + self.v3_UUID_TOKEN_BIND = '2f61f73e1c854cbb9534c487f9bd63c2' + self.v3_UUID_TOKEN_UNKNOWN_BIND = '7ed9781b62cd4880b8d8c6788ab1d1e2' self.REVOKED_TOKEN_HASH = utils.hash_signed_token(self.REVOKED_TOKEN) self.REVOKED_TOKEN_LIST = ( @@ -209,6 +215,50 @@ class Examples(fixtures.Fixture): } }, }, + self.UUID_TOKEN_BIND: { + 'access': { + 'token': { + 'bind': {'kerberos': self.KERBEROS_BIND}, + 'id': self.UUID_TOKEN_BIND, + 'expires': '2020-01-01T00:00:10.000123Z', + 'tenant': { + 'id': 'tenant_id1', + 'name': 'tenant_name1', + }, + }, + 'user': { + 'id': 'user_id1', + 'name': 'user_name1', + 'roles': [ + {'name': 'role1'}, + {'name': 'role2'}, + ], + }, + 'serviceCatalog': {} + }, + }, + self.UUID_TOKEN_UNKNOWN_BIND: { + 'access': { + 'token': { + 'bind': {'FOO': 'BAR'}, + 'id': self.UUID_TOKEN_UNKNOWN_BIND, + 'expires': '2020-01-01T00:00:10.000123Z', + 'tenant': { + 'id': 'tenant_id1', + 'name': 'tenant_name1', + }, + }, + 'user': { + 'id': 'user_id1', + 'name': 'user_name1', + 'roles': [ + {'name': 'role1'}, + {'name': 'role2'}, + ], + }, + 'serviceCatalog': {} + }, + }, self.v3_UUID_TOKEN_DEFAULT: { 'token': { 'expires_at': '2020-01-01T00:00:10.000123Z', @@ -328,6 +378,60 @@ class Examples(fixtures.Fixture): 'catalog': {} } }, + self.v3_UUID_TOKEN_BIND: { + 'token': { + 'bind': {'kerberos': self.KERBEROS_BIND}, + 'expires_at': '2020-01-01T00:00:10.000123Z', + 'user': { + 'id': 'user_id1', + 'name': 'user_name1', + 'domain': { + 'id': 'domain_id1', + 'name': 'domain_name1' + } + }, + 'project': { + 'id': 'tenant_id1', + 'name': 'tenant_name1', + 'domain': { + 'id': 'domain_id1', + 'name': 'domain_name1' + } + }, + 'roles': [ + {'name': 'role1', 'id': 'Role1'}, + {'name': 'role2', 'id': 'Role2'}, + ], + 'catalog': {} + } + }, + self.v3_UUID_TOKEN_UNKNOWN_BIND: { + 'token': { + 'bind': {'FOO': 'BAR'}, + 'expires_at': '2020-01-01T00:00:10.000123Z', + 'user': { + 'id': 'user_id1', + 'name': 'user_name1', + 'domain': { + 'id': 'domain_id1', + 'name': 'domain_name1' + } + }, + 'project': { + 'id': 'tenant_id1', + 'name': 'tenant_name1', + 'domain': { + 'id': 'domain_id1', + 'name': 'domain_name1' + } + }, + 'roles': [ + {'name': 'role1', 'id': 'Role1'}, + {'name': 'role2', 'id': 'Role2'}, + ], + 'catalog': {} + } + }, } self.JSON_TOKEN_RESPONSES = dict([(k, jsonutils.dumps(v)) for k, v in diff --git a/keystoneclient/tests/test_auth_token_middleware.py b/keystoneclient/tests/test_auth_token_middleware.py index 69358aca4..2f29b0b5f 100644 --- a/keystoneclient/tests/test_auth_token_middleware.py +++ b/keystoneclient/tests/test_auth_token_middleware.py @@ -247,6 +247,7 @@ class BaseAuthTokenMiddlewareTest(testtools.TestCase): 'auth_uri': 'https://keystone.example.com:1234', } + self.auth_version = auth_version self.response_status = None self.response_headers = None @@ -902,6 +903,170 @@ class CommonAuthTokenMiddlewareTest(object): self.assert_valid_request_200(self.token_dict['uuid_token_default'], with_catalog=False) + def assert_kerberos_bind(self, token, bind_level, + use_kerberos=True, success=True): + conf = { + 'enforce_token_bind': bind_level, + 'auth_version': self.auth_version, + } + self.set_middleware(conf=conf) + + req = webob.Request.blank('/') + req.headers['X-Auth-Token'] = token + + if use_kerberos: + if use_kerberos is True: + req.environ['REMOTE_USER'] = self.examples.KERBEROS_BIND + else: + req.environ['REMOTE_USER'] = use_kerberos + + req.environ['AUTH_TYPE'] = 'Negotiate' + + body = self.middleware(req.environ, self.start_fake_response) + + if success: + self.assertEqual(self.response_status, 200) + self.assertEqual(body, ['SUCCESS']) + self.assertIn('keystone.token_info', req.environ) + self.assert_valid_last_url(token) + else: + self.assertEqual(self.response_status, 401) + self.assertEqual(self.response_headers['WWW-Authenticate'], + "Keystone uri='https://keystone.example.com:1234'" + ) + + def test_uuid_bind_token_disabled_with_kerb_user(self): + for use_kerberos in [True, False]: + self.assert_kerberos_bind(self.token_dict['uuid_token_bind'], + bind_level='disabled', + use_kerberos=use_kerberos, + success=True) + + def test_uuid_bind_token_disabled_with_incorrect_ticket(self): + self.assert_kerberos_bind(self.token_dict['uuid_token_bind'], + bind_level='kerberos', + use_kerberos='ronald@MCDONALDS.COM', + success=False) + + def test_uuid_bind_token_permissive_with_kerb_user(self): + self.assert_kerberos_bind(self.token_dict['uuid_token_bind'], + bind_level='permissive', + use_kerberos=True, + success=True) + + def test_uuid_bind_token_permissive_without_kerb_user(self): + self.assert_kerberos_bind(self.token_dict['uuid_token_bind'], + bind_level='permissive', + use_kerberos=False, + success=False) + + def test_uuid_bind_token_permissive_with_unknown_bind(self): + token = self.token_dict['uuid_token_unknown_bind'] + + for use_kerberos in [True, False]: + self.assert_kerberos_bind(token, + bind_level='permissive', + use_kerberos=use_kerberos, + success=True) + + def test_uuid_bind_token_permissive_with_incorrect_ticket(self): + self.assert_kerberos_bind(self.token_dict['uuid_token_bind'], + bind_level='kerberos', + use_kerberos='ronald@MCDONALDS.COM', + success=False) + + def test_uuid_bind_token_strict_with_kerb_user(self): + self.assert_kerberos_bind(self.token_dict['uuid_token_bind'], + bind_level='strict', + use_kerberos=True, + success=True) + + def test_uuid_bind_token_strict_with_kerbout_user(self): + self.assert_kerberos_bind(self.token_dict['uuid_token_bind'], + bind_level='strict', + use_kerberos=False, + success=False) + + def test_uuid_bind_token_strict_with_unknown_bind(self): + token = self.token_dict['uuid_token_unknown_bind'] + + for use_kerberos in [True, False]: + self.assert_kerberos_bind(token, + bind_level='strict', + use_kerberos=use_kerberos, + success=False) + + def test_uuid_bind_token_required_with_kerb_user(self): + self.assert_kerberos_bind(self.token_dict['uuid_token_bind'], + bind_level='required', + use_kerberos=True, + success=True) + + def test_uuid_bind_token_required_without_kerb_user(self): + self.assert_kerberos_bind(self.token_dict['uuid_token_bind'], + bind_level='required', + use_kerberos=False, + success=False) + + def test_uuid_bind_token_required_with_unknown_bind(self): + token = self.token_dict['uuid_token_unknown_bind'] + + for use_kerberos in [True, False]: + self.assert_kerberos_bind(token, + bind_level='required', + use_kerberos=use_kerberos, + success=False) + + def test_uuid_bind_token_required_without_bind(self): + for use_kerberos in [True, False]: + self.assert_kerberos_bind(self.token_dict['uuid_token_default'], + bind_level='required', + use_kerberos=use_kerberos, + success=False) + + def test_uuid_bind_token_named_kerberos_with_kerb_user(self): + self.assert_kerberos_bind(self.token_dict['uuid_token_bind'], + bind_level='kerberos', + use_kerberos=True, + success=True) + + def test_uuid_bind_token_named_kerberos_without_kerb_user(self): + self.assert_kerberos_bind(self.token_dict['uuid_token_bind'], + bind_level='kerberos', + use_kerberos=False, + success=False) + + def test_uuid_bind_token_named_kerberos_with_unknown_bind(self): + token = self.token_dict['uuid_token_unknown_bind'] + + for use_kerberos in [True, False]: + self.assert_kerberos_bind(token, + bind_level='kerberos', + use_kerberos=use_kerberos, + success=False) + + def test_uuid_bind_token_named_kerberos_without_bind(self): + for use_kerberos in [True, False]: + self.assert_kerberos_bind(self.token_dict['uuid_token_default'], + bind_level='kerberos', + use_kerberos=use_kerberos, + success=False) + + def test_uuid_bind_token_named_kerberos_with_incorrect_ticket(self): + self.assert_kerberos_bind(self.token_dict['uuid_token_bind'], + bind_level='kerberos', + use_kerberos='ronald@MCDONALDS.COM', + success=False) + + def test_uuid_bind_token_with_unknown_named_FOO(self): + token = self.token_dict['uuid_token_bind'] + + for use_kerberos in [True, False]: + self.assert_kerberos_bind(token, + bind_level='FOO', + use_kerberos=use_kerberos, + success=False) + class CertDownloadMiddlewareTest(BaseAuthTokenMiddlewareTest, testresources.ResourcedTestCase): @@ -1043,6 +1208,8 @@ class v2AuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest, self.token_dict = { 'uuid_token_default': self.examples.UUID_TOKEN_DEFAULT, 'uuid_token_unscoped': self.examples.UUID_TOKEN_UNSCOPED, + 'uuid_token_bind': self.examples.UUID_TOKEN_BIND, + 'uuid_token_unknown_bind': self.examples.UUID_TOKEN_UNKNOWN_BIND, 'signed_token_scoped': self.examples.SIGNED_TOKEN_SCOPED, 'signed_token_scoped_expired': self.examples.SIGNED_TOKEN_SCOPED_EXPIRED, @@ -1069,6 +1236,8 @@ class v2AuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest, for token in (self.examples.UUID_TOKEN_DEFAULT, self.examples.UUID_TOKEN_UNSCOPED, + self.examples.UUID_TOKEN_BIND, + self.examples.UUID_TOKEN_UNKNOWN_BIND, self.examples.UUID_TOKEN_NO_SERVICE_CATALOG): httpretty.register_uri(httpretty.GET, "%s/v2.0/tokens/%s" % (BASE_URI, token), @@ -1223,6 +1392,9 @@ class v3AuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest, self.token_dict = { 'uuid_token_default': self.examples.v3_UUID_TOKEN_DEFAULT, 'uuid_token_unscoped': self.examples.v3_UUID_TOKEN_UNSCOPED, + 'uuid_token_bind': self.examples.v3_UUID_TOKEN_BIND, + 'uuid_token_unknown_bind': + self.examples.v3_UUID_TOKEN_UNKNOWN_BIND, 'signed_token_scoped': self.examples.SIGNED_v3_TOKEN_SCOPED, 'signed_token_scoped_expired': self.examples.SIGNED_TOKEN_SCOPED_EXPIRED,