From 4e5d28341dc926dd2f4fce23e70f815216b48fc3 Mon Sep 17 00:00:00 2001 From: Pulse Monitor Date: Sun, 17 Aug 2025 18:11:47 +0000 Subject: [PATCH] fix: support authentication through Cloudflare tunnels and reverse proxies (#325) - Detect when running behind a proxy/tunnel (X-Forwarded-*, CF-Ray headers) - Use SameSite=None for cookies when proxied to allow cross-origin access - Properly detect HTTPS when behind proxy using X-Forwarded-Proto - Fixes authentication not working through Cloudflare tunnels The issue was that SameSite=Strict/Lax cookies don't work when the origin changes (which happens with tunnels/proxies). Now we detect proxy headers and relax the cookie policy to SameSite=None when needed. --- internal/api/auth.go | 24 ++++++++++++++++++++---- internal/api/security.go | 18 ++++++++++++++++-- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/internal/api/auth.go b/internal/api/auth.go index 6b74f79ef..bff2b1025 100644 --- a/internal/api/auth.go +++ b/internal/api/auth.go @@ -252,14 +252,30 @@ func CheckAuth(cfg *config.Config, w http.ResponseWriter, r *http.Request) bool // Generate CSRF token csrfToken := generateCSRFToken(token) + // Detect if we're behind a proxy/tunnel (Cloudflare, reverse proxy, etc) + isProxied := r.Header.Get("X-Forwarded-For") != "" || + r.Header.Get("X-Real-IP") != "" || + r.Header.Get("CF-Ray") != "" || // Cloudflare + r.Header.Get("X-Forwarded-Proto") != "" + + // Determine SameSite policy based on proxy detection + sameSitePolicy := http.SameSiteLaxMode + if isProxied { + // For proxied connections, use None to allow cross-origin cookies + sameSitePolicy = http.SameSiteNoneMode + } + + // Determine if connection is secure (required for SameSite=None) + isSecure := r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" + // Set session cookie http.SetCookie(w, &http.Cookie{ Name: "pulse_session", Value: token, Path: "/", HttpOnly: true, - Secure: r.TLS != nil, - SameSite: http.SameSiteLaxMode, + Secure: isSecure, + SameSite: sameSitePolicy, MaxAge: 86400, // 24 hours }) @@ -268,8 +284,8 @@ func CheckAuth(cfg *config.Config, w http.ResponseWriter, r *http.Request) bool Name: "pulse_csrf", Value: csrfToken, Path: "/", - Secure: r.TLS != nil, - SameSite: http.SameSiteStrictMode, + Secure: isSecure, + SameSite: sameSitePolicy, MaxAge: 86400, // 24 hours }) diff --git a/internal/api/security.go b/internal/api/security.go index d859fb2c0..1b222ee24 100644 --- a/internal/api/security.go +++ b/internal/api/security.go @@ -122,13 +122,27 @@ func CheckCSRF(w http.ResponseWriter, r *http.Request) bool { // Valid session but mismatched CSRF - likely server restart // Generate a new CSRF token for this session newToken := generateCSRFToken(cookie.Value) + + // Detect if we're behind a proxy/tunnel + isProxied := r.Header.Get("X-Forwarded-For") != "" || + r.Header.Get("X-Real-IP") != "" || + r.Header.Get("CF-Ray") != "" || + r.Header.Get("X-Forwarded-Proto") != "" + + sameSitePolicy := http.SameSiteStrictMode + if isProxied { + sameSitePolicy = http.SameSiteNoneMode + } + + isSecure := r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" + // Set the new CSRF token as a cookie http.SetCookie(w, &http.Cookie{ Name: "pulse_csrf", Value: newToken, Path: "/", - Secure: r.TLS != nil, - SameSite: http.SameSiteStrictMode, + Secure: isSecure, + SameSite: sameSitePolicy, MaxAge: 86400, // 24 hours }) // For this request, we'll be lenient and allow it through