EJS Password Input Validation

Registered members can download the FREE Get Started App. This is the project I used to compose articles about setting up VS Code and developing Node with Express and the Embedded JavaScript (EJS) view engine.

This article describes password requirements for user registration. Password requirements should be listed and indicate if the requirement has been satisfied. If the user can toggle password visibility, the confirmed password field becomes obsolete.

This series will focus on posting form data from the client to the server. Validation should notify the user of invalid input as soon as possible. Password requirements should be listed and indicate if the requirement has been satisfied.

Password requirements have become more complex to help prevent brute-force attacks, credential stuffing, and ransomware. You can help the user comply by displaying the requirements upfront, using real-time validation indicating when each requirement is met, provide a toggle to show/hide the password, and remove the unnecessary confirm password field. I will use the user registration form from this site for this article The form uses Bootstrap classes. I use the same UI for the forgot password, change password, and must change password pages. I'll start with the password visibility implementation. Microsoft browsers (Internet Explorer, Edge) use a non-standard CSS pseudo-element "::-ms-reveal" to style the "eye" icon. I use a Bootstrap Icon so I prevent the MS icon from displaying. I use css classes to style a show/hide button and position it at the end of the password input.

style.css
input[type="password"]::-ms-reveal {
  display: none;
}

button.toggle-password {
  position: absolute;
  top: 3px;
  right: 10px;
  z-index: 9;
  width: 28px;
  height: 30px;
  background: 0;
  border: 0;
}

button.toggle-password:active,
button.toggle-password:focus,
button#\.toggle-password:hover {
  cursor: pointer;
}

button.toggle-password:focus {
  outline: none !important;
}

.input-password {
  padding-right: calc(1.5em + 0.75rem);
  background-repeat: no-repeat;
  background-position: right calc(0.375em + 0.1875rem) center;
  background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
}

The client-side setPasswordVisibilityToggle function converts the input's type between "text" and "password".

script.js
.const setPasswordVisibilityToggle = (passwordInputId, togglePasswordButtonId) => {
    const passwordInput = document.getElementById(passwordInputId);
    const togglePasswordButton = document.getElementById(togglePasswordButtonId);
    if (!passwordInput || !togglePasswordButton) {
        alert('PasswordInput or TogglePasswordButton not found.');
        return;
    }
    passwordInput.onclick = () => {
        passwordInput.classList.add("input-password");
        togglePasswordButton.classList.remove("d-none");
        const togglePassword = () => {
            togglePasswordButton.querySelector('i').classList.toggle('bi-eye');
            togglePasswordButton.querySelector('i').classList.toggle('bi-eye-slash');
            if (passwordInput.type === "password") {
                passwordInput.type = "text";
                togglePasswordButton.querySelector('i').setAttribute("title", "Hide");
                togglePasswordButton.setAttribute("aria-label", "Hide password.");
            } else {
                passwordInput.type = "password";
                togglePasswordButton.querySelector('i').setAttribute("title", "Show");
                togglePasswordButton.setAttribute(
                    "aria-label",
                    "Show password as plain text. Warning: this will display your password on the screen."
                );
            }
        }
        togglePasswordButton.addEventListener("click", togglePassword);        
    };
}

The initial input's type is set to "password". Besides the required attribute, the autocomplete="off", spellcheck="false", autocorrect="off", and autocapitalize="off" attributes disable "text" type features.

register.ejs
<div class="mb-3">
    <label for="password">Password:</label>                
    <div id="PasswordUpperDivId" class="invalid password_requirement">At least one upper case letter.</div>
    <div id="PasswordLowerDivId" class="invalid password_requirement">At least one lower case letter.</div>
    <div id="PasswordDigitDivId" class="invalid password_requirement">At least one digit.</div>
    <div id="PasswordSpecialDivId" class="invalid password_requirement">At least one special character.</div>
    <div id="PasswordLengthDivId" class="invalid password_requirement mb-2">At least 8 characters long.</div>                         
    <div class="input-group">
        <input class="form-control rounded" type="password" id="password" name="password" autocomplete="off" spellcheck="false" autocorrect="off" autocapitalize="off" required>
        <button class="toggle-password d-none" type="button" id="toggle-password">
            <i class="bi bi-eye fs-4" title="Show"></i> 
        </button>
    </div>
</div>
type="password"
Password Hidden
type="text"
Password Visible

The password requirements list is initially set to the invalid color "orange". When the requirement is met, the valid color "green" is applied.

style.css
.invalid.password_requirement {
    color: orange;
}

.valid.password_requirement {
    color: green;
}

The client-side Password Requirements Validation occurs when an input with the id attribute set to "password" or "newpassword" is found on any page other than the log in page.

script.js
// Password Requirements Validation
if (location.pathname.toLowerCase() !== '/account/login'
    && (document.querySelector('#password') ||
        document.querySelector('#newpassword'))) {

    let passwordInput = document.querySelector('#password');
    if (!passwordInput) passwordInput = document.querySelector('#newpassword');

    const upper = document.querySelector('#PasswordUpperDivId');
    const lower = document.querySelector('#PasswordLowerDivId');
    const digit = document.querySelector('#PasswordDigitDivId');
    const special = document.querySelector('#PasswordSpecialDivId');
    const length = document.querySelector('#PasswordLengthDivId');

    // When the user starts to type something inside the password field
    passwordInput.addEventListener('keyup', () => {

        // Validate capital letters
        const upperCaseLetters = /[A-Z]/g;
        if (passwordInput.value.match(upperCaseLetters)) {
            upper.classList.remove('invalid');
            upper.classList.add('valid');
        } else {
            upper.classList.remove('valid');
            upper.classList.add('invalid');
        }

        // Validate lowercase letters
        const lowerCaseLetters = /[a-z]/g;
        if (passwordInput.value.match(lowerCaseLetters)) {
            lower.classList.remove('invalid');
            lower.classList.add('valid');
        } else {
            lower.classList.remove('valid');
            lower.classList.add('invalid');
        }

        // Validate digit
        const numbers = /[0-9]/g;
        if (passwordInput.value.match(numbers)) {
            digit.classList.remove('invalid');
            digit.classList.add('valid');
        } else {
            digit.classList.remove('valid');
            digit.classList.add('invalid');
        }

        // Validate special
        const specials = /\W/g;
        if (passwordInput.value.match(specials)) {
            special.classList.remove('invalid');
            special.classList.add('valid');
        } else {
            special.classList.remove('valid');
            special.classList.add('invalid');
        }

        // Validate length
        if (passwordInput.value.length >= 8) {
            length.classList.remove('invalid');
            length.classList.add('valid');
        } else {
            length.classList.remove('valid');
            length.classList.add('invalid');
        }
    });
}

I use a single Regular Expression to validate the password on the server. I implement an InputValidator module in an input-validator.mjs file.

input-validator.mjs
InputValidator.password = (password = '') => {
    const result = { valid: false, errors: [] };
    const passwordRegExp = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,50}$/;
    if (!passwordRegExp.test(password)) result.errors.push('Password does not meet all requirements.');
    if (result.errors.length == 0) result.valid = true;
    return result;
}
Created: 2/16/26