Skip to content

Commit

Permalink
Implement Token Binding.
Browse files Browse the repository at this point in the history
Brings token binding to keystone server. There are a number of places
where the location or hardcoding of binding checks are not optimal
however fixing them will require having a proper authentication plugin
scheme so just assume that they will be moved when that happens.

DocImpact
Implements: blueprint authentication-tied-to-token
Change-Id: Ib34e5e0b6bd83837f6addbd45d4c5b828ce2f3bd
  • Loading branch information
Jamie Lennox authored and jamielennox committed Jul 17, 2013
1 parent 53a03b5 commit 2667c77
Show file tree
Hide file tree
Showing 14 changed files with 484 additions and 12 deletions.
38 changes: 38 additions & 0 deletions doc/source/configuration.rst
Expand Up @@ -506,6 +506,44 @@ default, but can be enabled by including the following in ``keystone.conf``.
enabled = True


Token Binding
-------------

Token binding refers to the practice of embedding information from external
authentication providers (like a company's Kerberos server) inside the token
such that a client may enforce that the token only be used in conjunction with
that specified authentication. This is an additional security mechanism as it
means that if a token is stolen it will not be usable without also providing the
external authentication.

To activate token binding you must specify the types of authentication that
token binding should be used for in ``keystone.conf`` e.g.::

[token]
bind = kerberos

Currently only ``kerberos`` is supported.

To enforce checking of token binding the ``enforce_token_bind`` parameter
should be set to one of the following modes:

* ``disabled`` disable token bind checking
* ``permissive`` enable bind checking, if a token is bound to a mechanism that
is unknown to the server then ignore it. This is the default.
* ``strict`` enable bind checking, if a token is bound to a mechanism that is
unknown to the server then this token should be rejected.
* ``required`` enable bind checking and require that at least 1 bind mechanism
is used for tokens.
* named enable bind checking and require that the specified authentication
mechanism is used. e.g.::

[token]
enforce_token_bind = kerberos

*Do not* set ``enforce_token_bind = named`` as there is not an authentication
mechanism called ``named``.


Sample Configuration Files
--------------------------

Expand Down
9 changes: 9 additions & 0 deletions etc/keystone.conf.sample
Expand Up @@ -133,6 +133,15 @@
# Amount of time a token should remain valid (in seconds)
# expiration = 86400

# External auth mechanisms that should add bind information to token.
# eg kerberos, x509
# bind =

# Enforcement policy on tokens presented to keystone with bind information.
# One of disabled, permissive, strict, required or a specifically required bind
# mode e.g. kerberos or x509 to require binding to that authentication.
# enforce_token_bind = permissive

[policy]
# driver = keystone.policy.backends.sql.Policy

Expand Down
2 changes: 1 addition & 1 deletion keystone/auth/controllers.py
Expand Up @@ -283,7 +283,7 @@ def authenticate_for_token(self, context, auth=None):

try:
auth_info = AuthInfo(context, auth=auth)
auth_context = {'extras': {}, 'method_names': []}
auth_context = {'extras': {}, 'method_names': [], 'bind': {}}
self.authenticate(context, auth_info, auth_context)
self._check_and_set_default_scoping(auth_info, auth_context)
(domain_id, project_id, trust) = auth_info.get_scope()
Expand Down
7 changes: 7 additions & 0 deletions keystone/auth/plugins/external.py
Expand Up @@ -42,6 +42,9 @@ def authenticate(self, context, auth_info, auth_context):
user_ref = auth_info.identity_api.get_user_by_name(username,
domain_id)
auth_context['user_id'] = user_ref['id']
if ('kerberos' in CONF.token.bind and
context.get('AUTH_TYPE', '').lower() == 'negotiate'):
auth_context['bind']['kerberos'] = username
except Exception:
msg = _('Unable to lookup user %s') % (REMOTE_USER)
raise exception.Unauthorized(msg)
Expand Down Expand Up @@ -75,6 +78,10 @@ def authenticate(self, context, auth_info, auth_context):
user_ref = auth_info.identity_api.get_user_by_name(username,
domain_id)
auth_context['user_id'] = user_ref['id']
if ('kerberos' in CONF.token.bind and
context.get('AUTH_TYPE', '').lower() == 'negotiate'):
auth_context['bind']['kerberos'] = username

except Exception:
msg = _('Unable to lookup user %s') % (REMOTE_USER)
raise exception.Unauthorized(msg)
2 changes: 2 additions & 0 deletions keystone/auth/plugins/token.py
Expand Up @@ -16,6 +16,7 @@

from keystone import auth
from keystone.common import logging
from keystone.common import wsgi
from keystone import exception
from keystone import token

Expand All @@ -36,6 +37,7 @@ def authenticate(self, context, auth_payload, user_context):
target=METHOD_NAME)
token_id = auth_payload['id']
token_ref = self.token_api.get_token(token_id)
wsgi.validate_token_bind(context, token_ref)
user_context.setdefault(
'user_id', token_ref['token_data']['token']['user']['id'])
# to support Grizzly-3 to Grizzly-RC1 transition
Expand Down
4 changes: 4 additions & 0 deletions keystone/common/config.py
Expand Up @@ -217,6 +217,10 @@ def configure():
# os_inherit
register_bool('enabled', group='os_inherit', default=False)

