Display number of login attempts remaining on front (WooCommerce login form)

Hi

I use Defender on one site that uses Front-end login.

The number of login attempts remaining is displayed on admin login, but not on the Front-end login form displayed by WooCommerce.

The issue is that people get banned after X attempts (as set in Defender) but don’t get warned that this will happen, leading to poor user experience.

Is there a way to add this warning (“3 login attempts remaining”) on front? I could just add a static message like “Attention: your account will be blocked after X attempts.” but that would be better to be able to show the actual remaining attempts allowed, and that’s Defender controlling this.

Any idea?

Thanks

  • Adam
    • Support Gorilla

    Hi Julien

    I hope you’re well today!

    Note: I’ve moved your ticket from Members forum (which is mostly for Members’ discussions) to our Support forum as it’s related to our Defender Pro plugin. I hope that’s fine.

    This isn’t possible “out of the box” and there are no shortocodes built-in that you could use, I’m afraid. But if Defender is blocking given form, it would mean that the failed-logins counter is increases so I think that it may be possible to do this with some custom code.

    I’ve asked our developers if it’s doable with some simple piece of code and if yes, we’ll let you know here soon how to do this so please keep track of this ticket.

    Best regards,
    Adam

  • Prashant
    • Staff

    Hi Julien

    I hope you are doing well.

    We have worked on your request and found a solution. Please add a must-use plugin to your site’s wp-content/mu-plugins folder like this https://wpmudev.com/docs/using-wordpress/installing-wordpress-plugins/#installing-mu-plugins, then add the following code to the plugin’s php file:

    remove_action( 'wp_loaded', array( 'WC_Form_Handler', 'process_login' ), 20 );
    add_action( 'wp_loaded', 'process_woo_login', 20);
    function process_woo_login(){
    	// The global form-login.php template used '_wpnonce' in template versions < 3.3.0.
    	$nonce_value = wc_get_var( $_REQUEST['woocommerce-login-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // codingStandardsIgnoreLine.
    
    	if ( isset( $_POST['login'], $_POST['username'], $_POST['password'] ) && wp_verify_nonce( $nonce_value, 'woocommerce-login' ) ) {
    
    		try {
    			$creds = array(
    				'user_login'    => trim( wp_unslash( $_POST['username'] ) ), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
    				'user_password' => $_POST['password'], // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
    				'remember'      => isset( $_POST['rememberme'] ), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
    			);
    
    			$validation_error = new WP_Error();
    			$validation_error = apply_filters( 'woocommerce_process_login_errors', $validation_error, $creds['user_login'], $creds['user_password'] );
    
    			if ( $validation_error->get_error_code() ) {
    				throw new Exception( '<strong>' . __( 'Error:', 'woocommerce' ) . '</strong> ' . $validation_error->get_error_message() );
    			}
    
    			if ( empty( $creds['user_login'] ) ) {
    				throw new Exception( '<strong>' . __( 'Error:', 'woocommerce' ) . '</strong> ' . __( 'Username is required.', 'woocommerce' ) );
    			}
    
    			// On multisite, ensure user exists on current site, if not add them before allowing login.
    			if ( is_multisite() ) {
    				$user_data = get_user_by( is_email( $creds['user_login'] ) ? 'email' : 'login', $creds['user_login'] );
    
    				if ( $user_data && ! is_user_member_of_blog( $user_data->ID, get_current_blog_id() ) ) {
    					add_user_to_blog( get_current_blog_id(), $user_data->ID, 'customer' );
    				}
    			}
    
    			// Perform the login.
    			$user = wp_signon( apply_filters( 'woocommerce_login_credentials', $creds ), is_ssl() );
    
    			if ( is_wp_error( $user ) ) {
    
    				throw new Exception( json_encode( $user->get_error_messages() ) );
    				
    				//throw new Exception( implode( ' ', $user->get_error_messages() ) );
    
    			} else {
    
    				if ( ! empty( $_POST['redirect'] ) ) {
    					$redirect = wp_unslash( $_POST['redirect'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
    				} elseif ( wc_get_raw_referer() ) {
    					$redirect = wc_get_raw_referer();
    				} else {
    					$redirect = wc_get_page_permalink( 'myaccount' );
    				}
    
    				wp_redirect( wp_validate_redirect( apply_filters( 'woocommerce_login_redirect', remove_query_arg( 'wc_error', $redirect ), $user ), wc_get_page_permalink( 'myaccount' ) ) ); // phpcs:ignore
    				exit;
    			}
    		} catch ( Exception $e ) {
    			$error_msgs = json_decode( $e->getMessage() );
    			if( is_array( $error_msgs ) ){
    				foreach ( $error_msgs as $err_key => $err_val ) {
    					wc_add_notice( apply_filters( 'login_errors', $err_val ), 'error' );
    				}
    			}else{
    				wc_add_notice( apply_filters( 'login_errors', $e->getMessage() ), 'error' );
    			}
    			do_action( 'woocommerce_login_failed' );
    		}
    	}
    }

    Alternatively, you can add the code in your child-theme’s functions.php file. We recommend to test this on the dev/staging version first before putting it on the live site.

    Hope it will solve your problem.

    Kind regards
    Prashant

  • Prashant
    • Staff

    Hi Julien

    Please replace the previously provided code with the following code:

    add_action( 'init', 'wpmudev_remove_woo_login_action' );
    function wpmudev_remove_woo_login_action(){
    	remove_action( 'wp_loaded', array( 'WC_Form_Handler', 'process_login' ), 20 );
    }
    add_action( 'wp_loaded', 'process_woo_login', 20);
    function process_woo_login(){
    	// The global form-login.php template used '_wpnonce' in template versions < 3.3.0.
    	$nonce_value = wc_get_var( $_REQUEST['woocommerce-login-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // codingStandardsIgnoreLine.
    
    	if ( isset( $_POST['login'], $_POST['username'], $_POST['password'] ) && wp_verify_nonce( $nonce_value, 'woocommerce-login' ) ) {
    
    		try {
    			$creds = array(
    				'user_login'    => trim( wp_unslash( $_POST['username'] ) ), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
    				'user_password' => $_POST['password'], // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
    				'remember'      => isset( $_POST['rememberme'] ), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
    			);
    
    			$validation_error = new WP_Error();
    			$validation_error = apply_filters( 'woocommerce_process_login_errors', $validation_error, $creds['user_login'], $creds['user_password'] );
    
    			if ( $validation_error->get_error_code() ) {
    				throw new Exception( '<strong>' . __( 'Error:', 'woocommerce' ) . '</strong> ' . $validation_error->get_error_message() );
    			}
    
    			if ( empty( $creds['user_login'] ) ) {
    				throw new Exception( '<strong>' . __( 'Error:', 'woocommerce' ) . '</strong> ' . __( 'Username is required.', 'woocommerce' ) );
    			}
    
    			// On multisite, ensure user exists on current site, if not add them before allowing login.
    			if ( is_multisite() ) {
    				$user_data = get_user_by( is_email( $creds['user_login'] ) ? 'email' : 'login', $creds['user_login'] );
    
    				if ( $user_data && ! is_user_member_of_blog( $user_data->ID, get_current_blog_id() ) ) {
    					add_user_to_blog( get_current_blog_id(), $user_data->ID, 'customer' );
    				}
    			}
    
    			// Perform the login.
    			$user = wp_signon( apply_filters( 'woocommerce_login_credentials', $creds ), is_ssl() );
    
    			if ( is_wp_error( $user ) ) {
    
    				throw new Exception( json_encode( $user->get_error_messages() ) );
    				
    
    			} else {
    
    				if ( ! empty( $_POST['redirect'] ) ) {
    					$redirect = wp_unslash( $_POST['redirect'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
    				} elseif ( wc_get_raw_referer() ) {
    					$redirect = wc_get_raw_referer();
    				} else {
    					$redirect = wc_get_page_permalink( 'myaccount' );
    				}
    
    				wp_redirect( wp_validate_redirect( apply_filters( 'woocommerce_login_redirect', remove_query_arg( 'wc_error', $redirect ), $user ), wc_get_page_permalink( 'myaccount' ) ) ); // phpcs:ignore
    				exit;
    			}
    		} catch ( Exception $e ) {
    			$error_msgs = json_decode( $e->getMessage() );
    			if( is_array( $error_msgs ) ){
    				foreach ( $error_msgs as $err_key => $err_val ) {
    					wc_add_notice( apply_filters( 'login_errors', $err_val ), 'error' );
    				}
    			}else{
    				wc_add_notice( apply_filters( 'login_errors', $e->getMessage() ), 'error' );
    			}
    			do_action( 'woocommerce_login_failed' );
    		}
    	}
    }

    Hope it will solve your problem.

    Kind Regards
    Prashant

  • Julien
    • WPMU DEV Initiate

    Well, now it only shows 1 error message,
    but it says I’m locked out after only 1 failed attempt, and checking logs and banned IP, it seems I am not (and actually I still can try again. My IP is not white-listed).

    Are those issues only on my side?

  • Adam
    • Support Gorilla

    Hi Julien

    I’ve just tested this last version of code on a test site and I couldn’t replicate that behavior.

    I tried logging-in through standard WordPress login form and through login form at WooCommerce checkout, trying to use non-existing account and then existing one but using wrong password (to trigger failed login).

    I had login limit set to 10 and the “counter” was correctly showing remaining number of available login attempts, I wasn’t also blocked/banned until I actually reached that number.

    I think there’s something additional involved here, though I’m not yet sure what. I would suggest to start with a simple thing first:

    – clear all cache on site/server
    – go to “Defender -> Firewall -> Login Protection” settings, disable it and enable again with same settings as there currently are and re-save settings
    – then clear all caches on site again.

    Let’s see if this changes anything.

    Best regards,
    Adam

  • Adam
    • Support Gorilla

    Hi Julien

    Thanks for response!

    I’ve visited that page and tried to login there to see how it works. Here’s result of 5 consecutive attempts (I used the same username so you can see it in logs):

    [attachments are only viewable by logged-in members]

    [attachments are only viewable by logged-in members]

    [attachments are only viewable by logged-in members]

    [attachments are only viewable by logged-in members]

    [attachments are only viewable by logged-in members]

    The counter is going down and eventually I got blocked, as expected. I don’t see anything wrong with it but if you are getting different results there must be something else involved in this.

    Have you tried different browsers? Is it any different in incognito vs regular browser tab?

    I obviously used a non-existing username but if you try non-existing one vs existing one (just with wrong password) is there any difference?

    Kind regards,
    Adam

  • Julien
    • WPMU DEV Initiate

    Hi Adam
    Again, thank you and Prashant for your time and kind efforts.

    It seems I’ve found an interesting point: using different browsers and incognito modes didn’t change the issue, nor trying existing VS non-existing usernames.

    What changed the result is my IP actually.
    When I tested the form with my phone on Wi-Fi, I got the same issue (show as blocked after only one try, though not really blocked), but when I turned off Wi-Fi and went 4G, it worked as expected (showing 4 remaining attempts).

    What could that mean?
    I have checked again: my IP is not listed as blocked by Defender…(and I am not blocked since I can aéccess the site).

    I hope this will help.

  • Adam
    • Support Gorilla

    Hi Julien

    Thanks for response!

    This is quite strange, I admit. I’m wondering if your IP – when you are connecting through Wi-Fi – is actually correctly recognized by WordPress/Defender.

    It should be since otherwise you would actually be locked-out but let’s better double-check it to make sure. Could you try this, please?

    1. make sure that you have Audit Logging feature enabled in Defender
    2. then e.g. add a new post on site
    3. and then check Audit Log in Defender

    You should see an entry there stating that you have created a post and if you click on that entry (expand it) it should be showing an IP address that’s supposed to be yours. Please compare it with an IP address that e.g. this service returns for you:

    https://www.whatismyip.com/

    The second thing is: who are you hosting the site with – is there any server-side cache active and is there any cache on site (like caching plugin: Hummingbird with Page Cache enabled or any other popular caching pluing)?

    Best regards,
    Adam

  • Julien
    • WPMU DEV Initiate

    Yes, I checked the IP logged by Defender matches the one I see on whatismyip.com
    The site is hosted by SiteGround and uses their own page cache (SiteGround Optimizer).

    Actually, I just tested with Page Cache and Memcached disabled and it works! I reenabled them and it still works.
    I know I already had purged the Page Cache, but not Memcached, so this might be linked to our issue.

    Thank you very much for your time and patience!

  • Adam
    • Support Gorilla

    Hi Julien

    Thanks for testing it!

    I know I already had purged the Page Cache, but not Memcached, so this might be linked to our issue.

    Yes, it would explain it. Memcached is an implementation of object cache which means it’s meant to cache “in memory object” (that can e.g. be result of some script execution and/or data fetched from DB).

    If only clearing Memcached doesn’t solve that for good, then it would be best to exclude that login page from it (actually caching all together) if it’s possible. You may, however, need to work on this with SiteGround directly as it’d be related to their configurations.

    Best regards,
    Adam