# 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