# binding
register_list('bind', group='token', default=[])
register_str('enforce_token_bind', group='token', default='permissive')

# ssl
register_bool('enable', group='ssl', default=False)
register_str('certfile', group='ssl',
Expand Down
4 changes: 4 additions & 0 deletions keystone/common/controller.py
Expand Up @@ -25,6 +25,10 @@ def _build_policy_check_credentials(self, action, context, kwargs):
LOG.warning(_('RBAC: Invalid token'))
raise exception.Unauthorized()

# NOTE(jamielennox): whilst this maybe shouldn't be within this function
# it would otherwise need to reload the token_ref from backing store.
wsgi.validate_token_bind(context, token_ref)

creds = {}
if 'token_data' in token_ref and 'token' in token_ref['token_data']:
#V3 Tokens
Expand Down
64 changes: 60 additions & 4 deletions keystone/common/wsgi.py
Expand Up @@ -73,6 +73,55 @@ def mask_password(message, is_unicode=False, secret="***"):
return result


def validate_token_bind(context, token_ref):
bind_mode = CONF.token.enforce_token_bind

if bind_mode == 'disabled':
return

bind = token_ref.get('bind', {})

# permissive and strict modes don't require there to be a bind
permissive = bind_mode in ('permissive', 'strict')

# get the named mode if bind_mode is not one of the known
name = None if permissive or bind_mode == 'required' else bind_mode

if not bind:
if permissive:
# no bind provided and none required
return
else:
LOG.info(_("No bind information present in token"))
raise exception.Unauthorized()

if name and name not in bind:
LOG.info(_("Named bind mode %s not in bind information"), name)
raise exception.Unauthorized()

for bind_type, identifier in bind.iteritems():
if bind_type == 'kerberos':
if not context.get('AUTH_TYPE', '').lower() == 'negotiate':
LOG.info(_("Kerberos credentials required and not present"))
raise exception.Unauthorized()

if not context.get('REMOTE_USER') == identifier:
LOG.info(_("Kerberos credentials do not match those in bind"))
raise exception.Unauthorized()

LOG.info(_("Kerberos bind authentication successful"))

elif bind_mode == 'permissive':
LOG.debug(_("Ignoring unknown bind for permissive mode: "
"{%(bind_type)s: %(identifier)s}"),
{'bind_type': bind_type, 'identifier': identifier})
else:
LOG.info(_("Couldn't verify unknown bind: "
"{%(bind_type)s: %(identifier)s}"),
{'bind_type': bind_type, 'identifier': identifier})
raise exception.Unauthorized()


class WritableLogger(object):
"""A thin wrapper that responds to `write` and logs."""

Expand Down Expand Up @@ -167,10 +216,16 @@ def __call__(self, req):
context['headers'] = dict(req.headers.iteritems())
context['path'] = req.environ['PATH_INFO']
params = req.environ.get(PARAMS_ENV, {})
if 'REMOTE_USER' in req.environ:
context['REMOTE_USER'] = req.environ['REMOTE_USER']
elif context.get('REMOTE_USER', None) is not None:
del context['REMOTE_USER']

for name in ['REMOTE_USER', 'AUTH_TYPE']:
try:
context[name] = req.environ[name]
except KeyError:
try:
del context[name]
except KeyError:
pass

params.update(arg_dict)

context.setdefault('is_admin', False)
Expand Down Expand Up @@ -233,6 +288,7 @@ def assert_admin(self, context):
except exception.TokenNotFound as e:
raise exception.Unauthorized(e)

validate_token_bind(context, user_token_ref)
creds = user_token_ref['metadata'].copy()

try:
Expand Down
20 changes: 16 additions & 4 deletions keystone/token/controllers.py
Expand Up @@ -5,6 +5,7 @@
from keystone.common import dependency
from keystone.common import logging
from keystone.common import utils
from keystone.common import wsgi
from keystone import config
from keystone import exception
from keystone.openstack.common import timeutils
Expand Down Expand Up @@ -78,7 +79,7 @@ def authenticate(self, context, auth=None):
auth_info = self._authenticate_local(
context, auth)

user_ref, tenant_ref, metadata_ref, expiry = auth_info
user_ref, tenant_ref, metadata_ref, expiry, bind = auth_info
core.validate_auth_info(self, user_ref, tenant_ref)
user_ref = self._filter_domain_id(user_ref)
if tenant_ref:
Expand All @@ -97,6 +98,8 @@ def authenticate(self, context, auth=None):
catalog_ref = {}

auth_token_data['id'] = 'placeholder'
if bind:
auth_token_data['bind'] = bind

roles_ref = []
for role_id in metadata_ref.get('roles', []):
Expand Down Expand Up @@ -133,6 +136,8 @@ def _authenticate_token(self, context, auth):
except exception.NotFound as e:
raise exception.Unauthorized(e)

wsgi.validate_token_bind(context, old_token_ref)

#A trust token cannot be used to get another token
if 'trust' in old_token_ref:
raise exception.Forbidden()
Expand Down Expand Up @@ -194,7 +199,9 @@ def _authenticate_token(self, context, auth):
metadata_ref['trustee_user_id'] = trust_ref['trustee_user_id']
metadata_ref['trust_id'] = trust_id

return (current_user_ref, tenant_ref, metadata_ref, expiry)
bind = old_token_ref.get('bind', None)

return (current_user_ref, tenant_ref, metadata_ref, expiry, bind)

def _authenticate_local(self, context, auth):
"""Try to authenticate against the identity backend.
Expand Down Expand Up @@ -252,7 +259,7 @@ def _authenticate_local(self, context, auth):
user_id, tenant_id)

expiry = core.default_expire_time()
return (user_ref, tenant_ref, metadata_ref, expiry)
return (user_ref, tenant_ref, metadata_ref, expiry, None)

def _authenticate_external(self, context, auth):
"""Try to authenticate an external user via REMOTE_USER variable.
Expand Down Expand Up @@ -281,7 +288,12 @@ def _authenticate_external(self, context, auth):
user_id, tenant_id)

