From c0ee91cca9cedae436d5cb1a9461fcdd7fa3fadd Mon Sep 17 00:00:00 2001 From: Rishikanth Chandrasekaran Date: Thu, 29 Jan 2026 18:57:03 -0800 Subject: [PATCH] Adjust cookie security for proxy/HTTP dev --- Makefile | 7 ++- README.md | 11 ++-- cmd/server/main.go | 6 ++ internal/api/cookies.go | 63 +++++++++++++++++++ internal/api/handlers.go | 10 +-- internal/config/config.go | 41 ++++++------ web/project-site/src/docs/Installation.mdx | 8 ++- web/project-site/src/docs/Troubleshooting.mdx | 3 +- 8 files changed, 115 insertions(+), 34 deletions(-) create mode 100644 internal/api/cookies.go diff --git a/Makefile b/Makefile index 53a7d743..726353b6 100644 --- a/Makefile +++ b/Makefile @@ -36,7 +36,8 @@ dev: ## Start development environment with Air (backend) and Vite (frontend) echo "placeholder" > internal/web/dist/dummy_asset; \ fi; \ \ - trap 'echo ""; echo "🛑 Stopping development servers..."; kill 0; exit 0' INT TERM; \ + pids=""; \ + trap 'echo ""; echo "🛑 Stopping development servers..."; for pid in $$pids; do kill $$pid 2>/dev/null || true; done; wait $$pids 2>/dev/null || true; exit 0' INT TERM; \ \ echo "🧠 Starting ASR engine..."; \ ASR_ENGINE_SOCKET=$${ASR_ENGINE_SOCKET:-/tmp/scriberr-asr.sock}; \ @@ -47,6 +48,7 @@ dev: ## Start development environment with Air (backend) and Vite (frontend) ( cd asr-engines/scriberr-asr-onnx && uv sync ) || true; \ fi; \ ( cd asr-engines/scriberr-asr-onnx && uv run asr-engine-server --socket $$ASR_ENGINE_SOCKET ) & \ + pids="$$pids $$!"; \ else \ echo "⚠️ 'uv' not found. ASR engine will not start. Install uv or run make asr-engine-dev separately."; \ fi; \ @@ -54,13 +56,16 @@ dev: ## Start development environment with Air (backend) and Vite (frontend) if [ "$$USE_GO_RUN" = true ]; then \ echo "🔧 Starting Go backend (standard run)..."; \ ASR_ENGINE_SOCKET=$$ASR_ENGINE_SOCKET ASR_ENGINE_CMD="$$ASR_ENGINE_CMD" go run cmd/server/main.go & \ + pids="$$pids $$!"; \ else \ echo "🔥 Starting Go backend (with Air live reload)..."; \ ASR_ENGINE_SOCKET=$$ASR_ENGINE_SOCKET ASR_ENGINE_CMD="$$ASR_ENGINE_CMD" air & \ + pids="$$pids $$!"; \ fi; \ \ echo "⚛️ Starting React frontend (Vite)..."; \ cd web/frontend && npm run dev & \ + pids="$$pids $$!"; \ \ wait diff --git a/README.md b/README.md index 67273f78..a1b0c2be 100644 --- a/README.md +++ b/README.md @@ -192,7 +192,7 @@ For a containerized setup, you can use Docker. We provide two configurations: on > [!IMPORTANT] > **Permissions:** Ensure you set the `PUID` and `PGID` environment variables to your host user's UID and GID (typically `1000` on Linux) to avoid permission issues with the SQLite database. You can find your UID/GID by running `id` on your host. > -> **HTTP vs HTTPS:** By default, Scriberr enables **Secure Cookies** in production. If you are accessing the app via plain HTTP (not HTTPS), you MUST set `SECURE_COOKIES=false` in your environment variables, otherwise you will encounter "Unable to load audio stream" errors. +> **HTTP vs HTTPS:** Scriberr now **auto-detects** secure cookies based on TLS or proxy headers (`X-Forwarded-Proto` / `Forwarded`). If you're behind a proxy, keep `TRUST_PROXY_HEADERS=true` (default). To force a mode, set `SECURE_COOKIES=true` (always secure) or `SECURE_COOKIES=false` (always insecure). #### Standard Deployment (CPU) @@ -215,7 +215,8 @@ services: - APP_ENV=production # DO NOT CHANGE THIS # CORS: comma-separated list of allowed origins for production # - ALLOWED_ORIGINS=https://your-domain.com - # - SECURE_COOKIES=false # Uncomment this ONLY if you are not using SSL + # - TRUST_PROXY_HEADERS=true # Default; set false if you don't trust proxy headers + # - SECURE_COOKIES=false # Force insecure cookies (not recommended in production) restart: unless-stopped volumes: @@ -262,7 +263,8 @@ services: - APP_ENV=production # DO NOT CHANGE THIS # CORS: comma-separated list of allowed origins for production # - ALLOWED_ORIGINS=https://your-domain.com - # - SECURE_COOKIES=false # Uncomment this ONLY if you are not using SSL + # - TRUST_PROXY_HEADERS=true # Default; set false if you don't trust proxy headers + # - SECURE_COOKIES=false # Force insecure cookies (not recommended in production) volumes: scriberr_data: {} @@ -358,13 +360,14 @@ Replace `1000` with the value you set for `PUID`/`PGID` (default is `1000`). If the application loads but you cannot play or see the audio waveform (receiving "Unable to load audio stream"), this is often due to the **Secure Cookies** security flag. -By default, when `APP_ENV=production`, Scriberr enables `SECURE_COOKIES=true`. This prevents cookies from being sent over insecure (HTTP) connections. +By default, Scriberr auto-detects secure cookies based on TLS or trusted proxy headers. **Solutions:** - **Recommended:** Deploy Scriberr behind a Reverse Proxy (like Nginx, Caddy, or Traefik) and use SSL/TLS (HTTPS). - **Alternative:** If you must access over plain HTTP, set the following environment variable in your `docker-compose.yml`: ```yaml environment: + - TRUST_PROXY_HEADERS=true - SECURE_COOKIES=false ``` diff --git a/cmd/server/main.go b/cmd/server/main.go index acdd3beb..cf1c33be 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -8,6 +8,7 @@ import ( "os" "os/signal" "path/filepath" + "strings" "syscall" "time" @@ -76,6 +77,11 @@ func main() { // Load configuration logger.Startup("config", "Loading configuration") cfg := config.Load() + if strings.EqualFold(cfg.SecureCookiesMode, "false") { + logger.Warn("Secure cookies disabled; auth cookies will be sent over HTTP", "env", cfg.Environment) + } else if strings.EqualFold(cfg.SecureCookiesMode, "auto") && !cfg.TrustProxyHeaders { + logger.Warn("Secure cookies auto-detect without proxy headers; HTTPS required for auth cookies", "env", cfg.Environment) + } // Register adapters with config-based paths registerAdapters(cfg) diff --git a/internal/api/cookies.go b/internal/api/cookies.go new file mode 100644 index 00000000..d89ba622 --- /dev/null +++ b/internal/api/cookies.go @@ -0,0 +1,63 @@ +package api + +import ( + "strings" + + "github.com/gin-gonic/gin" +) + +func (h *Handler) cookieSecure(c *gin.Context) bool { + switch strings.ToLower(h.config.SecureCookiesMode) { + case "true", "force", "secure": + return true + case "false", "off", "insecure": + return false + default: + return requestIsSecure(c, h.config.TrustProxyHeaders) + } +} + +func requestIsSecure(c *gin.Context, trustProxy bool) bool { + if c.Request.TLS != nil { + return true + } + if !trustProxy { + return false + } + if proto := firstForwardedProto(c.GetHeader("X-Forwarded-Proto")); proto != "" { + return strings.EqualFold(proto, "https") + } + if forwarded := c.GetHeader("Forwarded"); forwarded != "" { + if proto := forwardedProto(forwarded); proto != "" { + return strings.EqualFold(proto, "https") + } + } + return false +} + +func firstForwardedProto(value string) string { + if value == "" { + return "" + } + parts := strings.Split(value, ",") + return strings.TrimSpace(parts[0]) +} + +func forwardedProto(header string) string { + // Example: Forwarded: for=192.0.2.60;proto=https;by=203.0.113.43 + parts := strings.Split(header, ";") + for _, part := range parts { + part = strings.TrimSpace(part) + if part == "" { + continue + } + kv := strings.SplitN(part, "=", 2) + if len(kv) != 2 { + continue + } + if strings.EqualFold(strings.TrimSpace(kv[0]), "proto") { + return strings.Trim(strings.TrimSpace(kv[1]), "\"") + } + } + return "" +} diff --git a/internal/api/handlers.go b/internal/api/handlers.go index ad8413ba..8c5f4490 100644 --- a/internal/api/handlers.go +++ b/internal/api/handlers.go @@ -1567,7 +1567,7 @@ func (h *Handler) Login(c *gin.Context) { Path: "/", Expires: time.Now().Add(24 * time.Hour), // Match your token duration constant HttpOnly: true, - Secure: h.config.SecureCookies, // Use explicit secure flag + Secure: h.cookieSecure(c), SameSite: http.SameSiteLaxMode, }) @@ -1600,7 +1600,7 @@ func (h *Handler) Logout(c *gin.Context) { MaxAge: -1, HttpOnly: true, SameSite: http.SameSiteLaxMode, - Secure: h.config.SecureCookies, + Secure: h.cookieSecure(c), }) // Also clear access token http.SetCookie(c.Writer, &http.Cookie{ @@ -1611,7 +1611,7 @@ func (h *Handler) Logout(c *gin.Context) { MaxAge: -1, HttpOnly: true, SameSite: http.SameSiteLaxMode, - Secure: h.config.SecureCookies, + Secure: h.cookieSecure(c), }) c.JSON(http.StatusOK, gin.H{"message": "Logged out successfully"}) } @@ -1753,7 +1753,7 @@ func (h *Handler) Refresh(c *gin.Context) { Path: "/", Expires: time.Now().Add(24 * time.Hour), HttpOnly: true, - Secure: h.config.SecureCookies, + Secure: h.cookieSecure(c), SameSite: http.SameSiteLaxMode, }) @@ -1781,7 +1781,7 @@ func (h *Handler) issueRefreshToken(c *gin.Context, userID uint) error { MaxAge: int((14 * 24 * time.Hour).Seconds()), HttpOnly: true, SameSite: http.SameSiteLaxMode, - Secure: h.config.SecureCookies, + Secure: h.cookieSecure(c), }) return nil } diff --git a/internal/config/config.go b/internal/config/config.go index 72b99c3d..b7a63f53 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -33,9 +33,10 @@ type Config struct { WhisperXEnv string // Environment configuration - Environment string - AllowedOrigins []string - SecureCookies bool // Explicit control over Secure flag (for HTTPS deployments) + Environment string + AllowedOrigins []string + SecureCookiesMode string // "auto" (default), "true"/"false" + TrustProxyHeaders bool // Whether to trust X-Forwarded-Proto/Forwarded // OpenAI configuration OpenAIAPIKey string @@ -50,26 +51,26 @@ func Load() *Config { logger.Debug("No .env file found, using system environment variables") } - // Default SecureCookies to true in production, false otherwise - defaultSecure := "false" - if strings.ToLower(getEnv("APP_ENV", "development")) == "production" { - defaultSecure = "true" + defaultSecureMode := strings.ToLower(getEnv("SECURE_COOKIES", "auto")) + if defaultSecureMode == "" { + defaultSecureMode = "auto" } return &Config{ - Port: getEnv("PORT", "8080"), - Host: getEnv("HOST", "0.0.0.0"), - Environment: getEnv("APP_ENV", "development"), - AllowedOrigins: strings.Split(getEnv("ALLOWED_ORIGINS", "http://localhost:5173,http://localhost:8080"), ","), - DatabasePath: getEnv("DATABASE_PATH", "data/scriberr.db"), - JWTSecret: getJWTSecret(), - UploadDir: getEnv("UPLOAD_DIR", "data/uploads"), - TranscriptsDir: getEnv("TRANSCRIPTS_DIR", "data/transcripts"), - TempDir: getEnv("TEMP_DIR", "data/temp"), - WhisperXEnv: getEnv("WHISPERX_ENV", "data/whisperx-env"), - SecureCookies: getEnv("SECURE_COOKIES", defaultSecure) == "true", - OpenAIAPIKey: getEnv("OPENAI_API_KEY", ""), - HFToken: getEnv("HF_TOKEN", ""), + Port: getEnv("PORT", "8080"), + Host: getEnv("HOST", "0.0.0.0"), + Environment: getEnv("APP_ENV", "development"), + AllowedOrigins: strings.Split(getEnv("ALLOWED_ORIGINS", "http://localhost:5173,http://localhost:8080"), ","), + DatabasePath: getEnv("DATABASE_PATH", "data/scriberr.db"), + JWTSecret: getJWTSecret(), + UploadDir: getEnv("UPLOAD_DIR", "data/uploads"), + TranscriptsDir: getEnv("TRANSCRIPTS_DIR", "data/transcripts"), + TempDir: getEnv("TEMP_DIR", "data/temp"), + WhisperXEnv: getEnv("WHISPERX_ENV", "data/whisperx-env"), + SecureCookiesMode: defaultSecureMode, + TrustProxyHeaders: strings.ToLower(getEnv("TRUST_PROXY_HEADERS", "true")) == "true", + OpenAIAPIKey: getEnv("OPENAI_API_KEY", ""), + HFToken: getEnv("HF_TOKEN", ""), } } diff --git a/web/project-site/src/docs/Installation.mdx b/web/project-site/src/docs/Installation.mdx index 9bc7fa9c..4cf1f0e6 100644 --- a/web/project-site/src/docs/Installation.mdx +++ b/web/project-site/src/docs/Installation.mdx @@ -79,7 +79,7 @@ For a containerized setup, you can use Docker. We provide two configurations: on > **Permissions:** Ensure you set the `PUID` and `PGID` environment variables to your host user's UID and GID (typically `1000` on Linux) to avoid permission issues with the SQLite database. You can find your UID/GID by running `id` on your host. > -> **HTTP vs HTTPS:** By default, Scriberr enables **Secure Cookies** in production. If you are accessing the app via plain HTTP (not HTTPS), you MUST set `SECURE_COOKIES=false` in your environment variables, otherwise you will encounter "Unable to load audio stream" errors. +> **HTTP vs HTTPS:** Scriberr auto-detects secure cookies based on TLS or proxy headers (`X-Forwarded-Proto` / `Forwarded`). If you're behind a proxy, keep `TRUST_PROXY_HEADERS=true` (default). To force a mode, set `SECURE_COOKIES=true` (always secure) or `SECURE_COOKIES=false` (always insecure). ### Standard Deployment (CPU) @@ -102,7 +102,8 @@ services: - APP_ENV=production # DO NOT CHANGE THIS # CORS: comma-separated list of allowed origins for production # - ALLOWED_ORIGINS=https://your-domain.com - # - SECURE_COOKIES=false # Uncomment this ONLY if you are not using SSL + # - TRUST_PROXY_HEADERS=true # Default; set false if you don't trust proxy headers + # - SECURE_COOKIES=false # Force insecure cookies (not recommended in production) restart: unless-stopped volumes: @@ -149,7 +150,8 @@ services: - APP_ENV=production # DO NOT CHANGE THIS # CORS: comma-separated list of allowed origins for production # - ALLOWED_ORIGINS=https://your-domain.com - # - SECURE_COOKIES=false # Uncomment this ONLY if you are not using SSL + # - TRUST_PROXY_HEADERS=true # Default; set false if you don't trust proxy headers + # - SECURE_COOKIES=false # Force insecure cookies (not recommended in production) volumes: scriberr_data: {} diff --git a/web/project-site/src/docs/Troubleshooting.mdx b/web/project-site/src/docs/Troubleshooting.mdx index b8ad00cb..8c2145c1 100644 --- a/web/project-site/src/docs/Troubleshooting.mdx +++ b/web/project-site/src/docs/Troubleshooting.mdx @@ -23,12 +23,13 @@ Replace `10001` with the value you set for `PUID`/`PGID` (default is `1000`). If the application loads but you cannot play or see the audio waveform (receiving "Unable to load audio stream"), this is often due to the **Secure Cookies** security flag. -By default, when `APP_ENV=production`, Scriberr enables `SECURE_COOKIES=true`. This prevents cookies from being sent over insecure (HTTP) connections. +By default, Scriberr auto-detects secure cookies based on TLS or trusted proxy headers. ### Solutions: - **Recommended:** Deploy Scriberr behind a Reverse Proxy (like Nginx, Caddy, or Traefik) and use SSL/TLS (HTTPS). - **Alternative:** If you must access over plain HTTP, set the following environment variable in your `docker-compose.yml`: ```yaml environment: + - TRUST_PROXY_HEADERS=true - SECURE_COOKIES=false ```