Email-Only Login and Strong Passwords in WordPress and WooCommerce

Quick Summary

  • WordPress has accepted an email address in the login field since version 4.5 (wp_authenticate_email_password), on wp-login.php and on the WooCommerce login form alike.
  • To allow only email addresses, remove wp_authenticate_username_password from the authenticate filter at priority 20 and change the field label with the gettext filter.
  • WooCommerce's password strength meter only runs in the browser; enforce your rules on the server with woocommerce_process_registration_errors, validate_password_reset and the checkout, account details and profile hooks.

WordPress has accepted an email address in the login form since version 4.5, so users can already log in on wp-login.php and on WooCommerce’s My account page with their email address instead of their username. To make the email address the only option, remove wp_authenticate_username_password from the authenticate filter and change the field label. To require strong passwords, WooCommerce’s strength meter is a start, but it only runs in the browser, so add a server-side check to the hooks that run whenever a password is set.

To ask for a first name, last name and a confirmed password on the WooCommerce registration form, see adding name and confirm password fields to WooCommerce registration. This post covers the login and the password rules. The code goes in your child theme’s functions.php or a small plugin, and was checked against WordPress 7.1 and WooCommerce 11.1.

Can users already log in with their email address?

Yes. WordPress hooks two functions to the authenticate filter, both at priority 20. wp_authenticate_username_password() looks the login up as a username, and wp_authenticate_email_password(), added in WordPress 4.5, looks it up as an email address when it is one. That’s why the login field on wp-login.php is labelled “Username or Email Address”.

WooCommerce’s login form (“Username or email address”) passes whatever the customer typed to wp_signon(), so the same two functions decide there too. For registration, keep Generate account login ticked in WooCommerce > Settings > Accounts & Privacy (it’s on by default). Customers then never choose a username: WooCommerce creates one from their name, or from the email address when the name can’t be used.

Make the email address the only way to log in

PHP
// Log in with an email address and password only.
remove_filter( 'authenticate', 'wp_authenticate_username_password', 20 );

// A clear message when someone types a username.
add_filter( 'authenticate', 'nmh47_require_email_login', 25, 3 );

function nmh47_require_email_login( $user, $username, $password ) {
	if ( null === $user && '' !== $username && ! is_email( $username ) ) {
		return new WP_Error( 'email_required', '<strong>Error:</strong> Please log in with your email address.' );
	}

	return $user;
}

// Relabel the login fields and drop the "try your username" hint.
add_filter( 'gettext', 'nmh47_email_login_text', 10, 3 );

function nmh47_email_login_text( $translation, $text, $domain ) {
	$new = array(
		'default'     => array(
			'Username or Email Address' => 'Email Address',
			'Unknown email address. Check again or try your username.' => 'Unknown email address. Check again.',
		),
		'woocommerce' => array(
			'Username or email address' => 'Email address',
		),
	);

	return $new[ $domain ][ $text ] ?? $translation;
}
  • remove_filter() only works with the priority the function was added with, 20. WordPress adds its default filters before it loads plugins and the theme, so the removal in functions.php sticks.
  • Without the username function, wp_authenticate_email_password() ignores anything that isn’t an email address, nothing else accepts the login, and WordPress falls back to “Invalid username, email address or incorrect password.” The function at priority 25 runs after the built-in ones and replaces that with a message that says what to do.
  • Application passwords, used for the REST API and XML-RPC, are checked by a separate function, wp_authenticate_application_password(), which still accepts a username.
  • The gettext filter swaps the label on wp-login.php (it’s also the label on the lost password form, where an email address works too), in wp_login_form() and on the WooCommerce login form, and shortens WordPress’s “Unknown email address” error, which would otherwise suggest trying a username. It matches the original English text, so it works on a translated site as well; return your own translation there. The post on changing the “Lost your password?” text explains how gettext works.