expiry = core.default_expire_time()
return (user_ref, tenant_ref, metadata_ref, expiry)
bind = None
if ('kerberos' in CONF.token.bind and
context.get('AUTH_TYPE', '').lower() == 'negotiate'):
bind = {'kerberos': username}

return (user_ref, tenant_ref, metadata_ref, expiry, bind)

def _get_auth_token_data(self, user, tenant, metadata, expiry):
return dict(user=user,
Expand Down
11 changes: 10 additions & 1 deletion keystone/token/providers/uuid.py
Expand Up @@ -59,6 +59,8 @@ def format_token(cls, token_ref, roles_ref, catalog_ref=None):
}
}
}
if 'bind' in token_ref:
o['access']['token']['bind'] = token_ref['bind']
if 'tenant' in token_ref and token_ref['tenant']:
token_ref['tenant']['enabled'] = True
o['access']['token']['tenant'] = token_ref['tenant']
Expand Down Expand Up @@ -285,7 +287,8 @@ def _populate_token_dates(self, token_data, expires=None, trust=None):

def get_token_data(self, user_id, method_names, extras,
domain_id=None, project_id=None, expires=None,
trust=None, token=None, include_catalog=True):
trust=None, token=None, include_catalog=True,
bind=None):
token_data = {'methods': method_names,
'extras': extras}

Expand All @@ -299,6 +302,9 @@ def get_token_data(self, user_id, method_names, extras,
if user_id != trust['trustee_user_id']:
raise exception.Forbidden(_('User is not a trustee.'))

if bind:
token_data['bind'] = bind

self._populate_scope(token_data, domain_id, project_id)
self._populate_user(token_data, user_id, domain_id, project_id, trust)
self._populate_roles(token_data, user_id, domain_id, project_id, trust)
Expand Down Expand Up @@ -346,6 +352,7 @@ def _issue_v2_token(self, **kwargs):
tenant=token_ref['tenant'],
metadata=token_ref['metadata'],
token_data=token_data,
bind=token_ref.get('bind'),
trust_id=token_ref['metadata'].get('trust_id'))
self.token_api.create_token(token_id, data)
except Exception:
Expand Down Expand Up @@ -381,6 +388,7 @@ def _issue_v3_token(self, **kwargs):
project_id=project_id,
expires=expires_at,
trust=trust,
bind=auth_context.get('bind') if auth_context else None,
include_catalog=include_catalog)

token_id = self._get_token_id(token_data)
Expand Down Expand Up @@ -542,6 +550,7 @@ def _validate_v3_token(self, token_id):
['password', 'token'],
{},
project_id=project_id,
bind=token_ref.get('bind'),
expires=token_ref['expires'])
return token_data

Expand Down
4 changes: 2 additions & 2 deletions tests/auth_plugin_external_domain.conf
@@ -1,3 +1,3 @@
[auth]
methods = external
external = keystone.auth.plugins.external.ExternalDomain
methods = external, password, token
external = keystone.auth.plugins.external.ExternalDomain

0 comments on commit 2667c77

Please sign in to comment.