2 Commits

Author SHA1 Message Date
4430ce38a4 Allow htmx swap styles in CSP
Add 'unsafe-hashes' with the htmx swap indicator style hash to the
style-src CSP directive. Without this, htmx swap transitions are
blocked by CSP, which can prevent HX-Redirect from being processed
on the SSO login flow.

Security:
- Uses 'unsafe-hashes' (not 'unsafe-inline') so only the specific
  htmx style hash is permitted, not arbitrary inline styles

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 17:43:53 -07:00
4ed2cecec5 Fix SSO redirect failing with htmx login form
The login form uses hx-post, so htmx sends the POST via fetch. A 302
redirect to the cross-origin service callback URL fails silently because
fetch follows the redirect but gets blocked by CORS. Use HX-Redirect
header instead, which tells htmx to perform a full page navigation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 17:32:44 -07:00
2 changed files with 12 additions and 1 deletions

View File

@@ -211,6 +211,14 @@ func (u *UIServer) finishLogin(w http.ResponseWriter, r *http.Request, acct *mod
// SSO redirect flow: issue authorization code and redirect to service.
if ssoNonce != "" {
if callbackURL, ok := u.buildSSOCallback(r, ssoNonce, acct.ID); ok {
// Security: htmx follows 302 redirects via fetch, which fails
// cross-origin (no CORS on the service callback). Use HX-Redirect
// so htmx performs a full page navigation instead.
if isHTMX(r) {
w.Header().Set("HX-Redirect", callbackURL)
w.WriteHeader(http.StatusOK)
return
}
http.Redirect(w, r, callbackURL, http.StatusFound)
return
}

View File

@@ -749,8 +749,11 @@ func noDirListing(next http.Handler) http.Handler {
func securityHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h := w.Header()
// Security: 'unsafe-hashes' with the htmx swap indicator style hash
// allows htmx to apply its settling/swapping CSS transitions without
// opening the door to arbitrary inline styles.
h.Set("Content-Security-Policy",
"default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'")
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-hashes' 'sha256-bsV5JivYxvGywDAZ22EZJKBFip65Ng9xoJVLbBg7bdo='; img-src 'self' data:; font-src 'self'")
h.Set("X-Content-Type-Options", "nosniff")
h.Set("X-Frame-Options", "DENY")
h.Set("Strict-Transport-Security", "max-age=63072000; includeSubDomains")