mirror of
https://github.com/movixcorp/MovixOpenSource.git
synced 2026-07-26 20:42:08 +00:00
Merge 487a6e1481 into 56db6b3749
This commit is contained in:
commit
4939a9eb5d
6 changed files with 127 additions and 22 deletions
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
35
API/Mainapi/package-lock.json
generated
35
API/Mainapi/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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') {
|
||||
|
|
|
|||
Loading…
Reference in a new issue