From 30c661a364598bb03ce55bc0cc4d1fe0fed5369d Mon Sep 17 00:00:00 2001 From: Michael T Date: Fri, 23 Jan 2026 15:26:24 -0500 Subject: [PATCH] fix(auth): resolve cookie authentication failure over HTTP Cookies were set with secure flag based solely on NODE_ENV, causing 401 errors when accessing over HTTP with NODE_ENV=production. - Add COOKIE_SECURE env var for explicit control - Auto-detect HTTPS via X-Forwarded-Proto header in production - Extract isSecureCookie() utility to lib/auth/utils.ts - Document COOKIE_SECURE in README and .env.example Fixes #39 --- .env.example | 5 +++++ README.md | 5 +++++ app/api/auth/login/route.ts | 4 ++-- app/api/auth/refresh/route.ts | 4 ++-- app/api/lock/route.ts | 6 +++--- lib/auth/utils.ts | 13 +++++++++++++ 6 files changed, 30 insertions(+), 7 deletions(-) diff --git a/.env.example b/.env.example index f5799af..83b7420 100644 --- a/.env.example +++ b/.env.example @@ -12,6 +12,11 @@ SECRET= NODE_ENV=production PORT=3000 +# Cookie Security (Optional) +# Set to 'false' if accessing over HTTP (e.g., local LAN without HTTPS) +# When unset, auto-detects HTTPS via X-Forwarded-Proto header +# COOKIE_SECURE=false + # Timezone for logs (Optional) TZ=America/New_York diff --git a/README.md b/README.md index 09995a3..de86c5d 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,11 @@ PGID=1000 # Optional - JWT Secret (auto-generated if not provided) # Generate with: openssl rand -base64 32 SECRET= + +# Optional - Cookie Security +# Set to 'false' for HTTP access (e.g., local LAN without HTTPS) +# When unset, auto-detects HTTPS via X-Forwarded-Proto header +COOKIE_SECURE=false ``` ### User Permissions (PUID/PGID) diff --git a/app/api/auth/login/route.ts b/app/api/auth/login/route.ts index 22c5289..f4b54c4 100644 --- a/app/api/auth/login/route.ts +++ b/app/api/auth/login/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from 'next/server'; -import { generateAccessToken, generateRefreshToken, validateAdminCredentials } from '@/lib/auth/utils'; +import { generateAccessToken, generateRefreshToken, validateAdminCredentials, isSecureCookie } from '@/lib/auth/utils'; export async function POST(request: NextRequest) { try { @@ -36,7 +36,7 @@ export async function POST(request: NextRequest) { // Set cookies const cookieOptions = { httpOnly: true, - secure: process.env.NODE_ENV === 'production', + secure: isSecureCookie(request), sameSite: 'lax' as const, path: '/', }; diff --git a/app/api/auth/refresh/route.ts b/app/api/auth/refresh/route.ts index 677053c..537461f 100644 --- a/app/api/auth/refresh/route.ts +++ b/app/api/auth/refresh/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from 'next/server'; -import { generateAccessToken, generateRefreshToken, verifyRefreshToken } from '@/lib/auth/utils'; +import { generateAccessToken, generateRefreshToken, verifyRefreshToken, isSecureCookie } from '@/lib/auth/utils'; export async function POST(request: NextRequest) { try { @@ -49,7 +49,7 @@ export async function POST(request: NextRequest) { // Set new cookies const cookieOptions = { httpOnly: true, - secure: process.env.NODE_ENV === 'production', + secure: isSecureCookie(request), sameSite: 'lax' as const, path: '/', }; diff --git a/app/api/lock/route.ts b/app/api/lock/route.ts index 3d653bb..7b592cd 100644 --- a/app/api/lock/route.ts +++ b/app/api/lock/route.ts @@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { eq } from 'drizzle-orm'; import { db, settings } from '@/lib/db'; import crypto from 'crypto'; +import { isSecureCookie } from '@/lib/auth/utils'; // POST /api/lock - Verify password export async function POST(request: NextRequest) { @@ -41,12 +42,11 @@ export async function POST(request: NextRequest) { message: 'Password verified', }); - // Set an unlock cookie that expires in 24 hours response.cookies.set('site_unlocked', 'true', { httpOnly: true, - secure: process.env.NODE_ENV === 'production', + secure: isSecureCookie(request), sameSite: 'lax', - maxAge: 60 * 60 * 24, // 24 hours + maxAge: 60 * 60 * 24, path: '/', }); diff --git a/lib/auth/utils.ts b/lib/auth/utils.ts index 83f1781..f69920e 100644 --- a/lib/auth/utils.ts +++ b/lib/auth/utils.ts @@ -128,3 +128,16 @@ export function validateAdminCredentials(username: string, password: string): bo return username === adminUsername && password === adminPassword; } + +export function isSecureCookie(request: { headers: { get(name: string): string | null }; url: string }): boolean { + if (process.env.COOKIE_SECURE !== undefined) { + return process.env.COOKIE_SECURE === 'true'; + } + if (process.env.NODE_ENV === 'production') { + return ( + request.headers.get('x-forwarded-proto') === 'https' || + request.url.startsWith('https://') + ); + } + return false; +}