We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
OIDC: Create an audience route
Creating this as a reminder for myself.

The OIDC minting routes should include an `/_/oidc/audience` endpoint that just returns the OIDC audience expected by any particular Warehouse instance.

xref #12465 

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `warehouse/oidc/views.py`
Content:
```
1 # Licensed under the Apache License, Version 2.0 (the "License");
2 # you may not use this file except in compliance with the License.
3 # You may obtain a copy of the License at
4 #
5 # http://www.apache.org/licenses/LICENSE-2.0
6 #
7 # Unless required by applicable law or agreed to in writing, software
8 # distributed under the License is distributed on an "AS IS" BASIS,
9 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 # See the License for the specific language governing permissions and
11 # limitations under the License.
12 
13 import time
14 
15 from pydantic import BaseModel, StrictStr, ValidationError
16 from pyramid.view import view_config
17 from sqlalchemy import func
18 
19 from warehouse.admin.flags import AdminFlagValue
20 from warehouse.email import send_pending_oidc_publisher_invalidated_email
21 from warehouse.events.tags import EventTag
22 from warehouse.macaroons import caveats
23 from warehouse.macaroons.interfaces import IMacaroonService
24 from warehouse.oidc.interfaces import IOIDCPublisherService
25 from warehouse.oidc.models import PendingOIDCPublisher
26 from warehouse.packaging.interfaces import IProjectService
27 from warehouse.packaging.models import ProjectFactory
28 from warehouse.rate_limiting.interfaces import IRateLimiter
29 
30 
31 class TokenPayload(BaseModel):
32     token: StrictStr
33 
34 
35 def _ratelimiters(request):
36     return {
37         "user.oidc": request.find_service(
38             IRateLimiter, name="user_oidc.publisher.register"
39         ),
40         "ip.oidc": request.find_service(
41             IRateLimiter, name="ip_oidc.publisher.register"
42         ),
43     }
44 
45 
46 @view_config(
47     route_name="oidc.mint_token",
48     require_methods=["POST"],
49     renderer="json",
50     require_csrf=False,
51     has_translations=False,
52 )
53 def mint_token_from_oidc(request):
54     def _invalid(errors):
55         request.response.status = 422
56         return {"message": "Token request failed", "errors": errors}
57 
58     oidc_enabled = request.registry.settings[
59         "warehouse.oidc.enabled"
60     ] and not request.flags.enabled(AdminFlagValue.DISALLOW_OIDC)
61     if not oidc_enabled:
62         return _invalid(
63             errors=[
64                 {
65                     "code": "not-enabled",
66                     "description": "OIDC functionality not enabled",
67                 }
68             ]
69         )
70 
71     try:
72         payload = TokenPayload.parse_raw(request.body)
73         unverified_jwt = payload.token
74     except ValidationError as exc:
75         return _invalid(errors=[{"code": "invalid-payload", "description": str(exc)}])
76 
77     # For the time being, GitHub is our only OIDC publisher.
78     # In the future, this should locate the correct service based on an
79     # identifier in the request body.
80     oidc_service = request.find_service(IOIDCPublisherService, name="github")
81     claims = oidc_service.verify_jwt_signature(unverified_jwt)
82     if not claims:
83         return _invalid(
84             errors=[
85                 {"code": "invalid-token", "description": "malformed or invalid token"}
86             ]
87         )
88 
89     # First, try to find a pending publisher.
90     pending_publisher = oidc_service.find_publisher(claims, pending=True)
91     if pending_publisher is not None:
92         factory = ProjectFactory(request)
93 
94         # If the project already exists, this pending publisher is no longer
95         # valid and needs to be removed.
96         # NOTE: This is mostly a sanity check, since we dispose of invalidated
97         # pending publishers below.
98         if pending_publisher.project_name in factory:
99             request.db.delete(pending_publisher)
100             return _invalid(
101                 errors=[
102                     {
103                         "code": "invalid-pending-publisher",
104                         "description": "valid token, but project already exists",
105                     }
106                 ]
107             )
108 
109         # Create the new project, and reify the pending publisher against it.
110         project_service = request.find_service(IProjectService)
111         new_project = project_service.create_project(
112             pending_publisher.project_name,
113             pending_publisher.added_by,
114             ratelimited=False,
115         )
116         oidc_service.reify_pending_publisher(pending_publisher, new_project)
117 
118         # Successfully converting a pending publisher into a normal publisher
119         # is a positive signal, so we reset the associated ratelimits.
120         ratelimiters = _ratelimiters(request)
121         ratelimiters["user.oidc"].clear(pending_publisher.added_by.id)
122         ratelimiters["ip.oidc"].clear(request.remote_addr)
123 
124         # There might be other pending publishers for the same project name,
125         # which we've now invalidated by creating the project. These would
126         # be disposed of on use, but we explicitly dispose of them here while
127         # also sending emails to their owners.
128         stale_pending_publishers = (
129             request.db.query(PendingOIDCPublisher)
130             .filter(
131                 func.normalize_pep426_name(PendingOIDCPublisher.project_name)
132                 == func.normalize_pep426_name(pending_publisher.project_name)
133             )
134             .all()
135         )
136         for stale_publisher in stale_pending_publishers:
137             send_pending_oidc_publisher_invalidated_email(
138                 request,
139                 stale_publisher.added_by,
140                 project_name=stale_publisher.project_name,
141             )
142             request.db.delete(stale_publisher)
143 
144     # We either don't have a pending OIDC publisher, or we *did*
145     # have one and we've just converted it. Either way, look for a full publisher
146     # to actually do the macaroon minting with.
147     publisher = oidc_service.find_publisher(claims, pending=False)
148     if not publisher:
149         return _invalid(
150             errors=[
151                 {
152                     "code": "invalid-publisher",
153                     "description": "valid token, but no corresponding publisher",
154                 }
155             ]
156         )
157 
158     # At this point, we've verified that the given JWT is valid for the given
159     # project. All we need to do is mint a new token.
160     # NOTE: For OIDC-minted API tokens, the Macaroon's description string
161     # is purely an implementation detail and is not displayed to the user.
162     macaroon_service = request.find_service(IMacaroonService, context=None)
163     not_before = int(time.time())
164     expires_at = not_before + 900
165     serialized, dm = macaroon_service.create_macaroon(
166         request.domain,
167         f"OpenID token: {publisher.publisher_url} ({not_before})",
168         [
169             caveats.OIDCPublisher(oidc_publisher_id=str(publisher.id)),
170             caveats.ProjectID(project_ids=[str(p.id) for p in publisher.projects]),
171             caveats.Expiration(expires_at=expires_at, not_before=not_before),
172         ],
173         oidc_publisher_id=publisher.id,
174     )
175     for project in publisher.projects:
176         project.record_event(
177             tag=EventTag.Project.ShortLivedAPITokenAdded,
178             ip_address=request.remote_addr,
179             additional={
180                 "expires": expires_at,
181                 "publisher_name": publisher.publisher_name,
182                 "publisher_url": publisher.publisher_url,
183             },
184         )
185     return {"success": True, "token": serialized}
186
```

