Bookmark

How to: Create a custom ‘Forgot Password’ page in WordPress with Magic Links and wp_mail()

Mar 28, 2022  •  @themolitor  •  Tips & Tricks

Forgetting your password is inevitable. The more profiles we create online, the more chances we have of getting our logins confused. Having an easy-to-use method for users to regain access to their account is an important part of running a successful web-based service.

In this how-to we’re going to implement a system that (1) requests the user’s email address, (2) creates a unique “magic link”, (3) sends the user an email and (4) automatically logs the user into their account when they visit the magic link.

Request an email address

To request the user’s email address we’re going to use the page templates feature of WordPress to create a page that contains a simple form.

Create a page template

In your active theme’s directory, create a file called forgot-password.php and add the following to the page…

<?php
/*
Template Name: Forgot Password
*/

…After saving, you’ll be able to create a new page from your WordPress dashboard and assign the Forgot Password page template option.

Redirect logged in users

Next we’ll want to make sure that only users who are NOT logged in can interact with the form we’re going to build. We’ll use is_user_logged_in() to send logged in users to another page via wp_redirect()

<?php
/*
Template Name: Forgot Password
*/

//ALREADY LOGGED IN?
if(is_user_logged_in()){ 
	
    //REDIRECT
    wp_redirect( esc_url( home_url() ) );
    exit;
}

…Now that we know the current user is not logged in, we don’t have to worry about any accidental form submissions.

Basic page setup

In addition to get_header() and get_footer(), we want to setup variables we’ll be using throughout the page (including the form itself) as well as the main container for the content of the page — via the #page-content div…

<?php
/*
Template Name: Forgot Password
*/

//ALREADY LOGGED IN?
if(is_user_logged_in()){ 
	
    //REDIRECT
    wp_redirect( esc_url( home_url() ) );
    exit;
}

get_header();

//VAR SETUP
$user_login = isset( $_POST['user_login'] ) ? $_POST['user_login'] : '';
$form_output = '<form method="post" action="'.get_permalink().'">'.
        '<input type="text" name="user_login" id="user_login" value="'.$user_login.'" placeholder="[email protected]" />'.
        '<input type="hidden" name="action" value="reset" />'.
        '<input type="submit" value="→" class="button" id="submit" />'.
    '</form>';

//PAGE CONTENT START
echo '<div id="page-content">';

    //CHECK IF MAGIC LINK EXPIRED...
    if (isset($_GET['ref']) && "magicfail" == $_GET['ref']) { 

        //EXPIRED MESSAGE OUTPUT
        echo '<label for="user_login"><h3>Link Expired</h3><p>Request a new link below...</p></label>'.$form_output;

    //CHECK IF FORM SUBMITTED (action == reset)
    } else if( isset( $_POST['action'] ) && 'reset' == $_POST['action'] ) {

        // ...future code placeholder...

    //DEFAULT OUTPUT... 
    } else {
    
        echo '<label for="user_login">'.
            '<h3>Forgot password?</h3>'.
            '<p>We\'ll email a magic link to login...</p>'.
        '</label>'.$form_output;

    }

echo '</div>';

get_footer();

…The $user_login variable is where we’ll be storing the email address when our form is submitted. $form_output is essentially just a basic HTML form stored as a variable. We’re putting this into a PHP variable to enable us to echo the form at various parts of the process without having to make sure any edits we make to the form are applied throughout the page template.

After we open the #page-content section, there are a couple if checks before the “DEFAULT OUTPUT…” is echoed: (1) if the magic link expired — more on this later — and (2) if the form has been submitted, which we’ll cover next.

Validate form submissions

When the form is submitted, there are a few things we’ll want to check for before we send an email: (1) the email field is not empty, (2) the email is actually an email using is_email(), and (3) the email represents an actual user in our system via email_exists().

Let’s update the “…future code placeholder…” from the code example above…

//VAR SETUP...
$user_magic_email = trim($user_login);
		 
//EMAIL EMPTY ERROR
if( empty( $user_magic_email ) ) {
			 
    $error_message = 'Please enter an email address to reset your password...';
		 
//INVALID EMAIL ERROR
} else if( !is_email( $user_magic_email )) {
			 
    $error_message = 'Invalid e-mail address.';
		 
//NO USER FOUND
} else if( !email_exists( $user_magic_email ) ) {
			 
    $error_message = 'That email address is not in our system.';
		 
//NO ERRORS...
} else {

    $success_message = '<h3>Check your inbox!</h3><p>You should receive an email from us soon...</p>';
 
    /* ...another future code placeholder... */ 

} 

//ERROR MESSAGE OUTPUT 
if( isset( $error_message ) ) { 

    echo '<label for="user_login"> '. $error_message .'</label>'.$form_output; 

//SUCCESS MESSAGE OUTPUT 
} else if( isset( $success_message ) ) { 

    echo '<label for="user_login">'. $success_message .'</label>'; 

} 

…If there are any errors at the end of our if checks, we’ll set an $error_message variable for output inside the <label> tag. If there are “NO ERRORS…“, we can set a $success_message variable and move on to creating a magic link.

