From 974cbe93cdab2b0c2e6f78ce1d4b31a20cc5718d Mon Sep 17 00:00:00 2001 From: Giuseppe Raffa <77052701+sesee3@users.noreply.github.com> Date: Tue, 21 Apr 2026 20:08:59 +0200 Subject: [PATCH] fix: additional fix for auth login flow and auth web pages and database connection. --- .env.example | 4 + .gitignore | 4 +- auth/src/core/auth.core.js | 32 ++++---- auth/src/core/session.core.js | 2 +- auth/src/index.js | 1 + auth/src/middlewares/user.security.js | 40 ++++++---- auth/src/routes/auth.js | 29 +++++-- auth/src/routes/views/auth.js | 21 ++++- auth/src/routes/views/sessions.js | 2 +- auth/src/routes/views/user.js | 7 +- auth/src/static/style/style.css | 31 +++----- auth/src/templates/loginpage.html | 22 +++--- auth/src/templates/sessions.html | 108 ++++++++++++++++++++++++++ auth/src/templates/user.html | 86 ++++++++++++++++++++ auth/src/tools/jwt.js | 3 +- auth/src/tools/security.js | 8 +- docker-compose.yml | 18 ++--- 17 files changed, 327 insertions(+), 91 deletions(-) diff --git a/.env.example b/.env.example index e69de29..cd09878 100644 --- a/.env.example +++ b/.env.example @@ -0,0 +1,4 @@ +DOMAIN= + +#production= mebboat.it +#development= localhost \ No newline at end of file diff --git a/.gitignore b/.gitignore index a256828..4547e93 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,6 @@ Thumbs.db .vscode/ .idea/ **/tsconfig.tsbuildinfo -.eslintcache \ No newline at end of file +.eslintcache + +.venv/ \ No newline at end of file diff --git a/auth/src/core/auth.core.js b/auth/src/core/auth.core.js index 203038e..3cf534c 100644 --- a/auth/src/core/auth.core.js +++ b/auth/src/core/auth.core.js @@ -14,7 +14,7 @@ async function register(username, password) { throw new Error('User already exists'); } - const hashedPassword = security.hashPassword(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]); @@ -33,7 +33,7 @@ async function register(username, password) { * Esegue il login di un utente */ async function login(username, password) { - const result = await query('SELECT id, username, password_hash, active, created_at FROM users WHERE username = $1', [username]); + const result = await query('SELECT id, username, password_hash, created_at FROM users WHERE username = $1', [username]); if (result.rows.length === 0) { throw new Error('No user matched') } @@ -83,30 +83,24 @@ async function newSession(userId, userAgent, ip) { } /** - * Valida una sessione + * Valida una sessione tramite il suo UUID */ -async function validateSession(token) { - const parsed = security.parseSessionToken(token); - - if (!parsed) { - throw new Error('Invalid token format'); +async function validateSession(sessionId) { + if (!sessionId || typeof sessionId !== 'string') { + throw new Error('Invalid session ID'); } - const { code, username } = parsed; + 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] + ); - const result = await query('SELECT s.id as session_id, s.user_id, u.username, u.is_active, u.created_at FROM sessions s JOIN users u ON s.user_id = u.id WHERE s.session_code = $1 AND s.is_revoked = FALSE', [code]); if (result.rows.length === 0) { - throw new Error('Session not found or revoked') + throw new Error('Session not found or revoked'); } - const session = result.rows[0]; - - if (session.username !== username) { - throw new Error('Session user mismatch'); - } - - if (!session.active) { - throw new Error('Session is not active'); + if (!result.rows[0].is_active) { + throw new Error('User account is not active'); } } diff --git a/auth/src/core/session.core.js b/auth/src/core/session.core.js index 88a6b9b..39a853a 100644 --- a/auth/src/core/session.core.js +++ b/auth/src/core/session.core.js @@ -27,7 +27,7 @@ async function getCurrentSessionID(token) { throw new Error('Invalid token'); } - const result = await query('SELECT id FROM sessions WHERE session_code = $1', [parsed.code]); + const result = await query('SELECT id FROM sessions WHERE session_code = $1', [parsed]); return result.rows[0]?.id || null; } diff --git a/auth/src/index.js b/auth/src/index.js index d7268a0..66174bf 100644 --- a/auth/src/index.js +++ b/auth/src/index.js @@ -80,6 +80,7 @@ app.use(parser()); // ─── STATIC FILES ─────────────────────────────────────────────────── const staticFolder = path.join(__dirname, 'static'); app.use('/static', express.static(staticFolder)); +app.use('/api/static', express.static(staticFolder)); // ─── NUNJUCKS TEMPLATES ───────────────────────────────────────────── const templatesFolder = path.join(__dirname, 'templates'); diff --git a/auth/src/middlewares/user.security.js b/auth/src/middlewares/user.security.js index 8a6c501..c0316ac 100644 --- a/auth/src/middlewares/user.security.js +++ b/auth/src/middlewares/user.security.js @@ -1,31 +1,45 @@ const jwt = require('../tools/jwt'); +const { validateSession } = require('../core/auth.core'); -/** - * Middleware di autenticazione per utenti finali. - * Verifica il JWT dal cookie 'auth_token' o dall'header 'Authorization: Bearer '. - * - * Se valido, inietta req.user con { user_id, username, session_id }. - */ -const userAuth = (req, res, next) => { +const userAuth = async (req, res, next) => { const token = (req.cookies && req.cookies.auth_token) || jwt.getToken(req.headers['authorization']); - if (!token || typeof token !== 'string') { + const redirectToLogin = () => { + 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' }); + }; + + if (!token || typeof token !== 'string') { + return redirectToLogin(); } - // Limite ragionevole sulla lunghezza del token per evitare abusi if (token.length > 2048) { - return res.status(400).json({ error: 'Token non valido' }); + return redirectToLogin(); } const verified = jwt.verifyToken(token); if (!verified.valid) { - return res.status(401).json({ - error: 'Sessione non valida o scaduta', - 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 }); } + try { + await validateSession(verified.payload.session_id); + } catch { + if (req.accepts('html')) { + return res.redirect('/login'); + } + return res.status(401).json({ error: 'Sessione non valida o revocata' }); + } + req.user = verified.payload; next(); }; diff --git a/auth/src/routes/auth.js b/auth/src/routes/auth.js index 786055d..5d37449 100644 --- a/auth/src/routes/auth.js +++ b/auth/src/routes/auth.js @@ -44,27 +44,40 @@ router.post('/register', async (req, res) => { }); router.post('/login', async (req, res) => { - const { username, password, redirect } = req.body; + const { username, password, redirect, _csrf } = req.body; + + const loginRedirect = (errorKey, safeRedirect) => { + const params = new URLSearchParams({ error: errorKey }); + if (safeRedirect) params.set('redirect', safeRedirect); + return res.redirect(`/login?${params.toString()}`); + }; + + // Validazione CSRF (double-submit cookie) + const csrfCookie = req.cookies && req.cookies._csrf; + if (!_csrf || !csrfCookie || _csrf !== csrfCookie) { + return loginRedirect('csrf', ''); + } // Validazione base if (!username || !password || typeof username !== 'string' || typeof password !== 'string') { - return res.render('loginpage', { error: 'Credenziali non valide', redirect: redirect || '' }); + return loginRedirect('invalid_credentials', redirect || ''); } // Limiti di lunghezza per prevenire abuse if (username.length > 50 || password.length > PASSWORD_MAX_LENGTH) { - return res.render('loginpage', { error: 'Credenziali non valide', redirect: redirect || '' }); + return loginRedirect('invalid_credentials', redirect || ''); } // Validazione redirect URL per prevenire open redirect attacks + let safeRedirect = ''; if (redirect && typeof redirect === 'string') { try { const redirectUrl = new URL(redirect); const consoleUrl = new URL(CONSOLE_URL); - // Permetti redirect solo allo stesso dominio del CONSOLE_URL if (redirectUrl.hostname !== consoleUrl.hostname) { - return res.render('loginpage', { error: 'Redirect non autorizzato', redirect: '' }); + return loginRedirect('invalid_redirect', ''); } + safeRedirect = redirect; } catch { // URL relativo o non valido — ignora il redirect } @@ -87,13 +100,13 @@ router.post('/login', async (req, res) => { } res.cookie('auth_token', token, cookieOptions); + res.clearCookie('_csrf'); - const destination = redirect || CONSOLE_URL; + const destination = safeRedirect || CONSOLE_URL; res.redirect(destination); } catch (err) { console.error('[AUTH] Login failed:', err.message); - // Mai rivelare se è l'utente o la password ad essere sbagliati - res.render('loginpage', { error: 'Credenziali non valide', redirect: redirect || '' }); + return loginRedirect('invalid_credentials', safeRedirect); } }); diff --git a/auth/src/routes/views/auth.js b/auth/src/routes/views/auth.js index fdac9de..b2da50a 100644 --- a/auth/src/routes/views/auth.js +++ b/auth/src/routes/views/auth.js @@ -1,8 +1,25 @@ const router = require('express').Router(); +const crypto = require('crypto'); + +const ERROR_MESSAGES = { + invalid_credentials: 'Credenziali non valide', + invalid_redirect: 'Redirect non autorizzato', + csrf: 'Richiesta non valida, riprova' +}; router.get('/login', (req, res) => { const redirect = req.query.redirect || ''; - res.render('loginpage', { error: null, redirect }); + const errorKey = req.query.error; + const error = ERROR_MESSAGES[errorKey] || null; + + const csrfToken = crypto.randomBytes(32).toString('hex'); + res.cookie('_csrf', csrfToken, { + httpOnly: true, + sameSite: 'strict', + secure: process.env.NODE_ENV === 'production' + }); + + res.render('loginpage', { error, redirect, csrf_token: csrfToken }); }); -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/auth/src/routes/views/sessions.js b/auth/src/routes/views/sessions.js index 4cc4edd..9793420 100644 --- a/auth/src/routes/views/sessions.js +++ b/auth/src/routes/views/sessions.js @@ -4,7 +4,7 @@ const userAuth = require('../../middlewares/user.security'); router.use(userAuth); router.get('/', (req, res) => { - res.render('sessions'); + res.render('sessions', { user: req.user }); }); module.exports = router; \ No newline at end of file diff --git a/auth/src/routes/views/user.js b/auth/src/routes/views/user.js index 01d0063..19dd9a8 100644 --- a/auth/src/routes/views/user.js +++ b/auth/src/routes/views/user.js @@ -1,7 +1,10 @@ const router = require('express').Router(); +const userAuth = require('../../middlewares/user.security'); + +router.use(userAuth); router.get('/', (req, res) => { - res.render('user'); + res.render('user', { user: req.user }); }); -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/auth/src/static/style/style.css b/auth/src/static/style/style.css index 8b7defd..3c8b0e5 100644 --- a/auth/src/static/style/style.css +++ b/auth/src/static/style/style.css @@ -15,7 +15,8 @@ --header-border: #e2e8f0; --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); - --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); + --shadow-md: + 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); --radius-md: 8px; --radius-lg: 12px; } @@ -26,21 +27,21 @@ } @font-face { - font-family: 'Normal'; - src: url('../font/Quicksand-VariableFont_wght.ttf'); + font-family: "Normal"; + src: url("../font/sans-flex.ttf"); font-weight: 400; font-style: normal; } @font-face { - font-family: 'Bold'; - src: url('../font/Quicksand-VariableFont_wght.ttf'); + font-family: "Bold"; + src: url("../font/sans-flex.ttf"); font-weight: 700; font-style: normal; } body { - font-family: 'Normal', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + font-family: "Normal", Arial; color: var(--text-primary); -webkit-font-smoothing: antialiased; } @@ -53,7 +54,7 @@ button { color: var(--text-primary); font-size: 14px; font-weight: 600; - font-family: 'Bold', inherit; + font-family: "Bold", inherit; cursor: pointer; transition: all 0.5s cubic-bezier(0.8, 0, 0.2, 1); white-space: nowrap; @@ -79,13 +80,11 @@ button.prominent:hover { box-shadow: var(--shadow-md); } - button.prominent:active { transform: translateY(1px); box-shadow: var(--shadow-sm); } - /* INFO PANEL */ .info-panel { @@ -111,9 +110,6 @@ button.prominent:active { transition: transform 0.12s ease; } - - - /* GRID & CARD ITEMS */ .grid { @@ -132,7 +128,9 @@ button.prominent:active { border-radius: 20px; text-decoration: none; color: var(--text-primary); - transition: transform 0.12s ease, box-shadow 0.12s ease; + transition: + transform 0.12s ease, + box-shadow 0.12s ease; } .card h3 { @@ -158,10 +156,6 @@ button.prominent:active { grid-column: 1 / -1; } - - - - /* HEADER */ .header { @@ -179,7 +173,6 @@ button.prominent:active { user-select: none; } - .header h1 { color: var(--text-primary); font-size: 1.25rem; @@ -198,4 +191,4 @@ button.prominent:active { font-weight: 500; color: var(--text-secondary); padding-inline: 5px; -} \ No newline at end of file +} diff --git a/auth/src/templates/loginpage.html b/auth/src/templates/loginpage.html index 460bc84..2fad865 100644 --- a/auth/src/templates/loginpage.html +++ b/auth/src/templates/loginpage.html @@ -1,18 +1,19 @@ - - + - - - + + + Login — Console MEB + + +
- \ No newline at end of file + diff --git a/auth/src/templates/sessions.html b/auth/src/templates/sessions.html index 8b13789..e3c77dc 100644 --- a/auth/src/templates/sessions.html +++ b/auth/src/templates/sessions.html @@ -1 +1,109 @@ + + + + + + Sessioni — Console MEB + + + + + +
+

Console MEB

+
+

{{ user.username }}

+
+ +
+
+
+ +
+

Sessioni attive

+
+

Caricamento...

+
+
+ + + + + diff --git a/auth/src/templates/user.html b/auth/src/templates/user.html index 8b13789..39247bf 100644 --- a/auth/src/templates/user.html +++ b/auth/src/templates/user.html @@ -1 +1,87 @@ + + + + + + Profilo — Console MEB + + + + + +
+

Console MEB

+
+

{{ user.username }}

+
+ +
+
+
+ +
+

Profilo utente

+
+

Caricamento...

+
+
+ + + + + diff --git a/auth/src/tools/jwt.js b/auth/src/tools/jwt.js index a87fadb..4b7f994 100644 --- a/auth/src/tools/jwt.js +++ b/auth/src/tools/jwt.js @@ -63,8 +63,7 @@ function getToken(header) { return parts[1]; } - //TODO: valutare se modificare in return null per accettare solo metodo Bearer Authorization Token, - return header; + return null; } module.exports = { generateToken, verifyToken, getToken }; \ No newline at end of file diff --git a/auth/src/tools/security.js b/auth/src/tools/security.js index 13d4c3b..ff53d43 100644 --- a/auth/src/tools/security.js +++ b/auth/src/tools/security.js @@ -8,8 +8,8 @@ const saltRounds = 12; * @param {string} password - Password da hashare * @returns {string} - Hash della password */ -function hashPassword(password) { - return bcrypt.hashSync(password, saltRounds); +async function hashPassword(password) { + return bcrypt.hash(password, saltRounds); } /** @@ -18,8 +18,8 @@ function hashPassword(password) { * @param {string} hash - Hash della password * @returns {boolean} - True se la password è corretta, false altrimenti */ -function verifyPassword(password, hash) { - return bcrypt.compareSync(password, hash); +async function verifyPassword(password, hash) { + return bcrypt.compare(password, hash); } /** diff --git a/docker-compose.yml b/docker-compose.yml index b846e98..4006b75 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -23,7 +23,7 @@ services: - "3006:3006" labels: - "traefik.enable=true" - - "traefik.http.routers.auth.rule=Host(`auth.mebboat.it`)" + - "traefik.http.routers.auth.rule=Host(`auth.${DOMAIN:-mebboat.it}`)" - "traefik.http.routers.auth.entrypoints=websecure" - "traefik.http.services.auth.loadbalancer.server.port=3006" - "traefik.docker.network=meb-public" @@ -46,7 +46,7 @@ services: - meb-private labels: - "traefik.enable=true" - - "traefik.http.routers.api.rule=Host(`api.mebboat.it`)" + - "traefik.http.routers.api.rule=Host(`api.${DOMAIN:-mebboat.it}`)" - "traefik.http.routers.api.entrypoints=websecure" - "traefik.http.services.api.loadbalancer.server.port=3003" - "traefik.http.routers.api.tls.certresolver=letsencrypt" @@ -68,7 +68,7 @@ services: - meb-private labels: - "traefik.enable=true" - - "traefik.http.routers.console.rule=Host(`console.mebboat.it`)" + - "traefik.http.routers.console.rule=Host(`console.${DOMAIN:-mebboat.it}`)" - "traefik.http.routers.console.entrypoints=websecure" - "traefik.http.services.console.loadbalancer.server.port=3004" - "traefik.http.routers.console.tls.certresolver=letsencrypt" @@ -90,7 +90,7 @@ services: - meb-public labels: - "traefik.enable=true" - - "traefik.http.routers.realtime.rule=Host(`realtime.mebboat.it`)" + - "traefik.http.routers.realtime.rule=Host(`realtime.${DOMAIN:-mebboat.it}`)" - "traefik.http.routers.realtime.entrypoints=websecure" - "traefik.http.services.realtime.loadbalancer.server.port=3000" - "traefik.http.routers.realtime.tls.certresolver=letsencrypt" @@ -115,7 +115,7 @@ services: - meb-public labels: - "traefik.enable=true" - - "traefik.http.routers.ml.rule=Host(`ml.mebboat.it`)" + - "traefik.http.routers.ml.rule=Host(`ml.${DOMAIN:-mebboat.it}`)" - "traefik.http.routers.ml.entrypoints=websecure" - "traefik.http.services.ml.loadbalancer.server.port=8000" - "traefik.http.routers.ml.tls.certresolver=letsencrypt" @@ -139,7 +139,7 @@ services: # - meb-internal # labels: # - "traefik.enable=true" -# - "traefik.http.routers.marine.rule=Host(`api.${URL_DOMAIN}`) && PathPrefix(`/marine`)" +# - "traefik.http.routers.marine.rule=Host(`api.${DOMAIN:-mebboat.it}`) && PathPrefix(`/marine`)" # - "traefik.http.routers.marine.entrypoints=web" # - "traefik.http.services.marine.loadbalancer.server.port=8001" # - "traefik.docker.network=meb-proxy-net" @@ -155,8 +155,8 @@ services: # environment: # - DATABASE_URL=postgresql://meb:meb@meb-postgres:5432/circuits # - AUTH_SERVICE_URL=http://auth:3001 -# - AUTH_URL=http://auth.${URL_DOMAIN:-localhost} -# - API_URL=http://api.${URL_DOMAIN:-localhost} +# - AUTH_URL=http://auth.${DOMAIN:-localhost} +# - API_URL=http://api.${DOMAIN:-localhost} # - NODE_ENV=${NODE_ENV:-development} # volumes: # - ./circuits/src:/app/src @@ -173,7 +173,7 @@ services: # - meb-internal # labels: # - "traefik.enable=true" -# - "traefik.http.routers.circuits.rule=Host(`circuits.${URL_DOMAIN}`)" +# - "traefik.http.routers.circuits.rule=Host(`circuits.${DOMAIN:-mebboat.it}`)" # - "traefik.http.routers.circuits.entrypoints=web" # - "traefik.http.services.circuits.loadbalancer.server.port=3005" # - "traefik.docker.network=meb-proxy-net"