Enforce Strong Passwords Without a Plugin

Enforce strong WordPress passwords tutorial

Improve your WordPress site’s security and enforce strong passwords for all your site’s users. We’ll go through securing your site by adding just a little bit of code to your theme.

To follow this tutorial, you’ll need to be using a custom child theme, so we can make changes to functions.php. We’ll keep the code tidy by using a standalone PHP file, so it’ll be really easy to reuse it in other WordPress projects.

infoIf your site is hosted with Headwall Hosting then you don’t need to follow this tutorial, because our managed WordPress hosting packages automatically enforce strong user passwords for you.

infoTry combining this tutorial with disabling user enumeration and hiding your wp-login.php from bots to increase your website’s security even more.

Define the problem

Before we start tapping away at the keyboard, we need to be clear on what the problem is and how we’re going to solve it. We don’t need a big project plan, but if you have a clear idea of the logic-flow before you start writing code, it’ll be easier to write the code, and it will be better code – usually.

  • Prevent users of your website being able to set weak passwords.
  • Code should be in a standalone PHP file that we can just throw in to any of our WordPress projects in the future, and it’ll just work.
  • We don’t want to have any extra JavaScript dependencies if we can avoid them.

Modern versions WordPress have some built-in hooks to help us out here:

We’re just going to hook these actions, run a test on whatever password is given to us, then either reject it or accept it – based on a bit of easy-to-follow logic. We could get all clever and try to give the password a score, such as “weak”, “fair”, etc, but really all we want to do is stop anything less than a strong password.

Let’s write some code

We need somewhere to write our code, so go to the folder for your custom child theme, create a new file called functions-strong-passwords.php and paste the following into it.

<?php

/**
 * Enforce strong passwords (ESP) for site users.
 *
 * https://wp-tutorials.tech/optimise-wordpress/enforce-strong-passwords-without-a-plugin/
 *
 * To disable enforcing strong passwords:
 *   define('ESP_IS_ENABLED', false);
 */

defined('WPINC') || die();

/**
 * Initialise constants and handlers.
 */
function esp_init() {
	if (defined('ESP_IS_ENABLED') && (ESP_IS_ENABLED === false)) {
		// Disabled by configuration.
	} else {
		// Hook into WordPress here...
		// ...
	}
}
add_action('init', 'esp_init');

/**
 * Given a password, return true if it's OK, otherwise return false.
 */
function esp_is_password_ok($password, $user_name) {
	$is_ok = false;

	// ...

	return $is_ok;
}

This is the skeleton of our little module. To make sure it’s referenced by WordPress, we just need to add the following to your child theme’s functions.php file.

// Enforce strong passwords.
require_once dirname(__FILE__) . '/functions-strong-passwords.php';

These two chunks of code are the starting point for extending a custom child theme… getting our foot in-the-door. Now all we need to do is hook the three actions, and put our password-checking code in the esp_is_password_ok() function.

Enforce password strength

The function to enforce a strong password is quite simple – as it should be. Just a single function that takes a couple of input parameters and returns a true or false. Let’s take a look at it – don’t copy-and-paste this yet. The full code listing is towards the end of the post..

function esp_is_password_ok($password, $user_name) {
	// Default to the password not being valid - fail safe.
	$is_ok = false;

	$password = sanitize_text_field($password);
	$user_name = sanitize_text_field($user_name);

	$is_number_found = preg_match('/[0-9]/', $password);
	$is_lowercase_found = preg_match('/[a-z]/', $password);
	$is_uppercase_found = preg_match('/[A-Z]/', $password);
	$is_symbol_found = preg_match('/[^a-zA-Z0-9]/', $password);

	if (strlen($password) < 8) {
		// Too short
	} elseif (strtolower($user_name) == strtolower($password)) {
		// User name and password can't be the same.
	} elseif (!$is_number_found) {
		// ...
	} elseif (!$is_lowercase_found) {
		// ...
	} elseif (!$is_uppercase_found) {
		// ...
	} elseif (!$is_symbol_found) {
		// ...
	} else {
		// Password is OK.
		$is_ok = true;
	}

	return $is_ok;
}

