Skip to content

Commit

Permalink
oidc: implement client_credentials grant type
Browse files Browse the repository at this point in the history
Change-Id: If1538726cb7e4cb87fad82c5daf50c67b161b52d
  • Loading branch information
alvarolopez committed Jul 20, 2016
1 parent b6b2b12 commit e5fd66c
Show file tree
Hide file tree
Showing 8 changed files with 171 additions and 0 deletions.
3 changes: 3 additions & 0 deletions keystoneauth1/identity/__init__.py
Expand Up @@ -37,6 +37,9 @@
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`"""

Expand Down
1 change: 1 addition & 0 deletions keystoneauth1/identity/v3/__init__.py
Expand Up @@ -36,6 +36,7 @@
'TokenMethod',

'OidcAuthorizationCode',
'OidcClientCredentials',
'OidcPassword',

'TOTPMethod',
Expand Down
44 changes: 44 additions & 0 deletions keystoneauth1/identity/v3/oidc.py
Expand Up @@ -331,6 +331,50 @@ def get_payload(self, session):
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."""

Expand Down
12 changes: 12 additions & 0 deletions keystoneauth1/loading/_plugins/identity/v3.py
Expand Up @@ -123,6 +123,18 @@ def get_options(self):
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
Expand Down
70 changes: 70 additions & 0 deletions keystoneauth1/tests/unit/identity/test_identity_v3_oidc.py
Expand Up @@ -180,6 +180,76 @@ def test_wrong_grant_type(self):
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()
Expand Down
36 changes: 36 additions & 0 deletions keystoneauth1/tests/unit/loading/test_v3.py
Expand Up @@ -143,6 +143,42 @@ def test_base_options_are_there(self):
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"
Expand Down
@@ -0,0 +1,4 @@
---
features:
- Add support for the Client Credentials OpenID Connect
grant type.
1 change: 1 addition & 0 deletions setup.cfg
Expand Up @@ -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
Expand Down

0 comments on commit e5fd66c

Please sign in to comment.