6

Background

I'm attempting to make a seemless frontend login for a blog, while still leaving wp-login.php available to those users who know how to get there. wp-login.php is also required to authenticate frontend logins, so it cannot be removed completely.

I'm almost there, but I have one issue with login failures, specifically with an empty username.

Question

Does anybody know a way to both stop wp-login.php being displayed for empty username/password errors, while avoiding an error being displayed for the same reason when a user goes directly to wp-login.php?

My work

I located the wp_login_failed hook and created the function below. Basically it adds query args to say that the login has failed and what username was entered before redirecting the user back to the frontend login. With the query args I can then determain why the login failed and display a message to the user.

add_action('wp_login_failed', 'djg_front_end_login_fail');
function djg_front_end_login_fail($username){

    $referrer = (isset($_SERVER['HTTP_REFERER'])) ? $_SERVER['HTTP_REFERER'] : $_SERVER['PHP_SELF'];
    $referrer = add_query_arg('result', 'failed', $referrer);
    $referrer = add_query_arg('username', $username, $referrer);

    if(!empty($referrer) && !strstr($referrer, 'wp-login') && !strstr($referrer, 'wp-admin')) :
        wp_redirect($referrer);
        exit;
    endif;

}

However, I noticed that if the username field was empty I was not being redirected and instead the user was still being shown wp-login.php.

Upon further inspection I found the wp_authenticate() function where upon I noticed that if the first login error was either empty_username or empty_password, the wp_login_failed action hook was not added. To counter this, I ustilised the authenticate filter hook and actually changed the code of the aforementioned errors, prepending djg_ to them, so that they were not ignored.

add_filter('authenticate', 'djg_authenticate_login', 99, 3);
function djg_authenticate_login($user, $username, $password){;

    if(is_wp_error($user)) :

        $codes = $user->get_error_codes();
        $messages = $user->get_error_messages();

        $user = new WP_Error;

        for($i = 0; $i <= count($codes) - 1; $i++) :

            $code = $codes[$i];
            if(in_array($code, array('empty_username', 'empty_password'))) :
                $code = 'djg_' . $code;
            endif;

            $user->add($code, $messages[$i]);

        endfor;

    endif;

    return $user;

}

This works, but now if the users goes directly to the wp-login.php page, which has to remain available (as described in Background), an error is displayed -

ERROR: The username field is empty.
ERROR: The password field is empty.

Finally, after much hair pulling, I found in the wp_signon() function the code that was causing this, but to my eye I cannot see any filters that allow me to change the behaviour anywhere around the snippit. Basically it's saying that if the errors are exactly empty_username and empty_password, ignore them. -

if ( is_wp_error($user) ) {
    if ( $user->get_error_codes() == array('empty_username', 'empty_password') ) {
        $user = new WP_Error('', '');
    }

    return $user;
}

Details

  • wp_authenticate() is located in /wp-includes/pluggable.php
  • The authenticate filter hook is found within the wp_authenticate() function
  • The wp_login_failed action hook is found within the wp_authenticate() function
  • wp_signon() is located in /wp-includes/user.php

Question (again)

Does anybody know a way to both stop wp-login.php being displayed for empty username errors, while avoiding an error being displayed for the same reason when a user goes directly to wp-login.php?

8

WordPress handles login failed in two ways:

  1. If it is a bad credential, and both username and password have a value, then this action can be captured by wp_login_failed
  2. If both, or one, of the options are empty, then WordPress generates the error object as the first parameter in the authenticate filter; it does not open and wp_login_failed action captures this cause/event

For what we have done here, see comments in code:

add_filter( 'authenticate', function( $user, $username, $password ) {
    // forcefully capture login failed to forcefully open wp_login_failed action, 
    // so that this event can be captured
    if ( empty( $username ) || empty( $password ) ) {
        do_action( 'wp_login_failed', $user );
    }
    return $user;
}, 10, 3 );

// to handle even you can handle the error like
add_action( 'wp_login_failed', function( $username ) {
    if ( is_wp_error( $username ) ) {
        // perform operation on error object for empty error
    }
} );
  • 3
    Upvoted as a good answer but please correct the issue with the authenticate hook. Without passing a priority and number or arguments, this will always trigger a failed login action due to the way filters and hooks work (by default, they only allow one argument to be passed to the method). – Tom Dyer Mar 6 '18 at 12:33
  • The $username parameter in wp_login_failed is a string according to the developer docs for wp_login_failed - how do we get an error object from it now? – James Cushing Sep 15 '18 at 1:19
  • 1
    @TomDyer is right, deducing from the question the correct statement should be add_filter( 'authenticate', function(...) {...}, 99, 3); – GigiSan Oct 17 '18 at 9:58

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.