From 5d72277355d5579c52af0e001349d2ede7f8e619 Mon Sep 17 00:00:00 2001 From: Randall Stillwell Date: Sat, 23 May 2026 09:47:11 -0500 Subject: [PATCH] fix(auth): centralize JWT secret + 24h TTL (Brief 1 of fix-auth-bypass) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New `lib/auth-secret.js` is the single source of truth for `JWT_SECRET` and the canonical `JWT_TOKEN_TTL = '24h'`. Module throws at import time if `process.env.JWT_SECRET` is unset — no silent fallback to the literal `'your-secret-key-change-in-production'`. - 7 callers refactored to import from the helper: lib/permission-middleware.js pages/api/auth-utils.js (also drops unused `'7d'` → JWT_TOKEN_TTL) pages/api/auth/login.js (also routes via auth-utils.generateToken) pages/api/auth/register.js (same) pages/api/auth/verify.js (Brief 2 still owns the no-token admin branch) pages/api/favorites.js pages/api/users/search.js - `process.env.JWT_SECRET` now appears exactly once in the JS source (lib/auth-secret.js). `your-secret-key-change-in-production` is gone. - TTL drift reconciled: auth-utils used `'7d'`, login/register used inline `'24h'`. Both now route through imported `JWT_TOKEN_TTL` (24h). Pre-deploy reminder: Vercel must have `JWT_SECRET` set before merge or serverless functions refuse to boot. Existing tokens (signed against the fallback literal) will be invalidated — users will need to log in again. Resolves AGENTS.md gotcha #3. Brief 2/3/4/5 still pending in convoy. Convoy: fix-auth-bypass / Brief 1 Co-authored-by: Cursor --- lib/auth-secret.js | 12 ++++++++++++ lib/permission-middleware.js | 3 +-- pages/api/auth-utils.js | 5 ++--- pages/api/auth/login.js | 10 ++-------- pages/api/auth/register.js | 10 ++-------- pages/api/auth/verify.js | 3 +-- pages/api/favorites.js | 3 +-- pages/api/users/search.js | 3 +-- 8 files changed, 22 insertions(+), 27 deletions(-) create mode 100644 lib/auth-secret.js diff --git a/lib/auth-secret.js b/lib/auth-secret.js new file mode 100644 index 0000000..2916283 --- /dev/null +++ b/lib/auth-secret.js @@ -0,0 +1,12 @@ +const JWT_SECRET = process.env.JWT_SECRET; + +if (!JWT_SECRET) { + throw new Error( + 'JWT_SECRET environment variable is not set. ' + + 'Set it in .env.local for local dev, or in the Vercel project settings for deploys. ' + + 'Generate a strong secret with: openssl rand -hex 32' + ); +} + +export { JWT_SECRET }; +export const JWT_TOKEN_TTL = '24h'; diff --git a/lib/permission-middleware.js b/lib/permission-middleware.js index 53e1cad..49bd3d0 100644 --- a/lib/permission-middleware.js +++ b/lib/permission-middleware.js @@ -1,7 +1,6 @@ import { sql } from '@vercel/postgres'; import jwt from 'jsonwebtoken'; - -const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production'; +import { JWT_SECRET } from './auth-secret.js'; /** * Get user ID from request headers diff --git a/pages/api/auth-utils.js b/pages/api/auth-utils.js index b9169f4..3a92321 100644 --- a/pages/api/auth-utils.js +++ b/pages/api/auth-utils.js @@ -1,7 +1,6 @@ import jwt from 'jsonwebtoken'; import { db } from '../../lib/database.js'; - -const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key'; +import { JWT_SECRET, JWT_TOKEN_TTL } from '../../lib/auth-secret.js'; export async function hashPassword(password) { const bcrypt = await import('bcryptjs'); @@ -21,7 +20,7 @@ export function generateToken(user) { role: user.role }, JWT_SECRET, - { expiresIn: '7d' } + { expiresIn: JWT_TOKEN_TTL } ); } diff --git a/pages/api/auth/login.js b/pages/api/auth/login.js index ac2ec71..fd135c8 100644 --- a/pages/api/auth/login.js +++ b/pages/api/auth/login.js @@ -1,8 +1,6 @@ import bcrypt from 'bcryptjs'; -import jwt from 'jsonwebtoken'; import { sql } from '@vercel/postgres'; - -const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production'; +import { generateToken } from '../auth-utils.js'; export default async function handler(req, res) { // Set CORS headers @@ -48,11 +46,7 @@ export default async function handler(req, res) { } // Generate JWT token - const token = jwt.sign( - { userId: user.id, email: user.email, role: user.role }, - JWT_SECRET, - { expiresIn: '24h' } - ); + const token = generateToken({ id: user.id, email: user.email, role: user.role }); // Return user data (without password) and token const { password: _, ...userWithoutPassword } = user; diff --git a/pages/api/auth/register.js b/pages/api/auth/register.js index ea25ec4..44bb475 100644 --- a/pages/api/auth/register.js +++ b/pages/api/auth/register.js @@ -1,9 +1,7 @@ import bcrypt from 'bcryptjs'; -import jwt from 'jsonwebtoken'; import { sql } from '@vercel/postgres'; import { generateUniqueSlug } from '../../../lib/slug-utils.js'; - -const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production'; +import { generateToken } from '../auth-utils.js'; export default async function handler(req, res) { // Set CORS headers @@ -140,11 +138,7 @@ export default async function handler(req, res) { } // Generate JWT token - const token = jwt.sign( - { userId: user.id, email: user.email, role: user.role }, - JWT_SECRET, - { expiresIn: '24h' } - ); + const token = generateToken({ id: user.id, email: user.email, role: user.role }); // Return user data without password const userResponse = { diff --git a/pages/api/auth/verify.js b/pages/api/auth/verify.js index 174c6b7..7d9675d 100644 --- a/pages/api/auth/verify.js +++ b/pages/api/auth/verify.js @@ -1,7 +1,6 @@ import { sql } from '@vercel/postgres'; import jwt from 'jsonwebtoken'; - -const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production'; +import { JWT_SECRET } from '../../../lib/auth-secret.js'; export default async function handler(req, res) { // Set CORS headers diff --git a/pages/api/favorites.js b/pages/api/favorites.js index 5f9eec2..f6db406 100644 --- a/pages/api/favorites.js +++ b/pages/api/favorites.js @@ -1,7 +1,6 @@ import { sql } from '@vercel/postgres'; import jwt from 'jsonwebtoken'; - -const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production'; +import { JWT_SECRET } from '../../lib/auth-secret.js'; export default async function handler(req, res) { // Set CORS headers diff --git a/pages/api/users/search.js b/pages/api/users/search.js index 72ede3f..d80d6be 100644 --- a/pages/api/users/search.js +++ b/pages/api/users/search.js @@ -1,7 +1,6 @@ import { sql } from '@vercel/postgres'; import jwt from 'jsonwebtoken'; - -const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production'; +import { JWT_SECRET } from '../../../lib/auth-secret.js'; export default async function handler(req, res) { // Set CORS headers