From 487a6e1481c3cb0532ae934ae427ba9620849fbc Mon Sep 17 00:00:00 2001 From: Satanas1275 <107134873+Satanas1275@users.noreply.github.com> Date: Thu, 2 Jul 2026 00:13:41 +0200 Subject: [PATCH] fix: remove OAuth tokens from localStorage, add HttpOnly cookie auth - Stop storing discord_token and google_token in localStorage (XSS risk) These tokens are never read, only written; backend already stores them server-side in user data files. - Remove discord_token and google_token from AUTH_KEYS array - Add cookie-parser middleware to Express app - Add setAuthCookie() helper to set JWT as HttpOnly; Secure; SameSite=Lax cookie - Update auth routes (BIP39 register/login, Discord, Google) to set cookie - Update getAuthIfValid() to read JWT from cookie first, then Authorization header - Add extractJwtFromRequest() helper for dual-mode auth (cookie + header) - Add GET /api/auth/session endpoint to rehydrate JWT from cookie on page reload --- API/Mainapi/app.js | 4 +++ API/Mainapi/middleware/auth.js | 41 +++++++++++++++++++--- API/Mainapi/package-lock.json | 35 ++++++++++++------- API/Mainapi/package.json | 1 + API/Mainapi/routes/authRoutes.js | 60 +++++++++++++++++++++++++++++++- src/utils/accountAuth.ts | 8 ++--- 6 files changed, 127 insertions(+), 22 deletions(-) diff --git a/API/Mainapi/app.js b/API/Mainapi/app.js index e8f8df8..f1210d6 100644 --- a/API/Mainapi/app.js +++ b/API/Mainapi/app.js @@ -8,6 +8,7 @@ const express = require("express"); const http = require("http"); const https = require("https"); const compression = require("compression"); +const cookieParser = require("cookie-parser"); // Middleware modules const corsMiddleware = require("./middleware/cors"); @@ -255,6 +256,9 @@ app.use(domainRestriction); // 6. Body parsing app.use(express.json({ limit: "30mb" })); // Reduced from 1000mb to prevent abuse +// 6b. Cookie parsing (for HttpOnly JWT auth cookie) +app.use(cookieParser()); + // 7. JSON parse error handler (must come right after json parser) app.use(jsonParseErrorHandler); diff --git a/API/Mainapi/middleware/auth.js b/API/Mainapi/middleware/auth.js index 0220fc6..cb5fae4 100644 --- a/API/Mainapi/middleware/auth.js +++ b/API/Mainapi/middleware/auth.js @@ -125,10 +125,9 @@ function purgeSessionRecord(sessionId, userId, userType) { async function getAuthIfValid(req) { try { - const authHeader = req.headers['authorization'] || req.headers['Authorization']; - if (!authHeader || !authHeader.toLowerCase().startsWith('bearer ')) return null; - const token = authHeader.split(' ')[1]; - const payload = jwt.verify(token, JWT_SECRET, { algorithms: ['HS256'] }); + const token = extractJwtFromRequest(req); + if (!token) return null; + const payload = verifyJwt(token); const { userType, sub: userId, sessionId } = payload; const authMethod = AUTH_METHODS.includes(payload?.authMethod) ? payload.authMethod @@ -301,11 +300,43 @@ async function isUploaderOrAdmin(req, res, next) { } } +/** + * Set JWT as an HttpOnly cookie for automatic transmission on same-origin requests. + * Cookie options: HttpOnly, Secure, SameSite=Lax, Path=/api, 7-day TTL. + */ +function setAuthCookie(res, token) { + if (!res || !token) return; + res.cookie('auth_token', token, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + path: '/api', + maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days + }); +} + +/** + * Extract JWT from request: check cookies first, then Authorization header. + */ +function extractJwtFromRequest(req) { + if (req.cookies && req.cookies.auth_token) { + return req.cookies.auth_token; + } + const authHeader = req.headers['authorization'] || req.headers['Authorization']; + if (authHeader && authHeader.toLowerCase().startsWith('bearer ')) { + return authHeader.split(' ')[1]; + } + return null; +} + module.exports = { JWT_SECRET, issueJwt, + verifyJwt, isAdmin, isUploaderOrAdmin, getAuthIfValid, - updateSessionAccess + updateSessionAccess, + setAuthCookie, + extractJwtFromRequest, }; diff --git a/API/Mainapi/package-lock.json b/API/Mainapi/package-lock.json index fe2316e..9ed4e40 100644 --- a/API/Mainapi/package-lock.json +++ b/API/Mainapi/package-lock.json @@ -22,6 +22,7 @@ "chardet": "^2.0.0", "cheerio": "^1.1.2", "compression": "^1.8.1", + "cookie-parser": "^1.4.7", "cors": "^2.8.5", "crypto-js": "^4.2.0", "cycletls": "^1.0.27", @@ -1551,14 +1552,33 @@ } }, "node_modules/cookie": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "license": "MIT", "engines": { "node": ">= 0.6" } }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, "node_modules/cookie-signature": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", @@ -2018,15 +2038,6 @@ "node": ">=10.0.0" } }, - "node_modules/engine.io/node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", diff --git a/API/Mainapi/package.json b/API/Mainapi/package.json index 317c986..a7e39d0 100644 --- a/API/Mainapi/package.json +++ b/API/Mainapi/package.json @@ -24,6 +24,7 @@ "chardet": "^2.0.0", "cheerio": "^1.1.2", "compression": "^1.8.1", + "cookie-parser": "^1.4.7", "cors": "^2.8.5", "crypto-js": "^4.2.0", "cycletls": "^1.0.27", diff --git a/API/Mainapi/routes/authRoutes.js b/API/Mainapi/routes/authRoutes.js index 5b1fa9b..52a40d8 100644 --- a/API/Mainapi/routes/authRoutes.js +++ b/API/Mainapi/routes/authRoutes.js @@ -13,7 +13,7 @@ const { v4: uuidv4 } = require('uuid'); const rateLimit = require('express-rate-limit'); const { ipKeyGenerator } = require('express-rate-limit'); -const { issueJwt, getAuthIfValid } = require('../middleware/auth'); +const { issueJwt, verifyJwt, getAuthIfValid, setAuthCookie, extractJwtFromRequest } = require('../middleware/auth'); const { getPool } = require('../mysqlPool'); const { createRedisRateLimitStore } = require('../utils/redisRateLimitStore'); const { verifyTurnstileFromRequest } = require('../utils/turnstile'); @@ -484,6 +484,7 @@ router.post('/bip39/create', authRateLimit, async (req, res) => { }, }); + setAuthCookie(res, payload.token); return res.status(200).json({ ...payload, userId: payload.account.userId, @@ -526,6 +527,7 @@ router.post('/bip39/login', authRateLimit, async (req, res) => { }, }); + setAuthCookie(res, payload.token); return res.status(200).json({ ...payload, userId: payload.account.userId, @@ -555,6 +557,7 @@ router.post('/discord/verify', authRateLimit, async (req, res) => { externalUser: user, }); + setAuthCookie(res, payload.token); return res.status(200).json(payload); } catch (error) { console.error('Discord verify error:', error.response?.status || error.message); @@ -579,6 +582,7 @@ router.post('/google/verify', authRateLimit, async (req, res) => { externalUser: user, }); + setAuthCookie(res, payload.token); return res.status(200).json(payload); } catch (error) { console.error('Google verify error:', error.response?.status || error.message); @@ -727,4 +731,58 @@ router.delete('/links/:provider', async (req, res) => { } }); +/** + * GET /api/auth/session + * Retourne les infos de session si le cookie JWT est valide. + * Permet au frontend de réhydrater le token en mémoire après un reload. + */ +router.get('/session', async (req, res) => { + try { + const token = extractJwtFromRequest(req); + if (!token) { + return res.status(401).json({ success: false, error: 'Non authentifié' }); + } + + let payload; + try { + payload = verifyJwt(token); + } catch { + return res.status(401).json({ success: false, error: 'Session expirée' }); + } + + const { userType, sub: userId, sessionId, authMethod } = payload; + if (!['oauth', 'bip39'].includes(userType) || !userId || !sessionId) { + return res.status(401).json({ success: false, error: 'Session invalide' }); + } + + // Vérifier en MySQL + const pool = getPool(); + if (!pool) { + return res.status(503).json({ success: false, error: 'Service indisponible' }); + } + + const [rows] = await pool.execute( + 'SELECT id FROM user_sessions WHERE id = ? AND user_id = ? AND user_type = ?', + [sessionId, userId, userType] + ); + + if (rows.length === 0) { + return res.status(401).json({ success: false, error: 'Session expirée' }); + } + + // Retourner un nouveau token et les infos de session + const newToken = issueJwt(userType, userId, sessionId, authMethod); + setAuthCookie(res, newToken); + + return res.json({ + success: true, + token: newToken, + session: { userType, userId, sessionId, authMethod }, + }); + } catch (error) { + console.error('[AUTH] Session error:', error); + return res.status(500).json({ success: false, error: 'Erreur interne' }); + } +}); + module.exports = router; diff --git a/src/utils/accountAuth.ts b/src/utils/accountAuth.ts index dd3f85a..a11ef1a 100644 --- a/src/utils/accountAuth.ts +++ b/src/utils/accountAuth.ts @@ -83,10 +83,8 @@ const AUTH_KEYS = [ 'session_id', 'discord_auth', 'discord_user', - 'discord_token', 'google_auth', 'google_user', - 'google_token', 'bip39_auth', 'auth_method', 'resolved_user_type', @@ -426,7 +424,8 @@ export function persistResolvedSession( isAdmin: Boolean(rawUser?.isAdmin), linked: Boolean(payload.account?.linked), })); - if (options.accessToken) localStorage.setItem('discord_token', options.accessToken); + // Ne pas stocker le token OAuth en localStorage (risque XSS) + // Le backend stocke déjà ce token côté serveur } if (method === 'google') { @@ -440,7 +439,8 @@ export function persistResolvedSession( picture: rawUser?.picture || DEFAULT_AVATAR, linked: Boolean(payload.account?.linked), })); - if (options.accessToken) localStorage.setItem('google_token', options.accessToken); + // Ne pas stocker le token OAuth en localStorage (risque XSS) + // Le backend stocke déjà ce token côté serveur } if (method === 'bip39') {