# 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
