We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
OpenID authentication plugin is missing a profile view
When validating the token, the plugn is fetching the profile, but there are no endpoint available to fetch this info: https://github.com/Kinto/kinto/blob/master/kinto/plugins/openid/__init__.py#L74

I would be in favor of adding a `/v1/openid/google/profile` view in order to grab the username and the profile_pic of the user.

This information might also be added in the `user` field of the `/v1/` endpoint. What do you think?

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `kinto/core/__init__.py`
Content:
```
1 """Main entry point
2 """
3 import logging
4 import pkg_resources
5 import tempfile
6 
7 from cornice import Service as CorniceService
8 from dockerflow import logging as dockerflow_logging
9 from pyramid.settings import aslist
10 
11 from kinto.core import errors
12 from kinto.core import events
13 from kinto.core.initialization import (  # NOQA
14     initialize,
15     install_middlewares,
16     load_default_settings,
17 )
18 from kinto.core.utils import (
19     follow_subrequest,
20     current_service,
21     current_resource_name,
22     prefixed_userid,
23     prefixed_principals,
24     log_context,
25 )
26 
27 
28 logger = logging.getLogger(__name__)
29 
30 
31 # Module version, as defined in PEP-0396.
32 __version__ = pkg_resources.get_distribution("kinto").version  # FIXME?
33 
34 DEFAULT_SETTINGS = {
35     "backoff": None,
36     "backoff_percentage": None,
37     "batch_max_requests": 25,
38     "cache_backend": "",
39     "cache_hosts": "",
40     "cache_url": "",
41     "cache_pool_size": 25,
42     "cache_prefix": "",
43     "cache_max_size_bytes": 524288,
44     "cors_origins": "*",
45     "cors_max_age_seconds": 3600,
46     "eos": None,
47     "eos_message": None,
48     "eos_url": None,
49     "error_info_link": "https://github.com/Kinto/kinto/issues/",
50     "http_host": None,
51     "http_scheme": None,
52     "id_generator": "kinto.core.storage.generators.UUID4",
53     "includes": "",
54     "initialization_sequence": (
55         "kinto.core.initialization.setup_request_bound_data",
56         "kinto.core.initialization.setup_json_serializer",
57         "kinto.core.initialization.setup_logging",
58         "kinto.core.initialization.setup_storage",
59         "kinto.core.initialization.setup_permission",
60         "kinto.core.initialization.setup_cache",
61         "kinto.core.initialization.setup_requests_scheme",
62         "kinto.core.initialization.setup_version_redirection",
63         "kinto.core.initialization.setup_deprecation",
64         "kinto.core.initialization.setup_authentication",
65         "kinto.core.initialization.setup_backoff",
66         "kinto.core.initialization.setup_statsd",
67         "kinto.core.initialization.setup_listeners",
68         "kinto.core.events.setup_transaction_hook",
69     ),
70     "event_listeners": "",
71     "heartbeat_timeout_seconds": 10,
72     "newrelic_config": None,
73     "newrelic_env": "dev",
74     "paginate_by": None,
75     "pagination_token_validity_seconds": 10 * 60,
76     "permission_backend": "",
77     "permission_url": "",
78     "permission_pool_size": 25,
79     "profiler_dir": tempfile.gettempdir(),
80     "profiler_enabled": False,
81     "project_docs": "",
82     "project_name": "",
83     "project_version": "",
84     "readonly": False,
85     "retry_after_seconds": 30,
86     "settings_prefix": "",
87     "statsd_backend": "kinto.core.statsd",
88     "statsd_prefix": "kinto.core",
89     "statsd_url": None,
90     "storage_backend": "",
91     "storage_url": "",
92     "storage_max_fetch_size": 10000,
93     "storage_pool_size": 25,
94     "tm.annotate_user": False,  # Do annotate transactions with the user-id.
95     "transaction_per_request": True,
96     "userid_hmac_secret": "",
97     "version_json_path": "version.json",
98     "version_prefix_redirect_enabled": True,
99     "trailing_slash_redirect_enabled": True,
100     "multiauth.groupfinder": "kinto.core.authorization.groupfinder",
101     "multiauth.policies": "",
102     "multiauth.policy.basicauth.use": (
103         "kinto.core.authentication." "BasicAuthAuthenticationPolicy"
104     ),
105     "multiauth.authorization_policy": ("kinto.core.authorization." "AuthorizationPolicy"),
106 }
107 
108 
109 class Service(CorniceService):
110     """Subclass of the default cornice service.
111 
112     This is useful in order to attach specific behaviours without monkey
113     patching the default cornice service (which would impact other uses of it)
114     """
115 
116     default_cors_headers = ("Backoff", "Retry-After", "Alert", "Content-Length")
117 
118     def error_handler(self, request):
119         return errors.json_error_handler(request)
120 
121     @classmethod
122     def init_from_settings(cls, settings):
123         cls.cors_origins = tuple(aslist(settings["cors_origins"]))
124         cors_max_age = settings["cors_max_age_seconds"]
125         cls.cors_max_age = int(cors_max_age) if cors_max_age else None
126 
127 
128 class JsonLogFormatter(dockerflow_logging.JsonLogFormatter):
129     logger_name = "kinto"
130 
131     @classmethod
132     def init_from_settings(cls, settings):
133         cls.logger_name = settings["project_name"]
134 
135     def __init__(self, fmt=None, datefmt=None, style="%"):
136         # Do not let mozilla-cloud-services-logger constructor to improperly
137         # use style as the logger_name.
138         # See https://github.com/mozilla/mozilla-cloud-services-logger/issues/3
139         logger_name = self.logger_name
140         super().__init__(fmt, datefmt, style)
141         self.logger_name = logger_name
142 
143 
144 def get_user_info(request):
145     # Default user info (shown in hello view for example).
146     return {"id": request.prefixed_userid, "principals": request.prefixed_principals}
147 
148 
149 def includeme(config):
150     settings = config.get_settings()
151 
152     # Heartbeat registry.
153     config.registry.heartbeats = {}
154 
155     # Public settings registry.
156     config.registry.public_settings = {"batch_max_requests", "readonly"}
157 
158     # Directive to declare arbitrary API capabilities.
159     def add_api_capability(config, identifier, description="", url="", **kw):
160         existing = config.registry.api_capabilities.get(identifier)
161         if existing:
162             error_msg = "The '{}' API capability was already registered ({})."
163             raise ValueError(error_msg.format(identifier, existing))
164 
165         capability = dict(description=description, url=url, **kw)
166         config.registry.api_capabilities[identifier] = capability
167 
168     config.add_directive("add_api_capability", add_api_capability)
169     config.registry.api_capabilities = {}
170 
171     # Resource events helpers.
172     config.add_request_method(events.get_resource_events, name="get_resource_events")
173     config.add_request_method(events.notify_resource_event, name="notify_resource_event")
174 
175     # Setup cornice.
176     config.include("cornice")
177 
178     # Setup cornice api documentation
179     config.include("cornice_swagger")
180 
181     # Per-request transaction.
182     config.include("pyramid_tm")
183 
184     # Add CORS settings to the base kinto.core Service class.
185     Service.init_from_settings(settings)
186 
187     # Use the project name as the main logger name (Logger field in MozLog).
188     JsonLogFormatter.init_from_settings(settings)
189 
190     # Setup components.
191     for step in aslist(settings["initialization_sequence"]):
192         step_func = config.maybe_dotted(step)
193         step_func(config)
194 
195     # Custom helpers.
196     config.add_request_method(log_context)
197     config.add_request_method(follow_subrequest)
198     config.add_request_method(prefixed_userid, property=True)
199     config.add_request_method(prefixed_principals, reify=True)
200     config.add_request_method(get_user_info, name="get_user_info")
201     config.add_request_method(current_resource_name, reify=True)
202     config.add_request_method(current_service, reify=True)
203     config.commit()
204 
205     # Include plugins after init, unlike pyramid includes.
206     includes = aslist(settings["includes"])
207     for app in includes:
208         config.include(app)
209 
210     # # Show settings to output.
211     # for key, value in settings.items():
212     #     logger.info('Using {} = {}'.format(key, value))
213 
214     # Scan views.
215     config.scan("kinto.core.views")
216 
217     # Give sign of life.
218     msg = "Running {project_name} {project_version}."
219     logger.info(msg.format_map(settings))
220
```

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

{"golden_diff": "diff --git a/kinto/core/__init__.py b/kinto/core/__init__.py\n--- a/kinto/core/__init__.py\n+++ b/kinto/core/__init__.py\n@@ -143,7 +143,10 @@\n \n def get_user_info(request):\n     # Default user info (shown in hello view for example).\n-    return {\"id\": request.prefixed_userid, \"principals\": request.prefixed_principals}\n+    user_info = {\"id\": request.prefixed_userid, \"principals\": request.prefixed_principals}\n+    if hasattr(request, \"get_user_profile\"):\n+        user_info[\"profile\"] = request.get_user_profile()\n+    return user_info\n \n \n def includeme(config):\ndiff --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@@ -56,6 +56,7 @@\n                 return None\n         # Save for next time / refresh ttl.\n         request.registry.cache.set(cache_key, payload, ttl=self.verification_ttl)\n+        request.bound_data[\"user_profile\"] = payload\n         # Extract meaningful field from userinfo (eg. email or sub)\n         return payload.get(self.userid_field)\n \n@@ -79,6 +80,10 @@\n             return None\n \n \n+def get_user_profile(request):\n+    return request.bound_data.get(\"user_profile\", {})\n+\n+\n def includeme(config):\n     # Activate end-points.\n     config.scan(\"kinto.plugins.openid.views\")\n@@ -124,3 +129,4 @@\n         url=\"http://kinto.readthedocs.io/en/stable/api/1.x/authentication.html\",\n         providers=providers_infos,\n     )\n+    config.add_request_method(get_user_profile, name=\"get_user_profile\")\n", "issue": "OpenID authentication plugin is missing a profile view\nWhen validating the token, the plugn is fetching the profile, but there are no endpoint available to fetch this info: https://github.com/Kinto/kinto/blob/master/kinto/plugins/openid/__init__.py#L74\r\n\r\nI would be in favor of adding a `/v1/openid/google/profile` view in order to grab the username and the profile_pic of the user.\r\n\r\nThis information might also be added in the `user` field of the `/v1/` endpoint. What do you think?\n", "before_files": [{"content": "\"\"\"Main entry point\n\"\"\"\nimport logging\nimport pkg_resources\nimport tempfile\n\nfrom cornice import Service as CorniceService\nfrom dockerflow import logging as dockerflow_logging\nfrom pyramid.settings import aslist\n\nfrom kinto.core import errors\nfrom kinto.core import events\nfrom kinto.core.initialization import (  # NOQA\n    initialize,\n    install_middlewares,\n    load_default_settings,\n)\nfrom kinto.core.utils import (\n    follow_subrequest,\n    current_service,\n    current_resource_name,\n    prefixed_userid,\n    prefixed_principals,\n    log_context,\n)\n\n\nlogger = logging.getLogger(__name__)\n\n\n# Module version, as defined in PEP-0396.\n__version__ = pkg_resources.get_distribution(\"kinto\").version  # FIXME?\n\nDEFAULT_SETTINGS = {\n    \"backoff\": None,\n    \"backoff_percentage\": None,\n    \"batch_max_requests\": 25,\n    \"cache_backend\": \"\",\n    \"cache_hosts\": \"\",\n    \"cache_url\": \"\",\n    \"cache_pool_size\": 25,\n    \"cache_prefix\": \"\",\n    \"cache_max_size_bytes\": 524288,\n    \"cors_origins\": \"*\",\n    \"cors_max_age_seconds\": 3600,\n    \"eos\": None,\n    \"eos_message\": None,\n    \"eos_url\": None,\n    \"error_info_link\": \"https://github.com/Kinto/kinto/issues/\",\n    \"http_host\": None,\n    \"http_scheme\": None,\n    \"id_generator\": \"kinto.core.storage.generators.UUID4\",\n    \"includes\": \"\",\n    \"initialization_sequence\": (\n        \"kinto.core.initialization.setup_request_bound_data\",\n        \"kinto.core.initialization.setup_json_serializer\",\n        \"kinto.core.initialization.setup_logging\",\n        \"kinto.core.initialization.setup_storage\",\n        \"kinto.core.initialization.setup_permission\",\n        \"kinto.core.initialization.setup_cache\",\n        \"kinto.core.initialization.setup_requests_scheme\",\n        \"kinto.core.initialization.setup_version_redirection\",\n        \"kinto.core.initialization.setup_deprecation\",\n        \"kinto.core.initialization.setup_authentication\",\n        \"kinto.core.initialization.setup_backoff\",\n        \"kinto.core.initialization.setup_statsd\",\n        \"kinto.core.initialization.setup_listeners\",\n        \"kinto.core.events.setup_transaction_hook\",\n    ),\n    \"event_listeners\": \"\",\n    \"heartbeat_timeout_seconds\": 10,\n    \"newrelic_config\": None,\n    \"newrelic_env\": \"dev\",\n    \"paginate_by\": None,\n    \"pagination_token_validity_seconds\": 10 * 60,\n    \"permission_backend\": \"\",\n    \"permission_url\": \"\",\n    \"permission_pool_size\": 25,\n    \"profiler_dir\": tempfile.gettempdir(),\n    \"profiler_enabled\": False,\n    \"project_docs\": \"\",\n    \"project_name\": \"\",\n    \"project_version\": \"\",\n    \"readonly\": False,\n    \"retry_after_seconds\": 30,\n    \"settings_prefix\": \"\",\n    \"statsd_backend\": \"kinto.core.statsd\",\n    \"statsd_prefix\": \"kinto.core\",\n    \"statsd_url\": None,\n    \"storage_backend\": \"\",\n    \"storage_url\": \"\",\n    \"storage_max_fetch_size\": 10000,\n    \"storage_pool_size\": 25,\n    \"tm.annotate_user\": False,  # Do annotate transactions with the user-id.\n    \"transaction_per_request\": True,\n    \"userid_hmac_secret\": \"\",\n    \"version_json_path\": \"version.json\",\n    \"version_prefix_redirect_enabled\": True,\n    \"trailing_slash_redirect_enabled\": True,\n    \"multiauth.groupfinder\": \"kinto.core.authorization.groupfinder\",\n    \"multiauth.policies\": \"\",\n    \"multiauth.policy.basicauth.use\": (\n        \"kinto.core.authentication.\" \"BasicAuthAuthenticationPolicy\"\n    ),\n    \"multiauth.authorization_policy\": (\"kinto.core.authorization.\" \"AuthorizationPolicy\"),\n}\n\n\nclass Service(CorniceService):\n    \"\"\"Subclass of the default cornice service.\n\n    This is useful in order to attach specific behaviours without monkey\n    patching the default cornice service (which would impact other uses of it)\n    \"\"\"\n\n    default_cors_headers = (\"Backoff\", \"Retry-After\", \"Alert\", \"Content-Length\")\n\n    def error_handler(self, request):\n        return errors.json_error_handler(request)\n\n    @classmethod\n    def init_from_settings(cls, settings):\n        cls.cors_origins = tuple(aslist(settings[\"cors_origins\"]))\n        cors_max_age = settings[\"cors_max_age_seconds\"]\n        cls.cors_max_age = int(cors_max_age) if cors_max_age else None\n\n\nclass JsonLogFormatter(dockerflow_logging.JsonLogFormatter):\n    logger_name = \"kinto\"\n\n    @classmethod\n    def init_from_settings(cls, settings):\n        cls.logger_name = settings[\"project_name\"]\n\n    def __init__(self, fmt=None, datefmt=None, style=\"%\"):\n        # Do not let mozilla-cloud-services-logger constructor to improperly\n        # use style as the logger_name.\n        # See https://github.com/mozilla/mozilla-cloud-services-logger/issues/3\n        logger_name = self.logger_name\n        super().__init__(fmt, datefmt, style)\n        self.logger_name = logger_name\n\n\ndef get_user_info(request):\n    # Default user info (shown in hello view for example).\n    return {\"id\": request.prefixed_userid, \"principals\": request.prefixed_principals}\n\n\ndef includeme(config):\n    settings = config.get_settings()\n\n    # Heartbeat registry.\n    config.registry.heartbeats = {}\n\n    # Public settings registry.\n    config.registry.public_settings = {\"batch_max_requests\", \"readonly\"}\n\n    # Directive to declare arbitrary API capabilities.\n    def add_api_capability(config, identifier, description=\"\", url=\"\", **kw):\n        existing = config.registry.api_capabilities.get(identifier)\n        if existing:\n            error_msg = \"The '{}' API capability was already registered ({}).\"\n            raise ValueError(error_msg.format(identifier, existing))\n\n        capability = dict(description=description, url=url, **kw)\n        config.registry.api_capabilities[identifier] = capability\n\n    config.add_directive(\"add_api_capability\", add_api_capability)\n    config.registry.api_capabilities = {}\n\n    # Resource events helpers.\n    config.add_request_method(events.get_resource_events, name=\"get_resource_events\")\n    config.add_request_method(events.notify_resource_event, name=\"notify_resource_event\")\n\n    # Setup cornice.\n    config.include(\"cornice\")\n\n    # Setup cornice api documentation\n    config.include(\"cornice_swagger\")\n\n    # Per-request transaction.\n    config.include(\"pyramid_tm\")\n\n    # Add CORS settings to the base kinto.core Service class.\n    Service.init_from_settings(settings)\n\n    # Use the project name as the main logger name (Logger field in MozLog).\n    JsonLogFormatter.init_from_settings(settings)\n\n    # Setup components.\n    for step in aslist(settings[\"initialization_sequence\"]):\n        step_func = config.maybe_dotted(step)\n        step_func(config)\n\n    # Custom helpers.\n    config.add_request_method(log_context)\n    config.add_request_method(follow_subrequest)\n    config.add_request_method(prefixed_userid, property=True)\n    config.add_request_method(prefixed_principals, reify=True)\n    config.add_request_method(get_user_info, name=\"get_user_info\")\n    config.add_request_method(current_resource_name, reify=True)\n    config.add_request_method(current_service, reify=True)\n    config.commit()\n\n    # Include plugins after init, unlike pyramid includes.\n    includes = aslist(settings[\"includes\"])\n    for app in includes:\n        config.include(app)\n\n    # # Show settings to output.\n    # for key, value in settings.items():\n    #     logger.info('Using {} = {}'.format(key, value))\n\n    # Scan views.\n    config.scan(\"kinto.core.views\")\n\n    # Give sign of life.\n    msg = \"Running {project_name} {project_version}.\"\n    logger.info(msg.format_map(settings))\n", "path": "kinto/core/__init__.py"}, {"content": "import requests\nfrom pyramid import authentication as base_auth\nfrom pyramid.settings import aslist\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 = f\"openid:verify:{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 policy in aslist(settings[\"multiauth.policies\"]):\n        v = settings.get(\"multiauth.policy.%s.use\" % policy, \"\")\n        if v.endswith(\"OpenIDConnectPolicy\"):\n            openid_policies.append(policy)\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            {\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\n        OpenAPI.expose_authentication_method(\n            name, {\"type\": \"oauth2\", \"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    )\n", "path": "kinto/plugins/openid/__init__.py"}], "after_files": [{"content": "\"\"\"Main entry point\n\"\"\"\nimport logging\nimport pkg_resources\nimport tempfile\n\nfrom cornice import Service as CorniceService\nfrom dockerflow import logging as dockerflow_logging\nfrom pyramid.settings import aslist\n\nfrom kinto.core import errors\nfrom kinto.core import events\nfrom kinto.core.initialization import (  # NOQA\n    initialize,\n    install_middlewares,\n    load_default_settings,\n)\nfrom kinto.core.utils import (\n    follow_subrequest,\n    current_service,\n    current_resource_name,\n    prefixed_userid,\n    prefixed_principals,\n    log_context,\n)\n\n\nlogger = logging.getLogger(__name__)\n\n\n# Module version, as defined in PEP-0396.\n__version__ = pkg_resources.get_distribution(\"kinto\").version  # FIXME?\n\nDEFAULT_SETTINGS = {\n    \"backoff\": None,\n    \"backoff_percentage\": None,\n    \"batch_max_requests\": 25,\n    \"cache_backend\": \"\",\n    \"cache_hosts\": \"\",\n    \"cache_url\": \"\",\n    \"cache_pool_size\": 25,\n    \"cache_prefix\": \"\",\n    \"cache_max_size_bytes\": 524288,\n    \"cors_origins\": \"*\",\n    \"cors_max_age_seconds\": 3600,\n    \"eos\": None,\n    \"eos_message\": None,\n    \"eos_url\": None,\n    \"error_info_link\": \"https://github.com/Kinto/kinto/issues/\",\n    \"http_host\": None,\n    \"http_scheme\": None,\n    \"id_generator\": \"kinto.core.storage.generators.UUID4\",\n    \"includes\": \"\",\n    \"initialization_sequence\": (\n        \"kinto.core.initialization.setup_request_bound_data\",\n        \"kinto.core.initialization.setup_json_serializer\",\n        \"kinto.core.initialization.setup_logging\",\n        \"kinto.core.initialization.setup_storage\",\n        \"kinto.core.initialization.setup_permission\",\n        \"kinto.core.initialization.setup_cache\",\n        \"kinto.core.initialization.setup_requests_scheme\",\n        \"kinto.core.initialization.setup_version_redirection\",\n        \"kinto.core.initialization.setup_deprecation\",\n        \"kinto.core.initialization.setup_authentication\",\n        \"kinto.core.initialization.setup_backoff\",\n        \"kinto.core.initialization.setup_statsd\",\n        \"kinto.core.initialization.setup_listeners\",\n        \"kinto.core.events.setup_transaction_hook\",\n    ),\n    \"event_listeners\": \"\",\n    \"heartbeat_timeout_seconds\": 10,\n    \"newrelic_config\": None,\n    \"newrelic_env\": \"dev\",\n    \"paginate_by\": None,\n    \"pagination_token_validity_seconds\": 10 * 60,\n    \"permission_backend\": \"\",\n    \"permission_url\": \"\",\n    \"permission_pool_size\": 25,\n    \"profiler_dir\": tempfile.gettempdir(),\n    \"profiler_enabled\": False,\n    \"project_docs\": \"\",\n    \"project_name\": \"\",\n    \"project_version\": \"\",\n    \"readonly\": False,\n    \"retry_after_seconds\": 30,\n    \"settings_prefix\": \"\",\n    \"statsd_backend\": \"kinto.core.statsd\",\n    \"statsd_prefix\": \"kinto.core\",\n    \"statsd_url\": None,\n    \"storage_backend\": \"\",\n    \"storage_url\": \"\",\n    \"storage_max_fetch_size\": 10000,\n    \"storage_pool_size\": 25,\n    \"tm.annotate_user\": False,  # Do annotate transactions with the user-id.\n    \"transaction_per_request\": True,\n    \"userid_hmac_secret\": \"\",\n    \"version_json_path\": \"version.json\",\n    \"version_prefix_redirect_enabled\": True,\n    \"trailing_slash_redirect_enabled\": True,\n    \"multiauth.groupfinder\": \"kinto.core.authorization.groupfinder\",\n    \"multiauth.policies\": \"\",\n    \"multiauth.policy.basicauth.use\": (\n        \"kinto.core.authentication.\" \"BasicAuthAuthenticationPolicy\"\n    ),\n    \"multiauth.authorization_policy\": (\"kinto.core.authorization.\" \"AuthorizationPolicy\"),\n}\n\n\nclass Service(CorniceService):\n    \"\"\"Subclass of the default cornice service.\n\n    This is useful in order to attach specific behaviours without monkey\n    patching the default cornice service (which would impact other uses of it)\n    \"\"\"\n\n    default_cors_headers = (\"Backoff\", \"Retry-After\", \"Alert\", \"Content-Length\")\n\n    def error_handler(self, request):\n        return errors.json_error_handler(request)\n\n    @classmethod\n    def init_from_settings(cls, settings):\n        cls.cors_origins = tuple(aslist(settings[\"cors_origins\"]))\n        cors_max_age = settings[\"cors_max_age_seconds\"]\n        cls.cors_max_age = int(cors_max_age) if cors_max_age else None\n\n\nclass JsonLogFormatter(dockerflow_logging.JsonLogFormatter):\n    logger_name = \"kinto\"\n\n    @classmethod\n    def init_from_settings(cls, settings):\n        cls.logger_name = settings[\"project_name\"]\n\n    def __init__(self, fmt=None, datefmt=None, style=\"%\"):\n        # Do not let mozilla-cloud-services-logger constructor to improperly\n        # use style as the logger_name.\n        # See https://github.com/mozilla/mozilla-cloud-services-logger/issues/3\n        logger_name = self.logger_name\n        super().__init__(fmt, datefmt, style)\n        self.logger_name = logger_name\n\n\ndef get_user_info(request):\n    # Default user info (shown in hello view for example).\n    user_info = {\"id\": request.prefixed_userid, \"principals\": request.prefixed_principals}\n    if hasattr(request, \"get_user_profile\"):\n        user_info[\"profile\"] = request.get_user_profile()\n    return user_info\n\n\ndef includeme(config):\n    settings = config.get_settings()\n\n    # Heartbeat registry.\n    config.registry.heartbeats = {}\n\n    # Public settings registry.\n    config.registry.public_settings = {\"batch_max_requests\", \"readonly\"}\n\n    # Directive to declare arbitrary API capabilities.\n    def add_api_capability(config, identifier, description=\"\", url=\"\", **kw):\n        existing = config.registry.api_capabilities.get(identifier)\n        if existing:\n            error_msg = \"The '{}' API capability was already registered ({}).\"\n            raise ValueError(error_msg.format(identifier, existing))\n\n        capability = dict(description=description, url=url, **kw)\n        config.registry.api_capabilities[identifier] = capability\n\n    config.add_directive(\"add_api_capability\", add_api_capability)\n    config.registry.api_capabilities = {}\n\n    # Resource events helpers.\n    config.add_request_method(events.get_resource_events, name=\"get_resource_events\")\n    config.add_request_method(events.notify_resource_event, name=\"notify_resource_event\")\n\n    # Setup cornice.\n    config.include(\"cornice\")\n\n    # Setup cornice api documentation\n    config.include(\"cornice_swagger\")\n\n    # Per-request transaction.\n    config.include(\"pyramid_tm\")\n\n    # Add CORS settings to the base kinto.core Service class.\n    Service.init_from_settings(settings)\n\n    # Use the project name as the main logger name (Logger field in MozLog).\n    JsonLogFormatter.init_from_settings(settings)\n\n    # Setup components.\n    for step in aslist(settings[\"initialization_sequence\"]):\n        step_func = config.maybe_dotted(step)\n        step_func(config)\n\n    # Custom helpers.\n    config.add_request_method(log_context)\n    config.add_request_method(follow_subrequest)\n    config.add_request_method(prefixed_userid, property=True)\n    config.add_request_method(prefixed_principals, reify=True)\n    config.add_request_method(get_user_info, name=\"get_user_info\")\n    config.add_request_method(current_resource_name, reify=True)\n    config.add_request_method(current_service, reify=True)\n    config.commit()\n\n    # Include plugins after init, unlike pyramid includes.\n    includes = aslist(settings[\"includes\"])\n    for app in includes:\n        config.include(app)\n\n    # # Show settings to output.\n    # for key, value in settings.items():\n    #     logger.info('Using {} = {}'.format(key, value))\n\n    # Scan views.\n    config.scan(\"kinto.core.views\")\n\n    # Give sign of life.\n    msg = \"Running {project_name} {project_version}.\"\n    logger.info(msg.format_map(settings))\n", "path": "kinto/core/__init__.py"}, {"content": "import requests\nfrom pyramid import authentication as base_auth\nfrom pyramid.settings import aslist\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 = f\"openid:verify:{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        request.bound_data[\"user_profile\"] = payload\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 get_user_profile(request):\n    return request.bound_data.get(\"user_profile\", {})\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 policy in aslist(settings[\"multiauth.policies\"]):\n        v = settings.get(\"multiauth.policy.%s.use\" % policy, \"\")\n        if v.endswith(\"OpenIDConnectPolicy\"):\n            openid_policies.append(policy)\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            {\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\n        OpenAPI.expose_authentication_method(\n            name, {\"type\": \"oauth2\", \"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    )\n    config.add_request_method(get_user_profile, name=\"get_user_profile\")\n", "path": "kinto/plugins/openid/__init__.py"}]}