Passwordless Login With an Email Link
Adds an "Email me a login link" option to the login screen; members enter their email or username and get a one-time link that logs them in for 15 minutes.
Opens WPIDE on
Code
// Minutes a login link stays valid.
$wpide_magic_minutes = 15;
// Roles that may not use login links.
$wpide_magic_excluded_roles = array( 'administrator' );
add_action( 'login_form', function () {
echo '<p style="margin:0 0 16px"><a href="' . esc_url( add_query_arg( 'action', 'wpide_magic', wp_login_url() ) ) . '">Email me a login link instead</a></p>';
} );
add_action( 'login_form_wpide_magic', function () use ( $wpide_magic_minutes, $wpide_magic_excluded_roles ) {
if ( ! function_exists( 'login_header' ) ) {
return;
}
$sent = false;
if ( 'POST' === ( $_SERVER['REQUEST_METHOD'] ?? '' ) && isset( $_POST['wpide_magic_nonce'] ) && wp_verify_nonce( sanitize_key( $_POST['wpide_magic_nonce'] ), 'wpide_magic' ) ) {
$login = isset( $_POST['wpide_magic_login'] ) ? sanitize_text_field( wp_unslash( $_POST['wpide_magic_login'] ) ) : '';
$user = is_email( $login ) ? get_user_by( 'email', $login ) : get_user_by( 'login', $login );
$sent = true; // Same message either way, so nobody can test which accounts exist.
$recent = $user ? (int) get_user_meta( $user->ID, 'wpide_magic_sent', true ) : 0;
if ( $user && ! array_intersect( $wpide_magic_excluded_roles, (array) $user->roles ) && time() - $recent > MINUTE_IN_SECONDS ) {
$token = wp_generate_password( 40, false );
update_user_meta( $user->ID, 'wpide_magic_token', array( 'hash' => wp_hash( $token ), 'expires' => time() + $wpide_magic_minutes * MINUTE_IN_SECONDS ) );
update_user_meta( $user->ID, 'wpide_magic_sent', time() );
$link = add_query_arg( array( 'action' => 'wpide_magic_login', 'uid' => $user->ID, 'token' => $token ), wp_login_url() );
wp_mail(
$user->user_email,
sprintf( '[%s] Your login link', wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ) ),
sprintf( "Hi %s,\n\nClick this link to log in. It works once and expires in %d minutes:\n%s\n\nIf you did not ask for it, you can ignore this email.", $user->display_name, $wpide_magic_minutes, $link )
);
}
}
login_header( 'Email me a login link' );
if ( $sent ) {
echo '<p class="message">If that account exists, a login link is on its way. Check your email.</p>';
} else {
?>
<form method="post" action="<?php echo esc_url( add_query_arg( 'action', 'wpide_magic', wp_login_url() ) ); ?>">
<p><label for="wpide_magic_login">Username or email address</label>
<input type="text" name="wpide_magic_login" id="wpide_magic_login" class="input" autocomplete="username" required></p>
<?php wp_nonce_field( 'wpide_magic', 'wpide_magic_nonce' ); ?>
<p class="submit"><button type="submit" class="button button-primary button-large">Send login link</button></p>
</form>
<?php
}
echo '<p id="nav"><a href="' . esc_url( wp_login_url() ) . '">Log in with a password</a></p>';
login_footer();
exit;
} );
add_action( 'login_form_wpide_magic_login', function () {
$user_id = isset( $_GET['uid'] ) ? absint( $_GET['uid'] ) : 0;
$token = isset( $_GET['token'] ) ? sanitize_text_field( wp_unslash( $_GET['token'] ) ) : '';
$data = $user_id ? get_user_meta( $user_id, 'wpide_magic_token', true ) : '';
if ( ! $token || ! is_array( $data ) || time() > (int) $data['expires'] || ! hash_equals( $data['hash'], wp_hash( $token ) ) ) {
wp_safe_redirect( add_query_arg( 'wpide_magic', 'invalid', wp_login_url() ) );
exit;
}
delete_user_meta( $user_id, 'wpide_magic_token' );
wp_set_current_user( $user_id );
wp_set_auth_cookie( $user_id );
do_action( 'wp_login', get_userdata( $user_id )->user_login, get_userdata( $user_id ) );
wp_safe_redirect( apply_filters( 'login_redirect', admin_url(), '', get_userdata( $user_id ) ) );
exit;
} );
add_filter( 'login_message', function ( $message ) {
if ( isset( $_GET['wpide_magic'] ) ) {
$message .= '<p class="message">That login link is invalid or has expired. Please ask for a new one.</p>';
}
return $message;
} );
Notes
Links work once and expire after 15 minutes. Roles in $wpide_magic_excluded_roles (administrators by default) must still use their password.
How to add this snippet
- Install WPIDE on your WordPress site (it is free) and open Code Snippets.
- Use snippet above adds it from the library in one click. Or copy the code into a new snippet with the same code type.
- It is added inactive: review the code and settings, then switch it on.