Skip to content

Commit e0446a0

Browse files
committed
Add support for CSP nonce
1 parent a26956f commit e0446a0

File tree

4 files changed

+80
-1
lines changed

4 files changed

+80
-1
lines changed

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,20 @@ Sometimes, it is necessary to specify the protocol and port to access the openap
154154
config.pyramid_openapi3_add_explorer(proto_port=('https', 443))
155155
```
156156

157+
### CSP nonce
158+
159+
If a Content Security Policy (CSP) is used in your Pyramid application, you can pass a nonce to the OpenAPI explorer UI by setting the `csp_nonce` request parameter:
160+
161+
```python
162+
def inject_csp_header_tween(request):
163+
nonce = secrets.token_urlsafe(16)
164+
request.csp_nonce = nonce
165+
response = handler(request)
166+
response.headers["Content-Security-Policy"] = f"script-src 'self' 'nonce-{nonce}'"
167+
return response
168+
```
169+
170+
157171
## Demo / Examples
158172

159173
There are three examples provided with this package:

pyramid_openapi3/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,10 +204,15 @@ def explorer_view(request: Request) -> Response:
204204
}
205205
if ui_config:
206206
merged_ui_config.update(ui_config)
207+
# Check if request has a CSP nonce (for Content Security Policy)
208+
nonce = getattr(request, "csp_nonce", None)
209+
nonce_attr = f' nonce="{nonce}"' if nonce else ""
210+
207211
html = template.safe_substitute(
208212
ui_version=ui_version,
209213
ui_config=json.dumps(merged_ui_config),
210214
oauth_config=json.dumps(oauth_config),
215+
nonce_attr=nonce_attr,
211216
)
212217
return Response(html)
213218

pyramid_openapi3/static/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@
6767

6868
<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/${ui_version}/swagger-ui-bundle.js"> </script>
6969
<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/${ui_version}/swagger-ui-standalone-preset.js"> </script>
70-
<script>
70+
<script${nonce_attr}>
7171
window.onload = function() {
7272
const uiConfig = ${ui_config};
7373
Object.assign(uiConfig, {

pyramid_openapi3/tests/test_views.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -760,3 +760,63 @@ def test_cookie_parameters() -> None:
760760
response = view(context, request)
761761

762762
assert response.json == "foo"
763+
764+
765+
def test_add_explorer_view_with_csp_nonce() -> None:
766+
"""Test explorer view includes CSP nonce when available on request."""
767+
with testConfig() as config:
768+
config.include("pyramid_openapi3")
769+
770+
with tempfile.NamedTemporaryFile() as document:
771+
document.write(MINIMAL_DOCUMENT)
772+
document.seek(0)
773+
774+
config.pyramid_openapi3_spec(
775+
document.name, route="/foo.yaml", route_name="foo_api_spec"
776+
)
777+
778+
config.pyramid_openapi3_add_explorer()
779+
request = config.registry.queryUtility(
780+
IRouteRequest, name="pyramid_openapi3.explorer"
781+
)
782+
view = config.registry.adapters.registered(
783+
(IViewClassifier, request, Interface), IView, name=""
784+
)
785+
786+
# Test with CSP nonce
787+
dummy_request = DummyRequest(config=config)
788+
dummy_request.csp_nonce = "test-nonce-123"
789+
response = view(request=dummy_request, context=None)
790+
791+
assert b'<script nonce="test-nonce-123">' in response.body
792+
assert b"<title>Swagger UI</title>" in response.body
793+
794+
795+
def test_add_explorer_view_without_csp_nonce() -> None:
796+
"""Test explorer view works normally when no CSP nonce is present."""
797+
with testConfig() as config:
798+
config.include("pyramid_openapi3")
799+
800+
with tempfile.NamedTemporaryFile() as document:
801+
document.write(MINIMAL_DOCUMENT)
802+
document.seek(0)
803+
804+
config.pyramid_openapi3_spec(
805+
document.name, route="/foo.yaml", route_name="foo_api_spec"
806+
)
807+
808+
config.pyramid_openapi3_add_explorer()
809+
request = config.registry.queryUtility(
810+
IRouteRequest, name="pyramid_openapi3.explorer"
811+
)
812+
view = config.registry.adapters.registered(
813+
(IViewClassifier, request, Interface), IView, name=""
814+
)
815+
816+
# Test without CSP nonce
817+
dummy_request = DummyRequest(config=config)
818+
response = view(request=dummy_request, context=None)
819+
820+
assert b"<script>" in response.body
821+
assert b"nonce=" not in response.body
822+
assert b"<title>Swagger UI</title>" in response.body

0 commit comments

Comments
 (0)