const crypto = require('crypto'); const DEFAULT_TOKEN_LIFETIME_SECONDS = 60 * 60; function base64url(value) { return Buffer.from(value).toString('base64url'); } function getJwtSecret() { const secret = process.env.JWT_SECRET; if (!secret || secret.length < 32) { throw new Error('JWT_SECRET must be configured and contain at least 32 characters'); } return secret; } function signToken(userId) { const now = Math.floor(Date.now() / 1000); const configuredLifetime = Number.parseInt(process.env.JWT_EXPIRES_IN, 10); const lifetime = Number.isInteger(configuredLifetime) && configuredLifetime > 0 ? configuredLifetime : DEFAULT_TOKEN_LIFETIME_SECONDS; const payload = { user_id: userId, iat: now, exp: now + lifetime, }; const encodedHeader = base64url(JSON.stringify({ alg: 'HS256', typ: 'JWT' })); const encodedPayload = base64url(JSON.stringify(payload)); const content = `${encodedHeader}.${encodedPayload}`; const signature = crypto .createHmac('sha256', getJwtSecret()) .update(content) .digest('base64url'); return { token: `${content}.${signature}`, expiresAt: payload.exp }; } function authenticateToken(req, res, next) { const authorization = req.get('authorization') || ''; const match = authorization.match(/^Bearer\s+([^\s]+)$/i); if (!match) { return res.status(401).send('A valid Bearer token is required'); } try { const [encodedHeader, encodedPayload, providedSignature] = match[1].split('.'); if (!encodedHeader || !encodedPayload || !providedSignature) { throw new Error('Malformed token'); } const header = JSON.parse(Buffer.from(encodedHeader, 'base64url').toString('utf8')); const payload = JSON.parse(Buffer.from(encodedPayload, 'base64url').toString('utf8')); if (header.alg !== 'HS256' || header.typ !== 'JWT') { throw new Error('Unsupported token'); } const expectedSignature = crypto .createHmac('sha256', getJwtSecret()) .update(`${encodedHeader}.${encodedPayload}`) .digest('base64url'); const provided = Buffer.from(providedSignature); const expected = Buffer.from(expectedSignature); if (provided.length !== expected.length || !crypto.timingSafeEqual(provided, expected)) { throw new Error('Invalid signature'); } if (!Number.isInteger(payload.user_id) || payload.user_id <= 0 || !Number.isInteger(payload.exp) || payload.exp <= Math.floor(Date.now() / 1000)) { throw new Error('Invalid or expired token'); } req.user = { id: payload.user_id }; next(); } catch (err) { if (err.message.startsWith('JWT_SECRET')) { console.error('[auth] JWT configuration error:', err.message); return res.status(500).send('Authentication is not configured'); } return res.status(401).send('Invalid or expired token'); } } module.exports = { authenticateToken, signToken };