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
This commit is contained in:
@@ -12,6 +12,11 @@ SECRET=
|
|||||||
NODE_ENV=production
|
NODE_ENV=production
|
||||||
PORT=3000
|
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)
|
# Timezone for logs (Optional)
|
||||||
TZ=America/New_York
|
TZ=America/New_York
|
||||||
|
|
||||||
|
|||||||
@@ -102,6 +102,11 @@ PGID=1000
|
|||||||
# Optional - JWT Secret (auto-generated if not provided)
|
# Optional - JWT Secret (auto-generated if not provided)
|
||||||
# Generate with: openssl rand -base64 32
|
# Generate with: openssl rand -base64 32
|
||||||
SECRET=
|
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)
|
### User Permissions (PUID/PGID)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
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) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
@@ -36,7 +36,7 @@ export async function POST(request: NextRequest) {
|
|||||||
// Set cookies
|
// Set cookies
|
||||||
const cookieOptions = {
|
const cookieOptions = {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: process.env.NODE_ENV === 'production',
|
secure: isSecureCookie(request),
|
||||||
sameSite: 'lax' as const,
|
sameSite: 'lax' as const,
|
||||||
path: '/',
|
path: '/',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
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) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
@@ -49,7 +49,7 @@ export async function POST(request: NextRequest) {
|
|||||||
// Set new cookies
|
// Set new cookies
|
||||||
const cookieOptions = {
|
const cookieOptions = {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: process.env.NODE_ENV === 'production',
|
secure: isSecureCookie(request),
|
||||||
sameSite: 'lax' as const,
|
sameSite: 'lax' as const,
|
||||||
path: '/',
|
path: '/',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||||||
import { eq } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
import { db, settings } from '@/lib/db';
|
import { db, settings } from '@/lib/db';
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
|
import { isSecureCookie } from '@/lib/auth/utils';
|
||||||
|
|
||||||
// POST /api/lock - Verify password
|
// POST /api/lock - Verify password
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
@@ -41,12 +42,11 @@ export async function POST(request: NextRequest) {
|
|||||||
message: 'Password verified',
|
message: 'Password verified',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set an unlock cookie that expires in 24 hours
|
|
||||||
response.cookies.set('site_unlocked', 'true', {
|
response.cookies.set('site_unlocked', 'true', {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: process.env.NODE_ENV === 'production',
|
secure: isSecureCookie(request),
|
||||||
sameSite: 'lax',
|
sameSite: 'lax',
|
||||||
maxAge: 60 * 60 * 24, // 24 hours
|
maxAge: 60 * 60 * 24,
|
||||||
path: '/',
|
path: '/',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -128,3 +128,16 @@ export function validateAdminCredentials(username: string, password: string): bo
|
|||||||
|
|
||||||
return username === adminUsername && password === adminPassword;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user