Tested with WordPress 7.1.2 and its core authenticate callbacks. Before the change, an unknown username got the “is not registered on this site” error. After it, has_filter( 'authenticate', 'wp_authenticate_username_password' ) returned false, and:

  • a username, even a real one, got “Please log in with your email address.”;
  • an empty field got WordPress’s “The email field is empty.”;
  • an unknown email address got “Unknown email address. Check again.”;
  • a registered email address with a wrong password still got the usual “The password you entered for the email address … is incorrect.” error.

What WooCommerce’s password strength meter does

On the My account registration, account details and password reset forms, and when a customer creates an account on the classic checkout, WooCommerce loads a strength meter. It rates the password with zxcvbn, the library WordPress uses, on a scale from 0 to 4 and shows WordPress’s labels: Very weak (0), Weak (1 and 2), Medium (3) and Strong (4). Below the minimum it adds “Please enter a stronger password.” and, except on the checkout, disables the form’s submit button. The minimum is 3 by default.

PHP
add_filter( 'woocommerce_min_password_strength', 'nmh47_min_password_strength' );

function nmh47_min_password_strength() {
	return 4; // 0 = very weak ... 4 = strong. WooCommerce's default is 3.
}

// On the classic checkout the meter only warns; this disables "Place order" as well.
add_filter( 'woocommerce_enforce_password_strength_meter_on_checkout', '__return_true' );

The meter is useful feedback, but not a rule. It’s JavaScript, so a request sent without it (or with JavaScript switched off) skips it, and the Checkout block removes this script and uses its own password field. zxcvbn also judges guessability, not a written policy. Run through the copy of zxcvbn that ships with WordPress 7.1, Password123! scored 1 (Weak) although it has 12 characters, a capital and a symbol, while correct horse battery staple scored 4 (Strong) with neither.

Enforce a password policy on the server

This checks every new password against your rules and stops the form with an error when it fails. The example policy asks for at least 12 characters, one upper case letter and one special character.

PHP
/**
 * The password policy. Returns what's missing, or an empty array when the password passes.
 */
function nmh47_password_problems( $password ) {
	$problems = array();

	if ( mb_strlen( $password ) < 12 ) {
		$problems[] = 'Use at least 12 characters.';
	}

	if ( ! preg_match( '/\p{Lu}/u', $password ) ) {
		$problems[] = 'Include at least one upper case letter.';
	}

	if ( ! preg_match( '/[^\p{L}\p{N}\s]/u', $password ) ) {
		$problems[] = 'Include at least one special character, such as ! ? # or @.';
	}

	return $problems;
}

/**
 * Adds an error for each problem. $password is the raw (slashed) value from the request.
 */
function nmh47_add_password_errors( $errors, $password ) {
	if ( ! is_string( $password ) || '' === $password ) {
		return; // No new password in this request.
	}

	foreach ( nmh47_password_problems( wp_unslash( $password ) ) as $problem ) {
		$errors->add( 'nmh47_weak_password', $problem );
	}
}

// WooCommerce: the registration form on My account.
add_filter( 'woocommerce_process_registration_errors', 'nmh47_check_registration_password', 10, 4 );

function nmh47_check_registration_password( $errors, $username, $password, $email ) {
	nmh47_add_password_errors( $errors, $password ); // Empty when WooCommerce sends a password setup link.
	return $errors;
}

// WordPress and WooCommerce: password reset forms, including the link new accounts get.
add_action( 'validate_password_reset', 'nmh47_check_reset_password', 10, 2 );

function nmh47_check_reset_password( $errors, $user ) {
	// wp-login.php posts pass1; WooCommerce's form posts password_1.
	nmh47_add_password_errors( $errors, $_POST['pass1'] ?? $_POST['password_1'] ?? '' );
}

// WooCommerce: "Create an account" on the classic checkout.
add_action( 'woocommerce_after_checkout_validation', 'nmh47_check_checkout_password', 10, 2 );

function nmh47_check_checkout_password( $data, $errors ) {
	if ( ! is_user_logged_in() ) {
		nmh47_add_password_errors( $errors, $_POST['account_password'] ?? '' );
	}
}

// WooCommerce: My account > Account details.
add_action( 'woocommerce_save_account_details_errors', 'nmh47_check_account_password', 10, 2 );