This code should be easy to read from start to finish. The only black-art mojo is the preg_match() stuff. These funny functions are called regular expressions – a really powerful concept but they can be a bit tricky to get your head around at first. Because regular expressions can be tricky, we store the results of these functions in variable names that are easy to read. That way we can write a series of if/elseif statements that are really clean. Even a non-programmer can look at this function, see how it works and figure-out that passwords need…

  1. At least 8 characters in length.
  2. The user name and password can’t be the same.
  3. Passwords must have at least one number, one lowercase character, one uppercase and one symbol.

Putting it all together

We’ve laid out the structure of functions-strong-passwords.php and sorted out the logic for the password good-or-bad code, so let’s bring it all together. This is the complete contents of our functions-strong-passwords.php file.

<?php

/**
 * Enforce strong passwords (ESP) for all website users.
 *
 * https://wp-tutorials.tech/optimise-wordpress/enforce-strong-passwords-without-a-plugin/
 *
 * To disable enforcing strong passwords:
 *   define('ESP_IS_ENABLED', false);
 */

defined('WPINC') || die();

/**
 * Initialise constants and handlers.
 */
function esp_init() {
	if (defined('ESP_IS_ENABLED') && (ESP_IS_ENABLED === false)) {
		// Disabled by configuration.
	} else {
		add_action('user_profile_update_errors', 'esp_user_profile_update_errors', 0, 3);
		add_action('resetpass_form', 'esp_resetpass_form', 10);
		add_action('validate_password_reset', 'esp_validate_password_reset', 10, 2);
	}
}
add_action('init', 'esp_init');

function esp_user_profile_update_errors($errors, $update, $user_data) {
	return esp_validate_password_reset($errors, $user_data);
}

function esp_resetpass_form($user_data) {
	return esp_validate_password_reset(false, $user_data);
}

/**
 * Sanitise the input parameters and then check the password strength.
 */
function esp_validate_password_reset($errors, $user_data) {
	$is_password_ok = false;

	$user_name = null;
	if (isset($_POST['user_login'])) {
		$user_name = sanitize_text_field($_POST['user_login']);
	} elseif (isset($user_data->user_login)) {
		$user_name = $user_data->user_login;
	} else {
		// No user specified.
	}

	$password = null;
	if (isset($_POST['pass1']) && !empty(trim($_POST['pass1']))) {
		$password = sanitize_text_field(trim($_POST['pass1']));
	}

	$error_message = null;
	if (is_null($password)) {
		// Don't do anything if there isn't a password to check.
	} elseif (is_wp_error($errors) && $errors->get_error_data('pass')) {
		// We've already got a password-related error.
	} elseif (empty($user_name)) {
		$error_message = __('User name cannot be empty.');
	} elseif (!($is_password_ok = esp_is_password_ok($password, $user_name))) {
		$error_message = __('Password is not strong enough.');
	} else {
		// Password is strong enough. All OK.
	}

	if (!empty($error_message)) {
		$error_message = '<strong>ERROR</strong>: ' . $error_message;
		if (!is_a($errors, 'WP_Error')) {
			$errors = new WP_Error('pass', $error_message);
		} else {
			$errors->add('pass', $error_message);
		}
	}

	return $errors;
}

/**
 * Given a password, return true if it's OK, otherwise return false.
 */
function esp_is_password_ok($password, $user_name) {
	// Default to the password not being valid - fail safe.
	$is_ok = false;

	$password = sanitize_text_field($password);
	$user_name = sanitize_text_field($user_name);

	$is_number_found = preg_match('/[0-9]/', $password);
	$is_lowercase_found = preg_match('/[a-z]/', $password);
	$is_uppercase_found = preg_match('/[A-Z]/', $password);
	$is_symbol_found = preg_match('/[^a-zA-Z0-9]/', $password);

	if (strlen($password) < 8) {
		// Too short
	} elseif (strtolower($user_name) == strtolower($password)) {
		// User name and password can't be the same.
	} elseif (!$is_number_found) {
		// ...
	} elseif (!$is_lowercase_found) {
		// ...
	} elseif (!$is_uppercase_found) {
		// ...
	} elseif (!$is_symbol_found) {
		// ...
	} else {
		// Password is OK.
		$is_ok = true;
	}

	return $is_ok;
}

There you have it. Run some tests and try to set a weak password for one of your users – you should see that it won’t let you do it. Win!

image-4317040
Password too weak

No more users with “password1234” for their logins ✳✳✳✳⁉ 👍

Automatically generate a strong password

Although the built-in WordPress function for generating passwords supports two groups of special characters, it doesn’t enforce the requirement that a password includes at least one character from each of these sources. So let’s look at creating our own function to do that.

