oidc: implement client_credentials grant type

Change-Id: If1538726cb7e4cb87fad82c5daf50c67b161b52d
This commit is contained in:
Alvaro Lopez Garcia 2016-07-20 09:31:07 +02:00
parent b6b2b12ef9
commit e5fd66ca35
8 changed files with 171 additions and 0 deletions

View File

@ -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`"""

View File

@ -36,6 +36,7 @@ __all__ = ('Auth',
'TokenMethod',
'OidcAuthorizationCode',
'OidcClientCredentials',
'OidcPassword',
'TOTPMethod',

View File

@ -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."""

View File

@ -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

View File

@ -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()

View File

@ -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"

View File

@ -0,0 +1,4 @@
---
features:
- Add support for the Client Credentials OpenID Connect
grant type.

View File

@ -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