Create a “Magic Link”

The process of creating a magic link essentially boils down to 4 steps: (1) generate a unique value via wp_generate_password(), (2) get the user’s ID from their email address with get_user_by(), (3) updating their profile with the generated value using update_user_meta(), and (4) combine elements to create a unique URL with home_url().

Like before, let’s update the “…another future code placeholder…” from the code example above…

//CREATE MAGIC_LINK_ID
$magic_link_id = wp_generate_password(20);

//GET USER ID 
$user = get_user_by( 'email', $user_magic_email );
$user_id = $user->ID;

//ATTACH MAGIC_LINK_ID TO USER_META
update_user_meta($user_id, 'magic_link_id', $magic_link_id);

//CREATE A MAGIC_LINK_URL
$magic_link_url = home_url() . '/?magic=' . $magic_link_id . '&id=' . $user_id;

…Notice inside the wp_generate_password() function we’re using “20” to set how many characters we want the $magic_link_id variable to contain. There are additional options with this function that also allow the use of extra special characters for added control.

The main item we’ll be using next is the $magic_link_url variable, which is what we’ll send to our user via email.

Sending an email

Since we’re working with WordPress, there’s a built-in mailing function called wp_mail() that we can use with just a few simple lines…

$to = '[email protected]';
$subject = 'Magic Link Request';
$body = 'Here is your magic link to login:'.$magic_link_url;
$headers = array('Content-Type: text/html; charset=UTF-8');

wp_mail( $to, $subject, $body, $headers );

…Notice we’re including $magic_link_url inside the $body variable. This is a very simple example to demonstrate how this function works, but there other settings and options available.

PRO TIP: Another option to send emails is with a service called Postmark. Postmark uses their servers to send emails in a responsible manner that greatly reduces the risk of being flagged as spam. For details, check out the Postmark API documentation.

Final forgot-password.php template

If we add everything together, this is what our final forgot-password.php template file will look like…

<?php
/*
Template Name: Forgot Password
*/

//ALREADY LOGGED IN?
if(is_user_logged_in()){ 
	
    //REDIRECT
    wp_redirect( esc_url( home_url() ) );
    exit;
}

get_header();

//VAR SETUP
$user_login = isset( $_POST['user_login'] ) ? $_POST['user_login'] : '';
$form_output = '<form method="post" action="'.get_permalink().'">'.
        '<input type="text" name="user_login" id="user_login" value="'.$user_login.'" placeholder="[email protected]" />'.
        '<input type="hidden" name="action" value="reset" />'.
        '<input type="submit" value="→" class="button" id="submit" />'.
    '</form>';

//PAGE CONTENT START
echo '<div id="page-content">';

    //CHECK IF MAGIC LINK EXPIRED...
    if (isset($_GET['ref']) && "magicfail" == $_GET['ref']) { 

        //EXPIRED MESSAGE OUTPUT
        echo '<label for="user_login"><h3>Link Expired</h3><p>Request a new link below...</p></label>'.$form_output;

    //CHECK IF FORM SUBMITTED (action == reset)
    } else if( isset( $_POST['action'] ) && 'reset' == $_POST['action'] ) {

        //VAR SETUP...
        $user_magic_email = trim($user_login);
		 
		//EMAIL EMPTY ERROR
		if( empty( $user_magic_email ) ) {
			 
            $error_message = 'Please enter an email address to reset your password...';
		 
        //INVALID EMAIL ERROR
        } else if( !is_email( $user_magic_email )) {
			 
            $error_message = 'Invalid e-mail address.';
		 
        //NO USER FOUND
        } else if( !email_exists( $user_magic_email ) ) {
			 
            $error_message = 'That email address is not in our system.';
		 
        //NO ERRORS...
        } else {

            $success_message = '<h3>Check your inbox!</h3><p>You should receive an email from us soon...</p>';
 
	    //CREATE MAGIC_LINK_ID
	    $magic_link_id = wp_generate_password(20);
			
	    //GET USER ID 
	    $user = get_user_by( 'email', $user_magic_email );
	    $user_id = $user->ID;
			
	    //ATTACH MAGIC_LINK_ID TO USER_META
	    update_user_meta($user_id, 'magic_link_id', $magic_link_id);
			
	    //CREATE A MAGIC_LINK_URL
	    $magic_link_url = home_url() . '/?magic=' . $magic_link_id . '&id=' . $user_id;
			
	    $to = '[email protected]';
	    $subject = 'Magic Link Request';
	    $body = 'Here is your magic link to login:'.$magic_link_url;
	    $headers = array('Content-Type: text/html; charset=UTF-8');
			
	    wp_mail( $to, $subject, $body, $headers );

        } 

        //ERROR MESSAGE OUTPUT 
        if( isset( $error_message ) ) { 

            echo '<label for="user_login"> '. $error_message .'</label>'.$form_output; 

        //SUCCESS MESSAGE OUTPUT 
        } else if( isset( $success_message ) ) { 

            echo '<label for="user_login">'. $success_message .'</label>'; 

        }

    //DEFAULT OUTPUT... 
    } else {
    
        echo '<label for="user_login">'.
            '<h3>Forgot password?</h3>'.
            '<p>We\'ll email a magic link to login...</p>'.
        '</label>'.$form_output;

    }

