All files / src/helpers sessionTokens.js

0% Statements 0/36
0% Branches 0/20
0% Functions 0/4
0% Lines 0/35

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 105 106 107 108                                                                                                                                                                                                                       
/**
 * Helper pour l'emission de tokens de session (JWT + cookies).
 *
 * Centralise la logique dupliquee dans les endpoints 2FA (/validate et /backup)
 * pour creer les tokens d'acces + refresh, stocker en DB et poser les cookies.
 *
 * @module helpers/sessionTokens
 */
 
const jwt = require('jsonwebtoken');
const crypto = require('node:crypto');
const { pool: db } = require('../db');
 
const IS_PRODUCTION = process.env.NODE_ENV === 'production';
const COOKIE_DOMAIN = process.env.COOKIE_DOMAIN || undefined;
 
function cookieOptions(maxAgeMs) {
  const opts = {
    httpOnly: true,
    secure: IS_PRODUCTION,
    sameSite: IS_PRODUCTION ? 'None' : 'Lax',
    path: '/',
    maxAge: maxAgeMs,
  };
  if (COOKIE_DOMAIN) opts.domain = COOKIE_DOMAIN;
  return opts;
}
 
/**
 * Verifie un token JWT 2FA temporaire.
 *
 * @param {string} token — Token JWT
 * @returns {{ valid: boolean, payload?: object, error?: string }}
 */
function verify2FAToken(token) {
  try {
    const payload = jwt.verify(token, process.env.JWT_SECRET);
    if (payload.purpose !== '2fa') {
      return { valid: false, error: 'Token invalide.' };
    }
    return { valid: true, payload };
  } catch {
    return { valid: false, error: 'Token 2FA expire ou invalide. Reconnectez-vous.' };
  }
}
 
/**
 * Emet les tokens de session (access + refresh), les stocke en DB et pose les cookies.
 *
 * @param {{ id: number, email: string, plan: string }} user — Utilisateur
 * @param {import('express').Request} req — Requete Express (pour IP et user-agent)
 * @param {import('express').Response} res — Reponse Express (pour les cookies)
 */
async function issueSessionTokens(user, req, res) {
  const accessToken = jwt.sign(
    { sub: user.id, email: user.email, plan: user.plan },
    process.env.JWT_SECRET,
    { expiresIn: process.env.JWT_EXPIRES_IN || '15m' },
  );
  const refreshToken = jwt.sign(
    { sub: user.id },
    process.env.JWT_REFRESH_SECRET,
    { expiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '7d' },
  );
 
  const tokenHash = crypto.createHash('sha256').update(refreshToken).digest('hex');
  const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
  const forwarded = req.headers['x-forwarded-for'];
  const ip = forwarded ? forwarded.split(',')[0].trim() : (req.headers['x-real-ip'] || req.ip || req.socket?.remoteAddress || 'unknown');
  const userAgent = req.headers?.['user-agent']?.substring(0, 512) || null;
 
  await db.query(
    'INSERT INTO refresh_tokens (user_id, token_hash, expires_at, ip, user_agent) VALUES ($1, $2, $3, $4, $5)',
    [user.id, tokenHash, expiresAt, ip, userAgent],
  );
 
  res.cookie('access_token', accessToken, cookieOptions(15 * 60 * 1000));
  res.cookie('refresh_token', refreshToken, cookieOptions(7 * 24 * 60 * 60 * 1000));
}
 
/**
 * Remplace les codes de secours TOTP d'un utilisateur (dans une transaction).
 *
 * @param {number} userId — ID utilisateur
 * @param {string[]} backupHashes — Hashes des nouveaux codes
 */
async function replaceBackupCodes(userId, backupHashes) {
  const client = await db.connect();
  try {
    await client.query('BEGIN');
    await client.query('DELETE FROM totp_backup_codes WHERE user_id = $1', [userId]);
    for (const hash of backupHashes) {
      await client.query(
        'INSERT INTO totp_backup_codes (user_id, code_hash) VALUES ($1, $2)',
        [userId, hash],
      );
    }
    await client.query('COMMIT');
  } catch (err) {
    await client.query('ROLLBACK');
    throw err;
  } finally {
    client.release();
  }
}
 
module.exports = { verify2FAToken, issueSessionTokens, replaceBackupCodes, cookieOptions };