Need help with forcing recovery based on user login

I have a multisite that's using multiple accounts with the same email address so I have to force password recovery based only on username. Can you help me with this?

  • Antoine
    • WPMU DEV Initiate

    To be bit more specific :
    - Logins (user_login) are unique while there might be same email across the network
    - I need users to be able to recover their password for a given (current) blog either by providing their emails or user names
    - I've modified the login pages urls/redirections so that login forms are handled within the blog they want their password reset
    - If users provide an email, I can successfully retrieve their user_mame for the current blog, by hooking on "allow_password_reset" filter. (btw, existing user control is fired before so that I don't have to handle it).

    But what I'm missing is probably more trivial than the whole subject
    In short : How can I replace the submitted email by the retrieved user_name before Wordpress process the form and send the link ?.

    Below is an abstract of current testing code implemented in my child theme function.php.
    It is currently set to display an error message (that's noob debugging) instead of proceeding as mentioned above and it is not performing yet a test against submitted login (email or not) but you'll get the idea ...

    Thanks in advance for any help, sorry for my weird English,
    Thanks Michelle for posting OP,
    Antoine

    function myplugin_check_fields($allow, $user_id)
    {
    
        $usremail = get_user_by('id',$user_id)->user_email;
        $args = array(
            'search' => $usremail,
            'search_columns' => array( 'user_email' )
        );
    
        // The Query
        $user_query = new WP_User_Query( $args );
    
        // User Loop : as we are on the desired blog, WP_User_Query should only return blog member login (normally, only one, will also handle that later).
        $usrlogin ="";
        if ( ! empty( $user_query->get_results() ) ) {
            foreach ( $user_query->get_results() as $user ) {
                $usrlogin = $user->user_login ;
            }
        }
       // Just to test/view returned value (user_login) against submitted value (user_email)
        return new WP_Error('demo_error', __('<strong>ERROR</strong>: This is a demo error.', 'network-text-domain'). '<br> Submitted : '. $usremail.  '<br> To use : ' . $usrlogin);
    
      // Here (insead of error)I should "do something" to replace submitted email ($usremail) with user_login ($usrlogin)
         [...]
      // Then (probably) return (True,$user_id) so that the allow_password_reset filter succeeds.
    }
    
    add_filter('allow_password_reset', 'myplugin_check_fields', 10, 2);
  • Adam Czajczyk
    • Support Gorilla

    Hi Antoine

    I hope you're well today and thank you for your question!

    We've already asked our developers for advice on this so we'll update you here as soon as we get to know more from them. Please keep an eye on this ticket for further information but also in case we had some additional questions.

    Best regards,
    Adam

  • Antoine
    • WPMU DEV Initiate

    Adam, FYI, I'm working on a possible solution based on custom login forms.
    I believe this will offer a more easy environment to play with.
    So, the form may fail if you visit it from now, let me know if that's a problem (anyway, all the code is above).

  • Adam Czajczyk
    • Support Gorilla

    Hey Antoine

    Thank you for letting me know and keeping me updated!

    I'm sorry for keeping you waiting also. Our developers are dealing with a lot of complex stuff on daily basis (bug fixing, improvements and new features and so on) so their response time might be a bit longer than ours here on support forum but I just asked them again if they could give it a spin and let us know here as soon as they possibly can.

    Best regards,
    Adam

  • Panos
    • SLS

    Hi Antoine !

    Apologies for delay here! Not sure it is a good idea to use same emails for different users, as it will probably end up giving several issues in WordPress' core functions to insert/update users. Regarding your request, you could add a check if the input is an email to return an error message like:

    add_action( 'lostpassword_post', function( $errors ){
    
    	if ( ! empty( $_POST['user_login'] ) && is_email( $_POST['user_login'] ) ) {
    		$errors->add( 'invalid_input', __( '<strong>ERROR</strong>: Please insert your username instead of your email' ) );
    	}
    
    } );

    Hope this helps!

  • Antoine
    • WPMU DEV Initiate

    Hi Panos !
    Thank you for the response.
    Point is I cannot do that because, when user first register, he receives an email with username and pw. => yes, I know, its bad as mud security wise; but that's B2B and mandatory from the client's specs.
    So, chances are that if the user can find his login, same will apply for his password.

    So, what I need instead is :
    1. if the login is an email, I found the current blog username (if any)
    2. I replace the email sent by the form with the username
    3. I send the form with the username.

    Finally, it's nothing but changing the value of submitted "user_login" with my var $usrlogin (see code above) ...

  • Panos
    • SLS

    Actually this is one of the cases where it will cause issues and we don't recommend choosing such paths. WordPress has a main users table where it does the SELECTs queries. Once in a subsite's forgot-password page
    (by default it will load the main site's screen), it will redirect to main site after submit, as the form's action is hard-coded without any filter. So there is no way to check which blog_id you were on, neither in the lostpassword_post nor the login_form_lostpassword action hooks. Only way to achieve this is to use hack methods non-WordPress way though which we don't support (we only support WordPress). For such custom solutions you can hire a dev from our partners page :
    https://premium.wpmudev.org/partners/#wpmud-hg-discounts-services

    Kind regards!

  • Antoine
    • WPMU DEV Initiate

    Well, I thougjt didn't really need the blog Filter (even if I do have redirections for the forms) because user IDs and logins are different.
    Do you mean that WP core will always use emails to perform the search against users?

  • Panos
    • SLS

    Not sure I can explain in it properly, but I'll do my best :slight_smile:

    Do you mean that WP core will always use emails to perform the search against users?

    WordPress gets the input given in the username/email field and uses :
    $user_data = get_user_by( 'email', trim( wp_unslash( $_POST['user_login'] ) ) );
    to fetch user info. If this function is called from a subsite it will fetch the user if he exists in subsite, however this call is been done in main site during the password retrieval action which is not helpful for what you need. You need to fetch per sub-site if I understood correctly.

    Whenever you are at the login page of a subsite eg:
    http://subdomain.site.com/wp-login.php for subdomain installations or
    http://site.com/subdir/wp-login.php for subdirectory installations,
    once you click on the "Lost password" link it will redirect to the main site's page to reset the password:
    http://site.com/wp-login.php?action=lostpassword

    So from that point there is no info to use for the blog id. Even if you add the subdomain/subdirectory in the losttpassword's url, the form's action is hard-coded to the main site's lostpassword page. To be exact, at this point we can probably store the blog id on a very early action in the $_POST variable, but there are 2 issues:
    1. We need to force to load the lostpassword page for the subsite (by default it loads the main site's as explained above)
    2. If wrong input given, the second try will be for main site.

    That's why the best way would be to use custom methods and page to achieve what you need.

    Not sure if I managed to point out why this is hard to make it work, hope I gave a slight idea though :slight_smile:

  • Antoine
    • WPMU DEV Initiate

    Thank you, Panos,
    login pages are forced for the subsite using this "plugin" (actually it's in my theme function.php)
    [code]
    /**
    * Plugin Name: Multisite: Password Reset on subsite
    * Plugin URI: https://gist.github.com/eteubert/293e07a49f56f300ddbb
    * Description: By default, WordPress Multisite uses main site of a network for password resets.
    * This plugin enables users to stay in their subsite during the whole reset process.
    * Version: 1.0.0
    * Author: Eric Teubert
    * Author URI: http://ericteubert.de
    * License: MIT
    */

    // fixes "Lost Password?" URLs on login page
    add_filter("lostpassword_url", function ($url, $redirect) {
    $args = array(
    'action' => 'lostpassword'
    );

    if (! empty($redirect))
    $args['redirect_to'] = $redirect;
    return add_query_arg($args, site_url('wp-login.php'));
    }, 10, 2);

    // fixes other password reset related urls
    add_filter('network_site_url', function ($url, $path, $scheme) {

    if (stripos($url, "action=lostpassword") !== false)
    return site_url('wp-login.php?action=lostpassword', $scheme);

    if (stripos($url, "action=resetpass") !== false)
    return site_url('wp-login.php?action=resetpass', $scheme);

    return $url;
    }, 10, 3);
    // fixes URLs in email that goes out.
    add_filter("retrieve_password_message", function ($message, $key) {
    return str_replace(get_site_url(1), get_site_url(), $message);
    }, 10, 2);
    // fixes email title
    add_filter("retrieve_password_title", function ($title) {
    return "[" . wp_specialchars_decode(get_option('blogname'), ENT_QUOTES) . "] " . __('Password Reset');
    });
    [/code]

    But I believe my problem is somewhere else. In fact I'm trying to hook on filter "allow_password_reset" to replace the submitted by user "login" (either email or user login) with their user_login.
    But this has to be done before WP search for the user (function "retrieve_password()" in wp-login.php. And that would hack the whole process and - I agree - probably create a ton of user cases madness.

    So I'll go the custom form way ... and keep you posted. Thank you for the weekend work !

  • Antoine
    • WPMU DEV Initiate

    ** EDITED 19:19 PM (Paris time) **
    There was a flaw in my logic. Must check first if submitted login is an email. Corrected.
    *And* : you were damn right about the approach, just added a query to retrieve blogs/users names

    And finally ...
    As often, re-thinking the feature makes things easier.
    So, I'm in "done is better than perfect" scenario, so that I revised the feature as in :
    "If the user is registered in more than a blog, tell him to choose the login he wants the PW to be reset" . Easier uh ? :wink:

    With that in mid, I use the error handling in the filter "allow_password_reset" to display a blog/login list and the user will have to pick the desired login. Not perfect, but no custom form, just a hook ...
    Here you go, any comment very welcome. As you can guess, php is not my primary coding language ... :

    function myplugin_check_fields($allow, $user_id)
    {
    
        if(! empty( $_POST['user_login'] ) && is_email( $_POST['user_login'] )) { // We only perform the search if user submitted an email
            global $wpdb;
            $usremail = get_userdata($user_id)->user_email; //why not $_POST['user_login'] : welle, not sure of all security problems this could add; probably need to sanitize etc ...
            $rootprefix = $wpdb->base_prefix;
    
            // The Query
            $user_SQL = 'SELECT U.user_login, UM.meta_value AS blog_ID FROM ' .
                $rootprefix . 'users AS U INNER JOIN ' .
                $rootprefix . 'usermeta AS UM ON U.id = UM.user_id inner join '.
                $rootprefix . 'blogs B on UM.meta_value = B.blog_id where meta_key = \'primary_blog\' and user_email = \'' . $usremail .'\'';
    
            $user_query = $wpdb->get_results( $user_SQL, 'ARRAY_A' );
    
            // User logins Loop
            if ( count($user_query) > 1 ) {
                $usrlogins ="You have several accounts attached to this email please copy/paste the one you want to reset below.<br><br>";
                foreach ( $user_query as $user ) {
                    $blog_details = get_blog_details($user['blog_ID']);
                    $usrlogins .= '<strong>'.$blog_details->blogname . '</strong> : ' . $user['user_login'] . '<br>' ;
                }
                return new WP_Error('Error_multiLogin', $usrlogins);
            }
            return(true);
        }
    
        return(true);
    }
    add_filter('allow_password_reset', 'myplugin_check_fields', 10, 2);

    Unless there's something blatantly ugly in this code ... I believe I'm done with this one :slight_smile:
    Please confirm it's ok, then I'll close the topic.

    B.R
    Antoine

  • Panos
    • SLS

    i don't think that would work correctly. The allow_password_reset filter expects a Boolean to be returned instead of a WP_Error object:
    https://core.trac.wordpress.org/browser/tags/5.2/src/wp-includes/user.php#L2255

    I can't really suggest something that is 100% correct in this case. Maybe if the user would reach to that page from the subsite's login page and use the referrer url, we could try get blog id from url. That could happen on the login_footer for example, and once you get the blog id, you can store it in a cookie or inject it in a custom form field with some js.

    I don't know if this would be a working scenario for you, however I'll provide a simple way to retrieve the blog id, however it is not well tested and you would need to double triple test. Then you can try adding it in a cookie or form field and use it in the lostpassword_post hook :

    add_action( 'login_footer', function(){
    
    	$referrer = function_exists( 'wp_get_referrer' ) ? wp_get_referrer() : '';
    	$blog_id = false;
    
    	if ( empty( $referrer ) ) {
    
    		if ( isset( $_SERVER['HTTP_REFERER'] ) ) {
    			$referrer = $_SERVER['HTTP_REFERER'];
    		} else {
    			return;
    		}
    
    	}
    
    	if (  strpos( $referrer, 'action=lostpassword' ) !== false ) {
    		return;
    	}
    
    	$domain = preg_replace('/https?:\/\/(www\.)?/', '', $referrer);
    	if ( strpos($domain, '/') !== false ) {
    		$explode = explode('/', $domain);
    		$domain  = $explode['0'];
    	}
    
    	if ( is_subdomain_install() ) {
    		$blog_id = get_blog_id_from_url( $domain );
    	} else {
    		$site_url = trailingslashit( site_url() );
    		$parts = wp_parse_url( $referrer );
    		$subdir = explode( '/', str_replace( $site_url, '', $referrer ) )[0];
    		$path = explode( $subdir, $parts['path'] ) [0] . "{$subdir}/";
    		$blog_id = get_blog_id_from_url( $parts[ 'host' ], $path );
    	}
    
    	if ( $blog_id ) {
    		// You can store it in cookie or add custom form field with js
    		error_log('$blog_id : '. $blog_id );
    	}
    
    } );

    Not sure that would work, but seems like worth to try. Once you have sent the blog id to the lostpassword_post callback, you can use the switch_to_blog( $blog_id ) function and the retrieve the user by email using the get_user_by function:
    https://developer.wordpress.org/reference/functions/get_user_by/

    This is a really hacked way to do this and would still insist on not using the same email address for multiple users.

  • Antoine
    • WPMU DEV Initiate

    Hi Panos,
    I'm confused ...
    doesn' this (line 2259) :

    if ( ! $allow ) {
    2258	                return new WP_Error( 'no_password_reset', __( 'Password reset is not allowed for this user' ) );
    2259	        } elseif ( is_wp_error( $allow ) ) {
    2260	                return $allow;
    2261	        }

    mean that if the returned value is a WP_error it has to be returned (with the same behavior than if the result / returned value was false ) ?

    P.S: I've tested it live and I do get the error (blog/user_login list) and of course no validation.
    Do I miss something important ?

  • Antoine
    • WPMU DEV Initiate

    THANK YOU Panos
    Ok then, I'll keep this "dirty hack" as-is.
    P.S : I know the whole story is bad practice, but client's king. Be sure all (written) warnings and disclaimers have been issued though :slight_smile:.

    Key point : I have to notify user with his username and pass when he registers in the blog.
    AFAIK, there's no way to retrieve existing password in clear text so that it'll mean that I'll have to change "root user" password and client do not want the existing (blog) PW(s) to be changed ...
    Maybe I should have started asking before coding ... but hey, you know how we're built :wink:

  • Antoine
    • WPMU DEV Initiate

    Ok, final "build". I've added some extra logic to display only the current blog login. As I use redirections for my forms, others won't work anyway.

    function myplugin_check_fields($allow, $user_id)
    {
    
        if(! empty( $_POST['user_login'] ) && is_email( $_POST['user_login'] )) {
            global $wpdb;
            $usremail = get_userdata($user_id)->user_email;
            $rootprefix = $wpdb->base_prefix;
    
            // The Query
            $user_SQL = 'SELECT U.user_login, UM.meta_value AS blog_ID FROM ' .
                $rootprefix . 'users AS U INNER JOIN ' .
                $rootprefix . 'usermeta AS UM ON U.id = UM.user_id inner join '.
                $rootprefix . 'blogs B on UM.meta_value = B.blog_id ' .
                'where meta_key = \'primary_blog\' and user_email = \'' . $usremail .'\'';
    
            $user_query = $wpdb->get_results( $user_SQL, 'ARRAY_A' );
    
            // User logins Loop
            if ( count($user_query) > 1 ) {
                $usrlogins ="Vous possédez plusieurs identifiants rattachés à cette adresse email, veuillez copier/coller <span style='color:orange;'>l'identifiant ci-dessous</span> en lieu et place de votre email et re-soumettre le formulaire.<br><br>";
                foreach ( $user_query as $user ) {
                    $blog_details = get_blog_details($user['blog_ID']);
                    if ($blog_details->blogname == get_blog_details()->blogname){
                        $usrlogins .= '- <strong>'.$blog_details->blogname . '</strong> : <span style="color:orange;">' . $user['user_login'] . '</span><br>' ;
                    }
                }
                return new WP_Error('Error_multiLogin', $usrlogins);
            }
            return(true);
        }
    
        return(true);
    }
    add_filter('allow_password_reset', 'myplugin_check_fields', 10, 2);

    Again, thx Panos for your help.

Thank NAME, for their help.

Let NAME know exactly why they deserved these points.

Gift a custom amount of points.