7

I'm working on a plugin that uses WordPress as the Single Sign On provider for another application. I need to be certain that the user has verified their email address by replying to the email sent out by wp_new_user_notification().

So far the best approach I have found is to hook into the after_password_reset action and add user_metadata to indicate that the email is verified. This works, but all it's really telling me is that the reset_password function has been called.

Is there a better way to do this?

  • 2
    Maybe empty( get_userdata( $user_id )->user_activation_key ) ? – Samuel Elh Jul 9 '16 at 1:29
  • It looks like that is still just indicating that reset_password has been called. (reset_password calls wp_set_password, that's where the activation key gets set to ''.) – Simon Cossar Jul 9 '16 at 2:23
  • 1
    What about a custom counter of password resets for each user, stored in the user meta ? – birgire Jul 9 '16 at 2:59
3

I've tried a few different approaches for verifying the user's email. For now, what I am doing is this:

When a user first registers, set the user's user_metadata 'email_not_verified' to 1.

add_action( 'user_register', 'sc_user_email_not_verified' );
function sc_user_email_not_verified( $user_id ) {
  update_user_meta( $user_id, 'email_not_verified', 1 );
}

Then, override the wp_new_user_notification function so that it adds an 'email_verification_key' to the login url. It also saves that key as user_metadata.

function wp_new_user_notification( $user_id, $depreciated = null, $notify = '' ) {

...

$email_verification_key = wp_generate_password( 20, false );
update_user_meta( $user_id, 'email_verification_key', $email_verification_key );

$message = sprintf(__('Username: %s'), $user->user_login) . "\r\n\r\n";
$message .= __('To set your password, visit the following address:') . "\r\n\r\n";
$message .= '<' . network_site_url("wp-login.php?action=rp&key=$key&mail_key=$email_verification_key&login=" . rawurlencode($user->user_login), 'login') . ">\r\n\r\n";
$message .= wp_login_url() . "\r\n";

wp_mail($user->user_email, sprintf(__('[%s] Your username and password info'), $blogname), $message);
}

Then, hook into the 'validate_password_reset' action to check that the email verification key from the password reset request matches the saved key. If the keys don't match, delete the user and redirect them back to the registration form with an error of 'emailnotverified'. If the keys do match, delete the 'email_not_verified' metadata.

add_action( 'validate_password_reset', 'sc_verify_user_email', 10, 2 );
function sc_verify_user_email( $errors, $user ) {
    if ( isset( $_REQUEST['mail_key'] ) ) {
        $email_verification_key = $_REQUEST['mail_key'];
        $saved_key              = get_user_meta( $user->ID, 'email_verification_key', true );

        if ( ! ( $email_verification_key === $saved_key ) ) {
            require_once( ABSPATH . 'wp-admin/includes/user.php' );
            wp_delete_user( $user->ID );
            wp_redirect( network_site_url( "wp-login.php?action=register&error=emailnotverified" ) );
            exit;
        } else {
            delete_user_meta( $user->ID, 'email_not_verified' );
        }
    }
}

If the email is not verified, add a message that will be displayed on the registration page when there is an 'emailnotverified' error.

add_filter( 'login_message', 'sc_email_not_verified_message' );
function sc_email_not_verified_message() {
    $action = isset( $_REQUEST['action'] ) ? $_REQUEST['action'] : '';
    $error = isset( $_REQUEST['error'] ) ? $_REQUEST['error'] : '';

    if ( 'register' === $action && 'emailnotverified' === $error ) {
        $message = '<p class="message">' . __( 'Your email address could not be verified. Please try registering again.' ) . '</p>';
        return $message;
    }
}

Inside of the Single Sign On function, if the user has the 'email_not_verified' metadata, don't log them in to the client application. (This could happen if the user was created through a registration form that was added by a plugin.)

$current_user = wp_get_current_user();
if ( get_user_meta( $current_user->ID, 'email_not_verified', true ) ) {
    echo( 'Invalid request.' ); // This needs to be changed to a redirect.
    exit;
}

I've also added a checkbox to display and override the user's email verification status on the 'user-edit' page.


Edit:

Hooking into the validate_password_reset action is probably not the best way to do this. It is called before the password is reset, so the email will be verified even if there are errors in the password reset process (for example if the key is expired or invalid.)

A better approach seems to be to hook into the resetpass_form action and add a hidden field to the password reset form that holds the value of the 'mail_key':

add_action( 'resetpass_form' 'sc_mail_key_field' );
function sc_mail_key_field() {

    if ( isset( $_REQUEST['mail_key'] ) ) { 

        $mail_key = sanitize_key( wp_unslash( $_REQUEST['mail_key'] ) );
        wp_nonce_field( 'verify_email', 'verify_email_nonce' );
        echo '<input type="hidden" name="mail_key" value="' . esc_attr( $mail_key ) . '" />';
    }
}

It is then possible to hook into the after_password_reset action to verify the saved mail key against the $_POST['mail_key'] value.

An example plugin can be found here: email address verification plugin

  • 1
    Thanks for sharing what worked for you. PS: I would suggest prefixing custom meta data to avoid possible name collision with other plugins and it's recommended to add exit; after wp_redirect(). – birgire Jul 11 '16 at 13:33
  • Thanks, I edited the code on here to add exit; after wp_redirect(). In the code I am actually using, I am no longer deleting a user if their 'mail_key' doesn't match the saved key. Instead, if the keys don't match, the user is created, but with the 'email_not_verified' flag in place. – Simon Cossar Jul 12 '16 at 19:11
2

I had a look into the wp_usermeta table and noticed the default_password_nag meta key.

I checked and this was introduced in #9710 about 7 years ago.

If the user has an auto generated password then it's value is 1 and she will get a notice displayed on the dashboard screen.

When she first registers, default_password_nag is 1 and when she resets the password, following the email link, it becomes 0.

You can look further into this, if it can be of any use in your setup.

Otherwise one could create a custom counter or flag and store it in the user meta. E.g. hook into the user creation process and then update it when the user resets the password or logs in.

  • The problem is that it's possible for a plugin to allow the password to be reset without having the user respond to an email to reset it. WooCommerce does this if you select 'enable registration.' Possibly, it might be good enough to know that the password was reset and that the resetting was handled by wp-login.php. – Simon Cossar Jul 9 '16 at 16:06
  • yes you're correct, plugins could affect the state of it, e.g. through custom login/reset forms. Most likely a custom counter/flag might be better in that case. The extreme case would be a plugin with user creation and password resets through custom SQL queries ;-) @SimonCossar – birgire Jul 9 '16 at 21:02

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.