Path: `warehouse/oidc/__init__.py`
Content:
```
1 # Licensed under the Apache License, Version 2.0 (the "License");
2 # you may not use this file except in compliance with the License.
3 # You may obtain a copy of the License at
4 #
5 # http://www.apache.org/licenses/LICENSE-2.0
6 #
7 # Unless required by applicable law or agreed to in writing, software
8 # distributed under the License is distributed on an "AS IS" BASIS,
9 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 # See the License for the specific language governing permissions and
11 # limitations under the License.
12 
13 from warehouse.oidc.interfaces import IOIDCPublisherService
14 from warehouse.oidc.services import OIDCPublisherServiceFactory
15 from warehouse.oidc.utils import GITHUB_OIDC_ISSUER_URL
16 
17 
18 def includeme(config):
19     oidc_publisher_service_class = config.maybe_dotted(
20         config.registry.settings["oidc.backend"]
21     )
22 
23     config.register_service_factory(
24         OIDCPublisherServiceFactory(
25             publisher="github",
26             issuer_url=GITHUB_OIDC_ISSUER_URL,
27             service_class=oidc_publisher_service_class,
28         ),
29         IOIDCPublisherService,
30         name="github",
31     )
32 
33     # During deployments, we separate auth routes into their own subdomain
34     # to simplify caching exclusion.
35     auth = config.get_settings().get("auth.domain")
36 
37     config.add_route("oidc.mint_token", "/_/oidc/github/mint-token", domain=auth)
38
```
--- 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/warehouse/oidc/__init__.py b/warehouse/oidc/__init__.py
--- a/warehouse/oidc/__init__.py
+++ b/warehouse/oidc/__init__.py
@@ -34,4 +34,5 @@
     # to simplify caching exclusion.
     auth = config.get_settings().get("auth.domain")
 
