From 74915f6840be8816f3233da65a858abe2eb2fad8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0mahel?= <48548230+ceskyDJ@users.noreply.github.com> Date: Sat, 7 Jun 2025 22:52:41 +0200 Subject: [PATCH] fix: Strict password validation in login form Strict password validation is for registration, change password, reset password and similar forms. In login form, we don't want to interrupt user with unnecessary validation as it's completely irrelevant what they actually entered. The one thing it's about is the correctness of the email and the password. Disabling strict validation for password is a kind of security through obscurity, which could filter some untargeted attempts for trying passwords by bots. If the bot immediatelly receives notes about the requirements, it has more imformation for better attacks. --- .../TabPanels/Account/PasswordPanel.jsx | 6 ++-- .../TabPanels/Account/ProfilePanel.jsx | 4 +-- .../TabPanels/Account/TeamPanel.jsx | 6 ++-- client/src/Pages/Auth/ForgotPassword.jsx | 6 ++-- client/src/Pages/Auth/Login/Login.jsx | 8 +++--- client/src/Pages/Auth/Register/Register.jsx | 6 ++-- client/src/Pages/Auth/SetNewPassword.jsx | 4 +-- .../Pages/Auth/hooks/useValidatePassword.jsx | 4 +-- client/src/Validation/validation.js | 28 +++++++++++++++---- server/validation/joi.js | 12 +++----- 10 files changed, 49 insertions(+), 35 deletions(-) diff --git a/client/src/Components/TabPanels/Account/PasswordPanel.jsx b/client/src/Components/TabPanels/Account/PasswordPanel.jsx index 88604c72b..84d16e8b0 100644 --- a/client/src/Components/TabPanels/Account/PasswordPanel.jsx +++ b/client/src/Components/TabPanels/Account/PasswordPanel.jsx @@ -4,7 +4,7 @@ import { useTheme } from "@emotion/react"; import { Box, Stack, Typography, Button } from "@mui/material"; import { PasswordEndAdornment } from "../../Inputs/TextInput/Adornments"; import TextInput from "../../Inputs/TextInput"; -import { credentials } from "../../../Validation/validation"; +import { newOrChangedCredentials } from "../../../Validation/validation"; import Alert from "../../Alert"; import { update } from "../../../Features/Auth/authSlice"; import { useDispatch, useSelector } from "react-redux"; @@ -61,7 +61,7 @@ const PasswordPanel = () => { [name]: true, }; - const validation = credentials.validate( + const validation = newOrChangedCredentials.validate( { ...updatedData }, { abortEarly: false, context: { password: updatedData.newPassword } } ); @@ -79,7 +79,7 @@ const PasswordPanel = () => { const handleSubmit = async (event) => { event.preventDefault(); - const { error } = credentials.validate(localData, { + const { error } = newOrChangedCredentials.validate(localData, { abortEarly: false, context: { password: localData.newPassword }, }); diff --git a/client/src/Components/TabPanels/Account/ProfilePanel.jsx b/client/src/Components/TabPanels/Account/ProfilePanel.jsx index 53a8a8d77..46a2bdb79 100644 --- a/client/src/Components/TabPanels/Account/ProfilePanel.jsx +++ b/client/src/Components/TabPanels/Account/ProfilePanel.jsx @@ -5,7 +5,7 @@ import { Box, Button, Divider, Stack, Typography } from "@mui/material"; import Avatar from "../../Avatar"; import TextInput from "../../Inputs/TextInput"; import ImageUpload from "../../Inputs/ImageUpload"; -import { credentials } from "../../../Validation/validation"; +import { newOrChangedCredentials } from "../../../Validation/validation"; import { useDispatch, useSelector } from "react-redux"; import { clearAuthState, deleteUser, update } from "../../../Features/Auth/authSlice"; import { clearUptimeMonitorState } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice"; @@ -58,7 +58,7 @@ const ProfilePanel = () => { [name]: value, })); - validateField({ [name]: value }, credentials, name); + validateField({ [name]: value }, newOrChangedCredentials, name); }; // Validates input against provided schema and updates error state diff --git a/client/src/Components/TabPanels/Account/TeamPanel.jsx b/client/src/Components/TabPanels/Account/TeamPanel.jsx index c29762233..52e3ea9ee 100644 --- a/client/src/Components/TabPanels/Account/TeamPanel.jsx +++ b/client/src/Components/TabPanels/Account/TeamPanel.jsx @@ -4,7 +4,7 @@ import { Button, ButtonGroup, Stack, Typography } from "@mui/material"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import TextInput from "../../Inputs/TextInput"; -import { credentials } from "../../../Validation/validation"; +import { newOrChangedCredentials } from "../../../Validation/validation"; import { networkService } from "../../../main"; import { createToast } from "../../../Utils/toastUtils"; import Select from "../../Inputs/Select"; @@ -115,7 +115,7 @@ const TeamPanel = () => { email: newEmail, })); - const validation = credentials.validate({ email: newEmail }, { abortEarly: false }); + const validation = newOrChangedCredentials.validate({ email: newEmail }, { abortEarly: false }); setErrors((prev) => { const updatedErrors = { ...prev }; @@ -142,7 +142,7 @@ const TeamPanel = () => { if (!toInvite.role.includes("user") || !toInvite.role.includes("admin")) setToInvite((prev) => ({ ...prev, role: ["user"] })); - const { error } = credentials.validate( + const { error } = newOrChangedCredentials.validate( { email: toInvite.email }, { abortEarly: false, diff --git a/client/src/Pages/Auth/ForgotPassword.jsx b/client/src/Pages/Auth/ForgotPassword.jsx index 344bddac1..47b8fc3bf 100644 --- a/client/src/Pages/Auth/ForgotPassword.jsx +++ b/client/src/Pages/Auth/ForgotPassword.jsx @@ -4,7 +4,7 @@ import { createToast } from "../../Utils/toastUtils"; import { useDispatch, useSelector } from "react-redux"; import { forgotPassword } from "../../Features/Auth/authSlice"; import { useEffect, useState } from "react"; -import { credentials } from "../../Validation/validation"; +import { newOrChangedCredentials } from "../../Validation/validation"; import { useNavigate } from "react-router-dom"; import TextInput from "../../Components/Inputs/TextInput"; import Logo from "../../assets/icons/checkmate-icon.svg?react"; @@ -35,7 +35,7 @@ const ForgotPassword = () => { const handleSubmit = async (event) => { event.preventDefault(); - const { error } = credentials.validate(form, { abortEarly: false }); + const { error } = newOrChangedCredentials.validate(form, { abortEarly: false }); if (error) { // validation errors @@ -75,7 +75,7 @@ const ForgotPassword = () => { const { value } = event.target; setForm({ email: value }); - const { error } = credentials.validate({ email: value }, { abortEarly: false }); + const { error } = newOrChangedCredentials.validate({ email: value }, { abortEarly: false }); if (error) setErrors({ email: error.details[0].message }); else delete errors.email; diff --git a/client/src/Pages/Auth/Login/Login.jsx b/client/src/Pages/Auth/Login/Login.jsx index c81f0b892..f3b2253d2 100644 --- a/client/src/Pages/Auth/Login/Login.jsx +++ b/client/src/Pages/Auth/Login/Login.jsx @@ -2,7 +2,7 @@ import { useState, useEffect } from "react"; import { useNavigate } from "react-router-dom"; import { Box, Stack, Typography } from "@mui/material"; import { useTheme } from "@emotion/react"; -import { credentials } from "../../../Validation/validation"; +import { loginCredentials } from "../../../Validation/validation"; import { login } from "../../../Features/Auth/authSlice"; import { useDispatch, useSelector } from "react-redux"; import { createToast } from "../../../Utils/toastUtils"; @@ -59,7 +59,7 @@ const Login = () => { [name]: lowerCasedValue, })); - const { error } = credentials.validate( + const { error } = loginCredentials.validate( { [name]: lowerCasedValue }, { abortEarly: false } ); @@ -76,7 +76,7 @@ const Login = () => { event.preventDefault(); if (step === 0) { - const { error } = credentials.validate( + const { error } = loginCredentials.validate( { email: form.email }, { abortEarly: false } ); @@ -91,7 +91,7 @@ const Login = () => { setStep(1); } } else if (step === 1) { - const { error } = credentials.validate(form, { abortEarly: false }); + const { error } = loginCredentials.validate(form, { abortEarly: false }); if (error) { // validation errors diff --git a/client/src/Pages/Auth/Register/Register.jsx b/client/src/Pages/Auth/Register/Register.jsx index fd3f20cd9..49997e8cb 100644 --- a/client/src/Pages/Auth/Register/Register.jsx +++ b/client/src/Pages/Auth/Register/Register.jsx @@ -8,7 +8,7 @@ import { StepOne } from "./StepOne"; import { StepTwo } from "./StepTwo"; import { StepThree } from "./StepThree"; import { networkService } from "../../../main"; -import { credentials } from "../../../Validation/validation"; +import { newOrChangedCredentials } from "../../../Validation/validation"; import { createToast } from "../../../Utils/toastUtils"; import { register } from "../../../Features/Auth/authSlice"; import Background from "../../../assets/Images/background-grid.svg?react"; @@ -171,7 +171,7 @@ const Register = ({ isSuperAdmin }) => { * @returns {Object | undefined} - Returns the validation error object if there are validation errors; otherwise, `undefined`. */ const validateForm = (data, options = {}) => { - const { error } = credentials.validate(data, { + const { error } = newOrChangedCredentials.validate(data, { abortEarly: false, ...options, }); @@ -271,7 +271,7 @@ const Register = ({ isSuperAdmin }) => { [name]: lowerCasedValue, })); - const { error } = credentials.validate( + const { error } = newOrChangedCredentials.validate( { [name]: lowerCasedValue }, { abortEarly: false, context: { password: form.password } } ); diff --git a/client/src/Pages/Auth/SetNewPassword.jsx b/client/src/Pages/Auth/SetNewPassword.jsx index 2d0aa01bc..21dfe2d39 100644 --- a/client/src/Pages/Auth/SetNewPassword.jsx +++ b/client/src/Pages/Auth/SetNewPassword.jsx @@ -6,7 +6,7 @@ import { useTheme } from "@emotion/react"; import { Box, Stack, Typography, Button } from "@mui/material"; import { setNewPassword } from "../../Features/Auth/authSlice"; import { createToast } from "../../Utils/toastUtils"; -import { credentials } from "../../Validation/validation"; +import { newOrChangedCredentials } from "../../Validation/validation"; import Check from "../../Components/Check/Check"; import TextInput from "../../Components/Inputs/TextInput"; import { PasswordEndAdornment } from "../../Components/Inputs/TextInput/Adornments"; @@ -34,7 +34,7 @@ const SetNewPassword = () => { const handleSubmit = async (e) => { e.preventDefault(); - const { error } = credentials.validate(form, { + const { error } = newOrChangedCredentials.validate(form, { abortEarly: false, context: { password: form.password }, }); diff --git a/client/src/Pages/Auth/hooks/useValidatePassword.jsx b/client/src/Pages/Auth/hooks/useValidatePassword.jsx index b49d0944b..102d8d73e 100644 --- a/client/src/Pages/Auth/hooks/useValidatePassword.jsx +++ b/client/src/Pages/Auth/hooks/useValidatePassword.jsx @@ -1,5 +1,5 @@ import { useMemo, useState } from "react"; -import { credentials } from "../../../Validation/validation"; +import { newOrChangedCredentials } from "../../../Validation/validation"; const getFeedbackStatus = (form, errors, field, criteria) => { const fieldErrors = errors[field]; @@ -36,7 +36,7 @@ function useValidatePassword() { validateValue.password = form.password; } - const { error } = credentials.validate(validateValue, validateOptions); + const { error } = newOrChangedCredentials.validate(validateValue, validateOptions); const errors = error?.details.map((error) => ({ path: error.path[0], type: error.type, diff --git a/client/src/Validation/validation.js b/client/src/Validation/validation.js index b79d4c824..acb05116e 100644 --- a/client/src/Validation/validation.js +++ b/client/src/Validation/validation.js @@ -27,7 +27,7 @@ const lastnameSchema = joi "Surname must contain only letters, spaces, apostrophes, or hyphens", }); -const passwordSchema = joi +const newPasswordSchema = joi .string() .trim() .min(8) @@ -64,7 +64,7 @@ const passwordSchema = joi special: "Password must contain at least one special character", }); -const credentials = joi.object({ +const newOrChangedCredentials = joi.object({ firstName: nameSchema, lastName: lastnameSchema, email: joi @@ -76,8 +76,8 @@ const credentials = joi.object({ "string.empty": "authRegisterEmailRequired", "string.email": "authRegisterEmailInvalid", }), - password: passwordSchema, - newPassword: passwordSchema, + password: newPasswordSchema, + newPassword: newPasswordSchema, confirm: joi .string() .trim() @@ -97,6 +97,23 @@ const credentials = joi.object({ inviteToken: joi.string().allow(""), }); +const loginCredentials = joi.object({ + email: joi + .string() + .trim() + .email({ tlds: { allow: false } }) + .lowercase() + .messages({ + "string.empty": "authRegisterEmailRequired", + "string.email": "authRegisterEmailInvalid", + }), + password: joi + .string() + .messages({ + "string.empty": "Please enter your password", + }), +}); + const monitorValidation = joi.object({ url: joi.when("type", { is: "docker", @@ -391,7 +408,8 @@ const infrastructureMonitorValidation = joi.object({ }); export { - credentials, + newOrChangedCredentials, + loginCredentials, imageValidation, monitorValidation, settingsValidation, diff --git a/server/validation/joi.js b/server/validation/joi.js index 3efd202b5..9a3c61a7c 100755 --- a/server/validation/joi.js +++ b/server/validation/joi.js @@ -26,14 +26,10 @@ const loginValidation = joi.object({ .string() .email() .required() - .custom((value, helpers) => { - const lowercasedValue = value.toLowerCase(); - if (value !== lowercasedValue) { - return helpers.message("Email must be in lowercase"); - } - return lowercasedValue; - }), - password: joi.string().min(8).required().pattern(passwordPattern), + .lowercase(), + password: joi + .string() + .required(), }); const nameValidation = joi .string()