Make fernet work with oauth1 authentication

Previously, fernet didn't know how to handle oauth1 authentication
flows. This patch adds a new token version to the fernet provider and
allows it to issue oauth1 tokens.

This work is being done so that we can get fernet to be feature
equivalent with the uuid token provider. Then we will be slightly
closer to making fernet the default token provider in keystone.

Closes-Bug: 1534252

Change-Id: I638404952597bb23dff01f80efb728b653e5560c
This commit is contained in:
Lance Bragstad 2016-01-14 18:13:13 +00:00 committed by ayoung
parent 5c51dbbc0a
commit 03b4e82188
5 changed files with 130 additions and 42 deletions

View File

@ -30,6 +30,7 @@ from keystone.oauth1 import controllers
from keystone.oauth1 import core
from keystone.tests import unit
from keystone.tests.unit.common import test_notifications
from keystone.tests.unit import ksfixtures
from keystone.tests.unit.ksfixtures import temporaryfile
from keystone.tests.unit import test_v3
@ -599,6 +600,18 @@ class AuthTokenTests(OAuthFlowTests):
expected_status=http_client.FORBIDDEN)
class FernetAuthTokenTests(AuthTokenTests):
def config_overrides(self):
super(FernetAuthTokenTests, self).config_overrides()
self.config_fixture.config(group='token', provider='fernet')
self.useFixture(ksfixtures.KeyRepository(self.config_fixture))
def test_delete_keystone_tokens_by_consumer_id(self):
# NOTE(lbragstad): Fernet tokens are never persisted in the backend.
pass
class MaliciousOAuth1Tests(OAuth1Tests):
def test_bad_consumer_secret(self):

View File

@ -338,7 +338,8 @@ class TestPayloads(unit.TestCase):
def _test_payload(self, payload_class, exp_user_id=None, exp_methods=None,
exp_project_id=None, exp_domain_id=None,
exp_trust_id=None, exp_federated_info=None):
exp_trust_id=None, exp_federated_info=None,
exp_access_token_id=None):
exp_user_id = exp_user_id or uuid.uuid4().hex
exp_methods = exp_methods or ['password']
exp_expires_at = utils.isotime(timeutils.utcnow(), subsecond=True)
@ -346,10 +347,13 @@ class TestPayloads(unit.TestCase):
payload = payload_class.assemble(
exp_user_id, exp_methods, exp_project_id, exp_domain_id,
exp_expires_at, exp_audit_ids, exp_trust_id, exp_federated_info)
exp_expires_at, exp_audit_ids, exp_trust_id, exp_federated_info,
exp_access_token_id)
(user_id, methods, project_id, domain_id, expires_at, audit_ids,
trust_id, federated_info) = payload_class.disassemble(payload)
(user_id, methods, project_id,
domain_id, expires_at, audit_ids,
trust_id, federated_info,
access_token_id) = payload_class.disassemble(payload)
self.assertEqual(exp_user_id, user_id)
self.assertEqual(exp_methods, methods)
@ -358,6 +362,7 @@ class TestPayloads(unit.TestCase):
self.assertEqual(exp_project_id, project_id)
self.assertEqual(exp_domain_id, domain_id)
self.assertEqual(exp_trust_id, trust_id)
self.assertEqual(exp_access_token_id, access_token_id)
if exp_federated_info:
self.assertDictEqual(exp_federated_info, federated_info)
@ -463,6 +468,11 @@ class TestPayloads(unit.TestCase):
exp_domain_id=uuid.uuid4().hex,
exp_federated_info=exp_federated_info)
def test_oauth_scoped_payload(self):
self._test_payload(token_formatters.OauthScopedPayload,
exp_project_id=uuid.uuid4().hex,
exp_access_token_id=uuid.uuid4().hex)
class TestFernetKeyRotation(unit.TestCase):
def setUp(self):

View File

@ -84,6 +84,11 @@ class V2TokenDataHelper(object):
'API.')
raise exception.Unauthorized(msg)
if 'OS-OAUTH1' in v3_token:
msg = ('Unable to validate Oauth tokens using the version v2.0 '
'API.')
raise exception.Unauthorized(msg)
# Set user roles
user['roles'] = []
role_ids = []
@ -702,7 +707,7 @@ class BaseProvider(provider.Provider):
def validate_non_persistent_token(self, token_id):
try:
(user_id, methods, audit_ids, domain_id, project_id, trust_id,
federated_info, created_at, expires_at) = (
federated_info, access_token_id, created_at, expires_at) = (
self.token_formatter.validate_token(token_id))
except exception.ValidationError as e:
raise exception.TokenNotFound(e)
@ -725,6 +730,10 @@ class BaseProvider(provider.Provider):
if trust_id:
trust_ref = self.trust_api.get_trust(trust_id)
access_token = None
if access_token_id:
access_token = self.oauth_api.get_access_token(access_token_id)
return self.v3_token_data_helper.get_token_data(
user_id,
method_names=methods,
@ -734,6 +743,7 @@ class BaseProvider(provider.Provider):
expires=expires_at,
trust=trust_ref,
token=token_dict,
access_token=access_token,
audit_info=audit_ids)
def validate_v3_token(self, token_ref):

