From e5fd66ca35424108ca0c1234119d57dca85c93f7 Mon Sep 17 00:00:00 2001 From: Alvaro Lopez Garcia <aloga@ifca.unican.es> Date: Wed, 20 Jul 2016 09:31:07 +0200 Subject: [PATCH] oidc: implement client_credentials grant type Change-Id: If1538726cb7e4cb87fad82c5daf50c67b161b52d --- keystoneauth1/identity/__init__.py | 3 + keystoneauth1/identity/v3/__init__.py | 1 + keystoneauth1/identity/v3/oidc.py | 44 ++++++++++++ keystoneauth1/loading/_plugins/identity/v3.py | 12 ++++ .../unit/identity/test_identity_v3_oidc.py | 70 +++++++++++++++++++ keystoneauth1/tests/unit/loading/test_v3.py | 36 ++++++++++ ...c-client-credentials-2be065926ba4b849.yaml | 4 ++ setup.cfg | 1 + 8 files changed, 171 insertions(+) create mode 100644 releasenotes/notes/add-oidc-client-credentials-2be065926ba4b849.yaml diff --git a/keystoneauth1/identity/__init__.py b/keystoneauth1/identity/__init__.py index 74bb988c..18207ce3 100644 --- a/keystoneauth1/identity/__init__.py +++ b/keystoneauth1/identity/__init__.py @@ -37,6 +37,9 @@ Password = generic.Password Token = generic.Token """See :class:`keystoneauth1.identity.generic.Token`""" +V3OidcClientCredentials = oidc.OidcClientCredentials +"""See :class:`keystoneauth1.identity.v3.oidc.OidcClientCredentials`""" + V3OidcPassword = oidc.OidcPassword """See :class:`keystoneauth1.identity.v3.oidc.OidcPassword`""" diff --git a/keystoneauth1/identity/v3/__init__.py b/keystoneauth1/identity/v3/__init__.py index e44f8860..1df5d466 100644 --- a/keystoneauth1/identity/v3/__init__.py +++ b/keystoneauth1/identity/v3/__init__.py @@ -36,6 +36,7 @@ __all__ = ('Auth', 'TokenMethod', 'OidcAuthorizationCode', + 'OidcClientCredentials', 'OidcPassword', 'TOTPMethod', diff --git a/keystoneauth1/identity/v3/oidc.py b/keystoneauth1/identity/v3/oidc.py index b25e693d..a99b9b10 100644 --- a/keystoneauth1/identity/v3/oidc.py +++ b/keystoneauth1/identity/v3/oidc.py @@ -331,6 +331,50 @@ class OidcPassword(_OidcBase): return payload +class OidcClientCredentials(_OidcBase): + """Implementation for OpenID Connect Client Credentials.""" + + grant_type = 'client_credentials' + + @positional(4) + def __init__(self, auth_url, identity_provider, protocol, + client_id, client_secret, + access_token_endpoint=None, + discovery_endpoint=None, + access_token_type='access_token', + **kwargs): + """The OpenID Client Credentials expects the following. + + :param client_id: Client ID used to authenticate + :type username: string + + :param client_secret: Client Secret used to authenticate + :type password: string + """ + super(OidcClientCredentials, self).__init__( + auth_url=auth_url, + identity_provider=identity_provider, + protocol=protocol, + client_id=client_id, + client_secret=client_secret, + access_token_endpoint=access_token_endpoint, + discovery_endpoint=discovery_endpoint, + access_token_type=access_token_type, + **kwargs) + + def get_payload(self, session): + """Get an authorization grant for the client credentials grant type. + + :param session: a session object to send out HTTP requests. + :type session: keystoneauth1.session.Session + + :returns: a python dictionary containing the payload to be exchanged + :rtype: dict + """ + payload = {'scope': self.scope} + return payload + + class OidcAuthorizationCode(_OidcBase): """Implementation for OpenID Connect Authorization Code.""" diff --git a/keystoneauth1/loading/_plugins/identity/v3.py b/keystoneauth1/loading/_plugins/identity/v3.py index fb36a708..3faea15d 100644 --- a/keystoneauth1/loading/_plugins/identity/v3.py +++ b/keystoneauth1/loading/_plugins/identity/v3.py @@ -123,6 +123,18 @@ class _OpenIDConnectBase(loading.BaseFederationLoader): return options +class OpenIDConnectClientCredentials(_OpenIDConnectBase): + + @property + def plugin_class(self): + return identity.V3OidcClientCredentials + + def get_options(self): + options = super(OpenIDConnectClientCredentials, self).get_options() + + return options + + class OpenIDConnectPassword(_OpenIDConnectBase): @property diff --git a/keystoneauth1/tests/unit/identity/test_identity_v3_oidc.py b/keystoneauth1/tests/unit/identity/test_identity_v3_oidc.py index 4489d88e..9bf12181 100644 --- a/keystoneauth1/tests/unit/identity/test_identity_v3_oidc.py +++ b/keystoneauth1/tests/unit/identity/test_identity_v3_oidc.py @@ -180,6 +180,76 @@ class BaseOIDCTests(object): self.session) +class OIDCClientCredentialsTests(BaseOIDCTests, utils.TestCase): + def setUp(self): + super(OIDCClientCredentialsTests, self).setUp() + + self.GRANT_TYPE = 'client_credentials' + + self.plugin = oidc.OidcClientCredentials( + self.AUTH_URL, + self.IDENTITY_PROVIDER, + self.PROTOCOL, + client_id=self.CLIENT_ID, + client_secret=self.CLIENT_SECRET, + access_token_endpoint=self.ACCESS_TOKEN_ENDPOINT, + project_name=self.PROJECT_NAME) + + def test_initial_call_to_get_access_token(self): + """Test initial call, expect JSON access token.""" + # Mock the output that creates the access token + self.requests_mock.post( + self.ACCESS_TOKEN_ENDPOINT, + json=oidc_fixtures.ACCESS_TOKEN_VIA_PASSWORD_RESP) + + # Prep all the values and send the request + scope = 'profile email' + payload = {'grant_type': self.GRANT_TYPE, 'scope': scope} + self.plugin._get_access_token(self.session, payload) + + # Verify the request matches the expected structure + last_req = self.requests_mock.last_request + self.assertEqual(self.ACCESS_TOKEN_ENDPOINT, last_req.url) + self.assertEqual('POST', last_req.method) + encoded_payload = urllib.parse.urlencode(payload) + self.assertEqual(encoded_payload, last_req.body) + + def test_second_call_to_protected_url(self): + """Test subsequent call, expect Keystone token.""" + # Mock the output that creates the keystone token + self.requests_mock.post( + self.FEDERATION_AUTH_URL, + json=oidc_fixtures.UNSCOPED_TOKEN, + headers={'X-Subject-Token': KEYSTONE_TOKEN_VALUE}) + + res = self.plugin._get_keystone_token(self.session, + self.ACCESS_TOKEN) + + # Verify the request matches the expected structure + self.assertEqual(self.FEDERATION_AUTH_URL, res.request.url) + self.assertEqual('POST', res.request.method) + + headers = {'Authorization': 'Bearer ' + self.ACCESS_TOKEN} + self.assertEqual(headers['Authorization'], + res.request.headers['Authorization']) + + def test_end_to_end_workflow(self): + """Test full OpenID Connect workflow.""" + # Mock the output that creates the access token + self.requests_mock.post( + self.ACCESS_TOKEN_ENDPOINT, + json=oidc_fixtures.ACCESS_TOKEN_VIA_PASSWORD_RESP) + + # Mock the output that creates the keystone token + self.requests_mock.post( + self.FEDERATION_AUTH_URL, + json=oidc_fixtures.UNSCOPED_TOKEN, + headers={'X-Subject-Token': KEYSTONE_TOKEN_VALUE}) + + response = self.plugin.get_unscoped_auth_ref(self.session) + self.assertEqual(KEYSTONE_TOKEN_VALUE, response.auth_token) + + class OIDCPasswordTests(BaseOIDCTests, utils.TestCase): def setUp(self): super(OIDCPasswordTests, self).setUp() diff --git a/keystoneauth1/tests/unit/loading/test_v3.py b/keystoneauth1/tests/unit/loading/test_v3.py index 090e38f5..c83e9ce0 100644 --- a/keystoneauth1/tests/unit/loading/test_v3.py +++ b/keystoneauth1/tests/unit/loading/test_v3.py @@ -143,6 +143,42 @@ class OpenIDConnectBaseTests(object): self.assertIn('scope', [o.dest for o in options]) +class OpenIDConnectClientCredentialsTests(OpenIDConnectBaseTests, + utils.TestCase): + + plugin_name = "v3oidcclientcredentials" + + def test_options(self): + options = loading.get_plugin_loader(self.plugin_name).get_options() + self.assertTrue( + set(['openid-scope']).issubset( + set([o.name for o in options])) + ) + + def test_basic(self): + access_token_endpoint = uuid.uuid4().hex + scope = uuid.uuid4().hex + identity_provider = uuid.uuid4().hex + protocol = uuid.uuid4().hex + scope = uuid.uuid4().hex + client_id = uuid.uuid4().hex + client_secret = uuid.uuid4().hex + + oidc = self.create(identity_provider=identity_provider, + protocol=protocol, + access_token_endpoint=access_token_endpoint, + client_id=client_id, + client_secret=client_secret, + scope=scope) + + self.assertEqual(scope, oidc.scope) + self.assertEqual(identity_provider, oidc.identity_provider) + self.assertEqual(protocol, oidc.protocol) + self.assertEqual(access_token_endpoint, oidc.access_token_endpoint) + self.assertEqual(client_id, oidc.client_id) + self.assertEqual(client_secret, oidc.client_secret) + + class OpenIDConnectPasswordTests(OpenIDConnectBaseTests, utils.TestCase): plugin_name = "v3oidcpassword" diff --git a/releasenotes/notes/add-oidc-client-credentials-2be065926ba4b849.yaml b/releasenotes/notes/add-oidc-client-credentials-2be065926ba4b849.yaml new file mode 100644 index 00000000..a5dadd77 --- /dev/null +++ b/releasenotes/notes/add-oidc-client-credentials-2be065926ba4b849.yaml @@ -0,0 +1,4 @@ +--- +features: + - Add support for the Client Credentials OpenID Connect + grant type. diff --git a/setup.cfg b/setup.cfg index eb71d93a..07cc12ea 100644 --- a/setup.cfg +++ b/setup.cfg @@ -45,6 +45,7 @@ keystoneauth1.plugin = v2token = keystoneauth1.loading._plugins.identity.v2:Token v3password = keystoneauth1.loading._plugins.identity.v3:Password v3token = keystoneauth1.loading._plugins.identity.v3:Token + v3oidcclientcredentials = keystoneauth1.loading._plugins.identity.v3:OpenIDConnectClientCredentials v3oidcpassword = keystoneauth1.loading._plugins.identity.v3:OpenIDConnectPassword v3oidcauthcode = keystoneauth1.loading._plugins.identity.v3:OpenIDConnectAuthorizationCode v3oidcaccesstoken = keystoneauth1.loading._plugins.identity.v3:OpenIDConnectAccessToken