fix(api): make the session cookie's Secure flag overridable
Found on http://batch.dev.kyuno.fr/: login/signup succeeded (200/201, profile in the body) but every subsequent request 401'd. Cause: the session cookie is `secure: NODE_ENV === "production"`, and docker-compose.yml sets NODE_ENV=production regardless of whether the deployment actually has TLS in front of it. A Secure cookie is silently never sent back by the browser over plain HTTP — no error, just a cookie that never round-trips. Adds COOKIE_SECURE, independent from NODE_ENV, to override the flag per deployment. Unset (default) keeps prior behavior — secure in production. Set COOKIE_SECURE=false only for a deployment reachable over plain HTTP (no TLS yet), like this dev instance. Verified locally: docker compose up with COOKIE_SECURE=false persists and round-trips the cookie (signup -> /auth/me 200); without it, the cookie still gets Secure as before. Full pnpm --filter api test / test:bdd suites still pass (66 + 25 scenarios). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
9d19136c93
commit
266f540d88
4 changed files with 34 additions and 2 deletions
|
|
@ -14,3 +14,10 @@ JWT_SECRET=changeme-generate-a-real-random-secret-at-least-32-chars
|
||||||
# Optional — host port for the Docker review stack's single app container
|
# Optional — host port for the Docker review stack's single app container
|
||||||
# (docker-compose.yml), serving both the API and the built frontend.
|
# (docker-compose.yml), serving both the API and the built frontend.
|
||||||
# APP_PORT=3000
|
# APP_PORT=3000
|
||||||
|
|
||||||
|
# Optional — only set this to false if THIS deployment is served over
|
||||||
|
# plain HTTP (no TLS in front of it). Left unset, the session cookie
|
||||||
|
# requires HTTPS (Secure attribute) as it should for a real deployment;
|
||||||
|
# over plain HTTP a Secure cookie is silently never sent back by the
|
||||||
|
# browser, so login "succeeds" but every subsequent request 401s.
|
||||||
|
# COOKIE_SECURE=false
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,22 @@ const envSchema = z.object({
|
||||||
* the frontend instead.
|
* the frontend instead.
|
||||||
*/
|
*/
|
||||||
FRONTEND_DIST_DIR: z.string().optional(),
|
FRONTEND_DIST_DIR: z.string().optional(),
|
||||||
|
/**
|
||||||
|
* Overrides whether the session cookie gets the `Secure` attribute
|
||||||
|
* (HTTPS-only — see auth.routes.ts). Independent from NODE_ENV on
|
||||||
|
* purpose: NODE_ENV=production doesn't imply the deployment actually
|
||||||
|
* has TLS in front of it (e.g. an HTTP-only dev/staging instance), and
|
||||||
|
* a `Secure` cookie is silently never sent back by the browser over
|
||||||
|
* plain HTTP — every authenticated request 401s despite login
|
||||||
|
* succeeding, with no error to point at the cause. Unset (the default)
|
||||||
|
* falls back to NODE_ENV === "production", same as before this existed.
|
||||||
|
* Empty string counts as unset too, so `${COOKIE_SECURE:-}` in
|
||||||
|
* docker-compose.yml doesn't force it to `false` when not provided.
|
||||||
|
*/
|
||||||
|
COOKIE_SECURE: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.transform((value) => (value === undefined || value === "" ? undefined : value === "true")),
|
||||||
});
|
});
|
||||||
|
|
||||||
/** Parsed, validated environment — import this instead of reading `process.env` directly anywhere else. */
|
/** Parsed, validated environment — import this instead of reading `process.env` directly anywhere else. */
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,10 @@ const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
|
||||||
/** Cookie options shared by every route that sets the session cookie. */
|
/** Cookie options shared by every route that sets the session cookie. */
|
||||||
const cookieOptions: CookieOptions = {
|
const cookieOptions: CookieOptions = {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
// Only require HTTPS in production — local dev/CI serve over plain HTTP.
|
// Defaults to requiring HTTPS in production, but overridable via
|
||||||
secure: env.NODE_ENV === "production",
|
// COOKIE_SECURE — see its doc comment in config/env.ts for why this
|
||||||
|
// can't just be `NODE_ENV === "production"`.
|
||||||
|
secure: env.COOKIE_SECURE ?? env.NODE_ENV === "production",
|
||||||
sameSite: "lax",
|
sameSite: "lax",
|
||||||
maxAge: SEVEN_DAYS_MS,
|
maxAge: SEVEN_DAYS_MS,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,13 @@ services:
|
||||||
# always targets Postgres's internal port (5432).
|
# always targets Postgres's internal port (5432).
|
||||||
DATABASE_URL: "postgresql://${POSTGRES_USER:?set POSTGRES_USER in .env}:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}@postgres:5432/${POSTGRES_DB:?set POSTGRES_DB in .env}?schema=public"
|
DATABASE_URL: "postgresql://${POSTGRES_USER:?set POSTGRES_USER in .env}:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}@postgres:5432/${POSTGRES_DB:?set POSTGRES_DB in .env}?schema=public"
|
||||||
JWT_SECRET: ${JWT_SECRET:?set JWT_SECRET in .env}
|
JWT_SECRET: ${JWT_SECRET:?set JWT_SECRET in .env}
|
||||||
|
# Unset by default (falls back to NODE_ENV === "production", i.e.
|
||||||
|
# Secure cookie required) — set COOKIE_SECURE=false in .env only if
|
||||||
|
# this deployment is reachable over plain HTTP (no TLS in front of
|
||||||
|
# it yet), otherwise the session cookie never comes back and every
|
||||||
|
# authenticated request 401s despite login succeeding. See its doc
|
||||||
|
# comment in apps/api/src/config/env.ts.
|
||||||
|
COOKIE_SECURE: ${COOKIE_SECURE:-}
|
||||||
ports:
|
ports:
|
||||||
- "${APP_PORT:-3000}:3000"
|
- "${APP_PORT:-3000}:3000"
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue