From 69012029ad414010308eb18fdf548f93ef582ebe Mon Sep 17 00:00:00 2001 From: Giuseppe Raffa <77052701+sesee3@users.noreply.github.com> Date: Tue, 21 Apr 2026 20:47:32 +0200 Subject: [PATCH] refactor: clean up code and improve error handling in authentication and database modules --- auth/src/core/auth.core.js | 117 ++++++++------------------ auth/src/index.js | 13 ++- auth/src/middlewares/user.security.js | 38 ++------- auth/src/routes/auth.js | 117 +++++++++++++------------- auth/src/storage/database.js | 49 ++--------- auth/src/templates/loginpage.html | 9 +- 6 files changed, 115 insertions(+), 228 deletions(-) diff --git a/auth/src/core/auth.core.js b/auth/src/core/auth.core.js index fd03d5f..3e0c646 100644 --- a/auth/src/core/auth.core.js +++ b/auth/src/core/auth.core.js @@ -1,12 +1,8 @@ const query = require('../storage/database').query; -const track = require('../tools/tracking') +const track = require('../tools/tracking'); const { v4: uuid } = require('uuid'); -const security = require('../tools/security') +const security = require('../tools/security'); - -/** - * Registra un nuovo utente - */ async function register(username, password) { const userExists = await query('SELECT id FROM users WHERE username = $1', [username]); @@ -17,132 +13,89 @@ async function register(username, password) { const hashedPassword = await security.hashPassword(password); const id = uuid(); - await query('INSERT INTO users (id, username, password_hash) VALUES ($1, $2, $3)', [id, username, hashedPassword]); + await query( + 'INSERT INTO users (id, username, password_hash) VALUES ($1, $2, $3)', + [id, username, hashedPassword] + ); - return { - success: true, - user: { - id, - username - } - }; + return { success: true, user: { id, username } }; } - -/** - * Esegue il login di un utente - */ async function login(username, password) { - console.log('[DEBUG AUTH] login() START - username:', username); - - const result = await query('SELECT id, username, password_hash, created_at FROM users WHERE username = $1', [username]); - console.log('[DEBUG AUTH] login() - query result rows:', result.rows.length); - + const result = await query( + 'SELECT id, username, password_hash, is_active, created_at FROM users WHERE username = $1', + [username] + ); + if (result.rows.length === 0) { - console.log('[DEBUG AUTH] login() - user not found'); - throw new Error('No user matched') + throw new Error('No user matched'); } const user = result.rows[0]; - console.log('[DEBUG AUTH] login() - user found:', { id: user.id, username: user.username }); - - const isValid = await security.verifyPassword(password, user.password_hash); - console.log('[DEBUG AUTH] login() - password valid:', isValid); - if (!isValid) { - console.log('[DEBUG AUTH] login() - password mismatch'); - throw new Error('Password mismatch') + if (!user.is_active) { + throw new Error('User account is not active'); } - const result_obj = { + const isValid = await security.verifyPassword(password, user.password_hash); + + if (!isValid) { + throw new Error('Password mismatch'); + } + + return { id: user.id, username: user.username, created: user.created_at }; - console.log('[DEBUG AUTH] login() SUCCESS - returning user:', result_obj); - return result_obj; } -/** - * Esegue il logout di un utente - * - */ async function logout(sessionID) { if (!sessionID) { - throw new Error('no sessio id passed'); + throw new Error('No session ID provided'); } const result = await query('UPDATE sessions SET is_revoked = TRUE WHERE id = $1', [sessionID]); return result.rowCount > 0; } -/** - * Crea una nuova sessione per un utente che ha appaena eseguito il login - */ async function newSession(userId, userAgent, ip) { - console.log('[DEBUG AUTH] newSession() START - userId:', userId); - const id = uuid(); const sessionCode = security.generateSessionCode(); const metadata = track.getBasicMetadata(userAgent); - console.log('[DEBUG AUTH] newSession() - sessionCode generated:', sessionCode, 'metadata:', metadata); - const insertResult = await query( + await query( `INSERT INTO sessions (id, user_id, session_code, encoded_username, ip_address, user_agent, browser, os, device_type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`, [id, userId, sessionCode, '', ip, userAgent, metadata.browser, metadata.os, metadata.device_type] ); - console.log('[DEBUG AUTH] newSession() - INSERT result:', { rowCount: insertResult.rowCount }); - const result = { id, sessionCode }; - console.log('[DEBUG AUTH] newSession() SUCCESS - returning:', result); - return result; + return { id, sessionCode }; } -/** - * Valida una sessione tramite il suo UUID - */ async function validateSession(sessionId) { - console.log('[DEBUG AUTH] validateSession() START - sessionId:', sessionId); - if (!sessionId || typeof sessionId !== 'string') { - console.log('[DEBUG AUTH] validateSession() - invalid sessionId type'); throw new Error('Invalid session ID'); } - console.log('[DEBUG AUTH] validateSession() - executing query with sessionId:', sessionId); - - try { - const result = await query( - 'SELECT s.id, u.is_active FROM sessions s JOIN users u ON s.user_id = u.id WHERE s.id = $1 AND s.is_revoked = FALSE', - [sessionId] - ); - console.log('[DEBUG AUTH] validateSession() - query SUCCESS, rows:', result.rows.length); + const result = await query( + 'SELECT s.id, u.is_active FROM sessions s JOIN users u ON s.user_id = u.id WHERE s.id = $1 AND s.is_revoked = FALSE', + [sessionId] + ); - if (result.rows.length === 0) { - console.log('[DEBUG AUTH] validateSession() - session not found or revoked'); - throw new Error('Session not found or revoked'); - } + if (result.rows.length === 0) { + throw new Error('Session not found or revoked'); + } - console.log('[DEBUG AUTH] validateSession() - row data:', result.rows[0]); - - if (!result.rows[0].is_active) { - console.log('[DEBUG AUTH] validateSession() - user not active'); - throw new Error('User account is not active'); - } - - console.log('[DEBUG AUTH] validateSession() SUCCESS'); - } catch (err) { - console.error('[DEBUG AUTH] validateSession() ERROR:', err.message, err.code); - throw err; + if (!result.rows[0].is_active) { + throw new Error('User account is not active'); } } - module.exports = { register, login, logout, newSession, validateSession -} \ No newline at end of file +}; diff --git a/auth/src/index.js b/auth/src/index.js index 435aca7..d873fef 100644 --- a/auth/src/index.js +++ b/auth/src/index.js @@ -61,10 +61,13 @@ const authRateLimit = createRateLimiter(RATE_LIMIT_AUTH_MAX); app.use((req, res, next) => { res.setHeader('X-Content-Type-Options', 'nosniff'); res.setHeader('X-Frame-Options', 'DENY'); - res.setHeader('X-XSS-Protection', '0'); // Disabilitato a favore di CSP + res.setHeader('X-XSS-Protection', '0'); res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin'); res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()'); - // Rimuovi header che rivelano info sul server + res.setHeader( + 'Content-Security-Policy', + "default-src 'self'; style-src 'self'; script-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" + ); res.removeHeader('X-Powered-By'); next(); }); @@ -134,11 +137,7 @@ app.use((req, res) => { // ─── ERROR HANDLER GLOBALE ────────────────────────────────────────── app.use((err, req, res, _next) => { - console.error('[ERROR HANDLER] Global error caught:'); - console.error('[ERROR HANDLER] Message:', err.message); - console.error('[ERROR HANDLER] Code:', err.code); - console.error('[ERROR HANDLER] Full error:', err); - console.error('[ERROR HANDLER] Stack:', err.stack); + console.error('[ERROR]', err.message, '| code:', err.code); res.status(500).json({ error: 'Errore interno del server' }); }); diff --git a/auth/src/middlewares/user.security.js b/auth/src/middlewares/user.security.js index 73b01af..34b97dd 100644 --- a/auth/src/middlewares/user.security.js +++ b/auth/src/middlewares/user.security.js @@ -2,58 +2,32 @@ const jwt = require('../tools/jwt'); const { validateSession } = require('../core/auth.core'); const userAuth = async (req, res, next) => { - console.log('[DEBUG MIDDLEWARE] userAuth() START - path:', req.path); - const token = (req.cookies && req.cookies.auth_token) || jwt.getToken(req.headers['authorization']); - console.log('[DEBUG MIDDLEWARE] token found:', !!token); - const redirectToLogin = () => { - console.log('[DEBUG MIDDLEWARE] redirectToLogin()'); + const unauthorized = (reason) => { if (req.accepts('html')) { const redirect = encodeURIComponent(req.originalUrl); return res.redirect(`/login?redirect=${redirect}`); } - return res.status(401).json({ error: 'Accesso negato: token mancante' }); + return res.status(401).json({ error: reason || 'Non autorizzato' }); }; - if (!token || typeof token !== 'string') { - console.log('[DEBUG MIDDLEWARE] no token or invalid type'); - return redirectToLogin(); - } - - if (token.length > 2048) { - console.log('[DEBUG MIDDLEWARE] token too long'); - return redirectToLogin(); + if (!token || typeof token !== 'string' || token.length > 2048) { + return unauthorized('Token mancante o non valido'); } const verified = jwt.verifyToken(token); - console.log('[DEBUG MIDDLEWARE] jwt.verifyToken() result - valid:', verified.valid); - if (!verified.valid) { - console.log('[DEBUG MIDDLEWARE] token not valid - reason:', verified.reason); - if (req.accepts('html')) { - return res.redirect('/login'); - } - return res.status(401).json({ - error: 'Sessione non valida o scaduta', - reason: verified.reason - }); + return unauthorized(`Sessione non valida (${verified.reason})`); } try { - console.log('[DEBUG MIDDLEWARE] calling validateSession() with session_id:', verified.payload.session_id); await validateSession(verified.payload.session_id); - console.log('[DEBUG MIDDLEWARE] validateSession() SUCCESS'); } catch (err) { - console.error('[DEBUG MIDDLEWARE] validateSession() FAILED:', err.message); - if (req.accepts('html')) { - return res.redirect('/login'); - } - return res.status(401).json({ error: 'Sessione non valida o revocata' }); + return unauthorized('Sessione non valida o revocata'); } req.user = verified.payload; - console.log('[DEBUG MIDDLEWARE] auth SUCCESS - user_id:', req.user.user_id); next(); }; diff --git a/auth/src/routes/auth.js b/auth/src/routes/auth.js index 9293468..2e5e587 100644 --- a/auth/src/routes/auth.js +++ b/auth/src/routes/auth.js @@ -5,100 +5,93 @@ const jwt = require('../tools/jwt'); const CONSOLE_URL = process.env.CONSOLE_URL || 'http://localhost:3004'; const COOKIE_DOMAIN = process.env.COOKIE_DOMAIN || undefined; -// Validazione input const USERNAME_REGEX = /^[a-zA-Z0-9_.\-]{3,50}$/; const PASSWORD_MIN_LENGTH = 8; const PASSWORD_MAX_LENGTH = 128; +const ERROR_RESPONSES = { + csrf: { success: false, error: 'csrf', message: 'Richiesta non valida, riprova' }, + invalid_credentials: { success: false, error: 'invalid_credentials', message: 'Credenziali non valide' }, + internal: { success: false, error: 'internal', message: 'Errore interno, riprova più tardi' } +}; + +/** + * Restituisce un redirect sicuro, scartando URL che puntano ad API + * o ad host diversi da CONSOLE_URL. + */ +function resolveSafeRedirect(redirect) { + if (!redirect || typeof redirect !== 'string') return CONSOLE_URL; + + try { + const redirectUrl = new URL(redirect); + const consoleUrl = new URL(CONSOLE_URL); + + const sameHost = redirectUrl.hostname === consoleUrl.hostname; + const notApi = !redirectUrl.pathname.startsWith('/api/'); + + if (sameHost && notApi) return redirect; + } catch { + // URL non valido / relativo: fallback a CONSOLE_URL + } + + return CONSOLE_URL; +} + router.post('/register', async (req, res) => { const { username, password } = req.body; - if (!username || !password) { - return res.status(400).json({ error: 'Username e password richiesti' }); - } - - if (typeof username !== 'string' || typeof password !== 'string') { - return res.status(400).json({ error: 'Formato dati non valido' }); + if (!username || !password || typeof username !== 'string' || typeof password !== 'string') { + return res.status(400).json({ success: false, error: 'Username e password richiesti' }); } if (!USERNAME_REGEX.test(username)) { - return res.status(400).json({ - error: 'Username non valido. 3-50 caratteri alfanumerici, underscore, punto o trattino.' + return res.status(400).json({ + success: false, + error: 'Username non valido. 3-50 caratteri alfanumerici, underscore, punto o trattino.' }); } if (password.length < PASSWORD_MIN_LENGTH || password.length > PASSWORD_MAX_LENGTH) { - return res.status(400).json({ - error: `Password deve essere tra ${PASSWORD_MIN_LENGTH} e ${PASSWORD_MAX_LENGTH} caratteri` + return res.status(400).json({ + success: false, + error: `Password deve essere tra ${PASSWORD_MIN_LENGTH} e ${PASSWORD_MAX_LENGTH} caratteri` }); } try { await auth.register(username, password); - res.status(201).end(); + return res.status(201).json({ success: true }); } catch (err) { + if (err.message === 'User already exists') { + return res.status(409).json({ success: false, error: 'User already exists' }); + } console.error('[AUTH] Register failed:', err.message); - const status = err.message === 'User already exists' ? 409 : 500; - res.status(status).json({ error: err.message === 'User already exists' ? err.message : 'Errore interno' }); + return res.status(500).json({ success: false, error: 'Errore interno' }); } }); router.post('/login', async (req, res) => { const { username, password, redirect, _csrf } = req.body; - console.log('[DEBUG ROUTES] POST /api/auth/login START - username:', username); - - const ERROR_RESPONSES = { - csrf: { success: false, error: 'csrf', message: 'Richiesta non valida, riprova' }, - invalid_credentials: { success: false, error: 'invalid_credentials', message: 'Credenziali non valide' }, - invalid_redirect: { success: false, error: 'invalid_redirect', message: 'Redirect non autorizzato' } - }; - - // Validazione CSRF (double-submit cookie) const csrfCookie = req.cookies && req.cookies._csrf; if (!_csrf || !csrfCookie || _csrf !== csrfCookie) { - console.log('[DEBUG ROUTES] CSRF validation failed'); return res.status(400).json(ERROR_RESPONSES.csrf); } - // Validazione base if (!username || !password || typeof username !== 'string' || typeof password !== 'string') { - console.log('[DEBUG ROUTES] Invalid credentials format'); return res.status(400).json(ERROR_RESPONSES.invalid_credentials); } - // Limiti di lunghezza per prevenire abuse if (username.length > 50 || password.length > PASSWORD_MAX_LENGTH) { - console.log('[DEBUG ROUTES] Input too long'); return res.status(400).json(ERROR_RESPONSES.invalid_credentials); } - // Validazione redirect URL per prevenire open redirect attacks - let safeRedirect = CONSOLE_URL; - if (redirect && typeof redirect === 'string') { - try { - const redirectUrl = new URL(redirect); - const consoleUrl = new URL(CONSOLE_URL); - if (redirectUrl.hostname !== consoleUrl.hostname) { - console.log('[DEBUG ROUTES] Invalid redirect hostname'); - return res.status(400).json(ERROR_RESPONSES.invalid_redirect); - } - safeRedirect = redirect; - } catch { - // URL relativo o non valido — usa CONSOLE_URL di default - console.log('[DEBUG ROUTES] Redirect URL parse error, using default'); - } - } + const safeRedirect = resolveSafeRedirect(redirect); try { const user = await auth.login(username, password); - console.log('[DEBUG ROUTES] auth.login() success - user:', user); - const session = await auth.newSession(user.id, req.headers['user-agent'], req.ip); - console.log('[DEBUG ROUTES] auth.newSession() success - session:', session); - const token = jwt.generateToken(user, session.id); - console.log('[DEBUG ROUTES] jwt.generateToken() success'); const cookieOptions = { httpOnly: true, @@ -106,14 +99,10 @@ router.post('/login', async (req, res) => { sameSite: 'lax', maxAge: 7 * 24 * 60 * 60 * 1000 }; - - if (COOKIE_DOMAIN) { - cookieOptions.domain = COOKIE_DOMAIN; - } + if (COOKIE_DOMAIN) cookieOptions.domain = COOKIE_DOMAIN; res.cookie('auth_token', token, cookieOptions); res.clearCookie('_csrf'); - console.log('[DEBUG ROUTES] cookies set - returning response with redirect_url:', safeRedirect); return res.status(200).json({ success: true, @@ -121,8 +110,18 @@ router.post('/login', async (req, res) => { message: 'Login effettuato con successo' }); } catch (err) { - console.error('[DEBUG ROUTES] Login FAILED:', err.message, err.code); - return res.status(401).json(ERROR_RESPONSES.invalid_credentials); + if (err.message === 'No user matched' || err.message === 'Password mismatch') { + return res.status(401).json(ERROR_RESPONSES.invalid_credentials); + } + if (err.message === 'User account is not active') { + return res.status(403).json({ + success: false, + error: 'account_inactive', + message: 'Account disattivato' + }); + } + console.error('[AUTH] Login error:', err.message); + return res.status(500).json(ERROR_RESPONSES.internal); } }); @@ -141,12 +140,10 @@ router.post('/logout', async (req, res) => { } const clearOptions = { httpOnly: true, sameSite: 'lax' }; - if (COOKIE_DOMAIN) { - clearOptions.domain = COOKIE_DOMAIN; - } + if (COOKIE_DOMAIN) clearOptions.domain = COOKIE_DOMAIN; res.clearCookie('auth_token', clearOptions); - res.redirect('/login'); + return res.status(200).json({ success: true, redirect_url: '/login' }); }); module.exports = router; diff --git a/auth/src/storage/database.js b/auth/src/storage/database.js index 5e2247a..0a7cf41 100644 --- a/auth/src/storage/database.js +++ b/auth/src/storage/database.js @@ -5,74 +5,46 @@ const config = { password: process.env.DB_PASSWORD, host: process.env.DB_HOST, port: process.env.DB_PORT, + database: process.env.USERS_DB || process.env.DB_NAME, max: 10, idleTimeoutMillis: 30000, connectionTimeoutMillis: 5000 -} +}; -const pool = new Pool({ ...config, database: process.env.USERS_DB }); +const pool = new Pool(config); pool.on('error', (err) => { - console.error('Error in database', err); + console.error('[DB] Pool error:', err.message); }); -/** - * Execute a query with parameters - * @param {string} text - SQL query - * @param {Array} params - Query parameters - * @returns {Promise} Query result - */ async function query(text, params) { const start = Date.now(); - console.log('[DEBUG DB] Executing query:', text.substring(0, 120)); - if (params && params.length > 0) { - console.log('[DEBUG DB] Parameters:', params); - } - try { const result = await pool.query(text, params); const duration = Date.now() - start; - if (duration > 100) { console.warn(`[DB] Slow query (${duration}ms):`, text.substring(0, 80)); } - - console.log('[DEBUG DB] Query successful - rows:', result.rowCount, 'duration:', duration + 'ms'); return result; } catch (err) { - const duration = Date.now() - start; - console.error('[DEBUG DB] Query FAILED:', err.message); - console.error('[DEBUG DB] Error code:', err.code); - console.error('[DEBUG DB] Query:', text.substring(0, 120)); - console.error('[DEBUG DB] Duration:', duration + 'ms'); - console.error('[DEBUG DB] Full error:', err); + console.error('[DB] Query failed:', err.message, '| code:', err.code); throw err; } } -/** - * Get a client from pool for transactions - * @returns {Promise} Pool client - */ async function getClient() { return await pool.connect(); } -/** - * Initialize database and ensure tables exist - */ async function initDb() { - // Test connection await pool.query('SELECT NOW()'); - // Ensure pgcrypto extension (provides gen_random_uuid) - // Note: creating extensions requires proper DB permissions (usually superuser in PG) + try { await pool.query(`CREATE EXTENSION IF NOT EXISTS pgcrypto;`); } catch (err) { - console.warn('[DB] Could not create pgcrypto extension (may require superuser):', err.message); + console.warn('[DB] Could not create pgcrypto extension:', err.message); } - // Ensure tables exist (UUID default generated by DB) await pool.query(` CREATE TABLE IF NOT EXISTS users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), @@ -92,7 +64,7 @@ async function initDb() { id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, session_code VARCHAR(64) NOT NULL, - encoded_username TEXT NOT NULL, + encoded_username TEXT NOT NULL DEFAULT '', ip_address INET, user_agent TEXT, browser VARCHAR(100), @@ -105,9 +77,6 @@ async function initDb() { is_revoked BOOLEAN DEFAULT FALSE ); - -- Altera colonna in base al nuovo standard token 32 byte - 64 url chars - ALTER TABLE sessions ALTER COLUMN session_code TYPE VARCHAR(64); - CREATE INDEX IF NOT EXISTS idx_sessions_code ON sessions(session_code); CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id); `); @@ -117,7 +86,7 @@ async function checkPostgres() { try { await pool.query('SELECT NOW()'); return true; - } catch (error) { + } catch { return false; } } diff --git a/auth/src/templates/loginpage.html b/auth/src/templates/loginpage.html index c217746..c3b31ae 100644 --- a/auth/src/templates/loginpage.html +++ b/auth/src/templates/loginpage.html @@ -52,7 +52,6 @@ const formData = new FormData(form); try { - console.log('[LOGIN] Sending login request...'); const response = await fetch('/api/auth/login', { method: 'POST', headers: { @@ -67,14 +66,11 @@ }) }); - const data = await response.json(); - console.log('[LOGIN] Response:', data); + const data = await response.json().catch(() => ({})); - if (data.success) { - console.log('[LOGIN] Login successful, redirecting to:', data.redirect_url); + if (response.ok && data.success && data.redirect_url) { window.location.href = data.redirect_url; } else { - console.log('[LOGIN] Login failed:', data.error); const errorMsg = data.message || 'Errore durante il login'; if (errorMessage) { @@ -93,7 +89,6 @@ }, 2000); } } catch (err) { - console.error('[LOGIN] Error:', err); const errorMsg = 'Errore di connessione. Riprova più tardi.'; if (errorMessage) {