WordPress Passwordless Login

Cristian Antohe
Last Updated: 01/04/19

logo_150_150_03

WordPress Passwordless Login is a plugin that allows your users to login without a password. It’s as simple as installing it and adding a shortcode in a page or widget.

Download Passwordless Login from WordPress.org

These past months have been filled with security reports, articles and 0-day exploits. It’s fair to say they had little to do with WordPress, but that’s besides the point. What’s certain is that we’re now living in an increasingly technologically complex world and it’s getting harder and harder to keep everything safe and secure.

This article draws it’s inspiration from the Passwordless authentication: Secure, simple, and fast to deploy article. It explains how to get passwordless login for node.js but more importantly, why you should want to have a passwordless login.

Username + passwords will probably not be replaced anytime soon. But that doesn’t mean we can’t come up with alternatives.

Everybody has a “friend” that uses the same password or some variation: the pass with numbers in it, the pass with capital letters, the pass with special characters in it, the pass with the year at the end in it, etc.

Lately there have been great solutions involving one time passwords like Persona from Mozilla. Whenever a user wants to log in, they receive a short-lived, one-time link with a token via email or text message.

There is also Clef that uses a QR code and also supports WordPress via a plugin

But all these are third party so let’s look into building something native to WordPress.

Building a Passwordless Login Plugin

How it works

  • Instead of asking users for a password when they try to log in to your website, we simply ask them for their username or email
  • The plugin creates a temporary authorization token and saves it in a WordPress user meta as well as an expiration time for 10 minutes
  • Then we send the user an email with a link and the token
  • The user clicks the link and sends the authorization code to your server
  • The plugin then checks if the code is valid and creates the log in WordPress cookie, successfully authenticating the user.

passwordless login

passwordless login shortcode

passwordless login widget

passwordless login check email

passwordless login email

passwordless login success

Let’s get codding

You can find the entire plugin over at Bitbucket It will also be available for download from WordPress.org, but we’re still waiting for it to be approved.

Download

The plugin structure is composed out of a few main functions:

  1. wpa_front_end_login() – the shortcode form
  2. wpa_send_link() – the function that emails the user the proper login link
  3. wpa_autologin_via_url() – this is the main function that authenticates the user if the token is correct
  4. wpa_valid_account() – a simple function that checks if an account is valid
  5. wpa_generate_url() – generates the url the user receives
  6. wpa_create_onetime_token() – basically a hash based on an action name and the current time.

Now let’s have a quick look over some of the functions!

wpa_front_end_login()

This is what the user sees. We have a simple form that allows the user to enter it’s email address or username. Once they hit “Login” the form information will be processed by wpa_send_link().

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
 * Shortcode for the passwordless login form
 *
 * @since v.1.0
 *
 * @return html
 */
function wpa_front_end_login(){
	ob_start();
	$account = ( isset( $_POST['user_email_username']) ) ? $account = sanitize_text_field( $_POST['user_email_username'] ) : false;
	$nonce = ( isset( $_POST['nonce']) ) ? $nonce = sanitize_key( $_POST['nonce'] ) : false;
	$error_token = ( isset( $_GET['wpa_error_token']) ) ? $error_token = sanitize_key( $_GET['wpa_error_token'] ) : false;
 
	$sent_link = wpa_send_link($account, $nonce);
 
	if( $account && !is_wp_error($sent_link) ){
		echo '<p class="wpa-box wpa-success">'. apply_filters('wpa_success_link_msg', __('Please check your email. You will soon receive an email with a login link.', 'passwordless') ) .'</p>';
	} elseif ( is_user_logged_in() ) {
		$current_user = wp_get_current_user();
		echo '<p class="wpa-box wpa-alert">'.apply_filters('wpa_success_login_msg', sprintf(__( 'You are currently logged in as %1$s. %2$s', 'profilebuilder' ), '<a href="'.$authorPostsUrl = get_author_posts_url( $current_user->ID ).'" title="'.$current_user->display_name.'">'.$current_user->display_name.'</a>', '<a href="'.wp_logout_url( $redirectTo = wpa_curpageurl() ).'" title="'.__( 'Log out of this account', 'passwordless' ).'">'. __( 'Log out', 'passwordless').' &raquo;</a>' ) ) . '</p><!-- .alert-->';
	} else {
		if ( is_wp_error($sent_link) ){
			echo '<p class="wpa-box wpa-error">' . apply_filters( 'wpa_error', $sent_link->get_error_message() ) . '</p>';
		}
		if( $error_token ) {
			echo '<p class="wpa-box wpa-error">' . apply_filters( 'wpa_invalid_token_error', __('Your token has probably expired. Please try again.', 'passwordless') ) . '</p>';
		}
		?>
	<form name="wpaloginform" id="wpaloginform" action="" method="post">
		<p>
			<label for="user_email_username"><?php _e('Login with email or username') ?></label>
			<input type="text" name="user_email_username" id="user_email_username" class="input" value="<?php echo esc_attr( $account ); ?>" size="25" />
			<input type="submit" name="wpa-submit" id="wpa-submit" class="button-primary" value="<?php esc_attr_e('Log In'); ?>" />
		</p>
		<?php do_action('wpa_login_form'); ?>
		<?php wp_nonce_field( 'wpa_passwordless_login_request', 'nonce', false ) ?>
 
	</form>
<?php
	}
 
	$output = ob_get_contents();
	ob_end_clean();
	return $output;
}
add_shortcode( 'passwordless-login', 'wpa_front_end_login' );

