All files / services/shared internalAuth.js

95.19% Statements 99/104
70.37% Branches 19/27
100% Functions 6/6
95.19% Lines 99/104

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 1059x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 96x 96x 96x 9x 101x 101x 101x 101x 9x 9x 9x 9x 48x 48x 48x 48x 48x 48x 48x 9x 9x 9x 9x 55x 55x       55x 53x 53x 55x 53x 53x 53x 53x 53x 55x 55x 51x 51x 51x 51x 55x     51x 51x 55x 55x 51x 51x 55x 9x 9x 9x 9x 49x 49x 49x 49x 4x 4x 49x 49x 49x 9x 9x 9x 9x 48x 48x 48x 48x 9x 9x 9x 9x 9x 9x 9x 9x  
/**
 * Authentification interne service-to-service.
 *
 * Principe : un token signé unique dans le header `x-internal-token`.
 * Le token contient la signature HMAC-SHA256 + horodatage (iat/exp) :
 *   token = base64url(header) + "." + base64url(payload) + "." + signature
 *   signature = HMAC-SHA256(secret, header.payload)
 *   payload = { iat, exp, svc }
 *
 * Le récepteur vérifie en une opération : signature valide (authenticité),
 * exp non dépassée (anti-rejeu), iat pas dans le futur (horloge).
 * Le secret partagé (INTERNAL_API_KEY) ne circule jamais — seule la
 * signature HMAC dérivée transite.
 */
 
const crypto = require('crypto');
 
const INTERNAL_API_KEY = process.env.INTERNAL_API_KEY || '';
const TOKEN_HEADER = 'x-internal-token';
const TTL_SECONDS = 60;
const MAX_CLOCK_SKEW = 30;
 
function b64url(buf) {
  return Buffer.from(buf).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
}
 
function sign(secret, data) {
  return crypto.createHmac('sha256', secret).update(data).digest('base64')
    .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
}
 
/**
 * Génère un token interne signé (à mettre dans le header x-internal-token).
 */
function createInternalToken(serviceName = 'unknown') {
  const header = b64url(JSON.stringify({ alg: 'HS256', typ: 'internal' }));
  const now = Math.floor(Date.now() / 1000);
  const payload = b64url(JSON.stringify({ iat: now, exp: now + TTL_SECONDS, svc: serviceName }));
  const signature = sign(INTERNAL_API_KEY, `${header}.${payload}`);
  return `${header}.${payload}.${signature}`;
}
 
/**
 * Vérifie un token interne. Retourne le payload décodé ou null si invalide.
 */
function verifyInternalToken(token) {
  if (!INTERNAL_API_KEY) {
    console.error('[internalAuth] INTERNAL_API_KEY not configured');
    return null;
  }
  if (!token || typeof token !== 'string') return null;
 
  const parts = token.split('.');
  if (parts.length !== 3) return null;
 
  const [header, payload, signature] = parts;
  const expected = sign(INTERNAL_API_KEY, `${header}.${payload}`);
  const signatureBuf = Buffer.from(signature);
  const expectedBuf = Buffer.from(expected);
  if (signatureBuf.length !== expectedBuf.length) return null;
  if (!crypto.timingSafeEqual(signatureBuf, expectedBuf)) return null;
 
  let decoded;
  try {
    decoded = JSON.parse(Buffer.from(payload, 'base64').toString('utf8'));
  } catch {
    return null;
  }
 
  const now = Math.floor(Date.now() / 1000);
  if (decoded.exp && now > decoded.exp + MAX_CLOCK_SKEW) return null;
  if (decoded.iat && decoded.iat > now + MAX_CLOCK_SKEW) return null;
 
  return decoded;
}
 
/**
 * Middleware Express : exige un x-internal-token valide.
 */
function requireInternalKey(req, res, next) {
  const token = req.headers[TOKEN_HEADER];
  const decoded = verifyInternalToken(token);
  if (!decoded) {
    return res.status(401).json({ success: false, message: 'Unauthorized: invalid internal token' });
  }
  req.internalSvc = decoded.svc || 'unknown';
  next();
}
 
/**
 * Ajoute le header x-internal-token à une requête sortante (headers muté).
 */
function addInternalKey(headers = {}, serviceName = 'unknown') {
  if (INTERNAL_API_KEY) headers[TOKEN_HEADER] = createInternalToken(serviceName);
  return headers;
}
 
module.exports = {
  requireInternalKey,
  addInternalKey,
  createInternalToken,
  verifyInternalToken,
  TOKEN_HEADER,
};