fix(auth): centralize JWT secret + 24h TTL (Brief 1 of fix-auth-bypass)

- 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 <cursoragent@cursor.com>
This commit is contained in:
Randall Stillwell 2026-05-23 09:47:11 -05:00
parent bc0d1687d0
commit 5d72277355
8 changed files with 22 additions and 27 deletions

12
lib/auth-secret.js Normal file
View file

@ -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';

View file

@ -1,7 +1,6 @@
import { sql } from '@vercel/postgres'; import { sql } from '@vercel/postgres';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { JWT_SECRET } from './auth-secret.js';
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production';
/** /**
* Get user ID from request headers * Get user ID from request headers

View file

@ -1,7 +1,6 @@
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { db } from '../../lib/database.js'; import { db } from '../../lib/database.js';
import { JWT_SECRET, JWT_TOKEN_TTL } from '../../lib/auth-secret.js';
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
export async function hashPassword(password) { export async function hashPassword(password) {
const bcrypt = await import('bcryptjs'); const bcrypt = await import('bcryptjs');
@ -21,7 +20,7 @@ export function generateToken(user) {
role: user.role role: user.role
}, },
JWT_SECRET, JWT_SECRET,
{ expiresIn: '7d' } { expiresIn: JWT_TOKEN_TTL }
); );
} }

View file

@ -1,8 +1,6 @@
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import { sql } from '@vercel/postgres'; import { sql } from '@vercel/postgres';
import { generateToken } from '../auth-utils.js';
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production';
export default async function handler(req, res) { export default async function handler(req, res) {
// Set CORS headers // Set CORS headers
@ -48,11 +46,7 @@ export default async function handler(req, res) {
} }
// Generate JWT token // Generate JWT token
const token = jwt.sign( const token = generateToken({ id: user.id, email: user.email, role: user.role });
{ userId: user.id, email: user.email, role: user.role },
JWT_SECRET,
{ expiresIn: '24h' }
);
// Return user data (without password) and token // Return user data (without password) and token
const { password: _, ...userWithoutPassword } = user; const { password: _, ...userWithoutPassword } = user;

View file

@ -1,9 +1,7 @@
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import { sql } from '@vercel/postgres'; import { sql } from '@vercel/postgres';
import { generateUniqueSlug } from '../../../lib/slug-utils.js'; import { generateUniqueSlug } from '../../../lib/slug-utils.js';
import { generateToken } from '../auth-utils.js';
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production';
export default async function handler(req, res) { export default async function handler(req, res) {
// Set CORS headers // Set CORS headers
@ -140,11 +138,7 @@ export default async function handler(req, res) {
} }
// Generate JWT token // Generate JWT token
const token = jwt.sign( const token = generateToken({ id: user.id, email: user.email, role: user.role });
{ userId: user.id, email: user.email, role: user.role },
JWT_SECRET,
{ expiresIn: '24h' }
);
// Return user data without password // Return user data without password
const userResponse = { const userResponse = {

View file

@ -1,7 +1,6 @@
import { sql } from '@vercel/postgres'; import { sql } from '@vercel/postgres';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { JWT_SECRET } from '../../../lib/auth-secret.js';
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production';
export default async function handler(req, res) { export default async function handler(req, res) {
// Set CORS headers // Set CORS headers

View file

@ -1,7 +1,6 @@
import { sql } from '@vercel/postgres'; import { sql } from '@vercel/postgres';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { JWT_SECRET } from '../../lib/auth-secret.js';
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production';
export default async function handler(req, res) { export default async function handler(req, res) {
// Set CORS headers // Set CORS headers

View file

@ -1,7 +1,6 @@
import { sql } from '@vercel/postgres'; import { sql } from '@vercel/postgres';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { JWT_SECRET } from '../../../lib/auth-secret.js';
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production';
export default async function handler(req, res) { export default async function handler(req, res) {
// Set CORS headers // Set CORS headers