wpa_send_link()

Once the form is submitted, this function will receive the account name. If we have a valid username or email, we’ll simply send them an email with the URL. The url is generated using wpa_generate_url() function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
 * Sends an email with the unique login link.
 *
 * @since v.1.0
 *
 * @return bool / WP_Error
 */
function wpa_send_link( $email_account = false, $nonce = false ){
	if ( $email_account  == false ){
		return false;
	}
	$valid_email = wpa_valid_account( $email_account  );
	$errors = new WP_Error;
	if (is_wp_error($valid_email)){
		$errors->add('invalid_account', $valid_email->get_error_message());
	} else{
		$blog_name = get_bloginfo( 'name' );
		$unique_url = wpa_generate_url( $valid_email , $nonce );
		$subject = apply_filters('wpa_email_subject', __("Login at $blog_name"));
		$message = apply_filters('wpa_email_message', __("Login at $blog_name by visiting this url: $unique_url"), $unique_url);
		$sent_mail = wp_mail( $valid_email, $subject, $message);
 
		if ( !$sent_mail ){
			$errors->add('email_not_sent', __('There was a problem sending your email. Please try again or contact an admin.'));
		}
	}
	$error_codes = $errors->get_error_codes();
 
	if (empty( $error_codes  )){
		return false;
	}else{
		return $errors;
	}
}

wpa_create_onetime_token()

This is the function that generates our transient based on the user ID and the current time. It’s using wp_hash() so the token is salted.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
 * Create a nonce like token that you only use once based on transients
 *
 *
 * @since v.1.0
 *
 * @return string
 */
function wpa_create_onetime_token( $action = -1, $user_id = 0 ) {
	$time = time();
 
	// random salt
	$key = wp_generate_password( 20, false );
 
	require_once( ABSPATH . 'wp-includes/class-phpass.php');
	$wp_hasher = new PasswordHash(8, TRUE);
	$string = $key . $action . $time;
 
	// we're sending this to the user
	$token  = wp_hash( $string );
	$expiration = $time + 60*10;
	$expiration_action = $action . '_expiration';
 
	// we're storing a combination of token and expiration
	$stored_hash = $wp_hasher->HashPassword( $token . $expiration );
 
	update_user_meta( $user_id, $action , $stored_hash ); // adjust the lifetime of the token. Currently 10 min.
	update_user_meta( $user_id, $expiration_action , $expiration );
	return $token;
}

wpa_autologin_via_url()

This is the actual function that will login the username. It expects the user ID, token and a nonce (to verify intent? could be redundant).

We’re checking the transient in the database. If it matches, then we set the auth cookie and delete the current token (so it can only be used once)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
 * Automatically logs in a user with the correct nonce
 *
 * @since v.1.0
 *
 * @return string
 */
add_action( 'init', 'wpa_autologin_via_url' );
function wpa_autologin_via_url(){
	if( isset( $_GET['token'] ) && isset( $_GET['uid'] ) && isset( $_GET['nonce'] ) ){
		$uid = sanitize_key( $_GET['uid'] );
		$token  =  sanitize_key( $_REQUEST['token'] );
		$nonce  = sanitize_key( $_REQUEST['nonce'] );
 
		$hash_meta = get_user_meta( $uid, 'wpa_' . $uid, true);
		$hash_meta_expiration = get_user_meta( $uid, 'wpa_' . $uid . '_expiration', true);
		$arr_params = array( 'uid', 'token', 'nonce' );
		$current_page_url = remove_query_arg( $arr_params, wpa_curpageurl() );
 
		require_once( ABSPATH . 'wp-includes/class-phpass.php');
		$wp_hasher = new PasswordHash(8, TRUE);
		$time = time();
 
		if ( ! $wp_hasher->CheckPassword($token . $hash_meta_expiration, $hash_meta) || $hash_meta_expiration < $time || ! wp_verify_nonce( $nonce, 'wpa_passwordless_login_request' ) ){
			wp_redirect( $current_page_url . '?wpa_error_token=true' );
			exit;
		} else {
			wp_set_auth_cookie( $uid );
			delete_user_meta($uid, 'wpa_' . $uid );
			delete_user_meta($uid, 'wpa_' . $uid . '_expiration');
 
			$total_logins = get_option( 'wpa_total_logins', 0);
			update_option( 'wpa_total_logins', $total_logins + 1);
			wp_redirect( $current_page_url );
			exit;
		}
	}
}