If you look at the source code for wp_generate_password(), you’ll see that the newly generated password is rinsed through a filter called random_password. We can hook this, discard the password that WordPress created and replace it with our own.

Go back into “functions-strong-passwords.php” and add the following snippet to the end of the code:

/**
 * Replace the built-in password generator.
 *
 * Create multiple character pools. Each password contains at least one random
 * character from each pool.
 */
function esp_random_password($password = '', $length = 12, $special_chars = true, $extra_special_chars = false) {
	$character_pools = array(
		'abcdefghijklmnopqrstuvwxyz',
		'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
		'0123456789',
	);

	if ($special_chars) {
		$character_pools[] = '!@#$%^&*()';
	}

	if ($extra_special_chars) {
		$character_pools[] = '-_ []{}<>~`+=,.;:/?|';
	}

	$available_pool_count = count($character_pools);

	// Build an array of characters for the new password.
	$password_characters = array();

	for ($index = 0; $index < $length; ++$index) {
		// Pick a character pool...
		$pool_index = $index;
		if ($index >= $available_pool_count) {
			$pool_index = random_int(0, $available_pool_count - 1);
		}

		// How many characters are in that pool?
		$characters_count = strlen($character_pools[$pool_index]);

		// Pick a character from the chosen pool.
		$character_index = random_int(0, $characters_count - 1);

		// Append the character to the array of password characters.
		$password_characters[] = $character_pools[$pool_index][$character_index];
	}

	// Randomise the array of characters.
	// NOTE: If you're using PHP8.2, you should use \Random\Randomizer instead of shuffle().
	// INFO: https://www.php.net/manual/en/function.shuffle.php
	shuffle($password_characters);

	// Convert the randomised array of characters to a string.
	return implode('', $password_characters);
}
add_filter('random_password', 'esp_random_password', 100, 4);

As the password is being constructed in the for(...) loop, we make sure that each character pool is used at least once. So, as long as $special_chars is enabled, every password should pass the tests in esp_is_password_ok().

There we go – much better 👍

Like This Tutorial?

Let us know

WordPress plugins for developers

13 thoughts on “Enforce Strong Passwords Without a Plugin”

  1. 1. It still allows the USER to add a Weak Password from a Reset or Forgot Password Link.
    2. This disables the ability for an admin to send a Reset Link.

    Reply
    • I’ve been thinking about rewriting this tutorial – cleaning it up and improving its core. I think I’ll revisit it next week with a version 2.0.
      Cheers for the heads-up.

      Reply
  2. Thanks for the code. I was searching around for a solid solution and yours is by far the best.
    All other search results were:
    + Hide the “confirm weak password”-checkbox via css
    + Remove the checkbox with jquery
    + install some plugin were you don’t need any other feature from

    Also good code quality, very readable. I like it!

    Reply
  3. This is really useful, and a nice well-written tutorial to learn some WP inner-workings to boot!
    I’m learning those are REALLY hard to find — so, Thxalot !! o/

    The code works as advertised — enforcing the user’s Password entry against the set criteria.

    But, the WP password form still includes a “generate password” button — that doesn’t seem to follow the policy I set in this^^ code.

    Is it possible link the two? So that the generated/offered password matches/exceeds the policy?

    Reply
    • I’m glad you liked the tutorial.

      I’ve looked into your query and rewritten the WordPress wp_generate_password() function in a new section at the end of the tutorial – esp_random_password(). It looks good from here – every password that WordPress generates should include at least one of each type of character now. That should give your users a better experience.

      Reply
  4. Hi,

    Thanks a lot for the additional password policy bits. This is great 🙂

    Your code’s easily modified, and after digging around in the ‘pluggable.php’ WP sources, I’m figuring out how flexible ‘hooking’ can be!

    I’m now generating passwords that comply with my policy, so that’s cool.

    If I’ve enabled special characters use, when I ‘register’ a new user, and WP sends an email with the “set my password” link, the link itself includes special characters — and reports that it’s “invalid” when I try to use it.

    If I disable this^^ code, going back to unmodified WP-standard password generation, then all’s good again.

    I know I probably need a condition in here somewhere that modifies the password generation, but leaves the password (re)set link generation UNmodified (that seems to use only upper- & lower-case alpha characters, no special?).

    Any hints where to separate that condition out?

    Reply

Leave a comment