Context
We're at Spotify and have self-hosted crit-web behind an SSO reverse proxy that requires browser-based authentication. The proxy sits in front of the app and redirects unauthenticated requests to a login page — there's no way to exempt specific routes, and no programmatic auth mechanism (API keys, service tokens, etc.).
The self-hosted instance works great for viewing shared reviews. The problem is the share flow itself.
Problem
When a user clicks Share in the crit UI, the flow is:
- Browser JS sends
POST /api/share to the local Go server
- The Go server makes a server-side
POST {shareURL}/api/reviews to crit-web
- Our auth proxy intercepts this request (no session cookie), returns an HTML login page
- The Go server tries to decode HTML as JSON → fails with:
Share failed: decoding share response: invalid character '<' looking for beginning of value
The core issue: the Go binary's server-side HTTP call can never authenticate because the auth proxy requires an interactive browser-based login flow. There's no token or header it could attach.
The same problem affects unpublish, which makes a cross-origin DELETE from the browser to crit-web (app.js:5809-5837) — the auth proxy's session cookie isn't sent on cross-origin fetch requests due to SameSite=Lax.
Proposed Solution: Popup + postMessage
We'd like to propose adding a browser-native share flow using window.postMessage for self-hosted instances behind auth proxies:
Flow
- User clicks "Share" in crit UI (localhost:PORT)
- Browser JS opens popup: window.open(shareURL + '/share-receiver')
- Auth proxy authenticates user in popup (interactive browser login)
- /share-receiver page loads, sends postMessage('ready') to opener
- Localhost page sends review data via postMessage
- /share-receiver makes same-origin POST /api/reviews (cookie included automatically)
- Returns {url, delete_token} via postMessage back to opener
- Localhost page shows share URL
Why this works
- The popup is a top-level navigation to the crit-web origin, so the auth proxy authenticates normally
- After auth, the
/share-receiver page is on the same origin as /api/reviews, so the session cookie is included automatically
postMessage transfers data in-memory with no size limits (10MB review payloads are fine)
- The popup + postMessage pattern is well-established for OAuth flows (IETF draft)
- Unpublish can use the same relay — send
{type: 'unpublish', delete_token} via postMessage
Changes needed
crit-web:
- New page/route at
/share-receiver with JavaScript to handle the postMessage relay
- Route added to
router.ex
- Origin validation: receiver validates
event.origin matches localhost pattern (similar to existing LocalhostCors plug)
crit CLI (app.js):
- When
shareURL is configured (self-hosted), use popup flow instead of POST /api/share
- Could be opt-in via a config flag like
auth_required: true or auto-detected (try POST, fallback to popup on HTML response)
Security considerations
- Both sides validate
event.origin (localhost validates crit-web origin, receiver validates localhost origin)
window.open() is called from a user-initiated click (no popup blocker issues)
- crit-web does not set
Cross-Origin-Opener-Policy, so window.opener remains accessible
Questions
- Does this approach make sense for crit-web? Is there a simpler alternative we're missing?
- Would you prefer the popup flow to be the default for self-hosted instances, or opt-in via config?
- Should the
/share-receiver page be a LiveView or a simple static HTML page with vanilla JS?
- We're happy to contribute a PR for this — would that be welcome?
Environment
- crit-web: self-hosted via Docker (
ghcr.io/tomasz-tomczyk/crit-web:latest)
- Auth: SSO reverse proxy in front of the app (all routes require browser auth, no exemptions)
- crit CLI: latest version, configured with
CRIT_SHARE_URL pointing to our instance
Context
We're at Spotify and have self-hosted crit-web behind an SSO reverse proxy that requires browser-based authentication. The proxy sits in front of the app and redirects unauthenticated requests to a login page — there's no way to exempt specific routes, and no programmatic auth mechanism (API keys, service tokens, etc.).
The self-hosted instance works great for viewing shared reviews. The problem is the share flow itself.
Problem
When a user clicks Share in the crit UI, the flow is:
POST /api/shareto the local Go serverPOST {shareURL}/api/reviewsto crit-webShare failed: decoding share response: invalid character '<' looking for beginning of valueThe core issue: the Go binary's server-side HTTP call can never authenticate because the auth proxy requires an interactive browser-based login flow. There's no token or header it could attach.
The same problem affects unpublish, which makes a cross-origin
DELETEfrom the browser to crit-web (app.js:5809-5837) — the auth proxy's session cookie isn't sent on cross-origin fetch requests due toSameSite=Lax.Proposed Solution: Popup + postMessage
We'd like to propose adding a browser-native share flow using
window.postMessagefor self-hosted instances behind auth proxies:Flow
Why this works
/share-receiverpage is on the same origin as/api/reviews, so the session cookie is included automaticallypostMessagetransfers data in-memory with no size limits (10MB review payloads are fine){type: 'unpublish', delete_token}via postMessageChanges needed
crit-web:
/share-receiverwith JavaScript to handle the postMessage relayrouter.exevent.originmatcheslocalhostpattern (similar to existingLocalhostCorsplug)crit CLI (
app.js):shareURLis configured (self-hosted), use popup flow instead ofPOST /api/shareauth_required: trueor auto-detected (try POST, fallback to popup on HTML response)Security considerations
event.origin(localhost validates crit-web origin, receiver validates localhost origin)window.open()is called from a user-initiated click (no popup blocker issues)Cross-Origin-Opener-Policy, sowindow.openerremains accessibleQuestions
/share-receiverpage be a LiveView or a simple static HTML page with vanilla JS?Environment
ghcr.io/tomasz-tomczyk/crit-web:latest)CRIT_SHARE_URLpointing to our instance