fix: gate cookie secure flag on TRUST_PROXY not NODE_ENV
All checks were successful
Build and Push Docker Image / build (push) Successful in 36s
CI / Security audit (push) Successful in 49s
CI / Tests & coverage (push) Successful in 59s

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.
This commit is contained in:
2026-05-17 09:42:56 +01:00
parent c3ae3a80de
commit f41d14b2a9

View File

@@ -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: '/'
});