All files / services/shared internalAuth.js

46.53% Statements 47/101
50% Branches 1/2
0% Functions 0/6
46.53% Lines 47/101

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 1025x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x       5x         5x 5x 5x 5x               5x 5x 5x 5x                                                       5x 5x 5x 5x                   5x 5x 5x 5x         5x 5x 5x 5x 5x 5x 5x 5x  
/**
 * 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}`);
  if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) 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,
};