WordPress Passwordless Login

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, an article published something like two weeks ago. It explains how to get passwordless login for node.js but more importantly, why you want 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!

Subscribe to get early access

to new plugins, discounts and brief updates about what's new with Cozmoslabs!

38 thoughts on “WordPress Passwordless Login

  1. 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.

    1. 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.

  2. 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.

  3. 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

  4. 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

  5. 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.

  6. 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 🙂

  7. 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!

  8. 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!

  9. 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!

    1. 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.

    1. 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)?

  10. Hi,

    I really like this plugin a lot. I am wondering about one thing: Is there any chance to gather and store entered users’ e-mails (due to send them a newsletter for example)?

    Thank you…

  11. 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.

  12. 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!

    1. @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.

  13. 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.

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

    1. 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.

  15. 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

    1. 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.

  16. 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!

    1. 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.

      1. 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.

        1. Hey Paul,

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

    1. Hello Andrew,

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

      Regards.

Leave a Reply

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