This commit is contained in:
Satanas1275 2026-07-05 12:48:30 +02:00 committed by GitHub
commit 4939a9eb5d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 127 additions and 22 deletions

View file

@ -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);

View file

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

View file

@ -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",

View file

@ -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",

View file

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

View file

@ -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') {