We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
OpenID payload cache uses the wrong cache key
The `cache_key` becomes the same for every access token due to this bug: https://github.com/Kinto/kinto/blob/e1e0d6be0024418fd100210901f9d2ca06344fe1/kinto/plugins/openid/__init__.py#L51
No matter what the `hmac_tokens` variable is the `cache_key` always becomes `'openid:verify:%s'`.



--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `setup.py`
Content:
```
1 import codecs
2 import os
3 from setuptools import setup, find_packages
4 
5 # abspath here because setup.py may be __main__, in which case
6 # __file__ is not guaranteed to be absolute
7 here = os.path.abspath(os.path.dirname(__file__))
8 
9 
10 def read_file(filename):
11     """Open a related file and return its content."""
12     with codecs.open(os.path.join(here, filename), encoding='utf-8') as f:
13         content = f.read()
14     return content
15 
16 
17 README = read_file('README.rst')
18 CHANGELOG = read_file('CHANGELOG.rst')
19 CONTRIBUTORS = read_file('CONTRIBUTORS.rst')
20 
21 REQUIREMENTS = [
22     'bcrypt',
23     'colander >= 1.4.0',
24     'cornice',
25     'cornice_swagger >= 0.5.1',
26     'dockerflow',
27     'jsonschema',
28     'jsonpatch',
29     'logging-color-formatter >= 1.0.1',  # Message interpolations.
30     'python-dateutil',
31     'pyramid >= 1.9.1, < 2.0',
32     'pyramid_multiauth >= 0.8',  # User on policy selected event.
33     'transaction',
34     # pyramid_tm changed the location of their tween in 2.x and one of
35     # our tests fails on 2.0.
36     'pyramid_tm >= 2.1',
37     'requests',
38     'waitress',
39     'ujson >= 1.35',
40 ]
41 
42 POSTGRESQL_REQUIRES = [
43     'SQLAlchemy',
44     'psycopg2 > 2.5',
45     'zope.sqlalchemy',
46 ]
47 
48 REDIS_REQUIRES = [
49     'kinto_redis'
50 ]
51 
52 MEMCACHED_REQUIRES = [
53     'python-memcached'
54 ]
55 
56 SETUP_REQUIRES = [
57     'pytest-runner'
58 ]
59 
60 TEST_REQUIREMENTS = [
61     'bravado_core',
62     'pytest',
63     'WebTest'
64 ]
65 
66 DEPENDENCY_LINKS = []
67 
68 MONITORING_REQUIRES = [
69     'raven',
70     'statsd',
71     'newrelic',
72     'werkzeug',
73 ]
74 
75 ENTRY_POINTS = {
76     'paste.app_factory': [
77         'main = kinto:main',
78     ],
79     'console_scripts': [
80         'kinto = kinto.__main__:main'
81     ],
82 }
83 
84 
85 setup(name='kinto',
86       version='9.1.1',
87       description='Kinto Web Service - Store, Sync, Share, and Self-Host.',
88       long_description='{}\n\n{}\n\n{}'.format(README, CHANGELOG, CONTRIBUTORS),
89       license='Apache License (2.0)',
90       classifiers=[
91           'Programming Language :: Python',
92           'Programming Language :: Python :: 3',
93           'Programming Language :: Python :: 3.5',
94           'Programming Language :: Python :: 3.6',
95           'Programming Language :: Python :: Implementation :: CPython',
96           'Topic :: Internet :: WWW/HTTP',
97           'Topic :: Internet :: WWW/HTTP :: WSGI :: Application',
98           'License :: OSI Approved :: Apache Software License'
99       ],
100       keywords='web sync json storage services',
101       author='Mozilla Services',
102       author_email='storage-team@mozilla.com',
103       url='https://github.com/Kinto/kinto',
104       packages=find_packages(),
105       package_data={'': ['*.rst', '*.py', '*.yaml']},
106       include_package_data=True,
107       zip_safe=False,
108       setup_requires=SETUP_REQUIRES,
109       tests_require=TEST_REQUIREMENTS,
110       install_requires=REQUIREMENTS,
111       extras_require={
112           'redis': REDIS_REQUIRES,
113           'memcached': MEMCACHED_REQUIRES,
114           'postgresql': POSTGRESQL_REQUIRES,
115           'monitoring': MONITORING_REQUIRES,
116       },
117       test_suite='tests',
118       dependency_links=DEPENDENCY_LINKS,
119       entry_points=ENTRY_POINTS)
120
```

