fix(auth): give the OAuth popup a nonce CSP so its script runs #479

Merged
Claude-Run merged 1 commit from fix/PMS-691-oauth-popup-csp-nonce into main 2026-08-01 07:32:04 +02:00
Member

The global CSP in src/utils/security_headers.rs sets no script-src, so it falls back to default-src 'none' and the browser blocks the inline script in the Google OAuth callback page. That script is the entire function of the page: it postMessages the payload to the SPA opener and closes the popup, and the tokens exist nowhere else in the response, so sign-in completed server-side and the result was then discarded with the popup left open on "You can close this window."

google_login::callback_response now mints a fresh 128-bit base64url nonce per request, emits it as <script nonce="...">, and sets the response's own content-security-policy with script-src 'nonce-<value>' and no 'unsafe-inline'. Nonce over hash because the script body is interpolated per request, so its hash changes every call. The global policy is unchanged, so the JSON API and the not_a_frontend fallback keep default-src 'none' with no script-src at all.

The middleware itself needed one fix for that to hold: it runs after the handler and used insert, which clobbered a handler-set header rather than deferring to it as its doc comment claimed. It now uses entry(...).or_insert(...), so the handler wins and the middleware only fills in what is absent.

Tests: the callback CSP nonce is parsed out of the header and asserted equal to the <script> tag's nonce and free of 'unsafe-inline'; a global-CSP test asserts no script-src and no nonce on any other route; and a router test asserts a handler-set CSP survives the middleware while the rest of the header set is still applied.

#PMS-691

The global CSP in `src/utils/security_headers.rs` sets no `script-src`, so it falls back to `default-src 'none'` and the browser blocks the inline script in the Google OAuth callback page. That script is the entire function of the page: it `postMessage`s the payload to the SPA opener and closes the popup, and the tokens exist nowhere else in the response, so sign-in completed server-side and the result was then discarded with the popup left open on "You can close this window." `google_login::callback_response` now mints a fresh 128-bit base64url nonce per request, emits it as `<script nonce="...">`, and sets the response's own `content-security-policy` with `script-src 'nonce-<value>'` and no `'unsafe-inline'`. Nonce over hash because the script body is interpolated per request, so its hash changes every call. The global policy is unchanged, so the JSON API and the `not_a_frontend` fallback keep `default-src 'none'` with no script-src at all. The middleware itself needed one fix for that to hold: it runs after the handler and used `insert`, which clobbered a handler-set header rather than deferring to it as its doc comment claimed. It now uses `entry(...).or_insert(...)`, so the handler wins and the middleware only fills in what is absent. Tests: the callback CSP nonce is parsed out of the header and asserted equal to the `<script>` tag's nonce and free of `'unsafe-inline'`; a global-CSP test asserts no `script-src` and no nonce on any other route; and a router test asserts a handler-set CSP survives the middleware while the rest of the header set is still applied. #PMS-691
fix(auth): give the OAuth popup a nonce CSP so its script runs
All checks were successful
E2E / Playwright against staging (pull_request) Successful in 1m12s
Check / fmt + clippy + build + tests (pull_request) Successful in 3m55s
Integration / integration tests (pull_request) Successful in 12m47s
Create release / Gate (release-branch merges only) (pull_request) Successful in 0s
Create release / Create release from merged PR (pull_request) Has been skipped
d17051c25d
The global CSP in `src/utils/security_headers.rs` sets no `script-src`, so it falls back to `default-src 'none'` and the browser blocks the inline script in the Google OAuth callback page. That script is the entire function of the page: it `postMessage`s the payload to the SPA opener and closes the popup, and the tokens exist nowhere else in the response, so sign-in completed server-side and the result was then discarded with the popup left open on "You can close this window."

`google_login::callback_response` now mints a fresh 128-bit base64url nonce per request, emits it as `<script nonce="...">`, and sets the response's own `content-security-policy` with `script-src 'nonce-<value>'` and no `'unsafe-inline'`. Nonce over hash because the script body is interpolated per request, so its hash changes every call. The global policy is unchanged, so the JSON API and the `not_a_frontend` fallback keep `default-src 'none'` with no script-src at all.

The middleware itself needed one fix for that to hold: it runs after the handler and used `insert`, which clobbered a handler-set header rather than deferring to it as its doc comment claimed. It now uses `entry(...).or_insert(...)`, so the handler wins and the middleware only fills in what is absent.

Tests: the callback CSP nonce is parsed out of the header and asserted equal to the `<script>` tag's nonce and free of `'unsafe-inline'`; a global-CSP test asserts no `script-src` and no nonce on any other route; and a router test asserts a handler-set CSP survives the middleware while the rest of the header set is still applied.

#PMS-691
Claude-Run deleted branch fix/PMS-691-oauth-popup-csp-nonce 2026-08-01 07:32:05 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/mokosh-server!479
No description provided.