From f41d14b2a9b1d628df3d04571752657f7c1265f8 Mon Sep 17 00:00:00 2001 From: Gronod Date: Sun, 17 May 2026 09:42:56 +0100 Subject: [PATCH] fix: gate cookie secure flag on TRUST_PROXY not NODE_ENV MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit secure:true cookies are only sent by browsers over HTTPS connections. When NODE_ENV=production (always set in the Docker container) but no TLS proxy is in front, the browser receives the cookie on login but refuses to send it on subsequent HTTP requests — causing every authenticated endpoint (/stream, /status, etc.) to return 401. The correct signal is TRUST_PROXY: it is only set when a TLS-terminating reverse proxy is confirmed to be in front. Affects emby_user and csrf_token cookies across login, /csrf refresh, and logout. --- server/routes/auth.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/server/routes/auth.js b/server/routes/auth.js index de37fc1..caab701 100644 --- a/server/routes/auth.js +++ b/server/routes/auth.js @@ -70,13 +70,15 @@ router.post('/login', loginLimiter, async (req, res) => { // Set authentication cookie (signed when COOKIE_SECRET is set). // rememberMe=true → persistent cookie, expires in 30 days // rememberMe=false → session cookie, expires when browser closes - // secure is always true — the app should sit behind HTTPS in production; - // behind a reverse proxy set TRUST_PROXY=1 so req.secure works correctly. + // secure:true only when TRUST_PROXY is set — i.e. a TLS-terminating reverse + // proxy is in front. Without it the app may be accessed over plain HTTP and + // secure cookies would never be sent back by the browser. const cookiePayload = JSON.stringify({ id: user.Id, name: user.Name, isAdmin }); const signed = !!process.env.COOKIE_SECRET; + const secureCookie = !!process.env.TRUST_PROXY; // only send over HTTPS when behind a TLS proxy const cookieOptions = { httpOnly: true, - secure: process.env.NODE_ENV === 'production', + secure: secureCookie, sameSite: 'strict', signed, path: '/' @@ -91,7 +93,7 @@ router.post('/login', loginLimiter, async (req, res) => { const csrfToken = crypto.randomBytes(32).toString('hex'); res.cookie('csrf_token', csrfToken, { httpOnly: false, // intentionally readable by JS for the double-submit pattern - secure: process.env.NODE_ENV === 'production', + secure: secureCookie, sameSite: 'strict', path: '/' }); @@ -142,7 +144,7 @@ router.get('/csrf', (req, res) => { const csrfToken = crypto.randomBytes(32).toString('hex'); res.cookie('csrf_token', csrfToken, { httpOnly: false, - secure: process.env.NODE_ENV === 'production', + secure: !!process.env.TRUST_PROXY, sameSite: 'strict', path: '/' }); @@ -168,14 +170,14 @@ router.post('/logout', async (req, res) => { } res.clearCookie('emby_user', { httpOnly: true, - secure: process.env.NODE_ENV === 'production', + secure: !!process.env.TRUST_PROXY, sameSite: 'strict', signed: !!process.env.COOKIE_SECRET, path: '/' }); res.clearCookie('csrf_token', { httpOnly: false, - secure: process.env.NODE_ENV === 'production', + secure: !!process.env.TRUST_PROXY, sameSite: 'strict', path: '/' });