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?
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