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 }; |