echo '</div>';

get_footer();

Once the email has been sent, all we need to do next is write a function that will sign in the user when they visit the magic link.

Sign in with magic links

In order for the magic link to work, we need to (1) create a function that runs before the page content loads, (2) listens for the unique parameters we attached to the magic link URL, (3) checks the unique id in the URL with the unique id attached to the user, and (4) sign in the user if everything matches.

Create a function that runs before the page loads

We’ll start by adding the following to the functions.php file of the theme you’re working with — if you don’t have this file, create one…

function magic_link_sign_in() {

    /* ...future code placeholder... */
}

add_action('init', 'magic_link_sign_in');

…here we’re creating a function called magic_link_sign_in() and using add_action() to add our function to the 'init' action hook. This is a WordPress hook that fires after some core WordPress functions are loaded, but before the actual page is rendered, making it a good time to run this function.

Listen for the Magic ID Parameter

The URL we created and sent in the email uses the “magic” and “id” parameters, so that’s what we’re going to listen for using isset() and $_GET[]

function magic_link_sign_in() {

    //CHECK IF THE URL PARAMETERS ARE PRESENT
    if (isset($_GET['magic']) && isset($_GET['id'])) {

        /* ...future code placeholder... */
    }
}

add_action('init', 'magic_link_sign_in');

…once we’re inside this if statement, we know there’s a magic id and user id that we can use to verify the request to login.

Compare IDs to Verify the Request

In order to confirm the request to login, we need to check that the unique magic link in the URL matches the unique magic link we assigned to the user in earlier steps.

function magic_link_sign_in() {

    //CHECK IF THE URL PARAMETERS ARE PRESENT
    if (isset($_GET['magic']) && isset($_GET['id'])) {

        $magic_link_id = $_GET['magic'];
        $user_id = $_GET['id'];
        $magic_id = get_user_meta($user_id, 'magic_link_id', true);

        //IF MAGIC LINK IDs MATCH...
        if ($magic_link_id == $magic_id) {

            /* ...future code placeholder... */

        //MAGIC LINK EXPIRED...
        } else {

            //REDIRECT
            wp_redirect(home_url('/forgot-password/?ref=magicfail') );
            exit;
        }
    }
}

add_action('init', 'magic_link_sign_in');

…Using the id from the $_GET[] parameter, we can use get_user_meta() to get the 'magic_link_id' and see if they match via ==.

If the IDs don’t match, we’re going to redirect the user to the forgot-password page and attach the ?ref=magicfail so we can display the //EXPIRED MESSAGE OUTPUT from previous steps.

Sign In the User

Now that we know the magic id in the URL matches the magic id attached to the user, we can sign them into their account. Replace the “…future code placeholder…” from the previous step with the following…

//SIGN NEW USER IN
$user = get_user_by('id', $user_id); 

if( $user ) {
    wp_set_current_user( $user_id, $user->user_login );
    wp_set_auth_cookie( $user_id );
    do_action( 'wp_login', $user->user_login, $user );
}

//REDIRECT
wp_redirect( home_url('/?ref=magic-link') );
exit;

…First we’re getting the user object with get_user_by(). The user object gives us what we need to use some very handy functions for signing in a user to a WordPress site:

Finally, once the user is logged in we can redirect them to another page. In this example we’re sending them to the home page with ?ref=magic-link added so that we know how they got there and have the option of displaying a “Welcome back” message.

Final magic_link_sign_in() function

After we add the above steps together, our final magic_link_sign_in() function will look like this…

function magic_link_sign_in() {

    //CHECK IF THE URL PARAMETERS ARE PRESENT
    if (isset($_GET['magic']) && isset($_GET['id'])) {

        $magic_link_id = $_GET['magic'];
        $user_id = $_GET['id'];
        $magic_id = get_user_meta($user_id, 'magic_link_id', true);

        //IF MAGIC LINK IDs MATCH...
        if ($magic_link_id == $magic_id) {

            //SIGN NEW USER IN
            $user = get_user_by('id', $user_id); 

            if( $user ) {
                wp_set_current_user( $user_id, $user->user_login );
                wp_set_auth_cookie( $user_id );
                do_action( 'wp_login', $user->user_login, $user );
            }

            //REDIRECT
            wp_redirect( home_url('/?ref=magic-link') );
            exit;

        //MAGIC LINK EXPIRED...
        } else {

            //REDIRECT
            wp_redirect(home_url('/forgot-password/?ref=magicfail') );
            exit;
        }
    }
}

add_action('init', 'magic_link_sign_in');

That should be everything you need to create a custom “Forgot Password?” page for your WordPress site. If you have any questions or comments, my DMs are open @theMOLITOR

Front-end
HTML
PHP
UX Engineer
WordPress
Sign in
Forgot password?