Files
filing-cabinet-api/middleware/authenticate_token.js
T
2026-07-29 22:35:36 +02:00

106 lines
3.6 KiB
JavaScript

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 getTokenFromRequest(req) {
const authorization = req.get('authorization') || '';
const match = authorization.match(/^Bearer\s+([^\s]+)$/i);
if (match) {
return match[1];
}
const cookieHeader = req.get('cookie') || '';
if (cookieHeader) {
const cookies = cookieHeader.split(';');
for (const cookie of cookies) {
const [name, ...rest] = cookie.trim().split('=');
if (name === 'fc_session_token') {
return rest.join('=');
}
}
}
return null;
}
function authenticateToken(req, res, next) {
const token = getTokenFromRequest(req);
if (!token) {
return res.status(401).send('A valid Bearer token or session cookie is required');
}
try {
const [encodedHeader, encodedPayload, providedSignature] = token.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 };