fix(deploy): auth/CSRF cookies dropped on plain-HTTP prod
Every cookie was flagged Secure whenever NODE_ENV=production. Over plain HTTP (single-host compose deploy without TLS) browsers silently discard Secure cookies, so the access token, refresh token, and CSRF cookie all vanished after login — producing 401 Unauthorized on every GET and 403 "CSRF token missing or invalid" on every mutation. Add COOKIE_SECURE to ApiEnv: optional boolean, falls back to NODE_ENV === 'production' when unset. Controllers and middleware now read env.COOKIE_SECURE instead of the NODE_ENV shortcut. The compose file sets it to false by default with a comment to flip once TLS is in front; HTTPS deployments can override via .env or drop the override to pick up the secure default.
This commit is contained in:
@@ -9,7 +9,7 @@ import { errors } from '../lib/http-error.js';
|
|||||||
const accessCookieOpts: CookieOptions = {
|
const accessCookieOpts: CookieOptions = {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
sameSite: 'lax',
|
sameSite: 'lax',
|
||||||
secure: env.NODE_ENV === 'production',
|
secure: env.COOKIE_SECURE,
|
||||||
path: '/',
|
path: '/',
|
||||||
maxAge: authService.ACCESS_TOKEN_TTL_MS,
|
maxAge: authService.ACCESS_TOKEN_TTL_MS,
|
||||||
};
|
};
|
||||||
@@ -17,7 +17,7 @@ const accessCookieOpts: CookieOptions = {
|
|||||||
const refreshCookieOpts: CookieOptions = {
|
const refreshCookieOpts: CookieOptions = {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
sameSite: 'lax',
|
sameSite: 'lax',
|
||||||
secure: env.NODE_ENV === 'production',
|
secure: env.COOKIE_SECURE,
|
||||||
path: '/api/auth',
|
path: '/api/auth',
|
||||||
maxAge: authService.REFRESH_TOKEN_TTL_MS,
|
maxAge: authService.REFRESH_TOKEN_TTL_MS,
|
||||||
};
|
};
|
||||||
|
|||||||
+4
-1
@@ -11,4 +11,7 @@ if (!parsed.success) {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const env = parsed.data;
|
export const env = {
|
||||||
|
...parsed.data,
|
||||||
|
COOKIE_SECURE: parsed.data.COOKIE_SECURE ?? parsed.data.NODE_ENV === 'production',
|
||||||
|
};
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export function issueCsrfToken(res: Response): string {
|
|||||||
const opts: CookieOptions = {
|
const opts: CookieOptions = {
|
||||||
httpOnly: false,
|
httpOnly: false,
|
||||||
sameSite: 'lax',
|
sameSite: 'lax',
|
||||||
secure: env.NODE_ENV === 'production',
|
secure: env.COOKIE_SECURE,
|
||||||
path: '/',
|
path: '/',
|
||||||
};
|
};
|
||||||
res.cookie(CSRF_COOKIE, token, opts);
|
res.cookie(CSRF_COOKIE, token, opts);
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ services:
|
|||||||
DATABASE_URL: file:/data/vector.db
|
DATABASE_URL: file:/data/vector.db
|
||||||
JWT_SECRET: ${JWT_SECRET:?JWT_SECRET is required — see .env.example}
|
JWT_SECRET: ${JWT_SECRET:?JWT_SECRET is required — see .env.example}
|
||||||
CLIENT_ORIGIN: ${CLIENT_ORIGIN:-http://localhost:8080}
|
CLIENT_ORIGIN: ${CLIENT_ORIGIN:-http://localhost:8080}
|
||||||
|
# Browsers drop Secure cookies over plain HTTP. Flip to "true" once
|
||||||
|
# this deployment sits behind TLS (reverse proxy, Cloudflare, etc).
|
||||||
|
COOKIE_SECURE: ${COOKIE_SECURE:-false}
|
||||||
volumes:
|
volumes:
|
||||||
- vector-data:/data
|
- vector-data:/data
|
||||||
healthcheck:
|
healthcheck:
|
||||||
|
|||||||
@@ -13,5 +13,10 @@ export const ApiEnv = z.object({
|
|||||||
message: 'JWT_SECRET still matches the default placeholder — generate a real secret',
|
message: 'JWT_SECRET still matches the default placeholder — generate a real secret',
|
||||||
}),
|
}),
|
||||||
CLIENT_ORIGIN: z.string().url().default('http://localhost:5173'),
|
CLIENT_ORIGIN: z.string().url().default('http://localhost:5173'),
|
||||||
|
// Whether to mark auth + CSRF cookies Secure. Must be false for plain-HTTP
|
||||||
|
// deployments (browsers silently drop Secure cookies over http://). Leave
|
||||||
|
// unset to fall back to NODE_ENV === 'production'.
|
||||||
|
COOKIE_SECURE: z
|
||||||
|
.preprocess((v) => (typeof v === 'string' ? v === 'true' : v), z.boolean().optional()),
|
||||||
});
|
});
|
||||||
export type ApiEnv = z.infer<typeof ApiEnv>;
|
export type ApiEnv = z.infer<typeof ApiEnv>;
|
||||||
|
|||||||
Reference in New Issue
Block a user