-    config.add_route("oidc.mint_token", "/_/oidc/github/mint-token", domain=auth)
+    config.add_route("oidc.audience", "/_/oidc/audience", domain=auth)
+    config.add_route("oidc.github.mint_token", "/_/oidc/github/mint-token", domain=auth)
diff --git a/warehouse/oidc/views.py b/warehouse/oidc/views.py
--- a/warehouse/oidc/views.py
+++ b/warehouse/oidc/views.py
@@ -13,6 +13,7 @@
 import time
 
 from pydantic import BaseModel, StrictStr, ValidationError
+from pyramid.response import Response
 from pyramid.view import view_config
 from sqlalchemy import func
 
@@ -44,7 +45,26 @@
 
 
 @view_config(
-    route_name="oidc.mint_token",
+    route_name="oidc.audience",
+    require_methods=["GET"],
+    renderer="json",
+    require_csrf=False,
+    has_translations=False,
+)
+def oidc_audience(request):
+    oidc_enabled = request.registry.settings[
+        "warehouse.oidc.enabled"
+    ] and not request.flags.enabled(AdminFlagValue.DISALLOW_OIDC)
+
+    if not oidc_enabled:
+        return Response(status=403, json={"message": "OIDC functionality not enabled"})
+
+    audience = request.registry.settings["warehouse.oidc.audience"]
+    return {"audience": audience}
+
+
+@view_config(
+    route_name="oidc.github.mint_token",
     require_methods=["POST"],
     renderer="json",
     require_csrf=False,

{"golden_diff": "diff --git a/warehouse/oidc/__init__.py b/warehouse/oidc/__init__.py\n--- a/warehouse/oidc/__init__.py\n+++ b/warehouse/oidc/__init__.py\n@@ -34,4 +34,5 @@\n     # to simplify caching exclusion.\n     auth = config.get_settings().get(\"auth.domain\")\n \n-    config.add_route(\"oidc.mint_token\", \"/_/oidc/github/mint-token\", domain=auth)\n+    config.add_route(\"oidc.audience\", \"/_/oidc/audience\", domain=auth)\n+    config.add_route(\"oidc.github.mint_token\", \"/_/oidc/github/mint-token\", domain=auth)\ndiff --git a/warehouse/oidc/views.py b/warehouse/oidc/views.py\n--- a/warehouse/oidc/views.py\n+++ b/warehouse/oidc/views.py\n@@ -13,6 +13,7 @@\n import time\n \n from pydantic import BaseModel, StrictStr, ValidationError\n+from pyramid.response import Response\n from pyramid.view import view_config\n from sqlalchemy import func\n \n@@ -44,7 +45,26 @@\n \n \n @view_config(\n-    route_name=\"oidc.mint_token\",\n+    route_name=\"oidc.audience\",\n+    require_methods=[\"GET\"],\n+    renderer=\"json\",\n+    require_csrf=False,\n+    has_translations=False,\n+)\n+def oidc_audience(request):\n+    oidc_enabled = request.registry.settings[\n+        \"warehouse.oidc.enabled\"\n+    ] and not request.flags.enabled(AdminFlagValue.DISALLOW_OIDC)\n+\n+    if not oidc_enabled:\n+        return Response(status=403, json={\"message\": \"OIDC functionality not enabled\"})\n+\n+    audience = request.registry.settings[\"warehouse.oidc.audience\"]\n+    return {\"audience\": audience}\n+\n+\n+@view_config(\n+    route_name=\"oidc.github.mint_token\",\n     require_methods=[\"POST\"],\n     renderer=\"json\",\n     require_csrf=False,\n", "issue": "OIDC: Create an audience route\nCreating this as a reminder for myself.\r\n\r\nThe OIDC minting routes should include an `/_/oidc/audience` endpoint that just returns the OIDC audience expected by any particular Warehouse instance.\r\n\r\nxref #12465 \n", "before_files": [{"content": "# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nimport time\n\nfrom pydantic import BaseModel, StrictStr, ValidationError\nfrom pyramid.view import view_config\nfrom sqlalchemy import func\n\nfrom warehouse.admin.flags import AdminFlagValue\nfrom warehouse.email import send_pending_oidc_publisher_invalidated_email\nfrom warehouse.events.tags import EventTag\nfrom warehouse.macaroons import caveats\nfrom warehouse.macaroons.interfaces import IMacaroonService\nfrom warehouse.oidc.interfaces import IOIDCPublisherService\nfrom warehouse.oidc.models import PendingOIDCPublisher\nfrom warehouse.packaging.interfaces import IProjectService\nfrom warehouse.packaging.models import ProjectFactory\nfrom warehouse.rate_limiting.interfaces import IRateLimiter\n\n\nclass TokenPayload(BaseModel):\n    token: StrictStr\n\n\ndef _ratelimiters(request):\n    return {\n        \"user.oidc\": request.find_service(\n            IRateLimiter, name=\"user_oidc.publisher.register\"\n        ),\n        \"ip.oidc\": request.find_service(\n            IRateLimiter, name=\"ip_oidc.publisher.register\"\n        ),\n    }\n\n\n@view_config(\n    route_name=\"oidc.mint_token\",\n    require_methods=[\"POST\"],\n    renderer=\"json\",\n    require_csrf=False,\n    has_translations=False,\n)\ndef mint_token_from_oidc(request):\n    def _invalid(errors):\n        request.response.status = 422\n        return {\"message\": \"Token request failed\", \"errors\": errors}\n\n    oidc_enabled = request.registry.settings[\n        \"warehouse.oidc.enabled\"\n    ] and not request.flags.enabled(AdminFlagValue.DISALLOW_OIDC)\n    if not oidc_enabled:\n        return _invalid(\n            errors=[\n                {\n                    \"code\": \"not-enabled\",\n                    \"description\": \"OIDC functionality not enabled\",\n                }\n            ]\n        )\n\n    try:\n        payload = TokenPayload.parse_raw(request.body)\n        unverified_jwt = payload.token\n    except ValidationError as exc:\n        return _invalid(errors=[{\"code\": \"invalid-payload\", \"description\": str(exc)}])\n\n    # For the time being, GitHub is our only OIDC publisher.\n    # In the future, this should locate the correct service based on an\n    # identifier in the request body.\n    oidc_service = request.find_service(IOIDCPublisherService, name=\"github\")\n    claims = oidc_service.verify_jwt_signature(unverified_jwt)\n    if not claims:\n        return _invalid(\n            errors=[\n                {\"code\": \"invalid-token\", \"description\": \"malformed or invalid token\"}\n            ]\n        )\n\n    # First, try to find a pending publisher.\n    pending_publisher = oidc_service.find_publisher(claims, pending=True)\n    if pending_publisher is not None:\n        factory = ProjectFactory(request)\n\n        # If the project already exists, this pending publisher is no longer\n        # valid and needs to be removed.\n        # NOTE: This is mostly a sanity check, since we dispose of invalidated\n        # pending publishers below.\n        if pending_publisher.project_name in factory:\n            request.db.delete(pending_publisher)\n            return _invalid(\n                errors=[\n                    {\n                        \"code\": \"invalid-pending-publisher\",\n                        \"description\": \"valid token, but project already exists\",\n                    }\n                ]\n            )\n\n        # Create the new project, and reify the pending publisher against it.\n        project_service = request.find_service(IProjectService)\n        new_project = project_service.create_project(\n            pending_publisher.project_name,\n            pending_publisher.added_by,\n            ratelimited=False,\n        )\n        oidc_service.reify_pending_publisher(pending_publisher, new_project)\n\n        # Successfully converting a pending publisher into a normal publisher\n        # is a positive signal, so we reset the associated ratelimits.\n        ratelimiters = _ratelimiters(request)\n        ratelimiters[\"user.oidc\"].clear(pending_publisher.added_by.id)\n        ratelimiters[\"ip.oidc\"].clear(request.remote_addr)\n\n        # There might be other pending publishers for the same project name,\n        # which we've now invalidated by creating the project. These would\n        # be disposed of on use, but we explicitly dispose of them here while\n        # also sending emails to their owners.\n        stale_pending_publishers = (\n            request.db.query(PendingOIDCPublisher)\n            .filter(\n                func.normalize_pep426_name(PendingOIDCPublisher.project_name)\n                == func.normalize_pep426_name(pending_publisher.project_name)\n            )\n            .all()\n        )\n        for stale_publisher in stale_pending_publishers:\n            send_pending_oidc_publisher_invalidated_email(\n                request,\n                stale_publisher.added_by,\n                project_name=stale_publisher.project_name,\n            )\n            request.db.delete(stale_publisher)\n\n    # We either don't have a pending OIDC publisher, or we *did*\n    # have one and we've just converted it. Either way, look for a full publisher\n    # to actually do the macaroon minting with.\n    publisher = oidc_service.find_publisher(claims, pending=False)\n    if not publisher:\n        return _invalid(\n            errors=[\n                {\n                    \"code\": \"invalid-publisher\",\n                    \"description\": \"valid token, but no corresponding publisher\",\n                }\n            ]\n        )\n\n    # At this point, we've verified that the given JWT is valid for the given\n    # project. All we need to do is mint a new token.\n    # NOTE: For OIDC-minted API tokens, the Macaroon's description string\n    # is purely an implementation detail and is not displayed to the user.\n    macaroon_service = request.find_service(IMacaroonService, context=None)\n    not_before = int(time.time())\n    expires_at = not_before + 900\n    serialized, dm = macaroon_service.create_macaroon(\n        request.domain,\n        f\"OpenID token: {publisher.publisher_url} ({not_before})\",\n        [\n            caveats.OIDCPublisher(oidc_publisher_id=str(publisher.id)),\n            caveats.ProjectID(project_ids=[str(p.id) for p in publisher.projects]),\n            caveats.Expiration(expires_at=expires_at, not_before=not_before),\n        ],\n        oidc_publisher_id=publisher.id,\n    )\n    for project in publisher.projects:\n        project.record_event(\n            tag=EventTag.Project.ShortLivedAPITokenAdded,\n            ip_address=request.remote_addr,\n            additional={\n                \"expires\": expires_at,\n                \"publisher_name\": publisher.publisher_name,\n                \"publisher_url\": publisher.publisher_url,\n            },\n        )\n    return {\"success\": True, \"token\": serialized}\n", "path": "warehouse/oidc/views.py"}, {"content": "# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nfrom warehouse.oidc.interfaces import IOIDCPublisherService\nfrom warehouse.oidc.services import OIDCPublisherServiceFactory\nfrom warehouse.oidc.utils import GITHUB_OIDC_ISSUER_URL\n\n\ndef includeme(config):\n    oidc_publisher_service_class = config.maybe_dotted(\n        config.registry.settings[\"oidc.backend\"]\n    )\n\n    config.register_service_factory(\n        OIDCPublisherServiceFactory(\n            publisher=\"github\",\n            issuer_url=GITHUB_OIDC_ISSUER_URL,\n            service_class=oidc_publisher_service_class,\n        ),\n        IOIDCPublisherService,\n        name=\"github\",\n    )\n\n    # During deployments, we separate auth routes into their own subdomain\n    # to simplify caching exclusion.\n    auth = config.get_settings().get(\"auth.domain\")\n\n    config.add_route(\"oidc.mint_token\", \"/_/oidc/github/mint-token\", domain=auth)\n", "path": "warehouse/oidc/__init__.py"}], "after_files": [{"content": "# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nimport time\n\nfrom pydantic import BaseModel, StrictStr, ValidationError\nfrom pyramid.response import Response\nfrom pyramid.view import view_config\nfrom sqlalchemy import func\n\nfrom warehouse.admin.flags import AdminFlagValue\nfrom warehouse.email import send_pending_oidc_publisher_invalidated_email\nfrom warehouse.events.tags import EventTag\nfrom warehouse.macaroons import caveats\nfrom warehouse.macaroons.interfaces import IMacaroonService\nfrom warehouse.oidc.interfaces import IOIDCPublisherService\nfrom warehouse.oidc.models import PendingOIDCPublisher\nfrom warehouse.packaging.interfaces import IProjectService\nfrom warehouse.packaging.models import ProjectFactory\nfrom warehouse.rate_limiting.interfaces import IRateLimiter\n\n\nclass TokenPayload(BaseModel):\n    token: StrictStr\n\n\ndef _ratelimiters(request):\n    return {\n        \"user.oidc\": request.find_service(\n            IRateLimiter, name=\"user_oidc.publisher.register\"\n        ),\n        \"ip.oidc\": request.find_service(\n            IRateLimiter, name=\"ip_oidc.publisher.register\"\n        ),\n    }\n\n\n@view_config(\n    route_name=\"oidc.audience\",\n    require_methods=[\"GET\"],\n    renderer=\"json\",\n    require_csrf=False,\n    has_translations=False,\n)\ndef oidc_audience(request):\n    oidc_enabled = request.registry.settings[\n        \"warehouse.oidc.enabled\"\n    ] and not request.flags.enabled(AdminFlagValue.DISALLOW_OIDC)\n\n    if not oidc_enabled:\n        return Response(status=403, json={\"message\": \"OIDC functionality not enabled\"})\n\n    audience = request.registry.settings[\"warehouse.oidc.audience\"]\n    return {\"audience\": audience}\n\n\n@view_config(\n    route_name=\"oidc.github.mint_token\",\n    require_methods=[\"POST\"],\n    renderer=\"json\",\n    require_csrf=False,\n    has_translations=False,\n)\ndef mint_token_from_oidc(request):\n    def _invalid(errors):\n        request.response.status = 422\n        return {\"message\": \"Token request failed\", \"errors\": errors}\n\n    oidc_enabled = request.registry.settings[\n        \"warehouse.oidc.enabled\"\n    ] and not request.flags.enabled(AdminFlagValue.DISALLOW_OIDC)\n    if not oidc_enabled:\n        return _invalid(\n            errors=[\n                {\n                    \"code\": \"not-enabled\",\n                    \"description\": \"OIDC functionality not enabled\",\n                }\n            ]\n        )\n\n    try:\n        payload = TokenPayload.parse_raw(request.body)\n        unverified_jwt = payload.token\n    except ValidationError as exc:\n        return _invalid(errors=[{\"code\": \"invalid-payload\", \"description\": str(exc)}])\n\n    # For the time being, GitHub is our only OIDC publisher.\n    # In the future, this should locate the correct service based on an\n    # identifier in the request body.\n    oidc_service = request.find_service(IOIDCPublisherService, name=\"github\")\n    claims = oidc_service.verify_jwt_signature(unverified_jwt)\n    if not claims:\n        return _invalid(\n            errors=[\n                {\"code\": \"invalid-token\", \"description\": \"malformed or invalid token\"}\n            ]\n        )\n\n    # First, try to find a pending publisher.\n    pending_publisher = oidc_service.find_publisher(claims, pending=True)\n    if pending_publisher is not None:\n        factory = ProjectFactory(request)\n\n        # If the project already exists, this pending publisher is no longer\n        # valid and needs to be removed.\n        # NOTE: This is mostly a sanity check, since we dispose of invalidated\n        # pending publishers below.\n        if pending_publisher.project_name in factory:\n            request.db.delete(pending_publisher)\n            return _invalid(\n                errors=[\n                    {\n                        \"code\": \"invalid-pending-publisher\",\n                        \"description\": \"valid token, but project already exists\",\n                    }\n                ]\n            )\n\n        # Create the new project, and reify the pending publisher against it.\n        project_service = request.find_service(IProjectService)\n        new_project = project_service.create_project(\n            pending_publisher.project_name,\n            pending_publisher.added_by,\n            ratelimited=False,\n        )\n        oidc_service.reify_pending_publisher(pending_publisher, new_project)\n\n        # Successfully converting a pending publisher into a normal publisher\n        # is a positive signal, so we reset the associated ratelimits.\n        ratelimiters = _ratelimiters(request)\n        ratelimiters[\"user.oidc\"].clear(pending_publisher.added_by.id)\n        ratelimiters[\"ip.oidc\"].clear(request.remote_addr)\n\n        # There might be other pending publishers for the same project name,\n        # which we've now invalidated by creating the project. These would\n        # be disposed of on use, but we explicitly dispose of them here while\n        # also sending emails to their owners.\n        stale_pending_publishers = (\n            request.db.query(PendingOIDCPublisher)\n            .filter(\n                func.normalize_pep426_name(PendingOIDCPublisher.project_name)\n                == func.normalize_pep426_name(pending_publisher.project_name)\n            )\n            .all()\n        )\n        for stale_publisher in stale_pending_publishers:\n            send_pending_oidc_publisher_invalidated_email(\n                request,\n                stale_publisher.added_by,\n                project_name=stale_publisher.project_name,\n            )\n            request.db.delete(stale_publisher)\n\n    # We either don't have a pending OIDC publisher, or we *did*\n    # have one and we've just converted it. Either way, look for a full publisher\n    # to actually do the macaroon minting with.\n    publisher = oidc_service.find_publisher(claims, pending=False)\n    if not publisher:\n        return _invalid(\n            errors=[\n                {\n                    \"code\": \"invalid-publisher\",\n                    \"description\": \"valid token, but no corresponding publisher\",\n                }\n            ]\n        )\n\n    # At this point, we've verified that the given JWT is valid for the given\n    # project. All we need to do is mint a new token.\n    # NOTE: For OIDC-minted API tokens, the Macaroon's description string\n    # is purely an implementation detail and is not displayed to the user.\n    macaroon_service = request.find_service(IMacaroonService, context=None)\n    not_before = int(time.time())\n    expires_at = not_before + 900\n    serialized, dm = macaroon_service.create_macaroon(\n        request.domain,\n        f\"OpenID token: {publisher.publisher_url} ({not_before})\",\n        [\n            caveats.OIDCPublisher(oidc_publisher_id=str(publisher.id)),\n            caveats.ProjectID(project_ids=[str(p.id) for p in publisher.projects]),\n            caveats.Expiration(expires_at=expires_at, not_before=not_before),\n        ],\n        oidc_publisher_id=publisher.id,\n    )\n    for project in publisher.projects:\n        project.record_event(\n            tag=EventTag.Project.ShortLivedAPITokenAdded,\n            ip_address=request.remote_addr,\n            additional={\n                \"expires\": expires_at,\n                \"publisher_name\": publisher.publisher_name,\n                \"publisher_url\": publisher.publisher_url,\n            },\n        )\n    return {\"success\": True, \"token\": serialized}\n", "path": "warehouse/oidc/views.py"}, {"content": "# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nfrom warehouse.oidc.interfaces import IOIDCPublisherService\nfrom warehouse.oidc.services import OIDCPublisherServiceFactory\nfrom warehouse.oidc.utils import GITHUB_OIDC_ISSUER_URL\n\n\ndef includeme(config):\n    oidc_publisher_service_class = config.maybe_dotted(\n        config.registry.settings[\"oidc.backend\"]\n    )\n\n    config.register_service_factory(\n        OIDCPublisherServiceFactory(\n            publisher=\"github\",\n            issuer_url=GITHUB_OIDC_ISSUER_URL,\n            service_class=oidc_publisher_service_class,\n        ),\n        IOIDCPublisherService,\n        name=\"github\",\n    )\n\n    # During deployments, we separate auth routes into their own subdomain\n    # to simplify caching exclusion.\n    auth = config.get_settings().get(\"auth.domain\")\n\n    config.add_route(\"oidc.audience\", \"/_/oidc/audience\", domain=auth)\n    config.add_route(\"oidc.github.mint_token\", \"/_/oidc/github/mint-token\", domain=auth)\n", "path": "warehouse/oidc/__init__.py"}]}