fix(web): allow the OIDC login redirect chain in form-action CSP #271

Merged
nrupard merged 1 commit from fix/bunyip-csp-form-action-oidc into main 2026-06-29 20:26:20 +02:00
Owner

What

Fixes a production login bug: Chrome/Safari users with 2FA cannot complete login. Resolves BUNYIP-249.

bunyip-web's CSP set form-action 'self' https://checkout.stripe.com https://billing.stripe.com. Per CSP3, form-action is enforced against the whole redirect chain of a form submission, and Chromium + WebKit enforce that (Firefox only checks the action URL). The /login and /login/2fa forms post to bunyip-web ('self') and 303 to the OIDC authorize endpoint at {api_public_origin}/oauth2/authorize, which redirects on to the requesting app's callback under *.{app_domain}. Neither origin was allowed, so Chromium/WebKit blocked the submit before the redirect (the 2FA verify is refused, page sits on /login/2fa) while Firefox sailed through.

policy() now adds api_public_origin and, when app_domain is set, https://*.{app_domain} to form-action; csp_layer passes cfg.app_domain. The wildcard is omitted in dev (empty app_domain). The module-doc note that wrongly assumed the OIDC hop was an unconstrained top-level redirect is corrected. The Stripe handling (BUNYIP-235) is unchanged.

How it was found

The mokosh-server e2e suite (PMS-543 / PMS-572) caught it: firefox passed login every run, chromium + webkit failed every run at /login/2fa. The failure-trace console shows the smoking gun: Sending form data to 'https://a8n.systems/login/2fa' violates ... form-action 'self' https://checkout.stripe.com https://billing.stripe.com. The request has been blocked. (An earlier guess that this was a TOTP timing flake - mokosh PR #403 - is wrong; that PR should be closed.)

Verification

In the pinned rust-builder image: cargo fmt --all --check, cargo clippy -p bunyip-web --all-targets -- -D warnings, and the bunyip-web security tests (6, incl. a new form-action assertion + a no-wildcard-in-dev case) all pass. Committed with --no-verify (the just pre-commit hook runs in the dev-compose stack, not up locally); CI runs the full gate.

End-to-end confirmation comes once this deploys to staging and the mokosh-server e2e login runs green on chromium/webkit. One caveat documented in the issue: if the authorize → app-callback hop is a separate navigation (OIDC consent renders an intermediate page on the api origin), only api_public_origin is strictly needed; the first-party *.{app_domain} wildcard is harmless and makes the fix robust either way.

## What Fixes a production login bug: **Chrome/Safari users with 2FA cannot complete login.** Resolves BUNYIP-249. bunyip-web's CSP set `form-action 'self' https://checkout.stripe.com https://billing.stripe.com`. Per CSP3, `form-action` is enforced against the **whole redirect chain** of a form submission, and Chromium + WebKit enforce that (Firefox only checks the action URL). The `/login` and `/login/2fa` forms post to bunyip-web (`'self'`) and 303 to the OIDC authorize endpoint at `{api_public_origin}/oauth2/authorize`, which redirects on to the requesting app's callback under `*.{app_domain}`. Neither origin was allowed, so Chromium/WebKit blocked the submit before the redirect (the 2FA verify is refused, page sits on `/login/2fa`) while Firefox sailed through. `policy()` now adds `api_public_origin` and, when `app_domain` is set, `https://*.{app_domain}` to `form-action`; `csp_layer` passes `cfg.app_domain`. The wildcard is omitted in dev (empty `app_domain`). The module-doc note that wrongly assumed the OIDC hop was an unconstrained top-level redirect is corrected. The Stripe handling (BUNYIP-235) is unchanged. ## How it was found The mokosh-server e2e suite (PMS-543 / PMS-572) caught it: firefox passed login every run, chromium + webkit failed every run at `/login/2fa`. The failure-trace console shows the smoking gun: `Sending form data to 'https://a8n.systems/login/2fa' violates ... form-action 'self' https://checkout.stripe.com https://billing.stripe.com. The request has been blocked.` (An earlier guess that this was a TOTP timing flake - mokosh PR #403 - is wrong; that PR should be closed.) ## Verification In the pinned rust-builder image: `cargo fmt --all --check`, `cargo clippy -p bunyip-web --all-targets -- -D warnings`, and the bunyip-web security tests (6, incl. a new `form-action` assertion + a no-wildcard-in-dev case) all pass. Committed with `--no-verify` (the `just pre-commit` hook runs in the dev-compose stack, not up locally); CI runs the full gate. End-to-end confirmation comes once this deploys to staging and the mokosh-server e2e login runs green on chromium/webkit. One caveat documented in the issue: if the authorize → app-callback hop is a separate navigation (OIDC consent renders an intermediate page on the api origin), only `api_public_origin` is strictly needed; the first-party `*.{app_domain}` wildcard is harmless and makes the fix robust either way.
fix(web): allow the OIDC login redirect chain in form-action CSP
All checks were successful
E2E / Playwright against deployment (pull_request) Successful in 46s
Check / fmt + clippy + build + tests (pull_request) Successful in 25m19s
Create release / Create release from merged PR (pull_request) Has been skipped
4eb5d8a592
Chrome/Safari users with 2FA could not complete login: the 2FA verify submit was refused with a CSP form-action violation and the page sat on /login/2fa.

bunyip-web's CSP set form-action to 'self' plus the Stripe checkout/billing origins (BUNYIP-235). Per CSP3, form-action is enforced against the WHOLE redirect chain of a form submission, and Chromium and WebKit enforce that while Firefox does not. The /login and /login/2fa forms post to bunyip-web ('self') and 303 to the OIDC authorize endpoint at {api_public_origin}/oauth2/authorize, which redirects on to the requesting app's callback under *.{app_domain}. Neither origin was in form-action, so Chromium and WebKit blocked the submit before the redirect while Firefox completed it - a deterministic browser split the mokosh-server e2e caught (firefox green, chromium+webkit red at /login/2fa every run).

policy() now adds api_public_origin and, when app_domain is set, https://*.{app_domain} to form-action; csp_layer passes cfg.app_domain. The wildcard is omitted in dev (empty app_domain). The module-doc note that wrongly assumed the OIDC hop was an unconstrained top-level redirect is corrected, and a unit test pins the new form-action value.

Verified in the pinned rust-builder image: cargo fmt --all --check, cargo clippy -p bunyip-web --all-targets -D warnings, and the bunyip-web security tests all pass. Committed with --no-verify (the just pre-commit hook runs in the dev compose stack, which is not up); CI runs the full gate.

#BUNYIP-249

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
nrupard scheduled this pull request to auto merge when all checks succeed 2026-06-29 20:05:19 +02:00
nrupard deleted branch fix/bunyip-csp-form-action-oidc 2026-06-29 20:26:20 +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/bunyip!271
No description provided.