From e2d75733412eb26d43ca7800cff82c9beda9e4a6 Mon Sep 17 00:00:00 2001 From: Kristijan Plavsic Date: Tue, 10 Mar 2026 11:52:54 +0100 Subject: [PATCH 1/3] sec: add login rate limiter to protect login endpoint --- RentalCar/server/middleware/loginRateLimiter.js | 12 ++++++++++++ RentalCar/server/routes/userRoutes.js | 3 ++- 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 RentalCar/server/middleware/loginRateLimiter.js diff --git a/RentalCar/server/middleware/loginRateLimiter.js b/RentalCar/server/middleware/loginRateLimiter.js new file mode 100644 index 0000000..3984569 --- /dev/null +++ b/RentalCar/server/middleware/loginRateLimiter.js @@ -0,0 +1,12 @@ +import rateLimit from "express-rate-limit"; + +export const loginRateLimiter = rateLimit({ + windowMs: 10 * 60 * 1000, // 10 minuta + max: 5, // max 5 pokusaja u okviru 10 minuta po IP adresi + standardHeaders: true, + legacyHeaders: false, + message: { + success: false, + message: "Too many login attempts. Please try again later.", + }, +}); \ No newline at end of file diff --git a/RentalCar/server/routes/userRoutes.js b/RentalCar/server/routes/userRoutes.js index 965a8fa..a79cefb 100644 --- a/RentalCar/server/routes/userRoutes.js +++ b/RentalCar/server/routes/userRoutes.js @@ -6,6 +6,7 @@ import { registerUser, } from "../controllers/userController.js"; import { protect } from "../middleware/auth.js"; +import { loginRateLimiter } from "../middleware/loginRateLimiter.js"; const userRouter = express.Router(); @@ -58,7 +59,7 @@ userRouter.post("/register", registerUser); * schema: * $ref: '#/components/schemas/AuthResponse' */ -userRouter.post("/login", loginUser); +userRouter.post("/login", loginRateLimiter, loginUser); /** * @openapi From d5ca929138edbfbff2554e132dbe82eb5d291ee1 Mon Sep 17 00:00:00 2001 From: Kristijan Plavsic Date: Tue, 10 Mar 2026 12:17:03 +0100 Subject: [PATCH 2/3] sec: add account lockout after failed login attempts --- .../server/controllers/userController.js | 54 +++++++++++++++---- .../server/middleware/loginRateLimiter.js | 2 +- RentalCar/server/models/User.js | 7 ++- 3 files changed, 50 insertions(+), 13 deletions(-) diff --git a/RentalCar/server/controllers/userController.js b/RentalCar/server/controllers/userController.js index 295878c..852ba78 100644 --- a/RentalCar/server/controllers/userController.js +++ b/RentalCar/server/controllers/userController.js @@ -3,6 +3,9 @@ import bcrypt from "bcrypt"; import jwt from "jsonwebtoken"; import Car from "../models/Car.js"; +const MAX_LOGIN_ATTEMPTS = 5; +const LOCK_TIME_MS = 15 * 60 * 1000; // 15 minuta + //Generate JWT Token const generateToken = (userId) => { const payload = userId; @@ -56,33 +59,64 @@ export const loginUser = async (req, res) => { const { email, password } = req.body; const user = await User.findOne({ email }); + + const invalidMessage = { + success: false, + message: "Invalid email or password", + }; + if (!user) { - return res.json({ + return res.status(401).json(invalidMessage); + } + + if (user.lockUntil && user.lockUntil > new Date()) { + return res.status(423).json({ success: false, - message: "User not found", + message: "Account is temporarily locked. Please try again later.", }); } + if (user.lockUntil && user.lockUntil <= new Date()) { + user.loginAttempts = 0; + user.lockUntil = null; + await user.save(); + } + const isMatch = await bcrypt.compare(password, user.password); + if (!isMatch) { - return res.json({ - success: false, - message: "Invalid Credentials", - }); + user.loginAttempts += 1; + + if (user.loginAttempts >= MAX_LOGIN_ATTEMPTS) { + user.lockUntil = new Date(Date.now() + LOCK_TIME_MS); + await user.save(); + + return res.status(423).json({ + success: false, + message: "Account is temporarily locked. Please try again later.", + }); + } + + await user.save(); + + return res.status(401).json(invalidMessage); } + user.loginAttempts = 0; + user.lockUntil = null; + await user.save(); + const token = generateToken(user._id.toString()); - res.json({ + return res.status(200).json({ success: true, token, }); } catch (error) { - // error handling console.log(error.message); - res.json({ + return res.status(500).json({ success: false, - message: error.message, + message: "Server error", }); } }; diff --git a/RentalCar/server/middleware/loginRateLimiter.js b/RentalCar/server/middleware/loginRateLimiter.js index 3984569..0ac9b5e 100644 --- a/RentalCar/server/middleware/loginRateLimiter.js +++ b/RentalCar/server/middleware/loginRateLimiter.js @@ -2,7 +2,7 @@ import rateLimit from "express-rate-limit"; export const loginRateLimiter = rateLimit({ windowMs: 10 * 60 * 1000, // 10 minuta - max: 5, // max 5 pokusaja u okviru 10 minuta po IP adresi + max: 10, // max 5 pokusaja u okviru 10 minuta po IP adresi standardHeaders: true, legacyHeaders: false, message: { diff --git a/RentalCar/server/models/User.js b/RentalCar/server/models/User.js index b5011ad..2114f9d 100644 --- a/RentalCar/server/models/User.js +++ b/RentalCar/server/models/User.js @@ -7,16 +7,19 @@ const userSchema = new mongoose.Schema( password: { type: String, required: true }, role: { type: String, - enum: ["user","owner","admin"], + enum: ["user", "owner", "admin"], default: "user", }, image: { type: String, default: "" }, documents: [{ type: mongoose.Schema.Types.ObjectId, ref: "Document" }], + + loginAttempts: { type: Number, default: 0 }, + lockUntil: { type: Date, default: null }, }, { timestamps: true } ); const User = mongoose.model("User", userSchema); -export default User; +export default User; \ No newline at end of file From 5a74321507a37f9e7b6f5c33ffd54e30079187f9 Mon Sep 17 00:00:00 2001 From: Kristijan Plavsic Date: Tue, 10 Mar 2026 12:50:09 +0100 Subject: [PATCH 3/3] sec: standardize auth responses and status codes --- .../server/controllers/userController.js | 61 ++++++++++++------- .../server/middleware/loginRateLimiter.js | 2 +- 2 files changed, 39 insertions(+), 24 deletions(-) diff --git a/RentalCar/server/controllers/userController.js b/RentalCar/server/controllers/userController.js index 852ba78..d9a9d01 100644 --- a/RentalCar/server/controllers/userController.js +++ b/RentalCar/server/controllers/userController.js @@ -7,9 +7,15 @@ const MAX_LOGIN_ATTEMPTS = 5; const LOCK_TIME_MS = 15 * 60 * 1000; // 15 minuta //Generate JWT Token -const generateToken = (userId) => { - const payload = userId; - return jwt.sign(payload, process.env.JWT_SECRET); +const generateToken = (user) => { + return jwt.sign( + { + id: user._id.toString(), + role: user.role, + }, + process.env.JWT_SECRET, + { expiresIn: "1h" } + ); }; //Register User @@ -18,15 +24,15 @@ export const registerUser = async (req, res) => { const { name, email, password } = req.body; if (!name || !email || !password || password.length < 8) { - return res.json({ + return res.status(400).json({ success: false, - message: "Fill all the fields", + message: "Name, email and password are required, and password must be at least 8 characters long.", }); } const userExists = await User.findOne({ email }); if (userExists) { - return res.json({ + return res.status(409).json({ success: false, message: "User already exists", }); @@ -40,16 +46,19 @@ export const registerUser = async (req, res) => { password: hashedPassword, }); - const token = generateToken(user._id.toString()); + const token = generateToken(user); - res.json({ success: true, token }); + return res.status(201).json({ + success: true, + token, + }); } catch (error) { // error handling console.log(error.message); - res.json({ - success: false, - message: error.message, - }); + return res.status(500).json({ + success: false, + message: "Server error", + }); } }; @@ -106,7 +115,7 @@ export const loginUser = async (req, res) => { user.lockUntil = null; await user.save(); - const token = generateToken(user._id.toString()); + const token = generateToken(user); return res.status(200).json({ success: true, @@ -126,16 +135,16 @@ export const getUserData = async (req, res) => { try { const { user } = req; - res.json({ - success: true, - user, - }); + return res.status(200).json({ + success: true, + user, + }); } catch (error) { console.log(error.message); - res.json({ - success: false, - message: error.message, - }); + return res.status(500).json({ + success: false, + message: "Server error", + }); } }; @@ -143,9 +152,15 @@ export const getUserData = async (req, res) => { export const getCars = async (req, res) => { try { const cars = await Car.find({ isAvailable: true }); - res.json({ success: true, cars }); + return res.status(200).json({ + success: true, + cars, + }); } catch (error) { console.log(error.message); - res.json({ success: false, message: error.message }); + return res.status(500).json({ + success: false, + message: "Server error", + }); } }; diff --git a/RentalCar/server/middleware/loginRateLimiter.js b/RentalCar/server/middleware/loginRateLimiter.js index 0ac9b5e..3984569 100644 --- a/RentalCar/server/middleware/loginRateLimiter.js +++ b/RentalCar/server/middleware/loginRateLimiter.js @@ -2,7 +2,7 @@ import rateLimit from "express-rate-limit"; export const loginRateLimiter = rateLimit({ windowMs: 10 * 60 * 1000, // 10 minuta - max: 10, // max 5 pokusaja u okviru 10 minuta po IP adresi + max: 5, // max 5 pokusaja u okviru 10 minuta po IP adresi standardHeaders: true, legacyHeaders: false, message: {