oidc: add OidcAccessToken class to authenticate reusing an access token
Some services or users may have obtained an access token, so it would be possible to authenticate using this token directly (for example a service where the user has already logged in). This new class makes possible to use an access token to authenticate directly with Keystone, exchanging it for a Keystone token. Closes-bug: 1583780 Change-Id: I5a31270194a3d1aa48de709dba49afde460731e2
This commit is contained in:
parent
1c07cddcb2
commit
553a523830
|
@ -43,6 +43,9 @@ V3OidcPassword = oidc.OidcPassword
|
|||
V3OidcAuthorizationCode = oidc.OidcAuthorizationCode
|
||||
"""See :class:`keystoneauth1.identity.v3.oidc.OidcAuthorizationCode`"""
|
||||
|
||||
V3OidcAccessToken = oidc.OidcAccessToken
|
||||
"""See :class:`keystoneauth1.identity.v3.oidc.OidcAccessToken`"""
|
||||
|
||||
__all__ = ('BaseIdentityPlugin',
|
||||
'Password',
|
||||
'Token',
|
||||
|
@ -51,4 +54,5 @@ __all__ = ('BaseIdentityPlugin',
|
|||
'V3Password',
|
||||
'V3Token',
|
||||
'V3OidcPassword',
|
||||
'V3OidcAuthorizationCode')
|
||||
'V3OidcAuthorizationCode',
|
||||
'V3OidcAccessToken')
|
||||
|
|
|
@ -16,7 +16,8 @@ from keystoneauth1 import access
|
|||
from keystoneauth1.identity.v3 import federation
|
||||
|
||||
__all__ = ('OidcAuthorizationCode',
|
||||
'OidcPassword')
|
||||
'OidcPassword',
|
||||
'OidcAccessToken')
|
||||
|
||||
|
||||
class _OidcBase(federation.FederationBaseAuth):
|
||||
|
@ -243,3 +244,56 @@ class OidcAuthorizationCode(_OidcBase):
|
|||
|
||||
# grab the unscoped token
|
||||
return access.create(resp=response)
|
||||
|
||||
|
||||
class OidcAccessToken(_OidcBase):
|
||||
"""Implementation for OpenID Connect access token reuse."""
|
||||
|
||||
@positional(5)
|
||||
def __init__(self, auth_url, identity_provider, protocol,
|
||||
access_token, **kwargs):
|
||||
"""The OpenID Connect plugin based on the Access Token.
|
||||
|
||||
It expects the following:
|
||||
|
||||
:param auth_url: URL of the Identity Service
|
||||
:type auth_url: string
|
||||
|
||||
:param identity_provider: Name of the Identity Provider the client
|
||||
will authenticate against
|
||||
:type identity_provider: string
|
||||
|
||||
:param protocol: Protocol name as configured in keystone
|
||||
:type protocol: string
|
||||
|
||||
:param access_token: OpenID Connect Access token
|
||||
:type access_token: string
|
||||
"""
|
||||
super(OidcAccessToken, self).__init__(auth_url, identity_provider,
|
||||
protocol,
|
||||
client_id=None,
|
||||
client_secret=None,
|
||||
access_token_endpoint=None,
|
||||
grant_type=None,
|
||||
access_token_type=None,
|
||||
**kwargs)
|
||||
self.access_token = access_token
|
||||
|
||||
def get_unscoped_auth_ref(self, session):
|
||||
"""Authenticate with OpenID Connect and get back claims.
|
||||
|
||||
We exchange the access token upon accessing the protected Keystone
|
||||
endpoint (federated auth URL). This will trigger the OpenID Connect
|
||||
Provider to perform a user introspection and retrieve information
|
||||
(specified in the scope) about the user in the form of an OpenID
|
||||
Connect Claim. These claims will be sent to Keystone in the form of
|
||||
environment variables.
|
||||
|
||||
:param session: a session object to send out HTTP requests.
|
||||
:type session: keystoneclient.session.Session
|
||||
|
||||
:returns: a token data representation
|
||||
:rtype: :py:class:`keystoneauth1.access.AccessInfoV3`
|
||||
"""
|
||||
response = self._get_keystone_token(session, self.access_token)
|
||||
return access.create(resp=response)
|
||||
|
|
|
@ -123,3 +123,20 @@ class OpenIDConnectAuthorizationCode(_OpenIDConnectBase):
|
|||
])
|
||||
|
||||
return options
|
||||
|
||||
|
||||
class OpenIDConnectAcessToken(loading.BaseFederationLoader):
|
||||
|
||||
@property
|
||||
def plugin_class(self):
|
||||
return identity.V3OidcAccessToken
|
||||
|
||||
@classmethod
|
||||
def get_options(cls):
|
||||
options = super(OpenIDConnectAcessToken, cls).get_options()
|
||||
|
||||
options.extend([
|
||||
loading.StrOpt('access-token', secret=True,
|
||||
help='OAuth 2.0 Access Token'),
|
||||
])
|
||||
return options
|
||||
|
|
|
@ -36,6 +36,7 @@ class AuthenticateOIDCTests(utils.TestCase):
|
|||
self.PASSWORD = uuid.uuid4().hex
|
||||
self.CLIENT_ID = uuid.uuid4().hex
|
||||
self.CLIENT_SECRET = uuid.uuid4().hex
|
||||
self.ACCESS_TOKEN = uuid.uuid4().hex
|
||||
self.ACCESS_TOKEN_ENDPOINT = 'https://localhost:8020/oidc/token'
|
||||
self.FEDERATION_AUTH_URL = '%s/%s' % (
|
||||
self.AUTH_URL,
|
||||
|
@ -63,6 +64,12 @@ class AuthenticateOIDCTests(utils.TestCase):
|
|||
redirect_uri=self.REDIRECT_URL,
|
||||
code=self.CODE)
|
||||
|
||||
self.oidc_token = oidc.OidcAccessToken(
|
||||
self.AUTH_URL,
|
||||
self.IDENTITY_PROVIDER,
|
||||
self.PROTOCOL,
|
||||
access_token=self.ACCESS_TOKEN)
|
||||
|
||||
|
||||
class OIDCPasswordTests(AuthenticateOIDCTests):
|
||||
|
||||
|
@ -95,16 +102,14 @@ class OIDCPasswordTests(AuthenticateOIDCTests):
|
|||
json=oidc_fixtures.UNSCOPED_TOKEN,
|
||||
headers={'X-Subject-Token': KEYSTONE_TOKEN_VALUE})
|
||||
|
||||
# Prep all the values and send the request
|
||||
access_token = uuid.uuid4().hex
|
||||
res = self.oidc_password._get_keystone_token(self.session,
|
||||
access_token)
|
||||
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 ' + access_token}
|
||||
headers = {'Authorization': 'Bearer ' + self.ACCESS_TOKEN}
|
||||
self.assertEqual(headers['Authorization'],
|
||||
res.request.headers['Authorization'])
|
||||
|
||||
|
@ -147,3 +152,17 @@ class OIDCAuthorizationGrantTests(AuthenticateOIDCTests):
|
|||
self.assertEqual('POST', last_req.method)
|
||||
encoded_payload = urllib.parse.urlencode(payload)
|
||||
self.assertEqual(encoded_payload, last_req.body)
|
||||
|
||||
|
||||
class AuthenticateOIDCTokenTests(AuthenticateOIDCTests):
|
||||
|
||||
def test_end_to_end_workflow(self):
|
||||
"""Test full OpenID Connect workflow."""
|
||||
# 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.oidc_token.get_unscoped_auth_ref(self.session)
|
||||
self.assertEqual(KEYSTONE_TOKEN_VALUE, response.auth_token)
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
features:
|
||||
- Added a new OidcAccessToken plugin, accessible via the 'v3oidcaccesstoken'
|
||||
entry point, making possible to authenticate using an existing OpenID
|
||||
Connect Access token.
|
||||
fixes:
|
||||
- >
|
||||
[`bug 1583780 <https://bugs.launchpad.net/keystoneauth/+bug/1583780>`_]
|
||||
OpenID connect support should include authenticating using directly an access token.
|
|
@ -46,6 +46,7 @@ keystoneauth1.plugin =
|
|||
v3token = keystoneauth1.loading._plugins.identity.v3:Token
|
||||
v3oidcpassword = keystoneauth1.loading._plugins.identity.v3:OpenIDConnectPassword
|
||||
v3oidcauthcode = keystoneauth1.loading._plugins.identity.v3:OpenIDConnectAuthorizationCode
|
||||
v3oidcaccesstoken = keystoneauth1.loading._plugins.identity.v3:OpenIDConnectAccessToken
|
||||
v3oauth1 = keystoneauth1.extras.oauth1._loading:V3OAuth1
|
||||
|
||||
[build_sphinx]
|
||||
|
|
Loading…
Reference in New Issue