function nmh47_check_account_password( $errors, $user ) {
	nmh47_add_password_errors( $errors, $user->user_pass ?? '' );
}

// WordPress: the Profile, Edit User and Add New User screens.
add_action( 'user_profile_update_errors', 'nmh47_check_profile_password', 10, 3 );

function nmh47_check_profile_password( $errors, $update, $user ) {
	nmh47_add_password_errors( $errors, $user->user_pass ?? '' );
}

How it works

  • nmh47_password_problems() holds the rules, so you change them in one place. mb_strlen() counts characters, not bytes, and the \p{Lu} pattern accepts any upper case letter, not only A to Z. Anything that isn’t a letter, a digit or a space counts as a special character.
  • Each problem becomes its own error, so the visitor sees everything that’s missing at once. All five hooks hand you a WP_Error object, and adding an error to it stops the form: no account is created and no password is changed.
  • WordPress and WooCommerce pass passwords as they arrive in the request, with the slashes WordPress adds to request data (" becomes \"). wp_unslash() removes them before counting; without it, Quote"Test1 (11 characters) would count as 12.
  • The check only runs when a new password is present: WooCommerce’s registration passes an empty password when it emails a setup link, and the account details and profile screens only set user_pass when the user typed a new password.

Why these hooks and not registration_errors?

WordPress’s own registration form (wp-login.php?action=register) doesn’t ask for a password, and its registration_errors filter only receives the errors, the username and the email address. New users choose a password through the emailed link, which is the reset form, so validate_password_reset covers them. It receives the errors and the user; the password is in $_POST.

WooCommerce’s woocommerce_registration_errors filter doesn’t receive the password either ($errors, $username, $email). woocommerce_process_registration_errors runs earlier, only for the My account form, and gets $validation_error, $username, $password, $email.

The Checkout block’s optional password field is sent through WooCommerce’s Store API, and none of these hooks sees it. Leave Send password setup link ticked in the Accounts & Privacy settings (the default): the block then shows no password field, and new customers set their password on the reset form, where the check runs.

Test results

Run with PHP 8.2 against the hooks above, with slashed input as WordPress provides it:

  • password: all three problems.
  • Password1!: “Use at least 12 characters.”
  • Quote"Test1 on the WooCommerce reset form: “Use at least 12 characters.” Without wp_unslash() it passed, because the added slash made it 12 characters long.
  • correct horse battery staple: the upper case and special character errors.
  • Password123! and Zürich-Straße-2026: no errors.
  • An empty password at registration (setup link) or an account details form without a new password: no errors.

Which rules should you use?

The upper case and special character rules are what many sites ask for, but current guidance has moved away from them. NIST’s SP 800-63B-4 requires at least 15 characters for a password that is the only login factor (8 when it’s used with two-factor authentication), says not to impose composition rules such as mixing character types, and requires checking new passwords against a blocklist of common, expected or compromised passwords. The OWASP Authentication Cheat Sheet says the same. To follow it, change 12 to 15 and delete the two preg_match() checks. Password123! passing the example policy shows why the breached-password check matters; keep WooCommerce’s strength meter on as well, since it catches passwords like that one in the browser.

Use a plugin instead

If you’d rather not add code, the User Registration & Membership plugin by WPEverest (version 5.2.8 at the time of writing) has both settings. Its Login Methods option chooses between username or email, username only and email only, and each registration form has Require Strong Password with a Minimum Password Strength of Very Weak, Weak, Medium, Strong or Custom (minimum upper case letters, numbers, special symbols and length). Its settings apply to the plugin’s own forms.

Don’t combine its email-only login with the remove_filter() line above. The plugin looks the account up by email address and then signs in with the username, which WordPress no longer accepts once that filter is removed.

Share on X (opens in a new tab) Share on LinkedIn (opens in a new tab) Share on Facebook (opens in a new tab)

ChatGPT (opens in a new tab) Claude (opens in a new tab) Gemini (opens in a new tab) Perplexity (opens in a new tab) Grok (opens in a new tab)

More Insights

See More