csrf token handling, two jwt tokens

This commit is contained in:
jackiettran
2025-11-26 14:25:49 -05:00
parent f3a356d64b
commit 8b10103ae4
8 changed files with 114 additions and 76 deletions

View File

@@ -14,7 +14,7 @@ const authenticateToken = async (req, res, next) => {
} }
try { try {
const decoded = jwt.verify(token, process.env.JWT_SECRET); const decoded = jwt.verify(token, process.env.JWT_ACCESS_SECRET);
const userId = decoded.id; const userId = decoded.id;
if (!userId) { if (!userId) {
@@ -78,7 +78,7 @@ const optionalAuth = async (req, res, next) => {
} }
try { try {
const decoded = jwt.verify(token, process.env.JWT_SECRET); const decoded = jwt.verify(token, process.env.JWT_ACCESS_SECRET);
const userId = decoded.id; const userId = decoded.id;
if (!userId) { if (!userId) {

View File

@@ -72,7 +72,8 @@ const getCSRFToken = (req, res) => {
maxAge: 60 * 60 * 1000, maxAge: 60 * 60 * 1000,
}); });
res.json({ csrfToken: token }); res.set("X-CSRF-Token", token);
res.status(204).send();
}; };
module.exports = { module.exports = {

View File

@@ -4067,22 +4067,43 @@
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="
}, },
"node_modules/body-parser": { "node_modules/body-parser": {
"version": "2.2.0", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz",
"integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==",
"license": "MIT",
"dependencies": { "dependencies": {
"bytes": "^3.1.2", "bytes": "^3.1.2",
"content-type": "^1.0.5", "content-type": "^1.0.5",
"debug": "^4.4.0", "debug": "^4.4.3",
"http-errors": "^2.0.0", "http-errors": "^2.0.0",
"iconv-lite": "^0.6.3", "iconv-lite": "^0.7.0",
"on-finished": "^2.4.1", "on-finished": "^2.4.1",
"qs": "^6.14.0", "qs": "^6.14.0",
"raw-body": "^3.0.0", "raw-body": "^3.0.1",
"type-is": "^2.0.0" "type-is": "^2.0.1"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/body-parser/node_modules/iconv-lite": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz",
"integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
} }
}, },
"node_modules/bowser": { "node_modules/bowser": {
@@ -4181,6 +4202,7 @@
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"license": "MIT",
"engines": { "engines": {
"node": ">= 0.8" "node": ">= 0.8"
} }
@@ -4745,9 +4767,10 @@
} }
}, },
"node_modules/debug": { "node_modules/debug": {
"version": "4.4.1", "version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"license": "MIT",
"dependencies": { "dependencies": {
"ms": "^2.1.3" "ms": "^2.1.3"
}, },
@@ -5359,27 +5382,18 @@
} }
}, },
"node_modules/express-validator": { "node_modules/express-validator": {
"version": "7.2.1", "version": "7.3.1",
"resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.2.1.tgz", "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.3.1.tgz",
"integrity": "sha512-CjNE6aakfpuwGaHQZ3m8ltCG2Qvivd7RHtVMS/6nVxOM7xVGqr4bhflsm4+N5FP5zI7Zxp+Hae+9RE+o8e3ZOQ==", "integrity": "sha512-IGenaSf+DnWc69lKuqlRE9/i/2t5/16VpH5bXoqdxWz1aCpRvEdrBuu1y95i/iL5QP8ZYVATiwLFhwk3EDl5vg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"lodash": "^4.17.21", "lodash": "^4.17.21",
"validator": "~13.12.0" "validator": "~13.15.23"
}, },
"engines": { "engines": {
"node": ">= 8.0.0" "node": ">= 8.0.0"
} }
}, },
"node_modules/express-validator/node_modules/validator": {
"version": "13.12.0",
"resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz",
"integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==",
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/extend": { "node_modules/extend": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
@@ -5789,9 +5803,10 @@
} }
}, },
"node_modules/glob": { "node_modules/glob": {
"version": "10.4.5", "version": "10.5.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
"license": "ISC",
"dependencies": { "dependencies": {
"foreground-child": "^3.1.0", "foreground-child": "^3.1.0",
"jackspeak": "^3.1.2", "jackspeak": "^3.1.2",
@@ -6006,26 +6021,23 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/http-errors": { "node_modules/http-errors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
"dependencies": {
"depd": "2.0.0",
"inherits": "2.0.4",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"toidentifier": "1.0.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/http-errors/node_modules/statuses": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
"license": "MIT",
"dependencies": {
"depd": "~2.0.0",
"inherits": "~2.0.4",
"setprototypeof": "~1.2.0",
"statuses": "~2.0.2",
"toidentifier": "~1.0.1"
},
"engines": { "engines": {
"node": ">= 0.8" "node": ">= 0.8"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
} }
}, },
"node_modules/http-proxy-agent": { "node_modules/http-proxy-agent": {
@@ -7145,9 +7157,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/js-yaml": { "node_modules/js-yaml": {
"version": "3.14.1", "version": "3.14.2",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -8454,17 +8466,34 @@
} }
}, },
"node_modules/raw-body": { "node_modules/raw-body": {
"version": "3.0.0", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
"integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
"license": "MIT",
"dependencies": { "dependencies": {
"bytes": "3.1.2", "bytes": "~3.1.2",
"http-errors": "2.0.0", "http-errors": "~2.0.1",
"iconv-lite": "0.6.3", "iconv-lite": "~0.7.0",
"unpipe": "1.0.0" "unpipe": "~1.0.0"
}, },
"engines": { "engines": {
"node": ">= 0.8" "node": ">= 0.10"
}
},
"node_modules/raw-body/node_modules/iconv-lite": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz",
"integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
} }
}, },
"node_modules/react-is": { "node_modules/react-is": {
@@ -9812,6 +9841,7 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
"license": "MIT",
"engines": { "engines": {
"node": ">= 0.8" "node": ">= 0.8"
} }
@@ -9917,9 +9947,10 @@
} }
}, },
"node_modules/validator": { "node_modules/validator": {
"version": "13.15.15", "version": "13.15.23",
"resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz", "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.23.tgz",
"integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==", "integrity": "sha512-4yoz1kEWqUjzi5zsPbAS/903QXSYp0UOtHsPpp7p9rHAw/W+dkInskAE386Fat3oKRROwO98d9ZB0G4cObgUyw==",
"license": "MIT",
"engines": { "engines": {
"node": ">= 0.10" "node": ">= 0.10"
} }

View File

@@ -128,13 +128,13 @@ router.post(
const token = jwt.sign( const token = jwt.sign(
{ id: user.id, jwtVersion: user.jwtVersion }, { id: user.id, jwtVersion: user.jwtVersion },
process.env.JWT_SECRET, process.env.JWT_ACCESS_SECRET,
{ expiresIn: "15m" } // Short-lived access token { expiresIn: "15m" } // Short-lived access token
); );
const refreshToken = jwt.sign( const refreshToken = jwt.sign(
{ id: user.id, jwtVersion: user.jwtVersion, type: "refresh" }, { id: user.id, jwtVersion: user.jwtVersion, type: "refresh" },
process.env.JWT_SECRET, process.env.JWT_REFRESH_SECRET,
{ expiresIn: "7d" } { expiresIn: "7d" }
); );
@@ -223,13 +223,13 @@ router.post(
const token = jwt.sign( const token = jwt.sign(
{ id: user.id, jwtVersion: user.jwtVersion }, { id: user.id, jwtVersion: user.jwtVersion },
process.env.JWT_SECRET, process.env.JWT_ACCESS_SECRET,
{ expiresIn: "15m" } // Short-lived access token { expiresIn: "15m" } // Short-lived access token
); );
const refreshToken = jwt.sign( const refreshToken = jwt.sign(
{ id: user.id, jwtVersion: user.jwtVersion, type: "refresh" }, { id: user.id, jwtVersion: user.jwtVersion, type: "refresh" },
process.env.JWT_SECRET, process.env.JWT_REFRESH_SECRET,
{ expiresIn: "7d" } { expiresIn: "7d" }
); );
@@ -392,13 +392,13 @@ router.post(
// Generate JWT tokens // Generate JWT tokens
const token = jwt.sign( const token = jwt.sign(
{ id: user.id, jwtVersion: user.jwtVersion }, { id: user.id, jwtVersion: user.jwtVersion },
process.env.JWT_SECRET, process.env.JWT_ACCESS_SECRET,
{ expiresIn: "15m" } { expiresIn: "15m" }
); );
const refreshToken = jwt.sign( const refreshToken = jwt.sign(
{ id: user.id, jwtVersion: user.jwtVersion, type: "refresh" }, { id: user.id, jwtVersion: user.jwtVersion, type: "refresh" },
process.env.JWT_SECRET, process.env.JWT_REFRESH_SECRET,
{ expiresIn: "7d" } { expiresIn: "7d" }
); );
@@ -550,7 +550,7 @@ router.post(
}); });
} }
const decoded = jwt.verify(accessToken, process.env.JWT_SECRET); const decoded = jwt.verify(accessToken, process.env.JWT_ACCESS_SECRET);
const user = await User.findByPk(decoded.id); const user = await User.findByPk(decoded.id);
if (!user) { if (!user) {
@@ -625,7 +625,7 @@ router.post("/refresh", async (req, res) => {
} }
// Verify refresh token // Verify refresh token
const decoded = jwt.verify(refreshToken, process.env.JWT_SECRET); const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
if (!decoded.id || decoded.type !== "refresh") { if (!decoded.id || decoded.type !== "refresh") {
return res.status(401).json({ error: "Invalid refresh token" }); return res.status(401).json({ error: "Invalid refresh token" });
@@ -648,7 +648,7 @@ router.post("/refresh", async (req, res) => {
// Generate new access token // Generate new access token
const newAccessToken = jwt.sign( const newAccessToken = jwt.sign(
{ id: user.id, jwtVersion: user.jwtVersion }, { id: user.id, jwtVersion: user.jwtVersion },
process.env.JWT_SECRET, process.env.JWT_ACCESS_SECRET,
{ expiresIn: "15m" } { expiresIn: "15m" }
); );

View File

@@ -392,7 +392,8 @@ router.get('/images/:filename',
helmet.crossOriginResourcePolicy({ policy: "cross-origin" }), helmet.crossOriginResourcePolicy({ policy: "cross-origin" }),
async (req, res) => { async (req, res) => {
try { try {
const { filename } = req.params; // Sanitize filename to prevent path traversal attacks
const filename = path.basename(req.params.filename);
// Verify user is sender or receiver of a message with this image // Verify user is sender or receiver of a message with this image
const message = await Message.findOne({ const message = await Message.findOne({

View File

@@ -108,6 +108,7 @@ app.use(
origin: process.env.FRONTEND_URL || "http://localhost:3000", origin: process.env.FRONTEND_URL || "http://localhost:3000",
credentials: true, credentials: true,
optionsSuccessStatus: 200, optionsSuccessStatus: 200,
exposedHeaders: ["X-CSRF-Token"],
}) })
); );

View File

@@ -38,8 +38,8 @@ const authenticateSocket = async (socket, next) => {
return next(new Error("Authentication required")); return next(new Error("Authentication required"));
} }
// Verify JWT // Verify JWT (access tokens only)
const decoded = jwt.verify(token, process.env.JWT_SECRET); const decoded = jwt.verify(token, process.env.JWT_ACCESS_SECRET);
const userId = decoded.id; const userId = decoded.id;
if (!userId) { if (!userId) {

View File

@@ -39,7 +39,7 @@ const api = axios.create({
export const fetchCSRFToken = async (): Promise<string> => { export const fetchCSRFToken = async (): Promise<string> => {
try { try {
const response = await api.get("/auth/csrf-token"); const response = await api.get("/auth/csrf-token");
csrfToken = response.data.csrfToken || ""; csrfToken = response.headers["x-csrf-token"] || "";
return csrfToken || ""; return csrfToken || "";
} catch (error) { } catch (error) {
console.error("Failed to fetch CSRF token:", error); console.error("Failed to fetch CSRF token:", error);
@@ -286,10 +286,14 @@ export const forumAPI = {
deleteComment: (commentId: string) => deleteComment: (commentId: string) =>
api.delete(`/forum/comments/${commentId}`), api.delete(`/forum/comments/${commentId}`),
// Admin endpoints // Admin endpoints
adminDeletePost: (id: string, reason: string) => api.delete(`/forum/admin/posts/${id}`, { data: { reason } }), adminDeletePost: (id: string, reason: string) =>
adminRestorePost: (id: string) => api.patch(`/forum/admin/posts/${id}/restore`), api.delete(`/forum/admin/posts/${id}`, { data: { reason } }),
adminDeleteComment: (id: string, reason: string) => api.delete(`/forum/admin/comments/${id}`, { data: { reason } }), adminRestorePost: (id: string) =>
adminRestoreComment: (id: string) => api.patch(`/forum/admin/comments/${id}/restore`), api.patch(`/forum/admin/posts/${id}/restore`),
adminDeleteComment: (id: string, reason: string) =>
api.delete(`/forum/admin/comments/${id}`, { data: { reason } }),
adminRestoreComment: (id: string) =>
api.patch(`/forum/admin/comments/${id}/restore`),
adminClosePost: (id: string) => api.patch(`/forum/admin/posts/${id}/close`), adminClosePost: (id: string) => api.patch(`/forum/admin/posts/${id}/close`),
adminReopenPost: (id: string) => api.patch(`/forum/admin/posts/${id}/reopen`), adminReopenPost: (id: string) => api.patch(`/forum/admin/posts/${id}/reopen`),
}; };