Client-side Password Rule Validation

FusionAuth checks password rules in server-side validation, but you can also check them in your client. This page shows one method of doing so in JavaScript.

Client-side Password Validation Example

FusionAuth provides password rules, configurable at the tenant level. These include checks on a password value such as:

  • a password must have one special character
  • a password length must be at least 12 characters, in lowercase, uppercase, or a combination including special characters
  • a password must contain a numeric character

The latest guidance from NIST recommends avoiding such rules but if you have internal or external policies requiring password complexity rules, FusionAuth supports it.

These password rules are enforced in the hosted HTML login pages, but only after the user has submitted the form. They are also available as freemarker variables on the registration page and the change password page.

This example shows how to use the passwordValidationRules freemarker variables to offer user feedback client-side using JavaScript, in the hosted login pages. It will also disable submission of the form if the rules are not met.

To use this script, make FusionAuthPasswordChecker.js available at a public URL or modify the FusionAuth template files to include this JavaScript on the registration page and change password pages.

<script src="https://yourcdn.example.com/path/to/FusionAuthPasswordChecker.js"></script>

This JavaScript expects the names and DOM structure of the pages to be the same as the default theme structure. If you've modified your theme, this code will be a starting point, but is not guaranteed to work.

You'll also need to create CSS classes validation and ok to visually inform your users about the status of their password.

Password rules are also available via an unauthenticated API call if you'd prefer to build your own validation logic without using JavaScript. This might be useful for a mobile application, for example.

JavaScript Code

Here’s the example code.

class FusionAuthPasswordChecker {
  #minLength;
  #maxLength;
  #passwordField;
  #requireMixedCase;
  #requireNonAlpha;
  #requireNumber;
  #timer;

  constructor(minLength, maxLength, requireMixedCase, requireNonAlpha, requireNumber) {
    this.#minLength = minLength;
    this.#maxLength = maxLength;

    this.#requireMixedCase = requireMixedCase;
    this.#requireNonAlpha = requireNonAlpha;
    this.#requireNumber = requireNumber;

    this.#passwordField = document.querySelector('input[type="password"]');

    if (this.#passwordField !== null) {
      this.#passwordField.addEventListener('input', () => this.#score());
      this.#passwordField.closest('form').querySelector('button').disabled = true;
      this.#passwordField.closest('form').querySelector('button').classList.add('disabled');
    }
  }

  #check(password, check, errorTextSupplier) {
    if (check(password)) {
      return;
    }
    
    this.#invalid(errorTextSupplier());
  }

  #score() {
    if (this.#timer !== null) {
      clearTimeout(this.#timer);
    }

    this.#timer = setTimeout(() => {
      const error = this.#passwordField.closest('.form-row').querySelector('span.error');
      if (error !== null) {
        error.remove();
      }

      const password = this.#passwordField.value;
      if (password.length === 0) {
        return;
      }

      this.#check(password, (value) => value.length >= this.#minLength, () => 'too short');
      this.#check(password, (value) => value.length <= this.#maxLength, () => 'too long');

      if (this.#requireNumber) {
        this.#check(password, (value) => /\d/.test(value), () => 'must container a number');
      }

      if (this.#requireMixedCase) {
        this.#check(password, (value) => /[a-z]/.test(value) && /[A-Z]/.test(value), () => 'must contain mixed case');
      }

      if (this.#requireNonAlpha) {
        this.#check(password, (value) => /\W/.test(value), () => 'must contain a special character');
      }

      if (this.#passwordField.closest('.form-row').querySelector('span.error') === null) {
        // Add classes, or style to provide visual feedback
        this.#passwordField.classList.add('ok');
        this.#passwordField.classList.remove('validation');

        this.#passwordField.closest('form').querySelector('button').disabled = false;
        this.#passwordField.closest('form').querySelector('button').classList.remove('disabled');
      }
      
    }, 500);
  }

  #invalid(errorText) {
    // Add classes, or style to provide visual feedback
    this.#passwordField.classList.add('validation');
    this.#passwordField.classList.remove('ok');

    let errorSpan = this.#passwordField.closest('.form-row').querySelector('span.error');
    if (errorSpan === null) {
      errorSpan = document.createElement("span");
      errorSpan.classList.add('error');
      this.#passwordField.closest('.form-row').appendChild(errorSpan);
    }

    if (errorSpan.innerHTML !== '') {
      errorSpan.innerHTML = errorSpan.innerHTML + ', ';
    }

    errorSpan.innerHTML = errorSpan.innerHTML + errorText;
    this.#passwordField.closest('form').querySelector('button').disabled = true;
    this.#passwordField.closest('form').querySelector('button').classList.add('disabled');
  }
}

// Note, this will initialize these values during the server side .ftl template rendering.
const minLength = ${passwordValidationRules.minLength};
const maxLength = ${passwordValidationRules.maxLength}; 
const requireMixedCase = ${passwordValidationRules.requireMixedCase?c};
const requireNonAlpha = ${passwordValidationRules.requireNonAlpha?c};
const requireNumber = ${passwordValidationRules.requireNumber?c};
                        
document.addEventListener('DOMContentLoaded', () => new FusionAuthPasswordChecker(minLength, maxLength, requireMixedCase, requireNonAlpha, requireNumber));