OAuth 2.0 Mutual-TLS Support
Added a new OAuth2mTlsClientCredential plugin, accessible via the 'v3oauth2mtlsclientcredential' entry point, making possible to authenticate using an OAuth 2.0 Mutual-TLS client credentials. Co-Authored-By: Hiromu Asahina <hiromu.asahina.az@hco.ntt.co.jp> Change-Id: I0e02ef18da5d60cdd1bcde07b07c2071b74b73d6 Implements: blueprint support-oauth2-mtls
This commit is contained in:
parent
6ee21bd722
commit
ca28df8480
@ -782,3 +782,11 @@ class AccessInfoV3(AccessInfo):
|
||||
@_missingproperty
|
||||
def bind(self):
|
||||
return self._data['token']['bind']
|
||||
|
||||
@property
|
||||
def oauth2_credential(self):
|
||||
return self._data['token']['oauth2_credential']
|
||||
|
||||
@_missingproperty
|
||||
def oauth2_credential_thumbprint(self):
|
||||
return self._data['token']['oauth2_credential']['x5t#S256']
|
||||
|
@ -65,7 +65,8 @@ class Token(dict):
|
||||
application_credential_access_rules=None,
|
||||
oauth_access_token_id=None, oauth_consumer_id=None,
|
||||
audit_id=None, audit_chain_id=None,
|
||||
is_admin_project=None, project_is_domain=None):
|
||||
is_admin_project=None, project_is_domain=None,
|
||||
oauth2_thumbprint=None):
|
||||
super(Token, self).__init__()
|
||||
|
||||
self.user_id = user_id or uuid.uuid4().hex
|
||||
@ -129,6 +130,9 @@ class Token(dict):
|
||||
if is_admin_project is not None:
|
||||
self.is_admin_project = is_admin_project
|
||||
|
||||
if oauth2_thumbprint:
|
||||
self.oauth2_thumbprint = oauth2_thumbprint
|
||||
|
||||
@property
|
||||
def root(self):
|
||||
return self.setdefault('token', {})
|
||||
@ -395,6 +399,18 @@ class Token(dict):
|
||||
def is_admin_project(self):
|
||||
self.root.pop('is_admin_project', None)
|
||||
|
||||
@property
|
||||
def oauth2_thumbprint(self):
|
||||
return self.root.get('oauth2_credential', {}).get('x5t#S256')
|
||||
|
||||
@oauth2_thumbprint.setter
|
||||
def oauth2_thumbprint(self, value):
|
||||
self.root.setdefault('oauth2_credential', {})['x5t#S256'] = value
|
||||
|
||||
@property
|
||||
def oauth2_credential(self):
|
||||
return self.root.get('oauth2_credential')
|
||||
|
||||
def validate(self):
|
||||
project = self.root.get('project')
|
||||
domain = self.root.get('domain')
|
||||
|
@ -64,6 +64,9 @@ V3MultiFactor = v3.MultiFactor
|
||||
V3OAuth2ClientCredential = v3.OAuth2ClientCredential
|
||||
"""See :class:`keystoneauth1.identity.v3.OAuth2ClientCredential`"""
|
||||
|
||||
V3OAuth2mTlsClientCredential = v3.OAuth2mTlsClientCredential
|
||||
"""See :class:`keystoneauth1.identity.v3.OAuth2mTlsClientCredential`"""
|
||||
|
||||
__all__ = ('BaseIdentityPlugin',
|
||||
'Password',
|
||||
'Token',
|
||||
@ -78,4 +81,5 @@ __all__ = ('BaseIdentityPlugin',
|
||||
'V3TokenlessAuth',
|
||||
'V3ApplicationCredential',
|
||||
'V3MultiFactor',
|
||||
'V3OAuth2ClientCredential')
|
||||
'V3OAuth2ClientCredential',
|
||||
'V3OAuth2mTlsClientCredential')
|
||||
|
@ -24,6 +24,7 @@ from keystoneauth1.identity.v3.token import * # noqa
|
||||
from keystoneauth1.identity.v3.totp import * # noqa
|
||||
from keystoneauth1.identity.v3.tokenless_auth import * # noqa
|
||||
from keystoneauth1.identity.v3.oauth2_client_credential import * # noqa
|
||||
from keystoneauth1.identity.v3.oauth2_mtls_client_credential import * # noqa
|
||||
|
||||
|
||||
__all__ = ('ApplicationCredential',
|
||||
@ -59,4 +60,7 @@ __all__ = ('ApplicationCredential',
|
||||
'MultiFactor',
|
||||
|
||||
'OAuth2ClientCredential',
|
||||
'OAuth2ClientCredentialMethod',)
|
||||
'OAuth2ClientCredentialMethod',
|
||||
|
||||
'OAuth2mTlsClientCredential',
|
||||
)
|
||||
|
125
keystoneauth1/identity/v3/oauth2_mtls_client_credential.py
Normal file
125
keystoneauth1/identity/v3/oauth2_mtls_client_credential.py
Normal file
@ -0,0 +1,125 @@
|
||||
# Copyright 2022 OpenStack Foundation
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import abc
|
||||
|
||||
import six
|
||||
|
||||
from keystoneauth1 import access
|
||||
from keystoneauth1 import exceptions
|
||||
from keystoneauth1.identity.v3 import base
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class OAuth2mTlsClientCredential(base.BaseAuth):
|
||||
"""A plugin for authenticating via an OAuth2.0 mTLS client credential.
|
||||
|
||||
:param string auth_url: keystone authorization endpoint.
|
||||
:param string oauth2_endpoint: OAuth2.0 endpoint.
|
||||
:param string oauth2_client_id: OAuth2.0 client credential id.
|
||||
"""
|
||||
|
||||
def __init__(self, auth_url, oauth2_endpoint, oauth2_client_id,
|
||||
*args, **kwargs):
|
||||
super(OAuth2mTlsClientCredential, self).__init__(
|
||||
auth_url, *args, **kwargs)
|
||||
self.auth_url = auth_url
|
||||
self.oauth2_endpoint = oauth2_endpoint
|
||||
self.oauth2_client_id = oauth2_client_id
|
||||
self.oauth2_access_token = None
|
||||
|
||||
def get_auth_ref(self, session, **kwargs):
|
||||
"""Obtain a token from an OpenStack Identity Service.
|
||||
|
||||
This method is overridden by the various token version plugins.
|
||||
|
||||
This function should not be called independently and is expected to be
|
||||
invoked via the do_authenticate function.
|
||||
|
||||
This function will be invoked if the AcessInfo object cached by the
|
||||
plugin is not valid. Thus plugins should always fetch a new AccessInfo
|
||||
when invoked. If you are looking to just retrieve the current auth
|
||||
data then you should use get_access.
|
||||
|
||||
:param session: A session object that can be used for communication.
|
||||
:type session: keystoneauth1.session.Session
|
||||
|
||||
:raises keystoneauth1.exceptions.response.InvalidResponse:
|
||||
The response returned wasn't appropriate.
|
||||
:raises keystoneauth1.exceptions.http.HttpError:
|
||||
An error from an invalid HTTP response.
|
||||
:raises keystoneauth1.exceptions.ClientException:
|
||||
An error from getting OAuth2.0 access token.
|
||||
|
||||
:returns: Token access information.
|
||||
:rtype: :class:`keystoneauth1.access.AccessInfo`
|
||||
"""
|
||||
# Get OAuth2.0 access token and add the field 'Authorization' when
|
||||
# using the HTTPS protocol.
|
||||
data = {'grant_type': 'client_credentials',
|
||||
'client_id': self.oauth2_client_id}
|
||||
resp = session.post(url=self.oauth2_endpoint,
|
||||
authenticated=False,
|
||||
raise_exc=False,
|
||||
data=data)
|
||||
if resp.status_code == 200:
|
||||
oauth2 = resp.json()
|
||||
self.oauth2_access_token = oauth2.get('access_token')
|
||||
else:
|
||||
error = resp.json()
|
||||
msg = error.get('error_description')
|
||||
raise exceptions.ClientException(msg)
|
||||
|
||||
headers = {'Accept': 'application/json',
|
||||
'X-Auth-Token': self.oauth2_access_token,
|
||||
'X-Subject-Token': self.oauth2_access_token}
|
||||
|
||||
token_url = '%s/auth/tokens' % self.auth_url.rstrip('/')
|
||||
if not self.auth_url.rstrip('/').endswith('v3'):
|
||||
token_url = '%s/v3/auth/tokens' % self.auth_url.rstrip('/')
|
||||
resp = session.get(url=token_url,
|
||||
authenticated=False,
|
||||
headers=headers,
|
||||
log=False)
|
||||
try:
|
||||
resp_data = resp.json()
|
||||
except ValueError:
|
||||
raise exceptions.InvalidResponse(response=resp)
|
||||
if 'token' not in resp_data:
|
||||
raise exceptions.InvalidResponse(response=resp)
|
||||
|
||||
return access.AccessInfoV3(auth_token=self.oauth2_access_token,
|
||||
body=resp_data)
|
||||
|
||||
def get_headers(self, session, **kwargs):
|
||||
"""Fetch authentication headers for message.
|
||||
|
||||
:param session: The session object that the auth_plugin belongs to.
|
||||
:type session: keystoneauth1.session.Session
|
||||
|
||||
:returns: Headers that are set to authenticate a message or None for
|
||||
failure. Note that when checking this value that the empty
|
||||
dict is a valid, non-failure response.
|
||||
:rtype: dict
|
||||
"""
|
||||
# get headers for X-Auth-Token
|
||||
headers = super(OAuth2mTlsClientCredential, self).get_headers(
|
||||
session, **kwargs)
|
||||
|
||||
# add OAuth2.0 access token to the headers
|
||||
if headers:
|
||||
headers['Authorization'] = f'Bearer {self.oauth2_access_token}'
|
||||
else:
|
||||
headers = {'Authorization': f'Bearer {self.oauth2_access_token}'}
|
||||
return headers
|
@ -379,3 +379,34 @@ class OAuth2ClientCredential(loading.BaseV3Loader):
|
||||
raise exceptions.OptionError(m)
|
||||
|
||||
return super(OAuth2ClientCredential, self).load_from_options(**kwargs)
|
||||
|
||||
|
||||
class OAuth2mTlsClientCredential(loading.BaseV3Loader):
|
||||
|
||||
@property
|
||||
def plugin_class(self):
|
||||
return identity.V3OAuth2mTlsClientCredential
|
||||
|
||||
def get_options(self):
|
||||
options = super(OAuth2mTlsClientCredential, self).get_options()
|
||||
options.extend([
|
||||
loading.Opt('oauth2-endpoint',
|
||||
required=True,
|
||||
help='Endpoint for OAuth2.0 Mutual-TLS Authorization'),
|
||||
loading.Opt('oauth2-client-id',
|
||||
required=True,
|
||||
help='Client credential ID for OAuth2.0 Mutual-TLS '
|
||||
'Authorization')
|
||||
])
|
||||
return options
|
||||
|
||||
def load_from_options(self, **kwargs):
|
||||
if not kwargs.get('oauth2_endpoint'):
|
||||
m = 'You must provide an OAuth2.0 Mutual-TLS endpoint.'
|
||||
raise exceptions.OptionError(m)
|
||||
if not kwargs.get('oauth2_client_id'):
|
||||
m = ('You must provide an client credential ID for '
|
||||
'OAuth2.0 Mutual-TLS Authorization.')
|
||||
raise exceptions.OptionError(m)
|
||||
return super(OAuth2mTlsClientCredential,
|
||||
self).load_from_options(**kwargs)
|
||||
|
@ -225,6 +225,37 @@ class V3IdentityPlugin(utils.TestCase):
|
||||
"application_credential_restricted": True
|
||||
},
|
||||
}
|
||||
self.TEST_OAUTH2_MTLS_TOKEN_RESPONSE = {
|
||||
"token": {
|
||||
"methods": [
|
||||
"oauth2_credential"
|
||||
],
|
||||
|
||||
"expires_at": "%i-02-01T00:00:10.000123Z" % nextyear,
|
||||
"project": {
|
||||
"domain": {
|
||||
"id": self.TEST_DOMAIN_ID,
|
||||
"name": self.TEST_DOMAIN_NAME
|
||||
},
|
||||
"id": self.TEST_TENANT_ID,
|
||||
"name": self.TEST_TENANT_NAME
|
||||
},
|
||||
"user": {
|
||||
"domain": {
|
||||
"id": self.TEST_DOMAIN_ID,
|
||||
"name": self.TEST_DOMAIN_NAME
|
||||
},
|
||||
"id": self.TEST_USER,
|
||||
"name": self.TEST_USER
|
||||
},
|
||||
"issued_at": "2013-05-29T16:55:21.468960Z",
|
||||
"catalog": self.TEST_SERVICE_CATALOG,
|
||||
"service_providers": self.TEST_SERVICE_PROVIDERS,
|
||||
"oauth2_credential": {
|
||||
"x5t#S256": "7UN-z4yFIm9s4jakecGoKa4rc353pDCuFUo9fsDD_1s="
|
||||
}
|
||||
},
|
||||
}
|
||||
self.TEST_RECEIPT_RESPONSE = {
|
||||
"receipt": {
|
||||
"methods": ["password"],
|
||||
@ -1085,3 +1116,215 @@ class V3IdentityPlugin(utils.TestCase):
|
||||
f'Bearer {oauth2_token}')
|
||||
self.assertEqual(200, resp.status_code)
|
||||
self.assertEqual(resp_text, resp.text)
|
||||
|
||||
def test_oauth2_mtls_client_credential_method(self):
|
||||
base_https = self.TEST_URL.replace('http:', 'https:')
|
||||
token_endpoint = f'{self.TEST_URL}/auth/tokens'
|
||||
oauth2_endpoint = f'{base_https}/OS-OAUTH2/token'
|
||||
oauth2_token = 'HW9bB6oYWJywz6mAN_KyIBXlof15Pk'
|
||||
a = v3.OAuth2mTlsClientCredential(
|
||||
self.TEST_URL,
|
||||
oauth2_endpoint=oauth2_endpoint,
|
||||
oauth2_client_id=self.TEST_CLIENT_CRED_ID
|
||||
)
|
||||
|
||||
oauth2_post_resp = {
|
||||
'status_code': 200,
|
||||
'json': {
|
||||
'access_token': oauth2_token,
|
||||
'expires_in': 3600,
|
||||
'token_type': 'Bearer'
|
||||
}
|
||||
}
|
||||
self.requests_mock.post(oauth2_endpoint, [oauth2_post_resp])
|
||||
token_verify_resp = {
|
||||
'status_code': 200,
|
||||
'json': {
|
||||
**self.TEST_OAUTH2_MTLS_TOKEN_RESPONSE
|
||||
}
|
||||
}
|
||||
self.requests_mock.get(token_endpoint, [token_verify_resp])
|
||||
|
||||
sess = session.Session(auth=a)
|
||||
auth_ref = a.get_auth_ref(sess)
|
||||
self.assertEqual(auth_ref.auth_token, oauth2_token)
|
||||
self.assertEqual(auth_ref._data,
|
||||
self.TEST_OAUTH2_MTLS_TOKEN_RESPONSE)
|
||||
self.assertEqual(
|
||||
auth_ref.project_id,
|
||||
self.TEST_OAUTH2_MTLS_TOKEN_RESPONSE.get(
|
||||
'token', {}).get('project', {}).get('id'))
|
||||
self.assertIsNone(auth_ref.domain_id)
|
||||
self.assertEqual(
|
||||
auth_ref.oauth2_credential,
|
||||
self.TEST_OAUTH2_MTLS_TOKEN_RESPONSE.get(
|
||||
'token', {}).get('oauth2_credential'))
|
||||
self.assertEqual(
|
||||
auth_ref.oauth2_credential_thumbprint,
|
||||
self.TEST_OAUTH2_MTLS_TOKEN_RESPONSE.get(
|
||||
'token', {}).get('oauth2_credential', {}).get('x5t#S256')
|
||||
)
|
||||
|
||||
auth_head = sess.get_auth_headers()
|
||||
self.assertEqual(f'Bearer {oauth2_token}', auth_head['Authorization'])
|
||||
self.assertEqual(oauth2_token, auth_head['X-Auth-Token'])
|
||||
|
||||
def test_oauth2_mtls_client_credential_method_without_v3(self):
|
||||
base_https = self.TEST_URL.replace('http:', 'https:')
|
||||
token_endpoint = f'{self.TEST_URL}/auth/tokens'
|
||||
oauth2_endpoint = f'{base_https}/OS-OAUTH2/token'
|
||||
oauth2_token = 'HW9bB6oYWJywz6mAN_KyIBXlof15Pk'
|
||||
a = v3.OAuth2mTlsClientCredential(
|
||||
self.TEST_URL.replace('v3', ''),
|
||||
oauth2_endpoint=oauth2_endpoint,
|
||||
oauth2_client_id=self.TEST_CLIENT_CRED_ID
|
||||
)
|
||||
|
||||
oauth2_post_resp = {
|
||||
'status_code': 200,
|
||||
'json': {
|
||||
'access_token': oauth2_token,
|
||||
'expires_in': 3600,
|
||||
'token_type': 'Bearer'
|
||||
}
|
||||
}
|
||||
self.requests_mock.post(oauth2_endpoint, [oauth2_post_resp])
|
||||
token_verify_resp = {
|
||||
'status_code': 200,
|
||||
'json': {
|
||||
**self.TEST_OAUTH2_MTLS_TOKEN_RESPONSE
|
||||
}
|
||||
}
|
||||
self.requests_mock.get(token_endpoint, [token_verify_resp])
|
||||
|
||||
sess = session.Session(auth=a)
|
||||
auth_ref = a.get_auth_ref(sess)
|
||||
self.assertEqual(auth_ref.auth_token, oauth2_token)
|
||||
self.assertEqual(auth_ref._data,
|
||||
self.TEST_OAUTH2_MTLS_TOKEN_RESPONSE)
|
||||
self.assertEqual(
|
||||
auth_ref.oauth2_credential,
|
||||
self.TEST_OAUTH2_MTLS_TOKEN_RESPONSE.get(
|
||||
'token', {}).get('oauth2_credential'))
|
||||
self.assertEqual(
|
||||
auth_ref.oauth2_credential_thumbprint,
|
||||
self.TEST_OAUTH2_MTLS_TOKEN_RESPONSE.get(
|
||||
'token', {}).get('oauth2_credential', {}).get('x5t#S256')
|
||||
)
|
||||
auth_head = sess.get_auth_headers()
|
||||
self.assertEqual(f'Bearer {oauth2_token}', auth_head['Authorization'])
|
||||
self.assertEqual(oauth2_token, auth_head['X-Auth-Token'])
|
||||
|
||||
def test_oauth2_mtls_client_credential_method_resp_invalid_json(self):
|
||||
base_https = self.TEST_URL.replace('http:', 'https:')
|
||||
token_endpoint = f'{self.TEST_URL}/auth/tokens'
|
||||
oauth2_endpoint = f'{base_https}/OS-OAUTH2/token'
|
||||
oauth2_token = 'HW9bB6oYWJywz6mAN_KyIBXlof15Pk'
|
||||
a = v3.OAuth2mTlsClientCredential(
|
||||
self.TEST_URL,
|
||||
oauth2_endpoint=oauth2_endpoint,
|
||||
oauth2_client_id=self.TEST_CLIENT_CRED_ID
|
||||
)
|
||||
|
||||
oauth2_post_resp = {
|
||||
'status_code': 200,
|
||||
'json': {
|
||||
'access_token': oauth2_token,
|
||||
'expires_in': 3600,
|
||||
'token_type': 'Bearer'
|
||||
}
|
||||
}
|
||||
self.requests_mock.post(oauth2_endpoint, [oauth2_post_resp])
|
||||
token_verify_resp = {
|
||||
'status_code': 200,
|
||||
'text': 'invalid json'
|
||||
}
|
||||
self.requests_mock.get(token_endpoint, [token_verify_resp])
|
||||
|
||||
sess = session.Session(auth=a)
|
||||
self.assertRaises(exceptions.InvalidResponse, a.get_auth_ref, sess)
|
||||
|
||||
def test_oauth2_mtls_client_credential_method_resp_without_token(self):
|
||||
base_https = self.TEST_URL.replace('http:', 'https:')
|
||||
token_endpoint = f'{self.TEST_URL}/auth/tokens'
|
||||
oauth2_endpoint = f'{base_https}/OS-OAUTH2/token'
|
||||
oauth2_token = 'HW9bB6oYWJywz6mAN_KyIBXlof15Pk'
|
||||
a = v3.OAuth2mTlsClientCredential(
|
||||
self.TEST_URL,
|
||||
oauth2_endpoint=oauth2_endpoint,
|
||||
oauth2_client_id=self.TEST_CLIENT_CRED_ID
|
||||
)
|
||||
|
||||
oauth2_post_resp = {
|
||||
'status_code': 200,
|
||||
'json': {
|
||||
'access_token': oauth2_token,
|
||||
'expires_in': 3600,
|
||||
'token_type': 'Bearer'
|
||||
}
|
||||
}
|
||||
self.requests_mock.post(oauth2_endpoint, [oauth2_post_resp])
|
||||
token_verify_resp = {
|
||||
'status_code': 200,
|
||||
'json': {
|
||||
'without_token': {}
|
||||
}
|
||||
}
|
||||
self.requests_mock.get(token_endpoint, [token_verify_resp])
|
||||
|
||||
sess = session.Session(auth=a)
|
||||
self.assertRaises(exceptions.InvalidResponse, a.get_auth_ref, sess)
|
||||
|
||||
def test_oauth2_mtls_client_credential_method_client_exception(self):
|
||||
base_https = self.TEST_URL.replace('http:', 'https:')
|
||||
oauth2_endpoint = f'{base_https}/OS-OAUTH2/token'
|
||||
a = v3.OAuth2mTlsClientCredential(
|
||||
self.TEST_URL,
|
||||
oauth2_endpoint=oauth2_endpoint,
|
||||
oauth2_client_id=self.TEST_CLIENT_CRED_ID
|
||||
)
|
||||
|
||||
oauth2_post_resp = {
|
||||
'status_code': 400,
|
||||
'json': {}
|
||||
}
|
||||
self.requests_mock.post(oauth2_endpoint, [oauth2_post_resp])
|
||||
|
||||
sess = session.Session(auth=a)
|
||||
self.assertRaises(exceptions.ClientException, a.get_auth_ref, sess)
|
||||
|
||||
def test_oauth2_mtls_client_credential_method_base_header_none(self):
|
||||
base_https = self.TEST_URL.replace('http:', 'https:')
|
||||
token_endpoint = f'{self.TEST_URL}/auth/tokens'
|
||||
oauth2_endpoint = f'{base_https}/OS-OAUTH2/token'
|
||||
oauth2_token = 'HW9bB6oYWJywz6mAN_KyIBXlof15Pk'
|
||||
a = v3.OAuth2mTlsClientCredential(
|
||||
self.TEST_URL,
|
||||
oauth2_endpoint=oauth2_endpoint,
|
||||
oauth2_client_id=self.TEST_CLIENT_CRED_ID
|
||||
)
|
||||
oauth2_post_resp = {
|
||||
'status_code': 200,
|
||||
'json': {
|
||||
'access_token': oauth2_token,
|
||||
'expires_in': 3600,
|
||||
'token_type': 'Bearer'
|
||||
}
|
||||
}
|
||||
self.requests_mock.post(oauth2_endpoint,
|
||||
[oauth2_post_resp])
|
||||
token_verify_resp = {
|
||||
'status_code': 200,
|
||||
'json': {
|
||||
**self.TEST_OAUTH2_MTLS_TOKEN_RESPONSE
|
||||
}
|
||||
}
|
||||
self.requests_mock.get(token_endpoint, [token_verify_resp])
|
||||
sess = session.Session(auth=a)
|
||||
|
||||
with unittest.mock.patch(
|
||||
'keystoneauth1.plugin.BaseAuthPlugin.'
|
||||
'get_headers') as co_mock:
|
||||
co_mock.return_value = None
|
||||
auth_head = sess.get_auth_headers()
|
||||
self.assertEqual('Bearer None', auth_head['Authorization'])
|
||||
|
@ -539,3 +539,41 @@ class V3Oauth2ClientCredentialTests(utils.TestCase):
|
||||
self.create,
|
||||
oauth2_endpoint=oauth2_endpoint,
|
||||
oauth2_client_id=uuid.uuid4().hex)
|
||||
|
||||
|
||||
class V3Oauth2mTlsClientCredentialTests(utils.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(V3Oauth2mTlsClientCredentialTests, self).setUp()
|
||||
|
||||
self.auth_url = uuid.uuid4().hex
|
||||
|
||||
def create(self, **kwargs):
|
||||
kwargs.setdefault('auth_url', self.auth_url)
|
||||
loader = loading.get_plugin_loader('v3oauth2mtlsclientcredential')
|
||||
return loader.load_from_options(**kwargs)
|
||||
|
||||
def test_basic(self):
|
||||
client_id = uuid.uuid4().hex
|
||||
oauth2_endpoint = "https://localhost/token"
|
||||
|
||||
client_cred = self.create(oauth2_endpoint=oauth2_endpoint,
|
||||
oauth2_client_id=client_id
|
||||
)
|
||||
self.assertEqual(self.auth_url, client_cred.auth_url)
|
||||
self.assertEqual(client_id, client_cred.oauth2_client_id)
|
||||
self.assertEqual(oauth2_endpoint, client_cred.oauth2_endpoint)
|
||||
|
||||
def test_without_oauth2_endpoint(self):
|
||||
client_id = uuid.uuid4().hex
|
||||
self.assertRaises(exceptions.OptionError,
|
||||
self.create,
|
||||
oauth2_client_id=client_id,
|
||||
)
|
||||
|
||||
def test_without_client_id(self):
|
||||
oauth2_endpoint = "https://localhost/token"
|
||||
self.assertRaises(exceptions.OptionError,
|
||||
self.create,
|
||||
oauth2_endpoint=oauth2_endpoint,
|
||||
oauth2_client_secret=uuid.uuid4().hex)
|
||||
|
@ -346,3 +346,17 @@ class V3TokenTests(utils.TestCase):
|
||||
del token.is_admin_project
|
||||
self.assertIsNone(token.is_admin_project)
|
||||
self.assertNotIn('is_admin_project', token['token'])
|
||||
|
||||
def test_oauth2(self):
|
||||
methods = ['oauth2_credential']
|
||||
oauth2_thumbprint = uuid.uuid4().hex
|
||||
token = fixture.V3Token(
|
||||
methods=methods,
|
||||
oauth2_thumbprint=oauth2_thumbprint,
|
||||
)
|
||||
oauth2_credential = {
|
||||
'x5t#S256': oauth2_thumbprint,
|
||||
}
|
||||
self.assertEqual(methods, token.methods)
|
||||
self.assertEqual(oauth2_credential, token.oauth2_credential)
|
||||
self.assertEqual(oauth2_thumbprint, token.oauth2_thumbprint)
|
||||
|
@ -0,0 +1,10 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
[`blueprint support-oauth2-mtls <https://blueprints.launchpad.net/keystone/+spec/support-oauth2-mtls>`_]
|
||||
Added a new OAuth2mTlsClientCredential plugin, accessible via the
|
||||
'v3oauth2mtlsclientcredential' entry point, making possible to authenticate
|
||||
using an OAuth 2.0 Mutual-TLS client credentials. Keystoneauth can now
|
||||
be used to access the OpenStack APIs that use the keystone middleware to
|
||||
support OAuth2.0 mutual-TLS client authentication through the keystone
|
||||
identity server.
|
@ -62,3 +62,4 @@ keystoneauth1.plugin =
|
||||
v3applicationcredential = keystoneauth1.loading._plugins.identity.v3:ApplicationCredential
|
||||
v3multifactor = keystoneauth1.loading._plugins.identity.v3:MultiFactor
|
||||
v3oauth2clientcredential = keystoneauth1.loading._plugins.identity.v3:OAuth2ClientCredential
|
||||
v3oauth2mtlsclientcredential = keystoneauth1.loading._plugins.identity.v3:OAuth2mTlsClientCredential
|
||||
|
Loading…
Reference in New Issue
Block a user