Skip to main content

max / makenotwork

server: allow loopback callbacks in form-action CSP for /oauth/authorize form-action applies to redirect chains (CSP3 §5.1), and /oauth/authorize issues a 302 to the RP's registered redirect_uri. For native/desktop clients that redirect_uri is a loopback callback (RFC 8252 §7.3), which is cross-origin from makenot.work — so a bare 'self' silently blocks every OAuth PKCE login on GoingsOn/audiofiles/BB. The login POST succeeds and the code is minted, but the browser refuses to follow the callback redirect, so the user sees the login form re-post with no visible effect. Widen form-action to include the three RFC 8252 loopback forms (127.0.0.1, [::1], localhost) on /oauth/authorize only. All other routes stay at 'self'.
Co-Authored-By
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-26 19:04 UTC
Signed with PGP, not checked
Commit: 030d6661a0394a8afd6e25bf9cf3dadca9d0d793
Parent: a9a2aa6
1 file changed, +14 insertions, -2 deletions
@@ -652,7 +652,14 @@
652 652 request: axum::http::Request<axum::body::Body>,
653 653 next: middleware::Next,
654 654 ) -> axum::response::Response {
655 - let is_embed = request.uri().path().starts_with("/embed/");
655 + let path = request.uri().path();
656 + let is_embed = path.starts_with("/embed/");
657 + // OAuth PKCE consent posts redirect to the RP's registered redirect_uri,
658 + // which for native/desktop clients is a loopback callback per RFC 8252 §7.3.
659 + // form-action applies to redirect chains (CSP3 §5.1), so a bare 'self' blocks
660 + // the callback and the user sees the login form re-post with no visible
661 + // effect. Widen form-action on the authorize endpoints only.
662 + let is_oauth_authorize = path == "/oauth/authorize";
656 663 let mut response = next.run(request).await;
657 664 let headers = response.headers_mut();
658 665
@@ -710,6 +717,11 @@
710 717 "http"
711 718 };
712 719 let user_pages_origin = format!("{scheme}://{}", state.config.user_pages_host);
720 + let form_action = if is_oauth_authorize {
721 + "'self' http://127.0.0.1:* http://[::1]:* http://localhost:*"
722 + } else {
723 + "'self'"
724 + };
713 725 // script-src is 'self' (+ Stripe) with NO 'unsafe-inline': all inline
714 726 // on*/hx-on handlers were moved to delegated listeners in static/*.js
715 727 // (the data-action / data-hx-* dispatchers in mnw.js), so any injected
@@ -725,7 +737,7 @@
725 737 media-src 'self'{storage_origins}; \
726 738 frame-src 'self' https://js.stripe.com {user_pages_origin}; \
727 739 base-uri 'self'; \
728 - form-action 'self'; \
740 + form-action {form_action}; \
729 741 frame-ancestors 'none'"
730 742 );
731 743 if let Ok(value) = HeaderValue::from_str(&csp) {