View File

@ -23,7 +23,7 @@ from keystone.token.providers.fernet import token_formatters as tf
CONF = cfg.CONF
@dependency.requires('trust_api')
@dependency.requires('trust_api', 'oauth_api')
class Provider(common.BaseProvider):
def __init__(self, *args, **kwargs):
super(Provider, self).__init__(*args, **kwargs)
@ -154,9 +154,10 @@ class Provider(common.BaseProvider):
project_id = token_data['access']['token'].get('tenant', {}).get('id')
domain_id = None
trust_id = None
access_token_id = None
federated_info = None
return (user_id, expires_at, audit_ids, methods, domain_id, project_id,
trust_id, federated_info)
trust_id, access_token_id, federated_info)
def _extract_v3_token_data(self, token_data):
"""Extract information from a v3 token reference."""
@ -167,10 +168,12 @@ class Provider(common.BaseProvider):
domain_id = token_data['token'].get('domain', {}).get('id')
project_id = token_data['token'].get('project', {}).get('id')
trust_id = token_data['token'].get('OS-TRUST:trust', {}).get('id')
access_token_id = token_data['token'].get('OS-OAUTH1', {}).get(
'access_token_id')
federated_info = self._build_federated_info(token_data)
return (user_id, expires_at, audit_ids, methods, domain_id, project_id,
trust_id, federated_info)
trust_id, access_token_id, federated_info)
def _get_token_id(self, token_data):
"""Generate the token_id based upon the data in token_data.
@ -184,21 +187,24 @@ class Provider(common.BaseProvider):
# attribute.
if token_data.get('access'):
(user_id, expires_at, audit_ids, methods, domain_id, project_id,
trust_id, federated_info) = self._extract_v2_token_data(
token_data)
trust_id, access_token_id, federated_info) = (
self._extract_v2_token_data(token_data))
else:
(user_id, expires_at, audit_ids, methods, domain_id, project_id,
trust_id, federated_info) = self._extract_v3_token_data(
token_data)
trust_id, access_token_id, federated_info) = (
self._extract_v3_token_data(token_data))
return self.token_formatter.create_token(user_id,
expires_at,
audit_ids,
methods=methods,
domain_id=domain_id,
project_id=project_id,
trust_id=trust_id,
federated_info=federated_info)
return self.token_formatter.create_token(
user_id,
expires_at,
audit_ids,
methods=methods,
domain_id=domain_id,
project_id=project_id,
trust_id=trust_id,
federated_info=federated_info,
access_token_id=access_token_id
)
@property
def _supports_bind_authentication(self):

View File

@ -145,18 +145,19 @@ class TokenFormatter(object):
def create_token(self, user_id, expires_at, audit_ids, methods=None,
domain_id=None, project_id=None, trust_id=None,
federated_info=None):
federated_info=None, access_token_id=None):
"""Given a set of payload attributes, generate a Fernet token."""
for payload_class in PAYLOAD_CLASSES:
if payload_class.create_arguments_apply(
project_id=project_id, domain_id=domain_id,
trust_id=trust_id, federated_info=federated_info):
trust_id=trust_id, federated_info=federated_info,
access_token_id=access_token_id):
break
version = payload_class.version
payload = payload_class.assemble(
user_id, methods, project_id, domain_id, expires_at, audit_ids,
trust_id, federated_info
trust_id, federated_info, access_token_id
)
versioned_payload = (version,) + payload
@ -189,7 +190,7 @@ class TokenFormatter(object):
for payload_class in PAYLOAD_CLASSES:
if version == payload_class.version:
(user_id, methods, project_id, domain_id, expires_at,
audit_ids, trust_id, federated_info) = (
audit_ids, trust_id, federated_info, access_token_id) = (
payload_class.disassemble(payload))
break
else:
@ -206,7 +207,7 @@ class TokenFormatter(object):
expires_at = ks_utils.isotime(at=expires_at, subsecond=True)
return (user_id, methods, audit_ids, domain_id, project_id, trust_id,
federated_info, created_at, expires_at)
federated_info, access_token_id, created_at, expires_at)
class BasePayload(object):
@ -226,7 +227,7 @@ class BasePayload(object):
@classmethod
def assemble(cls, user_id, methods, project_id, domain_id, expires_at,
audit_ids, trust_id, federated_info):
audit_ids, trust_id, federated_info, access_token_id):
"""Assemble the payload of a token.
:param user_id: identifier of the user in the token request
@ -239,6 +240,7 @@ class BasePayload(object):
:param federated_info: dictionary containing group IDs, the identity
provider ID, protocol ID, and federated domain
ID
:param access_token_id: ID of the secret in OAuth1 authentication
:returns: the payload of a token
"""
@ -251,7 +253,7 @@ class BasePayload(object):
The tuple consists of::
(user_id, methods, project_id, domain_id, expires_at_str,
audit_ids, trust_id, federated_info)
audit_ids, trust_id, federated_info, access_token_id)
* ``methods`` are the auth methods.
* federated_info is a dict contains the group IDs, the identity
@ -336,7 +338,7 @@ class UnscopedPayload(BasePayload):
@classmethod
def assemble(cls, user_id, methods, project_id, domain_id, expires_at,
audit_ids, trust_id, federated_info):
audit_ids, trust_id, federated_info, access_token_id):
b_user_id = cls.attempt_convert_uuid_hex_to_bytes(user_id)
methods = auth_plugins.convert_method_list_to_integer(methods)
expires_at_int = cls._convert_time_string_to_float(expires_at)
@ -356,8 +358,9 @@ class UnscopedPayload(BasePayload):
domain_id = None
trust_id = None
federated_info = None
access_token_id = None
return (user_id, methods, project_id, domain_id, expires_at_str,
audit_ids, trust_id, federated_info)
audit_ids, trust_id, federated_info, access_token_id)
class DomainScopedPayload(BasePayload):
@ -369,7 +372,7 @@ class DomainScopedPayload(BasePayload):
@classmethod
def assemble(cls, user_id, methods, project_id, domain_id, expires_at,
audit_ids, trust_id, federated_info):
audit_ids, trust_id, federated_info, access_token_id):
b_user_id = cls.attempt_convert_uuid_hex_to_bytes(user_id)
methods = auth_plugins.convert_method_list_to_integer(methods)
try:
@ -404,9 +407,9 @@ class DomainScopedPayload(BasePayload):
project_id = None
trust_id = None
federated_info = None
access_token_id = None
return (user_id, methods, project_id, domain_id, expires_at_str,
audit_ids, trust_id, federated_info)
audit_ids, trust_id, federated_info, access_token_id)
class ProjectScopedPayload(BasePayload):
@ -418,7 +421,7 @@ class ProjectScopedPayload(BasePayload):
@classmethod
def assemble(cls, user_id, methods, project_id, domain_id, expires_at,
audit_ids, trust_id, federated_info):
audit_ids, trust_id, federated_info, access_token_id):
b_user_id = cls.attempt_convert_uuid_hex_to_bytes(user_id)
methods = auth_plugins.convert_method_list_to_integer(methods)
b_project_id = cls.attempt_convert_uuid_hex_to_bytes(project_id)
@ -441,9 +444,9 @@ class ProjectScopedPayload(BasePayload):
domain_id = None
trust_id = None
federated_info = None
access_token_id = None
return (user_id, methods, project_id, domain_id, expires_at_str,
audit_ids, trust_id, federated_info)
audit_ids, trust_id, federated_info, access_token_id)
class TrustScopedPayload(BasePayload):
@ -455,7 +458,7 @@ class TrustScopedPayload(BasePayload):
@classmethod
def assemble(cls, user_id, methods, project_id, domain_id, expires_at,
audit_ids, trust_id, federated_info):
audit_ids, trust_id, federated_info, access_token_id):
b_user_id = cls.attempt_convert_uuid_hex_to_bytes(user_id)
methods = auth_plugins.convert_method_list_to_integer(methods)
b_project_id = cls.attempt_convert_uuid_hex_to_bytes(project_id)
@ -481,9 +484,9 @@ class TrustScopedPayload(BasePayload):
trust_id = cls.convert_uuid_bytes_to_hex(payload[5])
domain_id = None
federated_info = None
access_token_id = None
return (user_id, methods, project_id, domain_id, expires_at_str,
audit_ids, trust_id, federated_info)
audit_ids, trust_id, federated_info, access_token_id)
class FederatedUnscopedPayload(BasePayload):
@ -506,7 +509,7 @@ class FederatedUnscopedPayload(BasePayload):
@classmethod
def assemble(cls, user_id, methods, project_id, domain_id, expires_at,
audit_ids, trust_id, federated_info):
audit_ids, trust_id, federated_info, access_token_id):
b_user_id = cls.attempt_convert_uuid_hex_to_bytes(user_id)
methods = auth_plugins.convert_method_list_to_integer(methods)
b_group_ids = list(map(cls.pack_group_id,
@ -539,8 +542,9 @@ class FederatedUnscopedPayload(BasePayload):
project_id = None
domain_id = None
trust_id = None
access_token_id = None
return (user_id, methods, project_id, domain_id, expires_at_str,
audit_ids, trust_id, federated_info)
audit_ids, trust_id, federated_info, access_token_id)
class FederatedScopedPayload(FederatedUnscopedPayload):
@ -548,7 +552,7 @@ class FederatedScopedPayload(FederatedUnscopedPayload):
@classmethod
def assemble(cls, user_id, methods, project_id, domain_id, expires_at,
audit_ids, trust_id, federated_info):
audit_ids, trust_id, federated_info, access_token_id):
b_user_id = cls.attempt_convert_uuid_hex_to_bytes(user_id)
methods = auth_plugins.convert_method_list_to_integer(methods)
b_scope_id = cls.attempt_convert_uuid_hex_to_bytes(
@ -590,8 +594,9 @@ class FederatedScopedPayload(FederatedUnscopedPayload):
federated_info = dict(idp_id=idp_id, protocol_id=protocol_id,
group_ids=group_ids)
trust_id = None
access_token_id = None
return (user_id, methods, project_id, domain_id, expires_at_str,
audit_ids, trust_id, federated_info)
audit_ids, trust_id, federated_info, access_token_id)
class FederatedProjectScopedPayload(FederatedScopedPayload):
@ -610,6 +615,49 @@ class FederatedDomainScopedPayload(FederatedScopedPayload):
return kwargs['domain_id'] and kwargs['federated_info']
class OauthScopedPayload(BasePayload):
version = 7
@classmethod
def create_arguments_apply(cls, **kwargs):
return kwargs['access_token_id']
@classmethod
def assemble(cls, user_id, methods, project_id, domain_id, expires_at,
audit_ids, trust_id, federated_info, access_token_id):
b_user_id = cls.attempt_convert_uuid_hex_to_bytes(user_id)
methods = auth_plugins.convert_method_list_to_integer(methods)
b_project_id = cls.attempt_convert_uuid_hex_to_bytes(project_id)
expires_at_int = cls._convert_time_string_to_float(expires_at)
b_audit_ids = list(map(provider.random_urlsafe_str_to_bytes,
audit_ids))
b_access_token_id = cls.attempt_convert_uuid_hex_to_bytes(
access_token_id)
return (b_user_id, methods, b_project_id, b_access_token_id,
expires_at_int, b_audit_ids)
@classmethod
def disassemble(cls, payload):
(is_stored_as_bytes, user_id) = payload[0]
if is_stored_as_bytes:
user_id = cls.convert_uuid_bytes_to_hex(user_id)
methods = auth_plugins.convert_integer_to_method_list(payload[1])
(is_stored_as_bytes, project_id) = payload[2]
if is_stored_as_bytes:
project_id = cls.convert_uuid_bytes_to_hex(project_id)
(is_stored_as_bytes, access_token_id) = payload[3]
if is_stored_as_bytes:
access_token_id = cls.convert_uuid_bytes_to_hex(access_token_id)
expires_at_str = cls._convert_float_to_time_string(payload[4])
audit_ids = list(map(provider.base64_encode, payload[5]))
domain_id = None
trust_id = None
federated_info = None
return (user_id, methods, project_id, domain_id, expires_at_str,
audit_ids, trust_id, federated_info, access_token_id)
# For now, the order of the classes in the following list is important. This
# is because the way they test that the payload applies to them in
# the create_arguments_apply method requires that the previous ones rejected
@ -618,6 +666,7 @@ class FederatedDomainScopedPayload(FederatedScopedPayload):
# TODO(blk-u): Clean up the create_arguments_apply methods so that they don't
# depend on the previous classes then these can be in any order.
PAYLOAD_CLASSES = [
OauthScopedPayload,
TrustScopedPayload,
FederatedProjectScopedPayload,
FederatedDomainScopedPayload,