keystonemiddleware/keystonemiddleware/external_oauth2_token.py

973 lines
43 KiB
Python

# 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 copy
import hashlib
import os
import ssl
import time
import uuid
import jwt.utils
import oslo_cache
from oslo_config import cfg
from oslo_log import log as logging
from oslo_serialization import jsonutils
import requests.auth
import webob.dec
import webob.exc
from keystoneauth1 import exceptions as ksa_exceptions
from keystoneauth1 import loading
from keystoneauth1.loading import session as session_loading
from keystonemiddleware._common import config
from keystonemiddleware.auth_token import _cache
from keystonemiddleware.exceptions import ConfigurationError
from keystonemiddleware.exceptions import KeystoneMiddlewareException
from keystonemiddleware.i18n import _
oslo_cache.configure(cfg.CONF)
_EXT_AUTH_CONFIG_GROUP_NAME = 'ext_oauth2_auth'
_EXTERNAL_AUTH2_OPTS = [
cfg.StrOpt('certfile',
help='Required if identity server requires client '
'certificate.'),
cfg.StrOpt('keyfile',
help='Required if identity server requires client '
'private key.'),
cfg.StrOpt('cafile',
help='A PEM encoded Certificate Authority to use when '
'verifying HTTPs connections. Defaults to system CAs.'),
cfg.BoolOpt('insecure', default=False, help='Verify HTTPS connections.'),
cfg.IntOpt('http_connect_timeout',
help='Request timeout value for communicating with Identity '
'API server.'),
cfg.StrOpt('introspect_endpoint',
help='The endpoint for introspect API, it is used to verify '
'that the OAuth 2.0 access token is valid.'),
cfg.StrOpt('audience',
help='The Audience should be the URL of the Authorization '
'Server\'s Token Endpoint. The Authorization Server will '
'verify that it is an intended audience for the token.'),
cfg.StrOpt('auth_method',
default='client_secret_basic',
choices=('client_secret_basic', 'client_secret_post',
'tls_client_auth', 'private_key_jwt',
'client_secret_jwt'),
help='The auth_method must use the authentication method '
'specified by the Authorization Server. The system '
'supports 5 authentication methods such as '
'tls_client_auth, client_secret_basic, '
'client_secret_post, client_secret_jwt, private_key_jwt.'),
cfg.StrOpt('client_id',
help='The OAuth 2.0 Client Identifier valid at the '
'Authorization Server.'),
cfg.StrOpt('client_secret',
help='The OAuth 2.0 client secret. When the auth_method is '
'client_secret_basic, client_secret_post, or '
'client_secret_jwt, the value is used, and otherwise the '
'value is ignored.'),
cfg.BoolOpt('thumbprint_verify', default=False,
help='If the access token generated by the Authorization '
'Server is bound to the OAuth 2.0 certificate '
'thumbprint, the value can be set to true, and then the '
'keystone middleware will verify the thumbprint.'),
cfg.StrOpt('jwt_key_file',
help='The jwt_key_file must use the certificate key file which '
'has been registered with the Authorization Server. '
'When the auth_method is private_key_jwt, the value is '
'used, and otherwise the value is ignored.'),
cfg.StrOpt('jwt_algorithm',
help='The jwt_algorithm must use the algorithm specified by '
'the Authorization Server. When the auth_method is '
'client_secret_jwt, this value is often set to HS256, '
'when the auth_method is private_key_jwt, the value is '
'often set to RS256, and otherwise the value is ignored.'),
cfg.IntOpt('jwt_bearer_time_out', default=3600,
help='This value is used to calculate the expiration time. If '
'after the expiration time, the access token can not be '
'accepted. When the auth_method is client_secret_jwt or '
'private_key_jwt, the value is used, and otherwise the '
'value is ignored.'),
cfg.StrOpt('mapping_project_id',
help='Specifies the method for obtaining the project ID that '
'currently needs to be accessed. '),
cfg.StrOpt('mapping_project_name',
help='Specifies the method for obtaining the project name that '
'currently needs to be accessed.'),
cfg.StrOpt('mapping_project_domain_id',
help='Specifies the method for obtaining the project domain ID '
'that currently needs to be accessed.'),
cfg.StrOpt('mapping_project_domain_name',
help='Specifies the method for obtaining the project domain '
'name that currently needs to be accessed.'),
cfg.StrOpt('mapping_user_id', default='client_id',
help='Specifies the method for obtaining the user ID.'),
cfg.StrOpt('mapping_user_name', default='username',
help='Specifies the method for obtaining the user name.'),
cfg.StrOpt('mapping_user_domain_id',
help='Specifies the method for obtaining the domain ID which '
'the user belongs.'),
cfg.StrOpt('mapping_user_domain_name',
help='Specifies the method for obtaining the domain name which '
'the user belongs.'),
cfg.StrOpt('mapping_roles',
help='Specifies the method for obtaining the list of roles in '
'a project or domain owned by the user.'),
cfg.StrOpt('mapping_system_scope',
help='Specifies the method for obtaining the scope information '
'indicating whether a token is system-scoped.'),
cfg.StrOpt('mapping_expires_at',
help='Specifies the method for obtaining the token expiration '
'time.'),
cfg.ListOpt('memcached_servers',
deprecated_name='memcache_servers',
help='Optionally specify a list of memcached server(s) to '
'use for caching. If left undefined, tokens will '
'instead be cached in-process.'),
cfg.IntOpt('token_cache_time',
default=300,
help='In order to prevent excessive effort spent validating '
'tokens, the middleware caches previously-seen tokens '
'for a configurable duration (in seconds). Set to -1 to '
'disable caching completely.'),
cfg.StrOpt('memcache_security_strategy',
default='None',
choices=('None', 'MAC', 'ENCRYPT'),
ignore_case=True,
help='(Optional) If defined, indicate whether token data '
'should be authenticated or authenticated and encrypted. '
'If MAC, token data is authenticated (with HMAC) in the '
'cache. If ENCRYPT, token data is encrypted and '
'authenticated in the cache. If the value is not one of '
'these options or empty, auth_token will raise an '
'exception on initialization.'),
cfg.StrOpt('memcache_secret_key',
secret=True,
help='(Optional, mandatory if memcache_security_strategy is '
'defined) This string is used for key derivation.'),
cfg.IntOpt('memcache_pool_dead_retry',
default=5 * 60,
help='(Optional) Number of seconds memcached server is '
'considered dead before it is tried again.'),
cfg.IntOpt('memcache_pool_maxsize',
default=10,
help='(Optional) Maximum total number of open connections to '
'every memcached server.'),
cfg.IntOpt('memcache_pool_socket_timeout',
default=3,
help='(Optional) Socket timeout in seconds for communicating '
'with a memcached server.'),
cfg.IntOpt('memcache_pool_unused_timeout',
default=60,
help='(Optional) Number of seconds a connection to memcached '
'is held unused in the pool before it is closed.'),
cfg.IntOpt('memcache_pool_conn_get_timeout',
default=10,
help='(Optional) Number of seconds that an operation will wait '
'to get a memcached client connection from the pool.'),
cfg.BoolOpt('memcache_use_advanced_pool',
default=True,
help='(Optional) Use the advanced (eventlet safe) memcached '
'client pool.')
]
cfg.CONF.register_opts(_EXTERNAL_AUTH2_OPTS,
group=_EXT_AUTH_CONFIG_GROUP_NAME)
class InvalidToken(KeystoneMiddlewareException):
"""Raise an InvalidToken Error.
When can not get necessary information from the token,
this error will be thrown.
"""
class ForbiddenToken(KeystoneMiddlewareException):
"""Raise a ForbiddenToken Error.
When can not get necessary information from the token,
this error will be thrown.
"""
class ServiceError(KeystoneMiddlewareException):
"""Raise a ServiceError.
When can not verify any tokens, this error will be thrown.
"""
class AbstractAuthClient(object, metaclass=abc.ABCMeta):
"""Abstract http client using to access the OAuth2.0 Server."""
def __init__(self, session, introspect_endpoint, audience, client_id,
func_get_config_option, logger):
self.session = session
self.introspect_endpoint = introspect_endpoint
self.audience = audience
self.client_id = client_id
self.get_config_option = func_get_config_option
self.logger = logger
@abc.abstractmethod
def introspect(self, access_token):
"""Access the introspect API."""
pass
class ClientSecretBasicAuthClient(AbstractAuthClient):
"""Http client with the auth method 'client_secret_basic'."""
def __init__(self, session, introspect_endpoint, audience, client_id,
func_get_config_option, logger):
super(ClientSecretBasicAuthClient, self).__init__(
session, introspect_endpoint, audience, client_id,
func_get_config_option, logger)
self.client_secret = self.get_config_option(
'client_secret', is_required=True)
def introspect(self, access_token):
"""Access the introspect API.
Access the Introspect API to verify the access token by
the auth method 'client_secret_basic'.
"""
req_data = {'token': access_token,
'token_type_hint': 'access_token'}
auth = requests.auth.HTTPBasicAuth(self.client_id,
self.client_secret)
http_response = self.session.request(
self.introspect_endpoint,
'POST',
authenticated=False,
data=req_data,
requests_auth=auth)
return http_response
class ClientSecretPostAuthClient(AbstractAuthClient):
"""Http client with the auth method 'client_secret_post'."""
def __init__(self, session, introspect_endpoint, audience, client_id,
func_get_config_option, logger):
super(ClientSecretPostAuthClient, self).__init__(
session, introspect_endpoint, audience, client_id,
func_get_config_option, logger)
self.client_secret = self.get_config_option(
'client_secret', is_required=True)
def introspect(self, access_token):
"""Access the introspect API.
Access the Introspect API to verify the access token by
the auth method 'client_secret_post'.
"""
req_data = {
'client_id': self.client_id,
'client_secret': self.client_secret,
'token': access_token,
'token_type_hint': 'access_token'
}
http_response = self.session.request(
self.introspect_endpoint,
'POST',
authenticated=False,
data=req_data)
return http_response
class TlsClientAuthClient(AbstractAuthClient):
"""Http client with the auth method 'tls_client_auth'."""
def introspect(self, access_token):
"""Access the introspect API.
Access the Introspect API to verify the access token by
the auth method 'tls_client_auth'.
"""
req_data = {
'client_id': self.client_id,
'token': access_token,
'token_type_hint': 'access_token'
}
http_response = self.session.request(
self.introspect_endpoint,
'POST',
authenticated=False,
data=req_data)
return http_response
class PrivateKeyJwtAuthClient(AbstractAuthClient):
"""Http client with the auth method 'private_key_jwt'."""
def __init__(self, session, introspect_endpoint, audience, client_id,
func_get_config_option, logger):
super(PrivateKeyJwtAuthClient, self).__init__(
session, introspect_endpoint, audience, client_id,
func_get_config_option, logger)
self.jwt_key_file = self.get_config_option(
'jwt_key_file', is_required=True)
self.jwt_bearer_time_out = self.get_config_option(
'jwt_bearer_time_out', is_required=True)
self.jwt_algorithm = self.get_config_option(
'jwt_algorithm', is_required=True)
self.logger = logger
def introspect(self, access_token):
"""Access the introspect API.
Access the Introspect API to verify the access token by
the auth method 'private_key_jwt'.
"""
if not os.path.isfile(self.jwt_key_file):
self.logger.critical('Configuration error. JWT key file is '
'not a file. path: %s' % self.jwt_key_file)
raise ConfigurationError(_('Configuration error. '
'JWT key file is not a file.'))
try:
with open(self.jwt_key_file, 'r') as jwt_file:
jwt_key = jwt_file.read()
except Exception as e:
self.logger.critical('Configuration error. Failed to read '
'the JWT key file. %s', e)
raise ConfigurationError(_('Configuration error. '
'Failed to read the JWT key file.'))
if not jwt_key:
self.logger.critical('Configuration error. The JWT key file '
'content is empty. path: %s'
% self.jwt_key_file)
raise ConfigurationError(_('Configuration error. The JWT key file '
'content is empty.'))
iat = round(time.time())
try:
client_assertion = jwt.encode(
payload={
'jti': str(uuid.uuid4()),
'iat': str(iat),
'exp': str(iat + self.jwt_bearer_time_out),
'iss': self.client_id,
'sub': self.client_id,
'aud': self.audience},
headers={
'typ': 'JWT',
'alg': self.jwt_algorithm},
key=jwt_key,
algorithm=self.jwt_algorithm)
except Exception as e:
self.logger.critical('Configuration error. JWT encoding with '
'the specified JWT key file and algorithm '
'failed. path: %s, algorithm: %s, error: %s' %
(self.jwt_key_file, self.jwt_algorithm, e))
raise ConfigurationError(_('Configuration error. JWT encoding '
'with the specified JWT key file '
'and algorithm failed.'))
req_data = {
'client_id': self.client_id,
'client_assertion_type':
'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
'client_assertion': client_assertion,
'token': access_token,
'token_type_hint': 'access_token'
}
http_response = self.session.request(
self.introspect_endpoint,
'POST',
authenticated=False,
data=req_data)
return http_response
class ClientSecretJwtAuthClient(AbstractAuthClient):
"""Http client with the auth method 'client_secret_jwt'."""
def __init__(self, session, introspect_endpoint, audience, client_id,
func_get_config_option, logger):
super(ClientSecretJwtAuthClient, self).__init__(
session, introspect_endpoint, audience, client_id,
func_get_config_option, logger)
self.client_secret = self.get_config_option(
'client_secret', is_required=True)
self.jwt_bearer_time_out = self.get_config_option(
'jwt_bearer_time_out', is_required=True)
self.jwt_algorithm = self.get_config_option(
'jwt_algorithm', is_required=True)
def introspect(self, access_token):
"""Access the introspect API.
Access the Introspect API to verify the access token by
the auth method 'client_secret_jwt'.
"""
ita = round(time.time())
try:
client_assertion = jwt.encode(
payload={
'jti': str(uuid.uuid4()),
'iat': str(ita),
'exp': str(ita + self.jwt_bearer_time_out),
'iss': self.client_id,
'sub': self.client_id,
'aud': self.audience},
headers={
'typ': 'JWT',
'alg': self.jwt_algorithm},
key=self.client_secret,
algorithm=self.jwt_algorithm)
except Exception as e:
self.logger.critical('Configuration error. JWT encoding with '
'the specified client_secret and algorithm '
'failed. algorithm: %s, error: %s'
% (self.jwt_algorithm, e))
raise ConfigurationError(_('Configuration error. JWT encoding '
'with the specified client_secret '
'and algorithm failed.'))
req_data = {
'client_id': self.client_id,
'client_assertion_type':
'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
'client_assertion': client_assertion,
'token': access_token,
'token_type_hint': 'access_token'
}
http_response = self.session.request(
self.introspect_endpoint,
'POST',
authenticated=False,
data=req_data)
return http_response
_ALL_AUTH_CLIENTS = {
'client_secret_basic': ClientSecretBasicAuthClient,
'client_secret_post': ClientSecretPostAuthClient,
'tls_client_auth': TlsClientAuthClient,
'private_key_jwt': PrivateKeyJwtAuthClient,
'client_secret_jwt': ClientSecretJwtAuthClient
}
def _get_http_client(auth_method, session, introspect_endpoint, audience,
client_id, func_get_config_option, logger):
"""Get an auth HTTP Client to access the OAuth2.0 Server."""
if auth_method in _ALL_AUTH_CLIENTS:
return _ALL_AUTH_CLIENTS.get(auth_method)(
session, introspect_endpoint, audience,
client_id, func_get_config_option, logger)
logger.critical('The value is incorrect for option '
'auth_method in group [%s]' %
_EXT_AUTH_CONFIG_GROUP_NAME)
raise ConfigurationError(_('The configuration parameter for '
'key "auth_method" in group [%s] '
'is incorrect.') %
_EXT_AUTH_CONFIG_GROUP_NAME)
class ExternalAuth2Protocol(object):
"""Middleware that handles External Server OAuth2.0 authentication."""
def __init__(self, application, conf):
super(ExternalAuth2Protocol, self).__init__()
self._application = application
self._log = logging.getLogger(conf.get('log_name', __name__))
self._log.info('Starting Keystone external_oauth2_token middleware')
config_opts = [
(_EXT_AUTH_CONFIG_GROUP_NAME, _EXTERNAL_AUTH2_OPTS
+ loading.get_auth_common_conf_options())
]
all_opts = [(g, copy.deepcopy(o)) for g, o in config_opts]
self._conf = config.Config('external_oauth2_token',
_EXT_AUTH_CONFIG_GROUP_NAME,
all_opts,
conf)
self._token_cache = self._token_cache_factory()
self._session = self._create_session()
self._audience = self._get_config_option('audience', is_required=True)
self._introspect_endpoint = self._get_config_option(
'introspect_endpoint', is_required=True)
self._auth_method = self._get_config_option(
'auth_method', is_required=True)
self._client_id = self._get_config_option(
'client_id', is_required=True)
self._http_client = _get_http_client(
self._auth_method, self._session, self._introspect_endpoint,
self._audience, self._client_id,
self._get_config_option, self._log)
def _token_cache_factory(self):
security_strategy = self._conf.get('memcache_security_strategy')
cache_kwargs = dict(
cache_time=int(self._conf.get('token_cache_time')),
memcached_servers=self._conf.get('memcached_servers'),
use_advanced_pool=self._conf.get(
'memcache_use_advanced_pool'),
dead_retry=self._conf.get('memcache_pool_dead_retry'),
maxsize=self._conf.get('memcache_pool_maxsize'),
unused_timeout=self._conf.get(
'memcache_pool_unused_timeout'),
conn_get_timeout=self._conf.get(
'memcache_pool_conn_get_timeout'),
socket_timeout=self._conf.get(
'memcache_pool_socket_timeout'),
)
if security_strategy.lower() != 'none':
secret_key = self._conf.get('memcache_secret_key')
return _cache.SecureTokenCache(self._log,
security_strategy,
secret_key,
**cache_kwargs)
return _cache.TokenCache(self._log, **cache_kwargs)
@webob.dec.wsgify()
def __call__(self, req):
"""Handle incoming request."""
self.process_request(req)
response = req.get_response(self._application)
return self.process_response(response)
def process_request(self, request):
"""Process request.
:param request: Incoming request
:type request: _request.AuthTokenRequest
"""
access_token = None
if (request.authorization and
request.authorization.authtype == 'Bearer'):
access_token = request.authorization.params
try:
if not access_token:
self._log.info('Unable to obtain the access token.')
raise InvalidToken(_('Unable to obtain the access token.'))
self._token_cache.initialize(request.environ)
token_data = self._fetch_token(access_token)
if (self._get_config_option('thumbprint_verify',
is_required=False)):
self._confirm_certificate_thumbprint(
request, token_data.get('origin_token_metadata'))
self._set_request_env(request, token_data)
except InvalidToken as error:
self._log.info('Rejecting request. '
'Need a valid OAuth 2.0 access token. '
'error: %s', error)
message = _('The request you have made is denied, '
'because the token is invalid.')
body = {'error': {
'code': 401,
'title': 'Unauthorized',
'message': message,
}}
raise webob.exc.HTTPUnauthorized(
body=jsonutils.dumps(body),
headers=self._reject_headers,
charset='UTF-8',
content_type='application/json')
except ForbiddenToken as error:
self._log.warning('Rejecting request. '
'The necessary information is required.'
'error: %s', error)
message = _('The request you have made is denied, '
'because the necessary information '
'could not be parsed.')
body = {'error': {
'code': 403,
'title': 'Forbidden',
'message': message,
}}
raise webob.exc.HTTPForbidden(
body=jsonutils.dumps(body),
charset='UTF-8',
content_type='application/json')
except ConfigurationError as error:
self._log.critical('Rejecting request. '
'The configuration parameters are incorrect. '
'error: %s', error)
message = _('The request you have made is denied, '
'because the configuration parameters are incorrect '
'and the token can not be verified.')
body = {'error': {
'code': 500,
'title': 'Internal Server Error',
'message': message,
}}
raise webob.exc.HTTPServerError(
body=jsonutils.dumps(body),
charset='UTF-8',
content_type='application/json')
except ServiceError as error:
self._log.warning('Rejecting request. An exception occurred and '
'the OAuth 2.0 access token can not be '
'verified. error: %s', error)
message = _('The request you have made is denied, '
'because an exception occurred while accessing '
'the external authentication server '
'for token validation.')
body = {'error': {
'code': 500,
'title': 'Internal Server Error',
'message': message,
}}
raise webob.exc.HTTPServerError(
body=jsonutils.dumps(body),
charset='UTF-8',
content_type='application/json')
def process_response(self, response):
"""Process Response.
Add ``WWW-Authenticate`` headers to requests that failed with
``401 Unauthenticated`` so users know where to authenticate for future
requests.
"""
if response.status_int == 401:
response.headers.extend(self._reject_headers)
return response
def _create_session(self, **kwargs):
"""Create session for HTTP access."""
kwargs.setdefault('cert', self._get_config_option(
'certfile', is_required=False))
kwargs.setdefault('key', self._get_config_option(
'keyfile', is_required=False))
kwargs.setdefault('cacert', self._get_config_option(
'cafile', is_required=False))
kwargs.setdefault('insecure', self._get_config_option(
'insecure', is_required=False))
kwargs.setdefault('timeout', self._get_config_option(
'http_connect_timeout', is_required=False))
kwargs.setdefault('user_agent', self._conf.user_agent)
return session_loading.Session().load_from_options(**kwargs)
def _get_config_option(self, key, is_required):
"""Read the value from config file by the config key."""
value = self._conf.get(key)
if not value:
if is_required:
self._log.critical('The value is required for option %s '
'in group [%s]' % (
key, _EXT_AUTH_CONFIG_GROUP_NAME))
raise ConfigurationError(
_('Configuration error. The parameter '
'is not set for "%s" in group [%s].') % (
key, _EXT_AUTH_CONFIG_GROUP_NAME))
else:
return None
else:
return value
@property
def _reject_headers(self):
"""Generate WWW-Authenticate Header.
When response status is 401, this method will be called to add
the 'WWW-Authenticate' header to the response.
"""
header_val = 'Authorization OAuth 2.0 uri="%s"' % self._audience
return [('WWW-Authenticate', header_val)]
def _fetch_token(self, access_token):
"""Use access_token to get the valid token meta_data.
Verify the access token through accessing the external
authorization server.
"""
try:
cached = self._token_cache.get(access_token)
if cached:
self._log.debug('The cached token: %s' % cached)
if (not isinstance(cached, dict)
or 'origin_token_metadata' not in cached):
self._log.warning('The cached data is invalid. %s' %
cached)
raise InvalidToken(_('The token is invalid.'))
origin_token_metadata = cached.get('origin_token_metadata')
if not origin_token_metadata.get('active'):
self._log.warning('The cached data is invalid. %s' %
cached)
raise InvalidToken(_('The token is invalid.'))
expire_at = self._read_data_from_token(
origin_token_metadata, 'mapping_expires_at',
is_required=False, value_type=int)
if expire_at:
if int(expire_at) < int(time.time()):
cached['origin_token_metadata']['active'] = False
self._token_cache.set(access_token, cached)
self._log.warning(
'The cached data is invalid. %s' % cached)
raise InvalidToken(_('The token is invalid.'))
return cached
http_response = self._http_client.introspect(access_token)
if http_response.status_code != 200:
self._log.critical('The introspect API returns an '
'incorrect response. '
'response_status: %s, response_text: %s' %
(http_response.status_code,
http_response.text))
raise ServiceError(_('The token cannot be verified '
'for validity.'))
origin_token_metadata = http_response.json()
self._log.debug('The introspect API response: %s' %
origin_token_metadata)
if not origin_token_metadata.get('active'):
self._token_cache.set(
access_token,
{'origin_token_metadata': origin_token_metadata})
self._log.info('The token is invalid. response: %s' %
origin_token_metadata)
raise InvalidToken(_('The token is invalid.'))
token_data = self._parse_necessary_info(origin_token_metadata)
self._token_cache.set(access_token, token_data)
return token_data
except (ConfigurationError, ForbiddenToken,
ServiceError, InvalidToken):
raise
except (ksa_exceptions.ConnectFailure,
ksa_exceptions.DiscoveryFailure,
ksa_exceptions.RequestTimeout) as error:
self._log.critical('Unable to validate token: %s', error)
raise ServiceError(
_('The Introspect API service is temporarily unavailable.'))
except Exception as error:
self._log.critical('Unable to validate token: %s', error)
raise ServiceError(_('An exception occurred during the token '
'verification process.'))
def _read_data_from_token(self, token_metadata, config_key,
is_required=False, value_type=None):
"""Read value from token metadata.
Read the necessary information from the token metadata with the
config key.
"""
if not value_type:
value_type = str
meta_key = self._get_config_option(config_key, is_required=is_required)
if not meta_key:
return None
if meta_key.find('.') >= 0:
meta_value = None
for temp_key in meta_key.split('.'):
if not temp_key:
self._log.critical('Configuration error. '
'config_key: %s , meta_key: %s ' %
(config_key, meta_key))
raise ConfigurationError(
_('Failed to parse the necessary information '
'for the field "%s".') % meta_key)
if not meta_value:
meta_value = token_metadata.get(temp_key)
else:
if not isinstance(meta_value, dict):
self._log.warning(
'Failed to parse the necessary information. '
'The meta_value is not of type dict.'
'config_key: %s , meta_key: %s, value: %s' %
(config_key, meta_key, meta_value))
raise ForbiddenToken(
_('Failed to parse the necessary information '
'for the field "%s".') % meta_key)
meta_value = meta_value.get(temp_key)
else:
meta_value = token_metadata.get(meta_key)
if not meta_value:
if is_required:
self._log.warning(
'Failed to parse the necessary information. '
'The meta value is required.'
'config_key: %s , meta_key: %s, value: %s, need_type: %s' %
(config_key, meta_key, meta_value, value_type))
raise ForbiddenToken(_('Failed to parse the necessary '
'information for the field "%s".') %
meta_key)
else:
meta_value = None
else:
if not isinstance(meta_value, value_type):
self._log.warning(
'Failed to parse the necessary information. '
'The meta value is of an incorrect type.'
'config_key: %s , meta_key: %s, value: %s, need_type: %s'
% (config_key, meta_key, meta_value, value_type))
raise ForbiddenToken(_('Failed to parse the necessary '
'information for the field "%s".') %
meta_key)
return meta_value
def _parse_necessary_info(self, token_metadata):
"""Parse the necessary information from the token metadata."""
token_data = dict()
token_data['origin_token_metadata'] = token_metadata
roles = self._read_data_from_token(token_metadata,
'mapping_roles',
is_required=True)
is_admin = 'false'
if 'admin' in roles.lower().split(','):
is_admin = 'true'
token_data['roles'] = roles
token_data['is_admin'] = is_admin
system_scope = self._read_data_from_token(
token_metadata, 'mapping_system_scope',
is_required=False, value_type=bool)
if system_scope:
token_data['system_scope'] = 'all'
else:
project_id = self._read_data_from_token(
token_metadata, 'mapping_project_id', is_required=False)
if project_id:
token_data['project_id'] = project_id
token_data['project_name'] = self._read_data_from_token(
token_metadata, 'mapping_project_name', is_required=True)
token_data['project_domain_id'] = self._read_data_from_token(
token_metadata, 'mapping_project_domain_id',
is_required=True)
token_data['project_domain_name'] = self._read_data_from_token(
token_metadata, 'mapping_project_domain_name',
is_required=True)
else:
token_data['domain_id'] = self._read_data_from_token(
token_metadata, 'mapping_project_domain_id',
is_required=True)
token_data['domain_name'] = self._read_data_from_token(
token_metadata, 'mapping_project_domain_name',
is_required=True)
token_data['user_id'] = self._read_data_from_token(
token_metadata, 'mapping_user_id', is_required=True)
token_data['user_name'] = self._read_data_from_token(
token_metadata, 'mapping_user_name', is_required=True)
token_data['user_domain_id'] = self._read_data_from_token(
token_metadata, 'mapping_user_domain_id', is_required=True)
token_data['user_domain_name'] = self._read_data_from_token(
token_metadata, 'mapping_user_domain_name', is_required=True)
return token_data
def _get_client_certificate(self, request):
"""Get the client certificate from request environ or socket."""
try:
pem_client_cert = request.environ.get('SSL_CLIENT_CERT')
if pem_client_cert:
peer_cert = ssl.PEM_cert_to_DER_cert(pem_client_cert)
else:
wsgi_input = request.environ.get('wsgi.input')
if not wsgi_input:
self._log.warn('Unable to obtain the client certificate. '
'The object for wsgi_input is none.')
raise InvalidToken(_('Unable to obtain the client '
'certificate.'))
socket = wsgi_input.get_socket()
if not socket:
self._log.warn('Unable to obtain the client certificate. '
'The object for socket is none.')
raise InvalidToken(_('Unable to obtain the client '
'certificate.'))
peer_cert = socket.getpeercert(binary_form=True)
if not peer_cert:
self._log.warn('Unable to obtain the client certificate. '
'The object for peer_cert is none.')
raise InvalidToken(_('Unable to obtain the client '
'certificate.'))
return peer_cert
except InvalidToken:
raise
except Exception as error:
self._log.warn('Unable to obtain the client certificate. %s' %
error)
raise InvalidToken(_('Unable to obtain the client certificate.'))
def _confirm_certificate_thumbprint(self, request, origin_token_metadata):
"""Check if the thumbprint in the token is valid."""
peer_cert = self._get_client_certificate(request)
try:
thumb_sha256 = hashlib.sha256(peer_cert).digest()
cert_thumb = jwt.utils.base64url_encode(thumb_sha256).decode(
'ascii')
except Exception as error:
self._log.warn('An Exception occurred. %s' % error)
raise InvalidToken(_('Can not generate the thumbprint.'))
token_thumb = origin_token_metadata.get('cnf', {}).get('x5t#S256')
if cert_thumb != token_thumb:
self._log.warn('The two thumbprints do not match. '
'token_thumbprint: %s, certificate_thumbprint %s' %
(token_thumb, cert_thumb))
raise InvalidToken(_('The two thumbprints do not match.'))
def _set_request_env(self, request, token_data):
"""Set request.environ with the necessary information."""
request.environ['external.token_info'] = token_data
request.environ['HTTP_X_IDENTITY_STATUS'] = 'Confirmed'
request.environ['HTTP_X_ROLES'] = token_data.get('roles')
request.environ['HTTP_X_ROLE'] = token_data.get('roles')
request.environ['HTTP_X_USER_ID'] = token_data.get('user_id')
request.environ['HTTP_X_USER_NAME'] = token_data.get('user_name')
request.environ['HTTP_X_USER_DOMAIN_ID'] = token_data.get(
'user_domain_id')
request.environ['HTTP_X_USER_DOMAIN_NAME'] = token_data.get(
'user_domain_name')
if token_data.get('is_admin') == 'true':
request.environ['HTTP_X_IS_ADMIN_PROJECT'] = token_data.get(
'is_admin')
request.environ['HTTP_X_USER'] = token_data.get('user_name')
if token_data.get('system_scope'):
request.environ['HTTP_OPENSTACK_SYSTEM_SCOPE'] = token_data.get(
'system_scope'
)
elif token_data.get('project_id'):
request.environ['HTTP_X_PROJECT_ID'] = token_data.get('project_id')
request.environ['HTTP_X_PROJECT_NAME'] = token_data.get(
'project_name')
request.environ['HTTP_X_PROJECT_DOMAIN_ID'] = token_data.get(
'project_domain_id')
request.environ['HTTP_X_PROJECT_DOMAIN_NAME'] = token_data.get(
'project_domain_name')
request.environ['HTTP_X_TENANT_ID'] = token_data.get('project_id')
request.environ['HTTP_X_TENANT_NAME'] = token_data.get(
'project_name')
request.environ['HTTP_X_TENANT'] = token_data.get('project_id')
else:
request.environ['HTTP_X_DOMAIN_ID'] = token_data.get('domain_id')
request.environ['HTTP_X_DOMAIN_NAME'] = token_data.get(
'domain_name')
self._log.debug('The access token data is %s.' % jsonutils.dumps(
token_data))
def filter_factory(global_conf, **local_conf):
"""Return a WSGI filter app for use with paste.deploy."""
conf = global_conf.copy()
conf.update(local_conf)
def auth_filter(app):
return ExternalAuth2Protocol(app, conf)
return auth_filter