From 9445d7280de503d675e6f6a5eafeb61f4e25fa50 Mon Sep 17 00:00:00 2001 From: PranavObliterates Date: Wed, 7 Jan 2026 16:42:58 +0530 Subject: [PATCH 1/3] feat: implement strong password policy for signup and reset-password --- server/routes/auth.js | 22 +++++---- server/utils/passwordValidator.js | 79 +++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 8 deletions(-) create mode 100644 server/utils/passwordValidator.js diff --git a/server/routes/auth.js b/server/routes/auth.js index 6eb2a55..4f38b4e 100644 --- a/server/routes/auth.js +++ b/server/routes/auth.js @@ -3,8 +3,10 @@ import bcrypt from "bcrypt"; import jwt from "jsonwebtoken"; import prisma from "../db.js"; import { generateOTP, sendOTPEmail, isOTPEnabled } from "../utils/email.js"; +import { validatePassword, formatPasswordErrors } from "../utils/passwordValidator.js"; import passport from "../config/passport.js"; + const router = express.Router(); // Generate JWT token @@ -153,10 +155,12 @@ router.post("/signup", async (req, res) => { .json({ error: "Username must be at least 3 characters" }); } - if (password.length < 6) { - return res - .status(400) - .json({ error: "Password must be at least 6 characters long" }); + // Validate password against strong password policy + const passwordValidation = validatePassword(password); + if (!passwordValidation.isValid) { + return res.status(400).json({ + error: formatPasswordErrors(passwordValidation.errors) + }); } // Verify OTP if enabled @@ -348,10 +352,12 @@ router.post("/reset-password", async (req, res) => { .json({ error: "Email, OTP, and new password are required" }); } - if (newPassword.length < 6) { - return res - .status(400) - .json({ error: "Password must be at least 6 characters long" }); + // Validate password against strong password policy + const passwordValidation = validatePassword(newPassword); + if (!passwordValidation.isValid) { + return res.status(400).json({ + error: formatPasswordErrors(passwordValidation.errors) + }); } // Verify OTP diff --git a/server/utils/passwordValidator.js b/server/utils/passwordValidator.js new file mode 100644 index 0000000..0d2c256 --- /dev/null +++ b/server/utils/passwordValidator.js @@ -0,0 +1,79 @@ +// Password validation utility +// Implements strong password policy for user authentication + +/** + * Validates password against security requirements + * @param {string} password - The password to validate + * @returns {Object} - { isValid: boolean, errors: string[] } + */ +export const validatePassword = (password) => { + const errors = []; + + // Check minimum length (8 characters) + if (password.length < 8) { + errors.push("Password must be at least 8 characters long"); + } + + // Check for at least one uppercase letter + if (!/[A-Z]/.test(password)) { + errors.push("Password must include at least one uppercase letter"); + } + + // Check for at least one lowercase letter + if (!/[a-z]/. test(password)) { + errors.push("Password must include at least one lowercase letter"); + } + + // Check for at least one number + if (!/[0-9]/.test(password)) { + errors.push("Password must include at least one number"); + } + + // Check for at least one special character + if (!/[! @#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password)) { + errors.push("Password must include at least one special character (! @#$%^&*...)"); + } + + // Check for common/weak passwords + const commonPasswords = [ + 'password', 'password123', '123456', '123456789', '12345678', + 'qwerty', 'abc123', 'monkey', '1234567', 'letmein', + 'trustno1', 'dragon', 'baseball', 'iloveyou', 'master', + 'sunshine', 'ashley', 'bailey', 'shadow', 'superman', + 'qazwsx', 'welcome', 'admin', 'login', 'passw0rd' + ]; + + if (commonPasswords.includes(password.toLowerCase())) { + errors.push("This password is too common. Please choose a more secure password"); + } + + // Check for sequential characters (e.g., "123", "abc") + if (/(abc|bcd|cde|def|efg|fgh|ghi|hij|ijk|jkl|klm|lmn|mno|nop|opq|pqr|qrs|rst|stu|tuv|uvw|vwx|wxy|xyz|012|123|234|345|456|567|678|789)/i.test(password)) { + errors.push("Password should not contain sequential characters"); + } + + // Check for repeated characters (e.g., "aaa", "111") + if (/(. )\1{2,}/.test(password)) { + errors.push("Password should not contain repeated characters (e.g., 'aaa', '111')"); + } + + return { + isValid: errors.length === 0, + errors: errors + }; +}; + +/** + * Returns formatted error message for validation errors + * @param {string[]} errors - Array of error messages + * @returns {string} - Formatted error message + */ +export const formatPasswordErrors = (errors) => { + if (errors.length === 0) return ''; + + if (errors.length === 1) { + return errors[0]; + } + + return `Password requirements not met:\n${errors.map((err, idx) => `${idx + 1}. ${err}`).join('\n')}`; +}; \ No newline at end of file From 651f9ed1a88381623ebf69292d0f5876e5e08ed5 Mon Sep 17 00:00:00 2001 From: PranavObliterates Date: Wed, 7 Jan 2026 18:32:54 +0530 Subject: [PATCH 2/3] feat: implement strong password policy with Zod schema validation --- server/package-lock.json | 13 ++++- server/package.json | 3 +- server/routes/auth.js | 49 ++++++------------ server/schemas/auth.schema.js | 85 +++++++++++++++++++++++++++++++ server/utils/passwordValidator.js | 79 ---------------------------- 5 files changed, 115 insertions(+), 114 deletions(-) create mode 100644 server/schemas/auth.schema.js delete mode 100644 server/utils/passwordValidator.js diff --git a/server/package-lock.json b/server/package-lock.json index e97d1f1..3f79208 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -27,7 +27,8 @@ "passport-google-oauth20": "^2.0.0", "qs": "6.14.1", "redis": "^5.8.3", - "socket.io": "^4.8.1" + "socket.io": "^4.8.1", + "zod": "^4.3.5" }, "devDependencies": { "@flydotio/dockerfile": "^0.7.10", @@ -3105,6 +3106,7 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -4582,6 +4584,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz", + "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/server/package.json b/server/package.json index 47df894..08a0269 100644 --- a/server/package.json +++ b/server/package.json @@ -31,7 +31,8 @@ "passport-google-oauth20": "^2.0.0", "qs": "6.14.1", "redis": "^5.8.3", - "socket.io": "^4.8.1" + "socket.io": "^4.8.1", + "zod": "^4.3.5" }, "devDependencies": { "@flydotio/dockerfile": "^0.7.10", diff --git a/server/routes/auth.js b/server/routes/auth.js index 4f38b4e..12fa2f9 100644 --- a/server/routes/auth.js +++ b/server/routes/auth.js @@ -3,7 +3,6 @@ import bcrypt from "bcrypt"; import jwt from "jsonwebtoken"; import prisma from "../db.js"; import { generateOTP, sendOTPEmail, isOTPEnabled } from "../utils/email.js"; -import { validatePassword, formatPasswordErrors } from "../utils/passwordValidator.js"; import passport from "../config/passport.js"; @@ -143,25 +142,14 @@ router.post("/signup", async (req, res) => { console.log("🔐 Signup attempt:", { username, email }); - if (!username || !email || !password) { - return res - .status(400) - .json({ error: "Username, email, and password are required" }); - } - - if (username.length < 3) { - return res - .status(400) - .json({ error: "Username must be at least 3 characters" }); - } - - // Validate password against strong password policy - const passwordValidation = validatePassword(password); - if (!passwordValidation.isValid) { - return res.status(400).json({ - error: formatPasswordErrors(passwordValidation.errors) - }); - } + // Validate request body with Zod schema +try { + signupSchema.parse({ username, email, password, otp }); +} catch (zodError) { + // Format Zod errors into user-friendly messages + const errors = zodError.errors.map(err => err.message).join('; '); + return res.status(400).json({ error: errors }); +} // Verify OTP if enabled if (isOTPEnabled()) { @@ -346,19 +334,14 @@ router.post("/reset-password", async (req, res) => { try { const { email, otp, newPassword } = req.body; - if (!email || !otp || !newPassword) { - return res - .status(400) - .json({ error: "Email, OTP, and new password are required" }); - } - - // Validate password against strong password policy - const passwordValidation = validatePassword(newPassword); - if (!passwordValidation.isValid) { - return res.status(400).json({ - error: formatPasswordErrors(passwordValidation.errors) - }); - } + // Validate request body with Zod schema +try { + resetPasswordSchema.parse({ email, otp, newPassword }); +} catch (zodError) { + // Format Zod errors into user-friendly messages + const errors = zodError.errors.map(err => err.message).join('; '); + return res.status(400).json({ error: errors }); +} // Verify OTP const otpRecord = await prisma.otp.findFirst({ diff --git a/server/schemas/auth.schema.js b/server/schemas/auth.schema.js new file mode 100644 index 0000000..cd93e64 --- /dev/null +++ b/server/schemas/auth.schema.js @@ -0,0 +1,85 @@ +import { z } from 'zod'; + +/** + * Password validation schema using Zod + * Aligned with frontend PasswordInput component from PR #85 + * + * Criteria (matching frontend strength calculator): + * - Minimum 8 characters + * - At least one uppercase letter + * - At least one lowercase letter + * - At least one number + * - At least one special character + */ +export const passwordSchema = z + .string() + .min(8, 'Password must be at least 8 characters long') + .regex(/[a-z]/, 'Password must include at least one lowercase letter') + .regex(/[A-Z]/, 'Password must include at least one uppercase letter') + .regex(/[0-9]/, 'Password must include at least one number') + .regex( + /[^A-Za-z0-9]/, + 'Password must include at least one special character (! @#$%^&*... )' + ) + .refine( + (password) => { + // Block common passwords + const commonPasswords = [ + 'password', 'password123', '123456', '123456789', '12345678', + 'qwerty', 'abc123', 'monkey', '1234567', 'letmein', + 'trustno1', 'dragon', 'baseball', 'iloveyou', 'master', + 'sunshine', 'ashley', 'bailey', 'shadow', 'superman', + 'qazwsx', 'welcome', 'admin', 'login', 'passw0rd' + ]; + return !commonPasswords.includes(password.toLowerCase()); + }, + { message: 'This password is too common. Please choose a more secure password' } + ) + .refine( + (password) => { + // Block sequential characters (abc, 123) + const sequential = /(abc|bcd|cde|def|efg|fgh|ghi|hij|ijk|jkl|klm|lmn|mno|nop|opq|pqr|qrs|rst|stu|tuv|uvw|vwx|wxy|xyz|012|123|234|345|456|567|678|789)/i; + return !sequential.test(password); + }, + { message: 'Password should not contain sequential characters (e.g., "abc", "123")' } + ) + .refine( + (password) => { + // Block repeated characters (aaa, 111) + return !/(. )\1{2,}/.test(password); + }, + { message: 'Password should not contain repeated characters (e.g., "aaa", "111")' } + ); + +/** + * Signup request validation schema + */ +export const signupSchema = z. object({ + username: z + .string() + .min(3, 'Username must be at least 3 characters') + .max(30, 'Username must not exceed 30 characters') + .regex(/^[a-zA-Z0-9_]+$/, 'Username can only contain letters, numbers, and underscores'), + email: z + . string() + .email('Please enter a valid email address'), + password: passwordSchema, + otp: z.string().optional() +}); + +/** + * Reset password request validation schema + */ +export const resetPasswordSchema = z. object({ + email: z. string().email('Please enter a valid email address'), + otp: z.string().min(6, 'OTP must be 6 digits'), + newPassword: passwordSchema +}); + +/** + * Login request validation schema + */ +export const loginSchema = z.object({ + usernameOrEmail: z.string().min(1, 'Username or email is required'), + password: z.string().min(1, 'Password is required') +}); \ No newline at end of file diff --git a/server/utils/passwordValidator.js b/server/utils/passwordValidator.js deleted file mode 100644 index 0d2c256..0000000 --- a/server/utils/passwordValidator.js +++ /dev/null @@ -1,79 +0,0 @@ -// Password validation utility -// Implements strong password policy for user authentication - -/** - * Validates password against security requirements - * @param {string} password - The password to validate - * @returns {Object} - { isValid: boolean, errors: string[] } - */ -export const validatePassword = (password) => { - const errors = []; - - // Check minimum length (8 characters) - if (password.length < 8) { - errors.push("Password must be at least 8 characters long"); - } - - // Check for at least one uppercase letter - if (!/[A-Z]/.test(password)) { - errors.push("Password must include at least one uppercase letter"); - } - - // Check for at least one lowercase letter - if (!/[a-z]/. test(password)) { - errors.push("Password must include at least one lowercase letter"); - } - - // Check for at least one number - if (!/[0-9]/.test(password)) { - errors.push("Password must include at least one number"); - } - - // Check for at least one special character - if (!/[! @#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password)) { - errors.push("Password must include at least one special character (! @#$%^&*...)"); - } - - // Check for common/weak passwords - const commonPasswords = [ - 'password', 'password123', '123456', '123456789', '12345678', - 'qwerty', 'abc123', 'monkey', '1234567', 'letmein', - 'trustno1', 'dragon', 'baseball', 'iloveyou', 'master', - 'sunshine', 'ashley', 'bailey', 'shadow', 'superman', - 'qazwsx', 'welcome', 'admin', 'login', 'passw0rd' - ]; - - if (commonPasswords.includes(password.toLowerCase())) { - errors.push("This password is too common. Please choose a more secure password"); - } - - // Check for sequential characters (e.g., "123", "abc") - if (/(abc|bcd|cde|def|efg|fgh|ghi|hij|ijk|jkl|klm|lmn|mno|nop|opq|pqr|qrs|rst|stu|tuv|uvw|vwx|wxy|xyz|012|123|234|345|456|567|678|789)/i.test(password)) { - errors.push("Password should not contain sequential characters"); - } - - // Check for repeated characters (e.g., "aaa", "111") - if (/(. )\1{2,}/.test(password)) { - errors.push("Password should not contain repeated characters (e.g., 'aaa', '111')"); - } - - return { - isValid: errors.length === 0, - errors: errors - }; -}; - -/** - * Returns formatted error message for validation errors - * @param {string[]} errors - Array of error messages - * @returns {string} - Formatted error message - */ -export const formatPasswordErrors = (errors) => { - if (errors.length === 0) return ''; - - if (errors.length === 1) { - return errors[0]; - } - - return `Password requirements not met:\n${errors.map((err, idx) => `${idx + 1}. ${err}`).join('\n')}`; -}; \ No newline at end of file From cd24f4ca9d545af27f08b3f86a34de980df3329e Mon Sep 17 00:00:00 2001 From: PranavObliterates Date: Thu, 8 Jan 2026 18:46:54 +0530 Subject: [PATCH 3/3] fix: add missing Zod schema imports in auth routes --- server/routes/auth.js | 1 + 1 file changed, 1 insertion(+) diff --git a/server/routes/auth.js b/server/routes/auth.js index 12fa2f9..445734d 100644 --- a/server/routes/auth.js +++ b/server/routes/auth.js @@ -3,6 +3,7 @@ import bcrypt from "bcrypt"; import jwt from "jsonwebtoken"; import prisma from "../db.js"; import { generateOTP, sendOTPEmail, isOTPEnabled } from "../utils/email.js"; +import { signupSchema, resetPasswordSchema } from "../schemas/auth.schema. js"; import passport from "../config/passport.js";