diff --git a/server/auth.js b/server/auth.js index 48c1243..5a63838 100644 --- a/server/auth.js +++ b/server/auth.js @@ -1,7 +1,7 @@ import { createHash, randomUUID, timingSafeEqual, createHmac, randomFillSync } from 'node:crypto' import { readFile, writeFile } from 'node:fs/promises' import path from 'node:path' -import { parseCookies } from './helpers.js' +import { parseCookies, cookieSecureFlag } from './helpers.js' import { DATA_DIR } from './paths.js' const TOTP_SECRET_FILE = path.join(DATA_DIR, 'totp-secret.json') @@ -18,7 +18,7 @@ const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD const adminSessions = new Map() function cookieFlags() { - return process.env.NODE_ENV === 'production' ? '; Secure' : '' + return cookieSecureFlag() } export function sha256(value) { diff --git a/server/helpers.js b/server/helpers.js index 818a683..50af0e2 100644 --- a/server/helpers.js +++ b/server/helpers.js @@ -505,10 +505,18 @@ export function hasVisitorConsent(req) { return cookies['vbn_analytics_consent'] === 'yes' } +// Secure cookies are required in production unless explicitly disabled with +// ALLOW_INSECURE_COOKIES=true — needed when the app is reached over plain +// HTTP (e.g. by LAN/VPN IP during a server migration, before TLS is set up), +// because browsers silently drop Secure cookies on http:// origins. +export function cookieSecureFlag() { + if (process.env.ALLOW_INSECURE_COOKIES === 'true') return '' + return process.env.NODE_ENV === 'production' ? '; Secure' : '' +} + export function setConsentCookie(res, consent) { const value = consent ? 'yes' : 'no' - const secureFlag = process.env.NODE_ENV === 'production' ? '; Secure' : '' - res.append('Set-Cookie', `vbn_analytics_consent=${value}; Max-Age=31536000; Path=/; SameSite=Lax${secureFlag}`) + res.append('Set-Cookie', `vbn_analytics_consent=${value}; Max-Age=31536000; Path=/; SameSite=Lax${cookieSecureFlag()}`) } export function isPrivateOrLocalIp(ip) { diff --git a/server/study-helpers.js b/server/study-helpers.js index 6ef2de4..6232bc1 100644 --- a/server/study-helpers.js +++ b/server/study-helpers.js @@ -1,6 +1,6 @@ import { createHash, randomUUID } from 'node:crypto' import path from 'node:path' -import { parseCookies } from './helpers.js' +import { parseCookies, cookieSecureFlag } from './helpers.js' import { STUDY_SESSION_COOKIE, STUDY_SESSION_TTL_MS, @@ -131,7 +131,7 @@ export function getStudyAvatarUrl(subject) { // ── Session management ───────────────────────────────────────────────────── export function cookieFlags() { - return process.env.NODE_ENV === 'production' ? '; Secure' : '' + return cookieSecureFlag() } export function createStudySession(userId) {