Files
chadebebe/lib/auth/utils.ts
Michael T 30c661a364 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
2026-01-23 15:26:24 -05:00

144 lines
3.8 KiB
TypeScript

import jwt from 'jsonwebtoken';
import crypto from 'crypto';
import fs from 'fs';
import path from 'path';
// Initialize secrets (auto-generate if not provided)
const secrets = initializeSecrets();
// Token expiry times
const TOKEN_EXPIRY = '72h';
const REFRESH_TOKEN_EXPIRY = '30d';
/**
* Initialize JWT secrets - auto-generate and persist if not provided in environment
*/
function initializeSecrets(): { secret: string; refreshSecret: string } {
const dataDir = path.join(process.cwd(), 'data');
const secretsFile = path.join(dataDir, 'secrets.json');
// If provided in environment, use those
if (process.env.SECRET) {
return {
secret: process.env.SECRET,
refreshSecret: process.env.REFRESH_SECRET || process.env.SECRET, // Use same secret if refresh not provided
};
}
// Ensure data directory exists
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true });
}
// Try to load existing secrets
if (fs.existsSync(secretsFile)) {
try {
const data = JSON.parse(fs.readFileSync(secretsFile, 'utf-8'));
return data;
} catch {
// Failed to load, will generate new ones
}
}
// Generate new cryptographically secure secrets (512 bits each)
const newSecrets = {
secret: crypto.randomBytes(64).toString('hex'),
refreshSecret: crypto.randomBytes(64).toString('hex'),
};
// Save to file with restricted permissions
try {
fs.writeFileSync(secretsFile, JSON.stringify(newSecrets, null, 2), { mode: 0o600 });
} catch (error) {
console.error('⚠️ Failed to save secrets file:', error);
console.error('⚠️ WARNING: Using in-memory secrets - tokens will be invalid after restart!');
}
return newSecrets;
}
export interface TokenPayload {
username: string;
type: 'access' | 'refresh';
}
/**
* Generate an access token
*/
export function generateAccessToken(username: string): string {
return jwt.sign(
{ username, type: 'access' } as TokenPayload,
secrets.secret,
{ expiresIn: TOKEN_EXPIRY } as jwt.SignOptions
);
}
/**
* Generate a refresh token
*/
export function generateRefreshToken(username: string): string {
return jwt.sign(
{ username, type: 'refresh' } as TokenPayload,
secrets.refreshSecret,
{ expiresIn: REFRESH_TOKEN_EXPIRY } as jwt.SignOptions
);
}
/**
* Verify an access token
*/
export function verifyAccessToken(token: string): TokenPayload | null {
try {
const payload = jwt.verify(token, secrets.secret) as TokenPayload;
if (payload.type !== 'access') {
return null;
}
return payload;
} catch (error) {
return null;
}
}
/**
* Verify a refresh token
*/
export function verifyRefreshToken(token: string): TokenPayload | null {
try {
const payload = jwt.verify(token, secrets.refreshSecret) as TokenPayload;
if (payload.type !== 'refresh') {
return null;
}
return payload;
} catch (error) {
return null;
}
}
/**
* Validate admin credentials against environment variables
*/
export function validateAdminCredentials(username: string, password: string): boolean {
const adminUsername = process.env.ADMIN_USERNAME || 'admin';
const adminPassword = process.env.ADMIN_PASSWORD;
if (!adminPassword) {
console.error('❌ ADMIN_PASSWORD not set in environment variables');
return false;
}
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;
}