2025-08-09 07:10:04 -04:00
|
|
|
import jwt from 'jsonwebtoken';
|
|
|
|
|
import fs from 'fs';
|
|
|
|
|
import path from 'path';
|
|
|
|
|
import os from 'os';
|
|
|
|
|
|
2025-12-05 14:21:52 -05:00
|
|
|
// JWT keys location - can be configured via environment variable
|
|
|
|
|
// In production, prefer using a secret management service (Vault, AWS Secrets Manager, etc.)
|
|
|
|
|
const secretFilePath = import.meta.env.JWT_KEYS_PATH || path.join(
|
2025-08-09 07:10:04 -04:00
|
|
|
os.homedir(),
|
|
|
|
|
'.config',
|
|
|
|
|
'api_jwt_keys.json'
|
|
|
|
|
);
|
|
|
|
|
|
2025-12-05 14:21:52 -05:00
|
|
|
// Warn if using default location in production
|
|
|
|
|
if (!import.meta.env.JWT_KEYS_PATH && !import.meta.env.DEV) {
|
|
|
|
|
console.warn(
|
|
|
|
|
'[SECURITY WARNING] JWT_KEYS_PATH not set. Using default location ~/.config/api_jwt_keys.json. ' +
|
|
|
|
|
'Consider using a secret management service in production.'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-19 11:59:00 -05:00
|
|
|
interface JwtKeyFile {
|
|
|
|
|
keys: Record<string, string>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface JwtPayload {
|
|
|
|
|
sub?: string;
|
|
|
|
|
exp?: number;
|
|
|
|
|
iat?: number;
|
|
|
|
|
roles?: string[];
|
|
|
|
|
[key: string]: unknown;
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-09 07:10:04 -04:00
|
|
|
// Load and parse keys JSON once at startup
|
2025-12-19 11:59:00 -05:00
|
|
|
let keyFileData: JwtKeyFile;
|
2025-12-05 14:21:52 -05:00
|
|
|
try {
|
|
|
|
|
keyFileData = JSON.parse(fs.readFileSync(secretFilePath, 'utf-8'));
|
|
|
|
|
} catch (err) {
|
2025-12-19 11:59:00 -05:00
|
|
|
console.error(`[CRITICAL] Failed to load JWT keys from ${secretFilePath}:`, (err as Error).message);
|
2025-12-05 14:21:52 -05:00
|
|
|
throw new Error('JWT keys file not found or invalid. Set JWT_KEYS_PATH environment variable.');
|
|
|
|
|
}
|
2025-08-09 07:10:04 -04:00
|
|
|
|
2025-12-19 11:59:00 -05:00
|
|
|
export function verifyToken(token: string | null | undefined): JwtPayload | null {
|
2025-08-09 07:10:04 -04:00
|
|
|
if (!token) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const decoded = jwt.decode(token, { complete: true });
|
|
|
|
|
if (!decoded?.header?.kid) {
|
|
|
|
|
throw new Error('No kid in token header');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const kid = decoded.header.kid;
|
|
|
|
|
const key = keyFileData.keys[kid];
|
|
|
|
|
|
|
|
|
|
if (!key) {
|
|
|
|
|
throw new Error(`Unknown kid: ${kid}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify using the correct key and HS256 algo
|
2025-12-19 11:59:00 -05:00
|
|
|
const payload = jwt.verify(token, key, { algorithms: ['HS256'] }) as JwtPayload;
|
2025-08-09 07:10:04 -04:00
|
|
|
return payload;
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
2025-12-19 11:59:00 -05:00
|
|
|
console.error('JWT verification failed:', (error as Error).message);
|
2025-08-09 07:10:04 -04:00
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|