redo ant csrf token

ensure anti csrf token and session is only created after login
This commit is contained in:
BIG2EYEZ 2024-01-13 01:17:07 +08:00
parent f2a9facfaf
commit 183e73eca2
7 changed files with 96 additions and 47 deletions

View File

@ -19,8 +19,10 @@ const PORT = process.env.PORT || 3000;
require("dotenv").config(); require("dotenv").config();
app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.urlencoded({ extended: true }));
app.use(
session({ app.set("view engine", "ejs");
app.use(session({
secret: process.env.key, secret: process.env.key,
resave: false, resave: false,
saveUninitialized: true, saveUninitialized: true,
@ -29,22 +31,7 @@ app.use(
httpOnly: true, httpOnly: true,
maxAge: 24 * 60 * 60 * 1000, // Session duration in milliseconds (here set to 1 day) maxAge: 24 * 60 * 60 * 1000, // Session duration in milliseconds (here set to 1 day)
}, },
}) }));
);
app.use((req, res, next) => {
if (!req.session.csrfToken) {
req.session.csrfToken = crypto.randomBytes(32).toString('hex');
}
// Make the CSRF token available in the response context
res.locals.csrfToken = req.session.csrfToken;
console.log(`Server-side CSRF Token: ${req.session.csrfToken}`);
next();
});
app.set("view engine", "ejs");
function isAuthenticated(req, res, next) { function isAuthenticated(req, res, next) {
if (req.session && req.session.authenticated) { if (req.session && req.session.authenticated) {
return next(); return next();
@ -144,17 +131,11 @@ const logActivity = async (username, success, message) => {
app.post('/login', [ app.post('/login', [
body('username').escape().trim().isLength({ min: 1 }).withMessage('Username must not be empty'), body('username').escape().trim().isLength({ min: 1 }).withMessage('Username must not be empty'),
body('password').escape().trim().isLength({ min: 1 }).withMessage('Password must not be empty'), body('password').escape().trim().isLength({ min: 1 }).withMessage('Password must not be empty'),
body('csrf_token').escape().trim().isLength({ min: 1 }).withMessage('CSRF token must not be empty'),
], ],
async (req, res) => { async (req, res) => {
try { try {
const errors = validationResult(req); const errors = validationResult(req);
// Validate CSRF token
if (req.body.csrf_token !== req.session.csrfToken) {
return res.status(403).send("Invalid CSRF token");
}
if (!errors.isEmpty()) { if (!errors.isEmpty()) {
// Handle validation errors, e.g., return an error message to the client // Handle validation errors, e.g., return an error message to the client
return res.render('login', { error: 'Invalid input. Please check your credentials.', csrfToken: req.session.csrfToken }); return res.render('login', { error: 'Invalid input. Please check your credentials.', csrfToken: req.session.csrfToken });
@ -222,17 +203,11 @@ async (req, res) => {
// OTP verification route // OTP verification route
app.post("/verify-otp", [ app.post("/verify-otp", [
body('otp').escape().trim().isLength({ min: 1 }).withMessage('OTP must not be empty'), body('otp').escape().trim().isLength({ min: 1 }).withMessage('OTP must not be empty'),
body('csrf_token').escape().trim().isLength({ min: 1 }).withMessage('CSRF token must not be empty'),
], ],
async (req, res) => { async (req, res) => {
try { try {
const errors = validationResult(req); const errors = validationResult(req);
// Validate CSRF token
if (req.body.csrf_token !== req.session.csrfToken) {
return res.status(403).send("Invalid CSRF token");
}
if (!errors.isEmpty()) { if (!errors.isEmpty()) {
// Handle validation errors, e.g., return an error message to the client // Handle validation errors, e.g., return an error message to the client
return res.render('otp', { error: 'Invalid OTP. Please try again.', username: req.body.username, csrfToken: req.session.csrfToken }); return res.render('otp', { error: 'Invalid OTP. Please try again.', username: req.body.username, csrfToken: req.session.csrfToken });
@ -240,10 +215,16 @@ async (req, res) => {
const enteredOTP = req.body.otp; const enteredOTP = req.body.otp;
if (!req.session) {
// If session is not defined, handle accordingly
console.error("Session is not defined.");
return res.status(500).send("Internal Server Error");
}
if (enteredOTP === req.session.otp) { if (enteredOTP === req.session.otp) {
// Log successful OTP entry and login // Log successful OTP entry
if (req.body.username) { if (req.body.username) {
await logActivity(req.body.username, true, "OTP entered correctly. Successful login"); await logActivity(req.body.username, true, "OTP entered correctly");
} }
// Correct OTP, generate a session token // Correct OTP, generate a session token
@ -253,8 +234,22 @@ async (req, res) => {
req.session.authenticated = true; req.session.authenticated = true;
req.session.username = req.body.username; req.session.username = req.body.username;
req.session.sessionToken = sessionToken; req.session.sessionToken = sessionToken;
// Generate and store anti-CSRF token in the session
req.session.csrfToken = crypto.randomBytes(32).toString('hex');
// Set anti-CSRF token in res.locals
res.locals.csrfToken = req.session.csrfToken; res.locals.csrfToken = req.session.csrfToken;
console.log(`Server-side CSRF Token: ${req.session.csrfToken}`);
// Log anti-CSRF token
console.log(`Generated Anti-CSRF Token: ${req.session.csrfToken}`);
// Implement secure session handling:
// 1. Set secure, HttpOnly, and SameSite flags
// 2. Set an expiration time for the session token
// 3. Regenerate the session after authentication
res.cookie('sessionToken', sessionToken, { secure: true, httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000) }); // Expires in 1 day
console.log(`Generated Session Token: ${sessionToken}`); console.log(`Generated Session Token: ${sessionToken}`);
// Redirect to home page with session token // Redirect to home page with session token
@ -274,6 +269,8 @@ async (req, res) => {
} }
}); });
app.get("/logout", (req, res) => { app.get("/logout", (req, res) => {
try { try {
const username = req.session.username || "Unknown User"; const username = req.session.username || "Unknown User";

View File

@ -146,6 +146,7 @@
</div> </div>
<script> <script>
const allUsers = <%- JSON.stringify(allUsers) %>; const allUsers = <%- JSON.stringify(allUsers) %>;
</script> </script>
<script src="https://code.jquery.com/jquery-3.6.4.min.js"></script> <script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>

View File

@ -1,6 +1,5 @@
$(document).ready(function () { $(document).ready(function () {
$('#resetPasswordLink').on('click', function () { $('#resetPasswordLink').on('click', function () {
$('#resetPasswordFormContainer').show(); $('#resetPasswordFormContainer').show();
@ -431,3 +430,5 @@ $('#resetPasswordForm').on('submit', function (e) {

View File

@ -86,7 +86,6 @@ button:hover {
<label for="password">Password</label> <label for="password">Password</label>
<input type="password" id="password" name="password" placeholder="Enter your password" required> <input type="password" id="password" name="password" placeholder="Enter your password" required>
<input type="hidden" name="csrf_token" value="<%= csrfToken %>">
<button type="submit">Login</button> <button type="submit">Login</button>
</form> </form>

View File

@ -69,7 +69,7 @@
<label for="otp">OTP:</label> <label for="otp">OTP:</label>
<input type="text" id="otp" name="otp" required> <input type="text" id="otp" name="otp" required>
<br> <br>
<input type="hidden" name="csrf_token" value="<%= csrfToken %>">
<button type="submit">Submit OTP</button> <button type="submit">Submit OTP</button>
</form> </form>
</body> </body>

57
package-lock.json generated
View File

@ -18,6 +18,16 @@
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"semver": "^7.3.5", "semver": "^7.3.5",
"tar": "^6.1.11" "tar": "^6.1.11"
},
"dependencies": {
"node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"requires": {
"whatwg-url": "^5.0.0"
}
}
} }
}, },
"@otplib/core": { "@otplib/core": {
@ -386,6 +396,11 @@
} }
} }
}, },
"data-uri-to-buffer": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="
},
"debug": { "debug": {
"version": "4.3.4", "version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@ -530,6 +545,11 @@
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="
}, },
"esm": {
"version": "3.2.25",
"resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz",
"integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA=="
},
"etag": { "etag": {
"version": "1.8.1", "version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
@ -662,6 +682,15 @@
"validator": "^13.9.0" "validator": "^13.9.0"
} }
}, },
"fetch-blob": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
"requires": {
"node-domexception": "^1.0.0",
"web-streams-polyfill": "^3.0.3"
}
},
"filelist": { "filelist": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
@ -718,6 +747,14 @@
"path-exists": "^4.0.0" "path-exists": "^4.0.0"
} }
}, },
"formdata-polyfill": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
"requires": {
"fetch-blob": "^3.1.2"
}
},
"forwarded": { "forwarded": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@ -1143,12 +1180,19 @@
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
"integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA=="
}, },
"node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="
},
"node-fetch": { "node-fetch": {
"version": "2.7.0", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
"requires": { "requires": {
"whatwg-url": "^5.0.0" "data-uri-to-buffer": "^4.0.0",
"fetch-blob": "^3.1.4",
"formdata-polyfill": "^4.0.10"
} }
}, },
"nodemailer": { "nodemailer": {
@ -1668,6 +1712,11 @@
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="
}, },
"web-streams-polyfill": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.2.tgz",
"integrity": "sha512-3pRGuxRF5gpuZc0W+EpwQRmCD7gRqcDOMt688KmdlDAgAyaB1XlN0zq2njfDNm44XVdIouE7pZ6GzbdyH47uIQ=="
},
"webidl-conversions": { "webidl-conversions": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",

View File

@ -23,6 +23,7 @@
"csurf": "^1.11.0", "csurf": "^1.11.0",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"ejs": "^3.1.9", "ejs": "^3.1.9",
"esm": "^3.2.25",
"express": "^4.18.2", "express": "^4.18.2",
"express-session": "^1.17.3", "express-session": "^1.17.3",
"express-validator": "^7.0.1", "express-validator": "^7.0.1",
@ -30,6 +31,7 @@
"moment": "^2.30.1", "moment": "^2.30.1",
"mqtt": "^5.3.3", "mqtt": "^5.3.3",
"mysql2": "^3.6.5", "mysql2": "^3.6.5",
"node-fetch": "^3.3.2",
"nodemailer": "^6.9.7", "nodemailer": "^6.9.7",
"otp-generator": "^4.0.1", "otp-generator": "^4.0.1",
"otplib": "^12.0.1", "otplib": "^12.0.1",