Merge pull request #17 from Newtbot/anti-csrf

redo ant csrf token
This commit is contained in:
noot 2024-01-13 02:53:11 +08:00 committed by GitHub
commit f91e99330a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 96 additions and 47 deletions

View File

@ -19,32 +19,19 @@ const PORT = process.env.PORT || 3000;
require("dotenv").config();
app.use(bodyParser.urlencoded({ extended: true }));
app.use(
session({
secret: process.env.key,
resave: false,
saveUninitialized: true,
cookie: {
secure: false, // Make sure to set this to true in a production environment with HTTPS
httpOnly: true,
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");
app.use(session({
secret: process.env.key,
resave: false,
saveUninitialized: true,
cookie: {
secure: false, // Make sure to set this to true in a production environment with HTTPS
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000, // Session duration in milliseconds (here set to 1 day)
},
}));
function isAuthenticated(req, res, next) {
if (req.session && req.session.authenticated) {
return next();
@ -144,17 +131,11 @@ const logActivity = async (username, success, message) => {
app.post('/login', [
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('csrf_token').escape().trim().isLength({ min: 1 }).withMessage('CSRF token must not be empty'),
],
async (req, res) => {
try {
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()) {
// 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 });
@ -222,17 +203,11 @@ async (req, res) => {
// OTP verification route
app.post("/verify-otp", [
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) => {
try {
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()) {
// 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 });
@ -240,10 +215,16 @@ async (req, res) => {
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) {
// Log successful OTP entry and login
// Log successful OTP entry
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
@ -253,8 +234,22 @@ async (req, res) => {
req.session.authenticated = true;
req.session.username = req.body.username;
req.session.sessionToken = sessionToken;
res.locals.csrfToken = req.session.csrfToken;
console.log(`Server-side CSRF Token: ${req.session.csrfToken}`);
// 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;
// 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}`);
// Redirect to home page with session token
@ -273,7 +268,9 @@ async (req, res) => {
res.status(500).send("Internal Server Error");
}
});
app.get("/logout", (req, res) => {
try {
const username = req.session.username || "Unknown User";

View File

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

View File

@ -1,6 +1,5 @@
$(document).ready(function () {
$('#resetPasswordLink').on('click', function () {
$('#resetPasswordFormContainer').show();
@ -429,5 +428,7 @@ $('#resetPasswordForm').on('submit', function (e) {
});
});

View File

@ -86,7 +86,6 @@ button:hover {
<label for="password">Password</label>
<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>
</form>

View File

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

57
package-lock.json generated
View File

@ -18,6 +18,16 @@
"rimraf": "^3.0.2",
"semver": "^7.3.5",
"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": {
@ -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": {
"version": "4.3.4",
"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",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="
},
"esm": {
"version": "3.2.25",
"resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz",
"integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA=="
},
"etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
@ -662,6 +682,15 @@
"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": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
@ -718,6 +747,14 @@
"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": {
"version": "0.2.0",
"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",
"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": {
"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==",
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
"requires": {
"whatwg-url": "^5.0.0"
"data-uri-to-buffer": "^4.0.0",
"fetch-blob": "^3.1.4",
"formdata-polyfill": "^4.0.10"
}
},
"nodemailer": {
@ -1668,6 +1712,11 @@
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"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": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",

View File

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