How can I be certain that a user has verified their email after registration?

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

Leave a Comment