fix(auth): default COOKIE_SECURE to 'auto' in production + first-install UX (#427)

Two intertwined bugs reported in #427 by @iSchumi6210:

1. Login silently fails over HTTP. Backend defaulted COOKIE_SECURE to true
   when NODE_ENV=production. Over plain HTTP the browser drops the Secure
   cookie → next /auth/session request returns 401 → redirect back to
   /admin/login → no error shown. picpeak-setup.sh writes
   NODE_ENV=production but never writes COOKIE_SECURE, so every first-time
   install without a reverse proxy hits this.

2. Admin password is generated but admins can't find it. The 001_init.js
   migration writes the generated password to data/ADMIN_CREDENTIALS.txt
   inside the backend container, but picpeak-setup.sh only copies it out
   when --reset-admin-password is passed. Default-path users never see it
   and resort to manual bcrypt updates in psql.

Changes:

- tokenUtils.js: production default goes from `true` to `'auto'`. On real
  HTTPS req.secure is true → Secure flag is still emitted (no security
  regression for reverse-proxy deployments). On plain HTTP req.secure is
  false → Secure flag omitted → login works. Users who explicitly want
  the strict HTTPS-only behaviour can still set COOKIE_SECURE=true.

- .env.example: rewrite the COOKIE_SECURE block to make the new default
  obvious and explain when to override (set =true for strict, =false to
  skip the per-request check, leave unset for the auto behaviour).

- picpeak-setup.sh (both Docker and native paths):
  - Write COOKIE_SECURE=auto explicitly to the generated .env (defense in
    depth so the right behaviour is preserved even if the backend default
    flips again later)
  - After migrations, ALWAYS copy ADMIN_CREDENTIALS.txt out of the
    backend container/data dir to the host data dir, chmod 600, and print
    the email + password to the install output. The credentials file
    remains as a backup record that the operator should delete after
    noting the password.

Verified locally with all 4 permutations of NODE_ENV × COOKIE_SECURE:

  production, unset      → HTTPS: secure=true ✓  HTTP: secure=false ✓ (was both true)
  production, =true      → both: secure=true (strict opt-in preserved)
  production, =auto      → HTTPS: secure=true   HTTP: secure=false (already-correct)
  development, unset     → both: secure=false (dev unchanged)
This commit is contained in:
Paul Nothaft
2026-05-09 15:55:09 +02:00
parent f6cf470291
commit 5c7de96b7f
3 changed files with 97 additions and 27 deletions
+22 -9
View File
@@ -10,18 +10,31 @@ PORT=3001
JWT_SECRET=your-very-secure-jwt-secret-at-least-32-characters-long-example123456
# Auth cookie Secure flag
# unset - default: follows NODE_ENV (production=true, dev=false)
# true - always set Secure (HTTPS-only cookies; breaks plain-HTTP access)
# unset - default: 'auto' in production, false in dev (#427)
# true - always set Secure (HTTPS-only cookies; breaks plain-HTTP access
# login appears to succeed but the browser silently drops the
# cookie, leaving you in a redirect loop. Only set this if you
# ALWAYS reach the site via HTTPS)
# false - never set Secure (allows HTTP; cookies not protected on HTTPS)
# auto - decide per request: Secure on HTTPS, not on HTTP
# auto - decide per request: Secure on HTTPS, not on HTTP. Reads
# req.secure from Express which respects X-Forwarded-Proto from a
# trusted reverse proxy. This is the default and is the right
# choice for most deployments.
#
# Use COOKIE_SECURE=auto if your deployment is reachable over both HTTPS
# (via a reverse proxy like Nginx Proxy Manager, Traefik, Caddy) AND plain
# HTTP (e.g. LAN access at http://192.168.x.x:3001). The backend reads
# req.secure from Express, which respects the X-Forwarded-Proto header
# when the proxy is in the trust list.
# Why 'auto' is the default in production:
# - On real HTTPS (reverse proxy with X-Forwarded-Proto), req.secure is
# true → Secure flag is still emitted. No security regression vs. true.
# - On plain HTTP (LAN access, first-time install before reverse proxy is
# wired up), req.secure is false → Secure flag is omitted → login works
# instead of silently looping back to /admin/login.
#
# Requirements for auto mode:
# When you'd set this explicitly:
# - COOKIE_SECURE=true → strict HTTPS-only deployments where you want
# defense in depth against accidentally serving over HTTP.
# - COOKIE_SECURE=false → you intentionally only ever serve over HTTP and
# don't want the per-request check (rare).
#
# Requirements for 'auto' mode to detect HTTPS correctly:
# 1. Your reverse proxy MUST send X-Forwarded-Proto: https on HTTPS
# requests. Standard configs for NPM/Traefik/Caddy do this by default.
# 2. The proxy must be on a trusted IP range. By default PicPeak trusts