Path: `kinto/plugins/openid/__init__.py`
Content:
```
1 import re
2 
3 import requests
4 from pyramid import authentication as base_auth
5 from pyramid.interfaces import IAuthenticationPolicy
6 from zope.interface import implementer
7 
8 from kinto.core import logger
9 from kinto.core import utils as core_utils
10 from kinto.core.openapi import OpenAPI
11 
12 from .utils import fetch_openid_config
13 
14 
15 @implementer(IAuthenticationPolicy)
16 class OpenIDConnectPolicy(base_auth.CallbackAuthenticationPolicy):
17     def __init__(self, issuer, client_id, realm='Realm', **kwargs):
18         self.realm = realm
19         self.issuer = issuer
20         self.client_id = client_id
21         self.client_secret = kwargs.get('client_secret', '')
22         self.header_type = kwargs.get('header_type', 'Bearer')
23         self.userid_field = kwargs.get('userid_field', 'sub')
24         self.verification_ttl = int(kwargs.get('verification_ttl_seconds', 86400))
25 
26         # Fetch OpenID config (at instantiation, ie. startup)
27         self.oid_config = fetch_openid_config(issuer)
28 
29         self._jwt_keys = None
30 
31     def unauthenticated_userid(self, request):
32         """Return the userid or ``None`` if token could not be verified.
33         """
34         settings = request.registry.settings
35         hmac_secret = settings['userid_hmac_secret']
36 
37         authorization = request.headers.get('Authorization', '')
38         try:
39             authmeth, access_token = authorization.split(' ', 1)
40         except ValueError:
41             return None
42 
43         if authmeth.lower() != self.header_type.lower():
44             return None
45 
46         # XXX JWT Access token
47         # https://auth0.com/docs/tokens/access-token#access-token-format
48 
49         # Check cache if these tokens were already verified.
50         hmac_tokens = core_utils.hmac_digest(hmac_secret, access_token)
51         cache_key = 'openid:verify:%s'.format(hmac_tokens)
52         payload = request.registry.cache.get(cache_key)
53         if payload is None:
54             # This can take some time.
55             payload = self._verify_token(access_token)
56             if payload is None:
57                 return None
58         # Save for next time / refresh ttl.
59         request.registry.cache.set(cache_key, payload, ttl=self.verification_ttl)
60         # Extract meaningful field from userinfo (eg. email or sub)
61         return payload.get(self.userid_field)
62 
63     def forget(self, request):
64         """A no-op. Credentials are sent on every request.
65         Return WWW-Authenticate Realm header for Bearer token.
66         """
67         return [('WWW-Authenticate', '%s realm="%s"' % (self.header_type, self.realm))]
68 
69     def _verify_token(self, access_token):
70         uri = self.oid_config['userinfo_endpoint']
71         # Opaque access token string. Fetch user info from profile.
72         try:
73             resp = requests.get(uri, headers={'Authorization': 'Bearer ' + access_token})
74             resp.raise_for_status()
75             userprofile = resp.json()
76             return userprofile
77 
78         except (requests.exceptions.HTTPError, ValueError, KeyError) as e:
79             logger.debug('Unable to fetch user profile from %s (%s)' % (uri, e))
80             return None
81 
82 
83 def includeme(config):
84     # Activate end-points.
85     config.scan('kinto.plugins.openid.views')
86 
87     settings = config.get_settings()
88 
89     openid_policies = []
90     for k, v in settings.items():
91         m = re.match('multiauth\.policy\.(.*)\.use', k)
92         if m:
93             if v.endswith('OpenIDConnectPolicy'):
94                 openid_policies.append(m.group(1))
95 
96     if len(openid_policies) == 0:
97         # Do not add the capability if no policy is configured.
98         return
99 
100     providers_infos = []
101     for name in openid_policies:
102         issuer = settings['multiauth.policy.%s.issuer' % name]
103         openid_config = fetch_openid_config(issuer)
104 
105         client_id = settings['multiauth.policy.%s.client_id' % name]
106         header_type = settings.get('multiauth.policy.%s.header_type', 'Bearer')
107 
108         providers_infos.append({
109             'name': name,
110             'issuer': openid_config['issuer'],
111             'auth_path': '/openid/%s/login' % name,
112             'client_id': client_id,
113             'header_type': header_type,
114             'userinfo_endpoint': openid_config['userinfo_endpoint'],
115         })
116 
117         OpenAPI.expose_authentication_method(name, {
118             'type': 'oauth2',
119             'authorizationUrl': openid_config['authorization_endpoint'],
120         })
121 
122     config.add_api_capability(
123         'openid',
124         description='OpenID connect support.',
125         url='http://kinto.readthedocs.io/en/stable/api/1.x/authentication.html',
126         providers=providers_infos)
127
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/kinto/plugins/openid/__init__.py b/kinto/plugins/openid/__init__.py
--- a/kinto/plugins/openid/__init__.py
+++ b/kinto/plugins/openid/__init__.py
@@ -48,7 +48,7 @@
 
         # Check cache if these tokens were already verified.
         hmac_tokens = core_utils.hmac_digest(hmac_secret, access_token)
-        cache_key = 'openid:verify:%s'.format(hmac_tokens)
+        cache_key = 'openid:verify:{}'.format(hmac_tokens)
         payload = request.registry.cache.get(cache_key)
         if payload is None:
             # This can take some time.
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -83,7 +83,7 @@
 
 
 setup(name='kinto',
-      version='9.1.1',
+      version='9.1.2',
       description='Kinto Web Service - Store, Sync, Share, and Self-Host.',
       long_description='{}\n\n{}\n\n{}'.format(README, CHANGELOG, CONTRIBUTORS),
       license='Apache License (2.0)',

{"golden_diff": "diff --git a/kinto/plugins/openid/__init__.py b/kinto/plugins/openid/__init__.py\n--- a/kinto/plugins/openid/__init__.py\n+++ b/kinto/plugins/openid/__init__.py\n@@ -48,7 +48,7 @@\n \n         # Check cache if these tokens were already verified.\n         hmac_tokens = core_utils.hmac_digest(hmac_secret, access_token)\n-        cache_key = 'openid:verify:%s'.format(hmac_tokens)\n+        cache_key = 'openid:verify:{}'.format(hmac_tokens)\n         payload = request.registry.cache.get(cache_key)\n         if payload is None:\n             # This can take some time.\ndiff --git a/setup.py b/setup.py\n--- a/setup.py\n+++ b/setup.py\n@@ -83,7 +83,7 @@\n \n \n setup(name='kinto',\n-      version='9.1.1',\n+      version='9.1.2',\n       description='Kinto Web Service - Store, Sync, Share, and Self-Host.',\n       long_description='{}\\n\\n{}\\n\\n{}'.format(README, CHANGELOG, CONTRIBUTORS),\n       license='Apache License (2.0)',\n", "issue": "OpenID payload cache uses the wrong cache key\nThe `cache_key` becomes the same for every access token due to this bug: https://github.com/Kinto/kinto/blob/e1e0d6be0024418fd100210901f9d2ca06344fe1/kinto/plugins/openid/__init__.py#L51\r\nNo matter what the `hmac_tokens` variable is the `cache_key` always becomes `'openid:verify:%s'`.\r\n\r\n\n", "before_files": [{"content": "import codecs\nimport os\nfrom setuptools import setup, find_packages\n\n# abspath here because setup.py may be __main__, in which case\n# __file__ is not guaranteed to be absolute\nhere = os.path.abspath(os.path.dirname(__file__))\n\n\ndef read_file(filename):\n    \"\"\"Open a related file and return its content.\"\"\"\n    with codecs.open(os.path.join(here, filename), encoding='utf-8') as f:\n        content = f.read()\n    return content\n\n\nREADME = read_file('README.rst')\nCHANGELOG = read_file('CHANGELOG.rst')\nCONTRIBUTORS = read_file('CONTRIBUTORS.rst')\n\nREQUIREMENTS = [\n    'bcrypt',\n    'colander >= 1.4.0',\n    'cornice',\n    'cornice_swagger >= 0.5.1',\n    'dockerflow',\n    'jsonschema',\n    'jsonpatch',\n    'logging-color-formatter >= 1.0.1',  # Message interpolations.\n    'python-dateutil',\n    'pyramid >= 1.9.1, < 2.0',\n    'pyramid_multiauth >= 0.8',  # User on policy selected event.\n    'transaction',\n    # pyramid_tm changed the location of their tween in 2.x and one of\n    # our tests fails on 2.0.\n    'pyramid_tm >= 2.1',\n    'requests',\n    'waitress',\n    'ujson >= 1.35',\n]\n\nPOSTGRESQL_REQUIRES = [\n    'SQLAlchemy',\n    'psycopg2 > 2.5',\n    'zope.sqlalchemy',\n]\n\nREDIS_REQUIRES = [\n    'kinto_redis'\n]\n\nMEMCACHED_REQUIRES = [\n    'python-memcached'\n]\n\nSETUP_REQUIRES = [\n    'pytest-runner'\n]\n\nTEST_REQUIREMENTS = [\n    'bravado_core',\n    'pytest',\n    'WebTest'\n]\n\nDEPENDENCY_LINKS = []\n\nMONITORING_REQUIRES = [\n    'raven',\n    'statsd',\n    'newrelic',\n    'werkzeug',\n]\n\nENTRY_POINTS = {\n    'paste.app_factory': [\n        'main = kinto:main',\n    ],\n    'console_scripts': [\n        'kinto = kinto.__main__:main'\n    ],\n}\n\n\nsetup(name='kinto',\n      version='9.1.1',\n      description='Kinto Web Service - Store, Sync, Share, and Self-Host.',\n      long_description='{}\\n\\n{}\\n\\n{}'.format(README, CHANGELOG, CONTRIBUTORS),\n      license='Apache License (2.0)',\n      classifiers=[\n          'Programming Language :: Python',\n          'Programming Language :: Python :: 3',\n          'Programming Language :: Python :: 3.5',\n          'Programming Language :: Python :: 3.6',\n          'Programming Language :: Python :: Implementation :: CPython',\n          'Topic :: Internet :: WWW/HTTP',\n          'Topic :: Internet :: WWW/HTTP :: WSGI :: Application',\n          'License :: OSI Approved :: Apache Software License'\n      ],\n      keywords='web sync json storage services',\n      author='Mozilla Services',\n      author_email='storage-team@mozilla.com',\n      url='https://github.com/Kinto/kinto',\n      packages=find_packages(),\n      package_data={'': ['*.rst', '*.py', '*.yaml']},\n      include_package_data=True,\n      zip_safe=False,\n      setup_requires=SETUP_REQUIRES,\n      tests_require=TEST_REQUIREMENTS,\n      install_requires=REQUIREMENTS,\n      extras_require={\n          'redis': REDIS_REQUIRES,\n          'memcached': MEMCACHED_REQUIRES,\n          'postgresql': POSTGRESQL_REQUIRES,\n          'monitoring': MONITORING_REQUIRES,\n      },\n      test_suite='tests',\n      dependency_links=DEPENDENCY_LINKS,\n      entry_points=ENTRY_POINTS)\n", "path": "setup.py"}, {"content": "import re\n\nimport requests\nfrom pyramid import authentication as base_auth\nfrom pyramid.interfaces import IAuthenticationPolicy\nfrom zope.interface import implementer\n\nfrom kinto.core import logger\nfrom kinto.core import utils as core_utils\nfrom kinto.core.openapi import OpenAPI\n\nfrom .utils import fetch_openid_config\n\n\n@implementer(IAuthenticationPolicy)\nclass OpenIDConnectPolicy(base_auth.CallbackAuthenticationPolicy):\n    def __init__(self, issuer, client_id, realm='Realm', **kwargs):\n        self.realm = realm\n        self.issuer = issuer\n        self.client_id = client_id\n        self.client_secret = kwargs.get('client_secret', '')\n        self.header_type = kwargs.get('header_type', 'Bearer')\n        self.userid_field = kwargs.get('userid_field', 'sub')\n        self.verification_ttl = int(kwargs.get('verification_ttl_seconds', 86400))\n\n        # Fetch OpenID config (at instantiation, ie. startup)\n        self.oid_config = fetch_openid_config(issuer)\n\n        self._jwt_keys = None\n\n    def unauthenticated_userid(self, request):\n        \"\"\"Return the userid or ``None`` if token could not be verified.\n        \"\"\"\n        settings = request.registry.settings\n        hmac_secret = settings['userid_hmac_secret']\n\n        authorization = request.headers.get('Authorization', '')\n        try:\n            authmeth, access_token = authorization.split(' ', 1)\n        except ValueError:\n            return None\n\n        if authmeth.lower() != self.header_type.lower():\n            return None\n\n        # XXX JWT Access token\n        # https://auth0.com/docs/tokens/access-token#access-token-format\n\n        # Check cache if these tokens were already verified.\n        hmac_tokens = core_utils.hmac_digest(hmac_secret, access_token)\n        cache_key = 'openid:verify:%s'.format(hmac_tokens)\n        payload = request.registry.cache.get(cache_key)\n        if payload is None:\n            # This can take some time.\n            payload = self._verify_token(access_token)\n            if payload is None:\n                return None\n        # Save for next time / refresh ttl.\n        request.registry.cache.set(cache_key, payload, ttl=self.verification_ttl)\n        # Extract meaningful field from userinfo (eg. email or sub)\n        return payload.get(self.userid_field)\n\n    def forget(self, request):\n        \"\"\"A no-op. Credentials are sent on every request.\n        Return WWW-Authenticate Realm header for Bearer token.\n        \"\"\"\n        return [('WWW-Authenticate', '%s realm=\"%s\"' % (self.header_type, self.realm))]\n\n    def _verify_token(self, access_token):\n        uri = self.oid_config['userinfo_endpoint']\n        # Opaque access token string. Fetch user info from profile.\n        try:\n            resp = requests.get(uri, headers={'Authorization': 'Bearer ' + access_token})\n            resp.raise_for_status()\n            userprofile = resp.json()\n            return userprofile\n\n        except (requests.exceptions.HTTPError, ValueError, KeyError) as e:\n            logger.debug('Unable to fetch user profile from %s (%s)' % (uri, e))\n            return None\n\n\ndef includeme(config):\n    # Activate end-points.\n    config.scan('kinto.plugins.openid.views')\n\n    settings = config.get_settings()\n\n    openid_policies = []\n    for k, v in settings.items():\n        m = re.match('multiauth\\.policy\\.(.*)\\.use', k)\n        if m:\n            if v.endswith('OpenIDConnectPolicy'):\n                openid_policies.append(m.group(1))\n\n    if len(openid_policies) == 0:\n        # Do not add the capability if no policy is configured.\n        return\n\n    providers_infos = []\n    for name in openid_policies:\n        issuer = settings['multiauth.policy.%s.issuer' % name]\n        openid_config = fetch_openid_config(issuer)\n\n        client_id = settings['multiauth.policy.%s.client_id' % name]\n        header_type = settings.get('multiauth.policy.%s.header_type', 'Bearer')\n\n        providers_infos.append({\n            'name': name,\n            'issuer': openid_config['issuer'],\n            'auth_path': '/openid/%s/login' % name,\n            'client_id': client_id,\n            'header_type': header_type,\n            'userinfo_endpoint': openid_config['userinfo_endpoint'],\n        })\n\n        OpenAPI.expose_authentication_method(name, {\n            'type': 'oauth2',\n            'authorizationUrl': openid_config['authorization_endpoint'],\n        })\n\n    config.add_api_capability(\n        'openid',\n        description='OpenID connect support.',\n        url='http://kinto.readthedocs.io/en/stable/api/1.x/authentication.html',\n        providers=providers_infos)\n", "path": "kinto/plugins/openid/__init__.py"}], "after_files": [{"content": "import codecs\nimport os\nfrom setuptools import setup, find_packages\n\n# abspath here because setup.py may be __main__, in which case\n# __file__ is not guaranteed to be absolute\nhere = os.path.abspath(os.path.dirname(__file__))\n\n\ndef read_file(filename):\n    \"\"\"Open a related file and return its content.\"\"\"\n    with codecs.open(os.path.join(here, filename), encoding='utf-8') as f:\n        content = f.read()\n    return content\n\n\nREADME = read_file('README.rst')\nCHANGELOG = read_file('CHANGELOG.rst')\nCONTRIBUTORS = read_file('CONTRIBUTORS.rst')\n\nREQUIREMENTS = [\n    'bcrypt',\n    'colander >= 1.4.0',\n    'cornice',\n    'cornice_swagger >= 0.5.1',\n    'dockerflow',\n    'jsonschema',\n    'jsonpatch',\n    'logging-color-formatter >= 1.0.1',  # Message interpolations.\n    'python-dateutil',\n    'pyramid >= 1.9.1, < 2.0',\n    'pyramid_multiauth >= 0.8',  # User on policy selected event.\n    'transaction',\n    # pyramid_tm changed the location of their tween in 2.x and one of\n    # our tests fails on 2.0.\n    'pyramid_tm >= 2.1',\n    'requests',\n    'waitress',\n    'ujson >= 1.35',\n]\n\nPOSTGRESQL_REQUIRES = [\n    'SQLAlchemy',\n    'psycopg2 > 2.5',\n    'zope.sqlalchemy',\n]\n\nREDIS_REQUIRES = [\n    'kinto_redis'\n]\n\nMEMCACHED_REQUIRES = [\n    'python-memcached'\n]\n\nSETUP_REQUIRES = [\n    'pytest-runner'\n]\n\nTEST_REQUIREMENTS = [\n    'bravado_core',\n    'pytest',\n    'WebTest'\n]\n\nDEPENDENCY_LINKS = []\n\nMONITORING_REQUIRES = [\n    'raven',\n    'statsd',\n    'newrelic',\n    'werkzeug',\n]\n\nENTRY_POINTS = {\n    'paste.app_factory': [\n        'main = kinto:main',\n    ],\n    'console_scripts': [\n        'kinto = kinto.__main__:main'\n    ],\n}\n\n\nsetup(name='kinto',\n      version='9.1.2',\n      description='Kinto Web Service - Store, Sync, Share, and Self-Host.',\n      long_description='{}\\n\\n{}\\n\\n{}'.format(README, CHANGELOG, CONTRIBUTORS),\n      license='Apache License (2.0)',\n      classifiers=[\n          'Programming Language :: Python',\n          'Programming Language :: Python :: 3',\n          'Programming Language :: Python :: 3.5',\n          'Programming Language :: Python :: 3.6',\n          'Programming Language :: Python :: Implementation :: CPython',\n          'Topic :: Internet :: WWW/HTTP',\n          'Topic :: Internet :: WWW/HTTP :: WSGI :: Application',\n          'License :: OSI Approved :: Apache Software License'\n      ],\n      keywords='web sync json storage services',\n      author='Mozilla Services',\n      author_email='storage-team@mozilla.com',\n      url='https://github.com/Kinto/kinto',\n      packages=find_packages(),\n      package_data={'': ['*.rst', '*.py', '*.yaml']},\n      include_package_data=True,\n      zip_safe=False,\n      setup_requires=SETUP_REQUIRES,\n      tests_require=TEST_REQUIREMENTS,\n      install_requires=REQUIREMENTS,\n      extras_require={\n          'redis': REDIS_REQUIRES,\n          'memcached': MEMCACHED_REQUIRES,\n          'postgresql': POSTGRESQL_REQUIRES,\n          'monitoring': MONITORING_REQUIRES,\n      },\n      test_suite='tests',\n      dependency_links=DEPENDENCY_LINKS,\n      entry_points=ENTRY_POINTS)\n", "path": "setup.py"}, {"content": "import re\n\nimport requests\nfrom pyramid import authentication as base_auth\nfrom pyramid.interfaces import IAuthenticationPolicy\nfrom zope.interface import implementer\n\nfrom kinto.core import logger\nfrom kinto.core import utils as core_utils\nfrom kinto.core.openapi import OpenAPI\n\nfrom .utils import fetch_openid_config\n\n\n@implementer(IAuthenticationPolicy)\nclass OpenIDConnectPolicy(base_auth.CallbackAuthenticationPolicy):\n    def __init__(self, issuer, client_id, realm='Realm', **kwargs):\n        self.realm = realm\n        self.issuer = issuer\n        self.client_id = client_id\n        self.client_secret = kwargs.get('client_secret', '')\n        self.header_type = kwargs.get('header_type', 'Bearer')\n        self.userid_field = kwargs.get('userid_field', 'sub')\n        self.verification_ttl = int(kwargs.get('verification_ttl_seconds', 86400))\n\n        # Fetch OpenID config (at instantiation, ie. startup)\n        self.oid_config = fetch_openid_config(issuer)\n\n        self._jwt_keys = None\n\n    def unauthenticated_userid(self, request):\n        \"\"\"Return the userid or ``None`` if token could not be verified.\n        \"\"\"\n        settings = request.registry.settings\n        hmac_secret = settings['userid_hmac_secret']\n\n        authorization = request.headers.get('Authorization', '')\n        try:\n            authmeth, access_token = authorization.split(' ', 1)\n        except ValueError:\n            return None\n\n        if authmeth.lower() != self.header_type.lower():\n            return None\n\n        # XXX JWT Access token\n        # https://auth0.com/docs/tokens/access-token#access-token-format\n\n        # Check cache if these tokens were already verified.\n        hmac_tokens = core_utils.hmac_digest(hmac_secret, access_token)\n        cache_key = 'openid:verify:{}'.format(hmac_tokens)\n        payload = request.registry.cache.get(cache_key)\n        if payload is None:\n            # This can take some time.\n            payload = self._verify_token(access_token)\n            if payload is None:\n                return None\n        # Save for next time / refresh ttl.\n        request.registry.cache.set(cache_key, payload, ttl=self.verification_ttl)\n        # Extract meaningful field from userinfo (eg. email or sub)\n        return payload.get(self.userid_field)\n\n    def forget(self, request):\n        \"\"\"A no-op. Credentials are sent on every request.\n        Return WWW-Authenticate Realm header for Bearer token.\n        \"\"\"\n        return [('WWW-Authenticate', '%s realm=\"%s\"' % (self.header_type, self.realm))]\n\n    def _verify_token(self, access_token):\n        uri = self.oid_config['userinfo_endpoint']\n        # Opaque access token string. Fetch user info from profile.\n        try:\n            resp = requests.get(uri, headers={'Authorization': 'Bearer ' + access_token})\n            resp.raise_for_status()\n            userprofile = resp.json()\n            return userprofile\n\n        except (requests.exceptions.HTTPError, ValueError, KeyError) as e:\n            logger.debug('Unable to fetch user profile from %s (%s)' % (uri, e))\n            return None\n\n\ndef includeme(config):\n    # Activate end-points.\n    config.scan('kinto.plugins.openid.views')\n\n    settings = config.get_settings()\n\n    openid_policies = []\n    for k, v in settings.items():\n        m = re.match('multiauth\\.policy\\.(.*)\\.use', k)\n        if m:\n            if v.endswith('OpenIDConnectPolicy'):\n                openid_policies.append(m.group(1))\n\n    if len(openid_policies) == 0:\n        # Do not add the capability if no policy is configured.\n        return\n\n    providers_infos = []\n    for name in openid_policies:\n        issuer = settings['multiauth.policy.%s.issuer' % name]\n        openid_config = fetch_openid_config(issuer)\n\n        client_id = settings['multiauth.policy.%s.client_id' % name]\n        header_type = settings.get('multiauth.policy.%s.header_type', 'Bearer')\n\n        providers_infos.append({\n            'name': name,\n            'issuer': openid_config['issuer'],\n            'auth_path': '/openid/%s/login' % name,\n            'client_id': client_id,\n            'header_type': header_type,\n            'userinfo_endpoint': openid_config['userinfo_endpoint'],\n        })\n\n        OpenAPI.expose_authentication_method(name, {\n            'type': 'oauth2',\n            'authorizationUrl': openid_config['authorization_endpoint'],\n        })\n\n    config.add_api_capability(\n        'openid',\n        description='OpenID connect support.',\n        url='http://kinto.readthedocs.io/en/stable/api/1.x/authentication.html',\n        providers=providers_infos)\n", "path": "kinto/plugins/openid/__init__.py"}]}