Conclusions

I personally like this approach. It takes away the pain point of having to remember yet another password (and no, normal users don’t use password managers).

It also allows you to force stronger and longer passwords on users, particularly if they don’t have to remember them. (in case you want both login types to work in parallel)

There are also some fallbacks:

  • the default login is still in place (can be fixed with a redirect of wp-login.php)
  • by default the expiration time of the auth cookie is 1 day or until you close the browser (but it can be modified to be long lived)
  • the security of the account is still based on the security of your email

I guess the main word describing WordPress passwordless authentication is potential. It has the potential of improving the security by taking passwords out of the picture entirely. And that is something I would really like to see one day!

52 thoughts on “WordPress Passwordless Login

    But sending a one-time access code via email relies on the assumption that your email account hasn’t been compromised. Sending it to a mobile phone relies on the assumption that your mobile hasn’t ended up in the wrong hands. I can’t see much improvement in either scenario.

    Reply

    If you’re using a password and your email is compromised then the attacker can simply reset the password and access your account.

    What this system helps with is not having to remember a new password or worse, reusing an existing one. It protects you against weak passwords and dictionary attacks.

    As for mobile, same thing. Usually on your mobile is always logged-in your email. So you can reset your password on any account you might have. That being said, that would imply a rather personal touch, something that a determined hacker wouldn’t have a problem doing.

    Reply

    I’ve thought about an application for this feature a number of times. I would potentially use that to limit the login session to a list of pages, so a user could respond to and act on a notification or request in an email, but need to fully log in to access the rest of the site.

    Reply

    If i understood correctly, the process needs to be repeated once the token expires?

    Reply

    Nice Plugin, but not what i need :D.

    Im looking for a kind of this stuff, but – a passwordless login to protected wp-pages or posts (login via provided link with link-expiration).

    Mh, so i need to g**gle further 🙂

    Cheers,
    Orwell

    Reply

    Hey Christian,

    Loving the plugin so far, but I have a question:

    Is it possible to integrate this functionality into other sections of a WP site? I have a specific situation where I’d like newly approved users to be sent a one-time auth link in their welcome email.

    I’ve tried using the wpa_create_onetime_token() function to help generate a URL, but when I click it, I’m still directed to the wp-login page.

    It seems like I’m so close to having a working solution, but I don’t want to waste hours trying to resolve this issue if it’s not possible!

    Thanks again,

    Pete

    Reply

    Hi Christian,

    Any news on an updated version (or updated compatibility note at wordpress.org)?

    Reply

    This plugin is awesome! Thank you so very much for making it!

    It is a perfect compliment to my high security website that often locks people out because they retry the wrong password too many times.

    Reply

    After fill out and confirm email form, If email address does not exist among user accounts, Is it possible to create automatically the new user and send login token to email? This would integrate registration and login process and that is what I need 🙂

    Reply

    This is not a feature of the plugin. We’re only dealing with loggin in the user.

    Reply

    I installed the passwordless-login plugin and it went into the sidebar just like it should. My problem is once a user logs in it comes up with the “check your email…” with a yellow background that makes it difficult to read the message in the box. The same with the next message regarding “you are logged in as…..” can’t read it. Is there a way I can change the colors so I can read it better.

    Thanks!

    Reply

    Hi Sherry,

    You’ll have to add a bit of css to your theme’s style.css file at the end of it:

    .wpa-error{
    color: #111;
    }

    Reply

    Thanks Christian! I’m new at this! Can you tell me where I find the theme’s style.css file?

    Reply

    Thanks so much for the detailed explanation and documentation of the custom functions. Your quality documentation is why I bought Profile Builder Pro, and why I’m starting to use several of your plugins!

    Reply

    Hi

    Thanks for the plugin – great idea.

    I would like to modify it so that the link only expires after a certain time, but can be used as many times as necessary within that timeframe.

    How can this be done?

    Thanks again!

    Reply

    Hi Claudia,

    I don’t recommend you do this, however by deleting these two lines, it should work:

    
    			delete_user_meta($uid, 'wpa_' . $uid );
    			delete_user_meta($uid, 'wpa_' . $uid . '_expiration');
    

    Please note I haven’t tested, so make sure you understand what you’re doing and why. Messing up with the login system can leave your site open for hacking.

    Reply

    I am getting same email 3 times, what is wrong?

    Reply

    Hi Arun,

    That sounds like a conflict with another plugin or your current theme. Can you please try to deactivate all other plugins and with a default theme try again?

    Also, do you happen to multiple passwordless login forms on the same page (like in the content and in the sidebar)?

    Reply

    I was wondering if you could add a feature to this plugin to create a permanent link to be shared? This would allow for my subscribers to share their login with viewers and would not require a password. If a subscriber is deactivated the link would no longer function.

    Thanks for producing some great plugins.

    Reply

    After a users enter their email address to login they receive the email with a login link. They click the link and are redirected to the website as a logged in user.

    Is it possible that for future visits they only have to enter their email to identify themselves, and the browser will use the cookie set from the first login session to remember the user?

    I love the idea of this plugin, but I would like it if the user only had to click the link in the email one time – when they log in for the first time. After that the cookie remembers them and they only need their email address as their username.

    In the code, the cookie duration can be set so this method of login could last for 90, 120, 365 days, etc…. it would really facilitate things for the users.

    Is this possible on a technical level? I am not a programmer and am simply trying to understand what is possible and what is not based on the current ways cookies are set and used.

    I would love to hear your ideas on this. Thank you!

    Reply

    @Cristian Antohe I’m very curious if this is possible, I’m interested in the same thing actually.

    I want user to be logged in once for the site is an intranet site (and is only for internal use).

    what piece of code must i recode for this?

    Love to hear from you as well.

    Reply

    Hi,

    Thanks! This is CLOSE to what I am looking for and it seems that a few simple mods might get me there, but I don’t want to mess with the code.

    I am interested in a passwordless login for specific user roles only (i.e. customer). I don’t need email authorization. I imagine you simply set the return transient check to “true”, but what about the specific user roles? I don’t want to create a passwordless admin.

    Reply

    I would like the email link not to be sent to a users email address but rather to extract it from the user meta by means of an api call. that will be very helpful to integrate a wordpress based webapp into a non-wp based webapp.

    e.g. sending an authenticated get request to domain.com/api/templogins/?username=xyz. Is this possible and if yes then would you mind elaborating in how I could mod your plugin to achieve this?

    Reply

    That should be easily done by simply replacing the function that sends an email with one that stores info in a user meta.

    However I think you should look into using a standard way of cross app login like oAuth.

    Also, if your app is on the same domain, just use cookie based auth for both, with WordPress as the single point of truth. If the logged in cookie is present, do stuff for logged in users.

    Reply

    Hi,
    thank you for your great plugin!

    I am wondering, what would happen if I removed the .$_SERVER[“SERVER_PORT”] part from the pageURL? I am on a https site, so 433 is added as a port. But the link also works if I remove the part from the link manually. Or could I change the code to if ($_SERVER[“SERVER_PORT”] != “433”)?
    One of my clients was really confused about that part in the link because it “looked untrustworthy”.
    Is there anything I can break by modifying the code in that way?

    Thank you and best,
    Simon

    Reply

    Hello,

    It should be ok.

    You could modify and do something like:
    if(isset($_SERVER[“HTTPS”]) && $_SERVER[“SERVER_PORT”] != “433”) {
    //add port
    }

    I don’t think something could break. But make sure to test this out, the plugin doesn’t provide much functionality which could break, so if the login works properly, you’re good to go.

    Regards.

    Reply

    Hello

    would it be possible to redirect users to the homepage (or to a certain page) after successful login? Could you give me a hint on how that code would look like?

    Thanks!

    Reply

    Hello Mike,

    Take a look at this code:
    function wppbc_pwless_redirect() {
    if ( !is_front_page() && !is_user_logged_in() ) {
    $url = “http://” . $_SERVER[‘HTTP_HOST’] . $_SERVER[‘REQUEST_URI’];

    wp_redirect( ‘http://mywebsite.com/?referer=’ . $url, 301 );
    die(”);
    }

    if ( is_front_page() && is_user_logged_in() ) {
    $referer = $_GET[‘referer’];

    wp_redirect( $referer, 301 );
    die(”);
    }
    }

    add_action( ‘template_redirect’, ‘wppbc_pwless_redirect’ );

    This will redirect any user which tries to access the website to the front-page, which happens to be the login page in this case. If the user did not access the main URL, but went to some other page like `/articles/`, he will get redirected back to that page after logging in.

    This should help you achieve what you want, let me know if you need more help !

    Regards.

    Reply

    Hello, Georgian. This ‘redirect to the calling page’ addition is much needed. I’m getting an error, however; I think I need to escape the colons in the http:// text strings—which I can’t get to work. Is there a simple fix for those two lines? Thanks.

    Reply

    Hey Paul,

    Can you show me exactly the error that you are getting ? There shouldn’t be a need to escape the two urls.

    This fixes it.

    function wppbc_pwless_redirect() {
    if ( !is_front_page() && !is_user_logged_in() ) {
    $url = “http://” . $_SERVER[‘HTTP_HOST’] . $_SERVER[‘REQUEST_URI’];

    wp_redirect( ‘http://mywebsite.com/?referer=’ . $url, 301 );
    //die(”);
    }

    if ( is_front_page() && is_user_logged_in() ) {
    $referer = $_GET[‘referer’];

    wp_redirect( $referer, 301 );
    //die(”);
    }
    }

    add_action( ‘template_redirect’, ‘wppbc_pwless_redirect’ );

    Is there any way this can be added as an option to the wp-login page?

    Reply

    Hello Andrew,

    This option is not available at the moment, you will need to setup the shortcode on an individual page.

    Regards.

    Reply

    I can’t get this plugin to work at all. When I click the link in the email, it simply returns me to the same page with my shortcode for [passwordless-login] and I’m not logged in … so just going round in circles.
    Any sugegstions?

    Reply

    Could you check if you have caching enable? If so, disable it for that page alone. Or just disable it for testing purposes and try again.

    Reply

    Hi! I have white screen. Conflict with plugin that use session_start()
    PHP Warning: session_start(): Cannot send session cookie – headers already sent by (output started at /wp-content/plugins/passwordless-login/passwordless_login.php:1) in /wp-content/pluginst/controller/ajax_controller.php on line 12

    On line 12:
    if( !session_id()) {
    session_start();
    }

    Reply

    Hi Alex, do you know what particular plugin causes this conflict? Thank you!

    Reply

    Hey there, would it be possible to also register new users with the same workflow? If so, how? Thanks in advance and awesome plugin!

    Reply

    Hello.

    Using Profile Builder it is possible to create a very stripped down registration form. You could ask the user only for his email and password and register him.

    If you also want to remove the password this will be possible with the next version of the Toolbox add-on: https://www.cozmoslabs.com/add-ons/customization-toolbox/

    Regards.

    Reply

    Hi – this works super! Are you still maintaining it, as doesn’t appear on your website?

    An extra feature I’d like is an auto-redirect on login, e.g. when the user click on the email, not to go to the same page but to the shop (in this case, the shop is for professional users only, and users must be logged in to see prices).

    Reply

    Hi, bug fixes and security issue will continue to be taken care of, but no new features will be developed for it.

    I’ve come to the conclusion that a much better solution is to use a password manager and not place the entire security of your accounts to the user’s email address.

    Reply

    Hey Christian, Thanks for you and the team for this awesome plugin.

    Can you explain why you reached this conclusion? To use a password manager instead of the user email.

    I’m wondering if I can make a pull request to enable an option to force users only logins with email instead of username or email.

    Thanks

    Reply

    Basically you’re placing the security of your user in the hands of their email provider. It just shifts the responsibility.
    There’s no way to enforce a stronger password if needed.

    It doesn’t particularly increase the security. Where as a password manager at the very least promotes better and unique passwords.

    Plus, the login flow while it sounds simpler at first glance, it’s not simpler then a password manager, where I just arrive at a login screen and my user / password is already filled.

    As for the pull request, if it’s an option (non-default), I’ll approve it.
    https://bitbucket.org/cozmoslabs/passwordless-auth/

    The plugin works most of the time, but not always. I’m constantly having to send people their Username and Password. Is there a way to make this more reliable?

    Reply

    How can I customize the email design that’s being sent out??

    Reply

    Hello Cristian.

    I am not a programmer, but my programmer and I are thinking about using your code.

    I’m a dummy when it comes to things like this ha ha so I am trying to picture how this would look to my future subscribers to my paid-newsletter.

    So, when they subscribe, they get an e-mail with a link in it. Then they click on it to finalize the sign-up process, and then there is nothing else for them to do, until that token runs outs? They can get my newsletters sent by Mailchimp???
    That’s it???

    Thanks in advance, sir.

    ~ Perry

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.